@@ -328,3 +328,4 @@ ASALocalRun/ | |||
# MFractors (Xamarin productivity tool) working folder | |||
.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 |