refactor: 文件/图片上传重构

This commit is contained in:
dap 2025-03-29 15:52:11 +08:00
parent dd57e3c9ae
commit ffcc21975e
7 changed files with 130 additions and 172 deletions

View File

@ -7,22 +7,6 @@ import { requestClient } from '#/api/request';
*/ */
export type AxiosProgressEvent = AxiosRequestConfig['onUploadProgress']; export type AxiosProgressEvent = AxiosRequestConfig['onUploadProgress'];
/**
*
* @param file
* @param onUploadProgress
* @returns
*/
export function uploadApi(
file: Blob | File,
onUploadProgress?: AxiosProgressEvent,
) {
return requestClient.upload(
'/resource/oss/upload',
{ file },
{ onUploadProgress, timeout: 60_000 },
);
}
/** /**
* *
*/ */
@ -31,3 +15,27 @@ export interface UploadResult {
fileName: string; fileName: string;
ossId: string; ossId: string;
} }
/**
*
* @param file
* @param otherData
* @param onUploadProgress
* @returns
*/
export function uploadApi(
file: Blob | File,
otherData?: Record<string, any>,
onUploadProgress?: AxiosProgressEvent,
) {
return requestClient.upload<UploadResult>(
'/resource/oss/upload',
{ file, ...otherData },
{ onUploadProgress, timeout: 60_000 },
);
}
/**
* api type
*/
export type UploadApi = typeof uploadApi;

View File

@ -5,6 +5,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { UploadFile } from 'ant-design-vue'; import type { UploadFile } from 'ant-design-vue';
import type { BaseUploadProps } from './props';
import { computed } from 'vue'; import { computed } from 'vue';
import { $t, I18nT } from '@vben/locales'; import { $t, I18nT } from '@vben/locales';
@ -12,68 +14,26 @@ import { $t, I18nT } from '@vben/locales';
import { InboxOutlined, UploadOutlined } from '@ant-design/icons-vue'; import { InboxOutlined, UploadOutlined } from '@ant-design/icons-vue';
import { Upload } from 'ant-design-vue'; import { Upload } from 'ant-design-vue';
import { uploadApi } from '#/api';
import { defaultFileAcceptExts, defaultFilePreview } from './helper'; import { defaultFileAcceptExts, defaultFilePreview } from './helper';
import { useUpload } from './hook'; import { useUpload } from './hook';
interface Props { interface FileUploadProps extends BaseUploadProps {
/**
* 文件上传失败 是否从展示列表中删除
* @default true
*/
removeOnError?: boolean;
/**
* 上传成功 是否展示提示信息
* @default true
*/
showSuccessMsg?: boolean;
/**
* 删除文件前是否需要确认
* @default false
*/
removeConfirm?: boolean;
/**
* 同antdv参数
*/
accept?: string;
/**
* 附带的请求参数
*/
data?: any;
/**
* 最大上传图片数量
* maxCount为1时 会被绑定为string而非string[]
* @default 1
*/
maxCount?: number;
/**
* 文件最大 单位M
* @default 5
*/
maxSize?: number;
/**
* 是否禁用
* @default false
*/
disabled?: boolean;
/**
* 是否显示文案 请上传不超过...
* @default true
*/
helpMessage?: boolean;
/**
* 是否支持多选文件ie10+ 支持开启后按住 ctrl 可选择多个文件
* @default false
*/
multiple?: boolean;
/** /**
* 自定义文件预览逻辑 比如: 你可以改为下载 * 自定义文件预览逻辑 比如: 你可以改为下载
* @param file file * @param file file
*/ */
preview?: (file: UploadFile) => Promise<void> | void; preview?: (file: UploadFile) => Promise<void> | void;
/**
* 是否支持拖拽上传
* @default false
*/
enableDragUpload?: boolean; enableDragUpload?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<FileUploadProps>(), {
api: () => uploadApi,
removeOnError: true, removeOnError: true,
showSuccessMsg: true, showSuccessMsg: true,
removeConfirm: false, removeConfirm: false,
@ -84,8 +44,11 @@ const props = withDefaults(defineProps<Props>(), {
disabled: false, disabled: false,
helpMessage: true, helpMessage: true,
preview: defaultFilePreview, preview: defaultFilePreview,
enableDragUpload: false,
directory: false,
}); });
/** 返回不同的上传组件 */
const CurrentUploadComponent = computed(() => { const CurrentUploadComponent = computed(() => {
if (props.enableDragUpload) { if (props.enableDragUpload) {
return Upload.Dragger; return Upload.Dragger;
@ -99,8 +62,7 @@ const ossIdList = defineModel<string | string[]>('value', {
}); });
const { const {
uploadUrl, customRequest,
headers,
acceptFormat, acceptFormat,
handleChange, handleChange,
handleRemove, handleRemove,
@ -113,15 +75,14 @@ const {
<div> <div>
<CurrentUploadComponent <CurrentUploadComponent
v-model:file-list="innerFileList" v-model:file-list="innerFileList"
:action="uploadUrl"
:headers="headers"
:data="data"
:accept="accept" :accept="accept"
:disabled="disabled" :disabled="disabled"
:directory="directory"
:max-count="maxCount" :max-count="maxCount"
:progress="{ showInfo: true }" :progress="{ showInfo: true }"
:multiple="multiple" :multiple="multiple"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:custom-request="customRequest"
@preview="preview" @preview="preview"
@change="handleChange" @change="handleChange"
@remove="handleRemove" @remove="handleRemove"

View File

@ -1,21 +1,21 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import type { UploadChangeParam, UploadFile } from 'ant-design-vue'; import type { UploadChangeParam, UploadFile } from 'ant-design-vue';
import type { FileType } from 'ant-design-vue/es/upload/interface'; import type { FileType } from 'ant-design-vue/es/upload/interface';
import type { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface';
import type { ModelRef } from 'vue'; import type { ModelRef } from 'vue';
import type { HttpResponse } from '@vben/request'; import type { BaseUploadProps } from './props';
import type { UploadResult } from '#/api'; import type { AxiosProgressEvent, UploadResult } from '#/api';
import type { OssFile } from '#/api/system/oss/model'; import type { OssFile } from '#/api/system/oss/model';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useAppConfig } from '@vben/hooks';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { useAccessStore } from '@vben/stores';
import { message, Modal } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { isFunction } from 'lodash-es';
import { ossInfo } from '#/api/system/oss'; import { ossInfo } from '#/api/system/oss';
@ -85,29 +85,9 @@ export function useUpload(
props: Readonly<BaseUploadProps>, props: Readonly<BaseUploadProps>,
bindValue: ModelRef<string | string[]>, bindValue: ModelRef<string | string[]>,
) { ) {
const { apiURL, clientId } = useAppConfig( // 组件内部维护fileList
import.meta.env,
import.meta.env.PROD,
);
// 内部维护fileList
const innerFileList = ref<UploadFile[]>([]); const innerFileList = ref<UploadFile[]>([]);
/** oss上传地址 */
const uploadUrl = `${apiURL}/resource/oss/upload`;
const accessStore = useAccessStore();
/**
* header参数 token和clientId
*/
const headers = computed(() => {
return {
Authorization: `Bearer ${accessStore.accessToken}`,
clientId,
};
});
const acceptFormat = computed(() => { const acceptFormat = computed(() => {
return props.accept return props.accept
?.split(',') ?.split(',')
@ -121,6 +101,11 @@ export function useUpload(
}); });
function handleChange(info: UploadChangeParam) { function handleChange(info: UploadChangeParam) {
/**
*
* @param currentFile
* @param currentFileList list
*/
function removeCurrentFile( function removeCurrentFile(
currentFile: UploadChangeParam['file'], currentFile: UploadChangeParam['file'],
currentFileList: UploadChangeParam['fileList'], currentFileList: UploadChangeParam['fileList'],
@ -140,19 +125,9 @@ export function useUpload(
if (!currentFile.response) { if (!currentFile.response) {
return; return;
} }
// 获取返回结果 // 获取返回结果 为customRequest的reslove参数
const response = currentFile.response as HttpResponse<UploadResult>; // 只有success才会走到这里
// 上传异常 const { ossId, fileName, url } = currentFile.response as UploadResult;
if (response.code !== 200) {
message.error(response.msg);
removeCurrentFile(currentFile, fileList);
return;
}
// 上传成功 做转换
if (props.showSuccessMsg) {
message.success($t('component.upload.uploadSuccess'));
}
const { ossId, fileName, url } = response.data;
currentFile.url = url; currentFile.url = url;
currentFile.fileName = fileName; currentFile.fileName = fileName;
currentFile.uid = ossId; currentFile.uid = ossId;
@ -166,7 +141,6 @@ export function useUpload(
} }
// 上传失败 网络原因导致httpStatus 不等于200 // 上传失败 网络原因导致httpStatus 不等于200
case 'error': { case 'error': {
message.error($t('component.upload.uploadError'));
removeCurrentFile(currentFile, fileList); removeCurrentFile(currentFile, fileList);
} }
} }
@ -207,6 +181,12 @@ export function useUpload(
}); });
} }
/**
*
* accept校验
* @param file file
* @returns file | false
*/
function beforeUpload(file: FileType) { function beforeUpload(file: FileType) {
const isLtMax = file.size / 1024 / 1024 < props.maxSize!; const isLtMax = file.size / 1024 / 1024 < props.maxSize!;
if (!isLtMax) { if (!isLtMax) {
@ -217,6 +197,37 @@ export function useUpload(
return file; return file;
} }
/**
*
* @param info
*/
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
if (!isFunction(api)) {
console.warn('upload api must exist and be a function');
return;
}
try {
// 进度条事件
const progressEvent: AxiosProgressEvent = (e) => {
const percent = Math.trunc((e.loaded / e.total!) * 100);
info.onProgress!({ percent });
};
const res = await api(
info.file as File,
props?.data ?? {},
progressEvent,
);
info.onSuccess!(res);
if (props.showSuccessMsg) {
message.success($t('component.upload.uploadSuccess'));
}
} catch (error: any) {
console.error(error);
info.onError!(error);
}
}
/** /**
* list地址变化 watch * list地址变化 watch
* immediate用于初始化触发 * immediate用于初始化触发
@ -244,11 +255,10 @@ export function useUpload(
); );
return { return {
uploadUrl,
headers,
handleChange, handleChange,
handleRemove, handleRemove,
beforeUpload, beforeUpload,
customRequest,
innerFileList, innerFileList,
acceptFormat, acceptFormat,
}; };

View File

@ -5,72 +5,28 @@
<script setup lang="ts"> <script setup lang="ts">
import type { UploadListType } from 'ant-design-vue/es/upload/interface'; import type { UploadListType } from 'ant-design-vue/es/upload/interface';
import type { BaseUploadProps } from './props';
import { $t, I18nT } from '@vben/locales'; import { $t, I18nT } from '@vben/locales';
import { PlusOutlined } from '@ant-design/icons-vue'; import { PlusOutlined } from '@ant-design/icons-vue';
import { Image, ImagePreviewGroup, Upload } from 'ant-design-vue'; import { Image, ImagePreviewGroup, Upload } from 'ant-design-vue';
import { uploadApi } from '#/api';
import { defaultImageAcceptExts } from './helper'; import { defaultImageAcceptExts } from './helper';
import { useImagePreview, useUpload } from './hook'; import { useImagePreview, useUpload } from './hook';
interface Props { interface ImageUploadProps extends BaseUploadProps {
/**
* 文件上传失败 是否从展示列表中删除
* @default true
*/
removeOnError?: boolean;
/**
* 上传成功 是否展示提示信息
* @default true
*/
showSuccessMsg?: boolean;
/**
* 删除文件前是否需要确认
* @default false
*/
removeConfirm?: boolean;
/**
* 同antdv参数
*/
accept?: string;
/**
* 附带的请求参数
*/
data?: any;
/**
* 最大上传图片数量
* maxCount为1时 会被绑定为string而非string[]
* @default 1
*/
maxCount?: number;
/**
* 文件最大 单位M
* @default 5
*/
maxSize?: number;
/**
* 是否禁用
* @default false
*/
disabled?: boolean;
/** /**
* 同antdv的listType * 同antdv的listType
* @default picture-card * @default picture-card
*/ */
listType?: UploadListType; listType?: UploadListType;
/**
* 是否显示文案 请上传不超过...
* @default true
*/
helpMessage?: boolean;
/**
* 是否支持多选文件ie10+ 支持开启后按住 ctrl 可选择多个文件
* @default false
*/
multiple?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<ImageUploadProps>(), {
api: () => uploadApi,
removeOnError: true, removeOnError: true,
showSuccessMsg: true, showSuccessMsg: true,
removeConfirm: false, removeConfirm: false,
@ -89,13 +45,12 @@ const ossIdList = defineModel<string | string[]>('value', {
}); });
const { const {
uploadUrl,
headers,
acceptFormat, acceptFormat,
handleChange, handleChange,
handleRemove, handleRemove,
beforeUpload, beforeUpload,
innerFileList, innerFileList,
customRequest,
} = useUpload(props, ossIdList); } = useUpload(props, ossIdList);
const { previewVisible, previewImage, handleCancel, handlePreview } = const { previewVisible, previewImage, handleCancel, handlePreview } =
@ -107,15 +62,14 @@ const { previewVisible, previewImage, handleCancel, handlePreview } =
<Upload <Upload
v-model:file-list="innerFileList" v-model:file-list="innerFileList"
:list-type="listType" :list-type="listType"
:action="uploadUrl"
:headers="headers"
:data="data"
:accept="accept" :accept="accept"
:disabled="disabled" :disabled="disabled"
:directory="directory"
:max-count="maxCount" :max-count="maxCount"
:progress="{ showInfo: true }" :progress="{ showInfo: true }"
:multiple="multiple" :multiple="multiple"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:custom-request="customRequest"
@preview="handlePreview" @preview="handlePreview"
@change="handleChange" @change="handleChange"
@remove="handleRemove" @remove="handleRemove"

View File

@ -19,4 +19,8 @@ return new Promise<FileType>((resolve) =>
); );
``` ```
根本原因在于`file-typ`库可能不支持Safari 去掉可以正常上传 根本原因在于`file-typ`库的`fileTypeFromBlob`方法不支持Safari 去掉可以正常上传
safari不支持`ReadableStreamBYOBReader`api
详见: https://github.com/sindresorhus/file-type/issues/690

View File

@ -1,4 +1,10 @@
interface BaseUploadProps { import type { UploadApi } from '#/api';
export interface BaseUploadProps {
/**
*
*/
api?: UploadApi;
/** /**
* *
* @default true * @default true
@ -48,4 +54,9 @@ interface BaseUploadProps {
* @default false * @default false
*/ */
multiple?: boolean; multiple?: boolean;
/**
*
* @default false
*/
directory?: boolean;
} }

View File

@ -9,7 +9,7 @@ import { Alert, Card, Modal } from 'ant-design-vue';
import { FileUpload, ImageUpload } from '#/components/upload'; import { FileUpload, ImageUpload } from '#/components/upload';
const singleImageId = ref('1905537674682916865'); const singleImageId = ref('');
const singleFileId = ref('1905191167882518529'); const singleFileId = ref('1905191167882518529');
const multipleImageId = ref<string[]>(['1905537674682916865']); const multipleImageId = ref<string[]>(['1905537674682916865']);
const multipleFileId = ref<string[]>(['1905191167882518529']); const multipleFileId = ref<string[]>(['1905191167882518529']);
@ -74,6 +74,16 @@ function handlePreview(file: UploadFile) {
<Card title="文件禁用上传" size="small"> <Card title="文件禁用上传" size="small">
<FileUpload :disabled="true" :max-count="3" :help-message="false" /> <FileUpload :disabled="true" :max-count="3" :help-message="false" />
</Card> </Card>
<Card title="文件夹上传" size="small">
<FileUpload
v-model:value="multipleFileId"
:max-count="3"
:directory="true"
accept="*"
/>
当前绑定值: {{ multipleFileId }}
</Card>
</div> </div>
</Page> </Page>
</template> </template>