Commit a065de4fbc8745323105ff5ba9032108dab91d24

Authored by LanceJiang
Committed by GitHub
1 parent fa5ecb09

feat: Form 自定义组件渲染 新增 opts: {disabled} 用于自定义渲染判断 示例: /comp/form/customerForm页面 (#2944)

* feat: Form 自定义组件渲染 示例: /comp/form/customerForm页面
1. 针对自定义渲染功能 FormSchema 中
render, renderColContent, renderComponentContent 新增 opts:{disabled} 扩展 帮助自定义渲染时做 条件判断、展示同步
渲染: ((renderCallbackParams) => any) ===> ((renderCallbackParams, opts) => any)
2. slot, colSlot 分别是 render, renderColContent 插槽,为方便插槽使用
slotFn 进行解构 #test={scope} scope==={...data, ...opts}

* feat: Form 自定义组件渲染 示例: /comp/form/customerForm页面

1. 针对自定义渲染功能 FormSchema 新增 [fields] 和 [defaultValueObj] 帮助
render, renderColContent 自定义渲染时 存在多个 表单字段操作(复合field 场景)
src/components/Form/src/components/FormItem.vue
... ... @@ -299,7 +299,7 @@
299 299 return <Comp {...compAttr} />;
300 300 }
301 301 const compSlot = isFunction(renderComponentContent)
302   - ? { ...renderComponentContent(unref(getValues)) }
  302 + ? { ...renderComponentContent(unref(getValues), { disabled: unref(getDisable) }) }
303 303 : {
304 304 default: () => renderComponentContent,
305 305 };
... ... @@ -333,7 +333,7 @@
333 333 const { itemProps, slot, render, field, suffix, component } = props.schema;
334 334 const { labelCol, wrapperCol } = unref(itemLabelWidthProp);
335 335 const { colon } = props.formProps;
336   -
  336 + const opts = { disabled: unref(getDisable) };
337 337 if (component === 'Divider') {
338 338 return (
339 339 <Col span={24}>
... ... @@ -343,9 +343,9 @@
343 343 } else {
344 344 const getContent = () => {
345 345 return slot
346   - ? getSlot(slots, slot, unref(getValues))
  346 + ? getSlot(slots, slot, unref(getValues), opts)
347 347 : render
348   - ? render(unref(getValues))
  348 + ? render(unref(getValues), opts)
349 349 : renderComponent();
350 350 };
351 351  
... ... @@ -391,12 +391,13 @@
391 391 const realColProps = { ...baseColProps, ...colProps };
392 392 const { isIfShow, isShow } = getShow();
393 393 const values = unref(getValues);
  394 + const opts = { disabled: unref(getDisable) };
394 395  
395 396 const getContent = () => {
396 397 return colSlot
397   - ? getSlot(slots, colSlot, values)
  398 + ? getSlot(slots, colSlot, values, opts)
398 399 : renderColContent
399   - ? renderColContent(values)
  400 + ? renderColContent(values, opts)
400 401 : renderItem();
401 402 };
402 403  
... ...
src/components/Form/src/hooks/useFormEvents.ts
... ... @@ -87,6 +87,13 @@ export function useFormEvents({
87 87  
88 88 Object.keys(formModel).forEach((key) => {
89 89 const schema = unref(getSchema).find((item) => item.field === key);
  90 + const defaultValueObj = schema?.defaultValueObj;
  91 + const fieldKeys = Object.keys(defaultValueObj || {});
  92 + if (fieldKeys.length) {
  93 + fieldKeys.map((field) => {
  94 + formModel[field] = defaultValueObj![field];
  95 + });
  96 + }
90 97 const isInput = schema?.component && defaultValueComponents.includes(schema.component);
91 98 const defaultValue = cloneDeep(defaultValueRef.value[key]);
92 99 formModel[key] = isInput ? defaultValue || '' : defaultValue;
... ... @@ -96,14 +103,17 @@ export function useFormEvents({
96 103 emit('reset', toRaw(formModel));
97 104 submitOnReset && handleSubmit();
98 105 }
99   -
  106 + // 获取表单fields
  107 + const getAllFields = () =>
  108 + unref(getSchema)
  109 + .map((item) => [...(item.fields || []), item.field])
  110 + .flat(1)
  111 + .filter(Boolean);
100 112 /**
101 113 * @description: Set form value
102 114 */
103 115 async function setFieldsValue(values: Recordable): Promise<void> {
104   - const fields = unref(getSchema)
105   - .map((item) => item.field)
106   - .filter(Boolean);
  116 + const fields = getAllFields();
107 117  
108 118 // key 支持 a.b.c 的嵌套写法
109 119 const delimiter = '.';
... ... @@ -340,8 +350,14 @@ export function useFormEvents({
340 350 return unref(formElRef)?.validateFields(nameList);
341 351 }
342 352  
343   - async function validate(nameList?: NamePath[] | undefined) {
344   - return await unref(formElRef)?.validate(nameList);
  353 + async function validate(nameList?: NamePath[] | false | undefined) {
  354 + let _nameList: any;
  355 + if (nameList === undefined) {
  356 + _nameList = getAllFields();
  357 + } else {
  358 + _nameList = nameList === Array.isArray(nameList) ? nameList : undefined;
  359 + }
  360 + return await unref(formElRef)?.validate(_nameList);
345 361 }
346 362  
347 363 async function clearValidate(name?: string | string[]) {
... ...
src/components/Form/src/hooks/useFormValues.ts
... ... @@ -127,7 +127,16 @@ export function useFormValues({
127 127 const schemas = unref(getSchema);
128 128 const obj: Recordable = {};
129 129 schemas.forEach((item) => {
130   - const { defaultValue } = item;
  130 + const { defaultValue, defaultValueObj } = item;
  131 + const fieldKeys = Object.keys(defaultValueObj || {});
  132 + if (fieldKeys.length) {
  133 + fieldKeys.map((field) => {
  134 + obj[field] = defaultValueObj![field];
  135 + if (formModel[field] === undefined) {
  136 + formModel[field] = defaultValueObj![field];
  137 + }
  138 + });
  139 + }
131 140 if (!isNullOrUnDef(defaultValue)) {
132 141 obj[item.field] = defaultValue;
133 142  
... ...
src/components/Form/src/types/form.ts
... ... @@ -39,7 +39,7 @@ export interface FormActionType {
39 39 first?: boolean | undefined,
40 40 ) => Promise<void>;
41 41 validateFields: (nameList?: NamePath[]) => Promise<any>;
42   - validate: (nameList?: NamePath[]) => Promise<any>;
  42 + validate: (nameList?: NamePath[] | false) => Promise<any>;
43 43 scrollToField: (name: NamePath, options?: ScrollOptions) => Promise<void>;
44 44 }
45 45  
... ... @@ -123,15 +123,21 @@ export interface FormProps {
123 123 transformDateFunc?: (date: any) => string;
124 124 colon?: boolean;
125 125 }
  126 +export type RenderOpts = {
  127 + disabled: boolean;
  128 + [key: string]: any;
  129 +};
126 130 export interface FormSchema {
127 131 // Field name
128 132 field: string;
  133 + // Extra Fields name[]
  134 + fields?: string[];
129 135 // Event name triggered by internal value change, default change
130 136 changeEvent?: string;
131 137 // Variable name bound to v-model Default value
132 138 valueField?: string;
133 139 // Label name
134   - label: string | VNode;
  140 + label?: string | VNode;
135 141 // Auxiliary text
136 142 subLabel?: string;
137 143 // Help text on the right side of the text
... ... @@ -175,6 +181,9 @@ export interface FormSchema {
175 181 // 默认值
176 182 defaultValue?: any;
177 183  
  184 + // 额外默认值数组对象
  185 + defaultValueObj?: { [key: string]: any };
  186 +
178 187 // 是否自动处理与时间相关组件的默认值
179 188 isHandleDateDefaultValue?: boolean;
180 189  
... ... @@ -188,13 +197,19 @@ export interface FormSchema {
188 197 show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
189 198  
190 199 // Render the content in the form-item tag
191   - render?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string;
  200 + render?: (
  201 + renderCallbackParams: RenderCallbackParams,
  202 + opts: RenderOpts,
  203 + ) => VNode | VNode[] | string;
192 204  
193 205 // Rendering col content requires outer wrapper form-item
194   - renderColContent?: (renderCallbackParams: RenderCallbackParams) => VNode | VNode[] | string;
  206 + renderColContent?: (
  207 + renderCallbackParams: RenderCallbackParams,
  208 + opts: RenderOpts,
  209 + ) => VNode | VNode[] | string;
195 210  
196 211 renderComponentContent?:
197   - | ((renderCallbackParams: RenderCallbackParams) => any)
  212 + | ((renderCallbackParams: RenderCallbackParams, opts: RenderOpts) => any)
198 213 | VNode
199 214 | VNode[]
200 215 | string;
... ...
src/utils/helper/tsxHelper.tsx
1 1 import { Slots } from 'vue';
2 2 import { isFunction } from '/@/utils/is';
  3 +import { RenderOpts } from '/@/components/Form';
3 4  
4 5 /**
5 6 * @description: Get slot to prevent empty error
6 7 */
7   -export function getSlot(slots: Slots, slot = 'default', data?: any) {
  8 +export function getSlot(slots: Slots, slot = 'default', data?: any, opts?: RenderOpts) {
8 9 if (!slots || !Reflect.has(slots, slot)) {
9 10 return null;
10 11 }
... ... @@ -14,7 +15,8 @@ export function getSlot(slots: Slots, slot = &#39;default&#39;, data?: any) {
14 15 }
15 16 const slotFn = slots[slot];
16 17 if (!slotFn) return null;
17   - return slotFn(data);
  18 + const params = { ...data, ...opts };
  19 + return slotFn(params);
18 20 }
19 21  
20 22 /**
... ...
src/views/demo/form/CustomerForm.vue
... ... @@ -2,21 +2,42 @@
2 2 <PageWrapper title="自定义组件示例">
3 3 <CollapseContainer title="自定义表单">
4 4 <BasicForm @register="register" @submit="handleSubmit">
5   - <template #f3="{ model, field }">
6   - <a-input v-model:value="model[field]" placeholder="自定义slot" />
  5 + <template #f3="{ model, field, disabled }">
  6 + <a-input v-model:value="model[field]" :disabled="disabled" placeholder="自定义slot" />
  7 + </template>
  8 + <template #colSlot_field5="{ model, field, disabled }">
  9 + <FormItem :name="field" label="自定义colSlot" :rules="[{ required: true }]">
  10 + <a-input
  11 + v-model:value="model[field]"
  12 + :disabled="disabled"
  13 + placeholder="自定义colSlot"
  14 + />
  15 + </FormItem>
7 16 </template>
8 17 </BasicForm>
9 18 </CollapseContainer>
10 19 </PageWrapper>
11 20 </template>
12   -<script lang="ts">
  21 +<script lang="tsx">
13 22 import { defineComponent, h } from 'vue';
14 23 import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
15 24 import { CollapseContainer } from '/@/components/Container/index';
16 25 import { useMessage } from '/@/hooks/web/useMessage';
17   - import { Input } from 'ant-design-vue';
  26 + import { Input, FormItem, Select } from 'ant-design-vue';
18 27 import { PageWrapper } from '/@/components/Page';
19 28  
  29 + const custom_typeKey2typeValueRules = (model) => {
  30 + return [
  31 + {
  32 + required: true,
  33 + validator: (rule, value, callback) => {
  34 + if (!model.typeKey) return callback('请选择类型');
  35 + if (!model.typeValue) return callback('请输入数据');
  36 + callback();
  37 + },
  38 + },
  39 + ];
  40 + };
20 41 const schemas: FormSchema[] = [
21 42 {
22 43 field: 'field1',
... ... @@ -25,14 +46,18 @@
25 46 colProps: {
26 47 span: 8,
27 48 },
  49 + dynamicDisabled: ({ values }) => {
  50 + return !!values.field_disabled;
  51 + },
28 52 rules: [{ required: true }],
29   - render: ({ model, field }) => {
  53 + render: ({ model, field }, { disabled }) => {
30 54 return h(Input, {
31 55 placeholder: '请输入',
32 56 value: model[field],
33   - onChange: (e: ChangeEvent) => {
  57 + onChange: (e) => {
34 58 model[field] = e.target.value;
35 59 },
  60 + disabled,
36 61 });
37 62 },
38 63 },
... ... @@ -43,10 +68,13 @@
43 68 colProps: {
44 69 span: 8,
45 70 },
  71 + dynamicDisabled: ({ values }) => {
  72 + return !!values.field_disabled;
  73 + },
46 74 rules: [{ required: true }],
47   - renderComponentContent: () => {
  75 + renderComponentContent: (_, { disabled }) => {
48 76 return {
49   - suffix: () => 'suffix',
  77 + suffix: () => (disabled ? 'suffix_disabled' : 'suffix_default'),
50 78 };
51 79 },
52 80 },
... ... @@ -58,11 +86,136 @@
58 86 colProps: {
59 87 span: 8,
60 88 },
  89 + dynamicDisabled: ({ values }) => {
  90 + return !!values.field_disabled;
  91 + },
61 92 rules: [{ required: true }],
62 93 },
  94 + {
  95 + field: 'field4',
  96 + component: 'Input',
  97 + // label: 'renderColContent渲染',
  98 + /**!!!renderColContent 没有FormItem 包裹, 若想要 Form 提交需要带上数据须 <FormItem name={}></FormItem> 包裹: 示例如下*/
  99 + renderColContent({ model, field }, { disabled }) {
  100 + return (
  101 + <FormItem name="field4" label="renderColContent渲染" rules={[{ required: true }]}>
  102 + <Input placeholder="请输入" v-model:value={model[field]} disabled={disabled}></Input>
  103 + </FormItem>
  104 + );
  105 + },
  106 + colProps: {
  107 + span: 8,
  108 + },
  109 + dynamicDisabled: ({ values }) => {
  110 + return !!values.field_disabled;
  111 + },
  112 + },
  113 + {
  114 + field: 'field5',
  115 + component: 'Input',
  116 + label: '自定义colSlot',
  117 + /**!!!renderColContent 没有FormItem 包裹, 若想要 Form 提交需要带上数据须 <FormItem name={}></FormItem> 包裹: 示例如下*/
  118 + colSlot: 'colSlot_field5',
  119 + colProps: {
  120 + span: 8,
  121 + },
  122 + dynamicDisabled: ({ values }) => {
  123 + return !!values.field_disabled;
  124 + },
  125 + },
  126 + // 复合field 场景 自定义表单控件 一个控件包含多个表单录入 示例: 选择+输入
  127 + {
  128 + required: true,
  129 + field: 'typeKey2',
  130 + defaultValue: '测试类型',
  131 + fields: ['typeValue2'],
  132 + defaultValueObj: { typeValue2: '默认测试_文字' },
  133 + component: 'Input',
  134 + label: '复合field render',
  135 + render({ model, field }, { disabled }) {
  136 + return (
  137 + <Input.Group compact>
  138 + <Select
  139 + disabled={disabled}
  140 + style="width: 120px"
  141 + allowClear
  142 + v-model:value={model[field]}
  143 + >
  144 + <Select.Option value="测试类型">测试类型</Select.Option>
  145 + <Select.Option value="测试名称">测试名称</Select.Option>
  146 + </Select>
  147 + <FormItem
  148 + name="typeValue2"
  149 + style="width: calc(100% - 120px); margin-left: -1px; border-right: 0; margin-bottom: 0;"
  150 + rules={[{ required: true }]}
  151 + >
  152 + <Input placeholder="请输入" v-model:value={model['typeValue2']} disabled={disabled} />
  153 + </FormItem>
  154 + </Input.Group>
  155 + );
  156 + },
  157 + colProps: {
  158 + span: 8,
  159 + },
  160 + dynamicDisabled: ({ values }) => {
  161 + return !!values.field_disabled;
  162 + },
  163 + },
  164 + // 复合field 场景 自定义表单控件 一个控件包含多个表单录入 示例: 选择+输入
  165 + {
  166 + field: 'typeKey',
  167 + defaultValue: '公司名称',
  168 + fields: ['typeValue'],
  169 + defaultValueObj: { typeValue: '默认文字' },
  170 + component: 'Input',
  171 + // label: 'renderColContent渲染',
  172 + /**!!!renderColContent 没有FormItem 包裹, 若想要 Form 提交需要带上数据须 <FormItem name={}></FormItem> 包裹: 示例如下*/
  173 + renderColContent({ model, field }, { disabled }) {
  174 + return (
  175 + <FormItem
  176 + name="typeKey"
  177 + label="复合field renderColContent"
  178 + rules={custom_typeKey2typeValueRules(model)}
  179 + >
  180 + <Input.Group compact>
  181 + <Select
  182 + allowClear
  183 + disabled={disabled}
  184 + style="width: 120px"
  185 + v-model:value={model[field]}
  186 + >
  187 + <Select.Option value="公司名称">公司名称</Select.Option>
  188 + <Select.Option value="产品名称">产品名称</Select.Option>
  189 + </Select>
  190 + <Input
  191 + style="width: calc(100% - 120px); margin-left: -1px;"
  192 + placeholder="请输入"
  193 + v-model:value={model['typeValue']}
  194 + disabled={disabled}
  195 + />
  196 + </Input.Group>
  197 + </FormItem>
  198 + );
  199 + },
  200 + colProps: {
  201 + span: 16,
  202 + },
  203 + dynamicDisabled: ({ values }) => {
  204 + return !!values.field_disabled;
  205 + },
  206 + },
  207 + {
  208 + field: 'field_disabled',
  209 + component: 'Switch',
  210 + label: '是否禁用 编辑字段',
  211 + colProps: {
  212 + span: 8,
  213 + },
  214 + labelWidth: 200,
  215 + },
63 216 ];
64 217 export default defineComponent({
65   - components: { BasicForm, CollapseContainer, PageWrapper, [Input.name]: Input },
  218 + components: { BasicForm, CollapseContainer, PageWrapper, [Input.name]: Input, FormItem },
66 219 setup() {
67 220 const { createMessage } = useMessage();
68 221 const [register, { setProps }] = useForm({
... ... @@ -76,6 +229,7 @@
76 229 register,
77 230 schemas,
78 231 handleSubmit: (values: any) => {
  232 + console.log('submit values', values);
79 233 createMessage.success('click search,values:' + JSON.stringify(values));
80 234 },
81 235 setProps,
... ...