BaseForm.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. <template>
  2. <el-form v-bind="ElFormProps" ref="formRef" class="base-form" @submit.prevent>
  3. <template v-for="formGroup in formGroups" :key="formGroup.groupKey">
  4. <component :is="props.transition ? ElCollapseTransition : LessRenderComponent">
  5. <custom-block v-show="!formGroup.hidden" :title="formGroup.groupTitle">
  6. <template v-for="row in formGroup.rows" :key="row.rowKey">
  7. <component :is="props.transition ? ElCollapseTransition : LessRenderComponent">
  8. <el-row v-show="!row.rowProp.hidden" v-bind="row.rowProp">
  9. <template v-for="col in row.cols" :key="col.colKey">
  10. <el-col v-show="!col.hidden" v-bind="col.colProp">
  11. <el-form-item
  12. v-bind="
  13. omit(col, ['colProp', 'slot', 'rowKey', 'colIndex', 'slotType', 'slotName', 'itemDescription'])
  14. "
  15. >
  16. <template
  17. v-for="[usedItemSlotName, itemSlotName] in getSlotKeys(col, 'item', ['label', 'error'])"
  18. #[itemSlotName]
  19. >
  20. <slot
  21. :name="usedItemSlotName"
  22. v-bind="{ item: col, model: props.model, index: col.colIndex }"
  23. ></slot>
  24. </template>
  25. <template #default>
  26. <slot
  27. :name="getSlotKeys(col, 'item', ['default'])[0]?.[0]"
  28. v-bind="{ item: col, model: props.model, index: col.colIndex }"
  29. >
  30. <component
  31. :is="FormItemComponentMap[col.slotType]"
  32. v-if="col.slotType"
  33. v-bind="Object.assign({}, FormItemComponentDefault[col.slotType], col.slot)"
  34. v-model="getFieldValue(props.model, col.prop).value"
  35. >
  36. <template
  37. v-for="[usedSlotName, slotName] in getSlotKeys(
  38. col,
  39. col.slotType,
  40. FormItemComponentSlots[col.slotType]
  41. )"
  42. #[slotName]
  43. >
  44. <slot
  45. :name="usedSlotName"
  46. v-bind="{ item: col, model: props.model, index: col.colIndex }"
  47. ></slot>
  48. </template>
  49. </component>
  50. </slot>
  51. </template>
  52. </el-form-item>
  53. </el-col>
  54. <el-row v-if="col.itemDescription?.description || getSlotKeys(col, 'item', ['description'])[0]?.[0]">
  55. <slot
  56. :name="getSlotKeys(col, 'item', ['description'])[0]?.[0]"
  57. v-bind="{ item: col, model: props.model, index: col.colIndex }"
  58. >
  59. <el-form-item label-width="0">
  60. <span
  61. v-if="col.itemDescription?.description"
  62. :key="getSlotKeys(col, 'item', ['description'])[0]?.[0]"
  63. class="inline-flex items-center form-item-description"
  64. :class="{ 'required-asterisk': col.itemDescription?.requiredAsterisk !== false }"
  65. >
  66. {{ col.itemDescription?.description }}
  67. </span>
  68. </el-form-item>
  69. </slot>
  70. </el-row>
  71. </template>
  72. </el-row>
  73. </component>
  74. </template>
  75. </custom-block>
  76. </component>
  77. </template>
  78. <slot></slot>
  79. </el-form>
  80. </template>
  81. <script setup lang="tsx" name="EpForm">
  82. import { computed, useSlots, shallowRef, markRaw, defineComponent } from 'vue'
  83. import {
  84. ElForm,
  85. ElFormItem,
  86. ElInput,
  87. ElInputNumber,
  88. ElAutocomplete,
  89. ElSwitch,
  90. ElRadio,
  91. ElDatePicker,
  92. ElRow,
  93. ElCol,
  94. ElCollapseTransition,
  95. } from 'element-plus'
  96. import CustomBlock from '@/components/common/CustomBlock.vue'
  97. import BaseSelect from '@/components/element/BaseSelect.vue'
  98. import BaseCheckBox from '@/components/element/BaseCheckBox.vue'
  99. import BaseRadio from '@/components/element/BaseRadio.vue'
  100. import { get, set, omit } from 'lodash-es'
  101. import type { EpFormItem, BaseFormProp, FormSupportComponentMap, FormGroup, EpFormRows } from 'global-type'
  102. const LessRenderComponent = defineComponent({
  103. name: 'LessRender',
  104. render() {
  105. const slots = useSlots()
  106. return slots?.default?.()
  107. },
  108. })
  109. interface BaseEpForm extends BaseFormProp {
  110. model: Record<string, unknown>
  111. rules?: Required<BaseFormProp>['rules']
  112. items?: EpFormItem[]
  113. rows?: EpFormRows
  114. groups?: FormGroup[]
  115. transition?: boolean
  116. }
  117. type RequiredGroup = Required<ExtractArrayValue<Required<BaseEpForm>['groups']>>
  118. type FormatterFormItem = AssignKeys<Required<BaseEpForm>['items'][0], { colIndex: number; colKey: string | number }>
  119. interface FormatterFormGroup extends Omit<RequiredGroup, 'rowKeys'> {
  120. rows: {
  121. rowKey: string | number
  122. rowProp: Omit<ExtractRecordValue<Required<BaseEpForm>['rows']>, 'rowKey'>
  123. cols: FormatterFormItem[]
  124. }[]
  125. }
  126. const FormItemComponentMap = markRaw<FormSupportComponentMap>({
  127. autocomplete: ElAutocomplete,
  128. input: ElInput,
  129. inputNumber: ElInputNumber,
  130. checkbox: BaseCheckBox,
  131. radio: BaseRadio,
  132. select: BaseSelect,
  133. date: ElDatePicker,
  134. dateTime: ElDatePicker,
  135. switch: ElSwitch,
  136. })
  137. const FormItemComponentSlots = markRaw<Record<keyof FormSupportComponentMap, string[]>>({
  138. autocomplete: ['prefix', 'suffix', 'prepend', 'append', 'default'],
  139. input: ['prefix', 'suffix', 'prepend', 'append'],
  140. inputNumber: [],
  141. checkbox: ['default'],
  142. radio: ['default'],
  143. select: ['prefix', 'empty', 'default'],
  144. switch: [],
  145. date: ['range-separator', 'default'],
  146. dateTime: ['range-separator', 'default'],
  147. })
  148. const FormItemComponentDefault = markRaw<Partial<Record<keyof FormSupportComponentMap, EpFormItem['slot']>>>({
  149. inputNumber: {
  150. controls: false,
  151. },
  152. })
  153. const props = defineProps<BaseEpForm>()
  154. const ElFormProps = computed(() => {
  155. const { items, groups, rows, ...formAttr } = props
  156. return formAttr
  157. })
  158. const formGroups = computed<FormatterFormGroup[]>(() => {
  159. const rows = props.items?.reduce((rowMap, formItem, index) => {
  160. const itemRowKey = formItem.rowKey || 'row-' + (index + 1)
  161. rowMap[itemRowKey] ??= { rowKey: itemRowKey, rowProp: props.rows?.[itemRowKey] || {}, groupUsed: false, cols: [] }
  162. rowMap[itemRowKey].cols.push({ ...formItem, colKey: `col-${rowMap[itemRowKey].cols.length}`, colIndex: index })
  163. return rowMap
  164. }, {} as Record<string | number, FormatterFormGroup['rows'][0] & { groupUsed: boolean }>)
  165. const groups = props.groups?.map((groupConfig, index) => {
  166. let groupRows: FormatterFormGroup['rows'] = []
  167. groupConfig.rowKeys?.forEach((rowKey) => {
  168. if (rows?.[rowKey]) {
  169. rows[rowKey].groupUsed = true
  170. groupRows.push(rows[rowKey])
  171. }
  172. })
  173. const groupInfo: FormatterFormGroup = {
  174. hidden: groupConfig.hidden ?? false,
  175. groupKey: groupConfig.groupKey || 'group-' + (index + 1),
  176. groupTitle: groupConfig.groupTitle || '',
  177. rows: groupRows,
  178. }
  179. return groupInfo
  180. })
  181. return (groups || []).concat({
  182. rows: Object.values(rows || {}).filter((row) => !row.groupUsed),
  183. groupKey: 'default-group',
  184. groupTitle: '',
  185. hidden: false,
  186. })
  187. })
  188. const slots = useSlots()
  189. function getSlotKeys(formItem: FormatterFormItem, itemName: string, slotNames: string[]) {
  190. const tag = (formItem.slotName || formItem.prop || formItem.colIndex) ?? undefined
  191. const slotKey = slotNames.map((slotName) => [
  192. ['form', itemName, tag, slotName === 'default' ? '' : slotName].filter((t) => t !== '').join('-'),
  193. slotName,
  194. ])
  195. return slotKey.filter(([slotName]) => typeof slots[slotName] === 'function')
  196. }
  197. function getFieldValue(prop: Record<string, any>, key?: string | string[]) {
  198. return {
  199. get value() {
  200. return key ? get(prop, key) : undefined
  201. },
  202. set value(val: any) {
  203. key ? set(prop, key, val) : void 0
  204. },
  205. }
  206. }
  207. const formRef = shallowRef<InstanceType<typeof ElForm>>()
  208. defineExpose({ formRef })
  209. </script>
  210. <style scoped lang="scss">
  211. .base-form {
  212. :deep(.el-select) {
  213. width: 100%;
  214. }
  215. &.el-form--inline :deep(.el-form-item) {
  216. align-items: center;
  217. margin-bottom: 0;
  218. .el-form-item__content {
  219. word-break: break-all;
  220. }
  221. }
  222. .form-item-description {
  223. margin-left: 40px;
  224. color: $FormItemDescriptionColor;
  225. font-size: $SmallFont;
  226. &.required-asterisk:before {
  227. content: '*';
  228. color: $DangerColor;
  229. margin-right: 0.285em;
  230. }
  231. }
  232. }
  233. </style>