├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README-zh.md ├── README.md ├── babel.config.js ├── build └── index.js ├── jest.config.js ├── jsconfig.json ├── mock ├── article.js ├── index.js ├── mock-server.js ├── remote-search.js ├── role │ ├── index.js │ └── routes.js ├── table.js ├── user.js └── utils.js ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ ├── article.js │ ├── qiniu.js │ ├── remote-search.js │ ├── role.js │ ├── table.js │ └── user.js ├── assets │ ├── 401_images │ │ └── 401.gif │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ └── custom-theme │ │ ├── fonts │ │ ├── element-icons.ttf │ │ └── element-icons.woff │ │ └── index.css ├── components │ ├── BackToTop │ │ └── index.vue │ ├── Breadcrumb │ │ └── index.vue │ ├── Charts │ │ ├── Keyboard.vue │ │ ├── LineMarker.vue │ │ ├── MixChart.vue │ │ └── mixins │ │ │ └── resize.js │ ├── DndList │ │ └── index.vue │ ├── DragSelect │ │ └── index.vue │ ├── ErrorLog │ │ └── index.vue │ ├── GithubCorner │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── HeaderSearch │ │ └── index.vue │ ├── JsonEditor │ │ └── index.vue │ ├── Kanban │ │ └── index.vue │ ├── MDinput │ │ └── index.vue │ ├── MarkdownEditor │ │ ├── default-options.js │ │ └── index.vue │ ├── NavLink │ │ └── index.vue │ ├── Pagination │ │ └── index.vue │ ├── PanThumb │ │ └── index.vue │ ├── RightPanel │ │ └── index.vue │ ├── Screenfull │ │ └── index.vue │ ├── Share │ │ └── DropdownMenu.vue │ ├── SizeSelect │ │ └── index.vue │ ├── Sticky │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ ├── TextHoverEffect │ │ └── Mallki.vue │ ├── ThemePicker │ │ └── index.vue │ ├── Tinymce │ │ ├── components │ │ │ └── EditorImage.vue │ │ ├── dynamicLoadScript.js │ │ ├── index.vue │ │ ├── plugins.js │ │ └── toolbar.js │ └── UploadExcel │ │ └── index.vue ├── directive │ ├── clipboard │ │ ├── clipboard.js │ │ └── index.js │ ├── el-drag-dialog │ │ ├── drag.js │ │ └── index.js │ ├── permission │ │ ├── index.js │ │ └── permission.js │ └── waves │ │ ├── index.js │ │ ├── waves.css │ │ └── waves.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.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 │ │ │ ├── ScrollPane.vue │ │ │ └── index.vue │ │ └── index.js │ ├── index.vue │ └── mixin │ │ └── ResizeHandler.js ├── main.js ├── permission.js ├── router │ ├── index.js │ └── modules │ │ ├── charts.js │ │ ├── components.js │ │ ├── nested.js │ │ └── table.js ├── settings.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── errorLog.js │ │ ├── permission.js │ │ ├── settings.js │ │ ├── tagsView.js │ │ └── user.js ├── styles │ ├── btn.scss │ ├── element-ui.scss │ ├── element-variables.scss │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── auth.js │ ├── clipboard.js │ ├── get-page-title.js │ ├── index.js │ ├── permission.js │ ├── request.js │ ├── scroll-to.js │ └── validate.js ├── vendor │ ├── Export2Excel.js │ └── Export2Zip.js └── views │ ├── 404.vue │ ├── charts │ ├── keyboard.vue │ ├── line.vue │ └── mix-chart.vue │ ├── clipboard │ └── index.vue │ ├── components-demo │ ├── back-to-top.vue │ ├── count-to.vue │ ├── dnd-list.vue │ ├── drag-dialog.vue │ ├── drag-kanban.vue │ ├── drag-select.vue │ ├── json-editor.vue │ ├── markdown.vue │ ├── mixin.vue │ ├── sticky.vue │ └── tinymce.vue │ ├── dashboard │ ├── admin │ │ ├── components │ │ │ ├── BarChart.vue │ │ │ ├── BoxCard.vue │ │ │ ├── LineChart.vue │ │ │ ├── PanelGroup.vue │ │ │ ├── PieChart.vue │ │ │ ├── RaddarChart.vue │ │ │ ├── TodoList │ │ │ │ ├── Todo.vue │ │ │ │ ├── index.scss │ │ │ │ └── index.vue │ │ │ ├── TransactionTable.vue │ │ │ └── mixins │ │ │ │ └── resize.js │ │ └── index.vue │ ├── editor │ │ └── index.vue │ └── index.vue │ ├── error-log │ ├── components │ │ ├── ErrorTestA.vue │ │ └── ErrorTestB.vue │ └── index.vue │ ├── error-page │ ├── 401.vue │ └── 404.vue │ ├── example │ ├── components │ │ ├── ArticleDetail.vue │ │ ├── Dropdown │ │ │ ├── Comment.vue │ │ │ ├── Platform.vue │ │ │ ├── SourceUrl.vue │ │ │ └── index.js │ │ └── Warning.vue │ ├── create.vue │ ├── edit.vue │ └── list.vue │ ├── excel │ ├── components │ │ ├── AutoWidthOption.vue │ │ ├── BookTypeOption.vue │ │ └── FilenameOption.vue │ ├── export-excel.vue │ ├── merge-header.vue │ ├── select-excel.vue │ └── upload-excel.vue │ ├── guide │ ├── index.vue │ └── steps.js │ ├── 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 │ ├── pdf │ ├── content.js │ ├── download.vue │ └── index.vue │ ├── permission │ ├── components │ │ └── SwitchRoles.vue │ ├── directive.vue │ ├── page.vue │ └── role.vue │ ├── profile │ ├── components │ │ ├── Account.vue │ │ ├── Activity.vue │ │ ├── Timeline.vue │ │ └── UserCard.vue │ └── index.vue │ ├── redirect │ └── index.vue │ ├── tab │ ├── components │ │ └── TabPane.vue │ └── index.vue │ ├── table │ ├── complex-table.vue │ ├── drag-table.vue │ ├── dynamic-table │ │ ├── components │ │ │ ├── FixedThead.vue │ │ │ └── UnfixedThead.vue │ │ └── index.vue │ ├── index.vue │ └── inline-edit-table.vue │ ├── theme │ └── index.vue │ └── zip │ └── index.vue ├── tests └── unit │ ├── .eslintrc.js │ ├── components │ ├── Breadcrumb.spec.js │ ├── Hamburger.spec.js │ └── SvgIcon.spec.js │ └── utils │ ├── formatTime.spec.js │ ├── param2Obj.spec.js │ ├── parseTime.spec.js │ └── validate.spec.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 | -------------------------------------------------------------------------------- /.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 | pnpm-lock.yaml 9 | yarn.lock 10 | tests/**/coverage/ 11 | 12 | # Editor directories and files 13 | .idea 14 | .vscode 15 | *.suo 16 | *.ntvs* 17 | *.njsproj 18 | *.sln 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present PanJiaChen 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 | # element3-admin 2 | 3 | > 这是一个极简的 element3 admin 管理后台模板。它只包含了 Element3 UI 4 | 5 | 目前版本为 `v4.0+` 基于 `vue-cli` 进行构建 6 | 7 | 8 | ## 相关项目 9 | 10 | - [element3-admin-template](https://github.com/hug-sun/element3-admin-template) 11 | 12 | - [element3-admin-ts-template](https://github.com/hug-sun/element3-admin-ts-template) 13 | 14 | - [element3-admin](https://github.com/hug-sun/element3-admin) 15 | 16 | ## Build Setup 17 | 18 | ```bash 19 | # 克隆项目 20 | git clone https://github.com/hug-sun/element3-admin-template.git 21 | 22 | # 进入项目目录 23 | cd element3-admin-template 24 | 25 | # 安装依赖 26 | npm install 27 | 28 | # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 29 | npm install --registry=https://registry.npm.taobao.org 30 | 31 | # 启动服务 32 | npm run dev 33 | ``` 34 | 35 | 浏览器访问 [http://localhost:9528](http://localhost:9528) 36 | 37 | ## 发布 38 | 39 | ```bash 40 | # 构建测试环境 41 | npm run build:stage 42 | 43 | # 构建生产环境 44 | npm run build:prod 45 | ``` 46 | 47 | ## 其它 48 | 49 | ```bash 50 | # 预览发布环境效果 51 | npm run preview 52 | 53 | # 预览发布环境效果 + 静态资源分析 54 | npm run preview -- --report 55 | 56 | # 代码格式检查 57 | npm run lint 58 | 59 | # 代码格式检查并自动修复 60 | npm run lint -- --fix 61 | ``` 62 | 63 | ## Browsers support 64 | 65 | Modern browsers and Internet Explorer 10+. 66 | 67 | | [](http://godban.github.io/browsers-support-badges/)IE / Edge | [](http://godban.github.io/browsers-support-badges/)Firefox | [](http://godban.github.io/browsers-support-badges/)Chrome | [](http://godban.github.io/browsers-support-badges/)Safari | 68 | | --------- | --------- | --------- | --------- | 69 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions 70 | 71 | 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # element3-admin 2 | 3 | English | [简体中文](./README-zh.md) 4 | 5 | > A minimal vue admin template with Element3 UI 6 | 7 | 8 | ## Build Setup 9 | 10 | ```bash 11 | # clone the project 12 | git clone https://github.com/hug-sun/element3-admin-template.git 13 | 14 | # enter the project directory 15 | cd element3-admin-template 16 | 17 | # install dependency 18 | npm install 19 | 20 | # develop 21 | npm run dev 22 | ``` 23 | 24 | This will automatically open http://localhost:9528 25 | 26 | ## Build 27 | 28 | ```bash 29 | # build for test environment 30 | npm run build:stage 31 | 32 | # build for production environment 33 | npm run build:prod 34 | ``` 35 | 36 | ## Advanced 37 | 38 | ```bash 39 | # preview the release environment effect 40 | npm run preview 41 | 42 | # preview the release environment effect + static resource analysis 43 | npm run preview -- --report 44 | 45 | # code format check 46 | npm run lint 47 | 48 | # code format check and auto fix 49 | npm run lint -- --fix 50 | ``` 51 | 52 | ## Extra 53 | 54 | For `typescript` version, you can use [element3-admin-ts-template](https://github.com/hug-sun/element3-admin-ts-template) 55 | 56 | ## Related Project 57 | 58 | - [element3-admin-template](https://github.com/hug-sun/element3-admin-template) 59 | 60 | - [element3-admin-ts-template](https://github.com/hug-sun/element3-admin-ts-template) 61 | 62 | - [element3-admin](https://github.com/hug-sun/element3-admin) 63 | 64 | ## Browsers support 65 | 66 | Modern browsers and Internet Explorer 10+. 67 | 68 | | [](http://godban.github.io/browsers-support-badges/)IE / Edge | [](http://godban.github.io/browsers-support-badges/)Firefox | [](http://godban.github.io/browsers-support-badges/)Chrome | [](http://godban.github.io/browsers-support-badges/)Safari | 69 | | --------- | --------- | --------- | --------- | 70 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions 71 | 72 | 73 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app 4 | '@vue/cli-plugin-babel/preset' 5 | ], 6 | 'env': { 7 | 'development': { 8 | // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). 9 | // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. 10 | // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html 11 | 'plugins': ['dynamic-import-node'] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const { param2Obj } = require('./utils') 3 | 4 | const user = require('./user') 5 | const table = require('./table') 6 | const role = require('./role') 7 | const article = require('./article') 8 | const search = require('./remote-search') 9 | 10 | const mocks = [ 11 | ...user, 12 | ...table, 13 | ...role, 14 | ...article, 15 | ...search 16 | ] 17 | 18 | // for front mock 19 | // please use it cautiously, it will redefine XMLHttpRequest, 20 | // which will cause many of your third-party libraries to be invalidated(like progress event). 21 | function mockXHR() { 22 | // mock patch 23 | // https://github.com/nuysoft/Mock/issues/300 24 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 25 | Mock.XHR.prototype.send = function() { 26 | if (this.custom.xhr) { 27 | this.custom.xhr.withCredentials = this.withCredentials || false 28 | 29 | if (this.responseType) { 30 | this.custom.xhr.responseType = this.responseType 31 | } 32 | } 33 | this.proxy_send(...arguments) 34 | } 35 | 36 | function XHR2ExpressReqWrap(respond) { 37 | return function(options) { 38 | let result = null 39 | if (respond instanceof Function) { 40 | const { body, type, url } = options 41 | // https://expressjs.com/en/4x/api.html#req 42 | result = respond({ 43 | method: type, 44 | body: JSON.parse(body), 45 | query: param2Obj(url) 46 | }) 47 | } else { 48 | result = respond 49 | } 50 | return Mock.mock(result) 51 | } 52 | } 53 | 54 | for (const i of mocks) { 55 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) 56 | } 57 | } 58 | 59 | module.exports = { 60 | mocks, 61 | mockXHR 62 | } 63 | -------------------------------------------------------------------------------- /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(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) 42 | } 43 | } 44 | } 45 | 46 | module.exports = app => { 47 | // parse app.body 48 | // https://expressjs.com/en/4x/api.html#req.body 49 | app.use(bodyParser.json()) 50 | app.use(bodyParser.urlencoded({ 51 | extended: true 52 | })) 53 | 54 | const mockRoutes = registerRoutes(app) 55 | var mockRoutesLength = mockRoutes.mockRoutesLength 56 | var mockStartIndex = mockRoutes.mockStartIndex 57 | 58 | // watch files, hot reload mock server 59 | chokidar.watch(mockDir, { 60 | ignored: /mock-server/, 61 | ignoreInitial: true 62 | }).on('all', (event, path) => { 63 | if (event === 'change' || event === 'add') { 64 | try { 65 | // remove mock routes stack 66 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 67 | 68 | // clear routes cache 69 | unregisterRoutes() 70 | 71 | const mockRoutes = registerRoutes(app) 72 | mockRoutesLength = mockRoutes.mockRoutesLength 73 | mockStartIndex = mockRoutes.mockStartIndex 74 | 75 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) 76 | } catch (error) { 77 | console.log(chalk.redBright(error)) 78 | } 79 | } 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /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(Mock.mock({ 8 | name: '@first' 9 | })) 10 | } 11 | NameList.push({ name: 'mock-Pan' }) 12 | 13 | module.exports = [ 14 | // username search 15 | { 16 | url: '/vue-element-admin/search/user', 17 | type: 'get', 18 | response: config => { 19 | const { name } = config.query 20 | const mockNameList = NameList.filter(item => { 21 | const lowerCaseName = item.name.toLowerCase() 22 | return !(name && lowerCaseName.indexOf(name.toLowerCase()) < 0) 23 | }) 24 | return { 25 | code: 20000, 26 | data: { items: mockNameList } 27 | } 28 | } 29 | }, 30 | 31 | // transaction list 32 | { 33 | url: '/vue-element-admin/transaction/list', 34 | type: 'get', 35 | response: _ => { 36 | return { 37 | code: 20000, 38 | data: { 39 | total: 20, 40 | 'items|20': [{ 41 | order_no: '@guid()', 42 | timestamp: +Mock.Random.date('T'), 43 | username: '@name()', 44 | price: '@float(1000, 15000, 0, 2)', 45 | 'status|1': ['success', 'pending'] 46 | }] 47 | } 48 | } 49 | } 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /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: 'Just a visitor. Can only see the home page and the document page', 24 | routes: [{ 25 | path: '', 26 | redirect: 'dashboard', 27 | children: [ 28 | { 29 | path: 'dashboard', 30 | name: 'Dashboard', 31 | meta: { title: 'dashboard', icon: 'dashboard' } 32 | } 33 | ] 34 | }] 35 | } 36 | ] 37 | 38 | module.exports = [ 39 | // mock get all routes form server 40 | { 41 | url: '/vue-element-admin/routes', 42 | type: 'get', 43 | response: _ => { 44 | return { 45 | code: 20000, 46 | data: routes 47 | } 48 | } 49 | }, 50 | 51 | // mock get all roles form server 52 | { 53 | url: '/vue-element-admin/roles', 54 | type: 'get', 55 | response: _ => { 56 | return { 57 | code: 20000, 58 | data: roles 59 | } 60 | } 61 | }, 62 | 63 | // add role 64 | { 65 | url: '/vue-element-admin/role', 66 | type: 'post', 67 | response: { 68 | code: 20000, 69 | data: { 70 | key: Mock.mock('@integer(300, 5000)') 71 | } 72 | } 73 | }, 74 | 75 | // update role 76 | { 77 | url: '/vue-element-admin/role/[A-Za-z0-9]', 78 | type: 'put', 79 | response: { 80 | code: 20000, 81 | data: { 82 | status: 'success' 83 | } 84 | } 85 | }, 86 | 87 | // delete role 88 | { 89 | url: '/vue-element-admin/role/[A-Za-z0-9]', 90 | type: 'delete', 91 | response: { 92 | code: 20000, 93 | data: { 94 | status: 'success' 95 | } 96 | } 97 | } 98 | ] 99 | -------------------------------------------------------------------------------- /mock/table.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | const data = Mock.mock({ 4 | 'items|30': [{ 5 | id: '@id', 6 | title: '@sentence(10, 20)', 7 | 'status|1': ['published', 'draft', 'deleted'], 8 | author: 'name', 9 | display_time: '@datetime', 10 | pageviews: '@integer(300, 5000)' 11 | }] 12 | }) 13 | 14 | module.exports = [ 15 | { 16 | url: '/vue-element-admin/table/list', 17 | type: 'get', 18 | response: config => { 19 | const items = data.items 20 | return { 21 | code: 20000, 22 | data: { 23 | total: items.length, 24 | items: items 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /mock/user.js: -------------------------------------------------------------------------------- 1 | 2 | const tokens = { 3 | admin: { 4 | token: 'admin-token' 5 | }, 6 | editor: { 7 | token: 'editor-token' 8 | } 9 | } 10 | 11 | const users = { 12 | 'admin-token': { 13 | roles: ['admin'], 14 | introduction: 'I am a super administrator', 15 | avatar: '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: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 22 | name: 'Normal Editor' 23 | } 24 | } 25 | 26 | module.exports = [ 27 | // user login 28 | { 29 | url: '/vue-element-admin/user/login', 30 | type: 'post', 31 | response: config => { 32 | const { username } = config.body 33 | const token = tokens[username] 34 | 35 | // mock error 36 | if (!token) { 37 | return { 38 | code: 60204, 39 | message: 'Account and password are incorrect.' 40 | } 41 | } 42 | 43 | return { 44 | code: 20000, 45 | data: token 46 | } 47 | } 48 | }, 49 | 50 | // get user info 51 | { 52 | url: '/vue-element-admin/user/info\.*', 53 | type: 'get', 54 | response: config => { 55 | const { token } = config.query 56 | const info = users[token] 57 | 58 | // mock error 59 | if (!info) { 60 | return { 61 | code: 50008, 62 | message: 'Login failed, unable to get user details.' 63 | } 64 | } 65 | 66 | return { 67 | code: 20000, 68 | data: info 69 | } 70 | } 71 | }, 72 | 73 | // user logout 74 | { 75 | url: '/vue-element-admin/user/logout', 76 | type: 'post', 77 | response: _ => { 78 | return { 79 | code: 20000, 80 | data: 'success' 81 | } 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | 'plugins': { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | 'autoprefixer': {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, // 每行代码长度(默认80) 3 | tabWidth: 2, // 每个tab相当于多少个空格(默认2) 4 | useTabs: false, // 是否使用tab进行缩进(默认false) 5 | singleQuote: true, // 使用单引号(默认false) 6 | semi: false, // 声明结尾使用分号(默认true) 7 | trailingComma: 'es5', // 多行使用拖尾逗号(默认none) 8 | bracketSpacing: true, // 对象字面量的大括号间使用空格(默认true) 9 | jsxBracketSameLine: false, // 多行JSX中的>放置在最后一行的结尾,而不是另起一行(默认false) 10 | arrowParens: 'avoid', // 只有一个参数的箭头函数的参数是否带圆括号(默认avoid) 11 | } 12 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hug-sun/element3-admin/d91ec877140141dd663fabb4567add5c5d99e1f2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue. 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/api/article.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchList(query) { 4 | return request({ 5 | url: '/vue-element-admin/article/list', 6 | method: 'get', 7 | params: query, 8 | }) 9 | } 10 | 11 | export function fetchArticle(id) { 12 | return request({ 13 | url: '/vue-element-admin/article/detail', 14 | method: 'get', 15 | params: { id }, 16 | }) 17 | } 18 | 19 | export function fetchPv(pv) { 20 | return request({ 21 | url: '/vue-element-admin/article/pv', 22 | method: 'get', 23 | params: { pv }, 24 | }) 25 | } 26 | 27 | export function createArticle(data) { 28 | return request({ 29 | url: '/vue-element-admin/article/create', 30 | method: 'post', 31 | data, 32 | }) 33 | } 34 | 35 | export function updateArticle(data) { 36 | return request({ 37 | url: '/vue-element-admin/article/update', 38 | method: 'post', 39 | data, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/api/qiniu.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getToken() { 4 | return request({ 5 | url: '/qiniu/upload/token', // 假地址 自行替换 6 | method: 'get', 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/api/remote-search.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function searchUser(name) { 4 | return request({ 5 | url: '/vue-element-admin/search/user', 6 | method: 'get', 7 | params: { name }, 8 | }) 9 | } 10 | 11 | export function transactionList(query) { 12 | return request({ 13 | url: '/vue-element-admin/transaction/list', 14 | method: 'get', 15 | params: query, 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/api/role.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getRoutes() { 4 | return request({ 5 | url: '/vue-element-admin/routes', 6 | method: 'get', 7 | }) 8 | } 9 | 10 | export function getRoles() { 11 | return request({ 12 | url: '/vue-element-admin/roles', 13 | method: 'get', 14 | }) 15 | } 16 | 17 | export function addRole(data) { 18 | return request({ 19 | url: '/vue-element-admin/role', 20 | method: 'post', 21 | data, 22 | }) 23 | } 24 | 25 | export function updateRole(id, data) { 26 | return request({ 27 | url: `/vue-element-admin/role/${id}`, 28 | method: 'put', 29 | data, 30 | }) 31 | } 32 | 33 | export function deleteRole(id) { 34 | return request({ 35 | url: `/vue-element-admin/role/${id}`, 36 | method: 'delete', 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/api/table.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList(params) { 4 | return request({ 5 | url: '/vue-element-admin/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: '/vue-element-admin/user/login', 6 | method: 'post', 7 | data, 8 | }) 9 | } 10 | 11 | export function getInfo(token) { 12 | return request({ 13 | url: '/vue-element-admin/user/info', 14 | method: 'get', 15 | params: { token }, 16 | }) 17 | } 18 | 19 | export function logout() { 20 | return request({ 21 | url: '/vue-element-admin/user/logout', 22 | method: 'post', 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hug-sun/element3-admin/d91ec877140141dd663fabb4567add5c5d99e1f2/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hug-sun/element3-admin/d91ec877140141dd663fabb4567add5c5d99e1f2/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hug-sun/element3-admin/d91ec877140141dd663fabb4567add5c5d99e1f2/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hug-sun/element3-admin/d91ec877140141dd663fabb4567add5c5d99e1f2/src/assets/custom-theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hug-sun/element3-admin/d91ec877140141dd663fabb4567add5c5d99e1f2/src/assets/custom-theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ item.meta.title }} 6 | {{ item.meta.title }} 7 | 8 | 9 | 10 | 11 | 12 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /src/components/Charts/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null, 7 | $_resizeHandler: null, 8 | } 9 | }, 10 | mounted() { 11 | this.initListener() 12 | }, 13 | activated() { 14 | if (!this.$_resizeHandler) { 15 | // avoid duplication init 16 | this.initListener() 17 | } 18 | 19 | // when keep-alive chart activated, auto resize 20 | this.resize() 21 | }, 22 | beforeUnmount() { 23 | this.destroyListener() 24 | }, 25 | deactivated() { 26 | this.destroyListener() 27 | }, 28 | methods: { 29 | // use $_ for mixins properties 30 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 31 | $_sidebarResizeHandler(e) { 32 | if (e.propertyName === 'width') { 33 | this.$_resizeHandler() 34 | } 35 | }, 36 | initListener() { 37 | this.$_resizeHandler = debounce(() => { 38 | this.resize() 39 | }, 100) 40 | window.addEventListener('resize', this.$_resizeHandler) 41 | 42 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 43 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) 44 | }, 45 | destroyListener() { 46 | window.removeEventListener('resize', this.$_resizeHandler) 47 | this.$_resizeHandler = null 48 | 49 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) 50 | }, 51 | resize() { 52 | const { chart } = this 53 | chart && chart.resize() 54 | }, 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /src/components/DragSelect/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 50 | 51 | 66 | -------------------------------------------------------------------------------- /src/components/ErrorLog/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Error Log 12 | Clear All 13 | 14 | 15 | 16 | 17 | 18 | Msg: 19 | 20 | {{ row.err.message }} 21 | 22 | 23 | 24 | 25 | Info: 26 | 27 | {{ row.vm.$vnode.tag }} error in {{ row.info }} 28 | 29 | 30 | 31 | 32 | Url: 33 | 34 | {{ row.url }} 35 | 36 | 37 | 38 | 39 | 40 | 41 | {{ scope.row.err.stack }} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 70 | 71 | 79 | -------------------------------------------------------------------------------- /src/components/GithubCorner/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 17 | 22 | 23 | 24 | 25 | 26 | 55 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 14 | 15 | 16 | 17 | 18 | 34 | 35 | 47 | -------------------------------------------------------------------------------- /src/components/JsonEditor/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 56 | 57 | 78 | -------------------------------------------------------------------------------- /src/components/Kanban/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ headerText }} 5 | 6 | 13 | 14 | {{ element.name }} {{ element.id }} 15 | 16 | 17 | 18 | 19 | 20 | 55 | 100 | -------------------------------------------------------------------------------- /src/components/MarkdownEditor/default-options.js: -------------------------------------------------------------------------------- 1 | // doc: https://nhnent.github.io/tui.editor/api/latest/ToastUIEditor.html#ToastUIEditor 2 | export default { 3 | minHeight: '200px', 4 | previewStyle: 'vertical', 5 | useCommandShortcut: true, 6 | useDefaultHTMLSanitizer: true, 7 | usageStatistics: false, 8 | hideModeSwitch: false, 9 | toolbarItems: [ 10 | 'heading', 11 | 'bold', 12 | 'italic', 13 | 'strike', 14 | 'divider', 15 | 'hr', 16 | 'quote', 17 | 'divider', 18 | 'ul', 19 | 'ol', 20 | 'task', 21 | 'indent', 22 | 'outdent', 23 | 'divider', 24 | 'table', 25 | 'image', 26 | 'link', 27 | 'divider', 28 | 'code', 29 | 'codeblock', 30 | ], 31 | } 32 | -------------------------------------------------------------------------------- /src/components/NavLink/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 39 | -------------------------------------------------------------------------------- /src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 92 | 93 | 102 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 53 | 54 | 64 | -------------------------------------------------------------------------------- /src/components/Share/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | {{ item.title }} 7 | {{ item.title }} 8 | 9 | 10 | 11 | 12 | 13 | 39 | 40 | 104 | -------------------------------------------------------------------------------- /src/components/SizeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | {{ item.label }} 14 | 15 | 16 | 17 | 18 | 19 | 61 | -------------------------------------------------------------------------------- /src/components/Sticky/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | sticky 9 | 10 | 11 | 12 | 13 | 14 | 92 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /src/components/Tinymce/dynamicLoadScript.js: -------------------------------------------------------------------------------- 1 | let callbacks = [] 2 | 3 | function loadedTinymce() { 4 | // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144 5 | // check is successfully downloaded script 6 | return window.tinymce 7 | } 8 | 9 | const dynamicLoadScript = (src, callback) => { 10 | const existingScript = document.getElementById(src) 11 | const cb = callback || function() {} 12 | 13 | if (!existingScript) { 14 | const script = document.createElement('script') 15 | script.src = src // src url for the third-party library being loaded. 16 | script.id = src 17 | document.body.appendChild(script) 18 | callbacks.push(cb) 19 | const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd 20 | onEnd(script) 21 | } 22 | 23 | if (existingScript && cb) { 24 | if (loadedTinymce()) { 25 | cb(null, existingScript) 26 | } else { 27 | callbacks.push(cb) 28 | } 29 | } 30 | 31 | function stdOnEnd(script) { 32 | script.onload = function() { 33 | // this.onload = null here is necessary 34 | // because even IE9 works not like others 35 | this.onerror = this.onload = null 36 | for (const cb of callbacks) { 37 | cb(null, script) 38 | } 39 | callbacks = null 40 | } 41 | script.onerror = function() { 42 | this.onerror = this.onload = null 43 | cb(new Error('Failed to load ' + src), script) 44 | } 45 | } 46 | 47 | function ieOnEnd(script) { 48 | script.onreadystatechange = function() { 49 | if (this.readyState !== 'complete' && this.readyState !== 'loaded') return 50 | this.onreadystatechange = null 51 | for (const cb of callbacks) { 52 | cb(null, script) // there is no way to catch loading errors in IE8 53 | } 54 | callbacks = null 55 | } 56 | } 57 | } 58 | 59 | export default dynamicLoadScript 60 | -------------------------------------------------------------------------------- /src/components/Tinymce/plugins.js: -------------------------------------------------------------------------------- 1 | // Any plugins you want to use has to be imported 2 | // Detail plugins list see https://www.tinymce.com/docs/plugins/ 3 | // Custom builds see https://www.tinymce.com/download/custom-builds/ 4 | 5 | const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount'] 6 | 7 | export default plugins 8 | -------------------------------------------------------------------------------- /src/components/Tinymce/toolbar.js: -------------------------------------------------------------------------------- 1 | // Here is a list of the toolbar 2 | // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols 3 | 4 | const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen'] 5 | 6 | export default toolbar 7 | -------------------------------------------------------------------------------- /src/directive/clipboard/clipboard.js: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/Inndy/vue-clipboard2 2 | const Clipboard = require('clipboard') 3 | if (!Clipboard) { 4 | throw new Error('you should npm install `clipboard` --save at first ') 5 | } 6 | 7 | export default { 8 | bind(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() { return binding.value }, 16 | action() { return binding.arg === 'cut' ? 'cut' : 'copy' } 17 | }) 18 | clipboard.on('success', e => { 19 | const callback = el._v_clipboard_success 20 | callback && callback(e) // eslint-disable-line 21 | }) 22 | clipboard.on('error', e => { 23 | const callback = el._v_clipboard_error 24 | callback && callback(e) // eslint-disable-line 25 | }) 26 | el._v_clipboard = clipboard 27 | } 28 | }, 29 | update(el, binding) { 30 | if (binding.arg === 'success') { 31 | el._v_clipboard_success = binding.value 32 | } else if (binding.arg === 'error') { 33 | el._v_clipboard_error = binding.value 34 | } else { 35 | el._v_clipboard.text = function() { return binding.value } 36 | el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' } 37 | } 38 | }, 39 | unbind(el, binding) { 40 | if (binding.arg === 'success') { 41 | delete el._v_clipboard_success 42 | } else if (binding.arg === 'error') { 43 | delete el._v_clipboard_error 44 | } else { 45 | el._v_clipboard.destroy() 46 | delete el._v_clipboard 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /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/el-drag-dialog/drag.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bind(el, binding, vnode) { 3 | const dialogHeaderEl = el.querySelector('.el-dialog__header') 4 | const dragDom = el.querySelector('.el-dialog') 5 | dialogHeaderEl.style.cssText += ';cursor:move;' 6 | dragDom.style.cssText += ';top:0px;' 7 | 8 | // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); 9 | const getStyle = (function() { 10 | if (window.document.currentStyle) { 11 | return (dom, attr) => dom.currentStyle[attr] 12 | } else { 13 | return (dom, attr) => getComputedStyle(dom, false)[attr] 14 | } 15 | })() 16 | 17 | dialogHeaderEl.onmousedown = (e) => { 18 | // 鼠标按下,计算当前元素距离可视区的距离 19 | const disX = e.clientX - dialogHeaderEl.offsetLeft 20 | const disY = e.clientY - dialogHeaderEl.offsetTop 21 | 22 | const dragDomWidth = dragDom.offsetWidth 23 | const dragDomHeight = dragDom.offsetHeight 24 | 25 | const screenWidth = document.body.clientWidth 26 | const screenHeight = document.body.clientHeight 27 | 28 | const minDragDomLeft = dragDom.offsetLeft 29 | const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth 30 | 31 | const minDragDomTop = dragDom.offsetTop 32 | const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight 33 | 34 | // 获取到的值带px 正则匹配替换 35 | let styL = getStyle(dragDom, 'left') 36 | let styT = getStyle(dragDom, 'top') 37 | 38 | if (styL.includes('%')) { 39 | styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100) 40 | styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100) 41 | } else { 42 | styL = +styL.replace(/\px/g, '') 43 | styT = +styT.replace(/\px/g, '') 44 | } 45 | 46 | document.onmousemove = function(e) { 47 | // 通过事件委托,计算移动的距离 48 | let left = e.clientX - disX 49 | let top = e.clientY - disY 50 | 51 | // 边界处理 52 | if (-(left) > minDragDomLeft) { 53 | left = -minDragDomLeft 54 | } else if (left > maxDragDomLeft) { 55 | left = maxDragDomLeft 56 | } 57 | 58 | if (-(top) > minDragDomTop) { 59 | top = -minDragDomTop 60 | } else if (top > maxDragDomTop) { 61 | top = maxDragDomTop 62 | } 63 | 64 | // 移动当前元素 65 | dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;` 66 | 67 | // emit onDrag event 68 | vnode.child.$emit('dragDialog') 69 | } 70 | 71 | document.onmouseup = function(e) { 72 | document.onmousemove = null 73 | document.onmouseup = null 74 | } 75 | } 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /src/directive/el-drag-dialog/index.js: -------------------------------------------------------------------------------- 1 | import drag from './drag' 2 | 3 | const install = function(app) { 4 | app.directive('el-drag-dialog', drag) 5 | } 6 | 7 | if (window.app) { 8 | window['el-drag-dialog'] = drag 9 | app.use(install); // eslint-disable-line 10 | } 11 | 12 | drag.install = install 13 | export default drag 14 | -------------------------------------------------------------------------------- /src/directive/permission/index.js: -------------------------------------------------------------------------------- 1 | import permission from './permission' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('permission', permission) 5 | } 6 | 7 | permission.install = install 8 | export default permission 9 | -------------------------------------------------------------------------------- /src/directive/permission/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | function checkPermission(el, binding) { 4 | const { value } = binding 5 | const roles = store.getters && store.getters.roles 6 | 7 | if (value && value instanceof Array) { 8 | if (value.length > 0) { 9 | const permissionRoles = value 10 | 11 | const hasPermission = roles.some(role => { 12 | return permissionRoles.includes(role) 13 | }) 14 | 15 | if (!hasPermission) { 16 | el.parentNode && el.parentNode.removeChild(el) 17 | } 18 | } 19 | } else { 20 | throw new Error(`need roles! Like v-permission="['admin','editor']"`) 21 | } 22 | } 23 | 24 | export default { 25 | inserted(el, binding) { 26 | checkPermission(el, binding) 27 | }, 28 | update(el, binding) { 29 | checkPermission(el, binding) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /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 | ele: el, // 波纹作用元素 10 | type: 'hit', // hit 点击位置扩散 center中心点扩展 11 | color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 12 | }, 13 | customOpts 14 | ) 15 | const target = opts.ele 16 | if (target) { 17 | target.style.position = 'relative' 18 | target.style.overflow = 'hidden' 19 | const rect = target.getBoundingClientRect() 20 | let ripple = target.querySelector('.waves-ripple') 21 | if (!ripple) { 22 | ripple = document.createElement('span') 23 | ripple.className = 'waves-ripple' 24 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' 25 | target.appendChild(ripple) 26 | } else { 27 | ripple.className = 'waves-ripple' 28 | } 29 | switch (opts.type) { 30 | case 'center': 31 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px' 32 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px' 33 | break 34 | default: 35 | ripple.style.top = 36 | (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop || 37 | document.body.scrollTop) + 'px' 38 | ripple.style.left = 39 | (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || 40 | document.body.scrollLeft) + 'px' 41 | } 42 | ripple.style.backgroundColor = opts.color 43 | ripple.className = 'waves-ripple z-active' 44 | return false 45 | } 46 | } 47 | 48 | if (!el[context]) { 49 | el[context] = { 50 | removeHandle: handle 51 | } 52 | } else { 53 | el[context].removeHandle = handle 54 | } 55 | 56 | return handle 57 | } 58 | 59 | export default { 60 | bind(el, binding) { 61 | el.addEventListener('click', handleClick(el, binding), false) 62 | }, 63 | update(el, binding) { 64 | el.removeEventListener('click', el[context].removeHandle, false) 65 | el.addEventListener('click', handleClick(el, binding), false) 66 | }, 67 | unbind(el) { 68 | el.removeEventListener('click', el[context].removeHandle, false) 69 | el[context] = null 70 | delete el[context] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import SvgIcon from '@/components/SvgIcon' // svg component 2 | 3 | export function useIcons(app) { 4 | // register globally 5 | app.component('SvgIcon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => 9 | requireContext.keys().map(requireContext) 10 | requireAll(req) 11 | } 12 | -------------------------------------------------------------------------------- /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.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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 21 | 22 | 45 | 46 | 54 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | }, 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | }, 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 45 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | 9 | 10 | {{ title }} 11 | 12 | 13 | 14 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 57 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | 5 | export { default as TagsView } from './TagsView' 6 | export { default as Settings } from './Settings' 7 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | }, 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeUnmount() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | }, 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /src/router/modules/charts.js: -------------------------------------------------------------------------------- 1 | /** When your routing table is too long, you can split it into small modules**/ 2 | 3 | import Layout from '@/layout' 4 | 5 | const chartsRouter = { 6 | path: '/charts', 7 | component: Layout, 8 | redirect: 'noRedirect', 9 | name: 'Charts', 10 | meta: { 11 | title: 'Charts', 12 | icon: 'chart', 13 | }, 14 | children: [ 15 | { 16 | path: 'keyboard', 17 | component: () => import('@/views/charts/keyboard'), 18 | name: 'KeyboardChart', 19 | meta: { title: 'Keyboard Chart', noCache: true }, 20 | }, 21 | { 22 | path: 'line', 23 | component: () => import('@/views/charts/line'), 24 | name: 'LineChart', 25 | meta: { title: 'Line Chart', noCache: true }, 26 | }, 27 | { 28 | path: 'mix-chart', 29 | component: () => import('@/views/charts/mix-chart'), 30 | name: 'MixChart', 31 | meta: { title: 'Mix Chart', noCache: true }, 32 | }, 33 | ], 34 | } 35 | 36 | export default chartsRouter 37 | -------------------------------------------------------------------------------- /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' 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: () => import('@/views/nested/menu1/index'), // Parent router-view 18 | name: 'Menu1', 19 | meta: { title: 'Menu 1' }, 20 | redirect: '/nested/menu1/menu1-1', 21 | children: [ 22 | { 23 | path: 'menu1-1', 24 | component: () => import('@/views/nested/menu1/menu1-1'), 25 | name: 'Menu1-1', 26 | meta: { title: 'Menu 1-1' }, 27 | }, 28 | { 29 | path: 'menu1-2', 30 | component: () => import('@/views/nested/menu1/menu1-2'), 31 | name: 'Menu1-2', 32 | redirect: '/nested/menu1/menu1-2/menu1-2-1', 33 | meta: { title: 'Menu 1-2' }, 34 | children: [ 35 | { 36 | path: 'menu1-2-1', 37 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'), 38 | name: 'Menu1-2-1', 39 | meta: { title: 'Menu 1-2-1' }, 40 | }, 41 | { 42 | path: 'menu1-2-2', 43 | component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'), 44 | name: 'Menu1-2-2', 45 | meta: { title: 'Menu 1-2-2' }, 46 | }, 47 | ], 48 | }, 49 | { 50 | path: 'menu1-3', 51 | component: () => import('@/views/nested/menu1/menu1-3'), 52 | name: 'Menu1-3', 53 | meta: { title: 'Menu 1-3' }, 54 | }, 55 | ], 56 | }, 57 | { 58 | path: 'menu2', 59 | name: 'Menu2', 60 | component: () => import('@/views/nested/menu2/index'), 61 | meta: { title: 'Menu 2' }, 62 | }, 63 | ], 64 | } 65 | 66 | export default nestedRouter 67 | -------------------------------------------------------------------------------- /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' 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: 'dynamic-table', 17 | component: () => import('@/views/table/dynamic-table/index'), 18 | name: 'DynamicTable', 19 | meta: { title: 'Dynamic Table' }, 20 | }, 21 | // { 22 | // path: 'drag-table', 23 | // component: () => import('@/views/table/drag-table'), 24 | // name: 'DragTable', 25 | // meta: { title: 'Drag Table' } 26 | // }, 27 | // { 28 | // path: 'inline-edit-table', 29 | // component: () => import('@/views/table/inline-edit-table'), 30 | // name: 'InlineEditTable', 31 | // meta: { title: 'Inline Edit' } 32 | // }, 33 | // { 34 | // path: 'complex-table', 35 | // component: () => import('@/views/table/complex-table'), 36 | // name: 'ComplexTable', 37 | // meta: { title: 'Complex Table' } 38 | // } 39 | ] 40 | } 41 | export default tableRouter 42 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Vue Admin Template', 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 fix the header 13 | */ 14 | fixedHeader: false, 15 | 16 | /** 17 | * @type {boolean} true | false 18 | * @description Whether show the logo in sidebar 19 | */ 20 | sidebarLogo: false, 21 | 22 | /** 23 | * @type {boolean} true | false 24 | * @description Whether need tagsView 25 | */ 26 | tagsView: true, 27 | 28 | /** 29 | * @type {string | array} 'production' | ['production', 'development'] 30 | * @description Need show err logs component. 31 | * The default is only used in the production env 32 | * If you want to also use it in dev, you can pass ['production', 'development'] 33 | */ 34 | errorLog: 'production', 35 | } 36 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: (state) => state.app.sidebar, 3 | device: (state) => state.app.device, 4 | token: (state) => state.user.token, 5 | avatar: (state) => state.user.avatar, 6 | name: (state) => state.user.name, 7 | roles: state => state.user.roles, 8 | permission_routes: (state) => state.permission.routes, 9 | size: (state) => state.app.size, 10 | errorLogs: (state) => state.errorLog.logs, 11 | } 12 | export default getters 13 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex' 2 | import getters from './getters' 3 | import app from './modules/app' 4 | import settings from './modules/settings' 5 | import user from './modules/user' 6 | import tagsView from './modules/tagsView' 7 | import permission from './modules/permission' 8 | import errorLog from './modules/errorLog' 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | app, 13 | settings, 14 | user, 15 | tagsView, 16 | permission, 17 | errorLog, 18 | }, 19 | getters, 20 | }) 21 | 22 | export default store 23 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false, 7 | }, 8 | device: 'desktop', 9 | size: Cookies.get('size') || 'medium', 10 | } 11 | 12 | const mutations = { 13 | TOGGLE_SIDEBAR: state => { 14 | state.sidebar.opened = !state.sidebar.opened 15 | state.sidebar.withoutAnimation = false 16 | if (state.sidebar.opened) { 17 | Cookies.set('sidebarStatus', 1) 18 | } else { 19 | Cookies.set('sidebarStatus', 0) 20 | } 21 | }, 22 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 23 | Cookies.set('sidebarStatus', 0) 24 | state.sidebar.opened = false 25 | state.sidebar.withoutAnimation = withoutAnimation 26 | }, 27 | TOGGLE_DEVICE: (state, device) => { 28 | state.device = device 29 | }, 30 | SET_SIZE: (state, size) => { 31 | state.size = size 32 | Cookies.set('size', size) 33 | }, 34 | } 35 | 36 | const actions = { 37 | toggleSideBar({ commit }) { 38 | commit('TOGGLE_SIDEBAR') 39 | }, 40 | closeSideBar({ commit }, { withoutAnimation }) { 41 | commit('CLOSE_SIDEBAR', withoutAnimation) 42 | }, 43 | toggleDevice({ commit }, device) { 44 | commit('TOGGLE_DEVICE', device) 45 | }, 46 | setSize({ commit }, size) { 47 | commit('SET_SIZE', size) 48 | }, 49 | } 50 | 51 | export default { 52 | namespaced: true, 53 | state, 54 | mutations, 55 | actions, 56 | } 57 | -------------------------------------------------------------------------------- /src/store/modules/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 | import router from '@/router' 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 | removeRoutes: [], // 用于删除动态路由 41 | } 42 | 43 | const mutations = { 44 | SET_ROUTES: (state, routes) => { 45 | state.addRoutes = routes 46 | state.routes = constantRoutes.concat(routes) 47 | }, 48 | SET_REMOVE_ROUTES: (state, routes) => { 49 | state.removeRoutes = routes 50 | }, 51 | } 52 | 53 | const actions = { 54 | generateRoutes({ commit }, roles) { 55 | return new Promise(resolve => { 56 | let accessedRoutes 57 | if (roles.includes('admin')) { 58 | accessedRoutes = asyncRoutes || [] 59 | } else { 60 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) 61 | } 62 | commit('SET_ROUTES', accessedRoutes) 63 | resolve(accessedRoutes) 64 | }) 65 | }, 66 | addRoutes({ commit }, accessRoutes) { 67 | // 添加动态路由,同时保存移除函数,将来如果需要重置路由可以用到它们 68 | const removeRoutes = [] 69 | accessRoutes.forEach(route => { 70 | const removeRoute = router.addRoute(route) 71 | removeRoutes.push(removeRoute) 72 | }) 73 | commit('SET_REMOVE_ROUTES', removeRoutes) 74 | }, 75 | resetRoutes({ commit, state }) { 76 | // 重置路由为初始状态,用户切换角色时需要用到 77 | state.removeRoutes.forEach(fn => fn()) 78 | // 路由数据重置 79 | commit('SET_ROUTES', []) 80 | }, 81 | } 82 | 83 | export default { 84 | namespaced: true, 85 | state, 86 | mutations, 87 | actions, 88 | } 89 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import variables from '@/styles/element-variables.scss' 2 | import defaultSettings from '@/settings' 3 | 4 | const { showSettings, fixedHeader, sidebarLogo, tagsView } = defaultSettings 5 | 6 | const state = { 7 | showSettings: showSettings, 8 | fixedHeader: fixedHeader, 9 | sidebarLogo: sidebarLogo, 10 | tagsView: tagsView, 11 | theme: variables.theme, 12 | } 13 | 14 | const mutations = { 15 | CHANGE_SETTING: (state, { key, value }) => { 16 | // eslint-disable-next-line no-prototype-builtins 17 | if (state.hasOwnProperty(key)) { 18 | state[key] = value 19 | } 20 | }, 21 | } 22 | 23 | const actions = { 24 | changeSetting({ commit }, data) { 25 | commit('CHANGE_SETTING', data) 26 | }, 27 | } 28 | 29 | export default { 30 | namespaced: true, 31 | state, 32 | mutations, 33 | actions, 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/styles/btn.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | @mixin colorBtn($color) { 4 | background: $color; 5 | 6 | &:hover { 7 | color: $color; 8 | 9 | &:before, 10 | &:after { 11 | background: $color; 12 | } 13 | } 14 | } 15 | 16 | .blue-btn { 17 | @include colorBtn($blue) 18 | } 19 | 20 | .light-blue-btn { 21 | @include colorBtn($light-blue) 22 | } 23 | 24 | .red-btn { 25 | @include colorBtn($red) 26 | } 27 | 28 | .pink-btn { 29 | @include colorBtn($pink) 30 | } 31 | 32 | .green-btn { 33 | @include colorBtn($green) 34 | } 35 | 36 | .tiffany-btn { 37 | @include colorBtn($tiffany) 38 | } 39 | 40 | .yellow-btn { 41 | @include colorBtn($yellow) 42 | } 43 | 44 | .pan-btn { 45 | font-size: 14px; 46 | color: #fff; 47 | padding: 14px 36px; 48 | border-radius: 8px; 49 | border: none; 50 | outline: none; 51 | transition: 600ms ease all; 52 | position: relative; 53 | display: inline-block; 54 | 55 | &:hover { 56 | background: #fff; 57 | 58 | &:before, 59 | &:after { 60 | width: 100%; 61 | transition: 600ms ease all; 62 | } 63 | } 64 | 65 | &:before, 66 | &:after { 67 | content: ''; 68 | position: absolute; 69 | top: 0; 70 | right: 0; 71 | height: 2px; 72 | width: 0; 73 | transition: 400ms ease all; 74 | } 75 | 76 | &::after { 77 | right: inherit; 78 | top: inherit; 79 | left: 0; 80 | bottom: 0; 81 | } 82 | } 83 | 84 | .custom-button { 85 | display: inline-block; 86 | line-height: 1; 87 | white-space: nowrap; 88 | cursor: pointer; 89 | background: #fff; 90 | color: #fff; 91 | -webkit-appearance: none; 92 | text-align: center; 93 | box-sizing: border-box; 94 | outline: 0; 95 | margin: 0; 96 | padding: 10px 15px; 97 | font-size: 14px; 98 | border-radius: 4px; 99 | } 100 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | .cell { 19 | .el-tag { 20 | margin-right: 0px; 21 | } 22 | } 23 | 24 | .small-padding { 25 | .cell { 26 | padding-left: 5px; 27 | padding-right: 5px; 28 | } 29 | } 30 | 31 | .fixed-width { 32 | .el-button--mini { 33 | padding: 7px 10px; 34 | min-width: 60px; 35 | } 36 | } 37 | 38 | .status-col { 39 | .cell { 40 | padding: 0 10px; 41 | text-align: center; 42 | 43 | .el-tag { 44 | margin-right: 0px; 45 | } 46 | } 47 | } 48 | 49 | // to fixed https://github.com/ElemeFE/element/issues/2461 50 | .el-dialog { 51 | transform: none; 52 | left: 0; 53 | position: relative; 54 | margin: 0 auto; 55 | } 56 | 57 | // refine element ui upload 58 | .upload-container { 59 | .el-upload { 60 | width: 100%; 61 | 62 | .el-upload-dragger { 63 | width: 100%; 64 | height: 200px; 65 | } 66 | } 67 | } 68 | 69 | // dropdown 70 | .el-dropdown-menu { 71 | a { 72 | display: block 73 | } 74 | } 75 | 76 | // fix date-picker ui bug in filter-item 77 | .el-range-editor.el-input__inner { 78 | display: inline-flex !important; 79 | } 80 | 81 | // to fix el-date-picker css style 82 | .el-range-separator { 83 | box-sizing: content-box; 84 | } 85 | -------------------------------------------------------------------------------- /src/styles/element-variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * I think element-ui's default theme color is too light for long-term use. 3 | * So I modified the default color and you can modify it to your liking. 4 | **/ 5 | 6 | /* theme color */ 7 | $--color-primary: #1890ff; 8 | $--color-success: #13ce66; 9 | $--color-warning: #ffba00; 10 | $--color-danger: #ff4949; 11 | // $--color-info: #1E1E1E; 12 | 13 | $--button-font-weight: 400; 14 | 15 | // $--color-text-regular: #1f2d3d; 16 | 17 | $--border-color-light: #dfe4ed; 18 | $--border-color-lighter: #e6ebf5; 19 | 20 | $--table-border: 1px solid #dfe6ec; 21 | 22 | /* icon font path, required */ 23 | $--font-path: "~element3/lib/theme-chalk/fonts"; 24 | 25 | @import "~element3/packages/theme-chalk/src/index"; 26 | 27 | // the :export directive is the magic sauce for webpack 28 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 29 | :export { 30 | theme: $--color-primary; 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | @mixin pct($pct) { 31 | width: #{$pct}; 32 | position: relative; 33 | margin: 0 auto; 34 | } 35 | 36 | @mixin triangle($width, $height, $color, $direction) { 37 | $width: $width/2; 38 | $color-border-style: $height solid $color; 39 | $transparent-border-style: $width solid transparent; 40 | height: 0; 41 | width: 0; 42 | 43 | @if $direction==up { 44 | border-bottom: $color-border-style; 45 | border-left: $transparent-border-style; 46 | border-right: $transparent-border-style; 47 | } 48 | 49 | @else if $direction==right { 50 | border-left: $color-border-style; 51 | border-top: $transparent-border-style; 52 | border-bottom: $transparent-border-style; 53 | } 54 | 55 | @else if $direction==down { 56 | border-top: $color-border-style; 57 | border-left: $transparent-border-style; 58 | border-right: $transparent-border-style; 59 | } 60 | 61 | @else if $direction==left { 62 | border-right: $color-border-style; 63 | border-top: $transparent-border-style; 64 | border-bottom: $transparent-border-style; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // base color 2 | $blue:#324157; 3 | $light-blue:#3A71A8; 4 | $red:#C03639; 5 | $pink: #E65D6E; 6 | $green: #30B08F; 7 | $tiffany: #4AB7BD; 8 | $yellow:#FEC171; 9 | $panGreen: #30B08F; 10 | 11 | // sidebar 12 | $menuText:#bfcbd9; 13 | $menuActiveText:#409EFF; 14 | $subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951 15 | 16 | $menuBg:#304156; 17 | $menuHover:#263445; 18 | 19 | $subMenuBg:#1f2d3d; 20 | $subMenuHover:#001528; 21 | 22 | $sideBarWidth: 210px; 23 | 24 | // the :export directive is the magic sauce for webpack 25 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 26 | :export { 27 | menuText: $menuText; 28 | menuActiveText: $menuActiveText; 29 | subMenuActiveText: $subMenuActiveText; 30 | menuBg: $menuBg; 31 | menuHover: $menuHover; 32 | subMenuBg: $subMenuBg; 33 | subMenuHover: $subMenuHover; 34 | sideBarWidth: $sideBarWidth; 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'vue_admin_template_token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/clipboard.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Clipboard from 'clipboard' 3 | // import { useMessage } from 'element3' 4 | 5 | // const Message = useMessage() 6 | function clipboardSuccess() { 7 | this.$message({ 8 | message: 'Copy successfully', 9 | type: 'success', 10 | duration: 1500, 11 | }) 12 | } 13 | 14 | function clipboardError() { 15 | Message({ 16 | message: 'Copy failed', 17 | type: 'error', 18 | }) 19 | } 20 | 21 | export default function handleClipboard(text, event) { 22 | const clipboard = new Clipboard(event.target, { 23 | text: () => text, 24 | }) 25 | clipboard.on('success', () => { 26 | clipboardSuccess() 27 | clipboard.destroy() 28 | }) 29 | clipboard.on('error', () => { 30 | clipboardError() 31 | clipboard.destroy() 32 | }) 33 | clipboard.onClick(event) 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Vue Admin Template' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | /** 4 | * @param {Array} value 5 | * @returns {Boolean} 6 | * @example see @/views/permission/directive.vue 7 | */ 8 | export default function checkPermission(value) { 9 | if (value && value instanceof Array && value.length > 0) { 10 | const roles = store.getters && store.getters.roles 11 | const permissionRoles = value 12 | 13 | const hasPermission = roles.some(role => { 14 | return permissionRoles.includes(role) 15 | }) 16 | return hasPermission 17 | } else { 18 | console.error(`need roles! Like v-permission="['admin','editor']"`) 19 | return false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | /** 16 | * Because it's so fucking difficult to detect the scrolling element, just move them all 17 | * @param {number} amount 18 | */ 19 | function move(amount) { 20 | document.documentElement.scrollTop = amount 21 | document.body.parentNode.scrollTop = amount 22 | document.body.scrollTop = amount 23 | } 24 | 25 | function position() { 26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 27 | } 28 | 29 | /** 30 | * @param {number} to 31 | * @param {number} duration 32 | * @param {Function} callback 33 | */ 34 | export function scrollTo(to, duration, callback) { 35 | const start = position() 36 | const change = to - start 37 | const increment = 20 38 | let currentTime = 0 39 | duration = (typeof (duration) === 'undefined') ? 500 : duration 40 | var animateScroll = function() { 41 | // increment the time 42 | currentTime += increment 43 | // find the value with the quadratic in-out easing function 44 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 45 | // move the document.body 46 | move(val) 47 | // do the animation unless its over 48 | if (currentTime < duration) { 49 | requestAnimFrame(animateScroll) 50 | } else { 51 | if (callback && typeof (callback) === 'function') { 52 | // the animation is done so lets callback 53 | callback() 54 | } 55 | } 56 | } 57 | animateScroll() 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen 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 valid_map = ['admin', 'editor'] 19 | return valid_map.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 | -------------------------------------------------------------------------------- /src/vendor/Export2Zip.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { saveAs } from 'file-saver' 3 | import JSZip from 'jszip' 4 | 5 | export function export_txt_to_zip(th, jsonData, txtName, zipName) { 6 | const zip = new JSZip() 7 | const txt_name = txtName || 'file' 8 | const zip_name = zipName || 'file' 9 | const data = jsonData 10 | let txtData = `${th}\r\n` 11 | data.forEach((row) => { 12 | let tempStr = '' 13 | tempStr = row.toString() 14 | txtData += `${tempStr}\r\n` 15 | }) 16 | zip.file(`${txt_name}.txt`, txtData) 17 | zip.generateAsync({ 18 | type: "blob" 19 | }).then((blob) => { 20 | saveAs(blob, `${zip_name}.zip`) 21 | }, (err) => { 22 | alert('导出失败') 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/views/charts/keyboard.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /src/views/charts/line.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /src/views/charts/mix-chart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /src/views/clipboard/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | copy 8 | 9 | 10 | 11 | 12 | 13 | copy 14 | 15 | 16 | 17 | 18 | 19 | 20 | 49 | 50 | -------------------------------------------------------------------------------- /src/views/components-demo/dnd-list.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /src/views/components-demo/drag-dialog.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | open a Drag Dialog 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 63 | -------------------------------------------------------------------------------- /src/views/components-demo/drag-kanban.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 39 | 66 | 67 | -------------------------------------------------------------------------------- /src/views/components-demo/drag-select.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ item }} 10 | 11 | 12 | 13 | 14 | 15 | 44 | -------------------------------------------------------------------------------- /src/views/components-demo/json-editor.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 29 | 30 | 36 | 37 | -------------------------------------------------------------------------------- /src/views/components-demo/tinymce.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 30 | 31 | 36 | 37 | -------------------------------------------------------------------------------- /src/views/dashboard/admin/components/BarChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 103 | -------------------------------------------------------------------------------- /src/views/dashboard/admin/components/PieChart.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 80 | -------------------------------------------------------------------------------- /src/views/dashboard/admin/components/TodoList/Todo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 22 | 23 | 24 | 25 | 82 | -------------------------------------------------------------------------------- /src/views/dashboard/admin/components/TransactionTable.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ scope.row.order_no }} 6 | 7 | 8 | 9 | 10 | ¥{{ $filters.toThousandFilter(scope.row.price) }} 11 | 12 | 13 | 14 | 15 | 16 | {{ row.status }} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 54 | -------------------------------------------------------------------------------- /src/views/dashboard/admin/components/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null, 7 | $_resizeHandler: null, 8 | } 9 | }, 10 | mounted() { 11 | this.$_resizeHandler = debounce(() => { 12 | if (this.chart) { 13 | this.chart.resize() 14 | } 15 | }, 100) 16 | this.$_initResizeEvent() 17 | this.$_initSidebarResizeEvent() 18 | }, 19 | beforeUnmount() { 20 | this.$_destroyResizeEvent() 21 | this.$_destroySidebarResizeEvent() 22 | }, 23 | // to fixed bug when cached by keep-alive 24 | // https://github.com/PanJiaChen/vue-element-admin/issues/2116 25 | activated() { 26 | this.$_initResizeEvent() 27 | this.$_initSidebarResizeEvent() 28 | }, 29 | deactivated() { 30 | this.$_destroyResizeEvent() 31 | this.$_destroySidebarResizeEvent() 32 | }, 33 | methods: { 34 | // use $_ for mixins properties 35 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 36 | $_initResizeEvent() { 37 | window.addEventListener('resize', this.$_resizeHandler) 38 | }, 39 | $_destroyResizeEvent() { 40 | window.removeEventListener('resize', this.$_resizeHandler) 41 | }, 42 | $_sidebarResizeHandler(e) { 43 | if (e.propertyName === 'width') { 44 | this.$_resizeHandler() 45 | } 46 | }, 47 | $_initSidebarResizeEvent() { 48 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 49 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) 50 | }, 51 | $_destroySidebarResizeEvent() { 52 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) 53 | }, 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /src/views/dashboard/editor/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Your roles: 6 | {{ item }} 7 | 8 | 9 | 10 | {{ name }} 11 | Editor's Dashboard 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 42 | 43 | 75 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 32 | -------------------------------------------------------------------------------- /src/views/error-log/components/ErrorTestA.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ a.a }} 5 | 6 | 7 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /src/views/error-log/components/ErrorTestB.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/views/error-log/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | // todo 4 | 6 | Please click the bug icon in the upper right corner 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /src/views/example/components/Dropdown/Comment.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ !comment_disabled?'Comment: opened':'Comment: closed' }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | Close comment 12 | 13 | 14 | Open comment 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 42 | -------------------------------------------------------------------------------- /src/views/example/components/Dropdown/Platform.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Platfroms({{ platforms.length }}) 5 | 6 | 7 | 8 | 9 | 10 | {{ item.name }} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 47 | -------------------------------------------------------------------------------- /src/views/example/components/Dropdown/SourceUrl.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Link 5 | 6 | 7 | 8 | 9 | 10 | 11 | URL 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 39 | -------------------------------------------------------------------------------- /src/views/example/components/Dropdown/index.js: -------------------------------------------------------------------------------- 1 | export { default as CommentDropdown } from './Comment' 2 | export { default as PlatformDropdown } from './Platform' 3 | export { default as SourceUrlDropdown } from './SourceUrl' 4 | -------------------------------------------------------------------------------- /src/views/example/components/Warning.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/example/create.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/example/edit.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/excel/components/AutoWidthOption.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cell Auto-Width: 4 | 5 | 6 | True 7 | 8 | 9 | False 10 | 11 | 12 | 13 | 14 | 15 | 35 | -------------------------------------------------------------------------------- /src/views/excel/components/BookTypeOption.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | Book Type: 4 | 5 | 11 | 12 | 13 | 14 | 15 | 40 | -------------------------------------------------------------------------------- /src/views/excel/components/FilenameOption.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | Filename: 4 | 5 | 6 | 7 | 8 | 28 | -------------------------------------------------------------------------------- /src/views/excel/upload-excel.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 43 | -------------------------------------------------------------------------------- /src/views/guide/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | Show Guide 10 | 11 | 12 | 13 | 14 | 37 | -------------------------------------------------------------------------------- /src/views/guide/steps.js: -------------------------------------------------------------------------------- 1 | const steps = [ 2 | { 3 | element: '#hamburger-container', 4 | popover: { 5 | title: 'Hamburger', 6 | description: 'Open && Close sidebar', 7 | position: 'bottom' 8 | } 9 | }, 10 | { 11 | element: '#breadcrumb-container', 12 | popover: { 13 | title: 'Breadcrumb', 14 | description: 'Indicate the current page location', 15 | position: 'bottom' 16 | } 17 | }, 18 | { 19 | element: '#header-search', 20 | popover: { 21 | title: 'Page Search', 22 | description: 'Page search, quick navigation', 23 | position: 'left' 24 | } 25 | }, 26 | { 27 | element: '#screenfull', 28 | popover: { 29 | title: 'Screenfull', 30 | description: 'Set the page into fullscreen', 31 | position: 'left' 32 | } 33 | }, 34 | { 35 | element: '#size-select', 36 | popover: { 37 | title: 'Switch Size', 38 | description: 'Switch the system size', 39 | position: 'left' 40 | } 41 | }, 42 | { 43 | element: '#tags-view-container', 44 | popover: { 45 | title: 'Tags view', 46 | description: 'The history of the page you visited', 47 | position: 'bottom' 48 | }, 49 | padding: 0 50 | } 51 | ] 52 | 53 | export default steps 54 | -------------------------------------------------------------------------------- /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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-1/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/menu1-2-1/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/menu1-2-2/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-3/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/nested/menu2/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/views/pdf/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Click to download PDF 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/permission/components/SwitchRoles.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Your roles: {{ roles }} 5 | 6 | Switch roles: 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /src/views/permission/page.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/views/profile/components/Account.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 14 | 15 | 16 | Update 17 | 18 | 19 | 20 | 21 | 45 | -------------------------------------------------------------------------------- /src/views/profile/components/Timeline.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ item.title }} 7 | {{ item.content }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 44 | -------------------------------------------------------------------------------- /src/views/profile/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 63 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/views/tab/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | mounted times :{{ createdTimes }} 4 | 5 | 6 | 7 | // todo keep--alive缓存失效 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 53 | 54 | 59 | -------------------------------------------------------------------------------- /src/views/table/dynamic-table/components/FixedThead.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | apple 7 | 8 | 9 | banana 10 | 11 | 12 | orange 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{ scope.row[fruit] }} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 66 | 67 | -------------------------------------------------------------------------------- /src/views/table/dynamic-table/components/UnfixedThead.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | apple 7 | 8 | 9 | banana 10 | 11 | 12 | orange 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{ scope.row[fruit] }} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 51 | -------------------------------------------------------------------------------- /src/views/table/dynamic-table/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fixed header, sorted by header order, 5 | 6 | 7 | 8 | 9 | Not fixed header, sorted by click order 10 | 11 | 12 | 13 | 14 | 15 | 24 | 25 | -------------------------------------------------------------------------------- /src/views/table/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | {{ scope.index_ + 1 }} 13 | 14 | 15 | 16 | 17 | {{ scope.row.title }} 18 | 19 | 20 | 21 | 22 | {{ scope.row.author }} 23 | 24 | 25 | 26 | 27 | {{ scope.row.pageviews }} 28 | 29 | 30 | 31 | 32 | {{ scope.row.status }} 33 | 34 | 35 | 36 | 37 | 38 | {{ scope.row.display_time }} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 77 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | 3 | describe('Utils:formatTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | const retrofit = 5 * 1000 6 | 7 | it('ten digits timestamp', () => { 8 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 9 | }) 10 | it('test now', () => { 11 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 12 | }) 13 | it('less two minute', () => { 14 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 15 | }) 16 | it('less two hour', () => { 17 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 18 | }) 19 | it('less one day', () => { 20 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 21 | }) 22 | it('more than one day', () => { 23 | expect(formatTime(d)).toBe('7月13日17时54分') 24 | }) 25 | it('format', () => { 26 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 27 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 28 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/unit/utils/param2Obj.spec.js: -------------------------------------------------------------------------------- 1 | import { param2Obj } from '@/utils/index.js' 2 | describe('Utils:param2Obj', () => { 3 | const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' 4 | 5 | it('param2Obj test', () => { 6 | expect(param2Obj(url)).toEqual({ 7 | name: 'bill', 8 | age: '29', 9 | sex: '1', 10 | field: window.btoa('test'), 11 | key: '测试' 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | it('timestamp string', () => { 9 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') 10 | }) 11 | it('ten digits timestamp', () => { 12 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 13 | }) 14 | it('new Date', () => { 15 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 16 | }) 17 | it('format', () => { 18 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 19 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 20 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 21 | }) 22 | it('get the day of the week', () => { 23 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 24 | }) 25 | it('get the day of the week', () => { 26 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 27 | }) 28 | it('empty argument', () => { 29 | expect(parseTime()).toBeNull() 30 | }) 31 | 32 | it('null', () => { 33 | expect(parseTime(null)).toBeNull() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, isExternal } from '@/utils/validate.js' 2 | 3 | describe('Utils:validate', () => { 4 | it('validUsername', () => { 5 | expect(validUsername('admin')).toBe(true) 6 | expect(validUsername('editor')).toBe(true) 7 | expect(validUsername('xxxx')).toBe(false) 8 | }) 9 | it('isExternal', () => { 10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) 13 | expect(isExternal('/dashboard')).toBe(false) 14 | expect(isExternal('./dashboard')).toBe(false) 15 | expect(isExternal('dashboard')).toBe(false) 16 | }) 17 | }) 18 | --------------------------------------------------------------------------------
{{ item.content }}