@veltra/desktop 1.1.35 → 1.2.0
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.js +1 -0
- package/dist/components/auto-complete/auto-complete.js.map +1 -1
- package/dist/components/batch-edit/batch-edit-form.js +12 -5
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.js +8 -3
- package/dist/components/batch-edit/batch-edit.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.vue.d.ts +40 -35
- package/dist/components/batch-edit/di.js.map +1 -1
- package/dist/components/batch-edit/use-edit.js +41 -23
- package/dist/components/batch-edit/use-edit.js.map +1 -1
- package/dist/components/cascade/cascade.js +1 -0
- package/dist/components/cascade/cascade.js.map +1 -1
- package/dist/components/cascade/cascade.vue.d.ts +5 -5
- package/dist/components/checkbox/checkbox-button.js +1 -0
- package/dist/components/checkbox/checkbox-button.js.map +1 -1
- package/dist/components/checkbox/checkbox-button.vue.d.ts +1 -1
- package/dist/components/checkbox/checkbox.js +1 -0
- package/dist/components/checkbox/checkbox.js.map +1 -1
- package/dist/components/checkbox-group/checkbox-group.js +1 -0
- package/dist/components/checkbox-group/checkbox-group.js.map +1 -1
- package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
- package/dist/components/code-editor/code-editor.js +1 -0
- package/dist/components/code-editor/code-editor.js.map +1 -1
- package/dist/components/condition-editor/core/evaluator.js +3 -3
- package/dist/components/condition-editor/core/evaluator.js.map +1 -1
- package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
- package/dist/components/date-picker/date-picker.js +1 -0
- package/dist/components/date-picker/date-picker.js.map +1 -1
- package/dist/components/date-picker/date-picker.vue.d.ts +1 -1
- package/dist/components/date-range-picker/date-range-picker.js +2 -0
- package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
- package/dist/components/date-range-picker/date-range-picker.vue.d.ts +1 -1
- package/dist/components/dialog/use-maximum.js +7 -1
- package/dist/components/dialog/use-maximum.js.map +1 -1
- package/dist/components/empty/empty.vue.d.ts +1 -1
- package/dist/components/expression-editor/expression-editor.js +1 -0
- package/dist/components/expression-editor/expression-editor.js.map +1 -1
- package/dist/components/file-picker/file-picker.js +1 -0
- package/dist/components/file-picker/file-picker.js.map +1 -1
- package/dist/components/form/form.js.map +1 -1
- package/dist/components/form/form.vue.d.ts +11 -19
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +24 -26
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form/index.d.ts +0 -3
- package/dist/components/form/use-form-fields.js +88 -0
- package/dist/components/form/use-form-fields.js.map +1 -0
- package/dist/components/form/use-node-interceptor.js +19 -30
- 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 +44 -18
- package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form-item/helper.js +4 -1
- package/dist/components/form-item/helper.js.map +1 -1
- package/dist/components/form-item/validate.d.ts +26 -0
- package/dist/components/form-item/validate.js +97 -0
- package/dist/components/form-item/validate.js.map +1 -0
- package/dist/components/group-input/group-input.js +1 -0
- package/dist/components/group-input/group-input.js.map +1 -1
- package/dist/components/index.d.ts +0 -3
- package/dist/components/index.js +0 -7
- package/dist/components/input/input.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/components/input/input.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/loading/style2.css +90 -159
- package/dist/components/multi-select/multi-select.js +1 -0
- package/dist/components/multi-select/multi-select.js.map +1 -1
- package/dist/components/multi-select/multi-select.vue.d.ts +3 -3
- package/dist/components/multi-tree-select/multi-tree-select.js +1 -0
- package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
- package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +4 -4
- package/dist/components/number-input/number-input.js +1 -0
- package/dist/components/number-input/number-input.js.map +1 -1
- package/dist/components/number-range-input/number-range-input.js +1 -0
- package/dist/components/number-range-input/number-range-input.js.map +1 -1
- package/dist/components/palette/palette.js +1 -0
- package/dist/components/palette/palette.js.map +1 -1
- package/dist/components/password-input/password-input.js +1 -0
- package/dist/components/password-input/password-input.js.map +1 -1
- package/dist/components/pop-confirm/pop-confirm.vue.d.ts +1 -1
- package/dist/components/radio/radio.js +1 -0
- package/dist/components/radio/radio.js.map +1 -1
- package/dist/components/radio-group/radio-group.js +1 -0
- package/dist/components/radio-group/radio-group.js.map +1 -1
- package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
- package/dist/components/rich-text-editor/rich-text-editor.js +3 -2
- package/dist/components/rich-text-editor/rich-text-editor.js.map +1 -1
- package/dist/components/rich-text-editor/toolbar.js +10 -7
- package/dist/components/rich-text-editor/toolbar.js.map +1 -1
- package/dist/components/select/select.js +6 -3
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/select/select.vue.d.ts +2 -2
- package/dist/components/slider/slider.js +15 -2
- package/dist/components/slider/slider.js.map +1 -1
- package/dist/components/steps/steps.vue.d.ts +1 -1
- package/dist/components/switch/switch.js +1 -0
- package/dist/components/switch/switch.js.map +1 -1
- package/dist/components/table/style2.css +4 -7
- package/dist/components/textarea/textarea.js +1 -0
- package/dist/components/textarea/textarea.js.map +1 -1
- package/dist/components/tip/tip.vue.d.ts +1 -1
- package/dist/components/tree-select/tree-select.js +1 -0
- package/dist/components/tree-select/tree-select.js.map +1 -1
- package/dist/components/tree-select/tree-select.vue.d.ts +4 -4
- package/dist/components/watermark/watermark.vue.d.ts +1 -1
- package/dist/index.d.ts +5 -6
- package/dist/index.js +1 -4
- package/dist/types/auto-complete.d.ts +3 -2
- package/dist/types/batch-edit.d.ts +4 -5
- package/dist/types/cascade.d.ts +3 -2
- package/dist/types/checkbox-group.d.ts +2 -2
- package/dist/types/checkbox.d.ts +4 -3
- package/dist/types/code-editor.d.ts +3 -2
- package/dist/types/date-picker.d.ts +3 -2
- package/dist/types/date-range-picker.d.ts +3 -2
- package/dist/types/expression-editor.d.ts +3 -2
- package/dist/types/file-picker.d.ts +3 -2
- package/dist/types/form-field.d.ts +12 -0
- package/dist/types/form-item.d.ts +2 -2
- package/dist/types/form.d.ts +9 -48
- package/dist/types/group-input.d.ts +3 -2
- package/dist/types/index.d.ts +4 -2
- package/dist/types/input.d.ts +3 -2
- package/dist/types/loading.d.ts +1 -1
- package/dist/types/multi-select.d.ts +3 -2
- package/dist/types/multi-tree-select.d.ts +3 -2
- package/dist/types/palette.d.ts +3 -2
- package/dist/types/radio-group.d.ts +3 -2
- package/dist/types/radio.d.ts +3 -2
- package/dist/types/rich-text-editor.d.ts +3 -2
- package/dist/types/select.d.ts +3 -2
- package/dist/types/slider.d.ts +3 -2
- package/dist/types/switch.d.ts +3 -2
- package/dist/types/text-editor.d.ts +3 -2
- package/dist/types/textarea.d.ts +3 -2
- package/dist/types/tree-select.d.ts +3 -2
- package/package.json +6 -6
- package/src/components/batch-edit/batch-edit-form.vue +20 -13
- package/src/components/batch-edit/batch-edit.vue +7 -8
- package/src/components/batch-edit/di.ts +1 -2
- package/src/components/batch-edit/use-edit.ts +58 -36
- package/src/components/date-range-picker/date-range-picker.vue +2 -0
- package/src/components/dialog/use-maximum.ts +8 -1
- package/src/components/form/form.vue +22 -38
- package/src/components/form/index.ts +0 -6
- package/src/components/form/use-form-fields.ts +126 -0
- package/src/components/form/use-node-interceptor.ts +27 -56
- package/src/components/form-item/form-item.vue +69 -18
- package/src/components/form-item/helper.ts +10 -4
- package/src/components/form-item/validate.ts +155 -0
- package/src/components/loading/style.scss +126 -355
- package/src/components/rich-text-editor/rich-text-editor.vue +28 -25
- package/src/components/rich-text-editor/toolbar.vue +23 -19
- package/src/components/select/select.vue +6 -4
- package/src/components/slider/slider.vue +12 -2
- package/src/components/table/style.scss +4 -14
- package/src/types/auto-complete.ts +4 -2
- package/src/types/batch-edit.ts +4 -5
- package/src/types/cascade.ts +4 -2
- package/src/types/checkbox-group.ts +2 -2
- package/src/types/checkbox.ts +5 -3
- package/src/types/code-editor.ts +4 -2
- package/src/types/date-picker.ts +4 -2
- package/src/types/date-range-picker.ts +4 -2
- package/src/types/expression-editor.ts +4 -2
- package/src/types/file-picker.ts +4 -2
- package/src/types/form-field.ts +11 -0
- package/src/types/form-item.ts +2 -2
- package/src/types/form.ts +9 -82
- package/src/types/group-input.ts +4 -2
- package/src/types/index.ts +1 -0
- package/src/types/input.ts +4 -2
- package/src/types/loading.ts +1 -1
- package/src/types/multi-select.ts +4 -2
- package/src/types/multi-tree-select.ts +5 -2
- package/src/types/palette.ts +4 -2
- package/src/types/radio-group.ts +4 -2
- package/src/types/radio.ts +4 -2
- package/src/types/rich-text-editor.ts +4 -2
- package/src/types/select.ts +4 -2
- package/src/types/slider.ts +4 -2
- package/src/types/switch.ts +4 -2
- package/src/types/text-editor.ts +4 -2
- package/src/types/textarea.ts +4 -2
- package/src/types/tree-select.ts +5 -2
- package/dist/components/form/dynamic-form-model.d.ts +0 -73
- package/dist/components/form/dynamic-form-model.js +0 -184
- package/dist/components/form/dynamic-form-model.js.map +0 -1
- package/dist/components/form/form-model.d.ts +0 -73
- package/dist/components/form/form-model.js +0 -179
- package/dist/components/form/form-model.js.map +0 -1
- package/dist/components/form/helper.d.ts +0 -16
- package/dist/components/form/helper.js +0 -20
- package/dist/components/form/helper.js.map +0 -1
- package/src/components/form/__test__/form-model.test.ts +0 -190
- package/src/components/form/dynamic-form-model.ts +0 -258
- package/src/components/form/form-model.ts +0 -255
- package/src/components/form/helper.ts +0 -23
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { o } from '@cat-kit/core'
|
|
2
|
+
import type { FormFieldItem } from '@veltra/utils'
|
|
3
|
+
import { nextTick, watch } from 'vue'
|
|
4
|
+
|
|
5
|
+
import type { FormProps } from '../../types/form'
|
|
6
|
+
|
|
7
|
+
interface Options {
|
|
8
|
+
props: FormProps
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 深拷贝 model,保留 undefined 等 o().copy() 会丢弃的值 */
|
|
12
|
+
function snapshotModel(model: Record<string, any>): Record<string, any> {
|
|
13
|
+
const result: Record<string, any> = {}
|
|
14
|
+
|
|
15
|
+
for (const key of Object.keys(model)) {
|
|
16
|
+
const value = model[key]
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
result[key] = value.map((item) =>
|
|
19
|
+
item !== null && typeof item === 'object' ? snapshotModel(item) : item
|
|
20
|
+
)
|
|
21
|
+
} else if (value !== null && typeof value === 'object') {
|
|
22
|
+
result[key] = snapshotModel(value)
|
|
23
|
+
} else {
|
|
24
|
+
result[key] = value
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return result
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 按快照逐字段写回 model,支持 undefined 与嵌套对象 */
|
|
32
|
+
function applySnapshot(target: Record<string, any>, source: Record<string, any>) {
|
|
33
|
+
for (const key of Object.keys(source)) {
|
|
34
|
+
const sourceValue = source[key]
|
|
35
|
+
const targetValue = target[key]
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
sourceValue !== null &&
|
|
39
|
+
typeof sourceValue === 'object' &&
|
|
40
|
+
!Array.isArray(sourceValue) &&
|
|
41
|
+
targetValue !== null &&
|
|
42
|
+
typeof targetValue === 'object' &&
|
|
43
|
+
!Array.isArray(targetValue)
|
|
44
|
+
) {
|
|
45
|
+
applySnapshot(targetValue, sourceValue)
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(sourceValue)) {
|
|
50
|
+
o(target).set(
|
|
51
|
+
key,
|
|
52
|
+
sourceValue.map((item) =>
|
|
53
|
+
item !== null && typeof item === 'object' ? snapshotModel(item) : item
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
o(target).set(key, sourceValue)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useFormFields(options: Options) {
|
|
64
|
+
const { props } = options
|
|
65
|
+
const fields: Record<string, FormFieldItem> = {}
|
|
66
|
+
|
|
67
|
+
let willValidate = true
|
|
68
|
+
|
|
69
|
+
/** model 初始快照,供 reset 恢复 */
|
|
70
|
+
let initialSnapshot: Record<string, any> | undefined
|
|
71
|
+
|
|
72
|
+
function runWithoutChangeValidate(fn: () => void) {
|
|
73
|
+
willValidate = false
|
|
74
|
+
fn()
|
|
75
|
+
nextTick(() => {
|
|
76
|
+
willValidate = true
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function shouldValidate() {
|
|
81
|
+
return willValidate
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
watch(
|
|
85
|
+
() => props.model,
|
|
86
|
+
(model) => {
|
|
87
|
+
initialSnapshot = model ? snapshotModel(model) : undefined
|
|
88
|
+
},
|
|
89
|
+
{ immediate: true, deep: false }
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
function registerField(field: string, item: FormFieldItem) {
|
|
93
|
+
fields[field] = item
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function unregisterField(field: string) {
|
|
97
|
+
delete fields[field]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function clearValidate() {
|
|
101
|
+
Object.values(fields).forEach((item) => item.clearValidate?.())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function validate(keys?: string[]) {
|
|
105
|
+
let validList: boolean[]
|
|
106
|
+
if (keys) {
|
|
107
|
+
validList = await Promise.all(
|
|
108
|
+
keys.map((key) => fields[key]?.validate() ?? Promise.resolve(true))
|
|
109
|
+
)
|
|
110
|
+
} else {
|
|
111
|
+
validList = await Promise.all(Object.values(fields).map((field) => field.validate()))
|
|
112
|
+
}
|
|
113
|
+
return validList.every((valid) => valid)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function reset() {
|
|
117
|
+
if (!props.model || !initialSnapshot) return
|
|
118
|
+
|
|
119
|
+
runWithoutChangeValidate(() => {
|
|
120
|
+
applySnapshot(props.model!, initialSnapshot!)
|
|
121
|
+
Object.values(fields).forEach((item) => item.clearValidate?.())
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { fields, registerField, validate, unregisterField, clearValidate, reset, shouldValidate }
|
|
126
|
+
}
|
|
@@ -1,87 +1,58 @@
|
|
|
1
1
|
import { o } from '@cat-kit/core'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { extractNormalVNodes } from '@veltra/utils'
|
|
3
|
+
import { useSlots, type VNode } from 'vue'
|
|
4
|
+
|
|
5
|
+
export type SlotRenderItem = {
|
|
6
|
+
isFormItem: boolean
|
|
7
|
+
formItemProps?: Record<string, any>
|
|
8
|
+
node: VNode
|
|
9
|
+
field?: string
|
|
10
|
+
modelValue?: any
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export type SlotRenderItem =
|
|
12
|
-
| {
|
|
13
|
-
isFormItem: true
|
|
14
|
-
formItemProps: Record<string, any>
|
|
15
|
-
node: VNode
|
|
16
|
-
field: undefined
|
|
17
|
-
modelValue?: any
|
|
18
|
-
}
|
|
19
|
-
| {
|
|
20
|
-
isFormItem: false
|
|
21
|
-
formItemProps: Record<string, any>
|
|
22
|
-
node: VNode
|
|
23
|
-
field: string
|
|
24
|
-
modelValue?: any
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const id = createIncrease(1)
|
|
28
|
-
|
|
29
13
|
/**
|
|
30
14
|
* 虚拟node拦截
|
|
31
|
-
* @param options 选项
|
|
32
15
|
* @returns
|
|
33
16
|
*/
|
|
34
|
-
export function useNodeInterceptor(
|
|
35
|
-
getSlotsNodes: () => SlotRenderItem[] | null
|
|
36
|
-
} {
|
|
37
|
-
const { props } = options
|
|
17
|
+
export function useNodeInterceptor(): { getSlotsNodes: () => SlotRenderItem[] | null } {
|
|
38
18
|
const slots = useSlots()
|
|
39
19
|
|
|
40
|
-
const formId = id()
|
|
41
|
-
|
|
42
20
|
function getSlotsNodes() {
|
|
43
|
-
const
|
|
44
|
-
const nodes = slots.default?.({ model, data: model?.data })
|
|
21
|
+
const nodes = slots.default?.()
|
|
45
22
|
if (!nodes?.length) return null
|
|
46
23
|
|
|
47
24
|
const flattedNodes = extractNormalVNodes(nodes)
|
|
48
25
|
|
|
49
26
|
const results: SlotRenderItem[] = []
|
|
50
27
|
|
|
51
|
-
// 渲染表单后得到的应当校验的字段
|
|
52
|
-
const fields: string[] = []
|
|
53
|
-
|
|
54
28
|
let i = 0
|
|
55
29
|
while (i < flattedNodes.length) {
|
|
56
30
|
const node = flattedNodes[i]!
|
|
57
31
|
i++
|
|
58
32
|
|
|
59
33
|
const { props, type } = node
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
34
|
+
const field = props?.field as string | undefined
|
|
35
|
+
const isFormItem = (type as any)?.name === 'FormItem'
|
|
36
|
+
const formItemProps = o((props ?? {}) as Record<string, any>).pick([
|
|
37
|
+
'label',
|
|
38
|
+
'rules',
|
|
39
|
+
'span',
|
|
40
|
+
'tips',
|
|
41
|
+
'readonly',
|
|
42
|
+
'field'
|
|
43
|
+
]) as Record<string, any>
|
|
44
|
+
|
|
45
|
+
results.push({
|
|
46
|
+
isFormItem,
|
|
47
|
+
formItemProps,
|
|
69
48
|
node,
|
|
70
|
-
field
|
|
49
|
+
field,
|
|
71
50
|
modelValue: props?.['model-value'] ?? props?.modelValue
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
item.field && fields.push(item.field)
|
|
75
|
-
results.push(item)
|
|
51
|
+
})
|
|
76
52
|
}
|
|
77
53
|
|
|
78
|
-
model.formKeys.set(formId, fields)
|
|
79
54
|
return results
|
|
80
55
|
}
|
|
81
56
|
|
|
82
|
-
onBeforeUnmount(() => {
|
|
83
|
-
props.model?.formKeys.delete(formId)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
57
|
return { getSlotsNodes }
|
|
87
58
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<u-grid-item :span="span" :class="className">
|
|
3
3
|
<label
|
|
4
4
|
v-if="props.label || $slots.label"
|
|
5
|
-
:class="[cls.e('label'), bem.is('required',
|
|
5
|
+
:class="[cls.e('label'), bem.is('required', !!rules?.required)]"
|
|
6
6
|
:style="labelStyles"
|
|
7
7
|
>
|
|
8
8
|
<u-tip v-if="tips" :content="tips" :class="cls.e('tips')">
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
23
|
<!-- 只有表单控件处于非只读状态时,才显示错误提示 -->
|
|
24
|
-
<section :class="cls.e('error')" v-if="!
|
|
24
|
+
<section :class="cls.e('error')" v-if="!readonly && !formProps?.noTips">
|
|
25
25
|
<transition name="form-item-tips" mode="out-in">
|
|
26
|
-
<span :class="cls.e('error-text')" v-if="!!
|
|
27
|
-
{{
|
|
26
|
+
<span :class="cls.e('error-text')" v-if="!!errorTip">
|
|
27
|
+
{{ errorTip }}
|
|
28
28
|
</span>
|
|
29
29
|
</transition>
|
|
30
30
|
</section>
|
|
@@ -33,15 +33,17 @@
|
|
|
33
33
|
</template>
|
|
34
34
|
|
|
35
35
|
<script lang="tsx" setup>
|
|
36
|
+
import { o } from '@cat-kit/core'
|
|
36
37
|
import { useConfig, useFallbackProps } from '@veltra/compositions'
|
|
37
38
|
import { bem, withUnit } from '@veltra/utils'
|
|
38
39
|
import { injectFormContext } from '@veltra/utils'
|
|
39
|
-
import { type CSSProperties, computed } from 'vue'
|
|
40
|
+
import { type CSSProperties, computed, onBeforeUnmount, shallowRef, watch } from 'vue'
|
|
40
41
|
|
|
41
42
|
import type { FormItemProps, ComponentSize } from '../../types'
|
|
42
43
|
import { UGridItem } from '../grid'
|
|
43
44
|
import { UTip } from '../tip'
|
|
44
|
-
import { formItemCls as cls } from './helper'
|
|
45
|
+
import { formItemCls as cls, defineField } from './helper'
|
|
46
|
+
import { validateField } from './validate'
|
|
45
47
|
|
|
46
48
|
defineOptions({ name: 'FormItem' })
|
|
47
49
|
|
|
@@ -54,7 +56,7 @@ defineSlots<{
|
|
|
54
56
|
}>()
|
|
55
57
|
|
|
56
58
|
/** 表单组件上下文 */
|
|
57
|
-
const { formProps } = injectFormContext()
|
|
59
|
+
const { formProps, registerField, unregisterField, shouldValidate } = injectFormContext()
|
|
58
60
|
|
|
59
61
|
const { config } = useConfig()
|
|
60
62
|
|
|
@@ -63,8 +65,14 @@ const { size, readonly } = useFallbackProps([formProps ?? {}, props], {
|
|
|
63
65
|
readonly: false
|
|
64
66
|
})
|
|
65
67
|
|
|
68
|
+
const errorTip = shallowRef<string>()
|
|
69
|
+
/** 递增序号,丢弃过期的异步校验结果 */
|
|
70
|
+
let validateSeq = 0
|
|
71
|
+
/** 最近一次 validate 的 Promise,供过期调用等待最新结果 */
|
|
72
|
+
let latestValidatePromise: Promise<boolean> = Promise.resolve(true)
|
|
73
|
+
|
|
66
74
|
const className = computed(() => {
|
|
67
|
-
return [cls.b, cls.m(size.value), bem.is('error', !!
|
|
75
|
+
return [cls.b, cls.m(size.value), bem.is('error', !!errorTip.value)].join(' ')
|
|
68
76
|
})
|
|
69
77
|
|
|
70
78
|
/** label样式 */
|
|
@@ -74,17 +82,60 @@ const labelStyles = computed<CSSProperties>(() => {
|
|
|
74
82
|
}
|
|
75
83
|
})
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
const fieldItem = defineField({
|
|
86
|
+
clearValidate() {
|
|
87
|
+
errorTip.value = ''
|
|
88
|
+
},
|
|
89
|
+
async validate() {
|
|
90
|
+
if (!props.field || !formProps?.model || !props.rules || !shouldValidate?.()) return true
|
|
91
|
+
|
|
92
|
+
const seq = ++validateSeq
|
|
93
|
+
|
|
94
|
+
latestValidatePromise = validateField(formProps.model!, props.field!, props.rules!).then(
|
|
95
|
+
(tip) => {
|
|
96
|
+
if (seq !== validateSeq) return latestValidatePromise
|
|
97
|
+
|
|
98
|
+
errorTip.value = tip
|
|
99
|
+
return !errorTip.value
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
return latestValidatePromise
|
|
103
|
+
}
|
|
81
104
|
})
|
|
82
105
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
watch(
|
|
107
|
+
() => props.field,
|
|
108
|
+
(field, oldField) => {
|
|
109
|
+
if (oldField) {
|
|
110
|
+
unregisterField?.(oldField)
|
|
111
|
+
}
|
|
112
|
+
if (field) {
|
|
113
|
+
registerField?.(field, fieldItem)
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{ immediate: true }
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
let stopWatchFieldValue: (() => void) | undefined
|
|
120
|
+
|
|
121
|
+
watch(
|
|
122
|
+
[() => formProps?.model, () => props.field],
|
|
123
|
+
([model, field]) => {
|
|
124
|
+
stopWatchFieldValue?.()
|
|
125
|
+
|
|
126
|
+
if (!field || !model) return
|
|
127
|
+
|
|
128
|
+
stopWatchFieldValue = watch(
|
|
129
|
+
() => o(model).get(field),
|
|
130
|
+
() => {
|
|
131
|
+
fieldItem.validate()
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
},
|
|
135
|
+
{ immediate: true }
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
onBeforeUnmount(() => {
|
|
139
|
+
props.field && unregisterField?.(props.field)
|
|
89
140
|
})
|
|
90
141
|
</script>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { o } from '@cat-kit/core'
|
|
2
|
-
import { bem, type BEM, type
|
|
2
|
+
import { bem, type BEM, type FormFieldItem } from '@veltra/utils'
|
|
3
|
+
|
|
4
|
+
import type { FormFieldComponentProps } from '../../types/form-field'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* 获取表单项的属性
|
|
@@ -7,12 +9,16 @@ import { bem, type BEM, type FormComponentProps } from '@veltra/utils'
|
|
|
7
9
|
* @returns
|
|
8
10
|
*/
|
|
9
11
|
export function getFormItemProps(
|
|
10
|
-
props:
|
|
11
|
-
): Pick<
|
|
12
|
+
props: FormFieldComponentProps
|
|
13
|
+
): Pick<FormFieldComponentProps, 'label' | 'field' | 'readonly' | 'span' | 'tips'> {
|
|
12
14
|
return o(props).pick(['label', 'field', 'readonly', 'span', 'tips']) as Pick<
|
|
13
|
-
|
|
15
|
+
FormFieldComponentProps,
|
|
14
16
|
'label' | 'field' | 'readonly' | 'span' | 'tips'
|
|
15
17
|
>
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export const formItemCls: BEM<'form-item'> = bem('form-item')
|
|
21
|
+
|
|
22
|
+
export function defineField(field: FormFieldItem) {
|
|
23
|
+
return field
|
|
24
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { isEmpty, o } from '@cat-kit/core'
|
|
2
|
+
import type { Undef } from '@veltra/utils'
|
|
3
|
+
|
|
4
|
+
export type PresetRule = 'email' | 'phone' | 'num' | 'url' | 'idCard'
|
|
5
|
+
|
|
6
|
+
/** 字段校验规则 */
|
|
7
|
+
export interface ValidateRule {
|
|
8
|
+
/** 是否必填 */
|
|
9
|
+
required?: boolean | string
|
|
10
|
+
/** 长度单位 */
|
|
11
|
+
length?: number | [number, string]
|
|
12
|
+
/** 最小值 */
|
|
13
|
+
min?: number | [number, string]
|
|
14
|
+
/** 最大值 */
|
|
15
|
+
max?: number | [number, string]
|
|
16
|
+
/** 最小长度 */
|
|
17
|
+
minLen?: number | [number, string]
|
|
18
|
+
/** 最大长度 */
|
|
19
|
+
maxLen?: number | [number, string]
|
|
20
|
+
/** 匹配 */
|
|
21
|
+
match?: RegExp | [RegExp, string] | string
|
|
22
|
+
/** 预设 */
|
|
23
|
+
preset?: PresetRule
|
|
24
|
+
/** 自定义校验 */
|
|
25
|
+
validator?: (value: any, data: Record<string, any>) => Promise<string> | string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const presetRules: Record<PresetRule, (value: string) => string | undefined> = {
|
|
29
|
+
email(v) {
|
|
30
|
+
const re = /^([\w_-]+)@([\w-]+[.]?)*[\w]+\.[a-zA-Z]{2,10}$/
|
|
31
|
+
if (!re.test(v)) {
|
|
32
|
+
return '邮箱格式不正确'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
phone(v) {
|
|
36
|
+
const re = /^\d{11}$/
|
|
37
|
+
if (!re.test(v)) {
|
|
38
|
+
return '手机号格式不正确'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
num(v) {
|
|
42
|
+
const re = /^\d+$/
|
|
43
|
+
if (!re.test(v)) {
|
|
44
|
+
return '数字格式不正确'
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
url(v) {
|
|
48
|
+
const re = /^(ftp|https?):\/\/([\w_-]+)\.([\w-]+[.]?)*[\w]+\.[a-zA-Z]{2,10}(.*)/
|
|
49
|
+
if (!re.test(v)) {
|
|
50
|
+
return '链接格式不正确'
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
idCard(v) {
|
|
55
|
+
const re = /^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/
|
|
56
|
+
if (!re.test(v)) {
|
|
57
|
+
return '身份证格式不正确'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 预设规则 */
|
|
63
|
+
const ruleTypes = {
|
|
64
|
+
required(value: any, required: ValidateRule['required']): Undef<string> {
|
|
65
|
+
if (required === false) return
|
|
66
|
+
|
|
67
|
+
const errMsg = typeof required === 'string' ? required : '该项不能为空'
|
|
68
|
+
if (isEmpty(value)) return errMsg
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(value) && !value.length) return errMsg
|
|
71
|
+
if (typeof value === 'string' && !value) return errMsg
|
|
72
|
+
},
|
|
73
|
+
min(value: any, rule: ValidateRule['min']): Undef<string> {
|
|
74
|
+
if (isEmpty(value)) return
|
|
75
|
+
let _rule = Array.isArray(rule) ? rule[0] : rule!
|
|
76
|
+
let errMsg = Array.isArray(rule) ? rule[1] : `该项必须大于等于${_rule}`
|
|
77
|
+
if (typeof value !== 'number') return `${value}不是一个数字`
|
|
78
|
+
if (value < _rule) return errMsg
|
|
79
|
+
},
|
|
80
|
+
max(value: any, rule: ValidateRule['max']): Undef<string> {
|
|
81
|
+
if (isEmpty(value)) return
|
|
82
|
+
let _rule = Array.isArray(rule) ? rule[0] : rule!
|
|
83
|
+
let errMsg = Array.isArray(rule) ? rule[1] : `该项必须小于等于${_rule}`
|
|
84
|
+
if (typeof value !== 'number') return `${value}不是一个数字`
|
|
85
|
+
if (value > _rule) return errMsg
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
minLen(value: any, rule: ValidateRule['minLen']): Undef<string> {
|
|
89
|
+
if (isEmpty(value)) return
|
|
90
|
+
let _rule = Array.isArray(rule) ? rule[0] : rule!
|
|
91
|
+
let errMsg = Array.isArray(rule) ? rule[1] : `该项长度必须大于等于${_rule}`
|
|
92
|
+
if (!Array.isArray(value) && typeof value !== 'string') return `${value}不是一个字符串或数组`
|
|
93
|
+
if (value.length < _rule) return errMsg
|
|
94
|
+
},
|
|
95
|
+
maxLen(value: any, rule: ValidateRule['maxLen']): Undef<string> {
|
|
96
|
+
if (isEmpty(value)) return
|
|
97
|
+
let _rule = Array.isArray(rule) ? rule[0] : rule!
|
|
98
|
+
let errMsg = Array.isArray(rule) ? rule[1] : `该项长度必须小于等于:${_rule}`
|
|
99
|
+
if (!Array.isArray(value) && typeof value !== 'string') return `${value}不是一个字符串或数组`
|
|
100
|
+
if (value.length > _rule) return errMsg
|
|
101
|
+
},
|
|
102
|
+
match(value: any, rule: ValidateRule['match']): Undef<string> {
|
|
103
|
+
if (isEmpty(value) || value === '') return
|
|
104
|
+
if (typeof rule === 'string') {
|
|
105
|
+
if (!rule) return
|
|
106
|
+
rule = new RegExp(rule)
|
|
107
|
+
}
|
|
108
|
+
let _rule = Array.isArray(rule) ? rule[0] : rule!
|
|
109
|
+
let errMsg = Array.isArray(rule) ? rule[1] : `该项不匹配正则:${_rule}`
|
|
110
|
+
if (typeof value !== 'string') return `${value}不是一个字符串`
|
|
111
|
+
if (!_rule.test(value)) return errMsg
|
|
112
|
+
},
|
|
113
|
+
preset(value: any, rule: ValidateRule['preset']): Undef<string> {
|
|
114
|
+
if (isEmpty(value) || value === '' || !rule) return
|
|
115
|
+
if (typeof value !== 'string') return `${value}不是一个字符串`
|
|
116
|
+
const ruleValidator = presetRules[rule]
|
|
117
|
+
return ruleValidator(value)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function validateField(
|
|
122
|
+
formData: Record<string, any>,
|
|
123
|
+
field: string,
|
|
124
|
+
rules: ValidateRule
|
|
125
|
+
): Promise<Undef<string>> {
|
|
126
|
+
const value = o(formData).get(field)
|
|
127
|
+
|
|
128
|
+
if (!rules) return
|
|
129
|
+
|
|
130
|
+
const { validator, required, ...normalRules } = rules
|
|
131
|
+
|
|
132
|
+
// 必填要先去校验
|
|
133
|
+
if (required) {
|
|
134
|
+
const err = ruleTypes.required(value, required)
|
|
135
|
+
if (err) return err
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 校验规则
|
|
139
|
+
for (const ruleKey in normalRules) {
|
|
140
|
+
const validate = ruleTypes[ruleKey as keyof typeof ruleTypes]
|
|
141
|
+
if (!validate) continue
|
|
142
|
+
|
|
143
|
+
const err = validate(value, normalRules[ruleKey])
|
|
144
|
+
|
|
145
|
+
if (err) return err
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 自定义校验最火校验
|
|
149
|
+
if (validator) {
|
|
150
|
+
const err = await validator(value, formData)
|
|
151
|
+
if (err) return err
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return
|
|
155
|
+
}
|