af-mobile-client-vue3 1.6.42 → 1.6.44

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.
Files changed (37) hide show
  1. package/.env +1 -1
  2. package/package.json +1 -1
  3. package/src/components/common/otherCharge/ChargePrintSelectorAndRemarks.vue +4 -1
  4. package/src/components/common/otherCharge/GridFileUploader.vue +4 -2
  5. package/src/components/core/ImageUploader/index.vue +69 -242
  6. package/src/components/core/Uploader/index.vue +6 -2
  7. package/src/components/core/XDatePicker/index.vue +8 -2
  8. package/src/components/core/XMultiSelect/index.vue +6 -2
  9. package/src/components/core/XSelect/index.vue +40 -15
  10. package/src/components/data/OtherCharge/OtherChargeGroupModal.vue +542 -542
  11. package/src/components/data/XCellList/index.vue +16 -9
  12. package/src/components/data/XCellListFilter/index.vue +26 -3
  13. package/src/components/data/XForm/index.vue +10 -5
  14. package/src/components/data/XFormItem/index.vue +57 -73
  15. package/src/components/data/XReportGrid/XReportDemo.vue +33 -33
  16. package/src/components/data/XReportGrid/print.js +184 -184
  17. package/src/stores/modules/safecheckStore.ts +47 -47
  18. package/src/utils/PhotoClean.ts +21 -21
  19. package/src/utils/appBackManager.ts +23 -23
  20. package/src/utils/nativeFilePicker.ts +10 -1
  21. package/src/utils/safeBack.ts +38 -38
  22. package/src/utils/timeUtil.ts +27 -27
  23. package/src/views/SafeInspection/SecurityCertificate/AddDevice/index.vue +9 -20
  24. package/src/views/SafeInspection/SecurityCertificate/OverallHiddenDangers/index.vue +21 -8
  25. package/src/views/SafeInspection/SecurityCertificate/contractSign/index.vue +0 -1
  26. package/src/views/SafeInspection/SecurityCertificate/index.vue +1 -2
  27. package/src/views/SafeInspection/SecurityCertificate/photoAiRecognition/components/PhotoCaptureCard.vue +21 -5
  28. package/src/views/SafeInspection/SecurityCertificate/photoSignature/SignatureComponent/SignatureComponent.vue +13 -3
  29. package/src/views/SafeInspection/SecurityCertificate/photoSignature/index.vue +9 -5
  30. package/src/views/SafeInspection/SecurityCertificate/userInfo/index.vue +11 -15
  31. package/src/views/SafeInspection/SecurityCertificate/userInfo/upaddress.vue +5 -4
  32. package/src/views/SafeInspection/SecurityFormItem/CameraView/index.vue +3 -9
  33. package/src/views/SafeInspection/SecurityFormItem/FormItem.vue +23 -15
  34. package/src/views/SafeInspection/SecurityFormItem/XMultiSelect/index.vue +8 -10
  35. package/src/views/SafeInspection/SecurityFormItem/index.vue +16 -10
  36. package/src/views/component/XFormView/index.vue +2 -2
  37. package/vite.config.ts +1 -1
package/.env CHANGED
@@ -5,7 +5,7 @@ VITE_APP_OUT_DIR=dist_af-library-mobile-web
5
5
  VITE_APP_VCONSOLE=false
6
6
  # 自定义参数
7
7
  VITE_APP_WEB_CONFIG_KEY=admin.webconfig
8
- VITE_APP_SYSTEM_NAME=af-safecheck
8
+ VITE_APP_SYSTEM_NAME=af-system
9
9
  # 最低兼容性 V4(最新产品)V3(V3产品) OA(公司OA)
10
10
  VITE_APP_COMPATIBLE=V4
11
11
  VITE_RSA_PRIVATE_KEY=MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIRrmftLDHCQqREEJ132Onu+W3vmFbdF7QD751SrcDDGDTfzuz1zBuElvkHhuDBb7KZkXrCIe+MhvX2IvxcLObl3faX+evYlnfj2HRbF0hIpQLuIq22tL06ZcV5w7wqLxUZRpFElIFm8gZTkUvfKXVuHw89e4daDVhU5hK3GHNGTAgMBAAECgYABiINrFaE1E8pkBYx1JJA5yuhL73aUktfd2TeCU00vFg6kyrWCI85Sa2RKu/6CJNZWeOFgdubEUv7a22tRrNIZb3yUMaqtTwSso78mspIOJqjWXTkTH9WPElfTcdpdIse/lgZtPz6egxkuhadSvwrM9Y6NgusiW/5+x95Ct08iOQJBAN5aK+7uISURvGQj2EaRtgGEd8+d4oHl+BYvvTeG3qSgUikHQW3j0sp4gXPw2kxw6sjVgLFOc4FB6LGqwzOTzokCQQCYdYG8ty3Uo/ebUlNzeJFxHXjy/KvBSytAUzAXkRu3nZrkEaPQsi3dgOkZgk+F1fMDzfQ4EbDIU6xvqOoZXHg7AkATCW9XfoXR8anKfRMoP5Nwn9HOMbtR2cmaxK2TknV/bMZ8AsYETYwfj5+tuIJIJybC2RyykX/sIiN1CqS5xr7ZAkArj19rMRdaKyMi8MnBM1Cy9g3Jt2HHj5ejAGG8SgyWUOShh1y70z0BjcSMMkxQXAncK2s83ekZw7aADM4eQupjAkARRgTwwMOnn3IoKmQusKhZk0uxilZ4Zc2LH6Z4GiWnvteM0W8Zw4Z1lJUcjgQq3dGqL2RdmzeQZ+HgPIOXrZVK
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "af-mobile-client-vue3",
3
3
  "type": "module",
4
- "version": "1.6.42",
4
+ "version": "1.6.44",
5
5
  "packageManager": "pnpm@10.13.1",
6
6
  "description": "Vue + Vite component lib",
7
7
  "engines": {
@@ -6,7 +6,7 @@ import {
6
6
  Picker as VanPicker,
7
7
  Popup as VanPopup,
8
8
  } from 'vant'
9
- import { ref, watch } from 'vue'
9
+ import { onBeforeUnmount, ref, watch } from 'vue'
10
10
 
11
11
  const props = defineProps<{
12
12
  modelValue: string
@@ -51,6 +51,9 @@ watch(() => props.modelValue, (newValue) => {
51
51
  watch(() => props.remarks, (newValue) => {
52
52
  remarks.value = newValue
53
53
  })
54
+ onBeforeUnmount(() => {
55
+ showPicker.value = false
56
+ })
54
57
  </script>
55
58
 
56
59
  <template>
@@ -10,7 +10,7 @@ import {
10
10
  Icon as VanIcon,
11
11
  ImagePreview as VanImagePreview,
12
12
  } from 'vant'
13
- import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
13
+ import {computed, onBeforeUnmount, onMounted, onUnmounted, reactive, ref} from 'vue'
14
14
 
15
15
  export interface FileTypeConfig {
16
16
  userType: string
@@ -371,7 +371,9 @@ function validateAll(): boolean {
371
371
  const minLength = computed(() => {
372
372
  return props.fileTypes.reduce((acc, config) => acc + config.picMinNum, 0)
373
373
  })
374
-
374
+ onBeforeUnmount(() => {
375
+ showPreview.value = false
376
+ })
375
377
  // 暴露验证方法给父组件
376
378
  defineExpose({
377
379
  validateAll,
@@ -1,15 +1,8 @@
1
1
  <script setup lang="ts">
2
- import type { NativePickedFile } from '@af-mobile-client-vue3/utils/nativeFilePicker'
3
2
  // import cameraIcon from '@af-mobile-client-vue3/assets/img/component/camera.png'
4
3
  import { deleteFile, upload } from '@af-mobile-client-vue3/services/api/common'
5
4
  import { detectEnvironment } from '@af-mobile-client-vue3/utils/environment'
6
- import { buildInputAccept, getFileTypeMeta, getUploadFormType, isImageFileName } from '@af-mobile-client-vue3/utils/fileType'
7
5
  import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
8
- import {
9
- openNativeFilePicker,
10
- triggerBrowserFileInput,
11
- } from '@af-mobile-client-vue3/utils/nativeFilePicker'
12
- import { toDataUrl } from '@af-mobile-client-vue3/utils/resourceUrl'
13
6
  import { formatNow } from '@af-mobile-client-vue3/utils/timeUtil'
14
7
  import {
15
8
  ActionSheet,
@@ -18,7 +11,7 @@ import {
18
11
  Icon as VanIcon,
19
12
  Uploader as vanUploader,
20
13
  } from 'vant'
21
- import { computed, inject, ref } from 'vue'
14
+ import { inject, onBeforeUnmount, ref } from 'vue'
22
15
 
23
16
  interface WatermarkConfig {
24
17
  fontSize?: number | string
@@ -35,7 +28,7 @@ const props = defineProps({
35
28
  outerIndex: { default: undefined },
36
29
  authority: { default: 'user' },
37
30
  uploadMode: { default: 'server' },
38
- attr: { type: Object as () => { type?: string, addOrEdit?: string, accept?: string[] | string, acceptCount?: number, minAcceptCount?: number, uploadImage?: boolean, watermark?: WatermarkConfig, useType?: string }, default: () => ({}) },
31
+ attr: { type: Object as () => { addOrEdit?: string, acceptCount?: number, minAcceptCount?: number, uploadImage?: boolean, watermark?: WatermarkConfig, useType?: string }, default: () => ({}) },
39
32
  mode: { default: '新增' }, // 预览
40
33
  serviceName: { type: String, default: '' },
41
34
  // 整体只读:只允许预览,禁止拍照/上传/删除
@@ -46,18 +39,28 @@ const emit = defineEmits(['updateFileList', 'updateAllFileList'])
46
39
 
47
40
  const imageList = ref<Array<any>>(props.imageList ?? [])
48
41
 
49
- const parentData: any = inject('provideParent')
50
- const imageInputRef = ref<HTMLInputElement | undefined>()
51
- const documentInputRef = ref<HTMLInputElement | undefined>()
52
-
53
- const documentAccept = computed(() => buildInputAccept(props.attr?.accept))
42
+ // 从父组件获取 commonId,仅读取一次而非持有引用
43
+ const parentCommonId = (() => {
44
+ try {
45
+ const parent = inject<any>('provideParent', undefined)
46
+ return parent?.commonId?.value ?? ''
47
+ }
48
+ catch {
49
+ return ''
50
+ }
51
+ })()
54
52
 
55
- function openImageFilePicker() {
56
- triggerBrowserFileInput(imageInputRef.value)
57
- }
53
+ // 浏览器 / 微信内 H5:统一用隐藏 file input(与单独 camera input 行为等价,均为系统选图)
54
+ // const fileInputRef = ref<HTMLInputElement | undefined>()
55
+ const parentData: any = inject('provideParent')
56
+ // 浏览器 / 微信内 H5:统一用隐藏 file input(与单独 camera input 行为等价,均为系统选图)
57
+ const fileInputRef = ref<HTMLInputElement | undefined>()
58
58
 
59
- function openDocumentFilePicker() {
60
- triggerBrowserFileInput(documentInputRef.value)
59
+ function openBrowserFilePicker() {
60
+ if (fileInputRef.value) {
61
+ fileInputRef.value.value = ''
62
+ fileInputRef.value.click()
63
+ }
61
64
  }
62
65
 
63
66
  function emitUpdatesAfterChange() {
@@ -74,31 +77,13 @@ function emitUpdatesAfterChange() {
74
77
  }
75
78
  }
76
79
 
77
- const DOC_TILE_PLACEHOLDER = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="82" height="82"><rect width="82" height="82" rx="8" fill="#f0f1f5"/></svg>')}`
78
-
79
- function isFileItemImage(item: any): boolean {
80
- if (item?.isDocument)
81
- return false
82
- if (item?.isImage === false)
83
- return false
84
- if (item?.isImage === true && !item?.isDocument)
85
- return true
86
- return isImageFileName(item?.name || item?.photo_name || item?.f_downloadpath || '')
87
- }
88
-
89
- function getItemFileMeta(item: any) {
90
- return getFileTypeMeta(item?.name || item?.photo_name || item?.f_downloadpath || '')
91
- }
92
-
93
- function createTempUploadingItem(name: string, previewUrl: string, isImage = true) {
80
+ function createTempUploadingItem(name: string, previewUrl: string) {
94
81
  const temp = {
95
82
  uid: Date.now() + Math.random().toString(36).substr(2, 5),
96
83
  name,
97
84
  status: 'uploading',
98
85
  message: '上传中...',
99
- url: isImage ? (previewUrl || undefined) : DOC_TILE_PLACEHOLDER,
100
- isImage: true,
101
- isDocument: !isImage,
86
+ url: previewUrl,
102
87
  }
103
88
  if (!imageList.value)
104
89
  imageList.value = [temp]
@@ -106,12 +91,12 @@ function createTempUploadingItem(name: string, previewUrl: string, isImage = tru
106
91
  return temp
107
92
  }
108
93
 
109
- function uploadFileViaHttp(file: File, previewUrl: string, temp: any, formType: 'image' | 'file' = 'image') {
94
+ function uploadFileViaHttp(file: File, previewUrl: string, temp: any) {
110
95
  const formData = new FormData()
111
96
  formData.append('avatar', file)
112
97
  formData.append('resUploadMode', props.uploadMode)
113
98
  formData.append('pathKey', 'Default')
114
- formData.append('formType', formType)
99
+ formData.append('formType', 'image')
115
100
  formData.append('useType', 'Default')
116
101
  formData.append('resUploadStock', '1')
117
102
  formData.append('filename', file.name)
@@ -130,15 +115,9 @@ function uploadFileViaHttp(file: File, previewUrl: string, temp: any, formType:
130
115
  if (index !== -1) {
131
116
  imageList.value[index].uid = res.data.id
132
117
  imageList.value[index].id = res.data.id
133
- imageList.value[index].f_downloadpath = res.data.f_downloadpath
134
- imageList.value[index].photo_name = res.data.f_filename
135
- imageList.value[index].isDocument = formType === 'file'
136
- imageList.value[index].isImage = true
137
- if (formType === 'file')
138
- imageList.value[index].url = DOC_TILE_PLACEHOLDER
139
118
  delete imageList.value[index].message
140
119
  imageList.value[index].status = 'done'
141
- // 与签名一致:保留本地 base64 预览,不因替换为服务器相对路径导致部分 APK 展示损坏
120
+ imageList.value[index].url = `${import.meta.env.VITE_APP_RESOURCE_PATH || ''}${res.data.f_downloadpath}`
142
121
  }
143
122
  }
144
123
  else {
@@ -165,7 +144,7 @@ function buildWatermarkText() {
165
144
  return lines.join('\n')
166
145
  }
167
146
 
168
- function handleBrowserImageFiles(files: FileList | null) {
147
+ function handleBrowserFiles(files: FileList | null) {
169
148
  if (!files || files.length === 0)
170
149
  return
171
150
  const max = props.attr?.acceptCount ?? Number.POSITIVE_INFINITY
@@ -173,117 +152,23 @@ function handleBrowserImageFiles(files: FileList | null) {
173
152
  if (imageList.value.length >= max)
174
153
  break
175
154
  const file = files[i]
176
- const formType = getUploadFormType(file)
177
- if (formType === 'file') {
178
- const temp = createTempUploadingItem(file.name, '', false)
179
- uploadFileViaHttp(file, '', temp, 'file')
180
- continue
181
- }
182
155
  const reader = new FileReader()
183
156
  reader.onload = (e: any) => {
184
157
  const previewUrl = e.target?.result as string
185
- const temp = createTempUploadingItem(file.name, previewUrl, true)
186
- uploadFileViaHttp(file, previewUrl, temp, 'image')
158
+ const temp = createTempUploadingItem(file.name, previewUrl)
159
+ uploadFileViaHttp(file, previewUrl, temp)
187
160
  }
188
161
  reader.readAsDataURL(file)
189
162
  }
190
163
  }
191
164
 
192
- function handleBrowserDocumentFiles(files: FileList | null) {
193
- if (!files || files.length === 0)
194
- return
195
- const max = props.attr?.acceptCount ?? Number.POSITIVE_INFINITY
196
- for (let i = 0; i < files.length; i++) {
197
- if (imageList.value.length >= max)
198
- break
199
- const file = files[i]
200
- const formType = getUploadFormType(file)
201
- if (formType === 'image') {
202
- const reader = new FileReader()
203
- reader.onload = (e: any) => {
204
- const previewUrl = e.target?.result as string
205
- const temp = createTempUploadingItem(file.name, previewUrl, true)
206
- uploadFileViaHttp(file, previewUrl, temp, 'image')
207
- }
208
- reader.readAsDataURL(file)
209
- }
210
- else {
211
- const temp = createTempUploadingItem(file.name, '', false)
212
- uploadFileViaHttp(file, '', temp, 'file')
213
- }
214
- }
215
- }
216
-
217
- function handleNativeDocumentUpload(fileData: NativePickedFile) {
218
- const filePath = fileData.filePath || fileData.path || ''
219
- const fileName = fileData.name || filePath.split('/').pop() || 'file'
220
- const tempFile = createTempUploadingItem(fileName, '', false)
221
-
222
- const param = {
223
- resUploadMode: props.uploadMode,
224
- pathKey: 'Default',
225
- formType: 'file' as const,
226
- useType: props.attr?.useType || 'Default',
227
- resUploadStock: '1',
228
- filename: fileName,
229
- filesize: fileData.size,
230
- f_operator: 'server',
231
- imgPath: filePath,
232
- urlPath: `/api/${props.serviceName || import.meta.env.VITE_APP_SYSTEM_NAME}/resource/upload`,
233
- commonId: parentData?.commonId?.value ?? '',
234
- }
235
-
236
- mobileUtil.execute({
237
- funcName: 'uploadResource',
238
- param,
239
- callbackFunc: (result: any) => {
240
- const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
241
- if (result.status === 'success') {
242
- if (index !== -1) {
243
- imageList.value[index].uid = result.data.id
244
- imageList.value[index].id = result.data.id
245
- imageList.value[index].f_downloadpath = result.data.f_downloadpath
246
- imageList.value[index].photo_name = result.data.f_filename
247
- imageList.value[index].isDocument = true
248
- imageList.value[index].isImage = true
249
- imageList.value[index].url = DOC_TILE_PLACEHOLDER
250
- delete imageList.value[index].message
251
- imageList.value[index].status = 'done'
252
- }
253
- }
254
- else if (index !== -1) {
255
- imageList.value[index].status = 'failed'
256
- imageList.value[index].message = '上传失败'
257
- }
258
- emitUpdatesAfterChange()
259
- },
260
- })
261
- }
262
-
263
- function triggerDocumentPicker() {
264
- const startedNative = openNativeFilePicker({
265
- accept: documentAccept.value,
266
- multiple: true,
267
- onSuccess: files => files.forEach(handleNativeDocumentUpload),
268
- onError: (msg) => {
269
- if (msg)
270
- showFailToast(msg)
271
- },
272
- })
273
- if (startedNative)
274
- return
275
-
276
- // 与「相册中选择」一致:ActionSheet 点击后立即调起系统文件选择器
277
- openDocumentFilePicker()
278
- }
279
-
280
165
  // 触发拍照
281
166
  function triggerCamera() {
282
167
  mobileUtil.execute({
283
168
  funcName: 'takePicture',
284
169
  param: (() => {
285
170
  const param: any = {}
286
- const watermark = (props.attr as any)?.watermark
171
+ const watermark = { ...(props.attr as any)?.watermark }
287
172
  if (watermark) {
288
173
  const txt = buildWatermarkText()
289
174
  if (txt)
@@ -302,9 +187,6 @@ function triggerCamera() {
302
187
  rePhotoCheck(result.data)
303
188
  }
304
189
  else if (result.status === 'error') {
305
- if (result.message && result.message.startsWith('图片质量检测失败')) {
306
- parentData?.onPhotoQualityFailed?.()
307
- }
308
190
  const env = detectEnvironment()
309
191
  // App 环境下弹窗提示错误
310
192
  if (env.isApp) {
@@ -314,8 +196,8 @@ function triggerCamera() {
314
196
  })
315
197
  return
316
198
  }
317
- // 浏览器模式:打开图片文件选择
318
- openImageFilePicker()
199
+ // 浏览器模式:打开文件选择
200
+ openBrowserFilePicker()
319
201
  }
320
202
  },
321
203
  })
@@ -348,7 +230,6 @@ function rePhotoCheck(photoData: any) {
348
230
  if (result && result.status === 'success') {
349
231
  const checkRePhotoResult = result.data
350
232
  if (checkRePhotoResult.is_rephoto === true || checkRePhotoResult.is_rephoto === 'true') {
351
- parentData?.onPhotoRephotodetected?.()
352
233
  console.warn(`检测到当前图片属于翻拍照片\n请上传合法图片!`)
353
234
  showFailToast({
354
235
  message: `当前图片属于翻拍照片\n请上传合法图片!`,
@@ -393,14 +274,13 @@ function handlePhotoUpload(photoData: any) {
393
274
  console.log('图片上传前的参数:', JSON.stringify(photoData))
394
275
  // 添加临时预览
395
276
  const mimeType = getImageMimeType(photoData.filePath)
396
- const previewUrl = toDataUrl(photoData.content, mimeType)
397
277
  const tempFile = {
398
278
  uid: Date.now() + Math.random().toString(36).substr(2, 5),
399
279
  name: photoData.filePath.split('/').pop(),
400
280
  status: 'uploading',
401
281
  message: '上传中...',
402
- url: previewUrl,
403
- content: previewUrl,
282
+ url: `data:${mimeType};base64,${photoData.content}`,
283
+ content: `data:image/png;base64,${photoData.content}`,
404
284
  isImage: true,
405
285
  type: mimeType,
406
286
  }
@@ -415,7 +295,7 @@ function handlePhotoUpload(photoData: any) {
415
295
  const param = {
416
296
  resUploadMode: props.uploadMode,
417
297
  pathKey: 'Default',
418
- formType: 'image' as const,
298
+ formType: 'image',
419
299
  useType: props.attr?.useType || 'Default',
420
300
  resUploadStock: '1',
421
301
  filename: photoData.name,
@@ -423,7 +303,7 @@ function handlePhotoUpload(photoData: any) {
423
303
  f_operator: 'server',
424
304
  imgPath: photoData.filePath,
425
305
  urlPath: `/api/${props.serviceName || import.meta.env.VITE_APP_SYSTEM_NAME}/resource/upload`,
426
- commonId: parentData?.commonId?.value ?? '',
306
+ commonId: parentCommonId,
427
307
  }
428
308
  if (props.isAsyncUpload) {
429
309
  // 添加上传队列
@@ -474,12 +354,9 @@ function handlePhotoUpload(photoData: any) {
474
354
  if (index !== -1) {
475
355
  imageList.value[index].uid = result.data.id
476
356
  imageList.value[index].id = result.data.id
477
- imageList.value[index].f_downloadpath = result.data.f_downloadpath
478
- imageList.value[index].photo_name = result.data.f_filename
479
- imageList.value[index].isImage = true
480
357
  delete imageList.value[index].message
481
358
  imageList.value[index].status = 'done'
482
- // 保留拍照后的 base64 预览,与 SignatureComponent 行为一致
359
+ imageList.value[index].url = `${import.meta.env.VITE_APP_RESOURCE_PATH || ''}${result.data.f_downloadpath}`
483
360
  }
484
361
  }
485
362
  else {
@@ -566,28 +443,34 @@ const showActionSheet = ref(false)
566
443
  const uploaderRef = ref()
567
444
 
568
445
  const actionOptions = [
569
- { name: '拍摄', key: 'camera' },
570
- { name: '相册中选择', key: 'album' },
571
- { name: '文件上传', key: 'document' },
446
+ { name: '拍照', key: 'camera' },
447
+ { name: '上传', key: 'file' },
572
448
  ]
573
449
 
574
450
  function handleUploadAreaClick() {
575
451
  if (props.readonly)
576
452
  return
577
- showActionSheet.value = true
453
+ const env = detectEnvironment()
454
+ // 微信内:直接走浏览器式文件选择,不弹出拍照/上传菜单
455
+ if (env.isWechat || env.isMiniprogram) {
456
+ openBrowserFilePicker()
457
+ return
458
+ }
459
+ if (props.attr?.uploadImage) {
460
+ showActionSheet.value = true
461
+ }
462
+ else {
463
+ triggerCamera()
464
+ }
578
465
  }
579
466
 
580
467
  function handleActionSelect(option: any) {
581
- if (option.key === 'document') {
582
- triggerDocumentPicker()
583
- showActionSheet.value = false
584
- return
585
- }
586
468
  showActionSheet.value = false
587
469
  if (option.key === 'camera') {
588
470
  triggerCamera()
589
471
  }
590
- else if (option.key === 'album') {
472
+ else if (option.key === 'file') {
473
+ // 手机文件上传
591
474
  mobileUtil.execute({
592
475
  funcName: 'photoAlbum',
593
476
  param: {},
@@ -598,7 +481,8 @@ function handleActionSelect(option: any) {
598
481
  })
599
482
  }
600
483
  else {
601
- openImageFilePicker()
484
+ // 手机文件选择失败,则走浏览器式文件选择
485
+ openBrowserFilePicker()
602
486
  }
603
487
  },
604
488
  })
@@ -623,27 +507,27 @@ function getUploadStatus() {
623
507
  defineExpose({
624
508
  getUploadStatus,
625
509
  })
510
+
511
+ /**
512
+ * 组件卸载时清理:关闭 ActionSheet,清空图片列表以释放闭包引用
513
+ */
514
+ onBeforeUnmount(() => {
515
+ showActionSheet.value = false
516
+ // 清空图片列表,释放闭包中对 props.attr 等对象的引用,防止内存泄漏
517
+ imageList.value = []
518
+ })
626
519
  </script>
627
520
 
628
521
  <template>
629
522
  <div class="uploader-container">
630
- <!-- 图片选择(拍摄 / 相册) -->
523
+ <!-- 浏览器 / 微信 H5:统一隐藏文件选择(多选图片) -->
631
524
  <input
632
- ref="imageInputRef"
525
+ ref="fileInputRef"
633
526
  type="file"
634
527
  multiple
635
528
  accept="image/*"
636
- class="hidden-file-input"
637
- @change="(e:any) => handleBrowserImageFiles(e.target.files)"
638
- >
639
- <!-- 通用文件选择(PDF / Word 等) -->
640
- <input
641
- ref="documentInputRef"
642
- type="file"
643
- multiple
644
- :accept="documentAccept"
645
- class="hidden-file-input"
646
- @change="(e:any) => handleBrowserDocumentFiles(e.target.files)"
529
+ style="display:none"
530
+ @change="(e:any) => handleBrowserFiles(e.target.files)"
647
531
  >
648
532
  <van-uploader
649
533
  ref="uploaderRef"
@@ -655,16 +539,6 @@ defineExpose({
655
539
  :preview-image="true"
656
540
  :before-delete="!props.readonly && props.attr?.addOrEdit !== 'readonly' && props.authority === 'admin' ? deleteFileFunction : undefined"
657
541
  >
658
- <template #preview-cover="{ index }">
659
- <div
660
- v-if="imageList[index] && !isFileItemImage(imageList[index])"
661
- class="file-type-tile"
662
- :style="{ background: getItemFileMeta(imageList[index]).color }"
663
- >
664
- <span class="file-type-tile__ext">{{ getItemFileMeta(imageList[index]).ext }}</span>
665
- <span class="file-type-tile__label">{{ getItemFileMeta(imageList[index]).label }}</span>
666
- </div>
667
- </template>
668
542
  <template #default>
669
543
  <div
670
544
  v-if="!props.readonly && props.mode !== '预览' && (imageList.length < props.attr?.acceptCount && props.attr?.addOrEdit !== 'readonly')"
@@ -698,19 +572,6 @@ defineExpose({
698
572
  // 该属性会影响表单布局
699
573
  // gap: 16px;
700
574
  }
701
-
702
- /* WebView 内 display:none 会导致 file input 无法被程序化触发,改用视觉隐藏 */
703
- .hidden-file-input {
704
- position: fixed;
705
- top: 0;
706
- left: 0;
707
- width: 1px;
708
- height: 1px;
709
- opacity: 0;
710
- overflow: hidden;
711
- pointer-events: none;
712
- z-index: -1;
713
- }
714
575
  /* 让自定义上传按钮可点击:关闭 Vant 覆盖在上方的透明 input 的事件捕获 */
715
576
  :deep(.custom-trigger-uploader .van-uploader__input) {
716
577
  pointer-events: none;
@@ -748,38 +609,4 @@ defineExpose({
748
609
  margin-top: 0;
749
610
  }
750
611
  }
751
-
752
- .file-type-tile {
753
- position: absolute;
754
- inset: 0;
755
- display: flex;
756
- flex-direction: column;
757
- align-items: center;
758
- justify-content: center;
759
- color: #fff;
760
- border-radius: 8px;
761
- padding: 4px;
762
- box-sizing: border-box;
763
- text-align: center;
764
- pointer-events: none;
765
-
766
- &__ext {
767
- font-size: 16px;
768
- font-weight: 700;
769
- line-height: 1.2;
770
- }
771
-
772
- &__label {
773
- font-size: 10px;
774
- margin-top: 2px;
775
- opacity: 0.95;
776
- }
777
- }
778
-
779
- :deep(.custom-trigger-uploader) {
780
- .van-uploader__preview {
781
- position: relative;
782
- overflow: hidden;
783
- }
784
- }
785
612
  </style>
@@ -4,7 +4,7 @@ import {
4
4
  Button as vanButton,
5
5
  Uploader as vanUploader,
6
6
  } from 'vant'
7
- import { ref, watch } from 'vue'
7
+ import { onBeforeUnmount, ref, watch } from 'vue'
8
8
 
9
9
  const props = defineProps({
10
10
  imageList: Array<any>,
@@ -17,7 +17,7 @@ const emit = defineEmits(['updateFileList'])
17
17
  const imageList = ref(props.imageList)
18
18
 
19
19
  // 监听 props.imageList 的变化
20
- watch(() => props.imageList, (newVal) => {
20
+ const stopWatchImageList = watch(() => props.imageList, (newVal) => {
21
21
  if (newVal?.length > 0) {
22
22
  const existingIds = imageList.value ? imageList.value.map(item => item.id) : []
23
23
  const newItems = newVal.filter(item => !item.id || !existingIds.includes(item.id)).map((item) => {
@@ -33,6 +33,10 @@ watch(() => props.imageList, (newVal) => {
33
33
  }
34
34
  }
35
35
  }, { immediate: true, deep: true })
36
+
37
+ onBeforeUnmount(() => {
38
+ stopWatchImageList()
39
+ })
36
40
  // 上传图片
37
41
  function fileUpload(file: any): void {
38
42
  const formData = new FormData()
@@ -7,7 +7,7 @@ import {
7
7
  Popup as VanPopup,
8
8
  TimePicker as VanTimePicker,
9
9
  } from 'vant'
10
- import { computed, ref } from 'vue'
10
+ import { computed, onBeforeUnmount, ref } from 'vue'
11
11
 
12
12
  interface Props {
13
13
  attr: any
@@ -258,6 +258,13 @@ function onDateTimePickerConfirm() {
258
258
  function onPickerCancel() {
259
259
  showDatePicker.value = false
260
260
  }
261
+
262
+ /**
263
+ * 组件卸载时清理:关闭弹窗
264
+ */
265
+ onBeforeUnmount(() => {
266
+ showDatePicker.value = false
267
+ })
261
268
  </script>
262
269
 
263
270
  <template>
@@ -300,7 +307,6 @@ function onPickerCancel() {
300
307
  <VanPopup
301
308
  v-model:show="showDatePicker"
302
309
  position="bottom"
303
- teleport="body"
304
310
  overlay-class="date-picker-overlay"
305
311
  >
306
312
  <VanPickerGroup
@@ -8,7 +8,7 @@ import {
8
8
  Popup as VanPopup,
9
9
  Search as VanSearch,
10
10
  } from 'vant'
11
- import { computed, ref, watch } from 'vue'
11
+ import { computed, onBeforeUnmount, ref, watch } from 'vue'
12
12
 
13
13
  const props = defineProps({
14
14
  columns: {
@@ -124,9 +124,13 @@ function showPopu(disabled) {
124
124
  watch(() => columnsData.value, (newVal, _oldVal) => {
125
125
  checkedAll.value = newVal.length && newVal.length === checkboxValue.value.length
126
126
  })
127
- watch(() => checkboxValue.value.length, (newVal, _oldVal) => {
127
+ const stopWatchCheckboxValue = watch(() => checkboxValue.value.length, (newVal, _oldVal) => {
128
128
  checkedAll.value = newVal && newVal === columnsData.value.length
129
129
  })
130
+
131
+ onBeforeUnmount(() => {
132
+ stopWatchCheckboxValue()
133
+ })
130
134
  const resultLabel = computed(() => {
131
135
  const res = props.columns.filter((item) => {
132
136
  return (resultValue.value as unknown as string[])?.includes(item[props.option.value])