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.
- package/components/CurdForm/src/CurdForm.vue +252 -39
- package/components/CurdForm/src/components/index.ts +2 -0
- package/components/CurdForm/src/components/schema-form-choose.vue +225 -0
- package/components/CurdForm/src/types.ts +7 -1
- package/components/CurdGroupForm/index.ts +2 -0
- package/components/CurdGroupForm/src/CurdGroupForm.vue +95 -0
- package/components/CurdGroupForm/src/components/CurdFormGroup.vue +39 -0
- package/components/CurdGroupForm/src/hooks/useFormHook.ts +55 -0
- package/components/CurdGroupForm/types.ts +22 -0
- package/components/FormDialog/src/FormDialog.vue +107 -47
- package/components/FormDialog/src/hooks/index.ts +2 -0
- package/components/FormDialog/src/hooks/useStepList.ts +49 -0
- package/components/FormDialog/src/hooks/useStepNavigation.ts +74 -0
- package/components/FormDialog/src/types.ts +29 -1
- package/index.ts +5 -2
- package/package.json +1 -1
- package/theme-chalk/curd-form.css +1 -1
- package/theme-chalk/curd-group-form.css +1 -0
- package/theme-chalk/index.css +1 -1
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<slot :name="formItem.prop + '
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
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
|
|
148
|
-
if (
|
|
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
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|