@veltra/desktop 1.7.11 → 1.7.13

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 (134) hide show
  1. package/dist/components/batch-edit/batch-edit-form.js +2 -6
  2. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  3. package/dist/components/batch-edit/batch-edit.js +0 -1
  4. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  5. package/dist/components/batch-edit/helper.d.ts +1 -2
  6. package/dist/components/condition-editor/core/evaluator.d.ts +4 -5
  7. package/dist/components/contextmenu/api.d.ts +1 -2
  8. package/dist/components/contextmenu/di.d.ts +2 -3
  9. package/dist/components/form/form.js.map +1 -1
  10. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +12 -13
  11. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
  12. package/dist/components/form/use-node-interceptor.js +31 -3
  13. package/dist/components/form/use-node-interceptor.js.map +1 -1
  14. package/dist/components/form-item/form-item.js.map +1 -1
  15. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js +1 -2
  16. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
  17. package/dist/components/loading/directive.d.ts +1 -2
  18. package/dist/components/message/message.d.ts +1 -2
  19. package/dist/components/message-confirm/message-confirm.d.ts +1 -2
  20. package/dist/components/notification/notification.d.ts +1 -2
  21. package/dist/components/table/node/col.js +19 -0
  22. package/dist/components/table/node/col.js.map +1 -1
  23. package/dist/components/table/table-cell.js +2 -0
  24. package/dist/components/table/table-cell.js.map +1 -1
  25. package/dist/components/table/table-foot.js +2 -0
  26. package/dist/components/table/table-foot.js.map +1 -1
  27. package/dist/components/table/table-head.js +1 -0
  28. package/dist/components/table/table-head.js.map +1 -1
  29. package/dist/components/table/table-row.js +2 -0
  30. package/dist/components/table/table-row.js.map +1 -1
  31. package/dist/components/table/use-columns.d.ts +1 -2
  32. package/dist/index.d.ts +2 -2
  33. package/dist/install.d.ts +3 -3
  34. package/dist/types/action.d.ts +7 -8
  35. package/dist/types/animation.d.ts +4 -5
  36. package/dist/types/auto-complete.d.ts +4 -5
  37. package/dist/types/badge.d.ts +4 -5
  38. package/dist/types/batch-edit.d.ts +9 -12
  39. package/dist/types/breadcrumb.d.ts +6 -7
  40. package/dist/types/button.d.ts +5 -6
  41. package/dist/types/calendar.d.ts +7 -8
  42. package/dist/types/card.d.ts +7 -8
  43. package/dist/types/cascade.d.ts +6 -7
  44. package/dist/types/check-tag.d.ts +4 -5
  45. package/dist/types/checkbox-group.d.ts +3 -4
  46. package/dist/types/checkbox.d.ts +5 -6
  47. package/dist/types/code-editor.d.ts +5 -6
  48. package/dist/types/collapse.d.ts +6 -7
  49. package/dist/types/condition-editor.d.ts +11 -12
  50. package/dist/types/contextmenu.d.ts +5 -6
  51. package/dist/types/css-transition.d.ts +4 -5
  52. package/dist/types/date-panel.d.ts +3 -4
  53. package/dist/types/date-picker.d.ts +5 -6
  54. package/dist/types/date-range-picker.d.ts +6 -7
  55. package/dist/types/dialog.d.ts +5 -6
  56. package/dist/types/drawer.d.ts +6 -7
  57. package/dist/types/dropdown.d.ts +4 -5
  58. package/dist/types/dual-nav.d.ts +6 -7
  59. package/dist/types/empty.d.ts +4 -5
  60. package/dist/types/expression-editor.d.ts +6 -7
  61. package/dist/types/file-picker.d.ts +4 -5
  62. package/dist/types/file-viewer.d.ts +6 -7
  63. package/dist/types/float-button.d.ts +5 -6
  64. package/dist/types/form-item.d.ts +3 -4
  65. package/dist/types/form.d.ts +4 -7
  66. package/dist/types/gantt-chart.d.ts +4 -5
  67. package/dist/types/grid-input.d.ts +4 -5
  68. package/dist/types/grid.d.ts +7 -8
  69. package/dist/types/group-input.d.ts +4 -5
  70. package/dist/types/group-nav.d.ts +3 -4
  71. package/dist/types/icon.d.ts +3 -4
  72. package/dist/types/image-cropper.d.ts +9 -10
  73. package/dist/types/index.d.ts +2 -2
  74. package/dist/types/input.d.ts +4 -5
  75. package/dist/types/layout.d.ts +4 -5
  76. package/dist/types/list.d.ts +4 -5
  77. package/dist/types/loading.d.ts +5 -6
  78. package/dist/types/message-confirm.d.ts +8 -9
  79. package/dist/types/message.d.ts +8 -9
  80. package/dist/types/multi-auto-complete.d.ts +4 -5
  81. package/dist/types/multi-select.d.ts +4 -5
  82. package/dist/types/multi-tree-select.d.ts +4 -5
  83. package/dist/types/nav.d.ts +5 -6
  84. package/dist/types/node-render.d.ts +2 -3
  85. package/dist/types/notification.d.ts +8 -9
  86. package/dist/types/number-input.d.ts +3 -4
  87. package/dist/types/number-range-input.d.ts +4 -5
  88. package/dist/types/number.d.ts +3 -4
  89. package/dist/types/paginator.d.ts +4 -5
  90. package/dist/types/palette.d.ts +7 -8
  91. package/dist/types/password-input.d.ts +4 -5
  92. package/dist/types/pop-confirm.d.ts +4 -5
  93. package/dist/types/pop.d.ts +4 -5
  94. package/dist/types/progress-nodes.d.ts +4 -5
  95. package/dist/types/progress.d.ts +4 -5
  96. package/dist/types/quick-batch-edit.d.ts +4 -5
  97. package/dist/types/radio-group.d.ts +4 -5
  98. package/dist/types/radio.d.ts +4 -5
  99. package/dist/types/rich-text-editor.d.ts +6 -7
  100. package/dist/types/scroll.d.ts +5 -6
  101. package/dist/types/segment.d.ts +5 -6
  102. package/dist/types/select.d.ts +4 -5
  103. package/dist/types/slider.d.ts +4 -5
  104. package/dist/types/steps.d.ts +5 -6
  105. package/dist/types/switch.d.ts +4 -5
  106. package/dist/types/table-editor.d.ts +4 -5
  107. package/dist/types/table.d.ts +23 -13
  108. package/dist/types/tabs.d.ts +13 -14
  109. package/dist/types/tag.d.ts +3 -4
  110. package/dist/types/text-editor.d.ts +5 -6
  111. package/dist/types/text.d.ts +4 -5
  112. package/dist/types/textarea.d.ts +4 -5
  113. package/dist/types/theme.d.ts +3 -4
  114. package/dist/types/tip.d.ts +6 -7
  115. package/dist/types/tree-select.d.ts +4 -5
  116. package/dist/types/tree.d.ts +6 -6
  117. package/dist/types/watermark.d.ts +4 -5
  118. package/package.json +5 -5
  119. package/src/components/batch-edit/__test__/batch-edit.test.ts +0 -138
  120. package/src/components/batch-edit/batch-edit-form.vue +2 -16
  121. package/src/components/form/__test__/form.test.ts +125 -33
  122. package/src/components/form/form.vue +10 -11
  123. package/src/components/form/use-node-interceptor.ts +36 -2
  124. package/src/components/form-item/__test__/form-item.test.ts +1 -10
  125. package/src/components/form-item/form-item.vue +2 -9
  126. package/src/components/table/__test__/column-style.test.ts +67 -0
  127. package/src/components/table/node/col.ts +30 -1
  128. package/src/components/table/table-cell.vue +7 -2
  129. package/src/components/table/table-foot.vue +2 -0
  130. package/src/components/table/table-head.tsx +1 -0
  131. package/src/components/table/table-row.tsx +2 -0
  132. package/src/types/batch-edit.ts +0 -2
  133. package/src/types/form.ts +0 -2
  134. package/src/types/table.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veltra/desktop",
3
- "version": "1.7.11",
3
+ "version": "1.7.13",
4
4
  "homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/cabinet-fe/ultra-ui/issues"
@@ -80,12 +80,12 @@
80
80
  "peerDependencies": {
81
81
  "@cat-kit/core": ">=1.2.1",
82
82
  "@cat-kit/fe": ">=1.2.1",
83
- "@veltra/compositions": "^1.7.11",
84
- "@veltra/directives": "^1.7.11",
83
+ "@veltra/compositions": "^1.7.13",
84
+ "@veltra/directives": "^1.7.13",
85
85
  "@veltra/icons": "^1.5.0",
86
86
  "@veltra/sheet-core": "^2.5.6",
87
- "@veltra/styles": "^1.7.11",
88
- "@veltra/utils": "^1.7.11",
87
+ "@veltra/styles": "^1.7.13",
88
+ "@veltra/utils": "^1.7.13",
89
89
  "vue": ">=3.5.42"
90
90
  },
91
91
  "peerDependenciesMeta": {
@@ -30,8 +30,6 @@ function mountBatchEdit() {
30
30
  apiMethod: undefined as string | undefined
31
31
  })
32
32
 
33
- const fieldChanges: { field: string; args: unknown[] }[] = []
34
-
35
33
  const app = createApp({
36
34
  render() {
37
35
  return h(
@@ -41,9 +39,6 @@ function mountBatchEdit() {
41
39
  'onUpdate:data': (value: any[]) => {
42
40
  data.value = value
43
41
  },
44
- 'onField:change': (field: string, ...args: unknown[]) => {
45
- fieldChanges.push({ field, args })
46
- },
47
42
  columns,
48
43
  model,
49
44
  rowKey: 'id',
@@ -94,7 +89,6 @@ function mountBatchEdit() {
94
89
  host,
95
90
  data,
96
91
  model,
97
- fieldChanges,
98
92
  unmount() {
99
93
  app.unmount()
100
94
  host.remove()
@@ -202,136 +196,4 @@ describe('UBatchEdit quick-edit 切换编辑行', () => {
202
196
 
203
197
  unmount()
204
198
  })
205
-
206
- it('切换行回显不触发 field:change', async () => {
207
- const { host, fieldChanges, unmount } = mountBatchEdit()
208
-
209
- fieldChanges.length = 0
210
- await clickRow(host, 0)
211
- await clickRow(host, 1)
212
-
213
- expect(fieldChanges).toHaveLength(0)
214
-
215
- unmount()
216
- })
217
-
218
- it('用户变更 Select 触发 field:change', async () => {
219
- const { host, fieldChanges, unmount } = mountBatchEdit()
220
-
221
- await clickRow(host, 0)
222
- fieldChanges.length = 0
223
-
224
- const input = host.querySelector<HTMLInputElement>('.u-select input')
225
- expect(input).toBeTruthy()
226
- input!.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
227
- await nextTick()
228
- await nextTick()
229
-
230
- const option = [...document.body.querySelectorAll<HTMLElement>('.u-select__option')].find(
231
- (el) => el.textContent === '接口'
232
- )
233
- expect(option).toBeTruthy()
234
- option!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
235
- await nextTick()
236
- await nextTick()
237
-
238
- expect(fieldChanges).toHaveLength(1)
239
- expect(fieldChanges[0]).toEqual({ field: 'behavior', args: [{ label: '接口', value: 'api' }] })
240
-
241
- unmount()
242
- })
243
-
244
- it('field:change 联动改其他字段不循环触发', async () => {
245
- const host = document.createElement('div')
246
- document.body.appendChild(host)
247
-
248
- const columns = defineTableColumns([
249
- { key: 'label', name: '名称' },
250
- { key: 'behavior', name: '行为' }
251
- ])
252
-
253
- const data = ref([
254
- { id: '1', label: '存草稿', behavior: 'common-resource', submitType: 'DRAFT' },
255
- { id: '2', label: '调接口', behavior: 'api', submitType: 'SUBMIT', apiMethod: 'POST' }
256
- ])
257
-
258
- const model = reactive({
259
- id: '',
260
- label: '',
261
- behavior: 'event',
262
- submitType: undefined as string | undefined,
263
- apiMethod: undefined as string | undefined
264
- })
265
-
266
- const fieldChanges: string[] = []
267
-
268
- const app = createApp({
269
- render() {
270
- return h(
271
- UBatchEdit,
272
- {
273
- data: data.value,
274
- 'onUpdate:data': (value: any[]) => {
275
- data.value = value
276
- },
277
- 'onField:change': (field: string) => {
278
- fieldChanges.push(field)
279
- if (field === 'behavior') {
280
- model.submitType = undefined
281
- }
282
- },
283
- columns,
284
- model,
285
- rowKey: 'id',
286
- quickEdit: true
287
- },
288
- {
289
- form: () => [
290
- h(UInput, { field: 'label', label: '名称' }),
291
- h(USelect, {
292
- field: 'behavior',
293
- label: '行为',
294
- options: [
295
- { label: '事件', value: 'event' },
296
- { label: '通用资源', value: 'common-resource' },
297
- { label: '接口', value: 'api' }
298
- ]
299
- }),
300
- h(USelect, {
301
- field: 'submitType',
302
- label: '提交类型',
303
- options: [
304
- { label: 'DRAFT', value: 'DRAFT' },
305
- { label: 'SUBMIT', value: 'SUBMIT' }
306
- ]
307
- })
308
- ]
309
- }
310
- )
311
- }
312
- })
313
-
314
- app.mount(host)
315
-
316
- await clickRow(host, 0)
317
- fieldChanges.length = 0
318
-
319
- const input = host.querySelector<HTMLInputElement>('.u-select input')
320
- input!.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
321
- await nextTick()
322
- await nextTick()
323
-
324
- const option = [...document.body.querySelectorAll<HTMLElement>('.u-select__option')].find(
325
- (el) => el.textContent === '接口'
326
- )
327
- option!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
328
- await nextTick()
329
- await nextTick()
330
-
331
- expect(fieldChanges).toEqual(['behavior'])
332
- expect(model.submitType).toBeUndefined()
333
-
334
- app.unmount()
335
- host.remove()
336
- })
337
199
  })
@@ -29,7 +29,6 @@
29
29
  :readonly="props.readonly"
30
30
  :label-width="props.labelWidth"
31
31
  @field:update="handleFieldUpdate"
32
- @field:change="handleFormFieldChange"
33
32
  >
34
33
  <slot
35
34
  v-bind="{
@@ -92,17 +91,8 @@ defineOptions({ name: 'UBatchEditForm' })
92
91
 
93
92
  const batchEditCtx = inject(BatchEditDIKey)!
94
93
 
95
- const {
96
- cls,
97
- props,
98
- state,
99
- emit,
100
- handleClose,
101
- handleSave,
102
- staticFeatures,
103
- dynamicFeatures,
104
- syncing
105
- } = batchEditCtx
94
+ const { cls, props, state, handleClose, handleSave, staticFeatures, dynamicFeatures, syncing } =
95
+ batchEditCtx
106
96
 
107
97
  const focused = toRef(batchEditCtx, 'focused')
108
98
 
@@ -124,10 +114,6 @@ function handleFieldUpdate(field: string, value: any) {
124
114
  }
125
115
  }
126
116
 
127
- function handleFormFieldChange(field: string, ...args: any[]) {
128
- emit('field:change', field, ...args)
129
- }
130
-
131
117
  const creatable = computed(() => {
132
118
  if (state.formActionType !== 'create') return false
133
119
  return staticFeatures.value.has('create') || dynamicFeatures.value.create?.(state.row)
@@ -1,42 +1,22 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { createApp, h, nextTick, reactive, ref } from 'vue'
2
+ import { createApp, defineComponent, h, nextTick, reactive, ref } from 'vue'
3
3
 
4
4
  import { UInput } from '../../input'
5
5
  import { USelect } from '../../select'
6
6
  import { UForm } from '../index'
7
7
 
8
- const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
9
-
10
8
  const behaviorOptions = [
11
9
  { label: '事件', value: 'event' },
12
10
  { label: '接口', value: 'api' }
13
11
  ]
14
12
 
15
- async function openSelect(host: HTMLElement) {
16
- const input = host.querySelector('input')!
17
- input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
18
- await nextTick()
19
- await nextTick()
20
- }
21
-
22
- async function clickSelectOption(label: string) {
23
- const option = [...document.body.querySelectorAll<HTMLElement>('.u-select__option')].find(
24
- (el) => el.textContent === label
25
- )
26
- expect(option, `选项「${label}」应存在`).toBeTruthy()
27
- option!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
28
- await nextTick()
29
- await sleep(50)
30
- }
31
-
32
13
  describe('UForm field events', () => {
33
- it('field:update and field:change are separate', async () => {
14
+ it('编程写入触发 field:update,不依赖用户操作', async () => {
34
15
  const host = document.createElement('div')
35
16
  document.body.appendChild(host)
36
17
 
37
18
  const model = reactive({ behavior: 'event' })
38
19
  const updates: { field: string; value: unknown }[] = []
39
- const changes: { field: string; args: unknown[] }[] = []
40
20
 
41
21
  const app = createApp({
42
22
  render() {
@@ -46,9 +26,6 @@ describe('UForm field events', () => {
46
26
  model,
47
27
  'onField:update': (field: string, value: unknown) => {
48
28
  updates.push({ field, value })
49
- },
50
- 'onField:change': (field: string, ...args: unknown[]) => {
51
- changes.push({ field, args })
52
29
  }
53
30
  },
54
31
  {
@@ -61,19 +38,11 @@ describe('UForm field events', () => {
61
38
 
62
39
  app.mount(host)
63
40
  updates.length = 0
64
- changes.length = 0
65
41
 
66
42
  model.behavior = 'api'
67
43
  await nextTick()
68
44
 
69
45
  expect(updates.some((item) => item.field === 'behavior' && item.value === 'api')).toBe(true)
70
- expect(changes).toHaveLength(0)
71
-
72
- await openSelect(host)
73
- await clickSelectOption('事件')
74
-
75
- expect(changes).toHaveLength(1)
76
- expect(changes[0]).toEqual({ field: 'behavior', args: [{ label: '事件', value: 'event' }] })
77
46
 
78
47
  app.unmount()
79
48
  host.remove()
@@ -122,3 +91,126 @@ describe('UForm field events', () => {
122
91
  host.remove()
123
92
  })
124
93
  })
94
+
95
+ /** 用 setup 期间生成的 id 断言实例身份 */
96
+ let probeSeq = 0
97
+ const Probe = defineComponent({
98
+ name: 'Probe',
99
+ props: { field: String, label: String, modelValue: String },
100
+ setup(props) {
101
+ const id = `p${++probeSeq}`
102
+ return () =>
103
+ h('div', { class: 'probe', 'data-field': props.field, 'data-id': id }, [
104
+ h('span', { class: 'probe-value' }, props.modelValue ?? '')
105
+ ])
106
+ }
107
+ })
108
+
109
+ describe('UForm slot identity', () => {
110
+ it('中间字段卸载后不复用相邻控件实例', async () => {
111
+ probeSeq = 0
112
+ const host = document.createElement('div')
113
+ document.body.appendChild(host)
114
+
115
+ const model = reactive({ a: 'A', b: 'B', c: 'C' })
116
+ const showB = ref(true)
117
+
118
+ const app = createApp({
119
+ render() {
120
+ return h(
121
+ UForm,
122
+ { model },
123
+ {
124
+ default: () => [
125
+ h(Probe, { field: 'a', label: 'A' }),
126
+ showB.value ? h(Probe, { field: 'b', label: 'B' }) : null,
127
+ h(Probe, { field: 'c', label: 'C' })
128
+ ]
129
+ }
130
+ )
131
+ }
132
+ })
133
+
134
+ app.mount(host)
135
+
136
+ const idOf = (field: string) =>
137
+ host.querySelector(`[data-field="${field}"]`)?.getAttribute('data-id')
138
+
139
+ const cId = idOf('c')
140
+ expect(cId).toBe('p3')
141
+
142
+ showB.value = false
143
+ await nextTick()
144
+
145
+ expect(host.querySelector('[data-field="b"]')).toBeNull()
146
+ expect(idOf('c')).toBe(cId)
147
+ expect(host.querySelector('[data-field="c"] .probe-value')!.textContent).toBe('C')
148
+
149
+ app.unmount()
150
+ host.remove()
151
+ })
152
+
153
+ it('重复 field 各自保有独立实例', async () => {
154
+ probeSeq = 0
155
+ const host = document.createElement('div')
156
+ document.body.appendChild(host)
157
+
158
+ const model = reactive({ name: '同名' })
159
+
160
+ const app = createApp({
161
+ render() {
162
+ return h(
163
+ UForm,
164
+ { model },
165
+ {
166
+ default: () => [
167
+ h(Probe, { field: 'name', label: '左' }),
168
+ h(Probe, { field: 'name', label: '右' })
169
+ ]
170
+ }
171
+ )
172
+ }
173
+ })
174
+
175
+ app.mount(host)
176
+
177
+ const probes = [...host.querySelectorAll('.probe')]
178
+ expect(probes).toHaveLength(2)
179
+ expect(probes[0]!.getAttribute('data-id')).toBe('p1')
180
+ expect(probes[1]!.getAttribute('data-id')).toBe('p2')
181
+ expect(probes[0]!.querySelector('.probe-value')!.textContent).toBe('同名')
182
+ expect(probes[1]!.querySelector('.probe-value')!.textContent).toBe('同名')
183
+
184
+ app.unmount()
185
+ host.remove()
186
+ })
187
+
188
+ it('showModified 变更前副本不抢走编辑控件', async () => {
189
+ const host = document.createElement('div')
190
+ document.body.appendChild(host)
191
+
192
+ const model = reactive({ name: '新值' })
193
+ const initialModel = { name: '旧值' }
194
+
195
+ const app = createApp({
196
+ render() {
197
+ return h(
198
+ UForm,
199
+ { model, initialModel, showModified: true },
200
+ { default: () => h(UInput, { field: 'name', label: '名称' }) }
201
+ )
202
+ }
203
+ })
204
+
205
+ app.mount(host)
206
+ await nextTick()
207
+
208
+ const live = host.querySelector('input') as HTMLInputElement | null
209
+ expect(live).toBeTruthy()
210
+ expect(live!.value).toBe('新值')
211
+ expect(host.querySelector('.u-form__data-before')?.textContent).toContain('旧值')
212
+
213
+ app.unmount()
214
+ host.remove()
215
+ })
216
+ })
@@ -8,8 +8,8 @@
8
8
  :class="[cls.b, bem.is('readonly', readonly)]"
9
9
  >
10
10
  <template
11
- v-for="{ node, isFormItem, formItemProps, field, modelValue } of getSlotsNodes()"
12
- :key="node.key"
11
+ v-for="{ node, isFormItem, formItemProps, field, modelValue, renderKey } of getSlotsNodes()"
12
+ :key="renderKey"
13
13
  >
14
14
  <u-form-item v-if="!isFormItem && field" v-bind="formItemProps">
15
15
  <component
@@ -19,7 +19,7 @@
19
19
  />
20
20
  <div v-if="shouldShowModified(field)" :class="cls.e('data-before')">
21
21
  <span :class="cls.e('changed-tag')">{{ modifiedLabel }}</span>
22
- <component :is="node" readonly :model-value="getBaselineFieldValue(field)" />
22
+ <component :is="createModifiedPreview(node, field)" />
23
23
  </div>
24
24
  </u-form-item>
25
25
 
@@ -32,14 +32,14 @@
32
32
  import { o } from '@cat-kit/core'
33
33
  import { bem } from '@veltra/utils'
34
34
  import { provideFormContext } from '@veltra/utils'
35
- import { nextTick, toRef, useTemplateRef } from 'vue'
35
+ import { nextTick, toRef, useTemplateRef, type VNode } from 'vue'
36
36
 
37
37
  import type { BreakCols, FormProps, _FormExposed, FormEmits } from '../../types'
38
38
  import { UFormItem } from '../form-item'
39
39
  import { UGrid } from '../grid'
40
40
  import { isFieldModified } from './is-field-modified'
41
41
  import { useFormFields } from './use-form-fields'
42
- import { useNodeInterceptor } from './use-node-interceptor'
42
+ import { forkUnmountedVNode, useNodeInterceptor } from './use-node-interceptor'
43
43
 
44
44
  defineOptions({ name: 'UForm' })
45
45
 
@@ -81,18 +81,13 @@ function handleFieldUpdate(field: string, value: any) {
81
81
  emit('field:update', field, value)
82
82
  }
83
83
 
84
- function handleFieldChange(field: string, ...args: any[]) {
85
- emit('field:change', field, ...args)
86
- }
87
-
88
84
  provideFormContext({
89
85
  formProps: props,
90
86
  registerField,
91
87
  unregisterField,
92
88
  validateFields: validate,
93
89
  shouldValidate,
94
- handleFieldUpdate,
95
- handleFieldChange
90
+ handleFieldUpdate
96
91
  })
97
92
 
98
93
  const { getSlotsNodes } = useNodeInterceptor()
@@ -106,6 +101,10 @@ function getBaselineFieldValue(field: string) {
106
101
  return o(getBaselineModel() ?? {}).get(field)
107
102
  }
108
103
 
104
+ function createModifiedPreview(node: VNode, field: string) {
105
+ return forkUnmountedVNode(node, { readonly: true, modelValue: getBaselineFieldValue(field) })
106
+ }
107
+
109
108
  function shouldShowModified(field: string) {
110
109
  if (!props.showModified) return false
111
110
  const baseline = getBaselineModel()
@@ -1,6 +1,6 @@
1
1
  import { o } from '@cat-kit/core'
2
2
  import { extractNormalVNodes } from '@veltra/utils'
3
- import { useSlots, type VNode } from 'vue'
3
+ import { cloneVNode, useSlots, type VNode } from 'vue'
4
4
 
5
5
  export type SlotRenderItem = {
6
6
  isFormItem: boolean
@@ -8,6 +8,28 @@ export type SlotRenderItem = {
8
8
  node: VNode
9
9
  field?: string
10
10
  modelValue?: any
11
+ /** v-for 身份:调用方 :key 优先,否则 type+field+出现次序 */
12
+ renderKey: string | number | symbol
13
+ }
14
+
15
+ function getVNodeTypeName(node: VNode): string {
16
+ const type = node.type as { name?: string; __name?: string } | string
17
+ if (typeof type === 'string') return type
18
+ if (typeof type === 'object' && type) return type.name || type.__name || ''
19
+ return ''
20
+ }
21
+
22
+ /**
23
+ * 从源 vnode 派生一份未挂载副本。
24
+ * Vue 的 cloneVNode 会拷贝 el/component,同一源 vnode 渲两次会把实例从编辑控件抢走。
25
+ */
26
+ export function forkUnmountedVNode(node: VNode, extra?: Record<string, unknown>): VNode {
27
+ const forked = extra ? cloneVNode(node, extra) : cloneVNode(node)
28
+ forked.el = null
29
+ forked.component = null
30
+ forked.anchor = null
31
+ forked.suspense = null
32
+ return forked
11
33
  }
12
34
 
13
35
  /**
@@ -24,6 +46,7 @@ export function useNodeInterceptor(): { getSlotsNodes: () => SlotRenderItem[] |
24
46
  const flattedNodes = extractNormalVNodes(nodes)
25
47
 
26
48
  const results: SlotRenderItem[] = []
49
+ const seen = new Map<string, number>()
27
50
 
28
51
  let i = 0
29
52
  while (i < flattedNodes.length) {
@@ -43,12 +66,23 @@ export function useNodeInterceptor(): { getSlotsNodes: () => SlotRenderItem[] |
43
66
  'field'
44
67
  ]) as Record<string, any>
45
68
 
69
+ let renderKey: string | number | symbol
70
+ if (node.key != null) {
71
+ renderKey = node.key
72
+ } else {
73
+ const base = `${getVNodeTypeName(node)}:${field ?? ''}`
74
+ const n = seen.get(base) ?? 0
75
+ seen.set(base, n + 1)
76
+ renderKey = n === 0 ? `uform:${base}` : `uform:${base}#${n}`
77
+ }
78
+
46
79
  results.push({
47
80
  isFormItem,
48
81
  formItemProps,
49
82
  node,
50
83
  field,
51
- modelValue: props?.['model-value'] ?? props?.modelValue
84
+ modelValue: props?.['model-value'] ?? props?.modelValue,
85
+ renderKey
52
86
  })
53
87
  }
54
88
 
@@ -37,18 +37,12 @@ describe('UFormItem change', () => {
37
37
 
38
38
  const model = reactive({ behavior: 'event' })
39
39
  const itemChanges: unknown[][] = []
40
- const formChanges: { field: string; args: unknown[] }[] = []
41
40
 
42
41
  const app = createApp({
43
42
  render() {
44
43
  return h(
45
44
  UForm,
46
- {
47
- model,
48
- 'onField:change': (field: string, ...args: unknown[]) => {
49
- formChanges.push({ field, args })
50
- }
51
- },
45
+ { model },
52
46
  {
53
47
  default: () =>
54
48
  h(
@@ -82,9 +76,6 @@ describe('UFormItem change', () => {
82
76
  expect(itemChanges).toHaveLength(1)
83
77
  expect(itemChanges[0]![0]).toEqual({ label: '接口', value: 'api' })
84
78
 
85
- expect(formChanges).toHaveLength(1)
86
- expect(formChanges[0]).toEqual({ field: 'behavior', args: [{ label: '接口', value: 'api' }] })
87
-
88
79
  app.unmount()
89
80
  host.remove()
90
81
  })
@@ -69,14 +69,8 @@ defineSlots<{
69
69
  }>()
70
70
 
71
71
  /** 表单组件上下文 */
72
- const {
73
- formProps,
74
- registerField,
75
- unregisterField,
76
- shouldValidate,
77
- handleFieldUpdate,
78
- handleFieldChange
79
- } = injectFormContext()
72
+ const { formProps, registerField, unregisterField, shouldValidate, handleFieldUpdate } =
73
+ injectFormContext()
80
74
 
81
75
  function wrapControlChange(node: VNode) {
82
76
  if (!node || typeof node.type === 'symbol') return node
@@ -84,7 +78,6 @@ function wrapControlChange(node: VNode) {
84
78
  return cloneVNode(node, {
85
79
  onChange: (...args: any[]) => {
86
80
  emit('change', ...args)
87
- if (props.field) handleFieldChange?.(props.field, ...args)
88
81
  }
89
82
  })
90
83
  }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { ColumnNode } from '../node/col'
4
+
5
+ describe('ColumnNode column styles', () => {
6
+ it('headerAlign 未指定时回退到 align', () => {
7
+ const column = new ColumnNode({ key: 'a', name: 'A', align: 'center' }, 0, 0)
8
+
9
+ expect(column.headerAlign).toBe('center')
10
+ })
11
+
12
+ it('headerAlign 显式指定时优先于 align', () => {
13
+ const column = new ColumnNode(
14
+ { key: 'a', name: 'A', align: 'center', headerAlign: 'right' },
15
+ 0,
16
+ 0
17
+ )
18
+
19
+ expect(column.headerAlign).toBe('right')
20
+ })
21
+
22
+ it('headerCellStyle 未指定 headerStyle 时回退到 style', () => {
23
+ const column = new ColumnNode(
24
+ { key: 'a', name: 'A', style: { color: 'red', fontSize: 14 } },
25
+ 0,
26
+ 0
27
+ )
28
+
29
+ expect(column.cellStyle).toEqual({ color: 'red', fontSize: '14px' })
30
+ expect(column.headerCellStyle).toEqual({ color: 'red', fontSize: '14px' })
31
+ })
32
+
33
+ it('headerCellStyle 显式指定 headerStyle 时优先于 style', () => {
34
+ const column = new ColumnNode(
35
+ {
36
+ key: 'a',
37
+ name: 'A',
38
+ style: { color: 'red', fontSize: 12 },
39
+ headerStyle: { color: 'blue', fontSize: 16 }
40
+ },
41
+ 0,
42
+ 0
43
+ )
44
+
45
+ expect(column.cellStyle).toEqual({ color: 'red', fontSize: '12px' })
46
+ expect(column.headerCellStyle).toEqual({ color: 'blue', fontSize: '16px' })
47
+ })
48
+
49
+ it('fontSize 数字转为 px,字符串原样保留', () => {
50
+ const column = new ColumnNode({ key: 'a', name: 'A', style: { fontSize: 18 } }, 0, 0)
51
+ const columnWithUnit = new ColumnNode(
52
+ { key: 'b', name: 'B', style: { fontSize: '1.2rem' } },
53
+ 0,
54
+ 0
55
+ )
56
+
57
+ expect(column.cellStyle).toEqual({ fontSize: '18px' })
58
+ expect(columnWithUnit.cellStyle).toEqual({ fontSize: '1.2rem' })
59
+ })
60
+
61
+ it('未配置样式时 getter 返回 undefined', () => {
62
+ const column = new ColumnNode({ key: 'a', name: 'A' }, 0, 0)
63
+
64
+ expect(column.cellStyle).toBeUndefined()
65
+ expect(column.headerCellStyle).toBeUndefined()
66
+ })
67
+ })