mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-26 00:26:20 +08:00
Compare commits
263 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7c7051a11e | ||
![]() |
aa27a2f7a1 | ||
![]() |
9ee6d06d50 | ||
![]() |
0cc1cb5a7b | ||
![]() |
0a9fc4e02d | ||
![]() |
be840460d8 | ||
![]() |
cb45987fe2 | ||
![]() |
5ffd7db8e0 | ||
![]() |
14377705e7 | ||
![]() |
b985ff0584 | ||
![]() |
b148b8ec92 | ||
![]() |
79de6bcbf7 | ||
![]() |
14bd6dd25d | ||
![]() |
7f269e0d69 | ||
![]() |
4baec83db5 | ||
![]() |
f7a4d13a4c | ||
![]() |
0936861da1 | ||
![]() |
3318d76bab | ||
![]() |
8f3881eabf | ||
![]() |
5252480b09 | ||
![]() |
d18f56177c | ||
![]() |
333998b518 | ||
![]() |
3fb4fba1cb | ||
![]() |
c7e6210c8d | ||
![]() |
d864085c13 | ||
![]() |
fcdc1a1602 | ||
![]() |
bf7496f0d5 | ||
![]() |
9700150653 | ||
![]() |
f0e9e55af2 | ||
![]() |
ff88274554 | ||
![]() |
afce9dc5c0 | ||
![]() |
b5700bd0b1 | ||
![]() |
a8c4786311 | ||
![]() |
2971ccc0b7 | ||
![]() |
4a2c7b313f | ||
![]() |
36bf6fc149 | ||
![]() |
f46ec30995 | ||
![]() |
9bd5a190c2 | ||
![]() |
86da3cedc2 | ||
![]() |
329a176a5c | ||
![]() |
9379093a4f | ||
![]() |
c9014d0338 | ||
![]() |
ed26dca64e | ||
![]() |
08c6496e24 | ||
![]() |
a8c5df38e9 | ||
![]() |
71e8d12b70 | ||
![]() |
d216fdca44 | ||
![]() |
384c5d7dbb | ||
![]() |
b0ad08dbbc | ||
![]() |
3600603016 | ||
![]() |
cde1a85394 | ||
![]() |
c623604ea9 | ||
![]() |
7933da8f66 | ||
![]() |
ecf518bb02 | ||
![]() |
1d9f1be004 | ||
![]() |
44138f578f | ||
![]() |
0e3abc2b53 | ||
![]() |
a96be3db98 | ||
![]() |
d6f239c564 | ||
![]() |
166e9a0e82 | ||
![]() |
06ccad9db0 | ||
![]() |
18722ce434 | ||
![]() |
a0feeb1966 | ||
![]() |
df6341f0b8 | ||
![]() |
dbc0b7e4a9 | ||
![]() |
aa2907323f | ||
![]() |
96d2bc52e9 | ||
![]() |
e91e4e0eea | ||
![]() |
c2b5f6497d | ||
![]() |
3c2d325d8c | ||
![]() |
a77bb8e68d | ||
![]() |
870cd86393 | ||
![]() |
0b650367f3 | ||
![]() |
1616a06bfd | ||
![]() |
3f0d30897f | ||
![]() |
66c1d390b9 | ||
![]() |
03ceb2aac5 | ||
![]() |
39888cebaa | ||
![]() |
efb69fc75f | ||
![]() |
711a179c69 | ||
![]() |
3133f8f8b9 | ||
![]() |
f0a43912d1 | ||
![]() |
b92ac5c36d | ||
![]() |
504070f3eb | ||
![]() |
feab6b3b30 | ||
![]() |
2d4ac33046 | ||
![]() |
17e2a02281 | ||
![]() |
096545c5a1 | ||
![]() |
04dff33ac5 | ||
![]() |
cfa18c2b8e | ||
![]() |
13354955db | ||
![]() |
bb683804f4 | ||
![]() |
e2a577de24 | ||
![]() |
89d963c81a | ||
![]() |
b37ed48b9d | ||
![]() |
4b9cfcb867 | ||
![]() |
f86c9f90ad | ||
![]() |
31a6ab59fb | ||
![]() |
34789645f7 | ||
![]() |
f380452ef0 | ||
![]() |
decd9c55e5 | ||
![]() |
e815f0ff89 | ||
![]() |
5ea6b4a8d8 | ||
![]() |
a53ca3faf1 | ||
![]() |
86fdd6c93b | ||
![]() |
0e0661fe02 | ||
![]() |
86ce65e0ea | ||
![]() |
c3eb4fab13 | ||
![]() |
7a476372e1 | ||
![]() |
5e421ce607 | ||
![]() |
1d8676f456 | ||
![]() |
0c3dd92592 | ||
![]() |
d33261d0c2 | ||
![]() |
7041c6a106 | ||
![]() |
12ffb310bf | ||
![]() |
d9799fec70 | ||
![]() |
4570d5b54b | ||
![]() |
d49e3e81a4 | ||
![]() |
579b1b486c | ||
![]() |
eba372062e | ||
![]() |
c9ccd2bbab | ||
![]() |
5aff8bac10 | ||
![]() |
1a12687027 | ||
![]() |
a221d2b491 | ||
![]() |
ccd99eb24d | ||
![]() |
c5c6760b5d | ||
![]() |
c07281bf41 | ||
![]() |
24bad09c74 | ||
![]() |
cddf71e600 | ||
![]() |
9f82052c71 | ||
![]() |
e0eb57d38d | ||
![]() |
b6b97accb1 | ||
![]() |
799934171a | ||
![]() |
10ebf03698 | ||
![]() |
cd258fbb52 | ||
![]() |
6cba181fad | ||
![]() |
f9504cece3 | ||
![]() |
182f1c9da8 | ||
![]() |
e7b009786b | ||
![]() |
5262233312 | ||
![]() |
a9f9031f49 | ||
![]() |
061fcf926d | ||
![]() |
7e7a5f3fd4 | ||
![]() |
f8bb396dc4 | ||
![]() |
a832edce0d | ||
![]() |
67d1f299b3 | ||
![]() |
cb7c0ecaa2 | ||
![]() |
e225159cce | ||
![]() |
195ceec9b4 | ||
![]() |
5bd73867b6 | ||
![]() |
22e6f28464 | ||
![]() |
5611f6c7f5 | ||
![]() |
3f0f4d50a1 | ||
![]() |
2d0859a727 | ||
![]() |
509b268fba | ||
![]() |
c3129663eb | ||
![]() |
816d1f5a69 | ||
![]() |
8cc903c0e1 | ||
![]() |
13087a10b7 | ||
![]() |
27a3888e35 | ||
![]() |
fb0ec05ff8 | ||
![]() |
76c4aa2c55 | ||
![]() |
e1c503e51e | ||
![]() |
5965755caa | ||
![]() |
1ad54561b0 | ||
![]() |
42e322012c | ||
![]() |
8cf6e8ec75 | ||
![]() |
79d4d2fb22 | ||
![]() |
b785bc5704 | ||
![]() |
6719e2679f | ||
![]() |
cb9c8db5ba | ||
![]() |
a2637313f8 | ||
![]() |
467689525f | ||
![]() |
1a04a05b79 | ||
![]() |
b8bffd884c | ||
![]() |
624beb6fa0 | ||
![]() |
7606b86854 | ||
![]() |
e10cbe23b9 | ||
![]() |
d34838bdd8 | ||
![]() |
c979c23e6b | ||
![]() |
516d0b8dc8 | ||
![]() |
99c7fd72f8 | ||
![]() |
2828e7a7b6 | ||
![]() |
16162c01ed | ||
![]() |
bbbdbfa912 | ||
![]() |
06cccc53fa | ||
![]() |
801c640724 | ||
![]() |
081d2aed23 | ||
![]() |
4d81b9d18d | ||
![]() |
e9dc613548 | ||
![]() |
3af22f7e91 | ||
![]() |
2135cb8ece | ||
![]() |
376aad5d26 | ||
![]() |
27ba45aa75 | ||
![]() |
de17007788 | ||
![]() |
e86a7906fe | ||
![]() |
4a8e6abc06 | ||
![]() |
2eb7fed9f4 | ||
![]() |
ff8d5ca351 | ||
![]() |
07c4ad05f4 | ||
![]() |
548c2e5500 | ||
![]() |
ec2c6eff6f | ||
![]() |
15fe82c62f | ||
![]() |
cb5ecf4a8a | ||
![]() |
68a7e790d8 | ||
![]() |
24a4935e85 | ||
![]() |
9a660827a6 | ||
![]() |
a44ff73dd3 | ||
![]() |
acd87b2250 | ||
![]() |
1853ba1d60 | ||
![]() |
85cbb3b842 | ||
![]() |
968c44572a | ||
![]() |
3201b843a8 | ||
![]() |
db5b727300 | ||
![]() |
10b3a16f79 | ||
![]() |
a97c998be5 | ||
![]() |
b22d900e27 | ||
![]() |
181e38733c | ||
![]() |
4fe44611d3 | ||
![]() |
593916d6aa | ||
![]() |
38805a0e1f | ||
![]() |
0f756503ff | ||
![]() |
f6faeb034e | ||
![]() |
2efb5b71c3 | ||
![]() |
22c1f86ca1 | ||
![]() |
ce4af37fd8 | ||
![]() |
f446cbf9e5 | ||
![]() |
7581fb381f | ||
![]() |
bedf19993d | ||
![]() |
e558087bcf | ||
![]() |
698daf46c7 | ||
![]() |
0410f1e1be | ||
![]() |
7fbf7b189a | ||
![]() |
be208fe915 | ||
![]() |
1d3729aa24 | ||
![]() |
cbca9ffd95 | ||
![]() |
ed465d2b5b | ||
![]() |
d308da6ba1 | ||
![]() |
7c4dfdc1c2 | ||
![]() |
991ada31ba | ||
![]() |
43adc943b9 | ||
![]() |
4a20156f3d | ||
![]() |
eec6f41f6a | ||
![]() |
2cc918f79d | ||
![]() |
07b1ad121c | ||
![]() |
e419b03cab | ||
![]() |
018ddc75c6 | ||
![]() |
d085736bac | ||
![]() |
305549e7f2 | ||
![]() |
958c8b4f21 | ||
![]() |
373766691f | ||
![]() |
bac0275624 | ||
![]() |
0fc0f13064 | ||
![]() |
b75a8e6a2b | ||
![]() |
68ab73bdb5 | ||
![]() |
4c1fc4a11e | ||
![]() |
03f166f8a4 | ||
![]() |
d42daf9ce0 | ||
![]() |
d1862fba27 | ||
![]() |
f0db3d6b79 | ||
![]() |
21d37a1be0 | ||
![]() |
fe236ea929 | ||
![]() |
05b4b61c6e |
18
.github/CODEOWNERS
vendored
18
.github/CODEOWNERS
vendored
@@ -1,14 +1,14 @@
|
||||
# default onwer
|
||||
* anncwb@126.com vince292007@gmail.com
|
||||
* anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com
|
||||
|
||||
# vben core onwer
|
||||
/.github/ anncwb@126.com vince292007@gmail.com
|
||||
/.vscode/ anncwb@126.com vince292007@gmail.com
|
||||
/packages/ anncwb@126.com vince292007@gmail.com
|
||||
/packages/@core/ anncwb@126.com vince292007@gmail.com
|
||||
/internal/ anncwb@126.com vince292007@gmail.com
|
||||
/scripts/ anncwb@126.com vince292007@gmail.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 @vbenjs/team-v5
|
||||
docs/ anncwb@126.com vince292007@gmail.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
|
||||
|
17
.github/workflows/deploy.yml
vendored
17
.github/workflows/deploy.yml
vendored
@@ -153,3 +153,20 @@ jobs:
|
||||
username: ${{ secrets.WEB_NAIVE_FTP_ACCOUNT }}
|
||||
password: ${{ secrets.WEB_NAIVE_FTP_PASSWORD }}
|
||||
local-dir: ./apps/web-naive/dist/
|
||||
|
||||
rerun-on-failure:
|
||||
name: Rerun on failure
|
||||
needs:
|
||||
- deploy-playground-ftp
|
||||
- deploy-docs-ftp
|
||||
- deploy-antd-ftp
|
||||
- deploy-ele-ftp
|
||||
- deploy-naive-ftp
|
||||
if: failure() && fromJSON(github.run_attempt) < 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Retry ${{ fromJSON(github.run_attempt) }} of 10
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh workflow run rerun.yml -F run_id=${{ github.run_id }}
|
||||
|
19
.github/workflows/rerun.yml
vendored
Normal file
19
.github/workflows/rerun.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Rerun workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_id:
|
||||
description: The workflow id to relanch
|
||||
required: true
|
||||
jobs:
|
||||
rerun:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: rerun ${{ inputs.run_id }}
|
||||
env:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh run watch ${{ inputs.run_id }} > /dev/null 2>&1
|
||||
gh run rerun ${{ inputs.run_id }} --failed
|
@@ -1,4 +1,10 @@
|
||||
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',
|
||||
@@ -7,14 +13,8 @@ export default {
|
||||
'prettier --cache --ignore-unknown --write',
|
||||
'stylelint --fix --allow-empty-input',
|
||||
],
|
||||
'*.md': ['prettier --cache --ignore-unknown --write'],
|
||||
'*.vue': [
|
||||
'prettier --write',
|
||||
'eslint --cache --fix',
|
||||
'stylelint --fix --allow-empty-input',
|
||||
],
|
||||
'package.json': ['prettier --cache --write'],
|
||||
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': [
|
||||
'prettier --cache --write--parser json',
|
||||
],
|
||||
'package.json': ['prettier --cache --write'],
|
||||
};
|
||||
|
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -9,7 +9,7 @@
|
||||
"url": "http://localhost:5555",
|
||||
"env": { "NODE_ENV": "development" },
|
||||
"sourceMaps": true,
|
||||
"webRoot": "${workspaceFolder}"
|
||||
"webRoot": "${workspaceFolder}/playground"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
@@ -18,7 +18,7 @@
|
||||
"url": "http://localhost:5666",
|
||||
"env": { "NODE_ENV": "development" },
|
||||
"sourceMaps": true,
|
||||
"webRoot": "${workspaceFolder}"
|
||||
"webRoot": "${workspaceFolder}/apps/web-antd"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
@@ -27,7 +27,7 @@
|
||||
"url": "http://localhost:5777",
|
||||
"env": { "NODE_ENV": "development" },
|
||||
"sourceMaps": true,
|
||||
"webRoot": "${workspaceFolder}"
|
||||
"webRoot": "${workspaceFolder}/apps/web-ele"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
@@ -36,7 +36,7 @@
|
||||
"url": "http://localhost:5888",
|
||||
"env": { "NODE_ENV": "development" },
|
||||
"sourceMaps": true,
|
||||
"webRoot": "${workspaceFolder}"
|
||||
"webRoot": "${workspaceFolder}/apps/web-naive"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
13
.vscode/settings.json
vendored
13
.vscode/settings.json
vendored
@@ -223,5 +223,16 @@
|
||||
"commentTranslate.multiLineMerge": true,
|
||||
"vue.server.hybridMode": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"oxc.enable": false
|
||||
"oxc.enable": false,
|
||||
"cSpell.words": [
|
||||
"archiver",
|
||||
"axios",
|
||||
"dotenv",
|
||||
"isequal",
|
||||
"jspm",
|
||||
"napi",
|
||||
"nolebase",
|
||||
"rollup",
|
||||
"vitest"
|
||||
]
|
||||
}
|
||||
|
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 = [];
|
||||
@@ -43,6 +43,31 @@ export default eventHandler(async (event) => {
|
||||
|
||||
await sleep(600);
|
||||
|
||||
const { page, pageSize } = getQuery(event);
|
||||
return usePageResponseSuccess(page as string, pageSize as string, mockData);
|
||||
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
|
||||
const listData = structuredClone(mockData);
|
||||
if (sortBy && Reflect.has(listData[0], sortBy as string)) {
|
||||
listData.sort((a, b) => {
|
||||
if (sortOrder === 'asc') {
|
||||
if (sortBy === 'price') {
|
||||
return (
|
||||
Number.parseFloat(a[sortBy as string]) -
|
||||
Number.parseFloat(b[sortBy as string])
|
||||
);
|
||||
} else {
|
||||
return a[sortBy as string] > b[sortBy as string] ? 1 : -1;
|
||||
}
|
||||
} else {
|
||||
if (sortBy === 'price') {
|
||||
return (
|
||||
Number.parseFloat(b[sortBy as string]) -
|
||||
Number.parseFloat(a[sortBy as string])
|
||||
);
|
||||
} else {
|
||||
return a[sortBy as string] < b[sortBy as string] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return usePageResponseSuccess(page as string, pageSize as string, listData);
|
||||
});
|
||||
|
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,
|
||||
});
|
||||
|
@@ -4,6 +4,7 @@ export interface UserInfo {
|
||||
realName: string;
|
||||
roles: string[];
|
||||
username: string;
|
||||
homePath?: string;
|
||||
}
|
||||
|
||||
export const MOCK_USERS: UserInfo[] = [
|
||||
@@ -20,6 +21,7 @@ export const MOCK_USERS: UserInfo[] = [
|
||||
realName: 'Admin',
|
||||
roles: ['admin'],
|
||||
username: 'admin',
|
||||
homePath: '/workspace',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -27,6 +29,7 @@ export const MOCK_USERS: UserInfo[] = [
|
||||
realName: 'Jack',
|
||||
roles: ['user'],
|
||||
username: 'jack',
|
||||
homePath: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -50,13 +53,12 @@ export const MOCK_CODES = [
|
||||
|
||||
const dashboardMenus = [
|
||||
{
|
||||
component: 'BasicLayout',
|
||||
meta: {
|
||||
order: -1,
|
||||
title: 'page.dashboard.title',
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/',
|
||||
path: '/dashboard',
|
||||
redirect: '/analytics',
|
||||
children: [
|
||||
{
|
||||
@@ -113,7 +115,6 @@ const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
|
||||
|
||||
return [
|
||||
{
|
||||
component: 'BasicLayout',
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
@@ -184,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=Vben Admin Antd
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=vben-web-antd
|
||||
|
||||
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
|
||||
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.5",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -3,52 +3,109 @@
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
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';
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { notification } from 'ant-design-vue';
|
||||
|
||||
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({
|
||||
inheritAttrs: false,
|
||||
name: component.name,
|
||||
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,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'AutoComplete'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
@@ -79,20 +136,20 @@ async function initComponentAdapter() {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiSelect,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
modelField: 'value',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
modelPropName: 'value',
|
||||
}),
|
||||
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
}),
|
||||
AutoComplete,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
@@ -102,7 +159,11 @@ async function initComponentAdapter() {
|
||||
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
IconPicker,
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'addonAfter',
|
||||
inputComponent: Input,
|
||||
modelValueProp: 'value',
|
||||
}),
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
|
@@ -1,12 +1,13 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
import type { RequestClientOptions } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
authenticateResponseInterceptor,
|
||||
defaultResponseInterceptor,
|
||||
errorMessageResponseInterceptor,
|
||||
RequestClient,
|
||||
} from '@vben/request';
|
||||
@@ -20,8 +21,9 @@ import { refreshTokenApi } from './core';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
function createRequestClient(baseURL: string) {
|
||||
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
const client = new RequestClient({
|
||||
...options,
|
||||
baseURL,
|
||||
});
|
||||
|
||||
@@ -69,19 +71,14 @@ function createRequestClient(baseURL: string) {
|
||||
},
|
||||
});
|
||||
|
||||
// response数据解构
|
||||
client.addResponseInterceptor<HttpResponse>({
|
||||
fulfilled: (response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
const { code, data } = responseData;
|
||||
if (status >= 200 && status < 400 && code === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
throw Object.assign({}, response, { response });
|
||||
},
|
||||
});
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
codeField: 'code',
|
||||
dataField: 'data',
|
||||
successCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
// token过期的处理
|
||||
client.addResponseInterceptor(
|
||||
@@ -109,6 +106,8 @@ function createRequestClient(baseURL: string) {
|
||||
return client;
|
||||
}
|
||||
|
||||
export const requestClient = createRequestClient(apiURL);
|
||||
export const requestClient = createRequestClient(apiURL, {
|
||||
responseReturn: 'data',
|
||||
});
|
||||
|
||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { createApp, watchEffect } from 'vue';
|
||||
|
||||
import { registerAccessDirective } from '@vben/access';
|
||||
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
@@ -18,8 +19,23 @@ async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
// fullscreenButton: false,
|
||||
// });
|
||||
// // 设置抽屉的默认配置
|
||||
// setDefaultDrawerProps({
|
||||
// zIndex: 1020,
|
||||
// });
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册v-loading指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
|
||||
spinning: 'spinning',
|
||||
});
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
|
||||
@@ -29,9 +45,17 @@ 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) {
|
||||
|
@@ -110,7 +110,7 @@ watch(
|
||||
async (enable) => {
|
||||
if (enable) {
|
||||
await updateWatermark({
|
||||
content: `${userStore.userInfo?.username}`,
|
||||
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||
});
|
||||
} else {
|
||||
destroyWatermark();
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
import type { Locale } from 'ant-design-vue/es/locale';
|
||||
|
||||
import type { App } from 'vue';
|
||||
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {
|
||||
|
@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
return decodeURIComponent(
|
||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
||||
(to.query?.redirect as string) ||
|
||||
userStore.userInfo?.homePath ||
|
||||
DEFAULT_HOME_PATH,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
|
||||
return {
|
||||
path: LOGIN_PATH,
|
||||
// 如不需要,直接删除 query
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
query:
|
||||
to.fullPath === DEFAULT_HOME_PATH
|
||||
? {}
|
||||
: { redirect: encodeURIComponent(to.fullPath) },
|
||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||
replace: true,
|
||||
};
|
||||
@@ -102,7 +107,10 @@ function setupAccessGuard(router: Router) {
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
|
||||
const redirectPath = (from.query.redirect ??
|
||||
(to.path === DEFAULT_HOME_PATH
|
||||
? userInfo.homePath || DEFAULT_HOME_PATH
|
||||
: to.fullPath)) as string;
|
||||
|
||||
return {
|
||||
...router.resolve(decodeURIComponent(redirectPath)),
|
||||
|
@@ -2,10 +2,10 @@ import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
||||
|
||||
import { AuthPageLayout } 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'),
|
||||
@@ -21,13 +21,21 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||
|
||||
/** 基本路由,这些路由是必须存在的 */
|
||||
const coreRoutes: RouteRecordRaw[] = [
|
||||
/**
|
||||
* 根路由
|
||||
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
|
||||
* 此路由必须存在,且不应修改
|
||||
*/
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
title: 'Root',
|
||||
},
|
||||
name: 'Root',
|
||||
path: '/',
|
||||
redirect: DEFAULT_HOME_PATH,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
component: AuthPageLayout,
|
||||
@@ -42,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'Login',
|
||||
path: 'login',
|
||||
component: Login,
|
||||
component: () => import('#/views/_core/authentication/login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.login'),
|
||||
},
|
||||
|
@@ -1,18 +1,16 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: -1,
|
||||
title: $t('page.dashboard.title'),
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/',
|
||||
path: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
name: 'Analytics',
|
||||
|
@@ -1,11 +1,9 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
|
@@ -8,30 +8,20 @@ import {
|
||||
VBEN_NAIVE_PREVIEW_URL,
|
||||
} from '@vben/constants';
|
||||
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: VBEN_LOGO_URL,
|
||||
order: 9999,
|
||||
order: 9998,
|
||||
title: $t('demos.vben.title'),
|
||||
},
|
||||
name: 'VbenProject',
|
||||
path: '/vben-admin',
|
||||
children: [
|
||||
{
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('demos.vben.about'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenDocument',
|
||||
path: '/vben-admin/document',
|
||||
@@ -76,6 +66,16 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('demos.vben.about'),
|
||||
order: 9999,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
@@ -10,6 +10,7 @@ import { $t } from '@vben/locales';
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
@@ -30,6 +31,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
@@ -41,7 +43,9 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -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[] = [
|
||||
{
|
||||
|
@@ -3,3 +3,6 @@ VITE_APP_TITLE=Vben Admin Ele
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=vben-web-ele
|
||||
|
||||
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
|
||||
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-ele",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.5",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -3,46 +3,166 @@
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElCheckboxGroup,
|
||||
ElDatePicker,
|
||||
ElDivider,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElNotification,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
ElSelectV2,
|
||||
ElSpace,
|
||||
ElSwitch,
|
||||
ElTimePicker,
|
||||
ElTreeSelect,
|
||||
ElUpload,
|
||||
} from 'element-plus';
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { ElNotification } from 'element-plus';
|
||||
|
||||
const ElButton = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/button/index'),
|
||||
import('element-plus/es/components/button/style/css'),
|
||||
]).then(([res]) => res.ElButton),
|
||||
);
|
||||
const ElCheckbox = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/checkbox/index'),
|
||||
import('element-plus/es/components/checkbox/style/css'),
|
||||
]).then(([res]) => res.ElCheckbox),
|
||||
);
|
||||
const ElCheckboxButton = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/checkbox/index'),
|
||||
import('element-plus/es/components/checkbox-button/style/css'),
|
||||
]).then(([res]) => res.ElCheckboxButton),
|
||||
);
|
||||
const ElCheckboxGroup = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/checkbox/index'),
|
||||
import('element-plus/es/components/checkbox-group/style/css'),
|
||||
]).then(([res]) => res.ElCheckboxGroup),
|
||||
);
|
||||
const ElDatePicker = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/date-picker/index'),
|
||||
import('element-plus/es/components/date-picker/style/css'),
|
||||
]).then(([res]) => res.ElDatePicker),
|
||||
);
|
||||
const ElDivider = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/divider/index'),
|
||||
import('element-plus/es/components/divider/style/css'),
|
||||
]).then(([res]) => res.ElDivider),
|
||||
);
|
||||
const ElInput = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/input/index'),
|
||||
import('element-plus/es/components/input/style/css'),
|
||||
]).then(([res]) => res.ElInput),
|
||||
);
|
||||
const ElInputNumber = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/input-number/index'),
|
||||
import('element-plus/es/components/input-number/style/css'),
|
||||
]).then(([res]) => res.ElInputNumber),
|
||||
);
|
||||
const ElRadio = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/radio/index'),
|
||||
import('element-plus/es/components/radio/style/css'),
|
||||
]).then(([res]) => res.ElRadio),
|
||||
);
|
||||
const ElRadioButton = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/radio/index'),
|
||||
import('element-plus/es/components/radio-button/style/css'),
|
||||
]).then(([res]) => res.ElRadioButton),
|
||||
);
|
||||
const ElRadioGroup = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/radio/index'),
|
||||
import('element-plus/es/components/radio-group/style/css'),
|
||||
]).then(([res]) => res.ElRadioGroup),
|
||||
);
|
||||
const ElSelectV2 = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/select-v2/index'),
|
||||
import('element-plus/es/components/select-v2/style/css'),
|
||||
]).then(([res]) => res.ElSelectV2),
|
||||
);
|
||||
const ElSpace = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/space/index'),
|
||||
import('element-plus/es/components/space/style/css'),
|
||||
]).then(([res]) => res.ElSpace),
|
||||
);
|
||||
const ElSwitch = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/switch/index'),
|
||||
import('element-plus/es/components/switch/style/css'),
|
||||
]).then(([res]) => res.ElSwitch),
|
||||
);
|
||||
const ElTimePicker = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/time-picker/index'),
|
||||
import('element-plus/es/components/time-picker/style/css'),
|
||||
]).then(([res]) => res.ElTimePicker),
|
||||
);
|
||||
const ElTreeSelect = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/tree-select/index'),
|
||||
import('element-plus/es/components/tree-select/style/css'),
|
||||
]).then(([res]) => res.ElTreeSelect),
|
||||
);
|
||||
const ElUpload = defineAsyncComponent(() =>
|
||||
Promise.all([
|
||||
import('element-plus/es/components/upload/index'),
|
||||
import('element-plus/es/components/upload/style/css'),
|
||||
]).then(([res]) => res.ElUpload),
|
||||
);
|
||||
|
||||
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({
|
||||
inheritAttrs: false,
|
||||
name: component.name,
|
||||
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,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
@@ -64,21 +184,39 @@ async function initComponentAdapter() {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: ElSelectV2,
|
||||
loadingSlot: 'loading',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
}),
|
||||
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: ElTreeSelect,
|
||||
props: { label: 'label', children: 'children' },
|
||||
nodeKey: 'value',
|
||||
loadingSlot: 'loading',
|
||||
optionsPropName: 'data',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
}),
|
||||
Checkbox: ElCheckbox,
|
||||
CheckboxGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options, isButton } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () =>
|
||||
options.map((option) =>
|
||||
h(isButton ? ElCheckboxButton : ElCheckbox, option),
|
||||
);
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ApiSelect,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
component: ElSelectV2,
|
||||
loadingSlot: 'loading',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
},
|
||||
slots,
|
||||
ElCheckboxGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ ...slots, default: defaultSlot },
|
||||
);
|
||||
},
|
||||
Checkbox: ElCheckbox,
|
||||
CheckboxGroup: ElCheckboxGroup,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
|
||||
@@ -88,15 +226,79 @@ async function initComponentAdapter() {
|
||||
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Divider: ElDivider,
|
||||
IconPicker,
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'append',
|
||||
modelValueProp: 'model-value',
|
||||
inputComponent: ElInput,
|
||||
}),
|
||||
Input: withDefaultPlaceholder(ElInput, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
|
||||
RadioGroup: ElRadioGroup,
|
||||
Select: withDefaultPlaceholder(ElSelect, 'select'),
|
||||
RadioGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () =>
|
||||
options.map((option) =>
|
||||
h(attrs.isButton ? ElRadioButton : ElRadio, option),
|
||||
);
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElRadioGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ ...slots, default: defaultSlot },
|
||||
);
|
||||
},
|
||||
Select: (props, { attrs, slots }) => {
|
||||
return h(ElSelectV2, { ...props, attrs }, slots);
|
||||
},
|
||||
Space: ElSpace,
|
||||
Switch: ElSwitch,
|
||||
TimePicker: ElTimePicker,
|
||||
DatePicker: ElDatePicker,
|
||||
TimePicker: (props, { attrs, slots }) => {
|
||||
const { name, id, isRange } = props;
|
||||
const extraProps: Recordable<any> = {};
|
||||
if (isRange) {
|
||||
if (name && !Array.isArray(name)) {
|
||||
extraProps.name = [name, `${name}_end`];
|
||||
}
|
||||
if (id && !Array.isArray(id)) {
|
||||
extraProps.id = [id, `${id}_end`];
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElTimePicker,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
...extraProps,
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
DatePicker: (props, { attrs, slots }) => {
|
||||
const { name, id, type } = props;
|
||||
const extraProps: Recordable<any> = {};
|
||||
if (type && type.includes('range')) {
|
||||
if (name && !Array.isArray(name)) {
|
||||
extraProps.name = [name, `${name}_end`];
|
||||
}
|
||||
if (id && !Array.isArray(id)) {
|
||||
extraProps.id = [id, `${id}_end`];
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ElDatePicker,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
...extraProps,
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
|
||||
Upload: ElUpload,
|
||||
};
|
||||
|
@@ -12,6 +12,7 @@ setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
modelPropNameMap: {
|
||||
Upload: 'fileList',
|
||||
CheckboxGroup: 'model-value',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
|
@@ -1,12 +1,13 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
import type { RequestClientOptions } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
authenticateResponseInterceptor,
|
||||
defaultResponseInterceptor,
|
||||
errorMessageResponseInterceptor,
|
||||
RequestClient,
|
||||
} from '@vben/request';
|
||||
@@ -20,8 +21,9 @@ import { refreshTokenApi } from './core';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
function createRequestClient(baseURL: string) {
|
||||
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
const client = new RequestClient({
|
||||
...options,
|
||||
baseURL,
|
||||
});
|
||||
|
||||
@@ -69,18 +71,14 @@ function createRequestClient(baseURL: string) {
|
||||
},
|
||||
});
|
||||
|
||||
// response数据解构
|
||||
client.addResponseInterceptor<HttpResponse>({
|
||||
fulfilled: (response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
const { code, data } = responseData;
|
||||
if (status >= 200 && status < 400 && code === 0) {
|
||||
return data;
|
||||
}
|
||||
throw Object.assign({}, response, { response });
|
||||
},
|
||||
});
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
codeField: 'code',
|
||||
dataField: 'data',
|
||||
successCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
// token过期的处理
|
||||
client.addResponseInterceptor(
|
||||
@@ -108,6 +106,8 @@ function createRequestClient(baseURL: string) {
|
||||
return client;
|
||||
}
|
||||
|
||||
export const requestClient = createRequestClient(apiURL);
|
||||
export const requestClient = createRequestClient(apiURL, {
|
||||
responseReturn: 'data',
|
||||
});
|
||||
|
||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { createApp, watchEffect } from 'vue';
|
||||
|
||||
import { registerAccessDirective } from '@vben/access';
|
||||
import { registerLoadingDirective } from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
@@ -18,11 +19,25 @@ import { router } from './router';
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
// fullscreenButton: false,
|
||||
// });
|
||||
// // 设置抽屉的默认配置
|
||||
// setDefaultDrawerProps({
|
||||
// zIndex: 2000,
|
||||
// });
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册Element Plus提供的v-loading指令
|
||||
app.directive('loading', ElLoading.directive);
|
||||
|
||||
// 注册Vben提供的v-loading和v-spinning指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: false, // Vben提供的v-loading指令和Element Plus提供的v-loading指令二选一即可,此处false表示不注册Vben提供的v-loading指令
|
||||
spinning: 'spinning',
|
||||
});
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
|
||||
@@ -32,9 +47,17 @@ 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) {
|
||||
|
@@ -110,7 +110,7 @@ watch(
|
||||
async (enable) => {
|
||||
if (enable) {
|
||||
await updateWatermark({
|
||||
content: `${userStore.userInfo?.username}`,
|
||||
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||
});
|
||||
} else {
|
||||
destroyWatermark();
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
import type { Language } from 'element-plus/es/locale';
|
||||
|
||||
import type { App } from 'vue';
|
||||
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"title": "Demos",
|
||||
"elementPlus": "Element Plus",
|
||||
"form": "Form",
|
||||
"vben": {
|
||||
"title": "Project",
|
||||
"about": "About",
|
||||
|
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"title": "演示",
|
||||
"elementPlus": "Element Plus",
|
||||
"form": "表单演示",
|
||||
"vben": {
|
||||
"title": "项目",
|
||||
"about": "关于",
|
||||
|
@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
return decodeURIComponent(
|
||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
||||
(to.query?.redirect as string) ||
|
||||
userStore.userInfo?.homePath ||
|
||||
DEFAULT_HOME_PATH,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
|
||||
return {
|
||||
path: LOGIN_PATH,
|
||||
// 如不需要,直接删除 query
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
query:
|
||||
to.fullPath === DEFAULT_HOME_PATH
|
||||
? {}
|
||||
: { redirect: encodeURIComponent(to.fullPath) },
|
||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||
replace: true,
|
||||
};
|
||||
@@ -102,7 +107,10 @@ function setupAccessGuard(router: Router) {
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
|
||||
const redirectPath = (from.query.redirect ??
|
||||
(to.path === DEFAULT_HOME_PATH
|
||||
? userInfo.homePath || DEFAULT_HOME_PATH
|
||||
: to.fullPath)) as string;
|
||||
|
||||
return {
|
||||
...router.resolve(decodeURIComponent(redirectPath)),
|
||||
|
@@ -2,10 +2,10 @@ import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
||||
|
||||
import { AuthPageLayout } 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'),
|
||||
@@ -21,13 +21,21 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||
|
||||
/** 基本路由,这些路由是必须存在的 */
|
||||
const coreRoutes: RouteRecordRaw[] = [
|
||||
/**
|
||||
* 根路由
|
||||
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
|
||||
* 此路由必须存在,且不应修改
|
||||
*/
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
title: 'Root',
|
||||
},
|
||||
name: 'Root',
|
||||
path: '/',
|
||||
redirect: DEFAULT_HOME_PATH,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
component: AuthPageLayout,
|
||||
@@ -42,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'Login',
|
||||
path: 'login',
|
||||
component: Login,
|
||||
component: () => import('#/views/_core/authentication/login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.login'),
|
||||
},
|
||||
|
@@ -1,18 +1,16 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: -1,
|
||||
title: $t('page.dashboard.title'),
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/',
|
||||
path: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
name: 'Analytics',
|
||||
|
@@ -1,11 +1,9 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
@@ -23,6 +21,14 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/demos/element',
|
||||
component: () => import('#/views/demos/element/index.vue'),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: $t('demos.form'),
|
||||
},
|
||||
name: 'BasicForm',
|
||||
path: '/demos/form',
|
||||
component: () => import('#/views/demos/form/basic.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@@ -9,30 +9,20 @@ import {
|
||||
} from '@vben/constants';
|
||||
import { SvgAntdvLogoIcon } from '@vben/icons';
|
||||
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: VBEN_LOGO_URL,
|
||||
order: 9999,
|
||||
order: 9998,
|
||||
title: $t('demos.vben.title'),
|
||||
},
|
||||
name: 'VbenProject',
|
||||
path: '/vben-admin',
|
||||
children: [
|
||||
{
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('demos.vben.about'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenDocument',
|
||||
path: '/vben-admin/document',
|
||||
@@ -77,6 +67,16 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('demos.vben.about'),
|
||||
order: 9999,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
@@ -10,6 +10,7 @@ import { $t } from '@vben/locales';
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
@@ -30,6 +31,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
@@ -41,7 +43,9 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -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[] = [
|
||||
{
|
||||
|
191
apps/web-ele/src/views/demos/form/basic.vue
Normal file
191
apps/web-ele/src/views/demos/form/basic.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElCard, ElCheckbox, ElMessage } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getAllMenusApi } from '#/api';
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
// wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
handleSubmit: (values) => {
|
||||
ElMessage.success(`表单数据:${JSON.stringify(values)}`);
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'IconPicker',
|
||||
fieldName: 'icon',
|
||||
label: 'IconPicker',
|
||||
},
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'ApiSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { name: string; path: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.path,
|
||||
}));
|
||||
},
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'api',
|
||||
// 界面显示的label
|
||||
label: 'ApiSelect',
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
childrenField: 'children',
|
||||
// 菜单接口转options格式
|
||||
labelField: 'name',
|
||||
valueField: 'path',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'apiTree',
|
||||
// 界面显示的label
|
||||
label: 'ApiTreeSelect',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'string',
|
||||
label: 'String',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'number',
|
||||
label: 'Number',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radio',
|
||||
label: 'Radio',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: 'A' },
|
||||
{ value: 'B', label: 'B' },
|
||||
{ value: 'C', label: 'C' },
|
||||
{ value: 'D', label: 'D' },
|
||||
{ value: 'E', label: 'E' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radioButton',
|
||||
label: 'RadioButton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: ['A', 'B', 'C', 'D', 'E', 'F'].map((v) => ({
|
||||
value: v,
|
||||
label: `选项${v}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox',
|
||||
label: 'Checkbox',
|
||||
componentProps: {
|
||||
options: ['A', 'B', 'C'].map((v) => ({ value: v, label: `选项${v}` })),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox1',
|
||||
label: 'Checkbox1',
|
||||
renderComponentContent: () => {
|
||||
return {
|
||||
default: () => {
|
||||
return ['A', 'B', 'C', 'D'].map((v) =>
|
||||
h(ElCheckbox, { label: v, value: v }),
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbotton',
|
||||
label: 'CheckBotton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'date',
|
||||
label: 'Date',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'select',
|
||||
label: 'Select',
|
||||
componentProps: {
|
||||
filterable: true,
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer();
|
||||
function setFormValues() {
|
||||
formApi.setValues({
|
||||
string: 'string',
|
||||
number: 123,
|
||||
radio: 'B',
|
||||
radioButton: 'C',
|
||||
checkbox: ['A', 'C'],
|
||||
checkbotton: ['B', 'C'],
|
||||
checkbox1: ['A', 'B'],
|
||||
date: new Date(),
|
||||
select: 'B',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
description="我们重新包装了CheckboxGroup、RadioGroup、Select,可以通过options属性传入选项属性数组以自动生成选项"
|
||||
title="表单演示"
|
||||
>
|
||||
<Drawer class="w-[600px]" title="基础表单示例">
|
||||
<Form />
|
||||
</Drawer>
|
||||
<ElCard>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<span class="flex-auto">基础表单演示</span>
|
||||
<ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<ElButton type="primary" @click="drawerApi.open"> 打开抽屉 </ElButton>
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
@@ -3,3 +3,6 @@ VITE_APP_TITLE=Vben Admin Naive
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=vben-web-naive
|
||||
|
||||
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
|
||||
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-naive",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.5",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -3,46 +3,112 @@
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { ApiSelect, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import {
|
||||
NButton,
|
||||
NCheckbox,
|
||||
NCheckboxGroup,
|
||||
NDatePicker,
|
||||
NDivider,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NRadioGroup,
|
||||
NSelect,
|
||||
NSpace,
|
||||
NSwitch,
|
||||
NTimePicker,
|
||||
NTreeSelect,
|
||||
NUpload,
|
||||
} from 'naive-ui';
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
h,
|
||||
ref,
|
||||
} from 'vue';
|
||||
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
|
||||
const NButton = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/button').then((res) => res.NButton),
|
||||
);
|
||||
const NCheckbox = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/checkbox').then((res) => res.NCheckbox),
|
||||
);
|
||||
const NCheckboxGroup = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/checkbox').then((res) => res.NCheckboxGroup),
|
||||
);
|
||||
const NDatePicker = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/date-picker').then((res) => res.NDatePicker),
|
||||
);
|
||||
const NDivider = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/divider').then((res) => res.NDivider),
|
||||
);
|
||||
const NInput = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/input').then((res) => res.NInput),
|
||||
);
|
||||
const NInputNumber = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/input-number').then((res) => res.NInputNumber),
|
||||
);
|
||||
const NRadio = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/radio').then((res) => res.NRadio),
|
||||
);
|
||||
const NRadioButton = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/radio').then((res) => res.NRadioButton),
|
||||
);
|
||||
const NRadioGroup = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/radio').then((res) => res.NRadioGroup),
|
||||
);
|
||||
const NSelect = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/select').then((res) => res.NSelect),
|
||||
);
|
||||
const NSpace = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/space').then((res) => res.NSpace),
|
||||
);
|
||||
const NSwitch = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/switch').then((res) => res.NSwitch),
|
||||
);
|
||||
const NTimePicker = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/time-picker').then((res) => res.NTimePicker),
|
||||
);
|
||||
const NTreeSelect = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/tree-select').then((res) => res.NTreeSelect),
|
||||
);
|
||||
const NUpload = defineAsyncComponent(() =>
|
||||
import('naive-ui/es/upload').then((res) => res.NUpload),
|
||||
);
|
||||
|
||||
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({
|
||||
inheritAttrs: false,
|
||||
name: component.name,
|
||||
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,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
@@ -65,20 +131,36 @@ async function initComponentAdapter() {
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: NSelect,
|
||||
modelPropName: 'value',
|
||||
}),
|
||||
ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', {
|
||||
component: NTreeSelect,
|
||||
nodeKey: 'value',
|
||||
loadingSlot: 'arrow',
|
||||
keyField: 'value',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'options',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
}),
|
||||
Checkbox: NCheckbox,
|
||||
CheckboxGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () => options.map((option) => h(NCheckbox, option));
|
||||
}
|
||||
}
|
||||
return h(
|
||||
ApiSelect,
|
||||
{
|
||||
...props,
|
||||
...attrs,
|
||||
component: NSelect,
|
||||
modelField: 'value',
|
||||
},
|
||||
slots,
|
||||
NCheckboxGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ default: defaultSlot },
|
||||
);
|
||||
},
|
||||
Checkbox: NCheckbox,
|
||||
CheckboxGroup: NCheckboxGroup,
|
||||
DatePicker: NDatePicker,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
@@ -89,10 +171,34 @@ async function initComponentAdapter() {
|
||||
return h(NButton, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Divider: NDivider,
|
||||
IconPicker,
|
||||
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
|
||||
iconSlot: 'suffix',
|
||||
inputComponent: NInput,
|
||||
}),
|
||||
Input: withDefaultPlaceholder(NInput, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(NInputNumber, 'input'),
|
||||
RadioGroup: NRadioGroup,
|
||||
RadioGroup: (props, { attrs, slots }) => {
|
||||
let defaultSlot;
|
||||
if (Reflect.has(slots, 'default')) {
|
||||
defaultSlot = slots.default;
|
||||
} else {
|
||||
const { options } = attrs;
|
||||
if (Array.isArray(options)) {
|
||||
defaultSlot = () =>
|
||||
options.map((option) =>
|
||||
h(attrs.isButton ? NRadioButton : NRadio, option),
|
||||
);
|
||||
}
|
||||
}
|
||||
const groupRender = h(
|
||||
NRadioGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ default: defaultSlot },
|
||||
);
|
||||
return attrs.isButton
|
||||
? h(NSpace, { vertical: true }, () => groupRender)
|
||||
: groupRender;
|
||||
},
|
||||
Select: withDefaultPlaceholder(NSelect, 'select'),
|
||||
Space: NSpace,
|
||||
Switch: NSwitch,
|
||||
|
@@ -10,8 +10,6 @@ import { $t } from '@vben/locales';
|
||||
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// naive-ui组件不接受onChang事件,所以需要禁用
|
||||
disabledOnChangeListener: true,
|
||||
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
||||
emptyStateValue: null,
|
||||
baseModelPropName: 'value',
|
||||
|
@@ -1,12 +1,13 @@
|
||||
/**
|
||||
* 该文件可自行根据业务逻辑进行调整
|
||||
*/
|
||||
import type { HttpResponse } from '@vben/request';
|
||||
import type { RequestClientOptions } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import {
|
||||
authenticateResponseInterceptor,
|
||||
defaultResponseInterceptor,
|
||||
errorMessageResponseInterceptor,
|
||||
RequestClient,
|
||||
} from '@vben/request';
|
||||
@@ -19,8 +20,9 @@ import { refreshTokenApi } from './core';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
function createRequestClient(baseURL: string) {
|
||||
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
const client = new RequestClient({
|
||||
...options,
|
||||
baseURL,
|
||||
});
|
||||
|
||||
@@ -68,18 +70,14 @@ function createRequestClient(baseURL: string) {
|
||||
},
|
||||
});
|
||||
|
||||
// response数据解构
|
||||
client.addResponseInterceptor<HttpResponse>({
|
||||
fulfilled: (response) => {
|
||||
const { data: responseData, status } = response;
|
||||
|
||||
const { code, data } = responseData;
|
||||
if (status >= 200 && status < 400 && code === 0) {
|
||||
return data;
|
||||
}
|
||||
throw Object.assign({}, response, { response });
|
||||
},
|
||||
});
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
codeField: 'code',
|
||||
dataField: 'data',
|
||||
successCode: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
// token过期的处理
|
||||
client.addResponseInterceptor(
|
||||
@@ -107,6 +105,8 @@ function createRequestClient(baseURL: string) {
|
||||
return client;
|
||||
}
|
||||
|
||||
export const requestClient = createRequestClient(apiURL);
|
||||
export const requestClient = createRequestClient(apiURL, {
|
||||
responseReturn: 'data',
|
||||
});
|
||||
|
||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import { createApp, watchEffect } from 'vue';
|
||||
|
||||
import { registerAccessDirective } from '@vben/access';
|
||||
import { registerLoadingDirective } from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
import '@vben/styles/naive';
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
|
||||
@@ -16,8 +18,24 @@ import { router } from './router';
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
initComponentAdapter();
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
// fullscreenButton: false,
|
||||
// });
|
||||
// // 设置抽屉的默认配置
|
||||
// setDefaultDrawerProps({
|
||||
// // zIndex: 2000,
|
||||
// });
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册v-loading指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
|
||||
spinning: 'spinning',
|
||||
});
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
|
||||
@@ -27,9 +45,17 @@ 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) {
|
||||
|
@@ -111,7 +111,7 @@ watch(
|
||||
async (enable) => {
|
||||
if (enable) {
|
||||
await updateWatermark({
|
||||
content: `${userStore.userInfo?.username}`,
|
||||
content: `${userStore.userInfo?.username} - ${userStore.userInfo?.realName}`,
|
||||
});
|
||||
} else {
|
||||
destroyWatermark();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
|
||||
import type { App } from 'vue';
|
||||
|
||||
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
|
||||
|
||||
import {
|
||||
$t,
|
||||
setupI18n as coreSetup,
|
||||
|
@@ -2,6 +2,7 @@
|
||||
"title": "Demos",
|
||||
"naive": "Naive UI",
|
||||
"table": "Table",
|
||||
"form": "Form",
|
||||
"vben": {
|
||||
"title": "Project",
|
||||
"about": "About",
|
||||
|
@@ -2,6 +2,7 @@
|
||||
"title": "演示",
|
||||
"naive": "Naive UI",
|
||||
"table": "Table",
|
||||
"form": "表单",
|
||||
"vben": {
|
||||
"title": "项目",
|
||||
"about": "关于",
|
||||
|
@@ -54,7 +54,9 @@ function setupAccessGuard(router: Router) {
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
return decodeURIComponent(
|
||||
(to.query?.redirect as string) || DEFAULT_HOME_PATH,
|
||||
(to.query?.redirect as string) ||
|
||||
userStore.userInfo?.homePath ||
|
||||
DEFAULT_HOME_PATH,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -72,7 +74,10 @@ function setupAccessGuard(router: Router) {
|
||||
return {
|
||||
path: LOGIN_PATH,
|
||||
// 如不需要,直接删除 query
|
||||
query: { redirect: encodeURIComponent(to.fullPath) },
|
||||
query:
|
||||
to.fullPath === DEFAULT_HOME_PATH
|
||||
? {}
|
||||
: { redirect: encodeURIComponent(to.fullPath) },
|
||||
// 携带当前跳转的页面,登录后重新跳转该页面
|
||||
replace: true,
|
||||
};
|
||||
@@ -101,7 +106,10 @@ function setupAccessGuard(router: Router) {
|
||||
accessStore.setAccessMenus(accessibleMenus);
|
||||
accessStore.setAccessRoutes(accessibleRoutes);
|
||||
accessStore.setIsAccessChecked(true);
|
||||
const redirectPath = (from.query.redirect ?? to.fullPath) as string;
|
||||
const redirectPath = (from.query.redirect ??
|
||||
(to.path === DEFAULT_HOME_PATH
|
||||
? userInfo.homePath || DEFAULT_HOME_PATH
|
||||
: to.fullPath)) as string;
|
||||
|
||||
return {
|
||||
...router.resolve(decodeURIComponent(redirectPath)),
|
||||
|
@@ -2,10 +2,10 @@ import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants';
|
||||
|
||||
import { AuthPageLayout } 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'),
|
||||
@@ -21,13 +21,21 @@ const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||
|
||||
/** 基本路由,这些路由是必须存在的 */
|
||||
const coreRoutes: RouteRecordRaw[] = [
|
||||
/**
|
||||
* 根路由
|
||||
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
|
||||
* 此路由必须存在,且不应修改
|
||||
*/
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
title: 'Root',
|
||||
},
|
||||
name: 'Root',
|
||||
path: '/',
|
||||
redirect: DEFAULT_HOME_PATH,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
component: AuthPageLayout,
|
||||
@@ -42,7 +50,7 @@ const coreRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'Login',
|
||||
path: 'login',
|
||||
component: Login,
|
||||
component: () => import('#/views/_core/authentication/login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.login'),
|
||||
},
|
||||
|
@@ -1,18 +1,16 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'lucide:layout-dashboard',
|
||||
order: -1,
|
||||
title: $t('page.dashboard.title'),
|
||||
},
|
||||
name: 'Dashboard',
|
||||
path: '/',
|
||||
path: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
name: 'Analytics',
|
||||
|
@@ -1,11 +1,9 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { BasicLayout } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
@@ -31,6 +29,14 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/demos/table',
|
||||
component: () => import('#/views/demos/table/index.vue'),
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
title: $t('demos.form'),
|
||||
},
|
||||
name: 'Form',
|
||||
path: '/demos/form',
|
||||
component: () => import('#/views/demos/form/basic.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@@ -9,30 +9,20 @@ import {
|
||||
} from '@vben/constants';
|
||||
import { SvgAntdvLogoIcon } from '@vben/icons';
|
||||
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { IFrameView } from '#/layouts';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
badgeType: 'dot',
|
||||
icon: VBEN_LOGO_URL,
|
||||
order: 9999,
|
||||
order: 9998,
|
||||
title: $t('demos.vben.title'),
|
||||
},
|
||||
name: 'VbenProject',
|
||||
path: '/vben-admin',
|
||||
children: [
|
||||
{
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('demos.vben.about'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'VbenDocument',
|
||||
path: '/vben-admin/document',
|
||||
@@ -77,6 +67,16 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'VbenAbout',
|
||||
path: '/vben-admin/about',
|
||||
component: () => import('#/views/_core/about/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:copyright',
|
||||
title: $t('demos.vben.about'),
|
||||
order: 9999,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
@@ -10,6 +10,7 @@ import { $t } from '@vben/locales';
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 6;
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
@@ -30,6 +31,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
@@ -41,7 +43,9 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().min(1, { message: $t('authentication.codeTip') }),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -1,11 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
EchartsUI,
|
||||
type EchartsUIType,
|
||||
useEcharts,
|
||||
} from '@vben/plugins/echarts';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
@@ -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[] = [
|
||||
{
|
||||
|
159
apps/web-naive/src/views/demos/form/basic.vue
Normal file
159
apps/web-naive/src/views/demos/form/basic.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { NButton, NCard, useMessage } from 'naive-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { getAllMenusApi } from '#/api';
|
||||
|
||||
const message = useMessage();
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
layout: 'horizontal',
|
||||
// 大屏一行显示3个,中屏一行显示2个,小屏一行显示1个
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
handleSubmit: (values) => {
|
||||
message.success(`表单数据:${JSON.stringify(values)}`);
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
// 组件需要在 #/adapter.ts内注册,并加上类型
|
||||
component: 'ApiSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口转options格式
|
||||
afterFetch: (data: { name: string; path: string }[]) => {
|
||||
return data.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.path,
|
||||
}));
|
||||
},
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'api',
|
||||
// 界面显示的label
|
||||
label: 'ApiSelect',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'ApiTreeSelect',
|
||||
// 对应组件的参数
|
||||
componentProps: {
|
||||
// 菜单接口
|
||||
api: getAllMenusApi,
|
||||
childrenField: 'children',
|
||||
// 菜单接口转options格式
|
||||
labelField: 'name',
|
||||
valueField: 'path',
|
||||
},
|
||||
// 字段名
|
||||
fieldName: 'apiTree',
|
||||
// 界面显示的label
|
||||
label: 'ApiTreeSelect',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'string',
|
||||
label: 'String',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'number',
|
||||
label: 'Number',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radio',
|
||||
label: 'Radio',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: 'A' },
|
||||
{ value: 'B', label: 'B' },
|
||||
{ value: 'C', label: 'C' },
|
||||
{ value: 'D', label: 'D' },
|
||||
{ value: 'E', label: 'E' },
|
||||
],
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
fieldName: 'radioButton',
|
||||
label: 'RadioButton',
|
||||
componentProps: {
|
||||
isButton: true,
|
||||
class: 'flex flex-wrap', // 如果选项过多,可以添加class来自动折叠
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
{ value: 'D', label: '选项D' },
|
||||
{ value: 'E', label: '选项E' },
|
||||
],
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox',
|
||||
label: 'Checkbox',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'date',
|
||||
label: 'Date',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'textArea',
|
||||
label: 'TextArea',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
});
|
||||
function setFormValues() {
|
||||
formApi.setValues({
|
||||
string: 'string',
|
||||
number: 123,
|
||||
radio: 'B',
|
||||
radioButton: 'C',
|
||||
checkbox: ['A', 'C'],
|
||||
date: Date.now(),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page
|
||||
description="表单适配器重新包装了CheckboxGroup和RadioGroup,可以通过options属性传递选项数据(选项数据将作为子组件的属性)"
|
||||
title="表单演示"
|
||||
>
|
||||
<NCard title="基础表单">
|
||||
<template #header-extra>
|
||||
<NButton type="primary" @click="setFormValues">设置表单值</NButton>
|
||||
</template>
|
||||
<Form />
|
||||
</NCard>
|
||||
</Page>
|
||||
</template>
|
@@ -1,7 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import type { NotificationType } from 'naive-ui';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { type NotificationType } from 'naive-ui';
|
||||
import { NButton, NCard, NSpace, useMessage, useNotification } from 'naive-ui';
|
||||
|
||||
const notification = useNotification();
|
||||
|
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { SetupContext } from 'vue';
|
||||
|
||||
import { computed, ref, useSlots } from 'vue';
|
||||
|
||||
import { VbenTooltip } from '@vben-core/shadcn-ui';
|
||||
@@ -25,7 +27,7 @@ const props = withDefaults(
|
||||
|
||||
const open = ref(false);
|
||||
|
||||
const slots = useSlots();
|
||||
const slots: SetupContext['slots'] = useSlots();
|
||||
|
||||
const tabs = computed(() => {
|
||||
return props.files.map((file) => {
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import { type DefaultTheme, defineConfig } from 'vitepress';
|
||||
import type { DefaultTheme } from 'vitepress';
|
||||
|
||||
import { defineConfig } from 'vitepress';
|
||||
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import { type DefaultTheme, defineConfig } from 'vitepress';
|
||||
import type { DefaultTheme } from 'vitepress';
|
||||
|
||||
import { defineConfig } from 'vitepress';
|
||||
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
@@ -162,6 +164,14 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||
collapsed: false,
|
||||
text: '通用组件',
|
||||
items: [
|
||||
{
|
||||
link: 'common-ui/vben-api-component',
|
||||
text: 'ApiComponent Api组件包装器',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-alert',
|
||||
text: 'Alert 轻量提示框',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-modal',
|
||||
text: 'Modal 模态框',
|
||||
@@ -182,6 +192,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||
link: 'common-ui/vben-count-to-animator',
|
||||
text: 'CountToAnimator 数字动画',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-ellipsis-text',
|
||||
text: 'EllipsisText 省略文本',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@@ -10,7 +10,6 @@ import {
|
||||
|
||||
// import { useAntdDesignTokens } from '@vben/hooks';
|
||||
// import { initPreferences } from '@vben/preferences';
|
||||
|
||||
import { ConfigProvider, theme } from 'ant-design-vue';
|
||||
import mediumZoom from 'medium-zoom';
|
||||
import { useRoute } from 'vitepress';
|
||||
|
@@ -104,6 +104,11 @@
|
||||
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||
|
||||
/**
|
||||
* modal zIndex
|
||||
*/
|
||||
--popup-z-index: 1000;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/docs",
|
||||
"version": "5.5.0",
|
||||
"version": "5.5.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vitepress build",
|
||||
|
@@ -3,9 +3,10 @@
|
||||
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
|
||||
*/
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { globalShareState } from '@vben/common-ui';
|
||||
|
@@ -14,8 +14,6 @@ initComponentAdapter();
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
baseModelPropName: 'value',
|
||||
// naive-ui组件不接受onChang事件,所以需要禁用
|
||||
disabledOnChangeListener: true,
|
||||
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
||||
emptyStateValue: null,
|
||||
modelPropNameMap: {
|
||||
|
@@ -3,7 +3,7 @@
|
||||
社区交流群主要是为了方便大家交流,提问,解答问题,分享经验等。偏自助方式,如果你有问题,可以通过以下方式加入社区交流群:
|
||||
|
||||
- [QQ频道](https://pd.qq.com/s/16p8lvvob):推荐!!!主要提供问题解答,分享经验等。
|
||||
- QQ群:[大群](https://qm.qq.com/q/MEmHoCLbG0),[1群](https://qm.qq.com/q/YacMHPYAMu)、[2群](https://qm.qq.com/q/ajVKZvFICk)、[3群](https://qm.qq.com/q/36zdwThP2E),[4群](https://qm.qq.com/q/sCzSlm3504),主要使用者的交流群。
|
||||
- QQ群:[大群](https://qm.qq.com/q/MEmHoCLbG0),[1群](https://qm.qq.com/q/YacMHPYAMu)、[2群](https://qm.qq.com/q/ajVKZvFICk)、[3群](https://qm.qq.com/q/36zdwThP2E),[4群](https://qm.qq.com/q/sCzSlm3504),[5群](https://qm.qq.com/q/ya9XrtbS6s),主要的使用者交流群。
|
||||
- [Discord](https://discord.com/invite/VU62jTecad): 主要提供问题解答,分享经验等。
|
||||
|
||||
::: tip
|
||||
|
166
docs/src/components/common-ui/vben-alert.md
Normal file
166
docs/src/components/common-ui/vben-alert.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben Alert 轻量提示框
|
||||
|
||||
框架提供的一些用于轻量提示的弹窗,仅使用js代码即可快速动态创建提示而不需要在template写任何代码。
|
||||
|
||||
::: info 应用场景
|
||||
|
||||
Alert提供的功能与Modal类似,但只适用于简单应用场景。例如临时性、动态地弹出模态确认框、输入框等。如果对弹窗有更复杂的需求,请使用VbenModal
|
||||
|
||||
:::
|
||||
|
||||
::: tip 注意
|
||||
|
||||
Alert提供的快捷方法alert、confirm、prompt动态创建的弹窗在已打开的情况下,不支持HMR(热更新),代码变更后需要关闭这些弹窗后重新打开。
|
||||
|
||||
:::
|
||||
|
||||
::: tip README
|
||||
|
||||
下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
|
||||
|
||||
:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
使用 `alert` 创建只有一个确认按钮的提示框。
|
||||
|
||||
<DemoPreview dir="demos/vben-alert/alert" />
|
||||
|
||||
使用 `confirm` 创建有确认和取消按钮的提示框。
|
||||
|
||||
<DemoPreview dir="demos/vben-alert/confirm" />
|
||||
|
||||
使用 `prompt` 创建有确认和取消按钮、接受用户输入的提示框。
|
||||
|
||||
<DemoPreview dir="demos/vben-alert/prompt" />
|
||||
|
||||
## useAlertContext
|
||||
|
||||
当弹窗的content、footer、icon使用自定义组件时,在这些组件中可以使用 `useAlertContext` 获取当前弹窗的上下文对象,用来主动控制弹窗。
|
||||
|
||||
::: tip 注意
|
||||
|
||||
`useAlertContext`只能用在setup或者函数式组件中。
|
||||
|
||||
:::
|
||||
|
||||
### Methods
|
||||
|
||||
| 方法 | 描述 | 类型 | 版本要求 |
|
||||
| --------- | ------------------ | -------- | -------- |
|
||||
| doConfirm | 调用弹窗的确认操作 | ()=>void | >5.5.4 |
|
||||
| doCancel | 调用弹窗的取消操作 | ()=>void | >5.5.4 |
|
||||
|
||||
## 类型说明
|
||||
|
||||
```ts
|
||||
/** 预置的图标类型 */
|
||||
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
|
||||
|
||||
export type BeforeCloseScope = {
|
||||
/** 是否为点击确认按钮触发的关闭 */
|
||||
isConfirm: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* alert 属性
|
||||
*/
|
||||
export type AlertProps = {
|
||||
/** 关闭前的回调,如果返回false,则终止关闭 */
|
||||
beforeClose?: (
|
||||
scope: BeforeCloseScope,
|
||||
) => boolean | Promise<boolean | undefined> | undefined;
|
||||
/** 边框 */
|
||||
bordered?: boolean;
|
||||
/** 按钮对齐方式 */
|
||||
buttonAlign?: 'center' | 'end' | 'start';
|
||||
/** 取消按钮的标题 */
|
||||
cancelText?: string;
|
||||
/** 是否居中显示 */
|
||||
centered?: boolean;
|
||||
/** 确认按钮的标题 */
|
||||
confirmText?: string;
|
||||
/** 弹窗容器的额外样式 */
|
||||
containerClass?: string;
|
||||
/** 弹窗提示内容 */
|
||||
content: Component | string;
|
||||
/** 弹窗内容的额外样式 */
|
||||
contentClass?: string;
|
||||
/** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
|
||||
contentMasking?: boolean;
|
||||
/** 弹窗底部内容(与按钮在同一个容器中) */
|
||||
footer?: Component | string;
|
||||
/** 弹窗的图标(在标题的前面) */
|
||||
icon?: Component | IconType;
|
||||
/**
|
||||
* 弹窗遮罩模糊效果
|
||||
*/
|
||||
overlayBlur?: number;
|
||||
/** 是否显示取消按钮 */
|
||||
showCancel?: boolean;
|
||||
/** 弹窗标题 */
|
||||
title?: string;
|
||||
};
|
||||
|
||||
/** prompt 属性 */
|
||||
export type PromptProps<T = any> = {
|
||||
/** 关闭前的回调,如果返回false,则终止关闭 */
|
||||
beforeClose?: (scope: {
|
||||
isConfirm: boolean;
|
||||
value: T | undefined;
|
||||
}) => boolean | Promise<boolean | undefined> | undefined;
|
||||
/** 用于接受用户输入的组件 */
|
||||
component?: Component;
|
||||
/** 输入组件的属性 */
|
||||
componentProps?: Recordable<any>;
|
||||
/** 输入组件的插槽 */
|
||||
componentSlots?: Recordable<Component>;
|
||||
/** 默认值 */
|
||||
defaultValue?: T;
|
||||
/** 输入组件的值属性名 */
|
||||
modelPropName?: string;
|
||||
} & Omit<AlertProps, 'beforeClose'>;
|
||||
|
||||
/**
|
||||
* 函数签名
|
||||
* alert和confirm的函数签名相同。
|
||||
* confirm默认会显示取消按钮,而alert默认只有一个按钮
|
||||
* */
|
||||
export function alert(options: AlertProps): Promise<void>;
|
||||
export function alert(
|
||||
message: string,
|
||||
options?: Partial<AlertProps>,
|
||||
): Promise<void>;
|
||||
export function alert(
|
||||
message: string,
|
||||
title?: string,
|
||||
options?: Partial<AlertProps>,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* 弹出输入框的函数签名。
|
||||
* beforeClose的参数会传入用户当前输入的值
|
||||
* component指定接受用户输入的组件,默认为Input
|
||||
* componentProps 为输入组件设置的属性数据
|
||||
* defaultValue 默认的值
|
||||
* modelPropName 输入组件的值属性名称。默认为modelValue
|
||||
*/
|
||||
export async function prompt<T = any>(
|
||||
options: Omit<AlertProps, 'beforeClose'> & {
|
||||
beforeClose?: (
|
||||
scope: BeforeCloseScope & {
|
||||
/** 输入组件的当前值 */
|
||||
value: T;
|
||||
},
|
||||
) => boolean | Promise<boolean | undefined> | undefined;
|
||||
component?: Component;
|
||||
componentProps?: Recordable<any>;
|
||||
defaultValue?: T;
|
||||
modelPropName?: string;
|
||||
},
|
||||
): Promise<T | undefined>;
|
||||
```
|
173
docs/src/components/common-ui/vben-api-component.md
Normal file
173
docs/src/components/common-ui/vben-api-component.md
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben ApiComponent Api组件包装器
|
||||
|
||||
框架提供的API“包装器”,它一般不独立使用,主要用于包装其它组件,为目标组件提供自动获取远程数据的能力,但仍然保持了目标组件的原始用法。
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
我们在各个应用的组件适配器中,使用ApiComponent包装了Select、TreeSelect组件,使得这些组件可以自动获取远程数据并生成选项。其它类似的组件(比如Cascader)如有需要也可以参考示例代码自行进行包装。
|
||||
|
||||
:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
通过 `component` 传入其它组件的定义,并配置相关的其它属性(主要是一些名称映射)。包装组件将通过`api`获取数据(`beforerFetch`、`afterFetch`将分别在`api`运行前、运行后被调用),使用`resultField`从中提取数组,使用`valueField`、`labelField`等来从数据中提取value和label(如果提供了`childrenField`,会将其作为树形结构递归处理每一级数据),之后将处理好的数据通过`optionsPropName`指定的属性传递给目标组件。
|
||||
|
||||
::: details 包装级联选择器,点击下拉时开始加载远程数据
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import { ApiComponent } from '@vben/common-ui';
|
||||
|
||||
import { Cascader } from 'ant-design-vue';
|
||||
|
||||
const treeData: Record<string, any> = [
|
||||
{
|
||||
label: '浙江',
|
||||
value: 'zhejiang',
|
||||
children: [
|
||||
{
|
||||
value: 'hangzhou',
|
||||
label: '杭州',
|
||||
children: [
|
||||
{
|
||||
value: 'xihu',
|
||||
label: '西湖',
|
||||
},
|
||||
{
|
||||
value: 'sudi',
|
||||
label: '苏堤',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'jiaxing',
|
||||
label: '嘉兴',
|
||||
children: [
|
||||
{
|
||||
value: 'wuzhen',
|
||||
label: '乌镇',
|
||||
},
|
||||
{
|
||||
value: 'meihuazhou',
|
||||
label: '梅花洲',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'zhoushan',
|
||||
label: '舟山',
|
||||
children: [
|
||||
{
|
||||
value: 'putuoshan',
|
||||
label: '普陀山',
|
||||
},
|
||||
{
|
||||
value: 'taohuadao',
|
||||
label: '桃花岛',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '江苏',
|
||||
value: 'jiangsu',
|
||||
children: [
|
||||
{
|
||||
value: 'nanjing',
|
||||
label: '南京',
|
||||
children: [
|
||||
{
|
||||
value: 'zhonghuamen',
|
||||
label: '中华门',
|
||||
},
|
||||
{
|
||||
value: 'zijinshan',
|
||||
label: '紫金山',
|
||||
},
|
||||
{
|
||||
value: 'yuhuatai',
|
||||
label: '雨花台',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
/**
|
||||
* 模拟请求接口
|
||||
*/
|
||||
function fetchApi(): Promise<Record<string, any>> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(treeData);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ApiComponent
|
||||
:api="fetchApi"
|
||||
:component="Cascader"
|
||||
:immediate="false"
|
||||
children-field="children"
|
||||
loading-slot="suffixIcon"
|
||||
visible-event="onDropdownVisibleChange"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## 并发和缓存
|
||||
|
||||
有些场景下可能需要使用多个ApiComponent,它们使用了相同的远程数据源(例如用在可编辑的表格中)。如果直接将请求后端接口的函数传递给api属性,则每一个实例都会访问一次接口,这会造成资源浪费,是完全没有必要的。Tanstack Query提供了并发控制、缓存、重试等诸多特性,我们可以将接口请求函数用useQuery包装一下再传递给ApiComponent,这样的话无论页面有多少个使用相同数据源的ApiComponent实例,都只会发起一次远程请求。演示效果请参考 [Playground vue-query](https://www.vben.pro/#/demos/features/vue-query),具体代码请查看项目文件[concurrency-caching](https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/views/demos/features/vue-query/concurrency-caching.vue)
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 | 版本要求 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| modelValue(v-model) | 当前值 | `any` | - | - |
|
||||
| component | 欲包装的组件(以下称为目标组件) | `Component` | - | - |
|
||||
| numberToString | 是否将value从数字转为string | `boolean` | `false` | - |
|
||||
| api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - | - |
|
||||
| params | 传递给api的参数 | `Record<string, any>` | - | - |
|
||||
| resultField | 从api返回的结果中提取options数组的字段名 | `string` | - | - |
|
||||
| labelField | label字段名 | `string` | `label` | - |
|
||||
| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` | - |
|
||||
| valueField | value字段名 | `string` | `value` | - |
|
||||
| optionsPropName | 目标组件接收options数据的属性名称 | `string` | `options` | - |
|
||||
| modelPropName | 目标组件的双向绑定属性名,默认为modelValue。部分组件可能为value | `string` | `modelValue` | - |
|
||||
| immediate | 是否立即调用api | `boolean` | `true` | - |
|
||||
| alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` | - |
|
||||
| beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - | - |
|
||||
| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - | - |
|
||||
| options | 直接传入选项数据,也作为api返回空数据时的后备数据 | `OptionsItem[]` | - | - |
|
||||
| visibleEvent | 触发重新请求数据的事件名 | `string` | - | - |
|
||||
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - | - |
|
||||
| autoSelect | 自动设置选项 | `'first' \| 'last' \| 'one'\| ((item: OptionsItem[]) => OptionsItem) \| false` | `false` | >5.5.4 |
|
||||
|
||||
#### autoSelect 自动设置选项
|
||||
|
||||
如果当前值为undefined,在选项数据成功加载之后,自动从备选项中选择一个作为当前值。默认值为`false`,即不自动选择选项。注意:该属性不应用于多选组件。可选值有:
|
||||
|
||||
- `"first"`:自动选择第一个选项
|
||||
- `"last"`:自动选择最后一个选项
|
||||
- `"one"`:有且仅有一个选项时,自动选择它
|
||||
- `自定义函数`:自定义选择逻辑,函数的参数为options,返回值为选择的选项
|
||||
- `false`:不自动选择选项
|
||||
|
||||
### Methods
|
||||
|
||||
| 方法 | 描述 | 类型 | 版本要求 |
|
||||
| --- | --- | --- | --- |
|
||||
| getComponentRef | 获取被包装的组件的实例 | ()=>T | >5.5.4 |
|
||||
| updateParam | 设置接口请求参数(将与params属性合并) | (newParams: Record<string, any>)=>void | >5.5.4 |
|
||||
| getOptions | 获取已加载的选项数据 | ()=>OptionsItem[] | >5.5.4 |
|
||||
| getValue | 获取当前值 | ()=>any | >5.5.4 |
|
@@ -42,11 +42,18 @@ outline: deep
|
||||
| transition | 动画效果 | `string` | `linear` |
|
||||
| decimals | 保留小数点位数 | `number` | `0` |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 描述 | 类型 |
|
||||
| -------------- | -------------- | -------------- |
|
||||
| started | 动画已开始 | `()=>void` |
|
||||
| finished | 动画已结束 | `()=>void` |
|
||||
| ~~onStarted~~ | ~~动画已开始~~ | ~~`()=>void`~~ |
|
||||
| ~~onFinished~~ | ~~动画已结束~~ | ~~`()=>void`~~ |
|
||||
|
||||
### Methods
|
||||
|
||||
以下事件,只有在 `useVbenModal({onCancel:()=>{}})` 中传入才会生效。
|
||||
|
||||
| 事件名 | 描述 | 类型 |
|
||||
| 方法名 | 描述 | 类型 |
|
||||
| ------ | ------------ | ---------- |
|
||||
| start | 开始执行动画 | `()=>void` |
|
||||
| reset | 重置 | `()=>void` |
|
||||
|
@@ -54,6 +54,8 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra
|
||||
|
||||
- `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。
|
||||
- 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。
|
||||
- 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。
|
||||
- 如果抽屉的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultDrawerProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。
|
||||
|
||||
:::
|
||||
|
||||
@@ -74,12 +76,16 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||
| connectedComponent | 连接另一个Modal组件 | `Component` | - |
|
||||
| destroyOnClose | 关闭时销毁`connectedComponent` | `boolean` | `false` |
|
||||
| title | 标题 | `string\|slot` | - |
|
||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||
| description | 描述信息 | `string\|slot` | - |
|
||||
| isOpen | 弹窗打开状态 | `boolean` | `false` |
|
||||
| loading | 弹窗加载状态 | `boolean` | `false` |
|
||||
| closable | 显示关闭按钮 | `boolean` | `true` |
|
||||
| closeIconPlacement | 关闭按钮位置 | `'left'\|'right'` | `right` |
|
||||
| modal | 显示遮罩 | `boolean` | `true` |
|
||||
| header | 显示header | `boolean` | `true` |
|
||||
| footer | 显示footer | `boolean\|slot` | `true` |
|
||||
@@ -95,35 +101,56 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
| contentClass | modal内容区域的class | `string` | - |
|
||||
| footerClass | modal底部区域的class | `string` | - |
|
||||
| headerClass | modal顶部区域的class | `string` | - |
|
||||
| zIndex | 抽屉的ZIndex层级 | `number` | `1000` |
|
||||
| overlayBlur | 遮罩模糊度 | `number` | - |
|
||||
|
||||
::: info appendToMain
|
||||
|
||||
`appendToMain`可以指定将抽屉挂载到内容区域,打开抽屉时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,抽屉会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便抽屉能够正确计算高度。
|
||||
|
||||
:::
|
||||
|
||||
### Event
|
||||
|
||||
以下事件,只有在 `useVbenDrawer({onCancel:()=>{}})` 中传入才会生效。
|
||||
|
||||
| 事件名 | 描述 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` |
|
||||
| onCancel | 点击取消按钮触发 | `()=>void` |
|
||||
| onConfirm | 点击确认按钮触发 | `()=>void` |
|
||||
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` |
|
||||
| 事件名 | 描述 | 类型 | 版本限制 |
|
||||
| --- | --- | --- | --- |
|
||||
| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` | --- |
|
||||
| onCancel | 点击取消按钮触发 | `()=>void` | --- |
|
||||
| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.5.2 |
|
||||
| onConfirm | 点击确认按钮触发 | `()=>void` | --- |
|
||||
| onOpenChange | 关闭或者打开弹窗时触发 | `(isOpen:boolean)=>void` | --- |
|
||||
| onOpened | 打开动画播放完毕时触发 | `()=>void` | >5.5.2 |
|
||||
|
||||
### Slots
|
||||
|
||||
除了上面的属性类型包含`slot`,还可以通过插槽来自定义弹窗的内容。
|
||||
|
||||
| 插槽名 | 描述 |
|
||||
| -------------- | ------------------- |
|
||||
| default | 默认插槽 - 弹窗内容 |
|
||||
| prepend-footer | 取消按钮左侧 |
|
||||
| append-footer | 取消按钮右侧 |
|
||||
| 插槽名 | 描述 |
|
||||
| -------------- | -------------------------------------------------- |
|
||||
| default | 默认插槽 - 弹窗内容 |
|
||||
| prepend-footer | 取消按钮左侧 |
|
||||
| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
|
||||
| append-footer | 确认按钮右侧 |
|
||||
| close-icon | 关闭按钮图标 |
|
||||
| extra | 额外内容(标题右侧) |
|
||||
|
||||
### modalApi
|
||||
### drawerApi
|
||||
|
||||
| 事件名 | 描述 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| setState | 动态设置弹窗状态属性 | `setState(props) \| setState((prev)=>(props))` |
|
||||
| open | 打开弹窗 | `()=>void` |
|
||||
| close | 关闭弹窗 | `()=>void` |
|
||||
| setData | 设置共享数据 | `<T>(data:T)=>void` |
|
||||
| getData | 获取共享数据 | `<T>()=>T` |
|
||||
| useStore | 获取可响应式状态 | - |
|
||||
| 方法 | 描述 | 类型 | 版本限制 |
|
||||
| --- | --- | --- | --- |
|
||||
| setState | 动态设置弹窗状态属性 | `(((prev: ModalState) => Partial<ModalState>)\| Partial<ModalState>)=>drawerApi` |
|
||||
| open | 打开弹窗 | `()=>void` | --- |
|
||||
| close | 关闭弹窗 | `()=>void` | --- |
|
||||
| setData | 设置共享数据 | `<T>(data:T)=>drawerApi` | --- |
|
||||
| getData | 获取共享数据 | `<T>()=>T` | --- |
|
||||
| useStore | 获取可响应式状态 | - | --- |
|
||||
| lock | 将抽屉标记为提交中,锁定当前状态 | `(isLock:boolean)=>drawerApi` | >5.5.3 |
|
||||
| unlock | lock方法的反操作,解除抽屉的锁定状态,也是lock(false)的别名 | `()=>drawerApi` | >5.5.3 |
|
||||
|
||||
::: info lock
|
||||
|
||||
`lock`方法用于锁定抽屉的状态,一般用于提交数据的过程中防止用户重复提交或者抽屉被意外关闭、表单数据被改变等等。当处于锁定状态时,抽屉的确认按钮会变为loading状态,同时禁用取消按钮和关闭按钮、禁止ESC或者点击遮罩等方式关闭抽屉、开启抽屉的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的抽屉时,会自动解锁。要主动解除这种状态,可以调用`unlock`方法或者再次调用lock方法并传入false参数。
|
||||
|
||||
:::
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user