diff --git a/.github/contributing.md b/.github/contributing.md index f22370f3b..304c51926 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -19,11 +19,9 @@ Project maintainers have the right and responsibility to remove, edit, or reject - Checkout a topic branch from the relevant branch, e.g. main, and merge back against that branch. - If adding a new feature: - - Provide a convincing reason to add this feature. Ideally, you should open a suggestion issue first and have it approved before working on it. - If fixing bug: - - Provide a detailed description of the bug in the PR. Live demo preferred. - It's OK to have multiple small commits as you work on the PR - GitHub can automatically squash them before merging. diff --git a/apps/backend-mock/api/demo/bigint.ts b/apps/backend-mock/api/demo/bigint.ts new file mode 100644 index 000000000..880cc5ea8 --- /dev/null +++ b/apps/backend-mock/api/demo/bigint.ts @@ -0,0 +1,28 @@ +export default eventHandler(async (event) => { + const userinfo = verifyAccessToken(event); + if (!userinfo) { + return unAuthorizedResponse(event); + } + const data = ` + { + "code": 0, + "message": "success", + "data": [ + { + "id": 123456789012345678901234567890123456789012345678901234567890, + "name": "John Doe", + "age": 30, + "email": "john-doe@demo.com" + }, + { + "id": 987654321098765432109876543210987654321098765432109876543210, + "name": "Jane Smith", + "age": 25, + "email": "jane@demo.com" + } + ] + } + `; + setHeader(event, 'Content-Type', 'application/json'); + return data; +}); diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index bb5ab7e12..6dcb91848 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -1,6 +1,6 @@ { "name": "@vben/web-antd", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://vben.pro", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index d452d5210..786a93dae 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -8,13 +8,7 @@ import type { Component } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { - defineAsyncComponent, - defineComponent, - getCurrentInstance, - h, - ref, -} from 'vue'; +import { defineAsyncComponent, defineComponent, h, ref } from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; @@ -82,16 +76,15 @@ const withDefaultPlaceholder = ( $t(`ui.placeholder.${type}`); // 透传组件暴露的方法 const innerRef = ref(); - const publicApi: Recordable = {}; - expose(publicApi); - const instance = getCurrentInstance(); - instance?.proxy?.$nextTick(() => { - for (const key in innerRef.value) { - if (typeof innerRef.value[key] === 'function') { - publicApi[key] = innerRef.value[key]; - } - } - }); + expose( + new Proxy( + {}, + { + get: (_target, key) => innerRef.value?.[key], + has: (_target, key) => key in (innerRef.value || {}), + }, + ), + ); return () => h( component, diff --git a/apps/web-antd/src/adapter/form.ts b/apps/web-antd/src/adapter/form.ts index 65ff793b6..983a7f516 100644 --- a/apps/web-antd/src/adapter/form.ts +++ b/apps/web-antd/src/adapter/form.ts @@ -8,40 +8,42 @@ import type { ComponentType } from './component'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; -setupVbenForm({ - config: { - // ant design vue组件库默认都是 v-model:value - baseModelPropName: 'value', +async function initSetupVbenForm() { + setupVbenForm({ + config: { + // ant design vue组件库默认都是 v-model:value + baseModelPropName: 'value', - // 一些组件是 v-model:checked 或者 v-model:fileList - modelPropNameMap: { - Checkbox: 'checked', - Radio: 'checked', - Switch: 'checked', - Upload: 'fileList', + // 一些组件是 v-model:checked 或者 v-model:fileList + modelPropNameMap: { + Checkbox: 'checked', + Radio: 'checked', + Switch: 'checked', + Upload: 'fileList', + }, }, - }, - defineRules: { - // 输入项目必填国际化适配 - required: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - return true; + defineRules: { + // 输入项目必填国际化适配 + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + return true; + }, + // 选择项目必填国际化适配 + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, }, - // 选择项目必填国际化适配 - selectRequired: (value, _params, ctx) => { - if (value === undefined || value === null) { - return $t('ui.formRules.selectRequired', [ctx.label]); - } - return true; - }, - }, -}); + }); +} const useVbenForm = useForm; -export { useVbenForm, z }; +export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; export type { VbenFormProps }; diff --git a/apps/web-antd/src/adapter/vxe-table.ts b/apps/web-antd/src/adapter/vxe-table.ts index d296b2050..7de2859de 100644 --- a/apps/web-antd/src/adapter/vxe-table.ts +++ b/apps/web-antd/src/adapter/vxe-table.ts @@ -1,3 +1,5 @@ +import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; + import { h } from 'vue'; import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; @@ -33,7 +35,7 @@ setupVbenVxeTable({ round: true, showOverflow: true, size: 'small', - }, + } as VxeTableGridOptions, }); // 表格配置项可以用 cellRender: { name: 'CellImage' }, diff --git a/apps/web-antd/src/bootstrap.ts b/apps/web-antd/src/bootstrap.ts index e4aaf4057..ec7211254 100644 --- a/apps/web-antd/src/bootstrap.ts +++ b/apps/web-antd/src/bootstrap.ts @@ -12,6 +12,7 @@ import { useTitle } from '@vueuse/core'; import { $t, setupI18n } from '#/locales'; import { initComponentAdapter } from './adapter/component'; +import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; @@ -19,6 +20,9 @@ async function bootstrap(namespace: string) { // 初始化组件适配器 await initComponentAdapter(); + // 初始化表单组件 + await initSetupVbenForm(); + // // 设置弹窗的默认配置 // setDefaultModalProps({ // fullscreenButton: false, diff --git a/apps/web-ele/package.json b/apps/web-ele/package.json index a51f5a9b6..386c36840 100644 --- a/apps/web-ele/package.json +++ b/apps/web-ele/package.json @@ -1,6 +1,6 @@ { "name": "@vben/web-ele", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://vben.pro", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/apps/web-ele/src/adapter/component/index.ts b/apps/web-ele/src/adapter/component/index.ts index e2f533cf5..79a463602 100644 --- a/apps/web-ele/src/adapter/component/index.ts +++ b/apps/web-ele/src/adapter/component/index.ts @@ -8,13 +8,7 @@ import type { Component } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { - defineAsyncComponent, - defineComponent, - getCurrentInstance, - h, - ref, -} from 'vue'; +import { defineAsyncComponent, defineComponent, h, ref } from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; @@ -139,16 +133,15 @@ const withDefaultPlaceholder = ( $t(`ui.placeholder.${type}`); // 透传组件暴露的方法 const innerRef = ref(); - const publicApi: Recordable = {}; - expose(publicApi); - const instance = getCurrentInstance(); - instance?.proxy?.$nextTick(() => { - for (const key in innerRef.value) { - if (typeof innerRef.value[key] === 'function') { - publicApi[key] = innerRef.value[key]; - } - } - }); + expose( + new Proxy( + {}, + { + get: (_target, key) => innerRef.value?.[key], + has: (_target, key) => key in (innerRef.value || {}), + }, + ), + ); return () => h( component, diff --git a/apps/web-ele/src/adapter/form.ts b/apps/web-ele/src/adapter/form.ts index 13ae9c428..936c3fe4c 100644 --- a/apps/web-ele/src/adapter/form.ts +++ b/apps/web-ele/src/adapter/form.ts @@ -8,32 +8,34 @@ import type { ComponentType } from './component'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; -setupVbenForm({ - config: { - modelPropNameMap: { - Upload: 'fileList', - CheckboxGroup: 'model-value', +async function initSetupVbenForm() { + setupVbenForm({ + config: { + modelPropNameMap: { + Upload: 'fileList', + CheckboxGroup: 'model-value', + }, }, - }, - defineRules: { - required: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - return true; + defineRules: { + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + return true; + }, + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, }, - selectRequired: (value, _params, ctx) => { - if (value === undefined || value === null) { - return $t('ui.formRules.selectRequired', [ctx.label]); - } - return true; - }, - }, -}); + }); +} const useVbenForm = useForm; -export { useVbenForm, z }; +export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; export type { VbenFormProps }; diff --git a/apps/web-ele/src/adapter/vxe-table.ts b/apps/web-ele/src/adapter/vxe-table.ts index 44b31eaed..40b8179d3 100644 --- a/apps/web-ele/src/adapter/vxe-table.ts +++ b/apps/web-ele/src/adapter/vxe-table.ts @@ -1,3 +1,5 @@ +import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; + import { h } from 'vue'; import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; @@ -33,7 +35,7 @@ setupVbenVxeTable({ round: true, showOverflow: true, size: 'small', - }, + } as VxeTableGridOptions, }); // 表格配置项可以用 cellRender: { name: 'CellImage' }, diff --git a/apps/web-ele/src/bootstrap.ts b/apps/web-ele/src/bootstrap.ts index be054f807..e5befb5a7 100644 --- a/apps/web-ele/src/bootstrap.ts +++ b/apps/web-ele/src/bootstrap.ts @@ -13,12 +13,17 @@ import { ElLoading } from 'element-plus'; import { $t, setupI18n } from '#/locales'; import { initComponentAdapter } from './adapter/component'; +import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; async function bootstrap(namespace: string) { // 初始化组件适配器 await initComponentAdapter(); + + // 初始化表单组件 + await initSetupVbenForm(); + // // 设置弹窗的默认配置 // setDefaultModalProps({ // fullscreenButton: false, diff --git a/apps/web-naive/package.json b/apps/web-naive/package.json index ed95930c2..b97ab64f7 100644 --- a/apps/web-naive/package.json +++ b/apps/web-naive/package.json @@ -1,6 +1,6 @@ { "name": "@vben/web-naive", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://vben.pro", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/apps/web-naive/src/adapter/component/index.ts b/apps/web-naive/src/adapter/component/index.ts index 7ff115331..f9df20273 100644 --- a/apps/web-naive/src/adapter/component/index.ts +++ b/apps/web-naive/src/adapter/component/index.ts @@ -8,13 +8,7 @@ import type { Component } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { - defineAsyncComponent, - defineComponent, - getCurrentInstance, - h, - ref, -} from 'vue'; +import { defineAsyncComponent, defineComponent, h, ref } from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; @@ -85,16 +79,15 @@ const withDefaultPlaceholder = ( $t(`ui.placeholder.${type}`); // 透传组件暴露的方法 const innerRef = ref(); - const publicApi: Recordable = {}; - expose(publicApi); - const instance = getCurrentInstance(); - instance?.proxy?.$nextTick(() => { - for (const key in innerRef.value) { - if (typeof innerRef.value[key] === 'function') { - publicApi[key] = innerRef.value[key]; - } - } - }); + expose( + new Proxy( + {}, + { + get: (_target, key) => innerRef.value?.[key], + has: (_target, key) => key in (innerRef.value || {}), + }, + ), + ); return () => h( component, diff --git a/apps/web-naive/src/adapter/form.ts b/apps/web-naive/src/adapter/form.ts index 2f2ed2abe..9de44a01d 100644 --- a/apps/web-naive/src/adapter/form.ts +++ b/apps/web-naive/src/adapter/form.ts @@ -8,36 +8,38 @@ import type { ComponentType } from './component'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; -setupVbenForm({ - config: { - // naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效 - emptyStateValue: null, - baseModelPropName: 'value', - modelPropNameMap: { - Checkbox: 'checked', - Radio: 'checked', - Upload: 'fileList', +async function initSetupVbenForm() { + setupVbenForm({ + config: { + // naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效 + emptyStateValue: null, + baseModelPropName: 'value', + modelPropNameMap: { + Checkbox: 'checked', + Radio: 'checked', + Upload: 'fileList', + }, }, - }, - defineRules: { - required: (value, _params, ctx) => { - if (value === undefined || value === null || value.length === 0) { - return $t('ui.formRules.required', [ctx.label]); - } - return true; + defineRules: { + required: (value, _params, ctx) => { + if (value === undefined || value === null || value.length === 0) { + return $t('ui.formRules.required', [ctx.label]); + } + return true; + }, + selectRequired: (value, _params, ctx) => { + if (value === undefined || value === null) { + return $t('ui.formRules.selectRequired', [ctx.label]); + } + return true; + }, }, - selectRequired: (value, _params, ctx) => { - if (value === undefined || value === null) { - return $t('ui.formRules.selectRequired', [ctx.label]); - } - return true; - }, - }, -}); + }); +} const useVbenForm = useForm; -export { useVbenForm, z }; +export { initSetupVbenForm, useVbenForm, z }; export type VbenFormSchema = FormSchema; export type { VbenFormProps }; diff --git a/apps/web-naive/src/adapter/vxe-table.ts b/apps/web-naive/src/adapter/vxe-table.ts index 081cfb29e..3bad067cd 100644 --- a/apps/web-naive/src/adapter/vxe-table.ts +++ b/apps/web-naive/src/adapter/vxe-table.ts @@ -1,3 +1,5 @@ +import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; + import { h } from 'vue'; import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table'; @@ -33,7 +35,7 @@ setupVbenVxeTable({ round: true, showOverflow: true, size: 'small', - }, + } as VxeTableGridOptions, }); // 表格配置项可以用 cellRender: { name: 'CellImage' }, diff --git a/apps/web-naive/src/bootstrap.ts b/apps/web-naive/src/bootstrap.ts index 5fddd7d87..df0b2cbb8 100644 --- a/apps/web-naive/src/bootstrap.ts +++ b/apps/web-naive/src/bootstrap.ts @@ -12,12 +12,16 @@ import { useTitle } from '@vueuse/core'; import { $t, setupI18n } from '#/locales'; import { initComponentAdapter } from './adapter/component'; +import { initSetupVbenForm } from './adapter/form'; import App from './app.vue'; import { router } from './router'; async function bootstrap(namespace: string) { // 初始化组件适配器 - initComponentAdapter(); + await initComponentAdapter(); + + // 初始化表单组件 + await initSetupVbenForm(); // // 设置弹窗的默认配置 // setDefaultModalProps({ diff --git a/apps/web-naive/src/views/demos/form/basic.vue b/apps/web-naive/src/views/demos/form/basic.vue index fe26624cc..60702a11e 100644 --- a/apps/web-naive/src/views/demos/form/basic.vue +++ b/apps/web-naive/src/views/demos/form/basic.vue @@ -1,11 +1,13 @@ diff --git a/apps/web-naive/src/views/demos/form/modal.vue b/apps/web-naive/src/views/demos/form/modal.vue new file mode 100644 index 000000000..52e23542d --- /dev/null +++ b/apps/web-naive/src/views/demos/form/modal.vue @@ -0,0 +1,71 @@ + + diff --git a/docs/package.json b/docs/package.json index a53005342..f57dfc854 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "@vben/docs", - "version": "5.5.6", + "version": "5.5.7", "private": true, "scripts": { "build": "vitepress build", diff --git a/docs/src/components/common-ui/vben-drawer.md b/docs/src/components/common-ui/vben-drawer.md index b66bd3a07..3a28cce79 100644 --- a/docs/src/components/common-ui/vben-drawer.md +++ b/docs/src/components/common-ui/vben-drawer.md @@ -22,7 +22,7 @@ outline: deep ## 基础用法 -使用 `useVbenDrawer` 创建最基础的模态框。 +使用 `useVbenDrawer` 创建最基础的抽屉。 @@ -52,7 +52,7 @@ Drawer 内的内容一般业务中,会比较复杂,所以我们可以将 dra ::: info 注意 -- `VbenDrawer` 组件对与参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。 +- `VbenDrawer` 组件对于参数的处理优先级是 `slot` > `props` > `state`(通过api更新的状态以及useVbenDrawer参数)。如果你已经传入了 `slot` 或者 `props`,那么 `setState` 将不会生效,这种情况下你可以通过 `slot` 或者 `props` 来更新状态。 - 如果你使用到了 `connectedComponent` 参数,那么会存在 2 个`useVbenDrawer`, 此时,如果同时设置了相同的参数,那么以内部为准(也就是没有设置 connectedComponent 的代码)。比如 同时设置了 `onConfirm`,那么以内部的 `onConfirm` 为准。`onOpenChange`事件除外,内外都会触发。 - 使用了`connectedComponent`参数时,可以配置`destroyOnClose`属性来决定当关闭弹窗时,是否要销毁`connectedComponent`组件(重新创建`connectedComponent`组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态。)。 - 如果抽屉的默认行为不符合你的预期,可以在`src\bootstrap.ts`中修改`setDefaultDrawerProps`的参数来设置默认的属性,如默认隐藏全屏按钮,修改默认ZIndex等。 @@ -77,7 +77,7 @@ const [Drawer, drawerApi] = useVbenDrawer({ | 属性名 | 描述 | 类型 | 默认值 | | --- | --- | --- | --- | | appendToMain | 是否挂载到内容区域(默认挂载到body) | `boolean` | `false` | -| connectedComponent | 连接另一个Modal组件 | `Component` | - | +| connectedComponent | 连接另一个Drawer组件 | `Component` | - | | destroyOnClose | 关闭时销毁 | `boolean` | `false` | | title | 标题 | `string\|slot` | - | | titleTooltip | 标题提示信息 | `string\|slot` | - | @@ -96,7 +96,7 @@ const [Drawer, drawerApi] = useVbenDrawer({ | cancelText | 取消按钮文本 | `string\|slot` | `取消` | | placement | 抽屉弹出位置 | `'left'\|'right'\|'top'\|'bottom'` | `right` | | showCancelButton | 显示取消按钮 | `boolean` | `true` | -| showConfirmButton | 显示确认按钮文本 | `boolean` | `true` | +| showConfirmButton | 显示确认按钮 | `boolean` | `true` | | class | modal的class,宽度通过这个配置 | `string` | - | | contentClass | modal内容区域的class | `string` | - | | footerClass | modal底部区域的class | `string` | - | diff --git a/docs/src/components/common-ui/vben-ellipsis-text.md b/docs/src/components/common-ui/vben-ellipsis-text.md index 109f1161c..ce6c0334a 100644 --- a/docs/src/components/common-ui/vben-ellipsis-text.md +++ b/docs/src/components/common-ui/vben-ellipsis-text.md @@ -26,6 +26,12 @@ outline: deep +## 自动显示 tooltip + +通过`tooltip-when-ellipsis`设置,仅在文本长度超出导致省略号出现时才触发 tooltip。 + + + ## API ### Props @@ -37,6 +43,8 @@ outline: deep | maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` | | placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` | | tooltip | 启用文本提示 | `boolean` | `true` | +| tooltipWhenEllipsis | 内容超出,自动启用文本提示 | `boolean` | `false` | +| ellipsisThreshold | 设置 tooltipWhenEllipsis 后才生效,文本截断检测的像素差异阈值,越大则判断越严格,如果碰见异常情况可以自己设置阈值 | `number` | `3` | | tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - | | tooltipColor | 提示文本的颜色 | `string` | - | | tooltipFontSize | 提示文本的大小 | `string` | - | diff --git a/docs/src/components/common-ui/vben-form.md b/docs/src/components/common-ui/vben-form.md index 7abb3051e..a6d455253 100644 --- a/docs/src/components/common-ui/vben-form.md +++ b/docs/src/components/common-ui/vben-form.md @@ -324,6 +324,7 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单 | submitOnEnter | 按下回车健时提交表单 | `boolean` | false | | submitOnChange | 字段值改变时提交表单(内部防抖,这个属性一般用于表格的搜索表单) | `boolean` | false | | compact | 是否紧凑模式(忽略为校验信息所预留的空间) | `boolean` | false | +| scrollToFirstError | 表单验证失败时是否自动滚动到第一个错误字段 | `boolean` | false | ::: tip handleValuesChange diff --git a/docs/src/demos/vben-ellipsis-text/auto-display/index.vue b/docs/src/demos/vben-ellipsis-text/auto-display/index.vue new file mode 100644 index 000000000..fb5a32a50 --- /dev/null +++ b/docs/src/demos/vben-ellipsis-text/auto-display/index.vue @@ -0,0 +1,16 @@ + + diff --git a/docs/src/demos/vben-form/rules/index.vue b/docs/src/demos/vben-form/rules/index.vue index 7abcc6f6c..78e598133 100644 --- a/docs/src/demos/vben-form/rules/index.vue +++ b/docs/src/demos/vben-form/rules/index.vue @@ -15,6 +15,7 @@ const [Form] = useVbenForm({ handleSubmit: onSubmit, // 垂直布局,label和input在不同行,值为vertical // 水平布局,label和input在同一行 + scrollToFirstError: true, layout: 'horizontal', schema: [ { diff --git a/docs/src/en/guide/essentials/server.md b/docs/src/en/guide/essentials/server.md index 95d505c0a..67e0b1fd0 100644 --- a/docs/src/en/guide/essentials/server.md +++ b/docs/src/en/guide/essentials/server.md @@ -150,8 +150,8 @@ export async function saveUserApi(user: UserInfo) { ```ts import { requestClient } from '#/api/request'; -export async function deleteUserApi(user: UserInfo) { - return requestClient.delete(`/user/${user.id}`, user); +export async function deleteUserApi(userId: number) { + return requestClient.delete(`/user/${userId}`); } ``` diff --git a/docs/src/en/guide/essentials/settings.md b/docs/src/en/guide/essentials/settings.md index b7cb3a2de..59a6c5c01 100644 --- a/docs/src/en/guide/essentials/settings.md +++ b/docs/src/en/guide/essentials/settings.md @@ -21,7 +21,7 @@ The rules are consistent with [Vite Env Variables and Modes](https://vitejs.dev/ console.log(import.meta.env.VITE_PROT); ``` -- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging. ::: +- Variables starting with `VITE_GLOB_*` will be added to the `_app.config.js` configuration file during packaging. ::: @@ -138,6 +138,27 @@ To add a new dynamically modifiable configuration item, simply follow the steps } ``` +- In `packages/effects/hooks/src/use-app-config.ts`, add the corresponding configuration item, such as: + + ```ts + export function useAppConfig( + env: Record, + isProduction: boolean, + ): ApplicationConfig { + // In production environment, directly use the window._VBEN_ADMIN_PRO_APP_CONF_ global variable + const config = isProduction + ? window._VBEN_ADMIN_PRO_APP_CONF_ + : (env as VbenAdminProAppConfigRaw); + + const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++] + + return { + apiURL: VITE_GLOB_API_URL, + otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++] + }; + } + ``` + At this point, you can use the `useAppConfig` method within the project to access the newly added configuration item. ```ts @@ -238,6 +259,7 @@ const defaultPreferences: Preferences = { }, logo: { enable: true, + fit: 'contain', source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp', }, navigation: { @@ -431,6 +453,8 @@ interface HeaderPreferences { interface LogoPreferences { /** Whether the logo is visible */ enable: boolean; + /** Logo image fitting method */ + fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; /** Logo URL */ source: string; } diff --git a/docs/src/en/guide/in-depth/access.md b/docs/src/en/guide/in-depth/access.md index 05997d7d5..545dddabd 100644 --- a/docs/src/en/guide/in-depth/access.md +++ b/docs/src/en/guide/in-depth/access.md @@ -4,10 +4,11 @@ outline: deep # Access Control -The framework has built-in two types of access control methods: +The framework has built-in three types of access control methods: - Determining whether a menu or button can be accessed based on user roles - Determining whether a menu or button can be accessed through an API +- Mixed mode: Using both frontend and backend access control simultaneously ## Frontend Access Control @@ -151,6 +152,43 @@ const dashboardMenus = [ At this point, the configuration is complete. You need to ensure that after logging in, the format of the menu returned by the interface is correct; otherwise, access will not be possible. +## Mixed Access Control + +**Implementation Principle**: Mixed mode combines both frontend access control and backend access control methods. The system processes frontend fixed route permissions and backend dynamic menu data in parallel, ultimately merging both parts of routes to provide a more flexible access control solution. + +**Advantages**: Combines the performance advantages of frontend control with the flexibility of backend control, suitable for complex business scenarios requiring permission management. + +### Steps + +- Ensure the current mode is set to mixed access control + +Adjust `preferences.ts` in the corresponding application directory to ensure `accessMode='mixed'`. + +```ts +import { defineOverridesPreferences } from '@vben/preferences'; + +export const overridesPreferences = defineOverridesPreferences({ + // overrides + app: { + accessMode: 'mixed', + }, +}); +``` + +- Configure frontend route permissions + +Same as the route permission configuration method in [Frontend Access Control](#frontend-access-control) mode. + +- Configure backend menu interface + +Same as the interface configuration method in [Backend Access Control](#backend-access-control) mode. + +- Ensure roles and permissions match + +Must satisfy both frontend route permission configuration and backend menu data return requirements, ensuring user roles match the permission configurations of both modes. + +At this point, the configuration is complete. Mixed mode will automatically merge frontend and backend routes, providing complete access control functionality. + ## Fine-grained Control of Buttons In some cases, we need to control the display of buttons with fine granularity. We can control the display of buttons through interfaces or roles. diff --git a/docs/src/en/guide/project/standard.md b/docs/src/en/guide/project/standard.md index e5417ce7c..4d880c467 100644 --- a/docs/src/en/guide/project/standard.md +++ b/docs/src/en/guide/project/standard.md @@ -4,7 +4,6 @@ - If you want to contribute code to the project, please ensure your code complies with the project's coding standards. - If you are using `vscode`, you need to install the following plugins: - - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Script code checking - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - Code formatting - [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - Word syntax checking @@ -157,7 +156,6 @@ The most effective solution is to perform Lint checks locally before committing. The project defines corresponding hooks inside `lefthook.yml`: - `pre-commit`: Runs before commit, used for code formatting and checking - - `code-workspace`: Updates VSCode workspace configuration - `lint-md`: Formats Markdown files - `lint-vue`: Formats and checks Vue files @@ -167,7 +165,6 @@ The project defines corresponding hooks inside `lefthook.yml`: - `lint-json`: Formats other JSON files - `post-merge`: Runs after merge, used for automatic dependency installation - - `install`: Runs `pnpm install` to install new dependencies - `commit-msg`: Runs during commit, used for checking commit message format diff --git a/docs/src/friend-links/index.md b/docs/src/friend-links/index.md index 84b61906f..a9dc7ccf4 100644 --- a/docs/src/friend-links/index.md +++ b/docs/src/friend-links/index.md @@ -18,7 +18,6 @@ ### 友情链接 - 在您的网站上添加我们的友情链接,链接如下: - - 名称:Vben Admin - 链接:https://www.vben.pro - 描述:Vben Admin 企业级开箱即用的中后台前端解决方案 diff --git a/docs/src/guide/essentials/server.md b/docs/src/guide/essentials/server.md index 9a4949673..cd61d3a51 100644 --- a/docs/src/guide/essentials/server.md +++ b/docs/src/guide/essentials/server.md @@ -180,8 +180,8 @@ export async function saveUserApi(user: UserInfo) { ```ts import { requestClient } from '#/api/request'; -export async function deleteUserApi(user: UserInfo) { - return requestClient.delete(`/user/${user.id}`, user); +export async function deleteUserApi(userId: number) { + return requestClient.delete(`/user/${userId}`); } ``` diff --git a/docs/src/guide/essentials/settings.md b/docs/src/guide/essentials/settings.md index cd7c9380d..9b47c04d3 100644 --- a/docs/src/guide/essentials/settings.md +++ b/docs/src/guide/essentials/settings.md @@ -21,7 +21,7 @@ console.log(import.meta.env.VITE_PROT); ``` -- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中. ::: +- 以 `VITE_GLOB_*` 开头的的变量,在打包的时候,会被加入 `_app.config.js`配置文件当中. ::: @@ -137,6 +137,27 @@ const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD); } ``` +- 在 `packages/effects/hooks/src/use-app-config.ts` 中,新增对应的配置项,如: + + ```ts + export function useAppConfig( + env: Record, + isProduction: boolean, + ): ApplicationConfig { + // 生产环境下,直接使用 window._VBEN_ADMIN_PRO_APP_CONF_ 全局变量 + const config = isProduction + ? window._VBEN_ADMIN_PRO_APP_CONF_ + : (env as VbenAdminProAppConfigRaw); + + const { VITE_GLOB_API_URL, VITE_GLOB_OTHER_API_URL } = config; // [!code ++] + + return { + apiURL: VITE_GLOB_API_URL, + otherApiURL: VITE_GLOB_OTHER_API_URL, // [!code ++] + }; + } + ``` + 到这里,就可以在项目内使用 `useAppConfig`方法获取到新增的配置项了。 ```ts @@ -237,6 +258,7 @@ const defaultPreferences: Preferences = { }, logo: { enable: true, + fit: 'contain', source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp', }, navigation: { @@ -431,6 +453,8 @@ interface HeaderPreferences { interface LogoPreferences { /** logo是否可见 */ enable: boolean; + /** logo图片适应方式 */ + fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; /** logo地址 */ source: string; } diff --git a/docs/src/guide/in-depth/access.md b/docs/src/guide/in-depth/access.md index 0dbd0819d..bbce30a20 100644 --- a/docs/src/guide/in-depth/access.md +++ b/docs/src/guide/in-depth/access.md @@ -4,10 +4,11 @@ outline: deep # 权限 -框架内置了两种权限控制方式: +框架内置了三种权限控制方式: - 通过用户角色来判断菜单或者按钮是否可以访问 - 通过接口来判断菜单或者按钮是否可以访问 +- 混合模式:同时使用前端和后端权限控制 ## 前端访问控制 @@ -159,6 +160,43 @@ const dashboardMenus = [ 到这里,就已经配置完成,你需要确保登录后,接口返回的菜单格式正确,否则无法访问。 +## 混合访问控制 + +**实现原理**: 混合模式同时结合了前端访问控制和后端访问控制两种方式。系统会并行处理前端固定路由权限和后端动态菜单数据,最终将两部分路由合并,提供更灵活的权限控制方案。 + +**优点**: 兼具前端控制的性能优势和后端控制的灵活性,适合复杂业务场景下的权限管理。 + +### 步骤 + +- 确保当前模式为混合访问控制模式 + +调整对应应用目录下的`preferences.ts`,确保`accessMode='mixed'`。 + +```ts +import { defineOverridesPreferences } from '@vben/preferences'; + +export const overridesPreferences = defineOverridesPreferences({ + // overrides + app: { + accessMode: 'mixed', + }, +}); +``` + +- 配置前端路由权限 + +同[前端访问控制](#前端访问控制)模式的路由权限配置方式。 + +- 配置后端菜单接口 + +同[后端访问控制](#后端访问控制)模式的接口配置方式。 + +- 确保角色和权限匹配 + +需要同时满足前端路由权限配置和后端菜单数据返回的要求,确保用户角色与两种模式的权限配置都匹配。 + +到这里,就已经配置完成,混合模式会自动合并前端和后端的路由,提供完整的权限控制功能。 + ## 按钮细粒度控制 在某些情况下,我们需要对按钮进行细粒度的控制,我们可以借助接口或者角色来控制按钮的显示。 diff --git a/docs/src/guide/project/standard.md b/docs/src/guide/project/standard.md index 12a13da3b..02fc5a91e 100644 --- a/docs/src/guide/project/standard.md +++ b/docs/src/guide/project/standard.md @@ -4,7 +4,6 @@ - 如果你想向项目贡献代码,请确保你的代码符合项目的代码规范。 - 如果你使用的是 `vscode`,需要安装以下插件: - - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - 脚本代码检查 - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - 代码格式化 - [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) - 单词语法检查 @@ -157,7 +156,6 @@ git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风 项目在 `lefthook.yml` 内部定义了相应的 hooks: - `pre-commit`: 在提交前运行,用于代码格式化和检查 - - `code-workspace`: 更新 VSCode 工作区配置 - `lint-md`: 格式化 Markdown 文件 - `lint-vue`: 格式化并检查 Vue 文件 @@ -167,7 +165,6 @@ git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风 - `lint-json`: 格式化其他 JSON 文件 - `post-merge`: 在合并后运行,用于自动安装依赖 - - `install`: 运行 `pnpm install` 安装新依赖 - `commit-msg`: 在提交时运行,用于检查提交信息格式 diff --git a/internal/lint-configs/commitlint-config/package.json b/internal/lint-configs/commitlint-config/package.json index c17cde2a2..a137f947b 100644 --- a/internal/lint-configs/commitlint-config/package.json +++ b/internal/lint-configs/commitlint-config/package.json @@ -1,6 +1,6 @@ { "name": "@vben/commitlint-config", - "version": "5.5.6", + "version": "5.5.7", "private": true, "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", diff --git a/internal/lint-configs/stylelint-config/package.json b/internal/lint-configs/stylelint-config/package.json index ee55c702a..8e2a97c2c 100644 --- a/internal/lint-configs/stylelint-config/package.json +++ b/internal/lint-configs/stylelint-config/package.json @@ -1,6 +1,6 @@ { "name": "@vben/stylelint-config", - "version": "5.5.6", + "version": "5.5.7", "private": true, "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", diff --git a/internal/node-utils/package.json b/internal/node-utils/package.json index 782c0b30f..8b8db7454 100644 --- a/internal/node-utils/package.json +++ b/internal/node-utils/package.json @@ -1,6 +1,6 @@ { "name": "@vben/node-utils", - "version": "5.5.6", + "version": "5.5.7", "private": true, "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", diff --git a/internal/tailwind-config/package.json b/internal/tailwind-config/package.json index 9f62a1b70..8506891b5 100644 --- a/internal/tailwind-config/package.json +++ b/internal/tailwind-config/package.json @@ -1,6 +1,6 @@ { "name": "@vben/tailwind-config", - "version": "5.5.6", + "version": "5.5.7", "private": true, "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", diff --git a/internal/tsconfig/package.json b/internal/tsconfig/package.json index dd4f6321c..44ee3f1b0 100644 --- a/internal/tsconfig/package.json +++ b/internal/tsconfig/package.json @@ -1,6 +1,6 @@ { "name": "@vben/tsconfig", - "version": "5.5.6", + "version": "5.5.7", "private": true, "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", diff --git a/package.json b/package.json index 0583d01cf..8b87dc385 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vben-admin-monorepo", - "version": "5.5.6", + "version": "5.5.7", "private": true, "keywords": [ "monorepo", @@ -98,7 +98,7 @@ "node": ">=20.10.0", "pnpm": ">=9.12.0" }, - "packageManager": "pnpm@10.10.0", + "packageManager": "pnpm@10.12.4", "pnpm": { "peerDependencyRules": { "allowedVersions": { diff --git a/packages/@core/base/design/package.json b/packages/@core/base/design/package.json index 4bdfbe32c..33e924749 100644 --- a/packages/@core/base/design/package.json +++ b/packages/@core/base/design/package.json @@ -1,6 +1,6 @@ { "name": "@vben-core/design", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/packages/@core/base/icons/package.json b/packages/@core/base/icons/package.json index 4d758ca84..9a349883c 100644 --- a/packages/@core/base/icons/package.json +++ b/packages/@core/base/icons/package.json @@ -1,6 +1,6 @@ { "name": "@vben-core/icons", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/packages/@core/base/shared/package.json b/packages/@core/base/shared/package.json index 05bb218ae..b02cc6e47 100644 --- a/packages/@core/base/shared/package.json +++ b/packages/@core/base/shared/package.json @@ -1,6 +1,6 @@ { "name": "@vben-core/shared", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/packages/@core/base/typings/package.json b/packages/@core/base/typings/package.json index dc99de243..e2ab18701 100644 --- a/packages/@core/base/typings/package.json +++ b/packages/@core/base/typings/package.json @@ -1,6 +1,6 @@ { "name": "@vben-core/typings", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/packages/@core/base/typings/src/app.d.ts b/packages/@core/base/typings/src/app.d.ts index 02da2466a..d2e86aec4 100644 --- a/packages/@core/base/typings/src/app.d.ts +++ b/packages/@core/base/typings/src/app.d.ts @@ -60,8 +60,9 @@ type BreadcrumbStyleType = 'background' | 'normal'; * 权限模式 * backend 后端权限模式 * frontend 前端权限模式 + * mixed 混合权限模式 */ -type AccessModeType = 'backend' | 'frontend'; +type AccessModeType = 'backend' | 'frontend' | 'mixed'; /** * 导航风格 diff --git a/packages/@core/composables/package.json b/packages/@core/composables/package.json index 3e03540f7..08db5106f 100644 --- a/packages/@core/composables/package.json +++ b/packages/@core/composables/package.json @@ -1,6 +1,6 @@ { "name": "@vben-core/composables", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap index df20b55d6..1cccbbb27 100644 --- a/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap +++ b/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap @@ -61,6 +61,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj }, "logo": { "enable": true, + "fit": "contain", "source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp", }, "navigation": { diff --git a/packages/@core/preferences/package.json b/packages/@core/preferences/package.json index 828370fde..726b473db 100644 --- a/packages/@core/preferences/package.json +++ b/packages/@core/preferences/package.json @@ -1,6 +1,6 @@ { "name": "@vben-core/preferences", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/packages/@core/preferences/src/config.ts b/packages/@core/preferences/src/config.ts index 70d15c3e3..835eed557 100644 --- a/packages/@core/preferences/src/config.ts +++ b/packages/@core/preferences/src/config.ts @@ -62,6 +62,7 @@ const defaultPreferences: Preferences = { logo: { enable: true, + fit: 'contain', source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp', }, navigation: { diff --git a/packages/@core/preferences/src/types.ts b/packages/@core/preferences/src/types.ts index cc0fefe31..e640edb5c 100644 --- a/packages/@core/preferences/src/types.ts +++ b/packages/@core/preferences/src/types.ts @@ -134,6 +134,8 @@ interface HeaderPreferences { interface LogoPreferences { /** logo是否可见 */ enable: boolean; + /** logo图片适应方式 */ + fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; /** logo地址 */ source: string; } diff --git a/packages/@core/ui-kit/form-ui/package.json b/packages/@core/ui-kit/form-ui/package.json index 651b3dc54..36ae1678f 100644 --- a/packages/@core/ui-kit/form-ui/package.json +++ b/packages/@core/ui-kit/form-ui/package.json @@ -1,6 +1,6 @@ { "name": "@vben-core/form-ui", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/packages/@core/ui-kit/form-ui/src/components/form-actions.vue b/packages/@core/ui-kit/form-ui/src/components/form-actions.vue index c5a3f9c2f..189755960 100644 --- a/packages/@core/ui-kit/form-ui/src/components/form-actions.vue +++ b/packages/@core/ui-kit/form-ui/src/components/form-actions.vue @@ -48,13 +48,18 @@ const queryFormStyle = computed(() => { async function handleSubmit(e: Event) { e?.preventDefault(); e?.stopPropagation(); - const { valid } = await form.validate(); + const props = unref(rootProps); + if (!props.formApi) { + return; + } + + const { valid } = await props.formApi.validate(); if (!valid) { return; } - const values = toRaw(await unref(rootProps).formApi?.getValues()); - await unref(rootProps).handleSubmit?.(values); + const values = toRaw(await props.formApi.getValues()); + await props.handleSubmit?.(values); } async function handleReset(e: Event) { diff --git a/packages/@core/ui-kit/form-ui/src/form-api.ts b/packages/@core/ui-kit/form-ui/src/form-api.ts index 0ae8ed77a..bdc44de78 100644 --- a/packages/@core/ui-kit/form-ui/src/form-api.ts +++ b/packages/@core/ui-kit/form-ui/src/form-api.ts @@ -11,7 +11,7 @@ import type { Recordable } from '@vben-core/typings'; import type { FormActions, FormSchema, VbenFormProps } from './types'; -import { toRaw } from 'vue'; +import { isRef, toRaw } from 'vue'; import { Store } from '@vben-core/shared/store'; import { @@ -39,6 +39,7 @@ function getDefaultState(): VbenFormProps { layout: 'horizontal', resetButtonOptions: {}, schema: [], + scrollToFirstError: false, showCollapseButton: false, showDefaultActions: true, submitButtonOptions: {}, @@ -100,9 +101,26 @@ export class FormApi { getFieldComponentRef( fieldName: string, ): T | undefined { - return this.componentRefMap.has(fieldName) - ? (this.componentRefMap.get(fieldName) as T) + let target = this.componentRefMap.has(fieldName) + ? (this.componentRefMap.get(fieldName) as ComponentPublicInstance) : undefined; + if ( + target && + target.$.type.name === 'AsyncComponentWrapper' && + target.$.subTree.ref + ) { + if (Array.isArray(target.$.subTree.ref)) { + if ( + target.$.subTree.ref.length > 0 && + isRef(target.$.subTree.ref[0]?.r) + ) { + target = target.$.subTree.ref[0]?.r.value as ComponentPublicInstance; + } + } else if (isRef(target.$.subTree.ref.r)) { + target = target.$.subTree.ref.r.value as ComponentPublicInstance; + } + } + return target as T; } /** @@ -236,6 +254,41 @@ export class FormApi { }); } + /** + * 滚动到第一个错误字段 + * @param errors 验证错误对象 + */ + scrollToFirstError(errors: Record | string) { + // https://github.com/logaretm/vee-validate/discussions/3835 + const firstErrorFieldName = + typeof errors === 'string' ? errors : Object.keys(errors)[0]; + + if (!firstErrorFieldName) { + return; + } + + let el = document.querySelector( + `[name="${firstErrorFieldName}"]`, + ) as HTMLElement; + + // 如果通过 name 属性找不到,尝试通过组件引用查找, 正常情况下不会走到这,怕哪天 vee-validate 改了 name 属性有个兜底的 + if (!el) { + const componentRef = this.getFieldComponentRef(firstErrorFieldName); + if (componentRef && componentRef.$el instanceof HTMLElement) { + el = componentRef.$el; + } + } + + if (el) { + // 滚动到错误字段,添加一些偏移量以确保字段完全可见 + el.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'nearest', + }); + } + } + async setFieldValue(field: string, value: any, shouldValidate?: boolean) { const form = await this.getForm(); form.setFieldValue(field, value, shouldValidate); @@ -360,14 +413,21 @@ export class FormApi { if (Object.keys(validateResult?.errors ?? {}).length > 0) { console.error('validate error', validateResult?.errors); + + if (this.state?.scrollToFirstError) { + this.scrollToFirstError(validateResult.errors); + } } return validateResult; } async validateAndSubmitForm() { const form = await this.getForm(); - const { valid } = await form.validate(); + const { valid, errors } = await form.validate(); if (!valid) { + if (this.state?.scrollToFirstError) { + this.scrollToFirstError(errors); + } return; } return await this.submitForm(); @@ -379,6 +439,10 @@ export class FormApi { if (Object.keys(validateResult?.errors ?? {}).length > 0) { console.error('validate error', validateResult?.errors); + + if (this.state?.scrollToFirstError) { + this.scrollToFirstError(fieldName); + } } return validateResult; } diff --git a/packages/@core/ui-kit/form-ui/src/types.ts b/packages/@core/ui-kit/form-ui/src/types.ts index 34312ae78..ccfe8dd89 100644 --- a/packages/@core/ui-kit/form-ui/src/types.ts +++ b/packages/@core/ui-kit/form-ui/src/types.ts @@ -387,6 +387,12 @@ export interface VbenFormProps< */ resetButtonOptions?: ActionButtonOptions; + /** + * 验证失败时是否自动滚动到第一个错误字段 + * @default false + */ + scrollToFirstError?: boolean; + /** * 是否显示默认操作按钮 * @default true diff --git a/packages/@core/ui-kit/form-ui/src/use-form-context.ts b/packages/@core/ui-kit/form-ui/src/use-form-context.ts index 60148bfba..4ef182edf 100644 --- a/packages/@core/ui-kit/form-ui/src/use-form-context.ts +++ b/packages/@core/ui-kit/form-ui/src/use-form-context.ts @@ -10,7 +10,7 @@ import { createContext } from '@vben-core/shadcn-ui'; import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils'; import { useForm } from 'vee-validate'; -import { object } from 'zod'; +import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod'; import { getDefaultsForSchema } from 'zod-defaults'; type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi }; @@ -52,7 +52,12 @@ export function useFormInitial( if (Reflect.has(item, 'defaultValue')) { set(initialValues, item.fieldName, item.defaultValue); } else if (item.rules && !isString(item.rules)) { + // 检查规则是否适合提取默认值 + const customDefaultValue = getCustomDefaultValue(item.rules); zodObject[item.fieldName] = item.rules; + if (customDefaultValue !== undefined) { + initialValues[item.fieldName] = customDefaultValue; + } } }); @@ -64,6 +69,38 @@ export function useFormInitial( } return mergeWithArrayOverride(initialValues, zodDefaults); } + // 自定义默认值提取逻辑 + function getCustomDefaultValue(rule: any): any { + if (rule instanceof ZodString) { + return ''; // 默认为空字符串 + } else if (rule instanceof ZodNumber) { + return null; // 默认为 null(避免显示 0) + } else if (rule instanceof ZodObject) { + // 递归提取嵌套对象的默认值 + const defaultValues: Record = {}; + for (const [key, valueSchema] of Object.entries(rule.shape)) { + defaultValues[key] = getCustomDefaultValue(valueSchema); + } + return defaultValues; + } else if (rule instanceof ZodIntersection) { + // 对于交集类型,从schema 提取默认值 + const leftDefaultValue = getCustomDefaultValue(rule._def.left); + const rightDefaultValue = getCustomDefaultValue(rule._def.right); + + // 如果左右两边都能提取默认值,合并它们 + if ( + typeof leftDefaultValue === 'object' && + typeof rightDefaultValue === 'object' + ) { + return { ...leftDefaultValue, ...rightDefaultValue }; + } + + // 否则优先使用左边的默认值 + return leftDefaultValue ?? rightDefaultValue; + } else { + return undefined; // 其他类型不提供默认值 + } + } return { delegatedSlots, diff --git a/packages/@core/ui-kit/layout-ui/package.json b/packages/@core/ui-kit/layout-ui/package.json index d62d18646..57a462fe1 100644 --- a/packages/@core/ui-kit/layout-ui/package.json +++ b/packages/@core/ui-kit/layout-ui/package.json @@ -1,6 +1,6 @@ { "name": "@vben-core/layout-ui", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/packages/@core/ui-kit/menu-ui/package.json b/packages/@core/ui-kit/menu-ui/package.json index 1ff5741c7..760d7646e 100644 --- a/packages/@core/ui-kit/menu-ui/package.json +++ b/packages/@core/ui-kit/menu-ui/package.json @@ -1,6 +1,6 @@ { "name": "@vben-core/menu-ui", - "version": "5.5.6", + "version": "5.5.7", "homepage": "https://github.com/vbenjs/vue-vben-admin", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { diff --git a/packages/@core/ui-kit/popup-ui/src/alert/alert.vue b/packages/@core/ui-kit/popup-ui/src/alert/alert.vue index 6223ecd75..6997235ac 100644 --- a/packages/@core/ui-kit/popup-ui/src/alert/alert.vue +++ b/packages/@core/ui-kit/popup-ui/src/alert/alert.vue @@ -34,7 +34,6 @@ const props = withDefaults(defineProps(), { bordered: true, buttonAlign: 'end', centered: true, - containerClass: 'w-[520px]', }); const emits = defineEmits(['closed', 'confirm', 'opened']); const open = defineModel('open', { default: false }); @@ -148,7 +147,7 @@ async function handleOpenChange(val: boolean) { :class=" cn( containerClass, - 'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:rounded-[var(--radius)] md:w-[520px] md:max-w-[80%]', + 'left-0 right-0 mx-auto flex max-h-[80%] flex-col p-0 duration-300 sm:w-[520px] sm:max-w-[80%] sm:rounded-[var(--radius)]', { 'border-border border': bordered, 'shadow-3xl': !bordered, diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue index ebd98fa2b..8a3c0c53f 100644 --- a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue +++ b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue @@ -1,7 +1,15 @@ @@ -53,6 +58,7 @@ withDefaults(defineProps(), { :alt="text" :src="src" :size="logoSize" + :fit="fit" class="relative rounded-none bg-transparent" />