refactor: ♻️ useUpload 添加选择文件的api (#976)

This commit is contained in:
不如摸鱼去 2025-03-26 22:57:54 +08:00 committed by GitHub
parent 1876fc3d0c
commit d23b2bf2c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 464 additions and 162 deletions

View File

@ -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<ChooseFile[]> |
### 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' |

View File

@ -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: ['<rootDir>/src/uni_modules/.*', '<rootDir>/src/uni_modules/wot-design-uni/package.json'],
// 或者使用 watchPathIgnorePatterns
watchPathIgnorePatterns: ['<rootDir>/src/uni_modules/'],
// 增加全局超时时间设置
testTimeout: 10000, // 设置为10秒
globals: {
'vue-jest': {
compilerOptions: {

View File

@ -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<string, UploadStatusType> = {
PENDING: 'pending',
@ -15,6 +15,8 @@ export interface UseUploadReturn {
abort: (task?: UniApp.UploadTask) => void
// 上传状态常量
UPLOAD_STATUS: Record<string, UploadStatusType>
// 选择文件
chooseFile: (options: ChooseFileOption) => Promise<ChooseFile[]>
}
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<ChooseFile[]> {
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
}
}

View File

@ -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<ChooseFile[]> {
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
}
})
}

View File

@ -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<VideoPreviewInstance>()
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,

View File

@ -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'
})
})
})