1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
| import { getEnvBaseUrl } from './index'
| import { toast } from './toast'
|
| /**
| * 文件上传钩子函数使用示例
| * @example
| * const { loading, error, data, progress, run } = useUpload<IUploadResult>(
| * uploadUrl,
| * {},
| * {
| * maxSize: 5, // 最大5MB
| * sourceType: ['album'], // 仅支持从相册选择
| * onProgress: (p) => console.log(`上传进度:${p}%`),
| * onSuccess: (res) => console.log('上传成功', res),
| * onError: (err) => console.error('上传失败', err),
| * },
| * )
| */
|
| /**
| * 上传文件的URL配置
| */
| export const uploadFileUrl = {
| /** 用户头像上传地址(动态读取当前生效的 BaseURL) */
| get USER_AVATAR() {
| return `${getEnvBaseUrl()}/user/avatar`
| },
| }
|
| /**
| * 通用文件上传函数(支持直接传入文件路径)
| * @param url 上传地址
| * @param filePath 本地文件路径
| * @param formData 额外表单数据
| * @param options 上传选项
| */
| export function useFileUpload<T = string>(url: string, filePath: string, formData: Record<string, any> = {}, options: Omit<UploadOptions, 'sourceType' | 'sizeType' | 'count'> = {}) {
| return useUpload<T>(
| url,
| formData,
| {
| ...options,
| sourceType: ['album'],
| sizeType: ['original'],
| },
| filePath,
| )
| }
|
| export interface UploadOptions {
| /** 最大可选择的图片数量,默认为1 */
| count?: number
| /** 所选的图片的尺寸,original-原图,compressed-压缩图 */
| sizeType?: Array<'original' | 'compressed'>
| /** 选择图片的来源,album-相册,camera-相机 */
| sourceType?: Array<'album' | 'camera'>
| /** 文件大小限制,单位:MB */
| maxSize?: number //
| /** 上传进度回调函数 */
| onProgress?: (progress: number) => void
| /** 上传成功回调函数 */
| onSuccess?: (res: Record<string, any>) => void
| /** 上传失败回调函数 */
| onError?: (err: Error | UniApp.GeneralCallbackResult) => void
| /** 上传完成回调函数(无论成功失败) */
| onComplete?: () => void
| }
|
| /**
| * 文件上传钩子函数
| * @template T 上传成功后返回的数据类型
| * @param url 上传地址
| * @param formData 额外的表单数据
| * @param options 上传选项
| * @returns 上传状态和控制对象
| */
| export function useUpload<T = string>(url: string, formData: Record<string, any> = {}, options: UploadOptions = {},
| /** 直接传入文件路径,跳过选择器 */
| directFilePath?: string) {
| /** 上传中状态 */
| const loading = ref(false)
| /** 上传错误状态 */
| const error = ref(false)
| /** 上传成功后的响应数据 */
| const data = ref<T>()
| /** 上传进度(0-100) */
| const progress = ref(0)
|
| /** 解构上传选项,设置默认值 */
| const {
| /** 最大可选择的图片数量 */
| count = 1,
| /** 所选的图片的尺寸 */
| sizeType = ['original', 'compressed'],
| /** 选择图片的来源 */
| sourceType = ['album', 'camera'],
| /** 文件大小限制(MB) */
| maxSize = 10,
| /** 进度回调 */
| onProgress,
| /** 成功回调 */
| onSuccess,
| /** 失败回调 */
| onError,
| /** 完成回调 */
| onComplete,
| } = options
|
| /**
| * 检查文件大小是否超过限制
| * @param size 文件大小(字节)
| * @returns 是否通过检查
| */
| const checkFileSize = (size: number) => {
| const sizeInMB = size / 1024 / 1024
| if (sizeInMB > maxSize) {
| toast.warning(`文件大小不能超过${maxSize}MB`)
| return false
| }
| return true
| }
| /**
| * 触发文件选择和上传
| * 根据平台使用不同的选择器:
| * - 微信小程序使用 chooseMedia
| * - 其他平台使用 chooseImage
| */
| const run = () => {
| if (directFilePath) {
| // 直接使用传入的文件路径
| loading.value = true
| progress.value = 0
| uploadFile<T>({
| url,
| tempFilePath: directFilePath,
| formData,
| data,
| error,
| loading,
| progress,
| onProgress,
| onSuccess,
| onError,
| onComplete,
| })
| return
| }
|
| // #ifdef MP-WEIXIN
| // 微信小程序环境下使用 chooseMedia API
| uni.chooseMedia({
| count,
| mediaType: ['image'], // 仅支持图片类型
| sourceType,
| success: (res) => {
| const file = res.tempFiles[0]
| // 检查文件大小是否符合限制
| if (!checkFileSize(file.size))
| return
|
| // 开始上传
| loading.value = true
| progress.value = 0
| uploadFile<T>({
| url,
| tempFilePath: file.tempFilePath,
| formData,
| data,
| error,
| loading,
| progress,
| onProgress,
| onSuccess,
| onError,
| onComplete,
| })
| },
| fail: (err) => {
| console.error('选择媒体文件失败:', err)
| error.value = true
| onError?.(err)
| },
| })
| // #endif
|
| // #ifndef MP-WEIXIN
| // 非微信小程序环境下使用 chooseImage API
| uni.chooseImage({
| count,
| sizeType,
| sourceType,
| success: (res) => {
| console.log('选择图片成功:', res)
|
| // 开始上传
| loading.value = true
| progress.value = 0
| uploadFile<T>({
| url,
| tempFilePath: res.tempFilePaths[0],
| formData,
| data,
| error,
| loading,
| progress,
| onProgress,
| onSuccess,
| onError,
| onComplete,
| })
| },
| fail: (err) => {
| console.error('选择图片失败:', err)
| error.value = true
| onError?.(err)
| },
| })
| // #endif
| }
|
| return { loading, error, data, progress, run }
| }
|
| /**
| * 文件上传选项接口
| * @template T 上传成功后返回的数据类型
| */
| interface UploadFileOptions<T> {
| /** 上传地址 */
| url: string
| /** 临时文件路径 */
| tempFilePath: string
| /** 额外的表单数据 */
| formData: Record<string, any>
| /** 上传成功后的响应数据 */
| data: Ref<T | undefined>
| /** 上传错误状态 */
| error: Ref<boolean>
| /** 上传中状态 */
| loading: Ref<boolean>
| /** 上传进度(0-100) */
| progress: Ref<number>
| /** 上传进度回调 */
| onProgress?: (progress: number) => void
| /** 上传成功回调 */
| onSuccess?: (res: Record<string, any>) => void
| /** 上传失败回调 */
| onError?: (err: Error | UniApp.GeneralCallbackResult) => void
| /** 上传完成回调 */
| onComplete?: () => void
| }
|
| /**
| * 执行文件上传
| * @template T 上传成功后返回的数据类型
| * @param options 上传选项
| */
| function uploadFile<T>({
| url,
| tempFilePath,
| formData,
| data,
| error,
| loading,
| progress,
| onProgress,
| onSuccess,
| onError,
| onComplete,
| }: UploadFileOptions<T>) {
| try {
| // 创建上传任务
| const uploadTask = uni.uploadFile({
| url,
| filePath: tempFilePath,
| name: 'file', // 文件对应的 key
| formData,
| header: {
| // H5环境下不需要手动设置Content-Type,让浏览器自动处理multipart格式
| // #ifndef H5
| 'Content-Type': 'multipart/form-data',
| // #endif
| },
| // 确保文件名称合法
| success: (uploadFileRes) => {
| console.log('上传文件成功:', uploadFileRes)
| try {
| // 解析响应数据
| const { data: _data } = JSON.parse(uploadFileRes.data)
| // 上传成功
| data.value = _data as T
| onSuccess?.(_data)
| }
| catch (err) {
| // 响应解析错误
| console.error('解析上传响应失败:', err)
| error.value = true
| onError?.(new Error('上传响应解析失败'))
| }
| },
| fail: (err) => {
| // 上传请求失败
| console.error('上传文件失败:', err)
| error.value = true
| onError?.(err)
| },
| complete: () => {
| // 无论成功失败都执行
| loading.value = false
| onComplete?.()
| },
| })
|
| // 监听上传进度
| uploadTask.onProgressUpdate((res) => {
| progress.value = res.progress
| onProgress?.(res.progress)
| })
| }
| catch (err) {
| // 创建上传任务失败
| console.error('创建上传任务失败:', err)
| error.value = true
| loading.value = false
| onError?.(new Error('创建上传任务失败'))
| }
| }
|
|