element查看文件和图片组件[vue2]

<!-- components/Preview.vue -->
<template>
	<div v-if="visible" class="preview-overlay" @click.self="close">
		<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 -->
					<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>

					<!-- 文本文件 -->
					<div v-else-if="type === 'text'" class="preview-text">
						<pre>{{ textContent }}</pre>
					</div>

					<!-- Office 文档 -->
					<div v-else-if="type === 'office'" class="office-preview">
						<div v-if="officePreviewUrl" class="iframe-wrapper">
							<iframe
								:src="officePreviewUrl"
								frameborder="0"
								class="preview-media"
								@load="onOfficeLoad"
							></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: "",
			fileType: "",
			fileName: "",
			textContent: "",
			officePreviewUrl: "",
			blobData: null,
			errorMessage: "",
			officeLoadTimer: null,
		};
	},
	computed: {
		type() {
			if (!this.fileType && this.fileUrl) {
				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";
			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: {
		guessTypeFromUrl(url) {
			try {
				const urlObj = new URL(url);
				const path = urlObj.pathname;
				const ext = path.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";
			} catch {
				const ext = url.split(".").pop().toLowerCase().split("?")[0];
				const officeExts = [
					"doc",
					"docx",
					"xls",
					"xlsx",
					"ppt",
					"pptx",
				];
				if (officeExts.includes(ext)) return "office";
				return "unknown";
			}
		},

		async open(file, options = {}) {
			this.close(true);
			this.visible = false;
			this.textContent = "";
			this.officePreviewUrl = "";
			this.errorMessage = "";
			this.loading = true;
			this.visible = true;

			try {
				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") {
					let fileName = "";
					let ext = "";
					try {
						const urlObj = new URL(file);
						const path = urlObj.pathname;
						fileName = path.split("/").pop() || "";
						ext = fileName.includes(".")
							? fileName.split(".").pop().toLowerCase()
							: "";
					} catch {
						const parts = file.split("/");
						fileName = parts[parts.length - 1].split("?")[0];
						ext = fileName.includes(".")
							? fileName.split(".").pop().toLowerCase()
							: "";
					}
					const officeExts = [
						"doc",
						"docx",
						"xls",
						"xlsx",
						"ppt",
						"pptx",
					];
					const isOffice = officeExts.includes(ext);

					if (isOffice) {
						rawUrl = file;
						rawType = options.type || "";
						rawName = options.name || fileName;
						// 注意:此时不设置 loading = false,将在 iframe load 或超时后关闭
					} else {
						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 || fileName;
						} catch (err) {
							console.error("获取文件失败", err);
							rawUrl = file;
							rawType = options.type || "";
							rawName = options.name || fileName;
							this.errorMessage = `文件加载失败:${err.message}`;
						} finally {
							this.loading = false;
						}
					}
				} else if (file && typeof file === "object") {
					rawUrl = file.url;
					rawType = file.type || "";
					rawName = file.name || "";
					this.loading = false;
				} else {
					throw new Error("无效的文件参数");
				}

				if (!this.visible) {
					if (rawUrl && rawUrl.startsWith("blob:"))
						URL.revokeObjectURL(rawUrl);
					return;
				}

				this.fileUrl = rawUrl;
				this.fileType = rawType;
				this.fileName = rawName;
				this.blobData = blobData;

				if (
					this.type === "text" &&
					!blobData &&
					rawUrl &&
					!rawUrl.startsWith("blob:")
				) {
					await this.loadTextContent();
				}

				if (this.type === "office") {
					const isPublicHttp =
						rawUrl &&
						(rawUrl.startsWith("http://") ||
							rawUrl.startsWith("https://")) &&
						!rawUrl.startsWith("blob:");
					if (isPublicHttp) {
						this.officePreviewUrl =
							this.getOfficePreviewUrl(rawUrl);
						// 保持 loading 为 true,等待 iframe 加载或超时
						this.loading = true;
						// 超时保护:10秒后自动关闭 loading
						this.officeLoadTimer = setTimeout(() => {
							this.loading = false;
							this.officeLoadTimer = null;
						}, 10000);
					} else {
						this.officePreviewUrl = "";
						this.loading = false;
					}
				}
			} catch (err) {
				console.error("预览出错", err);
				this.errorMessage = err.message || "预览失败";
				this.loading = false;
			}
		},

		onOfficeLoad() {
			if (this.officeLoadTimer) {
				clearTimeout(this.officeLoadTimer);
				this.officeLoadTimer = null;
			}
			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 = "无法加载文件内容";
			}
		},

		getOfficePreviewUrl(fileUrl) {
			const encodedUrl = encodeURIComponent(fileUrl);
			return `https://view.officeapps.live.com/op/embed.aspx?src=${encodedUrl}`;
		},

		close(skipCleanup = false) {
			if (this.officeLoadTimer) {
				clearTimeout(this.officeLoadTimer);
				this.officeLoadTimer = null;
			}
			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 = "";
			this.errorMessage = "";
		},
	},
};
</script>

<style scoped>
.download-btn {
	color: #3498db;
	cursor: pointer;
}
.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;
}
.preview-container {
	position: relative;
	width: 95vw;
	height: 90vh;
	max-width: 1400px;
	background: #fff;
	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;
}
/* 媒体元素填满容器 */
.preview-media {
	width: 90vw;
	height: 85vh;
	border-radius: 4px;
	border: none;
}
/* 图片保持原始比例 */
.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>

posted on 2026-04-17 17:50  jv_coder  阅读(9)  评论(0)    收藏  举报