├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── babel.config.js ├── build └── index.js ├── jest.config.js ├── jsconfig.json ├── mock ├── article-label.js ├── article.js ├── comment.js ├── file.js ├── index.js ├── link.js ├── mock-server.js ├── other.js ├── role │ ├── index.js │ └── routes.js ├── sys.js ├── user.js └── utils.js ├── package.json ├── plopfile.js ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ ├── article-label.js │ ├── article.js │ ├── comment.js │ ├── file.js │ ├── link.js │ ├── other.js │ ├── remote-search.js │ ├── role.js │ ├── sys.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 │ ├── Kanban │ │ └── index.vue │ ├── MDinput │ │ └── index.vue │ ├── MarkdownEditor │ │ └── 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 ├── 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 ├── 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 └── views │ ├── 404.vue │ ├── article │ ├── ManageArticle.vue │ ├── components │ │ ├── ArticleDetail.vue │ │ ├── Dropdown │ │ │ ├── Comment.vue │ │ │ ├── PublishType.vue │ │ │ └── index.js │ │ └── Warning.vue │ ├── create.vue │ ├── edit.vue │ ├── label.vue │ └── list.vue │ ├── comment │ └── ManageComment.vue │ ├── dashboard │ ├── components │ │ ├── BarChart.vue │ │ ├── CommentTable.vue │ │ ├── LineChart.vue │ │ ├── PanelGroup.vue │ │ ├── PieChart.vue │ │ ├── TodoList │ │ │ ├── Todo.vue │ │ │ ├── index.scss │ │ │ └── index.vue │ │ └── mixins │ │ │ └── resize.js │ └── index.vue │ ├── links │ └── FriendLink.vue │ ├── login │ └── index.vue │ ├── profile │ ├── about.vue │ ├── base.vue │ └── components │ │ ├── Account.vue │ │ └── UserCard.vue │ └── setting │ ├── storage.vue │ ├── sys.vue │ └── ui.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 = '/dev-api' 6 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = 'http://127.0.0.1:9000/api' 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM httpd:alpine 2 | 3 | LABEL "author"="WindSnowLi" 4 | LABEL "version"="1.0.1" 5 | LABEL "email"="windsnowli@163.com" 6 | # 配置环境变量支持中文 7 | ENV LANG="en_US.UTF-8" 8 | 9 | COPY ./dist /admin 10 | WORKDIR /admin 11 | 12 | RUN rm -rf /usr/local/apache2/htdocs/* 13 | RUN mv /admin/* /usr/local/apache2/htdocs/ 14 | # 后台端口 15 | EXPOSE 80 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 WindSnowLi 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 | -------------------------------------------------------------------------------- /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/article-label.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const List = [] 3 | const count = 100 4 | const coverPic = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3' 5 | 6 | for (let i = 0; i < count; i++) { 7 | List.push(Mock.mock({ 8 | id: '@increment', 9 | name: '@title(5, 10)', 10 | coverPic, 11 | describe: '这是其中一个标签的描述' 12 | })) 13 | } 14 | 15 | module.exports = [ 16 | // 文章可选标签 17 | { 18 | url: '/articleLabel/labels', 19 | type: 'post', 20 | dataType: 'json', 21 | response: _ => { 22 | return { 23 | code: 20000, 24 | message: '请求成功', 25 | data: [{ 26 | value: 'C++', 27 | label: 'C++' 28 | }, { 29 | value: 'Java', 30 | label: 'Java' 31 | }, { 32 | value: 'Python', 33 | label: 'Python' 34 | }] 35 | } 36 | } 37 | }, 38 | // 标签对象列表 39 | { 40 | url: '/articleLabel/labelList', 41 | type: 'post', 42 | dataType: 'json', 43 | response: _ => { 44 | return { 45 | code: 20000, 46 | message: '请求成功', 47 | data: List 48 | } 49 | } 50 | }, 51 | // 标签对象列表 52 | { 53 | url: '/articleLabel/setLabel', 54 | type: 'post', 55 | dataType: 'json', 56 | response: _ => { 57 | return { 58 | code: 20000, 59 | message: '请求成功' 60 | } 61 | } 62 | } 63 | ] 64 | -------------------------------------------------------------------------------- /mock/comment.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const List = [] 3 | const count = 20 4 | for (let i = 0; i < count; i++) { 5 | List.push(Mock.mock({ 6 | id: '@increment', 7 | content: '@guid()', 8 | time: +Mock.Random.date('T'), 9 | fromUser: { 10 | id: '@increment', 11 | nickname: '@name()' 12 | }, 13 | toUser: { 14 | id: '@increment', 15 | nickname: '@name()' 16 | }, 17 | target: { 18 | title: '@title(5, 10)' 19 | } 20 | })) 21 | } 22 | 23 | module.exports = [ 24 | // 获取评论列表 25 | { 26 | url: '/comment/getCommentList', 27 | type: 'post', 28 | dataType: 'json', 29 | response: _ => { 30 | return { 31 | code: 20000, 32 | data: { 33 | total: 20, 34 | items: List 35 | } 36 | } 37 | } 38 | }, 39 | // 修改评论状态 40 | { 41 | url: '/comment/setCommentStatus', 42 | type: 'post', 43 | dataType: 'json', 44 | response: _ => { 45 | return { 46 | code: 20000, 47 | message: 'OK' 48 | } 49 | } 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /mock/file.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | // 获取头像上传的Url,含上传后的文件名 3 | // {url: 上传签名Url, GetUrl: Get请求Url, method: 上传方式} 4 | { 5 | url: '/file/getUploadAvatarUrl', 6 | type: 'post', 7 | response: _ => { 8 | return { 9 | code: 20000, 10 | data: { 11 | urlParams: { 12 | key: 'cece089d-dd3b-11eb-a1d2-1f59eeaa999a', 13 | policy: '', 14 | success_action_status: '200', 15 | OSSAccessKeyId: '', 16 | signature: '' 17 | }, 18 | GetUrl: 'https://wpimg.wallstcn.com/577965b9-bb9e-4e02-9f0c-095b41417191', 19 | host: 'http://localhost:9527/dev-api/file/uploadData' 20 | } 21 | } 22 | } 23 | }, 24 | // 获取上传文章封面Url 25 | { 26 | url: '/file/getUploadArticleCoverImageUrl', 27 | type: 'post', 28 | response: _ => { 29 | return { 30 | code: 20000, 31 | data: { 32 | urlParams: { 33 | key: 'cece089d-dd3b-11eb-a1d2-1f59eeaa999a', 34 | policy: '', 35 | success_action_status: '200', 36 | OSSAccessKeyId: '', 37 | signature: '' 38 | }, 39 | GetUrl: 'https://wpimg.wallstcn.com/577965b9-bb9e-4e02-9f0c-095b41417191', 40 | host: 'http://localhost:9527/dev-api/file/uploadData' 41 | } 42 | } 43 | } 44 | }, 45 | // 获取上传文章内容图片的Url 46 | { 47 | url: '/file/getUploadArticleImageUrl', 48 | type: 'post', 49 | response: _ => { 50 | return { 51 | code: 20000, 52 | data: { 53 | urlParams: { 54 | key: 'cece089d-dd3b-11eb-a1d2-1f59eeaa999a', 55 | policy: '', 56 | success_action_status: '200', 57 | OSSAccessKeyId: '', 58 | signature: '' 59 | }, 60 | GetUrl: 'https://wpimg.wallstcn.com/577965b9-bb9e-4e02-9f0c-095b41417191', 61 | host: 'http://localhost:9527/dev-api/file/uploadData' 62 | } 63 | } 64 | } 65 | }, 66 | // 上传文件 67 | { 68 | url: '/file/uploadData', 69 | type: 'post', 70 | response: _ => { 71 | return { 72 | code: 20000, 73 | data: 'OK' 74 | } 75 | } 76 | }, 77 | // 删除文件 78 | { 79 | url: '/file/deleteObject', 80 | type: 'post', 81 | response: _ => { 82 | return { 83 | code: 20000, 84 | data: 'OK' 85 | } 86 | } 87 | } 88 | ] 89 | -------------------------------------------------------------------------------- /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 sysConfig = require('./sys') 8 | const fileConfig = require('./file') 9 | const link = require('./link') 10 | const comment = require('./comment') 11 | const other = require('./other') 12 | const articleLabel = require('./article-label') 13 | const mocks = [ 14 | ...user, 15 | ...role, 16 | ...article, 17 | ...sysConfig, 18 | ...fileConfig, 19 | ...link, 20 | ...comment, 21 | ...other, 22 | ...articleLabel 23 | ] 24 | 25 | // for front mock 26 | // please use it cautiously, it will redefine XMLHttpRequest, 27 | // which will cause many of your third-party libraries to be invalidated(like progress event). 28 | function mockXHR() { 29 | // mock patch 30 | // https://github.com/nuysoft/Mock/issues/300 31 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 32 | Mock.XHR.prototype.send = function () { 33 | if (this.custom.xhr) { 34 | this.custom.xhr.withCredentials = this.withCredentials || false 35 | 36 | if (this.responseType) { 37 | this.custom.xhr.responseType = this.responseType 38 | } 39 | } 40 | this.proxy_send(...arguments) 41 | } 42 | 43 | function XHR2ExpressReqWrap(respond) { 44 | return function (options) { 45 | let result = null 46 | if (respond instanceof Function) { 47 | const {body, type, url} = options 48 | // https://expressjs.com/en/4x/api.html#req 49 | result = respond({ 50 | method: type, 51 | body: JSON.parse(body), 52 | query: param2Obj(url) 53 | }) 54 | } else { 55 | result = respond 56 | } 57 | return Mock.mock(result) 58 | } 59 | } 60 | 61 | for (const i of mocks) { 62 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) 63 | } 64 | } 65 | 66 | module.exports = { 67 | mocks, 68 | mockXHR 69 | } 70 | -------------------------------------------------------------------------------- /mock/link.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const List = [] 3 | const count = 20 4 | 5 | for (let i = 0; i < count; i++) { 6 | List.push(Mock.mock({ 7 | id: '@increment', 8 | link: '@url', 9 | title: '@csentence', 10 | describe: '@csentence', 11 | email: '@email', 12 | 'status|1': ['PASS', 'APPLY', 'REFUSE', 'HIDE', 'DELETE'], 13 | coverPic: 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3', 14 | createTime: +Mock.Random.date('T'), 15 | updateTime: +Mock.Random.date('T') 16 | })) 17 | } 18 | 19 | module.exports = [ 20 | // 获取友链 21 | { 22 | url: '/links/getFriendLinks', 23 | type: 'post', 24 | response: _ => { 25 | return { 26 | code: 20000, 27 | data: List 28 | } 29 | } 30 | }, 31 | // 添加友链 32 | { 33 | url: '/links/applyFriendLink', 34 | type: 'post', 35 | response: _ => { 36 | return { 37 | code: 20000, 38 | data: 'OK' 39 | } 40 | } 41 | }, 42 | // 设置友链状态 43 | { 44 | url: '/links/setFriendLink', 45 | type: 'post', 46 | response: _ => { 47 | return { 48 | code: 20000, 49 | data: 'OK' 50 | } 51 | } 52 | } 53 | ] 54 | -------------------------------------------------------------------------------- /mock/mock-server.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const bodyParser = require('body-parser') 3 | const chalk = require('chalk') 4 | const path = require('path') 5 | const Mock = require('mockjs') 6 | 7 | const mockDir = path.join(process.cwd(), 'mock') 8 | 9 | function registerRoutes(app) { 10 | let mockLastIndex 11 | const { mocks } = require('./index.js') 12 | const mocksForServer = mocks.map(route => { 13 | return responseFake(route.url, route.type, route.response) 14 | }) 15 | for (const mock of mocksForServer) { 16 | app[mock.type](mock.url, mock.response) 17 | mockLastIndex = app._router.stack.length 18 | } 19 | const mockRoutesLength = Object.keys(mocksForServer).length 20 | return { 21 | mockRoutesLength: mockRoutesLength, 22 | mockStartIndex: mockLastIndex - mockRoutesLength 23 | } 24 | } 25 | 26 | function unregisterRoutes() { 27 | Object.keys(require.cache).forEach(i => { 28 | if (i.includes(mockDir)) { 29 | delete require.cache[require.resolve(i)] 30 | } 31 | }) 32 | } 33 | 34 | // for mock server 35 | const responseFake = (url, type, respond) => { 36 | return { 37 | url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), 38 | type: type || 'get', 39 | response(req, res) { 40 | console.log('request invoke:' + req.path) 41 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) 42 | } 43 | } 44 | } 45 | 46 | module.exports = app => { 47 | // parse app.body 48 | // https://expressjs.com/en/4x/api.html#req.body 49 | app.use(bodyParser.json()) 50 | app.use(bodyParser.urlencoded({ 51 | extended: true 52 | })) 53 | 54 | const mockRoutes = registerRoutes(app) 55 | var mockRoutesLength = mockRoutes.mockRoutesLength 56 | var mockStartIndex = mockRoutes.mockStartIndex 57 | 58 | // watch files, hot reload mock server 59 | chokidar.watch(mockDir, { 60 | ignored: /mock-server/, 61 | ignoreInitial: true 62 | }).on('all', (event, path) => { 63 | if (event === 'change' || event === 'add') { 64 | try { 65 | // remove mock routes stack 66 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 67 | 68 | // clear routes cache 69 | unregisterRoutes() 70 | 71 | const mockRoutes = registerRoutes(app) 72 | mockRoutesLength = mockRoutes.mockRoutesLength 73 | mockStartIndex = mockRoutes.mockStartIndex 74 | 75 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) 76 | } catch (error) { 77 | console.log(chalk.redBright(error)) 78 | } 79 | } 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /mock/role/index.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const { deepClone } = require('../utils') 3 | const { asyncRoutes, constantRoutes } = require('./routes.js') 4 | 5 | const routes = deepClone([...constantRoutes, ...asyncRoutes]) 6 | 7 | const roles = [ 8 | { 9 | key: 'admin', 10 | name: 'admin', 11 | description: 'Super Administrator. Have access to view all pages.', 12 | routes: routes 13 | }, 14 | { 15 | key: 'editor', 16 | name: 'editor', 17 | description: 'Normal Editor. Can see all pages except permission page', 18 | routes: routes.filter(i => i.path !== '/permission')// just a mock 19 | }, 20 | { 21 | key: 'visitor', 22 | name: 'visitor', 23 | description: 'Just a visitor. Can only see the home page and the document page', 24 | routes: [{ 25 | path: '', 26 | redirect: 'dashboard', 27 | children: [ 28 | { 29 | path: 'dashboard', 30 | name: 'Dashboard', 31 | meta: { title: 'dashboard', icon: 'dashboard' } 32 | } 33 | ] 34 | }] 35 | } 36 | ] 37 | 38 | module.exports = [ 39 | // mock get all routes form server 40 | { 41 | url: '/vue-element-admin/routes', 42 | type: 'get', 43 | response: _ => { 44 | return { 45 | code: 20000, 46 | data: routes 47 | } 48 | } 49 | }, 50 | 51 | // mock get all roles form server 52 | { 53 | url: '/vue-element-admin/roles', 54 | type: 'get', 55 | response: _ => { 56 | return { 57 | code: 20000, 58 | data: roles 59 | } 60 | } 61 | }, 62 | 63 | // add role 64 | { 65 | url: '/vue-element-admin/role', 66 | type: 'post', 67 | response: { 68 | code: 20000, 69 | data: { 70 | key: Mock.mock('@integer(300, 5000)') 71 | } 72 | } 73 | }, 74 | 75 | // update role 76 | { 77 | url: '/vue-element-admin/role/[A-Za-z0-9]', 78 | type: 'put', 79 | response: { 80 | code: 20000, 81 | data: { 82 | status: 'success' 83 | } 84 | } 85 | }, 86 | 87 | // delete role 88 | { 89 | url: '/vue-element-admin/role/[A-Za-z0-9]', 90 | type: 'delete', 91 | response: { 92 | code: 20000, 93 | data: { 94 | status: 'success' 95 | } 96 | } 97 | } 98 | ] 99 | -------------------------------------------------------------------------------- /mock/user.js: -------------------------------------------------------------------------------- 1 | const users = { 2 | roles: ['admin'], 3 | introduction: 'I am a super administrator', 4 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 5 | name: 'Super Admin', 6 | id: 0, 7 | account: 'admin@test.com', 8 | nickname: 'WindSnowLi', 9 | qq: '706623475', 10 | token: '66666666666666666' 11 | } 12 | 13 | module.exports = [ 14 | // user login 15 | { 16 | url: '/user/login', 17 | type: 'post', 18 | response: config => { 19 | // const { username } = config.body 20 | // const token = tokens[username] 21 | 22 | return { 23 | code: 20000, 24 | data: users 25 | } 26 | } 27 | }, 28 | 29 | // get user info 30 | { 31 | url: '/user/getInfo', 32 | type: 'post', 33 | response: config => { 34 | // const { token } = config.query 35 | // const info = users[token] 36 | 37 | // // mock error 38 | // if (!info) { 39 | // return { 40 | // code: 50008, 41 | // message: 'Login failed, unable to get user details.' 42 | // } 43 | // } 44 | 45 | return { 46 | code: 20000, 47 | data: users 48 | } 49 | } 50 | }, 51 | // set user info 52 | { 53 | url: '/user/setInfo', 54 | type: 'post', 55 | response: _ => { 56 | return { 57 | code: 20000, 58 | data: users 59 | } 60 | } 61 | }, 62 | 63 | // user logout 64 | { 65 | url: '/user/logout', 66 | type: 'post', 67 | response: _ => { 68 | return { 69 | code: 20000, 70 | data: 'success' 71 | } 72 | } 73 | }, 74 | // getWork 75 | { 76 | url: '/user/getWork', 77 | type: 'post', 78 | response: _ => { 79 | return { 80 | code: 20000, 81 | data: [ 82 | { 83 | name: 'Vue', 84 | value: 30 85 | }, 86 | { 87 | name: 'C++', 88 | value: 47 89 | }, 90 | { 91 | name: 'Java', 92 | value: 56 93 | }, 94 | { 95 | name: 'Python', 96 | value: 65 97 | } 98 | ] 99 | } 100 | } 101 | }, 102 | // getWork 103 | { 104 | url: '/user/setAvatar', 105 | type: 'post', 106 | response: _ => { 107 | return { 108 | code: 20000, 109 | data: 'OK' 110 | } 111 | } 112 | }, 113 | // 获取用户关于信息 114 | { 115 | url: '/user/getAboutByUserId', 116 | type: 'post', 117 | response: _ => { 118 | return { 119 | code: 20000, 120 | data: 'I LOVE YOU WITH ME!' 121 | } 122 | } 123 | }, 124 | // 获取用户关于信息 125 | { 126 | url: '/user/setAboutByUserToken', 127 | type: 'post', 128 | response: _ => { 129 | return { 130 | code: 20000, 131 | data: 'OK' 132 | } 133 | } 134 | } 135 | ] 136 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | const viewGenerator = require('./plop-templates/view/prompt') 2 | const componentGenerator = require('./plop-templates/component/prompt') 3 | const storeGenerator = require('./plop-templates/store/prompt.js') 4 | 5 | module.exports = function(plop) { 6 | plop.setGenerator('view', viewGenerator) 7 | plop.setGenerator('component', componentGenerator) 8 | plop.setGenerator('store', storeGenerator) 9 | } 10 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/706a4ed0d2e03fc4dd83e2c5724592007235c6f9/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= webpackConfig.name %> 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/api/article-label.js: -------------------------------------------------------------------------------- 1 | // 文章可选标签 2 | import request from '@/utils/request' 3 | 4 | /** 5 | * 文章可选择标签 6 | */ 7 | export function getLabels() { 8 | return request({ 9 | url: '/articleLabel/labels', 10 | method: 'post' 11 | }) 12 | } 13 | 14 | /** 15 | * 获取标签对象列表 16 | * @param query 分页查询参数对象 17 | */ 18 | export function getLabelByPage(query) { 19 | return request({ 20 | url: '/articleLabel/getLabelByPage', 21 | method: 'post', 22 | data: query 23 | }) 24 | } 25 | 26 | /** 27 | * 设置标签内容 28 | * @param label 标签对象 29 | */ 30 | export function setLabel(label) { 31 | return request({ 32 | url: '/articleLabel/setLabel', 33 | method: 'post', 34 | data: label 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/api/article.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | // 所有文章列表 4 | /** 5 | * 分页获取文章列表 6 | * @param query 分页查询参数对象 7 | */ 8 | export function fetchList(query) { 9 | return request({ 10 | url: '/article/getArticlesByPage', 11 | method: 'post', 12 | data: query 13 | }) 14 | } 15 | 16 | // 根据文章ID查询文章信息 17 | export function fetchArticle(id) { 18 | return request({ 19 | url: '/article/getArticleById', 20 | method: 'post', 21 | data: { 22 | id 23 | } 24 | }) 25 | } 26 | 27 | // 获取所有分类访问量 28 | export function getAllPVByType() { 29 | return request({ 30 | url: '/article/getAllPVByType', 31 | method: 'post' 32 | }) 33 | } 34 | 35 | // 获取文章创建历史 36 | export function getArticleCreateLog(token) { 37 | return request({ 38 | url: '/article/getArticleCreateLog', 39 | method: 'post', 40 | data: { 41 | token 42 | } 43 | }) 44 | } 45 | 46 | // 创建文章 47 | export function createArticle(token, article) { 48 | return request({ 49 | url: '/article/createArticle', 50 | method: 'post', 51 | data: { 52 | token, 53 | content: article 54 | } 55 | }) 56 | } 57 | 58 | // 更新文章 59 | export function updateArticle(article) { 60 | return request({ 61 | url: '/article/updateArticle', 62 | method: 'post', 63 | data: { 64 | content: article 65 | } 66 | }) 67 | } 68 | 69 | // 设置文章状态 70 | export function setStatus(articleId, status) { 71 | return request({ 72 | url: '/article/setStatus', 73 | method: 'post', 74 | data: { 75 | id: articleId, 76 | content: status 77 | } 78 | }) 79 | } 80 | 81 | // 删除文章 82 | export function delArticle(articleId) { 83 | return request({ 84 | url: '/article/delArticle', 85 | method: 'post', 86 | data: { 87 | id: articleId 88 | } 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /src/api/comment.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 评论列表 5 | * @param query 分页对象 6 | */ 7 | export function getCommentList(query) { 8 | const { page, limit, status, sort } = query 9 | return request({ 10 | url: '/comment/getCommentList', 11 | method: 'post', 12 | data: { 13 | page, 14 | limit, 15 | status, 16 | sort 17 | } 18 | }) 19 | } 20 | 21 | /** 22 | * 修改评论ID 23 | * @param id 评论ID 24 | * @param status 状态 25 | */ 26 | export function setCommentStatus(id, status) { 27 | return request({ 28 | url: '/comment/setCommentStatus', 29 | method: 'post', 30 | data: { 31 | id, 32 | content: status 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/api/file.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 获取头像上传的Url,含上传后的文件名 5 | * {url: 上传签名Url, GetUrl: Get请求Url} 6 | * @param {Object} token 验证信息 7 | */ 8 | export function getUploadAvatarUrl(token) { 9 | return request({ 10 | url: '/file/getUploadAvatarUrl', 11 | method: 'post', 12 | data: { token } 13 | }) 14 | } 15 | 16 | /** 17 | * 获取上传文章封面的Url,含上传后的文件名 18 | * {url: 上传签名Url, GetUrl: Get请求Url} 19 | * @param {Object} token 验证信息 20 | */ 21 | export function getUploadArticleCoverImageUrl(token) { 22 | return request({ 23 | url: '/file/getUploadArticleCoverImageUrl', 24 | method: 'post', 25 | data: { token } 26 | }) 27 | } 28 | 29 | /** 30 | * 获取上传文章内容图片的Url,含上传后的文件名 31 | * {url: 上传签名Url, GetUrl: Get请求Url} 32 | * @param {Object} token 验证信息 33 | */ 34 | export function getUploadArticleImageUrl(token) { 35 | return request({ 36 | url: '/file/getUploadArticleImageUrl', 37 | method: 'post', 38 | data: { token } 39 | }) 40 | } 41 | 42 | /** 43 | * 获取上传文章内容图片的Url,含上传后的文件名 44 | * {url: 上传签名Url, GetUrl: Get请求Url} 45 | * @param {Object} token 验证信息 46 | * @param {String} objectName 文件路径 47 | */ 48 | export function deleteObject(token, objectName) { 49 | return request({ 50 | url: '/file/deleteObject', 51 | method: 'post', 52 | data: { 53 | token, 54 | content: objectName 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/api/link.js: -------------------------------------------------------------------------------- 1 | import request from '../utils/request.js' 2 | 3 | /** 4 | * 获取友链列表 5 | */ 6 | export function getFriendLinks() { 7 | return request({ 8 | url: '/links/getFriendLinks', 9 | method: 'post', 10 | data: { 11 | status: 'ALL' 12 | } 13 | }) 14 | } 15 | 16 | /** 17 | * 申请友链 18 | */ 19 | export function applyFriendLink(friendLinks) { 20 | return request({ 21 | url: '/links/applyFriendLink', 22 | method: 'post', 23 | data: { 24 | ...friendLinks 25 | } 26 | }) 27 | } 28 | 29 | /** 30 | * 设置友链 31 | */ 32 | export function setFriendLink(link) { 33 | return request({ 34 | url: '/links/setFriendLink', 35 | method: 'post', 36 | data: { 37 | ...link 38 | } 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/api/other.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 获取仪表盘折线图和panel-group部分 5 | * @param token 验证信息 6 | */ 7 | export function getPanel(token) { 8 | return request({ 9 | url: '/other/getPanel', 10 | method: 'post', 11 | data: { token } 12 | }) 13 | } 14 | 15 | /** 16 | * 获取图表信息 17 | * @param token 验证信息 18 | */ 19 | export function getChart(token) { 20 | return request({ 21 | url: '/other/getChart', 22 | method: 'post', 23 | data: { token } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/api/remote-search.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function searchUser(name) { 4 | return request({ 5 | url: '/vue-element-admin/search/user', 6 | method: 'get', 7 | params: { name } 8 | }) 9 | } 10 | 11 | export function transactionList(query) { 12 | return request({ 13 | url: '/vue-element-admin/transaction/list', 14 | method: 'get', 15 | params: query 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/api/role.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getRoutes() { 4 | return request({ 5 | url: '/vue-element-admin/routes', 6 | method: 'get' 7 | }) 8 | } 9 | 10 | export function getRoles() { 11 | return request({ 12 | url: '/vue-element-admin/roles', 13 | method: 'get' 14 | }) 15 | } 16 | 17 | export function addRole(data) { 18 | return request({ 19 | url: '/vue-element-admin/role', 20 | method: 'post', 21 | data 22 | }) 23 | } 24 | 25 | export function updateRole(id, data) { 26 | return request({ 27 | url: `/vue-element-admin/role/${id}`, 28 | method: 'put', 29 | data 30 | }) 31 | } 32 | 33 | export function deleteRole(id) { 34 | return request({ 35 | url: `/vue-element-admin/role/${id}`, 36 | method: 'delete' 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/api/sys.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 获取系统存储配置 5 | */ 6 | export function getStorageConfig() { 7 | return request({ 8 | url: '/sys/getStorageConfig', 9 | method: 'post' 10 | }) 11 | } 12 | 13 | /** 14 | * 设置系统存储配置 15 | * @param {Object} storage 存储配置Json对象 16 | */ 17 | export function setStorageConfig(storage) { 18 | return request({ 19 | url: '/sys/setStorageConfig', 20 | method: 'post', 21 | data: { 22 | content: storage 23 | } 24 | }) 25 | } 26 | 27 | /** 28 | * 获取Gitee登录配置,包含表单描述信息 29 | */ 30 | export function getGiteeConfig() { 31 | return request({ 32 | url: '/sys/getGiteeConfig', 33 | method: 'post' 34 | }) 35 | } 36 | 37 | /** 38 | * 设置Gitee登录配置 39 | */ 40 | export function setGiteeConfig(config) { 41 | return request({ 42 | url: '/sys/setGiteeConfig', 43 | method: 'post', 44 | data: config 45 | }) 46 | } 47 | 48 | /** 49 | * 获取系统基础配置,包含表单描述信息 50 | */ 51 | export function getFixedConfig() { 52 | return request({ 53 | url: '/sys/getFixedConfig', 54 | method: 'post' 55 | }) 56 | } 57 | 58 | /** 59 | * 上传系统配置 60 | * @param {Object} config 系统设置信息 61 | */ 62 | export function setFixedConfig(config) { 63 | return request({ 64 | url: '/sys/setFixedConfig', 65 | method: 'post', 66 | data: { 67 | content: config 68 | } 69 | }) 70 | } 71 | 72 | /** 73 | * 获取用户Ui配置信息 74 | * @param {int} id 用户ID 75 | */ 76 | export function getUiConfig(id) { 77 | return request({ 78 | url: '/sys/getUiConfig', 79 | method: 'post', 80 | data: { id } 81 | }) 82 | } 83 | 84 | /** 85 | * 设置UI 86 | * @param {String} token 验证信息 87 | * @param {Object} data UI数据 88 | */ 89 | export function setUiConfig(token, data) { 90 | return request({ 91 | url: '/sys/setUiConfig', 92 | method: 'post', 93 | data: { 94 | token: token, 95 | content: data 96 | } 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 登录,返回验证信息 5 | * @param {Object} data 行号密码 6 | */ 7 | export function login(data) { 8 | return request({ 9 | url: '/user/login', 10 | method: 'post', 11 | data 12 | }) 13 | } 14 | 15 | /** 16 | * 获取用户信息 17 | * @param {Object} token 验证信息 18 | */ 19 | export function getInfo(token) { 20 | const data = { 'token': token } 21 | return request({ 22 | url: '/user/getInfo', 23 | method: 'post', 24 | data 25 | }) 26 | } 27 | 28 | /** 29 | * 设置个人信息 30 | * @param {Object} token 验证信息 31 | * @param {Object} user 新的用户信息对象 32 | */ 33 | export function setInfo(token, user) { 34 | return request({ 35 | url: '/user/setInfo', 36 | method: 'post', 37 | data: { token: token, content: user } 38 | }) 39 | } 40 | 41 | /** 42 | * 登出 43 | */ 44 | export function logout(token) { 45 | return request({ 46 | url: '/user/logout', 47 | method: 'post', 48 | data: { token } 49 | }) 50 | } 51 | 52 | /** 53 | * 获取最近活动记录 54 | * @param {Object} token 验证信息 55 | */ 56 | export function getActivity(token) { 57 | return request({ 58 | url: '/user/getActivity', 59 | method: 'post', 60 | data: { token } 61 | }) 62 | } 63 | 64 | /** 65 | * 获取工作概览 66 | * @param {Object} token 验证信息 67 | */ 68 | export function getWork(token) { 69 | return request({ 70 | url: '/user/getWork', 71 | method: 'post', 72 | data: { token } 73 | }) 74 | } 75 | 76 | /** 77 | * 设置头像 78 | * @param {Object} token 验证信息 79 | * @param {String} avatar 头像链接 80 | */ 81 | export function setAvatar(token, avatar) { 82 | return request({ 83 | url: '/user/setAvatar', 84 | method: 'post', 85 | data: { 86 | token, 87 | content: avatar 88 | } 89 | }) 90 | } 91 | 92 | /** 93 | * 获取用户关于信息 94 | * @param {int} id 用户ID 95 | */ 96 | export function getAboutByUserId(id) { 97 | return request({ 98 | url: '/user/getAboutByUserId', 99 | method: 'post', 100 | data: { id } 101 | }) 102 | } 103 | 104 | /** 105 | * 获取用户关于信息 106 | * @param token 验证信息 107 | * @param content 关于内容 108 | */ 109 | export function setAboutByUserToken(token, content) { 110 | return request({ 111 | url: '/user/setAboutByUserToken', 112 | method: 'post', 113 | data: { token, content } 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /src/assets/401_images/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/706a4ed0d2e03fc4dd83e2c5724592007235c6f9/src/assets/401_images/401.gif -------------------------------------------------------------------------------- /src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/706a4ed0d2e03fc4dd83e2c5724592007235c6f9/src/assets/404_images/404.png -------------------------------------------------------------------------------- /src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/706a4ed0d2e03fc4dd83e2c5724592007235c6f9/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/706a4ed0d2e03fc4dd83e2c5724592007235c6f9/src/assets/custom-theme/fonts/element-icons.ttf -------------------------------------------------------------------------------- /src/assets/custom-theme/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindSnowLi/vue-admin-blog/706a4ed0d2e03fc4dd83e2c5724592007235c6f9/src/assets/custom-theme/fonts/element-icons.woff -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 69 | 70 | 83 | -------------------------------------------------------------------------------- /src/components/Charts/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null, 7 | $_resizeHandler: null 8 | } 9 | }, 10 | mounted() { 11 | this.initListener() 12 | }, 13 | activated() { 14 | if (!this.$_resizeHandler) { 15 | // avoid duplication init 16 | this.initListener() 17 | } 18 | 19 | // when keep-alive chart activated, auto resize 20 | this.resize() 21 | }, 22 | beforeDestroy() { 23 | this.destroyListener() 24 | }, 25 | deactivated() { 26 | this.destroyListener() 27 | }, 28 | methods: { 29 | // use $_ for mixins properties 30 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 31 | $_sidebarResizeHandler(e) { 32 | if (e.propertyName === 'width') { 33 | this.$_resizeHandler() 34 | } 35 | }, 36 | initListener() { 37 | this.$_resizeHandler = debounce(() => { 38 | this.resize() 39 | }, 100) 40 | window.addEventListener('resize', this.$_resizeHandler) 41 | 42 | this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0] 43 | this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler) 44 | }, 45 | destroyListener() { 46 | window.removeEventListener('resize', this.$_resizeHandler) 47 | this.$_resizeHandler = null 48 | 49 | this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler) 50 | }, 51 | resize() { 52 | const { chart } = this 53 | chart && chart.resize() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/DragSelect/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 50 | 51 | 66 | -------------------------------------------------------------------------------- /src/components/ErrorLog/index.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 70 | 71 | 79 | -------------------------------------------------------------------------------- /src/components/GithubCorner/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 55 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 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/Kanban/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 54 | 99 | 100 | -------------------------------------------------------------------------------- /src/components/MarkdownEditor/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 77 | 78 | 84 | -------------------------------------------------------------------------------- /src/components/Pagination/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 92 | 93 | 102 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 50 | 51 | 61 | -------------------------------------------------------------------------------- /src/components/Share/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 39 | 40 | 104 | -------------------------------------------------------------------------------- /src/components/SizeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 58 | -------------------------------------------------------------------------------- /src/components/Sticky/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 92 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /src/components/TextHoverEffect/Mallki.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | 114 | -------------------------------------------------------------------------------- /src/components/Tinymce/dynamicLoadScript.js: -------------------------------------------------------------------------------- 1 | let callbacks = [] 2 | 3 | function loadedTinymce() { 4 | // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2144 5 | // check is successfully downloaded script 6 | return window.tinymce 7 | } 8 | 9 | const dynamicLoadScript = (src, callback) => { 10 | const existingScript = document.getElementById(src) 11 | const cb = callback || function() {} 12 | 13 | if (!existingScript) { 14 | const script = document.createElement('script') 15 | script.src = src // src url for the third-party library being loaded. 16 | script.id = src 17 | document.body.appendChild(script) 18 | callbacks.push(cb) 19 | const onEnd = 'onload' in script ? stdOnEnd : ieOnEnd 20 | onEnd(script) 21 | } 22 | 23 | if (existingScript && cb) { 24 | if (loadedTinymce()) { 25 | cb(null, existingScript) 26 | } else { 27 | callbacks.push(cb) 28 | } 29 | } 30 | 31 | function stdOnEnd(script) { 32 | script.onload = function() { 33 | // this.onload = null here is necessary 34 | // because even IE9 works not like others 35 | this.onerror = this.onload = null 36 | for (const cb of callbacks) { 37 | cb(null, script) 38 | } 39 | callbacks = null 40 | } 41 | script.onerror = function() { 42 | this.onerror = this.onload = null 43 | cb(new Error('Failed to load ' + src), script) 44 | } 45 | } 46 | 47 | function ieOnEnd(script) { 48 | script.onreadystatechange = function() { 49 | if (this.readyState !== 'complete' && this.readyState !== 'loaded') return 50 | this.onreadystatechange = null 51 | for (const cb of callbacks) { 52 | cb(null, script) // there is no way to catch loading errors in IE8 53 | } 54 | callbacks = null 55 | } 56 | } 57 | } 58 | 59 | export default dynamicLoadScript 60 | -------------------------------------------------------------------------------- /src/components/Tinymce/plugins.js: -------------------------------------------------------------------------------- 1 | // Any plugins you want to use has to be imported 2 | // Detail plugins list see https://www.tinymce.com/docs/plugins/ 3 | // Custom builds see https://www.tinymce.com/download/custom-builds/ 4 | 5 | const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount'] 6 | 7 | export default plugins 8 | -------------------------------------------------------------------------------- /src/components/Tinymce/toolbar.js: -------------------------------------------------------------------------------- 1 | // Here is a list of the toolbar 2 | // Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols 3 | 4 | const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen'] 5 | 6 | export default toolbar 7 | -------------------------------------------------------------------------------- /src/directive/clipboard/clipboard.js: -------------------------------------------------------------------------------- 1 | // Inspired by https://github.com/Inndy/vue-clipboard2 2 | const Clipboard = require('clipboard') 3 | if (!Clipboard) { 4 | throw new Error('you should npm install `clipboard` --save at first ') 5 | } 6 | 7 | export default { 8 | bind(el, binding) { 9 | if (binding.arg === 'success') { 10 | el._v_clipboard_success = binding.value 11 | } else if (binding.arg === 'error') { 12 | el._v_clipboard_error = binding.value 13 | } else { 14 | const clipboard = new Clipboard(el, { 15 | text() { return binding.value }, 16 | action() { return binding.arg === 'cut' ? 'cut' : 'copy' } 17 | }) 18 | clipboard.on('success', e => { 19 | const callback = el._v_clipboard_success 20 | callback && callback(e) // eslint-disable-line 21 | }) 22 | clipboard.on('error', e => { 23 | const callback = el._v_clipboard_error 24 | callback && callback(e) // eslint-disable-line 25 | }) 26 | el._v_clipboard = clipboard 27 | } 28 | }, 29 | update(el, binding) { 30 | if (binding.arg === 'success') { 31 | el._v_clipboard_success = binding.value 32 | } else if (binding.arg === 'error') { 33 | el._v_clipboard_error = binding.value 34 | } else { 35 | el._v_clipboard.text = function() { return binding.value } 36 | el._v_clipboard.action = function() { return binding.arg === 'cut' ? 'cut' : 'copy' } 37 | } 38 | }, 39 | unbind(el, binding) { 40 | if (binding.arg === 'success') { 41 | delete el._v_clipboard_success 42 | } else if (binding.arg === 'error') { 43 | delete el._v_clipboard_error 44 | } else { 45 | el._v_clipboard.destroy() 46 | delete el._v_clipboard 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/directive/clipboard/index.js: -------------------------------------------------------------------------------- 1 | import Clipboard from './clipboard' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('Clipboard', Clipboard) 5 | } 6 | 7 | if (window.Vue) { 8 | window.clipboard = Clipboard 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | Clipboard.install = install 13 | export default Clipboard 14 | -------------------------------------------------------------------------------- /src/directive/el-drag-dialog/drag.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bind(el, binding, vnode) { 3 | const dialogHeaderEl = el.querySelector('.el-dialog__header') 4 | const dragDom = el.querySelector('.el-dialog') 5 | dialogHeaderEl.style.cssText += ';cursor:move;' 6 | dragDom.style.cssText += ';top:0px;' 7 | 8 | // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null); 9 | const getStyle = (function() { 10 | if (window.document.currentStyle) { 11 | return (dom, attr) => dom.currentStyle[attr] 12 | } else { 13 | return (dom, attr) => getComputedStyle(dom, false)[attr] 14 | } 15 | })() 16 | 17 | dialogHeaderEl.onmousedown = (e) => { 18 | // 鼠标按下,计算当前元素距离可视区的距离 19 | const disX = e.clientX - dialogHeaderEl.offsetLeft 20 | const disY = e.clientY - dialogHeaderEl.offsetTop 21 | 22 | const dragDomWidth = dragDom.offsetWidth 23 | const dragDomHeight = dragDom.offsetHeight 24 | 25 | const screenWidth = document.body.clientWidth 26 | const screenHeight = document.body.clientHeight 27 | 28 | const minDragDomLeft = dragDom.offsetLeft 29 | const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth 30 | 31 | const minDragDomTop = dragDom.offsetTop 32 | const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight 33 | 34 | // 获取到的值带px 正则匹配替换 35 | let styL = getStyle(dragDom, 'left') 36 | let styT = getStyle(dragDom, 'top') 37 | 38 | if (styL.includes('%')) { 39 | styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100) 40 | styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100) 41 | } else { 42 | styL = +styL.replace(/\px/g, '') 43 | styT = +styT.replace(/\px/g, '') 44 | } 45 | 46 | document.onmousemove = function(e) { 47 | // 通过事件委托,计算移动的距离 48 | let left = e.clientX - disX 49 | let top = e.clientY - disY 50 | 51 | // 边界处理 52 | if (-(left) > minDragDomLeft) { 53 | left = -minDragDomLeft 54 | } else if (left > maxDragDomLeft) { 55 | left = maxDragDomLeft 56 | } 57 | 58 | if (-(top) > minDragDomTop) { 59 | top = -minDragDomTop 60 | } else if (top > maxDragDomTop) { 61 | top = maxDragDomTop 62 | } 63 | 64 | // 移动当前元素 65 | dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;` 66 | 67 | // emit onDrag event 68 | vnode.child.$emit('dragDialog') 69 | } 70 | 71 | document.onmouseup = function(e) { 72 | document.onmousemove = null 73 | document.onmouseup = null 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/directive/el-drag-dialog/index.js: -------------------------------------------------------------------------------- 1 | import drag from './drag' 2 | 3 | const install = function(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 | function checkPermission(el, binding) { 4 | const { value } = binding 5 | const roles = store.getters && store.getters.roles 6 | 7 | if (value && value instanceof Array) { 8 | if (value.length > 0) { 9 | const permissionRoles = value 10 | 11 | const hasPermission = roles.some(role => { 12 | return permissionRoles.includes(role) 13 | }) 14 | 15 | if (!hasPermission) { 16 | el.parentNode && el.parentNode.removeChild(el) 17 | } 18 | } 19 | } else { 20 | throw new Error(`need roles! Like v-permission="['admin','editor']"`) 21 | } 22 | } 23 | 24 | export default { 25 | inserted(el, binding) { 26 | checkPermission(el, binding) 27 | }, 28 | update(el, binding) { 29 | checkPermission(el, binding) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/directive/sticky.js: -------------------------------------------------------------------------------- 1 | const vueSticky = {} 2 | let listenAction 3 | vueSticky.install = Vue => { 4 | Vue.directive('sticky', { 5 | inserted(el, binding) { 6 | const params = binding.value || {} 7 | const stickyTop = params.stickyTop || 0 8 | const zIndex = params.zIndex || 1000 9 | const elStyle = el.style 10 | 11 | elStyle.position = '-webkit-sticky' 12 | elStyle.position = 'sticky' 13 | // if the browser support css sticky(Currently Safari, Firefox and Chrome Canary) 14 | // if (~elStyle.position.indexOf('sticky')) { 15 | // elStyle.top = `${stickyTop}px`; 16 | // elStyle.zIndex = zIndex; 17 | // return 18 | // } 19 | const elHeight = el.getBoundingClientRect().height 20 | const elWidth = el.getBoundingClientRect().width 21 | elStyle.cssText = `top: ${stickyTop}px; z-index: ${zIndex}` 22 | 23 | const parentElm = el.parentNode || document.documentElement 24 | const placeholder = document.createElement('div') 25 | placeholder.style.display = 'none' 26 | placeholder.style.width = `${elWidth}px` 27 | placeholder.style.height = `${elHeight}px` 28 | parentElm.insertBefore(placeholder, el) 29 | 30 | let active = false 31 | 32 | const getScroll = (target, top) => { 33 | const prop = top ? 'pageYOffset' : 'pageXOffset' 34 | const method = top ? 'scrollTop' : 'scrollLeft' 35 | let ret = target[prop] 36 | if (typeof ret !== 'number') { 37 | ret = window.document.documentElement[method] 38 | } 39 | return ret 40 | } 41 | 42 | const sticky = () => { 43 | if (active) { 44 | return 45 | } 46 | if (!elStyle.height) { 47 | elStyle.height = `${el.offsetHeight}px` 48 | } 49 | 50 | elStyle.position = 'fixed' 51 | elStyle.width = `${elWidth}px` 52 | placeholder.style.display = 'inline-block' 53 | active = true 54 | } 55 | 56 | const reset = () => { 57 | if (!active) { 58 | return 59 | } 60 | 61 | elStyle.position = '' 62 | placeholder.style.display = 'none' 63 | active = false 64 | } 65 | 66 | const check = () => { 67 | const scrollTop = getScroll(window, true) 68 | const offsetTop = el.getBoundingClientRect().top 69 | if (offsetTop < stickyTop) { 70 | sticky() 71 | } else { 72 | if (scrollTop < elHeight + stickyTop) { 73 | reset() 74 | } 75 | } 76 | } 77 | listenAction = () => { 78 | check() 79 | } 80 | 81 | window.addEventListener('scroll', listenAction) 82 | }, 83 | 84 | unbind() { 85 | window.removeEventListener('scroll', listenAction) 86 | } 87 | }) 88 | } 89 | 90 | export default vueSticky 91 | 92 | -------------------------------------------------------------------------------- /src/directive/waves/index.js: -------------------------------------------------------------------------------- 1 | import waves from './waves' 2 | 3 | const install = function(Vue) { 4 | Vue.directive('waves', waves) 5 | } 6 | 7 | if (window.Vue) { 8 | window.waves = waves 9 | Vue.use(install); // eslint-disable-line 10 | } 11 | 12 | waves.install = install 13 | export default waves 14 | -------------------------------------------------------------------------------- /src/directive/waves/waves.css: -------------------------------------------------------------------------------- 1 | .waves-ripple { 2 | position: absolute; 3 | border-radius: 100%; 4 | background-color: rgba(0, 0, 0, 0.15); 5 | background-clip: padding-box; 6 | pointer-events: none; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | -webkit-transform: scale(0); 12 | -ms-transform: scale(0); 13 | transform: scale(0); 14 | opacity: 1; 15 | } 16 | 17 | .waves-ripple.z-active { 18 | opacity: 0; 19 | -webkit-transform: scale(2); 20 | -ms-transform: scale(2); 21 | transform: scale(2); 22 | -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 23 | transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out; 24 | transition: opacity 1.2s ease-out, transform 0.6s ease-out; 25 | transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out; 26 | } -------------------------------------------------------------------------------- /src/directive/waves/waves.js: -------------------------------------------------------------------------------- 1 | import './waves.css' 2 | 3 | const context = '@@wavesContext' 4 | 5 | function handleClick(el, binding) { 6 | function handle(e) { 7 | const customOpts = Object.assign({}, binding.value) 8 | const opts = Object.assign({ 9 | ele: el, // 波纹作用元素 10 | type: 'hit', // hit 点击位置扩散 center中心点扩展 11 | color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色 12 | }, 13 | customOpts 14 | ) 15 | const target = opts.ele 16 | if (target) { 17 | target.style.position = 'relative' 18 | target.style.overflow = 'hidden' 19 | const rect = target.getBoundingClientRect() 20 | let ripple = target.querySelector('.waves-ripple') 21 | if (!ripple) { 22 | ripple = document.createElement('span') 23 | ripple.className = 'waves-ripple' 24 | ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px' 25 | target.appendChild(ripple) 26 | } else { 27 | ripple.className = 'waves-ripple' 28 | } 29 | switch (opts.type) { 30 | case 'center': 31 | ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px' 32 | ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px' 33 | break 34 | default: 35 | ripple.style.top = 36 | (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop || 37 | document.body.scrollTop) + 'px' 38 | ripple.style.left = 39 | (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft || 40 | document.body.scrollLeft) + 'px' 41 | } 42 | ripple.style.backgroundColor = opts.color 43 | ripple.className = 'waves-ripple z-active' 44 | return false 45 | } 46 | } 47 | 48 | if (!el[context]) { 49 | el[context] = { 50 | removeHandle: handle 51 | } 52 | } else { 53 | el[context].removeHandle = handle 54 | } 55 | 56 | return handle 57 | } 58 | 59 | export default { 60 | bind(el, binding) { 61 | el.addEventListener('click', handleClick(el, binding), false) 62 | }, 63 | update(el, binding) { 64 | el.removeEventListener('click', el[context].removeHandle, false) 65 | el.addEventListener('click', handleClick(el, binding), false) 66 | }, 67 | unbind(el) { 68 | el.removeEventListener('click', el[context].removeHandle, false) 69 | el[context] = null 70 | delete el[context] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/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/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/education.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/excel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/skill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree-table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/wechat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | 25 | 49 | 50 | 58 | -------------------------------------------------------------------------------- /src/layout/components/Settings/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 83 | 84 | 109 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // 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 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 55 | -------------------------------------------------------------------------------- /src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as AppMain } from './AppMain' 2 | export { default as Navbar } from './Navbar' 3 | export { default as Settings } from './Settings' 4 | export { default as Sidebar } from './Sidebar/index.vue' 5 | export { default as TagsView } from './TagsView/index.vue' 6 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 59 | 60 | 103 | -------------------------------------------------------------------------------- /src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import Cookies from 'js-cookie' 4 | 5 | import 'normalize.css/normalize.css' // a modern alternative to CSS resets 6 | // import 'default-passive-events' 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 | import * as filters from './filters' // global filters 20 | 21 | import VueForm from '@lljj/vue-json-schema-form' 22 | 23 | import mavonEditor from 'mavon-editor' 24 | import 'mavon-editor/dist/css/index.css' 25 | // use 26 | Vue.use(mavonEditor) 27 | 28 | /** 29 | * If you don't want to use mock-server 30 | * you want to use MockJs for mock api 31 | * you can execute: mockXHR() 32 | * 33 | * Currently MockJs will be used in the production environment, 34 | * please remove it before going online ! ! ! 35 | */ 36 | 37 | if (process.env.NODE_ENV !== 'production') { 38 | const { mockXHR } = require('../mock') 39 | mockXHR() 40 | } 41 | 42 | Vue.use(Element, { 43 | size: Cookies.get('size') || 'medium' // set element-ui default size 44 | }) 45 | 46 | // register global utility filters 47 | Object.keys(filters).forEach(key => { 48 | Vue.filter(key, filters[key]) 49 | }) 50 | 51 | Vue.component('VueForm', VueForm) 52 | 53 | Vue.config.productionTip = true 54 | 55 | new Vue({ 56 | el: '#app', 57 | router, 58 | store, 59 | render: h => h(App) 60 | }) 61 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const hasToken = getToken() 22 | 23 | if (hasToken) { 24 | if (to.path === '/login') { 25 | // if is logged in, redirect to the home page 26 | next({ path: '/' }) 27 | NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939 28 | } else { 29 | // determine whether the user has obtained his permission roles through getInfo 30 | const hasRoles = store.getters.roles && store.getters.roles.length > 0 31 | if (hasRoles) { 32 | next() 33 | } else { 34 | try { 35 | // get user info 36 | // note: roles must be a object array! such as: ['admin'] or ,['developer','editor'] 37 | const { roles } = await store.dispatch('user/getInfo') 38 | 39 | // generate accessible routes map based on roles 40 | const accessRoutes = await store.dispatch('permission/generateRoutes', roles) 41 | 42 | // dynamically add accessible routes 43 | router.addRoutes(accessRoutes) 44 | 45 | // hack method to ensure that addRoutes is complete 46 | // set the replace: true, so the navigation will not leave a history record 47 | next({ ...to, replace: true }) 48 | } catch (error) { 49 | // remove token and go to login page to re-login 50 | await store.dispatch('user/resetToken') 51 | Message.error(error || 'Has Error') 52 | next(`/login?redirect=${to.path}`) 53 | NProgress.done() 54 | } 55 | } 56 | } 57 | } else { 58 | /* has no token*/ 59 | 60 | if (whiteList.indexOf(to.path) !== -1) { 61 | // in the free login whitelist, go directly 62 | next() 63 | } else { 64 | // other pages that do not have permission to access are redirected to the login page. 65 | next(`/login?redirect=${to.path}`) 66 | NProgress.done() 67 | } 68 | } 69 | }) 70 | 71 | router.afterEach(() => { 72 | // finish progress bar 73 | NProgress.done() 74 | }) 75 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: '个人博客', 3 | 4 | /** 5 | * @type {boolean} true | false 6 | * @description Whether show the settings right-panel 7 | */ 8 | showSettings: true, 9 | 10 | /** 11 | * @type {boolean} true | false 12 | * @description Whether need tagsView 13 | */ 14 | tagsView: true, 15 | 16 | /** 17 | * @type {boolean} true | false 18 | * @description Whether fix the header 19 | */ 20 | fixedHeader: false, 21 | 22 | /** 23 | * @type {boolean} true | false 24 | * @description Whether show the logo in sidebar 25 | */ 26 | sidebarLogo: false, 27 | 28 | /** 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 | id: state => state.user.id, 9 | avatar: state => state.user.avatar, 10 | name: state => state.user.name, 11 | introduction: state => state.user.introduction, 12 | roles: state => state.user.roles, 13 | permission_routes: state => state.permission.routes, 14 | errorLogs: state => state.errorLog.logs 15 | } 16 | export default getters 17 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | 5 | Vue.use(Vuex) 6 | 7 | // https://webpack.js.org/guides/dependency-management/#requirecontext 8 | const modulesFiles = require.context('./modules', true, /\.js$/) 9 | 10 | // you do not need `import app from './modules/app'` 11 | // it will auto require all vuex module from modules file 12 | const modules = modulesFiles.keys().reduce((modules, modulePath) => { 13 | // set './app.js' => 'app' 14 | const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') 15 | const value = modulesFiles(modulePath) 16 | modules[moduleName] = value.default 17 | return modules 18 | }, {}) 19 | 20 | const store = new Vuex.Store({ 21 | modules, 22 | getters 23 | }) 24 | 25 | export default store 26 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop', 9 | size: Cookies.get('size') || 'medium' 10 | } 11 | 12 | const mutations = { 13 | TOGGLE_SIDEBAR: state => { 14 | state.sidebar.opened = !state.sidebar.opened 15 | state.sidebar.withoutAnimation = false 16 | if (state.sidebar.opened) { 17 | Cookies.set('sidebarStatus', 1) 18 | } else { 19 | Cookies.set('sidebarStatus', 0) 20 | } 21 | }, 22 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 23 | Cookies.set('sidebarStatus', 0) 24 | state.sidebar.opened = false 25 | state.sidebar.withoutAnimation = withoutAnimation 26 | }, 27 | TOGGLE_DEVICE: (state, device) => { 28 | state.device = device 29 | }, 30 | SET_SIZE: (state, size) => { 31 | state.size = size 32 | Cookies.set('size', size) 33 | } 34 | } 35 | 36 | const actions = { 37 | toggleSideBar({ commit }) { 38 | commit('TOGGLE_SIDEBAR') 39 | }, 40 | closeSideBar({ commit }, { withoutAnimation }) { 41 | commit('CLOSE_SIDEBAR', withoutAnimation) 42 | }, 43 | toggleDevice({ commit }, device) { 44 | commit('TOGGLE_DEVICE', device) 45 | }, 46 | setSize({ commit }, size) { 47 | commit('SET_SIZE', size) 48 | } 49 | } 50 | 51 | export default { 52 | namespaced: true, 53 | state, 54 | mutations, 55 | actions 56 | } 57 | -------------------------------------------------------------------------------- /src/store/modules/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 | // eslint-disable-next-line no-prototype-builtins 17 | if (state.hasOwnProperty(key)) { 18 | state[key] = value 19 | } 20 | } 21 | } 22 | 23 | const actions = { 24 | changeSetting({ commit }, data) { 25 | commit('CHANGE_SETTING', data) 26 | } 27 | } 28 | 29 | export default { 30 | namespaced: true, 31 | state, 32 | mutations, 33 | actions 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/styles/btn.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | @mixin colorBtn($color) { 4 | background: $color; 5 | 6 | &:hover { 7 | color: $color; 8 | 9 | &:before, 10 | &:after { 11 | background: $color; 12 | } 13 | } 14 | } 15 | 16 | .blue-btn { 17 | @include colorBtn($blue) 18 | } 19 | 20 | .light-blue-btn { 21 | @include colorBtn($light-blue) 22 | } 23 | 24 | .red-btn { 25 | @include colorBtn($red) 26 | } 27 | 28 | .pink-btn { 29 | @include colorBtn($pink) 30 | } 31 | 32 | .green-btn { 33 | @include colorBtn($green) 34 | } 35 | 36 | .tiffany-btn { 37 | @include colorBtn($tiffany) 38 | } 39 | 40 | .yellow-btn { 41 | @include colorBtn($yellow) 42 | } 43 | 44 | .pan-btn { 45 | font-size: 14px; 46 | color: #fff; 47 | padding: 14px 36px; 48 | border-radius: 8px; 49 | border: none; 50 | outline: none; 51 | transition: 600ms ease all; 52 | position: relative; 53 | display: inline-block; 54 | 55 | &:hover { 56 | background: #fff; 57 | 58 | &:before, 59 | &:after { 60 | width: 100%; 61 | transition: 600ms ease all; 62 | } 63 | } 64 | 65 | &:before, 66 | &:after { 67 | content: ''; 68 | position: absolute; 69 | top: 0; 70 | right: 0; 71 | height: 2px; 72 | width: 0; 73 | transition: 400ms ease all; 74 | } 75 | 76 | &::after { 77 | right: inherit; 78 | top: inherit; 79 | left: 0; 80 | bottom: 0; 81 | } 82 | } 83 | 84 | .custom-button { 85 | display: inline-block; 86 | line-height: 1; 87 | white-space: nowrap; 88 | cursor: pointer; 89 | background: #fff; 90 | color: #fff; 91 | -webkit-appearance: none; 92 | text-align: center; 93 | box-sizing: border-box; 94 | outline: 0; 95 | margin: 0; 96 | padding: 10px 15px; 97 | font-size: 14px; 98 | border-radius: 4px; 99 | } 100 | -------------------------------------------------------------------------------- /src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | .cell { 19 | .el-tag { 20 | margin-right: 0px; 21 | } 22 | } 23 | 24 | .small-padding { 25 | .cell { 26 | padding-left: 5px; 27 | padding-right: 5px; 28 | } 29 | } 30 | 31 | .fixed-width { 32 | .el-button--mini { 33 | padding: 7px 10px; 34 | min-width: 60px; 35 | } 36 | } 37 | 38 | .status-col { 39 | .cell { 40 | padding: 0 10px; 41 | text-align: center; 42 | 43 | .el-tag { 44 | margin-right: 0px; 45 | } 46 | } 47 | } 48 | 49 | // to fixed https://github.com/ElemeFE/element/issues/2461 50 | .el-dialog { 51 | transform: none; 52 | left: 0; 53 | position: relative; 54 | margin: 0 auto; 55 | } 56 | 57 | // refine element ui upload 58 | .upload-container { 59 | .el-upload { 60 | width: 100%; 61 | 62 | .el-upload-dragger { 63 | width: 100%; 64 | height: 200px; 65 | } 66 | } 67 | } 68 | 69 | // dropdown 70 | .el-dropdown-menu { 71 | a { 72 | display: block 73 | } 74 | } 75 | 76 | // fix date-picker ui bug in filter-item 77 | .el-range-editor.el-input__inner { 78 | display: inline-flex !important; 79 | } 80 | 81 | // to fix el-date-picker css style 82 | .el-range-separator { 83 | box-sizing: content-box; 84 | } 85 | -------------------------------------------------------------------------------- /src/styles/element-variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * I think element-ui's default theme color is too light for long-term use. 3 | * So I modified the default color and you can modify it to your liking. 4 | **/ 5 | 6 | /* theme color */ 7 | $--color-primary: #1890ff; 8 | $--color-success: #13ce66; 9 | $--color-warning: #ffba00; 10 | $--color-danger: #ff4949; 11 | // $--color-info: #1E1E1E; 12 | 13 | $--button-font-weight: 400; 14 | 15 | // $--color-text-regular: #1f2d3d; 16 | 17 | $--border-color-light: #dfe4ed; 18 | $--border-color-lighter: #e6ebf5; 19 | 20 | $--table-border: 1px solid #dfe6ec; 21 | 22 | /* icon font path, required */ 23 | $--font-path: "~element-ui/lib/theme-chalk/fonts"; 24 | 25 | @import "~element-ui/packages/theme-chalk/src/index"; 26 | 27 | // the :export directive is the magic sauce for webpack 28 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 29 | :export { 30 | theme: $--color-primary; 31 | } 32 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | @mixin pct($pct) { 31 | width: #{$pct}; 32 | position: relative; 33 | margin: 0 auto; 34 | } 35 | 36 | @mixin triangle($width, $height, $color, $direction) { 37 | $width: $width/2; 38 | $color-border-style: $height solid $color; 39 | $transparent-border-style: $width solid transparent; 40 | height: 0; 41 | width: 0; 42 | 43 | @if $direction==up { 44 | border-bottom: $color-border-style; 45 | border-left: $transparent-border-style; 46 | border-right: $transparent-border-style; 47 | } 48 | 49 | @else if $direction==right { 50 | border-left: $color-border-style; 51 | border-top: $transparent-border-style; 52 | border-bottom: $transparent-border-style; 53 | } 54 | 55 | @else if $direction==down { 56 | border-top: $color-border-style; 57 | border-left: $transparent-border-style; 58 | border-right: $transparent-border-style; 59 | } 60 | 61 | @else if $direction==left { 62 | border-right: $color-border-style; 63 | border-top: $transparent-border-style; 64 | border-bottom: $transparent-border-style; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // base color 2 | $blue:#324157; 3 | $light-blue:#3A71A8; 4 | $red:#C03639; 5 | $pink: #E65D6E; 6 | $green: #30B08F; 7 | $tiffany: #4AB7BD; 8 | $yellow:#FEC171; 9 | $panGreen: #30B08F; 10 | 11 | // sidebar 12 | $menuText:#bfcbd9; 13 | $menuActiveText:#409EFF; 14 | $subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951 15 | 16 | $menuBg:#304156; 17 | $menuHover:#263445; 18 | 19 | $subMenuBg:#1f2d3d; 20 | $subMenuHover:#001528; 21 | 22 | $sideBarWidth: 210px; 23 | 24 | // the :export directive is the magic sauce for webpack 25 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 26 | :export { 27 | menuText: $menuText; 28 | menuActiveText: $menuActiveText; 29 | subMenuActiveText: $subMenuActiveText; 30 | menuBg: $menuBg; 31 | menuHover: $menuHover; 32 | subMenuBg: $subMenuBg; 33 | subMenuHover: $subMenuHover; 34 | sideBarWidth: $sideBarWidth; 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'Admin-Token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/clipboard.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Clipboard from 'clipboard' 3 | 4 | function clipboardSuccess() { 5 | Vue.prototype.$message({ 6 | message: 'Copy successfully', 7 | type: 'success', 8 | duration: 1500 9 | }) 10 | } 11 | 12 | function clipboardError() { 13 | Vue.prototype.$message({ 14 | message: 'Copy failed', 15 | type: 'error' 16 | }) 17 | } 18 | 19 | export default function handleClipboard(text, event) { 20 | const clipboard = new Clipboard(event.target, { 21 | text: () => text 22 | }) 23 | clipboard.on('success', () => { 24 | clipboardSuccess() 25 | clipboard.destroy() 26 | }) 27 | clipboard.on('error', () => { 28 | clipboardError() 29 | clipboard.destroy() 30 | }) 31 | clipboard.onClick(event) 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/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 | return hasPermission 17 | } else { 18 | console.error(`need roles! Like v-permission="['admin','editor']"`) 19 | return false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 9 | // withCredentials: true, // send cookies when cross-domain requests 10 | timeout: 5000 // request timeout 11 | }) 12 | 13 | // request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // do something before request is sent 17 | 18 | if (store.getters.token) { 19 | // let each request carry token 20 | // ['X-Token'] is a custom headers key 21 | // please modify it according to the actual situation 22 | config.headers['token'] = getToken() 23 | } 24 | return config 25 | }, 26 | error => { 27 | // do something with request error 28 | console.log(error) // for debug 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | // response interceptor 34 | service.interceptors.response.use( 35 | /** 36 | * If you want to get http information such as headers or status 37 | * Please return response => response 38 | */ 39 | 40 | /** 41 | * Determine the request status by custom code 42 | * Here is just an example 43 | * You can also judge the status by HTTP Status Code 44 | */ 45 | response => { 46 | const res = response.data 47 | 48 | // if the custom code is not 20000, it is judged as an error. 49 | if (res.code !== 20000) { 50 | Message({ 51 | message: res.message || 'Error', 52 | type: 'error', 53 | duration: 5 * 1000 54 | }) 55 | 56 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 57 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 58 | // to re-login 59 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { 60 | confirmButtonText: 'Re-Login', 61 | cancelButtonText: 'Cancel', 62 | type: 'warning' 63 | }).then(() => { 64 | store.dispatch('user/resetToken').then(() => { 65 | location.reload() 66 | }) 67 | }) 68 | } 69 | return Promise.reject(new Error(res.message || 'Error')) 70 | } else { 71 | return res 72 | } 73 | }, 74 | error => { 75 | console.log('err' + error) // for debug 76 | Message({ 77 | message: error.message, 78 | type: 'error', 79 | duration: 5 * 1000 80 | }) 81 | return Promise.reject(error) 82 | } 83 | ) 84 | 85 | export default service 86 | -------------------------------------------------------------------------------- /src/utils/scroll-to.js: -------------------------------------------------------------------------------- 1 | Math.easeInOutQuad = function(t, b, c, d) { 2 | t /= d / 2 3 | if (t < 1) { 4 | return c / 2 * t * t + b 5 | } 6 | t-- 7 | return -c / 2 * (t * (t - 2) - 1) + b 8 | } 9 | 10 | // requestAnimationFrame for Smart Animating http://goo.gl/sx5sts 11 | var requestAnimFrame = (function() { 12 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) } 13 | })() 14 | 15 | /** 16 | * Because it's so fucking difficult to detect the scrolling element, just move them all 17 | * @param {number} amount 18 | */ 19 | function move(amount) { 20 | document.documentElement.scrollTop = amount 21 | document.body.parentNode.scrollTop = amount 22 | document.body.scrollTop = amount 23 | } 24 | 25 | function position() { 26 | return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop 27 | } 28 | 29 | /** 30 | * @param {number} to 31 | * @param {number} duration 32 | * @param {Function} callback 33 | */ 34 | export function scrollTo(to, duration, callback) { 35 | const start = position() 36 | const change = to - start 37 | const increment = 20 38 | let currentTime = 0 39 | duration = (typeof (duration) === 'undefined') ? 500 : duration 40 | var animateScroll = function() { 41 | // increment the time 42 | currentTime += increment 43 | // find the value with the quadratic in-out easing function 44 | var val = Math.easeInOutQuad(currentTime, start, change, duration) 45 | // move the document.body 46 | move(val) 47 | // do the animation unless its over 48 | if (currentTime < duration) { 49 | requestAnimFrame(animateScroll) 50 | } else { 51 | if (callback && typeof (callback) === 'function') { 52 | // the animation is done so lets callback 53 | callback() 54 | } 55 | } 56 | } 57 | animateScroll() 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | 13 | /** 14 | * @param {string} str 15 | * @returns {Boolean} 16 | */ 17 | export function validUsername(str) { 18 | const valid_map = ['admin', 'editor'] 19 | return valid_map.indexOf(str.trim()) >= 0 20 | } 21 | 22 | /** 23 | * @param {string} url 24 | * @returns {Boolean} 25 | */ 26 | export function validURL(url) { 27 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 28 | return reg.test(url) 29 | } 30 | 31 | /** 32 | * @param {string} str 33 | * @returns {Boolean} 34 | */ 35 | export function validLowerCase(str) { 36 | const reg = /^[a-z]+$/ 37 | return reg.test(str) 38 | } 39 | 40 | /** 41 | * @param {string} str 42 | * @returns {Boolean} 43 | */ 44 | export function validUpperCase(str) { 45 | const reg = /^[A-Z]+$/ 46 | return reg.test(str) 47 | } 48 | 49 | /** 50 | * @param {string} str 51 | * @returns {Boolean} 52 | */ 53 | export function validAlphabets(str) { 54 | const reg = /^[A-Za-z]+$/ 55 | return reg.test(str) 56 | } 57 | 58 | /** 59 | * @param {string} email 60 | * @returns {Boolean} 61 | */ 62 | export function validEmail(email) { 63 | const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 64 | return reg.test(email) 65 | } 66 | 67 | /** 68 | * @param {string} str 69 | * @returns {Boolean} 70 | */ 71 | export function isString(str) { 72 | if (typeof str === 'string' || str instanceof String) { 73 | return true 74 | } 75 | return false 76 | } 77 | 78 | /** 79 | * @param {Array} arg 80 | * @returns {Boolean} 81 | */ 82 | export function isArray(arg) { 83 | if (typeof Array.isArray === 'undefined') { 84 | return Object.prototype.toString.call(arg) === '[object Array]' 85 | } 86 | return Array.isArray(arg) 87 | } 88 | -------------------------------------------------------------------------------- /src/views/article/components/Dropdown/Comment.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 42 | -------------------------------------------------------------------------------- /src/views/article/components/Dropdown/PublishType.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 47 | -------------------------------------------------------------------------------- /src/views/article/components/Dropdown/index.js: -------------------------------------------------------------------------------- 1 | export { default as CommentDropdown } from './Comment' 2 | export { default as PublishType } from './PublishType' 3 | -------------------------------------------------------------------------------- /src/views/article/components/Warning.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/article/create.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | -------------------------------------------------------------------------------- /src/views/article/edit.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/dashboard/components/CommentTable.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 89 | -------------------------------------------------------------------------------- /src/views/dashboard/components/LineChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 117 | -------------------------------------------------------------------------------- /src/views/dashboard/components/PieChart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 87 | -------------------------------------------------------------------------------- /src/views/dashboard/components/TodoList/Todo.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 82 | -------------------------------------------------------------------------------- /src/views/dashboard/components/mixins/resize.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '@/utils' 2 | 3 | export default { 4 | data() { 5 | return { 6 | $_sidebarElm: null, 7 | $_resizeHandler: null 8 | } 9 | }, 10 | mounted() { 11 | this.$_resizeHandler = debounce(() => { 12 | if (this.chart) { 13 | this.chart.resize() 14 | } 15 | }, 100) 16 | this.$_initResizeEvent() 17 | this.$_initSidebarResizeEvent() 18 | }, 19 | beforeDestroy() { 20 | this.$_destroyResizeEvent() 21 | this.$_destroySidebarResizeEvent() 22 | }, 23 | // to fixed bug when cached by keep-alive 24 | // 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/profile/about.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 47 | -------------------------------------------------------------------------------- /src/views/profile/base.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 60 | -------------------------------------------------------------------------------- /src/views/profile/components/Account.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 60 | -------------------------------------------------------------------------------- /src/views/setting/storage.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 57 | 58 | 64 | -------------------------------------------------------------------------------- /src/views/setting/sys.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 90 | 91 | 97 | -------------------------------------------------------------------------------- /src/views/setting/ui.vue: -------------------------------------------------------------------------------- 1 | 29 | 81 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------