@veltra/desktop 1.7.9 → 1.7.10
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 +7 -3
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.js +1 -0
- package/dist/components/batch-edit/batch-edit.js.map +1 -1
- package/dist/components/batch-edit/use-edit-state.js.map +1 -1
- package/dist/components/form/form.js.map +1 -1
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +31 -6
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form/is-field-modified.js +12 -0
- package/dist/components/form/is-field-modified.js.map +1 -0
- package/dist/components/form/style2.css +37 -1
- package/dist/components/form/use-form-fields.js +11 -6
- package/dist/components/form/use-form-fields.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 +18 -5
- package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/types/batch-edit.d.ts +2 -0
- package/dist/types/form-item.d.ts +4 -1
- package/dist/types/form.d.ts +10 -1
- package/package.json +6 -6
- package/src/components/batch-edit/__test__/batch-edit.test.ts +138 -0
- package/src/components/batch-edit/batch-edit-form.vue +18 -4
- package/src/components/batch-edit/use-edit-state.ts +1 -1
- package/src/components/form/__test__/form.test.ts +80 -0
- package/src/components/form/__test__/is-field-modified.test.ts +37 -0
- package/src/components/form/form.vue +28 -4
- package/src/components/form/is-field-modified.ts +16 -0
- package/src/components/form/style.scss +53 -2
- package/src/components/form/use-form-fields.ts +20 -6
- package/src/components/form-item/__test__/form-item.test.ts +90 -0
- package/src/components/form-item/form-item.vue +42 -7
- package/src/types/batch-edit.ts +2 -0
- package/src/types/form-item.ts +4 -1
- package/src/types/form.ts +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veltra/desktop",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.10",
|
|
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.10",
|
|
84
|
+
"@veltra/directives": "^1.7.10",
|
|
85
85
|
"@veltra/icons": "^1.5.0",
|
|
86
|
-
"@veltra/sheet-core": "^2.5.
|
|
87
|
-
"@veltra/styles": "^1.7.
|
|
88
|
-
"@veltra/utils": "^1.7.
|
|
86
|
+
"@veltra/sheet-core": "^2.5.6",
|
|
87
|
+
"@veltra/styles": "^1.7.10",
|
|
88
|
+
"@veltra/utils": "^1.7.10",
|
|
89
89
|
"vue": ">=3.5.42"
|
|
90
90
|
},
|
|
91
91
|
"peerDependenciesMeta": {
|
|
@@ -30,6 +30,8 @@ function mountBatchEdit() {
|
|
|
30
30
|
apiMethod: undefined as string | undefined
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
+
const fieldChanges: { field: string; args: unknown[] }[] = []
|
|
34
|
+
|
|
33
35
|
const app = createApp({
|
|
34
36
|
render() {
|
|
35
37
|
return h(
|
|
@@ -39,6 +41,9 @@ function mountBatchEdit() {
|
|
|
39
41
|
'onUpdate:data': (value: any[]) => {
|
|
40
42
|
data.value = value
|
|
41
43
|
},
|
|
44
|
+
'onField:change': (field: string, ...args: unknown[]) => {
|
|
45
|
+
fieldChanges.push({ field, args })
|
|
46
|
+
},
|
|
42
47
|
columns,
|
|
43
48
|
model,
|
|
44
49
|
rowKey: 'id',
|
|
@@ -89,6 +94,7 @@ function mountBatchEdit() {
|
|
|
89
94
|
host,
|
|
90
95
|
data,
|
|
91
96
|
model,
|
|
97
|
+
fieldChanges,
|
|
92
98
|
unmount() {
|
|
93
99
|
app.unmount()
|
|
94
100
|
host.remove()
|
|
@@ -196,4 +202,136 @@ describe('UBatchEdit quick-edit 切换编辑行', () => {
|
|
|
196
202
|
|
|
197
203
|
unmount()
|
|
198
204
|
})
|
|
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
|
+
})
|
|
199
337
|
})
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
:model="props.model"
|
|
29
29
|
:readonly="props.readonly"
|
|
30
30
|
:label-width="props.labelWidth"
|
|
31
|
-
@field:
|
|
31
|
+
@field:update="handleFieldUpdate"
|
|
32
|
+
@field:change="handleFormFieldChange"
|
|
32
33
|
>
|
|
33
34
|
<slot
|
|
34
35
|
v-bind="{
|
|
@@ -91,8 +92,17 @@ defineOptions({ name: 'UBatchEditForm' })
|
|
|
91
92
|
|
|
92
93
|
const batchEditCtx = inject(BatchEditDIKey)!
|
|
93
94
|
|
|
94
|
-
const {
|
|
95
|
-
|
|
95
|
+
const {
|
|
96
|
+
cls,
|
|
97
|
+
props,
|
|
98
|
+
state,
|
|
99
|
+
emit,
|
|
100
|
+
handleClose,
|
|
101
|
+
handleSave,
|
|
102
|
+
staticFeatures,
|
|
103
|
+
dynamicFeatures,
|
|
104
|
+
syncing
|
|
105
|
+
} = batchEditCtx
|
|
96
106
|
|
|
97
107
|
const focused = toRef(batchEditCtx, 'focused')
|
|
98
108
|
|
|
@@ -105,7 +115,7 @@ defineExpose<_FormExposed>({
|
|
|
105
115
|
reset: () => formComponentRef.value?.reset()
|
|
106
116
|
})
|
|
107
117
|
|
|
108
|
-
function
|
|
118
|
+
function handleFieldUpdate(field: string, value: any) {
|
|
109
119
|
// 编程方式重置/回显期间不写回行数据,避免默认值污染源数据
|
|
110
120
|
if (syncing.value) return
|
|
111
121
|
|
|
@@ -114,6 +124,10 @@ function handleFieldChange(field: string, value: any) {
|
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
|
|
127
|
+
function handleFormFieldChange(field: string, ...args: any[]) {
|
|
128
|
+
emit('field:change', field, ...args)
|
|
129
|
+
}
|
|
130
|
+
|
|
117
131
|
const creatable = computed(() => {
|
|
118
132
|
if (state.formActionType !== 'create') return false
|
|
119
133
|
return staticFeatures.value.has('create') || dynamicFeatures.value.create?.(state.row)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createApp, h, nextTick, reactive } from 'vue'
|
|
3
|
+
|
|
4
|
+
import { USelect } from '../../select'
|
|
5
|
+
import { UForm } from '../index'
|
|
6
|
+
|
|
7
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
8
|
+
|
|
9
|
+
const behaviorOptions = [
|
|
10
|
+
{ label: '事件', value: 'event' },
|
|
11
|
+
{ label: '接口', value: 'api' }
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
async function openSelect(host: HTMLElement) {
|
|
15
|
+
const input = host.querySelector('input')!
|
|
16
|
+
input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
17
|
+
await nextTick()
|
|
18
|
+
await nextTick()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function clickSelectOption(label: string) {
|
|
22
|
+
const option = [...document.body.querySelectorAll<HTMLElement>('.u-select__option')].find(
|
|
23
|
+
(el) => el.textContent === label
|
|
24
|
+
)
|
|
25
|
+
expect(option, `选项「${label}」应存在`).toBeTruthy()
|
|
26
|
+
option!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
27
|
+
await nextTick()
|
|
28
|
+
await sleep(50)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('UForm field events', () => {
|
|
32
|
+
it('field:update and field:change are separate', async () => {
|
|
33
|
+
const host = document.createElement('div')
|
|
34
|
+
document.body.appendChild(host)
|
|
35
|
+
|
|
36
|
+
const model = reactive({ behavior: 'event' })
|
|
37
|
+
const updates: { field: string; value: unknown }[] = []
|
|
38
|
+
const changes: { field: string; args: unknown[] }[] = []
|
|
39
|
+
|
|
40
|
+
const app = createApp({
|
|
41
|
+
render() {
|
|
42
|
+
return h(
|
|
43
|
+
UForm,
|
|
44
|
+
{
|
|
45
|
+
model,
|
|
46
|
+
'onField:update': (field: string, value: unknown) => {
|
|
47
|
+
updates.push({ field, value })
|
|
48
|
+
},
|
|
49
|
+
'onField:change': (field: string, ...args: unknown[]) => {
|
|
50
|
+
changes.push({ field, args })
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
default: () =>
|
|
55
|
+
h(USelect, { field: 'behavior', label: '行为', options: behaviorOptions })
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
app.mount(host)
|
|
62
|
+
updates.length = 0
|
|
63
|
+
changes.length = 0
|
|
64
|
+
|
|
65
|
+
model.behavior = 'api'
|
|
66
|
+
await nextTick()
|
|
67
|
+
|
|
68
|
+
expect(updates.some((item) => item.field === 'behavior' && item.value === 'api')).toBe(true)
|
|
69
|
+
expect(changes).toHaveLength(0)
|
|
70
|
+
|
|
71
|
+
await openSelect(host)
|
|
72
|
+
await clickSelectOption('事件')
|
|
73
|
+
|
|
74
|
+
expect(changes).toHaveLength(1)
|
|
75
|
+
expect(changes[0]).toEqual({ field: 'behavior', args: [{ label: '事件', value: 'event' }] })
|
|
76
|
+
|
|
77
|
+
app.unmount()
|
|
78
|
+
host.remove()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { isFieldModified } from '../is-field-modified'
|
|
4
|
+
|
|
5
|
+
describe('isFieldModified', () => {
|
|
6
|
+
it('primitive 严格相等视为未变更', () => {
|
|
7
|
+
expect(isFieldModified('a', 'a')).toBe(false)
|
|
8
|
+
expect(isFieldModified(1, 1)).toBe(false)
|
|
9
|
+
expect(isFieldModified(true, true)).toBe(false)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('primitive 不等视为已变更', () => {
|
|
13
|
+
expect(isFieldModified('a', 'b')).toBe(true)
|
|
14
|
+
expect(isFieldModified(1, 2)).toBe(true)
|
|
15
|
+
expect(isFieldModified(true, false)).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('nullish 空值等价视为未变更', () => {
|
|
19
|
+
expect(isFieldModified(null, undefined)).toBe(false)
|
|
20
|
+
expect(isFieldModified(undefined, '')).toBe(false)
|
|
21
|
+
expect(isFieldModified('', null)).toBe(false)
|
|
22
|
+
expect(isFieldModified(null, null)).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('数组与对象按 JSON 深比较', () => {
|
|
26
|
+
expect(isFieldModified([1, 2], [1, 2])).toBe(false)
|
|
27
|
+
expect(isFieldModified([1, 2], [1, 3])).toBe(true)
|
|
28
|
+
expect(isFieldModified({ a: 1 }, { a: 1 })).toBe(false)
|
|
29
|
+
expect(isFieldModified({ a: 1 }, { a: 2 })).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('undefined 与有值视为已变更', () => {
|
|
33
|
+
expect(isFieldModified(undefined, 'x')).toBe(true)
|
|
34
|
+
expect(isFieldModified('x', undefined)).toBe(true)
|
|
35
|
+
expect(isFieldModified(null, 'x')).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
:model-value="modelValue ?? o(model ?? {}).get(field)"
|
|
18
18
|
@update:model-value="handleUpdateValue(field, $event)"
|
|
19
19
|
/>
|
|
20
|
+
<div v-if="shouldShowModified(field)" :class="cls.e('data-before')">
|
|
21
|
+
<span :class="cls.e('changed-tag')">{{ modifiedLabel }}</span>
|
|
22
|
+
<component :is="node" readonly :model-value="getBaselineFieldValue(field)" />
|
|
23
|
+
</div>
|
|
20
24
|
</u-form-item>
|
|
21
25
|
|
|
22
26
|
<component v-else :is="node" />
|
|
@@ -33,12 +37,13 @@ import { nextTick, toRef, useTemplateRef } from 'vue'
|
|
|
33
37
|
import type { BreakCols, FormProps, _FormExposed, FormEmits } from '../../types'
|
|
34
38
|
import { UFormItem } from '../form-item'
|
|
35
39
|
import { UGrid } from '../grid'
|
|
40
|
+
import { isFieldModified } from './is-field-modified'
|
|
36
41
|
import { useFormFields } from './use-form-fields'
|
|
37
42
|
import { useNodeInterceptor } from './use-node-interceptor'
|
|
38
43
|
|
|
39
44
|
defineOptions({ name: 'UForm' })
|
|
40
45
|
|
|
41
|
-
const props = defineProps<FormProps>()
|
|
46
|
+
const props = withDefaults(defineProps<FormProps>(), { modifiedLabel: '变更前:' })
|
|
42
47
|
|
|
43
48
|
const emit = defineEmits<FormEmits>()
|
|
44
49
|
|
|
@@ -56,7 +61,8 @@ const {
|
|
|
56
61
|
reset,
|
|
57
62
|
registerField,
|
|
58
63
|
unregisterField,
|
|
59
|
-
shouldValidate
|
|
64
|
+
shouldValidate,
|
|
65
|
+
getBaselineModel
|
|
60
66
|
} = useFormFields({ props })
|
|
61
67
|
|
|
62
68
|
async function validate(keys?: string[]) {
|
|
@@ -71,8 +77,12 @@ async function validate(keys?: string[]) {
|
|
|
71
77
|
return valid
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
function
|
|
75
|
-
emit('field:
|
|
80
|
+
function handleFieldUpdate(field: string, value: any) {
|
|
81
|
+
emit('field:update', field, value)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleFieldChange(field: string, ...args: any[]) {
|
|
85
|
+
emit('field:change', field, ...args)
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
provideFormContext({
|
|
@@ -81,6 +91,7 @@ provideFormContext({
|
|
|
81
91
|
unregisterField,
|
|
82
92
|
validateFields: validate,
|
|
83
93
|
shouldValidate,
|
|
94
|
+
handleFieldUpdate,
|
|
84
95
|
handleFieldChange
|
|
85
96
|
})
|
|
86
97
|
|
|
@@ -91,5 +102,18 @@ function handleUpdateValue(field: string, value: any) {
|
|
|
91
102
|
o(props.model).set(field, value)
|
|
92
103
|
}
|
|
93
104
|
|
|
105
|
+
function getBaselineFieldValue(field: string) {
|
|
106
|
+
return o(getBaselineModel() ?? {}).get(field)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function shouldShowModified(field: string) {
|
|
110
|
+
if (!props.showModified) return false
|
|
111
|
+
const baseline = getBaselineModel()
|
|
112
|
+
if (!baseline || !props.model) return false
|
|
113
|
+
const current = o(props.model).get(field)
|
|
114
|
+
const initial = o(baseline).get(field)
|
|
115
|
+
return isFieldModified(current, initial)
|
|
116
|
+
}
|
|
117
|
+
|
|
94
118
|
defineExpose<_FormExposed>({ el: toRef(() => gridRef.value?.el), validate, clearValidate, reset })
|
|
95
119
|
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** 判断字段当前值相对基准值是否已变更 */
|
|
2
|
+
export function isFieldModified(current: unknown, initial: unknown): boolean {
|
|
3
|
+
if (current === initial) return false
|
|
4
|
+
|
|
5
|
+
const currentNullish = current === null || current === undefined || current === ''
|
|
6
|
+
const initialNullish = initial === null || initial === undefined || initial === ''
|
|
7
|
+
if (currentNullish && initialNullish) return false
|
|
8
|
+
|
|
9
|
+
const currentIsObject = current !== null && typeof current === 'object'
|
|
10
|
+
const initialIsObject = initial !== null && typeof initial === 'object'
|
|
11
|
+
if (currentIsObject || initialIsObject) {
|
|
12
|
+
return JSON.stringify(current) !== JSON.stringify(initial)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
@@ -16,12 +16,63 @@
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
@include m.e(data-before) {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: 4px;
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
margin-top: 4px;
|
|
24
|
+
padding: 2px 8px;
|
|
25
|
+
border-left: 2px solid fn.use-var(color, info);
|
|
26
|
+
background-color: fn.color-a(color, 8, info);
|
|
27
|
+
border-radius: 2px;
|
|
28
|
+
width: fit-content;
|
|
29
|
+
max-width: 100%;
|
|
30
|
+
line-height: 1.4;
|
|
31
|
+
font-size: fn.use-var(font-size, assist, default);
|
|
32
|
+
|
|
33
|
+
// 文本类控件保持全宽,便于展示较长内容
|
|
34
|
+
&:has(#{fn.bem(input)}),
|
|
35
|
+
&:has(#{fn.bem(textarea)}),
|
|
36
|
+
&:has(#{fn.bem(select)}),
|
|
37
|
+
&:has(#{fn.bem(multi-select)}),
|
|
38
|
+
&:has(#{fn.bem(multi-tree-select)}),
|
|
39
|
+
&:has(#{fn.bem(auto-complete)}),
|
|
40
|
+
&:has(#{fn.bem(cascade)}),
|
|
41
|
+
&:has(#{fn.bem(date-picker)}),
|
|
42
|
+
&:has(#{fn.bem(date-range-picker)}),
|
|
43
|
+
&:has(#{fn.bem(number-input)}),
|
|
44
|
+
&:has(#{fn.bem(tree-select)}) {
|
|
45
|
+
width: 100%;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 让内部只读控件更像辅助说明,减少「第二个输入框」感
|
|
49
|
+
@include m.bem(form-item, content) {
|
|
50
|
+
background: transparent;
|
|
51
|
+
padding: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#{fn.bem(input)},
|
|
55
|
+
#{fn.bem(textarea)},
|
|
56
|
+
#{fn.bem(select)},
|
|
57
|
+
#{fn.bem(multi-select)},
|
|
58
|
+
#{fn.bem(multi-tree-select)},
|
|
59
|
+
#{fn.bem(auto-complete)},
|
|
60
|
+
#{fn.bem(cascade)},
|
|
61
|
+
#{fn.bem(date-picker)},
|
|
62
|
+
#{fn.bem(date-range-picker)} {
|
|
63
|
+
border: none;
|
|
64
|
+
box-shadow: none;
|
|
65
|
+
background: transparent;
|
|
66
|
+
min-height: auto;
|
|
67
|
+
height: auto;
|
|
68
|
+
padding: 0;
|
|
69
|
+
}
|
|
21
70
|
}
|
|
22
71
|
|
|
23
72
|
@include m.e(changed-tag) {
|
|
24
73
|
color: fn.use-var(color, info);
|
|
25
74
|
font-size: fn.use-var(font-size, assist, default);
|
|
75
|
+
font-style: normal;
|
|
76
|
+
flex-shrink: 0;
|
|
26
77
|
}
|
|
27
78
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { copy, o } from '@cat-kit/core'
|
|
2
2
|
import type { FormFieldItem } from '@veltra/utils'
|
|
3
|
-
import { nextTick, watch } from 'vue'
|
|
3
|
+
import { nextTick, shallowRef, watch } from 'vue'
|
|
4
4
|
|
|
5
5
|
import type { FormProps } from '../../types/form'
|
|
6
6
|
|
|
@@ -42,7 +42,7 @@ export function useFormFields(options: Options) {
|
|
|
42
42
|
let willValidate = true
|
|
43
43
|
|
|
44
44
|
/** model 初始快照,供 reset 恢复 */
|
|
45
|
-
|
|
45
|
+
const initialSnapshot = shallowRef<Record<string, any> | undefined>()
|
|
46
46
|
|
|
47
47
|
function runWithoutChangeValidate(fn: () => void) {
|
|
48
48
|
willValidate = false
|
|
@@ -59,7 +59,7 @@ export function useFormFields(options: Options) {
|
|
|
59
59
|
watch(
|
|
60
60
|
() => props.model,
|
|
61
61
|
(model) => {
|
|
62
|
-
initialSnapshot = model ? copy(model) : undefined
|
|
62
|
+
initialSnapshot.value = model ? copy(model) : undefined
|
|
63
63
|
},
|
|
64
64
|
{ immediate: true, deep: false }
|
|
65
65
|
)
|
|
@@ -89,13 +89,27 @@ export function useFormFields(options: Options) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function reset() {
|
|
92
|
-
if (!props.model || !initialSnapshot) return
|
|
92
|
+
if (!props.model || !initialSnapshot.value) return
|
|
93
93
|
|
|
94
94
|
runWithoutChangeValidate(() => {
|
|
95
|
-
applySnapshot(props.model!, initialSnapshot!)
|
|
95
|
+
applySnapshot(props.model!, initialSnapshot.value!)
|
|
96
96
|
Object.values(fields).forEach((item) => item.clearValidate?.())
|
|
97
97
|
})
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
/** 变更前基准数据:优先 initialModel,否则为 model 引用变更时的快照 */
|
|
101
|
+
function getBaselineModel() {
|
|
102
|
+
return props.initialModel ?? initialSnapshot.value
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
fields,
|
|
107
|
+
registerField,
|
|
108
|
+
validate,
|
|
109
|
+
unregisterField,
|
|
110
|
+
clearValidate,
|
|
111
|
+
reset,
|
|
112
|
+
shouldValidate,
|
|
113
|
+
getBaselineModel
|
|
114
|
+
}
|
|
101
115
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createApp, h, nextTick, reactive } from 'vue'
|
|
3
|
+
|
|
4
|
+
import { UForm } from '../../form'
|
|
5
|
+
import { USelect } from '../../select'
|
|
6
|
+
import { UFormItem } from '../index'
|
|
7
|
+
|
|
8
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
9
|
+
|
|
10
|
+
const behaviorOptions = [
|
|
11
|
+
{ label: '事件', value: 'event' },
|
|
12
|
+
{ label: '接口', value: 'api' }
|
|
13
|
+
]
|
|
14
|
+
|
|
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
|
+
describe('UFormItem change', () => {
|
|
33
|
+
it('forwards Select change with option object', async () => {
|
|
34
|
+
const host = document.createElement('div')
|
|
35
|
+
document.body.appendChild(host)
|
|
36
|
+
|
|
37
|
+
const model = reactive({ behavior: 'event' })
|
|
38
|
+
const itemChanges: unknown[][] = []
|
|
39
|
+
const formChanges: { field: string; args: unknown[] }[] = []
|
|
40
|
+
|
|
41
|
+
const app = createApp({
|
|
42
|
+
render() {
|
|
43
|
+
return h(
|
|
44
|
+
UForm,
|
|
45
|
+
{
|
|
46
|
+
model,
|
|
47
|
+
'onField:change': (field: string, ...args: unknown[]) => {
|
|
48
|
+
formChanges.push({ field, args })
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
default: () =>
|
|
53
|
+
h(
|
|
54
|
+
UFormItem,
|
|
55
|
+
{
|
|
56
|
+
field: 'behavior',
|
|
57
|
+
label: '行为',
|
|
58
|
+
onChange: (...args: unknown[]) => itemChanges.push(args)
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
default: () =>
|
|
62
|
+
h(USelect, {
|
|
63
|
+
modelValue: model.behavior,
|
|
64
|
+
'onUpdate:modelValue': (value: string) => {
|
|
65
|
+
model.behavior = value
|
|
66
|
+
},
|
|
67
|
+
options: behaviorOptions
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
app.mount(host)
|
|
77
|
+
|
|
78
|
+
await openSelect(host)
|
|
79
|
+
await clickSelectOption('接口')
|
|
80
|
+
|
|
81
|
+
expect(itemChanges).toHaveLength(1)
|
|
82
|
+
expect(itemChanges[0]![0]).toEqual({ label: '接口', value: 'api' })
|
|
83
|
+
|
|
84
|
+
expect(formChanges).toHaveLength(1)
|
|
85
|
+
expect(formChanges[0]).toEqual({ field: 'behavior', args: [{ label: '接口', value: 'api' }] })
|
|
86
|
+
|
|
87
|
+
app.unmount()
|
|
88
|
+
host.remove()
|
|
89
|
+
})
|
|
90
|
+
})
|