Compare commits
430 Commits
1.2.0-back
...
1.3.6-back
Author | SHA1 | Date | |
---|---|---|---|
![]() |
10b8b81954 | ||
![]() |
1f50c95c66 | ||
![]() |
cd4706b717 | ||
![]() |
769aceb55f | ||
![]() |
e89cf400c0 | ||
![]() |
9e67929ee7 | ||
![]() |
7926865bf9 | ||
![]() |
51fbfcedd2 | ||
![]() |
8f71d6a5d9 | ||
![]() |
90625782c0 | ||
![]() |
12d0ba24e5 | ||
![]() |
540f24ed43 | ||
![]() |
c57d3f32b5 | ||
![]() |
b52f3ba0c5 | ||
![]() |
84ef207d9c | ||
![]() |
e68fff58e8 | ||
![]() |
63c06e02b2 | ||
![]() |
bf70539221 | ||
![]() |
5949c73a30 | ||
![]() |
cc6c9bf7a0 | ||
![]() |
6b1aab9c67 | ||
![]() |
8f4d3d418d | ||
![]() |
aa086a2800 | ||
![]() |
3b3f8e4e44 | ||
![]() |
b0763d6429 | ||
![]() |
f94ca10adf | ||
![]() |
4471bc7a5d | ||
![]() |
5689ac60ff | ||
![]() |
045bc4e5ee | ||
![]() |
17a18fc9ba | ||
![]() |
41152d1722 | ||
![]() |
f1af9f8f6e | ||
![]() |
0517a7014f | ||
![]() |
3e6d608a2f | ||
![]() |
5de954baa4 | ||
![]() |
add1e61b6f | ||
![]() |
9f978cc9b0 | ||
![]() |
89dd4b8131 | ||
![]() |
a10a981fab | ||
![]() |
20c15f352f | ||
![]() |
8aa7dabeff | ||
![]() |
78c7c1589a | ||
![]() |
dd833ca56b | ||
![]() |
681c1dc267 | ||
![]() |
4545422ee0 | ||
![]() |
5f26f5662e | ||
![]() |
ca94ca906f | ||
![]() |
76de450c71 | ||
![]() |
dd2b1ed580 | ||
![]() |
baec89f896 | ||
![]() |
7c7051a11e | ||
![]() |
aa27a2f7a1 | ||
![]() |
9ee6d06d50 | ||
![]() |
0cc1cb5a7b | ||
![]() |
e662681ce2 | ||
![]() |
0a9fc4e02d | ||
![]() |
be840460d8 | ||
![]() |
cb45987fe2 | ||
![]() |
5ffd7db8e0 | ||
![]() |
14377705e7 | ||
![]() |
23503778d4 | ||
![]() |
f54fab0bae | ||
![]() |
b985ff0584 | ||
![]() |
eff2f2a0b1 | ||
![]() |
664fa800cd | ||
![]() |
5dc4448c01 | ||
![]() |
ccfe992779 | ||
![]() |
583504495d | ||
![]() |
7fb4bf3431 | ||
![]() |
b148b8ec92 | ||
![]() |
79de6bcbf7 | ||
![]() |
14bd6dd25d | ||
![]() |
9b577261e2 | ||
![]() |
7f269e0d69 | ||
![]() |
4baec83db5 | ||
![]() |
7d8416890b | ||
![]() |
2e2ffcd59e | ||
![]() |
2046bfa846 | ||
![]() |
0446adf778 | ||
![]() |
f7a4d13a4c | ||
![]() |
e587256425 | ||
![]() |
0936861da1 | ||
![]() |
3318d76bab | ||
![]() |
8f3881eabf | ||
![]() |
5252480b09 | ||
![]() |
f096dfc6e6 | ||
![]() |
d18f56177c | ||
![]() |
333998b518 | ||
![]() |
3fb4fba1cb | ||
![]() |
c7e6210c8d | ||
![]() |
d864085c13 | ||
![]() |
fcdc1a1602 | ||
![]() |
bf7496f0d5 | ||
![]() |
9700150653 | ||
![]() |
f0e9e55af2 | ||
![]() |
ff88274554 | ||
![]() |
afce9dc5c0 | ||
![]() |
b5700bd0b1 | ||
![]() |
e085083e42 | ||
![]() |
a47910f650 | ||
![]() |
a8c4786311 | ||
![]() |
2971ccc0b7 | ||
![]() |
4ead56eaf1 | ||
![]() |
4fad8d77de | ||
![]() |
9db1087d32 | ||
![]() |
4a2c7b313f | ||
![]() |
0f5fc5f54c | ||
![]() |
76108e7b8f | ||
![]() |
6018817906 | ||
![]() |
7e4bdf7bd6 | ||
![]() |
32117574f6 | ||
![]() |
a48dfa1de2 | ||
![]() |
36bf6fc149 | ||
![]() |
f46ec30995 | ||
![]() |
9bd5a190c2 | ||
![]() |
4dc7543bb6 | ||
![]() |
d8e7945f9f | ||
![]() |
2fd1fdcb32 | ||
![]() |
86da3cedc2 | ||
![]() |
44ba945a12 | ||
![]() |
2680101872 | ||
![]() |
1c2e27613c | ||
![]() |
3e7a2336b0 | ||
![]() |
022d5182d7 | ||
![]() |
329a176a5c | ||
![]() |
41962ef380 | ||
![]() |
9003df713c | ||
![]() |
ebb4738be7 | ||
![]() |
ad7c33a7d6 | ||
![]() |
a114335a56 | ||
![]() |
9379093a4f | ||
![]() |
c9014d0338 | ||
![]() |
b8ec8edb38 | ||
![]() |
ed26dca64e | ||
![]() |
08c6496e24 | ||
![]() |
a8c5df38e9 | ||
![]() |
5b9f647cfd | ||
![]() |
ae6bf6ee53 | ||
![]() |
77894d5df4 | ||
![]() |
ba8f36a2c0 | ||
![]() |
133abe9ded | ||
![]() |
ef390ae636 | ||
![]() |
6d2f4e8486 | ||
![]() |
c4962aaf85 | ||
![]() |
f7128b099e | ||
![]() |
5510b6dea4 | ||
![]() |
98f658d46f | ||
![]() |
e307db2f3d | ||
![]() |
e6dab8300d | ||
![]() |
eb9f278e7f | ||
![]() |
34e5812de9 | ||
![]() |
07587c0faf | ||
![]() |
88316d7498 | ||
![]() |
53e02d46c2 | ||
![]() |
5e1de6fc79 | ||
![]() |
7463df053a | ||
![]() |
1286b52135 | ||
![]() |
92fe406ae9 | ||
![]() |
5b72d9b79d | ||
![]() |
b97fe47afd | ||
![]() |
4f2354b53a | ||
![]() |
8f9006c96d | ||
![]() |
71e8d12b70 | ||
![]() |
ae5d45763f | ||
![]() |
3f037f146b | ||
![]() |
63db3ba143 | ||
![]() |
17ca1ef1b2 | ||
![]() |
203c2edf63 | ||
![]() |
09e0721db7 | ||
![]() |
3c2a52057e | ||
![]() |
06dd17eac3 | ||
![]() |
0e3eb887da | ||
![]() |
1b2ded7421 | ||
![]() |
7bc36d2b84 | ||
![]() |
21fb9c8c99 | ||
![]() |
84d6559f25 | ||
![]() |
78c16c1016 | ||
![]() |
7fc284a609 | ||
![]() |
29c062a4a8 | ||
![]() |
e1aa1f7636 | ||
![]() |
eae24d8d83 | ||
![]() |
88b3208afb | ||
![]() |
26587ac09a | ||
![]() |
b4d038e22f | ||
![]() |
d216fdca44 | ||
![]() |
384c5d7dbb | ||
![]() |
63630b17c1 | ||
![]() |
a2ed3fa48b | ||
![]() |
104039cdfb | ||
![]() |
16f033aa8f | ||
![]() |
0c0988404e | ||
![]() |
1a16c520a1 | ||
![]() |
e52fec291a | ||
![]() |
aeed0fd48e | ||
![]() |
542407dcd6 | ||
![]() |
123f234971 | ||
![]() |
2ca39dfdbb | ||
![]() |
b0ad08dbbc | ||
![]() |
3600603016 | ||
![]() |
116fe39b8d | ||
![]() |
cde1a85394 | ||
![]() |
fe53c33821 | ||
![]() |
bac71a30f0 | ||
![]() |
ec82510f49 | ||
![]() |
f1c4ed1412 | ||
![]() |
36683dd218 | ||
![]() |
2577ba5500 | ||
![]() |
b5150b5863 | ||
![]() |
5d47026908 | ||
![]() |
63e13069ea | ||
![]() |
56e7e840b3 | ||
![]() |
2d58a2172d | ||
![]() |
c623604ea9 | ||
![]() |
75d7a607f5 | ||
![]() |
354ff7ecf6 | ||
![]() |
7933da8f66 | ||
![]() |
38d39d5e3d | ||
![]() |
ecf518bb02 | ||
![]() |
98e3a4a34c | ||
![]() |
362bc84cfb | ||
![]() |
b67be83a19 | ||
![]() |
af118cef71 | ||
![]() |
bc2beefa7e | ||
![]() |
44ad2c5f8d | ||
![]() |
1d9f1be004 | ||
![]() |
44138f578f | ||
![]() |
13951a0caf | ||
![]() |
a84a713eaa | ||
![]() |
0e3abc2b53 | ||
![]() |
a96be3db98 | ||
![]() |
308853cce1 | ||
![]() |
0a19ec3122 | ||
![]() |
ebc571e13f | ||
![]() |
2b7713323e | ||
![]() |
d6f239c564 | ||
![]() |
166e9a0e82 | ||
![]() |
c0a5942c2a | ||
![]() |
bc9e3a50e1 | ||
![]() |
06ccad9db0 | ||
![]() |
18722ce434 | ||
![]() |
a0feeb1966 | ||
![]() |
825c2837ea | ||
![]() |
23ff03d40c | ||
![]() |
755a30583f | ||
![]() |
a302fdf119 | ||
![]() |
df6341f0b8 | ||
![]() |
062e999f35 | ||
![]() |
6c4d15136f | ||
![]() |
f16afe657e | ||
![]() |
b9843c6faf | ||
![]() |
621f79e7d8 | ||
![]() |
467d337515 | ||
![]() |
dbc0b7e4a9 | ||
![]() |
aa2907323f | ||
![]() |
3655cae900 | ||
![]() |
60c398df39 | ||
![]() |
69222807a4 | ||
![]() |
b3e2d758f6 | ||
![]() |
96d2bc52e9 | ||
![]() |
f4a88efb0f | ||
![]() |
b78b599a06 | ||
![]() |
ffcc21975e | ||
![]() |
dd57e3c9ae | ||
![]() |
8c1cd617ad | ||
![]() |
e91e4e0eea | ||
![]() |
c2b5f6497d | ||
![]() |
456f0e1112 | ||
![]() |
e5fa32bbae | ||
![]() |
024087b9b2 | ||
![]() |
c0476613d7 | ||
![]() |
3c2d325d8c | ||
![]() |
a77bb8e68d | ||
![]() |
870cd86393 | ||
![]() |
f5fada20e6 | ||
![]() |
0b650367f3 | ||
![]() |
1616a06bfd | ||
![]() |
b97c83fdf3 | ||
![]() |
bb5ad57d9c | ||
![]() |
a8f20a2baa | ||
![]() |
0e7b76d5ed | ||
![]() |
877cc06eff | ||
![]() |
ba6785931d | ||
![]() |
731c9be4f1 | ||
![]() |
e546c21ad6 | ||
![]() |
22ff5bddae | ||
![]() |
b2c66c07b4 | ||
![]() |
cd110433c1 | ||
![]() |
3f0d30897f | ||
![]() |
66c1d390b9 | ||
![]() |
6c942418b4 | ||
![]() |
db955071d7 | ||
![]() |
17e82fb766 | ||
![]() |
03ceb2aac5 | ||
![]() |
f3e455c8d3 | ||
![]() |
39888cebaa | ||
![]() |
efb69fc75f | ||
![]() |
711a179c69 | ||
![]() |
3133f8f8b9 | ||
![]() |
f0a43912d1 | ||
![]() |
949004c67f | ||
![]() |
feb6229383 | ||
![]() |
614d998daf | ||
![]() |
9e4f886197 | ||
![]() |
18f2b84093 | ||
![]() |
25616baa1d | ||
![]() |
b92ac5c36d | ||
![]() |
504070f3eb | ||
![]() |
7032f79069 | ||
![]() |
a0b5aaa4dd | ||
![]() |
7230b94b16 | ||
![]() |
cca456aa82 | ||
![]() |
5310bddc1c | ||
![]() |
2e064604c1 | ||
![]() |
f415664acf | ||
![]() |
786b617179 | ||
![]() |
e6ee1f57b4 | ||
![]() |
feab6b3b30 | ||
![]() |
7f1548b343 | ||
![]() |
2d4ac33046 | ||
![]() |
17e2a02281 | ||
![]() |
096545c5a1 | ||
![]() |
04dff33ac5 | ||
![]() |
cfa18c2b8e | ||
![]() |
13354955db | ||
![]() |
056dee009f | ||
![]() |
1309c8425e | ||
![]() |
f0ded13df1 | ||
![]() |
e78d367cea | ||
![]() |
bb683804f4 | ||
![]() |
209214f6a3 | ||
![]() |
ab003826a3 | ||
![]() |
817c4a265d | ||
![]() |
8ed893fb21 | ||
![]() |
55f5e6bd0c | ||
![]() |
62d03605a3 | ||
![]() |
6170da0870 | ||
![]() |
e2a577de24 | ||
![]() |
89d963c81a | ||
![]() |
a9b7bf6442 | ||
![]() |
a66e13eca6 | ||
![]() |
b37ed48b9d | ||
![]() |
e78f4e984d | ||
![]() |
9f70a61c24 | ||
![]() |
4b9cfcb867 | ||
![]() |
f86c9f90ad | ||
![]() |
3229899c40 | ||
![]() |
31a6ab59fb | ||
![]() |
11083d5b7e | ||
![]() |
7b5fb4f164 | ||
![]() |
34789645f7 | ||
![]() |
f380452ef0 | ||
![]() |
decd9c55e5 | ||
![]() |
d8cac9bb00 | ||
![]() |
1dacd96e3c | ||
![]() |
e815f0ff89 | ||
![]() |
43534b6142 | ||
![]() |
5ea6b4a8d8 | ||
![]() |
a53ca3faf1 | ||
![]() |
86fdd6c93b | ||
![]() |
0e0661fe02 | ||
![]() |
86ce65e0ea | ||
![]() |
c3eb4fab13 | ||
![]() |
7a476372e1 | ||
![]() |
5e421ce607 | ||
![]() |
279fc98d76 | ||
![]() |
36a78dda90 | ||
![]() |
1d8676f456 | ||
![]() |
0c3dd92592 | ||
![]() |
d33261d0c2 | ||
![]() |
4da1bb9896 | ||
![]() |
7041c6a106 | ||
![]() |
12ffb310bf | ||
![]() |
d9799fec70 | ||
![]() |
4570d5b54b | ||
![]() |
d49e3e81a4 | ||
![]() |
579b1b486c | ||
![]() |
eba372062e | ||
![]() |
c9ccd2bbab | ||
![]() |
5aff8bac10 | ||
![]() |
1a12687027 | ||
![]() |
a221d2b491 | ||
![]() |
98d5d607b6 | ||
![]() |
9ee5369f35 | ||
![]() |
ccd99eb24d | ||
![]() |
c5c6760b5d | ||
![]() |
fbb0d641db | ||
![]() |
c07281bf41 | ||
![]() |
24bad09c74 | ||
![]() |
cddf71e600 | ||
![]() |
9f82052c71 | ||
![]() |
e0eb57d38d | ||
![]() |
b6b97accb1 | ||
![]() |
799934171a | ||
![]() |
10ebf03698 | ||
![]() |
cd258fbb52 | ||
![]() |
6cba181fad | ||
![]() |
f9504cece3 | ||
![]() |
182f1c9da8 | ||
![]() |
6e0c79411b | ||
![]() |
de27e8691d | ||
![]() |
e7b009786b | ||
![]() |
5e7aeaf12e | ||
![]() |
bc6818f531 | ||
![]() |
f78bc4e4f7 | ||
![]() |
cd77063f68 | ||
![]() |
0be1a0825d | ||
![]() |
551841bdd7 | ||
![]() |
5262233312 | ||
![]() |
52dc3e1788 | ||
![]() |
a9f9031f49 | ||
![]() |
f7c00cd8f7 | ||
![]() |
bdc1cb6d3b | ||
![]() |
b8ed931eb6 | ||
![]() |
8edc7c8ea4 | ||
![]() |
ba3fc0fe10 | ||
![]() |
a37dccdec0 | ||
![]() |
1df9236a53 | ||
![]() |
049ccca3e0 | ||
![]() |
00c4501d13 | ||
![]() |
061fcf926d | ||
![]() |
7e7a5f3fd4 | ||
![]() |
f8bb396dc4 | ||
![]() |
a832edce0d | ||
![]() |
af3fdeb1da | ||
![]() |
67d1f299b3 | ||
![]() |
cb7c0ecaa2 | ||
![]() |
64d3a21153 | ||
![]() |
a8019ed88a | ||
![]() |
4959574f21 | ||
![]() |
e89d1a0520 |
18
.github/CODEOWNERS
vendored
18
.github/CODEOWNERS
vendored
@@ -1,14 +1,14 @@
|
||||
# default onwer
|
||||
* anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
* anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
|
||||
# vben core onwer
|
||||
/.github/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
/.vscode/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
/packages/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
/packages/@core/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
/internal/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
/scripts/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
/.github/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
/.vscode/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
/packages/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
/packages/@core/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
/internal/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
/scripts/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
|
||||
# vben team onwer
|
||||
apps/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5
|
||||
docs/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5
|
||||
apps/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com
|
||||
docs/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com
|
||||
|
@@ -2,5 +2,5 @@ ports:
|
||||
- port: 5555
|
||||
onOpen: open-preview
|
||||
tasks:
|
||||
- init: corepack enable && pnpm install
|
||||
- init: npm i -g corepack && pnpm install
|
||||
command: pnpm run dev:play
|
||||
|
@@ -1,6 +0,0 @@
|
||||
echo Start running commit-msg hook...
|
||||
|
||||
# Check whether the git commit information is standardized
|
||||
pnpm exec commitlint --edit "$1"
|
||||
|
||||
echo Run commit-msg hook done.
|
@@ -1,3 +0,0 @@
|
||||
# 每次 git pull 之后, 安装依赖
|
||||
|
||||
pnpm install
|
@@ -1,7 +0,0 @@
|
||||
# update `.vscode/vben-admin.code-workspace` file
|
||||
pnpm vsh code-workspace --auto-commit
|
||||
|
||||
# Format and submit code according to lintstagedrc.js configuration
|
||||
pnpm exec lint-staged
|
||||
|
||||
echo Run pre-commit hook done.
|
@@ -1,20 +0,0 @@
|
||||
export default {
|
||||
'*.md': ['prettier --cache --ignore-unknown --write'],
|
||||
'*.vue': [
|
||||
'prettier --write',
|
||||
'eslint --cache --fix',
|
||||
'stylelint --fix --allow-empty-input',
|
||||
],
|
||||
'*.{js,jsx,ts,tsx}': [
|
||||
'prettier --cache --ignore-unknown --write',
|
||||
'eslint --cache --fix',
|
||||
],
|
||||
'*.{scss,less,styl,html,vue,css}': [
|
||||
'prettier --cache --ignore-unknown --write',
|
||||
'stylelint --fix --allow-empty-input',
|
||||
],
|
||||
'package.json': ['prettier --cache --write'],
|
||||
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
|
||||
'prettier --cache --write--parser json',
|
||||
],
|
||||
};
|
@@ -1 +1 @@
|
||||
20.14.0
|
||||
22.1.0
|
||||
|
2
.npmrc
2
.npmrc
@@ -1,5 +1,5 @@
|
||||
registry = "https://registry.npmmirror.com"
|
||||
public-hoist-pattern[]=husky
|
||||
public-hoist-pattern[]=lefthook
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=prettier
|
||||
public-hoist-pattern[]=prettier-plugin-tailwindcss
|
||||
|
21
.vscode/settings.json
vendored
21
.vscode/settings.json
vendored
@@ -14,7 +14,7 @@
|
||||
"editor.tabSize": 2,
|
||||
"editor.detectIndentation": false,
|
||||
"editor.cursorBlinking": "expand",
|
||||
"editor.largeFileOptimizations": false,
|
||||
"editor.largeFileOptimizations": true,
|
||||
"editor.accessibilitySupport": "off",
|
||||
"editor.cursorSmoothCaretAnimation": "on",
|
||||
"editor.guides.bracketPairs": "active",
|
||||
@@ -91,6 +91,7 @@
|
||||
"**/bower_components": true,
|
||||
"**/.turbo": true,
|
||||
"**/.idea": true,
|
||||
"**/.vitepress": true,
|
||||
"**/tmp": true,
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
@@ -113,6 +114,8 @@
|
||||
"**/yarn.lock": true
|
||||
},
|
||||
|
||||
"typescript.tsserver.exclude": ["**/node_modules", "**/dist", "**/.turbo"],
|
||||
|
||||
// search
|
||||
"search.searchEditor.singleClickBehaviour": "peekDefinition",
|
||||
"search.followSymlinks": false,
|
||||
@@ -217,17 +220,27 @@
|
||||
"*.env": "$(capture).env.*",
|
||||
"README.md": "README*,CHANGELOG*,LICENSE,CNAME",
|
||||
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,.gitattributes,.gitignore,.gitpod.yml,.npmrc,.browserslistrc,.node-version,.git*,.tazerc.json",
|
||||
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json",
|
||||
"eslint.config.mjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,stylelint.config.*,.lintstagedrc.mjs,cspell.json,lefthook.yml",
|
||||
"tailwind.config.mjs": "postcss.*"
|
||||
},
|
||||
"commentTranslate.hover.enabled": false,
|
||||
"commentTranslate.multiLineMerge": true,
|
||||
"vue.server.hybridMode": true,
|
||||
"vitest.disableWorkspaceWarning": true,
|
||||
"cSpell.words": ["tinymce", "vditor"],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.linkedEditing": true, // 自动同步更改html标签,
|
||||
"vscodeCustomCodeColor.highlightValue": "v-access", // v-access显示的颜色
|
||||
"vscodeCustomCodeColor.highlightValueColor": "#CCFFFF",
|
||||
"oxc.enable": false
|
||||
"oxc.enable": false,
|
||||
"cSpell.words": [
|
||||
"archiver",
|
||||
"axios",
|
||||
"dotenv",
|
||||
"isequal",
|
||||
"jspm",
|
||||
"napi",
|
||||
"nolebase",
|
||||
"rollup",
|
||||
"vitest"
|
||||
]
|
||||
}
|
||||
|
152
CHANGELOG.md
152
CHANGELOG.md
@@ -1,3 +1,155 @@
|
||||
# 1.3.6
|
||||
|
||||
**BUG FIX**
|
||||
|
||||
- oss配置switch切换 导致报错`存储类型找不到`
|
||||
- 文件上传无法正确清除(innerList)
|
||||
|
||||
# 1.3.5
|
||||
|
||||
**BUG FIX**
|
||||
|
||||
- 某些带Vxe表格弹窗 关闭后没有正常清理表格数据的问题
|
||||
|
||||
# 1.3.4
|
||||
|
||||
**BUG FIX**
|
||||
|
||||
- 文件上传多次触发导致数据不一致 https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC3BK6
|
||||
|
||||
**PREFORMANCE**
|
||||
|
||||
- 浏览器返回按钮/手势操作时 弹窗不会被关闭(keepAlive导致)
|
||||
|
||||
# 1.3.3
|
||||
|
||||
**BUG FIX**
|
||||
|
||||
- 工作流list展示在开启缩放会有误差导致触底逻辑不会触发
|
||||
|
||||
**OTHER**
|
||||
|
||||
- 代码生成预览对模板的提示...(下载都懒得点一下吗)
|
||||
|
||||
# 1.3.2
|
||||
|
||||
**REFACTOR**
|
||||
|
||||
- 所有表格操作列宽度调整为'auto', 这样会根据子元素宽度适配(比如没有分配权限的情况)
|
||||
- 菜单图标更新了一部分 sql同步更新
|
||||
|
||||
**OTHER**
|
||||
|
||||
- 暂时锁死vite依赖 i18n会报错
|
||||
|
||||
# 1.3.1
|
||||
|
||||
**REFACTOR**
|
||||
|
||||
- 所有Modal/Drawer表单关闭前会进行表单数据对比来弹出提示框
|
||||
- 字典项颜色选择从`原生input type=color`改为`vue3-colorpicker`组件
|
||||
- 全局Header: ClientID 更改大小写 [spring的问题导致](https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC0BDS)
|
||||
|
||||
**BUG FIX**
|
||||
|
||||
- getVxePopupContainer逻辑调整 解决表格固定高度展开不全的问题
|
||||
|
||||
**FEATURES**
|
||||
|
||||
- 字典渲染支持loading(length为0情况)
|
||||
|
||||
**OTHERS**
|
||||
|
||||
- useForm的组件改为异步导入(官方更新) bootstrap.js体积从2M降到600K 首屏加载速度提升
|
||||
|
||||
# 1.3.0
|
||||
|
||||
注意: 如果你使用老版本的`文件上传`/`图片上传` 可暂时使用
|
||||
|
||||
- `component: 'ImageUploadOld'`
|
||||
- `component: 'FileUploadOld'`
|
||||
|
||||
代替 **建议替换为新版本**
|
||||
|
||||
大致变动:
|
||||
|
||||
- `accept string[] -> string`
|
||||
- `resultField 已经移除 统一使用ossId`
|
||||
- `maxNumber -> maxCount`
|
||||
|
||||
具体参数查看: `apps/web-antd/src/components/upload/src/props.d.ts`
|
||||
|
||||
不再推荐使用useDescription, 这个版本会标记为@deprecated, 下个次版本将会移除
|
||||
|
||||
框架所有使用useDescription组件的会替换为原生(TODO)
|
||||
|
||||
**REFACTOR**
|
||||
|
||||
- **文件上传/图片上传重构(破坏性更新 不兼容之前的api)**
|
||||
- **文件上传/图片上传不再支持url用法 强制使用ossId**
|
||||
- TableSwitch组件重构
|
||||
- 管理员租户切换不再返回首页 直接刷新当前页(除特殊页面外会回到首页)
|
||||
- 租户切换Select增加loading
|
||||
- ~~modalLoading/drawerLoading改为调用内部的lock/unlock方法~~ 有待商榷暂时按老版本逻辑不变
|
||||
- 登录验证码 增加loading
|
||||
- DictEnum使用const代替enum
|
||||
- TinyMCE组件重构 移除冗余代码/功能 增加loading
|
||||
|
||||
**ALPHA功能**
|
||||
|
||||
- 弹窗表单数据更改关闭时的提示框(可能最终不会加入) 测试页面: 参数管理
|
||||
|
||||
**BUG FIX**
|
||||
|
||||
- 重新登录 字典会unknown的情况[详细分析](https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IBY27D)
|
||||
- 测试菜单 请假申请 选中删除 需要根据状态判断
|
||||
- 修复文件/图片在Safari中无法上传 file-type库与Safari不兼容导致
|
||||
- 头像裁剪 图片加载失败一直处于loading无法上传
|
||||
- 头像裁剪 私有桶会拼接timestamp参数导致sign计算异常无法上传 感谢cropperjs作者 https://github.com/fengyuanchen/cropperjs/issues/1230
|
||||
- 租户选择下拉框会跟随body滚动(将下拉框样式的默认absolute改为fixed)
|
||||
|
||||
**OTHER**
|
||||
|
||||
- 字典管理 字典类型 表格选中行增加bold效果
|
||||
- 全局圆角修改 与antd保持一致
|
||||
- vditor(Markdown)升级3.10.9
|
||||
- 老版本的文件/图片上传将于下个版本移除
|
||||
- useDescription将于下个版本移除
|
||||
- getVxePopupContainer与新版Vxe不兼容 先返回body(会导致滚动不跟随)后续版本再优化
|
||||
|
||||
# 1.2.3
|
||||
|
||||
**BUG FIX**
|
||||
|
||||
- `withDefaultPlaceholder`中将`placeholder`修改为computed, 解决后续使用`updateSchema`无法正常更新显示placeholder(响应式问题)
|
||||
|
||||
- 流程定义 修改accept类型 解决无法拖拽上传
|
||||
|
||||
**FEATURES**
|
||||
|
||||
- 增加`环境变量`打包配置demo -> build:antd:test
|
||||
- 角色管理 勾选权限组件添加对错误用法的校验提示
|
||||
|
||||
**REFACTOR**
|
||||
|
||||
- OAuth内部逻辑重构 增加新的默认OAuth登录方式
|
||||
- 重构部分setup组件为setup语法糖形式
|
||||
|
||||
# 1.2.2
|
||||
|
||||
**FEATURES**
|
||||
|
||||
- 代码生成支持路径方式生成
|
||||
- 代码生成 支持选择表单生成类型(需要模板支持)
|
||||
- 工作流 支持按钮权限
|
||||
|
||||
# 1.2.1
|
||||
|
||||
# BUG FIXES
|
||||
|
||||
- 客户端管理 错误的status disabled
|
||||
- modal/drawer升级后zIndex(2000)会遮挡Tinymce的下拉框zIndex(1300)
|
||||
|
||||
# 1.2.0
|
||||
|
||||
**REFACTOR**
|
||||
|
@@ -1,8 +1,13 @@
|
||||
<div align="center"> <a href="https://github.com/anncwb/vue-vben-admin"> <img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp"> </a> <br> <br>
|
||||
<div align="center">
|
||||
<a href="https://github.com/anncwb/vue-vben-admin">
|
||||
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
<h1>Vue Vben Admin</h1>
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||
@@ -15,27 +20,27 @@ Vue Vben Adminは、最新の`vue3`、`vite`、`TypeScript`などの主流技術
|
||||
|
||||
## アップグレード通知
|
||||
|
||||
これは最新バージョン5.0であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。
|
||||
これは最新バージョン `5.0` であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。
|
||||
|
||||
## 特徴
|
||||
|
||||
- **最新技術スタック**: Vue 3やViteなどの最先端フロントエンド技術で開発
|
||||
- **TypeScript**: アプリケーション規模のJavaScriptのための言語
|
||||
- **テーマ**: 複数のテーマカラーが利用可能で、カスタマイズオプションも豊富
|
||||
- **国際化**: 完全な内蔵国際化サポート
|
||||
- **権限管理**: 動的ルートベースの権限生成ソリューションを内蔵
|
||||
- **最新技術スタック**:Vue 3やViteなどの最先端フロントエンド技術で開発
|
||||
- **TypeScript**:アプリケーション規模のJavaScriptのための言語
|
||||
- **テーマ**:複数のテーマカラーが利用可能で、カスタマイズオプションも豊富
|
||||
- **国際化**:完全な内蔵国際化サポート
|
||||
- **権限管理**:動的ルートベースの権限生成ソリューションを内蔵
|
||||
|
||||
## プレビュー
|
||||
|
||||
- [Vben Admin](https://vben.pro/) - フルバージョンの中国語サイト
|
||||
|
||||
テストアカウント: vben/123456
|
||||
テストアカウント:vben/123456
|
||||
|
||||
<p align="center">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
|
||||
</p>
|
||||
<div align="center">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
|
||||
</div>
|
||||
|
||||
### Gitpodを使用
|
||||
|
||||
@@ -49,30 +54,27 @@ Gitpod(GitHub用の無料オンライン開発環境)でプロジェクト
|
||||
|
||||
## インストールと使用
|
||||
|
||||
- プロジェクトコードを取得
|
||||
1. プロジェクトコードを取得
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vbenjs/vue-vben-admin.git
|
||||
```
|
||||
|
||||
- 依存関係のインストール
|
||||
2. 依存関係のインストール
|
||||
|
||||
```bash
|
||||
cd vue-vben-admin
|
||||
|
||||
corepack enable
|
||||
|
||||
npm i -g corepack
|
||||
pnpm install
|
||||
|
||||
```
|
||||
|
||||
- 実行
|
||||
3. 実行
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- ビルド
|
||||
4. ビルド
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
@@ -86,40 +88,39 @@ pnpm build
|
||||
|
||||
ご参加をお待ちしておりますするか、Pull Requestを送信してください。
|
||||
|
||||
**Pull Request:**
|
||||
**Pull Request プロセス:**
|
||||
|
||||
1. コードをフォーク!
|
||||
2. 自分のブランチを作成: `git checkout -b feat/xxxx`
|
||||
3. 変更をコミット: `git commit -am 'feat(function): add xxxxx'`
|
||||
4. ブランチをプッシュ: `git push origin feat/xxxx`
|
||||
1. コードをフォーク
|
||||
2. 自分のブランチを作成:`git checkout -b feat/xxxx`
|
||||
3. 変更をコミット:`git commit -am 'feat(function): add xxxxx'`
|
||||
4. ブランチをプッシュ:`git push origin feat/xxxx`
|
||||
5. `pull request`を送信
|
||||
|
||||
## Git貢献提出規則
|
||||
|
||||
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||
|
||||
- `feat` 新機能の追加
|
||||
- `fix` 問題/バグの修正
|
||||
- `style` コードスタイルに関連し、実行結果に影響しない
|
||||
- `perf` 最適化/パフォーマンス向上
|
||||
- `refactor` リファクタリング
|
||||
- `revert` 変更の取り消し
|
||||
- `test` テスト関連
|
||||
- `docs` ドキュメント/注釈
|
||||
- `chore` 依存関係の更新/スキャフォールディング設定の変更など
|
||||
- `ci` 継続的インテグレーション
|
||||
- `types` 型定義ファイルの変更
|
||||
- `wip` 開発中
|
||||
- `feat` 新機能の追加
|
||||
- `fix` 問題/バグの修正
|
||||
- `style` コードスタイルに関連し、実行結果に影響しない
|
||||
- `perf` 最適化/パフォーマンス向上
|
||||
- `refactor` リファクタリング
|
||||
- `revert` 変更の取り消し
|
||||
- `test` テスト関連
|
||||
- `docs` ドキュメント/注釈
|
||||
- `chore` 依存関係の更新/スキャフォールディング設定の変更など
|
||||
- `ci` 継続的インテグレーション
|
||||
- `types` 型定義ファイルの変更
|
||||
|
||||
## ブラウザサポート
|
||||
|
||||
ローカル開発には`Chrome 80+`ブラウザを推奨します
|
||||
ローカル開発には `Chrome 80+` ブラウザを推奨します
|
||||
|
||||
モダンブラウザをサポートし、IEはサポートしません
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||
| :-: | :-: | :-: | :-: | :-: |
|
||||
| サポートしない | 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||
| :-: | :-: | :-: | :-: |
|
||||
| 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
|
||||
|
||||
## メンテナー
|
||||
|
||||
@@ -140,8 +141,7 @@ pnpm build
|
||||
## 貢献者
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img alt="Contributors"
|
||||
src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
87
README.md
87
README.md
@@ -1,8 +1,13 @@
|
||||
<div align="center"> <a href="https://github.com/anncwb/vue-vben-admin"> <img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp"> </a> <br> <br>
|
||||
<div align="center">
|
||||
<a href="https://github.com/anncwb/vue-vben-admin">
|
||||
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
<h1>Vue Vben Admin</h1>
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||
@@ -17,7 +22,7 @@ Vue Vben Admin is a free and open source middle and back-end template. Using the
|
||||
|
||||
This is the latest version, 5.0, and it is not compatible with previous versions. If you are starting a new project, it is recommended to use the latest version. If you wish to view the old version, please use the [v2 branch](https://github.com/vbenjs/vue-vben-admin/tree/v2).
|
||||
|
||||
## Feature
|
||||
## Features
|
||||
|
||||
- **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite
|
||||
- **TypeScript**: A language for application-scale JavaScript
|
||||
@@ -31,11 +36,11 @@ This is the latest version, 5.0, and it is not compatible with previous versions
|
||||
|
||||
Test Account: vben/123456
|
||||
|
||||
<p align="center">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
|
||||
</p>
|
||||
<div align="center">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
|
||||
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
|
||||
</div>
|
||||
|
||||
### Use Gitpod
|
||||
|
||||
@@ -47,31 +52,29 @@ Open the project in Gitpod (free online dev environment for GitHub) and start co
|
||||
|
||||
[Document](https://doc.vben.pro/)
|
||||
|
||||
## Install and use
|
||||
## Install and Use
|
||||
|
||||
- Get the project code
|
||||
1. Get the project code
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vbenjs/vue-vben-admin.git
|
||||
```
|
||||
|
||||
- Installation dependencies
|
||||
2. Install dependencies
|
||||
|
||||
```bash
|
||||
cd vue-vben-admin
|
||||
|
||||
corepack enable
|
||||
|
||||
npm i -g corepack
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- run
|
||||
3. Run
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- build
|
||||
4. Build
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
@@ -81,44 +84,43 @@ pnpm build
|
||||
|
||||
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
|
||||
|
||||
## How to contribute
|
||||
## How to Contribute
|
||||
|
||||
You are very welcome to join Or submit a Pull Request。
|
||||
You are very welcome to join! [Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) or submit a Pull Request.
|
||||
|
||||
**Pull Request:**
|
||||
**Pull Request Process:**
|
||||
|
||||
1. Fork code!
|
||||
2. Create your own branch: `git checkout -b feat/xxxx`
|
||||
1. Fork the code
|
||||
2. Create your branch: `git checkout -b feat/xxxx`
|
||||
3. Submit your changes: `git commit -am 'feat(function): add xxxxx'`
|
||||
4. Push your branch: `git push origin feat/xxxx`
|
||||
5. submit`pull request`
|
||||
5. Submit `pull request`
|
||||
|
||||
## Git Contribution submission specification
|
||||
## Git Contribution Submission Specification
|
||||
|
||||
- reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||
Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||
|
||||
- `feat` Add new features
|
||||
- `fix` Fix the problem/BUG
|
||||
- `style` The code style is related and does not affect the running result
|
||||
- `perf` Optimization/performance improvement
|
||||
- `refactor` Refactor
|
||||
- `revert` Undo edit
|
||||
- `test` Test related
|
||||
- `docs` Documentation/notes
|
||||
- `chore` Dependency update/scaffolding configuration modification etc.
|
||||
- `ci` Continuous integration
|
||||
- `types` Type definition file changes
|
||||
- `wip` In development
|
||||
- `feat` Add new features
|
||||
- `fix` Fix the problem/BUG
|
||||
- `style` The code style is related and does not affect the running result
|
||||
- `perf` Optimization/performance improvement
|
||||
- `refactor` Refactor
|
||||
- `revert` Undo edit
|
||||
- `test` Test related
|
||||
- `docs` Documentation/notes
|
||||
- `chore` Dependency update/scaffolding configuration modification etc.
|
||||
- `ci` Continuous integration
|
||||
- `types` Type definition file changes
|
||||
|
||||
## Browser support
|
||||
## Browser Support
|
||||
|
||||
The `Chrome 80+` browser is recommended for local development
|
||||
|
||||
Support modern browsers, not IE
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||
| :-: | :-: | :-: | :-: | :-: |
|
||||
| not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||
| :-: | :-: | :-: | :-: |
|
||||
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
|
||||
|
||||
## Maintainer
|
||||
|
||||
@@ -136,11 +138,10 @@ If you think this project is helpful to you, you can help the author buy a cup o
|
||||
|
||||
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aee;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
|
||||
|
||||
## Contributor
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
<img alt="Contributors"
|
||||
src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
<img alt="Contributors" src="https://opencollective.com/vbenjs/contributors.svg?button=false" />
|
||||
</a>
|
||||
|
||||
## Discord
|
||||
|
@@ -6,7 +6,7 @@
|
||||
|
||||
v5版本采用分仓(包)目录结构, 具体开发路径为: `根目录/apps/web-antd`
|
||||
|
||||
目前对应后端版本: **分布式5.3.0/微服务2.2.3**
|
||||
目前对应后端版本: **分布式5.3.1/微服务2.3.0**
|
||||
|
||||
V1.1.0版本已支持离线图标
|
||||
|
||||
@@ -18,7 +18,7 @@ V1.2.0版本对接warmflow工作流
|
||||
|
||||
| 组件/框架 | 版本 |
|
||||
| :------------- | :----- |
|
||||
| vben | 5.5.3 |
|
||||
| vben | 5.5.4 |
|
||||
| ant-design-vue | 4.2.6 |
|
||||
| vue | 3.5.13 |
|
||||
|
||||
@@ -46,6 +46,14 @@ admin 账号: admin admin123
|
||||
|
||||
[RuoYi-Plus 文档地址](https://plus-doc.dromara.org/#/)
|
||||
|
||||
## 关于表单
|
||||
|
||||
如果你觉得`useVbenForm`难度很大, 完全可以**使用原生antd表单**进行开发, 不一定非得用`useVbenForm`进行开发
|
||||
|
||||
`apps/web-antd/src/views/system/notice/notice-modal.vue`即`通知公告modal`使用**原生antd form**进行(反向🤔)重构, 不想用`useVbenForm`可参考该页面进行表单开发
|
||||
|
||||
复杂表单(如各种联动, 需要自定义样式布局, 需要自定义组件)**优先使用原生表单**(反正说了也没人听听😅)
|
||||
|
||||
## 预览图
|
||||
|
||||
        
|
||||
@@ -68,7 +76,7 @@ admin 账号: admin admin123
|
||||
git clone https://gitee.com/dapppp/ruoyi-plus-vben5.git
|
||||
```
|
||||
|
||||
- 安装依赖
|
||||
2. 安装依赖
|
||||
|
||||
```bash
|
||||
cd ruoyi-plus-vben5
|
||||
@@ -142,7 +150,7 @@ VITE_GLOB_WEBSOCKET_ENABLE=false
|
||||
pnpm dev:antd
|
||||
```
|
||||
|
||||
- 打包
|
||||
4. 打包
|
||||
|
||||
```bash
|
||||
pnpm build:antd
|
||||
@@ -156,21 +164,21 @@ pnpm build:antd
|
||||
|
||||
## Git 贡献提交规范
|
||||
|
||||
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||
|
||||
- `feat` 增加新功能
|
||||
- `fix` 修复问题/BUG
|
||||
- `style` 代码风格相关无影响运行结果的
|
||||
- `perf` 优化/性能提升
|
||||
- `refactor` 重构
|
||||
- `revert` 撤销修改
|
||||
- `test` 测试相关
|
||||
- `docs` 文档/注释
|
||||
- `chore` 依赖更新/脚手架配置修改等
|
||||
- `workflow` 工作流改进
|
||||
- `ci` 持续集成
|
||||
- `types` 类型定义文件更改
|
||||
- `wip` 开发中
|
||||
- `feat` 增加新功能
|
||||
- `fix` 修复问题/BUG
|
||||
- `style` 代码风格相关无影响运行结果的
|
||||
- `perf` 优化/性能提升
|
||||
- `refactor` 重构
|
||||
- `revert` 撤销修改
|
||||
- `test` 测试相关
|
||||
- `docs` 文档/注释
|
||||
- `chore` 依赖更新/脚手架配置修改等
|
||||
- `workflow` 工作流改进
|
||||
- `ci` 持续集成
|
||||
- `types` 类型定义文件更改
|
||||
- `wip` 开发中
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
@@ -178,7 +186,7 @@ pnpm build:antd
|
||||
|
||||
本地开发推荐使用`Chrome` 最新版本浏览器
|
||||
|
||||
支持现代浏览器, 不支持 IE
|
||||
支持现代浏览器,不支持 IE
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
|
||||
| :-: | :-: | :-: | :-: | :-: |
|
||||
|
15
apps/backend-mock/api/system/dept/.post.ts
Normal file
15
apps/backend-mock/api/system/dept/.post.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
unAuthorizedResponse,
|
||||
useResponseSuccess,
|
||||
} from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
await sleep(600);
|
||||
return useResponseSuccess(null);
|
||||
});
|
15
apps/backend-mock/api/system/dept/[id].delete.ts
Normal file
15
apps/backend-mock/api/system/dept/[id].delete.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
unAuthorizedResponse,
|
||||
useResponseSuccess,
|
||||
} from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
await sleep(1000);
|
||||
return useResponseSuccess(null);
|
||||
});
|
15
apps/backend-mock/api/system/dept/[id].put.ts
Normal file
15
apps/backend-mock/api/system/dept/[id].put.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import {
|
||||
sleep,
|
||||
unAuthorizedResponse,
|
||||
useResponseSuccess,
|
||||
} from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
await sleep(2000);
|
||||
return useResponseSuccess(null);
|
||||
});
|
61
apps/backend-mock/api/system/dept/list.ts
Normal file
61
apps/backend-mock/api/system/dept/list.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
|
||||
function generateMockDataList(count: number) {
|
||||
const dataList = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const dataItem: Record<string, any> = {
|
||||
id: faker.string.uuid(),
|
||||
pid: 0,
|
||||
name: faker.commerce.department(),
|
||||
status: faker.helpers.arrayElement([0, 1]),
|
||||
createTime: formatterCN.format(
|
||||
faker.date.between({ from: '2021-01-01', to: '2022-12-31' }),
|
||||
),
|
||||
remark: faker.lorem.sentence(),
|
||||
};
|
||||
if (faker.datatype.boolean()) {
|
||||
dataItem.children = Array.from(
|
||||
{ length: faker.number.int({ min: 1, max: 5 }) },
|
||||
() => ({
|
||||
id: faker.string.uuid(),
|
||||
pid: dataItem.id,
|
||||
name: faker.commerce.department(),
|
||||
status: faker.helpers.arrayElement([0, 1]),
|
||||
createTime: formatterCN.format(
|
||||
faker.date.between({ from: '2023-01-01', to: '2023-12-31' }),
|
||||
),
|
||||
remark: faker.lorem.sentence(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
dataList.push(dataItem);
|
||||
}
|
||||
|
||||
return dataList;
|
||||
}
|
||||
|
||||
const mockData = generateMockDataList(10);
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
const listData = structuredClone(mockData);
|
||||
|
||||
return useResponseSuccess(listData);
|
||||
});
|
12
apps/backend-mock/api/system/menu/list.ts
Normal file
12
apps/backend-mock/api/system/menu/list.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
return useResponseSuccess(MOCK_MENU_LIST);
|
||||
});
|
28
apps/backend-mock/api/system/menu/name-exists.ts
Normal file
28
apps/backend-mock/api/system/menu/name-exists.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
const namesMap: Record<string, any> = {};
|
||||
|
||||
function getNames(menus: any[]) {
|
||||
menus.forEach((menu) => {
|
||||
namesMap[menu.name] = String(menu.id);
|
||||
if (menu.children) {
|
||||
getNames(menu.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
getNames(MOCK_MENU_LIST);
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
const { id, name } = getQuery(event);
|
||||
|
||||
return (name as string) in namesMap &&
|
||||
(!id || namesMap[name as string] !== String(id))
|
||||
? useResponseSuccess(true)
|
||||
: useResponseSuccess(false);
|
||||
});
|
28
apps/backend-mock/api/system/menu/path-exists.ts
Normal file
28
apps/backend-mock/api/system/menu/path-exists.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
const pathMap: Record<string, any> = { '/': 0 };
|
||||
|
||||
function getPaths(menus: any[]) {
|
||||
menus.forEach((menu) => {
|
||||
pathMap[menu.path] = String(menu.id);
|
||||
if (menu.children) {
|
||||
getPaths(menu.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
getPaths(MOCK_MENU_LIST);
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
const { id, path } = getQuery(event);
|
||||
|
||||
return (path as string) in pathMap &&
|
||||
(!id || pathMap[path as string] !== String(id))
|
||||
? useResponseSuccess(true)
|
||||
: useResponseSuccess(false);
|
||||
});
|
83
apps/backend-mock/api/system/role/list.ts
Normal file
83
apps/backend-mock/api/system/role/list.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
|
||||
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
||||
|
||||
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
|
||||
const menuIds = getMenuIds(MOCK_MENU_LIST);
|
||||
|
||||
function generateMockDataList(count: number) {
|
||||
const dataList = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const dataItem: Record<string, any> = {
|
||||
id: faker.string.uuid(),
|
||||
name: faker.commerce.product(),
|
||||
status: faker.helpers.arrayElement([0, 1]),
|
||||
createTime: formatterCN.format(
|
||||
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
|
||||
),
|
||||
permissions: faker.helpers.arrayElements(menuIds),
|
||||
remark: faker.lorem.sentence(),
|
||||
};
|
||||
|
||||
dataList.push(dataItem);
|
||||
}
|
||||
|
||||
return dataList;
|
||||
}
|
||||
|
||||
const mockData = generateMockDataList(100);
|
||||
|
||||
export default eventHandler(async (event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
name,
|
||||
id,
|
||||
remark,
|
||||
startTime,
|
||||
endTime,
|
||||
status,
|
||||
} = getQuery(event);
|
||||
let listData = structuredClone(mockData);
|
||||
if (name) {
|
||||
listData = listData.filter((item) =>
|
||||
item.name.toLowerCase().includes(String(name).toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (id) {
|
||||
listData = listData.filter((item) =>
|
||||
item.id.toLowerCase().includes(String(id).toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (remark) {
|
||||
listData = listData.filter((item) =>
|
||||
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
|
||||
);
|
||||
}
|
||||
if (startTime) {
|
||||
listData = listData.filter((item) => item.createTime >= startTime);
|
||||
}
|
||||
if (endTime) {
|
||||
listData = listData.filter((item) => item.createTime <= endTime);
|
||||
}
|
||||
if (['0', '1'].includes(status as string)) {
|
||||
listData = listData.filter((item) => item.status === Number(status));
|
||||
}
|
||||
return usePageResponseSuccess(page as string, pageSize as string, listData);
|
||||
});
|
@@ -1,6 +1,6 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
|
||||
|
||||
function generateMockDataList(count: number) {
|
||||
const dataList = [];
|
||||
|
13
apps/backend-mock/api/upload.ts
Normal file
13
apps/backend-mock/api/upload.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { verifyAccessToken } from '~/utils/jwt-utils';
|
||||
import { unAuthorizedResponse } from '~/utils/response';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const userinfo = verifyAccessToken(event);
|
||||
if (!userinfo) {
|
||||
return unAuthorizedResponse(event);
|
||||
}
|
||||
return useResponseSuccess({
|
||||
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||
});
|
||||
// return useResponseError("test")
|
||||
});
|
@@ -1,7 +1,19 @@
|
||||
export default defineEventHandler((event) => {
|
||||
import { forbiddenResponse, sleep } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
event.node.res.setHeader(
|
||||
'Access-Control-Allow-Origin',
|
||||
event.headers.get('Origin') ?? '*',
|
||||
);
|
||||
if (event.method === 'OPTIONS') {
|
||||
event.node.res.statusCode = 204;
|
||||
event.node.res.statusMessage = 'No Content.';
|
||||
return 'OK';
|
||||
} else if (
|
||||
['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) &&
|
||||
event.path.startsWith('/api/system/')
|
||||
) {
|
||||
await sleep(Math.floor(Math.random() * 2000));
|
||||
return forbiddenResponse(event, '演示环境,禁止修改');
|
||||
}
|
||||
});
|
||||
|
@@ -9,7 +9,8 @@ export default defineNitroConfig({
|
||||
cors: true,
|
||||
headers: {
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
'Access-Control-Allow-Headers':
|
||||
'Accept, Authorization, Content-Length, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
|
||||
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Expose-Headers': '*',
|
||||
|
@@ -7,6 +7,7 @@ export default defineEventHandler(() => {
|
||||
<li><a href="/api/menu">/api/menu/all</a></li>
|
||||
<li><a href="/api/auth/codes">/api/auth/codes</a></li>
|
||||
<li><a href="/api/auth/login">/api/auth/login</a></li>
|
||||
<li><a href="/api/upload">/api/upload</a></li>
|
||||
</ul>
|
||||
`;
|
||||
});
|
||||
|
@@ -14,7 +14,7 @@ export function setRefreshTokenCookie(
|
||||
) {
|
||||
setCookie(event, 'jwt', refreshToken, {
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000,
|
||||
maxAge: 24 * 60 * 60, // unit: seconds
|
||||
sameSite: 'none',
|
||||
secure: true,
|
||||
});
|
||||
|
@@ -58,7 +58,7 @@ const dashboardMenus = [
|
||||
title: 'page.dashboard.title',
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/',
|
||||
path: '/dashboard',
|
||||
redirect: '/analytics',
|
||||
children: [
|
||||
{
|
||||
@@ -185,3 +185,206 @@ export const MOCK_MENUS = [
|
||||
username: 'jack',
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_MENU_LIST = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Workspace',
|
||||
status: 1,
|
||||
type: 'menu',
|
||||
icon: 'mdi:dashboard',
|
||||
path: '/workspace',
|
||||
component: '/dashboard/workspace/index',
|
||||
meta: {
|
||||
icon: 'carbon:workspace',
|
||||
title: 'page.dashboard.workspace',
|
||||
affixTab: true,
|
||||
order: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
meta: {
|
||||
icon: 'carbon:settings',
|
||||
order: 9997,
|
||||
title: 'system.title',
|
||||
badge: 'new',
|
||||
badgeType: 'normal',
|
||||
badgeVariants: 'primary',
|
||||
},
|
||||
status: 1,
|
||||
type: 'catalog',
|
||||
name: 'System',
|
||||
path: '/system',
|
||||
children: [
|
||||
{
|
||||
id: 201,
|
||||
pid: 2,
|
||||
path: '/system/menu',
|
||||
name: 'SystemMenu',
|
||||
authCode: 'System:Menu:List',
|
||||
status: 1,
|
||||
type: 'menu',
|
||||
meta: {
|
||||
icon: 'carbon:menu',
|
||||
title: 'system.menu.title',
|
||||
},
|
||||
component: '/system/menu/list',
|
||||
children: [
|
||||
{
|
||||
id: 20_101,
|
||||
pid: 201,
|
||||
name: 'SystemMenuCreate',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Menu:Create',
|
||||
meta: { title: 'common.create' },
|
||||
},
|
||||
{
|
||||
id: 20_102,
|
||||
pid: 201,
|
||||
name: 'SystemMenuEdit',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Menu:Edit',
|
||||
meta: { title: 'common.edit' },
|
||||
},
|
||||
{
|
||||
id: 20_103,
|
||||
pid: 201,
|
||||
name: 'SystemMenuDelete',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Menu:Delete',
|
||||
meta: { title: 'common.delete' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 202,
|
||||
pid: 2,
|
||||
path: '/system/dept',
|
||||
name: 'SystemDept',
|
||||
status: 1,
|
||||
type: 'menu',
|
||||
authCode: 'System:Dept:List',
|
||||
meta: {
|
||||
icon: 'carbon:container-services',
|
||||
title: 'system.dept.title',
|
||||
},
|
||||
component: '/system/dept/list',
|
||||
children: [
|
||||
{
|
||||
id: 20_401,
|
||||
pid: 201,
|
||||
name: 'SystemDeptCreate',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Dept:Create',
|
||||
meta: { title: 'common.create' },
|
||||
},
|
||||
{
|
||||
id: 20_402,
|
||||
pid: 201,
|
||||
name: 'SystemDeptEdit',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Dept:Edit',
|
||||
meta: { title: 'common.edit' },
|
||||
},
|
||||
{
|
||||
id: 20_403,
|
||||
pid: 201,
|
||||
name: 'SystemDeptDelete',
|
||||
status: 1,
|
||||
type: 'button',
|
||||
authCode: 'System:Dept:Delete',
|
||||
meta: { title: 'common.delete' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
order: 9998,
|
||||
title: 'demos.vben.title',
|
||||
icon: 'carbon:data-center',
|
||||
},
|
||||
name: 'Project',
|
||||
path: '/vben-admin',
|
||||
type: 'catalog',
|
||||
status: 1,
|
||||
children: [
|
||||
{
|
||||
id: 901,
|
||||
pid: 9,
|
||||
name: 'VbenDocument',
|
||||
path: '/vben-admin/document',
|
||||
component: 'IFrameView',
|
||||
type: 'embedded',
|
||||
status: 1,
|
||||
meta: {
|
||||
icon: 'carbon:book',
|
||||
iframeSrc: 'https://doc.vben.pro',
|
||||
title: 'demos.vben.document',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 902,
|
||||
pid: 9,
|
||||
name: 'VbenGithub',
|
||||
path: '/vben-admin/github',
|
||||
component: 'IFrameView',
|
||||
type: 'link',
|
||||
status: 1,
|
||||
meta: {
|
||||
icon: 'carbon:logo-github',
|
||||
link: 'https://github.com/vbenjs/vue-vben-admin',
|
||||
title: 'Github',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 903,
|
||||
pid: 9,
|
||||
name: 'VbenAntdv',
|
||||
path: '/vben-admin/antdv',
|
||||
component: 'IFrameView',
|
||||
type: 'link',
|
||||
status: 0,
|
||||
meta: {
|
||||
icon: 'carbon:hexagon-vertical-solid',
|
||||
badgeType: 'dot',
|
||||
link: 'https://ant.vben.pro',
|
||||
title: 'demos.vben.antdv',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
component: '_core/about/index',
|
||||
type: 'menu',
|
||||
status: 1,
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
order: 9999,
|
||||
title: 'demos.vben.about',
|
||||
},
|
||||
name: 'About',
|
||||
path: '/about',
|
||||
},
|
||||
];
|
||||
|
||||
export function getMenuIds(menus: any[]) {
|
||||
const ids: number[] = [];
|
||||
menus.forEach((item) => {
|
||||
ids.push(item.id);
|
||||
if (item.children && item.children.length > 0) {
|
||||
ids.push(...getMenuIds(item.children));
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
@@ -3,3 +3,6 @@ VITE_APP_TITLE=Plus Admin
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=vben-web-antd
|
||||
|
||||
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
|
||||
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
|
||||
|
@@ -15,7 +15,7 @@ VITE_INJECT_APP_LOADING=true
|
||||
# 打包后是否生成dist.zip
|
||||
VITE_ARCHIVER=true
|
||||
|
||||
# 后台请求路径 具体在vite.config.mts配置代理
|
||||
# 后端接口地址
|
||||
VITE_GLOB_API_URL=/prod-api
|
||||
|
||||
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
|
||||
|
35
apps/web-antd/.env.test
Normal file
35
apps/web-antd/.env.test
Normal file
@@ -0,0 +1,35 @@
|
||||
# 该文件是为了给一个增加环境变量打包的例子
|
||||
# 对应在根目录package.json -> build:antd:test 命令
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
# 是否开启压缩,可以设置为 none, brotli, gzip
|
||||
VITE_COMPRESS=gzip
|
||||
|
||||
# 是否开启 PWA
|
||||
VITE_PWA=false
|
||||
|
||||
# vue-router 的模式
|
||||
VITE_ROUTER_HISTORY=history
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
|
||||
# 打包后是否生成dist.zip
|
||||
VITE_ARCHIVER=true
|
||||
|
||||
# 后端接口地址
|
||||
VITE_GLOB_API_URL=/test-api
|
||||
|
||||
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
|
||||
VITE_GLOB_ENABLE_ENCRYPT=true
|
||||
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
|
||||
VITE_GLOB_RSA_PUBLIC_KEY=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==
|
||||
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
|
||||
VITE_GLOB_RSA_PRIVATE_KEY=MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=
|
||||
# 客户端id
|
||||
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
|
||||
|
||||
# 开启SSE
|
||||
VITE_GLOB_SSE_ENABLE=true
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.6",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm vite build --mode production",
|
||||
"build": "pnpm vite build",
|
||||
"build:analyze": "pnpm vite build --mode analyze",
|
||||
"dev": "pnpm vite --mode development",
|
||||
"preview": "vite preview",
|
||||
@@ -54,7 +54,8 @@
|
||||
"tinymce": "^7.3.0",
|
||||
"unplugin-vue-components": "^0.27.3",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
"vue-router": "catalog:",
|
||||
"vue3-colorpicker": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
|
@@ -3,51 +3,124 @@
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { h } from 'vue';
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Input,
|
||||
InputNumber,
|
||||
InputPassword,
|
||||
Mentions,
|
||||
notification,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
RangePicker,
|
||||
Rate,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Textarea,
|
||||
TimePicker,
|
||||
TreeSelect,
|
||||
Upload,
|
||||
} from 'ant-design-vue';
|
||||
import { notification } from 'ant-design-vue';
|
||||
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||
import { FileUploadOld, ImageUploadOld } from '#/components/upload-old';
|
||||
|
||||
const RichTextarea = defineAsyncComponent(() =>
|
||||
import('#/components/tinymce/index').then((res) => res.Tinymce),
|
||||
);
|
||||
|
||||
const FileUpload = defineAsyncComponent(() =>
|
||||
import('#/components/upload').then((res) => res.FileUpload),
|
||||
);
|
||||
|
||||
const ImageUpload = defineAsyncComponent(() =>
|
||||
import('#/components/upload').then((res) => res.ImageUpload),
|
||||
);
|
||||
|
||||
const AutoComplete = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/auto-complete'),
|
||||
);
|
||||
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
|
||||
const Checkbox = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/checkbox'),
|
||||
);
|
||||
const CheckboxGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
|
||||
);
|
||||
const DatePicker = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/date-picker'),
|
||||
);
|
||||
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
|
||||
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
|
||||
const InputNumber = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/input-number'),
|
||||
);
|
||||
const InputPassword = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.InputPassword),
|
||||
);
|
||||
const Mentions = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/mentions'),
|
||||
);
|
||||
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
|
||||
const RadioGroup = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
|
||||
);
|
||||
const RangePicker = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
|
||||
);
|
||||
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
|
||||
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
|
||||
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
|
||||
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
|
||||
const Textarea = defineAsyncComponent(() =>
|
||||
import('ant-design-vue/es/input').then((res) => res.Textarea),
|
||||
);
|
||||
const TimePicker = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/time-picker'),
|
||||
);
|
||||
const TreeSelect = defineAsyncComponent(
|
||||
() => import('ant-design-vue/es/tree-select'),
|
||||
);
|
||||
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
|
||||
|
||||
const withDefaultPlaceholder = <T extends Component>(
|
||||
component: T,
|
||||
type: 'input' | 'select',
|
||||
componentProps: Recordable<any> = {},
|
||||
) => {
|
||||
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
|
||||
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
|
||||
return h(component, { ...props, ...attrs, placeholder }, slots);
|
||||
};
|
||||
return defineComponent({
|
||||
name: component.name,
|
||||
inheritAttrs: false,
|
||||
setup: (props: any, { attrs, expose, slots }) => {
|
||||
const placeholder =
|
||||
props?.placeholder ||
|
||||
attrs?.placeholder ||
|
||||
$t(`ui.placeholder.${type}`);
|
||||
|
||||
// 透传组件暴露的方法
|
||||
const innerRef = ref();
|
||||
const publicApi: Recordable<any> = {};
|
||||
expose(publicApi);
|
||||
const instance = getCurrentInstance();
|
||||
instance?.proxy?.$nextTick(() => {
|
||||
for (const key in innerRef.value) {
|
||||
if (typeof innerRef.value[key] === 'function') {
|
||||
publicApi[key] = innerRef.value[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return () =>
|
||||
h(
|
||||
component,
|
||||
{
|
||||
...componentProps,
|
||||
placeholder,
|
||||
...props,
|
||||
...attrs,
|
||||
ref: innerRef,
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
@@ -61,8 +134,10 @@ export type ComponentType =
|
||||
| 'DefaultButton'
|
||||
| 'Divider'
|
||||
| 'FileUpload'
|
||||
| 'FileUploadOld'
|
||||
| 'IconPicker'
|
||||
| 'ImageUpload'
|
||||
| 'ImageUploadOld'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'InputPassword'
|
||||
@@ -87,38 +162,34 @@ async function initComponentAdapter() {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
ApiTreeSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
ApiSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
),
|
||||
ApiTreeSelect: withDefaultPlaceholder(
|
||||
{
|
||||
...ApiComponent,
|
||||
name: 'ApiTreeSelect',
|
||||
},
|
||||
'select',
|
||||
{
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
),
|
||||
AutoComplete,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
@@ -128,13 +199,11 @@ async function initComponentAdapter() {
|
||||
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
IconPicker: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
IconPicker,
|
||||
{ iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'addonAfter',
|
||||
inputComponent: Input,
|
||||
modelValueProp: 'value',
|
||||
}),
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
@@ -157,6 +226,8 @@ async function initComponentAdapter() {
|
||||
ImageUpload,
|
||||
FileUpload,
|
||||
RichTextarea,
|
||||
ImageUploadOld,
|
||||
FileUploadOld,
|
||||
};
|
||||
|
||||
// 将组件注册到全局共享状态中
|
||||
|
@@ -7,22 +7,6 @@ import { requestClient } from '#/api/request';
|
||||
*/
|
||||
export type AxiosProgressEvent = AxiosRequestConfig['onUploadProgress'];
|
||||
|
||||
/**
|
||||
* 通过单文件上传接口
|
||||
* @param file 上传的文件
|
||||
* @param onUploadProgress 上传进度事件 非必传
|
||||
* @returns 上传结果
|
||||
*/
|
||||
export function uploadApi(
|
||||
file: Blob | File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) {
|
||||
return requestClient.upload(
|
||||
'/resource/oss/upload',
|
||||
{ file },
|
||||
{ onUploadProgress, timeout: 60_000 },
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 默认上传结果
|
||||
*/
|
||||
@@ -31,3 +15,33 @@ export interface UploadResult {
|
||||
fileName: string;
|
||||
ossId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过单文件上传接口
|
||||
* @param file 上传的文件
|
||||
* @param options 一些配置项
|
||||
* @param options.onUploadProgress 上传进度事件
|
||||
* @param options.signal 上传取消信号
|
||||
* @param options.otherData 其他请求参数 后端拓展可能会用到
|
||||
* @returns 上传结果
|
||||
*/
|
||||
export function uploadApi(
|
||||
file: Blob | File,
|
||||
options?: {
|
||||
onUploadProgress?: AxiosProgressEvent;
|
||||
otherData?: Record<string, any>;
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
) {
|
||||
const { onUploadProgress, signal, otherData = {} } = options ?? {};
|
||||
return requestClient.upload<UploadResult>(
|
||||
'/resource/oss/upload',
|
||||
{ file, ...otherData },
|
||||
{ onUploadProgress, signal, timeout: 60_000 },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传api type
|
||||
*/
|
||||
export type UploadApi = typeof uploadApi;
|
||||
|
@@ -3,14 +3,14 @@ import { requestClient } from './request';
|
||||
/**
|
||||
* @description: contentType
|
||||
*/
|
||||
export enum ContentTypeEnum {
|
||||
export const ContentTypeEnum = {
|
||||
// form-data upload
|
||||
FORM_DATA = 'multipart/form-data;charset=UTF-8',
|
||||
FORM_DATA: 'multipart/form-data;charset=UTF-8',
|
||||
// form-data qs
|
||||
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
FORM_URLENCODED: 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
// json
|
||||
JSON = 'application/json;charset=UTF-8',
|
||||
}
|
||||
JSON: 'application/json;charset=UTF-8',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 通用下载接口 封装一层
|
||||
|
@@ -9,4 +9,5 @@ export interface LoginLog {
|
||||
os: string;
|
||||
msg: string;
|
||||
loginTime: string;
|
||||
clientKey: string;
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ export interface OperationLog {
|
||||
operId: string;
|
||||
tenantId: string;
|
||||
title: string;
|
||||
businessType: number;
|
||||
businessType: string;
|
||||
businessTypes?: any;
|
||||
method: string;
|
||||
requestMethod: string;
|
||||
@@ -14,7 +14,7 @@ export interface OperationLog {
|
||||
operLocation: string;
|
||||
operParam: string;
|
||||
jsonResult: string;
|
||||
status: number;
|
||||
status: string;
|
||||
errorMsg: string;
|
||||
operTime: string;
|
||||
costTime: number;
|
||||
|
@@ -93,9 +93,12 @@ function createRequestClient(baseURL: string) {
|
||||
const language = preferences.app.locale.replace('-', '_');
|
||||
config.headers['Accept-Language'] = language;
|
||||
config.headers['Content-Language'] = language;
|
||||
// 添加全局clientId
|
||||
config.headers.clientId = clientId;
|
||||
|
||||
/**
|
||||
* 添加全局clientId
|
||||
* 关于header的clientId被错误绑定到实体类
|
||||
* https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IC0BDS
|
||||
*/
|
||||
config.headers.ClientID = clientId;
|
||||
/**
|
||||
* 格式化get/delete参数
|
||||
* 如果包含自定义的paramsSerializer则不走此逻辑
|
||||
@@ -225,7 +228,7 @@ function createRequestClient(baseURL: string) {
|
||||
case 401: {
|
||||
// 已经在登出过程中 不再执行
|
||||
if (isLogoutProcessing) {
|
||||
return;
|
||||
throw new Error(timeoutMsg);
|
||||
}
|
||||
isLogoutProcessing = true;
|
||||
const _msg = $t('http.loginTimeout');
|
||||
@@ -235,7 +238,7 @@ function createRequestClient(baseURL: string) {
|
||||
isLogoutProcessing = false;
|
||||
});
|
||||
// 不再执行下面逻辑
|
||||
return;
|
||||
throw new Error(_msg);
|
||||
}
|
||||
default: {
|
||||
if (msg) {
|
||||
|
@@ -59,7 +59,11 @@ export function clientUpdate(data: Partial<Client>) {
|
||||
* @param data 状态
|
||||
*/
|
||||
export function clientChangeStatus(data: any) {
|
||||
return requestClient.putWithMsg<void>(Api.clientChangeStatus, data);
|
||||
const requestData = {
|
||||
clientId: data.clientId,
|
||||
status: data.status,
|
||||
};
|
||||
return requestClient.putWithMsg<void>(Api.clientChangeStatus, requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -37,5 +37,10 @@ export function ossConfigRemove(ossConfigIds: IDS) {
|
||||
|
||||
// 更改OSS配置的状态
|
||||
export function ossConfigChangeStatus(data: any) {
|
||||
return requestClient.putWithMsg(Api.ossConfigChangeStatus, data);
|
||||
const requestData = {
|
||||
ossConfigId: data.ossConfigId,
|
||||
status: data.status,
|
||||
configKey: data.configKey,
|
||||
};
|
||||
return requestClient.putWithMsg(Api.ossConfigChangeStatus, requestData);
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ export function ossList(params?: PageQuery) {
|
||||
* @param ossIds id数组
|
||||
* @returns 信息数组
|
||||
*/
|
||||
export function ossInfo(ossIds: IDS) {
|
||||
export function ossInfo(ossIds: ID | IDS) {
|
||||
return requestClient.get<OssFile[]>(`${Api.ossInfo}/${ossIds}`);
|
||||
}
|
||||
|
||||
|
@@ -72,7 +72,11 @@ export function roleUpdate(data: Partial<Role>) {
|
||||
* @returns void
|
||||
*/
|
||||
export function roleChangeStatus(data: Partial<Role>) {
|
||||
return requestClient.putWithMsg<void>(Api.roleChangeStatus, data);
|
||||
const requestData = {
|
||||
roleId: data.roleId,
|
||||
status: data.status,
|
||||
};
|
||||
return requestClient.putWithMsg<void>(Api.roleChangeStatus, requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -74,7 +74,11 @@ export function packageUpdate(data: Partial<TenantPackage>) {
|
||||
* @returns void
|
||||
*/
|
||||
export function packageChangeStatus(data: Partial<TenantPackage>) {
|
||||
return requestClient.putWithMsg<void>(Api.packageChangeStatus, data);
|
||||
const packageId = {
|
||||
packageId: data.packageId,
|
||||
status: data.status,
|
||||
};
|
||||
return requestClient.putWithMsg<void>(Api.packageChangeStatus, packageId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -67,7 +67,12 @@ export function tenantUpdate(data: Partial<Tenant>) {
|
||||
* @returns void
|
||||
*/
|
||||
export function tenantStatusChange(data: Partial<Tenant>) {
|
||||
return requestClient.putWithMsg(Api.tenantStatus, data);
|
||||
const requestData = {
|
||||
id: data.id,
|
||||
tenantId: data.tenantId,
|
||||
status: data.status,
|
||||
};
|
||||
return requestClient.putWithMsg(Api.tenantStatus, requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -110,7 +110,11 @@ export function userUpdate(data: Partial<User>) {
|
||||
* @returns void
|
||||
*/
|
||||
export function userStatusChange(data: Partial<User>) {
|
||||
return requestClient.putWithMsg<void>(Api.userStatusChange, data);
|
||||
const requestData = {
|
||||
userId: data.userId,
|
||||
status: data.status,
|
||||
};
|
||||
return requestClient.putWithMsg<void>(Api.userStatusChange, requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
1
apps/web-antd/src/api/system/user/model.d.ts
vendored
1
apps/web-antd/src/api/system/user/model.d.ts
vendored
@@ -66,6 +66,7 @@ export interface User {
|
||||
roleIds?: string[];
|
||||
postIds?: number[];
|
||||
roleId: string;
|
||||
deptName: string;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
|
@@ -77,8 +77,8 @@ export function genDownload(tableId: ID) {
|
||||
}
|
||||
|
||||
// 生成代码(自定义路径)
|
||||
export function genDownloadWithPath(tableId: ID) {
|
||||
return requestClient.get(`${Api.download}/${tableId}`);
|
||||
export function genWithPath(tableId: ID) {
|
||||
return requestClient.get<void>(`${Api.genCode}/${tableId}`);
|
||||
}
|
||||
|
||||
// 同步数据库
|
||||
|
1
apps/web-antd/src/api/tool/gen/model.d.ts
vendored
1
apps/web-antd/src/api/tool/gen/model.d.ts
vendored
@@ -177,6 +177,7 @@ export interface Info {
|
||||
// 树表需要添加此属性
|
||||
params?: any;
|
||||
popupComponent?: string;
|
||||
formComponent?: string;
|
||||
}
|
||||
|
||||
export interface GenInfo {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
CompleteTaskReqData,
|
||||
NextNodeInfo,
|
||||
StartWorkFlowReqData,
|
||||
TaskInfo,
|
||||
TaskOperationData,
|
||||
@@ -156,3 +157,16 @@ export function getBackTaskNode(definitionId: string, nodeCode: string) {
|
||||
export function currentTaskAllUser(taskId: ID) {
|
||||
return requestClient.get<any>(`/workflow/task/currentTaskAllUser/${taskId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一节点
|
||||
* @param data data
|
||||
* @param data.taskId taskId
|
||||
* @returns NextNodeInfo
|
||||
*/
|
||||
export function getNextNodeList(data: { taskId: string }) {
|
||||
return requestClient.post<NextNodeInfo[]>(
|
||||
'/workflow/task/getNextNodeList',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
34
apps/web-antd/src/api/workflow/task/model.d.ts
vendored
34
apps/web-antd/src/api/workflow/task/model.d.ts
vendored
@@ -1,3 +1,9 @@
|
||||
export interface ButtonWithPermission {
|
||||
code: string;
|
||||
value: null | string;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export interface TaskInfo {
|
||||
id: string;
|
||||
categoryName: string;
|
||||
@@ -28,6 +34,7 @@ export interface TaskInfo {
|
||||
createBy: string;
|
||||
createByName: string;
|
||||
targetNodeName?: string;
|
||||
buttonList: ButtonWithPermission[];
|
||||
}
|
||||
|
||||
export interface CompleteTaskReqData {
|
||||
@@ -38,6 +45,8 @@ export interface CompleteTaskReqData {
|
||||
variables: any;
|
||||
// 附件ID 1,2,3,4形式
|
||||
fileId?: string;
|
||||
// 选人 key为节点code value为用户ID join(,)
|
||||
assigneeMap: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface StartWorkFlowReqData {
|
||||
@@ -72,3 +81,28 @@ export type TaskOperationType =
|
||||
| 'delegateTask'
|
||||
| 'reductionSignature'
|
||||
| 'transferTask';
|
||||
|
||||
export interface NextNodeInfo {
|
||||
skipList: string[];
|
||||
id: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
tenantId: string;
|
||||
delFlag: string;
|
||||
nodeType: number;
|
||||
definitionId: string;
|
||||
nodeCode: string;
|
||||
nodeName: string;
|
||||
permissionFlag: string;
|
||||
nodeRatio: string;
|
||||
coordinate: string;
|
||||
version: string;
|
||||
anyNodeSkip: any;
|
||||
listenerType: any;
|
||||
listenerPath: any;
|
||||
handlerType: any;
|
||||
handlerPath: any;
|
||||
formCustom: string;
|
||||
formPath: any;
|
||||
ext: string;
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { createApp, watchEffect } from 'vue';
|
||||
|
||||
import { registerAccessDirective } from '@vben/access';
|
||||
import { initTippy } from '@vben/common-ui';
|
||||
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
@@ -33,6 +33,11 @@ async function bootstrap(namespace: string) {
|
||||
|
||||
// 全局组件
|
||||
setupGlobalComponent(app);
|
||||
// 注册v-loading指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
|
||||
spinning: 'spinning',
|
||||
});
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
@@ -44,11 +49,16 @@ async function bootstrap(namespace: string) {
|
||||
registerAccessDirective(app);
|
||||
|
||||
// 初始化 tippy
|
||||
const { initTippy } = await import('@vben/common-ui/es/tippy');
|
||||
initTippy(app);
|
||||
|
||||
// 配置路由及路由守卫
|
||||
app.use(router);
|
||||
|
||||
// 配置Motion插件
|
||||
const { MotionPlugin } = await import('@vben/plugins/motion');
|
||||
app.use(MotionPlugin);
|
||||
|
||||
// 动态更新标题
|
||||
watchEffect(() => {
|
||||
if (preferences.app.dynamicTitle) {
|
||||
|
@@ -84,6 +84,10 @@ function handleReady(cropperInstance: Cropper) {
|
||||
modalLoading(false);
|
||||
}
|
||||
|
||||
function handleReadyError() {
|
||||
modalLoading(false);
|
||||
}
|
||||
|
||||
function handlerToolbar(event: string, arg?: number) {
|
||||
if (event === 'scaleX') {
|
||||
scaleX = arg = scaleX === -1 ? 1 : -1;
|
||||
@@ -128,9 +132,11 @@ async function handleOk() {
|
||||
v-if="src"
|
||||
:circled="circled"
|
||||
:src="src"
|
||||
crossorigin="anonymous"
|
||||
height="300px"
|
||||
@cropend="handleCropend"
|
||||
@ready="handleReady"
|
||||
@ready-error="handleReadyError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -322,7 +328,8 @@ async function handleOk() {
|
||||
&-cropper {
|
||||
height: 300px;
|
||||
background: #eee;
|
||||
background-image: linear-gradient(
|
||||
background-image:
|
||||
linear-gradient(
|
||||
45deg,
|
||||
rgb(0 0 0 / 25%) 25%,
|
||||
transparent 0,
|
||||
|
@@ -26,14 +26,16 @@ const props = defineProps({
|
||||
src: { required: true, type: String },
|
||||
});
|
||||
|
||||
const emit = defineEmits(['cropend', 'ready', 'cropendError']);
|
||||
const emit = defineEmits(['cropend', 'ready', 'cropendError', 'readyError']);
|
||||
|
||||
const defaultOptions: Options = {
|
||||
aspectRatio: 1,
|
||||
autoCrop: true,
|
||||
background: true,
|
||||
center: true,
|
||||
checkCrossOrigin: true,
|
||||
// 需要设置为false 否则会自动拼接timestamp 导致私有桶sign错误
|
||||
// 需要配合img crossorigin='anonymous'使用(默认已经做了处理)
|
||||
checkCrossOrigin: false,
|
||||
checkOrientation: true,
|
||||
cropBoxMovable: true,
|
||||
cropBoxResizable: true,
|
||||
@@ -94,6 +96,15 @@ async function init() {
|
||||
if (!imgEl) {
|
||||
return;
|
||||
}
|
||||
// 判断是否为正常访问的图片
|
||||
try {
|
||||
const resp = await fetch(props.src);
|
||||
if (resp.status !== 200) {
|
||||
emit('readyError');
|
||||
}
|
||||
} catch {
|
||||
emit('readyError');
|
||||
}
|
||||
cropper.value = new Cropper(imgEl, {
|
||||
...defaultOptions,
|
||||
crop() {
|
||||
|
@@ -34,6 +34,9 @@ const props = {
|
||||
useCollapse: { default: true, type: Boolean },
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated 使用antd原生组件替代 下个版本将会移除
|
||||
*/
|
||||
export default defineComponent({
|
||||
emits: ['register'],
|
||||
// eslint-disable-next-line vue/order-in-components
|
||||
|
@@ -6,6 +6,9 @@ import type {
|
||||
|
||||
import { getCurrentInstance, ref, unref } from 'vue';
|
||||
|
||||
/**
|
||||
* @deprecated 使用antd原生组件替代 下个版本将会移除
|
||||
*/
|
||||
export function useDescription(
|
||||
props?: Partial<DescriptionProps>,
|
||||
): UseDescReturnType {
|
||||
|
@@ -4,7 +4,7 @@ import type { DictData } from '#/api/system/dict/dict-data-model';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Tag } from 'ant-design-vue';
|
||||
import { Spin, Tag } from 'ant-design-vue';
|
||||
|
||||
import { tagTypes } from './data';
|
||||
|
||||
@@ -41,12 +41,22 @@ const label = computed<number | string>(() => {
|
||||
});
|
||||
|
||||
const tagComponent = computed(() => (color.value ? Tag : 'div'));
|
||||
|
||||
const loading = computed(() => {
|
||||
return props.dicts?.length === 0;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<component :is="tagComponent" :class="cssClass" :color="color">
|
||||
<component
|
||||
v-if="!loading"
|
||||
:is="tagComponent"
|
||||
:class="cssClass"
|
||||
:color="color"
|
||||
>
|
||||
{{ label }}
|
||||
</component>
|
||||
<Spin v-else :spinning="true" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -1,84 +1,134 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { Switch } from 'ant-design-vue';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Modal, Switch } from 'ant-design-vue';
|
||||
import { isFunction } from 'lodash-es';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TableSwitch',
|
||||
components: {
|
||||
Switch,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [Boolean, String, Number],
|
||||
default: false,
|
||||
},
|
||||
checkedText: {
|
||||
type: String,
|
||||
default: '启用',
|
||||
},
|
||||
unCheckedText: {
|
||||
type: String,
|
||||
default: '禁用',
|
||||
},
|
||||
// 使用严格相等判断 类型要正确
|
||||
checkedValue: {
|
||||
type: [Boolean, String, Number],
|
||||
default: '0',
|
||||
},
|
||||
unCheckedValue: {
|
||||
type: [Boolean, String, Number],
|
||||
default: '1',
|
||||
},
|
||||
api: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
reload: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
type CheckedType = boolean | number | string;
|
||||
async function onChange(checked: CheckedType, e: Event) {
|
||||
// 阻止事件冒泡 否则会跟行选中冲突
|
||||
e.stopPropagation();
|
||||
const { checkedValue, unCheckedValue } = props;
|
||||
// 原本的状态
|
||||
const lastStatus =
|
||||
checked === checkedValue ? unCheckedValue : checkedValue;
|
||||
// 切换状态
|
||||
emit('update:modelValue', checked);
|
||||
const { api, reload } = props;
|
||||
type CheckedType = boolean | number | string;
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* 选中的文本
|
||||
* @default i18n 启用
|
||||
*/
|
||||
checkedText?: string;
|
||||
/**
|
||||
* 未选中的文本
|
||||
* @default i18n 禁用
|
||||
*/
|
||||
unCheckedText?: string;
|
||||
checkedValue?: CheckedType;
|
||||
unCheckedValue?: CheckedType;
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* 需要自己在内部处理更新的逻辑 因为status已经双向绑定了 可以直接获取
|
||||
*/
|
||||
api: () => PromiseLike<void>;
|
||||
/**
|
||||
* 更新前是否弹窗确认
|
||||
* @default false
|
||||
*/
|
||||
confirm?: boolean;
|
||||
/**
|
||||
* 对应的提示内容
|
||||
* @param checked 选中的值(更新后的值)
|
||||
* @default string '确认要更新状态吗?'
|
||||
*/
|
||||
confirmText?: (checked: CheckedType) => string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
checkedText: undefined,
|
||||
unCheckedText: undefined,
|
||||
checkedValue: '0',
|
||||
unCheckedValue: '1',
|
||||
confirm: false,
|
||||
confirmText: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
// 修改为computed 支持语言切换
|
||||
const checkedTextComputed = computed(() => {
|
||||
return props.checkedText ?? $t('pages.common.enable');
|
||||
});
|
||||
|
||||
const unCheckedTextComputed = computed(() => {
|
||||
return props.unCheckedText ?? $t('pages.common.disable');
|
||||
});
|
||||
|
||||
const currentChecked = defineModel<CheckedType>('value', {
|
||||
default: false,
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
function confirmUpdate(checked: CheckedType, lastStatus: CheckedType) {
|
||||
const content = isFunction(props.confirmText)
|
||||
? props.confirmText(checked)
|
||||
: `确认要更新状态吗?`;
|
||||
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { api } = props;
|
||||
isFunction(api) && (await api());
|
||||
isFunction(reload) && (await reload());
|
||||
emit('reload');
|
||||
} catch {
|
||||
emit('update:modelValue', lastStatus);
|
||||
currentChecked.value = lastStatus;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
currentChecked.value = lastStatus;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleChange(checked: CheckedType, e: Event) {
|
||||
// 阻止事件冒泡 否则会跟行选中冲突
|
||||
e.stopPropagation();
|
||||
const { checkedValue, unCheckedValue } = props;
|
||||
// 原本的状态
|
||||
const lastStatus = checked === checkedValue ? unCheckedValue : checkedValue;
|
||||
// 切换状态
|
||||
currentChecked.value = checked;
|
||||
const { api } = props;
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
if (props.confirm) {
|
||||
confirmUpdate(checked, lastStatus);
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
onChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
isFunction(api) && (await api());
|
||||
emit('reload');
|
||||
} catch {
|
||||
currentChecked.value = lastStatus;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Switch
|
||||
v-bind="$attrs"
|
||||
:checked="modelValue"
|
||||
:checked-children="checkedText"
|
||||
:loading="loading"
|
||||
:disabled="disabled"
|
||||
:checked="currentChecked"
|
||||
:checked-children="checkedTextComputed"
|
||||
:checked-value="checkedValue"
|
||||
:un-checked-children="unCheckedText"
|
||||
:un-checked-children="unCheckedTextComputed"
|
||||
:un-checked-value="unCheckedValue"
|
||||
@change="onChange"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</template>
|
||||
|
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { MessageType } from 'ant-design-vue/es/message';
|
||||
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select';
|
||||
|
||||
import type { TenantOption } from '#/api';
|
||||
|
||||
import { computed, onMounted, ref, unref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { computed, onMounted, ref, shallowRef, unref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { DEFAULT_HOME_PATH } from '@vben/constants';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { message, Select } from 'ant-design-vue';
|
||||
import { message, Select, Spin } from 'ant-design-vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { tenantDynamicClear, tenantDynamicToggle } from '#/api/system/tenant';
|
||||
@@ -35,24 +35,39 @@ const showToggle = computed<boolean>(() => {
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
// 没有超级管理员权限 不会调用接口
|
||||
if (!hasAccessByRoles(['superadmin'])) {
|
||||
return;
|
||||
}
|
||||
await initTenant();
|
||||
});
|
||||
|
||||
const { closeAllTabs } = useTabs();
|
||||
const router = useRouter();
|
||||
function close(checked: boolean) {
|
||||
const route = useRoute();
|
||||
const { closeOtherTabs, refreshTab, closeAllTabs } = useTabs();
|
||||
|
||||
async function close(checked: boolean) {
|
||||
// store设置状态
|
||||
setChecked(checked);
|
||||
// 需要关闭全部标签页
|
||||
closeAllTabs();
|
||||
// 切换完加载首页
|
||||
router.push(DEFAULT_HOME_PATH);
|
||||
|
||||
/**
|
||||
* 切换租户需要回到首页的页面 一般为带id的页面
|
||||
* 其他则直接刷新页面
|
||||
*/
|
||||
if (route.meta.requireHomeRedirect) {
|
||||
await closeAllTabs();
|
||||
} else {
|
||||
// 先关闭再刷新 这里不用Promise.all()
|
||||
await closeOtherTabs();
|
||||
await refreshTab();
|
||||
}
|
||||
}
|
||||
|
||||
const dictStore = useDictStore();
|
||||
// 用于清理上一条message
|
||||
const messageInstance = shallowRef<MessageType | null>();
|
||||
// loading加载中效果
|
||||
const loading = ref(false);
|
||||
|
||||
/**
|
||||
* 选中租户的处理
|
||||
* @param tenantId tenantId
|
||||
@@ -63,23 +78,46 @@ const onSelected: SelectHandler = async (tenantId: string, option: any) => {
|
||||
// createMessage.info('选择一致');
|
||||
return;
|
||||
}
|
||||
await tenantDynamicToggle(tenantId);
|
||||
lastSelected.value = tenantId;
|
||||
message.success(
|
||||
`${$t('component.tenantToggle.switch')} ${option.companyName}`,
|
||||
);
|
||||
close(true);
|
||||
// 需要放在宏队列处理 直接清空页面由于没有字典会有样式问题(标签变成unknown)
|
||||
setTimeout(() => dictStore.resetCache());
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
await tenantDynamicToggle(tenantId);
|
||||
lastSelected.value = tenantId;
|
||||
|
||||
// 关闭之前的message 只保留一条
|
||||
messageInstance.value?.();
|
||||
messageInstance.value = message.success(
|
||||
`${$t('component.tenantToggle.switch')} ${option.companyName}`,
|
||||
);
|
||||
|
||||
close(true);
|
||||
// 需要放在宏队列处理 直接清空页面由于没有字典会有样式问题(标签变成unknown)
|
||||
setTimeout(() => dictStore.resetCache());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
async function onDeselect() {
|
||||
await tenantDynamicClear();
|
||||
message.success($t('component.tenantToggle.reset'));
|
||||
lastSelected.value = '';
|
||||
close(false);
|
||||
// 需要放在宏队列处理 直接清空页面由于没有字典会有样式问题(标签变成unknown)
|
||||
setTimeout(() => dictStore.resetCache());
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
await tenantDynamicClear();
|
||||
// 关闭之前的message 只保留一条
|
||||
messageInstance.value?.();
|
||||
messageInstance.value = message.success($t('component.tenantToggle.reset'));
|
||||
|
||||
lastSelected.value = '';
|
||||
close(false);
|
||||
// 需要放在宏队列处理 直接清空页面由于没有字典会有样式问题(标签变成unknown)
|
||||
setTimeout(() => dictStore.resetCache());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,18 +134,20 @@ function filterOption(input: string, option: TenantOption) {
|
||||
<div v-if="showToggle" class="mr-[8px] hidden md:block">
|
||||
<Select
|
||||
v-model:value="selected"
|
||||
:disabled="loading"
|
||||
:field-names="{ label: 'companyName', value: 'tenantId' }"
|
||||
:filter-option="filterOption"
|
||||
:options="tenantList"
|
||||
:placeholder="$t('component.tenantToggle.placeholder')"
|
||||
:dropdown-style="{ position: 'fixed', zIndex: 1024 }"
|
||||
allow-clear
|
||||
class="w-60"
|
||||
show-search
|
||||
@deselect="onDeselect"
|
||||
@select="onSelected"
|
||||
>
|
||||
<template #suffixIcon>
|
||||
<span class="icon-mdi--company"></span>
|
||||
<template v-if="loading" #suffixIcon>
|
||||
<Spin size="small" spinning />
|
||||
</template>
|
||||
</Select>
|
||||
</div>
|
||||
|
@@ -1,80 +1,50 @@
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import type { IPropTypes } from '@tinymce/tinymce-vue/lib/cjs/main/ts/components/EditorPropTypes';
|
||||
import type { Editor as EditorType } from 'tinymce/tinymce';
|
||||
|
||||
import type { PropType } from 'vue';
|
||||
import type { AxiosProgressEvent, UploadResult } from '#/api';
|
||||
|
||||
import type { UploadResult } from '#/api/core/upload';
|
||||
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onActivated,
|
||||
onBeforeUnmount,
|
||||
onDeactivated,
|
||||
onMounted,
|
||||
ref,
|
||||
unref,
|
||||
useAttrs,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import { computed, nextTick, ref, shallowRef, useAttrs, watch } from 'vue';
|
||||
|
||||
import { preferences, usePreferences } from '@vben/preferences';
|
||||
import { buildShortUUID } from '@vben/utils';
|
||||
|
||||
import Editor from '@tinymce/tinymce-vue';
|
||||
import { isNumber } from 'lodash-es';
|
||||
import { Spin } from 'ant-design-vue';
|
||||
import { camelCase } from 'lodash-es';
|
||||
|
||||
import { uploadApi } from '#/api/core/upload';
|
||||
|
||||
import { bindHandlers } from './helper';
|
||||
import ImgUpload from './img-upload.vue';
|
||||
import { uploadApi } from '#/api';
|
||||
import {
|
||||
plugins as defaultPlugins,
|
||||
toolbar as defaultToolbar,
|
||||
} from './tinymce';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
default: 400,
|
||||
required: false,
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
},
|
||||
options: {
|
||||
default: () => ({}),
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
type: Object as PropType<Partial<InitOptions>>,
|
||||
},
|
||||
|
||||
plugins: {
|
||||
default: defaultPlugins,
|
||||
type: String,
|
||||
},
|
||||
showImageUpload: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
toolbar: {
|
||||
default: defaultToolbar,
|
||||
type: String,
|
||||
},
|
||||
width: {
|
||||
default: 'auto',
|
||||
required: false,
|
||||
type: [Number, String] as PropType<number | string>,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
} from '#/components/tinymce/src/tinymce';
|
||||
|
||||
type InitOptions = IPropTypes['init'];
|
||||
|
||||
/**
|
||||
* 外部使用 v-model 绑定值
|
||||
*/
|
||||
const modelValue = defineModel('modelValue', { default: '', type: String });
|
||||
interface Props {
|
||||
height?: number | string;
|
||||
options?: Partial<InitOptions>;
|
||||
plugins?: string;
|
||||
toolbar?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'Tinymce',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 400,
|
||||
options: () => ({}),
|
||||
plugins: defaultPlugins,
|
||||
toolbar: defaultToolbar,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
mounted: [];
|
||||
}>();
|
||||
|
||||
/**
|
||||
* https://www.jianshu.com/p/59a9c3802443
|
||||
* 使用自托管方案(本地)代替cdn 没有key的限制
|
||||
@@ -82,21 +52,13 @@ const modelValue = defineModel('modelValue', { default: '', type: String });
|
||||
*/
|
||||
const tinymceScriptSrc = `${import.meta.env.VITE_BASE}tinymce/tinymce.min.js`;
|
||||
|
||||
const attrs = useAttrs();
|
||||
const editorRef = ref<EditorType>();
|
||||
const fullscreen = ref(false);
|
||||
const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
|
||||
const elRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const containerWidth = computed(() => {
|
||||
const width = props.width;
|
||||
if (isNumber(width)) {
|
||||
return `${width}px`;
|
||||
}
|
||||
return width;
|
||||
const content = defineModel<string>('modelValue', {
|
||||
default: '',
|
||||
});
|
||||
|
||||
const { isDark } = usePreferences();
|
||||
const editorRef = shallowRef<EditorType | null>(null);
|
||||
|
||||
const { isDark, locale } = usePreferences();
|
||||
const skinName = computed(() => {
|
||||
return isDark.value ? 'oxide-dark' : 'oxide';
|
||||
});
|
||||
@@ -105,30 +67,6 @@ const contentCss = computed(() => {
|
||||
return isDark.value ? 'dark' : 'default';
|
||||
});
|
||||
|
||||
/**
|
||||
* 通过v-if来挂载/卸载组件
|
||||
* 来完成主题切换/语言切换
|
||||
*/
|
||||
const init = ref(true);
|
||||
watch(
|
||||
() => [preferences.theme.mode, preferences.app.locale],
|
||||
() => {
|
||||
if (!editorRef.value) {
|
||||
return;
|
||||
}
|
||||
destroy();
|
||||
init.value = false;
|
||||
// 放在下一次tick来切换
|
||||
// 需要先加载组件 也就是v-if为true 然后需要拿到editorRef 必须放在setTimeout(相当于onMounted)
|
||||
nextTick(() => {
|
||||
init.value = true;
|
||||
setTimeout(() => {
|
||||
setEditorMode();
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* tinymce支持 en zh_CN
|
||||
*/
|
||||
@@ -140,6 +78,26 @@ const langName = computed(() => {
|
||||
return 'zh_CN';
|
||||
});
|
||||
|
||||
/**
|
||||
* 通过v-if来挂载/卸载组件来完成主题切换切换
|
||||
* 语言切换也需要监听 不监听在切换时候会显示原始<textarea>样式
|
||||
*/
|
||||
const init = ref(true);
|
||||
watch([isDark, locale], async () => {
|
||||
if (!editorRef.value) {
|
||||
return;
|
||||
}
|
||||
// 相当于手动unmounted清理 非常重要
|
||||
editorRef.value.destroy();
|
||||
init.value = false;
|
||||
// 放在下一次tick来切换
|
||||
// 需要先加载组件 也就是v-if为true 然后需要拿到editorRef 必须放在setTimeout(相当于onMounted)
|
||||
await nextTick();
|
||||
init.value = true;
|
||||
});
|
||||
|
||||
// 加载完毕前显示spin
|
||||
const loading = ref(true);
|
||||
const initOptions = computed((): InitOptions => {
|
||||
const { height, options, plugins, toolbar } = props;
|
||||
return {
|
||||
@@ -163,6 +121,7 @@ const initOptions = computed((): InitOptions => {
|
||||
* images_upload_handler启用时为上传
|
||||
*/
|
||||
paste_data_images: true,
|
||||
images_file_types: 'jpeg,jpg,png,gif,bmp,webp',
|
||||
plugins,
|
||||
quickbars_selection_toolbar:
|
||||
'bold italic | quicklink h2 h3 blockquote quickimage quicktable',
|
||||
@@ -175,12 +134,18 @@ const initOptions = computed((): InitOptions => {
|
||||
* @param blobInfo
|
||||
* 大坑 不要调用这两个函数 success failure:
|
||||
* 使用resolve/reject代替
|
||||
* (PS: 新版已经没有success failure)
|
||||
*/
|
||||
images_upload_handler: (blobInfo) => {
|
||||
images_upload_handler: (blobInfo, progress) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = blobInfo.blob();
|
||||
// const filename = blobInfo.filename();
|
||||
uploadApi(file)
|
||||
// 进度条事件
|
||||
const progressEvent: AxiosProgressEvent = (e) => {
|
||||
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
||||
progress(percent);
|
||||
};
|
||||
uploadApi(file, { onUploadProgress: progressEvent })
|
||||
.then((response) => {
|
||||
const { url } = response as unknown as UploadResult;
|
||||
console.log('tinymce上传图片:', url);
|
||||
@@ -188,186 +153,70 @@ const initOptions = computed((): InitOptions => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('tinymce上传图片失败:', error);
|
||||
reject(error.message);
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
reject({ message: error.message, remove: true });
|
||||
});
|
||||
});
|
||||
},
|
||||
setup: (editor) => {
|
||||
editorRef.value = editor;
|
||||
editor.on('init', (e) => initSetup(e));
|
||||
editor.on('init', () => {
|
||||
emit('mounted');
|
||||
loading.value = false;
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const attrs = useAttrs();
|
||||
/**
|
||||
* 监听options.readonly
|
||||
* 获取透传的事件 通过v-on绑定
|
||||
* 可绑定的事件 https://www.tiny.cloud/docs/tinymce/latest/vue-ref/#event-binding
|
||||
*/
|
||||
watch(
|
||||
() => props.options,
|
||||
(options) => {
|
||||
const getDisabled = options && Reflect.get(options, 'readonly');
|
||||
const editor = unref(editorRef);
|
||||
if (editor) {
|
||||
editor.mode.set(getDisabled ? 'readonly' : 'design');
|
||||
const events = computed(() => {
|
||||
const onEvents: Record<string, any> = {};
|
||||
for (const key in attrs) {
|
||||
if (key.startsWith('on')) {
|
||||
const eventKey = camelCase(key.split('on')[1]!);
|
||||
onEvents[eventKey] = attrs[key];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (!initOptions.value.inline) {
|
||||
tinymceId.value = buildShortUUID('tiny-vue');
|
||||
}
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
initEditor();
|
||||
setEditorMode();
|
||||
}, 30);
|
||||
});
|
||||
return onEvents;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroy();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
destroy();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
setEditorMode();
|
||||
});
|
||||
|
||||
function setEditorMode() {
|
||||
const editor = unref(editorRef);
|
||||
if (editor) {
|
||||
const mode = props.options.readonly ? 'readonly' : 'design';
|
||||
editor.mode.set(mode);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
const editor = unref(editorRef);
|
||||
editor?.destroy();
|
||||
}
|
||||
|
||||
function initEditor() {
|
||||
const el = unref(elRef);
|
||||
if (el) {
|
||||
el.style.visibility = '';
|
||||
}
|
||||
}
|
||||
|
||||
function initSetup(e: any) {
|
||||
const editor = unref(editorRef);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const value = modelValue.value || '';
|
||||
|
||||
editor.setContent(value);
|
||||
bindModelHandlers(editor);
|
||||
bindHandlers(e, attrs, unref(editorRef));
|
||||
}
|
||||
|
||||
function setValue(editor: Record<string, any>, val?: string, prevVal?: string) {
|
||||
if (
|
||||
editor &&
|
||||
typeof val === 'string' &&
|
||||
val !== prevVal &&
|
||||
val !== editor.getContent({ format: attrs.outputFormat })
|
||||
) {
|
||||
editor.setContent(val);
|
||||
}
|
||||
}
|
||||
|
||||
function bindModelHandlers(editor: any) {
|
||||
const modelEvents = attrs.modelEvents ?? null;
|
||||
const normalizedEvents = Array.isArray(modelEvents)
|
||||
? modelEvents.join(' ')
|
||||
: modelEvents;
|
||||
|
||||
watch(
|
||||
() => modelValue.value,
|
||||
(val, prevVal) => {
|
||||
setValue(editor, val, prevVal);
|
||||
},
|
||||
);
|
||||
|
||||
editor.on(normalizedEvents || 'change keyup undo redo', () => {
|
||||
const content = editor.getContent({ format: attrs.outputFormat });
|
||||
emit('change', content);
|
||||
});
|
||||
|
||||
editor.on('FullscreenStateChanged', (e: any) => {
|
||||
fullscreen.value = e.state;
|
||||
});
|
||||
}
|
||||
|
||||
const disabled = computed(() => props.options.readonly ?? false);
|
||||
|
||||
function getUploadingImgName(name: string) {
|
||||
return `[uploading:${name}]`;
|
||||
}
|
||||
|
||||
function handleImageUploading(name: string) {
|
||||
const editor = unref(editorRef);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
editor.execCommand('mceInsertContent', false, getUploadingImgName(name));
|
||||
const content = editor?.getContent() ?? '';
|
||||
setValue(editor, content);
|
||||
}
|
||||
|
||||
function handleDone(name: string, url: string) {
|
||||
const editor = unref(editorRef);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const content = editor?.getContent() ?? '';
|
||||
const val =
|
||||
content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? '';
|
||||
setValue(editor, val);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="{ width: containerWidth }" class="app-tinymce">
|
||||
<ImgUpload
|
||||
v-if="showImageUpload"
|
||||
v-show="editorRef"
|
||||
:disabled="disabled"
|
||||
:fullscreen="fullscreen"
|
||||
@done="handleDone"
|
||||
@uploading="handleImageUploading"
|
||||
/>
|
||||
<Editor
|
||||
v-if="!initOptions.inline && init"
|
||||
v-model="modelValue"
|
||||
:init="initOptions"
|
||||
:style="{ visibility: 'hidden' }"
|
||||
:tinymce-script-src="tinymceScriptSrc"
|
||||
license-key="gpl"
|
||||
/>
|
||||
<slot v-else></slot>
|
||||
<div class="app-tinymce">
|
||||
<Spin :spinning="loading">
|
||||
<Editor
|
||||
v-if="init"
|
||||
v-model="content"
|
||||
:init="initOptions"
|
||||
:tinymce-script-src="tinymceScriptSrc"
|
||||
:disabled="disabled"
|
||||
license-key="gpl"
|
||||
v-on="events"
|
||||
/>
|
||||
</Spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/**
|
||||
隐藏右上角upgrade按钮
|
||||
*/
|
||||
:deep(.tox-promotion) {
|
||||
display: none !important;
|
||||
<style lang="scss">
|
||||
/***
|
||||
由于modal/drawer的zIndex升级后为2000
|
||||
这里会造成遮挡 修改为更高的zIndex
|
||||
*/
|
||||
.tox.tox-silver-sink.tox-tinymce-aux {
|
||||
/** 该样式默认为1300的zIndex */
|
||||
z-index: 2025;
|
||||
}
|
||||
|
||||
.app-tinymce {
|
||||
position: relative;
|
||||
line-height: normal;
|
||||
|
||||
:deep(.textarea) {
|
||||
z-index: -1;
|
||||
visibility: hidden;
|
||||
/**
|
||||
隐藏右上角upgrade按钮
|
||||
*/
|
||||
.tox-promotion {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,85 +0,0 @@
|
||||
const validEvents = new Set([
|
||||
'onActivate',
|
||||
'onAddUndo',
|
||||
'onBeforeAddUndo',
|
||||
'onBeforeExecCommand',
|
||||
'onBeforeGetContent',
|
||||
'onBeforePaste',
|
||||
'onBeforeRenderUI',
|
||||
'onBeforeSetContent',
|
||||
'onBlur',
|
||||
'onChange',
|
||||
'onClearUndos',
|
||||
'onClick',
|
||||
'onContextMenu',
|
||||
'onCopy',
|
||||
'onCut',
|
||||
'onDblclick',
|
||||
'onDeactivate',
|
||||
'onDirty',
|
||||
'onDrag',
|
||||
'onDragDrop',
|
||||
'onDragEnd',
|
||||
'onDragGesture',
|
||||
'onDragOver',
|
||||
'onDrop',
|
||||
'onExecCommand',
|
||||
'onFocus',
|
||||
'onFocusIn',
|
||||
'onFocusOut',
|
||||
'onGetContent',
|
||||
'onHide',
|
||||
'onInit',
|
||||
'onKeyDown',
|
||||
'onKeyPress',
|
||||
'onKeyUp',
|
||||
'onLoadContent',
|
||||
'onMouseDown',
|
||||
'onMouseEnter',
|
||||
'onMouseLeave',
|
||||
'onMouseMove',
|
||||
'onMouseOut',
|
||||
'onMouseOver',
|
||||
'onMouseUp',
|
||||
'onNodeChange',
|
||||
'onObjectResized',
|
||||
'onObjectResizeStart',
|
||||
'onObjectSelected',
|
||||
'onPaste',
|
||||
'onPostProcess',
|
||||
'onPostRender',
|
||||
'onPreProcess',
|
||||
'onProgressState',
|
||||
'onRedo',
|
||||
'onRemove',
|
||||
'onReset',
|
||||
'onSaveContent',
|
||||
'onSelectionChange',
|
||||
'onSetAttrib',
|
||||
'onSetContent',
|
||||
'onShow',
|
||||
'onSubmit',
|
||||
'onUndo',
|
||||
'onVisualAid',
|
||||
]);
|
||||
|
||||
const isValidKey = (key: string) => validEvents.has(key);
|
||||
|
||||
export const bindHandlers = (
|
||||
initEvent: Event,
|
||||
listeners: any,
|
||||
editor: any,
|
||||
): void => {
|
||||
Object.keys(listeners)
|
||||
.filter((element) => isValidKey(element))
|
||||
.forEach((key: string) => {
|
||||
const handler = listeners[key];
|
||||
if (typeof handler === 'function') {
|
||||
if (key === 'onInit') {
|
||||
handler(initEvent, editor);
|
||||
} else {
|
||||
editor.on(key.slice(2), (e: any) => handler(e, editor));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
@@ -1,115 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { message, Upload } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'TinymceImageUpload' });
|
||||
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['uploading', 'done', 'error']);
|
||||
|
||||
let uploading = false;
|
||||
|
||||
const { apiURL, clientId } = useAppConfig(
|
||||
import.meta.env,
|
||||
import.meta.env.PROD,
|
||||
);
|
||||
const accessStore = useAccessStore();
|
||||
const uploadUrl = `${apiURL}/resource/oss/upload`;
|
||||
// 使用upload组件只能这样上传
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessStore.accessToken}`,
|
||||
clientId,
|
||||
};
|
||||
|
||||
const getButtonProps = computed(() => {
|
||||
const { disabled } = props;
|
||||
return {
|
||||
disabled,
|
||||
};
|
||||
});
|
||||
|
||||
function handleChange(info: Record<string, any>) {
|
||||
const file = info.file;
|
||||
const status = file?.status;
|
||||
// const url = file?.response?.data.url;
|
||||
const name = file?.name;
|
||||
|
||||
switch (status) {
|
||||
case 'done': {
|
||||
// http 200会走到这里 需要再次判断
|
||||
const { response } = file;
|
||||
const { code, data, msg = '服务器错误' } = response;
|
||||
if (code === 200) {
|
||||
const { url } = data;
|
||||
emit('done', name, url);
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
// emit('done', name, url);
|
||||
uploading = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
emit('error');
|
||||
uploading = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case 'uploading': {
|
||||
if (!uploading) {
|
||||
emit('uploading', name);
|
||||
uploading = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div :class="[{ fullscreen }]" class="tinymce-image-upload">
|
||||
<Upload
|
||||
:action="uploadUrl"
|
||||
:headers="headers"
|
||||
:show-upload-list="false"
|
||||
accept=".jpg,.jpeg,.gif,.png,.webp"
|
||||
multiple
|
||||
name="file"
|
||||
@change="handleChange"
|
||||
>
|
||||
<!-- 这里要改成i18n -->
|
||||
<a-button type="primary" v-bind="{ ...getButtonProps }">
|
||||
图片上传
|
||||
</a-button>
|
||||
</Upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tinymce-image-upload {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 10px;
|
||||
z-index: 20;
|
||||
|
||||
&.fullscreen {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -5,6 +5,7 @@ import type { MenuOption } from '#/api/system/menu/model';
|
||||
|
||||
import { eachTree, treeToList } from '@vben/utils';
|
||||
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { difference, isEmpty, isUndefined } from 'lodash-es';
|
||||
|
||||
/**
|
||||
@@ -48,6 +49,7 @@ export function rowAndChildrenChecked(
|
||||
*/
|
||||
export function menusWithPermissions(menus: MenuOption[]) {
|
||||
eachTree(menus, (item: MenuPermissionOption) => {
|
||||
validateMenuTree(item);
|
||||
if (item.children && item.children.length > 0) {
|
||||
/**
|
||||
* 所有为按钮的节点提取出来
|
||||
@@ -131,3 +133,50 @@ export function setTableChecked(
|
||||
tableApi.grid.setCheckboxRow(emptyRows, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验是否符合规范 给出warning提示
|
||||
*
|
||||
* 不符合规范
|
||||
* 比如: 菜单下放目录 菜单下放菜单
|
||||
* 比如: 按钮下放目录 按钮下放菜单 按钮下放按钮
|
||||
* @param menu menu
|
||||
*/
|
||||
function validateMenuTree(menu: MenuOption) {
|
||||
/**
|
||||
* C: { icon: markRaw(MenuIcon), value: '菜单' },
|
||||
F: { icon: markRaw(OkButtonIcon), value: '按钮' },
|
||||
M: { icon: markRaw(FolderIcon), value: '目录' },
|
||||
*/
|
||||
// 菜单下不能放目录/菜单
|
||||
if (menu.menuType === 'C') {
|
||||
menu.children?.forEach?.((item) => {
|
||||
if (['C', 'M'].includes(item.menuType)) {
|
||||
const description = `错误用法: [${menu.label} - 菜单]下不能放 目录/菜单 -> [${item.label}]`;
|
||||
console.warn(description);
|
||||
notification.warning({
|
||||
message: '提示',
|
||||
description,
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// 按钮为最末级 不能再放置
|
||||
if (menu.menuType === 'F') {
|
||||
/**
|
||||
* 其实可以直接判断length 这里为了更准确知道label 采用遍历的形式
|
||||
*/
|
||||
menu.children?.forEach?.((item) => {
|
||||
if (['C', 'F', 'M'].includes(item.menuType)) {
|
||||
const description = `错误用法: [${menu.label} - 按钮]下不能放置'目录/菜单/按钮' -> [${item.label}]`;
|
||||
console.warn(description);
|
||||
notification.warning({
|
||||
message: '提示',
|
||||
description,
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -55,8 +55,7 @@ const props = withDefaults(
|
||||
/**
|
||||
* 是否节点关联
|
||||
*/
|
||||
const association = defineModel('association', {
|
||||
type: Boolean,
|
||||
const association = defineModel<boolean>('association', {
|
||||
default: true,
|
||||
});
|
||||
|
||||
@@ -318,7 +317,7 @@ function getKeys(records: MenuPermissionOption[], addCurrent: boolean) {
|
||||
function getCheckedKeys() {
|
||||
// 节点关联
|
||||
if (association.value) {
|
||||
const records = tableApi?.grid?.getCheckboxRecords?.() ?? [];
|
||||
const records = tableApi?.grid?.getCheckboxRecords?.(true) ?? [];
|
||||
// 子节点
|
||||
const nodeKeys = getKeys(records, true);
|
||||
// 所有父节点
|
||||
@@ -330,7 +329,7 @@ function getCheckedKeys() {
|
||||
// 节点独立
|
||||
|
||||
// 勾选的行
|
||||
const records = tableApi?.grid?.getCheckboxRecords?.() ?? [];
|
||||
const records = tableApi?.grid?.getCheckboxRecords?.(true) ?? [];
|
||||
// 全部数据 用于获取permissions
|
||||
const allRecords = tableApi?.grid?.getData?.() ?? [];
|
||||
// 表格已经选中的行ids
|
||||
|
8
apps/web-antd/src/components/upload-old/index.ts
Normal file
8
apps/web-antd/src/components/upload-old/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @description: 旧版文件上传组件 使用FileUpload代替
|
||||
*/
|
||||
export { default as FileUploadOld } from './src/file-upload.vue';
|
||||
/**
|
||||
* @description: 旧版图片上传组件 使用ImageUpload代替
|
||||
*/
|
||||
export { default as ImageUploadOld } from './src/image-upload.vue';
|
240
apps/web-antd/src/components/upload-old/src/file-upload.vue
Normal file
240
apps/web-antd/src/components/upload-old/src/file-upload.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadFile, UploadProps } from 'ant-design-vue';
|
||||
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
|
||||
|
||||
import type { AxiosProgressEvent, UploadApi } from '#/api';
|
||||
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Upload } from 'ant-design-vue';
|
||||
import { isArray, isFunction, isObject, isString } from 'lodash-es';
|
||||
|
||||
import { uploadApi } from '#/api';
|
||||
|
||||
import { checkFileType } from './helper';
|
||||
import { UploadResultStatus } from './typing';
|
||||
import { useUploadType } from './use-upload';
|
||||
|
||||
defineOptions({ name: 'FileUpload', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/**
|
||||
* 建议使用拓展名(不带.)
|
||||
* 或者文件头 image/png等(测试判断不准确) 不支持image/*类似的写法
|
||||
* 需自行改造 ./helper/checkFileType方法
|
||||
*/
|
||||
accept?: string[];
|
||||
api?: UploadApi;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
// 返回的字段 默认url
|
||||
resultField?: 'fileName' | 'ossId' | 'url' | string;
|
||||
/**
|
||||
* 是否显示下面的描述
|
||||
*/
|
||||
showDescription?: boolean;
|
||||
value?: string[];
|
||||
}>(),
|
||||
{
|
||||
value: () => [],
|
||||
disabled: false,
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => [],
|
||||
multiple: false,
|
||||
api: () => uploadApi,
|
||||
resultField: '',
|
||||
showDescription: true,
|
||||
},
|
||||
);
|
||||
const emit = defineEmits(['change', 'update:value', 'delete']);
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
const isInnerOperate = ref<boolean>(false);
|
||||
const { getStringAccept } = useUploadType({
|
||||
acceptRef: accept,
|
||||
helpTextRef: helpText,
|
||||
maxNumberRef: maxNumber,
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
|
||||
const fileList = ref<UploadProps['fileList']>([]);
|
||||
const isLtMsg = ref<boolean>(true);
|
||||
const isActMsg = ref<boolean>(true);
|
||||
const isFirstRender = ref<boolean>(true);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(v) => {
|
||||
if (isInnerOperate.value) {
|
||||
isInnerOperate.value = false;
|
||||
return;
|
||||
}
|
||||
let value: string[] = [];
|
||||
if (v) {
|
||||
if (isArray(v)) {
|
||||
value = v;
|
||||
} else {
|
||||
value.push(v);
|
||||
}
|
||||
fileList.value = value.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: 'done',
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}) as UploadProps['fileList'];
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
isFirstRender.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
const handleRemove = async (file: UploadFile) => {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
index !== -1 && fileList.value.splice(index, 1);
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('change', value);
|
||||
emit('delete', file);
|
||||
}
|
||||
};
|
||||
|
||||
const beforeUpload = async (file: File) => {
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = await checkFileType(file, accept);
|
||||
if (!isAct) {
|
||||
message.error($t('component.upload.acceptUpload', [accept]));
|
||||
isActMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isActMsg.value = true), 1000);
|
||||
}
|
||||
const isLt = file.size / 1024 / 1024 > maxSize;
|
||||
if (isLt) {
|
||||
message.error($t('component.upload.maxSizeMultiple', [maxSize]));
|
||||
isLtMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isLtMsg.value = true), 1000);
|
||||
}
|
||||
return (isAct && !isLt) || Upload.LIST_IGNORE;
|
||||
};
|
||||
|
||||
async function customRequest(info: UploadRequestOption<any>) {
|
||||
const { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
console.warn('upload api must exist and be a function');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 进度条事件
|
||||
const progressEvent: AxiosProgressEvent = (e) => {
|
||||
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
||||
info.onProgress!({ percent });
|
||||
};
|
||||
const res = await api?.(info.file as File, {
|
||||
onUploadProgress: progressEvent,
|
||||
});
|
||||
/**
|
||||
* 由getValue处理 传对象过去
|
||||
* 直接传string(id)会被转为Number
|
||||
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
|
||||
*/
|
||||
info.onSuccess!(res);
|
||||
message.success($t('component.upload.uploadSuccess'));
|
||||
// 获取
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('change', value);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
info.onError!(error);
|
||||
}
|
||||
}
|
||||
|
||||
function getValue() {
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||
.map((item: any) => {
|
||||
if (item?.response && props?.resultField) {
|
||||
return item?.response?.[props.resultField];
|
||||
}
|
||||
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
|
||||
if (item?.url) {
|
||||
return item.url;
|
||||
}
|
||||
// 注意这里取的key为 url
|
||||
return item?.response?.url;
|
||||
});
|
||||
return list;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Upload
|
||||
v-bind="$attrs"
|
||||
v-model:file-list="fileList"
|
||||
:accept="getStringAccept"
|
||||
:before-upload="beforeUpload"
|
||||
:custom-request="customRequest"
|
||||
:disabled="disabled"
|
||||
:max-count="maxNumber"
|
||||
:multiple="multiple"
|
||||
list-type="text"
|
||||
:progress="{ showInfo: true }"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<div v-if="fileList && fileList.length < maxNumber">
|
||||
<a-button>
|
||||
<UploadOutlined />
|
||||
{{ $t('component.upload.upload') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-if="showDescription" class="mt-2 flex flex-wrap items-center">
|
||||
请上传不超过
|
||||
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
|
||||
的
|
||||
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
|
||||
格式文件
|
||||
</div>
|
||||
</Upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.ant-upload-select-picture-card i {
|
||||
font-size: 32px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.ant-upload-select-picture-card .ant-upload-text {
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
51
apps/web-antd/src/components/upload-old/src/helper.ts
Normal file
51
apps/web-antd/src/components/upload-old/src/helper.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { fileTypeFromBlob } from '@vben/utils';
|
||||
|
||||
/**
|
||||
* 不支持txt文件 @see https://github.com/sindresorhus/file-type/issues/55
|
||||
* 需要自行修改
|
||||
* @param file file对象
|
||||
* @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
|
||||
* @returns 是否通过文件类型校验
|
||||
*/
|
||||
export async function checkFileType(file: File, accepts: string[]) {
|
||||
if (!accepts || accepts?.length === 0) {
|
||||
return true;
|
||||
}
|
||||
console.log(file);
|
||||
const fileType = await fileTypeFromBlob(file);
|
||||
if (!fileType) {
|
||||
console.error('无法获取文件类型');
|
||||
return false;
|
||||
}
|
||||
console.log('文件类型', fileType);
|
||||
// 是否文件拓展名/文件头任意有一个匹配
|
||||
return accepts.includes(fileType.ext) || accepts.includes(fileType.mime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认图片类型
|
||||
*/
|
||||
export const defaultImageAccept = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
/**
|
||||
* 判断文件类型是否符合要求
|
||||
* @param file file对象
|
||||
* @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
|
||||
* @returns 是否通过文件类型校验
|
||||
*/
|
||||
export async function checkImageFileType(file: File, accepts: string[]) {
|
||||
// 空的accepts 使用默认规则
|
||||
if (!accepts || accepts.length === 0) {
|
||||
accepts = defaultImageAccept;
|
||||
}
|
||||
const fileType = await fileTypeFromBlob(file);
|
||||
if (!fileType) {
|
||||
console.error('无法获取文件类型');
|
||||
return false;
|
||||
}
|
||||
console.log('文件类型', fileType);
|
||||
// 是否文件拓展名/文件头任意有一个匹配
|
||||
if (accepts.includes(fileType.ext) || accepts.includes(fileType.mime)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
323
apps/web-antd/src/components/upload-old/src/image-upload.vue
Normal file
323
apps/web-antd/src/components/upload-old/src/image-upload.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadFile, UploadProps } from 'ant-design-vue';
|
||||
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
|
||||
|
||||
import type { AxiosProgressEvent, UploadApi } from '#/api';
|
||||
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Modal, Upload } from 'ant-design-vue';
|
||||
import { isArray, isFunction, isObject, isString, uniqueId } from 'lodash-es';
|
||||
|
||||
import { uploadApi } from '#/api';
|
||||
import { ossInfo } from '#/api/system/oss';
|
||||
|
||||
import { checkImageFileType, defaultImageAccept } from './helper';
|
||||
import { UploadResultStatus } from './typing';
|
||||
import { useUploadType } from './use-upload';
|
||||
|
||||
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/**
|
||||
* 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
|
||||
*/
|
||||
accept?: string[];
|
||||
api?: UploadApi;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
listType?: ListType;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
// 返回的字段 默认url
|
||||
resultField?: 'fileName' | 'ossId' | 'url';
|
||||
/**
|
||||
* 是否显示下面的描述
|
||||
*/
|
||||
showDescription?: boolean;
|
||||
value?: string | string[];
|
||||
}>(),
|
||||
{
|
||||
value: () => [],
|
||||
disabled: false,
|
||||
listType: 'picture-card',
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => defaultImageAccept,
|
||||
multiple: false,
|
||||
api: () => uploadApi,
|
||||
resultField: 'url',
|
||||
showDescription: true,
|
||||
},
|
||||
);
|
||||
const emit = defineEmits(['change', 'update:value', 'delete']);
|
||||
type ListType = 'picture' | 'picture-card' | 'text';
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
const isInnerOperate = ref<boolean>(false);
|
||||
const { getStringAccept } = useUploadType({
|
||||
acceptRef: accept,
|
||||
helpTextRef: helpText,
|
||||
maxNumberRef: maxNumber,
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
const previewOpen = ref<boolean>(false);
|
||||
const previewImage = ref<string>('');
|
||||
const previewTitle = ref<string>('');
|
||||
|
||||
const fileList = ref<UploadProps['fileList']>([]);
|
||||
const isLtMsg = ref<boolean>(true);
|
||||
const isActMsg = ref<boolean>(true);
|
||||
const isFirstRender = ref<boolean>(true);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
async (v) => {
|
||||
if (isInnerOperate.value) {
|
||||
isInnerOperate.value = false;
|
||||
return;
|
||||
}
|
||||
let value: string | string[] = [];
|
||||
if (v) {
|
||||
const _fileList: string[] = [];
|
||||
if (isString(v)) {
|
||||
_fileList.push(v);
|
||||
}
|
||||
if (isArray(v)) {
|
||||
_fileList.push(...v);
|
||||
}
|
||||
// 直接赋值 可能为string | string[]
|
||||
value = v;
|
||||
const withUrlList: UploadProps['fileList'] = [];
|
||||
for (const item of _fileList) {
|
||||
// ossId情况
|
||||
if (props.resultField === 'ossId') {
|
||||
const resp = await ossInfo([item]);
|
||||
if (item && isString(item)) {
|
||||
withUrlList.push({
|
||||
uid: item, // ossId作为uid 方便getValue获取
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: 'done',
|
||||
url: resp?.[0]?.url,
|
||||
});
|
||||
} else if (item && isObject(item)) {
|
||||
withUrlList.push({
|
||||
...(item as any),
|
||||
uid: item,
|
||||
url: resp?.[0]?.url,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 非ossId情况
|
||||
if (item && isString(item)) {
|
||||
withUrlList.push({
|
||||
uid: uniqueId(),
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: 'done',
|
||||
url: item,
|
||||
});
|
||||
} else if (item && isObject(item)) {
|
||||
withUrlList.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
fileList.value = withUrlList;
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
isFirstRender.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
function getBase64<T extends ArrayBuffer | null | string>(file: File) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.addEventListener('load', () => {
|
||||
resolve(reader.result as T);
|
||||
});
|
||||
reader.addEventListener('error', (error) => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64<string>(file.originFileObj!);
|
||||
}
|
||||
previewImage.value = file.url || file.preview || '';
|
||||
previewOpen.value = true;
|
||||
previewTitle.value =
|
||||
file.name ||
|
||||
previewImage.value.slice(
|
||||
Math.max(0, previewImage.value.lastIndexOf('/') + 1),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemove = async (file: UploadFile) => {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
index !== -1 && fileList.value.splice(index, 1);
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('change', value);
|
||||
emit('delete', file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
previewOpen.value = false;
|
||||
previewTitle.value = '';
|
||||
};
|
||||
|
||||
const beforeUpload = async (file: File) => {
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = await checkImageFileType(file, accept);
|
||||
if (!isAct) {
|
||||
message.error($t('component.upload.acceptUpload', [accept]));
|
||||
isActMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isActMsg.value = true), 1000);
|
||||
}
|
||||
const isLt = file.size / 1024 / 1024 > maxSize;
|
||||
if (isLt) {
|
||||
message.error($t('component.upload.maxSizeMultiple', [maxSize]));
|
||||
isLtMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isLtMsg.value = true), 1000);
|
||||
}
|
||||
return (isAct && !isLt) || Upload.LIST_IGNORE;
|
||||
};
|
||||
|
||||
async function customRequest(info: UploadRequestOption<any>) {
|
||||
const { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
console.warn('upload api must exist and be a function');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 进度条事件
|
||||
const progressEvent: AxiosProgressEvent = (e) => {
|
||||
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
||||
info.onProgress!({ percent });
|
||||
};
|
||||
const res = await api?.(info.file as File, {
|
||||
onUploadProgress: progressEvent,
|
||||
});
|
||||
/**
|
||||
* 由getValue处理 传对象过去
|
||||
* 直接传string(id)会被转为Number
|
||||
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
|
||||
*/
|
||||
info.onSuccess!(res);
|
||||
message.success($t('component.upload.uploadSuccess'));
|
||||
// 获取
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('change', value);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
info.onError!(error);
|
||||
}
|
||||
}
|
||||
|
||||
function getValue() {
|
||||
console.log(fileList.value);
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||
.map((item: any) => {
|
||||
if (item?.response && props?.resultField) {
|
||||
return item?.response?.[props.resultField];
|
||||
}
|
||||
// ossId兼容 uid为ossId直接返回
|
||||
if (props.resultField === 'ossId' && item.uid) {
|
||||
return item.uid;
|
||||
}
|
||||
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
|
||||
if (item?.url) {
|
||||
return item.url;
|
||||
}
|
||||
// 注意这里取的key为 url
|
||||
return item?.response?.url;
|
||||
});
|
||||
// 只有一张图片 默认绑定string而非string[]
|
||||
if (props.maxNumber === 1 && list.length === 1) {
|
||||
return list[0];
|
||||
}
|
||||
// 只有一张图片 && 删除图片时 可自行修改
|
||||
if (props.maxNumber === 1 && list.length === 0) {
|
||||
return '';
|
||||
}
|
||||
return list;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Upload
|
||||
v-bind="$attrs"
|
||||
v-model:file-list="fileList"
|
||||
:accept="getStringAccept"
|
||||
:before-upload="beforeUpload"
|
||||
:custom-request="customRequest"
|
||||
:disabled="disabled"
|
||||
:list-type="listType"
|
||||
:max-count="maxNumber"
|
||||
:multiple="multiple"
|
||||
:progress="{ showInfo: true }"
|
||||
@preview="handlePreview"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<div v-if="fileList && fileList.length < maxNumber">
|
||||
<PlusOutlined />
|
||||
<div style="margin-top: 8px">{{ $t('component.upload.upload') }}</div>
|
||||
</div>
|
||||
</Upload>
|
||||
<div
|
||||
v-if="showDescription"
|
||||
class="mt-2 flex flex-wrap items-center text-[14px]"
|
||||
>
|
||||
请上传不超过
|
||||
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
|
||||
的
|
||||
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
|
||||
格式文件
|
||||
</div>
|
||||
<Modal
|
||||
:footer="null"
|
||||
:open="previewOpen"
|
||||
:title="previewTitle"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<img :src="previewImage" alt="" style="width: 100%" />
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.ant-upload-select-picture-card i {
|
||||
font-size: 32px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.ant-upload-select-picture-card .ant-upload-text {
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
@@ -1,243 +1,150 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadFile, UploadProps } from 'ant-design-vue';
|
||||
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
|
||||
<!--
|
||||
不再支持url 统一使用ossId
|
||||
去除使用`file-type`库进行文件类型检测 在Safari无法使用
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { UploadListType } from 'ant-design-vue/es/upload/interface';
|
||||
|
||||
import type { AxiosResponse } from '@vben/request';
|
||||
import type { BaseUploadProps, UploadEmits } from './props';
|
||||
|
||||
import type { AxiosProgressEvent } from '#/api';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
import { $t, I18nT } from '@vben/locales';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Upload } from 'ant-design-vue';
|
||||
import { isArray, isFunction, isObject, isString } from 'lodash-es';
|
||||
import { InboxOutlined, UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { Upload } from 'ant-design-vue';
|
||||
|
||||
import { uploadApi } from '#/api';
|
||||
|
||||
import { checkFileType } from './helper';
|
||||
import { UploadResultStatus } from './typing';
|
||||
import { useUploadType } from './use-upload';
|
||||
import { defaultFileAcceptExts, defaultFilePreview } from './helper';
|
||||
import { useUpload } from './hook';
|
||||
|
||||
defineOptions({ name: 'FileUpload', inheritAttrs: false });
|
||||
interface FileUploadProps extends BaseUploadProps {
|
||||
/**
|
||||
* 同antdv的listType 但是排除picture-card
|
||||
* 文件上传不适合用picture-card显示
|
||||
* @default text
|
||||
*/
|
||||
listType?: Exclude<UploadListType, 'picture-card'>;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/**
|
||||
* 建议使用拓展名(不带.)
|
||||
* 或者文件头 image/png等(测试判断不准确) 不支持image/*类似的写法
|
||||
* 需自行改造 ./helper/checkFileType方法
|
||||
*/
|
||||
accept?: string[];
|
||||
api?: (
|
||||
file: Blob | File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) => Promise<AxiosResponse<any>>;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
// 返回的字段 默认url
|
||||
resultField?: 'fileName' | 'ossId' | 'url' | string;
|
||||
/**
|
||||
* 是否显示下面的描述
|
||||
*/
|
||||
showDescription?: boolean;
|
||||
value?: string[];
|
||||
}>(),
|
||||
{
|
||||
value: () => [],
|
||||
disabled: false,
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => [],
|
||||
multiple: false,
|
||||
api: uploadApi,
|
||||
resultField: '',
|
||||
showDescription: true,
|
||||
},
|
||||
);
|
||||
const emit = defineEmits(['change', 'update:value', 'delete']);
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
const isInnerOperate = ref<boolean>(false);
|
||||
const { getStringAccept } = useUploadType({
|
||||
acceptRef: accept,
|
||||
helpTextRef: helpText,
|
||||
maxNumberRef: maxNumber,
|
||||
maxSizeRef: maxSize,
|
||||
const props = withDefaults(defineProps<FileUploadProps>(), {
|
||||
api: () => uploadApi,
|
||||
removeOnError: true,
|
||||
showSuccessMsg: true,
|
||||
removeConfirm: false,
|
||||
accept: defaultFileAcceptExts.join(','),
|
||||
data: () => undefined,
|
||||
maxCount: 1,
|
||||
maxSize: 5,
|
||||
disabled: false,
|
||||
helpMessage: true,
|
||||
preview: defaultFilePreview,
|
||||
enableDragUpload: false,
|
||||
directory: false,
|
||||
abortOnUnmounted: true,
|
||||
listType: 'text',
|
||||
});
|
||||
|
||||
const fileList = ref<UploadProps['fileList']>([]);
|
||||
const isLtMsg = ref<boolean>(true);
|
||||
const isActMsg = ref<boolean>(true);
|
||||
const isFirstRender = ref<boolean>(true);
|
||||
const emit = defineEmits<UploadEmits>();
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(v) => {
|
||||
if (isInnerOperate.value) {
|
||||
isInnerOperate.value = false;
|
||||
return;
|
||||
}
|
||||
let value: string[] = [];
|
||||
if (v) {
|
||||
if (isArray(v)) {
|
||||
value = v;
|
||||
} else {
|
||||
value.push(v);
|
||||
}
|
||||
fileList.value = value.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: 'done',
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}) as UploadProps['fileList'];
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
isFirstRender.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
/** 返回不同的上传组件 */
|
||||
const CurrentUploadComponent = computed(() => {
|
||||
if (props.enableDragUpload) {
|
||||
return Upload.Dragger;
|
||||
}
|
||||
return Upload;
|
||||
});
|
||||
|
||||
const handleRemove = async (file: UploadFile) => {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
index !== -1 && fileList.value.splice(index, 1);
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('change', value);
|
||||
emit('delete', file);
|
||||
}
|
||||
};
|
||||
// 双向绑定 ossId
|
||||
const ossIdList = defineModel<string | string[]>('value', {
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
const beforeUpload = async (file: File) => {
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = await checkFileType(file, accept);
|
||||
if (!isAct) {
|
||||
message.error($t('component.upload.acceptUpload', [accept]));
|
||||
isActMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isActMsg.value = true), 1000);
|
||||
}
|
||||
const isLt = file.size / 1024 / 1024 > maxSize;
|
||||
if (isLt) {
|
||||
message.error($t('component.upload.maxSizeMultiple', [maxSize]));
|
||||
isLtMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isLtMsg.value = true), 1000);
|
||||
}
|
||||
return (isAct && !isLt) || Upload.LIST_IGNORE;
|
||||
};
|
||||
|
||||
async function customRequest(info: UploadRequestOption<any>) {
|
||||
const { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
console.warn('upload api must exist and be a function');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 进度条事件
|
||||
const progressEvent: AxiosProgressEvent = (e) => {
|
||||
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
||||
info.onProgress!({ percent });
|
||||
};
|
||||
const res = await api?.(info.file as File, progressEvent);
|
||||
/**
|
||||
* 由getValue处理 传对象过去
|
||||
* 直接传string(id)会被转为Number
|
||||
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
|
||||
*/
|
||||
info.onSuccess!(res);
|
||||
message.success($t('component.upload.uploadSuccess'));
|
||||
// 获取
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('change', value);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
info.onError!(error);
|
||||
}
|
||||
}
|
||||
|
||||
function getValue() {
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||
.map((item: any) => {
|
||||
if (item?.response && props?.resultField) {
|
||||
return item?.response?.[props.resultField];
|
||||
}
|
||||
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
|
||||
if (item?.url) {
|
||||
return item.url;
|
||||
}
|
||||
// 注意这里取的key为 url
|
||||
return item?.response?.url;
|
||||
});
|
||||
return list;
|
||||
}
|
||||
const {
|
||||
customRequest,
|
||||
acceptStr,
|
||||
handleChange,
|
||||
handleRemove,
|
||||
beforeUpload,
|
||||
innerFileList,
|
||||
} = useUpload(props, emit, ossIdList, 'file');
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Upload.Dragger只会影响样式
|
||||
使用普通Upload也是支持拖拽上传的
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<Upload
|
||||
v-bind="$attrs"
|
||||
v-model:file-list="fileList"
|
||||
:accept="getStringAccept"
|
||||
<CurrentUploadComponent
|
||||
v-model:file-list="innerFileList"
|
||||
:accept="accept"
|
||||
:list-type="listType"
|
||||
:disabled="disabled"
|
||||
:directory="directory"
|
||||
:max-count="maxCount"
|
||||
:progress="{ showInfo: true }"
|
||||
:multiple="multiple"
|
||||
:before-upload="beforeUpload"
|
||||
:custom-request="customRequest"
|
||||
:disabled="disabled"
|
||||
:max-count="maxNumber"
|
||||
:multiple="multiple"
|
||||
list-type="text"
|
||||
:progress="{ showInfo: true }"
|
||||
@preview="preview"
|
||||
@change="handleChange"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<div v-if="fileList && fileList.length < maxNumber">
|
||||
<a-button>
|
||||
<div v-if="!enableDragUpload && innerFileList?.length < maxCount">
|
||||
<a-button :disabled="disabled">
|
||||
<UploadOutlined />
|
||||
{{ $t('component.upload.upload') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-if="showDescription" class="mt-2 flex flex-wrap items-center">
|
||||
请上传不超过
|
||||
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
|
||||
的
|
||||
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
|
||||
格式文件
|
||||
<div v-if="enableDragUpload">
|
||||
<p class="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p class="ant-upload-text">
|
||||
{{ $t('component.upload.clickOrDrag') }}
|
||||
</p>
|
||||
</div>
|
||||
</Upload>
|
||||
</CurrentUploadComponent>
|
||||
<slot name="helpMessage" v-bind="{ maxCount, disabled, maxSize, accept }">
|
||||
<I18nT
|
||||
v-if="helpMessage"
|
||||
scope="global"
|
||||
keypath="component.upload.uploadHelpMessage"
|
||||
tag="div"
|
||||
class="mt-2"
|
||||
:class="{ 'upload-text__disabled': disabled }"
|
||||
>
|
||||
<template #size>
|
||||
<span
|
||||
class="text-primary mx-1 font-medium"
|
||||
:class="{ 'upload-text__disabled': disabled }"
|
||||
>
|
||||
{{ maxSize }}MB
|
||||
</span>
|
||||
</template>
|
||||
<template #ext>
|
||||
<span
|
||||
class="text-primary mx-1 font-medium"
|
||||
:class="{ 'upload-text__disabled': disabled }"
|
||||
>
|
||||
{{ acceptStr }}
|
||||
</span>
|
||||
</template>
|
||||
</I18nT>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.ant-upload-select-picture-card i {
|
||||
color: #999;
|
||||
font-size: 32px;
|
||||
}
|
||||
<style lang="scss">
|
||||
// 禁用的样式和antd保持一致
|
||||
.upload-text__disabled {
|
||||
color: rgb(50 54 57 / 25%);
|
||||
cursor: not-allowed;
|
||||
|
||||
.ant-upload-select-picture-card .ant-upload-text {
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
&:where(.dark, .dark *) {
|
||||
color: rgb(242 242 242 / 25%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,51 +1,28 @@
|
||||
import { fileTypeFromBlob } from '@vben/utils';
|
||||
import type { UploadFile } from 'ant-design-vue';
|
||||
|
||||
/**
|
||||
* 不支持txt文件 @see https://github.com/sindresorhus/file-type/issues/55
|
||||
* 需要自行修改
|
||||
* @param file file对象
|
||||
* @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
|
||||
* @returns 是否通过文件类型校验
|
||||
* 默认支持上传的图片文件类型
|
||||
*/
|
||||
export async function checkFileType(file: File, accepts: string[]) {
|
||||
if (!accepts || accepts?.length === 0) {
|
||||
return true;
|
||||
}
|
||||
console.log(file);
|
||||
const fileType = await fileTypeFromBlob(file);
|
||||
if (!fileType) {
|
||||
console.error('无法获取文件类型');
|
||||
return false;
|
||||
}
|
||||
console.log('文件类型', fileType);
|
||||
// 是否文件拓展名/文件头任意有一个匹配
|
||||
return accepts.includes(fileType.ext) || accepts.includes(fileType.mime);
|
||||
}
|
||||
export const defaultImageAcceptExts = [
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.png',
|
||||
'.gif',
|
||||
'.webp',
|
||||
];
|
||||
|
||||
/**
|
||||
* 默认图片类型
|
||||
* 默认支持上传的文件类型
|
||||
*/
|
||||
export const defaultImageAccept = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
export const defaultFileAcceptExts = ['.xlsx', '.csv', '.docx', '.pdf'];
|
||||
|
||||
/**
|
||||
* 判断文件类型是否符合要求
|
||||
* @param file file对象
|
||||
* @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
|
||||
* @returns 是否通过文件类型校验
|
||||
* 文件(非图片)的默认预览逻辑
|
||||
* 默认: window.open打开 交给浏览器接管
|
||||
* @param file file
|
||||
*/
|
||||
export async function checkImageFileType(file: File, accepts: string[]) {
|
||||
// 空的accepts 使用默认规则
|
||||
if (!accepts || accepts.length === 0) {
|
||||
accepts = defaultImageAccept;
|
||||
export function defaultFilePreview(file: UploadFile) {
|
||||
if (file?.url) {
|
||||
window.open(file.url);
|
||||
}
|
||||
const fileType = await fileTypeFromBlob(file);
|
||||
if (!fileType) {
|
||||
console.error('无法获取文件类型');
|
||||
return false;
|
||||
}
|
||||
console.log('文件类型', fileType);
|
||||
// 是否文件拓展名/文件头任意有一个匹配
|
||||
if (accepts.includes(fileType.ext) || accepts.includes(fileType.mime)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
385
apps/web-antd/src/components/upload/src/hook.ts
Normal file
385
apps/web-antd/src/components/upload/src/hook.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type { UploadChangeParam, UploadFile } from 'ant-design-vue';
|
||||
import type { FileType } from 'ant-design-vue/es/upload/interface';
|
||||
import type {
|
||||
RcFile,
|
||||
UploadRequestOption,
|
||||
} from 'ant-design-vue/es/vc-upload/interface';
|
||||
|
||||
import type { ModelRef } from 'vue';
|
||||
|
||||
import type {
|
||||
BaseUploadProps,
|
||||
CustomGetter,
|
||||
UploadEmits,
|
||||
UploadType,
|
||||
} from './props';
|
||||
|
||||
import type { AxiosProgressEvent, UploadResult } from '#/api';
|
||||
import type { OssFile } from '#/api/system/oss/model';
|
||||
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { isFunction, isString } from 'lodash-es';
|
||||
|
||||
import { ossInfo } from '#/api/system/oss';
|
||||
|
||||
/**
|
||||
* 图片预览hook
|
||||
* @returns 预览
|
||||
*/
|
||||
export function useImagePreview() {
|
||||
/**
|
||||
* 获取base64字符串
|
||||
* @param file 文件
|
||||
* @returns base64字符串
|
||||
*/
|
||||
function getBase64(file: File) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.addEventListener('load', () => resolve(reader.result));
|
||||
reader.addEventListener('error', (error) => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
// Modal可见
|
||||
const previewVisible = ref(false);
|
||||
// 预览的图片 url/base64
|
||||
const previewImage = ref('');
|
||||
// 预览的图片名称
|
||||
const previewTitle = ref('');
|
||||
|
||||
function handleCancel() {
|
||||
previewVisible.value = false;
|
||||
previewTitle.value = '';
|
||||
}
|
||||
|
||||
async function handlePreview(file: UploadFile) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
// 文件预览 取base64
|
||||
if (!file.url && !file.preview && file.originFileObj) {
|
||||
file.preview = (await getBase64(file.originFileObj)) as string;
|
||||
}
|
||||
// 这里不可能为空
|
||||
const url = file.url ?? '';
|
||||
previewImage.value = url || file.preview || '';
|
||||
previewVisible.value = true;
|
||||
previewTitle.value =
|
||||
file.name || url.slice(Math.max(0, url.lastIndexOf('/') + 1));
|
||||
}
|
||||
|
||||
return {
|
||||
previewVisible,
|
||||
previewImage,
|
||||
previewTitle,
|
||||
handleCancel,
|
||||
handlePreview,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传和文件上传的通用hook
|
||||
* @param props 组件props
|
||||
* @param emit 事件
|
||||
* @param bindValue 双向绑定的idList
|
||||
* @param uploadType 区分是文件还是图片上传
|
||||
* @returns hook
|
||||
*/
|
||||
export function useUpload(
|
||||
props: Readonly<BaseUploadProps>,
|
||||
emit: UploadEmits,
|
||||
bindValue: ModelRef<string | string[]>,
|
||||
uploadType: UploadType,
|
||||
) {
|
||||
// 组件内部维护fileList
|
||||
const innerFileList = ref<UploadFile[]>([]);
|
||||
|
||||
const acceptStr = computed(() => {
|
||||
// string类型
|
||||
if (isString(props.acceptFormat)) {
|
||||
return props.acceptFormat;
|
||||
}
|
||||
// 函数类型
|
||||
if (isFunction(props.acceptFormat)) {
|
||||
return props.acceptFormat(props.accept!);
|
||||
}
|
||||
// 默认 会对拓展名做处理
|
||||
return props.accept
|
||||
?.split(',')
|
||||
.map((item) => {
|
||||
if (item.startsWith('.')) {
|
||||
return item.slice(1);
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.join(', ');
|
||||
});
|
||||
|
||||
/**
|
||||
* 自定义文件显示名称 需要区分不同的接口
|
||||
* @param cb callback
|
||||
* @returns 文件名
|
||||
*/
|
||||
function transformFilename(cb: Parameters<CustomGetter<string>>[0]) {
|
||||
if (isFunction(props.customFilename)) {
|
||||
return props.customFilename(cb);
|
||||
}
|
||||
// info接口
|
||||
if (cb.type === 'info') {
|
||||
return cb.response.originalName;
|
||||
}
|
||||
// 上传接口
|
||||
return cb.response.fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义缩略图 需要区分不同的接口
|
||||
* @param cb callback
|
||||
* @returns 缩略图地址
|
||||
*/
|
||||
function transformThumbUrl(cb: Parameters<CustomGetter<undefined>>[0]) {
|
||||
if (isFunction(props.customThumbUrl)) {
|
||||
return props.customThumbUrl(cb);
|
||||
}
|
||||
// image 默认返回图片链接
|
||||
if (uploadType === 'image') {
|
||||
// info接口
|
||||
if (cb.type === 'info') {
|
||||
return cb.response.url;
|
||||
}
|
||||
// 上传接口
|
||||
return cb.response.url;
|
||||
}
|
||||
// 文件默认返回空 走antd默认的预览图逻辑
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 用来标识是否为上传 这样在watch内部不需要请求api
|
||||
let isUpload = false;
|
||||
function handleChange(info: UploadChangeParam) {
|
||||
/**
|
||||
* 移除当前文件
|
||||
* @param currentFile 当前文件
|
||||
* @param currentFileList 当前所有文件list
|
||||
*/
|
||||
function removeCurrentFile(
|
||||
currentFile: UploadChangeParam['file'],
|
||||
currentFileList: UploadChangeParam['fileList'],
|
||||
) {
|
||||
if (props.removeOnError) {
|
||||
currentFileList.splice(currentFileList.indexOf(currentFile), 1);
|
||||
} else {
|
||||
currentFile.status = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
const { file: currentFile, fileList } = info;
|
||||
|
||||
switch (currentFile.status) {
|
||||
// 上传成功 只是判断httpStatus 200 需要手动判断业务code
|
||||
case 'done': {
|
||||
if (!currentFile.response) {
|
||||
return;
|
||||
}
|
||||
// 获取返回结果 为customRequest的reslove参数
|
||||
// 只有success才会走到这里
|
||||
const { ossId, url } = currentFile.response as UploadResult;
|
||||
currentFile.url = url;
|
||||
currentFile.uid = ossId;
|
||||
|
||||
const cb = {
|
||||
type: 'upload',
|
||||
response: currentFile.response as UploadResult,
|
||||
} as const;
|
||||
|
||||
currentFile.fileName = transformFilename(cb);
|
||||
currentFile.name = transformFilename(cb);
|
||||
currentFile.thumbUrl = transformThumbUrl(cb);
|
||||
// 标记为上传 watch根据值做处理
|
||||
isUpload = true;
|
||||
// ossID添加 单个文件会被当做string
|
||||
if (props.maxCount === 1) {
|
||||
bindValue.value = ossId;
|
||||
} else {
|
||||
// 给默认值
|
||||
if (!Array.isArray(bindValue.value)) {
|
||||
bindValue.value = [];
|
||||
}
|
||||
// 直接使用.value无法触发useForm的更新(原生是正常的) 需要修改地址
|
||||
bindValue.value = [...bindValue.value, ossId];
|
||||
}
|
||||
break;
|
||||
}
|
||||
// 上传失败 网络原因导致httpStatus 不等于200
|
||||
case 'error': {
|
||||
removeCurrentFile(currentFile, fileList);
|
||||
}
|
||||
}
|
||||
emit('change', info);
|
||||
}
|
||||
|
||||
function handleRemove(currentFile: UploadFile) {
|
||||
function remove() {
|
||||
// fileList会自行处理删除 这里只需要处理ossId
|
||||
if (props.maxCount === 1) {
|
||||
bindValue.value = '';
|
||||
} else {
|
||||
(bindValue.value as string[]).splice(
|
||||
bindValue.value.indexOf(currentFile.uid),
|
||||
1,
|
||||
);
|
||||
}
|
||||
// 触发remove事件
|
||||
emit('remove', currentFile);
|
||||
}
|
||||
|
||||
if (!props.removeConfirm) {
|
||||
remove();
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
Modal.confirm({
|
||||
title: $t('pages.common.tip'),
|
||||
content: $t('component.upload.confirmDelete', [currentFile.name]),
|
||||
okButtonProps: { danger: true },
|
||||
centered: true,
|
||||
onOk() {
|
||||
resolve(true);
|
||||
remove();
|
||||
},
|
||||
onCancel() {
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前检测文件大小
|
||||
* 拖拽时候前置会有浏览器自身的accept校验 校验失败不会执行此方法
|
||||
* @param file file
|
||||
* @returns file | false
|
||||
*/
|
||||
function beforeUpload(file: FileType) {
|
||||
const isLtMax = file.size / 1024 / 1024 < props.maxSize!;
|
||||
if (!isLtMax) {
|
||||
message.error($t('component.upload.maxSize', [props.maxSize]));
|
||||
return false;
|
||||
}
|
||||
// 大坑 Safari不支持file-type库 去除文件类型的校验
|
||||
return file;
|
||||
}
|
||||
|
||||
const uploadAbort = new AbortController();
|
||||
/**
|
||||
* 自定义上传实现
|
||||
* @param info
|
||||
*/
|
||||
async function customRequest(info: UploadRequestOption<any>) {
|
||||
const { api } = props;
|
||||
if (!isFunction(api)) {
|
||||
console.warn('upload api must exist and be a function');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 进度条事件
|
||||
const progressEvent: AxiosProgressEvent = (e) => {
|
||||
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
||||
info.onProgress!({ percent });
|
||||
};
|
||||
const res = await api(info.file as File, {
|
||||
onUploadProgress: progressEvent,
|
||||
signal: uploadAbort.signal,
|
||||
otherData: props?.data,
|
||||
});
|
||||
info.onSuccess!(res);
|
||||
if (props.showSuccessMsg) {
|
||||
message.success($t('component.upload.uploadSuccess'));
|
||||
}
|
||||
emit('success', info.file as RcFile, res);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
info.onError!(error);
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
props.abortOnUnmounted && uploadAbort.abort();
|
||||
});
|
||||
|
||||
/**
|
||||
* 这里默认只监听list地址变化 即重新赋值才会触发watch
|
||||
* immediate用于初始化触发
|
||||
*/
|
||||
watch(
|
||||
() => bindValue.value,
|
||||
async (value) => {
|
||||
if (value.length === 0) {
|
||||
// 清空绑定值时,同时清空innerFileList,避免外部使用时还能读取到
|
||||
innerFileList.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 上传完毕 不需要调用获取信息接口
|
||||
if (isUpload) {
|
||||
// 清理 使下一次状态可用
|
||||
isUpload = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await ossInfo(value);
|
||||
function transformFile(info: OssFile) {
|
||||
const cb = { type: 'info', response: info } as const;
|
||||
|
||||
const fileitem: UploadFile = {
|
||||
uid: info.ossId,
|
||||
name: transformFilename(cb),
|
||||
fileName: transformFilename(cb),
|
||||
url: info.url,
|
||||
thumbUrl: transformThumbUrl(cb),
|
||||
status: 'done',
|
||||
};
|
||||
return fileitem;
|
||||
}
|
||||
const transformOptions = resp.map((item) => transformFile(item));
|
||||
innerFileList.value = transformOptions;
|
||||
// 单文件 丢弃策略
|
||||
if (props.maxCount === 1 && resp.length === 0 && !props.keepMissingId) {
|
||||
bindValue.value = '';
|
||||
return;
|
||||
}
|
||||
// 多文件
|
||||
// 单文件查到了也会走这里的逻辑 filter会报错 需要maxCount判断处理
|
||||
if (
|
||||
resp.length !== value.length &&
|
||||
!props.keepMissingId &&
|
||||
props.maxCount !== 1
|
||||
) {
|
||||
// 给默认值
|
||||
if (!Array.isArray(bindValue.value)) {
|
||||
bindValue.value = [];
|
||||
}
|
||||
bindValue.value = bindValue.value.filter((ossId) =>
|
||||
resp.map((res) => res.ossId).includes(ossId),
|
||||
);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return {
|
||||
handleChange,
|
||||
handleRemove,
|
||||
beforeUpload,
|
||||
customRequest,
|
||||
innerFileList,
|
||||
acceptStr,
|
||||
};
|
||||
}
|
@@ -1,326 +1,190 @@
|
||||
<script lang="ts" setup>
|
||||
import type { UploadFile, UploadProps } from 'ant-design-vue';
|
||||
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
|
||||
<!--
|
||||
不再支持url 统一使用ossId
|
||||
去除使用`file-type`库进行文件类型检测 在Safari无法使用
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
UploadFile,
|
||||
UploadListType,
|
||||
} from 'ant-design-vue/es/upload/interface';
|
||||
|
||||
import type { AxiosResponse } from '@vben/request';
|
||||
import type { BaseUploadProps, UploadEmits } from './props';
|
||||
|
||||
import type { AxiosProgressEvent } from '#/api';
|
||||
import { $t, I18nT } from '@vben/locales';
|
||||
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { message, Modal, Upload } from 'ant-design-vue';
|
||||
import { isArray, isFunction, isObject, isString, uniqueId } from 'lodash-es';
|
||||
import { PlusOutlined, UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { Image, ImagePreviewGroup, Upload } from 'ant-design-vue';
|
||||
import { isFunction } from 'lodash-es';
|
||||
|
||||
import { uploadApi } from '#/api';
|
||||
import { ossInfo } from '#/api/system/oss';
|
||||
|
||||
import { checkImageFileType, defaultImageAccept } from './helper';
|
||||
import { UploadResultStatus } from './typing';
|
||||
import { useUploadType } from './use-upload';
|
||||
import { defaultImageAcceptExts } from './helper';
|
||||
import { useImagePreview, useUpload } from './hook';
|
||||
|
||||
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
|
||||
interface ImageUploadProps extends BaseUploadProps {
|
||||
/**
|
||||
* 同antdv的listType
|
||||
* @default picture-card
|
||||
*/
|
||||
listType?: UploadListType;
|
||||
/**
|
||||
* 使用list-type: picture-card时 是否显示动画
|
||||
* 会有一个`弹跳`的效果 默认关闭
|
||||
* @default false
|
||||
*/
|
||||
withAnimation?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/**
|
||||
* 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
|
||||
*/
|
||||
accept?: string[];
|
||||
api?: (
|
||||
file: Blob | File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) => Promise<AxiosResponse<any>>;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
listType?: ListType;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
// 返回的字段 默认url
|
||||
resultField?: 'fileName' | 'ossId' | 'url';
|
||||
/**
|
||||
* 是否显示下面的描述
|
||||
*/
|
||||
showDescription?: boolean;
|
||||
value?: string | string[];
|
||||
}>(),
|
||||
{
|
||||
value: () => [],
|
||||
disabled: false,
|
||||
listType: 'picture-card',
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => defaultImageAccept,
|
||||
multiple: false,
|
||||
api: uploadApi,
|
||||
resultField: 'url',
|
||||
showDescription: true,
|
||||
},
|
||||
);
|
||||
const emit = defineEmits(['change', 'update:value', 'delete']);
|
||||
type ListType = 'picture' | 'picture-card' | 'text';
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
const isInnerOperate = ref<boolean>(false);
|
||||
const { getStringAccept } = useUploadType({
|
||||
acceptRef: accept,
|
||||
helpTextRef: helpText,
|
||||
maxNumberRef: maxNumber,
|
||||
maxSizeRef: maxSize,
|
||||
const props = withDefaults(defineProps<ImageUploadProps>(), {
|
||||
api: () => uploadApi,
|
||||
removeOnError: true,
|
||||
showSuccessMsg: true,
|
||||
removeConfirm: false,
|
||||
accept: defaultImageAcceptExts.join(','),
|
||||
data: () => undefined,
|
||||
maxCount: 1,
|
||||
maxSize: 5,
|
||||
disabled: false,
|
||||
listType: 'picture-card',
|
||||
helpMessage: true,
|
||||
enableDragUpload: false,
|
||||
abortOnUnmounted: true,
|
||||
withAnimation: false,
|
||||
});
|
||||
const previewOpen = ref<boolean>(false);
|
||||
const previewImage = ref<string>('');
|
||||
const previewTitle = ref<string>('');
|
||||
|
||||
const fileList = ref<UploadProps['fileList']>([]);
|
||||
const isLtMsg = ref<boolean>(true);
|
||||
const isActMsg = ref<boolean>(true);
|
||||
const isFirstRender = ref<boolean>(true);
|
||||
const emit = defineEmits<UploadEmits>();
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
async (v) => {
|
||||
if (isInnerOperate.value) {
|
||||
isInnerOperate.value = false;
|
||||
return;
|
||||
}
|
||||
let value: string | string[] = [];
|
||||
if (v) {
|
||||
const _fileList: string[] = [];
|
||||
if (isString(v)) {
|
||||
_fileList.push(v);
|
||||
}
|
||||
if (isArray(v)) {
|
||||
_fileList.push(...v);
|
||||
}
|
||||
// 直接赋值 可能为string | string[]
|
||||
value = v;
|
||||
const withUrlList: UploadProps['fileList'] = [];
|
||||
for (const item of _fileList) {
|
||||
// ossId情况
|
||||
if (props.resultField === 'ossId') {
|
||||
const resp = await ossInfo([item]);
|
||||
if (item && isString(item)) {
|
||||
withUrlList.push({
|
||||
uid: item, // ossId作为uid 方便getValue获取
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: 'done',
|
||||
url: resp?.[0]?.url,
|
||||
});
|
||||
} else if (item && isObject(item)) {
|
||||
withUrlList.push({
|
||||
...(item as any),
|
||||
uid: item,
|
||||
url: resp?.[0]?.url,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 非ossId情况
|
||||
if (item && isString(item)) {
|
||||
withUrlList.push({
|
||||
uid: uniqueId(),
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: 'done',
|
||||
url: item,
|
||||
});
|
||||
} else if (item && isObject(item)) {
|
||||
withUrlList.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
fileList.value = withUrlList;
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
isFirstRender.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
// 双向绑定 ossId
|
||||
const ossIdList = defineModel<string | string[]>('value', {
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
function getBase64<T extends ArrayBuffer | null | string>(file: File) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.addEventListener('load', () => {
|
||||
resolve(reader.result as T);
|
||||
});
|
||||
reader.addEventListener('error', (error) => reject(error));
|
||||
});
|
||||
}
|
||||
const {
|
||||
acceptStr,
|
||||
handleChange,
|
||||
handleRemove,
|
||||
beforeUpload,
|
||||
innerFileList,
|
||||
customRequest,
|
||||
} = useUpload(props, emit, ossIdList, 'image');
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64<string>(file.originFileObj!);
|
||||
const { previewVisible, previewImage, handleCancel, handlePreview } =
|
||||
useImagePreview();
|
||||
|
||||
function currentPreview(file: UploadFile) {
|
||||
// 有自定义预览逻辑走自定义
|
||||
if (isFunction(props.preview)) {
|
||||
return props.preview(file);
|
||||
}
|
||||
previewImage.value = file.url || file.preview || '';
|
||||
previewOpen.value = true;
|
||||
previewTitle.value =
|
||||
file.name ||
|
||||
previewImage.value.slice(
|
||||
Math.max(0, previewImage.value.lastIndexOf('/') + 1),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemove = async (file: UploadFile) => {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
index !== -1 && fileList.value.splice(index, 1);
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('change', value);
|
||||
emit('delete', file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
previewOpen.value = false;
|
||||
previewTitle.value = '';
|
||||
};
|
||||
|
||||
const beforeUpload = async (file: File) => {
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = await checkImageFileType(file, accept);
|
||||
if (!isAct) {
|
||||
message.error($t('component.upload.acceptUpload', [accept]));
|
||||
isActMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isActMsg.value = true), 1000);
|
||||
}
|
||||
const isLt = file.size / 1024 / 1024 > maxSize;
|
||||
if (isLt) {
|
||||
message.error($t('component.upload.maxSizeMultiple', [maxSize]));
|
||||
isLtMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isLtMsg.value = true), 1000);
|
||||
}
|
||||
return (isAct && !isLt) || Upload.LIST_IGNORE;
|
||||
};
|
||||
|
||||
async function customRequest(info: UploadRequestOption<any>) {
|
||||
const { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
console.warn('upload api must exist and be a function');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 进度条事件
|
||||
const progressEvent: AxiosProgressEvent = (e) => {
|
||||
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
||||
info.onProgress!({ percent });
|
||||
};
|
||||
const res = await api?.(info.file as File, progressEvent);
|
||||
/**
|
||||
* 由getValue处理 传对象过去
|
||||
* 直接传string(id)会被转为Number
|
||||
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
|
||||
*/
|
||||
info.onSuccess!(res);
|
||||
message.success($t('component.upload.uploadSuccess'));
|
||||
// 获取
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('change', value);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
info.onError!(error);
|
||||
}
|
||||
}
|
||||
|
||||
function getValue() {
|
||||
console.log(fileList.value);
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||
.map((item: any) => {
|
||||
if (item?.response && props?.resultField) {
|
||||
return item?.response?.[props.resultField];
|
||||
}
|
||||
// ossId兼容 uid为ossId直接返回
|
||||
if (props.resultField === 'ossId' && item.uid) {
|
||||
return item.uid;
|
||||
}
|
||||
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
|
||||
if (item?.url) {
|
||||
return item.url;
|
||||
}
|
||||
// 注意这里取的key为 url
|
||||
return item?.response?.url;
|
||||
});
|
||||
// 只有一张图片 默认绑定string而非string[]
|
||||
if (props.maxNumber === 1 && list.length === 1) {
|
||||
return list[0];
|
||||
}
|
||||
// 只有一张图片 && 删除图片时 可自行修改
|
||||
if (props.maxNumber === 1 && list.length === 0) {
|
||||
return '';
|
||||
}
|
||||
return list;
|
||||
// 否则走默认预览
|
||||
return handlePreview(file);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Upload
|
||||
v-bind="$attrs"
|
||||
v-model:file-list="fileList"
|
||||
:accept="getStringAccept"
|
||||
v-model:file-list="innerFileList"
|
||||
:class="{ 'upload-animation__disabled': !withAnimation }"
|
||||
:list-type="listType"
|
||||
:accept="accept"
|
||||
:disabled="disabled"
|
||||
:directory="directory"
|
||||
:max-count="maxCount"
|
||||
:progress="{ showInfo: true }"
|
||||
:multiple="multiple"
|
||||
:before-upload="beforeUpload"
|
||||
:custom-request="customRequest"
|
||||
:disabled="disabled"
|
||||
:list-type="listType"
|
||||
:max-count="maxNumber"
|
||||
:multiple="multiple"
|
||||
:progress="{ showInfo: true }"
|
||||
@preview="handlePreview"
|
||||
@preview="currentPreview"
|
||||
@change="handleChange"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<div v-if="fileList && fileList.length < maxNumber">
|
||||
<div
|
||||
v-if="innerFileList?.length < maxCount && listType === 'picture-card'"
|
||||
>
|
||||
<PlusOutlined />
|
||||
<div style="margin-top: 8px">{{ $t('component.upload.upload') }}</div>
|
||||
<div class="mt-[8px]">{{ $t('component.upload.upload') }}</div>
|
||||
</div>
|
||||
<a-button
|
||||
v-if="innerFileList?.length < maxCount && listType !== 'picture-card'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<UploadOutlined />
|
||||
{{ $t('component.upload.upload') }}
|
||||
</a-button>
|
||||
</Upload>
|
||||
<div
|
||||
v-if="showDescription"
|
||||
class="mt-2 flex flex-wrap items-center text-[14px]"
|
||||
<slot name="helpMessage" v-bind="{ maxCount, disabled, maxSize, accept }">
|
||||
<I18nT
|
||||
v-if="helpMessage"
|
||||
scope="global"
|
||||
keypath="component.upload.uploadHelpMessage"
|
||||
tag="div"
|
||||
:class="{
|
||||
'upload-text__disabled': disabled,
|
||||
'mt-2': listType !== 'picture-card',
|
||||
}"
|
||||
>
|
||||
<template #size>
|
||||
<span
|
||||
class="text-primary mx-1 font-medium"
|
||||
:class="{ 'upload-text__disabled': disabled }"
|
||||
>
|
||||
{{ maxSize }}MB
|
||||
</span>
|
||||
</template>
|
||||
<template #ext>
|
||||
<span
|
||||
class="text-primary mx-1 font-medium"
|
||||
:class="{ 'upload-text__disabled': disabled }"
|
||||
>
|
||||
{{ acceptStr }}
|
||||
</span>
|
||||
</template>
|
||||
</I18nT>
|
||||
</slot>
|
||||
|
||||
<ImagePreviewGroup
|
||||
:preview="{
|
||||
visible: previewVisible,
|
||||
onVisibleChange: handleCancel,
|
||||
}"
|
||||
>
|
||||
请上传不超过
|
||||
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
|
||||
的
|
||||
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
|
||||
格式文件
|
||||
</div>
|
||||
<Modal
|
||||
:footer="null"
|
||||
:open="previewOpen"
|
||||
:title="previewTitle"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<img :src="previewImage" alt="" style="width: 100%" />
|
||||
</Modal>
|
||||
<Image class="hidden" :src="previewImage" />
|
||||
</ImagePreviewGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.ant-upload-select-picture-card i {
|
||||
color: #999;
|
||||
font-size: 32px;
|
||||
<style lang="scss">
|
||||
.ant-upload-select-picture-card {
|
||||
i {
|
||||
@apply text-[32px] text-[#999];
|
||||
}
|
||||
|
||||
.ant-upload-text {
|
||||
@apply mt-[8px] text-[#666];
|
||||
}
|
||||
}
|
||||
|
||||
.ant-upload-select-picture-card .ant-upload-text {
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
.ant-upload-list-picture-card {
|
||||
.ant-upload-list-item::before {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用的样式和antd保持一致
|
||||
.upload-text__disabled {
|
||||
color: rgb(50 54 57 / 25%);
|
||||
cursor: not-allowed;
|
||||
|
||||
&:where(.dark, .dark *) {
|
||||
color: rgb(242 242 242 / 25%);
|
||||
}
|
||||
}
|
||||
|
||||
// list-type: picture-card动画效果关闭样式
|
||||
.upload-animation__disabled {
|
||||
.ant-upload-animate-inline {
|
||||
animation-duration: 0s !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
26
apps/web-antd/src/components/upload/src/note.md
Normal file
26
apps/web-antd/src/components/upload/src/note.md
Normal file
@@ -0,0 +1,26 @@
|
||||
Safari在执行到beforeUpload方法
|
||||
|
||||
有两种情况
|
||||
|
||||
1. 不继续执行 也无法上传(没有调用上传)
|
||||
2. 报错
|
||||
|
||||
Unhandled Promise Rejection: TypeError: ReadableStreamBYOBReader needs a ReadableByteStreamController
|
||||
|
||||
https://github.com/oven-sh/bun/issues/12908#issuecomment-2490151231
|
||||
|
||||
刚开始以为是异步的问题 由于`file-type`调用了异步方法 调试也是在这里没有后续打印了
|
||||
|
||||
使用别的异步代码测试结果是正常上传的
|
||||
|
||||
```js
|
||||
return new Promise<FileType>((resolve) =>
|
||||
setTimeout(() => resolve(file), 2000),
|
||||
);
|
||||
```
|
||||
|
||||
根本原因在于`file-typ`库的`fileTypeFromBlob`方法不支持Safari 去掉可以正常上传
|
||||
|
||||
safari不支持`ReadableStreamBYOBReader`api
|
||||
|
||||
详见: https://github.com/sindresorhus/file-type/issues/690
|
122
apps/web-antd/src/components/upload/src/props.d.ts
vendored
Normal file
122
apps/web-antd/src/components/upload/src/props.d.ts
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { UploadFile } from 'ant-design-vue';
|
||||
import type { RcFile } from 'ant-design-vue/es/vc-upload/interface';
|
||||
|
||||
import type { UploadApi, UploadResult } from '#/api';
|
||||
import type { OssFile } from '#/api/system/oss/model';
|
||||
|
||||
import { UploadChangeParam } from 'ant-design-vue';
|
||||
|
||||
export type UploadType = 'file' | 'image';
|
||||
|
||||
/**
|
||||
* 自定义返回文件名/缩略图使用 泛型控制返回是否必填
|
||||
* type 为不同的接口返回值 需要自行if判断
|
||||
*/
|
||||
export type CustomGetter<T extends string | undefined> = (
|
||||
cb:
|
||||
| { response: OssFile; type: 'info' }
|
||||
| { response: UploadResult; type: 'upload' },
|
||||
) => T extends undefined ? string | undefined : string;
|
||||
|
||||
export interface BaseUploadProps {
|
||||
/**
|
||||
* 上传接口
|
||||
*/
|
||||
api?: UploadApi;
|
||||
/**
|
||||
* 文件上传失败 是否从展示列表中删除
|
||||
* @default true
|
||||
*/
|
||||
removeOnError?: boolean;
|
||||
/**
|
||||
* 上传成功 是否展示提示信息
|
||||
* @default true
|
||||
*/
|
||||
showSuccessMsg?: boolean;
|
||||
/**
|
||||
* 删除文件前是否需要确认
|
||||
* @default false
|
||||
*/
|
||||
removeConfirm?: boolean;
|
||||
/**
|
||||
* 同antdv参数
|
||||
*/
|
||||
accept?: string;
|
||||
/**
|
||||
* 你可能使用的是application/pdf这种mime类型, 但是这样用户可能看不懂, 在这里自定义逻辑
|
||||
* @default 原始accept
|
||||
*/
|
||||
acceptFormat?: ((accept: string) => string) | string;
|
||||
/**
|
||||
* 附带的请求参数
|
||||
*/
|
||||
data?: any;
|
||||
/**
|
||||
* 最大上传图片数量
|
||||
* maxCount为1时 会被绑定为string而非string[]
|
||||
* @default 1
|
||||
*/
|
||||
maxCount?: number;
|
||||
/**
|
||||
* 文件最大 单位M
|
||||
* @default 5
|
||||
*/
|
||||
maxSize?: number;
|
||||
/**
|
||||
* 是否禁用
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* 是否显示文案 请上传不超过...
|
||||
* @default true
|
||||
*/
|
||||
helpMessage?: boolean;
|
||||
/**
|
||||
* 是否支持多选文件,ie10+ 支持。开启后按住 ctrl 可选择多个文件。
|
||||
* @default false
|
||||
*/
|
||||
multiple?: boolean;
|
||||
/**
|
||||
* 是否支持上传文件夹
|
||||
* @default false
|
||||
*/
|
||||
directory?: boolean;
|
||||
/**
|
||||
* 是否支持拖拽上传
|
||||
* @default false
|
||||
*/
|
||||
enableDragUpload?: boolean;
|
||||
/**
|
||||
* 当ossId查询不到文件信息时 比如被删除了
|
||||
* 是否保留列表对应的ossId 默认不保留
|
||||
* @default false
|
||||
*/
|
||||
keepMissingId?: boolean;
|
||||
/**
|
||||
* 自定义文件/图片预览逻辑 比如: 你可以改为下载
|
||||
* 图片上传默认为预览
|
||||
* 文件上传默认为window.open
|
||||
* @param file file
|
||||
*/
|
||||
preview?: (file: UploadFile) => Promise<void> | void;
|
||||
/**
|
||||
* 是否在组件Unmounted时取消上传
|
||||
* @default true
|
||||
*/
|
||||
abortOnUnmounted?: boolean;
|
||||
/**
|
||||
* 自定义文件名 需要区分两个接口的返回值
|
||||
*/
|
||||
customFilename?: CustomGetter<string>;
|
||||
/**
|
||||
* 自定义缩略图 需要区分两个接口的返回值
|
||||
*/
|
||||
customThumbUrl?: CustomGetter<undefined>;
|
||||
}
|
||||
|
||||
export interface UploadEmits {
|
||||
(e: 'success', file: RcFile, response: UploadResult): void;
|
||||
(e: 'remove', file: UploadFile): void;
|
||||
(e: 'change', info: UploadChangeParam): void;
|
||||
}
|
@@ -94,7 +94,7 @@ const menus = computed(() => {
|
||||
});
|
||||
|
||||
const avatar = computed(() => {
|
||||
return userStore.userInfo?.avatar ?? preferences.app.defaultAvatar;
|
||||
return userStore.userInfo?.avatar || preferences.app.defaultAvatar;
|
||||
});
|
||||
|
||||
async function handleLogout() {
|
||||
@@ -116,7 +116,7 @@ watch(
|
||||
async (enable) => {
|
||||
if (enable) {
|
||||
await updateWatermark({
|
||||
content: `${userStore.userInfo?.username}`,
|
||||
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||
});
|
||||
} else {
|
||||
destroyWatermark();
|
||||
|
@@ -50,6 +50,10 @@
|
||||
"uploadError": "Upload failed",
|
||||
"uploading": "Uploading",
|
||||
"uploadWait": "Please wait for the file upload to finish",
|
||||
"reUploadFailed": "Re-upload failed files"
|
||||
"reUploadFailed": "Re-upload failed files",
|
||||
"uploadHelpMessage": "Please upload a file in {ext} format that does not exceed {size} .",
|
||||
"unknownFileType": "Unknown file type, unable to upload",
|
||||
"confirmDelete": "Confirm file deletion {0}?",
|
||||
"clickOrDrag": "Click or drag file to this area to upload"
|
||||
}
|
||||
}
|
||||
|
@@ -18,6 +18,10 @@
|
||||
"refresh": "Refresh",
|
||||
"generate": "Generate",
|
||||
"downloadLoading": "Downloading... Please wait.",
|
||||
"preview": "Preview"
|
||||
"preview": "Preview",
|
||||
"tip": "Tip",
|
||||
"enable": "On",
|
||||
"disable": "Off",
|
||||
"beforeCloseTip": "You have unsaved changes. Are you sure you want to exit?"
|
||||
}
|
||||
}
|
||||
|
@@ -50,6 +50,10 @@
|
||||
"uploadError": "上传失败",
|
||||
"uploading": "上传中",
|
||||
"uploadWait": "请等待文件上传结束后操作",
|
||||
"reUploadFailed": "重新上传失败文件"
|
||||
"reUploadFailed": "重新上传失败文件",
|
||||
"uploadHelpMessage": "请上传不超过{size}的{ext}格式文件",
|
||||
"unknownFileType": "未知的文件类型, 无法上传",
|
||||
"confirmDelete": "确认删除文件 {0}?",
|
||||
"clickOrDrag": "点击或拖动文件到这个区域上传"
|
||||
}
|
||||
}
|
||||
|
@@ -18,6 +18,10 @@
|
||||
"refresh": "刷新",
|
||||
"generate": "生成",
|
||||
"downloadLoading": "下载中, 请稍后...",
|
||||
"preview": "预览"
|
||||
"preview": "预览",
|
||||
"tip": "提示",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"beforeCloseTip": "您有未保存的更改,确认要退出吗?"
|
||||
}
|
||||
}
|
||||
|
@@ -46,6 +46,11 @@ export const overridesPreferences = defineOverridesPreferences({
|
||||
* 浅色sidebar
|
||||
*/
|
||||
semiDarkSidebar: false,
|
||||
/**
|
||||
* 圆角大小 换算比例为1.6px = 0.1radius
|
||||
* 这里为6px 与antd保持一致
|
||||
*/
|
||||
radius: '0.375',
|
||||
},
|
||||
/**
|
||||
* !!! 更改配置后请清空浏览器缓存
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { Router } from 'vue-router';
|
||||
|
||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
import { startProgress, stopProgress } from '@vben/utils';
|
||||
@@ -18,7 +18,7 @@ function setupCommonGuard(router: Router) {
|
||||
// 记录已经加载的页面
|
||||
const loadedPaths = new Set<string>();
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
router.beforeEach((to) => {
|
||||
to.meta.loaded = loadedPaths.has(to.path);
|
||||
|
||||
// 页面加载进度条
|
||||
@@ -56,7 +56,7 @@ function setupAccessGuard(router: Router) {
|
||||
return decodeURIComponent(
|
||||
(to.query?.redirect as string) ||
|
||||
userStore.userInfo?.homePath ||
|
||||
DEFAULT_HOME_PATH,
|
||||
preferences.app.defaultHomePath,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -75,7 +75,7 @@ function setupAccessGuard(router: Router) {
|
||||
path: LOGIN_PATH,
|
||||
// 如不需要,直接删除 query
|
||||
query:
|
||||
to.fullPath === DEFAULT_HOME_PATH
|
||||
to.fullPath === preferences.app.defaultHomePath
|
||||
? {}
|
||||
: { redirect: encodeURIComponent(to.fullPath) },
|
||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||
@@ -108,8 +108,8 @@ function setupAccessGuard(router: Router) {
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
const redirectPath = (from.query.redirect ??
|
||||
(to.path === DEFAULT_HOME_PATH
|
||||
? userInfo.homePath || DEFAULT_HOME_PATH
|
||||
(to.path === preferences.app.defaultHomePath
|
||||
? userInfo.homePath || preferences.app.defaultHomePath
|
||||
: to.fullPath)) as string;
|
||||
|
||||
return {
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { AuthPageLayout, BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
import Login from '#/views/_core/authentication/login.vue';
|
||||
|
||||
const BasicLayout = () => import('#/layouts/basic.vue');
|
||||
const AuthPageLayout = () => import('#/layouts/auth.vue');
|
||||
/** 全局404页面 */
|
||||
const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||
component: () => import('#/views/_core/fallback/not-found.vue'),
|
||||
@@ -34,7 +35,7 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
},
|
||||
name: 'Root',
|
||||
path: '/',
|
||||
redirect: DEFAULT_HOME_PATH,
|
||||
redirect: preferences.app.defaultHomePath,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
@@ -58,7 +59,7 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'Login',
|
||||
path: 'login',
|
||||
component: Login,
|
||||
component: () => import('#/views/_core/authentication/login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.login'),
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@ import type { RouteRecordRaw } from 'vue-router';
|
||||
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
|
||||
|
||||
import { coreRoutes, fallbackNotFoundRoute } from './core';
|
||||
import { workflowIframeRoutes } from './workflow-iframe';
|
||||
|
||||
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
|
||||
eager: true,
|
||||
@@ -26,11 +27,14 @@ const externalRoutes: RouteRecordRaw[] = [];
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...externalRoutes,
|
||||
...workflowIframeRoutes,
|
||||
fallbackNotFoundRoute,
|
||||
];
|
||||
|
||||
/** 基本路由(登录, 第三方登录, 注册等) + workflowIframe路由不需要拦截 */
|
||||
const basicRoutes = [...coreRoutes, ...workflowIframeRoutes];
|
||||
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
const coreRouteNames = traverseTreeValues(basicRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
|
@@ -2,6 +2,11 @@ import type { RouteRecordStringComponent } from '@vben/types';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
const {
|
||||
version,
|
||||
// vite inject-metadata 插件注入的全局变量
|
||||
} = __VBEN_ADMIN_METADATA__ || {};
|
||||
|
||||
/**
|
||||
* 该文件放非后台返回的路由 比如个人中心 等需要跳转显示的页面
|
||||
*/
|
||||
@@ -12,6 +17,7 @@ const localRoutes: RouteRecordStringComponent[] = [
|
||||
icon: 'mingcute:profile-line',
|
||||
title: $t('ui.widgets.profile'),
|
||||
hideInMenu: true,
|
||||
requireHomeRedirect: true,
|
||||
},
|
||||
name: 'Profile',
|
||||
path: '/profile',
|
||||
@@ -23,6 +29,7 @@ const localRoutes: RouteRecordStringComponent[] = [
|
||||
icon: 'ant-design:setting-outlined',
|
||||
title: 'oss配置',
|
||||
hideInMenu: true,
|
||||
requireHomeRedirect: true,
|
||||
},
|
||||
name: 'OssConfig',
|
||||
path: '/system/oss-config',
|
||||
@@ -34,6 +41,7 @@ const localRoutes: RouteRecordStringComponent[] = [
|
||||
icon: 'tabler:code',
|
||||
title: '生成配置',
|
||||
hideInMenu: true,
|
||||
requireHomeRedirect: true,
|
||||
},
|
||||
name: 'GenConfig',
|
||||
path: '/code-gen/edit/:tableId',
|
||||
@@ -45,6 +53,7 @@ const localRoutes: RouteRecordStringComponent[] = [
|
||||
icon: 'eos-icons:role-binding-outlined',
|
||||
title: '分配角色',
|
||||
hideInMenu: true,
|
||||
requireHomeRedirect: true,
|
||||
},
|
||||
name: 'RoleAssign',
|
||||
path: '/system/role-assign/:roleId',
|
||||
@@ -56,10 +65,14 @@ const localRoutes: RouteRecordStringComponent[] = [
|
||||
icon: 'fluent-mdl2:flow',
|
||||
title: '流程设计',
|
||||
hideInMenu: true,
|
||||
requireHomeRedirect: true,
|
||||
},
|
||||
name: 'WorkflowDesigner',
|
||||
path: '/workflow/designer',
|
||||
},
|
||||
/**
|
||||
* 需要添加iframe路由 同目录的./workflow-iframe.ts
|
||||
*/
|
||||
{
|
||||
component: 'workflow/leave/leave-form',
|
||||
meta: {
|
||||
@@ -67,22 +80,11 @@ const localRoutes: RouteRecordStringComponent[] = [
|
||||
title: '请假申请',
|
||||
activePath: '/demo/leave',
|
||||
hideInMenu: true,
|
||||
requireHomeRedirect: true,
|
||||
},
|
||||
name: 'WorkflowLeaveIndex',
|
||||
path: '/workflow/leaveEdit/index',
|
||||
},
|
||||
// 这里是iframe使用的 去掉外层的BasicLayout
|
||||
{
|
||||
component: 'workflow/leave/leave-form',
|
||||
meta: {
|
||||
title: '请假申请',
|
||||
hideInMenu: true,
|
||||
// 不使用基础布局(仅在顶级生效)
|
||||
noBasicLayout: true,
|
||||
},
|
||||
name: 'WorkflowLeaveInner',
|
||||
path: '/workflow/leaveEdit/index/iframe',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -129,6 +131,18 @@ export const localMenuList: RouteRecordStringComponent[] = [
|
||||
title: $t('demos.vben.document'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'V5UpdateLog',
|
||||
path: '/changelog',
|
||||
component: '/演示使用自行删除/changelog/index',
|
||||
meta: {
|
||||
icon: 'lucide:book-open-text',
|
||||
keepAlive: true,
|
||||
title: '更新记录',
|
||||
badge: `当前: ${version}`,
|
||||
badgeVariants: 'bg-primary',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
18
apps/web-antd/src/router/routes/workflow-iframe.ts
Normal file
18
apps/web-antd/src/router/routes/workflow-iframe.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { RouteRecordRaw } from '@vben/types';
|
||||
|
||||
/**
|
||||
* 该文件存放workflow表单的iframe内嵌路由
|
||||
* 不需要权限认证 少走两个接口😅
|
||||
*/
|
||||
export const workflowIframeRoutes: RouteRecordRaw[] = [
|
||||
// 这里是iframe使用的 去掉外层的BasicLayout
|
||||
{
|
||||
name: 'WorkflowLeaveInner',
|
||||
path: '/workflow/leaveEdit/index/iframe',
|
||||
component: () => import('#/views/workflow/leave/leave-form.vue'),
|
||||
meta: {
|
||||
hideInTab: true,
|
||||
title: '请假申请',
|
||||
},
|
||||
},
|
||||
];
|
@@ -4,7 +4,8 @@ import type { UserInfo } from '@vben/types';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||
|
||||
import { notification } from 'ant-design-vue';
|
||||
@@ -55,7 +56,9 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
} else {
|
||||
onSuccess ? await onSuccess?.() : await router.push(DEFAULT_HOME_PATH);
|
||||
onSuccess
|
||||
? await onSuccess?.()
|
||||
: await router.push(preferences.app.defaultHomePath);
|
||||
}
|
||||
|
||||
if (userInfo?.realName) {
|
||||
|
@@ -59,7 +59,11 @@ export const useDictStore = defineStore('app-dict', () => {
|
||||
}
|
||||
|
||||
function resetCache() {
|
||||
dictRequestCache.clear();
|
||||
dictOptionsMap.clear();
|
||||
/**
|
||||
* 不需要清空dictRequestCache 每次请求成功/失败都清空key
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -27,6 +27,10 @@ function fetchAndCacheDictData<T>(
|
||||
// 内部处理了push的逻辑 这里不用push
|
||||
setDictInfo(dictName, resp, formatNumber);
|
||||
})
|
||||
.catch(() => {
|
||||
// 401时 移除字典缓存 下次登录重新获取
|
||||
dictRequestCache.delete(dictName);
|
||||
})
|
||||
.finally(() => {
|
||||
// 移除请求状态缓存
|
||||
/**
|
||||
|
126
apps/web-antd/src/utils/popup.ts
Normal file
126
apps/web-antd/src/utils/popup.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { ExtendedFormApi } from '@vben/common-ui';
|
||||
import type { MaybePromise } from '@vben/types';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { isFunction } from 'lodash-es';
|
||||
|
||||
interface BeforeCloseDiffProps {
|
||||
/**
|
||||
* 初始化值如何获取
|
||||
* @returns Promise<string>
|
||||
*/
|
||||
initializedGetter: () => MaybePromise<string>;
|
||||
/**
|
||||
* 当前值如何获取
|
||||
* @returns Promise<string>
|
||||
*/
|
||||
currentGetter: () => MaybePromise<string>;
|
||||
/**
|
||||
* 自定义比较函数
|
||||
* @param init 初始值
|
||||
* @param current 当前值
|
||||
* @returns boolean
|
||||
*/
|
||||
compare?: (init: string, current: string) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于Drawer/Modal使用 判断表单是否有变动来决定是否弹窗提示
|
||||
* @param props props
|
||||
* @returns hook
|
||||
*/
|
||||
export function useBeforeCloseDiff(props: BeforeCloseDiffProps) {
|
||||
const { initializedGetter, currentGetter, compare } = props;
|
||||
/**
|
||||
* 记录初始值 json
|
||||
*/
|
||||
const initialized = ref<string>('');
|
||||
/**
|
||||
* 是否已经初始化了 通过这个值判断是否需要进行对比 为false直接关闭 不弹窗
|
||||
*/
|
||||
const isInitialized = ref(false);
|
||||
|
||||
/**
|
||||
* 标记是否已经完成初始化 后续需要进行对比
|
||||
* @param data 自定义初始化数据 可选
|
||||
*/
|
||||
async function markInitialized(data?: string) {
|
||||
initialized.value = data || (await initializedGetter());
|
||||
isInitialized.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置初始化状态 需要在closed前调用 或者打开窗口时
|
||||
*/
|
||||
function resetInitialized() {
|
||||
initialized.value = '';
|
||||
isInitialized.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供给useVbenForm/useVbenDrawer使用
|
||||
* @returns 是否允许关闭
|
||||
*/
|
||||
async function onBeforeClose(): Promise<boolean> {
|
||||
// 如果还未初始化,直接允许关闭
|
||||
if (!isInitialized.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前表单数据
|
||||
const current = await currentGetter();
|
||||
// 自定义比较的情况
|
||||
if (isFunction(compare) && compare(initialized.value, current)) {
|
||||
return true;
|
||||
} else {
|
||||
// 如果数据没有变化,直接允许关闭
|
||||
if (current === initialized.value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 数据有变化,显示确认对话框
|
||||
return new Promise<boolean>((resolve) => {
|
||||
Modal.confirm({
|
||||
title: $t('pages.common.tip'),
|
||||
content: $t('pages.common.beforeCloseTip'),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: $t('common.cancel'),
|
||||
okText: $t('common.confirm'),
|
||||
onOk: () => {
|
||||
resolve(true);
|
||||
isInitialized.value = false;
|
||||
},
|
||||
onCancel: () => resolve(false),
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to compare data:', error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onBeforeClose,
|
||||
markInitialized,
|
||||
resetInitialized,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 给useVbenForm使用的 封装函数
|
||||
* @param formApi 表单实例
|
||||
* @returns getter
|
||||
*/
|
||||
export function defaultFormValueGetter(formApi: ExtendedFormApi) {
|
||||
return async () => {
|
||||
const v = await formApi.getValues();
|
||||
return JSON.stringify(v);
|
||||
};
|
||||
}
|
@@ -20,9 +20,9 @@ import {
|
||||
MicromessengerIcon,
|
||||
OperaIcon,
|
||||
OSXIcon,
|
||||
QQIcon,
|
||||
QuarkIcon,
|
||||
SafariIcon,
|
||||
SvgQQIcon,
|
||||
UcIcon,
|
||||
WindowsIcon,
|
||||
} from '@vben/icons';
|
||||
@@ -116,7 +116,7 @@ export function renderHttpMethodTag(type: string) {
|
||||
return <Tag color={color}>{title}</Tag>;
|
||||
}
|
||||
|
||||
export function renderDictTag(value: string, dicts: DictData[]) {
|
||||
export function renderDictTag(value: number | string, dicts: DictData[]) {
|
||||
return <DictTag dicts={dicts} value={value}></DictTag>;
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export function renderDictTags(
|
||||
* @param dictName dictName
|
||||
* @returns tag
|
||||
*/
|
||||
export function renderDict(value: string, dictName: string) {
|
||||
export function renderDict(value: number | string, dictName: string) {
|
||||
const dictInfo = getDictOptions(dictName);
|
||||
return renderDictTag(value, dictInfo);
|
||||
}
|
||||
@@ -197,7 +197,7 @@ const browserOptions = [
|
||||
{ icon: MicromessengerIcon, value: 'windowswechat' },
|
||||
{ icon: QuarkIcon, value: 'quark' },
|
||||
{ icon: MicromessengerIcon, value: 'wxwork' },
|
||||
{ icon: QQIcon, value: 'qq' },
|
||||
{ icon: SvgQQIcon, value: 'qq' },
|
||||
{ icon: DingtalkIcon, value: 'dingtalk' },
|
||||
{ icon: UcIcon, value: 'uc' },
|
||||
{ icon: BaiduIcon, value: 'baidu' },
|
||||
|
@@ -6,6 +6,7 @@ import type { TenantResp } from '#/api';
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||
import { DEFAULT_TENANT_ID } from '@vben/constants';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Alert, message } from 'ant-design-vue';
|
||||
@@ -17,7 +18,7 @@ import { useAuthStore } from '#/store';
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
const CODE_LENGTH = 4;
|
||||
|
||||
const tenantInfo = ref<TenantResp>({
|
||||
tenantEnabled: false,
|
||||
@@ -50,7 +51,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
})),
|
||||
placeholder: $t('authentication.selectAccount'),
|
||||
},
|
||||
defaultValue: '000000',
|
||||
defaultValue: DEFAULT_TENANT_ID,
|
||||
dependencies: {
|
||||
if: () => tenantInfo.value.tenantEnabled,
|
||||
triggerFields: [''],
|
||||
@@ -84,8 +85,8 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
: $t('authentication.sendCode');
|
||||
return text;
|
||||
},
|
||||
// 验证码长度 在这设置
|
||||
codeLength: 4,
|
||||
// 验证码长度
|
||||
codeLength: CODE_LENGTH,
|
||||
placeholder: $t('authentication.code'),
|
||||
handleSendCode: async () => {
|
||||
const { valid, value } = await form.validateField('phoneNumber');
|
||||
|
@@ -7,6 +7,7 @@ import type { CaptchaResponse } from '#/api/core/captcha';
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { AuthenticationLogin, z } from '@vben/common-ui';
|
||||
import { DEFAULT_TENANT_ID } from '@vben/constants';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { omit } from 'lodash-es';
|
||||
@@ -15,6 +16,7 @@ import { tenantList } from '#/api';
|
||||
import { captchaImage } from '#/api/core/captcha';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
import { useLoginTenantId } from '../oauth-common';
|
||||
import OAuthLogin from './oauth-login.vue';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
@@ -28,13 +30,23 @@ const captchaInfo = ref<CaptchaResponse>({
|
||||
img: '',
|
||||
uuid: '',
|
||||
});
|
||||
// 验证码loading
|
||||
const captchaLoading = ref(false);
|
||||
|
||||
async function loadCaptcha() {
|
||||
const resp = await captchaImage();
|
||||
if (resp.captchaEnabled) {
|
||||
resp.img = `data:image/png;base64,${resp.img}`;
|
||||
try {
|
||||
captchaLoading.value = true;
|
||||
|
||||
const resp = await captchaImage();
|
||||
if (resp.captchaEnabled) {
|
||||
resp.img = `data:image/png;base64,${resp.img}`;
|
||||
}
|
||||
captchaInfo.value = resp;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
captchaLoading.value = false;
|
||||
}
|
||||
captchaInfo.value = resp;
|
||||
}
|
||||
|
||||
const tenantInfo = ref<TenantResp>({
|
||||
@@ -56,6 +68,8 @@ onMounted(async () => {
|
||||
await Promise.all([loadCaptcha(), loadTenant()]);
|
||||
});
|
||||
|
||||
const { loginTenantId } = useLoginTenantId();
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
@@ -69,16 +83,13 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
})),
|
||||
placeholder: $t('authentication.selectAccount'),
|
||||
},
|
||||
defaultValue: '000000',
|
||||
defaultValue: DEFAULT_TENANT_ID,
|
||||
dependencies: {
|
||||
if: () => tenantInfo.value.tenantEnabled,
|
||||
// 这里大致上是watch的一个效果
|
||||
componentProps: (model) => {
|
||||
localStorage.setItem(
|
||||
'__oauth_tenant_id',
|
||||
model?.tenantId ?? '000000',
|
||||
);
|
||||
return {};
|
||||
// 可以把这里当做watch
|
||||
trigger: (model) => {
|
||||
// 给oauth登录使用
|
||||
loginTenantId.value = model?.tenantId ?? DEFAULT_TENANT_ID;
|
||||
},
|
||||
triggerFields: ['', 'tenantId'],
|
||||
},
|
||||
@@ -115,6 +126,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
class: 'focus:border-primary',
|
||||
onCaptchaClick: loadCaptcha,
|
||||
placeholder: $t('authentication.code'),
|
||||
loading: captchaLoading.value,
|
||||
},
|
||||
dependencies: {
|
||||
if: () => captchaInfo.value.captchaEnabled,
|
||||
@@ -122,7 +134,9 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.verifyRequiredTip') }),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
@@ -3,21 +3,16 @@ import { $t } from '@vben/locales';
|
||||
|
||||
import { Col, Row, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { accountBindList } from '../oauth-common';
|
||||
import { accountBindList, handleAuthBinding } from '../oauth-common';
|
||||
|
||||
defineOptions({
|
||||
name: 'OAuthLogin',
|
||||
});
|
||||
|
||||
/**
|
||||
* 有action方法才会显示
|
||||
*/
|
||||
const clientList = accountBindList.filter((item) => item.action);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full sm:mx-auto md:max-w-md">
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="my-4 flex items-center justify-between">
|
||||
<span class="border-input w-[35%] border-b dark:border-gray-600"></span>
|
||||
<span class="text-muted-foreground text-center text-xs uppercase">
|
||||
{{ $t('authentication.thirdPartyLogin') }}
|
||||
@@ -26,15 +21,20 @@ const clientList = accountBindList.filter((item) => item.action);
|
||||
</div>
|
||||
<Row class="enter-x flex items-center justify-evenly">
|
||||
<!-- todo 这里在点击登录时要disabled -->
|
||||
<Col v-for="item in clientList" :key="item.key" :span="4" class="my-2">
|
||||
<Col
|
||||
v-for="item in accountBindList"
|
||||
:key="item.source"
|
||||
:span="4"
|
||||
class="my-2"
|
||||
>
|
||||
<Tooltip :title="`${item.title}登录`">
|
||||
<span class="flex cursor-pointer items-center justify-center">
|
||||
<component
|
||||
:is="item.avatar"
|
||||
v-if="item.avatar"
|
||||
:style="{ color: item.color }"
|
||||
:is="item.avatar"
|
||||
:style="item?.style ?? {}"
|
||||
class="size-[24px]"
|
||||
@click="item.action"
|
||||
@click="handleAuthBinding(item.source)"
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
@@ -1,104 +1,102 @@
|
||||
import type { Component } from 'vue';
|
||||
import type { Component, CSSProperties } from 'vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { DEFAULT_TENANT_ID } from '@vben/constants';
|
||||
import {
|
||||
AlipayIcon,
|
||||
DingdingIcon,
|
||||
GiteeIcon,
|
||||
GithubOAuthIcon,
|
||||
TaobaoIcon,
|
||||
SvgMaxKeyIcon,
|
||||
SvgTopiamIcon,
|
||||
SvgWechatIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import { createGlobalState } from '@vueuse/core';
|
||||
|
||||
import { authBinding } from '#/api/core/auth';
|
||||
|
||||
/**
|
||||
* @description: 菜单
|
||||
* @param key key
|
||||
* @description: oauth登录
|
||||
* @param title 标题
|
||||
* @param description 描述
|
||||
* @param extra 按钮文字
|
||||
* @param avatar 图标
|
||||
* @param color 图标颜色可直接写英文颜色/hex
|
||||
*/
|
||||
export interface ListItem {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
extra?: string;
|
||||
avatar?: Component;
|
||||
color?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 绑定账号
|
||||
* @param source 来源 如gitee github 与后端的social-callback?source=xxx对应
|
||||
* @param bound 是否已经绑定
|
||||
* @param action 账号绑定回调
|
||||
*/
|
||||
export interface BindItem extends ListItem {
|
||||
source: string;
|
||||
bound?: boolean;
|
||||
action?: (source: string) => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* todo tenantId
|
||||
* 绑定授权从userStore.userInfo获取
|
||||
* 登录从localStorage获取
|
||||
* 这里存储登录页的tenantId 由于个人中心也会用到 需要共享
|
||||
* 所以使用`createGlobalState`
|
||||
* @see https://vueuse.org/shared/createGlobalState/
|
||||
*/
|
||||
export const useLoginTenantId = createGlobalState(() => {
|
||||
const loginTenantId = ref(DEFAULT_TENANT_ID);
|
||||
|
||||
return {
|
||||
loginTenantId,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 绑定授权
|
||||
* @param source
|
||||
*/
|
||||
async function handleAuthBinding(source: string) {
|
||||
const tenantId = localStorage.getItem('__oauth_tenant_id') ?? '000000';
|
||||
export async function handleAuthBinding(source: string) {
|
||||
const { loginTenantId } = useLoginTenantId();
|
||||
// 这里返回打开授权页面的链接
|
||||
const href = await authBinding(source, tenantId);
|
||||
const href = await authBinding(source, loginTenantId.value);
|
||||
window.location.href = href;
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号绑定 list
|
||||
* 添加账号绑定只需要在这里增加即可
|
||||
* 添加过的项目会在个人主页-绑定账号中显示
|
||||
* action不为空的会在登录页显示
|
||||
*/
|
||||
export const accountBindList: BindItem[] = [
|
||||
{
|
||||
avatar: TaobaoIcon,
|
||||
color: '#ff4000',
|
||||
description: '绑定淘宝账号',
|
||||
key: '1',
|
||||
source: 'taobao',
|
||||
title: '淘宝',
|
||||
},
|
||||
{
|
||||
avatar: AlipayIcon,
|
||||
color: '#2eabff',
|
||||
description: '绑定支付宝账号',
|
||||
key: '2',
|
||||
source: 'alipay',
|
||||
title: '支付宝',
|
||||
},
|
||||
{
|
||||
avatar: DingdingIcon,
|
||||
color: '#2eabff',
|
||||
description: '绑定钉钉账号',
|
||||
key: '3',
|
||||
source: 'ding',
|
||||
title: '钉钉',
|
||||
},
|
||||
{
|
||||
action: () => handleAuthBinding('gitee'),
|
||||
avatar: GiteeIcon,
|
||||
color: '#c71d23',
|
||||
description: '绑定GITEE账号',
|
||||
key: '4',
|
||||
description: '绑定Gitee账号',
|
||||
source: 'gitee',
|
||||
title: 'GITEE',
|
||||
title: 'Gitee',
|
||||
style: { color: '#c71d23' },
|
||||
},
|
||||
{
|
||||
action: () => handleAuthBinding('github'),
|
||||
avatar: GithubOAuthIcon,
|
||||
color: '',
|
||||
description: '绑定GITHUB账号',
|
||||
key: '5',
|
||||
description: '绑定Github账号',
|
||||
source: 'github',
|
||||
title: 'GITHUB',
|
||||
title: 'Github',
|
||||
},
|
||||
{
|
||||
avatar: SvgMaxKeyIcon,
|
||||
description: '绑定MaxKey账号',
|
||||
source: 'maxkey',
|
||||
title: 'MaxKey',
|
||||
},
|
||||
{
|
||||
avatar: SvgTopiamIcon,
|
||||
description: '绑定topiam账号',
|
||||
source: 'topiam',
|
||||
title: 'Topiam',
|
||||
},
|
||||
{
|
||||
avatar: SvgWechatIcon,
|
||||
description: '绑定wechat账号',
|
||||
source: 'wechat',
|
||||
title: 'Wechat',
|
||||
},
|
||||
];
|
||||
|
@@ -7,27 +7,12 @@ import { computed, ref, unref } from 'vue';
|
||||
|
||||
import { useVbenVxeGrid } from '@vben/plugins/vxe-table';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Card,
|
||||
List,
|
||||
ListItem,
|
||||
message,
|
||||
Modal,
|
||||
} from 'ant-design-vue';
|
||||
import { Alert, Avatar, Card, List, ListItem, Modal } from 'ant-design-vue';
|
||||
|
||||
import { authUnbinding } from '#/api';
|
||||
import { socialList } from '#/api/system/social';
|
||||
|
||||
import { accountBindList } from '../../oauth-common';
|
||||
|
||||
/**
|
||||
* 没有传递action事件则不支持绑定 弹出默认提示
|
||||
*/
|
||||
function defaultTip(title: string) {
|
||||
message.info({ content: `暂不支持绑定${title}` });
|
||||
}
|
||||
import { accountBindList, handleAuthBinding } from '../../oauth-common';
|
||||
|
||||
function buttonText(item: BindItem) {
|
||||
return item.bound ? '已绑定' : '绑定';
|
||||
@@ -46,12 +31,6 @@ const bindList = computed<BindItem[]>(() => {
|
||||
});
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'source',
|
||||
@@ -147,14 +126,12 @@ function handleUnbind(record: Record<string, any>) {
|
||||
<ListItem>
|
||||
<Card>
|
||||
<div class="flex w-full items-center gap-4">
|
||||
<div>
|
||||
<component
|
||||
:is="item.avatar"
|
||||
v-if="item.avatar"
|
||||
:style="{ color: item.color }"
|
||||
class="size-[40px]"
|
||||
/>
|
||||
</div>
|
||||
<component
|
||||
:is="item.avatar"
|
||||
v-if="item.avatar"
|
||||
:style="item?.style ?? {}"
|
||||
class="size-[40px]"
|
||||
/>
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h4
|
||||
@@ -170,9 +147,7 @@ function handleUnbind(record: Record<string, any>) {
|
||||
:disabled="item.bound"
|
||||
size="small"
|
||||
type="link"
|
||||
@click="
|
||||
item.action ? item.action() : defaultTip(item.title)
|
||||
"
|
||||
@click="handleAuthBinding(item.source)"
|
||||
>
|
||||
{{ buttonText(item) }}
|
||||
</a-button>
|
||||
@@ -191,10 +166,6 @@ function handleUnbind(record: Record<string, any>) {
|
||||
</span>
|
||||
中accountBindList按模板添加
|
||||
</p>
|
||||
<p>
|
||||
添加对应模板后会在此处显示绑定, 但只有
|
||||
<span class="font-bold">实现了action才能在登录页显示</span>
|
||||
</p>
|
||||
</template>
|
||||
</Alert>
|
||||
</div>
|
||||
|
@@ -24,7 +24,7 @@ defineEmits<{
|
||||
}>();
|
||||
|
||||
const avatar = computed(
|
||||
() => props.profile?.user.avatar ?? preferences.app.defaultAvatar,
|
||||
() => props.profile?.user.avatar || preferences.app.defaultAvatar,
|
||||
);
|
||||
|
||||
const { isDark } = usePreferences();
|
||||
|
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TabPane, Tabs } from 'ant-design-vue';
|
||||
|
||||
import AccountBind from './components/account-bind.vue';
|
||||
@@ -8,52 +6,34 @@ import BaseSetting from './components/base-setting.vue';
|
||||
import OnlineDevice from './components/online-device.vue';
|
||||
import SecureSetting from './components/secure-setting.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
AccountBind,
|
||||
BaseSetting,
|
||||
OnlineDevice,
|
||||
SecureSetting,
|
||||
TabPane,
|
||||
Tabs,
|
||||
const settingList = [
|
||||
{
|
||||
component: BaseSetting,
|
||||
key: '1',
|
||||
name: '基本设置',
|
||||
},
|
||||
setup() {
|
||||
const settingList = [
|
||||
{
|
||||
component: 'BaseSetting',
|
||||
key: '1',
|
||||
name: '基本设置',
|
||||
},
|
||||
{
|
||||
component: 'SecureSetting',
|
||||
key: '2',
|
||||
name: '安全设置',
|
||||
},
|
||||
{
|
||||
component: 'AccountBind',
|
||||
key: '3',
|
||||
name: '账号绑定',
|
||||
},
|
||||
{
|
||||
component: 'OnlineDevice',
|
||||
key: '4',
|
||||
name: '在线设备',
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
settingList,
|
||||
};
|
||||
{
|
||||
component: SecureSetting,
|
||||
key: '2',
|
||||
name: '安全设置',
|
||||
},
|
||||
});
|
||||
{
|
||||
component: AccountBind,
|
||||
key: '3',
|
||||
name: '账号绑定',
|
||||
},
|
||||
{
|
||||
component: OnlineDevice,
|
||||
key: '4',
|
||||
name: '在线设备',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tabs class="bg-background rounded-[var(--radius)] px-[16px] lg:flex-1">
|
||||
<template v-for="item in settingList" :key="item.key">
|
||||
<TabPane :tab="item.name">
|
||||
<component :is="item.component" v-bind="$attrs" />
|
||||
</TabPane>
|
||||
</template>
|
||||
<TabPane v-for="item in settingList" :key="item.key" :tab="item.name">
|
||||
<component :is="item.component" v-bind="$attrs" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</template>
|
||||
|
@@ -4,7 +4,8 @@ import type { AuthApi } from '#/api';
|
||||
import { onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { DEFAULT_HOME_PATH } from '@vben/constants';
|
||||
import { DEFAULT_TENANT_ID } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
@@ -22,7 +23,7 @@ const stateJson = JSON.parse(atob(state));
|
||||
// 来源
|
||||
const source = route.query.source as string;
|
||||
// 租户ID
|
||||
const defaultTenantId = '000000';
|
||||
const defaultTenantId = DEFAULT_TENANT_ID;
|
||||
const tenantId = (stateJson.tenantId as string) ?? defaultTenantId;
|
||||
const domain = stateJson.domain as string;
|
||||
|
||||
@@ -44,7 +45,7 @@ onMounted(async () => {
|
||||
try {
|
||||
// 已经实现的平台
|
||||
const currentClient = accountBindList.find(
|
||||
(item) => item.source === source && item.action,
|
||||
(item) => item.source === source,
|
||||
);
|
||||
if (!currentClient) {
|
||||
message.error({ content: `未找到${source}平台` });
|
||||
@@ -70,7 +71,7 @@ onMounted(async () => {
|
||||
// 500 你还没有绑定第三方账号,绑定后才可以登录!
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
router.push(DEFAULT_HOME_PATH);
|
||||
router.push(preferences.app.defaultHomePath);
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
|
@@ -15,10 +15,10 @@ import {
|
||||
} from '@vben/icons';
|
||||
|
||||
import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user