im-ui-mobile 0.1.0 → 0.1.2

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 (85) hide show
  1. package/components/im-avatar/im-avatar.vue +7 -7
  2. package/components/im-badge/im-badge.vue +326 -0
  3. package/components/im-button/im-button.vue +71 -34
  4. package/components/im-card/im-card.vue +563 -0
  5. package/components/im-chat-item/im-chat-item.vue +5 -4
  6. package/components/im-col/im-col.vue +191 -0
  7. package/components/im-dialog/im-dialog.vue +543 -0
  8. package/components/im-double-tap-view/im-double-tap-view.vue +93 -0
  9. package/components/im-emoji-picker/im-emoji-picker.vue +1143 -0
  10. package/components/im-friend-item/im-friend-item.vue +1 -1
  11. package/components/im-group-item/im-group-item.vue +1 -1
  12. package/components/im-group-member-selector/im-group-member-selector.vue +5 -5
  13. package/components/im-group-rtc-join/im-group-rtc-join.vue +8 -8
  14. package/components/im-icon/im-icon.vue +593 -0
  15. package/components/im-image-upload/im-image-upload.vue +0 -2
  16. package/components/im-link/im-link.vue +628 -0
  17. package/components/im-loading/im-loading.vue +13 -4
  18. package/components/im-mention-picker/im-mention-picker.vue +8 -7
  19. package/components/im-message-action/im-message-action.vue +678 -0
  20. package/components/im-message-item/im-message-item.vue +28 -26
  21. package/components/im-message-list/im-message-list.vue +1108 -0
  22. package/components/im-modal/im-modal.vue +373 -0
  23. package/components/im-nav-bar/im-nav-bar.vue +689 -75
  24. package/components/im-parse/im-parse.vue +1054 -0
  25. package/components/im-popup/im-popup.vue +467 -0
  26. package/components/im-read-receipt/im-read-receipt.vue +10 -10
  27. package/components/im-row/im-row.vue +189 -0
  28. package/components/im-search/im-search.vue +762 -0
  29. package/components/im-sku/im-sku.vue +720 -0
  30. package/components/im-sku/utils/helper.ts +182 -0
  31. package/components/im-stepper/im-stepper.vue +585 -0
  32. package/components/im-stepper/utils/helper.ts +167 -0
  33. package/components/im-tabs/im-tabs.vue +1022 -0
  34. package/components/im-tabs/tabs-navigation.vue +489 -0
  35. package/components/im-tabs/utils/helper.ts +181 -0
  36. package/components/im-tabs-tab-pane/im-tabs-tab-pane.vue +145 -0
  37. package/components/im-upload/im-upload.vue +1236 -0
  38. package/components/im-voice-input/im-voice-input.vue +1 -1
  39. package/index.js +3 -5
  40. package/index.scss +19 -0
  41. package/libs/emoji-data.ts +229 -0
  42. package/libs/index.ts +16 -16
  43. package/package.json +1 -2
  44. package/styles/button.scss +33 -33
  45. package/theme.scss +2 -2
  46. package/types/components/badge.d.ts +42 -0
  47. package/types/components/button.d.ts +2 -1
  48. package/types/components/card.d.ts +122 -0
  49. package/types/components/col.d.ts +37 -0
  50. package/types/components/dialog.d.ts +125 -0
  51. package/types/components/double-tap-view.d.ts +31 -0
  52. package/types/components/emoji-picker.d.ts +121 -0
  53. package/types/components/group-rtc-join.d.ts +1 -1
  54. package/types/components/icon.d.ts +77 -0
  55. package/types/components/link.d.ts +55 -0
  56. package/types/components/loading.d.ts +1 -0
  57. package/types/components/message-action.d.ts +96 -0
  58. package/types/components/message-item.d.ts +2 -2
  59. package/types/components/message-list.d.ts +136 -0
  60. package/types/components/modal.d.ts +106 -0
  61. package/types/components/nav-bar.d.ts +125 -0
  62. package/types/components/parse.d.ts +90 -0
  63. package/types/components/popup.d.ts +58 -0
  64. package/types/components/row.d.ts +31 -0
  65. package/types/components/search.d.ts +54 -0
  66. package/types/components/sku.d.ts +195 -0
  67. package/types/components/stepper.d.ts +99 -0
  68. package/types/components/tabs-tab-pane.d.ts +27 -0
  69. package/types/components/tabs.d.ts +117 -0
  70. package/types/components/upload.d.ts +137 -0
  71. package/types/components.d.ts +19 -1
  72. package/types/index.d.ts +38 -1
  73. package/types/libs/index.d.ts +10 -10
  74. package/types/utils/base64.d.ts +5 -0
  75. package/types/utils/dom.d.ts +3 -0
  76. package/types/utils/enums.d.ts +4 -5
  77. package/types/utils/validator.d.ts +74 -0
  78. package/utils/base64.js +18 -0
  79. package/utils/dom.js +353 -1
  80. package/utils/enums.js +4 -5
  81. package/utils/validator.js +230 -0
  82. package/components/im-file-upload/im-file-upload.vue +0 -309
  83. package/plugins/uview-plus.js +0 -29
  84. package/types/components/arrow-bar.d.ts +0 -14
  85. package/types/components/file-upload.d.ts +0 -58
@@ -0,0 +1,1236 @@
1
+ <template>
2
+ <view class="im-upload">
3
+ <!-- 上传区域 -->
4
+ <view
5
+ class="im-upload__area"
6
+ :class="[
7
+ `im-upload__area--${type}`,
8
+ { 'im-upload__area--disabled': disabled },
9
+ { 'im-upload__area--readonly': readonly },
10
+ { 'im-upload__area--drag-over': dragOver }
11
+ ]"
12
+ @tap="handleUploadTap"
13
+ @touchmove="handleDragOver"
14
+ @touchend="handleDragLeave"
15
+ >
16
+ <!-- 按钮类型 -->
17
+ <template v-if="type === 'button'">
18
+ <im-button
19
+ :type="buttonType"
20
+ :size="buttonSize"
21
+ :disabled="disabled"
22
+ :loading="uploading"
23
+ >
24
+ <view class="im-upload__button-content">
25
+ <text v-if="!uploading" class="im-upload__icon">+</text>
26
+ <text class="im-upload__text">{{ uploading ? '上传中...' : buttonText }}</text>
27
+ </view>
28
+ </im-button>
29
+ </template>
30
+
31
+ <!-- 卡片类型 -->
32
+ <template v-else-if="type === 'card'">
33
+ <view class="im-upload__card">
34
+ <text class="im-upload__card-icon">+</text>
35
+ <text class="im-upload__card-text">{{ cardText }}</text>
36
+ <text v-if="hint" class="im-upload__card-hint">{{ hint }}</text>
37
+ </view>
38
+ </template>
39
+
40
+ <!-- 头像类型 -->
41
+ <template v-else-if="type === 'avatar'">
42
+ <view class="im-upload__avatar">
43
+ <im-avatar
44
+ :src="fileList[0]?.url"
45
+ :size="avatarSize"
46
+ :radius="avatarRadius"
47
+ >
48
+ <template v-if="!fileList[0]?.url">
49
+ <text class="im-upload__avatar-icon">+</text>
50
+ </template>
51
+ </im-avatar>
52
+ <text class="im-upload__avatar-text">{{ avatarText }}</text>
53
+ </view>
54
+ </template>
55
+
56
+ <!-- 拖拽区域类型 -->
57
+ <template v-else-if="type === 'drag'">
58
+ <view class="im-upload__drag">
59
+ <text class="im-upload__drag-icon">📁</text>
60
+ <text class="im-upload__drag-text">{{ dragText }}</text>
61
+ <text v-if="hint" class="im-upload__drag-hint">{{ hint }}</text>
62
+ </view>
63
+ </template>
64
+
65
+ <slot v-else></slot>
66
+ </view>
67
+
68
+ <!-- 文件列表 -->
69
+ <view v-if="showList && fileList.length > 0" class="im-upload__list">
70
+ <view
71
+ v-for="(file, index) in fileList"
72
+ :key="file.uid"
73
+ class="im-upload__item"
74
+ :class="`im-upload__item--${listType}`"
75
+ >
76
+ <!-- 图片列表 -->
77
+ <template v-if="listType === 'picture'">
78
+ <view class="im-upload__item-preview">
79
+ <image
80
+ v-if="file.type?.startsWith('image/')"
81
+ class="im-upload__item-image"
82
+ :src="file.url || file.thumbUrl"
83
+ mode="aspectFill"
84
+ @tap="handlePreview(file)"
85
+ />
86
+ <view v-else class="im-upload__item-file">
87
+ <text class="im-upload__item-file-icon">📄</text>
88
+ <text class="im-upload__item-file-name" @tap="handlePreview(file)">
89
+ {{ file.name }}
90
+ </text>
91
+ </view>
92
+
93
+ <!-- 上传状态 -->
94
+ <view v-if="file.status === 'uploading'" class="im-upload__item-status">
95
+ <view class="im-upload__item-progress">
96
+ <view
97
+ class="im-upload__item-progress-bar"
98
+ :style="{ width: `${file.progress || 0}%` }"
99
+ ></view>
100
+ </view>
101
+ <text class="im-upload__item-percent">{{ file.progress || 0 }}%</text>
102
+ </view>
103
+
104
+ <view v-else-if="file.status === 'error'" class="im-upload__item-error">
105
+ <text class="im-upload__item-error-text">上传失败</text>
106
+ <text
107
+ v-if="!readonly"
108
+ class="im-upload__item-retry"
109
+ @tap="handleRetry(file, index)"
110
+ >
111
+ 重试
112
+ </text>
113
+ </view>
114
+
115
+ <!-- 操作按钮 -->
116
+ <view class="im-upload__item-actions">
117
+ <text
118
+ v-if="file.status === 'done' && previewable"
119
+ class="im-upload__item-action"
120
+ @tap="handlePreview(file)"
121
+ >
122
+ 👁️
123
+ </text>
124
+ <text
125
+ v-if="!readonly && removable"
126
+ class="im-upload__item-action"
127
+ @tap="handleRemove(file, index)"
128
+ >
129
+
130
+ </text>
131
+ </view>
132
+ </view>
133
+ </template>
134
+
135
+ <!-- 文本列表 -->
136
+ <template v-else>
137
+ <view class="im-upload__item-text">
138
+ <text class="im-upload__item-name">{{ file.name }}</text>
139
+ <text class="im-upload__item-size">{{ formatSize(file.size) }}</text>
140
+ <view class="im-upload__item-status-container">
141
+ <text class="im-upload__item-status-text" :class="`im-upload__item-status--${file.status}`">
142
+ {{ getStatusText(file.status) }}
143
+ </text>
144
+ <text v-if="file.status === 'uploading'" class="im-upload__item-percent-text">
145
+ {{ file.progress }}%
146
+ </text>
147
+ </view>
148
+ </view>
149
+
150
+ <!-- 操作按钮 -->
151
+ <view v-if="!readonly" class="im-upload__item-text-actions">
152
+ <text
153
+ v-if="file.status === 'error'"
154
+ class="im-upload__item-text-action"
155
+ @tap="handleRetry(file, index)"
156
+ >
157
+ 重试
158
+ </text>
159
+ <text
160
+ v-if="file.status === 'done' && previewable"
161
+ class="im-upload__item-text-action"
162
+ @tap="handlePreview(file)"
163
+ >
164
+ 预览
165
+ </text>
166
+ <text
167
+ v-if="removable"
168
+ class="im-upload__item-text-action im-upload__item-text-action--remove"
169
+ @tap="handleRemove(file, index)"
170
+ >
171
+ 删除
172
+ </text>
173
+ </view>
174
+ </template>
175
+ </view>
176
+ </view>
177
+
178
+ <!-- 上传提示 -->
179
+ <view v-if="showTip" class="im-upload__tip">
180
+ <text class="im-upload__tip-text">{{ tip }}</text>
181
+ <text v-if="fileList.length > 0" class="im-upload__tip-count">
182
+ 已选择 {{ fileList.length }} 个文件
183
+ </text>
184
+ </view>
185
+
186
+ <!-- 操作按钮 -->
187
+ <view v-if="showActions && fileList.length > 0 && !readonly" class="im-upload__actions">
188
+ <im-button
189
+ v-if="showUploadAll"
190
+ type="primary"
191
+ size="small"
192
+ :loading="uploadingAll"
193
+ :disabled="uploadingAll || disabled"
194
+ @click="handleUploadAll"
195
+ >
196
+ {{ uploadingAll ? '上传中...' : '全部上传' }}
197
+ </im-button>
198
+
199
+ <im-button
200
+ v-if="showClear"
201
+ type="default"
202
+ size="small"
203
+ :disabled="disabled"
204
+ @click="handleClearAll"
205
+ >
206
+ 清空列表
207
+ </im-button>
208
+ </view>
209
+ </view>
210
+ </template>
211
+
212
+ <script setup lang="ts">
213
+ import { ref, computed, watch } from 'vue'
214
+
215
+ // 定义文件类型
216
+ interface UploadFile {
217
+ uid: string
218
+ name: string
219
+ size: number
220
+ type?: string
221
+ url?: string
222
+ thumbUrl?: string
223
+ status: 'pending' | 'uploading' | 'done' | 'error'
224
+ progress?: number
225
+ response?: any
226
+ error?: Error
227
+ rawFile?: any
228
+ file?: File
229
+ }
230
+
231
+ // 定义 Props
232
+ interface Props {
233
+ // 值
234
+ modelValue?: UploadFile[]
235
+
236
+ // 上传类型
237
+ type?: 'button' | 'card' | 'avatar' | 'drag' | undefined
238
+
239
+ // 按钮配置
240
+ buttonText?: string
241
+ buttonType?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info' // 'primary' | 'default' | 'warning' | 'error'
242
+ buttonSize?: 'small' | 'medium' | 'large'
243
+
244
+ // 卡片配置
245
+ cardText?: string
246
+
247
+ // 头像配置
248
+ avatarText?: string
249
+ avatarSize?: number | string
250
+ avatarRadius?: string
251
+
252
+ // 拖拽配置
253
+ dragText?: string
254
+
255
+ // 通用配置
256
+ hint?: string
257
+ tip?: string
258
+ showTip?: boolean
259
+ showActions?: boolean
260
+ showUploadAll?: boolean
261
+ showClear?: boolean
262
+
263
+ // 文件配置
264
+ accept?: string // 比如:image/*,.pdf,.doc,.docx
265
+ multiple?: boolean
266
+ maxCount?: number
267
+ maxSize?: number // 单位:字节
268
+ beforeUpload?: (file: File) => boolean | Promise<boolean> // 上传前钩子
269
+
270
+ // 上传配置
271
+ action?: string
272
+ accessToken?: string,
273
+ headers?: Record<string, string>
274
+ data?: Record<string, any>
275
+ name?: string
276
+ withCredentials?: boolean
277
+ timeout?: number
278
+ autoUpload?: boolean
279
+
280
+ // 响应格式化
281
+ responseFormatter?: (response: any) => { url: string; [key: string]: any }
282
+
283
+ // 列表配置
284
+ showList?: boolean
285
+ listType?: 'text' | 'picture'
286
+ removable?: boolean
287
+ previewable?: boolean
288
+
289
+ // 状态
290
+ disabled?: boolean
291
+ readonly?: boolean
292
+
293
+ // 自定义上传
294
+ customRequest?: (file: File, onProgress: (percent: number) => void) => Promise<any>
295
+ }
296
+
297
+ // 定义 Emits
298
+ interface Emits {
299
+ (e: 'update:modelValue', files: UploadFile[]): void
300
+ (e: 'change', files: UploadFile[]): void
301
+ (e: 'select', file: File): void
302
+ (e: 'upload', file: UploadFile): void
303
+ (e: 'success', response: any, file: UploadFile): void
304
+ (e: 'error', error: Error, file: UploadFile): void
305
+ (e: 'progress', percent: number, file: UploadFile): void
306
+ (e: 'remove', file: UploadFile, index: number): void
307
+ (e: 'preview', file: UploadFile): void
308
+ (e: 'exceed', files: File[]): void
309
+ (e: 'before-upload', file: UploadFile): void
310
+ (e: 'after-upload', file: UploadFile): void
311
+ }
312
+
313
+ // 定义 Props 默认值
314
+ const props = withDefaults(defineProps<Props>(), {
315
+ modelValue: () => [],
316
+
317
+ type: undefined, // 'button',
318
+
319
+ buttonText: '上传文件',
320
+ buttonType: 'primary',
321
+ buttonSize: 'medium',
322
+
323
+ cardText: '点击上传',
324
+
325
+ avatarText: '上传头像',
326
+ avatarSize: 120,
327
+ avatarRadius: '50%',
328
+
329
+ dragText: '将文件拖到此处,或点击上传',
330
+
331
+ tip: '支持上传图片、文档等文件',
332
+ showTip: false,
333
+ showActions: false,
334
+ showUploadAll: true,
335
+ showClear: true,
336
+
337
+ accept: '*',
338
+ multiple: false,
339
+ maxCount: 9,
340
+ maxSize: 10 * 1024 * 1024,
341
+
342
+ action: '',
343
+ accessToken:'',
344
+ headers: () => ({}),
345
+ data: () => ({}),
346
+ name: 'file',
347
+ withCredentials: false,
348
+ timeout: 10000,
349
+ autoUpload: true,
350
+
351
+ responseFormatter: (response:any) => {
352
+ return {
353
+ url: response?.data?.url || response?.result?.url || response?.url || response?.fileUrl,
354
+ name: response?.data?.fileName || response?.result?.fileName ||response?.data?.name ||response?.result?.name ||response?.name,
355
+ ...response
356
+ }
357
+ },
358
+
359
+ showList: false,
360
+ listType: 'picture',
361
+ removable: true,
362
+ previewable: true,
363
+
364
+ disabled: false,
365
+ readonly: false
366
+ })
367
+
368
+ const emit = defineEmits<Emits>()
369
+
370
+ // 响应式状态
371
+ const fileList = ref<UploadFile[]>(props.modelValue || [])
372
+ const dragOver = ref(false)
373
+ const uploadingAll = ref(false)
374
+
375
+ // 上传任务映射
376
+ const uploadTasks = new Map<string, UniApp.UploadTask>()
377
+
378
+ // 监听 modelValue 变化
379
+ watch(() => props.modelValue, (val) => {
380
+ if (val) {
381
+ fileList.value = val
382
+ }
383
+ }, { deep: true })
384
+
385
+ // 计算属性
386
+ const uploading = computed(() => {
387
+ return fileList.value.some(file => file.status === 'uploading')
388
+ })
389
+
390
+ // 生成唯一ID
391
+ const generateUid = () => {
392
+ return Date.now() + '-' + Math.random().toString(36).substr(2, 9)
393
+ }
394
+
395
+ // 格式化文件大小
396
+ const formatSize = (bytes?: number) => {
397
+ if (!bytes) return '0 B'
398
+
399
+ const units = ['B', 'KB', 'MB', 'GB']
400
+ let size = bytes
401
+ let unitIndex = 0
402
+
403
+ while (size >= 1024 && unitIndex < units.length - 1) {
404
+ size /= 1024
405
+ unitIndex++
406
+ }
407
+
408
+ return `${size.toFixed(1)} ${units[unitIndex]}`
409
+ }
410
+
411
+ // 获取状态文本
412
+ const getStatusText = (status?: string) => {
413
+ const map: Record<string, string> = {
414
+ pending: '等待上传',
415
+ uploading: '上传中',
416
+ done: '已完成',
417
+ error: '上传失败'
418
+ }
419
+ return status ? map[status] || '未知' : '未知'
420
+ }
421
+
422
+ // 处理上传点击
423
+ const handleUploadTap = async () => {
424
+ if (props.disabled || props.readonly) {
425
+ return
426
+ }
427
+
428
+ const maxSelectable = props.multiple ?
429
+ Math.max(0, props.maxCount! - fileList.value.length) : 1
430
+
431
+ if (maxSelectable <= 0) {
432
+ emit('exceed', [])
433
+ uni.showToast({
434
+ title: `最多只能上传${props.maxCount}个文件`,
435
+ icon: 'none'
436
+ })
437
+ return
438
+ }
439
+
440
+ uni.chooseFile({
441
+ count: maxSelectable,
442
+ type: getFileType(props.accept),
443
+ extension: getFileExtensions(props.accept),
444
+ success: async (res) => {
445
+ for (const tempFile of res.tempFiles as Array<any>) {
446
+ await processFile(tempFile)
447
+ }
448
+ },
449
+ fail: (err) => {
450
+ console.error('选择文件失败:', err)
451
+ uni.showToast({
452
+ title: '选择文件失败',
453
+ icon: 'none'
454
+ })
455
+ }
456
+ })
457
+ }
458
+
459
+ // 获取文件类型
460
+ const getFileType = (accept: string): 'image' | 'video' | 'all' => {
461
+ if (accept.includes('image')) return 'image'
462
+ if (accept.includes('video')) return 'video'
463
+ return 'all'
464
+ }
465
+
466
+ // 获取文件扩展名
467
+ const getFileExtensions = (accept: string): string[] | undefined => {
468
+ if (accept === '*' || accept.includes('all')) {
469
+ return undefined
470
+ }
471
+
472
+ const extensions = accept
473
+ .split(',')
474
+ .map(ext => ext.trim())
475
+ .filter(ext => ext.startsWith('.'))
476
+ .map(ext => ext.substring(1))
477
+
478
+ return extensions.length > 0 ? extensions : undefined
479
+ }
480
+
481
+ // 处理文件
482
+ const processFile = async (uniFile: any) => {
483
+ // 检查文件大小
484
+ if (props.maxSize && uniFile.size > props.maxSize) {
485
+ uni.showToast({
486
+ title: `文件大小不能超过${formatSize(props.maxSize)}`,
487
+ icon: 'none'
488
+ })
489
+ return
490
+ }
491
+
492
+ // 检查文件类型
493
+ if (!isFileTypeAccepted(uniFile)) {
494
+ uni.showToast({
495
+ title: '不支持的文件类型',
496
+ icon: 'none'
497
+ })
498
+ return
499
+ }
500
+
501
+ // 创建文件对象
502
+ const file = createFileFromUniFile(uniFile)
503
+
504
+ // 执行上传前钩子
505
+ if (props.beforeUpload) {
506
+ try {
507
+ const result = await Promise.resolve(props.beforeUpload(file))
508
+ if (result === false) {
509
+ return
510
+ }
511
+ } catch (error) {
512
+ console.error('beforeUpload error:', error)
513
+ return
514
+ }
515
+ }
516
+
517
+ // 创建上传文件对象
518
+ const uploadFile: UploadFile = {
519
+ uid: generateUid(),
520
+ name: uniFile.name,
521
+ size: uniFile.size,
522
+ type: uniFile.type,
523
+ status: 'pending',
524
+ progress: 0,
525
+ url: uniFile.path,
526
+ thumbUrl: uniFile.type?.startsWith('image/') ? uniFile.path : undefined,
527
+ rawFile: uniFile,
528
+ file
529
+ }
530
+
531
+ // 添加到文件列表
532
+ if (!props.multiple) {
533
+ fileList.value = [uploadFile]
534
+ } else {
535
+ fileList.value.push(uploadFile)
536
+ }
537
+
538
+ emit('select', file)
539
+ updateFileList()
540
+
541
+ // 自动上传
542
+ if (props.autoUpload && (props.action || props.customRequest)) {
543
+ await startUpload(uploadFile)
544
+ }
545
+ }
546
+
547
+ // 检查文件类型是否被接受
548
+ const isFileTypeAccepted = (uniFile: any): boolean => {
549
+ if (props.accept === '*') return true
550
+
551
+ const acceptTypes = props.accept.split(',').map(type => type.trim())
552
+
553
+ for (const acceptType of acceptTypes) {
554
+ if (acceptType === '*') return true
555
+
556
+ // 检查 MIME 类型
557
+ if (acceptType.endsWith('/*')) {
558
+ const category = acceptType.split('/')[0]
559
+ if (uniFile.type?.startsWith(category + '/')) {
560
+ return true
561
+ }
562
+ }
563
+
564
+ // 检查扩展名
565
+ if (acceptType.startsWith('.')) {
566
+ const ext = acceptType.substring(1).toLowerCase()
567
+ const fileName = uniFile.name.toLowerCase()
568
+ if (fileName.endsWith('.' + ext)) {
569
+ return true
570
+ }
571
+ }
572
+
573
+ // 检查完整 MIME 类型
574
+ if (uniFile.type === acceptType) {
575
+ return true
576
+ }
577
+ }
578
+
579
+ return false
580
+ }
581
+
582
+ // 从 uniapp 文件创建 File 对象
583
+ const createFileFromUniFile = (uniFile: any): File => {
584
+ const file = new File([], uniFile.name, {
585
+ type: uniFile.type || 'application/octet-stream',
586
+ lastModified: uniFile.lastModified || Date.now()
587
+ })
588
+
589
+ Object.defineProperties(file, {
590
+ size: {
591
+ value: uniFile.size,
592
+ writable: false
593
+ },
594
+ path: {
595
+ value: uniFile.path,
596
+ writable: false
597
+ }
598
+ })
599
+
600
+ return file
601
+ }
602
+
603
+ // 开始上传
604
+ const startUpload = async (uploadFile: UploadFile) => {
605
+ uploadFile.status = 'uploading'
606
+ uploadFile.progress = 0
607
+ emit('upload', uploadFile)
608
+ emit('before-upload', uploadFile)
609
+ updateFileList()
610
+
611
+ try {
612
+ let response: any
613
+
614
+ if (props.customRequest) {
615
+ // 使用自定义上传函数
616
+ response = await props.customRequest(
617
+ uploadFile.file!,
618
+ (percent) => {
619
+ uploadFile.progress = percent
620
+ emit('progress', percent, uploadFile)
621
+ updateFileList()
622
+ }
623
+ )
624
+ } else if (props.action) {
625
+ // 使用默认上传
626
+ response = await defaultUpload(uploadFile)
627
+ } else {
628
+ throw new Error('请设置上传地址或自定义上传函数')
629
+ }
630
+
631
+ // 格式化响应数据
632
+ const formattedResponse = props.responseFormatter(response)
633
+
634
+ uploadFile.status = 'done'
635
+ uploadFile.progress = 100
636
+ uploadFile.response = formattedResponse
637
+ uploadFile.url = formattedResponse.url || uploadFile.url
638
+
639
+ emit('success', formattedResponse, uploadFile)
640
+ } catch (error) {
641
+ uploadFile.status = 'error'
642
+ uploadFile.error = error as Error
643
+ emit('error', error as Error, uploadFile)
644
+
645
+ uni.showToast({
646
+ title: `${uploadFile.name} 上传失败`,
647
+ icon: 'none'
648
+ })
649
+ } finally {
650
+ emit('after-upload', uploadFile)
651
+ updateFileList()
652
+ }
653
+ }
654
+
655
+ // 默认上传实现
656
+ const defaultUpload = (uploadFile: UploadFile): Promise<any> => {
657
+ const headers = {
658
+ ...props.headers,
659
+ 'Authorization': `Bearer ${props.accessToken}`,
660
+ 'AccessToken': props.accessToken,
661
+ }
662
+
663
+ return new Promise((resolve, reject) => {
664
+ uni.uploadFile({
665
+ url: props.action!,
666
+ filePath: uploadFile.rawFile.path,
667
+ name: props.name,
668
+ formData: {
669
+ ...props.data,
670
+ filename: uploadFile.name,
671
+ size: uploadFile.size,
672
+ type: uploadFile.type
673
+ },
674
+ header: headers,
675
+ timeout: props.timeout,
676
+ withCredentials: props.withCredentials,
677
+ // onProgressUpdate: (res: any) => {
678
+ // console.log('res.progress',res.progress)
679
+ // uploadFile.progress = res.progress
680
+ // emit('progress', res.progress, uploadFile)
681
+ // updateFileList()
682
+ // },
683
+ success: (res: any) => {
684
+ try {
685
+ const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
686
+ resolve(data)
687
+ } catch (e: any) {
688
+ reject(e)
689
+ }
690
+ },
691
+ complete: () => {
692
+ // 上传完成
693
+ },
694
+ fail: (e: any)=>{
695
+ reject(e)
696
+ }
697
+ })
698
+
699
+ // // 监听上传进度
700
+ // task.onProgressUpdate = (res:any) => {
701
+ // uploadFile.progress = res.progress
702
+ // emit('progress', res.progress, uploadFile)
703
+ // updateFileList()
704
+ // }
705
+
706
+ // 保存任务用于取消
707
+ // uploadTasks.set(uploadFile.uid, task)
708
+
709
+ // // 上传完成后清理
710
+ // task.then(() => {
711
+ // uploadTasks.delete(uploadFile.uid)
712
+ // }).catch(() => {
713
+ // uploadTasks.delete(uploadFile.uid)
714
+ // })
715
+ })
716
+ }
717
+
718
+ // 更新文件列表
719
+ const updateFileList = () => {
720
+ emit('change', [...fileList.value])
721
+ emit('update:modelValue', [...fileList.value])
722
+ }
723
+
724
+ // 处理删除
725
+ const handleRemove = (file: UploadFile, index: number) => {
726
+ if (props.disabled || props.readonly) {
727
+ return
728
+ }
729
+
730
+ // 如果正在上传,先取消上传
731
+ if (file.status === 'uploading') {
732
+ const task = uploadTasks.get(file.uid)
733
+ if (task) {
734
+ task.abort()
735
+ uploadTasks.delete(file.uid)
736
+ }
737
+ }
738
+
739
+ fileList.value.splice(index, 1)
740
+ emit('remove', file, index)
741
+ updateFileList()
742
+ }
743
+
744
+ // 处理重试
745
+ const handleRetry = async (file: UploadFile, index: number) => {
746
+ if (props.disabled || props.readonly) {
747
+ return
748
+ }
749
+
750
+ await startUpload(file)
751
+ }
752
+
753
+ // 处理预览
754
+ const handlePreview = (file: UploadFile) => {
755
+ if (!props.previewable) {
756
+ return
757
+ }
758
+
759
+ emit('preview', file)
760
+
761
+ // 如果是图片,使用 uni.previewImage
762
+ if (file.url && file.type?.startsWith('image/')) {
763
+ uni.previewImage({
764
+ urls: [file.url],
765
+ current: file.url
766
+ })
767
+ }
768
+ }
769
+
770
+ // 处理拖拽
771
+ const handleDragOver = (e: any) => {
772
+ if (props.type === 'drag' && !props.disabled && !props.readonly) {
773
+ dragOver.value = true
774
+ }
775
+ }
776
+
777
+ const handleDragLeave = (e: any) => {
778
+ dragOver.value = false
779
+ }
780
+
781
+ // 处理全部上传
782
+ const handleUploadAll = async () => {
783
+ if (props.disabled || props.readonly || !props.action) {
784
+ return
785
+ }
786
+
787
+ const pendingFiles = fileList.value.filter(file =>
788
+ file.status === 'pending' || file.status === 'error'
789
+ )
790
+
791
+ if (pendingFiles.length === 0) {
792
+ uni.showToast({
793
+ title: '没有需要上传的文件',
794
+ icon: 'none'
795
+ })
796
+ return
797
+ }
798
+
799
+ uploadingAll.value = true
800
+
801
+ for (const file of pendingFiles) {
802
+ await startUpload(file)
803
+ }
804
+
805
+ uploadingAll.value = false
806
+ }
807
+
808
+ // 处理清空列表
809
+ const handleClearAll = () => {
810
+ if (props.disabled || props.readonly) {
811
+ return
812
+ }
813
+
814
+ // 取消所有进行中的上传
815
+ uploadTasks.forEach(task => task.abort())
816
+ uploadTasks.clear()
817
+
818
+ fileList.value = []
819
+ updateFileList()
820
+
821
+ uni.showToast({
822
+ title: '已清空文件列表',
823
+ icon: 'success'
824
+ })
825
+ }
826
+
827
+ // 手动上传文件
828
+ const upload = async (file: File) => {
829
+ const uploadFile: UploadFile = {
830
+ uid: generateUid(),
831
+ name: file.name,
832
+ size: file.size,
833
+ type: file.type,
834
+ status: 'pending',
835
+ progress: 0,
836
+ file: file
837
+ }
838
+
839
+ if (!props.multiple) {
840
+ fileList.value = [uploadFile]
841
+ } else {
842
+ fileList.value.push(uploadFile)
843
+ }
844
+
845
+ emit('select', file)
846
+ updateFileList()
847
+
848
+ await startUpload(uploadFile)
849
+ }
850
+
851
+ // 清空文件列表
852
+ const clearFiles = () => {
853
+ handleClearAll()
854
+ }
855
+
856
+ // 取消指定上传
857
+ const abortUpload = (uid: string) => {
858
+ const task = uploadTasks.get(uid)
859
+ if (task) {
860
+ task.abort()
861
+ uploadTasks.delete(uid)
862
+
863
+ const file = fileList.value.find(f => f.uid === uid)
864
+ if (file) {
865
+ file.status = 'error'
866
+ file.error = new Error('上传已取消')
867
+ updateFileList()
868
+ }
869
+ }
870
+ }
871
+
872
+ // 暴露方法给父组件
873
+ defineExpose({
874
+ upload,
875
+ clearFiles,
876
+ abortUpload,
877
+ uploadAll: handleUploadAll,
878
+ getFiles: () => [...fileList.value]
879
+ })
880
+ </script>
881
+
882
+ <style lang="scss" scoped>
883
+ .im-upload__area {
884
+ position: relative;
885
+
886
+ &--disabled {
887
+ opacity: 0.6;
888
+ pointer-events: none;
889
+ }
890
+
891
+ &--drag-over {
892
+ border-color: #409eff !important;
893
+ background-color: #ecf5ff !important;
894
+ }
895
+ }
896
+
897
+ .im-upload__button-content {
898
+ display: flex;
899
+ align-items: center;
900
+ justify-content: center;
901
+ gap: 8rpx;
902
+ }
903
+
904
+ .im-upload__icon {
905
+ font-size: 32rpx;
906
+ font-weight: bold;
907
+ }
908
+
909
+ .im-upload__text {
910
+ font-size: 28rpx;
911
+ }
912
+
913
+ .im-upload__card {
914
+ display: flex;
915
+ flex-direction: column;
916
+ align-items: center;
917
+ justify-content: center;
918
+ padding: 60rpx 40rpx;
919
+ border: 2rpx dashed #dcdfe6;
920
+ border-radius: 8rpx;
921
+ background-color: #fafafa;
922
+ transition: all 0.3s;
923
+
924
+ &:active {
925
+ border-color: #409eff;
926
+ background-color: #ecf5ff;
927
+ }
928
+ }
929
+
930
+ .im-upload__card-icon {
931
+ font-size: 48rpx;
932
+ color: #8c939d;
933
+ margin-bottom: 20rpx;
934
+ }
935
+
936
+ .im-upload__card-text {
937
+ font-size: 28rpx;
938
+ color: #606266;
939
+ }
940
+
941
+ .im-upload__card-hint {
942
+ font-size: 24rpx;
943
+ color: #909399;
944
+ margin-top: 12rpx;
945
+ }
946
+
947
+ .im-upload__avatar {
948
+ display: flex;
949
+ flex-direction: column;
950
+ align-items: center;
951
+ gap: 20rpx;
952
+ }
953
+
954
+ .im-upload__avatar-icon {
955
+ font-size: 40rpx;
956
+ color: #8c939d;
957
+ }
958
+
959
+ .im-upload__avatar-text {
960
+ font-size: 26rpx;
961
+ color: #606266;
962
+ }
963
+
964
+ .im-upload__drag {
965
+ display: flex;
966
+ flex-direction: column;
967
+ align-items: center;
968
+ justify-content: center;
969
+ padding: 80rpx 40rpx;
970
+ border: 2rpx dashed #dcdfe6;
971
+ border-radius: 8rpx;
972
+ background-color: #fafafa;
973
+ transition: all 0.3s;
974
+
975
+ &:active {
976
+ border-color: #409eff;
977
+ background-color: #ecf5ff;
978
+ }
979
+ }
980
+
981
+ .im-upload__drag-icon {
982
+ font-size: 60rpx;
983
+ margin-bottom: 20rpx;
984
+ }
985
+
986
+ .im-upload__drag-text {
987
+ font-size: 28rpx;
988
+ color: #606266;
989
+ margin-bottom: 12rpx;
990
+ }
991
+
992
+ .im-upload__drag-hint {
993
+ font-size: 24rpx;
994
+ color: #909399;
995
+ }
996
+
997
+ .im-upload__list {
998
+ margin-top: 32rpx;
999
+ }
1000
+
1001
+ .im-upload__item {
1002
+ &--picture {
1003
+ margin-bottom: 24rpx;
1004
+ }
1005
+
1006
+ &--text {
1007
+ display: flex;
1008
+ justify-content: space-between;
1009
+ align-items: center;
1010
+ padding: 24rpx;
1011
+ margin-bottom: 16rpx;
1012
+ border: 1rpx solid #ebeef5;
1013
+ border-radius: 6rpx;
1014
+ background-color: #f5f7fa;
1015
+ }
1016
+ }
1017
+
1018
+ .im-upload__item-preview {
1019
+ position: relative;
1020
+ width: 200rpx;
1021
+ height: 200rpx;
1022
+ border-radius: 8rpx;
1023
+ overflow: hidden;
1024
+ border: 1rpx solid #ebeef5;
1025
+ }
1026
+
1027
+ .im-upload__item-image {
1028
+ width: 100%;
1029
+ height: 100%;
1030
+ }
1031
+
1032
+ .im-upload__item-file {
1033
+ display: flex;
1034
+ flex-direction: column;
1035
+ align-items: center;
1036
+ justify-content: center;
1037
+ width: 100%;
1038
+ height: 100%;
1039
+ background-color: #f5f7fa;
1040
+ }
1041
+
1042
+ .im-upload__item-file-icon {
1043
+ font-size: 60rpx;
1044
+ margin-bottom: 12rpx;
1045
+ }
1046
+
1047
+ .im-upload__item-file-name {
1048
+ font-size: 24rpx;
1049
+ color: #606266;
1050
+ text-align: center;
1051
+ padding: 0 16rpx;
1052
+ word-break: break-all;
1053
+ }
1054
+
1055
+ .im-upload__item-status {
1056
+ position: absolute;
1057
+ bottom: 0;
1058
+ left: 0;
1059
+ right: 0;
1060
+ padding: 16rpx;
1061
+ background-color: rgba(0, 0, 0, 0.6);
1062
+ color: white;
1063
+ font-size: 24rpx;
1064
+ }
1065
+
1066
+ .im-upload__item-progress {
1067
+ width: 100%;
1068
+ height: 8rpx;
1069
+ background-color: rgba(255, 255, 255, 0.3);
1070
+ border-radius: 4rpx;
1071
+ overflow: hidden;
1072
+ margin-bottom: 8rpx;
1073
+ }
1074
+
1075
+ .im-upload__item-progress-bar {
1076
+ height: 100%;
1077
+ background-color: #409eff;
1078
+ transition: width 0.3s;
1079
+ }
1080
+
1081
+ .im-upload__item-percent {
1082
+ font-size: 22rpx;
1083
+ }
1084
+
1085
+ .im-upload__item-error {
1086
+ position: absolute;
1087
+ bottom: 0;
1088
+ left: 0;
1089
+ right: 0;
1090
+ padding: 16rpx;
1091
+ background-color: rgba(255, 77, 79, 0.8);
1092
+ color: white;
1093
+ font-size: 24rpx;
1094
+ display: flex;
1095
+ justify-content: space-between;
1096
+ align-items: center;
1097
+ }
1098
+
1099
+ .im-upload__item-error-text {
1100
+ font-size: 22rpx;
1101
+ }
1102
+
1103
+ .im-upload__item-retry {
1104
+ font-size: 22rpx;
1105
+ text-decoration: underline;
1106
+ cursor: pointer;
1107
+ }
1108
+
1109
+ .im-upload__item-actions {
1110
+ position: absolute;
1111
+ top: 8rpx;
1112
+ right: 8rpx;
1113
+ display: flex;
1114
+ gap: 8rpx;
1115
+ opacity: 0;
1116
+ transition: opacity 0.3s;
1117
+ }
1118
+
1119
+ .im-upload__item-preview:hover .im-upload__item-actions {
1120
+ opacity: 1;
1121
+ }
1122
+
1123
+ .im-upload__item-action {
1124
+ display: flex;
1125
+ align-items: center;
1126
+ justify-content: center;
1127
+ width: 48rpx;
1128
+ height: 48rpx;
1129
+ background-color: rgba(0, 0, 0, 0.6);
1130
+ color: white;
1131
+ border-radius: 50%;
1132
+ font-size: 24rpx;
1133
+ cursor: pointer;
1134
+
1135
+ &:active {
1136
+ background-color: rgba(0, 0, 0, 0.8);
1137
+ }
1138
+ }
1139
+
1140
+ .im-upload__item-text {
1141
+ flex: 1;
1142
+ }
1143
+
1144
+ .im-upload__item-name {
1145
+ display: block;
1146
+ font-size: 28rpx;
1147
+ color: #303133;
1148
+ margin-bottom: 8rpx;
1149
+ word-break: break-all;
1150
+ }
1151
+
1152
+ .im-upload__item-size {
1153
+ display: block;
1154
+ font-size: 24rpx;
1155
+ color: #909399;
1156
+ margin-bottom: 4rpx;
1157
+ }
1158
+
1159
+ .im-upload__item-status-container {
1160
+ display: flex;
1161
+ align-items: center;
1162
+ gap: 16rpx;
1163
+ }
1164
+
1165
+ .im-upload__item-status-text {
1166
+ font-size: 24rpx;
1167
+
1168
+ &--pending {
1169
+ color: #909399;
1170
+ }
1171
+
1172
+ &--uploading {
1173
+ color: #409eff;
1174
+ }
1175
+
1176
+ &--done {
1177
+ color: #52c41a;
1178
+ }
1179
+
1180
+ &--error {
1181
+ color: #ff4d4f;
1182
+ }
1183
+ }
1184
+
1185
+ .im-upload__item-percent-text {
1186
+ font-size: 22rpx;
1187
+ color: #409eff;
1188
+ }
1189
+
1190
+ .im-upload__item-text-actions {
1191
+ display: flex;
1192
+ gap: 24rpx;
1193
+ }
1194
+
1195
+ .im-upload__item-text-action {
1196
+ font-size: 26rpx;
1197
+ color: #409eff;
1198
+ cursor: pointer;
1199
+
1200
+ &:active {
1201
+ color: #337ecc;
1202
+ }
1203
+
1204
+ &--remove {
1205
+ color: #ff4d4f;
1206
+
1207
+ &:active {
1208
+ color: #d9363e;
1209
+ }
1210
+ }
1211
+ }
1212
+
1213
+ .im-upload__tip {
1214
+ margin-top: 16rpx;
1215
+ display: flex;
1216
+ justify-content: space-between;
1217
+ align-items: center;
1218
+ }
1219
+
1220
+ .im-upload__tip-text {
1221
+ font-size: 24rpx;
1222
+ color: #909399;
1223
+ }
1224
+
1225
+ .im-upload__tip-count {
1226
+ font-size: 22rpx;
1227
+ color: #409eff;
1228
+ }
1229
+
1230
+ .im-upload__actions {
1231
+ margin-top: 24rpx;
1232
+ display: flex;
1233
+ gap: 20rpx;
1234
+ justify-content: flex-end;
1235
+ }
1236
+ </style>