Compare commits
424 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4c2e01e320 | ||
|
ce9918b21a | ||
|
f314c04a59 | ||
|
8d3b688a5f | ||
|
138def830f | ||
|
546d29c39b | ||
|
cf24e82e53 | ||
|
5dcd7105c0 | ||
|
7cca118cfd | ||
|
97d1e288a9 | ||
|
8d0c30b001 | ||
|
64c1dbb5c4 | ||
|
93e5537332 | ||
|
c2f5c8ee91 | ||
|
73fa762052 | ||
|
8d4588b029 | ||
|
a935696af0 | ||
|
2b081e4eb4 | ||
|
9acc5f156e | ||
|
a4d38a4307 | ||
|
2dd0aa8575 | ||
|
67abb3b2d9 | ||
|
70b0889be9 | ||
|
050fc557a6 | ||
|
879dd6b318 | ||
|
d8d1ad2ab7 | ||
|
95a8604a2f | ||
|
d82f3ec874 | ||
|
c84b7bcca1 | ||
|
010dbcbfe0 | ||
|
935493cce8 | ||
|
95167dcfc0 | ||
|
fc345dc77b | ||
|
0a438082a6 | ||
|
6ad8d08ff4 | ||
|
d34b7c95d0 | ||
|
c643b410ef | ||
|
5aa09b05db | ||
|
1fa9d8c1f7 | ||
|
197b3be224 | ||
|
ea34a957f0 | ||
|
a3ec9e2ad2 | ||
|
75206acfe3 | ||
|
272642ec97 | ||
|
63e3960465 | ||
|
2b21a29f09 | ||
|
e05e94f2b0 | ||
|
170269e171 | ||
|
b02c8c6f60 | ||
|
f7d4ea147e | ||
|
f331a2e655 | ||
|
c9859aa597 | ||
|
2431b21d26 | ||
|
45468c5d91 | ||
|
45d01eabea | ||
|
c43ee2bb30 | ||
|
ae8315586c | ||
|
cd338a5f4a | ||
|
956792ff40 | ||
|
4d5e6a03b2 | ||
|
aeaaa87a22 | ||
|
f5c7788692 | ||
|
1cef1684ef | ||
|
5e52ef5ad0 | ||
|
c35f0e7735 | ||
|
ab306c4e2c | ||
|
2685efa759 | ||
|
b85354b8d3 | ||
|
17ca9b58e4 | ||
|
5702a2f9e0 | ||
|
8d2f240381 | ||
|
52a5cb8dc6 | ||
|
a3f6c45866 | ||
|
50d862dec6 | ||
|
d3fc20a53a | ||
|
2d840ea675 | ||
|
1f531938a3 | ||
|
fb6b42d753 | ||
|
9f8e1beafe | ||
|
d631d46ab2 | ||
|
631efeed5d | ||
|
08ce7fd2f4 | ||
|
cfbefb10ab | ||
|
de349901df | ||
|
0c23ef9f8c | ||
|
df3f209be9 | ||
|
ebb8e808d4 | ||
|
503e2d8487 | ||
|
392927884b | ||
|
51823f8c6a | ||
|
ff5dcd66f1 | ||
|
0142c8a94d | ||
|
23f0c1b133 | ||
|
20cc0885d7 | ||
|
5338d12fd3 | ||
|
ea1fe99dfa | ||
|
fd075bd63d | ||
|
6e43607f7e | ||
|
6fd7dfe7aa | ||
|
8e423954ce | ||
|
6a39d67941 | ||
|
97030fa04b | ||
|
d3b09be774 | ||
|
05f8cc6404 | ||
|
1bd695db17 | ||
|
0fe9d6072b | ||
|
86ec9766dd | ||
|
0ac0719e21 | ||
|
f46f9f5091 | ||
|
fa0eadd986 | ||
|
0cbb1ea998 | ||
|
eef5807040 | ||
|
d0cfcead4a | ||
|
5a94ec4c3b | ||
|
0dc9d03237 | ||
|
f892c5d730 | ||
|
cc6635a6d9 | ||
|
9a02f69692 | ||
|
b0e93f6184 | ||
|
dc48bf4880 | ||
|
f6668e471c | ||
|
f2146ae716 | ||
|
835d4c4636 | ||
|
8cb796a290 | ||
|
1ce2353c1b | ||
|
77cfb43df6 | ||
|
a61ec9c71b | ||
|
fd6b76f990 | ||
|
f197777060 | ||
|
9363308494 | ||
|
24fd0a6253 | ||
|
cc65b1d6c7 | ||
|
8a224cc946 | ||
|
599d03a4cf | ||
|
609c1b4b4b | ||
|
663a2c7173 | ||
|
b01c8b29ef | ||
|
4bf0e7a998 | ||
|
31b2a5ba82 | ||
|
c1290cdf28 | ||
|
df2821ebe9 | ||
|
ea098a6eaa | ||
|
19b145597e | ||
|
5727b2245b | ||
|
4d518b06f4 | ||
|
6f87caf262 | ||
|
1a2924861d | ||
|
74543de942 | ||
|
51e7cd95a6 | ||
|
0f5f16cf0f | ||
|
9b359203fc | ||
|
23ab6b7ab5 | ||
|
0b517855b6 | ||
|
b5210eb452 | ||
|
921d4411d0 | ||
|
592fa82650 | ||
|
f121c01227 | ||
|
61997fd102 | ||
|
30b4754493 | ||
|
ff4ae8ba5b | ||
|
b18bf9d4b5 | ||
|
aada21b6c1 | ||
|
8dec01f011 | ||
|
23f5dc9c14 | ||
|
17b9ebb55c | ||
|
464218e72c | ||
|
7a29374c2e | ||
|
f9b1f080b5 | ||
|
14fca80a90 | ||
|
0fe2d1fbcf | ||
|
a415ade4c2 | ||
|
d29e5ba062 | ||
|
ea253ab1a9 | ||
|
b892bc0936 | ||
|
9473a13c53 | ||
|
5f611b9a13 | ||
|
3155c086ca | ||
|
b8033dc892 | ||
|
526c2cf2c6 | ||
|
c168ef7ab8 | ||
|
4f01de4e92 | ||
|
f2eba50938 | ||
|
4a0452d38e | ||
|
226cdd3d1f | ||
|
f3645cfdca | ||
|
787143e1c8 | ||
|
45bcc0545a | ||
|
84233e5ba6 | ||
|
c35e70ea77 | ||
|
727a3a4b60 | ||
|
cefad64730 | ||
|
933b7f0cd9 | ||
|
80afc6e13f | ||
|
d47428ccc7 | ||
|
28ed4c0977 | ||
|
7f02e18d37 | ||
|
c8571a5f55 | ||
|
92d0778992 | ||
|
43e359fffa | ||
|
4f4b4892b9 | ||
|
7748dc0fa4 | ||
|
394f6e16dc | ||
|
869986bc50 | ||
|
ade5d806c7 | ||
|
047909f661 | ||
|
03b2611786 | ||
|
738bd4a49f | ||
|
19f7b6049e | ||
|
e38525638d | ||
|
b3d935cdb0 | ||
|
396b8fac53 | ||
|
6af1ba94ff | ||
|
82bd4ee14a | ||
|
f83a72f097 | ||
|
ac3f53f86b | ||
|
2108c04388 | ||
|
6680e72645 | ||
|
6f2fbf4a90 | ||
|
13a53cc994 | ||
|
ffb6cb3a11 | ||
|
3a18c38218 | ||
|
bc0c8f4cc4 | ||
|
8523ec6695 | ||
|
0bcb7886d5 | ||
|
9b1142c106 | ||
|
cd3fcd3b1f | ||
|
9168c56359 | ||
|
3a9d344698 | ||
|
934eed8e7b | ||
|
256e48b0db | ||
|
bf8b5b99bb | ||
|
85bbfb5adc | ||
|
356ba4723b | ||
|
377f9a5141 | ||
|
0f140a61b2 | ||
|
7cd21e6a87 | ||
|
8201db2f66 | ||
|
e9440ff996 | ||
|
0a93a9f909 | ||
|
372a63439b | ||
|
7e0e868c48 | ||
|
7bda678d7c | ||
|
6893c70df0 | ||
|
f97824ddf8 | ||
|
ccdeaaeaba | ||
|
d77d7f7651 | ||
|
7e9eaab9da | ||
|
3bc8ca91f6 | ||
|
1c78445e44 | ||
|
4e6ed361c7 | ||
|
6f8755c47c | ||
|
82b5bd81ef | ||
|
5b531e75db | ||
|
20f9ba8595 | ||
|
246fca2692 | ||
|
7837bac010 | ||
|
7e22f8915f | ||
|
1186650df8 | ||
|
952b327261 | ||
|
b7987218c0 | ||
|
f528bde687 | ||
|
56f933d0ca | ||
|
6a6c39395c | ||
|
2fa3fc05fb | ||
|
c4526f72b1 | ||
|
bf17c67fda | ||
|
893ad57f7c | ||
|
5a2836c825 | ||
|
a01e68d381 | ||
|
f01b72f3c7 | ||
|
c5471b5fca | ||
|
d716248736 | ||
|
c0a950b383 | ||
|
c6225b3cf7 | ||
|
d8ab55fe2a | ||
|
58fc646116 | ||
|
ecfd39bcc2 | ||
|
2498f239c0 | ||
|
9963b590fd | ||
|
01249a4f5a | ||
|
2ad674b0b0 | ||
|
86b6562ab8 | ||
|
34276b5855 | ||
|
bf9d6eb7b6 | ||
|
975bfd24dc | ||
|
13165b7600 | ||
|
e81668e293 | ||
|
a00e57a1e8 | ||
|
b92a1a24fc | ||
|
160f389c60 | ||
|
8c7f4184a1 | ||
|
0f1afe1a94 | ||
|
02dca40083 | ||
|
0a4d896965 | ||
|
b2597f1ffd | ||
|
fff3eb9498 | ||
|
8fd01971fd | ||
|
47e83913e1 | ||
|
7f067b111b | ||
|
f5339314b8 | ||
|
2e1e72099a | ||
|
7317e96e7b | ||
|
9510639b49 | ||
|
56f294d51c | ||
|
d19b6da1d4 | ||
|
4d3d4e1057 | ||
|
9fb0b9b51a | ||
|
03928dfce5 | ||
|
aa98712984 | ||
|
3f6a140e11 | ||
|
1a1c8886d2 | ||
|
b2eaa29b1e | ||
|
f50ccba6d4 | ||
|
3810f269bf | ||
|
16f2e1ad2f | ||
|
536802ed6a | ||
|
7783d577b0 | ||
|
8d9e16ba79 | ||
|
91350be394 | ||
|
7307f4caa2 | ||
|
a91b8848a9 | ||
|
9ddcf09dfd | ||
|
029e695be2 | ||
|
80462fd2fc | ||
|
8006a2af55 | ||
|
c5e3684495 | ||
|
5d1eeec01f | ||
|
fc9e188572 | ||
|
ca9674c4cd | ||
|
a36c560810 | ||
|
d20023b355 | ||
|
536803c3c1 | ||
|
bcb97a6c49 | ||
|
561261cf58 | ||
|
8e83db5228 | ||
|
574f47cb00 | ||
|
7bfacf333a | ||
|
6d6ee82379 | ||
|
41c7ac1c9a | ||
|
f1918b4bb8 | ||
|
2c760e077b | ||
|
a02d150533 | ||
|
be7425d54d | ||
|
65530215e0 | ||
|
2e28d0db26 | ||
|
9ebed9753d | ||
|
413305322d | ||
|
f3e71e0795 | ||
|
d2952e0d16 | ||
|
cc87ab3ccc | ||
|
2ea0a80160 | ||
|
2c7d5b2b76 | ||
|
e43edd4739 | ||
|
33f0131c48 | ||
|
46e9ea2f28 | ||
|
6593db0d21 | ||
|
1ef54abc1e | ||
|
07f321ecbb | ||
|
6b62edc323 | ||
|
28f1c0718b | ||
|
a7e078930c | ||
|
15df444f20 | ||
|
36f8c8acf2 | ||
|
a3dce0e0f2 | ||
|
a267429e5b | ||
|
41973c2013 | ||
|
5113d60565 | ||
|
086e9a8c60 | ||
|
8c89dbd743 | ||
|
a486b8c18c | ||
|
56bb2c2492 | ||
|
adff68b7e8 | ||
|
59a412ace3 | ||
|
28edb3775a | ||
|
db3de00fcf | ||
|
86f11d42b8 | ||
|
84885b7bc8 | ||
|
88033a823d | ||
|
2cd7b161e8 | ||
|
ab6aa20695 | ||
|
82c3409b7d | ||
|
30ec43d490 | ||
|
31288109c4 | ||
|
f966b45ace | ||
|
8eb8876f04 | ||
|
8488bfdff2 | ||
|
4a27131c6d | ||
|
5f0e91c5ec | ||
|
f12f33014e | ||
|
e32e3226d6 | ||
|
ee91984cf5 | ||
|
9f486757b8 | ||
|
ac1b621667 | ||
|
ad9ff59a40 | ||
|
45b0bea731 | ||
|
a47cea68a9 | ||
|
df4ff20df8 | ||
|
307810d9c0 | ||
|
2d561982f7 | ||
|
1aacb0cd1e | ||
|
1ae44be0bc | ||
|
6f381ffd54 | ||
|
4571a12742 | ||
|
58cfdebcca | ||
|
310ab846fa | ||
|
8a0b8c5b3a | ||
|
d5afb53c25 | ||
|
bee0691d08 | ||
|
3531250c51 | ||
|
444e04d19b | ||
|
89a0bf43ea | ||
|
a4ab651282 | ||
|
9710c359af | ||
|
135cf8fc1e | ||
|
f9677d9a4a | ||
|
025a69883f | ||
|
442ae06c47 | ||
|
c3ad3c0ce1 | ||
|
e2ebc1da85 | ||
|
f800a8d202 | ||
|
3fd5dbc412 | ||
|
665bdbec7a | ||
|
57d1df3f6e | ||
|
83678f7370 |
@ -1,24 +1,24 @@
|
|||||||
# 配置项文档:https://editorconfig.org/
|
# 配置项文档:https://editorconfig.org(修改配置后重启编辑器)
|
||||||
|
|
||||||
# 告知 EditorConfig 插件,当前即是根文件
|
## 告知 EditorConfig 插件,当前即是根文件
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
# 适用全部文件
|
## 适用全部文件
|
||||||
[*]
|
[*]
|
||||||
## 设置字符集
|
### 设置字符集
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
## 缩进风格 space | tab,建议 space
|
### 缩进风格 space | tab,建议 space
|
||||||
indent_style = space
|
indent_style = space
|
||||||
## 缩进的空格数(修改这里的话需要将 prettier.config.js 和 .vscode -> settings.json 也同步修改)
|
### 缩进的空格数
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
## 换行符类型 lf | cr | crlf,一般都是设置为 lf
|
### 换行符类型 lf | cr | crlf,一般都是设置为 lf
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
## 是否在文件末尾插入空白行
|
### 是否在文件末尾插入空白行
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
## 是否删除一行中的前后空格
|
### 是否删除一行中的前后空格
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
# 适用 .md 文件
|
## 适用 .md 文件
|
||||||
[*.md]
|
[*.md]
|
||||||
insert_final_newline = false
|
insert_final_newline = false
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
7
.env
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# 所有环境的环境变量(命名必须以 VITE_ 开头)
|
||||||
|
|
||||||
|
## 项目标题
|
||||||
|
VITE_APP_TITLE = V3 Admin Vite
|
||||||
|
|
||||||
|
## 路由模式 hash 或 html5
|
||||||
|
VITE_ROUTER_HISTORY = hash
|
@ -1,10 +1,7 @@
|
|||||||
# 自定义的环境变量(命名必须以 VITE_ 开头)
|
# 开发环境的环境变量(命名必须以 VITE_ 开头)
|
||||||
|
|
||||||
## 后端接口公共路径(如果解决跨域问题采用反向代理就只需写公共路径)
|
## 后端接口地址(如果解决跨域问题采用反向代理就只需写相对路径)
|
||||||
VITE_BASE_API = '/api/v1'
|
VITE_BASE_URL = /api/v1
|
||||||
|
|
||||||
## 路由模式 hash 或 html5
|
## 开发环境域名和静态资源公共路径(一般 / 或 ./ 都可以)
|
||||||
VITE_ROUTER_HISTORY = 'hash'
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
## 开发环境地址前缀(一般 '/','./' 都可以)
|
|
||||||
VITE_PUBLIC_PATH = '/'
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
# 自定义的环境变量(命名必须以 VITE_ 开头)
|
# 生产环境的环境变量(命名必须以 VITE_ 开头)
|
||||||
|
|
||||||
## 后端接口公共路径(如果解决跨域问题采用 CORS 就需要写全路径)
|
## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径)
|
||||||
VITE_BASE_API = 'https://mock.mengxuegu.com/mock/63218b5fb4c53348ed2bc212/api/v1'
|
VITE_BASE_URL = https://apifoxmock.com/m1/2930465-2145633-default/api/v1
|
||||||
|
|
||||||
## 路由模式 hash 或 html5
|
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/v3-admin-vite/ 域名下就需要填写 /v3-admin-vite/)
|
||||||
VITE_ROUTER_HISTORY = 'hash'
|
VITE_PUBLIC_PATH = /v3-admin-vite/
|
||||||
|
|
||||||
## 打包路径(就是网站前缀,例如部署到 https://un-pany.github.io/v3-admin-vite/ 域名下,就需要填写 /v3-admin-vite/)
|
|
||||||
VITE_PUBLIC_PATH = '/v3-admin-vite/'
|
|
||||||
|
13
.env.staging
@ -1,10 +1,7 @@
|
|||||||
# 自定义的环境变量(命名必须以 VITE_ 开头)
|
# 预发布环境的环境变量(命名必须以 VITE_ 开头)
|
||||||
|
|
||||||
## 后端接口公共路径(如果解决跨域问题采用 CORS 就需要写全路径)
|
## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径)
|
||||||
VITE_BASE_API = 'https://mock.mengxuegu.com/mock/63218b5fb4c53348ed2bc212/api/v1'
|
VITE_BASE_URL = https://apifoxmock.com/m1/2930465-2145633-default/api/v1
|
||||||
|
|
||||||
## 路由模式 hash 或 html5
|
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/ 域名下就需要填写 /)
|
||||||
VITE_ROUTER_HISTORY = 'hash'
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
## 打包路径(就是网站前缀,例如部署到 https://un-pany.github.io/v3-admin-vite/ 域名下,就需要填写 /v3-admin-vite/)
|
|
||||||
VITE_PUBLIC_PATH = '/v3-admin-vite/'
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
# Eslint 会忽略的文件
|
|
||||||
|
|
||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
.npmrc
|
|
74
.eslintrc.js
@ -1,74 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
node: true,
|
|
||||||
es6: true
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
"plugin:vue/vue3-essential",
|
|
||||||
"eslint:recommended",
|
|
||||||
"@vue/typescript/recommended",
|
|
||||||
"@vue/prettier",
|
|
||||||
"@vue/eslint-config-typescript"
|
|
||||||
],
|
|
||||||
parser: "vue-eslint-parser",
|
|
||||||
parserOptions: {
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
sourceType: "module",
|
|
||||||
jsxPragma: "React",
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
tsx: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
// TS
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"no-debugger": "off",
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
||||||
"@typescript-eslint/ban-types": "off",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
argsIgnorePattern: "^_",
|
|
||||||
varsIgnorePattern: "^_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
argsIgnorePattern: "^_",
|
|
||||||
varsIgnorePattern: "^_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Vue
|
|
||||||
"vue/no-v-html": "off",
|
|
||||||
"vue/require-default-prop": "off",
|
|
||||||
"vue/require-explicit-emits": "off",
|
|
||||||
"vue/multi-word-component-names": "off",
|
|
||||||
"vue/html-self-closing": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
html: {
|
|
||||||
void: "always",
|
|
||||||
normal: "always",
|
|
||||||
component: "always"
|
|
||||||
},
|
|
||||||
svg: "always",
|
|
||||||
math: "always"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// Prettier
|
|
||||||
"prettier/prettier": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
endOfLine: "auto"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
custom: https://github.com/un-pany/v3-admin-vite/issues/69
|
8
.github/workflows/deploy.yml
vendored
@ -14,18 +14,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node.js 18.16.1
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@master
|
uses: actions/setup-node@master
|
||||||
with:
|
with:
|
||||||
node-version: "18.16.1"
|
node-version: 22.12.0
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v2
|
uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: "8.6.3"
|
version: 10.2.0
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm install && pnpm build:prod
|
run: pnpm install && pnpm build
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
uses: JamesIves/github-pages-deploy-action@releases/v3
|
uses: JamesIves/github-pages-deploy-action@releases/v3
|
||||||
|
27
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
registry-url: https://registry.npmjs.org/
|
||||||
|
node-version: lts/*
|
||||||
|
|
||||||
|
- run: npx changelogithub
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.V3_ADMIN_VITE }}
|
31
.gitignore
vendored
@ -1,35 +1,18 @@
|
|||||||
# Git 会忽略的文件
|
# Common
|
||||||
|
|
||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
node_modules
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
vite.config.*.timestamp*
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# Local env files
|
# Local env files
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Use the pnpm
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/*.code-snippets
|
|
||||||
.idea
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
# Use the PNPM
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
# 全局 ts 类型检查(此操作会增加 git commit 时长)
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
npx vue-tsc
|
||||||
|
# 执行 lint-staged 中配置的任务
|
||||||
npx lint-staged
|
npx lint-staged
|
||||||
|
7
.npmrc
@ -1,2 +1,5 @@
|
|||||||
# 通过该配置兜底解决组件没有类型提示的问题
|
# China mirror of npm
|
||||||
shamefully-hoist = true
|
registry = https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# 安装依赖时锁定版本号
|
||||||
|
save-exact = true
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
# Prettier 会忽略的文件
|
|
||||||
|
|
||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
.npmrc
|
|
6
.vscode/extensions.json
vendored
@ -1,12 +1,10 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
|
"vue.volar",
|
||||||
"editorconfig.editorconfig",
|
"editorconfig.editorconfig",
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"vue.vscode-typescript-vue-plugin",
|
|
||||||
"vue.volar",
|
|
||||||
"antfu.unocss",
|
"antfu.unocss",
|
||||||
"zixuanchen.vitest-explorer",
|
"vitest.explorer",
|
||||||
"wiensss.region-highlighter"
|
"wiensss.region-highlighter"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
9
.vscode/hook.code-snippets
vendored
@ -1,16 +1,15 @@
|
|||||||
{
|
{
|
||||||
"Vue3 Hook 代码结构一键生成": {
|
"Vue3 Composable 代码结构一键生成": {
|
||||||
"prefix": "Vue3 Hook",
|
"prefix": "Vue3 Composable",
|
||||||
"body": [
|
"body": [
|
||||||
"import { ref } from \"vue\"\n",
|
|
||||||
"const refName1 = ref<string>(\"这是一个响应式变量\")\n",
|
"const refName1 = ref<string>(\"这是一个响应式变量\")\n",
|
||||||
"export function useHookName() {",
|
"export function useName() {",
|
||||||
"\tconst refName2 = ref<string>(\"这是一个响应式变量\")\n",
|
"\tconst refName2 = ref<string>(\"这是一个响应式变量\")\n",
|
||||||
"\tconst fnName = () => {}\n",
|
"\tconst fnName = () => {}\n",
|
||||||
"\treturn { refName1, refName2, fnName }",
|
"\treturn { refName1, refName2, fnName }",
|
||||||
"}",
|
"}",
|
||||||
"$1"
|
"$1"
|
||||||
],
|
],
|
||||||
"description": "Vue3 Hook"
|
"description": "Vue3 Composable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
75
.vscode/settings.json
vendored
@ -1,30 +1,53 @@
|
|||||||
{
|
{
|
||||||
"editor.tabSize": 2,
|
// Use workspace TypeScript version
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
|
||||||
|
// Disable the default formatter, use eslint instead
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
|
||||||
|
// Auto fix
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
},
|
},
|
||||||
"[vue]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||||
},
|
"eslint.rules.customizations": [
|
||||||
"[javascript]": {
|
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||||
},
|
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||||
"[typescript]": {
|
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||||
},
|
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||||
"[json]": {
|
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||||
},
|
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||||
"[jsonc]": {
|
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
],
|
||||||
},
|
|
||||||
"[html]": {
|
// Enable eslint for all supported languages
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"eslint.validate": [
|
||||||
},
|
"javascript",
|
||||||
"[css]": {
|
"javascriptreact",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"typescript",
|
||||||
},
|
"typescriptreact",
|
||||||
"[scss]": {
|
"vue",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"html",
|
||||||
}
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"yaml",
|
||||||
|
"toml",
|
||||||
|
"xml",
|
||||||
|
"gql",
|
||||||
|
"graphql",
|
||||||
|
"astro",
|
||||||
|
"svelte",
|
||||||
|
"css",
|
||||||
|
"less",
|
||||||
|
"scss",
|
||||||
|
"pcss",
|
||||||
|
"postcss"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
6
.vscode/vue.code-snippets
vendored
@ -4,9 +4,11 @@
|
|||||||
"body": [
|
"body": [
|
||||||
"<script lang=\"ts\" setup></script>\n",
|
"<script lang=\"ts\" setup></script>\n",
|
||||||
"<template>",
|
"<template>",
|
||||||
"\t<div class=\"app-container\">...</div>",
|
"\t<div class=\"app-container\">",
|
||||||
|
"\t\t...",
|
||||||
|
"\t</div>",
|
||||||
"</template>\n",
|
"</template>\n",
|
||||||
"<style scoped></style>",
|
"<style lang=\"scss\" scoped></style>",
|
||||||
"$1"
|
"$1"
|
||||||
],
|
],
|
||||||
"description": "Vue3 SFC"
|
"description": "Vue3 SFC"
|
||||||
|
275
README.md
@ -1,158 +1,227 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img alt="V3 Admin Vite Logo" width="120" height="120" src="./src/assets/layout/logo.png">
|
<img alt="logo" width="120" height="120" src="./src/common/assets/images/layouts/logo.png">
|
||||||
<h1>V3 Admin Vite</h1>
|
<h1>V3 Admin Vite</h1>
|
||||||
<span>English | <a href="./README.zh-CN.md">中文</a></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## ⚡ Introduction
|
[](https://github.com/un-pany/v3-admin-vite/releases)
|
||||||
|
[](https://github.com/un-pany/v3-admin-vite/stargazers)
|
||||||
|
[](https://gitee.com/un-pany/v3-admin-vite/stargazers)
|
||||||
|
|
||||||
v3-admin-vite is a free and open source middle and background management system basic solution, based on mainstream framework such as Vue3, TypeScript, Element Plus, Pinia and Vite.
|
<b>English | <a href="./README.zh-CN.md">中文</a></b>
|
||||||
|
|
||||||
- Vue-Cli 5.x: [v3-admin](https://github.com/un-pany/v3-admin)
|
## Introduction
|
||||||
- Electron desktop: [v3-electron-vite](https://github.com/un-pany/v3-electron-vite)
|
|
||||||
|
|
||||||
## Feature
|
V3 Admin Vite is a well-crafted backend management system template, built with popular technologies such as Vue3, Vite, TypeScript, and Element Plus
|
||||||
|
|
||||||
- **Vue3**:The latest Vue3 composition API using Vue3 + script setup
|
## Notifications
|
||||||
- **Element Plus**:Vue3 version of Element UI
|
|
||||||
- **Pinia**: An alternative to Vuex in Vue3
|
|
||||||
- **Vite**:Really fast
|
|
||||||
- **Vue Router**:router
|
|
||||||
- **TypeScript**:JavaScript With Syntax For Types
|
|
||||||
- **PNPM**:Faster, disk space saving package management tool
|
|
||||||
- **Scss**:Consistent with Element Plus
|
|
||||||
- **CSS variable**:Mainly controls the layout and color of the item
|
|
||||||
- **ESlint**:Code verification
|
|
||||||
- **Prettier**: Code formatting
|
|
||||||
- **Axios**: Promise based HTTP client (encapsulated)
|
|
||||||
- **UnoCSS**: Real-time atomized CSS engine with high performance and flexibility
|
|
||||||
- **Annotation**:Each configuration item is written with as detailed comments as possible
|
|
||||||
- **Mobile Compatible**: The layout is compatible with mobile page resolution
|
|
||||||
|
|
||||||
## Functions
|
> [!NOTE]
|
||||||
|
> Powered by love! All source code is free and open-source. If you find it helpful, feel free to give a star to support!
|
||||||
|
|
||||||
- **User management**: log in, log out of the demo
|
> [!IMPORTANT]
|
||||||
- **Authority management**: Built-in page permissions (dynamic routing), instruction permissions, permission functions
|
> Welcome to experience the brand-new version 5.0, currently in the beta stage. It will be a masterpiece!
|
||||||
- **Multiple Environments**: Development, Staging, Production
|
|
||||||
- **Multiple themes**: Normal, Dark, Dark Blue, theme modes
|
|
||||||
- **Error page**: 403, 404
|
|
||||||
- **Dashboard**: Display different Dashboard pages according to different users
|
|
||||||
- **Other functions**:SVG, Dynamic Sidebar, Dynamic Breadcrumb Navigation, Tabbed Navigation, Screenfull, Adaptive Shrink Sidebar
|
|
||||||
|
|
||||||
## 📚 Document
|
> [!WARNING]
|
||||||
|
> Version 4.x will no longer be maintained unless there are critical bugs! [Click to switch to the 4.x branch](https://github.com/un-pany/v3-admin-vite/tree/4.x)
|
||||||
|
|
||||||
[Chinese documentation](https://juejin.cn/post/7089377403717287972)
|
> [!TIP]
|
||||||
|
> Paid services are officially launched! If you don’t want to do it yourself but want to remove TS or other modules, try the lazy package! [Click to check it out](https://github.com/un-pany/v3-admin-vite/issues/225)
|
||||||
|
|
||||||
[Chinese getting started tutorial](https://juejin.cn/column/7207659644487139387)
|
> [!TIP]
|
||||||
|
> If you have mobile web app needs, try the new open-source template. [MobVue](https://github.com/un-pany/mobvue)
|
||||||
|
|
||||||
## Gitee repository
|
## Usage
|
||||||
|
|
||||||
[Gitee](https://gitee.com/un-pany/v3-admin-vite)
|
<details>
|
||||||
|
<summary>Recommended Environment</summary>
|
||||||
|
|
||||||
## Online preview
|
<br>
|
||||||
|
|
||||||
| Location | account | Link |
|
- Latest version of `Visual Studio Code`
|
||||||
| ------------ | ------------------- | ----------------------------------------------- |
|
- Install the recommended plugins in the `.vscode/extensions.json` file
|
||||||
| github-pages | `admin` or `editor` | [Link](https://un-pany.github.io/v3-admin-vite) |
|
- `node` 20.x or 22+
|
||||||
|
- `pnpm` 9.x or 10+
|
||||||
|
|
||||||
## 🚀 Development
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Local Development</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# configure
|
# Clone the project
|
||||||
1. installation of the recommended plugins in the .vscode directory
|
|
||||||
2. node version 16+
|
|
||||||
3. pnpm version 8.x
|
|
||||||
|
|
||||||
# clone
|
|
||||||
git clone https://github.com/un-pany/v3-admin-vite.git
|
git clone https://github.com/un-pany/v3-admin-vite.git
|
||||||
|
|
||||||
# enter the project directory
|
# Enter the project directory
|
||||||
cd v3-admin-vite
|
cd v3-admin-vite
|
||||||
|
|
||||||
# install dependencies
|
# Install dependencies
|
||||||
pnpm i
|
pnpm i
|
||||||
|
|
||||||
# start the service
|
# Start the development server
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## ✔️ Preview
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Build</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# stage environment
|
# Build for the staging environment
|
||||||
pnpm preview:stage
|
pnpm build:staging
|
||||||
|
|
||||||
# prod environment
|
# Build for the production environment
|
||||||
pnpm preview:prod
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📦️ Multi-environment packaging
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Local Preview</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# build the stage environment
|
# Execute the build command first to generate the dist directory, then run the preview command
|
||||||
pnpm build:stage
|
pnpm preview
|
||||||
|
|
||||||
# build the prod environment
|
|
||||||
pnpm build:prod
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Code inspection
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Code Check</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# code formatting
|
# Code linting and formatting
|
||||||
pnpm lint
|
pnpm lint
|
||||||
|
|
||||||
# unit test
|
# Unit tests
|
||||||
pnpm test
|
pnpm test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Git commit specification reference
|
</details>
|
||||||
|
|
||||||
- `feat` add new functions
|
<details>
|
||||||
- `fix` Fix issues/bugs
|
<summary>Commit Guidelines</summary>
|
||||||
- `perf` Optimize performance
|
|
||||||
- `style` Change the code style without affecting the running result
|
|
||||||
- `refactor` Re-factor code
|
|
||||||
- `revert` Undo changes
|
|
||||||
- `test` Test related, does not involve changes to business code
|
|
||||||
- `docs` Documentation and Annotation
|
|
||||||
- `chore` Updating dependencies/modifying scaffolding configuration, etc.
|
|
||||||
- `workflow` Work flow Improvements
|
|
||||||
- `ci` CICD
|
|
||||||
- `types` Type definition
|
|
||||||
- `wip` In development
|
|
||||||
|
|
||||||
## Project preview
|
<br>
|
||||||
|
|
||||||

|
`feat` New feature
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
## 💕 Contributors
|
`fix` Bug fix
|
||||||
|
|
||||||
Thanks to all the contributors!
|
`perf` Performance improvement
|
||||||
|
|
||||||
|
`refactor` Code refactoring
|
||||||
|
|
||||||
|
`docs` Documentation and comments
|
||||||
|
|
||||||
|
`types` Type-related changes
|
||||||
|
|
||||||
|
`test` Unit tests related
|
||||||
|
|
||||||
|
`ci` Continuous integration, workflows
|
||||||
|
|
||||||
|
`revert` Revert changes
|
||||||
|
|
||||||
|
`chore` Chores (update dependencies, modify configurations, etc)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
**Online Preview**: [github-pages](https://un-pany.github.io/v3-admin-vite)
|
||||||
|
|
||||||
|
**Chinese Documentation**: [link](https://juejin.cn/post/7089377403717287972)
|
||||||
|
|
||||||
|
**Zero to Hero Tutorial**: [link](https://juejin.cn/column/7207659644487139387)
|
||||||
|
|
||||||
|
**Mobile Web App**: [mobvue](https://github.com/un-pany/mobvue)
|
||||||
|
|
||||||
|
**Electron Desktop Version**: [v3-electron-vite](https://github.com/un-pany/v3-electron-vite)
|
||||||
|
|
||||||
|
**Chinese Repository**: [gitee](https://gitee.com/un-pany/v3-admin-vite)
|
||||||
|
|
||||||
|
**Optional Group**: [check how to join](https://github.com/un-pany/v3-admin-vite/issues/191)
|
||||||
|
|
||||||
|
**Donations**: [buy a coffee for the author](https://github.com/un-pany/v3-admin-vite/issues/69)
|
||||||
|
|
||||||
|
**Releases & Changelog**: [releases](https://github.com/un-pany/v3-admin-vite/releases)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
**Simplified structure**: No complex encapsulation, no complicated type gymnastics, just enough to meet the needs
|
||||||
|
|
||||||
|
**Detailed comments**: Every configuration item comes with as detailed comments as possible
|
||||||
|
|
||||||
|
**Latest dependencies**: Keeps all third-party dependencies up to date
|
||||||
|
|
||||||
|
**Consistency**: Unified code style, naming conventions, and comment style
|
||||||
|
|
||||||
|
## Built-in Features
|
||||||
|
|
||||||
|
**User Management**: Login, logout demonstration
|
||||||
|
|
||||||
|
**Permission Management**: Page-level permissions (dynamic routing), button-level permissions (permission directives, permission functions), route guards
|
||||||
|
|
||||||
|
**Multiple Environments**: Development, staging, and production environments
|
||||||
|
|
||||||
|
**Multiple Themes**: Normal, dark, and deep blue themes
|
||||||
|
|
||||||
|
**Multiple Layouts**: Left-side, top, and hybrid layouts
|
||||||
|
|
||||||
|
**Homepage**: Different dashboard pages for different users
|
||||||
|
|
||||||
|
**Error Pages**: 403, 404
|
||||||
|
|
||||||
|
**Mobile Compatibility**: Layouts compatible with mobile screen resolutions
|
||||||
|
|
||||||
|
**Others**: SVG sprite sheet, dynamic sidebar, dynamic breadcrumbs, tab navigation, content zoom and fullscreen, composable functions
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
**Vue3**: Vue3 + script setup with the latest Vue3 Composition API
|
||||||
|
|
||||||
|
**Element Plus**: The Vue3 version of Element UI
|
||||||
|
|
||||||
|
**Pinia**: The legendary Vuex5
|
||||||
|
|
||||||
|
**Vite**: Really fast
|
||||||
|
|
||||||
|
**Vue Router**: The routing system
|
||||||
|
|
||||||
|
**TypeScript**: A superset of JavaScript
|
||||||
|
|
||||||
|
**pnpm**: A faster, disk-space-saving package manager
|
||||||
|
|
||||||
|
**Scss**: Consistent with Element Plus
|
||||||
|
|
||||||
|
**CSS Variables**: Primarily controls layout and color in the project
|
||||||
|
|
||||||
|
**ESLint**: Code linting and formatting
|
||||||
|
|
||||||
|
**Axios**: Sends network requests
|
||||||
|
|
||||||
|
**UnoCSS**: A high-performance, flexible atomic CSS engine
|
||||||
|
|
||||||
|
## Project Preview Image
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
A big thank you to all the contributors!
|
||||||
|
|
||||||
<a href="https://github.com/un-pany/v3-admin-vite/graphs/contributors">
|
<a href="https://github.com/un-pany/v3-admin-vite/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=un-pany/v3-admin-vite" />
|
<img src="https://contrib.rocks/image?repo=un-pany/v3-admin-vite">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## 💕 Thanks star
|
## License
|
||||||
|
|
||||||
Small projects are not easy to get a star, if you like this project, welcome to support a star! This is the only motivation for the author to maintain it on an ongoing basis (whisper: it's free after all)
|
[MIT](./LICENSE) License © 2022-PRESENT [pany](https://github.com/pany-ang)
|
||||||
|
|
||||||
## ☕ Donate
|
|
||||||
|
|
||||||
[See how to donate](https://github.com/un-pany/v3-admin-vite/issues/69)
|
|
||||||
|
|
||||||
## Group
|
|
||||||
|
|
||||||
QQ group:1014374415 (left) && add me on WeChat,Invite you to join WeChat group (right)
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
[MIT](./LICENSE)
|
|
||||||
|
|
||||||
Copyright (c) 2022-present [pany](https://github.com/pany-ang)
|
|
||||||
|
263
README.zh-CN.md
@ -1,68 +1,55 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img alt="V3 Admin Vite Logo" width="120" height="120" src="./src/assets/layout/logo.png">
|
<img alt="logo" width="120" height="120" src="./src/common/assets/images/layouts/logo.png">
|
||||||
<h1>V3 Admin Vite</h1>
|
<h1>V3 Admin Vite</h1>
|
||||||
<span><a href="./README.md">English</a> | 中文</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## ⚡ 简介
|
[](https://github.com/un-pany/v3-admin-vite/releases)
|
||||||
|
[](https://github.com/un-pany/v3-admin-vite/stargazers)
|
||||||
|
[](https://gitee.com/un-pany/v3-admin-vite/stargazers)
|
||||||
|
|
||||||
一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术.
|
<b><a href="./README.md">English</a> | 中文</b>
|
||||||
|
|
||||||
- Vue-Cli 5.x 版: [v3-admin](https://github.com/un-pany/v3-admin)
|
## 简介
|
||||||
- Electron 桌面版: [v3-electron-vite](https://github.com/un-pany/v3-electron-vite)
|
|
||||||
|
|
||||||
## 特性
|
V3 Admin Vite 是一个精心制作的后台管理系统模板,基于 Vue3、Vite、TypeScript、Element Plus 等主流技术
|
||||||
|
|
||||||
- **Vue3**:采用 Vue3 + script setup 最新的 Vue3 组合式 API
|
## 通知
|
||||||
- **Element Plus**:Element UI 的 Vue3 版本
|
|
||||||
- **Pinia**: 传说中的 Vuex5
|
|
||||||
- **Vite**:真的很快
|
|
||||||
- **Vue Router**:路由路由
|
|
||||||
- **TypeScript**:JavaScript 语言的超集
|
|
||||||
- **PNPM**:更快速的,节省磁盘空间的包管理工具
|
|
||||||
- **Scss**:和 Element Plus 保持一致
|
|
||||||
- **CSS 变量**:主要控制项目的布局和颜色
|
|
||||||
- **ESlint**:代码校验
|
|
||||||
- **Prettier**:代码格式化
|
|
||||||
- **Axios**:发送网络请求(已封装好)
|
|
||||||
- **UnoCSS**:具有高性能且极具灵活性的即时原子化 CSS 引擎
|
|
||||||
- **注释**:各个配置项都写有尽可能详细的注释
|
|
||||||
- **兼容移动端**: 布局兼容移动端页面分辨率
|
|
||||||
|
|
||||||
## 功能
|
> [!NOTE]
|
||||||
|
> 为爱发电!所有源码均免费开源,如果对你有帮助,欢迎点个 Star 支持一下!
|
||||||
|
|
||||||
- **用户管理**:登录、登出演示
|
> [!IMPORTANT]
|
||||||
- **权限管理**:内置页面权限(动态路由)、指令权限、权限函数、路由守卫
|
> 欢迎体验全新的 5.0 版本,目前正在 beta 阶段,它将是一次匠心之作!
|
||||||
- **多环境**:开发环境(development)、预发布环境(staging)、正式环境(production)
|
|
||||||
- **多主题**:内置普通、黑暗、深蓝三种主题模式
|
|
||||||
- **错误页面**: 403、404
|
|
||||||
- **Dashboard**:根据不同用户显示不同的 Dashboard 页面
|
|
||||||
- **其他内置功能**:SVG、动态侧边栏、动态面包屑、标签页快捷导航、Screenfull 全屏、自适应收缩侧边栏
|
|
||||||
|
|
||||||
## 📚 文档
|
> [!WARNING]
|
||||||
|
> 4.x 版本如果没有严重的 BUG 将不再维护
|
||||||
|
|
||||||
[中文文档](https://juejin.cn/post/7089377403717287972)
|
> [!TIP]
|
||||||
|
> 正式推出付费服务,如果不想自己动手,但想移除 TS 或其他模块?试试懒人套餐
|
||||||
|
|
||||||
[手摸手教程](https://juejin.cn/column/7207659644487139387)
|
> [!TIP]
|
||||||
|
> 如果你有移动端 H5 需求,试试新的开源模板。[MobVue](https://github.com/un-pany/mobvue)
|
||||||
|
|
||||||
## 国内仓库
|
## 使用
|
||||||
|
|
||||||
[Gitee](https://gitee.com/un-pany/v3-admin-vite)
|
<details>
|
||||||
|
<summary>推荐环境</summary>
|
||||||
|
|
||||||
## 在线预览
|
<br>
|
||||||
|
|
||||||
| 位置 | 账号 | 链接 |
|
- 新版 `Visual Studio Code`
|
||||||
| ------------ | --------------- | ----------------------------------------------- |
|
- 安装 `.vscode/extensions.json` 文件中推荐的插件
|
||||||
| github-pages | admin 或 editor | [链接](https://un-pany.github.io/v3-admin-vite) |
|
- `node` 20.x 或 22+
|
||||||
|
- `pnpm` 9.x 或 10+
|
||||||
|
|
||||||
## 🚀 开发
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>本地开发</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 配置
|
|
||||||
1. 一键安装 .vscode 目录中推荐的插件
|
|
||||||
2. node 版本 16+
|
|
||||||
3. pnpm 版本 8.x
|
|
||||||
|
|
||||||
# 克隆项目
|
# 克隆项目
|
||||||
git clone https://github.com/un-pany/v3-admin-vite.git
|
git clone https://github.com/un-pany/v3-admin-vite.git
|
||||||
|
|
||||||
@ -76,83 +63,165 @@ pnpm i
|
|||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## ✔️ 预览
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>打包构建</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 预览预发布环境
|
# 打包构建预发布环境
|
||||||
pnpm preview:stage
|
pnpm build:staging
|
||||||
|
|
||||||
# 预览正式环境
|
# 打包构建生产环境
|
||||||
pnpm preview:prod
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📦️ 多环境打包
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>本地预览</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 构建预发布环境
|
# 先执行打包构建命令生成 dist 目录后再执行以下预览命令
|
||||||
pnpm build:stage
|
pnpm preview
|
||||||
|
|
||||||
# 构建正式环境
|
|
||||||
pnpm build:prod
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 代码检查
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>代码检查</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 代码格式化
|
# 代码校验与格式化
|
||||||
pnpm lint
|
pnpm lint
|
||||||
|
|
||||||
# 单元测试
|
# 单元测试
|
||||||
pnpm test
|
pnpm test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Git 提交规范参考
|
</details>
|
||||||
|
|
||||||
- `feat` 增加新的业务功能
|
<details>
|
||||||
- `fix` 修复业务问题/BUG
|
<summary>代码提交规范</summary>
|
||||||
- `perf` 优化性能
|
|
||||||
- `style` 更改代码风格, 不影响运行结果
|
<br>
|
||||||
- `refactor` 重构代码
|
|
||||||
- `revert` 撤销更改
|
`feat` 新功能
|
||||||
- `test` 测试相关, 不涉及业务代码的更改
|
|
||||||
- `docs` 文档和注释相关
|
`fix` 修复错误
|
||||||
- `chore` 更新依赖/修改脚手架配置等琐事
|
|
||||||
- `workflow` 工作流改进
|
`perf` 性能优化
|
||||||
- `ci` 持续集成相关
|
|
||||||
- `types` 类型定义文件更改
|
`refactor` 重构代码
|
||||||
- `wip` 开发中
|
|
||||||
|
`docs` 文档和注释
|
||||||
|
|
||||||
|
`types` 类型相关
|
||||||
|
|
||||||
|
`test` 单测相关
|
||||||
|
|
||||||
|
`ci` 持续集成、工作流
|
||||||
|
|
||||||
|
`revert` 撤销更改
|
||||||
|
|
||||||
|
`chore` 琐事(更新依赖、修改配置等)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 链接
|
||||||
|
|
||||||
|
**在线预览**:[github-pages](https://un-pany.github.io/v3-admin-vite)
|
||||||
|
|
||||||
|
**中文文档**:[链接](https://juejin.cn/post/7445151895121543209)
|
||||||
|
|
||||||
|
**零基础教程**:[链接](https://juejin.cn/column/7207659644487139387)
|
||||||
|
|
||||||
|
**移动端 H5**:[mobvue](https://github.com/un-pany/mobvue)
|
||||||
|
|
||||||
|
**Electron 桌面版**:[v3-electron-vite](https://github.com/un-pany/v3-electron-vite)
|
||||||
|
|
||||||
|
**国内仓库**:[gitee](https://gitee.com/un-pany/v3-admin-vite)
|
||||||
|
|
||||||
|
**可有可无的群**:[查看进群方式](https://github.com/un-pany/v3-admin-vite/issues/191)
|
||||||
|
|
||||||
|
**捐赠**:[请作者喝咖啡](https://github.com/un-pany/v3-admin-vite/issues/69)
|
||||||
|
|
||||||
|
**发行版 & 更新日志**:[releases](https://github.com/un-pany/v3-admin-vite/releases)
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
**结构精简**:没有复杂的封装,没有复杂的类型体操,刚好够用
|
||||||
|
|
||||||
|
**详细的注释**:各个配置项都写有尽可能详细的注释
|
||||||
|
|
||||||
|
**最新的依赖**:及时更新所有三方依赖至最新版
|
||||||
|
|
||||||
|
**有一点规范**:代码风格统一、命名风格统一、注释风格统一
|
||||||
|
|
||||||
|
## 内置功能
|
||||||
|
|
||||||
|
**用户管理**:登录、登出演示
|
||||||
|
|
||||||
|
**权限管理**:页面级权限(动态路由)、按钮级权限(权限指令、权限函数)、路由守卫
|
||||||
|
|
||||||
|
**多环境**:开发环境(development)、预发布环境(staging)、生产环境(production)
|
||||||
|
|
||||||
|
**多主题**:普通、黑暗、深蓝, 三种主题模式
|
||||||
|
|
||||||
|
**多布局**:左侧、顶部、混合, 三种布局模式
|
||||||
|
|
||||||
|
**首页**:根据不同用户显示不同的 Dashboard 页面
|
||||||
|
|
||||||
|
**错误页**:403、404
|
||||||
|
|
||||||
|
**兼容移动端**:布局兼容移动端页面分辨率
|
||||||
|
|
||||||
|
**其他**:SVG 雪碧图、动态侧边栏、动态面包屑、标签页快捷导航、内容区放大与全屏、组合式函数
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
**Vue3**:采用 Vue3 + script setup 最新的 Vue3 组合式 API
|
||||||
|
|
||||||
|
**Element Plus**:Element UI 的 Vue3 版本
|
||||||
|
|
||||||
|
**Pinia**:传说中的 Vuex5
|
||||||
|
|
||||||
|
**Vite**:真的很快
|
||||||
|
|
||||||
|
**Vue Router**:路由路由
|
||||||
|
|
||||||
|
**TypeScript**:JavaScript 语言的超集
|
||||||
|
|
||||||
|
**pnpm**:更快速的,节省磁盘空间的包管理工具
|
||||||
|
|
||||||
|
**Scss**:和 Element Plus 保持一致
|
||||||
|
|
||||||
|
**CSS 变量**:主要控制项目的布局和颜色
|
||||||
|
|
||||||
|
**ESLint**:代码校验与格式化
|
||||||
|
|
||||||
|
**Axios**:发送网络请求(已封装好)
|
||||||
|
|
||||||
|
**UnoCSS**:具有高性能且极具灵活性的即时原子化 CSS 引擎
|
||||||
|
|
||||||
## 项目预览图
|
## 项目预览图
|
||||||
|
|
||||||

|

|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
## 💕 贡献者
|
## 贡献者
|
||||||
|
|
||||||
感谢所有的贡献者!
|
在此感谢所有的贡献者!
|
||||||
|
|
||||||
<a href="https://github.com/un-pany/v3-admin-vite/graphs/contributors">
|
<a href="https://github.com/un-pany/v3-admin-vite/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=un-pany/v3-admin-vite" />
|
<img src="https://contrib.rocks/image?repo=un-pany/v3-admin-vite">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## 💕 感谢 Star
|
## License
|
||||||
|
|
||||||
小项目获取 star 不易,如果你喜欢这个项目的话,欢迎支持一个 star!这是作者持续维护的唯一动力(小声:毕竟是免费的)
|
[MIT](./LICENSE) License © 2022-PRESENT [pany](https://github.com/pany-ang)
|
||||||
|
|
||||||
## ☕ Donate
|
|
||||||
|
|
||||||
[查看捐赠方式](https://github.com/un-pany/v3-admin-vite/issues/69)
|
|
||||||
|
|
||||||
## 可有可无的群
|
|
||||||
|
|
||||||
QQ 群:1014374415(左)&& 加我微信,拉你进微信群(右)
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
[MIT](./LICENSE)
|
|
||||||
|
|
||||||
Copyright (c) 2022-present [pany](https://github.com/pany-ang)
|
|
||||||
|
43
eslint.config.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import antfu from "@antfu/eslint-config"
|
||||||
|
|
||||||
|
// 更多自定义配置可查阅仓库:https://github.com/antfu/eslint-config
|
||||||
|
export default antfu(
|
||||||
|
{
|
||||||
|
// 使用外部格式化程序格式化 css、html、markdown 等文件
|
||||||
|
formatters: true,
|
||||||
|
// 启用样式规则
|
||||||
|
stylistic: {
|
||||||
|
// 缩进级别
|
||||||
|
indent: 2,
|
||||||
|
// 引号风格 'single' | 'double'
|
||||||
|
quotes: "double",
|
||||||
|
// 是否启用分号
|
||||||
|
semi: false
|
||||||
|
},
|
||||||
|
// 忽略文件
|
||||||
|
ignores: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 对所有文件都生效的规则
|
||||||
|
rules: {
|
||||||
|
// vue
|
||||||
|
"vue/block-order": ["error", { order: ["script", "template", "style"] }],
|
||||||
|
"vue/attributes-order": "off",
|
||||||
|
// ts
|
||||||
|
"ts/no-use-before-define": "off",
|
||||||
|
// node
|
||||||
|
"node/prefer-global/process": "off",
|
||||||
|
// style
|
||||||
|
"style/comma-dangle": ["error", "never"],
|
||||||
|
"style/brace-style": ["error", "1tbs"],
|
||||||
|
// regexp
|
||||||
|
"regexp/no-unused-capturing-group": "off",
|
||||||
|
// other
|
||||||
|
"no-console": "off",
|
||||||
|
"no-debugger": "off",
|
||||||
|
"symbol-description": "off",
|
||||||
|
"antfu/if-newline": "off",
|
||||||
|
"unicorn/no-instanceof-builtins": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
@ -1,11 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<link rel="stylesheet" href="/app-loading.css" />
|
<link rel="stylesheet" href="/app-loading.css" />
|
||||||
<title>V3 Admin Vite</title>
|
<title>%VITE_APP_TITLE%</title>
|
||||||
|
<script src="/detect-ie.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
130
package.json
@ -1,102 +1,64 @@
|
|||||||
{
|
{
|
||||||
"name": "v3-admin-vite",
|
"name": "v3-admin-vite",
|
||||||
"version": "4.0.0",
|
"type": "module",
|
||||||
"description": "一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术.",
|
"version": "5.0.0-beta.6",
|
||||||
"author": {
|
"description": "A crafted admin template, built with Vue3, Vite, TypeScript, Element Plus, and more",
|
||||||
"name": "pany",
|
"author": "pany <939630029@qq.com> (https://github.com/pany-ang)",
|
||||||
"email": "939630029@qq.com",
|
"repository": "https://github.com/un-pany/v3-admin-vite",
|
||||||
"url": "https://github.com/pany-ang"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/un-pany/v3-admin-vite.git"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build:stage": "vue-tsc --noEmit && vite build --mode staging",
|
"build:staging": "vue-tsc && vite build --mode staging",
|
||||||
"build:prod": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc && vite build",
|
||||||
"preview:stage": "pnpm build:stage && vite preview",
|
"preview": "vite preview",
|
||||||
"preview:prod": "pnpm build:prod && vite preview",
|
"lint": "eslint . --fix",
|
||||||
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,tests,types}/**/*.{vue,js,jsx,ts,tsx}\" --fix",
|
"prepare": "husky",
|
||||||
"lint:prettier": "prettier --write \"{src,tests,types}/**/*.{vue,js,jsx,ts,tsx,json,css,less,scss,html,md}\"",
|
|
||||||
"lint": "pnpm lint:eslint && pnpm lint:prettier",
|
|
||||||
"prepare": "husky install",
|
|
||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "2.1.0",
|
"@element-plus/icons-vue": "2.3.1",
|
||||||
"axios": "1.4.0",
|
"axios": "1.8.4",
|
||||||
"dayjs": "1.11.8",
|
"dayjs": "1.11.13",
|
||||||
"element-plus": "2.3.6",
|
"element-plus": "2.9.7",
|
||||||
"js-cookie": "3.0.5",
|
"js-cookie": "3.0.5",
|
||||||
"lodash-es": "4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
|
"mitt": "3.0.1",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"path-browserify": "1.0.1",
|
"path-browserify": "1.0.1",
|
||||||
"path-to-regexp": "6.2.1",
|
"path-to-regexp": "8.2.0",
|
||||||
"pinia": "2.1.4",
|
"pinia": "3.0.2",
|
||||||
"screenfull": "6.0.2",
|
"screenfull": "6.0.2",
|
||||||
"vue": "3.3.4",
|
"vue": "3.5.13",
|
||||||
"vue-router": "4.2.2",
|
"vue-router": "4.5.0",
|
||||||
"vxe-table": "4.4.1",
|
"vxe-table": "4.6.25"
|
||||||
"vxe-table-plugin-element": "3.0.7",
|
|
||||||
"xe-utils": "3.5.11"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/js-cookie": "3.0.3",
|
"@antfu/eslint-config": "4.12.0",
|
||||||
"@types/lodash-es": "4.17.7",
|
"@types/js-cookie": "3.0.6",
|
||||||
"@types/node": "20.3.1",
|
"@types/lodash-es": "4.17.12",
|
||||||
"@types/nprogress": "0.2.0",
|
"@types/node": "22.14.1",
|
||||||
"@types/path-browserify": "1.0.0",
|
"@types/nprogress": "0.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "5.60.0",
|
"@types/path-browserify": "1.0.3",
|
||||||
"@typescript-eslint/parser": "5.60.0",
|
"@vitejs/plugin-vue": "5.2.3",
|
||||||
"@vitejs/plugin-vue": "4.2.3",
|
"@vitejs/plugin-vue-jsx": "4.1.2",
|
||||||
"@vitejs/plugin-vue-jsx": "3.0.1",
|
"@vue/test-utils": "2.4.6",
|
||||||
"@vue/eslint-config-prettier": "7.1.0",
|
"eslint": "9.24.0",
|
||||||
"@vue/eslint-config-typescript": "11.0.3",
|
"eslint-plugin-format": "1.0.1",
|
||||||
"@vue/test-utils": "2.3.2",
|
"happy-dom": "17.4.4",
|
||||||
"eslint": "8.43.0",
|
"husky": "9.1.7",
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"lint-staged": "15.5.1",
|
||||||
"eslint-plugin-vue": "9.15.0",
|
"sass": "1.78.0",
|
||||||
"husky": "8.0.3",
|
"typescript": "5.8.3",
|
||||||
"jsdom": "22.1.0",
|
"unocss": "66.1.0-beta.12",
|
||||||
"lint-staged": "13.2.2",
|
"unplugin-auto-import": "19.1.2",
|
||||||
"prettier": "2.8.8",
|
"unplugin-svg-component": "0.12.1",
|
||||||
"sass": "1.63.5",
|
"unplugin-vue-components": "28.5.0",
|
||||||
"terser": "5.18.1",
|
"vite": "6.3.2",
|
||||||
"typescript": "5.1.3",
|
"vite-svg-loader": "5.1.0",
|
||||||
"unocss": "0.53.1",
|
"vitest": "3.1.1",
|
||||||
"vite": "4.3.9",
|
"vue-tsc": "2.2.8"
|
||||||
"vite-plugin-svg-icons": "2.0.1",
|
|
||||||
"vite-svg-loader": "4.0.0",
|
|
||||||
"vitest": "0.32.2",
|
|
||||||
"vue-eslint-parser": "9.3.1",
|
|
||||||
"vue-tsc": "1.8.1"
|
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{vue,js,jsx,ts,tsx}": [
|
"*": "eslint --fix"
|
||||||
"eslint --fix",
|
}
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"*.{css,less,scss,html,md}": [
|
|
||||||
"prettier --write"
|
|
||||||
],
|
|
||||||
"package.json": [
|
|
||||||
"prettier --write"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"vue",
|
|
||||||
"vue3",
|
|
||||||
"admin",
|
|
||||||
"vue-admin",
|
|
||||||
"vue3-admin",
|
|
||||||
"vite",
|
|
||||||
"vite-admin",
|
|
||||||
"element-plus",
|
|
||||||
"element-plus-admin",
|
|
||||||
"ts",
|
|
||||||
"typescript"
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
}
|
}
|
||||||
|
10497
pnpm-lock.yaml
generated
@ -1,21 +0,0 @@
|
|||||||
/** 配置项文档:https://prettier.io/docs/en/configuration.html */
|
|
||||||
module.exports = {
|
|
||||||
/** 每一行的宽度 */
|
|
||||||
printWidth: 120,
|
|
||||||
/** Tab 键的空格数 */
|
|
||||||
tabWidth: 2,
|
|
||||||
/** 在对象中的括号之间是否用空格来间隔 */
|
|
||||||
bracketSpacing: true,
|
|
||||||
/** 箭头函数的参数无论有几个,都要括号包裹 */
|
|
||||||
arrowParens: "always",
|
|
||||||
/** 换行符的使用 */
|
|
||||||
endOfLine: "auto",
|
|
||||||
/** 是否采用单引号 */
|
|
||||||
singleQuote: false,
|
|
||||||
/** 对象或者数组的最后一个元素后面不要加逗号 */
|
|
||||||
trailingComma: "none",
|
|
||||||
/** 是否加分号 */
|
|
||||||
semi: false,
|
|
||||||
/** 是否使用 Tab 格式化 */
|
|
||||||
useTabs: false
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
/** 白屏阶段会执行的 CSS 加载动画 */
|
/* 白屏阶段会执行的 CSS 加载动画 */
|
||||||
|
|
||||||
#app-loading {
|
#app-loading {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -9,26 +9,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#app-loading,
|
#app-loading,
|
||||||
#app-loading:before,
|
#app-loading::before,
|
||||||
#app-loading:after {
|
#app-loading::after {
|
||||||
width: 2em;
|
width: 2em;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: 2s ease-in-out infinite app-loading-animation;
|
animation: 2s ease-in-out infinite app-loading-animation;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app-loading:before,
|
#app-loading::before,
|
||||||
#app-loading:after {
|
#app-loading::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app-loading:before {
|
#app-loading::before {
|
||||||
left: -4em;
|
left: -4em;
|
||||||
animation-delay: -0.2s;
|
animation-delay: -0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app-loading:after {
|
#app-loading::after {
|
||||||
left: 4em;
|
left: 4em;
|
||||||
animation-delay: 0.2s;
|
animation-delay: 0.2s;
|
||||||
}
|
}
|
||||||
|
4
public/detect-ie.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Tip: Simple judgments may not fully cover
|
||||||
|
if (/MSIE\s|Trident\//.test(navigator.userAgent)) {
|
||||||
|
document.body.innerHTML = "<strong>Sorry, this browser is currently not supported. We recommend using the latest version of a modern browser. For example, Chrome/Firefox/Edge.</strong>"
|
||||||
|
}
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 8.4 KiB |
34
src/App.vue
@ -1,30 +1,24 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { h } from "vue"
|
import { useGreyAndColorWeakness } from "@@/composables/useGreyAndColorWeakness"
|
||||||
import { useTheme } from "@/hooks/useTheme"
|
import { usePany } from "@@/composables/usePany"
|
||||||
import { ElNotification } from "element-plus"
|
import { useTheme } from "@@/composables/useTheme"
|
||||||
// 将 Element Plus 的语言设置为中文
|
import zhCn from "element-plus/es/locale/lang/zh-cn" // Element Plus 中文包
|
||||||
import zhCn from "element-plus/lib/locale/lang/zh-cn"
|
|
||||||
|
|
||||||
const { initTheme } = useTheme()
|
const { initTheme } = useTheme()
|
||||||
|
const { initGreyAndColorWeakness } = useGreyAndColorWeakness()
|
||||||
|
const { initStarNotification, initStoreNotification } = usePany()
|
||||||
|
|
||||||
/** 初始化主题 */
|
// 初始化主题
|
||||||
initTheme()
|
initTheme()
|
||||||
|
// 初始化灰色模式和色弱模式
|
||||||
/** 作者小心思 */
|
initGreyAndColorWeakness()
|
||||||
ElNotification({
|
// 初始化通知
|
||||||
title: "Hello",
|
initStarNotification()
|
||||||
message: h(
|
initStoreNotification()
|
||||||
"a",
|
|
||||||
{ style: "color: teal", target: "_blank", href: "https://github.com/un-pany/v3-admin-vite" },
|
|
||||||
"小项目获取 star 不易,如果你喜欢这个项目的话,欢迎点击这里支持一个 star !这是作者持续维护的唯一动力(小声:毕竟是免费的)"
|
|
||||||
),
|
|
||||||
duration: 0,
|
|
||||||
position: "bottom-right"
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElConfigProvider :locale="zhCn">
|
<el-config-provider :locale="zhCn">
|
||||||
<router-view />
|
<router-view />
|
||||||
</ElConfigProvider>
|
</el-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import { request } from "@/utils/service"
|
|
||||||
import type * as Login from "./types/login"
|
|
||||||
|
|
||||||
/** 获取登录验证码 */
|
|
||||||
export function getLoginCodeApi() {
|
|
||||||
return request<Login.LoginCodeResponseData>({
|
|
||||||
url: "login/code",
|
|
||||||
method: "get"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 登录并返回 Token */
|
|
||||||
export function loginApi(data: Login.LoginRequestData) {
|
|
||||||
return request<Login.LoginResponseData>({
|
|
||||||
url: "users/login",
|
|
||||||
method: "post",
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取用户详情 */
|
|
||||||
export function getUserInfoApi() {
|
|
||||||
return request<Login.UserInfoResponseData>({
|
|
||||||
url: "users/info",
|
|
||||||
method: "get"
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
import { request } from "@/utils/service"
|
|
||||||
import type * as Table from "./types/table"
|
|
||||||
|
|
||||||
/** 增 */
|
|
||||||
export function createTableDataApi(data: Table.CreateTableRequestData) {
|
|
||||||
return request({
|
|
||||||
url: "table",
|
|
||||||
method: "post",
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 删 */
|
|
||||||
export function deleteTableDataApi(id: string) {
|
|
||||||
return request({
|
|
||||||
url: `table/${id}`,
|
|
||||||
method: "delete"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 改 */
|
|
||||||
export function updateTableDataApi(data: Table.UpdateTableRequestData) {
|
|
||||||
return request({
|
|
||||||
url: "table",
|
|
||||||
method: "put",
|
|
||||||
data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 查 */
|
|
||||||
export function getTableDataApi(params: Table.GetTableRequestData) {
|
|
||||||
return request<Table.GetTableResponseData>({
|
|
||||||
url: "table",
|
|
||||||
method: "get",
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
Before Width: | Height: | Size: 101 KiB |
Before Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 373 KiB |
Before Width: | Height: | Size: 407 KiB |
Before Width: | Height: | Size: 26 KiB |
37
src/common/apis/tables/index.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type * as Tables from "./type"
|
||||||
|
import { request } from "@/http/axios"
|
||||||
|
|
||||||
|
/** 增 */
|
||||||
|
export function createTableDataApi(data: Tables.CreateOrUpdateTableRequestData) {
|
||||||
|
return request({
|
||||||
|
url: "tables",
|
||||||
|
method: "post",
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删 */
|
||||||
|
export function deleteTableDataApi(id: number) {
|
||||||
|
return request({
|
||||||
|
url: `tables/${id}`,
|
||||||
|
method: "delete"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 改 */
|
||||||
|
export function updateTableDataApi(data: Tables.CreateOrUpdateTableRequestData) {
|
||||||
|
return request({
|
||||||
|
url: "tables",
|
||||||
|
method: "put",
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查 */
|
||||||
|
export function getTableDataApi(params: Tables.TableRequestData) {
|
||||||
|
return request<Tables.TableResponseData>({
|
||||||
|
url: "tables",
|
||||||
|
method: "get",
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
@ -1,15 +1,10 @@
|
|||||||
export interface CreateTableRequestData {
|
export interface CreateOrUpdateTableRequestData {
|
||||||
username: string
|
id?: number
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateTableRequestData {
|
|
||||||
id: string
|
|
||||||
username: string
|
username: string
|
||||||
password?: string
|
password?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetTableRequestData {
|
export interface TableRequestData {
|
||||||
/** 当前页码 */
|
/** 当前页码 */
|
||||||
currentPage: number
|
currentPage: number
|
||||||
/** 查询条数 */
|
/** 查询条数 */
|
||||||
@ -20,17 +15,17 @@ export interface GetTableRequestData {
|
|||||||
phone?: string
|
phone?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetTableData {
|
export interface TableData {
|
||||||
createTime: string
|
createTime: string
|
||||||
email: string
|
email: string
|
||||||
id: string
|
id: number
|
||||||
phone: string
|
phone: string
|
||||||
roles: string
|
roles: string
|
||||||
status: boolean
|
status: boolean
|
||||||
username: string
|
username: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetTableResponseData = ApiResponseData<{
|
export type TableResponseData = ApiResponseData<{
|
||||||
list: GetTableData[]
|
list: TableData[]
|
||||||
total: number
|
total: number
|
||||||
}>
|
}>
|
10
src/common/apis/users/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import type * as Users from "./type"
|
||||||
|
import { request } from "@/http/axios"
|
||||||
|
|
||||||
|
/** 获取当前登录用户详情 */
|
||||||
|
export function getCurrentUserApi() {
|
||||||
|
return request<Users.CurrentUserResponseData>({
|
||||||
|
url: "users/me",
|
||||||
|
method: "get"
|
||||||
|
})
|
||||||
|
}
|
1
src/common/apis/users/type.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type CurrentUserResponseData = ApiResponseData<{ username: string, roles: string[] }>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 747 B After Width: | Height: | Size: 747 B |
Before Width: | Height: | Size: 746 B After Width: | Height: | Size: 746 B |
1
src/common/assets/icons/keyboard-down.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="15" height="15" aria-label="Arrow down" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M7.5 3.5v8M10.5 8.5l-3 3-3-3"></path></g></svg>
|
After Width: | Height: | Size: 223 B |
1
src/common/assets/icons/keyboard-enter.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="15" height="15" aria-label="Enter key" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M12 3.53088v3c0 1-1 2-2 2H4M7 11.53088l-3-3 3-3"></path></g></svg>
|
After Width: | Height: | Size: 241 B |
1
src/common/assets/icons/keyboard-esc.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="15" height="15" aria-label="Escape key" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M13.6167 8.936c-.1065.3583-.6883.962-1.4875.962-.7993 0-1.653-.9165-1.653-2.1258v-.5678c0-1.2548.7896-2.1016 1.653-2.1016.8634 0 1.3601.4778 1.4875 1.0724M9 6c-.1352-.4735-.7506-.9219-1.46-.8972-.7092.0246-1.344.57-1.344 1.2166s.4198.8812 1.3445.9805C8.465 7.3992 8.968 7.9337 9 8.5c.032.5663-.454 1.398-1.4595 1.398C6.6593 9.898 6 9 5.963 8.4851m-1.4748.5368c-.2635.5941-.8099.876-1.5443.876s-1.7073-.6248-1.7073-2.204v-.4603c0-1.0416.721-2.131 1.7073-2.131.9864 0 1.6425 1.031 1.5443 2.2492h-2.956"></path></g></svg>
|
After Width: | Height: | Size: 694 B |
1
src/common/assets/icons/keyboard-up.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="15" height="15" aria-label="Arrow up" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M7.5 11.5v-8M10.5 6.5l-3-3-3 3"></path></g></svg>
|
After Width: | Height: | Size: 223 B |
11
src/common/assets/icons/preserve-color/README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
## 目录说明
|
||||||
|
|
||||||
|
- `common/assets/icons/preserve-color` 目录下存放带颜色的 svg icon
|
||||||
|
|
||||||
|
- `common/assets/icons` 目录存放的 svg icon 会被插件重写 `fill` 和 `stroke` 属性,使得图片自带的颜色丢失,从而继承父元素的颜色
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
`common/assets/icons/preserve-color` 目录下需要添加 `preserve-color/` 前缀,像这样: `<SvgIcon name="preserve-color/name" />`
|
||||||
|
|
||||||
|
`common/assets/icons` 目录下则不需要,像这样: `<SvgIcon name="name" />`
|
1
src/common/assets/icons/search.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg t="1691398959507" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2431" width="200" height="200"><path d="M862.609 816.955L726.44 680.785l-0.059-0.056a358.907 358.907 0 0 0 56.43-91.927c18.824-44.507 28.369-91.767 28.369-140.467 0-48.701-9.545-95.96-28.369-140.467-18.176-42.973-44.19-81.56-77.319-114.689-33.13-33.129-71.717-59.144-114.69-77.32-44.507-18.825-91.767-28.37-140.467-28.37-48.701 0-95.96 9.545-140.467 28.37-42.973 18.176-81.56 44.19-114.689 77.32-33.13 33.129-59.144 71.717-77.32 114.689-18.825 44.507-28.37 91.767-28.37 140.467 0 48.7 9.545 95.96 28.37 140.467 18.176 42.974 44.19 81.561 77.32 114.69 33.129 33.129 71.717 59.144 114.689 77.319 44.507 18.824 91.767 28.369 140.467 28.369 48.7 0 95.96-9.545 140.467-28.369 32.78-13.864 62.997-32.303 90.197-54.968 0.063 0.064 0.122 0.132 0.186 0.195l136.169 136.17c6.25 6.25 14.438 9.373 22.628 9.373 8.188 0 16.38-3.125 22.627-9.372 12.496-12.496 12.496-32.758 0-45.254z m-412.274-69.466c-79.907 0-155.031-31.118-211.534-87.62-56.503-56.503-87.62-131.627-87.62-211.534s31.117-155.031 87.62-211.534c56.502-56.503 131.626-87.62 211.534-87.62s155.031 31.117 211.534 87.62c56.502 56.502 87.62 131.626 87.62 211.534s-31.118 155.031-87.62 211.534c-56.503 56.502-131.627 87.62-211.534 87.62z" p-id="2432"></path></svg>
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/common/assets/images/docs/preview.png
Normal file
After Width: | Height: | Size: 492 KiB |
BIN
src/common/assets/images/layouts/logo-text-1.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
src/common/assets/images/layouts/logo-text-2.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
src/common/assets/images/layouts/logo.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
90
src/common/assets/styles/element-plus.css
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* @description dark-blue 主题模式下的 Element Plus CSS 变量
|
||||||
|
* @description 在此查阅所有可自定义的变量:https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss
|
||||||
|
* @description 也可以打开浏览器控制台选择元素,查看要覆盖的变量名
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 基础颜色 */
|
||||||
|
html.dark-blue {
|
||||||
|
/* color-primary */
|
||||||
|
--el-color-primary: #00bb99;
|
||||||
|
--el-color-primary-light-3: #00bb99b3;
|
||||||
|
--el-color-primary-light-5: #00bb9980;
|
||||||
|
--el-color-primary-light-7: #00bb994d;
|
||||||
|
--el-color-primary-light-8: #00bb9933;
|
||||||
|
--el-color-primary-light-9: #00bb991a;
|
||||||
|
--el-color-primary-dark-2: #00bb99;
|
||||||
|
/* color-success */
|
||||||
|
--el-color-success: #67c23a;
|
||||||
|
--el-color-success-light-3: #67c23ab3;
|
||||||
|
--el-color-success-light-5: #67c23a80;
|
||||||
|
--el-color-success-light-7: #67c23a4d;
|
||||||
|
--el-color-success-light-8: #67c23a33;
|
||||||
|
--el-color-success-light-9: #67c23a1a;
|
||||||
|
--el-color-success-dark-2: #67c23a;
|
||||||
|
/* color-warning */
|
||||||
|
--el-color-warning: #e6a23c;
|
||||||
|
--el-color-warning-light-3: #e6a23cb3;
|
||||||
|
--el-color-warning-light-5: #e6a23c80;
|
||||||
|
--el-color-warning-light-7: #e6a23c4d;
|
||||||
|
--el-color-warning-light-8: #e6a23c33;
|
||||||
|
--el-color-warning-light-9: #e6a23c1a;
|
||||||
|
--el-color-warning-dark-2: #e6a23c;
|
||||||
|
/* color-danger */
|
||||||
|
--el-color-danger: #f56c6c;
|
||||||
|
--el-color-danger-light-3: #f56c6cb3;
|
||||||
|
--el-color-danger-light-5: #f56c6c80;
|
||||||
|
--el-color-danger-light-7: #f56c6c4d;
|
||||||
|
--el-color-danger-light-8: #f56c6c33;
|
||||||
|
--el-color-danger-light-9: #f56c6c1a;
|
||||||
|
--el-color-danger-dark-2: #f56c6c;
|
||||||
|
/* color-error */
|
||||||
|
--el-color-error: #f56c6c;
|
||||||
|
--el-color-error-light-3: #f56c6cb3;
|
||||||
|
--el-color-error-light-5: #f56c6c80;
|
||||||
|
--el-color-error-light-7: #f56c6c4d;
|
||||||
|
--el-color-error-light-8: #f56c6c33;
|
||||||
|
--el-color-error-light-9: #f56c6c1a;
|
||||||
|
--el-color-error-dark-2: #f56c6c;
|
||||||
|
/* color-info */
|
||||||
|
--el-color-info: #909399;
|
||||||
|
--el-color-info-light-3: #909399b3;
|
||||||
|
--el-color-info-light-5: #90939980;
|
||||||
|
--el-color-info-light-7: #9093994d;
|
||||||
|
--el-color-info-light-8: #90939933;
|
||||||
|
--el-color-info-light-9: #9093991a;
|
||||||
|
--el-color-info-dark-2: #909399;
|
||||||
|
/* text-color */
|
||||||
|
--el-text-color-primary: #e5eaf3;
|
||||||
|
--el-text-color-regular: #cfd3dc;
|
||||||
|
--el-text-color-secondary: #a3a6ad;
|
||||||
|
--el-text-color-placeholder: #8d9095;
|
||||||
|
--el-text-color-disabled: #6c6e72;
|
||||||
|
/* border-color */
|
||||||
|
--el-border-color-darker: #003380;
|
||||||
|
--el-border-color-dark: #003380;
|
||||||
|
--el-border-color: #003380;
|
||||||
|
--el-border-color-light: #003380;
|
||||||
|
--el-border-color-lighter: #003380;
|
||||||
|
--el-border-color-extra-light: #003380;
|
||||||
|
/* fill-color */
|
||||||
|
--el-fill-color-darker: #002b6b;
|
||||||
|
--el-fill-color-dark: #002b6b;
|
||||||
|
--el-fill-color: #002b6b;
|
||||||
|
--el-fill-color-light: #002359;
|
||||||
|
--el-fill-color-lighter: #002359;
|
||||||
|
--el-fill-color-blank: #001b44;
|
||||||
|
--el-fill-color-extra-light: #001b44;
|
||||||
|
/* bg-color */
|
||||||
|
--el-bg-color-page: #001535;
|
||||||
|
--el-bg-color: #001b44;
|
||||||
|
--el-bg-color-overlay: #002359;
|
||||||
|
/* mask-color */
|
||||||
|
--el-mask-color: rgba(0, 0, 0, 0.5);
|
||||||
|
--el-mask-color-extra-light: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* button */
|
||||||
|
html.dark-blue .el-button {
|
||||||
|
--el-button-disabled-text-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
@ -1,11 +1,8 @@
|
|||||||
/** 自定义 Element Plus 样式 */
|
// 自定义 Element Plus 样式
|
||||||
|
|
||||||
// 表格
|
// 卡片
|
||||||
.el-table {
|
.el-card {
|
||||||
// 表头
|
background-color: var(--el-bg-color) !important;
|
||||||
th.el-table__cell {
|
|
||||||
background-color: var(--el-fill-color-light) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
@ -17,7 +14,7 @@
|
|||||||
.el-pagination__jump,
|
.el-pagination__jump,
|
||||||
.btn-prev,
|
.btn-prev,
|
||||||
.btn-next {
|
.btn-next {
|
||||||
display: none !important;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,13 +3,17 @@
|
|||||||
// Transition
|
// Transition
|
||||||
@import "./transition.scss";
|
@import "./transition.scss";
|
||||||
// Element Plus
|
// Element Plus
|
||||||
|
@import "./element-plus.css";
|
||||||
@import "./element-plus.scss";
|
@import "./element-plus.scss";
|
||||||
// Vxe Table
|
// Vxe Table
|
||||||
|
@import "./vxe-table.css";
|
||||||
@import "./vxe-table.scss";
|
@import "./vxe-table.scss";
|
||||||
// 注册多主题
|
// 注册多主题
|
||||||
@import "./theme/register.scss";
|
@import "./theme/register.scss";
|
||||||
// mixin
|
// Mixins
|
||||||
@import "./mixins.scss";
|
@import "./mixins.scss";
|
||||||
|
// View Transition
|
||||||
|
@import "./view-transition.scss";
|
||||||
|
|
||||||
// 业务页面几乎都应该在根元素上挂载 class="app-container",以保持页面美观
|
// 业务页面几乎都应该在根元素上挂载 class="app-container",以保持页面美观
|
||||||
.app-container {
|
.app-container {
|
||||||
@ -18,16 +22,26 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
// 灰色模式
|
||||||
|
&.grey-mode {
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
// 色弱模式
|
||||||
|
&.color-weakness {
|
||||||
|
filter: invert(0.8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
color: var(--v3-body-text-color);
|
||||||
background-color: var(--v3-body-bg-color);
|
background-color: var(--v3-body-bg-color);
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial,
|
font-family:
|
||||||
|
Inter, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
@include scrollbar();
|
@extend %scrollbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
@ -35,8 +49,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*:before,
|
*::before,
|
||||||
*:after {
|
*::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
@ -1,14 +1,14 @@
|
|||||||
/** 清除浮动 */
|
// 清除浮动
|
||||||
@mixin clearfix {
|
%clearfix {
|
||||||
&:after {
|
&::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: table;
|
display: table;
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 美化原生滚动条 */
|
// 美化原生滚动条
|
||||||
@mixin scrollbar {
|
%scrollbar {
|
||||||
// 整个滚动条
|
// 整个滚动条
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@ -31,8 +31,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 文本溢出时显示省略号 */
|
// 文本溢出时显示省略号
|
||||||
@mixin ellipsis {
|
%ellipsis {
|
||||||
// 隐藏溢出的文本
|
// 隐藏溢出的文本
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
// 防止文本换行
|
// 防止文本换行
|
29
src/common/assets/styles/theme/core/element-plus.scss
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Element Plus 相关
|
||||||
|
|
||||||
|
// 侧边栏的 item 的 popper
|
||||||
|
.el-popper {
|
||||||
|
.el-menu {
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
.el-menu-item {
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
&.is-active,
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--el-bg-color-overlay);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-sub-menu__title {
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
.el-sub-menu {
|
||||||
|
&.is-active {
|
||||||
|
> .el-sub-menu__title {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-menu--horizontal {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
4
src/common/assets/styles/theme/core/index.scss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.#{$theme-name} {
|
||||||
|
@import "./layouts.scss";
|
||||||
|
@import "./element-plus.scss";
|
||||||
|
}
|
34
src/common/assets/styles/theme/core/layouts.scss
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Layout 相关
|
||||||
|
|
||||||
|
.app-wrapper {
|
||||||
|
// 侧边栏
|
||||||
|
.sidebar-container {
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
.el-menu {
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
.el-menu-item {
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
&.is-active,
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--el-bg-color-overlay);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.el-sub-menu__title {
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
}
|
||||||
|
.el-sub-menu {
|
||||||
|
&.is-active {
|
||||||
|
> .el-sub-menu__title {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧设置面板
|
||||||
|
.handle-button {
|
||||||
|
background-color: lighten($theme-bg-color, 20%) !important;
|
||||||
|
}
|
6
src/common/assets/styles/theme/dark-blue/variables.scss
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// dark-blue 主题下的变量
|
||||||
|
|
||||||
|
// 主题名称
|
||||||
|
$theme-name: "dark-blue";
|
||||||
|
// 主题背景颜色
|
||||||
|
$theme-bg-color: #001b44;
|
2
src/common/assets/styles/theme/dark/index.scss
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@import "./variables.scss";
|
||||||
|
@import "../core/index.scss";
|
6
src/common/assets/styles/theme/dark/variables.scss
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// dark 主题下的变量
|
||||||
|
|
||||||
|
// 主题名称
|
||||||
|
$theme-name: "dark";
|
||||||
|
// 主题背景颜色
|
||||||
|
$theme-bg-color: #141414;
|
@ -1,4 +1,4 @@
|
|||||||
// See https://cn.vuejs.org/guide/built-ins/transition.html for detail
|
// https://cn.vuejs.org/guide/built-ins/transition
|
||||||
|
|
||||||
// fade-transform
|
// fade-transform
|
||||||
.fade-transform-leave-active,
|
.fade-transform-leave-active,
|
||||||
@ -14,12 +14,12 @@
|
|||||||
transform: translateX(30px);
|
transform: translateX(30px);
|
||||||
}
|
}
|
||||||
|
|
||||||
// sidebar-logo-fade
|
// layout-logo-fade
|
||||||
.sidebar-logo-fade-enter-active,
|
.layout-logo-fade-enter-active,
|
||||||
.sidebar-logo-fade-leave-active {
|
.layout-logo-fade-leave-active {
|
||||||
transition: opacity 1.5s;
|
transition: opacity 1.5s;
|
||||||
}
|
}
|
||||||
.sidebar-logo-fade-enter-from,
|
.layout-logo-fade-enter-from,
|
||||||
.sidebar-logo-fade-leave-to {
|
.layout-logo-fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
72
src/common/assets/styles/variables.css
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/* 全局 CSS 变量,这种变量不仅可以在 CSS 和 SCSS 中使用,还可以导入到 JS 中使用 */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Body */
|
||||||
|
--v3-body-text-color: var(--el-text-color-primary);
|
||||||
|
--v3-body-bg-color: var(--el-bg-color-page);
|
||||||
|
/* Header 区域 = NavigationBar 组件 + TagsView 组件 */
|
||||||
|
--v3-header-height: calc(
|
||||||
|
var(--v3-navigationbar-height) + var(--v3-tagsview-height) + var(--v3-header-border-bottom-width)
|
||||||
|
);
|
||||||
|
--v3-header-bg-color: var(--el-bg-color);
|
||||||
|
--v3-header-box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
--v3-header-border-bottom-width: 1px;
|
||||||
|
--v3-header-border-bottom: var(--v3-header-border-bottom-width) solid var(--el-fill-color);
|
||||||
|
/* NavigationBar 组件 */
|
||||||
|
--v3-navigationbar-height: 50px;
|
||||||
|
--v3-navigationbar-text-color: var(--el-text-color-regular);
|
||||||
|
/* Sidebar 组件(左侧模式全部生效、顶部模式全部不生效、混合模式非颜色部分生效) */
|
||||||
|
--v3-sidebar-width: 220px;
|
||||||
|
--v3-sidebar-hide-width: 58px;
|
||||||
|
--v3-sidebar-border-right: 1px solid var(--el-fill-color);
|
||||||
|
--v3-sidebar-menu-item-height: 60px;
|
||||||
|
--v3-sidebar-menu-tip-line-bg-color: var(--el-color-primary);
|
||||||
|
--v3-sidebar-menu-bg-color: #001428;
|
||||||
|
--v3-sidebar-menu-hover-bg-color: #409eff10;
|
||||||
|
--v3-sidebar-menu-text-color: #cfd3dc;
|
||||||
|
--v3-sidebar-menu-active-text-color: #ffffff;
|
||||||
|
/* TagsView 组件 */
|
||||||
|
--v3-tagsview-height: 34px;
|
||||||
|
--v3-tagsview-text-color: var(--el-text-color-regular);
|
||||||
|
--v3-tagsview-tag-active-text-color: #ffffff;
|
||||||
|
--v3-tagsview-tag-bg-color: var(--el-bg-color);
|
||||||
|
--v3-tagsview-tag-active-bg-color: var(--el-color-primary);
|
||||||
|
--v3-tagsview-tag-border-radius: 2px;
|
||||||
|
--v3-tagsview-tag-border-color: var(--el-border-color-lighter);
|
||||||
|
--v3-tagsview-tag-active-border-color: var(--el-color-primary);
|
||||||
|
--v3-tagsview-tag-icon-hover-bg-color: #00000030;
|
||||||
|
--v3-tagsview-tag-icon-hover-color: #ffffff;
|
||||||
|
--v3-tagsview-contextmenu-text-color: var(--el-text-color-regular);
|
||||||
|
--v3-tagsview-contextmenu-hover-text-color: var(--el-text-color-primary);
|
||||||
|
--v3-tagsview-contextmenu-bg-color: var(--el-bg-color-overlay);
|
||||||
|
--v3-tagsview-contextmenu-hover-bg-color: var(--el-fill-color);
|
||||||
|
--v3-tagsview-contextmenu-box-shadow: var(--el-box-shadow);
|
||||||
|
/* Hamburger 组件 */
|
||||||
|
--v3-hamburger-text-color: var(--el-text-color-primary);
|
||||||
|
/* RightPanel 组件 */
|
||||||
|
--v3-rightpanel-button-bg-color: #001428;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区放大时,将不需要的组件隐藏 */
|
||||||
|
body.content-large {
|
||||||
|
/* Header 区域 = TagsView 组件 */
|
||||||
|
--v3-header-height: var(--v3-tagsview-height);
|
||||||
|
/* NavigationBar 组件 */
|
||||||
|
--v3-navigationbar-height: 0px;
|
||||||
|
/* Sidebar 组件 */
|
||||||
|
--v3-sidebar-width: 0px;
|
||||||
|
--v3-sidebar-hide-width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区全屏时,将不需要的组件隐藏 */
|
||||||
|
body.content-full {
|
||||||
|
/* Header 区域 */
|
||||||
|
--v3-header-height: 0px;
|
||||||
|
/* NavigationBar 组件 */
|
||||||
|
--v3-navigationbar-height: 0px;
|
||||||
|
/* Sidebar 组件 */
|
||||||
|
--v3-sidebar-width: 0px;
|
||||||
|
--v3-sidebar-hide-width: 0px;
|
||||||
|
/* TagsView 组件 */
|
||||||
|
--v3-tagsview-height: 0px;
|
||||||
|
}
|
20
src/common/assets/styles/view-transition.scss
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// 控制切换主题时的动画效果(只在较新的浏览器上生效,例如 Chrome 111+)
|
||||||
|
|
||||||
|
::view-transition-old(root) {
|
||||||
|
animation: none;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: 0.5s ease-in clip-animation;
|
||||||
|
mix-blend-mode: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes clip-animation {
|
||||||
|
from {
|
||||||
|
clip-path: circle(0px at var(--v3-theme-x) var(--v3-theme-y));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
clip-path: circle(var(--v3-theme-r) at var(--v3-theme-x) var(--v3-theme-y));
|
||||||
|
}
|
||||||
|
}
|
97
src/common/assets/styles/vxe-table.css
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* @description 所有主题模式下的 Vxe Table CSS 变量
|
||||||
|
* @description 用 Element Plus 的 CSS 变量来覆写 Vxe Table 的 CSS 变量,目的是使 Vxe Table 支持多主题模式且样式统一
|
||||||
|
* @description 在此查阅所有可自定义的变量:https://github.com/x-extends/vxe-table/blob/master/styles/css-variable.scss
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* color */
|
||||||
|
--vxe-font-color: var(--el-text-color-regular);
|
||||||
|
--vxe-primary-color: var(--el-color-primary);
|
||||||
|
--vxe-success-color: var(--el-color-success);
|
||||||
|
--vxe-info-color: var(--el-color-info);
|
||||||
|
--vxe-warning-color: var(--el-color-warning);
|
||||||
|
--vxe-danger-color: var(--el-color-danger);
|
||||||
|
|
||||||
|
--vxe-font-lighten-color: var(--el-text-color-primary);
|
||||||
|
--vxe-primary-lighten-color: var(--el-color-primary-light-3);
|
||||||
|
--vxe-success-lighten-color: var(--el-color-success-light-3);
|
||||||
|
--vxe-info-lighten-color: var(--el-color-info-light-3);
|
||||||
|
--vxe-warning-lighten-color: var(--el-color-warning-light-3);
|
||||||
|
--vxe-danger-lighten-color: var(--el-color-danger-light-3);
|
||||||
|
|
||||||
|
--vxe-font-darken-color: var(--el-text-color-secondary);
|
||||||
|
--vxe-primary-darken-color: var(--el-color-primary-dark-2);
|
||||||
|
--vxe-success-darken-color: var(--el-color-success-dark-2);
|
||||||
|
--vxe-info-darken-color: var(--el-color-info-dark-2);
|
||||||
|
--vxe-warning-darken-color: var(--el-color-warning-dark-2);
|
||||||
|
--vxe-danger-darken-color: var(--el-color-danger-dark-2);
|
||||||
|
|
||||||
|
--vxe-font-disabled-color: var(--el-text-color-disabled);
|
||||||
|
--vxe-primary-disabled-color: var(--el-color-primary-light-5);
|
||||||
|
--vxe-success-disabled-color: var(--el-color-success-light-5);
|
||||||
|
--vxe-info-disabled-color: var(--el-color-info-light-5);
|
||||||
|
--vxe-warning-disabled-color: var(--el-color-warning-light-5);
|
||||||
|
--vxe-danger-disabled-color: var(--el-color-danger-light-5);
|
||||||
|
|
||||||
|
/* input/radio/checkbox */
|
||||||
|
--vxe-input-border-color: var(--el-border-color);
|
||||||
|
--vxe-input-disabled-color: var(--el-text-color-disabled);
|
||||||
|
--vxe-input-disabled-background-color: var(--el-fill-color-light);
|
||||||
|
--vxe-input-placeholder-color: var(--el-text-color-placeholder);
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
--vxe-table-popup-border-color: var(--el-border-color);
|
||||||
|
|
||||||
|
/* table */
|
||||||
|
--vxe-table-header-font-color: var(--el-text-color-regular);
|
||||||
|
--vxe-table-footer-font-color: var(--el-text-color-regular);
|
||||||
|
--vxe-table-border-color: var(--el-border-color-lighter);
|
||||||
|
--vxe-table-header-background-color: var(--el-bg-color);
|
||||||
|
--vxe-table-body-background-color: var(--el-bg-color);
|
||||||
|
--vxe-table-footer-background-color: var(--el-bg-color);
|
||||||
|
|
||||||
|
--vxe-table-row-hover-background-color: var(--el-fill-color-light);
|
||||||
|
--vxe-table-row-current-background-color: var(--el-fill-color-light);
|
||||||
|
--vxe-table-row-hover-current-background-color: var(--el-fill-color-light);
|
||||||
|
|
||||||
|
--vxe-table-checkbox-range-background-color: var(--el-fill-color-light);
|
||||||
|
|
||||||
|
/* menu */
|
||||||
|
--vxe-table-menu-background-color: var(--el-bg-color-overlay);
|
||||||
|
|
||||||
|
/* loading */
|
||||||
|
--vxe-loading-color: var(--el-color-primary);
|
||||||
|
--vxe-loading-background-color: var(--el-mask-color);
|
||||||
|
|
||||||
|
/* validate */
|
||||||
|
--vxe-table-validate-error-color: var(--el-color-danger);
|
||||||
|
|
||||||
|
/* toolbar */
|
||||||
|
--vxe-toolbar-background-color: var(--el-bg-color);
|
||||||
|
--vxe-toolbar-custom-active-background-color: var(--el-bg-color-overlay);
|
||||||
|
--vxe-toolbar-panel-background-color: var(--el-bg-color-overlay);
|
||||||
|
|
||||||
|
/* pager */
|
||||||
|
--vxe-pager-background-color: var(--el-bg-color);
|
||||||
|
|
||||||
|
/* modal */
|
||||||
|
--vxe-modal-header-background-color: var(--el-bg-color);
|
||||||
|
--vxe-modal-body-background-color: var(--el-bg-color);
|
||||||
|
--vxe-modal-border-color: var(--el-border-color);
|
||||||
|
|
||||||
|
/* button */
|
||||||
|
--vxe-button-default-background-color: var(--el-bg-color-overlay);
|
||||||
|
|
||||||
|
/* input */
|
||||||
|
--vxe-input-background-color: var(--el-fill-color-blank);
|
||||||
|
--vxe-input-panel-background-color: var(--el-fill-color-blank);
|
||||||
|
|
||||||
|
/* form */
|
||||||
|
--vxe-form-background-color: var(--el-bg-color);
|
||||||
|
--vxe-form-validate-error-color: var(--el-color-danger);
|
||||||
|
|
||||||
|
/* select */
|
||||||
|
--vxe-select-option-hover-background-color: var(--el-bg-color-overlay);
|
||||||
|
--vxe-select-panel-background-color: var(--el-bg-color);
|
||||||
|
}
|
@ -1,26 +1,26 @@
|
|||||||
/** 自定义 Vxe Table 样式 */
|
// 自定义 Vxe Table 样式
|
||||||
|
|
||||||
.vxe-grid {
|
.vxe-grid {
|
||||||
// 表单
|
// 表单
|
||||||
&--form-wrapper {
|
&--form-wrapper {
|
||||||
.vxe-form {
|
.vxe-form {
|
||||||
padding: 10px 20px !important;
|
padding: 10px 20px;
|
||||||
margin-bottom: 20px !important;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 工具栏
|
// 工具栏
|
||||||
&--toolbar-wrapper {
|
&--toolbar-wrapper {
|
||||||
.vxe-toolbar {
|
.vxe-toolbar {
|
||||||
padding: 20px !important;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
&--pager-wrapper {
|
&--pager-wrapper {
|
||||||
.vxe-pager {
|
.vxe-pager {
|
||||||
height: 70px !important;
|
height: 70px;
|
||||||
padding: 0 20px !important;
|
padding: 0 20px;
|
||||||
&--wrapper {
|
&--wrapper {
|
||||||
// 参考 Bootstrap 的响应式设计 WIDTH = 768
|
// 参考 Bootstrap 的响应式设计 WIDTH = 768
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
@ -29,7 +29,7 @@
|
|||||||
.vxe-pager--jump,
|
.vxe-pager--jump,
|
||||||
.vxe-pager--jump-prev,
|
.vxe-pager--jump-prev,
|
||||||
.vxe-pager--jump-next {
|
.vxe-pager--jump-next {
|
||||||
display: none !important;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,16 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { type ListItem } from "./data"
|
import type { NotifyItem } from "./type"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
list: ListItem[]
|
data: NotifyItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-empty v-if="props.list.length === 0" />
|
<el-empty v-if="props.data.length === 0" />
|
||||||
<el-card v-else v-for="(item, index) in props.list" :key="index" shadow="never" class="card-container">
|
<el-card v-else v-for="(item, index) in props.data" :key="index" shadow="never" class="card-container">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div>
|
<div>
|
||||||
@ -18,10 +18,12 @@ const props = defineProps<Props>()
|
|||||||
<span class="card-title">{{ item.title }}</span>
|
<span class="card-title">{{ item.title }}</span>
|
||||||
<el-tag v-if="item.extra" :type="item.status" effect="plain" size="small">{{ item.extra }}</el-tag>
|
<el-tag v-if="item.extra" :type="item.status" effect="plain" size="small">{{ item.extra }}</el-tag>
|
||||||
</span>
|
</span>
|
||||||
<div class="card-time">{{ item.datetime }}</div>
|
<div class="card-time">
|
||||||
|
{{ item.datetime }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.avatar" class="card-avatar">
|
<div v-if="item.avatar" class="card-avatar">
|
||||||
<img :src="item.avatar" width="34" />
|
<img :src="item.avatar" width="34">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -44,7 +46,7 @@ const props = defineProps<Props>()
|
|||||||
}
|
}
|
||||||
.card-time {
|
.card-time {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: grey;
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
.card-avatar {
|
.card-avatar {
|
||||||
display: flex;
|
display: flex;
|
@ -1,29 +1,21 @@
|
|||||||
export interface ListItem {
|
import type { NotifyItem } from "./type"
|
||||||
avatar?: string
|
|
||||||
title: string
|
|
||||||
datetime?: string
|
|
||||||
description?: string
|
|
||||||
status?: "" | "success" | "info" | "warning" | "danger"
|
|
||||||
extra?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const notifyData: ListItem[] = [
|
export const notifyData: NotifyItem[] = [
|
||||||
{
|
{
|
||||||
avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
|
avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
|
||||||
title: "V3 Admin Vite 上线啦",
|
title: "V3 Admin Vite 上线啦",
|
||||||
datetime: "半年前",
|
datetime: "两年前",
|
||||||
description:
|
description: "一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术"
|
||||||
"一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
|
avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
|
||||||
title: "V3 Admin 上线啦",
|
title: "V3 Admin 上线啦",
|
||||||
datetime: "一年前",
|
datetime: "三年前",
|
||||||
description: "一个中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus 和 Pinia"
|
description: "一个中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus 和 Pinia"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const messageData: ListItem[] = [
|
export const messageData: NotifyItem[] = [
|
||||||
{
|
{
|
||||||
avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
|
avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
|
||||||
title: "来自楚门的世界",
|
title: "来自楚门的世界",
|
||||||
@ -44,7 +36,7 @@ export const messageData: ListItem[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const todoData: ListItem[] = [
|
export const todoData: NotifyItem[] = [
|
||||||
{
|
{
|
||||||
title: "任务名称",
|
title: "任务名称",
|
||||||
description: "这家伙很懒,什么都没留下",
|
description: "这家伙很懒,什么都没留下",
|
||||||
@ -55,7 +47,7 @@ export const todoData: ListItem[] = [
|
|||||||
title: "任务名称",
|
title: "任务名称",
|
||||||
description: "这家伙很懒,什么都没留下",
|
description: "这家伙很懒,什么都没留下",
|
||||||
extra: "进行中",
|
extra: "进行中",
|
||||||
status: ""
|
status: "primary"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "任务名称",
|
title: "任务名称",
|
@ -1,28 +1,29 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from "vue"
|
import type { NotifyItem } from "./type"
|
||||||
import { ElMessage } from "element-plus"
|
|
||||||
import { Bell } from "@element-plus/icons-vue"
|
import { Bell } from "@element-plus/icons-vue"
|
||||||
import NotifyList from "./NotifyList.vue"
|
import { messageData, notifyData, todoData } from "./data"
|
||||||
import { type ListItem, notifyData, messageData, todoData } from "./data"
|
import List from "./List.vue"
|
||||||
|
|
||||||
type TabName = "通知" | "消息" | "待办"
|
type TabName = "通知" | "消息" | "待办"
|
||||||
|
|
||||||
interface DataItem {
|
interface DataItem {
|
||||||
name: TabName
|
name: TabName
|
||||||
type: "primary" | "success" | "warning" | "danger" | "info"
|
type: "primary" | "success" | "warning" | "danger" | "info"
|
||||||
list: ListItem[]
|
list: NotifyItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 角标当前值 */
|
/** 角标当前值 */
|
||||||
const badgeValue = computed(() => {
|
const badgeValue = computed(() => data.value.reduce((sum, item) => sum + item.list.length, 0))
|
||||||
return data.value.reduce((sum, item) => sum + item.list.length, 0)
|
|
||||||
})
|
|
||||||
/** 角标最大值 */
|
/** 角标最大值 */
|
||||||
const badgeMax = 99
|
const badgeMax = 99
|
||||||
|
|
||||||
/** 面板宽度 */
|
/** 面板宽度 */
|
||||||
const popoverWidth = 350
|
const popoverWidth = 350
|
||||||
|
|
||||||
/** 当前 Tab */
|
/** 当前 Tab */
|
||||||
const activeName = ref<TabName>("通知")
|
const activeName = ref<TabName>("通知")
|
||||||
|
|
||||||
/** 所有数据 */
|
/** 所有数据 */
|
||||||
const data = ref<DataItem[]>([
|
const data = ref<DataItem[]>([
|
||||||
// 通知数据
|
// 通知数据
|
||||||
@ -45,7 +46,7 @@ const data = ref<DataItem[]>([
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
const handleHistory = () => {
|
function handleHistory() {
|
||||||
ElMessage.success(`跳转到${activeName.value}历史页面`)
|
ElMessage.success(`跳转到${activeName.value}历史页面`)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -64,18 +65,20 @@ const handleHistory = () => {
|
|||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<el-tabs v-model="activeName" class="demo-tabs" stretch>
|
<el-tabs v-model="activeName" class="demo-tabs" stretch>
|
||||||
<el-tab-pane v-for="(item, index) in data" :name="item.name" :key="index">
|
<el-tab-pane v-for="(item, index) in data" :key="index" :name="item.name">
|
||||||
<template #label>
|
<template #label>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
<el-badge :value="item.list.length" :max="badgeMax" :type="item.type" />
|
<el-badge :value="item.list.length" :max="badgeMax" :type="item.type" />
|
||||||
</template>
|
</template>
|
||||||
<el-scrollbar height="400px">
|
<el-scrollbar height="400px">
|
||||||
<NotifyList :list="item.list" />
|
<List :data="item.list" />
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
<div class="notify-history">
|
<div class="notify-history">
|
||||||
<el-button link @click="handleHistory">查看{{ activeName }}历史</el-button>
|
<el-button link @click="handleHistory">
|
||||||
|
查看{{ activeName }}历史
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
@ -83,10 +86,6 @@ const handleHistory = () => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.notify {
|
|
||||||
margin-right: 10px;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
}
|
|
||||||
.notify-history {
|
.notify-history {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
8
src/common/components/Notify/type.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface NotifyItem {
|
||||||
|
avatar?: string
|
||||||
|
title: string
|
||||||
|
datetime?: string
|
||||||
|
description?: string
|
||||||
|
status?: "primary" | "success" | "info" | "warning" | "danger"
|
||||||
|
extra?: string
|
||||||
|
}
|
111
src/common/components/Screenfull/index.vue
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import screenfull from "screenfull"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** 全屏的元素,默认是 html */
|
||||||
|
element?: string
|
||||||
|
/** 打开全屏提示语 */
|
||||||
|
openTips?: string
|
||||||
|
/** 关闭全屏提示语 */
|
||||||
|
exitTips?: string
|
||||||
|
/** 是否只针对内容区 */
|
||||||
|
content?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
element: "html",
|
||||||
|
openTips: "全屏",
|
||||||
|
exitTips: "退出全屏",
|
||||||
|
content: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const CONTENT_LARGE = "content-large"
|
||||||
|
|
||||||
|
const CONTENT_FULL = "content-full"
|
||||||
|
|
||||||
|
const classList = document.body.classList
|
||||||
|
|
||||||
|
// #region 全屏
|
||||||
|
const isEnabled = screenfull.isEnabled
|
||||||
|
const isFullscreen = ref<boolean>(false)
|
||||||
|
const fullscreenTips = computed(() => (isFullscreen.value ? props.exitTips : props.openTips))
|
||||||
|
const fullscreenSvgName = computed(() => (isFullscreen.value ? "fullscreen-exit" : "fullscreen"))
|
||||||
|
|
||||||
|
function handleFullscreenClick() {
|
||||||
|
const dom = document.querySelector(props.element) || undefined
|
||||||
|
isEnabled ? screenfull.toggle(dom) : ElMessage.warning("您的浏览器无法工作")
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFullscreenChange() {
|
||||||
|
isFullscreen.value = screenfull.isFullscreen
|
||||||
|
// 退出全屏时清除相关的 class
|
||||||
|
isFullscreen.value || classList.remove(CONTENT_LARGE, CONTENT_FULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect((onCleanup) => {
|
||||||
|
if (isEnabled) {
|
||||||
|
// 挂载组件时自动执行
|
||||||
|
screenfull.on("change", handleFullscreenChange)
|
||||||
|
// 卸载组件时自动执行
|
||||||
|
onCleanup(() => {
|
||||||
|
screenfull.off("change", handleFullscreenChange)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region 内容区
|
||||||
|
const isContentLarge = ref<boolean>(false)
|
||||||
|
const contentLargeTips = computed(() => (isContentLarge.value ? "内容区复原" : "内容区放大"))
|
||||||
|
const contentLargeSvgName = computed(() => (isContentLarge.value ? "fullscreen-exit" : "fullscreen"))
|
||||||
|
|
||||||
|
function handleContentLargeClick() {
|
||||||
|
isContentLarge.value = !isContentLarge.value
|
||||||
|
// 内容区放大时,将不需要的组件隐藏
|
||||||
|
classList.toggle(CONTENT_LARGE, isContentLarge.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleContentFullClick() {
|
||||||
|
// 取消内容区放大
|
||||||
|
isContentLarge.value && handleContentLargeClick()
|
||||||
|
// 内容区全屏时,将不需要的组件隐藏
|
||||||
|
classList.add(CONTENT_FULL)
|
||||||
|
// 开启全屏
|
||||||
|
handleFullscreenClick()
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 全屏 -->
|
||||||
|
<el-tooltip v-if="!props.content" effect="dark" :content="fullscreenTips" placement="bottom">
|
||||||
|
<SvgIcon :name="fullscreenSvgName" @click="handleFullscreenClick" class="svg-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
<!-- 内容区 -->
|
||||||
|
<el-dropdown v-else :disabled="isFullscreen">
|
||||||
|
<SvgIcon :name="contentLargeSvgName" class="svg-icon" />
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<!-- 内容区放大 -->
|
||||||
|
<el-dropdown-item @click="handleContentLargeClick">
|
||||||
|
{{ contentLargeTips }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<!-- 内容区全屏 -->
|
||||||
|
<el-dropdown-item @click="handleContentFullClick">
|
||||||
|
内容区全屏
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.svg-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
54
src/common/components/SearchMenu/Footer.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useDevice } from "@@/composables/useDevice"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const { isMobile } = useDevice()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="search-footer">
|
||||||
|
<template v-if="!isMobile">
|
||||||
|
<span class="search-footer-item">
|
||||||
|
<SvgIcon name="keyboard-enter" class="svg-icon" />
|
||||||
|
<span>确认</span>
|
||||||
|
</span>
|
||||||
|
<span class="search-footer-item">
|
||||||
|
<SvgIcon name="keyboard-up" class="svg-icon" />
|
||||||
|
<SvgIcon name="keyboard-down" class="svg-icon" />
|
||||||
|
<span>切换</span>
|
||||||
|
</span>
|
||||||
|
<span class="search-footer-item">
|
||||||
|
<SvgIcon name="keyboard-esc" class="svg-icon" />
|
||||||
|
<span>关闭</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span class="search-footer-total">共 {{ props.total }} 项</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-footer {
|
||||||
|
display: flex;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
&-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
.svg-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
padding: 2px;
|
||||||
|
font-size: 20px;
|
||||||
|
background-color: var(--el-fill-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-total {
|
||||||
|
margin: 0 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
193
src/common/components/SearchMenu/Modal.vue
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ElScrollbar } from "element-plus"
|
||||||
|
import type { RouteRecordNameGeneric, RouteRecordRaw } from "vue-router"
|
||||||
|
import { usePermissionStore } from "@/pinia/stores/permission"
|
||||||
|
import { useDevice } from "@@/composables/useDevice"
|
||||||
|
import { isExternal } from "@@/utils/validate"
|
||||||
|
import { cloneDeep, debounce } from "lodash-es"
|
||||||
|
import Footer from "./Footer.vue"
|
||||||
|
import Result from "./Result.vue"
|
||||||
|
|
||||||
|
/** 控制 modal 显隐 */
|
||||||
|
const modelValue = defineModel<boolean>({ required: true })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { isMobile } = useDevice()
|
||||||
|
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const scrollbarRef = ref<InstanceType<typeof ElScrollbar> | null>(null)
|
||||||
|
const resultRef = ref<InstanceType<typeof Result> | null>(null)
|
||||||
|
|
||||||
|
const keyword = ref<string>("")
|
||||||
|
const result = shallowRef<RouteRecordRaw[]>([])
|
||||||
|
const activeRouteName = ref<RouteRecordNameGeneric | undefined>(undefined)
|
||||||
|
/** 是否按下了上键或下键(用于解决和 mouseenter 事件的冲突) */
|
||||||
|
const isPressUpOrDown = ref<boolean>(false)
|
||||||
|
|
||||||
|
/** 控制搜索对话框宽度 */
|
||||||
|
const modalWidth = computed(() => (isMobile.value ? "80vw" : "40vw"))
|
||||||
|
/** 树形菜单 */
|
||||||
|
const menus = computed(() => cloneDeep(usePermissionStore().routes))
|
||||||
|
|
||||||
|
/** 搜索(防抖) */
|
||||||
|
const handleSearch = debounce(() => {
|
||||||
|
const flatMenus = flatTree(menus.value)
|
||||||
|
const _keywords = keyword.value.toLocaleLowerCase().trim()
|
||||||
|
result.value = flatMenus.filter(menu => keyword.value ? menu.meta?.title?.toLocaleLowerCase().includes(_keywords) : false)
|
||||||
|
// 默认选中搜索结果的第一项
|
||||||
|
const length = result.value?.length
|
||||||
|
activeRouteName.value = length > 0 ? result.value[0].name : undefined
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
/** 将树形菜单扁平化为一维数组,用于菜单搜索 */
|
||||||
|
function flatTree(arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) {
|
||||||
|
arr.forEach((item) => {
|
||||||
|
result.push(item)
|
||||||
|
item.children && flatTree(item.children, result)
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 关闭搜索对话框 */
|
||||||
|
function handleClose() {
|
||||||
|
modelValue.value = false
|
||||||
|
// 延时处理防止用户看到重置数据的操作
|
||||||
|
setTimeout(() => {
|
||||||
|
keyword.value = ""
|
||||||
|
result.value = []
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据下标位置进行滚动 */
|
||||||
|
function scrollTo(index: number) {
|
||||||
|
if (!resultRef.value) return
|
||||||
|
const scrollTop = resultRef.value.getScrollTop(index)
|
||||||
|
// 手动控制 el-scrollbar 滚动条滚动,设置滚动条到顶部的距离
|
||||||
|
scrollbarRef.value?.setScrollTop(scrollTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 键盘上键 */
|
||||||
|
function handleUp() {
|
||||||
|
isPressUpOrDown.value = true
|
||||||
|
const { length } = result.value
|
||||||
|
if (length === 0) return
|
||||||
|
// 获取该 name 在菜单中第一次出现的位置
|
||||||
|
const index = result.value.findIndex(item => item.name === activeRouteName.value)
|
||||||
|
// 如果已处在顶部
|
||||||
|
if (index === 0) {
|
||||||
|
const bottomName = result.value[length - 1].name
|
||||||
|
// 如果顶部和底部的 bottomName 相同,且长度大于 1,就再跳一个位置(可解决遇到首尾两个相同 name 导致的上键不能生效的问题)
|
||||||
|
if (activeRouteName.value === bottomName && length > 1) {
|
||||||
|
activeRouteName.value = result.value[length - 2].name
|
||||||
|
scrollTo(length - 2)
|
||||||
|
} else {
|
||||||
|
// 跳转到底部
|
||||||
|
activeRouteName.value = bottomName
|
||||||
|
scrollTo(length - 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeRouteName.value = result.value[index - 1].name
|
||||||
|
scrollTo(index - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 键盘下键 */
|
||||||
|
function handleDown() {
|
||||||
|
isPressUpOrDown.value = true
|
||||||
|
const { length } = result.value
|
||||||
|
if (length === 0) return
|
||||||
|
// 获取该 name 在菜单中最后一次出现的位置(可解决遇到连续两个相同 name 导致的下键不能生效的问题)
|
||||||
|
const index = result.value.map(item => item.name).lastIndexOf(activeRouteName.value)
|
||||||
|
// 如果已处在底部
|
||||||
|
if (index === length - 1) {
|
||||||
|
const topName = result.value[0].name
|
||||||
|
// 如果底部和顶部的 topName 相同,且长度大于 1,就再跳一个位置(可解决遇到首尾两个相同 name 导致的下键不能生效的问题)
|
||||||
|
if (activeRouteName.value === topName && length > 1) {
|
||||||
|
activeRouteName.value = result.value[1].name
|
||||||
|
scrollTo(1)
|
||||||
|
} else {
|
||||||
|
// 跳转到顶部
|
||||||
|
activeRouteName.value = topName
|
||||||
|
scrollTo(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeRouteName.value = result.value[index + 1].name
|
||||||
|
scrollTo(index + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 键盘回车键 */
|
||||||
|
function handleEnter() {
|
||||||
|
const { length } = result.value
|
||||||
|
if (length === 0) return
|
||||||
|
const name = activeRouteName.value
|
||||||
|
const path = result.value.find(item => item.name === name)?.path
|
||||||
|
if (path && isExternal(path)) return window.open(path, "_blank", "noopener, noreferrer")
|
||||||
|
if (!name) return ElMessage.warning("无法通过搜索进入该菜单,请为对应的路由设置唯一的 Name")
|
||||||
|
try {
|
||||||
|
router.push({ name })
|
||||||
|
} catch {
|
||||||
|
return ElMessage.warning("该菜单有必填的动态参数,无法通过搜索进入")
|
||||||
|
}
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 释放上键或下键 */
|
||||||
|
function handleReleaseUpOrDown() {
|
||||||
|
isPressUpOrDown.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="modelValue"
|
||||||
|
:before-close="handleClose"
|
||||||
|
:width="modalWidth"
|
||||||
|
top="5vh"
|
||||||
|
class="search-modal__private"
|
||||||
|
append-to-body
|
||||||
|
@opened="inputRef?.focus()"
|
||||||
|
@closed="inputRef?.blur()"
|
||||||
|
@keydown.up="handleUp"
|
||||||
|
@keydown.down="handleDown"
|
||||||
|
@keydown.enter="handleEnter"
|
||||||
|
@keyup.up.down="handleReleaseUpOrDown"
|
||||||
|
>
|
||||||
|
<el-input ref="inputRef" v-model="keyword" placeholder="搜索菜单" size="large" clearable @input="handleSearch">
|
||||||
|
<template #prefix>
|
||||||
|
<SvgIcon name="search" class="svg-icon" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-empty v-if="result.length === 0" description="暂无搜索结果" :image-size="100" />
|
||||||
|
<template v-else>
|
||||||
|
<p>搜索结果</p>
|
||||||
|
<el-scrollbar ref="scrollbarRef" max-height="40vh" always>
|
||||||
|
<Result
|
||||||
|
ref="resultRef"
|
||||||
|
v-model="activeRouteName"
|
||||||
|
:data="result"
|
||||||
|
:is-press-up-or-down="isPressUpOrDown"
|
||||||
|
@click="handleEnter"
|
||||||
|
/>
|
||||||
|
</el-scrollbar>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<Footer :total="result.length" />
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.search-modal__private {
|
||||||
|
.svg-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.el-dialog__header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.el-dialog__footer {
|
||||||
|
border-top: 1px solid var(--el-border-color);
|
||||||
|
padding-top: var(--el-dialog-padding-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
115
src/common/components/SearchMenu/Result.vue
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { RouteRecordNameGeneric, RouteRecordRaw } from "vue-router"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: RouteRecordRaw[]
|
||||||
|
isPressUpOrDown: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
/** 选中的菜单 */
|
||||||
|
const modelValue = defineModel<RouteRecordNameGeneric | undefined>({ required: true })
|
||||||
|
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
|
||||||
|
const scrollbarHeight = ref<number>(0)
|
||||||
|
|
||||||
|
/** 菜单的样式 */
|
||||||
|
function itemStyle(item: RouteRecordRaw) {
|
||||||
|
const flag = item.name === modelValue.value
|
||||||
|
return {
|
||||||
|
background: flag ? "var(--el-color-primary)" : "",
|
||||||
|
color: flag ? "#ffffff" : ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 鼠标移入 */
|
||||||
|
function handleMouseenter(item: RouteRecordRaw) {
|
||||||
|
// 如果上键或下键与 mouseenter 事件同时生效,则以上下键为准,不执行该函数的赋值逻辑
|
||||||
|
if (props.isPressUpOrDown) return
|
||||||
|
modelValue.value = item.name
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算滚动可视区高度 */
|
||||||
|
function getScrollbarHeight() {
|
||||||
|
// el-scrollbar max-height="40vh"
|
||||||
|
scrollbarHeight.value = Number((window.innerHeight * 0.4).toFixed(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据下标计算到顶部的距离 */
|
||||||
|
function getScrollTop(index: number) {
|
||||||
|
const currentInstance = instance?.proxy?.$refs[`resultItemRef${index}`] as HTMLDivElement[]
|
||||||
|
if (!currentInstance) return 0
|
||||||
|
const currentRef = currentInstance[0]
|
||||||
|
// 128 = 两个 result-item (56 + 56 = 112)高度与上下 margin(8 + 8 = 16)大小之和
|
||||||
|
const scrollTop = currentRef.offsetTop + 128
|
||||||
|
return scrollTop > scrollbarHeight.value ? scrollTop - scrollbarHeight.value : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组件挂载前添加窗口大小变化事件监听器
|
||||||
|
onBeforeMount(() => {
|
||||||
|
window.addEventListener("resize", getScrollbarHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在组件挂载时立即计算滚动可视区高度
|
||||||
|
onMounted(() => {
|
||||||
|
getScrollbarHeight()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在组件卸载前移除窗口大小变化事件监听器
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener("resize", getScrollbarHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({ getScrollTop })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 外层 div 不能删除,是用来接收父组件 click 事件的 -->
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in props.data"
|
||||||
|
:key="index"
|
||||||
|
:ref="`resultItemRef${index}`"
|
||||||
|
class="result-item"
|
||||||
|
:style="itemStyle(item)"
|
||||||
|
@mouseenter="handleMouseenter(item)"
|
||||||
|
>
|
||||||
|
<SvgIcon v-if="item.meta?.svgIcon" :name="item.meta.svgIcon" class="svg-icon" />
|
||||||
|
<component v-else-if="item.meta?.elIcon" :is="item.meta.elIcon" class="el-icon" />
|
||||||
|
<span class="result-item-title">
|
||||||
|
{{ item.meta?.title }}
|
||||||
|
</span>
|
||||||
|
<SvgIcon v-if="modelValue && modelValue === item.name" name="keyboard-enter" class="svg-icon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@@/assets/styles/mixins.scss";
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 56px;
|
||||||
|
padding: 0 15px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
.svg-icon {
|
||||||
|
min-width: 1em;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.el-icon {
|
||||||
|
width: 1em;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
&-title {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 12px;
|
||||||
|
@extend %ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
29
src/common/components/SearchMenu/index.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import Modal from "./Modal.vue"
|
||||||
|
|
||||||
|
/** 控制 modal 显隐 */
|
||||||
|
const visible = ref<boolean>(false)
|
||||||
|
|
||||||
|
/** 打开 modal */
|
||||||
|
function handleOpen() {
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-tooltip effect="dark" content="搜索菜单" placement="bottom">
|
||||||
|
<SvgIcon name="search" @click="handleOpen" class="svg-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
<Modal v-model="visible" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.svg-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,12 +1,12 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useTheme } from "@/hooks/useTheme"
|
import { useTheme } from "@@/composables/useTheme"
|
||||||
import { MagicStick } from "@element-plus/icons-vue"
|
import { MagicStick } from "@element-plus/icons-vue"
|
||||||
|
|
||||||
const { themeList, activeThemeName, setTheme } = useTheme()
|
const { themeList, activeThemeName, setTheme } = useTheme()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dropdown trigger="click" @command="setTheme">
|
<el-dropdown trigger="click">
|
||||||
<div>
|
<div>
|
||||||
<el-tooltip effect="dark" content="主题模式" placement="bottom">
|
<el-tooltip effect="dark" content="主题模式" placement="bottom">
|
||||||
<el-icon :size="20">
|
<el-icon :size="20">
|
||||||
@ -20,7 +20,7 @@ const { themeList, activeThemeName, setTheme } = useTheme()
|
|||||||
v-for="(theme, index) in themeList"
|
v-for="(theme, index) in themeList"
|
||||||
:key="index"
|
:key="index"
|
||||||
:disabled="activeThemeName === theme.name"
|
:disabled="activeThemeName === theme.name"
|
||||||
:command="theme.name"
|
@click="(e: MouseEvent) => setTheme(e, theme.name)"
|
||||||
>
|
>
|
||||||
<span>{{ theme.title }}</span>
|
<span>{{ theme.title }}</span>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
12
src/common/composables/useDevice.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useAppStore } from "@/pinia/stores/app"
|
||||||
|
import { DeviceEnum } from "@@/constants/app-key"
|
||||||
|
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const isMobile = computed(() => appStore.device === DeviceEnum.Mobile)
|
||||||
|
const isDesktop = computed(() => appStore.device === DeviceEnum.Desktop)
|
||||||
|
|
||||||
|
/** 设备类型 Composable */
|
||||||
|
export function useDevice() {
|
||||||
|
return { isMobile, isDesktop }
|
||||||
|
}
|
@ -1,5 +1,3 @@
|
|||||||
import { ref, onMounted } from "vue"
|
|
||||||
|
|
||||||
type OptionValue = string | number
|
type OptionValue = string | number
|
||||||
|
|
||||||
/** Select 需要的数据格式 */
|
/** Select 需要的数据格式 */
|
||||||
@ -10,17 +8,14 @@ interface SelectOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 接口响应格式 */
|
/** 接口响应格式 */
|
||||||
interface ApiData {
|
type ApiData = ApiResponseData<SelectOption[]>
|
||||||
code: number
|
|
||||||
data: SelectOption[]
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 入参格式,暂时只需要传递 api 函数即可 */
|
/** 入参格式,暂时只需要传递 api 函数即可 */
|
||||||
interface FetchSelectProps {
|
interface FetchSelectProps {
|
||||||
api: () => Promise<ApiData>
|
api: () => Promise<ApiData>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 下拉选择器 Composable */
|
||||||
export function useFetchSelect(props: FetchSelectProps) {
|
export function useFetchSelect(props: FetchSelectProps) {
|
||||||
const { api } = props
|
const { api } = props
|
||||||
|
|
||||||
@ -28,15 +23,13 @@ export function useFetchSelect(props: FetchSelectProps) {
|
|||||||
const options = ref<SelectOption[]>([])
|
const options = ref<SelectOption[]>([])
|
||||||
const value = ref<OptionValue>("")
|
const value = ref<OptionValue>("")
|
||||||
|
|
||||||
/** 调用接口获取数据 */
|
// 调用接口获取数据
|
||||||
const loadData = () => {
|
const loadData = () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
options.value = []
|
options.value = []
|
||||||
api()
|
api().then((res) => {
|
||||||
.then((res) => {
|
|
||||||
options.value = res.data
|
options.value = res.data
|
||||||
})
|
}).finally(() => {
|
||||||
.finally(() => {
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -45,9 +38,5 @@ export function useFetchSelect(props: FetchSelectProps) {
|
|||||||
loadData()
|
loadData()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return { loading, options, value }
|
||||||
loading,
|
|
||||||
options,
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,22 +1,24 @@
|
|||||||
import { type LoadingOptions, ElLoading } from "element-plus"
|
import type { LoadingOptions } from "element-plus"
|
||||||
|
|
||||||
const defaultOptions = {
|
interface UseFullscreenLoading {
|
||||||
lock: true,
|
<T extends (...args: Parameters<T>) => ReturnType<T>>(
|
||||||
text: "加载中..."
|
fn: T,
|
||||||
|
options?: LoadingOptions
|
||||||
|
): (...args: Parameters<T>) => Promise<ReturnType<T>>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadingInstance {
|
interface LoadingInstance {
|
||||||
close: () => void
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseFullscreenLoading {
|
const DEFAULT_OPTIONS = {
|
||||||
<T extends (...args: any[]) => ReturnType<T>>(fn: T, options?: LoadingOptions): (
|
lock: true,
|
||||||
...args: Parameters<T>
|
text: "加载中..."
|
||||||
) => Promise<ReturnType<T>>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 传入一个函数 fn,在它执行周期内,加上「全屏」loading
|
* @name 全屏加载 Composable
|
||||||
|
* @description 传入一个函数 fn,在它执行周期内,加上「全屏」Loading
|
||||||
* @param fn 要执行的函数
|
* @param fn 要执行的函数
|
||||||
* @param options LoadingOptions
|
* @param options LoadingOptions
|
||||||
* @returns 返回一个新的函数,该函数返回一个 Promise
|
* @returns 返回一个新的函数,该函数返回一个 Promise
|
||||||
@ -25,10 +27,10 @@ export const useFullscreenLoading: UseFullscreenLoading = (fn, options = {}) =>
|
|||||||
let loadingInstance: LoadingInstance
|
let loadingInstance: LoadingInstance
|
||||||
return async (...args) => {
|
return async (...args) => {
|
||||||
try {
|
try {
|
||||||
loadingInstance = ElLoading.service({ ...defaultOptions, ...options })
|
loadingInstance = ElLoading.service({ ...DEFAULT_OPTIONS, ...options })
|
||||||
return await fn(...args)
|
return await fn(...args)
|
||||||
} finally {
|
} finally {
|
||||||
loadingInstance?.close()
|
loadingInstance.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
20
src/common/composables/useGreyAndColorWeakness.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useSettingsStore } from "@/pinia/stores/settings"
|
||||||
|
|
||||||
|
const GREY_MODE = "grey-mode"
|
||||||
|
const COLOR_WEAKNESS = "color-weakness"
|
||||||
|
|
||||||
|
const classList = document.documentElement.classList
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
function initGreyAndColorWeakness() {
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
watchEffect(() => {
|
||||||
|
classList.toggle(GREY_MODE, settingsStore.showGreyMode)
|
||||||
|
classList.toggle(COLOR_WEAKNESS, settingsStore.showColorWeakness)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 灰色模式和色弱模式 Composable */
|
||||||
|
export function useGreyAndColorWeakness() {
|
||||||
|
return { initGreyAndColorWeakness }
|
||||||
|
}
|
17
src/common/composables/useLayoutMode.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useSettingsStore } from "@/pinia/stores/settings"
|
||||||
|
import { LayoutModeEnum } from "@@/constants/app-key"
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
const isLeft = computed(() => settingsStore.layoutMode === LayoutModeEnum.Left)
|
||||||
|
const isTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.Top)
|
||||||
|
const isLeftTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.LeftTop)
|
||||||
|
|
||||||
|
function setLayoutMode(mode: LayoutModeEnum) {
|
||||||
|
settingsStore.layoutMode = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 布局模式 Composable */
|
||||||
|
export function useLayoutMode() {
|
||||||
|
return { isLeft, isTop, isLeftTop, setLayoutMode }
|
||||||
|
}
|
@ -1,13 +1,3 @@
|
|||||||
import { reactive } from "vue"
|
|
||||||
|
|
||||||
interface DefaultPaginationData {
|
|
||||||
total: number
|
|
||||||
currentPage: number
|
|
||||||
pageSizes: number[]
|
|
||||||
pageSize: number
|
|
||||||
layout: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PaginationData {
|
interface PaginationData {
|
||||||
total?: number
|
total?: number
|
||||||
currentPage?: number
|
currentPage?: number
|
||||||
@ -17,7 +7,7 @@ interface PaginationData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 默认的分页参数 */
|
/** 默认的分页参数 */
|
||||||
const defaultPaginationData: DefaultPaginationData = {
|
const DEFAULT_PAGINATION_DATA = {
|
||||||
total: 0,
|
total: 0,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSizes: [10, 20, 50],
|
pageSizes: [10, 20, 50],
|
||||||
@ -25,14 +15,15 @@ const defaultPaginationData: DefaultPaginationData = {
|
|||||||
layout: "total, sizes, prev, pager, next, jumper"
|
layout: "total, sizes, prev, pager, next, jumper"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePagination(initialPaginationData: PaginationData = {}) {
|
/** 分页 Composable */
|
||||||
/** 合并分页参数 */
|
export function usePagination(initPaginationData: PaginationData = {}) {
|
||||||
const paginationData = reactive({ ...defaultPaginationData, ...initialPaginationData })
|
// 合并分页参数
|
||||||
/** 改变当前页码 */
|
const paginationData = reactive({ ...DEFAULT_PAGINATION_DATA, ...initPaginationData })
|
||||||
|
// 改变当前页码
|
||||||
const handleCurrentChange = (value: number) => {
|
const handleCurrentChange = (value: number) => {
|
||||||
paginationData.currentPage = value
|
paginationData.currentPage = value
|
||||||
}
|
}
|
||||||
/** 改变页面大小 */
|
// 改变每页显示条数
|
||||||
const handleSizeChange = (value: number) => {
|
const handleSizeChange = (value: number) => {
|
||||||
paginationData.pageSize = value
|
paginationData.pageSize = value
|
||||||
}
|
}
|
42
src/common/composables/usePany.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
function initStarNotification() {
|
||||||
|
setTimeout(() => {
|
||||||
|
ElNotification({
|
||||||
|
title: "为爱发电!",
|
||||||
|
type: "success",
|
||||||
|
message: h(
|
||||||
|
"div",
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
h("div", null, "所有源码均免费开源,如果对你有帮助,欢迎点个 Star 支持一下!"),
|
||||||
|
h("a", { style: "color: teal", target: "_blank", href: "https://github.com/un-pany/v3-admin-vite" }, "点击传送")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
duration: 0,
|
||||||
|
position: "bottom-right"
|
||||||
|
})
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function initStoreNotification() {
|
||||||
|
setTimeout(() => {
|
||||||
|
ElNotification({
|
||||||
|
title: "懒人服务?",
|
||||||
|
type: "warning",
|
||||||
|
message: h(
|
||||||
|
"div",
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
h("div", null, "不想自己动手,但想移除 TS 或其他模块?也有懒人套餐!"),
|
||||||
|
h("a", { style: "color: teal", target: "_blank", href: "https://github.com/un-pany/v3-admin-vite/issues/225" }, "点击查看")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
duration: 0,
|
||||||
|
position: "bottom-right"
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 作者的小心思 */
|
||||||
|
export function usePany() {
|
||||||
|
return { initStarNotification, initStoreNotification }
|
||||||
|
}
|
52
src/common/composables/useRouteListener.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { Handler } from "mitt"
|
||||||
|
import type { RouteLocationNormalizedGeneric } from "vue-router"
|
||||||
|
import mitt from "mitt"
|
||||||
|
|
||||||
|
/** 回调函数的类型 */
|
||||||
|
type Callback = (route: RouteLocationNormalizedGeneric) => void
|
||||||
|
|
||||||
|
const emitter = mitt()
|
||||||
|
|
||||||
|
const key = Symbol("ROUTE_CHANGE")
|
||||||
|
|
||||||
|
let latestRoute: RouteLocationNormalizedGeneric
|
||||||
|
|
||||||
|
/** 设置最新的路由信息,触发路由变化事件 */
|
||||||
|
export function setRouteChange(to: RouteLocationNormalizedGeneric) {
|
||||||
|
// 触发事件
|
||||||
|
emitter.emit(key, to)
|
||||||
|
// 缓存最新的路由信息
|
||||||
|
latestRoute = to
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name 订阅路由变化 Composable
|
||||||
|
* @description 1. 单独用 watch 监听路由会浪费渲染性能
|
||||||
|
* @description 2. 可优先选择使用该发布订阅模式去进行分发管理
|
||||||
|
*/
|
||||||
|
export function useRouteListener() {
|
||||||
|
// 回调函数集合
|
||||||
|
const callbackList: Callback[] = []
|
||||||
|
|
||||||
|
// 监听路由变化(可以选择立即执行)
|
||||||
|
const listenerRouteChange = (callback: Callback, immediate = false) => {
|
||||||
|
// 缓存回调函数
|
||||||
|
callbackList.push(callback)
|
||||||
|
// 监听事件
|
||||||
|
emitter.on(key, callback as Handler)
|
||||||
|
// 可以选择立即执行一次回调函数
|
||||||
|
immediate && latestRoute && callback(latestRoute)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除路由变化事件监听器
|
||||||
|
const removeRouteListener = (callback: Callback) => {
|
||||||
|
emitter.off(key, callback as Handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件销毁前移除监听器
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
callbackList.forEach(removeRouteListener)
|
||||||
|
})
|
||||||
|
|
||||||
|
return { listenerRouteChange, removeRouteListener }
|
||||||
|
}
|
75
src/common/composables/useTheme.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { getActiveThemeName, setActiveThemeName } from "@@/utils/cache/local-storage"
|
||||||
|
import { setCssVar } from "@@/utils/css"
|
||||||
|
|
||||||
|
const DEFAULT_THEME_NAME = "normal"
|
||||||
|
|
||||||
|
type DefaultThemeName = typeof DEFAULT_THEME_NAME
|
||||||
|
|
||||||
|
/** 注册的主题名称, 其中 DefaultThemeName 是必填的 */
|
||||||
|
export type ThemeName = DefaultThemeName | "dark" | "dark-blue"
|
||||||
|
|
||||||
|
interface ThemeList {
|
||||||
|
title: string
|
||||||
|
name: ThemeName
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 主题列表 */
|
||||||
|
const themeList: ThemeList[] = [
|
||||||
|
{
|
||||||
|
title: "默认",
|
||||||
|
name: DEFAULT_THEME_NAME
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "黑暗",
|
||||||
|
name: "dark"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "深蓝",
|
||||||
|
name: "dark-blue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 正在应用的主题名称 */
|
||||||
|
const activeThemeName = ref<ThemeName>(getActiveThemeName() || DEFAULT_THEME_NAME)
|
||||||
|
|
||||||
|
/** 设置主题 */
|
||||||
|
function setTheme({ clientX, clientY }: MouseEvent, value: ThemeName) {
|
||||||
|
const maxRadius = Math.hypot(
|
||||||
|
Math.max(clientX, window.innerWidth - clientX),
|
||||||
|
Math.max(clientY, window.innerHeight - clientY)
|
||||||
|
)
|
||||||
|
setCssVar("--v3-theme-x", `${clientX}px`)
|
||||||
|
setCssVar("--v3-theme-y", `${clientY}px`)
|
||||||
|
setCssVar("--v3-theme-r", `${maxRadius}px`)
|
||||||
|
const handler = () => {
|
||||||
|
activeThemeName.value = value
|
||||||
|
}
|
||||||
|
document.startViewTransition ? document.startViewTransition(handler) : handler()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在 html 根元素上挂载 class */
|
||||||
|
function addHtmlClass(value: ThemeName) {
|
||||||
|
document.documentElement.classList.add(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在 html 根元素上移除其他主题 class */
|
||||||
|
function removeHtmlClass(value: ThemeName) {
|
||||||
|
const otherThemeNameList = themeList.map(item => item.name).filter(name => name !== value)
|
||||||
|
document.documentElement.classList.remove(...otherThemeNameList)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
function initTheme() {
|
||||||
|
// watchEffect 来收集副作用
|
||||||
|
watchEffect(() => {
|
||||||
|
const value = activeThemeName.value
|
||||||
|
removeHtmlClass(value)
|
||||||
|
addHtmlClass(value)
|
||||||
|
setActiveThemeName(value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 主题 Composable */
|
||||||
|
export function useTheme() {
|
||||||
|
return { themeList, activeThemeName, initTheme, setTheme }
|
||||||
|
}
|
22
src/common/composables/useTitle.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/** 项目标题 */
|
||||||
|
const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE ?? "V3 Admin Vite"
|
||||||
|
|
||||||
|
/** 动态标题 */
|
||||||
|
const dynamicTitle = ref<string>("")
|
||||||
|
|
||||||
|
/** 设置标题 */
|
||||||
|
function setTitle(title?: string) {
|
||||||
|
dynamicTitle.value = title ? `${VITE_APP_TITLE} | ${title}` : VITE_APP_TITLE
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听标题变化
|
||||||
|
watch(dynamicTitle, (value, oldValue) => {
|
||||||
|
if (document && value !== oldValue) {
|
||||||
|
document.title = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 标题 Composable */
|
||||||
|
export function useTitle() {
|
||||||
|
return { setTitle }
|
||||||
|
}
|
233
src/common/composables/useWatermark.ts
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
import type { Ref } from "vue"
|
||||||
|
import { debounce } from "lodash-es"
|
||||||
|
|
||||||
|
/** 默认配置 */
|
||||||
|
const DEFAULT_CONFIG = {
|
||||||
|
/** 防御(默认开启,能防御水印被删除或隐藏,但可能会有性能损耗) */
|
||||||
|
defense: true,
|
||||||
|
/** 文本颜色 */
|
||||||
|
color: "#c0c4cc",
|
||||||
|
/** 文本透明度 */
|
||||||
|
opacity: 0.5,
|
||||||
|
/** 文本字体大小 */
|
||||||
|
size: 16,
|
||||||
|
/** 文本字体 */
|
||||||
|
family: "serif",
|
||||||
|
/** 文本倾斜角度 */
|
||||||
|
angle: -20,
|
||||||
|
/** 一处水印所占宽度(数值越大水印密度越低) */
|
||||||
|
width: 300,
|
||||||
|
/** 一处水印所占高度(数值越大水印密度越低) */
|
||||||
|
height: 200
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultConfig = typeof DEFAULT_CONFIG
|
||||||
|
|
||||||
|
interface Observer {
|
||||||
|
watermarkElMutationObserver?: MutationObserver
|
||||||
|
parentElMutationObserver?: MutationObserver
|
||||||
|
parentElResizeObserver?: ResizeObserver
|
||||||
|
}
|
||||||
|
|
||||||
|
/** body 元素 */
|
||||||
|
const bodyEl = ref<HTMLElement>(document.body)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name 水印 Composable
|
||||||
|
* @description 1. 可以选择传入挂载水印的容器元素,默认是 body
|
||||||
|
* @description 2. 做了水印防御,能有效防御别人打开控制台删除或隐藏水印
|
||||||
|
*/
|
||||||
|
export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||||
|
// 备份文本
|
||||||
|
let backupText: string
|
||||||
|
// 最终配置
|
||||||
|
let mergeConfig: DefaultConfig
|
||||||
|
// 水印元素
|
||||||
|
let watermarkEl: HTMLElement | null = null
|
||||||
|
// 观察器
|
||||||
|
const observer: Observer = {
|
||||||
|
watermarkElMutationObserver: undefined,
|
||||||
|
parentElMutationObserver: undefined,
|
||||||
|
parentElResizeObserver: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置水印
|
||||||
|
const setWatermark = (text: string, config: Partial<DefaultConfig> = {}) => {
|
||||||
|
if (!parentEl.value) return console.warn("请在 DOM 挂载完成后再调用 setWatermark 方法设置水印")
|
||||||
|
// 备份文本
|
||||||
|
backupText = text
|
||||||
|
// 合并配置
|
||||||
|
mergeConfig = { ...DEFAULT_CONFIG, ...config }
|
||||||
|
// 创建或更新水印元素
|
||||||
|
watermarkEl ? updateWatermarkEl() : createWatermarkEl()
|
||||||
|
// 监听水印元素和容器元素的变化
|
||||||
|
addElListener(parentEl.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建水印元素
|
||||||
|
const createWatermarkEl = () => {
|
||||||
|
const isBody = parentEl.value!.tagName.toLowerCase() === bodyEl.value.tagName.toLowerCase()
|
||||||
|
const watermarkElPosition = isBody ? "fixed" : "absolute"
|
||||||
|
const parentElPosition = isBody ? "" : "relative"
|
||||||
|
watermarkEl = document.createElement("div")
|
||||||
|
watermarkEl.style.pointerEvents = "none"
|
||||||
|
watermarkEl.style.top = "0"
|
||||||
|
watermarkEl.style.left = "0"
|
||||||
|
watermarkEl.style.position = watermarkElPosition
|
||||||
|
watermarkEl.style.zIndex = "99999"
|
||||||
|
const { clientWidth, clientHeight } = parentEl.value!
|
||||||
|
updateWatermarkEl({ width: clientWidth, height: clientHeight })
|
||||||
|
// 设置水印容器为相对定位
|
||||||
|
parentEl.value!.style.position = parentElPosition
|
||||||
|
// 将水印元素添加到水印容器中
|
||||||
|
parentEl.value!.appendChild(watermarkEl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新水印元素
|
||||||
|
const updateWatermarkEl = (
|
||||||
|
options: Partial<{
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}> = {}
|
||||||
|
) => {
|
||||||
|
if (!watermarkEl) return
|
||||||
|
backupText && (watermarkEl.style.background = `url(${createBase64()}) left top repeat`)
|
||||||
|
options.width && (watermarkEl.style.width = `${options.width}px`)
|
||||||
|
options.height && (watermarkEl.style.height = `${options.height}px`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 base64 图片
|
||||||
|
const createBase64 = () => {
|
||||||
|
const { color, opacity, size, family, angle, width, height } = mergeConfig
|
||||||
|
const canvasEl = document.createElement("canvas")
|
||||||
|
canvasEl.width = width
|
||||||
|
canvasEl.height = height
|
||||||
|
const ctx = canvasEl.getContext("2d")
|
||||||
|
if (ctx) {
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.globalAlpha = opacity
|
||||||
|
ctx.font = `${size}px ${family}`
|
||||||
|
ctx.rotate((Math.PI / 180) * angle)
|
||||||
|
ctx.fillText(backupText, 0, height / 2)
|
||||||
|
}
|
||||||
|
return canvasEl.toDataURL()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除水印
|
||||||
|
const clearWatermark = () => {
|
||||||
|
if (!parentEl.value || !watermarkEl) return
|
||||||
|
// 移除对水印元素和容器元素的监听
|
||||||
|
removeListener()
|
||||||
|
// 移除水印元素
|
||||||
|
try {
|
||||||
|
parentEl.value.removeChild(watermarkEl)
|
||||||
|
} catch {
|
||||||
|
// 比如在无防御情况下,用户打开控制台删除了这个元素
|
||||||
|
console.warn("水印元素已不存在,请重新创建")
|
||||||
|
} finally {
|
||||||
|
watermarkEl = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新水印(防御时调用)
|
||||||
|
const updateWatermark = debounce(() => {
|
||||||
|
clearWatermark()
|
||||||
|
createWatermarkEl()
|
||||||
|
addElListener(parentEl.value!)
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
// 监听水印元素和容器元素的变化(DOM 变化 & DOM 大小变化)
|
||||||
|
const addElListener = (targetNode: HTMLElement) => {
|
||||||
|
// 判断是否开启防御
|
||||||
|
if (mergeConfig.defense) {
|
||||||
|
// 防止重复添加监听
|
||||||
|
if (!observer.watermarkElMutationObserver && !observer.parentElMutationObserver) {
|
||||||
|
// 监听 DOM 变化
|
||||||
|
addMutationListener(targetNode)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 无防御时不需要 mutation 监听
|
||||||
|
removeListener("mutation")
|
||||||
|
}
|
||||||
|
// 防止重复添加监听
|
||||||
|
if (!observer.parentElResizeObserver) {
|
||||||
|
// 监听 DOM 大小变化
|
||||||
|
addResizeListener(targetNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除对水印元素和容器元素的监听,传参可指定要移除哪个监听,不传默认移除全部监听
|
||||||
|
const removeListener = (kind: "mutation" | "resize" | "all" = "all") => {
|
||||||
|
// 移除 mutation 监听
|
||||||
|
if (kind === "mutation" || kind === "all") {
|
||||||
|
observer.watermarkElMutationObserver?.disconnect()
|
||||||
|
observer.watermarkElMutationObserver = undefined
|
||||||
|
observer.parentElMutationObserver?.disconnect()
|
||||||
|
observer.parentElMutationObserver = undefined
|
||||||
|
}
|
||||||
|
// 移除 resize 监听
|
||||||
|
if (kind === "resize" || kind === "all") {
|
||||||
|
observer.parentElResizeObserver?.disconnect()
|
||||||
|
observer.parentElResizeObserver = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 DOM 变化
|
||||||
|
const addMutationListener = (targetNode: HTMLElement) => {
|
||||||
|
// 当观察到变动时执行的回调
|
||||||
|
const mutationCallback = debounce((mutationList: MutationRecord[]) => {
|
||||||
|
// 水印的防御(防止用户手动删除水印元素或通过 CSS 隐藏水印)
|
||||||
|
mutationList.forEach(
|
||||||
|
debounce((mutation: MutationRecord) => {
|
||||||
|
switch (mutation.type) {
|
||||||
|
case "attributes":
|
||||||
|
mutation.target === watermarkEl && updateWatermark()
|
||||||
|
break
|
||||||
|
case "childList":
|
||||||
|
mutation.removedNodes.forEach((item) => {
|
||||||
|
item === watermarkEl && targetNode.appendChild(watermarkEl)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
)
|
||||||
|
}, 100)
|
||||||
|
// 创建观察器实例并传入回调
|
||||||
|
observer.watermarkElMutationObserver = new MutationObserver(mutationCallback)
|
||||||
|
observer.parentElMutationObserver = new MutationObserver(mutationCallback)
|
||||||
|
// 以上述配置开始观察目标节点
|
||||||
|
observer.watermarkElMutationObserver.observe(watermarkEl!, {
|
||||||
|
// 观察目标节点属性是否变动,默认为 true
|
||||||
|
attributes: true,
|
||||||
|
// 观察目标子节点是否有添加或者删除,默认为 false
|
||||||
|
childList: false,
|
||||||
|
// 是否拓展到观察所有后代节点,默认为 false
|
||||||
|
subtree: false
|
||||||
|
})
|
||||||
|
observer.parentElMutationObserver.observe(targetNode, {
|
||||||
|
attributes: false,
|
||||||
|
childList: true,
|
||||||
|
subtree: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 DOM 大小变化
|
||||||
|
const addResizeListener = (targetNode: HTMLElement) => {
|
||||||
|
// 当 targetNode 元素大小变化时去更新整个水印的大小
|
||||||
|
const resizeCallback = debounce(() => {
|
||||||
|
const { clientWidth, clientHeight } = targetNode
|
||||||
|
updateWatermarkEl({ width: clientWidth, height: clientHeight })
|
||||||
|
}, 500)
|
||||||
|
// 创建一个观察器实例并传入回调
|
||||||
|
observer.parentElResizeObserver = new ResizeObserver(resizeCallback)
|
||||||
|
// 开始观察目标节点
|
||||||
|
observer.parentElResizeObserver.observe(targetNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组件卸载前移除水印以及各种监听
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearWatermark()
|
||||||
|
})
|
||||||
|
|
||||||
|
return { setWatermark, clearWatermark }
|
||||||
|
}
|
@ -4,10 +4,19 @@ export enum DeviceEnum {
|
|||||||
Desktop
|
Desktop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 布局模式 */
|
||||||
|
export enum LayoutModeEnum {
|
||||||
|
Left = "left",
|
||||||
|
Top = "top",
|
||||||
|
LeftTop = "left-top"
|
||||||
|
}
|
||||||
|
|
||||||
/** 侧边栏打开状态常量 */
|
/** 侧边栏打开状态常量 */
|
||||||
export const SIDEBAR_OPENED = "opened"
|
export const SIDEBAR_OPENED = "opened"
|
||||||
|
|
||||||
/** 侧边栏关闭状态常量 */
|
/** 侧边栏关闭状态常量 */
|
||||||
export const SIDEBAR_CLOSED = "closed"
|
export const SIDEBAR_CLOSED = "closed"
|
||||||
|
|
||||||
export type SidebarOpened = typeof SIDEBAR_OPENED
|
export type SidebarOpened = typeof SIDEBAR_OPENED
|
||||||
|
|
||||||
export type SidebarClosed = typeof SIDEBAR_CLOSED
|
export type SidebarClosed = typeof SIDEBAR_CLOSED
|
@ -1,10 +1,11 @@
|
|||||||
const SYSTEM_NAME = "v3-admin-vite"
|
const SYSTEM_NAME = "v3-admin-vite"
|
||||||
|
|
||||||
/** 缓存数据时用到的 Key */
|
/** 缓存数据时用到的 Key */
|
||||||
class CacheKey {
|
export class CacheKey {
|
||||||
static readonly TOKEN = `${SYSTEM_NAME}-token-key`
|
static readonly TOKEN = `${SYSTEM_NAME}-token-key`
|
||||||
|
static readonly CONFIG_LAYOUT = `${SYSTEM_NAME}-config-layout-key`
|
||||||
static readonly SIDEBAR_STATUS = `${SYSTEM_NAME}-sidebar-status-key`
|
static readonly SIDEBAR_STATUS = `${SYSTEM_NAME}-sidebar-status-key`
|
||||||
static readonly ACTIVE_THEME_NAME = `${SYSTEM_NAME}-active-theme-name-key`
|
static readonly ACTIVE_THEME_NAME = `${SYSTEM_NAME}-active-theme-name-key`
|
||||||
|
static readonly VISITED_VIEWS = `${SYSTEM_NAME}-visited-views-key`
|
||||||
|
static readonly CACHED_VIEWS = `${SYSTEM_NAME}-cached-views-key`
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CacheKey
|
|
16
src/common/utils/cache/cookies.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// 统一处理 Cookie
|
||||||
|
|
||||||
|
import { CacheKey } from "@@/constants/cache-key"
|
||||||
|
import Cookies from "js-cookie"
|
||||||
|
|
||||||
|
export function getToken() {
|
||||||
|
return Cookies.get(CacheKey.TOKEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setToken(token: string) {
|
||||||
|
Cookies.set(CacheKey.TOKEN, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeToken() {
|
||||||
|
Cookies.remove(CacheKey.TOKEN)
|
||||||
|
}
|
60
src/common/utils/cache/local-storage.ts
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// 统一处理 localStorage
|
||||||
|
|
||||||
|
import type { LayoutsConfig } from "@/layouts/config"
|
||||||
|
import type { TagView } from "@/pinia/stores/tags-view"
|
||||||
|
import type { ThemeName } from "@@/composables/useTheme"
|
||||||
|
import type { SidebarClosed, SidebarOpened } from "@@/constants/app-key"
|
||||||
|
import { CacheKey } from "@@/constants/cache-key"
|
||||||
|
|
||||||
|
// #region 系统布局配置
|
||||||
|
export function getLayoutsConfig() {
|
||||||
|
const json = localStorage.getItem(CacheKey.CONFIG_LAYOUT)
|
||||||
|
return json ? (JSON.parse(json) as LayoutsConfig) : null
|
||||||
|
}
|
||||||
|
export function setLayoutsConfig(settings: LayoutsConfig) {
|
||||||
|
localStorage.setItem(CacheKey.CONFIG_LAYOUT, JSON.stringify(settings))
|
||||||
|
}
|
||||||
|
export function removeLayoutsConfig() {
|
||||||
|
localStorage.removeItem(CacheKey.CONFIG_LAYOUT)
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region 侧边栏状态
|
||||||
|
export function getSidebarStatus() {
|
||||||
|
return localStorage.getItem(CacheKey.SIDEBAR_STATUS)
|
||||||
|
}
|
||||||
|
export function setSidebarStatus(sidebarStatus: SidebarOpened | SidebarClosed) {
|
||||||
|
localStorage.setItem(CacheKey.SIDEBAR_STATUS, sidebarStatus)
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region 正在应用的主题名称
|
||||||
|
export function getActiveThemeName() {
|
||||||
|
return localStorage.getItem(CacheKey.ACTIVE_THEME_NAME) as ThemeName | null
|
||||||
|
}
|
||||||
|
export function setActiveThemeName(themeName: ThemeName) {
|
||||||
|
localStorage.setItem(CacheKey.ACTIVE_THEME_NAME, themeName)
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region 标签栏
|
||||||
|
export function getVisitedViews() {
|
||||||
|
const json = localStorage.getItem(CacheKey.VISITED_VIEWS)
|
||||||
|
return JSON.parse(json ?? "[]") as TagView[]
|
||||||
|
}
|
||||||
|
export function setVisitedViews(views: TagView[]) {
|
||||||
|
views.forEach((view) => {
|
||||||
|
// 删除不必要的属性,防止 JSON.stringify 处理到循环引用
|
||||||
|
delete view.matched
|
||||||
|
delete view.redirectedFrom
|
||||||
|
})
|
||||||
|
localStorage.setItem(CacheKey.VISITED_VIEWS, JSON.stringify(views))
|
||||||
|
}
|
||||||
|
export function getCachedViews() {
|
||||||
|
const json = localStorage.getItem(CacheKey.CACHED_VIEWS)
|
||||||
|
return JSON.parse(json ?? "[]") as string[]
|
||||||
|
}
|
||||||
|
export function setCachedViews(views: string[]) {
|
||||||
|
localStorage.setItem(CacheKey.CACHED_VIEWS, JSON.stringify(views))
|
||||||
|
}
|
||||||
|
// #endregion
|
18
src/common/utils/css.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/** 获取指定元素(默认全局)上的 CSS 变量的值 */
|
||||||
|
export function getCssVar(varName: string, element: HTMLElement = document.documentElement) {
|
||||||
|
if (!varName?.startsWith("--")) {
|
||||||
|
console.error("CSS 变量名应以 '--' 开头")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// 没有拿到值时,会返回空串
|
||||||
|
return getComputedStyle(element).getPropertyValue(varName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置指定元素(默认全局)上的 CSS 变量的值 */
|
||||||
|
export function setCssVar(varName: string, value: string, element: HTMLElement = document.documentElement) {
|
||||||
|
if (!varName?.startsWith("--")) {
|
||||||
|
console.error("CSS 变量名应以 '--' 开头")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
element.style.setProperty(varName, value)
|
||||||
|
}
|
9
src/common/utils/datetime.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import dayjs from "dayjs"
|
||||||
|
|
||||||
|
const INVALID_DATE = "N/A"
|
||||||
|
|
||||||
|
/** 格式化日期时间 */
|
||||||
|
export function formatDateTime(datetime: string | number | Date = "", template: string = "YYYY-MM-DD HH:mm:ss") {
|
||||||
|
const day = dayjs(datetime)
|
||||||
|
return day.isValid() ? day.format(template) : INVALID_DATE
|
||||||
|
}
|