803 lines
27 KiB
Vue
803 lines
27 KiB
Vue
<template>
|
|
<div class="component-upload-image">
|
|
<el-upload
|
|
ref="imageUploadRef"
|
|
multiple
|
|
:action="uploadImgUrl"
|
|
list-type="picture-card"
|
|
:on-success="handleUploadSuccess"
|
|
:before-upload="handleBeforeUpload"
|
|
:limit="limit"
|
|
:accept="fileAccept"
|
|
:on-error="handleUploadError"
|
|
:on-exceed="handleExceed"
|
|
:before-remove="handleDelete"
|
|
:show-file-list="true"
|
|
:headers="headers"
|
|
:file-list="fileList"
|
|
:on-preview="handlePictureCardPreview"
|
|
:class="{ hide: fileList.length >= limit }"
|
|
>
|
|
<el-icon class="avatar-uploader-icon">
|
|
<plus />
|
|
</el-icon>
|
|
</el-upload>
|
|
<!-- 上传提示 -->
|
|
<div v-if="showTip" class="el-upload__tip">
|
|
请上传
|
|
<template v-if="fileSize">
|
|
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
|
|
</template>
|
|
<template v-if="fileType">
|
|
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
|
|
</template>
|
|
的文件
|
|
</div>
|
|
|
|
<el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
|
|
<div v-loading="previewLoading" style="text-align: center; min-height: 200px">
|
|
<img
|
|
v-if="dialogImageUrl"
|
|
:src="dialogImageUrl"
|
|
style="display: block; max-width: 100%; margin: 0 auto"
|
|
@error="handlePreviewError"
|
|
@load="handlePreviewLoad"
|
|
/>
|
|
<div v-if="previewError" class="preview-error">
|
|
<el-icon size="48"><picture-filled /></el-icon>
|
|
<div class="error-text">图片预览失败</div>
|
|
</div>
|
|
</div>
|
|
</el-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch, nextTick, getCurrentInstance, onBeforeUnmount, type ComponentInternalInstance } from 'vue';
|
|
import { listByIds, delOss } from '@/api/system/oss';
|
|
import { OssVO } from '@/api/system/oss/types';
|
|
import { propTypes } from '@/utils/propTypes';
|
|
import { globalHeaders } from '@/utils/request';
|
|
import { compressAccurately } from 'image-conversion';
|
|
import { PictureFilled, ZoomIn, Delete } from '@element-plus/icons-vue';
|
|
import request from '@/utils/request';
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: [String, Object, Array],
|
|
default: () => []
|
|
},
|
|
// 图片数量限制
|
|
limit: propTypes.number.def(15),
|
|
// 大小限制(MB)
|
|
fileSize: propTypes.number.def(10),
|
|
// 文件类型, 例如['png', 'jpg', 'jpeg']
|
|
fileType: propTypes.array.def(['png', 'jpg', 'jpeg']),
|
|
// 是否显示提示
|
|
isShowTip: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
// 是否支持压缩,默认否
|
|
compressSupport: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
// 压缩目标大小,单位KB。默认300KB以上文件才压缩,并压缩至300KB以内
|
|
compressTargetSize: propTypes.number.def(300)
|
|
});
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
const emit = defineEmits(['update:modelValue']);
|
|
const number = ref(0);
|
|
const uploadList = ref<any[]>([]);
|
|
const dialogImageUrl = ref('');
|
|
const dialogVisible = ref(false);
|
|
|
|
// 预览相关状态
|
|
const previewLoading = ref(false);
|
|
const previewError = ref(false);
|
|
const previewUrlCache = ref<Map<string, string>>(new Map());
|
|
const previewBlobUrls = ref<string[]>([]);
|
|
|
|
// 异步加载相关状态
|
|
const loadingStates = ref<Map<string, boolean>>(new Map());
|
|
const loadErrorStates = ref<Map<string, boolean>>(new Map());
|
|
|
|
// 使用环境变量
|
|
const baseUrl = import.meta.env.VITE_APP_BASE_API;
|
|
const uploadImgUrl = ref(baseUrl + '/resource/oss/upload'); // 上传的图片服务器地址
|
|
const headers = ref(globalHeaders());
|
|
|
|
const fileList = ref<any[]>([]);
|
|
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
|
|
|
|
const imageUploadRef = ref<any>();
|
|
|
|
// 添加标志位,避免上传成功后重复处理
|
|
const isInternalUpdate = ref(false);
|
|
|
|
// 监听 fileType 变化,更新 fileAccept
|
|
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','));
|
|
|
|
// 清理预览blob URL,防止内存泄漏
|
|
const cleanupPreviewBlobUrls = () => {
|
|
previewBlobUrls.value.forEach((url) => {
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
previewBlobUrls.value = [];
|
|
previewUrlCache.value.clear();
|
|
};
|
|
|
|
// 组件卸载时清理blob URLs
|
|
onBeforeUnmount(() => {
|
|
cleanupPreviewBlobUrls();
|
|
});
|
|
|
|
// 验证blob数据是否为有效图片
|
|
const validateImageBlob = (blob: Blob): boolean => {
|
|
// 允许的图片类型,包括通用的octet-stream类型(OSS服务器常返回此类型)
|
|
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'application/octet-stream'];
|
|
|
|
// 基本验证:类型检查和大小检查
|
|
const hasValidType = validTypes.includes(blob.type) || blob.type === '' || blob.type === 'application/octet-stream';
|
|
const hasValidSize = blob.size > 0;
|
|
|
|
return hasValidType && hasValidSize;
|
|
};
|
|
|
|
// 通过认证请求获取图片数据并转换为blob URL
|
|
const getPreviewImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
|
try {
|
|
// 检查是否已有缓存的blob URL
|
|
const cachedUrl = previewUrlCache.value.get(imageUrl);
|
|
if (cachedUrl) {
|
|
return cachedUrl;
|
|
}
|
|
|
|
// 通过request请求图片数据,这样会自动带上token和clientId
|
|
const response = await request({
|
|
url: imageUrl,
|
|
method: 'get',
|
|
responseType: 'blob',
|
|
timeout: 300000 // 300秒超时
|
|
});
|
|
|
|
// 由于request响应拦截器对blob类型响应直接返回blob数据
|
|
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
|
const blob = response;
|
|
|
|
// 验证响应数据
|
|
if (!blob) {
|
|
throw new Error('响应数据为空');
|
|
}
|
|
|
|
if (!(blob instanceof Blob)) {
|
|
// 如果不是Blob,尝试获取实际内容用于调试
|
|
let actualContent = 'Unknown';
|
|
try {
|
|
if (typeof blob === 'string') {
|
|
actualContent = (blob as string).substring(0, 200);
|
|
} else if (blob && typeof blob.toString === 'function') {
|
|
actualContent = blob.toString().substring(0, 200);
|
|
}
|
|
} catch (e) {
|
|
actualContent = 'Cannot convert to string';
|
|
}
|
|
|
|
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
|
}
|
|
|
|
// 检查blob内容类型
|
|
// const contentType = blob.type;
|
|
|
|
// 如果Content-Type明确表明这不是图片数据且不是通用二进制格式,进行检查
|
|
// if (
|
|
// contentType &&
|
|
// !contentType.toLowerCase().startsWith('image/') &&
|
|
// !contentType.toLowerCase().includes('octet-stream') &&
|
|
// (contentType.toLowerCase().includes('text/') || contentType.toLowerCase().includes('html'))
|
|
// ) {
|
|
// const text = await blob.text();
|
|
// const preview = text.substring(0, 300);
|
|
// throw new Error(`响应Content-Type不是图片格式: ${contentType}。响应内容预览: ${preview}`);
|
|
// }
|
|
|
|
// 验证是否为有效图片
|
|
// if (!validateImageBlob(blob)) {
|
|
// throw new Error(`无效的图片格式: ${blob.type}, 大小: ${blob.size}`);
|
|
// }
|
|
|
|
// 创建blob URL
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
|
|
// 缓存blob URL
|
|
previewUrlCache.value.set(imageUrl, blobUrl);
|
|
previewBlobUrls.value.push(blobUrl);
|
|
|
|
return blobUrl;
|
|
} catch (error) {
|
|
console.error('获取预览图片失败:', error);
|
|
return '';
|
|
}
|
|
};
|
|
|
|
// 通过ossid获取预览URL
|
|
const getPreviewUrlByOssId = async (ossId: string): Promise<string> => {
|
|
try {
|
|
// const response = await listByIds(ossId);
|
|
// // RuoYi框架的响应格式:{code, msg, data},实际数据在data字段中
|
|
// const ossList: OssVO[] = (response as any)?.data || response;
|
|
|
|
// if (!ossList || ossList.length === 0) {
|
|
// throw new Error(`OSS ID ${ossId} 在数据库中不存在或已被删除`);
|
|
// }
|
|
|
|
// const ossFile = ossList[0];
|
|
|
|
// if (!ossFile || !ossFile.url) {
|
|
// throw new Error(`OSS文件缺少url属性`);
|
|
// }
|
|
|
|
// const imageUrl = ossFile.url;
|
|
|
|
// // 检查URL是否有效
|
|
// if (!imageUrl || typeof imageUrl !== 'string') {
|
|
// throw new Error(`OSS接口返回的URL无效`);
|
|
// }
|
|
|
|
// 获取认证的blob URL
|
|
return await getPreviewImageBlobUrl('/resource/oss/download/' + ossId);
|
|
} catch (error) {
|
|
console.error('通过ossId获取预览图片失败:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// 异步加载单个文件的图片
|
|
const loadFileImageAsync = async (item: any, index: number): Promise<void> => {
|
|
const fileKey = item.ossId || item.url || `file_${index}`;
|
|
|
|
// 设置加载状态
|
|
loadingStates.value.set(fileKey, true);
|
|
loadErrorStates.value.set(fileKey, false);
|
|
|
|
try {
|
|
let blobUrl = '';
|
|
|
|
if (item.ossId) {
|
|
// 优先通过OSS ID获取预览URL
|
|
console.log(`loadFileImageAsync: 通过OSS ID获取预览URL for ${item.ossId}`);
|
|
blobUrl = await getPreviewUrlByOssId(item.ossId);
|
|
} else if (typeof item === 'string') {
|
|
// 直接URL情况
|
|
console.log(`loadFileImageAsync: 直接URL情况 for ${item}`);
|
|
blobUrl = await getPreviewImageBlobUrl(item);
|
|
} else if (item.url) {
|
|
// OSS对象情况(降级处理)
|
|
console.log(`loadFileImageAsync: OSS对象情况 for ${item.url}`);
|
|
blobUrl = await getPreviewImageBlobUrl(item.url);
|
|
}
|
|
|
|
if (blobUrl) {
|
|
// 更新文件列表中的对应项
|
|
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId || f.url === item.url || f.originalUrl === item.originalUrl);
|
|
|
|
if (fileIndex !== -1) {
|
|
fileList.value[fileIndex].url = blobUrl;
|
|
fileList.value[fileIndex].isLoaded = true;
|
|
fileList.value[fileIndex].isPlaceholder = false;
|
|
console.log(`loadFileImageAsync: 成功更新文件 ${item.ossId || item.url}`);
|
|
}
|
|
} else {
|
|
throw new Error('获取图片blob URL失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('异步加载图片失败:', error);
|
|
loadErrorStates.value.set(fileKey, true);
|
|
|
|
// 降级处理:尝试通过OSS ID重新获取
|
|
if (item.ossId) {
|
|
try {
|
|
console.log(`loadFileImageAsync: 降级处理,重新通过OSS ID获取 ${item.ossId}`);
|
|
const fallbackBlobUrl = await getPreviewUrlByOssId(item.ossId);
|
|
|
|
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
|
|
if (fileIndex !== -1 && fallbackBlobUrl) {
|
|
fileList.value[fileIndex].url = fallbackBlobUrl;
|
|
fileList.value[fileIndex].isLoaded = true;
|
|
fileList.value[fileIndex].isPlaceholder = false;
|
|
console.log(`loadFileImageAsync: 降级处理成功 for ${item.ossId}`);
|
|
}
|
|
} catch (fallbackError) {
|
|
console.error('loadFileImageAsync: 降级处理也失败:', fallbackError);
|
|
}
|
|
}
|
|
} finally {
|
|
loadingStates.value.set(fileKey, false);
|
|
}
|
|
};
|
|
|
|
// 批量异步加载图片
|
|
const loadImagesAsync = async (items: any[]): Promise<void> => {
|
|
// 先创建占位框,使用一个占位图片URL
|
|
const placeholderItems = items.map((item, index) => {
|
|
const name =
|
|
item.url && (item.url.startsWith('http://') || item.url.startsWith('https://')) ? `${item.url}_${index}` : item.ossId || `file_${index}`;
|
|
|
|
return {
|
|
name: name,
|
|
url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjZjVmN2ZhIi8+CjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjE1IiBzdHJva2U9IiM5MDkzOTkiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtZGFzaGFycmF5PSI2LCAzIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KCTxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjAgNTAgNTA7MzYwIDUwIDUwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIvPgo8L2NpcmNsZT4KPC9zdmc+', // 小圆圈浅灰色加载中占位图片
|
|
originalUrl: item.url,
|
|
ossId: item.ossId,
|
|
isLoaded: false, // 标记为未加载
|
|
isPlaceholder: true // 标记为占位符
|
|
};
|
|
});
|
|
|
|
// 立即更新文件列表显示占位框
|
|
fileList.value = placeholderItems;
|
|
|
|
// 异步加载所有图片
|
|
const loadPromises = items.map((item, index) => loadFileImageAsync(item, index));
|
|
|
|
// 等待所有图片加载完成(使用Promise.all替代allSettled以兼容旧版本)
|
|
await Promise.all(loadPromises);
|
|
};
|
|
|
|
// 图片预览加载成功
|
|
const handlePreviewLoad = () => {
|
|
previewError.value = false;
|
|
};
|
|
|
|
// 图片预览加载失败
|
|
const handlePreviewError = () => {
|
|
previewError.value = true;
|
|
};
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
async (val: string) => {
|
|
// 如果是内部更新触发的,跳过处理
|
|
if (isInternalUpdate.value) {
|
|
return;
|
|
}
|
|
|
|
if (val) {
|
|
// 首先将值转为数组
|
|
let list: OssVO[] = [];
|
|
if (Array.isArray(val)) {
|
|
list = val as OssVO[];
|
|
} else if (typeof val === 'string') {
|
|
console.log('val', val);
|
|
// 检查是否包含逗号分隔的OSS ID
|
|
if (val.includes(',')) {
|
|
// 多个OSS ID用逗号分隔,需要调用接口获取详细信息
|
|
const ossIds = val.split(',').filter((id) => id.trim());
|
|
try {
|
|
const res = await listByIds(ossIds.join(','));
|
|
list = res.data;
|
|
} catch (error) {
|
|
console.error('调用listByIds接口失败:', error);
|
|
list = [];
|
|
}
|
|
} else if (val.startsWith('http://') || val.startsWith('https://')) {
|
|
// 单个URL
|
|
list = [{ ossId: val, url: val, originalName: 'image' }] as any;
|
|
} else {
|
|
// 单个OSS ID,调用接口获取详细信息
|
|
try {
|
|
const res = await listByIds(val);
|
|
list = res.data;
|
|
} catch (error) {
|
|
console.error('调用listByIds接口失败:', error);
|
|
list = [];
|
|
}
|
|
}
|
|
}
|
|
|
|
// 使用异步加载方式处理文件列表
|
|
try {
|
|
await loadImagesAsync(list);
|
|
console.log('fileList with async loaded images', fileList.value);
|
|
} catch (error) {
|
|
console.error('异步加载文件列表失败:', error);
|
|
// 降级处理:使用原始URL
|
|
fileList.value = list.map((item, index) => {
|
|
let itemData;
|
|
if (typeof item === 'string') {
|
|
itemData = { name: item, url: item, originalUrl: item, isLoaded: true };
|
|
} else {
|
|
const name = item.url && (item.url.startsWith('http://') || item.url.startsWith('https://')) ? `${item.url}_${index}` : item.ossId;
|
|
itemData = { name: name, url: item.url, originalUrl: item.url, ossId: item.ossId, isLoaded: true };
|
|
}
|
|
return itemData;
|
|
});
|
|
console.log('fileList (fallback)', fileList.value);
|
|
}
|
|
} else {
|
|
fileList.value = [];
|
|
return [];
|
|
}
|
|
},
|
|
{ deep: true, immediate: true }
|
|
);
|
|
|
|
/** 上传前loading加载 */
|
|
const handleBeforeUpload = (file: any) => {
|
|
let isImg = false;
|
|
if (props.fileType.length) {
|
|
let fileExtension = '';
|
|
if (file.name.lastIndexOf('.') > -1) {
|
|
fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
|
|
}
|
|
isImg = props.fileType.some((type: any) => {
|
|
if (file.type.indexOf(type) > -1) return true;
|
|
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
|
|
return false;
|
|
});
|
|
} else {
|
|
isImg = file.type.indexOf('image') > -1;
|
|
}
|
|
if (!isImg) {
|
|
(proxy as any)?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`);
|
|
return false;
|
|
}
|
|
if (file.name.includes(',')) {
|
|
(proxy as any)?.$modal.msgError('文件名不正确,不能包含英文逗号!');
|
|
return false;
|
|
}
|
|
if (props.fileSize) {
|
|
const isLt = file.size / 1024 / 1024 < props.fileSize;
|
|
if (!isLt) {
|
|
(proxy as any)?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//压缩图片,开启压缩并且大于指定的压缩大小时才压缩
|
|
if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
|
|
(proxy as any)?.$modal.loading('正在上传图片,请稍候...');
|
|
number.value++;
|
|
return compressAccurately(file, props.compressTargetSize);
|
|
} else {
|
|
(proxy as any)?.$modal.loading('正在上传图片,请稍候...');
|
|
number.value++;
|
|
}
|
|
};
|
|
|
|
// 文件个数超出
|
|
const handleExceed = () => {
|
|
(proxy as any)?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
|
|
};
|
|
|
|
// 上传成功回调
|
|
const handleUploadSuccess = (res: any, file: any) => {
|
|
console.log('handleUploadSuccess:res', res);
|
|
console.log('handleUploadSuccess:file', file);
|
|
|
|
if (res.code === 200) {
|
|
const uploadItem = {
|
|
name: res.data.fileName,
|
|
url: res.data.url,
|
|
ossId: res.data.ossId
|
|
};
|
|
console.log('添加上传项到uploadList:', uploadItem);
|
|
uploadList.value.push(uploadItem);
|
|
uploadedSuccessfully();
|
|
} else {
|
|
number.value--;
|
|
(proxy as any)?.$modal.closeLoading();
|
|
(proxy as any)?.$modal.msgError(res.msg);
|
|
imageUploadRef.value?.handleRemove(file);
|
|
uploadedSuccessfully();
|
|
}
|
|
};
|
|
|
|
// 删除图片
|
|
const handleDelete = (file: any): boolean => {
|
|
const findex = fileList.value.map((f) => f.name).indexOf(file.name);
|
|
if (findex > -1 && uploadList.value.length === number.value) {
|
|
const ossId = fileList.value[findex].ossId;
|
|
if (ossId) {
|
|
// delOss(ossId);
|
|
}
|
|
|
|
// 清理对应的blob URL
|
|
const fileToDelete = fileList.value[findex];
|
|
if (fileToDelete.url && fileToDelete.url.startsWith('blob:')) {
|
|
URL.revokeObjectURL(fileToDelete.url);
|
|
}
|
|
|
|
fileList.value.splice(findex, 1);
|
|
// 设置内部更新标志
|
|
isInternalUpdate.value = true;
|
|
emit('update:modelValue', listToString(fileList.value));
|
|
// 重置标志
|
|
nextTick(() => {
|
|
isInternalUpdate.value = false;
|
|
});
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// 上传结束处理
|
|
const uploadedSuccessfully = async () => {
|
|
console.log('uploadedSuccessfully called, number:', number.value, 'uploadList length:', uploadList.value.length);
|
|
|
|
if (number.value > 0 && uploadList.value.length === number.value) {
|
|
console.log('开始处理新上传的文件:', uploadList.value);
|
|
|
|
// 对新上传的文件进行异步加载处理
|
|
const newUploadedFiles = uploadList.value.map((item) => ({
|
|
...item,
|
|
isLoaded: false,
|
|
isPlaceholder: false
|
|
}));
|
|
|
|
console.log('处理后的新上传文件:', newUploadedFiles);
|
|
|
|
// 先显示新上传的文件(使用占位符)
|
|
const placeholderItems = newUploadedFiles.map((item, index) => ({
|
|
name: item.name,
|
|
url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjZjVmN2ZhIi8+CjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjE1IiBzdHJva2U9IiM5MDkzOTkiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtZGFzaGFycmF5PSI2LCAzIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KCTxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjAgNTAgNTA7MzYwIDUwIDUwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIvPgo8L2NpcmNsZT4KPC9zdmc+',
|
|
originalUrl: item.url,
|
|
ossId: item.ossId,
|
|
isLoaded: false,
|
|
isPlaceholder: true
|
|
}));
|
|
|
|
console.log('占位符项目:', placeholderItems);
|
|
|
|
// 合并现有文件和新上传的文件
|
|
const existingFiles = fileList.value.filter((f) => f.url !== undefined);
|
|
console.log('现有文件:', existingFiles);
|
|
|
|
fileList.value = existingFiles.concat(placeholderItems);
|
|
console.log('合并后的文件列表:', fileList.value);
|
|
|
|
// 异步加载新上传的图片
|
|
const loadPromises = newUploadedFiles.map((item, index) => {
|
|
const fileKey = item.ossId || item.url || `file_${index}`;
|
|
|
|
console.log(`开始加载文件 ${index}:`, item);
|
|
|
|
// 设置加载状态
|
|
loadingStates.value.set(fileKey, true);
|
|
loadErrorStates.value.set(fileKey, false);
|
|
|
|
return (async () => {
|
|
try {
|
|
let blobUrl = '';
|
|
|
|
if (item.ossId) {
|
|
console.log(`通过OSS ID获取预览URL for ${item.ossId}`);
|
|
// 通过OSS ID获取预览URL
|
|
blobUrl = await getPreviewUrlByOssId(item.ossId);
|
|
console.log(`获取到的blob URL for ${item.ossId}:`, blobUrl);
|
|
} else if (item.url) {
|
|
console.log(`通过URL获取blob URL for ${item.ossId}:`, item.url);
|
|
// 直接URL情况(降级处理)
|
|
blobUrl = await getPreviewImageBlobUrl(item.url);
|
|
console.log(`获取到的blob URL for ${item.ossId}:`, blobUrl);
|
|
}
|
|
|
|
if (blobUrl) {
|
|
// 更新文件列表中的对应项
|
|
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
|
|
console.log(`更新文件列表索引 ${fileIndex} for ${item.ossId}`);
|
|
|
|
if (fileIndex !== -1) {
|
|
fileList.value[fileIndex].url = blobUrl;
|
|
fileList.value[fileIndex].isLoaded = true;
|
|
fileList.value[fileIndex].isPlaceholder = false;
|
|
console.log(`成功更新文件 ${item.ossId}`);
|
|
}
|
|
} else {
|
|
throw new Error('获取图片blob URL失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('异步加载新上传图片失败:', error);
|
|
loadErrorStates.value.set(fileKey, true);
|
|
|
|
// 降级处理:通过OSS ID重新获取
|
|
try {
|
|
console.log(`降级处理:重新通过OSS ID获取 ${item.ossId}`);
|
|
const fallbackBlobUrl = await getPreviewUrlByOssId(item.ossId);
|
|
|
|
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
|
|
if (fileIndex !== -1 && fallbackBlobUrl) {
|
|
fileList.value[fileIndex].url = fallbackBlobUrl;
|
|
fileList.value[fileIndex].isLoaded = true;
|
|
fileList.value[fileIndex].isPlaceholder = false;
|
|
console.log(`降级处理成功,通过OSS ID重新获取 for ${item.ossId}`);
|
|
} else {
|
|
// 最后的降级:使用原始URL
|
|
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
|
|
if (fileIndex !== -1) {
|
|
fileList.value[fileIndex].url = item.url;
|
|
fileList.value[fileIndex].isLoaded = true;
|
|
fileList.value[fileIndex].isPlaceholder = false;
|
|
console.log(`最终降级处理,使用原始URL for ${item.ossId}`);
|
|
}
|
|
}
|
|
} catch (fallbackError) {
|
|
console.error('降级处理也失败:', fallbackError);
|
|
// 最后的降级:使用原始URL
|
|
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
|
|
if (fileIndex !== -1) {
|
|
fileList.value[fileIndex].url = item.url;
|
|
fileList.value[fileIndex].isLoaded = true;
|
|
fileList.value[fileIndex].isPlaceholder = false;
|
|
console.log(`最终降级处理,使用原始URL for ${item.ossId}`);
|
|
}
|
|
}
|
|
} finally {
|
|
loadingStates.value.set(fileKey, false);
|
|
}
|
|
})();
|
|
});
|
|
|
|
// 等待所有图片加载完成
|
|
await Promise.all(loadPromises);
|
|
console.log('所有图片加载完成,最终文件列表:', fileList.value);
|
|
|
|
uploadList.value = [];
|
|
number.value = 0;
|
|
// 设置内部更新标志
|
|
isInternalUpdate.value = true;
|
|
emit('update:modelValue', listToString(fileList.value));
|
|
// 重置标志
|
|
nextTick(() => {
|
|
isInternalUpdate.value = false;
|
|
});
|
|
(proxy as any)?.$modal.closeLoading();
|
|
}
|
|
};
|
|
|
|
// 上传失败
|
|
const handleUploadError = () => {
|
|
(proxy as any)?.$modal.msgError('上传图片失败');
|
|
(proxy as any)?.$modal.closeLoading();
|
|
};
|
|
|
|
// 预览
|
|
const handlePictureCardPreview = async (file: any) => {
|
|
console.log('预览文件:', file);
|
|
previewLoading.value = true;
|
|
previewError.value = false;
|
|
dialogVisible.value = true;
|
|
|
|
try {
|
|
let previewUrl = '';
|
|
|
|
// 判断是否为ossId
|
|
if (file.ossId) {
|
|
// 通过ossId获取预览URL
|
|
previewUrl = await getPreviewUrlByOssId(file.ossId);
|
|
} else if (file.originalUrl) {
|
|
// 使用原始URL进行预览(避免重复转换)
|
|
previewUrl = await getPreviewImageBlobUrl(file.originalUrl);
|
|
} else if (file.url) {
|
|
// 直接URL预览
|
|
previewUrl = await getPreviewImageBlobUrl(file.url);
|
|
} else {
|
|
throw new Error('文件缺少必要的URL或ossId信息');
|
|
}
|
|
|
|
dialogImageUrl.value = previewUrl;
|
|
} catch (error) {
|
|
console.error('图片预览失败:', error);
|
|
previewError.value = true;
|
|
dialogImageUrl.value = '';
|
|
(proxy as any)?.$modal.msgError('图片预览失败');
|
|
} finally {
|
|
previewLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// 对象转成指定字符串分隔
|
|
const listToString = (list: any[], separator?: string) => {
|
|
let strs = '';
|
|
separator = separator || ',';
|
|
for (const i in list) {
|
|
if (undefined !== list[i].ossId && list[i].ossId.indexOf('blob:') !== 0) {
|
|
// 返回ossId用于数据存储
|
|
strs += list[i].ossId + separator;
|
|
}
|
|
}
|
|
return strs != '' ? strs.substring(0, strs.length - 1) : '';
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
// .el-upload--picture-card 控制加号部分
|
|
:deep(.hide .el-upload--picture-card) {
|
|
display: none;
|
|
}
|
|
|
|
// 预览错误状态样式
|
|
.preview-error {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
color: #909399;
|
|
min-height: 200px;
|
|
|
|
.error-text {
|
|
font-size: 14px;
|
|
margin-top: 8px;
|
|
text-align: center;
|
|
}
|
|
}
|
|
|
|
// 异步加载优化样式
|
|
:deep(.el-upload-list--picture-card .el-upload-list__item) {
|
|
// 占位图片加载时的样式
|
|
.el-upload-list__item-thumbnail {
|
|
transition: opacity 0.3s ease;
|
|
|
|
&.loading {
|
|
opacity: 0.6;
|
|
}
|
|
}
|
|
|
|
// 占位符图片样式优化
|
|
&.placeholder-item {
|
|
.el-upload-list__item-thumbnail {
|
|
background-color: #f5f7fa;
|
|
border: 1px dashed #d9d9d9;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
img {
|
|
opacity: 0.7;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
}
|
|
|
|
&:hover .el-upload-list__item-thumbnail img {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
// 加载完成后的样式
|
|
&.loaded-item {
|
|
.el-upload-list__item-thumbnail {
|
|
img {
|
|
opacity: 1;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 加载动画
|
|
@keyframes loading-rotate {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
// 占位符加载动画
|
|
@keyframes placeholder-pulse {
|
|
0%,
|
|
100% {
|
|
opacity: 0.6;
|
|
}
|
|
50% {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.placeholder-loading {
|
|
animation: placeholder-pulse 1.5s ease-in-out infinite;
|
|
}
|
|
</style>
|