easybill-ui 1.3.3 → 1.4.0

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.
@@ -28,31 +28,24 @@
28
28
 
29
29
  <!-- 其他类型使用 el-form-item 包裹 -->
30
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
- >
31
+ <el-form-item v-if="!$slots[formItem.prop + 'Item']" :required="hasRequiredRule(formItem.prop)" :label="formItem.label" :prop="formItem.prop" :label-width="formItem.labelWidth" :class="getFormItemStyle(formItem)" v-bind="getFormItemProps(formItem)">
40
32
  <template v-for="(item, key) in getFormItemProps(formItem)?.slots || {}" :key="key" #[key]="row">
41
33
  <component :is="item" v-bind="row" />
42
34
  </template>
43
-
35
+ <component :is="formItem.slots?.top" :formItem="formItem" :formModel="formModel" />
44
36
  <slot :name="formItem.prop" :form-item="formItem" :form-model="formModel"></slot>
45
37
  <FormItem v-if="!(formItem.prop && $slots[formItem.prop])" :form-item="formItem" :form-model="formModel" @change="onChange">
46
38
  <template #prefix>
47
- <slot :name="formItem.prop + 'Prefix'" :form-item="formItem" :form-model="formModel"></slot>
39
+ <slot :name="formItem.prop + 'Prefix'" :formItem="formItem" :formModel="formModel"></slot>
48
40
  </template>
49
41
  <template #suffix>
50
- <slot :name="formItem.prop + 'Suffix'" :form-item="formItem" :form-model="formModel"></slot>
42
+ <slot :name="formItem.prop + 'Suffix'" :formItem="formItem" :formModel="formModel"></slot>
51
43
  </template>
52
44
  </FormItem>
53
- <slot :name="formItem.prop + 'Bottom'" :form-item="formItem" :form-model="formModel"></slot>
45
+ <slot :name="formItem.prop + 'Bottom'" :formItem="formItem" :formModel="formModel"></slot>
46
+ <component :is="formItem.slots?.bottom" :formItem="formItem" :formModel="formModel" />
54
47
  </el-form-item>
55
- <slot :name="formItem.prop + 'Item'" :form-item="formItem" :form-model="formModel"></slot>
48
+ <slot :name="formItem.prop + 'Item'" :formItem="formItem" :formModel="formModel"></slot>
56
49
  </template>
57
50
  </template>
58
51
  <template v-if="$slots['operate-button']">
@@ -64,10 +57,10 @@
64
57
  </template>
65
58
 
66
59
  <script lang="ts" setup>
67
- import { ElForm, type FormRules } from "element-plus"
60
+ import { ElForm, type FormItemRule, type FormRules } from "element-plus"
68
61
  import { computed, getCurrentInstance, onMounted, provide, reactive, ref, triggerRef, watch, type PropType } from "vue"
69
62
  import FormItem from "./FormItem.vue"
70
- import type { CurdFormOptionItem, Fields, FormContext, FormItem as FormItemType, FormSchema } from "./types"
63
+ import type { CurdFormOptionItem, Fields, FormContext, FormItemProvider, FormItemSearchResult, FormItem as FormItemType, FormSchema } from "./types"
71
64
  import { deepClone } from "./utils/common"
72
65
 
73
66
  const props = defineProps({
@@ -97,6 +90,101 @@ const sFormSchema = ref(props.formSchema)
97
90
  let formModel = reactive<Fields>(props.modelValue || {})
98
91
  const curdFormContext = reactive<FormContext>({} as FormContext)
99
92
  const instance = getCurrentInstance()
93
+ const formItemProviders = new Map<string | symbol, FormItemProvider["getItems"]>()
94
+
95
+ const normalizeFormItems = (items?: FormItemType[] | null) => {
96
+ return Array.isArray(items) ? items : []
97
+ }
98
+
99
+ const walkBuiltInFormItems = (items: FormItemType[], callback: (item: FormItemType, parent?: FormItemType, path?: string) => void | boolean, parent?: FormItemType, parentPath = "") => {
100
+ for (const item of normalizeFormItems(items)) {
101
+ const currentPath = parentPath ? `${parentPath}.${item.prop}` : item.prop
102
+ const shouldStop = callback(item, parent, currentPath)
103
+ if (shouldStop) return true
104
+
105
+ if (item.type === "choose" && item.options) {
106
+ for (const option of item.options) {
107
+ if (option.items?.length && walkBuiltInFormItems(option.items, callback, item, `${currentPath}.${option.value}`)) {
108
+ return true
109
+ }
110
+ }
111
+ }
112
+ }
113
+ return false
114
+ }
115
+
116
+ const getRegisteredFormItems = (providerId?: string | symbol) => {
117
+ const items: FormItemType[] = []
118
+ formItemProviders.forEach((getItems, id) => {
119
+ if (typeof providerId !== "undefined" && id !== providerId) return
120
+ items.push(...normalizeFormItems(getItems()))
121
+ })
122
+ return items
123
+ }
124
+
125
+ const collectDefaultValues = (items: FormItemType[]) => {
126
+ const values: Fields = {}
127
+ walkBuiltInFormItems(items, (item) => {
128
+ item.eventObject ??= {}
129
+ if (typeof item.value !== "undefined" && item.prop && (typeof formModel[item.prop] == "undefined" || formModel[item.prop] === "" || formModel[item.prop] === null)) {
130
+ values[item.prop] = item.value
131
+ }
132
+ })
133
+ return values
134
+ }
135
+
136
+ const loadFormItemOptions = async (formItem: FormItemType, option?: unknown) => {
137
+ if (formItem.asyncOptions && !instance?.isUnmounted) {
138
+ formItem.loading = true
139
+ triggerRef(schemaItems)
140
+ formItem.options =
141
+ (await formItem
142
+ .asyncOptions(formModel, formItem, curdFormContext, option)
143
+ .catch((err) => console.error("loadOptionError", err))
144
+ .finally(() => (formItem.loading = false))) || []
145
+ triggerRef(schemaItems)
146
+ if (!instance?.isUnmounted && formItem.eventObject?.optionLoaded) formItem.eventObject.optionLoaded(formModel, formItem, curdFormContext, option)
147
+ }
148
+ return formItem.options || []
149
+ }
150
+
151
+ const loadAutoloadFormItems = (items: FormItemType[]) => {
152
+ walkBuiltInFormItems(items, (item) => {
153
+ if (item.asyncOptions && (item.autoload || typeof item.autoload == "undefined") && item.asyncOptions instanceof Function) {
154
+ void loadFormItemOptions(item)
155
+ }
156
+ })
157
+ }
158
+
159
+ const findFormItemInItems = (items: FormItemType[], prop: string, providerId?: string | symbol): FormItemSearchResult | undefined => {
160
+ let result: FormItemSearchResult | undefined
161
+ walkBuiltInFormItems(items, (item, parent, path) => {
162
+ if (item.prop == prop) {
163
+ result = { item, parent, path: path || prop, providerId }
164
+ return true
165
+ }
166
+ return false
167
+ })
168
+ return result
169
+ }
170
+
171
+ const refreshFormItems = (providerId?: string | symbol) => {
172
+ const registeredItems = getRegisteredFormItems(providerId)
173
+ Object.assign(formModel, collectDefaultValues(registeredItems))
174
+ loadAutoloadFormItems(registeredItems)
175
+ triggerRef(schemaItems)
176
+ triggerRef(rules)
177
+ }
178
+
179
+ const registerFormItems = (provider: FormItemProvider) => {
180
+ formItemProviders.set(provider.id, provider.getItems)
181
+ refreshFormItems(provider.id)
182
+ return () => {
183
+ formItemProviders.delete(provider.id)
184
+ triggerRef(schemaItems)
185
+ triggerRef(rules)
186
+ }
187
+ }
100
188
 
101
189
  watch(
102
190
  () => props.modelValue,
@@ -116,28 +204,7 @@ watch(
116
204
  { deep: true },
117
205
  )
118
206
  // 先从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
- }, {})
207
+ const schemaValues = collectDefaultValues(sFormSchema.value.formItem)
141
208
 
142
209
  Object.assign(formModel, schemaValues)
143
210
  // 如果有默认值,则覆盖
@@ -157,29 +224,7 @@ const schemaItems = computed(() => {
157
224
  })
158
225
  })
159
226
  // 异步设置默认数据
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
- })
227
+ loadAutoloadFormItems(sFormSchema.value.formItem)
183
228
 
184
229
  // 生成表单验证规则
185
230
  const rules = computed(() => {
@@ -196,7 +241,7 @@ const rules = computed(() => {
196
241
  baseRules = sFormSchema.value.rules || {}
197
242
  }
198
243
 
199
- // 合并 choose 类型 items 的验证规则
244
+ // 合并 choose 类型 items 和自定义组件注册项的验证规则
200
245
  const mergedRules: FormRules = { ...baseRules }
201
246
  sFormSchema.value.formItem.forEach((formItem) => {
202
247
  if (formItem.type === "choose" && formItem.options) {
@@ -211,38 +256,39 @@ const rules = computed(() => {
211
256
  })
212
257
  }
213
258
  })
259
+ walkBuiltInFormItems(getRegisteredFormItems(), (formItem) => {
260
+ if (formItem.prop && formItem.rules) {
261
+ mergedRules[formItem.prop] = formItem.rules
262
+ }
263
+ })
214
264
 
215
265
  return mergedRules
216
266
  })
267
+ const normalizeFormItemRules = (rule: FormItemRule | FormItemRule[] | undefined) => {
268
+ if (!rule) return []
269
+ return Array.isArray(rule) ? rule : [rule]
270
+ }
271
+ const hasRequiredRule = (prop: string) => {
272
+ return normalizeFormItemRules(rules.value[prop] as FormItemRule | FormItemRule[] | undefined).some((rule) => Object.prototype.hasOwnProperty.call(rule, "required") && rule.required)
273
+ }
217
274
 
218
275
  /**
219
276
  * 递归查找表单项
220
277
  * @param prop 表单项的 prop
221
278
  * @returns 找到的表单项对象,包含 parent 信息
222
279
  */
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
- }
280
+ const findFormItem = (prop: string): FormItemSearchResult | undefined => {
281
+ const schemaResult = findFormItemInItems(sFormSchema.value.formItem, prop)
282
+ if (schemaResult) return schemaResult
229
283
 
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
- }
284
+ for (const [providerId, getItems] of formItemProviders) {
285
+ const providerResult = findFormItemInItems(normalizeFormItems(getItems()), prop, providerId)
286
+ if (providerResult) return providerResult
242
287
  }
243
288
 
244
289
  return undefined
245
290
  }
291
+ const hasFormItem = (prop: string) => Boolean(findFormItem(prop))
246
292
 
247
293
  // 供外部使用
248
294
  const validate = (callback: (valid: boolean) => void) => {
@@ -257,18 +303,14 @@ const loadOptions = async (prop: string, option?: unknown) => {
257
303
  }
258
304
 
259
305
  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)
306
+ return loadFormItemOptions(cur, option)
307
+ }
308
+ const contextLoadOptions = (prop: string, option?: unknown) => {
309
+ if (hasFormItem(prop)) {
310
+ return loadOptions(prop, option)
270
311
  }
271
- return cur?.options || []
312
+
313
+ return props.extendContext?.loadOptions?.(prop, option)
272
314
  }
273
315
  // 给某个item赋值options
274
316
  const setOptions = async (prop: string, options: CurdFormOptionItem[], option?: unknown) => {
@@ -360,13 +402,21 @@ const getFormProps = computed(() => {
360
402
  return { ...args }
361
403
  })
362
404
 
363
- curdFormContext.loadOptions = loadOptions
405
+ curdFormContext.loadOptions = contextLoadOptions
364
406
  curdFormContext.setOptions = setOptions
365
407
  curdFormContext.change = onChange
366
408
  curdFormContext.formModel = formModel
367
409
  curdFormContext.formSchema = props.formSchema
368
-
369
- if (props.extendContext) Object.assign(curdFormContext, props.extendContext)
410
+ curdFormContext.findFormItem = findFormItem
411
+ curdFormContext.hasFormItem = hasFormItem
412
+ curdFormContext.registerFormItems = registerFormItems
413
+ curdFormContext.refreshFormItems = refreshFormItems
414
+
415
+ if (props.extendContext) {
416
+ const extendContext = { ...props.extendContext }
417
+ delete extendContext.loadOptions
418
+ Object.assign(curdFormContext, extendContext)
419
+ }
370
420
 
371
421
  provide("curdFormContext", curdFormContext)
372
422
  onMounted(() => {
@@ -384,6 +434,10 @@ defineExpose({
384
434
  getFormItemStyle,
385
435
  getFormItemProps,
386
436
  validate,
437
+ findFormItem,
438
+ hasFormItem,
439
+ registerFormItems,
440
+ refreshFormItems,
387
441
  loadOptions,
388
442
  setOptions,
389
443
  onChange,
@@ -5,12 +5,12 @@
5
5
  <slot name="prefix"></slot>
6
6
  <component :is="comp" v-if="comp" v-model="modelRef[props.formItem.prop || '']" :form-item="props.formItem" :form-model="modelRef" :props="formItemProps" :event-object="eventObject">
7
7
  <template v-for="(item, key) in formItemProps.slots" :key="key" #[key]="slotScope">
8
- <component :is="item" v-model="modelRef[props.formItem.prop || '']" :form-item="props.formItem" :form-model="modelRef" :props="formItemProps" :event-object="eventObject" />
8
+ <component :is="item" v-model="modelRef[props.formItem.prop || '']" v-bind="slotScope" :form-item="props.formItem" :form-model="modelRef" :props="formItemProps" :event-object="eventObject" />
9
9
  </template>
10
10
  </component>
11
11
  <component :is="props.formItem.type" v-else v-model="modelRef[props.formItem.prop || '']" v-bind="formItemProps" v-on="eventObject">
12
12
  <template v-for="(item, key) in formItemProps.slots" :key="key" #[key]="slotScope">
13
- <component :is="item" v-model="modelRef[props.formItem.prop || '']" :form-item="props.formItem" :form-model="modelRef" :props="formItemProps" :event-object="eventObject" />
13
+ <component :is="item" v-model="modelRef[props.formItem.prop || '']" v-bind="slotScope" :form-item="props.formItem" :form-model="modelRef" :props="formItemProps" :event-object="eventObject" />
14
14
  </template>
15
15
  </component>
16
16
 
@@ -10,12 +10,11 @@
10
10
  </template>
11
11
  <script lang="ts" setup>
12
12
  import { Warning } from "@element-plus/icons-vue"
13
- import { type ElTooltipProps } from "element-plus"
14
13
  import { type PropType, computed } from "vue"
15
- import type { Fields, FormItem } from "./types"
14
+ import type { Fields, FormItem, TooltipProps } from "./types"
16
15
  const props = defineProps({
17
16
  tooltip: {
18
- type: [String, Function] as PropType<string | ((formModel: Fields, formItem: FormItem) => Partial<ElTooltipProps> | string) | Partial<ElTooltipProps>>,
17
+ type: [String, Function, Object] as PropType<string | ((formModel: Fields, formItem: FormItem) => Partial<TooltipProps> | string) | Partial<TooltipProps>>,
19
18
  default: null,
20
19
  },
21
20
  formItem: {
@@ -40,11 +39,11 @@ const tooltipModel = computed(() => {
40
39
  return getTooltip(tooltip)
41
40
  })
42
41
  // 获取组件tooltip内容
43
- const getTooltip = (tooltip: Partial<ElTooltipProps> | string): Partial<ElTooltipProps> => {
42
+ const getTooltip = (tooltip: Partial<TooltipProps> | string): Partial<TooltipProps> => {
44
43
  if (!tooltip) {
45
44
  return { content: "" }
46
45
  }
47
- let t: Partial<ElTooltipProps> = {}
46
+ let t: Partial<TooltipProps> = {}
48
47
  if (typeof tooltip == "string") {
49
48
  t = { content: tooltip }
50
49
  } else {
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <el-date-picker v-model="model" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD" v-bind="props" v-on="eventObject">
3
- <template v-for="(item, key) in props.props?.slots" :key="key" #[key]="slotScope">
4
- <component :is="item" v-model="model" :form-item="props.formItem" :form-model="props.formModel" :props="formItemProps" :event-object="eventObject" />
3
+ <template v-for="(item, key) in props.slots" :key="key" #[key]="slotScope">
4
+ <component :is="item" v-model="model" v-bind="slotScope" :form-item="formItem" :form-model="formModel" :props="props" :event-object="eventObject" />
5
5
  </template>
6
6
  </el-date-picker>
7
7
  </template>
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <el-input-number v-model="model" v-bind="props.props" v-on="eventObject">
3
3
  <template v-for="(item, key) in props.props?.slots" :key="key" #[key]="slotScope">
4
- <component :is="item" v-model="model" :form-item="props.formItem" :form-model="props.formModel" :props="props.props" :event-object="eventObject" />
4
+ <component :is="item" v-model="model" v-bind="slotScope" :form-item="props.formItem" :form-model="props.formModel" :props="props.props" :event-object="eventObject" />
5
5
  </template>
6
6
  </el-input-number>
7
7
  </template>
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <el-time-picker v-model="model" v-bind="props" v-on="eventObject">
3
- <template v-for="(item, key) in props.props?.slots" :key="key" #[key]="slotScope">
4
- <component :is="item" v-model="model" :form-item="formItem" :form-model="formModel" :props="formItemProps" :event-object="eventObject" />
3
+ <template v-for="(item, key) in props.slots" :key="key" #[key]="slotScope">
4
+ <component :is="item" v-model="model" v-bind="slotScope" :form-item="formItem" :form-model="formModel" :props="props" :event-object="eventObject" />
5
5
  </template>
6
6
  </el-time-picker>
7
7
  </template>
@@ -1,4 +1,4 @@
1
- import { ElForm, type FormItemRule, type FormRules, type TooltipTriggerType } from "element-plus"
1
+ import { ElForm, type FormItemRule, type FormRules } from "element-plus"
2
2
  import type { Arrayable } from "element-plus/es/utils"
3
3
  import { defineComponent, type PropType, type VNode } from "vue"
4
4
  import type { OptionItem } from "../../ConstantStatus"
@@ -32,7 +32,7 @@ export interface FormItem {
32
32
  hidden?: boolean | ((model: Fields) => boolean)
33
33
  rules?: Arrayable<FormItemRule>
34
34
  props?: FormItemPropObject | ((formModel: Fields, formItem: FormItem) => FormItemPropObject)
35
- formItemProps?: FormItemPropObject | ((formModel: Fields, formItem: FormItem) => void)
35
+ formItemProps?: FormItemPropObject | ((formModel: Fields, formItem: FormItem) => FormItemPropObject)
36
36
  labelWidth?: string | number
37
37
  labelPosition?: "left" | "right" | "top" | string
38
38
  span?: number
@@ -47,37 +47,41 @@ export interface FormItem {
47
47
  export type FormItemTypeEmun = "input" | "select" | "radio" | "checkbox" | "input-number" | "switch" | "file" | "date-picker" | "time-picker" | "color-picker" | "value" | "tree-select" | "autocomplete" | "choose"
48
48
  export interface FormItemPropObject extends Fields {
49
49
  slots?: Record<string, (props?: FormItemPropObject) => VNode | VNode[] | string | null>
50
- all?: boolean | CurdFormOptionItem
50
+ all?: boolean | OptionItem
51
51
  empty?: string | VNode
52
52
  noDataText?: string
53
53
  style?: Record<string, string> | string
54
54
  }
55
- export interface TooltipProps {
56
- effect: "dark" | "light"
57
- content: string
58
- rawContent: boolean
59
- placement: import("element-plus/es/components/popper").Placement
60
- disabled: boolean
61
- offset: number
62
- transition: string
63
- popperOptions: unknown
64
- showAfter: number
65
- showArrow: boolean
66
- hideAfter: number
67
- autoClose: number
68
- popperClass: string
69
- enterable: boolean
70
- teleported: boolean
71
- trigger: TooltipTriggerType
55
+ export interface TooltipProps extends Record<string, unknown> {
56
+ effect?: "dark" | "light" | string
57
+ content?: string
58
+ rawContent?: boolean
59
+ placement?: string
60
+ disabled?: boolean
61
+ offset?: number
62
+ transition?: string
63
+ popperOptions?: unknown
64
+ showAfter?: number
65
+ showArrow?: boolean
66
+ hideAfter?: number
67
+ autoClose?: number
68
+ popperClass?: string
69
+ enterable?: boolean
70
+ teleported?: boolean
71
+ trigger?: string | string[]
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,13 +96,24 @@ 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
  // }
112
+ export type EventObjectHandler = (formModel: Fields, formItem: FormItem, context?: FormContext, ...args: unknown[]) => void | boolean
98
113
  export interface EventObject {
99
- change?: (formModel: Fields, formItem: FormItem, context: FormContext, ...args: unknown[]) => void | boolean
100
- optionLoaded?: (formModel: Fields, formItem: FormItem, context: FormContext, config?: unknown) => void
101
- [key: string]: ((formModel: Fields, formItem: FormItem, context: FormContext, config?: unknown) => void) | undefined
114
+ change?: EventObjectHandler
115
+ optionLoaded?: EventObjectHandler
116
+ [key: string]: EventObjectHandler | undefined
102
117
  }
103
118
  // export interface OptionItem {
104
119
  // label: string
@@ -121,7 +136,7 @@ export const FormItemProps = {
121
136
  default: () => ({}),
122
137
  },
123
138
  eventObject: {
124
- type: Object as PropType<Fields & { change: (formModel: Fields, formItem: FormItem) => void | boolean }>,
139
+ type: Object as PropType<EventObject>,
125
140
  default: () => ({}),
126
141
  },
127
142
  modelValue: {
@@ -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) {
@@ -1,21 +1,21 @@
1
1
  import { CurdForm } from "easybill-ui"
2
2
  import { ref, type Ref } from "vue"
3
- import type { Schema, SchemaItem } from "../../types"
3
+ import type { CurdGroupFormSchema, CurdGroupFormSchemaItem } from "../../types"
4
4
 
5
5
  export const useFormHook = (): {
6
6
  form: Ref<Record<string, unknown>>
7
7
  formRef: Ref<Record<string, InstanceType<typeof CurdForm>>>
8
8
  submit: () => void
9
- schema: Ref<Schema | undefined>
9
+ schema: Ref<CurdGroupFormSchema | undefined>
10
10
  validate: (callback: (valid: boolean) => void) => void
11
11
  loading: Ref<boolean>
12
- schemaItemHidden: (item: SchemaItem) => boolean
12
+ schemaItemHidden: (item: CurdGroupFormSchemaItem) => boolean
13
13
  } => {
14
14
  const formRef = ref<Record<string, InstanceType<typeof CurdForm>>>({})
15
15
  const form = ref<Record<string, unknown>>({})
16
- const schema = ref<Schema>()
16
+ const schema = ref<CurdGroupFormSchema>()
17
17
  const loading = ref(false)
18
- const schemaItemHidden = (item: SchemaItem) => {
18
+ const schemaItemHidden = (item: CurdGroupFormSchemaItem) => {
19
19
  if (item?.hidden && item?.hidden instanceof Function) {
20
20
  return item.hidden(form.value, item)
21
21
  }
@@ -21,7 +21,7 @@
21
21
  <el-table-column v-else-if="item.type == 'selection'" type="selection" v-bind="item" />
22
22
  <STableItem v-else :ref="getTableItemRef(item.prop)" :schema="item" :is-slot="!!$slots[item.prop]" :is-slot-header="!!$slots[item.prop + 'Header']" :option="option" @search="onItemChange">
23
23
  <template v-for="(_slot, key) in $slots" :key="key" #[key]="scopeChild">
24
- <slot :name="key" v-bind="scopeChild"></slot>
24
+ <slot :name="key" v-bind="normalizeSlotScope(scopeChild)"></slot>
25
25
  </template>
26
26
  </STableItem>
27
27
  </template>
@@ -133,12 +133,12 @@ const props = defineProps({
133
133
  },
134
134
  // 自定义创建函数
135
135
  fetchCreate: {
136
- type: Function as PropType<(formModel: Fields) => Promise<void>>,
136
+ type: Function as PropType<(formModel: Fields) => void | boolean | Promise<void | boolean>>,
137
137
  default: null,
138
138
  },
139
139
  // 自定义编辑函数
140
140
  fetchEdit: {
141
- type: Function as PropType<(row: unknown, formItem: unknown, index: number) => Promise<void>>,
141
+ type: Function as PropType<(formModel: Fields, row: Record<string, unknown>, index?: number) => void | boolean | Promise<void | boolean>>,
142
142
 
143
143
  default: null,
144
144
  },
@@ -248,6 +248,7 @@ const fetchData = async (opt?: FetchDataOpt) => {
248
248
  }
249
249
  }
250
250
  const tableItemRefs: Ref<Record<string, InstanceType<typeof STableItem>>> = ref({})
251
+ const normalizeSlotScope = (scope: unknown) => (scope && typeof scope === "object" ? scope : {})
251
252
  const getTableItemRef = (prop: string) => {
252
253
  return (el: unknown) => {
253
254
  if (el) {
@@ -404,8 +405,10 @@ const create = (row?: Record<string, unknown>, index?: number) => {
404
405
  fields: row as Fields,
405
406
  width: formAttrs?.width || 600,
406
407
  handleOk: async (modelRef: Fields) => {
407
- const fun = row ? "fetchEdit" : "fetchCreate"
408
- return await (props[fun] && props[fun](modelRef, row, index))
408
+ if (row) {
409
+ return await props.fetchEdit?.(modelRef, row, index)
410
+ }
411
+ return await props.fetchCreate?.(modelRef)
409
412
  },
410
413
  ...eoptions,
411
414
  })
@@ -14,7 +14,7 @@
14
14
  <template v-for="item in props.schema.children" :key="item.label">
15
15
  <STableItem :ref="(el) => (tableItemRefs[item.prop] = el)" :schema="item" :is-slot="!!$slots[item.prop]" :is-slot-header="!!$slots[item.prop + 'Header']" :option="option" @search="onItemChange">
16
16
  <template v-for="(_slot, key) in $slots" :key="key" #[key]="scopeChild">
17
- <slot :name="key" v-bind="scopeChild"></slot>
17
+ <slot :name="key" v-bind="normalizeSlotScope(scopeChild)"></slot>
18
18
  </template>
19
19
  </STableItem>
20
20
  </template>
@@ -44,6 +44,7 @@ import { useLocale } from "easybill-ui"
44
44
  import { ElMessage } from "element-plus"
45
45
  import { computed, inject, type PropType, ref, type Ref } from "vue"
46
46
  import ConstantStatus from "../../ConstantStatus"
47
+ import type { Fields } from "../../CurdForm"
47
48
  import type { ParamsItem } from "../../TableFilter"
48
49
  import { copy } from "../utils/common"
49
50
  import STableItemFilter from "./STableItemFilter.vue"
@@ -72,6 +73,7 @@ const props = defineProps({
72
73
  default: () => ({}),
73
74
  },
74
75
  })
76
+ defineSlots<Record<string, (scope: Record<string, unknown>) => unknown>>()
75
77
  const selectParams = inject<Ref<Array<ParamsItem>>>("selectParams")
76
78
  const filterSchema = computed(() => {
77
79
  let result = selectParams?.value.filter((a) => a.prop == props.schema.filter?.prop || a.prop == props.schema.prop) //getFilterFromColumn(props.schema)
@@ -94,7 +96,7 @@ const copyValue = async (value: unknown) => {
94
96
  ElMessage.success(t("el.table.clickCopySuccess"))
95
97
  }
96
98
  const emits = defineEmits(["search"])
97
- const search = inject("search")
99
+ const search = inject<Fields>("search", {})
98
100
  const getColumnHidden = computed(() => {
99
101
  if (props.schema.hidden && props.schema.hidden instanceof Function) {
100
102
  return props.schema.hidden(props.schema, search)
@@ -109,6 +111,7 @@ const onSearch = (opt: { listQuery: Record<string, unknown> }) => {
109
111
  if (tableItemFilterRef.value) tableItemFilterRef.value.search(opt)
110
112
  }
111
113
  const tableItemRefs: Ref<Record<string, unknown>> = ref({})
114
+ const normalizeSlotScope = (scope: unknown) => (scope && typeof scope === "object" ? scope : {})
112
115
  const onItemChange = (prop: string, value: string) => {
113
116
  emits("search", prop, value, filterSchema.value)
114
117
  }
@@ -8,6 +8,8 @@ import type { EventObject, FormSchema } from "./../../CurdForm/src/types"
8
8
  import type { VNode } from "vue"
9
9
  import type { FormDialogOptions } from "../../FormDialog/src/types"
10
10
 
11
+ export type MaybePromise<T> = T | Promise<T>
12
+
11
13
  export interface CurdTableProps<T = unknown> extends Partial<TableProps<T>> {
12
14
  rowKey?: string
13
15
  data?: T[]
@@ -15,22 +17,22 @@ export interface CurdTableProps<T = unknown> extends Partial<TableProps<T>> {
15
17
  pageOptions?: { pageIndex?: number; pageSize?: number; total?: number } & Fields
16
18
  option?: Partial<PropOption>
17
19
  fetchData?: (opt: FeachDataParam) => Promise<{ total?: number; list?: T[] } | void>
18
- fetchCreate?: (formModel: Fields) => Promise<void>
19
- fetchEdit?: (row: T) => Promise<void>
20
- fetchRemove?: (row: T) => Promise<void>
20
+ fetchCreate?: (formModel: Fields) => MaybePromise<void | boolean>
21
+ fetchEdit?: (formModel: Fields, row: T, index?: number) => MaybePromise<void | boolean>
22
+ fetchRemove?: (row: T, index: number) => Promise<void>
21
23
  }
22
24
  // extends Partial<TableColumnCtx<T>>
23
25
  export interface ColumnItemCtx<T> extends Partial<TableColumnCtx<T>> {
24
26
  prop: string
25
27
  label: string
26
28
  type?: string
27
- hidden?: boolean | ((item: ColumnItemCtx<T>, search: (opt: { listQuery: Record<string, unknown> }) => void) => boolean)
29
+ hidden?: boolean | ((item: ColumnItemCtx<T>, query: Fields) => boolean)
28
30
  neverShow?: boolean // 表示不在列控制器里
29
31
  children?: ColumnItem<T>[]
30
32
  options?: Array<OptionItem> //数据字典
31
33
  asyncOptions?: () => Promise<OptionItem[]>
32
34
  eventObject?: EventObject
33
- form?: Partial<FormItemType> | ((formItem: FormItemType, row: T, query: Fields) => Partial<FormItemType>)
35
+ form?: Partial<FormItemType> | ((column: ColumnItemCtx<T>, row: T, query: Fields) => Partial<FormItemType>)
34
36
  filter?: ColumnItemFilter
35
37
  empty?: string | unknown
36
38
  detail?: ColumnItemDetail
@@ -14,8 +14,8 @@ export interface DetailDataItem extends Record<string, unknown> {
14
14
  labelWidth?: number | string
15
15
  props?: Record<string, string>
16
16
  slot?: string
17
- tooltip?: string | import("element-plus/es/components/tooltip").ElTooltipProps
18
- labelPosition?: "string" | "left" | "right" | "center"
17
+ tooltip?: string | ElTooltipProps
18
+ labelPosition?: string | "left" | "right" | "center" | "top"
19
19
  labelStyle?: string | Record<string, string | number>
20
20
  showOverflowTooltip?: boolean | string | Partial<ElTooltipProps>
21
21
  rawContent?: boolean
@@ -28,10 +28,11 @@
28
28
 
29
29
  <script lang="ts">
30
30
  import { CurdForm, CurdGroupForm, type CurdGroupFormSchema, useLocale } from "easybill-ui"
31
- import { computed, defineComponent, type PropType, provide, reactive, ref, type Ref, toRefs, useAttrs } from "vue"
31
+ import { type Component, computed, defineComponent, type PropType, provide, reactive, ref, type Ref, toRefs, useAttrs } from "vue"
32
32
  import { type Fields, type FormSchema } from "../../CurdForm"
33
33
  import { useStepList } from "./hooks/useStepList"
34
34
  import { useStepNavigation } from "./hooks/useStepNavigation"
35
+ import type { FormDialogHandleOk } from "./types"
35
36
  export default defineComponent({
36
37
  name: "FormDialog",
37
38
  components: {
@@ -69,7 +70,7 @@ export default defineComponent({
69
70
  },
70
71
  // 点击确定
71
72
  handleOk: {
72
- type: Function,
73
+ type: Function as PropType<FormDialogHandleOk>,
73
74
  default: null,
74
75
  },
75
76
  // 关闭的回调,通过类型判断是点取消按钮还是右上角关闭按钮
@@ -99,11 +100,11 @@ export default defineComponent({
99
100
  default: "dialog",
100
101
  },
101
102
  stepComponent: {
102
- type: Object,
103
+ type: Object as PropType<Component>,
103
104
  default: null,
104
105
  },
105
106
  footerComponent: {
106
- type: Object,
107
+ type: Object as PropType<Component>,
107
108
  default: null,
108
109
  },
109
110
  },
@@ -151,7 +152,7 @@ export default defineComponent({
151
152
  state.confirmLoading = false
152
153
  return
153
154
  }
154
- const pass = await (props.handleOk && props.handleOk(form.value, state)).finally(() => (state.confirmLoading = false))
155
+ const pass = await Promise.resolve(props.handleOk?.(form.value, state)).finally(() => (state.confirmLoading = false))
155
156
  if (typeof pass == "undefined" || pass) {
156
157
  state.visible = false
157
158
  onCancel()
@@ -1,32 +1,37 @@
1
1
  import type { DialogProps, StepProps } from "element-plus"
2
+ import type { Component, Ref } from "vue"
2
3
  import type { Fields, FormContext, FormSchema } from "../../CurdForm"
3
4
  import type { CurdGroupFormSchema } from "../../CurdGroupForm/types"
4
5
  import type { StepItem } from "./hooks/useStepList"
5
6
 
7
+ export interface FormDialogState {
8
+ visible: boolean
9
+ confirmLoading: boolean
10
+ }
11
+
12
+ export type FormDialogHandleOk = (modelRef: Fields, state: FormDialogState) => void | boolean | Promise<void | boolean>
13
+
6
14
  export interface FormDialogOptions extends Partial<DialogProps> {
7
15
  title?: string
8
16
  width?: string | number
9
17
  fields?: Fields
10
18
  formSchema?: FormSchema
11
19
  groupSchema?: CurdGroupFormSchema
12
- handleOk?: (modelRef: Fields) => Promise<void>
20
+ handleOk?: FormDialogHandleOk
13
21
  handleClose?: (e: "close" | "cancel") => void
14
- setForm?: (form: Fields) => void
22
+ setForm?: (form: Ref<Fields>) => void
15
23
  stepProps?: Partial<StepProps & Fields>
16
24
  extendContext?: Partial<FormContext>
17
25
  confirmBtnText?: string
18
26
  cancelBtnText?: string
19
- stepComponent?: object
20
- footerComponent?: object
27
+ stepComponent?: Component
28
+ footerComponent?: Component
21
29
  mode?: "dialog" | "drawer"
22
30
  }
23
31
 
24
32
  export interface FormDialogContext {
25
33
  form: Fields
26
- state: {
27
- visible: boolean
28
- confirmLoading: boolean
29
- }
34
+ state: FormDialogState
30
35
  stepList: StepItem[]
31
36
  activeStep: { value: number }
32
37
  loadOptions: (prop: string, config?: unknown) => void
@@ -1,5 +1,5 @@
1
1
  import { defineComponent } from "vue"
2
- import type { FormContext, FormItem, FormItemTypeEmun } from "../CurdForm"
2
+ import type { CurdFormOptionItem, FormContext, FormItem, FormItemTypeEmun } from "../CurdForm"
3
3
  export interface ParamsItem extends FormItem {
4
4
  type?: FormItemTypeEmun | ReturnType<typeof defineComponent>
5
5
  tableKey?: Array<string>
@@ -9,10 +9,16 @@ export interface ParamsItem extends FormItem {
9
9
  tagNames?: string
10
10
  }
11
11
  export type FilterItem = ParamsItem
12
- export interface TableFilterContext extends FormContext {
12
+ export interface TableFilterContext {
13
13
  loadOptions: (prop: string, config?: unknown) => void
14
+ setOptions: (prop: string, options: CurdFormOptionItem[], config?: unknown) => void
14
15
  setValue: (prop: string, value: unknown) => void
15
16
  search: () => void
17
+ formModel: ListQuery
18
+ components: FormContext["components"]
19
+ change: FormContext["change"]
20
+ formRef?: FormContext["formRef"]
21
+ formSchema?: FormContext["formSchema"]
16
22
  }
17
23
 
18
24
  export interface ListQuery {
package/package.json CHANGED
@@ -1,11 +1,66 @@
1
1
  {
2
2
  "name": "easybill-ui",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "A component library for easybill",
5
5
  "author": "tuchongyang <779311998@qq.com>",
6
6
  "private": false,
7
7
  "license": "ISC",
8
+ "scripts": {
9
+ "typecheck": "vue-tsc -p tsconfig.typecheck.json --noEmit --skipLibCheck"
10
+ },
8
11
  "main": "index.ts",
12
+ "types": "index.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./index.ts",
16
+ "import": "./index.ts",
17
+ "default": "./index.ts"
18
+ },
19
+ "./index": {
20
+ "types": "./index.ts",
21
+ "import": "./index.ts",
22
+ "default": "./index.ts"
23
+ },
24
+ "./index.ts": {
25
+ "types": "./index.ts",
26
+ "import": "./index.ts",
27
+ "default": "./index.ts"
28
+ },
29
+ "./components/CurdForm/src/types": {
30
+ "types": "./components/CurdForm/src/types.ts",
31
+ "import": "./components/CurdForm/src/types.ts",
32
+ "default": "./components/CurdForm/src/types.ts"
33
+ },
34
+ "./components/*": {
35
+ "types": "./components/*",
36
+ "import": "./components/*",
37
+ "default": "./components/*"
38
+ },
39
+ "./utils/*": {
40
+ "types": "./utils/*",
41
+ "import": "./utils/*",
42
+ "default": "./utils/*"
43
+ },
44
+ "./locale/*": {
45
+ "types": "./locale/*",
46
+ "import": "./locale/*",
47
+ "default": "./locale/*"
48
+ },
49
+ "./theme-chalk/*": {
50
+ "types": "./theme-chalk/*",
51
+ "import": "./theme-chalk/*",
52
+ "default": "./theme-chalk/*"
53
+ },
54
+ "./package.json": "./package.json"
55
+ },
56
+ "files": [
57
+ "index.ts",
58
+ "components",
59
+ "utils",
60
+ "locale",
61
+ "theme-chalk",
62
+ "README.md"
63
+ ],
9
64
  "peerDependencies": {
10
65
  "@element-plus/icons-vue": "^2.0.6",
11
66
  "element-plus": "^2.7.0",
@@ -1,23 +1,25 @@
1
- import { computed, isRef, ref, unref, type ComputedRef } from "vue"
1
+ import { computed, isRef, ref, type ComputedRef, type Ref } from "vue"
2
2
  import en from "../../locale/lang/en"
3
3
 
4
- import type { MaybeRef } from "@vueuse/core"
5
4
  import { useGlobalConfig } from "easybill-ui"
6
- import type { Ref } from "vue"
7
5
  import type { Language } from "../../locale"
8
6
 
9
7
  export type TranslatorOption = Record<string, string | number> | string[]
10
8
  export type Translator = (path: string, option?: TranslatorOption) => string
9
+ export type LocaleSource = Language | Ref<Language> | ComputedRef<Language>
10
+ export type LocaleRef = Ref<Language> | ComputedRef<Language>
11
11
  export type LocaleContext = {
12
- locale: Ref<Language>
13
- lang: Ref<string>
12
+ locale: LocaleRef
13
+ lang: ComputedRef<string>
14
14
  t: Translator
15
15
  }
16
16
 
17
+ const resolveLocale = (locale: LocaleSource): Language => (isRef(locale) ? locale.value : locale)
18
+
17
19
  export const buildTranslator =
18
- (locale: MaybeRef<Language>): Translator =>
20
+ (locale: LocaleSource): Translator =>
19
21
  (path, option) =>
20
- translate(path, option, unref(locale))
22
+ translate(path, option, resolveLocale(locale))
21
23
 
22
24
  export const translate = (path: string, option: undefined | TranslatorOption, locale: Language): string => {
23
25
  const value = get(locale, path, path) as string
@@ -27,9 +29,9 @@ export const translate = (path: string, option: undefined | TranslatorOption, lo
27
29
  return value.replace(/\{(\w+)\}/g, (_, key) => `${option?.[key] ?? `{${key}}`}`)
28
30
  }
29
31
 
30
- export const buildLocaleContext = (locale: ComputedRef<Language>): LocaleContext => {
31
- const lang = computed(() => unref(locale).name)
32
- const localeRef = isRef(locale) ? locale : ref(locale)
32
+ export const buildLocaleContext = (locale: LocaleSource): LocaleContext => {
33
+ const localeRef = (isRef(locale) ? locale : ref(locale)) as LocaleRef
34
+ const lang = computed(() => localeRef.value.name)
33
35
  return {
34
36
  lang,
35
37
  locale: localeRef,