uploadFile.ts 8.2 KB

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