af-mobile-client-vue3 1.6.39 → 1.6.40

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.39",
4
+ "version": "1.6.40",
5
5
  "packageManager": "pnpm@10.13.1",
6
6
  "description": "Vue + Vite component lib",
7
7
  "engines": {
@@ -1,9 +1,16 @@
1
1
  <script setup lang="ts">
2
+ import type { NativePickedFile } from '@af-mobile-client-vue3/utils/nativeFilePicker'
2
3
  // import cameraIcon from '@af-mobile-client-vue3/assets/img/component/camera.png'
3
4
  import { deleteFile, upload } from '@af-mobile-client-vue3/services/api/common'
4
5
  import { detectEnvironment } from '@af-mobile-client-vue3/utils/environment'
5
6
  import { buildInputAccept, getFileTypeMeta, getUploadFormType, isImageFileName } from '@af-mobile-client-vue3/utils/fileType'
6
- import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
7
+ import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
8
+ import {
9
+
10
+ openNativeFilePicker,
11
+ shouldUseDocumentPickerPanel,
12
+ triggerBrowserFileInput,
13
+ } from '@af-mobile-client-vue3/utils/nativeFilePicker'
7
14
  import { toDataUrl } from '@af-mobile-client-vue3/utils/resourceUrl'
8
15
  import { formatNow } from '@af-mobile-client-vue3/utils/timeUtil'
9
16
  import {
@@ -44,21 +51,23 @@ const imageList = ref<Array<any>>(props.imageList ?? [])
44
51
  const parentData: any = inject('provideParent')
45
52
  const imageInputRef = ref<HTMLInputElement | undefined>()
46
53
  const documentInputRef = ref<HTMLInputElement | undefined>()
54
+ const documentPanelInputRef = ref<HTMLInputElement | undefined>()
55
+ const showDocumentPickerPopup = ref(false)
47
56
 
48
57
  const documentAccept = computed(() => buildInputAccept(props.attr?.accept))
49
58
 
50
59
  function openImageFilePicker() {
51
- if (imageInputRef.value) {
52
- imageInputRef.value.value = ''
53
- imageInputRef.value.click()
54
- }
60
+ triggerBrowserFileInput(imageInputRef.value)
55
61
  }
56
62
 
57
63
  function openDocumentFilePicker() {
58
- if (documentInputRef.value) {
59
- documentInputRef.value.value = ''
60
- documentInputRef.value.click()
61
- }
64
+ triggerBrowserFileInput(documentInputRef.value)
65
+ }
66
+
67
+ function onDocumentPanelInputChange(e: Event) {
68
+ const input = e.target as HTMLInputElement
69
+ handleBrowserDocumentFiles(input.files)
70
+ showDocumentPickerPopup.value = false
62
71
  }
63
72
 
64
73
  function emitUpdatesAfterChange() {
@@ -215,6 +224,74 @@ function handleBrowserDocumentFiles(files: FileList | null) {
215
224
  }
216
225
  }
217
226
 
227
+ function handleNativeDocumentUpload(fileData: NativePickedFile) {
228
+ const filePath = fileData.filePath || fileData.path || ''
229
+ const fileName = fileData.name || filePath.split('/').pop() || 'file'
230
+ const tempFile = createTempUploadingItem(fileName, '', false)
231
+
232
+ const param = {
233
+ resUploadMode: props.uploadMode,
234
+ pathKey: 'Default',
235
+ formType: 'file' as const,
236
+ useType: props.attr?.useType || 'Default',
237
+ resUploadStock: '1',
238
+ filename: fileName,
239
+ filesize: fileData.size,
240
+ f_operator: 'server',
241
+ imgPath: filePath,
242
+ urlPath: `/api/${props.serviceName || import.meta.env.VITE_APP_SYSTEM_NAME}/resource/upload`,
243
+ commonId: parentData?.commonId?.value ?? '',
244
+ }
245
+
246
+ mobileUtil.execute({
247
+ funcName: 'uploadResource',
248
+ param,
249
+ callbackFunc: (result: any) => {
250
+ const index = imageList.value.findIndex(item => item.uid === tempFile.uid)
251
+ if (result.status === 'success') {
252
+ if (index !== -1) {
253
+ imageList.value[index].uid = result.data.id
254
+ imageList.value[index].id = result.data.id
255
+ imageList.value[index].f_downloadpath = result.data.f_downloadpath
256
+ imageList.value[index].photo_name = result.data.f_filename
257
+ imageList.value[index].isDocument = true
258
+ imageList.value[index].isImage = true
259
+ imageList.value[index].url = DOC_TILE_PLACEHOLDER
260
+ delete imageList.value[index].message
261
+ imageList.value[index].status = 'done'
262
+ }
263
+ }
264
+ else if (index !== -1) {
265
+ imageList.value[index].status = 'failed'
266
+ imageList.value[index].message = '上传失败'
267
+ }
268
+ emitUpdatesAfterChange()
269
+ },
270
+ })
271
+ }
272
+
273
+ function triggerDocumentPicker() {
274
+ const startedNative = openNativeFilePicker({
275
+ accept: documentAccept.value,
276
+ multiple: true,
277
+ onSuccess: files => files.forEach(handleNativeDocumentUpload),
278
+ onError: (msg) => {
279
+ if (msg)
280
+ showFailToast(msg)
281
+ },
282
+ })
283
+ if (startedNative)
284
+ return
285
+
286
+ // App WebView 内 hidden input 的 programmatic click 无效,改用用户直接点击的弹层
287
+ if (shouldUseDocumentPickerPanel()) {
288
+ showDocumentPickerPopup.value = true
289
+ return
290
+ }
291
+
292
+ openDocumentFilePicker()
293
+ }
294
+
218
295
  // 触发拍照
219
296
  function triggerCamera() {
220
297
  mobileUtil.execute({
@@ -512,6 +589,11 @@ function handleUploadAreaClick() {
512
589
  }
513
590
 
514
591
  function handleActionSelect(option: any) {
592
+ if (option.key === 'document') {
593
+ triggerDocumentPicker()
594
+ showActionSheet.value = false
595
+ return
596
+ }
515
597
  showActionSheet.value = false
516
598
  if (option.key === 'camera') {
517
599
  triggerCamera()
@@ -532,9 +614,6 @@ function handleActionSelect(option: any) {
532
614
  },
533
615
  })
534
616
  }
535
- else if (option.key === 'document') {
536
- openDocumentFilePicker()
537
- }
538
617
  }
539
618
 
540
619
  function getUploadStatus() {
@@ -617,6 +696,35 @@ defineExpose({
617
696
  cancel-text="取消"
618
697
  @select="handleActionSelect"
619
698
  />
699
+
700
+ <!-- App 内文件选择:用户直接点击 label 触发系统文件管理器 -->
701
+ <van-popup
702
+ v-model:show="showDocumentPickerPopup"
703
+ position="bottom"
704
+ round
705
+ teleport="body"
706
+ safe-area-inset-bottom
707
+ >
708
+ <div class="document-picker-panel">
709
+ <div class="document-picker-title">
710
+ 选择文件
711
+ </div>
712
+ <p class="document-picker-tip">
713
+ 支持 PDF、Word、Excel 等格式
714
+ </p>
715
+ <label class="document-picker-btn">
716
+ 点击选择文件
717
+ <input
718
+ ref="documentPanelInputRef"
719
+ type="file"
720
+ multiple
721
+ :accept="documentAccept"
722
+ class="document-picker-input"
723
+ @change="onDocumentPanelInputChange"
724
+ >
725
+ </label>
726
+ </div>
727
+ </van-popup>
620
728
  </div>
621
729
  </template>
622
730
 
@@ -701,4 +809,44 @@ defineExpose({
701
809
  overflow: hidden;
702
810
  }
703
811
  }
812
+
813
+ .document-picker-panel {
814
+ padding: 20px 16px 28px;
815
+ text-align: center;
816
+ }
817
+
818
+ .document-picker-title {
819
+ font-size: 16px;
820
+ font-weight: 600;
821
+ color: #323233;
822
+ margin-bottom: 8px;
823
+ }
824
+
825
+ .document-picker-tip {
826
+ font-size: 13px;
827
+ color: #969799;
828
+ margin: 0 0 20px;
829
+ }
830
+
831
+ .document-picker-btn {
832
+ display: block;
833
+ position: relative;
834
+ width: 100%;
835
+ padding: 12px 0;
836
+ font-size: 15px;
837
+ color: #fff;
838
+ background: #1989fa;
839
+ border-radius: 8px;
840
+ cursor: pointer;
841
+ overflow: hidden;
842
+ }
843
+
844
+ .document-picker-input {
845
+ position: absolute;
846
+ inset: 0;
847
+ width: 100%;
848
+ height: 100%;
849
+ opacity: 0;
850
+ cursor: pointer;
851
+ }
704
852
  </style>
@@ -1,172 +1,172 @@
1
- <script setup lang="ts">
2
- import type { SignatureComponentExpose, SignatureComponentProps } from './signature'
3
- import XSignature from '@af-mobile-client-vue3/components/data/XSignature/index.vue'
4
- import { upload } from '@af-mobile-client-vue3/services/api/common'
5
- import { isNativeApp } from '@af-mobile-client-vue3/utils/environment'
6
- import { showFailToast, Field as VanField } from 'vant'
7
- import { onMounted, ref, watch } from 'vue'
8
- import SignatureComponent from './SignatureComponent.vue'
9
-
10
- const props = withDefaults(defineProps<SignatureComponentProps & {
11
- serviceName?: string
12
- }>(), {
13
- label: '用户签字',
14
- required: false,
15
- disabled: false,
16
- uploadMode: 'server',
17
- imageList: null,
18
- formReadonly: false,
19
- isAsyncUpload: false,
20
- serviceName: undefined,
21
- })
22
-
23
- const emit = defineEmits<{
24
- signatureComplete: [data: any]
25
- }>()
26
-
27
- // App / Flutter 壳走原生签字板;浏览器、微信等走 H5 签名
28
- const isApp = ref(isNativeApp())
29
- onMounted(() => {
30
- // 部分 WebView 桥接注入略晚于首屏,挂载后再判一次
31
- isApp.value = isNativeApp()
32
- })
33
-
34
- const nativeSignatureRef = ref<InstanceType<typeof SignatureComponent>>()
35
- const webSignatureImage = ref('')
36
- const webSignatureData = ref<any>(null)
37
- const webUploading = ref(false)
38
-
39
- const serviceName = props.serviceName || import.meta.env.VITE_APP_SYSTEM_NAME
40
-
41
- watch(
42
- () => props.imageList,
43
- (list) => {
44
- if (isApp.value || !Array.isArray(list) || !list[0]?.url)
45
- return
46
- webSignatureImage.value = list[0].url
47
- },
48
- { immediate: true, deep: true },
49
- )
50
-
51
- async function uploadWebSignature(base64Image: string) {
52
- const blob = await (await fetch(base64Image)).blob()
53
- const filename = `signature_${Date.now()}.png`
54
- const file = new File([blob], filename, { type: blob.type || 'image/png' })
55
- const formData = new FormData()
56
- formData.append('avatar', file)
57
- formData.append('resUploadMode', props.uploadMode)
58
- formData.append('pathKey', 'Default')
59
- formData.append('formType', 'image')
60
- formData.append('useType', 'Default')
61
- formData.append('resUploadStock', '1')
62
- formData.append('filename', filename)
63
- formData.append('filesize', (file.size / 1024 / 1024).toFixed(4))
64
- formData.append('f_operator', 'server')
65
-
66
- return upload(formData, serviceName, { 'Content-Type': 'multipart/form-data' })
67
- }
68
-
69
- function resetWebSignature() {
70
- webSignatureImage.value = ''
71
- webSignatureData.value = null
72
- }
73
-
74
- async function handleWebSignatureSave(base64Image: string) {
75
- if (props.disabled || props.formReadonly || webUploading.value)
76
- return
77
-
78
- webUploading.value = true
79
- try {
80
- const res: any = await uploadWebSignature(base64Image)
81
- if (!res?.data?.id) {
82
- showFailToast('签名上传失败')
83
- resetWebSignature()
84
- return
85
- }
86
- webSignatureData.value = res.data
87
- emit('signatureComplete', { status: 'success', data: res.data })
88
- }
89
- catch (error) {
90
- console.error('签名上传失败:', error)
91
- showFailToast('签名上传失败')
92
- resetWebSignature()
93
- }
94
- finally {
95
- webUploading.value = false
96
- }
97
- }
98
-
99
- function handleWebSignatureClear() {
100
- resetWebSignature()
101
- emit('signatureComplete', { base64: '', status: 'cleared' })
102
- }
103
-
104
- defineExpose<SignatureComponentExpose>({
105
- clearSignature() {
106
- if (isApp.value)
107
- nativeSignatureRef.value?.clearSignature()
108
- else
109
- handleWebSignatureClear()
110
- },
111
- hasSignature() {
112
- if (isApp.value)
113
- return nativeSignatureRef.value?.hasSignature() ?? false
114
- return !!webSignatureData.value?.id || !!webSignatureImage.value
115
- },
116
- getSignatureData() {
117
- if (isApp.value)
118
- return nativeSignatureRef.value?.getSignatureData() ?? ''
119
- return webSignatureImage.value.replace(/^data:image\/\w+;base64,/, '')
120
- },
121
- previewSignature() {
122
- nativeSignatureRef.value?.previewSignature()
123
- },
124
- getSignatureList() {
125
- if (isApp.value)
126
- return nativeSignatureRef.value?.getSignatureList() ?? []
127
- if (!webSignatureData.value)
128
- return []
129
- return [{
130
- photo_name: webSignatureData.value.f_filename,
131
- id: webSignatureData.value.id,
132
- f_downloadpath: webSignatureData.value.f_downloadpath,
133
- }]
134
- },
135
- })
136
- </script>
137
-
138
- <template>
139
- <!-- App:原生签字板 -->
140
- <SignatureComponent
141
- v-if="isApp"
142
- ref="nativeSignatureRef"
143
- :label="label"
144
- :required="required"
145
- :disabled="disabled"
146
- :upload-mode="uploadMode"
147
- :image-list="imageList"
148
- :form-readonly="formReadonly"
149
- :is-async-upload="isAsyncUpload"
150
- @signature-complete="emit('signatureComplete', $event)"
151
- />
152
- <!-- 非 App:H5 在线签名(浏览器、微信等) -->
153
- <VanField
154
- v-else
155
- center
156
- name="signature"
157
- :label="label"
158
- :required="required"
159
- >
160
- <template #input>
161
- <XSignature
162
- v-model="webSignatureImage"
163
- button-text="点击签字"
164
- :show-button-after-signed="true"
165
- :show-sign-button="!formReadonly"
166
- :show-preview="true"
167
- @save="handleWebSignatureSave"
168
- @clear="handleWebSignatureClear"
169
- />
170
- </template>
171
- </VanField>
172
- </template>
1
+ <script setup lang="ts">
2
+ import type { SignatureComponentExpose, SignatureComponentProps } from './signature'
3
+ import XSignature from '@af-mobile-client-vue3/components/data/XSignature/index.vue'
4
+ import { upload } from '@af-mobile-client-vue3/services/api/common'
5
+ import { isNativeApp } from '@af-mobile-client-vue3/utils/environment'
6
+ import { showFailToast, Field as VanField } from 'vant'
7
+ import { onMounted, ref, watch } from 'vue'
8
+ import SignatureComponent from './SignatureComponent.vue'
9
+
10
+ const props = withDefaults(defineProps<SignatureComponentProps & {
11
+ serviceName?: string
12
+ }>(), {
13
+ label: '用户签字',
14
+ required: false,
15
+ disabled: false,
16
+ uploadMode: 'server',
17
+ imageList: null,
18
+ formReadonly: false,
19
+ isAsyncUpload: false,
20
+ serviceName: undefined,
21
+ })
22
+
23
+ const emit = defineEmits<{
24
+ signatureComplete: [data: any]
25
+ }>()
26
+
27
+ // App / Flutter 壳走原生签字板;浏览器、微信等走 H5 签名
28
+ const isApp = ref(isNativeApp())
29
+ onMounted(() => {
30
+ // 部分 WebView 桥接注入略晚于首屏,挂载后再判一次
31
+ isApp.value = isNativeApp()
32
+ })
33
+
34
+ const nativeSignatureRef = ref<InstanceType<typeof SignatureComponent>>()
35
+ const webSignatureImage = ref('')
36
+ const webSignatureData = ref<any>(null)
37
+ const webUploading = ref(false)
38
+
39
+ const serviceName = props.serviceName || import.meta.env.VITE_APP_SYSTEM_NAME
40
+
41
+ watch(
42
+ () => props.imageList,
43
+ (list) => {
44
+ if (isApp.value || !Array.isArray(list) || !list[0]?.url)
45
+ return
46
+ webSignatureImage.value = list[0].url
47
+ },
48
+ { immediate: true, deep: true },
49
+ )
50
+
51
+ async function uploadWebSignature(base64Image: string) {
52
+ const blob = await (await fetch(base64Image)).blob()
53
+ const filename = `signature_${Date.now()}.png`
54
+ const file = new File([blob], filename, { type: blob.type || 'image/png' })
55
+ const formData = new FormData()
56
+ formData.append('avatar', file)
57
+ formData.append('resUploadMode', props.uploadMode)
58
+ formData.append('pathKey', 'Default')
59
+ formData.append('formType', 'image')
60
+ formData.append('useType', 'Default')
61
+ formData.append('resUploadStock', '1')
62
+ formData.append('filename', filename)
63
+ formData.append('filesize', (file.size / 1024 / 1024).toFixed(4))
64
+ formData.append('f_operator', 'server')
65
+
66
+ return upload(formData, serviceName, { 'Content-Type': 'multipart/form-data' })
67
+ }
68
+
69
+ function resetWebSignature() {
70
+ webSignatureImage.value = ''
71
+ webSignatureData.value = null
72
+ }
73
+
74
+ async function handleWebSignatureSave(base64Image: string) {
75
+ if (props.disabled || props.formReadonly || webUploading.value)
76
+ return
77
+
78
+ webUploading.value = true
79
+ try {
80
+ const res: any = await uploadWebSignature(base64Image)
81
+ if (!res?.data?.id) {
82
+ showFailToast('签名上传失败')
83
+ resetWebSignature()
84
+ return
85
+ }
86
+ webSignatureData.value = res.data
87
+ emit('signatureComplete', { status: 'success', data: res.data })
88
+ }
89
+ catch (error) {
90
+ console.error('签名上传失败:', error)
91
+ showFailToast('签名上传失败')
92
+ resetWebSignature()
93
+ }
94
+ finally {
95
+ webUploading.value = false
96
+ }
97
+ }
98
+
99
+ function handleWebSignatureClear() {
100
+ resetWebSignature()
101
+ emit('signatureComplete', { base64: '', status: 'cleared' })
102
+ }
103
+
104
+ defineExpose<SignatureComponentExpose>({
105
+ clearSignature() {
106
+ if (isApp.value)
107
+ nativeSignatureRef.value?.clearSignature()
108
+ else
109
+ handleWebSignatureClear()
110
+ },
111
+ hasSignature() {
112
+ if (isApp.value)
113
+ return nativeSignatureRef.value?.hasSignature() ?? false
114
+ return !!webSignatureData.value?.id || !!webSignatureImage.value
115
+ },
116
+ getSignatureData() {
117
+ if (isApp.value)
118
+ return nativeSignatureRef.value?.getSignatureData() ?? ''
119
+ return webSignatureImage.value.replace(/^data:image\/\w+;base64,/, '')
120
+ },
121
+ previewSignature() {
122
+ nativeSignatureRef.value?.previewSignature()
123
+ },
124
+ getSignatureList() {
125
+ if (isApp.value)
126
+ return nativeSignatureRef.value?.getSignatureList() ?? []
127
+ if (!webSignatureData.value)
128
+ return []
129
+ return [{
130
+ photo_name: webSignatureData.value.f_filename,
131
+ id: webSignatureData.value.id,
132
+ f_downloadpath: webSignatureData.value.f_downloadpath,
133
+ }]
134
+ },
135
+ })
136
+ </script>
137
+
138
+ <template>
139
+ <!-- App:原生签字板 -->
140
+ <SignatureComponent
141
+ v-if="isApp"
142
+ ref="nativeSignatureRef"
143
+ :label="label"
144
+ :required="required"
145
+ :disabled="disabled"
146
+ :upload-mode="uploadMode"
147
+ :image-list="imageList"
148
+ :form-readonly="formReadonly"
149
+ :is-async-upload="isAsyncUpload"
150
+ @signature-complete="emit('signatureComplete', $event)"
151
+ />
152
+ <!-- 非 App:H5 在线签名(浏览器、微信等) -->
153
+ <VanField
154
+ v-else
155
+ center
156
+ name="signature"
157
+ :label="label"
158
+ :required="required"
159
+ >
160
+ <template #input>
161
+ <XSignature
162
+ v-model="webSignatureImage"
163
+ button-text="点击签字"
164
+ :show-button-after-signed="true"
165
+ :show-sign-button="!formReadonly"
166
+ :show-preview="true"
167
+ @save="handleWebSignatureSave"
168
+ @clear="handleWebSignatureClear"
169
+ />
170
+ </template>
171
+ </VanField>
172
+ </template>