├── .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 | "",
25 | " ",
26 | " $2",
27 | "
",
28 | " ",
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 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 | | [ ](https://godban.github.io/browsers-support-badges/)IE / Edge | [ ](https://godban.github.io/browsers-support-badges/)Firefox | [ ](https://godban.github.io/browsers-support-badges/)Chrome | [ ](https://godban.github.io/browsers-support-badges/)Safari |
86 | | --------- | --------- | --------- | --------- |
87 | | Edge | last 2 versions | last 2 versions | last 2 versions |
88 |
89 | ## 捐赠
90 |
91 | 如果你觉得这个项目帮助到了你,你可以帮作者买一杯果汁表示鼓励 :tropical_drink:
92 | 
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
17 | {{ v.mark }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
74 |
75 |
146 |
--------------------------------------------------------------------------------
/src/components/CardList/CardListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | *
6 |
7 | :
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{ val.subTitle }}
14 | {{ val.tag }}
15 |
16 |
17 |
18 | {{ val.subTitle }}
19 | {{ val.tag }}
20 |
21 |
{{ val.time }}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
{{ val.title }}
43 |
44 |
45 |
46 | {{ val.subTitle }}
47 |
48 |
{{ val.subTitle }}
49 |
50 |
51 |
{{ val.tag }}
52 |
{{ val.time }}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
95 |
96 |
--------------------------------------------------------------------------------
/src/components/OpenWindow/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
{{ title }}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
41 |
42 |
--------------------------------------------------------------------------------
/src/components/TableSearch/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 高级搜索
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
33 |
34 |
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 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/layout/components/content.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
45 |
46 |
--------------------------------------------------------------------------------
/src/layout/components/menubar.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/layout/components/menubarItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ menuList.meta.title }}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 | {{ menuList.meta.title }}
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/layout/components/navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 主页
12 | {{ v.title }}
13 |
14 |
15 |
16 |
17 |
26 |
27 |
28 |
29 |
30 |
31 | {{ userInfo.name }}
32 |
33 |
34 |
35 |
36 |
37 | 个人中心
38 |
39 |
40 | 项目地址
41 |
42 | 退出登录
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
113 |
114 |
--------------------------------------------------------------------------------
/src/layout/components/notice.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
61 |
62 |
--------------------------------------------------------------------------------
/src/layout/components/screenfull.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/layout/components/search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
17 |
18 |
19 |
146 |
147 |
--------------------------------------------------------------------------------
/src/layout/components/sideSetting.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
整体风格设置
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
导航模式
25 |
26 |
27 |
36 |
37 |
38 |
46 |
47 |
48 |
49 |
50 |
51 |
开启 Tags-View
52 |
53 |
54 |
58 |
62 |
63 |
64 |
65 |
66 |
67 |
99 |
--------------------------------------------------------------------------------
/src/layout/components/tags.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 | {{ v.title }}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 刷新
23 | 关闭其它
24 | 关闭所有
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/layout/components/theme.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
41 |
42 |
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 |
2 |
3 | budApply
4 |
5 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/src/views/Bud/BudList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | budList
4 |
5 |
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/src/views/Components/ListTest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 操作
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
61 |
--------------------------------------------------------------------------------
/src/views/Components/OpenWindowTest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
打开窗体
4 |
5 | aaa
6 |
7 | 默认按钮
8 | 默认按钮
9 | 默认按钮
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/views/Dashboard/Workplace/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 | 工作台
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
你好,{{ user.name }} ,祝你开心每一天!
23 |
饿了么-某某某某某某-某某某某某-某某某某某-某某某
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
38 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
59 |
60 |
61 |
62 | {{ '操作 ' + o }}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/src/views/Dashboard/Workplace/_Components/Chart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/views/Dashboard/Workplace/_Components/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 | 操作
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/views/ErrorPage/401.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Oops!
6 | gif来源
7 | airbnb
8 | 页面
9 | 你没有权限去该页面
10 | 如有不满请联系你领导
11 | 回首页
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
37 |
38 |
--------------------------------------------------------------------------------
/src/views/Nav/SecondNav/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
--------------------------------------------------------------------------------
/src/views/Nav/SecondNav/ThirdNav/Index.vue:
--------------------------------------------------------------------------------
1 |
2 | 三级导航
3 |
4 |
5 |
11 |
12 |
--------------------------------------------------------------------------------
/src/views/Nav/SecondText/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
--------------------------------------------------------------------------------
/src/views/Nav/SecondText/ThirdText/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | ThirdText
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/views/Permission/Directive.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
当前用户: {{ username }}
4 |
5 | 切换用户:
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 添加权限
15 | v-action='"add"'
16 |
17 |
18 | 添加权限
19 | v-if='checkPermission("add")'
20 |
21 |
22 | 修改权限
23 | v-action='"update"'
24 |
25 |
26 | 删除权限
27 | v-action='"remove"'
28 |
29 |
30 |
31 | 添加,编辑,删除权限(或者关系,满足一个就可以显示)
32 | v-action='["add", "update", "remove"]'
33 |
34 |
35 | 添加,编辑,删除权限(并且关系,全部满足才能显示)
36 | v-action:and='["add", "update", "remove"]'
37 |
38 |
39 |
40 |
41 |
63 |
64 |
--------------------------------------------------------------------------------
/src/views/Project/ProjectDetail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
项目详情
4 |
项目编码:{{ projName }}
5 |
6 |
7 |
8 |
9 |
23 |
24 |
--------------------------------------------------------------------------------
/src/views/Project/ProjectImport.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
高度超出,滚动条测试
4 |
aa
5 |
6 |
7 |
8 |
20 |
21 |
--------------------------------------------------------------------------------
/src/views/Project/ProjectList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 编辑
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
61 |
62 |
--------------------------------------------------------------------------------
/src/views/User/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
系统登陆
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 登录
14 |
15 |
16 |
17 |
18 |
账号: admin 密码: admin
19 |
账号: dev 密码: dev
20 |
账号: test 密码: test
21 |
22 |
第三方登录
23 |
24 |
25 |
26 |
27 |
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 |
88 |
89 |
90 | 申请单号
91 |
92 |
93 | 2020001686
94 |
95 |
96 |
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 |
25 |
26 | 默认按钮
27 |
28 |
29 | 默认按钮
30 |
31 |
32 | 默认按钮
33 |
34 |
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 |
--------------------------------------------------------------------------------