物业代码生成
This commit is contained in:
2
apps/web-antd/src/components/upload/index.ts
Normal file
2
apps/web-antd/src/components/upload/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as FileUpload } from './src/file-upload.vue';
|
||||
export { default as ImageUpload } from './src/image-upload.vue';
|
150
apps/web-antd/src/components/upload/src/file-upload.vue
Normal file
150
apps/web-antd/src/components/upload/src/file-upload.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<!--
|
||||
不再支持url 统一使用ossId
|
||||
去除使用`file-type`库进行文件类型检测 在Safari无法使用
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { UploadListType } from 'ant-design-vue/es/upload/interface';
|
||||
|
||||
import type { BaseUploadProps, UploadEmits } from './props';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { $t, I18nT } from '@vben/locales';
|
||||
|
||||
import { InboxOutlined, UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { Upload } from 'ant-design-vue';
|
||||
|
||||
import { uploadApi } from '#/api';
|
||||
|
||||
import { defaultFileAcceptExts, defaultFilePreview } from './helper';
|
||||
import { useUpload } from './hook';
|
||||
|
||||
interface FileUploadProps extends BaseUploadProps {
|
||||
/**
|
||||
* 同antdv的listType 但是排除picture-card
|
||||
* 文件上传不适合用picture-card显示
|
||||
* @default text
|
||||
*/
|
||||
listType?: Exclude<UploadListType, 'picture-card'>;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FileUploadProps>(), {
|
||||
api: () => uploadApi,
|
||||
removeOnError: true,
|
||||
showSuccessMsg: true,
|
||||
removeConfirm: false,
|
||||
accept: defaultFileAcceptExts.join(','),
|
||||
data: () => undefined,
|
||||
maxCount: 1,
|
||||
maxSize: 5,
|
||||
disabled: false,
|
||||
helpMessage: true,
|
||||
preview: defaultFilePreview,
|
||||
enableDragUpload: false,
|
||||
directory: false,
|
||||
abortOnUnmounted: true,
|
||||
listType: 'text',
|
||||
});
|
||||
|
||||
const emit = defineEmits<UploadEmits>();
|
||||
|
||||
/** 返回不同的上传组件 */
|
||||
const CurrentUploadComponent = computed(() => {
|
||||
if (props.enableDragUpload) {
|
||||
return Upload.Dragger;
|
||||
}
|
||||
return Upload;
|
||||
});
|
||||
|
||||
// 双向绑定 ossId
|
||||
const ossIdList = defineModel<string | string[]>('value', {
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
const {
|
||||
customRequest,
|
||||
acceptStr,
|
||||
handleChange,
|
||||
handleRemove,
|
||||
beforeUpload,
|
||||
innerFileList,
|
||||
} = useUpload(props, emit, ossIdList, 'file');
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Upload.Dragger只会影响样式
|
||||
使用普通Upload也是支持拖拽上传的
|
||||
-->
|
||||
<template>
|
||||
<div>
|
||||
<CurrentUploadComponent
|
||||
v-model:file-list="innerFileList"
|
||||
:accept="accept"
|
||||
:list-type="listType"
|
||||
:disabled="disabled"
|
||||
:directory="directory"
|
||||
:max-count="maxCount"
|
||||
:progress="{ showInfo: true }"
|
||||
:multiple="multiple"
|
||||
:before-upload="beforeUpload"
|
||||
:custom-request="customRequest"
|
||||
@preview="preview"
|
||||
@change="handleChange"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<div v-if="!enableDragUpload && innerFileList?.length < maxCount">
|
||||
<a-button :disabled="disabled">
|
||||
<UploadOutlined />
|
||||
{{ $t('component.upload.upload') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-if="enableDragUpload">
|
||||
<p class="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p class="ant-upload-text">
|
||||
{{ $t('component.upload.clickOrDrag') }}
|
||||
</p>
|
||||
</div>
|
||||
</CurrentUploadComponent>
|
||||
<slot name="helpMessage" v-bind="{ maxCount, disabled, maxSize, accept }">
|
||||
<I18nT
|
||||
v-if="helpMessage"
|
||||
scope="global"
|
||||
keypath="component.upload.uploadHelpMessage"
|
||||
tag="div"
|
||||
class="mt-2"
|
||||
:class="{ 'upload-text__disabled': disabled }"
|
||||
>
|
||||
<template #size>
|
||||
<span
|
||||
class="text-primary mx-1 font-medium"
|
||||
:class="{ 'upload-text__disabled': disabled }"
|
||||
>
|
||||
{{ maxSize }}MB
|
||||
</span>
|
||||
</template>
|
||||
<template #ext>
|
||||
<span
|
||||
class="text-primary mx-1 font-medium"
|
||||
:class="{ 'upload-text__disabled': disabled }"
|
||||
>
|
||||
{{ acceptStr }}
|
||||
</span>
|
||||
</template>
|
||||
</I18nT>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
// 禁用的样式和antd保持一致
|
||||
.upload-text__disabled {
|
||||
color: rgb(50 54 57 / 25%);
|
||||
cursor: not-allowed;
|
||||
|
||||
&:where(.dark, .dark *) {
|
||||
color: rgb(242 242 242 / 25%);
|
||||
}
|
||||
}
|
||||
</style>
|
28
apps/web-antd/src/components/upload/src/helper.ts
Normal file
28
apps/web-antd/src/components/upload/src/helper.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { UploadFile } from 'ant-design-vue';
|
||||
|
||||
/**
|
||||
* 默认支持上传的图片文件类型
|
||||
*/
|
||||
export const defaultImageAcceptExts = [
|
||||
'.jpg',
|
||||
'.jpeg',
|
||||
'.png',
|
||||
'.gif',
|
||||
'.webp',
|
||||
];
|
||||
|
||||
/**
|
||||
* 默认支持上传的文件类型
|
||||
*/
|
||||
export const defaultFileAcceptExts = ['.xlsx', '.csv', '.docx', '.pdf'];
|
||||
|
||||
/**
|
||||
* 文件(非图片)的默认预览逻辑
|
||||
* 默认: window.open打开 交给浏览器接管
|
||||
* @param file file
|
||||
*/
|
||||
export function defaultFilePreview(file: UploadFile) {
|
||||
if (file?.url) {
|
||||
window.open(file.url);
|
||||
}
|
||||
}
|
385
apps/web-antd/src/components/upload/src/hook.ts
Normal file
385
apps/web-antd/src/components/upload/src/hook.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type { UploadChangeParam, UploadFile } from 'ant-design-vue';
|
||||
import type { FileType } from 'ant-design-vue/es/upload/interface';
|
||||
import type {
|
||||
RcFile,
|
||||
UploadRequestOption,
|
||||
} from 'ant-design-vue/es/vc-upload/interface';
|
||||
|
||||
import type { ModelRef } from 'vue';
|
||||
|
||||
import type {
|
||||
BaseUploadProps,
|
||||
CustomGetter,
|
||||
UploadEmits,
|
||||
UploadType,
|
||||
} from './props';
|
||||
|
||||
import type { AxiosProgressEvent, UploadResult } from '#/api';
|
||||
import type { OssFile } from '#/api/system/oss/model';
|
||||
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { isFunction, isString } from 'lodash-es';
|
||||
|
||||
import { ossInfo } from '#/api/system/oss';
|
||||
|
||||
/**
|
||||
* 图片预览hook
|
||||
* @returns 预览
|
||||
*/
|
||||
export function useImagePreview() {
|
||||
/**
|
||||
* 获取base64字符串
|
||||
* @param file 文件
|
||||
* @returns base64字符串
|
||||
*/
|
||||
function getBase64(file: File) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.addEventListener('load', () => resolve(reader.result));
|
||||
reader.addEventListener('error', (error) => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
// Modal可见
|
||||
const previewVisible = ref(false);
|
||||
// 预览的图片 url/base64
|
||||
const previewImage = ref('');
|
||||
// 预览的图片名称
|
||||
const previewTitle = ref('');
|
||||
|
||||
function handleCancel() {
|
||||
previewVisible.value = false;
|
||||
previewTitle.value = '';
|
||||
}
|
||||
|
||||
async function handlePreview(file: UploadFile) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
// 文件预览 取base64
|
||||
if (!file.url && !file.preview && file.originFileObj) {
|
||||
file.preview = (await getBase64(file.originFileObj)) as string;
|
||||
}
|
||||
// 这里不可能为空
|
||||
const url = file.url ?? '';
|
||||
previewImage.value = url || file.preview || '';
|
||||
previewVisible.value = true;
|
||||
previewTitle.value =
|
||||
file.name || url.slice(Math.max(0, url.lastIndexOf('/') + 1));
|
||||
}
|
||||
|
||||
return {
|
||||
previewVisible,
|
||||
previewImage,
|
||||
previewTitle,
|
||||
handleCancel,
|
||||
handlePreview,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传和文件上传的通用hook
|
||||
* @param props 组件props
|
||||
* @param emit 事件
|
||||
* @param bindValue 双向绑定的idList
|
||||
* @param uploadType 区分是文件还是图片上传
|
||||
* @returns hook
|
||||
*/
|
||||
export function useUpload(
|
||||
props: Readonly<BaseUploadProps>,
|
||||
emit: UploadEmits,
|
||||
bindValue: ModelRef<string | string[]>,
|
||||
uploadType: UploadType,
|
||||
) {
|
||||
// 组件内部维护fileList
|
||||
const innerFileList = ref<UploadFile[]>([]);
|
||||
|
||||
const acceptStr = computed(() => {
|
||||
// string类型
|
||||
if (isString(props.acceptFormat)) {
|
||||
return props.acceptFormat;
|
||||
}
|
||||
// 函数类型
|
||||
if (isFunction(props.acceptFormat)) {
|
||||
return props.acceptFormat(props.accept!);
|
||||
}
|
||||
// 默认 会对拓展名做处理
|
||||
return props.accept
|
||||
?.split(',')
|
||||
.map((item) => {
|
||||
if (item.startsWith('.')) {
|
||||
return item.slice(1);
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.join(', ');
|
||||
});
|
||||
|
||||
/**
|
||||
* 自定义文件显示名称 需要区分不同的接口
|
||||
* @param cb callback
|
||||
* @returns 文件名
|
||||
*/
|
||||
function transformFilename(cb: Parameters<CustomGetter<string>>[0]) {
|
||||
if (isFunction(props.customFilename)) {
|
||||
return props.customFilename(cb);
|
||||
}
|
||||
// info接口
|
||||
if (cb.type === 'info') {
|
||||
return cb.response.originalName;
|
||||
}
|
||||
// 上传接口
|
||||
return cb.response.fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义缩略图 需要区分不同的接口
|
||||
* @param cb callback
|
||||
* @returns 缩略图地址
|
||||
*/
|
||||
function transformThumbUrl(cb: Parameters<CustomGetter<undefined>>[0]) {
|
||||
if (isFunction(props.customThumbUrl)) {
|
||||
return props.customThumbUrl(cb);
|
||||
}
|
||||
// image 默认返回图片链接
|
||||
if (uploadType === 'image') {
|
||||
// info接口
|
||||
if (cb.type === 'info') {
|
||||
return cb.response.url;
|
||||
}
|
||||
// 上传接口
|
||||
return cb.response.url;
|
||||
}
|
||||
// 文件默认返回空 走antd默认的预览图逻辑
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 用来标识是否为上传 这样在watch内部不需要请求api
|
||||
let isUpload = false;
|
||||
function handleChange(info: UploadChangeParam) {
|
||||
/**
|
||||
* 移除当前文件
|
||||
* @param currentFile 当前文件
|
||||
* @param currentFileList 当前所有文件list
|
||||
*/
|
||||
function removeCurrentFile(
|
||||
currentFile: UploadChangeParam['file'],
|
||||
currentFileList: UploadChangeParam['fileList'],
|
||||
) {
|
||||
if (props.removeOnError) {
|
||||
currentFileList.splice(currentFileList.indexOf(currentFile), 1);
|
||||
} else {
|
||||
currentFile.status = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
const { file: currentFile, fileList } = info;
|
||||
|
||||
switch (currentFile.status) {
|
||||
// 上传成功 只是判断httpStatus 200 需要手动判断业务code
|
||||
case 'done': {
|
||||
if (!currentFile.response) {
|
||||
return;
|
||||
}
|
||||
// 获取返回结果 为customRequest的reslove参数
|
||||
// 只有success才会走到这里
|
||||
const { ossId, url } = currentFile.response as UploadResult;
|
||||
currentFile.url = url;
|
||||
currentFile.uid = ossId;
|
||||
|
||||
const cb = {
|
||||
type: 'upload',
|
||||
response: currentFile.response as UploadResult,
|
||||
} as const;
|
||||
|
||||
currentFile.fileName = transformFilename(cb);
|
||||
currentFile.name = transformFilename(cb);
|
||||
currentFile.thumbUrl = transformThumbUrl(cb);
|
||||
// 标记为上传 watch根据值做处理
|
||||
isUpload = true;
|
||||
// ossID添加 单个文件会被当做string
|
||||
if (props.maxCount === 1) {
|
||||
bindValue.value = ossId;
|
||||
} else {
|
||||
// 给默认值
|
||||
if (!Array.isArray(bindValue.value)) {
|
||||
bindValue.value = [];
|
||||
}
|
||||
// 直接使用.value无法触发useForm的更新(原生是正常的) 需要修改地址
|
||||
bindValue.value = [...bindValue.value, ossId];
|
||||
}
|
||||
break;
|
||||
}
|
||||
// 上传失败 网络原因导致httpStatus 不等于200
|
||||
case 'error': {
|
||||
removeCurrentFile(currentFile, fileList);
|
||||
}
|
||||
}
|
||||
emit('change', info);
|
||||
}
|
||||
|
||||
function handleRemove(currentFile: UploadFile) {
|
||||
function remove() {
|
||||
// fileList会自行处理删除 这里只需要处理ossId
|
||||
if (props.maxCount === 1) {
|
||||
bindValue.value = '';
|
||||
} else {
|
||||
(bindValue.value as string[]).splice(
|
||||
bindValue.value.indexOf(currentFile.uid),
|
||||
1,
|
||||
);
|
||||
}
|
||||
// 触发remove事件
|
||||
emit('remove', currentFile);
|
||||
}
|
||||
|
||||
if (!props.removeConfirm) {
|
||||
remove();
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
Modal.confirm({
|
||||
title: $t('pages.common.tip'),
|
||||
content: $t('component.upload.confirmDelete', [currentFile.name]),
|
||||
okButtonProps: { danger: true },
|
||||
centered: true,
|
||||
onOk() {
|
||||
resolve(true);
|
||||
remove();
|
||||
},
|
||||
onCancel() {
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前检测文件大小
|
||||
* 拖拽时候前置会有浏览器自身的accept校验 校验失败不会执行此方法
|
||||
* @param file file
|
||||
* @returns file | false
|
||||
*/
|
||||
function beforeUpload(file: FileType) {
|
||||
const isLtMax = file.size / 1024 / 1024 < props.maxSize!;
|
||||
if (!isLtMax) {
|
||||
message.error($t('component.upload.maxSize', [props.maxSize]));
|
||||
return false;
|
||||
}
|
||||
// 大坑 Safari不支持file-type库 去除文件类型的校验
|
||||
return file;
|
||||
}
|
||||
|
||||
const uploadAbort = new AbortController();
|
||||
/**
|
||||
* 自定义上传实现
|
||||
* @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, {
|
||||
onUploadProgress: progressEvent,
|
||||
signal: uploadAbort.signal,
|
||||
otherData: props?.data,
|
||||
});
|
||||
info.onSuccess!(res);
|
||||
if (props.showSuccessMsg) {
|
||||
message.success($t('component.upload.uploadSuccess'));
|
||||
}
|
||||
emit('success', info.file as RcFile, res);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
info.onError!(error);
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
props.abortOnUnmounted && uploadAbort.abort();
|
||||
});
|
||||
|
||||
/**
|
||||
* 这里默认只监听list地址变化 即重新赋值才会触发watch
|
||||
* immediate用于初始化触发
|
||||
*/
|
||||
watch(
|
||||
() => bindValue.value,
|
||||
async (value) => {
|
||||
if (value.length === 0) {
|
||||
// 清空绑定值时,同时清空innerFileList,避免外部使用时还能读取到
|
||||
innerFileList.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 上传完毕 不需要调用获取信息接口
|
||||
if (isUpload) {
|
||||
// 清理 使下一次状态可用
|
||||
isUpload = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await ossInfo(value);
|
||||
function transformFile(info: OssFile) {
|
||||
const cb = { type: 'info', response: info } as const;
|
||||
|
||||
const fileitem: UploadFile = {
|
||||
uid: info.ossId,
|
||||
name: transformFilename(cb),
|
||||
fileName: transformFilename(cb),
|
||||
url: info.url,
|
||||
thumbUrl: transformThumbUrl(cb),
|
||||
status: 'done',
|
||||
};
|
||||
return fileitem;
|
||||
}
|
||||
const transformOptions = resp.map((item) => transformFile(item));
|
||||
innerFileList.value = transformOptions;
|
||||
// 单文件 丢弃策略
|
||||
if (props.maxCount === 1 && resp.length === 0 && !props.keepMissingId) {
|
||||
bindValue.value = '';
|
||||
return;
|
||||
}
|
||||
// 多文件
|
||||
// 单文件查到了也会走这里的逻辑 filter会报错 需要maxCount判断处理
|
||||
if (
|
||||
resp.length !== value.length &&
|
||||
!props.keepMissingId &&
|
||||
props.maxCount !== 1
|
||||
) {
|
||||
// 给默认值
|
||||
if (!Array.isArray(bindValue.value)) {
|
||||
bindValue.value = [];
|
||||
}
|
||||
bindValue.value = bindValue.value.filter((ossId) =>
|
||||
resp.map((res) => res.ossId).includes(ossId),
|
||||
);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
return {
|
||||
handleChange,
|
||||
handleRemove,
|
||||
beforeUpload,
|
||||
customRequest,
|
||||
innerFileList,
|
||||
acceptStr,
|
||||
};
|
||||
}
|
190
apps/web-antd/src/components/upload/src/image-upload.vue
Normal file
190
apps/web-antd/src/components/upload/src/image-upload.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<!--
|
||||
不再支持url 统一使用ossId
|
||||
去除使用`file-type`库进行文件类型检测 在Safari无法使用
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
UploadFile,
|
||||
UploadListType,
|
||||
} from 'ant-design-vue/es/upload/interface';
|
||||
|
||||
import type { BaseUploadProps, UploadEmits } from './props';
|
||||
|
||||
import { $t, I18nT } from '@vben/locales';
|
||||
|
||||
import { PlusOutlined, UploadOutlined } from '@ant-design/icons-vue';
|
||||
import { Image, ImagePreviewGroup, Upload } from 'ant-design-vue';
|
||||
import { isFunction } from 'lodash-es';
|
||||
|
||||
import { uploadApi } from '#/api';
|
||||
|
||||
import { defaultImageAcceptExts } from './helper';
|
||||
import { useImagePreview, useUpload } from './hook';
|
||||
|
||||
interface ImageUploadProps extends BaseUploadProps {
|
||||
/**
|
||||
* 同antdv的listType
|
||||
* @default picture-card
|
||||
*/
|
||||
listType?: UploadListType;
|
||||
/**
|
||||
* 使用list-type: picture-card时 是否显示动画
|
||||
* 会有一个`弹跳`的效果 默认关闭
|
||||
* @default false
|
||||
*/
|
||||
withAnimation?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ImageUploadProps>(), {
|
||||
api: () => uploadApi,
|
||||
removeOnError: true,
|
||||
showSuccessMsg: true,
|
||||
removeConfirm: false,
|
||||
accept: defaultImageAcceptExts.join(','),
|
||||
data: () => undefined,
|
||||
maxCount: 1,
|
||||
maxSize: 5,
|
||||
disabled: false,
|
||||
listType: 'picture-card',
|
||||
helpMessage: true,
|
||||
enableDragUpload: false,
|
||||
abortOnUnmounted: true,
|
||||
withAnimation: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<UploadEmits>();
|
||||
|
||||
// 双向绑定 ossId
|
||||
const ossIdList = defineModel<string | string[]>('value', {
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
const {
|
||||
acceptStr,
|
||||
handleChange,
|
||||
handleRemove,
|
||||
beforeUpload,
|
||||
innerFileList,
|
||||
customRequest,
|
||||
} = useUpload(props, emit, ossIdList, 'image');
|
||||
|
||||
const { previewVisible, previewImage, handleCancel, handlePreview } =
|
||||
useImagePreview();
|
||||
|
||||
function currentPreview(file: UploadFile) {
|
||||
// 有自定义预览逻辑走自定义
|
||||
if (isFunction(props.preview)) {
|
||||
return props.preview(file);
|
||||
}
|
||||
// 否则走默认预览
|
||||
return handlePreview(file);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Upload
|
||||
v-model:file-list="innerFileList"
|
||||
:class="{ 'upload-animation__disabled': !withAnimation }"
|
||||
:list-type="listType"
|
||||
:accept="accept"
|
||||
:disabled="disabled"
|
||||
:directory="directory"
|
||||
:max-count="maxCount"
|
||||
:progress="{ showInfo: true }"
|
||||
:multiple="multiple"
|
||||
:before-upload="beforeUpload"
|
||||
:custom-request="customRequest"
|
||||
@preview="currentPreview"
|
||||
@change="handleChange"
|
||||
@remove="handleRemove"
|
||||
>
|
||||
<div
|
||||
v-if="innerFileList?.length < maxCount && listType === 'picture-card'"
|
||||
>
|
||||
<PlusOutlined />
|
||||
<div class="mt-[8px]">{{ $t('component.upload.upload') }}</div>
|
||||
</div>
|
||||
<a-button
|
||||
v-if="innerFileList?.length < maxCount && listType !== 'picture-card'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<UploadOutlined />
|
||||
{{ $t('component.upload.upload') }}
|
||||
</a-button>
|
||||
</Upload>
|
||||
<slot name="helpMessage" v-bind="{ maxCount, disabled, maxSize, accept }">
|
||||
<I18nT
|
||||
v-if="helpMessage"
|
||||
scope="global"
|
||||
keypath="component.upload.uploadHelpMessage"
|
||||
tag="div"
|
||||
:class="{
|
||||
'upload-text__disabled': disabled,
|
||||
'mt-2': listType !== 'picture-card',
|
||||
}"
|
||||
>
|
||||
<template #size>
|
||||
<span
|
||||
class="text-primary mx-1 font-medium"
|
||||
:class="{ 'upload-text__disabled': disabled }"
|
||||
>
|
||||
{{ maxSize }}MB
|
||||
</span>
|
||||
</template>
|
||||
<template #ext>
|
||||
<span
|
||||
class="text-primary mx-1 font-medium"
|
||||
:class="{ 'upload-text__disabled': disabled }"
|
||||
>
|
||||
{{ acceptStr }}
|
||||
</span>
|
||||
</template>
|
||||
</I18nT>
|
||||
</slot>
|
||||
|
||||
<ImagePreviewGroup
|
||||
:preview="{
|
||||
visible: previewVisible,
|
||||
onVisibleChange: handleCancel,
|
||||
}"
|
||||
>
|
||||
<Image class="hidden" :src="previewImage" />
|
||||
</ImagePreviewGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.ant-upload-select-picture-card {
|
||||
i {
|
||||
@apply text-[32px] text-[#999];
|
||||
}
|
||||
|
||||
.ant-upload-text {
|
||||
@apply mt-[8px] text-[#666];
|
||||
}
|
||||
}
|
||||
|
||||
.ant-upload-list-picture-card {
|
||||
.ant-upload-list-item::before {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用的样式和antd保持一致
|
||||
.upload-text__disabled {
|
||||
color: rgb(50 54 57 / 25%);
|
||||
cursor: not-allowed;
|
||||
|
||||
&:where(.dark, .dark *) {
|
||||
color: rgb(242 242 242 / 25%);
|
||||
}
|
||||
}
|
||||
|
||||
// list-type: picture-card动画效果关闭样式
|
||||
.upload-animation__disabled {
|
||||
.ant-upload-animate-inline {
|
||||
animation-duration: 0s !important;
|
||||
}
|
||||
}
|
||||
</style>
|
26
apps/web-antd/src/components/upload/src/note.md
Normal file
26
apps/web-antd/src/components/upload/src/note.md
Normal file
@@ -0,0 +1,26 @@
|
||||
Safari在执行到beforeUpload方法
|
||||
|
||||
有两种情况
|
||||
|
||||
1. 不继续执行 也无法上传(没有调用上传)
|
||||
2. 报错
|
||||
|
||||
Unhandled Promise Rejection: TypeError: ReadableStreamBYOBReader needs a ReadableByteStreamController
|
||||
|
||||
https://github.com/oven-sh/bun/issues/12908#issuecomment-2490151231
|
||||
|
||||
刚开始以为是异步的问题 由于`file-type`调用了异步方法 调试也是在这里没有后续打印了
|
||||
|
||||
使用别的异步代码测试结果是正常上传的
|
||||
|
||||
```js
|
||||
return new Promise<FileType>((resolve) =>
|
||||
setTimeout(() => resolve(file), 2000),
|
||||
);
|
||||
```
|
||||
|
||||
根本原因在于`file-typ`库的`fileTypeFromBlob`方法不支持Safari 去掉可以正常上传
|
||||
|
||||
safari不支持`ReadableStreamBYOBReader`api
|
||||
|
||||
详见: https://github.com/sindresorhus/file-type/issues/690
|
122
apps/web-antd/src/components/upload/src/props.d.ts
vendored
Normal file
122
apps/web-antd/src/components/upload/src/props.d.ts
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { UploadFile } from 'ant-design-vue';
|
||||
import type { RcFile } from 'ant-design-vue/es/vc-upload/interface';
|
||||
|
||||
import type { UploadApi, UploadResult } from '#/api';
|
||||
import type { OssFile } from '#/api/system/oss/model';
|
||||
|
||||
import { UploadChangeParam } from 'ant-design-vue';
|
||||
|
||||
export type UploadType = 'file' | 'image';
|
||||
|
||||
/**
|
||||
* 自定义返回文件名/缩略图使用 泛型控制返回是否必填
|
||||
* type 为不同的接口返回值 需要自行if判断
|
||||
*/
|
||||
export type CustomGetter<T extends string | undefined> = (
|
||||
cb:
|
||||
| { response: OssFile; type: 'info' }
|
||||
| { response: UploadResult; type: 'upload' },
|
||||
) => T extends undefined ? string | undefined : string;
|
||||
|
||||
export interface BaseUploadProps {
|
||||
/**
|
||||
* 上传接口
|
||||
*/
|
||||
api?: UploadApi;
|
||||
/**
|
||||
* 文件上传失败 是否从展示列表中删除
|
||||
* @default true
|
||||
*/
|
||||
removeOnError?: boolean;
|
||||
/**
|
||||
* 上传成功 是否展示提示信息
|
||||
* @default true
|
||||
*/
|
||||
showSuccessMsg?: boolean;
|
||||
/**
|
||||
* 删除文件前是否需要确认
|
||||
* @default false
|
||||
*/
|
||||
removeConfirm?: boolean;
|
||||
/**
|
||||
* 同antdv参数
|
||||
*/
|
||||
accept?: string;
|
||||
/**
|
||||
* 你可能使用的是application/pdf这种mime类型, 但是这样用户可能看不懂, 在这里自定义逻辑
|
||||
* @default 原始accept
|
||||
*/
|
||||
acceptFormat?: ((accept: string) => string) | 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;
|
||||
/**
|
||||
* 是否支持上传文件夹
|
||||
* @default false
|
||||
*/
|
||||
directory?: boolean;
|
||||
/**
|
||||
* 是否支持拖拽上传
|
||||
* @default false
|
||||
*/
|
||||
enableDragUpload?: boolean;
|
||||
/**
|
||||
* 当ossId查询不到文件信息时 比如被删除了
|
||||
* 是否保留列表对应的ossId 默认不保留
|
||||
* @default false
|
||||
*/
|
||||
keepMissingId?: boolean;
|
||||
/**
|
||||
* 自定义文件/图片预览逻辑 比如: 你可以改为下载
|
||||
* 图片上传默认为预览
|
||||
* 文件上传默认为window.open
|
||||
* @param file file
|
||||
*/
|
||||
preview?: (file: UploadFile) => Promise<void> | void;
|
||||
/**
|
||||
* 是否在组件Unmounted时取消上传
|
||||
* @default true
|
||||
*/
|
||||
abortOnUnmounted?: boolean;
|
||||
/**
|
||||
* 自定义文件名 需要区分两个接口的返回值
|
||||
*/
|
||||
customFilename?: CustomGetter<string>;
|
||||
/**
|
||||
* 自定义缩略图 需要区分两个接口的返回值
|
||||
*/
|
||||
customThumbUrl?: CustomGetter<undefined>;
|
||||
}
|
||||
|
||||
export interface UploadEmits {
|
||||
(e: 'success', file: RcFile, response: UploadResult): void;
|
||||
(e: 'remove', file: UploadFile): void;
|
||||
(e: 'change', info: UploadChangeParam): void;
|
||||
}
|
Reference in New Issue
Block a user