@antsoo-lib/core 2.0.0 → 2.0.3

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 (73) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core.css +1 -0
  3. package/dist/index.cjs +57 -0
  4. package/dist/index.js +4154 -0
  5. package/dist/types/BaseSearch/index.d.ts +59 -0
  6. package/dist/types/BaseTable/index.d.ts +204 -0
  7. package/dist/types/Form/CoreForm.d.ts +82 -0
  8. package/dist/types/Form/types.d.ts +57 -0
  9. package/dist/types/SSelectPage/index.d.ts +102 -0
  10. package/dist/types/index.d.ts +13 -0
  11. package/dist/types/render/AreaCascader.d.ts +5 -0
  12. package/dist/types/render/AutoComplete.d.ts +4 -0
  13. package/dist/types/render/Button.d.ts +4 -0
  14. package/dist/types/render/Cascader.d.ts +5 -0
  15. package/dist/types/render/Checkbox.d.ts +4 -0
  16. package/dist/types/render/CheckboxGroup.d.ts +4 -0
  17. package/dist/types/render/Custom.d.ts +8 -0
  18. package/dist/types/render/DatePicker.d.ts +4 -0
  19. package/dist/types/render/Input.d.ts +4 -0
  20. package/dist/types/render/InputGroup.d.ts +5 -0
  21. package/dist/types/render/InputNumber.d.ts +4 -0
  22. package/dist/types/render/InputPassword.d.ts +4 -0
  23. package/dist/types/render/InputRange.d.ts +9 -0
  24. package/dist/types/render/RadioGroup.d.ts +4 -0
  25. package/dist/types/render/Select.d.ts +4 -0
  26. package/dist/types/render/SselectPage.d.ts +4 -0
  27. package/dist/types/render/Switch.d.ts +4 -0
  28. package/dist/types/render/Tree.d.ts +9 -0
  29. package/dist/types/render/TreeSelect.d.ts +4 -0
  30. package/dist/types/render/Upload.d.ts +4 -0
  31. package/dist/types/render/helper.d.ts +10 -0
  32. package/dist/types/render/index.d.ts +43 -0
  33. package/dist/types/render/registry.d.ts +9 -0
  34. package/dist/types/render/state.d.ts +19 -0
  35. package/dist/types/render/types.d.ts +435 -0
  36. package/dist/types/utils/attrMapping.d.ts +26 -0
  37. package/package.json +10 -12
  38. package/src/BaseSearch/index.vue +371 -0
  39. package/src/BaseTable/index.vue +62 -36
  40. package/src/Form/CoreForm.vue +782 -0
  41. package/src/Form/types.ts +86 -0
  42. package/src/SSelectPage/index.vue +607 -0
  43. package/src/index.ts +15 -1
  44. package/src/{BaseTable/renderers → render}/AreaCascader.tsx +3 -3
  45. package/src/{BaseTable/renderers → render}/AutoComplete.tsx +3 -3
  46. package/src/{BaseTable/renderers → render}/Button.tsx +2 -2
  47. package/src/{BaseTable/renderers → render}/Cascader.tsx +3 -3
  48. package/src/{BaseTable/renderers → render}/Checkbox.tsx +2 -2
  49. package/src/{BaseTable/renderers → render}/CheckboxGroup.tsx +2 -2
  50. package/src/render/Custom.tsx +19 -0
  51. package/src/{BaseTable/renderers → render}/DatePicker.tsx +2 -2
  52. package/src/{BaseTable/renderers → render}/Input.tsx +3 -3
  53. package/src/{BaseTable/renderers → render}/InputGroup.tsx +3 -3
  54. package/src/{BaseTable/renderers → render}/InputNumber.tsx +3 -3
  55. package/src/{BaseTable/renderers → render}/InputPassword.tsx +3 -3
  56. package/src/render/InputRange.tsx +154 -0
  57. package/src/{BaseTable/renderers → render}/RadioGroup.tsx +2 -2
  58. package/src/{BaseTable/renderers → render}/Select.tsx +2 -2
  59. package/src/{BaseTable/renderers → render}/SselectPage.tsx +3 -3
  60. package/src/{BaseTable/renderers → render}/Switch.tsx +2 -2
  61. package/src/render/Tree.tsx +136 -0
  62. package/src/{BaseTable/renderers → render}/TreeSelect.tsx +2 -2
  63. package/src/{BaseTable/renderers → render}/Upload.tsx +4 -5
  64. package/src/{BaseTable/utils.tsx → render/helper.tsx} +86 -9
  65. package/src/{BaseTable/renderers → render}/index.ts +45 -4
  66. package/src/{BaseTable → render}/types.ts +62 -2
  67. package/src/utils/attrMapping.ts +106 -0
  68. package/vite.config.ts +15 -2
  69. package/index.css +0 -2
  70. package/index.ts +0 -21
  71. package/src/BaseTable/helpers.tsx +0 -91
  72. /package/src/{BaseTable → render}/registry.ts +0 -0
  73. /package/src/{BaseTable → render}/state.ts +0 -0
@@ -0,0 +1,782 @@
1
+ <script lang="ts" setup>
2
+ import { Col, Form, FormItem, FormItemRest, Row, Tooltip } from '@antsoo-lib/components'
3
+ import { QuestionCircleOutlined } from '@antsoo-lib/icons'
4
+ import type { AnyObject } from '@antsoo-lib/shared'
5
+ import { env } from '@antsoo-lib/utils'
6
+ import { isBoolean, isEqual, isFunction } from 'lodash-es'
7
+
8
+ import { computed, inject, nextTick, ref, watch } from 'vue'
9
+
10
+ import { rendererMap } from '../render'
11
+ import { useToolbarState } from '../render/state'
12
+ import { applyAttrMapping, buildValueFromAttr } from '../utils/attrMapping'
13
+ import { COMMON_CODE_SERVICE_KEY } from './types'
14
+ import type { BaseFormProps, FormField } from './types'
15
+
16
+ const props = withDefaults(defineProps<BaseFormProps>(), {
17
+ value: () => ({}),
18
+ fields: () => [],
19
+ disabled: false,
20
+ labelWidth: 90,
21
+ labelAlign: 'right',
22
+ labelPosition: 'horizontal',
23
+ inlineActions: false,
24
+ actionsSpan: 8,
25
+ colon: false,
26
+ gutter: 0,
27
+ })
28
+ const emit = defineEmits<{
29
+ 'update:value': [value: AnyObject]
30
+ change: [value: AnyObject]
31
+ }>()
32
+ const { isDev, isTest } = env()
33
+ const showHelpText = isDev || isTest
34
+
35
+ // 表单引用
36
+ const formRef = ref()
37
+ const isUpdatingFromProps = ref(false)
38
+ // 表单数据 - 合并默认值
39
+ const formData = ref<AnyObject>({})
40
+
41
+ // 初始化 commonCode store(可选注入:业务未提供时回退为空实现)
42
+ const commonCodeStore = inject(COMMON_CODE_SERVICE_KEY, {
43
+ getCodesByType: () => [],
44
+ })
45
+
46
+ // 创建工具栏状态实例
47
+ const toolbarState = useToolbarState()
48
+
49
+ /**
50
+ * 获取表单字段的属性值
51
+ */
52
+ const getFieldTitle = (field: FormField) => {
53
+ if (field.type === 'datePicker' && field.attr) {
54
+ return field.attr?.join(', ')
55
+ }
56
+ if (field.type === 'areaCascader' && field.attr) {
57
+ const { ids = [], names = [] } = (field.attr as AnyObject) || {}
58
+ return ids.concat(names).join(', ')
59
+ }
60
+ return field.key
61
+ }
62
+
63
+ /**
64
+ * 获取 helpText 的显示内容
65
+ */
66
+ const getHelpText = (field: FormField) => {
67
+ // 显式设置为 false 时不显示
68
+ if (field.helpText === false) return null
69
+ // 有值则显示
70
+ if (field.helpText) return field.helpText
71
+ // 开发/测试环境下,默认显示 key 或 attr 用于辅助调试
72
+ if (showHelpText) return getFieldTitle(field)
73
+
74
+ return null
75
+ }
76
+
77
+ // 生成表单校验规则
78
+ const formRules = computed(() => {
79
+ const rules: Record<string, any> = {}
80
+ props.fields.forEach((field) => {
81
+ if (!field.key) return
82
+ if (field.rules && field.rules.length > 0) {
83
+ rules[field.key] = field.rules.map((rule) => ({
84
+ ...rule,
85
+ message: rule.message || `请输入${field.name}`,
86
+ trigger: rule.trigger || 'change',
87
+ }))
88
+ } else if (field.required) {
89
+ // 如果没有自定义规则但是必填,添加默认必填规则
90
+ rules[field.key] = [
91
+ {
92
+ required: true,
93
+ message: `请输入${field.name}`,
94
+ trigger: 'change',
95
+ },
96
+ ]
97
+ }
98
+ })
99
+ return rules
100
+ })
101
+
102
+ // 初始化表单数据
103
+ const initFormData = () => {
104
+ const initialData = { ...props.value }
105
+ // 设置字段默认值
106
+ props.fields.forEach((field) => {
107
+ let val = field.props?.value
108
+ if (val === undefined) return
109
+
110
+ // 尝试格式化 dayjs 对象
111
+ const format = field.props?.valueFormat
112
+ const formatVal = (v: any) =>
113
+ format && v && typeof v.format === 'function' ? v.format(format) : v
114
+
115
+ if (Array.isArray(val)) {
116
+ val = val.map(formatVal).filter((v) => v !== null && v !== undefined && v !== '')
117
+ } else {
118
+ val = formatVal(val)
119
+ }
120
+
121
+ if (field.key) {
122
+ if (initialData[field.key] === undefined) {
123
+ if (val !== undefined && (Array.isArray(val) ? val.length > 0 : true)) {
124
+ initialData[field.key] = val
125
+ } else if (
126
+ (field.type === 'select' || field.type === 'sselectPage') &&
127
+ (field.props?.mode === 'multiple' || field.props?.mode === 'tags')
128
+ ) {
129
+ initialData[field.key] = []
130
+ } else if (field.type === 'checkboxGroup' || field.type === 'upload') {
131
+ initialData[field.key] = []
132
+ } else if (
133
+ field.type === 'treeSelect' &&
134
+ (field.props?.multiple || field.props?.treeCheckable)
135
+ ) {
136
+ initialData[field.key] = []
137
+ }
138
+ }
139
+ } else if (Array.isArray(field.attr)) {
140
+ // 处理 attr 映射(主要用于 datePicker range 模式)
141
+ field.attr.forEach((targetKey: string, idx: number) => {
142
+ if (targetKey && initialData[targetKey] === undefined) {
143
+ initialData[targetKey] = Array.isArray(val) ? val[idx] : val
144
+ }
145
+ })
146
+ }
147
+ })
148
+ formData.value = { ...initialData }
149
+ Object.keys(formData.value).forEach((key) => {
150
+ toolbarState.setValue(key, formData.value[key]) // 同步更新toolbarState
151
+ })
152
+ }
153
+
154
+ // 监听props.value变化
155
+ watch(
156
+ () => props.value,
157
+ (newVal) => {
158
+ if (isEqual(newVal, formData.value)) return
159
+ isUpdatingFromProps.value = true
160
+ // 覆盖表单数据(不直接写入 toolbarState,避免非渲染周期的副作用导致 UI 异常)
161
+ formData.value = { ...newVal }
162
+ nextTick(() => {
163
+ isUpdatingFromProps.value = false
164
+ })
165
+ },
166
+ { deep: true, immediate: true },
167
+ )
168
+
169
+ // 监听formData变化并触发事件
170
+ watch(
171
+ formData,
172
+ (newVal) => {
173
+ if (!isUpdatingFromProps.value) {
174
+ Object.keys(newVal).forEach((key) => {
175
+ toolbarState.setValue(key, newVal[key])
176
+ })
177
+ emit('update:value', { ...newVal })
178
+ emit('change', { ...newVal })
179
+ }
180
+ },
181
+ { deep: true },
182
+ )
183
+
184
+ // 过滤隐藏的字段
185
+ const visibleFields = computed(() => {
186
+ return props.fields.filter((field) => {
187
+ if (isBoolean(field.beforeCreate)) {
188
+ return field.beforeCreate
189
+ }
190
+ if (isFunction(field.beforeCreate) && !field.beforeCreate(field)) {
191
+ return false
192
+ }
193
+ return true
194
+ })
195
+ })
196
+
197
+ // 渲染表单字段
198
+ const renderField = (field: FormField) => {
199
+ // 处理动态 props:支持函数式配置
200
+ let resolvedProps = field.props
201
+ if (isFunction(resolvedProps)) {
202
+ resolvedProps = resolvedProps(formData.value, toolbarState, field)
203
+ }
204
+ // 确保 resolvedProps 为对象
205
+ if (!resolvedProps) resolvedProps = {}
206
+
207
+ // 处理 commonCode 字段,为 select 类型自动添加选项
208
+ let processedField = { ...field, props: resolvedProps }
209
+
210
+ if (field.commonCode && field.type === 'select') {
211
+ const codes = commonCodeStore ? commonCodeStore.getCodesByType(field.commonCode) : []
212
+ const options = codes.map((item) => ({
213
+ label: item.name,
214
+ value: item.code,
215
+ }))
216
+
217
+ processedField = {
218
+ ...processedField,
219
+ props: {
220
+ ...processedField.props,
221
+ options,
222
+ },
223
+ }
224
+ }
225
+
226
+ // 计算通用初始值(不引入任何特定业务逻辑):
227
+ // 1) 优先使用 key 对应的值
228
+ // 2) 若未设置 key,但存在 attr 映射,则从 formData 中根据 attr 规则聚合初始值
229
+ // 3) 当 key 与 attr 同时存在时:使用 key 作为 value,attr 原样透传给组件(仅在组件需要时)
230
+ const deriveInitialValue = () => {
231
+ const hasKey = field.key !== undefined && field.key !== ''
232
+ const hasAttr = !!processedField.attr
233
+
234
+ // 情况一:仅有 key,直接返回 key 对应的值(若未设置则回退 props.value)
235
+ if (hasKey && !hasAttr) {
236
+ const keyVal = formData.value[field.key!]
237
+ return keyVal !== undefined ? keyVal : processedField.props?.value
238
+ }
239
+
240
+ // 情况二:仅有 attr(维持已有逻辑,使用 attr 生成组件初始值)
241
+ if (!hasKey && hasAttr) {
242
+ const attr = processedField.attr as any
243
+ // 使用通用构造函数统一处理对象映射与分组数组映射
244
+ return buildValueFromAttr(formData.value, attr)
245
+ }
246
+
247
+ // 情况三:key 与 attr 同时存在
248
+ // 特例:对于 areaCascader,优先根据 attr 从 formData 聚合对象,用于路径回显(省/市/区)
249
+ if (hasKey && hasAttr) {
250
+ if (processedField.type === 'areaCascader') {
251
+ const byAttr = buildValueFromAttr(formData.value, processedField.attr as any)
252
+ if (byAttr !== undefined) return byAttr
253
+ }
254
+ const keyVal = formData.value[field.key!]
255
+ return keyVal !== undefined ? keyVal : processedField.props?.value
256
+ }
257
+
258
+ // 情况四:二者都不存在,回退到 props.value
259
+ return processedField.props?.value
260
+ }
261
+
262
+ // 合并字段属性
263
+ const fieldConfig = {
264
+ ...processedField,
265
+ props: {
266
+ ...processedField.props,
267
+ value: deriveInitialValue(),
268
+ // 情况三:仅当组件类型为 sselectPage 且同时存在 key 与 attr 时,将 attr 以 props 方式透传给组件
269
+ ...(processedField.type === 'sselectPage' && processedField.attr && processedField.key
270
+ ? { attr: processedField.attr }
271
+ : {}),
272
+ },
273
+ // 全局禁用状态优先,若未全局禁用则使用字段级别的禁用状态
274
+ disabled: props.disabled
275
+ ? true
276
+ : isFunction(field.disabled)
277
+ ? !!field.disabled(formData.value, toolbarState, processedField)
278
+ : field.disabled !== undefined
279
+ ? field.disabled
280
+ : false,
281
+ events: {
282
+ // 预先捕获用户自定义事件,避免被覆盖
283
+ // 注意:不要在最终 events 对象中再次展开这两个键,否则会覆盖拦截器
284
+ ...(() => {
285
+ const { events = {} } = processedField as any
286
+ const {
287
+ 'onUpdate:value': userOnUpdateValue,
288
+ onChange: userOnChange,
289
+ ...rest
290
+ } = events as AnyObject
291
+ // 将除 onUpdate:value / onChange 外的用户事件原样透传
292
+ return rest
293
+ })(),
294
+ // 添加默认的值更新事件(组件中立):支持 key 存储或按 attr 映射拆分
295
+ 'onUpdate:value': (value: any) => {
296
+ const attr = processedField.attr as any
297
+
298
+ // 若存在 attr 映射:
299
+ // - 对数组映射保持原有写入逻辑
300
+ // - 对对象映射:
301
+ // a) 若符合旧版 { ids: string[]; names: string[] } 结构,仍按数组下标写入(保持兼容)
302
+ // b) 其他对象映射,优先由 onChange 携带的 option 进行写入(此处仅更新 key)
303
+ if (attr) {
304
+ if (Array.isArray(attr)) {
305
+ const isObj = value && typeof value === 'object' && !Array.isArray(value)
306
+ const isArr = Array.isArray(value)
307
+ ;(attr as string[]).forEach((targetKey: string, idx: number) => {
308
+ if (!targetKey) return
309
+ const v = isArr
310
+ ? (value as any[])[idx]
311
+ : isObj
312
+ ? (value as any)[targetKey]
313
+ : undefined
314
+ formData.value[targetKey] = v
315
+ toolbarState.setValue(targetKey, v)
316
+ })
317
+
318
+ // 用户自定义回调
319
+ if (field.events?.['onUpdate:value']) {
320
+ try {
321
+ field.events['onUpdate:value'](value, formData.value, toolbarState)
322
+ } catch (e) {
323
+ console.warn('onUpdate:value user handler error:', e)
324
+ }
325
+ }
326
+ return
327
+ } else if (
328
+ typeof attr === 'object' &&
329
+ (Array.isArray((attr as any).ids) || Array.isArray((attr as any).names))
330
+ ) {
331
+ // 兼容旧版对象映射:{ ids: string[]; names: string[] }
332
+ const isObj = value && typeof value === 'object' && !Array.isArray(value)
333
+ const ids: string[] = Array.isArray(attr?.ids) ? attr.ids : []
334
+ const names: string[] = Array.isArray(attr?.names) ? attr.names : []
335
+ ids.forEach((targetKey: string, idx: number) => {
336
+ if (!targetKey) return
337
+ const v = isObj
338
+ ? (value as any)[targetKey]
339
+ : Array.isArray(value)
340
+ ? (value as any[])[idx]
341
+ : undefined
342
+ formData.value[targetKey] = v
343
+ toolbarState.setValue(targetKey, v)
344
+ })
345
+ names.forEach((targetKey: string) => {
346
+ if (!targetKey) return
347
+ const v = isObj ? (value as any)[targetKey] : undefined // 数组模式无法携带名称
348
+ formData.value[targetKey] = v
349
+ toolbarState.setValue(targetKey, v)
350
+ })
351
+
352
+ if (field.events?.['onUpdate:value']) {
353
+ try {
354
+ field.events['onUpdate:value'](value, formData.value, toolbarState)
355
+ } catch (e) {
356
+ console.warn('onUpdate:value user handler error:', e)
357
+ }
358
+ }
359
+ return
360
+ }
361
+ }
362
+
363
+ // 未配置 attr:回退到 key 字段(若存在)
364
+ if (processedField.key) {
365
+ // 输入框类型在 update 时不做 trim,在 onChange 时 trim
366
+ formData.value[processedField.key] = value
367
+ toolbarState.setValue(processedField.key, value)
368
+ }
369
+
370
+ // 如果有自定义事件,也要调用(使用预先捕获的用户事件)
371
+ const userHandler = (processedField as any)?.events?.['onUpdate:value']
372
+ if (userHandler) userHandler(value, formData.value, toolbarState)
373
+ },
374
+ // 新增:通用 onChange 事件拦截,用于对象映射(特别是 SselectPage)根据 option 写入 formData
375
+ onChange: (value: any, option: any, allOptions?: any) => {
376
+ const shouldAutoValidateOnChange = processedField.type !== 'upload'
377
+
378
+ // 处理输入框 trim 逻辑
379
+ if (processedField.type === 'input') {
380
+ let val: string | undefined
381
+ // 兼容事件对象
382
+ if (value && typeof value === 'object' && 'target' in value) {
383
+ val = value.target?.value
384
+ } else if (typeof value === 'string') {
385
+ val = value
386
+ }
387
+
388
+ if (typeof val === 'string') {
389
+ const trimmedVal = val.trim()
390
+ // 更新表单数据
391
+ if (processedField.key) {
392
+ formData.value[processedField.key] = trimmedVal
393
+ toolbarState.setValue(processedField.key, trimmedVal)
394
+ }
395
+ // 更新传递给用户的 value
396
+ value = trimmedVal
397
+ }
398
+ }
399
+
400
+ // 仅在存在 attr 对象映射时执行,保持对其他组件的兼容
401
+ const attr = processedField.attr as any
402
+ if (attr && typeof attr === 'object' && !Array.isArray(attr)) {
403
+ try {
404
+ // 识别是否为多选
405
+ const isMultiple = Array.isArray(value)
406
+ applyAttrMapping(formData.value, toolbarState, attr, value, option, isMultiple)
407
+ try {
408
+ const mappedKeys = Object.entries(attr)
409
+ .filter(
410
+ ([k, v]) => typeof v === 'string' && typeof k === 'string' && k.trim().length > 0,
411
+ )
412
+ .map(([k]) => k)
413
+ const extraKeys = processedField.key ? [processedField.key] : []
414
+ const validateKeys = Array.from(new Set([...mappedKeys, ...extraKeys]))
415
+ if (validateKeys.length) {
416
+ if (shouldAutoValidateOnChange) {
417
+ nextTick(() => {
418
+ formRef.value?.validateFields(validateKeys)
419
+ })
420
+ }
421
+ }
422
+ } catch {}
423
+ } catch (e) {
424
+ console.warn('onChange attr mapping error:', e)
425
+ }
426
+ } else if (processedField?.key) {
427
+ try {
428
+ if (shouldAutoValidateOnChange) {
429
+ nextTick(() => {
430
+ formRef.value?.validateFields([processedField.key as string])
431
+ })
432
+ }
433
+ } catch {}
434
+ }
435
+
436
+ const userOnChange = (processedField as any)?.events?.onChange
437
+ if (userOnChange) {
438
+ try {
439
+ userOnChange(value, option, allOptions, formData.value, toolbarState)
440
+ } catch (e) {
441
+ console.warn('onChange user handler error:', e)
442
+ }
443
+ }
444
+ },
445
+ // 其余事件已在顶部透传,这里不再展开 field.events,避免覆盖拦截器
446
+ },
447
+ }
448
+
449
+ if (!field.type) return
450
+ // 保持组件渲染通用性:将当前表单的全量值透传给渲染器(第三参数),避免渲染器仅依赖 toolbarState 的缓存
451
+ return (rendererMap as any)[field.type](fieldConfig, toolbarState, formData.value)
452
+ }
453
+
454
+ // 解析目标字段集合:以表单 fields 为基础,附加传入的额外字段(若有)
455
+ const resolveTargetKeys = (extraKeys?: string[]) => {
456
+ const baseKeys: string[] = Array.isArray(props.fields)
457
+ ? props.fields.map((f) => f.key).filter((k): k is string => !!k)
458
+ : []
459
+ const extras: string[] = Array.isArray(extraKeys) && extraKeys.length > 0 ? extraKeys : []
460
+ return Array.from(new Set([...baseKeys, ...extras]))
461
+ }
462
+
463
+ const getFormValue = (key?: string[]) => {
464
+ const targetKeys = resolveTargetKeys(key)
465
+ const val: AnyObject = {}
466
+ targetKeys.forEach((k) => {
467
+ val[k] = formData.value[k]
468
+ })
469
+ return val
470
+ }
471
+
472
+ const setFormValue = (values: AnyObject, key?: string[]) => {
473
+ const targetKeys = resolveTargetKeys(key)
474
+ targetKeys.forEach((k) => {
475
+ if (Object.prototype.hasOwnProperty.call(values, k)) {
476
+ const v = values[k]
477
+ formData.value[k] = v
478
+ toolbarState.setValue(k, v)
479
+ }
480
+ })
481
+ }
482
+
483
+ // 新增:统一的存取方法(接收对象,返回对象),用于按指定字段读取或写入表单值
484
+ interface GetAccessParams {
485
+ operation: 'get'
486
+ keys: string[] | string
487
+ }
488
+ interface SetAccessParams {
489
+ operation: 'set'
490
+ keys: string[] | string
491
+ values: AnyObject
492
+ }
493
+ type AccessFormFieldsParams = GetAccessParams | SetAccessParams
494
+ type AccessFormFieldsResult =
495
+ | { success: true; operation: 'get'; keys: string[]; data: AnyObject; missingKeys?: string[] }
496
+ | { success: true; operation: 'set'; keys: string[]; applied: string[]; skipped: string[] }
497
+ | { success: false; message: string }
498
+
499
+ function accessFormFields(params: AccessFormFieldsParams): AccessFormFieldsResult {
500
+ // 规范化 keys:支持 string 或 string[];去重并过滤空白键
501
+ const rawKeys = typeof params.keys === 'string' ? [params.keys] : params.keys
502
+ const targetKeys = Array.isArray(rawKeys)
503
+ ? Array.from(new Set(rawKeys.filter((k) => typeof k === 'string' && k.trim().length > 0)))
504
+ : []
505
+ if (targetKeys.length === 0) {
506
+ return { success: false, message: 'accessFormFields: 必须提供非空的 keys(字符串或数组)' }
507
+ }
508
+
509
+ // 读取模式:返回指定字段的值集合,仅限于传入的 keys
510
+ if (params.operation === 'get') {
511
+ const data: AnyObject = {}
512
+ const missingKeys: string[] = []
513
+ for (const k of targetKeys) {
514
+ if (Object.prototype.hasOwnProperty.call(formData.value, k)) data[k] = formData.value[k]
515
+ else missingKeys.push(k)
516
+ }
517
+ const result: {
518
+ success: true
519
+ operation: 'get'
520
+ keys: string[]
521
+ data: AnyObject
522
+ missingKeys?: string[]
523
+ } = {
524
+ success: true,
525
+ operation: 'get',
526
+ keys: targetKeys,
527
+ data,
528
+ }
529
+ if (missingKeys.length) result.missingKeys = missingKeys
530
+ return result
531
+ }
532
+
533
+ // 写入模式:仅对传入的 keys 进行写入;未在 values 中出现的键将被跳过
534
+ if (params.operation === 'set') {
535
+ const values = (params as SetAccessParams).values
536
+ if (!values || typeof values !== 'object')
537
+ return { success: false, message: 'accessFormFields: 写入操作需要提供有效的 values 对象' }
538
+ const applied: string[] = []
539
+ const skipped: string[] = []
540
+ for (const k of targetKeys) {
541
+ if (!Object.prototype.hasOwnProperty.call(values, k)) {
542
+ skipped.push(k)
543
+ continue
544
+ }
545
+ const v = values[k]
546
+ formData.value[k] = v
547
+ toolbarState.setValue(k, v)
548
+ applied.push(k)
549
+ }
550
+ return { success: true, operation: 'set', keys: targetKeys, applied, skipped }
551
+ }
552
+
553
+ return { success: false, message: 'accessFormFields: 不支持的 operation 类型' }
554
+ }
555
+
556
+ const resetFormValue = (key?: string[]) => {
557
+ const targetKeys = resolveTargetKeys(key)
558
+ targetKeys.forEach((k) => {
559
+ // 尝试在 fields 中找到对应配置,以便根据类型/props重置默认值
560
+ const field = Array.isArray(props.fields) ? props.fields.find((f) => f.key === k) : undefined
561
+ let resetValue: any
562
+ if (field && field.props?.value !== undefined) {
563
+ resetValue = field.props.value
564
+ } else if (
565
+ field &&
566
+ (field.type === 'select' || field.type === 'sselectPage') &&
567
+ (field.props?.mode === 'multiple' || field.props?.mode === 'tags')
568
+ ) {
569
+ // 多选/标签模式默认重置为空数组,避免出现空标签
570
+ resetValue = []
571
+ } else if (field && (field.type === 'checkboxGroup' || field.type === 'upload')) {
572
+ resetValue = []
573
+ } else if (
574
+ field &&
575
+ field.type === 'treeSelect' &&
576
+ (field.props?.multiple || field.props?.treeCheckable)
577
+ ) {
578
+ resetValue = []
579
+ } else {
580
+ resetValue = undefined
581
+ }
582
+ formData.value[k] = resetValue
583
+ toolbarState.setValue(k, resetValue)
584
+ })
585
+ // 清除校验状态
586
+ formRef.value?.clearValidate()
587
+ }
588
+
589
+ // 校验表单
590
+ const validateForm = async (): Promise<{ valid: boolean; errors?: any }> => {
591
+ try {
592
+ await formRef.value?.validate()
593
+ return { valid: true }
594
+ } catch (errors) {
595
+ return { valid: false, errors }
596
+ }
597
+ }
598
+
599
+ // 校验指定字段
600
+ const validateFields = async (fieldKey: string[]): Promise<{ valid: boolean; errors?: any }> => {
601
+ try {
602
+ await formRef.value?.validateFields(fieldKey)
603
+ return { valid: true }
604
+ } catch (errors) {
605
+ return { valid: false, errors }
606
+ }
607
+ }
608
+
609
+ // 清除校验状态
610
+ const clearValidate = (fieldKeys?: string[]) => {
611
+ if (fieldKeys) {
612
+ formRef.value?.clearValidate(fieldKeys)
613
+ } else {
614
+ formRef.value?.clearValidate()
615
+ }
616
+ }
617
+
618
+ // 暴露方法给父组件
619
+ defineExpose({
620
+ getFormValue,
621
+ setFormValue,
622
+ accessFormFields,
623
+ resetFormValue,
624
+ validateForm,
625
+ validateFields,
626
+ clearValidate,
627
+ })
628
+
629
+ // 初始化
630
+ initFormData()
631
+ </script>
632
+
633
+ <template>
634
+ <div class="form-container">
635
+ <Form
636
+ ref="formRef"
637
+ :model="formData"
638
+ :rules="formRules"
639
+ :label-col="{ style: { width: `${props.labelWidth}px` } }"
640
+ :label-align="props.labelAlign"
641
+ :disabled="props.disabled"
642
+ :layout="props.labelPosition"
643
+ :colon="props.colon"
644
+ >
645
+ <Row :gutter="props.gutter">
646
+ <Col
647
+ v-for="(field, index) in visibleFields"
648
+ v-show="!field.hidden"
649
+ :key="field.key ?? index"
650
+ :span="field.span || 24"
651
+ >
652
+ <FormItem
653
+ :label-align="field.labelAlign || props.labelAlign"
654
+ :name="field.type === 'inputGroup' ? undefined : field.key"
655
+ :label-col="
656
+ field.labelWidth ? { style: { width: `${field.labelWidth}px` } } : undefined
657
+ "
658
+ :required="field.required"
659
+ :html-for="undefined"
660
+ >
661
+ <template #label>
662
+ <span>{{ field.name }}</span>
663
+ <Tooltip v-if="getHelpText(field)" :title="getHelpText(field)">
664
+ <QuestionCircleOutlined class="form-help-text" />
665
+ </Tooltip>
666
+ </template>
667
+ <!-- 为 inputGroup 类型包裹 a-form-item-rest,避免一个 FormItem 收集多个字段导致的警告 -->
668
+ <template v-if="field.type === 'inputGroup'">
669
+ <FormItemRest>
670
+ <component :is="renderField(field)" />
671
+ </FormItemRest>
672
+ </template>
673
+ <template v-else>
674
+ <component :is="renderField(field)" />
675
+ </template>
676
+ </FormItem>
677
+ </Col>
678
+ <Col
679
+ v-if="$slots.search && props.inlineActions"
680
+ :span="props.actionsSpan"
681
+ class="form-actions-inline"
682
+ >
683
+ <FormItem>
684
+ <slot name="search" />
685
+ </FormItem>
686
+ </Col>
687
+ </Row>
688
+ <div v-if="$slots.actions" class="form-actions">
689
+ <slot name="actions" />
690
+ </div>
691
+ </Form>
692
+ </div>
693
+ </template>
694
+
695
+ <style scoped lang="scss">
696
+ .ant-form {
697
+ width: 100%;
698
+ }
699
+
700
+ .ant-form-vertical {
701
+ .ant-row {
702
+ row-gap: 8px;
703
+ }
704
+ :deep(.ant-form-item) {
705
+ margin-bottom: 0;
706
+ .ant-form-item-label {
707
+ padding: 0 0 4px;
708
+ > label {
709
+ white-space: normal;
710
+ overflow-wrap: break-word;
711
+ height: auto;
712
+ line-height: 1.5;
713
+ }
714
+ }
715
+ }
716
+ }
717
+
718
+ .ant-form-horizontal {
719
+ .ant-row {
720
+ row-gap: 20px;
721
+ }
722
+ :deep(.ant-form-item) {
723
+ margin-bottom: 0;
724
+ .ant-form-item-control {
725
+ position: relative;
726
+ }
727
+ .ant-form-item-explain {
728
+ position: absolute;
729
+ top: 100%;
730
+ left: 0;
731
+ width: 100%;
732
+ font-size: 12px;
733
+ }
734
+ .ant-form-item-explain-error {
735
+ height: 20px;
736
+ line-height: 20px;
737
+ }
738
+ .ant-form-item-label {
739
+ > label {
740
+ white-space: normal;
741
+ overflow-wrap: break-word;
742
+ height: 32px;
743
+ line-height: 1;
744
+ vertical-align: 0.2em;
745
+ }
746
+ }
747
+ }
748
+ }
749
+
750
+ .ant-input-number {
751
+ width: 100%;
752
+ }
753
+
754
+ .ant-picker {
755
+ width: 100%;
756
+ }
757
+
758
+ // 修复带 addonBefore/addonAfter 时 InputNumber 未能撑满容器宽度的问题
759
+ :deep(.ant-input-number-group-wrapper) {
760
+ display: block;
761
+ width: 100%;
762
+ }
763
+
764
+ // 同步修复普通 Input 在存在 addon 时的 group-wrapper 宽度问题,确保表现一致
765
+ :deep(.ant-input-group-wrapper) {
766
+ display: block;
767
+ width: 100%;
768
+ }
769
+
770
+ .form-container {
771
+ width: 100%;
772
+ background: #fff;
773
+ border-radius: 15px;
774
+ height: 100%;
775
+ }
776
+
777
+ .form-help-text {
778
+ margin-left: 4px;
779
+ color: #999;
780
+ cursor: help;
781
+ }
782
+ </style>