houzhongjian
2024-08-08 820397e43a0b64d35c6d31d2a55475061438593b
提交 | 用户 | 时间
820397 1 <script lang="tsx">
H 2 import { computed, defineComponent, onMounted, PropType, ref, unref, watch } from 'vue'
3 import { ElCol, ElForm, ElFormItem, ElRow, ElTooltip } from 'element-plus'
4 import { componentMap } from './componentMap'
5 import { propTypes } from '@/utils/propTypes'
6 import { getSlot } from '@/utils/tsxHelper'
7 import {
8   initModel,
9   setComponentProps,
10   setFormItemSlots,
11   setGridProp,
12   setItemComponentSlots,
13   setTextPlaceholder
14 } from './helper'
15 import { useRenderSelect } from './components/useRenderSelect'
16 import { useRenderRadio } from './components/useRenderRadio'
17 import { useRenderCheckbox } from './components/useRenderCheckbox'
18 import { useDesign } from '@/hooks/web/useDesign'
19 import { findIndex } from '@/utils'
20 import { set } from 'lodash-es'
21 import { FormProps } from './types'
22 import { Icon } from '@/components/Icon'
23 import { FormSchema, FormSetPropsType } from '@/types/form'
24
25 const { getPrefixCls } = useDesign()
26
27 const prefixCls = getPrefixCls('form')
28
29 export default defineComponent({
30   // eslint-disable-next-line vue/no-reserved-component-names
31   name: 'Form',
32   props: {
33     // 生成Form的布局结构数组
34     schema: {
35       type: Array as PropType<FormSchema[]>,
36       default: () => []
37     },
38     // 是否需要栅格布局
39     // update by 芋艿:将 true 改成 false,因为项目更常用这种方式
40     isCol: propTypes.bool.def(false),
41     // 表单数据对象
42     model: {
43       type: Object as PropType<Recordable>,
44       default: () => ({})
45     },
46     // 是否自动设置placeholder
47     autoSetPlaceholder: propTypes.bool.def(true),
48     // 是否自定义内容
49     isCustom: propTypes.bool.def(false),
50     // 表单label宽度
51     labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
52     // 是否 loading 数据中 add by 芋艿
53     vLoading: propTypes.bool.def(false)
54   },
55   emits: ['register'],
56   setup(props, { slots, expose, emit }) {
57     // element form 实例
58     const elFormRef = ref<ComponentRef<typeof ElForm>>()
59
60     // useForm传入的props
61     const outsideProps = ref<FormProps>({})
62
63     const mergeProps = ref<FormProps>({})
64
65     const getProps = computed(() => {
66       const propsObj = { ...props }
67       Object.assign(propsObj, unref(mergeProps))
68       return propsObj
69     })
70
71     // 表单数据
72     const formModel = ref<Recordable>({})
73
74     onMounted(() => {
75       emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
76     })
77
78     // 对表单赋值
79     const setValues = (data: Recordable = {}) => {
80       formModel.value = Object.assign(unref(formModel), data)
81     }
82
83     const setProps = (props: FormProps = {}) => {
84       mergeProps.value = Object.assign(unref(mergeProps), props)
85       outsideProps.value = props
86     }
87
88     const delSchema = (field: string) => {
89       const { schema } = unref(getProps)
90
91       const index = findIndex(schema, (v: FormSchema) => v.field === field)
92       if (index > -1) {
93         schema.splice(index, 1)
94       }
95     }
96
97     const addSchema = (formSchema: FormSchema, index?: number) => {
98       const { schema } = unref(getProps)
99       if (index !== void 0) {
100         schema.splice(index, 0, formSchema)
101         return
102       }
103       schema.push(formSchema)
104     }
105
106     const setSchema = (schemaProps: FormSetPropsType[]) => {
107       const { schema } = unref(getProps)
108       for (const v of schema) {
109         for (const item of schemaProps) {
110           if (v.field === item.field) {
111             set(v, item.path, item.value)
112           }
113         }
114       }
115     }
116
117     const getElFormRef = (): ComponentRef<typeof ElForm> => {
118       return unref(elFormRef) as ComponentRef<typeof ElForm>
119     }
120
121     expose({
122       setValues,
123       formModel,
124       setProps,
125       delSchema,
126       addSchema,
127       setSchema,
128       getElFormRef
129     })
130
131     // 监听表单结构化数组,重新生成formModel
132     watch(
133       () => unref(getProps).schema,
134       (schema = []) => {
135         formModel.value = initModel(schema, unref(formModel))
136       },
137       {
138         immediate: true,
139         deep: true
140       }
141     )
142
143     // 渲染包裹标签,是否使用栅格布局
144     const renderWrap = () => {
145       const { isCol } = unref(getProps)
146       const content = isCol ? (
147         <ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
148       ) : (
149         renderFormItemWrap()
150       )
151       return content
152     }
153
154     // 是否要渲染el-col
155     const renderFormItemWrap = () => {
156       // hidden属性表示隐藏,不做渲染
157       const { schema = [], isCol } = unref(getProps)
158
159       return schema
160         .filter((v) => !v.hidden)
161         .map((item) => {
162           // 如果是 Divider 组件,需要自己占用一行
163           const isDivider = item.component === 'Divider'
164           const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
165           return isDivider ? (
166             <Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
167           ) : isCol ? (
168             // 如果需要栅格,需要包裹 ElCol
169             <ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
170           ) : (
171             renderFormItem(item)
172           )
173         })
174     }
175
176     // 渲染formItem
177     const renderFormItem = (item: FormSchema) => {
178       // 单独给只有options属性的组件做判断
179       const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
180       const slotsMap: Recordable = {
181         ...setItemComponentSlots(slots, item?.componentProps?.slots, item.field)
182       }
183       if (
184         item?.component !== 'SelectV2' &&
185         item?.component !== 'Cascader' &&
186         item?.componentProps?.options
187       ) {
188         slotsMap.default = () => renderOptions(item)
189       }
190
191       const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
192       // 如果有 labelMessage,自动使用插槽渲染
193       if (item?.labelMessage) {
194         formItemSlots.label = () => {
195           return (
196             <>
197               <span>{item.label}</span>
198               <ElTooltip placement="right" raw-content>
199                 {{
200                   content: () => <span v-dompurify-html={item.labelMessage}></span>,
201                   default: () => (
202                     <Icon
203                       icon="ep:warning"
204                       size={16}
205                       color="var(--el-color-primary)"
206                       class="relative top-1px ml-2px"
207                     ></Icon>
208                   )
209                 }}
210               </ElTooltip>
211             </>
212           )
213         }
214       }
215       return (
216         <ElFormItem {...(item.formItemProps || {})} prop={item.field} label={item.label || ''}>
217           {{
218             ...formItemSlots,
219             default: () => {
220               const Com = componentMap[item.component as string] as ReturnType<
221                 typeof defineComponent
222               >
223
224               const { autoSetPlaceholder } = unref(getProps)
225
226               return slots[item.field] ? (
227                 getSlot(slots, item.field, formModel.value)
228               ) : (
229                 <Com
230                   vModel={formModel.value[item.field]}
231                   {...(autoSetPlaceholder && setTextPlaceholder(item))}
232                   {...setComponentProps(item)}
233                   style={item.componentProps?.style}
234                   {...(notRenderOptions.includes(item?.component as string) &&
235                   item?.componentProps?.options
236                     ? { options: item?.componentProps?.options || [] }
237                     : {})}
238                 >
239                   {{ ...slotsMap }}
240                 </Com>
241               )
242             }
243           }}
244         </ElFormItem>
245       )
246     }
247
248     // 渲染options
249     const renderOptions = (item: FormSchema) => {
250       switch (item.component) {
251         case 'Select':
252         case 'SelectV2':
253           const { renderSelectOptions } = useRenderSelect(slots)
254           return renderSelectOptions(item)
255         case 'Radio':
256         case 'RadioButton':
257           const { renderRadioOptions } = useRenderRadio()
258           return renderRadioOptions(item)
259         case 'Checkbox':
260         case 'CheckboxButton':
261           const { renderCheckboxOptions } = useRenderCheckbox()
262           return renderCheckboxOptions(item)
263         default:
264           break
265       }
266     }
267
268     // 过滤传入Form组件的属性
269     const getFormBindValue = () => {
270       // 避免在标签上出现多余的属性
271       const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
272       const props = { ...unref(getProps) }
273       for (const key in props) {
274         if (delKeys.indexOf(key) !== -1) {
275           delete props[key]
276         }
277       }
278       return props
279     }
280
281     return () => (
282       <ElForm
283         ref={elFormRef}
284         {...getFormBindValue()}
285         model={props.isCustom ? props.model : formModel}
286         class={prefixCls}
287         v-loading={props.vLoading}
288       >
289         {{
290           // 如果需要自定义,就什么都不渲染,而是提供默认插槽
291           default: () => {
292             const { isCustom } = unref(getProps)
293             return isCustom ? getSlot(slots, 'default') : renderWrap()
294           }
295         }}
296       </ElForm>
297     )
298   }
299 })
300 </script>
301
302 <style lang="scss" scoped>
303 .#{$elNamespace}-form.#{$namespace}-form .#{$elNamespace}-row {
304   margin-right: 0 !important;
305   margin-left: 0 !important;
306 }
307 </style>