af-mobile-client-vue3 1.4.62 → 1.4.64

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,510 +1,510 @@
1
- <script setup lang="ts">
2
- // import cameraIcon from '@af-mobile-client-vue3/assets/img/component/camera.png'
3
- import { deleteFile, upload } from '@af-mobile-client-vue3/services/api/common'
4
- import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
5
- import { formatNow } from '@af-mobile-client-vue3/utils/timeUtil'
6
- import {
7
- ActionSheet,
8
- Icon as VanIcon,
9
- Uploader as vanUploader,
10
- } from 'vant'
11
- import { inject, ref } from 'vue'
12
-
13
- interface WatermarkConfig {
14
- fontSize?: number | string
15
- color?: string
16
- alpha?: number
17
- userName?: string
18
- format?: string
19
- customLines?: string[]
20
- }
21
-
22
- const props = defineProps({
23
- imageList: Array<any>,
24
- outerIndex: { default: undefined },
25
- authority: { default: 'user' },
26
- uploadMode: { default: 'server' },
27
- attr: { type: Object as () => { addOrEdit?: string, acceptCount?: number, uploadImage?: boolean, watermark?: WatermarkConfig }, default: () => ({}) },
28
- mode: { default: '新增' }, // 预览
29
- serviceName: { type: String, default: '' },
30
- // 整体只读:只允许预览,禁止拍照/上传/删除
31
- readonly: { type: Boolean, default: false },
32
- isAsyncUpload: { type: Boolean, default: false },
33
- })
34
- const emit = defineEmits(['updateFileList', 'updateAllFileList'])
35
-
36
- const imageList = ref<Array<any>>(props.imageList ?? [])
37
-
38
- const parentData: any = inject('provideParent')
39
- // 浏览器模式:隐藏的文件输入(选择 / 拍照)
40
- const fileInputRef = ref<HTMLInputElement | undefined>()
41
- const cameraInputRef = ref<HTMLInputElement | undefined>()
42
-
43
- function openBrowserFilePicker() {
44
- if (fileInputRef.value) {
45
- fileInputRef.value.value = ''
46
- fileInputRef.value.click()
47
- }
48
- }
49
-
50
- function openBrowserCameraPicker() {
51
- if (cameraInputRef.value) {
52
- cameraInputRef.value.value = ''
53
- cameraInputRef.value.click()
54
- }
55
- }
56
-
57
- function emitUpdatesAfterChange() {
58
- // 新增:对外抛出完整与ID列表
59
- (emit as any)('updateAllFileList', imageList.value.filter(item => item.status === 'done').map(item => item))
60
- const doneIds = Object.values(imageList.value)
61
- .filter(item => item.status === 'done')
62
- .map(item => item.id)
63
- if (props.outerIndex !== undefined) {
64
- (emit as any)('updateFileList', doneIds, props.outerIndex)
65
- }
66
- else {
67
- (emit as any)('updateFileList', doneIds)
68
- }
69
- }
70
-
71
- function createTempUploadingItem(name: string, previewUrl: string) {
72
- const temp = {
73
- uid: Date.now() + Math.random().toString(36).substr(2, 5),
74
- name,
75
- status: 'uploading',
76
- message: '上传中...',
77
- url: previewUrl,
78
- }
79
- if (!imageList.value)
80
- imageList.value = [temp]
81
- else imageList.value.push(temp)
82
- return temp
83
- }
84
-
85
- function uploadFileViaHttp(file: File, previewUrl: string, temp: any) {
86
- const formData = new FormData()
87
- formData.append('avatar', file)
88
- formData.append('resUploadMode', props.uploadMode)
89
- formData.append('pathKey', 'Default')
90
- formData.append('formType', 'image')
91
- formData.append('useType', 'Default')
92
- formData.append('resUploadStock', '1')
93
- formData.append('filename', file.name)
94
- formData.append('filesize', (file.size / 1024 / 1024).toFixed(4))
95
- formData.append('f_operator', 'server')
96
-
97
- upload(formData, props.serviceName || import.meta.env.VITE_APP_SYSTEM_NAME, { 'Content-Type': 'multipart/form-data' }).then((res: any) => {
98
- const index = imageList.value.findIndex(item => item.uid === temp.uid)
99
- if (res?.data?.id) {
100
- if (index !== -1) {
101
- imageList.value[index].uid = res.data.id
102
- imageList.value[index].id = res.data.id
103
- delete imageList.value[index].message
104
- imageList.value[index].status = 'done'
105
- imageList.value[index].url = res.data.f_downloadpath
106
- }
107
- }
108
- else {
109
- if (index !== -1) {
110
- imageList.value[index].status = 'failed'
111
- imageList.value[index].message = '上传失败'
112
- }
113
- }
114
- emitUpdatesAfterChange()
115
- })
116
- }
117
-
118
- function buildWatermarkText() {
119
- const wm = (props.attr as any)?.watermark ?? {}
120
- const user = wm.userName ?? ''
121
- const time = formatNow(wm.format || 'YYYY-MM-DD HH:mm:ss')
122
- const lines: string[] = []
123
- if (user)
124
- lines.push(`拍摄人:${user}`)
125
- if (time)
126
- lines.push(`拍摄时间:${time}`)
127
- if (Array.isArray(wm.customLines) && wm.customLines.length > 0)
128
- lines.push(...wm.customLines.filter((v: any) => typeof v === 'string' && v !== ''))
129
- return lines.join('\n')
130
- }
131
-
132
- function handleBrowserFiles(files: FileList | null) {
133
- if (!files || files.length === 0)
134
- return
135
- const max = props.attr?.acceptCount ?? Number.POSITIVE_INFINITY
136
- for (let i = 0; i < files.length; i++) {
137
- if (imageList.value.length >= max)
138
- break
139
- const file = files[i]
140
- const reader = new FileReader()
141
- reader.onload = (e: any) => {
142
- const previewUrl = e.target?.result as string
143
- const temp = createTempUploadingItem(file.name, previewUrl)
144
- uploadFileViaHttp(file, previewUrl, temp)
145
- }
146
- reader.readAsDataURL(file)
147
- }
148
- }
149
-
150
- // 触发拍照
151
- function triggerCamera() {
152
- mobileUtil.execute({
153
- funcName: 'takePicture',
154
- param: (() => {
155
- const param: any = {}
156
- const watermark = (props.attr as any)?.watermark
157
- if (watermark) {
158
- const txt = buildWatermarkText()
159
- if (txt)
160
- param.watermark = txt
161
- if (watermark.fontSize !== undefined && String(watermark.fontSize) !== '')
162
- param.watermarkFontSize = watermark.fontSize
163
- if (watermark.color)
164
- param.watermarkColor = watermark.color
165
- if (watermark.alpha !== undefined && String(watermark.alpha) !== '')
166
- param.watermarkAlpha = watermark.alpha
167
- }
168
- return param
169
- })(),
170
- callbackFunc: (result: any) => {
171
- if (result.status === 'success') {
172
- handlePhotoUpload(result.data)
173
- }
174
- },
175
- })
176
- }
177
-
178
- // 处理拍照后的上传
179
- function getImageMimeType(fileName: string): string {
180
- const ext = fileName.split('.').pop()?.toLowerCase()
181
- if (ext === 'jpg' || ext === 'jpeg')
182
- return 'image/jpeg'
183
- if (ext === 'png')
184
- return 'image/png'
185
- if (ext === 'gif')
186
- return 'image/gif'
187
- return 'image/png' // 默认
188
- }
189
-
190
- function handlePhotoUpload(photoData: any) {
191
- // 添加临时预览
192
- const mimeType = getImageMimeType(photoData.filePath)
193
- const tempFile = {
194
- uid: Date.now() + Math.random().toString(36).substr(2, 5),
195
- name: photoData.filePath.split('/').pop(),
196
- status: 'uploading',
197
- message: '上传中...',
198
- url: `data:${mimeType};base64,${photoData.content}`,
199
- isImage: true,
200
- type: mimeType,
201
- }
202
-
203
- if (!imageList.value) {
204
- imageList.value = [tempFile]
205
- }
206
- else {
207
- imageList.value.push(tempFile)
208
- }
209
-
210
- const param = {
211
- resUploadMode: props.uploadMode,
212
- pathKey: 'Default',
213
- formType: 'image',
214
- useType: 'Default',
215
- resUploadStock: '1',
216
- filename: photoData.name,
217
- filesize: photoData.size,
218
- f_operator: 'server',
219
- imgPath: photoData.filePath,
220
- urlPath: `/api/${props.serviceName || import.meta.env.VITE_APP_SYSTEM_NAME}/resource/upload`,
221
- commonId: parentData?.commonId?.value ?? '',
222
- }
223
- if (props.isAsyncUpload) {
224
- // 添加上传队列
225
- mobileUtil.execute({
226
- funcName: 'queueUpload',
227
- param,
228
- callbackFunc: (res: any) => {
229
- console.warn('上传结果', res)
230
- // 成功
231
- if (res.data && res.data.enqueued) {
232
- const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
233
- console.log('index--------', index)
234
- if (index !== -1) {
235
- delete imageList.value[index].message
236
- imageList.value[index].status = 'done'
237
- imageList.value[index].photo_name = photoData.name
238
- imageList.value[index].type = mimeType
239
- }
240
- else {
241
- if (index !== -1) {
242
- imageList.value[index].status = 'failed'
243
- imageList.value[index].message = '上传失败'
244
- }
245
- }
246
- }
247
-
248
- emit('updateAllFileList', imageList.value.filter(item => item.status === 'done').map(item => item)) // 新增
249
-
250
- const doneIds = Object.values(imageList.value)
251
- .filter(item => !item.status || item.status === 'done')
252
- .map(item => item.photo_name)
253
-
254
- if (props.outerIndex !== undefined)
255
- emit('updateFileList', doneIds, props.outerIndex)
256
- else
257
- emit('updateFileList', doneIds)
258
- },
259
- })
260
- }
261
- else {
262
- // 上传到服务器
263
- mobileUtil.execute({
264
- funcName: 'uploadResource',
265
- param,
266
- callbackFunc: (result: any) => {
267
- const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
268
- if (result.status === 'success') {
269
- if (index !== -1) {
270
- imageList.value[index].uid = result.data.id
271
- imageList.value[index].id = result.data.id
272
- delete imageList.value[index].message
273
- imageList.value[index].status = 'done'
274
- imageList.value[index].url = result.data.f_downloadpath
275
- }
276
- }
277
- else {
278
- if (index !== -1) {
279
- imageList.value[index].status = 'failed'
280
- imageList.value[index].message = '上传失败'
281
- }
282
- }
283
-
284
- emit('updateAllFileList', imageList.value.filter(item => item.status === 'done').map(item => item)) // 新增
285
-
286
- const doneIds = Object.values(imageList.value)
287
- .filter(item => item.status === 'done')
288
- .map(item => item.id)
289
-
290
- if (props.outerIndex !== undefined)
291
- emit('updateFileList', doneIds, props.outerIndex)
292
- else
293
- emit('updateFileList', doneIds)
294
- },
295
- })
296
- }
297
- }
298
-
299
- // 删除图片
300
- function deleteFileFunction(file: any) {
301
- // 情况1:有后端id的已上传文件,先调接口再本地移除
302
- if (file.id) {
303
- deleteFile({ ids: [file.id], f_state: '删除' }).then((res: any) => {
304
- if (res.msg !== undefined) {
305
- const targetIndex = imageList.value.findIndex(item => item.id === file.id)
306
- if (targetIndex !== -1)
307
- imageList.value.splice(targetIndex, 1)
308
-
309
- emit('updateAllFileList', imageList.value.filter(item => item.status === 'done').map(item => item))
310
-
311
- const doneIds = ref(null)
312
- if (props.isAsyncUpload) {
313
- doneIds.value = Object.values(imageList.value)
314
- .filter(item => !item.status || item.status === 'done')
315
- .map(item => item.photo_name)
316
- }
317
- else {
318
- doneIds.value = Object.values(imageList.value)
319
- .filter(item => item.status === 'done')
320
- .map(item => item.id)
321
- }
322
- if (props.outerIndex !== undefined)
323
- emit('updateFileList', doneIds.value, props.outerIndex)
324
- else
325
- emit('updateFileList', doneIds.value)
326
- }
327
- })
328
- return false // 已手动移除,阻止van-uploader再次移除
329
- }
330
-
331
- // 情况2:上传失败/上传中的临时文件(无id),允许直接删除
332
- const targetIndex = imageList.value.findIndex(item => item.uid === file.uid || item.url === file.url)
333
- if (targetIndex !== -1)
334
- imageList.value.splice(targetIndex, 1)
335
-
336
- emit('updateAllFileList', imageList.value.filter(item => item.status === 'done').map(item => item))
337
-
338
- const doneIds = ref(null)
339
- if (props.isAsyncUpload) {
340
- doneIds.value = Object.values(imageList.value)
341
- .filter(item => !item.status || item.status === 'done')
342
- .map(item => item.photo_name)
343
- }
344
- else {
345
- doneIds.value = Object.values(imageList.value)
346
- .filter(item => item.status === 'done')
347
- .map(item => item.id)
348
- }
349
- if (props.outerIndex !== undefined)
350
- emit('updateFileList', doneIds.value, props.outerIndex)
351
- else
352
- emit('updateFileList', doneIds.value)
353
-
354
- return false // 阻止van-uploader二次处理
355
- }
356
-
357
- const showActionSheet = ref(false)
358
- const uploaderRef = ref()
359
-
360
- const actionOptions = [
361
- { name: '拍照', key: 'camera' },
362
- { name: '上传', key: 'file' },
363
- ]
364
-
365
- function handleUploadAreaClick() {
366
- if (props.readonly)
367
- return
368
- if (props.attr?.uploadImage) {
369
- showActionSheet.value = true
370
- }
371
- else {
372
- triggerCamera()
373
- }
374
- }
375
-
376
- function handleActionSelect(option: any) {
377
- showActionSheet.value = false
378
- if (option.key === 'camera') {
379
- if (import.meta.env.VITE_APP_WEI_XIN) {
380
- openBrowserCameraPicker()
381
- return
382
- }
383
- triggerCamera()
384
- }
385
- else if (option.key === 'file') {
386
- if (import.meta.env.VITE_APP_WEI_XIN) {
387
- openBrowserFilePicker()
388
- return
389
- }
390
- mobileUtil.execute({
391
- funcName: 'photoAlbum',
392
- param: {},
393
- callbackFunc: (result: any) => {
394
- console.log('>>>> result: ', result)
395
- if (result.status === 'success') {
396
- result.data?.photos.forEach((photo: any) => {
397
- handlePhotoUpload(photo)
398
- })
399
- }
400
- else {
401
- // 浏览器模式:打开文件选择
402
- openBrowserFilePicker()
403
- }
404
- },
405
- })
406
- }
407
- }
408
- </script>
409
-
410
- <template>
411
- <div class="uploader-container">
412
- <!-- 浏览器模式隐藏输入:文件选择 -->
413
- <input
414
- ref="fileInputRef"
415
- type="file"
416
- multiple
417
- accept="image/*"
418
- style="display:none"
419
- @change="(e:any) => handleBrowserFiles(e.target.files)"
420
- >
421
- <!-- 浏览器模式隐藏输入:拍照(相机) -->
422
- <input
423
- ref="cameraInputRef"
424
- type="file"
425
- accept="image/*"
426
- capture="environment"
427
- style="display:none"
428
- @change="(e:any) => handleBrowserFiles(e.target.files)"
429
- >
430
- <van-uploader
431
- ref="uploaderRef"
432
- v-model="imageList"
433
- class="custom-trigger-uploader"
434
- :show-upload="true"
435
- :deletable="!props.readonly && (props.attr?.addOrEdit !== 'readonly' && props.authority === 'admin') && props.mode !== '预览'"
436
- :multiple="!props.readonly && props.authority === 'admin'"
437
- :preview-image="true"
438
- :before-delete="!props.readonly && props.attr?.addOrEdit !== 'readonly' && props.authority === 'admin' ? deleteFileFunction : undefined"
439
- :before-read="() => false"
440
- >
441
- <template #default>
442
- <div
443
- v-if="!props.readonly && props.mode !== '预览' && (imageList.length < props.attr?.acceptCount && props.attr?.addOrEdit !== 'readonly')"
444
- class="custom-upload-area"
445
- @click="handleUploadAreaClick"
446
- >
447
- <VanIcon name="plus" size="35" color="#d9d9d9" />
448
- <div v-if="props.attr?.acceptCount" class="upload-desc">
449
- {{ imageList.length }} / {{ props.attr?.acceptCount }}
450
- </div>
451
- </div>
452
- </template>
453
- </van-uploader>
454
- <ActionSheet
455
- v-model:show="showActionSheet"
456
- :actions="actionOptions"
457
- cancel-text="取消"
458
- @select="handleActionSelect"
459
- />
460
- </div>
461
- </template>
462
-
463
- <style scoped lang="less">
464
- .uploader-container {
465
- display: flex;
466
- flex-direction: column;
467
- // 默认图片上传区域高度会影响布局
468
- width: 100%;
469
- height: 100%;
470
- // 该属性会影响表单布局
471
- // gap: 16px;
472
- }
473
- /* 让自定义上传按钮可点击:关闭 Vant 覆盖在上方的透明 input 的事件捕获 */
474
- :deep(.custom-trigger-uploader .van-uploader__input) {
475
- pointer-events: none;
476
- }
477
- .custom-upload-area {
478
- width: 82px;
479
- height: 82px;
480
- box-sizing: border-box;
481
- border: 1px dashed #d9d9d9;
482
- border-radius: 10px;
483
- background: #fff;
484
- display: flex;
485
- flex-direction: column;
486
- align-items: center;
487
- justify-content: center;
488
- cursor: pointer;
489
- padding: 8px 4px;
490
- flex: 1;
491
- .upload-camera-img {
492
- width: 28px;
493
- height: 28px;
494
- margin-bottom: 6px;
495
- object-fit: contain;
496
- display: block;
497
- }
498
- .upload-title {
499
- font-size: 14px;
500
- color: #bfc1c6;
501
- font-weight: 400;
502
- margin-bottom: 2px;
503
- }
504
- .upload-desc {
505
- font-size: 12px;
506
- color: #bfc1c6;
507
- margin-top: 0;
508
- }
509
- }
510
- </style>
1
+ <script setup lang="ts">
2
+ // import cameraIcon from '@af-mobile-client-vue3/assets/img/component/camera.png'
3
+ import { deleteFile, upload } from '@af-mobile-client-vue3/services/api/common'
4
+ import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
5
+ import { formatNow } from '@af-mobile-client-vue3/utils/timeUtil'
6
+ import {
7
+ ActionSheet,
8
+ Icon as VanIcon,
9
+ Uploader as vanUploader,
10
+ } from 'vant'
11
+ import { inject, ref } from 'vue'
12
+
13
+ interface WatermarkConfig {
14
+ fontSize?: number | string
15
+ color?: string
16
+ alpha?: number
17
+ userName?: string
18
+ format?: string
19
+ customLines?: string[]
20
+ }
21
+
22
+ const props = defineProps({
23
+ imageList: Array<any>,
24
+ outerIndex: { default: undefined },
25
+ authority: { default: 'user' },
26
+ uploadMode: { default: 'server' },
27
+ attr: { type: Object as () => { addOrEdit?: string, acceptCount?: number, uploadImage?: boolean, watermark?: WatermarkConfig }, default: () => ({}) },
28
+ mode: { default: '新增' }, // 预览
29
+ serviceName: { type: String, default: '' },
30
+ // 整体只读:只允许预览,禁止拍照/上传/删除
31
+ readonly: { type: Boolean, default: false },
32
+ isAsyncUpload: { type: Boolean, default: false },
33
+ })
34
+ const emit = defineEmits(['updateFileList', 'updateAllFileList'])
35
+
36
+ const imageList = ref<Array<any>>(props.imageList ?? [])
37
+
38
+ const parentData: any = inject('provideParent')
39
+ // 浏览器模式:隐藏的文件输入(选择 / 拍照)
40
+ const fileInputRef = ref<HTMLInputElement | undefined>()
41
+ const cameraInputRef = ref<HTMLInputElement | undefined>()
42
+
43
+ function openBrowserFilePicker() {
44
+ if (fileInputRef.value) {
45
+ fileInputRef.value.value = ''
46
+ fileInputRef.value.click()
47
+ }
48
+ }
49
+
50
+ function openBrowserCameraPicker() {
51
+ if (cameraInputRef.value) {
52
+ cameraInputRef.value.value = ''
53
+ cameraInputRef.value.click()
54
+ }
55
+ }
56
+
57
+ function emitUpdatesAfterChange() {
58
+ // 新增:对外抛出完整与ID列表
59
+ (emit as any)('updateAllFileList', imageList.value.filter(item => item.status === 'done').map(item => item))
60
+ const doneIds = Object.values(imageList.value)
61
+ .filter(item => item.status === 'done')
62
+ .map(item => item.id)
63
+ if (props.outerIndex !== undefined) {
64
+ (emit as any)('updateFileList', doneIds, props.outerIndex)
65
+ }
66
+ else {
67
+ (emit as any)('updateFileList', doneIds)
68
+ }
69
+ }
70
+
71
+ function createTempUploadingItem(name: string, previewUrl: string) {
72
+ const temp = {
73
+ uid: Date.now() + Math.random().toString(36).substr(2, 5),
74
+ name,
75
+ status: 'uploading',
76
+ message: '上传中...',
77
+ url: previewUrl,
78
+ }
79
+ if (!imageList.value)
80
+ imageList.value = [temp]
81
+ else imageList.value.push(temp)
82
+ return temp
83
+ }
84
+
85
+ function uploadFileViaHttp(file: File, previewUrl: string, temp: any) {
86
+ const formData = new FormData()
87
+ formData.append('avatar', file)
88
+ formData.append('resUploadMode', props.uploadMode)
89
+ formData.append('pathKey', 'Default')
90
+ formData.append('formType', 'image')
91
+ formData.append('useType', 'Default')
92
+ formData.append('resUploadStock', '1')
93
+ formData.append('filename', file.name)
94
+ formData.append('filesize', (file.size / 1024 / 1024).toFixed(4))
95
+ formData.append('f_operator', 'server')
96
+
97
+ upload(formData, props.serviceName || import.meta.env.VITE_APP_SYSTEM_NAME, { 'Content-Type': 'multipart/form-data' }).then((res: any) => {
98
+ const index = imageList.value.findIndex(item => item.uid === temp.uid)
99
+ if (res?.data?.id) {
100
+ if (index !== -1) {
101
+ imageList.value[index].uid = res.data.id
102
+ imageList.value[index].id = res.data.id
103
+ delete imageList.value[index].message
104
+ imageList.value[index].status = 'done'
105
+ imageList.value[index].url = `${'' || import.meta.env.VITE_APP_RESOURCE_PATH}${res.data.f_downloadpath}`
106
+ }
107
+ }
108
+ else {
109
+ if (index !== -1) {
110
+ imageList.value[index].status = 'failed'
111
+ imageList.value[index].message = '上传失败'
112
+ }
113
+ }
114
+ emitUpdatesAfterChange()
115
+ })
116
+ }
117
+
118
+ function buildWatermarkText() {
119
+ const wm = (props.attr as any)?.watermark ?? {}
120
+ const user = wm.userName ?? ''
121
+ const time = formatNow(wm.format || 'YYYY-MM-DD HH:mm:ss')
122
+ const lines: string[] = []
123
+ if (user)
124
+ lines.push(`拍摄人:${user}`)
125
+ if (time)
126
+ lines.push(`拍摄时间:${time}`)
127
+ if (Array.isArray(wm.customLines) && wm.customLines.length > 0)
128
+ lines.push(...wm.customLines.filter((v: any) => typeof v === 'string' && v !== ''))
129
+ return lines.join('\n')
130
+ }
131
+
132
+ function handleBrowserFiles(files: FileList | null) {
133
+ if (!files || files.length === 0)
134
+ return
135
+ const max = props.attr?.acceptCount ?? Number.POSITIVE_INFINITY
136
+ for (let i = 0; i < files.length; i++) {
137
+ if (imageList.value.length >= max)
138
+ break
139
+ const file = files[i]
140
+ const reader = new FileReader()
141
+ reader.onload = (e: any) => {
142
+ const previewUrl = e.target?.result as string
143
+ const temp = createTempUploadingItem(file.name, previewUrl)
144
+ uploadFileViaHttp(file, previewUrl, temp)
145
+ }
146
+ reader.readAsDataURL(file)
147
+ }
148
+ }
149
+
150
+ // 触发拍照
151
+ function triggerCamera() {
152
+ mobileUtil.execute({
153
+ funcName: 'takePicture',
154
+ param: (() => {
155
+ const param: any = {}
156
+ const watermark = (props.attr as any)?.watermark
157
+ if (watermark) {
158
+ const txt = buildWatermarkText()
159
+ if (txt)
160
+ param.watermark = txt
161
+ if (watermark.fontSize !== undefined && String(watermark.fontSize) !== '')
162
+ param.watermarkFontSize = watermark.fontSize
163
+ if (watermark.color)
164
+ param.watermarkColor = watermark.color
165
+ if (watermark.alpha !== undefined && String(watermark.alpha) !== '')
166
+ param.watermarkAlpha = watermark.alpha
167
+ }
168
+ return param
169
+ })(),
170
+ callbackFunc: (result: any) => {
171
+ if (result.status === 'success') {
172
+ handlePhotoUpload(result.data)
173
+ }
174
+ },
175
+ })
176
+ }
177
+
178
+ // 处理拍照后的上传
179
+ function getImageMimeType(fileName: string): string {
180
+ const ext = fileName.split('.').pop()?.toLowerCase()
181
+ if (ext === 'jpg' || ext === 'jpeg')
182
+ return 'image/jpeg'
183
+ if (ext === 'png')
184
+ return 'image/png'
185
+ if (ext === 'gif')
186
+ return 'image/gif'
187
+ return 'image/png' // 默认
188
+ }
189
+
190
+ function handlePhotoUpload(photoData: any) {
191
+ // 添加临时预览
192
+ const mimeType = getImageMimeType(photoData.filePath)
193
+ const tempFile = {
194
+ uid: Date.now() + Math.random().toString(36).substr(2, 5),
195
+ name: photoData.filePath.split('/').pop(),
196
+ status: 'uploading',
197
+ message: '上传中...',
198
+ url: `data:${mimeType};base64,${photoData.content}`,
199
+ isImage: true,
200
+ type: mimeType,
201
+ }
202
+
203
+ if (!imageList.value) {
204
+ imageList.value = [tempFile]
205
+ }
206
+ else {
207
+ imageList.value.push(tempFile)
208
+ }
209
+
210
+ const param = {
211
+ resUploadMode: props.uploadMode,
212
+ pathKey: 'Default',
213
+ formType: 'image',
214
+ useType: 'Default',
215
+ resUploadStock: '1',
216
+ filename: photoData.name,
217
+ filesize: photoData.size,
218
+ f_operator: 'server',
219
+ imgPath: photoData.filePath,
220
+ urlPath: `/api/${props.serviceName || import.meta.env.VITE_APP_SYSTEM_NAME}/resource/upload`,
221
+ commonId: parentData?.commonId?.value ?? '',
222
+ }
223
+ if (props.isAsyncUpload) {
224
+ // 添加上传队列
225
+ mobileUtil.execute({
226
+ funcName: 'queueUpload',
227
+ param,
228
+ callbackFunc: (res: any) => {
229
+ console.warn('上传结果', res)
230
+ // 成功
231
+ if (res.data && res.data.enqueued) {
232
+ const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
233
+ console.log('index--------', index)
234
+ if (index !== -1) {
235
+ delete imageList.value[index].message
236
+ imageList.value[index].status = 'done'
237
+ imageList.value[index].photo_name = photoData.name
238
+ imageList.value[index].type = mimeType
239
+ }
240
+ else {
241
+ if (index !== -1) {
242
+ imageList.value[index].status = 'failed'
243
+ imageList.value[index].message = '上传失败'
244
+ }
245
+ }
246
+ }
247
+
248
+ emit('updateAllFileList', imageList.value.filter(item => item.status === 'done').map(item => item)) // 新增
249
+
250
+ const doneIds = Object.values(imageList.value)
251
+ .filter(item => !item.status || item.status === 'done')
252
+ .map(item => item.photo_name)
253
+
254
+ if (props.outerIndex !== undefined)
255
+ emit('updateFileList', doneIds, props.outerIndex)
256
+ else
257
+ emit('updateFileList', doneIds)
258
+ },
259
+ })
260
+ }
261
+ else {
262
+ // 上传到服务器
263
+ mobileUtil.execute({
264
+ funcName: 'uploadResource',
265
+ param,
266
+ callbackFunc: (result: any) => {
267
+ const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
268
+ if (result.status === 'success') {
269
+ if (index !== -1) {
270
+ imageList.value[index].uid = result.data.id
271
+ imageList.value[index].id = result.data.id
272
+ delete imageList.value[index].message
273
+ imageList.value[index].status = 'done'
274
+ imageList.value[index].url = `${'' || import.meta.env.VITE_APP_RESOURCE_PATH}${result.data.f_downloadpath}`
275
+ }
276
+ }
277
+ else {
278
+ if (index !== -1) {
279
+ imageList.value[index].status = 'failed'
280
+ imageList.value[index].message = '上传失败'
281
+ }
282
+ }
283
+
284
+ emit('updateAllFileList', imageList.value.filter(item => item.status === 'done').map(item => item)) // 新增
285
+
286
+ const doneIds = Object.values(imageList.value)
287
+ .filter(item => item.status === 'done')
288
+ .map(item => item.id)
289
+
290
+ if (props.outerIndex !== undefined)
291
+ emit('updateFileList', doneIds, props.outerIndex)
292
+ else
293
+ emit('updateFileList', doneIds)
294
+ },
295
+ })
296
+ }
297
+ }
298
+
299
+ // 删除图片
300
+ function deleteFileFunction(file: any) {
301
+ // 情况1:有后端id的已上传文件,先调接口再本地移除
302
+ if (file.id) {
303
+ deleteFile({ ids: [file.id], f_state: '删除' }).then((res: any) => {
304
+ if (res.msg !== undefined) {
305
+ const targetIndex = imageList.value.findIndex(item => item.id === file.id)
306
+ if (targetIndex !== -1)
307
+ imageList.value.splice(targetIndex, 1)
308
+
309
+ emit('updateAllFileList', imageList.value.filter(item => item.status === 'done').map(item => item))
310
+
311
+ const doneIds = ref(null)
312
+ if (props.isAsyncUpload) {
313
+ doneIds.value = Object.values(imageList.value)
314
+ .filter(item => !item.status || item.status === 'done')
315
+ .map(item => item.photo_name)
316
+ }
317
+ else {
318
+ doneIds.value = Object.values(imageList.value)
319
+ .filter(item => item.status === 'done')
320
+ .map(item => item.id)
321
+ }
322
+ if (props.outerIndex !== undefined)
323
+ emit('updateFileList', doneIds.value, props.outerIndex)
324
+ else
325
+ emit('updateFileList', doneIds.value)
326
+ }
327
+ })
328
+ return false // 已手动移除,阻止van-uploader再次移除
329
+ }
330
+
331
+ // 情况2:上传失败/上传中的临时文件(无id),允许直接删除
332
+ const targetIndex = imageList.value.findIndex(item => item.uid === file.uid || item.url === file.url)
333
+ if (targetIndex !== -1)
334
+ imageList.value.splice(targetIndex, 1)
335
+
336
+ emit('updateAllFileList', imageList.value.filter(item => item.status === 'done').map(item => item))
337
+
338
+ const doneIds = ref(null)
339
+ if (props.isAsyncUpload) {
340
+ doneIds.value = Object.values(imageList.value)
341
+ .filter(item => !item.status || item.status === 'done')
342
+ .map(item => item.photo_name)
343
+ }
344
+ else {
345
+ doneIds.value = Object.values(imageList.value)
346
+ .filter(item => item.status === 'done')
347
+ .map(item => item.id)
348
+ }
349
+ if (props.outerIndex !== undefined)
350
+ emit('updateFileList', doneIds.value, props.outerIndex)
351
+ else
352
+ emit('updateFileList', doneIds.value)
353
+
354
+ return false // 阻止van-uploader二次处理
355
+ }
356
+
357
+ const showActionSheet = ref(false)
358
+ const uploaderRef = ref()
359
+
360
+ const actionOptions = [
361
+ { name: '拍照', key: 'camera' },
362
+ { name: '上传', key: 'file' },
363
+ ]
364
+
365
+ function handleUploadAreaClick() {
366
+ if (props.readonly)
367
+ return
368
+ if (props.attr?.uploadImage) {
369
+ showActionSheet.value = true
370
+ }
371
+ else {
372
+ triggerCamera()
373
+ }
374
+ }
375
+
376
+ function handleActionSelect(option: any) {
377
+ showActionSheet.value = false
378
+ if (option.key === 'camera') {
379
+ if (import.meta.env.VITE_APP_WEI_XIN) {
380
+ openBrowserCameraPicker()
381
+ return
382
+ }
383
+ triggerCamera()
384
+ }
385
+ else if (option.key === 'file') {
386
+ if (import.meta.env.VITE_APP_WEI_XIN) {
387
+ openBrowserFilePicker()
388
+ return
389
+ }
390
+ mobileUtil.execute({
391
+ funcName: 'photoAlbum',
392
+ param: {},
393
+ callbackFunc: (result: any) => {
394
+ console.log('>>>> result: ', result)
395
+ if (result.status === 'success') {
396
+ result.data?.photos.forEach((photo: any) => {
397
+ handlePhotoUpload(photo)
398
+ })
399
+ }
400
+ else {
401
+ // 浏览器模式:打开文件选择
402
+ openBrowserFilePicker()
403
+ }
404
+ },
405
+ })
406
+ }
407
+ }
408
+ </script>
409
+
410
+ <template>
411
+ <div class="uploader-container">
412
+ <!-- 浏览器模式隐藏输入:文件选择 -->
413
+ <input
414
+ ref="fileInputRef"
415
+ type="file"
416
+ multiple
417
+ accept="image/*"
418
+ style="display:none"
419
+ @change="(e:any) => handleBrowserFiles(e.target.files)"
420
+ >
421
+ <!-- 浏览器模式隐藏输入:拍照(相机) -->
422
+ <input
423
+ ref="cameraInputRef"
424
+ type="file"
425
+ accept="image/*"
426
+ capture="environment"
427
+ style="display:none"
428
+ @change="(e:any) => handleBrowserFiles(e.target.files)"
429
+ >
430
+ <van-uploader
431
+ ref="uploaderRef"
432
+ v-model="imageList"
433
+ class="custom-trigger-uploader"
434
+ :show-upload="true"
435
+ :deletable="!props.readonly && (props.attr?.addOrEdit !== 'readonly' && props.authority === 'admin') && props.mode !== '预览'"
436
+ :multiple="!props.readonly && props.authority === 'admin'"
437
+ :preview-image="true"
438
+ :before-delete="!props.readonly && props.attr?.addOrEdit !== 'readonly' && props.authority === 'admin' ? deleteFileFunction : undefined"
439
+ :before-read="() => false"
440
+ >
441
+ <template #default>
442
+ <div
443
+ v-if="!props.readonly && props.mode !== '预览' && (imageList.length < props.attr?.acceptCount && props.attr?.addOrEdit !== 'readonly')"
444
+ class="custom-upload-area"
445
+ @click="handleUploadAreaClick"
446
+ >
447
+ <VanIcon name="plus" size="35" color="#d9d9d9" />
448
+ <div v-if="props.attr?.acceptCount" class="upload-desc">
449
+ {{ imageList.length }} / {{ props.attr?.acceptCount }}
450
+ </div>
451
+ </div>
452
+ </template>
453
+ </van-uploader>
454
+ <ActionSheet
455
+ v-model:show="showActionSheet"
456
+ :actions="actionOptions"
457
+ cancel-text="取消"
458
+ @select="handleActionSelect"
459
+ />
460
+ </div>
461
+ </template>
462
+
463
+ <style scoped lang="less">
464
+ .uploader-container {
465
+ display: flex;
466
+ flex-direction: column;
467
+ // 默认图片上传区域高度会影响布局
468
+ width: 100%;
469
+ height: 100%;
470
+ // 该属性会影响表单布局
471
+ // gap: 16px;
472
+ }
473
+ /* 让自定义上传按钮可点击:关闭 Vant 覆盖在上方的透明 input 的事件捕获 */
474
+ :deep(.custom-trigger-uploader .van-uploader__input) {
475
+ pointer-events: none;
476
+ }
477
+ .custom-upload-area {
478
+ width: 82px;
479
+ height: 82px;
480
+ box-sizing: border-box;
481
+ border: 1px dashed #d9d9d9;
482
+ border-radius: 10px;
483
+ background: #fff;
484
+ display: flex;
485
+ flex-direction: column;
486
+ align-items: center;
487
+ justify-content: center;
488
+ cursor: pointer;
489
+ padding: 8px 4px;
490
+ flex: 1;
491
+ .upload-camera-img {
492
+ width: 28px;
493
+ height: 28px;
494
+ margin-bottom: 6px;
495
+ object-fit: contain;
496
+ display: block;
497
+ }
498
+ .upload-title {
499
+ font-size: 14px;
500
+ color: #bfc1c6;
501
+ font-weight: 400;
502
+ margin-bottom: 2px;
503
+ }
504
+ .upload-desc {
505
+ font-size: 12px;
506
+ color: #bfc1c6;
507
+ margin-top: 0;
508
+ }
509
+ }
510
+ </style>