easybill-ui 1.3.3 → 1.3.5

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.
@@ -40,19 +40,20 @@
40
40
  <template v-for="(item, key) in getFormItemProps(formItem)?.slots || {}" :key="key" #[key]="row">
41
41
  <component :is="item" v-bind="row" />
42
42
  </template>
43
-
43
+ <component :is="formItem.slots?.top" :formItem="formItem" :formModel="formModel" />
44
44
  <slot :name="formItem.prop" :form-item="formItem" :form-model="formModel"></slot>
45
45
  <FormItem v-if="!(formItem.prop && $slots[formItem.prop])" :form-item="formItem" :form-model="formModel" @change="onChange">
46
46
  <template #prefix>
47
- <slot :name="formItem.prop + 'Prefix'" :form-item="formItem" :form-model="formModel"></slot>
47
+ <slot :name="formItem.prop + 'Prefix'" :formItem="formItem" :formModel="formModel"></slot>
48
48
  </template>
49
49
  <template #suffix>
50
- <slot :name="formItem.prop + 'Suffix'" :form-item="formItem" :form-model="formModel"></slot>
50
+ <slot :name="formItem.prop + 'Suffix'" :formItem="formItem" :formModel="formModel"></slot>
51
51
  </template>
52
52
  </FormItem>
53
- <slot :name="formItem.prop + 'Bottom'" :form-item="formItem" :form-model="formModel"></slot>
53
+ <slot :name="formItem.prop + 'Bottom'" :formItem="formItem" :formModel="formModel"></slot>
54
+ <component :is="formItem.slots?.bottom" :formItem="formItem" :formModel="formModel" />
54
55
  </el-form-item>
55
- <slot :name="formItem.prop + 'Item'" :form-item="formItem" :form-model="formModel"></slot>
56
+ <slot :name="formItem.prop + 'Item'" :formItem="formItem" :formModel="formModel"></slot>
56
57
  </template>
57
58
  </template>
58
59
  <template v-if="$slots['operate-button']">
@@ -67,7 +68,7 @@
67
68
  import { ElForm, type FormRules } from "element-plus"
68
69
  import { computed, getCurrentInstance, onMounted, provide, reactive, ref, triggerRef, watch, type PropType } from "vue"
69
70
  import FormItem from "./FormItem.vue"
70
- import type { CurdFormOptionItem, Fields, FormContext, FormItem as FormItemType, FormSchema } from "./types"
71
+ import type { CurdFormOptionItem, Fields, FormContext, FormItemProvider, FormItemSearchResult, FormItem as FormItemType, FormSchema } from "./types"
71
72
  import { deepClone } from "./utils/common"
72
73
 
73
74
  const props = defineProps({
@@ -97,6 +98,101 @@ const sFormSchema = ref(props.formSchema)
97
98
  let formModel = reactive<Fields>(props.modelValue || {})
98
99
  const curdFormContext = reactive<FormContext>({} as FormContext)
99
100
  const instance = getCurrentInstance()
101
+ const formItemProviders = new Map<string | symbol, FormItemProvider["getItems"]>()
102
+
103
+ const normalizeFormItems = (items?: FormItemType[] | null) => {
104
+ return Array.isArray(items) ? items : []
105
+ }
106
+
107
+ const walkBuiltInFormItems = (items: FormItemType[], callback: (item: FormItemType, parent?: FormItemType, path?: string) => void | boolean, parent?: FormItemType, parentPath = "") => {
108
+ for (const item of normalizeFormItems(items)) {
109
+ const currentPath = parentPath ? `${parentPath}.${item.prop}` : item.prop
110
+ const shouldStop = callback(item, parent, currentPath)
111
+ if (shouldStop) return true
112
+
113
+ if (item.type === "choose" && item.options) {
114
+ for (const option of item.options) {
115
+ if (option.items?.length && walkBuiltInFormItems(option.items, callback, item, `${currentPath}.${option.value}`)) {
116
+ return true
117
+ }
118
+ }
119
+ }
120
+ }
121
+ return false
122
+ }
123
+
124
+ const getRegisteredFormItems = (providerId?: string | symbol) => {
125
+ const items: FormItemType[] = []
126
+ formItemProviders.forEach((getItems, id) => {
127
+ if (typeof providerId !== "undefined" && id !== providerId) return
128
+ items.push(...normalizeFormItems(getItems()))
129
+ })
130
+ return items
131
+ }
132
+
133
+ const collectDefaultValues = (items: FormItemType[]) => {
134
+ const values: Fields = {}
135
+ walkBuiltInFormItems(items, (item) => {
136
+ item.eventObject ??= {}
137
+ if (typeof item.value !== "undefined" && item.prop && (typeof formModel[item.prop] == "undefined" || formModel[item.prop] === "" || formModel[item.prop] === null)) {
138
+ values[item.prop] = item.value
139
+ }
140
+ })
141
+ return values
142
+ }
143
+
144
+ const loadFormItemOptions = async (formItem: FormItemType, option?: unknown) => {
145
+ if (formItem.asyncOptions && !instance?.isUnmounted) {
146
+ formItem.loading = true
147
+ triggerRef(schemaItems)
148
+ formItem.options =
149
+ (await formItem
150
+ .asyncOptions(formModel, formItem, curdFormContext, option)
151
+ .catch((err) => console.error("loadOptionError", err))
152
+ .finally(() => (formItem.loading = false))) || []
153
+ triggerRef(schemaItems)
154
+ if (!instance?.isUnmounted && formItem.eventObject?.optionLoaded) formItem.eventObject.optionLoaded(formModel, formItem, curdFormContext, option)
155
+ }
156
+ return formItem.options || []
157
+ }
158
+
159
+ const loadAutoloadFormItems = (items: FormItemType[]) => {
160
+ walkBuiltInFormItems(items, (item) => {
161
+ if (item.asyncOptions && (item.autoload || typeof item.autoload == "undefined") && item.asyncOptions instanceof Function) {
162
+ void loadFormItemOptions(item)
163
+ }
164
+ })
165
+ }
166
+
167
+ const findFormItemInItems = (items: FormItemType[], prop: string, providerId?: string | symbol): FormItemSearchResult | undefined => {
168
+ let result: FormItemSearchResult | undefined
169
+ walkBuiltInFormItems(items, (item, parent, path) => {
170
+ if (item.prop == prop) {
171
+ result = { item, parent, path: path || prop, providerId }
172
+ return true
173
+ }
174
+ return false
175
+ })
176
+ return result
177
+ }
178
+
179
+ const refreshFormItems = (providerId?: string | symbol) => {
180
+ const registeredItems = getRegisteredFormItems(providerId)
181
+ Object.assign(formModel, collectDefaultValues(registeredItems))
182
+ loadAutoloadFormItems(registeredItems)
183
+ triggerRef(schemaItems)
184
+ triggerRef(rules)
185
+ }
186
+
187
+ const registerFormItems = (provider: FormItemProvider) => {
188
+ formItemProviders.set(provider.id, provider.getItems)
189
+ refreshFormItems(provider.id)
190
+ return () => {
191
+ formItemProviders.delete(provider.id)
192
+ triggerRef(schemaItems)
193
+ triggerRef(rules)
194
+ }
195
+ }
100
196
 
101
197
  watch(
102
198
  () => props.modelValue,
@@ -116,28 +212,7 @@ watch(
116
212
  { deep: true },
117
213
  )
118
214
  // 先从schema中读取默认值
119
- const schemaValues = sFormSchema.value.formItem.reduce<Fields>((previousValue, currentValue) => {
120
- currentValue.eventObject ??= {}
121
- if (typeof currentValue.value !== "undefined" && currentValue.prop && (typeof formModel[currentValue.prop] == "undefined" || formModel[currentValue.prop] === "" || formModel[currentValue.prop] === null)) {
122
- previousValue[currentValue.prop] = currentValue.value
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
-
139
- return previousValue
140
- }, {})
215
+ const schemaValues = collectDefaultValues(sFormSchema.value.formItem)
141
216
 
142
217
  Object.assign(formModel, schemaValues)
143
218
  // 如果有默认值,则覆盖
@@ -157,29 +232,7 @@ const schemaItems = computed(() => {
157
232
  })
158
233
  })
159
234
  // 异步设置默认数据
160
- sFormSchema.value.formItem.forEach(async (item) => {
161
- // 异步选项 - 顶层表单项
162
- if (item.asyncOptions && (item.autoload || typeof item.autoload == "undefined") && item.asyncOptions instanceof Function) {
163
- item.loading = true
164
- item.options = await item.asyncOptions(formModel, item, curdFormContext).finally(() => (item.loading = false))
165
- if (!instance?.isUnmounted && item.eventObject?.optionLoaded) item.eventObject.optionLoaded(formModel, item, curdFormContext)
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
- }
182
- })
235
+ loadAutoloadFormItems(sFormSchema.value.formItem)
183
236
 
184
237
  // 生成表单验证规则
185
238
  const rules = computed(() => {
@@ -196,7 +249,7 @@ const rules = computed(() => {
196
249
  baseRules = sFormSchema.value.rules || {}
197
250
  }
198
251
 
199
- // 合并 choose 类型 items 的验证规则
252
+ // 合并 choose 类型 items 和自定义组件注册项的验证规则
200
253
  const mergedRules: FormRules = { ...baseRules }
201
254
  sFormSchema.value.formItem.forEach((formItem) => {
202
255
  if (formItem.type === "choose" && formItem.options) {
@@ -211,6 +264,11 @@ const rules = computed(() => {
211
264
  })
212
265
  }
213
266
  })
267
+ walkBuiltInFormItems(getRegisteredFormItems(), (formItem) => {
268
+ if (formItem.prop && formItem.rules) {
269
+ mergedRules[formItem.prop] = formItem.rules
270
+ }
271
+ })
214
272
 
215
273
  return mergedRules
216
274
  })
@@ -220,29 +278,18 @@ const rules = computed(() => {
220
278
  * @param prop 表单项的 prop
221
279
  * @returns 找到的表单项对象,包含 parent 信息
222
280
  */
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
- }
281
+ const findFormItem = (prop: string): FormItemSearchResult | undefined => {
282
+ const schemaResult = findFormItemInItems(sFormSchema.value.formItem, prop)
283
+ if (schemaResult) return schemaResult
229
284
 
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
- }
285
+ for (const [providerId, getItems] of formItemProviders) {
286
+ const providerResult = findFormItemInItems(normalizeFormItems(getItems()), prop, providerId)
287
+ if (providerResult) return providerResult
242
288
  }
243
289
 
244
290
  return undefined
245
291
  }
292
+ const hasFormItem = (prop: string) => Boolean(findFormItem(prop))
246
293
 
247
294
  // 供外部使用
248
295
  const validate = (callback: (valid: boolean) => void) => {
@@ -257,18 +304,14 @@ const loadOptions = async (prop: string, option?: unknown) => {
257
304
  }
258
305
 
259
306
  const cur = result.item
260
- if (cur.asyncOptions && !instance?.isUnmounted) {
261
- cur.loading = true
262
- triggerRef(schemaItems)
263
- cur.options =
264
- (await cur
265
- .asyncOptions(formModel, cur, curdFormContext, option)
266
- .catch((err) => console.error("loadOptionError", err))
267
- .finally(() => (cur.loading = false))) || []
268
- triggerRef(schemaItems)
269
- if (!instance?.isUnmounted && cur.eventObject?.optionLoaded) cur.eventObject.optionLoaded(formModel, cur, curdFormContext, option)
307
+ return loadFormItemOptions(cur, option)
308
+ }
309
+ const contextLoadOptions = (prop: string, option?: unknown) => {
310
+ if (hasFormItem(prop)) {
311
+ return loadOptions(prop, option)
270
312
  }
271
- return cur?.options || []
313
+
314
+ return props.extendContext?.loadOptions?.(prop, option)
272
315
  }
273
316
  // 给某个item赋值options
274
317
  const setOptions = async (prop: string, options: CurdFormOptionItem[], option?: unknown) => {
@@ -360,13 +403,21 @@ const getFormProps = computed(() => {
360
403
  return { ...args }
361
404
  })
362
405
 
363
- curdFormContext.loadOptions = loadOptions
406
+ curdFormContext.loadOptions = contextLoadOptions
364
407
  curdFormContext.setOptions = setOptions
365
408
  curdFormContext.change = onChange
366
409
  curdFormContext.formModel = formModel
367
410
  curdFormContext.formSchema = props.formSchema
368
-
369
- if (props.extendContext) Object.assign(curdFormContext, props.extendContext)
411
+ curdFormContext.findFormItem = findFormItem
412
+ curdFormContext.hasFormItem = hasFormItem
413
+ curdFormContext.registerFormItems = registerFormItems
414
+ curdFormContext.refreshFormItems = refreshFormItems
415
+
416
+ if (props.extendContext) {
417
+ const extendContext = { ...props.extendContext }
418
+ delete extendContext.loadOptions
419
+ Object.assign(curdFormContext, extendContext)
420
+ }
370
421
 
371
422
  provide("curdFormContext", curdFormContext)
372
423
  onMounted(() => {
@@ -384,6 +435,10 @@ defineExpose({
384
435
  getFormItemStyle,
385
436
  getFormItemProps,
386
437
  validate,
438
+ findFormItem,
439
+ hasFormItem,
440
+ registerFormItems,
441
+ refreshFormItems,
387
442
  loadOptions,
388
443
  setOptions,
389
444
  onChange,
@@ -71,13 +71,17 @@ export interface TooltipProps {
71
71
  trigger: TooltipTriggerType
72
72
  }
73
73
  export interface FormContext {
74
- loadOptions: (prop: string, config?: Fields) => void
75
- setOptions: (prop: string, options: CurdFormOptionItem[], config?: unknown) => void
74
+ loadOptions: (prop: string, config?: unknown) => void | Promise<CurdFormOptionItem[]> | CurdFormOptionItem[]
75
+ setOptions: (prop: string, options: CurdFormOptionItem[], config?: unknown) => void | Promise<void>
76
76
  change: (formModel: Fields, formItem: FormItem) => void
77
77
  formModel: Fields
78
78
  formSchema: FormSchema
79
79
  formRef: InstanceType<typeof ElForm> | undefined
80
80
  components: Record<string, unknown>
81
+ findFormItem?: (prop: string) => FormItemSearchResult | undefined
82
+ hasFormItem?: (prop: string) => boolean
83
+ registerFormItems?: (provider: FormItemProvider) => () => void
84
+ refreshFormItems?: (providerId?: string | symbol) => void
81
85
  }
82
86
  export interface Fields {
83
87
  [key: string]: unknown
@@ -92,6 +96,16 @@ export interface CurdFormOptionItem extends OptionItem {
92
96
  /** 内联表单项定义,用于 choose 类型 */
93
97
  items?: FormItem[]
94
98
  }
99
+ export interface FormItemProvider {
100
+ id: string | symbol
101
+ getItems: () => FormItem[] | undefined | null
102
+ }
103
+ export interface FormItemSearchResult {
104
+ item: FormItem
105
+ parent?: FormItem
106
+ path: string
107
+ providerId?: string | symbol
108
+ }
95
109
  // interface EventObjectDefault {
96
110
  // change?: (formModel: Fields, formItem: FormItem, proxy: any) => void
97
111
  // }
@@ -70,13 +70,11 @@ const loadOptions = (prop: string, config?: unknown) => {
70
70
  let has = false
71
71
  for (const index in props.schema.items) {
72
72
  const group = props.schema.items[index]
73
- const formItem = group.schema.formItem
74
- for (const i in formItem) {
75
- const item = formItem[i]
76
- if (item.prop == prop) {
77
- has = true
78
- formRef.value[(group.step || "") + group.groupName + index]?.loadOptions(prop, config)
79
- }
73
+ const form = formRef.value[(group.step || "") + group.groupName + index]
74
+ const hasFormItem = form?.hasFormItem ? form.hasFormItem(prop) : group.schema.formItem.some((item) => item.prop == prop)
75
+ if (hasFormItem) {
76
+ has = true
77
+ form?.loadOptions(prop, config)
80
78
  }
81
79
  }
82
80
  if (!has) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easybill-ui",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "A component library for easybill",
5
5
  "author": "tuchongyang <779311998@qq.com>",
6
6
  "private": false,