Commit 77bf29e3 authored by 赵啸非's avatar 赵啸非

添加基础前端工程

parent c7d86a77
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
VUE_APP_API_BASE_URL=http://api.iczer.com
\ No newline at end of file
VUE_APP_API_BASE_URL=http://dev.iczer.com
.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
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.
[简体中文](./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>
- Preview:https://iczer.gitee.io/vue-antd-admin
- Documentation:https://iczer.gitee.io/vue-antd-admin-docs
- FAQ:https://iczer.gitee.io/vue-antd-admin-docs/start/faq.html
- Mirror Repo in China:https://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)
简体中文 | [English](./README.en-US.md)
<h1 align="center">Vue Antd Admin</h1>
<div align="center">
[Ant Design Pro](https://github.com/ant-design/ant-design-pro) 的 Vue 实现版本
开箱即用的中后台前端/设计解决方案
[![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)
多种主题模式可选:
![image](./src/assets/img/preview-nine.png)
</div>
- 预览地址:https://iczer.gitee.io/vue-antd-admin
- 使用文档:https://iczer.gitee.io/vue-antd-admin-docs
- 常见问题:https://iczer.gitee.io/vue-antd-admin-docs/start/faq.html
- 国内镜像:https://gitee.com/iczer/vue-antd-admin
## 浏览器支持
现代浏览器及 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 |
## 使用
### 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
```
更多信息参考 [使用文档](https://iczer.gitee.io/vue-antd-admin-docs)
## 参与贡献
我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :star2::
- 在你的公司或个人项目中使用 Vue Antd Admin。
- 通过 [Issue](https://github.com/iczer/vue-antd-admin/issues) 报告:bug:或进行咨询。
- 提交 [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) 改进 Admin 的代码。
- 加入社群,与小伙伴们一同交流心得。QQ群:942083829、 812277510(已满)、610090280(已满)
## 打赏
如果该项目对您有所帮助,可以请作者喝一杯咖啡。
<p>
<img src="./src/assets/img/alipay.png" width="320px" style="display: inline-block;" />
<img src="./src/assets/img/wechatpay.png" width="320px" style="display: inline-block; margin-left: 24px;" />
</p>
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
}
{
"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",
"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",
"clipboard": "^2.0.6",
"core-js": "^3.6.5",
"date-fns": "^2.14.0",
"enquire.js": "^2.1.6",
"highlight.js": "^10.2.1",
"js-cookie": "^2.2.1",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"viser-vue": "^2.4.8",
"vue": "^2.6.11",
"vue-i18n": "^8.18.2",
"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"
]
}
<!DOCTYPE html>
<html lang="en" 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">
<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>
<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 'CN':
this.locale = require('ant-design-vue/es/locale-provider/zh_CN').default
break
case 'HK':
this.locale = require('ant-design-vue/es/locale-provider/zh_TW').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" scoped>
#id{
}
</style>
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
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])
}
}
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
.mini-chart{
position: relative;
width: 100%;
.chart-content{
position: absolute;
bottom: -28px;
width: 100%;
}
}
<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>
<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>
import ColorCheckbox from '@/components/checkbox/ColorCheckbox'
import ImgCheckbox from '@/components/checkbox/ImgCheckbox'
export {
ColorCheckbox,
ImgCheckbox
}
<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>
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
<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>
<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>
<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>
<template>
<a-layout-sider :theme="sideTheme" :class="['side-menu', 'beauty-scroll', isMobile ? null : 'shadow']" width="256px" :collapsible="collapsible" v-model="collapsed" :trigger="null">
<div :class="['logo', theme]">
<router-link to="/dashboard/workplace">
<img src="@/assets/img/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'
return this.theme
},
...mapState('setting', ['isMobile', 'systemName'])
},
methods: {
onSelect (obj) {
this.$emit('menuSelect', obj)
}
}
}
</script>
<style lang="less" scoped>
@import "index";
</style>
.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: 18px;
margin: 0 0 0 12px;
display: inline-block;
vertical-align: middle;
line-height: 1.2;
width: 150px;
text-align: center;
}
img{
width: 32px;
vertical-align: middle;
}
}
/deep/.ant-menu-item-selected{
border-top-left-radius: 20px !important;
border-bottom-left-radius: 20px !important;
}
}
.menu{
padding: 16px 0;
// padding-left: 10px;
}
.ant-layout-sider-light{
box-shadow: none;
/deep/.ant-menu-item-selected{
border-radius: 8px !important;
background-color:@primary-color !important;
background: linear-gradient(180deg, #32AFF6 0%, #3B88FF 100%) !important;
}
/deep/.ant-menu-item-selected>a, .ant-menu-item-selected>a:hover{
color: #fff !important;
}
/deep/.ant-menu-item,/deep/.ant-menu-submenu-title{
font-size: 20px;
height: 44px;
line-height: 44px;
margin: 10px 0;
.anticon{
font-size: 20px !important;
}
}
.menu{
padding: 16px;
}
.logo{
background-color:@primary-color !important;
background: linear-gradient(180deg, #32AFF6 0%, #3B88FF 100%) !important;
h1{
color: #fff !important;
}
}
}
\ No newline at end of file
/**
* 该插件可根据菜单配置自动生成 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: '监控中心'}}
* HK: {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)
)
}
}
<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 class="detail">
<div class="main">
<div class="row">
<h1 v-if="showPageTitle && title" class="title">{{title}}</h1>
<div class="action"><slot name="action"></slot></div>
</div>
<div class="row">
<div v-if="this.$slots.content" class="content">
<div v-if="avatar" class="avatar"><a-avatar :src="avatar" :size="72" /></div>
<slot name="content"></slot>
</div>
<div v-if="this.$slots.extra" class="extra"><slot name="extra"></slot></div>
</div>
</div>
</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>
.page-header{
background: @base-bg-color;
padding: 16px 24px;
&.head.fixed{
margin: auto;
max-width: 1400px;
}
.page-header-wide{
.breadcrumb{
margin-bottom: 20px;
}
.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: 16px;
}
.content{
display: flex;
flex-wrap: wrap;
color: @text-color-second;
}
.extra{
display: flex;
}
}
}
}
}
<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>
<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>
<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>
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: '重置配置',
},
HK: {
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',
}
}
}
<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"
>
<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'
},
pagination: {
type: [Object, Boolean],
default: true
},
selectedRows: Array,
expandedRowKeys: Array,
expandedRowRender: Function
},
data () {
return {
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})
}
},
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;
}
}
}
</style>
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
<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>
import AdvanceTable from './AdvanceTable'
export default AdvanceTable
\ No newline at end of file
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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
// 此处延迟执行,是为解决mouted未完全完成情况下引发的trigger显示bug
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>
<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>
<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>
// 自定义配置,参考 ./default/setting.config.js,需要自定义的属性在这里配置即可
module.exports = {
theme: {
color: '#13c2c2',
mode: 'dark',
},
multiPage: true,
animate: {
name: 'lightSpeed',
direction: 'left'
}
}
// 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
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
// 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',
'menu-size': '14px'
},
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',
'menu-size': '20px'
},
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
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}
// 此配置为系统默认设置,需修改的设置项,在src/config/config.js中添加修改项即可。也可直接在此文件中修改。
module.exports = {
lang: 'CN', //语言,可选 CN(简体)、HK(繁体)、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: false, //隐藏设置抽屉,true:隐藏,false:不隐藏
systemName: '智慧政务一体化综 合 管 理 平 台', //系统名称
copyright: '2021 ICZER 信宏翔科技有限公司', //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'}
],
}
const deepMerge = require('deepmerge')
const _config = require('./config')
const {setting} = require('./default')
const config = deepMerge(setting, _config)
module.exports = config
/**
* 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}
/**
* 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文本) 和 cssObj(css对象)参数; 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) {
console.log(cssObj)
cssObj.selector = '.ant-layout-sider-dark'
return cssObj.toText()
}
},
'/keyframes/': false
}
module.exports = cssResolve
<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;height:100%">
<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 - 41,
collapsed: false,
showSetting: false,
drawerOpen: false
}
},
provide() {
return {
adminLayout: this
}
},
watch: {
$route(val) {
this.setActivated(val)
},
layout() {
this.setActivated(this.$route)
},
isMobile(val) {
console.log(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' : '256px'
},
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)
let _this = this
window.onresize = function(){
_this.minHeight = window.innerHeight - 64 - 41
}
},
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 24px 0;
// overflow-x: hidden;
min-height: calc(100vh - 64px - 41px);
}
.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>
<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>
<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>
<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: 24px 0 0;
&.side{
}
&.head.fixed{
margin: 0 auto;
max-width: 1400px;
}
}
</style>
<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>
<template>
<div class="footer">
<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: 10px 16px 10px;
/*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>
<template>
<a-layout-header :class="[headerTheme, headerThememode, '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/img/logo.png" />
<h1 v-if="!isMobile">{{systemName}}</h1>
</router-link>
<a-divider v-if="isMobile" type="vertical" />
<a-icon v-if="layout !== 'head'" 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]">
<a-tooltip class="header-item" title="返回门户" placement="bottom" >
<a href="" target="_blank">
<a-icon type="home" /> 返回门户
</a>
</a-tooltip>
<a-tooltip class="header-item" title="平台设置" placement="bottom" >
<a href="" target="_blank">
<a-icon type="setting" /> 平台设置
</a>
</a-tooltip>
<header-notice class="header-item"/>
<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 HeaderSearch from './HeaderSearch'
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, HeaderNotice},
props: ['collapsed', 'menuData'],
data() {
return {
langList: [
{key: 'CN', name: '简体中文', alias: '简体'},
{key: 'HK', name: '繁體中文', alias: '繁體'},
{key: 'US', name: 'English', alias: 'English'}
],
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
},
headerThememode () {
return `header-${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>
<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/user'
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>
<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>
<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>
.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 24px;
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;
}
}
})
}
}
}
.header-light{
background-color:@primary-color !important;
background: linear-gradient(180deg, #32AFF6 0%, #3B88FF 100%) !important;
color: #fff;
}
\ No newline at end of file
<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: {
CN: {
lock: '点击锁定页签头',
unlock: '点击解除锁定',
},
HK: {
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: 256px;
&.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>
<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;
// overflow-y: auto;
&.head.fixed{
width: 1400px;
margin: 0 auto;
}
}
</style>
module.exports = {
messages: {
CN: {
closeLeft: '关闭左侧',
closeRight: '关闭右侧',
closeOthers: '关闭其它',
refresh: '刷新页面',
warn: '这是最后一页,不能再关闭了',
},
HK: {
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',
},
}
}
\ No newline at end of file
import TabsView from './TabsView'
export default TabsView
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-cn'
const router = initRouter(store.state.setting.asyncRoutes)
const i18n = initI18n('CN', 'US')
Vue.use(Antd)
Vue.config.productionTip = false
Vue.use(Viser)
Vue.use(Plugins)
bootstrap({router, store, i18n, message: Vue.prototype.$message})
new Vue({
router,
store,
i18n,
render: h => h(App),
}).$mount('#app')
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工程师 | 蚂蚁金服-计算服务事业群-微信平台部',
HK: 'Java工程師 | 螞蟻金服-計算服務事業群-微信平台部',
US: 'Java engineer | Ant financial - Computing services business group - WeChat platform division'
},{
CN: '前端工程师 | 蚂蚁金服-计算服务事业群-VUE平台',
HK: '前端工程師 | 螞蟻金服-計算服務事業群-VUE平台',
US: 'Front-end engineer | Ant Financial - Computing services business group - VUE platform'
},{
CN: '前端工程师 | 蚂蚁金服-计算服务事业群-REACT平台',
HK: '前端工程師 | 螞蟻金服-計算服務事業群-REACT平台',
US: 'Front-end engineer | Ant Financial - Computing services business group - REACT platform'
},{
CN: '产品分析师 | 蚂蚁金服-计算服务事业群-IOS平台部',
HK: '產品分析師 | 螞蟻金服-計算服務事業群-IOS平台部',
US: 'Product analyst | Ant Financial - Computing services business group - IOS platform division'
}
]
const admins = ['ICZER', 'JACK', 'LUIS', 'DAVID']
export {positions, avatars, admins}
import Mock from 'mockjs'
import {positions, avatars, admins} from '../common'
const Random = Mock.Random
const timeList = [
{
CN: '早上好',
HK: '早晨啊',
US: 'Good morning',
},{
CN: '上午好',
HK: '上午好',
US: 'Good morning',
},{
CN: '中午好',
HK: '中午好',
US: 'Good afternoon',
},{
CN: '下午好',
HK: '下午好',
US: 'Good afternoon',
},{
CN: '晚上好',
HK: '晚上好',
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)
}
})
import Mock from 'mockjs'
import '@/mock/user/login'
import '@/mock/user/routes'
// 设置全局延时
Mock.setup({
timeout: '300-600'
})
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}/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 = 0
result.message = Mock.mock('@TIMEFIX').CN + ',欢迎回来'
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
})
import Mock from 'mockjs'
Mock.mock(`${process.env.VUE_APP_API_BASE_URL}/routes`, 'get', () => {
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
})
<template>
<div class="new-page" :style="`min-height: ${pageMinHeight}px`">
<h1>{{$t('content')}}</h1>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'Demo',
i18n: require('./i18n'),
data() {
return {
}
},
computed: {
...mapState('setting', ['pageMinHeight']),
desc() {
return this.$t('description')
}
}
}
</script>
<style scoped lang="less">
@import "index";
</style>
\ No newline at end of file
module.exports = {
messages: {
CN: {
content: '演示页面',
description: '这是一个演示页面'
},
HK: {
content: '演示頁面',
description: '這是一個演示頁面'
},
US: {
content: 'Demo Page',
description: 'This is a demo page'
}
}
}
\ No newline at end of file
import Demo from './Demo.vue'
export default Demo
\ No newline at end of file
.new-page{
height: 100%;
background-color: @base-bg-color;
text-align: center;
padding: 200px 0 0 0;
border-radius: 4px;
//margin-top: -24px;
h1{
font-size: 48px;
}
}
\ No newline at end of file
<template>
<exception-page home-route="/demo" :style="`min-height: ${minHeight}`" type="403" />
</template>
<script>
import ExceptionPage from '@/components/exception/ExceptionPage'
import {mapState} from 'vuex'
export default {
name: 'Exp403',
components: {ExceptionPage},
computed: {
...mapState('setting', ['pageMinHeight']),
minHeight() {
return this.pageMinHeight ? this.pageMinHeight + 'px' : '100vh'
}
}
}
</script>
<style scoped lang="less">
</style>
<template>
<exception-page home-route="/demo" :style="`min-height: ${minHeight}`" type="404" />
</template>
<script>
import ExceptionPage from '@/components/exception/ExceptionPage'
import {mapState} from 'vuex'
export default {
name: 'Exp404',
components: {ExceptionPage},
computed: {
...mapState('setting', ['pageMinHeight']),
minHeight() {
return this.pageMinHeight ? this.pageMinHeight + 'px' : '100vh'
}
}
}
</script>
<style scoped lang="less">
</style>
<template>
<exception-page home-route="/demo" :style="`min-height: ${minHeight}`" type="500" />
</template>
<script>
import ExceptionPage from '@/components/exception/ExceptionPage'
import {mapState} from 'vuex'
export default {
name: 'Exp500',
components: {ExceptionPage},
computed: {
...mapState('setting', ['pageMinHeight']),
minHeight() {
return this.pageMinHeight ? this.pageMinHeight + 'px' : '100vh'
}
}
}
</script>
<style scoped lang="less">
</style>
<template>
<common-layout>
<div class="top">
<div class="header">
<img alt="logo" class="logo" src="@/assets/img/logo.png" />
<span class="title">{{systemName}}</span>
</div>
<div class="desc">Ant Design 是西湖区最具影响力的 Web 设计规范</div>
</div>
<div class="login">
<a-form @submit="onSubmit" :form="form">
<a-tabs size="large" :tabBarStyle="{textAlign: 'center'}" style="padding: 0 2px;">
<a-tab-pane tab="账户密码登录" key="1">
<a-alert type="error" :closable="true" v-if="error" :message="error" @close='onClose' showIcon style="margin-bottom: 24px;" />
<a-form-item>
<a-input
autocomplete="autocomplete"
size="large"
placeholder="admin"
v-decorator="['name', {rules: [{ required: true, message: '请输入账户名', whitespace: true}]}]"
>
<a-icon slot="prefix" type="user" />
</a-input>
</a-form-item>
<a-form-item>
<a-input
size="large"
placeholder="888888"
autocomplete="autocomplete"
type="password"
v-decorator="['password', {rules: [{ required: true, message: '请输入密码', whitespace: true}]}]"
>
<a-icon slot="prefix" type="lock" />
</a-input>
</a-form-item>
</a-tab-pane>
<a-tab-pane tab="手机号登录" key="2">
<a-form-item>
<a-input size="large" placeholder="mobile number" >
<a-icon slot="prefix" type="mobile" />
</a-input>
</a-form-item>
<a-form-item>
<a-row :gutter="8" style="margin: 0 -4px">
<a-col :span="16">
<a-input size="large" placeholder="captcha">
<a-icon slot="prefix" type="mail" />
</a-input>
</a-col>
<a-col :span="8" style="padding-left: 4px">
<a-button style="width: 100%" class="captcha-button" size="large">获取验证码</a-button>
</a-col>
</a-row>
</a-form-item>
</a-tab-pane>
</a-tabs>
<div>
<a-checkbox :checked="true" >自动登录</a-checkbox>
<a style="float: right">忘记密码</a>
</div>
<a-form-item>
<a-button :loading="logging" style="width: 100%;margin-top: 24px" size="large" htmlType="submit" type="primary">登录</a-button>
</a-form-item>
<div>
其他登录方式
<a-icon class="icon" type="alipay-circle" />
<a-icon class="icon" type="taobao-circle" />
<a-icon class="icon" type="weibo-circle" />
<router-link style="float: right" to="/dashboard/workplace" >注册账户</router-link>
</div>
</a-form>
</div>
</common-layout>
</template>
<script>
import CommonLayout from '@/layouts/CommonLayout'
import {login, getRoutesConfig} from '@/services/user'
import {setAuthorization} from '@/utils/request'
import {loadRoutes} from '@/utils/routerUtil'
import {mapMutations} from 'vuex'
export default {
name: 'Login',
components: {CommonLayout},
data () {
return {
logging: false,
error: '',
form: this.$form.createForm(this)
}
},
computed: {
systemName () {
return this.$store.state.setting.systemName
}
},
methods: {
...mapMutations('account', ['setUser', 'setPermissions', 'setRoles']),
onSubmit (e) {
e.preventDefault()
this.form.validateFields((err) => {
if (!err) {
this.logging = true
const name = this.form.getFieldValue('name')
const password = this.form.getFieldValue('password')
login(name, password).then(this.afterLogin)
}
})
},
afterLogin(res) {
this.logging = false
const loginRes = res.data
if (loginRes.code >= 0) {
const {user, permissions, roles} = loginRes.data
this.setUser(user)
this.setPermissions(permissions)
this.setRoles(roles)
setAuthorization({token: loginRes.data.token, expireAt: new Date(loginRes.data.expireAt)})
// 获取路由配置
getRoutesConfig().then(result => {
const routesConfig = result.data.data
loadRoutes(routesConfig)
this.$router.push('/demo')
this.$message.success(loginRes.message, 3)
})
} else {
this.error = loginRes.message
}
},
onClose() {
this.error = false
}
}
}
</script>
<style lang="less" scoped>
.common-layout{
.top {
text-align: center;
.header {
height: 44px;
line-height: 44px;
a {
text-decoration: none;
}
.logo {
height: 44px;
vertical-align: top;
margin-right: 16px;
}
.title {
font-size: 33px;
color: @title-color;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-weight: 600;
position: relative;
top: 2px;
}
}
.desc {
font-size: 14px;
color: @text-color-second;
margin-top: 12px;
margin-bottom: 40px;
}
}
.login{
width: 368px;
margin: 0 auto;
@media screen and (max-width: 576px) {
width: 95%;
}
@media screen and (max-width: 320px) {
.captcha-button{
font-size: 14px;
}
}
.icon {
font-size: 24px;
color: @text-color-second;
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
}
}
</style>
import Login from './Login'
export default Login
/**
* 获取路由需要的权限
* @param permissions
* @param route
* @returns {Permission}
*/
const getRoutePermission = (permissions, route) => permissions.find(item => item.id === route.meta.authority.permission)
/**
* 获取路由需要的角色
* @param roles
* @param route
* @returns {Array[Role]}
*/
const getRouteRole = (roles, route) => {
const requiredRoles = route.meta.authority.role
return requiredRoles ? roles.filter(item => requiredRoles.findIndex(required => required === item.id) !== -1) : []
}
/**
* 判断是否已为方法注入权限认证
* @param method
* @returns {boolean}
*/
const hasInjected = (method) => method.toString().indexOf('//--auth-inject') !== -1
/**
* 操作权限校验
* @param authConfig
* @param permission
* @param role
* @param permissions
* @param roles
* @returns {boolean}
*/
const auth = function(authConfig, permission, role, permissions, roles) {
const {check, type} = authConfig
if (check && typeof check === 'function') {
return check.apply(this, [permission, role, permissions, roles])
}
if (type === 'permission') {
return checkFromPermission(check, permission)
} else if (type === 'role') {
return checkFromRoles(check, role)
} else {
return checkFromPermission(check, permission) || checkFromRoles(check, role)
}
}
/**
* 检查权限是否有操作权限
* @param check 需要检查的操作权限
* @param permission 权限
* @returns {boolean}
*/
const checkFromPermission = function(check, permission) {
return permission && permission.operation && permission.operation.indexOf(check) !== -1
}
/**
* 检查 roles 是否有操作权限
* @param check 需要检查的操作权限
* @param roles 角色数组
* @returns {boolean}
*/
const checkFromRoles = function(check, roles) {
if (!roles) {
return false
}
for (let role of roles) {
const {operation} = role
if (operation && operation.indexOf(check) !== -1) {
return true
}
}
return false
}
const checkInject = function (el, binding,vnode) {
const type = binding.arg
const check = binding.value
const instance = vnode.context
const $auth = instance.$auth
if (!$auth || !$auth(check, type)) {
addDisabled(el)
} else {
removeDisabled(el)
}
}
const addDisabled = function (el) {
if (el.tagName === 'BUTTON') {
el.disabled = true
} else {
el.classList.add('disabled')
}
el.setAttribute('title', '无此权限')
}
const removeDisabled = function (el) {
el.disabled = false
el.classList.remove('disabled')
el.removeAttribute('title')
}
const AuthorityPlugin = {
install(Vue) {
Vue.directive('auth', {
bind(el, binding,vnode) {
setTimeout(() => checkInject(el, binding, vnode), 10)
},
componentUpdated(el, binding,vnode) {
setTimeout(() => checkInject(el, binding, vnode), 10)
},
unbind(el) {
removeDisabled(el)
}
})
Vue.mixin({
beforeCreate() {
if (this.$options.authorize) {
const authorize = this.$options.authorize
Object.keys(authorize).forEach(key => {
if (this.$options.methods[key]) {
const method = this.$options.methods[key]
if (!hasInjected(method)) {
let authConfig = authorize[key]
authConfig = (typeof authConfig === 'string') ? {check: authConfig} : authConfig
const {check, type, onFailure} = authConfig
this.$options.methods[key] = function () {
//--auth-inject
if (this.$auth(check, type)) {
return method.apply(this, arguments)
} else {
if (onFailure && typeof onFailure === 'function') {
this[`$${check}Failure`] = onFailure
return this[`$${check}Failure`](check)
} else {
this.$message.error(`对不起,您没有操作权限:${check}`)
}
return 0
}
}
}
}
})
}
},
methods: {
/**
* 操作权限校验
* @param check 需要校验的操作名
* @param type 校验类型,通过 permission 校验,还是通过 role 校验。
* 如未设置,则自动识别,如匹配到当前路由 permission 则 type = permission,否则 type = role
* @returns {boolean} 是否校验通过
*/
$auth(check, type) {
const permissions = this.$store.getters['account/permissions']
const roles = this.$store.getters['account/roles']
const permission = getRoutePermission(permissions, this.$route)
const role = getRouteRole(roles, this.$route)
return auth.apply(this, [{check, type}, permission, role, permissions, roles])
}
}
})
}
}
export default AuthorityPlugin
// 语句模式
const MODE = {
STATEMENTS: 's', //语句模式
PHRASAL: 'p', //词组模式
}
const VueI18nPlugin = {
install: function (Vue) {
Vue.mixin({
methods: {
$ta(syntaxKey, mode) {
let _mode = mode || MODE.STATEMENTS
let keys = syntaxKey.split('|')
let _this = this
let locale = this.$i18n.locale
let message = ''
let splitter = locale == 'US' ? ' ' : ''
// 拼接 message
keys.forEach(key => {
message += _this.$t(key) + splitter
})
// 英文环境语句模式下,转换单词大小写
if (keys.length > 0 && _mode == MODE.STATEMENTS && locale == 'US') {
message = message.charAt(0).toUpperCase() + message.toLowerCase().substring(1)
}
return message
}
}
})
}
}
export default VueI18nPlugin
import VueI18nPlugin from './i18n-extend'
import AuthorityPlugin from './authority-plugin'
import TabsPagePlugin from './tabs-page-plugin'
const Plugins = {
install: function (Vue) {
Vue.use(VueI18nPlugin)
Vue.use(AuthorityPlugin)
Vue.use(TabsPagePlugin)
}
}
export default Plugins
const TabsPagePlugin = {
install(Vue) {
Vue.mixin({
methods: {
$closePage(closeRoute, nextRoute) {
const event = new CustomEvent('page:close', {detail:{closeRoute, nextRoute}})
window.dispatchEvent(event)
},
$refreshPage(route) {
const path = typeof route === 'object' ? route.path : route
const event = new CustomEvent('page:refresh', {detail:{pageKey: path}})
window.dispatchEvent(event)
},
$openPage(route, title) {
this.$setPageTitle(route, title)
this.$router.push(route)
},
$setPageTitle(route, title) {
if (title) {
let path = typeof route === 'object' ? route.path : route
path = path && path.split('?')[0]
this.$store.commit('setting/setCustomTitle', {path, title})
}
}
},
computed: {
customTitle() {
const customTitles = this.$store.state.setting.customTitles
const path = this.$route.path.split('?')[0]
const custom = customTitles.find(item => item.path === path)
return custom && custom.title
}
}
})
}
}
export default TabsPagePlugin
import routerMap from './router.map'
import {parseRoutes} from '@/utils/routerUtil'
// 异步路由配置
const routesConfig = [
'login',
'root',
{
router: 'exp404',
path: '*',
name: '404'
},
{
router: 'exp403',
path: '/403',
name: '403'
}
]
const options = {
routes: parseRoutes(routesConfig, routerMap)
}
export default options
// 视图组件
const view = {
tabs: () => import('@/layouts/tabs'),
blank: () => import('@/layouts/BlankView'),
page: () => import('@/layouts/PageView')
}
// 路由组件注册
const routerMap = {
login: {
authority: '*',
path: '/login',
component: () => import('@/pages/login')
},
demo: {
name: '演示页',
renderMenu: false,
component: () => import('@/pages/demo')
},
exp403: {
authority: '*',
name: 'exp403',
path: '403',
component: () => import('@/pages/exception/403')
},
exp404: {
name: 'exp404',
path: '404',
component: () => import('@/pages/exception/404')
},
exp500: {
name: 'exp500',
path: '500',
component: () => import('@/pages/exception/500')
},
root: {
path: '/',
name: '首页',
redirect: '/login',
component: view.tabs
},
parent1: {
name: '父级路由1',
icon: 'dashboard',
component: view.blank
},
parent2: {
name: '父级路由2',
icon: 'form',
component: view.page
},
exception: {
name: '异常页',
icon: 'warning',
component: view.blank
}
}
export default routerMap
import TabsView from '@/layouts/tabs/TabsView'
import BlankView from '@/layouts/BlankView'
import PageView from '@/layouts/PageView'
// 路由配置
const options = {
routes: [
{
path: '/login',
name: '登录页',
component: () => import('@/pages/login')
},
{
path: '*',
name: '404',
component: () => import('@/pages/exception/404'),
},
{
path: '/403',
name: '403',
component: () => import('@/pages/exception/403'),
},
{
path: '/',
name: '首页',
component: TabsView,
redirect: '/login',
children: [
{
path: 'demo',
name: '演示页',
meta: {
icon: 'file-ppt'
},
component: () => import('@/pages/demo')
},
{
path: 'parent1',
name: '父级路由1',
meta: {
icon: 'dashboard',
},
component: BlankView,
children: [
{
path: 'demo1',
name: '演示页面1',
component: () => import('@/pages/demo'),
}
]
},
{
path: 'parent2',
name: '父级路由2',
meta: {
icon: 'form'
},
component: PageView,
children: [
{
path: 'demo2',
name: '演示页面2',
component: () => import('@/pages/demo'),
}
]
},
{
path: 'exception',
name: '异常页',
meta: {
icon: 'warning',
},
component: BlankView,
children: [
{
path: '404',
name: 'Exp404',
component: () => import('@/pages/exception/404')
},
{
path: '403',
name: 'Exp403',
component: () => import('@/pages/exception/403')
},
{
path: '500',
name: 'Exp500',
component: () => import('@/pages/exception/500')
}
]
},
{
name: '验权页面',
path: 'auth/demo',
meta: {
icon: 'file-ppt',
authority: {
permission: 'form',
role: 'manager'
},
component: () => import('@/pages/demo')
}
}
]
}
]
}
export default options
import {hasAuthority} from '@/utils/authority-utils'
import {loginIgnore} from '@/router/index'
import {checkAuthorization} from '@/utils/request'
import NProgress from 'nprogress'
NProgress.configure({ showSpinner: false })
/**
* 进度条开始
* @param to
* @param form
* @param next
*/
const progressStart = (to, from, next) => {
// start progress bar
if (!NProgress.isStarted()) {
NProgress.start()
}
next()
}
/**
* 登录守卫
* @param to
* @param form
* @param next
* @param options
*/
const loginGuard = (to, from, next, options) => {
const {message} = options
if (!loginIgnore.includes(to) && !checkAuthorization()) {
message.warning('登录已失效,请重新登录')
next({path: '/login'})
} else {
next()
}
}
/**
* 权限守卫
* @param to
* @param form
* @param next
* @param options
*/
const authorityGuard = (to, from, next, options) => {
const {store, message} = options
const permissions = store.getters['account/permissions']
const roles = store.getters['account/roles']
if (!hasAuthority(to, permissions, roles)) {
message.warning(`对不起,您无权访问页面: ${to.fullPath},请联系管理员`)
next({path: '/403'})
// NProgress.done()
} else {
next()
}
}
/**
* 混合导航模式下一级菜单跳转重定向
* @param to
* @param from
* @param next
* @param options
* @returns {*}
*/
const redirectGuard = (to, from, next, options) => {
const {store} = options
const getFirstChild = (routes) => {
const route = routes[0]
if (!route.children || route.children.length === 0) {
return route
}
return getFirstChild(route.children)
}
if (store.state.setting.layout === 'mix') {
const firstMenu = store.getters['setting/firstMenu']
if (firstMenu.find(item => item.fullPath === to.fullPath)) {
store.commit('setting/setActivatedFirst', to.fullPath)
const subMenu = store.getters['setting/subMenu']
if (subMenu.length > 0) {
const redirect = getFirstChild(subMenu)
return next({path: redirect.fullPath})
}
}
}
next()
}
/**
* 进度条结束
* @param to
* @param form
* @param options
*/
const progressDone = () => {
// finish progress bar
NProgress.done()
}
export default {
beforeEach: [progressStart, loginGuard, authorityGuard, redirectGuard],
afterEach: [progressDone]
}
module.exports = {
messages: {
CN: {
home: {name: '首页'},
},
US: {
home: {name: 'home'},
},
HK: {
home: {name: '首頁'},
demo: {
name: '演示頁'
},
parent1: {
name: '父級路由1',
demo: {name: '演示頁面1'},
},
parent2: {
name: '父級路由2',
demo: {name: '演示頁面2'},
},
exception: {
name: '異常頁',
404: {name: '404'},
403: {name: '403'},
500: {name: '500'}
}
}
}
}
import Vue from 'vue'
import Router from 'vue-router'
import {formatRoutes} from '@/utils/routerUtil'
Vue.use(Router)
// 不需要登录拦截的路由配置
const loginIgnore = {
names: ['404', '403'], //根据路由名称匹配
paths: ['/login'], //根据路由fullPath匹配
/**
* 判断路由是否包含在该配置中
* @param route vue-router 的 route 对象
* @returns {boolean}
*/
includes(route) {
return this.names.includes(route.name) || this.paths.includes(route.path)
}
}
/**
* 初始化路由实例
* @param isAsync 是否异步路由模式
* @returns {VueRouter}
*/
function initRouter(isAsync) {
const options = isAsync ? require('./async/config.async').default : require('./config').default
formatRoutes(options.routes)
return new Router(options)
}
export {loginIgnore, initRouter}
//跨域代理前缀
// const API_PROXY_PREFIX='/api'
// const BASE_URL = process.env.NODE_ENV === 'production' ? process.env.VUE_APP_API_BASE_URL : API_PROXY_PREFIX
const BASE_URL = process.env.VUE_APP_API_BASE_URL
module.exports = {
LOGIN: `${BASE_URL}/login`,
ROUTES: `${BASE_URL}/routes`
}
import userService from './user'
export {
userService
}
import {LOGIN, ROUTES} from '@/services/api'
import {request, METHOD, removeAuthorization} from '@/utils/request'
/**
* 登录服务
* @param name 账户名
* @param password 账户密码
* @returns {Promise<AxiosResponse<T>>}
*/
export async function login(name, password) {
return request(LOGIN, METHOD.POST, {
name: name,
password: password
})
}
export async function getRoutesConfig() {
return request(ROUTES, METHOD.GET)
}
/**
* 退出登录
*/
export function logout() {
localStorage.removeItem(process.env.VUE_APP_ROUTES_KEY)
localStorage.removeItem(process.env.VUE_APP_PERMISSIONS_KEY)
localStorage.removeItem(process.env.VUE_APP_ROLES_KEY)
removeAuthorization()
}
export default {
login,
logout,
getRoutesConfig
}
import Vue from 'vue'
import Vuex from 'vuex'
import modules from './modules'
Vue.use(Vuex)
const store = new Vuex.Store({modules})
export default store
export default {
namespaced: true,
state: {
user: undefined,
permissions: null,
roles: null,
routesConfig: null
},
getters: {
user: state => {
if (!state.user) {
try {
const user = localStorage.getItem(process.env.VUE_APP_USER_KEY)
state.user = JSON.parse(user)
} catch (e) {
console.error(e)
}
}
return state.user
},
permissions: state => {
if (!state.permissions) {
try {
const permissions = localStorage.getItem(process.env.VUE_APP_PERMISSIONS_KEY)
state.permissions = JSON.parse(permissions)
state.permissions = state.permissions ? state.permissions : []
} catch (e) {
console.error(e.message)
}
}
return state.permissions
},
roles: state => {
if (!state.roles) {
try {
const roles = localStorage.getItem(process.env.VUE_APP_ROLES_KEY)
state.roles = JSON.parse(roles)
state.roles = state.roles ? state.roles : []
} catch (e) {
console.error(e.message)
}
}
return state.roles
},
routesConfig: state => {
if (!state.routesConfig) {
try {
const routesConfig = localStorage.getItem(process.env.VUE_APP_ROUTES_KEY)
state.routesConfig = JSON.parse(routesConfig)
state.routesConfig = state.routesConfig ? state.routesConfig : []
} catch (e) {
console.error(e.message)
}
}
return state.routesConfig
}
},
mutations: {
setUser (state, user) {
state.user = user
localStorage.setItem(process.env.VUE_APP_USER_KEY, JSON.stringify(user))
},
setPermissions(state, permissions) {
state.permissions = permissions
localStorage.setItem(process.env.VUE_APP_PERMISSIONS_KEY, JSON.stringify(permissions))
},
setRoles(state, roles) {
state.roles = roles
localStorage.setItem(process.env.VUE_APP_ROLES_KEY, JSON.stringify(roles))
},
setRoutesConfig(state, routesConfig) {
state.routesConfig = routesConfig
localStorage.setItem(process.env.VUE_APP_ROUTES_KEY, JSON.stringify(routesConfig))
}
}
}
import account from './account'
import setting from './setting'
export default {account, setting}
\ No newline at end of file
import config from '@/config'
import {ADMIN} from '@/config/default'
import {formatFullPath} from '@/utils/i18n'
import {filterMenu} from '@/utils/authority-utils'
import {getLocalSetting} from '@/utils/themeUtil'
import deepClone from 'lodash.clonedeep'
console.log(config)
console.log(ADMIN)
const localSetting = getLocalSetting(true)
const customTitlesStr = sessionStorage.getItem(process.env.VUE_APP_TBAS_TITLES_KEY)
const customTitles = (customTitlesStr && JSON.parse(customTitlesStr)) || []
export default {
namespaced: true,
state: {
isMobile: false,
animates: ADMIN.animates,
palettes: ADMIN.palettes,
pageMinHeight: 0,
menuData: [],
activatedFirst: undefined,
customTitles,
...config,
...localSetting
},
getters: {
menuData(state, getters, rootState) {
if (state.filterMenu) {
const {permissions, roles} = rootState.account
return filterMenu(deepClone(state.menuData), permissions, roles)
}
return state.menuData
},
firstMenu(state, getters) {
const {menuData} = getters
if (menuData.length > 0 && !menuData[0].fullPath) {
formatFullPath(menuData)
}
return menuData.map(item => {
const menuItem = {...item}
delete menuItem.children
return menuItem
})
},
subMenu(state) {
const {menuData, activatedFirst} = state
if (menuData.length > 0 && !menuData[0].fullPath) {
formatFullPath(menuData)
}
const current = menuData.find(menu => menu.fullPath === activatedFirst)
return current && current.children || []
}
},
mutations: {
setDevice (state, isMobile) {
state.isMobile = isMobile
},
setTheme (state, theme) {
console.log(theme)
state.theme = theme
},
setLayout (state, layout) {
state.layout = layout
},
setMultiPage (state, multiPage) {
state.multiPage = multiPage
},
setAnimate (state, animate) {
state.animate = animate
},
setWeekMode(state, weekMode) {
state.weekMode = weekMode
},
setFixedHeader(state, fixedHeader) {
state.fixedHeader = fixedHeader
},
setFixedSideBar(state, fixedSideBar) {
state.fixedSideBar = fixedSideBar
},
setLang(state, lang) {
state.lang = lang
},
setHideSetting(state, hideSetting) {
state.hideSetting = hideSetting
},
correctPageMinHeight(state, minHeight) {
state.pageMinHeight += minHeight
},
setMenuData(state, menuData) {
state.menuData = menuData
},
setAsyncRoutes(state, asyncRoutes) {
state.asyncRoutes = asyncRoutes
},
setPageWidth(state, pageWidth) {
state.pageWidth = pageWidth
},
setActivatedFirst(state, activatedFirst) {
state.activatedFirst = activatedFirst
},
setFixedTabs(state, fixedTabs) {
state.fixedTabs = fixedTabs
},
setCustomTitle(state, {path, title}) {
if (title) {
const obj = state.customTitles.find(item => item.path === path)
if (obj) {
obj.title = title
} else {
state.customTitles.push({path, title})
}
sessionStorage.setItem(process.env.VUE_APP_TBAS_TITLES_KEY, JSON.stringify(state.customTitles))
}
}
}
}
.ant-menu-inline-collapsed-tooltip a{
color: @text-color-inverse;
}
.ant-table-thead{
tr{
th{
&.ant-table-column-has-actions{
&.ant-table-column-has-sorters:hover{
background-color: @background-color-base;
}
&.ant-table-column-has-filters{
&:hover{
.anticon-filter, .anticon-filter:hover{
background-color: @background-color-base;
}
}
.anticon-filter.ant-table-filter-open{
background-color: @background-color-base;
}
}
}
}
}
}
.ant-time-picker-panel-input{
background-color: @component-background;
}
@import "ant-time-picker";
@import "ant-message";
@import "ant-table";
@import "ant-menu";
\ No newline at end of file
@import '~ant-design-vue/lib/style/themes/default';
@gray-1: #ffffff;
@gray-2: #fafafa;
@gray-3: #f5f5f5;
@gray-4: #f0f0f0;
@gray-5: #d9d9d9;
@gray-6: #bfbfbf;
@gray-7: #8c8c8c;
@gray-8: #595959;
@gray-9: #434343;
@gray-10: #262626;
@gray-11: #1f1f1f;
@gray-12: #141414;
@gray-13: #000000;
@primary-color: @primary-color;
@success-color: @success-color;
@warning-color: @warning-color;
@error-color: @warning-color;
@title-color: @heading-color;
@text-color: @text-color;
@text-color-second: @text-color-secondary;
@layout-bg-color: @layout-body-background;
@base-bg-color: @body-background;
@hover-bg-color: rgba(0, 0, 0, 0.025);
@border-color: @border-color-split;
@shadow-color: @shadow-color;
@text-color-inverse: @text-color-inverse;
@hover-bg-color-light: @hover-bg-color;
@hover-bg-color-dark: @primary-7;
@hover-bg-color-night: rgba(255, 255, 255, 0.025);
@header-bg-color-dark: @layout-header-background;
@shadow-down: @shadow-1-down;
@shadow-up: @shadow-1-up;
@shadow-left: @shadow-1-left;
@shadow-right: @shadow-1-right;
@theme-list: light, dark, night;
@h1-size: 12px;
@table-size: 14px;
@menu-size:14px;
@menu-height:40px;
\ No newline at end of file
@import "color";
@import "style";
@import "nprogress";
@import "fontsizw";
\ No newline at end of file
@import '~ant-design-vue/lib/style/themes/default';
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: @primary-color;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px @primary-color, 0 0 5px @primary-color;
opacity: 1.0;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: @primary-color;
border-left-color: @primary-color;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes nprogress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.week-mode{
overflow: hidden;
filter: invert(80%);
}
.beauty-scroll{
scrollbar-color: @primary-color @primary-2;
scrollbar-width: thin;
-ms-overflow-style:none;
position: relative;
&::-webkit-scrollbar{
width: 3px;
height: 1px;
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background: @primary-color;
}
&::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0);
border-radius: 3px;
background: @primary-3;
}
}
.split-right{
&:not(:last-child) {
border-right: 1px solid rgba(98, 98, 98, 0.2);
}
}
.disabled{
cursor: not-allowed;
color: @disabled-color;
pointer-events: none;
}
@import '~ant-design-vue/dist/antd.less';
@import "default/index";
@import "antd/index";
/**
* 给对象注入属性
* @param keys 属性key数组, 如 keys = ['config', 'path'] , 则会给对象注入 object.config.path 的属性
* @param value 属性值
* @returns {Object}
*/
Object.defineProperty(Object.prototype, 'assignProps', {
writable: false,
enumerable: false,
configurable: true,
value: function (keys, value) {
let props = this
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
if (i == keys.length - 1) {
props[key] = value
} else {
props[key] = props[key] == undefined ? {} : props[key]
props = props[key]
}
}
return this
}
})
/**
* 判断是否有路由的权限
* @param authority 路由权限配置
* @param permissions 用户权限集合
* @returns {boolean|*}
*/
function hasPermission(authority, permissions) {
let required = '*'
if (typeof authority === 'string') {
required = authority
} else if (typeof authority === 'object') {
required = authority.permission
}
return required === '*' || (permissions && permissions.findIndex(item => item === required || item.id === required) !== -1)
}
/**
* 判断是否有路由需要的角色
* @param authority 路由权限配置
* @param roles 用户角色集合
*/
function hasRole(authority, roles) {
let required = undefined
if (typeof authority === 'object') {
required = authority.role
}
return authority === '*' || hasAnyRole(required, roles)
}
/**
* 判断是否有需要的任意一个角色
* @param required {String | Array[String]} 需要的角色,可以是单个角色或者一个角色数组
* @param roles 拥有的角色
* @returns {boolean}
*/
function hasAnyRole(required, roles) {
if (!required) {
return false
} else if(Array.isArray(required)) {
return roles.findIndex(role => {
return required.findIndex(item => item === role || item === role.id) !== -1
}) !== -1
} else {
return roles.findIndex(role => role === required || role.id === required) !== -1
}
}
/**
* 路由权限校验
* @param route 路由
* @param permissions 用户权限集合
* @param roles 用户角色集合
* @returns {boolean}
*/
function hasAuthority(route, permissions, roles) {
const authorities = [...route.meta.pAuthorities, route.meta.authority]
for (let authority of authorities) {
if (!hasPermission(authority, permissions) && !hasRole(authority, roles)) {
return false
}
}
return true
}
/**
* 根据权限配置过滤菜单数据
* @param menuData
* @param permissions
* @param roles
*/
function filterMenu(menuData, permissions, roles) {
return menuData.filter(menu => {
if (menu.meta && menu.meta.invisible === undefined) {
if (!hasAuthority(menu, permissions, roles)) {
return false
}
}
if (menu.children && menu.children.length > 0) {
menu.children = filterMenu(menu.children, permissions, roles)
}
return true
})
}
export {filterMenu, hasAuthority}
import Cookie from 'js-cookie'
// 401拦截
const resp401 = {
/**
* 响应数据之前做点什么
* @param response 响应对象
* @param options 应用配置 包含: {router, i18n, store, message}
* @returns {*}
*/
onFulfilled(response, options) {
const {message} = options
if (response.code === 401) {
message.error('无此权限')
}
return response
},
/**
* 响应出错时执行
* @param error 错误对象
* @param options 应用配置 包含: {router, i18n, store, message}
* @returns {Promise<never>}
*/
onRejected(error, options) {
const {message} = options
const {response} = error
if (response.status === 401) {
message.error('无此权限')
}
return Promise.reject(error)
}
}
const resp403 = {
onFulfilled(response, options) {
const {message} = options
if (response.code === 403) {
message.error('请求被拒绝')
}
return response
},
onRejected(error, options) {
const {message} = options
const {response} = error
if (response.status === 403) {
message.error('请求被拒绝')
}
return Promise.reject(error)
}
}
const reqCommon = {
/**
* 发送请求之前做些什么
* @param config axios config
* @param options 应用配置 包含: {router, i18n, store, message}
* @returns {*}
*/
onFulfilled(config, options) {
const {message} = options
const {url, xsrfCookieName} = config
if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {
message.warning('认证 token 已过期,请重新登录')
}
return config
},
/**
* 请求出错时做点什么
* @param error 错误对象
* @param options 应用配置 包含: {router, i18n, store, message}
* @returns {Promise<never>}
*/
onRejected(error, options) {
const {message} = options
message.error(error.message)
return Promise.reject(error)
}
}
export default {
request: [reqCommon], // 请求拦截
response: [resp401, resp403] // 响应拦截
}
const varyColor = require('webpack-theme-color-replacer/client/varyColor')
const {generate} = require('@ant-design/colors')
const {ADMIN, ANTD} = require('../config/default')
const Config = require('../config')
const themeMode = ADMIN.theme.mode
// 获取 ant design 色系
function getAntdColors(color, mode) {
let options = mode && (mode == themeMode.NIGHT) ? {theme: 'dark'} : undefined
return generate(color, options)
}
// 获取功能性颜色
function getFunctionalColors(mode) {
let options = mode && (mode == themeMode.NIGHT) ? {theme: 'dark'} : undefined
let {success, warning, error} = ANTD.primary
const {success: s1, warning: w1, error: e1} = Config.theme
success = success && s1
warning = success && w1
error = success && e1
const successColors = generate(success, options)
const warningColors = generate(warning, options)
const errorColors = generate(error, options)
return {
success: successColors,
warning: warningColors,
error: errorColors
}
}
// 获取菜单色系
function getMenuColors(color, mode) {
if (mode == themeMode.NIGHT) {
return ANTD.primary.night.menuColors
} else if (color == ANTD.primary.color) {
return ANTD.primary.dark.menuColors
} else {
return [varyColor.darken(color, 0.93), varyColor.darken(color, 0.83), varyColor.darken(color, 0.73)]
}
}
// 获取主题模式切换色系
function getThemeToggleColors(color, mode) {
//主色系
const mainColors = getAntdColors(color, mode)
const primary = mainColors[5]
//辅助色系,因为 antd 目前没针对夜间模式设计,所以增加辅助色系以保证夜间模式的正常切换
const subColors = getAntdColors(primary, themeMode.LIGHT)
//菜单色系
const menuColors = getMenuColors(color, mode)
//内容色系(包含背景色、文字颜色等)
const themeCfg = ANTD.theme[mode]
let contentColors = Object.keys(themeCfg)
.map(key => themeCfg[key])
.map(color => isHex(color) ? color : toNum3(color).join(','))
// 内容色去重
contentColors = [...new Set(contentColors)]
// rgb 格式的主题色
let rgbColors = [toNum3(primary).join(',')]
let functionalColors = getFunctionalColors(mode)
return {primary, mainColors, subColors, menuColors, contentColors, rgbColors, functionalColors}
}
function toNum3(color) {
if (isHex(color)) {
return varyColor.toNum3(color)
}
let colorStr = ''
if (isRgb(color)) {
colorStr = color.slice(5, color.length)
} else if (isRgba(color)) {
colorStr = color.slice(6, color.lastIndexOf(','))
}
let rgb = colorStr.split(',')
const r = parseInt(rgb[0])
const g = parseInt(rgb[1])
const b = parseInt(rgb[2])
return [r, g, b]
}
function isHex(color) {
return color.length >= 4 && color[0] == '#'
}
function isRgb(color) {
return color.length >= 10 && color.slice(0, 3) == 'rgb'
}
function isRgba(color) {
return color.length >= 13 && color.slice(0, 4) == 'rgba'
}
module.exports = {
isHex,
isRgb,
isRgba,
toNum3,
getAntdColors,
getMenuColors,
getThemeToggleColors,
getFunctionalColors
}
/**
* 把对象按照 js配置文件的格式进行格式化
* @param obj 格式化的对象
* @param dep 层级,此项无需传值
* @returns {string}
*/
function formatConfig(obj, dep) {
dep = dep || 1
const LN = '\n', TAB = ' '
let indent = ''
for (let i = 0; i < dep; i++) {
indent += TAB
}
let isArray = false, arrayLastIsObj = false
let str = '', prefix = '{', subfix = '}'
if (Array.isArray(obj)) {
isArray = true
prefix = '['
subfix = ']'
str = obj.map((item, index) => {
let format = ''
if (typeof item == 'function') {
//
} else if (typeof item == 'object') {
arrayLastIsObj = true
format = `${LN}${indent}${formatConfig(item,dep + 1)},`
} else if ((typeof item == 'number' && !isNaN(item)) || typeof item == 'boolean') {
format = `${item},`
} else if (typeof item == 'string') {
format = `'${item}',`
}
if (index == obj.length - 1) {
format = format.substring(0, format.length - 1)
} else {
arrayLastIsObj = false
}
return format
}).join('')
} else if (typeof obj != 'function' && typeof obj == 'object') {
str = Object.keys(obj).map((key, index, keys) => {
const val = obj[key]
let format = ''
if (typeof val == 'function') {
//
} else if (typeof val == 'object') {
format = `${LN}${indent}${key}: ${formatConfig(val,dep + 1)},`
} else if ((typeof val == 'number' && !isNaN(val)) || typeof val == 'boolean') {
format = `${LN}${indent}${key}: ${val},`
} else if (typeof val == 'string') {
format = `${LN}${indent}${key}: '${val}',`
}
if (index == keys.length - 1) {
format = format.substring(0, format.length - 1)
}
return format
}).join('')
}
const len = TAB.length
if (indent.length >= len) {
indent = indent.substring(0, indent.length - len)
}
if (!isArray || arrayLastIsObj) {
subfix = LN + indent +subfix
}
return`${prefix}${str}${subfix}`
}
module.exports = {formatConfig}
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import routesI18n from '@/router/i18n'
import './Objects'
import {getI18nKey} from '@/utils/routerUtil'
/**
* 创建 i18n 配置
* @param locale 本地化语言
* @param fallback 回退语言
* @returns {VueI18n}
*/
function initI18n(locale, fallback) {
Vue.use(VueI18n)
let i18nOptions = {
locale,
fallbackLocale: fallback,
silentFallbackWarn: true,
}
return new VueI18n(i18nOptions)
}
/**
* 根据 router options 配置生成 国际化语言
* @param lang
* @param routes
* @param valueKey
* @returns {*}
*/
function generateI18n(lang, routes, valueKey) {
routes.forEach(route => {
let keys = getI18nKey(route.fullPath).split('.')
let value = valueKey === 'path' ? route[valueKey].split('/').filter(item => !item.startsWith(':') && item != '').join('.') : route[valueKey]
lang.assignProps(keys, value)
if (route.children) {
generateI18n(lang, route.children, valueKey)
}
})
return lang
}
/**
* 格式化 router.options.routes,生成 fullPath
* @param routes
* @param parentPath
*/
function formatFullPath(routes, parentPath = '') {
routes.forEach(route => {
let isFullPath = route.path.substring(0, 1) === '/'
route.fullPath = isFullPath ? route.path : (parentPath === '/' ? parentPath + route.path : parentPath + '/' + route.path)
if (route.children) {
formatFullPath(route.children, route.fullPath)
}
})
}
/**
* 从路由提取国际化数据
* @param i18n
* @param routes
*/
function mergeI18nFromRoutes(i18n, routes) {
formatFullPath(routes)
const CN = generateI18n(new Object(), routes, 'name')
const US = generateI18n(new Object(), routes, 'path')
i18n.mergeLocaleMessage('CN', CN)
i18n.mergeLocaleMessage('US', US)
const messages = routesI18n.messages
Object.keys(messages).forEach(lang => {
i18n.mergeLocaleMessage(lang, messages[lang])
})
}
export {
initI18n,
mergeI18nFromRoutes,
formatFullPath
}
import axios from 'axios'
import Cookie from 'js-cookie'
// 跨域认证信息 header 名
const xsrfHeaderName = 'Authorization'
axios.defaults.timeout = 5000
axios.defaults.withCredentials= true
axios.defaults.xsrfHeaderName= xsrfHeaderName
axios.defaults.xsrfCookieName= xsrfHeaderName
// 认证类型
const AUTH_TYPE = {
BEARER: 'Bearer',
BASIC: 'basic',
AUTH1: 'auth1',
AUTH2: 'auth2',
}
// http method
const METHOD = {
GET: 'get',
POST: 'post'
}
/**
* axios请求
* @param url 请求地址
* @param method {METHOD} http method
* @param params 请求参数
* @returns {Promise<AxiosResponse<T>>}
*/
async function request(url, method, params, config) {
switch (method) {
case METHOD.GET:
return axios.get(url, {params, ...config})
case METHOD.POST:
return axios.post(url, params, config)
default:
return axios.get(url, {params, ...config})
}
}
/**
* 设置认证信息
* @param auth {Object}
* @param authType {AUTH_TYPE} 认证类型,默认:{AUTH_TYPE.BEARER}
*/
function setAuthorization(auth, authType = AUTH_TYPE.BEARER) {
switch (authType) {
case AUTH_TYPE.BEARER:
Cookie.set(xsrfHeaderName, 'Bearer ' + auth.token, {expires: auth.expireAt})
break
case AUTH_TYPE.BASIC:
case AUTH_TYPE.AUTH1:
case AUTH_TYPE.AUTH2:
default:
break
}
}
/**
* 移出认证信息
* @param authType {AUTH_TYPE} 认证类型
*/
function removeAuthorization(authType = AUTH_TYPE.BEARER) {
switch (authType) {
case AUTH_TYPE.BEARER:
Cookie.remove(xsrfHeaderName)
break
case AUTH_TYPE.BASIC:
case AUTH_TYPE.AUTH1:
case AUTH_TYPE.AUTH2:
default:
break
}
}
/**
* 检查认证信息
* @param authType
* @returns {boolean}
*/
function checkAuthorization(authType = AUTH_TYPE.BEARER) {
switch (authType) {
case AUTH_TYPE.BEARER:
if (Cookie.get(xsrfHeaderName)) {
return true
}
break
case AUTH_TYPE.BASIC:
case AUTH_TYPE.AUTH1:
case AUTH_TYPE.AUTH2:
default:
break
}
return false
}
/**
* 加载 axios 拦截器
* @param interceptors
* @param options
*/
function loadInterceptors(interceptors, options) {
const {request, response} = interceptors
// 加载请求拦截器
request.forEach(item => {
let {onFulfilled, onRejected} = item
if (!onFulfilled || typeof onFulfilled !== 'function') {
onFulfilled = config => config
}
if (!onRejected || typeof onRejected !== 'function') {
onRejected = error => Promise.reject(error)
}
axios.interceptors.request.use(
config => onFulfilled(config, options),
error => onRejected(error, options)
)
})
// 加载响应拦截器
response.forEach(item => {
let {onFulfilled, onRejected} = item
if (!onFulfilled || typeof onFulfilled !== 'function') {
onFulfilled = response => response
}
if (!onRejected || typeof onRejected !== 'function') {
onRejected = error => Promise.reject(error)
}
axios.interceptors.response.use(
response => onFulfilled(response, options),
error => onRejected(error, options)
)
})
}
/**
* 解析 url 中的参数
* @param url
* @returns {Object}
*/
function parseUrlParams(url) {
const params = {}
if (!url || url === '' || typeof url !== 'string') {
return params
}
const paramsStr = url.split('?')[1]
if (!paramsStr) {
return params
}
const paramsArr = paramsStr.replace(/&|=/g, ' ').split(' ')
for (let i = 0; i < paramsArr.length / 2; i++) {
const value = paramsArr[i * 2 + 1]
params[paramsArr[i * 2]] = value === 'true' ? true : (value === 'false' ? false : value)
}
return params
}
export {
METHOD,
AUTH_TYPE,
request,
setAuthorization,
removeAuthorization,
checkAuthorization,
loadInterceptors,
parseUrlParams
}
import routerMap from '@/router/async/router.map'
import {mergeI18nFromRoutes} from '@/utils/i18n'
import Router from 'vue-router'
import deepMerge from 'deepmerge'
import basicOptions from '@/router/async/config.async'
//应用配置
let appOptions = {
router: undefined,
i18n: undefined,
store: undefined
}
/**
* 设置应用配置
* @param options
*/
function setAppOptions(options) {
const {router, store, i18n} = options
appOptions.router = router
appOptions.store = store
appOptions.i18n = i18n
}
/**
* 根据 路由配置 和 路由组件注册 解析路由
* @param routesConfig 路由配置
* @param routerMap 本地路由组件注册配置
*/
function parseRoutes(routesConfig, routerMap) {
let routes = []
routesConfig.forEach(item => {
// 获取注册在 routerMap 中的 router,初始化 routeCfg
let router = undefined, routeCfg = {}
if (typeof item === 'string') {
router = routerMap[item]
routeCfg = {path: (router && router.path) || item, router: item}
} else if (typeof item === 'object') {
router = routerMap[item.router]
routeCfg = item
}
if (!router) {
console.warn(`can't find register for router ${routeCfg.router}, please register it in advance.`)
router = typeof item === 'string' ? {path: item, name: item} : item
}
// 从 router 和 routeCfg 解析路由
const meta = {
authority: router.authority,
icon: router.icon,
page: router.page,
link: router.link,
params: router.params,
query: router.query,
...router.meta
}
const cfgMeta = {
authority: routeCfg.authority,
icon: routeCfg.icon,
page: routeCfg.page,
link: routeCfg.link,
params: routeCfg.params,
query: routeCfg.query,
...routeCfg.meta
}
Object.keys(cfgMeta).forEach(key => {
if (cfgMeta[key] === undefined || cfgMeta[key] === null || cfgMeta[key] === '') {
delete cfgMeta[key]
}
})
Object.assign(meta, cfgMeta)
const route = {
path: routeCfg.path || router.path || routeCfg.router,
name: routeCfg.name || router.name,
component: router.component,
redirect: routeCfg.redirect || router.redirect,
meta: {...meta, authority: meta.authority || '*'}
}
if (routeCfg.invisible || router.invisible) {
route.meta.invisible = true
}
if (routeCfg.children && routeCfg.children.length > 0) {
route.children = parseRoutes(routeCfg.children, routerMap)
}
routes.push(route)
})
return routes
}
/**
* 加载路由
* @param routesConfig {RouteConfig[]} 路由配置
*/
function loadRoutes(routesConfig) {
//兼容 0.6.1 以下版本
/*************** 兼容 version < v0.6.1 *****************/
if (arguments.length > 0) {
const arg0 = arguments[0]
if (arg0.router || arg0.i18n || arg0.store) {
routesConfig = arguments[1]
console.error('the usage of signature loadRoutes({router, store, i18n}, routesConfig) is out of date, please use the new signature: loadRoutes(routesConfig).')
console.error('方法签名 loadRoutes({router, store, i18n}, routesConfig) 的用法已过时, 请使用新的方法签名 loadRoutes(routesConfig)。')
}
}
/*************** 兼容 version < v0.6.1 *****************/
// 应用配置
const {router, store, i18n} = appOptions
// 如果 routesConfig 有值,则更新到本地,否则从本地获取
if (routesConfig) {
store.commit('account/setRoutesConfig', routesConfig)
} else {
routesConfig = store.getters['account/routesConfig']
}
// 如果开启了异步路由,则加载异步路由配置
const asyncRoutes = store.state.setting.asyncRoutes
if (asyncRoutes) {
if (routesConfig && routesConfig.length > 0) {
const routes = parseRoutes(routesConfig, routerMap)
const finalRoutes = mergeRoutes(basicOptions.routes, routes)
formatRoutes(finalRoutes)
router.options = {...router.options, routes: finalRoutes}
router.matcher = new Router({...router.options, routes:[]}).matcher
router.addRoutes(finalRoutes)
}
}
// 提取路由国际化数据
mergeI18nFromRoutes(i18n, router.options.routes)
// 初始化Admin后台菜单数据
const rootRoute = router.options.routes.find(item => item.path === '/')
const menuRoutes = rootRoute && rootRoute.children
if (menuRoutes) {
store.commit('setting/setMenuData', menuRoutes)
}
}
/**
* 合并路由
* @param target {Route[]}
* @param source {Route[]}
* @returns {Route[]}
*/
function mergeRoutes(target, source) {
const routesMap = {}
target.forEach(item => routesMap[item.path] = item)
source.forEach(item => routesMap[item.path] = item)
return Object.values(routesMap)
}
/**
* 深度合并路由
* @param target {Route[]}
* @param source {Route[]}
* @returns {Route[]}
*/
function deepMergeRoutes(target, source) {
// 映射路由数组
const mapRoutes = routes => {
const routesMap = {}
routes.forEach(item => {
routesMap[item.path] = {
...item,
children: item.children ? mapRoutes(item.children) : undefined
}
})
return routesMap
}
const tarMap = mapRoutes(target)
const srcMap = mapRoutes(source)
// 合并路由
const merge = deepMerge(tarMap, srcMap)
// 转换为 routes 数组
const parseRoutesMap = routesMap => {
return Object.values(routesMap).map(item => {
if (item.children) {
item.children = parseRoutesMap(item.children)
} else {
delete item.children
}
return item
})
}
return parseRoutesMap(merge)
}
/**
* 格式化路由
* @param routes 路由配置
*/
function formatRoutes(routes) {
routes.forEach(route => {
const {path} = route
if (!path.startsWith('/') && path !== '*') {
route.path = '/' + path
}
})
formatAuthority(routes)
}
/**
* 格式化路由的权限配置
* @param routes 路由
* @param pAuthorities 父级路由权限配置集合
*/
function formatAuthority(routes, pAuthorities = []) {
routes.forEach(route => {
const meta = route.meta
const defaultAuthority = pAuthorities[pAuthorities.length - 1] || {permission: '*'}
if (meta) {
let authority = {}
if (!meta.authority) {
authority = defaultAuthority
}else if (typeof meta.authority === 'string') {
authority.permission = meta.authority
} else if (typeof meta.authority === 'object') {
authority = meta.authority
const {role} = authority
if (typeof role === 'string') {
authority.role = [role]
}
if (!authority.permission && !authority.role) {
authority = defaultAuthority
}
}
meta.authority = authority
} else {
const authority = defaultAuthority
route.meta = {authority}
}
route.meta.pAuthorities = pAuthorities
if (route.children) {
formatAuthority(route.children, [...pAuthorities, route.meta.authority])
}
})
}
/**
* 从路由 path 解析 i18n key
* @param path
* @returns {*}
*/
function getI18nKey(path) {
const keys = path.split('/').filter(item => !item.startsWith(':') && item != '')
keys.push('name')
return keys.join('.')
}
/**
* 加载导航守卫
* @param guards
* @param options
*/
function loadGuards(guards, options) {
const {beforeEach, afterEach} = guards
const {router} = options
beforeEach.forEach(guard => {
if (guard && typeof guard === 'function') {
router.beforeEach((to, from, next) => guard(to, from, next, options))
}
})
afterEach.forEach(guard => {
if (guard && typeof guard === 'function') {
router.afterEach((to, from) => guard(to, from, options))
}
})
}
export {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards, deepMergeRoutes, formatRoutes, setAppOptions}
const {cssResolve} = require('../config/replacer')
// 修正 webpack-theme-color-replacer 插件提取的 css 结果
function resolveCss(output, srcArr) {
let regExps = []
// 提取 resolve 配置中所有的正则配置
Object.keys(cssResolve).forEach(key => {
let isRegExp = false
let reg = {}
try {
reg = eval(key)
isRegExp = reg instanceof RegExp
} catch (e) {
isRegExp = false
}
if (isRegExp) {
regExps.push([reg, cssResolve[key]])
}
})
// 去重
srcArr = dropDuplicate(srcArr)
// 处理 css
let outArr = []
srcArr.forEach(text => {
// 转换为 css 对象
let cssObj = parseCssObj(text)
// 根据selector匹配配置,匹配成功,则按配置处理 css
if (cssResolve[cssObj.selector] != undefined) {
let cfg = cssResolve[cssObj.selector]
if (cfg) {
outArr.push(cfg.resolve(text, cssObj))
}
} else {
let cssText = ''
// 匹配不成功,则测试是否有匹配的正则配置,有则按正则对应的配置处理
for (let regExp of regExps) {
if (regExp[0].test(cssObj.selector)) {
let cssCfg = regExp[1]
cssText = cssCfg ? cssCfg.resolve(text, cssObj) : ''
break
}
// 未匹配到正则,则设置 cssText 为默认的 css(即不处理)
cssText = text
}
if (cssText != '') {
outArr.push(cssText)
}
}
})
output = outArr.join('\n')
return output
}
// 数组去重
function dropDuplicate(arr) {
let map = {}
let r = []
for (let s of arr) {
if (!map[s]) {
r.push(s)
map[s] = 1
}
}
return r
}
/**
* 从字符串解析 css 对象
* @param cssText
* @returns {{
* name: String,
* rules: Array[String],
* toText: function
* }}
*/
function parseCssObj(cssText) {
let css = {}
const ruleIndex = cssText.indexOf('{')
css.selector = cssText.substring(0, ruleIndex)
const ruleBody = cssText.substring(ruleIndex + 1, cssText.length - 1)
const rules = ruleBody.split(';')
css.rules = rules
css.toText = function () {
let body = ''
this.rules.forEach(item => {body += item + ';'})
return `${this.selector}{${body}}`
}
return css
}
module.exports = {resolveCss}
const client = require('webpack-theme-color-replacer/client')
const {theme} = require('../config')
const {getMenuColors, getAntdColors, getThemeToggleColors, getFunctionalColors} = require('../utils/colors')
const {ANTD} = require('../config/default')
function getThemeColors(color, $theme) {
const _color = color || theme.color
const mode = $theme || theme.mode
const replaceColors = getThemeToggleColors(_color, mode)
const themeColors = [
...replaceColors.mainColors,
...replaceColors.subColors,
...replaceColors.menuColors,
...replaceColors.contentColors,
...replaceColors.rgbColors,
...replaceColors.functionalColors.success,
...replaceColors.functionalColors.warning,
...replaceColors.functionalColors.error,
]
return themeColors
}
function changeThemeColor(newColor, $theme) {
let promise = client.changer.changeColor({newColors: getThemeColors(newColor, $theme)})
return promise
}
function modifyVars(color) {
let _color = color || theme.color
const palettes = getAntdColors(_color, theme.mode)
const menuColors = getMenuColors(_color, theme.mode)
const {success, warning, error} = getFunctionalColors(theme.mode)
const primary = palettes[5]
return {
'primary-color': primary,
'primary-1': palettes[0],
'primary-2': palettes[1],
'primary-3': palettes[2],
'primary-4': palettes[3],
'primary-5': palettes[4],
'primary-6': palettes[5],
'primary-7': palettes[6],
'primary-8': palettes[7],
'primary-9': palettes[8],
'primary-10': palettes[9],
'info-color': primary,
'success-color': success[5],
'warning-color': warning[5],
'error-color': error[5],
'alert-info-bg-color': palettes[0],
'alert-info-border-color': palettes[2],
'alert-success-bg-color': success[0],
'alert-success-border-color': success[2],
'alert-warning-bg-color': warning[0],
'alert-warning-border-color': warning[2],
'alert-error-bg-color': error[0],
'alert-error-border-color': error[2],
'processing-color': primary,
'menu-dark-submenu-bg': menuColors[0],
'layout-header-background': menuColors[1],
'layout-trigger-background': menuColors[2],
'btn-danger-bg': error[4],
'btn-danger-border': error[4],
...ANTD.theme[theme.mode]
}
}
function loadLocalTheme(localSetting) {
if (localSetting && localSetting.theme) {
let {color, mode} = localSetting.theme
color = color || theme.color
mode = mode || theme.mode
changeThemeColor(color, mode)
}
}
/**
* 获取本地保存的配置
* @param load {boolean} 是否加载配置中的主题
* @returns {Object}
*/
function getLocalSetting(loadTheme) {
let localSetting = {}
try {
const localSettingStr = localStorage.getItem(process.env.VUE_APP_SETTING_KEY)
localSetting = JSON.parse(localSettingStr)
} catch (e) {
console.error(e)
}
if (loadTheme) {
loadLocalTheme(localSetting)
}
return localSetting
}
module.exports = {
getThemeColors,
changeThemeColor,
modifyVars,
loadLocalTheme,
getLocalSetting
}
import enquireJs from 'enquire.js'
export function isDef (v){
return v !== undefined && v !== null
}
/**
* Remove an item from an array.
*/
export function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
export function isRegExp (v) {
return _toString.call(v) === '[object RegExp]'
}
export function enquireScreen(call) {
const handler = {
match: function () {
call && call(true)
},
unmatch: function () {
call && call(false)
}
}
enquireJs.register('only screen and (max-width: 767.99px)', handler)
}
const _toString = Object.prototype.toString
let path = require('path')
const webpack = require('webpack')
const ThemeColorReplacer = require('webpack-theme-color-replacer')
const {getThemeColors, modifyVars} = require('./src/utils/themeUtil')
const {resolveCss} = require('./src/utils/theme-color-replacer-extend')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const productionGzipExtensions = ['js', 'css']
const isProd = process.env.NODE_ENV === 'production'
const assetsCDN = {
// webpack build externals
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex',
axios: 'axios',
nprogress: 'NProgress',
clipboard: 'ClipboardJS',
'@antv/data-set': 'DataSet',
'js-cookie': 'Cookies'
},
css: [
],
js: [
'//cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js',
'//cdn.jsdelivr.net/npm/vue-router@3.3.4/dist/vue-router.min.js',
'//cdn.jsdelivr.net/npm/vuex@3.4.0/dist/vuex.min.js',
'//cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js',
'//cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js',
'//cdn.jsdelivr.net/npm/clipboard@2.0.6/dist/clipboard.min.js',
'//cdn.jsdelivr.net/npm/@antv/data-set@0.11.4/build/data-set.min.js',
'//cdn.jsdelivr.net/npm/js-cookie@2.2.1/src/js.cookie.min.js'
]
}
module.exports = {
devServer: {
// proxy: {
// '/api': { //此处要与 /services/api.js 中的 API_PROXY_PREFIX 值保持一致
// target: process.env.VUE_APP_API_BASE_URL,
// changeOrigin: true,
// pathRewrite: {
// '^/api': ''
// }
// }
// }
},
pluginOptions: {
'style-resources-loader': {
preProcessor: 'less',
patterns: [path.resolve(__dirname, "./src/theme/theme.less")],
}
},
configureWebpack: config => {
config.entry.app = ["babel-polyfill", "whatwg-fetch", "./src/main.js"];
config.performance = {
hints: false
}
config.plugins.push(
new ThemeColorReplacer({
fileName: 'css/theme-colors-[contenthash:8].css',
matchColors: getThemeColors(),
injectCss: true,
resolveCss
})
)
// Ignore all locale files of moment.js
config.plugins.push(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
// 生产环境下将资源压缩成gzip格式
if (isProd) {
// add `CompressionWebpack` plugin to webpack plugins
config.plugins.push(new CompressionWebpackPlugin({
algorithm: 'gzip',
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
threshold: 10240,
minRatio: 0.8
}))
}
// if prod, add externals
if (isProd) {
config.externals = assetsCDN.externals
}
},
chainWebpack: config => {
// 生产环境下关闭css压缩的 colormin 项,因为此项优化与主题色替换功能冲突
if (isProd) {
config.plugin('optimize-css')
.tap(args => {
args[0].cssnanoOptions.preset[1].colormin = false
return args
})
}
// 生产环境下使用CDN
if (isProd) {
config.plugin('html')
.tap(args => {
args[0].cdn = assetsCDN
return args
})
}
},
css: {
loaderOptions: {
less: {
lessOptions: {
modifyVars: modifyVars(),
javascriptEnabled: true
}
}
}
},
publicPath: process.env.VUE_APP_PUBLIC_PATH,
outputDir: 'dist',
assetsDir: 'static',
productionSourceMap: false
}
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment