af-mobile-client-vue3 1.3.78 → 1.3.80

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.
@@ -1,1431 +1,1433 @@
1
- <script setup lang="ts">
2
- import type { FieldType } from 'vant'
3
- import type { Numeric } from 'vant/es/utils'
4
- import ImageUploader from '@af-mobile-client-vue3/components/core/ImageUploader/index.vue'
5
- import XGridDropOption from '@af-mobile-client-vue3/components/core/XGridDropOption/index.vue'
6
- import XMultiSelect from '@af-mobile-client-vue3/components/core/XMultiSelect/index.vue'
7
- import XSelect from '@af-mobile-client-vue3/components/core/XSelect/index.vue'
8
- import XLocationPicker from '@af-mobile-client-vue3/components/data/XOlMap/XLocationPicker/index.vue'
9
- import { getConfigByNameAsync, runLogic } from '@af-mobile-client-vue3/services/api/common'
10
- import { post } from '@af-mobile-client-vue3/services/restTools'
11
- import { searchToListOption, searchToOption } from '@af-mobile-client-vue3/services/v3Api'
12
- import { useUserStore } from '@af-mobile-client-vue3/stores/modules/user'
13
- import { getDict } from '@af-mobile-client-vue3/utils/dictUtil'
14
- import { executeStrFunctionByContext } from '@af-mobile-client-vue3/utils/runEvalFunction'
15
- import { areaList } from '@vant/area-data'
16
- import dayjs from 'dayjs/esm/index'
17
- import { debounce } from 'lodash-es'
18
- import {
19
- Area as VanArea,
20
- Button as VanButton,
21
- Calendar as VanCalendar,
22
- Cascader as VanCascader,
23
- Checkbox as VanCheckbox,
24
- CheckboxGroup as vanCheckboxGroup,
25
- DatePicker as VanDatePicker,
26
- Field as VanField,
27
- Picker as VanPicker,
28
- PickerGroup as VanPickerGroup,
29
- Popup as VanPopup,
30
- Radio as VanRadio,
31
- RadioGroup as VanRadioGroup,
32
- Rate as VanRate,
33
- Slider as VanSlider,
34
- Stepper as VanStepper,
35
- Switch as VanSwitch,
36
- TimePicker as VanTimePicker,
37
- } from 'vant'
38
- import { computed, defineEmits, defineModel, defineProps, getCurrentInstance, onBeforeMount, ref, watch } from 'vue'
39
-
40
- const props = defineProps({
41
- attr: {
42
- type: Object,
43
- },
44
- form: {
45
- type: Object,
46
- },
47
- // 整表只读:来自 XForm,统一控制交互
48
- formReadonly: {
49
- type: Boolean,
50
- default: false,
51
- },
52
- datePickerFilter: {
53
- type: Function,
54
- default: () => true,
55
- },
56
- datePickerFormatter: {
57
- type: Function,
58
- default: (type, val) => val,
59
- },
60
- mode: {
61
- type: String,
62
- default: '查询',
63
- },
64
- serviceName: {
65
- type: String,
66
- default: undefined,
67
- },
68
- // 调用logic获取数据源的追加参数
69
- getDataParams: {
70
- type: Object,
71
- default: undefined,
72
- },
73
- disabled: {
74
- type: Boolean,
75
- default: false,
76
- },
77
- rules: {
78
- type: Object,
79
- default: () => {},
80
- },
81
- // 用 defineModel 替代 modelValue/emit
82
- modelValue: {
83
- type: [String, Number, Boolean, Array, Object],
84
- default: undefined,
85
- },
86
- showLabel: {
87
- type: Boolean,
88
- default: true,
89
- },
90
- // radio/checkbox/select/mul-select 选项数据结构
91
- columnsField: {
92
- type: Object,
93
- default: () => {
94
- return { text: 'label', value: 'value' }
95
- },
96
- },
97
- isAsyncUpload: {
98
- type: Boolean,
99
- default: false,
100
- },
101
-
102
- })
103
-
104
- const emits = defineEmits(['setForm', 'xFormItemEmitFunc'])
105
-
106
- // 用 defineModel 替代 modelValue/emit
107
- const modelData = defineModel<string | number | boolean | any[] | Record<string, any>>()
108
-
109
- // 获取字典
110
- interface OptionItem {
111
- label: string
112
- value: any
113
- children?: OptionItem[]
114
- }
115
-
116
- // 判断并初始化防抖函数
117
- let debouncedUserLinkFunc: (() => void) | null = null
118
- let debouncedDepLinkFunc: (() => void) | null = null
119
- let debouncedUpdateOptions: (() => void) | null = null
120
-
121
- const { attr, form, mode, serviceName, getDataParams, columnsField } = props
122
- // 配置的表单值格式(仅针对 datePicker 生效)
123
- // 作用:统一控制日期值的格式化输入/输出与选择器展示粒度
124
- // 可选:'YYYY' | 'YYYY-MM' | 'YYYY-MM-DD' | 'YYYY-MM-DD HH' | 'YYYY-MM-DD HH:mm' | 'YYYY-MM-DD HH:mm:ss'
125
- const formValueFormat = computed(() => {
126
- // 默认全格式
127
- return (attr && (attr as any).formValueFormat) || 'YYYY-MM-DD HH:mm:ss'
128
- })
129
-
130
- // 根据 formValueFormat 动态计算日期列类型(决定 VanDatePicker 展示年/月/日)
131
- // 例如:YYYY 只显示年;YYYY-MM 显示年、月;YYYY-MM-DD 显示年、月、日
132
- const dateColumnsType = computed(() => {
133
- const format = formValueFormat.value
134
- const columns: string[] = ['year']
135
- if (format.includes('MM'))
136
- columns.push('month')
137
- if (format.includes('DD'))
138
- columns.push('day')
139
- return columns as any
140
- })
141
-
142
- // 是否包含时间(决定是否展示时间页签与 VanTimePicker)
143
- const hasTime = computed(() => formValueFormat.value.includes('HH'))
144
-
145
- // 根据 formValueFormat 动态计算时间列类型(决定时/分/秒的展示)
146
- // 例如:包含 HH 显示小时;包含 mm 显示分钟;包含 ss 显示秒
147
- const timeColumnsType = computed(() => {
148
- const format = formValueFormat.value
149
- const columns: string[] = []
150
- if (format.includes('HH'))
151
- columns.push('hour')
152
- if (format.includes('mm'))
153
- columns.push('minute')
154
- if (format.includes('ss'))
155
- columns.push('second')
156
- return columns as any
157
- })
158
- const calendarShow = ref(false)
159
- const option = ref([])
160
- const pickerValue = ref(undefined)
161
- const timePickerValue = ref(undefined)
162
- const area = ref<any>(undefined)
163
- const showPicker = ref(false)
164
- const showDatePicker = ref(false)
165
- const showTimePicker = ref(false)
166
- const showArea = ref(false)
167
- const errorMessage = ref('')
168
- const showTreeSelect = ref(false)
169
- const treeValue = ref('')
170
-
171
- // 登录信息 (可以在配置的动态函数中使用 this.setupState 获取到当前组件内的全部函数和变量 例:this.setupState.userState)
172
- const userState = useUserStore().getLogin()
173
- const currUser = computed(() => userState.f.resources.id)
174
- const userInfo = computed(() => ({
175
- orgId: userState.f.resources.orgid,
176
- userId: userState.f.resources.id,
177
- }))
178
-
179
- // 是否展示当前项
180
- const showItem = ref(true)
181
-
182
- // 当前组件实例(不推荐使用,可能会在后续的版本更迭中调整,暂时用来绑定函数的上下文)
183
- const currInst = getCurrentInstance()
184
-
185
- // 配置中心->表单项变更触发函数
186
- async function dataChangeFunc() {
187
- if (attr.dataChangeFunc) {
188
- await executeStrFunctionByContext(currInst, attr.dataChangeFunc, [props.form, (formData: any) => emits('setForm', formData), attr, null, mode, runLogic, getConfigByNameAsync])
189
- }
190
- }
191
- const dataChangeFuncdebounce = debounce(dataChangeFunc, 300)
192
- // 配置中心->表单项展示函数
193
- async function showFormItemFunc() {
194
- if (attr.showFormItemFunc) {
195
- const obj = await executeStrFunctionByContext(currInst, attr.showFormItemFunc, [form, attr, null, mode])
196
- // 判断是 bool 还是 obj 兼容
197
- if (typeof obj === 'boolean') {
198
- showItem.value = obj
199
- }
200
- else if (obj && typeof obj === 'object') {
201
- // obj 是一个对象,并且不是数组
202
- showItem.value = obj?.show
203
- }
204
- }
205
- }
206
- const showFormItemFuncdebounce = debounce(showFormItemFunc, 300)
207
- /**
208
- * 检测是否传入了有效的值
209
- * @returns any
210
- */
211
- function checkModel(val = props.modelValue) {
212
- if (val === null || val === undefined || val === '')
213
- return false
214
- if (Array.isArray(val))
215
- return val.length > 0
216
- return true
217
- }
218
- /**
219
- * 获取表单项的默认值
220
- * @returns any
221
- */
222
- function getDefaultValue() {
223
- const val = props.modelValue
224
- console.warn('>>>>>props', props)
225
- console.warn('>>>> attr', attr)
226
- // 如果有有效值,直接返回(datePicker 初始值需按 formValueFormat 归一)
227
- // 目的:外部通过 formData 传入的初始值在组件挂载即与配置格式一致
228
- if (checkModel(val)) {
229
- if (attr.type === 'datePicker' && typeof val === 'string') {
230
- const parsed = dayjs(val)
231
- if (parsed.isValid())
232
- return parsed.format(formValueFormat.value)
233
- }
234
- return val
235
- }
236
-
237
- // 根据类型获取默认值
238
- const getDefaultByType = () => {
239
- const def = mode !== '查询' ? attr.formDefault : attr.queryFormDefault
240
-
241
- if (['treeSelect', 'select', 'checkbox'].includes(attr.type) && ['curOrgId', 'curDepId', 'curUserId'].includes(def)) {
242
- if (def === 'curOrgId') {
243
- if (attr.type === 'treeSelect') {
244
- treeValue.value = userState.f.resources.orgs
245
- }
246
- return attr.type === 'select' ? userState.f.resources.orgid : [userState.f.resources.orgid]
247
- }
248
- if (def === 'curDepId') {
249
- if (attr.type === 'treeSelect') {
250
- treeValue.value = userState.f.resources.deps
251
- }
252
- return attr.type === 'select' ? userState.f.resources.depids : [userState.f.resources.depids]
253
- }
254
- if (def === 'curUserId') {
255
- if (attr.type === 'treeSelect') {
256
- treeValue.value = userState.f.resources.name
257
- }
258
- return attr.type === 'select' ? userState.f.resources.id : [userState.f.resources.id]
259
- }
260
- }
261
-
262
- // 数组类型默认值
263
- const arrayTypes = ['uploader', 'checkbox', 'file', 'area', 'image', 'treeSelect']
264
- if (arrayTypes.includes(attr.type)) {
265
- return def !== undefined ? def : []
266
- }
267
-
268
- // 特殊类型默认值
269
- const specialDefaults = {
270
- switch: false,
271
- stepper: 1,
272
- addressSearch: val,
273
- }
274
-
275
- if (specialDefaults[attr.type] !== undefined) {
276
- return specialDefaults[attr.type]
277
- }
278
-
279
- // 日期时间类型:调用 getDateRange,并传入 formValueFormat
280
- // 说明:让初始化/查询默认值同样遵循配置的格式显示
281
- const dateTypes = ['rangePicker', 'yearPicker', 'monthPicker', 'yearRangePicker', 'monthRangePicker', 'datePicker', 'timePicker']
282
- if (dateTypes.includes(attr.type)) {
283
- return getDateRange({
284
- type: attr.type,
285
- formDefault: attr.formDefault ?? '',
286
- queryFormDefault: attr.queryFormDefault ?? '',
287
- queryType: attr.queryType,
288
- queryValueFormat: attr.queryValueFormat,
289
- name: attr.name ?? '',
290
- formValueFormat: formValueFormat.value,
291
- }) ?? []
292
- }
293
-
294
- // 其他类型(字符串、数字等)
295
- return def ?? ''
296
- }
297
-
298
- return getDefaultByType()
299
- }
300
-
301
- // 初始化日期组件初始值,组件自定义格式显示值和实际值(日期相关初始化都在此函数中操作)
302
- function getDateRange({
303
- type,
304
- formDefault: defaultValue,
305
- queryFormDefault,
306
- queryType,
307
- queryValueFormat: defaultFormat,
308
- name,
309
- formValueFormat: formFormat,
310
- }: {
311
- type: string
312
- formDefault: string
313
- queryFormDefault: string
314
- queryType?: string
315
- queryValueFormat?: string
316
- name: string
317
- // 新增:用于优先覆盖 datePicker 的显示/存储格式
318
- formValueFormat?: string
319
- }): string | [string, string] | undefined {
320
- const formatMap: Record<string, string> = {
321
- yearPicker: 'YYYY',
322
- yearRangePicker: 'YYYY',
323
- monthPicker: 'YYYY-MM',
324
- monthRangePicker: 'YYYY-MM',
325
- datePicker: 'YYYY-MM-DD HH:mm:ss',
326
- rangePicker: 'YYYY-MM-DD HH:mm:ss',
327
- }
328
- if (mode) {
329
- // datePicker 优先使用 formValueFormat(否则退回 queryValueFormat 或默认映射)
330
- const preferFormat = type === 'datePicker' ? (formFormat || defaultFormat) : defaultFormat
331
- const format = preferFormat || formatMap[type]
332
- const val = mode === '查询' ? queryFormDefault : defaultValue
333
- let start: string, end: string
334
- switch (val) {
335
- case 'curYear':
336
- start = dayjs().startOf('year').format(format)
337
- end = dayjs().endOf('year').format(format)
338
- break
339
- case 'curMonth':
340
- start = dayjs().startOf('month').format(format)
341
- end = dayjs().endOf('month').format(format)
342
- break
343
- case 'curDay':
344
- start = dayjs().startOf('day').format(format)
345
- end = dayjs().endOf('day').format(format)
346
- break
347
- case 'curTime':
348
- start = dayjs().format(format)
349
- end = dayjs().format(format)
350
- break
351
- default:
352
- return undefined
353
- }
354
- if (['monthPicker', 'yearPicker', 'datePicker'].includes(type)) {
355
- if (mode !== '查询') {
356
- if (queryType === 'BETWEEN') {
357
- return [start, end]
358
- }
359
- if (name.includes('开始') || name.includes('起始')) {
360
- return start
361
- }
362
- else {
363
- return end
364
- }
365
- }
366
- else {
367
- return start
368
- }
369
- }
370
- // rangePicker组件表单显示的值
371
- if (mode === '查询' && type === 'rangePicker') {
372
- pickerValue.value = `${start} ~ ${end}`
373
- }
374
- return mode !== '查询' ? start : [start, end]
375
- }
376
- else {
377
- return undefined
378
- }
379
- }
380
-
381
- // 监听 props.form 的变化
382
- watch(
383
- () => props.form,
384
- (newVal, oldVal) => {
385
- // 如果是从函数获取 options
386
- if (props.attr.keyName && (props.attr.keyName.toString().includes('async ') || props.attr.keyName.toString().includes('function'))) {
387
- debouncedUpdateOptions()
388
- }
389
- if (props.attr.showFormItemFunc) {
390
- showFormItemFuncdebounce()
391
- }
392
- },
393
- { deep: true },
394
- )
395
-
396
- // 监听 modelData 的变化,调用 dataChangeFunc
397
- watch(
398
- () => modelData.value,
399
- (newVal, oldVal) => {
400
- // 避免初始化时的调用
401
- if (newVal !== oldVal) {
402
- dataChangeFuncdebounce()
403
- }
404
- },
405
- { deep: true },
406
- )
407
-
408
- // 监听 option.value 的变化
409
- watch(
410
- () => option.value,
411
- (newOption) => {
412
- if (attr.type === 'treeSelect' && newOption && newOption.length > 0) {
413
- // 你可以在这里调用 findOptionInTree 函数来查找默认值对应的 label
414
- const result = findOptionInTree(option.value, modelData.value)
415
- if (attr.type === 'treeSelect' && result)
416
- treeValue.value = result.label
417
- }
418
- },
419
- )
420
-
421
- function updateFile(files, _index) {
422
- modelData.value = files
423
- }
424
-
425
- // 表单校验的类型校验
426
- function formTypeCheck(attr, value) {
427
- if (mode === '查询' || mode === '预览')
428
- return
429
- // if (!attr.rule || !attr.rule.required || attr.rule.required === 'false')
430
- // return
431
- switch (attr.rule.type) {
432
- case 'string':
433
- if (value.length === 0) {
434
- errorMessage.value = `请输入${attr.name}`
435
- return
436
- }
437
- break
438
- case 'number':
439
- if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(value)) {
440
- errorMessage.value = `${attr.name}必须为数字`
441
- return
442
- }
443
- break
444
- case 'boolean':
445
- case 'array':
446
- case 'regexp':
447
- if (value) {
448
- errorMessage.value = ''
449
- return
450
- }
451
- break
452
- case 'integer':
453
- if (!/^-?\d+$/.test(value)) {
454
- errorMessage.value = `${attr.name}必须为整数`
455
- return
456
- }
457
- break
458
- case 'float':
459
- if (!/^-?\d+\.\d+$/.test(value)) {
460
- errorMessage.value = `${attr.name}必须为小数`
461
- return
462
- }
463
- break
464
- case 'email':
465
- if (!/^[\w.%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(value)) {
466
- errorMessage.value = `请输入正确的邮箱地址`
467
- return
468
- }
469
- break
470
- case 'idNumber':
471
- if (!/^(?:\d{15}|\d{17}[0-9X])$/.test(value)) {
472
- errorMessage.value = `请输入正确的身份证号码`
473
- return
474
- }
475
- break
476
- case 'userPhone':
477
- if (!/^1[3-9]\d{9}$/.test(value)) {
478
- errorMessage.value = `请输入正确的手机号码`
479
- return
480
- }
481
- break
482
- case 'landlineNumber':
483
- if (!/^0\d{2,3}[-\s]?\d{7,8}$/.test(value)) {
484
- errorMessage.value = `请输入正确的座机号码`
485
- return
486
- }
487
- break
488
- case 'greaterThanZero':
489
- if (!/^(?:[1-9]\d*(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(value)) {
490
- errorMessage.value = `请输入一个大于0的数字`
491
- return
492
- }
493
- break
494
- case 'greaterThanOrEqualZero':
495
- if (!/^(?:\d+|\d*\.\d+)$/.test(value)) {
496
- errorMessage.value = `请输入一个大于等于0的数字`
497
- return
498
- }
499
- break
500
- case 'stringLength':
501
- if (!(value.length >= attr.rule.minLen && value.length < attr.rule.maxLen)) {
502
- errorMessage.value = `长度必须在${attr.rule.minLen}~${attr.rule.maxLen}之间`
503
- return
504
- }
505
- break
506
- case 'customJs':
507
- // eslint-disable-next-line no-case-declarations
508
- const funcStr = attr.rule.customValidatorFunc
509
- // 输入的数据是否符合条件
510
- // eslint-disable-next-line no-case-declarations
511
- const status = ref(true)
512
- // 提取函数体部分
513
- // eslint-disable-next-line no-case-declarations
514
- const funcBodyMatch = funcStr.match(/function\s*\(.*?\)\s*\{([\s\S]*)\}/)
515
- if (!funcBodyMatch)
516
- throw new Error('自定义校验函数不合法')
517
- // eslint-disable-next-line no-case-declarations
518
- const funcBody = funcBodyMatch[1].trim() // 提取函数体
519
- // 使用 new Function 创建函数
520
- // eslint-disable-next-line no-new-func,no-case-declarations
521
- const customValidatorFunc = new Function('rule', 'value', 'callback', 'form', 'attr', 'util', funcBody)
522
- // 定义 callback 函数
523
- // eslint-disable-next-line no-case-declarations
524
- const callback = (error) => {
525
- if (error) {
526
- errorMessage.value = `${error}`
527
- status.value = false // 表示有错误发生
528
- }
529
- }
530
- // 调用自定义校验函数
531
- customValidatorFunc(
532
- attr.rule,
533
- value,
534
- callback,
535
- form,
536
- attr,
537
- {}, // util 对象(可以根据需要传递)
538
- )
539
- if (!status.value)
540
- return
541
- break
542
- default:
543
- errorMessage.value = ''
544
- break
545
- }
546
- errorMessage.value = ''
547
- }
548
-
549
- onBeforeMount(() => {
550
- init()
551
- modelData.value = getDefaultValue()
552
- showFormItemFunc()
553
- dataChangeFunc()
554
- if (attr?.keyName?.toString()?.startsWith('search@根据表单项[') && attr?.keyName?.toString().endsWith(']联动人员'))
555
- debouncedUserLinkFunc = debounce(() => updateResOptions('人员'), 200)
556
-
557
- if (attr?.keyName?.toString()?.startsWith('search@根据表单项[') && attr?.keyName?.toString().endsWith(']联动部门'))
558
- debouncedDepLinkFunc = debounce(() => updateResOptions('部门'), 200)
559
-
560
- if (attr.keyName && (attr?.keyName?.toString().indexOf('async ') !== -1 || attr?.keyName?.toString()?.indexOf('function') !== -1)) {
561
- debouncedUpdateOptions = debounce(updateOptions, 200)
562
- }
563
- })
564
- // 是否展示表单左侧label文字
565
- const labelData = computed(() => {
566
- return props.showLabel ? attr.name : null
567
- })
568
- // 是否展示表单左侧label文字
569
- const labelAlign = computed(() => {
570
- return attr.labelAlign ? attr.labelAlign : 'left'
571
- })
572
- // 是否只读
573
- const readonly = computed(() => {
574
- return props.formReadonly || attr.addOrEdit === 'readonly' || mode === '预览'
575
- })
576
- // 提示内容
577
- const placeholder = computed(() => {
578
-
579
- if (attr.addOrEdit === 'readonly' || mode === '预览')
580
- return '暂无内容 ~ '
581
- else
582
- if (attr.placeholder)
583
- return attr.placeholder
584
- else
585
- switch (attr.type) {
586
- case 'datePicker':
587
- case 'timePicker':
588
- case 'rangePicker':
589
- case 'radio':
590
- case 'select':
591
- case 'treeSelect':
592
- case 'area':
593
- case 'citySelect':
594
- case 'picker':
595
- return `请选择${attr.name}`
596
- case 'addressSearch':
597
- case 'input':
598
- case 'textarea':
599
- case 'intervalPicker':
600
- return `请输入${attr.name}`
601
- default:
602
- return `请选择${attr.name}`
603
- }
604
- })
605
-
606
- const formatDate = date => `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
607
-
608
- function onCalendarConfirm(values) {
609
- modelData.value = [formatDate(values[0]), formatDate(values[1])]
610
- pickerValue.value = `${formatDate(values[0])} ~ ${formatDate(values[1])}`
611
- calendarShow.value = false
612
- }
613
-
614
- // js 函数作为数据源
615
- async function updateOptions() {
616
- if (attr.keyName && (attr.keyName.toString().includes('async ') || attr.keyName.toString().includes('function '))) {
617
- option.value = await executeStrFunctionByContext(this, attr.keyName, [props.form, runLogic, props.mode, getConfigByNameAsync, post])
618
- }
619
- }
620
-
621
- function init() {
622
- if (attr.keyName && typeof attr.keyName === 'string') {
623
- if (attr.keyName && attr.keyName.includes('logic@')) {
624
- getData({}, (res) => {
625
- option.value = res
626
- initRadioValue()
627
- })
628
- }
629
- else if (attr.keyName && attr.keyName.includes('config@')) {
630
- const configName = attr.keyName.substring(7)
631
- getDict(configName, (result) => {
632
- if (result)
633
- option.value = result
634
- }, serviceName)
635
- }
636
- else if (attr.keyName && attr.keyName.includes('search@')) {
637
- let source = attr.keyName.substring(7)
638
- const userid = currUser.value
639
- let roleName = 'roleName'
640
- if (source.startsWith('根据角色[') && source.endsWith(']获取人员')) {
641
- const startIndex = source.indexOf('[') + 1
642
- const endIndex = source.indexOf(']', startIndex)
643
- roleName = source.substring(startIndex, endIndex)
644
- source = '根据角色获取人员'
645
- }
646
- const searchData = { source, userid, roleName }
647
- if (source.startsWith('根据表单项[') && source.endsWith(']联动人员'))
648
- updateResOptions('人员')
649
- else if (source.startsWith('根据表单项[') && source.endsWith(']联动部门'))
650
- updateResOptions('部门')
651
- else if (attr.type === 'select' || attr.type === 'checkbox')
652
- searchToListOption(searchData, res => getDataCallback(res))
653
- else
654
- searchToOption(searchData, res => getDataCallback(res))
655
- }
656
- else if (attr.keyName.toString().includes('async ') || attr.keyName.toString().includes('function ')) {
657
- updateOptions()
658
- }
659
- else {
660
- initRadioValue()
661
- }
662
- }
663
- }
664
-
665
- function getDataCallback(res) {
666
- option.value = res
667
- if (attr.type === 'radio')
668
- initRadioValue()
669
- }
670
-
671
- async function updateResOptions(type) {
672
- if (attr?.keyName?.toString()?.startsWith('search@根据表单项[') && attr?.keyName?.toString()?.endsWith(`]联动${type}`)) {
673
- const searchData = { source: `获取${type}`, userid: currUser.value }
674
- const startIndex = attr.keyName.indexOf('[') + 1
675
- const endIndex = attr.keyName.indexOf(']', startIndex)
676
- const formModel = attr.keyName.substring(startIndex, endIndex).replace('.', '_')
677
- const formModelData = form[formModel]
678
- if (formModel?.length && formModelData?.length) {
679
- await searchToListOption(searchData, (res) => {
680
- getDataCallback(res.filter((h) => {
681
- return formModelData['0'] === h.f_organization_id || formModelData['0'] === h.f_department_id || formModelData['0'] === h.parentid
682
- // if (formModel.indexOf('org') > -1) {
683
- // return formModelData?.includes(h.orgid || h.f_organization_id || h.parentid)
684
- // } else {
685
- // return formModelData?.includes(h?.parentid)
686
- // }
687
- }))
688
- })
689
- }
690
- }
691
- }
692
-
693
- function initRadioValue() {
694
- if ((mode === '新增' || mode === '修改') && attr.type === 'radio' && !props.modelValue) {
695
- if (attr.keys && attr.keys.length > 0)
696
- modelData.value = attr.keys[0].value
697
- else if (option.value && option.value.length > 0)
698
- modelData.value = option.value[0].value
699
- }
700
- }
701
-
702
- function getData(value, callback) {
703
- if (value !== '') {
704
- const logicName = attr.keyName
705
- const logic = logicName.substring(6)
706
- // 调用logic前设置参数
707
- if (getDataParams && getDataParams[attr.model])
708
- Object.assign(value, getDataParams[attr.model])
709
- Object.assign(value, userInfo.value)
710
- runLogic(logic, value, serviceName).then((res) => {
711
- callback(res)
712
- })
713
- }
714
- }
715
-
716
- // 已废弃 不进行维护
717
- function onPickerConfirm({ selectedOptions }) {
718
- showPicker.value = false
719
- modelData.value = selectedOptions[0].text
720
- }
721
-
722
- // 日期时间选择数据
723
- const dateTimePickerValue = ref<any>({})
724
- // 展示日期时间选择器:根据 formValueFormat 初始化 VanDatePicker/VanTimePicker 的当前值
725
- // 规则:
726
- // - 若已有字符串值,按 formValueFormat 解析并拆分为日期/时间数组
727
- // - 若无值,按当前时间生成匹配格式的默认数组
728
- function showDataTimePicker() {
729
- if (props.modelValue && typeof props.modelValue === 'string') {
730
- const base = dayjs(props.modelValue as string, formValueFormat.value)
731
- const dateArr: string[] = []
732
- dateArr.push(base.format('YYYY'))
733
- if (dateColumnsType.value.includes('month'))
734
- dateArr.push(base.format('MM'))
735
- if (dateColumnsType.value.includes('day'))
736
- dateArr.push(base.format('DD'))
737
- const timeArr: string[] = []
738
- if (hasTime.value) {
739
- if (timeColumnsType.value.includes('hour'))
740
- timeArr.push(base.format('HH'))
741
- if (timeColumnsType.value.includes('minute'))
742
- timeArr.push(base.format('mm'))
743
- if (timeColumnsType.value.includes('second'))
744
- timeArr.push(base.format('ss'))
745
- }
746
- dateTimePickerValue.value = {
747
- date: dateArr,
748
- time: timeArr.length ? timeArr : ['00', '00', '00'].slice(0, timeColumnsType.value.length),
749
- }
750
- }
751
- else {
752
- const now = dayjs()
753
- const dateArr: string[] = []
754
- dateArr.push(now.format('YYYY'))
755
- if (dateColumnsType.value.includes('month'))
756
- dateArr.push(now.format('MM'))
757
- if (dateColumnsType.value.includes('day'))
758
- dateArr.push(now.format('DD'))
759
- const timeArr: string[] = []
760
- if (hasTime.value) {
761
- if (timeColumnsType.value.includes('hour'))
762
- timeArr.push(now.format('HH'))
763
- if (timeColumnsType.value.includes('minute'))
764
- timeArr.push(now.format('mm'))
765
- if (timeColumnsType.value.includes('second'))
766
- timeArr.push(now.format('ss'))
767
- }
768
- dateTimePickerValue.value = {
769
- date: dateArr,
770
- time: timeArr.length ? timeArr : ['00', '00', '00'].slice(0, timeColumnsType.value.length),
771
- }
772
- }
773
- showDatePicker.value = true
774
- }
775
-
776
- function onDatePickerConfirm({ selectedValues }) {
777
- showDatePicker.value = false
778
- modelData.value = selectedValues.join('-')
779
- }
780
-
781
- // 已废弃 不进行维护
782
- function onTimePickerConfirm({ selectedValues }) {
783
- showTimePicker.value = false
784
- timePickerValue.value = selectedValues.join(':')
785
- modelData.value = timePickerValue.value
786
- }
787
-
788
- // 没人用到本次先不动。后续需要看这个组件需要怎么使用
789
- function onAreaConfirm({ selectedOptions }) {
790
- area.value = `${selectedOptions[0].text}-${selectedOptions[1].text}-${selectedOptions[2].text}`
791
- showArea.value = false
792
- modelData.value = [{
793
- province: selectedOptions[0].text,
794
- city: selectedOptions[1].text,
795
- district: selectedOptions[2].text,
796
- }]
797
- }
798
-
799
- // 日期时间选择确认:将选择的年月日(+时分秒)拼装后,最终使用 formValueFormat 输出到 v-model
800
- function onDateTimePickerConfirm() {
801
- showDatePicker.value = false
802
- const dateParts = dateTimePickerValue.value.date as string[]
803
- const timeParts = (dateTimePickerValue.value.time as string[]) || []
804
- const year = dateParts[0]
805
- const month = dateParts[1] || '01'
806
- const day = dateParts[2] || '01'
807
- const hour = hasTime.value && timeParts[0] ? timeParts[0] : '00'
808
- const minute = hasTime.value && timeParts[1] ? timeParts[1] : '00'
809
- const second = hasTime.value && timeParts[2] ? timeParts[2] : '00'
810
- const full = `${year}-${month}-${day} ${hour}:${minute}:${second}`
811
- modelData.value = dayjs(full, 'YYYY-MM-DD HH:mm:ss').format(formValueFormat.value)
812
- }
813
-
814
- function onPickerCancel() {
815
- showDatePicker.value = false
816
- }
817
-
818
- const showAddressPicker = ref(false)
819
- const addressValue = ref('')
820
-
821
- // XLocationPicker 默认加载的中心点
822
- const defaultMapCenter = computed(() => {
823
- const lonLat = form[`${attr.model}_lon_lat`]
824
- if (lonLat && typeof lonLat === 'string' && lonLat.includes(',')) {
825
- const [lon, lat] = lonLat.split(',').map(Number)
826
- if (!Number.isNaN(lon) && !Number.isNaN(lat)) {
827
- return [lon, lat] as [number, number]
828
- }
829
- }
830
- return undefined
831
- })
832
-
833
- // 处理地址选择器确认
834
- function handleAddressConfirm(location) {
835
- // 构造新的数据格式
836
- const formData = {
837
- [`${attr.model}_lon_lat`]: `${location.longitude},${location.latitude}`,
838
- [attr.model]: location.address,
839
- }
840
- // 更新表单数据
841
- emits('setForm', formData)
842
- showAddressPicker.value = false
843
- }
844
-
845
- // 重置方法,供父组件调用
846
- function reset() {
847
- modelData.value = getDefaultValue()
848
- errorMessage.value = ''
849
- treeValue.value = null
850
- area.value = null
851
- pickerValue.value = null
852
- }
853
-
854
- defineExpose({
855
- reset,
856
- })
857
-
858
- // 数据处理
859
- function cleanEmptyChildren(options, fieldNames = { text: 'label', value: 'value', children: 'children' }) {
860
- if (!Array.isArray(options))
861
- return options
862
-
863
- const childrenKey = fieldNames.children || 'children'
864
-
865
- return options.map((option) => {
866
- // 深拷贝选项,避免修改原始数据
867
- const newOption = { ...option }
868
-
869
- // 如果存在children属性且是空数组
870
- if (newOption[childrenKey] && Array.isArray(newOption[childrenKey]) && newOption[childrenKey].length === 0) {
871
- delete newOption[childrenKey]
872
- }
873
- // 如果存在children属性且非空,则递归处理
874
- else if (newOption[childrenKey] && Array.isArray(newOption[childrenKey])) {
875
- newOption[childrenKey] = cleanEmptyChildren(newOption[childrenKey], fieldNames)
876
- }
877
- return newOption
878
- })
879
- }
880
-
881
- // 级联选择完成事件
882
- function onTreeSelectFinish({ selectedOptions }) {
883
- const index = selectedOptions.length - 1
884
- treeValue.value = selectedOptions[index].label
885
- if (mode === '查询') {
886
- modelData.value = [selectedOptions[index].value]
887
- }
888
- else {
889
- modelData.value = selectedOptions[index].value
890
- }
891
- showTreeSelect.value = false
892
- }
893
-
894
- function emitFunc(func, data) {
895
- emits('xFormItemEmitFunc', func, data, data?.model ? form[data.model] : form)
896
- }
897
-
898
- function findOptionInTree(options, value) {
899
- // 在当前层级查找
900
- const foundItem = options.find(item => item[columnsField.value] === value)
901
- if (foundItem) {
902
- return foundItem
903
- }
904
- // 递归查找子级
905
- for (const item of options) {
906
- if (item.children?.length) {
907
- const foundInChildren = findOptionInTree(item.children, value)
908
- if (foundInChildren)
909
- return foundInChildren
910
- }
911
- }
912
-
913
- return null
914
- }
915
- </script>
916
-
917
- <template>
918
- <div>
919
- <!-- switch开关 -->
920
- <VanField
921
- v-if="attr.type === 'switch' && showItem"
922
- name="switch"
923
- :label="labelData"
924
- :label-align="labelAlign"
925
- :input-align="attr.inputAlign ? attr.inputAlign : 'right'"
926
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
927
- :required="attr.rule.required === 'true'"
928
- >
929
- <template #input>
930
- <VanSwitch v-model="modelData" />
931
- </template>
932
- </VanField>
933
-
934
- <!-- 复选框 -->
935
- <!-- <VanField
936
- v-if="attr.type === 'checkbox'"
937
- name="checkbox"
938
- :label="labelData"
939
- >
940
- <template #input>
941
- <VanCheckbox v-model="modelData" shape="square" />
942
- </template>
943
- </VanField> -->
944
-
945
- <!-- 多选框-checkbox-复选框组 -->
946
- <template v-if="attr.type === 'checkbox' && showItem">
947
- <!-- 勾选 -->
948
- <VanField
949
- v-if="attr.showMode === 'checkbox' && mode !== '查询'"
950
- name="checkboxGroup"
951
- :label="labelData"
952
- :label-align="labelAlign"
953
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
954
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
955
- :required="attr.rule.required === 'true'"
956
- >
957
- <template #input>
958
- <van-checkbox-group v-model="modelData as any[]" direction="horizontal" shape="square" :disabled="readonly">
959
- <VanCheckbox v-for="(item, index) in option" :key="index" style="padding: 2px" :name="item[columnsField.value]" :shape="rules?.[attr.model].shape" :value="item[columnsField.value]">
960
- {{ item[columnsField.text] }}
961
- </VanCheckbox>
962
- </van-checkbox-group>
963
- </template>
964
- </VanField>
965
- <VanField
966
- v-if="attr.showMode === 'checkbox' && mode === '查询'"
967
- name="checkboxGroup"
968
- :label="labelData"
969
- :label-align="labelAlign"
970
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
971
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
972
- >
973
- <template #input>
974
- <XGridDropOption
975
- v-model="(modelData as string[])"
976
- :column-num="labelData ? 3 : 4"
977
- :multiple="true"
978
- :columns="option"
979
- />
980
- </template>
981
- </VanField>
982
- <!-- 下拉 -->
983
- <XMultiSelect
984
- v-if="attr.showMode === 'select' && mode === '查询'"
985
- v-model="modelData"
986
- :label="labelData"
987
- :readonly="readonly"
988
- :placeholder="placeholder"
989
- :columns="option"
990
- :select-value="Array.isArray(modelData) ? modelData : []"
991
- :option="attr.option ? attr.option : columnsField"
992
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
993
- :required="attr.rule.required === 'true'"
994
- />
995
- </template>
996
-
997
- <!-- 单选框 -->
998
- <VanField
999
- v-if="attr.type === 'radio' && mode !== '查询' && showItem"
1000
- name="radio"
1001
- center
1002
- :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1003
- :label="labelData"
1004
- :label-align="labelAlign"
1005
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1006
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1007
- :required="attr.rule.required === 'true'"
1008
- >
1009
- <template #input>
1010
- <VanRadioGroup v-model="modelData" direction="horizontal" :disabled="readonly">
1011
- <VanRadio v-for="(item, index) in option" :key="index" style="padding: 2px" :name="item[columnsField.value]" :value="item[columnsField.value]">
1012
- {{ item[columnsField.text] }}
1013
- </VanRadio>
1014
- </VanRadioGroup>
1015
- </template>
1016
- </VanField>
1017
-
1018
- <!-- 单选框-查询 -->
1019
- <VanField
1020
- v-if="attr.type === 'radio' && mode === '查询' && showItem"
1021
- name="radio"
1022
- :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1023
- :label="labelData"
1024
- :label-align="labelAlign"
1025
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1026
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1027
- >
1028
- <template #input>
1029
- <XGridDropOption
1030
- v-model="(modelData as string)"
1031
- :column-num="labelData ? 3 : 4"
1032
- :columns="option"
1033
- />
1034
- </template>
1035
- </VanField>
1036
-
1037
- <!-- 步进器 -->
1038
- <VanField
1039
- v-if="attr.type === 'stepper' && showItem"
1040
- name="stepper"
1041
- :label="labelData"
1042
- :label-align="labelAlign"
1043
- :input-align="attr.inputAlign ? attr.inputAlign : 'center'"
1044
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1045
- :required="attr.rule.required === 'true'"
1046
- >
1047
- <template #input>
1048
- <VanStepper v-model="modelData as any" :disabled="readonly" />
1049
- </template>
1050
- </VanField>
1051
-
1052
- <!-- 评分 -->
1053
- <VanField
1054
- v-if="attr.type === 'rate' && showItem"
1055
- name="rate"
1056
- :label="labelData"
1057
- :label-align="labelAlign"
1058
- :input-align="attr.inputAlign ? attr.inputAlign : 'center'"
1059
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1060
- :required="attr.rule.required === 'true'"
1061
- >
1062
- <template #input>
1063
- <VanRate v-model="modelData as number" :size="25" :readonly="readonly" void-color="#eee" void-icon="star" color="#ffd21e" />
1064
- </template>
1065
- </VanField>
1066
-
1067
- <!-- 滑块 -->
1068
- <VanField
1069
- v-if="attr.type === 'slider' && showItem"
1070
- name="slider"
1071
- :label="labelData"
1072
- :label-align="labelAlign"
1073
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1074
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1075
- :required="attr.rule.required === 'true'"
1076
- >
1077
- <template #input>
1078
- <VanSlider v-model="modelData as number" :readonly="readonly" />
1079
- </template>
1080
- </VanField>
1081
-
1082
- <!-- 文件上传 -->
1083
- <!-- 图片上传, 手机端拍照 -->
1084
- <VanField
1085
- v-if="(attr.type === 'image' || attr.type === 'file') && showItem"
1086
- name="image"
1087
- :label="labelData"
1088
- :label-align="labelAlign"
1089
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1090
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1091
- :required="attr.rule.required === 'true'"
1092
- >
1093
- <template #input>
1094
- <ImageUploader
1095
- upload-mode="server"
1096
- :image-list="(modelData as any[])"
1097
- authority="admin"
1098
- :attr="attr"
1099
- :mode="props.mode"
1100
- :readonly="readonly"
1101
- :is-async-upload="isAsyncUpload"
1102
- @update-file-list="updateFile"
1103
- />
1104
- </template>
1105
- </VanField>
1106
-
1107
- <!-- 选择器 琉璃中不存在,不进行维护后续将删除 -->
1108
- <VanField
1109
- v-if="attr.type === 'picker' && showItem"
1110
- v-model="pickerValue"
1111
- name="picker"
1112
- :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1113
- :label="labelData"
1114
- :label-align="labelAlign"
1115
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1116
- readonly
1117
- is-link
1118
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1119
- @click="readonly ? null : showPicker = true"
1120
- />
1121
- <VanPopup v-model:show="showPicker" round position="bottom" teleport="body" overlay-class="date-picker-overlay">
1122
- <VanPicker
1123
- v-model="(modelData as Numeric[])"
1124
- :title="attr.name"
1125
- :columns="attr.selectKey"
1126
- :readonly="readonly"
1127
- :columns-field-names="attr.customFieldName ? attr.customFieldName : { text: 'text', value: 'value', children: 'children' }"
1128
- :confirm-button-text="attr.confirmButtonText || attr.confirmButtonText === '' ? attr.confirmButtonText : '确认'"
1129
- :cancel-button-text="attr.cancelButtonText || attr.cancelButtonText === '' ? attr.cancelButtonText : '取消'"
1130
- @cancel="showPicker = false"
1131
- @confirm="onPickerConfirm"
1132
- />
1133
- </VanPopup>
1134
-
1135
- <!-- 日历选择-查询 -->
1136
- <VanField
1137
- v-if="attr.type === 'rangePicker' && mode === '查询' && showItem"
1138
- v-model="(pickerValue as string | number)"
1139
- is-link
1140
- readonly
1141
- name="rangePicker"
1142
- :label="labelData"
1143
- :label-align="labelAlign"
1144
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1145
- :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1146
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1147
- @click="calendarShow = true"
1148
- />
1149
- <VanCalendar
1150
- v-model:show="calendarShow"
1151
- switch-mode="year-month"
1152
- type="range"
1153
- teleport="body"
1154
- overlay-class="date-picker-overlay"
1155
- :show-confirm="attr.showConfirm"
1156
- @confirm="onCalendarConfirm"
1157
- />
1158
-
1159
- <!-- 日期选择-非查询 -->
1160
- <VanField
1161
- v-if="(attr.type === 'datePicker' || attr.type === 'rangePicker') && mode !== '查询' && showItem"
1162
- v-model="(modelData as string | number)"
1163
- name="datePicker"
1164
- :label="labelData"
1165
- :label-align="labelAlign"
1166
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1167
- readonly
1168
- :is-link="true"
1169
- :placeholder="placeholder"
1170
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1171
- :required="attr.rule.required === 'true'"
1172
- @click="readonly ? null : showDataTimePicker()"
1173
- />
1174
- <VanPopup v-model:show="showDatePicker" position="bottom" teleport="body" overlay-class="date-picker-overlay">
1175
- <VanPickerGroup
1176
- :title="attr.name"
1177
- :tabs="hasTime ? ['选择日期', '选择时间'] : ['选择日期']"
1178
- next-step-text="下一步"
1179
- :confirm-button-text="attr.confirmButtonText ? attr.confirmButtonText : '确认'"
1180
- :cancel-button-text="attr.cancelButtonText ? attr.cancelButtonText : '取消'"
1181
- @confirm="onDateTimePickerConfirm"
1182
- @cancel="onPickerCancel"
1183
- >
1184
- <VanDatePicker
1185
- v-model="dateTimePickerValue.date"
1186
- :columns-type="attr.dateColumnsType || dateColumnsType"
1187
- />
1188
- <VanTimePicker
1189
- v-if="hasTime"
1190
- v-model="dateTimePickerValue.time"
1191
- :columns-type="attr.timeColumnsType || timeColumnsType"
1192
- :min-time="attr.minTime ? attr.minTime : '00:00:00'"
1193
- :max-time="attr.maxTime ? attr.maxTime : '23:59:59'"
1194
- />
1195
- </VanPickerGroup>
1196
- </VanPopup>
1197
-
1198
- <!-- 日期选择-查询 -->
1199
- <VanField
1200
- v-if="attr.type === 'datePicker' && mode === '查询' && showItem"
1201
- v-model="(modelData as string | number)"
1202
- name="datePicker"
1203
- :label="labelData"
1204
- :label-align="labelAlign"
1205
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1206
- readonly
1207
- :is-link="true"
1208
- :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1209
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1210
- @click="readonly ? null : showDataTimePicker()"
1211
- />
1212
- <VanPopup v-model:show="showDatePicker" position="bottom" teleport="body" overlay-class="date-picker-overlay">
1213
- <VanPickerGroup
1214
- :title="attr.name"
1215
- :tabs="hasTime ? ['选择日期', '选择时间'] : ['选择日期']"
1216
- next-step-text="下一步"
1217
- :confirm-button-text="attr.confirmButtonText ? attr.confirmButtonText : '确认'"
1218
- :cancel-button-text="attr.cancelButtonText ? attr.cancelButtonText : '取消'"
1219
- @confirm="onDateTimePickerConfirm"
1220
- @cancel="onPickerCancel"
1221
- >
1222
- <VanDatePicker
1223
- v-model="dateTimePickerValue.date"
1224
- :columns-type="attr.dateColumnsType || dateColumnsType"
1225
- />
1226
- <VanTimePicker
1227
- v-if="hasTime"
1228
- v-model="dateTimePickerValue.time"
1229
- :columns-type="attr.timeColumnsType || timeColumnsType"
1230
- :min-time="attr.minTime ? attr.minTime : '00:00:00'"
1231
- :max-time="attr.maxTime ? attr.maxTime : '23:59:59'"
1232
- />
1233
- </VanPickerGroup>
1234
- </VanPopup>
1235
-
1236
- <!-- 时间选择 --该配置未在pc找到不进行维护 后续将删除 -->
1237
- <VanField
1238
- v-if="attr.type === 'timePicker' && showItem"
1239
- v-model="timePickerValue"
1240
- name="timePicker"
1241
- is-link
1242
- readonly
1243
- :placeholder="attr.placeholder"
1244
- :label="labelData"
1245
- :label-align="labelAlign"
1246
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1247
- :rules="[{ required: attr.rule.required === 'true', message: '请选择' }]"
1248
- @click="readonly ? null : (showTimePicker = true)"
1249
- />
1250
- <VanPopup v-model:show="showTimePicker" position="bottom" teleport="body" overlay-class="date-picker-overlay">
1251
- <VanTimePicker
1252
- v-model="modelData as string[]"
1253
- :title="attr.name"
1254
- :columns-type="attr.columnsType ? attr.columnsType : ['hour', 'minute', 'second']"
1255
- :min-time="attr.minTime ? attr.minTime : '00:00:00'"
1256
- :max-time="attr.maxTime ? attr.maxTime : '23:59:59'"
1257
- :readonly="readonly"
1258
- @cancel="showTimePicker = false"
1259
- @confirm="onTimePickerConfirm"
1260
- />
1261
- </VanPopup>
1262
-
1263
- <!-- 省市区选择 -->
1264
- <VanField
1265
- v-if="(attr.type === 'area' || attr.type === 'citySelect') && showItem"
1266
- v-model="area"
1267
- name="area"
1268
- :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1269
- is-link
1270
- readonly
1271
- :label="labelData"
1272
- :label-align="labelAlign"
1273
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1274
- :rules="[{ required: attr.rule.required === 'true', message: '请选择' }]"
1275
- :required="attr.rule.required === 'true'"
1276
- @click="readonly ? null : (showArea = true)"
1277
- />
1278
- <VanPopup v-model:show="showArea" position="bottom" teleport="body" overlay-class="date-picker-overlay">
1279
- <VanArea
1280
- v-model="modelData as string" :title="attr.name" :area-list="areaList"
1281
- @confirm="onAreaConfirm"
1282
- @cancel="showArea = false"
1283
- />
1284
- </VanPopup>
1285
-
1286
- <!-- 单选下拉列表 -->
1287
- <XSelect
1288
- v-if="attr.type === 'select' && showItem"
1289
- v-model="modelData"
1290
- :label="labelData"
1291
- :readonly="readonly"
1292
- clearable
1293
- :placeholder="placeholder"
1294
- :columns="option"
1295
- :option="attr.option ? attr.option : columnsField"
1296
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1297
- :required="attr.rule.required === 'true'"
1298
- />
1299
-
1300
- <!-- 文本区域 -->
1301
- <VanField
1302
- v-if="attr.type === 'textarea' && showItem"
1303
- v-model="(modelData as string)"
1304
- rows="3"
1305
- autosize
1306
- :label="labelData"
1307
- :label-align="labelAlign"
1308
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1309
- type="textarea"
1310
- :readonly="readonly"
1311
- :maxlength="attr.maxlength ? attr.maxlength : 200"
1312
- :placeholder="attr.placeholder ? attr.placeholder : `请输入${attr.name}`"
1313
- show-word-limit
1314
- :rules="[{ required: attr.rule.required === 'true', message: `请填写${attr.name}` }]"
1315
- :required="attr.rule.required === 'true'"
1316
- />
1317
-
1318
- <!-- 文本输入框 -->
1319
- <VanField
1320
- v-if="(attr.type === 'input' || attr.type === 'intervalPicker') && showItem"
1321
- v-model="(modelData as string)"
1322
- style="align-items: center"
1323
- :label="labelData"
1324
- :label-align="labelAlign"
1325
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1326
- :type="attr.type as FieldType"
1327
- :readonly="readonly"
1328
- :disabled="attr.disabled"
1329
- :placeholder="placeholder"
1330
- :error-message="errorMessage"
1331
- :clearable="attr.clearable"
1332
- :rules="[{ required: attr.rule.required === 'true', message: `请填写${attr.name}` }]"
1333
- :required="attr.rule.required === 'true'"
1334
- @blur="() => formTypeCheck(attr, modelData as string)"
1335
- >
1336
- <template #input>
1337
- <input
1338
- :value="modelData"
1339
- :readonly="readonly"
1340
- class="van-field__control"
1341
- :placeholder="placeholder"
1342
- style="flex: 1; min-width: 0;"
1343
- @input="e => modelData = (e.target as HTMLInputElement).value"
1344
- @blur="() => formTypeCheck(attr, modelData as string)"
1345
- >
1346
- <VanButton
1347
- v-if="attr.inputOnAfterName && attr.inputOnAfterFunc && !attr.inputOnAfterName.includes('|')"
1348
- class="action-btn"
1349
- round
1350
- type="primary"
1351
- size="small"
1352
- @click="emitFunc(attr.inputOnAfterFunc, attr)"
1353
- >
1354
- {{ attr.inputOnAfterName }}
1355
- </VanButton>
1356
- </template>
1357
- </VanField>
1358
-
1359
- <!-- 地址选择器 -->
1360
- <VanField
1361
- v-if="attr.type === 'addressSearch' && showItem"
1362
- v-model="modelData as string"
1363
- name="addressSearch"
1364
- :label="labelData"
1365
- :label-align="labelAlign"
1366
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1367
- readonly
1368
- is-link
1369
- :placeholder="placeholder"
1370
- :rules="[{ required: attr.rule.required === 'true', message: '请选择地址' }]"
1371
- :required="attr.rule.required === 'true'"
1372
- @click="readonly ? null : (showAddressPicker = true)"
1373
- />
1374
- <VanPopup
1375
- v-model:show="showAddressPicker"
1376
- position="bottom"
1377
- :style="{ height: '80vh' }"
1378
- teleport="body"
1379
- overlay-class="date-picker-overlay"
1380
- >
1381
- <XLocationPicker
1382
- :default-center="defaultMapCenter"
1383
- :service-name="serviceName"
1384
- @confirm="handleAddressConfirm"
1385
- />
1386
- </VanPopup>
1387
-
1388
- <!-- pc的树形选择框————》 手机端采用 Cascader 级联选择 -->
1389
- <VanField
1390
- v-if="attr.type === 'treeSelect' && showItem"
1391
- v-model="treeValue"
1392
- name="treeSelect"
1393
- :label="labelData"
1394
- :label-align="labelAlign"
1395
- :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1396
- readonly
1397
- is-link
1398
- :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1399
- :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1400
- :required="attr.rule.required === 'true'"
1401
- @click="readonly ? null : (showTreeSelect = true)"
1402
- />
1403
- <VanPopup
1404
- v-model:show="showTreeSelect"
1405
- position="bottom"
1406
- teleport="body"
1407
- overlay-class="date-picker-overlay"
1408
- >
1409
- <VanCascader
1410
- :options="cleanEmptyChildren(option, attr.customFieldName ? attr.customFieldName : { text: 'label', value: 'value', children: 'children' })"
1411
- :field-names="attr.customFieldName ? attr.customFieldName : { text: 'label', value: 'value', children: 'children' }"
1412
- :title="attr.name"
1413
- :closeable="true"
1414
- @close="showTreeSelect = false"
1415
- @finish="onTreeSelectFinish"
1416
- />
1417
- </VanPopup>
1418
- </div>
1419
- </template>
1420
-
1421
- <style scoped>
1422
- .date-picker-overlay {
1423
- background-color: rgba(0, 0, 0, 0.2); /* 设置为半透明的黑色 */
1424
- }
1425
- .action-btn {
1426
- border-radius: 10px;
1427
- margin-left: 8px;
1428
- min-width: 4rem;
1429
- max-width: 6rem;
1430
- }
1431
- </style>
1
+ <script setup lang="ts">
2
+ import type { FieldType } from 'vant'
3
+ import type { Numeric } from 'vant/es/utils'
4
+ import ImageUploader from '@af-mobile-client-vue3/components/core/ImageUploader/index.vue'
5
+ import XGridDropOption from '@af-mobile-client-vue3/components/core/XGridDropOption/index.vue'
6
+ import XMultiSelect from '@af-mobile-client-vue3/components/core/XMultiSelect/index.vue'
7
+ import XSelect from '@af-mobile-client-vue3/components/core/XSelect/index.vue'
8
+ import XLocationPicker from '@af-mobile-client-vue3/components/data/XOlMap/XLocationPicker/index.vue'
9
+ import { getConfigByNameAsync, runLogic } from '@af-mobile-client-vue3/services/api/common'
10
+ import { post } from '@af-mobile-client-vue3/services/restTools'
11
+ import { searchToListOption, searchToOption } from '@af-mobile-client-vue3/services/v3Api'
12
+ import { useUserStore } from '@af-mobile-client-vue3/stores/modules/user'
13
+ import { getDict } from '@af-mobile-client-vue3/utils/dictUtil'
14
+ import { executeStrFunctionByContext } from '@af-mobile-client-vue3/utils/runEvalFunction'
15
+ import { areaList } from '@vant/area-data'
16
+ import dayjs from 'dayjs/esm/index'
17
+ import { debounce } from 'lodash-es'
18
+ import {
19
+ Area as VanArea,
20
+ Button as VanButton,
21
+ Calendar as VanCalendar,
22
+ Cascader as VanCascader,
23
+ Checkbox as VanCheckbox,
24
+ CheckboxGroup as vanCheckboxGroup,
25
+ DatePicker as VanDatePicker,
26
+ Field as VanField,
27
+ Picker as VanPicker,
28
+ PickerGroup as VanPickerGroup,
29
+ Popup as VanPopup,
30
+ Radio as VanRadio,
31
+ RadioGroup as VanRadioGroup,
32
+ Rate as VanRate,
33
+ Slider as VanSlider,
34
+ Stepper as VanStepper,
35
+ Switch as VanSwitch,
36
+ TimePicker as VanTimePicker,
37
+ } from 'vant'
38
+ import { computed, defineEmits, defineModel, defineProps, getCurrentInstance, onBeforeMount, ref, watch } from 'vue'
39
+
40
+ const props = defineProps({
41
+ attr: {
42
+ type: Object,
43
+ },
44
+ form: {
45
+ type: Object,
46
+ },
47
+ // 整表只读:来自 XForm,统一控制交互
48
+ formReadonly: {
49
+ type: Boolean,
50
+ default: false,
51
+ },
52
+ datePickerFilter: {
53
+ type: Function,
54
+ default: () => true,
55
+ },
56
+ datePickerFormatter: {
57
+ type: Function,
58
+ default: (type, val) => val,
59
+ },
60
+ mode: {
61
+ type: String,
62
+ default: '查询',
63
+ },
64
+ serviceName: {
65
+ type: String,
66
+ default: undefined,
67
+ },
68
+ // 调用logic获取数据源的追加参数
69
+ getDataParams: {
70
+ type: Object,
71
+ default: undefined,
72
+ },
73
+ disabled: {
74
+ type: Boolean,
75
+ default: false,
76
+ },
77
+ rules: {
78
+ type: Object,
79
+ default: () => {},
80
+ },
81
+ // 用 defineModel 替代 modelValue/emit
82
+ modelValue: {
83
+ type: [String, Number, Boolean, Array, Object],
84
+ default: undefined,
85
+ },
86
+ showLabel: {
87
+ type: Boolean,
88
+ default: true,
89
+ },
90
+ // radio/checkbox/select/mul-select 选项数据结构
91
+ columnsField: {
92
+ type: Object,
93
+ default: () => {
94
+ return { text: 'label', value: 'value' }
95
+ },
96
+ },
97
+ isAsyncUpload: {
98
+ type: Boolean,
99
+ default: false,
100
+ },
101
+
102
+ })
103
+
104
+ const emits = defineEmits(['setForm', 'xFormItemEmitFunc'])
105
+
106
+ // 用 defineModel 替代 modelValue/emit
107
+ const modelData = defineModel<string | number | boolean | any[] | Record<string, any>>()
108
+
109
+ // 获取字典
110
+ interface OptionItem {
111
+ label: string
112
+ value: any
113
+ children?: OptionItem[]
114
+ }
115
+
116
+ // 判断并初始化防抖函数
117
+ let debouncedUserLinkFunc: (() => void) | null = null
118
+ let debouncedDepLinkFunc: (() => void) | null = null
119
+ let debouncedUpdateOptions: (() => void) | null = null
120
+
121
+ const { attr, form, mode, serviceName, getDataParams, columnsField } = props
122
+ // 配置的表单值格式(仅针对 datePicker 生效)
123
+ // 作用:统一控制日期值的格式化输入/输出与选择器展示粒度
124
+ // 可选:'YYYY' | 'YYYY-MM' | 'YYYY-MM-DD' | 'YYYY-MM-DD HH' | 'YYYY-MM-DD HH:mm' | 'YYYY-MM-DD HH:mm:ss'
125
+ const formValueFormat = computed(() => {
126
+ // 默认全格式
127
+ return (attr && (attr as any).formValueFormat) || 'YYYY-MM-DD HH:mm:ss'
128
+ })
129
+
130
+ // 根据 formValueFormat 动态计算日期列类型(决定 VanDatePicker 展示年/月/日)
131
+ // 例如:YYYY 只显示年;YYYY-MM 显示年、月;YYYY-MM-DD 显示年、月、日
132
+ const dateColumnsType = computed(() => {
133
+ const format = formValueFormat.value
134
+ const columns: string[] = ['year']
135
+ if (format.includes('MM'))
136
+ columns.push('month')
137
+ if (format.includes('DD'))
138
+ columns.push('day')
139
+ return columns as any
140
+ })
141
+
142
+ // 是否包含时间(决定是否展示时间页签与 VanTimePicker)
143
+ const hasTime = computed(() => formValueFormat.value.includes('HH'))
144
+
145
+ // 根据 formValueFormat 动态计算时间列类型(决定时/分/秒的展示)
146
+ // 例如:包含 HH 显示小时;包含 mm 显示分钟;包含 ss 显示秒
147
+ const timeColumnsType = computed(() => {
148
+ const format = formValueFormat.value
149
+ const columns: string[] = []
150
+ if (format.includes('HH'))
151
+ columns.push('hour')
152
+ if (format.includes('mm'))
153
+ columns.push('minute')
154
+ if (format.includes('ss'))
155
+ columns.push('second')
156
+ return columns as any
157
+ })
158
+ const calendarShow = ref(false)
159
+ const option = ref([])
160
+ const pickerValue = ref(undefined)
161
+ const timePickerValue = ref(undefined)
162
+ const area = ref<any>(undefined)
163
+ const showPicker = ref(false)
164
+ const showDatePicker = ref(false)
165
+ const showTimePicker = ref(false)
166
+ const showArea = ref(false)
167
+ const errorMessage = ref('')
168
+ const showTreeSelect = ref(false)
169
+ const treeValue = ref('')
170
+
171
+ // 登录信息 (可以在配置的动态函数中使用 this.setupState 获取到当前组件内的全部函数和变量 例:this.setupState.userState)
172
+ const userState = useUserStore().getLogin()
173
+ const currUser = computed(() => userState.f.resources.id)
174
+ const userInfo = computed(() => ({
175
+ orgId: userState.f.resources.orgid,
176
+ userId: userState.f.resources.id,
177
+ }))
178
+
179
+ // 是否展示当前项
180
+ const showItem = ref(true)
181
+
182
+ // 当前组件实例(不推荐使用,可能会在后续的版本更迭中调整,暂时用来绑定函数的上下文)
183
+ const currInst = getCurrentInstance()
184
+
185
+ // 配置中心->表单项变更触发函数
186
+ async function dataChangeFunc() {
187
+ if (attr.dataChangeFunc) {
188
+ await executeStrFunctionByContext(currInst, attr.dataChangeFunc, [props.form, (formData: any) => emits('setForm', formData), attr, null, mode, runLogic, getConfigByNameAsync])
189
+ }
190
+ }
191
+ const dataChangeFuncdebounce = debounce(dataChangeFunc, 300)
192
+ // 配置中心->表单项展示函数
193
+ async function showFormItemFunc() {
194
+ if (attr.showFormItemFunc) {
195
+ const obj = await executeStrFunctionByContext(currInst, attr.showFormItemFunc, [form, attr, null, mode])
196
+ // 判断是 bool 还是 obj 兼容
197
+ if (typeof obj === 'boolean') {
198
+ showItem.value = obj
199
+ }
200
+ else if (obj && typeof obj === 'object') {
201
+ // obj 是一个对象,并且不是数组
202
+ showItem.value = obj?.show
203
+ }
204
+ }
205
+ }
206
+ const showFormItemFuncdebounce = debounce(showFormItemFunc, 300)
207
+ /**
208
+ * 检测是否传入了有效的值
209
+ * @returns any
210
+ */
211
+ function checkModel(val = props.modelValue) {
212
+ if (val === null || val === undefined || val === '')
213
+ return false
214
+ if (Array.isArray(val))
215
+ return val.length > 0
216
+ return true
217
+ }
218
+ /**
219
+ * 获取表单项的默认值
220
+ * @returns any
221
+ */
222
+ function getDefaultValue() {
223
+ const val = props.modelValue
224
+ console.warn('>>>>>props', props)
225
+ console.warn('>>>> attr', attr)
226
+ // 如果有有效值,直接返回(datePicker 初始值需按 formValueFormat 归一)
227
+ // 目的:外部通过 formData 传入的初始值在组件挂载即与配置格式一致
228
+ if (checkModel(val)) {
229
+ if (attr.type === 'datePicker' && typeof val === 'string') {
230
+ const parsed = dayjs(val)
231
+ if (parsed.isValid())
232
+ return parsed.format(formValueFormat.value)
233
+ }
234
+ return val
235
+ }
236
+
237
+ // 根据类型获取默认值
238
+ const getDefaultByType = () => {
239
+ const def = mode !== '查询' ? attr.formDefault : attr.queryFormDefault
240
+
241
+ if (['treeSelect', 'select', 'checkbox'].includes(attr.type) && ['curOrgId', 'curDepId', 'curUserId'].includes(def)) {
242
+ if (def === 'curOrgId') {
243
+ if (attr.type === 'treeSelect') {
244
+ treeValue.value = userState.f.resources.orgs
245
+ }
246
+ return attr.type === 'select' ? userState.f.resources.orgid : [userState.f.resources.orgid]
247
+ }
248
+ if (def === 'curDepId') {
249
+ if (attr.type === 'treeSelect') {
250
+ treeValue.value = userState.f.resources.deps
251
+ }
252
+ return attr.type === 'select' ? userState.f.resources.depids : [userState.f.resources.depids]
253
+ }
254
+ if (def === 'curUserId') {
255
+ if (attr.type === 'treeSelect') {
256
+ treeValue.value = userState.f.resources.name
257
+ }
258
+ return attr.type === 'select' ? userState.f.resources.id : [userState.f.resources.id]
259
+ }
260
+ }
261
+
262
+ // 数组类型默认值
263
+ const arrayTypes = ['uploader', 'checkbox', 'file', 'area', 'image', 'treeSelect']
264
+ if (arrayTypes.includes(attr.type)) {
265
+ return def !== undefined ? def : []
266
+ }
267
+
268
+ // 特殊类型默认值
269
+ const specialDefaults = {
270
+ switch: false,
271
+ stepper: 1,
272
+ addressSearch: val,
273
+ }
274
+
275
+ if (specialDefaults[attr.type] !== undefined) {
276
+ return specialDefaults[attr.type]
277
+ }
278
+
279
+ // 日期时间类型:调用 getDateRange,并传入 formValueFormat
280
+ // 说明:让初始化/查询默认值同样遵循配置的格式显示
281
+ const dateTypes = ['rangePicker', 'yearPicker', 'monthPicker', 'yearRangePicker', 'monthRangePicker', 'datePicker', 'timePicker']
282
+ if (dateTypes.includes(attr.type)) {
283
+ return getDateRange({
284
+ type: attr.type,
285
+ formDefault: attr.formDefault ?? '',
286
+ queryFormDefault: attr.queryFormDefault ?? '',
287
+ queryType: attr.queryType,
288
+ queryValueFormat: attr.queryValueFormat,
289
+ name: attr.name ?? '',
290
+ formValueFormat: formValueFormat.value,
291
+ }) ?? []
292
+ }
293
+
294
+ // 其他类型(字符串、数字等)
295
+ return def ?? ''
296
+ }
297
+
298
+ return getDefaultByType()
299
+ }
300
+
301
+ // 初始化日期组件初始值,组件自定义格式显示值和实际值(日期相关初始化都在此函数中操作)
302
+ function getDateRange({
303
+ type,
304
+ formDefault: defaultValue,
305
+ queryFormDefault,
306
+ queryType,
307
+ queryValueFormat: defaultFormat,
308
+ name,
309
+ formValueFormat: formFormat,
310
+ }: {
311
+ type: string
312
+ formDefault: string
313
+ queryFormDefault: string
314
+ queryType?: string
315
+ queryValueFormat?: string
316
+ name: string
317
+ // 新增:用于优先覆盖 datePicker 的显示/存储格式
318
+ formValueFormat?: string
319
+ }): string | [string, string] | undefined {
320
+ const formatMap: Record<string, string> = {
321
+ yearPicker: 'YYYY',
322
+ yearRangePicker: 'YYYY',
323
+ monthPicker: 'YYYY-MM',
324
+ monthRangePicker: 'YYYY-MM',
325
+ datePicker: 'YYYY-MM-DD HH:mm:ss',
326
+ rangePicker: 'YYYY-MM-DD HH:mm:ss',
327
+ }
328
+ if (mode) {
329
+ // datePicker 优先使用 formValueFormat(否则退回 queryValueFormat 或默认映射)
330
+ const preferFormat = type === 'datePicker' ? (formFormat || defaultFormat) : defaultFormat
331
+ const format = preferFormat || formatMap[type]
332
+ const val = mode === '查询' ? queryFormDefault : defaultValue
333
+ let start: string, end: string
334
+ switch (val) {
335
+ case 'curYear':
336
+ start = dayjs().startOf('year').format(format)
337
+ end = dayjs().endOf('year').format(format)
338
+ break
339
+ case 'curMonth':
340
+ start = dayjs().startOf('month').format(format)
341
+ end = dayjs().endOf('month').format(format)
342
+ break
343
+ case 'curDay':
344
+ start = dayjs().startOf('day').format(format)
345
+ end = dayjs().endOf('day').format(format)
346
+ break
347
+ case 'curTime':
348
+ start = dayjs().format(format)
349
+ end = dayjs().format(format)
350
+ break
351
+ default:
352
+ return undefined
353
+ }
354
+ if (['monthPicker', 'yearPicker', 'datePicker'].includes(type)) {
355
+ if (mode !== '查询') {
356
+ if (queryType === 'BETWEEN') {
357
+ return [start, end]
358
+ }
359
+ if (name.includes('开始') || name.includes('起始')) {
360
+ return start
361
+ }
362
+ else {
363
+ return end
364
+ }
365
+ }
366
+ else {
367
+ return start
368
+ }
369
+ }
370
+ // rangePicker组件表单显示的值
371
+ if (mode === '查询' && type === 'rangePicker') {
372
+ pickerValue.value = `${start} ~ ${end}`
373
+ }
374
+ return mode !== '查询' ? start : [start, end]
375
+ }
376
+ else {
377
+ return undefined
378
+ }
379
+ }
380
+
381
+ // 监听 props.form 的变化
382
+ watch(
383
+ () => props.form,
384
+ (newVal, oldVal) => {
385
+ // 如果是从函数获取 options
386
+ if (props.attr.keyName && (props.attr.keyName.toString().includes('async ') || props.attr.keyName.toString().includes('function'))) {
387
+ debouncedUpdateOptions()
388
+ }
389
+ if (props.attr.showFormItemFunc) {
390
+ showFormItemFuncdebounce()
391
+ }
392
+ },
393
+ { deep: true },
394
+ )
395
+
396
+ // 监听 modelData 的变化,调用 dataChangeFunc
397
+ watch(
398
+ () => modelData.value,
399
+ (newVal, oldVal) => {
400
+ // 避免初始化时的调用
401
+ if (newVal !== oldVal) {
402
+ dataChangeFuncdebounce()
403
+ }
404
+ },
405
+ { deep: true },
406
+ )
407
+
408
+ // 监听 option.value 的变化
409
+ watch(
410
+ () => option.value,
411
+ (newOption) => {
412
+ if (attr.type === 'treeSelect' && newOption && newOption.length > 0) {
413
+ // 你可以在这里调用 findOptionInTree 函数来查找默认值对应的 label
414
+ const result = findOptionInTree(option.value, modelData.value)
415
+ if (attr.type === 'treeSelect' && result)
416
+ treeValue.value = result.label
417
+ }
418
+ },
419
+ )
420
+
421
+ function updateFile(files, _index) {
422
+ modelData.value = files
423
+ }
424
+
425
+ // 表单校验的类型校验
426
+ function formTypeCheck(attr, value) {
427
+ if (mode === '查询' || mode === '预览')
428
+ return
429
+ // if (!attr.rule || !attr.rule.required || attr.rule.required === 'false')
430
+ // return
431
+ switch (attr.rule.type) {
432
+ case 'string':
433
+ if (value.length === 0) {
434
+ errorMessage.value = `请输入${attr.name}`
435
+ return
436
+ }
437
+ break
438
+ case 'number':
439
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(value)) {
440
+ errorMessage.value = `${attr.name}必须为数字`
441
+ return
442
+ }
443
+ break
444
+ case 'boolean':
445
+ case 'array':
446
+ case 'regexp':
447
+ if (value) {
448
+ errorMessage.value = ''
449
+ return
450
+ }
451
+ break
452
+ case 'integer':
453
+ if (!/^-?\d+$/.test(value)) {
454
+ errorMessage.value = `${attr.name}必须为整数`
455
+ return
456
+ }
457
+ break
458
+ case 'float':
459
+ if (!/^-?\d+\.\d+$/.test(value)) {
460
+ errorMessage.value = `${attr.name}必须为小数`
461
+ return
462
+ }
463
+ break
464
+ case 'email':
465
+ if (!/^[\w.%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(value)) {
466
+ errorMessage.value = `请输入正确的邮箱地址`
467
+ return
468
+ }
469
+ break
470
+ case 'idNumber':
471
+ if (!/^(?:\d{15}|\d{17}[0-9X])$/.test(value)) {
472
+ errorMessage.value = `请输入正确的身份证号码`
473
+ return
474
+ }
475
+ break
476
+ case 'userPhone':
477
+ if (!/^1[3-9]\d{9}$/.test(value)) {
478
+ errorMessage.value = `请输入正确的手机号码`
479
+ return
480
+ }
481
+ break
482
+ case 'landlineNumber':
483
+ if (!/^0\d{2,3}[-\s]?\d{7,8}$/.test(value)) {
484
+ errorMessage.value = `请输入正确的座机号码`
485
+ return
486
+ }
487
+ break
488
+ case 'greaterThanZero':
489
+ if (!/^(?:[1-9]\d*(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(value)) {
490
+ errorMessage.value = `请输入一个大于0的数字`
491
+ return
492
+ }
493
+ break
494
+ case 'greaterThanOrEqualZero':
495
+ if (!/^(?:\d+|\d*\.\d+)$/.test(value)) {
496
+ errorMessage.value = `请输入一个大于等于0的数字`
497
+ return
498
+ }
499
+ break
500
+ case 'stringLength':
501
+ if (!(value.length >= attr.rule.minLen && value.length < attr.rule.maxLen)) {
502
+ errorMessage.value = `长度必须在${attr.rule.minLen}~${attr.rule.maxLen}之间`
503
+ return
504
+ }
505
+ break
506
+ case 'customJs':
507
+ // eslint-disable-next-line no-case-declarations
508
+ const funcStr = attr.rule.customValidatorFunc
509
+ // 输入的数据是否符合条件
510
+ // eslint-disable-next-line no-case-declarations
511
+ const status = ref(true)
512
+ // 提取函数体部分
513
+ // eslint-disable-next-line no-case-declarations
514
+ const funcBodyMatch = funcStr.match(/function\s*\(.*?\)\s*\{([\s\S]*)\}/)
515
+ if (!funcBodyMatch)
516
+ throw new Error('自定义校验函数不合法')
517
+ // eslint-disable-next-line no-case-declarations
518
+ const funcBody = funcBodyMatch[1].trim() // 提取函数体
519
+ // 使用 new Function 创建函数
520
+ // eslint-disable-next-line no-new-func,no-case-declarations
521
+ const customValidatorFunc = new Function('rule', 'value', 'callback', 'form', 'attr', 'util', funcBody)
522
+ // 定义 callback 函数
523
+ // eslint-disable-next-line no-case-declarations
524
+ const callback = (error) => {
525
+ if (error) {
526
+ errorMessage.value = `${error}`
527
+ status.value = false // 表示有错误发生
528
+ }
529
+ }
530
+ // 调用自定义校验函数
531
+ customValidatorFunc(
532
+ attr.rule,
533
+ value,
534
+ callback,
535
+ form,
536
+ attr,
537
+ {}, // util 对象(可以根据需要传递)
538
+ )
539
+ if (!status.value)
540
+ return
541
+ break
542
+ default:
543
+ errorMessage.value = ''
544
+ break
545
+ }
546
+ errorMessage.value = ''
547
+ }
548
+
549
+ onBeforeMount(() => {
550
+ init()
551
+ modelData.value = getDefaultValue()
552
+ showFormItemFunc()
553
+ dataChangeFunc()
554
+ if (attr?.keyName?.toString()?.startsWith('search@根据表单项[') && attr?.keyName?.toString().endsWith(']联动人员'))
555
+ debouncedUserLinkFunc = debounce(() => updateResOptions('人员'), 200)
556
+
557
+ if (attr?.keyName?.toString()?.startsWith('search@根据表单项[') && attr?.keyName?.toString().endsWith(']联动部门'))
558
+ debouncedDepLinkFunc = debounce(() => updateResOptions('部门'), 200)
559
+
560
+ if (attr.keyName && (attr?.keyName?.toString().indexOf('async ') !== -1 || attr?.keyName?.toString()?.indexOf('function') !== -1)) {
561
+ debouncedUpdateOptions = debounce(updateOptions, 200)
562
+ }
563
+ })
564
+ // 是否展示表单左侧label文字
565
+ const labelData = computed(() => {
566
+ return props.showLabel ? attr.name : null
567
+ })
568
+ // 是否展示表单左侧label文字
569
+ const labelAlign = computed(() => {
570
+ return attr.labelAlign ? attr.labelAlign : 'left'
571
+ })
572
+ // 是否只读
573
+ const readonly = computed(() => {
574
+ return props.formReadonly || attr.addOrEdit === 'readonly' || mode === '预览'
575
+ })
576
+ // 提示内容
577
+ const placeholder = computed(() => {
578
+ if (attr.addOrEdit === 'readonly' || mode === '预览') {
579
+ return '暂无内容 ~ '
580
+ }
581
+ else
582
+ if (attr.placeholder) {
583
+ return attr.placeholder
584
+ }
585
+ else {
586
+ switch (attr.type) {
587
+ case 'datePicker':
588
+ case 'timePicker':
589
+ case 'rangePicker':
590
+ case 'radio':
591
+ case 'select':
592
+ case 'treeSelect':
593
+ case 'area':
594
+ case 'citySelect':
595
+ case 'picker':
596
+ return `请选择${attr.name}`
597
+ case 'addressSearch':
598
+ case 'input':
599
+ case 'textarea':
600
+ case 'intervalPicker':
601
+ return `请输入${attr.name}`
602
+ default:
603
+ return `请选择${attr.name}`
604
+ }
605
+ }
606
+ })
607
+
608
+ const formatDate = date => `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
609
+
610
+ function onCalendarConfirm(values) {
611
+ modelData.value = [formatDate(values[0]), formatDate(values[1])]
612
+ pickerValue.value = `${formatDate(values[0])} ~ ${formatDate(values[1])}`
613
+ calendarShow.value = false
614
+ }
615
+
616
+ // js 函数作为数据源
617
+ async function updateOptions() {
618
+ if (attr.keyName && (attr.keyName.toString().includes('async ') || attr.keyName.toString().includes('function '))) {
619
+ option.value = await executeStrFunctionByContext(this, attr.keyName, [props.form, runLogic, props.mode, getConfigByNameAsync, post])
620
+ }
621
+ }
622
+
623
+ function init() {
624
+ if (attr.keyName && typeof attr.keyName === 'string') {
625
+ if (attr.keyName && attr.keyName.includes('logic@')) {
626
+ getData({}, (res) => {
627
+ option.value = res
628
+ initRadioValue()
629
+ })
630
+ }
631
+ else if (attr.keyName && attr.keyName.includes('config@')) {
632
+ const configName = attr.keyName.substring(7)
633
+ getDict(configName, (result) => {
634
+ if (result)
635
+ option.value = result
636
+ }, serviceName)
637
+ }
638
+ else if (attr.keyName && attr.keyName.includes('search@')) {
639
+ let source = attr.keyName.substring(7)
640
+ const userid = currUser.value
641
+ let roleName = 'roleName'
642
+ if (source.startsWith('根据角色[') && source.endsWith(']获取人员')) {
643
+ const startIndex = source.indexOf('[') + 1
644
+ const endIndex = source.indexOf(']', startIndex)
645
+ roleName = source.substring(startIndex, endIndex)
646
+ source = '根据角色获取人员'
647
+ }
648
+ const searchData = { source, userid, roleName }
649
+ if (source.startsWith('根据表单项[') && source.endsWith(']联动人员'))
650
+ updateResOptions('人员')
651
+ else if (source.startsWith('根据表单项[') && source.endsWith(']联动部门'))
652
+ updateResOptions('部门')
653
+ else if (attr.type === 'select' || attr.type === 'checkbox')
654
+ searchToListOption(searchData, res => getDataCallback(res))
655
+ else
656
+ searchToOption(searchData, res => getDataCallback(res))
657
+ }
658
+ else if (attr.keyName.toString().includes('async ') || attr.keyName.toString().includes('function ')) {
659
+ updateOptions()
660
+ }
661
+ else {
662
+ initRadioValue()
663
+ }
664
+ }
665
+ }
666
+
667
+ function getDataCallback(res) {
668
+ option.value = res
669
+ if (attr.type === 'radio')
670
+ initRadioValue()
671
+ }
672
+
673
+ async function updateResOptions(type) {
674
+ if (attr?.keyName?.toString()?.startsWith('search@根据表单项[') && attr?.keyName?.toString()?.endsWith(`]联动${type}`)) {
675
+ const searchData = { source: `获取${type}`, userid: currUser.value }
676
+ const startIndex = attr.keyName.indexOf('[') + 1
677
+ const endIndex = attr.keyName.indexOf(']', startIndex)
678
+ const formModel = attr.keyName.substring(startIndex, endIndex).replace('.', '_')
679
+ const formModelData = form[formModel]
680
+ if (formModel?.length && formModelData?.length) {
681
+ await searchToListOption(searchData, (res) => {
682
+ getDataCallback(res.filter((h) => {
683
+ return formModelData['0'] === h.f_organization_id || formModelData['0'] === h.f_department_id || formModelData['0'] === h.parentid
684
+ // if (formModel.indexOf('org') > -1) {
685
+ // return formModelData?.includes(h.orgid || h.f_organization_id || h.parentid)
686
+ // } else {
687
+ // return formModelData?.includes(h?.parentid)
688
+ // }
689
+ }))
690
+ })
691
+ }
692
+ }
693
+ }
694
+
695
+ function initRadioValue() {
696
+ if ((mode === '新增' || mode === '修改') && attr.type === 'radio' && !props.modelValue) {
697
+ if (attr.keys && attr.keys.length > 0)
698
+ modelData.value = attr.keys[0].value
699
+ else if (option.value && option.value.length > 0)
700
+ modelData.value = option.value[0].value
701
+ }
702
+ }
703
+
704
+ function getData(value, callback) {
705
+ if (value !== '') {
706
+ const logicName = attr.keyName
707
+ const logic = logicName.substring(6)
708
+ // 调用logic前设置参数
709
+ if (getDataParams && getDataParams[attr.model])
710
+ Object.assign(value, getDataParams[attr.model])
711
+ Object.assign(value, userInfo.value)
712
+ runLogic(logic, value, serviceName).then((res) => {
713
+ callback(res)
714
+ })
715
+ }
716
+ }
717
+
718
+ // 已废弃 不进行维护
719
+ function onPickerConfirm({ selectedOptions }) {
720
+ showPicker.value = false
721
+ modelData.value = selectedOptions[0].text
722
+ }
723
+
724
+ // 日期时间选择数据
725
+ const dateTimePickerValue = ref<any>({})
726
+ // 展示日期时间选择器:根据 formValueFormat 初始化 VanDatePicker/VanTimePicker 的当前值
727
+ // 规则:
728
+ // - 若已有字符串值,按 formValueFormat 解析并拆分为日期/时间数组
729
+ // - 若无值,按当前时间生成匹配格式的默认数组
730
+ function showDataTimePicker() {
731
+ if (props.modelValue && typeof props.modelValue === 'string') {
732
+ const base = dayjs(props.modelValue as string, formValueFormat.value)
733
+ const dateArr: string[] = []
734
+ dateArr.push(base.format('YYYY'))
735
+ if (dateColumnsType.value.includes('month'))
736
+ dateArr.push(base.format('MM'))
737
+ if (dateColumnsType.value.includes('day'))
738
+ dateArr.push(base.format('DD'))
739
+ const timeArr: string[] = []
740
+ if (hasTime.value) {
741
+ if (timeColumnsType.value.includes('hour'))
742
+ timeArr.push(base.format('HH'))
743
+ if (timeColumnsType.value.includes('minute'))
744
+ timeArr.push(base.format('mm'))
745
+ if (timeColumnsType.value.includes('second'))
746
+ timeArr.push(base.format('ss'))
747
+ }
748
+ dateTimePickerValue.value = {
749
+ date: dateArr,
750
+ time: timeArr.length ? timeArr : ['00', '00', '00'].slice(0, timeColumnsType.value.length),
751
+ }
752
+ }
753
+ else {
754
+ const now = dayjs()
755
+ const dateArr: string[] = []
756
+ dateArr.push(now.format('YYYY'))
757
+ if (dateColumnsType.value.includes('month'))
758
+ dateArr.push(now.format('MM'))
759
+ if (dateColumnsType.value.includes('day'))
760
+ dateArr.push(now.format('DD'))
761
+ const timeArr: string[] = []
762
+ if (hasTime.value) {
763
+ if (timeColumnsType.value.includes('hour'))
764
+ timeArr.push(now.format('HH'))
765
+ if (timeColumnsType.value.includes('minute'))
766
+ timeArr.push(now.format('mm'))
767
+ if (timeColumnsType.value.includes('second'))
768
+ timeArr.push(now.format('ss'))
769
+ }
770
+ dateTimePickerValue.value = {
771
+ date: dateArr,
772
+ time: timeArr.length ? timeArr : ['00', '00', '00'].slice(0, timeColumnsType.value.length),
773
+ }
774
+ }
775
+ showDatePicker.value = true
776
+ }
777
+
778
+ function onDatePickerConfirm({ selectedValues }) {
779
+ showDatePicker.value = false
780
+ modelData.value = selectedValues.join('-')
781
+ }
782
+
783
+ // 已废弃 不进行维护
784
+ function onTimePickerConfirm({ selectedValues }) {
785
+ showTimePicker.value = false
786
+ timePickerValue.value = selectedValues.join(':')
787
+ modelData.value = timePickerValue.value
788
+ }
789
+
790
+ // 没人用到本次先不动。后续需要看这个组件需要怎么使用
791
+ function onAreaConfirm({ selectedOptions }) {
792
+ area.value = `${selectedOptions[0].text}-${selectedOptions[1].text}-${selectedOptions[2].text}`
793
+ showArea.value = false
794
+ modelData.value = [{
795
+ province: selectedOptions[0].text,
796
+ city: selectedOptions[1].text,
797
+ district: selectedOptions[2].text,
798
+ }]
799
+ }
800
+
801
+ // 日期时间选择确认:将选择的年月日(+时分秒)拼装后,最终使用 formValueFormat 输出到 v-model
802
+ function onDateTimePickerConfirm() {
803
+ showDatePicker.value = false
804
+ const dateParts = dateTimePickerValue.value.date as string[]
805
+ const timeParts = (dateTimePickerValue.value.time as string[]) || []
806
+ const year = dateParts[0]
807
+ const month = dateParts[1] || '01'
808
+ const day = dateParts[2] || '01'
809
+ const hour = hasTime.value && timeParts[0] ? timeParts[0] : '00'
810
+ const minute = hasTime.value && timeParts[1] ? timeParts[1] : '00'
811
+ const second = hasTime.value && timeParts[2] ? timeParts[2] : '00'
812
+ const full = `${year}-${month}-${day} ${hour}:${minute}:${second}`
813
+ modelData.value = dayjs(full, 'YYYY-MM-DD HH:mm:ss').format(formValueFormat.value)
814
+ }
815
+
816
+ function onPickerCancel() {
817
+ showDatePicker.value = false
818
+ }
819
+
820
+ const showAddressPicker = ref(false)
821
+ const addressValue = ref('')
822
+
823
+ // XLocationPicker 默认加载的中心点
824
+ const defaultMapCenter = computed(() => {
825
+ const lonLat = form[`${attr.model}_lon_lat`]
826
+ if (lonLat && typeof lonLat === 'string' && lonLat.includes(',')) {
827
+ const [lon, lat] = lonLat.split(',').map(Number)
828
+ if (!Number.isNaN(lon) && !Number.isNaN(lat)) {
829
+ return [lon, lat] as [number, number]
830
+ }
831
+ }
832
+ return undefined
833
+ })
834
+
835
+ // 处理地址选择器确认
836
+ function handleAddressConfirm(location) {
837
+ // 构造新的数据格式
838
+ const formData = {
839
+ [`${attr.model}_lon_lat`]: `${location.longitude},${location.latitude}`,
840
+ [attr.model]: location.address,
841
+ }
842
+ // 更新表单数据
843
+ emits('setForm', formData)
844
+ showAddressPicker.value = false
845
+ }
846
+
847
+ // 重置方法,供父组件调用
848
+ function reset() {
849
+ modelData.value = getDefaultValue()
850
+ errorMessage.value = ''
851
+ treeValue.value = null
852
+ area.value = null
853
+ pickerValue.value = null
854
+ }
855
+
856
+ defineExpose({
857
+ reset,
858
+ })
859
+
860
+ // 数据处理
861
+ function cleanEmptyChildren(options, fieldNames = { text: 'label', value: 'value', children: 'children' }) {
862
+ if (!Array.isArray(options))
863
+ return options
864
+
865
+ const childrenKey = fieldNames.children || 'children'
866
+
867
+ return options.map((option) => {
868
+ // 深拷贝选项,避免修改原始数据
869
+ const newOption = { ...option }
870
+
871
+ // 如果存在children属性且是空数组
872
+ if (newOption[childrenKey] && Array.isArray(newOption[childrenKey]) && newOption[childrenKey].length === 0) {
873
+ delete newOption[childrenKey]
874
+ }
875
+ // 如果存在children属性且非空,则递归处理
876
+ else if (newOption[childrenKey] && Array.isArray(newOption[childrenKey])) {
877
+ newOption[childrenKey] = cleanEmptyChildren(newOption[childrenKey], fieldNames)
878
+ }
879
+ return newOption
880
+ })
881
+ }
882
+
883
+ // 级联选择完成事件
884
+ function onTreeSelectFinish({ selectedOptions }) {
885
+ const index = selectedOptions.length - 1
886
+ treeValue.value = selectedOptions[index].label
887
+ if (mode === '查询') {
888
+ modelData.value = [selectedOptions[index].value]
889
+ }
890
+ else {
891
+ modelData.value = selectedOptions[index].value
892
+ }
893
+ showTreeSelect.value = false
894
+ }
895
+
896
+ function emitFunc(func, data) {
897
+ emits('xFormItemEmitFunc', func, data, data?.model ? form[data.model] : form)
898
+ }
899
+
900
+ function findOptionInTree(options, value) {
901
+ // 在当前层级查找
902
+ const foundItem = options.find(item => item[columnsField.value] === value)
903
+ if (foundItem) {
904
+ return foundItem
905
+ }
906
+ // 递归查找子级
907
+ for (const item of options) {
908
+ if (item.children?.length) {
909
+ const foundInChildren = findOptionInTree(item.children, value)
910
+ if (foundInChildren)
911
+ return foundInChildren
912
+ }
913
+ }
914
+
915
+ return null
916
+ }
917
+ </script>
918
+
919
+ <template>
920
+ <div>
921
+ <!-- switch开关 -->
922
+ <VanField
923
+ v-if="attr.type === 'switch' && showItem"
924
+ name="switch"
925
+ :label="labelData"
926
+ :label-align="labelAlign"
927
+ :input-align="attr.inputAlign ? attr.inputAlign : 'right'"
928
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
929
+ :required="attr.rule.required === 'true'"
930
+ >
931
+ <template #input>
932
+ <VanSwitch v-model="modelData" />
933
+ </template>
934
+ </VanField>
935
+
936
+ <!-- 复选框 -->
937
+ <!-- <VanField
938
+ v-if="attr.type === 'checkbox'"
939
+ name="checkbox"
940
+ :label="labelData"
941
+ >
942
+ <template #input>
943
+ <VanCheckbox v-model="modelData" shape="square" />
944
+ </template>
945
+ </VanField> -->
946
+
947
+ <!-- 多选框-checkbox-复选框组 -->
948
+ <template v-if="attr.type === 'checkbox' && showItem">
949
+ <!-- 勾选 -->
950
+ <VanField
951
+ v-if="attr.showMode === 'checkbox' && mode !== '查询'"
952
+ name="checkboxGroup"
953
+ :label="labelData"
954
+ :label-align="labelAlign"
955
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
956
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
957
+ :required="attr.rule.required === 'true'"
958
+ >
959
+ <template #input>
960
+ <van-checkbox-group v-model="modelData as any[]" direction="horizontal" shape="square" :disabled="readonly">
961
+ <VanCheckbox v-for="(item, index) in option" :key="index" style="padding: 2px" :name="item[columnsField.value]" :shape="rules?.[attr.model].shape" :value="item[columnsField.value]">
962
+ {{ item[columnsField.text] }}
963
+ </VanCheckbox>
964
+ </van-checkbox-group>
965
+ </template>
966
+ </VanField>
967
+ <VanField
968
+ v-if="attr.showMode === 'checkbox' && mode === '查询'"
969
+ name="checkboxGroup"
970
+ :label="labelData"
971
+ :label-align="labelAlign"
972
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
973
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
974
+ >
975
+ <template #input>
976
+ <XGridDropOption
977
+ v-model="(modelData as string[])"
978
+ :column-num="labelData ? 3 : 4"
979
+ :multiple="true"
980
+ :columns="option"
981
+ />
982
+ </template>
983
+ </VanField>
984
+ <!-- 下拉 -->
985
+ <XMultiSelect
986
+ v-if="attr.showMode === 'select' && mode === '查询'"
987
+ v-model="modelData"
988
+ :label="labelData"
989
+ :readonly="readonly"
990
+ :placeholder="placeholder"
991
+ :columns="option"
992
+ :select-value="Array.isArray(modelData) ? modelData : []"
993
+ :option="attr.option ? attr.option : columnsField"
994
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
995
+ :required="attr.rule.required === 'true'"
996
+ />
997
+ </template>
998
+
999
+ <!-- 单选框 -->
1000
+ <VanField
1001
+ v-if="attr.type === 'radio' && mode !== '查询' && showItem"
1002
+ name="radio"
1003
+ center
1004
+ :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1005
+ :label="labelData"
1006
+ :label-align="labelAlign"
1007
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1008
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1009
+ :required="attr.rule.required === 'true'"
1010
+ >
1011
+ <template #input>
1012
+ <VanRadioGroup v-model="modelData" direction="horizontal" :disabled="readonly">
1013
+ <VanRadio v-for="(item, index) in option" :key="index" style="padding: 2px" :name="item[columnsField.value]" :value="item[columnsField.value]">
1014
+ {{ item[columnsField.text] }}
1015
+ </VanRadio>
1016
+ </VanRadioGroup>
1017
+ </template>
1018
+ </VanField>
1019
+
1020
+ <!-- 单选框-查询 -->
1021
+ <VanField
1022
+ v-if="attr.type === 'radio' && mode === '查询' && showItem"
1023
+ name="radio"
1024
+ :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1025
+ :label="labelData"
1026
+ :label-align="labelAlign"
1027
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1028
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1029
+ >
1030
+ <template #input>
1031
+ <XGridDropOption
1032
+ v-model="(modelData as string)"
1033
+ :column-num="labelData ? 3 : 4"
1034
+ :columns="option"
1035
+ />
1036
+ </template>
1037
+ </VanField>
1038
+
1039
+ <!-- 步进器 -->
1040
+ <VanField
1041
+ v-if="attr.type === 'stepper' && showItem"
1042
+ name="stepper"
1043
+ :label="labelData"
1044
+ :label-align="labelAlign"
1045
+ :input-align="attr.inputAlign ? attr.inputAlign : 'center'"
1046
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1047
+ :required="attr.rule.required === 'true'"
1048
+ >
1049
+ <template #input>
1050
+ <VanStepper v-model="modelData as any" :disabled="readonly" />
1051
+ </template>
1052
+ </VanField>
1053
+
1054
+ <!-- 评分 -->
1055
+ <VanField
1056
+ v-if="attr.type === 'rate' && showItem"
1057
+ name="rate"
1058
+ :label="labelData"
1059
+ :label-align="labelAlign"
1060
+ :input-align="attr.inputAlign ? attr.inputAlign : 'center'"
1061
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1062
+ :required="attr.rule.required === 'true'"
1063
+ >
1064
+ <template #input>
1065
+ <VanRate v-model="modelData as number" :size="25" :readonly="readonly" void-color="#eee" void-icon="star" color="#ffd21e" />
1066
+ </template>
1067
+ </VanField>
1068
+
1069
+ <!-- 滑块 -->
1070
+ <VanField
1071
+ v-if="attr.type === 'slider' && showItem"
1072
+ name="slider"
1073
+ :label="labelData"
1074
+ :label-align="labelAlign"
1075
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1076
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1077
+ :required="attr.rule.required === 'true'"
1078
+ >
1079
+ <template #input>
1080
+ <VanSlider v-model="modelData as number" :readonly="readonly" />
1081
+ </template>
1082
+ </VanField>
1083
+
1084
+ <!-- 文件上传 -->
1085
+ <!-- 图片上传, 手机端拍照 -->
1086
+ <VanField
1087
+ v-if="(attr.type === 'image' || attr.type === 'file') && showItem"
1088
+ name="image"
1089
+ :label="labelData"
1090
+ :label-align="labelAlign"
1091
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1092
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1093
+ :required="attr.rule.required === 'true'"
1094
+ >
1095
+ <template #input>
1096
+ <ImageUploader
1097
+ upload-mode="server"
1098
+ :image-list="(modelData as any[])"
1099
+ authority="admin"
1100
+ :attr="attr"
1101
+ :mode="props.mode"
1102
+ :readonly="readonly"
1103
+ :is-async-upload="isAsyncUpload"
1104
+ @update-file-list="updateFile"
1105
+ />
1106
+ </template>
1107
+ </VanField>
1108
+
1109
+ <!-- 选择器 琉璃中不存在,不进行维护后续将删除 -->
1110
+ <VanField
1111
+ v-if="attr.type === 'picker' && showItem"
1112
+ v-model="pickerValue"
1113
+ name="picker"
1114
+ :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1115
+ :label="labelData"
1116
+ :label-align="labelAlign"
1117
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1118
+ readonly
1119
+ is-link
1120
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1121
+ @click="readonly ? null : showPicker = true"
1122
+ />
1123
+ <VanPopup v-model:show="showPicker" round position="bottom" teleport="body" overlay-class="date-picker-overlay">
1124
+ <VanPicker
1125
+ v-model="(modelData as Numeric[])"
1126
+ :title="attr.name"
1127
+ :columns="attr.selectKey"
1128
+ :readonly="readonly"
1129
+ :columns-field-names="attr.customFieldName ? attr.customFieldName : { text: 'text', value: 'value', children: 'children' }"
1130
+ :confirm-button-text="attr.confirmButtonText || attr.confirmButtonText === '' ? attr.confirmButtonText : '确认'"
1131
+ :cancel-button-text="attr.cancelButtonText || attr.cancelButtonText === '' ? attr.cancelButtonText : '取消'"
1132
+ @cancel="showPicker = false"
1133
+ @confirm="onPickerConfirm"
1134
+ />
1135
+ </VanPopup>
1136
+
1137
+ <!-- 日历选择-查询 -->
1138
+ <VanField
1139
+ v-if="attr.type === 'rangePicker' && mode === '查询' && showItem"
1140
+ v-model="(pickerValue as string | number)"
1141
+ is-link
1142
+ readonly
1143
+ name="rangePicker"
1144
+ :label="labelData"
1145
+ :label-align="labelAlign"
1146
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1147
+ :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1148
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1149
+ @click="calendarShow = true"
1150
+ />
1151
+ <VanCalendar
1152
+ v-model:show="calendarShow"
1153
+ switch-mode="year-month"
1154
+ type="range"
1155
+ teleport="body"
1156
+ overlay-class="date-picker-overlay"
1157
+ :show-confirm="attr.showConfirm"
1158
+ @confirm="onCalendarConfirm"
1159
+ />
1160
+
1161
+ <!-- 日期选择-非查询 -->
1162
+ <VanField
1163
+ v-if="(attr.type === 'datePicker' || attr.type === 'rangePicker') && mode !== '查询' && showItem"
1164
+ v-model="(modelData as string | number)"
1165
+ name="datePicker"
1166
+ :label="labelData"
1167
+ :label-align="labelAlign"
1168
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1169
+ readonly
1170
+ :is-link="true"
1171
+ :placeholder="placeholder"
1172
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1173
+ :required="attr.rule.required === 'true'"
1174
+ @click="readonly ? null : showDataTimePicker()"
1175
+ />
1176
+ <VanPopup v-model:show="showDatePicker" position="bottom" teleport="body" overlay-class="date-picker-overlay">
1177
+ <VanPickerGroup
1178
+ :title="attr.name"
1179
+ :tabs="hasTime ? ['选择日期', '选择时间'] : ['选择日期']"
1180
+ next-step-text="下一步"
1181
+ :confirm-button-text="attr.confirmButtonText ? attr.confirmButtonText : '确认'"
1182
+ :cancel-button-text="attr.cancelButtonText ? attr.cancelButtonText : '取消'"
1183
+ @confirm="onDateTimePickerConfirm"
1184
+ @cancel="onPickerCancel"
1185
+ >
1186
+ <VanDatePicker
1187
+ v-model="dateTimePickerValue.date"
1188
+ :columns-type="attr.dateColumnsType || dateColumnsType"
1189
+ />
1190
+ <VanTimePicker
1191
+ v-if="hasTime"
1192
+ v-model="dateTimePickerValue.time"
1193
+ :columns-type="attr.timeColumnsType || timeColumnsType"
1194
+ :min-time="attr.minTime ? attr.minTime : '00:00:00'"
1195
+ :max-time="attr.maxTime ? attr.maxTime : '23:59:59'"
1196
+ />
1197
+ </VanPickerGroup>
1198
+ </VanPopup>
1199
+
1200
+ <!-- 日期选择-查询 -->
1201
+ <VanField
1202
+ v-if="attr.type === 'datePicker' && mode === '查询' && showItem"
1203
+ v-model="(modelData as string | number)"
1204
+ name="datePicker"
1205
+ :label="labelData"
1206
+ :label-align="labelAlign"
1207
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1208
+ readonly
1209
+ :is-link="true"
1210
+ :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1211
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1212
+ @click="readonly ? null : showDataTimePicker()"
1213
+ />
1214
+ <VanPopup v-model:show="showDatePicker" position="bottom" teleport="body" overlay-class="date-picker-overlay">
1215
+ <VanPickerGroup
1216
+ :title="attr.name"
1217
+ :tabs="hasTime ? ['选择日期', '选择时间'] : ['选择日期']"
1218
+ next-step-text="下一步"
1219
+ :confirm-button-text="attr.confirmButtonText ? attr.confirmButtonText : '确认'"
1220
+ :cancel-button-text="attr.cancelButtonText ? attr.cancelButtonText : '取消'"
1221
+ @confirm="onDateTimePickerConfirm"
1222
+ @cancel="onPickerCancel"
1223
+ >
1224
+ <VanDatePicker
1225
+ v-model="dateTimePickerValue.date"
1226
+ :columns-type="attr.dateColumnsType || dateColumnsType"
1227
+ />
1228
+ <VanTimePicker
1229
+ v-if="hasTime"
1230
+ v-model="dateTimePickerValue.time"
1231
+ :columns-type="attr.timeColumnsType || timeColumnsType"
1232
+ :min-time="attr.minTime ? attr.minTime : '00:00:00'"
1233
+ :max-time="attr.maxTime ? attr.maxTime : '23:59:59'"
1234
+ />
1235
+ </VanPickerGroup>
1236
+ </VanPopup>
1237
+
1238
+ <!-- 时间选择 --该配置未在pc找到不进行维护 后续将删除 -->
1239
+ <VanField
1240
+ v-if="attr.type === 'timePicker' && showItem"
1241
+ v-model="timePickerValue"
1242
+ name="timePicker"
1243
+ is-link
1244
+ readonly
1245
+ :placeholder="attr.placeholder"
1246
+ :label="labelData"
1247
+ :label-align="labelAlign"
1248
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1249
+ :rules="[{ required: attr.rule.required === 'true', message: '请选择' }]"
1250
+ @click="readonly ? null : (showTimePicker = true)"
1251
+ />
1252
+ <VanPopup v-model:show="showTimePicker" position="bottom" teleport="body" overlay-class="date-picker-overlay">
1253
+ <VanTimePicker
1254
+ v-model="modelData as string[]"
1255
+ :title="attr.name"
1256
+ :columns-type="attr.columnsType ? attr.columnsType : ['hour', 'minute', 'second']"
1257
+ :min-time="attr.minTime ? attr.minTime : '00:00:00'"
1258
+ :max-time="attr.maxTime ? attr.maxTime : '23:59:59'"
1259
+ :readonly="readonly"
1260
+ @cancel="showTimePicker = false"
1261
+ @confirm="onTimePickerConfirm"
1262
+ />
1263
+ </VanPopup>
1264
+
1265
+ <!-- 省市区选择 -->
1266
+ <VanField
1267
+ v-if="(attr.type === 'area' || attr.type === 'citySelect') && showItem"
1268
+ v-model="area"
1269
+ name="area"
1270
+ :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1271
+ is-link
1272
+ readonly
1273
+ :label="labelData"
1274
+ :label-align="labelAlign"
1275
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1276
+ :rules="[{ required: attr.rule.required === 'true', message: '请选择' }]"
1277
+ :required="attr.rule.required === 'true'"
1278
+ @click="readonly ? null : (showArea = true)"
1279
+ />
1280
+ <VanPopup v-model:show="showArea" position="bottom" teleport="body" overlay-class="date-picker-overlay">
1281
+ <VanArea
1282
+ v-model="modelData as string" :title="attr.name" :area-list="areaList"
1283
+ @confirm="onAreaConfirm"
1284
+ @cancel="showArea = false"
1285
+ />
1286
+ </VanPopup>
1287
+
1288
+ <!-- 单选下拉列表 -->
1289
+ <XSelect
1290
+ v-if="attr.type === 'select' && showItem"
1291
+ v-model="modelData"
1292
+ :label="labelData"
1293
+ :readonly="readonly"
1294
+ clearable
1295
+ :placeholder="placeholder"
1296
+ :columns="option"
1297
+ :option="attr.option ? attr.option : columnsField"
1298
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1299
+ :required="attr.rule.required === 'true'"
1300
+ />
1301
+
1302
+ <!-- 文本区域 -->
1303
+ <VanField
1304
+ v-if="attr.type === 'textarea' && showItem"
1305
+ v-model="(modelData as string)"
1306
+ rows="3"
1307
+ autosize
1308
+ :label="labelData"
1309
+ :label-align="labelAlign"
1310
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1311
+ type="textarea"
1312
+ :readonly="readonly"
1313
+ :maxlength="attr.maxlength ? attr.maxlength : 200"
1314
+ :placeholder="attr.placeholder ? attr.placeholder : `请输入${attr.name}`"
1315
+ show-word-limit
1316
+ :rules="[{ required: attr.rule.required === 'true', message: `请填写${attr.name}` }]"
1317
+ :required="attr.rule.required === 'true'"
1318
+ />
1319
+
1320
+ <!-- 文本输入框 -->
1321
+ <VanField
1322
+ v-if="(attr.type === 'input' || attr.type === 'intervalPicker') && showItem"
1323
+ v-model="(modelData as string)"
1324
+ style="align-items: center"
1325
+ :label="labelData"
1326
+ :label-align="labelAlign"
1327
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1328
+ :type="attr.type as FieldType"
1329
+ :readonly="readonly"
1330
+ :disabled="attr.disabled"
1331
+ :placeholder="placeholder"
1332
+ :error-message="errorMessage"
1333
+ :clearable="attr.clearable"
1334
+ :rules="[{ required: attr.rule.required === 'true', message: `请填写${attr.name}` }]"
1335
+ :required="attr.rule.required === 'true'"
1336
+ @blur="() => formTypeCheck(attr, modelData as string)"
1337
+ >
1338
+ <template #input>
1339
+ <input
1340
+ :value="modelData"
1341
+ :readonly="readonly"
1342
+ class="van-field__control"
1343
+ :placeholder="placeholder"
1344
+ style="flex: 1; min-width: 0;"
1345
+ @input="e => modelData = (e.target as HTMLInputElement).value"
1346
+ @blur="() => formTypeCheck(attr, modelData as string)"
1347
+ >
1348
+ <VanButton
1349
+ v-if="!readonly && attr.inputOnAfterName && attr.inputOnAfterFunc && !attr.inputOnAfterName.includes('|')"
1350
+ class="action-btn"
1351
+ round
1352
+ type="primary"
1353
+ size="small"
1354
+ @click="emitFunc(attr.inputOnAfterFunc, attr)"
1355
+ >
1356
+ {{ attr.inputOnAfterName }}
1357
+ </VanButton>
1358
+ </template>
1359
+ </VanField>
1360
+
1361
+ <!-- 地址选择器 -->
1362
+ <VanField
1363
+ v-if="attr.type === 'addressSearch' && showItem"
1364
+ v-model="modelData as string"
1365
+ name="addressSearch"
1366
+ :label="labelData"
1367
+ :label-align="labelAlign"
1368
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1369
+ readonly
1370
+ is-link
1371
+ :placeholder="placeholder"
1372
+ :rules="[{ required: attr.rule.required === 'true', message: '请选择地址' }]"
1373
+ :required="attr.rule.required === 'true'"
1374
+ @click="readonly ? null : (showAddressPicker = true)"
1375
+ />
1376
+ <VanPopup
1377
+ v-model:show="showAddressPicker"
1378
+ position="bottom"
1379
+ :style="{ height: '80vh' }"
1380
+ teleport="body"
1381
+ overlay-class="date-picker-overlay"
1382
+ >
1383
+ <XLocationPicker
1384
+ :default-center="defaultMapCenter"
1385
+ :service-name="serviceName"
1386
+ @confirm="handleAddressConfirm"
1387
+ />
1388
+ </VanPopup>
1389
+
1390
+ <!-- pc的树形选择框————》 手机端采用 Cascader 级联选择 -->
1391
+ <VanField
1392
+ v-if="attr.type === 'treeSelect' && showItem"
1393
+ v-model="treeValue"
1394
+ name="treeSelect"
1395
+ :label="labelData"
1396
+ :label-align="labelAlign"
1397
+ :input-align="attr.inputAlign ? attr.inputAlign : 'left'"
1398
+ readonly
1399
+ is-link
1400
+ :placeholder="attr.placeholder ? attr.placeholder : `请选择${attr.name}`"
1401
+ :rules="[{ required: attr.rule.required === 'true', message: `请选择${attr.name}` }]"
1402
+ :required="attr.rule.required === 'true'"
1403
+ @click="readonly ? null : (showTreeSelect = true)"
1404
+ />
1405
+ <VanPopup
1406
+ v-model:show="showTreeSelect"
1407
+ position="bottom"
1408
+ teleport="body"
1409
+ overlay-class="date-picker-overlay"
1410
+ >
1411
+ <VanCascader
1412
+ :options="cleanEmptyChildren(option, attr.customFieldName ? attr.customFieldName : { text: 'label', value: 'value', children: 'children' })"
1413
+ :field-names="attr.customFieldName ? attr.customFieldName : { text: 'label', value: 'value', children: 'children' }"
1414
+ :title="attr.name"
1415
+ :closeable="true"
1416
+ @close="showTreeSelect = false"
1417
+ @finish="onTreeSelectFinish"
1418
+ />
1419
+ </VanPopup>
1420
+ </div>
1421
+ </template>
1422
+
1423
+ <style scoped>
1424
+ .date-picker-overlay {
1425
+ background-color: rgba(0, 0, 0, 0.2); /* 设置为半透明的黑色 */
1426
+ }
1427
+ .action-btn {
1428
+ border-radius: 10px;
1429
+ margin-left: 8px;
1430
+ min-width: 4rem;
1431
+ max-width: 6rem;
1432
+ }
1433
+ </style>