af-mobile-client-vue3 1.4.68 → 1.4.70

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 (50) hide show
  1. package/__dummy__ +9 -9
  2. package/build/vite/optimize.ts +36 -36
  3. package/package.json +120 -121
  4. package/public/favicon.svg +4 -4
  5. package/scripts/verifyCommit.js +19 -19
  6. package/src/components/common/MateChat/components/MateChatContent.vue +274 -274
  7. package/src/components/common/MateChat/components/MateChatHeader.vue +337 -337
  8. package/src/components/common/MateChat/index.vue +444 -444
  9. package/src/components/common/MateChat/types.ts +247 -247
  10. package/src/components/common/otherCharge/ChargePrintSelectorAndRemarks.vue +137 -137
  11. package/src/components/common/otherCharge/CodePayment.vue +357 -357
  12. package/src/components/common/otherCharge/FileUploader.vue +602 -602
  13. package/src/components/common/otherCharge/GridFileUploader.vue +846 -846
  14. package/src/components/common/otherCharge/PaymentMethodSelector.vue +202 -202
  15. package/src/components/common/otherCharge/PaymentMethodSelectorCard.vue +45 -45
  16. package/src/components/common/otherCharge/ReceiptModal.vue +273 -273
  17. package/src/components/common/otherCharge/index.ts +43 -43
  18. package/src/components/core/ImageUploader/index.vue +9 -2
  19. package/src/components/data/OtherCharge/OtherChargeItemModal.vue +547 -547
  20. package/src/components/data/UserDetail/types.ts +1 -1
  21. package/src/components/data/XCellList/index.vue +1 -1
  22. package/src/components/data/XReportGrid/XAddReport/index.ts +1 -1
  23. package/src/components/data/XReportGrid/XReportDrawer/index.ts +1 -1
  24. package/src/components/data/XTag/index.vue +10 -10
  25. package/src/components/layout/TabBarLayout/index.vue +40 -40
  26. package/src/hooks/useCommon.ts +9 -9
  27. package/src/plugins/AppData.ts +38 -38
  28. package/src/router/invoiceRoutes.ts +33 -33
  29. package/src/services/api/common.ts +109 -109
  30. package/src/services/api/manage.ts +8 -8
  31. package/src/services/api/search.ts +16 -16
  32. package/src/services/restTools.ts +56 -56
  33. package/src/utils/authority-utils.ts +84 -84
  34. package/src/utils/crypto.ts +39 -39
  35. package/src/utils/queryFormDefaultRangePicker.ts +57 -57
  36. package/src/utils/runEvalFunction.ts +13 -13
  37. package/src/views/component/EvaluateRecordView/index.vue +40 -40
  38. package/src/views/component/MateChat/MateChatView.vue +10 -10
  39. package/src/views/component/XCellDetailView/index.vue +217 -217
  40. package/src/views/component/XCellListView/index.vue +138 -107
  41. package/src/views/component/XFormGroupView/index.vue +82 -78
  42. package/src/views/component/XFormView/index.vue +46 -41
  43. package/src/views/component/XReportFormIframeView/index.vue +47 -47
  44. package/src/views/component/XReportFormView/index.vue +13 -13
  45. package/src/views/component/XSignatureView/index.vue +50 -50
  46. package/src/views/component/notice.vue +46 -46
  47. package/src/views/component/topNav.vue +36 -36
  48. package/src/views/invoiceShow/index.vue +61 -61
  49. package/src/views/user/login/index.vue +22 -22
  50. package/pnpm-lock.yaml +0 -11070
@@ -1,602 +1,602 @@
1
- <script setup lang="ts">
2
- import CardHeader from '@af-mobile-client-vue3/components/data/CardContainer/CardHeader.vue'
3
- import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
4
- import {
5
- showFailToast,
6
- showImagePreview,
7
- } from 'vant'
8
- import { computed } from 'vue'
9
-
10
- export interface FileItem {
11
- uid: string
12
- name: string
13
- size: number
14
- type: string
15
- url?: string
16
- filePath?: string
17
- userType?: string
18
- result?: any // android上传图片返回的结果示例数据: {"f_file_size":0.0671,"f_form_type":"image","f_use_type":"Default","f_downloadpath":"/resource/af-revenue/images/15868b507cc0460a9913a6b94f1912d5.jpg","f_stock_id":0,"f_realpath":"/usr/local/tomcat/files/af-revenue/images/15868b507cc0460a9913a6b94f1912d5.jpg","f_type":"IMAGE","f_operator":"server","fusetype":"Default","f_filetype":"jpg","f_upload_mode":"server","id":"57900","f_filename":"15868b507cc0460a9913a6b94f1912d5.jpg"}
19
- status: 'success' | 'error' | 'uploading'
20
- }
21
-
22
- const props = defineProps({
23
- title: {
24
- type: String,
25
- default: '上传附件',
26
- },
27
- icon: {
28
- type: String,
29
- default: '',
30
- },
31
- fileList: {
32
- type: Array as () => FileItem[],
33
- default: () => [],
34
- },
35
- showFileList: {
36
- type: Boolean,
37
- default: true,
38
- },
39
- multiple: {
40
- type: Boolean,
41
- default: false,
42
- },
43
- accept: {
44
- type: String,
45
- default: '',
46
- },
47
- maxSize: {
48
- type: Number,
49
- default: 10 * 1024 * 1024, // 10MB
50
- },
51
- allowedTypes: {
52
- type: Array as () => string[],
53
- default: () => ['image/png', 'image/jpeg', 'application/pdf'],
54
- },
55
- useType: {
56
- type: String,
57
- default: 'Default',
58
- },
59
- })
60
-
61
- const emit = defineEmits(['update:fileList', 'fileAdded', 'fileRemoved'])
62
-
63
- // const fileInput = ref<HTMLInputElement | null>(null)
64
- const fileTypesText = computed(() => {
65
- return `单个文件不超过${formatFileSize(props.maxSize)}`
66
- })
67
-
68
- // 判断是否为开发环境
69
- const isDev = import.meta.env.MODE === 'development'
70
-
71
- // 模拟图片的base64数据
72
- const mockImageBase64 = 'iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABmJLR0QA/wD/AP+gvaeTAAABdUlEQVRoge3ZsU4CQRAG4H9BYQyhgoTCGF/Axlfw0FdQKysrKwvfwsJEn8JECxKthAQbe2I8QgyRGIgFhRazJOqR7N3ezc6GKXeamW+zNze3N0Dg36PqXrDdbpfudjcRKWezWU9rPbXxXZoG0loPAbycTqdfIwBvs9nsnrbQzWbzhYg+T8/vP6HMcQbAMIyRlBK9Xm+nlLLaYCETAExT1iQiAgAiEgBc5fJDiMh1URQHXdJa92wCVb5RKSWKLWIB4FqWZWITyBrIdrJsE1UDWa1WViWrG8jlThWvbwwUqOlBFm9dXKBaTbBarVar/CKuWK1RFQVqNaiiKE7nrxPVZdSYyGQyUYPBgOu1Nq7xAjUTMrGBiIgQEUJp5zqWZtcwVFmWOB6PSKdTLJdLJEniOJEaIjJst1uUZWlUC7n27GQyQZZlyOdzxHGM+XwOz/O45n9nPB5ju90iyzLc7/eo1WrYbDZwHMd1NIPBYGDUDXl/1EAgYMwPPB0oPT3eUE4AAAAASUVORK5CYII='
73
-
74
- function triggerFileInput() {
75
- console.log('点击事件')
76
-
77
- // 判断是否为开发环境
78
- if (isDev) {
79
- console.log('开发环境,使用模拟上传并调用真实接口')
80
-
81
- // 基本信息
82
- const filename = 'test.png'
83
- const fileSize = 0.0003 // MB
84
- const operator = '测试管理员'
85
-
86
- // 创建临时文件对象(将base64转换为文件)
87
- const byteString = atob(mockImageBase64)
88
- const ab = new ArrayBuffer(byteString.length)
89
- const ia = new Uint8Array(ab)
90
- for (let i = 0; i < byteString.length; i++) {
91
- ia[i] = byteString.charCodeAt(i)
92
- }
93
- const blob = new Blob([ab], { type: 'image/png' })
94
- const file = new File([blob], filename, { type: 'image/png' })
95
-
96
- // 创建FormData
97
- const formData = new FormData()
98
- formData.append('avatar', file) // 文件二进制数据
99
- formData.append('resUploadMode', 'server')
100
- formData.append('formType', 'file')
101
- formData.append('useType', props.useType)
102
- formData.append('resUploadStock', '1')
103
- formData.append('filename', filename)
104
- formData.append('filesize', fileSize.toString())
105
- formData.append('f_operator', operator)
106
-
107
- // 调用接口上传
108
- fetch('/api/af-revenue/resource/upload', {
109
- method: 'POST',
110
- body: formData,
111
- })
112
- .then(response => response.json())
113
- .then((result) => {
114
- console.log('上传成功:', result)
115
- // 创建临时文件项
116
- const newFile: FileItem = {
117
- uid: Date.now() + Math.random().toString(36).substr(2, 5),
118
- name: filename,
119
- size: file.size,
120
- type: 'image/png',
121
- status: 'success',
122
- url: `data:image/png;base64,${mockImageBase64}`,
123
- result: result.data.data,
124
- }
125
- updateFileList([...props.fileList, newFile])
126
- emit('fileAdded', newFile)
127
- })
128
- .catch((error) => {
129
- console.error('上传失败:', error)
130
- showFailToast(`上传文件失败: ${error.message || error.msg || '请稍后重试'}`)
131
- })
132
-
133
- return
134
- }
135
-
136
- // 非开发环境,使用原生功能
137
- mobileUtil.execute({
138
- funcName: 'takePicture',
139
- param: {},
140
- callbackFunc: (result: any) => {
141
- if (result.status === 'success') {
142
- uploadFile(result.data)
143
- }
144
- },
145
- })
146
- // fileInput.value?.click()
147
- }
148
- function uploadFile(file: any) {
149
- mobileUtil.execute({
150
- funcName: 'uploadResource',
151
- param: {
152
- resUploadMode: 'server',
153
- pathKey: props.useType,
154
- formType: 'image',
155
- useType: props.useType,
156
- resUploadStock: '1',
157
- filename: file?.name,
158
- filesize: file?.size,
159
- f_operator: 'server',
160
- imgPath: file?.filePath,
161
- urlPath: '/api/af-revenue/resource/upload',
162
- },
163
- callbackFunc: (result: any) => {
164
- if (result.status === 'success') {
165
- const newFile: FileItem = {
166
- uid: Date.now() + Math.random().toString(36).substr(2, 5),
167
- name: file.name,
168
- size: file.size,
169
- type: 'image/jpeg',
170
- status: 'success',
171
- url: `data:image/png;base64,${file.content}`,
172
- filePath: file?.filePath,
173
- result: result.data,
174
- }
175
- updateFileList([...props.fileList, newFile])
176
- emit('fileAdded', newFile)
177
- }
178
- else {
179
- showFailToast(`上传图片失败,${result.message}`)
180
- }
181
- },
182
- })
183
- }
184
- /* function handleFileChange(event: Event) {
185
- console.log('触发变更===')
186
- const input = event.target as HTMLInputElement
187
- if (!input.files?.length)
188
- return
189
-
190
- const files = Array.from(input.files)
191
-
192
- files.forEach((file) => {
193
- // 检查文件类型
194
- if (props.allowedTypes.length && !props.allowedTypes.includes(file.type)) {
195
- // 可以添加错误处理
196
- return
197
- }
198
-
199
- // 检查文件大小
200
- if (file.size > props.maxSize) {
201
- // 可以添加错误处理
202
- return
203
- }
204
- console.log('file==', file)
205
- const newFile: FileItem = {
206
- uid: Date.now() + Math.random().toString(36).substr(2, 5),
207
- name: file.name,
208
- size: file.size,
209
- type: file.type,
210
- status: 'success',
211
- }
212
- console.log('newFile==', newFile)
213
- // 处理图片预览
214
- if (file.type.startsWith('image/')) {
215
- const reader = new FileReader()
216
- reader.onload = (e) => {
217
- if (e.target?.result) {
218
- newFile.url = e.target.result as string
219
- updateFileList([...props.fileList, newFile])
220
- }
221
- }
222
- reader.readAsDataURL(file)
223
- }
224
- else {
225
- updateFileList([...props.fileList, newFile])
226
- }
227
-
228
- emit('fileAdded', { file, fileItem: newFile })
229
- })
230
-
231
- // 重置文件输入,允许选择相同文件
232
- input.value = ''
233
- } */
234
-
235
- function removeFile(file: FileItem) {
236
- const newFileList = props.fileList.filter(item => item.uid !== file.uid)
237
- updateFileList(newFileList)
238
- emit('fileRemoved', file)
239
- }
240
-
241
- function updateFileList(files: FileItem[]) {
242
- emit('update:fileList', files)
243
- }
244
-
245
- function getFileIcon(type: string) {
246
- if (type.startsWith('image/'))
247
- return 'fas fa-image text-green-500'
248
- if (type === 'application/pdf')
249
- return 'fas fa-file-pdf text-red-500'
250
- if (type.includes('word') || type.includes('document'))
251
- return 'fas fa-file-word text-blue-500'
252
- if (type.includes('excel') || type.includes('sheet'))
253
- return 'fas fa-file-excel text-green-600'
254
- return 'fas fa-file-alt text-gray-500'
255
- }
256
-
257
- function formatFileSize(size: number) {
258
- if (size < 1024)
259
- return `${size} B`
260
- if (size < 1024 * 1024)
261
- return `${(size / 1024).toFixed(1)} KB`
262
- return `${(size / (1024 * 1024)).toFixed(1)} MB`
263
- }
264
-
265
- // 打开图片预览
266
- function previewImage(file: FileItem) {
267
- if (file.url) {
268
- const imageUrls = props.fileList
269
- .filter(item => item.url)
270
- .map(item => item.url as string)
271
-
272
- const startPosition = imageUrls.findIndex(url => url === file.url)
273
-
274
- showImagePreview({
275
- images: imageUrls,
276
- startPosition: startPosition >= 0 ? startPosition : 0,
277
- closeable: true,
278
- })
279
- }
280
- }
281
-
282
- defineExpose({
283
- triggerFileInput,
284
- })
285
- </script>
286
-
287
- <template>
288
- <div class="file-uploader">
289
- <div class="file-uploader__header">
290
- <CardHeader :title="title" />
291
- </div>
292
-
293
- <div class="file-uploader__container">
294
- <div class="file-uploader__dropzone" @click="triggerFileInput">
295
- <svg class="file-uploader__icon" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
296
- <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
297
- </svg>
298
- <div class="file-uploader__text">
299
- <label class="file-uploader__button">
300
- <span>{{ isDev ? '上传图片' : '拍照' }}</span>
301
- <!-- <input
302
- ref="fileInput"
303
- type="file"
304
- class="file-uploader__input"
305
- :multiple="multiple"
306
- :accept="accept"
307
- @change="handleFileChange"
308
- > -->
309
- </label>
310
- <!-- <p class="file-uploader__hint">
311
- 点击上传
312
- </p> -->
313
- </div>
314
- <p class="file-uploader__tip">
315
- {{ fileTypesText }}
316
- </p>
317
- </div>
318
-
319
- <!-- 文件列表 -->
320
- <div v-if="showFileList && fileList.length > 0" class="file-uploader__list-container">
321
- <h5 class="file-uploader__list-title">
322
- 已上传附件 ({{ fileList.length }})
323
- </h5>
324
- <div class="file-uploader__list">
325
- <div v-for="file in fileList" :key="file.uid" class="file-uploader__file-item">
326
- <div class="file-uploader__file-item-content">
327
- <!-- 图片预览 -->
328
- <div v-if="file.url" class="file-uploader__preview" @click.stop="previewImage(file)">
329
- <img :src="file.url" class="file-uploader__preview-image">
330
- </div>
331
-
332
- <!-- 文件图标 -->
333
- <div v-else class="file-uploader__file-icon">
334
- <div class="file-uploader__file-icon-container">
335
- <i :class="getFileIcon(file.type)" />
336
- </div>
337
- </div>
338
-
339
- <div class="file-uploader__file-info">
340
- <p class="file-uploader__file-name">
341
- {{ file.name }}
342
- </p>
343
- <p class="file-uploader__file-size">
344
- <span>{{ formatFileSize(file.size) }}</span>
345
- <span v-if="file.status === 'success'" class="file-uploader__file-status">
346
- <i class="fas fa-check-circle" />
347
- </span>
348
- <span v-else-if="file.status === 'uploading'" class="file-uploader__file-status file-uploader__file-status--uploading">
349
- <i class="fas fa-circle-notch fa-spin" />
350
- </span>
351
- <span v-else-if="file.status === 'error'" class="file-uploader__file-status file-uploader__file-status--error">
352
- <i class="fas fa-exclamation-circle" />
353
- </span>
354
- </p>
355
- </div>
356
- </div>
357
-
358
- <div class="file-uploader__file-actions">
359
- <button
360
- type="button"
361
- class="file-uploader__file-remove"
362
- @click="removeFile(file)"
363
- >
364
- <svg class="file-uploader__file-remove-icon" fill="currentColor" viewBox="0 0 20 20">
365
- <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
366
- </svg>
367
- </button>
368
- </div>
369
- </div>
370
- </div>
371
- </div>
372
- </div>
373
- </div>
374
- </template>
375
-
376
- <style lang="less" scoped>
377
- .file-uploader {
378
- &__header {
379
- margin-bottom: 8px;
380
- }
381
-
382
- &__container {
383
- display: flex;
384
- flex-direction: column;
385
- }
386
-
387
- &__dropzone {
388
- display: flex;
389
- flex-direction: column;
390
- align-items: center;
391
- justify-content: center;
392
- padding: 16px;
393
- border: 1px dashed #d9d9d9;
394
- border-radius: 6px;
395
- background-color: #fafafa;
396
- cursor: pointer;
397
- transition: border-color 0.3s;
398
-
399
- &:hover {
400
- border-color: var(--van-primary-color);
401
- }
402
- }
403
-
404
- &__icon {
405
- width: 32px;
406
- height: 32px;
407
- color: #c0c4cc;
408
- margin-bottom: 6px;
409
- }
410
-
411
- &__text {
412
- display: flex;
413
- align-items: center;
414
- font-size: 14px;
415
- color: #666;
416
- }
417
-
418
- &__button {
419
- display: inline-block;
420
- padding: 6px 12px;
421
- background-color: #fff;
422
- border: 1px solid #dcdfe6;
423
- border-radius: 4px;
424
- font-size: 14px;
425
- color: var(--van-primary-color);
426
- cursor: pointer;
427
- margin-right: 4px;
428
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
429
-
430
- &:hover {
431
- color: #409eff;
432
- border-color: #c6e2ff;
433
- background-color: #ecf5ff;
434
- }
435
- }
436
-
437
- &__input {
438
- display: none;
439
- }
440
-
441
- &__hint {
442
- font-size: 14px;
443
- color: #666;
444
- }
445
-
446
- &__tip {
447
- margin-top: 6px;
448
- font-size: 12px;
449
- color: #666;
450
- }
451
-
452
- &__list-container {
453
- margin-top: 16px;
454
- }
455
-
456
- &__list-title {
457
- font-size: 16px;
458
- font-weight: 600;
459
- color: #333;
460
- margin-bottom: 12px;
461
- }
462
-
463
- &__list {
464
- display: flex;
465
- flex-direction: column;
466
- gap: 8px;
467
- }
468
-
469
- &__file-item {
470
- display: flex;
471
- align-items: center;
472
- justify-content: space-between;
473
- padding: 12px;
474
- border: 1px solid #e8e8e8;
475
- border-radius: 8px;
476
- background-color: #f5f7fa;
477
- }
478
-
479
- &__file-item-content {
480
- display: flex;
481
- align-items: center;
482
- flex: 1;
483
- }
484
-
485
- &__preview {
486
- width: 36px;
487
- height: 36px;
488
- margin-right: 12px;
489
- overflow: hidden;
490
- flex-shrink: 0;
491
- cursor: pointer;
492
- position: relative;
493
- border-radius: 4px;
494
- border: 1px solid #e8e8e8;
495
-
496
- &:hover::after {
497
- content: '';
498
- position: absolute;
499
- top: 0;
500
- left: 0;
501
- width: 100%;
502
- height: 100%;
503
- background-color: rgba(0, 0, 0, 0.1);
504
- border-radius: 4px;
505
- }
506
- }
507
-
508
- &__preview-image {
509
- width: 100%;
510
- height: 100%;
511
- object-fit: cover;
512
- border-radius: 4px;
513
- }
514
-
515
- &__file-icon {
516
- display: flex;
517
- align-items: center;
518
- justify-content: center;
519
- width: 30px;
520
- height: 30px;
521
- margin-right: 12px;
522
- flex-shrink: 0;
523
- }
524
-
525
- &__file-icon-container {
526
- display: flex;
527
- align-items: center;
528
- justify-content: center;
529
- width: 100%;
530
- height: 100%;
531
- border-radius: 4px;
532
- background-color: #fff;
533
-
534
- i {
535
- font-size: 16px;
536
- }
537
- }
538
-
539
- &__file-info {
540
- flex: 1;
541
- min-width: 0;
542
- }
543
-
544
- &__file-name {
545
- margin: 0;
546
- font-size: 14px;
547
- font-weight: 600;
548
- color: #333;
549
- white-space: nowrap;
550
- overflow: hidden;
551
- text-overflow: ellipsis;
552
- }
553
-
554
- &__file-size {
555
- margin: 4px 0 0;
556
- font-size: 12px;
557
- color: #666;
558
- display: flex;
559
- align-items: center;
560
- }
561
-
562
- &__file-status {
563
- margin-left: 4px;
564
- color: #67c23a;
565
-
566
- &--uploading {
567
- color: #409eff;
568
- }
569
-
570
- &--error {
571
- color: #f56c6c;
572
- }
573
- }
574
-
575
- &__file-actions {
576
- display: flex;
577
- }
578
-
579
- &__file-remove {
580
- display: flex;
581
- align-items: center;
582
- justify-content: center;
583
- width: 28px;
584
- height: 28px;
585
- border-radius: 50%;
586
- background-color: #e0e0e0;
587
- border: none;
588
- cursor: pointer;
589
- color: #606266;
590
-
591
- &:hover {
592
- background-color: #f56c6c;
593
- color: #fff;
594
- }
595
- }
596
-
597
- &__file-remove-icon {
598
- width: 14px;
599
- height: 14px;
600
- }
601
- }
602
- </style>
1
+ <script setup lang="ts">
2
+ import CardHeader from '@af-mobile-client-vue3/components/data/CardContainer/CardHeader.vue'
3
+ import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
4
+ import {
5
+ showFailToast,
6
+ showImagePreview,
7
+ } from 'vant'
8
+ import { computed } from 'vue'
9
+
10
+ export interface FileItem {
11
+ uid: string
12
+ name: string
13
+ size: number
14
+ type: string
15
+ url?: string
16
+ filePath?: string
17
+ userType?: string
18
+ result?: any // android上传图片返回的结果示例数据: {"f_file_size":0.0671,"f_form_type":"image","f_use_type":"Default","f_downloadpath":"/resource/af-revenue/images/15868b507cc0460a9913a6b94f1912d5.jpg","f_stock_id":0,"f_realpath":"/usr/local/tomcat/files/af-revenue/images/15868b507cc0460a9913a6b94f1912d5.jpg","f_type":"IMAGE","f_operator":"server","fusetype":"Default","f_filetype":"jpg","f_upload_mode":"server","id":"57900","f_filename":"15868b507cc0460a9913a6b94f1912d5.jpg"}
19
+ status: 'success' | 'error' | 'uploading'
20
+ }
21
+
22
+ const props = defineProps({
23
+ title: {
24
+ type: String,
25
+ default: '上传附件',
26
+ },
27
+ icon: {
28
+ type: String,
29
+ default: '',
30
+ },
31
+ fileList: {
32
+ type: Array as () => FileItem[],
33
+ default: () => [],
34
+ },
35
+ showFileList: {
36
+ type: Boolean,
37
+ default: true,
38
+ },
39
+ multiple: {
40
+ type: Boolean,
41
+ default: false,
42
+ },
43
+ accept: {
44
+ type: String,
45
+ default: '',
46
+ },
47
+ maxSize: {
48
+ type: Number,
49
+ default: 10 * 1024 * 1024, // 10MB
50
+ },
51
+ allowedTypes: {
52
+ type: Array as () => string[],
53
+ default: () => ['image/png', 'image/jpeg', 'application/pdf'],
54
+ },
55
+ useType: {
56
+ type: String,
57
+ default: 'Default',
58
+ },
59
+ })
60
+
61
+ const emit = defineEmits(['update:fileList', 'fileAdded', 'fileRemoved'])
62
+
63
+ // const fileInput = ref<HTMLInputElement | null>(null)
64
+ const fileTypesText = computed(() => {
65
+ return `单个文件不超过${formatFileSize(props.maxSize)}`
66
+ })
67
+
68
+ // 判断是否为开发环境
69
+ const isDev = import.meta.env.MODE === 'development'
70
+
71
+ // 模拟图片的base64数据
72
+ const mockImageBase64 = 'iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABmJLR0QA/wD/AP+gvaeTAAABdUlEQVRoge3ZsU4CQRAG4H9BYQyhgoTCGF/Axlfw0FdQKysrKwvfwsJEn8JECxKthAQbe2I8QgyRGIgFhRazJOqR7N3ezc6GKXeamW+zNze3N0Dg36PqXrDdbpfudjcRKWezWU9rPbXxXZoG0loPAbycTqdfIwBvs9nsnrbQzWbzhYg+T8/vP6HMcQbAMIyRlBK9Xm+nlLLaYCETAExT1iQiAgAiEgBc5fJDiMh1URQHXdJa92wCVb5RKSWKLWIB4FqWZWITyBrIdrJsE1UDWa1WViWrG8jlThWvbwwUqOlBFm9dXKBaTbBarVar/CKuWK1RFQVqNaiiKE7nrxPVZdSYyGQyUYPBgOu1Nq7xAjUTMrGBiIgQEUJp5zqWZtcwVFmWOB6PSKdTLJdLJEniOJEaIjJst1uUZWlUC7n27GQyQZZlyOdzxHGM+XwOz/O45n9nPB5ju90iyzLc7/eo1WrYbDZwHMd1NIPBYGDUDXl/1EAgYMwPPB0oPT3eUE4AAAAASUVORK5CYII='
73
+
74
+ function triggerFileInput() {
75
+ console.log('点击事件')
76
+
77
+ // 判断是否为开发环境
78
+ if (isDev) {
79
+ console.log('开发环境,使用模拟上传并调用真实接口')
80
+
81
+ // 基本信息
82
+ const filename = 'test.png'
83
+ const fileSize = 0.0003 // MB
84
+ const operator = '测试管理员'
85
+
86
+ // 创建临时文件对象(将base64转换为文件)
87
+ const byteString = atob(mockImageBase64)
88
+ const ab = new ArrayBuffer(byteString.length)
89
+ const ia = new Uint8Array(ab)
90
+ for (let i = 0; i < byteString.length; i++) {
91
+ ia[i] = byteString.charCodeAt(i)
92
+ }
93
+ const blob = new Blob([ab], { type: 'image/png' })
94
+ const file = new File([blob], filename, { type: 'image/png' })
95
+
96
+ // 创建FormData
97
+ const formData = new FormData()
98
+ formData.append('avatar', file) // 文件二进制数据
99
+ formData.append('resUploadMode', 'server')
100
+ formData.append('formType', 'file')
101
+ formData.append('useType', props.useType)
102
+ formData.append('resUploadStock', '1')
103
+ formData.append('filename', filename)
104
+ formData.append('filesize', fileSize.toString())
105
+ formData.append('f_operator', operator)
106
+
107
+ // 调用接口上传
108
+ fetch('/api/af-revenue/resource/upload', {
109
+ method: 'POST',
110
+ body: formData,
111
+ })
112
+ .then(response => response.json())
113
+ .then((result) => {
114
+ console.log('上传成功:', result)
115
+ // 创建临时文件项
116
+ const newFile: FileItem = {
117
+ uid: Date.now() + Math.random().toString(36).substr(2, 5),
118
+ name: filename,
119
+ size: file.size,
120
+ type: 'image/png',
121
+ status: 'success',
122
+ url: `data:image/png;base64,${mockImageBase64}`,
123
+ result: result.data.data,
124
+ }
125
+ updateFileList([...props.fileList, newFile])
126
+ emit('fileAdded', newFile)
127
+ })
128
+ .catch((error) => {
129
+ console.error('上传失败:', error)
130
+ showFailToast(`上传文件失败: ${error.message || error.msg || '请稍后重试'}`)
131
+ })
132
+
133
+ return
134
+ }
135
+
136
+ // 非开发环境,使用原生功能
137
+ mobileUtil.execute({
138
+ funcName: 'takePicture',
139
+ param: {},
140
+ callbackFunc: (result: any) => {
141
+ if (result.status === 'success') {
142
+ uploadFile(result.data)
143
+ }
144
+ },
145
+ })
146
+ // fileInput.value?.click()
147
+ }
148
+ function uploadFile(file: any) {
149
+ mobileUtil.execute({
150
+ funcName: 'uploadResource',
151
+ param: {
152
+ resUploadMode: 'server',
153
+ pathKey: props.useType,
154
+ formType: 'image',
155
+ useType: props.useType,
156
+ resUploadStock: '1',
157
+ filename: file?.name,
158
+ filesize: file?.size,
159
+ f_operator: 'server',
160
+ imgPath: file?.filePath,
161
+ urlPath: '/api/af-revenue/resource/upload',
162
+ },
163
+ callbackFunc: (result: any) => {
164
+ if (result.status === 'success') {
165
+ const newFile: FileItem = {
166
+ uid: Date.now() + Math.random().toString(36).substr(2, 5),
167
+ name: file.name,
168
+ size: file.size,
169
+ type: 'image/jpeg',
170
+ status: 'success',
171
+ url: `data:image/png;base64,${file.content}`,
172
+ filePath: file?.filePath,
173
+ result: result.data,
174
+ }
175
+ updateFileList([...props.fileList, newFile])
176
+ emit('fileAdded', newFile)
177
+ }
178
+ else {
179
+ showFailToast(`上传图片失败,${result.message}`)
180
+ }
181
+ },
182
+ })
183
+ }
184
+ /* function handleFileChange(event: Event) {
185
+ console.log('触发变更===')
186
+ const input = event.target as HTMLInputElement
187
+ if (!input.files?.length)
188
+ return
189
+
190
+ const files = Array.from(input.files)
191
+
192
+ files.forEach((file) => {
193
+ // 检查文件类型
194
+ if (props.allowedTypes.length && !props.allowedTypes.includes(file.type)) {
195
+ // 可以添加错误处理
196
+ return
197
+ }
198
+
199
+ // 检查文件大小
200
+ if (file.size > props.maxSize) {
201
+ // 可以添加错误处理
202
+ return
203
+ }
204
+ console.log('file==', file)
205
+ const newFile: FileItem = {
206
+ uid: Date.now() + Math.random().toString(36).substr(2, 5),
207
+ name: file.name,
208
+ size: file.size,
209
+ type: file.type,
210
+ status: 'success',
211
+ }
212
+ console.log('newFile==', newFile)
213
+ // 处理图片预览
214
+ if (file.type.startsWith('image/')) {
215
+ const reader = new FileReader()
216
+ reader.onload = (e) => {
217
+ if (e.target?.result) {
218
+ newFile.url = e.target.result as string
219
+ updateFileList([...props.fileList, newFile])
220
+ }
221
+ }
222
+ reader.readAsDataURL(file)
223
+ }
224
+ else {
225
+ updateFileList([...props.fileList, newFile])
226
+ }
227
+
228
+ emit('fileAdded', { file, fileItem: newFile })
229
+ })
230
+
231
+ // 重置文件输入,允许选择相同文件
232
+ input.value = ''
233
+ } */
234
+
235
+ function removeFile(file: FileItem) {
236
+ const newFileList = props.fileList.filter(item => item.uid !== file.uid)
237
+ updateFileList(newFileList)
238
+ emit('fileRemoved', file)
239
+ }
240
+
241
+ function updateFileList(files: FileItem[]) {
242
+ emit('update:fileList', files)
243
+ }
244
+
245
+ function getFileIcon(type: string) {
246
+ if (type.startsWith('image/'))
247
+ return 'fas fa-image text-green-500'
248
+ if (type === 'application/pdf')
249
+ return 'fas fa-file-pdf text-red-500'
250
+ if (type.includes('word') || type.includes('document'))
251
+ return 'fas fa-file-word text-blue-500'
252
+ if (type.includes('excel') || type.includes('sheet'))
253
+ return 'fas fa-file-excel text-green-600'
254
+ return 'fas fa-file-alt text-gray-500'
255
+ }
256
+
257
+ function formatFileSize(size: number) {
258
+ if (size < 1024)
259
+ return `${size} B`
260
+ if (size < 1024 * 1024)
261
+ return `${(size / 1024).toFixed(1)} KB`
262
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`
263
+ }
264
+
265
+ // 打开图片预览
266
+ function previewImage(file: FileItem) {
267
+ if (file.url) {
268
+ const imageUrls = props.fileList
269
+ .filter(item => item.url)
270
+ .map(item => item.url as string)
271
+
272
+ const startPosition = imageUrls.findIndex(url => url === file.url)
273
+
274
+ showImagePreview({
275
+ images: imageUrls,
276
+ startPosition: startPosition >= 0 ? startPosition : 0,
277
+ closeable: true,
278
+ })
279
+ }
280
+ }
281
+
282
+ defineExpose({
283
+ triggerFileInput,
284
+ })
285
+ </script>
286
+
287
+ <template>
288
+ <div class="file-uploader">
289
+ <div class="file-uploader__header">
290
+ <CardHeader :title="title" />
291
+ </div>
292
+
293
+ <div class="file-uploader__container">
294
+ <div class="file-uploader__dropzone" @click="triggerFileInput">
295
+ <svg class="file-uploader__icon" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
296
+ <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
297
+ </svg>
298
+ <div class="file-uploader__text">
299
+ <label class="file-uploader__button">
300
+ <span>{{ isDev ? '上传图片' : '拍照' }}</span>
301
+ <!-- <input
302
+ ref="fileInput"
303
+ type="file"
304
+ class="file-uploader__input"
305
+ :multiple="multiple"
306
+ :accept="accept"
307
+ @change="handleFileChange"
308
+ > -->
309
+ </label>
310
+ <!-- <p class="file-uploader__hint">
311
+ 点击上传
312
+ </p> -->
313
+ </div>
314
+ <p class="file-uploader__tip">
315
+ {{ fileTypesText }}
316
+ </p>
317
+ </div>
318
+
319
+ <!-- 文件列表 -->
320
+ <div v-if="showFileList && fileList.length > 0" class="file-uploader__list-container">
321
+ <h5 class="file-uploader__list-title">
322
+ 已上传附件 ({{ fileList.length }})
323
+ </h5>
324
+ <div class="file-uploader__list">
325
+ <div v-for="file in fileList" :key="file.uid" class="file-uploader__file-item">
326
+ <div class="file-uploader__file-item-content">
327
+ <!-- 图片预览 -->
328
+ <div v-if="file.url" class="file-uploader__preview" @click.stop="previewImage(file)">
329
+ <img :src="file.url" class="file-uploader__preview-image">
330
+ </div>
331
+
332
+ <!-- 文件图标 -->
333
+ <div v-else class="file-uploader__file-icon">
334
+ <div class="file-uploader__file-icon-container">
335
+ <i :class="getFileIcon(file.type)" />
336
+ </div>
337
+ </div>
338
+
339
+ <div class="file-uploader__file-info">
340
+ <p class="file-uploader__file-name">
341
+ {{ file.name }}
342
+ </p>
343
+ <p class="file-uploader__file-size">
344
+ <span>{{ formatFileSize(file.size) }}</span>
345
+ <span v-if="file.status === 'success'" class="file-uploader__file-status">
346
+ <i class="fas fa-check-circle" />
347
+ </span>
348
+ <span v-else-if="file.status === 'uploading'" class="file-uploader__file-status file-uploader__file-status--uploading">
349
+ <i class="fas fa-circle-notch fa-spin" />
350
+ </span>
351
+ <span v-else-if="file.status === 'error'" class="file-uploader__file-status file-uploader__file-status--error">
352
+ <i class="fas fa-exclamation-circle" />
353
+ </span>
354
+ </p>
355
+ </div>
356
+ </div>
357
+
358
+ <div class="file-uploader__file-actions">
359
+ <button
360
+ type="button"
361
+ class="file-uploader__file-remove"
362
+ @click="removeFile(file)"
363
+ >
364
+ <svg class="file-uploader__file-remove-icon" fill="currentColor" viewBox="0 0 20 20">
365
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
366
+ </svg>
367
+ </button>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ </div>
374
+ </template>
375
+
376
+ <style lang="less" scoped>
377
+ .file-uploader {
378
+ &__header {
379
+ margin-bottom: 8px;
380
+ }
381
+
382
+ &__container {
383
+ display: flex;
384
+ flex-direction: column;
385
+ }
386
+
387
+ &__dropzone {
388
+ display: flex;
389
+ flex-direction: column;
390
+ align-items: center;
391
+ justify-content: center;
392
+ padding: 16px;
393
+ border: 1px dashed #d9d9d9;
394
+ border-radius: 6px;
395
+ background-color: #fafafa;
396
+ cursor: pointer;
397
+ transition: border-color 0.3s;
398
+
399
+ &:hover {
400
+ border-color: var(--van-primary-color);
401
+ }
402
+ }
403
+
404
+ &__icon {
405
+ width: 32px;
406
+ height: 32px;
407
+ color: #c0c4cc;
408
+ margin-bottom: 6px;
409
+ }
410
+
411
+ &__text {
412
+ display: flex;
413
+ align-items: center;
414
+ font-size: 14px;
415
+ color: #666;
416
+ }
417
+
418
+ &__button {
419
+ display: inline-block;
420
+ padding: 6px 12px;
421
+ background-color: #fff;
422
+ border: 1px solid #dcdfe6;
423
+ border-radius: 4px;
424
+ font-size: 14px;
425
+ color: var(--van-primary-color);
426
+ cursor: pointer;
427
+ margin-right: 4px;
428
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
429
+
430
+ &:hover {
431
+ color: #409eff;
432
+ border-color: #c6e2ff;
433
+ background-color: #ecf5ff;
434
+ }
435
+ }
436
+
437
+ &__input {
438
+ display: none;
439
+ }
440
+
441
+ &__hint {
442
+ font-size: 14px;
443
+ color: #666;
444
+ }
445
+
446
+ &__tip {
447
+ margin-top: 6px;
448
+ font-size: 12px;
449
+ color: #666;
450
+ }
451
+
452
+ &__list-container {
453
+ margin-top: 16px;
454
+ }
455
+
456
+ &__list-title {
457
+ font-size: 16px;
458
+ font-weight: 600;
459
+ color: #333;
460
+ margin-bottom: 12px;
461
+ }
462
+
463
+ &__list {
464
+ display: flex;
465
+ flex-direction: column;
466
+ gap: 8px;
467
+ }
468
+
469
+ &__file-item {
470
+ display: flex;
471
+ align-items: center;
472
+ justify-content: space-between;
473
+ padding: 12px;
474
+ border: 1px solid #e8e8e8;
475
+ border-radius: 8px;
476
+ background-color: #f5f7fa;
477
+ }
478
+
479
+ &__file-item-content {
480
+ display: flex;
481
+ align-items: center;
482
+ flex: 1;
483
+ }
484
+
485
+ &__preview {
486
+ width: 36px;
487
+ height: 36px;
488
+ margin-right: 12px;
489
+ overflow: hidden;
490
+ flex-shrink: 0;
491
+ cursor: pointer;
492
+ position: relative;
493
+ border-radius: 4px;
494
+ border: 1px solid #e8e8e8;
495
+
496
+ &:hover::after {
497
+ content: '';
498
+ position: absolute;
499
+ top: 0;
500
+ left: 0;
501
+ width: 100%;
502
+ height: 100%;
503
+ background-color: rgba(0, 0, 0, 0.1);
504
+ border-radius: 4px;
505
+ }
506
+ }
507
+
508
+ &__preview-image {
509
+ width: 100%;
510
+ height: 100%;
511
+ object-fit: cover;
512
+ border-radius: 4px;
513
+ }
514
+
515
+ &__file-icon {
516
+ display: flex;
517
+ align-items: center;
518
+ justify-content: center;
519
+ width: 30px;
520
+ height: 30px;
521
+ margin-right: 12px;
522
+ flex-shrink: 0;
523
+ }
524
+
525
+ &__file-icon-container {
526
+ display: flex;
527
+ align-items: center;
528
+ justify-content: center;
529
+ width: 100%;
530
+ height: 100%;
531
+ border-radius: 4px;
532
+ background-color: #fff;
533
+
534
+ i {
535
+ font-size: 16px;
536
+ }
537
+ }
538
+
539
+ &__file-info {
540
+ flex: 1;
541
+ min-width: 0;
542
+ }
543
+
544
+ &__file-name {
545
+ margin: 0;
546
+ font-size: 14px;
547
+ font-weight: 600;
548
+ color: #333;
549
+ white-space: nowrap;
550
+ overflow: hidden;
551
+ text-overflow: ellipsis;
552
+ }
553
+
554
+ &__file-size {
555
+ margin: 4px 0 0;
556
+ font-size: 12px;
557
+ color: #666;
558
+ display: flex;
559
+ align-items: center;
560
+ }
561
+
562
+ &__file-status {
563
+ margin-left: 4px;
564
+ color: #67c23a;
565
+
566
+ &--uploading {
567
+ color: #409eff;
568
+ }
569
+
570
+ &--error {
571
+ color: #f56c6c;
572
+ }
573
+ }
574
+
575
+ &__file-actions {
576
+ display: flex;
577
+ }
578
+
579
+ &__file-remove {
580
+ display: flex;
581
+ align-items: center;
582
+ justify-content: center;
583
+ width: 28px;
584
+ height: 28px;
585
+ border-radius: 50%;
586
+ background-color: #e0e0e0;
587
+ border: none;
588
+ cursor: pointer;
589
+ color: #606266;
590
+
591
+ &:hover {
592
+ background-color: #f56c6c;
593
+ color: #fff;
594
+ }
595
+ }
596
+
597
+ &__file-remove-icon {
598
+ width: 14px;
599
+ height: 14px;
600
+ }
601
+ }
602
+ </style>