update: 兼容旧版本上传 增加ImageUploadOld/FileUploadOld(下个版本将移除)
This commit is contained in:
parent
2577ba5500
commit
36683dd218
19
CHANGELOG.md
19
CHANGELOG.md
@ -1,9 +1,24 @@
|
|||||||
# 1.3.0
|
# 1.3.0
|
||||||
|
|
||||||
|
注意: 如果你使用老版本的`文件上传`/`图片上传` 可暂时使用
|
||||||
|
|
||||||
|
- `component: 'ImageUploadOld'`
|
||||||
|
- `component: 'FileUploadOld'`
|
||||||
|
|
||||||
|
代替 **建议替换为新版本**
|
||||||
|
|
||||||
|
大致变动:
|
||||||
|
|
||||||
|
- `accept string[] -> string`
|
||||||
|
- `resultField 已经移除 统一使用ossId`
|
||||||
|
- `maxNumber -> maxCount`
|
||||||
|
|
||||||
|
具体参数查看: `apps/web-antd/src/components/upload/src/props.d.ts`
|
||||||
|
|
||||||
**REFACTOR**
|
**REFACTOR**
|
||||||
|
|
||||||
- 文件上传/图片上传重构(破坏性更新 不兼容之前的api)
|
- **文件上传/图片上传重构(破坏性更新 不兼容之前的api)**
|
||||||
- 文件上传/图片上传**不再支持**url用法 强制使用ossId
|
- **文件上传/图片上传**不再支持**url用法 强制使用ossId**
|
||||||
- TableSwitch组件重构
|
- TableSwitch组件重构
|
||||||
- 管理员租户切换不再返回首页 直接刷新当前页(除特殊页面外会回到首页)
|
- 管理员租户切换不再返回首页 直接刷新当前页(除特殊页面外会回到首页)
|
||||||
- modalLoading/drawerLoading改为调用内部的lock/unlock方法
|
- modalLoading/drawerLoading改为调用内部的lock/unlock方法
|
||||||
|
@ -40,6 +40,7 @@ import {
|
|||||||
|
|
||||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||||
import { FileUpload, ImageUpload } from '#/components/upload';
|
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||||
|
import { FileUploadOld, ImageUploadOld } from '#/components/upload-old';
|
||||||
|
|
||||||
const withDefaultPlaceholder = <T extends Component>(
|
const withDefaultPlaceholder = <T extends Component>(
|
||||||
component: T,
|
component: T,
|
||||||
@ -99,8 +100,10 @@ export type ComponentType =
|
|||||||
| 'DefaultButton'
|
| 'DefaultButton'
|
||||||
| 'Divider'
|
| 'Divider'
|
||||||
| 'FileUpload'
|
| 'FileUpload'
|
||||||
|
| 'FileUploadOld'
|
||||||
| 'IconPicker'
|
| 'IconPicker'
|
||||||
| 'ImageUpload'
|
| 'ImageUpload'
|
||||||
|
| 'ImageUploadOld'
|
||||||
| 'Input'
|
| 'Input'
|
||||||
| 'InputNumber'
|
| 'InputNumber'
|
||||||
| 'InputPassword'
|
| 'InputPassword'
|
||||||
@ -175,6 +178,8 @@ async function initComponentAdapter() {
|
|||||||
ImageUpload,
|
ImageUpload,
|
||||||
FileUpload,
|
FileUpload,
|
||||||
RichTextarea,
|
RichTextarea,
|
||||||
|
ImageUploadOld,
|
||||||
|
FileUploadOld,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 将组件注册到全局共享状态中
|
// 将组件注册到全局共享状态中
|
||||||
|
@ -19,18 +19,21 @@ export interface UploadResult {
|
|||||||
/**
|
/**
|
||||||
* 通过单文件上传接口
|
* 通过单文件上传接口
|
||||||
* @param file 上传的文件
|
* @param file 上传的文件
|
||||||
* @param otherData 其他请求参数 后端拓展可能会用到
|
|
||||||
* @param options 一些配置项
|
* @param options 一些配置项
|
||||||
* @param options.onUploadProgress 上传进度事件
|
* @param options.onUploadProgress 上传进度事件
|
||||||
* @param options.signal 上传取消信号
|
* @param options.signal 上传取消信号
|
||||||
|
* @param options.otherData 其他请求参数 后端拓展可能会用到
|
||||||
* @returns 上传结果
|
* @returns 上传结果
|
||||||
*/
|
*/
|
||||||
export function uploadApi(
|
export function uploadApi(
|
||||||
file: Blob | File,
|
file: Blob | File,
|
||||||
otherData?: Record<string, any>,
|
options?: {
|
||||||
options?: { onUploadProgress?: AxiosProgressEvent; signal?: AbortSignal },
|
onUploadProgress?: AxiosProgressEvent;
|
||||||
|
otherData?: Record<string, any>;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const { onUploadProgress, signal } = options ?? {};
|
const { onUploadProgress, signal, otherData = {} } = options ?? {};
|
||||||
return requestClient.upload<UploadResult>(
|
return requestClient.upload<UploadResult>(
|
||||||
'/resource/oss/upload',
|
'/resource/oss/upload',
|
||||||
{ file, ...otherData },
|
{ file, ...otherData },
|
||||||
|
8
apps/web-antd/src/components/upload-old/index.ts
Normal file
8
apps/web-antd/src/components/upload-old/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @description: 旧版文件上传组件 使用FileUpload代替
|
||||||
|
*/
|
||||||
|
export { default as FileUploadOld } from './src/file-upload.vue';
|
||||||
|
/**
|
||||||
|
* @description: 旧版图片上传组件 使用ImageUpload代替
|
||||||
|
*/
|
||||||
|
export { default as ImageUploadOld } from './src/image-upload.vue';
|
240
apps/web-antd/src/components/upload-old/src/file-upload.vue
Normal file
240
apps/web-antd/src/components/upload-old/src/file-upload.vue
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
<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, UploadApi } from '#/api';
|
||||||
|
|
||||||
|
import { ref, toRefs, watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { message, Upload } from 'ant-design-vue';
|
||||||
|
import { isArray, isFunction, isObject, isString } from 'lodash-es';
|
||||||
|
|
||||||
|
import { uploadApi } from '#/api';
|
||||||
|
|
||||||
|
import { checkFileType } from './helper';
|
||||||
|
import { UploadResultStatus } from './typing';
|
||||||
|
import { useUploadType } from './use-upload';
|
||||||
|
|
||||||
|
defineOptions({ name: 'FileUpload', inheritAttrs: false });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/**
|
||||||
|
* 建议使用拓展名(不带.)
|
||||||
|
* 或者文件头 image/png等(测试判断不准确) 不支持image/*类似的写法
|
||||||
|
* 需自行改造 ./helper/checkFileType方法
|
||||||
|
*/
|
||||||
|
accept?: string[];
|
||||||
|
api?: UploadApi;
|
||||||
|
disabled?: boolean;
|
||||||
|
helpText?: string;
|
||||||
|
// 最大数量的文件,Infinity不限制
|
||||||
|
maxNumber?: number;
|
||||||
|
// 文件最大多少MB
|
||||||
|
maxSize?: number;
|
||||||
|
// 是否支持多选
|
||||||
|
multiple?: boolean;
|
||||||
|
// support xxx.xxx.xx
|
||||||
|
// 返回的字段 默认url
|
||||||
|
resultField?: 'fileName' | 'ossId' | 'url' | string;
|
||||||
|
/**
|
||||||
|
* 是否显示下面的描述
|
||||||
|
*/
|
||||||
|
showDescription?: boolean;
|
||||||
|
value?: string[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
value: () => [],
|
||||||
|
disabled: false,
|
||||||
|
helpText: '',
|
||||||
|
maxSize: 2,
|
||||||
|
maxNumber: 1,
|
||||||
|
accept: () => [],
|
||||||
|
multiple: false,
|
||||||
|
api: () => uploadApi,
|
||||||
|
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 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 (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: '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 = await checkFileType(file, accept);
|
||||||
|
if (!isAct) {
|
||||||
|
message.error($t('component.upload.acceptUpload', [accept]));
|
||||||
|
isActMsg.value = false;
|
||||||
|
// 防止弹出多个错误提示
|
||||||
|
setTimeout(() => (isActMsg.value = true), 1000);
|
||||||
|
}
|
||||||
|
const isLt = file.size / 1024 / 1024 > maxSize;
|
||||||
|
if (isLt) {
|
||||||
|
message.error($t('component.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, {
|
||||||
|
onUploadProgress: progressEvent,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 由getValue处理 传对象过去
|
||||||
|
* 直接传string(id)会被转为Number
|
||||||
|
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
|
||||||
|
*/
|
||||||
|
info.onSuccess!(res);
|
||||||
|
message.success($t('component.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?.[props.resultField];
|
||||||
|
}
|
||||||
|
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
|
||||||
|
if (item?.url) {
|
||||||
|
return item.url;
|
||||||
|
}
|
||||||
|
// 注意这里取的key为 url
|
||||||
|
return item?.response?.url;
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Upload
|
||||||
|
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">
|
||||||
|
<a-button>
|
||||||
|
<UploadOutlined />
|
||||||
|
{{ $t('component.upload.upload') }}
|
||||||
|
</a-button>
|
||||||
|
</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>
|
||||||
|
</Upload>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ant-upload-select-picture-card i {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-upload-select-picture-card .ant-upload-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
51
apps/web-antd/src/components/upload-old/src/helper.ts
Normal file
51
apps/web-antd/src/components/upload-old/src/helper.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { fileTypeFromBlob } from '@vben/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不支持txt文件 @see https://github.com/sindresorhus/file-type/issues/55
|
||||||
|
* 需要自行修改
|
||||||
|
* @param file file对象
|
||||||
|
* @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
|
||||||
|
* @returns 是否通过文件类型校验
|
||||||
|
*/
|
||||||
|
export async function checkFileType(file: File, accepts: string[]) {
|
||||||
|
if (!accepts || accepts?.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.log(file);
|
||||||
|
const fileType = await fileTypeFromBlob(file);
|
||||||
|
if (!fileType) {
|
||||||
|
console.error('无法获取文件类型');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log('文件类型', fileType);
|
||||||
|
// 是否文件拓展名/文件头任意有一个匹配
|
||||||
|
return accepts.includes(fileType.ext) || accepts.includes(fileType.mime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认图片类型
|
||||||
|
*/
|
||||||
|
export const defaultImageAccept = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||||
|
/**
|
||||||
|
* 判断文件类型是否符合要求
|
||||||
|
* @param file file对象
|
||||||
|
* @param accepts 文件类型数组 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
|
||||||
|
* @returns 是否通过文件类型校验
|
||||||
|
*/
|
||||||
|
export async function checkImageFileType(file: File, accepts: string[]) {
|
||||||
|
// 空的accepts 使用默认规则
|
||||||
|
if (!accepts || accepts.length === 0) {
|
||||||
|
accepts = defaultImageAccept;
|
||||||
|
}
|
||||||
|
const fileType = await fileTypeFromBlob(file);
|
||||||
|
if (!fileType) {
|
||||||
|
console.error('无法获取文件类型');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log('文件类型', fileType);
|
||||||
|
// 是否文件拓展名/文件头任意有一个匹配
|
||||||
|
if (accepts.includes(fileType.ext) || accepts.includes(fileType.mime)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
323
apps/web-antd/src/components/upload-old/src/image-upload.vue
Normal file
323
apps/web-antd/src/components/upload-old/src/image-upload.vue
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
<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, UploadApi } from '#/api';
|
||||||
|
|
||||||
|
import { ref, toRefs, watch } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { message, Modal, Upload } from 'ant-design-vue';
|
||||||
|
import { isArray, isFunction, isObject, isString, uniqueId } from 'lodash-es';
|
||||||
|
|
||||||
|
import { uploadApi } from '#/api';
|
||||||
|
import { ossInfo } from '#/api/system/oss';
|
||||||
|
|
||||||
|
import { checkImageFileType, defaultImageAccept } from './helper';
|
||||||
|
import { UploadResultStatus } from './typing';
|
||||||
|
import { useUploadType } from './use-upload';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/**
|
||||||
|
* 包括拓展名(不带点) 文件头(image/png等 不包括泛写法即image/*)
|
||||||
|
*/
|
||||||
|
accept?: string[];
|
||||||
|
api?: UploadApi;
|
||||||
|
disabled?: boolean;
|
||||||
|
helpText?: string;
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
listType?: ListType;
|
||||||
|
// 最大数量的文件,Infinity不限制
|
||||||
|
maxNumber?: number;
|
||||||
|
// 文件最大多少MB
|
||||||
|
maxSize?: number;
|
||||||
|
// 是否支持多选
|
||||||
|
multiple?: boolean;
|
||||||
|
// support xxx.xxx.xx
|
||||||
|
// 返回的字段 默认url
|
||||||
|
resultField?: 'fileName' | 'ossId' | 'url';
|
||||||
|
/**
|
||||||
|
* 是否显示下面的描述
|
||||||
|
*/
|
||||||
|
showDescription?: boolean;
|
||||||
|
value?: string | string[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
value: () => [],
|
||||||
|
disabled: false,
|
||||||
|
listType: 'picture-card',
|
||||||
|
helpText: '',
|
||||||
|
maxSize: 2,
|
||||||
|
maxNumber: 1,
|
||||||
|
accept: () => defaultImageAccept,
|
||||||
|
multiple: false,
|
||||||
|
api: () => uploadApi,
|
||||||
|
resultField: 'url',
|
||||||
|
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) {
|
||||||
|
const _fileList: string[] = [];
|
||||||
|
if (isString(v)) {
|
||||||
|
_fileList.push(v);
|
||||||
|
}
|
||||||
|
if (isArray(v)) {
|
||||||
|
_fileList.push(...v);
|
||||||
|
}
|
||||||
|
// 直接赋值 可能为string | string[]
|
||||||
|
value = v;
|
||||||
|
const withUrlList: UploadProps['fileList'] = [];
|
||||||
|
for (const item of _fileList) {
|
||||||
|
// ossId情况
|
||||||
|
if (props.resultField === 'ossId') {
|
||||||
|
const resp = await ossInfo([item]);
|
||||||
|
if (item && isString(item)) {
|
||||||
|
withUrlList.push({
|
||||||
|
uid: item, // ossId作为uid 方便getValue获取
|
||||||
|
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||||
|
status: 'done',
|
||||||
|
url: resp?.[0]?.url,
|
||||||
|
});
|
||||||
|
} else if (item && isObject(item)) {
|
||||||
|
withUrlList.push({
|
||||||
|
...(item as any),
|
||||||
|
uid: item,
|
||||||
|
url: resp?.[0]?.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非ossId情况
|
||||||
|
if (item && isString(item)) {
|
||||||
|
withUrlList.push({
|
||||||
|
uid: uniqueId(),
|
||||||
|
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||||
|
status: 'done',
|
||||||
|
url: item,
|
||||||
|
});
|
||||||
|
} else if (item && isObject(item)) {
|
||||||
|
withUrlList.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileList.value = withUrlList;
|
||||||
|
}
|
||||||
|
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 = await checkImageFileType(file, accept);
|
||||||
|
if (!isAct) {
|
||||||
|
message.error($t('component.upload.acceptUpload', [accept]));
|
||||||
|
isActMsg.value = false;
|
||||||
|
// 防止弹出多个错误提示
|
||||||
|
setTimeout(() => (isActMsg.value = true), 1000);
|
||||||
|
}
|
||||||
|
const isLt = file.size / 1024 / 1024 > maxSize;
|
||||||
|
if (isLt) {
|
||||||
|
message.error($t('component.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, {
|
||||||
|
onUploadProgress: progressEvent,
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 由getValue处理 传对象过去
|
||||||
|
* 直接传string(id)会被转为Number
|
||||||
|
* 内部的逻辑由requestClient.upload处理 这里不用判断业务状态码 不符合会自动reject
|
||||||
|
*/
|
||||||
|
info.onSuccess!(res);
|
||||||
|
message.success($t('component.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() {
|
||||||
|
console.log(fileList.value);
|
||||||
|
const list = (fileList.value || [])
|
||||||
|
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||||
|
.map((item: any) => {
|
||||||
|
if (item?.response && props?.resultField) {
|
||||||
|
return item?.response?.[props.resultField];
|
||||||
|
}
|
||||||
|
// ossId兼容 uid为ossId直接返回
|
||||||
|
if (props.resultField === 'ossId' && item.uid) {
|
||||||
|
return item.uid;
|
||||||
|
}
|
||||||
|
// 适用于已经有图片 回显的情况 会默认在init处理为{url: 'xx'}
|
||||||
|
if (item?.url) {
|
||||||
|
return item.url;
|
||||||
|
}
|
||||||
|
// 注意这里取的key为 url
|
||||||
|
return item?.response?.url;
|
||||||
|
});
|
||||||
|
// 只有一张图片 默认绑定string而非string[]
|
||||||
|
if (props.maxNumber === 1 && list.length === 1) {
|
||||||
|
return list[0];
|
||||||
|
}
|
||||||
|
// 只有一张图片 && 删除图片时 可自行修改
|
||||||
|
if (props.maxNumber === 1 && list.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
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">
|
||||||
|
<PlusOutlined />
|
||||||
|
<div style="margin-top: 8px">{{ $t('component.upload.upload') }}</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="" style="width: 100%" />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ant-upload-select-picture-card i {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-upload-select-picture-card .ant-upload-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
37
apps/web-antd/src/components/upload-old/src/typing.ts
Normal file
37
apps/web-antd/src/components/upload-old/src/typing.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
export enum UploadResultStatus {
|
||||||
|
DONE = 'done',
|
||||||
|
ERROR = 'error',
|
||||||
|
SUCCESS = 'success',
|
||||||
|
UPLOADING = 'uploading',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileItem {
|
||||||
|
thumbUrl?: string;
|
||||||
|
name: string;
|
||||||
|
size: number | string;
|
||||||
|
type?: string;
|
||||||
|
percent: number;
|
||||||
|
file: File;
|
||||||
|
status?: UploadResultStatus;
|
||||||
|
response?: Recordable<any> | { fileName: string; ossId: string; url: string };
|
||||||
|
uuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Wrapper {
|
||||||
|
record: FileItem;
|
||||||
|
uidKey: string;
|
||||||
|
valueKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseFileItem {
|
||||||
|
uid: number | string;
|
||||||
|
url: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
export interface PreviewFileItem {
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
61
apps/web-antd/src/components/upload-old/src/use-upload.ts
Normal file
61
apps/web-antd/src/components/upload-old/src/use-upload.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import { computed, unref } from 'vue';
|
||||||
|
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
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('component.upload.accept', [accept.join(',')]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSize = unref(maxSizeRef);
|
||||||
|
if (maxSize) {
|
||||||
|
helpTexts.push($t('component.upload.maxSize', [maxSize]));
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxNumber = unref(maxNumberRef);
|
||||||
|
if (maxNumber && maxNumber !== Infinity) {
|
||||||
|
helpTexts.push($t('component.upload.maxNumber', [maxNumber]));
|
||||||
|
}
|
||||||
|
return helpTexts.join(',');
|
||||||
|
});
|
||||||
|
return { getAccept, getStringAccept, getHelpText };
|
||||||
|
}
|
@ -286,9 +286,10 @@ export function useUpload(
|
|||||||
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
||||||
info.onProgress!({ percent });
|
info.onProgress!({ percent });
|
||||||
};
|
};
|
||||||
const res = await api(info.file as File, props?.data ?? {}, {
|
const res = await api(info.file as File, {
|
||||||
onUploadProgress: progressEvent,
|
onUploadProgress: progressEvent,
|
||||||
signal: uploadAbort.signal,
|
signal: uploadAbort.signal,
|
||||||
|
otherData: props?.data,
|
||||||
});
|
});
|
||||||
info.onSuccess!(res);
|
info.onSuccess!(res);
|
||||||
if (props.showSuccessMsg) {
|
if (props.showSuccessMsg) {
|
||||||
|
Loading…
Reference in New Issue
Block a user