first commit

main
Wayne Hsu 3 years ago
commit 64c6e4c2e1

@ -0,0 +1,9 @@
VUE_APP_PUBLIC_PATH=/
VUE_APP_NAME=Admin
VUE_APP_ROUTES_KEY=admin.routes
VUE_APP_PERMISSIONS_KEY=admin.permissions
VUE_APP_ROLES_KEY=admin.roles
VUE_APP_USER_KEY=admin.user
VUE_APP_SETTING_KEY=admin.setting
VUE_APP_TBAS_KEY=admin.tabs
VUE_APP_TBAS_TITLES_KEY=admin.tabs.titles

@ -0,0 +1,6 @@
VUE_ENV = development
VUE_APP_PUBLIC_PATH = '/admin'
# VUE_APP_API_BASE_URL=https://mock.localhost.com
VUE_APP_API_URL=https://card.h888.fun/adminapi/v1

@ -0,0 +1,5 @@
VUE_ENV = production
VUE_APP_PUBLIC_PATH = '/admin'
VUE_APP_API_URL=https://utel.vip/adminapi/v1

@ -0,0 +1,6 @@
VUE_ENV = production
VUE_APP_PUBLIC_PATH = '/admin'
VUE_APP_API_URL=https://card.h888.fun/adminapi/v1

@ -0,0 +1,6 @@
VUE_ENV = stage
VUE_APP_PUBLIC_PATH = '/admin'
VUE_APP_API_URL=https://utel.zltest.com.tw/adminapi/v1

20
.gitignore vendored

@ -0,0 +1,20 @@
.DS_Store
node_modules/
dist/
admindb/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/test/unit/coverage/
/test/e2e/reports/
selenium-debug.log
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
package-lock.json
.env.production.local

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 iczer
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,53 @@
[简体中文](./README.md) | English
<h1 align="center">Vue Antd Admin</h1>
<div align="center">
[Ant Design Pro](https://github.com/ant-design/ant-design-pro)'s implementation with Vue.
An out-of-box UI solution for enterprise applications as a React boilerplate.
[![MIT](https://img.shields.io/github/license/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/blob/master/LICENSE)
[![Dependence](https://img.shields.io/david/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin)
[![DevDependencies](https://img.shields.io/david/dev/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin?type=dev)
[![Release](https://img.shields.io/github/v/release/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/releases/latest)
![image](./src/assets/img/preview.png)
Multiple theme modes available
![image](./src/assets/img/preview-nine.png)
</div>
- Previewhttps://iczer.gitee.io/vue-antd-admin
- Documentationhttps://iczer.gitee.io/vue-antd-admin-docs
- FAQhttps://iczer.gitee.io/vue-antd-admin-docs/start/faq.html
- Mirror Repo in Chinahttps://gitee.com/iczer/vue-antd-admin
## Browsers support
Modern browsers and IE10.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/opera/opera_48x48.png" alt="Opera" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Opera |
| --- | --- | --- | --- | --- |
| IE10, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## Usage
### clone
```bash
$ git clone https://github.com/iczer/vue-antd-admin.git
```
### yarn
```bash
$ yarn install
$ yarn serve
```
### or npm
```
$ npm install
$ npm run serve
```
More instructions at [documentation](https://iczer.gitee.io/vue-antd-admin-docs).
## Contributing
Any type of contribution is welcome, here are some examples of how you may contribute to this project: :star2:
- Use Vue Antd Admin in your daily work.
- Submit [Issue](https://github.com/iczer/vue-antd-admin/issues) to report :bug: or ask questions.
- Propose [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) to improve our code.
- Join the community and share your experiences with us. QQ Group:942083829、812277510(full)、610090280(full)

@ -0,0 +1 @@
# 名片系統後台管理

@ -0,0 +1,13 @@
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV)
const plugins = []
if (IS_PROD) {
plugins.push('transform-remove-console')
}
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins
}

@ -0,0 +1,90 @@
{
"name": "vue-antd-admin",
"version": "0.7.4",
"homepage": "https://iczer.github.io/vue-antd-admin",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"build:dev": "vue-cli-service build --mode development",
"build:sta": "vue-cli-service build --mode stage",
"build:slash": "vue-cli-service build --mode slash",
"lint": "vue-cli-service lint",
"predeploy": "yarn build",
"deploy": "gh-pages -d dist -b pages -r https://gitee.com/iczer/vue-antd-admin.git",
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs",
"docs:deploy": "vuepress build docs && gh-pages -d docs/.vuepress/dist -b master -r https://gitee.com/iczer/vue-antd-admin-docs.git"
},
"dependencies": {
"@antv/data-set": "^0.11.4",
"animate.css": "^4.1.0",
"ant-design-vue": "1.7.2",
"axios": "^0.19.2",
"ckeditor4-vue": "^2.0.0",
"clipboard": "^2.0.6",
"core-js": "^3.6.5",
"date-fns": "^2.14.0",
"echarts": "^4.9.0",
"enquire.js": "^2.1.6",
"highlight.js": "^10.2.1",
"js-cookie": "^2.2.1",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"postcss-loader": "^4.3.0",
"qrcodejs2": "^0.0.2",
"raw-loader": "^4.0.2",
"viser-vue": "^2.4.8",
"vue": "^2.6.11",
"vue-clipboard2": "^0.3.3",
"vue-echarts": "^4.0.3",
"vue-element-resize-event": "^0.1.0",
"vue-i18n": "^8.18.2",
"vue-print-nb": "^1.7.5",
"vue-router": "^3.3.4",
"vuedraggable": "^2.23.2",
"vuex": "^3.4.0"
},
"devDependencies": {
"@ant-design/colors": "^4.0.1",
"@vue/cli-plugin-babel": "^4.4.0",
"@vue/cli-plugin-eslint": "^4.4.0",
"@vue/cli-service": "^4.4.0",
"@vuepress/plugin-back-to-top": "^1.5.2",
"babel-eslint": "^10.1.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"babel-polyfill": "^6.26.0",
"compression-webpack-plugin": "^2.0.0",
"deepmerge": "^4.2.2",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"fast-deep-equal": "^3.1.3",
"gh-pages": "^3.1.0",
"less-loader": "^6.1.1",
"style-resources-loader": "^1.3.2",
"vue-cli-plugin-style-resources-loader": "^0.1.4",
"vue-template-compiler": "^2.6.11",
"vuepress": "^1.5.2",
"webpack-theme-color-replacer": "1.3.18",
"whatwg-fetch": "^3.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
]
}

@ -0,0 +1,349 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

@ -0,0 +1,222 @@
@charset "utf-8";
body,
div,
dl,
footer,
html,
img,
menu,
p,
span {
margin: 0;
padding: 0;
border: 0;
}
body {
font-size: 14px;
line-height: 1.5;
-webkit-user-select: none;
-webkit-touch-callout: none;
background-color: #fffff6 !important;
padding-bottom: 49px;
}
a,
a:hover,
a:visited {
color: #999;
text-decoration: none;
outline: 0;
}
ul {
margin: 0;
padding: 0;
list-style-type: none;
}
@-webkit-keyframes pop-hide {
0% {
-webkit-transform: scale(0.8);
opacity: 0;
}
2% {
-webkit-transform: scale(1.1);
opacity: 1;
}
6% {
-webkit-transform: scale(1);
}
90% {
-webkit-transform: scale(1);
opacity: 1;
}
100% {
-webkit-transform: scale(0.9);
opacity: 0;
}
}
@-webkit-keyframes pop {
0% {
-webkit-transform: scale(0.8);
opacity: 0;
}
40% {
-webkit-transform: scale(1.1);
opacity: 1;
}
100% {
-webkit-transform: scale(1);
}
}
@-webkit-keyframes slideup {
0% {
-webkit-transform: translateY(100%);
}
40% {
-webkit-transform: translateY(-10%);
}
100% {
-webkit-transform: translateY(0);
}
}
.left {
float: left;
}
.rel {
position: relative;
}
a,
a:visited {
text-decoration: none;
color: #333;
}
.text-icon {
font-family: base_icon;
display: inline-block;
vertical-align: middle;
font-style: normal;
}
.my-account {
color: #333;
position: relative;
display: block;
width: 100%;
position: relative;
height: 6rem;
}
.account-bg {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: -1;
}
.account-bg img {
height: 100%;
width: 100%;
}
.my-account > img {
height: 100%;
position: absolute;
right: 0;
top: 0;
z-index: 0;
}
.my-account .user-info {
z-index: 1;
position: absolute;
top: 20px;
left: 70px;
box-sizing: border-box;
padding-left: 1.9em;
font-size: 13px;
color: #666;
}
.my-account .uname {
font-size: 18px;
color: #fff;
margin-top: 0.1em;
margin-bottom: 0.2em;
text-shadow: 0.07em 0.07em #333;
}
.my-account .umoney {
color: #fff;
margin-bottom: 0.06em;
text-shadow: 0.05em 0.05em #333;
}
.my-account .avatar_box {
position: absolute;
top: 1em;
left: 1em;
width: 5em;
height: 5em;
z-index: 1;
border-radius: 100%;
border: 2px solid #ffd44a;
-moz-border-radius: 100%;
-webkit-border-radius: 100%;
overflow: hidden;
}
.my-account .avater {
width: 100%;
height: 100%;
}
.phone {
width: 105px;
float: left;
z-index: 100;
}
.set {
position: absolute;
width: 60px;
right: 10px;
top: 20px;
z-index: 100;
color: #fff;
border: none;
border-radius: 15px;
background-color: #fdaf00;
text-align: center;
margin-top: -7px;
padding: 2px 2px;
}
.set a {
color: #fff !important;
}
.dl01 {
padding: 0 10px 10px;
background-color: #fff;
margin-top: 10px;
}
.titleImg {
width: 25px;
height: 25px;
margin-right: 10px;
margin-top: 15px;
float: left;
}
.dl02 {
padding: 0 10px;
background-color: #fff;
margin-top: 10px;
margin-bottom: 10px;
}
.dl02 a .menu {
border-bottom: 1px solid #ffe9b7;
background: url(../images/right.png) no-repeat right center;
background-size: 10px;
}
.dl02 a .menu div {
padding-top: 16px;
font-size: 15px;
color: #666;
}
.dl02 a .menu div.left {
float: left;
width: 40%;
}
.dl02 a .menu div.right {
float: left;
text-align: right;
width: 45%;
padding-right: 5px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="zh-TW" class="beauty-scroll">
<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, maximum-scale=1.0, user-scalable=0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= process.env.VUE_APP_NAME %></title>
<!-- require cdn assets css -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="popContainer" class="beauty-scroll" style="height: 100vh; overflow-y: scroll">
<div id="app"></div>
</div>
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<!-- built files will be auto injected -->
</body>
</html>

@ -0,0 +1,120 @@
<template>
<a-config-provider :locale="locale" :get-popup-container="popContainer">
<router-view/>
</a-config-provider>
</template>
<script>
import {enquireScreen} from './utils/util'
import {mapState, mapMutations} from 'vuex'
import themeUtil from '@/utils/themeUtil';
import {getI18nKey} from '@/utils/routerUtil'
export default {
name: 'App',
data() {
return {
locale: {}
}
},
created () {
this.setHtmlTitle()
this.setLanguage(this.lang)
enquireScreen(isMobile => this.setDevice(isMobile))
},
mounted() {
this.setWeekModeTheme(this.weekMode)
},
watch: {
weekMode(val) {
this.setWeekModeTheme(val)
},
lang(val) {
this.setLanguage(val)
this.setHtmlTitle()
},
$route() {
this.setHtmlTitle()
},
'theme.mode': function(val) {
let closeMessage = this.$message.loading(`您選擇了主題模式 ${val}, 正在切換...`)
themeUtil.changeThemeColor(this.theme.color, val).then(closeMessage)
},
'theme.color': function(val) {
let closeMessage = this.$message.loading(`您選擇了主題色 ${val}, 正在切換...`)
themeUtil.changeThemeColor(val, this.theme.mode).then(closeMessage)
},
'layout': function() {
window.dispatchEvent(new Event('resize'))
}
},
computed: {
...mapState('setting', ['layout', 'theme', 'weekMode', 'lang'])
},
methods: {
...mapMutations('setting', ['setDevice']),
setWeekModeTheme(weekMode) {
if (weekMode) {
document.body.classList.add('week-mode')
} else {
document.body.classList.remove('week-mode')
}
},
setLanguage(lang) {
this.$i18n.locale = lang
switch (lang) {
case 'TW':
this.locale = require('ant-design-vue/es/locale-provider/zh_TW').default
break
case 'CN':
this.locale = require('ant-design-vue/es/locale-provider/zh_CN').default
break
case 'US':
default:
this.locale = require('ant-design-vue/es/locale-provider/en_US').default
break
}
},
setHtmlTitle() {
const route = this.$route
const key = route.path === '/' ? 'home.name' : getI18nKey(route.matched[route.matched.length - 1].path)
document.title = process.env.VUE_APP_NAME + ' | ' + this.$t(key)
},
popContainer() {
return document.getElementById("popContainer")
}
}
}
</script>
<style lang="less">
#id{
}
.edit-btn{
color: #239DFA;
}
.delete-btn{
color: #F95838;
}
.ant-drawer-header{
background-color: #87e8de !important;
.ant-drawer-title{
color: #FFF !important;
}
}
.ant-drawer-content-wrapper {
width: 60% !important;
}
@media only screen and (max-width: @screen-md) {
.ant-drawer-content-wrapper {
width: 100% !important;
}
}
</style>

@ -0,0 +1,349 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

@ -0,0 +1,222 @@
@charset "utf-8";
body,
div,
dl,
footer,
html,
img,
menu,
p,
span {
margin: 0;
padding: 0;
border: 0;
}
body {
font-size: 14px;
line-height: 1.5;
-webkit-user-select: none;
-webkit-touch-callout: none;
background-color: #fffff6 !important;
padding-bottom: 49px;
}
a,
a:hover,
a:visited {
color: #999;
text-decoration: none;
outline: 0;
}
ul {
margin: 0;
padding: 0;
list-style-type: none;
}
@-webkit-keyframes pop-hide {
0% {
-webkit-transform: scale(0.8);
opacity: 0;
}
2% {
-webkit-transform: scale(1.1);
opacity: 1;
}
6% {
-webkit-transform: scale(1);
}
90% {
-webkit-transform: scale(1);
opacity: 1;
}
100% {
-webkit-transform: scale(0.9);
opacity: 0;
}
}
@-webkit-keyframes pop {
0% {
-webkit-transform: scale(0.8);
opacity: 0;
}
40% {
-webkit-transform: scale(1.1);
opacity: 1;
}
100% {
-webkit-transform: scale(1);
}
}
@-webkit-keyframes slideup {
0% {
-webkit-transform: translateY(100%);
}
40% {
-webkit-transform: translateY(-10%);
}
100% {
-webkit-transform: translateY(0);
}
}
.left {
float: left;
}
.rel {
position: relative;
}
a,
a:visited {
text-decoration: none;
color: #333;
}
.text-icon {
font-family: base_icon;
display: inline-block;
vertical-align: middle;
font-style: normal;
}
.my-account {
color: #333;
position: relative;
display: block;
width: 100%;
position: relative;
height: 6rem;
}
.account-bg {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: -1;
}
.account-bg img {
height: 100%;
width: 100%;
}
.my-account > img {
height: 100%;
position: absolute;
right: 0;
top: 0;
z-index: 0;
}
.my-account .user-info {
z-index: 1;
position: absolute;
top: 20px;
left: 70px;
box-sizing: border-box;
padding-left: 1.9em;
font-size: 13px;
color: #666;
}
.my-account .uname {
font-size: 18px;
color: #fff;
margin-top: 0.1em;
margin-bottom: 0.2em;
text-shadow: 0.07em 0.07em #333;
}
.my-account .umoney {
color: #fff;
margin-bottom: 0.06em;
text-shadow: 0.05em 0.05em #333;
}
.my-account .avatar_box {
position: absolute;
top: 1em;
left: 1em;
width: 5em;
height: 5em;
z-index: 1;
border-radius: 100%;
border: 2px solid #ffd44a;
-moz-border-radius: 100%;
-webkit-border-radius: 100%;
overflow: hidden;
}
.my-account .avater {
width: 100%;
height: 100%;
}
.phone {
width: 105px;
float: left;
z-index: 100;
}
.set {
position: absolute;
width: 60px;
right: 10px;
top: 20px;
z-index: 100;
color: #fff;
border: none;
border-radius: 15px;
background-color: #fdaf00;
text-align: center;
margin-top: -7px;
padding: 2px 2px;
}
.set a {
color: #fff !important;
}
.dl01 {
padding: 0 10px 10px;
background-color: #fff;
margin-top: 10px;
}
.titleImg {
width: 25px;
height: 25px;
margin-right: 10px;
margin-top: 15px;
float: left;
}
.dl02 {
padding: 0 10px;
background-color: #fff;
margin-top: 10px;
margin-bottom: 10px;
}
.dl02 a .menu {
border-bottom: 1px solid #ffe9b7;
background: url(../images/right.png) no-repeat right center;
background-size: 10px;
}
.dl02 a .menu div {
padding-top: 16px;
font-size: 15px;
color: #666;
}
.dl02 a .menu div.left {
float: left;
width: 40%;
}
.dl02 a .menu div.right {
float: left;
text-align: right;
width: 45%;
padding-right: 5px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

25
src/bootstrap.js vendored

@ -0,0 +1,25 @@
import {loadRoutes, loadGuards, setAppOptions} from '@/utils/routerUtil'
import {loadInterceptors} from '@/utils/request'
import guards from '@/router/guards'
import interceptors from '@/utils/axios-interceptors'
/**
* 啟動引導方法
* 應用啟動時需要執行的操作放在這裡
* @param router 應用的路由例項
* @param store 應用的 vuex.store 例項
* @param i18n 應用的 vue-i18n 例項
* @param i18n 應用的 message 例項
*/
function bootstrap({router, store, i18n, message}) {
// 設定應用配置
setAppOptions({router, store, i18n})
// 載入 axios 攔截器
loadInterceptors(interceptors, {router, store, i18n, message})
// 載入路由
loadRoutes()
// 載入路由守衛
loadGuards(guards, {router, store, i18n, message})
}
export default bootstrap

@ -0,0 +1,172 @@
import {isDef, isRegExp, remove} from '@/utils/util'
const patternTypes = [String, RegExp, Array]
function matches (pattern, name) {
if (Array.isArray(pattern)) {
if (pattern.indexOf(name) > -1) {
return true
} else {
for (let item of pattern) {
if (isRegExp(item) && item.test(name)) {
return true
}
}
return false
}
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
return pattern.test(name)
}
/* istanbul ignore next */
return false
}
function getComponentName (opts) {
return opts && (opts.Ctor.options.name || opts.tag)
}
function getComponentKey (vnode) {
const {componentOptions, key} = vnode
return key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: key + componentOptions.Ctor.cid
}
function getFirstComponentChild (children) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (isDef(c) && (isDef(c.componentOptions) || c.isAsyncPlaceholder)) {
return c
}
}
}
}
function pruneCache (keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode = cache[key]
if (cachedNode) {
const name = getComponentName(cachedNode.componentOptions)
const componentKey = getComponentKey(cachedNode)
if (name && !filter(name, componentKey)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
function pruneCacheEntry2(cache, key, keys) {
const cached = cache[key]
if (cached) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
function pruneCacheEntry (cache, key, keys, current) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
export default {
name: 'AKeepAlive',
abstract: true,
model: {
prop: 'clearCaches',
event: 'clear',
},
props: {
include: patternTypes,
exclude: patternTypes,
excludeKeys: patternTypes,
max: [String, Number],
clearCaches: Array
},
watch: {
clearCaches: function(val) {
if (val && val.length > 0) {
const {cache, keys} = this
val.forEach(key => {
pruneCacheEntry2(cache, key, keys)
})
this.$emit('clear', [])
}
}
},
created() {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.$watch('include', val => {
pruneCache(this, (name) => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, (name) => !matches(val, name))
})
this.$watch('excludeKeys', val => {
pruneCache(this, (name, key) => !matches(val, key))
})
},
render () {
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
const componentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name = getComponentName(componentOptions)
const componentKey = getComponentKey(vnode)
const { include, exclude, excludeKeys } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name)) ||
(excludeKeys && componentKey && matches(excludeKeys, componentKey))
) {
return vnode
}
const { cache, keys } = this
const key = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key + componentOptions.Ctor.cid
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}

@ -0,0 +1,78 @@
<template>
<a-card :loading="loading" :body-style="{padding: '20px 24px 8px'}" :bordered="false">
<div class="chart-card-header">
<div class="meta">
<span class="chart-card-title">{{title}}</span>
<span class="chart-card-action">
<slot name="action"></slot>
</span>
</div>
<div class="total"><span>{{total}}</span></div>
</div>
<div class="chart-card-content">
<div class="content-fix">
<slot></slot>
</div>
</div>
<div class="chart-card-footer">
<slot name="footer"></slot>
</div>
</a-card>
</template>
<script>
export default {
name: 'ChartCard',
props: ['title', 'total', 'loading']
}
</script>
<style scoped lang="less">
.chart-card-header{
position: relative;
overflow: hidden;
width: 100%;
}
.chart-card-header .meta{
position: relative;
overflow: hidden;
width: 100%;
color: @text-color-second;
font-size: 14px;
line-height: 22px;
}
.chart-card-action{
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
.total {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
margin-top: 4px;
margin-bottom: 0;
font-size: 30px;
line-height: 38px;
height: 38px;
}
.chart-card-footer{
border-top: 1px solid @border-color-base;
padding-top: 9px;
margin-top: 8px;
}
.chart-card-content{
margin-bottom: 12px;
position: relative;
height: 46px;
width: 100%;
}
.chart-card-content .content-fix{
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
</style>

@ -0,0 +1,59 @@
<template>
<div class="bar">
<h4>{{title}}</h4>
<div class="chart">
<v-chart :force-fit="true" height="312" :data="data" :padding="[24, 0, 0, 0]">
<v-tooltip />
<v-axis />
<v-bar position="x*y"/>
</v-chart>
</div>
</div>
</template>
<script>
const data = []
for (let i = 0; i < 12; i += 1) {
data.push({
x: `${i + 1}`,
y: Math.floor(Math.random() * 1000) + 200
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
export default {
name: 'Bar',
props: ['title'],
data () {
return {
data,
scale,
tooltip
}
}
}
</script>
<style scoped lang="less">
.bar{
position: relative;
.chart{
}
}
</style>

@ -0,0 +1,67 @@
<template>
<div class="mini-chart">
<div class="chart-content" :style="{height: 46}">
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
<v-tooltip />
<v-smooth-area position="x*y" />
</v-chart>
</div>
</div>
</template>
<script>
import {format} from 'date-fns'
const data = []
const beginDay = new Date().getTime()
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]
for (let i = 0; i < fakeY.length; i += 1) {
data.push({
x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),
y: fakeY[i]
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
export default {
name: 'MiniArea',
data () {
return {
data,
scale,
tooltip,
height: 100
}
}
}
</script>
<style scoped>
.mini-chart {
position: relative;
width: 100%
}
.mini-chart .chart-content{
position: absolute;
bottom: -28px;
width: 100%;
}
</style>

@ -0,0 +1,59 @@
<template>
<div class="mini-chart">
<div class="chart-content" :style="{height: 46}">
<v-chart :force-fit="true" :height="height" :data="data" :padding="[36, 5, 18, 5]">
<v-tooltip />
<v-bar position="x*y" />
</v-chart>
</div>
</div>
</template>
<script>
import {format} from 'date-fns'
const data = []
const beginDay = new Date().getTime()
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5]
for (let i = 0; i < fakeY.length; i += 1) {
data.push({
x: format(new Date(beginDay + 1000 * 60 * 60 * 24 * i), 'yyyy-MM-dd'),
y: fakeY[i]
})
}
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y
})
]
const scale = [{
dataKey: 'x',
min: 2
}, {
dataKey: 'y',
title: '时间',
min: 1,
max: 22
}]
export default {
name: 'MiniBar',
data () {
return {
data,
scale,
tooltip,
height: 100
}
}
}
</script>
<style lang="less" scoped>
@import "index.less";
</style>

@ -0,0 +1,56 @@
<template>
<div class="mini-progress">
<a-tooltip :title="'目标值:' + target + '%'">
<div class="target" :style="{left: target + '%'}">
<span :style="{backgroundColor: color}" />
<span :style="{backgroundColor: color}" />
</div>
</a-tooltip>
<div class="wrap">
<div class="progress" :style="{backgroundColor: color, width: percent + '%', height: height}" />
</div>
</div>
</template>
<script>
export default {
name: 'MiniProgress',
props: ['target', 'color', 'percent', 'height']
}
</script>
<style lang="less" scoped>
.mini-progress {
padding: 5px 0;
position: relative;
width: 100%;
.wrap {
background-color: @layout-bg-color;
position: relative;
}
.progress {
transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s;
border-radius: 1px 0 0 1px;
background-color: #13C2C2;
width: 0;
height: 100%;
}
.target {
position: absolute;
top: 0;
bottom: 0;
span {
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 2px;
}
span:last-child {
top: auto;
bottom: 0;
}
}
}
</style>

@ -0,0 +1,80 @@
<template>
<v-chart :forceFit="true" height="400" :data="data" :padding="[20, 20, 95, 20]" :scale="scale">
<v-tooltip />
<v-axis :dataKey="axis1Opts.dataKey" :line="axis1Opts.line" :tickLine="axis1Opts.tickLine" :grid="axis1Opts.grid" />
<v-axis :dataKey="axis2Opts.dataKey" :line="axis2Opts.line" :tickLine="axis2Opts.tickLine" :grid="axis2Opts.grid" />
<v-legend dataKey="user" marker="circle" :offset="30" />
<v-coord type="polar" radius="0.8" />
<v-line position="item*score" color="user" :size="2" />
<v-point position="item*score" color="user" :size="4" shape="circle" />
</v-chart>
</template>
<script>
const DataSet = require('@antv/data-set')
const sourceData = [
{item: '引用', a: 70, b: 30, c: 40},
{item: '口碑', a: 60, b: 70, c: 40},
{item: '产量', a: 50, b: 60, c: 40},
{item: '贡献', a: 40, b: 50, c: 40},
{item: '热度', a: 60, b: 70, c: 40},
{item: '引用', a: 70, b: 50, c: 40}
]
const dv = new DataSet.View().source(sourceData)
dv.transform({
type: 'fold',
fields: ['a', 'b', 'c'],
key: 'user',
value: 'score'
})
const scale = [{
dataKey: 'score',
min: 0,
max: 80
}]
const data = dv.rows
const axis1Opts = {
dataKey: 'item',
line: null,
tickLine: null,
grid: {
lineStyle: {
lineDash: null
},
hideFirstLine: false
}
}
const axis2Opts = {
dataKey: 'score',
line: null,
tickLine: null,
grid: {
type: 'polygon',
lineStyle: {
lineDash: null
}
}
}
export default {
name: 'Radar',
data () {
return {
sourceData,
data,
axis1Opts,
axis2Opts,
scale
}
}
}
</script>
<style scoped>
</style>

@ -0,0 +1,59 @@
<template>
<div class="rank">
<h4 class="title">{{title}}</h4>
<ul class="list">
<li :key="index" v-for="(item, index) in list">
<span :class="index < 3 ? 'active' : null">{{index + 1}}</span>
<span >{{item.name}}</span>
<span >{{item.total}}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'RankingList',
props: ['title', 'list']
}
</script>
<style lang="less" scoped>
.rank{
padding: 0 32px 32px 72px;
.title{
}
.list{
margin: 25px 0 0;
padding: 0;
list-style: none;
li {
margin-top: 16px;
span {
color: @text-color-second;
font-size: 14px;
line-height: 22px;
}
span:first-child {
background-color: @layout-bg-color;
border-radius: 20px;
display: inline-block;
font-size: 12px;
font-weight: 600;
margin-right: 24px;
height: 20px;
line-height: 20px;
width: 20px;
text-align: center;
}
span.active {
background-color: #314659 !important;
color: @text-color-inverse !important;
}
span:last-child {
float: right;
}
}
}
}
</style>

@ -0,0 +1,79 @@
<template>
<div class="chart-trend">
{{term}}
<span>{{rate}}%</span>
<span :class="['chart-trend-icon', trend]" style=""><a-icon :type="'caret-' + trend" /></span>
</div>
</template>
<script>
export default {
name: 'Trend',
props: {
term: {
type: String,
required: true
},
target: {
type: Number,
required: false,
default: 0
},
value: {
type: Number,
required: false,
default: 0
},
isIncrease: {
type: Boolean,
required: false,
default: null
},
percent: {
type: Number,
required: false,
default: null
},
scale: {
type: Number,
required: false,
default: 2
}
},
data () {
return {
trend: this.isIncrease ? 'up' : 'down',
rate: this.percent
}
},
created () {
this.trend = this.caulateTrend()
this.rate = this.caulateRate()
},
methods: {
caulateRate () {
return (this.percent === null ? Math.abs(this.value - this.target) * 100 / this.target : this.percent).toFixed(this.scale)
},
caulateTrend () {
let isIncrease = this.isIncrease === null ? this.value >= this.target : this.isIncrease
return isIncrease ? 'up' : 'down'
}
}
}
</script>
<style lang="less" scoped>
.chart-trend{
display: inline-block;
font-size: 14px;
.chart-trend-icon{
font-size: 12px;
&.up{
color: @red-6;
}
&.down{
color: @green-6;
}
}
}
</style>

@ -0,0 +1,9 @@
.mini-chart{
position: relative;
width: 100%;
.chart-content{
position: absolute;
bottom: -28px;
width: 100%;
}
}

@ -0,0 +1,157 @@
<template>
<div class="theme-color" :style="{backgroundColor: color}" @click="toggle">
<a-icon v-if="sChecked" type="check" />
</div>
</template>
<script>
const Group = {
name: 'ColorCheckboxGroup',
props: {
defaultValues: {
type: Array,
required: false,
default: () => []
},
multiple: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
values: [],
options: []
}
},
computed: {
colors () {
let colors = []
this.options.forEach(item => {
if (item.sChecked) {
colors.push(item.color)
}
})
return colors
}
},
provide () {
return {
groupContext: this
}
},
watch: {
values(value) {
this.$emit('change', value, this.colors)
}
},
methods: {
handleChange (option) {
if (!option.checked) {
if (this.values.indexOf(option.value) > -1) {
this.values = this.values.filter(item => item != option.value)
}
} else {
if (!this.multiple) {
this.values = [option.value]
this.options.forEach(item => {
if (item.value != option.value) {
item.sChecked = false
}
})
} else {
this.values.push(option.value)
}
}
}
},
render (h) {
const clear = h('div', {attrs: {style: 'clear: both'}})
return h(
'div',
{},
[this.$slots.default, clear]
)
}
}
export default {
name: 'ColorCheckbox',
Group: Group,
props: {
color: {
type: String,
required: true
},
value: {
type: [String, Number],
required: true
},
checked: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
sChecked: this.initChecked()
}
},
computed: {
},
inject: ['groupContext'],
watch: {
'sChecked': function () {
const value = {
value: this.value,
color: this.color,
checked: this.sChecked
}
this.$emit('change', value)
const groupContext = this.groupContext
if (groupContext) {
groupContext.handleChange(value)
}
}
},
created () {
const groupContext = this.groupContext
if (groupContext) {
groupContext.options.push(this)
}
},
methods: {
toggle () {
if (this.groupContext.multiple || !this.sChecked) {
this.sChecked = !this.sChecked
}
},
initChecked() {
let groupContext = this.groupContext
if (!groupContext) {
return this.checked
}else if (groupContext.multiple) {
return groupContext.defaultValues.indexOf(this.value) > -1
} else {
return groupContext.defaultValues[0] == this.value
}
}
}
}
</script>
<style lang="less" scoped>
.theme-color{
float: left;
width: 20px;
height: 20px;
border-radius: 2px;
cursor: pointer;
margin-right: 8px;
text-align: center;
color: @base-bg-color;
font-weight: bold;
}
</style>

@ -0,0 +1,161 @@
<template>
<a-tooltip :title="title" :overlayStyle="{zIndex: 2001}">
<div class="img-check-box" @click="toggle">
<img :src="img" />
<div v-if="sChecked" class="check-item">
<a-icon type="check" />
</div>
</div>
</a-tooltip>
</template>
<script>
const Group = {
name: 'ImgCheckboxGroup',
props: {
multiple: {
type: Boolean,
required: false,
default: false
},
defaultValues: {
type: Array,
required: false,
default: () => []
}
},
data () {
return {
values: [],
options: []
}
},
provide () {
return {
groupContext: this
}
},
watch: {
'values': function (value) {
this.$emit('change', value)
// // chang
// if (!(newVal.length === 1 && oldVal.length === 1 && newVal[0] === oldVal[0])) {
// this.$emit('change', this.values)
// }
}
},
methods: {
handleChange (option) {
if (!option.checked) {
if (this.values.indexOf(option.value) > -1) {
this.values = this.values.filter(item => item != option.value)
}
} else {
if (!this.multiple) {
this.values = [option.value]
this.options.forEach(item => {
if (item.value != option.value) {
item.sChecked = false
}
})
} else {
this.values.push(option.value)
}
}
}
},
render (h) {
return h(
'div',
{
attrs: {style: 'display: flex'}
},
[this.$slots.default]
)
}
}
export default {
name: 'ImgCheckbox',
Group,
props: {
checked: {
type: Boolean,
required: false,
default: false
},
img: {
type: String,
required: true
},
value: {
required: true
},
title: String
},
data () {
return {
sChecked: this.initChecked()
}
},
inject: ['groupContext'],
watch: {
'sChecked': function () {
const option = {
value: this.value,
checked: this.sChecked
}
this.$emit('change', option)
const groupContext = this.groupContext
if (groupContext) {
groupContext.handleChange(option)
}
}
},
created () {
const groupContext = this.groupContext
if (groupContext) {
this.sChecked = groupContext.defaultValues.length > 0 ? groupContext.defaultValues.indexOf(this.value) >= 0 : this.sChecked
groupContext.options.push(this)
}
},
methods: {
toggle () {
if (this.groupContext.multiple || !this.sChecked) {
this.sChecked = !this.sChecked
}
},
initChecked() {
let groupContext = this.groupContext
if (!groupContext) {
return this.checked
}else if (groupContext.multiple) {
return groupContext.defaultValues.indexOf(this.value) > -1
} else {
return groupContext.defaultValues[0] == this.value
}
}
}
}
</script>
<style lang="less" scoped>
.img-check-box{
margin-right: 16px;
position: relative;
border-radius: 4px;
cursor: pointer;
.check-item{
position: absolute;
top: 0;
right: 0;
width: 100%;
padding-top: 15px;
padding-left: 24px;
height: 100%;
color: @primary-color;
font-size: 14px;
font-weight: bold;
}
}
</style>

@ -0,0 +1,7 @@
import ColorCheckbox from '@/components/checkbox/ColorCheckbox'
import ImgCheckbox from '@/components/checkbox/ImgCheckbox'
export {
ColorCheckbox,
ImgCheckbox
}

@ -0,0 +1,69 @@
<template>
<div class="exception-page">
<div class="img">
<img :src="config[type].img" />
</div>
<div class="content">
<h1>{{config[type].title}}</h1>
<div class="desc">{{config[type].desc}}</div>
<div class="action">
<a-button type="primary" @click="backHome"></a-button>
</div>
</div>
</div>
</template>
<script>
import Config from './typeConfig'
export default {
name: 'ExceptionPage',
props: ['type', 'homeRoute'],
data () {
return {
config: Config
}
},
methods: {
backHome() {
if (this.homeRoute) {
this.$router.push(this.homeRoute)
}
this.$emit('backHome', this.type)
}
}
}
</script>
<style lang="less" scoped>
.exception-page{
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
background-color: @base-bg-color;
.img{
padding-right: 52px;
zoom: 1;
img{
max-width: 430px;
}
}
.content{
h1{
color: #434e59;
font-size: 72px;
font-weight: 600;
line-height: 72px;
margin-bottom: 24px;
}
.desc{
color: @text-color-second;
font-size: 20px;
line-height: 28px;
margin-bottom: 16px;
}
}
}
</style>

@ -0,0 +1,19 @@
const config = {
403: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
title: '403',
desc: '抱歉,你無權訪問該頁面'
},
404: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
title: '404',
desc: '抱歉,你訪問的頁面不存在或仍在開發中'
},
500: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
title: '500',
desc: '抱歉,伺服器出錯了'
}
}
export default config

@ -0,0 +1,51 @@
<template>
<div class="form-row">
<div class="label">
<span>{{label}}</span>
</div>
<div class="content">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'FormRow',
props: ['label']
}
</script>
<style lang="less" scoped>
.form-row{
display: flex;
border-bottom: 1px dashed @border-color-base;
margin-bottom: 16px;
.label {
color: @title-color;
font-size: 14px;
margin-right: 24px;
flex: 0 0 auto;
text-align: right;
& > span {
display: inline-block;
height: 39px;
line-height: 39px;
&:after {
content: '';
}
}
}
.content {
flex: 1 1 0;
:global {
.ant-form-item:last-child {
margin-right: 0;
}
.ant-form-item {
margin-bottom: 0px;
}
}
}
}
</style>

@ -0,0 +1,66 @@
<template>
<a-input
:addon-after="addonAfter"
:addon-before="addonBefore"
:default-value="defaultValue"
:disabled="disabled"
:id="id"
:max-length="maxLength"
:prefix="prefix"
:size="size"
:suffix="suffix || lenSuffix"
:type="type"
:allow-clear="allowClear"
v-model="sValue"
:value="value"
@change="onChange"
@input="onInput"
@pressEnter="onPressEnter"
@keydown="onKeydown"
>
<template :slot="slot" v-for="slot in Object.keys($slots)">
<slot :name="slot"></slot>
</template>
</a-input>
</template>
<script>
export default {
name: 'IInput',
model: {
prop: 'value',
event: 'change.value'
},
props: ['addonAfter', 'addonBefore', 'defaultValue', 'disabled', 'id', 'maxLength', 'prefix', 'size', 'suffix', 'type', 'value', 'allowClear'],
data() {
return {
sValue: this.value || this.defaultValue || ''
}
},
watch: {
value(val) {
this.sValue = val
}
},
computed: {
lenSuffix() {
return this.maxLength && `${(this.sValue + '').length}/${this.maxLength}`
}
},
methods: {
onChange(e) {
this.$emit('change', e)
this.$emit('change.value', e.target.value)
},
onInput(e) {
this.$emit('input', e)
},
onPressEnter(e) {
this.$emit('pressEnter', e)
},
onKeydown(e) {
this.$emit('keydown', e)
}
}
}
</script>

@ -0,0 +1,84 @@
<template>
<a-menu
v-show="visible"
class="contextmenu"
:style="style"
:selectedKeys="selectedKeys"
@click="handleClick"
>
<a-menu-item :key="item.key" v-for="item in itemList">
<a-icon v-if="item.icon" :type="item.icon" />
<span>{{ item.text }}</span>
</a-menu-item>
</a-menu>
</template>
<script>
export default {
name: 'Contextmenu',
props: {
visible: {
type: Boolean,
required: false,
default: false
},
itemList: {
type: Array,
required: true,
default: () => []
}
},
data () {
return {
left: 0,
top: 0,
target: null,
meta: null,
selectedKeys: []
}
},
computed: {
style () {
return {
left: this.left + 'px',
top: this.top + 'px'
}
}
},
created () {
window.addEventListener('click', this.closeMenu)
window.addEventListener('contextmenu', this.setPosition)
},
beforeDestroy() {
window.removeEventListener('click', this.closeMenu)
window.removeEventListener('contextmenu', this.setPosition)
},
methods: {
closeMenu () {
this.$emit('update:visible', false)
},
setPosition (e) {
this.left = e.clientX
this.top = e.clientY
this.target = e.target
this.meta = e.meta
},
handleClick ({ key }) {
this.$emit('select', key, this.target, this.meta)
this.closeMenu()
}
}
}
</script>
<style lang="less" scoped>
.contextmenu{
position: fixed;
z-index: 1000;
border-radius: 4px;
box-shadow: -4px 4px 16px 1px @shadow-color !important;
}
.ant-menu-item {
margin: 0 !important //
}
</style>

@ -0,0 +1,56 @@
<template>
<a-layout-sider :theme="sideTheme" :class="['side-menu', 'beauty-scroll', isMobile ? null : 'shadow']" width="200px" :collapsible="collapsible" v-model="collapsed" :trigger="null">
<div :class="['logo', theme]">
<router-link to="/dashboard/workplace">
<img src="@/assets/images/logo.png">
<h1>{{systemName}}</h1>
</router-link>
</div>
<i-menu :theme="theme" :collapsed="collapsed" :options="menuData" @select="onSelect" class="menu"/>
</a-layout-sider>
</template>
<script>
import IMenu from './menu'
import {mapState} from 'vuex'
export default {
name: 'SideMenu',
components: {IMenu},
props: {
collapsible: {
type: Boolean,
required: false,
default: false
},
collapsed: {
type: Boolean,
required: false,
default: false
},
menuData: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
}
},
computed: {
sideTheme() {
return this.theme == 'light' ? this.theme : 'dark'
},
...mapState('setting', ['isMobile', 'systemName'])
},
methods: {
onSelect (obj) {
this.$emit('menuSelect', obj)
}
}
}
</script>
<style lang="less" scoped>
@import "index";
</style>

@ -0,0 +1,38 @@
.shadow{
box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
}
.side-menu{
min-height: 100vh;
overflow-y: auto;
z-index: 10;
.logo{
height: 64px;
position: relative;
line-height: 64px;
padding-left: 24px;
-webkit-transition: all .3s;
transition: all .3s;
overflow: hidden;
background-color: @layout-trigger-background;
&.light{
background-color: #fff;
h1{
color: @primary-color;
}
}
h1{
color: @menu-dark-highlight-color;
font-size: 20px;
margin: 0 0 0 12px;
display: inline-block;
vertical-align: middle;
}
img{
width: 32px;
vertical-align: middle;
}
}
}
.menu{
padding: 16px 0;
}

@ -0,0 +1,268 @@
/**
* 該插件可根據菜單配置自動生成 ANTD menu組件
* menuOptions示例
* [
* {
* name: '菜單名稱',
* path: '菜單路由',
* meta: {
* icon: '菜單圖標',
* invisible: 'boolean, 是否不可見, 默認 false',
* },
* children: [子菜單配置]
* },
* {
* name: '菜單名稱',
* path: '菜單路由',
* meta: {
* icon: '菜單圖標',
* invisible: 'boolean, 是否不可見, 默認 false',
* },
* children: [子菜單配置]
* }
* ]
*
* i18n: 國際化配置系統默認會根據 options route配置的 path name 生成英文以及中文的國際化配置如需自定義或增加其他語言配置
* 此項即可
* i18n: {
* messages: {
* CN: {dashboard: {name: '監控中心'}}
* TW: {dashboard: {name: '監控中心'}}
* }
* }
**/
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
import fastEqual from 'fast-deep-equal'
import {getI18nKey} from '@/utils/routerUtil'
const {Item, SubMenu} = Menu
const resolvePath = (path, params = {}) => {
let _path = path
Object.entries(params).forEach(([key, value]) => {
_path = _path.replace(new RegExp(`:${key}`, 'g'), value)
})
return _path
}
const toRoutesMap = (routes) => {
const map = {}
routes.forEach(route => {
map[route.fullPath] = route
if (route.children && route.children.length > 0) {
const childrenMap = toRoutesMap(route.children)
Object.assign(map, childrenMap)
}
})
return map
}
export default {
name: 'IMenu',
props: {
options: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
},
mode: {
type: String,
required: false,
default: 'inline'
},
collapsed: {
type: Boolean,
required: false,
default: false
},
i18n: Object,
openKeys: Array
},
data () {
return {
selectedKeys: [],
sOpenKeys: [],
cachedOpenKeys: []
}
},
computed: {
menuTheme() {
return this.theme == 'light' ? this.theme : 'dark'
},
routesMap() {
return toRoutesMap(this.options)
}
},
created () {
this.updateMenu()
if (this.options.length > 0 && !this.options[0].fullPath) {
this.formatOptions(this.options, '')
}
// 自定義國際化配置
if(this.i18n && this.i18n.messages) {
const messages = this.i18n.messages
Object.keys(messages).forEach(key => {
this.$i18n.mergeLocaleMessage(key, messages[key])
})
}
},
watch: {
options(val) {
if (val.length > 0 && !val[0].fullPath) {
this.formatOptions(this.options, '')
}
},
i18n(val) {
if(val && val.messages) {
const messages = this.i18n.messages
Object.keys(messages).forEach(key => {
this.$i18n.mergeLocaleMessage(key, messages[key])
})
}
},
collapsed (val) {
if (val) {
this.cachedOpenKeys = this.sOpenKeys
this.sOpenKeys = []
} else {
this.sOpenKeys = this.cachedOpenKeys
}
},
'$route': function () {
this.updateMenu()
},
sOpenKeys(val) {
this.$emit('openChange', val)
this.$emit('update:openKeys', val)
}
},
methods: {
renderIcon: function (h, icon, key) {
if (this.$scopedSlots.icon && icon && icon !== 'none') {
const vnodes = this.$scopedSlots.icon({icon, key})
vnodes.forEach(vnode => {
vnode.data.class = vnode.data.class ? vnode.data.class : []
vnode.data.class.push('anticon')
})
return vnodes
}
return !icon || icon == 'none' ? null : h(Icon, {props: {type: icon}})
},
renderMenuItem: function (h, menu) {
let tag = 'router-link'
const path = resolvePath(menu.fullPath, menu.meta.params)
let config = {props: {to: {path, query: menu.meta.query}, }, attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}}
if (menu.meta && menu.meta.link) {
tag = 'a'
config = {attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;', href: menu.meta.link, target: '_blank'}}
}
return h(
Item, {key: menu.fullPath},
[
h(tag, config,
[
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),
this.$t(getI18nKey(menu.fullPath))
]
)
]
)
},
renderSubMenu: function (h, menu) {
let this_ = this
let subItem = [h('span', {slot: 'title', attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}},
[
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),
this.$t(getI18nKey(menu.fullPath))
]
)]
let itemArr = []
menu.children.forEach(function (item) {
itemArr.push(this_.renderItem(h, item))
})
return h(SubMenu, {key: menu.fullPath},
subItem.concat(itemArr)
)
},
renderItem: function (h, menu) {
const meta = menu.meta
if (!meta || !meta.invisible) {
let renderChildren = false
const children = menu.children
if (children != undefined) {
for (let i = 0; i < children.length; i++) {
const childMeta = children[i].meta
if (!childMeta || !childMeta.invisible) {
renderChildren = true
break
}
}
}
return (menu.children && renderChildren) ? this.renderSubMenu(h, menu) : this.renderMenuItem(h, menu)
}
},
renderMenu: function (h, menuTree) {
let this_ = this
let menuArr = []
menuTree.forEach(function (menu, i) {
menuArr.push(this_.renderItem(h, menu, '0', i))
})
return menuArr
},
formatOptions(options, parentPath) {
options.forEach(route => {
let isFullPath = route.path.substring(0, 1) == '/'
route.fullPath = isFullPath ? route.path : parentPath + '/' + route.path
if (route.children) {
this.formatOptions(route.children, route.fullPath)
}
})
},
updateMenu () {
this.selectedKeys = this.getSelectedKeys()
let openKeys = this.selectedKeys.filter(item => item !== '')
openKeys = openKeys.slice(0, openKeys.length -1)
if (!fastEqual(openKeys, this.sOpenKeys)) {
this.collapsed || this.mode === 'horizontal' ? this.cachedOpenKeys = openKeys : this.sOpenKeys = openKeys
}
},
getSelectedKeys() {
let matches = this.$route.matched
const route = matches[matches.length - 1]
let chose = this.routesMap[route.path]
if (chose.meta && chose.meta.highlight) {
chose = this.routesMap[chose.meta.highlight]
const resolve = this.$router.resolve({path: chose.fullPath})
matches = (resolve.resolved && resolve.resolved.matched) || matches
}
return matches.map(item => item.path)
}
},
render (h) {
return h(
Menu,
{
props: {
theme: this.menuTheme,
mode: this.$props.mode,
selectedKeys: this.selectedKeys,
openKeys: this.openKeys ? this.openKeys : this.sOpenKeys
},
on: {
'update:openKeys': (val) => {
this.sOpenKeys = val
},
click: (obj) => {
obj.selectedKeys = [obj.key]
this.$emit('select', obj)
}
}
}, this.renderMenu(h, this.options)
)
}
}

@ -0,0 +1,45 @@
<template>
<div :class="['page-header', layout, pageWidth]">
<div class="page-header-wide">
<div class="breadcrumb">
<a-breadcrumb>
<a-breadcrumb-item :key="index" v-for="(item, index) in breadcrumb">
<span>{{item}}</span>
</a-breadcrumb-item>
</a-breadcrumb>
</div>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'PageHeader',
props: {
title: {
type: [String, Boolean],
required: false
},
breadcrumb: {
type: Array,
required: false
},
logo: {
type: String,
required: false
},
avatar: {
type: String,
required: false
},
},
computed: {
...mapState('setting', ['layout', 'showPageTitle', 'pageWidth'])
}
}
</script>
<style lang="less" scoped>
@import "index";
</style>

@ -0,0 +1,40 @@
.page-header{
background: @base-bg-color;
padding: 16px 24px;
&.head.fixed{
margin: auto;
max-width: 1400px;
}
.page-header-wide{
.breadcrumb{
margin: 0px 5px;
}
.detail{
display: flex;
.row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.avatar {
margin:0 24px 0 0;
}
.main{
width: 100%;
.title{
font-size: 20px;
color: @title-color;
margin-bottom: 0px;
}
.content{
display: flex;
flex-wrap: wrap;
color: @text-color-second;
}
.extra{
display: flex;
}
}
}
}
}

@ -0,0 +1,64 @@
<template>
<div class="result">
<div >
<a-icon :class="[isSuccess ? 'success' : 'error' ,'icon']" :type="isSuccess ? 'check-circle' : 'close-circle'" />
</div>
<div class="title" v-if="title">{{title}}</div>
<div class="desc" v-if="description">{{description}}</div>
<div class="content">
<slot></slot>
</div>
<div class="action">
<slot name="action"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'Result',
props: ['isSuccess', 'title', 'description']
}
</script>
<style lang="less" scoped>
.result{
text-align: center;
width: 72%;
margin: 0 auto;
.icon{
font-size: 72px;
line-height: 72px;
margin-bottom: 24px;
}
.success {
color: @success-color;
}
.error {
color: @error-color;
}
.title{
font-size: 24px;
color: @title-color;
font-weight: 500;
line-height: 32px;
margin-bottom: 16px;
}
.desc{
font-size: 14px;
line-height: 22px;
color: @text-color-second;
margin-bottom: 24px;
}
.content{
background-color: @background-color-light;
padding: 24px 40px;
border-radius: 2px;
text-align: left;
}
.action{
margin-top: 32px;
}
}
</style>

@ -0,0 +1,224 @@
<template>
<div class="side-setting">
<setting-item>
<a-button @click="saveSetting" type="primary" icon="save">{{$t('save')}}</a-button>
<a-button @click="resetSetting" type="dashed" icon="redo" style="float: right">{{$t('reset')}}</a-button>
</setting-item>
<setting-item :title="$t('theme.title')">
<img-checkbox-group
@change="values => setTheme({...theme, mode: values[0]})"
:default-values="[theme.mode]"
>
<img-checkbox :title="$t('theme.dark')" img="https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg" value="dark"/>
<img-checkbox :title="$t('theme.light')" img="https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg" value="light"/>
<img-checkbox :title="$t('theme.night')" img="https://gw.alipayobjects.com/zos/antfincdn/hmKaLQvmY2/LCkqqYNmvBEbokSDscrm.svg" value="night"/>
</img-checkbox-group>
</setting-item>
<setting-item :title="$t('theme.color')">
<color-checkbox-group
@change="(values, colors) => setTheme({...theme, color: colors[0]})"
:defaultValues="[palettes.indexOf(theme.color)]" :multiple="false"
>
<color-checkbox v-for="(color, index) in palettes" :key="index" :color="color" :value="index" />
</color-checkbox-group>
</setting-item>
<a-divider/>
<setting-item :title="$t('navigate.title')">
<img-checkbox-group
@change="values => setLayout(values[0])"
:default-values="[layout]"
>
<img-checkbox :title="$t('navigate.side')" img="https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg" value="side"/>
<img-checkbox :title="$t('navigate.head')" img="https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg" value="head"/>
<img-checkbox :title="$t('navigate.mix')" img="https://gw.alipayobjects.com/zos/antfincdn/x8Ob%26B8cy8/LCkqqYNmvBEbokSDscrm.svg" value="mix"/>
</img-checkbox-group>
</setting-item>
<setting-item>
<a-list :split="false">
<a-list-item>
{{$t('navigate.content.title')}}
<a-select
:getPopupContainer="getPopupContainer"
:value="pageWidth"
@change="setPageWidth"
class="select-item" size="small" slot="actions"
>
<a-select-option value="fluid">{{$t('navigate.content.fluid')}}</a-select-option>
<a-select-option value="fixed">{{$t('navigate.content.fixed')}}</a-select-option>
</a-select>
</a-list-item>
<a-list-item>
{{$t('navigate.fixedHeader')}}
<a-switch :checked="fixedHeader" slot="actions" size="small" @change="setFixedHeader" />
</a-list-item>
<a-list-item>
{{$t('navigate.fixedSideBar')}}
<a-switch :checked="fixedSideBar" slot="actions" size="small" @change="setFixedSideBar" />
</a-list-item>
</a-list>
</setting-item>
<a-divider />
<setting-item :title="$t('other.title')">
<a-list :split="false">
<a-list-item>
{{$t('other.weekMode')}}
<a-switch :checked="weekMode" slot="actions" size="small" @change="setWeekMode" />
</a-list-item>
<a-list-item>
{{$t('other.multiPages')}}
<a-switch :checked="multiPage" slot="actions" size="small" @change="setMultiPage" />
</a-list-item>
<a-list-item>
{{$t('other.hideSetting')}}
<a-switch :checked="hideSetting" slot="actions" size="small" @change="setHideSetting" />
</a-list-item>
</a-list>
</setting-item>
<a-divider />
<setting-item :title="$t('animate.title')">
<a-list :split="false">
<a-list-item>
{{$t('animate.disable')}}
<a-switch :checked="animate.disabled" slot="actions" size="small" @change="val => setAnimate({...animate, disabled: val})" />
</a-list-item>
<a-list-item>
{{$t('animate.effect')}}
<a-select
:value="animate.name"
:getPopupContainer="getPopupContainer"
@change="val => setAnimate({...animate, name: val})"
class="select-item" size="small" slot="actions"
>
<a-select-option :key="index" :value="item.name" v-for="(item, index) in animates">{{item.alias}}</a-select-option>
</a-select>
</a-list-item>
<a-list-item>
{{$t('animate.direction')}}
<a-select
:value="animate.direction"
:getPopupContainer="getPopupContainer"
@change="val => setAnimate({...animate, direction: val})"
class="select-item" size="small" slot="actions"
>
<a-select-option :key="index" :value="item" v-for="(item, index) in directions">{{item}}</a-select-option>
</a-select>
</a-list-item>
</a-list>
</setting-item>
<a-alert
v-if="isDev"
style="max-width: 240px; margin: -16px 0 8px; word-break: break-all"
type="warning"
:message="$t('alert')"
>
</a-alert>
<a-button v-if="isDev" id="copyBtn" :data-clipboard-text="copyConfig" @click="copyCode" style="width: 100%" icon="copy" >{{$t('copy')}}</a-button>
</div>
</template>
<script>
import SettingItem from './SettingItem'
import {ColorCheckbox, ImgCheckbox} from '@/components/checkbox'
import Clipboard from 'clipboard'
import { mapState, mapMutations } from 'vuex'
import {formatConfig} from '@/utils/formatter'
import {setting} from '@/config/default'
import sysConfig from '@/config/config'
import fastEqual from 'fast-deep-equal'
import deepMerge from 'deepmerge'
const ColorCheckboxGroup = ColorCheckbox.Group
const ImgCheckboxGroup = ImgCheckbox.Group
export default {
name: 'Setting',
i18n: require('./i18n'),
components: {ImgCheckboxGroup, ImgCheckbox, ColorCheckboxGroup, ColorCheckbox, SettingItem},
data() {
return {
copyConfig: 'Sorry, you have copied nothing O(∩_∩)O~',
isDev: process.env.NODE_ENV === 'development'
}
},
computed: {
directions() {
return this.animates.find(item => item.name == this.animate.name).directions
},
...mapState('setting', ['theme', 'layout', 'animate', 'animates', 'palettes', 'multiPage', 'weekMode', 'fixedHeader', 'fixedSideBar', 'hideSetting', 'pageWidth'])
},
watch: {
'animate.name': function(val) {
this.setAnimate({name: val, direction: this.directions[0]})
}
},
methods: {
getPopupContainer() {
return this.$el.parentNode
},
copyCode () {
let config = this.extractConfig(false)
this.copyConfig = `// 自定义配置,参考 ./default/setting.config.js需要自定义的属性在这里配置即可
module.exports = ${formatConfig(config)}
`
let clipboard = new Clipboard('#copyBtn')
clipboard.on('success', () => {
this.$message.success(`复制成功,覆盖文件 src/config/config.js 然后重启项目即可生效`).then(() => {
const localConfig = localStorage.getItem(process.env.VUE_APP_SETTING_KEY)
if (localConfig) {
console.warn('检测到本地有历史保存的主题配置,想要要拷贝的配置代码生效,您可能需要先重置配置')
this.$message.warn('检测到本地有历史保存的主题配置,想要要拷贝的配置代码生效,您可能需要先重置配置', 5)
}
})
clipboard.destroy()
})
},
saveSetting() {
const closeMessage = this.$message.loading('正在保存到本地,请稍后...', 0)
const config = this.extractConfig(true)
localStorage.setItem(process.env.VUE_APP_SETTING_KEY, JSON.stringify(config))
setTimeout(closeMessage, 800)
},
resetSetting() {
this.$confirm({
title: '重置主题会刷新页面,当前页面内容不会保留,确认重置?',
onOk() {
localStorage.removeItem(process.env.VUE_APP_SETTING_KEY)
window.location.reload()
}
})
},
//
extractConfig(local = false) {
let config = {}
let mySetting = this.$store.state.setting
let dftSetting = local ? deepMerge(setting, sysConfig) : setting
Object.keys(mySetting).forEach(key => {
const dftValue = dftSetting[key], myValue = mySetting[key]
if (dftValue != undefined && !fastEqual(dftValue, myValue)) {
config[key] = myValue
}
})
return config
},
...mapMutations('setting', ['setTheme', 'setLayout', 'setMultiPage', 'setWeekMode',
'setFixedSideBar', 'setFixedHeader', 'setAnimate', 'setHideSetting', 'setPageWidth'])
}
}
</script>
<style lang="less" scoped>
.side-setting{
min-height: 100%;
background-color: @base-bg-color;
padding: 24px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
position: relative;
.flex{
display: flex;
}
.select-item{
width: 80px;
}
}
</style>

@ -0,0 +1,25 @@
<template>
<div class="setting-item">
<h3 v-if="title" class="title">{{title}}</h3>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'SettingItem',
props: ['title']
}
</script>
<style lang="less" scoped>
.setting-item{
margin-bottom: 24px;
.title{
font-size: 14px;
color: @title-color;
line-height: 22px;
margin-bottom: 12px;
}
}
</style>

@ -0,0 +1,117 @@
module.exports = {
messages: {
CN: {
theme: {
title: '整体风格设置',
light: '亮色菜单风格',
dark: '暗色菜单风格',
night: '深夜模式',
color: '主题色'
},
navigate: {
title: '导航设置',
side: '侧边导航',
head: '顶部导航',
mix: '混合导航',
content: {
title: '内容区域宽度',
fluid: '流式',
fixed: '定宽'
},
fixedHeader: '固定Header',
fixedSideBar: '固定侧边栏',
},
other: {
title: '其他设置',
weekMode: '色弱模式',
multiPages: '多页签模式',
hideSetting: '隐藏设置抽屉'
},
animate: {
title: '页面切换动画',
disable: '禁用动画',
effect: '动画效果',
direction: '动画方向'
},
alert: '拷贝配置后,直接覆盖文件 src/config/config.js 中的全部内容,然后重启即可。(注意:仅会拷贝与默认配置不同的项)',
copy: '拷贝配置',
save: '保存配置',
reset: '重置配置',
},
TW: {
theme: {
title: '整體風格設置',
light: '亮色菜單風格',
dark: '暗色菜單風格',
night: '深夜模式',
color: '主題色'
},
navigate: {
title: '導航設置',
side: '側邊導航',
head: '頂部導航',
content: {
title: '內容區域寬度',
fluid: '流式',
fixed: '定寬'
},
fixedHeader: '固定Header',
fixedSideBar: '固定側邊欄',
},
other: {
title: '其他設置',
weekMode: '色弱模式',
multiPages: '多頁簽模式',
hideSetting: '隱藏設置抽屜'
},
animate: {
title: '頁面切換動畫',
disable: '禁用動畫',
effect: '動畫效果',
direction: '動畫方向'
},
alert: '拷貝配置后,直接覆蓋文件 src/config/config.js 中的全部內容,然後重啟即可。(注意:僅會拷貝與默認配置不同的項)',
copy: '拷貝配置',
save: '保存配置',
reset: '重置配置',
},
US: {
theme: {
title: 'Page Style Setting',
light: 'Light Style',
dark: 'Dark Style',
night: 'Night Style',
color: 'Theme Color'
},
navigate: {
title: 'Navigation Mode',
side: 'Side Menu Layout',
head: 'Top Menu Layout',
mix: 'Mix Menu Layout',
content: {
title: 'Content Width',
fluid: 'Fluid',
fixed: 'Fixed'
},
fixedHeader: 'Fixed Header',
fixedSideBar: 'Fixed SideBar',
},
other: {
title: 'Other Setting',
weekMode: 'Week Mode',
multiPages: 'Multi Pages',
hideSetting: 'Hide Setting Drawer'
},
animate: {
title: 'Page Toggle Animation',
disable: 'Disable',
effect: 'Effect',
direction: 'Direction'
},
alert: 'After copying the configuration code, directly cover all contents in the file src/config/config.js, then restart the server. (Note: only items that are different from the default configuration will be copied)',
copy: 'Copy Setting',
save: 'Save',
reset: 'Reset',
}
}
}

@ -0,0 +1,144 @@
<template>
<div class="standard-table">
<div class="alert">
<a-alert type="info" :show-icon="true" v-if="selectedRows">
<div class="message" slot="message">
已選擇&nbsp;<a>{{selectedRows.length}}</a>&nbsp; <a class="clear" @click="onClear"></a>
<template v-for="(item, index) in needTotalList" >
<div v-if="item.needTotal" :key="index">
{{item.title}}總計&nbsp;
<a>{{item.customRender ? item.customRender(item.total) : item.total}}</a>
</div>
</template>
</div>
</a-alert>
</div>
<a-table
:bordered="bordered"
:loading="loading"
:columns="columns"
:dataSource="dataSource"
:rowKey="rowKey"
:pagination="pagination"
:expandedRowKeys="expandedRowKeys"
:expandedRowRender="expandedRowRender"
@change="onChange"
:rowSelection="selectedRows ? {selectedRowKeys: selectedRowKeys, onChange: updateSelect} : undefined"
:scroll="scroll"
>
<template slot-scope="text, record, index" :slot="slot" v-for="slot in Object.keys($scopedSlots).filter(key => key !== 'expandedRowRender') ">
<slot :name="slot" v-bind="{text, record, index}"></slot>
</template>
<template :slot="slot" v-for="slot in Object.keys($slots)">
<slot :name="slot"></slot>
</template>
<template slot-scope="record, index, indent, expanded" :slot="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''">
<slot v-bind="{record, index, indent, expanded}" :name="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''"></slot>
</template>
</a-table>
</div>
</template>
<script>
export default {
name: 'StandardTable',
props: {
// bordered: Boolean,
loading: [Boolean, Object],
columns: Array,
dataSource: Array,
rowKey: {
type: [String, Function],
default: 'key'
},
scroll: Object,
pagination: [Boolean, Object],
selectedRows: Array,
expandedRowKeys: Array,
expandedRowRender: Function
},
data () {
return {
bordered: true,
needTotalList: []
}
},
methods: {
updateSelect (selectedRowKeys, selectedRows) {
this.$emit('update:selectedRows', selectedRows)
this.$emit('selectedRowChange', selectedRowKeys, selectedRows)
},
initTotalList (columns) {
const totalList = columns.filter(item => item.needTotal)
.map(item => {
return {
...item,
total: 0
}
})
return totalList
},
onClear() {
this.updateSelect([], [])
this.$emit('clear')
},
onChange(pagination, filters, sorter, {currentDataSource}) {
this.$emit('change', pagination, filters, sorter, {currentDataSource})
},
onShowSizeChange(current, size){
this.$emit('showSizeChange', current, size)
}
},
created () {
this.needTotalList = this.initTotalList(this.columns)
},
watch: {
selectedRows (selectedRows) {
this.needTotalList = this.needTotalList.map(item => {
return {
...item,
total: selectedRows.reduce((sum, val) => {
let v
try{
v = val[item.dataIndex] ? val[item.dataIndex] : eval(`val.${item.dataIndex}`);
}catch(_){
v = val[item.dataIndex];
}
v = !isNaN(parseFloat(v)) ? parseFloat(v) : 0;
return sum + v
}, 0)
}
})
}
},
computed: {
selectedRowKeys() {
return this.selectedRows.map(record => {
return (typeof this.rowKey === 'function') ? this.rowKey(record) : record[this.rowKey]
})
}
}
}
</script>
<style scoped lang="less">
.standard-table{
.alert{
margin-bottom: 16px;
.message{
a{
font-weight: 600;
}
}
.clear{
float: right;
}
}
}
.pagination{
margin-top: 10px;
}
</style>

@ -0,0 +1,155 @@
<template>
<div class="action-columns" ref="root">
<a-popover v-model="visible" placement="bottomRight" trigger="click" :get-popup-container="() => $refs.root">
<div slot="title">
<a-checkbox :indeterminate="indeterminate" :checked="checkAll" @change="onCheckAllChange" class="check-all" />列展示
<a-button @click="resetColumns" style="float: right" type="link" size="small">重置</a-button>
</div>
<a-list style="width: 100%" size="small" :key="i" v-for="(col, i) in columns" slot="content">
<a-list-item>
<a-checkbox v-model="col.visible" @change="e => onCheckChange(e, col)"/>
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<template slot="actions">
<a-tooltip title="固定在列头" :mouseEnterDelay="0.5" :get-popup-container="() => $refs.root">
<a-icon :class="['left', {active: col.fixed === 'left'}]" @click="fixColumn('left', col)" type="vertical-align-top" />
</a-tooltip>
<a-tooltip title="固定在列尾" :mouseEnterDelay="0.5" :get-popup-container="() => $refs.root">
<a-icon :class="['right', {active: col.fixed === 'right'}]" @click="fixColumn('right', col)" type="vertical-align-bottom" />
</a-tooltip>
<a-tooltip title="添加搜索" :mouseEnterDelay="0.5" :get-popup-container="() => $refs.root">
<a-icon :class="{active: col.searchAble}" @click="setSearch(col)" type="search" />
</a-tooltip>
</template>
</a-list-item>
</a-list>
<a-icon class="action" type="setting" />
</a-popover>
</div>
</template>
<script>
import cloneDeep from 'lodash.clonedeep'
export default {
name: 'ActionColumns',
props: ['columns', 'visibleColumns'],
data() {
return {
visible: false,
indeterminate: false,
checkAll: true,
checkedCounts: this.columns.length,
backColumns: cloneDeep(this.columns)
}
},
watch: {
checkedCounts(val) {
this.checkAll = val === this.columns.length
this.indeterminate = val > 0 && val < this.columns.length
},
columns(newVal, oldVal) {
if (newVal != oldVal) {
this.checkedCounts = newVal.length
this.formatColumns(newVal)
}
}
},
created() {
this.formatColumns(this.columns)
},
methods: {
onCheckChange(e, col) {
if (!col.visible) {
this.checkedCounts -= 1
} else {
this.checkedCounts += 1
}
},
fixColumn(fixed, col) {
if (fixed !== col.fixed) {
this.$set(col, 'fixed', fixed)
} else {
this.$set(col, 'fixed', undefined)
}
},
setSearch(col) {
this.$set(col, 'searchAble', !col.searchAble)
if (!col.searchAble && col.search) {
this.resetSearch(col)
}
},
resetSearch(col) {
// col.search.value = col.dataType === 'boolean' ? false : undefined
col.search.value = undefined
col.search.backup = undefined
},
resetColumns() {
const {columns, backColumns} = this
let counts = columns.length
backColumns.forEach((back, index) => {
const column = columns[index]
column.visible = back.visible === undefined || back.visible
if (!column.visible) {
counts -= 1
}
if (back.fixed !== undefined) {
column.fixed = back.fixed
} else {
this.$set(column, 'fixed', undefined)
}
this.$set(column, 'searchAble', back.searchAble)
// column.searchAble = back.searchAble
this.resetSearch(column)
})
this.checkedCounts = counts
this.visible = false
this.$emit('reset', this.getConditions(columns))
},
onCheckAllChange(e) {
if (e.target.checked) {
this.checkedCounts = this.columns.length
this.columns.forEach(col => col.visible = true)
} else {
this.checkedCounts = 0
this.columns.forEach(col => col.visible = false)
}
},
getConditions(columns) {
const conditions = {}
columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)
.forEach(col => {
conditions[col.dataIndex] = col.search.value
})
return conditions
},
formatColumns(columns) {
for (let col of columns) {
if (col.visible === undefined) {
this.$set(col, 'visible', true)
}
if (!col.visible) {
this.checkedCounts -= 1
}
}
}
}
}
</script>
<style scoped lang="less">
.action-columns{
display: inline-block;
.check-all{
margin-right: 8px;
}
.left,.right{
transform: rotate(-90deg);
}
.active{
color: @primary-color;
}
}
</style>

@ -0,0 +1,44 @@
<template>
<div class="action-size" ref="root">
<a-tooltip title="密度">
<a-dropdown placement="bottomCenter" :trigger="['click']" :get-popup-container="() => $refs.root">
<a-icon class="action" type="column-height" />
<a-menu :selected-keys="[value]" slot="overlay" @click="onClick">
<a-menu-item key="default">
默认
</a-menu-item>
<a-menu-item key="middle">
中等
</a-menu-item>
<a-menu-item key="small">
紧密
</a-menu-item>
</a-menu>
</a-dropdown>
</a-tooltip>
</div>
</template>
<script>
export default {
name: 'ActionSize',
props: ['value'],
inject: ['table'],
data() {
return {
selectedKeys: ['middle']
}
},
methods: {
onClick({key}) {
this.$emit('input', key)
}
}
}
</script>
<style scoped lang="less">
.action-size{
display: inline-block;
}
</style>

@ -0,0 +1,249 @@
<template>
<div ref="table" :id="id" class="advanced-table">
<a-spin :spinning="loading">
<div :class="['header-bar', size]">
<div class="title">
<template v-if="title">{{title}}</template>
<slot v-else-if="$slots.title" name="title"></slot>
<template v-else></template>
</div>
<div class="search">
<search-area :format-conditions="formatConditions" @change="onSearchChange" :columns="columns" >
<template :slot="slot" v-for="slot in slots">
<slot :name="slot"></slot>
</template>
</search-area>
</div>
<div class="actions">
<a-tooltip title="刷新">
<a-icon @click="refresh" class="action" :type="loading ? 'loading' : 'reload'" />
</a-tooltip>
<action-size v-model="sSize" class="action" />
<a-tooltip title="列配置">
<action-columns :columns="columns" @reset="onColumnsReset" class="action">
<template :slot="slot" v-for="slot in slots">
<slot :name="slot"></slot>
</template>
</action-columns>
</a-tooltip>
<a-tooltip title="全屏">
<a-icon @click="toggleScreen" class="action" :type="fullScreen ? 'fullscreen-exit' : 'fullscreen'" />
</a-tooltip>
</div>
</div>
<a-table
v-bind="{...$props, columns: visibleColumns, title: undefined, loading: false}"
:size="sSize"
@expandedRowsChange="onExpandedRowsChange"
@change="onChange"
@expand="onExpand"
>
<template slot-scope="text, record, index" :slot="slot" v-for="slot in scopedSlots ">
<slot :name="slot" v-bind="{text, record, index}"></slot>
</template>
<template :slot="slot" v-for="slot in slots">
<slot :name="slot"></slot>
</template>
<template slot-scope="record, index, indent, expanded" :slot="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''">
<slot v-bind="{record, index, indent, expanded}" :name="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''"></slot>
</template>
</a-table>
</a-spin>
</div>
</template>
<script>
import ActionSize from '@/components/table/advance/ActionSize'
import ActionColumns from '@/components/table/advance/ActionColumns'
import SearchArea from '@/components/table/advance/SearchArea'
export default {
name: 'AdvanceTable',
components: {SearchArea, ActionColumns, ActionSize},
props: {
tableLayout: String,
bordered: Boolean,
childrenColumnName: {type: String, default: 'children'},
columns: Array,
components: Object,
dataSource: Array,
defaultExpandAllRows: Array[String],
expandedRowKeys: Array[String],
expandedRowRender: Function,
expandIcon: Function,
expandRowByClick: Boolean,
expandIconColumnIndex: Number,
footer: Function,
indentSize: Number,
loading: Boolean,
locale: Object,
pagination: [Object, Boolean],
rowClassName: Function,
rowKey: [String, Function],
rowSelection: Object,
scroll: Object,
showHeader: {type: Boolean, default: true},
size: String,
title: String,
customHeaderRow: Function,
customRow: Function,
getPopupContainer: Function,
transformCellText: Function,
formatConditions: Boolean
},
provide() {
return {
table: this
}
},
data() {
return {
id: `${new Date().getTime()}-${Math.floor(Math.random() * 10)}`,
sSize: this.size || 'default',
fullScreen: false,
conditions: {}
}
},
computed: {
slots() {
return Object.keys(this.$slots).filter(slot => slot !== 'title')
},
scopedSlots() {
return Object.keys(this.$scopedSlots).filter(slot => slot !== 'expandedRowRender' && slot !== 'title')
},
visibleColumns(){
return this.columns.filter(col => col.visible)
}
},
created() {
this.addListener()
},
beforeDestroy() {
this.removeListener()
},
methods: {
refresh() {
this.$emit('refresh', this.conditions)
},
onSearchChange(conditions, searchOptions) {
this.conditions = conditions
this.$emit('search', conditions, searchOptions)
},
toggleScreen() {
if (this.fullScreen) {
this.outFullScreen()
} else {
this.inFullScreen()
}
},
inFullScreen() {
const el = this.$refs.table
el.classList.add('beauty-scroll')
if (el.requestFullscreen) {
el.requestFullscreen()
return true
} else if (el.webkitRequestFullScreen) {
el.webkitRequestFullScreen()
return true
} else if (el.mozRequestFullScreen) {
el.mozRequestFullScreen()
return true
} else if (el.msRequestFullscreen) {
el.msRequestFullscreen()
return true
}
this.$message.warn('对不起,您的浏览器不支持全屏模式')
el.classList.remove('beauty-scroll')
return false
},
outFullScreen() {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.msExitFullscreen) {
document.msExitFullscreen()
}
this.$refs.table.classList.remove('beauty-scroll')
},
onColumnsReset(conditions) {
this.$emit('reset', conditions)
},
onExpandedRowsChange(expandedRows) {
this.$emit('expandedRowsChange', expandedRows)
},
onChange(pagination, filters, sorter, options) {
this.$emit('change', pagination, filters, sorter, options)
},
onExpand(expanded, record) {
this.$emit('expand', expanded, record)
},
addListener() {
document.addEventListener('fullscreenchange', this.fullScreenListener)
document.addEventListener('webkitfullscreenchange', this.fullScreenListener)
document.addEventListener('mozfullscreenchange', this.fullScreenListener)
document.addEventListener('msfullscreenchange', this.fullScreenListener)
},
removeListener() {
document.removeEventListener('fullscreenchange', this.fullScreenListener)
document.removeEventListener('webkitfullscreenchange', this.fullScreenListener)
document.removeEventListener('mozfullscreenchange', this.fullScreenListener)
document.removeEventListener('msfullscreenchange', this.fullScreenListener)
},
fullScreenListener(e) {
if (e.target.id === this.id) {
this.fullScreen = !this.fullScreen
}
}
}
}
</script>
<style scoped lang="less">
.advanced-table{
overflow-y: auto;
background-color: @component-background;
.header-bar{
padding: 16px 24px;
display: flex;
align-items: center;
border-radius: 4px;
transition: all 0.3s;
&.middle{
padding: 12px 16px;
}
&.small{
padding: 8px 12px;
border: 1px solid @border-color;
border-bottom: 0;
.title{
font-size: 16px;
}
}
.title{
transition: all 0.3s;
font-size: 18px;
color: @title-color;
font-weight: 700;
}
.search{
flex: 1;
text-align: right;
margin: 0 24px;
}
.actions{
text-align: right;
font-size: 17px;
color: @text-color;
.action{
margin: 0 8px;
cursor: pointer;
&:hover{
color: @primary-color;
}
}
}
}
}
</style>

@ -0,0 +1,313 @@
<template>
<div class="search-area" ref="root">
<div class="select-root" ref="selectRoot"></div>
<div class="search-item" :key="index" v-for="(col, index) in searchCols">
<div v-if="col.dataType === 'boolean'" :class="['title', {active: col.search.value !== undefined}]">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-switch @change="onSwitchChange(col)" class="switch" v-model="col.search.value" size="small"
:checked-children="(col.search.switchOptions && col.search.switchOptions.checkedText) || '是'"
:un-checked-children="(col.search.switchOptions && col.search.switchOptions.uncheckedText) || '否'"
/>
<a-icon v-if="col.search.value !== undefined" class="close" @click="e => onCloseClick(e, col)" type="close-circle" theme="filled" />
</div>
<div v-else-if="col.dataType === 'time'" :class="['title', {active: col.search.value}]">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-time-picker :format="col.search.format" v-model="col.search.value" placeholder="选择时间" @change="(time, timeStr) => onCalendarChange(time, timeStr, col)" @openChange="open => onCalendarOpenChange(open, col)" class="time-picker" size="small" :get-popup-container="() => $refs.root"/>
</div>
<div v-else-if="col.dataType === 'date'" :class="['title', {active: col.search.value}]">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-date-picker :format="col.search.format" v-model="col.search.value" @change="onDateChange(col)" class="date-picker" size="small" :getCalendarContainer="() => $refs.root"/>
</div>
<div v-else-if="col.dataType === 'datetime'" class="title datetime active">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-date-picker :format="col.search.format" v-model="col.search.value" @change="(date, dateStr) => onCalendarChange(date, dateStr, col)" @openChange="open => onCalendarOpenChange(open, col)" class="datetime-picker" size="small" show-time :getCalendarContainer="() => $refs.root"/>
</div>
<div v-else-if="col.dataType === 'select'" :class="['title', {active: col.search.value !== undefined}]">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-select :allowClear="true" :options="col.search.selectOptions" v-model="col.search.value" placeholder="请选择..." @change="onSelectChange(col)" class="select" slot="content" size="small" :get-popup-container="() => $refs.selectRoot">
</a-select>
</div>
<div v-else :class="['title', {active: col.search.value}]">
<a-popover @visibleChange="onVisibleChange(col, index)" v-model="col.search.visible" placement="bottom" :trigger="['click']" :get-popup-container="() => $refs.root">
<template v-if="col.title">
{{col.title}}
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<div class="value " v-if="col.search.value">:&nbsp;&nbsp;{{col.search.format && typeof col.search.format === 'function' ? col.search.format(col.search.value) : col.search.value}}</div>
<a-icon v-if="!col.search.value" class="icon-down" type="down"/>
<div class="operations" slot="content">
<a-button @click="onCancel(col)" class="btn" size="small" type="link">取消</a-button>
<a-button @click="onConfirm(col)" class="btn" size="small" type="primary">确认</a-button>
</div>
<div class="search-overlay" slot="title">
<a-input :id="`${searchIdPrefix}${index}`" :allow-clear="true" @keyup.esc="onCancel(col)" @keyup.enter="onConfirm(col)" v-model="col.search.value" size="small" />
</div>
</a-popover>
<a-icon v-if="col.search.value" @click="e => onCloseClick(e, col)" class="close" type="close-circle" theme="filled"/>
</div>
</div>
</div>
</template>
<script>
import fastEqual from 'fast-deep-equal'
import moment from 'moment'
export default {
name: 'SearchArea',
props: ['columns', 'formatConditions'],
inject: ['table'],
created() {
this.formatColumns(this.columns)
},
watch: {
columns(newVal, oldVal) {
if (newVal != oldVal) {
this.formatColumns(newVal)
}
},
searchCols(newVal, oldVal) {
if (newVal.length != oldVal.length) {
const newConditions = this.getConditions(newVal)
const newSearchOptions = this.getSearchOptions(newVal)
if (!fastEqual(newConditions, this.conditions)) {
this.conditions = newConditions
this.searchOptions = newSearchOptions
this.$emit('change', this.conditions, this.searchOptions)
}
}
}
},
data() {
return {
conditions: {},
searchOptions: []
}
},
computed: {
searchCols() {
return this.columns.filter(item => item.searchAble)
},
searchIdPrefix() {
return this.table.id + '-ipt-'
}
},
methods: {
onCloseClick(e, col) {
e.preventDefault()
e.stopPropagation()
col.search.value = undefined
const {backup, value} = col.search
if (backup !== value) {
this.backupAndEmitChange(col)
}
},
onCancel(col) {
col.search.value = col.search.backup
col.search.visible = false
},
onConfirm(col) {
const {backup, value} = col.search
col.search.visible = false
if (backup !== value) {
this.backupAndEmitChange(col)
}
},
onSwitchChange(col) {
const {backup, value} = col.search
if (backup !== value) {
this.backupAndEmitChange(col)
}
},
onSelectChange(col) {
this.backupAndEmitChange(col)
},
onCalendarOpenChange(open, col) {
col.search.visible = open
const {momentEqual, backupAndEmitChange} = this
const {value, backup, format} = col.search
if (!open && !momentEqual(value, backup, format)) {
backupAndEmitChange(col, moment(value))
}
},
onCalendarChange(date, dateStr, col) {
const {momentEqual, backupAndEmitChange} = this
const {value, backup, format} = col.search
if (!col.search.visible && !momentEqual(value, backup, format)) {
backupAndEmitChange(col, moment(value))
}
},
onDateChange(col) {
const {momentEqual, backupAndEmitChange} = this
const {value, backup, format} = col.search
if (!momentEqual(value, backup, format)) {
backupAndEmitChange(col, moment(value))
}
},
getFormat(col) {
if (col.search && col.search.format) {
return col.search.format
}
const dataType = col.dataType
switch(dataType) {
case 'time': return 'HH:mm:ss'
case 'date': return 'YYYY-MM-DD'
case 'datetime': return 'YYYY-MM-DD HH:mm:ss'
default: return undefined
}
},
backupAndEmitChange(col, backValue = col.search.value) {
const {getConditions, getSearchOptions} = this
col.search.backup = backValue
this.conditions = getConditions(this.searchCols)
this.searchOptions = getSearchOptions(this.searchCols)
this.$emit('change', this.conditions, this.searchOptions)
},
getConditions(columns) {
const conditions = {}
columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)
.forEach(col => {
const {value, format} = col.search
if (this.formatConditions && format) {
if (typeof format === 'function') {
conditions[col.dataIndex] = format(col.search.value)
} else if (typeof format === 'string' && value.constructor.name === 'Moment') {
conditions[col.dataIndex] = value.format(format)
} else {
conditions[col.dataIndex] = value
}
} else {
conditions[col.dataIndex] = value
}
})
return conditions
},
getSearchOptions(columns) {
return columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)
.map(({dataIndex, search}) => ({field: dataIndex, value: search.value, format: search.format}))
},
onVisibleChange(col, index) {
if (!col.search.visible) {
col.search.value = col.search.backup
} else {
let input = document.getElementById(`${this.searchIdPrefix}${index}`)
if (input) {
setTimeout(() => {input.focus()}, 0)
} else {
this.$nextTick(() => {
input = document.getElementById(`${this.searchIdPrefix}${index}`)
input.focus()
})
}
}
},
momentEqual(target, source, format) {
if (target === source) {
return true
} else if (target && source && target.format(format) === source.format(format)) {
return true
}
return false
},
formatColumns(columns) {
columns.forEach(item => {
this.$set(item, 'search', {...item.search, visible: false, value: undefined, format: this.getFormat(item)})
})
}
}
}
</script>
<style scoped lang="less">
.search-area{
.select-root{
text-align: left;
}
margin: -4px 0;
.search-item{
margin: 4px 4px;
display: inline-block;
.title{
padding: 4px 8px;
cursor: pointer;
border-radius: 4px;
user-select: none;
display: inline-flex;
align-items: center;
.close{
color: @text-color-second;
margin-left: 4px;
font-size: 12px;
vertical-align: middle;
:hover{
color: @text-color;
}
}
.switch{
margin-left: 4px;
}
.time-picker{
margin-left: 4px;
width: 96px;
}
.date-picker{
margin-left: 4px;
width: 120px;
}
.datetime-picker{
margin-left: 4px;
width: 195px;
}
.value{
display: inline-block;
overflow: hidden;
flex:1;
vertical-align: middle;
max-width: 144px;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
&.active{
background-color: @layout-bg-color;
}
}
.icon-down{
vertical-align: middle;
font-size: 12px;
}
}
.search-overlay{
padding: 8px 0px;
text-align: center;
}
.select{
margin-left: 4px;
max-width: 144px;
min-width: 96px;
text-align: left;
}
.operations{
display: flex;
margin: -6px 0;
justify-content: space-between;
.btn{
}
}
}
</style>

@ -0,0 +1,2 @@
import AdvanceTable from './AdvanceTable'
export default AdvanceTable

@ -0,0 +1,79 @@
<template>
<div class="task-group">
<div class="task-head">
<h3 class="title"><span v-if="count">{{count}}</span>{{title}}</h3>
<div class="actions" style="float: right">
<a-icon class="add" type="plus" draggable="true"/>
<a-icon class="more" style="margin-left: 8px" type="ellipsis" />
</div>
</div>
<div class="task-content">
<draggable :options="dragOptions">
<slot></slot>
</draggable>
</div>
</div>
</template>
<script>
import Draggable from 'vuedraggable'
const dragOptions = {
sort: true,
scroll: true,
scrollSpeed: 2,
animation: 150,
ghostClass: 'dragable-ghost',
chosenClass: 'dragable-chose',
dragClass: 'dragable-drag'
}
export default {
name: 'TaskGroup',
components: {Draggable},
props: ['title', 'group'],
data () {
return {
dragOptions: {...dragOptions, group: this.group}
}
},
computed: {
count () {
return this.$slots.default.length
}
}
}
</script>
<style lang="less">
.task-group{
width: 33.33%;
padding: 8px 8px;
background-color: @background-color-light;
border-radius: 6px;
border: 1px solid @shadow-color;
.task-head{
margin-bottom: 8px;
.title{
display: inline-block;
span{
display: inline-block;
border-radius: 10px;
margin: 0 8px;
font-size: 12px;
padding: 2px 6px;
background-color: @base-bg-color;
}
}
.actions{
display: inline-block;
float: right;
font-size: 18px;
font-weight: bold;
i{
cursor: pointer;
}
}
}
}
</style>

@ -0,0 +1,25 @@
<template>
<a-card class="task-item" type="inner">
{{content}}
</a-card>
</template>
<script>
export default {
name: 'TaskItem',
props: ['content']
}
</script>
<style lang="less" scoped>
.task-item{
margin-bottom: 16px;
box-shadow: 0 1px 1px @shadow-color;
border-radius: 6px;
& :hover{
cursor: move;
box-shadow: 0 1px 2px @shadow-color;
border-radius: 6px;
}
}
</style>

@ -0,0 +1,59 @@
<template>
<div
:class="['step-item', link ? 'linkable' : null]"
@click="go"
>
<span :style="titleStyle">{{title}}</span>
<a-icon v-if="icon" :style="iconStyle" :type="icon" />
<slot></slot>
</div>
</template>
<script>
const Group = {
name: 'AStepItemGroup',
props: {
align: {
type: String,
default: 'center',
validator(value) {
return ['left', 'center', 'right'].indexOf(value) != -1
}
}
},
render (h) {
return h(
'div',
{attrs: {style: `text-align: ${this.align}; margin-top: 8px`}},
[h('div', {attrs: {style: 'text-align: left; display: inline-block;'}}, [this.$slots.default])]
)
}
}
export default {
name: 'AStepItem',
Group: Group,
props: ['title', 'icon', 'link', 'titleStyle', 'iconStyle'],
methods: {
go () {
const link = this.link
if (link) {
this.$router.push(link)
}
}
}
}
</script>
<style lang="less" scoped>
.step-item{
cursor: pointer;
}
:global{
.ant-steps-item-process{
.linkable{
color: @primary-color;
}
}
}
</style>

@ -0,0 +1,69 @@
<template>
<div class="avatar-list">
<slot>
</slot>
</div>
</template>
<script>
import AAvatar from 'ant-design-vue/es/avatar/Avatar'
import ATooltip from 'ant-design-vue/es/tooltip/Tooltip'
const Item = {
name: 'AvatarListItem',
props: {
size: {
type: String,
required: false,
default: 'small'
},
src: {
type: String,
required: true
},
tips: {
type: String,
required: false
}
},
methods: {
renderAvatar (h, size, src) {
return h(AAvatar, {props: {size: size, src: src}}, [])
}
},
render (h) {
const avatar = this.renderAvatar(h, this.$props.size, this.$props.src)
return h(
'li',
{class: 'avatar-item'},
[this.$props.tips ? h(ATooltip, {props: {title: this.$props.tips}}, [avatar]) : avatar]
)
}
}
export default {
name: 'AvatarList',
Item: Item
}
</script>
<style lang="less" scoped>
.avatar-list {
display: inline-block;
display: inline-block;
margin-left: 8px;
font-size: 0;
.avatar-item {
display: inline-block;
font-size: 14px;
margin-left: -8px;
width: 20px;
height: 20px;
:global {
.ant-avatar {
border: 1px solid #fff;
width: 20px;
height: 20px;
}
}
}
}
</style>

@ -0,0 +1,156 @@
<template>
<div :class="['detail-list', size === 'small' ? 'small' : 'large', layout === 'vertical' ? 'vertical': 'horizontal']">
<div v-if="title" class="title">{{title}}</div>
<a-row>
<slot></slot>
</a-row>
</div>
</template>
<script>
import ACol from 'ant-design-vue/es/grid/Col'
const Item = {
name: 'DetailListItem',
props: {
term: {
type: String,
required: false
}
},
inject: {
col: {
type: Number
}
},
methods: {
renderTerm (h, term) {
return term ? h(
'div',
{
attrs: {
class: 'term'
}
},
[term]
) : null
},
renderContent (h, content) {
return h(
'div',
{
attrs: {
class: 'content'
}
},
[content]
)
}
},
render (h) {
const term = this.renderTerm(h, this.$props.term)
const content = this.renderContent(h, this.$slots.default)
return h(
ACol,
{
props: responsive[this.col]
},
[term, content]
)
}
}
const responsive = {
1: { xs: 24 },
2: { xs: 24, sm: 12 },
3: { xs: 24, sm: 12, md: 8 },
4: { xs: 24, sm: 12, md: 6 }
}
export default {
name: 'DetailList',
Item: Item,
props: {
title: {
type: String,
required: false
},
col: {
type: Number,
required: false,
default: 3
},
size: {
type: String,
required: false,
default: 'large'
},
layout: {
type: String,
required: false,
default: 'horizontal'
}
},
provide () {
return {
col: this.col > 4 ? 4 : this.col
}
}
}
</script>
<style lang="less">
.detail-list{
.title {
font-size: 16px;
color: @title-color;
font-weight: bolder;
margin-bottom: 16px;
}
.term {
// Line-height is 22px IE dom height will calculate error
line-height: 20px;
padding-bottom: 16px;
margin-right: 8px;
color: @title-color;
white-space: nowrap;
display: table-cell;
&:after {
content: ':';
margin: 0 8px 0 2px;
position: relative;
top: -0.5px;
}
}
.content{
line-height: 22px;
width: 100%;
padding-bottom: 16px;
color: @text-color;
display: table-cell;
}
&.small{
.title{
font-size: 14px;
color: @text-color;
font-weight: normal;
margin-bottom: 12px;
}
.term,.content{
padding-bottom: 8px;
}
}
&.large{
.term,.content{
padding-bottom: 16px;
}
}
&.vertical{
.term {
padding-bottom: 8px;
}
.term,.content{
display: block;
}
}
}
</style>

@ -0,0 +1,142 @@
<template>
<div >
<div :class="['mask', visible ? 'open' : 'close']" @click="close"></div>
<div :class="['drawer', placement, visible ? 'open' : 'close']">
<div ref="drawer" class="content beauty-scroll">
<slot></slot>
</div>
<div v-if="showHandler" :class="['handler-container', placement, visible ? 'open' : 'close']" ref="handler" @click="toggle">
<slot v-if="$slots.handler" name="handler"></slot>
<div v-else class="handler">
<a-icon :type="visible ? 'close' : 'bars'" />
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Drawer',
data () {
return {
}
},
model: {
prop: 'visible',
event: 'change'
},
props: {
visible: {
type: Boolean,
required: false,
default: false
},
placement: {
type: String,
required: false,
default: 'left'
},
showHandler: {
type: Boolean,
required: false,
default: true
}
},
methods: {
open () {
this.$emit('change', true)
},
close () {
this.$emit('change', false)
},
toggle () {
this.$emit('change', !this.visible)
}
}
}
</script>
<style lang="less" scoped>
.mask{
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
background-color: @shadow-color;
transition: all 0.5s;
z-index: 100;
&.open{
display: inline-block;
}
&.close{
display: none;
}
}
.drawer{
position: fixed;
transition: all 0.5s;
height: 100vh;
z-index: 100;
&.left{
left: 0px;
&.open{
.content{
box-shadow: 2px 0 8px @shadow-color;
}
}
&.close{
transform: translateX(-100%);
}
}
&.right{
right: 0px;
.content{
float: right;
}
&.open{
.content{
box-shadow: -2px 0 8px @shadow-color;
}
}
&.close{
transform: translateX(100%);
}
}
}
.content{
display: inline-block;
height: 100vh;
overflow-y: auto;
}
.handler-container{
position: absolute;
display: inline-block;
text-align: center;
transition: all 0.5s;
cursor: pointer;
top: 200px;
z-index: 100;
.handler {
height: 40px;
width: 40px;
background-color: @base-bg-color;
font-size: 26px;
box-shadow: 0 2px 8px @shadow-color;
line-height: 40px;
}
&.left{
right: -40px;
.handler{
border-radius: 0 5px 5px 0;
}
}
&.right{
left: -40px;
.handler{
border-radius: 5px 0 0 5px;
}
}
}
</style>

@ -0,0 +1,30 @@
<template>
<div class="toolbar">
<div style="float: left">
<slot name="extra"></slot>
</div>
<div style="float: right">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'FooterToolBar'
}
</script>
<style lang="less" scoped>
.toolbar{
position: fixed;
width: 100%;
bottom: 0;
right: 0;
box-shadow: 0 -1px 2px @shadow-color;
background: @base-bg-color;
border-top: 1px solid @border-color-split;
padding: 12px 24px;
z-index: 9;
}
</style>

@ -0,0 +1,34 @@
<template>
<div class="head-info">
<span>{{title}}</span>
<p>{{content}}</p>
</div>
</template>
<script>
export default {
name: 'HeadInfo',
props: ['title', 'content', 'bordered']
}
</script>
<style lang="less" scoped>
.head-info{
text-align: center;
padding: 0 24px;
flex-grow: 1;
flex-shrink: 0;
align-self: center;
span{
color: @text-color-second;
display: inline-block;
font-size: 14px;
margin-bottom: 4px;
}
p{
color: @text-color;
font-size: 24px;
margin: 0;
}
}
</style>

@ -0,0 +1,83 @@
<template>
<div class="tag-select">
<tag-select-option @click="toggleCheck"></tag-select-option>
<slot></slot>
<a @click="toggle" v-show="showTrigger" ref="trigger" class="trigger"><a-icon style="margin-left: 5px" :type="collapsed ? 'down' : 'up'"/></a>
</div>
</template>
<script>
import TagSelectOption from './TagSelectOption'
export default {
name: 'TagSelect',
Option: TagSelectOption,
components: {TagSelectOption},
data () {
return {
showTrigger: false,
collapsed: true,
screenWidth: document.body.clientWidth,
checkAll: false
}
},
watch: {
screenWidth: function () {
this.showTrigger = this.needTrigger()
},
collapsed: function (val) {
this.$el.style.maxHeight = val ? '39px' : '78px'
}
},
mounted () {
let _this = this
// moutedtriggerbug
setTimeout(() => {
_this.showTrigger = _this.needTrigger()
_this.$refs.trigger.style.display = _this.showTrigger ? 'inline' : 'none'
}, 1)
window.onresize = () => {
return (() => {
window.screenWidth = document.body.clientWidth
_this.screenWidth = window.screenWidth
})()
}
},
methods: {
needTrigger () {
return this.$el.clientHeight < this.$el.scrollHeight || this.$el.scrollHeight > 39
},
toggle () {
this.collapsed = !this.collapsed
},
getAllTags () {
const tagList = this.$children.filter((item) => {
return item.isTagSelectOption
})
return tagList
},
toggleCheck () {
this.checkAll = !this.checkAll
const tagList = this.getAllTags()
tagList.forEach((item) => {
item.checked = this.checkAll
})
}
}
}
</script>
<style lang="less" scoped>
.tag-select{
user-select: none;
position: relative;
overflow: hidden;
max-height: 39px;
padding-right: 50px;
display: inline-block;
}
.trigger{
position: absolute;
top: 0;
right: 0;
}
</style>

@ -0,0 +1,33 @@
<template>
<a-checkable-tag @change="$emit('click')" class="tag-default" v-model="checked">
<slot></slot>
</a-checkable-tag>
</template>
<script>
export default {
name: 'TagSelectOption',
props: {
size: {
type: String,
required: false,
default: 'default'
}
},
data () {
return {
checked: false,
isTagSelectOption: true
}
}
}
</script>
<style lang="less" scoped>
.tag-default{
font-size: 14px;
padding: 0 8px;
height: auto;
margin-right: 24px;
}
</style>

@ -0,0 +1,96 @@
<template>
<transition
v-if="!disabled"
:enter-active-class="`animated ${enterAnimate} page-toggle-enter-active`"
:leave-active-class="`animated ${leaveAnimate} page-toggle-leave-active`"
>
<slot></slot>
</transition>
<div v-else><slot></slot></div>
</template>
<script>
import {preset as animates} from '@/config/default/animate.config'
export default {
name: 'PageToggleTransition',
props: {
disabled: {
type: Boolean,
default: false
},
animate: {
type: String,
validator(value) {
return animates.findIndex(item => item.name == value) != -1
},
default: 'bounce'
},
direction: {
type: String,
validator(value) {
return ['x', 'y', 'left', 'right', 'up', 'down', 'downLeft', 'upRight', 'downRight', 'upLeft', 'downBig',
'upBig', 'downLeft', 'downRight', 'topRight', 'bottomLeft', 'topLeft', 'bottomRight', 'default'].indexOf(value) > -1
}
},
reverse: {
type: Boolean,
default: true
}
},
computed: {
enterAnimate() {
return this.activeClass(false)
},
leaveAnimate() {
return this.activeClass(true)
}
},
methods: {
activeClass(isLeave) {
let animate = animates.find(item => this.animate == item.name)
if (animate == undefined) {
return ''
}
let direction = ''
if (this.direction == undefined) {
direction = animate.directions[0]
} else {
direction = animate.directions.find(item => item == this.direction)
}
direction = (direction == undefined || direction === 'default') ? '' : direction
if (direction != '') {
direction = isLeave && this.reverse ? this.reversePosition(direction, animate.directions) : direction
direction = direction[0].toUpperCase() + direction.substring(1)
}
let t = isLeave ? 'Out' : 'In'
return animate.name + t + direction
},
reversePosition(direction, directions) {
if(direction.length == 0 || direction == 'x' || direction == 'y') {
return direction
}
let index = directions.indexOf(direction)
index = (index % 2 == 1) ? index - 1 : index + 1
return directions[index]
}
}
}
</script>
<style lang="less">
.page-toggle-enter-active{
position: absolute !important;
animation-duration: 0.8s !important;
width: calc(100%) !important;
}
.page-toggle-leave-active{
position: absolute !important;
animation-duration: 0.8s !important;
width: calc(100%) !important;
}
.page-toggle-enter{
}
.page-toggle-leave-to{
}
</style>

@ -0,0 +1,16 @@
// 自定义配置,参考 ./default/setting.config.js需要自定义的属性在这里配置即可
module.exports = {
systemName: 'SlashCard',
theme: {
color: '#13c2c2',
mode: 'dark',
},
multiPage: true,
asyncRoutes: false,
layout: 'side',
hideSetting: true,
animate: {
name: 'lightSpeed',
direction: 'left'
}
}

@ -0,0 +1,18 @@
// admin 配置
const ADMIN = {
palettes: ['#f5222d', '#fa541c', '#fadb14', '#3eaf7c', '#13c2c2', '#1890ff', '#722ed1', '#eb2f96'],
animates: require('./animate.config').preset,
theme: {
mode: {
DARK: 'dark',
LIGHT: 'light',
NIGHT: 'night'
}
},
layout: {
SIDE: 'side',
HEAD: 'head'
}
}
module.exports = ADMIN

@ -0,0 +1,21 @@
const direct_s = ['left', 'right']
const direct_1 = ['left', 'right', 'down', 'up']
const direct_1_b = ['downBig', 'upBig', 'leftBig', 'rightBig']
const direct_2 = ['topLeft', 'bottomRight', 'topRight', 'bottomLeft']
const direct_3 = ['downLeft', 'upRight', 'downRight', 'upLeft']
// animate.css 配置
const ANIMATE = {
preset: [ //预设动画配置
{name: 'back', alias: '渐近', directions: direct_1},
{name: 'bounce', alias: '弹跳', directions: direct_1.concat('default')},
{name: 'fade', alias: '淡化', directions: direct_1.concat(direct_1_b).concat(direct_2).concat('default')},
{name: 'flip', alias: '翻转', directions: ['x', 'y']},
{name: 'lightSpeed', alias: '光速', directions: direct_s},
{name: 'rotate', alias: '旋转', directions: direct_3.concat('default')},
{name: 'roll', alias: '翻滚', directions: ['default']},
{name: 'zoom', alias: '缩放', directions: direct_1.concat('default')},
{name: 'slide', alias: '滑动', directions: direct_1},
]
}
module.exports = ANIMATE

@ -0,0 +1,84 @@
// antd 配置
const ANTD = {
primary: {
color: '#1890ff',
warning: '#faad14',
success: '#52c41a',
error: '#f5222d',
light: {
menuColors: ['#000c17', '#001529', '#002140']
},
dark: {
menuColors: ['#000c17', '#001529', '#002140']
},
night: {
menuColors: ['#151515', '#1f1f1f', '#1e1e1e'],
}
},
theme: {
dark: {
'layout-body-background': '#f0f2f5',
'body-background': '#fff',
'component-background': '#fff',
'heading-color': 'rgba(0, 0, 0, 0.85)',
'text-color': 'rgba(0, 0, 0, 0.65)',
'text-color-inverse': '#fff',
'text-color-secondary': 'rgba(0, 0, 0, 0.45)',
'shadow-color': 'rgba(0, 0, 0, 0.15)',
'border-color-split': '#f0f0f0',
'background-color-light': '#fafafa',
'background-color-base': '#f5f5f5',
'table-selected-row-bg': '#fafafa',
'table-expanded-row-bg': '#fbfbfb',
'checkbox-check-color': '#fff',
'disabled-color': 'rgba(0, 0, 0, 0.25)',
'menu-dark-color': 'rgba(254, 254, 254, 0.65)',
'menu-dark-highlight-color': '#fefefe',
'menu-dark-arrow-color': '#fefefe',
'btn-primary-color': '#fff',
},
light: {
'layout-body-background': '#f0f2f5',
'body-background': '#fff',
'component-background': '#fff',
'heading-color': 'rgba(0, 0, 0, 0.85)',
'text-color': 'rgba(0, 0, 0, 0.65)',
'text-color-inverse': '#fff',
'text-color-secondary': 'rgba(0, 0, 0, 0.45)',
'shadow-color': 'rgba(0, 0, 0, 0.15)',
'border-color-split': '#f0f0f0',
'background-color-light': '#fafafa',
'background-color-base': '#f5f5f5',
'table-selected-row-bg': '#fafafa',
'table-expanded-row-bg': '#fbfbfb',
'checkbox-check-color': '#fff',
'disabled-color': 'rgba(0, 0, 0, 0.25)',
'menu-dark-color': 'rgba(1, 1, 1, 0.65)',
'menu-dark-highlight-color': '#fefefe',
'menu-dark-arrow-color': '#fefefe',
'btn-primary-color': '#fff',
},
night: {
'layout-body-background': '#000',
'body-background': '#141414',
'component-background': '#141414',
'heading-color': 'rgba(255, 255, 255, 0.85)',
'text-color': 'rgba(255, 255, 255, 0.85)',
'text-color-inverse': '#141414',
'text-color-secondary': 'rgba(255, 255, 255, 0.45)',
'shadow-color': 'rgba(255, 255, 255, 0.15)',
'border-color-split': '#303030',
'background-color-light': '#ffffff0a',
'background-color-base': '#2a2a2a',
'table-selected-row-bg': '#ffffff0a',
'table-expanded-row-bg': '#ffffff0b',
'checkbox-check-color': '#141414',
'disabled-color': 'rgba(255, 255, 255, 0.25)',
'menu-dark-color': 'rgba(254, 254, 254, 0.65)',
'menu-dark-highlight-color': '#fefefe',
'menu-dark-arrow-color': '#fefefe',
'btn-primary-color': '#141414',
}
}
}
module.exports = ANTD

@ -0,0 +1,6 @@
const ANTD = require('./antd.config')
const ADMIN = require('./admin.config')
const ANIMATE = require('./animate.config')
const setting = require('./setting.config')
module.exports = {ANTD, ADMIN, ANIMATE, setting}

@ -0,0 +1,35 @@
// 此配置為系統預設設定需修改的設定項在src/config/config.js中新增修改項即可。也可直接在此檔案中修改。
module.exports = {
lang: 'TW', //語言,可選 CN(簡體)、TW(繁體)、US(英語),也可擴充套件其它語言
theme: { //主題
color: '#1890ff', //主題色
mode: 'dark', //主題模式 可選 dark、 light 和 night
success: '#52c41a', //成功色
warning: '#faad14', //警告色
error: '#f5222f', //錯誤色
},
layout: 'side', //導航佈局,可選 side 和 head分別為側邊導航和頂部導航
fixedHeader: false, //固定頭部狀態列true:固定false:不固定
fixedSideBar: true, //固定側邊欄true:固定false:不固定
fixedTabs: false, //固定頁籤頭true:固定false:不固定
pageWidth: 'fixed', //內容區域寬度fixed:固定寬度fluid:流式寬度
weekMode: false, //色弱模式true:開啟false:不開啟
multiPage: false, //多頁籤模式true:開啟false:不開啟
cachePage: true, //是否快取頁面資料僅多頁籤模式下生效true 快取, false 不快取
hideSetting: true, //隱藏設定抽屜true:隱藏false:不隱藏
systemName: 'UTel電子名片系統', //系統名稱
copyright: '2021 ZL corp.', //copyright
asyncRoutes: false, //非同步載入路由true:開啟false:不開啟
showPageTitle: true, //是否顯示頁面標題PageLayout 佈局中的頁面標題true:顯示false:不顯示
filterMenu: true, //根據許可權過濾選單true:過濾false:不過濾
animate: { //動畫設定
disabled: false, //禁用動畫true:禁用false:啟用
name: 'bounce', //動畫效果,支援的動畫效果可參考 ./animate.config.js
direction: 'left' //動畫方向,切換頁面時動畫的方向,參考 ./animate.config.js
},
footerLinks: [ //頁面底部連結,{link: '連結地址', name: '名稱/顯示文字', icon: '圖示,支援 ant design vue 圖示庫'}
// {link: 'https://pro.ant.design', name: 'Pro首頁'},
// {link: 'https://github.com/iczer/vue-antd-admin', icon: 'github'},
// {link: 'https://ant.design', name: 'Ant Design'}
],
}

@ -0,0 +1,6 @@
const deepMerge = require('deepmerge')
const _config = require('./config')
const {setting} = require('./default')
const config = deepMerge(setting, _config)
module.exports = config

@ -0,0 +1,10 @@
/**
* webpack-theme-color-replacer 配置
* webpack-theme-color-replacer 是一个高效的主题色替换插件可以实现系统运行时动态切换主题功能
* 但有些情景下我们需要为 webpack-theme-color-replacer 配置一些规则以达到我们的个性化需求的目的
*
* @cssResolve: css处理规则 webpack-theme-color-replacer 提取 需要替换主题色的 css 应用此规则一般在
* webpack-theme-color-replacer 默认规则无法达到我们的要求时使用
*/
const cssResolve = require('./resolve.config')
module.exports = {cssResolve}

@ -0,0 +1,67 @@
/**
* webpack-theme-color-replacer 插件的 resolve 配置
* 为特定的 css 选择器selector配置 resolve 规则
*
* key css selector 值或合法的正则表达式字符串
* key 设置 css selector 值时会匹配对应的 css
* key 设置为正则表达式时会匹配所有满足此正则表达式的的 css
*
* value 可以设置为 boolean false 一个对象
* value false 则会忽略此 css即此 css 不纳入 webpack-theme-color-replacer 管理
* value 对象时会调用该对象的 resolve 函数并传入 cssText原始的 css文本 cssObjcss对象参数; resolve函数应该返
* 回一个处理后的合法的 css字符串包含 selector
* 注意: value 不能设置为 true
*/
const cssResolve = {
'.ant-checkbox-checked .ant-checkbox-inner::after': {
resolve(cssText, cssObj) {
cssObj.rules.push('border-top:0', 'border-left:0')
return cssObj.toText()
}
},
'.ant-tree-checkbox-checked .ant-tree-checkbox-inner::after': {
resolve(cssText, cssObj) {
cssObj.rules.push('border-top:0', 'border-left:0')
return cssObj.toText()
}
},
'.ant-checkbox-checked .ant-checkbox-inner:after': {
resolve(cssText, cssObj) {
cssObj.rules.push('border-top:0', 'border-left:0')
return cssObj.toText()
}
},
'.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after': {
resolve(cssText, cssObj) {
cssObj.rules.push('border-top:0', 'border-left:0')
return cssObj.toText()
}
},
'.ant-menu-dark .ant-menu-inline.ant-menu-sub': {
resolve(cssText, cssObj) {
cssObj.rules = cssObj.rules.filter(rule => rule.indexOf('box-shadow') == -1)
return cssObj.toText()
}
},
'.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu:hover,.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-submenu-selected': {
resolve(cssText, cssObj) {
cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)')
return cssObj.toText()
}
},
'.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover': {
resolve(cssText, cssObj) {
cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)')
return cssObj.toText()
}
},
'.ant-layout-sider': {
resolve(cssText, cssObj) {
cssObj.selector = '.ant-layout-sider-dark'
return cssObj.toText()
}
},
'/keyframes/': false
}
module.exports = cssResolve

@ -0,0 +1,175 @@
<template>
<a-layout :class="['admin-layout', 'beauty-scroll']">
<drawer v-if="isMobile" v-model="drawerOpen">
<side-menu :theme="theme.mode" :menuData="menuData" :collapsed="false" :collapsible="false" @menuSelect="onMenuSelect"/>
</drawer>
<side-menu :class="[fixedSideBar ? 'fixed-side' : '']" :theme="theme.mode" v-else-if="layout === 'side' || layout === 'mix'" :menuData="sideMenuData" :collapsed="collapsed" :collapsible="true" />
<div v-if="fixedSideBar && !isMobile" :style="`width: ${sideMenuWidth}; min-width: ${sideMenuWidth};max-width: ${sideMenuWidth};`" class="virtual-side"></div>
<drawer v-if="!hideSetting" v-model="showSetting" placement="right">
<div class="setting" slot="handler">
<a-icon :type="showSetting ? 'close' : 'setting'"/>
</div>
<setting />
</drawer>
<a-layout class="admin-layout-main beauty-scroll">
<admin-header :class="[{'fixed-tabs': fixedTabs, 'fixed-header': fixedHeader, 'multi-page': multiPage}]" :style="headerStyle" :menuData="headMenuData" :collapsed="collapsed" @toggleCollapse="toggleCollapse"/>
<a-layout-header :class="['virtual-header', {'fixed-tabs' : fixedTabs, 'fixed-header': fixedHeader, 'multi-page': multiPage}]" v-show="fixedHeader"></a-layout-header>
<a-layout-content class="admin-layout-content" :style="`min-height: ${minHeight}px;`">
<div style="position: relative">
<slot></slot>
</div>
</a-layout-content>
<a-layout-footer style="padding: 0px">
<page-footer :link-list="footerLinks" :copyright="copyright" />
</a-layout-footer>
</a-layout>
</a-layout>
</template>
<script>
import AdminHeader from './header/AdminHeader'
import PageFooter from './footer/PageFooter'
import Drawer from '../components/tool/Drawer'
import SideMenu from '../components/menu/SideMenu'
import Setting from '../components/setting/Setting'
import {mapState, mapMutations, mapGetters} from 'vuex'
// const minHeight = window.innerHeight - 64 - 122
export default {
name: 'AdminLayout',
components: {Setting, SideMenu, Drawer, PageFooter, AdminHeader},
data () {
return {
minHeight: window.innerHeight - 64 - 122,
collapsed: false,
showSetting: false,
drawerOpen: false
}
},
provide() {
return {
adminLayout: this
}
},
watch: {
$route(val) {
this.setActivated(val)
},
layout() {
this.setActivated(this.$route)
},
isMobile(val) {
if(!val) {
this.drawerOpen = false
}
}
},
computed: {
...mapState('setting', ['isMobile', 'theme', 'layout', 'footerLinks', 'copyright', 'fixedHeader', 'fixedSideBar',
'fixedTabs', 'hideSetting', 'multiPage']),
...mapGetters('setting', ['firstMenu', 'subMenu', 'menuData']),
sideMenuWidth() {
return this.collapsed ? '80px' : '200px'
},
headerStyle() {
let width = (this.fixedHeader && this.layout !== 'head' && !this.isMobile) ? `calc(100% - ${this.sideMenuWidth})` : '100%'
let position = this.fixedHeader ? 'fixed' : 'static'
return `width: ${width}; position: ${position};`
},
headMenuData() {
const {layout, menuData, firstMenu} = this
return layout === 'mix' ? firstMenu : menuData
},
sideMenuData() {
const {layout, menuData, subMenu} = this
return layout === 'mix' ? subMenu : menuData
}
},
methods: {
...mapMutations('setting', ['correctPageMinHeight', 'setActivatedFirst']),
toggleCollapse () {
this.collapsed = !this.collapsed
},
onMenuSelect () {
this.toggleCollapse()
},
setActivated(route) {
if (this.layout === 'mix') {
let matched = route.matched
matched = matched.slice(0, matched.length - 1)
const {firstMenu} = this
for (let menu of firstMenu) {
if (matched.findIndex(item => item.path === menu.fullPath) !== -1) {
this.setActivatedFirst(menu.fullPath)
break
}
}
}
}
},
created() {
this.correctPageMinHeight(this.minHeight - 24)
this.setActivated(this.$route)
},
beforeDestroy() {
this.correctPageMinHeight(-this.minHeight + 24)
}
}
</script>
<style lang="less" scoped>
.admin-layout{
.side-menu{
&.fixed-side{
position: fixed;
height: 100vh;
left: 0;
top: 0;
}
}
.virtual-side{
transition: all 0.2s;
}
.virtual-header{
transition: all 0.2s;
opacity: 0;
&.fixed-tabs.multi-page:not(.fixed-header){
height: 0;
}
}
.admin-layout-main{
.admin-header{
top: 0;
right: 0;
overflow: hidden;
transition: all 0.2s;
&.fixed-tabs.multi-page:not(.fixed-header){
height: 0;
}
}
}
.admin-layout-content{
padding: 24px 5px 0;
/*overflow-x: hidden;*/
/*min-height: calc(100vh - 64px - 122px);*/
}
.setting{
background-color: @primary-color;
color: @base-bg-color;
border-radius: 5px 0 0 5px;
line-height: 40px;
font-size: 22px;
width: 40px;
height: 40px;
box-shadow: -2px 0 8px @shadow-color;
}
}
</style>

@ -0,0 +1,22 @@
<template>
<page-toggle-transition :disabled="animate.disabled" :animate="animate.name" :direction="animate.direction">
<router-view />
</page-toggle-transition>
</template>
<script>
import PageToggleTransition from '../components/transition/PageToggleTransition';
import {mapState} from 'vuex'
export default {
name: 'BlankView',
components: {PageToggleTransition},
computed: {
...mapState('setting', ['multiPage', 'animate'])
}
}
</script>
<style scoped>
</style>

@ -0,0 +1,42 @@
<template>
<div class="common-layout">
<div class="content"><slot></slot></div>
<page-footer :link-list="footerLinks" :copyright="copyright"></page-footer>
</div>
</template>
<script>
import PageFooter from '@/layouts/footer/PageFooter'
import {mapState} from 'vuex'
export default {
name: 'CommonLayout',
components: {PageFooter},
computed: {
...mapState('setting', ['footerLinks', 'copyright'])
}
}
</script>
<style scoped lang="less">
.common-layout{
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background-color: @layout-body-background;
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position-x: center;
background-position-y: 110px;
background-size: 100%;
.content{
padding: 32px 0;
flex: 1;
@media (min-width: 768px){
padding: 112px 0 24px;
}
}
}
</style>

@ -0,0 +1,144 @@
<template>
<div class="page-layout">
<page-header ref="pageHeader" :style="`margin-top: ${multiPage ? 0 : -24}px`" :breadcrumb="breadcrumb" :title="pageTitle" :logo="logo" :avatar="avatar">
<slot name="action" slot="action"></slot>
<slot slot="content" name="headerContent"></slot>
<div slot="content" v-if="!this.$slots.headerContent && desc">
<p>{{desc}}</p>
<div v-if="this.linkList" class="link">
<template v-for="(link, index) in linkList">
<a :key="index" :href="link.href"><a-icon :type="link.icon" />{{link.title}}</a>
</template>
</div>
</div>
<slot v-if="this.$slots.extra" slot="extra" name="extra"></slot>
</page-header>
<div ref="page" :class="['page-content', layout, pageWidth]" >
<slot></slot>
</div>
</div>
</template>
<script>
import PageHeader from '@/components/page/header/PageHeader'
import {mapState, mapMutations} from 'vuex'
import {getI18nKey} from '@/utils/routerUtil'
export default {
name: 'PageLayout',
components: {PageHeader},
props: ['desc', 'logo', 'title', 'avatar', 'linkList', 'extraImage'],
data () {
return {
page: {},
pageHeaderHeight: 0,
}
},
watch: {
$route() {
this.page = this.$route.meta.page
}
},
updated() {
if (!this._inactive) {
this.updatePageHeight()
}
},
activated() {
this.updatePageHeight()
},
deactivated() {
this.updatePageHeight(0)
},
mounted() {
this.updatePageHeight()
},
created() {
this.page = this.$route.meta.page
},
beforeDestroy() {
this.updatePageHeight(0)
},
computed: {
...mapState('setting', ['layout', 'multiPage', 'pageMinHeight', 'pageWidth', 'customTitles']),
pageTitle() {
let pageTitle = this.page && this.page.title
return this.customTitle || (pageTitle && this.$t(pageTitle)) || this.title || this.routeName
},
routeName() {
const route = this.$route
return this.$t(getI18nKey(route.matched[route.matched.length - 1].path))
},
breadcrumb() {
let page = this.page
let breadcrumb = page && page.breadcrumb
if (breadcrumb) {
let i18nBreadcrumb = []
breadcrumb.forEach(item => {
i18nBreadcrumb.push(this.$t(item))
})
return i18nBreadcrumb
} else {
return this.getRouteBreadcrumb()
}
},
marginCorrect() {
return this.multiPage ? 24 : 0
}
},
methods: {
...mapMutations('setting', ['correctPageMinHeight']),
getRouteBreadcrumb() {
let routes = this.$route.matched
const path = this.$route.path
let breadcrumb = []
routes.filter(item => path.includes(item.path))
.forEach(route => {
const path = route.path.length === 0 ? '/home' : route.path
breadcrumb.push(this.$t(getI18nKey(path)))
})
let pageTitle = this.page && this.page.title
if (this.customTitle || pageTitle) {
breadcrumb[breadcrumb.length - 1] = this.customTitle || pageTitle
}
return breadcrumb
},
/**
* 用于计算页面内容最小高度
* @param newHeight
*/
updatePageHeight(newHeight = this.$refs.pageHeader.$el.offsetHeight + this.marginCorrect) {
this.correctPageMinHeight(this.pageHeaderHeight - newHeight)
this.pageHeaderHeight = newHeight
}
}
}
</script>
<style lang="less">
.page-header{
margin: 0 -24px 0;
}
.link{
/*margin-top: 16px;*/
line-height: 24px;
a{
font-size: 14px;
margin-right: 32px;
i{
font-size: 22px;
margin-right: 8px;
}
}
}
.page-content{
position: relative;
padding: 5px 0 0;
&.side{
}
&.head.fixed{
margin: 0 auto;
max-width: 1400px;
}
}
</style>

@ -0,0 +1,55 @@
<template>
<page-layout :desc="desc" :linkList="linkList">
<div v-if="this.extraImage && !isMobile" slot="extra" class="extraImg">
<img :src="extraImage"/>
</div>
<page-toggle-transition :disabled="animate.disabled" :animate="animate.name" :direction="animate.direction">
<router-view ref="page" />
</page-toggle-transition>
</page-layout>
</template>
<script>
import PageLayout from './PageLayout'
import PageToggleTransition from '../components/transition/PageToggleTransition';
import {mapState} from 'vuex'
export default {
name: 'PageView',
components: {PageToggleTransition, PageLayout},
data () {
return {
page: {}
}
},
computed: {
...mapState('setting', ['isMobile', 'multiPage', 'animate']),
desc() {
return this.page.desc
},
linkList() {
return this.page.linkList
},
extraImage() {
return this.page.extraImage
}
},
mounted () {
this.page = this.$refs.page
},
updated () {
this.page = this.$refs.page
}
}
</script>
<style lang="less" scoped>
.extraImg{
margin-top: -60px;
text-align: center;
width: 195px;
img{
width: 100%;
}
}
</style>

@ -0,0 +1,45 @@
<template>
<div class="footer">
<div class="links">
<a target="_blank" :key="index" :href="item.link ? item.link : 'javascript: void(0)'" v-for="(item, index) in linkList">
<a-icon v-if="item.icon" :type="item.icon"/>{{item.name}}
</a>
</div>
<div class="copyright">
Copyright<a-icon type="copyright" />{{copyright}}
</div>
</div>
</template>
<script>
export default {
name: 'PageFooter',
props: ['copyright', 'linkList']
}
</script>
<style lang="less" scoped>
.footer{
padding: 48px 16px 24px;
/*margin: 48px 0 24px;*/
text-align: center;
.copyright{
color: @text-color-second;
font-size: 14px;
i {
margin: 0 4px;
}
}
.links{
margin-bottom: 8px;
a:not(:last-child) {
margin-right: 40px;
}
a{
color: @text-color-second;
-webkit-transition: all .3s;
transition: all .3s;
}
}
}
</style>

@ -0,0 +1,81 @@
<template>
<a-layout-header :class="[headerTheme, 'admin-header']">
<div :class="['admin-header-wide', layout, pageWidth]">
<router-link v-if="isMobile || layout === 'head'" to="/" :class="['logo', isMobile ? null : 'pc', headerTheme]">
<img width="32" src="@/assets/images/logo.png" />
<h1 v-if="!isMobile">{{systemName}}</h1>
</router-link>
<!-- <a-divider v-if="isMobile" type="vertical" /> -->
<a-icon v-if="layout !== 'head' && !isMobile" class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggleCollapse"/>
<div v-if="layout !== 'side' && !isMobile" class="admin-header-menu" :style="`width: ${menuWidth};`">
<i-menu class="head-menu" :theme="headerTheme" mode="horizontal" :options="menuData" @select="onSelect"/>
</div>
<div :class="['admin-header-right', headerTheme]">
<header-avatar class="header-item"/>
<!-- <a-dropdown class="lang header-item">
<div>
<a-icon type="global"/> {{langAlias}}
</div>
<a-menu @click="val => setLang(val.key)" :selected-keys="[lang]" slot="overlay">
<a-menu-item v-for=" lang in langList" :key="lang.key">{{lang.key.toLowerCase() + ' ' + lang.name}}</a-menu-item>
</a-menu>
</a-dropdown> -->
</div>
</div>
</a-layout-header>
</template>
<script>
// import HeaderNotice from './HeaderNotice'
import HeaderAvatar from './HeaderAvatar'
import IMenu from '@/components/menu/menu'
import {mapState, mapMutations} from 'vuex'
export default {
name: 'AdminHeader',
components: {IMenu, HeaderAvatar},
props: ['collapsed', 'menuData'],
data() {
return {
langList: [
{key: 'TW', name: '繁體中文', alias: '繁體'},
{key: 'CN', name: '简体中文', alias: '简体'},
{key: 'US', name: 'English', alias: 'EN'}
],
searchActive: false
}
},
computed: {
...mapState('setting', ['theme', 'isMobile', 'layout', 'systemName', 'lang', 'pageWidth']),
headerTheme () {
if (this.layout == 'side' && this.theme.mode == 'dark' && !this.isMobile) {
return 'light'
}
return this.theme.mode
},
langAlias() {
let lang = this.langList.find(item => item.key == this.lang)
return lang.alias
},
menuWidth() {
const {layout, searchActive} = this
const headWidth = layout === 'head' ? '100% - 188px' : '100%'
const extraWidth = searchActive ? '600px' : '400px'
return `calc(${headWidth} - ${extraWidth})`
}
},
methods: {
toggleCollapse () {
this.$emit('toggleCollapse')
},
onSelect (obj) {
this.$emit('menuSelect', obj)
},
...mapMutations('setting', ['setLang'])
}
}
</script>
<style lang="less" scoped>
@import "index";
</style>

@ -0,0 +1,60 @@
<template>
<a-dropdown>
<div class="header-avatar" style="cursor: pointer">
<!-- <a-avatar class="avatar" size="small" shape="circle" :src="user.avatar"/> -->
<span class="name">{{user.name}}</span>
</div>
<a-menu :class="['avatar-menu']" slot="overlay">
<!-- <a-menu-item>
<a-icon type="user" />
<span>個人中心</span>
</a-menu-item>
<a-menu-item>
<a-icon type="setting" />
<span>設定</span>
</a-menu-item> -->
<!-- <a-menu-divider /> -->
<a-menu-item @click="logout">
<a-icon style="margin-right: 8px;" type="poweroff" />
<span>登出</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<script>
import {mapGetters} from 'vuex'
import {logout} from '@/services/admin'
export default {
name: 'HeaderAvatar',
computed: {
...mapGetters('account', ['user']),
},
methods: {
logout() {
logout()
this.$router.push('/login')
}
}
}
</script>
<style lang="less">
.header-avatar{
display: inline-flex;
.avatar, .name{
align-self: center;
}
.avatar{
margin-right: 8px;
}
.name{
font-weight: 500;
}
}
.avatar-menu{
width: 150px;
}
</style>

@ -0,0 +1,92 @@
<template>
<a-dropdown :trigger="['click']" v-model="show">
<div slot="overlay">
<a-spin :spinning="loading">
<a-tabs class="dropdown-tabs" :tabBarStyle="{textAlign: 'center'}" :style="{width: '297px'}">
<a-tab-pane tab="通知" key="1">
<a-list class="tab-pane">
<a-list-item>
<a-list-item-meta title="你收到了 14 份新周报" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png"/>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta title="你推荐的 曲妮妮 已通过第三轮面试" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png"/>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-list-item-meta title="这种模板可以区分多种通知类型" description="一年前">
<a-avatar style="background-color: white" slot="avatar" src="https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png"/>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane tab="消息" key="2">
<a-list class="tab-pane"></a-list>
</a-tab-pane>
<a-tab-pane tab="待办" key="3">
<a-list class="tab-pane"></a-list>
</a-tab-pane>
</a-tabs>
</a-spin>
</div>
<span @click="fetchNotice" class="header-notice">
<a-badge class="notice-badge" count="12">
<a-icon :class="['header-notice-icon']" type="bell" />
</a-badge>
</span>
</a-dropdown>
</template>
<script>
export default {
name: 'HeaderNotice',
data () {
return {
loading: false,
show: false
}
},
computed: {
},
methods: {
fetchNotice () {
if (this.loading) {
this.loading = false
return
}
this.loadding = true
setTimeout(() => {
this.loadding = false
}, 1000)
}
}
}
</script>
<style lang="less">
.header-notice{
display: inline-block;
transition: all 0.3s;
span {
vertical-align: initial;
}
.notice-badge{
color: inherit;
.header-notice-icon{
font-size: 16px;
padding: 4px;
}
}
}
.dropdown-tabs{
background-color: @base-bg-color;
box-shadow: 0 2px 8px @shadow-color;
border-radius: 4px;
.tab-pane{
padding: 0 24px 12px;
min-height: 250px;
}
}
</style>

@ -0,0 +1,67 @@
<template>
<div class="header-search">
<a-icon type="search" class="search-icon" @click="enterSearchMode"/>
<a-auto-complete
ref="input"
:getPopupContainer="e => {return e.parentNode || document.body}"
:dataSource="dataSource"
:class="['search-input', searchMode ? 'enter' : 'leave']"
placeholder="站内搜索"
@blur="leaveSearchMode"
>
</a-auto-complete>
</div>
</template>
<script>
export default {
name: 'HeaderSearch',
data () {
return {
dataSource: ['选项一', '选项二'],
searchMode: false
}
},
methods: {
enterSearchMode () {
this.searchMode = true
this.$emit('active', true)
setTimeout(() => this.$refs.input.focus(), 300)
},
leaveSearchMode () {
this.searchMode = false
setTimeout(() => this.$emit('active', false), 300)
}
}
}
</script>
<style lang="less">
.header-search{
.search-icon{
font-size: 16px;
cursor: pointer;
}
.search-input{
border: 0;
border-bottom: 1px solid @border-color-split;
transition: width 0.3s ease-in-out;
input{
border: 0;
box-shadow: 0 0 0 0;
}
&.leave{
width: 0px;
input{
display: none;
}
}
&.enter{
width: 200px;
input:focus{
box-shadow: 0 0 0 0;
}
}
}
}
</style>

@ -0,0 +1,92 @@
.admin-header{
padding: 0;
z-index: 2;
box-shadow: @shadow-down;
position: relative;
background: @base-bg-color;
.head-menu{
height: 64px;
line-height: 64px;
vertical-align: middle;
box-shadow: none;
}
&.dark{
background: @header-bg-color-dark;
color: white;
}
&.night{
.head-menu{
background: @base-bg-color;
}
}
.admin-header-wide{
padding-left: 24px;
&.head.fixed{
max-width: 1400px;
margin: auto;
padding-left: 0;
}
&.side{
padding-right: 12px;
}
.logo {
height: 64px;
line-height: 58px;
vertical-align: top;
display: inline-block;
padding: 0 12px 0 24px;
cursor: pointer;
font-size: 20px;
color: inherit;
&.pc{
padding: 0 12px 0 0;
}
img {
vertical-align: middle;
}
h1{
color: inherit;
display: inline-block;
font-size: 16px;
}
}
.trigger {
font-size: 20px;
line-height: 64px;
padding: 0 0px;
cursor: pointer;
transition: color .3s;
&:hover{
color: @primary-color;
}
}
.admin-header-menu{
display: inline-block;
}
.admin-header-right{
float: right;
display: flex;
color: inherit;
.header-item{
color: inherit;
padding: 0 12px;
cursor: pointer;
align-self: center;
a{
color: inherit;
i{
font-size: 16px;
}
}
}
each(@theme-list, {
&.@{value} .header-item{
&:hover{
@class: ~'hover-bg-color-@{value}';
background-color: @@class;
}
}
})
}
}
}

@ -0,0 +1,188 @@
<template>
<div :class="['tabs-head', layout, pageWidth]">
<a-tabs
type="editable-card"
:class="['tabs-container', layout, pageWidth, {'affixed' : affixed, 'fixed-header' : fixedHeader, 'collapsed' : adminLayout.collapsed}]"
:active-key="active"
:hide-add="true"
>
<a-tooltip placement="left" :title="lockTitle" slot="tabBarExtraContent">
<a-icon
theme="filled"
@click="onLockClick"
class="header-lock"
:type="fixedTabs ? 'lock' : 'unlock'"
/>
</a-tooltip>
<a-tab-pane v-for="page in pageList" :key="page.path">
<div slot="tab" class="tab" @contextmenu="e => onContextmenu(page.path, e)">
<a-icon @click="onRefresh(page)" :class="['icon-sync', {'hide': page.path !== active && !page.loading}]" :type="page.loading ? 'loading' : 'sync'" />
<div class="title" @click="onTabClick(page.path)" >{{pageName(page)}}</div>
<a-icon v-if="!page.unclose" @click="onClose(page.path)" class="icon-close" type="close"/>
</div>
</a-tab-pane>
</a-tabs>
<div v-if="affixed" class="virtual-tabs"></div>
</div>
</template>
<script>
import {mapState, mapMutations} from 'vuex'
import {getI18nKey} from '@/utils/routerUtil'
export default {
name: 'TabsHead',
i18n: {
messages: {
TW: {
lock: '點擊鎖定頁簽頭',
unlock: '點擊解除鎖定',
},
CN: {
lock: '点击锁定页签头',
unlock: '点击解除锁定',
},
US: {
lock: 'click to lock the tabs head',
unlock: 'click to unlock',
}
}
},
props: {
pageList: Array,
active: String,
fixed: Boolean
},
data() {
return {
affixed: false,
}
},
inject:['adminLayout'],
created() {
this.affixed = this.fixedTabs
},
computed: {
...mapState('setting', ['layout', 'pageWidth', 'fixedHeader', 'fixedTabs', 'customTitles']),
lockTitle() {
return this.$t(this.fixedTabs ? 'unlock' : 'lock')
}
},
methods: {
...mapMutations('setting', ['setFixedTabs']),
onLockClick() {
this.setFixedTabs(!this.fixedTabs)
if (this.fixedTabs) {
setTimeout(() => {
this.affixed = true
}, 200)
} else {
this.affixed = false
}
},
onTabClick(key) {
if (this.active !== key) {
this.$emit('change', key)
}
},
onClose(key) {
this.$emit('close', key)
},
onRefresh(page) {
this.$emit('refresh', page.path, page)
},
onContextmenu(pageKey, e) {
this.$emit('contextmenu', pageKey, e)
},
pageName(page) {
const pagePath = page.fullPath.split('?')[0]
const custom = this.customTitles.find(item => item.path === pagePath)
return (custom && custom.title) || page.title || this.$t(getI18nKey(page.keyPath))
}
}
}
</script>
<style scoped lang="less">
.tab{
margin: 0 -16px;
padding: 0 16px;
font-size: 14px;
user-select: none;
transition: all 0.2s;
.title{
display: inline-block;
height: 100%;
}
.icon-close{
font-size: 12px;
margin-left: 6px;
margin-right: -4px !important;
color: @text-color-second;
&:hover{
color: @text-color;
}
}
.icon-sync{
margin-left: -4px;
color: @primary-4;
transition: all 0.3s ease-in-out;
&:hover{
color: @primary-color;
}
font-size: 14px;
&.hide{
font-size: 0;
}
}
}
.tabs-head{
margin: 0 auto;
&.head.fixed{
width: 1400px;
}
}
.tabs-container{
margin: -16px auto 8px;
transition: top,left 0.2s;
.header-lock{
font-size: 18px;
cursor: pointer;
color: @primary-3;
&:hover{
color: @primary-color;
}
}
&.affixed{
margin: 0 auto;
top: 0px;
padding: 8px 24px 0;
position: fixed;
height: 48px;
z-index: 1;
background-color: @layout-body-background;
&.side,&.mix{
right: 0;
left: 200px;
&.collapsed{
left: 80px;
}
}
&.head{
width: inherit;
padding: 8px 0 0;
&.fluid{
left: 0;
right: 0;
padding: 8px 24px 0;
}
}
&.fixed-header{
top: 64px;
}
}
}
.virtual-tabs{
height: 48px;
}
</style>

@ -0,0 +1,330 @@
<template>
<admin-layout>
<contextmenu :itemList="menuItemList" :visible.sync="menuVisible" @select="onMenuSelect" />
<tabs-head
v-if="multiPage"
:active="activePage"
:page-list="pageList"
@change="changePage"
@close="remove"
@refresh="refresh"
@contextmenu="onContextmenu"
/>
<div :class="['tabs-view-content', layout, pageWidth]" :style="`margin-top: ${multiPage ? -24 : 0}px`">
<page-toggle-transition :disabled="animate.disabled" :animate="animate.name" :direction="animate.direction">
<a-keep-alive :exclude-keys="excludeKeys" v-if="multiPage && cachePage" v-model="clearCaches">
<router-view v-if="!refreshing" ref="tabContent" :key="$route.path" />
</a-keep-alive>
<router-view ref="tabContent" v-else-if="!refreshing" />
</page-toggle-transition>
</div>
</admin-layout>
</template>
<script>
import AdminLayout from '@/layouts/AdminLayout'
import Contextmenu from '@/components/menu/Contextmenu'
import PageToggleTransition from '@/components/transition/PageToggleTransition'
import {mapState, mapMutations} from 'vuex'
import {getI18nKey} from '@/utils/routerUtil'
import AKeepAlive from '@/components/cache/AKeepAlive'
import TabsHead from '@/layouts/tabs/TabsHead'
export default {
name: 'TabsView',
i18n: require('./i18n'),
components: {TabsHead, PageToggleTransition, Contextmenu, AdminLayout , AKeepAlive },
data () {
return {
clearCaches: [],
pageList: [],
activePage: '',
menuVisible: false,
refreshing: false,
excludeKeys: []
}
},
computed: {
...mapState('setting', ['multiPage', 'cachePage', 'animate', 'layout', 'pageWidth']),
menuItemList() {
return [
{ key: '1', icon: 'vertical-right', text: this.$t('closeLeft') },
{ key: '2', icon: 'vertical-left', text: this.$t('closeRight') },
{ key: '3', icon: 'close', text: this.$t('closeOthers') },
{ key: '4', icon: 'sync', text: this.$t('refresh') },
]
},
tabsOffset() {
return this.multiPage ? 24 : 0
}
},
created () {
this.loadCacheConfig(this.$router?.options?.routes)
this.loadCachedTabs()
const route = this.$route
if (this.pageList.findIndex(item => item.path === route.path) === -1) {
this.pageList.push(this.createPage(route))
}
this.activePage = route.path
if (this.multiPage) {
this.$nextTick(() => {
this.setCachedKey(route)
})
this.addListener()
}
},
mounted () {
this.correctPageMinHeight(-this.tabsOffset)
},
beforeDestroy() {
this.removeListener()
this.correctPageMinHeight(this.tabsOffset)
},
watch: {
'$router.options.routes': function (val) {
this.excludeKeys = []
this.loadCacheConfig(val)
},
'$route': function (newRoute) {
this.activePage = newRoute.path
const page = this.pageList.find(item => item.path === newRoute.path)
if (!this.multiPage) {
this.pageList = [this.createPage(newRoute)]
} else if (page) {
page.fullPath = newRoute.fullPath
} else if (!page) {
this.pageList.push(this.createPage(newRoute))
}
if (this.multiPage) {
this.$nextTick(() => {
this.setCachedKey(newRoute)
})
}
},
'multiPage': function (newVal) {
if (!newVal) {
this.pageList = [this.createPage(this.$route)]
this.removeListener()
} else {
this.addListener()
}
},
tabsOffset(newVal, oldVal) {
this.correctPageMinHeight(oldVal - newVal)
}
},
methods: {
changePage (key) {
this.activePage = key
const page = this.pageList.find(item => item.path === key)
this.$router.push(page.fullPath)
},
remove (key, next) {
if (this.pageList.length === 1) {
return this.$message.warning(this.$t('warn'))
}
//
let index = this.pageList.findIndex(item => item.path === key)
this.clearCaches = this.pageList.splice(index, 1).map(page => page.cachedKey)
if (next) {
this.$router.push(next)
} else if (key === this.activePage) {
index = index >= this.pageList.length ? this.pageList.length - 1 : index
this.activePage = this.pageList[index].path
this.$router.push(this.activePage)
}
},
refresh (key, page) {
page = page || this.pageList.find(item => item.path === key)
page.loading = true
this.clearCache(page)
if (key === this.activePage) {
this.reloadContent(() => page.loading = false)
} else {
// loading
setTimeout(() => page.loading = false, 500)
}
},
onContextmenu(pageKey, e) {
if (pageKey) {
e.preventDefault()
e.meta = pageKey
this.menuVisible = true
}
},
onMenuSelect (key, target, pageKey) {
switch (key) {
case '1': this.closeLeft(pageKey); break
case '2': this.closeRight(pageKey); break
case '3': this.closeOthers(pageKey); break
case '4': this.refresh(pageKey); break
default: break
}
},
closeOthers (pageKey) {
//
const clearPages = this.pageList.filter(item => item.path !== pageKey && !item.unclose)
this.clearCaches = clearPages.map(item => item.cachedKey)
this.pageList = this.pageList.filter(item => !clearPages.includes(item))
//
if (this.activePage != pageKey) {
this.activePage = pageKey
this.$router.push(this.activePage)
}
},
closeLeft (pageKey) {
const index = this.pageList.findIndex(item => item.path === pageKey)
//
const clearPages = this.pageList.filter((item, i) => i < index && !item.unclose)
this.clearCaches = clearPages.map(item => item.cachedKey)
this.pageList = this.pageList.filter(item => !clearPages.includes(item))
//
if (!this.pageList.find(item => item.path === this.activePage)) {
this.activePage = pageKey
this.$router.push(this.activePage)
}
},
closeRight (pageKey) {
//
const index = this.pageList.findIndex(item => item.path === pageKey)
const clearPages = this.pageList.filter((item, i) => i > index && !item.unclose)
this.clearCaches = clearPages.map(item => item.cachedKey)
this.pageList = this.pageList.filter(item => !clearPages.includes(item))
//
if (!this.pageList.find(item => item.path === this.activePage)) {
this.activePage = pageKey
this.$router.push(this.activePage)
}
},
clearCache(page) {
page._init_ = false
this.clearCaches = [page.cachedKey]
},
reloadContent(onLoaded) {
this.refreshing = true
setTimeout(() => {
this.refreshing = false
this.$nextTick(() => {
this.setCachedKey(this.$route)
if (typeof onLoaded === 'function') {
onLoaded.apply(this, [])
}
})
}, 200)
},
pageName(page) {
return this.$t(getI18nKey(page.keyPath))
},
/**
* 添加监听器
*/
addListener() {
window.addEventListener('page:close', this.closePageListener)
window.addEventListener('page:refresh', this.refreshPageListener)
window.addEventListener('unload', this.unloadListener)
},
/**
* 移出监听器
*/
removeListener() {
window.removeEventListener('page:close', this.closePageListener)
window.removeEventListener('page:refresh', this.refreshPageListener)
window.removeEventListener('unload', this.unloadListener)
},
/**
* 页签关闭事件监听
* @param event 页签关闭事件
*/
closePageListener(event) {
const {closeRoute, nextRoute} = event.detail
const closePath = typeof closeRoute === 'string' ? closeRoute : closeRoute.path
const path = closePath && closePath.split('?')[0]
this.remove(path, nextRoute)
},
/**
* 页面刷新事件监听
* @param event 页签关闭事件
*/
refreshPageListener(event) {
const {pageKey} = event.detail
const path = pageKey && pageKey.split('?')[0]
this.refresh(path)
},
/**
* 页面 unload 事件监听器添加页签到 session 缓存用于刷新时保留页签
*/
unloadListener() {
const tabs = this.pageList.map(item => ({...item, _init_: false}))
sessionStorage.setItem(process.env.VUE_APP_TBAS_KEY, JSON.stringify(tabs))
},
createPage(route) {
return {
keyPath: route.matched[route.matched.length - 1].path,
fullPath: route.fullPath, loading: false,
path: route.path,
title: route.meta && route.meta.page && route.meta.page.title,
unclose: route.meta && route.meta.page && (route.meta.page.closable === false),
}
},
/**
* 设置页面缓存的key
* @param route 页面对应的路由
*/
setCachedKey(route) {
const page = this.pageList.find(item => item.path === route.path)
page.unclose = route.meta && route.meta.page && (route.meta.page.closable === false)
if (!page._init_) {
const vnode = this.$refs.tabContent.$vnode
page.cachedKey = vnode.key + vnode.componentOptions.Ctor.cid
page._init_ = true
}
},
/**
* 加载缓存的 tabs
*/
loadCachedTabs() {
const cachedTabsStr = sessionStorage.getItem(process.env.VUE_APP_TBAS_KEY)
if (cachedTabsStr) {
try {
const cachedTabs = JSON.parse(cachedTabsStr)
if (cachedTabs.length > 0) {
this.pageList = cachedTabs
}
} catch (e) {
console.warn('failed to load cached tabs, got exception:', e)
} finally {
sessionStorage.removeItem(process.env.VUE_APP_TBAS_KEY)
}
}
},
loadCacheConfig(routes, pCache = true) {
routes.forEach(item => {
const cacheAble = item.meta?.page?.cacheAble ?? pCache ?? true
if (!cacheAble) {
this.excludeKeys.push(new RegExp(`${item.path}\\d+$`))
}
if (item.children) {
this.loadCacheConfig(item.children, cacheAble)
}
})
},
...mapMutations('setting', ['correctPageMinHeight'])
}
}
</script>
<style scoped lang="less">
.tabs-view{
margin: -16px auto 8px;
&.head.fixed{
max-width: 1400px;
}
}
.tabs-view-content{
position: relative;
&.head.fixed{
width: 1400px;
margin: 0 auto;
}
}
</style>

@ -0,0 +1,25 @@
module.exports = {
messages: {
TW: {
closeLeft: '關閉左側',
closeRight: '關閉右側',
closeOthers: '關閉其它',
refresh: '刷新頁面',
warn: '這是最後一頁,不能再關閉了',
},
CN: {
closeLeft: '关闭左侧',
closeRight: '关闭右侧',
closeOthers: '关闭其它',
refresh: '刷新页面',
warn: '这是最后一页,不能再关闭了',
},
US: {
closeLeft: 'close left',
closeRight: 'close right',
closeOthers: 'close others',
refresh: 'refresh the page',
warn: 'This is the last page, you can\'t close it',
},
}
}

@ -0,0 +1,2 @@
import TabsView from './TabsView'
export default TabsView

@ -0,0 +1,37 @@
import Vue from 'vue'
import App from './App.vue'
import {initRouter} from './router'
import './theme/index.less'
import Antd from 'ant-design-vue'
import Viser from 'viser-vue'
// import '@/mock'
import store from './store'
import 'animate.css/source/animate.css'
import Plugins from '@/plugins'
import {initI18n} from '@/utils/i18n'
import bootstrap from '@/bootstrap'
import 'moment/locale/zh-tw'
import CKEditor from 'ckeditor4-vue';
import VueClipboard from 'vue-clipboard2'
const router = initRouter(store.state.setting.asyncRoutes)
const i18n = initI18n('TW', 'CN', 'US')
import * as ElResize from 'vue-element-resize-event'
Vue.use(ElResize)
Vue.use(Antd)
Vue.config.productionTip = false
Vue.use(Viser)
Vue.use(Plugins)
Vue.use(CKEditor)
Vue.use(VueClipboard)
bootstrap({router, store, i18n, message: Vue.prototype.$message})
new Vue({
router,
store,
i18n,
render: h => h(App),
}).$mount('#app')

@ -0,0 +1,32 @@
const avatars = [
'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png',
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png',
'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png',
'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png',
'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png'
]
const positions = [
{
CN: 'Java工程师 | 蚂蚁金服-计算服务事业群-微信平台部',
TW: 'Java工程師 | 螞蟻金服-計算服務事業群-微信平台部',
US: 'Java engineer | Ant financial - Computing services business group - WeChat platform division'
},{
CN: '前端工程师 | 蚂蚁金服-计算服务事业群-VUE平台',
TW: '前端工程師 | 螞蟻金服-計算服務事業群-VUE平台',
US: 'Front-end engineer | Ant Financial - Computing services business group - VUE platform'
},{
CN: '前端工程师 | 蚂蚁金服-计算服务事业群-REACT平台',
TW: '前端工程師 | 螞蟻金服-計算服務事業群-REACT平台',
US: 'Front-end engineer | Ant Financial - Computing services business group - REACT platform'
},{
CN: '产品分析师 | 蚂蚁金服-计算服务事业群-IOS平台部',
TW: '產品分析師 | 螞蟻金服-計算服務事業群-IOS平台部',
US: 'Product analyst | Ant Financial - Computing services business group - IOS platform division'
}
]
const admins = ['ICZER', 'JACK', 'LUIS', 'DAVID']
export {positions, avatars, admins}

@ -0,0 +1,46 @@
import Mock from 'mockjs'
import {positions, avatars, admins} from '../common'
const Random = Mock.Random
const timeList = [
{
CN: '早上好',
TW: '早晨啊',
US: 'Good morning',
},{
CN: '上午好',
TW: '上午好',
US: 'Good morning',
},{
CN: '中午好',
TW: '中午好',
US: 'Good afternoon',
},{
CN: '下午好',
TW: '下午好',
US: 'Good afternoon',
},{
CN: '晚上好',
TW: '晚上好',
US: 'Good evening',
}
]
Random.extend({
admin () {
return this.pick(admins)
},
timeFix () {
const time = new Date()
const hour = time.getHours()
return hour < 9
? timeList[0] : (hour <= 11 ? timeList[1] : (hour <= 13 ? timeList[2] : (hour <= 20 ? timeList[3] : timeList[4])))
},
avatar () {
return this.pick(avatars)
},
position () {
return this.pick(positions)
}
})

@ -0,0 +1,8 @@
import Mock from 'mockjs'
import '@/mock/user/login'
import '@/mock/user/routes'
// 设置全局延时
Mock.setup({
timeout: '300-600'
})

@ -0,0 +1,39 @@
import Mock from 'mockjs'
import '@/mock/extend'
const user = Mock.mock({
name: '@ADMIN',
avatar: '@AVATAR',
address: '@CITY',
position: '@POSITION'
})
Mock.mock(`${process.env.VUE_APP_API_BASE_URL}/auth/login`, 'post', ({body}) => {
let result = {data: {}}
const {name, password} = JSON.parse(body)
let success = false
if (name === 'admin' && password === '888888') {
success = true
result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit']}]
result.data.roles = [{id: 'admin', operation: ['add', 'edit', 'delete']}]
} else if (name === 'test' || password === '888888') {
success = true
result.data.permissions = [{id: 'queryForm', operation: ['add', 'edit']}]
result.data.roles = [{id: 'test', operation: ['add', 'edit', 'delete']}]
} else {
success = false
}
if (success) {
result.code = 200
result.message = Mock.mock('@TIMEFIX').TW + ',歡迎回來'
result.data.user = user
result.data.token = 'Authorization:' + Math.random()
result.data.expireAt = new Date(new Date().getTime() + 30 * 60 * 1000)
} else {
result.code = -1
result.message = '賬戶名或密碼錯誤admin/888888 or test/888888'
}
return result
})

@ -0,0 +1,45 @@
import Mock from 'mockjs'
Mock.mock(`${process.env.VUE_APP_API_BASE_URL}/auth/getRoute`, 'get', () => {
console.log('mock routes')
let result = {}
result.code = 0
result.data = [{
router: 'root',
children: ['demo',
{
router: 'parent1',
children: [{
router: 'demo',
name: 'demo1',
authority: {
permission: 'demo',
role: 'admin'
}
}],
},
{
router: 'parent2',
children: [{
router: 'demo',
name: 'demo2'
}],
},
{
router: 'exception',
children: ['exp404', 'exp403', 'exp500'],
},
{
router: 'demo',
icon: 'file-ppt',
path: 'auth/demo',
name: '验权页面',
authority: {
permission: 'form',
role: 'manager'
}
}
]
}]
return result
})

@ -0,0 +1,163 @@
<template>
<a-card>
<div>
<a-space class="operator">
</a-space>
<standard-table
:columns="columns"
:dataSource="dataSource"
:pagination="pagination"
@clear="onClear"
@change="onChange"
@selectedRowChange="onSelectChange"
@showSizeChange="onShowSizeChange"
:scroll="{x: 600}"
>
<div slot="description" slot-scope="{text}">
{{text}}
</div>
<div slot="action" slot-scope="{text, record}">
<a style="margin-right: 8px">
<a-icon type="edit"/>編輯
</a>
<a @click="deleteRecord(record.key)">
<a-icon type="delete" />刪除
</a>
&nbsp;
<router-link :to="`/list/query/detail/${record.key}`" >
<a-icon type="delete" />詳情
</router-link>
</div>
<template slot="statusTitle">
<a-icon @click.native="onStatusTitleClick" type="info-circle" />
</template>
</standard-table>
</div>
</a-card>
</template>
<script>
import StandardTable from '@/components/table/StandardTable'
import { getAdminLogs } from '@/services/admin'
const columns = [
{
title: '編號',
width: 50,
// dataIndex: 'id'
customRender: (text, record, index) => `${index + 1}`
},
{
title: '操作者',
dataIndex: 'admin_name'
},
{
title: '操作日期',
dataIndex: 'time'
},
{
title: 'IP位址',
dataIndex: 'ip',
},
{
title: '操作紀錄',
dataIndex: 'content',
}
]
export default {
name: 'AdminLog',
components: {StandardTable},
data () {
return {
advanced: true,
columns: columns,
pagination: {
size: 'small',
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: total => `${total} 筆資料`
},
dataSource: []
}
},
created(){
this.genTable()
},
methods: {
async genTable(){
try{
const {data : res} = await getAdminLogs({
current: this.pagination.current,
size: this.pagination.pageSize,
search: this.search
})
this.pagination.total = res.total
this.dataSource = res.data
}catch (e){
this.dataSource = []
}
},
deleteRecord(key) {
this.dataSource = this.dataSource.filter(item => item.key !== key)
},
toggleAdvanced () {
this.advanced = !this.advanced
},
remove () {
},
onClear() {
this.$message.info('您清空了勾選的所有行')
},
onStatusTitleClick() {
this.$message.info('你點選了狀態列表頭')
},
onChange() {
this.$message.info('表格狀態改變了')
},
onSelectChange() {
this.$message.info('選中行改變了')
},
addNew () {
this.dataSource.unshift({
key: this.dataSource.length,
no: 'NO ' + this.dataSource.length,
description: '這是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 4,
updatedAt: '2018-07-26'
})
},
handleMenuClick (e) {
if (e.key === 'delete') {
this.remove()
}
}
}
}
</script>
<style lang="less" scoped>
.search{
margin-bottom: 54px;
}
.fold{
width: calc(100% - 216px);
display: inline-block
}
.operator{
// margin-bottom: 18px;
}
@media screen and (max-width: 900px) {
.fold {
width: 100%;
}
}
</style>

@ -0,0 +1,196 @@
<template>
<div>
<a-card>
<div>
<a-space class="operator">
<a-button @click="handleAddDraw" type="primary" size="small">新增</a-button>
</a-space>
<standard-table
:columns="columns"
:dataSource="dataSource"
:pagination="false"
@clear="onClear"
@change="onChange"
@selectedRowChange="onSelectChange"
:scroll="{x: 600}"
>
<div slot="action" slot-scope="{text, record}">
<a class="edit-btn" @click="handleEditDraw(record.id)" style="margin-right: 8px">
<a-icon type="edit"/>編輯
</a>
<a class="delete-btn" @click="deleteRole(record.id)">
<a-icon type="delete" />刪除
</a>
</div>
</standard-table>
</div>
</a-card>
<add-form
:visible="showAddDraw"
@close="onAddDrawClose"
@submit="onAddSubmit"
>
</add-form>
<edit-form
:editid="editId"
:visible="showEditDraw"
@close="onEditDrawClose"
@submit="onEditSubmit"
>
</edit-form>
</div>
</template>
<script>
import StandardTable from '@/components/table/StandardTable'
import AddForm from './components/AddForm.vue'
import EditForm from './components/EditForm.vue'
import {getRoles, deleteRole} from '@/services/role'
const columns = [
{
title: '#',
width: 50,
customRender: (text, record, index) => `${index + 1}`
},
{
title: '角色名稱',
width: 200,
dataIndex: 'name',
},
{
title: '角色描述',
dataIndex: 'desc',
ellipsis: true,
},
{
title: '操作',
width: 180,
scopedSlots: { customRender: 'action' }
}
]
export default {
name: 'RoleList',
i18n: require('./i18n'),
components: {StandardTable, AddForm, EditForm},
data () {
return {
advanced: true,
editId: 0,
columns: columns,
showAddDraw: false,
showEditDraw: false,
dataSource: [],
}
},
async mounted(){
this.genTable()
},
authorize: {
// deleteRecord: 'delete'
},
methods: {
async genTable(){
try{
const res = await getRoles()
this.dataSource = res.data.data
}catch (e){
this.dataSource = []
}
},
deleteRole(id) {
console.log(id)
this.$confirm({
title: '確認刪除?',
content: '確定刪除此筆資料',
okText: '確定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
let res = await deleteRole({id:id})
if(res.code === 200){
this.genTable()
this.$message.success('刪除成功')
}
},
onCancel() {
return false
},
});
},
toggleAdvanced () {
this.advanced = !this.advanced
},
remove () {
},
onClear() {
this.$message.info('您清空了勾選的所有行')
},
onStatusTitleClick() {
this.$message.info('你點選了狀態列表頭')
},
onChange() {
this.$message.info('表格狀態改變了')
},
onSelectChange() {
this.$message.info('選中行改變了')
},
//
handleAddDraw () {
this.showAddDraw = true
},
onAddDrawClose() {
this.showAddDraw = false
},
onAddSubmit () {
this.genTable()
this.showAddDraw = false
this.$message.success('新增成功')
},
//
handleEditDraw(id) {
this.editId = id
this.showEditDraw = true
},
onEditDrawClose() {
this.editId=0
this.showEditDraw = false
},
onEditSubmit () {
this.editId=0
this.genTable()
this.showEditDraw = false
this.$message.success('編輯成功')
},
handleMenuClick (e) {
if (e.key === 'delete') {
this.remove()
}
}
}
}
</script>
<style lang="less" scoped>
.search{
margin-bottom: 54px;
}
.fold{
width: calc(100% - 216px);
display: inline-block
}
.operator{
// margin-bottom: 18px;
}
@media screen and (max-width: 900px) {
.fold {
width: 100%;
}
}
</style>

@ -0,0 +1,144 @@
<template>
<div>
<a-drawer
title="新增角色"
:destroyOnClose="true"
:visible="visible"
:body-style="{ paddingBottom: '80px' }"
@close="onClose"
>
<a-form-model
ref="ruleForm"
:model="form"
:rules="rules"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-model-item label="角色名稱" ref="name" prop="name">
<a-input
v-model="form.name"
@blur="
() => {
$refs.name.onFieldBlur();
}
"
/>
</a-form-model-item>
<a-form-model-item label="角色描述">
<a-input v-model="form.desc" type="textarea" />
</a-form-model-item>
<!-- <a-form-model-item label="角色權限">
<a-tree
v-model="form.permission"
checkable
:tree-data="treeData"
defaultExpandAll
@select="onSelect"
@check="onCheck"
>
<span slot="title0010" style="color: #1890ff">sss</span>
</a-tree>
</a-form-model-item> -->
</a-form-model>
<div
:style="{
position: 'absolute',
right: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
zIndex: 1,
}"
>
<a-button :style="{ marginRight: '8px' }" @click="onClose">
關閉
</a-button>
<a-button type="primary" @click="onSubmit">
送出
</a-button>
</div>
</a-drawer>
</div>
</template>
<script>
import {addRole} from '@/services/role'
// import {getTree} from '@/services/right'
export default {
name: 'AddForm',
data(){
return {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
treeData: [],
form: {
name:'',
desc:'',
permission: [],
status: true,
},
rules: {
name: [
{ required: true, message: '請輸入角色名稱', trigger: 'blur' },
],
}
}
},
props: {
visible: Boolean
},
async created(){
// let res = await getTree()
// console.log(res.data)
// this.treeData = res.data
},
methods:{
onClose(){
this.$refs.ruleForm.resetFields();
this.$emit('close',true)
},
onSubmit(){
console.log(this.form)
this.$refs.ruleForm.validate(async valid => {
if (valid) {
let res = await addRole(this.form)
if(res.code===200){
this.$refs.ruleForm.resetFields();
this.$emit('submit',true)
}else{
this.$message.error('新增失敗')
}
} else {
return false;
}
});
},
onSelect(selectedKeys, info) {
console.log('selected', selectedKeys, info);
},
onCheck(checkedKeys, info) {
console.log('onCheck', checkedKeys, info);
},
}
}
</script>
<style lang="less" scoped>
.ant-drawer-header{
background-color: #87e8de !important;
.ant-drawer-title{
color: #FFF !important;
}
}
.ant-drawer-content-wrapper{
width: 50% !important;
}
</style>

@ -0,0 +1,158 @@
<template>
<div>
<a-drawer
title="編輯角色"
:destroyOnClose="true"
:visible="visible"
:body-style="{ paddingBottom: '80px' }"
@close="onClose"
>
<a-spin :spinning="spinning">
<div class="spin-content">
<a-form-model
ref="ruleForm"
:model="form"
:rules="rules"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-model-item label="角色名稱" ref="name" prop="name">
<a-input
v-model="form.name"
@blur="
() => {
$refs.name.onFieldBlur();
}
"
/>
</a-form-model-item>
<a-form-model-item label="角色描述">
<a-input v-model="form.desc" type="textarea" />
</a-form-model-item>
<!-- <a-form-model-item label="角色權限">
<a-tree
v-model="form.permission"
checkable
:tree-data="treeData"
defaultExpandAll
@select="onSelect"
@check="onCheck"
>
<span slot="title0010" style="color: #1890ff">sss</span>
</a-tree>
</a-form-model-item> -->
</a-form-model>
</div>
</a-spin>
<div
:style="{
position: 'absolute',
right: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
zIndex: 1,
}"
>
<a-button :style="{ marginRight: '8px' }" @click="onClose">
關閉
</a-button>
<a-button type="primary" @click="onSubmit">
送出
</a-button>
</div>
</a-drawer>
</div>
</template>
<script>
import {getRoleById, updateRole} from '@/services/role'
// import {getTree} from '@/services/right'
export default {
name: 'EditForm',
data(){
return {
spinning: false,
labelCol: { span: 8 },
wrapperCol: { span: 16 },
treeData: [],
form: {},
rules: {
name: [
{ required: true, message: '請輸入角色名稱', trigger: 'blur' },
],
}
}
},
props: {
visible: Boolean,
editid: Number,
},
async created(){
// let res = await getTree()
// console.log(res.data)
// this.treeData = res.data
},
watch: {
editid: async function (val) {
if(val){
this.spinning=true
let res=await getRoleById({id: val})
this.form = res.data
this.spinning=false
}
}
},
methods:{
onClose(){
this.form={}
this.$refs.ruleForm.resetFields();
this.$emit('close',true)
},
onSubmit(){
console.log(this.form)
this.$refs.ruleForm.validate(async valid => {
if (valid || 1===2) {
let res = await updateRole(this.form)
if(res.code===200){
this.$refs.ruleForm.resetFields();
this.$emit('submit',true)
}else{
this.$message.error('編輯失敗')
}
} else {
return false;
}
});
},
onSelect(selectedKeys, info) {
console.log('selected', selectedKeys, info);
},
onCheck(checkedKeys, info) {
// console.log(checkedKeys.concat(info.halfCheckedKeys))
// this.form.permission=checkedKeys.concat(info.halfCheckedKeys)
console.log('onCheck', checkedKeys, info);
},
}
}
</script>
<style lang="less" scoped>
.ant-drawer-header{
background-color: #87e8de !important;
.ant-drawer-title{
color: #FFF !important;
}
}
.ant-drawer-content-wrapper{
width: 50% !important;
}
</style>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save