├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── build └── index.js ├── jest.config.js ├── jsconfig.json ├── package.json ├── plopfile.js ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── server ├── app.js ├── model │ ├── init.js │ ├── menus.js │ ├── roles.js │ ├── user-logs.js │ ├── users-roles.js │ └── users.js ├── mysql.sql ├── package.json ├── router │ ├── index.js │ ├── menu.js │ ├── role.js │ ├── user-log.js │ └── user.js └── utils │ └── tools.js ├── src ├── App.vue ├── api │ ├── system │ │ ├── menu.js │ │ ├── role.js │ │ ├── user-log.js │ │ └── user.js │ └── user.js ├── assets │ ├── 401_images │ │ └── 401.gif │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ ├── custom-theme │ │ ├── fonts │ │ │ ├── element-icons.ttf │ │ │ └── element-icons.woff │ │ └── index.css │ └── pays.png ├── components │ ├── BackToTop │ │ └── index.vue │ ├── Breadcrumb │ │ └── index.vue │ ├── Charts │ │ ├── Keyboard.vue │ │ ├── LineMarker.vue │ │ ├── MixChart.vue │ │ └── mixins │ │ │ └── resize.js │ ├── GithubCorner │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── HeaderSearch │ │ └── index.vue │ ├── IconSelect │ │ ├── index.vue │ │ └── requireIcons.js │ ├── Pagination │ │ └── index.vue │ ├── RightPanel │ │ └── index.vue │ ├── Screenfull │ │ └── index.vue │ ├── SizeSelect │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ ├── TextHoverEffect │ │ └── Mallki.vue │ └── ThemePicker │ │ └── index.vue ├── directive │ └── permission │ │ ├── index.js │ │ └── permission.js ├── filters │ └── index.js ├── icons │ ├── index.js │ ├── svg │ │ ├── 404.svg │ │ ├── bug.svg │ │ ├── build.svg │ │ ├── calendar.svg │ │ ├── cascader.svg │ │ ├── chart.svg │ │ ├── checkbox.svg │ │ ├── clipboard.svg │ │ ├── code.svg │ │ ├── color.svg │ │ ├── component.svg │ │ ├── dashboard.svg │ │ ├── date-range.svg │ │ ├── date.svg │ │ ├── dict.svg │ │ ├── documentation.svg │ │ ├── download.svg │ │ ├── drag copy.svg │ │ ├── drag.svg │ │ ├── druid.svg │ │ ├── edit.svg │ │ ├── education.svg │ │ ├── email.svg │ │ ├── example.svg │ │ ├── excel.svg │ │ ├── exit-fullscreen.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── form.svg │ │ ├── fullscreen.svg │ │ ├── github.svg │ │ ├── guide.svg │ │ ├── icon.svg │ │ ├── input.svg │ │ ├── international.svg │ │ ├── job.svg │ │ ├── language.svg │ │ ├── link.svg │ │ ├── list.svg │ │ ├── lock.svg │ │ ├── log.svg │ │ ├── logininfor.svg │ │ ├── message.svg │ │ ├── money.svg │ │ ├── monitor.svg │ │ ├── nested.svg │ │ ├── network.svg │ │ ├── number.svg │ │ ├── online.svg │ │ ├── pass.svg │ │ ├── password.svg │ │ ├── pdf.svg │ │ ├── people.svg │ │ ├── peoples.svg │ │ ├── phone.svg │ │ ├── post.svg │ │ ├── qq.svg │ │ ├── question.svg │ │ ├── radio.svg │ │ ├── rate.svg │ │ ├── row.svg │ │ ├── search.svg │ │ ├── select.svg │ │ ├── server.svg │ │ ├── shopping.svg │ │ ├── size.svg │ │ ├── skill.svg │ │ ├── slider.svg │ │ ├── star.svg │ │ ├── swagger.svg │ │ ├── switch.svg │ │ ├── system.svg │ │ ├── tab.svg │ │ ├── table.svg │ │ ├── textarea.svg │ │ ├── theme.svg │ │ ├── time-range.svg │ │ ├── time.svg │ │ ├── tool.svg │ │ ├── tree-table.svg │ │ ├── tree.svg │ │ ├── upload.svg │ │ ├── user.svg │ │ ├── validCode.svg │ │ ├── vip.svg │ │ ├── wechat.svg │ │ └── zip.svg │ └── svgo.yml ├── layout │ ├── components │ │ ├── AppMain.vue │ │ ├── Navbar.vue │ │ ├── Settings │ │ │ └── index.vue │ │ ├── Sidebar │ │ │ ├── FixiOSBug.js │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ ├── Logo.vue │ │ │ ├── SidebarItem.vue │ │ │ └── index.vue │ │ ├── TagsView │ │ │ ├── ScrollPane.vue │ │ │ └── index.vue │ │ └── index.js │ ├── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── main.js ├── permission.js ├── router │ └── index.js ├── settings.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── permission.js │ │ ├── settings.js │ │ ├── tagsView.js │ │ └── user.js ├── styles │ ├── btn.scss │ ├── element-ui.scss │ ├── element-variables.scss │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── auth.js │ ├── costum.js │ ├── get-page-title.js │ ├── index.js │ ├── open-window.js │ ├── permission.js │ ├── request.js │ ├── scroll-to.js │ └── validate.js └── views │ ├── dashboard │ ├── components │ │ ├── BarChart.vue │ │ ├── LineChart.vue │ │ ├── PanelGroup.vue │ │ ├── PieChart.vue │ │ ├── RaddarChart.vue │ │ ├── TransactionTable.vue │ │ └── mixins │ │ │ └── resize.js │ └── index.vue │ ├── demo │ ├── demo-1 │ │ ├── demo-1-1 │ │ │ └── index.vue │ │ └── index.vue │ └── demo-2 │ │ ├── demo-2-1 │ │ └── index.vue │ │ └── index.vue │ ├── error-page │ ├── 401.vue │ └── 404.vue │ ├── login │ ├── auth-redirect.vue │ └── index.vue │ ├── redirect │ └── index.vue │ └── system │ ├── log │ └── index.vue │ ├── menu │ └── index.vue │ ├── role │ └── index.vue │ └── user │ └── index.vue ├── tests └── unit │ ├── .eslintrc.js │ ├── components │ ├── Hamburger.spec.js │ └── SvgIcon.spec.js │ └── utils │ ├── formatTime.spec.js │ ├── parseTime.spec.js │ └── validate.spec.js └── vue.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/api' 6 | 7 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, 8 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled. 9 | # It only does one thing by converting all import() to require(). 10 | # This configuration can significantly increase the speed of hot updates, 11 | # when you have a large number of pages. 12 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/app.js 13 | 14 | VUE_CLI_BABEL_TRANSPILE_MODULES = true 15 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/api' 6 | 7 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | **/*.log 8 | 9 | tests/**/coverage/ 10 | tests/e2e/reports 11 | selenium-debug.log 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.local 21 | 22 | package-lock.json 23 | yarn.lock 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present limuen.cn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | [vue-admin](https://admin.limuen.cn) 是一个后台前端解决方案,它基于 [vue](https://github.com/vuejs/vue) 和 [element-ui](https://github.com/ElemeFE/element)以及[express](https://www.expressjs.com.cn/)实现。它使用了最新的前端技术栈,内置了动态路由,权限验证,并提供了简单的后端API服务,开箱即用,帮助你快速构建前后端分离动态路由和权限模型。 4 | 5 | 本项目通过[vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)精简改造,移除了大部分组件,如需要其他组件,可前往搬运。 6 | 7 | 8 | - [github仓库](https://github.com/limuen/vue-element-admin-express) 9 | 10 | 11 | ## 前序准备 12 | 13 | 你需要在本地安装 [node](http://nodejs.org/) 、[git](https://git-scm.com/) 和 [mysql](https://www.mysql.com/)。本项目技术栈基于 [ES2015+](http://es6.ruanyifeng.com/)、[vue](https://cn.vuejs.org/index.html)、[vuex](https://vuex.vuejs.org/zh-cn/)、[vue-router](https://router.vuejs.org/zh-cn/) 、[vue-cli](https://github.com/vuejs/vue-cli) 、[axios](https://github.com/axios/axios) 、 [element-ui](https://github.com/ElemeFE/element) 和 [express](https://www.expressjs.com.cn/),提前了解和学习这些知识会对使用本项目有很大的帮助。 14 | 15 | 16 | ## 功能 17 | 18 | ``` 19 | - 登录 / 注销 20 | 21 | - 系统管理 22 | - 用户管理 23 | - 菜单管理 24 | - 角色管理 25 | - 登录日志 26 | ``` 27 | 28 | ## 服务端 29 | 30 | ```bash 31 | # 进入server端目录 32 | cd vue-admin/server 33 | 34 | # 安装依赖 35 | npm install 36 | 37 | # 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 38 | npm install --registry=https://registry.npm.taobao.org 39 | 40 | # vue-admin/server/mysql.sql 导入数据表到mysql数据库 41 | # vue-admin/server/model/init.js 配置数据库信息:数据库名,登录账号,登录密码,ip,端口 42 | 43 | # 启动 默认绑定ip:127.0.0.1 端口:3001, 可以在app.js指定你需要的ip和端口 44 | npm run start 45 | 46 | ``` 47 | 48 | 接口地址:http://127.0.0.1:3001 49 | 50 | ## 前台 51 | 52 | ```bash 53 | # 克隆项目 54 | git clone https://github.com/limuen/vue-admin.git 55 | 56 | # 进入项目目录 57 | cd vue-admin 58 | 59 | # 安装依赖 60 | npm install 61 | 62 | # 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 63 | npm install --registry=https://registry.npm.taobao.org 64 | 65 | # 启动服务 66 | npm run dev 67 | ``` 68 | 69 | 浏览器访问 http://localhost:9527 70 | 71 | ## 发布 72 | 73 | ```bash 74 | # 构建测试环境 75 | npm run build:stage 76 | 77 | # 构建生产环境 78 | npm run build:prod 79 | ``` 80 | 81 | ## 其它 82 | 83 | ```bash 84 | # 预览发布环境效果 85 | npm run preview 86 | 87 | # 预览发布环境效果 + 静态资源分析 88 | npm run preview -- --report 89 | 90 | # 代码格式检查 91 | npm run lint 92 | 93 | # 代码格式检查并自动修复 94 | npm run lint -- --fix 95 | ``` 96 | 97 | 98 | ## 支持浏览器版本 99 | 100 | Modern browsers and Internet Explorer 10+. 101 | 102 | | IE / Edge | Firefox | Chrome | Safari | 103 | | --------- | --------- | --------- | --------- | 104 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions 105 | 106 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const IS_PROD = process.env.NODE_ENV === 'production' 2 | const plugins = [] 3 | if(IS_PROD){ 4 | plugins.unshift([ 5 | 'transform-remove-console', 6 | { 7 | exclude: ['error', 'warn'] 8 | } 9 | ]) 10 | } 11 | module.exports = { 12 | presets: [ 13 | '@vue/app' 14 | ], 15 | plugins 16 | } -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | const viewGenerator = require('./plop-templates/view/prompt') 2 | const componentGenerator = require('./plop-templates/component/prompt') 3 | const storeGenerator = require('./plop-templates/store/prompt.js') 4 | 5 | module.exports = function(plop) { 6 | plop.setGenerator('view', viewGenerator) 7 | plop.setGenerator('component', componentGenerator) 8 | plop.setGenerator('store', storeGenerator) 9 | } 10 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limuen/vue-admin/b5589994cbb1e6177f53c2e701f3e2e378b862a3/public/favicon.ico -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const expressJwt = require('express-jwt') 4 | const bodyParser = require('body-parser') 5 | const userRouter = require('./router/user') 6 | const roleRouter = require('./router/role') 7 | const menuRouter = require('./router/menu') 8 | const logRouter = require('./router/user-log') 9 | 10 | app.use(expressJwt({ 11 | secret: 'limuen.cn' // 签名的密钥 或 PublicKey 12 | }).unless({ 13 | path: ['/user/login'] // 指定路径不经过 Token 解析 14 | })) 15 | 16 | app.use(bodyParser.urlencoded({ 17 | extends: false 18 | })) 19 | 20 | app.use(bodyParser.json()) 21 | 22 | 23 | app.use(function(err, req, res, next) { 24 | if (err.name === 'UnauthorizedError') { 25 | res.status(401).send('token已失效') 26 | } else { 27 | res.status(500).send('服务器错误') 28 | } 29 | }) 30 | 31 | // 使用路由 / 是路由指向名称 32 | app.use('/user', userRouter) 33 | app.use('/user-log', logRouter) 34 | app.use('/role', roleRouter) 35 | app.use('/menu', menuRouter) 36 | 37 | // 配置服务端口 38 | const port = 3001 39 | const hostname = '127.0.0.1' 40 | app.listen(port, hostname, () => { 41 | console.log(`Server running at http://${hostname}:${port}/`) 42 | }) 43 | 44 | -------------------------------------------------------------------------------- /server/model/init.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize') 2 | 3 | const sequelize = new Sequelize( 4 | 'limuen', // 数据库名 5 | 'root', // 用户名 6 | 'limuen520', // 密码 7 | { 8 | 'dialect': 'mysql', // 数据库类型 9 | 'host': '127.0.0.1', // ip 10 | 'port': 3306, // 端口 11 | define: { 12 | timestamps: false 13 | }, 14 | timezone: '+08:00' // 东八时区 15 | } 16 | ) 17 | 18 | module.exports = sequelize 19 | -------------------------------------------------------------------------------- /server/model/menus.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize') 2 | const moment = require('moment') 3 | const sequelize = require('./init') 4 | const tools = require('../utils/tools') 5 | 6 | // 定义表的模型 7 | const MenusModel = sequelize.define('menus', { 8 | menu_id: { 9 | type: Sequelize.INTEGER, 10 | primaryKey: true, 11 | autoIncrement: true 12 | }, 13 | parent_id: { 14 | type: Sequelize.INTEGER, 15 | defaultValue: 0 16 | }, 17 | title: { 18 | type: Sequelize.STRING(255) 19 | }, 20 | sort: { 21 | type: Sequelize.INTEGER, 22 | defaultValue: 0 23 | }, 24 | type: { 25 | type: Sequelize.CHAR(1) 26 | }, 27 | icon: { 28 | type: Sequelize.STRING(255) 29 | }, 30 | name: { 31 | type: Sequelize.STRING(255) 32 | }, 33 | component: { 34 | type: Sequelize.STRING(255) 35 | }, 36 | path: { 37 | type: Sequelize.STRING(255) 38 | }, 39 | permission: { 40 | type: Sequelize.STRING(255) 41 | }, 42 | redirect: { 43 | type: Sequelize.STRING(255) 44 | }, 45 | hidden: { 46 | type: Sequelize.TINYINT(1) 47 | }, 48 | update_time: { 49 | type: Sequelize.DATE, 50 | get() { 51 | return this.getDataValue('update_time') ? moment(this.getDataValue('update_time')).format('YYYY-MM-DD HH:mm:ss') : null 52 | } 53 | }, 54 | create_time: { 55 | type: Sequelize.DATE, 56 | defaultValue: Sequelize.NOW, 57 | get() { 58 | return moment(this.getDataValue('create_time')).format('YYYY-MM-DD HH:mm:ss') 59 | } 60 | } 61 | }) 62 | 63 | MenusModel.getListTree = async function(where = {}) { 64 | const menus = await MenusModel.findAll({ 65 | where: where, 66 | order: [['sort', 'DESC']] 67 | }) 68 | const menusArr = menus.map(function(item) { 69 | return item.get({ plain: true }) 70 | }) 71 | return tools.getTreeData(menusArr, null, 'menu_id') 72 | } 73 | 74 | module.exports = MenusModel 75 | -------------------------------------------------------------------------------- /server/model/roles.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize') 2 | const moment = require('moment') 3 | const sequelize = require('./init') 4 | const UsersRolesModel = require('./users-roles') 5 | 6 | // 定义表的模型 7 | const RolesModel = sequelize.define('roles', { 8 | role_id: { 9 | type: Sequelize.INTEGER, 10 | primaryKey: true, 11 | autoIncrement: true 12 | }, 13 | role_name: { 14 | type: Sequelize.STRING(255) 15 | }, 16 | remark: { 17 | type: Sequelize.STRING(255) 18 | }, 19 | status: { 20 | type: Sequelize.TINYINT, 21 | defaultValue: 0 22 | }, 23 | menu_ids: { 24 | type: Sequelize.TEXT, 25 | set(val) { 26 | this.setDataValue('menu_ids', val && val.length > 0 ? JSON.stringify(val) : JSON.stringify([])) 27 | }, 28 | get() { 29 | return this.getDataValue('menu_ids') ? JSON.parse(this.getDataValue('menu_ids')) : [] 30 | } 31 | }, 32 | buttons: { 33 | type: Sequelize.TEXT, 34 | set(val) { 35 | this.setDataValue('buttons', val && val.length > 0 ? JSON.stringify(val) : JSON.stringify([])) 36 | }, 37 | get() { 38 | return this.getDataValue('buttons') ? JSON.parse(this.getDataValue('buttons')) : [] 39 | } 40 | }, 41 | update_time: { 42 | type: Sequelize.DATE, 43 | get() { 44 | return this.getDataValue('update_time') ? moment(this.getDataValue('update_time')).format('YYYY-MM-DD HH:mm:ss') : null 45 | } 46 | }, 47 | create_time: { 48 | type: Sequelize.DATE, 49 | defaultValue: Sequelize.NOW, 50 | get() { 51 | return moment(this.getDataValue('create_time')).format('YYYY-MM-DD HH:mm:ss') 52 | } 53 | } 54 | }) 55 | 56 | RolesModel.delRole = async function(role_ids) { 57 | const t = await sequelize.transaction() 58 | try { 59 | await RolesModel.destroy({ 60 | where: { role_id: role_ids } 61 | }) 62 | await UsersRolesModel.destroy({ 63 | where: { role_id: role_ids } 64 | }) 65 | t.commit() 66 | return true 67 | } catch (e) { 68 | t.rollback() 69 | return false 70 | } 71 | } 72 | 73 | module.exports = RolesModel 74 | -------------------------------------------------------------------------------- /server/model/user-logs.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize') 2 | const moment = require('moment') 3 | const sequelize = require('./init') 4 | 5 | // 定义表的模型 6 | const UserLogsModel = sequelize.define('user_logs', { 7 | user_log_id: { 8 | type: Sequelize.INTEGER, 9 | primaryKey: true, 10 | autoIncrement: true 11 | }, 12 | user_id: { 13 | type: Sequelize.INTEGER 14 | }, 15 | ip: { 16 | type: Sequelize.STRING(255) 17 | }, 18 | ua: { 19 | type: Sequelize.STRING(500) 20 | }, 21 | create_time: { 22 | type: Sequelize.DATE, 23 | defaultValue: Sequelize.NOW, 24 | get() { 25 | return moment(this.getDataValue('create_time')).format('YYYY-MM-DD HH:mm:ss') 26 | } 27 | } 28 | }) 29 | 30 | module.exports = UserLogsModel 31 | -------------------------------------------------------------------------------- /server/model/users-roles.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize') 2 | const moment = require('moment') 3 | const sequelize = require('./init') 4 | 5 | // 定义表的模型 6 | const UsresRolesModel = sequelize.define('users_roles', { 7 | user_role_id: { 8 | type: Sequelize.INTEGER, 9 | primaryKey: true, 10 | autoIncrement: true 11 | }, 12 | role_id: { 13 | type: Sequelize.INTEGER 14 | }, 15 | user_id: { 16 | type: Sequelize.INTEGER 17 | }, 18 | create_time: { 19 | type: Sequelize.DATE, 20 | defaultValue: Sequelize.NOW, 21 | get() { 22 | return moment(this.getDataValue('create_time')).format('YYYY-MM-DD HH:mm:ss') 23 | } 24 | } 25 | }) 26 | 27 | module.exports = UsresRolesModel 28 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "node服务端", 5 | "scripts": { 6 | "start": "node ./app.js", 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "limuen-cn", 10 | "license": "MIT", 11 | "dependencies": { 12 | "body-parser": "^1.19.0", 13 | "express": "^4.17.1", 14 | "express-jwt": "^5.3.3", 15 | "moment": "^2.25.3", 16 | "mysql2": "^2.1.0", 17 | "sequelize": "^5.21.9" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/router/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | 4 | router.use((req, res, next) => { 5 | console.log('路由执行成功啦~~~', Date.now()) 6 | next() 7 | }) 8 | 9 | router.get('/', (req, res, next) => { 10 | res.json({ 11 | status: 200, 12 | data: '请求成功' 13 | }) 14 | }) 15 | 16 | router.get('/data', (req, res, next) => { 17 | res.json({ 18 | status: 200, 19 | data: [1, 2, 3, 4, 5, 6, 7] 20 | }) 21 | }) 22 | 23 | module.exports = router 24 | -------------------------------------------------------------------------------- /server/router/menu.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const MenusModel = require('../model/menus') 4 | const Sequelize = require('sequelize') 5 | const Op = Sequelize.Op 6 | 7 | router.get('/list', (req, res, next) => { 8 | MenusModel.getListTree(req.query) 9 | .then(function(menuTree) { 10 | return res.json({ 11 | code: 20000, 12 | message: '获取成功', 13 | data: menuTree || [] 14 | }) 15 | }) 16 | }) 17 | 18 | router.post('/add', (req, res, next) => { 19 | MenusModel.create(req.body).then(function(menu) { 20 | if (!menu) { 21 | return res.json({ 22 | code: 40000, 23 | message: '创建失败', 24 | data: null 25 | }) 26 | } 27 | return res.json({ 28 | code: 20000, 29 | message: '创建成功', 30 | data: menu.menu_id 31 | }) 32 | }) 33 | }) 34 | 35 | router.post('/edit', (req, res, next) => { 36 | delete req.body.menu_id 37 | const data = req.body 38 | data.update_time = new Date() 39 | MenusModel.update(data, { 40 | where: { 41 | menu_id: req.query.menu_id || 0 42 | } 43 | }).then(function(menu) { 44 | if (!menu) { 45 | return res.json({ 46 | code: 40000, 47 | message: '修改失败', 48 | data: null 49 | }) 50 | } 51 | return res.json({ 52 | code: 20000, 53 | message: '修改成功', 54 | data: menu 55 | }) 56 | }) 57 | }) 58 | 59 | router.post('/del', (req, res, next) => { 60 | MenusModel.destroy({ 61 | where: { 62 | [Op.or]: [{ menu_id: req.body }, { parent_id: req.body }] 63 | } 64 | }).then(function(menu) { 65 | return res.json({ 66 | code: 20000, 67 | message: '删除', 68 | data: menu 69 | }) 70 | }) 71 | }) 72 | 73 | router.get('/get', (req, res, next) => { 74 | MenusModel.findOne({ 75 | where: req.query 76 | }).then(function(menu) { 77 | return res.json({ 78 | code: 20000, 79 | message: '获取成功', 80 | data: menu 81 | }) 82 | }) 83 | }) 84 | 85 | module.exports = router 86 | -------------------------------------------------------------------------------- /server/router/role.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const RoleModel = require('../model/roles') 4 | 5 | router.get('/list', (req, res, next) => { 6 | RoleModel.findAll({ 7 | where: req.query 8 | }).then(function(roles) { 9 | const data = { 10 | roles: roles, 11 | count: roles ? roles.length : 0 12 | } 13 | return res.json({ 14 | code: 20000, 15 | message: '获取成功', 16 | data: data 17 | }) 18 | }) 19 | }) 20 | 21 | router.post('/add', (req, res, next) => { 22 | RoleModel.create(req.body).then(function(role) { 23 | if (!role) { 24 | return res.json({ 25 | code: 40000, 26 | message: '创建失败', 27 | data: null 28 | }) 29 | } 30 | return res.json({ 31 | code: 20000, 32 | message: '创建成功', 33 | data: role.role_id 34 | }) 35 | }) 36 | }) 37 | 38 | router.post('/edit', (req, res, next) => { 39 | const data = req.body 40 | data.update_time = new Date() 41 | RoleModel.update(data, { 42 | where: req.query 43 | }).then(function(role) { 44 | if (!role) { 45 | return res.json({ 46 | code: 40000, 47 | message: '修改失败', 48 | data: null 49 | }) 50 | } 51 | return res.json({ 52 | code: 20000, 53 | message: '修改成功', 54 | data: role 55 | }) 56 | }) 57 | }) 58 | 59 | router.post('/del', (req, res, next) => { 60 | const role_ids = req.body 61 | RoleModel.delRole(role_ids || []).then(function(role) { 62 | if (!role) { 63 | return res.json({ 64 | code: 40000, 65 | message: '删除失败', 66 | data: null 67 | }) 68 | } 69 | return res.json({ 70 | code: 20000, 71 | message: '删除成功', 72 | data: role 73 | }) 74 | }) 75 | }) 76 | 77 | router.get('/get', (req, res, next) => { 78 | RoleModel.findOne({ 79 | where: req.query 80 | }).then(function(role) { 81 | return res.json({ 82 | code: 20000, 83 | message: '获取成功', 84 | data: role 85 | }) 86 | }) 87 | }) 88 | 89 | module.exports = router 90 | -------------------------------------------------------------------------------- /server/router/user-log.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const UserLogsModel = require('../model/user-logs') 4 | const UsersModel = require('../model/users') 5 | const Sequelize = require('sequelize') 6 | const Op = Sequelize.Op 7 | 8 | UserLogsModel.hasOne(UsersModel, { foreignKey: 'user_id', sourceKey: 'user_id' }) 9 | 10 | router.get('/list', (req, res, next) => { 11 | if (req.query.page <= 0) { 12 | req.query.page = 1 13 | } 14 | if (req.query.limit > 50) { 15 | req.query.limit = 50 16 | } 17 | let where = {} 18 | if (req.query.date && req.query.date.length === 2) { 19 | where = { 20 | create_time: { 21 | [Op.between]: req.query.date 22 | } 23 | } 24 | } 25 | const offset = (req.query.page - 1) * req.query.limit 26 | UserLogsModel.findAndCountAll({ 27 | offset, 28 | limit: parseInt(req.query.limit) || 20, 29 | include: [ 30 | { 31 | model: UsersModel, 32 | attributes: ['user_name'] 33 | } 34 | ], 35 | where: where, 36 | order: [['create_time', 'DESC']] 37 | }).then(function(logs) { 38 | return res.json({ 39 | code: 20000, 40 | message: '获取成功', 41 | data: { 42 | logs: logs.rows, 43 | total: logs.count 44 | } 45 | }) 46 | }) 47 | }) 48 | 49 | router.post('/del', (req, res, next) => { 50 | const log_ids = req.body 51 | UserLogsModel.destroy({ where: { user_log_id: log_ids }}).then(function(role) { 52 | if (!role) { 53 | return res.json({ 54 | code: 40000, 55 | message: '删除失败', 56 | data: null 57 | }) 58 | } 59 | return res.json({ 60 | code: 20000, 61 | message: '删除成功', 62 | data: role 63 | }) 64 | }) 65 | }) 66 | 67 | module.exports = router 68 | -------------------------------------------------------------------------------- /server/utils/tools.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * 获取树形结构数据 5 | * @param data 数据 6 | * @param level 节点 7 | * @param idFeild 字段名 8 | * @param pidFeild 上一级字段名 9 | * @returns {null|[]} 10 | */ 11 | const getTreeData = function(data, level = null, idFeild = 'id', pidFeild = 'parent_id') { 12 | const tree = [] 13 | const _level = [] 14 | if (level === null) { 15 | data.forEach(function(item) { 16 | _level.push(item[pidFeild]) 17 | }) 18 | level = Math.min(..._level) 19 | } 20 | data.forEach(function(item) { 21 | if (item[pidFeild] === level) { 22 | tree.push(item) 23 | } 24 | }) 25 | if (tree.length === 0) { 26 | return null 27 | } 28 | 29 | // 对于父节点为0的进行循环,然后查出父节点为上面结果id的节点内容 30 | tree.forEach(function(item) { 31 | const childData = getTreeData(data, item[idFeild], idFeild, pidFeild) 32 | if (childData != null) { 33 | item['children'] = childData 34 | } 35 | }) 36 | return tree 37 | } 38 | 39 | /** 40 | * 获取两个数组交集 41 | * @param arr1 42 | * @param arr2 43 | * @returns {*[]} 44 | */ 45 | const intersectArr = function(arr1 = [], arr2 = []) { 46 | return arr1.filter(function(v) { return arr2.indexOf(v) > -1 }) 47 | } 48 | 49 | /** 50 | * 获取两个数组差集 51 | * @param arr1 52 | * @param arr2 53 | * @returns {*[]} 54 | */ 55 | const minustArr = function(arr1 = [], arr2 = []) { 56 | return arr1.filter(function(v) { return arr2.indexOf(v) === -1 }) 57 | } 58 | 59 | module.exports = { 60 | getTreeData: getTreeData, 61 | intersectArr: intersectArr, 62 | minustArr: minustArr 63 | } 64 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/api/system/menu.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function listMenu(params) { 4 | return request({ 5 | url: '/menu/list', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function getMenu(params) { 12 | return request({ 13 | url: '/menu/get', 14 | method: 'get', 15 | params 16 | }) 17 | } 18 | 19 | export function delMenu(data) { 20 | return request({ 21 | url: '/menu/del', 22 | method: 'post', 23 | data 24 | }) 25 | } 26 | 27 | export function addMenu(data) { 28 | return request({ 29 | url: '/menu/add', 30 | method: 'post', 31 | data 32 | }) 33 | } 34 | 35 | export function updateMenu(menu_id, data) { 36 | return request({ 37 | url: '/menu/edit?menu_id=' + menu_id, 38 | method: 'post', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/api/system/role.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function listRole(params) { 4 | return request({ 5 | url: '/role/list', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function getRole(params) { 12 | return request({ 13 | url: '/role/get', 14 | method: 'get', 15 | params 16 | }) 17 | } 18 | 19 | export function delRole(data) { 20 | return request({ 21 | url: '/role/del', 22 | method: 'post', 23 | data 24 | }) 25 | } 26 | 27 | export function addRole(data) { 28 | return request({ 29 | url: '/role/add', 30 | method: 'post', 31 | data 32 | }) 33 | } 34 | 35 | export function updateRole(role_id, data) { 36 | return request({ 37 | url: '/role/edit?role_id=' + role_id, 38 | method: 'post', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/api/system/user-log.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function listLog(params) { 4 | return request({ 5 | url: '/user-log/list', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function delLog(data) { 12 | return request({ 13 | url: '/user-log/del', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/api/system/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function listUser(params) { 4 | return request({ 5 | url: '/user/list', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | 11 | export function addUser(data) { 12 | return request({ 13 | url: '/user/add', 14 | method: 'post', 15 | data 16 | }) 17 | } 18 | 19 | export function delUser(data) { 20 | return request({ 21 | url: '/user/del', 22 | method: 'post', 23 | data 24 | }) 25 | } 26 | 27 | export function updateUser(user_id, data) { 28 | return request({ 29 | url: '/user/edit?user_id=' + user_id, 30 | method: 'post', 31 | data 32 | }) 33 | } 34 | 35 | export function updatePwd(user_id, data) { 36 | return request({ 37 | url: '/user/edit-pwd?user_id=' + user_id, 38 | method: 'post', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/user/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getInfo(token) { 12 | return request({ 13 | url: '/user/info', 14 | method: 'get' 15 | }) 16 | } 17 | 18 | export function logout() { 19 | return request({ 20 | url: '/user/logout', 21 | method: 'post' 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limuen/vue-admin/b5589994cbb1e6177f53c2e701f3e2e378b862a3/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limuen/vue-admin/b5589994cbb1e6177f53c2e701f3e2e378b862a3/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limuen/vue-admin/b5589994cbb1e6177f53c2e701f3e2e378b862a3/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limuen/vue-admin/b5589994cbb1e6177f53c2e701f3e2e378b862a3/src/assets/custom-theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limuen/vue-admin/b5589994cbb1e6177f53c2e701f3e2e378b862a3/src/assets/custom-theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /src/assets/pays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limuen/vue-admin/b5589994cbb1e6177f53c2e701f3e2e378b862a3/src/assets/pays.png -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 68 | 69 | 82 | -------------------------------------------------------------------------------- /src/components/Charts/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null, 7 | $_resizeHandler: null 8 | } 9 | }, 10 | mounted() { 11 | this.initListener() 12 | }, 13 | activated() { 14 | if (!this.$_resizeHandler) { 15 | // avoid duplication init 16 | this.initListener() 17 | } 18 | 19 | // when keep-alive chart activated, auto resize 20 | this.resize() 21 | }, 22 | beforeDestroy() { 23 | this.destroyListener() 24 | }, 25 | deactivated() { 26 | this.destroyListener() 27 | }, 28 | methods: { 29 | // use $_ for mixins properties 30 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 31 | $_sidebarResizeHandler(e) { 32 | if (e.propertyName === 'width') { 33 | this.$_resizeHandler() 34 | } 35 | }, 36 | initListener() { 37 | this.$_resizeHandler = debounce(() => { 38 | this.resize() 39 | }, 100) 40 | window.addEventListener('resize', this.$_resizeHandler) 41 | 42 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 43 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) 44 | }, 45 | destroyListener() { 46 | window.removeEventListener('resize', this.$_resizeHandler) 47 | this.$_resizeHandler = null 48 | 49 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) 50 | }, 51 | resize() { 52 | const { chart } = this 53 | chart && chart.resize() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/GithubCorner/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 55 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /src/components/IconSelect/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 44 | 45 | 69 | -------------------------------------------------------------------------------- /src/components/IconSelect/requireIcons.js: -------------------------------------------------------------------------------- 1 | const req = require.context('../../icons/svg', false, /\.svg$/) 2 | const requireAll = requireContext => requireContext.keys() 3 | 4 | const re = /\.\/(.*)\.svg/ 5 | 6 | const icons = requireAll(req).map(i => { 7 | return i.match(re)[1] 8 | }) 9 | 10 | export default icons 11 | -------------------------------------------------------------------------------- /src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 92 | 93 | 102 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 50 | 51 | 61 | -------------------------------------------------------------------------------- /src/components/SizeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 57 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 46 | 47 | 62 | -------------------------------------------------------------------------------- /src/components/TextHoverEffect/Mallki.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | 114 | -------------------------------------------------------------------------------- /src/directive/permission/index.js: -------------------------------------------------------------------------------- 1 | import permission from './permission' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('permission', permission) 5 | } 6 | 7 | if (window.Vue) { 8 | window['permission'] = permission 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | permission.install = install 13 | export default permission 14 | -------------------------------------------------------------------------------- /src/directive/permission/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | export default { 4 | inserted(el, binding, vnode) { 5 | const { value } = binding 6 | const buttons = store.getters && store.getters.buttons 7 | 8 | if (value && value instanceof Array && value.length > 0) { 9 | const permissionButtons = value 10 | 11 | const hasPermission = buttons.some(button => { 12 | return permissionButtons.includes(button) 13 | }) 14 | 15 | if (!hasPermission) { 16 | el.parentNode && el.parentNode.removeChild(el) 17 | } 18 | } else { 19 | throw new Error(`需要权限标识,例: v-permission="['system:menu:query','system:menu:add']"`) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/filters/index.js: -------------------------------------------------------------------------------- 1 | // import parseTime, formatTime and set to filter 2 | export { parseTime, formatTime } from '@/utils' 3 | 4 | /** 5 | * Show plural label if time is plural number 6 | * @param {number} time 7 | * @param {string} label 8 | * @return {string} 9 | */ 10 | function pluralize(time, label) { 11 | if (time === 1) { 12 | return time + label 13 | } 14 | return time + label + 's' 15 | } 16 | 17 | /** 18 | * @param {number} time 19 | */ 20 | export function timeAgo(time) { 21 | const between = Date.now() / 1000 - Number(time) 22 | if (between < 3600) { 23 | return pluralize(~~(between / 60), ' minute') 24 | } else if (between < 86400) { 25 | return pluralize(~~(between / 3600), ' hour') 26 | } else { 27 | return pluralize(~~(between / 86400), ' day') 28 | } 29 | } 30 | 31 | /** 32 | * Number formatting 33 | * like 10000 => 10k 34 | * @param {number} num 35 | * @param {number} digits 36 | */ 37 | export function numberFormatter(num, digits) { 38 | const si = [ 39 | { value: 1E18, symbol: 'E' }, 40 | { value: 1E15, symbol: 'P' }, 41 | { value: 1E12, symbol: 'T' }, 42 | { value: 1E9, symbol: 'G' }, 43 | { value: 1E6, symbol: 'M' }, 44 | { value: 1E3, symbol: 'k' } 45 | ] 46 | for (let i = 0; i < si.length; i++) { 47 | if (num >= si[i].value) { 48 | return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol 49 | } 50 | } 51 | return num.toString() 52 | } 53 | 54 | /** 55 | * 10000 => "10,000" 56 | * @param {number} num 57 | */ 58 | export function toThousandFilter(num) { 59 | return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ',')) 60 | } 61 | 62 | /** 63 | * Upper case first char 64 | * @param {String} string 65 | */ 66 | export function uppercaseFirst(string) { 67 | return string.charAt(0).toUpperCase() + string.slice(1) 68 | } 69 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /src/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/build.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/icons/svg/cascader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/checkbox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/component.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/date-range.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dict.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/drag copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/druid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/education.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/excel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/input.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/job.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/log.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/logininfor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/monitor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/phone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/post.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/question.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/radio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/rate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/row.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/select.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/server.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/skill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/slider.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/swagger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/switch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/system.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/textarea.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/time-range.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/time.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tool.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree-table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/validCode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/wechat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 49 | 50 | 58 | -------------------------------------------------------------------------------- /src/layout/components/Settings/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 83 | 84 | 109 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | this.fixBugIniOS() 10 | }, 11 | methods: { 12 | fixBugIniOS() { 13 | const $subMenu = this.$refs.subMenu 14 | if ($subMenu) { 15 | const handleMouseleave = $subMenu.handleMouseleave 16 | $subMenu.handleMouseleave = (e) => { 17 | if (this.device === 'mobile') { 18 | return 19 | } 20 | handleMouseleave(e) 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 30 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 55 | -------------------------------------------------------------------------------- /src/layout/components/TagsView/ScrollPane.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 78 | 79 | 95 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as AppMain } from './AppMain' 2 | export { default as Navbar } from './Navbar' 3 | export { default as Settings } from './Settings' 4 | export { default as Sidebar } from './Sidebar/index.vue' 5 | export { default as TagsView } from './TagsView/index.vue' 6 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 59 | 60 | 103 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import Cookies from 'js-cookie' 4 | 5 | import 'normalize.css/normalize.css' // a modern alternative to CSS resets 6 | 7 | import Element from 'element-ui' 8 | import './styles/element-variables.scss' 9 | // import enLang from 'element-ui/lib/locale/lang/zh-CN'// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖 10 | 11 | import '@/styles/index.scss' // global css 12 | 13 | import App from './App' 14 | import store from './store' 15 | import router from './router' 16 | import permission from './directive/permission' 17 | 18 | import './icons' // icon 19 | import './permission' // permission control 20 | 21 | import * as filters from './filters' // global filters 22 | 23 | import { parseTime, resetForm } from '@/utils/costum' 24 | // 全局方法挂载 25 | Vue.prototype.parseTime = parseTime 26 | Vue.prototype.resetForm = resetForm 27 | 28 | Vue.prototype.msgSuccess = function(msg) { 29 | this.$message({ showClose: true, message: msg, type: 'success' }) 30 | } 31 | 32 | Vue.prototype.msgError = function(msg) { 33 | this.$message({ showClose: true, message: msg, type: 'error' }) 34 | } 35 | 36 | Vue.prototype.msgInfo = function(msg) { 37 | this.$message.info(msg) 38 | } 39 | 40 | Vue.use(permission) 41 | 42 | Vue.use(Element, { 43 | size: Cookies.get('size') || 'medium' // set element-ui default size 44 | }) 45 | 46 | // register global utility filters 47 | Object.keys(filters).forEach(key => { 48 | Vue.filter(key, filters[key]) 49 | }) 50 | 51 | Vue.config.productionTip = false 52 | 53 | new Vue({ 54 | el: '#app', 55 | router, 56 | store, 57 | render: h => h(App) 58 | }) 59 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const hasToken = getToken() 22 | 23 | if (hasToken) { 24 | if (to.path === '/login') { 25 | // if is logged in, redirect to the home page 26 | next({ path: '/' }) 27 | NProgress.done() 28 | } else { 29 | // determine whether the user has obtained his permission roles through getInfo 30 | const hasRoles = store.getters.roles && store.getters.roles.length > 0 31 | if (hasRoles) { 32 | next() 33 | } else { 34 | try { 35 | // get user info 36 | // note: roles must be a object array! such as: ['admin'] or ,['developer','editor'] 37 | const { menus } = await store.dispatch('user/getInfo') 38 | // generate accessible routes map based on roles 39 | const accessRoutes = await store.dispatch('permission/generateRoutes', menus) 40 | // dynamically add accessible routes 41 | router.addRoutes(accessRoutes) 42 | 43 | // hack method to ensure that addRoutes is complete 44 | // set the replace: true, so the navigation will not leave a history record 45 | next({ ...to, replace: true }) 46 | } catch (error) { 47 | // remove token and go to login page to re-login 48 | await store.dispatch('user/resetToken') 49 | Message.error(error || 'Has Error') 50 | next(`/login?redirect=${to.path}`) 51 | NProgress.done() 52 | } 53 | } 54 | } 55 | } else { 56 | /* has no token*/ 57 | 58 | if (whiteList.indexOf(to.path) !== -1) { 59 | // in the free login whitelist, go directly 60 | next() 61 | } else { 62 | // other pages that do not have permission to access are redirected to the login page. 63 | next(`/login?redirect=${to.path}`) 64 | NProgress.done() 65 | } 66 | } 67 | }) 68 | 69 | router.afterEach(() => { 70 | // finish progress bar 71 | NProgress.done() 72 | }) 73 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Vue Element Admin', 3 | 4 | /** 5 | * @type {boolean} true | false 6 | * @description Whether show the settings right-panel 7 | */ 8 | showSettings: true, 9 | 10 | /** 11 | * @type {boolean} true | false 12 | * @description Whether need tagsView 13 | */ 14 | tagsView: true, 15 | 16 | /** 17 | * @type {boolean} true | false 18 | * @description Whether fix the header 19 | */ 20 | fixedHeader: false, 21 | 22 | /** 23 | * @type {boolean} true | false 24 | * @description Whether show the logo in sidebar 25 | */ 26 | sidebarLogo: true, 27 | 28 | /** 29 | * @type {string | array} 'production' | ['production', 'development'] 30 | * @description Need show err logs component. 31 | * The default is only used in the production env 32 | * If you want to also use it in dev, you can pass ['production', 'development'] 33 | */ 34 | errorLog: 'production' 35 | } 36 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | size: state => state.app.size, 4 | device: state => state.app.device, 5 | visitedViews: state => state.tagsView.visitedViews, 6 | cachedViews: state => state.tagsView.cachedViews, 7 | token: state => state.user.token, 8 | avatar: state => state.user.avatar, 9 | user_id: state => state.user.user_id, 10 | name: state => state.user.name, 11 | menus: state => state.user.menus, 12 | buttons: state => state.user.buttons, 13 | roles: state => state.user.roles, 14 | permission_routes: state => state.permission.routes 15 | } 16 | export default getters 17 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | 5 | Vue.use(Vuex) 6 | 7 | // https://webpack.js.org/guides/dependency-management/#requirecontext 8 | const modulesFiles = require.context('./modules', true, /\.js$/) 9 | 10 | // you do not need `import app from './modules/app'` 11 | // it will auto require all vuex module from modules file 12 | const modules = modulesFiles.keys().reduce((modules, modulePath) => { 13 | // set './app.js' => 'app' 14 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 15 | const value = modulesFiles(modulePath) 16 | modules[moduleName] = value.default 17 | return modules 18 | }, {}) 19 | 20 | const store = new Vuex.Store({ 21 | modules, 22 | getters 23 | }) 24 | 25 | export default store 26 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop', 9 | size: Cookies.get('size') || 'medium' 10 | } 11 | 12 | const mutations = { 13 | TOGGLE_SIDEBAR: state => { 14 | state.sidebar.opened = !state.sidebar.opened 15 | state.sidebar.withoutAnimation = false 16 | if (state.sidebar.opened) { 17 | Cookies.set('sidebarStatus', 1) 18 | } else { 19 | Cookies.set('sidebarStatus', 0) 20 | } 21 | }, 22 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 23 | Cookies.set('sidebarStatus', 0) 24 | state.sidebar.opened = false 25 | state.sidebar.withoutAnimation = withoutAnimation 26 | }, 27 | TOGGLE_DEVICE: (state, device) => { 28 | state.device = device 29 | }, 30 | SET_SIZE: (state, size) => { 31 | state.size = size 32 | Cookies.set('size', size) 33 | } 34 | } 35 | 36 | const actions = { 37 | toggleSideBar({ commit }) { 38 | commit('TOGGLE_SIDEBAR') 39 | }, 40 | closeSideBar({ commit }, { withoutAnimation }) { 41 | commit('CLOSE_SIDEBAR', withoutAnimation) 42 | }, 43 | toggleDevice({ commit }, device) { 44 | commit('TOGGLE_DEVICE', device) 45 | }, 46 | setSize({ commit }, size) { 47 | commit('SET_SIZE', size) 48 | } 49 | } 50 | 51 | export default { 52 | namespaced: true, 53 | state, 54 | mutations, 55 | actions 56 | } 57 | -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { errorRoutes, constantRoutes } from '@/router' 2 | import Layout from '@/layout' 3 | 4 | /** 5 | * Filter asynchronous routing tables by recursion 6 | * @param routes asyncRoutes 7 | * @param roles 8 | */ 9 | export function filterAsyncRoutes(routes) { 10 | const res = [] 11 | 12 | routes.forEach(route => { 13 | const component = route.component 14 | const tmp = { 15 | path: route.path, 16 | component: route.component === 'Layout' ? Layout : () => import(`@/views${component}`), // resolve => require([`@/views${component}`], resolve), 17 | redirect: route.redirect || undefined, 18 | hidden: !!route.hidden, 19 | name: route.name, 20 | meta: {}, 21 | children: route.children || undefined 22 | } 23 | tmp.meta.title = route.title 24 | if (route.icon) { 25 | tmp.meta.icon = route.icon 26 | } 27 | if (tmp.children) { 28 | if (tmp.children.length) { 29 | tmp.alwaysShow = true 30 | } 31 | tmp.children = filterAsyncRoutes(tmp.children) 32 | } 33 | res.push(tmp) 34 | }) 35 | return res 36 | } 37 | 38 | const state = { 39 | routes: [], 40 | addRoutes: [] 41 | } 42 | 43 | const mutations = { 44 | SET_ROUTES: (state, routes) => { 45 | state.addRoutes = routes 46 | state.routes = constantRoutes.concat(routes) 47 | } 48 | } 49 | 50 | const actions = { 51 | generateRoutes({ commit }, menus) { 52 | return new Promise(resolve => { 53 | const accessedRoutes = filterAsyncRoutes(menus).concat(errorRoutes) 54 | commit('SET_ROUTES', accessedRoutes) 55 | resolve(accessedRoutes) 56 | }) 57 | } 58 | } 59 | 60 | export default { 61 | namespaced: true, 62 | state, 63 | mutations, 64 | actions 65 | } 66 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import variables from '@/styles/element-variables.scss' 2 | import defaultSettings from '@/settings' 3 | 4 | const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings 5 | 6 | const state = { 7 | theme: variables.theme, 8 | showSettings: showSettings, 9 | tagsView: tagsView, 10 | fixedHeader: fixedHeader, 11 | sidebarLogo: sidebarLogo 12 | } 13 | 14 | const mutations = { 15 | CHANGE_SETTING: (state, { key, value }) => { 16 | if (state.hasOwnProperty(key)) { 17 | state[key] = value 18 | } 19 | } 20 | } 21 | 22 | const actions = { 23 | changeSetting({ commit }, data) { 24 | commit('CHANGE_SETTING', data) 25 | } 26 | } 27 | 28 | export default { 29 | namespaced: true, 30 | state, 31 | mutations, 32 | actions 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/styles/btn.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | @mixin colorBtn($color) { 4 | background: $color; 5 | 6 | &:hover { 7 | color: $color; 8 | 9 | &:before, 10 | &:after { 11 | background: $color; 12 | } 13 | } 14 | } 15 | 16 | .blue-btn { 17 | @include colorBtn($blue) 18 | } 19 | 20 | .light-blue-btn { 21 | @include colorBtn($light-blue) 22 | } 23 | 24 | .red-btn { 25 | @include colorBtn($red) 26 | } 27 | 28 | .pink-btn { 29 | @include colorBtn($pink) 30 | } 31 | 32 | .green-btn { 33 | @include colorBtn($green) 34 | } 35 | 36 | .tiffany-btn { 37 | @include colorBtn($tiffany) 38 | } 39 | 40 | .yellow-btn { 41 | @include colorBtn($yellow) 42 | } 43 | 44 | .pan-btn { 45 | font-size: 14px; 46 | color: #fff; 47 | padding: 14px 36px; 48 | border-radius: 8px; 49 | border: none; 50 | outline: none; 51 | transition: 600ms ease all; 52 | position: relative; 53 | display: inline-block; 54 | 55 | &:hover { 56 | background: #fff; 57 | 58 | &:before, 59 | &:after { 60 | width: 100%; 61 | transition: 600ms ease all; 62 | } 63 | } 64 | 65 | &:before, 66 | &:after { 67 | content: ''; 68 | position: absolute; 69 | top: 0; 70 | right: 0; 71 | height: 2px; 72 | width: 0; 73 | transition: 400ms ease all; 74 | } 75 | 76 | &::after { 77 | right: inherit; 78 | top: inherit; 79 | left: 0; 80 | bottom: 0; 81 | } 82 | } 83 | 84 | .custom-button { 85 | display: inline-block; 86 | line-height: 1; 87 | white-space: nowrap; 88 | cursor: pointer; 89 | background: #fff; 90 | color: #fff; 91 | -webkit-appearance: none; 92 | text-align: center; 93 | box-sizing: border-box; 94 | outline: 0; 95 | margin: 0; 96 | padding: 10px 15px; 97 | font-size: 14px; 98 | border-radius: 4px; 99 | } 100 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | .cell { 19 | .el-tag { 20 | margin-right: 0px; 21 | } 22 | } 23 | 24 | .small-padding { 25 | .cell { 26 | padding-left: 5px; 27 | padding-right: 5px; 28 | } 29 | } 30 | 31 | .fixed-width { 32 | .el-button--mini { 33 | padding: 7px 10px; 34 | min-width: 60px; 35 | } 36 | } 37 | 38 | .status-col { 39 | .cell { 40 | padding: 0 10px; 41 | text-align: center; 42 | 43 | .el-tag { 44 | margin-right: 0px; 45 | } 46 | } 47 | } 48 | 49 | // to fixed https://github.com/ElemeFE/element/issues/2461 50 | .el-dialog { 51 | transform: none; 52 | left: 0; 53 | position: relative; 54 | margin: 0 auto; 55 | } 56 | 57 | // refine element ui upload 58 | .upload-container { 59 | .el-upload { 60 | width: 100%; 61 | 62 | .el-upload-dragger { 63 | width: 100%; 64 | height: 200px; 65 | } 66 | } 67 | } 68 | 69 | // dropdown 70 | .el-dropdown-menu { 71 | a { 72 | display: block 73 | } 74 | } 75 | 76 | // fix date-picker ui bug in filter-item 77 | .el-range-editor.el-input__inner { 78 | display: inline-flex !important; 79 | } 80 | 81 | // to fix el-date-picker css style 82 | .el-range-separator { 83 | box-sizing: content-box; 84 | } 85 | -------------------------------------------------------------------------------- /src/styles/element-variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * I think element-ui's default theme color is too light for long-term use. 3 | * So I modified the default color and you can modify it to your liking. 4 | **/ 5 | 6 | /* theme color */ 7 | $--color-primary: #1890ff; 8 | $--color-success: #13ce66; 9 | $--color-warning: #ffba00; 10 | $--color-danger: #ff4949; 11 | // $--color-info: #1E1E1E; 12 | 13 | $--button-font-weight: 400; 14 | 15 | // $--color-text-regular: #1f2d3d; 16 | 17 | $--border-color-light: #dfe4ed; 18 | $--border-color-lighter: #e6ebf5; 19 | 20 | $--table-border: 1px solid #dfe6ec; 21 | 22 | /* icon font path, required */ 23 | $--font-path: "~element-ui/lib/theme-chalk/fonts"; 24 | 25 | @import "~element-ui/packages/theme-chalk/src/index"; 26 | 27 | // the :export directive is the magic sauce for webpack 28 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 29 | :export { 30 | theme: $--color-primary; 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | @mixin pct($pct) { 31 | width: #{$pct}; 32 | position: relative; 33 | margin: 0 auto; 34 | } 35 | 36 | @mixin triangle($width, $height, $color, $direction) { 37 | $width: $width/2; 38 | $color-border-style: $height solid $color; 39 | $transparent-border-style: $width solid transparent; 40 | height: 0; 41 | width: 0; 42 | 43 | @if $direction==up { 44 | border-bottom: $color-border-style; 45 | border-left: $transparent-border-style; 46 | border-right: $transparent-border-style; 47 | } 48 | 49 | @else if $direction==right { 50 | border-left: $color-border-style; 51 | border-top: $transparent-border-style; 52 | border-bottom: $transparent-border-style; 53 | } 54 | 55 | @else if $direction==down { 56 | border-top: $color-border-style; 57 | border-left: $transparent-border-style; 58 | border-right: $transparent-border-style; 59 | } 60 | 61 | @else if $direction==left { 62 | border-right: $color-border-style; 63 | border-top: $transparent-border-style; 64 | border-bottom: $transparent-border-style; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // base color 2 | $blue:#324157; 3 | $light-blue:#3A71A8; 4 | $red:#C03639; 5 | $pink: #E65D6E; 6 | $green: #30B08F; 7 | $tiffany: #4AB7BD; 8 | $yellow:#FEC171; 9 | $panGreen: #30B08F; 10 | 11 | // sidebar 12 | $menuText:#bfcbd9; 13 | $menuActiveText:#409EFF; 14 | $subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951 15 | 16 | $menuBg:#304156; 17 | $menuHover:#263445; 18 | 19 | $subMenuBg:#1f2d3d; 20 | $subMenuHover:#001528; 21 | 22 | $sideBarWidth: 210px; 23 | 24 | // the :export directive is the magic sauce for webpack 25 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 26 | :export { 27 | menuText: $menuText; 28 | menuActiveText: $menuActiveText; 29 | subMenuActiveText: $subMenuActiveText; 30 | menuBg: $menuBg; 31 | menuHover: $menuHover; 32 | subMenuBg: $subMenuBg; 33 | subMenuHover: $subMenuHover; 34 | sideBarWidth: $sideBarWidth; 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'Admin-Token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/costum.js: -------------------------------------------------------------------------------- 1 | // 日期格式化 2 | export function parseTime(time, pattern) { 3 | if (arguments.length === 0 || !time) { 4 | return null 5 | } 6 | const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}' 7 | let date 8 | if (typeof time === 'object') { 9 | date = time 10 | } else { 11 | if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { 12 | time = parseInt(time) 13 | } 14 | if ((typeof time === 'number') && (time.toString().length === 10)) { 15 | time = time * 1000 16 | } 17 | date = new Date(time) 18 | } 19 | const formatObj = { 20 | y: date.getFullYear(), 21 | m: date.getMonth() + 1, 22 | d: date.getDate(), 23 | h: date.getHours(), 24 | i: date.getMinutes(), 25 | s: date.getSeconds(), 26 | a: date.getDay() 27 | } 28 | const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { 29 | let value = formatObj[key] 30 | // Note: getDay() returns 0 on Sunday 31 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] } 32 | if (result.length > 0 && value < 10) { 33 | value = '0' + value 34 | } 35 | return value || 0 36 | }) 37 | return time_str 38 | } 39 | 40 | // 表单重置 41 | export function resetForm(refName) { 42 | if (this.$refs[refName]) { 43 | this.$refs[refName].resetFields() 44 | } 45 | } 46 | 47 | // 添加日期范围 48 | export function addDateRange(params, dateRange) { 49 | var search = params 50 | search.beginTime = '' 51 | search.endTime = '' 52 | if (dateRange != null && dateRange !== '') { 53 | search.beginTime = this.dateRange[0] 54 | search.endTime = this.dateRange[1] 55 | } 56 | return search 57 | } 58 | 59 | // 回显数据字典 60 | export function selectDictLabel(datas, value) { 61 | var actions = [] 62 | Object.keys(datas).map((key) => { 63 | if (datas[key].dictValue === ('' + value)) { 64 | actions.push(datas[key].dictLabel) 65 | return false 66 | } 67 | }) 68 | return actions.join('') 69 | } 70 | 71 | // 字符串格式化(%s ) 72 | export function sprintf(str) { 73 | var args = arguments; var flag = true; var i = 1 74 | str = str.replace(/%s/g, function() { 75 | var arg = args[i++] 76 | if (typeof arg === 'undefined') { 77 | flag = false 78 | return '' 79 | } 80 | return arg 81 | }) 82 | return flag ? str : '' 83 | } 84 | 85 | // 转换字符串,undefined,null等转化为"" 86 | export function praseStrEmpty(str) { 87 | if (!str || str === 'undefined' || str === 'null') { 88 | return '' 89 | } 90 | return str 91 | } 92 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Vue Element Admin' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/open-window.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Sting} url 3 | * @param {Sting} title 4 | * @param {Number} w 5 | * @param {Number} h 6 | */ 7 | export default function openWindow(url, title, w, h) { 8 | // Fixes dual-screen position Most browsers Firefox 9 | const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left 10 | const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top 11 | 12 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width 13 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height 14 | 15 | const left = ((width / 2) - (w / 2)) + dualScreenLeft 16 | const top = ((height / 2) - (h / 2)) + dualScreenTop 17 | const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left) 18 | 19 | // Puts focus on the newWindow 20 | if (window.focus) { 21 | newWindow.focus() 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/utils/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | /** 4 | * @param {Array} value 5 | * @returns {Boolean} 6 | * @example see @/views/permission/directive.vue 7 | */ 8 | export default function checkPermission(value) { 9 | if (value && value instanceof Array && value.length > 0) { 10 | const roles = store.getters && store.getters.roles 11 | const permissionRoles = value 12 | 13 | const hasPermission = roles.some(role => { 14 | return permissionRoles.includes(role) 15 | }) 16 | 17 | if (!hasPermission) { 18 | return false 19 | } 20 | return true 21 | } else { 22 | console.error(`need roles! Like v-permission="['admin','editor']"`) 23 | return false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 9 | // withCredentials: true, // send cookies when cross-domain requests 10 | timeout: 5000 // request timeout 11 | }) 12 | 13 | // request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // do something before request is sent 17 | 18 | if (store.getters.token) { 19 | // let each request carry token 20 | // ['X-Token'] is a custom headers key 21 | // please modify it according to the actual situation 22 | config.headers['Authorization'] = getToken() 23 | } 24 | return config 25 | }, 26 | error => { 27 | // do something with request error 28 | return Promise.reject(error) 29 | } 30 | ) 31 | 32 | // response interceptor 33 | service.interceptors.response.use( 34 | /** 35 | * If you want to get http information such as headers or status 36 | * Please return response => response 37 | */ 38 | 39 | /** 40 | * Determine the request status by custom code 41 | * Here is just an example 42 | * You can also judge the status by HTTP Status Code 43 | */ 44 | response => { 45 | const res = response.data 46 | 47 | // if the custom code is not 20000, it is judged as an error. 48 | if (res.code !== 20000) { 49 | Message({ 50 | message: res.message || 'Error', 51 | type: 'error', 52 | duration: 5 * 1000 53 | }) 54 | return Promise.reject(new Error(res.message || 'Error')) 55 | } else { 56 | return res 57 | } 58 | }, 59 | error => { 60 | // 401: Illegal token: Other clients logged in: Token expired; 61 | if (error.response.status === 401) { 62 | // to re-login 63 | MessageBox.confirm('登陆已过期,请重新登录', '重新登录', { 64 | confirmButtonText: '重新登录', 65 | cancelButtonText: '取消', 66 | type: 'warning' 67 | }).finally(() => { 68 | store.dispatch('user/resetToken').then(() => { 69 | location.reload() 70 | }) 71 | }) 72 | } else { 73 | Message({ 74 | message: error.message, 75 | type: 'error', 76 | duration: 5 * 1000 77 | }) 78 | return Promise.reject(error) 79 | } 80 | } 81 | ) 82 | 83 | export default service 84 | -------------------------------------------------------------------------------- /src/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | /** 16 | * Because it's so fucking difficult to detect the scrolling element, just move them all 17 | * @param {number} amount 18 | */ 19 | function move(amount) { 20 | document.documentElement.scrollTop = amount 21 | document.body.parentNode.scrollTop = amount 22 | document.body.scrollTop = amount 23 | } 24 | 25 | function position() { 26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 27 | } 28 | 29 | /** 30 | * @param {number} to 31 | * @param {number} duration 32 | * @param {Function} callback 33 | */ 34 | export function scrollTo(to, duration, callback) { 35 | const start = position() 36 | const change = to - start 37 | const increment = 20 38 | let currentTime = 0 39 | duration = (typeof (duration) === 'undefined') ? 500 : duration 40 | var animateScroll = function() { 41 | // increment the time 42 | currentTime += increment 43 | // find the value with the quadratic in-out easing function 44 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 45 | // move the document.body 46 | move(val) 47 | // do the animation unless its over 48 | if (currentTime < duration) { 49 | requestAnimFrame(animateScroll) 50 | } else { 51 | if (callback && typeof (callback) === 'function') { 52 | // the animation is done so lets callback 53 | callback() 54 | } 55 | } 56 | } 57 | animateScroll() 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param {string} path 4 | * @returns {Boolean} 5 | */ 6 | export function isExternal(path) { 7 | return /^(https?:|mailto:|tel:)/.test(path) 8 | } 9 | 10 | /** 11 | * @param {string} url 12 | * @returns {Boolean} 13 | */ 14 | export function validURL(url) { 15 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 16 | return reg.test(url) 17 | } 18 | 19 | /** 20 | * @param {string} str 21 | * @returns {Boolean} 22 | */ 23 | export function validLowerCase(str) { 24 | const reg = /^[a-z]+$/ 25 | return reg.test(str) 26 | } 27 | 28 | /** 29 | * @param {string} str 30 | * @returns {Boolean} 31 | */ 32 | export function validUpperCase(str) { 33 | const reg = /^[A-Z]+$/ 34 | return reg.test(str) 35 | } 36 | 37 | /** 38 | * @param {string} str 39 | * @returns {Boolean} 40 | */ 41 | export function validAlphabets(str) { 42 | const reg = /^[A-Za-z]+$/ 43 | return reg.test(str) 44 | } 45 | 46 | /** 47 | * @param {string} email 48 | * @returns {Boolean} 49 | */ 50 | export function validEmail(email) { 51 | const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 52 | return reg.test(email) 53 | } 54 | 55 | /** 56 | * @param {string} str 57 | * @returns {Boolean} 58 | */ 59 | export function isString(str) { 60 | if (typeof str === 'string' || str instanceof String) { 61 | return true 62 | } 63 | return false 64 | } 65 | 66 | /** 67 | * @param {Array} arg 68 | * @returns {Boolean} 69 | */ 70 | export function isArray(arg) { 71 | if (typeof Array.isArray === 'undefined') { 72 | return Object.prototype.toString.call(arg) === '[object Array]' 73 | } 74 | return Array.isArray(arg) 75 | } 76 | -------------------------------------------------------------------------------- /src/views/dashboard/components/BarChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 103 | -------------------------------------------------------------------------------- /src/views/dashboard/components/PieChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 80 | -------------------------------------------------------------------------------- /src/views/dashboard/components/TransactionTable.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 45 | -------------------------------------------------------------------------------- /src/views/dashboard/components/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null, 7 | $_resizeHandler: null 8 | } 9 | }, 10 | mounted() { 11 | this.$_resizeHandler = debounce(() => { 12 | if (this.chart) { 13 | this.chart.resize() 14 | } 15 | }, 100) 16 | this.$_initResizeEvent() 17 | this.$_initSidebarResizeEvent() 18 | }, 19 | beforeDestroy() { 20 | this.$_destroyResizeEvent() 21 | this.$_destroySidebarResizeEvent() 22 | }, 23 | // to fixed bug when cached by keep-alive 24 | activated() { 25 | this.$_initResizeEvent() 26 | this.$_initSidebarResizeEvent() 27 | }, 28 | deactivated() { 29 | this.$_destroyResizeEvent() 30 | this.$_destroySidebarResizeEvent() 31 | }, 32 | methods: { 33 | // use $_ for mixins properties 34 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 35 | $_initResizeEvent() { 36 | window.addEventListener('resize', this.$_resizeHandler) 37 | }, 38 | $_destroyResizeEvent() { 39 | window.removeEventListener('resize', this.$_resizeHandler) 40 | }, 41 | $_sidebarResizeHandler(e) { 42 | if (e.propertyName === 'width') { 43 | this.$_resizeHandler() 44 | } 45 | }, 46 | $_initSidebarResizeEvent() { 47 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 48 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) 49 | }, 50 | $_destroySidebarResizeEvent() { 51 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 81 | 82 | 112 | -------------------------------------------------------------------------------- /src/views/demo/demo-1/demo-1-1/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/demo/demo-1/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/demo/demo-2/demo-2-1/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/demo/demo-2/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/error-page/401.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 60 | 61 | 100 | -------------------------------------------------------------------------------- /src/views/login/auth-redirect.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | describe('Utils:formatTime', () => { 3 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 4 | const retrofit = 5 * 1000 5 | 6 | it('ten digits timestamp', () => { 7 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 8 | }) 9 | it('test now', () => { 10 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 11 | }) 12 | it('less two minute', () => { 13 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 14 | }) 15 | it('less two hour', () => { 16 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 17 | }) 18 | it('less one day', () => { 19 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 20 | }) 21 | it('more than one day', () => { 22 | expect(formatTime(d)).toBe('7月13日17时54分') 23 | }) 24 | it('format', () => { 25 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 26 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 27 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | 9 | it('timestamp string', () => { 10 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') 11 | }) 12 | 13 | it('ten digits timestamp', () => { 14 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 15 | }) 16 | it('new Date', () => { 17 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 18 | }) 19 | it('format', () => { 20 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 21 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 22 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 23 | }) 24 | it('get the day of the week', () => { 25 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 26 | }) 27 | it('get the day of the week', () => { 28 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 29 | }) 30 | it('empty argument', () => { 31 | expect(parseTime()).toBeNull() 32 | }) 33 | 34 | it('null', () => { 35 | expect(parseTime(null)).toBeNull() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, validURL, validLowerCase, validUpperCase, validAlphabets } from '@/utils/validate.js' 2 | describe('Utils:validate', () => { 3 | it('validUsername', () => { 4 | expect(validUsername('admin')).toBe(true) 5 | expect(validUsername('editor')).toBe(true) 6 | expect(validUsername('xxxx')).toBe(false) 7 | }) 8 | it('validURL', () => { 9 | expect(validURL('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 10 | expect(validURL('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(validURL('github.com/PanJiaChen/vue-element-admin')).toBe(false) 12 | }) 13 | it('validLowerCase', () => { 14 | expect(validLowerCase('abc')).toBe(true) 15 | expect(validLowerCase('Abc')).toBe(false) 16 | expect(validLowerCase('123abc')).toBe(false) 17 | }) 18 | it('validUpperCase', () => { 19 | expect(validUpperCase('ABC')).toBe(true) 20 | expect(validUpperCase('Abc')).toBe(false) 21 | expect(validUpperCase('123ABC')).toBe(false) 22 | }) 23 | it('validAlphabets', () => { 24 | expect(validAlphabets('ABC')).toBe(true) 25 | expect(validAlphabets('Abc')).toBe(true) 26 | expect(validAlphabets('123aBC')).toBe(false) 27 | }) 28 | }) 29 | --------------------------------------------------------------------------------