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

<!-- 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>

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