diff --git a/apps/web-antd/src/adapter/form.ts b/apps/web-antd/src/adapter/form.ts index b46f9b21..3a9352fb 100644 --- a/apps/web-antd/src/adapter/form.ts +++ b/apps/web-antd/src/adapter/form.ts @@ -4,6 +4,7 @@ import type { VbenFormProps, } from '@vben/common-ui'; +import type { Component, SetupContext } from 'vue'; import { h } from 'vue'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; @@ -61,6 +62,16 @@ export type FormComponentType = | 'Upload' | BaseFormComponentType; +const withDefaultPlaceholder = ( + component: T, + type: 'input' | 'select', +) => { + return (props: any, { attrs, slots }: Omit) => { + const placeholder = props?.placeholder || $t(`placeholder.${type}`); + return h(component, { ...props, ...attrs, placeholder }, slots); + }; +}; + // 初始化表单组件,并注册到form组件内部 setupVbenForm({ components: { @@ -77,21 +88,21 @@ setupVbenForm({ return h(Button, { ...props, attrs, type: 'primary' }, slots); }, Divider, - Input, - InputNumber, - InputPassword, - Mentions, + Input: withDefaultPlaceholder(Input, 'input'), + InputNumber: withDefaultPlaceholder(InputNumber, 'input'), + InputPassword: withDefaultPlaceholder(InputPassword, 'input'), + Mentions: withDefaultPlaceholder(Mentions, 'input'), Radio, RadioGroup, RangePicker, Rate, RichTextarea, - Select, + Select: withDefaultPlaceholder(Select, 'select'), Space, Switch, - Textarea, + Textarea: withDefaultPlaceholder(Textarea, 'input'), TimePicker, - TreeSelect, + TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), Upload, }, config: { diff --git a/apps/web-ele/src/adapter/form.ts b/apps/web-ele/src/adapter/form.ts index b4fa0a06..36e20033 100644 --- a/apps/web-ele/src/adapter/form.ts +++ b/apps/web-ele/src/adapter/form.ts @@ -4,6 +4,7 @@ import type { VbenFormProps, } from '@vben/common-ui'; +import type { Component, SetupContext } from 'vue'; import { h } from 'vue'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; @@ -42,6 +43,16 @@ export type FormComponentType = | 'Upload' | BaseFormComponentType; +const withDefaultPlaceholder = ( + component: T, + type: 'input' | 'select', +) => { + return (props: any, { attrs, slots }: Omit) => { + const placeholder = props?.placeholder || $t(`placeholder.${type}`); + return h(component, { ...props, ...attrs, placeholder }, slots); + }; +}; + // 初始化表单组件,并注册到form组件内部 setupVbenForm({ components: { @@ -56,14 +67,14 @@ setupVbenForm({ return h(ElButton, { ...props, attrs, type: 'primary' }, slots); }, Divider: ElDivider, - Input: ElInput, - InputNumber: ElInputNumber, + Input: withDefaultPlaceholder(ElInput, 'input'), + InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'), RadioGroup: ElRadioGroup, - Select: ElSelect, + Select: withDefaultPlaceholder(ElSelect, 'select'), Space: ElSpace, Switch: ElSwitch, TimePicker: ElTimePicker, - TreeSelect: ElTreeSelect, + TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'), Upload: ElUpload, }, config: { diff --git a/apps/web-naive/src/adapter/form.ts b/apps/web-naive/src/adapter/form.ts index 552919af..1c051b4a 100644 --- a/apps/web-naive/src/adapter/form.ts +++ b/apps/web-naive/src/adapter/form.ts @@ -4,6 +4,7 @@ import type { VbenFormProps, } from '@vben/common-ui'; +import type { Component, SetupContext } from 'vue'; import { h } from 'vue'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; @@ -43,6 +44,16 @@ export type FormComponentType = | 'Upload' | BaseFormComponentType; +const withDefaultPlaceholder = ( + component: T, + type: 'input' | 'select', +) => { + return (props: any, { attrs, slots }: Omit) => { + const placeholder = props?.placeholder || $t(`placeholder.${type}`); + return h(component, { ...props, ...attrs, placeholder }, slots); + }; +}; + // 初始化表单组件,并注册到form组件内部 setupVbenForm({ components: { @@ -62,17 +73,18 @@ setupVbenForm({ ); }, Divider: NDivider, - Input: NInput, - InputNumber: NInputNumber, + Input: withDefaultPlaceholder(NInput, 'input'), + InputNumber: withDefaultPlaceholder(NInputNumber, 'input'), RadioGroup: NRadioGroup, - Select: NSelect, + Select: withDefaultPlaceholder(NSelect, 'select'), Space: NSpace, Switch: NSwitch, TimePicker: NTimePicker, - TreeSelect: NTreeSelect, + TreeSelect: withDefaultPlaceholder(NTreeSelect, 'select'), Upload: NUpload, }, config: { + disabledOnChangeListener: true, baseModelPropName: 'value', modelPropNameMap: { Checkbox: 'checked', diff --git a/apps/web-naive/src/router/routes/modules/demos.ts b/apps/web-naive/src/router/routes/modules/demos.ts index 5a2dc12a..cac10a9b 100644 --- a/apps/web-naive/src/router/routes/modules/demos.ts +++ b/apps/web-naive/src/router/routes/modules/demos.ts @@ -25,7 +25,6 @@ const routes: RouteRecordRaw[] = [ }, { meta: { - icon: 'mdi:shield-key-outline', title: $t('page.demos.table'), }, name: 'Table', diff --git a/docs/src/components/common-ui/vben-form.md b/docs/src/components/common-ui/vben-form.md index 0206dbee..5b4c12ce 100644 --- a/docs/src/components/common-ui/vben-form.md +++ b/docs/src/components/common-ui/vben-form.md @@ -31,6 +31,7 @@ import type { VbenFormProps, } from '@vben/common-ui'; +import type { Component, SetupContext } from 'vue'; import { h } from 'vue'; import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui'; @@ -84,6 +85,16 @@ export type FormComponentType = | 'Upload' | BaseFormComponentType; +const withDefaultPlaceholder = ( + component: T, + type: 'input' | 'select', +) => { + return (props: any, { attrs, slots }: Omit) => { + const placeholder = props?.placeholder || $t(`placeholder.${type}`); + return h(component, { ...props, ...attrs, placeholder }, slots); + }; +}; + // 初始化表单组件,并注册到form组件内部 setupVbenForm({ components: { @@ -100,26 +111,27 @@ setupVbenForm({ return h(Button, { ...props, attrs, type: 'primary' }, slots); }, Divider, - Input, - InputNumber, - InputPassword, - Mentions, + Input: withDefaultPlaceholder(Input, 'input'), + InputNumber: withDefaultPlaceholder(InputNumber, 'input'), + InputPassword: withDefaultPlaceholder(InputPassword, 'input'), + Mentions: withDefaultPlaceholder(Mentions, 'input'), Radio, RadioGroup, RangePicker, Rate, - Select, + Select: withDefaultPlaceholder(Select, 'select'), Space, Switch, - Textarea, + Textarea: withDefaultPlaceholder(Textarea, 'input'), TimePicker, - TreeSelect, + TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), Upload, }, config: { + // 是否禁用onChange事件监听,naive ui组件库默认不需要监听onChange事件,否则会在控制台报错 + disabledOnChangeListener: true, // ant design vue组件库默认都是 v-model:value baseModelPropName: 'value', - // 一些组件是 v-model:checked 或者 v-model:fileList modelPropNameMap: { Checkbox: 'checked', diff --git a/package.json b/package.json index cef174e3..940fbfba 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "node": ">=20.10.0", "pnpm": ">=9.5.0" }, - "packageManager": "pnpm@9.11.0", + "packageManager": "pnpm@9.12.0", "pnpm": { "peerDependencyRules": { "allowedVersions": { diff --git a/packages/@core/ui-kit/form-ui/src/config.ts b/packages/@core/ui-kit/form-ui/src/config.ts index 3e85148d..aec2642d 100644 --- a/packages/@core/ui-kit/form-ui/src/config.ts +++ b/packages/@core/ui-kit/form-ui/src/config.ts @@ -1,4 +1,8 @@ -import type { BaseFormComponentType, VbenFormAdapterOptions } from './types'; +import type { + BaseFormComponentType, + FormCommonConfig, + VbenFormAdapterOptions, +} from './types'; import type { Component } from 'vue'; import { h } from 'vue'; @@ -17,6 +21,8 @@ import { defineRule } from 'vee-validate'; const DEFAULT_MODEL_PROP_NAME = 'modelValue'; +export const DEFAULT_FORM_COMMON_CONFIG: FormCommonConfig = {}; + export const COMPONENT_MAP: Record = { DefaultResetActionButton: h(VbenButton, { size: 'sm', variant: 'outline' }), DefaultSubmitActionButton: h(VbenButton, { size: 'sm', variant: 'default' }), @@ -39,6 +45,9 @@ export function setupVbenForm< >(options: VbenFormAdapterOptions) { const { components, config, defineRules } = options; + DEFAULT_FORM_COMMON_CONFIG.disabledOnChangeListener = + config?.disabledOnChangeListener ?? false; + if (defineRules) { for (const key of Object.keys(defineRules)) { defineRule(key, defineRules[key as never]); 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 4aeb7264..c116d49c 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 } from 'vue'; +import { computed, nextTick, useTemplateRef, watch } from 'vue'; import { FormControl, @@ -32,6 +32,7 @@ const { dependencies, description, disabled, + disabledOnChangeListener, fieldName, formFieldProps, label, @@ -49,6 +50,7 @@ const { componentBindEventMap, componentMap, isVertical } = useFormContext(); const formRenderProps = injectRenderFormProps(); const values = useFormValues(); const errors = useFieldError(fieldName); +const fieldComponentRef = useTemplateRef('fieldComponentRef'); const formApi = formRenderProps.form; const isInValid = computed(() => errors.value?.length > 0); @@ -156,6 +158,18 @@ const computedProps = computed(() => { }; }); +watch( + () => computedProps.value?.autofocus, + (value) => { + if (value === true) { + nextTick(() => { + autofocus(); + }); + } + }, + { immediate: true }, +); + const shouldDisabled = computed(() => { return isDisabled.value || disabled || computedProps.value?.disabled; }); @@ -177,7 +191,7 @@ const fieldProps = computed(() => { keepValue: true, label, ...(rules ? { rules } : {}), - ...formFieldProps, + ...(formFieldProps as Record), }; }); @@ -200,15 +214,16 @@ function fieldBindEvent(slotProps: Record) { return { [`onUpdate:${bindEventField}`]: handler, [bindEventField]: value, - onChange: (e: Record) => { - const shouldUnwrap = isEventObjectLike(e); - const onChange = slotProps?.componentField?.onChange; - if (!shouldUnwrap) { - return onChange?.(e); - } - - return onChange?.(e?.target?.[bindEventField] ?? e); - }, + onChange: disabledOnChangeListener + ? undefined + : (e: Record) => { + const shouldUnwrap = isEventObjectLike(e); + const onChange = slotProps?.componentField?.onChange; + if (!shouldUnwrap) { + return onChange?.(e); + } + return onChange?.(e?.target?.[bindEventField] ?? e); + }, onInput: () => {}, }; } @@ -226,6 +241,17 @@ function createComponentProps(slotProps: Record) { return binds; } + +function autofocus() { + if ( + fieldComponentRef.value && + isFunction(fieldComponentRef.value.focus) && + // 检查当前是否有元素被聚焦 + document.activeElement !== fieldComponentRef.value + ) { + fieldComponentRef.value?.focus?.(); + } +}