@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.
- package/CHANGELOG.md +12 -0
- package/dist/core.css +1 -0
- package/dist/index.cjs +57 -0
- package/dist/index.js +4154 -0
- package/dist/types/BaseSearch/index.d.ts +59 -0
- package/dist/types/BaseTable/index.d.ts +204 -0
- package/dist/types/Form/CoreForm.d.ts +82 -0
- package/dist/types/Form/types.d.ts +57 -0
- package/dist/types/SSelectPage/index.d.ts +102 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/render/AreaCascader.d.ts +5 -0
- package/dist/types/render/AutoComplete.d.ts +4 -0
- package/dist/types/render/Button.d.ts +4 -0
- package/dist/types/render/Cascader.d.ts +5 -0
- package/dist/types/render/Checkbox.d.ts +4 -0
- package/dist/types/render/CheckboxGroup.d.ts +4 -0
- package/dist/types/render/Custom.d.ts +8 -0
- package/dist/types/render/DatePicker.d.ts +4 -0
- package/dist/types/render/Input.d.ts +4 -0
- package/dist/types/render/InputGroup.d.ts +5 -0
- package/dist/types/render/InputNumber.d.ts +4 -0
- package/dist/types/render/InputPassword.d.ts +4 -0
- package/dist/types/render/InputRange.d.ts +9 -0
- package/dist/types/render/RadioGroup.d.ts +4 -0
- package/dist/types/render/Select.d.ts +4 -0
- package/dist/types/render/SselectPage.d.ts +4 -0
- package/dist/types/render/Switch.d.ts +4 -0
- package/dist/types/render/Tree.d.ts +9 -0
- package/dist/types/render/TreeSelect.d.ts +4 -0
- package/dist/types/render/Upload.d.ts +4 -0
- package/dist/types/render/helper.d.ts +10 -0
- package/dist/types/render/index.d.ts +43 -0
- package/dist/types/render/registry.d.ts +9 -0
- package/dist/types/render/state.d.ts +19 -0
- package/dist/types/render/types.d.ts +435 -0
- package/dist/types/utils/attrMapping.d.ts +26 -0
- package/package.json +10 -12
- package/src/BaseSearch/index.vue +371 -0
- package/src/BaseTable/index.vue +62 -36
- package/src/Form/CoreForm.vue +782 -0
- package/src/Form/types.ts +86 -0
- package/src/SSelectPage/index.vue +607 -0
- package/src/index.ts +15 -1
- package/src/{BaseTable/renderers → render}/AreaCascader.tsx +3 -3
- package/src/{BaseTable/renderers → render}/AutoComplete.tsx +3 -3
- package/src/{BaseTable/renderers → render}/Button.tsx +2 -2
- package/src/{BaseTable/renderers → render}/Cascader.tsx +3 -3
- package/src/{BaseTable/renderers → render}/Checkbox.tsx +2 -2
- package/src/{BaseTable/renderers → render}/CheckboxGroup.tsx +2 -2
- package/src/render/Custom.tsx +19 -0
- package/src/{BaseTable/renderers → render}/DatePicker.tsx +2 -2
- package/src/{BaseTable/renderers → render}/Input.tsx +3 -3
- package/src/{BaseTable/renderers → render}/InputGroup.tsx +3 -3
- package/src/{BaseTable/renderers → render}/InputNumber.tsx +3 -3
- package/src/{BaseTable/renderers → render}/InputPassword.tsx +3 -3
- package/src/render/InputRange.tsx +154 -0
- package/src/{BaseTable/renderers → render}/RadioGroup.tsx +2 -2
- package/src/{BaseTable/renderers → render}/Select.tsx +2 -2
- package/src/{BaseTable/renderers → render}/SselectPage.tsx +3 -3
- package/src/{BaseTable/renderers → render}/Switch.tsx +2 -2
- package/src/render/Tree.tsx +136 -0
- package/src/{BaseTable/renderers → render}/TreeSelect.tsx +2 -2
- package/src/{BaseTable/renderers → render}/Upload.tsx +4 -5
- package/src/{BaseTable/utils.tsx → render/helper.tsx} +86 -9
- package/src/{BaseTable/renderers → render}/index.ts +45 -4
- package/src/{BaseTable → render}/types.ts +62 -2
- package/src/utils/attrMapping.ts +106 -0
- package/vite.config.ts +15 -2
- package/index.css +0 -2
- package/index.ts +0 -21
- package/src/BaseTable/helpers.tsx +0 -91
- /package/src/{BaseTable → render}/registry.ts +0 -0
- /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>
|