├── .babelrc ├── .env ├── .env.development ├── .env.production ├── .eslintrc.js ├── .gitignore ├── .stylelintrc.js ├── .vscode ├── launch.json ├── settings.json └── vue.code-snippets ├── LICENSE ├── index.html ├── jest.config.js ├── mock ├── data │ └── user.ts ├── index.ts ├── mockProdServer.ts └── response.ts ├── package-lock.json ├── package.json ├── public ├── element-plus │ └── index@2.0.2.css └── favicon.ico ├── readme.md ├── src ├── App.vue ├── api │ ├── components │ │ └── index.ts │ └── layout │ │ └── index.ts ├── assets │ ├── css │ │ └── index.css │ └── img │ │ ├── 401.gif │ │ ├── 404.png │ │ ├── 404_cloud.png │ │ └── icon.png ├── components │ ├── CardList │ │ ├── CardList.vue │ │ └── CardListItem.vue │ ├── Echart │ │ └── index.ts │ ├── List │ │ └── index.vue │ ├── OpenWindow │ │ └── index.vue │ ├── SvnIcon │ │ ├── elIcon.ts │ │ └── index.vue │ └── TableSearch │ │ └── index.vue ├── config │ └── theme.ts ├── directive │ ├── action.ts │ ├── format.ts │ └── index.ts ├── icons │ └── svg │ │ ├── 404.svg │ │ ├── bug.svg │ │ ├── chart.svg │ │ ├── clipboard.svg │ │ ├── component.svg │ │ ├── dashboard.svg │ │ ├── documentation.svg │ │ ├── drag.svg │ │ ├── edit.svg │ │ ├── education.svg │ │ ├── email.svg │ │ ├── example.svg │ │ ├── excel.svg │ │ ├── exit-fullscreen.svg │ │ ├── eye-open.svg │ │ ├── eye.svg │ │ ├── form.svg │ │ ├── fullscreen.svg │ │ ├── guide.svg │ │ ├── icon.svg │ │ ├── international.svg │ │ ├── language.svg │ │ ├── link.svg │ │ ├── list.svg │ │ ├── lock.svg │ │ ├── message.svg │ │ ├── money.svg │ │ ├── nested.svg │ │ ├── password.svg │ │ ├── pdf.svg │ │ ├── people.svg │ │ ├── peoples.svg │ │ ├── qq.svg │ │ ├── search.svg │ │ ├── shopping.svg │ │ ├── size.svg │ │ ├── skill.svg │ │ ├── star.svg │ │ ├── tab.svg │ │ ├── table.svg │ │ ├── theme.svg │ │ ├── tree-table.svg │ │ ├── tree.svg │ │ ├── user.svg │ │ ├── wechat.svg │ │ └── zip.svg ├── layout │ ├── blank.vue │ ├── components │ │ ├── content.vue │ │ ├── menubar.vue │ │ ├── menubarItem.vue │ │ ├── navbar.vue │ │ ├── notice.vue │ │ ├── screenfull.vue │ │ ├── search.vue │ │ ├── sideSetting.vue │ │ ├── tags.vue │ │ └── theme.vue │ ├── index.vue │ └── redirect.vue ├── main.ts ├── permission.ts ├── router │ ├── asyncRouter.ts │ └── index.ts ├── store │ ├── index.ts │ └── modules │ │ └── layout.ts ├── type │ ├── config │ │ └── theme.ts │ ├── index.d.ts │ ├── shims-vue.d.ts │ ├── store │ │ └── layout.ts │ ├── utils │ │ └── tools.ts │ └── views │ │ └── Components │ │ └── TableSearchTest.ts ├── utils │ ├── animate.ts │ ├── base64.ts │ ├── changeThemeColor.ts │ ├── formExtend.ts │ ├── permission.ts │ ├── request.ts │ └── tools.ts └── views │ ├── Bud │ ├── BudApply.vue │ └── BudList.vue │ ├── Components │ ├── CardListTest.vue │ ├── ListTest.vue │ ├── OpenWindowTest.vue │ └── TableSearchTest.vue │ ├── Dashboard │ └── Workplace │ │ ├── Index.vue │ │ └── _Components │ │ ├── Chart.vue │ │ └── List.vue │ ├── ErrorPage │ ├── 401.vue │ └── 404.vue │ ├── Nav │ ├── SecondNav │ │ ├── Index.vue │ │ └── ThirdNav │ │ │ └── Index.vue │ └── SecondText │ │ ├── Index.vue │ │ └── ThirdText │ │ └── Index.vue │ ├── Permission │ └── Directive.vue │ ├── Project │ ├── ProjectDetail.vue │ ├── ProjectImport.vue │ └── ProjectList.vue │ └── User │ └── Login.vue ├── tailwind.config.js ├── test ├── components │ ├── CardList.spec.ts │ └── OpenWindow.spec.ts └── utils │ └── format.spec.ts ├── tsconfig.json └── vite.config.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]] 3 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_APP_TITLE = Vue3 ElementPlus Admin 2 | VITE_PORT = 3002 3 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsiangleev/element-plus-admin/bc03bad7fa0421b88d74f43c151a5d9732ba7e49/.env.development -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_PROXY = [["/api","http://localhost:3001"]] -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'vue-eslint-parser', 3 | parserOptions: { 4 | parser: '@typescript-eslint/parser', 5 | sourceType: 'module', 6 | ecmaFeatures: { 7 | jsx: true, 8 | tsx: true 9 | } 10 | }, 11 | env: { 12 | browser: true, 13 | node: true 14 | }, 15 | plugins: [ 16 | '@typescript-eslint' 17 | ], 18 | extends: [ 19 | 'plugin:@typescript-eslint/recommended', 20 | 'plugin:vue/vue3-recommended' 21 | ], 22 | rules: { 23 | 'vue/max-attributes-per-line': ['error', { 24 | singleline: { 25 | max: 5 26 | }, 27 | multiline: { 28 | max: 1 29 | } 30 | }], 31 | 'vue/singleline-html-element-content-newline': 'off', 32 | 'vue/multiline-html-element-content-newline':'off', 33 | 'vue/html-indent': ['error', 4], 34 | indent: ['error', 4], // 4行缩进 35 | 'vue/script-indent': ['error', 4], 36 | quotes: ['error', 'single'], // 单引号 37 | 'vue/html-quotes': ['error', 'single'], 38 | semi: ['error', 'never'], // 禁止使用分号 39 | 'space-infix-ops': ['error', { int32Hint: false }], // 要求操作符周围有空格 40 | 'no-multi-spaces': 'error', // 禁止多个空格 41 | 'no-whitespace-before-property': 'error', // 禁止在属性前使用空格 42 | 'space-before-blocks': 'error', // 在块之前强制保持一致的间距 43 | 'space-before-function-paren': ['error', 'never'], // 在“ function”定义打开括号之前强制不加空格 44 | 'space-in-parens': ['error', 'never'], // 强制括号左右的不加空格 45 | 'space-infix-ops': 'error', // 运算符之间留有间距 46 | 'spaced-comment': ['error', 'always'], // 注释间隔 47 | 'template-tag-spacing': ['error', 'always'], // 在模板标签及其文字之间需要空格 48 | 'no-var': 'error', 49 | 'prefer-destructuring': ['error', { // 优先使用数组和对象解构 50 | array: true, 51 | object: true 52 | }, { 53 | enforceForRenamedProperties: false 54 | }], 55 | // 组件名称为多个单词,忽略的组件名称 56 | 'vue/multi-word-component-names': ['off'], 57 | 'comma-dangle': ['error', 'never'], // 最后一个属性不允许有逗号 58 | 'arrow-spacing': 'error', // 箭头函数空格 59 | 'prefer-template': 'error', 60 | 'template-curly-spacing': 'error', 61 | 'quote-props': ['error', 'as-needed'], // 对象字面量属性名称使用引号 62 | 'object-curly-spacing': ['error', 'always'], // 强制在花括号中使用一致的空格 63 | 'no-unneeded-ternary': 'error', // 禁止可以表达为更简单结构的三元操作符 64 | 'no-restricted-syntax': ['error', 'WithStatement', 'BinaryExpression[operator="in"]'], // 禁止with/in语句 65 | 'no-lonely-if': 'error', // 禁止 if 语句作为唯一语句出现在 else 语句块中 66 | 'newline-per-chained-call': ['error', { ignoreChainWithDepth: 2 }], // 要求方法链中每个调用都有一个换行符 67 | // 路径别名设置 68 | 'no-submodule-imports': ['off', '/@'], 69 | 'no-implicit-dependencies': ['off', ['/@']], 70 | '@typescript-eslint/no-explicit-any': 'off' // 类型可以使用any 71 | } 72 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.local 4 | dist 5 | upload.ps1 -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | processors: [], 3 | plugins: [], 4 | extends: "stylelint-config-standard", // 这是官方推荐的方式 5 | ignoreFiles: ["node_modules/**", "dist/**"], 6 | rules: { 7 | "at-rule-no-unknown": [ true, { 8 | "ignoreAtRules": [ 9 | "responsive", 10 | "tailwind" 11 | ] 12 | }], 13 | "indentation": 4, // 4个空格 14 | "selector-pseudo-element-no-unknown": [true, { 15 | "ignorePseudoElements": ["v-deep"] 16 | }], 17 | "value-keyword-case": null 18 | } 19 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "edge", 9 | "request": "launch", 10 | "name": "element-plus-admin", 11 | "url": "http://localhost:3002", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true, 4 | "source.fixAll.tslint": true, 5 | "source.fixAll.stylelint": true 6 | }, 7 | "editor.tabSize": 4, 8 | "vetur.validation.template": false, 9 | "vetur.experimental.templateInterpolationService": true, 10 | "vetur.validation.interpolation": false, 11 | "scss.lint.unknownAtRules": "ignore", 12 | "css.validate": true, 13 | "scss.validate": true, 14 | "files.associations": { 15 | "*.css": "scss" 16 | }, 17 | "volar.tsPlugin": true, 18 | "volar.tsPluginStatus": false 19 | } -------------------------------------------------------------------------------- /.vscode/vue.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "console.log": { 3 | "scope": "javascript,typescript,vue", 4 | "prefix": "con", 5 | "body": [ 6 | "console.log($1)" 7 | ], 8 | "description": "Log output to console" 9 | }, 10 | "style postcss scoped": { 11 | "scope": "vue", 12 | "prefix": "style", 13 | "body": [ 14 | "" 17 | ], 18 | "description": "" 19 | }, 20 | "vue新建页面": { 21 | "scope": "vue", 22 | "prefix": "page", 23 | "body": [ 24 | "", 29 | 30 | "" 43 | ], 44 | "description": "" 45 | } 46 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 李祥 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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | '/@/(.*)$': '/src/$1' 4 | }, 5 | // 转义 6 | transform: { 7 | '^.+\\.vue$': 'vue-jest', 8 | '^.+\\js$': 'babel-jest', 9 | '^.+\\.(t|j)sx?$': 'ts-jest' 10 | }, 11 | moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node'] 12 | } -------------------------------------------------------------------------------- /mock/data/user.ts: -------------------------------------------------------------------------------- 1 | import { IMenubarList } from '/@/type/store/layout' 2 | export const user = [ 3 | { name: 'admin', pwd: 'admin' }, 4 | { name: 'dev', pwd: 'dev' }, 5 | { name: 'test', pwd: 'test' } 6 | ] 7 | 8 | export const role = [ 9 | { name: 'admin', description: '管理员' }, 10 | { name: 'dev', description: '开发人员' }, 11 | { name: 'test', description: '测试人员' } 12 | ] 13 | 14 | export const user_role = [ 15 | { userName: 'admin', roleName: 'admin' }, 16 | { userName: 'dev', roleName: 'dev' }, 17 | { userName: 'test', roleName: 'test' } 18 | ] 19 | 20 | export const permission = [ 21 | { name: 'add', description: '新增' }, 22 | { name: 'update', description: '修改' }, 23 | { name: 'remove', description: '删除' } 24 | ] 25 | 26 | export const role_route = [ 27 | { roleName: 'admin', id: 1, permission: [] }, 28 | { roleName: 'admin', id: 10, permission: [] }, 29 | { roleName: 'admin', id: 2, permission: [] }, 30 | { roleName: 'admin', id: 20, permission: [] }, 31 | { roleName: 'admin', id: 21, permission: [] }, 32 | { roleName: 'admin', id: 22, permission: [] }, 33 | { roleName: 'admin', id: 3, permission: [] }, 34 | { roleName: 'admin', id: 30, permission: [] }, 35 | { roleName: 'admin', id: 300, permission: [] }, 36 | { roleName: 'admin', id: 31, permission: [] }, 37 | { roleName: 'admin', id: 310, permission: [] }, 38 | { roleName: 'admin', id: 4, permission: [] }, 39 | { roleName: 'admin', id: 40, permission: [] }, 40 | { roleName: 'admin', id: 41, permission: [] }, 41 | { roleName: 'admin', id: 42, permission: [] }, 42 | { roleName: 'admin', id: 43, permission: [] }, 43 | { roleName: 'admin', id: 5, permission: [] }, 44 | { roleName: 'admin', id: 50, permission: ['add', 'update', 'remove'] }, 45 | 46 | { roleName: 'dev', id: 1, permission: [] }, 47 | { roleName: 'dev', id: 10, permission: [] }, 48 | { roleName: 'dev', id: 5, permission: [] }, 49 | { roleName: 'dev', id: 50, permission: ['add'] }, 50 | 51 | { roleName: 'test', id: 1, permission: [] }, 52 | { roleName: 'test', id: 10, permission: [] }, 53 | { roleName: 'test', id: 5, permission: [] }, 54 | { roleName: 'test', id: 50, permission: ['update'] } 55 | ] 56 | 57 | export const route:Array = [ 58 | { 59 | id: 2, 60 | parentId: 0, 61 | name: 'Project', 62 | path: '/Project', 63 | component: 'Layout', 64 | redirect: '/Project/ProjectList', 65 | meta: { title: '项目管理', icon: 'el-icon-phone' } 66 | }, 67 | { 68 | id: 20, 69 | parentId: 2, 70 | name: 'ProjectList', 71 | path: '/Project/ProjectList', 72 | component: 'ProjectList', 73 | meta: { title: '项目列表', icon: 'el-icon-goods' } 74 | }, 75 | { 76 | id: 21, 77 | parentId: 2, 78 | name: 'ProjectDetail', 79 | path: '/Project/ProjectDetail/:projName', 80 | component: 'ProjectDetail', 81 | meta: { title: '项目详情', icon: 'el-icon-question', activeMenu: '/Project/ProjectList', hidden: true } 82 | }, 83 | { 84 | id: 22, 85 | parentId: 2, 86 | name: 'ProjectImport', 87 | path: '/Project/ProjectImport', 88 | component: 'ProjectImport', 89 | meta: { title: '项目导入', icon: 'el-icon-help' } 90 | }, 91 | { 92 | id: 3, 93 | parentId: 0, 94 | name: 'Nav', 95 | path: '/Nav', 96 | component: 'Layout', 97 | redirect: '/Nav/SecondNav/ThirdNav', 98 | meta: { title: '多级导航', icon: 'el-icon-picture' } 99 | }, 100 | { 101 | id: 30, 102 | parentId: 3, 103 | name: 'SecondNav', 104 | path: '/Nav/SecondNav', 105 | redirect: '/Nav/SecondNav/ThirdNav', 106 | component: 'SecondNav', 107 | meta: { title: '二级导航', icon: 'el-icon-camera', alwaysShow: true } 108 | }, 109 | { 110 | id: 300, 111 | parentId: 30, 112 | name: 'ThirdNav', 113 | path: '/Nav/SecondNav/ThirdNav', 114 | component: 'ThirdNav', 115 | meta: { title: '三级导航', icon: 'el-icon-platform' } 116 | }, 117 | { 118 | id: 31, 119 | parentId: 3, 120 | name: 'SecondText', 121 | path: '/Nav/SecondText', 122 | redirect: '/Nav/SecondText/ThirdText', 123 | component: 'SecondText', 124 | meta: { title: '二级文本', icon: 'el-icon-opportunity', alwaysShow: true } 125 | }, 126 | { 127 | id: 310, 128 | parentId: 31, 129 | name: 'ThirdText', 130 | path: '/Nav/SecondText/ThirdText', 131 | component: 'ThirdText', 132 | meta: { title: '三级文本', icon: 'el-icon-menu' } 133 | }, 134 | { 135 | id: 4, 136 | parentId: 0, 137 | name: 'Components', 138 | path: '/Components', 139 | component: 'Layout', 140 | redirect: '/Components/OpenWindowTest', 141 | meta: { title: '组件测试', icon: 'el-icon-phone' } 142 | }, 143 | { 144 | id: 40, 145 | parentId: 4, 146 | name: 'OpenWindowTest', 147 | path: '/Components/OpenWindowTest', 148 | component: 'OpenWindowTest', 149 | meta: { title: '选择页', icon: 'el-icon-goods' } 150 | }, 151 | { 152 | id: 41, 153 | parentId: 4, 154 | name: 'CardListTest', 155 | path: '/Components/CardListTest', 156 | component: 'CardListTest', 157 | meta: { title: '卡片列表', icon: 'el-icon-question-filled' } 158 | }, 159 | { 160 | id: 42, 161 | parentId: 4, 162 | name: 'TableSearchTest', 163 | path: '/Components/TableSearchTest', 164 | component: 'TableSearchTest', 165 | meta: { title: '表格搜索', icon: 'el-icon-question-filled' } 166 | }, 167 | { 168 | id: 43, 169 | parentId: 4, 170 | name: 'ListTest', 171 | path: '/Components/ListTest', 172 | component: 'ListTest', 173 | meta: { title: '标签页列表', icon: 'el-icon-question-filled' } 174 | }, 175 | { 176 | id: 5, 177 | parentId: 0, 178 | name: 'Permission', 179 | path: '/Permission', 180 | component: 'Layout', 181 | redirect: '/Permission/Directive', 182 | meta: { title: '权限管理', icon: 'el-icon-phone', alwaysShow: true } 183 | }, 184 | { 185 | id: 50, 186 | parentId: 5, 187 | name: 'Directive', 188 | path: '/Permission/Directive', 189 | component: 'Directive', 190 | meta: { title: '指令管理', icon: 'el-icon-goods' } 191 | } 192 | ] -------------------------------------------------------------------------------- /mock/index.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from 'vite-plugin-mock' 2 | import { mock, Random } from 'mockjs' 3 | import { login, setToken, checkToken, getUser, getRoute } from '/mock/response' 4 | 5 | export interface IReq { 6 | body: any; 7 | query: any, 8 | headers: any; 9 | } 10 | 11 | Random.extend({ 12 | tag: function() { 13 | const tag = ['家', '公司', '学校', '超市'] 14 | return this.pick(tag) 15 | } 16 | }) 17 | interface ITableList { 18 | list: Array<{ 19 | date: string 20 | name: string 21 | address: string 22 | tag: '家' | '公司' | '学校' | '超市' 23 | amt: number 24 | }> 25 | } 26 | const tableList: ITableList = mock({ 27 | // 属性 list 的值是一个数组,其中含有 1 到 10 个元素 28 | 'list|100': [{ 29 | // 属性 id 是一个自增数,起始值为 1,每次增 1 30 | 'id|+1': 1, 31 | date: () => Random.date('yyyy-MM-dd'), 32 | name: () => Random.name(), 33 | address: () => Random.cparagraph(1), 34 | tag: () => Random.tag(), 35 | amt: () => Number(Random.float(-100000,100000).toFixed(2)) 36 | }] 37 | }) 38 | 39 | const responseData = (code: number, msg: string, data: any) => { 40 | return { 41 | Code: code, 42 | Msg: msg, 43 | Data: data 44 | } 45 | } 46 | 47 | export default [ 48 | { 49 | url: '/api/User/login', 50 | method: 'post', 51 | timeout: 300, 52 | response: (req: IReq) => { 53 | const { username, password } = req.body 54 | if(login(username, password)) return responseData(200, '登陆成功', setToken(username)) 55 | return responseData(401, '用户名或密码错误', '') 56 | } 57 | }, 58 | { 59 | url: '/api/User/getUser', 60 | method: 'get', 61 | timeout: 300, 62 | response: (req: IReq) => { 63 | const userName = checkToken(req) 64 | if(!userName) return responseData(401, '身份认证失败', '') 65 | return responseData(200, '', getUser(userName)) 66 | } 67 | }, 68 | { 69 | url: '/api/User/getRoute', 70 | method: 'get', 71 | timeout: 300, 72 | response: (req: IReq) => { 73 | const userName = checkToken(req) 74 | if(!userName) return responseData(401, '身份认证失败', '') 75 | return responseData(200, '', getRoute(userName)) 76 | } 77 | }, 78 | { 79 | url: '/api/getTableList', 80 | method: 'get', 81 | timeout: 600, 82 | response: (req: IReq) => { 83 | const userName = checkToken(req) 84 | if(!userName) return responseData(401, '身份认证失败', '') 85 | const { page, size, tag } = req.query 86 | const data = tag === '所有' ? tableList.list : tableList.list.filter(v => v.tag === tag) 87 | const d = { 88 | data: data.filter((v,i) => i >= (page - 1) * size && i < page * size), 89 | total: data.length 90 | } 91 | return responseData(200, '', d) 92 | } 93 | } 94 | ] as MockMethod[] 95 | -------------------------------------------------------------------------------- /mock/mockProdServer.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' 2 | 3 | import testModule from '/mock/index' 4 | 5 | export function setupProdMockServer():void { 6 | createProdMockServer([...testModule]) 7 | } -------------------------------------------------------------------------------- /mock/response.ts: -------------------------------------------------------------------------------- 1 | import { user, user_role, role_route, route } from '/mock/data/user' 2 | import { IMenubarList } from '/@/type/store/layout' 3 | import { IReq } from '/mock/index' 4 | 5 | export const setToken = function(name: string):string { 6 | return `token_${name}_token` 7 | } 8 | 9 | export const checkToken = function(req:IReq):string { 10 | const token = req.headers['access-token'] 11 | const match = token.match(/^token_([\w|\W]+?)_token/) 12 | const userName = match ? match[1] : '' 13 | return userName 14 | } 15 | 16 | export const login = function(name: string, pwd: string):boolean { 17 | return user.findIndex(v => v.name === name && v.pwd === pwd) !== -1 18 | } 19 | 20 | export const getUser = function(name: string):{name:string, role: Array} { 21 | return { 22 | name, 23 | role: user_role.filter(v => v.userName === name).map(v => v.roleName) 24 | } 25 | } 26 | 27 | export const getRoute = function(name: string):Array { 28 | const { role } = getUser(name) 29 | const arr = role_route.filter(v => role.findIndex(val => val === v.roleName) !== -1) 30 | const filterRoute:Array = [] 31 | route.forEach(v => { 32 | arr.forEach(val => { 33 | if(val.id === v.id) { 34 | const obj = Object.assign({},v) 35 | obj.meta.permission = val.permission 36 | filterRoute.push(obj) 37 | } 38 | }) 39 | }) 40 | return filterRoute 41 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "hsianglee", 3 | "name": "element-plus-admin", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vuedx-typecheck . && vite build", 8 | "preview": "vite preview", 9 | "test": "jest ./test", 10 | "eslint": "npx eslint . --ext .js,.jsx,.ts,.tsx --fix", 11 | "stylelint": "npx stylelint **/*.{css,vue} --fix" 12 | }, 13 | "dependencies": { 14 | "echarts": "^5.3.0", 15 | "element-plus": "^2.0.2", 16 | "vue": "^3.2.31" 17 | }, 18 | "license": "MIT", 19 | "repository": "https://github.com/hsiangleev/element-plus-admin", 20 | "devDependencies": { 21 | "@babel/preset-env": "^7.12.11", 22 | "@element-plus/icons-vue": "^0.2.7", 23 | "@testing-library/jest-dom": "^5.11.8", 24 | "@types/jest": "^26.0.20", 25 | "@types/mockjs": "^1.0.3", 26 | "@types/node": "^14.14.20", 27 | "@types/nprogress": "^0.2.0", 28 | "@types/pinyin": "^2.8.2", 29 | "@typescript-eslint/eslint-plugin": "^5.12.1", 30 | "@typescript-eslint/parser": "^5.12.1", 31 | "@vitejs/plugin-vue": "^2.2.2", 32 | "@vue/compiler-sfc": "^3.2.31", 33 | "@vue/test-utils": "^2.0.0-beta.13", 34 | "@vuedx/typecheck": "^0.7.4", 35 | "@vuedx/typescript-plugin-vue": "^0.7.4", 36 | "autoprefixer": "^10.1.0", 37 | "axios": "^0.21.1", 38 | "babel-jest": "^26.6.3", 39 | "eslint": "^8.9.0", 40 | "eslint-plugin-vue": "^8.4.1", 41 | "fuse.js": "^6.4.6", 42 | "jest": "^26.6.3", 43 | "jsencrypt": "^3.1.0", 44 | "mockjs": "^1.1.0", 45 | "nprogress": "^0.2.0", 46 | "pinia": "^2.0.11", 47 | "pinyin": "^2.10.2", 48 | "postcss": "^8.4.6", 49 | "postcss-import": "^14.0.2", 50 | "postcss-simple-vars": "^6.0.3", 51 | "screenfull": "^5.1.0", 52 | "stylelint": "^13.8.0", 53 | "stylelint-config-standard": "^20.0.0", 54 | "tailwindcss": "^3.0.23", 55 | "ts-jest": "^26.4.4", 56 | "tslint": "^6.1.3", 57 | "typescript": "^4.5.5", 58 | "vite": "^2.8.4", 59 | "vite-plugin-mock": "^2.6.3", 60 | "vite-plugin-svg-icons": "^0.7.0", 61 | "vue-eslint-parser": "^8.2.0", 62 | "vue-jest": "^5.0.0-alpha.7", 63 | "vue-router": "^4.0.12" 64 | }, 65 | "browserslist": [ 66 | "> 1%", 67 | "last 2 versions", 68 | "not ie <= 10" 69 | ], 70 | "eslintIgnore": [ 71 | "node_modules", 72 | "dist" 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsiangleev/element-plus-admin/bc03bad7fa0421b88d74f43c151a5d9732ba7e49/public/favicon.ico -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 |

7 | 8 | vue 9 | 10 | 11 | element-plus 12 | 13 | 14 | vite 15 | 16 | 17 | typescript 18 | 19 | 20 | postcss 21 | 22 | 23 | license 24 | 25 |

26 | 27 | ## 简介 28 | 29 | [element-plus-admin](https://github.com/hsiangleev/element-plus-admin) 是一个后台前端解决方案,它基于 [vue-next](https://github.com/vuejs/vue-next) 和 [element-plus](https://github.com/element-plus/element-plus)实现。它使用了最新的前端技术栈vite,typescript和postcss构建,内置了 动态路由,权限验证,皮肤更换,提供了丰富的功能组件,它可以帮助你快速搭建中后台产品原型。 30 | 31 | - [在线预览](https://element-plus-admin.hsianglee.cn/) 32 | 33 | - [Gitee](https://gitee.com/hsiangleev/element-plus-admin) 34 | 35 | ## 前序准备 36 | 37 | 你需要在本地安装 [node](http://nodejs.org/) 和 [git](https://git-scm.com/)。本项目技术栈基于 [ES2015+](http://es6.ruanyifeng.com/)、[vue-next](https://github.com/vuejs/vue-next)、[typescript](https://github.com/microsoft/TypeScript)、[vite](https://github.com/vitejs/vite)、[postcss](https://github.com/postcss/postcss) 和 [element-plus](https://github.com/element-plus/element-plus),所有的请求数据都使用[Mock.js](https://github.com/nuysoft/Mock)进行模拟,提前了解和学习这些知识会对使用本项目有很大的帮助。 38 | 39 |

40 | 41 |

42 | 43 | ## 开发 44 | 45 | ```bash 46 | # 克隆项目 47 | git clone https://github.com/hsiangleev/element-plus-admin.git 48 | 49 | # 进入项目目录 50 | cd element-plus-admin 51 | 52 | # 安装依赖 53 | npm install 54 | 55 | # 启动服务 56 | npm run dev 57 | ``` 58 | 59 | 浏览器访问 http://localhost:3002 60 | 61 | ## 发布 62 | 63 | ```bash 64 | # 发布 65 | npm run build 66 | 67 | # 预览 68 | npm run preview 69 | ``` 70 | 71 | ## 其它 72 | 73 | ```bash 74 | # eslint代码校验 75 | npm run eslint 76 | 77 | # stylelint代码校验 78 | npm run stylelint 79 | ``` 80 | 81 | ## 浏览器 82 | 83 | **目前仅支持现代浏览器** 84 | 85 | | [IE / Edge](https://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](https://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](https://godban.github.io/browsers-support-badges/)
Chrome | [Safari](https://godban.github.io/browsers-support-badges/)
Safari | 86 | | --------- | --------- | --------- | --------- | 87 | | Edge | last 2 versions | last 2 versions | last 2 versions | 88 | 89 | ## 捐赠 90 | 91 | 如果你觉得这个项目帮助到了你,你可以帮作者买一杯果汁表示鼓励 :tropical_drink:
92 | ![donate](https://images.hsianglee.cn/pay/pay.png?v=0.0.2) 93 | 94 | ## License 95 | 96 | [MIT](https://github.com/hsiangleev/element-plus-admin/blob/master/LICENSE) 97 | 98 | Copyright (c) 2020-present hsiangleev 99 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 43 | 44 | -------------------------------------------------------------------------------- /src/api/components/index.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request' 2 | import { AxiosResponse } from 'axios' 3 | const api = { 4 | getTableList: '/api/getTableList' 5 | } 6 | export type ITag = '所有' | '家' | '公司' | '学校' | '超市' 7 | export interface ITableList { 8 | page: number 9 | size: number 10 | tag: ITag 11 | } 12 | export function getTableList(tableList: ITableList): Promise> { 13 | return request({ 14 | url: api.getTableList, 15 | method: 'get', 16 | params: tableList 17 | }) 18 | } -------------------------------------------------------------------------------- /src/api/layout/index.ts: -------------------------------------------------------------------------------- 1 | import request from '/@/utils/request' 2 | import { AxiosResponse } from 'axios' 3 | import { IMenubarList } from '/@/type/store/layout' 4 | 5 | const api = { 6 | login: '/api/User/login', 7 | getUser: '/api/User/getUser', 8 | getRouterList: '/api/User/getRoute', 9 | publickey: '/api/User/Publickey' 10 | } 11 | 12 | export interface loginParam { 13 | username: string, 14 | password: string 15 | } 16 | 17 | export function login(param: loginParam):Promise>> { 18 | return request({ 19 | url: api.login, 20 | method: 'post', 21 | data: param 22 | }) 23 | } 24 | 25 | export function publickey():Promise>> { 26 | return request({ 27 | url: api.publickey, 28 | method: 'get' 29 | }) 30 | } 31 | 32 | interface IGetuserRes { 33 | name: string 34 | role: Array 35 | } 36 | 37 | export function getUser(): Promise>> { 38 | return request({ 39 | url: api.getUser, 40 | method: 'get' 41 | }) 42 | } 43 | export function getRouterList(): Promise>>> { 44 | return request({ 45 | url: api.getRouterList, 46 | method: 'get' 47 | }) 48 | } -------------------------------------------------------------------------------- /src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | overflow: hidden; 4 | margin: 0; 5 | } 6 | 7 | /*! @import */ 8 | @tailwind base; 9 | @tailwind components; 10 | @tailwind utilities; 11 | 12 | button:focus { 13 | outline: none; 14 | } 15 | 16 | @layer components { 17 | .transition-width { 18 | transition-property: width; 19 | } 20 | 21 | .flex-center { 22 | justify-content: center; 23 | align-items: center; 24 | } 25 | 26 | .min-height-10 { 27 | min-height: 2.5rem; 28 | } 29 | 30 | .min-width-32 { 31 | min-width: 8rem; 32 | } 33 | 34 | .leading-12 { 35 | line-height: 3rem; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/img/401.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsiangleev/element-plus-admin/bc03bad7fa0421b88d74f43c151a5d9732ba7e49/src/assets/img/401.gif -------------------------------------------------------------------------------- /src/assets/img/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsiangleev/element-plus-admin/bc03bad7fa0421b88d74f43c151a5d9732ba7e49/src/assets/img/404.png -------------------------------------------------------------------------------- /src/assets/img/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsiangleev/element-plus-admin/bc03bad7fa0421b88d74f43c151a5d9732ba7e49/src/assets/img/404_cloud.png -------------------------------------------------------------------------------- /src/assets/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsiangleev/element-plus-admin/bc03bad7fa0421b88d74f43c151a5d9732ba7e49/src/assets/img/icon.png -------------------------------------------------------------------------------- /src/components/CardList/CardList.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 74 | 75 | 146 | -------------------------------------------------------------------------------- /src/components/CardList/CardListItem.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/Echart/index.ts: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts/core' 2 | import { 3 | BarChart, 4 | // 系列类型的定义后缀都为 SeriesOption 5 | BarSeriesOption, 6 | LineChart, 7 | RadarChart, 8 | PieChart, 9 | PieSeriesOption, 10 | RadarSeriesOption, 11 | LineSeriesOption 12 | } from 'echarts/charts' 13 | import { 14 | TitleComponent, 15 | TooltipComponent, 16 | RadarComponent, 17 | GridComponent, 18 | LegendComponent, 19 | // 组件类型的定义后缀都为 ComponentOption 20 | TitleComponentOption, 21 | GridComponentOption, 22 | LegendComponentOption 23 | } from 'echarts/components' 24 | import { 25 | CanvasRenderer 26 | } from 'echarts/renderers' 27 | 28 | // 通过 ComposeOption 来组合出一个只有必须组件和图表的 Option 类型 29 | type ECOption = echarts.ComposeOption< 30 | BarSeriesOption | LineSeriesOption | TitleComponentOption | GridComponentOption | RadarSeriesOption | PieSeriesOption | LegendComponentOption 31 | >; 32 | 33 | // 注册必须的组件 34 | echarts.use( 35 | [TitleComponent, TooltipComponent, GridComponent, BarChart, LineChart, RadarChart, RadarComponent, PieChart, CanvasRenderer, LegendComponent] 36 | ) 37 | 38 | export { 39 | ECOption, 40 | echarts 41 | } -------------------------------------------------------------------------------- /src/components/List/index.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 95 | 96 | -------------------------------------------------------------------------------- /src/components/OpenWindow/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 48 | 49 | 65 | -------------------------------------------------------------------------------- /src/components/SvnIcon/elIcon.ts: -------------------------------------------------------------------------------- 1 | 2 | import { h, defineComponent, Component, resolveComponent } from 'vue' 3 | 4 | export function UseElIcon(icon: string, color = 'inherit', size?: number | string): Component { 5 | return defineComponent({ 6 | name: 'UseElIcon', 7 | render() { 8 | // return h(resolveComponent(icon)) 9 | return h(resolveComponent('el-icon'), { 10 | color: color, 11 | size: size || '' 12 | }, () => [ 13 | h(resolveComponent(icon)) 14 | ]) 15 | } 16 | }) 17 | } -------------------------------------------------------------------------------- /src/components/SvnIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/TableSearch/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 76 | 77 | -------------------------------------------------------------------------------- /src/config/theme.ts: -------------------------------------------------------------------------------- 1 | import { ITheme } from '/@/type/config/theme' 2 | import { useLayoutStore } from '/@/store/modules/layout' 3 | 4 | const theme:() => ITheme[] = () => { 5 | const { color } = useLayoutStore().getSetting 6 | return [ 7 | { 8 | tagsActiveColor: '#fff', 9 | tagsActiveBg: color.primary, 10 | mainBg: '#f0f2f5', 11 | sidebarColor: '#fff', 12 | sidebarBg: '#001529', 13 | sidebarChildrenBg: '#000c17', 14 | sidebarActiveColor: '#fff', 15 | sidebarActiveBg: color.primary, 16 | sidebarActiveBorderRightBG: '#1890ff' 17 | }, 18 | { 19 | tagsActiveColor: '#fff', 20 | tagsActiveBg: color.primary, 21 | navbarColor: '#fff', 22 | navbarBg: '#393D49', 23 | mainBg: '#f0f2f5', 24 | sidebarColor: '#fff', 25 | sidebarBg: '#001529', 26 | sidebarChildrenBg: '#000c17', 27 | sidebarActiveColor: '#fff', 28 | sidebarActiveBg: color.primary, 29 | sidebarActiveBorderRightBG: '#1890ff' 30 | }, 31 | { 32 | tagsActiveColor: '#fff', 33 | tagsActiveBg: color.primary, 34 | mainBg: '#f0f2f5', 35 | sidebarColor: '#333', 36 | sidebarBg: '#fff', 37 | sidebarChildrenBg: '#fff', 38 | sidebarActiveColor: color.primary, 39 | sidebarActiveBg: '#e6f7ff', 40 | sidebarActiveBorderRightBG: color.primary 41 | }, 42 | { 43 | logoColor: 'rgba(255,255,255,.7)', 44 | logoBg: '#50314F', 45 | tagsColor: '#333', 46 | tagsBg: '#fff', 47 | tagsActiveColor: '#fff', 48 | tagsActiveBg: '#7A4D7B', 49 | mainBg: '#f0f2f5', 50 | sidebarColor: 'rgba(255,255,255,.7)', 51 | sidebarBg: '#50314F', 52 | sidebarChildrenBg: '#382237', 53 | sidebarActiveColor: '#fff', 54 | sidebarActiveBg: '#7A4D7B', 55 | sidebarActiveBorderRightBG: '#7A4D7B' 56 | }, 57 | { 58 | logoColor: 'rgba(255,255,255,.7)', 59 | logoBg: '#50314F', 60 | navbarColor: 'rgba(255,255,255,.7)', 61 | navbarBg: '#50314F', 62 | tagsColor: '#333', 63 | tagsBg: '#fff', 64 | tagsActiveColor: '#fff', 65 | tagsActiveBg: '#7A4D7B', 66 | mainBg: '#f0f2f5', 67 | sidebarColor: 'rgba(255,255,255,.7)', 68 | sidebarBg: '#50314F', 69 | sidebarChildrenBg: '#382237', 70 | sidebarActiveColor: '#fff', 71 | sidebarActiveBg: '#7A4D7B', 72 | sidebarActiveBorderRightBG: '#7A4D7B' 73 | } 74 | ] 75 | } 76 | 77 | export default theme -------------------------------------------------------------------------------- /src/directive/action.ts: -------------------------------------------------------------------------------- 1 | import { App, DirectiveBinding } from 'vue' 2 | import { checkPermission, IPermissionType } from '/@/utils/permission' 3 | 4 | const actionPermission = (el:HTMLElement, binding:DirectiveBinding) => { 5 | const value:Array = typeof binding.value === 'string' ? [binding.value] : binding.value 6 | const arg:IPermissionType = binding.arg === 'and' ? 'and' : 'or' 7 | if(!checkPermission(value, arg)) { 8 | el.parentNode && el.parentNode.removeChild(el) 9 | } 10 | } 11 | 12 | export default (app:App):void => { 13 | app.directive('action', { 14 | mounted: (el, binding) => actionPermission(el, binding) 15 | }) 16 | } -------------------------------------------------------------------------------- /src/directive/format.ts: -------------------------------------------------------------------------------- 1 | import { App, nextTick } from 'vue' 2 | import { format, unformat } from '/@/utils/tools' 3 | 4 | export default (app:App):void => { 5 | app.directive('format', { 6 | beforeMount(el, binding) { 7 | const { arg, value } = binding 8 | if(arg === 'money') { 9 | const elem = el.firstElementChild 10 | nextTick(() => elem.value = format(elem.value)) 11 | elem.addEventListener('focus', (event:MouseEvent) => { 12 | if(!event.target) return 13 | const target = event.target as HTMLInputElement 14 | target.value = String(unformat(target.value)) 15 | value[0][value[1]] = target.value 16 | }, true) 17 | elem.addEventListener('blur', (event: MouseEvent) => { 18 | if(!event.target) return 19 | const target = event.target as HTMLInputElement 20 | const val = unformat(format(target.value)) 21 | value[0][value[1]] = val === '' ? 0 : val 22 | nextTick(() => target.value = format(val)) 23 | }, true) 24 | } 25 | } 26 | }) 27 | } -------------------------------------------------------------------------------- /src/directive/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | 3 | const modules = import.meta.glob('../directive/**/**.ts') 4 | // 自动导入当前文件夹下的所有自定义指令(默认导出项) 5 | export default (app:App):void => { 6 | for (const path in modules) { 7 | // 排除当前文件 8 | if(path !== '../directive/index.ts') { 9 | modules[path]().then((mod) => { 10 | mod.default(app) 11 | }) 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/icons/svg/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/component.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/drag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/education.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/excel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/exit-fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/international.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/language.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/message.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/money.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/pdf.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/peoples.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/qq.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/skill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/star.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree-table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/wechat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/svg/zip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/blank.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/layout/components/content.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 45 | 46 | -------------------------------------------------------------------------------- /src/layout/components/menubar.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /src/layout/components/menubarItem.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | -------------------------------------------------------------------------------- /src/layout/components/navbar.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 113 | 114 | -------------------------------------------------------------------------------- /src/layout/components/notice.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 61 | 62 | -------------------------------------------------------------------------------- /src/layout/components/screenfull.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/layout/components/search.vue: -------------------------------------------------------------------------------- 1 | 19 | 146 | 147 | -------------------------------------------------------------------------------- /src/layout/components/sideSetting.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 99 | -------------------------------------------------------------------------------- /src/layout/components/tags.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | -------------------------------------------------------------------------------- /src/layout/components/theme.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 86 | 87 | -------------------------------------------------------------------------------- /src/layout/redirect.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from '/@/App.vue' 3 | import ElementPlus from 'element-plus' 4 | import direct from '/@/directive/index' 5 | import router from '/@/router/index' 6 | import { pinia } from '/@/store' 7 | import '/@/permission' 8 | import '/@/assets/css/index.css' 9 | 10 | import 'element-plus/dist/index.css' 11 | import 'element-plus/theme-chalk/display.css' 12 | import 'nprogress/nprogress.css' 13 | import 'virtual:svg-icons-register' 14 | import SvgIcon from '/@/components/SvnIcon/index.vue' 15 | 16 | import * as ElIcons from '@element-plus/icons-vue' 17 | 18 | 19 | const app = createApp(App) 20 | direct(app) 21 | app.use(ElementPlus) 22 | app.use(router) 23 | app.use(pinia) 24 | app.component('SvgIcon', SvgIcon) 25 | 26 | const ElIconsData = ElIcons as unknown as Array<() => Promise> 27 | for (const iconName in ElIconsData) { 28 | app.component(`ElIcon${iconName}`, ElIconsData[iconName]) 29 | } 30 | 31 | app.mount('#app') -------------------------------------------------------------------------------- /src/permission.ts: -------------------------------------------------------------------------------- 1 | import router from '/@/router' 2 | import { configure, start, done } from 'nprogress' 3 | import { RouteRecordRaw } from 'vue-router' 4 | import { decode, encode } from '/@/utils/tools' 5 | import { useLayoutStore } from '/@/store/modules/layout' 6 | import { useLocal } from '/@/utils/tools' 7 | 8 | configure({ showSpinner: false }) 9 | 10 | const loginRoutePath = '/Login' 11 | const defaultRoutePath = '/' 12 | 13 | router.beforeEach(async(to, from) => { 14 | start() 15 | const { getStatus, getMenubar, getTags, setToken, logout, GenerateRoutes, getUser, concatAllowRoutes, changeTagNavList, addCachedViews, changeNocacheViewStatus } = useLayoutStore() 16 | // 修改页面title 17 | const reg = new RegExp(/^(.+)(\s\|\s.+)$/) 18 | const appTitle = import.meta.env.VITE_APP_TITLE 19 | document.title = !to.meta.title 20 | ? appTitle 21 | : appTitle.match(reg) 22 | ? appTitle.replace(reg, `${to.meta.title}$2`) 23 | : `${to.meta.title} | ${appTitle}` 24 | // 判断当前是否在登陆页面 25 | if (to.path.toLocaleLowerCase() === loginRoutePath.toLocaleLowerCase()) { 26 | done() 27 | if(getStatus.ACCESS_TOKEN) return typeof to.query.from === 'string' ? decode(to.query.from) : defaultRoutePath 28 | return 29 | } 30 | // 判断是否登录 31 | if(!getStatus.ACCESS_TOKEN) { 32 | return loginRoutePath + (to.fullPath ? `?from=${encode(to.fullPath)}` : '') 33 | } 34 | 35 | // 前端检查token是否失效 36 | useLocal('token') 37 | .then(d => setToken(d.ACCESS_TOKEN)) 38 | .catch(() => logout()) 39 | 40 | 41 | // 判断是否还没添加过路由 42 | if(getMenubar.menuList.length === 0) { 43 | await GenerateRoutes() 44 | await getUser() 45 | for(let i = 0;i < getMenubar.menuList.length;i++) { 46 | router.addRoute(getMenubar.menuList[i] as RouteRecordRaw) 47 | } 48 | concatAllowRoutes() 49 | return to.fullPath 50 | } 51 | changeTagNavList(to) // 切换导航,记录打开的导航(标签页) 52 | 53 | // 离开当前页面时是否需要添加当前页面缓存 54 | !new RegExp(/^\/redirect\//).test(from.path) 55 | && getTags.tagsList.some(v => v.name === from.name) 56 | && !getTags.cachedViews.some(v => v === from.name) 57 | && !getTags.isNocacheView 58 | && addCachedViews({ name: from.name as string, noCache: from.meta.noCache as boolean }) 59 | 60 | // 缓存重置 61 | changeNocacheViewStatus(false) 62 | }) 63 | 64 | router.afterEach(() => { 65 | done() 66 | }) -------------------------------------------------------------------------------- /src/router/asyncRouter.ts: -------------------------------------------------------------------------------- 1 | import { IMenubarList } from '/@/type/store/layout' 2 | import { listToTree } from '/@/utils/tools' 3 | import { useLayoutStore } from '/@/store/modules/layout' 4 | 5 | // 动态路由名称映射表 6 | const modules = import.meta.glob('../views/**/**.vue') 7 | const components:IObject<() => Promise> = { 8 | Layout: (() => import('/@/layout/index.vue')) as unknown as () => Promise 9 | } 10 | Object.keys(modules).forEach(key => { 11 | const nameMatch = key.match(/^\.\.\/views\/(.+)\.vue/) 12 | if(!nameMatch) return 13 | // 排除_Components文件夹下的文件 14 | if(nameMatch[1].includes('_Components')) return 15 | // 如果页面以Index命名,则使用父文件夹作为name 16 | const indexMatch = nameMatch[1].match(/(.*)\/Index$/i) 17 | let name = indexMatch ? indexMatch[1] : nameMatch[1]; 18 | [name] = name.split('/').splice(-1) 19 | components[name] = modules[key] as () => Promise 20 | }) 21 | 22 | const asyncRouter:IMenubarList[] = [ 23 | { 24 | path: '/:pathMatch(.*)*', 25 | name: 'NotFound', 26 | component: components['404'], 27 | meta: { 28 | title: 'NotFound', 29 | icon: '', 30 | hidden: true 31 | }, 32 | redirect: { 33 | name: '404' 34 | } 35 | } 36 | ] 37 | 38 | const generatorDynamicRouter = (data:IMenubarList[]):void => { 39 | const { setRoutes } = useLayoutStore() 40 | const routerList:IMenubarList[] = listToTree(data, 0) 41 | asyncRouter.forEach(v => routerList.push(v)) 42 | const f = (data:IMenubarList[], pData:IMenubarList|null) => { 43 | for(let i = 0,len = data.length;i < len;i++) { 44 | const v:IMenubarList = data[i] 45 | if(typeof v.component === 'string') v.component = components[v.component] 46 | if(!v.meta.permission || pData && v.meta.permission.length === 0) { 47 | v.meta.permission = pData && pData.meta && pData.meta.permission ? pData.meta.permission : [] 48 | } 49 | if(v.children && v.children.length > 0) { 50 | f(v.children, v) 51 | } 52 | } 53 | } 54 | f(routerList, null) 55 | setRoutes(routerList) 56 | } 57 | 58 | export { 59 | components, 60 | generatorDynamicRouter 61 | } 62 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' 2 | import { IMenubarList } from '/@/type/store/layout' 3 | import { components } from '/@/router/asyncRouter' 4 | 5 | const Components:IObject<() => Promise> = Object.assign({}, components, { 6 | Layout: (() => import('/@/layout/index.vue')) as unknown as () => Promise, 7 | Redirect: (() => import('/@/layout/redirect.vue')) as unknown as () => Promise, 8 | LayoutBlank: (() => import('/@/layout/blank.vue')) as unknown as () => Promise 9 | }) 10 | 11 | // 静态路由页面 12 | export const allowRouter:Array = [ 13 | { 14 | name: 'Dashboard', 15 | path: '/', 16 | component: Components['Layout'], 17 | redirect: '/Dashboard/Workplace', 18 | meta: { title: '仪表盘', icon: 'el-icon-eleme' }, 19 | children: [ 20 | { 21 | name: 'Workplace', 22 | path: '/Dashboard/Workplace', 23 | component: Components['Workplace'], 24 | meta: { title: '工作台', icon: 'el-icon-tools' } 25 | } 26 | // { 27 | // name: 'Welcome', 28 | // path: '/Dashboard/Welcome', 29 | // component: Components['Welcome'], 30 | // meta: { title: '欢迎页', icon: 'el-icon-tools' } 31 | // } 32 | ] 33 | }, 34 | { 35 | name: 'ErrorPage', 36 | path: '/ErrorPage', 37 | meta: { title: '错误页面', icon: 'el-icon-eleme' }, 38 | component: Components.Layout, 39 | redirect: '/ErrorPage/404', 40 | children: [ 41 | { 42 | name: '401', 43 | path: '/ErrorPage/401', 44 | component: Components['401'], 45 | meta: { title: '401', icon: 'el-icon-tools' } 46 | }, 47 | { 48 | name: '404', 49 | path: '/ErrorPage/404', 50 | component: Components['404'], 51 | meta: { title: '404', icon: 'el-icon-tools' } 52 | } 53 | ] 54 | }, 55 | { 56 | name: 'RedirectPage', 57 | path: '/redirect', 58 | component: Components['Layout'], 59 | meta: { title: '重定向页面', icon: 'el-icon-eleme', hidden: true }, 60 | children: [ 61 | { 62 | name: 'Redirect', 63 | path: '/redirect/:pathMatch(.*)*', 64 | meta: { 65 | title: '重定向页面', 66 | icon: '' 67 | }, 68 | component: Components.Redirect 69 | } 70 | ] 71 | }, 72 | { 73 | name: 'Login', 74 | path: '/Login', 75 | component: Components.Login, 76 | meta: { title: '登录', icon: 'el-icon-eleme', hidden: true } 77 | } 78 | ] 79 | 80 | const router = createRouter({ 81 | history: createWebHashHistory(), // createWebHistory 82 | routes: allowRouter as RouteRecordRaw[] 83 | }) 84 | 85 | export default router -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | export const pinia = createPinia() 3 | -------------------------------------------------------------------------------- /src/type/config/theme.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ITheme { 3 | // logo不传则使用侧边栏sidebar样式 4 | logoColor?: string 5 | logoBg?: string 6 | // 顶部导航栏和标签栏不传则背景色使用白色,字体颜色默认 7 | navbarColor?: string 8 | navbarBg?: string 9 | tagsColor?: string 10 | tagsBg?: string 11 | tagsActiveColor?: string 12 | tagsActiveBg?: string 13 | mainBg: string 14 | sidebarColor: string 15 | sidebarBg: string 16 | sidebarChildrenBg: string 17 | sidebarActiveColor: string 18 | sidebarActiveBg: string 19 | sidebarActiveBorderRightBG?: string 20 | } -------------------------------------------------------------------------------- /src/type/index.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | declare global { 3 | interface IResponse { 4 | Code: number; 5 | Msg: string; 6 | Data: T; 7 | } 8 | interface IObject { 9 | [index: string]: T 10 | } 11 | 12 | interface ITable { 13 | data : Array 14 | total: number 15 | page: number 16 | size: number 17 | } 18 | interface ImportMetaEnv { 19 | VITE_APP_TITLE: string 20 | VITE_PORT: number; 21 | VITE_PROXY: string; 22 | } 23 | } -------------------------------------------------------------------------------- /src/type/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { defineComponent } from 'vue' 3 | const Component: ReturnType 4 | export default Component 5 | } -------------------------------------------------------------------------------- /src/type/store/layout.ts: -------------------------------------------------------------------------------- 1 | export enum IMenubarStatus { 2 | PCE, // 电脑展开 3 | PCN, // 电脑合并 4 | PHE, // 手机展开 5 | PHN // 手机合并 6 | } 7 | export interface ISetting { 8 | theme: number 9 | showTags: boolean 10 | color: { 11 | primary: string 12 | } 13 | usePinyinSearch: boolean 14 | mode: 'horizontal' | 'vertical' // 导航模式 15 | } 16 | export interface IMenubar { 17 | status: IMenubarStatus 18 | menuList: Array 19 | isPhone: boolean 20 | } 21 | export interface IUserInfo { 22 | name: string, 23 | role: string[] 24 | } 25 | export interface ITags { 26 | tagsList: Array 27 | cachedViews: string[] 28 | isNocacheView: boolean 29 | } 30 | export interface IStatus { 31 | isLoading: boolean 32 | ACCESS_TOKEN: string 33 | } 34 | export interface ILayout { 35 | // 左侧导航栏 36 | menubar: IMenubar 37 | // 用户信息 38 | userInfo: IUserInfo 39 | // 标签栏 40 | tags: ITags 41 | setting: ISetting 42 | status:IStatus 43 | } 44 | export interface IMenubarList { 45 | parentId?: number | string 46 | id?: number | string 47 | name: string 48 | path: string 49 | redirect?: string | {name: string} 50 | meta: { 51 | icon: string 52 | title: string 53 | permission?: string[] 54 | activeMenu?: string // 路由设置了该属性,则会高亮相对应的侧边栏 55 | noCache?: boolean // 页面是否不缓存 56 | hidden?: boolean // 是否隐藏路由 57 | alwaysShow?: boolean // 当子路由只有一个的时候是否显示当前路由 58 | } 59 | component: (() => Promise) | string 60 | children?: Array 61 | } 62 | 63 | export interface ITagsList { 64 | name: string 65 | title: string 66 | path: string 67 | isActive: boolean 68 | } -------------------------------------------------------------------------------- /src/type/utils/tools.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ILocalStore { 3 | startTime: number 4 | expires: number 5 | [propName: string]: any 6 | } -------------------------------------------------------------------------------- /src/type/views/Components/TableSearchTest.ts: -------------------------------------------------------------------------------- 1 | export type ITag = '家' | '公司' | '学校' | '超市' 2 | export interface IRenderTableList { 3 | date: string 4 | name: string 5 | address: string 6 | tag: ITag 7 | amt: number 8 | } -------------------------------------------------------------------------------- /src/utils/animate.ts: -------------------------------------------------------------------------------- 1 | import { Ref, nextTick } from 'vue' 2 | interface IAnimate { 3 | timing(p: number): number 4 | draw(p: number): void 5 | duration: number 6 | } 7 | export function animate(param:IAnimate):void { 8 | const { timing, draw, duration } = param 9 | const start = performance.now() 10 | requestAnimationFrame(function animate(time) { 11 | // timeFraction 从 0 增加到 1 12 | let timeFraction = (time - start) / duration 13 | if (timeFraction > 1) timeFraction = 1 14 | 15 | // 计算当前动画状态,百分比,0-1 16 | const progress = timing(timeFraction) 17 | 18 | draw(progress) // 绘制 19 | 20 | if (timeFraction < 1) { 21 | requestAnimationFrame(animate) 22 | } 23 | }) 24 | } 25 | /** 26 | * 下拉动画,0=>auto,auto=>0 27 | * @param el dom节点 28 | * @param isShow 是否显示 29 | * @param duration 持续时间 30 | */ 31 | export async function slide(el:Ref, isShow:boolean, duration = 200):Promise { 32 | if(!el.value) return 33 | const { position, zIndex } = getComputedStyle(el.value) 34 | if(isShow) { 35 | el.value.style.position = 'absolute' 36 | el.value.style.zIndex = '-100000' 37 | el.value.style.height = 'auto' 38 | } 39 | await nextTick() 40 | const height = el.value.offsetHeight 41 | if(isShow) { 42 | el.value.style.position = position 43 | el.value.style.zIndex = zIndex 44 | el.value.style.height = '0px' 45 | } 46 | animate({ 47 | timing: timing.linear, 48 | draw: function(progress) { 49 | if(!el.value) return 50 | el.value.style.height = isShow 51 | ? progress === 1 52 | ? 'auto' 53 | : (`${progress * height}px`) 54 | : progress === 0 55 | ? 'auto' 56 | : (`${(1 - progress) * height}px`) 57 | }, 58 | duration: duration 59 | }) 60 | } 61 | 62 | const timing = { 63 | // 线性 64 | linear(timeFraction: number): number { 65 | return timeFraction 66 | }, 67 | // n 次幂 68 | quad(timeFraction: number, n = 2): number { 69 | return Math.pow(timeFraction, n) 70 | }, 71 | // 圆弧 72 | circle(timeFraction: number): number { 73 | return 1 - Math.sin(Math.acos(timeFraction)) 74 | } 75 | } -------------------------------------------------------------------------------- /src/utils/base64.ts: -------------------------------------------------------------------------------- 1 | const Base64 = { 2 | _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', 3 | _utf8_encode: function(string:string) { 4 | string = string.replace(/\r\n/g, '\n') 5 | let utftext = '' 6 | for (let n = 0; n < string.length; n++) { 7 | const c = string.charCodeAt(n) 8 | if (c < 128) { 9 | utftext += String.fromCharCode(c) 10 | } else if ((c > 127) && (c < 2048)) { 11 | utftext += String.fromCharCode((c >> 6) | 192) 12 | utftext += String.fromCharCode((c & 63) | 128) 13 | } else { 14 | utftext += String.fromCharCode((c >> 12) | 224) 15 | utftext += String.fromCharCode(((c >> 6) & 63) | 128) 16 | utftext += String.fromCharCode((c & 63) | 128) 17 | } 18 | } 19 | return utftext 20 | }, 21 | _utf8_decode: function(utftext:string) { 22 | let string = '' 23 | let i = 0 24 | let c = 0 25 | let c1 = 0 26 | let c2 = 0 27 | while (i < utftext.length) { 28 | c = utftext.charCodeAt(i) 29 | if (c < 128) { 30 | string += String.fromCharCode(c) 31 | i++ 32 | } else if ((c > 191) && (c < 224)) { 33 | c1 = utftext.charCodeAt(i + 1) 34 | string += String.fromCharCode(((c & 31) << 6) | (c1 & 63)) 35 | i += 2 36 | } else { 37 | c1 = utftext.charCodeAt(i + 1) 38 | c2 = utftext.charCodeAt(i + 2) 39 | string += String.fromCharCode(((c & 15) << 12) | ((c1 & 63) << 6) | (c2 & 63)) 40 | i += 3 41 | } 42 | } 43 | return string 44 | }, 45 | encode: function(input:string) { 46 | let output = '' 47 | let chr1, chr2, chr3, enc1, enc2, enc3, enc4 48 | let i = 0 49 | input = this._utf8_encode(input) 50 | while (i < input.length) { 51 | chr1 = input.charCodeAt(i++) 52 | chr2 = input.charCodeAt(i++) 53 | chr3 = input.charCodeAt(i++) 54 | enc1 = chr1 >> 2 55 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4) 56 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6) 57 | enc4 = chr3 & 63 58 | if (isNaN(chr2)) { 59 | enc3 = enc4 = 64 60 | } else if (isNaN(chr3)) { 61 | enc4 = 64 62 | } 63 | output = output + 64 | this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) + 65 | this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4) 66 | } 67 | return output 68 | }, 69 | decode: function(input:string) { 70 | let output = '' 71 | let chr1, chr2, chr3 72 | let enc1, enc2, enc3, enc4 73 | let i = 0 74 | // eslint-disable-next-line no-useless-escape 75 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '') 76 | while (i < input.length) { 77 | enc1 = this._keyStr.indexOf(input.charAt(i++)) 78 | enc2 = this._keyStr.indexOf(input.charAt(i++)) 79 | enc3 = this._keyStr.indexOf(input.charAt(i++)) 80 | enc4 = this._keyStr.indexOf(input.charAt(i++)) 81 | chr1 = (enc1 << 2) | (enc2 >> 4) 82 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2) 83 | chr3 = ((enc3 & 3) << 6) | enc4 84 | output = output + String.fromCharCode(chr1) 85 | if (enc3 !== 64) { 86 | output = output + String.fromCharCode(chr2) 87 | } 88 | if (enc4 !== 64) { 89 | output = output + String.fromCharCode(chr3) 90 | } 91 | } 92 | output = this._utf8_decode(output) 93 | return output 94 | } 95 | } 96 | 97 | const base64 = { 98 | encode: Base64.encode.bind(Base64), 99 | decode: Base64.decode.bind(Base64) 100 | } 101 | 102 | export default base64 103 | -------------------------------------------------------------------------------- /src/utils/changeThemeColor.ts: -------------------------------------------------------------------------------- 1 | import { ref, Ref } from 'vue' 2 | import { version } from 'element-plus' 3 | import { useLayoutStore } from '/@/store/modules/layout' 4 | 5 | const getTheme = (theme: string, prevTheme: Ref) => { 6 | const themeCluster = getThemeCluster(theme.substr(1)) 7 | const originalCluster = getThemeCluster(prevTheme.value.substr(1)) 8 | prevTheme.value = theme 9 | return { themeCluster, originalCluster } 10 | } 11 | 12 | const getThemeCluster: (theme: string) => string[] = (theme) => { 13 | const tintColor = (color: string, tint: number) => { 14 | let red = parseInt(color.slice(0, 2), 16) 15 | let green = parseInt(color.slice(2, 4), 16) 16 | let blue = parseInt(color.slice(4, 6), 16) 17 | 18 | if (tint === 0) return [red, green, blue].join(',') 19 | 20 | red += Math.round(tint * (255 - red)) 21 | green += Math.round(tint * (255 - green)) 22 | blue += Math.round(tint * (255 - blue)) 23 | return `#${red.toString(16)}${green.toString(16)}${blue.toString(16)}` 24 | } 25 | 26 | const shadeColor = (color: string, shade: number) => { 27 | let red = parseInt(color.slice(0, 2), 16) 28 | let green = parseInt(color.slice(2, 4), 16) 29 | let blue = parseInt(color.slice(4, 6), 16) 30 | 31 | red = Math.round((1 - shade) * red) 32 | green = Math.round((1 - shade) * green) 33 | blue = Math.round((1 - shade) * blue) 34 | 35 | return `#${red.toString(16)}${green.toString(16)}${blue.toString(16)}` 36 | } 37 | 38 | const clusters = [theme] 39 | for (let i = 0; i <= 9; i++) { 40 | clusters.push(tintColor(theme, Number((i / 10).toFixed(2)))) 41 | } 42 | clusters.push(shadeColor(theme, 0.1)) 43 | return clusters 44 | } 45 | 46 | const getStyleElem: (id: string) => HTMLElement = (id) => { 47 | let styleTag = document.getElementById(id) 48 | if (!styleTag) { 49 | styleTag = document.createElement('style') 50 | styleTag.setAttribute('id', id) 51 | document.head.appendChild(styleTag) 52 | } 53 | 54 | return styleTag 55 | } 56 | 57 | const getCSSString: (url: string, chalk: Ref) => Promise = (url, chalk) => { 58 | return new Promise(resolve => { 59 | const xhr = new XMLHttpRequest() 60 | xhr.onreadystatechange = () => { 61 | if (xhr.readyState === 4 && xhr.status === 200) { 62 | chalk.value = xhr.responseText.replace(/@font-face{[^}]+}/, '') 63 | resolve() 64 | } 65 | } 66 | xhr.open('GET', url, true) 67 | xhr.send() 68 | }) 69 | } 70 | 71 | 72 | // 切换主题色,并记录 73 | const prevTheme = ref('#409eff') 74 | const chalk = ref('') 75 | export default async function changeThemeColor(theme: string): Promise { 76 | const { changeThemeColor } = useLayoutStore() 77 | const { themeCluster, originalCluster } = getTheme(theme, prevTheme) 78 | if (!chalk.value) { 79 | // const url = `https://unpkg.com/element-plus@${version}/dist/index.css` 80 | const url = `/element-plus/index@${version}.css` 81 | await getCSSString(url, chalk) 82 | } 83 | originalCluster.forEach((color, index) => { 84 | chalk.value = chalk.value.replace(new RegExp(color, 'ig'), themeCluster[index]) 85 | }) 86 | const styleTag = getStyleElem('chalk-style') 87 | styleTag.innerText = chalk.value 88 | 89 | const systemSetting = document.querySelector('style.layout-side-setting') as HTMLElement 90 | if(systemSetting) { 91 | let systemSettingText = systemSetting.innerText 92 | originalCluster.forEach((color, index) => { 93 | systemSettingText = systemSettingText.replace(new RegExp(color, 'ig'), themeCluster[index]) 94 | }) 95 | systemSetting.innerText = systemSettingText 96 | } 97 | 98 | changeThemeColor(`#${themeCluster[0]}`) 99 | } 100 | 101 | export async function changeThemeDefaultColor():Promise { 102 | const { getSetting } = useLayoutStore() 103 | const defaultTheme = ref(getSetting.color.primary) 104 | // 判断是否修改过主题色 105 | defaultTheme.value.toLowerCase() !== '#409eff' && await changeThemeColor(defaultTheme.value) 106 | } -------------------------------------------------------------------------------- /src/utils/formExtend.ts: -------------------------------------------------------------------------------- 1 | import { Ref, unref } from 'vue' 2 | 3 | /** 4 | * 表单校验 5 | * @param ref 节点 6 | * @param isGetError 是否获取错误项 7 | */ 8 | export async function validate(ref: Ref|any, isGetError = false):Promise { 9 | const validateFn = unref(ref).validate 10 | return new Promise(resolve => validateFn((valid:boolean, object: any) => isGetError ? resolve({ valid, object }) : resolve(valid))) 11 | } 12 | 13 | /** 14 | * 对部分表单字段进行校验的方法 15 | * @param ref 节点 16 | * @param props 字段属性 17 | */ 18 | export async function validateField(ref: Ref|any, props: Array | string):Promise { 19 | const validateFieldFn = unref(ref).validateField 20 | return new Promise(resolve => validateFieldFn(props, (errorMessage: string) => resolve(errorMessage))) 21 | } 22 | 23 | /** 24 | * 重置表单 25 | * @param ref 节点 26 | */ 27 | export function resetFields(ref: Ref|any):void { 28 | const resetFieldsFn = unref(ref).resetFields 29 | resetFieldsFn() 30 | } 31 | 32 | /** 33 | * 移除表单项的校验结果 34 | * @param ref 节点 35 | * @param props 字段属性 36 | */ 37 | export function clearValidate(ref: Ref|any, props?: Array | string):void { 38 | const clearValidateFn = unref(ref).clearValidate 39 | props ? clearValidateFn(props) : clearValidateFn() 40 | } -------------------------------------------------------------------------------- /src/utils/permission.ts: -------------------------------------------------------------------------------- 1 | 2 | import router from '/@/router/index' 3 | export type IPermissionType = 'or' | 'and' 4 | export function checkPermission(permission:string|Array, type:IPermissionType = 'or'):boolean { 5 | const value:Array = typeof permission === 'string' ? [permission] : permission 6 | const currentRoute = router.currentRoute.value 7 | const roles:Array = (currentRoute.meta.permission || []) as Array 8 | const isShow = type === 'and' 9 | ? value.every(v => roles.includes(v)) 10 | : value.some(v => roles.includes(v)) 11 | 12 | return isShow 13 | } -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutStore } from '/@/store/modules/layout' 2 | import axios from 'axios' 3 | import { AxiosResponse } from 'axios' 4 | import { ElLoading, ElNotification } from 'element-plus' 5 | 6 | let loading:{close():void} 7 | // 创建 axios 实例 8 | const request = axios.create({ 9 | // API 请求的默认前缀 10 | baseURL: import.meta.env.VUE_APP_API_BASE_URL as string | undefined, 11 | timeout: 60000 // 请求超时时间 12 | }) 13 | 14 | // 异常拦截处理器 15 | const errorHandler = (error:{message:string}) => { 16 | loading.close() 17 | console.log(`err${error}`) 18 | ElNotification({ 19 | title: '请求失败', 20 | message: error.message, 21 | type: 'error' 22 | }) 23 | return Promise.reject(error) 24 | } 25 | 26 | // request interceptor 27 | request.interceptors.request.use(config => { 28 | const { getStatus } = useLayoutStore() 29 | loading = ElLoading.service({ 30 | lock: true, 31 | text: 'Loading', 32 | spinner: 'el-icon-loading', 33 | background: 'rgba(0, 0, 0, 0.4)' 34 | }) 35 | const token = getStatus.ACCESS_TOKEN 36 | // 如果 token 存在 37 | // 让每个请求携带自定义 token 请根据实际情况自行修改 38 | if (token) { 39 | config.headers['Access-Token'] = token 40 | } 41 | return config 42 | }, errorHandler) 43 | 44 | // response interceptor 45 | request.interceptors.response.use((response:AxiosResponse) => { 46 | const { data } = response 47 | const { getStatus, logout } = useLayoutStore() 48 | loading.close() 49 | if(data.Code !== 200) { 50 | let title = '请求失败' 51 | if(data.Code === 401) { 52 | if (getStatus.ACCESS_TOKEN) { 53 | logout() 54 | } 55 | title = '身份认证失败' 56 | } 57 | ElNotification({ 58 | title, 59 | message: data.Msg, 60 | type: 'error' 61 | }) 62 | return Promise.reject(new Error(data.Msg || 'Error')) 63 | } 64 | return response 65 | }, errorHandler) 66 | 67 | export default request -------------------------------------------------------------------------------- /src/utils/tools.ts: -------------------------------------------------------------------------------- 1 | import { ILocalStore } from '/@/type/utils/tools' 2 | import { IMenubarList } from '/@/type/store/layout' 3 | /** 4 | * 睡眠函数 5 | * @param time 6 | */ 7 | export async function sleep(time:number):Promise { 8 | await new Promise(resolve => { 9 | setTimeout(() => resolve, time) 10 | }) 11 | } 12 | /** 13 | * 金额格式化 14 | * @param num 金额 15 | * @param symbol 金额前面修饰符号,如$,¥ 16 | */ 17 | export function format(num:number|string, symbol = '¥'):string { 18 | if(Number.isNaN(Number(num))) return `${symbol}0.00` 19 | return symbol + (Number(num).toFixed(2)) 20 | .replace(/(\d)(?=(\d{3})+\.)/g, '$1,') 21 | } 22 | /** 23 | * 取消金额格式化 24 | * @param str 金额 25 | */ 26 | export function unformat(str:string):number|string { 27 | const s = str.substr(1).replace(/\,/g, '') 28 | return Number.isNaN(Number(s)) || Number(s) === 0 ? '' : Number(s) 29 | } 30 | /** 31 | * 表格合计行 32 | * @param str 金额 33 | */ 34 | export function tableSummaries(param: { columns: any; data: any }):Array { 35 | const { columns, data } = param 36 | const sums:Array = [] 37 | columns.forEach((column: { property: string | number }, index:number) => { 38 | if (index === 0) { 39 | sums[index] = '合计' 40 | return 41 | } 42 | const values = data.map((item: { [x: string]: any }) => Number(item[column.property])) 43 | if (!values.every((value: number) => isNaN(value))) { 44 | sums[index] = values.reduce((prev: number, curr: number) => { 45 | const value = Number(curr) 46 | if (!isNaN(value)) { 47 | return prev + curr 48 | } else { 49 | return prev 50 | } 51 | }, 0) 52 | const sum = sums[index] 53 | if(typeof sum === 'number') { 54 | sums[index] = format(sum.toFixed(2)) 55 | } 56 | } else { 57 | sums[index] = 'N/A' 58 | } 59 | }) 60 | 61 | return sums 62 | } 63 | 64 | export function isInput(el: HTMLElement): boolean { 65 | return el.nodeName.toLocaleLowerCase() === 'input' 66 | } 67 | export function isTextarea(el: HTMLElement): boolean { 68 | return el.nodeName.toLocaleLowerCase() === 'textarea' 69 | } 70 | 71 | /** 72 | * localStorage设置有效期 73 | * @param name localStorage设置名称 74 | * @param data 数据对象 75 | * @param pExpires 有效期(默认100年) 76 | */ 77 | export function setLocal(name:string, data:IObject, pExpires = 1000 * 60 * 60 * 24 * 365 * 100):void { 78 | const d = data as ILocalStore 79 | d.startTime = Date.now() 80 | d.expires = pExpires 81 | localStorage.setItem(name, JSON.stringify(data)) 82 | } 83 | /** 84 | * 判断localStorage有效期是否失效 85 | * @param name localStorage设置名称 86 | */ 87 | export async function useLocal(name: string):Promise { 88 | return new Promise((resolve, reject) => { 89 | const local = getLocal(name) 90 | if(local.startTime + local.expires < Date.now()) reject(`${name}已超过有效期`) 91 | resolve(local) 92 | }) 93 | } 94 | /** 95 | * 获取localStorage对象并转成对应的类型 96 | * @param name localStorage设置名称 97 | */ 98 | export function getLocal(name:string):T { 99 | const l = localStorage.getItem(name) 100 | const local = JSON.parse(l !== null ? l : '{}') as unknown as T 101 | return local 102 | } 103 | 104 | /** 105 | * 函数节流 106 | * @param time 间隔时间 107 | */ 108 | export function throttle(time = 500):()=>Promise { 109 | let timer:NodeJS.Timeout | null = null 110 | let firstTime = true 111 | return () => { 112 | return new Promise(resolve => { 113 | if(firstTime) { 114 | resolve() 115 | return firstTime = false 116 | } 117 | if(timer) return false 118 | timer = setTimeout(() => { 119 | if(timer) clearTimeout(timer) 120 | timer = null 121 | resolve() 122 | }, time) 123 | }) 124 | } 125 | } 126 | 127 | /** 128 | * list结构转tree 129 | * @param data list原始数据 130 | * @param pid 最外层pid 131 | */ 132 | export function listToTree(data:Array, pid: string | number = 1, isChildNull = false):Array { 133 | const d:Array = [] 134 | data.forEach(val => { 135 | if(val.parentId == pid) { 136 | const list = listToTree(data, val.id, isChildNull) 137 | const obj:IMenubarList = { ...val } 138 | if(!isChildNull || list.length !== 0) { 139 | obj.children = list 140 | } 141 | d.push(obj) 142 | } 143 | }) 144 | return d 145 | } 146 | /** 147 | * 字符串首字母大写 148 | * @param str 149 | * @returns 150 | */ 151 | export function firstUpperCase(str: string): string { 152 | return str.replace(/^\S/, s => s.toUpperCase()) 153 | } 154 | 155 | /** 156 | * 加载store状态 157 | * @param modules 158 | * @returns 159 | */ 160 | export function loadStorePage(modules: IObject): IObject { 161 | const page: IObject = {} 162 | Object.keys(modules).forEach(key => { 163 | const nameMatch = key.substr(2).replace('.ts', '') 164 | .split('/') 165 | .map(v => firstUpperCase(v)) 166 | .join('') 167 | page[nameMatch] = modules[key].default 168 | }) 169 | return page 170 | } 171 | 172 | /** 173 | * 两次编码url 174 | * @param url 175 | * @returns 176 | */ 177 | export function decode(url: string): string { 178 | return decodeURIComponent(decodeURIComponent(url)) 179 | } 180 | 181 | /** 182 | * 两次解码url 183 | * @param url 184 | * @returns 185 | */ 186 | export function encode(url: string): string { 187 | return encodeURIComponent(encodeURIComponent(url)) 188 | } 189 | -------------------------------------------------------------------------------- /src/views/Bud/BudApply.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/Bud/BudList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/Components/ListTest.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 61 | -------------------------------------------------------------------------------- /src/views/Components/OpenWindowTest.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /src/views/Dashboard/Workplace/Index.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | -------------------------------------------------------------------------------- /src/views/Dashboard/Workplace/_Components/Chart.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /src/views/Dashboard/Workplace/_Components/List.vue: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /src/views/ErrorPage/401.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 37 | 38 | -------------------------------------------------------------------------------- /src/views/Nav/SecondNav/Index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /src/views/Nav/SecondNav/ThirdNav/Index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /src/views/Nav/SecondText/Index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /src/views/Nav/SecondText/ThirdText/Index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/views/Permission/Directive.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 63 | 64 | -------------------------------------------------------------------------------- /src/views/Project/ProjectDetail.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | -------------------------------------------------------------------------------- /src/views/Project/ProjectImport.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/Project/ProjectList.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 61 | 62 | -------------------------------------------------------------------------------- /src/views/User/Login.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 96 | 97 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // important: 'body', 3 | content: ['./src/**/*.{vue,js,ts,jsx,tsx}'], 4 | theme: { 5 | extend: {} 6 | }, 7 | variants: {}, 8 | plugins: [] 9 | } -------------------------------------------------------------------------------- /test/components/CardList.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount, VueWrapper } from '@vue/test-utils' 2 | import { nextTick, ComponentPublicInstance, ref } from 'vue' 3 | import CardList from '/@/components/CardList/CardList.vue' 4 | import CardListItem from '/@/components/CardList/CardListItem.vue' 5 | import ElementPlus from 'element-plus' 6 | 7 | describe('CardList.vue', () => { 8 | const createCardList = function(props:string, opts:IObject | null, slot?:string): VueWrapper { 9 | return mount(Object.assign({ 10 | components: { 11 | CardList, 12 | CardListItem 13 | }, 14 | template: ` 15 | ${slot} 18 | ` 19 | }, opts), { 20 | global: { 21 | plugins: [ElementPlus] 22 | } 23 | }) 24 | } 25 | const listItem = ref([ 26 | { text: '标题标题标题标题标题标题标题标题标题标题', mark: '2020/12/21', url: 'http://baidu.com', target: '_blank' }, 27 | { text: '标题标题标题标题标题标题标题标题标题标题', mark: '2020/12/21' }, 28 | { text: '标题标题标题标题标题标题标题标题标题标题', mark: '2020/12/21' } 29 | ]) 30 | it('show title', async() => { 31 | const wrapper: VueWrapper = createCardList( 32 | ':list-item="listItem" :show-header="true" title="显示标题"', 33 | { 34 | setup() { 35 | return { listItem } 36 | } 37 | } 38 | ) 39 | await nextTick() 40 | expect(wrapper.find('.card-list .el-card__header>div>span').text()).toEqual('显示标题') 41 | }) 42 | it('hide liststyle', async() => { 43 | const wrapper: VueWrapper = createCardList( 44 | ':list-item="listItem" :show-liststyle="false"', 45 | { 46 | setup() { 47 | return { listItem } 48 | } 49 | } 50 | ) 51 | await nextTick() 52 | expect(wrapper.find('.card-list .card-list-body .card-list-item-circle').exists()).toBe(false) 53 | }) 54 | it('wrap', async() => { 55 | const wrapper: VueWrapper = createCardList( 56 | ':list-item="listItem" :is-nowrap="false"', 57 | { 58 | setup() { 59 | return { listItem } 60 | } 61 | } 62 | ) 63 | await nextTick() 64 | expect(wrapper.find('.card-list .card-list-body .card-list-text').classes()).toContain('wrap') 65 | }) 66 | it('hide liststyle', async() => { 67 | const wrapper: VueWrapper = createCardList( 68 | ':list-item="listItem" :show-liststyle="false"', 69 | { 70 | setup() { 71 | return { listItem } 72 | } 73 | } 74 | ) 75 | await nextTick() 76 | expect(wrapper.find('.card-list .card-list-body .card-list-item-circle').exists()).toBe(false) 77 | }) 78 | it('keyvalue', async() => { 79 | const wrapper: VueWrapper = createCardList( 80 | 'type="keyvalue"', 81 | { 82 | setup() { 83 | return { } 84 | } 85 | }, 86 | ` 87 | 97 | ` 98 | ) 99 | await nextTick() 100 | expect(wrapper.find('.card-list .card-list-item .text-right span').text()).toEqual(':') 101 | expect(wrapper.find('.card-list .card-list-item .font-semibold.truncate').text()).toEqual('2020001686') 102 | }) 103 | }) -------------------------------------------------------------------------------- /test/components/OpenWindow.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount, VueWrapper } from '@vue/test-utils' 2 | import { nextTick, ComponentPublicInstance, ref } from 'vue' 3 | import OpenWindow from '/@/components/OpenWindow/index.vue' 4 | import ElementPlus from 'element-plus' 5 | 6 | describe('OpenWindow.vue', () => { 7 | const wrapper: VueWrapper = mount({ 8 | components: { 9 | OpenWindow 10 | }, 11 | template: ` 12 |
13 | 14 | 打开窗体 15 | 16 | 21 |

22 | aaa 23 |

24 | 35 |
36 |
37 | `, 38 | setup() { 39 | const show = ref(false) 40 | 41 | return { 42 | show 43 | } 44 | } 45 | }, { 46 | global: { 47 | plugins: [ElementPlus] 48 | } 49 | }) 50 | it('hide', async() => { 51 | await nextTick() 52 | expect(wrapper.find('.open-select').exists()).toBe(false) 53 | }) 54 | it('click show', async() => { 55 | await nextTick() 56 | const btn = wrapper.find('.content .el-button') 57 | btn.trigger('click') 58 | await nextTick() 59 | expect(wrapper.find('.open-select').exists()).toBe(true) 60 | }) 61 | 62 | it('attr title', async() => { 63 | await nextTick() 64 | const btn = wrapper.find('.content .el-button') 65 | btn.trigger('click') 66 | await nextTick() 67 | expect(wrapper.find('.open-select>div>span').text()).toEqual('选择页') 68 | }) 69 | }) -------------------------------------------------------------------------------- /test/utils/format.spec.ts: -------------------------------------------------------------------------------- 1 | import { format, unformat } from '/@/utils/tools' 2 | 3 | describe('tools', () => { 4 | it('format', () => { 5 | expect(format(15.693)).toBe('¥15.69') 6 | expect(format('15.693')).toBe('¥15.69') 7 | expect(format('qwe')).toBe('¥0.00') 8 | expect(format('15.693', '$')).toBe('$15.69') 9 | expect(format('36915.693')).toBe('¥36,915.69') 10 | expect(format('1236915.693')).toBe('¥1,236,915.69') 11 | }) 12 | it('unformat', () => { 13 | expect(unformat('¥15.69')).toBe(15.69) 14 | expect(unformat('$15.69')).toBe(15.69) 15 | expect(unformat('¥36,915.69')).toBe(36915.69) 16 | expect(unformat('¥1,236,915.69')).toBe(1236915.69) 17 | }) 18 | }) 19 | 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "strict": true, 5 | // "noImplicitAny": true, 6 | "module": "esnext", 7 | "target": "es6", 8 | "moduleResolution": "Node", 9 | "baseUrl": ".", 10 | "paths": { 11 | "/@/*": ["src/*"], 12 | "/mock/*": ["mock/*"], 13 | "/server/*": ["server/*"] 14 | }, 15 | "lib": ["esnext", "dom"], 16 | "types": ["vite/client", "jest", "node"], 17 | "esModuleInterop": true, 18 | "plugins": [ 19 | { "name": "@vuedx/typescript-plugin-vue" } 20 | ] 21 | }, 22 | "include": [ 23 | "**/*.ts", "**/*.d.ts", "**/*.vue" 24 | ], 25 | "exclude": [ 26 | "node_modules" 27 | ] 28 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { UserConfigExport, ConfigEnv, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import path from 'path' 4 | import { viteMockServe } from 'vite-plugin-mock' 5 | import viteSvgIcons from 'vite-plugin-svg-icons' 6 | 7 | const setAlias = (alias: [string, string][]) => alias.map(v => {return { find: v[0], replacement: path.resolve(__dirname, v[1]) }}) 8 | const proxy = (list: [string, string][]) => { 9 | const obj:IObject = {} 10 | list.forEach((v) => { 11 | obj[v[0]] = { 12 | target: v[1], 13 | changeOrigin: true, 14 | rewrite: (path:any) => path.replace(new RegExp(`^${v[0]}`), ''), 15 | ...(/^https:\/\//.test(v[1]) ? { secure: false } : {}) 16 | } 17 | }) 18 | return obj 19 | } 20 | 21 | export default ({ command, mode }: ConfigEnv): UserConfigExport => { 22 | const root = process.cwd() 23 | const env = loadEnv(mode, root) as unknown as ImportMetaEnv 24 | const prodMock = true 25 | return { 26 | resolve: { 27 | alias: setAlias([ 28 | ['/@', 'src'], 29 | ['/mock', 'mock'], 30 | ['/server', 'server'] 31 | ]) 32 | }, 33 | server: { 34 | proxy: env.VITE_PROXY ? proxy(JSON.parse(env.VITE_PROXY)) : {}, 35 | port: env.VITE_PORT 36 | }, 37 | build: { 38 | // sourcemap: true, 39 | manifest: true, 40 | rollupOptions: { 41 | output: { 42 | manualChunks: { 43 | 'element-plus': ['element-plus'], 44 | echarts: ['echarts'], 45 | pinyin: ['pinyin'] 46 | } 47 | } 48 | }, 49 | chunkSizeWarningLimit: 600 50 | }, 51 | plugins: [ 52 | vue(), 53 | viteMockServe({ 54 | mockPath: 'mock', 55 | localEnabled: command === 'serve', 56 | prodEnabled: command !== 'serve' && prodMock, 57 | // 这样可以控制关闭mock的时候不让mock打包到最终代码内 58 | injectCode: ` 59 | import { setupProdMockServer } from '/mock/mockProdServer'; 60 | setupProdMockServer(); 61 | ` 62 | }), 63 | viteSvgIcons({ 64 | // 指定需要缓存的图标文件夹 65 | iconDirs: [path.resolve(process.cwd(), 'src/icons')], 66 | // 指定symbolId格式 67 | symbolId: 'icon-[dir]-[name]' 68 | }) 69 | ], 70 | css: { 71 | postcss: { 72 | plugins: [ 73 | require('autoprefixer'), 74 | require('tailwindcss/nesting'), 75 | require('tailwindcss'), 76 | require('postcss-simple-vars'), 77 | require('postcss-import') 78 | ] 79 | } 80 | } 81 | } 82 | } 83 | --------------------------------------------------------------------------------