From d23b2bf2c8ae31888b3a642f97d016ffc424f8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E5=A6=82=E6=91=B8=E9=B1=BC=E5=8E=BB?= <1780903673@qq.com> Date: Wed, 26 Mar 2025 22:57:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E2=99=BB=EF=B8=8F=20=20useUpload?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=E9=80=89=E6=8B=A9=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=9A=84api=20(#976)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/component/use-upload.md | 25 +- jest.config.js | 12 +- .../components/composables/useUpload.ts | 154 +++++++++- .../components/wd-upload/utils.ts | 152 ---------- .../components/wd-upload/wd-upload.vue | 5 +- tests/composables/useUpload.spec.ts | 278 +++++++++++++++++- 6 files changed, 464 insertions(+), 162 deletions(-) delete mode 100644 src/uni_modules/wot-design-uni/components/wd-upload/utils.ts diff --git a/docs/component/use-upload.md b/docs/component/use-upload.md index 59eca6e7..cd1da9ab 100644 --- a/docs/component/use-upload.md +++ b/docs/component/use-upload.md @@ -1,13 +1,20 @@ # useUpload -用于处理文件上传相关的逻辑。 +用于处理文件上传和选择相关的逻辑。 ## 基础用法 ```ts import { useUpload } from '@/uni_modules/wot-design-uni' -const { startUpload, abort, UPLOAD_STATUS } = useUpload() +const { startUpload, abort, chooseFile, UPLOAD_STATUS } = useUpload() + +// 选择文件 +const files = await chooseFile({ + accept: 'image', + multiple: true, + maxCount: 9 +}) // 开始上传 const file = { @@ -41,6 +48,7 @@ abort() |-------|------|------|--------| | startUpload | 开始上传文件 | file: UploadFileItem, options: UseUploadOptions | UniApp.UploadTask \| void | | abort | 中断上传 | task?: UniApp.UploadTask | void | +| chooseFile | 选择文件 | options: ChooseFileOption | Promise | ### UseUploadOptions @@ -56,3 +64,16 @@ abort() | onSuccess | 上传成功回调 | Function | - | | onError | 上传失败回调 | Function | - | | onProgress | 上传进度回调 | Function | - | + +### ChooseFileOption + +| 参数 | 说明 | 类型 | 默认值 | +|-----|------|------|--------| +| multiple | 是否支持多选文件 | boolean | false | +| sizeType | 所选的图片的尺寸 | Array | ['original', 'compressed'] | +| sourceType | 选择文件的来源 | Array | ['album', 'camera'] | +| maxCount | 最大可选数量 | number | 9 | +| accept | 接受的文件类型 | 'image' \| 'video' \| 'media' \| 'file' \| 'all' | 'image' | +| compressed | 是否压缩视频 | boolean | true | +| maxDuration | 视频最大时长(秒) | number | 60 | +| camera | 摄像头朝向 | 'back' \| 'front' | 'back' | diff --git a/jest.config.js b/jest.config.js index a47e8003..0090849e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,7 @@ /* * @Author: weisheng * @Date: 2025-03-24 12:33:15 - * @LastEditTime: 2025-03-24 22:02:51 + * @LastEditTime: 2025-03-26 13:29:54 * @LastEditors: weisheng * @Description: * @FilePath: /wot-design-uni/jest.config.js @@ -21,6 +21,16 @@ module.exports = { }, testRegex: '(/tests/.*\\.spec)\\.(jsx?|tsx?)$', // 只匹配 .spec 结尾的文件 testPathIgnorePatterns: ['/node_modules/', '/tests/setup.js', '/tests/__mocks__/'], + + // 添加 modulePathIgnorePatterns 配置,忽略 uni_modules 下的 package.json + modulePathIgnorePatterns: ['/src/uni_modules/.*', '/src/uni_modules/wot-design-uni/package.json'], + + // 或者使用 watchPathIgnorePatterns + watchPathIgnorePatterns: ['/src/uni_modules/'], + + // 增加全局超时时间设置 + testTimeout: 10000, // 设置为10秒 + globals: { 'vue-jest': { compilerOptions: { diff --git a/src/uni_modules/wot-design-uni/components/composables/useUpload.ts b/src/uni_modules/wot-design-uni/components/composables/useUpload.ts index 65aecd61..0c951530 100644 --- a/src/uni_modules/wot-design-uni/components/composables/useUpload.ts +++ b/src/uni_modules/wot-design-uni/components/composables/useUpload.ts @@ -1,5 +1,5 @@ -import { isFunction } from '../common/util' -import type { UploadFileItem, UploadMethod, UploadStatusType } from '../wd-upload/types' +import { isArray, isDef, isFunction } from '../common/util' +import type { ChooseFile, ChooseFileOption, UploadFileItem, UploadMethod, UploadStatusType } from '../wd-upload/types' export const UPLOAD_STATUS: Record = { PENDING: 'pending', @@ -15,6 +15,8 @@ export interface UseUploadReturn { abort: (task?: UniApp.UploadTask) => void // 上传状态常量 UPLOAD_STATUS: Record + // 选择文件 + chooseFile: (options: ChooseFileOption) => Promise } export interface UseUploadOptions { @@ -155,9 +157,155 @@ export function useUpload(): UseUploadReturn { } } + /** + * 格式化图片信息 + */ + function formatImage(res: UniApp.ChooseImageSuccessCallbackResult): ChooseFile[] { + // #ifdef MP-DINGTALK + // 钉钉文件在files中 + res.tempFiles = isDef((res as any).files) ? (res as any).files : res.tempFiles + // #endif + if (isArray(res.tempFiles)) { + return res.tempFiles.map((item: any) => ({ + path: item.path || '', + name: item.name || '', + size: item.size, + type: 'image', + thumb: item.path || '' + })) + } + return [ + { + path: (res.tempFiles as any).path || '', + name: (res.tempFiles as any).name || '', + size: (res.tempFiles as any).size, + type: 'image', + thumb: (res.tempFiles as any).path || '' + } + ] + } + + /** + * 格式化视频信息 + */ + function formatVideo(res: UniApp.ChooseVideoSuccess): ChooseFile[] { + return [ + { + path: res.tempFilePath || (res as any).filePath || '', + name: res.name || '', + size: res.size, + type: 'video', + thumb: (res as any).thumbTempFilePath || '', + duration: res.duration + } + ] + } + + /** + * 格式化媒体信息 + */ + function formatMedia(res: UniApp.ChooseMediaSuccessCallbackResult): ChooseFile[] { + return res.tempFiles.map((item) => ({ + type: item.fileType, + path: item.tempFilePath, + thumb: item.fileType === 'video' ? item.thumbTempFilePath : item.tempFilePath, + size: item.size, + duration: item.duration + })) + } + + /** + * 选择文件 + */ + function chooseFile({ + multiple, + sizeType, + sourceType, + maxCount, + accept, + compressed, + maxDuration, + camera + }: ChooseFileOption): Promise { + return new Promise((resolve, reject) => { + switch (accept) { + case 'image': + uni.chooseImage({ + count: multiple ? Math.min(maxCount, 9) : 1, + sizeType, + sourceType, + success: (res) => resolve(formatImage(res)), + fail: reject + }) + break + case 'video': + uni.chooseVideo({ + sourceType, + compressed, + maxDuration, + camera, + success: (res) => resolve(formatVideo(res)), + fail: reject + }) + break + // #ifdef MP-WEIXIN + case 'media': + uni.chooseMedia({ + count: multiple ? Math.min(maxCount, 9) : 1, + sourceType, + sizeType, + camera, + maxDuration, + success: (res) => resolve(formatMedia(res)), + fail: reject + }) + break + case 'file': + uni.chooseMessageFile({ + count: multiple ? Math.min(maxCount, 100) : 1, + type: accept, + success: (res) => resolve(res.tempFiles), + fail: reject + }) + break + // #endif + case 'all': + // #ifdef H5 + uni.chooseFile({ + count: multiple ? Math.min(maxCount, 100) : 1, + type: accept, + success: (res) => resolve(res.tempFiles as ChooseFile[]), + fail: reject + }) + // #endif + // #ifdef MP-WEIXIN + uni.chooseMessageFile({ + count: multiple ? Math.min(maxCount, 100) : 1, + type: accept, + success: (res) => resolve(res.tempFiles), + fail: reject + }) + // #endif + + break + default: + // 默认选择图片 + uni.chooseImage({ + count: multiple ? Math.min(maxCount, 9) : 1, + sizeType, + sourceType, + success: (res) => resolve(formatImage(res)), + fail: reject + }) + break + } + }) + } + return { startUpload, abort, - UPLOAD_STATUS + UPLOAD_STATUS, + chooseFile } } diff --git a/src/uni_modules/wot-design-uni/components/wd-upload/utils.ts b/src/uni_modules/wot-design-uni/components/wd-upload/utils.ts deleted file mode 100644 index 63ef4251..00000000 --- a/src/uni_modules/wot-design-uni/components/wd-upload/utils.ts +++ /dev/null @@ -1,152 +0,0 @@ -/* - * @Author: weisheng - * @Date: 2024-03-18 22:36:44 - * @LastEditTime: 2024-12-07 18:08:48 - * @LastEditors: weisheng - * @Description: - * @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-upload/utils.ts - * 记得注释 - */ -import { isArray, isDef } from '../common/util' -import type { ChooseFile, ChooseFileOption } from './types' - -function formatImage(res: UniApp.ChooseImageSuccessCallbackResult): ChooseFile[] { - // #ifdef MP-DINGTALK - // 钉钉文件在files中 - res.tempFiles = isDef((res as any).files) ? (res as any).files : res.tempFiles - // #endif - if (isArray(res.tempFiles)) { - const result: ChooseFile[] = [] - res.tempFiles.forEach(async (item: any) => { - result.push({ - path: item.path || '', - name: item.name || '', - size: item.size, - type: 'image', - thumb: item.path || '' - }) - }) - return result - } else { - return [ - { - path: (res.tempFiles as any).path || '', - name: (res.tempFiles as any).name || '', - size: (res.tempFiles as any).size, - type: 'image', - thumb: (res.tempFiles as any).path || '' - } - ] - } -} - -function formatVideo(res: UniApp.ChooseVideoSuccess): ChooseFile[] { - return [ - { - path: res.tempFilePath || (res as any).filePath || '', - name: res.name || '', - size: res.size, - type: 'video', - thumb: (res as any).thumbTempFilePath || '', - duration: res.duration - } - ] -} - -function formatMedia(res: UniApp.ChooseMediaSuccessCallbackResult): ChooseFile[] { - return res.tempFiles.map((item) => ({ - type: item.fileType, - path: item.tempFilePath, - thumb: item.fileType === 'video' ? item.thumbTempFilePath : item.tempFilePath, - size: item.size, - duration: item.duration - })) -} -export function chooseFile({ - multiple, - sizeType, - sourceType, - maxCount, - accept, - compressed, - maxDuration, - camera -}: ChooseFileOption): Promise { - return new Promise((resolve, reject) => { - switch (accept) { - case 'image': - uni.chooseImage({ - count: multiple ? Math.min(maxCount, 9) : 1, // 最多可以选择的数量,如果不支持多选则数量为1 - sizeType, - sourceType, - success: (res) => resolve(formatImage(res)), - fail: (error) => { - reject(error) - } - }) - break - case 'video': - uni.chooseVideo({ - sourceType, - compressed, - maxDuration, - camera, - success: (res) => { - resolve(formatVideo(res)) - }, - fail: reject - }) - - break - // #ifdef MP-WEIXIN - case 'media': - uni.chooseMedia({ - count: multiple ? Math.min(maxCount, 9) : 1, - sourceType, - sizeType, - camera, - maxDuration, - success: (res) => resolve(formatMedia(res)), - fail: reject - }) - break - case 'file': - uni.chooseMessageFile({ - count: multiple ? Math.min(maxCount, 100) : 1, - type: accept, - success: (res) => resolve(res.tempFiles), - fail: reject - }) - break - // #endif - case 'all': - // #ifdef MP-WEIXIN - uni.chooseMessageFile({ - count: multiple ? Math.min(maxCount, 100) : 1, - type: accept, - success: (res) => resolve(res.tempFiles), - fail: reject - }) - // #endif - // #ifdef H5 - uni.chooseFile({ - count: multiple ? Math.min(maxCount, 100) : 1, - type: accept, - success: (res) => resolve(res.tempFiles as ChooseFile[]), - fail: reject - }) - // #endif - break - default: - // 默认选择图片 - uni.chooseImage({ - count: multiple ? Math.min(maxCount, 9) : 1, // 最多可以选择的数量,如果不支持多选则数量为1 - sizeType, - sourceType, - success: (res) => resolve(formatImage(res)), - fail: reject - }) - break - } - }) -} diff --git a/src/uni_modules/wot-design-uni/components/wd-upload/wd-upload.vue b/src/uni_modules/wot-design-uni/components/wd-upload/wd-upload.vue index 355290ab..107b4b8b 100644 --- a/src/uni_modules/wot-design-uni/components/wd-upload/wd-upload.vue +++ b/src/uni_modules/wot-design-uni/components/wd-upload/wd-upload.vue @@ -102,7 +102,6 @@ import wdLoading from '../wd-loading/wd-loading.vue' import { computed, ref, watch } from 'vue' import { context, isEqual, isImageUrl, isVideoUrl, isFunction, isDef, deepClone } from '../common/util' -import { chooseFile } from './utils' import { useTranslate } from '../composables/useTranslate' import { useUpload } from '../composables/useUpload' import { @@ -146,7 +145,7 @@ const showUpload = computed(() => !props.limit || uploadFiles.value.length < pro const videoPreview = ref() -const { startUpload, abort, UPLOAD_STATUS } = useUpload() +const { startUpload, abort, chooseFile, UPLOAD_STATUS } = useUpload() watch( () => props.fileList, @@ -411,7 +410,7 @@ function handleProgress(res: UniApp.OnProgressUpdateResult, file: UploadFileItem */ function onChooseFile(currentIndex?: number) { const { multiple, maxSize, accept, sizeType, limit, sourceType, compressed, maxDuration, camera, beforeUpload } = props - // 文件选择 + chooseFile({ multiple, sizeType, diff --git a/tests/composables/useUpload.spec.ts b/tests/composables/useUpload.spec.ts index 5138d4eb..3ed08e1c 100644 --- a/tests/composables/useUpload.spec.ts +++ b/tests/composables/useUpload.spec.ts @@ -22,7 +22,7 @@ const mockUploadTask = { } as any describe('useUpload', () => { - const { startUpload, abort, UPLOAD_STATUS } = useUpload() + const { startUpload, abort, chooseFile, UPLOAD_STATUS } = useUpload() beforeEach(() => { jest.clearAllMocks() @@ -284,4 +284,280 @@ describe('useUpload', () => { file ) }) + + // 测试选择图片文件 + it('should choose image files', async () => { + const mockChooseImage = jest.fn().mockImplementation((options) => { + options.success({ + tempFiles: [ + { path: 'temp/image1.jpg', size: 1024, name: 'image1.jpg' }, + { path: 'temp/image2.jpg', size: 2048, name: 'image2.jpg' } + ] + }) + }) + ;(global as any).uni.chooseImage = mockChooseImage + + const files = await chooseFile({ + accept: 'image', + multiple: true, + maxCount: 9, + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + compressed: true, + maxDuration: 60, + camera: 'back' + }) + + expect(files).toHaveLength(2) + expect(files[0]).toEqual({ + path: 'temp/image1.jpg', + size: 1024, + name: 'image1.jpg', + type: 'image', + thumb: 'temp/image1.jpg' + }) + }) + + // 测试选择视频文件 + it('should choose video file', async () => { + const mockChooseVideo = jest.fn().mockImplementation((options) => { + options.success({ + tempFilePath: 'temp/video.mp4', + size: 10240, + duration: 15, + thumbTempFilePath: 'temp/thumb.jpg', + name: 'video.mp4' + }) + }) + ;(global as any).uni.chooseVideo = mockChooseVideo + + const files = await chooseFile({ + accept: 'video', + multiple: false, + maxCount: 1, + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + compressed: true, + maxDuration: 60, + camera: 'back' + }) + + expect(files).toHaveLength(1) + expect(files[0]).toEqual({ + path: 'temp/video.mp4', + size: 10240, + name: 'video.mp4', + type: 'video', + thumb: 'temp/thumb.jpg', + duration: 15 + }) + }) + + // 测试选择媒体文件 + it('should choose media files', async () => { + const mockChooseMedia = jest.fn().mockImplementation((options) => { + options.success({ + tempFiles: [ + { + fileType: 'image', + tempFilePath: 'temp/image.jpg', + size: 1024, + duration: 0 + }, + { + fileType: 'video', + tempFilePath: 'temp/video.mp4', + thumbTempFilePath: 'temp/thumb.jpg', + size: 10240, + duration: 15 + } + ] + }) + }) + ;(global as any).uni.chooseMedia = mockChooseMedia + + const files = await chooseFile({ + accept: 'media', + multiple: true, + maxCount: 9, + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + compressed: true, + maxDuration: 60, + camera: 'back' + }) + + expect(files).toHaveLength(2) + expect(files[1]).toEqual({ + type: 'video', + path: 'temp/video.mp4', + thumb: 'temp/thumb.jpg', + size: 10240, + duration: 15 + }) + }) + + // 测试选择文件失败的情况 + it('should handle choose file failure', async () => { + const mockChooseImage = jest.fn().mockImplementation((options) => { + options.fail(new Error('Permission denied')) + }) + ;(global as any).uni.chooseImage = mockChooseImage + + await expect( + chooseFile({ + accept: 'image', + multiple: false, + maxCount: 1, + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + compressed: true, + maxDuration: 60, + camera: 'back' + }) + ).rejects.toThrow('Permission denied') + }) + + // 测试多选限制 + it('should respect maxCount limit', async () => { + const mockChooseImage = jest.fn().mockImplementation((options) => { + options.success({ + tempFiles: [{ path: 'temp/image1.jpg', size: 1024, name: 'image1.jpg' }] + }) + }) + ;(global as any).uni.chooseImage = mockChooseImage + + await chooseFile({ + accept: 'image', + multiple: true, + maxCount: 3, + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + compressed: true, + maxDuration: 60, + camera: 'back' + }) + + expect(mockChooseImage).toHaveBeenCalledWith( + expect.objectContaining({ + count: 3, + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'] + }) + ) + + // 确保异步操作完成 + await new Promise((resolve) => setTimeout(resolve, 0)) + }) + + // 测试文件来源限制 + it('should respect sourceType option', async () => { + const mockChooseImage = jest.fn().mockImplementation((options) => { + // 立即调用 success 回调,返回测试数据 + options.success({ + tempFiles: [{ path: 'temp/image1.jpg', size: 1024, name: 'image1.jpg' }] + }) + }) + ;(global as any).uni.chooseImage = mockChooseImage + + await chooseFile({ + accept: 'image', + multiple: false, + maxCount: 1, + sizeType: ['original', 'compressed'], + sourceType: ['camera'], + compressed: true, + maxDuration: 60, + camera: 'back' + }) + + expect(mockChooseImage).toHaveBeenCalledWith( + expect.objectContaining({ + count: 1, + sizeType: ['original', 'compressed'], + sourceType: ['camera'] + }) + ) + }) + + // 测试选择消息文件(仅微信小程序) + // it('should choose message file in WeChat MP', async () => { + // const mockChooseMessageFile = jest.fn().mockImplementation((options) => { + // options.success({ + // tempFiles: [ + // { + // path: 'temp/doc.pdf', + // size: 1024, + // name: 'doc.pdf', + // type: 'file' + // } + // ] + // }) + // }) + // ;(global as any).uni.chooseMessageFile = mockChooseMessageFile + + // const files = await chooseFile({ + // accept: 'file', + // multiple: true, + // maxCount: 100, + // sizeType: ['original', 'compressed'], + // sourceType: ['album', 'camera'], + // compressed: true, + // maxDuration: 60, + // camera: 'back' + // }) + + // expect(mockChooseMessageFile).toHaveBeenCalledWith( + // expect.objectContaining({ + // count: 100, + // type: 'file' + // }) + // ) + // expect(files[0]).toEqual({ + // path: 'temp/doc.pdf', + // size: 1024, + // name: 'doc.pdf', + // type: 'file' + // }) + // }) + + // 测试选择全部类型文件(H5) + it('should choose all type files in H5', async () => { + const mockChooseFile = jest.fn().mockImplementation((options) => { + options.success({ + tempFiles: [ + { + path: 'temp/file.txt', + size: 512, + name: 'file.txt' + // 移除 type 属性,因为 H5 的 chooseFile 返回的数据不包含 type + } + ] + }) + }) + ;(global as any).uni.chooseFile = mockChooseFile + + const files = await chooseFile({ + accept: 'all', + multiple: true, + maxCount: 100, + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + compressed: true, + maxDuration: 60, + camera: 'back' + }) + + expect(mockChooseFile).toHaveBeenCalledWith( + expect.objectContaining({ + count: 100, + type: 'all' + }) + ) + expect(files[0]).toEqual({ + path: 'temp/file.txt', + size: 512, + name: 'file.txt' + }) + }) })