4
0
mirror of https://github.com/iczer/vue-antd-admin.git synced 2025-10-03 21:29:58 +08:00

Compare commits

..

No commits in common. "master" and "0.1.1" have entirely different histories.

203 changed files with 2149 additions and 8696 deletions

10
.env
View File

@ -1,10 +0,0 @@
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

View File

@ -1 +0,0 @@
VUE_APP_API_BASE_URL=http://dev.iczer.com

View File

@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

1
.gitignore vendored
View File

@ -17,4 +17,3 @@ selenium-debug.log
*.njsproj
*.sln
package-lock.json
.env.production.local

View File

@ -10,15 +10,13 @@ An out-of-box UI solution for enterprise applications as a React boilerplate.
[![Dependence](https://img.shields.io/david/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin)
[![DevDependencies](https://img.shields.io/david/dev/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin?type=dev)
[![Release](https://img.shields.io/github/v/release/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/releases/latest)
![image](./src/assets/img/preview.png)
![image](./src/assets/img/preview.png)
Multiple theme modes available
![image](./src/assets/img/preview-nine.png)
</div>
- Previewhttps://vue-antd-admin.pages.dev
- Documentationhttps://doc.vue-antd-admin.pages.dev
- FAQhttps://doc.vue-antd-admin.pages.dev/start/faq.html
- Previewhttps://iczer.gitee.io/vue-antd-admin
- Documentationhttps://iczer.github.io/vue-antd-admin
- FAQhttps://iczer.github.io/vue-antd-admin/start/faq.html
- Mirror Repo in Chinahttps://gitee.com/iczer/vue-antd-admin
## Browsers support
@ -43,11 +41,10 @@ $ yarn serve
$ npm install
$ npm run serve
```
More instructions at [documentation](https://iczer.gitee.io/vue-antd-admin-docs).
More instructions at [documentation](https://iczer.github.io/vue-antd-admin).
## Contributing
Any type of contribution is welcome, here are some examples of how you may contribute to this project: :star2:
- Use Vue Antd Admin in your daily work.
- Submit [Issue](https://github.com/iczer/vue-antd-admin/issues) to report :bug: or ask questions.
- Propose [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) to improve our code.
- Join the community and share your experiences with us. QQ Group:942083829、812277510(full)、610090280(full)

View File

@ -1,5 +1,3 @@
简体中文 | [English](./README.en-US.md)
<h1 align="center">Vue Antd Admin</h1>
@ -8,25 +6,17 @@
[Ant Design Pro](https://github.com/ant-design/ant-design-pro) 的 Vue 实现版本
开箱即用的中后台前端/设计解决方案
:star::star::star:
vue3 版本现已推出,更名为
[stepin-template](https://github.com/stepui/stepin-template),欢迎体验,
[立即前往](https://github.com/stepui/stepin-template)
--
[![MIT](https://img.shields.io/github/license/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/blob/master/LICENSE)
[![Dependence](https://img.shields.io/david/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin)
[![DevDependencies](https://img.shields.io/david/dev/iczer/vue-antd-admin)](https://david-dm.org/iczer/vue-antd-admin?type=dev)
[![Release](https://img.shields.io/github/v/release/iczer/vue-antd-admin)](https://github.com/iczer/vue-antd-admin/releases/latest)
![image](./src/assets/img/preview.png)
![image](./src/assets/img/preview.png)
多种主题模式可选:
![image](./src/assets/img/preview-nine.png)
</div>
- 预览地址https://vue-antd-admin.pages.dev
- 使用文档https://doc.vue-antd-admin.pages.dev
- 常见问题https://doc.vue-antd-admin.pages.dev/start/faq.html
- 预览地址https://iczer.gitee.io/vue-antd-admin
- 使用文档https://iczer.github.io/vue-antd-admin
- 常见问题https://iczer.github.io/vue-antd-admin/start/faq.html
- 国内镜像https://gitee.com/iczer/vue-antd-admin
## 浏览器支持
@ -51,18 +41,10 @@ $ yarn serve
$ npm install
$ npm run serve
```
更多信息参考 [使用文档](https://iczer.gitee.io/vue-antd-admin-docs)
更多信息参考 [使用文档](https://iczer.github.io/vue-antd-admin)
## 参与贡献
我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :star2:
- 在你的公司或个人项目中使用 Vue Antd Admin。
- 通过 [Issue](https://github.com/iczer/vue-antd-admin/issues) 报告:bug:或进行咨询。
- 提交 [Pull Request](https://github.com/iczer/vue-antd-admin/pulls) 改进 Admin 的代码。
- 加入社群与小伙伴们一同交流心得。QQ群942083829、 812277510已满、610090280已满
## 打赏
如果该项目对您有所帮助,可以请作者喝一杯咖啡。
<p>
<img src="./src/assets/img/alipay.png" width="320px" style="display: inline-block;" />
<img src="./src/assets/img/wechatpay.png" width="320px" style="display: inline-block; margin-left: 24px;" />
</p>

View File

@ -1,13 +1,5 @@
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
]
}

View File

@ -1,42 +0,0 @@
<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>

View File

@ -1,35 +0,0 @@
<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>

View File

@ -1,18 +0,0 @@
<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>

View File

@ -1,7 +1,7 @@
module.exports = {
title: 'Vue Antd Admin',
description: 'Vue Antd Admin',
base: '/',
base: '/vue-antd-admin/',
head: [
['link', { rel: 'icon', href: '/favicon.ico' }]
],
@ -10,10 +10,9 @@ module.exports = {
repo: 'iczer/vue-antd-admin',
docsDir: 'docs',
editLinks: true,
editLinkText: '在 Github 上帮助我们编辑此页',
nav: [
{text: '指南', link: '/'},
{text: '配置', link: '/develop/layout'},
{text: '配置', link: '/develop/'},
{text: '主题', link: '/advance/theme'},
],
lastUpdated: 'Last Updated',
@ -36,8 +35,7 @@ module.exports = {
title: '进阶',
collapsable: false,
children: [
'/advance/i18n', '/advance/async', '/advance/authority', '/advance/login', '/advance/guard', '/advance/interceptors',
'/advance/api'
'/advance/i18n', '/advance/chart', '/advance/theme', '/advance/error', '/advance/authority'
]
},
{
@ -51,7 +49,7 @@ module.exports = {
nextLinks: true,
prevLinks: true,
},
plugins: ['@vuepress/back-to-top', require('./plugins/alert')],
plugins: ['@vuepress/back-to-top'],
markdown: {
lineNumbers: true
}

View File

@ -1,46 +0,0 @@
<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>

View File

@ -1,38 +0,0 @@
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

View File

@ -1,5 +0,0 @@
export default {
updated() {
this.$page.alert.top = 100
}
}

View File

@ -1,5 +0,0 @@
import AlertMixin from './alertMixin'
export default ({Vue}) => {
Vue.use(AlertMixin)
}

View File

@ -1,13 +0,0 @@
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')
}
}

View File

@ -1,12 +1,9 @@
.custom-block.tip{
border-color: #1890ff
border-color: #52c41a
}
.theme-default-content code .token.inserted{
color: #60bd90;
.custom-block.warning{
border-color: #fa8c16
}
.custom-block.error{
border-color: #f5222d
}
//.custom-block.warning{
// border-color: #fa8c16
//}
//.custom-block.error{
// border-color: #f5222d
//}

View File

@ -1,42 +0,0 @@
---
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`,即通过权限校验还是角色进行校验,可不传(不传的话,会对两种类型都进行匹配,任意一种匹配成功即校验通过)。

View File

@ -1,258 +0,0 @@
---
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('account/setRoutesConfig', routesConfig)
} else {
routesConfig = store.getters['account/routesConfig']
}
// 如果开启了异步路由,则加载异步路由配置
const asyncRoutes = store.state.setting.asyncRoutes
if (asyncRoutes) {
if (routesConfig && routesConfig.length > 0) {
const routes = parseRoutes(routesConfig, routerMap)
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('setting/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('setting/setMenuData', menuRoutes)
}
```
:::tip
如果你不想从根路由 `/` 下获取菜单数据,可以根据自己的需求更改。
:::

View File

@ -3,228 +3,3 @@ 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'] //权限下的操作权限
}
```
你也可以设置 permission 的值为字符串,比如 permission = 'form', 它等同于:
```js
permission = {
id: 'form'
}
```
### 设置用户的角色和权限
你只需为用户配置 roles 和 permissions 两者中的其中一种,即可完成权限管理功能。当然你也可以两者都配置。
获取到用户权限或角色后,将其格式化转为 Vue Antd Admin 可用的格式,然后使用 `store.commit('account/setPermissions', permissions)``store.commit('account/setRoles', roles)`
将其存在本地即可。如下:
```js
getPermissions().then(res => {
const permissions = res.data
this.$store.commit('account/setPermissions', permissions)
})
getRoles().then(res => {
const roles = res.data
this.$store.commit('account/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',
}
}
]
}]
```

View File

@ -3,5 +3,3 @@ title: 图表
lang: zn-CN
---
# 图表
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页

View File

@ -3,5 +3,3 @@ title: 错误处理
lang: zn-CN
---
# 错误处理
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页

View File

@ -1,109 +0,0 @@
---
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['account/permissions']
const roles = store.getters['account/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]
}
```
:::

View File

@ -3,127 +3,3 @@ 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/) 。

View File

@ -1,131 +0,0 @@
---
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
我们会为 onRejected 钩子函数注入 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] // 响应拦截
}
```
:::

View File

@ -1,76 +0,0 @@
---
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 文件中找到。
:::

View File

@ -1,10 +0,0 @@
---
title: 108个小技巧
lang: zn-CN
---
# 108个小技巧
## 自定义菜单icon
## 隐藏页面标题
## 关闭页签API
## 权限校验PI

View File

@ -3,5 +3,3 @@ title: 更换主题
lang: zn-CN
---
# 更换主题
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -29,7 +29,7 @@ lang: zh-CN
![blank-view](../assets/blank-view.png)
## 如何使用
通常我们会把视图组件和路由配置结合一起使用,我们把配置信息抽离在路由配置文件中 [src/router/config.js](https://github.com/iczer/vue-antd-admin/blob/master/src/router/config.js) 。如下:
通常我们会把视图组件和路由配置结合一起使用,我们把配置信息抽离在路由配置文件中 [router/index.js](https://github.com/iczer/vue-antd-admin/blob/master/src/router/index.js) 。如下:
```jsx {7,12}
{
path: 'form',

View File

@ -3,5 +3,3 @@ title: Mock
lang: zh-CN
---
# Mock
### 作者还没来得及编辑该页面,如果你感兴趣,可以点击下方链接,帮助作者完善此页

View File

@ -3,216 +3,3 @@ 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) 中,我们会对此作详细讲解。
:::

View File

@ -17,8 +17,9 @@ lang: zh-CN
## 路由
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 = {
```js {8,14}
import Router from 'vue-router'
export default new Router({
routes: [{
path: '/',
name: '首页',
@ -44,9 +45,9 @@ const options = {
}]
}]
}]
}
})
```
完整配置示例,请查看 [src/router/config.js](https://github.com/iczer/vue-antd-admin/blob/master/src/router/config.js)
完整配置示例,请查看 [router/index.js](https://github.com/iczer/vue-antd-admin/blob/master/src/router/index.js)
## 菜单
Admin 系统的菜单直接通过路由配置生成,路由属性和菜单功能对应关系如下

View File

@ -3,159 +3,3 @@ 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服务器配置。
:::

View File

@ -3,500 +3,3 @@ 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 中,你可以通过提交 setting/mutations 实时修改设置项。
如何使用 [mutations](https://vuex.vuejs.org/zh/guide/mutations.html)
:::

View File

@ -3,6 +3,3 @@ title: 社区
lang: zh-CN
---
# 社区
## 交流学习
### QQ群812277510、610090280已满

View File

@ -13,17 +13,7 @@ $ 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` 分支!!!
:::
## 目录结构
我们已经为你生成了一个完整的开发框架,提供了涵盖中后台开发的各类功能和坑位,下面是整个项目的目录结构。
@ -34,19 +24,17 @@ master 分支是 Vue Antd Admin 的标准版代码,此分支代码适合用于
│ └── index.html # 入口 HTML
├── src
│ ├── assets # 本地静态资源
│ ├── components # 内置通用组件
│ ├── components # 业务通用组件
│ ├── config # 系统配置
│ ├── layouts # 通用布局
│ ├── mock # 本地 mock 数据
│ ├── pages # 页面组件和通用模板
│ ├── plugins # vue 插件
│ ├── router # 路由配置
│ ├── services # 数据服务模块
│ ├── store # vuex 状态管理配置
│ ├── theme # 主题相关
│ ├── utils # js 工具
│ ├── App.vue # 应用入口组件
│ ├── bootstrap.js # 应用启动引导js
│ └── App.vue # 应用入口组件
│ └── main.js # 应用入口js
├── package.json # package.json
├── README.md # README.md
@ -56,7 +44,5 @@ master 分支是 Vue Antd Admin 的标准版代码,此分支代码适合用于
启动服务
```bash
$ yarn serve
or
$ npm run serve
```
启动成功后,会看到一个本地预览地址,通常是 http://localhost:8080 。接下来就可以修改代码,并实时预览修改结果啦!

View File

@ -1,32 +1,29 @@
{
"name": "vue-antd-admin",
"version": "0.7.4",
"version": "0.1.0",
"homepage": "https://iczer.github.io/vue-antd-admin",
"private": true,
"scripts": {
"serve": "export NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
"build": "export NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service build",
"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://github.com/iczer/vue-antd-admin.git",
"docs:dev": "export NODE_OPTIONS=--openssl-legacy-provider && vuepress dev docs",
"docs:build": "export NODE_OPTIONS=--openssl-legacy-provider && vuepress build docs",
"predocs:deploy": "yarn docs:build",
"docs:deploy": "gh-pages -d docs/.vuepress/dist -b doc -r https://github.com/iczer/vue-antd-admin.git"
"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"
},
"dependencies": {
"@antv/data-set": "^0.11.4",
"animate.css": "^4.1.0",
"ant-design-vue": "1.7.2",
"ant-design-vue": "^1.6.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",
"pouchdb": "^7.2.1",
"viser-vue": "^2.4.8",
"vue": "^2.6.11",
"vue-i18n": "^8.18.2",
@ -41,20 +38,17 @@
"@vue/cli-service": "^4.4.0",
"@vuepress/plugin-back-to-top": "^1.5.2",
"babel-eslint": "^10.1.0",
"babel-plugin-transform-remove-console": "^6.9.4",
"babel-polyfill": "^6.26.0",
"compression-webpack-plugin": "^2.0.0",
"deepmerge": "^4.2.2",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"fast-deep-equal": "^3.1.3",
"gh-pages": "^3.1.0",
"less-loader": "^6.1.1",
"style-resources-loader": "^1.3.2",
"vue-cli-plugin-style-resources-loader": "^0.1.4",
"vue-template-compiler": "^2.6.11",
"vuepress": "^1.5.2",
"webpack-theme-color-replacer": "1.3.18",
"webpack-theme-color-replacer": "^1.3.12",
"whatwg-fetch": "^3.0.0"
},
"eslintConfig": {

View File

@ -1,27 +1,17 @@
<!DOCTYPE html>
<html lang="en" class="beauty-scroll">
<html lang="en">
<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] %>" />
<% } %>
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<body class="beauty-scroll">
<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>
<% } %>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,26 +1,26 @@
<template>
<a-config-provider :locale="locale" :get-popup-container="popContainer">
<a-config-provider :locale="locale">
<router-view/>
</a-config-provider>
</template>
<script>
import {enquireScreen} from './utils/util'
import {mapState, mapMutations} from 'vuex'
import themeUtil from '@/utils/themeUtil';
import {getI18nKey} from '@/utils/routerUtil'
import zh_CN from 'ant-design-vue/es/locale-provider/zh_CN'
import enquireScreen from './utils/device'
import {mapState} from 'vuex'
export default {
name: 'App',
data() {
return {
locale: {}
locale: zh_CN
}
},
created () {
this.setHtmlTitle()
this.setLanguage(this.lang)
enquireScreen(isMobile => this.setDevice(isMobile))
let _this = this
enquireScreen(isMobile => {
_this.$store.commit('setting/setDevice', isMobile)
})
},
mounted() {
this.setWeekModeTheme(this.weekMode)
@ -30,39 +30,8 @@ export default {
this.setWeekModeTheme(val)
},
lang(val) {
this.setLanguage(val)
this.setHtmlTitle()
},
$route() {
this.setHtmlTitle()
},
'theme.mode': function(val) {
let closeMessage = this.$message.loading(`您选择了主题模式 ${val}, 正在切换...`)
themeUtil.changeThemeColor(this.theme.color, val).then(closeMessage)
},
'theme.color': function(val) {
let closeMessage = this.$message.loading(`您选择了主题色 ${val}, 正在切换...`)
themeUtil.changeThemeColor(val, this.theme.mode).then(closeMessage)
},
'layout': function() {
window.dispatchEvent(new Event('resize'))
}
},
computed: {
...mapState('setting', ['layout', 'theme', 'weekMode', 'lang'])
},
methods: {
...mapMutations('setting', ['setDevice']),
setWeekModeTheme(weekMode) {
if (weekMode) {
document.body.classList.add('week-mode')
} else {
document.body.classList.remove('week-mode')
}
},
setLanguage(lang) {
this.$i18n.locale = lang
switch (lang) {
this.$i18n.locale = val
switch (val) {
case 'CN':
this.locale = require('ant-design-vue/es/locale-provider/zh_CN').default
break
@ -74,14 +43,18 @@ export 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")
}
},
computed: {
...mapState('setting', ['weekMode', 'lang'])
},
methods: {
setWeekModeTheme(weekMode) {
if (weekMode) {
document.body.classList.add('week-mode')
} else {
document.body.classList.remove('week-mode')
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

25
src/bootstrap.js vendored
View File

@ -1,25 +0,0 @@
import {loadRoutes, loadGuards, setAppOptions} from '@/utils/routerUtil'
import {loadInterceptors} from '@/utils/request'
import guards from '@/router/guards'
import interceptors from '@/utils/axios-interceptors'
/**
* 启动引导方法
* 应用启动时需要执行的操作放在这里
* @param router 应用的路由实例
* @param store 应用的 vuex.store 实例
* @param i18n 应用的 vue-i18n 实例
* @param i18n 应用的 message 实例
*/
function bootstrap({router, store, i18n, message}) {
// 设置应用配置
setAppOptions({router, store, i18n})
// 加载 axios 拦截器
loadInterceptors(interceptors, {router, store, i18n, message})
// 加载路由
loadRoutes()
// 加载路由守卫
loadGuards(guards, {router, store, i18n, message})
}
export default bootstrap

View File

@ -0,0 +1,113 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h2>Essential Links</h2>
<ul>
<li>
<a
href="https://vuejs.org"
target="_blank"
>
Core Docs
</a>
</li>
<li>
<a
href="https://forum.vuejs.org"
target="_blank"
>
Forum
</a>
</li>
<li>
<a
href="https://chat.vuejs.org"
target="_blank"
>
Community Chat
</a>
</li>
<li>
<a
href="https://twitter.com/vuejs"
target="_blank"
>
Twitter
</a>
</li>
<br>
<li>
<a
href="http://vuejs-templates.github.io/webpack/"
target="_blank"
>
Docs for This Template
</a>
</li>
</ul>
<h2>Ecosystem</h2>
<ul>
<li>
<a
href="http://router.vuejs.org/"
target="_blank"
>
vue-router
</a>
</li>
<li>
<a
href="http://vuex.vuejs.org/"
target="_blank"
>
vuex
</a>
</li>
<li>
<a
href="http://vue-loader.vuejs.org/"
target="_blank"
>
vue-loader
</a>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
>
awesome-vue
</a>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

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

View File

@ -27,7 +27,7 @@ export default {
}
</script>
<style scoped lang="less">
<style scoped>
.chart-card-header{
position: relative;
overflow: hidden;
@ -37,7 +37,7 @@ export default {
position: relative;
overflow: hidden;
width: 100%;
color: @text-color-second;
color: rgba(0,0,0,.45);
font-size: 14px;
line-height: 22px;
}
@ -52,6 +52,7 @@ export default {
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
color: #000;
margin-top: 4px;
margin-bottom: 0;
font-size: 30px;
@ -59,7 +60,7 @@ export default {
height: 38px;
}
.chart-card-footer{
border-top: 1px solid @border-color-base;
border-top: 1px solid #e8e8e8;
padding-top: 9px;
margin-top: 8px;
}

View File

@ -25,7 +25,7 @@ export default {
position: relative;
width: 100%;
.wrap {
background-color: @layout-bg-color;
background-color: #f5f5f5;
position: relative;
}
.progress {

View File

@ -30,12 +30,12 @@ export default {
li {
margin-top: 16px;
span {
color: @text-color-second;
color: rgba(0,0,0,.65);
font-size: 14px;
line-height: 22px;
}
span:first-child {
background-color: @layout-bg-color;
background-color: #f5f5f5;
border-radius: 20px;
display: inline-block;
font-size: 12px;
@ -47,8 +47,8 @@ export default {
text-align: center;
}
span.active {
background-color: #314659 !important;
color: @text-color-inverse !important;
background-color: #314659;
color: #fff;
}
span:last-child {
float: right;

View File

@ -69,10 +69,10 @@ export default {
.chart-trend-icon{
font-size: 12px;
&.up{
color: @red-6;
color: #f5222d;
}
&.down{
color: @green-6;
color: #52c41a;
}
}
}

View File

@ -42,19 +42,28 @@ const Group = {
}
},
watch: {
values(value) {
this.$emit('change', value, this.colors)
values() {
this.$emit('change', this.values, this.colors)
},
defaultValues(value) {
if (this.multiple) {
this.options.forEach(item => {
item.sChecked = value.indexOf(item.value) > -1
})
} else {
this.options.forEach(item => {
let first = value[0]
item.sChecked = first && first == item.value
})
}
}
},
methods: {
handleChange (option) {
if (!option.checked) {
if (this.values.indexOf(option.value) > -1) {
this.values = this.values.filter(item => item != option.value)
}
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
@ -124,14 +133,12 @@ export default {
},
methods: {
toggle () {
if (this.groupContext.multiple || !this.sChecked) {
this.sChecked = !this.sChecked
}
this.sChecked = !this.sChecked
},
initChecked() {
let groupContext = this.groupContext
if (!groupContext) {
return this.checked
return this.check
}else if (groupContext.multiple) {
return groupContext.defaultValues.indexOf(this.value) > -1
} else {
@ -151,7 +158,7 @@ export default {
cursor: pointer;
margin-right: 8px;
text-align: center;
color: @base-bg-color;
color: #fff;
font-weight: bold;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<a-tooltip :title="title" :overlayStyle="{zIndex: 2001}">
<a-tooltip :title="title">
<div class="img-check-box" @click="toggle">
<img :src="img" />
<div v-if="sChecked" class="check-item">
@ -36,25 +36,22 @@ const Group = {
}
},
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)
// }
'values': function (newVal, oldVal) {
// 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)
}
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) {
if (item.value !== option.value) {
item.sChecked = false
}
})
@ -95,7 +92,7 @@ export default {
},
data () {
return {
sChecked: this.initChecked()
sChecked: this.checked
}
},
inject: ['groupContext'],
@ -121,19 +118,7 @@ export default {
},
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
}
this.sChecked = !this.sChecked
}
}
}

View File

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

View File

@ -1,5 +1,5 @@
<template>
<div class="exception-page">
<div class="exception-page" :style="{height: layoutMinHeight - 32 + 'px'}">
<div class="img">
<img :src="config[type].img" />
</div>
@ -7,7 +7,7 @@
<h1>{{config[type].title}}</h1>
<div class="desc">{{config[type].desc}}</div>
<div class="action">
<a-button type="primary" @click="backHome">返回首页</a-button>
<a-button type="primary" >返回首页</a-button>
</div>
</div>
</div>
@ -18,20 +18,13 @@ import Config from './typeConfig'
export default {
name: 'ExceptionPage',
props: ['type', 'homeRoute'],
props: ['type'],
data () {
return {
config: Config
}
},
methods: {
backHome() {
if (this.homeRoute) {
this.$router.push(this.homeRoute)
}
this.$emit('backHome', this.type)
}
}
inject: ['layoutMinHeight']
}
</script>
@ -41,7 +34,7 @@ export default {
display: flex;
justify-content: center;
align-items: center;
background-color: @base-bg-color;
background-color: white;
.img{
padding-right: 52px;
zoom: 1;
@ -58,7 +51,7 @@ export default {
margin-bottom: 24px;
}
.desc{
color: @text-color-second;
color: rgba(0,0,0,.45);
font-size: 20px;
line-height: 28px;
margin-bottom: 16px;

View File

@ -19,10 +19,10 @@ export default {
<style lang="less" scoped>
.form-row{
display: flex;
border-bottom: 1px dashed @border-color-base;
border-bottom: 1px dashed #e8e8e8;
margin-bottom: 16px;
.label {
color: @title-color;
color: rgba(0, 0, 0, 0.85);
font-size: 14px;
margin-right: 24px;
flex: 0 0 auto;

View File

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

View File

@ -1,14 +1,7 @@
<template>
<a-menu
v-show="visible"
class="contextmenu"
:style="style"
:selectedKeys="selectedKeys"
@click="handleClick"
>
<a-menu :style="style" class="contextmenu" v-show="visible" @click="handleClick" :selectedKeys="selectedKeys">
<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-icon role="menuitemicon" v-if="item.icon" :type="item.icon" />{{item.text}}
</a-menu-item>
</a-menu>
</template>
@ -33,7 +26,6 @@ export default {
left: 0,
top: 0,
target: null,
meta: null,
selectedKeys: []
}
},
@ -46,26 +38,23 @@ export default {
}
},
created () {
window.addEventListener('click', this.closeMenu)
window.addEventListener('contextmenu', this.setPosition)
},
beforeDestroy() {
window.removeEventListener('click', this.closeMenu)
window.removeEventListener('contextmenu', this.setPosition)
window.addEventListener('mousedown', e => this.closeMenu(e))
window.addEventListener('contextmenu', e => this.setPosition(e))
},
methods: {
closeMenu () {
this.$emit('update:visible', false)
closeMenu (e) {
if (['menuitemicon', 'menuitem'].indexOf(e.target.getAttribute('role')) < 0) {
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()
handleClick ({key}) {
this.$emit('select', key, this.target)
this.$emit('update:visible', false)
}
}
}
@ -74,11 +63,9 @@ export default {
<style lang="less" scoped>
.contextmenu{
position: fixed;
z-index: 1000;
z-index: 1;
border-radius: 4px;
box-shadow: -4px 4px 16px 1px @shadow-color !important;
}
.ant-menu-item {
margin: 0 !important //
box-shadow: -4px 4px 16px 1px #e6e6e6 !important;
}
</style>

View File

@ -1,21 +1,22 @@
<template>
<a-layout-sider :theme="sideTheme" :class="['side-menu', 'beauty-scroll', isMobile ? null : 'shadow']" width="256px" :collapsible="collapsible" v-model="collapsed" :trigger="null">
<a-layout-sider :theme="theme" :class="['side', 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"/>
<i-menu @i18nComplete="setRoutesI18n" :i18n="menuI18n" :theme="theme" :collapsed="collapsed" :options="menuData" @select="onSelect" class="menu"/>
</a-layout-sider>
</template>
<script>
import IMenu from './menu'
import {mapState} from 'vuex'
import {mapState, mapMutations} from 'vuex'
export default {
name: 'SideMenu',
components: {IMenu},
inject: ['menuI18n'],
props: {
collapsible: {
type: Boolean,
@ -38,19 +39,52 @@ export default {
}
},
computed: {
sideTheme() {
return this.theme == 'light' ? this.theme : 'dark'
},
...mapState('setting', ['isMobile', 'systemName'])
},
methods: {
onSelect (obj) {
this.$emit('menuSelect', obj)
}
},
...mapMutations('setting', ['setRoutesI18n'])
}
}
</script>
<style lang="less" scoped>
@import "index";
.shadow{
box-shadow: 2px 0 6px rgba(0,21,41,.35);
}
.side{
z-index: 10;
.logo{
height: 64px;
position: relative;
line-height: 64px;
padding-left: 24px;
-webkit-transition: all .3s;
transition: all .3s;
overflow: hidden;
background-color: @layout-trigger-background;
&.light{
background-color: #fff;
h1{
color: @primary-color;
}
}
h1{
color: #fff;
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>

View File

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

View File

@ -22,42 +22,19 @@
* }
* ]
*
* i18n: 国际化配置系统默认会根据 options route配置的 path name 生成英文以及中文的国际化配置如需自定义或增加其他语言配置
* i18n: 国际化配置组件默认会根据 options route配置的 path name 生成英文以及中文的国际化配置如需自定义或增加其他语言配置
* 此项即可
* i18n: {
* messages: {
* CN: {dashboard: {name: '监控中心'}}
* HK: {dashboard: {name: '監控中心'}}
* }
* CN: {dashboard: {name: '监控中心'}}
* HK: {dashboard: {name: '監控中心'}}
* }
**/
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
import fastEqual from 'fast-deep-equal'
import {getI18nKey} from '@/utils/routerUtil'
import '@/utils/Objects'
const {Item, SubMenu} = Menu
const resolvePath = (path, params = {}) => {
let _path = path
Object.entries(params).forEach(([key, value]) => {
_path = _path.replace(new RegExp(`:${key}`, 'g'), value)
})
return _path
}
const toRoutesMap = (routes) => {
const map = {}
routes.forEach(route => {
map[route.fullPath] = route
if (route.children && route.children.length > 0) {
const childrenMap = toRoutesMap(route.children)
Object.assign(map, childrenMap)
}
})
return map
}
export default {
name: 'IMenu',
props: {
@ -80,94 +57,65 @@ export default {
required: false,
default: false
},
i18n: Object,
openKeys: Array
i18n: Object
},
data () {
return {
openKeys: [],
selectedKeys: [],
sOpenKeys: [],
cachedOpenKeys: []
}
},
computed: {
menuTheme() {
return this.theme == 'light' ? this.theme : 'dark'
},
routesMap() {
return toRoutesMap(this.options)
rootSubmenuKeys: (vm) => {
let keys = []
vm.options.forEach(item => {
keys.push(item.path)
})
return keys
}
},
beforeMount() {
let CN = this.generateI18n(new Object(), this.options, 'name')
let US = this.generateI18n(new Object(), this.options, 'path')
this.$i18n.setLocaleMessage('CN', CN)
this.$i18n.setLocaleMessage('US', US)
if(this.i18n) {
Object.keys(this.i18n).forEach(key => {
this.$i18n.mergeLocaleMessage(key, this.i18n[key])
})
}
this.$emit('i18nComplete', this.$i18n._getMessages())
},
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])
})
}
this.formatOptions(this.options, '')
},
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 = []
this.cachedOpenKeys = this.openKeys
this.openKeys = []
} else {
this.sOpenKeys = this.cachedOpenKeys
this.openKeys = 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
}
renderIcon: function (h, icon) {
return !icon || icon == 'none' ? null : h(Icon, {props: {type: icon}})
},
renderMenuItem: function (h, menu) {
let tag = 'router-link'
const path = resolvePath(menu.fullPath, menu.meta.params)
let config = {props: {to: {path, query: menu.meta.query}, }, attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}}
if (menu.meta && menu.meta.link) {
tag = 'a'
config = {attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;', href: menu.meta.link, target: '_blank'}}
}
return h(
Item, {key: menu.fullPath},
[
h(tag, config,
h('router-link', {props: {to: menu.fullPath}},
[
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),
this.$t(getI18nKey(menu.fullPath))
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none'),
h('span', [this.$t(menu.fullPath.substring(1).replace(new RegExp('/', 'g'), '.') + '.name')])
]
)
]
@ -175,10 +123,10 @@ export default {
},
renderSubMenu: function (h, menu) {
let this_ = this
let subItem = [h('span', {slot: 'title', attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}},
let subItem = [h('span', {slot: 'title'},
[
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),
this.$t(getI18nKey(menu.fullPath))
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none'),
h('span', [this.$t(menu.fullPath.substring(1).replace(new RegExp('/', 'g'), '.') + '.name')])
]
)]
let itemArr = []
@ -215,32 +163,48 @@ export default {
return menuArr
},
formatOptions(options, parentPath) {
let this_ = this
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)
this_.formatOptions(route.children, route.fullPath)
}
})
},
updateMenu () {
this.selectedKeys = this.getSelectedKeys()
let openKeys = this.selectedKeys.filter(item => item !== '')
openKeys = openKeys.slice(0, openKeys.length -1)
if (!fastEqual(openKeys, this.sOpenKeys)) {
this.collapsed || this.mode === 'horizontal' ? this.cachedOpenKeys = openKeys : this.sOpenKeys = openKeys
onOpenChange (openKeys) {
const latestOpenKey = openKeys.find(key => this.openKeys.indexOf(key) === -1)
if (this.rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
this.openKeys = openKeys
} else {
this.openKeys = latestOpenKey ? [latestOpenKey] : []
}
},
getSelectedKeys() {
let matches = this.$route.matched
const route = matches[matches.length - 1]
let chose = this.routesMap[route.path]
if (chose && chose.meta && chose.meta.highlight) {
chose = this.routesMap[chose.meta.highlight]
const resolve = this.$router.resolve({path: chose.fullPath})
matches = (resolve.resolved && resolve.resolved.matched) || matches
updateMenu () {
let routes = this.$route.matched.concat()
const route = routes.pop()
this.selectedKeys = [this.getSelectedKey(route)]
let openKeys = []
routes.forEach((item) => {
openKeys.push(item.path)
})
this.collapsed || this.mode === 'horizontal' ? this.cachedOpenKeys = openKeys : this.openKeys = openKeys
},
getSelectedKey (route) {
if (route.meta.invisible && route.parent) {
return this.getSelectedKey(route.parent)
}
return matches.map(item => item.path)
return route.path
},
generateI18n(lang, options, valueKey) {
options.forEach(menu => {
let keys = menu.fullPath.substring(1).split('/').concat('name')
lang.assignProps(keys, menu[valueKey])
if (menu.children) {
this.generateI18n(lang, menu.children, valueKey)
}
})
return lang
}
},
render (h) {
@ -248,17 +212,15 @@ export default {
Menu,
{
props: {
theme: this.menuTheme,
theme: this.$props.theme,
mode: this.$props.mode,
selectedKeys: this.selectedKeys,
openKeys: this.openKeys ? this.openKeys : this.sOpenKeys
openKeys: this.openKeys,
selectedKeys: this.selectedKeys
},
on: {
'update:openKeys': (val) => {
this.sOpenKeys = val
},
click: (obj) => {
obj.selectedKeys = [obj.key]
openChange: this.onOpenChange,
select: (obj) => {
this.selectedKeys = obj.selectedKeys
this.$emit('select', obj)
}
}

View File

@ -1,6 +1,6 @@
<template>
<div :class="['page-header', layout, pageWidth]">
<div class="page-header-wide">
<div class="page-header">
<div :class="['page-header-wide', layout]">
<div class="breadcrumb">
<a-breadcrumb>
<a-breadcrumb-item :key="index" v-for="(item, index) in breadcrumb">
@ -11,7 +11,7 @@
<div class="detail">
<div class="main">
<div class="row">
<h1 v-if="showPageTitle && title" class="title">{{title}}</h1>
<h1 v-if="title" class="title">{{title}}</h1>
<div class="action"><slot name="action"></slot></div>
</div>
<div class="row">
@ -28,12 +28,11 @@
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'PageHeader',
props: {
title: {
type: [String, Boolean],
type: String,
required: false
},
breadcrumb: {
@ -50,11 +49,54 @@ export default {
},
},
computed: {
...mapState('setting', ['layout', 'showPageTitle', 'pageWidth'])
layout () {
return this.$store.state.setting.layout
}
}
}
</script>
<style lang="less" scoped>
@import "index";
.page-header{
background: #fff;
padding: 16px 24px;
border-bottom: 1px solid #e8e8e8;
.page-header-wide{
&.head{
margin: auto;
max-width: 1400px;
}
&.side{
}
.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: rgba(0,0,0,.85);
margin-bottom: 16px;
}
.content{
display: flex;
flex-wrap: wrap;
}
.extra{
display: flex;
}
}
}
}
}
</style>

View File

@ -1,40 +0,0 @@
.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;
}
}
}
}
}

View File

@ -35,11 +35,11 @@ export default {
color: @success-color;
}
.error {
color: @error-color;
color: red;
}
.title{
font-size: 24px;
color: @title-color;
color: rgba(0,0,0,.85);
font-weight: 500;
line-height: 32px;
margin-bottom: 16px;
@ -47,11 +47,11 @@ export default {
.desc{
font-size: 14px;
line-height: 22px;
color: @text-color-second;
color: rgba(0, 0, 0, 0.45);
margin-bottom: 24px;
}
.content{
background-color: @background-color-light;
background: #fafafa;
padding: 24px 40px;
border-radius: 2px;
text-align: left;

View File

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

View File

@ -1,6 +1,6 @@
<template>
<div class="setting-item">
<h3 v-if="title" class="title">{{title}}</h3>
<h3 class="title">{{title}}</h3>
<slot></slot>
</div>
</template>
@ -17,7 +17,7 @@ export default {
margin-bottom: 24px;
.title{
font-size: 14px;
color: @title-color;
color: rgba(0,0,0,.85);
line-height: 22px;
margin-bottom: 12px;
}

View File

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

View File

@ -1,15 +1,18 @@
<template>
<div class="standard-table">
<div class="alert">
<a-alert type="info" :show-icon="true" v-if="selectedRows">
<div class="message" slot="message">
已选择&nbsp;<a>{{selectedRows.length}}</a>&nbsp; <a class="clear" @click="onClear">清空</a>
<template v-for="(item, index) in needTotalList" >
<div v-if="item.needTotal" :key="index">
<a-alert type="info" :show-icon="true">
<div slot="message">
已选择&nbsp;<a style="font-weight: 600">{{selectedRows.length}}</a>&nbsp;&nbsp;&nbsp;
<div v-for="(item, index) in needTotalList" :key="index">
<div v-if="item.needTotal">
{{item.title}}总计&nbsp;
<a>{{item.customRender ? item.customRender(item.total) : item.total}}</a>
<a :key="index" style="font-weight: 600">
{{item.customRender ? item.customRender(item.total) : item.total}}
</a>&nbsp;&nbsp;
</div>
</template>
</div>
<a style="margin-left: 24px">清空</a>
</div>
</a-alert>
</div>
@ -20,20 +23,8 @@
:dataSource="dataSource"
:rowKey="rowKey"
:pagination="pagination"
:expandedRowKeys="expandedRowKeys"
:expandedRowRender="expandedRowRender"
@change="onChange"
:rowSelection="selectedRows ? {selectedRowKeys, onSelect, onSelectAll} : undefined"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: updateSelect}"
>
<template slot-scope="text, record, index" :slot="slot" v-for="slot in Object.keys($scopedSlots).filter(key => key !== 'expandedRowRender') ">
<slot :name="slot" v-bind="{text, record, index}"></slot>
</template>
<template :slot="slot" v-for="slot in Object.keys($slots)">
<slot :name="slot"></slot>
</template>
<template slot-scope="record, index, indent, expanded" :slot="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''">
<slot v-bind="{record, index, indent, expanded}" :name="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''"></slot>
</template>
</a-table>
</div>
</template>
@ -41,141 +32,57 @@
<script>
export default {
name: 'StandardTable',
props: {
bordered: Boolean,
loading: [Boolean, Object],
columns: Array,
dataSource: Array,
rowKey: {
type: [String, Function],
default: 'key'
},
pagination: {
type: [Object, Boolean],
default: true
},
selectedRows: Array,
expandedRowKeys: Array,
expandedRowRender: Function
},
props: ['bordered', 'loading', 'columns', 'dataSource', 'rowKey', 'pagination', 'selectedRows'],
data () {
return {
needTotalList: []
needTotalList: [],
selectedRowKeys: []
}
},
methods: {
equals(record1, record2) {
if (record1 === record2) {
return true
}
const {rowKey} = this
if (rowKey && typeof rowKey === 'string') {
return record1[rowKey] === record2[rowKey]
} else if (rowKey && typeof rowKey === 'function') {
return rowKey(record1) === rowKey(record2)
}
return false
},
contains(arr, item) {
if (!arr || arr.length === 0) {
return false
}
const {equals} = this
for (let i = 0; i < arr.length; i++) {
if (equals(arr[i], item)) {
return true
updateSelect (selectedRowKeys, selectedRows) {
this.selectedRowKeys = selectedRowKeys
let list = this.needTotalList
this.needTotalList = list.map(item => {
return {
...item,
total: selectedRows.reduce((sum, val) => {
return sum + val[item.dataIndex]
}, 0)
}
}
return false
},
onSelectAll(selected, rows) {
const {getKey, contains} = this
const unselected = this.dataSource.filter(item => !contains(rows, item, this.rowKey))
const _selectedRows = this.selectedRows.filter(item => !contains(unselected, item, this.rowKey))
const set = {}
_selectedRows.forEach(item => set[getKey(item)] = item)
rows.forEach(item => set[getKey(item)] = item)
const _rows = Object.values(set)
this.$emit('update:selectedRows', _rows)
this.$emit('selectedRowChange', _rows.map(item => getKey(item)), _rows)
},
getKey(record) {
const {rowKey} = this
if (!rowKey || !record) {
return undefined
}
if (typeof rowKey === 'string') {
return record[rowKey]
} else {
return rowKey(record)
}
},
onSelect(record, selected) {
const {equals, selectedRows, getKey} = this
const _selectedRows = selected ? [...selectedRows, record] : selectedRows.filter(row => !equals(row, record))
this.$emit('update:selectedRows', _selectedRows)
this.$emit('selectedRowChange', _selectedRows.map(item => getKey(item)), _selectedRows)
})
this.$emit('change', selectedRowKeys, selectedRows)
},
initTotalList (columns) {
return columns.filter(item => item.needTotal)
.map(item => {
return {
...item,
total: 0
}
})
},
onClear() {
this.$emit('update:selectedRows', [])
this.$emit('selectedRowChange', [], [])
this.$emit('clear')
},
onChange(pagination, filters, sorter, {currentDataSource}) {
this.$emit('change', pagination, filters, sorter, {currentDataSource})
const totalList = []
columns.forEach(column => {
if (column.needTotal) {
totalList.push({...column, total: 0})
}
})
return totalList
}
},
created () {
this.needTotalList = this.initTotalList(this.columns)
},
watch: {
selectedRows (selectedRows) {
'selectedRows': function (selectedRows) {
this.needTotalList = this.needTotalList.map(item => {
return {
...item,
total: selectedRows.reduce((sum, val) => {
let v
try{
v = val[item.dataIndex] ? val[item.dataIndex] : eval(`val.${item.dataIndex}`);
}catch(_){
v = val[item.dataIndex];
}
v = !isNaN(parseFloat(v)) ? parseFloat(v) : 0;
return sum + v
return sum + val[item.dataIndex]
}, 0)
}
})
}
},
computed: {
selectedRowKeys() {
return this.selectedRows.map(record => this.getKey(record))
},
}
}
</script>
<style scoped lang="less">
.standard-table{
<style scoped>
.alert{
margin-bottom: 16px;
.message{
a{
font-weight: 600;
}
}
.clear{
float: right;
}
}
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,49 +0,0 @@
<template>
<a-table :data-source="apiSource" :pagination="false">
<h2 v-if="title" style="margin: 0 16px 0" slot="title">{{title}}</h2>
<a-table-column width="20%" data-index="param" title="参数">
<div slot-scope="text" v-html="text"></div>
</a-table-column>
<a-table-column width="50%" data-index="desc" title="说明">
<div slot-scope="text" v-html="text"></div>
</a-table-column>
<a-table-column v-if="isApi" width="15%" data-index="type" title="类型">
<div slot-scope="text" v-html="text"></div>
</a-table-column>
<a-table-column v-if="isApi" width="15%" data-index="default" title="默认值">
<div slot-scope="text" v-html="text"></div>
</a-table-column>
<a-table-column v-if="!isApi" width="30%" data-index="callback" title="回调函数">
<div slot-scope="text" v-html="text"></div>
</a-table-column>
</a-table>
</template>
<script>
export default {
name: 'ApiTable',
props: {
title: {
type: String,
default: 'API'
},
type: {
type: String,
default: 'api',
validator(value) {
return ['api', 'event'].includes(value)
}
},
apiSource: Array
},
computed: {
isApi() {
return this.type === 'api'
}
}
}
</script>
<style scoped>
</style>

View File

@ -49,9 +49,9 @@ export default {
.task-group{
width: 33.33%;
padding: 8px 8px;
background-color: @background-color-light;
background-color: #e1e4e8;
border-radius: 6px;
border: 1px solid @shadow-color;
border: 1px solid #d1d4d8;
.task-head{
margin-bottom: 8px;
.title{
@ -62,7 +62,7 @@ export default {
margin: 0 8px;
font-size: 12px;
padding: 2px 6px;
background-color: @base-bg-color;
background-color: rgba(27,31,35,0.15);
}
}
.actions{

View File

@ -14,11 +14,12 @@ export default {
<style lang="less" scoped>
.task-item{
margin-bottom: 16px;
box-shadow: 0 1px 1px @shadow-color;
box-shadow: 0 1px 1px rgba(27,31,35,0.1);
border-radius: 6px;
color: #24292e;
& :hover{
cursor: move;
box-shadow: 0 1px 2px @shadow-color;
box-shadow: 0 1px 1px rgba(27,31,35,0.15);
border-radius: 6px;
}
}

View File

@ -4,7 +4,7 @@
@click="go"
>
<span :style="titleStyle">{{title}}</span>
<a-icon v-if="icon" :style="iconStyle" :type="icon" />
<a-icon :style="iconStyle" :type="icon" />
<slot></slot>
</div>
</template>
@ -52,7 +52,7 @@ export default {
:global{
.ant-steps-item-process{
.linkable{
color: @primary-color;
color: #40a9ff;
}
}
}

View File

@ -1,7 +1,9 @@
<template>
<div class="avatar-list">
<slot>
</slot>
<ul>
<slot>
</slot>
</ul>
</div>
</template>
@ -35,7 +37,7 @@ const Item = {
return h(
'li',
{class: 'avatar-item'},
[this.$props.tips ? h(ATooltip, {props: {title: this.$props.tips}}, [avatar]) : avatar]
[!this.$props.tips ? h(ATooltip, {props: {title: this.$props.tips}}, [avatar]) : avatar]
)
}
}
@ -45,9 +47,10 @@ export default {
}
</script>
<style lang="less" scoped>
<style lang="less">
.avatar-list {
display: inline-block;
ul {
display: inline-block;
margin-left: 8px;
font-size: 0;
@ -65,5 +68,6 @@ export default {
}
}
}
}
}
</style>

View File

@ -102,7 +102,7 @@ export default {
.detail-list{
.title {
font-size: 16px;
color: @title-color;
color: rgba(0,0,0,.85);
font-weight: bolder;
margin-bottom: 16px;
}
@ -111,7 +111,7 @@ export default {
line-height: 20px;
padding-bottom: 16px;
margin-right: 8px;
color: @title-color;
color: rgba(0,0,0,.85);
white-space: nowrap;
display: table-cell;
&:after {
@ -125,13 +125,13 @@ export default {
line-height: 22px;
width: 100%;
padding-bottom: 16px;
color: @text-color;
color: rgba(0,0,0,.65);
display: table-cell;
}
&.small{
.title{
font-size: 14px;
color: @text-color;
color: rgba(0,0,0,.65);
font-weight: normal;
margin-bottom: 12px;
}

View File

@ -1,14 +1,14 @@
<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">
<div :class="['mask', openDrawer ? 'open' : 'close']" @click="close"></div>
<div :class="['drawer', placement, openDrawer ? 'open' : 'close']">
<div ref="drawer" class="content">
<slot></slot>
</div>
<div v-if="showHandler" :class="['handler-container', placement, visible ? 'open' : 'close']" ref="handler" @click="toggle">
<div v-if="showHandler" :class="['handler-container', placement, openDrawer ? 'open' : 'close']" ref="handler" @click="handle">
<slot v-if="$slots.handler" name="handler"></slot>
<div v-else class="handler">
<a-icon :type="visible ? 'close' : 'bars'" />
<a-icon :type="openDrawer ? 'close' : 'bars'" />
</div>
</div>
</div>
@ -20,14 +20,11 @@ export default {
name: 'Drawer',
data () {
return {
drawerWidth: 0
}
},
model: {
prop: 'visible',
event: 'change'
},
props: {
visible: {
openDrawer: {
type: Boolean,
required: false,
default: false
@ -43,6 +40,18 @@ export default {
default: true
}
},
mounted () {
this.drawerWidth = this.getDrawerWidth()
},
watch: {
'drawerWidth': function () {
if (this.placement === 'left') {
//this.$refs.handler.style.left = val + 'px'
} else {
//this.$refs.handler.style.right = val + 'px'
}
}
},
methods: {
open () {
this.$emit('change', true)
@ -50,8 +59,11 @@ export default {
close () {
this.$emit('change', false)
},
toggle () {
this.$emit('change', !this.visible)
handle () {
this.$emit('change', !this.openDrawer)
},
getDrawerWidth () {
return this.$refs.drawer.clientWidth
}
}
}
@ -64,7 +76,7 @@ export default {
right: 0;
bottom: 0;
top: 0;
background-color: @shadow-color;
background-color: rgba(0, 0, 0, 0.2);
transition: all 0.5s;
z-index: 100;
&.open{
@ -76,14 +88,14 @@ export default {
}
.drawer{
position: fixed;
height: 100%;
transition: all 0.5s;
height: 100vh;
z-index: 100;
&.left{
left: 0px;
&.open{
.content{
box-shadow: 2px 0 8px @shadow-color;
box-shadow: 2px 0 8px rgba(0,0,0,.15);
}
}
&.close{
@ -97,7 +109,7 @@ export default {
}
&.open{
.content{
box-shadow: -2px 0 8px @shadow-color;
box-shadow: -2px 0 8px rgba(0,0,0,.15);
}
}
&.close{
@ -107,8 +119,7 @@ export default {
}
.content{
display: inline-block;
height: 100vh;
overflow-y: auto;
height: 100%;
}
.handler-container{
position: absolute;
@ -121,9 +132,9 @@ export default {
.handler {
height: 40px;
width: 40px;
background-color: @base-bg-color;
background-color: #fff;
font-size: 26px;
box-shadow: 0 2px 8px @shadow-color;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
line-height: 40px;
}
&.left{

View File

@ -21,9 +21,9 @@ export default {
width: 100%;
bottom: 0;
right: 0;
box-shadow: 0 -1px 2px @shadow-color;
background: @base-bg-color;
border-top: 1px solid @border-color-split;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
background: #fff;
border-top: 1px solid #e8e8e8;
padding: 12px 24px;
z-index: 9;
}

View File

@ -20,13 +20,13 @@ export default {
flex-shrink: 0;
align-self: center;
span{
color: @text-color-second;
color: rgba(0,0,0,.45);
display: inline-block;
font-size: 14px;
margin-bottom: 4px;
}
p{
color: @text-color;
color: rgba(0,0,0,.85);
font-size: 24px;
margin: 0;
}

View File

@ -1,24 +1,18 @@
<template>
<transition
v-if="!disabled"
:enter-active-class="`animated ${enterAnimate} page-toggle-enter-active`"
:leave-active-class="`animated ${leaveAnimate} page-toggle-leave-active`"
>
<slot></slot>
</transition>
<div v-else><slot></slot></div>
</template>
<script>
import {preset as animates} from '@/config/default/animate.config'
import {animates} from '@/config/default'
export default {
name: 'PageToggleTransition',
props: {
disabled: {
type: Boolean,
default: false
},
animate: {
type: String,
validator(value) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,90 +0,0 @@
// antd 配置
const ANTD = {
primary: {
color: '#1890ff',
warning: '#faad14',
success: '#52c41a',
error: '#f5222d',
light: {
menuColors: ['#000c17', '#001529', '#002140']
},
dark: {
menuColors: ['#000c17', '#001529', '#002140']
},
night: {
menuColors: ['#151515', '#1f1f1f', '#1e1e1e'],
}
},
theme: {
dark: {
'layout-body-background': '#f0f2f5',
'body-background': '#fff',
'component-background': '#fff',
'heading-color': 'rgba(0, 0, 0, 0.85)',
'text-color': 'rgba(0, 0, 0, 0.65)',
'text-color-inverse': '#fefefe',
'text-color-secondary': 'rgba(0, 0, 0, 0.45)',
'shadow-color': 'rgba(0, 0, 0, 0.15)',
'border-color-split': '#f0f0f0',
'border-color-base': '#d9d9d9',
'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-selected-item-icon-color': '#fefefe',
'menu-dark-arrow-color': '#fefefe',
'btn-primary-color': '#fefefe',
},
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': '#fefefe',
'text-color-secondary': 'rgba(0, 0, 0, 0.45)',
'shadow-color': 'rgba(0, 0, 0, 0.15)',
'border-color-split': '#f0f0f0',
'border-color-base': '#d9d9d9',
'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-selected-item-icon-color': '#fefefe',
'menu-dark-arrow-color': '#fefefe',
'btn-primary-color': '#fefefe',
},
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': '#fefefe',
'text-color-secondary': 'rgba(255, 255, 255, 0.45)',
'shadow-color': 'rgba(255, 255, 255, 0.15)',
'border-color-split': '#303030',
'border-color-base': '#282828',
'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-selected-item-icon-color': '#fefefe',
'menu-dark-arrow-color': '#fefefe',
'btn-primary-color': '#fefefe',
}
}
}
module.exports = ANTD

View File

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

View File

@ -1,35 +0,0 @@
// 此配置为系统默认设置需修改的设置项在src/config/config.js中添加修改项即可。也可直接在此文件中修改。
module.exports = {
lang: 'CN', //语言,可选 CN(简体)、HK(繁体)、US(英语),也可扩展其它语言
theme: { //主题
color: '#1890ff', //主题色
mode: 'dark', //主题模式 可选 dark、 light 和 night
success: '#52c41a', //成功色
warning: '#faad14', //警告色
error: '#f5222f', //错误色
},
layout: 'side', //导航布局,可选 side 和 head分别为侧边导航和顶部导航
fixedHeader: false, //固定头部状态栏true:固定false:不固定
fixedSideBar: true, //固定侧边栏true:固定false:不固定
fixedTabs: false, //固定页签头true:固定false:不固定
pageWidth: 'fixed', //内容区域宽度fixed:固定宽度fluid:流式宽度
weekMode: false, //色弱模式true:开启false:不开启
multiPage: false, //多页签模式true:开启false:不开启
cachePage: true, //是否缓存页面数据仅多页签模式下生效true 缓存, false 不缓存
hideSetting: false, //隐藏设置抽屉true:隐藏false:不隐藏
systemName: 'Vue Antd Admin', //系统名称
copyright: '2018 ICZER 工作室出品', //copyright
asyncRoutes: false, //异步加载路由true:开启false:不开启
showPageTitle: true, //是否显示页面标题PageLayout 布局中的页面标题true:显示false:不显示
filterMenu: true, //根据权限过滤菜单true:过滤false:不过滤
animate: { //动画设置
disabled: false, //禁用动画true:禁用false:启用
name: 'bounce', //动画效果,支持的动画效果可参考 ./animate.config.js
direction: 'left' //动画方向,切换页面时动画的方向,参考 ./animate.config.js
},
footerLinks: [ //页面底部链接,{link: '链接地址', name: '名称/显示文字', icon: '图标,支持 ant design vue 图标库'}
{link: 'https://pro.ant.design', name: 'Pro首页'},
{link: 'https://github.com/iczer/vue-antd-admin', icon: 'github'},
{link: 'https://ant.design', name: 'Ant Design'}
],
}

View File

@ -0,0 +1,21 @@
module.exports = {
lang: 'CN',
themeColor: '#1890ff',
theme: 'dark',
layout: 'side',
fixedHeader: false,
fixedSideBar: true,
weekMode: false,
multiPage: false,
systemName: 'Vue Antd Admin',
copyright: '2018 ICZER 工作室出品',
animate: {
name: 'bounce',
direction: 'left'
},
footerLinks: [
{link: 'https://pro.ant.design', name: 'Pro首页'},
{link: 'https://github.com/iczer/vue-antd-admin', icon: 'github'},
{link: 'https://ant.design', name: 'Ant Design'}
],
}

View File

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

View File

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

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