+
请上传不超过
{{ maxSize }}MB
的
@@ -195,3 +311,35 @@ function getValue() {
+
+
diff --git a/apps/web-antd/src/components/upload/image-upload.vue b/apps/web-antd/src/components/upload/image-upload.vue
index db9928ba4aee2784152bf6394b20f714eb6ad5ae..4f0e9d1bf13a5747d278889e9c8029ca8d7d3747 100644
--- a/apps/web-antd/src/components/upload/image-upload.vue
+++ b/apps/web-antd/src/components/upload/image-upload.vue
@@ -6,7 +6,7 @@ import type { FileUploadProps } from './typing';
import type { AxiosProgressEvent } from '#/api/infra/file';
-import { ref, toRefs, watch } from 'vue';
+import { computed, ref, toRefs, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
@@ -22,6 +22,7 @@ defineOptions({ name: 'ImageUpload', inheritAttrs: false });
const props = withDefaults(defineProps
(), {
value: () => [],
+ modelValue: undefined,
directory: undefined,
disabled: false,
listType: 'picture-card',
@@ -34,7 +35,12 @@ const props = withDefaults(defineProps(), {
resultField: '',
showDescription: true,
});
-const emit = defineEmits(['change', 'update:value', 'delete']);
+const emit = defineEmits([
+ 'change',
+ 'update:value',
+ 'update:modelValue',
+ 'delete',
+]);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref(false);
const { getStringAccept } = useUploadType({
@@ -43,6 +49,16 @@ const { getStringAccept } = useUploadType({
maxNumberRef: maxNumber,
maxSizeRef: maxSize,
});
+
+// 计算当前绑定的值,优先使用 modelValue
+const currentValue = computed(() => {
+ return props.modelValue === undefined ? props.value : props.modelValue;
+});
+
+// 判断是否使用 modelValue
+const isUsingModelValue = computed(() => {
+ return props.modelValue !== undefined;
+});
const previewOpen = ref(false); // 是否展示预览
const previewImage = ref(''); // 预览图片
const previewTitle = ref(''); // 预览标题
@@ -51,9 +67,11 @@ const fileList = ref([]);
const isLtMsg = ref(true); // 文件大小错误提示
const isActMsg = ref(true); // 文件类型错误提示
const isFirstRender = ref(true); // 是否第一次渲染
+const uploadNumber = ref(0); // 上传文件计数器
+const uploadList = ref([]); // 临时上传列表
watch(
- () => props.value,
+ currentValue,
async (v) => {
if (isInnerOperate.value) {
isInnerOperate.value = false;
@@ -122,6 +140,7 @@ async function handleRemove(file: UploadFile) {
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
+ emit('update:modelValue', value);
emit('change', value);
emit('delete', file);
}
@@ -133,6 +152,12 @@ function handleCancel() {
}
async function beforeUpload(file: File) {
+ // 检查文件数量限制
+ if (fileList.value!.length >= props.maxNumber) {
+ message.error($t('ui.upload.maxNumber', [props.maxNumber]));
+ return Upload.LIST_IGNORE;
+ }
+
const { maxSize, accept } = props;
const isAct = checkImgType(file, accept);
if (!isAct) {
@@ -140,6 +165,7 @@ async function beforeUpload(file: File) {
isActMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isActMsg.value = true), 1000);
+ return Upload.LIST_IGNORE;
}
const isLt = file.size / 1024 / 1024 > maxSize;
if (isLt) {
@@ -147,8 +173,12 @@ async function beforeUpload(file: File) {
isLtMsg.value = false;
// 防止弹出多个错误提示
setTimeout(() => (isLtMsg.value = true), 1000);
+ return Upload.LIST_IGNORE;
}
- return (isAct && !isLt) || Upload.LIST_IGNORE;
+
+ // 只有在验证通过后才增加计数器
+ uploadNumber.value++;
+ return true;
}
async function customRequest(info: UploadRequestOption) {
@@ -163,20 +193,59 @@ async function customRequest(info: UploadRequestOption) {
info.onProgress!({ percent });
};
const res = await api?.(info.file as File, progressEvent);
+
+ // 处理上传成功后的逻辑
+ handleUploadSuccess(res, info.file as File);
+
info.onSuccess!(res);
message.success($t('ui.upload.uploadSuccess'));
+ } catch (error: any) {
+ console.error(error);
+ info.onError!(error);
+ handleUploadError(error);
+ }
+}
- // 更新文件
+// 处理上传成功
+function handleUploadSuccess(res: any, file: File) {
+ // 删除临时文件
+ const index = fileList.value?.findIndex((item) => item.name === file.name);
+ if (index !== -1) {
+ fileList.value?.splice(index!, 1);
+ }
+
+ // 添加到临时上传列表
+ const fileUrl = res?.url || res?.data || res;
+ uploadList.value.push({
+ name: file.name,
+ url: fileUrl,
+ status: UploadResultStatus.DONE,
+ uid: file.name + Date.now(),
+ });
+
+ // 检查是否所有文件都上传完成
+ if (uploadList.value.length >= uploadNumber.value) {
+ fileList.value?.push(...uploadList.value);
+ uploadList.value = [];
+ uploadNumber.value = 0;
+
+ // 更新值
const value = getValue();
isInnerOperate.value = true;
emit('update:value', value);
+ emit('update:modelValue', value);
emit('change', value);
- } catch (error: any) {
- console.error(error);
- info.onError!(error);
}
}
+// 处理上传错误
+function handleUploadError(error: any) {
+ console.error('上传错误:', error);
+ message.error($t('ui.upload.uploadError'));
+ // 上传失败时减少计数器
+ uploadNumber.value = Math.max(0, uploadNumber.value - 1);
+}
+
function getValue() {
const list = (fileList.value || [])
.filter((item) => item?.status === UploadResultStatus.DONE)
@@ -186,11 +255,26 @@ function getValue() {
}
return item?.url || item?.response?.url || item?.response;
});
- // add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型
+
+ // 单个文件的情况,根据输入参数类型决定返回格式
if (props.maxNumber === 1) {
- return list.length > 0 ? list[0] : '';
+ const singleValue = list.length > 0 ? list[0] : '';
+ // 如果原始值是字符串或 modelValue 是字符串,返回字符串
+ if (
+ isString(props.value) ||
+ (isUsingModelValue.value && isString(props.modelValue))
+ ) {
+ return singleValue;
+ }
+ return singleValue;
}
- return list;
+
+ // 多文件情况,根据输入参数类型决定返回格式
+ if (isUsingModelValue.value) {
+ return Array.isArray(props.modelValue) ? list : list.join(',');
+ }
+
+ return Array.isArray(props.value) ? list : list.join(',');
}
diff --git a/apps/web-antd/src/components/upload/typing.ts b/apps/web-antd/src/components/upload/typing.ts
index 90134ff6fdbef3995195cb7b1be198d5a2e41b43..ada73d244fb0a3b04a8131afeb5177fcc25d7b32 100644
--- a/apps/web-antd/src/components/upload/typing.ts
+++ b/apps/web-antd/src/components/upload/typing.ts
@@ -21,10 +21,12 @@ export interface FileUploadProps {
// 上传的目录
directory?: string;
disabled?: boolean;
+ drag?: boolean; // 是否支持拖拽上传
helpText?: string;
listType?: UploadListType;
// 最大数量的文件,Infinity不限制
maxNumber?: number;
+ modelValue?: string | string[]; // v-model 支持
// 文件最大多少MB
maxSize?: number;
// 是否支持多选
diff --git a/packages/icons/src/iconify/index.ts b/packages/icons/src/iconify/index.ts
index 54857f9c2591030dffc3e51f8bc75664558c2545..784a746b6a10ce1cb5094161d86383510d53987d 100644
--- a/packages/icons/src/iconify/index.ts
+++ b/packages/icons/src/iconify/index.ts
@@ -61,15 +61,3 @@ export const MenuOutlined = createIconifyIcon('ant-design:menu-outlined');
export const PlusOutlined = createIconifyIcon('ant-design:plus-outlined');
export const SelectOutlined = createIconifyIcon('ant-design:select-outlined');
-
-export const CheckCircleFilled = createIconifyIcon(
- 'ant-design:check-circle-filled',
-);
-
-export const ExclamationCircleFilled = createIconifyIcon(
- 'ant-design:exclamation-circle-filled',
-);
-
-export const QuestionCircleFilled = createIconifyIcon(
- 'ant-design:question-circle-filled',
-);