├── .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 |
2 |
3 |
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 |
2 |
3 |
4 |
8 |
9 | {{
10 | generateTitle(item.meta.title)
11 | }}
12 |
13 | {{
14 | generateTitle(item.meta.title)
15 | }}
16 |
17 |
18 |
19 |
20 |
21 |
59 |
60 |
83 |
--------------------------------------------------------------------------------
/src/components/Guide/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
28 |
29 |
30 |
130 |
131 |
167 |
--------------------------------------------------------------------------------
/src/components/LangSelect/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 中文
16 |
17 |
18 | English
19 |
20 |
21 |
22 |
23 |
24 |
25 |
53 |
--------------------------------------------------------------------------------
/src/components/PanThumb/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
35 |
128 |
--------------------------------------------------------------------------------
/src/components/Screenfull/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
12 |
13 |
45 |
46 |
61 |
--------------------------------------------------------------------------------
/src/components/TagsView/ContextMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
45 |
46 |
68 |
--------------------------------------------------------------------------------
/src/components/TagsView/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
16 | {{ tag.title }}
17 |
22 |
23 |
24 |
25 |
26 |
31 |
32 |
33 |
34 |
95 |
96 |
158 |
--------------------------------------------------------------------------------
/src/components/ThemeSelect/components/SelectColor.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
{{ $t('msg.theme.themeColorChange') }}
10 |
14 |
15 |
16 |
22 |
23 |
24 |
25 |
26 |
82 |
83 |
91 |
--------------------------------------------------------------------------------
/src/components/ThemeSelect/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ $t('msg.theme.themeColorChange') }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/components/UploadExcel/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ $t('msg.uploadExcel.upload') }}
6 |
7 |
8 |
9 |
16 |
17 |
23 |
24 | {{ $t('msg.uploadExcel.drop') }}
25 |
26 |
27 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
75 |
76 |
86 |
--------------------------------------------------------------------------------
/src/layout/components/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
36 |
37 |
38 |
39 |
55 |
56 |
110 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/MenuItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 | {{ generateTitle(title) }}
12 |
13 |
14 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/SidebarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
31 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/SidebarMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
18 |
19 |
20 |
21 |
41 |
--------------------------------------------------------------------------------
/src/layout/components/Sidebar/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 | imooc-admin
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
45 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
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 |
2 |
3 |
4 |
5 | {{
6 | $t('msg.article.commit')
7 | }}
8 |
9 |
10 |
11 |
12 |
86 |
87 |
95 |
--------------------------------------------------------------------------------
/src/views/article-create/components/Markdown.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{
7 | $t('msg.article.commit')
8 | }}
9 |
10 |
11 |
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 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
58 |
59 |
64 |
--------------------------------------------------------------------------------
/src/views/article-detail/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ detail.title }}
4 |
17 |
18 |
19 |
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 |
2 |
3 |
16 |
17 |
18 |
24 |
25 | {{ $filters.relativeTime(row.publicDate) }}
26 |
27 |
28 | {{ $t('msg.article.show') }}
34 | {{ $t('msg.article.remove') }}
40 |
41 |
42 |
43 |
44 |
55 |
56 |
57 |
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 |
2 | 401
3 |
4 |
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/views/error-page/404.vue:
--------------------------------------------------------------------------------
1 |
2 | 404
3 |
4 |
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/views/import/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
3 |
9 |
10 |
{{ $t('msg.login.title') }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
36 |
37 |
38 |
42 |
43 |
44 |
45 |
46 | {{ $t('msg.login.loginBtn') }}
53 |
54 |
55 |
56 |
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 |
2 |
3 |
4 |
12 |
17 |
18 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/views/profile/components/Author.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
18 | {{ $t('msg.profile.Introduction') }}
19 |
20 |
21 |
22 |
23 |
27 |
28 |
51 |
--------------------------------------------------------------------------------
/src/views/profile/components/Chapter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 | {{ item.content }}
11 |
12 |
13 |
14 |
15 |
16 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/views/profile/components/Feature.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
24 |
25 |
37 |
--------------------------------------------------------------------------------
/src/views/profile/components/ProjectCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
18 | Hello
19 | {{ $store.getters.userInfo.title }}
20 |
21 |
22 |
23 |
24 |
25 | {{ $store.getters.userInfo.username }}
26 |
27 |
28 | {{ $store.getters.userInfo.title }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
39 |
40 |
41 | {{ $t('msg.profile.muted') }}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
53 |
54 |
55 |
{{ item.title }}
56 |
57 |
58 |
59 |
60 |
61 |
62 |
72 |
73 |
124 |
--------------------------------------------------------------------------------
/src/views/profile/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
46 |
47 |
54 |
--------------------------------------------------------------------------------
/src/views/role-list/components/DistributePermission.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
96 |
--------------------------------------------------------------------------------
/src/views/role-list/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
17 |
23 | {{ $t('msg.role.assignPermissions') }}
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/views/user-info/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{
5 | $t('msg.userInfo.print')
6 | }}
7 |
8 |
9 |
10 |
11 |
{{ $t('msg.userInfo.title') }}
12 |
13 |
56 |
57 |
58 |
59 |
60 |
61 | -
62 |
63 | {{ $filters.dateFilter(item.startTime, 'YYYY/MM') }}
64 | ----
65 | {{ $filters.dateFilter(item.endTime, 'YYYY/MM') }}
67 | {{ item.title }}
68 | {{ item.desc }}
69 |
70 |
71 |
72 |
73 | {{ detailData.major }}
74 |
75 |
76 | {{ detailData.glory }}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
124 |
125 |
169 |
--------------------------------------------------------------------------------
/src/views/user-manage/components/Export2Excel.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
12 |
13 |
19 |
20 |
21 |
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 |
2 |
7 |
8 |
13 |
14 |
15 |
16 |
22 |
23 |
24 |
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 |
--------------------------------------------------------------------------------