@veltra/desktop 1.1.36 → 1.2.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.
Files changed (191) hide show
  1. package/dist/components/action/action-group.vue.d.ts +1 -1
  2. package/dist/components/action/action.vue.d.ts +1 -1
  3. package/dist/components/auto-complete/auto-complete.js +1 -0
  4. package/dist/components/auto-complete/auto-complete.js.map +1 -1
  5. package/dist/components/batch-edit/batch-edit-form.js +12 -5
  6. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  7. package/dist/components/batch-edit/batch-edit.js +8 -3
  8. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  9. package/dist/components/batch-edit/batch-edit.vue.d.ts +40 -35
  10. package/dist/components/batch-edit/di.js.map +1 -1
  11. package/dist/components/batch-edit/use-edit.js +41 -23
  12. package/dist/components/batch-edit/use-edit.js.map +1 -1
  13. package/dist/components/cascade/cascade.js +1 -0
  14. package/dist/components/cascade/cascade.js.map +1 -1
  15. package/dist/components/cascade/cascade.vue.d.ts +4 -4
  16. package/dist/components/checkbox/checkbox-button.js +1 -0
  17. package/dist/components/checkbox/checkbox-button.js.map +1 -1
  18. package/dist/components/checkbox/checkbox-button.vue.d.ts +1 -1
  19. package/dist/components/checkbox/checkbox.js +1 -0
  20. package/dist/components/checkbox/checkbox.js.map +1 -1
  21. package/dist/components/checkbox-group/checkbox-group.js +1 -0
  22. package/dist/components/checkbox-group/checkbox-group.js.map +1 -1
  23. package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
  24. package/dist/components/code-editor/code-editor.js +1 -0
  25. package/dist/components/code-editor/code-editor.js.map +1 -1
  26. package/dist/components/condition-editor/core/evaluator.js +3 -3
  27. package/dist/components/condition-editor/core/evaluator.js.map +1 -1
  28. package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
  29. package/dist/components/date-picker/date-picker.js +1 -0
  30. package/dist/components/date-picker/date-picker.js.map +1 -1
  31. package/dist/components/date-picker/date-picker.vue.d.ts +1 -1
  32. package/dist/components/date-range-picker/date-range-picker.js +2 -0
  33. package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
  34. package/dist/components/date-range-picker/date-range-picker.vue.d.ts +1 -1
  35. package/dist/components/empty/empty.vue.d.ts +1 -1
  36. package/dist/components/expression-editor/expression-editor.js +1 -0
  37. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  38. package/dist/components/file-picker/file-picker.js +1 -0
  39. package/dist/components/file-picker/file-picker.js.map +1 -1
  40. package/dist/components/form/form.js.map +1 -1
  41. package/dist/components/form/form.vue.d.ts +11 -19
  42. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +24 -26
  43. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
  44. package/dist/components/form/index.d.ts +0 -3
  45. package/dist/components/form/use-form-fields.js +88 -0
  46. package/dist/components/form/use-form-fields.js.map +1 -0
  47. package/dist/components/form/use-node-interceptor.js +19 -30
  48. package/dist/components/form/use-node-interceptor.js.map +1 -1
  49. package/dist/components/form-item/form-item.js.map +1 -1
  50. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js +44 -18
  51. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
  52. package/dist/components/form-item/helper.js +4 -1
  53. package/dist/components/form-item/helper.js.map +1 -1
  54. package/dist/components/form-item/validate.d.ts +26 -0
  55. package/dist/components/form-item/validate.js +97 -0
  56. package/dist/components/form-item/validate.js.map +1 -0
  57. package/dist/components/group-input/group-input.js +1 -0
  58. package/dist/components/group-input/group-input.js.map +1 -1
  59. package/dist/components/index.d.ts +0 -3
  60. package/dist/components/index.js +0 -7
  61. package/dist/components/input/input.vue.d.ts +4 -4
  62. package/dist/components/input/input.vue_vue_type_script_setup_true_lang.js +1 -0
  63. package/dist/components/input/input.vue_vue_type_script_setup_true_lang.js.map +1 -1
  64. package/dist/components/multi-select/multi-select.js +1 -0
  65. package/dist/components/multi-select/multi-select.js.map +1 -1
  66. package/dist/components/multi-select/multi-select.vue.d.ts +3 -3
  67. package/dist/components/multi-tree-select/multi-tree-select.js +1 -0
  68. package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
  69. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +4 -4
  70. package/dist/components/number-input/number-input.js +1 -0
  71. package/dist/components/number-input/number-input.js.map +1 -1
  72. package/dist/components/number-range-input/number-range-input.js +1 -0
  73. package/dist/components/number-range-input/number-range-input.js.map +1 -1
  74. package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
  75. package/dist/components/palette/palette.js +1 -0
  76. package/dist/components/palette/palette.js.map +1 -1
  77. package/dist/components/password-input/password-input.js +1 -0
  78. package/dist/components/password-input/password-input.js.map +1 -1
  79. package/dist/components/radio/radio.js +1 -0
  80. package/dist/components/radio/radio.js.map +1 -1
  81. package/dist/components/radio-group/radio-group.js +1 -0
  82. package/dist/components/radio-group/radio-group.js.map +1 -1
  83. package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
  84. package/dist/components/rich-text-editor/rich-text-editor.js +3 -2
  85. package/dist/components/rich-text-editor/rich-text-editor.js.map +1 -1
  86. package/dist/components/rich-text-editor/toolbar.js +10 -7
  87. package/dist/components/rich-text-editor/toolbar.js.map +1 -1
  88. package/dist/components/select/select.js +6 -3
  89. package/dist/components/select/select.js.map +1 -1
  90. package/dist/components/select/select.vue.d.ts +2 -2
  91. package/dist/components/slider/slider.js +15 -2
  92. package/dist/components/slider/slider.js.map +1 -1
  93. package/dist/components/switch/switch.js +1 -0
  94. package/dist/components/switch/switch.js.map +1 -1
  95. package/dist/components/tabs/tabs-horizontal.vue.d.ts +2 -2
  96. package/dist/components/tabs/tabs-vertical.vue.d.ts +2 -2
  97. package/dist/components/tabs/tabs.vue.d.ts +2 -2
  98. package/dist/components/textarea/textarea.js +1 -0
  99. package/dist/components/textarea/textarea.js.map +1 -1
  100. package/dist/components/tip/tip.vue.d.ts +3 -3
  101. package/dist/components/tree-select/tree-select.js +1 -0
  102. package/dist/components/tree-select/tree-select.js.map +1 -1
  103. package/dist/components/tree-select/tree-select.vue.d.ts +4 -4
  104. package/dist/index.d.ts +5 -6
  105. package/dist/index.js +1 -4
  106. package/dist/types/auto-complete.d.ts +3 -2
  107. package/dist/types/batch-edit.d.ts +4 -5
  108. package/dist/types/cascade.d.ts +3 -2
  109. package/dist/types/checkbox-group.d.ts +2 -2
  110. package/dist/types/checkbox.d.ts +4 -3
  111. package/dist/types/code-editor.d.ts +3 -2
  112. package/dist/types/date-picker.d.ts +3 -2
  113. package/dist/types/date-range-picker.d.ts +3 -2
  114. package/dist/types/expression-editor.d.ts +3 -2
  115. package/dist/types/file-picker.d.ts +3 -2
  116. package/dist/types/form-field.d.ts +12 -0
  117. package/dist/types/form-item.d.ts +2 -2
  118. package/dist/types/form.d.ts +9 -48
  119. package/dist/types/group-input.d.ts +3 -2
  120. package/dist/types/index.d.ts +4 -2
  121. package/dist/types/input.d.ts +3 -2
  122. package/dist/types/multi-select.d.ts +3 -2
  123. package/dist/types/multi-tree-select.d.ts +3 -2
  124. package/dist/types/palette.d.ts +3 -2
  125. package/dist/types/radio-group.d.ts +3 -2
  126. package/dist/types/radio.d.ts +3 -2
  127. package/dist/types/rich-text-editor.d.ts +3 -2
  128. package/dist/types/select.d.ts +3 -2
  129. package/dist/types/slider.d.ts +3 -2
  130. package/dist/types/switch.d.ts +3 -2
  131. package/dist/types/text-editor.d.ts +3 -2
  132. package/dist/types/textarea.d.ts +3 -2
  133. package/dist/types/tree-select.d.ts +3 -2
  134. package/package.json +6 -6
  135. package/src/components/batch-edit/batch-edit-form.vue +20 -13
  136. package/src/components/batch-edit/batch-edit.vue +7 -8
  137. package/src/components/batch-edit/di.ts +1 -2
  138. package/src/components/batch-edit/use-edit.ts +58 -36
  139. package/src/components/date-range-picker/date-range-picker.vue +2 -0
  140. package/src/components/form/form.vue +22 -38
  141. package/src/components/form/index.ts +0 -6
  142. package/src/components/form/use-form-fields.ts +126 -0
  143. package/src/components/form/use-node-interceptor.ts +27 -56
  144. package/src/components/form-item/form-item.vue +69 -18
  145. package/src/components/form-item/helper.ts +10 -4
  146. package/src/components/form-item/validate.ts +155 -0
  147. package/src/components/rich-text-editor/rich-text-editor.vue +28 -25
  148. package/src/components/rich-text-editor/toolbar.vue +23 -19
  149. package/src/components/select/select.vue +6 -4
  150. package/src/components/slider/slider.vue +12 -2
  151. package/src/types/auto-complete.ts +4 -2
  152. package/src/types/batch-edit.ts +4 -5
  153. package/src/types/cascade.ts +4 -2
  154. package/src/types/checkbox-group.ts +2 -2
  155. package/src/types/checkbox.ts +5 -3
  156. package/src/types/code-editor.ts +4 -2
  157. package/src/types/date-picker.ts +4 -2
  158. package/src/types/date-range-picker.ts +4 -2
  159. package/src/types/expression-editor.ts +4 -2
  160. package/src/types/file-picker.ts +4 -2
  161. package/src/types/form-field.ts +11 -0
  162. package/src/types/form-item.ts +2 -2
  163. package/src/types/form.ts +9 -82
  164. package/src/types/group-input.ts +4 -2
  165. package/src/types/index.ts +1 -0
  166. package/src/types/input.ts +4 -2
  167. package/src/types/multi-select.ts +4 -2
  168. package/src/types/multi-tree-select.ts +5 -2
  169. package/src/types/palette.ts +4 -2
  170. package/src/types/radio-group.ts +4 -2
  171. package/src/types/radio.ts +4 -2
  172. package/src/types/rich-text-editor.ts +4 -2
  173. package/src/types/select.ts +4 -2
  174. package/src/types/slider.ts +4 -2
  175. package/src/types/switch.ts +4 -2
  176. package/src/types/text-editor.ts +4 -2
  177. package/src/types/textarea.ts +4 -2
  178. package/src/types/tree-select.ts +5 -2
  179. package/dist/components/form/dynamic-form-model.d.ts +0 -73
  180. package/dist/components/form/dynamic-form-model.js +0 -184
  181. package/dist/components/form/dynamic-form-model.js.map +0 -1
  182. package/dist/components/form/form-model.d.ts +0 -73
  183. package/dist/components/form/form-model.js +0 -179
  184. package/dist/components/form/form-model.js.map +0 -1
  185. package/dist/components/form/helper.d.ts +0 -16
  186. package/dist/components/form/helper.js +0 -20
  187. package/dist/components/form/helper.js.map +0 -1
  188. package/src/components/form/__test__/form-model.test.ts +0 -190
  189. package/src/components/form/dynamic-form-model.ts +0 -258
  190. package/src/components/form/form-model.ts +0 -255
  191. package/src/components/form/helper.ts +0 -23
@@ -2,6 +2,7 @@ import { last, o, safeRun } from '@cat-kit/core'
2
2
  import { computed, nextTick, shallowReactive, shallowRef, watch, type ShallowRef } from 'vue'
3
3
 
4
4
  import type { TableRow, BatchEditEmits, BatchEditProps, TableExposed } from '../../types'
5
+ import type { FormExposed } from '../../types/form'
5
6
 
6
7
  /** 按索引路径在原始数据树中定位节点(等价于历史 Forest.visit 路径访问) */
7
8
  function visitDataByIndexPath<T extends Record<string, unknown>>(
@@ -29,6 +30,7 @@ interface Options {
29
30
  props: BatchEditProps
30
31
  emit: BatchEditEmits
31
32
  tableRef: ShallowRef<TableExposed | undefined>
33
+ formRef: ShallowRef<FormExposed | undefined>
32
34
  }
33
35
 
34
36
  export interface BatchEditStates {
@@ -58,8 +60,19 @@ export interface EditReturned {
58
60
  handleInsertChild: (row: TableRow) => void
59
61
  }
60
62
 
63
+ function snapshotData(data?: Record<string, any>) {
64
+ return safeRun(() => JSON.parse(JSON.stringify(data ?? {})), {})
65
+ }
66
+
67
+ function replaceModelData(model: Record<string, any>, data: Record<string, any>) {
68
+ for (const key of Object.keys(model)) {
69
+ delete model[key]
70
+ }
71
+ Object.assign(model, data)
72
+ }
73
+
61
74
  export function useEdit(options: Options): EditReturned {
62
- const { props, emit, tableRef } = options
75
+ const { props, emit, tableRef, formRef } = options
63
76
 
64
77
  /** 组件状态 */
65
78
  const state = shallowReactive<BatchEditStates>({
@@ -88,19 +101,39 @@ export function useEdit(options: Options): EditReturned {
88
101
  * 阶段把规范化后的值再写回模型时被误判为「用户已修改」
89
102
  */
90
103
  let baselineSnapshot: Record<string, any> | undefined
104
+ let emptySnapshot: Record<string, any> | undefined
91
105
 
92
106
  function snapshotModelData() {
93
- return safeRun(() => JSON.parse(JSON.stringify(props.model?.data ?? {})), {})
107
+ return snapshotData(props.model)
108
+ }
109
+
110
+ function resetFormData(data?: Record<string, any>) {
111
+ if (!props.model) return
112
+ replaceModelData(props.model, snapshotData(data ?? emptySnapshot))
113
+ formRef.value?.clearValidate()
114
+ }
115
+
116
+ function setFormData(formData: Record<string, any>) {
117
+ if (!props.model) return
118
+ replaceModelData(props.model, snapshotData(formData))
94
119
  }
95
120
 
121
+ watch(
122
+ () => props.model,
123
+ (model) => {
124
+ emptySnapshot = snapshotData(model)
125
+ },
126
+ { immediate: true }
127
+ )
128
+
96
129
  watch(
97
130
  () => state.row,
98
131
  (row) => {
99
- props.model?.resetData()
132
+ resetFormData()
100
133
  if (row) {
101
134
  state.type = 'update'
102
135
  state.visible = true
103
- props.model?.setData(row.data)
136
+ setFormData(row.data)
104
137
  } else {
105
138
  state.visible = false
106
139
  }
@@ -111,16 +144,14 @@ export function useEdit(options: Options): EditReturned {
111
144
  { flush: 'sync' }
112
145
  )
113
146
 
114
- const changeCb = (field: string, val: any) => {
115
- // 与基线比较 — 同值(如组件 mount 时把当前值再写一次)不算"用户修改"
116
- if (baselineSnapshot) {
117
- const baseVal = o(baselineSnapshot).get(field)
118
- if (isSameValue(baseVal, val)) {
119
- return
120
- }
121
- }
122
- state.dataUpdated = true
123
- }
147
+ watch(
148
+ () => props.model,
149
+ () => {
150
+ if (!baselineSnapshot || !state.visible) return
151
+ state.dataUpdated = !isSameValue(baselineSnapshot, snapshotModelData())
152
+ },
153
+ { deep: true }
154
+ )
124
155
 
125
156
  function isSameValue(a: any, b: any) {
126
157
  if (a === b) return true
@@ -136,19 +167,6 @@ export function useEdit(options: Options): EditReturned {
136
167
  return false
137
168
  }
138
169
 
139
- watch(
140
- () => props.model,
141
- (model, oldModel) => {
142
- if (oldModel) {
143
- oldModel.offChange(changeCb)
144
- }
145
- if (model) {
146
- model.onChange(changeCb)
147
- }
148
- },
149
- { immediate: true }
150
- )
151
-
152
170
  const childrenKey = computed(() => {
153
171
  return typeof props.tree === 'string' ? props.tree : 'children'
154
172
  })
@@ -183,7 +201,7 @@ export function useEdit(options: Options): EditReturned {
183
201
  }
184
202
 
185
203
  function getInsertData(): Record<string, any> {
186
- return safeRun(() => JSON.parse(JSON.stringify(props.model?.data ?? {})), {})
204
+ return snapshotModelData()
187
205
  }
188
206
 
189
207
  async function runCreate(cb: () => void) {
@@ -193,7 +211,7 @@ export function useEdit(options: Options): EditReturned {
193
211
  }
194
212
  state.row = undefined
195
213
  state.type = 'create'
196
- props.model?.resetData()
214
+ resetFormData()
197
215
  cb()
198
216
 
199
217
  let item: Record<string, any> | undefined = undefined
@@ -201,6 +219,8 @@ export function useEdit(options: Options): EditReturned {
201
219
  await nextTick()
202
220
 
203
221
  state.visible = true
222
+ baselineSnapshot = snapshotModelData()
223
+ state.dataUpdated = false
204
224
 
205
225
  if (item) {
206
226
  const row = tableRef.value?.getRowByData(item)
@@ -256,8 +276,9 @@ export function useEdit(options: Options): EditReturned {
256
276
  runCreate(() => {
257
277
  state.depth = row.depth
258
278
  insertIndexes.value = [...row.indexes.slice(0, -1), row.index + 1]
259
- const cloned = safeRun(() => JSON.parse(JSON.stringify(row.data ?? {})), {})
260
- props.model?.setData(cloned)
279
+ setFormData(row.data)
280
+ baselineSnapshot = snapshotModelData()
281
+ state.dataUpdated = false
261
282
  })
262
283
  }
263
284
 
@@ -285,8 +306,8 @@ export function useEdit(options: Options): EditReturned {
285
306
 
286
307
  if (!model) return
287
308
 
288
- model.clearValidate()
289
- const valid = await model.validate()
309
+ formRef.value?.clearValidate()
310
+ const valid = await formRef.value?.validate()
290
311
 
291
312
  if (!valid) return
292
313
 
@@ -304,13 +325,14 @@ export function useEdit(options: Options): EditReturned {
304
325
  if (state.type === 'create') {
305
326
  insert(item)
306
327
 
307
- return model?.resetData()
328
+ resetFormData()
329
+ return
308
330
  }
309
331
  // 更新
310
332
  if (state.type === 'update') {
311
333
  const { row } = state
312
334
  row &&
313
- model.allKeys.forEach((key) => {
335
+ Object.keys(item).forEach((key) => {
314
336
  o(row.data).set(key, o(item).get(key))
315
337
  })
316
338
  }
@@ -354,7 +376,7 @@ export function useEdit(options: Options): EditReturned {
354
376
  state.row = undefined
355
377
  state.parentRow = undefined
356
378
  state.depth = undefined
357
- props.model?.resetData()
379
+ resetFormData()
358
380
  insertIndexes.value = []
359
381
  }
360
382
 
@@ -127,6 +127,8 @@ watch(
127
127
  if (isUserActive()) return
128
128
  if (val?.length === 2) {
129
129
  currentRangeDate.value = [date(val[0]), date(val[1])]
130
+ } else {
131
+ currentRangeDate.value = undefined
130
132
  }
131
133
  },
132
134
  { immediate: true }
@@ -10,26 +10,20 @@
10
10
  v-for="{ node, isFormItem, formItemProps, field, modelValue } of getSlotsNodes()"
11
11
  :key="node.key"
12
12
  >
13
- <component v-if="isFormItem || !field" :is="node" />
14
-
15
- <u-form-item v-else v-bind="formItemProps">
13
+ <u-form-item v-if="!isFormItem && field" v-bind="formItemProps">
16
14
  <component
17
15
  :is="node"
18
- :model-value="modelValue ?? o(model?.data ?? {}).get(field)"
16
+ :model-value="modelValue ?? o(model ?? {}).get(field)"
19
17
  @update:model-value="handleUpdateValue(field, $event)"
20
18
  />
21
-
22
- <div :class="cls.e('data-before')" v-if="showInitialNode(field)">
23
- <i :class="cls.e('changed-tag')">变更前:</i>
24
-
25
- <component :is="node" readonly :model-value="o(model?.initialData ?? {}).get(field)" />
26
- </div>
27
19
  </u-form-item>
20
+
21
+ <component v-else :is="node" />
28
22
  </template>
29
23
  </u-grid>
30
24
  </template>
31
25
 
32
- <script lang="tsx" setup generic="Model extends FormModel | DynamicFormModel">
26
+ <script lang="tsx" setup>
33
27
  import { o } from '@cat-kit/core'
34
28
  import { bem } from '@veltra/utils'
35
29
  import { provideFormContext } from '@veltra/utils'
@@ -38,48 +32,38 @@ import { shallowRef, toRef } from 'vue'
38
32
  import type { BreakCols, GridExposed, FormProps, _FormExposed } from '../../types'
39
33
  import { UFormItem } from '../form-item'
40
34
  import { UGrid } from '../grid'
41
- import type { DynamicFormModel } from './dynamic-form-model'
42
- import type { FormModel } from './form-model'
35
+ import { useFormFields } from './use-form-fields'
43
36
  import { useNodeInterceptor } from './use-node-interceptor'
44
37
 
45
38
  defineOptions({ name: 'Form' })
46
39
 
47
- const props = defineProps<FormProps<Model>>()
40
+ const props = defineProps<FormProps>()
48
41
 
49
- defineSlots<{
50
- default(props: {
51
- /** 表单数据 */
52
- data: Model['data']
53
- /** 表单模型 */
54
- model: Model
55
- }): any
56
- }>()
42
+ defineSlots<{ default(props?: { data: Record<string, any> | undefined }): any }>()
57
43
 
58
44
  const cls = bem('form')
59
45
 
60
46
  const breakpointCols: BreakCols = { xs: 1, md: 2, lg: 3, xl: 4, default: 4 }
61
47
 
62
- provideFormContext(props)
48
+ const { validate, clearValidate, reset, registerField, unregisterField, shouldValidate } =
49
+ useFormFields({ props })
63
50
 
64
- const { getSlotsNodes } = useNodeInterceptor({ props })
51
+ provideFormContext({
52
+ formProps: props,
53
+ registerField,
54
+ unregisterField,
55
+ validateFields: validate,
56
+ shouldValidate
57
+ })
65
58
 
66
- function handleUpdateValue(field: string, value: any) {
67
- const { model } = props
68
- if (!model) return
69
- o(model.data).set(field, value)
70
- }
71
-
72
- function showInitialNode(field: string) {
73
- const { data, initialData } = props.model || {}
59
+ const { getSlotsNodes } = useNodeInterceptor()
74
60
 
75
- const currentValue = o(data ?? {}).get(field)
76
- const initialValue = o(initialData ?? {}).get(field)
77
- const notEqual = !(initialValue === currentValue || (!initialValue && !currentValue))
78
-
79
- return props.showInitialData && notEqual
61
+ function handleUpdateValue(field: string, value: any) {
62
+ if (!props.model) return
63
+ o(props.model).set(field, value)
80
64
  }
81
65
 
82
66
  const gridRef = shallowRef<GridExposed>()
83
67
 
84
- defineExpose<_FormExposed>({ el: toRef(() => gridRef.value?.el) })
68
+ defineExpose<_FormExposed>({ el: toRef(() => gridRef.value?.el), validate, clearValidate, reset })
85
69
  </script>
@@ -1,7 +1 @@
1
1
  export { default as UForm } from './form.vue'
2
-
3
- export { formField, nestField } from './helper'
4
- export type { NestedFieldMarker } from './helper'
5
-
6
- export { FormModel } from './form-model'
7
- export { DynamicFormModel } from './dynamic-form-model'
@@ -0,0 +1,126 @@
1
+ import { o } from '@cat-kit/core'
2
+ import type { FormFieldItem } from '@veltra/utils'
3
+ import { nextTick, watch } from 'vue'
4
+
5
+ import type { FormProps } from '../../types/form'
6
+
7
+ interface Options {
8
+ props: FormProps
9
+ }
10
+
11
+ /** 深拷贝 model,保留 undefined 等 o().copy() 会丢弃的值 */
12
+ function snapshotModel(model: Record<string, any>): Record<string, any> {
13
+ const result: Record<string, any> = {}
14
+
15
+ for (const key of Object.keys(model)) {
16
+ const value = model[key]
17
+ if (Array.isArray(value)) {
18
+ result[key] = value.map((item) =>
19
+ item !== null && typeof item === 'object' ? snapshotModel(item) : item
20
+ )
21
+ } else if (value !== null && typeof value === 'object') {
22
+ result[key] = snapshotModel(value)
23
+ } else {
24
+ result[key] = value
25
+ }
26
+ }
27
+
28
+ return result
29
+ }
30
+
31
+ /** 按快照逐字段写回 model,支持 undefined 与嵌套对象 */
32
+ function applySnapshot(target: Record<string, any>, source: Record<string, any>) {
33
+ for (const key of Object.keys(source)) {
34
+ const sourceValue = source[key]
35
+ const targetValue = target[key]
36
+
37
+ if (
38
+ sourceValue !== null &&
39
+ typeof sourceValue === 'object' &&
40
+ !Array.isArray(sourceValue) &&
41
+ targetValue !== null &&
42
+ typeof targetValue === 'object' &&
43
+ !Array.isArray(targetValue)
44
+ ) {
45
+ applySnapshot(targetValue, sourceValue)
46
+ continue
47
+ }
48
+
49
+ if (Array.isArray(sourceValue)) {
50
+ o(target).set(
51
+ key,
52
+ sourceValue.map((item) =>
53
+ item !== null && typeof item === 'object' ? snapshotModel(item) : item
54
+ )
55
+ )
56
+ continue
57
+ }
58
+
59
+ o(target).set(key, sourceValue)
60
+ }
61
+ }
62
+
63
+ export function useFormFields(options: Options) {
64
+ const { props } = options
65
+ const fields: Record<string, FormFieldItem> = {}
66
+
67
+ let willValidate = true
68
+
69
+ /** model 初始快照,供 reset 恢复 */
70
+ let initialSnapshot: Record<string, any> | undefined
71
+
72
+ function runWithoutChangeValidate(fn: () => void) {
73
+ willValidate = false
74
+ fn()
75
+ nextTick(() => {
76
+ willValidate = true
77
+ })
78
+ }
79
+
80
+ function shouldValidate() {
81
+ return willValidate
82
+ }
83
+
84
+ watch(
85
+ () => props.model,
86
+ (model) => {
87
+ initialSnapshot = model ? snapshotModel(model) : undefined
88
+ },
89
+ { immediate: true, deep: false }
90
+ )
91
+
92
+ function registerField(field: string, item: FormFieldItem) {
93
+ fields[field] = item
94
+ }
95
+
96
+ function unregisterField(field: string) {
97
+ delete fields[field]
98
+ }
99
+
100
+ function clearValidate() {
101
+ Object.values(fields).forEach((item) => item.clearValidate?.())
102
+ }
103
+
104
+ async function validate(keys?: string[]) {
105
+ let validList: boolean[]
106
+ if (keys) {
107
+ validList = await Promise.all(
108
+ keys.map((key) => fields[key]?.validate() ?? Promise.resolve(true))
109
+ )
110
+ } else {
111
+ validList = await Promise.all(Object.values(fields).map((field) => field.validate()))
112
+ }
113
+ return validList.every((valid) => valid)
114
+ }
115
+
116
+ function reset() {
117
+ if (!props.model || !initialSnapshot) return
118
+
119
+ runWithoutChangeValidate(() => {
120
+ applySnapshot(props.model!, initialSnapshot!)
121
+ Object.values(fields).forEach((item) => item.clearValidate?.())
122
+ })
123
+ }
124
+
125
+ return { fields, registerField, validate, unregisterField, clearValidate, reset, shouldValidate }
126
+ }
@@ -1,87 +1,58 @@
1
1
  import { o } from '@cat-kit/core'
2
- import { createIncrease, extractNormalVNodes } from '@veltra/utils'
3
- import { onBeforeUnmount, useSlots, type VNode } from 'vue'
4
-
5
- import type { FormProps } from '../../types'
6
-
7
- interface Options {
8
- props: FormProps
2
+ import { extractNormalVNodes } from '@veltra/utils'
3
+ import { useSlots, type VNode } from 'vue'
4
+
5
+ export type SlotRenderItem = {
6
+ isFormItem: boolean
7
+ formItemProps?: Record<string, any>
8
+ node: VNode
9
+ field?: string
10
+ modelValue?: any
9
11
  }
10
12
 
11
- export type SlotRenderItem =
12
- | {
13
- isFormItem: true
14
- formItemProps: Record<string, any>
15
- node: VNode
16
- field: undefined
17
- modelValue?: any
18
- }
19
- | {
20
- isFormItem: false
21
- formItemProps: Record<string, any>
22
- node: VNode
23
- field: string
24
- modelValue?: any
25
- }
26
-
27
- const id = createIncrease(1)
28
-
29
13
  /**
30
14
  * 虚拟node拦截
31
- * @param options 选项
32
15
  * @returns
33
16
  */
34
- export function useNodeInterceptor(options: Options): {
35
- getSlotsNodes: () => SlotRenderItem[] | null
36
- } {
37
- const { props } = options
17
+ export function useNodeInterceptor(): { getSlotsNodes: () => SlotRenderItem[] | null } {
38
18
  const slots = useSlots()
39
19
 
40
- const formId = id()
41
-
42
20
  function getSlotsNodes() {
43
- const { model } = props
44
- const nodes = slots.default?.({ model, data: model?.data })
21
+ const nodes = slots.default?.()
45
22
  if (!nodes?.length) return null
46
23
 
47
24
  const flattedNodes = extractNormalVNodes(nodes)
48
25
 
49
26
  const results: SlotRenderItem[] = []
50
27
 
51
- // 渲染表单后得到的应当校验的字段
52
- const fields: string[] = []
53
-
54
28
  let i = 0
55
29
  while (i < flattedNodes.length) {
56
30
  const node = flattedNodes[i]!
57
31
  i++
58
32
 
59
33
  const { props, type } = node
60
- const item = {
61
- isFormItem: (type as any)?.name === 'FormItem',
62
- formItemProps: o((props ?? {}) as Record<string, any>).pick([
63
- 'label',
64
- 'field',
65
- 'span',
66
- 'tips',
67
- 'readonly'
68
- ]),
34
+ const field = props?.field as string | undefined
35
+ const isFormItem = (type as any)?.name === 'FormItem'
36
+ const formItemProps = o((props ?? {}) as Record<string, any>).pick([
37
+ 'label',
38
+ 'rules',
39
+ 'span',
40
+ 'tips',
41
+ 'readonly',
42
+ 'field'
43
+ ]) as Record<string, any>
44
+
45
+ results.push({
46
+ isFormItem,
47
+ formItemProps,
69
48
  node,
70
- field: props?.field,
49
+ field,
71
50
  modelValue: props?.['model-value'] ?? props?.modelValue
72
- }
73
-
74
- item.field && fields.push(item.field)
75
- results.push(item)
51
+ })
76
52
  }
77
53
 
78
- model.formKeys.set(formId, fields)
79
54
  return results
80
55
  }
81
56
 
82
- onBeforeUnmount(() => {
83
- props.model?.formKeys.delete(formId)
84
- })
85
-
86
57
  return { getSlotsNodes }
87
58
  }
@@ -2,7 +2,7 @@
2
2
  <u-grid-item :span="span" :class="className">
3
3
  <label
4
4
  v-if="props.label || $slots.label"
5
- :class="[cls.e('label'), bem.is('required', fieldRequired)]"
5
+ :class="[cls.e('label'), bem.is('required', !!rules?.required)]"
6
6
  :style="labelStyles"
7
7
  >
8
8
  <u-tip v-if="tips" :content="tips" :class="cls.e('tips')">
@@ -21,10 +21,10 @@
21
21
  </div>
22
22
 
23
23
  <!-- 只有表单控件处于非只读状态时,才显示错误提示 -->
24
- <section :class="cls.e('error')" v-if="!formProps?.readonly && !formProps?.noTips">
24
+ <section :class="cls.e('error')" v-if="!readonly && !formProps?.noTips">
25
25
  <transition name="form-item-tips" mode="out-in">
26
- <span :class="cls.e('error-text')" v-if="!!errorTips">
27
- {{ errorTips }}
26
+ <span :class="cls.e('error-text')" v-if="!!errorTip">
27
+ {{ errorTip }}
28
28
  </span>
29
29
  </transition>
30
30
  </section>
@@ -33,15 +33,17 @@
33
33
  </template>
34
34
 
35
35
  <script lang="tsx" setup>
36
+ import { o } from '@cat-kit/core'
36
37
  import { useConfig, useFallbackProps } from '@veltra/compositions'
37
38
  import { bem, withUnit } from '@veltra/utils'
38
39
  import { injectFormContext } from '@veltra/utils'
39
- import { type CSSProperties, computed } from 'vue'
40
+ import { type CSSProperties, computed, onBeforeUnmount, shallowRef, watch } from 'vue'
40
41
 
41
42
  import type { FormItemProps, ComponentSize } from '../../types'
42
43
  import { UGridItem } from '../grid'
43
44
  import { UTip } from '../tip'
44
- import { formItemCls as cls } from './helper'
45
+ import { formItemCls as cls, defineField } from './helper'
46
+ import { validateField } from './validate'
45
47
 
46
48
  defineOptions({ name: 'FormItem' })
47
49
 
@@ -54,7 +56,7 @@ defineSlots<{
54
56
  }>()
55
57
 
56
58
  /** 表单组件上下文 */
57
- const { formProps } = injectFormContext()
59
+ const { formProps, registerField, unregisterField, shouldValidate } = injectFormContext()
58
60
 
59
61
  const { config } = useConfig()
60
62
 
@@ -63,8 +65,14 @@ const { size, readonly } = useFallbackProps([formProps ?? {}, props], {
63
65
  readonly: false
64
66
  })
65
67
 
68
+ const errorTip = shallowRef<string>()
69
+ /** 递增序号,丢弃过期的异步校验结果 */
70
+ let validateSeq = 0
71
+ /** 最近一次 validate 的 Promise,供过期调用等待最新结果 */
72
+ let latestValidatePromise: Promise<boolean> = Promise.resolve(true)
73
+
66
74
  const className = computed(() => {
67
- return [cls.b, cls.m(size.value), bem.is('error', !!errorTips.value)].join(' ')
75
+ return [cls.b, cls.m(size.value), bem.is('error', !!errorTip.value)].join(' ')
68
76
  })
69
77
 
70
78
  /** label样式 */
@@ -74,17 +82,60 @@ const labelStyles = computed<CSSProperties>(() => {
74
82
  }
75
83
  })
76
84
 
77
- /** 错误提示 */
78
- const errorTips = computed<string | undefined>(() => {
79
- if (!props.field) return undefined
80
- return formProps?.model?.errors.get(props.field)?.[0]
85
+ const fieldItem = defineField({
86
+ clearValidate() {
87
+ errorTip.value = ''
88
+ },
89
+ async validate() {
90
+ if (!props.field || !formProps?.model || !props.rules || !shouldValidate?.()) return true
91
+
92
+ const seq = ++validateSeq
93
+
94
+ latestValidatePromise = validateField(formProps.model!, props.field!, props.rules!).then(
95
+ (tip) => {
96
+ if (seq !== validateSeq) return latestValidatePromise
97
+
98
+ errorTip.value = tip
99
+ return !errorTip.value
100
+ }
101
+ )
102
+ return latestValidatePromise
103
+ }
81
104
  })
82
105
 
83
- /** 字段是否必须 */
84
- const fieldRequired = computed<boolean>(() => {
85
- const { field } = props
86
- if (!field || readonly.value) return false
87
- const required = formProps?.model?.fields[field]?.required
88
- return Boolean(required)
106
+ watch(
107
+ () => props.field,
108
+ (field, oldField) => {
109
+ if (oldField) {
110
+ unregisterField?.(oldField)
111
+ }
112
+ if (field) {
113
+ registerField?.(field, fieldItem)
114
+ }
115
+ },
116
+ { immediate: true }
117
+ )
118
+
119
+ let stopWatchFieldValue: (() => void) | undefined
120
+
121
+ watch(
122
+ [() => formProps?.model, () => props.field],
123
+ ([model, field]) => {
124
+ stopWatchFieldValue?.()
125
+
126
+ if (!field || !model) return
127
+
128
+ stopWatchFieldValue = watch(
129
+ () => o(model).get(field),
130
+ () => {
131
+ fieldItem.validate()
132
+ }
133
+ )
134
+ },
135
+ { immediate: true }
136
+ )
137
+
138
+ onBeforeUnmount(() => {
139
+ props.field && unregisterField?.(props.field)
89
140
  })
90
141
  </script>