focusin-mini-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,752 @@
1
+ <template>
2
+ <view class="z-form">
3
+ <wd-form ref="formRef" errorType="toast" :model="model" :rules="mode === 'detail' ? {} : rules"
4
+ :custom-class="'form-model'">
5
+ <template v-for="(itemConfig, i) in visibleFormConfig" :key="i">
6
+ <template v-if="itemConfig.config">
7
+ <view class="common-collapse-body">
8
+ <view v-if="itemConfig.config && itemConfig.config.length">
9
+ <wd-cell-group border>
10
+ <template v-for="item in itemConfig.config" :key="item.prop">
11
+ <!-- 选择器 -->
12
+ <template v-if="item.temp === 'wd-select-picker' && isItemVisible(item)">
13
+ <wd-select-picker filterable v-if="mode !== 'detail'" :custom-class="'form-model'"
14
+ :label="item.label" :show-confirm="item.showConfirm" :label-width="item.labelWidth || '100px'"
15
+ :prop="item.prop" :disabled="item.readonly || mode === 'detail'"
16
+ :label-key="item.labelKey || 'label'" :value-key="item.valueKey || 'value'"
17
+ :type="item.type || 'radio'" v-model="model[item.prop]" :columns="item.columns"
18
+ :placeholder="'请选择' + item.label" :rules="mode === 'detail' ? [] : item.rules"
19
+ @change="item.change" @confirm="item.confirm" />
20
+ <wd-cell v-else :title="item.label" :title-width="item.labelWidth || '100px'">
21
+ <view class="text-right text-[#5c6278] text_32">
22
+ {{ getSelectDisplay(item) }}
23
+ </view>
24
+ </wd-cell>
25
+ </template>
26
+
27
+ <template v-if="item.temp === 'wd-col-picker' && isItemVisible(item)">
28
+ <CommonColPicker v-if="mode !== 'detail'" :custom-class="'form-model'" :label="item.label"
29
+ :label-width="item.labelWidth || '100px'" :prop="item.prop"
30
+ :disabled="item.readonly || mode === 'detail'" :label-key="item.labelKey || 'label'"
31
+ :value-key="item.valueKey || 'value'" v-model="model[item.prop]"
32
+ :column-change="item.columnChange" :before-confirm="item.beforeConfirm" @confirm="item.confirm"
33
+ :columns="item.columns" :display-format="item.displayFormat" :placeholder="'请选择' + item.label"
34
+ :rules="mode === 'detail' ? [] : item.rules" />
35
+ <wd-cell v-else :title="item.label" :title-width="item.labelWidth || '100px'">
36
+ <view class="text-right text-[#5c6278] text_32" :style="{
37
+ 'font-size': item.valueFontSize ? item.valueFontSize : '32rpx',
38
+ }">
39
+ {{ getColDisplay(item) }}
40
+ </view>
41
+ </wd-cell>
42
+ </template>
43
+ <template v-if="item.temp === 'wd-datetime' && isItemVisible(item)">
44
+ <wd-datetime-picker v-if="mode !== 'detail'" :type="item.type || 'date'"
45
+ :displayFormat="item.displayFormat" :custom-class="'form-model'" :label="item.label"
46
+ :label-width="item.labelWidth || '100px'" :prop="item.prop"
47
+ :readonly="item.readonly || mode === 'detail'" :label-key="item.labelKey || 'label'"
48
+ :value-key="item.valueKey || 'value'" :minDate="item.minDate || new Date(`${new Date().getFullYear() - 1}-01-01`).getTime()
49
+ " :maxDate="item.maxDate || new Date(`${new Date().getFullYear() + 1}-12-31`).getTime()
50
+ " v-model="model[item.prop]" :column-change="item.columnChange"
51
+ :before-confirm="item.beforeConfirm" @confirm="item.confirm" :placeholder="'请选择' + item.label"
52
+ :rules="mode === 'detail' ? [] : item.rules" />
53
+ <wd-cell v-else :title="item.label" :title-width="item.labelWidth || '100px'">
54
+ <view class="text-right text-[#5c6278] text_32" :style="{
55
+ 'font-size': item.valueFontSize ? item.valueFontSize : '32rpx',
56
+ }">
57
+ {{ getTimeDisplay(item) || '-' }}
58
+ </view>
59
+ </wd-cell>
60
+ </template>
61
+
62
+ <template v-if="item.temp === 'wd-calendar' && isItemVisible(item)">
63
+ <wd-calendar v-if="mode !== 'detail'" :type="item.type || 'date'"
64
+ :displayFormat="item.displayFormat" :custom-class="'form-model'" :label="item.label"
65
+ :label-width="item.labelWidth || '100px'" :prop="item.prop"
66
+ :readonly="item.readonly || mode === 'detail'" :label-key="item.labelKey || 'label'"
67
+ :value-key="item.valueKey || 'value'" :minDate="item.minDate || new Date(`${new Date().getFullYear() - 1}-01-01`).getTime()
68
+ " :maxDate="item.maxDate || new Date(`${new Date().getFullYear() + 1}-12-31`).getTime()
69
+ " v-model="model[item.prop]" :before-confirm="item.beforeConfirm" @confirm="item.confirm"
70
+ :placeholder="'请选择' + item.label" :rules="mode === 'detail' ? [] : item.rules" />
71
+ <wd-cell v-else :title="item.label" :title-width="item.labelWidth || '100px'">
72
+ <view class="text-right text-[#5c6278] text_32" :style="{
73
+ 'font-size': item.valueFontSize ? item.valueFontSize : '32rpx',
74
+ }">
75
+ {{ getTimeDisplay(item) || '-' }}
76
+ </view>
77
+ </wd-cell>
78
+ </template>
79
+
80
+ <!-- 输入框 -->
81
+ <template v-if="item.temp === 'wd-input' && isItemVisible(item)">
82
+ <wd-input v-if="mode !== 'detail' && !item.readonly" :custom-class="'form-model'"
83
+ :label="item.label" :type="item.type" :maxlength="item.maxlength"
84
+ :label-width="item.labelWidth || '100px'" :prop="item.prop"
85
+ :rules="mode === 'detail' ? [] : item.rules" clearable
86
+ :readonly="item.readonly || mode === 'detail'" v-model="model[item.prop]" @blur="item.blur"
87
+ @input="item.input" :placeholder="'请输入' + item.label" />
88
+ <wd-cell v-else :title="item.label" :title-width="item.labelWidth || '100px'">
89
+ <view class="text-right text-[#5c6278] text_32" :style="{
90
+ 'font-size': item.valueFontSize ? item.valueFontSize : '32rpx',
91
+ }">
92
+ {{
93
+ model[item.prop] !== null && model[item.prop] !== undefined
94
+ ? model[item.prop]
95
+ : '-'
96
+ }}
97
+ </view>
98
+ </wd-cell>
99
+ </template>
100
+ <template v-if="item.temp === 'wd-span' && isItemVisible(item)">
101
+ <wd-cell :title="item.label" :title-width="item.labelWidth || '100px'">
102
+ <view class="text-right text-[#5c6278] text_32" :style="{
103
+ 'font-size': item.valueFontSize ? item.valueFontSize : '32rpx',
104
+ }">
105
+ {{ model[item.prop] || '-' }}
106
+ </view>
107
+ </wd-cell>
108
+ </template>
109
+
110
+ <!-- 单选框 -->
111
+ <template v-if="item.temp === 'wd-radio' && isItemVisible(item)">
112
+ <wd-cell :prop="item.prop" :title="item.label" :title-width="item.labelWidth || '100px'"
113
+ :rules="mode === 'detail' ? [] : item.rules || []">
114
+ <view class="text-right">
115
+ <CommonRadio :label-key="item.labelKey || 'label'" :value-key="item.valueKey || 'value'"
116
+ v-if="mode !== 'detail' && !item.readonly" :value="model[item.prop]" :options="item.columns"
117
+ @onchange="item.onchange" @change="(e) => radioChange(e, item.prop, item.valueKey)" />
118
+ <span v-else class="text-[#5c6278] text_32" :style="{
119
+ 'font-size': item.valueFontSize ? item.valueFontSize : '32rpx',
120
+ }">
121
+ {{ getRadioDisplay(item) || '-' }}
122
+ </span>
123
+ </view>
124
+ </wd-cell>
125
+ </template>
126
+
127
+ <!-- 文本域 -->
128
+ <template v-if="item.temp === 'wd-textarea' && isItemVisible(item)">
129
+ <wd-cell vertical :prop="item.prop" :title="item.label" :title-width="item.labelWidth || '100px'"
130
+ :rules="mode === 'detail' ? [] : item.rules || []">
131
+ <wd-textarea v-if="mode !== 'detail' && !item.readonly" :custom-class="'form-model-left'"
132
+ :maxlength="item.maxlength" clearable show-word-limit
133
+ :readonly="item.readonly || mode === 'detail'" v-model="model[item.prop]"
134
+ :placeholder="'请输入' + item.label" />
135
+ <view v-else class="text-[#5c6278] text_32" :style="{
136
+ 'font-size': item.valueFontSize ? item.valueFontSize : '32rpx',
137
+ }">
138
+ {{
139
+ model[item.prop] !== null && model[item.prop] !== undefined
140
+ ? model[item.prop]
141
+ : '-'
142
+ }}
143
+ </view>
144
+ </wd-cell>
145
+ </template>
146
+
147
+ <!-- 上传组件 -->
148
+ <template v-if="item.temp === 'wd-upload' && isItemVisible(item)">
149
+ <wd-cell vertical :prop="item.prop" :title="item.label" :title-width="item.labelWidth || '100px'"
150
+ :rules="mode === 'detail' ? [] : item.rules || []">
151
+ <view v-if="!model[item.prop]?.length && (mode == 'detail' || item.readonly)"
152
+ class="text-[#5c6278]">
153
+ -
154
+ </view>
155
+ <wd-upload v-else :disabled="mode === 'detail' || item.readonly"
156
+ :limit="mode === 'detail' ? model[item.prop]?.length : item.limit || 1"
157
+ v-model:file-list="model[item.prop]" :upload-method="customUpload" :multiple="true" />
158
+ </wd-cell>
159
+ </template>
160
+
161
+ <!-- 选择地点 -->
162
+ <template v-if="item.temp === 'wd-location-picker' && isItemVisible(item)">
163
+ <wd-cell :title="item.label" :title-width="item.labelWidth || '100px'" :prop="item.prop"
164
+ :rules="mode === 'detail' ? [] : item.rules">
165
+ <view class="flex justify-end text-[#5c6278] text_32" @click="handleSelectLocation(item)">
166
+ <span class="number">
167
+ {{ model[item.prop] || item.placeholder || '请选择地点' }}
168
+ </span>
169
+ <wd-icon v-if="item.rightIcon" name="arrow-right" size="18px"
170
+ :color="!model[item.prop] ? '#9198aa' : '#5c6278'" />
171
+ </view>
172
+ </wd-cell>
173
+ </template>
174
+
175
+ <!-- 插槽 -->
176
+ <template v-if="item.temp === 'slot' && isItemVisible(item)">
177
+ <wd-cell :vertical="item.vertical" :prop="item.prop" :title="item.label"
178
+ :title-width="item.labelWidth || '100px'" :rules="mode === 'detail' ? [] : item.rules || []">
179
+ <!-- :class="isEmpty(model[item.prop]) ? 'text-[#9198aa]' : 'text-[#5c6278]'" -->
180
+ <view class="flex justify-end text_32"
181
+ :style="{ color: isEmpty(model[item.prop]) ? '#9198aa' : '#5c6278' }">
182
+ <!-- <div class="text-[#9198aa]" v-if="!model[item.prop]">请选择</div> -->
183
+ <slot :name="item.prop"></slot>
184
+ <div class="w-[20rpx]" v-if="item.rightIcon"></div>
185
+ <wd-icon v-if="item.rightIcon" name="arrow-right" size="18px"
186
+ :color="isEmpty(model[item.prop]) ? '#9198aa' : '#5c6278'"></wd-icon>
187
+ </view>
188
+ </wd-cell>
189
+ </template>
190
+ </template>
191
+ </wd-cell-group>
192
+ </view>
193
+ </view>
194
+ </template>
195
+ </template>
196
+ </wd-form>
197
+ </view>
198
+ </template>
199
+ <script lang="ts" setup>
200
+ import { ref, PropType, unref } from 'vue'
201
+
202
+ import { chooseLocation, formatTimestamp } from '@/utils'
203
+ import { isString } from 'xe-utils'
204
+ import { reverseGeocoding, geoconv } from '@/libs/baiduMap'
205
+ // 工具函数:将文件路径转换为Blob对象(H5需要)
206
+ const dataURLtoBlob = (dataurl: string): Blob => {
207
+ const arr = dataurl.split(',')
208
+ const mime = arr[0].match(/:(.*?);/)![1]
209
+ const bstr = atob(arr[1])
210
+ let n = bstr.length
211
+ const u8arr = new Uint8Array(n)
212
+ while (n--) {
213
+ u8arr[n] = bstr.charCodeAt(n)
214
+ }
215
+ return new Blob([u8arr], { type: mime })
216
+ }
217
+
218
+
219
+
220
+ const formatTimestamp = (timestamp, format) => {
221
+ if (timestamp === '-' || !timestamp) {
222
+ return '-'
223
+ }
224
+ const date = new Date(Number(timestamp))
225
+ // 定义格式化规则
226
+ const formatMap = {
227
+ YYYY: date.getFullYear(), // 年份
228
+ MM: String(date.getMonth() + 1).padStart(2, '0'), // 月份(补零)
229
+ DD: String(date.getDate()).padStart(2, '0'), // 日期(补零)
230
+ HH: String(date.getHours()).padStart(2, '0'), // 小时(补零)
231
+ mm: String(date.getMinutes()).padStart(2, '0'), // 分钟(补零)
232
+ ss: String(date.getSeconds()).padStart(2, '0'), // 秒(补零)
233
+ }
234
+
235
+ // 替换格式字符串中的占位符
236
+ return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) => formatMap[match])
237
+ }
238
+ const customUpload: any = async (file, formData, options) => {
239
+ // 更可靠的H5环境判断
240
+ const isH5 =
241
+ process.env.UNI_PLATFORM === 'h5' || (typeof window !== 'undefined' && 'document' in window)
242
+
243
+ if (isH5) {
244
+ try {
245
+ const fd = new FormData()
246
+ console.log(options)
247
+ // options.fileName += '.png'
248
+ options.name += '.png'
249
+ // 合并额外表单数据
250
+ for (const key in formData) {
251
+ console.log(key)
252
+ console.log(formData[key])
253
+ fd.append(key, formData[key])
254
+ }
255
+
256
+ // 处理文件对象
257
+ let fileObj: File | Blob
258
+ if (file.file instanceof File) {
259
+ fileObj = file.file
260
+ } else {
261
+ console.log(file.url)
262
+ // H5端需要将文件路径转换为Blob
263
+ fileObj = await new Promise<Blob>((resolve) => {
264
+ const xhr = new XMLHttpRequest()
265
+ xhr.open('GET', file.url, true)
266
+ xhr.responseType = 'blob'
267
+ xhr.onload = () => {
268
+ resolve(xhr.response)
269
+ }
270
+ xhr.send()
271
+ })
272
+ }
273
+ console.log(fileObj)
274
+ console.log(options.name || file.name)
275
+ fd.append('file', fileObj, options.name || file.name)
276
+ // fd.append('fileType', '.png')
277
+
278
+ return new Promise((resolve, reject) => {
279
+ const xhr = new XMLHttpRequest()
280
+
281
+ xhr.open(
282
+ 'POST',
283
+ import.meta.env.VITE_APP_BASE_API + '/api/tenant/rdsys/rd_sys_attachment/upload',
284
+ )
285
+
286
+ // 设置请求头
287
+ xhr.setRequestHeader('CenterToken', uni.getStorageSync('token'))
288
+ xhr.setRequestHeader('userCode', uni.getStorageSync('userCode'))
289
+
290
+ // 进度处理
291
+ xhr.upload.onprogress = (e) => {
292
+ if (e.lengthComputable) {
293
+ options.onProgress(
294
+ {
295
+ progress: Math.round((e.loaded / e.total) * 100),
296
+ totalBytesSent: e.loaded,
297
+ totalBytesExpectedToSend: e.total,
298
+ },
299
+ file,
300
+ )
301
+ }
302
+ }
303
+
304
+ xhr.onload = () => {
305
+ if (xhr.status === options.statusCode) {
306
+ try {
307
+ const res = JSON.parse(xhr.responseText)
308
+ console.log(res)
309
+ console.log(formData)
310
+ options.onSuccess(res, file, formData)
311
+ resolve(res)
312
+ } catch (e) {
313
+ const err = { errMsg: '解析响应失败', statusCode: xhr.status }
314
+ options.onError(err, file, formData)
315
+ reject(err)
316
+ }
317
+ } else {
318
+ const err = {
319
+ errMsg: xhr.statusText || '上传失败',
320
+ statusCode: xhr.status,
321
+ }
322
+ options.onError(err, file, formData)
323
+ reject(err)
324
+ }
325
+ }
326
+
327
+ xhr.onerror = () => {
328
+ const err = { errMsg: '网络错误' }
329
+ options.onError(err, file, formData)
330
+ reject(err)
331
+ }
332
+
333
+ xhr.send(fd)
334
+ })
335
+ } catch (error) {
336
+ options.onError({ errMsg: '上传处理失败' }, file, formData)
337
+ throw error
338
+ }
339
+ } else {
340
+ // 小程序端实现
341
+ const uploadTask = uni.uploadFile({
342
+ url: import.meta.env.VITE_APP_BASE_API + '/api/tenant/rdsys/rd_sys_attachment/upload',
343
+ header: {
344
+ CenterToken: uni.getStorageSync('token'),
345
+ 'Content-Type': 'multipart/form-data',
346
+ userCode: uni.getStorageSync('userCode'),
347
+ },
348
+ name: 'file',
349
+ fileName: options.name,
350
+ fileType: options.fileType,
351
+ formData,
352
+ filePath: file.url,
353
+ success(res) {
354
+ if (res.statusCode === options.statusCode) {
355
+ try {
356
+ // const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
357
+ options.onSuccess(res, file, formData)
358
+ } catch (e) {
359
+ options.onError({ ...res, errMsg: '解析响应失败' }, file, formData)
360
+ }
361
+ } else {
362
+ options.onError({ ...res, errMsg: res.errMsg || '上传失败' }, file, formData)
363
+ }
364
+ },
365
+ fail(err) {
366
+ options.onError(err, file, formData)
367
+ },
368
+ })
369
+
370
+ uploadTask.onProgressUpdate((res) => {
371
+ options.onProgress(res, file)
372
+ })
373
+
374
+ return {
375
+ abort: () => uploadTask.abort(),
376
+ }
377
+ }
378
+ }
379
+
380
+ function isEmpty(value) {
381
+ // 判断是否为 null 或 undefined
382
+ if (value === null || value === undefined) {
383
+ return true
384
+ }
385
+
386
+ // 判断是否为空字符串
387
+ if (typeof value === 'string' && value.trim() === '') {
388
+ return true
389
+ }
390
+
391
+ // 判断是否为空数组
392
+ if (Array.isArray(value) && value.length === 0) {
393
+ return true
394
+ }
395
+
396
+ // 判断是否为空对象
397
+ if (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) {
398
+ return true
399
+ }
400
+
401
+ // 其他情况返回 false
402
+ return false
403
+ }
404
+
405
+ const visibleFormConfig = computed(() => {
406
+ return props.formConfig.map((group) => ({
407
+ ...group,
408
+ config: group.config?.filter((item) => isItemVisible(item)),
409
+ }))
410
+ })
411
+ const isItemVisible = (item: any) => {
412
+ if (typeof item.visible === 'function') {
413
+ return item.visible()
414
+ }
415
+ if (typeof item.visible === 'object') {
416
+ return item.visible.value
417
+ }
418
+ if (typeof item.visible === 'boolean') {
419
+ return item.visible
420
+ }
421
+ return item.visible !== false // 默认显示
422
+ }
423
+ const props = defineProps({
424
+ model: { type: Object, required: true }, // 表单数据
425
+ formConfig: { type: Array, required: true }, // 表单配置
426
+ mode: { type: String, default: 'add' }, // 模式:add/edit/detail
427
+ labelWidth: String,
428
+ })
429
+ const getSelectDisplay = (item: any) => {
430
+ const value = props.model[item.prop]
431
+ if (!value && value !== 0) return '-'
432
+ const columns = unref(item.columns)
433
+ const option = Array.isArray(columns)
434
+ ? columns.find((opt: any) => opt[item.valueKey || 'value'] == value)
435
+ : {}
436
+ return option ? option[item.labelKey || 'label'] : '-'
437
+ }
438
+
439
+ const getColDisplay = (item) => {
440
+ const columns = unref(item.columns)
441
+ const flatArray = columns.flat()
442
+ return props.model[item.prop]
443
+ ?.reduce((result, id) => {
444
+ const foundItem = flatArray.find((otem) => otem[item.valueKey || 'value'] === id)
445
+ if (foundItem) {
446
+ result.push(foundItem[item.labelKey || 'label'])
447
+ }
448
+ return result
449
+ }, [])
450
+ .join('-')
451
+ }
452
+ const handleSelectLocation = (item: any) => {
453
+ if (props.mode === 'detail') return
454
+
455
+ chooseLocation().then(async (res) => {
456
+ console.log(res)
457
+ const bd = (await geoconv(res.longitude, res.latitude)) as any
458
+ // const r = (await reverseGeocoding(bd.x, bd.y)) as any
459
+ // 更新表单模型
460
+ Object.assign(props.model, {
461
+ [item.prop]: res.address, // 病害点位地址
462
+ [item.longitudeKey || 'longitude']: bd.x, // 经度
463
+ [item.latitudeKey || 'latitude']: bd.y, // 纬度
464
+ })
465
+ console.log(props.model)
466
+ })
467
+ }
468
+
469
+ const getRadioDisplay = (item: any) => {
470
+ const value = props.model[item.prop]
471
+ const columns = unref(item.columns)
472
+ const option = Array.isArray(columns)
473
+ ? columns.find((opt: any) => opt[item.valueKey || 'value'] == value)
474
+ : {}
475
+ return option ? option[item.labelKey || 'label'] : '-'
476
+ }
477
+ const getTimeDisplay = (item: any) => {
478
+ const value = props.model[item.prop]
479
+ const format = item.format || 'YYYY-MM-DD HH:mm:ss'
480
+ if (value && isString(value) && value.indexOf('-') === 4) {
481
+ return value
482
+ }
483
+ if (Array.isArray(value)) {
484
+ if (value.length === 0) return '-'
485
+
486
+ const startDate = formatTimestamp(value[0], format)
487
+ const endDate = value[1] ? formatTimestamp(value[1], format) : '-'
488
+
489
+ return `${startDate} 至 ${endDate}`
490
+ } else {
491
+ return value ? formatTimestamp(value, format) : '-'
492
+ }
493
+ }
494
+ function validateArrayEmpty(value, rule, callback) {
495
+ if (!Array.isArray(value) || value.length === 0) {
496
+ // eslint-disable-next-line prefer-promise-reject-errors
497
+ // || '请上传图片'
498
+ return Promise.reject(rule.message)
499
+ } else {
500
+ return Promise.resolve()
501
+ }
502
+ }
503
+ const rules = computed(() => {
504
+ if (props.mode === 'detail') return {}
505
+ const rules: Record<string, any> = {}
506
+ props.formConfig.forEach((group: any) => {
507
+ group.config?.forEach((item: any) => {
508
+ if (item.rules) {
509
+ rules[item.prop] = item.rules
510
+ }
511
+ // 自动为 upload 类型字段添加非空数组验证
512
+ if (item.temp === 'wd-upload' || item.temp === 'wd-col-picker') {
513
+ // 合并用户自定义规则和自动生成的规则
514
+ rules[item.prop] = rules[item.prop]
515
+ ? [{ validator: validateArrayEmpty, message: `${rules[item.prop][0].message}` }]
516
+ : []
517
+ }
518
+ })
519
+ })
520
+ return rules
521
+ })
522
+
523
+ const radioChange = (e, e2, e3) => {
524
+ if (props.mode === 'detail') return
525
+ Object.assign(props.model, {
526
+ [e2]: e[e3 || 'value'],
527
+ })
528
+ }
529
+
530
+ // // 处理数字输入
531
+ // const handleNumberInput = ({ value }, item) => {
532
+ // if (item.type !== 'number' && item.type !== 'digit') return
533
+ // // 限制只能输入数字和小数点
534
+ // const filteredValue = value.replace(/[^0-9.]/g, '')
535
+ // // 处理多个小数点的情况
536
+ // const parts = filteredValue.split('.')
537
+ // if (parts.length > 2) {
538
+ // // 如果输入了多个小数点,只保留第一个
539
+ // Object.assign(props.model, {
540
+ // [item.prop]: `${parts[0]}.${parts.slice(1).join('')}`,
541
+ // })
542
+ // console.log(1111111, props.model)
543
+ // return
544
+ // }
545
+
546
+ // // 限制小数位数
547
+ // if (parts[1] && parts[1].length > item.precision) {
548
+ // if (item.precision === 0) {
549
+ // Object.assign(props.model, {
550
+ // [item.prop]: `${parts[0]}`,
551
+ // })
552
+ // } else {
553
+ // Object.assign(props.model, {
554
+ // [item.prop]: `${parts[0]}.${parts[1].slice(0, item.precision)}`,
555
+ // })
556
+ // }
557
+
558
+ // console.log(22222222, props.model)
559
+ // return
560
+ // }
561
+
562
+ // Object.assign(props.model, {
563
+ // [item.prop]: filteredValue,
564
+ // })
565
+ // console.log(3333333, props.model)
566
+ // }
567
+
568
+ // // 处理数字失焦
569
+ // const handleNumberBlur = (value, item) => {
570
+ // if (item.type !== 'number' || !item.precision) return
571
+
572
+ // let num = parseFloat(value)
573
+ // if (isNaN(num)) {
574
+ // Object.assign(props.model, {
575
+ // [item.prop]: '',
576
+ // })
577
+ // return
578
+ // }
579
+
580
+ // // 四舍五入到指定精度
581
+ // const factor = Math.pow(10, item.precision)
582
+ // num = Math.round(num * factor) / factor
583
+
584
+ // Object.assign(props.model, {
585
+ // [item.prop]: num.toString(),
586
+ // })
587
+ // }
588
+
589
+ // // 格式化数字显示
590
+ // const formatNumberDisplay = (value, item) => {
591
+ // if (!value && value !== 0) return '-'
592
+ // if ((item.type !== 'number' && item.type !== 'digit') || !item.precision) return value
593
+ // }
594
+
595
+ const formRef = ref()
596
+ const formSubmit = () => {
597
+ // 优化
598
+ return new Promise((resolve) => {
599
+ // 1. 过滤掉 `temp === 'wd-span'` 的配置项
600
+ const filteredConfig = props.formConfig
601
+ .map((group: any) => ({
602
+ ...group,
603
+ config: group.config?.filter((item: any) => item.temp !== 'wd-span'),
604
+ }))
605
+ .filter((group: any) => group.config?.length > 0) // 移除空配置组
606
+ console.log(filteredConfig[0].config.map((item) => item.prop))
607
+ formRef.value.validate(filteredConfig[0].config.map((item) => item.prop)).then(({ valid }) => {
608
+ console.log(valid)
609
+
610
+ if (valid) {
611
+ const processedModel = { ...props.model }
612
+ props.formConfig.forEach((group: any) => {
613
+ group.config?.forEach((item: any) => {
614
+ if (item.temp === 'wd-upload') {
615
+ const prop = item.prop
616
+ const fileList = processedModel[prop]
617
+
618
+ const isH5 =
619
+ process.env.UNI_PLATFORM === 'h5' ||
620
+ (typeof window !== 'undefined' && 'document' in window)
621
+
622
+ console.log(isH5)
623
+ if (Array.isArray(fileList)) {
624
+ const ids = fileList
625
+ .map((file) => {
626
+ if (isH5) {
627
+ try {
628
+ if (file.response) {
629
+ const resData =
630
+ typeof file.response === 'string'
631
+ ? JSON.parse(file.response).data
632
+ : file.response
633
+ console.log(resData)
634
+ return resData.id // 根据实际结构调整路径
635
+ }
636
+ } catch (e) {
637
+ console.error('解析response失败', e)
638
+ }
639
+ } else {
640
+ console.log(file)
641
+ try {
642
+ if (file.response) {
643
+ const resData = JSON.parse(file.response).data
644
+ console.log(resData)
645
+ return resData.id // 根据实际结构调整路径
646
+ }
647
+ } catch (e) {
648
+ console.error('解析response失败', e)
649
+ }
650
+ }
651
+
652
+ return null
653
+ })
654
+ .filter((id) => id !== null)
655
+ // 根据需求决定拼接方式,默认为逗号分隔
656
+ console.log(ids)
657
+ processedModel[prop] = item.join !== false ? ids.join(',') : ids
658
+ }
659
+ }
660
+
661
+ if (item.temp === 'wd-calendar') {
662
+ processedModel[item.prop] = formatTimestamp(
663
+ processedModel[item.prop],
664
+ item.format || 'YYYY-MM-DD HH:mm:ss',
665
+ )
666
+ console.log(processedModel[item.prop])
667
+ }
668
+ })
669
+ })
670
+ console.log({ valid: true, model: processedModel })
671
+ resolve({ valid: true, model: processedModel })
672
+ } else {
673
+ resolve({ valid: false })
674
+ }
675
+ })
676
+ })
677
+ }
678
+
679
+ defineExpose({
680
+ formSubmit,
681
+ formRef,
682
+ })
683
+ </script>
684
+
685
+ <style lang="scss" scoped>
686
+ :deep(.wd-form) {
687
+ --wot-cell-title-fs: 32rpx;
688
+
689
+ .wd-input__body,
690
+ .wd-picker__body,
691
+ .wd-select-picker__body,
692
+ .wd-calendar__body {
693
+ text-align: right !important;
694
+ }
695
+
696
+ .wd-switch {
697
+ font-size: 20px !important;
698
+ }
699
+
700
+ .common-collapse {
701
+ margin-bottom: 24rpx;
702
+ }
703
+
704
+ .common-collapse-body {
705
+ // padding: 0 32rpx;
706
+ background: #fff;
707
+
708
+ &:not(:last-child) {
709
+ margin-bottom: 24rpx;
710
+ }
711
+
712
+ &>uni-view {
713
+ border-bottom: 2rpx solid #f2f4f7;
714
+ }
715
+ }
716
+
717
+ &:not(:has(.wd-action-sheet)) {
718
+ .wd-radio-group {
719
+ display: flex;
720
+ align-items: center;
721
+ justify-content: flex-end;
722
+ }
723
+ }
724
+ }
725
+
726
+ .footer {
727
+ padding: 12px;
728
+ }
729
+
730
+ /* 添加详情模式样式 */
731
+ :deep(.wd-form) {
732
+ --wot-input-disabled-color: #5c6278 !important;
733
+
734
+ .wd-input--readonly,
735
+ .wd-textarea--readonly,
736
+ .wd-picker--disabled,
737
+ .wd-select-picker--disabled {
738
+
739
+ .wd-input__value,
740
+ .wd-textarea__value,
741
+ .wd-picker__value,
742
+ .wd-select-picker__value {
743
+ color: #5c6278 !important;
744
+ }
745
+ }
746
+
747
+ .wd-upload__disabled {
748
+ opacity: 0.6;
749
+ pointer-events: none;
750
+ }
751
+ }
752
+ </style>