@veltra/desktop 1.1.24 → 1.1.26

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 (250) hide show
  1. package/dist/components/action/action-group.vue.d.ts +2 -2
  2. package/dist/components/action/action.js.map +1 -1
  3. package/dist/components/action/action.vue.d.ts +1 -1
  4. package/dist/components/badge/badge.js.map +1 -1
  5. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  6. package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
  7. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  8. package/dist/components/breadcrumb/breadcrumb.js.map +1 -1
  9. package/dist/components/button/button-group.js.map +1 -1
  10. package/dist/components/button/button.js.map +1 -1
  11. package/dist/components/calendar/calendar.js.map +1 -1
  12. package/dist/components/card/card-action.js.map +1 -1
  13. package/dist/components/card/card-content.js.map +1 -1
  14. package/dist/components/card/card-cover.js.map +1 -1
  15. package/dist/components/card/card.js.map +1 -1
  16. package/dist/components/cascade/cascade-panel-item.js.map +1 -1
  17. package/dist/components/cascade/cascade.js.map +1 -1
  18. package/dist/components/cascade/cascade.vue.d.ts +1 -1
  19. package/dist/components/check-tag/check-tag.js.map +1 -1
  20. package/dist/components/checkbox/checkbox-button.js.map +1 -1
  21. package/dist/components/checkbox/checkbox.js.map +1 -1
  22. package/dist/components/checkbox-group/checkbox-group.js.map +1 -1
  23. package/dist/components/code-editor/code-editor.js.map +1 -1
  24. package/dist/components/collapse/collapse-item.js.map +1 -1
  25. package/dist/components/collapse/collapse.js.map +1 -1
  26. package/dist/components/condition-editor/components/condition-row.js.map +1 -1
  27. package/dist/components/condition-editor/condition-editor.js.map +1 -1
  28. package/dist/components/context-menu/context-menu-item.js.map +1 -1
  29. package/dist/components/context-menu/context-menu.js.map +1 -1
  30. package/dist/components/date-panel/date-panel.js.map +1 -1
  31. package/dist/components/date-panel/headers/day.js.map +1 -1
  32. package/dist/components/date-panel/headers/month.js.map +1 -1
  33. package/dist/components/date-panel/headers/year.js.map +1 -1
  34. package/dist/components/date-panel/panels/day.js.map +1 -1
  35. package/dist/components/date-panel/panels/month.js.map +1 -1
  36. package/dist/components/date-panel/panels/year.js.map +1 -1
  37. package/dist/components/date-picker/date-picker.js.map +1 -1
  38. package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
  39. package/dist/components/dialog/dialog.js.map +1 -1
  40. package/dist/components/drawer/drawer.js.map +1 -1
  41. package/dist/components/empty/empty.js.map +1 -1
  42. package/dist/components/expression-editor/components/path-preview.js.map +1 -1
  43. package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
  44. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  45. package/dist/components/file-picker/file-picker.js.map +1 -1
  46. package/dist/components/file-viewer/file-viewer.js.map +1 -1
  47. package/dist/components/file-viewer/previewers/pdf-previewer.js.map +1 -1
  48. package/dist/components/file-viewer/previewers/pdf-viewport-scroll.js.map +1 -1
  49. package/dist/components/file-viewer/previewers/pdf-zoom-bridge.js.map +1 -1
  50. package/dist/components/file-viewer/previewers/sheet-previewer.js.map +1 -1
  51. package/dist/components/float-button/float-button.js.map +1 -1
  52. package/dist/components/form/dynamic-form-model.d.ts +7 -5
  53. package/dist/components/form/dynamic-form-model.js +31 -12
  54. package/dist/components/form/dynamic-form-model.js.map +1 -1
  55. package/dist/components/form/form-model.d.ts +8 -8
  56. package/dist/components/form/form-model.js +17 -3
  57. package/dist/components/form/form-model.js.map +1 -1
  58. package/dist/components/form/form.js.map +1 -1
  59. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
  60. package/dist/components/form/helper.d.ts +6 -1
  61. package/dist/components/form/helper.js +7 -1
  62. package/dist/components/form/helper.js.map +1 -1
  63. package/dist/components/form/index.d.ts +2 -2
  64. package/dist/components/gantt-chart/gantt-chart.js.map +1 -1
  65. package/dist/components/grid/grid-item.js.map +1 -1
  66. package/dist/components/grid/grid.js.map +1 -1
  67. package/dist/components/grid-input/grid-input.js.map +1 -1
  68. package/dist/components/group-input/group-input.js.map +1 -1
  69. package/dist/components/icon/icon.js.map +1 -1
  70. package/dist/components/index.d.ts +1 -1
  71. package/dist/components/index.js +2 -1
  72. package/dist/components/input/input.js.map +1 -1
  73. package/dist/components/input/input.vue_vue_type_script_setup_true_lang.js.map +1 -1
  74. package/dist/components/layout/layout-resizer.js.map +1 -1
  75. package/dist/components/layout/layout.js.map +1 -1
  76. package/dist/components/list/list-item.js.map +1 -1
  77. package/dist/components/list/list.js.map +1 -1
  78. package/dist/components/loading/loading.js.map +1 -1
  79. package/dist/components/menu/menu-icon.js.map +1 -1
  80. package/dist/components/menu/menu-sub-collapsed.js.map +1 -1
  81. package/dist/components/menu/menu.js.map +1 -1
  82. package/dist/components/message/message-box.js.map +1 -1
  83. package/dist/components/message/message.js.map +1 -1
  84. package/dist/components/message-confirm/message-confirm.js.map +1 -1
  85. package/dist/components/multi-select/multi-select-option.js.map +1 -1
  86. package/dist/components/multi-select/multi-select.vue.d.ts +1 -1
  87. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +1 -1
  88. package/dist/components/notification/notification.js.map +1 -1
  89. package/dist/components/notification/notification.vue.d.ts +2 -2
  90. package/dist/components/number/number.js.map +1 -1
  91. package/dist/components/number-input/number-input.js.map +1 -1
  92. package/dist/components/number-range-input/number-range-input.js.map +1 -1
  93. package/dist/components/paginator/paginator.js.map +1 -1
  94. package/dist/components/palette/palette-alpha.js.map +1 -1
  95. package/dist/components/palette/palette-color-switch.js.map +1 -1
  96. package/dist/components/palette/palette-hue.js.map +1 -1
  97. package/dist/components/palette/palette-sv.js.map +1 -1
  98. package/dist/components/palette/palette.js.map +1 -1
  99. package/dist/components/pop-confirm/pop-confirm.js.map +1 -1
  100. package/dist/components/pop-confirm/pop-confirm.vue.d.ts +1 -1
  101. package/dist/components/progress/progress.js.map +1 -1
  102. package/dist/components/progress-nodes/progress-nodes.js.map +1 -1
  103. package/dist/components/radio/radio.js.map +1 -1
  104. package/dist/components/radio-group/radio-group.js.map +1 -1
  105. package/dist/components/scroll/scroll-bar.js.map +1 -1
  106. package/dist/components/scroll/scroll.js.map +1 -1
  107. package/dist/components/slider/slider-thumb.js.map +1 -1
  108. package/dist/components/slider/slider.js.map +1 -1
  109. package/dist/components/steps/steps.js.map +1 -1
  110. package/dist/components/switch/switch.js.map +1 -1
  111. package/dist/components/table/table-body.js.map +1 -1
  112. package/dist/components/table/table-cell.js.map +1 -1
  113. package/dist/components/table/table-foot.js.map +1 -1
  114. package/dist/components/table-editor/table-editor.js.map +1 -1
  115. package/dist/components/tabs/tabs-horizontal.js.map +1 -1
  116. package/dist/components/tabs/tabs-horizontal.vue.d.ts +4 -4
  117. package/dist/components/tabs/tabs-vertical.js.map +1 -1
  118. package/dist/components/tabs/tabs-vertical.vue.d.ts +3 -3
  119. package/dist/components/tabs/tabs.js.map +1 -1
  120. package/dist/components/tabs/tabs.vue.d.ts +4 -4
  121. package/dist/components/tag/tag.js.map +1 -1
  122. package/dist/components/text/text.js.map +1 -1
  123. package/dist/components/textarea/textarea.js.map +1 -1
  124. package/dist/components/textarea/textarea.vue.d.ts +1 -1
  125. package/dist/components/theme/theme.js.map +1 -1
  126. package/dist/components/tip/tip.js.map +1 -1
  127. package/dist/components/tip/tip.vue.d.ts +1 -1
  128. package/dist/components/tree/tree-node.js.map +1 -1
  129. package/dist/components/tree/tree.js.map +1 -1
  130. package/dist/components/tree-select/tree-select.vue.d.ts +1 -1
  131. package/dist/components/watermark/watermark.js.map +1 -1
  132. package/dist/index.d.ts +4 -4
  133. package/dist/index.js +2 -2
  134. package/dist/types/form.d.ts +12 -9
  135. package/dist/types/index.d.ts +2 -2
  136. package/package.json +6 -6
  137. package/src/components/action/action.vue +1 -4
  138. package/src/components/badge/badge.vue +3 -9
  139. package/src/components/batch-edit/batch-edit-form.vue +1 -5
  140. package/src/components/batch-edit/batch-edit-list.vue +2 -6
  141. package/src/components/batch-edit/batch-edit.vue +2 -12
  142. package/src/components/breadcrumb/breadcrumb.vue +4 -13
  143. package/src/components/button/button-group.vue +2 -6
  144. package/src/components/button/button.vue +3 -9
  145. package/src/components/calendar/calendar.vue +1 -3
  146. package/src/components/card/card-action.vue +1 -3
  147. package/src/components/card/card-content.vue +1 -6
  148. package/src/components/card/card-cover.vue +1 -3
  149. package/src/components/card/card.vue +2 -6
  150. package/src/components/cascade/cascade-panel-item.vue +2 -8
  151. package/src/components/cascade/cascade.vue +5 -37
  152. package/src/components/check-tag/check-tag.vue +1 -3
  153. package/src/components/checkbox/checkbox-button.vue +2 -6
  154. package/src/components/checkbox/checkbox.vue +1 -3
  155. package/src/components/checkbox-group/checkbox-group.vue +1 -3
  156. package/src/components/code-editor/code-editor.vue +4 -12
  157. package/src/components/collapse/collapse-item.vue +1 -3
  158. package/src/components/collapse/collapse.vue +2 -8
  159. package/src/components/condition-editor/components/condition-row.vue +2 -8
  160. package/src/components/condition-editor/condition-editor.vue +2 -6
  161. package/src/components/context-menu/context-menu-item.vue +3 -10
  162. package/src/components/context-menu/context-menu.vue +5 -17
  163. package/src/components/date-panel/date-panel.vue +3 -11
  164. package/src/components/date-panel/headers/day.vue +1 -3
  165. package/src/components/date-panel/headers/month.vue +1 -3
  166. package/src/components/date-panel/headers/year.vue +1 -3
  167. package/src/components/date-panel/panels/day.vue +1 -4
  168. package/src/components/date-panel/panels/month.vue +1 -3
  169. package/src/components/date-panel/panels/year.vue +1 -3
  170. package/src/components/date-picker/date-picker.vue +1 -3
  171. package/src/components/date-range-picker/date-range-picker.vue +1 -3
  172. package/src/components/dialog/dialog.vue +7 -24
  173. package/src/components/drawer/drawer.vue +3 -11
  174. package/src/components/empty/empty.vue +2 -7
  175. package/src/components/expression-editor/components/path-preview.vue +2 -6
  176. package/src/components/expression-editor/components/variable-picker.vue +1 -3
  177. package/src/components/expression-editor/expression-editor.vue +2 -6
  178. package/src/components/expression-editor/style.scss +1 -1
  179. package/src/components/file-picker/file-picker.vue +2 -7
  180. package/src/components/file-viewer/file-viewer.vue +4 -22
  181. package/src/components/file-viewer/previewers/pdf-previewer.vue +1 -5
  182. package/src/components/file-viewer/previewers/pdf-viewport-scroll.vue +1 -7
  183. package/src/components/file-viewer/previewers/pdf-zoom-bridge.vue +2 -7
  184. package/src/components/file-viewer/previewers/sheet-previewer.vue +4 -10
  185. package/src/components/float-button/float-button.vue +1 -3
  186. package/src/components/form/__test__/form-model.test.ts +190 -0
  187. package/src/components/form/dynamic-form-model.ts +47 -19
  188. package/src/components/form/form-model.ts +40 -28
  189. package/src/components/form/form.vue +3 -13
  190. package/src/components/form/helper.ts +9 -0
  191. package/src/components/form/index.ts +2 -1
  192. package/src/components/gantt-chart/gantt-chart.vue +1 -3
  193. package/src/components/grid/grid-item.vue +3 -9
  194. package/src/components/grid/grid.vue +5 -18
  195. package/src/components/grid-input/grid-input.vue +2 -6
  196. package/src/components/group-input/group-input.vue +2 -7
  197. package/src/components/icon/icon.vue +2 -6
  198. package/src/components/input/input.vue +2 -6
  199. package/src/components/layout/layout-resizer.vue +1 -3
  200. package/src/components/layout/layout.vue +3 -9
  201. package/src/components/list/list-item.vue +1 -3
  202. package/src/components/list/list.vue +2 -6
  203. package/src/components/loading/loading.vue +3 -9
  204. package/src/components/menu/menu-icon.vue +2 -6
  205. package/src/components/menu/menu-sub-collapsed.vue +2 -8
  206. package/src/components/menu/menu.vue +3 -9
  207. package/src/components/menu/style.scss +1 -1
  208. package/src/components/message/message-box.vue +3 -10
  209. package/src/components/message/message.vue +3 -9
  210. package/src/components/message-confirm/message-confirm.vue +2 -6
  211. package/src/components/multi-select/multi-select-option.vue +3 -9
  212. package/src/components/notification/notification.vue +3 -11
  213. package/src/components/number/number.vue +4 -14
  214. package/src/components/number-input/number-input.vue +7 -36
  215. package/src/components/number-range-input/number-range-input.vue +4 -17
  216. package/src/components/paginator/paginator.vue +7 -22
  217. package/src/components/palette/palette-alpha.vue +2 -6
  218. package/src/components/palette/palette-color-switch.vue +1 -3
  219. package/src/components/palette/palette-hue.vue +2 -6
  220. package/src/components/palette/palette-sv.vue +5 -17
  221. package/src/components/palette/palette.vue +2 -12
  222. package/src/components/pop-confirm/pop-confirm.vue +2 -6
  223. package/src/components/progress/progress.vue +2 -7
  224. package/src/components/progress-nodes/progress-nodes.vue +2 -7
  225. package/src/components/radio/radio.vue +2 -6
  226. package/src/components/radio-group/radio-group.vue +1 -3
  227. package/src/components/scroll/scroll-bar.vue +5 -17
  228. package/src/components/scroll/scroll.vue +5 -16
  229. package/src/components/slider/slider-thumb.vue +4 -12
  230. package/src/components/slider/slider.vue +3 -14
  231. package/src/components/steps/steps.vue +3 -10
  232. package/src/components/switch/switch.vue +2 -7
  233. package/src/components/table/table-body.vue +2 -8
  234. package/src/components/table/table-cell.vue +2 -7
  235. package/src/components/table/table-foot.vue +2 -9
  236. package/src/components/table-editor/table-editor.vue +1 -3
  237. package/src/components/tabs/tabs-horizontal.vue +2 -6
  238. package/src/components/tabs/tabs-vertical.vue +1 -3
  239. package/src/components/tabs/tabs.vue +1 -3
  240. package/src/components/tag/tag.vue +2 -6
  241. package/src/components/text/text.vue +3 -9
  242. package/src/components/textarea/textarea.vue +1 -3
  243. package/src/components/theme/group-item.vue +1 -3
  244. package/src/components/theme/group.vue +1 -3
  245. package/src/components/theme/theme.vue +6 -26
  246. package/src/components/tip/tip.vue +1 -3
  247. package/src/components/tree/tree-node.vue +1 -3
  248. package/src/components/tree/tree.vue +7 -30
  249. package/src/components/watermark/watermark.vue +4 -15
  250. package/src/types/form.ts +29 -11
@@ -0,0 +1,190 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { DynamicFormModel } from '../dynamic-form-model'
4
+ import { FormModel } from '../form-model'
5
+ import { formField, nestField } from '../helper'
6
+
7
+ describe('FormModel with Nested Fields', () => {
8
+ it('should flatten fields and initialize data correctly', () => {
9
+ const model = new FormModel({
10
+ name: formField({ value: 'Alice' }),
11
+ contact: nestField({
12
+ email: formField({ value: 'alice@example.com' }),
13
+ phone: formField({ value: '12345678901' })
14
+ })
15
+ })
16
+
17
+ // Verify allKeys
18
+ expect(model.allKeys).toEqual(['name', 'contact.email', 'contact.phone'])
19
+
20
+ // Verify initialData and data
21
+ expect(model.data).toEqual({
22
+ name: 'Alice',
23
+ contact: { email: 'alice@example.com', phone: '12345678901' }
24
+ })
25
+ expect(model.initialData).toEqual({
26
+ name: 'Alice',
27
+ contact: { email: 'alice@example.com', phone: '12345678901' }
28
+ })
29
+ })
30
+
31
+ it('should validate flat and nested fields correctly', async () => {
32
+ const model = new FormModel({
33
+ name: formField({ value: '', required: 'Name is required' }),
34
+ contact: nestField({
35
+ email: formField({ value: 'invalid-email', preset: 'email' }),
36
+ phone: formField({ value: '12345678901', required: true })
37
+ })
38
+ })
39
+
40
+ // Expect initial validation to fail on name and contact.email
41
+ await expect(model.validate()).rejects.toBe(false)
42
+ expect(model.errors.get('name')).toEqual(['Name is required'])
43
+ expect(model.errors.get('contact.email')).toEqual(['邮箱格式不正确'])
44
+ expect(model.errors.get('contact.phone')).toBeUndefined()
45
+
46
+ // Fix name and contact.email, and trigger validation
47
+ model.data.name = 'Bob'
48
+ model.data.contact.email = 'bob@example.com'
49
+
50
+ // We need to wait for watch effects/nextTick if reactive triggers validation on change
51
+ // Since validate() is an async function, let's call it directly
52
+ const valid = await model.validate()
53
+ expect(valid).toBe(true)
54
+ expect(model.errors.size).toBe(0)
55
+ })
56
+
57
+ it('should set data and reset data correctly', () => {
58
+ const model = new FormModel({
59
+ name: formField({ value: 'Alice' }),
60
+ contact: nestField({
61
+ email: formField({ value: 'alice@example.com' }),
62
+ phone: formField({ value: '12345678901' })
63
+ })
64
+ })
65
+
66
+ // Update data via setData
67
+ model.setData({ name: 'Bob', contact: { email: 'bob@example.com', phone: '98765432109' } })
68
+
69
+ expect(model.data).toEqual({
70
+ name: 'Bob',
71
+ contact: { email: 'bob@example.com', phone: '98765432109' }
72
+ })
73
+
74
+ // Reset data
75
+ model.resetData()
76
+ expect(model.data).toEqual({
77
+ name: 'Alice',
78
+ contact: { email: 'alice@example.com', phone: '12345678901' }
79
+ })
80
+ })
81
+ })
82
+
83
+ describe('DynamicFormModel with Nested Fields', () => {
84
+ it('should flatten fields and initialize data correctly', () => {
85
+ const model = new DynamicFormModel({
86
+ name: formField({ value: 'Alice' }),
87
+ contact: nestField({
88
+ email: formField({ value: 'alice@example.com' }),
89
+ phone: formField({ value: '12345678901' })
90
+ })
91
+ })
92
+
93
+ // Verify allKeys
94
+ expect(model.allKeys).toEqual(['name', 'contact.email', 'contact.phone'])
95
+
96
+ // Verify initialData and data
97
+ expect(model.data).toEqual({
98
+ name: 'Alice',
99
+ contact: { email: 'alice@example.com', phone: '12345678901' }
100
+ })
101
+ expect(model.initialData).toEqual({
102
+ name: 'Alice',
103
+ contact: { email: 'alice@example.com', phone: '12345678901' }
104
+ })
105
+ })
106
+
107
+ it('should support dynamic add and delete on nested fields', () => {
108
+ const model = new DynamicFormModel({ name: formField({ value: 'Alice' }) })
109
+
110
+ expect(model.allKeys).toEqual(['name'])
111
+
112
+ // Dynamically add nested fields
113
+ model.add(
114
+ 'contact',
115
+ nestField({
116
+ email: formField({ value: 'alice@example.com' }),
117
+ phone: formField({ value: '12345678901' })
118
+ })
119
+ )
120
+
121
+ expect(model.allKeys).toEqual(['name', 'contact.email', 'contact.phone'])
122
+ expect(model.data).toEqual({
123
+ name: 'Alice',
124
+ contact: { email: 'alice@example.com', phone: '12345678901' }
125
+ })
126
+ expect(model.initialData).toEqual({
127
+ name: 'Alice',
128
+ contact: { email: 'alice@example.com', phone: '12345678901' }
129
+ })
130
+
131
+ // Dynamically delete nested fields
132
+ model.delete('contact')
133
+
134
+ expect(model.allKeys).toEqual(['name'])
135
+ expect(model.data.name).toBe('Alice')
136
+ expect(model.fields.contact).toBeUndefined()
137
+ })
138
+
139
+ it('should validate flat and nested fields correctly', async () => {
140
+ const model = new DynamicFormModel()
141
+
142
+ model.add('name', formField({ value: '', required: 'Name is required' }))
143
+ model.add(
144
+ 'contact',
145
+ nestField({
146
+ email: formField({ value: 'invalid-email', preset: 'email' }),
147
+ phone: formField({ value: '12345678901', required: true })
148
+ })
149
+ )
150
+
151
+ // Expect initial validation to fail on name and contact.email
152
+ await expect(model.validate()).resolves.toBe(false)
153
+ expect(model.errors.get('name')).toEqual(['Name is required'])
154
+ expect(model.errors.get('contact.email')).toEqual(['邮箱格式不正确'])
155
+ expect(model.errors.get('contact.phone')).toBeUndefined()
156
+
157
+ // Fix name and contact.email, and trigger validation
158
+ model.data.name = 'Bob'
159
+ model.data.contact.email = 'bob@example.com'
160
+
161
+ const valid = await model.validate()
162
+ expect(valid).toBe(true)
163
+ expect(model.errors.size).toBe(0)
164
+ })
165
+
166
+ it('should set data and reset data correctly for nested fields', () => {
167
+ const model = new DynamicFormModel({
168
+ name: formField({ value: 'Alice' }),
169
+ contact: nestField({
170
+ email: formField({ value: 'alice@example.com' }),
171
+ phone: formField({ value: '12345678901' })
172
+ })
173
+ })
174
+
175
+ // Update data via setData
176
+ model.setData({ name: 'Bob', contact: { email: 'bob@example.com', phone: '98765432109' } })
177
+
178
+ expect(model.data).toEqual({
179
+ name: 'Bob',
180
+ contact: { email: 'bob@example.com', phone: '98765432109' }
181
+ })
182
+
183
+ // Reset data
184
+ model.resetData()
185
+ expect(model.data).toEqual({
186
+ name: 'Alice',
187
+ contact: { email: 'alice@example.com', phone: '12345678901' }
188
+ })
189
+ })
190
+ })
@@ -2,7 +2,21 @@ import { o } from '@cat-kit/core'
2
2
  import { Validator } from '@veltra/utils'
3
3
  import { isReactive, reactive, shallowReactive, watch } from 'vue'
4
4
 
5
- import type { FormModelItem, DataSettingConfig } from '../../types'
5
+ import type { FormModelField, FormModelItem, DataSettingConfig } from '../../types'
6
+
7
+ function flattenFields(fields: Record<string, any>, prefix = ''): Record<string, FormModelItem> {
8
+ const result: Record<string, FormModelItem> = {}
9
+ for (const key in fields) {
10
+ const item = fields[key]
11
+ const fullPath = prefix ? `${prefix}.${key}` : key
12
+ if (item && typeof item === 'object' && '__isNested' in item && item.__isNested) {
13
+ Object.assign(result, flattenFields((item as any).fields, fullPath))
14
+ } else {
15
+ result[fullPath] = item
16
+ }
17
+ }
18
+ return result
19
+ }
6
20
 
7
21
  /**
8
22
  * 动态表单模型
@@ -25,10 +39,12 @@ export class DynamicFormModel {
25
39
  }
26
40
 
27
41
  /** 表单规则 */
28
- readonly fields: Record<string, FormModelItem> = shallowReactive(
29
- {} as Record<string, FormModelItem>
42
+ readonly fields: Record<string, FormModelField> = shallowReactive(
43
+ {} as Record<string, FormModelField>
30
44
  )
31
45
 
46
+ private flatFields: Record<string, FormModelItem>
47
+
32
48
  private validator: Validator
33
49
 
34
50
  /**
@@ -52,8 +68,17 @@ export class DynamicFormModel {
52
68
  return this._allKeys
53
69
  }
54
70
 
71
+ private updateFlatFields(): void {
72
+ for (const key in this.flatFields) {
73
+ delete this.flatFields[key]
74
+ }
75
+ const flatted = flattenFields(this.fields)
76
+ Object.assign(this.flatFields, flatted)
77
+ this.getAllKeys()
78
+ }
79
+
55
80
  private getAllKeys(): void {
56
- this._allKeys = Object.keys(this.fields)
81
+ this._allKeys = Object.keys(this.flatFields)
57
82
  }
58
83
 
59
84
  /** 初始数据 */
@@ -64,10 +89,10 @@ export class DynamicFormModel {
64
89
  */
65
90
  private readonly oldData: Record<string, any> = {}
66
91
 
67
- constructor(fields?: Record<string, FormModelItem>) {
92
+ constructor(fields?: Record<string, FormModelField>) {
93
+ this.flatFields = shallowReactive({} as Record<string, FormModelItem>)
94
+ this.validator = new Validator(this.flatFields)
68
95
  this.append(fields ?? {})
69
- this.validator = new Validator(this.fields)
70
- this.getAllKeys()
71
96
  this.watchData()
72
97
  }
73
98
 
@@ -94,9 +119,9 @@ export class DynamicFormModel {
94
119
  })
95
120
  }
96
121
 
97
- append(fields: Record<string, FormModelItem>): void {
122
+ append(fields: Record<string, FormModelField>): void {
98
123
  for (const key in fields) {
99
- this.add(key, fields[key] ?? {})
124
+ this.add(key, fields[key]!)
100
125
  }
101
126
  }
102
127
 
@@ -105,17 +130,20 @@ export class DynamicFormModel {
105
130
  * @param field 字段
106
131
  * @param item 字段配置项
107
132
  */
108
- add(field: string, item: FormModelItem): void {
133
+ add(field: string, item: FormModelField): void {
109
134
  this.fields[field] = item
110
- const { value } = item
111
- const v = typeof value === 'function' ? value() : value
112
- /** 这是有bug的 */
113
- if (v !== undefined) {
114
- this.initialData[field] = JSON.parse(JSON.stringify(v))
135
+ const flatItem = flattenFields({ [field]: item })
136
+ for (const key in flatItem) {
137
+ const fieldItem = flatItem[key]!
138
+ const { value } = fieldItem
139
+ const v = typeof value === 'function' ? value() : value
140
+ if (v !== undefined) {
141
+ o(this.initialData).set(key, JSON.parse(JSON.stringify(v)))
142
+ }
143
+ o(this.data).set(key, v)
144
+ o(this.oldData).set(key, v)
115
145
  }
116
- o(this.data).set(field, v)
117
- o(this.oldData).set(field, v)
118
- this.getAllKeys()
146
+ this.updateFlatFields()
119
147
  }
120
148
 
121
149
  /**
@@ -124,7 +152,7 @@ export class DynamicFormModel {
124
152
  */
125
153
  delete(field: string): void {
126
154
  delete this.fields[field]
127
- this.getAllKeys()
155
+ this.updateFlatFields()
128
156
  }
129
157
 
130
158
  /**
@@ -2,19 +2,30 @@ import { o } from '@cat-kit/core'
2
2
  import { middleProxy, Validator } from '@veltra/utils'
3
3
  import { nextTick, reactive, shallowReactive, watch, type Reactive } from 'vue'
4
4
 
5
- import type {
6
- FormModelItem,
7
- ModelData,
8
- ModelRules,
9
- DataSettingConfig,
10
- IFormModel
11
- } from '../../types'
5
+ import type { ModelData, DataSettingConfig, IFormModel, AllKeys, FormModelItem } from '../../types'
6
+
7
+ /**
8
+ * 递归展平字段逻辑
9
+ */
10
+ function flattenFields(fields: Record<string, any>, prefix = ''): Record<string, FormModelItem> {
11
+ const result: Record<string, FormModelItem> = {}
12
+ for (const key in fields) {
13
+ const item = fields[key]
14
+ const fullPath = prefix ? `${prefix}.${key}` : key
15
+ if (item && typeof item === 'object' && '__isNested' in item && item.__isNested) {
16
+ Object.assign(result, flattenFields((item as any).fields, fullPath))
17
+ } else {
18
+ result[fullPath] = item
19
+ }
20
+ }
21
+ return result
22
+ }
12
23
 
13
24
  /**
14
25
  * 表单模型
15
26
  */
16
27
  export class FormModel<
17
- Fields extends Record<string, FormModelItem> = Record<string, FormModelItem>
28
+ Fields extends Record<string, any> = Record<string, any>
18
29
  > implements IFormModel<Fields> {
19
30
  /** 表单数据 */
20
31
  data!: ModelData<Fields>
@@ -30,14 +41,14 @@ export class FormModel<
30
41
  * @description
31
42
  * 这个值会在表单组件渲染时由表单设置,因为只有真正渲染的组件才应该被校验
32
43
  */
33
- formKeys: Map<number, (keyof Fields)[]> = new Map()
44
+ formKeys: Map<number, AllKeys<Fields>[]> = new Map()
34
45
 
35
46
  /** 初始数据 */
36
47
  readonly initialData: ModelData<Fields>
37
48
 
38
- readonly errors: Map<keyof Fields, string[] | undefined> = shallowReactive(new Map())
49
+ readonly errors: Map<AllKeys<Fields>, string[] | undefined> = shallowReactive(new Map())
39
50
 
40
- private validator: Validator<ModelRules<Fields>>
51
+ private validator: Validator<Record<string, FormModelItem>>
41
52
 
42
53
  /**
43
54
  * 是否在表单值更新时校验
@@ -48,11 +59,12 @@ export class FormModel<
48
59
 
49
60
  constructor(fields: Fields) {
50
61
  this.fields = fields
62
+ const flatFields = flattenFields(fields)
51
63
  const rawData = {} as ModelData<Fields>
52
64
  const allKeys: string[] = []
53
65
 
54
- for (const key in fields) {
55
- const fieldItem = fields[key]!
66
+ for (const key in flatFields) {
67
+ const fieldItem = flatFields[key]!
56
68
  allKeys.push(key)
57
69
  const { value } = fieldItem
58
70
  let v = value
@@ -72,7 +84,7 @@ export class FormModel<
72
84
 
73
85
  this.setProxyData(reactive(rawData))
74
86
 
75
- this.validator = new Validator(this.fields)
87
+ this.validator = new Validator(flatFields)
76
88
 
77
89
  return shallowReactive(this)
78
90
  }
@@ -93,23 +105,23 @@ export class FormModel<
93
105
  this.validateOnFieldChange = true
94
106
  return
95
107
  }
96
- this.validate(fields)
108
+ this.validate(fields as any)
97
109
  }
98
110
  })
99
111
 
100
112
  this.data = data as ModelData<Fields>
101
113
  }
102
114
 
103
- private getValidateFields(fields?: keyof Fields | (keyof Fields)[]) {
115
+ private getValidateFields(fields?: AllKeys<Fields> | AllKeys<Fields>[]) {
104
116
  if (!fields) {
105
117
  if (this.formKeys.size) {
106
- let _fields: (keyof Fields)[] = []
118
+ let _fields: AllKeys<Fields>[] = []
107
119
  this.formKeys.forEach((fields) => {
108
120
  _fields = _fields.concat(fields)
109
121
  })
110
122
  return _fields
111
123
  } else {
112
- return this.allKeys
124
+ return this.allKeys as AllKeys<Fields>[]
113
125
  }
114
126
  }
115
127
 
@@ -124,23 +136,23 @@ export class FormModel<
124
136
  * @param fields 需要校验的字段, 不传则校验所有字段
125
137
  * @returns
126
138
  */
127
- async validate(fields?: keyof Fields | (keyof Fields)[]): Promise<boolean> {
139
+ async validate(fields?: AllKeys<Fields> | AllKeys<Fields>[]): Promise<boolean> {
128
140
  const { errors, validator, data } = this
129
141
 
130
- const results = await validator.validate(data, this.getValidateFields(fields))
142
+ const results = await validator.validate(data, this.getValidateFields(fields) as any)
131
143
 
132
144
  // 全量校验
133
145
  if (!fields) {
134
146
  errors.clear()
135
147
 
136
148
  for (const field in results) {
137
- errors.set(field, results[field])
149
+ errors.set(field as any, results[field])
138
150
  }
139
151
  }
140
152
  // 局部校验
141
153
  else {
142
154
  ~(Array.isArray(fields) ? fields : [fields]).forEach((field) => {
143
- const errs = results[field]
155
+ const errs = results[field as string]
144
156
  if (errs?.length) {
145
157
  errors.set(field, errs)
146
158
  } else {
@@ -165,12 +177,12 @@ export class FormModel<
165
177
  * 重置数据
166
178
  * @param fields 需要重置的字段
167
179
  */
168
- resetData(keys?: keyof Fields | (keyof Fields)[]): void {
180
+ resetData(keys?: AllKeys<Fields> | AllKeys<Fields>[]): void {
169
181
  if (typeof keys === 'string') {
170
182
  keys = [keys]
171
183
  } else if (Array.isArray(keys)) {
172
184
  } else {
173
- keys = this.allKeys
185
+ keys = this.allKeys as AllKeys<Fields>[]
174
186
  }
175
187
 
176
188
  this.clearValidate()
@@ -229,12 +241,12 @@ export class FormModel<
229
241
  * 监听值变更
230
242
  * @param cb 回调
231
243
  */
232
- onChange(cb: (field: keyof Fields, val: any) => void): void {
233
- this.modelChangeCallback.add(cb)
244
+ onChange(cb: (field: AllKeys<Fields>, val: any) => void): void {
245
+ this.modelChangeCallback.add(cb as any)
234
246
  }
235
247
 
236
248
  /** 关闭监听值变更 */
237
- offChange(cb: (field: keyof Fields, val: any) => void): void {
238
- this.modelChangeCallback.delete(cb)
249
+ offChange(cb: (field: AllKeys<Fields>, val: any) => void): void {
250
+ this.modelChangeCallback.delete(cb as any)
239
251
  }
240
252
  }
@@ -42,9 +42,7 @@ import type { DynamicFormModel } from './dynamic-form-model'
42
42
  import type { FormModel } from './form-model'
43
43
  import { useNodeInterceptor } from './use-node-interceptor'
44
44
 
45
- defineOptions({
46
- name: 'Form'
47
- })
45
+ defineOptions({ name: 'Form' })
48
46
 
49
47
  const props = defineProps<FormProps<Model>>()
50
48
 
@@ -59,13 +57,7 @@ defineSlots<{
59
57
 
60
58
  const cls = bem('form')
61
59
 
62
- const breakpointCols: BreakCols = {
63
- xs: 1,
64
- md: 2,
65
- lg: 3,
66
- xl: 4,
67
- default: 4
68
- }
60
+ const breakpointCols: BreakCols = { xs: 1, md: 2, lg: 3, xl: 4, default: 4 }
69
61
 
70
62
  provideFormContext(props)
71
63
 
@@ -89,7 +81,5 @@ function showInitialNode(field: string) {
89
81
 
90
82
  const gridRef = shallowRef<GridExposed>()
91
83
 
92
- defineExpose<_FormExposed>({
93
- el: toRef(() => gridRef.value?.el)
94
- })
84
+ defineExpose<_FormExposed>({ el: toRef(() => gridRef.value?.el) })
95
85
  </script>
@@ -12,3 +12,12 @@ export function formField<Val = unknown>(item?: FormModelItem<Val>): FormModelIt
12
12
 
13
13
  return item
14
14
  }
15
+
16
+ export interface NestedFieldMarker<T extends Record<string, any> = Record<string, any>> {
17
+ __isNested: true
18
+ fields: T
19
+ }
20
+
21
+ export function nestField<T extends Record<string, any>>(fields: T): NestedFieldMarker<T> {
22
+ return { __isNested: true, fields }
23
+ }
@@ -1,6 +1,7 @@
1
1
  export { default as UForm } from './form.vue'
2
2
 
3
- export * from './helper'
3
+ export { formField, nestField } from './helper'
4
+ export type { NestedFieldMarker } from './helper'
4
5
 
5
6
  export { FormModel } from './form-model'
6
7
  export { DynamicFormModel } from './dynamic-form-model'
@@ -7,9 +7,7 @@ import { bem } from '@veltra/utils'
7
7
 
8
8
  import type { GanttChartProps } from '../../types'
9
9
 
10
- defineOptions({
11
- name: 'GanttChart'
12
- })
10
+ defineOptions({ name: 'GanttChart' })
13
11
 
14
12
  defineProps<GanttChartProps>()
15
13
 
@@ -11,13 +11,9 @@ import { computed, inject, type CSSProperties, watch } from 'vue'
11
11
  import type { GridItemProps } from '../../types'
12
12
  import { GridDIKey } from './di'
13
13
 
14
- defineOptions({
15
- name: 'GridItem'
16
- })
14
+ defineOptions({ name: 'GridItem' })
17
15
 
18
- const props = withDefaults(defineProps<GridItemProps>(), {
19
- tag: 'div'
20
- })
16
+ const props = withDefaults(defineProps<GridItemProps>(), { tag: 'div' })
21
17
 
22
18
  const cls = bem('grid-item')
23
19
 
@@ -41,9 +37,7 @@ const span = computed<number | 'full'>(() => {
41
37
  })
42
38
 
43
39
  const style = computed<CSSProperties>(() => {
44
- return {
45
- gridColumn: span.value === 'full' ? '1 / -1' : `span ${span.value}`
46
- }
40
+ return { gridColumn: span.value === 'full' ? '1 / -1' : `span ${span.value}` }
47
41
  })
48
42
 
49
43
  watch(
@@ -13,13 +13,9 @@ import { getBreakpointCols } from './breakpoint'
13
13
  import { GridDIKey } from './di'
14
14
  import { useResponsive } from './use-responsive'
15
15
 
16
- defineOptions({
17
- name: 'Grid'
18
- })
16
+ defineOptions({ name: 'Grid' })
19
17
 
20
- const props = withDefaults(defineProps<GridProps>(), {
21
- tag: 'div'
22
- })
18
+ const props = withDefaults(defineProps<GridProps>(), { tag: 'div' })
23
19
 
24
20
  const emit = defineEmits<GridEmits>()
25
21
 
@@ -27,11 +23,7 @@ const cls = bem('grid')
27
23
 
28
24
  const gridRef = useTemplateRef<HTMLElement>('grid')
29
25
 
30
- const { currentBreakpoint, gridItemsProps } = useResponsive({
31
- props,
32
- emit,
33
- gridRef
34
- })
26
+ const { currentBreakpoint, gridItemsProps } = useResponsive({ props, emit, gridRef })
35
27
 
36
28
  const styles = computed(() => {
37
29
  const { gap, cols } = props
@@ -77,12 +69,7 @@ watch([styles, gridRef], ([styles, dom]) => {
77
69
  setStyles(dom, styles)
78
70
  })
79
71
 
80
- provide(GridDIKey, {
81
- currentBreakpoint,
82
- gridItemsProps
83
- })
72
+ provide(GridDIKey, { currentBreakpoint, gridItemsProps })
84
73
 
85
- defineExpose<_GridExposed>({
86
- el: gridRef
87
- })
74
+ defineExpose<_GridExposed>({ el: gridRef })
88
75
  </script>
@@ -26,9 +26,7 @@ import { computed, ref, getCurrentInstance } from 'vue'
26
26
 
27
27
  import type { GridInputProps, GridInputEmits, GridInputExposed } from '../../types'
28
28
 
29
- defineOptions({
30
- name: 'GridInput'
31
- })
29
+ defineOptions({ name: 'GridInput' })
32
30
 
33
31
  const emit = defineEmits<GridInputEmits>()
34
32
 
@@ -38,9 +36,7 @@ const props = withDefaults(defineProps<GridInputProps>(), {
38
36
  separator: '-'
39
37
  })
40
38
 
41
- const { size } = useFallbackProps([props], {
42
- size: 'default'
43
- })
39
+ const { size } = useFallbackProps([props], { size: 'default' })
44
40
 
45
41
  const cls = bem('grid-input')
46
42
 
@@ -51,9 +51,7 @@ import type { GroupInputEmits, GroupInputProps } from '../../types'
51
51
  import { UButton } from '../button'
52
52
  import { useGroupItems } from './use-group-items'
53
53
 
54
- defineOptions({
55
- name: 'GroupInput'
56
- })
54
+ defineOptions({ name: 'GroupInput' })
57
55
 
58
56
  const props = withDefaults(defineProps<GroupInputProps<GroupItem>>(), {
59
57
  modelValue: () => [],
@@ -73,10 +71,7 @@ const { disabled, size, readonly } = useFormFallbackProps([formProps ?? {}, prop
73
71
  readonly: false
74
72
  })
75
73
 
76
- const { items, createItem, runByEvent } = useGroupItems({
77
- props,
78
- emit
79
- })
74
+ const { items, createItem, runByEvent } = useGroupItems({ props, emit })
80
75
 
81
76
  const isEmpty = computed(() => {
82
77
  return !items.value.length
@@ -10,17 +10,13 @@ import { type CSSProperties, computed } from 'vue'
10
10
 
11
11
  import type { IconProps } from '../../types'
12
12
 
13
- defineOptions({
14
- name: 'Icon'
15
- })
13
+ defineOptions({ name: 'Icon' })
16
14
 
17
15
  const props = defineProps<IconProps>()
18
16
 
19
17
  const cls = bem('icon')
20
18
 
21
19
  const style = computed<CSSProperties>(() => {
22
- return {
23
- fontSize: withUnit(props.size, 'px')
24
- }
20
+ return { fontSize: withUnit(props.size, 'px') }
25
21
  })
26
22
  </script>