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,7 +299,7 @@
299 return <Comp {...compAttr} />; 299 return <Comp {...compAttr} />;
300 } 300 }
301 const compSlot = isFunction(renderComponentContent) 301 const compSlot = isFunction(renderComponentContent)
302 - ? { ...renderComponentContent(unref(getValues)) } 302 + ? { ...renderComponentContent(unref(getValues), { disabled: unref(getDisable) }) }
303 : { 303 : {
304 default: () => renderComponentContent, 304 default: () => renderComponentContent,
305 }; 305 };
@@ -333,7 +333,7 @@ @@ -333,7 +333,7 @@
333 const { itemProps, slot, render, field, suffix, component } = props.schema; 333 const { itemProps, slot, render, field, suffix, component } = props.schema;
334 const { labelCol, wrapperCol } = unref(itemLabelWidthProp); 334 const { labelCol, wrapperCol } = unref(itemLabelWidthProp);
335 const { colon } = props.formProps; 335 const { colon } = props.formProps;
336 - 336 + const opts = { disabled: unref(getDisable) };
337 if (component === 'Divider') { 337 if (component === 'Divider') {
338 return ( 338 return (
339 <Col span={24}> 339 <Col span={24}>
@@ -343,9 +343,9 @@ @@ -343,9 +343,9 @@
343 } else { 343 } else {
344 const getContent = () => { 344 const getContent = () => {
345 return slot 345 return slot
346 - ? getSlot(slots, slot, unref(getValues)) 346 + ? getSlot(slots, slot, unref(getValues), opts)
347 : render 347 : render
348 - ? render(unref(getValues)) 348 + ? render(unref(getValues), opts)
349 : renderComponent(); 349 : renderComponent();
350 }; 350 };
351 351
@@ -391,12 +391,13 @@ @@ -391,12 +391,13 @@
391 const realColProps = { ...baseColProps, ...colProps }; 391 const realColProps = { ...baseColProps, ...colProps };
392 const { isIfShow, isShow } = getShow(); 392 const { isIfShow, isShow } = getShow();
393 const values = unref(getValues); 393 const values = unref(getValues);
  394 + const opts = { disabled: unref(getDisable) };
394 395
395 const getContent = () => { 396 const getContent = () => {
396 return colSlot 397 return colSlot
397 - ? getSlot(slots, colSlot, values) 398 + ? getSlot(slots, colSlot, values, opts)
398 : renderColContent 399 : renderColContent
399 - ? renderColContent(values) 400 + ? renderColContent(values, opts)
400 : renderItem(); 401 : renderItem();
401 }; 402 };
402 403
src/components/Form/src/hooks/useFormEvents.ts
@@ -87,6 +87,13 @@ export function useFormEvents({ @@ -87,6 +87,13 @@ export function useFormEvents({
87 87
88 Object.keys(formModel).forEach((key) => { 88 Object.keys(formModel).forEach((key) => {
89 const schema = unref(getSchema).find((item) => item.field === key); 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 const isInput = schema?.component && defaultValueComponents.includes(schema.component); 97 const isInput = schema?.component && defaultValueComponents.includes(schema.component);
91 const defaultValue = cloneDeep(defaultValueRef.value[key]); 98 const defaultValue = cloneDeep(defaultValueRef.value[key]);
92 formModel[key] = isInput ? defaultValue || '' : defaultValue; 99 formModel[key] = isInput ? defaultValue || '' : defaultValue;
@@ -96,14 +103,17 @@ export function useFormEvents({ @@ -96,14 +103,17 @@ export function useFormEvents({
96 emit('reset', toRaw(formModel)); 103 emit('reset', toRaw(formModel));
97 submitOnReset && handleSubmit(); 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 * @description: Set form value 113 * @description: Set form value
102 */ 114 */
103 async function setFieldsValue(values: Recordable): Promise<void> { 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 // key 支持 a.b.c 的嵌套写法 118 // key 支持 a.b.c 的嵌套写法
109 const delimiter = '.'; 119 const delimiter = '.';
@@ -340,8 +350,14 @@ export function useFormEvents({ @@ -340,8 +350,14 @@ export function useFormEvents({
340 return unref(formElRef)?.validateFields(nameList); 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 async function clearValidate(name?: string | string[]) { 363 async function clearValidate(name?: string | string[]) {
src/components/Form/src/hooks/useFormValues.ts
@@ -127,7 +127,16 @@ export function useFormValues({ @@ -127,7 +127,16 @@ export function useFormValues({
127 const schemas = unref(getSchema); 127 const schemas = unref(getSchema);
128 const obj: Recordable = {}; 128 const obj: Recordable = {};
129 schemas.forEach((item) => { 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 if (!isNullOrUnDef(defaultValue)) { 140 if (!isNullOrUnDef(defaultValue)) {
132 obj[item.field] = defaultValue; 141 obj[item.field] = defaultValue;
133 142
src/components/Form/src/types/form.ts
@@ -39,7 +39,7 @@ export interface FormActionType { @@ -39,7 +39,7 @@ export interface FormActionType {
39 first?: boolean | undefined, 39 first?: boolean | undefined,
40 ) => Promise<void>; 40 ) => Promise<void>;
41 validateFields: (nameList?: NamePath[]) => Promise<any>; 41 validateFields: (nameList?: NamePath[]) => Promise<any>;
42 - validate: (nameList?: NamePath[]) => Promise<any>; 42 + validate: (nameList?: NamePath[] | false) => Promise<any>;
43 scrollToField: (name: NamePath, options?: ScrollOptions) => Promise<void>; 43 scrollToField: (name: NamePath, options?: ScrollOptions) => Promise<void>;
44 } 44 }
45 45
@@ -123,15 +123,21 @@ export interface FormProps { @@ -123,15 +123,21 @@ export interface FormProps {
123 transformDateFunc?: (date: any) => string; 123 transformDateFunc?: (date: any) => string;
124 colon?: boolean; 124 colon?: boolean;
125 } 125 }
  126 +export type RenderOpts = {
  127 + disabled: boolean;
  128 + [key: string]: any;
  129 +};
126 export interface FormSchema { 130 export interface FormSchema {
127 // Field name 131 // Field name
128 field: string; 132 field: string;
  133 + // Extra Fields name[]
  134 + fields?: string[];
129 // Event name triggered by internal value change, default change 135 // Event name triggered by internal value change, default change
130 changeEvent?: string; 136 changeEvent?: string;
131 // Variable name bound to v-model Default value 137 // Variable name bound to v-model Default value
132 valueField?: string; 138 valueField?: string;
133 // Label name 139 // Label name
134 - label: string | VNode; 140 + label?: string | VNode;
135 // Auxiliary text 141 // Auxiliary text
136 subLabel?: string; 142 subLabel?: string;
137 // Help text on the right side of the text 143 // Help text on the right side of the text
@@ -175,6 +181,9 @@ export interface FormSchema { @@ -175,6 +181,9 @@ export interface FormSchema {
175 // 默认值 181 // 默认值
176 defaultValue?: any; 182 defaultValue?: any;
177 183
  184 + // 额外默认值数组对象
  185 + defaultValueObj?: { [key: string]: any };
  186 +
178 // 是否自动处理与时间相关组件的默认值 187 // 是否自动处理与时间相关组件的默认值
179 isHandleDateDefaultValue?: boolean; 188 isHandleDateDefaultValue?: boolean;
180 189
@@ -188,13 +197,19 @@ export interface FormSchema { @@ -188,13 +197,19 @@ export interface FormSchema {
188 show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean); 197 show?: boolean | ((renderCallbackParams: RenderCallbackParams) => boolean);
189 198
190 // Render the content in the form-item tag 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 // Rendering col content requires outer wrapper form-item 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 renderComponentContent?: 211 renderComponentContent?:
197 - | ((renderCallbackParams: RenderCallbackParams) => any) 212 + | ((renderCallbackParams: RenderCallbackParams, opts: RenderOpts) => any)
198 | VNode 213 | VNode
199 | VNode[] 214 | VNode[]
200 | string; 215 | string;
src/utils/helper/tsxHelper.tsx
1 import { Slots } from 'vue'; 1 import { Slots } from 'vue';
2 import { isFunction } from '/@/utils/is'; 2 import { isFunction } from '/@/utils/is';
  3 +import { RenderOpts } from '/@/components/Form';
3 4
4 /** 5 /**
5 * @description: Get slot to prevent empty error 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 if (!slots || !Reflect.has(slots, slot)) { 9 if (!slots || !Reflect.has(slots, slot)) {
9 return null; 10 return null;
10 } 11 }
@@ -14,7 +15,8 @@ export function getSlot(slots: Slots, slot = &#39;default&#39;, data?: any) { @@ -14,7 +15,8 @@ export function getSlot(slots: Slots, slot = &#39;default&#39;, data?: any) {
14 } 15 }
15 const slotFn = slots[slot]; 16 const slotFn = slots[slot];
16 if (!slotFn) return null; 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,21 +2,42 @@
2 <PageWrapper title="自定义组件示例"> 2 <PageWrapper title="自定义组件示例">
3 <CollapseContainer title="自定义表单"> 3 <CollapseContainer title="自定义表单">
4 <BasicForm @register="register" @submit="handleSubmit"> 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 </template> 16 </template>
8 </BasicForm> 17 </BasicForm>
9 </CollapseContainer> 18 </CollapseContainer>
10 </PageWrapper> 19 </PageWrapper>
11 </template> 20 </template>
12 -<script lang="ts"> 21 +<script lang="tsx">
13 import { defineComponent, h } from 'vue'; 22 import { defineComponent, h } from 'vue';
14 import { BasicForm, FormSchema, useForm } from '/@/components/Form/index'; 23 import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
15 import { CollapseContainer } from '/@/components/Container/index'; 24 import { CollapseContainer } from '/@/components/Container/index';
16 import { useMessage } from '/@/hooks/web/useMessage'; 25 import { useMessage } from '/@/hooks/web/useMessage';
17 - import { Input } from 'ant-design-vue'; 26 + import { Input, FormItem, Select } from 'ant-design-vue';
18 import { PageWrapper } from '/@/components/Page'; 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 const schemas: FormSchema[] = [ 41 const schemas: FormSchema[] = [
21 { 42 {
22 field: 'field1', 43 field: 'field1',
@@ -25,14 +46,18 @@ @@ -25,14 +46,18 @@
25 colProps: { 46 colProps: {
26 span: 8, 47 span: 8,
27 }, 48 },
  49 + dynamicDisabled: ({ values }) => {
  50 + return !!values.field_disabled;
  51 + },
28 rules: [{ required: true }], 52 rules: [{ required: true }],
29 - render: ({ model, field }) => { 53 + render: ({ model, field }, { disabled }) => {
30 return h(Input, { 54 return h(Input, {
31 placeholder: '请输入', 55 placeholder: '请输入',
32 value: model[field], 56 value: model[field],
33 - onChange: (e: ChangeEvent) => { 57 + onChange: (e) => {
34 model[field] = e.target.value; 58 model[field] = e.target.value;
35 }, 59 },
  60 + disabled,
36 }); 61 });
37 }, 62 },
38 }, 63 },
@@ -43,10 +68,13 @@ @@ -43,10 +68,13 @@
43 colProps: { 68 colProps: {
44 span: 8, 69 span: 8,
45 }, 70 },
  71 + dynamicDisabled: ({ values }) => {
  72 + return !!values.field_disabled;
  73 + },
46 rules: [{ required: true }], 74 rules: [{ required: true }],
47 - renderComponentContent: () => { 75 + renderComponentContent: (_, { disabled }) => {
48 return { 76 return {
49 - suffix: () => 'suffix', 77 + suffix: () => (disabled ? 'suffix_disabled' : 'suffix_default'),
50 }; 78 };
51 }, 79 },
52 }, 80 },
@@ -58,11 +86,136 @@ @@ -58,11 +86,136 @@
58 colProps: { 86 colProps: {
59 span: 8, 87 span: 8,
60 }, 88 },
  89 + dynamicDisabled: ({ values }) => {
  90 + return !!values.field_disabled;
  91 + },
61 rules: [{ required: true }], 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 export default defineComponent({ 217 export default defineComponent({
65 - components: { BasicForm, CollapseContainer, PageWrapper, [Input.name]: Input }, 218 + components: { BasicForm, CollapseContainer, PageWrapper, [Input.name]: Input, FormItem },
66 setup() { 219 setup() {
67 const { createMessage } = useMessage(); 220 const { createMessage } = useMessage();
68 const [register, { setProps }] = useForm({ 221 const [register, { setProps }] = useForm({
@@ -76,6 +229,7 @@ @@ -76,6 +229,7 @@
76 register, 229 register,
77 schemas, 230 schemas,
78 handleSubmit: (values: any) => { 231 handleSubmit: (values: any) => {
  232 + console.log('submit values', values);
79 createMessage.success('click search,values:' + JSON.stringify(values)); 233 createMessage.success('click search,values:' + JSON.stringify(values));
80 }, 234 },
81 setProps, 235 setProps,