af-mobile-client-vue3 1.4.18 → 1.4.20

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