@@ -328,3 +328,4 @@ ASALocalRun/ | |||||
# MFractors (Xamarin productivity tool) working folder | # MFractors (Xamarin productivity tool) working folder | ||||
.mfractor/ | .mfractor/ | ||||
*.json |
@@ -0,0 +1,5 @@ | |||||
{ | |||||
"presets": [ | |||||
"@vue/app" | |||||
] | |||||
} |
@@ -0,0 +1,9 @@ | |||||
root = true | |||||
[*] | |||||
charset = utf-8 | |||||
indent_style = space | |||||
indent_size = 2 | |||||
end_of_line = lf | |||||
insert_final_newline = true | |||||
trim_trailing_whitespace = true |
@@ -0,0 +1,19 @@ | |||||
module.exports = { | |||||
root: true, | |||||
'extends': [ | |||||
'plugin:vue/essential', | |||||
'@vue/standard' | |||||
], | |||||
rules: { | |||||
// allow async-await | |||||
'generator-star-spacing': 'off', | |||||
// allow debugger during development | |||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', | |||||
'vue/no-parsing-error': [2, { 'x-invalid-end-tag': false }], | |||||
'no-undef': 'off', | |||||
'camelcase': 'off' | |||||
}, | |||||
parserOptions: { | |||||
parser: 'babel-eslint' | |||||
} | |||||
} |
@@ -0,0 +1,26 @@ | |||||
.DS_Store | |||||
node_modules | |||||
/dist | |||||
/tests/e2e/videos/ | |||||
/tests/e2e/screenshots/ | |||||
# local env files | |||||
.env.local | |||||
.env.*.local | |||||
# Log files | |||||
npm-debug.log* | |||||
yarn-debug.log* | |||||
yarn-error.log* | |||||
# Editor directories and files | |||||
.idea | |||||
.vscode | |||||
*.suo | |||||
*.ntvs* | |||||
*.njsproj | |||||
*.sln | |||||
*.sw* | |||||
build/env.js |
@@ -0,0 +1,5 @@ | |||||
module.exports = { | |||||
plugins: { | |||||
autoprefixer: {} | |||||
} | |||||
} |
@@ -0,0 +1,5 @@ | |||||
language: node_js | |||||
node_js: stable | |||||
script: npm run lint | |||||
notifications: | |||||
email: false |
@@ -0,0 +1,21 @@ | |||||
MIT License | |||||
Copyright (c) 2017 iView | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. |
@@ -0,0 +1,187 @@ | |||||
<p align="center"> | |||||
<a href="https://www.iviewui.com"> | |||||
<img width="200" src="https://file.iviewui.com/logo-new.svg"> | |||||
</a> | |||||
</p> | |||||
# iView Admin | |||||
[](https://github.com/iview/iview-admin/releases) | |||||
[](https://travis-ci.org/iview/iview-admin) | |||||
[](https://github.com/vuejs/vue) | |||||
[](https://github.com/iview/iview) | |||||
[]() | |||||
[更新日志](https://github.com/iview/iview-admin/releases) | |||||
[使用文档](https://lison16.github.io/iview-admin-doc/#/) | |||||
[在线访问](https://admin.iviewui.com/) | |||||
[简化版模板](https://github.com/iview/iview-admin/tree/template) | |||||
[教学视频(26课时)](https://segmentfault.com/ls/1650000016221751?utm_source=banner) | |||||
`注:在线版本会在开发版本新小版本发布后更新到相应版本,所以如果想体验最新版本iview-admin,请clone完整项目代码到本地运行。` | |||||
## Install | |||||
```bush | |||||
// install dependencies | |||||
npm install | |||||
``` | |||||
## Run | |||||
### Development | |||||
```bush | |||||
npm run dev | |||||
``` | |||||
### Production(Build) | |||||
```bush | |||||
npm run build | |||||
``` | |||||
## 简介 | |||||
  iView admin是基于Vue.js,搭配使用[iView](https://www.iviewui.com) UI组件库形成的一套后台集成解决方案,由TalkingData前端可视化团队部分成员开发维护。iView admin遵守iView设计和开发约定,风格统一,设计考究,并且更多功能在不停开发中。 | |||||
如果您想查看iview-admin的更新动态,您可以到[更新日志](https://github.com/iview/iview-admin/releases)查看了解最新更新;如果您是新手,想快速入手iview-admin,您可以到[使用教程](https://github.com/iview/iview-admin/wiki)查看讲解;如果您想在线体验iview-admin,您可以到[在线访问](https://admin.iviewui.com/)体验。如果你只是想要一个清醒爽朗的界面,那你可以下载[简化版模板](https://github.com/iview/iview-admin/tree/template)来做开发。 | |||||
## 功能 | |||||
- 登录/登出 | |||||
- 权限管理 | |||||
- 列表过滤 | |||||
- 权限切换 | |||||
- 多语言切换 | |||||
- 组件 | |||||
- 富文本编辑器 | |||||
- Markdown编辑器 | |||||
- 城市级联 | |||||
- 图片预览编辑 | |||||
- 可拖拽列表 | |||||
- 文件上传 | |||||
- 数字渐变 | |||||
- split-pane | |||||
- 表单编辑 | |||||
- 文章发布 | |||||
- 工作流 | |||||
- 表格 | |||||
- 可拖拽排序 | |||||
- 可编辑表格 | |||||
- 行内编辑 | |||||
- 单元格编辑 | |||||
- 可搜索表格 | |||||
- 表格导出数据 | |||||
- 导出为Csv文件 | |||||
- 导出为Xls文件 | |||||
- 表格转图片 | |||||
- 错误页面 | |||||
- 403页面 | |||||
- 404页面 | |||||
- 500页面 | |||||
- 高级路由 | |||||
- 动态路由 | |||||
- 带参页面 | |||||
- 换肤 | |||||
- 收缩侧边栏 | |||||
- tag标签导航 | |||||
- 面包屑导航 | |||||
- 全屏/退出全屏 | |||||
- 锁屏 | |||||
- 消息中心 | |||||
- 个人中心 | |||||
## 文件结构 | |||||
```shell | |||||
. | |||||
├── build 项目构建配置 | |||||
├── config 开发相关配置 | |||||
├── public 打包所需静态资源 | |||||
└── src | |||||
├── api AJAX请求 | |||||
└── assets 项目静态资源 | |||||
├── icons 自定义图标资源 | |||||
└── images 图片资源 | |||||
├── components 业务组件 | |||||
├── config 项目运行配置 | |||||
├── directive 自定义指令 | |||||
├── libs 封装工具函数 | |||||
├── locale 多语言文件 | |||||
├── mock mock模拟数据 | |||||
├── router 路由配置 | |||||
├── store Vuex配置 | |||||
├── view 页面文件 | |||||
└── tests 测试相关 | |||||
``` | |||||
## Links | |||||
- [TalkingData](https://github.com/TalkingData) | |||||
- [iView](https://github.com/iview/iview) | |||||
- [Vue](https://github.com/vuejs/vue) | |||||
- [Webpack](https://github.com/webpack/webpack) | |||||
## 效果展示 | |||||
- 响应式布局首页 | |||||
 | |||||
- 标签导航 | |||||
 | |||||
- 权限管理 | |||||
 | |||||
- 可拖拽列表 | |||||
 | |||||
- 图片预览编辑 | |||||
 | |||||
- 文件上传 | |||||
 | |||||
- 数字渐变 | |||||
 | |||||
- split-pane | |||||
 | |||||
- 文章发布 | |||||
 | |||||
- 工作流 | |||||
 | |||||
- 可拖拽表格 | |||||
 | |||||
- 可编辑表格 | |||||
 | |||||
- 表格导出数据 | |||||
 | |||||
- 表格转图片 | |||||
 | |||||
- 错误页面 | |||||
 | |||||
- 锁屏 | |||||
 | |||||
- 可收缩侧边栏 | |||||
 | |||||
- 主题切换 | |||||
 | |||||
- 消息中心 | |||||
 | |||||
### 💖💖 If you find this project helpful, maybe you can buy me a coffee. 💖💖 | |||||
 | |||||
## License | |||||
[MIT](http://opensource.org/licenses/MIT) | |||||
Copyright (c) 2016-present, iView |
@@ -0,0 +1,17 @@ | |||||
<!DOCTYPE html> | |||||
<html> | |||||
<head> | |||||
<meta charset="utf-8"> | |||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"> | |||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> | |||||
<title></title> | |||||
</head> | |||||
<body> | |||||
<noscript> | |||||
<strong>We're sorry but iview-admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> | |||||
</noscript> | |||||
<div id="app"></div> | |||||
<!-- built files will be auto injected --> | |||||
</body> | |||||
</html> |
@@ -0,0 +1,27 @@ | |||||
<template> | |||||
<div id="app"> | |||||
<router-view/> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'App' | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.size{ | |||||
width: 100%; | |||||
height: 100%; | |||||
} | |||||
html,body{ | |||||
.size; | |||||
overflow: hidden; | |||||
margin: 0; | |||||
padding: 0; | |||||
} | |||||
#app { | |||||
.size; | |||||
} | |||||
</style> |
@@ -0,0 +1,8 @@ | |||||
import axios from '@/libs/api.request' | |||||
export const GetAtomicCounter = () => { | |||||
return axios.request({ | |||||
url: 'AtomicCounter/GetAtomicCounter', | |||||
method: 'get' | |||||
}) | |||||
} |
@@ -0,0 +1,37 @@ | |||||
import axios from '@/libs/api.request' | |||||
export const getTableData = () => { | |||||
return axios.request({ | |||||
url: 'get_table_data', | |||||
method: 'get' | |||||
}) | |||||
} | |||||
export const getDragList = () => { | |||||
return axios.request({ | |||||
url: 'get_drag_list', | |||||
method: 'get' | |||||
}) | |||||
} | |||||
export const errorReq = () => { | |||||
return axios.request({ | |||||
url: 'error_url', | |||||
method: 'post' | |||||
}) | |||||
} | |||||
export const saveErrorLogger = info => { | |||||
return axios.request({ | |||||
url: 'save_error_logger', | |||||
data: info, | |||||
method: 'post' | |||||
}) | |||||
} | |||||
export const uploadImg = formData => { | |||||
return axios.request({ | |||||
url: 'image/upload', | |||||
data: formData | |||||
}) | |||||
} |
@@ -0,0 +1,11 @@ | |||||
import axios from '@/libs/api.request' | |||||
export const getRouterReq = (access) => { | |||||
return axios.request({ | |||||
url: 'get_router', | |||||
params: { | |||||
access | |||||
}, | |||||
method: 'get' | |||||
}) | |||||
} |
@@ -0,0 +1,22 @@ | |||||
import axios from '@/libs/api.request' | |||||
export const RemoveByTerminalPhoneNo = (terminalPhoneN) => { | |||||
return axios.request({ | |||||
url: 'Session/RemoveByTerminalPhoneNo/' + terminalPhoneN, | |||||
method: 'get' | |||||
}) | |||||
} | |||||
export const RemoveByChannelId = (channelId) => { | |||||
return axios.request({ | |||||
url: 'Session/RemoveByChannelId/' + channelId, | |||||
method: 'get' | |||||
}) | |||||
} | |||||
export const GetAll = () => { | |||||
return axios.request({ | |||||
url: 'Session/GetAll', | |||||
method: 'get' | |||||
}) | |||||
} |
@@ -0,0 +1,32 @@ | |||||
import axios from '@/libs/api.request' | |||||
export const Add = ({ Host, Port }) => { | |||||
const data = { | |||||
Host, | |||||
Port | |||||
} | |||||
return axios.request({ | |||||
url: 'SourcePackage/Add', | |||||
data, | |||||
method: 'post' | |||||
}) | |||||
} | |||||
export const Remove = ({ Host, Port }) => { | |||||
const data = { | |||||
Host, | |||||
Port | |||||
} | |||||
return axios.request({ | |||||
url: 'SourcePackage/Remove', | |||||
data, | |||||
method: 'post' | |||||
}) | |||||
} | |||||
export const GetAll = () => { | |||||
return axios.request({ | |||||
url: 'SourcePackage/GetAll', | |||||
method: 'get' | |||||
}) | |||||
} |
@@ -0,0 +1,32 @@ | |||||
import axios from '@/libs/api.request' | |||||
export const Add = ({ Host, Port }) => { | |||||
const data = { | |||||
Host, | |||||
Port | |||||
} | |||||
return axios.request({ | |||||
url: 'Transmit/Add', | |||||
data, | |||||
method: 'post' | |||||
}) | |||||
} | |||||
export const Remove = ({ Host, Port }) => { | |||||
const data = { | |||||
Host, | |||||
Port | |||||
} | |||||
return axios.request({ | |||||
url: 'Transmit/Remove', | |||||
data, | |||||
method: 'post' | |||||
}) | |||||
} | |||||
export const GetAll = () => { | |||||
return axios.request({ | |||||
url: 'Transmit/GetAll', | |||||
method: 'get' | |||||
}) | |||||
} |
@@ -0,0 +1,8 @@ | |||||
import axios from '@/libs/api.request' | |||||
export const SendText = ({ terminalPhoneNo, text }) => { | |||||
return axios.request({ | |||||
url: 'UnificationSend/SendText/' + terminalPhoneNo + '/' + text, | |||||
method: 'get' | |||||
}) | |||||
} |
@@ -0,0 +1,84 @@ | |||||
import axios from '@/libs/api.request' | |||||
export const login = ({ userName, password }) => { | |||||
const data = { | |||||
userName, | |||||
password | |||||
} | |||||
return axios.request({ | |||||
url: 'login', | |||||
data, | |||||
method: 'post' | |||||
}) | |||||
} | |||||
export const getUserInfo = (token) => { | |||||
return axios.request({ | |||||
url: 'get_info', | |||||
params: { | |||||
token | |||||
}, | |||||
method: 'get' | |||||
}) | |||||
} | |||||
export const logout = (token) => { | |||||
return axios.request({ | |||||
url: 'logout', | |||||
method: 'post' | |||||
}) | |||||
} | |||||
export const getUnreadCount = () => { | |||||
return axios.request({ | |||||
url: 'message/count', | |||||
method: 'get' | |||||
}) | |||||
} | |||||
export const getMessage = () => { | |||||
return axios.request({ | |||||
url: 'message/init', | |||||
method: 'get' | |||||
}) | |||||
} | |||||
export const getContentByMsgId = msg_id => { | |||||
return axios.request({ | |||||
url: 'message/content', | |||||
method: 'get', | |||||
params: { | |||||
msg_id | |||||
} | |||||
}) | |||||
} | |||||
export const hasRead = msg_id => { | |||||
return axios.request({ | |||||
url: 'message/has_read', | |||||
method: 'post', | |||||
data: { | |||||
msg_id | |||||
} | |||||
}) | |||||
} | |||||
export const removeReaded = msg_id => { | |||||
return axios.request({ | |||||
url: 'message/remove_readed', | |||||
method: 'post', | |||||
data: { | |||||
msg_id | |||||
} | |||||
}) | |||||
} | |||||
export const restoreTrash = msg_id => { | |||||
return axios.request({ | |||||
url: 'message/restore', | |||||
method: 'post', | |||||
data: { | |||||
msg_id | |||||
} | |||||
}) | |||||
} |
@@ -0,0 +1,37 @@ | |||||
@font-face {font-family: "iconfont"; | |||||
src: url('iconfont.eot?t=1541579316141'); /* IE9*/ | |||||
src: url('iconfont.eot?t=1541579316141#iefix') format('embedded-opentype'), /* IE6-IE8 */ | |||||
url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAiEAAsAAAAADmgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAARAAAAFY8eUnXY21hcAAAAYAAAACjAAACLi+YJuBnbHlmAAACJAAABAgAAAcg4dRWHmhlYWQAAAYsAAAAMQAAADYTL8piaGhlYQAABmAAAAAgAAAAJAfdA4xobXR4AAAGgAAAABQAAAAsLAD//2xvY2EAAAaUAAAAGAAAABgImgpGbWF4cAAABqwAAAAfAAAAIAEcAG5uYW1lAAAGzAAAAUUAAAJtPlT+fXBvc3QAAAgUAAAAbgAAAI54roygeJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2BkYWCcwMDKwMHUyXSGgYGhH0IzvmYwYuRgYGBiYGVmwAoC0lxTGByeMTx/ytzwv4EhhrmBoRkozAiSAwDuUwzMeJzlkUEKwkAMRd/YabXFhQvxFF6qPYPrUujGY7jyIr1JoZNjtMnEhag3MOEN5MMk8D9QAoVyVSKEJwGrh6oh6wVN1iM3nc+cVImJVKdOehlklElmWdYVstp+ql8VdIv15a1NLW0zFXsO7Kjz3erH/3+rY37vr6kxnx1LKNWOJZlaxxJNnWOpSu+ot8jgqMvI6KjfyOSo88jsaAbI4tBsig89rQB4nLVUTWwbRRSeNzO767i2g7N/FP9s7MRrE5ON4/V6rSZyU0PiINSSNImES4IUoapWz6hEiqiMBDQqEojkAkiFStyKRC+9VSoFCeUEyqESVUAqEkcu3OAQb3hrJxAXwSGI3X0/szPz5vvm2x0i7O/vf8IJe5VkSJnUyUtklRBQJE1VIjRtUafkmk6pSu2ipleh4+xikkKxSksWTUeo8m8NoagpYtoslTmxrLl37z64e33esuJjU8P5Wd262LxoPVnPZ06Pxfe+C0YjkhSJygPhQCA8ABPOykwuN7NyuRvgUnAgLEnhATkaCQQiUe/7XKUyV6nQz+t2o7l66+rs7NVbq82GXTdrdjxjRGU5amTids2bUDMFtzCsqsMYMqr3IDY6OT05GjsI8Exv/6CSkOWEQigh+y3clxY5QVTcEZFIGtHLxDUJs6WsHR1y9SFKdr1HggCp3V1ICYL36OOpVmvKN9bC1u6R3vZ0qwWtVovgJfqOfUvfIYxIWL+fyETHNVJqSkIT1JTjW8ZWh3yDJDz0ctvsyt51etvrg9/QHhqGlzMM+vbmizPnDWPLMNbW19e7tffvsBzL99aWEfBRY46t+tbe3PypXv/IMDYN43WsQBe9HL2NC33RuxABrPsG+xH3o4bVRE2KgCRqulbWNf8W/UYVHM129aKra24VshZkq+CWD/Oy6Xt8cGYEthgHVlVliCfynAlqjo6oysTKlYUAD4docMI5/1ZioN+GwZNBcTwWUmTdBUqhTwX29QebXzF4An4JJMzwfMl+WQ01+IlQZVR4yhie53ycA16pOI/ODiYNGK4MChdCgXNnX5gIJXPCSYnf2OF850aQ+zJIyOs+u8+mMO8jQdwtg1TIWVRjKAnFcslMi8KfGUPoSUCergUyUk77dMyS69Ms6tijKZKYwUGKbpfdzu+iYeZYAHMFiOVi+MD7h9mb99qC0L7X8c+XatMfTj97KZ5IxJt/pd43tYYQKEjAnXMOB6kQEBrwg+LPjindAPOHNdC3q3ait0I3/ZIunZEARLNYNEUA6czSP3N/7j9wz6ZESdX0VNl1zGNS/szbQaQSIGk4DtVPcZf8AgXpf9A2OyTit5s2syZmand46bhEe2WtodLHkvaoqtTXuXN2/c42WADP9HGfbUcUW7JgqHss4xHtlMys679FqUomdP9VJBQBdnlPABBubpuNwqnmQj6/0HwNQzKxDUJFgKiXurBG6dqFjmeBzsvtRPJgGIZThYa5fdOvsReOticPh6JHHXxsv7ItJpOniYPYsmZ/x0QD/o5P105DeQwF6MH33ogoLi+KQp7zpY3HQV5bFMURzheXeds7gpP+jKNXljjHuYvXHke7cdCxLLZf6YX7B63UcCV4nGNgZGBgAOKAN2ZR8fw2Xxm4WRhA4AbHYRMY/f///1oWBuYGIJeDgQkkCgAvWgs2AAAAeJxjYGRgYG7438AQw8Lw/z8DAwsDA1AEBXADAHXiBHJ4nGNhYGBgYfj/nwVM48cATwECKwAAAAAAjAC6AOgBFAGAAf4CbgLqAzgDkHicY2BkYGDgZkhiYGcAASYg5gJCBob/YD4DABOmAYsAeJxlj01OwzAQhV/6B6QSqqhgh+QFYgEo/RGrblhUavdddN+mTpsqiSPHrdQDcB6OwAk4AtyAO/BIJ5s2lsffvHljTwDc4Acejt8t95E9XDI7cg0XuBeuU38QbpBfhJto41W4Rf1N2MczpsJtdGF5g9e4YvaEd2EPHXwI13CNT+E69S/hBvlbuIk7/Aq30PHqwj7mXle4jUcv9sdWL5xeqeVBxaHJIpM5v4KZXu+Sha3S6pxrW8QmU4OgX0lTnWlb3VPs10PnIhVZk6oJqzpJjMqt2erQBRvn8lGvF4kehCblWGP+tsYCjnEFhSUOjDFCGGSIyujoO1Vm9K+xQ8Jee1Y9zed0WxTU/3OFAQL0z1xTurLSeTpPgT1fG1J1dCtuy56UNJFezUkSskJe1rZUQuoBNmVXjhF6XNGJPyhnSP8ACVpuyAAAAHicbYhdDoIwEAb3a6k/YIIX8VArWewmdJFWJOnpJTG+OQ+TzJCjLy39p4ODR4OAA4444YwWHS7U3IVzn6Voldtb8ksHnvohrlqjjmw1rmzXsvdT7fEbblnCmOfNfJIYStJJfGIL27yb6AOCGR89AAA=') format('woff'), | |||||
url('iconfont.ttf?t=1541579316141') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ | |||||
url('iconfont.svg?t=1541579316141#iconfont') format('svg'); /* iOS 4.1- */ | |||||
} | |||||
.iconfont { | |||||
font-family:"iconfont" !important; | |||||
font-size:16px; | |||||
font-style:normal; | |||||
-webkit-font-smoothing: antialiased; | |||||
-moz-osx-font-smoothing: grayscale; | |||||
} | |||||
.icon-bear:before { content: "\e600"; } | |||||
.icon-resize-vertical:before { content: "\e7c3"; } | |||||
.icon-chuizhifanzhuan:before { content: "\e661"; } | |||||
.icon-shuipingfanzhuan:before { content: "\e662"; } | |||||
.icon-qq:before { content: "\e609"; } | |||||
.icon-frown:before { content: "\e77e"; } | |||||
.icon-meh:before { content: "\e780"; } | |||||
.icon-smile:before { content: "\e783"; } | |||||
.icon-man:before { content: "\e7e2"; } | |||||
.icon-woman:before { content: "\e7e5"; } | |||||
@@ -0,0 +1,56 @@ | |||||
<?xml version="1.0" standalone="no"?> | |||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > | |||||
<!-- | |||||
2013-9-30: Created. | |||||
--> | |||||
<svg> | |||||
<metadata> | |||||
Created by iconfont | |||||
</metadata> | |||||
<defs> | |||||
<font id="iconfont" horiz-adv-x="1024" > | |||||
<font-face | |||||
font-family="iconfont" | |||||
font-weight="500" | |||||
font-stretch="normal" | |||||
units-per-em="1024" | |||||
ascent="896" | |||||
descent="-128" | |||||
/> | |||||
<missing-glyph /> | |||||
<glyph glyph-name="bear" unicode="" d="M1024 683.008q0-70.656-46.08-121.856 46.08-89.088 46.08-193.536 0-96.256-39.936-181.248t-109.568-147.968-162.816-99.328-199.68-36.352-199.68 36.352-162.304 99.328-109.568 147.968-40.448 181.248q0 104.448 46.08 193.536-46.08 51.2-46.08 121.856 0 37.888 13.824 71.168t37.376 58.368 55.808 39.424 68.096 14.336q43.008 0 78.848-18.432t59.392-50.176q46.08 17.408 96.256 26.624t102.4 9.216 102.4-9.216 96.256-26.624q24.576 31.744 59.904 50.176t78.336 18.432q36.864 0 68.608-14.336t55.296-39.424 37.376-58.368 13.824-71.168zM205.824 268.288q10.24 0 18.944 10.24t15.36 28.672 10.24 42.496 3.584 51.712-3.584 51.712-10.24 41.984-15.36 28.16-18.944 10.24q-9.216 0-17.92-10.24t-15.36-28.16-10.752-41.984-4.096-51.712 4.096-51.712 10.752-42.496 15.36-28.672 17.92-10.24zM512-31.744000000000028q53.248 0 99.84 13.312t81.408 35.84 54.784 52.736 19.968 65.024q0 33.792-19.968 64t-54.784 52.736-81.408 35.84-99.84 13.312-99.84-13.312-81.408-35.84-54.784-52.736-19.968-64q0-34.816 19.968-65.024t54.784-52.736 81.408-35.84 99.84-13.312zM818.176 268.288q10.24 0 18.944 10.24t15.36 28.672 10.24 42.496 3.584 51.712-3.584 51.712-10.24 41.984-15.36 28.16-18.944 10.24q-9.216 0-17.92-10.24t-15.36-28.16-10.752-41.984-4.096-51.712 4.096-51.712 10.752-42.496 15.36-28.672 17.92-10.24zM512 235.51999999999998q39.936 0 68.096-9.728t28.16-24.064-28.16-24.064-68.096-9.728-68.096 9.728-28.16 24.064 28.16 24.064 68.096 9.728z" horiz-adv-x="1024" /> | |||||
<glyph glyph-name="resize-vertical" unicode="" d="M512 896C229.248 896 0 666.752 0 384s229.248-512 512-512 512 229.248 512 512S794.752 896 512 896zM576 192l64 0-128-128-128 128 64 0L448 576l-64 0 128 128 128-128-64 0L576 192z" horiz-adv-x="1024" /> | |||||
<glyph glyph-name="chuizhifanzhuan" unicode="" d="M286.01856 645.08416l472.4224 0 0-146.2784-472.4224 0 0 146.2784ZM87.19872 420.37248l885.80096 0 0-70.87104-885.80096 0 0 70.87104ZM773.55008 268.05248l0-31.0016L270.6688 237.05088l0 31.0016L773.55008 268.05248zM773.55008 121.4208l0-31.0016L270.6688 90.4192l0 31.0016L773.55008 121.4208zM742.54848 240.75776l31.0016 0 0-123.04896-31.0016 0L742.54848 240.75776zM270.70464 240.57856l31.0016 0 0-123.04896-31.0016 0L270.70464 240.57856z" horiz-adv-x="1024" /> | |||||
<glyph glyph-name="shuipingfanzhuan" unicode="" d="M252.76928 596.096l146.2784 0 0-472.42752-146.2784 0 0 472.42752ZM477.48096 810.65472l70.87104 0 0-885.80608-70.87104 0 0 885.80608ZM629.80096 611.2l31.0016 0 0-502.88128-31.0016 0L629.80096 611.2zM776.42752 611.2l31.0016 0 0-502.88128-31.0016 0L776.42752 611.2zM657.09056 580.1984l0 31.0016 123.04896 0 0-31.0016L657.09056 580.1984zM657.27488 108.35456l0 31.0016 123.04896 0 0-31.0016L657.27488 108.35456z" horiz-adv-x="1024" /> | |||||
<glyph glyph-name="qq" unicode="" d="M147.372058 491.394284c-5.28997-13.909921 2.431986-22.698872 0-75.732573-0.682996-14.25092-62.165649-78.762555-86.569511-145.791177-24.192863-66.517625-27.519845-135.978232 9.811944-163.285078 37.419789-27.305846 72.191593 90.879487 76.757567 73.685584 1.961989-7.509958 4.436975-15.317914 7.423958-23.338868a331.945126 331.945126 0 0 1 61.140655-101.162429c5.929967-6.783962-36.009797-19.199892-61.140655-61.99365-25.173858-42.751759 7.209959-120.49032 132.223254-120.49032 161.27909 0 197.288886 56.70368 200.574868 56.447681 12.031932-0.895995 12.841928 0 25.599855 0 15.572912 0 9.129948-1.279993 23.593867 0 7.807956 0.682996 86.186514-67.839617 194.686901-56.447681 184.873956 19.45589 156.586116 81.40754 142.079198 120.48932-15.103915 40.83277-68.692612 59.946662-66.303626 62.549647 44.28775 48.938724 51.285711 79.018554 66.346626 123.9463 6.143965 18.473896 49.066723-101.674426 82.089537-73.685584 13.781922 11.690934 41.301767 60.24566 13.781922 163.285078-27.519845 102.996419-80.767544 126.505286-79.615551 145.791177 2.389987 40.191773 1.023994 68.436614-1.023994 75.732573-9.812945 35.4128-30.378829 27.604844-30.378829 35.4128C858.450044 730.752933 705.10691 896 515.966978 896s-342.398067-165.289067-342.398068-369.192916c0-16.169909-14.378919-4.223976-26.154852-35.4128z" horiz-adv-x="1024" /> | |||||
<glyph glyph-name="frown" unicode="" d="M336 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM688 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM512 832C264.6 832 64 631.4 64 384s200.6-448 448-448 448 200.6 448 448S759.4 832 512 832z m263-711c-34.2-34.2-74-61-118.3-79.8C611 21.8 562.3 12 512 12c-50.3 0-99 9.8-144.8 29.2-44.3 18.7-84.1 45.6-118.3 79.8-34.2 34.2-61 74-79.8 118.3C149.8 285 140 333.7 140 384s9.8 99 29.2 144.8c18.7 44.3 45.6 84.1 79.8 118.3 34.2 34.2 74 61 118.3 79.8C413 746.2 461.7 756 512 756c50.3 0 99-9.8 144.8-29.2 44.3-18.7 84.1-45.6 118.3-79.8 34.2-34.2 61-74 79.8-118.3C874.2 483 884 434.3 884 384s-9.8-99-29.2-144.8c-18.7-44.3-45.6-84.1-79.8-118.2zM512 363c-85.5 0-155.6-67.3-160-151.6-0.2-4.6 3.4-8.4 8-8.4h48.1c4.2 0 7.8 3.2 8.1 7.4C420 259.9 461.5 299 512 299s92.1-39.1 95.8-88.6c0.3-4.2 3.9-7.4 8.1-7.4H664c4.6 0 8.2 3.8 8 8.4-4.4 84.3-74.5 151.6-160 151.6z" horiz-adv-x="1024" /> | |||||
<glyph glyph-name="meh" unicode="" d="M336 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM688 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM512 832C264.6 832 64 631.4 64 384s200.6-448 448-448 448 200.6 448 448S759.4 832 512 832z m263-711c-34.2-34.2-74-61-118.3-79.8C611 21.8 562.3 12 512 12c-50.3 0-99 9.8-144.8 29.2-44.3 18.7-84.1 45.6-118.3 79.8-34.2 34.2-61 74-79.8 118.3C149.8 285 140 333.7 140 384s9.8 99 29.2 144.8c18.7 44.3 45.6 84.1 79.8 118.3 34.2 34.2 74 61 118.3 79.8C413 746.2 461.7 756 512 756c50.3 0 99-9.8 144.8-29.2 44.3-18.7 84.1-45.6 118.3-79.8 34.2-34.2 61-74 79.8-118.3C874.2 483 884 434.3 884 384s-9.8-99-29.2-144.8c-18.7-44.3-45.6-84.1-79.8-118.2zM664 331H360c-4.4 0-8-3.6-8-8v-48c0-4.4 3.6-8 8-8h304c4.4 0 8 3.6 8 8v48c0 4.4-3.6 8-8 8z" horiz-adv-x="1024" /> | |||||
<glyph glyph-name="smile" unicode="" d="M336 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM688 475m-48 0a48 48 0 1 1 96 0 48 48 0 1 1-96 0ZM512 832C264.6 832 64 631.4 64 384s200.6-448 448-448 448 200.6 448 448S759.4 832 512 832z m263-711c-34.2-34.2-74-61-118.3-79.8C611 21.8 562.3 12 512 12c-50.3 0-99 9.8-144.8 29.2-44.3 18.7-84.1 45.6-118.3 79.8-34.2 34.2-61 74-79.8 118.3C149.8 285 140 333.7 140 384s9.8 99 29.2 144.8c18.7 44.3 45.6 84.1 79.8 118.3 34.2 34.2 74 61 118.3 79.8C413 746.2 461.7 756 512 756c50.3 0 99-9.8 144.8-29.2 44.3-18.7 84.1-45.6 118.3-79.8 34.2-34.2 61-74 79.8-118.3C874.2 483 884 434.3 884 384s-9.8-99-29.2-144.8c-18.7-44.3-45.6-84.1-79.8-118.2zM664 363h-48.1c-4.2 0-7.8-3.2-8.1-7.4C604 306.1 562.5 267 512 267s-92.1 39.1-95.8 88.6c-0.3 4.2-3.9 7.4-8.1 7.4H360c-4.6 0-8.2-3.8-8-8.4 4.4-84.3 74.5-151.6 160-151.6s155.6 67.3 160 151.6c0.2 4.6-3.4 8.4-8 8.4z" horiz-adv-x="1024" /> | |||||
<glyph glyph-name="man" unicode="" d="M874 776H622c-3.3 0-6-2.7-6-6v-56c0-3.3 2.7-6 6-6h160.4L583.1 508.7c-50 38.5-111 59.3-175.1 59.3-76.9 0-149.3-30-203.6-84.4S120 356.9 120 280s30-149.3 84.4-203.6C258.7 22 331.1-8 408-8s149.3 30 203.6 84.4C666 130.7 696 203.1 696 280c0 64.1-20.8 124.9-59.2 174.9L836 654.1V494c0-3.3 2.7-6 6-6h56c3.3 0 6 2.7 6 6V746c0 16.5-13.5 30-30 30zM408 68c-116.9 0-212 95.1-212 212s95.1 212 212 212 212-95.1 212-212-95.1-212-212-212z" horiz-adv-x="1024" /> | |||||
<glyph glyph-name="woman" unicode="" d="M909.7 739.4l-42.2 42.2c-3.1 3.1-8.2 3.1-11.3 0L764 689.4l-84.2 84.2c-3.1 3.1-8.2 3.1-11.3 0l-42.1-42.1c-3.1-3.1-3.1-8.1 0-11.3l84.2-84.2-135.5-135.3c-50 38.5-111 59.3-175.1 59.3-76.9 0-149.3-30-203.6-84.4S112 348.9 112 272s30-149.3 84.4-203.6C250.7 14 323.1-16 400-16s149.3 30 203.6 84.4C658 122.7 688 195.1 688 272c0 64.2-20.9 125.1-59.3 175.1l135.4 135.4 84.2-84.2c3.1-3.1 8.2-3.1 11.3 0l42.1 42.1c3.1 3.1 3.1 8.1 0 11.3l-84.2 84.2 92.2 92.2c3.1 3.1 3.1 8.2 0 11.3zM400 60c-116.9 0-212 95.1-212 212s95.1 212 212 212 212-95.1 212-212-95.1-212-212-212z" horiz-adv-x="1024" /> | |||||
</font> | |||||
</defs></svg> |
@@ -0,0 +1,58 @@ | |||||
<template> | |||||
<div ref="dom" class="charts chart-bar"></div> | |||||
</template> | |||||
<script> | |||||
import echarts from 'echarts' | |||||
import tdTheme from './theme.json' | |||||
import { on, off } from '@/libs/tools' | |||||
echarts.registerTheme('tdTheme', tdTheme) | |||||
export default { | |||||
name: 'ChartBar', | |||||
props: { | |||||
value: Object, | |||||
text: String, | |||||
subtext: String | |||||
}, | |||||
data () { | |||||
return { | |||||
dom: null | |||||
} | |||||
}, | |||||
methods: { | |||||
resize () { | |||||
this.dom.resize() | |||||
} | |||||
}, | |||||
mounted () { | |||||
this.$nextTick(() => { | |||||
let xAxisData = Object.keys(this.value) | |||||
let seriesData = Object.values(this.value) | |||||
let option = { | |||||
title: { | |||||
text: this.text, | |||||
subtext: this.subtext, | |||||
x: 'center' | |||||
}, | |||||
xAxis: { | |||||
type: 'category', | |||||
data: xAxisData | |||||
}, | |||||
yAxis: { | |||||
type: 'value' | |||||
}, | |||||
series: [{ | |||||
data: seriesData, | |||||
type: 'bar' | |||||
}] | |||||
} | |||||
this.dom = echarts.init(this.$refs.dom, 'tdTheme') | |||||
this.dom.setOption(option) | |||||
on(window, 'resize', this.resize) | |||||
}) | |||||
}, | |||||
beforeDestroy () { | |||||
off(window, 'resize', this.resize) | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,3 @@ | |||||
import ChartPie from './pie.vue' | |||||
import ChartBar from './bar.vue' | |||||
export { ChartPie, ChartBar } |
@@ -0,0 +1,70 @@ | |||||
<template> | |||||
<div ref="dom" class="charts chart-pie"></div> | |||||
</template> | |||||
<script> | |||||
import echarts from 'echarts' | |||||
import tdTheme from './theme.json' | |||||
import { on, off } from '@/libs/tools' | |||||
echarts.registerTheme('tdTheme', tdTheme) | |||||
export default { | |||||
name: 'ChartPie', | |||||
props: { | |||||
value: Array, | |||||
text: String, | |||||
subtext: String | |||||
}, | |||||
data () { | |||||
return { | |||||
dom: null | |||||
} | |||||
}, | |||||
methods: { | |||||
resize () { | |||||
this.dom.resize() | |||||
} | |||||
}, | |||||
mounted () { | |||||
this.$nextTick(() => { | |||||
let legend = this.value.map(_ => _.name) | |||||
let option = { | |||||
title: { | |||||
text: this.text, | |||||
subtext: this.subtext, | |||||
x: 'center' | |||||
}, | |||||
tooltip: { | |||||
trigger: 'item', | |||||
formatter: '{a} <br/>{b} : {c} ({d}%)' | |||||
}, | |||||
legend: { | |||||
orient: 'vertical', | |||||
left: 'left', | |||||
data: legend | |||||
}, | |||||
series: [ | |||||
{ | |||||
type: 'pie', | |||||
radius: '55%', | |||||
center: ['50%', '60%'], | |||||
data: this.value, | |||||
itemStyle: { | |||||
emphasis: { | |||||
shadowBlur: 10, | |||||
shadowOffsetX: 0, | |||||
shadowColor: 'rgba(0, 0, 0, 0.5)' | |||||
} | |||||
} | |||||
} | |||||
] | |||||
} | |||||
this.dom = echarts.init(this.$refs.dom, 'tdTheme') | |||||
this.dom.setOption(option) | |||||
on(window, 'resize', this.resize) | |||||
}) | |||||
}, | |||||
beforeDestroy () { | |||||
off(window, 'resize', this.resize) | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,42 @@ | |||||
<template> | |||||
<component :is="iconType" :type="iconName" :color="iconColor" :size="iconSize"/> | |||||
</template> | |||||
<script> | |||||
import Icons from '_c/icons' | |||||
export default { | |||||
name: 'CommonIcon', | |||||
components: {Icons}, | |||||
props: { | |||||
type: { | |||||
type: String, | |||||
required: true | |||||
}, | |||||
color: String, | |||||
size: Number | |||||
}, | |||||
computed: { | |||||
iconType () { | |||||
return this.type.indexOf('_') === 0 ? 'Icons' : 'Icon' | |||||
}, | |||||
iconName () { | |||||
return this.iconType === 'Icons' ? this.getCustomIconName(this.type) : this.type | |||||
}, | |||||
iconSize () { | |||||
return this.size || (this.iconType === 'Icons' ? 12 : undefined) | |||||
}, | |||||
iconColor () { | |||||
return this.color || '' | |||||
} | |||||
}, | |||||
methods: { | |||||
getCustomIconName (iconName) { | |||||
return iconName.slice(1) | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
<style> | |||||
</style> |
@@ -0,0 +1,2 @@ | |||||
import CommonIcon from './common-icon.vue' | |||||
export default CommonIcon |
@@ -0,0 +1,8 @@ | |||||
.no-select{ | |||||
-webkit-touch-callout: none; | |||||
-webkit-user-select: none; | |||||
-khtml-user-select: none; | |||||
-moz-user-select: none; | |||||
-ms-user-select: none; | |||||
user-select: none; | |||||
} |
@@ -0,0 +1,3 @@ | |||||
export const showTitle = (item, vm) => { | |||||
return vm.$config.useI18n ? vm.$t(item.name) : ((item.meta && item.meta.title) || item.name) | |||||
} |
@@ -0,0 +1,174 @@ | |||||
<template> | |||||
<div class="count-to-wrapper"> | |||||
<slot name="left"/> | |||||
<p class="content-outer"><span :class="['count-to-count-text', countClass]" :id="counterId">{{ init }}</span><i :class="['count-to-unit-text', unitClass]">{{ unitText }}</i></p> | |||||
<slot name="right"/> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import CountUp from 'countup' | |||||
import './index.less' | |||||
export default { | |||||
name: 'CountTo', | |||||
props: { | |||||
init: { | |||||
type: Number, | |||||
default: 0 | |||||
}, | |||||
/** | |||||
* @description 起始值,即动画开始前显示的数值 | |||||
*/ | |||||
startVal: { | |||||
type: Number, | |||||
default: 0 | |||||
}, | |||||
/** | |||||
* @description 结束值,即动画结束后显示的数值 | |||||
*/ | |||||
end: { | |||||
type: Number, | |||||
required: true | |||||
}, | |||||
/** | |||||
* @description 保留几位小数 | |||||
*/ | |||||
decimals: { | |||||
type: Number, | |||||
default: 0 | |||||
}, | |||||
/** | |||||
* @description 分隔整数和小数的符号,默认是小数点 | |||||
*/ | |||||
decimal: { | |||||
type: String, | |||||
default: '.' | |||||
}, | |||||
/** | |||||
* @description 动画持续的时间,单位是秒 | |||||
*/ | |||||
duration: { | |||||
type: Number, | |||||
default: 2 | |||||
}, | |||||
/** | |||||
* @description 动画延迟开始的时间,单位是秒 | |||||
*/ | |||||
delay: { | |||||
type: Number, | |||||
default: 0 | |||||
}, | |||||
/** | |||||
* @description 是否禁用easing动画效果 | |||||
*/ | |||||
uneasing: { | |||||
type: Boolean, | |||||
default: false | |||||
}, | |||||
/** | |||||
* @description 是否使用分组,分组后每三位会用一个符号分隔 | |||||
*/ | |||||
usegroup: { | |||||
type: Boolean, | |||||
default: false | |||||
}, | |||||
/** | |||||
* @description 用于分组(usegroup)的符号 | |||||
*/ | |||||
separator: { | |||||
type: String, | |||||
default: ',' | |||||
}, | |||||
/** | |||||
* @description 是否简化显示,设为true后会使用unit单位来做相关省略 | |||||
*/ | |||||
simplify: { | |||||
type: Boolean, | |||||
default: false | |||||
}, | |||||
/** | |||||
* @description 自定义单位,如[3, 'K+'], [6, 'M+']即大于3位数小于6位数的用k+来做省略 | |||||
* 1000即显示为1K+ | |||||
*/ | |||||
unit: { | |||||
type: Array, | |||||
default () { | |||||
return [[3, 'K+'], [6, 'M+'], [9, 'B+']] | |||||
} | |||||
}, | |||||
countClass: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
unitClass: { | |||||
type: String, | |||||
default: '' | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
counter: null, | |||||
unitText: '' | |||||
} | |||||
}, | |||||
computed: { | |||||
counterId () { | |||||
return `count_to_${this._uid}` | |||||
} | |||||
}, | |||||
methods: { | |||||
getHandleVal (val, len) { | |||||
return { | |||||
endVal: parseInt(val / Math.pow(10, this.unit[len - 1][0])), | |||||
unitText: this.unit[len - 1][1] | |||||
} | |||||
}, | |||||
transformValue (val) { | |||||
let len = this.unit.length | |||||
let res = { | |||||
endVal: 0, | |||||
unitText: '' | |||||
} | |||||
if (val < Math.pow(10, this.unit[0][0])) res.endVal = val | |||||
else { | |||||
for (let i = 1; i < len; i++) { | |||||
if (val >= Math.pow(10, this.unit[i - 1][0]) && val < Math.pow(10, this.unit[i][0])) res = this.getHandleVal(val, i) | |||||
} | |||||
} | |||||
if (val > Math.pow(10, this.unit[len - 1][0])) res = this.getHandleVal(val, len) | |||||
return res | |||||
}, | |||||
getValue (val) { | |||||
let res = 0 | |||||
if (this.simplify) { | |||||
let { endVal, unitText } = this.transformValue(val) | |||||
this.unitText = unitText | |||||
res = endVal | |||||
} else { | |||||
res = val | |||||
} | |||||
return res | |||||
} | |||||
}, | |||||
mounted () { | |||||
this.$nextTick(() => { | |||||
let endVal = this.getValue(this.end) | |||||
this.counter = new CountUp(this.counterId, this.startVal, endVal, this.decimals, this.duration, { | |||||
useEasing: !this.uneasing, | |||||
useGrouping: this.useGroup, | |||||
separator: this.separator, | |||||
decimal: this.decimal | |||||
}) | |||||
setTimeout(() => { | |||||
if (!this.counter.error) this.counter.start() | |||||
}, this.delay) | |||||
}) | |||||
}, | |||||
watch: { | |||||
end (newVal) { | |||||
let endVal = this.getValue(newVal) | |||||
this.counter.update(endVal) | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,2 @@ | |||||
import countTo from './count-to.vue' | |||||
export default countTo |
@@ -0,0 +1,10 @@ | |||||
@prefix: ~"count-to"; | |||||
.@{prefix}-wrapper{ | |||||
.content-outer{ | |||||
display: inline-block; | |||||
.@{prefix}-unit-text{ | |||||
font-style: normal; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,2 @@ | |||||
import Cropper from './index.vue' | |||||
export default Cropper |
@@ -0,0 +1,35 @@ | |||||
.bg{ | |||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC") | |||||
} | |||||
.cropper-wrapper{ | |||||
width: 600px; | |||||
height: 340px; | |||||
.img-box{ | |||||
height: 340px; | |||||
width: 430px; | |||||
border: 1px solid #ebebeb; | |||||
display: inline-block; | |||||
.bg; | |||||
img{ | |||||
max-width: 100%; | |||||
display: block; | |||||
} | |||||
} | |||||
.right-con{ | |||||
display: inline-block; | |||||
width: 170px; | |||||
vertical-align: top; | |||||
box-sizing: border-box; | |||||
padding: 0 10px; | |||||
.preview-box{ | |||||
height: 150px !important; | |||||
width: 100% !important; | |||||
overflow: hidden; | |||||
border: 1px solid #ebebeb; | |||||
.bg; | |||||
} | |||||
.button-box{ | |||||
padding: 10px 0 0; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,139 @@ | |||||
<template> | |||||
<div class="cropper-wrapper"> | |||||
<div class="img-box"> | |||||
<img class="cropper-image" :id="imgId" alt=""> | |||||
</div> | |||||
<div class="right-con"> | |||||
<div v-if="preview" class="preview-box" :id="previewId"></div> | |||||
<div class="button-box"> | |||||
<slot> | |||||
<Upload action="image/upload" :before-upload="beforeUpload"> | |||||
<Button style="width: 150px;" type="primary">上传图片</Button> | |||||
</Upload> | |||||
</slot> | |||||
<div v-show="insideSrc"> | |||||
<Button type="primary" @click="rotate"> | |||||
<Icon type="md-refresh" :size="18"/> | |||||
</Button> | |||||
<Button type="primary" @click="shrink"> | |||||
<Icon type="md-remove" :size="18"/> | |||||
</Button> | |||||
<Button type="primary" @click="magnify"> | |||||
<Icon type="md-add" :size="18"/> | |||||
</Button> | |||||
<Button type="primary" @click="scale('X')"> | |||||
<Icon custom="iconfont icon-shuipingfanzhuan" :size="18"/> | |||||
</Button> | |||||
<Button type="primary" @click="scale('Y')"> | |||||
<Icon custom="iconfont icon-chuizhifanzhuan" :size="18"/> | |||||
</Button> | |||||
<Button type="primary" @click="move(0, -moveStep)"> | |||||
<Icon type="md-arrow-round-up" :size="18"/> | |||||
</Button> | |||||
<Button type="primary" @click="move(-moveStep, 0)"> | |||||
<Icon type="md-arrow-round-back" :size="18"/> | |||||
</Button> | |||||
<Button type="primary" @click="move(0, moveStep)"> | |||||
<Icon type="md-arrow-round-down" :size="18"/> | |||||
</Button> | |||||
<Button type="primary" @click="move(moveStep, 0)"> | |||||
<Icon type="md-arrow-round-forward" :size="18"/> | |||||
</Button> | |||||
<Button style="width: 150px;margin-top: 10px;" type="primary" @click="crop">{{ cropButtonText }}</Button> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import Cropper from 'cropperjs' | |||||
import './index.less' | |||||
import 'cropperjs/dist/cropper.min.css' | |||||
export default { | |||||
name: 'Cropper', | |||||
props: { | |||||
src: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
preview: { | |||||
type: Boolean, | |||||
default: true | |||||
}, | |||||
moveStep: { | |||||
type: Number, | |||||
default: 4 | |||||
}, | |||||
cropButtonText: { | |||||
type: String, | |||||
default: '裁剪' | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
cropper: null, | |||||
insideSrc: '' | |||||
} | |||||
}, | |||||
computed: { | |||||
imgId () { | |||||
return `cropper${this._uid}` | |||||
}, | |||||
previewId () { | |||||
return `cropper_preview${this._uid}` | |||||
} | |||||
}, | |||||
watch: { | |||||
src (src) { | |||||
this.replace(src) | |||||
}, | |||||
insideSrc (src) { | |||||
this.replace(src) | |||||
} | |||||
}, | |||||
methods: { | |||||
beforeUpload (file) { | |||||
const reader = new FileReader() | |||||
reader.readAsDataURL(file) | |||||
reader.onload = (event) => { | |||||
this.insideSrc = event.srcElement.result | |||||
} | |||||
return false | |||||
}, | |||||
replace (src) { | |||||
this.cropper.replace(src) | |||||
this.insideSrc = src | |||||
}, | |||||
rotate () { | |||||
this.cropper.rotate(90) | |||||
}, | |||||
shrink () { | |||||
this.cropper.zoom(-0.1) | |||||
}, | |||||
magnify () { | |||||
this.cropper.zoom(0.1) | |||||
}, | |||||
scale (d) { | |||||
this.cropper[`scale${d}`](-this.cropper.getData()[`scale${d}`]) | |||||
}, | |||||
move (...argu) { | |||||
this.cropper.move(...argu) | |||||
}, | |||||
crop () { | |||||
this.cropper.getCroppedCanvas().toBlob(blob => { | |||||
this.$emit('on-crop', blob) | |||||
}) | |||||
} | |||||
}, | |||||
mounted () { | |||||
this.$nextTick(() => { | |||||
let dom = document.getElementById(this.imgId) | |||||
this.cropper = new Cropper(dom, { | |||||
preview: `#${this.previewId}`, | |||||
checkCrossOrigin: true | |||||
}) | |||||
}) | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,92 @@ | |||||
<template> | |||||
<div class="drag-list-wrapper"> | |||||
<div class="drag-list-con con1"> | |||||
<slot name="left-title"></slot> | |||||
<draggable class="drop-box1" :class="dropConClass.left" :options="options" :value="list1" @input="handleListChange($event, 'left')" @end="handleEnd($event, 'left')"> | |||||
<div class="drag-list-item" v-for="(itemLeft, index) in list1" :key="`drag_li1_${index}`"> | |||||
<slot name="left" :itemLeft="itemLeft">{{ itemLeft }}</slot> | |||||
</div> | |||||
</draggable> | |||||
</div> | |||||
<div class="drag-list-con con2"> | |||||
<slot name="right-title"></slot> | |||||
<draggable class="drop-box2" :class="dropConClass.right" :options="options" :value="list2" @input="handleListChange($event, 'right')" @end="handleEnd($event, 'right')"> | |||||
<div class="drag-list-item" v-for="(itemRight, index) in list2" :key="`drag_li2_${index}`"> | |||||
<slot name="right" :itemRight="itemRight">{{ itemRight }}</slot> | |||||
</div> | |||||
</draggable> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import draggable from 'vuedraggable' | |||||
export default { | |||||
name: 'DragList', | |||||
components: { | |||||
draggable | |||||
}, | |||||
props: { | |||||
list1: { | |||||
type: Array, | |||||
required: true | |||||
}, | |||||
list2: { | |||||
type: Array, | |||||
default: () => [] | |||||
}, | |||||
dropConClass: { | |||||
type: Object, | |||||
default: () => ({}) | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
options: { group: 'drag_list' } | |||||
} | |||||
}, | |||||
methods: { | |||||
handleListChange (value, type) { | |||||
if (type === 'left') this.$emit('update:list1', value) | |||||
else this.$emit('update:list2', value) | |||||
}, | |||||
handleEnd (event, type) { | |||||
const srcClassName = (event.srcElement || event.target).classList[0] | |||||
const targetClassName = event.to.classList[0] | |||||
let src = '' | |||||
let target = '' | |||||
if (srcClassName === targetClassName) { | |||||
if (type === 'left') { | |||||
src = 'left' | |||||
target = 'left' | |||||
} else { | |||||
src = 'right' | |||||
target = 'right' | |||||
} | |||||
} else { | |||||
if (type === 'left') { | |||||
src = 'left' | |||||
target = 'right' | |||||
} else { | |||||
src = 'right' | |||||
target = 'left' | |||||
} | |||||
} | |||||
this.$emit('on-change', { | |||||
src: src, | |||||
target: target, | |||||
oldIndex: event.oldIndex, | |||||
newIndex: event.newIndex | |||||
}) | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.drag-list-wrapper{ | |||||
height: 100%; | |||||
.drag-list-con{ | |||||
width: 50%; | |||||
float: left; | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,2 @@ | |||||
import DragList from './drag-list.vue' | |||||
export default DragList |
@@ -0,0 +1,75 @@ | |||||
<template> | |||||
<div class="editor-wrapper"> | |||||
<div :id="editorId"></div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import Editor from 'wangeditor' | |||||
import 'wangeditor/release/wangEditor.min.css' | |||||
import { oneOf } from '@/libs/tools' | |||||
export default { | |||||
name: 'Editor', | |||||
props: { | |||||
value: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
/** | |||||
* 绑定的值的类型, enum: ['html', 'text'] | |||||
*/ | |||||
valueType: { | |||||
type: String, | |||||
default: 'html', | |||||
validator: (val) => { | |||||
return oneOf(val, ['html', 'text']) | |||||
} | |||||
}, | |||||
/** | |||||
* @description 设置change事件触发时间间隔 | |||||
*/ | |||||
changeInterval: { | |||||
type: Number, | |||||
default: 200 | |||||
}, | |||||
/** | |||||
* @description 是否开启本地存储 | |||||
*/ | |||||
cache: { | |||||
type: Boolean, | |||||
default: true | |||||
} | |||||
}, | |||||
computed: { | |||||
editorId () { | |||||
return `editor${this._uid}` | |||||
} | |||||
}, | |||||
methods: { | |||||
setHtml (val) { | |||||
this.editor.txt.html(val) | |||||
} | |||||
}, | |||||
mounted () { | |||||
this.editor = new Editor(`#${this.editorId}`) | |||||
this.editor.customConfig.onchange = (html) => { | |||||
let text = this.editor.txt.text() | |||||
if (this.cache) localStorage.editorCache = html | |||||
this.$emit('input', this.valueType === 'html' ? html : text) | |||||
this.$emit('on-change', html, text) | |||||
} | |||||
this.editor.customConfig.onchangeTimeout = this.changeInterval | |||||
// create这个方法一定要在所有配置项之后调用 | |||||
this.editor.create() | |||||
// 如果本地有存储加载本地存储内容 | |||||
let html = this.value || localStorage.editorCache | |||||
if (html) this.editor.txt.html(html) | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.editor-wrapper *{ | |||||
z-index: 100 !important; | |||||
} | |||||
</style> |
@@ -0,0 +1,2 @@ | |||||
import Editor from './editor.vue' | |||||
export default Editor |
@@ -0,0 +1,35 @@ | |||||
<template> | |||||
<i :class="`iconfont icon-${type}`" :style="styles"></i> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'Icons', | |||||
props: { | |||||
type: { | |||||
type: String, | |||||
required: true | |||||
}, | |||||
color: { | |||||
type: String, | |||||
default: '#5c6b77' | |||||
}, | |||||
size: { | |||||
type: Number, | |||||
default: 16 | |||||
} | |||||
}, | |||||
computed: { | |||||
styles () { | |||||
return { | |||||
fontSize: `${this.size}px`, | |||||
color: this.color | |||||
} | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
<style> | |||||
</style> |
@@ -0,0 +1,2 @@ | |||||
import Icons from './icons.vue' | |||||
export default Icons |
@@ -0,0 +1,2 @@ | |||||
import InforCard from './infor-card.vue' | |||||
export default InforCard |
@@ -0,0 +1,94 @@ | |||||
<template> | |||||
<Card :shadow="shadow" class="info-card-wrapper" :padding="0"> | |||||
<div class="content-con"> | |||||
<div class="left-area" :style="{background: color, width: leftWidth}"> | |||||
<common-icon class="icon" :type="icon" :size="iconSize" color="#fff"/> | |||||
</div> | |||||
<div class="right-area" :style="{width: rightWidth}"> | |||||
<div> | |||||
<slot></slot> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</Card> | |||||
</template> | |||||
<script> | |||||
import CommonIcon from '_c/common-icon' | |||||
export default { | |||||
name: 'InforCard', | |||||
components: { | |||||
CommonIcon | |||||
}, | |||||
props: { | |||||
left: { | |||||
type: Number, | |||||
default: 36 | |||||
}, | |||||
color: { | |||||
type: String, | |||||
default: '#2d8cf0' | |||||
}, | |||||
icon: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
iconSize: { | |||||
type: Number, | |||||
default: 20 | |||||
}, | |||||
shadow: { | |||||
type: Boolean, | |||||
default: false | |||||
} | |||||
}, | |||||
computed: { | |||||
leftWidth () { | |||||
return `${this.left}%` | |||||
}, | |||||
rightWidth () { | |||||
return `${100 - this.left}%` | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.common{ | |||||
float: left; | |||||
height: 100%; | |||||
display: table; | |||||
text-align: center; | |||||
} | |||||
.size{ | |||||
width: 100%; | |||||
height: 100%; | |||||
} | |||||
.middle-center{ | |||||
display: table-cell; | |||||
vertical-align: middle; | |||||
} | |||||
.info-card-wrapper{ | |||||
.size; | |||||
overflow: hidden; | |||||
.ivu-card-body{ | |||||
.size; | |||||
} | |||||
.content-con{ | |||||
.size; | |||||
position: relative; | |||||
.left-area{ | |||||
.common; | |||||
& > .icon{ | |||||
.middle-center; | |||||
} | |||||
} | |||||
.right-area{ | |||||
.common; | |||||
& > div{ | |||||
.middle-center; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,2 @@ | |||||
import LoginForm from './login-form.vue' | |||||
export default LoginForm |
@@ -0,0 +1,72 @@ | |||||
<template> | |||||
<Form ref="loginForm" :model="form" :rules="rules" @keydown.enter.native="handleSubmit"> | |||||
<FormItem prop="userName"> | |||||
<Input v-model="form.userName" placeholder="请输入用户名"> | |||||
<span slot="prepend"> | |||||
<Icon :size="16" type="ios-person"></Icon> | |||||
</span> | |||||
</Input> | |||||
</FormItem> | |||||
<FormItem prop="password"> | |||||
<Input type="password" v-model="form.password" placeholder="请输入密码"> | |||||
<span slot="prepend"> | |||||
<Icon :size="14" type="md-lock"></Icon> | |||||
</span> | |||||
</Input> | |||||
</FormItem> | |||||
<FormItem> | |||||
<Button @click="handleSubmit" type="primary" long>登录</Button> | |||||
</FormItem> | |||||
</Form> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'LoginForm', | |||||
props: { | |||||
userNameRules: { | |||||
type: Array, | |||||
default: () => { | |||||
return [ | |||||
{ required: true, message: '账号不能为空', trigger: 'blur' } | |||||
] | |||||
} | |||||
}, | |||||
passwordRules: { | |||||
type: Array, | |||||
default: () => { | |||||
return [ | |||||
{ required: true, message: '密码不能为空', trigger: 'blur' } | |||||
] | |||||
} | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
form: { | |||||
userName: 'super_admin', | |||||
password: '' | |||||
} | |||||
} | |||||
}, | |||||
computed: { | |||||
rules () { | |||||
return { | |||||
userName: this.userNameRules, | |||||
password: this.passwordRules | |||||
} | |||||
} | |||||
}, | |||||
methods: { | |||||
handleSubmit () { | |||||
this.$refs.loginForm.validate((valid) => { | |||||
if (valid) { | |||||
this.$emit('on-success-valid', { | |||||
userName: this.form.userName, | |||||
password: this.form.password | |||||
}) | |||||
} | |||||
}) | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,2 @@ | |||||
import ABackTop from './index.vue' | |||||
export default ABackTop |
@@ -0,0 +1,90 @@ | |||||
<template> | |||||
<div :class="classes" :style="styles" @click="back"> | |||||
<slot> | |||||
<div :class="innerClasses"> | |||||
<i class="ivu-icon ivu-icon-ios-arrow-up"></i> | |||||
</div> | |||||
</slot> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { scrollTop } from '@/libs/util' | |||||
import { on, off } from '@/libs/tools' | |||||
const prefixCls = 'ivu-back-top' | |||||
export default { | |||||
name: 'ABackTop', | |||||
props: { | |||||
height: { | |||||
type: Number, | |||||
default: 400 | |||||
}, | |||||
bottom: { | |||||
type: Number, | |||||
default: 30 | |||||
}, | |||||
right: { | |||||
type: Number, | |||||
default: 30 | |||||
}, | |||||
duration: { | |||||
type: Number, | |||||
default: 1000 | |||||
}, | |||||
container: { | |||||
type: null, | |||||
default: window | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
backTop: false | |||||
} | |||||
}, | |||||
mounted () { | |||||
// window.addEventListener('scroll', this.handleScroll, false) | |||||
// window.addEventListener('resize', this.handleScroll, false) | |||||
on(this.containerEle, 'scroll', this.handleScroll) | |||||
on(this.containerEle, 'resize', this.handleScroll) | |||||
}, | |||||
beforeDestroy () { | |||||
// window.removeEventListener('scroll', this.handleScroll, false) | |||||
// window.removeEventListener('resize', this.handleScroll, false) | |||||
off(this.containerEle, 'scroll', this.handleScroll) | |||||
off(this.containerEle, 'resize', this.handleScroll) | |||||
}, | |||||
computed: { | |||||
classes () { | |||||
return [ | |||||
`${prefixCls}`, | |||||
{ | |||||
[`${prefixCls}-show`]: this.backTop | |||||
} | |||||
] | |||||
}, | |||||
styles () { | |||||
return { | |||||
bottom: `${this.bottom}px`, | |||||
right: `${this.right}px` | |||||
} | |||||
}, | |||||
innerClasses () { | |||||
return `${prefixCls}-inner` | |||||
}, | |||||
containerEle () { | |||||
return this.container === window ? window : document.querySelector(this.container) | |||||
} | |||||
}, | |||||
methods: { | |||||
handleScroll () { | |||||
this.backTop = this.containerEle.scrollTop >= this.height | |||||
}, | |||||
back () { | |||||
let target = typeof this.container === 'string' ? this.containerEle : (document.documentElement || document.body) | |||||
const sTop = target.scrollTop | |||||
scrollTop(this.containerEle, sTop, 0, this.duration) | |||||
this.$emit('on-click') | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,49 @@ | |||||
<template> | |||||
<div class="error-store"> | |||||
<Badge dot :count="countComputed"> | |||||
<Button type="text" @click="openErrorLoggerPage"> | |||||
<Icon :size="20" type="ios-bug"/> | |||||
</Button> | |||||
</Badge> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'ErrorStore', | |||||
props: { | |||||
count: { | |||||
type: Number, | |||||
default: 0 | |||||
}, | |||||
hasRead: { | |||||
type: Boolean, | |||||
default: false | |||||
} | |||||
}, | |||||
computed: { | |||||
countComputed () { | |||||
return this.hasRead ? 0 : this.count | |||||
} | |||||
}, | |||||
methods: { | |||||
openErrorLoggerPage () { | |||||
this.$router.push({ | |||||
name: 'error_logger_page' | |||||
}) | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.error-store{ | |||||
margin-right: 12px; | |||||
.ivu-badge-dot{ | |||||
top: 20px; | |||||
} | |||||
.ivu-btn.ivu-btn-text{ | |||||
padding: 5px 1px 6px; | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,2 @@ | |||||
import ErrorStore from './error-store.vue' | |||||
export default ErrorStore |
@@ -0,0 +1,84 @@ | |||||
<template> | |||||
<div v-if="showFullScreenBtn" class="full-screen-btn-con"> | |||||
<Tooltip :content="value ? '退出全屏' : '全屏'" placement="bottom"> | |||||
<Icon @click.native="handleChange" :type="value ? 'md-contract' : 'md-expand'" :size="23"></Icon> | |||||
</Tooltip> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'Fullscreen', | |||||
computed: { | |||||
showFullScreenBtn () { | |||||
return window.navigator.userAgent.indexOf('MSIE') < 0 | |||||
} | |||||
}, | |||||
props: { | |||||
value: { | |||||
type: Boolean, | |||||
default: false | |||||
} | |||||
}, | |||||
methods: { | |||||
handleFullscreen () { | |||||
let main = document.body | |||||
if (this.value) { | |||||
if (document.exitFullscreen) { | |||||
document.exitFullscreen() | |||||
} else if (document.mozCancelFullScreen) { | |||||
document.mozCancelFullScreen() | |||||
} else if (document.webkitCancelFullScreen) { | |||||
document.webkitCancelFullScreen() | |||||
} else if (document.msExitFullscreen) { | |||||
document.msExitFullscreen() | |||||
} | |||||
} else { | |||||
if (main.requestFullscreen) { | |||||
main.requestFullscreen() | |||||
} else if (main.mozRequestFullScreen) { | |||||
main.mozRequestFullScreen() | |||||
} else if (main.webkitRequestFullScreen) { | |||||
main.webkitRequestFullScreen() | |||||
} else if (main.msRequestFullscreen) { | |||||
main.msRequestFullscreen() | |||||
} | |||||
} | |||||
}, | |||||
handleChange () { | |||||
this.handleFullscreen() | |||||
} | |||||
}, | |||||
mounted () { | |||||
let isFullscreen = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen | |||||
isFullscreen = !!isFullscreen | |||||
document.addEventListener('fullscreenchange', () => { | |||||
this.$emit('input', !this.value) | |||||
this.$emit('on-change', !this.value) | |||||
}) | |||||
document.addEventListener('mozfullscreenchange', () => { | |||||
this.$emit('input', !this.value) | |||||
this.$emit('on-change', !this.value) | |||||
}) | |||||
document.addEventListener('webkitfullscreenchange', () => { | |||||
this.$emit('input', !this.value) | |||||
this.$emit('on-change', !this.value) | |||||
}) | |||||
document.addEventListener('msfullscreenchange', () => { | |||||
this.$emit('input', !this.value) | |||||
this.$emit('on-change', !this.value) | |||||
}) | |||||
this.$emit('input', isFullscreen) | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.full-screen-btn-con .ivu-tooltip-rel{ | |||||
height: 64px; | |||||
line-height: 56px; | |||||
i{ | |||||
cursor: pointer; | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,2 @@ | |||||
import Fullscreen from './fullscreen.vue' | |||||
export default Fullscreen |
@@ -0,0 +1,4 @@ | |||||
.custom-bread-crumb{ | |||||
display: inline-block; | |||||
vertical-align: top; | |||||
} |
@@ -0,0 +1,46 @@ | |||||
<template> | |||||
<div class="custom-bread-crumb"> | |||||
<Breadcrumb :style="{fontSize: `${fontSize}px`}"> | |||||
<BreadcrumbItem v-for="item in list" :to="item.to" :key="`bread-crumb-${item.name}`"> | |||||
<common-icon style="margin-right: 4px;" :type="item.icon || ''"/> | |||||
{{ showTitle(item) }} | |||||
</BreadcrumbItem> | |||||
</Breadcrumb> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { showTitle } from '@/libs/util' | |||||
import CommonIcon from '_c/common-icon' | |||||
import './custom-bread-crumb.less' | |||||
export default { | |||||
name: 'customBreadCrumb', | |||||
components: { | |||||
CommonIcon | |||||
}, | |||||
props: { | |||||
list: { | |||||
type: Array, | |||||
default: () => [] | |||||
}, | |||||
fontSize: { | |||||
type: Number, | |||||
default: 14 | |||||
}, | |||||
showIcon: { | |||||
type: Boolean, | |||||
default: false | |||||
} | |||||
}, | |||||
methods: { | |||||
showTitle (item) { | |||||
return showTitle(item, this) | |||||
}, | |||||
isCustomIcon (iconName) { | |||||
return iconName.indexOf('_') === 0 | |||||
}, | |||||
getCustomIconName (iconName) { | |||||
return iconName.slice(1) | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,2 @@ | |||||
import customBreadCrumb from './custom-bread-crumb.vue' | |||||
export default customBreadCrumb |
@@ -0,0 +1,14 @@ | |||||
.header-bar{ | |||||
width: 100%; | |||||
height: 100%; | |||||
position: relative; | |||||
.custom-content-con{ | |||||
float: right; | |||||
height: auto; | |||||
padding-right: 20px; | |||||
line-height: 64px; | |||||
& > *{ | |||||
float: right; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,34 @@ | |||||
<template> | |||||
<div class="header-bar"> | |||||
<sider-trigger :collapsed="collapsed" icon="md-menu" @on-change="handleCollpasedChange"></sider-trigger> | |||||
<custom-bread-crumb show-icon style="margin-left: 30px;" :list="breadCrumbList"></custom-bread-crumb> | |||||
<div class="custom-content-con"> | |||||
<slot></slot> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import siderTrigger from './sider-trigger' | |||||
import customBreadCrumb from './custom-bread-crumb' | |||||
import './header-bar.less' | |||||
export default { | |||||
name: 'HeaderBar', | |||||
components: { | |||||
siderTrigger, | |||||
customBreadCrumb | |||||
}, | |||||
props: { | |||||
collapsed: Boolean | |||||
}, | |||||
computed: { | |||||
breadCrumbList () { | |||||
return this.$store.state.app.breadCrumbList | |||||
} | |||||
}, | |||||
methods: { | |||||
handleCollpasedChange (state) { | |||||
this.$emit('on-coll-change', state) | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,2 @@ | |||||
import HeaderBar from './header-bar' | |||||
export default HeaderBar |
@@ -0,0 +1,2 @@ | |||||
import siderTrigger from './sider-trigger.vue' | |||||
export default siderTrigger |
@@ -0,0 +1,21 @@ | |||||
.trans{ | |||||
transition: transform .2s ease; | |||||
} | |||||
@size: 40px; | |||||
.sider-trigger-a{ | |||||
padding: 6px; | |||||
width: @size; | |||||
height: @size; | |||||
display: inline-block; | |||||
text-align: center; | |||||
color: #5c6b77; | |||||
margin-top: 12px; | |||||
i{ | |||||
.trans; | |||||
vertical-align: top; | |||||
} | |||||
&.collapsed i{ | |||||
transform: rotateZ(90deg); | |||||
.trans; | |||||
} | |||||
} |
@@ -0,0 +1,27 @@ | |||||
<template> | |||||
<a @click="handleChange" type="text" :class="['sider-trigger-a', collapsed ? 'collapsed' : '']"><Icon :type="icon" :size="size" /></a> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'siderTrigger', | |||||
props: { | |||||
collapsed: Boolean, | |||||
icon: { | |||||
type: String, | |||||
default: 'navicon-round' | |||||
}, | |||||
size: { | |||||
type: Number, | |||||
default: 26 | |||||
} | |||||
}, | |||||
methods: { | |||||
handleChange () { | |||||
this.$emit('on-change', !this.collapsed) | |||||
} | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
@import './sider-trigger.less'; | |||||
</style> |
@@ -0,0 +1,2 @@ | |||||
import Language from './language.vue' | |||||
export default Language |
@@ -0,0 +1,51 @@ | |||||
<template> | |||||
<div> | |||||
<Dropdown trigger="click" @on-click="selectLang"> | |||||
<a href="javascript:void(0)"> | |||||
{{ title }} | |||||
<Icon :size="18" type="md-arrow-dropdown" /> | |||||
</a> | |||||
<DropdownMenu slot="list"> | |||||
<DropdownItem v-for="(value, key) in localList" :name="key" :key="`lang-${key}`">{{ value }}</DropdownItem> | |||||
</DropdownMenu> | |||||
</Dropdown> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'Language', | |||||
props: { | |||||
lang: String | |||||
}, | |||||
data () { | |||||
return { | |||||
langList: { | |||||
'zh-CN': '语言', | |||||
'zh-TW': '語言', | |||||
'en-US': 'Lang' | |||||
}, | |||||
localList: { | |||||
'zh-CN': '中文简体', | |||||
'zh-TW': '中文繁体', | |||||
'en-US': 'English' | |||||
} | |||||
} | |||||
}, | |||||
watch: { | |||||
lang (lang) { | |||||
this.$i18n.locale = lang | |||||
} | |||||
}, | |||||
computed: { | |||||
title () { | |||||
return this.langList[this.lang] | |||||
} | |||||
}, | |||||
methods: { | |||||
selectLang (name) { | |||||
this.$emit('on-lang-change', name) | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,51 @@ | |||||
<template> | |||||
<Dropdown ref="dropdown" @on-click="handleClick" :class="hideTitle ? '' : 'collased-menu-dropdown'" :transfer="hideTitle" :placement="placement"> | |||||
<a class="drop-menu-a" type="text" @mouseover="handleMousemove($event, children)" :style="{textAlign: !hideTitle ? 'left' : ''}"><common-icon :size="rootIconSize" :color="textColor" :type="parentItem.icon"/><span class="menu-title" v-if="!hideTitle">{{ showTitle(parentItem) }}</span><Icon style="float: right;" v-if="!hideTitle" type="ios-arrow-forward" :size="16"/></a> | |||||
<DropdownMenu ref="dropdown" slot="list"> | |||||
<template v-for="child in children"> | |||||
<collapsed-menu v-if="showChildren(child)" :icon-size="iconSize" :parent-item="child" :key="`drop-${child.name}`"></collapsed-menu> | |||||
<DropdownItem v-else :key="`drop-${child.name}`" :name="child.name"><common-icon :size="iconSize" :type="child.icon"/><span class="menu-title">{{ showTitle(child) }}</span></DropdownItem> | |||||
</template> | |||||
</DropdownMenu> | |||||
</Dropdown> | |||||
</template> | |||||
<script> | |||||
import mixin from './mixin' | |||||
import itemMixin from './item-mixin' | |||||
import { findNodeUpperByClasses } from '@/libs/util' | |||||
export default { | |||||
name: 'CollapsedMenu', | |||||
mixins: [ mixin, itemMixin ], | |||||
props: { | |||||
hideTitle: { | |||||
type: Boolean, | |||||
default: false | |||||
}, | |||||
rootIconSize: { | |||||
type: Number, | |||||
default: 16 | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
placement: 'right-end' | |||||
} | |||||
}, | |||||
methods: { | |||||
handleClick (name) { | |||||
this.$emit('on-click', name) | |||||
}, | |||||
handleMousemove (event, children) { | |||||
const { pageY } = event | |||||
const height = children.length * 38 | |||||
const isOverflow = pageY + height < window.innerHeight | |||||
this.placement = isOverflow ? 'right-start' : 'right-end' | |||||
} | |||||
}, | |||||
mounted () { | |||||
let dropdown = findNodeUpperByClasses(this.$refs.dropdown.$el, ['ivu-select-dropdown', 'ivu-dropdown-transfer']) | |||||
if (dropdown) dropdown.style.overflow = 'visible' | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,2 @@ | |||||
import SideMenu from './side-menu.vue' | |||||
export default SideMenu |
@@ -0,0 +1,21 @@ | |||||
export default { | |||||
props: { | |||||
parentItem: { | |||||
type: Object, | |||||
default: () => {} | |||||
}, | |||||
theme: String, | |||||
iconSize: Number | |||||
}, | |||||
computed: { | |||||
parentName () { | |||||
return this.parentItem.name | |||||
}, | |||||
children () { | |||||
return this.parentItem.children | |||||
}, | |||||
textColor () { | |||||
return this.theme === 'dark' ? '#fff' : '#495060' | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,18 @@ | |||||
import CommonIcon from '_c/common-icon' | |||||
import { showTitle } from '@/libs/util' | |||||
export default { | |||||
components: { | |||||
CommonIcon | |||||
}, | |||||
methods: { | |||||
showTitle (item) { | |||||
return showTitle(item, this) | |||||
}, | |||||
showChildren (item) { | |||||
return item.children && (item.children.length > 1 || (item.meta && item.meta.showAlways)) | |||||
}, | |||||
getNameOrHref (item, children0) { | |||||
return item.href ? `isTurnByHref_${item.href}` : (children0 ? item.children[0].name : item.name) | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,26 @@ | |||||
<template> | |||||
<Submenu :name="`${parentName}`"> | |||||
<template slot="title"> | |||||
<common-icon :type="parentItem.icon || ''"/> | |||||
<span>{{ showTitle(parentItem) }}</span> | |||||
</template> | |||||
<template v-for="item in children"> | |||||
<template v-if="item.children && item.children.length === 1"> | |||||
<side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item> | |||||
<menu-item v-else :name="getNameOrHref(item, true)" :key="`menu-${item.children[0].name}`"><common-icon :type="item.children[0].icon || ''"/><span>{{ showTitle(item.children[0]) }}</span></menu-item> | |||||
</template> | |||||
<template v-else> | |||||
<side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item> | |||||
<menu-item v-else :name="getNameOrHref(item)" :key="`menu-${item.name}`"><common-icon :type="item.icon || ''"/><span>{{ showTitle(item) }}</span></menu-item> | |||||
</template> | |||||
</template> | |||||
</Submenu> | |||||
</template> | |||||
<script> | |||||
import mixin from './mixin' | |||||
import itemMixin from './item-mixin' | |||||
export default { | |||||
name: 'SideMenuItem', | |||||
mixins: [ mixin, itemMixin ] | |||||
} | |||||
</script> |
@@ -0,0 +1,40 @@ | |||||
.side-menu-wrapper{ | |||||
user-select: none; | |||||
.menu-collapsed{ | |||||
padding-top: 10px; | |||||
.ivu-dropdown{ | |||||
width: 100%; | |||||
.ivu-dropdown-rel a{ | |||||
width: 100%; | |||||
} | |||||
} | |||||
.ivu-tooltip{ | |||||
width: 100%; | |||||
.ivu-tooltip-rel{ | |||||
width: 100%; | |||||
} | |||||
.ivu-tooltip-popper .ivu-tooltip-content{ | |||||
.ivu-tooltip-arrow{ | |||||
border-right-color: #fff; | |||||
} | |||||
.ivu-tooltip-inner{ | |||||
background: #fff; | |||||
color: #495060; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
a.drop-menu-a{ | |||||
display: inline-block; | |||||
padding: 6px 15px; | |||||
width: 100%; | |||||
text-align: center; | |||||
color: #495060; | |||||
} | |||||
} | |||||
.menu-title{ | |||||
padding-left: 6px; | |||||
} |
@@ -0,0 +1,114 @@ | |||||
<template> | |||||
<div class="side-menu-wrapper"> | |||||
<slot></slot> | |||||
<Menu ref="menu" v-show="!collapsed" :active-name="activeName" :open-names="openedNames" :accordion="accordion" :theme="theme" width="auto" @on-select="handleSelect"> | |||||
<template v-for="item in menuList"> | |||||
<template v-if="item.children && item.children.length === 1"> | |||||
<side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item> | |||||
<menu-item v-else :name="getNameOrHref(item, true)" :key="`menu-${item.children[0].name}`"><common-icon :type="item.children[0].icon || ''"/><span>{{ showTitle(item.children[0]) }}</span></menu-item> | |||||
</template> | |||||
<template v-else> | |||||
<side-menu-item v-if="showChildren(item)" :key="`menu-${item.name}`" :parent-item="item"></side-menu-item> | |||||
<menu-item v-else :name="getNameOrHref(item)" :key="`menu-${item.name}`"><common-icon :type="item.icon || ''"/><span>{{ showTitle(item) }}</span></menu-item> | |||||
</template> | |||||
</template> | |||||
</Menu> | |||||
<div class="menu-collapsed" v-show="collapsed" :list="menuList"> | |||||
<template v-for="item in menuList"> | |||||
<collapsed-menu v-if="item.children && item.children.length > 1" @on-click="handleSelect" hide-title :root-icon-size="rootIconSize" :icon-size="iconSize" :theme="theme" :parent-item="item" :key="`drop-menu-${item.name}`"></collapsed-menu> | |||||
<Tooltip transfer v-else :content="showTitle(item.children && item.children[0] ? item.children[0] : item)" placement="right" :key="`drop-menu-${item.name}`"> | |||||
<a @click="handleSelect(getNameOrHref(item, true))" class="drop-menu-a" :style="{textAlign: 'center'}"><common-icon :size="rootIconSize" :color="textColor" :type="item.icon || (item.children && item.children[0].icon)"/></a> | |||||
</Tooltip> | |||||
</template> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import SideMenuItem from './side-menu-item.vue' | |||||
import CollapsedMenu from './collapsed-menu.vue' | |||||
import { getUnion } from '@/libs/tools' | |||||
import mixin from './mixin' | |||||
export default { | |||||
name: 'SideMenu', | |||||
mixins: [ mixin ], | |||||
components: { | |||||
SideMenuItem, | |||||
CollapsedMenu | |||||
}, | |||||
props: { | |||||
menuList: { | |||||
type: Array, | |||||
default () { | |||||
return [] | |||||
} | |||||
}, | |||||
collapsed: { | |||||
type: Boolean | |||||
}, | |||||
theme: { | |||||
type: String, | |||||
default: 'dark' | |||||
}, | |||||
rootIconSize: { | |||||
type: Number, | |||||
default: 20 | |||||
}, | |||||
iconSize: { | |||||
type: Number, | |||||
default: 16 | |||||
}, | |||||
accordion: Boolean, | |||||
activeName: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
openNames: { | |||||
type: Array, | |||||
default: () => [] | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
openedNames: [] | |||||
} | |||||
}, | |||||
methods: { | |||||
handleSelect (name) { | |||||
this.$emit('on-select', name) | |||||
}, | |||||
getOpenedNamesByActiveName (name) { | |||||
return this.$route.matched.map(item => item.name).filter(item => item !== name) | |||||
}, | |||||
updateOpenName (name) { | |||||
if (name === this.$config.homeName) this.openedNames = [] | |||||
else this.openedNames = this.getOpenedNamesByActiveName(name) | |||||
} | |||||
}, | |||||
computed: { | |||||
textColor () { | |||||
return this.theme === 'dark' ? '#fff' : '#495060' | |||||
} | |||||
}, | |||||
watch: { | |||||
activeName (name) { | |||||
if (this.accordion) this.openedNames = this.getOpenedNamesByActiveName(name) | |||||
else this.openedNames = getUnion(this.openedNames, this.getOpenedNamesByActiveName(name)) | |||||
}, | |||||
openNames (newNames) { | |||||
this.openedNames = newNames | |||||
}, | |||||
openedNames () { | |||||
this.$nextTick(() => { | |||||
this.$refs.menu.updateOpened() | |||||
}) | |||||
} | |||||
}, | |||||
mounted () { | |||||
this.openedNames = getUnion(this.openedNames, this.getOpenedNamesByActiveName(name)) | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
@import './side-menu.less'; | |||||
</style> |
@@ -0,0 +1,2 @@ | |||||
import TagsNav from './tags-nav.vue' | |||||
export default TagsNav |
@@ -0,0 +1,87 @@ | |||||
.no-select{ | |||||
-webkit-touch-callout: none; | |||||
-webkit-user-select: none; | |||||
-khtml-user-select: none; | |||||
-moz-user-select: none; | |||||
-ms-user-select: none; | |||||
user-select: none; | |||||
} | |||||
.size{ | |||||
width: 100%; | |||||
height: 100%; | |||||
} | |||||
.tags-nav{ | |||||
position: relative; | |||||
border-top: 1px solid #F0F0F0; | |||||
border-bottom: 1px solid #F0F0F0; | |||||
.no-select; | |||||
.size; | |||||
.close-con{ | |||||
position: absolute; | |||||
right: 0; | |||||
top: 0; | |||||
height: 100%; | |||||
width: 32px; | |||||
background: #fff; | |||||
text-align: center; | |||||
z-index: 10; | |||||
} | |||||
.btn-con{ | |||||
position: absolute; | |||||
top: 0px; | |||||
height: 100%; | |||||
background: #fff; | |||||
padding-top: 3px; | |||||
z-index: 10; | |||||
button{ | |||||
padding: 6px 4px; | |||||
line-height: 14px; | |||||
text-align: center; | |||||
} | |||||
&.left-btn{ | |||||
left: 0px; | |||||
} | |||||
&.right-btn{ | |||||
right: 32px; | |||||
border-right: 1px solid #F0F0F0; | |||||
} | |||||
} | |||||
.scroll-outer{ | |||||
position: absolute; | |||||
left: 28px; | |||||
right: 61px; | |||||
top: 0; | |||||
bottom: 0; | |||||
box-shadow: 0px 0 3px 2px rgba(100,100,100,.1) inset; | |||||
.scroll-body{ | |||||
height: ~"calc(100% - 1px)"; | |||||
display: inline-block; | |||||
padding: 1px 4px 0; | |||||
position: absolute; | |||||
overflow: visible; | |||||
white-space: nowrap; | |||||
transition: left .3s ease; | |||||
.ivu-tag-dot-inner{ | |||||
transition: background .2s ease; | |||||
} | |||||
} | |||||
} | |||||
.contextmenu { | |||||
position: absolute; | |||||
margin: 0; | |||||
padding: 5px 0; | |||||
background: #fff; | |||||
z-index: 100; | |||||
list-style-type: none; | |||||
border-radius: 4px; | |||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); | |||||
li { | |||||
margin: 0; | |||||
padding: 5px 15px; | |||||
cursor: pointer; | |||||
&:hover { | |||||
background: #eee; | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,209 @@ | |||||
<template> | |||||
<div class="tags-nav"> | |||||
<div class="close-con"> | |||||
<Dropdown transfer @on-click="handleTagsOption" style="margin-top:7px;"> | |||||
<Button size="small" type="text"> | |||||
<Icon :size="18" type="ios-close-circle-outline" /> | |||||
</Button> | |||||
<DropdownMenu slot="list"> | |||||
<DropdownItem name="close-all">关闭所有</DropdownItem> | |||||
<DropdownItem name="close-others">关闭其他</DropdownItem> | |||||
</DropdownMenu> | |||||
</Dropdown> | |||||
</div> | |||||
<ul v-show="visible" :style="{left: contextMenuLeft + 'px', top: contextMenuTop + 'px'}" class="contextmenu"> | |||||
<li v-for="(item, key) of menuList" @click="handleTagsOption(key)" :key="key">{{item}}</li> | |||||
</ul> | |||||
<div class="btn-con left-btn"> | |||||
<Button type="text" @click="handleScroll(240)"> | |||||
<Icon :size="18" type="ios-arrow-back" /> | |||||
</Button> | |||||
</div> | |||||
<div class="btn-con right-btn"> | |||||
<Button type="text" @click="handleScroll(-240)"> | |||||
<Icon :size="18" type="ios-arrow-forward" /> | |||||
</Button> | |||||
</div> | |||||
<div class="scroll-outer" ref="scrollOuter" @DOMMouseScroll="handlescroll" @mousewheel="handlescroll"> | |||||
<div ref="scrollBody" class="scroll-body" :style="{left: tagBodyLeft + 'px'}"> | |||||
<transition-group name="taglist-moving-animation"> | |||||
<Tag | |||||
type="dot" | |||||
v-for="(item, index) in list" | |||||
ref="tagsPageOpened" | |||||
:key="`tag-nav-${index}`" | |||||
:name="item.name" | |||||
:data-route-item="item" | |||||
@on-close="handleClose(item)" | |||||
@click.native="handleClick(item)" | |||||
:closable="item.name !== $config.homeName" | |||||
:color="isCurrentTag(item) ? 'primary' : 'default'" | |||||
@contextmenu.prevent.native="contextMenu(item, $event)" | |||||
>{{ showTitleInside(item) }}</Tag> | |||||
</transition-group> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { showTitle, routeEqual } from '@/libs/util' | |||||
import beforeClose from '@/router/before-close' | |||||
export default { | |||||
name: 'TagsNav', | |||||
props: { | |||||
value: Object, | |||||
list: { | |||||
type: Array, | |||||
default () { | |||||
return [] | |||||
} | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
tagBodyLeft: 0, | |||||
rightOffset: 40, | |||||
outerPadding: 4, | |||||
contextMenuLeft: 0, | |||||
contextMenuTop: 0, | |||||
visible: false, | |||||
menuList: { | |||||
others: '关闭其他', | |||||
all: '关闭所有' | |||||
} | |||||
} | |||||
}, | |||||
computed: { | |||||
currentRouteObj () { | |||||
const { name, params, query } = this.value | |||||
return { name, params, query } | |||||
} | |||||
}, | |||||
methods: { | |||||
handlescroll (e) { | |||||
var type = e.type | |||||
let delta = 0 | |||||
if (type === 'DOMMouseScroll' || type === 'mousewheel') { | |||||
delta = (e.wheelDelta) ? e.wheelDelta : -(e.detail || 0) * 40 | |||||
} | |||||
this.handleScroll(delta) | |||||
}, | |||||
handleScroll (offset) { | |||||
const outerWidth = this.$refs.scrollOuter.offsetWidth | |||||
const bodyWidth = this.$refs.scrollBody.offsetWidth | |||||
if (offset > 0) { | |||||
this.tagBodyLeft = Math.min(0, this.tagBodyLeft + offset) | |||||
} else { | |||||
if (outerWidth < bodyWidth) { | |||||
if (this.tagBodyLeft < -(bodyWidth - outerWidth)) { | |||||
this.tagBodyLeft = this.tagBodyLeft | |||||
} else { | |||||
this.tagBodyLeft = Math.max(this.tagBodyLeft + offset, outerWidth - bodyWidth) | |||||
} | |||||
} else { | |||||
this.tagBodyLeft = 0 | |||||
} | |||||
} | |||||
}, | |||||
handleTagsOption (type) { | |||||
if (type.includes('all')) { | |||||
// 关闭所有,除了home | |||||
let res = this.list.filter(item => item.name === this.$config.homeName) | |||||
this.$emit('on-close', res, 'all') | |||||
} else if (type.includes('others')) { | |||||
// 关闭除当前页和home页的其他页 | |||||
let res = this.list.filter(item => routeEqual(this.currentRouteObj, item) || item.name === this.$config.homeName) | |||||
this.$emit('on-close', res, 'others', this.currentRouteObj) | |||||
setTimeout(() => { | |||||
this.getTagElementByName(this.currentRouteObj.name) | |||||
}, 100) | |||||
} | |||||
}, | |||||
handleClose (current) { | |||||
if (current.meta && current.meta.beforeCloseName && current.meta.beforeCloseName in beforeClose) { | |||||
new Promise(beforeClose[current.meta.beforeCloseName]).then(close => { | |||||
if (close) { | |||||
this.close(current) | |||||
} | |||||
}) | |||||
} else { | |||||
this.close(current) | |||||
} | |||||
}, | |||||
close (route) { | |||||
let res = this.list.filter(item => !routeEqual(route, item)) | |||||
this.$emit('on-close', res, undefined, route) | |||||
}, | |||||
handleClick (item) { | |||||
this.$emit('input', item) | |||||
}, | |||||
showTitleInside (item) { | |||||
return showTitle(item, this) | |||||
}, | |||||
isCurrentTag (item) { | |||||
return routeEqual(this.currentRouteObj, item) | |||||
}, | |||||
moveToView (tag) { | |||||
const outerWidth = this.$refs.scrollOuter.offsetWidth | |||||
const bodyWidth = this.$refs.scrollBody.offsetWidth | |||||
if (bodyWidth < outerWidth) { | |||||
this.tagBodyLeft = 0 | |||||
} else if (tag.offsetLeft < -this.tagBodyLeft) { | |||||
// 标签在可视区域左侧 | |||||
this.tagBodyLeft = -tag.offsetLeft + this.outerPadding | |||||
} else if (tag.offsetLeft > -this.tagBodyLeft && tag.offsetLeft + tag.offsetWidth < -this.tagBodyLeft + outerWidth) { | |||||
// 标签在可视区域 | |||||
this.tagBodyLeft = Math.min(0, outerWidth - tag.offsetWidth - tag.offsetLeft - this.outerPadding) | |||||
} else { | |||||
// 标签在可视区域右侧 | |||||
this.tagBodyLeft = -(tag.offsetLeft - (outerWidth - this.outerPadding - tag.offsetWidth)) | |||||
} | |||||
}, | |||||
getTagElementByName (route) { | |||||
this.$nextTick(() => { | |||||
this.refsTag = this.$refs.tagsPageOpened | |||||
this.refsTag.forEach((item, index) => { | |||||
if (routeEqual(route, item.$attrs['data-route-item'])) { | |||||
let tag = this.refsTag[index].$el | |||||
this.moveToView(tag) | |||||
} | |||||
}) | |||||
}) | |||||
}, | |||||
contextMenu (item, e) { | |||||
if (item.name === this.$config.homeName) { | |||||
return | |||||
} | |||||
this.visible = true | |||||
const offsetLeft = this.$el.getBoundingClientRect().left | |||||
this.contextMenuLeft = e.clientX - offsetLeft + 10 | |||||
this.contextMenuTop = e.clientY - 64 | |||||
}, | |||||
closeMenu () { | |||||
this.visible = false | |||||
} | |||||
}, | |||||
watch: { | |||||
'$route' (to) { | |||||
this.getTagElementByName(to) | |||||
}, | |||||
visible (value) { | |||||
if (value) { | |||||
document.body.addEventListener('click', this.closeMenu) | |||||
} else { | |||||
document.body.removeEventListener('click', this.closeMenu) | |||||
} | |||||
} | |||||
}, | |||||
mounted () { | |||||
setTimeout(() => { | |||||
this.getTagElementByName(this.$route) | |||||
}, 200) | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
@import './tags-nav.less'; | |||||
</style> |
@@ -0,0 +1,2 @@ | |||||
import User from './user.vue' | |||||
export default User |
@@ -0,0 +1,12 @@ | |||||
.user{ | |||||
&-avator-dropdown{ | |||||
cursor: pointer; | |||||
display: inline-block; | |||||
// height: 64px; | |||||
vertical-align: middle; | |||||
// line-height: 64px; | |||||
.ivu-badge-dot{ | |||||
top: 16px; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,59 @@ | |||||
<template> | |||||
<div class="user-avator-dropdown"> | |||||
<Dropdown @on-click="handleClick"> | |||||
<Badge :dot="!!messageUnreadCount"> | |||||
<Avatar :src="userAvator"/> | |||||
</Badge> | |||||
<Icon :size="18" type="md-arrow-dropdown"></Icon> | |||||
<DropdownMenu slot="list"> | |||||
<DropdownItem name="message"> | |||||
消息中心<Badge style="margin-left: 10px" :count="messageUnreadCount"></Badge> | |||||
</DropdownItem> | |||||
<DropdownItem name="logout">退出登录</DropdownItem> | |||||
</DropdownMenu> | |||||
</Dropdown> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import './user.less' | |||||
import { mapActions } from 'vuex' | |||||
export default { | |||||
name: 'User', | |||||
props: { | |||||
userAvator: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
messageUnreadCount: { | |||||
type: Number, | |||||
default: 0 | |||||
} | |||||
}, | |||||
methods: { | |||||
...mapActions([ | |||||
'handleLogOut' | |||||
]), | |||||
logout () { | |||||
this.handleLogOut().then(() => { | |||||
this.$router.push({ | |||||
name: 'login' | |||||
}) | |||||
}) | |||||
}, | |||||
message () { | |||||
this.$router.push({ | |||||
name: 'message_page' | |||||
}) | |||||
}, | |||||
handleClick (name) { | |||||
switch (name) { | |||||
case 'logout': this.logout() | |||||
break | |||||
case 'message': this.message() | |||||
break | |||||
} | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,2 @@ | |||||
import Main from './main.vue' | |||||
export default Main |
@@ -0,0 +1,75 @@ | |||||
.main{ | |||||
.logo-con{ | |||||
height: 64px; | |||||
padding: 10px; | |||||
img{ | |||||
height: 44px; | |||||
width: auto; | |||||
display: block; | |||||
margin: 0 auto; | |||||
} | |||||
} | |||||
.header-con{ | |||||
background: #fff; | |||||
padding: 0 20px; | |||||
width: 100%; | |||||
} | |||||
.main-layout-con{ | |||||
height: 100%; | |||||
overflow: hidden; | |||||
} | |||||
.main-content-con{ | |||||
height: ~"calc(100% - 60px)"; | |||||
overflow: hidden; | |||||
} | |||||
.tag-nav-wrapper{ | |||||
padding: 0; | |||||
height:40px; | |||||
background:#F0F0F0; | |||||
} | |||||
.content-wrapper{ | |||||
padding: 18px; | |||||
height: ~"calc(100% - 80px)"; | |||||
overflow: auto; | |||||
} | |||||
.left-sider{ | |||||
.ivu-layout-sider-children{ | |||||
overflow-y: scroll; | |||||
margin-right: -18px; | |||||
} | |||||
} | |||||
} | |||||
.ivu-menu-item > i{ | |||||
margin-right: 12px !important; | |||||
} | |||||
.ivu-menu-submenu > .ivu-menu > .ivu-menu-item > i { | |||||
margin-right: 8px !important; | |||||
} | |||||
.collased-menu-dropdown{ | |||||
width: 100%; | |||||
margin: 0; | |||||
line-height: normal; | |||||
padding: 7px 0 6px 16px; | |||||
clear: both; | |||||
font-size: 12px !important; | |||||
white-space: nowrap; | |||||
list-style: none; | |||||
cursor: pointer; | |||||
transition: background 0.2s ease-in-out; | |||||
&:hover{ | |||||
background: rgba(100, 100, 100, 0.1); | |||||
} | |||||
& * { | |||||
color: #515a6e; | |||||
} | |||||
.ivu-menu-item > i{ | |||||
margin-right: 12px !important; | |||||
} | |||||
.ivu-menu-submenu > .ivu-menu > .ivu-menu-item > i { | |||||
margin-right: 8px !important; | |||||
} | |||||
} | |||||
.ivu-select-dropdown.ivu-dropdown-transfer{ | |||||
max-height: 400px; | |||||
} |
@@ -0,0 +1,183 @@ | |||||
<template> | |||||
<Layout style="height: 100%" class="main"> | |||||
<Sider hide-trigger collapsible :width="256" :collapsed-width="64" v-model="collapsed" class="left-sider" :style="{overflow: 'hidden'}"> | |||||
<side-menu accordion ref="sideMenu" :active-name="$route.name" :collapsed="collapsed" @on-select="turnToPage" :menu-list="menuList"> | |||||
<!-- 需要放在菜单上面的内容,如Logo,写在side-menu标签内部,如下 --> | |||||
<div class="logo-con"> | |||||
<img v-show="!collapsed" :src="maxLogo" key="max-logo" /> | |||||
<img v-show="collapsed" :src="minLogo" key="min-logo" /> | |||||
</div> | |||||
</side-menu> | |||||
</Sider> | |||||
<Layout> | |||||
<Header class="header-con"> | |||||
<header-bar :collapsed="collapsed" @on-coll-change="handleCollapsedChange"> | |||||
<user :message-unread-count="unreadCount" :user-avator="userAvator"/> | |||||
<language v-if="$config.useI18n" @on-lang-change="setLocal" style="margin-right: 10px;" :lang="local"/> | |||||
<error-store v-if="$config.plugin['error-store'] && $config.plugin['error-store'].showInHeader" :has-read="hasReadErrorPage" :count="errorCount"></error-store> | |||||
<fullscreen v-model="isFullscreen" style="margin-right: 10px;"/> | |||||
</header-bar> | |||||
</Header> | |||||
<Content class="main-content-con"> | |||||
<Layout class="main-layout-con"> | |||||
<div class="tag-nav-wrapper"> | |||||
<tags-nav :value="$route" @input="handleClick" :list="tagNavList" @on-close="handleCloseTag"/> | |||||
</div> | |||||
<Content class="content-wrapper"> | |||||
<keep-alive :include="cacheList"> | |||||
<router-view/> | |||||
</keep-alive> | |||||
<ABackTop :height="100" :bottom="80" :right="50" container=".content-wrapper"></ABackTop> | |||||
</Content> | |||||
</Layout> | |||||
</Content> | |||||
</Layout> | |||||
</Layout> | |||||
</template> | |||||
<script> | |||||
import SideMenu from './components/side-menu' | |||||
import HeaderBar from './components/header-bar' | |||||
import TagsNav from './components/tags-nav' | |||||
import User from './components/user' | |||||
import ABackTop from './components/a-back-top' | |||||
import Fullscreen from './components/fullscreen' | |||||
import Language from './components/language' | |||||
import ErrorStore from './components/error-store' | |||||
import { mapMutations, mapActions, mapGetters } from 'vuex' | |||||
import { getNewTagList, getNextRoute, routeEqual } from '@/libs/util' | |||||
import routers from '@/router/routers' | |||||
import minLogo from '@/assets/images/logo-min.jpg' | |||||
import maxLogo from '@/assets/images/logo.jpg' | |||||
import './main.less' | |||||
export default { | |||||
name: 'Main', | |||||
components: { | |||||
SideMenu, | |||||
HeaderBar, | |||||
Language, | |||||
TagsNav, | |||||
Fullscreen, | |||||
ErrorStore, | |||||
User, | |||||
ABackTop | |||||
}, | |||||
data () { | |||||
return { | |||||
collapsed: false, | |||||
minLogo, | |||||
maxLogo, | |||||
isFullscreen: false | |||||
} | |||||
}, | |||||
computed: { | |||||
...mapGetters([ | |||||
'errorCount' | |||||
]), | |||||
tagNavList () { | |||||
return this.$store.state.app.tagNavList | |||||
}, | |||||
tagRouter () { | |||||
return this.$store.state.app.tagRouter | |||||
}, | |||||
userAvator () { | |||||
return this.$store.state.user.avatorImgPath | |||||
}, | |||||
cacheList () { | |||||
return ['ParentView', ...this.tagNavList.length ? this.tagNavList.filter(item => !(item.meta && item.meta.notCache)).map(item => item.name) : []] | |||||
}, | |||||
menuList () { | |||||
return this.$store.getters.menuList | |||||
}, | |||||
local () { | |||||
return this.$store.state.app.local | |||||
}, | |||||
hasReadErrorPage () { | |||||
return this.$store.state.app.hasReadErrorPage | |||||
}, | |||||
unreadCount () { | |||||
return this.$store.state.user.unreadCount | |||||
} | |||||
}, | |||||
methods: { | |||||
...mapMutations([ | |||||
'setBreadCrumb', | |||||
'setTagNavList', | |||||
'addTag', | |||||
'setLocal', | |||||
'setHomeRoute' | |||||
]), | |||||
...mapActions([ | |||||
'handleLogin', | |||||
'getUnreadMessageCount' | |||||
]), | |||||
turnToPage (route) { | |||||
let { name, params, query } = {} | |||||
if (typeof route === 'string') name = route | |||||
else { | |||||
name = route.name | |||||
params = route.params | |||||
query = route.query | |||||
} | |||||
if (name.indexOf('isTurnByHref_') > -1) { | |||||
window.open(name.split('_')[1]) | |||||
return | |||||
} | |||||
this.$router.push({ | |||||
name, | |||||
params, | |||||
query | |||||
}) | |||||
}, | |||||
handleCollapsedChange (state) { | |||||
this.collapsed = state | |||||
}, | |||||
handleCloseTag (res, type, route) { | |||||
if (type === 'all') { | |||||
this.turnToPage(this.$config.homeName) | |||||
} else if (routeEqual(this.$route, route)) { | |||||
if (type !== 'others') { | |||||
const nextRoute = getNextRoute(this.tagNavList, route) | |||||
this.$router.push(nextRoute) | |||||
} | |||||
} | |||||
this.setTagNavList(res) | |||||
}, | |||||
handleClick (item) { | |||||
this.turnToPage(item) | |||||
} | |||||
}, | |||||
watch: { | |||||
'$route' (newRoute) { | |||||
const { name, query, params, meta } = newRoute | |||||
this.addTag({ | |||||
route: { name, query, params, meta }, | |||||
type: 'push' | |||||
}) | |||||
this.setBreadCrumb(newRoute) | |||||
this.setTagNavList(getNewTagList(this.tagNavList, newRoute)) | |||||
this.$refs.sideMenu.updateOpenName(newRoute.name) | |||||
} | |||||
}, | |||||
mounted () { | |||||
/** | |||||
* @description 初始化设置面包屑导航和标签导航 | |||||
*/ | |||||
this.setTagNavList() | |||||
this.setHomeRoute(routers) | |||||
this.addTag({ | |||||
route: this.$store.state.app.homeRoute | |||||
}) | |||||
this.setBreadCrumb(this.$route) | |||||
// 设置初始语言 | |||||
this.setLocal(this.$i18n.locale) | |||||
// 如果当前打开页面不在标签栏中,跳到homeName页 | |||||
if (!this.tagNavList.find(item => item.name === this.$route.name)) { | |||||
this.$router.push({ | |||||
name: this.$config.homeName | |||||
}) | |||||
} | |||||
// 获取未读消息条数 | |||||
this.getUnreadMessageCount() | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,2 @@ | |||||
import MarkdownEditor from './markdown.vue' | |||||
export default MarkdownEditor |
@@ -0,0 +1,76 @@ | |||||
<template> | |||||
<div class="markdown-wrapper"> | |||||
<textarea ref="editor"></textarea> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import Simplemde from 'simplemde' | |||||
import 'simplemde/dist/simplemde.min.css' | |||||
export default { | |||||
name: 'MarkdownEditor', | |||||
props: { | |||||
value: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
options: { | |||||
type: Object, | |||||
default: () => { | |||||
return {} | |||||
} | |||||
}, | |||||
localCache: { | |||||
type: Boolean, | |||||
default: true | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
editor: null | |||||
} | |||||
}, | |||||
methods: { | |||||
addEvents () { | |||||
this.editor.codemirror.on('change', () => { | |||||
let value = this.editor.value() | |||||
if (this.localCache) localStorage.markdownContent = value | |||||
this.$emit('input', value) | |||||
this.$emit('on-change', value) | |||||
}) | |||||
this.editor.codemirror.on('focus', () => { | |||||
this.$emit('on-focus', this.editor.value()) | |||||
}) | |||||
this.editor.codemirror.on('blur', () => { | |||||
this.$emit('on-blur', this.editor.value()) | |||||
}) | |||||
} | |||||
}, | |||||
mounted () { | |||||
this.editor = new Simplemde(Object.assign(this.options, { | |||||
element: this.$refs.editor | |||||
})) | |||||
/** | |||||
* 事件列表为Codemirror编辑器的事件,更多事件类型,请参考: | |||||
* https://codemirror.net/doc/manual.html#events | |||||
*/ | |||||
this.addEvents() | |||||
let content = localStorage.markdownContent | |||||
if (content) this.editor.value(content) | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
.markdown-wrapper{ | |||||
.editor-toolbar.fullscreen{ | |||||
z-index: 9999; | |||||
} | |||||
.CodeMirror-fullscreen{ | |||||
z-index: 9999; | |||||
} | |||||
.CodeMirror-fullscreen ~ .editor-preview-side{ | |||||
z-index: 9999; | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,2 @@ | |||||
import ParentView from './parent-view.vue' | |||||
export default ParentView |
@@ -0,0 +1,20 @@ | |||||
<template> | |||||
<keep-alive :exclude="notCacheName"> | |||||
<router-view ref="child"/> | |||||
</keep-alive> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'ParentView', | |||||
data () { | |||||
return { | |||||
cacheChaildName: '' | |||||
} | |||||
}, | |||||
computed: { | |||||
notCacheName () { | |||||
return (this.$route.meta && this.$route.meta.notCache) ? this.$route.name : '' | |||||
} | |||||
} | |||||
} | |||||
</script> |
@@ -0,0 +1,2 @@ | |||||
import PasteEditor from './paste-editor.vue' | |||||
export default PasteEditor |
@@ -0,0 +1,26 @@ | |||||
.paste-editor-wrapper{ | |||||
width: 100%; | |||||
height: 100%; | |||||
border: 1px dashed gainsboro; | |||||
textarea.textarea-el{ | |||||
width: 100%; | |||||
height: 100%; | |||||
} | |||||
.CodeMirror{ | |||||
height: 100%; | |||||
padding: 0; | |||||
.CodeMirror-code div .CodeMirror-line > span > span.cm-tab{ | |||||
&::after{ | |||||
content: '→'; | |||||
color: #BFBFBF; | |||||
} | |||||
} | |||||
} | |||||
.first-row{ | |||||
font-weight: 700; | |||||
font-size: 14px; | |||||
} | |||||
.incorrect-row{ | |||||
background: #F5CBD1; | |||||
} | |||||
} |
@@ -0,0 +1,115 @@ | |||||
<template> | |||||
<div class="paste-editor-wrapper"> | |||||
<textarea ref="codemirror" class="textarea-el"></textarea> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import CodeMirror from 'codemirror' | |||||
import 'codemirror/lib/codemirror.css' | |||||
import { forEach } from '@/libs/tools' | |||||
import createPlaceholder from './plugins/placeholder' | |||||
export default { | |||||
name: 'PasteEditor', | |||||
props: { | |||||
value: Array, | |||||
pasteData: { | |||||
type: String, | |||||
default: '' | |||||
}, | |||||
placeholder: { | |||||
type: String, | |||||
default: '从网页或其他应用软件复制表格数据,粘贴到这里 。默认第一行是表头,使用回车键添加新行,使用Tab键区分列。' | |||||
} | |||||
}, | |||||
data () { | |||||
return { | |||||
pasteDataArr: [], | |||||
rowArrLength: 0, | |||||
editor: null | |||||
} | |||||
}, | |||||
watch: { | |||||
pasteData (val) { | |||||
if (val === '') { | |||||
this.editor.setValue('') | |||||
} | |||||
} | |||||
}, | |||||
computed: { | |||||
rowNum () { | |||||
return this.pasteDataArr.length | |||||
}, | |||||
colNum () { | |||||
return this.pasteDataArr[0] ? this.pasteDataArr[0].length : 0 | |||||
} | |||||
}, | |||||
methods: { | |||||
handleKeyup (e) { | |||||
this.handleAreaData() | |||||
}, | |||||
/** | |||||
* @description 处理粘贴操作 | |||||
*/ | |||||
handleContentChanged (content) { | |||||
let pasteData = content.trim() | |||||
this.$emit('on-content-change', pasteData) | |||||
let rows = pasteData.split((/[\n\u0085\u2028\u2029]|\r\n?/g)).map(row => { | |||||
return row.split('\t') | |||||
}) | |||||
if (content === '') rows = [] | |||||
this.pasteDataArr = rows | |||||
this.clearLineClass() | |||||
this.checkColNumInEveryRow() | |||||
this.$emit('input', this.pasteDataArr) | |||||
}, | |||||
/** | |||||
* @description 检查除第一行的每一行列数是否与第一行相同 | |||||
*/ | |||||
checkColNumInEveryRow () { | |||||
let i = 0 | |||||
const len = this.rowNum | |||||
if (len === 0) return | |||||
while (++i < len) { | |||||
let item = this.pasteDataArr[i] | |||||
if (item.length !== this.colNum && (!(i === len - 1 && item.length === 1 && item[0] === '') || i !== len - 1)) { | |||||
this.markIncorrectRow(i) | |||||
this.$emit('on-error', i) | |||||
return false | |||||
} | |||||
} | |||||
this.$emit('on-success', this.pasteDataArr) | |||||
return true | |||||
}, | |||||
/** | |||||
* @description 标记不符合格式的一行 | |||||
*/ | |||||
markIncorrectRow (index) { | |||||
this.editor.addLineClass(index, 'text', 'incorrect-row') | |||||
}, | |||||
/** | |||||
* @description 标记不符合格式的一行 | |||||
*/ | |||||
clearLineClass () { | |||||
forEach(this.pasteDataArr, (item, index) => { | |||||
this.editor.removeLineClass(index, 'text', 'incorrect-row') | |||||
}) | |||||
} | |||||
}, | |||||
mounted () { | |||||
createPlaceholder(CodeMirror) | |||||
this.editor = CodeMirror.fromTextArea(this.$refs.codemirror, { | |||||
lineNumbers: true, | |||||
tabSize: 1, | |||||
lineWrapping: true, | |||||
placeholder: this.placeholder | |||||
}) | |||||
this.editor.on('change', (editor) => { | |||||
this.handleContentChanged(editor.getValue()) | |||||
}) | |||||
this.editor.addLineClass(0, 'text', 'first-row') | |||||
} | |||||
} | |||||
</script> | |||||
<style lang="less"> | |||||
@import './paste-editor.less'; | |||||
</style> |
@@ -0,0 +1,58 @@ | |||||
export default (codemirror) => { | |||||
(function (mod) { | |||||
mod(codemirror) | |||||
})(function (CodeMirror) { | |||||
CodeMirror.defineOption('placeholder', '', function (cm, val, old) { | |||||
var prev = old && old !== CodeMirror.Init | |||||
if (val && !prev) { | |||||
cm.on('blur', onBlur) | |||||
cm.on('change', onChange) | |||||
cm.on('swapDoc', onChange) | |||||
onChange(cm) | |||||
} else if (!val && prev) { | |||||
cm.off('blur', onBlur) | |||||
cm.off('change', onChange) | |||||
cm.off('swapDoc', onChange) | |||||
clearPlaceholder(cm) | |||||
var wrapper = cm.getWrapperElement() | |||||
wrapper.className = wrapper.className.replace(' CodeMirror-empty', '') | |||||
} | |||||
if (val && !cm.hasFocus()) onBlur(cm) | |||||
}) | |||||
function clearPlaceholder (cm) { | |||||
if (cm.state.placeholder) { | |||||
cm.state.placeholder.parentNode.removeChild(cm.state.placeholder) | |||||
cm.state.placeholder = null | |||||
} | |||||
} | |||||
function setPlaceholder (cm) { | |||||
clearPlaceholder(cm) | |||||
var elt = cm.state.placeholder = document.createElement('pre') | |||||
elt.style.cssText = 'height: 0; overflow: visible; color: #80848f;' | |||||
elt.style.direction = cm.getOption('direction') | |||||
elt.className = 'CodeMirror-placeholder' | |||||
var placeHolder = cm.getOption('placeholder') | |||||
if (typeof placeHolder === 'string') placeHolder = document.createTextNode(placeHolder) | |||||
elt.appendChild(placeHolder) | |||||
cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild) | |||||
} | |||||
function onBlur (cm) { | |||||
if (isEmpty(cm)) setPlaceholder(cm) | |||||
} | |||||
function onChange (cm) { | |||||
let wrapper = cm.getWrapperElement() | |||||
let empty = isEmpty(cm) | |||||
wrapper.className = wrapper.className.replace(' CodeMirror-empty', '') + (empty ? ' CodeMirror-empty' : '') | |||||
if (empty) setPlaceholder(cm) | |||||
else clearPlaceholder(cm) | |||||
} | |||||
function isEmpty (cm) { | |||||
return (cm.lineCount() === 1) && (cm.getLine(0) === '') | |||||
} | |||||
}) | |||||
} |
@@ -0,0 +1,2 @@ | |||||
import Split from './split.vue' | |||||
export default Split |