| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- <template>
- <el-image
- :src="realSrc"
- fit="cover"
- :style="`width:${realWidth};height:${realHeight};`"
- :preview-src-list="previewDisabled ? [] : realSrcList"
- preview-teleported
- v-loading="loading"
- @error="handleImageError"
- @load="handleImageLoad"
- >
- <template #error>
- <div class="image-slot">
- <el-icon><picture-filled /></el-icon>
- <div class="error-text">图片加载失败</div>
- </div>
- </template>
- </el-image>
- </template>
- <script setup lang="ts">
- import { computed, ref, watch, onBeforeUnmount } from 'vue';
- import { listByIds } from '@/api/system/oss';
- import type { OssVO } from '@/api/system/oss/types';
- import request from '@/utils/request';
- const props = defineProps({
- src: {
- type: String,
- default: ''
- },
- width: {
- type: [Number, String],
- default: ''
- },
- height: {
- type: [Number, String],
- default: ''
- },
- // 是否为ossid模式
- isOssId: {
- type: Boolean,
- default: true
- },
- // 是否禁用预览功能
- previewDisabled: {
- type: Boolean,
- default: false
- }
- });
- const loading = ref(false);
- const urlCache = ref<Map<string, string>>(new Map());
- const blobUrls = ref<string[]>([]);
- // 图片加载成功事件
- const handleImageLoad = () => {
- // 图片加载成功
- };
- // 图片加载失败事件
- const handleImageError = (error: any) => {
- // 图片加载失败处理
- };
- // 清理blob URL,防止内存泄漏
- const cleanupBlobUrls = () => {
- blobUrls.value.forEach((url) => {
- URL.revokeObjectURL(url);
- });
- blobUrls.value = [];
- urlCache.value.clear();
- };
- // 组件卸载时清理blob URLs
- onBeforeUnmount(() => {
- cleanupBlobUrls();
- });
- // 验证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 getImageBlobUrl = async (imageUrl: string): Promise<string> => {
- try {
- // 检查是否已有缓存的blob URL
- const cachedUrl = urlCache.value.get(imageUrl);
- if (cachedUrl) {
- return cachedUrl;
- }
- // 通过axios请求图片数据,这样会自动带上token和clientId
- const response = await request({
- url: imageUrl,
- method: 'get',
- responseType: 'blob',
- timeout: 300000 // 300秒超时
- });
- // 由于axios响应拦截器对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
- // urlCache.value.set(imageUrl, blobUrl);
- // blobUrls.value.push(blobUrl);
- return blobUrl;
- } catch (error) {
- return '';
- }
- };
- // 通过ossid获取真实URL
- const getUrlByOssId = async (ossId: string): Promise<string> => {
- try {
- loading.value = true;
- // 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} 在数据库中不存在或已被删除。可能原因:1) OSS ID不正确 2) 文件已被删除 3) 权限不足`);
- // }
- // const ossFile = ossList[0];
- // // 额外验证ossFile对象的完整性
- // if (!ossFile || typeof ossFile !== 'object') {
- // throw new Error(`OSS数据格式异常:期望对象,实际获得 ${typeof ossFile}`);
- // }
- // if (!ossFile.url) {
- // throw new Error(`OSS文件缺少url属性。完整数据:${JSON.stringify(ossFile)}`);
- // }
- // const imageUrl = ossFile.url;
- // // 检查URL是否有效
- // if (!imageUrl || typeof imageUrl !== 'string') {
- // throw new Error(`OSS接口返回的URL无效。URL: ${imageUrl}, 类型: ${typeof imageUrl}`);
- // }
- // // 如果URL不是以/开头的下载路径,可能是直接的OSS URL
- // if (!imageUrl.startsWith('/resource/oss/download/')) {
- // // 对于直接的OSS URL,我们仍然通过认证请求获取
- // }
- // 获取认证的blob URL
- return await getImageBlobUrl('/resource/oss/download/' + ossId);
- } catch (error) {
- return '';
- } finally {
- loading.value = false;
- }
- };
- // 处理多个ossid的情况
- const getUrlsByOssIds = async (ossIds: string[]): Promise<string[]> => {
- const urls: string[] = [];
- for (const ossId of ossIds) {
- if (ossId.trim()) {
- const url = await getUrlByOssId(ossId.trim());
- if (url) {
- urls.push(url);
- }
- }
- }
- return urls;
- };
- // 处理普通URL模式(非ossId)
- const getUrlsByDirectUrls = async (urls: string[]): Promise<string[]> => {
- const blobUrls: string[] = [];
- for (const url of urls) {
- if (!url.trim()) {
- continue;
- }
- const u = url.trim();
- // 如果已经是 blob / data URL,直接使用
- if (u.startsWith('blob:') || u.startsWith('data:')) {
- blobUrls.push(u);
- continue;
- }
- // 如果是本地静态资源(例如 Vite 打包的 /src 或 assets 路径),或者是绝对路径但不是受保护的下载接口,
- // 直接使用 URL,不经由带认证的请求去获取(避免对静态资源发带 token 的 XHR 导致失败)
- const isHttp = /^https?:\/\//i.test(u);
- const isProtectedDownload = u.startsWith('/resource/oss/download/');
- const isLocalAsset = u.startsWith('/src') || u.startsWith('/assets') || (u.startsWith('/') && !isProtectedDownload && !isHttp);
- if (isLocalAsset || isHttp) {
- // 对于 http(s) 链接直接使用原始 URL(多数第三方/公开图片可直接加载)
- blobUrls.push(u);
- continue;
- }
- // 对于受保护的资源或需认证获取的场景,使用认证请求获取 blob 并创建 blob URL
- const blobUrl = await getImageBlobUrl(u);
- if (blobUrl) {
- blobUrls.push(blobUrl);
- }
- }
- return blobUrls;
- };
- const realSrc = ref('');
- const realSrcList = ref<string[]>([]);
- // 处理src变化
- watch(
- () => props.src,
- async (newSrc) => {
- // 清理之前的blob URLs
- cleanupBlobUrls();
- if (!newSrc) {
- realSrc.value = '';
- realSrcList.value = [];
- return;
- }
- loading.value = true;
- try {
- if (props.isOssId) {
- // console.log('ossid模式');
- // console.log(newSrc);
- // ossid模式:通过接口获取真实URL,然后转换为认证的blob URL
- const ossIds = newSrc.split(',').filter((id) => id.trim());
- const urls = await getUrlsByOssIds(ossIds);
- if (urls.length > 0) {
- realSrc.value = urls[0];
- realSrcList.value = urls;
- } else {
- realSrc.value = '';
- realSrcList.value = [];
- }
- } else {
- // 普通模式:直接使用src作为URL,但仍需要通过认证请求转换为blob URL
- const urls = newSrc.split(',').filter((url) => url.trim());
- const blobUrls = await getUrlsByDirectUrls(urls);
- if (blobUrls.length > 0) {
- realSrc.value = blobUrls[0];
- realSrcList.value = blobUrls;
- } else {
- realSrc.value = '';
- realSrcList.value = [];
- }
- }
- } catch (error) {
- realSrc.value = '';
- realSrcList.value = [];
- } finally {
- loading.value = false;
- }
- },
- { immediate: true }
- );
- const realWidth = computed(() => (typeof props.width == 'string' ? props.width : `${props.width}px`));
- const realHeight = computed(() => (typeof props.height == 'string' ? props.height : `${props.height}px`));
- </script>
- <style lang="scss" scoped>
- .el-image {
- border-radius: 5px;
- background-color: #ebeef5;
- box-shadow: 0 0 5px 1px #ccc;
- :deep(.el-image__inner) {
- transition: all 0.3s;
- cursor: pointer;
- &:hover {
- transform: scale(1.2);
- }
- }
- :deep(.image-slot) {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- width: 100%;
- height: 100%;
- color: #909399;
- font-size: 30px;
- .error-text {
- font-size: 12px;
- margin-top: 8px;
- text-align: center;
- }
- }
- }
- </style>
|