@veltra/desktop 1.1.23 → 1.1.25
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/action/action-group.vue.d.ts +2 -2
- package/dist/components/action/action.vue.d.ts +1 -1
- package/dist/components/auto-complete/auto-complete.vue.d.ts +1 -1
- package/dist/components/batch-edit/batch-edit.vue.d.ts +2 -2
- package/dist/components/cascade/cascade.vue.d.ts +4 -4
- package/dist/components/checkbox/checkbox-button.vue.d.ts +1 -1
- package/dist/components/checkbox-group/checkbox-group.vue.d.ts +1 -1
- package/dist/components/date-picker/date-picker.vue.d.ts +2 -2
- package/dist/components/date-range-picker/date-range-picker.vue.d.ts +2 -2
- package/dist/components/drawer/drawer.vue.d.ts +2 -2
- package/dist/components/empty/empty.vue.d.ts +1 -1
- package/dist/components/file-picker/file-picker.vue.d.ts +1 -1
- package/dist/components/file-viewer/file-viewer.vue.d.ts +1 -1
- package/dist/components/form/dynamic-form-model.d.ts +7 -5
- package/dist/components/form/dynamic-form-model.js +31 -12
- package/dist/components/form/dynamic-form-model.js.map +1 -1
- package/dist/components/form/form-model.d.ts +8 -8
- package/dist/components/form/form-model.js +17 -3
- package/dist/components/form/form-model.js.map +1 -1
- package/dist/components/form/helper.d.ts +6 -1
- package/dist/components/form/helper.js +7 -1
- package/dist/components/form/helper.js.map +1 -1
- package/dist/components/form/index.d.ts +2 -2
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +2 -1
- package/dist/components/input/input.vue.d.ts +3 -3
- package/dist/components/multi-select/multi-select.vue.d.ts +3 -3
- package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +5 -5
- package/dist/components/number/number.vue.d.ts +1 -1
- package/dist/components/number-input/number-input.vue.d.ts +3 -3
- package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
- package/dist/components/password-input/password-input.vue.d.ts +1 -1
- package/dist/components/progress-nodes/progress-nodes.vue.d.ts +2 -2
- package/dist/components/radio-group/radio-group.vue.d.ts +1 -1
- package/dist/components/rich-text-editor/rich-text-editor.vue.d.ts +1 -1
- package/dist/components/select/select.vue.d.ts +2 -2
- package/dist/components/table/table.vue.d.ts +2 -2
- package/dist/components/tabs/tabs-horizontal.vue.d.ts +4 -4
- package/dist/components/tabs/tabs-vertical.vue.d.ts +4 -4
- package/dist/components/tabs/tabs.vue.d.ts +4 -4
- package/dist/components/textarea/textarea.vue.d.ts +3 -3
- package/dist/components/tree-select/tree-select.vue.d.ts +5 -5
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/types/form.d.ts +12 -9
- package/dist/types/index.d.ts +2 -2
- package/package.json +6 -6
- package/src/components/form/__test__/form-model.test.ts +190 -0
- package/src/components/form/dynamic-form-model.ts +47 -19
- package/src/components/form/form-model.ts +40 -28
- package/src/components/form/helper.ts +9 -0
- package/src/components/form/index.ts +2 -1
- 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,
|
|
29
|
-
{} as Record<string,
|
|
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.
|
|
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,
|
|
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,
|
|
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:
|
|
133
|
+
add(field: string, item: FormModelField): void {
|
|
109
134
|
this.fields[field] = item
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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,
|
|
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,
|
|
44
|
+
formKeys: Map<number, AllKeys<Fields>[]> = new Map()
|
|
34
45
|
|
|
35
46
|
/** 初始数据 */
|
|
36
47
|
readonly initialData: ModelData<Fields>
|
|
37
48
|
|
|
38
|
-
readonly errors: Map<
|
|
49
|
+
readonly errors: Map<AllKeys<Fields>, string[] | undefined> = shallowReactive(new Map())
|
|
39
50
|
|
|
40
|
-
private validator: Validator<
|
|
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
|
|
55
|
-
const fieldItem =
|
|
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(
|
|
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?:
|
|
115
|
+
private getValidateFields(fields?: AllKeys<Fields> | AllKeys<Fields>[]) {
|
|
104
116
|
if (!fields) {
|
|
105
117
|
if (this.formKeys.size) {
|
|
106
|
-
let _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?:
|
|
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?:
|
|
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:
|
|
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:
|
|
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
|
}
|
|
@@ -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
|
|
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'
|
package/src/types/form.ts
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
import type { ComponentProps, DeconstructValue, ValidateRule } from '@veltra/utils'
|
|
2
2
|
import type { ShallowRef } from 'vue'
|
|
3
3
|
|
|
4
|
+
import type { NestedFieldMarker } from '../components/form/helper'
|
|
5
|
+
|
|
4
6
|
export interface FormModelItem<Val = any> extends ValidateRule {
|
|
5
7
|
/** 模型值 */
|
|
6
8
|
value?: Val
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export type
|
|
10
|
-
|
|
11
|
+
export type FormModelField = FormModelItem | NestedFieldMarker
|
|
12
|
+
|
|
13
|
+
// 最终暴露的 ModelData 类型
|
|
14
|
+
export type ModelData<Fields extends Record<string, any>> = {
|
|
15
|
+
[key in keyof Fields]: Fields[key] extends NestedFieldMarker<infer SubFields>
|
|
16
|
+
? ModelData<SubFields>
|
|
17
|
+
: Fields[key] extends FormModelItem
|
|
18
|
+
? Fields[key]['value'] extends () => infer T
|
|
19
|
+
? T
|
|
20
|
+
: Fields[key]['value']
|
|
21
|
+
: never
|
|
11
22
|
}
|
|
12
23
|
|
|
24
|
+
// 获取所有的扁平路径 Union(如 'name' | 'contact.email')
|
|
25
|
+
export type AllKeys<Fields extends Record<string, any>> = {
|
|
26
|
+
[K in keyof Fields]: K extends string
|
|
27
|
+
? Fields[K] extends NestedFieldMarker<infer SubFields>
|
|
28
|
+
? `${K}.${AllKeys<SubFields>}`
|
|
29
|
+
: K
|
|
30
|
+
: never
|
|
31
|
+
}[keyof Fields]
|
|
32
|
+
|
|
13
33
|
export type ModelRules<Fields extends Record<string, FormModelItem>> = {
|
|
14
34
|
[key in keyof Fields]: Omit<Fields[key], 'value'>
|
|
15
35
|
}
|
|
@@ -22,9 +42,7 @@ export interface DataSettingConfig {
|
|
|
22
42
|
validate?: boolean
|
|
23
43
|
}
|
|
24
44
|
|
|
25
|
-
export type IFormModel<
|
|
26
|
-
Fields extends Record<string, FormModelItem> = Record<string, FormModelItem>
|
|
27
|
-
> = {
|
|
45
|
+
export type IFormModel<Fields extends Record<string, any> = Record<string, any>> = {
|
|
28
46
|
/** 表单数据 */
|
|
29
47
|
readonly data: ModelData<Fields>
|
|
30
48
|
/** 字段校验规则 */
|
|
@@ -34,16 +52,16 @@ export type IFormModel<
|
|
|
34
52
|
*/
|
|
35
53
|
readonly allKeys: string[]
|
|
36
54
|
/** 需要校验的key */
|
|
37
|
-
formKeys: Map<number,
|
|
55
|
+
formKeys: Map<number, AllKeys<Fields>[]>
|
|
38
56
|
/** 错误 */
|
|
39
|
-
readonly errors: Map<
|
|
57
|
+
readonly errors: Map<AllKeys<Fields>, string[] | undefined>
|
|
40
58
|
/**
|
|
41
59
|
* 字段校验
|
|
42
60
|
* @param fields 字段, 如果不传入时将会使用keyFields来进行校验
|
|
43
61
|
*/
|
|
44
|
-
validate: (fields?:
|
|
62
|
+
validate: (fields?: AllKeys<Fields> | AllKeys<Fields>[]) => Promise<boolean>
|
|
45
63
|
/** 重置数据 */
|
|
46
|
-
resetData(fields?:
|
|
64
|
+
resetData(fields?: AllKeys<Fields> | AllKeys<Fields>[]): void
|
|
47
65
|
|
|
48
66
|
/**
|
|
49
67
|
* 设置数据
|
|
@@ -54,9 +72,9 @@ export type IFormModel<
|
|
|
54
72
|
/** 清除校验 */
|
|
55
73
|
clearValidate(): void
|
|
56
74
|
/** 监听值变更 */
|
|
57
|
-
onChange(cb: (field:
|
|
75
|
+
onChange(cb: (field: AllKeys<Fields>, val: any) => void): void
|
|
58
76
|
/** 关闭监听值变更 */
|
|
59
|
-
offChange(cb: (field:
|
|
77
|
+
offChange(cb: (field: AllKeys<Fields>, val: any) => void): void
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
/** 表单组件属性 */
|