├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README-zh.md ├── README.md ├── babel.config.js ├── build └── index.js ├── jsconfig.json ├── mock ├── article.js ├── charts.js ├── index.js ├── mock-server.js ├── online.js ├── public.js ├── remote-search.js ├── role │ ├── index.js │ └── routes.js ├── table.js ├── user.js └── utils.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── index.html └── province │ ├── anhui.json │ ├── aomen.json │ ├── beijing.json │ ├── china-contour.json │ ├── chongqing.json │ ├── fujian.json │ ├── gansu.json │ ├── guangdong.json │ ├── guangxi.json │ ├── guizhou.json │ ├── hainan.json │ ├── hebei.json │ ├── heilongjiang.json │ ├── henan.json │ ├── hubei.json │ ├── hunan.json │ ├── jiangsu.json │ ├── jiangxi.json │ ├── jilin.json │ ├── liaoning.json │ ├── neimenggu.json │ ├── ningxia.json │ ├── qinghai.json │ ├── shandong.json │ ├── shanghai.json │ ├── shanxi.json │ ├── sichuan.json │ ├── taiwan.json │ ├── tianjin.json │ ├── xianggang.json │ ├── xinjiang.json │ ├── xizang.json │ ├── yunnan.json │ └── zhejiang.json ├── src ├── App.vue ├── api │ ├── article.js │ ├── charts.js │ ├── online.js │ ├── public.js │ ├── table.js │ └── user.js ├── assets │ ├── 401_images │ │ └── 401.gif │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ └── mapJson │ │ ├── HK.json │ │ └── china.json ├── components │ ├── Breadcrumb │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── Online │ │ └── list.vue │ ├── Pagination │ │ └── index.vue │ ├── ProDialog │ │ ├── draggable.js │ │ └── index.vue │ ├── ProForm │ │ └── index.vue │ ├── ProTable │ │ ├── index.vue │ │ └── table-setting.vue │ ├── Redirect │ │ └── index.vue │ ├── RightPanel │ │ └── index.vue │ ├── SearchForm │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ └── sendCode │ │ └── index.vue ├── core │ ├── echarts.js │ ├── globalProperties.js │ ├── lazy_use.js │ └── use.js ├── directive │ ├── clipboard │ │ ├── clipboard.js │ │ └── index.js │ ├── fullscreen │ │ ├── fullscreen.js │ │ └── index.js │ ├── index.js │ └── waves │ │ ├── index.js │ │ ├── waves.css │ │ └── waves.js ├── hooks │ ├── dict.js │ ├── echarts.js │ └── table.js ├── icons │ ├── index.js │ ├── svg │ │ ├── 404.svg │ │ ├── bug.svg │ │ ├── chart.svg │ │ ├── clipboard.svg │ │ ├── component.svg │ │ ├── dashboard.svg │ │ ├── documentation.svg │ │ ├── drag.svg │ │ ├── edit.svg │ │ ├── education.svg │ │ ├── email.svg │ │ ├── example.svg │ │ ├── excel.svg │ │ ├── exit-fullscreen.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── form.svg │ │ ├── fullscreen-exit.svg │ │ ├── fullscreen.svg │ │ ├── guide.svg │ │ ├── icon.svg │ │ ├── international.svg │ │ ├── language.svg │ │ ├── link.svg │ │ ├── list.svg │ │ ├── lock.svg │ │ ├── message.svg │ │ ├── money.svg │ │ ├── nested.svg │ │ ├── password.svg │ │ ├── pdf.svg │ │ ├── people.svg │ │ ├── peoples.svg │ │ ├── qq.svg │ │ ├── search.svg │ │ ├── shopping.svg │ │ ├── size.svg │ │ ├── skill.svg │ │ ├── star.svg │ │ ├── tab.svg │ │ ├── table.svg │ │ ├── theme.svg │ │ ├── tree-table.svg │ │ ├── tree.svg │ │ ├── user.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 │ │ │ └── index.vue │ │ └── index.js │ ├── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── main.js ├── permission.js ├── router │ ├── index.js │ └── modules │ │ ├── chart.js │ │ ├── components-page.js │ │ ├── error-page.js │ │ ├── form.js │ │ ├── nested.js │ │ └── table.js ├── settings.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── charts.js │ │ ├── errorLog.js │ │ ├── permission.js │ │ ├── settings.js │ │ ├── tagsView.js │ │ └── user.js ├── styles │ ├── editable-cell.scss │ ├── element-ui.scss │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── auth.js │ ├── clipboard.js │ ├── echarts.js │ ├── get-page-title.js │ ├── index.js │ ├── request.js │ ├── scroll-to.js │ └── validate.js └── views │ ├── charts │ ├── Bar.vue │ ├── Line.vue │ ├── Map.vue │ ├── Pie.vue │ ├── components │ │ └── CardChart.vue │ └── data.js │ ├── components-page │ └── pro-dialog-view.vue │ ├── dashboard │ └── index.vue │ ├── error-page │ ├── 401.vue │ └── 404.vue │ ├── form │ ├── advanced-form.vue │ ├── columns │ │ ├── advance.js │ │ ├── linkage.js │ │ ├── list.js │ │ ├── modal.js │ │ └── step-list.js │ ├── index.vue │ ├── linkage-form.vue │ ├── modal-form.vue │ └── step-form.vue │ ├── icons │ ├── element-icons.js │ ├── index.vue │ └── svg-icons.js │ ├── login │ └── index.vue │ ├── nested │ ├── menu1 │ │ ├── index.vue │ │ ├── menu1-1 │ │ │ └── index.vue │ │ ├── menu1-2 │ │ │ ├── index.vue │ │ │ ├── menu1-2-1 │ │ │ │ └── index.vue │ │ │ └── menu1-2-2 │ │ │ │ └── index.vue │ │ └── menu1-3 │ │ │ └── index.vue │ └── menu2 │ │ └── index.vue │ ├── online │ └── createList.vue │ ├── table │ ├── columns │ │ ├── inline.js │ │ └── list.js │ ├── complex-table.vue │ ├── components │ │ └── editable-cell │ │ │ ├── Important.vue │ │ │ └── Status.vue │ └── inline-edit-table.vue │ ├── tailwindcss │ └── index.vue │ └── tree │ └── index.vue ├── tailwind.config.js └── vue.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://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 = '/dev-api' 6 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/prod-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 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'vue-eslint-parser', 4 | parserOptions: { 5 | parser: 'babel-eslint', 6 | ecmaVersion: 2020, 7 | sourceType: 'module', 8 | }, 9 | env: { 10 | browser: true, 11 | node: true, 12 | es6: true, 13 | }, 14 | // https://eslint.vuejs.org/user-guide/#usage 15 | extends: [ 16 | 'plugin:vue/vue3-recommended', 17 | 'eslint:recommended', 18 | 'prettier', 19 | 'prettier/vue', 20 | 'plugin:prettier/recommended', 21 | ], 22 | plugins: ['prettier'], 23 | // add your custom rules here 24 | // it is base on https://github.com/vuejs/eslint-config-vue 25 | rules: { 26 | 'vue/no-v-html': 'off', 27 | 'vue/no-v-model-argument': 'off', 28 | 'no-console': 'off', 29 | 'vue/no-mutating-props': 'off', 30 | 'no-var': 2, 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report(报告问题) 3 | about: Create a report to help us improve 4 | --- 5 | 10 | 11 | 12 | ## Bug report(问题描述) 13 | 14 | #### Steps to reproduce(问题复现步骤) 15 | 20 | 21 | #### Screenshot or Gif(截图或动态图) 22 | 23 | 24 | #### Link to minimal reproduction(最小可在线还原demo) 25 | 26 | 29 | 30 | #### Other relevant information(格外信息) 31 | - Your OS: 32 | - Node.js version: 33 | - vue3-admin-pro version: 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request(新功能建议) 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | ## Feature request(新功能建议) 7 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question(提问) 3 | about: Asking questions about use 4 | --- 5 | 6 | ## Question(提问) 7 | 8 | 15 | 16 | #### Steps to reproduce(问题复现步骤) 17 | 22 | 23 | #### Screenshot or Gif(截图或动态图) 24 | 25 | 26 | #### Link to minimal reproduction(最小可在线还原demo) 27 | 28 | 31 | 32 | #### Other relevant information(格外信息) 33 | - Your OS: 34 | - Node.js version: 35 | - vue3-admin-pro version: 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Build and Deploy Demo 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly. 12 | with: 13 | persist-credentials: false 14 | - name: Install and Build 15 | run: | 16 | npm install 17 | npm run-script build:prod 18 | - name: Deploy 19 | uses: JamesIves/github-pages-deploy-action@releases/v3 20 | with: 21 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 22 | BRANCH: gh-pages 23 | FOLDER: dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | tests/**/coverage/ 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "arrowParens": "always", 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present HaitaoWang555 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-zh.md: -------------------------------------------------------------------------------- 1 | # vue3-admin-pro 2 | 3 | > 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。 4 | 5 | [线上地址](https://HaitaoWang555.github.io/vue3-admin-pro) 6 | [线上地址](http://wanghaitao555.gitee.io/vue3-admin-pro) 7 | 8 | ## Build Setup 9 | 10 | ```bash 11 | # 克隆项目 12 | git clone https://github.com/HaitaoWang555/vue3-admin-pro.git 13 | 14 | # 进入项目目录 15 | cd vue3-admin-pro 16 | 17 | # 安装依赖 18 | npm install 19 | 20 | # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 21 | npm install --registry=https://registry.npm.taobao.org 22 | 23 | # 启动服务 24 | npm run dev 25 | ``` 26 | 27 | 浏览器访问 [http://localhost:9528](http://localhost:9528) 28 | 29 | ## 发布 30 | 31 | ```bash 32 | # 构建测试环境 33 | npm run build:stage 34 | 35 | # 构建生产环境 36 | npm run build:prod 37 | ``` 38 | 39 | ## 其它 40 | 41 | ```bash 42 | # 预览发布环境效果 43 | npm run preview 44 | 45 | # 预览发布环境效果 + 静态资源分析 46 | npm run preview -- --report 47 | 48 | # 代码格式检查 49 | npm run lint 50 | 51 | # 代码格式检查并自动修复 52 | npm run lint -- --fix 53 | ``` 54 | 55 | ## License 56 | 57 | [MIT](https://github.com/HaitaoWang555/vue3-admin-pro/blob/master/LICENSE) license. 58 | 59 | Copyright (c) 2017-present HaitaoWang555 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue3-admin-pro 2 | 3 | English | [简体中文](./README-zh.md) 4 | 5 | > A minimal Vue Admin Pro with Element UI & axios & iconfont & permission control & lint 6 | 7 | **Live demo:** https://HaitaoWang555.github.io/vue3-admin-pro 8 | **Live demo:** http://wanghaitao555.gitee.io/vue3-admin-pro 9 | 10 | 11 | ## Build Setup 12 | 13 | ```bash 14 | # clone the project 15 | git clone https://github.com/HaitaoWang555/vue3-admin-pro.git 16 | 17 | # enter the project directory 18 | cd vue3-admin-pro 19 | 20 | # install dependency 21 | npm install 22 | 23 | # develop 24 | npm run dev 25 | ``` 26 | 27 | This will automatically open http://localhost:9528 28 | 29 | ## Build 30 | 31 | ```bash 32 | # build for test environment 33 | npm run build:stage 34 | 35 | # build for production environment 36 | npm run build:prod 37 | ``` 38 | 39 | ## Advanced 40 | 41 | ```bash 42 | # preview the release environment effect 43 | npm run preview 44 | 45 | # preview the release environment effect + static resource analysis 46 | npm run preview -- --report 47 | 48 | # code format check 49 | npm run lint 50 | 51 | # code format check and auto fix 52 | npm run lint -- --fix 53 | ``` 54 | 55 | ## License 56 | 57 | [MIT](https://github.com/HaitaoWang555/vue3-admin-pro/blob/master/LICENSE) license. 58 | 59 | Copyright (c) 2017-present HaitaoWang555 60 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | plugins: [ 4 | [ 5 | 'import', 6 | { 7 | libraryName: 'element-plus', 8 | customStyleName: (name) => { 9 | name = name.slice(3) 10 | return `element-plus/packages/theme-chalk/src/${name}.scss` 11 | }, 12 | }, 13 | ], 14 | ], 15 | } 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 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["src/*"] 7 | } 8 | }, 9 | "exclude": ["node_modules", "dist"], 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /mock/charts.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | const data = { 4 | xData: [ 5 | '1月', 6 | '2月', 7 | '3月', 8 | '4月', 9 | '5月', 10 | '6月', 11 | '7月', 12 | '8月', 13 | '9月', 14 | '10月', 15 | '11月', 16 | '12月', 17 | ], 18 | yData: [], 19 | } 20 | 21 | const stackData = { 22 | xData: [ 23 | '1月', 24 | '2月', 25 | '3月', 26 | '4月', 27 | '5月', 28 | '6月', 29 | '7月', 30 | '8月', 31 | '9月', 32 | '10月', 33 | '11月', 34 | '12月', 35 | ], 36 | yData: { 37 | data1: [], 38 | data2: [], 39 | data3: [], 40 | data4: [], 41 | data5: [], 42 | data6: [], 43 | data7: [], 44 | data8: [], 45 | data9: [], 46 | }, 47 | } 48 | 49 | for (let index = 0; index < 12; index++) { 50 | data.yData.push(Mock.mock('@integer(300, 5000)')) 51 | stackData.yData.data1.push(Mock.mock('@integer(300, 5000)')) 52 | stackData.yData.data2.push(Mock.mock('@integer(300, 5000)')) 53 | stackData.yData.data3.push(Mock.mock('@integer(300, 5000)')) 54 | stackData.yData.data4.push(Mock.mock('@integer(300, 5000)')) 55 | stackData.yData.data5.push(Mock.mock('@integer(300, 5000)')) 56 | stackData.yData.data6.push(Mock.mock('@integer(300, 5000)')) 57 | stackData.yData.data7.push(Mock.mock('@integer(300, 5000)')) 58 | stackData.yData.data8.push(Mock.mock('@integer(300, 5000)')) 59 | stackData.yData.data9.push(Mock.mock('@integer(300, 5000)')) 60 | } 61 | 62 | module.exports = [ 63 | { 64 | url: '/vue3-admin-pro/charts/common', 65 | type: 'get', 66 | response: () => { 67 | return { 68 | code: 20000, 69 | data, 70 | } 71 | }, 72 | }, 73 | { 74 | url: '/vue3-admin-pro/charts/stack', 75 | type: 'get', 76 | response: () => { 77 | return { 78 | code: 20000, 79 | data: stackData, 80 | } 81 | }, 82 | }, 83 | ] 84 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const { param2Obj } = require('./utils') 3 | 4 | const user = require('./user') 5 | const publicApi = require('./public') 6 | const table = require('./table') 7 | const role = require('./role') 8 | const article = require('./article') 9 | const search = require('./remote-search') 10 | const charts = require('./charts') 11 | const online = require('./online') 12 | 13 | const mocks = [ 14 | ...publicApi, 15 | ...user, 16 | ...role, 17 | ...article, 18 | ...search, 19 | ...table, 20 | ...charts, 21 | ...online, 22 | ] 23 | 24 | // for front mock 25 | // please use it cautiously, it will redefine XMLHttpRequest, 26 | // which will cause many of your third-party libraries to be invalidated(like progress event). 27 | function mockXHR() { 28 | // mock patch 29 | // https://github.com/nuysoft/Mock/issues/300 30 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 31 | Mock.XHR.prototype.send = function () { 32 | if (this.custom.xhr) { 33 | this.custom.xhr.withCredentials = this.withCredentials || false 34 | 35 | if (this.responseType) { 36 | this.custom.xhr.responseType = this.responseType 37 | } 38 | } 39 | this.proxy_send(...arguments) 40 | } 41 | 42 | function XHR2ExpressReqWrap(respond) { 43 | return function (options) { 44 | let result = null 45 | if (respond instanceof Function) { 46 | const { body, type, url } = options 47 | // https://expressjs.com/en/4x/api.html#req 48 | result = respond({ 49 | method: type, 50 | body: JSON.parse(body), 51 | query: param2Obj(url), 52 | }) 53 | } else { 54 | result = respond 55 | } 56 | return Mock.mock(result) 57 | } 58 | } 59 | 60 | for (const i of mocks) { 61 | Mock.mock( 62 | new RegExp(i.url), 63 | i.type || 'get', 64 | XHR2ExpressReqWrap(i.response) 65 | ) 66 | } 67 | } 68 | 69 | module.exports = { 70 | mocks, 71 | mockXHR, 72 | } 73 | -------------------------------------------------------------------------------- /mock/mock-server.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const bodyParser = require('body-parser') 3 | const chalk = require('chalk') 4 | const path = require('path') 5 | const Mock = require('mockjs') 6 | 7 | const mockDir = path.join(process.cwd(), 'mock') 8 | 9 | function registerRoutes(app) { 10 | let mockLastIndex 11 | const { mocks } = require('./index.js') 12 | const mocksForServer = mocks.map((route) => { 13 | return responseFake(route.url, route.type, route.response) 14 | }) 15 | for (const mock of mocksForServer) { 16 | app[mock.type](mock.url, mock.response) 17 | mockLastIndex = app._router.stack.length 18 | } 19 | const mockRoutesLength = Object.keys(mocksForServer).length 20 | return { 21 | mockRoutesLength: mockRoutesLength, 22 | mockStartIndex: mockLastIndex - mockRoutesLength, 23 | } 24 | } 25 | 26 | function unregisterRoutes() { 27 | Object.keys(require.cache).forEach((i) => { 28 | if (i.includes(mockDir)) { 29 | delete require.cache[require.resolve(i)] 30 | } 31 | }) 32 | } 33 | 34 | // for mock server 35 | const responseFake = (url, type, respond) => { 36 | return { 37 | url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), 38 | type: type || 'get', 39 | response(req, res) { 40 | console.log('request invoke:' + req.path) 41 | res.json( 42 | Mock.mock(respond instanceof Function ? respond(req, res) : respond) 43 | ) 44 | }, 45 | } 46 | } 47 | 48 | module.exports = (app) => { 49 | // parse app.body 50 | // https://expressjs.com/en/4x/api.html#req.body 51 | app.use(bodyParser.json()) 52 | app.use( 53 | bodyParser.urlencoded({ 54 | extended: true, 55 | }) 56 | ) 57 | 58 | const mockRoutes = registerRoutes(app) 59 | let mockRoutesLength = mockRoutes.mockRoutesLength 60 | let mockStartIndex = mockRoutes.mockStartIndex 61 | 62 | // watch files, hot reload mock server 63 | chokidar 64 | .watch(mockDir, { 65 | ignored: /mock-server/, 66 | ignoreInitial: true, 67 | }) 68 | .on('all', (event, path) => { 69 | if (event === 'change' || event === 'add') { 70 | try { 71 | // remove mock routes stack 72 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 73 | 74 | // clear routes cache 75 | unregisterRoutes() 76 | 77 | const mockRoutes = registerRoutes(app) 78 | mockRoutesLength = mockRoutes.mockRoutesLength 79 | mockStartIndex = mockRoutes.mockStartIndex 80 | 81 | console.log( 82 | chalk.magentaBright( 83 | `\n > Mock Server hot reload success! changed ${path}` 84 | ) 85 | ) 86 | } catch (error) { 87 | console.log(chalk.redBright(error)) 88 | } 89 | } 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /mock/public.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | const data = Mock.mock('@integer(999, 9999)') 4 | 5 | module.exports = [ 6 | { 7 | url: '/vue3-admin-pro/public/sendSms', 8 | type: 'get', 9 | response: () => { 10 | return { 11 | code: 20000, 12 | msg: 'success', 13 | data, 14 | } 15 | }, 16 | }, 17 | // submit 18 | { 19 | url: '/vue3-admin-pro/public/submit', 20 | type: 'post', 21 | response: () => { 22 | return { 23 | code: 20000, 24 | msg: 'success', 25 | } 26 | }, 27 | }, 28 | ] 29 | -------------------------------------------------------------------------------- /mock/remote-search.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | const NameList = [] 4 | const count = 100 5 | 6 | for (let i = 0; i < count; i++) { 7 | NameList.push( 8 | Mock.mock({ 9 | name: '@first', 10 | }) 11 | ) 12 | } 13 | NameList.push({ name: 'mock-Pan' }) 14 | 15 | module.exports = [ 16 | // username search 17 | { 18 | url: '/vue3-admin-pro/search/user', 19 | type: 'get', 20 | response: (config) => { 21 | const { name } = config.query 22 | const mockNameList = NameList.filter((item) => { 23 | const lowerCaseName = item.name.toLowerCase() 24 | return !(name && lowerCaseName.indexOf(name.toLowerCase()) < 0) 25 | }) 26 | return { 27 | code: 20000, 28 | data: { items: mockNameList }, 29 | } 30 | }, 31 | }, 32 | 33 | // transaction list 34 | { 35 | url: '/vue3-admin-pro/transaction/list', 36 | type: 'get', 37 | response: () => { 38 | return { 39 | code: 20000, 40 | data: { 41 | total: 20, 42 | 'items|20': [ 43 | { 44 | order_no: '@guid()', 45 | timestamp: +Mock.Random.date('T'), 46 | username: '@name()', 47 | price: '@float(1000, 15000, 0, 2)', 48 | 'status|1': ['success', 'pending'], 49 | }, 50 | ], 51 | }, 52 | } 53 | }, 54 | }, 55 | ] 56 | -------------------------------------------------------------------------------- /mock/role/index.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const { deepClone } = require('../utils') 3 | const { asyncRoutes, constantRoutes } = require('./routes.js') 4 | 5 | const routes = deepClone([...constantRoutes, ...asyncRoutes]) 6 | 7 | const roles = [ 8 | { 9 | key: 'admin', 10 | name: 'admin', 11 | description: 'Super Administrator. Have access to view all pages.', 12 | routes: routes, 13 | }, 14 | { 15 | key: 'editor', 16 | name: 'editor', 17 | description: 'Normal Editor. Can see all pages except permission page', 18 | routes: routes.filter((i) => i.path !== '/permission'), // just a mock 19 | }, 20 | { 21 | key: 'visitor', 22 | name: 'visitor', 23 | description: 24 | 'Just a visitor. Can only see the home page and the document page', 25 | routes: [ 26 | { 27 | path: '', 28 | redirect: 'dashboard', 29 | children: [ 30 | { 31 | path: 'dashboard', 32 | name: 'Dashboard', 33 | meta: { title: 'dashboard', icon: 'dashboard' }, 34 | }, 35 | ], 36 | }, 37 | ], 38 | }, 39 | ] 40 | 41 | module.exports = [ 42 | // mock get all routes form server 43 | { 44 | url: '/vue3-admin-pro/routes', 45 | type: 'get', 46 | response: () => { 47 | return { 48 | code: 20000, 49 | data: routes, 50 | } 51 | }, 52 | }, 53 | 54 | // mock get all roles form server 55 | { 56 | url: '/vue3-admin-pro/roles', 57 | type: 'get', 58 | response: () => { 59 | return { 60 | code: 20000, 61 | data: roles, 62 | } 63 | }, 64 | }, 65 | 66 | // add role 67 | { 68 | url: '/vue3-admin-pro/role', 69 | type: 'post', 70 | response: { 71 | code: 20000, 72 | data: { 73 | key: Mock.mock('@integer(300, 5000)'), 74 | }, 75 | }, 76 | }, 77 | 78 | // update role 79 | { 80 | url: '/vue3-admin-pro/role/[A-Za-z0-9]', 81 | type: 'put', 82 | response: { 83 | code: 20000, 84 | data: { 85 | status: 'success', 86 | }, 87 | }, 88 | }, 89 | 90 | // delete role 91 | { 92 | url: '/vue3-admin-pro/role/[A-Za-z0-9]', 93 | type: 'delete', 94 | response: { 95 | code: 20000, 96 | data: { 97 | status: 'success', 98 | }, 99 | }, 100 | }, 101 | ] 102 | -------------------------------------------------------------------------------- /mock/table.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | const data = Mock.mock({ 4 | 'items|30': [ 5 | { 6 | id: '@id', 7 | title: '@sentence(10, 20)', 8 | 'status|1': ['published', 'draft', 'deleted'], 9 | author: 'name', 10 | display_time: '@datetime', 11 | pageviews: '@integer(300, 5000)', 12 | }, 13 | ], 14 | }) 15 | 16 | module.exports = [ 17 | { 18 | url: '/vue3-admin-pro/table/list', 19 | type: 'get', 20 | response: () => { 21 | const items = data.items 22 | return { 23 | code: 20000, 24 | data: { 25 | total: items.length, 26 | items: items, 27 | }, 28 | } 29 | }, 30 | }, 31 | ] 32 | -------------------------------------------------------------------------------- /mock/user.js: -------------------------------------------------------------------------------- 1 | const tokens = { 2 | admin: { 3 | token: 'admin-token', 4 | }, 5 | editor: { 6 | token: 'editor-token', 7 | }, 8 | } 9 | 10 | const users = { 11 | 'admin-token': { 12 | roles: ['admin'], 13 | introduction: 'I am a super administrator', 14 | avatar: 15 | 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 16 | name: 'Super Admin', 17 | }, 18 | 'editor-token': { 19 | roles: ['editor'], 20 | introduction: 'I am an editor', 21 | avatar: 22 | 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 23 | name: 'Normal Editor', 24 | }, 25 | } 26 | 27 | module.exports = [ 28 | // user login 29 | { 30 | url: '/vue3-admin-pro/user/login', 31 | type: 'post', 32 | response: (config) => { 33 | const { username } = config.body 34 | const token = tokens[username] 35 | 36 | // mock error 37 | if (!token) { 38 | return { 39 | code: 60204, 40 | message: 'Account and password are incorrect.', 41 | } 42 | } 43 | 44 | return { 45 | code: 20000, 46 | data: token, 47 | } 48 | }, 49 | }, 50 | 51 | // get user info 52 | { 53 | url: '/vue3-admin-pro/user/info.*', 54 | type: 'get', 55 | response: (config) => { 56 | const { token } = config.query 57 | const info = users[token] 58 | 59 | // mock error 60 | if (!info) { 61 | return { 62 | code: 50008, 63 | message: 'Login failed, unable to get user details.', 64 | } 65 | } 66 | 67 | return { 68 | code: 20000, 69 | data: info, 70 | } 71 | }, 72 | }, 73 | 74 | // user logout 75 | { 76 | url: '/vue3-admin-pro/user/logout', 77 | type: 'post', 78 | response: () => { 79 | return { 80 | code: 20000, 81 | data: 'success', 82 | } 83 | }, 84 | }, 85 | ] 86 | -------------------------------------------------------------------------------- /mock/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} url 3 | * @returns {Object} 4 | */ 5 | function param2Obj(url) { 6 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 7 | if (!search) { 8 | return {} 9 | } 10 | const obj = {} 11 | const searchArr = search.split('&') 12 | searchArr.forEach((v) => { 13 | const index = v.indexOf('=') 14 | if (index !== -1) { 15 | const name = v.substring(0, index) 16 | const val = v.substring(index + 1, v.length) 17 | obj[name] = val 18 | } 19 | }) 20 | return obj 21 | } 22 | 23 | /** 24 | * This is just a simple version of deep copy 25 | * Has a lot of edge cases bug 26 | * If you want to use a perfect deep copy, use lodash's _.cloneDeep 27 | * @param {Object} source 28 | * @returns {Object} 29 | */ 30 | function deepClone(source) { 31 | if (!source && typeof source !== 'object') { 32 | throw new Error('error arguments', 'deepClone') 33 | } 34 | const targetObj = source.constructor === Array ? [] : {} 35 | Object.keys(source).forEach((keys) => { 36 | if (source[keys] && typeof source[keys] === 'object') { 37 | targetObj[keys] = deepClone(source[keys]) 38 | } else { 39 | targetObj[keys] = source[keys] 40 | } 41 | }) 42 | return targetObj 43 | } 44 | 45 | module.exports = { 46 | param2Obj, 47 | deepClone, 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-admin-pro", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://HaitaoWang555.github.io/vue3-admin-pro", 6 | "scripts": { 7 | "dev": "vue-cli-service serve", 8 | "build:prod": "vue-cli-service build --report", 9 | "preview": "node build/index.js --preview", 10 | "build:stage": "vue-cli-service build --mode staging", 11 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", 12 | "lint": "vue-cli-service lint" 13 | }, 14 | "dependencies": { 15 | "axios": "^0.21.1", 16 | "clipboard": "^2.0.6", 17 | "core-js": "^3.6.5", 18 | "echarts": "^5.0.2", 19 | "element-plus": "^1.0.2-beta.71", 20 | "normalize.css": "^8.0.1", 21 | "nprogress": "^0.2.0", 22 | "path-to-regexp": "^6.2.0", 23 | "store": "^2.0.12", 24 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.7", 25 | "vue": "^3.2.4", 26 | "vue-router": "^4.0.11", 27 | "vuedraggable": "^4.0.1", 28 | "vuex": "^4.0.2" 29 | }, 30 | "devDependencies": { 31 | "@tailwindcss/postcss7-compat": "^2.2.7", 32 | "@typescript-eslint/eslint-plugin": "^4.10.0", 33 | "@typescript-eslint/parser": "^4.10.0", 34 | "@vue/cli-plugin-babel": "~4.5.0", 35 | "@vue/cli-plugin-eslint": "~4.5.0", 36 | "@vue/cli-service": "^4.5.13", 37 | "@vue/compiler-sfc": "^3.2.4", 38 | "@vue/eslint-config-standard": "^6.0.0", 39 | "babel-eslint": "^10.1.0", 40 | "babel-plugin-component": "^1.1.1", 41 | "babel-plugin-import": "^1.13.3", 42 | "chalk": "^4.1.0", 43 | "connect": "^3.7.0", 44 | "eslint": "^7.25.0", 45 | "eslint-config-prettier": "^7.1.0", 46 | "eslint-plugin-html": "^6.1.1", 47 | "eslint-plugin-import": "^2.22.1", 48 | "eslint-plugin-node": "^11.1.0", 49 | "eslint-plugin-prettier": "^3.3.0", 50 | "eslint-plugin-promise": "^4.2.1", 51 | "eslint-plugin-standard": "^5.0.0", 52 | "eslint-plugin-vue": "^7.9.0", 53 | "mockjs": "^1.1.0", 54 | "prettier": "^2.2.1", 55 | "runjs": "^4.4.2", 56 | "sass": "^1.38.0", 57 | "sass-loader": "^10.2.0", 58 | "script-ext-html-webpack-plugin": "^2.1.5", 59 | "svg-sprite-loader": "^5.2.1", 60 | "svgo": "^1.3.2", 61 | "webpack-bundle-analyzer": "^4.4.1" 62 | }, 63 | "browserslist": [ 64 | "> 1%", 65 | "last 2 versions", 66 | "not dead" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HaitaoWang555/vue3-admin-pro/03c70012a3068dbc2fb33f4c71316f0ed091f278/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/province/aomen.json: -------------------------------------------------------------------------------- 1 | {"type":"FeatureCollection","features":[{"id":"820001","type":"Feature","geometry":{"type":"MultiPolygon","coordinates":[["@@LADC^umZ@DONWE@DALBBF@H@DFBBTC"],["@@P@LC@AGM@OECMBABBTCD@DDH"]],"encodeOffsets":[[[116285,22746]],[[116303,22746]]]},"properties":{"cp":[113.552965,22.207882],"name":"花地玛堂区","childNum":2}},{"id":"820002","type":"Feature","geometry":{"type":"Polygon","coordinates":["@@MK@CA@AAGDEB@NVFJG"],"encodeOffsets":[[116281,22734]]},"properties":{"cp":[113.549052,22.199175],"name":"花王堂区","childNum":1}},{"id":"820003","type":"Feature","geometry":{"type":"Polygon","coordinates":["@@EGOB@DNLHE@C"],"encodeOffsets":[[116285,22729]]},"properties":{"cp":[113.550252,22.193791],"name":"望德堂区","childNum":1}},{"id":"820004","type":"Feature","geometry":{"type":"Polygon","coordinates":["@@ŸYMVAN@BFCBBDAFHDBBFDHIJJEFDPCHHlYJQ"],"encodeOffsets":[[116313,22707]]},"properties":{"cp":[113.55374,22.188119],"name":"大堂区","childNum":1}},{"id":"820005","type":"Feature","geometry":{"type":"Polygon","coordinates":["@@JICGAECACGEBAAEDBFNXB@"],"encodeOffsets":[[116266,22728]]},"properties":{"cp":[113.54167,22.187778],"name":"风顺堂区","childNum":1}},{"id":"820006","type":"Feature","geometry":{"type":"Polygon","coordinates":["@@ ZNWRquZCBCC@AEA@@ADCDCAACEAGBQ@INEL"],"encodeOffsets":[[116265,22694]]},"properties":{"cp":[113.558783,22.154124],"name":"嘉模堂区","childNum":1}},{"id":"820007","type":"Feature","geometry":{"type":"Polygon","coordinates":["@@MOIAIEI@@GE@AAUCBdCFIFR@HAFBBDDBDCBC@@FB@BDDDA\\M"],"encodeOffsets":[[116316,22676]]},"properties":{"cp":[113.56925,22.136546],"name":"路凼填海区","childNum":1}},{"id":"820008","type":"Feature","geometry":{"type":"Polygon","coordinates":["@@DKMMa_GC_COD@dVDBBF@@HJ@JFJBNPZK"],"encodeOffsets":[[116329,22670]]},"properties":{"cp":[113.559954,22.124049],"name":"圣方济各堂区","childNum":1}}],"UTF8Encoding":true} -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /src/api/article.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchList(query) { 4 | return request({ 5 | url: '/vue3-admin-pro/article/list', 6 | method: 'get', 7 | params: query, 8 | }) 9 | } 10 | 11 | export function fetchArticle(id) { 12 | return request({ 13 | url: '/vue3-admin-pro/article/detail', 14 | method: 'get', 15 | params: { id }, 16 | }) 17 | } 18 | 19 | export function fetchPv(pv) { 20 | return request({ 21 | url: '/vue3-admin-pro/article/pv', 22 | method: 'get', 23 | params: { pv }, 24 | }) 25 | } 26 | 27 | export function createArticle(data) { 28 | return request({ 29 | url: '/vue3-admin-pro/article/create', 30 | method: 'post', 31 | data, 32 | }) 33 | } 34 | 35 | export function updateArticle(data) { 36 | return request({ 37 | url: '/vue3-admin-pro/article/update', 38 | method: 'post', 39 | data, 40 | }) 41 | } 42 | export function getStatus() { 43 | return request({ 44 | url: '/vue3-admin-pro/article/getStatus', 45 | method: 'get', 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/api/charts.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 获取图表通用数据 5 | * @returns 6 | */ 7 | export function getLineList() { 8 | return request({ 9 | url: '/vue3-admin-pro/charts/common', 10 | method: 'get', 11 | }) 12 | } 13 | 14 | export function getLineStackList() { 15 | return request({ 16 | url: '/vue3-admin-pro/charts/stack', 17 | method: 'get', 18 | }) 19 | } 20 | export function getBarList() { 21 | return request({ 22 | url: '/vue3-admin-pro/charts/common', 23 | method: 'get', 24 | }) 25 | } 26 | 27 | export function getBarStackList() { 28 | return request({ 29 | url: '/vue3-admin-pro/charts/stack', 30 | method: 'get', 31 | }) 32 | } 33 | export function getPieList() { 34 | return request({ 35 | url: '/vue3-admin-pro/charts/common', 36 | method: 'get', 37 | }) 38 | } 39 | 40 | export function getPieRoseTypeList() { 41 | return request({ 42 | url: '/vue3-admin-pro/charts/stack', 43 | method: 'get', 44 | }) 45 | } 46 | export function getMapJson(name) { 47 | return request({ 48 | url: `/vue3-admin-pro/province/${name}.json`, 49 | method: 'get', 50 | baseURL: '', 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /src/api/online.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getOnlineListOptions(query) { 4 | return request({ 5 | url: '/vue3-admin-pro/online/options', 6 | method: 'get', 7 | params: query, 8 | }) 9 | } 10 | export function getOnlineList(query) { 11 | return request({ 12 | url: '/vue3-admin-pro/online/list', 13 | method: 'get', 14 | params: query, 15 | }) 16 | } 17 | export function get(url, query) { 18 | return request({ 19 | url: url, 20 | method: 'get', 21 | params: query, 22 | }) 23 | } 24 | export function post(url, data) { 25 | return request({ 26 | url: url, 27 | method: 'post', 28 | data, 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/api/public.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function sendSms(params) { 4 | return request({ 5 | url: '/vue3-admin-pro/public/sendSms', 6 | method: 'get', 7 | params, 8 | }) 9 | } 10 | export function submit(data) { 11 | return request({ 12 | url: '/vue3-admin-pro/public/submit', 13 | method: 'post', 14 | data, 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/api/table.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList(params) { 4 | return request({ 5 | url: '/vue3-admin-pro/table/list', 6 | method: 'get', 7 | params, 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/vue3-admin-pro/user/login', 6 | method: 'post', 7 | data, 8 | }) 9 | } 10 | 11 | export function getInfo(token) { 12 | return request({ 13 | url: '/vue3-admin-pro/user/info', 14 | method: 'get', 15 | params: { token }, 16 | }) 17 | } 18 | 19 | export function logout() { 20 | return request({ 21 | url: '/vue3-admin-pro/user/logout', 22 | method: 'post', 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HaitaoWang555/vue3-admin-pro/03c70012a3068dbc2fb33f4c71316f0ed091f278/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HaitaoWang555/vue3-admin-pro/03c70012a3068dbc2fb33f4c71316f0ed091f278/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HaitaoWang555/vue3-admin-pro/03c70012a3068dbc2fb33f4c71316f0ed091f278/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 74 | 75 | 88 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 38 | 39 | 51 | -------------------------------------------------------------------------------- /src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 96 | 97 | 109 | -------------------------------------------------------------------------------- /src/components/ProDialog/draggable.js: -------------------------------------------------------------------------------- 1 | export default function (el) { 2 | const dialogHeaderEl = el.querySelector('.el-dialog__header') 3 | const dragDom = el 4 | dialogHeaderEl.style.cssText += ';cursor:move;' 5 | dragDom.style.cssText += ';top:0px;' 6 | 7 | // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); 8 | const getStyle = (function () { 9 | if (window.document.currentStyle) { 10 | return (dom, attr) => dom.currentStyle[attr] 11 | } else { 12 | return (dom, attr) => getComputedStyle(dom, false)[attr] 13 | } 14 | })() 15 | 16 | if (dialogHeaderEl.onmousedown) return 17 | 18 | dialogHeaderEl.onmousedown = (e) => { 19 | // 鼠标按下,计算当前元素距离可视区的距离 20 | const disX = e.clientX - dialogHeaderEl.offsetLeft 21 | const disY = e.clientY - dialogHeaderEl.offsetTop 22 | 23 | const dragDomWidth = dragDom.offsetWidth 24 | const dragDomHeight = dragDom.offsetHeight 25 | 26 | const screenWidth = document.body.clientWidth 27 | const screenHeight = document.body.clientHeight 28 | 29 | const minDragDomLeft = dragDom.offsetLeft 30 | const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth 31 | 32 | const minDragDomTop = dragDom.offsetTop 33 | const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight 34 | 35 | // 获取到的值带px 正则匹配替换 36 | let styL = getStyle(dragDom, 'left') 37 | let styT = getStyle(dragDom, 'top') 38 | 39 | if (styL.includes('%')) { 40 | // eslint-disable-next-line no-useless-escape 41 | styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100) 42 | // eslint-disable-next-line no-useless-escape 43 | styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100) 44 | } else { 45 | styL = +styL.replace(/\px/g, '') 46 | styT = +styT.replace(/\px/g, '') 47 | } 48 | 49 | document.onmousemove = function (e) { 50 | // 通过事件委托,计算移动的距离 51 | let left = e.clientX - disX 52 | let top = e.clientY - disY 53 | 54 | // 边界处理 55 | if (-left > minDragDomLeft) { 56 | left = -minDragDomLeft 57 | } else if (left > maxDragDomLeft) { 58 | left = maxDragDomLeft 59 | } 60 | 61 | if (-top > minDragDomTop) { 62 | top = -minDragDomTop 63 | } else if (top > maxDragDomTop) { 64 | top = maxDragDomTop 65 | } 66 | 67 | // 移动当前元素 68 | dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;` 69 | } 70 | 71 | document.onmouseup = function () { 72 | document.onmousemove = null 73 | document.onmouseup = null 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/components/ProDialog/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 113 | -------------------------------------------------------------------------------- /src/components/Redirect/index.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 59 | 60 | 75 | -------------------------------------------------------------------------------- /src/components/sendCode/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 72 | -------------------------------------------------------------------------------- /src/core/echarts.js: -------------------------------------------------------------------------------- 1 | // Minimal Bundle 按需引入 2 | 3 | import * as echarts from 'echarts/core' 4 | 5 | import { 6 | BarChart, 7 | LineChart, 8 | PieChart, 9 | MapChart, 10 | PictorialBarChart, 11 | RadarChart, 12 | ScatterChart, 13 | } from 'echarts/charts' 14 | 15 | import { 16 | TitleComponent, 17 | TooltipComponent, 18 | GridComponent, 19 | PolarComponent, 20 | AriaComponent, 21 | ParallelComponent, 22 | LegendComponent, 23 | RadarComponent, 24 | VisualMapComponent, 25 | GeoComponent, 26 | } from 'echarts/components' 27 | 28 | import { CanvasRenderer } from 'echarts/renderers' 29 | 30 | echarts.use([ 31 | LegendComponent, 32 | TitleComponent, 33 | TooltipComponent, 34 | GridComponent, 35 | PolarComponent, 36 | AriaComponent, 37 | ParallelComponent, 38 | BarChart, 39 | LineChart, 40 | PieChart, 41 | MapChart, 42 | RadarChart, 43 | CanvasRenderer, 44 | PictorialBarChart, 45 | ScatterChart, 46 | RadarComponent, 47 | VisualMapComponent, 48 | GeoComponent, 49 | ]) 50 | 51 | export default echarts 52 | -------------------------------------------------------------------------------- /src/core/globalProperties.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils' 2 | 3 | export default (app) => { 4 | // filters 5 | app.config.globalProperties.$filters = { 6 | parseTime(value) { 7 | return parseTime(value, '{y}-{m}-{d} {h}:{i}:{s}') 8 | }, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/core/use.js: -------------------------------------------------------------------------------- 1 | // use components 2 | import SvgIcon from '@/components/SvgIcon/index.vue' // svg component 3 | import ProDialog from '@/components/ProDialog/index.vue' 4 | import ProTable from '@/components/ProTable/index.vue' 5 | import ProForm from '@/components/ProForm/index.vue' 6 | 7 | export default (app) => { 8 | app.component('SvgIcon', SvgIcon) 9 | app.component('ProDialog', ProDialog) 10 | app.component('ProTable', ProTable) 11 | app.component('ProForm', ProForm) 12 | } 13 | -------------------------------------------------------------------------------- /src/directive/clipboard/clipboard.js: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/Inndy/vue-clipboard2 2 | import Clipboard from 'clipboard' 3 | if (!Clipboard) { 4 | throw new Error('you should npm install `clipboard` --save at first ') 5 | } 6 | 7 | export default { 8 | mounted(el, binding) { 9 | if (binding.arg === 'success') { 10 | el._v_clipboard_success = binding.value 11 | } else if (binding.arg === 'error') { 12 | el._v_clipboard_error = binding.value 13 | } else { 14 | const clipboard = new Clipboard(el, { 15 | text() { 16 | return binding.value 17 | }, 18 | action() { 19 | return binding.arg === 'cut' ? 'cut' : 'copy' 20 | }, 21 | }) 22 | clipboard.on('success', (e) => { 23 | const callback = el._v_clipboard_success 24 | callback && callback(e) // eslint-disable-line 25 | }) 26 | clipboard.on('error', (e) => { 27 | const callback = el._v_clipboard_error 28 | callback && callback(e) // eslint-disable-line 29 | }) 30 | el._v_clipboard = clipboard 31 | } 32 | }, 33 | update(el, binding) { 34 | if (binding.arg === 'success') { 35 | el._v_clipboard_success = binding.value 36 | } else if (binding.arg === 'error') { 37 | el._v_clipboard_error = binding.value 38 | } else { 39 | el._v_clipboard.text = function () { 40 | return binding.value 41 | } 42 | el._v_clipboard.action = function () { 43 | return binding.arg === 'cut' ? 'cut' : 'copy' 44 | } 45 | } 46 | }, 47 | unmounted(el, binding) { 48 | if (binding.arg === 'success') { 49 | delete el._v_clipboard_success 50 | } else if (binding.arg === 'error') { 51 | delete el._v_clipboard_error 52 | } else { 53 | el._v_clipboard.destroy() 54 | delete el._v_clipboard 55 | } 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /src/directive/clipboard/index.js: -------------------------------------------------------------------------------- 1 | import Clipboard from './clipboard' 2 | 3 | const install = function (Vue) { 4 | Vue.directive('Clipboard', Clipboard) 5 | } 6 | 7 | if (window.Vue) { 8 | window.clipboard = Clipboard 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | Clipboard.install = install 13 | export default Clipboard 14 | -------------------------------------------------------------------------------- /src/directive/fullscreen/fullscreen.js: -------------------------------------------------------------------------------- 1 | import { 2 | addClass, 3 | hasClass, 4 | removeClass, 5 | getScrollBarWidth, 6 | } from '@/utils/index' 7 | 8 | const scrollBarWidth = getScrollBarWidth() 9 | let elFull, scrollY 10 | 11 | function handleClick(el) { 12 | if (hasClass(el, 'content-screenfull')) { 13 | removeClass(el, 'content-screenfull') 14 | document.documentElement.style.overflow = '' 15 | document.body.style.borderRight = '' 16 | window.scrollTo(0, scrollY) 17 | const callback = el._v_fullscreen_normal 18 | callback && callback() // eslint-disable-line 19 | } else { 20 | scrollY = window.scrollY 21 | addClass(el, 'content-screenfull') 22 | document.documentElement.style.overflow = 'hidden' 23 | document.body.style.borderRight = scrollBarWidth + 'px solid transparent' 24 | const callback = el._v_fullscreen_big 25 | callback && callback() // eslint-disable-line 26 | } 27 | } 28 | 29 | export default { 30 | mounted(el, binding) { 31 | switch (binding.arg) { 32 | case 'top': 33 | elFull = document.createElement('i') 34 | elFull.className = 'el-icon-full-screen absolute-wrap' 35 | elFull.style.top = binding.value + 'px' 36 | addClass(el, 'relative-wrap') 37 | el.appendChild(elFull) 38 | elFull.addEventListener( 39 | 'click', 40 | () => { 41 | handleClick(el) 42 | }, 43 | false 44 | ) 45 | break 46 | case 'big': 47 | el._v_fullscreen_big = binding.value 48 | break 49 | case 'normal': 50 | el._v_fullscreen_normal = binding.value 51 | break 52 | 53 | default: 54 | break 55 | } 56 | }, 57 | unmounted(el) { 58 | if (!elFull) return 59 | elFull.removeEventListener( 60 | 'click', 61 | () => { 62 | handleClick(el) 63 | }, 64 | false 65 | ) 66 | elFull = null 67 | }, 68 | } 69 | -------------------------------------------------------------------------------- /src/directive/fullscreen/index.js: -------------------------------------------------------------------------------- 1 | import fullscreen from './fullscreen' 2 | 3 | const install = function (Vue) { 4 | Vue.directive('fullscreen', fullscreen) 5 | } 6 | 7 | if (window.Vue) { 8 | window.fullscreen = fullscreen 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | fullscreen.install = install 13 | export default fullscreen 14 | -------------------------------------------------------------------------------- /src/directive/index.js: -------------------------------------------------------------------------------- 1 | import fullscreen from '@/directive/fullscreen' 2 | import clipboard from '@/directive/clipboard' 3 | import waves from '@/directive/waves' 4 | 5 | export { fullscreen, clipboard, waves } 6 | -------------------------------------------------------------------------------- /src/directive/waves/index.js: -------------------------------------------------------------------------------- 1 | import waves from './waves' 2 | 3 | const install = function (Vue) { 4 | Vue.directive('waves', waves) 5 | } 6 | 7 | if (window.Vue) { 8 | window.waves = waves 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | waves.install = install 13 | export default waves 14 | -------------------------------------------------------------------------------- /src/directive/waves/waves.css: -------------------------------------------------------------------------------- 1 | .waves-ripple { 2 | position: absolute; 3 | border-radius: 100%; 4 | background-color: rgba(0, 0, 0, 0.15); 5 | background-clip: padding-box; 6 | pointer-events: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | -webkit-transform: scale(0); 12 | -ms-transform: scale(0); 13 | transform: scale(0); 14 | opacity: 1; 15 | } 16 | 17 | .waves-ripple.z-active { 18 | opacity: 0; 19 | -webkit-transform: scale(2); 20 | -ms-transform: scale(2); 21 | transform: scale(2); 22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out; 25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out; 26 | } -------------------------------------------------------------------------------- /src/directive/waves/waves.js: -------------------------------------------------------------------------------- 1 | import './waves.css' 2 | 3 | const context = '@@wavesContext' 4 | 5 | function handleClick(el, binding) { 6 | function handle(e) { 7 | const customOpts = Object.assign({}, binding.value) 8 | const opts = Object.assign( 9 | { 10 | ele: el, // 波纹作用元素 11 | type: 'hit', // hit 点击位置扩散 center中心点扩展 12 | color: 'rgba(0, 0, 0, 0.15)', // 波纹颜色 13 | }, 14 | customOpts 15 | ) 16 | const target = opts.ele 17 | if (target) { 18 | target.style.position = 'relative' 19 | target.style.overflow = 'hidden' 20 | const rect = target.getBoundingClientRect() 21 | let ripple = target.querySelector('.waves-ripple') 22 | if (!ripple) { 23 | ripple = document.createElement('span') 24 | ripple.className = 'waves-ripple' 25 | ripple.style.height = ripple.style.width = 26 | Math.max(rect.width, rect.height) + 'px' 27 | target.appendChild(ripple) 28 | } else { 29 | ripple.className = 'waves-ripple' 30 | } 31 | switch (opts.type) { 32 | case 'center': 33 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px' 34 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px' 35 | break 36 | default: 37 | ripple.style.top = 38 | (e.pageY - 39 | rect.top - 40 | ripple.offsetHeight / 2 - 41 | document.documentElement.scrollTop || document.body.scrollTop) + 42 | 'px' 43 | ripple.style.left = 44 | (e.pageX - 45 | rect.left - 46 | ripple.offsetWidth / 2 - 47 | document.documentElement.scrollLeft || document.body.scrollLeft) + 48 | 'px' 49 | } 50 | ripple.style.backgroundColor = opts.color 51 | ripple.className = 'waves-ripple z-active' 52 | return false 53 | } 54 | } 55 | 56 | if (!el[context]) { 57 | el[context] = { 58 | removeHandle: handle, 59 | } 60 | } else { 61 | el[context].removeHandle = handle 62 | } 63 | 64 | return handle 65 | } 66 | 67 | export default { 68 | mounted(el, binding) { 69 | el.addEventListener('click', handleClick(el, binding), false) 70 | }, 71 | update(el, binding) { 72 | el.removeEventListener('click', el[context].removeHandle, false) 73 | el.addEventListener('click', handleClick(el, binding), false) 74 | }, 75 | unmounted(el) { 76 | el.removeEventListener('click', el[context].removeHandle, false) 77 | el[context] = null 78 | delete el[context] 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /src/hooks/dict.js: -------------------------------------------------------------------------------- 1 | const typeFilter = { 2 | CN: 'China', 3 | EU: 'Eurozone', 4 | JP: 'Japan', 5 | US: 'USA', 6 | } 7 | const statusMap = { 8 | published: 'success', 9 | draft: 'info', 10 | deleted: 'danger', 11 | } 12 | 13 | const dict = { 14 | typeFilter, 15 | statusMap, 16 | } 17 | 18 | export default function useDict(type, key) { 19 | if (type && key) { 20 | if (dict[type]) { 21 | return dict[type][key] 22 | } else { 23 | console.error('没有设置此字典 - ' + type) 24 | } 25 | } else { 26 | return '' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/echarts.js: -------------------------------------------------------------------------------- 1 | import echarts from '@/core/echarts' 2 | import { unref, nextTick, onMounted, onUnmounted } from 'vue' 3 | import { debounce } from '@/utils' 4 | import { useStore } from 'vuex' 5 | 6 | export function useEcharts(elRef) { 7 | let mychart 8 | let voptions 9 | 10 | const store = useStore() 11 | 12 | function init() { 13 | if (!elRef) return 14 | const el = unref(elRef) 15 | mychart = echarts.init(el) 16 | } 17 | 18 | function setOptions(options) { 19 | if (!mychart) return 20 | voptions = options 21 | mychart.setOption(options) 22 | } 23 | 24 | function resize(obj) { 25 | mychart && mychart.resize(obj) 26 | if (voptions.geo && voptions.geo.roam) { 27 | const center = store.state.charts.center 28 | if (center && center.length > 0) { 29 | voptions.geo.center = center 30 | } else { 31 | voptions.geo.center = [104.114129, 35.950339] 32 | } 33 | mychart.setOption(voptions) 34 | } 35 | } 36 | 37 | function getInstance() { 38 | return mychart 39 | } 40 | 41 | // sidebar resize 42 | let $_sidebarElm = null 43 | let $_resizeHandler = null 44 | 45 | function $_sidebarResizeHandler(e) { 46 | if (e.propertyName === 'width') { 47 | $_resizeHandler() 48 | } 49 | } 50 | function initListener() { 51 | $_resizeHandler = debounce(() => { 52 | resize() 53 | }, 100) 54 | window.addEventListener('resize', $_resizeHandler) 55 | 56 | $_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 57 | $_sidebarElm && 58 | $_sidebarElm.addEventListener('transitionend', $_sidebarResizeHandler) 59 | } 60 | function destroyListener() { 61 | window.removeEventListener('resize', $_resizeHandler) 62 | $_resizeHandler = null 63 | 64 | $_sidebarElm && 65 | $_sidebarElm.removeEventListener('transitionend', $_sidebarResizeHandler) 66 | } 67 | 68 | onMounted(() => { 69 | nextTick(() => { 70 | init() 71 | initListener() 72 | }) 73 | }) 74 | 75 | onUnmounted(() => { 76 | mychart.dispose() 77 | mychart = null 78 | destroyListener() 79 | }) 80 | 81 | return { 82 | setOptions, 83 | resize, 84 | echarts, 85 | getInstance, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/hooks/table.js: -------------------------------------------------------------------------------- 1 | const calendarTypeOptions = [ 2 | { key: 'CN', display_name: 'China' }, 3 | { key: 'US', display_name: 'USA' }, 4 | { key: 'JP', display_name: 'Japan' }, 5 | { key: 'EU', display_name: 'Eurozone' }, 6 | ] 7 | 8 | // arr to obj, such as { CN : "China", US : "USA" } 9 | const calendarTypeKeyValue = calendarTypeOptions.reduce((acc, cur) => { 10 | acc[cur.key] = cur.display_name 11 | return acc 12 | }, {}) 13 | 14 | export function useFilter() { 15 | function typeFilter(type) { 16 | return calendarTypeKeyValue[type] 17 | } 18 | function statusFilter(status) { 19 | const statusMap = { 20 | published: 'success', 21 | draft: 'info', 22 | deleted: 'danger', 23 | } 24 | return statusMap[status] 25 | } 26 | return { 27 | typeFilter, 28 | statusFilter, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | const req = require.context('./svg', false, /\.svg$/) 2 | const requireAll = (requireContext) => requireContext.keys().map(requireContext) 3 | requireAll(req) 4 | -------------------------------------------------------------------------------- /src/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/component.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/drag.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-exit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/international.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/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/money.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/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/skill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree-table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.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 | 8 | 9 | 25 | 26 | 44 | 45 | 53 | -------------------------------------------------------------------------------- /src/layout/components/Settings/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 67 | 68 | 93 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, computed } from 'vue' 2 | import { useStore } from 'vuex' 3 | 4 | export function useFixBug() { 5 | const store = useStore() 6 | const device = computed(() => store.state.app.device) 7 | 8 | const subMenu = ref(null) 9 | 10 | function fixBugIniOS() { 11 | if (subMenu.value) { 12 | const handleMouseleave = subMenu.value.handleMouseleave 13 | subMenu.value.handleMouseleave = (e) => { 14 | if (device.value === 'mobile') { 15 | return 16 | } 17 | handleMouseleave(e) 18 | } 19 | } 20 | } 21 | 22 | onMounted(() => { 23 | fixBugIniOS() 24 | }) 25 | 26 | return { 27 | subMenu, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 42 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 48 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 44 | 45 | 94 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 68 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar.vue' 2 | export { default as Sidebar } from './Sidebar/index.vue' 3 | export { default as AppMain } from './AppMain.vue' 4 | export { default as Settings } from './Settings/index.vue' 5 | export { default as TagsView } from './TagsView/index.vue' 6 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 73 | 74 | 115 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import { onMounted, computed, onBeforeMount, onBeforeUnmount, watch } from 'vue' 2 | import { useStore } from 'vuex' 3 | import { useRoute } from 'vue-router' 4 | 5 | const { body } = document 6 | const WIDTH = 992 // refer to Bootstrap's responsive design 7 | 8 | export function useResizeHandler() { 9 | const store = useStore() 10 | const route = useRoute() 11 | 12 | const sidebar = computed(() => store.state.app.sidebar) 13 | const device = computed(() => store.state.app.device) 14 | 15 | // use $_ for mixins properties 16 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 17 | function $_isMobile() { 18 | const rect = body.getBoundingClientRect() 19 | return rect.width - 1 < WIDTH 20 | } 21 | 22 | function $_resizeHandler() { 23 | if (!document.hidden) { 24 | const isMobile = $_isMobile() 25 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 26 | 27 | if (isMobile) { 28 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 29 | } 30 | } 31 | } 32 | 33 | onMounted(() => { 34 | const isMobile = $_isMobile() 35 | if (isMobile) { 36 | store.dispatch('app/toggleDevice', 'mobile') 37 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 38 | } 39 | }) 40 | 41 | onBeforeMount(() => { 42 | window.addEventListener('resize', $_resizeHandler) 43 | }) 44 | 45 | onBeforeUnmount(() => { 46 | window.removeEventListener('resize', $_resizeHandler) 47 | }) 48 | 49 | watch( 50 | () => route.path, 51 | () => { 52 | if (device.value === 'mobile' && sidebar.value.opened) { 53 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 54 | } 55 | } 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | import App from '@/App.vue' 4 | // style 5 | import 'tailwindcss/tailwind.css' // tailwind 6 | import 'normalize.css/normalize.css' // a modern alternative to CSS resets 7 | import '@/styles/index.scss' // global css 8 | 9 | // import bootstrap from './core/bootstrap' 10 | 11 | // use lazy load element-plus 12 | import loadElementPlus from '@/core/lazy_use' 13 | // use components 14 | import loadComponents from '@/core/use' 15 | // globalProperties 16 | import globalProperties from '@/core/globalProperties' 17 | // icon 18 | import(/* webpackChunkName: "icon" */ '@/icons') 19 | // vue router 20 | import router from '@/router' 21 | // vue vuex 22 | import store from '@/store' 23 | 24 | import '@/permission' // permission control 25 | 26 | import * as directive from '@/directive' // global directive 27 | 28 | /** 29 | * If you don't want to use mock-server 30 | * you want to use MockJs for mock api 31 | * you can execute: mockXHR() 32 | * 33 | * Currently MockJs will be used in the production environment, 34 | * please remove it before going online ! ! ! 35 | */ 36 | if (process.env.NODE_ENV === 'production') { 37 | const { mockXHR } = require('../mock') 38 | mockXHR() 39 | } 40 | 41 | const app = createApp(App) 42 | 43 | loadElementPlus(app) 44 | loadComponents(app) 45 | globalProperties(app) 46 | 47 | // register global directive 48 | Object.keys(directive).forEach((key) => { 49 | directive[key].install(app) 50 | }) 51 | 52 | // element-plus config 53 | app.config.globalProperties.$ELEMENT = { size: 'small', zIndex: 3000 } 54 | 55 | app.use(router).use(store).mount('#app') 56 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { ElMessage } from 'element-plus' 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'] // 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 | const hasGetUserInfo = store.getters.name 30 | if (hasGetUserInfo) { 31 | next() 32 | } else { 33 | try { 34 | // get user info 35 | await store.dispatch('user/getInfo') 36 | 37 | next() 38 | } catch (error) { 39 | // remove token and go to login page to re-login 40 | await store.dispatch('user/resetToken') 41 | ElMessage.error(error || 'Has Error') 42 | next(`/login?redirect=${to.path}`) 43 | NProgress.done() 44 | } 45 | } 46 | } 47 | } else { 48 | /* has no token*/ 49 | 50 | if (whiteList.indexOf(to.path) !== -1) { 51 | // in the free login whitelist, go directly 52 | next() 53 | } else { 54 | // other pages that do not have permission to access are redirected to the login page. 55 | next(`/login?redirect=${to.path}`) 56 | NProgress.done() 57 | } 58 | } 59 | }) 60 | 61 | router.afterEach(() => { 62 | // finish progress bar 63 | NProgress.done() 64 | }) 65 | -------------------------------------------------------------------------------- /src/router/modules/chart.js: -------------------------------------------------------------------------------- 1 | /** When your routing charts is too long, you can split it into small modules **/ 2 | 3 | import Layout from '@/layout/index.vue' 4 | 5 | const chartsRouter = { 6 | path: '/charts', 7 | component: Layout, 8 | redirect: '/charts/complex-charts', 9 | name: 'charts', 10 | meta: { 11 | title: 'charts', 12 | icon: 'chart', 13 | }, 14 | children: [ 15 | { 16 | path: 'line-charts', 17 | component: () => 18 | import(/* webpackChunkName: "charts" */ '@/views/charts/Line.vue'), 19 | name: 'LineCharts', 20 | meta: { title: 'line' }, 21 | }, 22 | { 23 | path: 'bar-charts', 24 | component: () => 25 | import(/* webpackChunkName: "charts" */ '@/views/charts/Bar.vue'), 26 | name: 'BarCharts', 27 | meta: { title: 'bar' }, 28 | }, 29 | { 30 | path: 'pie-charts', 31 | component: () => 32 | import(/* webpackChunkName: "charts" */ '@/views/charts/Pie.vue'), 33 | name: 'PieCharts', 34 | meta: { title: 'pie' }, 35 | }, 36 | { 37 | path: 'map-charts', 38 | component: () => 39 | import(/* webpackChunkName: "charts" */ '@/views/charts/Map.vue'), 40 | name: 'MapCharts', 41 | meta: { title: 'map' }, 42 | }, 43 | ], 44 | } 45 | export default chartsRouter 46 | -------------------------------------------------------------------------------- /src/router/modules/components-page.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules **/ 2 | 3 | import Layout from '@/layout/index.vue' 4 | 5 | const componentsRouter = { 6 | path: '/components', 7 | component: Layout, 8 | redirect: 'noRedirect', 9 | name: 'componentsPages', 10 | meta: { 11 | title: 'Components', 12 | icon: 'el-icon-files', 13 | }, 14 | children: [ 15 | { 16 | path: 'pro-dialog', 17 | component: () => 18 | import( 19 | /* webpackChunkName: "components" */ '@/views/components-page/pro-dialog-view.vue' 20 | ), 21 | name: 'pro-dialog', 22 | meta: { title: 'pro-dialog', noCache: true }, 23 | }, 24 | { 25 | path: 'tailwindcss', 26 | name: 'Tailwindcss', 27 | component: () => 28 | import( 29 | /* webpackChunkName: "components" */ '@/views/tailwindcss/index' 30 | ), 31 | meta: { title: 'Tailwindcss' }, 32 | }, 33 | ], 34 | } 35 | 36 | export default componentsRouter 37 | -------------------------------------------------------------------------------- /src/router/modules/error-page.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules **/ 2 | 3 | import Layout from '@/layout/index.vue' 4 | 5 | const errorRouter = { 6 | path: '/error', 7 | component: Layout, 8 | redirect: 'noRedirect', 9 | name: 'ErrorPages', 10 | meta: { 11 | title: 'Error Pages', 12 | icon: '404', 13 | }, 14 | children: [ 15 | { 16 | path: '401', 17 | component: () => 18 | import(/* webpackChunkName: "error" */ '@/views/error-page/401.vue'), 19 | name: 'Page401', 20 | meta: { title: '401', noCache: true }, 21 | }, 22 | { 23 | path: '404', 24 | component: () => 25 | import(/* webpackChunkName: "error" */ '@/views/error-page/404.vue'), 26 | name: 'Page404', 27 | meta: { title: '404', noCache: true }, 28 | }, 29 | ], 30 | } 31 | 32 | export default errorRouter 33 | -------------------------------------------------------------------------------- /src/router/modules/form.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules **/ 2 | 3 | import Layout from '@/layout/index.vue' 4 | 5 | const formRouter = { 6 | path: '/form', 7 | component: Layout, 8 | name: 'Form', 9 | redirect: '/form/index', 10 | meta: { 11 | title: 'Form', 12 | icon: 'form', 13 | }, 14 | children: [ 15 | { 16 | path: 'index', 17 | name: 'BasicForm', 18 | component: () => 19 | import(/* webpackChunkName: "form" */ '@/views/form/index.vue'), 20 | meta: { title: 'Basic Form' }, 21 | }, 22 | { 23 | path: 'advanced-form', 24 | name: 'AdvancedForm', 25 | component: () => 26 | import(/* webpackChunkName: "form" */ '@/views/form/advanced-form.vue'), 27 | meta: { title: 'Advanced Form' }, 28 | }, 29 | { 30 | path: 'modal-form', 31 | name: 'ModalForm', 32 | component: () => 33 | import(/* webpackChunkName: "form" */ '@/views/form/modal-form.vue'), 34 | meta: { title: 'Modal Form' }, 35 | }, 36 | { 37 | path: 'linkage-form', 38 | name: 'LinkageForm', 39 | component: () => 40 | import(/* webpackChunkName: "form" */ '@/views/form/linkage-form.vue'), 41 | meta: { title: 'Linkage Form' }, 42 | }, 43 | { 44 | path: 'step-form', 45 | name: 'StepForm', 46 | component: () => 47 | import(/* webpackChunkName: "form" */ '@/views/form/step-form.vue'), 48 | meta: { title: 'Step Form' }, 49 | }, 50 | ], 51 | } 52 | 53 | export default formRouter 54 | -------------------------------------------------------------------------------- /src/router/modules/nested.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules **/ 2 | 3 | import Layout from '@/layout/index.vue' 4 | 5 | const nestedRouter = { 6 | path: '/nested', 7 | component: Layout, 8 | redirect: '/nested/menu1/menu1-1', 9 | name: 'Nested', 10 | meta: { 11 | title: 'Nested Routes', 12 | icon: 'nested', 13 | }, 14 | children: [ 15 | { 16 | path: 'menu1', 17 | component: () => 18 | import( 19 | /* webpackChunkName: "nested" */ '@/views/nested/menu1/index.vue' 20 | ), // Parent router-view 21 | name: 'Menu1', 22 | meta: { title: 'Menu 1' }, 23 | redirect: '/nested/menu1/menu1-1', 24 | children: [ 25 | { 26 | path: 'menu1-1', 27 | component: () => 28 | import( 29 | /* webpackChunkName: "nested" */ '@/views/nested/menu1/menu1-1/index.vue' 30 | ), 31 | name: 'Menu1-1', 32 | meta: { title: 'Menu 1-1' }, 33 | }, 34 | { 35 | path: 'menu1-2', 36 | component: () => 37 | import( 38 | /* webpackChunkName: "nested" */ '@/views/nested/menu1/menu1-2/index.vue' 39 | ), 40 | name: 'Menu1-2', 41 | redirect: '/nested/menu1/menu1-2/menu1-2-1', 42 | meta: { title: 'Menu 1-2' }, 43 | children: [ 44 | { 45 | path: 'menu1-2-1', 46 | component: () => 47 | import( 48 | /* webpackChunkName: "nested" */ '@/views/nested/menu1/menu1-2/menu1-2-1/index.vue' 49 | ), 50 | name: 'Menu1-2-1', 51 | meta: { title: 'Menu 1-2-1' }, 52 | }, 53 | { 54 | path: 'menu1-2-2', 55 | component: () => 56 | import( 57 | /* webpackChunkName: "nested" */ '@/views/nested/menu1/menu1-2/menu1-2-2/index.vue' 58 | ), 59 | name: 'Menu1-2-2', 60 | meta: { title: 'Menu 1-2-2' }, 61 | }, 62 | ], 63 | }, 64 | { 65 | path: 'menu1-3', 66 | component: () => 67 | import( 68 | /* webpackChunkName: "nested" */ '@/views/nested/menu1/menu1-3/index.vue' 69 | ), 70 | name: 'Menu1-3', 71 | meta: { title: 'Menu 1-3' }, 72 | }, 73 | ], 74 | }, 75 | { 76 | path: 'menu2', 77 | name: 'Menu2', 78 | component: () => 79 | import( 80 | /* webpackChunkName: "nested" */ '@/views/nested/menu2/index.vue' 81 | ), 82 | meta: { title: 'Menu 2' }, 83 | }, 84 | ], 85 | } 86 | 87 | export default nestedRouter 88 | -------------------------------------------------------------------------------- /src/router/modules/table.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules **/ 2 | 3 | import Layout from '@/layout/index.vue' 4 | 5 | const tableRouter = { 6 | path: '/table', 7 | component: Layout, 8 | redirect: '/table/complex-table', 9 | name: 'Table', 10 | meta: { 11 | title: 'Table', 12 | icon: 'table', 13 | }, 14 | children: [ 15 | { 16 | path: 'inline-edit-table', 17 | component: () => 18 | import( 19 | /* webpackChunkName: "table" */ '@/views/table/inline-edit-table.vue' 20 | ), 21 | name: 'InlineEditTable', 22 | meta: { title: 'Inline Edit' }, 23 | }, 24 | { 25 | path: 'complex-table', 26 | component: () => 27 | import( 28 | /* webpackChunkName: "table" */ '@/views/table/complex-table.vue' 29 | ), 30 | name: 'ComplexTable', 31 | meta: { title: 'Complex Table' }, 32 | }, 33 | ], 34 | } 35 | export default tableRouter 36 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Vue Admin Pro', 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: false, 27 | } 28 | -------------------------------------------------------------------------------- /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 | name: (state) => state.user.name, 10 | introduction: (state) => state.user.introduction, 11 | roles: (state) => state.user.roles, 12 | permission_routes: (state) => state.permission.routes, 13 | errorLogs: (state) => state.errorLog.logs, 14 | } 15 | export default getters 16 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | 3 | import getters from './getters' 4 | // import app from './modules/app' 5 | // import settings from './modules/settings' 6 | // import user from './modules/user' 7 | 8 | // https://webpack.js.org/guides/dependency-management/#requirecontext 9 | const modulesFiles = require.context('./modules', true, /\.js$/) 10 | 11 | // you do not need `import app from './modules/app'` 12 | // it will auto require all vuex module from modules file 13 | const modules = modulesFiles.keys().reduce((modules, modulePath) => { 14 | // set './app.js' => 'app' 15 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 16 | const value = modulesFiles(modulePath) 17 | modules[moduleName] = value.default 18 | return modules 19 | }, {}) 20 | 21 | const store = createStore({ 22 | modules, 23 | getters, 24 | }) 25 | 26 | export default store 27 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Store from 'store' 2 | 3 | const state = () => { 4 | return { 5 | sidebar: { 6 | opened: Store.get('sidebarStatus') ? !!+Store.get('sidebarStatus') : true, 7 | withoutAnimation: false, 8 | }, 9 | device: 'desktop', 10 | } 11 | } 12 | 13 | const mutations = { 14 | TOGGLE_SIDEBAR: (state) => { 15 | state.sidebar.opened = !state.sidebar.opened 16 | state.sidebar.withoutAnimation = false 17 | if (state.sidebar.opened) { 18 | Store.set('sidebarStatus', 1) 19 | } else { 20 | Store.set('sidebarStatus', 0) 21 | } 22 | }, 23 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 24 | Store.set('sidebarStatus', 0) 25 | state.sidebar.opened = false 26 | state.sidebar.withoutAnimation = withoutAnimation 27 | }, 28 | TOGGLE_DEVICE: (state, device) => { 29 | state.device = device 30 | }, 31 | } 32 | 33 | const actions = { 34 | toggleSideBar({ commit }) { 35 | commit('TOGGLE_SIDEBAR') 36 | }, 37 | closeSideBar({ commit }, { withoutAnimation }) { 38 | commit('CLOSE_SIDEBAR', withoutAnimation) 39 | }, 40 | toggleDevice({ commit }, device) { 41 | commit('TOGGLE_DEVICE', device) 42 | }, 43 | } 44 | 45 | export default { 46 | namespaced: true, 47 | state, 48 | mutations, 49 | actions, 50 | } 51 | -------------------------------------------------------------------------------- /src/store/modules/charts.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | center: [], 3 | } 4 | 5 | const mutations = { 6 | ADD_CENTER: (state, center) => { 7 | state.center = center 8 | }, 9 | CLEAR_CENTER: (state) => { 10 | state.center = [] 11 | }, 12 | } 13 | 14 | const actions = { 15 | addCENTER({ commit }, center) { 16 | commit('ADD_CENTER', center) 17 | }, 18 | clearCENTER({ commit }) { 19 | commit('CLEAR_CENTER') 20 | }, 21 | } 22 | 23 | export default { 24 | namespaced: true, 25 | state, 26 | mutations, 27 | actions, 28 | } 29 | -------------------------------------------------------------------------------- /src/store/modules/errorLog.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | logs: [], 3 | } 4 | 5 | const mutations = { 6 | ADD_ERROR_LOG: (state, log) => { 7 | state.logs.push(log) 8 | }, 9 | CLEAR_ERROR_LOG: (state) => { 10 | state.logs.splice(0) 11 | }, 12 | } 13 | 14 | const actions = { 15 | addErrorLog({ commit }, log) { 16 | commit('ADD_ERROR_LOG', log) 17 | }, 18 | clearErrorLog({ commit }) { 19 | commit('CLEAR_ERROR_LOG') 20 | }, 21 | } 22 | 23 | export default { 24 | namespaced: true, 25 | state, 26 | mutations, 27 | actions, 28 | } 29 | -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { asyncRoutes, constantRoutes } from '@/router' 2 | 3 | /** 4 | * Use meta.role to determine if the current user has permission 5 | * @param roles 6 | * @param route 7 | */ 8 | function hasPermission(roles, route) { 9 | if (route.meta && route.meta.roles) { 10 | return roles.some((role) => route.meta.roles.includes(role)) 11 | } else { 12 | return true 13 | } 14 | } 15 | 16 | /** 17 | * Filter asynchronous routing tables by recursion 18 | * @param routes asyncRoutes 19 | * @param roles 20 | */ 21 | export function filterAsyncRoutes(routes, roles) { 22 | const res = [] 23 | 24 | routes.forEach((route) => { 25 | const tmp = { ...route } 26 | if (hasPermission(roles, tmp)) { 27 | if (tmp.children) { 28 | tmp.children = filterAsyncRoutes(tmp.children, roles) 29 | } 30 | res.push(tmp) 31 | } 32 | }) 33 | 34 | return res 35 | } 36 | 37 | const state = { 38 | routes: [], 39 | addRoutes: [], 40 | } 41 | 42 | const mutations = { 43 | SET_ROUTES: (state, routes) => { 44 | state.addRoutes = routes 45 | state.routes = constantRoutes.concat(routes) 46 | }, 47 | } 48 | 49 | const actions = { 50 | generateRoutes({ commit }, roles) { 51 | return new Promise((resolve) => { 52 | let accessedRoutes 53 | if (roles.includes('admin')) { 54 | accessedRoutes = asyncRoutes || [] 55 | } else { 56 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) 57 | } 58 | commit('SET_ROUTES', accessedRoutes) 59 | resolve(accessedRoutes) 60 | }) 61 | }, 62 | } 63 | 64 | export default { 65 | namespaced: true, 66 | state, 67 | mutations, 68 | actions, 69 | } 70 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings 4 | 5 | const state = { 6 | showSettings: showSettings, 7 | tagsView: tagsView, 8 | fixedHeader: fixedHeader, 9 | sidebarLogo: sidebarLogo, 10 | } 11 | 12 | const mutations = { 13 | CHANGE_SETTING: (state, { key, value }) => { 14 | // eslint-disable-next-line no-prototype-builtins 15 | if (state.hasOwnProperty(key)) { 16 | state[key] = value 17 | } 18 | }, 19 | } 20 | 21 | const actions = { 22 | changeSetting({ commit }, data) { 23 | commit('CHANGE_SETTING', data) 24 | }, 25 | } 26 | 27 | export default { 28 | namespaced: true, 29 | state, 30 | mutations, 31 | actions, 32 | } 33 | -------------------------------------------------------------------------------- /src/styles/editable-cell.scss: -------------------------------------------------------------------------------- 1 | .editable-cell { 2 | position: relative; 3 | 4 | .editable-cell-input-wrapper, 5 | .editable-cell-text-wrapper { 6 | padding-right: 24px; 7 | display : flex; 8 | align-items : center; 9 | } 10 | 11 | .editable-cell-icon, 12 | .editable-cell-icon-check { 13 | position : absolute; 14 | right : 0; 15 | font-size: 14px; 16 | cursor : pointer; 17 | } 18 | 19 | .editable-cell-icon { 20 | line-height: 36px; 21 | display : none; 22 | right : 10px; 23 | } 24 | 25 | .editable-cell-icon-check { 26 | line-height: 36px; 27 | } 28 | 29 | .editable-cell-icon-close { 30 | right : 20px; 31 | position : absolute; 32 | font-size : 14px; 33 | cursor : pointer; 34 | line-height: 36px; 35 | } 36 | 37 | &:hover .editable-cell-icon { 38 | display: inline-block; 39 | } 40 | 41 | .editable-cell-icon:hover, 42 | .editable-cell-icon-close:hover, 43 | .editable-cell-icon-check:hover { 44 | color: #108ee9; 45 | } 46 | .el-input--mini .el-input__inner { 47 | height: 24px; 48 | line-height: 24px; 49 | } 50 | .el-input--mini .el-input__icon { 51 | line-height: 26px; 52 | } 53 | } -------------------------------------------------------------------------------- /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 | 19 | // to fixed https://github.com/ElemeFE/element/issues/2461 20 | .el-dialog { 21 | transform: none; 22 | left: 0; 23 | position: relative; 24 | margin: 0 auto; 25 | } 26 | 27 | // refine element ui upload 28 | .upload-container { 29 | .el-upload { 30 | width: 100%; 31 | 32 | .el-upload-dragger { 33 | width: 100%; 34 | height: 200px; 35 | } 36 | } 37 | } 38 | 39 | // dropdown 40 | .el-dropdown-menu { 41 | a { 42 | display: block 43 | } 44 | } 45 | 46 | // to fix el-date-picker css style 47 | .el-range-separator { 48 | box-sizing: content-box; 49 | } 50 | 51 | .fixed-width { 52 | .el-button--mini { 53 | padding: 7px 10px; 54 | min-width: 60px; 55 | } 56 | } 57 | 58 | .el-pagination { 59 | text-align: center; 60 | } 61 | .el-table__body-wrapper { 62 | min-height: calc(100vh - 300px); 63 | } 64 | 65 | // pro-table sticky 66 | .pro-table.sticky { 67 | .el-table { 68 | overflow: inherit; 69 | } 70 | .el-table__header-wrapper { 71 | position: sticky; 72 | top: 0; 73 | z-index: 1; 74 | } 75 | } 76 | 77 | // search-form 78 | #search-form-wrap .search-form .el-form-item { 79 | width: 100%; 80 | display: flex; 81 | align-items: center; 82 | justify-content: flex-start; 83 | .el-form-item__content { 84 | flex-grow: 1; 85 | } 86 | } 87 | 88 | // CardChart 89 | .CardChart { 90 | .el-card__body { 91 | padding: 0; 92 | } 93 | } 94 | 95 | // ProDialog 96 | .ProDialog { 97 | 98 | border-radius: 4px; 99 | box-shadow: 0 4px 12px rgb(0 0 0 / 15%); 100 | margin-bottom: 0!important; 101 | 102 | .el-dialog__header { 103 | padding: 16px 24px; 104 | color: rgba(0, 0, 0, 0.65); 105 | background: #fff; 106 | border-bottom: 1px solid #e8e8e8; 107 | border-radius: 4px 4px 0 0; 108 | } 109 | .el-dialog__footer { 110 | padding: 10px 16px; 111 | border-top: 1px solid #e8e8e8; 112 | border-radius: 0 0 4px 4px; 113 | } 114 | .el-dialog__body { 115 | max-height: calc(100vh - 15vh - 110px); 116 | overflow-y: auto; 117 | } 118 | &.is-fullscreen { 119 | overflow: hidden; 120 | .el-dialog__body { 121 | height: calc(100vh - 112px); 122 | max-height: calc(100vh - 112px); 123 | overflow: auto; 124 | } 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | // theme 2 | /* theme color */ 3 | $--color-primary: #1890ff; 4 | 5 | /* icon font path, required */ 6 | $--font-path: '~element-plus/lib/theme-chalk/fonts'; 7 | 8 | @import "~element-plus/packages/theme-chalk/src/index"; 9 | 10 | 11 | // sidebar 12 | $menuText:#bfcbd9; 13 | $menuActiveText:#409EFF; 14 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 15 | 16 | $menuBg:#001529; 17 | $menuHover:#263445; 18 | 19 | $subMenuBg:#000c17; 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 Store from 'store' 2 | 3 | const TokenKey = 'Admin-Token' 4 | 5 | export function getToken() { 6 | return Store.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Store.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Store.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/clipboard.js: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus' 2 | import Clipboard from 'clipboard' 3 | 4 | function clipboardSuccess() { 5 | ElMessage({ 6 | message: 'Copy successfully', 7 | type: 'success', 8 | duration: 1500, 9 | }) 10 | } 11 | 12 | function clipboardError() { 13 | ElMessage({ 14 | message: 'Copy failed', 15 | type: 'error', 16 | }) 17 | } 18 | 19 | export default function handleClipboard(text, event) { 20 | const clipboard = new Clipboard(event.target, { 21 | text: () => text, 22 | }) 23 | clipboard.on('success', () => { 24 | clipboardSuccess() 25 | clipboard.destroy() 26 | }) 27 | clipboard.on('error', () => { 28 | clipboardError() 29 | clipboard.destroy() 30 | }) 31 | clipboard.onClick(event) 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Vue Element-plus Admin' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { ElMessageBox, ElMessage } from 'element-plus' 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['X-Token'] = getToken() 23 | } 24 | return config 25 | }, 26 | (error) => { 27 | // do something with request error 28 | console.log(error) // for debug 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | // response interceptor 34 | service.interceptors.response.use( 35 | /** 36 | * If you want to get http information such as headers or status 37 | * Please return response => response 38 | */ 39 | 40 | /** 41 | * Determine the request status by custom code 42 | * Here is just an example 43 | * You can also judge the status by HTTP Status Code 44 | */ 45 | (response) => { 46 | const res = response.data 47 | if (res.code === undefined) { 48 | return res 49 | } 50 | 51 | // if the custom code is not 20000, it is judged as an error. 52 | if (res.code !== 20000) { 53 | ElMessage({ 54 | message: res.message || 'Error', 55 | type: 'error', 56 | duration: 5 * 1000, 57 | }) 58 | 59 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 60 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 61 | // to re-login 62 | ElMessageBox.confirm( 63 | '您已登出,可以取消停留在此页面上,或者再次登录', 64 | '重新登录', 65 | { 66 | confirmButtonText: '重新登录', 67 | cancelButtonText: '离开', 68 | type: 'warning', 69 | } 70 | ).then(() => { 71 | store.dispatch('user/resetToken').then(() => { 72 | location.reload() 73 | }) 74 | }) 75 | } 76 | return Promise.reject(new Error(res.message || 'Error')) 77 | } else { 78 | return res 79 | } 80 | }, 81 | (error) => { 82 | console.log('err' + error) // for debug 83 | ElMessage({ 84 | message: error.message, 85 | type: 'error', 86 | duration: 5 * 1000, 87 | }) 88 | return Promise.reject(error) 89 | } 90 | ) 91 | 92 | export default service 93 | -------------------------------------------------------------------------------- /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 | let requestAnimFrame = (function () { 12 | return ( 13 | window.requestAnimationFrame || 14 | window.webkitRequestAnimationFrame || 15 | window.mozRequestAnimationFrame || 16 | function (callback) { 17 | window.setTimeout(callback, 1000 / 60) 18 | } 19 | ) 20 | })() 21 | 22 | /** 23 | * Because it's so fucking difficult to detect the scrolling element, just move them all 24 | * @param {number} amount 25 | */ 26 | function move(amount) { 27 | document.documentElement.scrollTop = amount 28 | document.body.parentNode.scrollTop = amount 29 | document.body.scrollTop = amount 30 | } 31 | 32 | function position() { 33 | return ( 34 | document.documentElement.scrollTop || 35 | document.body.parentNode.scrollTop || 36 | document.body.scrollTop 37 | ) 38 | } 39 | 40 | /** 41 | * @param {number} to 42 | * @param {number} duration 43 | * @param {Function} callback 44 | */ 45 | export function scrollTo(to, duration, callback) { 46 | const start = position() 47 | const change = to - start 48 | const increment = 20 49 | let currentTime = 0 50 | duration = typeof duration === 'undefined' ? 500 : duration 51 | let animateScroll = function () { 52 | // increment the time 53 | currentTime += increment 54 | // find the value with the quadratic in-out easing function 55 | let val = Math.easeInOutQuad(currentTime, start, change, duration) 56 | // move the document.body 57 | move(val) 58 | // do the animation unless its over 59 | if (currentTime < duration) { 60 | requestAnimFrame(animateScroll) 61 | } else { 62 | if (callback && typeof callback === 'function') { 63 | // the animation is done so lets callback 64 | callback() 65 | } 66 | } 67 | } 68 | animateScroll() 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by NineSwords on 16/11/18. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | 13 | /** 14 | * @param {string} str 15 | * @returns {Boolean} 16 | */ 17 | export function validUsername(str) { 18 | const validMap = ['admin', 'editor'] 19 | return validMap.indexOf(str.trim()) >= 0 20 | } 21 | 22 | /** 23 | * @param {string} url 24 | * @returns {Boolean} 25 | */ 26 | export function validURL(url) { 27 | 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.,?'\\+&%$#=~_-]+))*$/ 28 | return reg.test(url) 29 | } 30 | 31 | /** 32 | * @param {string} str 33 | * @returns {Boolean} 34 | */ 35 | export function validLowerCase(str) { 36 | const reg = /^[a-z]+$/ 37 | return reg.test(str) 38 | } 39 | 40 | /** 41 | * @param {string} str 42 | * @returns {Boolean} 43 | */ 44 | export function validUpperCase(str) { 45 | const reg = /^[A-Z]+$/ 46 | return reg.test(str) 47 | } 48 | 49 | /** 50 | * @param {string} str 51 | * @returns {Boolean} 52 | */ 53 | export function validAlphabets(str) { 54 | const reg = /^[A-Za-z]+$/ 55 | return reg.test(str) 56 | } 57 | 58 | /** 59 | * @param {string} email 60 | * @returns {Boolean} 61 | */ 62 | export function validEmail(email) { 63 | 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,}))$/ 64 | return reg.test(email) 65 | } 66 | 67 | /** 68 | * @param {string} str 69 | * @returns {Boolean} 70 | */ 71 | export function isString(str) { 72 | return typeof str === 'string' || str instanceof String 73 | } 74 | 75 | /** 76 | * @param {Array} arg 77 | * @returns {Boolean} 78 | */ 79 | export function isArray(arg) { 80 | if (typeof Array.isArray === 'undefined') { 81 | return Object.prototype.toString.call(arg) === '[object Array]' 82 | } 83 | return Array.isArray(arg) 84 | } 85 | -------------------------------------------------------------------------------- /src/views/charts/Bar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 87 | -------------------------------------------------------------------------------- /src/views/charts/Line.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 73 | -------------------------------------------------------------------------------- /src/views/charts/Pie.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 84 | -------------------------------------------------------------------------------- /src/views/charts/components/CardChart.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 69 | -------------------------------------------------------------------------------- /src/views/charts/data.js: -------------------------------------------------------------------------------- 1 | const dict = { 2 | 北京: 'BeiJing', 3 | 天津: 'TianJin', 4 | 河北: 'HeBei', 5 | 山西: 'ShanXi', 6 | 内蒙古: 'NeiMengGu', 7 | 辽宁: 'LiaoNing', 8 | 吉林: 'JiLin', 9 | 黑龙江: 'HeiLongJiang', 10 | 上海: 'ShangHai', 11 | 江苏: 'JiangSu', 12 | 浙江: 'ZheJiang', 13 | 安徽: 'AnHui', 14 | 福建: 'FuJian', 15 | 江西: 'JiangXi', 16 | 山东: 'ShanDong', 17 | 河南: 'HeNan', 18 | 湖北: 'HuBei', 19 | 湖南: 'HuNan', 20 | 广东: 'GuangDong', 21 | 广西: 'GuangXi', 22 | 海南: 'HaiNan', 23 | 重庆: 'ZhongQing', 24 | 四川: 'SiChuan', 25 | 贵州: 'GuiZhou', 26 | 云南: 'YunNan', 27 | 西藏: 'Xizang', 28 | 陕西: 'ShanXi', 29 | 甘肃: 'GanSu', 30 | 青海: 'QingHai', 31 | 宁夏: 'NingXia', 32 | 新疆: 'XinJiang', 33 | 台湾: 'TaiWan', 34 | 香港: 'xianggang', 35 | 澳门: 'aomen', 36 | } 37 | 38 | export { dict } 39 | -------------------------------------------------------------------------------- /src/views/components-page/pro-dialog-view.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 74 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /src/views/error-page/401.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 79 | 80 | 119 | -------------------------------------------------------------------------------- /src/views/form/advanced-form.vue: -------------------------------------------------------------------------------- 1 | 2 | 32 | 33 | 95 | -------------------------------------------------------------------------------- /src/views/form/columns/linkage.js: -------------------------------------------------------------------------------- 1 | const list = [ 2 | { 3 | dataIndex: 'name', 4 | title: 'Activity name', 5 | isForm: true, 6 | valueType: 'input', 7 | prop: [ 8 | { 9 | required: true, 10 | message: 'Please enter the Activity name', 11 | trigger: 'blur', 12 | }, 13 | ], 14 | }, 15 | { 16 | dataIndex: 'region', 17 | title: 'Activity zone', 18 | isForm: true, 19 | valueType: 'select', 20 | option: [ 21 | { 22 | label: 'Zone one', 23 | value: 'shanghai', 24 | }, 25 | { 26 | label: 'Zone two', 27 | value: 'beijing', 28 | }, 29 | ], 30 | prop: [ 31 | { 32 | required: true, 33 | message: 'Please select the Activity zone', 34 | trigger: 'change', 35 | }, 36 | ], 37 | }, 38 | { 39 | dataIndex: 'date', 40 | title: 'Activity time', 41 | valueType: 'date-picker', 42 | attrs: { 43 | type: 'daterange', 44 | format: 'YYYY-MM-DD', 45 | 'start-placeholder': 'Please select Activity time', 46 | }, 47 | isForm: true, 48 | }, 49 | { 50 | dataIndex: 'delivery', 51 | title: 'Instant delivery', 52 | valueType: 'switch', 53 | isForm: true, 54 | }, 55 | { 56 | dataIndex: 'type', 57 | title: 'Activity type', 58 | valueType: 'checkbox', 59 | option: [ 60 | { 61 | label: 'Online activities', 62 | value: 'Online activities', 63 | }, 64 | { 65 | label: 'Promotion activities', 66 | value: 'Promotion activities', 67 | }, 68 | { 69 | label: 'Offline activities', 70 | value: 'Offline activities', 71 | }, 72 | { 73 | label: 'Simple brand exposure', 74 | value: 'Simple brand exposure', 75 | }, 76 | ], 77 | isForm: true, 78 | isShowFormItem: (formParam) => { 79 | return formParam && formParam.delivery 80 | }, 81 | }, 82 | { 83 | dataIndex: 'resource', 84 | title: 'Resources', 85 | valueType: 'radio', 86 | option: [ 87 | { 88 | label: 'Sponsor', 89 | value: 'Sponsor', 90 | }, 91 | { 92 | label: 'Venue', 93 | value: 'Venue', 94 | }, 95 | ], 96 | isForm: true, 97 | isShowFormItem: (formParam) => { 98 | return formParam && formParam.delivery 99 | }, 100 | }, 101 | { 102 | dataIndex: 'desc', 103 | title: 'Activity form', 104 | valueType: 'input', 105 | inpuType: 'textarea', 106 | isForm: true, 107 | isShowFormItem: (formParam) => { 108 | return formParam && formParam.delivery 109 | }, 110 | }, 111 | { 112 | dataIndex: 'check-code', 113 | title: 'Verification Code', 114 | isForm: true, 115 | valueType: 'check-code', 116 | sendCode: { 117 | smsType: 'pay', 118 | code: 'pay', 119 | }, 120 | prop: [ 121 | { 122 | required: true, 123 | message: 'Please enter the verification code', 124 | trigger: 'blur', 125 | }, 126 | ], 127 | isShowFormItem: (formParam) => { 128 | return formParam && formParam.delivery 129 | }, 130 | }, 131 | ] 132 | 133 | export { list } 134 | -------------------------------------------------------------------------------- /src/views/form/columns/list.js: -------------------------------------------------------------------------------- 1 | const list = [ 2 | { 3 | dataIndex: 'name', 4 | title: 'Activity name', 5 | isForm: true, 6 | valueType: 'input', 7 | prop: [ 8 | { 9 | required: true, 10 | message: 'Please enter the Activity name', 11 | trigger: 'blur', 12 | }, 13 | ], 14 | }, 15 | { 16 | dataIndex: 'region', 17 | title: 'Activity zone', 18 | isForm: true, 19 | valueType: 'select', 20 | option: [ 21 | { 22 | label: 'Zone one', 23 | value: 'shanghai', 24 | }, 25 | { 26 | label: 'Zone two', 27 | value: 'beijing', 28 | }, 29 | ], 30 | prop: [ 31 | { 32 | required: true, 33 | message: 'Please select the Activity zone', 34 | trigger: 'change', 35 | }, 36 | ], 37 | }, 38 | { 39 | dataIndex: 'date', 40 | title: 'Activity time', 41 | valueType: 'date-picker', 42 | attrs: { 43 | type: 'daterange', 44 | format: 'YYYY-MM-DD', 45 | 'start-placeholder': 'Please select Activity time', 46 | }, 47 | isForm: true, 48 | }, 49 | { 50 | dataIndex: 'delivery', 51 | title: 'Instant delivery', 52 | valueType: 'switch', 53 | isForm: true, 54 | }, 55 | { 56 | dataIndex: 'type', 57 | title: 'Activity type', 58 | valueType: 'checkbox', 59 | option: [ 60 | { 61 | label: 'Online activities', 62 | value: 'Online activities', 63 | }, 64 | { 65 | label: 'Promotion activities', 66 | value: 'Promotion activities', 67 | }, 68 | { 69 | label: 'Offline activities', 70 | value: 'Offline activities', 71 | }, 72 | { 73 | label: 'Simple brand exposure', 74 | value: 'Simple brand exposure', 75 | }, 76 | ], 77 | isForm: true, 78 | }, 79 | { 80 | dataIndex: 'resource', 81 | title: 'Resources', 82 | valueType: 'radio', 83 | option: [ 84 | { 85 | label: 'Sponsor', 86 | value: 'Sponsor', 87 | }, 88 | { 89 | label: 'Venue', 90 | value: 'Venue', 91 | }, 92 | ], 93 | isForm: true, 94 | }, 95 | { 96 | dataIndex: 'desc', 97 | title: 'Activity form', 98 | valueType: 'input', 99 | inpuType: 'textarea', 100 | isForm: true, 101 | }, 102 | { 103 | dataIndex: 'check-code', 104 | title: 'Verification Code', 105 | isForm: true, 106 | valueType: 'check-code', 107 | sendCode: { 108 | smsType: 'pay', 109 | code: 'pay', 110 | }, 111 | prop: [ 112 | { 113 | required: true, 114 | message: 'Please enter the verification code', 115 | trigger: 'blur', 116 | }, 117 | ], 118 | }, 119 | ] 120 | 121 | export { list } 122 | -------------------------------------------------------------------------------- /src/views/form/columns/modal.js: -------------------------------------------------------------------------------- 1 | const list = [ 2 | { 3 | dataIndex: 'name', 4 | title: 'Activity name', 5 | isForm: true, 6 | valueType: 'input', 7 | prop: [ 8 | { 9 | required: true, 10 | message: 'Please enter the Activity name', 11 | trigger: 'blur', 12 | }, 13 | ], 14 | }, 15 | { 16 | dataIndex: 'region', 17 | title: 'Activity zone', 18 | isForm: true, 19 | valueType: 'select', 20 | option: [ 21 | { 22 | label: 'Zone one', 23 | value: 'shanghai', 24 | }, 25 | { 26 | label: 'Zone two', 27 | value: 'beijing', 28 | }, 29 | ], 30 | prop: [ 31 | { 32 | required: true, 33 | message: 'Please select the Activity zone', 34 | trigger: 'change', 35 | }, 36 | ], 37 | }, 38 | { 39 | dataIndex: 'date', 40 | title: 'Activity time', 41 | valueType: 'date-picker', 42 | attrs: { 43 | type: 'daterange', 44 | format: 'YYYY-MM-DD', 45 | 'start-placeholder': 'Please select Activity time', 46 | }, 47 | isForm: true, 48 | }, 49 | { 50 | dataIndex: 'delivery', 51 | title: 'Instant delivery', 52 | valueType: 'switch', 53 | isForm: true, 54 | }, 55 | { 56 | dataIndex: 'type', 57 | title: 'Activity type', 58 | valueType: 'checkbox', 59 | option: [ 60 | { 61 | label: 'Online activities', 62 | value: 'Online activities', 63 | }, 64 | { 65 | label: 'Promotion activities', 66 | value: 'Promotion activities', 67 | }, 68 | { 69 | label: 'Offline activities', 70 | value: 'Offline activities', 71 | }, 72 | { 73 | label: 'Simple brand exposure', 74 | value: 'Simple brand exposure', 75 | }, 76 | ], 77 | isForm: true, 78 | }, 79 | { 80 | dataIndex: 'resource', 81 | title: 'Resources', 82 | valueType: 'radio', 83 | option: [ 84 | { 85 | label: 'Sponsor', 86 | value: 'Sponsor', 87 | }, 88 | { 89 | label: 'Venue', 90 | value: 'Venue', 91 | }, 92 | ], 93 | isForm: true, 94 | }, 95 | { 96 | dataIndex: 'desc', 97 | title: 'Activity form', 98 | valueType: 'input', 99 | inpuType: 'textarea', 100 | isForm: true, 101 | }, 102 | { 103 | dataIndex: 'check-code', 104 | title: 'Verification Code', 105 | isForm: true, 106 | valueType: 'check-code', 107 | sendCode: { 108 | smsType: 'pay', 109 | code: 'pay', 110 | }, 111 | prop: [ 112 | { 113 | required: true, 114 | message: 'Please enter the verification code', 115 | trigger: 'blur', 116 | }, 117 | ], 118 | }, 119 | ] 120 | 121 | export { list } 122 | -------------------------------------------------------------------------------- /src/views/form/columns/step-list.js: -------------------------------------------------------------------------------- 1 | const list = [ 2 | { 3 | dataIndex: 'payAccount', 4 | title: '付款账户', 5 | isForm: true, 6 | valueType: 'select', 7 | option: [ 8 | { 9 | label: 'ant-design@alipay.com', 10 | value: 'ant-design@alipay.com', 11 | }, 12 | ], 13 | prop: [ 14 | { 15 | required: true, 16 | message: '请选择付款账户', 17 | trigger: 'change', 18 | }, 19 | ], 20 | step: 0, 21 | }, 22 | { 23 | dataIndex: 'receiver', 24 | title: '收款账户', 25 | isForm: true, 26 | form_slot: 'receiver', 27 | step: 0, 28 | }, 29 | { 30 | dataIndex: 'receiverName', 31 | title: '收款人姓名', 32 | isForm: true, 33 | valueType: 'input', 34 | prop: [ 35 | { 36 | required: true, 37 | message: '请输入收款人姓名', 38 | trigger: 'blur', 39 | }, 40 | ], 41 | step: 0, 42 | }, 43 | { 44 | dataIndex: 'amount', 45 | title: '转账金额', 46 | isForm: true, 47 | valueType: 'input-number', 48 | attrs: { 49 | min: 0, 50 | precision: 2, 51 | step: 0.1, 52 | max: 100, 53 | }, 54 | prop: [ 55 | { 56 | required: true, 57 | validator: (rule, value, callback) => { 58 | if (!value) { 59 | callback(new Error('请输入转账金额!')) 60 | } else { 61 | callback() 62 | } 63 | }, 64 | trigger: 'change', 65 | }, 66 | ], 67 | step: 0, 68 | }, 69 | { 70 | dataIndex: 'desinfo', 71 | isForm: true, 72 | form_slot: 'desinfo', 73 | step: 1, 74 | }, 75 | { 76 | dataIndex: 'password', 77 | title: '支付密码', 78 | isForm: true, 79 | valueType: 'input', 80 | prop: [ 81 | { 82 | required: true, 83 | message: '需要支付密码才能进行支付', 84 | trigger: 'blur', 85 | }, 86 | ], 87 | attrs: { 88 | 'show-password': true, 89 | }, 90 | step: 1, 91 | }, 92 | { 93 | dataIndex: 'result', 94 | isForm: true, 95 | form_slot: 'result', 96 | step: 2, 97 | form_span: 24, 98 | }, 99 | ] 100 | 101 | export { list } 102 | -------------------------------------------------------------------------------- /src/views/form/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 76 | -------------------------------------------------------------------------------- /src/views/form/linkage-form.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 76 | -------------------------------------------------------------------------------- /src/views/form/modal-form.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 95 | -------------------------------------------------------------------------------- /src/views/icons/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 78 | 79 | 111 | -------------------------------------------------------------------------------- /src/views/icons/svg-icons.js: -------------------------------------------------------------------------------- 1 | const req = require.context('../../icons/svg', false, /\.svg$/) 2 | const requireAll = (requireContext) => requireContext.keys() 3 | 4 | const re = /\.\/(.*)\.svg/ 5 | 6 | const svgIcons = requireAll(req).map((i) => { 7 | return i.match(re)[1] 8 | }) 9 | 10 | export default svgIcons 11 | -------------------------------------------------------------------------------- /src/views/nested/menu1/index.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-1/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/index.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/menu1-2-1/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/menu1-2-2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-3/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/nested/menu2/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/table/columns/inline.js: -------------------------------------------------------------------------------- 1 | const columnList = [ 2 | { 3 | dataIndex: 'id', 4 | width: 80, 5 | title: 'ID', 6 | }, 7 | { 8 | dataIndex: 'timestamp', 9 | width: 180, 10 | title: 'Date', 11 | scopedSlots: { customRender: 'date' }, 12 | valueType: 'date-picker', 13 | attrs: { 14 | type: 'date', 15 | format: 'YYYY-MM-DD HH-mm-ss', 16 | 'start-placeholder': 'Please select Date', 17 | }, 18 | }, 19 | { 20 | dataIndex: 'author', 21 | width: 120, 22 | title: 'Author', 23 | }, 24 | { 25 | dataIndex: 'importance', 26 | width: 200, 27 | title: 'Imp', 28 | scopedSlots: { customRender: 'importance' }, 29 | }, 30 | { 31 | dataIndex: 'status', 32 | width: 180, 33 | title: 'Status', 34 | scopedSlots: { customRender: 'status' }, 35 | }, 36 | { 37 | dataIndex: 'title', 38 | width: null, 39 | minWidth: 450, 40 | align: 'left', 41 | title: 'Title', 42 | }, 43 | ] 44 | 45 | export { columnList } 46 | -------------------------------------------------------------------------------- /src/views/table/components/editable-cell/Important.vue: -------------------------------------------------------------------------------- 1 | 2 | 26 | 27 | 80 | -------------------------------------------------------------------------------- /src/views/table/components/editable-cell/Status.vue: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 106 | -------------------------------------------------------------------------------- /src/views/table/inline-edit-table.vue: -------------------------------------------------------------------------------- 1 | 13 | 52 | -------------------------------------------------------------------------------- /src/views/tree/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 102 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | // tailwind.config.js 2 | const colors = require('tailwindcss/colors') 3 | 4 | module.exports = { 5 | purge: ['./src/**/*.vue'], 6 | darkMode: false, // or 'media' or 'class' 7 | theme: { 8 | extend: { 9 | colors: { 10 | sky: colors.sky, 11 | cyan: colors.cyan, 12 | }, 13 | }, 14 | }, 15 | corePlugins: { 16 | preflight: false, 17 | }, 18 | variants: {}, 19 | plugins: [], 20 | } 21 | --------------------------------------------------------------------------------