Files
hzdyjs/web/src/components/ImageUpload/index.vue
T
2025-08-03 17:38:03 +08:00

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>