├── .browserslistrc ├── .cz-config.js ├── .editorconfig ├── .env.development ├── .env.production ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── commitlint.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── api │ ├── article.js │ ├── permission.js │ ├── role.js │ ├── sys.js │ ├── user-manage.js │ └── user.js ├── assets │ └── logo.png ├── components │ ├── Breadcrumb │ │ └── index.vue │ ├── Guide │ │ ├── index.vue │ │ └── steps.js │ ├── Hamburger │ │ └── index.vue │ ├── HeaderSearch │ │ ├── FuseData.js │ │ └── index.vue │ ├── LangSelect │ │ └── index.vue │ ├── PanThumb │ │ └── index.vue │ ├── Screenfull │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ ├── TagsView │ │ ├── ContextMenu.vue │ │ └── index.vue │ ├── ThemeSelect │ │ ├── components │ │ │ └── SelectColor.vue │ │ └── index.vue │ └── UploadExcel │ │ ├── index.vue │ │ └── utils.js ├── constant │ ├── formula.json │ └── index.js ├── directives │ ├── index.js │ └── permission.js ├── filter │ └── index.js ├── i18n │ ├── index.js │ └── lang │ │ ├── en.js │ │ └── zh.js ├── icons │ ├── index.js │ └── svg │ │ ├── article-create.svg │ │ ├── article-ranking.svg │ │ ├── article.svg │ │ ├── change-theme.svg │ │ ├── dashboard.svg │ │ ├── example.svg │ │ ├── exit-fullscreen.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── fullscreen.svg │ │ ├── guide.svg │ │ ├── hamburger-closed.svg │ │ ├── hamburger-opened.svg │ │ ├── international.svg │ │ ├── introduce.svg │ │ ├── language.svg │ │ ├── link.svg │ │ ├── nested.svg │ │ ├── password.svg │ │ ├── permission.svg │ │ ├── personnel-info.svg │ │ ├── personnel-manage.svg │ │ ├── personnel.svg │ │ ├── reward.svg │ │ ├── role.svg │ │ ├── search.svg │ │ ├── table.svg │ │ ├── tree.svg │ │ └── user.svg ├── layout │ ├── components │ │ ├── AppMain.vue │ │ ├── Navbar.vue │ │ └── Sidebar │ │ │ ├── MenuItem.vue │ │ │ ├── SidebarItem.vue │ │ │ ├── SidebarMenu.vue │ │ │ └── index.vue │ └── index.vue ├── main.js ├── permission.js ├── plugins │ └── element.js ├── router │ ├── index.js │ └── modules │ │ ├── Article.js │ │ ├── ArticleCreate.js │ │ ├── PermissionList.js │ │ ├── RoleList.js │ │ └── UserManage.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── permission.js │ │ ├── theme.js │ │ └── user.js ├── styles │ ├── element.scss │ ├── index.scss │ ├── mixin.scss │ ├── sidebar.scss │ ├── transition.scss │ └── variables.scss ├── utils │ ├── Export2Excel.js │ ├── auth.js │ ├── i18n.js │ ├── request.js │ ├── route.js │ ├── storage.js │ ├── tags.js │ ├── theme.js │ └── validate.js └── views │ ├── article-create │ ├── components │ │ ├── Editor.vue │ │ ├── Markdown.vue │ │ └── commit.js │ └── index.vue │ ├── article-detail │ └── index.vue │ ├── article-ranking │ ├── dynamic │ │ ├── DynamicData.js │ │ └── index.js │ ├── index.vue │ └── sortable │ │ └── index.js │ ├── error-page │ ├── 401.vue │ └── 404.vue │ ├── import │ ├── index.vue │ └── utils.js │ ├── login │ ├── index.vue │ └── rules.js │ ├── permission-list │ └── index.vue │ ├── profile │ ├── components │ │ ├── Author.vue │ │ ├── Chapter.vue │ │ ├── Feature.vue │ │ └── ProjectCard.vue │ └── index.vue │ ├── role-list │ ├── components │ │ └── DistributePermission.vue │ └── index.vue │ ├── user-info │ └── index.vue │ └── user-manage │ ├── components │ ├── Export2Excel.vue │ ├── Export2ExcelConstants.js │ └── roles.vue │ └── index.vue └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 可选类型 3 | types: [ 4 | { value: 'feat', name: 'feat: 新功能' }, 5 | { value: 'fix', name: 'fix: 修复' }, 6 | { value: 'docs', name: 'docs: 文档变更' }, 7 | { value: 'style', name: 'style: 代码格式(不影响代码运行的变动)' }, 8 | { value: 'refactor', name: 'refactor: 重构代码' }, 9 | { value: 'perf', name: 'perf: 性能优化' }, 10 | { value: 'test', name: 'test: 测试' }, 11 | { value: 'chore', name: 'chore: 构建过程或辅助工具的变动' }, 12 | { value: 'revert', name: 'revert: 回滚' }, 13 | { value: 'build', name: 'build: 打包' } 14 | ], 15 | // 消息步骤 16 | messages: { 17 | type: '选择你的提交类型:', 18 | customScope: '选择你的修改范围(可选):', 19 | subject: '请简要描述提交(必填):', 20 | body: '请输入详细内容(可选):', 21 | footer: '请输入要关闭的issue(可选):', 22 | confirmCommit: '确认提交?(y/n)' 23 | }, 24 | // 跳过问题 25 | skipQuestions: ['body', 'footer'], 26 | // subject文字长度默认72 27 | subjectLimit: 72 28 | } 29 | 30 | // module.exports = { 31 | // types: [ 32 | // { value: 'feat', name: 'feat: A new feature' }, 33 | // { value: 'fix', name: 'fix: A bug fix' }, 34 | // { value: 'docs', name: 'docs: Documentation only changes' }, 35 | // { 36 | // value: 'style', 37 | // name: 38 | // 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)', 39 | // }, 40 | // { 41 | // value: 'refactor', 42 | // name: 'refactor: A code change that neither fixes a bug nor adds a feature', 43 | // }, 44 | // { 45 | // value: 'perf', 46 | // name: 'perf: A code change that improves performance', 47 | // }, 48 | // { value: 'test', name: 'test: Adding missing tests' }, 49 | // { 50 | // value: 'chore', 51 | // name: 52 | // 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation', 53 | // }, 54 | // { value: 'revert', name: 'revert: Revert to a commit' }, 55 | // { value: 'WIP', name: 'WIP: Work in progress' }, 56 | // ], 57 | 58 | // scopes: [{ name: 'accounts' }, { name: 'admin' }, { name: 'exampleScope' }, { name: 'changeMe' }], 59 | 60 | // allowTicketNumber: false, 61 | // isTicketNumberRequired: false, 62 | // ticketNumberPrefix: 'TICKET-', 63 | // ticketNumberRegExp: '\\d{1,5}', 64 | 65 | // // it needs to match the value for field type. Eg.: 'fix' 66 | // /* 67 | // scopeOverrides: { 68 | // fix: [ 69 | // {name: 'merge'}, 70 | // {name: 'style'}, 71 | // {name: 'e2eTest'}, 72 | // {name: 'unitTest'} 73 | // ] 74 | // }, 75 | // */ 76 | // // override the messages, defaults are as follows 77 | // messages: { 78 | // type: "Select the type of change that you're committing:", 79 | // scope: '\nDenote the SCOPE of this change (optional):', 80 | // // used if allowCustomScopes is true 81 | // customScope: 'Denote the SCOPE of this change:', 82 | // subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n', 83 | // body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n', 84 | // breaking: 'List any BREAKING CHANGES (optional):\n', 85 | // footer: 'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n', 86 | // confirmCommit: 'Are you sure you want to proceed with the commit above?', 87 | // }, 88 | 89 | // allowCustomScopes: true, 90 | // allowBreakingChanges: ['feat', 'fix'], 91 | // // skip any questions you want 92 | // skipQuestions: ['body'], 93 | 94 | // // limit subject length 95 | // subjectLimit: 100, 96 | // // breaklineChar: '|', // It is supported for fields body and footer. 97 | // // footerPrefix : 'ISSUES CLOSED:' 98 | // // askForBreakingChangeFirst : true, // default is false 99 | // }; 100 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 标志 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/api' -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 标志 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/prod-api' -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // 表示当前为根目录, ESlint规则被限制在此目录下 4 | root: true, 5 | 6 | // env 表示启用ESLint 检测的环境,在此处指定为node环境 7 | env: { 8 | node: true 9 | }, 10 | 11 | // ESLint 中基础配置需要继承的配置 12 | extends: [ 13 | 'plugin:vue/vue3-essential', 14 | '@vue/standard' 15 | ], 16 | 17 | // 解析器 18 | parserOptions: { 19 | parser: 'babel-eslint' 20 | }, 21 | 22 | // 需要修改的启用规则及其各自级别 23 | 24 | /** 25 | * 错误级别分为三种: 26 | * “off"或”0“ - 关闭规则 27 | * “warn”或”1“ - 开启规则, 使用警告级别的错误: warn (不会导致程序退出) 28 | * “error”或”2“ - 开启规则,使用错误级别的错误:error (会导致程序退出) 29 | */ 30 | rules: { 31 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 32 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 33 | 'space-before-function-paren': 'off' 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # npx eslint --ext .js,.vue src 5 | npx lint-staged -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 skylqflin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imooc-admin 2 | 3 | ## 描述 4 | 5 | 这个项目改写于vue-element-admin 6 | 7 | > 如果这个项目能够帮助到你,请记得star哦! 8 | 9 | 功能: 10 | - 基于i18n实现国际化 11 | - 基于scss实现的动态换肤 12 | - 实现了excel导入导出 13 | - 基于wangEditor和tui.editor实现了富文本和markdown编辑功能 14 | - 基于sortablejs实现的拖拽排序 15 | - 基于vue-print-nb实现的打印功能 16 | 17 | ## 项目运行 18 | 19 | - 安装依赖 20 | ``` 21 | npm install 22 | ``` 23 | - 启动项目 24 | ``` 25 | npm run serve 26 | ``` 27 | - 打包项目 28 | ``` 29 | npm run build 30 | ``` 31 | - 发布项目 32 | ``` 33 | npm run release 34 | ``` 35 | 36 | ### 项目后台 37 | 38 | - 地址[imooc-api](https://github.com/mafqla/imooc-api.git/) 39 | - 后台项目的接口文档地址[imooc-api-doc](https://www.apifox.cn/apidoc/shared-e909ede9-d941-4078-994b-7046e54a9f2a) 40 | - 后台的文章排序接口未完成,能有实现的请提交你的代码到后台项目的仓库 41 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 继承规则 3 | extends: ['@commitlint/config-conventional'], 4 | // 定义规则 5 | roles: { 6 | // type的类型定义 7 | 'type-enum': [ 8 | // 提交类型 9 | // 当前验证错误的级别 10 | 2, 11 | // 在什么情况可以验证 12 | 'always', 13 | // 泛型内容 14 | [ 15 | 'feat', // 新功能 16 | 'fix', // 修复 17 | 'docs', // 文档变更 18 | 'style', // 代码格式(不影响代码运行的变动) 19 | 'refactor', // 重构代码 20 | 'perf', // 性能优化 21 | 'test', // 测试 22 | 'chore', // 构建过程或辅助工具的变动 23 | 'revert', // 回滚 24 | 'build' // 打包 25 | ] 26 | ], 27 | // subject大小写不做校验 28 | 'subject-case': [0] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imooc-admin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "prepare": "husky install" 10 | }, 11 | "dependencies": { 12 | "@element-plus/icons-vue": "^1.1.4", 13 | "@toast-ui/editor": "^3.0.2", 14 | "axios": "^0.26.1", 15 | "core-js": "^3.6.5", 16 | "css-color-function": "^1.3.3", 17 | "dayjs": "^1.10.6", 18 | "driver.js": "^0.9.8", 19 | "element-plus": "^2.1.7", 20 | "file-saver": "^2.0.5", 21 | "fuse.js": "^6.4.6", 22 | "i18next": "^20.4.0", 23 | "md5": "^2.3.0", 24 | "rgb-hex": "^4.0.0", 25 | "screenfull": "^5.1.0", 26 | "sortablejs": "^1.14.0", 27 | "vue": "^3.2.8", 28 | "vue-i18n": "^9.2.0-beta.33", 29 | "vue-router": "^4.0.11", 30 | "vue3-print-nb": "^0.1.4", 31 | "vuex": "^4.0.2", 32 | "wangeditor": "^4.7.6", 33 | "xlsx": "^0.17.0" 34 | }, 35 | "devDependencies": { 36 | "@commitlint/cli": "^12.1.4", 37 | "@commitlint/config-conventional": "^12.1.4", 38 | "@vue/cli-plugin-babel": "~4.5.15", 39 | "@vue/cli-plugin-eslint": "~4.5.15", 40 | "@vue/cli-plugin-router": "~4.5.15", 41 | "@vue/cli-plugin-vuex": "~4.5.15", 42 | "@vue/cli-service": "~4.5.15", 43 | "@vue/compiler-sfc": "^3.0.0", 44 | "@vue/eslint-config-standard": "^5.1.2", 45 | "babel-eslint": "^10.1.0", 46 | "cz-customizable": "^6.3.0", 47 | "eslint": "^6.7.2", 48 | "eslint-plugin-import": "^2.20.2", 49 | "eslint-plugin-node": "^11.1.0", 50 | "eslint-plugin-promise": "^4.2.1", 51 | "eslint-plugin-standard": "^4.0.0", 52 | "eslint-plugin-vue": "^7.0.0", 53 | "husky": "^7.0.1", 54 | "sass": "^1.26.5", 55 | "sass-loader": "^8.0.2", 56 | "svg-sprite-loader": "^6.0.9", 57 | "vue-cli-plugin-element-plus": "~0.0.13" 58 | }, 59 | "config": { 60 | "commitizen": { 61 | "path": "./node_modules/cz-customizable" 62 | } 63 | }, 64 | "gitHooks": { 65 | "pre-commit": "lint-staged" 66 | }, 67 | "lint-staged": { 68 | "src/**/*.{js,vue}": [ 69 | "eslint --fix", 70 | "git add" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mafqla/imooc-admin/4a1577e9103def69125133a0905526294fa43a68/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/api/article.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 获取列表数据 5 | */ 6 | export const getArticleList = (data) => { 7 | return request({ 8 | url: '/article/list', 9 | params: data 10 | }) 11 | } 12 | /** 13 | * 修改排序 14 | */ 15 | export const articleSort = (data) => { 16 | return request({ 17 | url: '/article/sort', 18 | method: 'POST', 19 | data 20 | }) 21 | } 22 | /** 23 | * 删除文章 24 | */ 25 | export const deleteArticle = (articleId) => { 26 | return request({ 27 | url: `/article/delete/${articleId}` 28 | }) 29 | } 30 | /** 31 | * 获取文章详情 32 | */ 33 | export const articleDetail = (articleId) => { 34 | return request({ 35 | url: `/article/${articleId}` 36 | }) 37 | } 38 | /** 39 | * 创建文章 40 | */ 41 | export const createArticle = (data) => { 42 | return request({ 43 | url: '/article/create', 44 | method: 'POST', 45 | data 46 | }) 47 | } 48 | /** 49 | * 编辑文章详情 50 | */ 51 | export const articleEdit = (data) => { 52 | return request({ 53 | url: '/article/edit', 54 | method: 'POST', 55 | data 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/api/permission.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 获取所有权限 5 | */ 6 | export const permissionList = () => { 7 | return request({ 8 | url: '/permission/list' 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/api/role.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 获取所有角色 5 | */ 6 | export const roleList = () => { 7 | return request({ 8 | url: '/role/list' 9 | }) 10 | } 11 | /** 12 | * 获取指定角色的权限 13 | */ 14 | export const rolePermission = (roleId) => { 15 | return request({ 16 | url: `/role/permission/${roleId}` 17 | }) 18 | } 19 | /** 20 | * 为角色修改权限 21 | */ 22 | export const distributePermission = (data) => { 23 | return request({ 24 | url: '/role/distribute-permission', 25 | method: 'POST', 26 | data 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/api/sys.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 登录 5 | */ 6 | export const login = data => { 7 | return request({ 8 | url: '/sys/login', 9 | method: 'POST', 10 | data 11 | }) 12 | } 13 | 14 | /** 15 | * 获取用户信息 16 | */ 17 | export const getUserInfo = () => { 18 | return request({ 19 | url: '/sys/profile' 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/api/user-manage.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | /** 4 | * 获取用户列表数据 5 | */ 6 | export const getUserManageList = (data) => { 7 | return request({ 8 | url: '/user-manage/list', 9 | params: data 10 | }) 11 | } 12 | /** 13 | * 获取所有用户列表数据 14 | */ 15 | export const getUserManageAllList = () => { 16 | return request({ 17 | url: '/user-manage/all-list' 18 | }) 19 | } 20 | /** 21 | * 批量导入 22 | */ 23 | export const userBatchImport = (data) => { 24 | return request({ 25 | url: '/user-manage/batch/import', 26 | method: 'POST', 27 | data 28 | }) 29 | } 30 | /** 31 | * 删除指定数据 32 | */ 33 | export const deleteUser = (id) => { 34 | return request({ 35 | url: `/user-manage/detele/${id}` 36 | }) 37 | } 38 | /** 39 | * 获取用户详情 40 | */ 41 | export const userDetail = (id) => { 42 | return request({ 43 | url: `/user-manage/detail/${id}` 44 | }) 45 | } 46 | /** 47 | * 获取指定用户角色 48 | */ 49 | export const userRoles = (id) => { 50 | return request({ 51 | url: `/user-manage/role/${id}` 52 | }) 53 | } 54 | /** 55 | * 分用户分配角色 56 | */ 57 | export const updateRole = (id, roles) => { 58 | return request({ 59 | url: `/user-manage/update-role/${id}`, 60 | method: 'POST', 61 | data: { 62 | roles 63 | } 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export const feature = () => { 4 | return request({ 5 | url: '/user/feature' 6 | }) 7 | } 8 | 9 | // 获取章节模块 10 | export const chapter = () => { 11 | return request({ 12 | url: '/user/chapter' 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mafqla/imooc-admin/4a1577e9103def69125133a0905526294fa43a68/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 59 | 60 | 83 | -------------------------------------------------------------------------------- /src/components/Guide/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/Guide/steps.js: -------------------------------------------------------------------------------- 1 | // 此处不要导入 @/i18n 使用 i18n.global ,因为我们在 router 中 layout 不是按需加载,所以会在 Guide 会在 I18n 初始化完成之前被直接调用。导致 i18n 为 undefined 2 | const steps = (i18n) => { 3 | return [ 4 | { 5 | element: '#guide-start', 6 | popover: { 7 | title: i18n.t('msg.guide.guideTitle'), 8 | description: i18n.t('msg.guide.guideDesc'), 9 | position: 'bottom-right' 10 | } 11 | }, 12 | { 13 | element: '#guide-hamburger', 14 | popover: { 15 | title: i18n.t('msg.guide.hamburgerTitle'), 16 | description: i18n.t('msg.guide.hamburgerDesc') 17 | } 18 | }, 19 | { 20 | element: '#guide-breadcrumb', 21 | popover: { 22 | title: i18n.t('msg.guide.breadcrumbTitle'), 23 | description: i18n.t('msg.guide.breadcrumbDesc') 24 | } 25 | }, 26 | { 27 | element: '#guide-search', 28 | popover: { 29 | title: i18n.t('msg.guide.searchTitle'), 30 | description: i18n.t('msg.guide.searchDesc'), 31 | position: 'bottom-right' 32 | } 33 | }, 34 | { 35 | element: '#guide-full', 36 | popover: { 37 | title: i18n.t('msg.guide.fullTitle'), 38 | description: i18n.t('msg.guide.fullDesc'), 39 | position: 'bottom-right' 40 | } 41 | }, 42 | { 43 | element: '#guide-theme', 44 | popover: { 45 | title: i18n.t('msg.guide.themeTitle'), 46 | description: i18n.t('msg.guide.themeDesc'), 47 | position: 'bottom-right' 48 | } 49 | }, 50 | { 51 | element: '#guide-lang', 52 | popover: { 53 | title: i18n.t('msg.guide.langTitle'), 54 | description: i18n.t('msg.guide.langDesc'), 55 | position: 'bottom-right' 56 | } 57 | }, 58 | { 59 | element: '#guide-tags', 60 | popover: { 61 | title: i18n.t('msg.guide.tagTitle'), 62 | description: i18n.t('msg.guide.tagDesc') 63 | } 64 | }, 65 | { 66 | element: '#guide-sidebar', 67 | popover: { 68 | title: i18n.t('msg.guide.sidebarTitle'), 69 | description: i18n.t('msg.guide.sidebarDesc'), 70 | position: 'right-center' 71 | } 72 | } 73 | ] 74 | } 75 | export default steps 76 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 32 | -------------------------------------------------------------------------------- /src/components/HeaderSearch/FuseData.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import i18n from '@/i18n' 3 | /** 4 | * 筛选出可供搜索的路由对象 5 | * @param routes 路由表 6 | * @param basePath 基础路径,默认为 / 7 | * @param prefixTitle 8 | */ 9 | export const generateRoutes = (routes, basePath = '/', prefixTitle = []) => { 10 | // 创建 result 数据 11 | let res = [] 12 | // 循环 routes 路由 13 | for (const route of routes) { 14 | // 创建包含 path 和 title 的 item 15 | const data = { 16 | path: path.resolve(basePath, route.path), 17 | title: [...prefixTitle] 18 | } 19 | // 当前存在 meta 时,使用 i18n 解析国际化数据,组合成新的 title 内容 20 | // 动态路由不允许被搜索 21 | // 匹配动态路由的正则 22 | const re = /.*\/:.*/ 23 | if (route.meta && route.meta.title && !re.exec(route.path)) { 24 | const i18ntitle = i18n.global.t(`msg.route.${route.meta.title}`) 25 | data.title = [...data.title, i18ntitle] 26 | res.push(data) 27 | } 28 | 29 | // 存在 children 时,迭代调用 30 | if (route.children) { 31 | const tempRoutes = generateRoutes(route.children, data.path, data.title) 32 | if (tempRoutes.length >= 1) { 33 | res = [...res, ...tempRoutes] 34 | } 35 | } 36 | } 37 | return res 38 | } 39 | -------------------------------------------------------------------------------- /src/components/HeaderSearch/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 130 | 131 | 167 | -------------------------------------------------------------------------------- /src/components/LangSelect/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 53 | -------------------------------------------------------------------------------- /src/components/PanThumb/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | 128 | -------------------------------------------------------------------------------- /src/components/Screenfull/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 45 | 46 | 61 | -------------------------------------------------------------------------------- /src/components/TagsView/ContextMenu.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 45 | 46 | 68 | -------------------------------------------------------------------------------- /src/components/TagsView/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 95 | 96 | 158 | -------------------------------------------------------------------------------- /src/components/ThemeSelect/components/SelectColor.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 82 | 83 | 91 | -------------------------------------------------------------------------------- /src/components/ThemeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/UploadExcel/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 144 | 145 | 176 | -------------------------------------------------------------------------------- /src/components/UploadExcel/utils.js: -------------------------------------------------------------------------------- 1 | import XLSX from 'xlsx' 2 | /** 3 | * 获取表头(通用方式) 4 | */ 5 | export const getHeaderRow = (sheet) => { 6 | const headers = [] 7 | const range = XLSX.utils.decode_range(sheet['!ref']) 8 | let C 9 | const R = range.s.r 10 | /* start in the first row */ 11 | for (C = range.s.c; C <= range.e.c; ++C) { 12 | /* walk every column in the range */ 13 | const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })] 14 | /* find the cell in the first row */ 15 | let hdr = 'UNKNOWN ' + C // <-- replace with your desired default 16 | if (cell && cell.t) hdr = XLSX.utils.format_cell(cell) 17 | headers.push(hdr) 18 | } 19 | return headers 20 | } 21 | export const isExcel = (file) => { 22 | return /\.(xlsx|xls|csv)$/.test(file.name) 23 | } 24 | -------------------------------------------------------------------------------- /src/constant/formula.json: -------------------------------------------------------------------------------- 1 | { 2 | "shade-1": "color(primary shade(10%))", 3 | "light-1": "color(primary tint(10%))", 4 | "light-2": "color(primary tint(20%))", 5 | "light-3": "color(primary tint(30%))", 6 | "light-4": "color(primary tint(40%))", 7 | "light-5": "color(primary tint(50%))", 8 | "light-6": "color(primary tint(60%))", 9 | "light-7": "color(primary tint(70%))", 10 | "light-8": "color(primary tint(80%))", 11 | "light-9": "color(primary tint(90%))", 12 | "subMenuHover": "color(primary tint(70%))", 13 | "subMenuBg": "color(primary tint(80%))", 14 | "menuHover": "color(primary tint(90%))", 15 | "menuBg": "color(primary)" 16 | } -------------------------------------------------------------------------------- /src/constant/index.js: -------------------------------------------------------------------------------- 1 | export const TOKEN = 'token' 2 | // token 时间戳 3 | export const TIME_STAMP = 'timeStamp' 4 | // 超时时长(毫秒) 两小时 5 | export const TOKEN_TIMEOUT_VALUE = 2 * 3600 * 1000 6 | // 国际化 7 | export const LANG = 'language' 8 | // 主题色保存的 key 9 | export const MAIN_COLOR = 'mainColor' 10 | // 默认色值 11 | export const DEFAULT_COLOR = '#409eff' 12 | // tags 13 | export const TAGS_VIEW = 'tagsView' 14 | -------------------------------------------------------------------------------- /src/directives/index.js: -------------------------------------------------------------------------------- 1 | import print from 'vue3-print-nb' 2 | import permission from './permission' 3 | 4 | export default (app) => { 5 | app.use(print) 6 | app.directive('permission', permission) 7 | } 8 | -------------------------------------------------------------------------------- /src/directives/permission.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | function checkPermission(el, binding) { 4 | // 获取绑定的值,此处为权限 5 | const { value } = binding 6 | // 获取所有的功能指令 7 | const points = store.getters.userInfo.permission.points 8 | // 当传入的指令集为数组时 9 | if (value && value instanceof Array) { 10 | // 匹配对应的指令 11 | const hasPermission = points.some((point) => { 12 | return value.includes(point) 13 | }) 14 | // 如果无法匹配,则表示当前用户无该指令,那么删除对应的功能按钮 15 | if (!hasPermission) { 16 | el.parentNode && el.parentNode.removeChild(el) 17 | } 18 | } else { 19 | // eslint-disabled-next-line 20 | throw new Error('v-permission value is ["admin","editor"]') 21 | } 22 | } 23 | 24 | export default { 25 | // 在绑定元素的父组件被挂载后调用 26 | mounted(el, binding) { 27 | checkPermission(el, binding) 28 | }, 29 | // 在包含组件的 VNode 及其子组件的 VNode 更新后调用 30 | update(el, binding) { 31 | checkPermission(el, binding) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/filter/index.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import rt from 'dayjs/plugin/relativeTime' 3 | // 语言包 4 | import 'dayjs/locale/zh-cn' 5 | import store from '@/store' 6 | 7 | export const dateFilter = (val, format = 'YYYY-MM-DD') => { 8 | if (!isNaN(val)) { 9 | val = parseInt(val) 10 | } 11 | 12 | return dayjs(val).format(format) 13 | } 14 | // 加载相对时间插件 15 | dayjs.extend(rt) 16 | function relativeTime(val) { 17 | if (!isNaN(val)) { 18 | val = parseInt(val) 19 | } 20 | return dayjs() 21 | .locale(store.getters.language === 'zh' ? 'zh-cn' : 'en') 22 | .to(dayjs(val)) 23 | } 24 | export default (app) => { 25 | app.config.globalProperties.$filters = { 26 | dateFilter, 27 | relativeTime 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import mZhLocale from './lang/zh' 3 | import mEnLocale from './lang/en' 4 | import store from '@/store' 5 | 6 | const messages = { 7 | en: { 8 | msg: { 9 | ...mEnLocale 10 | } 11 | }, 12 | zh: { 13 | msg: { 14 | ...mZhLocale 15 | } 16 | } 17 | } 18 | 19 | // const locale = 'zh' 20 | /** 21 | * 返回当前 lang 22 | */ 23 | function getLanguage() { 24 | return store && store.getters && store.getters.language 25 | } 26 | 27 | const i18n = createI18n({ 28 | // 使用 Composition API 模式,则需要将其设置为false 29 | legacy: false, 30 | // 全局注入 $t 函数 31 | globalInjection: true, 32 | locale: getLanguage() || 'zh', 33 | messages 34 | }) 35 | 36 | export default i18n 37 | -------------------------------------------------------------------------------- /src/i18n/lang/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | login: { 3 | title: 'User Login', 4 | loginBtn: 'Login', 5 | usernameRule: 'Username is required', 6 | passwordRule: 'Password cannot be less than 6 digits', 7 | desc: ` 8 | Test authority account:
9 | Provide three kinds of authority accounts:
10 | 1. Super administrator account: super-admin
11 | 2. Administrator account: admin
12 | 3. Test configurable account: test
13 | The uniform password is: 123456
14 |
15 | Import user account:
16 | You can log in with the imported username
17 | The password is unified as: 123456
18 | Note: Import user-discriminatory Chinese and English libraries! ! ! ! 19 | ` 20 | }, 21 | route: { 22 | profile: 'Profile', 23 | user: 'user', 24 | excelImport: 'ExcelImport', 25 | userManage: 'EmployeeManage', 26 | userInfo: 'UserInfo', 27 | roleList: 'RoleList', 28 | permissionList: 'PermissionList', 29 | article: 'article', 30 | articleRanking: 'ArticleRanking', 31 | articleCreate: 'ArticleCreate', 32 | articleDetail: 'ArticleDetail', 33 | articleEditor: 'ArticleEditor' 34 | }, 35 | toast: { 36 | switchLangSuccess: 'Switch Language Success' 37 | }, 38 | tagsView: { 39 | refresh: 'Refresh', 40 | closeRight: 'Close Rights', 41 | closeOther: 'Close Others' 42 | }, 43 | theme: { 44 | themeColorChange: 'Theme Color Change', 45 | themeChange: 'Theme Change' 46 | }, 47 | universal: { 48 | title: 'reminder', 49 | confirm: 'confirm', 50 | cancel: 'cancel' 51 | }, 52 | navBar: { 53 | themeChange: 'Theme Modification', 54 | headerSearch: 'Page Search', 55 | screenfull: 'Full Screen Replacement', 56 | lang: 'Globalization', 57 | guide: 'Function Guide', 58 | home: 'Home', 59 | course: 'Course homepage', 60 | logout: 'Log out' 61 | }, 62 | guide: { 63 | close: 'close', 64 | next: 'next', 65 | prev: 'previous', 66 | guideTitle: 'guidance', 67 | guideDesc: 'Turn on the boot function', 68 | hamburgerTitle: 'Hamburger button', 69 | hamburgerDesc: 'Open and close the left menu', 70 | breadcrumbTitle: 'Bread crumbs', 71 | breadcrumbDesc: 'Indicates the current page position', 72 | searchTitle: 'search', 73 | searchDesc: 'Page link search', 74 | fullTitle: 'full screen', 75 | fullDesc: 'Page display switching', 76 | themeTitle: 'theme', 77 | themeDesc: 'Change project theme', 78 | langTitle: 'globalization', 79 | langDesc: 'Language switch', 80 | tagTitle: 'Label', 81 | tagDesc: 'Opened page tab', 82 | sidebarTitle: 'menu', 83 | sidebarDesc: 'Project function menu' 84 | }, 85 | profile: { 86 | muted: 87 | '"Vue3 rewrite vue-element-admin, realize the back-end front-end integrated solution" project demonstration', 88 | introduce: 'Introduce', 89 | projectIntroduction: 'Project Introduction', 90 | projectFunction: 'Project Function', 91 | feature: 'Feature', 92 | chapter: 'Chapter', 93 | author: 'Author', 94 | name: 'Sunday', 95 | job: 'A front-end development program', 96 | Introduction: 97 | 'A senior technical expert, once worked in a domestic first-line Internet company, and has coordinated multiple large-scale projects with more than tens of millions of users. Committed to researching big front-end technology, he has been invited to participate in domestic front-end technology sharing sessions many times, such as: Google China Technology Sharing Session in 2018.' 98 | }, 99 | userInfo: { 100 | print: 'Print', 101 | title: 'Employee information', 102 | name: 'name', 103 | sex: 'gender', 104 | nation: 'nationality', 105 | mobile: 'phone number', 106 | province: 'Place of residence', 107 | date: 'Entry Time', 108 | remark: 'Remark', 109 | address: 'contact address', 110 | experience: 'Experience', 111 | major: 'Professional', 112 | glory: 'Glory', 113 | foot: 'Signature:___________Date:___________' 114 | }, 115 | uploadExcel: { 116 | upload: 'Click upload', 117 | drop: 'Drag files here' 118 | }, 119 | excel: { 120 | importExcel: 'excel import', 121 | exportExcel: 'excel export', 122 | exportZip: 'zip export', 123 | name: 'Name', 124 | mobile: 'contact details', 125 | avatar: 'Avatar', 126 | role: 'Role', 127 | openTime: 'Opening time', 128 | action: 'Operate', 129 | show: 'Check', 130 | showRole: 'Role', 131 | defaultRole: 'Staff', 132 | remove: 'delete', 133 | removeSuccess: 'Deleted successfully', 134 | title: 'Export to excel', 135 | placeholder: 'excel file name', 136 | defaultName: 'Staff Management Form', 137 | close: 'Cancel', 138 | confirm: 'Export', 139 | importSuccess: ' Employee data imported successfully', 140 | dialogTitle1: 'Are you sure you want to delete the user ', 141 | dialogTitle2: ' Is it?', 142 | roleDialogTitle: 'Configure roles' 143 | }, 144 | role: { 145 | buttonTxt: 'New Role', 146 | index: 'Serial number', 147 | name: 'name', 148 | desc: 'describe', 149 | action: 'operate', 150 | assignPermissions: 'assign permissions', 151 | removeRole: 'Delete role', 152 | dialogTitle: 'New role', 153 | dialogRole: 'Role Name', 154 | dialogDesc: 'Role description', 155 | updateRoleSuccess: 'User role updated successfully' 156 | }, 157 | permission: { 158 | name: 'Authority name', 159 | mark: 'Authority ID', 160 | desc: 'Permission description' 161 | }, 162 | article: { 163 | ranking: 'Ranking', 164 | title: 'Title', 165 | author: 'Author', 166 | publicDate: 'release time', 167 | desc: 'brief introduction', 168 | action: 'operate', 169 | dynamicTitle: 'Dynamic display', 170 | show: 'check', 171 | remove: 'delete', 172 | edit: 'editor', 173 | dialogTitle1: 'Are you sure you want to delete the article ', 174 | dialogTitle2: ' NS?', 175 | removeSuccess: 'Article deleted successfully', 176 | titlePlaceholder: 'Please enter the title of the article', 177 | markdown: 'Markdown', 178 | richText: 'Rich Text', 179 | commit: 'commit', 180 | createSuccess: 'The article was created successfully', 181 | editorSuccess: 'Article modified successfully', 182 | sortSuccess: 'Article ranking modified successfully' 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/i18n/lang/zh.js: -------------------------------------------------------------------------------- 1 | export default { 2 | login: { 3 | title: '用户登录', 4 | loginBtn: '登录', 5 | usernameRule: '用户名为必填项', 6 | passwordRule: '密码不能少于6位', 7 | desc: ` 8 | 测试权限账号:
9 | 提供三种权限账号:
10 | 1. 超级管理员账号: super-admin
11 | 2. 管理员账号:admin
12 | 3. 测试可配置账号:test
13 | 密码统一为:123456
14 |
15 | 导入用户账号:
16 | 可使用导入的用户名登录
17 | 密码统一为:123456
18 | 注意:导入用户区分中英文库!!!! 19 | ` 20 | }, 21 | route: { 22 | profile: '个人中心', 23 | user: '用户', 24 | excelImport: 'Excel导入', 25 | userManage: '员工管理', 26 | userInfo: '员工信息', 27 | roleList: '角色列表', 28 | permissionList: '权限列表', 29 | article: '文章', 30 | articleRanking: '文章排名', 31 | articleCreate: '创建文章', 32 | articleDetail: '文章详情', 33 | articleEditor: '文章编辑' 34 | }, 35 | toast: { 36 | switchLangSuccess: '切换语言成功' 37 | }, 38 | tagsView: { 39 | refresh: '刷新', 40 | closeRight: '关闭右侧', 41 | closeOther: '关闭其他' 42 | }, 43 | theme: { 44 | themeColorChange: '主题色更换', 45 | themeChange: '主题更换' 46 | }, 47 | universal: { 48 | title: '提示', 49 | confirm: '确定', 50 | cancel: '取消' 51 | }, 52 | navBar: { 53 | themeChange: '主题修改', 54 | headerSearch: '页面搜索', 55 | screenfull: '全屏替换', 56 | lang: '国际化', 57 | guide: '功能引导', 58 | home: '首页', 59 | course: '课程主页', 60 | logout: '退出登录' 61 | }, 62 | guide: { 63 | close: '关闭', 64 | next: '下一个', 65 | prev: '上一个', 66 | guideTitle: '引导', 67 | guideDesc: '打开引导功能', 68 | hamburgerTitle: '汉堡按钮', 69 | hamburgerDesc: '打开和关闭左侧菜单', 70 | breadcrumbTitle: '面包屑', 71 | breadcrumbDesc: '指示当前页面位置', 72 | searchTitle: '搜索', 73 | searchDesc: '页面链接搜索', 74 | fullTitle: '全屏', 75 | fullDesc: '页面显示切换', 76 | themeTitle: '主题', 77 | themeDesc: '更换项目主题', 78 | langTitle: '国际化', 79 | langDesc: '语言切换', 80 | tagTitle: '标签', 81 | tagDesc: '已打开页面标签', 82 | sidebarTitle: '菜单', 83 | sidebarDesc: '项目功能菜单' 84 | }, 85 | profile: { 86 | muted: '《vue3 改写 vue-element-admin,实现后台前端综合解决方案》项目演示', 87 | introduce: '介绍', 88 | projectIntroduction: '项目介绍', 89 | projectFunction: '项目功能', 90 | feature: '功能', 91 | chapter: '章节', 92 | author: '作者', 93 | name: 'Sunday', 94 | job: '一个前端开发程序猿', 95 | Introduction: 96 | '高级技术专家,曾就职于国内一线互联网公司,统筹过的多个大型项目用户数已过千万级。致力于研究大前端技术,多次受邀参加国内前端技术分享会,如:2018 年 Google 中国技术分享会。' 97 | }, 98 | userInfo: { 99 | print: '打印', 100 | title: '员工信息', 101 | name: '姓名', 102 | sex: '性别', 103 | nation: '民族', 104 | mobile: '手机号', 105 | province: '居住地', 106 | date: '入职时间', 107 | remark: '备注', 108 | address: '联系地址', 109 | experience: '经历', 110 | major: '专业', 111 | glory: '荣耀', 112 | foot: '签字:___________日期:___________' 113 | }, 114 | uploadExcel: { 115 | upload: '点击上传', 116 | drop: '将文件拖到此处' 117 | }, 118 | excel: { 119 | importExcel: 'excel 导入', 120 | exportExcel: 'excel 导出', 121 | exportZip: 'zip 导出', 122 | name: '姓名', 123 | mobile: '联系方式', 124 | avatar: '头像', 125 | role: '角色', 126 | openTime: '开通时间', 127 | action: '操作', 128 | show: '查看', 129 | showRole: '角色', 130 | defaultRole: '员工', 131 | remove: '删除', 132 | removeSuccess: '删除成功', 133 | title: '导出为 excel', 134 | placeholder: 'excel 文件名称', 135 | defaultName: '员工管理表', 136 | close: '取 消', 137 | confirm: '导 出', 138 | importSuccess: ' 条员工数据导入成功', 139 | dialogTitle1: '确定要删除用户 ', 140 | dialogTitle2: ' 吗?', 141 | roleDialogTitle: '配置角色' 142 | }, 143 | role: { 144 | buttonTxt: '新增角色', 145 | index: '序号', 146 | name: '名称', 147 | desc: '描述', 148 | action: '操作', 149 | assignPermissions: '分配权限', 150 | removeRole: '删除角色', 151 | dialogTitle: '新增角色', 152 | dialogRole: '角色名称', 153 | dialogDesc: '角色描述', 154 | updateRoleSuccess: '用户角色更新成功' 155 | }, 156 | permission: { 157 | name: '权限名称', 158 | mark: '权限标识', 159 | desc: '权限描述' 160 | }, 161 | article: { 162 | ranking: '排名', 163 | title: '标题', 164 | author: '作者', 165 | publicDate: '发布时间', 166 | desc: '内容简介', 167 | action: '操作', 168 | dynamicTitle: '动态展示', 169 | show: '查看', 170 | remove: '删除', 171 | edit: '编辑', 172 | dialogTitle1: '确定要删除文章 ', 173 | dialogTitle2: ' 吗?', 174 | removeSuccess: '文章删除成功', 175 | titlePlaceholder: '请输入文章标题', 176 | markdown: 'markdown', 177 | richText: '富文本', 178 | commit: '提交', 179 | createSuccess: '文章创建成功', 180 | editorSuccess: '文章修改成功', 181 | sortSuccess: '文章排名修改成功' 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import SvgIcon from '@/components/SvgIcon' 2 | 3 | // https://webpack.docschina.org/guides/dependency-management/#requirecontext 4 | // 通过 require.context() 函数来创建自己的 context 5 | const svgRequire = require.context('./svg', false, /\.svg$/) 6 | // 此时返回一个 require 的函数,可以接受一个 request 的参数,用于 require 的导入。 7 | // 该函数提供了三个属性,可以通过 require.keys() 获取到所有的 svg 图标 8 | // 遍历图标,把图标作为 request 传入到 require 导入函数中,完成本地 svg 图标的导入 9 | svgRequire.keys().forEach(svgIcon => svgRequire(svgIcon)) 10 | 11 | export default app => { 12 | app.component('svg-icon', SvgIcon) 13 | } 14 | -------------------------------------------------------------------------------- /src/icons/svg/article-create.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/article-ranking.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/article.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/change-theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/hamburger-closed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/hamburger-opened.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/introduce.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/permission.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/personnel-info.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/personnel-manage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/personnel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/reward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/role.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 75 | 76 | 86 | -------------------------------------------------------------------------------- /src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 55 | 56 | 110 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/MenuItem.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/SidebarMenu.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 41 | -------------------------------------------------------------------------------- /src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | 25 | 45 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 32 | 33 | 65 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import i18n from '@/i18n' 6 | // import installElementPlus from './plugins/element' 7 | 8 | import ElementPlus from 'element-plus' 9 | import 'element-plus/dist/index.css' 10 | // 导入全局样式 11 | import './styles/index.scss' 12 | // 导入 svgIcon 13 | import installIcons from '@/icons' 14 | // 导入路由鉴权 15 | import './permission' 16 | 17 | import * as EleIcons from '@element-plus/icons-vue' 18 | // filter 19 | import installFilter from '@/filter' 20 | // 指令 21 | import installDirective from '@/directives' 22 | 23 | const app = createApp(App) 24 | installIcons(app) 25 | installFilter(app) 26 | installDirective(app) 27 | 28 | for (const name in EleIcons) { 29 | app.component(name, EleIcons[name]) 30 | } 31 | app.use(store).use(router).use(ElementPlus).use(i18n).mount('#app') 32 | -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { generateTitle } from '@/utils/i18n' 4 | 5 | // 白名单 6 | const whiteList = ['/login'] 7 | /** 8 | * 路由前置守卫 9 | * @param {*} to 要到哪里去 10 | * @param {*} from 要从哪里来 11 | * @param {*} next 是否要去 12 | */ 13 | router.beforeEach(async (to, from, next) => { 14 | // 存在 token ,进入主页 15 | // if (store.state.user.token) { 16 | // 快捷访问 17 | 18 | if (to.meta.title) { 19 | const main = 'mooc-admin' 20 | const center = ' | ' 21 | const title = to.meta.title 22 | document.title = generateTitle(title) + center + main 23 | } 24 | if (store.getters.token) { 25 | if (to.path === '/login') { 26 | next('/') 27 | } else { 28 | // 判断用户资料是否获取 29 | // 若不存在用户信息,则需要获取用户信息 30 | if (!store.getters.hasUserInfo) { 31 | // 触发获取用户信息的 action,并获取用户当前权限 32 | const { permission } = await store.dispatch('user/getUserInfo') 33 | // 处理用户权限,筛选出需要添加的权限 34 | const filterRoutes = await store.dispatch( 35 | 'permission/filterRoutes', 36 | permission.menus 37 | ) 38 | // 利用 addRoute 循环添加 39 | filterRoutes.forEach((item) => { 40 | router.addRoute(item) 41 | }) 42 | // 添加完动态路由之后,需要在进行一次主动跳转 43 | return next(to.path) 44 | } 45 | next() 46 | } 47 | } else { 48 | // 没有token的情况下,可以进入白名单 49 | if (whiteList.indexOf(to.path) > -1) { 50 | next() 51 | } else { 52 | next('/login') 53 | } 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /src/plugins/element.js: -------------------------------------------------------------------------------- 1 | import ElementPlus from 'element-plus' 2 | import 'element-plus/dist/index.css' 3 | import zhCn from 'element-plus/es/locale/lang/zh-cn' 4 | import en from 'element-plus/lib/locale/lang/en' 5 | import store from '@/store' 6 | 7 | export default (app) => { 8 | app.use(ElementPlus, { 9 | locale: store.getters.language === 'en' ? en : zhCn 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import layout from '@/layout/index' 3 | import ArticleCreaterRouter from './modules/ArticleCreate' 4 | import ArticleRouter from './modules/Article' 5 | import PermissionListRouter from './modules/PermissionList' 6 | import RoleListRouter from './modules/RoleList' 7 | import UserManageRouter from './modules/UserManage' 8 | import store from '@/store' 9 | 10 | /** 11 | * 私有路由表 12 | */ 13 | export const privateRoutes = [ 14 | RoleListRouter, 15 | UserManageRouter, 16 | PermissionListRouter, 17 | ArticleCreaterRouter, 18 | ArticleRouter 19 | ] 20 | // console.log(privateRoutes) 21 | 22 | /** 23 | * @description: 公开路由表 24 | */ 25 | export const publicRoutes = [ 26 | { 27 | path: '/login', 28 | component: () => import('@/views/login/index') 29 | }, 30 | { 31 | path: '/', 32 | // 注意:带有路径“/”的记录中的组件“默认”是一个不返回 Promise 的函数 33 | component: layout, 34 | redirect: '/profile', 35 | children: [ 36 | { 37 | path: '/profile', 38 | name: 'profile', 39 | component: () => import('@/views/profile/index'), 40 | meta: { 41 | title: 'profile', 42 | icon: 'el-icon', 43 | iconName: 'User' 44 | } 45 | }, 46 | { 47 | path: '/404', 48 | name: '404', 49 | component: () => import('@/views/error-page/404') 50 | }, 51 | { 52 | path: '/401', 53 | name: '401', 54 | component: () => import('@/views/error-page/401') 55 | } 56 | ] 57 | } 58 | ] 59 | const router = createRouter({ 60 | history: createWebHashHistory(), 61 | routes: publicRoutes 62 | }) 63 | /** 64 | * 初始化路由表 65 | */ 66 | export function resetRouter() { 67 | if ( 68 | store.getters.userInfo && 69 | store.getters.userInfo.permission && 70 | store.getters.userInfo.permission.menus 71 | ) { 72 | const menus = store.getters.userInfo.permission.menus 73 | menus.forEach((menu) => { 74 | router.removeRoute(menu) 75 | }) 76 | } 77 | } 78 | export default router 79 | -------------------------------------------------------------------------------- /src/router/modules/Article.js: -------------------------------------------------------------------------------- 1 | import layout from '@/layout' 2 | 3 | export default { 4 | path: '/article', 5 | component: layout, 6 | redirect: '/article/ranking', 7 | name: 'articleRanking', 8 | meta: { title: 'article', icon: 'article' }, 9 | children: [ 10 | { 11 | path: '/article/ranking', 12 | component: () => import('@/views/article-ranking/index'), 13 | meta: { 14 | title: 'articleRanking', 15 | icon: 'article-ranking' 16 | } 17 | }, 18 | { 19 | path: '/article/:id', 20 | component: () => import('@/views/article-detail/index'), 21 | meta: { 22 | title: 'articleDetail' 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/router/modules/ArticleCreate.js: -------------------------------------------------------------------------------- 1 | import layout from '@/layout' 2 | 3 | export default { 4 | path: '/article', 5 | component: layout, 6 | redirect: '/article/ranking', 7 | name: 'articleCreate', 8 | meta: { title: 'article', icon: 'article' }, 9 | children: [ 10 | { 11 | path: '/article/create', 12 | component: () => import('@/views/article-create/index'), 13 | meta: { 14 | title: 'articleCreate', 15 | icon: 'article-create' 16 | } 17 | }, 18 | { 19 | path: '/article/editor/:id', 20 | component: () => import('@/views/article-create/index'), 21 | meta: { 22 | title: 'articleEditor' 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/router/modules/PermissionList.js: -------------------------------------------------------------------------------- 1 | import layout from '@/layout' 2 | 3 | export default { 4 | path: '/user', 5 | component: layout, 6 | redirect: '/user/manage', 7 | name: 'permissionList', 8 | meta: { 9 | title: 'user', 10 | icon: 'personnel' 11 | }, 12 | children: [ 13 | { 14 | path: '/user/permission', 15 | component: () => import('@/views/permission-list/index'), 16 | meta: { 17 | title: 'permissionList', 18 | icon: 'permission' 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/router/modules/RoleList.js: -------------------------------------------------------------------------------- 1 | import layout from '@/layout' 2 | 3 | export default { 4 | path: '/user', 5 | component: layout, 6 | redirect: '/user/manage', 7 | name: 'roleList', 8 | meta: { 9 | title: 'user', 10 | icon: 'personnel' 11 | }, 12 | children: [ 13 | { 14 | path: '/user/role', 15 | component: () => import('@/views/role-list/index'), 16 | meta: { 17 | title: 'roleList', 18 | icon: 'role' 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/router/modules/UserManage.js: -------------------------------------------------------------------------------- 1 | import layout from '@/layout' 2 | 3 | export default { 4 | path: '/user', 5 | component: layout, 6 | redirect: '/user/manage', 7 | name: 'userManage', 8 | meta: { 9 | title: 'user', 10 | icon: 'personnel' 11 | }, 12 | children: [ 13 | { 14 | path: '/user/manage', 15 | component: () => import('@/views/user-manage/index'), 16 | meta: { 17 | title: 'userManage', 18 | icon: 'personnel-manage' 19 | } 20 | }, 21 | { 22 | path: '/user/info/:id', 23 | name: 'userInfo', 24 | component: () => import('@/views/user-info/index'), 25 | props: true, 26 | meta: { 27 | title: 'userInfo' 28 | } 29 | }, 30 | { 31 | path: '/user/import', 32 | name: 'import', 33 | component: () => import('@/views/import/index'), 34 | meta: { 35 | title: 'excelImport' 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | import { MAIN_COLOR } from '@/constant' 2 | import { getItem } from '@/utils/storage' 3 | import { generateColors } from '@/utils/theme' 4 | 5 | const getters = { 6 | token: (state) => state.user.token, 7 | /** 8 | * @returns true 表示已存在用户信息 9 | */ 10 | hasUserInfo: (state) => { 11 | return JSON.stringify(state.user.userInfo) !== '{}' 12 | }, 13 | userInfo: (state) => state.user.userInfo, 14 | cssVar: (state) => { 15 | return { 16 | ...state.theme.variables, 17 | ...generateColors(getItem(MAIN_COLOR)) 18 | } 19 | }, 20 | sidebarOpened: (state) => state.app.sidebarOpened, 21 | language: (state) => state.app.language, 22 | mainColor: (state) => state.theme.mainColor, 23 | tagsViewList: (state) => state.app.tagsViewList 24 | } 25 | export default getters 26 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import user from './modules/user.js' 3 | import getters from './getters' 4 | import app from './modules/app' 5 | import theme from './modules/theme.js' 6 | import permission from './modules/permission.js' 7 | 8 | export default createStore({ 9 | state: {}, 10 | mutations: {}, 11 | actions: {}, 12 | getters, 13 | modules: { 14 | user, 15 | app, 16 | theme, 17 | permission 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import { LANG, TAGS_VIEW } from '@/constant' 2 | import { getItem, setItem } from '@/utils/storage' 3 | export default { 4 | namespaced: true, 5 | state: () => ({ 6 | sidebarOpened: true, 7 | language: getItem(LANG) || 'zh', 8 | tagsViewList: getItem(TAGS_VIEW) || [] 9 | }), 10 | mutations: { 11 | triggerSidebarOpened(state) { 12 | state.sidebarOpened = !state.sidebarOpened 13 | }, 14 | /** 15 | * 设置国际化 16 | */ 17 | setLanguage(state, lang) { 18 | setItem(LANG, lang) 19 | state.language = lang 20 | }, 21 | /** 22 | * 添加 tags 23 | */ 24 | addTagsViewList(state, tag) { 25 | const isFind = state.tagsViewList.find((item) => { 26 | return item.path === tag.path 27 | }) 28 | // 处理重复 29 | if (!isFind) { 30 | state.tagsViewList.push(tag) 31 | setItem(TAGS_VIEW, state.tagsViewList) 32 | } 33 | }, 34 | /** 35 | * 为指定的 tag 修改 title 36 | */ 37 | changeTagsView(state, { index, tag }) { 38 | state.tagsViewList[index] = tag 39 | setItem(TAGS_VIEW, state.tagsViewList) 40 | }, 41 | /** 42 | * 删除 tag 43 | * @param {type: 'other'||'right'||'index', index: index} payload 44 | */ 45 | removeTagsView(state, payload) { 46 | if (payload.type === 'index') { 47 | state.tagsViewList.splice(payload.index, 1) 48 | return 49 | } else if (payload.type === 'other') { 50 | state.tagsViewList.splice( 51 | payload.index + 1, 52 | state.tagsViewList.length - payload.index + 1 53 | ) 54 | state.tagsViewList.splice(0, payload.index) 55 | } else if (payload.type === 'right') { 56 | state.tagsViewList.splice( 57 | payload.index + 1, 58 | state.tagsViewList.length - payload.index + 1 59 | ) 60 | } 61 | setItem(TAGS_VIEW, state.tagsViewList) 62 | } 63 | }, 64 | actions: {} 65 | } 66 | -------------------------------------------------------------------------------- /src/store/modules/permission.js: -------------------------------------------------------------------------------- 1 | // 专门处理权限路由的模块 2 | import { publicRoutes, privateRoutes } from '@/router' 3 | export default { 4 | namespaced: true, 5 | state: { 6 | // 路由表:初始拥有静态路由权限 7 | routes: publicRoutes 8 | }, 9 | mutations: { 10 | /** 11 | * 增加路由 12 | */ 13 | setRoutes(state, newRoutes) { 14 | // 永远在静态路由的基础上增加新路由 15 | state.routes = [...publicRoutes, ...newRoutes] 16 | } 17 | }, 18 | actions: { 19 | /** 20 | * 根据权限筛选路由 21 | */ 22 | filterRoutes(context, menus) { 23 | const routes = [] 24 | // 路由权限匹配 25 | menus.forEach((key) => { 26 | // 权限名 与 路由的 name 匹配 27 | routes.push(...privateRoutes.filter((item) => item.name === key)) 28 | }) 29 | /** 30 | * 最后添加 不匹配路由进入 404 31 | * 所有不匹配的路由全部进入404 32 | * 该配置必须在所有路由指定之后 33 | */ 34 | routes.push({ 35 | path: '/:catchAll(.*)', 36 | redirect: '/404' 37 | }) 38 | context.commit('setRoutes', routes) 39 | return routes 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/store/modules/theme.js: -------------------------------------------------------------------------------- 1 | import { getItem, setItem } from '@/utils/storage' 2 | import { MAIN_COLOR, DEFAULT_COLOR } from '@/constant' 3 | import variables from '@/styles/variables.scss' 4 | export default { 5 | namespaced: true, 6 | state: () => ({ 7 | mainColor: getItem(MAIN_COLOR) || DEFAULT_COLOR, 8 | variables 9 | }), 10 | mutations: { 11 | /** 12 | * 设置主题色 13 | */ 14 | setMainColor(state, newColor) { 15 | state.mainColor = newColor 16 | setItem(MAIN_COLOR, newColor) 17 | state.variables.menuBg = newColor 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { login, getUserInfo } from '@/api/sys' 2 | import md5 from 'md5' 3 | import { setItem, getItem, removeAllItem } from '@/utils/storage' 4 | import { TOKEN } from '@/constant' 5 | import router, { resetRouter } from '@/router' 6 | import { setTimeStamp } from '@/utils/auth' 7 | 8 | export default { 9 | namespaced: true, 10 | state: () => ({ 11 | token: getItem(TOKEN) || '', 12 | userInfo: {} 13 | }), 14 | mutations: { 15 | setToken(state, token) { 16 | state.token = token 17 | setItem(TOKEN, token) 18 | }, 19 | setUserInfo(state, userInfo) { 20 | state.userInfo = userInfo 21 | } 22 | }, 23 | actions: { 24 | /** 25 | * 登录请求动作 26 | * 27 | */ 28 | login(context, userInfo) { 29 | const { username, password } = userInfo 30 | return new Promise((resolve, reject) => { 31 | login({ 32 | username, 33 | password: md5(password) 34 | }) 35 | .then((data) => { 36 | this.commit('user/setToken', data.token) 37 | // 登录后操作 38 | router.push('/') 39 | // 保存登录时间 40 | setTimeStamp() 41 | resolve() 42 | }) 43 | .catch((err) => { 44 | reject(err) 45 | }) 46 | }) 47 | }, 48 | /** 49 | * 50 | * 获取用户信息 51 | */ 52 | async getUserInfo(context) { 53 | const res = await getUserInfo() 54 | this.commit('user/setUserInfo', res) 55 | return res 56 | }, 57 | // 退出登录 58 | logout() { 59 | resetRouter() 60 | this.commit('user/setToken', '') 61 | this.commit('user/setUserInfo', {}) 62 | removeAllItem() 63 | router.push('/login') 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/styles/element.scss: -------------------------------------------------------------------------------- 1 | .el-avatar { 2 | --el-avatar-bg-color:none; 3 | } 4 | .el-input{ 5 | --el-input-focus-border-color: none; 6 | --el-input-border-color: none; 7 | --el-input-hover-border-color: none; 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './sidebar.scss'; 4 | @import './element.scss'; 5 | @import './transition.scss'; 6 | 7 | html, 8 | body { 9 | height: 100%; 10 | margin: 0; 11 | padding: 0; 12 | -moz-osx-font-smoothing: grayscale; 13 | -webkit-font-smoothing: antialiased; 14 | text-rendering: optimizeLegibility; 15 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, 16 | Microsoft YaHei, Arial, sans-serif; 17 | } 18 | 19 | #app { 20 | height: 100%; 21 | } 22 | 23 | *, 24 | *:before, 25 | *:after { 26 | box-sizing: inherit; 27 | margin: 0; 28 | padding: 0; 29 | } 30 | 31 | a:focus, 32 | a:active { 33 | outline: none; 34 | } 35 | 36 | a, 37 | a:focus, 38 | a:hover { 39 | cursor: pointer; 40 | color: inherit; 41 | text-decoration: none; 42 | } 43 | 44 | div:focus { 45 | outline: none; 46 | } 47 | 48 | .clearfix { 49 | &:after { 50 | visibility: hidden; 51 | display: block; 52 | font-size: 0; 53 | content: ' '; 54 | clear: both; 55 | height: 0; 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ''; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | .main-container { 3 | min-height: 100%; 4 | transition: margin-left #{$sideBarDuration}; 5 | margin-left: $sideBarWidth; 6 | position: relative; 7 | } 8 | 9 | .sidebar-container { 10 | transition: width #{$sideBarDuration}; 11 | width: $sideBarWidth !important; 12 | height: 100%; 13 | position: fixed; 14 | top: 0; 15 | bottom: 0; 16 | left: 0; 17 | z-index: 1001; 18 | overflow: hidden; 19 | 20 | // 重置 element-plus 的css 21 | .horizontal-collapse-transition { 22 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 23 | 0s padding-right ease-in-out; 24 | } 25 | 26 | .scrollbar-wrapper { 27 | overflow-x: hidden !important; 28 | } 29 | 30 | .el-scrollbar__bar.is-vertical { 31 | right: 0px; 32 | } 33 | 34 | .el-scrollbar { 35 | height: 100%; 36 | } 37 | 38 | &.has-logo { 39 | .el-scrollbar { 40 | height: calc(100% - 50px); 41 | } 42 | } 43 | 44 | .is-horizontal { 45 | display: none; 46 | } 47 | 48 | a { 49 | display: inline-block; 50 | width: 100%; 51 | overflow: hidden; 52 | } 53 | 54 | .svg-icon { 55 | margin-right: 16px; 56 | } 57 | 58 | .sub-el-icon { 59 | margin-right: 12px; 60 | margin-left: -2px; 61 | } 62 | 63 | .el-menu { 64 | border: none; 65 | height: 100%; 66 | width: 100% !important; 67 | } 68 | 69 | .is-active > .el-sub-menu__title { 70 | color: $subMenuActiveText !important; 71 | } 72 | 73 | & .nest-menu .el-sub-menu > .el-sub-menu__title, 74 | & .el-sub-menu .el-menu-item { 75 | min-width: $sideBarWidth !important; 76 | padding-right: 20px; 77 | } 78 | } 79 | 80 | .hideSidebar { 81 | .sidebar-container { 82 | width: 54px !important; 83 | } 84 | 85 | .main-container { 86 | margin-left: 54px; 87 | } 88 | 89 | .submenu-title-noDropdown { 90 | padding: 0 !important; 91 | position: relative; 92 | 93 | .el-tooltip { 94 | padding: 0 !important; 95 | 96 | .svg-icon { 97 | margin-left: 20px; 98 | } 99 | 100 | .sub-el-icon { 101 | margin-left: 19px; 102 | } 103 | } 104 | } 105 | 106 | .el-sub-menu { 107 | overflow: hidden; 108 | 109 | & > .el-sub-menu__title { 110 | padding: 0 !important; 111 | 112 | .svg-icon { 113 | margin-left: 20px; 114 | } 115 | 116 | .sub-el-icon { 117 | margin-left: 19px; 118 | } 119 | 120 | .el-sub-menu__icon-arrow { 121 | display: none; 122 | } 123 | } 124 | } 125 | 126 | .el-menu--collapse { 127 | .el-sub-menu { 128 | & > .el-sub-menu__title { 129 | & > span { 130 | height: 0; 131 | width: 0; 132 | overflow: hidden; 133 | visibility: hidden; 134 | display: inline-block; 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | .el-menu--collapse .el-menu .el-sub-menu { 142 | min-width: $sideBarWidth !important; 143 | } 144 | 145 | .withoutAnimation { 146 | .main-container, 147 | .sidebar-container { 148 | transition: none; 149 | } 150 | } 151 | } 152 | 153 | .el-menu--vertical { 154 | & > .el-menu { 155 | .svg-icon { 156 | margin-right: 16px; 157 | } 158 | .sub-el-icon { 159 | margin-right: 12px; 160 | margin-left: -2px; 161 | } 162 | } 163 | 164 | // 菜单项过长时 165 | > .el-menu--popup { 166 | max-height: 100vh; 167 | overflow-y: auto; 168 | 169 | &::-webkit-scrollbar-track-piece { 170 | background: #d3dce6; 171 | } 172 | 173 | &::-webkit-scrollbar { 174 | width: 6px; 175 | } 176 | 177 | &::-webkit-scrollbar-thumb { 178 | background: #99a9bf; 179 | border-radius: 20px; 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | .breadcrumb-enter-active, 2 | .breadcrumb-leave-active { 3 | transition: all 0.25s; 4 | } 5 | 6 | .breadcrumb-enter-from, 7 | .breadcrumb-leave-active { 8 | opacity: 0; 9 | transform: translateX(20px); 10 | } 11 | 12 | .breadcrumb-leave-active { 13 | position: absolute; 14 | } 15 | /* fade-transform */ 16 | .fade-transform-leave-active, 17 | .fade-transform-enter-active { 18 | transition: all 0.5s; 19 | } 20 | 21 | .fade-transform-enter-from { 22 | opacity: 0; 23 | transform: translateX(-30px); 24 | } 25 | 26 | .fade-transform-leave-to { 27 | opacity: 0; 28 | transform: translateX(30px); 29 | } -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText: #bfcbd9; 3 | $menuActiveText: #ffffff; 4 | $subMenuActiveText: #f4f4f5; 5 | 6 | $menuBg: #304156; 7 | $menuHover: #263445; 8 | 9 | $subMenuBg: #1f2d3d; 10 | $subMenuHover: #001528; 11 | 12 | $sideBarWidth: 210px; 13 | 14 | $hideSideBarWidth: 54px; 15 | $tagViewsList:#42b983; 16 | 17 | // 处理动画时长 18 | $sideBarDuration: 0.28s; 19 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 20 | // JS 与 scss 共享变量,在 scss 中通过 :export 进行导出,在 js 中可通过 ESM 进行导入 21 | :export { 22 | menuText: $menuText; 23 | menuActiveText: $menuActiveText; 24 | subMenuActiveText: $subMenuActiveText; 25 | menuBg: $menuBg; 26 | menuHover: $menuHover; 27 | subMenuBg: $subMenuBg; 28 | subMenuHover: $subMenuHover; 29 | sideBarWidth: $sideBarWidth; 30 | tagViewsList:#42b983; 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/Export2Excel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { saveAs } from 'file-saver' 3 | import XLSX from 'xlsx' 4 | 5 | function datenum(v, date1904) { 6 | if (date1904) v += 1462 7 | var epoch = Date.parse(v) 8 | return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000) 9 | } 10 | 11 | function sheet_from_array_of_arrays(data, opts) { 12 | var ws = {} 13 | var range = { 14 | s: { 15 | c: 10000000, 16 | r: 10000000 17 | }, 18 | e: { 19 | c: 0, 20 | r: 0 21 | } 22 | } 23 | for (var R = 0; R != data.length; ++R) { 24 | for (var C = 0; C != data[R].length; ++C) { 25 | if (range.s.r > R) range.s.r = R 26 | if (range.s.c > C) range.s.c = C 27 | if (range.e.r < R) range.e.r = R 28 | if (range.e.c < C) range.e.c = C 29 | var cell = { 30 | v: data[R][C] 31 | } 32 | if (cell.v == null) continue 33 | var cell_ref = XLSX.utils.encode_cell({ 34 | c: C, 35 | r: R 36 | }) 37 | 38 | if (typeof cell.v === 'number') cell.t = 'n' 39 | else if (typeof cell.v === 'boolean') cell.t = 'b' 40 | else if (cell.v instanceof Date) { 41 | cell.t = 'n' 42 | cell.z = XLSX.SSF._table[14] 43 | cell.v = datenum(cell.v) 44 | } else cell.t = 's' 45 | 46 | ws[cell_ref] = cell 47 | } 48 | } 49 | if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range) 50 | return ws 51 | } 52 | 53 | function Workbook() { 54 | if (!(this instanceof Workbook)) return new Workbook() 55 | this.SheetNames = [] 56 | this.Sheets = {} 57 | } 58 | 59 | function s2ab(s) { 60 | var buf = new ArrayBuffer(s.length) 61 | var view = new Uint8Array(buf) 62 | for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff 63 | return buf 64 | } 65 | 66 | export const export_json_to_excel = ({ 67 | multiHeader = [], 68 | header, 69 | data, 70 | filename, 71 | merges = [], 72 | autoWidth = true, 73 | bookType = 'xlsx' 74 | } = {}) => { 75 | // 1. 设置文件名称 76 | filename = filename || 'excel-list' 77 | // 2. 把数据解析为数组,并把表头添加到数组的头部 78 | data = [...data] 79 | data.unshift(header) 80 | // 3. 解析多表头,把多表头的数据添加到数组头部(二维数组) 81 | for (let i = multiHeader.length - 1; i > -1; i--) { 82 | data.unshift(multiHeader[i]) 83 | } 84 | // 4. 设置 Excel 表工作簿(第一张表格)名称 85 | var ws_name = 'SheetJS' 86 | // 5. 生成工作簿对象 87 | var wb = new Workbook() 88 | // 6. 将 data 数组(json格式)转化为 Excel 数据格式 89 | var ws = sheet_from_array_of_arrays(data) 90 | // 7. 合并单元格相关(['A1:A2', 'B1:D1', 'E1:E2']) 91 | if (merges.length > 0) { 92 | if (!ws['!merges']) ws['!merges'] = [] 93 | merges.forEach((item) => { 94 | ws['!merges'].push(XLSX.utils.decode_range(item)) 95 | }) 96 | } 97 | // 8. 单元格宽度相关 98 | if (autoWidth) { 99 | /*设置 worksheet 每列的最大宽度*/ 100 | const colWidth = data.map((row) => 101 | row.map((val) => { 102 | /*先判断是否为null/undefined*/ 103 | if (val == null) { 104 | return { 105 | wch: 10 106 | } 107 | } else if (val.toString().charCodeAt(0) > 255) { 108 | /*再判断是否为中文*/ 109 | return { 110 | wch: val.toString().length * 2 111 | } 112 | } else { 113 | return { 114 | wch: val.toString().length 115 | } 116 | } 117 | }) 118 | ) 119 | /*以第一行为初始值*/ 120 | let result = colWidth[0] 121 | for (let i = 1; i < colWidth.length; i++) { 122 | for (let j = 0; j < colWidth[i].length; j++) { 123 | if (result[j]['wch'] < colWidth[i][j]['wch']) { 124 | result[j]['wch'] = colWidth[i][j]['wch'] 125 | } 126 | } 127 | } 128 | ws['!cols'] = result 129 | } 130 | 131 | // 9. 添加工作表(解析后的 excel 数据)到工作簿 132 | wb.SheetNames.push(ws_name) 133 | wb.Sheets[ws_name] = ws 134 | // 10. 写入数据 135 | var wbout = XLSX.write(wb, { 136 | bookType: bookType, 137 | bookSST: false, 138 | type: 'binary' 139 | }) 140 | // 11. 下载数据 141 | saveAs( 142 | new Blob([s2ab(wbout)], { 143 | type: 'application/octet-stream' 144 | }), 145 | `${filename}.${bookType}` 146 | ) 147 | } 148 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import { TIME_STAMP, TOKEN_TIMEOUT_VALUE } from '@/constant' 2 | import { setItem, getItem } from '@/utils/storage' 3 | /** 4 | * 获取时间戳 5 | */ 6 | export function getTimeStamp() { 7 | return getItem(TIME_STAMP) 8 | } 9 | /** 10 | * 设置时间戳 11 | */ 12 | export function setTimeStamp() { 13 | setItem(TIME_STAMP, Date.now()) 14 | } 15 | /** 16 | * 是否超时 17 | */ 18 | export function isCheckTimeout() { 19 | // 当前时间戳 20 | var currentTime = Date.now() 21 | // 缓存时间戳 22 | var timeStamp = getTimeStamp() 23 | return currentTime - timeStamp > TOKEN_TIMEOUT_VALUE 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from '@/i18n' 2 | import { watch } from 'vue' 3 | import store from '@/store' 4 | 5 | export function generateTitle(title) { 6 | return i18n.global.t('msg.route.' + title) 7 | } 8 | /** 9 | * 10 | * @param {...any} cbs 所有的回调 11 | */ 12 | export function watchSwitchLang(...cbs) { 13 | watch( 14 | () => store.getters.language, 15 | () => { 16 | cbs.forEach((cb) => cb(store.getters.language)) 17 | } 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import store from '@/store' 3 | import { ElMessage } from 'element-plus' 4 | import { isCheckTimeout } from '@/utils/auth' 5 | import md5 from 'md5' 6 | const service = axios.create({ 7 | baseURL: process.env.VUE_APP_BASE_API, 8 | timeout: 5000 9 | }) 10 | 11 | // 请求拦截器 12 | service.interceptors.request.use( 13 | (config) => { 14 | const { icode, time } = getTestICode() 15 | config.headers.icode = icode 16 | config.headers.codeType = time 17 | // 在这个位置需要统一的去注入token 18 | if (store.getters.token) { 19 | // 如果token存在 注入token 20 | config.headers.Authorization = `Bearer ${store.getters.token}` 21 | if (isCheckTimeout()) { 22 | // 登出操作 23 | store.dispatch('user/logout') 24 | return Promise.reject(new Error('token 失效')) 25 | } 26 | } 27 | // 配置接口国际化 28 | config.headers['Accept-Language'] = store.getters.language 29 | return config // 必须返回配置 30 | }, 31 | (error) => { 32 | return Promise.reject(error) 33 | } 34 | ) 35 | 36 | // 响应拦截器 37 | service.interceptors.response.use( 38 | (response) => { 39 | const { success, message, data } = response.data 40 | // 要根据success的成功与否决定下面的操作 41 | if (success) { 42 | // 成功返回解析后的数据 43 | return data 44 | } else { 45 | // 业务错误 46 | ElMessage.error(message) // 提示错误消息 47 | return Promise.reject(new Error(message)) 48 | } 49 | }, 50 | (error) => { 51 | // 处理 token 超时问题 52 | if ( 53 | error.response && 54 | error.response.data && 55 | error.response.data.code === 401 56 | ) { 57 | // token超时 58 | store.dispatch('user/logout') 59 | } 60 | ElMessage.error(error.message) // 提示错误信息 61 | return Promise.reject(error) 62 | } 63 | ) 64 | 65 | /** 66 | * 返回 Icode 的实现 67 | */ 68 | function getTestICode() { 69 | const now = parseInt(Date.now() / 1000) 70 | const code = now + 'LGD_Sunday-1991-12-30' 71 | return { 72 | icode: md5(code), 73 | time: now 74 | } 75 | } 76 | export default service 77 | -------------------------------------------------------------------------------- /src/utils/route.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | /** 4 | * 返回所有子路由 5 | */ 6 | const getChildrenRoutes = (routes) => { 7 | const result = [] 8 | routes.forEach((route) => { 9 | if (route.children && route.children.length > 0) { 10 | result.push(...route.children) 11 | } 12 | }) 13 | return result 14 | } 15 | /** 16 | * 处理脱离层级的路由:某个一级路由为其他子路由,则剔除该一级路由,保留路由层级 17 | * @param {*} routes router.getRoutes() 18 | */ 19 | export const filterRouters = (routes) => { 20 | const childrenRoutes = getChildrenRoutes(routes) 21 | return routes.filter((route) => { 22 | return !childrenRoutes.find((childrenRoute) => { 23 | return childrenRoute.path === route.path 24 | }) 25 | }) 26 | } 27 | 28 | /** 29 | * 判断数据是否为空值 30 | */ 31 | function isNull(data) { 32 | if (!data) return true 33 | if (JSON.stringify(data) === '{}') return true 34 | if (JSON.stringify(data) === '[]') return true 35 | return false 36 | } 37 | /** 38 | * 根据 routes 数据,返回对应 menu 规则数组 39 | */ 40 | export function generateMenus(routes, basePath = '') { 41 | const result = [] 42 | // 遍历路由表 43 | routes.forEach((item) => { 44 | // 不存在 children && 不存在 meta 直接 return 45 | if (isNull(item.meta) && isNull(item.children)) return 46 | // 存在 children 不存在 meta,进入迭代 47 | if (isNull(item.meta) && !isNull(item.children)) { 48 | result.push(...generateMenus(item.children)) 49 | return 50 | } 51 | // 合并 path 作为跳转路径 52 | const routePath = path.resolve(basePath, item.path) 53 | // 路由分离之后,存在同名父路由的情况,需要单独处理 54 | let route = result.find((item) => item.path === routePath) 55 | if (!route) { 56 | route = { 57 | ...item, 58 | path: routePath, 59 | children: [] 60 | } 61 | 62 | // icon 与 title 必须全部存在 63 | if (route.meta.icon && route.meta.title) { 64 | // meta 存在生成 route 对象,放入 arr 65 | result.push(route) 66 | } 67 | } 68 | 69 | // 存在 children 进入迭代到children 70 | if (item.children) { 71 | route.children.push(...generateMenus(item.children, route.path)) 72 | } 73 | }) 74 | return result 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 存储数据 3 | */ 4 | export const setItem = (key, value) => { 5 | // 将数组、对象类型的数据转化为 JSON 字符串进行存储 6 | if (typeof value === 'object') { 7 | value = JSON.stringify(value) 8 | } 9 | window.localStorage.setItem(key, value) 10 | } 11 | 12 | /** 13 | * 获取数据 14 | */ 15 | export const getItem = key => { 16 | const data = window.localStorage.getItem(key) 17 | try { 18 | return JSON.parse(data) 19 | } catch (err) { 20 | return data 21 | } 22 | } 23 | 24 | /** 25 | * 删除数据 26 | */ 27 | export const removeItem = key => { 28 | window.localStorage.removeItem(key) 29 | } 30 | 31 | /** 32 | * 删除所有数据 33 | */ 34 | export const removeAllItem = key => { 35 | window.localStorage.clear() 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/tags.js: -------------------------------------------------------------------------------- 1 | const whiteList = ['/login', '/import', '/404', '/401'] 2 | 3 | /** 4 | * path 是否需要被缓存 5 | * @param {*} path 6 | * @returns 7 | */ 8 | export function isTags(path) { 9 | return !whiteList.includes(path) 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/theme.js: -------------------------------------------------------------------------------- 1 | import color from 'css-color-function' 2 | import rgbHex from 'rgb-hex' 3 | import formula from '@/constant/formula.json' 4 | import axios from 'axios' 5 | /** 6 | * 写入新样式到 style 7 | * @param {*} elNewStyle element-plus 的新样式 8 | * @param {*} isNewStyleTag 是否生成新的 style 标签 9 | */ 10 | export const writeNewStyle = (elNewStyle) => { 11 | const style = document.createElement('style') 12 | style.innerText = elNewStyle 13 | document.head.appendChild(style) 14 | } 15 | 16 | /** 17 | * 根据主色值,生成最新的样式表 18 | */ 19 | export const generateNewStyle = async (primaryColor) => { 20 | // 1. 根据主色生成色值表 21 | const colors = generateColors(primaryColor) 22 | // console.log('colors', colors) 23 | // 2. 获取当前 element-plus 的默认样式,并且把需要进行替换的色值打上标记 24 | let cssText = await getOriginalStyle() 25 | // 3.遍历生成的色值表,在默认样式进行全局替换 26 | // 遍历生成的样式表,在 CSS 的原样式中进行全局替换 27 | Object.keys(colors).forEach(key => { 28 | cssText = cssText.replace( 29 | new RegExp('(:|\\s+)' + key, 'g'), 30 | '$1' + colors[key] 31 | ) 32 | }) 33 | 34 | return cssText 35 | } 36 | 37 | /** 38 | * 根据主色生成色值表 39 | */ 40 | export const generateColors = (primary) => { 41 | if (!primary) return 42 | const colors = { 43 | primary 44 | } 45 | Object.keys(formula).forEach((key) => { 46 | const value = formula[key].replace(/primary/g, primary) 47 | colors[key] = '#' + rgbHex(color.convert(value)) 48 | }) 49 | return colors 50 | } 51 | 52 | /** 53 | * 获取当前 element-plus 的默认样式表 54 | */ 55 | export const getOriginalStyle = async () => { 56 | const version = require('element-plus/package.json').version 57 | const url = `https://unpkg.com/element-plus@${version}/dist/index.css` 58 | const { data } = await axios(url) 59 | // 把获取到的数据筛选为原样式模板 60 | return getStyleTemplate(data) 61 | } 62 | 63 | /** 64 | * 返回 style 的 template 65 | */ 66 | const getStyleTemplate = (data) => { 67 | // element-plus 默认色值 68 | const colorMap = { 69 | '#3a8ee6': 'shade-1', 70 | '#409eff': 'primary', 71 | '#53a8ff': 'light-1', 72 | '#66b1ff': 'light-2', 73 | '#79bbff': 'light-3', 74 | '#8cc5ff': 'light-4', 75 | '#a0cfff': 'light-5', 76 | '#b3d8ff': 'light-6', 77 | '#c6e2ff': 'light-7', 78 | '#d9ecff': 'light-8', 79 | '#ecf5ff': 'light-9' 80 | } 81 | // 根据默认色值为要替换的色值打上标记 82 | Object.keys(colorMap).forEach((key) => { 83 | const value = colorMap[key] 84 | data = data.replace(new RegExp(key, 'ig'), value) 85 | }) 86 | return data 87 | } 88 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 判断是否为外部资源 3 | */ 4 | export function isExternal(path) { 5 | return /^(https?:|mailto:|tel:)/.test(path) 6 | } 7 | -------------------------------------------------------------------------------- /src/views/article-create/components/Editor.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 86 | 87 | 95 | -------------------------------------------------------------------------------- /src/views/article-create/components/Markdown.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 93 | 94 | 102 | -------------------------------------------------------------------------------- /src/views/article-create/components/commit.js: -------------------------------------------------------------------------------- 1 | import { createArticle, articleEdit } from '@/api/article' 2 | import { ElMessage } from 'element-plus' 3 | import i18n from '@/i18n' 4 | const t = i18n.global.t 5 | 6 | export const commitArticle = async (data) => { 7 | const res = await createArticle(data) 8 | ElMessage.success(t('msg.article.createSuccess')) 9 | return res 10 | } 11 | export const editArticle = async (data) => { 12 | const res = await articleEdit(data) 13 | ElMessage.success(t('msg.article.editorSuccess')) 14 | return res 15 | } 16 | -------------------------------------------------------------------------------- /src/views/article-create/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 58 | 59 | 64 | -------------------------------------------------------------------------------- /src/views/article-detail/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 41 | 42 | 72 | -------------------------------------------------------------------------------- /src/views/article-ranking/dynamic/DynamicData.js: -------------------------------------------------------------------------------- 1 | import i18n from '@/i18n' 2 | 3 | const t = i18n.global.t 4 | 5 | export default () => [ 6 | { 7 | label: t('msg.article.ranking'), 8 | prop: 'ranking' 9 | }, 10 | { 11 | label: t('msg.article.title'), 12 | prop: 'title' 13 | }, 14 | { 15 | label: t('msg.article.author'), 16 | prop: 'author' 17 | }, 18 | { 19 | label: t('msg.article.publicDate'), 20 | prop: 'publicDate' 21 | }, 22 | { 23 | label: t('msg.article.desc'), 24 | prop: 'desc' 25 | }, 26 | { 27 | label: t('msg.article.action'), 28 | prop: 'action' 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /src/views/article-ranking/dynamic/index.js: -------------------------------------------------------------------------------- 1 | import getDynamicData from './DynamicData' 2 | import { watchSwitchLang } from '@/utils/i18n' 3 | import { watch, ref } from 'vue' 4 | 5 | // 暴露出动态列数据 6 | export const dynamicData = ref(getDynamicData()) 7 | 8 | // 监听 语言变化 9 | watchSwitchLang(() => { 10 | // 重新获取国际化的值 11 | dynamicData.value = getDynamicData() 12 | // 重新处理被勾选的列数据 13 | initSelectDynamicLabel() 14 | }) 15 | 16 | // 创建被勾选的动态列数据 17 | export const selectDynamicLabel = ref([]) 18 | // 默认全部勾选 19 | const initSelectDynamicLabel = () => { 20 | selectDynamicLabel.value = dynamicData.value.map((item) => item.label) 21 | } 22 | initSelectDynamicLabel() 23 | 24 | // 声明 table 的列数据 25 | export const tableColumns = ref([]) 26 | // 监听选中项的变化,根据选中项动态改变 table 列数据的值 27 | watch( 28 | selectDynamicLabel, 29 | (val) => { 30 | tableColumns.value = [] 31 | // 遍历选中项 32 | const selectData = dynamicData.value.filter((item) => { 33 | return val.includes(item.label) 34 | }) 35 | tableColumns.value.push(...selectData) 36 | }, 37 | { 38 | immediate: true 39 | } 40 | ) 41 | -------------------------------------------------------------------------------- /src/views/article-ranking/index.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 132 | 133 | 163 | -------------------------------------------------------------------------------- /src/views/article-ranking/sortable/index.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import Sortable from 'sortablejs' 3 | import i18n from '@/i18n' 4 | import { articleSort } from '@/api/article' 5 | import { ElMessage } from 'element-plus' 6 | 7 | // 排序相关 8 | export const tableRef = ref(null) 9 | 10 | /** 11 | * 初始化排序 12 | */ 13 | export const initSortable = (tableData, cb) => { 14 | // 设置拖拽效果 15 | const el = tableRef.value.$el.querySelectorAll('.el-table__body > tbody')[0] 16 | // 1. 要拖拽的元素 17 | // 2. 配置对象 18 | Sortable.create(el, { 19 | // 拖拽时类名 20 | ghostClass: 'sortable-ghost', 21 | // 拖拽结束的回调方法 22 | async onEnd(event) { 23 | const { newIndex, oldIndex } = event 24 | // 修改数据 25 | await articleSort({ 26 | initRanking: tableData.value[oldIndex].ranking, 27 | finalRanking: tableData.value[newIndex].ranking 28 | }) 29 | ElMessage.success({ 30 | message: i18n.global.t('msg.article.sortSuccess'), 31 | type: 'success' 32 | }) 33 | // 直接重新获取数据无法刷新 table!! 34 | tableData.value = [] 35 | // 重新获取数据 36 | cb && cb() 37 | } 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /src/views/error-page/401.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/error-page/404.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/import/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/views/import/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 导入数据对应表 3 | */ 4 | export const USER_RELATIONS = { 5 | 姓名: 'username', 6 | 联系方式: 'mobile', 7 | 角色: 'role', 8 | 开通时间: 'openTime' 9 | } 10 | /** 11 | * 解析 excel 导入的时间格式 12 | */ 13 | export const formatDate = (numb) => { 14 | const time = new Date((numb - 1) * 24 * 3600000 + 1) 15 | time.setYear(time.getFullYear() - 70) 16 | const year = time.getFullYear() + '' 17 | const month = time.getMonth() + 1 + '' 18 | const date = time.getDate() - 1 + '' 19 | return ( 20 | year + 21 | '-' + 22 | (month < 10 ? '0' + month : month) + 23 | '-' + 24 | (date < 10 ? '0' + date : date) 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 120 | 121 | 212 | -------------------------------------------------------------------------------- /src/views/login/rules.js: -------------------------------------------------------------------------------- 1 | import i18n from '@/i18n' 2 | export const validatePassword = () => { 3 | return (rule, value, callback) => { 4 | if (value.length < 6) { 5 | callback(new Error(i18n.global.t('msg.login.passwordRule'))) 6 | } else { 7 | callback() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/views/permission-list/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/views/profile/components/Author.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 27 | 28 | 51 | -------------------------------------------------------------------------------- /src/views/profile/components/Chapter.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/views/profile/components/Feature.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 37 | -------------------------------------------------------------------------------- /src/views/profile/components/ProjectCard.vue: -------------------------------------------------------------------------------- 1 | 62 | 72 | 73 | 124 | -------------------------------------------------------------------------------- /src/views/profile/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 46 | 47 | 54 | -------------------------------------------------------------------------------- /src/views/role-list/components/DistributePermission.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 96 | -------------------------------------------------------------------------------- /src/views/role-list/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/views/user-info/index.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 124 | 125 | 169 | -------------------------------------------------------------------------------- /src/views/user-manage/components/Export2Excel.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 101 | -------------------------------------------------------------------------------- /src/views/user-manage/components/Export2ExcelConstants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 导入数据对应表 3 | */ 4 | export const USER_RELATIONS = { 5 | 姓名: 'username', 6 | 联系方式: 'mobile', 7 | 角色: 'role', 8 | 开通时间: 'openTime' 9 | } 10 | -------------------------------------------------------------------------------- /src/views/user-manage/components/roles.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | function resolve(dir) { 3 | return path.join(__dirname, dir) 4 | } 5 | // https://cli.vuejs.org/zh/guide/webpack.html#%E7%AE%80%E5%8D%95%E7%9A%84%E9%85%8D%E7%BD%AE%E6%96%B9%E5%BC%8F 6 | module.exports = { 7 | // webpack devserver 提供代理的功能,该代理可以把所有请求到当前的服务器的请求转发(代理)到另外的服务器上 8 | devServer: { 9 | // 配置反向代理 10 | proxy: { 11 | // 当地址中有/api的时候会触发代理机制 12 | '/api': { 13 | // 要代理的服务器地址 这里不用写 api 14 | target: 'https://api.imooc-admin.lgdsunday.club/', 15 | // target: 'http://127.0.0.1:4523/mock/797275', 16 | changeOrigin: true // 是否跨域 17 | } 18 | } 19 | }, 20 | chainWebpack(config) { 21 | // 设置 svg-sprite-loader 22 | config.module.rule('svg').exclude.add(resolve('src/icons')).end() 23 | config.resolve.alias.set('vue-i18n', 'vue-i18n/dist/vue-i18n.cjs.js') 24 | config.module 25 | .rule('icons') 26 | .test(/\.svg$/) 27 | .include.add(resolve('src/icons')) 28 | .end() 29 | .use('svg-sprite-loader') 30 | .loader('svg-sprite-loader') 31 | .options({ 32 | symbolId: 'icon-[name]' 33 | }) 34 | .end() 35 | } 36 | } 37 | --------------------------------------------------------------------------------