uploadFile.ts 8.2 KB

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