feat: add upload comp
parent
684d564ea1
commit
477e387ce9
|
@ -0,0 +1,221 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { UploadFile, UploadProps } from 'naive-ui';
|
||||||
|
import type { UploadRequestOption } from 'naive-ui/lib/vc-upload/interface';
|
||||||
|
|
||||||
|
import type { AxiosResponse } from '@vben/request';
|
||||||
|
|
||||||
|
import type { AxiosProgressEvent } from '#/api/infra/file';
|
||||||
|
|
||||||
|
import { ref, toRefs, watch } from 'vue';
|
||||||
|
|
||||||
|
import { CloudUpload } from '@vben/icons';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import { isFunction, isObject, isString } from '@vben/utils';
|
||||||
|
|
||||||
|
import { NButton, NUpload } from 'naive-ui';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
|
||||||
|
import { checkFileType } from './helper';
|
||||||
|
import { UploadResultStatus } from './typing';
|
||||||
|
import { useUpload, useUploadType } from './use-upload';
|
||||||
|
|
||||||
|
defineOptions({ name: 'FileUpload', inheritAttrs: false });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
// 根据后缀,或者其他
|
||||||
|
accept?: string[];
|
||||||
|
api?: (
|
||||||
|
file: File,
|
||||||
|
onUploadProgress?: AxiosProgressEvent,
|
||||||
|
) => Promise<AxiosResponse<any>>;
|
||||||
|
// 上传的目录
|
||||||
|
directory?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
helpText?: string;
|
||||||
|
// 最大数量的文件,Infinity不限制
|
||||||
|
maxNumber?: number;
|
||||||
|
// 文件最大多少MB
|
||||||
|
maxSize?: number;
|
||||||
|
// 是否支持多选
|
||||||
|
multiple?: boolean;
|
||||||
|
// support xxx.xxx.xx
|
||||||
|
resultField?: string;
|
||||||
|
// 是否显示下面的描述
|
||||||
|
showDescription?: boolean;
|
||||||
|
value?: string | string[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
value: () => [],
|
||||||
|
directory: undefined,
|
||||||
|
disabled: false,
|
||||||
|
helpText: '',
|
||||||
|
maxSize: 2,
|
||||||
|
maxNumber: 1,
|
||||||
|
accept: () => [],
|
||||||
|
multiple: false,
|
||||||
|
api: undefined,
|
||||||
|
resultField: '',
|
||||||
|
showDescription: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const emit = defineEmits(['change', 'update:value', 'delete']);
|
||||||
|
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||||
|
const isInnerOperate = ref<boolean>(false);
|
||||||
|
const { getStringAccept } = useUploadType({
|
||||||
|
acceptRef: accept,
|
||||||
|
helpTextRef: helpText,
|
||||||
|
maxNumberRef: maxNumber,
|
||||||
|
maxSizeRef: maxSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileList = ref<UploadProps['fileList']>([]);
|
||||||
|
const isLtMsg = ref<boolean>(true); // 文件大小错误提示
|
||||||
|
const isActMsg = ref<boolean>(true); // 文件类型错误提示
|
||||||
|
const isFirstRender = ref<boolean>(true); // 是否第一次渲染
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(v) => {
|
||||||
|
if (isInnerOperate.value) {
|
||||||
|
isInnerOperate.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let value: 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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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 beforeUpload = async (file: File) => {
|
||||||
|
const { maxSize, accept } = props;
|
||||||
|
const isAct = checkFileType(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>) {
|
||||||
|
let { api } = props;
|
||||||
|
if (!api || !isFunction(api)) {
|
||||||
|
api = useUpload(props.directory).httpRequest;
|
||||||
|
}
|
||||||
|
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) => {
|
||||||
|
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>
|
||||||
|
<NUpload
|
||||||
|
v-bind="$attrs"
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
:accept="getStringAccept"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:custom-request="customRequest"
|
||||||
|
:disabled="disabled"
|
||||||
|
:max-count="maxNumber"
|
||||||
|
:multiple="multiple"
|
||||||
|
list-type="text"
|
||||||
|
:progress="{ showInfo: true }"
|
||||||
|
@remove="handleRemove"
|
||||||
|
>
|
||||||
|
<div v-if="fileList && fileList.length < maxNumber">
|
||||||
|
<NButton>
|
||||||
|
<CloudUpload />
|
||||||
|
{{ $t('ui.upload.upload') }}
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
<div v-if="showDescription" class="mt-2 flex flex-wrap items-center">
|
||||||
|
请上传不超过
|
||||||
|
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
|
||||||
|
的
|
||||||
|
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
|
||||||
|
格式文件
|
||||||
|
</div>
|
||||||
|
</NUpload>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,20 @@
|
||||||
|
export function checkFileType(file: File, accepts: string[]) {
|
||||||
|
if (!accepts || accepts.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const newTypes = accepts.join('|');
|
||||||
|
const reg = new RegExp(`${String.raw`\.(` + newTypes})$`, 'i');
|
||||||
|
return reg.test(file.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认图片类型
|
||||||
|
*/
|
||||||
|
export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||||
|
|
||||||
|
export function checkImgType(
|
||||||
|
file: File,
|
||||||
|
accepts: string[] = defaultImageAccepts,
|
||||||
|
) {
|
||||||
|
return checkFileType(file, accepts);
|
||||||
|
}
|
|
@ -0,0 +1,276 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { UploadFile, UploadProps } from 'naive-ui';
|
||||||
|
import type { UploadRequestOption } from 'naive-ui/lib/vc-upload/interface';
|
||||||
|
|
||||||
|
import type { AxiosResponse } from '@vben/request';
|
||||||
|
|
||||||
|
import type { UploadListType } from './typing';
|
||||||
|
|
||||||
|
import type { AxiosProgressEvent } from '#/api/infra/file';
|
||||||
|
|
||||||
|
import { ref, toRefs, watch } from 'vue';
|
||||||
|
|
||||||
|
import { CloudUpload } from '@vben/icons';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
import { isFunction, isObject, isString } from '@vben/utils';
|
||||||
|
|
||||||
|
import { NModal, NUpload } from 'naive-ui';
|
||||||
|
|
||||||
|
import { message } from '#/adapter/naive';
|
||||||
|
|
||||||
|
import { checkImgType, defaultImageAccepts } from './helper';
|
||||||
|
import { UploadResultStatus } from './typing';
|
||||||
|
import { useUpload, useUploadType } from './use-upload';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
// 根据后缀,或者其他
|
||||||
|
accept?: string[];
|
||||||
|
api?: (
|
||||||
|
file: File,
|
||||||
|
onUploadProgress?: AxiosProgressEvent,
|
||||||
|
) => Promise<AxiosResponse<any>>;
|
||||||
|
// 上传的目录
|
||||||
|
directory?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
helpText?: string;
|
||||||
|
listType?: UploadListType;
|
||||||
|
// 最大数量的文件,Infinity不限制
|
||||||
|
maxNumber?: number;
|
||||||
|
// 文件最大多少MB
|
||||||
|
maxSize?: number;
|
||||||
|
// 是否支持多选
|
||||||
|
multiple?: boolean;
|
||||||
|
// support xxx.xxx.xx
|
||||||
|
resultField?: string;
|
||||||
|
// 是否显示下面的描述
|
||||||
|
showDescription?: boolean;
|
||||||
|
value?: string | string[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
value: () => [],
|
||||||
|
directory: undefined,
|
||||||
|
disabled: false,
|
||||||
|
listType: 'picture-card',
|
||||||
|
helpText: '',
|
||||||
|
maxSize: 2,
|
||||||
|
maxNumber: 1,
|
||||||
|
accept: () => defaultImageAccepts,
|
||||||
|
multiple: false,
|
||||||
|
api: undefined,
|
||||||
|
resultField: '',
|
||||||
|
showDescription: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const emit = defineEmits(['change', 'update:value', 'delete']);
|
||||||
|
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>) {
|
||||||
|
let { api } = props;
|
||||||
|
if (!api || !isFunction(api)) {
|
||||||
|
api = useUpload(props.directory).httpRequest;
|
||||||
|
}
|
||||||
|
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) => {
|
||||||
|
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>
|
||||||
|
<NUpload
|
||||||
|
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>
|
||||||
|
</NUpload>
|
||||||
|
<div
|
||||||
|
v-if="showDescription"
|
||||||
|
class="mt-2 flex flex-wrap items-center text-[14px]"
|
||||||
|
>
|
||||||
|
请上传不超过
|
||||||
|
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
|
||||||
|
的
|
||||||
|
<div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
|
||||||
|
格式文件
|
||||||
|
</div>
|
||||||
|
<NModal
|
||||||
|
:footer="null"
|
||||||
|
:open="previewOpen"
|
||||||
|
:title="previewTitle"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<img :src="previewImage" alt="" class="w-full" />
|
||||||
|
</NModal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ant-upload-select-picture-card {
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as FileUpload } from './file-upload.vue';
|
||||||
|
export { default as ImageUpload } from './image-upload.vue';
|
|
@ -0,0 +1,8 @@
|
||||||
|
export enum UploadResultStatus {
|
||||||
|
DONE = 'done',
|
||||||
|
ERROR = 'error',
|
||||||
|
SUCCESS = 'success',
|
||||||
|
UPLOADING = 'uploading',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UploadListType = 'picture' | 'picture-card' | 'text';
|
|
@ -0,0 +1,166 @@
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { AxiosProgressEvent, InfraFileApi } from '#/api/infra/file';
|
||||||
|
|
||||||
|
import { computed, unref } from 'vue';
|
||||||
|
|
||||||
|
import { useAppConfig } from '@vben/hooks';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
// import CryptoJS from 'crypto-js';
|
||||||
|
import { createFile, getFilePresignedUrl, uploadFile } from '#/api/infra/file';
|
||||||
|
import { baseRequestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传类型
|
||||||
|
*/
|
||||||
|
enum UPLOAD_TYPE {
|
||||||
|
// 客户端直接上传(只支持S3服务)
|
||||||
|
CLIENT = 'client',
|
||||||
|
// 客户端发送到后端上传
|
||||||
|
SERVER = 'server',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUploadType({
|
||||||
|
acceptRef,
|
||||||
|
helpTextRef,
|
||||||
|
maxNumberRef,
|
||||||
|
maxSizeRef,
|
||||||
|
}: {
|
||||||
|
acceptRef: Ref<string[]>;
|
||||||
|
helpTextRef: Ref<string>;
|
||||||
|
maxNumberRef: Ref<number>;
|
||||||
|
maxSizeRef: Ref<number>;
|
||||||
|
}) {
|
||||||
|
// 文件类型限制
|
||||||
|
const getAccept = computed(() => {
|
||||||
|
const accept = unref(acceptRef);
|
||||||
|
if (accept && accept.length > 0) {
|
||||||
|
return accept;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
const getStringAccept = computed(() => {
|
||||||
|
return unref(getAccept)
|
||||||
|
.map((item) => {
|
||||||
|
return item.indexOf('/') > 0 || item.startsWith('.')
|
||||||
|
? item
|
||||||
|
: `.${item}`;
|
||||||
|
})
|
||||||
|
.join(',');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 支持jpg、jpeg、png格式,不超过2M,最多可选择10张图片,。
|
||||||
|
const getHelpText = computed(() => {
|
||||||
|
const helpText = unref(helpTextRef);
|
||||||
|
if (helpText) {
|
||||||
|
return helpText;
|
||||||
|
}
|
||||||
|
const helpTexts: string[] = [];
|
||||||
|
|
||||||
|
const accept = unref(acceptRef);
|
||||||
|
if (accept.length > 0) {
|
||||||
|
helpTexts.push($t('ui.upload.accept', [accept.join(',')]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSize = unref(maxSizeRef);
|
||||||
|
if (maxSize) {
|
||||||
|
helpTexts.push($t('ui.upload.maxSize', [maxSize]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxNumber = unref(maxNumberRef);
|
||||||
|
if (maxNumber && maxNumber !== Infinity) {
|
||||||
|
helpTexts.push($t('ui.upload.maxNumber', [maxNumber]));
|
||||||
|
}
|
||||||
|
return helpTexts.join(',');
|
||||||
|
});
|
||||||
|
return { getAccept, getStringAccept, getHelpText };
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构
|
||||||
|
export function useUpload(directory?: string) {
|
||||||
|
// 后端上传地址
|
||||||
|
const uploadUrl = getUploadUrl();
|
||||||
|
// 是否使用前端直连上传
|
||||||
|
const isClientUpload =
|
||||||
|
UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE;
|
||||||
|
// 重写ElUpload上传方法
|
||||||
|
const httpRequest = async (
|
||||||
|
file: File,
|
||||||
|
onUploadProgress?: AxiosProgressEvent,
|
||||||
|
) => {
|
||||||
|
// 模式一:前端上传
|
||||||
|
if (isClientUpload) {
|
||||||
|
// 1.1 生成文件名称
|
||||||
|
const fileName = await generateFileName(file);
|
||||||
|
// 1.2 获取文件预签名地址
|
||||||
|
const presignedInfo = await getFilePresignedUrl(fileName, directory);
|
||||||
|
// 1.3 上传文件
|
||||||
|
return baseRequestClient
|
||||||
|
.put(presignedInfo.uploadUrl, file, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': file.type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// 1.4. 记录文件信息到后端(异步)
|
||||||
|
createFile0(presignedInfo, file);
|
||||||
|
// 通知成功,数据格式保持与后端上传的返回结果一致
|
||||||
|
return { url: presignedInfo.url };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 模式二:后端上传
|
||||||
|
return uploadFile({ file, directory }, onUploadProgress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploadUrl,
|
||||||
|
httpRequest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得上传 URL
|
||||||
|
*/
|
||||||
|
export function getUploadUrl(): string {
|
||||||
|
return `${apiURL}/infra/file/upload`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建文件信息
|
||||||
|
*
|
||||||
|
* @param vo 文件预签名信息
|
||||||
|
* @param file 文件
|
||||||
|
*/
|
||||||
|
function createFile0(vo: InfraFileApi.FilePresignedUrlRespVO, file: File) {
|
||||||
|
const fileVO = {
|
||||||
|
configId: vo.configId,
|
||||||
|
url: vo.url,
|
||||||
|
path: vo.path,
|
||||||
|
name: file.name,
|
||||||
|
type: file.type,
|
||||||
|
size: file.size,
|
||||||
|
};
|
||||||
|
createFile(fileVO);
|
||||||
|
return fileVO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成文件名称(使用算法SHA256)
|
||||||
|
*
|
||||||
|
* @param file 要上传的文件
|
||||||
|
*/
|
||||||
|
async function generateFileName(file: File) {
|
||||||
|
// // 读取文件内容
|
||||||
|
// const data = await file.arrayBuffer();
|
||||||
|
// const wordArray = CryptoJS.lib.WordArray.create(data);
|
||||||
|
// // 计算SHA256
|
||||||
|
// const sha256 = CryptoJS.SHA256(wordArray).toString();
|
||||||
|
// // 拼接后缀
|
||||||
|
// const ext = file.name.slice(Math.max(0, file.name.lastIndexOf('.')));
|
||||||
|
// return `${sha256}${ext}`;
|
||||||
|
return file.name;
|
||||||
|
}
|
Loading…
Reference in New Issue