feat: 增加 image 文件上传 80%

pull/75/MERGE
YunaiV 2025-04-18 13:08:20 +08:00
parent f6e2dc55ff
commit 1bacb6759f
4 changed files with 282 additions and 11 deletions

View File

@ -1,18 +1,17 @@
export function checkFileType(file: File, accepts: string[]) {
let reg;
if (!accepts || accepts.length === 0) {
reg = /.(jpg|jpeg|png|gif|webp)$/i;
} else {
const newTypes = accepts.join('|');
reg = new RegExp('\\.(' + newTypes + ')$', 'i');
return true;
}
const newTypes = accepts.join('|');
const reg = new RegExp('\\.(' + newTypes + ')$', 'i');
return reg.test(file.name);
}
export function checkImgType(file: File) {
return isImgTypeByName(file.name);
}
/**
*
*/
export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
export function isImgTypeByName(name: string) {
return /\.(jpg|jpeg|png|gif|webp)$/i.test(name);
export function checkImgType(file: File, accepts: string[] = defaultImageAccepts) {
return checkFileType(file, accepts);
}

View File

@ -0,0 +1,270 @@
<script lang="ts" setup>
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosProgressEvent } from '#/api/infra/file';
import type { AxiosResponse } from '@vben/request';
import { CloudUpload } from '@vben/icons';
import { message, Upload, Modal } from 'ant-design-vue';
import { $t } from '@vben/locales';
import { ref, toRefs, watch } from 'vue';
import { isFunction, isObject, isString } from '@vben/utils';
import { checkImgType, defaultImageAccepts } from './helper';
import { UploadResultStatus } from './typing';
import { useUploadType } from './use-upload';
import { uploadFile } from '#/api/infra/file';
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
const props = withDefaults(
defineProps<{
//
accept?: string[];
api?: (
file: Blob | File,
onUploadProgress?: AxiosProgressEvent,
) => Promise<AxiosResponse<any>>;
disabled?: boolean;
helpText?: string;
listType?: ListType;
// Infinity
maxNumber?: number;
// MB
maxSize?: number;
//
multiple?: boolean;
// support xxx.xxx.xx
resultField?: string;
//
showDescription?: boolean;
value?: string[] | string;
}>(),
{
value: () => [],
disabled: false,
listType: 'picture-card',
helpText: '',
maxSize: 2,
maxNumber: 1,
accept: () => defaultImageAccepts,
multiple: false,
api: (file: Blob | File, onUploadProgress?: AxiosProgressEvent) => {
// TODO @
debugger
return uploadFile({ file }, onUploadProgress);
},
resultField: '',
showDescription: true,
},
);
const emit = defineEmits(['change', 'update:value', 'delete']);
type ListType = 'picture' | 'picture-card' | 'text';
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
acceptRef: accept,
helpTextRef: helpText,
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
});
const previewOpen = ref<boolean>(false); //
const previewImage = ref<string>(''); //
const previewTitle = ref<string>(''); //
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true); //
const isActMsg = ref<boolean>(true); //
const isFirstRender = ref<boolean>(true); //
watch(
() => props.value,
async (v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
return;
}
let value: string | string[] = [];
if (v) {
if (Array.isArray(v)) {
value = v;
} else {
value.push(v);
}
fileList.value = value.map((item, i) => {
if (item && isString(item)) {
return {
uid: `${-i}`,
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
status: UploadResultStatus.DONE,
url: item,
};
} else if (item && isObject(item)) {
return item;
}
return null;
}) as UploadProps['fileList'];
}
if (!isFirstRender.value) {
emit('change', value);
isFirstRender.value = false;
}
},
{
immediate: true,
deep: true,
},
);
function getBase64<T extends ArrayBuffer | null | string>(file: File) {
return new Promise<T>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => {
resolve(reader.result as T);
});
reader.addEventListener('error', (error) => reject(error));
});
}
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64<string>(file.originFileObj!);
}
previewImage.value = file.url || file.preview || '';
previewOpen.value = true;
previewTitle.value =
file.name ||
previewImage.value.slice(
Math.max(0, previewImage.value.lastIndexOf('/') + 1),
);
};
const handleRemove = async (file: UploadFile) => {
if (fileList.value) {
const index = fileList.value.findIndex((item) => item.uid === file.uid);
index !== -1 && fileList.value.splice(index, 1);
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
emit('delete', file);
}
};
const handleCancel = () => {
previewOpen.value = false;
previewTitle.value = '';
};
const beforeUpload = async (file: File) => {
const { maxSize, accept } = props;
const isAct = checkImgType(file, accept);
if (!isAct) {
message.error($t('ui.upload.acceptUpload', [accept]));
isActMsg.value = false;
//
setTimeout(() => (isActMsg.value = true), 1000);
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
message.error($t('ui.upload.maxSizeMultiple', [maxSize]));
isLtMsg.value = false;
//
setTimeout(() => (isLtMsg.value = true), 1000);
}
return (isAct && !isLt) || Upload.LIST_IGNORE;
};
async function customRequest(info: UploadRequestOption<any>) {
const { api } = props;
if (!api || !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, progressEvent);
info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess'));
//
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
emit('change', value);
} catch (error: any) {
console.error(error);
info.onError!(error);
}
}
function getValue() {
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
.map((item: any) => {
debugger
if (item?.response && props?.resultField) {
return item?.response;
}
return item?.url || item?.response?.url || item?.response;
});
// add by String
if (props.maxNumber === 1) {
return list.length > 0 ? list[0] : '';
}
return list;
}
</script>
<template>
<div>
<Upload
v-bind="$attrs"
v-model:file-list="fileList"
:accept="getStringAccept"
:before-upload="beforeUpload"
:custom-request="customRequest"
:disabled="disabled"
:list-type="listType"
:max-count="maxNumber"
:multiple="multiple"
:progress="{ showInfo: true }"
@preview="handlePreview"
@remove="handleRemove"
>
<div v-if="fileList && fileList.length < maxNumber" class="flex flex-col items-center justify-center">
<CloudUpload />
<div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div>
</div>
</Upload>
<div
v-if="showDescription"
class="mt-2 flex flex-wrap items-center text-[14px]"
>
请上传不超过
<div class="mx-1 font-bold text-primary">{{ maxSize }}MB</div>
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
格式文件
</div>
<Modal
:footer="null"
:open="previewOpen"
:title="previewTitle"
@cancel="handleCancel"
>
<img :src="previewImage" alt="" class="w-full" />
</Modal>
</div>
</template>
<style>
.ant-upload-select-picture-card {
@apply flex items-center justify-center;
}
</style>

View File

@ -109,6 +109,7 @@
},
"upload": {
"upload": "Upload",
"imgUpload": "ImageUpload",
"accept": "Support {0} format",
"acceptUpload": "Only upload files in {0} format",
"maxSize": "A single file does not exceed {0}MB",

View File

@ -109,6 +109,7 @@
},
"upload": {
"upload": "上传",
"imgUpload": "图片上传",
"accept": "支持{0}格式",
"acceptUpload": "只能上传{0}格式文件",
"maxSize": "单个文件不超过{0}MB",