├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── build └── index.js ├── docAssets └── vue-element-admin-simplify.svg ├── jest.config.js ├── jsconfig.json ├── mock ├── article.js ├── index.js ├── mock-server.js ├── remote-search.js ├── role │ ├── index.js │ └── routes.js ├── user.js └── utils.js ├── package.json ├── plop-templates ├── component │ ├── index.hbs │ └── prompt.js ├── store │ ├── index.hbs │ └── prompt.js ├── utils.js └── view │ ├── index.hbs │ └── prompt.js ├── plopfile.js ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ ├── account.js │ ├── article.js │ ├── email.js │ ├── inviteCode.js │ ├── notice.js │ ├── qiniu.js │ ├── remote-search.js │ ├── role.js │ ├── server.js │ ├── serverConfig.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 │ ├── Dropzone │ │ └── index.vue │ ├── ErrorLog │ │ └── index.vue │ ├── GithubCorner │ │ └── index.vue │ ├── Hamburger │ │ └── index.vue │ ├── HeaderSearch │ │ └── index.vue │ ├── ImageCropper │ │ ├── index.vue │ │ └── utils │ │ │ ├── data2blob.js │ │ │ ├── effectRipple.js │ │ │ ├── language.js │ │ │ └── mimes.js │ ├── JsonEditor │ │ └── index.vue │ ├── Kanban │ │ └── index.vue │ ├── MDinput │ │ └── index.vue │ ├── MarkdownEditor │ │ ├── default-options.js │ │ └── 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 │ ├── Upload │ │ ├── SingleImage.vue │ │ ├── SingleImage2.vue │ │ └── SingleImage3.vue │ └── UploadExcel │ │ └── index.vue ├── directive │ ├── clipboard │ │ ├── clipboard.js │ │ └── index.js │ ├── el-drag-dialog │ │ ├── drag.js │ │ └── index.js │ ├── el-table │ │ ├── adaptive.js │ │ └── index.js │ ├── permission │ │ ├── index.js │ │ └── permission.js │ ├── sticky.js │ └── waves │ │ ├── index.js │ │ ├── waves.css │ │ └── waves.js ├── filters │ └── index.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 │ ├── error-log.js │ ├── get-page-title.js │ ├── index.js │ ├── open-window.js │ ├── permission.js │ ├── request.js │ ├── scroll-to.js │ └── validate.js ├── vendor │ ├── Export2Excel.js │ └── Export2Zip.js └── views │ ├── account │ ├── VipAccount.vue │ └── userAccount.vue │ ├── adminUser │ ├── components │ │ ├── Dropdown │ │ │ ├── Comment.vue │ │ │ ├── Platform.vue │ │ │ ├── SourceUrl.vue │ │ │ └── index.js │ │ └── UserDetail.vue │ ├── create.vue │ └── list.vue │ ├── charts │ ├── keyboard.vue │ ├── line.vue │ └── mix-chart.vue │ ├── clipboard │ └── index.vue │ ├── components-demo │ ├── avatar-upload.vue │ ├── back-to-top.vue │ ├── count-to.vue │ ├── dnd-list.vue │ ├── drag-dialog.vue │ ├── drag-kanban.vue │ ├── drag-select.vue │ ├── dropzone.vue │ ├── json-editor.vue │ ├── markdown.vue │ ├── mixin.vue │ ├── split-pane.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 │ ├── index.vue │ └── vip │ │ ├── components │ │ └── Notice.vue │ │ └── index.vue │ ├── documentation │ └── 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 │ ├── inviteCode │ └── list.vue │ ├── login │ ├── auth-redirect.vue │ └── 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 │ ├── notice │ ├── components │ │ └── NoticeDetail.vue │ ├── create.vue │ ├── edit.vue │ └── list.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 │ ├── qiniu │ └── upload.vue │ ├── redirect │ └── index.vue │ ├── server │ ├── components │ │ ├── Dropdown │ │ │ ├── Comment.vue │ │ │ ├── Platform.vue │ │ │ ├── SourceUrl.vue │ │ │ └── index.js │ │ └── ServerDetail.vue │ ├── create.vue │ ├── edit.vue │ └── list.vue │ ├── serverConfig │ ├── components │ │ ├── Dropdown │ │ │ ├── Comment.vue │ │ │ ├── Platform.vue │ │ │ ├── SourceUrl.vue │ │ │ └── index.js │ │ └── ServerConfigDetail.vue │ ├── create.vue │ ├── edit.vue │ └── list.vue │ ├── tab │ ├── components │ │ └── TabPane.vue │ └── index.vue │ ├── table │ ├── complex-table.vue │ ├── drag-table.vue │ ├── dynamic-table │ │ ├── components │ │ │ ├── FixedThead.vue │ │ │ └── UnfixedThead.vue │ │ └── index.vue │ └── inline-edit-table.vue │ ├── theme │ └── index.vue │ └── zip │ └── index.vue ├── tests └── unit │ ├── .eslintrc.js │ ├── components │ ├── Hamburger.spec.js │ └── SvgIcon.spec.js │ └── utils │ ├── formatTime.spec.js │ ├── param2Obj.spec.js │ ├── parseTime.spec.js │ └── validate.spec.js └── vue.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = 'http://127.0.0.1/api' 6 | 7 | 8 | # vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, 9 | # to control whether the babel-plugin-dynamic-import-node plugin is enabled. 10 | # It only does one thing by converting all import() to require(). 11 | # This configuration can significantly increase the speed of hot updates, 12 | # when you have a large number of pages. 13 | # Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js 14 | 15 | VUE_CLI_BABEL_TRANSPILE_MODULES = true 16 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/api' 6 | 7 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | **/*.log 8 | 9 | tests/**/coverage/ 10 | tests/e2e/reports 11 | selenium-debug.log 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.local 21 | 22 | package-lock.json 23 | yarn.lock 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 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.md: -------------------------------------------------------------------------------- 1 | [主项目/master project ](https://github.com/master-coder-ll/v2ray-web-manager) 2 | 3 | ## 环境 4 | node v16.9.1 5 | [node_modules](https://gitee.com/wubie/vue2-element-admin-simplify/raw/master/%5Bbackup%5Dnode_modules.zip) 6 | 7 | ## Build 8 | 9 | ```bash 10 | # build for test environment 11 | npm run build:stage 12 | 13 | # build for production environment 14 | npm run build:prod 15 | ``` 16 | 17 | ## Advanced 18 | 19 | ```bash 20 | # preview the release environment effect 21 | npm run preview 22 | 23 | # preview the release environment effect + static resource analysis 24 | npm run preview -- --report 25 | 26 | # code format check 27 | npm run lint 28 | 29 | # code format check and auto fix 30 | npm run lint -- --fix 31 | ``` 32 | 33 | Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information 34 | 35 | ## Browsers support 36 | 37 | Modern browsers and Internet Explorer 10+. 38 | 39 | | [](https://godban.github.io/browsers-support-badges/)IE / Edge | [](https://godban.github.io/browsers-support-badges/)Firefox | [](https://godban.github.io/browsers-support-badges/)Chrome | [](https://godban.github.io/browsers-support-badges/)Safari | 40 | | --------- | --------- | --------- | --------- | 41 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions 42 | 43 | ## License 44 | 45 | [MIT](https://github.com/PanJiaChen/vue-element-admin/blob/master/LICENSE) 46 | 47 | Copyright (c) 2017-present PanJiaChen 48 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const { param2Obj } = require('./utils') 3 | 4 | const user = require('./user') 5 | const role = require('./role') 6 | const article = require('./article') 7 | const search = require('./remote-search') 8 | 9 | const mocks = [ 10 | ...user, 11 | ...role, 12 | ...article, 13 | ...search 14 | ] 15 | 16 | // for front mock 17 | // please use it cautiously, it will redefine XMLHttpRequest, 18 | // which will cause many of your third-party libraries to be invalidated(like progress event). 19 | function mockXHR() { 20 | // mock patch 21 | // https://github.com/nuysoft/Mock/issues/300 22 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 23 | Mock.XHR.prototype.send = function() { 24 | if (this.custom.xhr) { 25 | this.custom.xhr.withCredentials = this.withCredentials || false 26 | 27 | if (this.responseType) { 28 | this.custom.xhr.responseType = this.responseType 29 | } 30 | } 31 | this.proxy_send(...arguments) 32 | } 33 | 34 | function XHR2ExpressReqWrap(respond) { 35 | return function(options) { 36 | let result = null 37 | if (respond instanceof Function) { 38 | const { body, type, url } = options 39 | // https://expressjs.com/en/4x/api.html#req 40 | result = respond({ 41 | method: type, 42 | body: JSON.parse(body), 43 | query: param2Obj(url) 44 | }) 45 | } else { 46 | result = respond 47 | } 48 | return Mock.mock(result) 49 | } 50 | } 51 | 52 | for (const i of mocks) { 53 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) 54 | } 55 | } 56 | 57 | module.exports = { 58 | mocks, 59 | mockXHR 60 | } 61 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /plop-templates/component/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if template}} 2 | 3 | 4 | 5 | {{/if}} 6 | 7 | {{#if script}} 8 | 20 | {{/if}} 21 | 22 | {{#if style}} 23 | 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /plop-templates/component/prompt.js: -------------------------------------------------------------------------------- 1 | const { notEmpty } = require('../utils.js') 2 | 3 | module.exports = { 4 | description: 'generate vue component', 5 | prompts: [{ 6 | type: 'input', 7 | name: 'name', 8 | message: 'component name please', 9 | validate: notEmpty('name') 10 | }, 11 | { 12 | type: 'checkbox', 13 | name: 'blocks', 14 | message: 'Blocks:', 15 | choices: [{ 16 | name: '', 17 | value: 'template', 18 | checked: true 19 | }, 20 | { 21 | name: ' 20 | {{/if}} 21 | 22 | {{#if style}} 23 | 26 | {{/if}} 27 | -------------------------------------------------------------------------------- /plop-templates/view/prompt.js: -------------------------------------------------------------------------------- 1 | const { notEmpty } = require('../utils.js') 2 | 3 | module.exports = { 4 | description: 'generate a view', 5 | prompts: [{ 6 | type: 'input', 7 | name: 'name', 8 | message: 'view name please', 9 | validate: notEmpty('name') 10 | }, 11 | { 12 | type: 'checkbox', 13 | name: 'blocks', 14 | message: 'Blocks:', 15 | choices: [{ 16 | name: '', 17 | value: 'template', 18 | checked: true 19 | }, 20 | { 21 | name: ' 12 | -------------------------------------------------------------------------------- /src/api/account.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 获取一个用户的账号列表 5 | * @param {*} id 6 | */ 7 | export function getAccount(id) { 8 | return request({ 9 | url: `/account/${id}`, 10 | method: 'get' 11 | }) 12 | } 13 | 14 | 15 | /** 16 | * 生成新的订阅地址 17 | * @param {type} data type 0,通用 1以上预留 18 | */ 19 | export function generatorSubscriptionUrl(data) { 20 | return request({ 21 | url: `/account/generatorSubscriptionUrl`, 22 | method: 'get', 23 | data 24 | }) 25 | } 26 | /** 27 | * 获取账号连接数 28 | * @param {*} id 29 | */ 30 | export function getConnection(id) { 31 | return request({ 32 | url: `account/connection/${id}`, 33 | method: 'get' 34 | }) 35 | } 36 | 37 | /** 38 | * 根据服务器获取V2rayAccount 39 | * @param {serverId} data serverId 40 | */ 41 | export function getV2rayAccount(data) { 42 | return request({ 43 | url: `/account/v2rayAccount`, 44 | method: 'get', 45 | params: data 46 | }) 47 | } 48 | /** 49 | * 获取所有用户的用户列表 50 | * @param {*} data 51 | */ 52 | export function accountsList(data) { 53 | return request({ 54 | url: `/account`, 55 | method: 'get', 56 | params: data 57 | }) 58 | } 59 | 60 | 61 | /** 62 | * 更新v2ray账号 63 | * @param data 64 | */ 65 | export function updateAccount(data) { 66 | return request({ 67 | url: `/account`, 68 | method: 'put', 69 | data 70 | }) 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/api/article.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function fetchList(query) { 4 | return request({ 5 | url: '/article/list', 6 | method: 'get', 7 | params: query 8 | }) 9 | } 10 | 11 | export function fetchArticle(id) { 12 | return request({ 13 | url: '/article/detail', 14 | method: 'get', 15 | params: { id } 16 | }) 17 | } 18 | 19 | export function fetchPv(pv) { 20 | return request({ 21 | url: '/article/pv', 22 | method: 'get', 23 | params: { pv } 24 | }) 25 | } 26 | 27 | export function createArticle(data) { 28 | return request({ 29 | url: '/article/create', 30 | method: 'post', 31 | data 32 | }) 33 | } 34 | 35 | export function updateArticle(data) { 36 | return request({ 37 | url: '/article/update', 38 | method: 'post', 39 | data 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/api/email.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function sendEmail(email, type,inviteCode) { 4 | return request({ 5 | url: '/user/send-email', 6 | method: 'get', 7 | params: { 'email': email, 'type': type ,'inviteCode':inviteCode} 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/api/inviteCode.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 获取邀请码列表 5 | * @param {*} page 6 | */ 7 | export function list(page) { 8 | return request({ 9 | url: `invite-code`, 10 | method: 'get', 11 | params: page 12 | }) 13 | } 14 | 15 | /** 16 | * 删除 17 | * @param {*} id 18 | */ 19 | export function del(id) { 20 | return request({ 21 | url: `invite-code/${id}`, 22 | method: 'DELETE' 23 | }) 24 | } 25 | /** 26 | * 更新v2ray账号 27 | * @param data 28 | */ 29 | export function generate(data) { 30 | return request({ 31 | url: `invite-code`, 32 | method: 'post', 33 | data 34 | }) 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/api/notice.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function listNotice() { 4 | return request({ 5 | url: 'notice', 6 | method: 'get', 7 | }) 8 | } 9 | 10 | export function addNotice(data) { 11 | return request({ 12 | url: 'notice', 13 | method: 'post', 14 | data 15 | }) 16 | } 17 | 18 | export function updateNotice(data) { 19 | return request({ 20 | url: 'notice', 21 | method: 'put', 22 | data 23 | }) 24 | } 25 | export function getNotice(id) { 26 | return request({ 27 | url: `notice/${id}`, 28 | method: 'get' 29 | }) 30 | } 31 | export function delNotice(id) { 32 | return request({ 33 | url: `notice/${id}`, 34 | method: 'delete' 35 | }) 36 | } -------------------------------------------------------------------------------- /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: '/search/user', 6 | method: 'get', 7 | params: { name } 8 | }) 9 | } 10 | 11 | export function transactionList(query) { 12 | return request({ 13 | url: '/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: '/routes', 6 | method: 'get' 7 | }) 8 | } 9 | 10 | export function getRoles() { 11 | return request({ 12 | url: '/roles', 13 | method: 'get' 14 | }) 15 | } 16 | 17 | export function addRole(data) { 18 | return request({ 19 | url: '/role', 20 | method: 'post', 21 | data 22 | }) 23 | } 24 | 25 | export function updateRole(id, data) { 26 | return request({ 27 | url: `/role/${id}`, 28 | method: 'put', 29 | data 30 | }) 31 | } 32 | 33 | export function deleteRole(id) { 34 | return request({ 35 | url: `/role/${id}`, 36 | method: 'delete' 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/api/server.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function addServer(data) { 4 | return request({ 5 | url: '/server', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | /** 11 | * 更新 12 | * @param {} data 13 | */ 14 | export function updateServer(data) { 15 | return request({ 16 | url: '/server', 17 | method: 'put', 18 | data 19 | }) 20 | } 21 | 22 | /** 23 | * 获取一个server 24 | * @param {} id 25 | */ 26 | export function getServer(id) { 27 | return request({ 28 | url: `/server/${id}`, 29 | method: 'get' 30 | }) 31 | } 32 | /** 33 | * list admin 34 | * @param {int}} page 35 | * @param {int} pageSize 36 | */ 37 | export function serverList(page) { 38 | return request({ 39 | url: `/server`, 40 | method: 'get', 41 | params: page 42 | }) 43 | } 44 | /** 45 | * 获取可用的服务器列表 vip 46 | 47 | */ 48 | export function availableServers() { 49 | return request({ 50 | url: `/server/findServersForAccount`, 51 | method: 'get', 52 | }) 53 | } 54 | /** 55 | * 删除 56 | * @param {*} id 57 | */ 58 | export function deleteServer(id) { 59 | return request({ 60 | url: `/server/${id}`, 61 | method: 'DELETE' 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /src/api/serverConfig.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function addServerConfig(data) { 4 | return request({ 5 | url: 'serverConfig', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | /** 11 | * 更新 12 | * @param {} data 13 | */ 14 | export function updateServerConfig(data) { 15 | return request({ 16 | url: 'serverConfig', 17 | method: 'put', 18 | data 19 | }) 20 | } 21 | 22 | /** 23 | * 获取一个server 24 | * @param {} id 25 | */ 26 | export function getServerConfig(id) { 27 | return request({ 28 | url: `serverConfig/${id}`, 29 | method: 'get' 30 | }) 31 | } 32 | /** 33 | * list 34 | * @param {int}} page 35 | * @param {int} pageSize 36 | */ 37 | export function listServerConfig(page) { 38 | return request({ 39 | url: `serverConfig`, 40 | method: 'get', 41 | params: page 42 | }) 43 | } 44 | 45 | /** 46 | * 删除 47 | * @param {*} id 48 | */ 49 | export function deleteServerConfig(id) { 50 | return request({ 51 | url: `serverConfig/${id}`, 52 | method: 'DELETE' 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-coder-ll/v2ray-manager-console/0ef7883ea70417383b21e2fe953d80cf9340bf2b/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-coder-ll/v2ray-manager-console/0ef7883ea70417383b21e2fe953d80cf9340bf2b/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-coder-ll/v2ray-manager-console/0ef7883ea70417383b21e2fe953d80cf9340bf2b/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-coder-ll/v2ray-manager-console/0ef7883ea70417383b21e2fe953d80cf9340bf2b/src/assets/custom-theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/master-coder-ll/v2ray-manager-console/0ef7883ea70417383b21e2fe953d80cf9340bf2b/src/assets/custom-theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /src/components/Charts/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null 7 | } 8 | }, 9 | mounted() { 10 | this.__resizeHandler = debounce(() => { 11 | if (this.chart) { 12 | this.chart.resize() 13 | } 14 | }, 100) 15 | window.addEventListener('resize', this.__resizeHandler) 16 | 17 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 18 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) 19 | }, 20 | beforeDestroy() { 21 | window.removeEventListener('resize', this.__resizeHandler) 22 | 23 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) 24 | }, 25 | methods: { 26 | // use $_ for mixins properties 27 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 28 | $_sidebarResizeHandler(e) { 29 | if (e.propertyName === 'width') { 30 | this.__resizeHandler() 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/DragSelect/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 50 | 51 | 62 | -------------------------------------------------------------------------------- /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 | 12 | 13 | 14 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /src/components/ImageCropper/utils/data2blob.js: -------------------------------------------------------------------------------- 1 | /** 2 | * database64文件格式转换为2进制 3 | * 4 | * @param {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了 5 | * @param {[String]} mime [description] 6 | * @return {[blob]} [description] 7 | */ 8 | export default function(data, mime) { 9 | data = data.split(',')[1] 10 | data = window.atob(data) 11 | var ia = new Uint8Array(data.length) 12 | for (var i = 0; i < data.length; i++) { 13 | ia[i] = data.charCodeAt(i) 14 | } 15 | // canvas.toDataURL 返回的默认格式就是 image/png 16 | return new Blob([ia], { 17 | type: mime 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/ImageCropper/utils/effectRipple.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 点击波纹效果 3 | * 4 | * @param {[event]} e [description] 5 | * @param {[Object]} arg_opts [description] 6 | * @return {[bollean]} [description] 7 | */ 8 | export default function(e, arg_opts) { 9 | var opts = Object.assign({ 10 | ele: e.target, // 波纹作用元素 11 | type: 'hit', // hit点击位置扩散center中心点扩展 12 | bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 13 | }, arg_opts) 14 | var target = opts.ele 15 | if (target) { 16 | var rect = target.getBoundingClientRect() 17 | var ripple = target.querySelector('.e-ripple') 18 | if (!ripple) { 19 | ripple = document.createElement('span') 20 | ripple.className = 'e-ripple' 21 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' 22 | target.appendChild(ripple) 23 | } else { 24 | ripple.className = 'e-ripple' 25 | } 26 | switch (opts.type) { 27 | case 'center': 28 | ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px' 29 | ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px' 30 | break 31 | default: 32 | ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px' 33 | ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px' 34 | } 35 | ripple.style.backgroundColor = opts.bgc 36 | ripple.className = 'e-ripple z-active' 37 | return false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/ImageCropper/utils/mimes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'jpg': 'image/jpeg', 3 | 'png': 'image/png', 4 | 'gif': 'image/gif', 5 | 'svg': 'image/svg+xml', 6 | 'psd': 'image/photoshop' 7 | } 8 | -------------------------------------------------------------------------------- /src/components/JsonEditor/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 56 | 57 | 73 | -------------------------------------------------------------------------------- /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/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 50 | 51 | 61 | -------------------------------------------------------------------------------- /src/components/SizeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ 9 | item.label }} 10 | 11 | 12 | 13 | 14 | 15 | 58 | -------------------------------------------------------------------------------- /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/index.js: -------------------------------------------------------------------------------- 1 | import drag from './drag' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-drag-dialog', drag) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-drag-dialog'] = drag 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | drag.install = install 13 | export default drag 14 | -------------------------------------------------------------------------------- /src/directive/el-table/adaptive.js: -------------------------------------------------------------------------------- 1 | import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event' 2 | 3 | /** 4 | * How to use 5 | * ... 6 | * el-table height is must be set 7 | * bottomOffset: 30(default) // The height of the table from the bottom of the page. 8 | */ 9 | 10 | const doResize = (el, binding, vnode) => { 11 | const { componentInstance: $table } = vnode 12 | 13 | const { value } = binding 14 | 15 | if (!$table.height) { 16 | throw new Error(`el-$table must set the height. Such as height='100px'`) 17 | } 18 | const bottomOffset = (value && value.bottomOffset) || 30 19 | 20 | if (!$table) return 21 | 22 | const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset 23 | $table.layout.setHeight(height) 24 | $table.doLayout() 25 | } 26 | 27 | export default { 28 | bind(el, binding, vnode) { 29 | el.resizeListener = () => { 30 | doResize(el, binding, vnode) 31 | } 32 | // parameter 1 is must be "Element" type 33 | addResizeListener(window.document.body, el.resizeListener) 34 | }, 35 | inserted(el, binding, vnode) { 36 | doResize(el, binding, vnode) 37 | }, 38 | unbind(el) { 39 | removeResizeListener(window.document.body, el.resizeListener) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/directive/el-table/index.js: -------------------------------------------------------------------------------- 1 | import adaptive from './adaptive' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('el-height-adaptive-table', adaptive) 5 | } 6 | 7 | if (window.Vue) { 8 | window['el-height-adaptive-table'] = adaptive 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | adaptive.install = install 13 | export default adaptive 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 | if (window.Vue) { 8 | window['permission'] = permission 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | permission.install = install 13 | export default permission 14 | -------------------------------------------------------------------------------- /src/directive/permission/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | export default { 4 | inserted(el, binding, vnode) { 5 | const { value } = binding 6 | const roles = store.getters && store.getters.roles 7 | 8 | if (value && value instanceof Array && 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 | } else { 19 | throw new Error(`need roles! Like v-permission="['admin','editor']"`) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /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/filters/index.js: -------------------------------------------------------------------------------- 1 | // import parseTime, formatTime and set to filter 2 | export { parseTime, formatTime } from '@/utils' 3 | 4 | /** 5 | * Show plural label if time is plural number 6 | * @param {number} time 7 | * @param {string} label 8 | * @return {string} 9 | */ 10 | function pluralize(time, label) { 11 | if (time === 1) { 12 | return time + label 13 | } 14 | return time + label + 's' 15 | } 16 | 17 | /** 18 | * @param {number} time 19 | */ 20 | export function timeAgo(time) { 21 | const between = Date.now() / 1000 - Number(time) 22 | if (between < 3600) { 23 | return pluralize(~~(between / 60), ' minute') 24 | } else if (between < 86400) { 25 | return pluralize(~~(between / 3600), ' hour') 26 | } else { 27 | return pluralize(~~(between / 86400), ' day') 28 | } 29 | } 30 | 31 | /** 32 | * Number formatting 33 | * like 10000 => 10k 34 | * @param {number} num 35 | * @param {number} digits 36 | */ 37 | export function numberFormatter(num, digits) { 38 | const si = [ 39 | { value: 1E18, symbol: 'E' }, 40 | { value: 1E15, symbol: 'P' }, 41 | { value: 1E12, symbol: 'T' }, 42 | { value: 1E9, symbol: 'G' }, 43 | { value: 1E6, symbol: 'M' }, 44 | { value: 1E3, symbol: 'k' } 45 | ] 46 | for (let i = 0; i < si.length; i++) { 47 | if (num >= si[i].value) { 48 | return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol 49 | } 50 | } 51 | return num.toString() 52 | } 53 | 54 | /** 55 | * 10000 => "10,000" 56 | * @param {number} num 57 | */ 58 | export function toThousandFilter(num) { 59 | return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ',')) 60 | } 61 | 62 | /** 63 | * Upper case first char 64 | * @param {String} string 65 | */ 66 | export function uppercaseFirst(string) { 67 | return string.charAt(0).toUpperCase() + string.slice(1) 68 | } 69 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /src/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/component.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/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/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 | 24 | 25 | 49 | 50 | 58 | -------------------------------------------------------------------------------- /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 | 30 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 37 | -------------------------------------------------------------------------------- /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 | 16 | 17 | 18 | 19 | 20 | 21 | 55 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as AppMain } from './AppMain' 2 | export { default as Navbar } from './Navbar' 3 | export { default as Settings } from './Settings' 4 | export { default as Sidebar } from './Sidebar/index.vue' 5 | export { default as TagsView } from './TagsView/index.vue' 6 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import Cookies from 'js-cookie' 4 | 5 | import 'normalize.css/normalize.css' // a modern alternative to CSS resets 6 | 7 | import Element from 'element-ui' 8 | import './styles/element-variables.scss' 9 | 10 | import '@/styles/index.scss' // global css 11 | 12 | import App from './App' 13 | import store from './store' 14 | import router from './router' 15 | 16 | import './icons' // icon 17 | import './permission' // permission control 18 | import './utils/error-log' // error log 19 | 20 | import * as filters from './filters' // global filters 21 | 22 | /** 23 | * If you don't want to use mock-server 24 | * you want to use MockJs for mock api 25 | * you can execute: mockXHR() 26 | * 27 | * Currently MockJs will be used in the production environment, 28 | * please remove it before going online! ! ! 29 | */ 30 | // import { mockXHR } from '../mock' 31 | // if (process.env.NODE_ENV === 'production') { 32 | // mockXHR() 33 | // } 34 | 35 | Vue.use(Element, { 36 | size: Cookies.get('size') || 'medium' // set element-ui default size 37 | }) 38 | 39 | // register global utility filters 40 | Object.keys(filters).forEach(key => { 41 | Vue.filter(key, filters[key]) 42 | }) 43 | 44 | Vue.config.productionTip = false 45 | 46 | new Vue({ 47 | el: '#app', 48 | router, 49 | store, 50 | render: h => h(App) 51 | }) 52 | -------------------------------------------------------------------------------- /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: 'Connect The World', 3 | 4 | /** 5 | * @type {boolean} true | false 6 | * @description Whether show the settings right-panel 7 | */ 8 | showSettings: false, 9 | 10 | /** 11 | * @type {boolean} true | false 12 | * @description Whether need tagsView 13 | */ 14 | tagsView: true, 15 | 16 | /** 17 | * @type {boolean} true | false 18 | * @description Whether fix the header 19 | */ 20 | fixedHeader: false, 21 | 22 | /** 23 | * @type {boolean} true | false 24 | * @description Whether show the logo in sidebar 25 | */ 26 | sidebarLogo: false, 27 | 28 | /** 29 | * @type {string | array} 'production' | ['production', 'development'] 30 | * @description Need show err logs component. 31 | * The default is only used in the production env 32 | * If you want to also use it in dev, you can pass ['production', 'development'] 33 | */ 34 | errorLog: 'production' 35 | } 36 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | size: state => state.app.size, 4 | device: state => state.app.device, 5 | visitedViews: state => state.tagsView.visitedViews, 6 | cachedViews: state => state.tagsView.cachedViews, 7 | token: state => state.user.token, 8 | avatar: state => state.user.avatar, 9 | name: state => state.user.name, 10 | introduction: state => state.user.introduction, 11 | roles: state => state.user.roles, 12 | permission_routes: state => state.permission.routes, 13 | errorLogs: state => state.errorLog.logs 14 | } 15 | export default getters 16 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | 5 | Vue.use(Vuex) 6 | 7 | // https://webpack.js.org/guides/dependency-management/#requirecontext 8 | const modulesFiles = require.context('./modules', true, /\.js$/) 9 | 10 | // you do not need `import app from './modules/app'` 11 | // it will auto require all vuex module from modules file 12 | const modules = modulesFiles.keys().reduce((modules, modulePath) => { 13 | // set './app.js' => 'app' 14 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 15 | const value = modulesFiles(modulePath) 16 | modules[moduleName] = value.default 17 | return modules 18 | }, {}) 19 | 20 | const store = new Vuex.Store({ 21 | modules, 22 | getters 23 | }) 24 | 25 | export default store 26 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop', 9 | size: Cookies.get('size') || 'medium' 10 | } 11 | 12 | const mutations = { 13 | TOGGLE_SIDEBAR: state => { 14 | state.sidebar.opened = !state.sidebar.opened 15 | state.sidebar.withoutAnimation = false 16 | if (state.sidebar.opened) { 17 | Cookies.set('sidebarStatus', 1) 18 | } else { 19 | Cookies.set('sidebarStatus', 0) 20 | } 21 | }, 22 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 23 | Cookies.set('sidebarStatus', 0) 24 | state.sidebar.opened = false 25 | state.sidebar.withoutAnimation = withoutAnimation 26 | }, 27 | TOGGLE_DEVICE: (state, device) => { 28 | state.device = device 29 | }, 30 | SET_SIZE: (state, size) => { 31 | state.size = size 32 | Cookies.set('size', size) 33 | } 34 | } 35 | 36 | const actions = { 37 | toggleSideBar({ commit }) { 38 | commit('TOGGLE_SIDEBAR') 39 | }, 40 | closeSideBar({ commit }, { withoutAnimation }) { 41 | commit('CLOSE_SIDEBAR', withoutAnimation) 42 | }, 43 | toggleDevice({ commit }, device) { 44 | commit('TOGGLE_DEVICE', device) 45 | }, 46 | setSize({ commit }, size) { 47 | commit('SET_SIZE', size) 48 | } 49 | } 50 | 51 | export default { 52 | namespaced: true, 53 | state, 54 | mutations, 55 | actions 56 | } 57 | -------------------------------------------------------------------------------- /src/store/modules/errorLog.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | logs: [] 3 | } 4 | 5 | const mutations = { 6 | ADD_ERROR_LOG: (state, log) => { 7 | state.logs.push(log) 8 | }, 9 | CLEAR_ERROR_LOG: (state) => { 10 | state.logs.splice(0) 11 | } 12 | } 13 | 14 | const actions = { 15 | addErrorLog({ commit }, log) { 16 | commit('ADD_ERROR_LOG', log) 17 | }, 18 | clearErrorLog({ commit }) { 19 | commit('CLEAR_ERROR_LOG') 20 | } 21 | } 22 | 23 | export default { 24 | namespaced: true, 25 | state, 26 | mutations, 27 | actions 28 | } 29 | -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | import { asyncRoutes, constantRoutes } from '@/router' 2 | 3 | /** 4 | * Use meta.role to determine if the current user has permission 5 | * @param roles 6 | * @param route 7 | */ 8 | function hasPermission(roles, route) { 9 | if (route.meta && route.meta.roles) { 10 | return roles.some(role => route.meta.roles.includes(role)) 11 | } else { 12 | return true 13 | } 14 | } 15 | 16 | /** 17 | * Filter asynchronous routing tables by recursion 18 | * @param routes asyncRoutes 19 | * @param roles 20 | */ 21 | export function filterAsyncRoutes(routes, roles) { 22 | const res = [] 23 | 24 | routes.forEach(route => { 25 | const tmp = { ...route } 26 | if (hasPermission(roles, tmp)) { 27 | if (tmp.children) { 28 | tmp.children = filterAsyncRoutes(tmp.children, roles) 29 | } 30 | res.push(tmp) 31 | } 32 | }) 33 | 34 | return res 35 | } 36 | 37 | const state = { 38 | routes: [], 39 | addRoutes: [] 40 | } 41 | 42 | const mutations = { 43 | SET_ROUTES: (state, routes) => { 44 | state.addRoutes = routes 45 | state.routes = constantRoutes.concat(routes) 46 | } 47 | } 48 | 49 | const actions = { 50 | generateRoutes({ commit }, roles) { 51 | return new Promise(resolve => { 52 | let accessedRoutes 53 | if (roles.includes('admin')) { 54 | accessedRoutes = asyncRoutes || [] 55 | } else { 56 | accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) 57 | } 58 | commit('SET_ROUTES', accessedRoutes) 59 | resolve(accessedRoutes) 60 | }) 61 | } 62 | } 63 | 64 | export default { 65 | namespaced: true, 66 | state, 67 | mutations, 68 | actions 69 | } 70 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import variables from '@/styles/element-variables.scss' 2 | import defaultSettings from '@/settings' 3 | 4 | const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings 5 | 6 | const state = { 7 | theme: variables.theme, 8 | showSettings: showSettings, 9 | tagsView: tagsView, 10 | fixedHeader: fixedHeader, 11 | sidebarLogo: sidebarLogo 12 | } 13 | 14 | const mutations = { 15 | CHANGE_SETTING: (state, { key, value }) => { 16 | if (state.hasOwnProperty(key)) { 17 | state[key] = value 18 | } 19 | } 20 | } 21 | 22 | const actions = { 23 | changeSetting({ commit }, data) { 24 | commit('CHANGE_SETTING', data) 25 | } 26 | } 27 | 28 | export default { 29 | namespaced: true, 30 | state, 31 | mutations, 32 | actions 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/styles/btn.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | @mixin colorBtn($color) { 4 | background: $color; 5 | 6 | &:hover { 7 | color: $color; 8 | 9 | &:before, 10 | &:after { 11 | background: $color; 12 | } 13 | } 14 | } 15 | 16 | .blue-btn { 17 | @include colorBtn($blue) 18 | } 19 | 20 | .light-blue-btn { 21 | @include colorBtn($light-blue) 22 | } 23 | 24 | .red-btn { 25 | @include colorBtn($red) 26 | } 27 | 28 | .pink-btn { 29 | @include colorBtn($pink) 30 | } 31 | 32 | .green-btn { 33 | @include colorBtn($green) 34 | } 35 | 36 | .tiffany-btn { 37 | @include colorBtn($tiffany) 38 | } 39 | 40 | .yellow-btn { 41 | @include colorBtn($yellow) 42 | } 43 | 44 | .pan-btn { 45 | font-size: 14px; 46 | color: #fff; 47 | padding: 14px 36px; 48 | border-radius: 8px; 49 | border: none; 50 | outline: none; 51 | transition: 600ms ease all; 52 | position: relative; 53 | display: inline-block; 54 | 55 | &:hover { 56 | background: #fff; 57 | 58 | &:before, 59 | &:after { 60 | width: 100%; 61 | transition: 600ms ease all; 62 | } 63 | } 64 | 65 | &:before, 66 | &:after { 67 | content: ''; 68 | position: absolute; 69 | top: 0; 70 | right: 0; 71 | height: 2px; 72 | width: 0; 73 | transition: 400ms ease all; 74 | } 75 | 76 | &::after { 77 | right: inherit; 78 | top: inherit; 79 | left: 0; 80 | bottom: 0; 81 | } 82 | } 83 | 84 | .custom-button { 85 | display: inline-block; 86 | line-height: 1; 87 | white-space: nowrap; 88 | cursor: pointer; 89 | background: #fff; 90 | color: #fff; 91 | -webkit-appearance: none; 92 | text-align: center; 93 | box-sizing: border-box; 94 | outline: 0; 95 | margin: 0; 96 | padding: 10px 15px; 97 | font-size: 14px; 98 | border-radius: 4px; 99 | } 100 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | .cell { 19 | .el-tag { 20 | margin-right: 0px; 21 | } 22 | } 23 | 24 | .small-padding { 25 | .cell { 26 | padding-left: 5px; 27 | padding-right: 5px; 28 | } 29 | } 30 | 31 | .fixed-width { 32 | .el-button--mini { 33 | padding: 7px 10px; 34 | width: 60px; 35 | } 36 | } 37 | 38 | .status-col { 39 | .cell { 40 | padding: 0 10px; 41 | text-align: center; 42 | 43 | .el-tag { 44 | margin-right: 0px; 45 | } 46 | } 47 | } 48 | 49 | // to fixed https://github.com/ElemeFE/element/issues/2461 50 | .el-dialog { 51 | transform: none; 52 | left: 0; 53 | position: relative; 54 | margin: 0 auto; 55 | } 56 | 57 | // refine element ui upload 58 | .upload-container { 59 | .el-upload { 60 | width: 100%; 61 | 62 | .el-upload-dragger { 63 | width: 100%; 64 | height: 200px; 65 | } 66 | } 67 | } 68 | 69 | // dropdown 70 | .el-dropdown-menu { 71 | a { 72 | display: block 73 | } 74 | } 75 | 76 | // fix date-picker ui bug in filter-item 77 | .el-range-editor.el-input__inner { 78 | display: inline-flex !important; 79 | } 80 | 81 | // to fix el-date-picker css style 82 | .el-range-separator { 83 | box-sizing: content-box; 84 | } 85 | -------------------------------------------------------------------------------- /src/styles/element-variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * I think element-ui's default theme color is too light for long-term use. 3 | * So I modified the default color and you can modify it to your liking. 4 | **/ 5 | 6 | /* theme color */ 7 | $--color-primary: #1890ff; 8 | $--color-success: #13ce66; 9 | $--color-warning: #FFBA00; 10 | $--color-danger: #ff4949; 11 | // $--color-info: #1E1E1E; 12 | 13 | $--button-font-weight: 400; 14 | 15 | // $--color-text-regular: #1f2d3d; 16 | 17 | $--border-color-light: #dfe4ed; 18 | $--border-color-lighter: #e6ebf5; 19 | 20 | $--table-border:1px solid#dfe6ec; 21 | 22 | /* icon font path, required */ 23 | $--font-path: '~element-ui/lib/theme-chalk/fonts'; 24 | 25 | @import "~element-ui/packages/theme-chalk/src/index"; 26 | 27 | // the :export directive is the magic sauce for webpack 28 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 29 | :export { 30 | theme: $--color-primary; 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | @mixin pct($pct) { 31 | width: #{$pct}; 32 | position: relative; 33 | margin: 0 auto; 34 | } 35 | 36 | @mixin triangle($width, $height, $color, $direction) { 37 | $width: $width/2; 38 | $color-border-style: $height solid $color; 39 | $transparent-border-style: $width solid transparent; 40 | height: 0; 41 | width: 0; 42 | 43 | @if $direction==up { 44 | border-bottom: $color-border-style; 45 | border-left: $transparent-border-style; 46 | border-right: $transparent-border-style; 47 | } 48 | 49 | @else if $direction==right { 50 | border-left: $color-border-style; 51 | border-top: $transparent-border-style; 52 | border-bottom: $transparent-border-style; 53 | } 54 | 55 | @else if $direction==down { 56 | border-top: $color-border-style; 57 | border-left: $transparent-border-style; 58 | border-right: $transparent-border-style; 59 | } 60 | 61 | @else if $direction==left { 62 | border-right: $color-border-style; 63 | border-top: $transparent-border-style; 64 | border-bottom: $transparent-border-style; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // base color 2 | $blue:#324157; 3 | $light-blue:#3A71A8; 4 | $red:#C03639; 5 | $pink: #E65D6E; 6 | $green: #30B08F; 7 | $tiffany: #4AB7BD; 8 | $yellow:#FEC171; 9 | $panGreen: #30B08F; 10 | 11 | // sidebar 12 | $menuText:#bfcbd9; 13 | $menuActiveText:#409EFF; 14 | $subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951 15 | 16 | $menuBg:#304156; 17 | $menuHover:#263445; 18 | 19 | $subMenuBg:#1f2d3d; 20 | $subMenuHover:#001528; 21 | 22 | $sideBarWidth: 210px; 23 | 24 | // the :export directive is the magic sauce for webpack 25 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 26 | :export { 27 | menuText: $menuText; 28 | menuActiveText: $menuActiveText; 29 | subMenuActiveText: $subMenuActiveText; 30 | menuBg: $menuBg; 31 | menuHover: $menuHover; 32 | subMenuBg: $subMenuBg; 33 | subMenuHover: $subMenuHover; 34 | sideBarWidth: $sideBarWidth; 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'auth' 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 | 4 | function clipboardSuccess() { 5 | Vue.prototype.$message({ 6 | message: '复制成功', 7 | type: 'success', 8 | duration: 1500 9 | }) 10 | } 11 | 12 | function clipboardError() { 13 | Vue.prototype.$message({ 14 | message: '复制失败', 15 | type: 'error' 16 | }) 17 | } 18 | 19 | export default function handleClipboard(text, event) { 20 | const clipboard = new Clipboard(event.target, { 21 | text: () => text 22 | }) 23 | clipboard.on('success', () => { 24 | clipboardSuccess() 25 | clipboard.destroy() 26 | }) 27 | clipboard.on('error', () => { 28 | clipboardError() 29 | clipboard.destroy() 30 | }) 31 | clipboard.onClick(event) 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/error-log.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from '@/store' 3 | import { isString, isArray } from '@/utils/validate' 4 | import settings from '@/settings' 5 | 6 | // you can set in settings.js 7 | // errorLog:'production' | ['production', 'development'] 8 | const { errorLog: needErrorLog } = settings 9 | 10 | function checkNeed() { 11 | const env = process.env.NODE_ENV 12 | if (isString(needErrorLog)) { 13 | return env === needErrorLog 14 | } 15 | if (isArray(needErrorLog)) { 16 | return needErrorLog.includes(env) 17 | } 18 | return false 19 | } 20 | 21 | if (checkNeed()) { 22 | Vue.config.errorHandler = function(err, vm, info, a) { 23 | // Don't ask me why I use Vue.nextTick, it just a hack. 24 | // detail see https://forum.vuejs.org/t/dispatch-in-vue-config-errorhandler-has-some-problem/23500 25 | Vue.nextTick(() => { 26 | store.dispatch('errorLog/addErrorLog', { 27 | err, 28 | vm, 29 | info, 30 | url: window.location.href 31 | }) 32 | console.error(err, info) 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || 'Vue Element Admin' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/open-window.js: -------------------------------------------------------------------------------- 1 | /** 2 | *Created by PanJiaChen on 16/11/29. 3 | * @param {Sting} url 4 | * @param {Sting} title 5 | * @param {Number} w 6 | * @param {Number} h 7 | */ 8 | export default function openWindow(url, title, w, h) { 9 | // Fixes dual-screen position Most browsers Firefox 10 | const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left 11 | const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top 12 | 13 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width 14 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height 15 | 16 | const left = ((width / 2) - (w / 2)) + dualScreenLeft 17 | const top = ((height / 2) - (h / 2)) + dualScreenTop 18 | const newWindow = window.open(url, title, 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left) 19 | 20 | // Puts focus on the newWindow 21 | if (window.focus) { 22 | newWindow.focus() 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/utils/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | /** 4 | * @param {Array} value 5 | * @returns {Boolean} 6 | * @example see @/views/permission/directive.vue 7 | */ 8 | export default function checkPermission(value) { 9 | if (value && value instanceof Array && value.length > 0) { 10 | const roles = store.getters && store.getters.roles 11 | const permissionRoles = value 12 | 13 | const hasPermission = roles.some(role => { 14 | return permissionRoles.includes(role) 15 | }) 16 | 17 | if (!hasPermission) { 18 | return false 19 | } 20 | return true 21 | } else { 22 | console.error(`need roles! Like v-permission="['admin','editor']"`) 23 | return false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/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/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/adminUser/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/adminUser/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/adminUser/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/adminUser/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/adminUser/create.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /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/avatar-upload.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | Change Avatar 12 | 13 | 14 | 24 | 25 | 26 | 27 | 53 | 54 | 61 | 62 | -------------------------------------------------------------------------------- /src/views/components-demo/dnd-list.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 39 | 40 | -------------------------------------------------------------------------------- /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 | 62 | -------------------------------------------------------------------------------- /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/dropzone.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 31 | 32 | -------------------------------------------------------------------------------- /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/split-pane.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 39 | 40 | 68 | -------------------------------------------------------------------------------- /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/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 | orderNoFilter }} 6 | 7 | 8 | 9 | 10 | ¥{{ scope.row.price | toThousandFilter }} 11 | 12 | 13 | 14 | 15 | 16 | {{ row.status }} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 56 | -------------------------------------------------------------------------------- /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 | beforeDestroy() { 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/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 | 4 | 5 | Please click the bug icon in the upper right corner 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 27 | 28 | 33 | -------------------------------------------------------------------------------- /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/element-icons.js: -------------------------------------------------------------------------------- 1 | const elementIcons = [ 2 | 'info', 3 | 'error', 4 | 'success', 5 | 'warning', 6 | 'question', 7 | 'back', 8 | 'arrow-left', 9 | 'arrow-down', 10 | 'arrow-right', 11 | 'arrow-up', 12 | 'caret-left', 13 | 'caret-bottom', 14 | 'caret-top', 15 | 'caret-right', 16 | 'd-arrow-left', 17 | 'd-arrow-right', 18 | 'minus', 19 | 'plus', 20 | 'remove', 21 | 'circle-plus', 22 | 'remove-outline', 23 | 'circle-plus-outline', 24 | 'close', 25 | 'check', 26 | 'circle-close', 27 | 'circle-check', 28 | 'circle-close-outline', 29 | 'circle-check-outline', 30 | 'zoom-out', 31 | 'zoom-in', 32 | 'd-caret', 33 | 'sort', 34 | 'sort-down', 35 | 'sort-up', 36 | 'tickets', 37 | 'document', 38 | 'goods', 39 | 'sold-out', 40 | 'news', 41 | 'message', 42 | 'date', 43 | 'printer', 44 | 'time', 45 | 'bell', 46 | 'mobile-phone', 47 | 'service', 48 | 'view', 49 | 'menu', 50 | 'more', 51 | 'more-outline', 52 | 'star-on', 53 | 'star-off', 54 | 'location', 55 | 'location-outline', 56 | 'phone', 57 | 'phone-outline', 58 | 'picture', 59 | 'picture-outline', 60 | 'delete', 61 | 'search', 62 | 'edit', 63 | 'edit-outline', 64 | 'rank', 65 | 'refresh', 66 | 'share', 67 | 'setting', 68 | 'upload', 69 | 'upload2', 70 | 'download', 71 | 'loading' 72 | ] 73 | 74 | export default elementIcons 75 | -------------------------------------------------------------------------------- /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/login/auth-redirect.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/views/nested/menu1/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-1/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/notice/create.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/notice/edit.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /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 | 5 | 6 | 7 | 8 | 9 | 10 | Update 11 | 12 | 13 | 14 | 15 | 39 | -------------------------------------------------------------------------------- /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 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 72 | -------------------------------------------------------------------------------- /src/views/qiniu/upload.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 将文件拖到此处,或点击上传 6 | 7 | 8 | 9 | 10 | 42 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/views/server/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/server/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/server/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/server/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/server/create.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/server/edit.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /src/views/serverConfig/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/serverConfig/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/serverConfig/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/serverConfig/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/serverConfig/create.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/serverConfig/edit.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | -------------------------------------------------------------------------------- /src/views/tab/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | mounted times :{{ createdTimes }} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 52 | 53 | 58 | -------------------------------------------------------------------------------- /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 | 62 | 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | describe('Utils:formatTime', () => { 3 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 4 | const retrofit = 5 * 1000 5 | 6 | it('ten digits timestamp', () => { 7 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 8 | }) 9 | it('test now', () => { 10 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 11 | }) 12 | it('less two minute', () => { 13 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 14 | }) 15 | it('less two hour', () => { 16 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 17 | }) 18 | it('less one day', () => { 19 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 20 | }) 21 | it('more than one day', () => { 22 | expect(formatTime(d)).toBe('7月13日17时54分') 23 | }) 24 | it('format', () => { 25 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 26 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 27 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/unit/utils/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 | 9 | it('timestamp string', () => { 10 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') 11 | }) 12 | 13 | it('ten digits timestamp', () => { 14 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 15 | }) 16 | it('new Date', () => { 17 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 18 | }) 19 | it('format', () => { 20 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 21 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 22 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 23 | }) 24 | it('get the day of the week', () => { 25 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 26 | }) 27 | it('get the day of the week', () => { 28 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 29 | }) 30 | it('empty argument', () => { 31 | expect(parseTime()).toBeNull() 32 | }) 33 | 34 | it('null', () => { 35 | expect(parseTime(null)).toBeNull() 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, validURL, validLowerCase, validUpperCase, validAlphabets } from '@/utils/validate.js' 2 | describe('Utils:validate', () => { 3 | it('validUsername', () => { 4 | expect(validUsername('admin')).toBe(true) 5 | expect(validUsername('editor')).toBe(true) 6 | expect(validUsername('xxxx')).toBe(false) 7 | }) 8 | it('validURL', () => { 9 | expect(validURL('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 10 | expect(validURL('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(validURL('github.com/PanJiaChen/vue-element-admin')).toBe(false) 12 | }) 13 | it('validLowerCase', () => { 14 | expect(validLowerCase('abc')).toBe(true) 15 | expect(validLowerCase('Abc')).toBe(false) 16 | expect(validLowerCase('123abc')).toBe(false) 17 | }) 18 | it('validUpperCase', () => { 19 | expect(validUpperCase('ABC')).toBe(true) 20 | expect(validUpperCase('Abc')).toBe(false) 21 | expect(validUpperCase('123ABC')).toBe(false) 22 | }) 23 | it('validAlphabets', () => { 24 | expect(validAlphabets('ABC')).toBe(true) 25 | expect(validAlphabets('Abc')).toBe(true) 26 | expect(validAlphabets('123aBC')).toBe(false) 27 | }) 28 | }) 29 | --------------------------------------------------------------------------------
{{ item.content }}