@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.
- package/dist/components/batch-edit/batch-edit-form.js +2 -6
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.js +0 -1
- package/dist/components/batch-edit/batch-edit.js.map +1 -1
- package/dist/components/batch-edit/helper.d.ts +1 -2
- package/dist/components/condition-editor/core/evaluator.d.ts +4 -5
- package/dist/components/contextmenu/api.d.ts +1 -2
- package/dist/components/contextmenu/di.d.ts +2 -3
- package/dist/components/form/form.js.map +1 -1
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +12 -13
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form/use-node-interceptor.js +31 -3
- package/dist/components/form/use-node-interceptor.js.map +1 -1
- package/dist/components/form-item/form-item.js.map +1 -1
- package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js +1 -2
- package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/loading/directive.d.ts +1 -2
- package/dist/components/message/message.d.ts +1 -2
- package/dist/components/message-confirm/message-confirm.d.ts +1 -2
- package/dist/components/notification/notification.d.ts +1 -2
- package/dist/components/table/node/col.js +19 -0
- package/dist/components/table/node/col.js.map +1 -1
- package/dist/components/table/table-cell.js +2 -0
- package/dist/components/table/table-cell.js.map +1 -1
- package/dist/components/table/table-foot.js +2 -0
- package/dist/components/table/table-foot.js.map +1 -1
- package/dist/components/table/table-head.js +1 -0
- package/dist/components/table/table-head.js.map +1 -1
- package/dist/components/table/table-row.js +2 -0
- package/dist/components/table/table-row.js.map +1 -1
- package/dist/components/table/use-columns.d.ts +1 -2
- package/dist/index.d.ts +2 -2
- package/dist/install.d.ts +3 -3
- package/dist/types/action.d.ts +7 -8
- package/dist/types/animation.d.ts +4 -5
- package/dist/types/auto-complete.d.ts +4 -5
- package/dist/types/badge.d.ts +4 -5
- package/dist/types/batch-edit.d.ts +9 -12
- package/dist/types/breadcrumb.d.ts +6 -7
- package/dist/types/button.d.ts +5 -6
- package/dist/types/calendar.d.ts +7 -8
- package/dist/types/card.d.ts +7 -8
- package/dist/types/cascade.d.ts +6 -7
- package/dist/types/check-tag.d.ts +4 -5
- package/dist/types/checkbox-group.d.ts +3 -4
- package/dist/types/checkbox.d.ts +5 -6
- package/dist/types/code-editor.d.ts +5 -6
- package/dist/types/collapse.d.ts +6 -7
- package/dist/types/condition-editor.d.ts +11 -12
- package/dist/types/contextmenu.d.ts +5 -6
- package/dist/types/css-transition.d.ts +4 -5
- package/dist/types/date-panel.d.ts +3 -4
- package/dist/types/date-picker.d.ts +5 -6
- package/dist/types/date-range-picker.d.ts +6 -7
- package/dist/types/dialog.d.ts +5 -6
- package/dist/types/drawer.d.ts +6 -7
- package/dist/types/dropdown.d.ts +4 -5
- package/dist/types/dual-nav.d.ts +6 -7
- package/dist/types/empty.d.ts +4 -5
- package/dist/types/expression-editor.d.ts +6 -7
- package/dist/types/file-picker.d.ts +4 -5
- package/dist/types/file-viewer.d.ts +6 -7
- package/dist/types/float-button.d.ts +5 -6
- package/dist/types/form-item.d.ts +3 -4
- package/dist/types/form.d.ts +4 -7
- package/dist/types/gantt-chart.d.ts +4 -5
- package/dist/types/grid-input.d.ts +4 -5
- package/dist/types/grid.d.ts +7 -8
- package/dist/types/group-input.d.ts +4 -5
- package/dist/types/group-nav.d.ts +3 -4
- package/dist/types/icon.d.ts +3 -4
- package/dist/types/image-cropper.d.ts +9 -10
- package/dist/types/index.d.ts +2 -2
- package/dist/types/input.d.ts +4 -5
- package/dist/types/layout.d.ts +4 -5
- package/dist/types/list.d.ts +4 -5
- package/dist/types/loading.d.ts +5 -6
- package/dist/types/message-confirm.d.ts +8 -9
- package/dist/types/message.d.ts +8 -9
- package/dist/types/multi-auto-complete.d.ts +4 -5
- package/dist/types/multi-select.d.ts +4 -5
- package/dist/types/multi-tree-select.d.ts +4 -5
- package/dist/types/nav.d.ts +5 -6
- package/dist/types/node-render.d.ts +2 -3
- package/dist/types/notification.d.ts +8 -9
- package/dist/types/number-input.d.ts +3 -4
- package/dist/types/number-range-input.d.ts +4 -5
- package/dist/types/number.d.ts +3 -4
- package/dist/types/paginator.d.ts +4 -5
- package/dist/types/palette.d.ts +7 -8
- package/dist/types/password-input.d.ts +4 -5
- package/dist/types/pop-confirm.d.ts +4 -5
- package/dist/types/pop.d.ts +4 -5
- package/dist/types/progress-nodes.d.ts +4 -5
- package/dist/types/progress.d.ts +4 -5
- package/dist/types/quick-batch-edit.d.ts +4 -5
- package/dist/types/radio-group.d.ts +4 -5
- package/dist/types/radio.d.ts +4 -5
- package/dist/types/rich-text-editor.d.ts +6 -7
- package/dist/types/scroll.d.ts +5 -6
- package/dist/types/segment.d.ts +5 -6
- package/dist/types/select.d.ts +4 -5
- package/dist/types/slider.d.ts +4 -5
- package/dist/types/steps.d.ts +5 -6
- package/dist/types/switch.d.ts +4 -5
- package/dist/types/table-editor.d.ts +4 -5
- package/dist/types/table.d.ts +23 -13
- package/dist/types/tabs.d.ts +13 -14
- package/dist/types/tag.d.ts +3 -4
- package/dist/types/text-editor.d.ts +5 -6
- package/dist/types/text.d.ts +4 -5
- package/dist/types/textarea.d.ts +4 -5
- package/dist/types/theme.d.ts +3 -4
- package/dist/types/tip.d.ts +6 -7
- package/dist/types/tree-select.d.ts +4 -5
- package/dist/types/tree.d.ts +6 -6
- package/dist/types/watermark.d.ts +4 -5
- package/package.json +5 -5
- package/src/components/batch-edit/__test__/batch-edit.test.ts +0 -138
- package/src/components/batch-edit/batch-edit-form.vue +2 -16
- package/src/components/form/__test__/form.test.ts +125 -33
- package/src/components/form/form.vue +10 -11
- package/src/components/form/use-node-interceptor.ts +36 -2
- package/src/components/form-item/__test__/form-item.test.ts +1 -10
- package/src/components/form-item/form-item.vue +2 -9
- package/src/components/table/__test__/column-style.test.ts +67 -0
- package/src/components/table/node/col.ts +30 -1
- package/src/components/table/table-cell.vue +7 -2
- package/src/components/table/table-foot.vue +2 -0
- package/src/components/table/table-head.tsx +1 -0
- package/src/components/table/table-row.tsx +2 -0
- package/src/types/batch-edit.ts +0 -2
- package/src/types/form.ts +0 -2
- 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.
|
|
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.
|
|
84
|
-
"@veltra/directives": "^1.7.
|
|
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.
|
|
88
|
-
"@veltra/utils": "^1.7.
|
|
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
|
-
|
|
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
|
|
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="
|
|
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
|
|
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
|
-
|
|
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
|
+
})
|