@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.
Files changed (198) hide show
  1. package/dist/components/action/action-group.vue.d.ts +2 -2
  2. package/dist/components/action/action.vue.d.ts +1 -1
  3. package/dist/components/auto-complete/auto-complete.js +1 -0
  4. package/dist/components/auto-complete/auto-complete.js.map +1 -1
  5. package/dist/components/batch-edit/batch-edit-form.js +12 -5
  6. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  7. package/dist/components/batch-edit/batch-edit.js +8 -3
  8. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  9. package/dist/components/batch-edit/batch-edit.vue.d.ts +40 -35
  10. package/dist/components/batch-edit/di.js.map +1 -1
  11. package/dist/components/batch-edit/use-edit.js +41 -23
  12. package/dist/components/batch-edit/use-edit.js.map +1 -1
  13. package/dist/components/cascade/cascade.js +1 -0
  14. package/dist/components/cascade/cascade.js.map +1 -1
  15. package/dist/components/cascade/cascade.vue.d.ts +5 -5
  16. package/dist/components/checkbox/checkbox-button.js +1 -0
  17. package/dist/components/checkbox/checkbox-button.js.map +1 -1
  18. package/dist/components/checkbox/checkbox-button.vue.d.ts +1 -1
  19. package/dist/components/checkbox/checkbox.js +1 -0
  20. package/dist/components/checkbox/checkbox.js.map +1 -1
  21. package/dist/components/checkbox-group/checkbox-group.js +1 -0
  22. package/dist/components/checkbox-group/checkbox-group.js.map +1 -1
  23. package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
  24. package/dist/components/code-editor/code-editor.js +1 -0
  25. package/dist/components/code-editor/code-editor.js.map +1 -1
  26. package/dist/components/condition-editor/core/evaluator.js +3 -3
  27. package/dist/components/condition-editor/core/evaluator.js.map +1 -1
  28. package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
  29. package/dist/components/date-picker/date-picker.js +1 -0
  30. package/dist/components/date-picker/date-picker.js.map +1 -1
  31. package/dist/components/date-picker/date-picker.vue.d.ts +1 -1
  32. package/dist/components/date-range-picker/date-range-picker.js +2 -0
  33. package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
  34. package/dist/components/date-range-picker/date-range-picker.vue.d.ts +1 -1
  35. package/dist/components/dialog/use-maximum.js +7 -1
  36. package/dist/components/dialog/use-maximum.js.map +1 -1
  37. package/dist/components/empty/empty.vue.d.ts +1 -1
  38. package/dist/components/expression-editor/expression-editor.js +1 -0
  39. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  40. package/dist/components/file-picker/file-picker.js +1 -0
  41. package/dist/components/file-picker/file-picker.js.map +1 -1
  42. package/dist/components/form/form.js.map +1 -1
  43. package/dist/components/form/form.vue.d.ts +11 -19
  44. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +24 -26
  45. package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
  46. package/dist/components/form/index.d.ts +0 -3
  47. package/dist/components/form/use-form-fields.js +88 -0
  48. package/dist/components/form/use-form-fields.js.map +1 -0
  49. package/dist/components/form/use-node-interceptor.js +19 -30
  50. package/dist/components/form/use-node-interceptor.js.map +1 -1
  51. package/dist/components/form-item/form-item.js.map +1 -1
  52. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js +44 -18
  53. package/dist/components/form-item/form-item.vue_vue_type_script_setup_true_lang.js.map +1 -1
  54. package/dist/components/form-item/helper.js +4 -1
  55. package/dist/components/form-item/helper.js.map +1 -1
  56. package/dist/components/form-item/validate.d.ts +26 -0
  57. package/dist/components/form-item/validate.js +97 -0
  58. package/dist/components/form-item/validate.js.map +1 -0
  59. package/dist/components/group-input/group-input.js +1 -0
  60. package/dist/components/group-input/group-input.js.map +1 -1
  61. package/dist/components/index.d.ts +0 -3
  62. package/dist/components/index.js +0 -7
  63. package/dist/components/input/input.vue_vue_type_script_setup_true_lang.js +1 -0
  64. package/dist/components/input/input.vue_vue_type_script_setup_true_lang.js.map +1 -1
  65. package/dist/components/loading/style2.css +90 -159
  66. package/dist/components/multi-select/multi-select.js +1 -0
  67. package/dist/components/multi-select/multi-select.js.map +1 -1
  68. package/dist/components/multi-select/multi-select.vue.d.ts +3 -3
  69. package/dist/components/multi-tree-select/multi-tree-select.js +1 -0
  70. package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
  71. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +4 -4
  72. package/dist/components/number-input/number-input.js +1 -0
  73. package/dist/components/number-input/number-input.js.map +1 -1
  74. package/dist/components/number-range-input/number-range-input.js +1 -0
  75. package/dist/components/number-range-input/number-range-input.js.map +1 -1
  76. package/dist/components/palette/palette.js +1 -0
  77. package/dist/components/palette/palette.js.map +1 -1
  78. package/dist/components/password-input/password-input.js +1 -0
  79. package/dist/components/password-input/password-input.js.map +1 -1
  80. package/dist/components/pop-confirm/pop-confirm.vue.d.ts +1 -1
  81. package/dist/components/radio/radio.js +1 -0
  82. package/dist/components/radio/radio.js.map +1 -1
  83. package/dist/components/radio-group/radio-group.js +1 -0
  84. package/dist/components/radio-group/radio-group.js.map +1 -1
  85. package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
  86. package/dist/components/rich-text-editor/rich-text-editor.js +3 -2
  87. package/dist/components/rich-text-editor/rich-text-editor.js.map +1 -1
  88. package/dist/components/rich-text-editor/toolbar.js +10 -7
  89. package/dist/components/rich-text-editor/toolbar.js.map +1 -1
  90. package/dist/components/select/select.js +6 -3
  91. package/dist/components/select/select.js.map +1 -1
  92. package/dist/components/select/select.vue.d.ts +2 -2
  93. package/dist/components/slider/slider.js +15 -2
  94. package/dist/components/slider/slider.js.map +1 -1
  95. package/dist/components/steps/steps.vue.d.ts +1 -1
  96. package/dist/components/switch/switch.js +1 -0
  97. package/dist/components/switch/switch.js.map +1 -1
  98. package/dist/components/table/style2.css +4 -7
  99. package/dist/components/textarea/textarea.js +1 -0
  100. package/dist/components/textarea/textarea.js.map +1 -1
  101. package/dist/components/tip/tip.vue.d.ts +1 -1
  102. package/dist/components/tree-select/tree-select.js +1 -0
  103. package/dist/components/tree-select/tree-select.js.map +1 -1
  104. package/dist/components/tree-select/tree-select.vue.d.ts +4 -4
  105. package/dist/components/watermark/watermark.vue.d.ts +1 -1
  106. package/dist/index.d.ts +5 -6
  107. package/dist/index.js +1 -4
  108. package/dist/types/auto-complete.d.ts +3 -2
  109. package/dist/types/batch-edit.d.ts +4 -5
  110. package/dist/types/cascade.d.ts +3 -2
  111. package/dist/types/checkbox-group.d.ts +2 -2
  112. package/dist/types/checkbox.d.ts +4 -3
  113. package/dist/types/code-editor.d.ts +3 -2
  114. package/dist/types/date-picker.d.ts +3 -2
  115. package/dist/types/date-range-picker.d.ts +3 -2
  116. package/dist/types/expression-editor.d.ts +3 -2
  117. package/dist/types/file-picker.d.ts +3 -2
  118. package/dist/types/form-field.d.ts +12 -0
  119. package/dist/types/form-item.d.ts +2 -2
  120. package/dist/types/form.d.ts +9 -48
  121. package/dist/types/group-input.d.ts +3 -2
  122. package/dist/types/index.d.ts +4 -2
  123. package/dist/types/input.d.ts +3 -2
  124. package/dist/types/loading.d.ts +1 -1
  125. package/dist/types/multi-select.d.ts +3 -2
  126. package/dist/types/multi-tree-select.d.ts +3 -2
  127. package/dist/types/palette.d.ts +3 -2
  128. package/dist/types/radio-group.d.ts +3 -2
  129. package/dist/types/radio.d.ts +3 -2
  130. package/dist/types/rich-text-editor.d.ts +3 -2
  131. package/dist/types/select.d.ts +3 -2
  132. package/dist/types/slider.d.ts +3 -2
  133. package/dist/types/switch.d.ts +3 -2
  134. package/dist/types/text-editor.d.ts +3 -2
  135. package/dist/types/textarea.d.ts +3 -2
  136. package/dist/types/tree-select.d.ts +3 -2
  137. package/package.json +6 -6
  138. package/src/components/batch-edit/batch-edit-form.vue +20 -13
  139. package/src/components/batch-edit/batch-edit.vue +7 -8
  140. package/src/components/batch-edit/di.ts +1 -2
  141. package/src/components/batch-edit/use-edit.ts +58 -36
  142. package/src/components/date-range-picker/date-range-picker.vue +2 -0
  143. package/src/components/dialog/use-maximum.ts +8 -1
  144. package/src/components/form/form.vue +22 -38
  145. package/src/components/form/index.ts +0 -6
  146. package/src/components/form/use-form-fields.ts +126 -0
  147. package/src/components/form/use-node-interceptor.ts +27 -56
  148. package/src/components/form-item/form-item.vue +69 -18
  149. package/src/components/form-item/helper.ts +10 -4
  150. package/src/components/form-item/validate.ts +155 -0
  151. package/src/components/loading/style.scss +126 -355
  152. package/src/components/rich-text-editor/rich-text-editor.vue +28 -25
  153. package/src/components/rich-text-editor/toolbar.vue +23 -19
  154. package/src/components/select/select.vue +6 -4
  155. package/src/components/slider/slider.vue +12 -2
  156. package/src/components/table/style.scss +4 -14
  157. package/src/types/auto-complete.ts +4 -2
  158. package/src/types/batch-edit.ts +4 -5
  159. package/src/types/cascade.ts +4 -2
  160. package/src/types/checkbox-group.ts +2 -2
  161. package/src/types/checkbox.ts +5 -3
  162. package/src/types/code-editor.ts +4 -2
  163. package/src/types/date-picker.ts +4 -2
  164. package/src/types/date-range-picker.ts +4 -2
  165. package/src/types/expression-editor.ts +4 -2
  166. package/src/types/file-picker.ts +4 -2
  167. package/src/types/form-field.ts +11 -0
  168. package/src/types/form-item.ts +2 -2
  169. package/src/types/form.ts +9 -82
  170. package/src/types/group-input.ts +4 -2
  171. package/src/types/index.ts +1 -0
  172. package/src/types/input.ts +4 -2
  173. package/src/types/loading.ts +1 -1
  174. package/src/types/multi-select.ts +4 -2
  175. package/src/types/multi-tree-select.ts +5 -2
  176. package/src/types/palette.ts +4 -2
  177. package/src/types/radio-group.ts +4 -2
  178. package/src/types/radio.ts +4 -2
  179. package/src/types/rich-text-editor.ts +4 -2
  180. package/src/types/select.ts +4 -2
  181. package/src/types/slider.ts +4 -2
  182. package/src/types/switch.ts +4 -2
  183. package/src/types/text-editor.ts +4 -2
  184. package/src/types/textarea.ts +4 -2
  185. package/src/types/tree-select.ts +5 -2
  186. package/dist/components/form/dynamic-form-model.d.ts +0 -73
  187. package/dist/components/form/dynamic-form-model.js +0 -184
  188. package/dist/components/form/dynamic-form-model.js.map +0 -1
  189. package/dist/components/form/form-model.d.ts +0 -73
  190. package/dist/components/form/form-model.js +0 -179
  191. package/dist/components/form/form-model.js.map +0 -1
  192. package/dist/components/form/helper.d.ts +0 -16
  193. package/dist/components/form/helper.js +0 -20
  194. package/dist/components/form/helper.js.map +0 -1
  195. package/src/components/form/__test__/form-model.test.ts +0 -190
  196. package/src/components/form/dynamic-form-model.ts +0 -258
  197. package/src/components/form/form-model.ts +0 -255
  198. 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 { createIncrease, extractNormalVNodes } from '@veltra/utils'
3
- import { onBeforeUnmount, useSlots, type VNode } from 'vue'
4
-
5
- import type { FormProps } from '../../types'
6
-
7
- interface Options {
8
- props: FormProps
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(options: Options): {
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 { model } = props
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 item = {
61
- isFormItem: (type as any)?.name === 'FormItem',
62
- formItemProps: o((props ?? {}) as Record<string, any>).pick([
63
- 'label',
64
- 'field',
65
- 'span',
66
- 'tips',
67
- 'readonly'
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: props?.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', fieldRequired)]"
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="!formProps?.readonly && !formProps?.noTips">
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="!!errorTips">
27
- {{ errorTips }}
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', !!errorTips.value)].join(' ')
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
- const errorTips = computed<string | undefined>(() => {
79
- if (!props.field) return undefined
80
- return formProps?.model?.errors.get(props.field)?.[0]
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
- const fieldRequired = computed<boolean>(() => {
85
- const { field } = props
86
- if (!field || readonly.value) return false
87
- const required = formProps?.model?.fields[field]?.required
88
- return Boolean(required)
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 FormComponentProps } from '@veltra/utils'
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: FormComponentProps
11
- ): Pick<FormComponentProps, 'label' | 'field' | 'readonly' | 'span' | 'tips'> {
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
- FormComponentProps,
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
+ }