easybill-ui 1.2.26 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,31 +1,59 @@
1
1
  <template>
2
2
  <el-form ref="schemaFormRef" :model="formModel" :rules="rules" v-bind="{ ...$attrs, ...getFormProps }" class="curd-form" :style="getFormStyle()" @submit.prevent>
3
3
  <template v-for="formItem in schemaItems" :key="formItem.prop">
4
- <el-form-item
5
- v-if="!$slots[formItem.prop + 'Item']"
6
- :required="rules && rules[formItem.prop]?.find((a) => a.hasOwnProperty('required'))?.required"
7
- :label="formItem.label"
8
- :prop="formItem.prop"
9
- :label-width="formItem.labelWidth"
10
- :class="getFormItemStyle(formItem)"
11
- v-bind="getFormItemProps(formItem)"
12
- >
13
- <template v-for="(item, key) in getFormItemProps(formItem)?.slots || {}" :key="key" #[key]="row">
14
- <component :is="item" v-bind="row" />
15
- </template>
16
-
17
- <slot :name="formItem.prop" :form-item="formItem" :form-model="formModel"></slot>
18
- <FormItem v-if="!(formItem.prop && $slots[formItem.prop])" :form-item="formItem" :form-model="formModel" @change="onChange">
19
- <template #prefix>
20
- <slot :name="formItem.prop + 'Prefix'" :form-item="formItem" :form-model="formModel"></slot>
21
- </template>
22
- <template #suffix>
23
- <slot :name="formItem.prop + 'Suffix'" :form-item="formItem" :form-model="formModel"></slot>
4
+ <!-- choose 类型特殊处理,不使用 el-form-item 包裹,避免嵌套问题 -->
5
+ <template v-if="formItem.type === 'choose'">
6
+ <div v-if="!$slots[formItem.prop + 'Item']" class="el-form-item choose-group-wrapper" :class="[getFormItemStyle(formItem), getChooseFormItemClass(formItem)]" :style="getChooseFormItemStyle(formItem)">
7
+ <!-- label 区域 -->
8
+ <label v-if="formItem.label" class="el-form-item__label" :style="{ width: getLabelWidth(formItem) }">
9
+ {{ formItem.label }}
10
+ </label>
11
+
12
+ <!-- content 区域 -->
13
+ <div class="el-form-item__content" :style="{ marginLeft: getLabelPosition(formItem) !== 'top' ? '' : '' }">
14
+ <slot :name="formItem.prop" :form-item="formItem" :form-model="formModel"></slot>
15
+ <FormItem v-if="!(formItem.prop && $slots[formItem.prop])" :form-item="formItem" :form-model="formModel" @change="onChange">
16
+ <template #prefix>
17
+ <slot :name="formItem.prop + 'Prefix'" :form-item="formItem" :form-model="formModel"></slot>
18
+ </template>
19
+ <template #suffix>
20
+ <slot :name="formItem.prop + 'Suffix'" :form-item="formItem" :form-model="formModel"></slot>
21
+ </template>
22
+ </FormItem>
23
+ <slot :name="formItem.prop + 'Bottom'" :form-item="formItem" :form-model="formModel"></slot>
24
+ </div>
25
+ </div>
26
+ <slot :name="formItem.prop + 'Item'" :form-item="formItem" :form-model="formModel"></slot>
27
+ </template>
28
+
29
+ <!-- 其他类型使用 el-form-item 包裹 -->
30
+ <template v-else>
31
+ <el-form-item
32
+ v-if="!$slots[formItem.prop + 'Item']"
33
+ :required="rules && rules[formItem.prop]?.find((a) => a.hasOwnProperty('required'))?.required"
34
+ :label="formItem.label"
35
+ :prop="formItem.prop"
36
+ :label-width="formItem.labelWidth"
37
+ :class="getFormItemStyle(formItem)"
38
+ v-bind="getFormItemProps(formItem)"
39
+ >
40
+ <template v-for="(item, key) in getFormItemProps(formItem)?.slots || {}" :key="key" #[key]="row">
41
+ <component :is="item" v-bind="row" />
24
42
  </template>
25
- </FormItem>
26
- <slot :name="formItem.prop + 'Bottom'" :form-item="formItem" :form-model="formModel"></slot>
27
- </el-form-item>
28
- <slot :name="formItem.prop + 'Item'" :form-item="formItem" :form-model="formModel"></slot>
43
+
44
+ <slot :name="formItem.prop" :form-item="formItem" :form-model="formModel"></slot>
45
+ <FormItem v-if="!(formItem.prop && $slots[formItem.prop])" :form-item="formItem" :form-model="formModel" @change="onChange">
46
+ <template #prefix>
47
+ <slot :name="formItem.prop + 'Prefix'" :form-item="formItem" :form-model="formModel"></slot>
48
+ </template>
49
+ <template #suffix>
50
+ <slot :name="formItem.prop + 'Suffix'" :form-item="formItem" :form-model="formModel"></slot>
51
+ </template>
52
+ </FormItem>
53
+ <slot :name="formItem.prop + 'Bottom'" :form-item="formItem" :form-model="formModel"></slot>
54
+ </el-form-item>
55
+ <slot :name="formItem.prop + 'Item'" :form-item="formItem" :form-model="formModel"></slot>
56
+ </template>
29
57
  </template>
30
58
  <template v-if="$slots['operate-button']">
31
59
  <el-form-item style="max-width: 100%">
@@ -36,6 +64,7 @@
36
64
  </template>
37
65
 
38
66
  <script lang="ts" setup>
67
+ import { ElForm, type FormRules } from "element-plus"
39
68
  import { computed, getCurrentInstance, onMounted, provide, reactive, ref, triggerRef, watch, type PropType } from "vue"
40
69
  import FormItem from "./FormItem.vue"
41
70
  import type { CurdFormOptionItem, Fields, FormContext, FormItem as FormItemType, FormSchema } from "./types"
@@ -92,6 +121,21 @@ const schemaValues = sFormSchema.value.formItem.reduce<Fields>((previousValue, c
92
121
  if (typeof currentValue.value !== "undefined" && currentValue.prop && (typeof formModel[currentValue.prop] == "undefined" || formModel[currentValue.prop] === "" || formModel[currentValue.prop] === null)) {
93
122
  previousValue[currentValue.prop] = currentValue.value
94
123
  }
124
+
125
+ // 处理 choose 类型 items 的默认值
126
+ if (currentValue.type === "choose" && currentValue.options) {
127
+ currentValue.options.forEach((option) => {
128
+ if (option.items) {
129
+ option.items.forEach((subItem) => {
130
+ subItem.eventObject ??= {}
131
+ if (typeof subItem.value !== "undefined" && subItem.prop && (typeof formModel[subItem.prop] == "undefined" || formModel[subItem.prop] === "" || formModel[subItem.prop] === null)) {
132
+ previousValue[subItem.prop] = subItem.value
133
+ }
134
+ })
135
+ }
136
+ })
137
+ }
138
+
95
139
  return previousValue
96
140
  }, {})
97
141
 
@@ -114,13 +158,27 @@ const schemaItems = computed(() => {
114
158
  })
115
159
  // 异步设置默认数据
116
160
  sFormSchema.value.formItem.forEach(async (item) => {
117
- //
118
- // 异步选项
161
+ // 异步选项 - 顶层表单项
119
162
  if (item.asyncOptions && (item.autoload || typeof item.autoload == "undefined") && item.asyncOptions instanceof Function) {
120
163
  item.loading = true
121
164
  item.options = await item.asyncOptions(formModel, item, curdFormContext).finally(() => (item.loading = false))
122
165
  if (!instance?.isUnmounted && item.eventObject?.optionLoaded) item.eventObject.optionLoaded(formModel, item, curdFormContext)
123
166
  }
167
+
168
+ // 处理 choose 类型的 items 的异步选项
169
+ if (item.type === "choose" && item.options) {
170
+ for (const option of item.options) {
171
+ if (option.items && option.items.length > 0) {
172
+ for (const subItem of option.items) {
173
+ if (subItem.asyncOptions && (subItem.autoload || typeof subItem.autoload == "undefined") && subItem.asyncOptions instanceof Function) {
174
+ subItem.loading = true
175
+ subItem.options = await subItem.asyncOptions(formModel, subItem, curdFormContext).finally(() => (subItem.loading = false))
176
+ if (!instance?.isUnmounted && subItem.eventObject?.optionLoaded) subItem.eventObject.optionLoaded(formModel, subItem, curdFormContext)
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
124
182
  })
125
183
 
126
184
  // 生成表单验证规则
@@ -129,23 +187,77 @@ const rules = computed(() => {
129
187
  Promise.resolve().then(() => {
130
188
  triggerRef(rules)
131
189
  })
190
+ let baseRules: FormRules = {}
132
191
  if (typeof sFormSchema.value.rules == "function") {
133
- return sFormSchema.value.rules(formModel, curdFormContext)
192
+ baseRules = sFormSchema.value.rules(formModel, curdFormContext)
193
+ } else if (typeof sFormSchema.value.getRules == "function") {
194
+ baseRules = sFormSchema.value.getRules(formModel, curdFormContext)
195
+ } else {
196
+ baseRules = sFormSchema.value.rules || {}
134
197
  }
135
- if (typeof sFormSchema.value.getRules == "function") {
136
- return sFormSchema.value.getRules(formModel, curdFormContext)
137
- }
138
- return sFormSchema.value.rules
198
+
199
+ // 合并 choose 类型 items 的验证规则
200
+ const mergedRules: FormRules = { ...baseRules }
201
+ sFormSchema.value.formItem.forEach((formItem) => {
202
+ if (formItem.type === "choose" && formItem.options) {
203
+ formItem.options.forEach((option) => {
204
+ if (option.items) {
205
+ option.items.forEach((subItem) => {
206
+ if (subItem.prop && subItem.rules) {
207
+ mergedRules[subItem.prop] = subItem.rules
208
+ }
209
+ })
210
+ }
211
+ })
212
+ }
213
+ })
214
+
215
+ return mergedRules
139
216
  })
140
217
 
218
+ /**
219
+ * 递归查找表单项
220
+ * @param prop 表单项的 prop
221
+ * @returns 找到的表单项对象,包含 parent 信息
222
+ */
223
+ const findFormItem = (prop: string): { item: FormItemType; parent?: FormItemType; path: string } | undefined => {
224
+ // 先在顶层查找
225
+ const topLevelItem = sFormSchema.value.formItem.find((a) => a.prop == prop)
226
+ if (topLevelItem) {
227
+ return { item: topLevelItem, path: prop }
228
+ }
229
+
230
+ // 在 choose 类型的 options.items 中递归查找
231
+ for (const formItem of sFormSchema.value.formItem) {
232
+ if (formItem.type === "choose" && formItem.options) {
233
+ for (const option of formItem.options) {
234
+ if (option.items) {
235
+ const foundItem = option.items.find((item) => item.prop === prop)
236
+ if (foundItem) {
237
+ return { item: foundItem, parent: formItem, path: `${formItem.prop}.${option.value}.${prop}` }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ return undefined
245
+ }
246
+
141
247
  // 供外部使用
142
248
  const validate = (callback: (valid: boolean) => void) => {
143
249
  return schemaFormRef.value?.validate(callback)
144
250
  }
145
251
  // 调用某个表单项的异步数据接口
146
252
  const loadOptions = async (prop: string, option?: unknown) => {
147
- const cur: FormItemType | undefined = sFormSchema.value.formItem.find((a) => a.prop == prop)
148
- if (cur && cur.asyncOptions && !instance?.isUnmounted) {
253
+ const result = findFormItem(prop)
254
+ if (!result) {
255
+ console.warn(`[CurdForm] loadOptions: 未找到 prop 为 "${prop}" 的表单项`)
256
+ return []
257
+ }
258
+
259
+ const cur = result.item
260
+ if (cur.asyncOptions && !instance?.isUnmounted) {
149
261
  cur.loading = true
150
262
  triggerRef(schemaItems)
151
263
  cur.options =
@@ -160,10 +272,13 @@ const loadOptions = async (prop: string, option?: unknown) => {
160
272
  }
161
273
  // 给某个item赋值options
162
274
  const setOptions = async (prop: string, options: CurdFormOptionItem[], option?: unknown) => {
163
- const cur = sFormSchema.value.formItem.find((a) => a.prop == prop)
164
- if (cur) {
165
- cur.options = options
166
- if (!instance?.isUnmounted && cur.eventObject?.optionLoaded) cur.eventObject.optionLoaded(formModel, cur, curdFormContext, option)
275
+ const result = findFormItem(prop)
276
+ if (result) {
277
+ result.item.options = options
278
+ triggerRef(schemaItems)
279
+ if (!instance?.isUnmounted && result.item.eventObject?.optionLoaded) result.item.eventObject.optionLoaded(formModel, result.item, curdFormContext, option)
280
+ } else {
281
+ console.warn(`[CurdForm] setOptions: 未找到 prop 为 "${prop}" 的表单项`)
167
282
  }
168
283
  // return cur
169
284
  }
@@ -179,11 +294,53 @@ const getFormItemProps = (formItem: FormItemType) => {
179
294
  if (formItem.formItemProps instanceof Function) {
180
295
  return formItem.formItemProps(formModel, formItem)
181
296
  }
182
- const { slots, ...attrs } = formItem.formItemProps
183
- return { ...attrs }
297
+ return { ...formItem.formItemProps }
184
298
  }
185
299
  return {}
186
300
  }
301
+
302
+ // 获取表单项的 labelWidth
303
+ const getLabelWidth = (formItem: FormItemType) => {
304
+ const itemLabelWidth = formItem.labelWidth
305
+ if (itemLabelWidth !== undefined) {
306
+ return typeof itemLabelWidth === "number" ? itemLabelWidth + "px" : itemLabelWidth
307
+ }
308
+ const formLabelWidth = props.formSchema.labelWidth
309
+ if (formLabelWidth !== undefined) {
310
+ return typeof formLabelWidth === "number" ? formLabelWidth + "px" : formLabelWidth
311
+ }
312
+ return undefined
313
+ }
314
+
315
+ // 获取表单项的 labelPosition
316
+ const getLabelPosition = (formItem: FormItemType) => {
317
+ return formItem.labelPosition || props.formSchema.labelPosition || ""
318
+ }
319
+
320
+ // 获取 choose 类型的额外样式类
321
+ const getChooseFormItemClass = (_formItem: FormItemType) => {
322
+ const classes: string[] = []
323
+ const labelPosition = getLabelPosition(_formItem)
324
+ if (labelPosition) {
325
+ classes.push("el-form-item--label-" + labelPosition)
326
+ }
327
+ return classes.join(" ")
328
+ }
329
+
330
+ // 获取 choose 类型的样式
331
+ const getChooseFormItemStyle = (formItem: FormItemType) => {
332
+ const style: Record<string, string> = {}
333
+ const labelPosition = getLabelPosition(formItem)
334
+ if (labelPosition === "top") {
335
+ // top 模式下不需要额外样式
336
+ } else {
337
+ const labelWidth = getLabelWidth(formItem)
338
+ if (labelWidth) {
339
+ // 用于让 content 区域正确对齐
340
+ }
341
+ }
342
+ return style
343
+ }
187
344
  const getFormStyle = () => {
188
345
  const gutter = props.formSchema.gutter
189
346
  if (gutter) {
@@ -207,6 +364,7 @@ curdFormContext.loadOptions = loadOptions
207
364
  curdFormContext.setOptions = setOptions
208
365
  curdFormContext.change = onChange
209
366
  curdFormContext.formModel = formModel
367
+ curdFormContext.formSchema = props.formSchema
210
368
 
211
369
  if (props.extendContext) Object.assign(curdFormContext, props.extendContext)
212
370
 
@@ -231,3 +389,58 @@ defineExpose({
231
389
  onChange,
232
390
  })
233
391
  </script>
392
+
393
+ <style scoped lang="scss">
394
+ // choose 类型容器样式,模拟 el-form-item 布局
395
+ /*
396
+ .choose-group-wrapper {
397
+ display: flex;
398
+ align-items: flex-start;
399
+
400
+ :deep(.el-form-item__label) {
401
+ display: inline-flex;
402
+ justify-content: flex-end;
403
+ align-items: flex-start;
404
+ flex-shrink: 0;
405
+ font-size: var(--el-font-size-base);
406
+ color: var(--el-text-color-regular);
407
+ height: 32px;
408
+ line-height: 32px;
409
+ padding: 0 12px 0 0;
410
+ box-sizing: border-box;
411
+ }
412
+
413
+ :deep(.el-form-item__asterisk) {
414
+ color: var(--el-color-danger);
415
+ font-size: var(--el-font-size-base);
416
+ margin-left: 4px;
417
+ }
418
+
419
+ :deep(.el-form-item__content) {
420
+ display: flex;
421
+ flex-wrap: wrap;
422
+ align-items: center;
423
+ flex: 1;
424
+ position: relative;
425
+ font-size: var(--el-font-size-base);
426
+ min-width: 0;
427
+ }
428
+
429
+ :deep(.top-label) {
430
+ justify-content: flex-start;
431
+ padding: 0 0 8px 0;
432
+ height: auto;
433
+ line-height: 1.5;
434
+ }
435
+
436
+ // label-top 模式
437
+ &.el-form-item--label-top {
438
+ flex-direction: column;
439
+
440
+ :deep(.el-form-item__content) {
441
+ margin-left: 0 !important;
442
+ }
443
+ }
444
+ }
445
+ */
446
+ </style>
@@ -2,6 +2,7 @@ import { isReactive, isRef, isVNode, type Component, type VNode } from "vue"
2
2
  import SchemaFormAutocomplete from "./schema-form-autocomplete.vue"
3
3
  import SchemaFormCascader from "./schema-form-cascader.vue"
4
4
  import SchemaFormCheckbox from "./schema-form-checkbox.vue"
5
+ import SchemaFormChoose from "./schema-form-choose.vue"
5
6
  import SchemaFormColorPicker from "./schema-form-color-picker.vue"
6
7
  import SchemaFormDatePicker from "./schema-form-date-picker.vue"
7
8
  import SchemaFormInputNumber from "./schema-form-input-number.vue"
@@ -31,6 +32,7 @@ const presetMap: PresetMap = {
31
32
  "schema-cascader": SchemaFormCascader,
32
33
  "schema-autocomplete": SchemaFormAutocomplete,
33
34
  "schema-tree-select": SchemaFormTreeSelect,
35
+ "schema-choose": SchemaFormChoose,
34
36
  }
35
37
 
36
38
  export default presetMap
@@ -0,0 +1,225 @@
1
+ <template>
2
+ <div class="schema-form-choose">
3
+ <div class="choose-content-wrapper">
4
+ <div v-if="formItem.loading" class="loading">
5
+ <el-icon class="is-loading"><Loading /></el-icon> {{ t("el.form.loading") }}
6
+ </div>
7
+ <el-radio-group v-else-if="options?.length" v-model="currentValue" :class="[props.props?.showType, chooseGroupClass]" v-bind="props.props" v-on="props.eventObject">
8
+ <template v-for="(option, index) in options" :key="option.value">
9
+ <component :is="props?.props?.componentName == 'button' ? 'el-radio-button' : 'el-radio'" :value="option.value" :disabled="option.disabled" :border="option.border" :size="option.size" :style="option.style">
10
+ <component v-if="option.component" :is="option.component" :row="option" :$index="index" :form-model="formModel" :form-item="formItem" :props="props" />
11
+ <span v-else-if="option.html" v-html="option.html"></span>
12
+ <span v-else>{{ option.label }}</span>
13
+ <FormTooltip v-if="option.tooltip" :tooltip="option.tooltip" :form-item="formItem" :form-model="formModel" />
14
+ </component>
15
+ </template>
16
+ </el-radio-group>
17
+ <div v-else class="empty">
18
+ <component :is="empty" v-if="empty" :form-model="formModel" :form-item="formItem" :props="props" />
19
+ <template v-else>
20
+ <el-icon><Warning /></el-icon> <span>{{ props.props?.noDataText || t("el.form.nodata") }}</span>
21
+ </template>
22
+ </div>
23
+
24
+ <!-- 子表单项区域 -->
25
+ <div class="choose-sub-items-wrapper" :style="wrapperStyle">
26
+ <div ref="innerRef" class="choose-sub-items" :class="subItemsClass">
27
+ <template v-for="subItem in displaySubItems" :key="subItem.prop">
28
+ <el-form-item :prop="subItem.prop" :label="subItem.label" :label-width="subItem.labelWidth" :class="getSubFormItemStyle(subItem)" v-bind="getSubFormItemProps(subItem)">
29
+ <template v-for="(item, key) in getSubFormItemProps(subItem)?.slots || {}" :key="key" #[key]="row">
30
+ <component :is="item" v-bind="row" />
31
+ </template>
32
+ <FormItem :form-item="subItem" :form-model="formModel" @change="onSubItemChange">
33
+ <template #prefix>
34
+ <slot :name="subItem.prop + 'Prefix'" :form-item="subItem" :form-model="formModel"></slot>
35
+ </template>
36
+ <template #suffix>
37
+ <slot :name="subItem.prop + 'Suffix'" :form-item="subItem" :form-model="formModel"></slot>
38
+ </template>
39
+ </FormItem>
40
+ </el-form-item>
41
+ </template>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </template>
47
+
48
+ <script lang="ts" setup>
49
+ import { Loading, Warning } from "@element-plus/icons-vue"
50
+ import { useLocale } from "easybill-ui"
51
+ import { computed, inject, nextTick, onUnmounted, ref, toRaw, watch, type PropType } from "vue"
52
+ import FormItem from "../FormItem.vue"
53
+ import FormTooltip from "../FormTooltip.vue"
54
+ import { FormItemProps, type CurdFormOptionItem, type Fields, type FormContext, type FormItem as FormItemType } from "../types"
55
+
56
+ const props = defineProps({
57
+ ...FormItemProps,
58
+ modelValue: {
59
+ type: [String, Number, Boolean] as PropType<string | number | boolean | null>,
60
+ default: null,
61
+ },
62
+ })
63
+
64
+ const emit = defineEmits(["update:modelValue"])
65
+ const { t } = useLocale()
66
+ const formContext = inject<FormContext>("curdFormContext")
67
+
68
+ const chooseGroupClass = computed(() => {
69
+ return props?.props?.showType || ""
70
+ })
71
+
72
+ const subItemsClass = computed(() => {
73
+ return props?.props?.subItemsClass || ""
74
+ })
75
+
76
+ const currentValue = computed({
77
+ get: () => props.modelValue,
78
+ set: (val) => {
79
+ emit("update:modelValue", val)
80
+ },
81
+ })
82
+
83
+ const options = computed(() => {
84
+ const allOptions: CurdFormOptionItem[] = []
85
+ if (props.props?.all) {
86
+ const optionItem: CurdFormOptionItem = { value: "", label: t("el.form.all") }
87
+ if (typeof props.props.all === "object") {
88
+ Object.assign(optionItem, props.props.all)
89
+ }
90
+ allOptions.push(optionItem)
91
+ }
92
+ return [...allOptions, ...(props.formItem.options || [])]
93
+ })
94
+
95
+ const empty = computed(() => toRaw(props.props?.empty) || "")
96
+
97
+ const currentOption = computed(() => {
98
+ if (!currentValue.value) return null
99
+ return options.value.find((opt) => opt.value === currentValue.value)
100
+ })
101
+
102
+ // 动画控制状态
103
+ const innerRef = ref<HTMLElement | null>(null)
104
+ const wrapperStyle = ref<Record<string, string>>({})
105
+ const displaySubItems = ref<FormItemType[]>([])
106
+ const isAnimating = ref(false)
107
+
108
+ // 辅助函数:从父级 schema 中查找指定的表单项
109
+ const findFormItemsByProps = (props: string[]) => {
110
+ const parentSchema = formContext?.formSchema || { formItem: [] }
111
+ const allFormItems = parentSchema.formItem || []
112
+ return allFormItems.filter((item: FormItemType) => props.includes(item.prop))
113
+ }
114
+
115
+ // 获取当前选项下需要显示的子表单项
116
+ const visibleSubItems = computed(() => {
117
+ const opt = currentOption.value
118
+ if (!opt) return []
119
+
120
+ if (opt.items?.length) return opt.items
121
+ if (opt.props?.length) return findFormItemsByProps(opt.props)
122
+
123
+ return []
124
+ })
125
+
126
+ // 更新外层高度
127
+ const updateWrapperHeight = (height: number, animate = true) => {
128
+ if (animate) {
129
+ wrapperStyle.value = { height: height + "px" }
130
+ } else {
131
+ wrapperStyle.value = { height: height + "px", transition: "none" }
132
+ nextTick(() => {
133
+ wrapperStyle.value.transition = ""
134
+ })
135
+ }
136
+ }
137
+
138
+ // 监听选项变化,动态计算高度实现平滑过渡
139
+ watch(
140
+ visibleSubItems,
141
+ async (newItems) => {
142
+ // 获取旧高度
143
+ const oldHeight = innerRef.value?.scrollHeight || 0
144
+
145
+ if (newItems.length > 0) {
146
+ // 更新内容
147
+ displaySubItems.value = newItems
148
+ // 等待 DOM 渲染
149
+ await nextTick()
150
+ // 获取新高度
151
+ const newHeight = innerRef.value?.scrollHeight || 0
152
+ if (oldHeight !== newHeight) {
153
+ // 先设置回旧高度(防止跳动)
154
+ wrapperStyle.value = { height: oldHeight + "px" }
155
+ isAnimating.value = true
156
+ // 下一帧设置新高度触发过渡
157
+ requestAnimationFrame(() => {
158
+ updateWrapperHeight(newHeight)
159
+ })
160
+ } else {
161
+ updateWrapperHeight(newHeight, false)
162
+ }
163
+ } else {
164
+ // 无内容时收起
165
+ updateWrapperHeight(0)
166
+ isAnimating.value = false
167
+ setTimeout(() => {
168
+ displaySubItems.value = []
169
+ }, 300)
170
+ }
171
+ },
172
+ { immediate: true },
173
+ )
174
+
175
+ // ResizeObserver 监听内层高度变化
176
+ let resizeObserver: ResizeObserver | null = null
177
+
178
+ watch(innerRef, (el) => {
179
+ if (el) {
180
+ resizeObserver = new ResizeObserver((entries) => {
181
+ // 只在非动画状态且高度变化时更新
182
+ const newHeight = entries[0]?.contentRect.height
183
+ if (newHeight !== undefined && !isAnimating.value) {
184
+ updateWrapperHeight(newHeight, false)
185
+ }
186
+ })
187
+ resizeObserver.observe(el as unknown as Element)
188
+ }
189
+ })
190
+
191
+ onUnmounted(() => {
192
+ if (resizeObserver) {
193
+ resizeObserver.disconnect()
194
+ }
195
+ })
196
+
197
+ // 获取子表单项的额外 props
198
+ const getSubFormItemProps = (subItem: FormItemType) => {
199
+ if (subItem.formItemProps) {
200
+ if (subItem.formItemProps instanceof Function) {
201
+ return subItem.formItemProps(props.formModel, subItem)
202
+ }
203
+ return { ...subItem.formItemProps }
204
+ }
205
+ return {}
206
+ }
207
+
208
+ // 获取子表单项的样式类
209
+ const getSubFormItemStyle = (subItem: FormItemType) => {
210
+ if (!subItem.span) return "el-form-item-full"
211
+ return "el-form-item-" + subItem.span
212
+ }
213
+
214
+ // 子表单项变化事件
215
+ const onSubItemChange = (formModel: Fields, formItem: FormItemType) => {
216
+ // 1. 先执行子表单项自己的 change 事件
217
+ if (formItem.eventObject?.change) {
218
+ formItem.eventObject.change(formModel, formItem, formContext!)
219
+ }
220
+ // 2. 向上传递到 CurdForm,保持与其他表单项一致
221
+ if (formContext?.change) {
222
+ formContext.change(formModel, formItem)
223
+ }
224
+ }
225
+ </script>
@@ -34,6 +34,7 @@ export interface FormItem {
34
34
  props?: FormItemPropObject | ((formModel: Fields, formItem: FormItem) => FormItemPropObject)
35
35
  formItemProps?: FormItemPropObject | ((formModel: Fields, formItem: FormItem) => void)
36
36
  labelWidth?: string | number
37
+ labelPosition?: "left" | "right" | "top" | string
37
38
  span?: number
38
39
  disabled?: boolean
39
40
  tooltip?: string | ((formModel: Fields, formItem: FormItem) => Partial<TooltipProps> | string) | Partial<TooltipProps>
@@ -43,7 +44,7 @@ export interface FormItem {
43
44
  empty?: string | unknown
44
45
  sortIndex?: number
45
46
  }
46
- export type FormItemTypeEmun = "input" | "select" | "radio" | "checkbox" | "input-number" | "switch" | "file" | "date-picker" | "time-picker" | "color-picker" | "value" | "tree-select" | "autocomplete"
47
+ export type FormItemTypeEmun = "input" | "select" | "radio" | "checkbox" | "input-number" | "switch" | "file" | "date-picker" | "time-picker" | "color-picker" | "value" | "tree-select" | "autocomplete" | "choose"
47
48
  export interface FormItemPropObject extends Fields {
48
49
  slots?: Record<string, (props?: FormItemPropObject) => VNode | VNode[] | string | null>
49
50
  all?: boolean | CurdFormOptionItem
@@ -74,6 +75,7 @@ export interface FormContext {
74
75
  setOptions: (prop: string, options: CurdFormOptionItem[], config?: unknown) => void
75
76
  change: (formModel: Fields, formItem: FormItem) => void
76
77
  formModel: Fields
78
+ formSchema: FormSchema
77
79
  formRef: InstanceType<typeof ElForm> | undefined
78
80
  components: Record<string, unknown>
79
81
  }
@@ -85,6 +87,10 @@ export interface CurdFormOptionItem extends OptionItem {
85
87
  disabled?: boolean
86
88
  type?: string | "radio" | "button"
87
89
  rawContent?: boolean
90
+ /** 引用的表单项 prop 数组,用于 choose 类型 */
91
+ props?: string[]
92
+ /** 内联表单项定义,用于 choose 类型 */
93
+ items?: FormItem[]
88
94
  }
89
95
  // interface EventObjectDefault {
90
96
  // change?: (formModel: Fields, formItem: FormItem, proxy: any) => void
@@ -0,0 +1,2 @@
1
+ export { default as CurdGroupForm } from "./src/CurdGroupForm.vue"
2
+ export * from "./types"