Commit f509285f authored by wb-ct393452's avatar wb-ct393452

init application

parents
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
VUE_APP_API_BASE_URL=http://dev.iczer.com
#生成环境 - API调用路径 根据实际修改
VUE_APP_API_BASE_URL=http://api.akina.com
#测试环境 - API调用路径 根据实际修改
VUE_APP_API_BASE_URL=http://test.akina.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
{
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 110,
"proseWrap": "never",
"overrides": [
{
"files": ".prettierrc",
"options": {
"parser": "json"
}
}
]
}
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.
## 浏览器支持
现代浏览器及 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
```
\ No newline at end of file
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
}
<template>
<div class="alert" :style="`top: ${top}px`">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Alert',
props: ['show'],
data() {
return {
top: 100
}
},
mounted() {
console.log(this)
// this.$page.alert = this.$page.alert ? this.$page.alert : {top: 100}
// this.$page.alert.top += 20
// this.top = this.$page.alert.top
setTimeout(() => {
this.$el.remove()
}, 1000)
}
}
</script>
<style scoped>
.alert{
position: absolute;
padding: 6px 8px;
background-color: #f0f2f5;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
border-radius: 4px;
margin: 0 auto;
z-index: 999;
top: 100px;
width: fit-content;
left: 0;
right: 0;
}
</style>
\ No newline at end of file
<template>
<div :data-clipboard-text="color" class="color" @click="onClick" :style="`background-color:${color}`" />
</template>
<script>
import Clipboard from 'clipboard'
export default {
name: 'Color',
props: ['color'],
data() {
return {
alert: false
}
},
methods: {
onClick() {
let clipboard = new Clipboard('.color')
clipboard.on('success', () => {
this.$alert(`颜色代码已复制:${this.color}`)
clipboard.destroy()
})
}
}
}
</script>
<style scoped>
.color{
border: 1px dashed #a0d911;
display: inline-block;
width: 20px;
height: 20px;
cursor: pointer;
}
</style>
\ No newline at end of file
<template>
<div>
<color class="color" :key="index" v-for="(color, index) in colors" :color="color" ></color>
</div>
</template>
<script>
export default {
name: 'ColorList',
props: ['colors']
}
</script>
<style scoped>
.color{
margin: 0 2px;
}
</style>
\ No newline at end of file
module.exports = {
title: 'Vue Antd Admin',
description: 'Vue Antd Admin',
base: '/vue-antd-admin-docs/',
head: [
['link', { rel: 'icon', href: '/favicon.ico' }]
],
themeConfig: {
logo: '/logo.png',
repo: 'iczer/vue-antd-admin',
docsDir: 'docs',
editLinks: true,
editLinkText: '在 Github 上帮助我们编辑此页',
nav: [
{text: '指南', link: '/'},
{text: '配置', link: '/develop/layout'},
{text: '主题', link: '/advance/theme'},
],
lastUpdated: 'Last Updated',
sidebar: [
{
title: '开始',
collapsable: false,
children: [
'/start/use', '/start/faq'
]
},
{
title: '开发',
collapsable: false,
children: [
'/develop/layout', '/develop/router', '/develop/page', '/develop/theme', '/develop/service', '/develop/mock'
]
},
{
title: '进阶',
collapsable: false,
children: [
'/advance/i18n', '/advance/async', '/advance/authority', '/advance/login', '/advance/guard', '/advance/interceptors',
'/advance/api'
]
},
{
title: '其它',
collapsable: false,
children: [
'/other/upgrade', '/other/community'
]
}
],
nextLinks: true,
prevLinks: true,
},
plugins: ['@vuepress/back-to-top', require('./plugins/alert')],
markdown: {
lineNumbers: true
}
}
<template>
<div class="alert" :style="`top: ${top}px`">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Alert',
props: ['alert'],
data() {
return {
top: 0
}
},
beforeMount() {
this.top = this.alert.top
},
mounted() {
window.addEventListener('alert_remove', (e) => {
this.top -= e.detail.height
})
},
watch: {
'page.alert.top': function (value) {
}
}
}
</script>
<style scoped>
.alert{
position: fixed;
padding: 6px 8px;
background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.25);
border-radius: 4px;
margin: 0 auto;
z-index: 999;
top: 100px;
width: fit-content;
left: 0;
right: 0;
transition: top 0.3s;
}
</style>
\ No newline at end of file
import Alert from './Alert'
const AlertMixin = {
install(Vue) {
Vue.mixin({
methods: {
$alert(message, duration = 2000) {
let Constructor= Vue.extend(Alert)
let alert = new Constructor()
alert.$slots.default = message
alert.$props.alert = this.$page.alert
alert.$mount()
document.body.appendChild(alert.$el)
const appendHeight = alert.$el.offsetHeight + 16
this.$page.alert.top += appendHeight
setTimeout(() => {
this.$page.alert.top -= appendHeight
this.triggerRemoveAlert(appendHeight)
setTimeout(() => {
alert.$destroy()
alert.$el.remove()
}, 100)
}, duration)
},
triggerRemoveAlert(height) {
const event = new CustomEvent('alert_remove', {
detail: {height}
})
window.dispatchEvent(event)
}
}
})
}
}
export default AlertMixin
export default {
updated() {
this.$page.alert.top = 100
}
}
import AlertMixin from './alertMixin'
export default ({Vue}) => {
Vue.use(AlertMixin)
}
\ No newline at end of file
const path = require('path')
module.exports = (options, ctx) => {
return {
clientRootMixin: path.resolve(__dirname, 'clientRootMixin.js'),
extendPageData($page) {
$page.alert = {
top: 100
}
},
enhanceAppFiles: path.resolve(__dirname, 'enhanceApp.js')
}
}
\ No newline at end of file
.custom-block.tip{
border-color: #1890ff
}
.theme-default-content code .token.inserted{
color: #60bd90;
}
//.custom-block.warning{
// border-color: #fa8c16
//}
//.custom-block.error{
// border-color: #f5222d
//}
$accentColor = #1890ff
$contentWidth = 940px
---
title: 首页
home: true
heroImage: /logo.png
heroText: Vue Antd Admin
tagline: 开箱即用的中台前端/设计解决方案
actionText: 快速上手 →
actionLink: /start/use
features:
- title: 简洁
details: 以 Markdown 为中心的项目结构,以最少的配置帮助你专注于写作。
- title: 优雅
details: 享受 Vue + webpack 的开发体验,在 Markdown 中使用 Vue 组件,同时可以使用 Vue 来开发自定义主题。
- title: 自然
details: VuePress 为每个页面预渲染生成静态的 HTML,同时在页面被加载的时候,将作为 SPA 运行。
footer: MIT Licensed | Copyright © 2018-present iczer
---
---
title: 进阶
lang: zn-CN
---
# 进阶
---
title: 全局API
lang: zn-CN
---
# 全局API
我们提供了一些全局Api,在日常功能开发中或许会有帮助,它们均被绑定到了页面组件或子组件实例上。
在组件内可以直接通过`this.$[apiName]`的方式调用。如下:
## 多页签
### $closePage(closeRoute, nextRoute)
该api用于关闭当前已打开的页签,接收两个参数:
* **closeRoute**
要关闭的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。
* **nextRoute**
关闭页签要后跳转的 route 对象,可不传,不传则会自动选择打开页签(临近原则)。
### $refreshPage(route)
该api用于刷新路由对应的页签,接收一个参数:
* **route**
要刷新的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。
### $openPage(route, title)
该api用于打开一个新页签,接收两个参数:
* **route**
要打开的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。
* **title**
设置打开页签的标题,可不传。
### $setPageTitle(route, title)
该api用于设置页签的标题,接收两个参数:
* **route**
要设置的页签对应的 route 对象,可简写为路由的 fullPath 字符串值。
* **title**
页签的标题。
## 权限
### $auth(check, type)
该api可以用于操作权限校验,接收两个参数:
* **check**
需要要校验的操作权限
* **type**
操作权限校验类别,可选 `permission``role`,即通过权限校验还是角色进行校验,可不传(不传的话,会对两种类型都进行匹配,任意一种匹配成功即校验通过)。
\ No newline at end of file
---
title: 异步路由和菜单
lang: zn-CN
---
# 异步路由和菜单
在现实业务中,存在这样的场景,系统的路由和菜单会根据用户的角色变化而变化,或者路由菜单根据用户的权限动态生成。我们为此准备了一套完整的异步加载方案,
可以让你很方便的从服务端加载路由和菜单配置,并应用到系统中。
## 异步加载路由
动态路由的实现主要有以下四个步骤:
### 开启异步路由设置
`/config/config.js` 文件中设置 `asyncRoutes` 的值为 true:
```js {7}
module.exports = {
theme: {
color: '#13c2c2',
mode: 'night'
},
multiPage: true,
asyncRoutes: true, //异步加载路由,true:开启,false:不开启
animate: {
name: 'roll',
direction: 'default'
}
}
```
### 注册路由组件
基础路由组件包含路由基本配置和对应的视图组件,我们统一在 `/router/async/router.map.js` 文件中注册它们。它和正常的路由配置基本无异,相当于把完整的路由拆分成单个的路由配置进行注册,为后面的路由动态配置打好基础。
一个单独的路由组件注册示例如下:
```jsx
registerName: { //路由组件注册名称,唯一标识
path: 'path', //路由path,可缺省,默认取路由注册名称 registerName 的值
name: '演示页', //路由名称
redirect: '/login', //路由重定向
component: () => import('@/pages/demo'), //路由视图
icon: 'permission', //路由的菜单icon,会注入到路由元数据meta中
invisible: false, //是否隐藏菜单项,true 隐藏,false 不隐藏,会注入到路由元数据meta中。
authority: { //路由权限配置,会注入到路由元数据meta中。可缺省,默认为 ‘*’, 即无权限限制
permission: 'form', //路由需要的权限
role: 'admin' //路由需要的角色。当permission未设置,通过 role 检查权限
},
page: { //路由的页面数据,会注入到路由元数据meta中
title: '演示页', //页面标题
breadcrumb: ['首页', '演示页'] //页面面包屑
}
}
```
:::details 点击查看完整的路由注册示例:
```js
// 视图组件
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
```
:::
### 配置基本路由
如果没有任何路由,你的应用是无法访问的,所以我们需要在本地配置一些基本的路由,比如登录页、404、403 等。你可以在 `/router/async/config.async.js` 文件中配置一些本地必要的路由。如下:
```js
const routesConfig = [
'login', //匹配 router.map.js 中注册的 registerName = login 的路由
'root', //匹配 router.map.js 中注册的 registerName = root 的路由
{
router: 'exp404', //匹配 router.map.js 中注册的 registerName = exp404 的路由
path: '*', //重写 exp404 路由的 path 属性
name: '404' //重写 exp404 路由的 name 属性
},
{
router: 'exp403', //匹配 router.map.js 中注册的 registerName = exp403 的路由
path: '/403', //重写 exp403 路由的 path 属性
name: '403' //重写 exp403 路由的 name 属性
}
]
```
完成配置后,即可通过 `routesConfig` 和已注册的 `routerMap` 生成 [router.options.routes](https://router.vuejs.org/zh/api/#router-%E6%9E%84%E5%BB%BA%E9%80%89%E9%A1%B9) 配置,如下:
```js
const options = {
routes: parseRoutes(routesConfig, routerMap)
}
```
:::details 点击查看完整的 config.async.js 代码
```js
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
```
:::
完成以上设置后,本地就已经有了包含 login、404、403 页面的路由,并且这些路由是可以直接访问的。
### 异步获取路由配置
当用户登录后(或者其它的前提条件),你可能想根据不同用户加载不同的路由和菜单。
那么我们就需要先从后端服务获取异步路由配置,后端返回的异步路由配置 `routesConfig` 是一个异步路由配置数组, 应当如下格式:
```jsx
[{
router: 'root', //匹配 router.map.js 中注册名 registerName = root 的路由
children: [ //root 路由的子路由配置
{
router: 'dashboard', //匹配 router.map.js 中注册名 registerName = dashboard 的路由
children: ['workplace', 'analysis'], //dashboard 路由的子路由配置,依次匹配 registerName 为 workplace 和 analysis 的路由
},
{
router: 'form', //匹配 router.map.js 中注册名 registerName = form 的路由
children: [ //form 路由的子路由配置
'basicForm', //匹配 router.map.js 中注册名 registerName = basicForm 的路由
'stepForm', //匹配 router.map.js 中注册名 registerName = stepForm 的路由
{
router: 'advanceForm', //匹配 router.map.js 中注册名 registerName = advanceForm 的路由
path: 'advance' //重写 advanceForm 路由的 path 属性
}
]
},
{
router: 'basicForm', //匹配 router.map.js 中注册名 registerName = basicForm 的路由
name: '验权表单', //重写 basicForm 路由的 name 属性
icon: 'file-excel', //重写 basicForm 路由的 icon 属性
authority: 'form' //重写 basicForm 路由的 authority 属性
}
]
}]
```
其中 `router` 属性 对应 `router.map.js` 中已注册的`基础路由`的注册名称 `registerName``children` 属性为路由的嵌套子路由配置。
有些情况下你可能想重写已注册路由的属性,你可以为 `routesConfig` 配置同名属性去覆盖它。如上面的`验权表单`路由覆盖了注册路由的 `name``icon``authority` 属性。
### 加载路由并应用
我们提供了一个路由加载工具,你只需调用 `/utils/routerUtil.js` 中的 `loadRoutes` 方法加载上一步获取到的 `routesConfig` 即可,如下:
```js {3}
getRoutesConfig().then(result => {
const routesConfig = result.data.data
loadRoutes(routesConfig)
})
```
至此,异步路由的加载就完成了,你可以访问异步加载的路由了。
:::tip
上面获取异步路由的代码,在 /pages/login/Login.vue 文件中可以找到。
loadRoutes 方法会合并 /router/async/config.async.js 文件中配置的基本路由。
:::
:::details 点击查看 loadRoutes 的详细代码
```js
/**
* 加载路由
* @param routesConfig 路由配置
*/
function loadRoutes(routesConfig) {
// 如果 routesConfig 有值,则更新到本地,否则从本地获取
if (routesConfig) {
store.commit('accountModule/setRoutesConfig', routesConfig)
} else {
routesConfig = store.getters['accountModule/routesConfig']
}
// 如果开启了异步路由,则加载异步路由配置
const asyncRoutes = store.state.settingModule.asyncRoutes
if (asyncRoutes) {
if (routesConfig && routesConfig.length > 0) {
const routes = parseRoutes(routesConfig, routerMap)
formatAuthority(routes)
const finalRoutes = mergeRoutes(router.options.routes, routes)
router.options = {...router.options, routes: finalRoutes}
router.matcher = new Router({...router.options, routes:[]}).matcher
router.addRoutes(finalRoutes)
}
}
// 初始化Admin后台菜单数据
const rootRoute = router.options.routes.find(item => item.path === '/')
const menuRoutes = rootRoute && rootRoute.children
if (menuRoutes) {
mergeI18nFromRoutes(i18n, menuRoutes)
store.commit('settingModule/setMenuData', menuRoutes)
}
}
```
:::
## 异步加载菜单
Vue Antd Admin 的菜单,是根据路由配置自动生成的,默认获取根路由 `‘/’` 下所有子路由作为菜单配置。
当你完成了异步路由的加载,菜单也会随之改变,无需你做其它额外的操作。主要代码如下:
```js
// 初始化Admin后台菜单数据
const rootRoute = router.options.routes.find(item => item.path === '/')
const menuRoutes = rootRoute && rootRoute.children
if (menuRoutes) {
mergeI18nFromRoutes(i18n, menuRoutes)
store.commit('settingModule/setMenuData', menuRoutes)
}
```
:::tip
如果你不想从根路由 `‘/’` 下获取菜单数据,可以根据自己的需求更改。
:::
---
title: 权限管理
lang: zn-CN
---
# 权限管理
权限控制是中后台系统中常见的需求之一,你可以利用 Vue Antd Admin 提供的权限控制脚手架,实现一些基本的权限控制功能。
## 角色和权限
通常情况下有两种方式可以控制用户权限,一种是通过用户角色 role 来控制权限,另一种是通过更细致的权限 permission 来控制。
这两种方式 Vue Antd Admin 都支持。
我们定义了 role 和 permission 的基本格式,如果你获取的 role 和 permission 数据格式与 Vue Antd Admin 不一致,
你需要在获取到 role 和 permission 后将其转换为 Vue Antd Admin 的格式。
### 角色
Vue Antd Admin 的 `角色/role` 包含 `id``operation` 两个属性。其中 `id``角色/role` 的 id,`operation``角色/role` 具有的操作权限,是一个字符串数组。
```js
role = {
id: 'admin', //角色ID
operation: ['add', 'delete', 'edit', 'close'] //角色的操作权限
}
```
你也可以设置 role 的值为字符串,比如 role = 'admin', 它等同于:
```js
role = {
id: 'admin'
}
```
### 权限
Vue Antd Admin 的 `权限/permission` 也包含 `id``operation` 两个属性。其中 `id``权限/permission` 的 id,`operation``权限/permission` 下的操作权限,是一个字符串数组。
```js
permission = {
id: 'form', //权限ID
operation: ['add', 'delete', 'edit', 'close'] //权限下的操作权限
}
```
你也可以设置 role 的值为字符串,比如 permission = 'form', 它等同于:
```js
permission = {
id: 'form'
}
```
### 设置用户的角色和权限
你只需为用户配置 roles 和 permissions 两者中的其中一种,即可完成权限管理功能。当然你也可以两者都配置。
获取到用户权限或角色后,将其格式化转为 Vue Antd Admin 可用的格式,然后使用 `store.commit('accountModule/setPermissions', permissions)``store.commit('accountModule/setRoles', roles)`
将其存在本地即可。如下:
```js
getPermissions().then(res => {
const permissions = res.data
this.$store.commit('accountModule/setPermissions', permissions)
})
getRoles().then(res => {
const roles = res.data
this.$store.commit('accountModule/setRoles', roles)
})
```
:::tip
注意,存在本地的 permissions 和 roles 都应该是数组。
你可以在 /pages/login/Login.vue 查看完整的用户角色和权限设置代码。
:::
## 页面权限
如果你想给一些页面设置准入权限,只需要给该页面对应的路由设置元数据 authority 即可。 authority 的值可以是一个字符串,也可以是对象。
如下路由配置,则表明 `验权页面` 需要准入权限(permission): `form`
```js {5}
const route = {
name: '验权页面',
path: 'auth/demo',
meta: {
authority: 'form',
},
component: () => import('@/pages/demo')
}
```
下面是 authority 的值为对象的写法,这种写法和上面字符串的写法具有相同的效果:
```js {5-7}
const route = {
name: '验权页面',
path: 'auth/demo',
meta: {
authority: {
permission: 'form'
}
},
component: () => import('@/pages/demo')
}
```
有时你可能需要通过用户角色来配置页面权限,我们同样支持,用法和上面类似。
如下配置,表明 `验权页面` 需要准入角色(role) `admin`
```js {5-7}
const route = {
name: '验权页面',
path: 'auth/demo',
meta: {
authority: {
role: 'admin'
}
},
component: () => import('@/pages/demo')
}
```
:::tip
当你未设置 authority 或 设置 authority 的值 为 `*` 时,等同于该页面无需权限限制,我们会忽略此页面的权限检查。
:::
:::tip
当 authority 的值为字符串时,会以 [权限/permission](#权限) 验证权限。如果你需要以 [角色/role](#角色) 验证权限,请以对象形式设置 authority 的值。
:::
## 操作权限
在一些复杂的些场景下,权限可能不仅仅是页面层级这么简单。在一些页面你可能需要校验用户是否具有某些操作的权限,比如 增、删、改、查等。
为此,我们提供了 `权限校验注入``权限校验指令` 两个实用的功能。
### 权限校验注入
通过对Vue组件的实例方法进行 `权限校验注入`,我们可以控制该实例方法的执行权限,从而精准且安全的验证用户操作。
比如,QueryList 页面的 deleteRecord 方法,我们希望具有操作权限 `delete` 的用户才能调用此方法。
只需为 `deleteRecord` 方法注入权限校验,按如下方式配置 `authorize` 即可:
```vue {9-11,13}
<template>
...
</template>
<script>
...
export default {
name: 'QueryList',
data () {...},
authorize: { //权限校验注入设置
deleteRecord: 'delete' //key为需要注入权限校验的方法名,这里为 deleteRecord 方法;值为需要校验的操作权限,这里为 delete 操作权限
},
methods: {
deleteRecord(key) {
this.dataSource = this.dataSource.filter(item => item.key !== key)
this.selectedRows = this.selectedRows.filter(item => item.key !== key)
},
...
}
}
</script>
```
如果用户没有 `delete` 权限,调用 deleteRecord 方法,会看到如下提示:
![无此权限](../assets/permission.png)
### 操作权限校验的类型
`authorize` 会根据当前页面匹配到的权限类型([permission](#权限) / [role](#角色)),来判断是使用 `permission.operation` 还是 `role.operation` 来进行权限校验。
如果当前页面同时匹配到了 permission 和 role 权限,则默认通过 permission.operation 来进行操作权限校验。
当然你也可以指定操作权限校验的类型,如下设置即可:
```js {2-5}
authorize: {
deleteRecord: { //需要 注入权限校验 的方法名:deleteRecord
check: 'delete', //需要校验的操作权限:check
type: 'role' //指定操作权限校验的类型,可选 permission 和 role。这里指定以 role.operation 校验操作权限
}
}
```
### 权限校验指令
有时我们可能希望用户能够更直观的了解自己的操作权限。比如给没有操作权限的控件应用 disable 样式,禁用 click 事件等。
我们提供了权限校验指令 `v-auth` 来实现这个功能。
比如,我们想为 QueryList 页面的删除控件进行 `delete` 操作权限校验,只需为删除控件设置 v-auth="\`delete\`" 指令即可,如下:
```vue {6}
<template>
<a-card>...
<standard-table ...>
...
<div slot="action" slot-scope="{text, record}">
<a @click="deleteRecord(record.key)" v-auth="`delete`">
<a-icon type="delete" />删除
</a>
</div>
...
</standard-table>
</a-card>
</template>
```
假如用户没有 `delete` 操作权限,则控件会被应用 disable 样式,且 click 事件无效,如下图:
![权限校验指令](../assets/auth.png)
:::warning 重要!!!
v-auth 是我们自定义的一个 [Vue指令](https://cn.vuejs.org/v2/guide/custom-directive.html#ad)。因为 `Vue指令` 的值需要是一个 javascript 表达式,因此你不能直接给 v-auth 赋值为字符串,
需要把 v-auth 的字符串值用 ` `` ` 包裹起来,否则可能会报 undefined 错误。
:::
### 权限校验指令的类型
你同样也可以指定 v-auth 的权限校验类型,可选 [permission](#权限)[role](#角色)。它的校验方式和 [authorize](#权限校验注入) 类似,如未指定则会自动识别。
`v-auth:role` 表示通过 `role.operation` 进行校验,`v-auth:permission` 表示通过 `permission.operation` 进行校验。
如下,指定通过 `role.operation` 校验删除控件的操作权限:
```vue {3}
<div slot="action" slot-scope="{text, record}">
...
<a v-auth:role="`delete`">
<a-icon type="delete" />删除
</a>
...
</div>
```
## 异步路由权限
异步路由同样可以进行权限校验配置,它和正常的路由权限配置基本无异,只是无需把 [authority](#页面权限) 配置在元数据属性 meta 里。
你可以在路由组件注册时设置 authority,也可以在异步路由配置里设置 authority。
路由组件注册时设置 [authority](#页面权限)
```js {6}
// 路由组件注册
const routerMap = {
...
demo: {
name: '演示页',
authority: 'form',
component: () => import('@/pages/demo')
}
...
}
```
异步路由配置里设置 [authority](#页面权限)
```js {11-13}
const routesConfig = [{
router: 'root',
children: ['demo',
{router: 'parent1'...},
...
{
router: 'demo',
icon: 'file-ppt',
path: 'auth/demo',
name: '验权页面',
authority: {
permission: 'form',
}
}
]
}]
```
---
title: 图表
lang: zn-CN
---
# 图表
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页
---
title: 错误处理
lang: zn-CN
---
# 错误处理
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页
---
title: 路由守卫
lang: zn-CN
---
# 路由守卫
Vue Antd Admin 使用 vue-router 实现路由导航功能,因此可以为路由配置一些守卫。
我们统一把导航守卫配置在 router/guards.js 文件中。
## 前置守卫
Vue Antd Admin 为每个前置导航守卫函数注入 to,from,next,options 四个参数:
* `to: Route`: 即将要进入的目标[路由对象](https://router.vuejs.org/zh/api/#%E8%B7%AF%E7%94%B1%E5%AF%B9%E8%B1%A1)
* `from: Route`: 当前导航正要离开的路由对象
* `next: Function`: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。详情查看 [Vue Router #导航守卫](https://router.vuejs.org/zh/guide/advanced/navigation-guards.html)
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
如下,是登录拦截导航守卫的定义
```js
const loginGuard = (to, from, next, options) => {
const {message} = options
if (!loginIgnore.includes(to) && !checkAuthorization()) {
message.warning('登录已失效,请重新登录')
next({path: '/login'})
} else {
next()
}
}
```
## 后置守卫
你也可以定义后置导航守卫,Vue Antd Admin 为每个后置导航函数注入 to,from,options 三个参数:
* `to: Route`: 即将要进入的目标[路由对象](https://router.vuejs.org/zh/api/#%E8%B7%AF%E7%94%B1%E5%AF%B9%E8%B1%A1)
* `from: Route`: 当前导航正要离开的路由对象
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
如下,是一个后置导航守卫的定义
```js
const afterGuard = (to, from, options) => {
const {store, message} = options
// 做些什么
message.info('do something')
}
```
## 导出守卫配置
定义好导航守卫后,只需按照类别在 guard.js 中导出即可。分为两类,`前置守卫``后置守卫`。如下:
```js
export default {
beforeEach: [loginGuard, authorityGuard],
afterEach: [afterGuard]
}
```
:::details 点击查看完整的导航守卫配置
```js
import {loginIgnore} from '@/router/index'
import {checkAuthorization} from '@/utils/request'
/**
* 登录守卫
* @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['accountModule/permissions']
const roles = store.getters['accountModule/roles']
if (!hasAuthority(to, permissions, roles)) {
message.warning(`对不起,您无权访问页面: ${to.fullPath},请联系管理员`)
next({path: '/403'})
} else {
next()
}
}
/**
* 后置守卫
* @param to
* @param form
* @param options
*/
const afterGuard = (to, from, options) => {
const {store, message} = options
// 做些什么
message.info('do something')
}
export default {
beforeEach: [loginGuard, authorityGuard],
afterEach: [afterGuard]
}
```
:::
\ No newline at end of file
---
title: 国际化
lang: zn-CN
---
# 国际化
vue-antd-admin 采用 [vue-i18n](https://kazupon.github.io/vue-i18n/) 插件来实现国际化,该项目已经内置并且加载好了基础配置。可以直接上手使用。
> 如果你还没有看快速入门,请先移步查看: [页面 -> i18n国际化配置](../develop/page.html#i18n国际化配置)
## 菜单和路由
### 默认情况
如果你没有对菜单进行国际化配置,admin 默认会从路由数据中提取数据作为国际化配置。route.name 作为中文语言,route.path 作为英文语言。
国际化提取函数定义在 `@/utils/i18n.js` 文件中,会在路由加载时调用,如下:
```js
/**
* 从路由提取国际化数据
* @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])
})
}
```
### 自定义
如果你想自定义菜单国际化数据,可在 `@/router/i18n.js` 文件中配置。我们以路由的 path 作为 key(嵌套path 的写法也会被解析),name 作为 国际化语言的值。
假设你有一个路由的配置如下:
```js
[{
path: 'parent',
...
children: [{
path: 'self',
...
}]
}]
or
[{
path: 'other',
...
children: [{
path: '/parent/self', // 在国际化配置中 key 会解析为 parent.self
...
}]
}]
```
那么你需要在 `@/router/i18n.js` 中这样配置:
```jsx
messages: {
CN: {
parent: {
name: '父級菜單',
self: {name: '菜單名'},
},
US: {
parent: {
name: 'parent menu',
self: {name: 'menu name'},
},
HK: {
parent: {
name: '父級菜單',
self: {name: '菜單名'},
},
```
## 添加语言
首先在 `@/layouts/header/AdminHeader.vue` ,新增一门语言 (多个同理)。
```vue {15}
<template>
...
</template>
<script>
...
export default {
...
data() {
return {
langList: [
{key: 'CN', name: '简体中文', alias: '简体'},
{key: 'HK', name: '繁體中文', alias: '繁體'},
{key: 'US', name: 'English', alias: 'English'},
// 新增一个语言选项, key是i18n的索引,name是菜单显示名称
{key: 'JP', name: 'Japanese', alias: 'Japanese'}
],
searchActive: false
}
},
}
</script>
```
> TIP: 后续开发建议把这里改成动态配置的方式!
然后开始往 `@/router/i18n.js``@/pages/你的页面/i18n.js` 里面分别添加上语言的翻译。
```vue {12,13,14}
module.exports = {
messages: {
CN: {
home: {name: '首页'},
},
US: {
home: {name: 'home'},
},
HK: {
home: {name: '首頁'},
},
JP: {
home: {name: '最初のページ'},
},
}
}
```
> Notice: 更多用法请移步到 [vue-i18n](https://kazupon.github.io/vue-i18n/) 。
\ No newline at end of file
---
title: 拦截器配置
lang: zn-CN
---
# 拦截器配置
Vue Antd Admin 基于 aixos 封装了 http 通信功能,我们可以为 http 请求响应配置一些拦截器。拦截器统一配置在 /utils/axios-interceptors.js 文件中。
## 请求拦截器
你可以为每个请求拦截器配置 `onFulfilled``onRejected` 两个钩子函数。
### onFulfilled
我们会为 onFulfilled 钩子函数注入 config 和 options 两个参数:
* `config: AxiosRequestConfig`: axios 请求配置,详情参考 [axios 请求配置](http://www.axios-js.com/zh-cn/docs/#%E8%AF%B7%E6%B1%82%E9%85%8D%E7%BD%AE)
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
### onRejected
我们会为 onFulfilled 钩子函数注入 error 和 options 两个参数:
* `error: Error`: axios 请求错误对象
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
如下,为一个完整的请求拦截器配置:
```js
const tokenCheck = {
// 发送请求之前做些什么
onFulfilled(config, options) {
const {message} = options
const {url, xsrfCookieName} = config
if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {
message.warning('认证 token 已过期,请重新登录')
}
return config
},
// 请求出错时做点什么
onRejected(error, options) {
const {message} = options
message.error(error.message)
return Promise.reject(error)
}
}
```
## 响应拦截器
响应拦截器也同样可以配置 `onFulfilled``onRejected` 两个钩子函数。
### onFulfilled
我们会为 onFulfilled 钩子函数注入 response 和 options 两个参数:
* `response: AxiosResponse`: axios 响应对象,详情参考 [axios 响应对象](http://www.axios-js.com/zh-cn/docs/#%E5%93%8D%E5%BA%94%E7%BB%93%E6%9E%84)
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
### onRejected
我们会为 onFulfilled 钩子函数注入 error 和 options 两个参数:
* `error: Error`: axios 请求错误对象
* `options: Object`: 应用配置,包含: {router, i18n, store, message},可根据需要扩展。
如下,为一个完整的响应拦截器配置:
```js
const resp401 = {
// 响应数据之前做点什么
onFulfilled(response, options) {
const {message} = options
if (response.status === 401) {
message.error('无此接口权限')
}
return response
},
// 响应出错时做点什么
onRejected(error, options) {
const {message} = options
if (response.status === 401) {
message.error('无此接口权限')
}
return Promise.reject(error)
}
}
```
## 导出拦截器
定义好拦截器后,只需在 axios-interceptors.js 文件中导出即可。分为两类,`请求拦截器``响应拦截器`。如下:
```js
export default {
request: [tokenCheck], // 请求拦截
response: [resp401] // 响应拦截
}
```
:::details 点击查看完整的拦截器配置示例
```js
import Cookie from 'js-cookie'
// 401拦截
const resp401 = {
onFulfilled(response, options) {
const {message} = options
if (response.status === 401) {
message.error('无此接口权限')
}
return response
},
onRejected(error, options) {
const {message} = options
message.error(error.message)
return Promise.reject(error)
}
}
const resp403 = {
onFulfilled(response, options) {
const {message} = options
if (response.status === 403) {
message.error(`请求被拒绝`)
}
return response
}
}
const reqCommon = {
onFulfilled(config, options) {
const {message} = options
const {url, xsrfCookieName} = config
if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {
message.warning('认证 token 已过期,请重新登录')
}
return config
},
onRejected(error, options) {
const {message} = options
message.error(error.message)
return Promise.reject(error)
}
}
export default {
request: [reqCommon], // 请求拦截
response: [resp401, resp403] // 响应拦截
}
```
:::
\ No newline at end of file
---
title: 登录认证
lang: zn-CN
---
# 登录认证
Vue Antd Admin 使用 js-cookie.js 管理用户的 token,结合 axios 配置,可以为每个请求头加上 token 信息。
## token名称
后端系统通常会从请求 header 中获取用户的 token,因此我们需要配置好 token 名称,好让后端能正确的识别到用户 token。
Vue Antd Admin 默认token 名称为 `Authorization`,你可以在 /utils/request.js 中修改它。
```js{5}
import axios from 'axios'
import Cookie from 'js-cookie'
// 跨域认证信息 header 名
const xsrfHeaderName = 'Authorization'
...
```
## token 设置
调用登录接口后拿到用户的 token 和 token 过期时间(如无过期时间,可忽略),并使用 /utils/request.js #setAuthorization 方法保存token。
```js{5}
import {setAuthorization} from '@/utils/request'
login(name, password).then(res => {
const {token, expireAt} = res.data
setAuthorization({token, expireAt: new Date(expireAt)})
})
```
## token 校验
Vue Antd Admin 默认添加了登录导航守卫,如检查到本地cookie 中不包含 token 信息,则会拦截跳转至登录页。你可以在 /router/index.js 中配置
不需要登录拦截的路由
```js
// 不需要登录拦截的路由配置
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)
}
}
```
或者在 /router/guards.js 中移出登录守卫
```diff
...
export default {
- beforeEach: [loginGuard, authorityGuard, redirectGuard],
+ beforeEach: [authorityGuard, redirectGuard],
afterEach: []
}
```
## Api
### setAuthorization(auth, authType)
来源:/utils/request.js
该方法用于保存用户 token,接收两个参数:
* **auth**
认证信息,包含 token、expireAt 等认证数据。
* **authType**
认证类型,默认为 `AUTH_TYPE.BEARER`(AUTH_TYPE.BEARER 默认会给token 加上 Bearer 识别前缀),可根据自己的认证类型自行扩展。
### checkAuthorization(authType)
该方法用于校验用户 token 是否过期,接收一个参数:
* **authType**
认证类型,默认为 `AUTH_TYPE.BEARER`
### removeAuthorization(authType)
该方法用于移出用户本地存储的 token,接收一个参数:
* **authType**
认证类型,默认为 `AUTH_TYPE.BEARER`
:::tip
以上 Api 均可在 /utils/request.js 文件中找到。
:::
\ No newline at end of file
---
title: 108个小技巧
lang: zn-CN
---
# 108个小技巧
## 自定义菜单icon
## 隐藏页面标题
## 关闭页签API
## 权限校验PI
---
title: 更换主题
lang: zn-CN
---
# 更换主题
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页
---
title: 开发
lang: zh-CN
---
# 开发
---
title: 布局
lang: zh-CN
---
# 布局
页面整体布局是一个产品最外层的框架结构,往往会包含导航、页脚、侧边栏、通知栏以及内容等。在页面之中,也有很多区块的布局结构。在真实项目中,页面布局通常统领整个应用的界面,有非常重要的作用。
## Admin 的布局
在 Vue Antd Admin 中,我们抽离了使用过程中一些常用的布局,都放在 layouts 目录中,分别为:
* [AdminLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/AdminLayout.vue) / **管理后台布局**,包含了头部导航,侧边导航、内容区和页脚,一般用于后台系统的整体布局
![admin-layout](../assets/admin-layout.png)
* [PageLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue) / **页面布局**,包含了页头和内容区,常用于需要页头(包含面包屑、标题、额外操作等)的页面
![page-layout](../assets/page-layout.png)
* [CommonLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/CommonLayout.vue) / **通用布局**,仅包含内容区和页脚的简单布局,项目中常用于注册、登录或展示页面
![common-layout](../assets/common-layout.png)
## Admin 的视图
在 Vue Antd Admin 中,除了基本布局外,通常有很多页面的结构是相似的。因此,我们把这部分结构抽离为视图组件。
一个视图组件通常包含一个基本布局组件、视图公共区块、路由视图内容区、页脚等,常常结合路由配置使用。它们也被放入了 layouts 目录中,分别为:
* [TabsView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/TabsView.vue) / **多页签视图**,包含了 AdminLayout 布局、多页签头和路由视图内容区
![tabs-view](../assets/tabs-view.png)
* [PageView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageView.vue) / **页面视图**,包含了 PageLayout 布局和路由视图内容区
![page-view](../assets/page-view.png)
* [BlankView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/BlankView.vue) / **空白视图**,仅包含一个路由视图内容区
![blank-view](../assets/blank-view.png)
## 如何使用
通常我们会把视图组件和路由配置结合一起使用,我们把配置信息抽离在路由配置文件中 [src/router/config.js](https://github.com/iczer/vue-antd-admin/blob/master/src/router/config.js) 。如下:
```jsx {7,12}
{
path: 'form',
name: '表单页',
meta: {
icon: 'form',
},
component: PageView,
children: [
{
path: 'basic',
name: '基础表单',
component: () => import('@/pages/form/basic/BasicForm'),
}
]
}
```
当然,如果这满足不了你的需求,你也可以自定义一些视图组件,或者直接在页面组件中使用布局。参考
[workplace](https://github.com/iczer/vue-antd-admin/blob/master/src/pages/dashboard/workplace/WorkPlace.vue) 页面:
```vue {2,13}
<template>
<page-layout :avatar="currUser.avatar">
<div slot="headerContent">
<div class="title">{{$t('timeFix')}},{{currUser.name}},{{$t('welcome')}}</div>
<div>{{$t('position')}}</div>
</div>
<template slot="extra">
<head-info class="split-right" :title="$t('project')" content="56"/>
<head-info class="split-right" :title="$t('ranking')" content="8/24"/>
<head-info class="split-right" :title="$t('visit')" content="2,223"/>
</template>
<div>...</div>
</page-layout>
</template>
```
## 其它布局组件
除了 Admin 里的内建布局以外,在一些页面中需要进行布局,还可以使用 Ant Design Vue 提供的布局组件:Grid 和 Layout。
### Grid 组件
栅格布局是网页中最常用的布局,其特点就是按照一定比例划分页面,能够随着屏幕的变化依旧保持比例,从而具有弹性布局的特点。
而 Ant Design Vue 的栅格组件提供的功能更为强大,能够设置间距、具有支持响应式的比例设置,以及支持 flex 模式,基本上涵盖了大部分的布局场景,详情查看:[Grid](https://www.antdv.com/components/grid-cn/)
### Layout 组件
如果你需要辅助页面框架级别的布局设计,那么 Layout 则是你最佳的选择,它抽象了大部分框架布局结构,使得只需要填空就可以开发规范专业的页面整体布局,详情查看:[Layout](https://www.antdv.com/components/layout-cn/)
### 根据不同场景区分抽离布局组件
在大部分场景下,我们需要基于上面两个组件封装一些适用于当下具体业务的组件,包含了通用的导航、侧边栏、顶部通知、页面标题等元素。例如 Vue Antd Admin 的 [AdminLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/AdminLayout.vue)
通常,我们会把抽象出来的布局组件,放到 layouts 文件夹中方便管理。需要注意的是,这些布局组件和我们平时使用的其它组件并没有什么不同,只不过功能性上是为了处理布局问题而单独归类。
---
title: Mock
lang: zh-CN
---
# Mock
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页
---
title: 页面
lang: zh-CN
---
# 页面
这里的『页面』包含新建页面文件,配置路由、样式文件及i18n国际化等。通常情况下,你仅需简单的配置就可以添加一个新的页面。
## 新建页面文件
在 src/pages 下创建新的 .vue 文件。如果页面相关文件过多,您可以创建一个文件夹来放置这些文件。
```diff
├── public
├── src
│ ├── assets # 本地静态资源
: :
│ ├── pages # 页面组件和通用模板
+ │ │ └── NewPage.vue # 新页面文件
or
+ │ │ └── newPage # 为新页面创建一个文件夹
+ │ │ ├── NewPage.vue # 新页面文件
+ │ │ ├── index.less # 页面样式文件
+ │ │ └── index.js # import 引导文件
: :
│ └── main.js # 应用入口js
├── package.json # package.json
├── README.md # README.md
└── vue.config.js # vue 配置文件
```
为了更好地演示,我们初始化 NewPage.vue 文件如下:
```vue
<template>
<div class="new-page" :style="`min-height: ${pageMinHeight}px`">
<h1>演示页面</h1>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'NewPage',
data() {
return {
desc: '这是一个演示页面'
}
},
computed: {
...mapState('setting', ['pageMinHeight']),
}
}
</script>
<style scoped lang="less">
@import "index.less";
</style>
```
index.less 文件:
```less
.new-page{
height: 100%;
background-color: @base-bg-color;
text-align: center;
padding: 200px 0 0 0;
margin-top: -24px;
h1{
font-size: 48px;
}
}
```
index.js 文件:
```js
import NewPage from './NewPage'
export default NewPage
```
## 配置路由
路由配置在 src/router/config.js 文件中,我们把上面创建的页面文件加入路由配置中
```js {10-14}
const options = {
routes: [
{name: '登录页'...},
{
path: '/',
name: '首页',
component: TabsView,
redirect: '/login',
children: [
{
path: 'newPage',
name: '新页面',
component: () => import('@/pages/newPage'),
},
{
path: 'dashboard',
name: 'Dashboard',
meta: {
icon: 'dashboard'
},
component: BlankView,
children: [...]
}
]
...
}
]
}
```
:::tip
我们建议使用英文设置路由的 path 属性,用中文设置路由的 name 属性。因为系统将自动提取路由的 path 和 name 属性作为国际化配置。这在后面的章节
[进阶>国际化](../advance/i18n.md)中将会讲到。
当然,如果你的项目不需要国际化,可以忽略。
:::
启动服务,你将看到新增页面如下:
![newPage](../assets/new-page.png)
如果你想把它配置为二级页面或更深层级的页面,只需为它配置一个父级路由,并为父级路由配置一个[视图组件](./layout.md#admin-的视图)
这里我们选择 [PageView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageView.vue),如下:
```js {10-21}
const options = {
routes: [
{name: '登录页'...},
{
path: '/',
name: '首页',
component: TabsView,
redirect: '/login',
children: [
{
path: 'parent',
name: '父级路由',
component: PageView,
children: [
{
path: 'newPage',
name: '新页面',
component: () => import('@/pages/newPage'),
}
]
},
{name: 'dashboard'...}
]
...
}
]
}
```
:::warning
页面所有父级路由的组件必须配置为[视图组件](../develop/layout.md#admin-的视图),否则页面的内容可能不会显示。
目前有 [PageView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageView.vue)
[TabsView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/tabs/TabsView.vue)
[BlankView](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/BlankView.vue) 可选,
你也可以自己创建视图组件。([什么是视图组件?](../develop/layout.md#admin-的视图)
:::
页面如下:
![newPage2](../assets/new-page-2.png)
## i18n国际化配置
如果你想为页面增加i18n国际化配置,只需在页面同级文件夹下创建 i18n.js 文件,然后在页面文件中引入并使用即可。
创建 i18n.js 文件:
```diff {9}
├── public
├── src
│ ├── assets # 本地静态资源
: :
│ ├── pages # 页面组件和通用模板
│ │ └── newPage # 为新页面创建一个文件夹
│ │ ├── NewPage.vue # 新页面文件
│ │ ├── index.less # 页面样式文件
+ │ │ ├── i18n.js # i18n 国际化配置文件
│ │ └── index.js # import 引导文件
: :
│ └── main.js # 应用入口js
├── package.json # package.json
├── README.md # README.md
└── vue.config.js # vue 配置文件
```
i18n.js 文件内容:
```js
module.exports = {
messages: {
CN: {
content: '演示页面',
description: '这是一个演示页面'
},
HK: {
content: '演示頁面',
description: '這是一個演示頁面'
},
US: {
content: 'Demo Page',
description: 'This is a demo page'
}
}
}
```
在 NewPage.vue 文件中引入 i18n.js,并添加需要国际化的内容。如下修改:
```vue {3,10,13-15}
<template>
<div class="new-page" :style="`min-height: ${pageMinHeight}px`">
<h1>{{$t('content')}}</h1>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'NewPage',
i18n: require('./i18n'),
computed: {
...mapState('setting', ['pageMinHeight']),
desc() {
return this.$t('description')
}
}
}
</script>
<style scoped lang="less">
@import "index";
</style>
```
然后页面右上角语言项选择 ``English``,你会发现,页面语言切换为英文了。如下:
![newPageUs](../assets/new-page-us.png)
一切就是这么的简单!
:::tip
如果你尝试切换为繁体语言,可能会发现``页面标题````面包屑``显示为英文。
这涉及到路由的国际化配置,在章节 [进阶 > 国际化](../advance/i18n.md) 中,我们会对此作详细讲解。
:::
---
title: 路由和菜单
lang: zh-CN
---
# 路由和菜单
路由和菜单起到组织一个应用的关键骨架的作用,Vue Antd Admin 使用 [vue-router](https://router.vuejs.org/zh/) 来配置和管理我们的路由和菜单。
## 基本结构
得益于 vue-router 路由配置的可扩展性,Vue Antd Admin 通过结合 router 配置文件、基本算法及 [menu.js](https://github.com/iczer/vue-antd-admin/blob/master/src/components/menu/menu.js) 菜单生成工具,搭建了路由和菜单的基本框架,主要涉及以下几个模块/功能:
|功能 |配置 |
|:----------|:-------------------------------|
|*路由管理* |通过 [vue-router](https://router.vuejs.org/zh/) 的路由规则进行管理和配置|
|*菜单生成* |根据路由配置自动生成菜单,菜单项名称、图标和层级等全部可以通过路由配置进行自定义|
|*面包屑* |布局组件 [PageLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue) 提取当前页面路由,并根据当前路由层次关系自动生成面包屑,当然你也可以自定义面包屑|
|*页面标题* |同面包屑,布局组件 [PageLayout](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue) 根据提取到的当前页面的路由名称设置为页面标题,你也同样可以自定义标题|
## 路由
Vue Antd Admin 的路由配置完全遵循 vue-router 的 [routes 配置规则](https://router.vuejs.org/zh/api/#routes)
另外我们还在 routes 的元数据属性 [meta](https://router.vuejs.org/zh/guide/advanced/meta.html#%E8%B7%AF%E7%94%B1%E5%85%83%E4%BF%A1%E6%81%AF) 中注入了三个属性 icon、invisible 和 page,它们将在生成菜单和页头时发挥作用。配置示例如下:
```js {7,13}
const options = {
routes: [{
path: '/',
name: '首页',
component: TabsView,
meta: {
invisible: true
},
children: [{
path: 'dashboard',
name: 'Dashboard',
meta: {
icon: 'dashboard'
},
component: BlankView,
children: [{
path: 'workplace',
name: '工作台',
component: () => import('@/pages/dashboard/workplace/WorkPlace'),
}, {
path: 'analysis',
name: '分析页',
component: () => import('@/pages/dashboard/analysis/Analysis'),
}]
}]
}]
}
```
完整配置示例,请查看 [src/router/config.js](https://github.com/iczer/vue-antd-admin/blob/master/src/router/config.js)
## 菜单
Admin 系统的菜单直接通过路由配置生成,路由属性和菜单功能对应关系如下
|路由属性|对应菜单功能|
|:-----------------|:-------|
|**name** |菜单名称 |
|**path** |点击菜单时的跳转链接|
|**meta.icon** |菜单图标,图标使用 ant-design-vue 图标库,对应 [Icon](https://www.antdv.com/components/icon-cn/#API) 组件 的 type 属性|
|**meta.invisible**|是否不将此路由项渲染为菜单项,默认false;如设置为 true,则生成菜单时将忽略此路由|
假如使用上面 [路由](#路由) 文档中的 [配置示例](#路由),将会生成如下菜单:
![menu-demo](../assets/menu-demo.png)
实际项目中,我们是在 AdminLayout 组件创建之前,提取 router 配置中根路由 '/' 下所有子路由配置,
并将此配置传递给 menu.js 插件,从而生成菜单。如下:
```vue {4,12,13,14}
<template>
<a-layout :class="['admin-layout'...]">
...
<side-menu :menuData="menuData".../>
</a-layout>
</template>
<script>
import ...
export default {
name: 'AdminLayout',
...
beforeCreate () {
menuData = this.$router.options.routes.find((item) => item.path === '/').children
}
}
</script>
```
详细代码可查看 [layouts/AdminLayout#L83](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/AdminLayout.vue#L83)
当然你也可以不使用 router 配置生成菜单,你只需按照配置规则给菜单传递你所定义配置即可。菜单组件配置规则如下:
```jsx {}
[{
name: '菜单标题',
path: '菜单路由',
meta: {
icon: '菜单图标',
invisible: 'boolean, 是否隐藏此菜单项, 默认 false',
},
children: [ //子菜单配置
{
name: '子菜单标题',
path: '子菜单路由',
meta: {
icon: '子菜单图标',
invisible: 'boolean, 是否隐藏此菜单项, 默认 false',
},
}
]
}]
```
更多细节可查看 [components/menu/menu.js](https://github.com/iczer/vue-antd-admin/blob/master/src/components/menu/menu.js)
## 面包屑
面包屑由 [PageHeader](https://github.com/iczer/vue-antd-admin/blob/master/src/components/page/PageHeader.vue) 实现,PageLayout 组件会从当前页面路由提取面包屑配置(如未设置,则根据当前路由层次关系生成面包屑)。所以只要页面中使用了 PageLayout 布局或者它的父级组件使用了 PageLayout 布局,面包屑都将自动生成。
当然,如果你想在某个页面自定义面包屑,只需在对应的路由元数据 meta 中定义 page.breadcrumb 属性即可。Vue Antd Admin 将会优先使用路由元数据 meta 中定义的面包屑配置。
比如,想自定义工作台页面面包屑,可以在工作台的 route 配置中如下设置:
```jsx {5,6,7}
{
path: 'workplace',
name: '工作台',
meta: {
page: {
breadcrumb: ['首页', 'Dashboard', '自定义']
}
},
component: () => import('@/pages/dashboard/workplace/WorkPlace'),
}
```
更多细节可查看 [layouts/PageLayout.vue#L55](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue#L55)
## 页面标题
页面标题的实现方式与面包屑基本一致,也是由 PageLayout 组件从当前页面路由提取标题(如未设置,则提取当前路由名称作为标题)。
如果你想自定义页面标题,在页面对应的路由元数据 meta 中定义 page.title 属性即可,如下示例,定义了工作台页面的标题:
```jsx {5,6,7}
{
path: 'workplace',
name: '工作台',
meta: {
page: {
title: '自定义标题'
}
},
component: () => import('@/pages/dashboard/workplace/WorkPlace'),
}
```
更多细节可查看 [layouts/PageLayout.vue#L48](https://github.com/iczer/vue-antd-admin/blob/master/src/layouts/PageLayout.vue#L48)
---
title: 服务端交互
lang: zh-CN
---
# 服务端交互
数据服务是一个应用的灵魂,它驱动着应用的各个功能模块的正常运转。Vue Antd Admin 在 service 模块封装了服务端交互,通过 API 的形式可以和任何技术栈的服务端应用一起工作。
## 服务交互流程
在 Vue Antd Admin 中,服务端交互流程如下:
* 组件内调用 service 服务 API
* service 服务 API 封装请求数据,通过 request.js 发送请求
* 组件获取 service 返回的数据,更新视图数据或触发其它行为
我们以登录为例,Login.vue 组件内,用户输入账号密码,点击登录,调用 services/user/login api
```vue {5,17}
<template>
...
</template>
<script>
import {login} from '@/services/user'
...
export default {
name: 'Login',
methods: {
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(res => this.afterLogin(res))
}
})
}
}
}
</script>
```
`services/user/login` 封装账户密码数据,通过 `request.js` 发送登录服务请求
```js
import {request, METHOD} from '@/utils/request'
/**
* 登录服务
* @param name 账户名
* @param password 账户密码
* @returns {Promise<AxiosResponse<T>>}
*/
async function login(name, password) {
return request(LOGIN, METHOD.POST, {
name: name,
password: password
})
}
```
Login.vue 获取登录服务返回的数据,进行后续操作
```vue {14,18-23}
<template>
...
</template>
<script>
import {login} from '@/services/user'
...
export default {
name: 'Login',
methods: {
onSubmit (e) {
this.form.validateFields((err) => {
if (!err) {
...
login(name, password).then(res => this.afterLogin(res))
}
})
},
afterLogin(res) {
if (res.data.code >= 0) { //登录成功
...
} else { //登录失败
this.error = loginRes.message
}
}
}
}
</script>
```
## 服务模块结构
服务模块结构如下:
```bash
...
├── src
│ └── services # 数据服务模块
│ ├── user.js # 用户数据服务
│ ├── product.js # 产品服务
│ ...
│ ├── api.js # api 地址管理
│ └── index.js # 服务模块导出
...
│ └── utils # 数据服务模块
│ ├── request.js # 基于 axios 的 http 请求工具
...
```
services 文件夹下, api.js 用于服务请求地址的统一管理,index.js 用于模块化导出服务,其它 *.js 文件对应各个服务模块。
## request.js
request.js 基于 axios 封装了一些常用的函数,如下:
```js
export {
METHOD, //http method 常量
AUTH_TYPE, //凭证认证类型 常量
request, //http请求函数
setAuthorization, //设置身份凭证函数
removeAuthorization, //移除身份凭证函数
checkAuthorization //检查身份凭证是否过期函数
}
```
:::tip
凭证认证类型默认为 [Bearer](https://www.jianshu.com/p/8f7009456abc),你可以根据自己的需要实现其它类型的认证
:::
## Base url 配置
你可以在项目根目录下的环境变量文件(.env 和 .env.development)中配置你的 API 服务 base url 地址。
生产环境,.env 文件
```properties
VUE_APP_API_BASE_URL=https://www.server.com
```
开发环境,.env.development 文件:
```properties
VUE_APP_API_BASE_URL=https://localhost:8000
```
## 跨域设置
在开发环境中,通常我们的Vue应用和服务应用运行在不同的地址或端口上。我们可以通过简单的设置,代理前端请求,来避免跨域问题。如下:
首先,在 services/api.js 文件中设置 API_PROXY_PREFIX 常量,BASE_URL 像下面这样设置:
```js {2,4}
//跨域代理前缀
const API_PROXY_PREFIX='/api'
//base url
const BASE_URL = process.env.NODE_ENV === 'production' ? process.env.VUE_APP_API_BASE_URL : API_PROXY_PREFIX
//导出api服务地址
module.exports = {
LOGIN: `${BASE_URL}/login`,
ROUTES: `${BASE_URL}/routes`
}
```
然后,在 vue.config.js 文件中配置代理:
```js
model.exports = {
devServer: {
proxy: {
'/api': { //此处要与 /services/api.js 中的 API_PROXY_PREFIX 值保持一致
target: process.env.VUE_APP_API_BASE_URL,
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
```
:::tip
此代理配置仅适用于开发环境,生产环境的跨域代理请在自己的web服务器配置。
:::
---
title: 主题定制
lang: zh-CN
---
# 主题定制
## 主题颜色
### 主题色
我们内置了一个色盘供您选择
<color color="#fa541c"/>
<color color="#fadb14"/>
<color color="#3eaf7c"/>
<color color="#13c2c2"/>
<color color="#1890ff"/>
<color color="#722ed1"/>
<color color="#eb2f96"/>
如果这不能满足你的需求,你也可以使用任何你喜欢的颜色,只需要在 src/config/config.js 文件中配置你的主题色即可。如:
```js {3}
module.exports = {
theme: {
color: '#13c2c2', //换成任何你喜欢的颜色,支持 hex 色值
mode: 'night'
},
multiPage: true,
animate: {
name: 'roll',
direction: 'default'
}
}
```
当你设置好主题色后,系统会根据这个主题色为你生成一系列配套的颜色,并应用到vue组件中。
:::tip
你可以在你的样式文件中直接使用 less 变量 ``@theme-color``
:::
:::warning
主题色目前只支持 ``hex`` 模式的色值。如果设置为 ``rgb`` 或其它模式的色值,可能会导致配套颜色无法生成。
:::
### 功能色
除了主题色,系统还有一些功能性颜色,分别为:成功色、警告色和错误色。默认色值分别为:
|名称|success |warning |error |
|:-:|:--------:|:-------:|:-----:|
|色值|``#52c41a``|``#faad14``|``#f5222d``|
|颜色|<color color="#52c41a"/>|<color color="#faad14"/>|<color color="#f5222d" />|
|less变量|@success-color|@warning-color|@error-color|
你也可以在 src/config/config.js 重新定义这些功能色
```js {5-7}
module.exports = {
theme: {
color: '#13c2c2',
mode: 'night',
success: '#52c41a', //定义成功色,支持 hex 色值
warning: '#faad14', //定义警告色,支持 hex 色值
error: '#f5222d' //定义错误色,支持 hex 色值
},
multiPage: true,
animate: {
name: 'roll',
direction: 'default'
}
}
```
:::tip
想在在你的样式文件中使用以上各功能色,引用各功能色对应的 less 变量即可。
:::
:::warning
功能色目前也只支持 ``hex`` 模式的色值。如果设置为 ``rgb`` 或其它模式的色值,可能会导致配套颜色无法生成。
:::
### 文本色
<table style="text-align: center" >
<tr>
<th>主题模式</th>
<th>标题色</th>
<th>文本色</th>
<th>次级文本色</th>
</tr>
<tr>
<td rowspan="2">light/dark</td>
<td><color color="rgba(0,0,0,0.85)"/></td>
<td><color color="rgba(0,0,0,0.65)"/></td>
<td><color color="rgba(0,0,0,0.45)"/></td>
</tr>
<tr>
<td><code>rgba(0,0,0,0.85)</code></td>
<td><code>rgba(0,0,0,0.65)</code></td>
<td><code>rgba(0,0,0,0.45)</code></td>
</tr>
<tr>
<td rowspan="2">night</td>
<td><color color="rgba(255,255,255,0.85)"/></td>
<td><color color="rgba(255,255,255,0.65)"/></td>
<td><color color="rgba(255,255,255,0.45)"/></td>
</tr>
<tr>
<td><code>rgba(255,255,255,0.85)</code></td>
<td><code>rgba(255,255,255,0.65)</code></td>
<td><code>rgba(255,255,255,0.45)</code></td>
</tr>
<tr>
<td>less变量</td>
<td>@title-color</td>
<td>@text-color</td>
<td>@text-color-second</td>
</tr>
</table>
:::tip
想在在你的样式文件中使用以上文本色,引用各文本色对应的 less 变量即可。
:::
:::warning
目前不支持自定义文本色,因为涉及到主题模式切换时文本色的置换问题。如强行修改,可能会导致主题模式切换时出现样式异常。
如果你的项目不需要主题模式切换,可自行替换以上文本色。
:::
### 背景色
<table style="text-align: center">
<tr>
<th>主题模式</th>
<th>布局背景色</th>
<th>基础背景色</th>
<th>hover背景色</th>
<th>边框颜色</th>
<th>阴影颜色</th>
</tr>
<tr>
<td rowspan="2">light/dark</td>
<td><color color="#f0f2f5"/></td>
<td><color color="#fff"/></td>
<td><color color="rgba(0,0,0,0.025)"/></td>
<td><color color="#f0f0f0"/></td>
<td><color color="rgba(0,0,0,0.15)"/></td>
</tr>
<tr>
<td><code>#f0f2f5</code></td>
<td><code>#fff</code></td>
<td><code>rgba(0,0,0,0.025)</code></td>
<td><code>#f0f0f0</code></td>
<td><code>rgba(0,0,0,0.15)</code></td>
</tr>
<tr>
<td rowspan="2">night</td>
<td><color color="#000"/></td>
<td><color color="#141414"/></td>
<td><color color="rgba(255,255,255,0.025)"/></td>
<td><color color="#303030"/></td>
<td><color color="rgba(255,255,255,0.15)"/></td>
</tr>
<tr>
<td><code>#000</code></td>
<td><code>#141414</code></td>
<td><code>rgba(255,255,255,0.025)</code></td>
<td><code>#303030</code></td>
<td><code>rgba(255,255,255,0.15)</code></td>
</tr>
<tr>
<td>less变量</td>
<td>@layout-bg-color</td>
<td>@base-bg-color</td>
<td>@hover-bg-color</td>
<td>@border-color</td>
<td>@shadow-color</td>
</tr>
</table>
:::tip
想在在你的样式文件中使用以上背景色,引用各背景色对应的 less 变量即可。
:::
:::warning
目前也不支持自定义背景色,因为涉及到主题模式切换时背景色的置换问题。如强行修改,可能会导致主题模式切换时出现样式异常。
如果你的项目不需要主题模式切换,可自行替换以上背景色。
:::
### antd 的色系
除了以上颜色,我们还引入了 ant-design 内置的色系。如下:
<table style="text-align: center">
<tr>
<th>色系</th>
<th>类型</th>
<th>颜色</th>
</tr>
<tr>
<td rowspan="2">blue/拂晓蓝</td>
<td>色盘</td>
<td >
<color-list
:colors="['#e6f7ff', '#bae7ff', '#91d5ff', '#69c0ff', '#40a9ff', '#1890ff', '#096dd9', '#0050b3', '#003a8c', '#002766']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@blue-1</code>
<code>@blue-2</code>
<code>...</code>
<code>@blue-10</code>
</td>
</tr>
<tr>
<td rowspan="2">purple/酱紫</td>
<td>色盘</td>
<td>
<color-list
:colors="['#f9f0ff', '#efdbff', '#d3adf7', '#b37feb', '#9254de', '#722ed1', '#531dab', '#391085', '#22075e', '#120338']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@purple-1</code>
<code>@purple-2</code>
<code>...</code>
<code>@purple-10</code>
</td>
</tr>
<tr>
<td rowspan="2">cyan/明青</td>
<td>色盘</td>
<td>
<color-list
:colors="['#e6fffb', '#b5f5ec', '#87e8de', '#5cdbd3', '#36cfc9', '#13c2c2', '#08979c', '#006d75', '#00474f', '#002329']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@cyan-1</code>
<code>@cyan-2</code>
<code>...</code>
<code>@cyan-10</code>
</td>
</tr>
<tr>
<td rowspan="2">green/极光绿</td>
<td>色盘</td>
<td>
<color-list
:colors="['#f6ffed', '#d9f7be', '#b7eb8f', '#95de64', '#73d13d', '#52c41a', '#389e0d', '#237804', '#135200', '#092b00']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@green-1</code>
<code>@green-2</code>
<code>...</code>
<code>@green-10</code>
</td>
</tr>
<tr>
<td rowspan="2">magenta/法式洋红</td>
<td>色盘</td>
<td>
<color-list
:colors="['#fff0f6', '#ffd6e7', '#ffadd2', '#ff85c0', '#f759ab', '#eb2f96', '#c41d7f', '#9e1068', '#780650', '#520339']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@magenta-1</code>
<code>@magenta-2</code>
<code>...</code>
<code>@magenta-10</code>
</td>
</tr>
<tr>
<td rowspan="2">red/薄暮</td>
<td>色盘</td>
<td>
<color-list
:colors="['#fff1f0', '#ffccc7', '#ffa39e', '#ff7875', '#ff4d4f', '#f5222d', '#cf1322', '#a8071a', '#820014', '#5c0011']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@red-1</code>
<code>@red-2</code>
<code>...</code>
<code>@red-10</code>
</td>
</tr>
<tr>
<td rowspan="2">orange/日暮</td>
<td>色盘</td>
<td>
<color-list
:colors="['#fff7e6', '#ffe7ba', '#ffd591', '#ffc069', '#ffa940', '#fa8c16', '#d46b08', '#ad4e00', '#873800', '#612500']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@orange-1</code>
<code>@orange-2</code>
<code>...</code>
<code>@orange-10</code>
</td>
</tr>
<tr>
<td rowspan="2">yellow/日出</td>
<td>色盘</td>
<td>
<color-list
:colors="['#feffe6', '#ffffb8', '#fffb8f', '#fff566', '#ffec3d', '#fadb14', '#d4b106', '#ad8b00', '#876800', '#614700']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@yellow-1</code>
<code>@yellow-2</code>
<code>...</code>
<code>@yellow-10</code>
</td>
</tr>
<tr>
<td rowspan="2">volcano/火山</td>
<td>色盘</td>
<td>
<color-list
:colors="['#fff2e8', '#ffd8bf', '#ffbb96', '#ff9c6e', '#ff7a45', '#fa541c', '#d4380d', '#ad2102', '#871400', '#610b00']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@volcano-1</code>
<code>@volcano-2</code>
<code>...</code>
<code>@volcano-10</code>
</td>
</tr>
<tr>
<td rowspan="2">geekblue/极客蓝</td>
<td>色盘</td>
<td>
<color-list
:colors="['#f0f5ff', '#d6e4ff', '#adc6ff', '#85a5ff', '#597ef7', '#2f54eb', '#1d39c4', '#10239e', '#061178', '#030852']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@geekblue-1</code>
<code>@geekblue-2</code>
<code>...</code>
<code>@geekblue-10</code>
</td>
</tr>
<tr>
<td rowspan="2">lime/青柠</td>
<td>色盘</td>
<td>
<color-list
:colors="['#fcffe6', '#f4ffb8', '#eaff8f', '#d3f261', '#bae637', '#a0d911', '#7cb305', '#5b8c00', '#3f6600', '#254000']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@lime-1</code>
<code>@lime-2</code>
<code>...</code>
<code>@lime-10</code>
</td>
</tr>
<tr>
<td rowspan="2">gold/金盏花</td>
<td>色盘</td>
<td>
<color-list
:colors="['#fffbe6', '#fff1b8', '#ffe58f', '#ffd666', '#ffc53d', '#faad14', '#d48806', '#ad6800', '#874d00', '#613400']"
/>
</td>
</tr>
<tr>
<td>less变量</td>
<td>
<code>@gold-1</code>
<code>@gold-2</code>
<code>...</code>
<code>@gold-10</code>
</td>
</tr>
</table>
以上色系对应的less变量均可以在你的样式代码中直接使用。
:::tip
我们建议在开发中使用 `less变量` 而不是直接使用 `颜色值` 来设置颜色。这样做对主题色和主题模式切换很有帮助。
:::
## 主题模式
Vue Antd Admin 有三种主题模式,分别为:`light/亮色菜单模式``dark/暗色菜单模式``night/黑夜模式`
light / 亮色菜单模式:
![light](../assets/mode-light.png)
dark / 暗色菜单模式:
![dark](../assets/mode-dark.png)
night / 黑夜模式:
![night](../assets/mode-night.png)
你可以在这三种模式之间随意切换,也可以在 src/config/config.js 中设置默认的主题模式。
```js {4}
module.exports = {
theme: {
color: '#13c2c2',
mode: 'night' //设置你的默认主题模式,可选 light、dark 和 night
},
multiPage: true,
animate: {
name: 'roll',
direction: 'default'
}
}
```
## 导航布局
Vue Antd Admin 有两种导航布局,`side/侧边导航``head/顶部导航`
默认为侧边导航,你可以在 src/config/config.js 中修改导航布局
```js {6}
module.exports = {
theme: {
color: '#13c2c2',
mode: 'night'
},
layout: 'side', //设置你的默认导航布局,有 side 和 head 可选
multiPage: true,
animate: {
name: 'roll',
direction: 'default'
}
}
```
## 动画
Vue Antd Admin 内置了 [animate.css](https://animate.style) 动画库,在页面切换时会应用动画效果。你可以在 src/config/config.js 中配置动画效果或者禁用动画。
```js {7-11}
module.exports = {
theme: {
color: '#13c2c2',
mode: 'night'
},
multiPage: true,
animate: {
disabled: false, //禁用动画,true:禁用,false:启用
name: 'roll', //动画效果,支持的动画效果可参考 src/config/default/animate.config.js
direction: 'default' //动画方向,切换页面时动画的方向,参考 src/config/default/animate.config.js
}
}
```
支持的动画特效种类,可以参考 src/config/default/animate.config.js 文件。
## 其它
### 色弱模式
对于有视觉障碍的群体,我们提供了色弱模式,你可以通过配置 src/config/config.js 启用色弱模式
```js {7}
module.exports = {
theme: {
color: '#13c2c2',
mode: 'night'
},
multiPage: true,
weekMode: false, //色弱模式,true:开启,false:不开启
animate: {
name: 'roll',
direction: 'default'
}
}
```
### 多页签
在 src/config/config.js 设置 multiPage 来启用或关闭多页签模式
```js {6}
module.exports = {
theme: {
color: '#13c2c2',
mode: 'night'
},
multiPage: true, //多页签模式,true:开启,false:不开启
animate: {
name: 'roll',
direction: 'default'
}
}
```
完整的系统设置参考 src/config/default/setting.config.js
:::tip
以上所有主题设置项,均已映射到 vuex/setting 模块的 state 中,你可以通过提交 settingModule/mutations 实时修改设置项。
如何使用 [mutations](https://vuex.vuejs.org/zh/guide/mutations.html)
:::
---
title: 其它
lang: zh-CN
---
# 其它
---
title: 社区
lang: zh-CN
---
# 社区
## 交流学习
### QQ群:812277510、610090280(已满)
---
title: 更新日志
lang: zh-CN
---
# 更新日志
---
title: 开始
lang: zh-CN
---
## 开始
---
title: 常见问题
lang: zh-CN
---
# 常见问题
### 为什么不是 Ant Design Pro Vue ?
[Ant Design Pro Vue](https://github.com/vueComponent/ant-design-vue-pro)[Ant Design Pro](https://github.com/ant-design/ant-design-pro) 的 Vue 版本,其中项目结构、组件、
布局和使用方法等基本与 Ant Design Pro 的 react 版本保持一致。如果你比较熟悉 react 版,或者你已经在使用它,这确实是一个不错的选择。
[Vue Antd Admin](https://github.com/iczer/vue-antd-admin) 同样实现了 Ant Design Pro 的所有功能。与此同时,我们还根据 Vue 的特性,对 Ant Design Pro 的一些组件和布局作出了相应的修改及优化,同时不影响保持与 Ant Design Pro 的一致。
另外,我们还在添加一些 Ant Design Pro 没有的功能,比如全局动画、多页签模式等。
如果你想使用 Ant Design Pro,但又觉得它缺乏一些你想要的功能,不妨看看 [Vue Antd Admin](https://github.com/iczer/vue-antd-admin),我们会认真考虑每个用户的需求。
因此,如果你有一些不错的想法和建议,欢迎随时和我们交流,很可能你的想法就在我们下一个版本中实现。
### 如何使用 Vue Antd Admin ?
请阅读文档 [开始使用](./use.md)。有任何疑问,欢迎在 github 上给我们提交 [issue](https://github.com/iczer/vue-antd-admin/issues/new)
### 是否支持国际化 ?
Vue Antd Admin 引入了 vue-i18n 支持。因此你可以使用 vue-i18n 的特性对项目做国际化修改,详细请查看 [国际化](../advance/i18n.md)
---
title: 使用
lang: zh-CN
---
# 使用
## 准备
你的本地环境需要安装 yarn、node 和 git。我们的技术栈基于 ES2015+、Vue、Antd,提前学习这些知识会非常有帮助。
## 安装
克隆本项目到本地
```bash
$ git clone https://github.com/iczer/vue-antd-admin.git
```
安装依赖
```bash
$ yarn install
or
$ npm install
```
:::tip
master 分支是 Vue Antd Admin 的标准版代码,此分支代码适合用于用于学习研究,不推荐在此分支做正式开发。
我们在 basic 分支提供了 Vue Antd Admin 的基础版代码,正式开发请切换至此分支,以便于后续的版本更新。
:::
:::warning
如果基于 `master分支` 进行开发,在版本更新时遇到的代码冲突问题请自行解决,我们不对基于 `master分支` 开发时遇到的问题提供技术支持。
再次强调,`master分支` 仅推荐用于学习参考,正式开发请切换至 `basic` 分支!!!
:::
## 目录结构
我们已经为你生成了一个完整的开发框架,提供了涵盖中后台开发的各类功能和坑位,下面是整个项目的目录结构。
```bash
├── docs # 使用文档
├── public
│ └── favicon.png # favicon
│ └── index.html # 入口 HTML
├── src
│ ├── assets # 本地静态资源
│ ├── components # 内置通用组件
│ ├── config # 系统配置
│ ├── layouts # 通用布局
│ ├── mock # 本地 mock 数据
│ ├── pages # 页面组件和通用模板
│ ├── plugins # vue 插件
│ ├── router # 路由配置
│ ├── services # 数据服务模块
│ ├── store # vuex 状态管理配置
│ ├── theme # 主题相关
│ ├── utils # js 工具
│ ├── App.vue # 应用入口组件
│ ├── bootstrap.js # 应用启动引导js
│ └── main.js # 应用入口js
├── package.json # package.json
├── README.md # README.md
└── vue.config.js # vue 配置文件
```
## 本地开发
启动服务
```bash
$ yarn serve
or
$ npm run serve
```
启动成功后,会看到一个本地预览地址,通常是 http://localhost:8080 。接下来就可以修改代码,并实时预览修改结果啦!
{
"name": "vue-antd-admin",
"version": "0.7.2",
"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-prettier": "3.3.1",
"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.12",
"whatwg-fetch": "^3.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {
"no-unused-vars": "off"
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"lint-staged": {
"*.{js,jsx,vue}": [
"vue-cli-service lint",
"git add"
]
}
}
<!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>
assets OK
components No
config OK
layout Tomorrow
mock Tomorrow
pages No
plugins OK
router Tomorrow
service Tomorrow
theme Tomorrow
util OK
frame OK
\ No newline at end of file
<template>
<a-config-provider :locale="locale" :get-popup-container="popContainer">
<router-view />
</a-config-provider>
</template>
<script>
import { enquireScreen } from './utils/commonUtil';
import { mapState, mapMutations } from 'vuex';
import { getI18nKey } from '@/utils/routerUtil';
import { changeThemeColor } from '@/utils/themeUtil';
export default {
name: 'App',
data() {
return {
locale: {},
};
},
created() {
this.setHtmlTitle();
this.setLanguage(this.lang);
enquireScreen((isMobile) => this.setDevice(isMobile));
},
mounted() {},
watch: {
lang(val) {
this.setLanguage(val);
this.setHtmlTitle();
},
$route() {
this.setHtmlTitle();
},
'theme.mode': function (val) {
let closeMessage = this.$message.loading(`您选择了主题模式 ${val}, 正在切换...`);
changeThemeColor(this.theme.color, val).then(closeMessage);
},
'theme.color': function (val) {
let closeMessage = this.$message.loading(`您选择了主题色 ${val}, 正在切换...`);
changeThemeColor(val, this.theme.mode).then(closeMessage);
},
layout: function () {
window.dispatchEvent(new Event('resize'));
},
},
computed: {
//setting = mapStore.name
...mapState('settingModule', ['layout', 'theme', 'lang']),
},
methods: {
...mapMutations('settingModule', ['setDevice']),
setLanguage(lang) {
this.$i18n.locale = lang;
switch (lang) {
case 'zh_CN':
this.locale = require('ant-design-vue/es/locale-provider/zh_CN').default;
break;
case 'en_US':
this.locale = require('ant-design-vue/es/locale-provider/en_US').default;
break;
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>
</style>
import { isDef, isRegExp, remove } from '@/utils/commonUtil'
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;
}
</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>
.mini-chart {
position: relative;
width: 100%;
.chart-content {
position: absolute;
bottom: -28px;
width: 100%;
}
}
</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;
.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>
<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>
export default {
name: 'ExceptionPage',
props: ['type', 'homeRoute'],
data() {
return {
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: '抱歉,服务器出错了',
},
},
};
},
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>
<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';
},
...mapState('settingModule', ['isMobile', 'systemName']),
},
methods: {
onSelect(obj) {
this.$emit('menuSelect', obj);
},
},
};
</script>
<style lang="less" scoped>
.shadow {
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.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 0.3s;
transition: all 0.3s;
overflow: hidden;
background-color: @layout-trigger-background;
&.light {
background-color: #fff;
h1 {
color: @primary-color;
}
}
h1 {
color: @menu-dark-highlight-color;
font-size: 20px;
margin: 0 0 0 12px;
display: inline-block;
vertical-align: middle;
}
img {
width: 32px;
vertical-align: middle;
}
}
}
.menu {
padding: 16px 0;
}
</style>
/**
* 该插件可根据菜单配置自动生成 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: {
* zh_CN: {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;
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";
},
},
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";
let config = {
props: { to: menu.fullPath },
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() {
const matchedRoutes = this.$route.matched.filter((item) => item.path !== "");
this.selectedKeys = this.getSelectedKey(this.$route);
let openKeys = matchedRoutes.map((item) => item.path);
openKeys = openKeys.slice(0, openKeys.length - 1);
if (!fastEqual(openKeys, this.sOpenKeys)) {
this.collapsed || this.mode === "horizontal"
? (this.cachedOpenKeys = openKeys)
: (this.sOpenKeys = openKeys);
}
},
getSelectedKey(route) {
return route.matched.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('settingModule', ['layout', 'showPageTitle', 'pageWidth']),
},
};
</script>
<style lang="less" scoped>
.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;
}
}
}
}
}
</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="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>
<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 {globalConfig} from '@/config'
const animates = globalConfig.animates.preset;
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,
},
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>
/**
* 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 cssResolverConfig = {
'.ant-checkbox-checked .ant-checkbox-inner::after': {
resolve(cssText, cssObj) {
cssObj.rules.push('border-top:0', 'border-left:0')
return cssObj.toText()
}
},
'.ant-tree-checkbox-checked .ant-tree-checkbox-inner::after': {
resolve(cssText, cssObj) {
cssObj.rules.push('border-top:0', 'border-left:0')
return cssObj.toText()
}
},
'.ant-checkbox-checked .ant-checkbox-inner:after': {
resolve(cssText, cssObj) {
cssObj.rules.push('border-top:0', 'border-left:0')
return cssObj.toText()
}
},
'.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after': {
resolve(cssText, cssObj) {
cssObj.rules.push('border-top:0', 'border-left:0')
return cssObj.toText()
}
},
'.ant-menu-dark .ant-menu-inline.ant-menu-sub': {
resolve(cssText, cssObj) {
cssObj.rules = cssObj.rules.filter(rule => rule.indexOf('box-shadow') == -1)
return cssObj.toText()
}
},
'.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu:hover,.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-submenu-selected': {
resolve(cssText, cssObj) {
cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)')
return cssObj.toText()
}
},
'.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover': {
resolve(cssText, cssObj) {
cssObj.selector = cssObj.selector.replace(/.ant-menu-horizontal/g, '.ant-menu-horizontal:not(.ant-menu-dark)')
return cssObj.toText()
}
},
'.ant-layout-sider': {
resolve(cssText, cssObj) {
cssObj.selector = '.ant-layout-sider-dark'
return cssObj.toText()
}
},
'/keyframes/': false
};
module.exports = cssResolverConfig;
// 全部的配置数据
const globalConfig = {
//支持语言
langs: [
{key: 'zh_CN', name: '简体中文', alias: '简体'},
{key: 'en_US', name: 'English', alias: 'English'}
],
//调色板 颜色
palettes: ["#f5222d", "#fa541c", "#fadb14", "#3eaf7c", "#13c2c2", "#1890ff", "#722ed1", "#eb2f96"],
//tab出入动画效果
animates: {
preset: [
//参考Animate.css 各种特效的css命名,default对应没有 direction的情况
{ name: "back", alias: "渐近", directions: ["Left", "Right"] },
{ name: "bounce", alias: "弹跳", directions: ["Left", "Right", "Default"] },
{
name: "fade",
alias: "淡化",
directions: ["Left", "LeftBig", "Right", "RightBig", "Default"],
},
{ name: "lightSpeed", alias: "光速", directions: ["Left", "Right"] },
{ name: "slide", alias: "滑动", directions: ["Left", "Right"] },
],
},
//当前主色调 + 3个功能颜色 + 3个模式菜单颜色
primary: {
color: "#3eaf7c",
warning: "#faad14",
success: "#52c41a",
error: "#f5222d",
light: {
menuColors: ["#000c17", "#001529", "#002140"],
},
dark: {
menuColors: ["#000c17", "#001529", "#002140"],
},
night: {
menuColors: ["#151515", "#1f1f1f", "#1e1e1e"],
},
},
//主题
theme: {
mode: {
DARK: "dark",
LIGHT: "light",
NIGHT: "night",
},
dark: {
"layout-body-background": "#f0f2f5",
"body-background": "#fff",
"component-background": "#fff",
"heading-color": "rgba(0, 0, 0, 0.85)",
"text-color": "rgba(0, 0, 0, 0.65)",
"text-color-inverse": "#fff",
"text-color-secondary": "rgba(0, 0, 0, 0.45)",
"shadow-color": "rgba(0, 0, 0, 0.15)",
"border-color-split": "#f0f0f0",
"background-color-light": "#fafafa",
"background-color-base": "#f5f5f5",
"table-selected-row-bg": "#fafafa",
"table-expanded-row-bg": "#fbfbfb",
"checkbox-check-color": "#fff",
"disabled-color": "rgba(0, 0, 0, 0.25)",
"menu-dark-color": "rgba(254, 254, 254, 0.65)",
"menu-dark-highlight-color": "#fefefe",
"menu-dark-arrow-color": "#fefefe",
"btn-primary-color": "#fff",
},
light: {
"layout-body-background": "#f0f2f5",
"body-background": "#fff",
"component-background": "#fff",
"heading-color": "rgba(0, 0, 0, 0.85)",
"text-color": "rgba(0, 0, 0, 0.65)",
"text-color-inverse": "#fff",
"text-color-secondary": "rgba(0, 0, 0, 0.45)",
"shadow-color": "rgba(0, 0, 0, 0.15)",
"border-color-split": "#f0f0f0",
"background-color-light": "#fafafa",
"background-color-base": "#f5f5f5",
"table-selected-row-bg": "#fafafa",
"table-expanded-row-bg": "#fbfbfb",
"checkbox-check-color": "#fff",
"disabled-color": "rgba(0, 0, 0, 0.25)",
"menu-dark-color": "rgba(1, 1, 1, 0.65)",
"menu-dark-highlight-color": "#fefefe",
"menu-dark-arrow-color": "#fefefe",
"btn-primary-color": "#fff",
},
night: {
"layout-body-background": "#000",
"body-background": "#141414",
"component-background": "#141414",
"heading-color": "rgba(255, 255, 255, 0.85)",
"text-color": "rgba(255, 255, 255, 0.85)",
"text-color-inverse": "#141414",
"text-color-secondary": "rgba(255, 255, 255, 0.45)",
"shadow-color": "rgba(255, 255, 255, 0.15)",
"border-color-split": "#303030",
"background-color-light": "#ffffff0a",
"background-color-base": "#2a2a2a",
"table-selected-row-bg": "#ffffff0a",
"table-expanded-row-bg": "#ffffff0b",
"checkbox-check-color": "#141414",
"disabled-color": "rgba(255, 255, 255, 0.25)",
"menu-dark-color": "rgba(254, 254, 254, 0.65)",
"menu-dark-highlight-color": "#fefefe",
"menu-dark-arrow-color": "#fefefe",
"btn-primary-color": "#141414",
},
},
layout: {
SIDE: "side",
HEAD: "head",
},
};
module.exports = globalConfig;
\ No newline at end of file
import globalConfig from './global.config';
import settingConfig from './setting.config';
import cssResolverConfig from './cssResolver.config';
/**
* global.config setting.config cssResolver.config 每个单独的文件只能采用 require的方式
* 原因是: vue.config.js里面需要使用,而其被webpack调用 只能用require的方式
*/
export {globalConfig, settingConfig ,cssResolverConfig};
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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