perf: 复用 ImageUpload 组件构建 fc 专属 ImagesUpload
parent
e7934d81a1
commit
1fb5a9e31d
|
@ -0,0 +1,25 @@
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
import ImageUpload from '#/components/upload/image-upload.vue';
|
||||||
|
|
||||||
|
export const useImagesUpload = () => {
|
||||||
|
return defineComponent({
|
||||||
|
props: {
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
maxNumber: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
// TODO: @dhb52 其实还是靠 props 默认参数起作用,没能从 formCreate 传递
|
||||||
|
return (props: { maxNumber?: number; multiple?: boolean }) => (
|
||||||
|
<ImageUpload maxNumber={props.maxNumber} multiple={props.multiple} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
name: 'ImagesUpload',
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,272 +0,0 @@
|
||||||
<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 { 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 { message, Modal, Upload } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { checkImgType, defaultImageAccepts } from './helper';
|
|
||||||
import { UploadResultStatus } from './typing';
|
|
||||||
import { useUpload, useUploadType } from './use-upload';
|
|
||||||
|
|
||||||
type ListType = 'picture' | 'picture-card' | 'text';
|
|
||||||
|
|
||||||
defineOptions({ name: 'ImagesUpload', inheritAttrs: false });
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
// 根据后缀,或者其他
|
|
||||||
accept?: string[];
|
|
||||||
api?: (
|
|
||||||
file: 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: 5,
|
|
||||||
accept: () => defaultImageAccepts,
|
|
||||||
multiple: true,
|
|
||||||
api: useUpload().httpRequest,
|
|
||||||
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>) {
|
|
||||||
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) => {
|
|
||||||
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="text-primary mx-1 font-bold">{{ 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>
|
|
|
@ -1,3 +1,2 @@
|
||||||
export { default as FileUpload } from './file-upload.vue';
|
export { default as FileUpload } from './file-upload.vue';
|
||||||
export { default as ImageUpload } from './image-upload.vue';
|
export { default as ImageUpload } from './image-upload.vue';
|
||||||
export { default as ImagesUpload } from './images-upload.vue';
|
|
||||||
|
|
|
@ -7,8 +7,9 @@ import Antd from 'ant-design-vue';
|
||||||
// ======================= 自定义组件 =======================
|
// ======================= 自定义组件 =======================
|
||||||
import { useApiSelect } from '#/components/FormCreate';
|
import { useApiSelect } from '#/components/FormCreate';
|
||||||
import DictSelect from '#/components/FormCreate/src/components/DictSelect.vue';
|
import DictSelect from '#/components/FormCreate/src/components/DictSelect.vue';
|
||||||
|
import { useImagesUpload } from '#/components/FormCreate/src/components/useImagesUpload';
|
||||||
import { Tinymce } from '#/components/tinymce';
|
import { Tinymce } from '#/components/tinymce';
|
||||||
import { FileUpload, ImagesUpload, ImageUpload } from '#/components/upload';
|
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||||
|
|
||||||
const UserSelect = useApiSelect({
|
const UserSelect = useApiSelect({
|
||||||
name: 'UserSelect',
|
name: 'UserSelect',
|
||||||
|
@ -25,6 +26,7 @@ const DeptSelect = useApiSelect({
|
||||||
const ApiSelect = useApiSelect({
|
const ApiSelect = useApiSelect({
|
||||||
name: 'ApiSelect',
|
name: 'ApiSelect',
|
||||||
});
|
});
|
||||||
|
const ImagesUpload = useImagesUpload();
|
||||||
|
|
||||||
const components = [
|
const components = [
|
||||||
ImageUpload,
|
ImageUpload,
|
||||||
|
|
Loading…
Reference in New Issue