Form.vue 9.0 KB

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