<!-- components/Preview.vue -->
<template>
<div
v-if="visible"
class="preview-overlay"
@click.self="close"
:loading="loading"
>
<div class="preview-container">
<button class="close-btn" @click="close">×</button>
<div class="preview-content-wrapper">
<!-- loading 状态 -->
<div v-if="loading" class="preview-loading">
<div class="spinner"></div>
<p>加载文件中...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="errorMessage" class="preview-error">
<p>{{ errorMessage }}</p>
<button @click="close" class="error-close-btn">关闭</button>
</div>
<!-- 正常预览内容(原有内容) -->
<template v-else>
<!-- 图片 -->
<img v-if="type === 'image'" :src="fileUrl" class="preview-image" />
<!-- PDF (使用 embed 或 iframe) -->
<iframe
v-else-if="type === 'pdf'"
:src="fileUrl"
class="preview-media"
frameborder="0"
></iframe>
<!-- 视频 -->
<video v-else-if="type === 'video'" controls class="preview-media">
<source :src="fileUrl" :type="fileType" />
您的浏览器不支持视频播放。
</video>
<!-- 音频 -->
<audio v-else-if="type === 'audio'" controls class="preview-audio">
<source :src="fileUrl" :type="fileType" />
</audio>
<!-- 文本文件 (txt, js, css, json, xml, html 等) -->
<div v-else-if="type === 'text'" class="preview-text">
<pre>{{ textContent }}</pre>
</div>
<!-- Office 文档 (通过 Office Online 或 Google Docs 预览) -->
<div v-else-if="type === 'office'" class="office-preview">
<div v-if="officePreviewUrl" class="iframe-wrapper">
<iframe
:src="officePreviewUrl"
frameborder="0"
class="preview-media"
></iframe>
</div>
<div v-else class="download-prompt">
<p>无法在线预览此文档,请下载后查看。</p>
<a :href="fileUrl" :download="fileName" class="download-btn"
>下载文件</a
>
</div>
</div>
<!-- 不支持的类型 -->
<div v-else class="unsupported">
暂不支持预览该文件类型 ({{ fileType || "未知类型" }})
</div>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
name: "FilePreview",
data() {
return {
visible: false,
loading: false,
fileUrl: "", // blob URL 或 远程 URL
fileType: "", // MIME 类型,如 'image/png'
fileName: "", // 可选,用于判断扩展名
textContent: "", // 文本文件内容
officePreviewUrl: "", // Office 在线预览地址
blobData: null // Blob 数据
};
},
computed: {
type() {
if (!this.fileType && this.fileUrl) {
// 如果没有 MIME,尝试从 URL 后缀推断
return this.guessTypeFromUrl(this.fileUrl);
}
if (this.fileType.startsWith("image/")) return "image";
if (this.fileType === "application/pdf") return "pdf";
if (this.fileType.startsWith("video/")) return "video";
if (this.fileType.startsWith("audio/")) return "audio";
if (this.fileType.startsWith("text/")) return "text";
// Office 常见 MIME
const officeMimes = [
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
];
if (officeMimes.includes(this.fileType)) return "office";
// 其他类型可扩展
return "unknown";
}
},
methods: {
// 从 URL 后缀猜测类型(当没有 MIME 时备用)
guessTypeFromUrl(url) {
const ext = url
.split(".")
.pop()
.toLowerCase();
const imageExts = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"];
const videoExts = ["mp4", "webm", "ogg", "mov", "avi"];
const audioExts = ["mp3", "wav", "ogg", "flac"];
const textExts = ["txt", "js", "css", "json", "xml", "html", "htm", "md"];
const officeExts = ["doc", "docx", "xls", "xlsx", "ppt", "pptx"];
if (imageExts.includes(ext)) return "image";
if (ext === "pdf") return "pdf";
if (videoExts.includes(ext)) return "video";
if (audioExts.includes(ext)) return "audio";
if (textExts.includes(ext)) return "text";
if (officeExts.includes(ext)) return "office";
return "unknown";
},
// 主入口:打开预览
async open(file, options = {}) {
// 重置状态
this.close(true); // 强制重置不触发清理?我们单独处理
this.visible = false;
this.textContent = "";
this.officePreviewUrl = "";
this.loading = true;
this.visible = true;
try {
// file 可以是 File/Blob、string URL、或 { url, type, name } 对象
let rawUrl = "";
let rawType = "";
let rawName = "";
let blobData = null;
if (file instanceof File || file instanceof Blob) {
blobData = file;
rawUrl = URL.createObjectURL(file);
rawType = file.type;
rawName = file.name || "";
this.loading = false;
} else if (typeof file === "string") {
try {
const response = await fetch(file, {});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
blobData = blob;
rawUrl = URL.createObjectURL(blob);
rawType = blob.type;
rawName = options.name || file.split("/").pop();
} catch (err) {
console.error("获取文件失败", err);
// 回退到直接使用原 URL(可能还是无法预览)
rawUrl = file;
rawType = options.type || "";
rawName = options.name || "";
} finally {
this.loading = false;
}
} else if (file && typeof file === "object") {
rawUrl = file.url;
rawType = file.type || "";
rawName = file.name || "";
this.loading = false;
} else {
console.error("无效的文件参数");
return;
}
this.fileUrl = rawUrl;
this.fileType = rawType;
this.fileName = rawName;
this.blobData = blobData;
// 如果是文本类型,需要异步获取内容
if (this.type === "text" && !blobData) {
await this.loadTextContent();
}
// 如果是 Office 类型,构造在线预览 URL(使用 Microsoft Office Online)
if (this.type === "office") {
// 如果文件是通过 fetch + blob 生成的 URL,或者原始 URL 需要认证(无法公网访问),则不使用 Office Online 预览
const isPublicAccessible =
this.fileUrl &&
(this.fileUrl.startsWith("http://") ||
this.fileUrl.startsWith("https://")) &&
!this.fileUrl.startsWith("blob:") &&
!this.fileUrl.includes("out.bj.51db.com"); // 或其他内网/认证域名
if (isPublicAccessible) {
this.officePreviewUrl = this.getOfficePreviewUrl(this.fileUrl);
} else {
this.officePreviewUrl = ""; // 触发下载提示
}
}
} catch (err) {
console.error("预览出错", err);
this.errorMessage = err.message || "预览失败";
this.loading = false;
}
},
// 加载文本文件内容
async loadTextContent() {
if (!this.fileUrl) return;
try {
const response = await fetch(this.fileUrl);
this.textContent = await response.text();
} catch (error) {
console.error("加载文本文件失败", error);
this.textContent = "无法加载文件内容";
}
},
// 生成 Office 在线预览链接 (需要文件可公开访问)
getOfficePreviewUrl(fileUrl) {
// 使用 Microsoft Office Online 的嵌入预览
const encodedUrl = encodeURIComponent(fileUrl);
return `https://view.officeapps.live.com/op/embed.aspx?src=${encodedUrl}`;
},
close(skipCleanup = false) {
this.visible = false;
this.loading = false;
if (!skipCleanup && this.fileUrl && this.fileUrl.startsWith("blob:")) {
URL.revokeObjectURL(this.fileUrl);
}
this.blobData = null;
this.fileUrl = "";
this.fileType = "";
this.fileName = "";
this.textContent = "";
this.officePreviewUrl = "";
}
}
};
</script>
<style scoped>
.preview-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #666;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.preview-error {
text-align: center;
color: #e74c3c;
}
.error-close-btn {
margin-top: 16px;
padding: 6px 12px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.preview-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
/* 预览主容器:占据绝大部分屏幕,并使用 flex 列布局 */
.preview-container {
position: relative;
width: 95vw; /* 宽度占视口 95% */
height: 90vh; /* 高度占视口 90% */
max-width: 1400px; /* 可选:防止过宽 */
background: #fff;
/* border-radius: 12px; */
display: flex;
flex-direction: column; /* 关键:垂直排列 */
overflow: hidden; /* 防止溢出圆角 */
}
.close-btn {
position: absolute;
top: 12px;
right: 20px;
font-size: 28px;
color: #333;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
background: #FFF;
}
/* 内容包装区:自动填满剩余高度,并居中内部内容 */
.preview-content-wrapper {
flex: 1; /* 占据剩余所有空间 */
display: flex;
justify-content: center;
align-items: center;
overflow: auto; /* 如果内容溢出,允许滚动 */
padding: 16px;
}
/* 媒体元素(图片、视频、iframe)自适应缩放,保持比例 */
.preview-media {
max-width: 100%;
max-height: 100%;
width: 90%;
height: 90%;
border-radius: 4px;
}
.preview-image {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain; /* 保证完整显示,不裁剪 */
border-radius: 4px;
}
/* 音频控件单独样式(通常不需要很大) */
.preview-audio {
width: 80%;
min-width: 300px;
}
/* 文本预览区域:填满并允许滚动 */
.preview-text {
width: 100%;
height: 100%;
overflow: auto;
background: #f5f5f5;
padding: 16px;
border-radius: 8px;
}
.preview-text pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
font-family: "Courier New", monospace;
font-size: 14px;
}
/* 不支持的类型提示 */
.unsupported {
text-align: center;
color: #666;
font-size: 18px;
padding: 40px;
}
</style>