Browse Source

Merge pull request #1055 from colinin/vben-5-5-1

upgrade(vben): 同步vben5.5.1版本
pull/1059/head
yx lin 1 year ago
committed by GitHub
parent
commit
ba3e32aa14
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      apps/vben5/apps/app-antd/package.json
  2. 48
      apps/vben5/apps/app-antd/src/adapter/component/index.ts
  3. 4
      apps/vben5/apps/app-antd/src/adapter/vxe-table.ts
  4. 14
      apps/vben5/apps/app-antd/src/router/guard.ts
  5. 29
      apps/vben5/apps/backend-mock/api/table/list.ts
  6. 3
      apps/vben5/apps/backend-mock/utils/mock-data.ts
  7. 2
      apps/vben5/apps/web-antd/package.json
  8. 45
      apps/vben5/apps/web-antd/src/adapter/component/index.ts
  9. 14
      apps/vben5/apps/web-antd/src/router/guard.ts
  10. 6
      apps/vben5/apps/web-antd/src/views/_core/authentication/code-login.vue
  11. 2
      apps/vben5/apps/web-ele/package.json
  12. 144
      apps/vben5/apps/web-ele/src/adapter/component/index.ts
  13. 1
      apps/vben5/apps/web-ele/src/adapter/form.ts
  14. 4
      apps/vben5/apps/web-ele/src/bootstrap.ts
  15. 1
      apps/vben5/apps/web-ele/src/locales/langs/en-US/demos.json
  16. 1
      apps/vben5/apps/web-ele/src/locales/langs/zh-CN/demos.json
  17. 14
      apps/vben5/apps/web-ele/src/router/guard.ts
  18. 8
      apps/vben5/apps/web-ele/src/router/routes/modules/demos.ts
  19. 6
      apps/vben5/apps/web-ele/src/views/_core/authentication/code-login.vue
  20. 96
      apps/vben5/apps/web-ele/src/views/demos/element/index.vue
  21. 181
      apps/vben5/apps/web-ele/src/views/demos/form/basic.vue
  22. 2
      apps/vben5/apps/web-naive/package.json
  23. 85
      apps/vben5/apps/web-naive/src/adapter/component/index.ts
  24. 2
      apps/vben5/apps/web-naive/src/adapter/form.ts
  25. 1
      apps/vben5/apps/web-naive/src/locales/langs/en-US/demos.json
  26. 1
      apps/vben5/apps/web-naive/src/locales/langs/zh-CN/demos.json
  27. 14
      apps/vben5/apps/web-naive/src/router/guard.ts
  28. 8
      apps/vben5/apps/web-naive/src/router/routes/modules/demos.ts
  29. 6
      apps/vben5/apps/web-naive/src/views/_core/authentication/code-login.vue
  30. 143
      apps/vben5/apps/web-naive/src/views/demos/form/basic.vue
  31. 18
      apps/vben5/docs/.vitepress/config/zh.mts
  32. 2
      apps/vben5/docs/package.json
  33. 2
      apps/vben5/docs/src/_env/adapter/form.ts
  34. 152
      apps/vben5/docs/src/components/common-ui/vben-api-component.md
  35. 8
      apps/vben5/docs/src/components/common-ui/vben-drawer.md
  36. 56
      apps/vben5/docs/src/components/common-ui/vben-ellipsis-text.md
  37. 43
      apps/vben5/docs/src/components/common-ui/vben-form.md
  38. 8
      apps/vben5/docs/src/components/common-ui/vben-modal.md
  39. 20
      apps/vben5/docs/src/components/common-ui/vben-vxe-table.md
  40. 4
      apps/vben5/docs/src/components/introduction.md
  41. 44
      apps/vben5/docs/src/components/layout-ui/page.md
  42. 100
      apps/vben5/docs/src/demos/vben-api-component/cascader/index.vue
  43. 10
      apps/vben5/docs/src/demos/vben-ellipsis-text/expand/index.vue
  44. 10
      apps/vben5/docs/src/demos/vben-ellipsis-text/line/index.vue
  45. 14
      apps/vben5/docs/src/demos/vben-ellipsis-text/tooltip/index.vue
  46. 7
      apps/vben5/docs/src/demos/vben-vxe-table/form/index.vue
  47. 1
      apps/vben5/docs/src/en/guide/essentials/settings.md
  48. 1
      apps/vben5/docs/src/guide/essentials/settings.md
  49. 2
      apps/vben5/docs/src/guide/in-depth/ui-framework.md
  50. 2
      apps/vben5/internal/lint-configs/commitlint-config/package.json
  51. 1
      apps/vben5/internal/lint-configs/eslint-config/src/configs/vue.ts
  52. 2
      apps/vben5/internal/lint-configs/stylelint-config/package.json
  53. 2
      apps/vben5/internal/node-utils/package.json
  54. 2
      apps/vben5/internal/tailwind-config/package.json
  55. 2
      apps/vben5/internal/tsconfig/package.json
  56. 2
      apps/vben5/internal/vite-config/package.json
  57. 4
      apps/vben5/internal/vite-config/src/config/application.ts
  58. 21
      apps/vben5/internal/vite-config/src/utils/env.ts
  59. 5
      apps/vben5/package.json
  60. 2
      apps/vben5/packages/@abp/account/package.json
  61. 4
      apps/vben5/packages/@abp/core/package.json
  62. 2
      apps/vben5/packages/@abp/core/src/hooks/useLocalization.ts
  63. 2
      apps/vben5/packages/@abp/identity/package.json
  64. 2
      apps/vben5/packages/@abp/request/package.json
  65. 48
      apps/vben5/packages/@abp/ui/src/adapter/component/index.ts
  66. 2
      apps/vben5/packages/@core/base/design/package.json
  67. 2
      apps/vben5/packages/@core/base/icons/package.json
  68. 1
      apps/vben5/packages/@core/base/icons/src/lucide.ts
  69. 6
      apps/vben5/packages/@core/base/shared/package.json
  70. 3
      apps/vben5/packages/@core/base/shared/src/constants/globals.ts
  71. 8
      apps/vben5/packages/@core/base/shared/src/utils/date.ts
  72. 2
      apps/vben5/packages/@core/base/shared/src/utils/index.ts
  73. 2
      apps/vben5/packages/@core/base/typings/package.json
  74. 2
      apps/vben5/packages/@core/composables/package.json
  75. 2
      apps/vben5/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap
  76. 2
      apps/vben5/packages/@core/preferences/package.json
  77. 2
      apps/vben5/packages/@core/preferences/src/config.ts
  78. 4
      apps/vben5/packages/@core/preferences/src/types.ts
  79. 2
      apps/vben5/packages/@core/ui-kit/form-ui/package.json
  80. 51
      apps/vben5/packages/@core/ui-kit/form-ui/src/components/form-actions.vue
  81. 8
      apps/vben5/packages/@core/ui-kit/form-ui/src/config.ts
  82. 29
      apps/vben5/packages/@core/ui-kit/form-ui/src/form-api.ts
  83. 20
      apps/vben5/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
  84. 6
      apps/vben5/packages/@core/ui-kit/form-ui/src/form-render/form.vue
  85. 26
      apps/vben5/packages/@core/ui-kit/form-ui/src/types.ts
  86. 28
      apps/vben5/packages/@core/ui-kit/form-ui/src/vben-use-form.vue
  87. 3
      apps/vben5/packages/@core/ui-kit/layout-ui/package.json
  88. 2
      apps/vben5/packages/@core/ui-kit/layout-ui/src/components/layout-header.vue
  89. 2
      apps/vben5/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue
  90. 6
      apps/vben5/packages/@core/ui-kit/layout-ui/src/vben-layout.vue
  91. 2
      apps/vben5/packages/@core/ui-kit/menu-ui/package.json
  92. 1
      apps/vben5/packages/@core/ui-kit/menu-ui/src/index.ts
  93. 13
      apps/vben5/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts
  94. 13
      apps/vben5/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue
  95. 11
      apps/vben5/packages/@core/ui-kit/popup-ui/src/modal/modal.ts
  96. 11
      apps/vben5/packages/@core/ui-kit/popup-ui/src/modal/modal.vue
  97. 2
      apps/vben5/packages/@core/ui-kit/shadcn-ui/package.json
  98. 4
      apps/vben5/packages/@core/ui-kit/shadcn-ui/src/components/pin-input/input.vue
  99. 4
      apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/button/button.ts
  100. 27
      apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue

2
apps/vben5/apps/app-antd/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/app-antd",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

48
apps/vben5/apps/app-antd/src/adapter/component/index.ts

@ -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 {
@ -21,7 +21,6 @@ import {
Input,
InputNumber,
InputPassword,
InputSearch,
Mentions,
notification,
Radio,
@ -49,16 +48,18 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'InputSearch'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
@ -79,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,
@ -89,10 +121,16 @@ 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'),
InputSearch,
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {

4
apps/vben5/apps/app-antd/src/adapter/vxe-table.ts

@ -20,6 +20,10 @@ setupVbenVxeTable({
// 全局禁用vxe-table的表单配置,使用formOptions
enabled: false,
},
pagerConfig: {
pageSize: 10,
pageSizes: [10, 15, 25, 50, 100],
},
proxyConfig: {
autoLoad: true,
response: {

14
apps/vben5/apps/app-antd/src/router/guard.ts

@ -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)),

29
apps/vben5/apps/backend-mock/api/table/list.ts

@ -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);
});

3
apps/vben5/apps/backend-mock/utils/mock-data.ts

@ -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',
},
];

2
apps/vben5/apps/web-antd/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/web-antd",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

45
apps/vben5/apps/web-antd/src/adapter/component/index.ts

@ -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'),

14
apps/vben5/apps/web-antd/src/router/guard.ts

@ -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)),

6
apps/vben5/apps/web-antd/src/views/_core/authentication/code-login.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]),
}),
},
];
});

2
apps/vben5/apps/web-ele/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/web-ele",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

144
apps/vben5/apps/web-ele/src/adapter/component/index.ts

@ -4,24 +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,
@ -41,10 +45,13 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'RadioGroup'
@ -61,9 +68,57 @@ 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 },
);
},
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
@ -73,14 +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,
DatePicker: ElDatePicker,
TimePicker: (props, { attrs, slots }) => {
const { name, id, isRange } = props;
const extraProps: Recordable<any> = {};
if (isRange) {
if (name && !Array.isArray(name)) {
extraProps.name = [name, `${name}_end`];
}
if (id && !Array.isArray(id)) {
extraProps.id = [id, `${id}_end`];
}
}
return h(
ElTimePicker,
{
...props,
...attrs,
...extraProps,
},
slots,
);
},
DatePicker: (props, { attrs, slots }) => {
const { name, id, type } = props;
const extraProps: Recordable<any> = {};
if (type && type.includes('range')) {
if (name && !Array.isArray(name)) {
extraProps.name = [name, `${name}_end`];
}
if (id && !Array.isArray(id)) {
extraProps.id = [id, `${id}_end`];
}
}
return h(
ElDatePicker,
{
...props,
...attrs,
...extraProps,
},
slots,
);
},
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
Upload: ElUpload,
};

1
apps/vben5/apps/web-ele/src/adapter/form.ts

@ -12,6 +12,7 @@ setupVbenForm<ComponentType>({
config: {
modelPropNameMap: {
Upload: 'fileList',
CheckboxGroup: 'model-value',
},
},
defineRules: {

4
apps/vben5/apps/web-ele/src/bootstrap.ts

@ -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
apps/vben5/apps/web-ele/src/locales/langs/en-US/demos.json

@ -1,6 +1,7 @@
{
"title": "Demos",
"elementPlus": "Element Plus",
"form": "Form",
"vben": {
"title": "Project",
"about": "About",

1
apps/vben5/apps/web-ele/src/locales/langs/zh-CN/demos.json

@ -1,6 +1,7 @@
{
"title": "演示",
"elementPlus": "Element Plus",
"form": "表单演示",
"vben": {
"title": "项目",
"about": "关于",

14
apps/vben5/apps/web-ele/src/router/guard.ts

@ -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)),

8
apps/vben5/apps/web-ele/src/router/routes/modules/demos.ts

@ -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'),
},
],
},
];

6
apps/vben5/apps/web-ele/src/views/_core/authentication/code-login.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]),
}),
},
];
});

96
apps/vben5/apps/web-ele/src/views/demos/element/index.vue

@ -61,49 +61,57 @@ const segmentedOptions = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
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">
<template #header> Segmented </template>
<ElSegmented
v-model="segmentedValue"
:options="segmentedOptions"
size="large"
/>
</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/vben5/apps/web-ele/src/views/demos/form/basic.vue

@ -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',
// 321
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>

2
apps/vben5/apps/web-naive/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/web-naive",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

85
apps/vben5/apps/web-naive/src/adapter/component/index.ts

@ -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,

2
apps/vben5/apps/web-naive/src/adapter/form.ts

@ -10,8 +10,6 @@ import { $t } from '@vben/locales';
setupVbenForm<ComponentType>({
config: {
// naive-ui组件不接受onChang事件,所以需要禁用
disabledOnChangeListener: true,
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
emptyStateValue: null,
baseModelPropName: 'value',

1
apps/vben5/apps/web-naive/src/locales/langs/en-US/demos.json

@ -2,6 +2,7 @@
"title": "Demos",
"naive": "Naive UI",
"table": "Table",
"form": "Form",
"vben": {
"title": "Project",
"about": "About",

1
apps/vben5/apps/web-naive/src/locales/langs/zh-CN/demos.json

@ -2,6 +2,7 @@
"title": "演示",
"naive": "Naive UI",
"table": "Table",
"form": "表单",
"vben": {
"title": "项目",
"about": "关于",

14
apps/vben5/apps/web-naive/src/router/guard.ts

@ -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)),

8
apps/vben5/apps/web-naive/src/router/routes/modules/demos.ts

@ -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'),
},
],
},
];

6
apps/vben5/apps/web-naive/src/views/_core/authentication/code-login.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/vben5/apps/web-naive/src/views/demos/form/basic.vue

@ -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',
// 321
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>

18
apps/vben5/docs/.vitepress/config/zh.mts

@ -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 省略文本',
},
],
},
];

2
apps/vben5/docs/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/docs",
"version": "5.4.8",
"version": "5.5.1",
"private": true,
"scripts": {
"build": "vitepress build",

2
apps/vben5/docs/src/_env/adapter/form.ts

@ -14,8 +14,6 @@ initComponentAdapter();
setupVbenForm<ComponentType>({
config: {
baseModelPropName: 'value',
// naive-ui组件不接受onChang事件,所以需要禁用
disabledOnChangeListener: true,
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
emptyStateValue: null,
modelPropNameMap: {

152
apps/vben5/docs/src/components/common-ui/vben-api-component.md

@ -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` | - |
```
```

8
apps/vben5/docs/src/components/common-ui/vben-drawer.md

@ -74,6 +74,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
| title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - |
@ -95,6 +96,13 @@ const [Drawer, drawerApi] = useVbenDrawer({
| 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
apps/vben5/docs/src/components/common-ui/vben-ellipsis-text.md

@ -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 | 启用文本提示时,用来定制提示内容 |

43
apps/vben5/docs/src/components/common-ui/vben-form.md

@ -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,7 +431,7 @@ export interface FormSchema<
dependencies?: FormItemDependencies;
/** 描述 */
description?: string;
/** 字段名 */
/** 字段名,也作为自定义插槽的名称 */
fieldName: string;
/** 帮助信息 */
help?: string;
@ -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: "请输入" })

8
apps/vben5/docs/src/components/common-ui/vben-modal.md

@ -80,6 +80,7 @@ const [Modal, modalApi] = useVbenModal({
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` |
| title | 标题 | `string\|slot` | - |
| titleTooltip | 标题提示信息 | `string\|slot` | - |
| description | 描述信息 | `string\|slot` | - |
@ -106,6 +107,13 @@ const [Modal, modalApi] = useVbenModal({
| 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

20
apps/vben5/docs/src/components/common-ui/vben-vxe-table.md

@ -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` |

4
apps/vben5/docs/src/components/introduction.md

@ -6,6 +6,10 @@
:::
## 布局组件
布局组件一般在页面内容区域用作顶层容器组件,提供一些统一的布局样式和基本功能。
## 通用组件
通用组件是一些常用的组件,比如弹窗、抽屉、表单等。大部分基于 `Tailwind CSS` 实现,可适用于不同 UI 组件库的应用。

44
apps/vben5/docs/src/components/layout-ui/page.md

@ -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
apps/vben5/docs/src/demos/vben-api-component/cascader/index.vue

@ -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
apps/vben5/docs/src/demos/vben-ellipsis-text/expand/index.vue

@ -0,0 +1,10 @@
<script lang="ts" setup>
import { EllipsisText } from '@vben/common-ui';
const text = `
Vben Admin 是一个基于 Vue3.0Vite TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案包括二次封装组件utilshooks动态菜单权限校验多主题配置按钮级别权限控制等功能项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型也可以作为一个示例用于学习 vue3vitets 等主流技术该项目会持续跟进最新技术并将其应用在项目中Vben Admin 是一个基于 Vue3.0Vite TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案包括二次封装组件utilshooks动态菜单权限校验多主题配置按钮级别权限控制等功能项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型也可以作为一个示例用于学习 vue3vitets 等主流技术该项目会持续跟进最新技术并将其应用在项目中Vben Admin 是一个基于 Vue3.0Vite TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案包括二次封装组件utilshooks动态菜单权限校验多主题配置按钮级别权限控制等功能项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型也可以作为一个示例用于学习 vue3vitets 等主流技术该项目会持续跟进最新技术并将其应用在项目中Vben Admin 是一个基于 Vue3.0Vite TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案包括二次封装组件utilshooks动态菜单权限校验多主题配置按钮级别权限控制等功能项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型也可以作为一个示例用于学习 vue3vitets 等主流技术该项目会持续跟进最新技术并将其应用在项目中
`;
</script>
<template>
<EllipsisText :line="3" expand>{{ text }}</EllipsisText>
</template>

10
apps/vben5/docs/src/demos/vben-ellipsis-text/line/index.vue

@ -0,0 +1,10 @@
<script lang="ts" setup>
import { EllipsisText } from '@vben/common-ui';
const text = `
Vben Admin 是一个基于 Vue3.0Vite TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案包括二次封装组件utilshooks动态菜单权限校验多主题配置按钮级别权限控制等功能项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型也可以作为一个示例用于学习 vue3vitets 等主流技术该项目会持续跟进最新技术并将其应用在项目中Vben Admin 是一个基于 Vue3.0Vite TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案包括二次封装组件utilshooks动态菜单权限校验多主题配置按钮级别权限控制等功能项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型也可以作为一个示例用于学习 vue3vitets 等主流技术该项目会持续跟进最新技术并将其应用在项目中Vben Admin 是一个基于 Vue3.0Vite TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案包括二次封装组件utilshooks动态菜单权限校验多主题配置按钮级别权限控制等功能项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型也可以作为一个示例用于学习 vue3vitets 等主流技术该项目会持续跟进最新技术并将其应用在项目中Vben Admin 是一个基于 Vue3.0Vite TypeScript 的后台解决方案目标是为开发中大型项目提供开箱即用的解决方案包括二次封装组件utilshooks动态菜单权限校验多主题配置按钮级别权限控制等功能项目会使用前端较新的技术栈可以作为项目的启动模版以帮助你快速搭建企业级中后台产品原型也可以作为一个示例用于学习 vue3vitets 等主流技术该项目会持续跟进最新技术并将其应用在项目中
`;
</script>
<template>
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
</template>

14
apps/vben5/docs/src/demos/vben-ellipsis-text/tooltip/index.vue

@ -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>

7
apps/vben5/docs/src/demos/vben-vxe-table/form/index.vue

@ -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 });

1
apps/vben5/docs/src/en/guide/essentials/settings.md

@ -217,6 +217,7 @@ const defaultPreferences: Preferences = {
globalSearch: true,
},
sidebar: {
autoActivateChild: false,
collapsed: false,
collapsedShowTitle: false,
enable: true,

1
apps/vben5/docs/src/guide/essentials/settings.md

@ -240,6 +240,7 @@ const defaultPreferences: Preferences = {
globalSearch: true,
},
sidebar: {
autoActivateChild: false,
collapsed: false,
collapsedShowTitle: false,
enable: true,

2
apps/vben5/docs/src/guide/in-depth/ui-framework.md

@ -4,7 +4,7 @@
## 新增组件库应用
如果你想用其他别的组件库,你只需要按下步骤进行操作:
如果你想用其他别的组件库,你只需要按下步骤进行操作:
1. 在`apps`内创建一个新的文件夹,例如`apps/web-xxx`。
2. 更改`apps/web-xxx/package.json`的`name`字段为`web-xxx`。

2
apps/vben5/internal/lint-configs/commitlint-config/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/commitlint-config",
"version": "5.4.8",
"version": "5.5.1",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

1
apps/vben5/internal/lint-configs/eslint-config/src/configs/vue.ts

@ -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

2
apps/vben5/internal/lint-configs/stylelint-config/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/stylelint-config",
"version": "5.4.8",
"version": "5.5.1",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

2
apps/vben5/internal/node-utils/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/node-utils",
"version": "5.4.8",
"version": "5.5.1",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

2
apps/vben5/internal/tailwind-config/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/tailwind-config",
"version": "5.4.8",
"version": "5.5.1",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

2
apps/vben5/internal/tsconfig/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/tsconfig",
"version": "5.4.8",
"version": "5.5.1",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

2
apps/vben5/internal/vite-config/package.json

@ -1,6 +1,6 @@
{
"name": "@vben/vite-config",
"version": "5.4.8",
"version": "5.5.1",
"private": true,
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",

4
apps/vben5/internal/vite-config/src/config/application.ts

@ -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

21
apps/vben5/internal/vite-config/src/utils/env.ts

@ -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);
}

5
apps/vben5/package.json

@ -1,6 +1,6 @@
{
"name": "vben-admin-monorepo",
"version": "5.4.8",
"version": "5.5.1",
"private": true,
"keywords": [
"monorepo",
@ -101,7 +101,7 @@
"node": ">=20.10.0",
"pnpm": ">=9.12.0"
},
"packageManager": "pnpm@9.14.4",
"packageManager": "pnpm@9.15.0",
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
@ -112,6 +112,7 @@
"@ast-grep/napi": "catalog:",
"@ctrl/tinycolor": "catalog:",
"clsx": "catalog:",
"esbuild": "0.24.0",
"pinia": "catalog:",
"vue": "catalog:"
},

2
apps/vben5/packages/@abp/account/package.json

@ -1,6 +1,6 @@
{
"name": "@abp/account",
"version": "8.2.3",
"version": "8.3.2",
"homepage": "https://github.com/colinin/abp-next-admin",
"bugs": "https://github.com/colinin/abp-next-admin/issues",
"repository": {

4
apps/vben5/packages/@abp/core/package.json

@ -37,12 +37,12 @@
"dependencies": {
"@vueuse/core": "catalog:",
"dayjs": "catalog:",
"lodash": "catalog:",
"lodash.merge": "catalog:",
"pinia": "catalog:",
"pinia-plugin-persistedstate": "catalog:",
"vue": "catalog:"
},
"devDependencies": {
"@types/lodash": "catalog:"
"@types/lodash.merge": "catalog:"
}
}

2
apps/vben5/packages/@abp/core/src/hooks/useLocalization.ts

@ -2,7 +2,7 @@ import type { Dictionary, StringLocalizer } from '../types';
import { computed } from 'vue';
import { merge } from 'lodash';
import merge from 'lodash.merge';
import { useAbpStore } from '../store/abp';
import { format } from '../utils/string';

2
apps/vben5/packages/@abp/identity/package.json

@ -1,6 +1,6 @@
{
"name": "@abp/identity",
"version": "8.2.3",
"version": "8.3.2",
"homepage": "https://github.com/colinin/abp-next-admin",
"bugs": "https://github.com/colinin/abp-next-admin/issues",
"repository": {

2
apps/vben5/packages/@abp/request/package.json

@ -1,6 +1,6 @@
{
"name": "@abp/request",
"version": "8.2.3",
"version": "8.3.2",
"homepage": "https://github.com/colinin/abp-next-admin",
"bugs": "https://github.com/colinin/abp-next-admin/issues",
"repository": {

48
apps/vben5/packages/@abp/ui/src/adapter/component/index.ts

@ -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 {
@ -21,7 +21,6 @@ import {
Input,
InputNumber,
InputPassword,
InputSearch,
Mentions,
notification,
Radio,
@ -49,16 +48,18 @@ const withDefaultPlaceholder = <T extends Component>(
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'InputSearch'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
@ -79,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',
modelPropName: 'value',
visibleEvent: 'onDropdownVisibleChange',
},
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,
@ -89,10 +121,16 @@ 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'),
InputSearch,
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {

2
apps/vben5/packages/@core/base/design/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/design",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
apps/vben5/packages/@core/base/icons/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/icons",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

1
apps/vben5/packages/@core/base/icons/src/lucide.ts

@ -28,6 +28,7 @@ export {
Fullscreen,
Github,
Grip,
GripVertical,
Info,
InspectionPanel,
Languages,

6
apps/vben5/packages/@core/base/shared/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/shared",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
@ -86,12 +86,16 @@
"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:"
}
}

3
apps/vben5/packages/@core/base/shared/src/constants/globals.ts

@ -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
*/

8
apps/vben5/packages/@core/base/shared/src/utils/date.ts

@ -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);
}

2
apps/vben5/packages/@core/base/shared/src/utils/index.ts

@ -15,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';

2
apps/vben5/packages/@core/base/typings/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/typings",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
apps/vben5/packages/@core/composables/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/composables",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
apps/vben5/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap

@ -65,6 +65,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"globalSearch": true,
},
"sidebar": {
"autoActivateChild": false,
"collapsed": false,
"collapsedShowTitle": false,
"enable": true,
@ -83,6 +84,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"showMaximize": true,
"showMore": true,
"styleType": "chrome",
"wheelable": true,
},
"theme": {
"builtinType": "default",

2
apps/vben5/packages/@core/preferences/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/preferences",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

2
apps/vben5/packages/@core/preferences/src/config.ts

@ -65,6 +65,7 @@ const defaultPreferences: Preferences = {
globalSearch: true,
},
sidebar: {
autoActivateChild: false,
collapsed: false,
collapsedShowTitle: false,
enable: true,
@ -83,6 +84,7 @@ const defaultPreferences: Preferences = {
showMaximize: true,
showMore: true,
styleType: 'chrome',
wheelable: true,
},
theme: {
builtinType: 'default',

4
apps/vben5/packages/@core/preferences/src/types.ts

@ -125,6 +125,8 @@ interface NavigationPreferences {
}
interface SidebarPreferences {
/** 点击目录时自动激活子菜单 */
autoActivateChild: boolean;
/** 侧边栏是否折叠 */
collapsed: boolean;
/** 侧边栏折叠时,是否显示title */
@ -173,6 +175,8 @@ interface TabbarPreferences {
showMore: boolean;
/** 标签页风格 */
styleType: TabsStyleType;
/** 是否开启鼠标滚轮响应 */
wheelable: boolean;
}
interface ThemePreferences {

2
apps/vben5/packages/@core/ui-kit/form-ui/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/form-ui",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

51
apps/vben5/packages/@core/ui-kit/form-ui/src/components/form-actions.vue

@ -138,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"
@ -156,18 +176,21 @@ defineExpose({
{{ resetButtonOptions.content }}
</component>
<!-- 提交按钮前 -->
<slot name="submit-before"></slot>
<component
:is="COMPONENT_MAP.PrimaryButton"
v-if="submitButtonOptions.show"
type="button"
@click="handleSubmit"
v-bind="submitButtonOptions"
>
{{ submitButtonOptions.content }}
</component>
<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="expand-before"></slot>

8
apps/vben5/packages/@core/ui-kit/form-ui/src/config.ts

@ -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,
});

29
apps/vben5/packages/@core/ui-kit/form-ui/src/form-api.ts

@ -14,6 +14,8 @@ import { Store } from '@vben-core/shared/store';
import {
bindMethods,
createMerge,
isDate,
isDayjsObject,
isFunction,
isObject,
mergeWithArrayOverride,
@ -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, {
@ -251,10 +259,19 @@ export class FormApi {
return;
}
/**
* 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])
!Array.isArray(obj[key]) &&
isObject(obj[key]) &&
!isDayjsObject(obj[key]) &&
!isDate(obj[key])
? fieldMergeFn(obj[key], value)
: value;
}
@ -336,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;
}
}

20
apps/vben5/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue

@ -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(() => {
@ -227,10 +229,13 @@ function fieldBindEvent(slotProps: Record<string, any>) {
return onChange?.(e?.target?.[bindEventField] ?? e);
},
onInput: () => {},
...(disabledOnInputListener ? { onInput: undefined } : {}),
};
}
return {};
return {
...(disabledOnInputListener ? { onInput: undefined } : {}),
...(disabledOnChangeListener ? { onChange: undefined } : {}),
};
}
function createComponentProps(slotProps: Record<string, any>) {
@ -276,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
@ -296,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)">

6
apps/vben5/packages/@core/ui-kit/form-ui/src/form-render/form.vue

@ -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,

26
apps/vben5/packages/@core/ui-kit/form-ui/src/types.ts

@ -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;
/**
* ,undefinednaive-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>>;
};

28
apps/vben5/packages/@core/ui-kit/form-ui/src/vben-use-form.vue

@ -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,11 +42,7 @@ const handleUpdateCollapsed = (value: boolean) => {
};
function handleKeyDownEnter(event: KeyboardEvent) {
if (
!state.value.submitOnEnter ||
!formActionsRef.value ||
!formActionsRef.value.handleSubmit
) {
if (!state.value.submitOnEnter || !forward.value.formApi?.isMounted) {
return;
}
// textarea
@ -54,8 +52,19 @@ function handleKeyDownEnter(event: KeyboardEvent) {
}
event.preventDefault();
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>
@ -79,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"
>

3
apps/vben5/packages/@core/ui-kit/layout-ui/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/layout-ui",
"version": "5.4.8",
"version": "5.5.1",
"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:"

2
apps/vben5/packages/@core/ui-kit/layout-ui/src/components/layout-header.vue

@ -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>

2
apps/vben5/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue

@ -166,7 +166,7 @@ const headerStyle = computed((): CSSProperties => {
return {
...(isSidebarMixed ? { display: 'flex', justifyContent: 'center' } : {}),
height: `${headerHeight}px`,
height: `${headerHeight - 1}px`,
...contentWidthStyle.value,
};
});

6
apps/vben5/packages/@core/ui-kit/layout-ui/src/vben-layout.vue

@ -11,6 +11,7 @@ import {
} from '@vben-core/composables';
import { Menu } from '@vben-core/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
import { useMouse, useScroll, useThrottleFn } from '@vueuse/core';
@ -457,6 +458,8 @@ function handleHeaderToggle() {
emit('toggleSidebar');
}
}
const idMainContent = ELEMENT_ID_MAIN_CONTENT;
</script>
<template>
@ -533,7 +536,7 @@ function handleHeaderToggle() {
<template #toggle-button>
<VbenIconButton
v-if="showHeaderToggleButton"
class="my-0 ml-2 mr-1 rounded-md"
class="my-0 mr-1 rounded-md"
@click="handleHeaderToggle"
>
<Menu class="size-4" />
@ -553,6 +556,7 @@ function handleHeaderToggle() {
<!-- </div> -->
<LayoutContent
:id="idMainContent"
:content-compact="contentCompact"
:content-compact-width="contentCompactWidth"
:padding="contentPadding"

2
apps/vben5/packages/@core/ui-kit/menu-ui/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/menu-ui",
"version": "5.4.8",
"version": "5.5.1",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {

1
apps/vben5/packages/@core/ui-kit/menu-ui/src/index.ts

@ -1,3 +1,4 @@
export { default as MenuBadge } from './components/menu-badge.vue';
export * from './components/normal-menu';
export { default as Menu } from './menu.vue';
export type * from './types';

13
apps/vben5/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts

@ -7,6 +7,11 @@ import type { Component, Ref } from 'vue';
export type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top';
export interface DrawerProps {
/**
*
* @default false
*/
appendToMain?: boolean;
/**
*
*/
@ -59,12 +64,12 @@ export interface DrawerProps {
*
*/
headerClass?: ClassType;
/**
*
* @default false
*/
loading?: boolean;
/**
*
* @default true
@ -74,12 +79,12 @@ export interface DrawerProps {
*
*/
openAutoFocus?: boolean;
/**
*
* @default right
*/
placement?: DrawerPlacement;
/**
*
* @default true
@ -98,6 +103,10 @@ export interface DrawerProps {
*
*/
titleTooltip?: string;
/**
*
*/
zIndex?: number;
}
export interface DrawerState extends DrawerProps {

13
apps/vben5/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
import { provide, ref, useId, watch } from 'vue';
import { computed, provide, ref, useId, watch } from 'vue';
import {
useIsMobile,
@ -23,6 +23,7 @@ import {
VbenLoading,
VisuallyHidden,
} from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
import { globalShareState } from '@vben-core/shared/global-state';
import { cn } from '@vben-core/shared/utils';
@ -31,7 +32,9 @@ interface Props extends DrawerProps {
}
const props = withDefaults(defineProps<Props>(), {
appendToMain: false,
drawerApi: undefined,
zIndex: 1000,
});
const components = globalShareState.getComponents();
@ -46,6 +49,7 @@ const { isMobile } = useIsMobile();
const state = props.drawerApi?.useStore?.();
const {
appendToMain,
cancelText,
class: drawerClass,
closable,
@ -67,6 +71,7 @@ const {
showConfirmButton,
title,
titleTooltip,
zIndex,
} = usePriorityValues(props, state);
watch(
@ -110,6 +115,10 @@ function handleFocusOutside(e: Event) {
e.preventDefault();
e.stopPropagation();
}
const getAppendTo = computed(() => {
return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
});
</script>
<template>
<Sheet
@ -118,6 +127,7 @@ function handleFocusOutside(e: Event) {
@update:open="() => drawerApi?.close()"
>
<SheetContent
:append-to="getAppendTo"
:class="
cn('flex w-[520px] flex-col', drawerClass, {
'!w-full': isMobile || placement === 'bottom' || placement === 'top',
@ -127,6 +137,7 @@ function handleFocusOutside(e: Event) {
:modal="modal"
:open="state?.isOpen"
:side="placement"
:z-index="zIndex"
@close-auto-focus="handleFocusOutside"
@escape-key-down="escapeKeyDown"
@focus-outside="handleFocusOutside"

11
apps/vben5/packages/@core/ui-kit/popup-ui/src/modal/modal.ts

@ -3,6 +3,11 @@ import type { ModalApi } from './modal-api';
import type { Component, Ref } from 'vue';
export interface ModalProps {
/**
*
* @default false
*/
appendToMain?: boolean;
/**
*
* @default false
@ -12,7 +17,6 @@ export interface ModalProps {
*
*/
cancelText?: string;
/**
*
* @default false
@ -20,6 +24,7 @@ export interface ModalProps {
centered?: boolean;
class?: string;
/**
*
* @default true
@ -112,6 +117,10 @@ export interface ModalProps {
*
*/
titleTooltip?: string;
/**
*
*/
zIndex?: number;
}
export interface ModalState extends ModalProps {

11
apps/vben5/packages/@core/ui-kit/popup-ui/src/modal/modal.vue

@ -22,6 +22,7 @@ import {
VbenLoading,
VisuallyHidden,
} from '@vben-core/shadcn-ui';
import { ELEMENT_ID_MAIN_CONTENT } from '@vben-core/shared/constants';
import { globalShareState } from '@vben-core/shared/global-state';
import { cn } from '@vben-core/shared/utils';
@ -32,6 +33,7 @@ interface Props extends ModalProps {
}
const props = withDefaults(defineProps<Props>(), {
appendToMain: false,
modalApi: undefined,
});
@ -52,6 +54,7 @@ const { isMobile } = useIsMobile();
const state = props.modalApi?.useStore?.();
const {
appendToMain,
bordered,
cancelText,
centered,
@ -78,6 +81,7 @@ const {
showConfirmButton,
title,
titleTooltip,
zIndex,
} = usePriorityValues(props, state);
const shouldFullscreen = computed(
@ -161,6 +165,9 @@ function handleFocusOutside(e: Event) {
e.preventDefault();
e.stopPropagation();
}
const getAppendTo = computed(() => {
return appendToMain.value ? `#${ELEMENT_ID_MAIN_CONTENT}` : undefined;
});
</script>
<template>
<Dialog
@ -170,9 +177,10 @@ function handleFocusOutside(e: Event) {
>
<DialogContent
ref="contentRef"
:append-to="getAppendTo"
:class="
cn(
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0 sm:rounded-2xl',
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0 sm:rounded-[var(--radius)]',
modalClass,
{
'border-border border': bordered,
@ -187,6 +195,7 @@ function handleFocusOutside(e: Event) {
:modal="modal"
:open="state?.isOpen"
:show-close="closable"
:z-index="zIndex"
close-class="top-3"
@close-auto-focus="handleFocusOutside"
@closed="() => modalApi?.onClosed()"

2
apps/vben5/packages/@core/ui-kit/shadcn-ui/package.json

@ -1,6 +1,6 @@
{
"name": "@vben-core/shadcn-ui",
"version": "5.4.8",
"version": "5.5.1",
"#main": "./dist/index.mjs",
"#module": "./dist/index.mjs",
"homepage": "https://github.com/vbenjs/vue-vben-admin",

4
apps/vben5/packages/@core/ui-kit/shadcn-ui/src/components/pin-input/input.vue

@ -47,6 +47,10 @@ watch(
},
);
watch(inputValue, (val) => {
modelValue.value = val.join('');
});
function handleComplete(e: string[]) {
modelValue.value = e.join('');
emit('complete');

4
apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/button/button.ts

@ -11,8 +11,8 @@ export const buttonVariants = cva(
size: {
default: 'h-9 px-4 py-2',
icon: 'h-8 w-8 rounded-sm px-1 text-lg',
lg: 'h-10 rounded-md px-8',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-4',
sm: 'h-8 rounded-md px-2 text-xs',
xs: 'h-8 w-8 rounded-sm px-1 text-xs',
},
variant: {

27
apps/vben5/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue

@ -20,14 +20,16 @@ import DialogOverlay from './DialogOverlay.vue';
const props = withDefaults(
defineProps<
{
appendTo?: HTMLElement | string;
class?: ClassType;
closeClass?: ClassType;
modal?: boolean;
open?: boolean;
showClose?: boolean;
zIndex?: number;
} & DialogContentProps
>(),
{ showClose: true },
{ appendTo: 'body', showClose: true, zIndex: 1000 },
);
const emits = defineEmits<
{ close: []; closed: []; opened: [] } & DialogContentEmits
@ -45,6 +47,18 @@ const delegatedProps = computed(() => {
return delegated;
});
function isAppendToBody() {
return (
props.appendTo === 'body' ||
props.appendTo === document.body ||
!props.appendTo
);
}
const position = computed(() => {
return isAppendToBody() ? 'fixed' : 'absolute';
});
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
@ -64,17 +78,22 @@ defineExpose({
</script>
<template>
<DialogPortal>
<DialogPortal :to="appendTo">
<Transition name="fade">
<DialogOverlay v-if="open && modal" @click="() => emits('close')" />
<DialogOverlay
v-if="open && modal"
:style="{ zIndex, position }"
@click="() => emits('close')"
/>
</Transition>
<DialogContent
ref="contentRef"
:style="{ zIndex, position }"
@animationend="onAnimationEnd"
v-bind="forwarded"
:class="
cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] fixed z-[1000] w-full p-6 shadow-lg outline-none sm:rounded-xl',
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] w-full p-6 shadow-lg outline-none sm:rounded-xl',
props.class,
)
"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save