af-mobile-client-vue3 1.6.41 → 1.6.43

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.
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.41",
4
+ "version": "1.6.43",
5
5
  "packageManager": "pnpm@10.13.1",
6
6
  "description": "Vue + Vite component lib",
7
7
  "engines": {
@@ -6,6 +6,7 @@ import { detectEnvironment } from '@af-mobile-client-vue3/utils/environment'
6
6
  import { buildInputAccept, getFileTypeMeta, getUploadFormType, isImageFileName } from '@af-mobile-client-vue3/utils/fileType'
7
7
  import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
8
8
  import {
9
+ hasNativeFilePicker,
9
10
  openNativeFilePicker,
10
11
  triggerBrowserFileInput,
11
12
  } from '@af-mobile-client-vue3/utils/nativeFilePicker'
@@ -49,6 +50,7 @@ const imageList = ref<Array<any>>(props.imageList ?? [])
49
50
  const parentData: any = inject('provideParent')
50
51
  const imageInputRef = ref<HTMLInputElement | undefined>()
51
52
  const documentInputRef = ref<HTMLInputElement | undefined>()
53
+ const showActionSheet = ref(false)
52
54
 
53
55
  const documentAccept = computed(() => buildInputAccept(props.attr?.accept))
54
56
 
@@ -56,9 +58,7 @@ function openImageFilePicker() {
56
58
  triggerBrowserFileInput(imageInputRef.value)
57
59
  }
58
60
 
59
- function openDocumentFilePicker() {
60
- triggerBrowserFileInput(documentInputRef.value)
61
- }
61
+ const useNativeDocumentPicker = computed(() => hasNativeFilePicker())
62
62
 
63
63
  function emitUpdatesAfterChange() {
64
64
  // 新增:对外抛出完整与ID列表
@@ -260,8 +260,16 @@ function handleNativeDocumentUpload(fileData: NativePickedFile) {
260
260
  })
261
261
  }
262
262
 
263
- function triggerDocumentPicker() {
264
- const startedNative = openNativeFilePicker({
263
+ function onDocumentInputChange(e: Event) {
264
+ const input = e.target as HTMLInputElement
265
+ handleBrowserDocumentFiles(input.files)
266
+ showActionSheet.value = false
267
+ input.value = ''
268
+ }
269
+
270
+ function onPickDocumentNative() {
271
+ showActionSheet.value = false
272
+ openNativeFilePicker({
265
273
  accept: documentAccept.value,
266
274
  multiple: true,
267
275
  onSuccess: files => files.forEach(handleNativeDocumentUpload),
@@ -270,11 +278,6 @@ function triggerDocumentPicker() {
270
278
  showFailToast(msg)
271
279
  },
272
280
  })
273
- if (startedNative)
274
- return
275
-
276
- // 与「相册中选择」一致:ActionSheet 点击后立即调起系统文件选择器
277
- openDocumentFilePicker()
278
281
  }
279
282
 
280
283
  // 触发拍照
@@ -302,6 +305,9 @@ function triggerCamera() {
302
305
  rePhotoCheck(result.data)
303
306
  }
304
307
  else if (result.status === 'error') {
308
+ if (result.message && result.message.startsWith('图片质量检测失败')) {
309
+ parentData?.onPhotoQualityFailed?.()
310
+ }
305
311
  const env = detectEnvironment()
306
312
  // App 环境下弹窗提示错误
307
313
  if (env.isApp) {
@@ -345,6 +351,7 @@ function rePhotoCheck(photoData: any) {
345
351
  if (result && result.status === 'success') {
346
352
  const checkRePhotoResult = result.data
347
353
  if (checkRePhotoResult.is_rephoto === true || checkRePhotoResult.is_rephoto === 'true') {
354
+ parentData?.onPhotoRephotodetected?.()
348
355
  console.warn(`检测到当前图片属于翻拍照片\n请上传合法图片!`)
349
356
  showFailToast({
350
357
  message: `当前图片属于翻拍照片\n请上传合法图片!`,
@@ -558,47 +565,35 @@ function deleteFileFunction(file: any) {
558
565
  return false // 阻止van-uploader二次处理
559
566
  }
560
567
 
561
- const showActionSheet = ref(false)
562
568
  const uploaderRef = ref()
563
569
 
564
- const actionOptions = [
565
- { name: '拍摄', key: 'camera' },
566
- { name: '相册中选择', key: 'album' },
567
- { name: '文件上传', key: 'document' },
568
- ]
569
-
570
570
  function handleUploadAreaClick() {
571
571
  if (props.readonly)
572
572
  return
573
573
  showActionSheet.value = true
574
574
  }
575
575
 
576
- function handleActionSelect(option: any) {
577
- if (option.key === 'document') {
578
- triggerDocumentPicker()
579
- showActionSheet.value = false
580
- return
581
- }
576
+ function onPickCamera() {
582
577
  showActionSheet.value = false
583
- if (option.key === 'camera') {
584
- triggerCamera()
585
- }
586
- else if (option.key === 'album') {
587
- mobileUtil.execute({
588
- funcName: 'photoAlbum',
589
- param: {},
590
- callbackFunc: (result: any) => {
591
- if (result.status === 'success') {
592
- result.data?.photos.forEach((photo: any) => {
593
- rePhotoCheck(photo)
594
- })
595
- }
596
- else {
597
- openImageFilePicker()
598
- }
599
- },
600
- })
601
- }
578
+ triggerCamera()
579
+ }
580
+
581
+ function onPickAlbum() {
582
+ showActionSheet.value = false
583
+ mobileUtil.execute({
584
+ funcName: 'photoAlbum',
585
+ param: {},
586
+ callbackFunc: (result: any) => {
587
+ if (result.status === 'success') {
588
+ result.data?.photos.forEach((photo: any) => {
589
+ rePhotoCheck(photo)
590
+ })
591
+ }
592
+ else {
593
+ openImageFilePicker()
594
+ }
595
+ },
596
+ })
602
597
  }
603
598
 
604
599
  function getUploadStatus() {
@@ -632,15 +627,6 @@ defineExpose({
632
627
  class="hidden-file-input"
633
628
  @change="(e:any) => handleBrowserImageFiles(e.target.files)"
634
629
  >
635
- <!-- 通用文件选择(PDF / Word 等) -->
636
- <input
637
- ref="documentInputRef"
638
- type="file"
639
- multiple
640
- :accept="documentAccept"
641
- class="hidden-file-input"
642
- @change="(e:any) => handleBrowserDocumentFiles(e.target.files)"
643
- >
644
630
  <van-uploader
645
631
  ref="uploaderRef"
646
632
  v-model="imageList"
@@ -676,11 +662,37 @@ defineExpose({
676
662
  </van-uploader>
677
663
  <ActionSheet
678
664
  v-model:show="showActionSheet"
679
- :actions="actionOptions"
680
665
  teleport="body"
681
666
  cancel-text="取消"
682
- @select="handleActionSelect"
683
- />
667
+ >
668
+ <div class="upload-source-sheet">
669
+ <button type="button" class="upload-source-sheet__item" @click="onPickCamera">
670
+ 拍摄
671
+ </button>
672
+ <button type="button" class="upload-source-sheet__item" @click="onPickAlbum">
673
+ 相册中选择
674
+ </button>
675
+ <button
676
+ v-if="useNativeDocumentPicker"
677
+ type="button"
678
+ class="upload-source-sheet__item"
679
+ @click="onPickDocumentNative"
680
+ >
681
+ 文件上传
682
+ </button>
683
+ <label v-else class="upload-source-sheet__item upload-source-sheet__item--file">
684
+ 文件上传
685
+ <input
686
+ ref="documentInputRef"
687
+ type="file"
688
+ multiple
689
+ :accept="documentAccept"
690
+ class="upload-source-sheet__file-input"
691
+ @change="onDocumentInputChange"
692
+ >
693
+ </label>
694
+ </div>
695
+ </ActionSheet>
684
696
  </div>
685
697
  </template>
686
698
 
@@ -745,6 +757,42 @@ defineExpose({
745
757
  }
746
758
  }
747
759
 
760
+ .upload-source-sheet {
761
+ &__item {
762
+ display: block;
763
+ width: 100%;
764
+ padding: 14px 16px;
765
+ font-size: 16px;
766
+ line-height: 22px;
767
+ text-align: center;
768
+ background: #fff;
769
+ border: none;
770
+ color: #323233;
771
+ cursor: pointer;
772
+ position: relative;
773
+ box-sizing: border-box;
774
+
775
+ & + & {
776
+ border-top: 1px solid #ebedf0;
777
+ }
778
+ }
779
+
780
+ &__item--file {
781
+ margin: 0;
782
+ }
783
+
784
+ /* 用户手指直接点 input,WebView 才允许调起系统文件选择器(程序化 click 在 APK 内会被拦截) */
785
+ &__file-input {
786
+ position: absolute;
787
+ inset: 0;
788
+ width: 100%;
789
+ height: 100%;
790
+ opacity: 0;
791
+ cursor: pointer;
792
+ font-size: 0;
793
+ }
794
+ }
795
+
748
796
  .file-type-tile {
749
797
  position: absolute;
750
798
  inset: 0;
@@ -1,89 +1,98 @@
1
- import { getNativeBridgeWindow, hasFlutterBridge } from './environment'
2
- import { mobileUtil } from './mobileUtil'
3
-
4
- /** 原生文件选择桥接名(按优先级) */
5
- const NATIVE_FILE_PICKER_BRIDGES = ['pickFile', 'chooseFile', 'filePicker', 'selectFile', 'openFilePicker']
6
-
7
- export interface NativePickedFile {
8
- name?: string
9
- size?: number | string
10
- filePath?: string
11
- path?: string
12
- content?: string
13
- }
14
-
15
- export function getNativeFilePickerBridge(): string | null {
16
- for (const name of NATIVE_FILE_PICKER_BRIDGES) {
17
- if (hasFlutterBridge(name))
18
- return name
19
- }
20
- return null
21
- }
22
-
23
- export function hasNativeFilePicker(): boolean {
24
- return !!getNativeFilePickerBridge()
25
- }
26
-
27
- function normalizePickedFiles(data: any): NativePickedFile[] {
28
- if (!data)
29
- return []
30
- if (Array.isArray(data.files))
31
- return data.files
32
- if (Array.isArray(data))
33
- return data
34
- return [data]
35
- }
36
-
37
- /**
38
- * App 内打开系统文件选择器(需 Flutter 侧实现对应桥接)。
39
- * 返回 true 表示已发起原生调用。
40
- */
41
- export function openNativeFilePicker(
42
- options: {
43
- accept?: string
44
- multiple?: boolean
45
- onSuccess: (files: NativePickedFile[]) => void
46
- onError?: (message?: string) => void
47
- },
48
- ): boolean {
49
- const bridgeName = getNativeFilePickerBridge()
50
- if (!bridgeName)
51
- return false
52
-
53
- mobileUtil.execute({
54
- funcName: bridgeName,
55
- param: {
56
- accept: options.accept || '*/*',
57
- multiple: options.multiple ?? true,
58
- },
59
- callbackFunc: (result: any) => {
60
- if (result?.status === 'success') {
61
- const files = normalizePickedFiles(result.data)
62
- if (files.length > 0)
63
- options.onSuccess(files)
64
- else
65
- options.onError?.('未选择文件')
66
- return
67
- }
68
- if (result?.data?.status === 'cancelled')
69
- return
70
- options.onError?.(result?.message || result?.msg || '文件选择失败')
71
- },
72
- })
73
- return true
74
- }
75
-
76
- /** 同步触发浏览器 file input(须在用户点击事件的同一调用栈内) */
77
- export function triggerBrowserFileInput(input: HTMLInputElement | undefined | null): boolean {
78
- if (!input)
79
- return false
80
- input.value = ''
81
- input.click()
82
- return true
83
- }
84
-
85
- /** 调试:列出当前 window 上已注入的桥接名 */
86
- export function listNativeBridges(): string[] {
87
- const win = getNativeBridgeWindow()
88
- return Object.keys(win).filter(key => typeof win[key]?.postMessage === 'function')
89
- }
1
+ import { getNativeBridgeWindow, hasFlutterBridge } from './environment'
2
+ import { mobileUtil } from './mobileUtil'
3
+
4
+ /** 原生文件选择桥接名(按优先级) */
5
+ const NATIVE_FILE_PICKER_BRIDGES = [
6
+ 'pickFile',
7
+ 'chooseFile',
8
+ 'filePicker',
9
+ 'selectFile',
10
+ 'openFilePicker',
11
+ 'fileAlbum',
12
+ 'documentPicker',
13
+ 'chooseDocument',
14
+ ]
15
+
16
+ export interface NativePickedFile {
17
+ name?: string
18
+ size?: number | string
19
+ filePath?: string
20
+ path?: string
21
+ content?: string
22
+ }
23
+
24
+ export function getNativeFilePickerBridge(): string | null {
25
+ for (const name of NATIVE_FILE_PICKER_BRIDGES) {
26
+ if (hasFlutterBridge(name))
27
+ return name
28
+ }
29
+ return null
30
+ }
31
+
32
+ export function hasNativeFilePicker(): boolean {
33
+ return !!getNativeFilePickerBridge()
34
+ }
35
+
36
+ function normalizePickedFiles(data: any): NativePickedFile[] {
37
+ if (!data)
38
+ return []
39
+ if (Array.isArray(data.files))
40
+ return data.files
41
+ if (Array.isArray(data))
42
+ return data
43
+ return [data]
44
+ }
45
+
46
+ /**
47
+ * App 内打开系统文件选择器(需 Flutter 侧实现对应桥接)。
48
+ * 返回 true 表示已发起原生调用。
49
+ */
50
+ export function openNativeFilePicker(
51
+ options: {
52
+ accept?: string
53
+ multiple?: boolean
54
+ onSuccess: (files: NativePickedFile[]) => void
55
+ onError?: (message?: string) => void
56
+ },
57
+ ): boolean {
58
+ const bridgeName = getNativeFilePickerBridge()
59
+ if (!bridgeName)
60
+ return false
61
+
62
+ mobileUtil.execute({
63
+ funcName: bridgeName,
64
+ param: {
65
+ accept: options.accept || '*/*',
66
+ multiple: options.multiple ?? true,
67
+ },
68
+ callbackFunc: (result: any) => {
69
+ if (result?.status === 'success') {
70
+ const files = normalizePickedFiles(result.data)
71
+ if (files.length > 0)
72
+ options.onSuccess(files)
73
+ else
74
+ options.onError?.('未选择文件')
75
+ return
76
+ }
77
+ if (result?.data?.status === 'cancelled')
78
+ return
79
+ options.onError?.(result?.message || result?.msg || '文件选择失败')
80
+ },
81
+ })
82
+ return true
83
+ }
84
+
85
+ /** 同步触发浏览器 file input(须在用户点击事件的同一调用栈内) */
86
+ export function triggerBrowserFileInput(input: HTMLInputElement | undefined | null): boolean {
87
+ if (!input)
88
+ return false
89
+ input.value = ''
90
+ input.click()
91
+ return true
92
+ }
93
+
94
+ /** 调试:列出当前 window 上已注入的桥接名 */
95
+ export function listNativeBridges(): string[] {
96
+ const win = getNativeBridgeWindow()
97
+ return Object.keys(win).filter(key => typeof win[key]?.postMessage === 'function')
98
+ }
@@ -165,6 +165,21 @@ const errorCounts = reactive({
165
165
  photoSignature: 0,
166
166
  })
167
167
 
168
+ // 照片质量统计
169
+ const photoStats = reactive({
170
+ rephotoCount: 0, // 翻拍照片数
171
+ qualityFailedCount: 0, // 质量不合格照片数
172
+ })
173
+
174
+ function onPhotoQualityFailed() {
175
+ console.warn('质量不合格照片数++')
176
+ photoStats.qualityFailedCount++
177
+ }
178
+ function onPhotoRephotodetected() {
179
+ console.warn('翻拍照片数++')
180
+ photoStats.rephotoCount++
181
+ }
182
+
168
183
  // 计时器
169
184
  const timer = ref(null)
170
185
  const elapsedSeconds = ref(0)
@@ -810,6 +825,8 @@ function resetLocalState() {
810
825
  errorCounts.tabs = []
811
826
  errorCounts.hiddenDangers = 0
812
827
  errorCounts.photoSignature = 0
828
+ photoStats.rephotoCount = 0
829
+ photoStats.qualityFailedCount = 0
813
830
 
814
831
  // 重置导航
815
832
  actions.value = [{ text: '用户信息', value: 0, icon: '', errorCount: 0 }]
@@ -1368,6 +1385,7 @@ async function organizeSubmitData() {
1368
1385
  draftType: draftType.value,
1369
1386
  changeTableId: safecheckStore.getChangeTableId(),
1370
1387
  verificationCodeKey: verificationCodeKey.value,
1388
+ photoStats: { ...photoStats },
1371
1389
  }
1372
1390
  }
1373
1391
 
@@ -2702,6 +2720,8 @@ function clear() {
2702
2720
  }
2703
2721
 
2704
2722
  provide('provideParent', {
2723
+ onPhotoQualityFailed,
2724
+ onPhotoRephotodetected,
2705
2725
  // 后续需要什么就加什么
2706
2726
  commonId: computed({
2707
2727
  get() {
@@ -149,12 +149,52 @@ function triggerCamera() {
149
149
  callbackFunc: (result: any) => {
150
150
  // 检查返回状态是否成功
151
151
  if (result?.status !== 'success' || !result?.data) {
152
+ if (result?.message && result.message.startsWith('图片质量检测失败')) {
153
+ parentData?.onPhotoQualityFailed?.()
154
+ }
152
155
  showFailToast(result?.message || '拍照失败,请重试')
153
156
  return
154
157
  }
155
158
 
156
- // 拍照成功,将照片加入上传队列
157
- queueUpload(result.data)
159
+ // 拍照成功,进行翻拍检测后上传
160
+ rePhotoCheck(result.data)
161
+ },
162
+ })
163
+ }
164
+
165
+ /**
166
+ * 翻拍检测
167
+ */
168
+ function rePhotoCheck(photoData: any) {
169
+ mobileUtil.execute({
170
+ funcName: 'detectAntiCopy',
171
+ param: {
172
+ image_base64: photoData.content,
173
+ },
174
+ callbackFunc: (result: any) => {
175
+ try {
176
+ if (result && result.status === 'success') {
177
+ const checkRePhotoResult = result.data
178
+ if (checkRePhotoResult.is_rephoto === true || checkRePhotoResult.is_rephoto === 'true') {
179
+ parentData?.onPhotoRephotodetected?.()
180
+ console.warn(`检测到当前图片属于翻拍照片\n请上传合法图片!`)
181
+ showFailToast({
182
+ message: `当前图片属于翻拍照片\n请上传合法图片!`,
183
+ duration: 3000,
184
+ })
185
+ }
186
+ else {
187
+ queueUpload(photoData)
188
+ }
189
+ }
190
+ else {
191
+ console.error(`翻拍检测失败照片,${result.message}`)
192
+ showFailToast(`翻拍检测失败照片,${result.message}`)
193
+ }
194
+ }
195
+ catch (e) {
196
+ console.error('翻拍检测', JSON.stringify(e))
197
+ }
158
198
  },
159
199
  })
160
200
  }
@@ -186,6 +186,9 @@ function triggerCamera() {
186
186
  rePhotoCheck(result.data)
187
187
  }
188
188
  else if (result.status === 'error') {
189
+ if (result.message && result.message.startsWith('图片质量检测失败')) {
190
+ parentData?.onPhotoQualityFailed?.()
191
+ }
189
192
  showDialog({
190
193
  title: '拍照异常',
191
194
  message: result.message || '拍照失败,请重试',
@@ -239,6 +242,7 @@ function rePhotoCheck(photoData: any) {
239
242
  if (result && result.status === 'success') {
240
243
  const checkRePhotoResult = result.data
241
244
  if (checkRePhotoResult.is_rephoto === true || checkRePhotoResult.is_rephoto === 'true') {
245
+ parentData?.onPhotoRephotodetected?.()
242
246
  console.warn(`检测到当前图片属于翻拍照片\n请上传合法图片!`)
243
247
  showFailToast({
244
248
  message: `当前图片属于翻拍照片\n请上传合法图片!`,