mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-26 00:26:20 +08:00
Compare commits
114 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
7ab00250bf | ||
![]() |
9896a67c21 | ||
![]() |
db38ef522f | ||
![]() |
845f2a2abd | ||
![]() |
9b73792dc9 | ||
![]() |
fccbe44cf7 | ||
![]() |
e23486dbc6 | ||
![]() |
935df713f3 | ||
![]() |
17c7ce8f21 | ||
![]() |
24b9aa44d2 | ||
![]() |
014e6d38a0 | ||
![]() |
12f216c0e7 | ||
![]() |
ae3f7cb909 | ||
![]() |
32117b73aa | ||
![]() |
e8992a1d16 | ||
![]() |
3c4af23edf | ||
![]() |
e3a93970f4 | ||
![]() |
7b9866158b | ||
![]() |
3fb286b552 | ||
![]() |
253abc5ef1 | ||
![]() |
5f55799572 | ||
![]() |
54a9ff088f | ||
![]() |
73502677ff | ||
![]() |
dedba18553 | ||
![]() |
f85badf482 | ||
![]() |
12f25cf3a2 | ||
![]() |
c8dd9bbf0b | ||
![]() |
3587ec54eb | ||
![]() |
dbcb7138f2 | ||
![]() |
fe58af2e78 | ||
![]() |
94c68c966e | ||
![]() |
77083abcc5 | ||
![]() |
1302092798 | ||
![]() |
ec53bf8084 | ||
![]() |
b87d41bada | ||
![]() |
788a29a8cb | ||
![]() |
3bd5ef4523 | ||
![]() |
86e52ce58a | ||
![]() |
9ddaba5333 | ||
![]() |
5b079471b9 | ||
![]() |
8cc73cf59c | ||
![]() |
a89711610d | ||
![]() |
67c2b13713 | ||
![]() |
1ff1e4a8d7 | ||
![]() |
ea8af98324 | ||
![]() |
dc15accd04 | ||
![]() |
94efcec7da | ||
![]() |
a3d0d2ed34 | ||
![]() |
90dc00b168 | ||
![]() |
ba36ce8836 |
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
|
||||
|
||||
# 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
|
||||
/.vscode/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
/packages/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
/packages/@core/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
/internal/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
/scripts/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com
|
||||
|
||||
# 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
|
||||
docs/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5
|
||||
|
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -62,7 +62,7 @@ body:
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
# description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com).
|
||||
options:
|
||||
- label: Read the [docs](https://anncwb.github.io/vue-vben-admin-doc/)
|
||||
- label: Read the [docs](https://doc.vben.pro/)
|
||||
required: true
|
||||
- label: Ensure the code is up to date. (Some issues have been fixed in the latest version)
|
||||
required: true
|
||||
|
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -62,7 +62,7 @@ body:
|
||||
label: Validations
|
||||
description: Before submitting the issue, please make sure you do the following
|
||||
options:
|
||||
- label: Read the [docs](https://anncwb.github.io/vue-vben-admin-doc/)
|
||||
- label: Read the [docs](https://doc.vben.pro/)
|
||||
required: true
|
||||
- label: Ensure the code is up to date. (Some issues have been fixed in the latest version)
|
||||
required: true
|
||||
|
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -222,5 +222,6 @@
|
||||
"commentTranslate.hover.enabled": false,
|
||||
"commentTranslate.multiLineMerge": true,
|
||||
"vue.server.hybridMode": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"oxc.enable": false
|
||||
}
|
||||
|
@@ -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);
|
||||
});
|
||||
|
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-antd",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -8,7 +8,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { globalShareState } from '@vben/common-ui';
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
@@ -48,12 +48,15 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'AutoComplete'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'DefaultButton'
|
||||
| 'Divider'
|
||||
| 'IconPicker'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'InputPassword'
|
||||
@@ -77,7 +80,38 @@ async function initComponentAdapter() {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: Select,
|
||||
loadingSlot: 'suffixIcon',
|
||||
visibleEvent: 'onDropdownVisibleChange',
|
||||
modelPropName: 'value',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
ApiTreeSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: TreeSelect,
|
||||
fieldNames: { label: 'label', value: 'value', children: 'children' },
|
||||
loadingSlot: 'suffixIcon',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'treeData',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
AutoComplete,
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
@@ -87,6 +121,13 @@ async function initComponentAdapter() {
|
||||
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
IconPicker: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
IconPicker,
|
||||
{ iconSlot: 'addonAfter', inputComponent: Input, ...props, ...attrs },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
|
@@ -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)),
|
||||
|
@@ -17,12 +17,12 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
/** 不需要权限的菜单列表(会显示在菜单中) */
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由+静态路由组成 */
|
||||
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||
* 无需走权限验证(会一直显示在菜单中) */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...externalRoutes,
|
||||
@@ -32,5 +32,6 @@ const routes: RouteRecordRaw[] = [
|
||||
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
export { accessRoutes, coreRouteNames, 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,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-ele",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -4,23 +4,28 @@
|
||||
*/
|
||||
|
||||
import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { globalShareState } from '@vben/common-ui';
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElCheckbox,
|
||||
ElCheckboxButton,
|
||||
ElCheckboxGroup,
|
||||
ElDatePicker,
|
||||
ElDivider,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElNotification,
|
||||
ElRadio,
|
||||
ElRadioButton,
|
||||
ElRadioGroup,
|
||||
ElSelect,
|
||||
ElSelectV2,
|
||||
ElSpace,
|
||||
ElSwitch,
|
||||
ElTimePicker,
|
||||
@@ -40,10 +45,13 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'Divider'
|
||||
| 'IconPicker'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'RadioGroup'
|
||||
@@ -60,11 +68,59 @@ async function initComponentAdapter() {
|
||||
// 如果你的组件体积比较大,可以使用异步加载
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: ElSelectV2,
|
||||
loadingSlot: 'loading',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
ApiTreeSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: ElTreeSelect,
|
||||
props: { label: 'label', children: 'children' },
|
||||
nodeKey: 'value',
|
||||
loadingSlot: 'loading',
|
||||
optionsPropName: 'data',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
Checkbox: ElCheckbox,
|
||||
CheckboxGroup: ElCheckboxGroup,
|
||||
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(
|
||||
ElCheckboxGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ ...slots, default: defaultSlot },
|
||||
);
|
||||
},
|
||||
// 自定义默认按钮
|
||||
DefaulButton: (props, { attrs, slots }) => {
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
|
||||
},
|
||||
// 自定义主要按钮
|
||||
@@ -72,13 +128,87 @@ async function initComponentAdapter() {
|
||||
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Divider: ElDivider,
|
||||
IconPicker: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
IconPicker,
|
||||
{
|
||||
iconSlot: 'append',
|
||||
modelValueProp: 'model-value',
|
||||
inputComponent: ElInput,
|
||||
...props,
|
||||
...attrs,
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
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,
|
||||
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: {
|
||||
|
@@ -7,6 +7,7 @@ import '@vben/styles';
|
||||
import '@vben/styles/ele';
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
import { ElLoading } from 'element-plus';
|
||||
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
|
||||
@@ -19,6 +20,9 @@ async function bootstrap(namespace: string) {
|
||||
await initComponentAdapter();
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册Element Plus提供的v-loading指令
|
||||
app.directive('loading', ElLoading.directive);
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
|
||||
|
@@ -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)),
|
||||
|
@@ -17,12 +17,12 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
/** 不需要权限的菜单列表(会显示在菜单中) */
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由+静态路由组成 */
|
||||
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||
* 无需走权限验证(会一直显示在菜单中) */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...externalRoutes,
|
||||
@@ -32,5 +32,6 @@ const routes: RouteRecordRaw[] = [
|
||||
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
export { accessRoutes, coreRouteNames, routes };
|
||||
|
@@ -23,6 +23,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'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@@ -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,4 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
@@ -6,6 +8,7 @@ import {
|
||||
ElCard,
|
||||
ElMessage,
|
||||
ElNotification,
|
||||
ElSegmented,
|
||||
ElSpace,
|
||||
ElTable,
|
||||
} from 'element-plus';
|
||||
@@ -47,6 +50,10 @@ const tableData = [
|
||||
{ prop1: '5', prop2: 'E' },
|
||||
{ prop1: '6', prop2: 'F' },
|
||||
];
|
||||
|
||||
const segmentedValue = ref('Mon');
|
||||
|
||||
const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -54,41 +61,57 @@ const tableData = [
|
||||
description="支持多语言,主题功能集成切换等"
|
||||
title="Element Plus组件使用演示"
|
||||
>
|
||||
<ElCard class="mb-5">
|
||||
<template #header> 按钮 </template>
|
||||
<ElSpace>
|
||||
<ElButton text>Text</ElButton>
|
||||
<ElButton>Default</ElButton>
|
||||
<ElButton type="primary"> Primary </ElButton>
|
||||
<ElButton type="info"> Info </ElButton>
|
||||
<ElButton type="success"> Success </ElButton>
|
||||
<ElButton type="warning"> Warning </ElButton>
|
||||
<ElButton type="danger"> Error </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5">
|
||||
<template #header> Message </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="info"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="error"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="warning"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="success"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5">
|
||||
<template #header> Notification </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="notify('info')"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="notify('error')"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="notify('warning')"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="notify('success')"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5">
|
||||
<ElTable :data="tableData" stripe>
|
||||
<ElTable.TableColumn label="测试列1" prop="prop1" />
|
||||
<ElTable.TableColumn label="测试列2" prop="prop2" />
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
<div class="flex flex-wrap gap-5">
|
||||
<ElCard class="mb-5 w-auto">
|
||||
<template #header> 按钮 </template>
|
||||
<ElSpace>
|
||||
<ElButton text>Text</ElButton>
|
||||
<ElButton>Default</ElButton>
|
||||
<ElButton type="primary"> Primary </ElButton>
|
||||
<ElButton type="info"> Info </ElButton>
|
||||
<ElButton type="success"> Success </ElButton>
|
||||
<ElButton type="warning"> Warning </ElButton>
|
||||
<ElButton type="danger"> Error </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> Message </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="info"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="error"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="warning"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="success"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> Notification </template>
|
||||
<ElSpace>
|
||||
<ElButton type="info" @click="notify('info')"> 信息 </ElButton>
|
||||
<ElButton type="danger" @click="notify('error')"> 错误 </ElButton>
|
||||
<ElButton type="warning" @click="notify('warning')"> 警告 </ElButton>
|
||||
<ElButton type="success" @click="notify('success')"> 成功 </ElButton>
|
||||
</ElSpace>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-auto">
|
||||
<template #header> Segmented </template>
|
||||
<ElSegmented
|
||||
v-model="segmentedValue"
|
||||
:options="segmentedOptions"
|
||||
size="large"
|
||||
/>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<template #header> V-Loading </template>
|
||||
<div class="flex size-72 items-center justify-center" v-loading="true">
|
||||
一些演示的内容
|
||||
</div>
|
||||
</ElCard>
|
||||
<ElCard class="mb-5 w-80">
|
||||
<ElTable :data="tableData" stripe>
|
||||
<ElTable.TableColumn label="测试列1" prop="prop1" />
|
||||
<ElTable.TableColumn label="测试列2" prop="prop2" />
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
181
apps/web-ele/src/views/demos/form/basic.vue
Normal file
181
apps/web-ele/src/views/demos/form/basic.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<script lang="ts" setup>
|
||||
import { h } from 'vue';
|
||||
|
||||
import { Page } 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: [
|
||||
{
|
||||
// 组件需要在 #/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' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
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="表单演示"
|
||||
>
|
||||
<ElCard>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<span class="flex-auto">基础表单演示</span>
|
||||
<ElButton type="primary" @click="setFormValues">设置表单值</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<Form />
|
||||
</ElCard>
|
||||
</Page>
|
||||
</template>
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/web-naive",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://vben.pro",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -8,7 +8,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { globalShareState } from '@vben/common-ui';
|
||||
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
NDivider,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NRadio,
|
||||
NRadioButton,
|
||||
NRadioGroup,
|
||||
NSelect,
|
||||
NSpace,
|
||||
@@ -42,10 +44,13 @@ const withDefaultPlaceholder = <T extends Component>(
|
||||
|
||||
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
|
||||
export type ComponentType =
|
||||
| 'ApiSelect'
|
||||
| 'ApiTreeSelect'
|
||||
| 'Checkbox'
|
||||
| 'CheckboxGroup'
|
||||
| 'DatePicker'
|
||||
| 'Divider'
|
||||
| 'IconPicker'
|
||||
| 'Input'
|
||||
| 'InputNumber'
|
||||
| 'RadioGroup'
|
||||
@@ -63,8 +68,54 @@ async function initComponentAdapter() {
|
||||
// Button: () =>
|
||||
// import('xxx').then((res) => res.Button),
|
||||
|
||||
ApiSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: NSelect,
|
||||
modelPropName: 'value',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
ApiTreeSelect: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
ApiComponent,
|
||||
{
|
||||
placeholder: $t('ui.placeholder.select'),
|
||||
...props,
|
||||
...attrs,
|
||||
component: NTreeSelect,
|
||||
nodeKey: 'value',
|
||||
loadingSlot: 'arrow',
|
||||
keyField: 'value',
|
||||
modelPropName: 'value',
|
||||
optionsPropName: 'options',
|
||||
visibleEvent: 'onVisibleChange',
|
||||
},
|
||||
slots,
|
||||
);
|
||||
},
|
||||
Checkbox: NCheckbox,
|
||||
CheckboxGroup: NCheckboxGroup,
|
||||
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(
|
||||
NCheckboxGroup,
|
||||
{ ...props, ...attrs },
|
||||
{ default: defaultSlot },
|
||||
);
|
||||
},
|
||||
DatePicker: NDatePicker,
|
||||
// 自定义默认按钮
|
||||
DefaultButton: (props, { attrs, slots }) => {
|
||||
@@ -75,9 +126,37 @@ async function initComponentAdapter() {
|
||||
return h(NButton, { ...props, attrs, type: 'primary' }, slots);
|
||||
},
|
||||
Divider: NDivider,
|
||||
IconPicker: (props, { attrs, slots }) => {
|
||||
return h(
|
||||
IconPicker,
|
||||
{ iconSlot: 'suffix', inputComponent: NInput, ...props, ...attrs },
|
||||
slots,
|
||||
);
|
||||
},
|
||||
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',
|
||||
|
@@ -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)),
|
||||
|
@@ -17,12 +17,12 @@ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
/** 不需要权限的菜单列表(会显示在菜单中) */
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由+静态路由组成 */
|
||||
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||
* 无需走权限验证(会一直显示在菜单中) */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...externalRoutes,
|
||||
@@ -32,5 +32,6 @@ const routes: RouteRecordRaw[] = [
|
||||
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
export { accessRoutes, coreRouteNames, routes };
|
||||
|
@@ -31,6 +31,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'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
@@ -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]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
143
apps/web-naive/src/views/demos/form/basic.vue
Normal file
143
apps/web-naive/src/views/demos/form/basic.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<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',
|
||||
},
|
||||
{
|
||||
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,
|
||||
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' },
|
||||
{ value: 'F', label: '选项F' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'CheckboxGroup',
|
||||
fieldName: 'checkbox',
|
||||
label: 'Checkbox',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ value: 'A', label: '选项A' },
|
||||
{ value: 'B', label: '选项B' },
|
||||
{ value: 'C', label: '选项C' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'date',
|
||||
label: 'Date',
|
||||
},
|
||||
],
|
||||
});
|
||||
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>
|
54
cspell.json
54
cspell.json
@@ -4,53 +4,55 @@
|
||||
"language": "en,en-US",
|
||||
"allowCompoundWords": true,
|
||||
"words": [
|
||||
"clsx",
|
||||
"esno",
|
||||
"demi",
|
||||
"unref",
|
||||
"taze",
|
||||
"acmr",
|
||||
"antd",
|
||||
"lucide",
|
||||
"antdv",
|
||||
"astro",
|
||||
"brotli",
|
||||
"clsx",
|
||||
"defu",
|
||||
"demi",
|
||||
"echarts",
|
||||
"ependencies",
|
||||
"esno",
|
||||
"etag",
|
||||
"execa",
|
||||
"iconify",
|
||||
"iconoir",
|
||||
"intlify",
|
||||
"lockb",
|
||||
"lucide",
|
||||
"minh",
|
||||
"minw",
|
||||
"mkdist",
|
||||
"mockjs",
|
||||
"vitejs",
|
||||
"naiveui",
|
||||
"nocheck",
|
||||
"noopener",
|
||||
"noreferrer",
|
||||
"nprogress",
|
||||
"nuxt",
|
||||
"pinia",
|
||||
"prefixs",
|
||||
"publint",
|
||||
"qrcode",
|
||||
"shadcn",
|
||||
"sonner",
|
||||
"sortablejs",
|
||||
"styl",
|
||||
"taze",
|
||||
"ui-kit",
|
||||
"uicons",
|
||||
"unplugin",
|
||||
"unref",
|
||||
"vben",
|
||||
"vbenjs",
|
||||
"vueuse",
|
||||
"yxxx",
|
||||
"nuxt",
|
||||
"lockb",
|
||||
"astro",
|
||||
"ui-kit",
|
||||
"styl",
|
||||
"vnode",
|
||||
"nocheck",
|
||||
"prefixs",
|
||||
"vitepress",
|
||||
"antdv",
|
||||
"ependencies",
|
||||
"vite",
|
||||
"echarts",
|
||||
"sortablejs",
|
||||
"etag",
|
||||
"naiveui",
|
||||
"uicons",
|
||||
"iconoir"
|
||||
"vitejs",
|
||||
"vitepress",
|
||||
"vnode",
|
||||
"vueuse",
|
||||
"yxxx"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"**/node_modules/**",
|
||||
|
@@ -221,9 +221,9 @@ function nav(): DefaultTheme.NavItem[] {
|
||||
link: '/commercial/community',
|
||||
text: '👨👦👦 Community',
|
||||
},
|
||||
{
|
||||
link: '/friend-links/',
|
||||
text: '🤝 Friend Links',
|
||||
},
|
||||
// {
|
||||
// link: '/friend-links/',
|
||||
// text: '🤝 Friend Links',
|
||||
// },
|
||||
];
|
||||
}
|
||||
|
@@ -124,7 +124,7 @@ function sidebarCommercial(): DefaultTheme.SidebarItem[] {
|
||||
return [
|
||||
{
|
||||
link: 'community',
|
||||
text: '社区',
|
||||
text: '交流群',
|
||||
},
|
||||
{
|
||||
link: 'technical-support',
|
||||
@@ -148,10 +148,24 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
collapsed: false,
|
||||
text: '布局组件',
|
||||
items: [
|
||||
{
|
||||
link: 'layout-ui/page',
|
||||
text: 'Page 页面',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
collapsed: false,
|
||||
text: '通用组件',
|
||||
items: [
|
||||
{
|
||||
link: 'common-ui/vben-api-component',
|
||||
text: 'ApiComponent Api组件包装器',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-modal',
|
||||
text: 'Modal 模态框',
|
||||
@@ -172,6 +186,10 @@ function sidebarComponents(): DefaultTheme.SidebarItem[] {
|
||||
link: 'common-ui/vben-count-to-animator',
|
||||
text: 'CountToAnimator 数字动画',
|
||||
},
|
||||
{
|
||||
link: 'common-ui/vben-ellipsis-text',
|
||||
text: 'EllipsisText 省略文本',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -266,7 +284,7 @@ function nav(): DefaultTheme.NavItem[] {
|
||||
},
|
||||
{
|
||||
link: '/commercial/community',
|
||||
text: '👨👦👦 社区',
|
||||
text: '👨👦👦 交流群',
|
||||
// items: [
|
||||
// {
|
||||
// link: 'https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=22ySzj7pKiw&businessType=9&from=246610&biz=ka&mainSourceId=share&subSourceId=others&jumpsource=shorturl#/pc',
|
||||
@@ -282,10 +300,10 @@ function nav(): DefaultTheme.NavItem[] {
|
||||
// },
|
||||
// ],
|
||||
},
|
||||
{
|
||||
link: '/friend-links/',
|
||||
text: '🤝 友情链接',
|
||||
},
|
||||
// {
|
||||
// link: '/friend-links/',
|
||||
// text: '🤝 友情链接',
|
||||
// },
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/docs",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vitepress build",
|
||||
|
@@ -14,8 +14,6 @@ initComponentAdapter();
|
||||
setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
baseModelPropName: 'value',
|
||||
// naive-ui组件不接受onChang事件,所以需要禁用
|
||||
disabledOnChangeListener: true,
|
||||
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
|
||||
emptyStateValue: null,
|
||||
modelPropNameMap: {
|
||||
|
@@ -20,7 +20,10 @@
|
||||
|
||||
::: tip
|
||||
|
||||
因为微信群人数有限制,加微信群前,你可以通过[赞助](../sponsor/personal.md)任意金额,主动发送截图给作者,备注`加入微信群`即可。
|
||||
因为微信群人数有限制,加微信群要求:
|
||||
|
||||
- 通过[赞助](../sponsor/personal.md)任意金额。
|
||||
- 发送赞助`截图`,备注`加入微信群`即可。
|
||||
|
||||
:::
|
||||
|
||||
|
152
docs/src/components/common-ui/vben-api-component.md
Normal file
152
docs/src/components/common-ui/vben-api-component.md
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
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>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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` | - |
|
||||
|
||||
```
|
||||
|
||||
```
|
@@ -74,6 +74,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||
| title | 标题 | `string\|slot` | - |
|
||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||
| description | 描述信息 | `string\|slot` | - |
|
||||
@@ -88,12 +89,20 @@ const [Drawer, drawerApi] = useVbenDrawer({
|
||||
| closeOnPressEscape | esc 关闭弹窗 | `boolean` | `true` |
|
||||
| confirmText | 确认按钮文本 | `string\|slot` | `确认` |
|
||||
| cancelText | 取消按钮文本 | `string\|slot` | `取消` |
|
||||
| placement | 抽屉弹出位置 | `'left'\|'right'\|'top'\|'bottom'` | `right` |
|
||||
| showCancelButton | 显示取消按钮 | `boolean` | `true` |
|
||||
| showConfirmButton | 显示确认按钮文本 | `boolean` | `true` |
|
||||
| class | modal的class,宽度通过这个配置 | `string` | - |
|
||||
| contentClass | modal内容区域的class | `string` | - |
|
||||
| footerClass | modal底部区域的class | `string` | - |
|
||||
| headerClass | modal顶部区域的class | `string` | - |
|
||||
| zIndex | 抽屉的ZIndex层级 | `number` | `1000` |
|
||||
|
||||
::: info appendToMain
|
||||
|
||||
`appendToMain`可以指定将抽屉挂载到内容区域,打开抽屉时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,抽屉会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便抽屉能够正确计算高度。
|
||||
|
||||
:::
|
||||
|
||||
### Event
|
||||
|
||||
|
56
docs/src/components/common-ui/vben-ellipsis-text.md
Normal file
56
docs/src/components/common-ui/vben-ellipsis-text.md
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Vben EllipsisText 省略文本
|
||||
|
||||
框架提供的文本展示组件,可配置超长省略、tooltip提示、展开收起等功能。
|
||||
|
||||
> 如果文档内没有参数说明,可以尝试在在线示例内寻找
|
||||
|
||||
## 基础用法
|
||||
|
||||
通过默认插槽设置文本内容,`maxWidth`属性设置最大宽度。
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/line" />
|
||||
|
||||
## 可折叠的文本块
|
||||
|
||||
通过`line`设置折叠后的行数,`expand`属性设置是否支持展开收起。
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/expand" />
|
||||
|
||||
## 自定义提示浮层
|
||||
|
||||
通过名为`tooltip`的插槽定制提示信息。
|
||||
|
||||
<DemoPreview dir="demos/vben-ellipsis-text/tooltip" />
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| expand | 支持点击展开或收起 | `boolean` | `false` |
|
||||
| line | 文本最大行数 | `number` | `1` |
|
||||
| maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
|
||||
| placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` |
|
||||
| tooltip | 启用文本提示 | `boolean` | `true` |
|
||||
| tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - |
|
||||
| tooltipColor | 提示文本的颜色 | `string` | - |
|
||||
| tooltipFontSize | 提示文本的大小 | `string` | - |
|
||||
| tooltipMaxWidth | 提示浮层的最大宽度。如不设置则保持与文本宽度一致 | `number` | - |
|
||||
| tooltipOverlayStyle | 提示框内容区域样式 | `CSSProperties` | `{ textAlign: 'justify' }` |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 描述 | 类型 |
|
||||
| ------------ | ------------ | -------------------------- |
|
||||
| expandChange | 展开状态改变 | `(isExpand:boolean)=>void` |
|
||||
|
||||
### Slots
|
||||
|
||||
| 插槽名 | 描述 |
|
||||
| ------- | -------------------------------- |
|
||||
| tooltip | 启用文本提示时,用来定制提示内容 |
|
@@ -87,7 +87,7 @@ import type { BaseFormComponentType } from '@vben/common-ui';
|
||||
import type { Component, SetupContext } from 'vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { globalShareState } from '@vben/common-ui';
|
||||
import { globalShareState, IconPicker } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import {
|
||||
@@ -149,6 +149,7 @@ export type ComponentType =
|
||||
| 'TimePicker'
|
||||
| 'TreeSelect'
|
||||
| 'Upload'
|
||||
| 'IconPicker';
|
||||
| BaseFormComponentType;
|
||||
|
||||
async function initComponentAdapter() {
|
||||
@@ -166,6 +167,7 @@ async function initComponentAdapter() {
|
||||
return h(Button, { ...props, attrs, type: 'default' }, slots);
|
||||
},
|
||||
Divider,
|
||||
IconPicker,
|
||||
Input: withDefaultPlaceholder(Input, 'input'),
|
||||
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
|
||||
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
|
||||
@@ -285,6 +287,8 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` |
|
||||
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` |
|
||||
| validate | 表单校验 | `()=>Promise<void>` |
|
||||
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` |
|
||||
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` |
|
||||
| resetValidate | 重置表单校验 | `()=>Promise<void>` |
|
||||
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` |
|
||||
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` |
|
||||
@@ -304,16 +308,19 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| actionWrapperClass | 表单操作区域class | `any` | - |
|
||||
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>,) => void` | - |
|
||||
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
|
||||
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
|
||||
| submitButtonOptions | 提交按钮组件参数 | `ActionButtonOptions` | - |
|
||||
| showDefaultActions | 是否显示默认操作按钮 | `boolean` | `true` |
|
||||
| collapsed | 是否折叠,在`是否展开,在showCollapseButton=true`时生效 | `boolean` | `false` |
|
||||
| collapsed | 是否折叠,在`showCollapseButton`为`true`时生效 | `boolean` | `false` |
|
||||
| collapseTriggerResize | 折叠时,触发`resize`事件 | `boolean` | `false` |
|
||||
| collapsedRows | 折叠时保持的行数 | `number` | `1` |
|
||||
| fieldMappingTime | 用于将表单内时间区域的应设成 2 个字段 | `[string, [string, string], string?][]` | - |
|
||||
| fieldMappingTime | 用于将表单内时间区域组件的数组值映射成 2 个字段 | `[string, [string, string], string?][]` | - |
|
||||
| commonConfig | 表单项的通用配置,每个配置都会传递到每个表单项,表单项可覆盖 | `FormCommonConfig` | - |
|
||||
| schema | 表单项的每一项配置 | `FormSchema` | - |
|
||||
| schema | 表单项的每一项配置 | `FormSchema[]` | - |
|
||||
| submitOnEnter | 按下回车健时提交表单 | `boolean` | false |
|
||||
| submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false |
|
||||
|
||||
### TS 类型说明
|
||||
|
||||
@@ -350,10 +357,21 @@ export interface FormCommonConfig {
|
||||
* 所有表单项的props
|
||||
*/
|
||||
componentProps?: ComponentProps;
|
||||
/**
|
||||
* 是否紧凑模式(移除表单底部为显示校验错误信息所预留的空间)。
|
||||
* 在有设置校验规则的场景下,建议不要将其设置为true
|
||||
* 默认为false。但用作表格的搜索表单时,默认为true
|
||||
* @default false
|
||||
*/
|
||||
compact?: boolean;
|
||||
/**
|
||||
* 所有表单项的控件样式
|
||||
*/
|
||||
controlClass?: string;
|
||||
/**
|
||||
* 在表单项的Label后显示一个冒号
|
||||
*/
|
||||
colon?: boolean;
|
||||
/**
|
||||
* 所有表单项的禁用状态
|
||||
* @default false
|
||||
@@ -413,13 +431,13 @@ export interface FormSchema<
|
||||
dependencies?: FormItemDependencies;
|
||||
/** 描述 */
|
||||
description?: string;
|
||||
/** 字段名 */
|
||||
/** 字段名,也作为自定义插槽的名称 */
|
||||
fieldName: string;
|
||||
/** 帮助信息 */
|
||||
help?: string;
|
||||
/** 表单项 */
|
||||
label?: string;
|
||||
// 自定义组件内部渲染
|
||||
/** 自定义组件内部渲染 */
|
||||
renderComponentContent?: RenderComponentContentType;
|
||||
/** 字段规则 */
|
||||
rules?: FormSchemaRuleType;
|
||||
@@ -436,7 +454,7 @@ export interface FormSchema<
|
||||
|
||||
```ts
|
||||
dependencies: {
|
||||
// 只有当 name 字段的值变化时,才会触发联动
|
||||
// 触发字段。只有这些字段值变动时,联动才会触发
|
||||
triggerFields: ['name'],
|
||||
// 动态判断当前字段是否需要显示,不显示则直接销毁
|
||||
if(values,formApi){},
|
||||
@@ -457,11 +475,11 @@ dependencies: {
|
||||
|
||||
### 表单校验
|
||||
|
||||
表单联动需要通过 schema 内的 `rules` 属性进行配置。
|
||||
表单校验需要通过 schema 内的 `rules` 属性进行配置。
|
||||
|
||||
rules的值可以是一个字符串,也可以是一个zod的schema。
|
||||
rules的值可以是字符串(预定义的校验规则名称),也可以是一个zod的schema。
|
||||
|
||||
#### 字符串
|
||||
#### 预定义的校验规则
|
||||
|
||||
```ts
|
||||
// 表示字段必填,默认会根据适配器的required进行国际化
|
||||
@@ -487,11 +505,16 @@ import { z } from '#/adapter/form';
|
||||
rules: z.string().min(1, { message: '请输入字符串' });
|
||||
}
|
||||
|
||||
// 可选,并且携带默认值
|
||||
// 可选(可以是undefined),并且携带默认值。注意zod的optional不包括空字符串''
|
||||
{
|
||||
rules: z.string().default('默认值').optional(),
|
||||
}
|
||||
|
||||
// 可以是空字符串、undefined或者一个邮箱地址
|
||||
{
|
||||
rules: z.union(z.string().email().optional(), z.literal(""))
|
||||
}
|
||||
|
||||
// 复杂校验
|
||||
{
|
||||
z.string().min(1, { message: "请输入" })
|
||||
@@ -500,3 +523,20 @@ import { z } from '#/adapter/form';
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Slots
|
||||
|
||||
可以使用以下插槽在表单中插入自定义的内容
|
||||
|
||||
| 插槽名 | 描述 |
|
||||
| ------------- | ------------------ |
|
||||
| reset-before | 重置按钮之前的位置 |
|
||||
| submit-before | 提交按钮之前的位置 |
|
||||
| expand-before | 展开按钮之前的位置 |
|
||||
| expand-after | 展开按钮之后的位置 |
|
||||
|
||||
::: tip 字段插槽
|
||||
|
||||
除了以上内置插槽之外,`schema`属性中每个字段的`fieldName`都可以作为插槽名称,这些字段插槽的优先级高于`component`定义的组件。也就是说,当提供了与`fieldName`同名的插槽时,这些插槽的内容将会作为这些字段的组件,此时`component`的值将会被忽略。
|
||||
|
||||
:::
|
||||
|
@@ -80,6 +80,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
|
||||
| title | 标题 | `string\|slot` | - |
|
||||
| titleTooltip | 标题提示信息 | `string\|slot` | - |
|
||||
| description | 描述信息 | `string\|slot` | - |
|
||||
@@ -93,18 +94,26 @@ const [Modal, modalApi] = useVbenModal({
|
||||
| modal | 显示遮罩 | `boolean` | `true` |
|
||||
| header | 显示header | `boolean` | `true` |
|
||||
| footer | 显示footer | `boolean\|slot` | `true` |
|
||||
| confirmDisabled | 禁用确认按钮 | `boolean` | `false` |
|
||||
| confirmLoading | 确认按钮loading状态 | `boolean` | `false` |
|
||||
| closeOnClickModal | 点击遮罩关闭弹窗 | `boolean` | `true` |
|
||||
| closeOnPressEscape | esc 关闭弹窗 | `boolean` | `true` |
|
||||
| confirmText | 确认按钮文本 | `string\|slot` | `确认` |
|
||||
| cancelText | 取消按钮文本 | `string\|slot` | `取消` |
|
||||
| showCancelButton | 显示取消按钮 | `boolean` | `true` |
|
||||
| showConfirmButton | 显示确认按钮文本 | `boolean` | `true` |
|
||||
| showConfirmButton | 显示确认按钮 | `boolean` | `true` |
|
||||
| class | modal的class,宽度通过这个配置 | `string` | - |
|
||||
| contentClass | modal内容区域的class | `string` | - |
|
||||
| footerClass | modal底部区域的class | `string` | - |
|
||||
| headerClass | modal顶部区域的class | `string` | - |
|
||||
| bordered | 是否显示border | `boolean` | `false` |
|
||||
| zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
|
||||
|
||||
::: info appendToMain
|
||||
|
||||
`appendToMain`可以指定将弹窗挂载到内容区域,打开这种弹窗时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,弹窗会挂载到body上。但是:挂载到内容区域时,作为页面根容器的`Page`组件,需要设置`auto-content-height`属性,以便弹窗能够正确计算高度。
|
||||
|
||||
:::
|
||||
|
||||
### Event
|
||||
|
||||
|
@@ -165,6 +165,8 @@ vxeUI.renderer.add('CellLink', {
|
||||
|
||||
**表单搜索** 部分采用了`Vben Form 表单`,参考 [Vben Form 表单文档](/components/common-ui/vben-form)。
|
||||
|
||||
当启用了表单搜索时,可以在toolbarConfig中配置`search`为`true`来让表格在工具栏区域显示一个搜索表单控制按钮。
|
||||
|
||||
<DemoPreview dir="demos/vben-vxe-table/form" />
|
||||
|
||||
## 单元格编辑
|
||||
@@ -215,14 +217,15 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
|
||||
useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表单的方法。
|
||||
|
||||
| 方法名 | 描述 | 类型 |
|
||||
| --- | --- | --- |
|
||||
| setLoading | 设置loading状态 | `(loading)=>void` |
|
||||
| setGridOptions | 设置vxe-table grid组件参数 | `(options: Partial<VxeGridProps['gridOptions'])=>void` |
|
||||
| reload | 重载表格,会进行初始化 | `(params:any)=>void` |
|
||||
| query | 重载表格,会保留当前分页 | `(params:any)=>void` |
|
||||
| grid | vxe-table grid实例 | `VxeGridInstance` |
|
||||
| formApi | vbenForm api实例 | `FormApi` |
|
||||
| 方法名 | 描述 | 类型 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| setLoading | 设置loading状态 | `(loading)=>void` | - |
|
||||
| setGridOptions | 设置vxe-table grid组件参数 | `(options: Partial<VxeGridProps['gridOptions'])=>void` | - |
|
||||
| reload | 重载表格,会进行初始化 | `(params:any)=>void` | - |
|
||||
| query | 重载表格,会保留当前分页 | `(params:any)=>void` | - |
|
||||
| grid | vxe-table grid实例 | `VxeGridInstance` | - |
|
||||
| formApi | vbenForm api实例 | `FormApi` | - |
|
||||
| toggleSearchForm | 设置搜索表单显示状态 | `(show?: boolean)=>boolean` | 当省略参数时,则将表单在显示和隐藏两种状态之间切换 |
|
||||
|
||||
## Props
|
||||
|
||||
@@ -236,3 +239,4 @@ useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表
|
||||
| gridOptions | grid组件的参数 | `VxeTableGridProps` |
|
||||
| gridEvents | grid组件的触发的⌚️ | `VxeGridListeners` |
|
||||
| formOptions | 表单参数 | `VbenFormProps` |
|
||||
| showSearchForm | 是否显示搜索表单 | `boolean` |
|
||||
|
@@ -6,6 +6,10 @@
|
||||
|
||||
:::
|
||||
|
||||
## 布局组件
|
||||
|
||||
布局组件一般在页面内容区域用作顶层容器组件,提供一些统一的布局样式和基本功能。
|
||||
|
||||
## 通用组件
|
||||
|
||||
通用组件是一些常用的组件,比如弹窗、抽屉、表单等。大部分基于 `Tailwind CSS` 实现,可适用于不同 UI 组件库的应用。
|
||||
|
44
docs/src/components/layout-ui/page.md
Normal file
44
docs/src/components/layout-ui/page.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Page 常规页面组件
|
||||
|
||||
提供一个常规页面布局的组件,包括头部、内容区域、底部三个部分。
|
||||
|
||||
::: info 写在前面
|
||||
|
||||
本组件是一个基本布局组件。如果有更多的通用页面布局需求(比如双列布局等),可以根据实际需求自行封装。
|
||||
|
||||
:::
|
||||
|
||||
## 基础用法
|
||||
|
||||
将`Page`作为你的业务页面的根组件即可。
|
||||
|
||||
### Props
|
||||
|
||||
| 属性名 | 描述 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| title | 页面标题 | `string\|slot` | - | - |
|
||||
| description | 页面描述(标题下的内容) | `string\|slot` | - | - |
|
||||
| contentClass | 内容区域的class | `string` | - | - |
|
||||
| headerClass | 头部区域的class | `string` | - | - |
|
||||
| footerClass | 底部区域的class | `string` | - | - |
|
||||
| autoContentHeight | 自动调整内容区域的高度 | `boolean` | `false` | - |
|
||||
|
||||
::: tip 注意
|
||||
|
||||
如果`title`、`description`、`extra`三者均未提供有效内容(通过`props`或者`slots`均可),则页面头部区域不会渲染。
|
||||
|
||||
:::
|
||||
|
||||
### Slots
|
||||
|
||||
| 插槽名称 | 描述 |
|
||||
| ----------- | ------------ |
|
||||
| default | 页面内容 |
|
||||
| title | 页面标题 |
|
||||
| description | 页面描述 |
|
||||
| extra | 页面头部右侧 |
|
||||
| footer | 页面底部 |
|
100
docs/src/demos/vben-api-component/cascader/index.vue
Normal file
100
docs/src/demos/vben-api-component/cascader/index.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<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>
|
10
docs/src/demos/vben-ellipsis-text/expand/index.vue
Normal file
10
docs/src/demos/vben-ellipsis-text/expand/index.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
|
||||
const text = `
|
||||
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
|
||||
`;
|
||||
</script>
|
||||
<template>
|
||||
<EllipsisText :line="3" expand>{{ text }}</EllipsisText>
|
||||
</template>
|
10
docs/src/demos/vben-ellipsis-text/line/index.vue
Normal file
10
docs/src/demos/vben-ellipsis-text/line/index.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
|
||||
const text = `
|
||||
Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、多主题配置、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3、vite、ts 等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
|
||||
`;
|
||||
</script>
|
||||
<template>
|
||||
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
|
||||
</template>
|
14
docs/src/demos/vben-ellipsis-text/tooltip/index.vue
Normal file
14
docs/src/demos/vben-ellipsis-text/tooltip/index.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
</script>
|
||||
<template>
|
||||
<EllipsisText :max-width="240">
|
||||
住在我心里孤独的 孤独的海怪 痛苦之王 开始厌倦 深海的光 停滞的海浪
|
||||
<template #tooltip>
|
||||
<div style="text-align: center">
|
||||
《秦皇岛》<br />住在我心里孤独的<br />孤独的海怪 痛苦之王<br />开始厌倦
|
||||
深海的光 停滞的海浪
|
||||
</div>
|
||||
</template>
|
||||
</EllipsisText>
|
||||
</template>
|
@@ -76,6 +76,8 @@ const formOptions: VbenFormProps = {
|
||||
submitButtonOptions: {
|
||||
content: '查询',
|
||||
},
|
||||
// 是否在字段值改变时提交表单
|
||||
submitOnChange: false,
|
||||
// 按下回车时是否提交表单
|
||||
submitOnEnter: false,
|
||||
};
|
||||
@@ -108,6 +110,11 @@ const gridOptions: VxeGridProps<RowType> = {
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbarConfig: {
|
||||
// 是否显示搜索表单控制按钮
|
||||
// @ts-ignore 正式环境时有完整的类型声明
|
||||
search: true,
|
||||
},
|
||||
};
|
||||
|
||||
const [Grid] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||
|
@@ -2,42 +2,66 @@
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Routing and Menus
|
||||
# Routes and Menus
|
||||
|
||||
In the project, the framework provides a basic routing system and **automatically generates the corresponding menu structure based on the routing file**.
|
||||
::: info
|
||||
|
||||
## Route Types
|
||||
This page is translated by machine translation and may not be very accurate.
|
||||
|
||||
Routes are divided into static routes and dynamic routes. Static routes are routes that have been determined when the project starts. Dynamic routes are generally routes that are dynamically generated based on the user's permissions after the user logs in.
|
||||
:::
|
||||
|
||||
In the project, the framework provides a basic routing system and **automatically generates the corresponding menu structure based on the routing files**.
|
||||
|
||||
## Types of Routes
|
||||
|
||||
Routes are divided into core routes, static routes, and dynamic routes. Core routes are built-in routes of the framework, including root routes, login routes, 404 routes, etc.; static routes are routes that are determined when the project starts; dynamic routes are generally generated dynamically based on the user's permissions after the user logs in.
|
||||
|
||||
Both static and dynamic routes go through permission control, which can be controlled by configuring the `authority` field in the `meta` property of the route.
|
||||
|
||||
### Core Routes
|
||||
|
||||
Core routes are built-in routes of the framework, including root routes, login routes, 404 routes, etc. The configuration of core routes is in the `src/router/routes/core` directory under the application.
|
||||
|
||||
::: tip
|
||||
|
||||
Core routes are mainly used for the basic functions of the framework, so it is not recommended to put business-related routes in core routes. It is recommended to put business-related routes in static or dynamic routes.
|
||||
|
||||
:::
|
||||
|
||||
### Static Routes
|
||||
|
||||
If your page project does not require permission control, you can directly use static routes. The configuration of static routes is in the `src/router/routes/index` directory under the application. Open the commented file content:
|
||||
The configuration of static routes is in the `src/router/routes/index` directory under the application. Open the commented file content:
|
||||
|
||||
::: tip
|
||||
|
||||
Permission control is controlled by the `authority` field in the `meta` property of the route. If your page project does not require permission control, you can omit the `authority` field.
|
||||
|
||||
:::
|
||||
|
||||
```ts
|
||||
// If necessary, you can open your own comments and create folders
|
||||
// Uncomment if needed and create the folder
|
||||
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true }); // [!code --]
|
||||
const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true }); // [!code ++]
|
||||
/** Dynamic routing */
|
||||
/** Dynamic routes */
|
||||
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** External routing lists, which can be accessed without Layout, may be used for embedding in other systems */
|
||||
/** External route list, these pages can be accessed without Layout, possibly used for embedding in other systems */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles) // [!code --]
|
||||
const externalRoutes: RouteRecordRaw[] = []; // [!code --]
|
||||
const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles); // [!code ++]
|
||||
```
|
||||
|
||||
### Dynamic routing
|
||||
### Dynamic Routes
|
||||
|
||||
The configuration of dynamic routing is in the corresponding application `src/router/routes/modules` directory. All routing files are stored in this directory. The content format of each file is as follows, which is consistent with the routing configuration format of Vue Router. The following is the configuration of secondary routes and multi-level routes.
|
||||
The configuration of dynamic routes is in the `src/router/routes/modules` directory under the corresponding application. This directory contains all the route files. The content format of each file is consistent with the Vue Router route configuration format. Below is the configuration of secondary and multi-level routes.
|
||||
|
||||
## Define the route
|
||||
## Route Definition
|
||||
|
||||
Static routes and dynamic routes are configured in the same way. The configuration of the level-2 and multi-level routes is as follows:
|
||||
The configuration method of static routes and dynamic routes is the same. Below is the configuration of secondary and multi-level routes:
|
||||
|
||||
### Secondary route
|
||||
### Secondary Routes
|
||||
|
||||
::: details Example code of the secondary route
|
||||
::: details Secondary Route Example Code
|
||||
|
||||
```ts
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
@@ -81,17 +105,16 @@ export default routes;
|
||||
|
||||
:::
|
||||
|
||||
### Multilevel routing
|
||||
### Multi-level Routes
|
||||
|
||||
::: tip
|
||||
|
||||
- The parent route of multi-level routing does not need to set the 'component' attribute, only the 'children' attribute needs to be set. Unless you really need to display content under nested parent routing.
|
||||
|
||||
- If there are no special circumstances, the 'redirect' attribute of the parent route does not need to be specified and will default to the first child route.
|
||||
- The parent route of multi-level routes does not need to set the `component` property, just set the `children` property. Unless you really need to display content nested under the parent route.
|
||||
- In most cases, the `redirect` property of the parent route does not need to be specified, it will default to the first child route.
|
||||
|
||||
:::
|
||||
|
||||
::: details Multilevel Routing Example Code
|
||||
::: details Multi-level Route Example Code
|
||||
|
||||
```ts
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
@@ -112,7 +135,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: '/demos',
|
||||
redirect: '/demos/access',
|
||||
children: [
|
||||
// 嵌套菜单
|
||||
// Nested menu
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:round-menu',
|
||||
@@ -208,13 +231,13 @@ export default routes;
|
||||
|
||||
:::
|
||||
|
||||
## Add a New Page
|
||||
## Adding a New Page
|
||||
|
||||
To add a new page, you only need to add a route and the corresponding page component.
|
||||
|
||||
### Add a Route
|
||||
### Adding a Route
|
||||
|
||||
Add a route object in the corresponding routing file as follows:
|
||||
Add a route object in the corresponding route file, as follows:
|
||||
|
||||
```ts
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
@@ -251,9 +274,9 @@ const routes: RouteRecordRaw[] = [
|
||||
export default routes;
|
||||
```
|
||||
|
||||
### Add Page Component
|
||||
### Adding a Page Component
|
||||
|
||||
In `#/views/home/`, add a new `index.vue` file as follows:
|
||||
In `#/views/home/`, add a new `index.vue` file, as follows:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
@@ -265,11 +288,11 @@ In `#/views/home/`, add a new `index.vue` file as follows:
|
||||
|
||||
### Verification
|
||||
|
||||
At this point, the page has been added. Access `http://localhost:5555/home/index` to see the corresponding page.
|
||||
At this point, the page has been added. Visit `http://localhost:5555/home/index` to see the corresponding page.
|
||||
|
||||
## Route Configuration
|
||||
|
||||
The route configuration mainly resides in the `meta` attribute of the route object. Below are some commonly used configuration items:
|
||||
The route configuration items are mainly in the `meta` property of the route object. The following are common configuration items:
|
||||
|
||||
```ts {5-8}
|
||||
const routes = [
|
||||
@@ -293,22 +316,21 @@ interface RouteMeta {
|
||||
*/
|
||||
activeIcon?: string;
|
||||
/**
|
||||
* The currently active menu, used when you want to activate a parent menu instead of the existing one
|
||||
* @default false
|
||||
* The currently active menu, sometimes you don't want to activate the existing menu, use this to activate the parent menu
|
||||
*/
|
||||
activePath?: string;
|
||||
/**
|
||||
* Whether to affix the tab
|
||||
* Whether to fix the tab
|
||||
* @default false
|
||||
*/
|
||||
affixTab?: boolean;
|
||||
/**
|
||||
* The order of the affixed tab
|
||||
* The order of fixed tabs
|
||||
* @default 0
|
||||
*/
|
||||
affixTabOrder?: number;
|
||||
/**
|
||||
* Specific role identifiers required for access
|
||||
* Specific roles required to access
|
||||
* @default []
|
||||
*/
|
||||
authority?: string[];
|
||||
@@ -331,22 +353,22 @@ interface RouteMeta {
|
||||
| 'warning'
|
||||
| string;
|
||||
/**
|
||||
* Children of the current route do not show in the menu
|
||||
* The children of the current route are not displayed in the menu
|
||||
* @default false
|
||||
*/
|
||||
hideChildrenInMenu?: boolean;
|
||||
/**
|
||||
* The current route does not show in the breadcrumb
|
||||
* The current route is not displayed in the breadcrumb
|
||||
* @default false
|
||||
*/
|
||||
hideInBreadcrumb?: boolean;
|
||||
/**
|
||||
* The current route does not show in the menu
|
||||
* The current route is not displayed in the menu
|
||||
* @default false
|
||||
*/
|
||||
hideInMenu?: boolean;
|
||||
/**
|
||||
* The current route does not show in tabs
|
||||
* The current route is not displayed in the tab
|
||||
* @default false
|
||||
*/
|
||||
hideInTab?: boolean;
|
||||
@@ -359,16 +381,16 @@ interface RouteMeta {
|
||||
*/
|
||||
iframeSrc?: string;
|
||||
/**
|
||||
* Ignore access, can be accessed directly
|
||||
* Ignore permissions, can be accessed directly
|
||||
* @default false
|
||||
*/
|
||||
ignoreAccess?: boolean;
|
||||
/**
|
||||
* Enable KeepAlive caching
|
||||
* Enable KeepAlive cache
|
||||
*/
|
||||
keepAlive?: boolean;
|
||||
/**
|
||||
* External link - redirect path
|
||||
* External link - jump path
|
||||
*/
|
||||
link?: string;
|
||||
/**
|
||||
@@ -381,7 +403,7 @@ interface RouteMeta {
|
||||
*/
|
||||
maxNumOfOpenTab?: number;
|
||||
/**
|
||||
* The menu is visible, but access will be redirected to 403
|
||||
* The menu can be seen, but access will be redirected to 403
|
||||
*/
|
||||
menuVisibleWithForbidden?: boolean;
|
||||
/**
|
||||
@@ -389,9 +411,13 @@ interface RouteMeta {
|
||||
*/
|
||||
openInNewWindow?: boolean;
|
||||
/**
|
||||
* Used for route->menu sorting
|
||||
* Used for route -> menu sorting
|
||||
*/
|
||||
order?: number;
|
||||
/**
|
||||
* Parameters carried by the menu
|
||||
*/
|
||||
query?: Recordable;
|
||||
/**
|
||||
* Title name
|
||||
*/
|
||||
@@ -404,153 +430,169 @@ interface RouteMeta {
|
||||
### title
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the page title, which will be displayed in the menu and tabs. It is generally used in conjunction with internationalization.
|
||||
Used to configure the title of the page, which will be displayed in the menu and tab. Generally used with internationalization.
|
||||
|
||||
### icon
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the page icon, which will be displayed in the menu and tabs. It is generally used in conjunction with an icon library. If it is an `http` link, the image will be automatically loaded.
|
||||
Used to configure the icon of the page, which will be displayed in the menu and tab. Generally used with an icon library, if it is an `http` link, the image will be loaded automatically.
|
||||
|
||||
### activeIcon
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the active icon of the page, which will be displayed in the menu. It is generally used in conjunction with an icon library. If it is an `http` link, the image will be automatically loaded.
|
||||
Used to configure the active icon of the page, which will be displayed in the menu. Generally used with an icon library, if it is an `http` link, the image will be loaded automatically.
|
||||
|
||||
### keepAlive
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page caching is enabled. Once enabled, the page will be cached and not reloaded, only effective when tabs are enabled.
|
||||
Used to configure whether the page cache is enabled. When enabled, the page will be cached and will not reload, only effective when the tab is enabled.
|
||||
|
||||
### hideInMenu
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page is hidden in the menu. If hidden, the page will not be displayed in the menu.
|
||||
Used to configure whether the page is hidden in the menu. When hidden, the page will not be displayed in the menu.
|
||||
|
||||
### hideInTab
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page is hidden in tabs. If hidden, the page will not be displayed in tabs.
|
||||
Used to configure whether the page is hidden in the tab. When hidden, the page will not be displayed in the tab.
|
||||
|
||||
### hideInBreadcrumb
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page is hidden in the breadcrumb. If hidden, the page will not be displayed in the breadcrumb.
|
||||
Used to configure whether the page is hidden in the breadcrumb. When hidden, the page will not be displayed in the breadcrumb.
|
||||
|
||||
### hideChildrenInMenu
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the child pages of the page are hidden in the menu. If hidden, the child pages will not be displayed in the menu.
|
||||
Used to configure whether the subpages of the page are hidden in the menu. When hidden, the subpages will not be displayed in the menu.
|
||||
|
||||
### authority
|
||||
|
||||
- Type: `string[]`
|
||||
- Default value: `[]`
|
||||
- Default: `[]`
|
||||
|
||||
Used to configure the page's permissions. Only users with corresponding permissions can access the page. If not configured, no permissions are required.
|
||||
Used to configure the permissions of the page. Only users with the corresponding permissions can access the page. If not configured, no permissions are required.
|
||||
|
||||
### badge
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the page's badge, which will be displayed in the menu.
|
||||
Used to configure the badge of the page, which will be displayed in the menu.
|
||||
|
||||
### badgeType
|
||||
|
||||
- Type: `'dot' | 'normal'`
|
||||
- Default value: `'normal'`
|
||||
- Default: `'normal'`
|
||||
|
||||
Used to configure the type of the page's badge. `dot` is a small red dot, `normal` is text.
|
||||
Used to configure the badge type of the page. `dot` is a small red dot, `normal` is text.
|
||||
|
||||
### badgeVariants
|
||||
|
||||
- Type: `'default' | 'destructive' | 'primary' | 'success' | 'warning' | string`
|
||||
- Default value: `'success'`
|
||||
- Default: `'success'`
|
||||
|
||||
Used to configure the color of the page's badge.
|
||||
Used to configure the badge color of the page.
|
||||
|
||||
### activePath
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the currently active menu. Sometimes when the page is not displayed in the menu, it is used to activate the parent menu.
|
||||
Used to configure the currently active menu. Sometimes the page is not displayed in the menu, and this is used to activate the parent menu.
|
||||
|
||||
### affixTab
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page tab is pinned. Once pinned, the page cannot be closed.
|
||||
Used to configure whether the page is fixed in the tab. When fixed, the page cannot be closed.
|
||||
|
||||
### affixTabOrder
|
||||
|
||||
- Type: `number`
|
||||
- Default value: `0`
|
||||
- Default: `0`
|
||||
|
||||
Used to configure the order of the pinned page tabs, sorted in ascending order.
|
||||
Used to configure the order of fixed tabs, sorted in ascending order.
|
||||
|
||||
### iframeSrc
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the `iframe` address of the embedded page. Once set, the corresponding page will be embedded in the current page.
|
||||
Used to configure the `iframe` address of the embedded page. When set, the corresponding page will be embedded in the current page.
|
||||
|
||||
### ignoreAccess
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page ignores permissions and can be accessed directly.
|
||||
|
||||
### link
|
||||
|
||||
- Type: `string`
|
||||
- Default value: `''`
|
||||
- Default: `''`
|
||||
|
||||
Used to configure the external link jump path, which will be opened in a new window.
|
||||
Used to configure the external link jump path, which will open in a new window.
|
||||
|
||||
### maxNumOfOpenTab
|
||||
|
||||
- Type: `number`
|
||||
- Default value: `-1`
|
||||
- Default: `-1`
|
||||
|
||||
Used to configure the maximum number of open tabs. Once set, the earliest opened tab will be automatically closed when a new tab is opened (only effective when opening tabs with the same name).
|
||||
Used to configure the maximum number of open tabs. When set, the earliest opened tab will be automatically closed when opening a new tab (only effective when opening tabs with the same name).
|
||||
|
||||
### menuVisibleWithForbidden
|
||||
|
||||
- Type: `boolean`
|
||||
- Default value: `false`
|
||||
- Default: `false`
|
||||
|
||||
Used to configure whether the page can be seen in the menu, but access will be redirected to 403.
|
||||
|
||||
### openInNewWindow
|
||||
|
||||
- Type: `boolean`
|
||||
- Default: `false`
|
||||
|
||||
When set to `true`, the page will open in a new window.
|
||||
|
||||
### order
|
||||
|
||||
- Type: `number`
|
||||
- Default value: `0`
|
||||
- Default: `0`
|
||||
|
||||
Used to configure the page's order, for routing to menu sorting.
|
||||
Used to configure the sorting of the page, used for route to menu sorting.
|
||||
|
||||
_Note:_ Sorting is only effective for first-level menus. The sorting of second-level menus needs to be set in the corresponding first-level menu in code order.
|
||||
|
||||
### query
|
||||
|
||||
- Type: `Recordable`
|
||||
- Default: `{}`
|
||||
|
||||
Used to configure the menu parameters of the page, which will be passed to the page in the menu.
|
||||
|
||||
## Route Refresh
|
||||
|
||||
The way to refresh the route is as follows:
|
||||
The route refresh method is as follows:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
|
@@ -217,6 +217,7 @@ const defaultPreferences: Preferences = {
|
||||
globalSearch: true,
|
||||
},
|
||||
sidebar: {
|
||||
autoActivateChild: false,
|
||||
collapsed: false,
|
||||
collapsedShowTitle: false,
|
||||
enable: true,
|
||||
|
@@ -18,7 +18,7 @@
|
||||
- **Permission Validation**: Comprehensive permission validation solutions, including button-level permission control.
|
||||
- **Multi-Theme**: Built-in multiple theme configurations & dark mode to meet personalized needs.
|
||||
- **Dynamic Menu**: Supports dynamic menus that can display based on permissions.
|
||||
- **Mock Data**: High-performance local Mock data solution based on Nitro.
|
||||
- **Mock Data**: High-performance local Mock data solution based on `Nitro`.
|
||||
- **Rich Components**: Provides a wide range of components to meet most business needs.
|
||||
- **Standardization**: Code quality is ensured with tools like `ESLint`, `Prettier`, `Stylelint`, `Publint`, and `CSpell`.
|
||||
- **Engineering**: Development efficiency is improved with tools like `Pnpm Monorepo`, `TurboRepo`, and `Changeset`.
|
||||
@@ -26,9 +26,9 @@
|
||||
|
||||
## Browser Support
|
||||
|
||||
**Local development** is recommended using the **latest version of Chrome**. **Versions below Chrome 80 are not supported**.
|
||||
- **Local development** is recommended using the **latest version of Chrome**. **Versions below Chrome 80 are not supported**.
|
||||
|
||||
**Production environment** supports modern browsers, IE is not supported.
|
||||
- **Production environment** supports modern browsers, IE is not supported.
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png" alt="IE" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Safari |
|
||||
| :-: | :-: | :-: | :-: | :-: |
|
||||
@@ -37,12 +37,10 @@
|
||||
## Contribution
|
||||
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) is still being actively updated. Contributions are welcome to help maintain and improve the project, aiming to create a better mid- to backend solution.
|
||||
- If you wish to join us, you can provide suggestions or submit pull requests. We will invite you to join based on your activity.
|
||||
- If you wish to join us, you can start by contributing in the following ways, and we will invite you to join based on your activity.
|
||||
|
||||
::: info Join Us
|
||||
|
||||
If you wish to join us, you can start by contributing in the following ways, and we will invite you to join based on your activity:
|
||||
|
||||
- Regularly submit `PRs`.
|
||||
- Provide valuable suggestions.
|
||||
- Participate in discussions and help resolve some `issues`.
|
||||
|
@@ -8,11 +8,29 @@ outline: deep
|
||||
|
||||
## 路由类型
|
||||
|
||||
路由分为静态路由和动态路由,静态路由是在项目启动时就已经确定的路由。动态路由一般是在用户登录后,根据用户的权限动态生成的路由。
|
||||
路由分为核心路由、静态路由和动态路由,核心路由是框架内置的路由,包含了根路由、登录路由、404路由等;静态路由是在项目启动时就已经确定的路由;动态路由一般是在用户登录后,根据用户的权限动态生成的路由。
|
||||
|
||||
静态路由和动态路由都会走权限控制,可以通过配置路由的 `meta` 属性中的 `authority` 字段来控制权限,可以参考[路由权限控制](https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/router/routes/modules/demos.ts)。
|
||||
|
||||
### 核心路由
|
||||
|
||||
核心路由是框架内置的路由,包含了根路由、登录路由、404路由等,核心路由的配置在应用下 `src/router/routes/core` 目录下
|
||||
|
||||
::: tip
|
||||
|
||||
核心路由主要用于框架的基础功能,因此不建议将业务相关的路由放在核心路由中,推荐将业务相关的路由放在静态路由或动态路由中。
|
||||
|
||||
:::
|
||||
|
||||
### 静态路由
|
||||
|
||||
如果你的页面项目不需要权限控制,可以直接使用静态路由,静态路由的配置在应用下 `src/router/routes/index` 目录下,打开注释的文件内容:
|
||||
静态路由的配置在应用下 `src/router/routes/index` 目录下,打开注释的文件内容:
|
||||
|
||||
::: tip
|
||||
|
||||
权限控制是通过路由的 `meta` 属性中的 `authority` 字段来控制的,如果你的页面项目不需要权限控制,可以不设置 `authority` 字段。
|
||||
|
||||
:::
|
||||
|
||||
```ts
|
||||
// 有需要可以自行打开注释,并创建文件夹
|
||||
|
@@ -240,6 +240,7 @@ const defaultPreferences: Preferences = {
|
||||
globalSearch: true,
|
||||
},
|
||||
sidebar: {
|
||||
autoActivateChild: false,
|
||||
collapsed: false,
|
||||
collapsedShowTitle: false,
|
||||
enable: true,
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
## 新增组件库应用
|
||||
|
||||
如果你想用其他别的组件库,你只需要按一下步骤进行操作:
|
||||
如果你想用其他别的组件库,你只需要按以下步骤进行操作:
|
||||
|
||||
1. 在`apps`内创建一个新的文件夹,例如`apps/web-xxx`。
|
||||
2. 更改`apps/web-xxx/package.json`的`name`字段为`web-xxx`。
|
||||
|
@@ -18,7 +18,7 @@
|
||||
- **权限验证**:完善的权限验证方案,按钮级别权限控制。
|
||||
- **多主题**:内置多种主题配置和黑暗模式,满足个性化需求。
|
||||
- **动态菜单**:支持动态菜单,可以根据权限配置显示菜单。
|
||||
- **Mock 数据**:基于 Nitro 的本地高性能 Mock 数据方案。
|
||||
- **Mock 数据**:基于 `Nitro` 的本地高性能 Mock 数据方案。
|
||||
- **组件丰富**:提供了丰富的组件,可以满足大部分的业务需求。
|
||||
- **规范**:代码规范,使用 `ESLint`、`Prettier`、`Stylelint`、`Publint`、`CSpell` 等工具保证代码质量。
|
||||
- **工程化**:使用 `Pnpm Monorepo`、`TurboRepo`、`Changeset` 等工具,提高开发效率。
|
||||
@@ -26,9 +26,9 @@
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
**本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**`Chrome 80`以下版本。
|
||||
- **本地开发**推荐使用`Chrome 最新版`浏览器,**不支持**`Chrome 80`以下版本。
|
||||
|
||||
**生产环境**支持现代浏览器,不支持 IE。
|
||||
- **生产环境**支持现代浏览器,不支持 IE。
|
||||
|
||||
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png" alt="IE" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)IE | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt=" Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)Safari |
|
||||
| :-: | :-: | :-: | :-: | :-: |
|
||||
@@ -37,13 +37,13 @@
|
||||
## 贡献
|
||||
|
||||
- [Vben Admin](https://github.com/vbenjs/vue-vben-admin) 还在持续更新中,本项目欢迎您的参与,共同维护,逐步完善,打造更好的中后台解决方案。
|
||||
- 如果你想加入我们,可以提供有价值的建议或者参与讨论,协助解决 issue,- 如果你想加入我们,可以提供有价值的建议或者参与讨论,协助解决 issue,我们会根据你的活跃度邀请你加入。。
|
||||
- 如果你有兴趣加入我们,可以通过以下方式开始,我们会根据你的活跃度邀请你加入。
|
||||
|
||||
::: info 加入我们
|
||||
|
||||
- 长期提交 `PR`。
|
||||
- 提供一些好的建议。
|
||||
- 参与讨论,帮助解决一些 `issue`。
|
||||
- 提供有价值的建议。
|
||||
- 参与讨论,帮助解决 `issue`。
|
||||
- 共同维护文档。
|
||||
|
||||
:::
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/commitlint-config",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -22,7 +22,7 @@ export async function typescript(): Promise<Linter.Config[]> {
|
||||
ecmaVersion: 'latest',
|
||||
extraFileExtensions: ['.vue'],
|
||||
jsxPragma: 'React',
|
||||
project: './tsconfig.*?.json',
|
||||
project: './tsconfig.*.json',
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
|
@@ -18,6 +18,7 @@ export async function unicorn(): Promise<Linter.Config[]> {
|
||||
'unicorn/better-regex': 'off',
|
||||
'unicorn/consistent-destructuring': 'off',
|
||||
'unicorn/consistent-function-scoping': 'off',
|
||||
'unicorn/expiring-todo-comments': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
'unicorn/no-array-for-each': 'off',
|
||||
|
@@ -4,7 +4,6 @@ import { interopDefault } from '../util';
|
||||
|
||||
export async function vue(): Promise<Linter.Config[]> {
|
||||
const [pluginVue, parserVue, parserTs] = await Promise.all([
|
||||
// @ts-expect-error missing types
|
||||
interopDefault(import('eslint-plugin-vue')),
|
||||
interopDefault(import('vue-eslint-parser')),
|
||||
// @ts-expect-error missing types
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/stylelint-config",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/node-utils",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/tailwind-config",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/tsconfig",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben/vite-config",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type { UserConfig } from 'vite';
|
||||
import type { CSSOptions, UserConfig } from 'vite';
|
||||
|
||||
import type { DefineApplicationOptions } from '../typing';
|
||||
|
||||
@@ -100,7 +100,7 @@ function defineApplicationConfig(userConfigPromise?: DefineApplicationOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
function createCssOptions(injectGlobalScss = true) {
|
||||
function createCssOptions(injectGlobalScss = true): CSSOptions {
|
||||
const root = findMonorepoRoot();
|
||||
return {
|
||||
preprocessorOptions: injectGlobalScss
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import type { ApplicationPluginOptions } from '../typing';
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { fs } from '@vben/node-utils';
|
||||
@@ -21,12 +22,11 @@ function getConfFiles() {
|
||||
const script = process.env.npm_lifecycle_script as string;
|
||||
const reg = /--mode ([\d_a-z]+)/;
|
||||
const result = reg.exec(script);
|
||||
|
||||
let mode = 'production';
|
||||
if (result) {
|
||||
const mode = result[1];
|
||||
return ['.env', `.env.${mode}`];
|
||||
mode = result[1] as string;
|
||||
}
|
||||
return ['.env', '.env.production'];
|
||||
return ['.env', '.env.local', `.env.${mode}`, `.env.${mode}.local`];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,11 +42,14 @@ async function loadEnv<T = Record<string, string>>(
|
||||
|
||||
for (const confFile of confFiles) {
|
||||
try {
|
||||
const envPath = await fs.readFile(join(process.cwd(), confFile), {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const env = dotenv.parse(envPath);
|
||||
envConfig = { ...envConfig, ...env };
|
||||
const confFilePath = join(process.cwd(), confFile);
|
||||
if (existsSync(confFilePath)) {
|
||||
const envPath = await fs.readFile(confFilePath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const env = dotenv.parse(envPath);
|
||||
envConfig = { ...envConfig, ...env };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error while parsing ${confFile}`, error);
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vben-admin-monorepo",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"private": true,
|
||||
"keywords": [
|
||||
"monorepo",
|
||||
@@ -99,7 +99,7 @@
|
||||
"node": ">=20.10.0",
|
||||
"pnpm": ">=9.12.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.12.3",
|
||||
"packageManager": "pnpm@9.15.1",
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
@@ -110,6 +110,7 @@
|
||||
"@ast-grep/napi": "catalog:",
|
||||
"@ctrl/tinycolor": "catalog:",
|
||||
"clsx": "catalog:",
|
||||
"esbuild": "0.24.0",
|
||||
"pinia": "catalog:",
|
||||
"vue": "catalog:"
|
||||
},
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/design",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@@ -28,7 +28,7 @@
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/style.css"
|
||||
"default": "./dist/design.css"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
|
@@ -58,6 +58,8 @@
|
||||
|
||||
/* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */
|
||||
--accent: 216 5% 19%;
|
||||
--accent-dark: 240 0% 22%;
|
||||
--accent-darker: 240 0% 26%;
|
||||
--accent-lighter: 216 5% 12%;
|
||||
--accent-hover: 216 5% 24%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
@@ -58,6 +58,8 @@
|
||||
|
||||
/* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */
|
||||
--accent: 240 5% 96%;
|
||||
--accent-dark: 216 14% 93%;
|
||||
--accent-darker: 216 11% 91%;
|
||||
--accent-lighter: 240 0% 98%;
|
||||
--accent-hover: 200deg 10% 90%;
|
||||
--accent-foreground: 240 6% 10%;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/icons",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -28,6 +28,7 @@ export {
|
||||
Fullscreen,
|
||||
Github,
|
||||
Grip,
|
||||
GripVertical,
|
||||
Info,
|
||||
InspectionPanel,
|
||||
Languages,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/shared",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@@ -81,19 +81,21 @@
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "catalog:",
|
||||
"@tanstack/vue-store": "catalog:",
|
||||
"@types/lodash.get": "catalog:",
|
||||
"@vue/shared": "catalog:",
|
||||
"clsx": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"defu": "catalog:",
|
||||
"lodash.clonedeep": "catalog:",
|
||||
"lodash.get": "catalog:",
|
||||
"lodash.isequal": "catalog:",
|
||||
"nprogress": "catalog:",
|
||||
"tailwind-merge": "catalog:",
|
||||
"theme-colors": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.clonedeep": "catalog:",
|
||||
"@types/lodash.get": "catalog:",
|
||||
"@types/lodash.isequal": "catalog:",
|
||||
"@types/nprogress": "catalog:"
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,9 @@ export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;
|
||||
/** layout footer 组件的高度 */
|
||||
export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;
|
||||
|
||||
/** 内容区域的组件ID */
|
||||
export const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`;
|
||||
|
||||
/**
|
||||
* @zh_CN 默认命名空间
|
||||
*/
|
||||
|
@@ -16,3 +16,11 @@ export function formatDate(time: number | string, format = 'YYYY-MM-DD') {
|
||||
export function formatDateTime(time: number | string) {
|
||||
return formatDate(time, 'YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
export function isDate(value: any): value is Date {
|
||||
return value instanceof Date;
|
||||
}
|
||||
|
||||
export function isDayjsObject(value: any): value is dayjs.Dayjs {
|
||||
return dayjs.isDayjs(value);
|
||||
}
|
||||
|
156
packages/@core/base/shared/src/utils/download.ts
Normal file
156
packages/@core/base/shared/src/utils/download.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { openWindow } from './window';
|
||||
|
||||
interface DownloadOptions<T = string> {
|
||||
fileName?: string;
|
||||
source: T;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_FILENAME = 'downloaded_file';
|
||||
|
||||
/**
|
||||
* 通过 URL 下载文件,支持跨域
|
||||
* @throws {Error} - 当下载失败时抛出错误
|
||||
*/
|
||||
export async function downloadFileFromUrl({
|
||||
fileName,
|
||||
source,
|
||||
target = '_blank',
|
||||
}: DownloadOptions): Promise<void> {
|
||||
if (!source || typeof source !== 'string') {
|
||||
throw new Error('Invalid URL.');
|
||||
}
|
||||
|
||||
const isChrome = window.navigator.userAgent.toLowerCase().includes('chrome');
|
||||
const isSafari = window.navigator.userAgent.toLowerCase().includes('safari');
|
||||
|
||||
if (/iP/.test(window.navigator.userAgent)) {
|
||||
console.error('Your browser does not support download!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isChrome || isSafari) {
|
||||
triggerDownload(source, resolveFileName(source, fileName));
|
||||
}
|
||||
if (!source.includes('?')) {
|
||||
source += '?download';
|
||||
}
|
||||
|
||||
openWindow(source, { target });
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Base64 下载文件
|
||||
*/
|
||||
export function downloadFileFromBase64({ fileName, source }: DownloadOptions) {
|
||||
if (!source || typeof source !== 'string') {
|
||||
throw new Error('Invalid Base64 data.');
|
||||
}
|
||||
|
||||
const resolvedFileName = fileName || DEFAULT_FILENAME;
|
||||
triggerDownload(source, resolvedFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过图片 URL 下载图片文件
|
||||
*/
|
||||
export async function downloadFileFromImageUrl({
|
||||
fileName,
|
||||
source,
|
||||
}: DownloadOptions) {
|
||||
const base64 = await urlToBase64(source);
|
||||
downloadFileFromBase64({ fileName, source: base64 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 Blob 下载文件
|
||||
*/
|
||||
export function downloadFileFromBlob({
|
||||
fileName = DEFAULT_FILENAME,
|
||||
source,
|
||||
}: DownloadOptions<Blob>): void {
|
||||
if (!(source instanceof Blob)) {
|
||||
throw new TypeError('Invalid Blob data.');
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(source);
|
||||
triggerDownload(url, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件,支持 Blob、字符串和其他 BlobPart 类型
|
||||
*/
|
||||
export function downloadFileFromBlobPart({
|
||||
fileName = DEFAULT_FILENAME,
|
||||
source,
|
||||
}: DownloadOptions<BlobPart>): void {
|
||||
// 如果 data 不是 Blob,则转换为 Blob
|
||||
const blob =
|
||||
source instanceof Blob
|
||||
? source
|
||||
: new Blob([source], { type: 'application/octet-stream' });
|
||||
|
||||
// 创建对象 URL 并触发下载
|
||||
const url = URL.createObjectURL(blob);
|
||||
triggerDownload(url, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* img url to base64
|
||||
* @param url
|
||||
*/
|
||||
export function urlToBase64(url: string, mineType?: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
const img = new Image();
|
||||
img.crossOrigin = '';
|
||||
img.addEventListener('load', () => {
|
||||
if (!canvas || !ctx) {
|
||||
return reject(new Error('Failed to create canvas.'));
|
||||
}
|
||||
canvas.height = img.height;
|
||||
canvas.width = img.width;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const dataURL = canvas.toDataURL(mineType || 'image/png');
|
||||
canvas = null;
|
||||
resolve(dataURL);
|
||||
});
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用下载触发函数
|
||||
* @param href - 文件下载的 URL
|
||||
* @param fileName - 下载文件的名称,如果未提供则自动识别
|
||||
* @param revokeDelay - 清理 URL 的延迟时间 (毫秒)
|
||||
*/
|
||||
export function triggerDownload(
|
||||
href: string,
|
||||
fileName: string | undefined,
|
||||
revokeDelay: number = 100,
|
||||
): void {
|
||||
const defaultFileName = 'downloaded_file';
|
||||
const finalFileName = fileName || defaultFileName;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = href;
|
||||
link.download = finalFileName;
|
||||
link.style.display = 'none';
|
||||
|
||||
if (link.download === undefined) {
|
||||
link.setAttribute('target', '_blank');
|
||||
}
|
||||
|
||||
document.body.append(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
|
||||
// 清理临时 URL 以释放内存
|
||||
setTimeout(() => URL.revokeObjectURL(href), revokeDelay);
|
||||
}
|
||||
|
||||
function resolveFileName(url: string, fileName?: string): string {
|
||||
return fileName || url.slice(url.lastIndexOf('/') + 1) || DEFAULT_FILENAME;
|
||||
}
|
@@ -2,6 +2,7 @@ export * from './cn';
|
||||
export * from './date';
|
||||
export * from './diff';
|
||||
export * from './dom';
|
||||
export * from './download';
|
||||
export * from './inference';
|
||||
export * from './letter';
|
||||
export * from './merge';
|
||||
@@ -14,3 +15,5 @@ export * from './update-css-variables';
|
||||
export * from './util';
|
||||
export * from './window';
|
||||
export { default as cloneDeep } from 'lodash.clonedeep';
|
||||
export { default as get } from 'lodash.get';
|
||||
export { default as isEqual } from 'lodash.isequal';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/typings",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
2
packages/@core/base/typings/src/app.d.ts
vendored
2
packages/@core/base/typings/src/app.d.ts
vendored
@@ -38,6 +38,7 @@ type BuiltinThemeType =
|
||||
type ContentCompactType = 'compact' | 'wide';
|
||||
|
||||
type LayoutHeaderModeType = 'auto' | 'auto-scroll' | 'fixed' | 'static';
|
||||
type LayoutHeaderMenuAlignType = 'center' | 'end' | 'start';
|
||||
|
||||
/**
|
||||
* 登录过期模式
|
||||
@@ -95,6 +96,7 @@ export type {
|
||||
BreadcrumbStyleType,
|
||||
BuiltinThemeType,
|
||||
ContentCompactType,
|
||||
LayoutHeaderMenuAlignType,
|
||||
LayoutHeaderModeType,
|
||||
LayoutType,
|
||||
LoginExpiredModeType,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/composables",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -46,6 +46,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
||||
"header": {
|
||||
"enable": true,
|
||||
"hidden": false,
|
||||
"menuAlign": "start",
|
||||
"mode": "fixed",
|
||||
},
|
||||
"logo": {
|
||||
@@ -65,6 +66,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
||||
"globalSearch": true,
|
||||
},
|
||||
"sidebar": {
|
||||
"autoActivateChild": false,
|
||||
"collapsed": false,
|
||||
"collapsedShowTitle": false,
|
||||
"enable": true,
|
||||
@@ -83,6 +85,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
||||
"showMaximize": true,
|
||||
"showMore": true,
|
||||
"styleType": "chrome",
|
||||
"wheelable": true,
|
||||
},
|
||||
"theme": {
|
||||
"builtinType": "default",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/preferences",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -46,6 +46,7 @@ const defaultPreferences: Preferences = {
|
||||
header: {
|
||||
enable: true,
|
||||
hidden: false,
|
||||
menuAlign: 'start',
|
||||
mode: 'fixed',
|
||||
},
|
||||
logo: {
|
||||
@@ -65,6 +66,7 @@ const defaultPreferences: Preferences = {
|
||||
globalSearch: true,
|
||||
},
|
||||
sidebar: {
|
||||
autoActivateChild: false,
|
||||
collapsed: false,
|
||||
collapsedShowTitle: false,
|
||||
enable: true,
|
||||
@@ -83,6 +85,7 @@ const defaultPreferences: Preferences = {
|
||||
showMaximize: true,
|
||||
showMore: true,
|
||||
styleType: 'chrome',
|
||||
wheelable: true,
|
||||
},
|
||||
theme: {
|
||||
builtinType: 'default',
|
||||
|
@@ -5,6 +5,7 @@ import type {
|
||||
BuiltinThemeType,
|
||||
ContentCompactType,
|
||||
DeepPartial,
|
||||
LayoutHeaderMenuAlignType,
|
||||
LayoutHeaderModeType,
|
||||
LayoutType,
|
||||
LoginExpiredModeType,
|
||||
@@ -104,6 +105,8 @@ interface HeaderPreferences {
|
||||
enable: boolean;
|
||||
/** 顶栏是否隐藏,css-隐藏 */
|
||||
hidden: boolean;
|
||||
/** 顶栏菜单位置 */
|
||||
menuAlign: LayoutHeaderMenuAlignType;
|
||||
/** header显示模式 */
|
||||
mode: LayoutHeaderModeType;
|
||||
}
|
||||
@@ -125,6 +128,8 @@ interface NavigationPreferences {
|
||||
}
|
||||
|
||||
interface SidebarPreferences {
|
||||
/** 点击目录时自动激活子菜单 */
|
||||
autoActivateChild: boolean;
|
||||
/** 侧边栏是否折叠 */
|
||||
collapsed: boolean;
|
||||
/** 侧边栏折叠时,是否显示title */
|
||||
@@ -173,6 +178,8 @@ interface TabbarPreferences {
|
||||
showMore: boolean;
|
||||
/** 标签页风格 */
|
||||
styleType: TabsStyleType;
|
||||
/** 是否开启鼠标滚轮响应 */
|
||||
wheelable: boolean;
|
||||
}
|
||||
|
||||
interface ThemePreferences {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/form-ui",
|
||||
"version": "5.2.1",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
|
@@ -91,6 +91,11 @@ function handleRangeTimeValue(values: Record<string, any>) {
|
||||
|
||||
fieldMappingTime.forEach(
|
||||
([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {
|
||||
if (startTimeKey && endTimeKey && values[field] === null) {
|
||||
delete values[startTimeKey];
|
||||
delete values[endTimeKey];
|
||||
}
|
||||
|
||||
if (!values[field]) {
|
||||
delete values[field];
|
||||
return;
|
||||
@@ -133,17 +138,37 @@ defineExpose({
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn('col-span-full w-full pb-6 text-right', rootProps.actionWrapperClass)
|
||||
cn(
|
||||
'col-span-full w-full text-right',
|
||||
rootProps.compact ? 'pb-2' : 'pb-6',
|
||||
rootProps.actionWrapperClass,
|
||||
)
|
||||
"
|
||||
:style="queryFormStyle"
|
||||
>
|
||||
<template v-if="rootProps.actionButtonsReverse">
|
||||
<!-- 提交按钮前 -->
|
||||
<slot name="submit-before"></slot>
|
||||
|
||||
<component
|
||||
:is="COMPONENT_MAP.PrimaryButton"
|
||||
v-if="submitButtonOptions.show"
|
||||
class="ml-3"
|
||||
type="button"
|
||||
@click="handleSubmit"
|
||||
v-bind="submitButtonOptions"
|
||||
>
|
||||
{{ submitButtonOptions.content }}
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<!-- 重置按钮前 -->
|
||||
<slot name="reset-before"></slot>
|
||||
|
||||
<component
|
||||
:is="COMPONENT_MAP.DefaultButton"
|
||||
v-if="resetButtonOptions.show"
|
||||
class="mr-3"
|
||||
class="ml-3"
|
||||
type="button"
|
||||
@click="handleReset"
|
||||
v-bind="resetButtonOptions"
|
||||
@@ -151,18 +176,21 @@ defineExpose({
|
||||
{{ resetButtonOptions.content }}
|
||||
</component>
|
||||
|
||||
<!-- 提交按钮前 -->
|
||||
<slot name="submit-before"></slot>
|
||||
<template v-if="!rootProps.actionButtonsReverse">
|
||||
<!-- 提交按钮前 -->
|
||||
<slot name="submit-before"></slot>
|
||||
|
||||
<component
|
||||
:is="COMPONENT_MAP.PrimaryButton"
|
||||
v-if="submitButtonOptions.show"
|
||||
type="button"
|
||||
@click="handleSubmit"
|
||||
v-bind="submitButtonOptions"
|
||||
>
|
||||
{{ submitButtonOptions.content }}
|
||||
</component>
|
||||
<component
|
||||
:is="COMPONENT_MAP.PrimaryButton"
|
||||
v-if="submitButtonOptions.show"
|
||||
class="ml-3"
|
||||
type="button"
|
||||
@click="handleSubmit"
|
||||
v-bind="submitButtonOptions"
|
||||
>
|
||||
{{ submitButtonOptions.content }}
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<!-- 展开按钮前 -->
|
||||
<slot name="expand-before"></slot>
|
||||
|
@@ -44,11 +44,15 @@ export function setupVbenForm<
|
||||
>(options: VbenFormAdapterOptions<T>) {
|
||||
const { config, defineRules } = options;
|
||||
|
||||
const { disabledOnChangeListener = false, emptyStateValue = undefined } =
|
||||
(config || {}) as FormCommonConfig;
|
||||
const {
|
||||
disabledOnChangeListener = true,
|
||||
disabledOnInputListener = true,
|
||||
emptyStateValue = undefined,
|
||||
} = (config || {}) as FormCommonConfig;
|
||||
|
||||
Object.assign(DEFAULT_FORM_COMMON_CONFIG, {
|
||||
disabledOnChangeListener,
|
||||
disabledOnInputListener,
|
||||
emptyStateValue,
|
||||
});
|
||||
|
||||
|
@@ -13,13 +13,15 @@ import { toRaw } from 'vue';
|
||||
import { Store } from '@vben-core/shared/store';
|
||||
import {
|
||||
bindMethods,
|
||||
createMerge,
|
||||
isDate,
|
||||
isDayjsObject,
|
||||
isFunction,
|
||||
isObject,
|
||||
mergeWithArrayOverride,
|
||||
StateHandler,
|
||||
} from '@vben-core/shared/utils';
|
||||
|
||||
import { objectPick } from '@vueuse/core';
|
||||
|
||||
function getDefaultState(): VbenFormProps {
|
||||
return {
|
||||
actionWrapperClass: '',
|
||||
@@ -36,6 +38,7 @@ function getDefaultState(): VbenFormProps {
|
||||
showCollapseButton: false,
|
||||
showDefaultActions: true,
|
||||
submitButtonOptions: {},
|
||||
submitOnChange: false,
|
||||
submitOnEnter: false,
|
||||
wrapperClass: 'grid-cols-1',
|
||||
};
|
||||
@@ -127,6 +130,11 @@ export class FormApi {
|
||||
return form.values;
|
||||
}
|
||||
|
||||
async isFieldValid(fieldName: string) {
|
||||
const form = await this.getForm();
|
||||
return form.isFieldValid(fieldName);
|
||||
}
|
||||
|
||||
merge(formApi: FormApi) {
|
||||
const chain = [this, formApi];
|
||||
const proxy = new Proxy(formApi, {
|
||||
@@ -185,7 +193,7 @@ export class FormApi {
|
||||
const fieldSet = new Set(fields);
|
||||
const schema = this.state?.schema ?? [];
|
||||
|
||||
const filterSchema = schema.filter((item) => fieldSet.has(item.fieldName));
|
||||
const filterSchema = schema.filter((item) => !fieldSet.has(item.fieldName));
|
||||
|
||||
this.setState({
|
||||
schema: filterSchema,
|
||||
@@ -250,8 +258,26 @@ export class FormApi {
|
||||
form.setValues(fields, shouldValidate);
|
||||
return;
|
||||
}
|
||||
const fieldNames = this.state?.schema?.map((item) => item.fieldName) ?? [];
|
||||
const filteredFields = objectPick(fields, fieldNames);
|
||||
|
||||
/**
|
||||
* 合并算法有待改进,目前的算法不支持object类型的值。
|
||||
* antd的日期时间相关组件的值类型为dayjs对象
|
||||
* element-plus的日期时间相关组件的值类型可能为Date对象
|
||||
* 以上两种类型需要排除深度合并
|
||||
*/
|
||||
const fieldMergeFn = createMerge((obj, key, value) => {
|
||||
if (key in obj) {
|
||||
obj[key] =
|
||||
!Array.isArray(obj[key]) &&
|
||||
isObject(obj[key]) &&
|
||||
!isDayjsObject(obj[key]) &&
|
||||
!isDate(obj[key])
|
||||
? fieldMergeFn(obj[key], value)
|
||||
: value;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const filteredFields = fieldMergeFn(fields, form.values);
|
||||
form.setValues(filteredFields, shouldValidate);
|
||||
}
|
||||
|
||||
@@ -327,4 +353,14 @@ export class FormApi {
|
||||
}
|
||||
return await this.submitForm();
|
||||
}
|
||||
|
||||
async validateField(fieldName: string, opts?: Partial<ValidationOptions>) {
|
||||
const form = await this.getForm();
|
||||
const validateResult = await form.validateField(fieldName, opts);
|
||||
|
||||
if (Object.keys(validateResult?.errors ?? {}).length > 0) {
|
||||
console.error('validate error', validateResult?.errors);
|
||||
}
|
||||
return validateResult;
|
||||
}
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ import { isEventObjectLike } from './helper';
|
||||
interface Props extends FormSchema {}
|
||||
|
||||
const {
|
||||
colon,
|
||||
commonComponentProps,
|
||||
component,
|
||||
componentProps,
|
||||
@@ -33,6 +34,7 @@ const {
|
||||
description,
|
||||
disabled,
|
||||
disabledOnChangeListener,
|
||||
disabledOnInputListener,
|
||||
emptyStateValue,
|
||||
fieldName,
|
||||
formFieldProps,
|
||||
@@ -53,7 +55,7 @@ const values = useFormValues();
|
||||
const errors = useFieldError(fieldName);
|
||||
const fieldComponentRef = useTemplateRef<HTMLInputElement>('fieldComponentRef');
|
||||
const formApi = formRenderProps.form;
|
||||
|
||||
const compact = formRenderProps.compact;
|
||||
const isInValid = computed(() => errors.value?.length > 0);
|
||||
|
||||
const FieldComponent = computed(() => {
|
||||
@@ -209,8 +211,9 @@ function fieldBindEvent(slotProps: Record<string, any>) {
|
||||
if (modelValue && isObject(modelValue) && bindEventField) {
|
||||
value = isEventObjectLike(modelValue)
|
||||
? modelValue?.target?.[bindEventField]
|
||||
: modelValue;
|
||||
: (modelValue?.[bindEventField] ?? modelValue);
|
||||
}
|
||||
|
||||
if (bindEventField) {
|
||||
return {
|
||||
[`onUpdate:${bindEventField}`]: handler,
|
||||
@@ -223,12 +226,16 @@ function fieldBindEvent(slotProps: Record<string, any>) {
|
||||
if (!shouldUnwrap) {
|
||||
return onChange?.(e);
|
||||
}
|
||||
|
||||
return onChange?.(e?.target?.[bindEventField] ?? e);
|
||||
},
|
||||
onInput: () => {},
|
||||
...(disabledOnInputListener ? { onInput: undefined } : {}),
|
||||
};
|
||||
}
|
||||
return {};
|
||||
return {
|
||||
...(disabledOnInputListener ? { onInput: undefined } : {}),
|
||||
...(disabledOnChangeListener ? { onChange: undefined } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function createComponentProps(slotProps: Record<string, any>) {
|
||||
@@ -238,6 +245,12 @@ function createComponentProps(slotProps: Record<string, any>) {
|
||||
...slotProps.componentField,
|
||||
...computedProps.value,
|
||||
...bindEvents,
|
||||
...(Reflect.has(computedProps.value, 'onChange')
|
||||
? { onChange: computedProps.value.onChange }
|
||||
: {}),
|
||||
...(Reflect.has(computedProps.value, 'onInput')
|
||||
? { onInput: computedProps.value.onInput }
|
||||
: {}),
|
||||
};
|
||||
|
||||
return binds;
|
||||
@@ -268,8 +281,10 @@ function autofocus() {
|
||||
'form-valid-error': isInValid,
|
||||
'flex-col': isVertical,
|
||||
'flex-row items-center': !isVertical,
|
||||
'pb-6': !compact,
|
||||
'pb-2': compact,
|
||||
}"
|
||||
class="flex pb-6"
|
||||
class="flex"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<FormLabel
|
||||
@@ -288,7 +303,10 @@ function autofocus() {
|
||||
:required="shouldRequired && !hideRequiredMark"
|
||||
:style="labelStyle"
|
||||
>
|
||||
{{ label }}
|
||||
<template v-if="label">
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="colon" class="ml-[2px]">:</span>
|
||||
</template>
|
||||
</FormLabel>
|
||||
<div :class="cn('relative flex w-full items-center', wrapperClass)">
|
||||
<FormControl :class="cn(controlClass)">
|
||||
|
@@ -86,10 +86,12 @@ const computedSchema = computed(
|
||||
formFieldProps: Record<string, any>;
|
||||
} & Omit<FormSchema, 'formFieldProps'>)[] => {
|
||||
const {
|
||||
colon = false,
|
||||
componentProps = {},
|
||||
controlClass = '',
|
||||
disabled,
|
||||
disabledOnChangeListener = false,
|
||||
disabledOnChangeListener = true,
|
||||
disabledOnInputListener = true,
|
||||
emptyStateValue = undefined,
|
||||
formFieldProps = {},
|
||||
formItemClass = '',
|
||||
@@ -109,8 +111,10 @@ const computedSchema = computed(
|
||||
: false;
|
||||
|
||||
return {
|
||||
colon,
|
||||
disabled,
|
||||
disabledOnChangeListener,
|
||||
disabledOnInputListener,
|
||||
emptyStateValue,
|
||||
hideLabel,
|
||||
hideRequiredMark,
|
||||
|
@@ -136,6 +136,10 @@ type ComponentProps =
|
||||
| MaybeComponentProps;
|
||||
|
||||
export interface FormCommonConfig {
|
||||
/**
|
||||
* 在Label后显示一个冒号
|
||||
*/
|
||||
colon?: boolean;
|
||||
/**
|
||||
* 所有表单项的props
|
||||
*/
|
||||
@@ -151,9 +155,14 @@ export interface FormCommonConfig {
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* 是否禁用所有表单项的change事件监听
|
||||
* @default false
|
||||
* @default true
|
||||
*/
|
||||
disabledOnChangeListener?: boolean;
|
||||
/**
|
||||
* 是否禁用所有表单项的input事件监听
|
||||
* @default true
|
||||
*/
|
||||
disabledOnInputListener?: boolean;
|
||||
/**
|
||||
* 所有表单项的空状态值,默认都是undefined,naive-ui的空状态值是null
|
||||
*/
|
||||
@@ -264,6 +273,10 @@ export interface FormRenderProps<
|
||||
* 表单项通用后备配置,当子项目没配置时使用这里的配置,子项目配置优先级高于此配置
|
||||
*/
|
||||
commonConfig?: FormCommonConfig;
|
||||
/**
|
||||
* 紧凑模式(移除表单每一项底部为校验信息预留的空间)
|
||||
*/
|
||||
compact?: boolean;
|
||||
/**
|
||||
* 组件v-model事件绑定
|
||||
*/
|
||||
@@ -307,6 +320,10 @@ export interface VbenFormProps<
|
||||
FormRenderProps<T>,
|
||||
'componentBindEventMap' | 'componentMap' | 'form'
|
||||
> {
|
||||
/**
|
||||
* 操作按钮是否反转(提交按钮前置)
|
||||
*/
|
||||
actionButtonsReverse?: boolean;
|
||||
/**
|
||||
* 表单操作区域class
|
||||
*/
|
||||
@@ -342,6 +359,12 @@ export interface VbenFormProps<
|
||||
*/
|
||||
submitButtonOptions?: ActionButtonOptions;
|
||||
|
||||
/**
|
||||
* 是否在字段值改变时提交表单
|
||||
* @default false
|
||||
*/
|
||||
submitOnChange?: boolean;
|
||||
|
||||
/**
|
||||
* 是否在回车时提交表单
|
||||
* @default false
|
||||
@@ -361,6 +384,7 @@ export interface VbenFormAdapterOptions<
|
||||
config?: {
|
||||
baseModelPropName?: string;
|
||||
disabledOnChangeListener?: boolean;
|
||||
disabledOnInputListener?: boolean;
|
||||
emptyStateValue?: null | undefined;
|
||||
modelPropNameMap?: Partial<Record<T, string>>;
|
||||
};
|
||||
|
@@ -6,7 +6,11 @@ import type { ExtendedFormApi, VbenFormProps } from './types';
|
||||
import { useForwardPriorityValues } from '@vben-core/composables';
|
||||
// import { isFunction } from '@vben-core/shared/utils';
|
||||
|
||||
import { useTemplateRef } from 'vue';
|
||||
import { nextTick, onMounted, watch } from 'vue';
|
||||
|
||||
import { cloneDeep } from '@vben-core/shared/utils';
|
||||
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
|
||||
import FormActions from './components/form-actions.vue';
|
||||
import {
|
||||
@@ -23,8 +27,6 @@ interface Props extends VbenFormProps {
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const formActionsRef = useTemplateRef<typeof FormActions>('formActionsRef');
|
||||
|
||||
const state = props.formApi?.useStore?.();
|
||||
|
||||
const forward = useForwardPriorityValues(props, state);
|
||||
@@ -40,6 +42,9 @@ const handleUpdateCollapsed = (value: boolean) => {
|
||||
};
|
||||
|
||||
function handleKeyDownEnter(event: KeyboardEvent) {
|
||||
if (!state.value.submitOnEnter || !forward.value.formApi?.isMounted) {
|
||||
return;
|
||||
}
|
||||
// 如果是 textarea 不阻止默认行为,否则会导致无法换行。
|
||||
// 跳过 textarea 的回车提交处理
|
||||
if (event.target instanceof HTMLTextAreaElement) {
|
||||
@@ -47,11 +52,19 @@ function handleKeyDownEnter(event: KeyboardEvent) {
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
if (!state.value.submitOnEnter || !formActionsRef.value) {
|
||||
return;
|
||||
}
|
||||
formActionsRef.value?.handleSubmit?.();
|
||||
forward.value.formApi.validateAndSubmitForm();
|
||||
}
|
||||
|
||||
const handleValuesChangeDebounced = useDebounceFn((newVal) => {
|
||||
forward.value.handleValuesChange?.(cloneDeep(newVal));
|
||||
state.value.submitOnChange && forward.value.formApi?.validateAndSubmitForm();
|
||||
}, 300);
|
||||
|
||||
onMounted(async () => {
|
||||
// 只在挂载后开始监听,form.values会有一个初始化的过程
|
||||
await nextTick();
|
||||
watch(() => form.values, handleValuesChangeDebounced, { deep: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -75,7 +88,6 @@ function handleKeyDownEnter(event: KeyboardEvent) {
|
||||
<slot v-bind="slotProps">
|
||||
<FormActions
|
||||
v-if="forward.showDefaultActions"
|
||||
ref="formActionsRef"
|
||||
:model-value="state.collapsed"
|
||||
@update:model-value="handleUpdateCollapsed"
|
||||
>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vben-core/layout-ui",
|
||||
"version": "5.4.5",
|
||||
"version": "5.5.2",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"repository": {
|
||||
@@ -40,6 +40,7 @@
|
||||
"@vben-core/composables": "workspace:*",
|
||||
"@vben-core/icons": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"vue": "catalog:"
|
||||
|
@@ -63,7 +63,7 @@ const logoStyle = computed((): CSSProperties => {
|
||||
<header
|
||||
:class="theme"
|
||||
:style="style"
|
||||
class="border-border bg-header top-0 flex w-full flex-[0_0_auto] items-center border-b transition-[margin-top] duration-200"
|
||||
class="border-border bg-header top-0 flex w-full flex-[0_0_auto] items-center border-b pl-2 transition-[margin-top] duration-200"
|
||||
>
|
||||
<div v-if="slots.logo" :style="logoStyle">
|
||||
<slot name="logo"></slot>
|
||||
|
@@ -166,7 +166,7 @@ const headerStyle = computed((): CSSProperties => {
|
||||
|
||||
return {
|
||||
...(isSidebarMixed ? { display: 'flex', justifyContent: 'center' } : {}),
|
||||
height: `${headerHeight}px`,
|
||||
height: `${headerHeight - 1}px`,
|
||||
...contentWidthStyle.value,
|
||||
};
|
||||
});
|
||||
@@ -191,7 +191,10 @@ watchEffect(() => {
|
||||
function calcMenuWidthStyle(isHiddenDom: boolean): CSSProperties {
|
||||
const { extraWidth, fixedExtra, isSidebarMixed, show, width } = props;
|
||||
|
||||
let widthValue = `${width + (isSidebarMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
|
||||
let widthValue =
|
||||
width === 0
|
||||
? '0px'
|
||||
: `${width + (isSidebarMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
|
||||
|
||||
const { collapseWidth } = props;
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user