diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 80f1ced6..10ab89d5 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -3,11 +3,12 @@ * 可用于 vben-form、vben-modal、vben-drawer 等组件使用, */ -import type { Component, SetupContext } from 'vue'; +import type { Component } from 'vue'; import type { BaseFormComponentType } from '@vben/common-ui'; +import type { Recordable } from '@vben/types'; -import { h } from 'vue'; +import { defineComponent, getCurrentInstance, h, ref } from 'vue'; import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui'; import { $t } from '@vben/locales'; @@ -44,10 +45,30 @@ const withDefaultPlaceholder = ( component: T, type: 'input' | 'select', ) => { - return (props: any, { attrs, slots }: Omit) => { - const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`); - return h(component, { ...props, ...attrs, placeholder }, slots); - }; + return defineComponent({ + inheritAttrs: false, + name: component.name, + setup: (props: any, { attrs, expose, slots }) => { + const placeholder = + props?.placeholder || + attrs?.placeholder || + $t(`ui.placeholder.${type}`); + // 透传组件暴露的方法 + const innerRef = ref(); + const publicApi: Recordable = {}; + expose(publicApi); + const instance = getCurrentInstance(); + instance?.proxy?.$nextTick(() => { + for (const key in innerRef.value) { + if (typeof innerRef.value[key] === 'function') { + publicApi[key] = innerRef.value[key]; + } + } + }); + return () => + h(component, { ...props, ...attrs, placeholder, ref: innerRef }, slots); + }, + }); }; // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 diff --git a/docs/src/components/common-ui/vben-form.md b/docs/src/components/common-ui/vben-form.md index 9babb67a..a18ee42d 100644 --- a/docs/src/components/common-ui/vben-form.md +++ b/docs/src/components/common-ui/vben-form.md @@ -279,22 +279,24 @@ const [Form, formApi] = useVbenForm({ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单的方法。 -| 方法名 | 描述 | 类型 | -| --- | --- | --- | -| submitForm | 提交表单 | `(e:Event)=>Promise>` | -| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise>` | -| resetForm | 重置表单 | `()=>Promise` | -| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record, filterFields?: boolean, shouldValidate?: boolean) => Promise` | -| getValues | 获取表单值 | `(fields:Record,shouldValidate: boolean = false)=>Promise` | -| validate | 表单校验 | `()=>Promise` | -| validateField | 校验指定字段 | `(fieldName: string)=>Promise>` | -| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise` | -| resetValidate | 重置表单校验 | `()=>Promise` | -| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` | -| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise` | -| setState | 设置组件状态(props) | `(stateOrFn:\| ((prev: VbenFormProps) => Partial)\| Partial)=>Promise` | -| getState | 获取组件状态(props) | `()=>Promise` | -| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - | +| 方法名 | 描述 | 类型 | 版本号 | +| --- | --- | --- | --- | +| submitForm | 提交表单 | `(e:Event)=>Promise>` | - | +| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise>` | - | +| resetForm | 重置表单 | `()=>Promise` | - | +| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record, filterFields?: boolean, shouldValidate?: boolean) => Promise` | - | +| getValues | 获取表单值 | `(fields:Record,shouldValidate: boolean = false)=>Promise` | - | +| validate | 表单校验 | `()=>Promise` | - | +| validateField | 校验指定字段 | `(fieldName: string)=>Promise>` | - | +| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise` | - | +| resetValidate | 重置表单校验 | `()=>Promise` | - | +| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` | - | +| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise` | - | +| setState | 设置组件状态(props) | `(stateOrFn:\| ((prev: VbenFormProps) => Partial)\| Partial)=>Promise` | - | +| getState | 获取组件状态(props) | `()=>Promise` | - | +| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - | - | +| getFieldComponentRef | 获取指定字段的组件实例 | `(fieldName: string)=>T` | >5.5.3 | +| getFocusedField | 获取当前已获得焦点的字段 | `()=>string\|undefined` | >5.5.3 | ## Props diff --git a/docs/src/components/common-ui/vben-vxe-table.md b/docs/src/components/common-ui/vben-vxe-table.md index ff19b163..011f4f5d 100644 --- a/docs/src/components/common-ui/vben-vxe-table.md +++ b/docs/src/components/common-ui/vben-vxe-table.md @@ -165,7 +165,7 @@ vxeUI.renderer.add('CellLink', { **表单搜索** 部分采用了`Vben Form 表单`,参考 [Vben Form 表单文档](/components/common-ui/vben-form)。 -当启用了表单搜索时,可以在toolbarConfig中配置`search`为`true`来让表格在工具栏区域显示一个搜索表单控制按钮。 +当启用了表单搜索时,可以在toolbarConfig中配置`search`为`true`来让表格在工具栏区域显示一个搜索表单控制按钮。表格的所有以`form-`开头的命名插槽都会被传递给搜索表单。 @@ -250,3 +250,9 @@ useVbenVxeGrid 返回的第二个参数,是一个对象,包含了一些表 | toolbar-actions | 工具栏左侧部分(表格标题附近) | | toolbar-tools | 工具栏右侧部分(vxeTable原生工具按钮的左侧) | | table-title | 表格标题插槽 | + +::: info 搜索表单的插槽 + +对于使用了搜索表单的表格来说,所有以`form-`开头的命名插槽都会传递给表单。 + +::: diff --git a/package.json b/package.json index ac65a86b..5777bd48 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "node": ">=20.10.0", "pnpm": ">=9.12.0" }, - "packageManager": "pnpm@9.15.6", + "packageManager": "pnpm@9.15.7", "pnpm": { "peerDependencyRules": { "allowedVersions": { 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 db09d7c7..6a440368 100644 --- a/packages/@core/ui-kit/form-ui/src/form-api.ts +++ b/packages/@core/ui-kit/form-ui/src/form-api.ts @@ -5,6 +5,8 @@ import type { ValidationOptions, } from 'vee-validate'; +import type { ComponentPublicInstance } from 'vue'; + import type { Recordable } from '@vben-core/typings'; import type { FormActions, FormSchema, VbenFormProps } from './types'; @@ -56,6 +58,11 @@ export class FormApi { public store: Store; + /** + * 组件实例映射 + */ + private componentRefMap: Map = new Map(); + // 最后一次点击提交时的表单值 private latestSubmissionValues: null | Recordable = null; @@ -85,6 +92,46 @@ export class FormApi { bindMethods(this); } + /** + * 获取字段组件实例 + * @param fieldName 字段名 + * @returns 组件实例 + */ + getFieldComponentRef( + fieldName: string, + ): T | undefined { + return this.componentRefMap.has(fieldName) + ? (this.componentRefMap.get(fieldName) as T) + : undefined; + } + + /** + * 获取当前聚焦的字段,如果没有聚焦的字段则返回undefined + */ + getFocusedField() { + for (const fieldName of this.componentRefMap.keys()) { + const ref = this.getFieldComponentRef(fieldName); + if (ref) { + let el: HTMLElement | null = null; + if (ref instanceof HTMLElement) { + el = ref; + } else if (ref.$el instanceof HTMLElement) { + el = ref.$el; + } + if (!el) { + continue; + } + if ( + el === document.activeElement || + el.contains(document.activeElement) + ) { + return fieldName; + } + } + } + return undefined; + } + getLatestSubmissionValues() { return this.latestSubmissionValues || {}; } @@ -143,13 +190,14 @@ export class FormApi { return proxy; } - mount(formActions: FormActions) { + mount(formActions: FormActions, componentRefMap: Map) { if (!this.isMounted) { Object.assign(this.form, formActions); this.stateHandler.setConditionTrue(); this.setLatestSubmissionValues({ ...toRaw(this.handleRangeTimeValue(this.form.values)), }); + this.componentRefMap = componentRefMap; this.isMounted = true; } } diff --git a/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue b/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue index 191c3019..873afe1d 100644 --- a/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue +++ b/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue @@ -3,7 +3,7 @@ import type { ZodType } from 'zod'; import type { FormSchema, MaybeComponentProps } from '../types'; -import { computed, nextTick, useTemplateRef, watch } from 'vue'; +import { computed, nextTick, onUnmounted, useTemplateRef, watch } from 'vue'; import { FormControl, @@ -18,6 +18,7 @@ import { cn, isFunction, isObject, isString } from '@vben-core/shared/utils'; import { toTypedSchema } from '@vee-validate/zod'; import { useFieldError, useFormValues } from 'vee-validate'; +import { injectComponentRefMap } from '../use-form-context'; import { injectRenderFormProps, useFormContext } from './context'; import useDependencies from './dependencies'; import FormLabel from './form-label.vue'; @@ -267,6 +268,15 @@ function autofocus() { fieldComponentRef.value?.focus?.(); } } +const componentRefMap = injectComponentRefMap(); +watch(fieldComponentRef, (componentRef) => { + componentRefMap?.set(fieldName, componentRef); +}); +onUnmounted(() => { + if (componentRefMap?.has(fieldName)) { + componentRefMap.delete(fieldName); + } +});