【Vue实战】打造全能文件预览组件:支持PDF/Word/Excel/PPT/图片/音视频及Markdown(基于vue-office)

【Vue实战】打造全能文件预览组件:支持PDF/Word/Excel/PPT/音视频及Markdown(基于vue-office)

文章目录

前言

好久没有更新了,一直在忙于准备高项考试,唉~,今天发一篇!!

业务场景

公司业务体系中,出彩在外的销售团队,客户经理等,在途中需要用电脑平板向客户展示公司的手册,技术方案,报价等各种文档证明,并且可提供下载,经过讨论,最终封装成文档组件查看器。

先看效果在谈实现

pdf预览效果:

docx预览效果:

excel预览效果:

ppt预览效果:

图片预览效果

视频预览效果

.txt文本文件预览效果

MD文档预览效果

技术栈

框架:

  • Vue 2.x

UI 库:

  • Element UI

核心依赖:

  • @vue-office/pdf (PDF 预览)
  • @vue-office/docx (Word 预览)
  • @vue-office/excel (Excel 预览)
  • @vue-office/pptx (PPT 预览 - 注:部分场景需公网链接)
  • marked (Markdown 渲染)
  • axios (文件流下载)

安装依赖

bash 复制代码
npm install @vue-office/pdf @vue-office/docx @vue-office/excel @vue-office/pptx marked axios

实现代码:

javascript 复制代码
<template>
  <div class="file-preview-container">
    <!-- 顶部导航栏:包含返回和下载 -->
    <div class="preview-header">
      <div class="header-left">
        <el-button icon="el-icon-arrow-left" circle @click="goBack"></el-button>
        <span class="file-title">{{ fileName }}</span>
      </div>
      <div class="header-right">
        <el-button type="primary" icon="el-icon-download" @click="downloadFile">
          下载文件
        </el-button>
      </div>
    </div>

    <!-- 预览内容区域 -->
    <div
      class="preview-content"
      v-loading="loading"
      element-loading-text="正在加载文件..."
      style="position: relative"
    >
      <!-- 1. 图片预览 -->
      <div v-if="fileType === 'image'" class="media-wrapper">
        <img
          :src="fileUrl"
          alt="预览图片"
          style="max-width: 100%; max-height: 100%; object-fit: contain"
        />
      </div>

      <!-- 2. 视频预览 -->
      <div v-else-if="fileType === 'video'" class="media-wrapper">
        <video
          :src="fileUrl"
          controls
          style="width: 100%; max-height: 100%"
          autoplay
        ></video>
      </div>

      <!-- 3. 音频预览 -->
      <div v-else-if="fileType === 'audio'" class="media-wrapper">
        <audio :src="fileUrl" controls style="width: 100%" autoplay></audio>
      </div>

      <!-- 4. PDF 预览 (使用 vue-office) -->
      <div
        v-else-if="fileType === 'pdf'"
        class="office-wrapper office-wrapper-pdf"
      >
        <vue-office-pdf
          :src="fileUrl"
          @rendered="renderedHandler"
          style="height: 100%; width: 100%"
        />
      </div>

      <!-- 5. Word (.docx) 预览 (使用 vue-office) -->
      <div v-else-if="fileType === 'word'" class="office-wrapper">
        <div class="office-wrapper-docx">
          <vue-office-docx
            :src="fileUrl"
            @rendered="renderedHandler"
            style="height: 100%; width: 100%"
          />
        </div>
      </div>

      <!-- 6. Excel (.xlsx/.xls) 预览 (使用 vue-office) -->
      <div
        v-else-if="fileType === 'excel'"
        class="office-wrapper office-wrapper-excel"
      >
        <vue-office-excel
          :src="fileUrl"
          :options="options"
          @rendered="renderedExcelHandler"
        />
      </div>

      <!-- 7. TXT / MD 预览 -->
      <div
        v-else-if="fileType === 'text' || fileType === 'markdown'"
        class="text-wrapper"
      >
        <pre v-if="fileType === 'text'">{{ textContent }}</pre>
        <div
          v-else-if="fileType === 'markdown'"
          class="markdown-body"
          v-html="markdownContent"
        ></div>
      </div>

      <!-- 8. PPT (.pptx) 预览 -->
      <!-- 注意:纯前端预览 PPTX 较复杂,vue-office 目前主要支持 docx/xlsx/pdf。
           方案 A: 如果后端能转 PDF,最好转 PDF 后用 pdf 模式预览。
           方案 B: 使用微软在线预览 (需公网链接)。
           这里暂时用提示或尝试用微软预览 iframe (如果链接是公网) -->
      <div
        v-else-if="fileType === 'ppt' || fileType === 'pptx'"
        class="ppt-wrapper"
      >
        <vue-office-pptx
          v-if="isPublicUrl"
          :src="fileUrl"
          @rendered="renderedHandler"
          style="height: 100%; width: 100%"
        />
        <div v-else class="empty-tip">
          <i class="el-icon-warning-outline"></i>
          <p>PPT 预览需要公网链接或后端转换为 PDF。</p>
          <p>当前为内网链接,请直接下载查看。</p>
        </div>
      </div>

      <!-- 9. 不支持的格式 -->
      <div v-else class="empty-tip">
        <i class="el-icon-document-remove"></i>
        <p>暂不支持预览该文件格式</p>
        <el-button type="primary" @click="downloadFile">立即下载</el-button>
      </div>
    </div>
  </div>
</template>

<script lang="">
import VueOfficePdf from "@vue-office/pdf";
import VueOfficeDocx from "@vue-office/docx";
import VueOfficeExcel from "@vue-office/excel";
import VueOfficePptx from "@vue-office/pptx";
import { marked } from "marked";

import "@vue-office/excel/lib/index.css";
import "@vue-office/docx/lib/index.css";
// import "@vue-office/pptx/lib/index.css";
import axios from "axios";
export default {
  name: "FilePreview",
  components: {
    VueOfficePdf,
    VueOfficeDocx,
    VueOfficeExcel,
    VueOfficePptx,
  },
  data() {
    return {
      fileUrl: "",
      fileName: "",
      fileType: "", // image, video, audio, pdf, word, excel, ppt, text, markdown
      textContent: "",
      loading: true,
      isPublicUrl: false, // 简单判断是否为 http/https 开头
      options: {
        xls: false, //预览xlsx文件设为false;预览xls文件设为true
        minColLength: 1000, // excel最少渲染多少列,如果想实现xlsx文件内容有几列,就渲染几列,可以将此值设置为0.
        minRowLength: 1000, // excel最少渲染多少行,如果想实现根据xlsx实际函数渲染,可以将此值设置为0.
        widthOffset: 10, //如果渲染出来的结果感觉单元格宽度不够,可以在默认渲染的列表宽度上再加 Npx宽
        heightOffset: 10, //在默认渲染的列表高度上再加 Npx高
        beforeTransformData: (workbookData) => {
          return workbookData;
        }, //底层通过exceljs获取excel文件内容,通过该钩子函数,可以对获取的excel文件内容进行修改,比如某个单元格的数据显示不正确,可以在此自行修改每个单元格的value值。
        transformData: (workbookData) => {
          return workbookData;
        }, //将获取到的excel数据进行处理之后且渲染到页面之前,可通过transformData对即将渲染的数据及样式进行修改,此时每个单元格的text值就是即将渲染到页面上的内容
      },
    };
  },
  computed: {
    markdownContent() {
      // 1. 获取 .md 文件所在的目录路径
      let baseUrl = "";
      if (this.fileUrl) {
        const urlObj = new URL(this.fileUrl, window.location.origin);
        // 移除文件名,保留目录路径,并确保以 / 结尾
        baseUrl = urlObj.pathname.substring(
          0,
          urlObj.pathname.lastIndexOf("/") + 1
        );

        // 如果是相对路径启动的 fetch,这里可能需要更复杂的逻辑来确定 base
        // 简单处理:如果 fileUrl 包含 http,直接截取
        if (this.fileUrl.startsWith("http")) {
          baseUrl = this.fileUrl.substring(
            0,
            this.fileUrl.lastIndexOf("/") + 1
          );
        }
      }

      // 2. 配置 marked 选项
      marked.setOptions({
        baseUrl: baseUrl, // 关键:设置基础路径,marked 会自动拼接相对路径
        gfm: true, // 启用 GitHub 风格 Markdown
        breaks: true, // 启用换行
      });

      return marked(this.textContent);
    },
  },
  created() {
    // 从路由获取参数
    this.fileUrl = this.$route.query.url;
    this.fileName = this.$route.query.name || "未知文件";

    if (!this.fileUrl) {
      this.$message.error("未找到文件地址");
      this.loading = false;
      return;
    }

    this.isPublicUrl =
      this.fileUrl.startsWith("http://") || this.fileUrl.startsWith("https://");
    this.analyzeFileType();
  },
  methods: {
    analyzeFileType() {
      const lowerName = this.fileName.toLowerCase();
      const urlLower = this.fileUrl.toLowerCase();

      // 优先从文件名判断,其次从 URL
      const getNameExt = () => {
        if (lowerName.includes("."))
          return lowerName.substring(lowerName.lastIndexOf(".") + 1);
        if (urlLower.includes("."))
          return urlLower.substring(urlLower.lastIndexOf(".") + 1);
        return "";
      };

      const ext = getNameExt();

      if (["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"].includes(ext)) {
        this.fileType = "image";
        this.loading = false;
      } else if (["mp4", "webm", "ogg", "mov"].includes(ext)) {
        this.fileType = "video";
        this.loading = false;
      } else if (["mp3", "wav", "ogg", "aac"].includes(ext)) {
        this.fileType = "audio";
        this.loading = false;
      } else if (ext === "pdf") {
        this.fileType = "pdf";
      } else if (ext === "docx") {
        this.fileType = "word";
      } else if (["xlsx", "xls"].includes(ext)) {
        this.fileType = "excel";
      } else if (ext === "pptx") {
        this.fileType = "ppt";
        this.loading = false; // PPT 主要是 iframe 加载,不需要 vue-office 的 rendered 事件
      } else if (ext === "txt") {
        this.fileType = "text";
        this.fetchTextContent();
      } else if (ext === "md") {
        this.fileType = "markdown";
        this.fetchTextContent();
      } else {
        this.fileType = "unknown";
        this.loading = false;
      }
    },

    // 获取文本内容 (TXT / MD)
    fetchTextContent() {
      this.loading = true;
      // 注意:如果文件跨域,可能需要后端代理或配置 CORS
      fetch(this.fileUrl)
        .then((res) => res.text())
        .then((text) => {
          this.textContent = text;
          this.loading = false;
        })
        .catch((err) => {
          console.error(err);
          this.$message.error("文件内容加载失败,可能是跨域问题");
          this.loading = false;
        });
    },
    renderedExcelHandler() {
      console.log("Excel 渲染完成");
      this.loading = false;
      // 方案 A: 延迟触发 resize,等待 DOM 完全稳定
      setTimeout(() => {
        window.dispatchEvent(new Event("resize"));
      }, 100);
    },
    renderedHandler() {
      console.log("Excel 渲染完成");
      this.loading = false;

      // 方案 A: 延迟触发 resize,等待 DOM 完全稳定
      setTimeout(() => {
        window.dispatchEvent(new Event("resize"));
      }, 100);
    },

    goBack() {
      window.close();
      setTimeout(() => {
        if (!window.closed) {
          window.history.back();
        }
      }, 100);
    },

    async downloadFile() {
      this.$message.success(`开始下载:${this.fileName}`);
      // 1. 获取 URL
      const url = this.fileUrl;
      let customFileName = this.fileName || "未命名";
      if (!customFileName) {
        //  fallback: 如果没名字,再从 URL 截取
        customFileName = url.substring(url.lastIndexOf("/") + 1);
      }
      if (customFileName && !customFileName.includes(".")) {
        // 你可以根据实际业务判断后缀,比如从 url 里取后缀,或者硬编码
        const suffix = url.substring(url.lastIndexOf("."));
        customFileName = customFileName + (suffix || ".txt");
      }
      try {
        // 2. 发起请求
        const response = await axios({
          method: "get",
          url: url,
          responseType: "blob",
          // headers: { Authorization: "Bearer " + getToken() }, // 如果需要鉴权请解开
        });

        // 3. 创建 Blob 对象
        const blob = new Blob([response.data], { type: "text/plain" });

        // 4. 创建临时下载链接
        const link = document.createElement("a");
        link.href = window.URL.createObjectURL(blob);

        // 使用自定义的名字
        link.download = customFileName;

        // 5. 触发点击
        document.body.appendChild(link);
        link.click();

        // 6. 清理资源
        document.body.removeChild(link);
        window.URL.revokeObjectURL(link.href);
      } catch (error) {
        console.error("下载失败:", error);
        this.$message.error("下载失败,请检查网络或跨域设置");
      }
    },
  },
};
</script>

<style lang="less" scoped>
.file-preview-container {
  height: calc(100vh - 10px);
  overflow: hidden;
  background-color: #f5f7fa;
}

.preview-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 20px;
  height: 60px;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  z-index: 10;
  width: 100%;

  .header-left {
    display: flex;
    align-items: center;
    .file-title {
      margin-left: 15px;
      font-size: 16px;
      font-weight: bold;
      color: #303133;
      max-width: 600px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }
}

.preview-content {
  padding: 20px;
  margin-top: 10px;
  padding-bottom: 16px;
  background: #fff;
  margin-left: 20px;
  margin-right: 20px;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  position: relative;

  .media-wrapper,
  .office-wrapper {
    width: 100%;
    height: 100%;
    display: block;
    overflow: hidden;
  }
  .office-wrapper-docx {
    height: calc(100vh - 100px) !important;
    overflow: auto !important;
  }

  .text-wrapper {
    width: 100%;
    height: 100%;
    height: calc(100vh - 100px) !important;
    overflow: auto;
    text-align: left;
    padding: 20px;
    box-sizing: border-box;
    background: #fafafa;

    pre {
      font-family: Consolas, Monaco, "Andale Mono", monospace;
      white-space: pre-wrap;
      word-wrap: break-word;
      font-size: 14px;
      line-height: 1.6;
    }

    .markdown-body {
      font-size: 16px;
      line-height: 1.6;
      color: #2c3e50;
    }
  }

  .ppt-wrapper {
    width: 100%;
    height: calc(100vh - 100px);
    iframe {
      border: none;
    }
  }

  .empty-tip {
    text-align: center;
    color: #909399;

    i {
      font-size: 48px;
      margin-bottom: 10px;
      display: block;
    }
    p {
      margin: 10px 0;
    }
  }
}

.office-wrapper-excel {
  height: calc(100vh - 120px) !important;
  width: 100% !important;
  overflow: auto !important;
}

.office-wrapper-pdf {
  height: calc(100vh - 120px) !important;
  width: 100% !important;
  overflow: auto !important;
}
.media-wrapper {
  width: 100% !important;
  height: calc(100vh - 120px) !important; /* 确保继承父容器高度 */
  display: flex !important; /* 开启 Flex */
  justify-content: center !important; /* 水平居中 */
  align-items: center !important; /* 垂直居中 */
  overflow: hidden !important; /* 防止内容溢出 */
  background-color: #f5f7fa !important;
}

.media-wrapper img,
.media-wrapper video {
  max-width: 100% !important;
  max-height: 100% !important;
  object-fit: contain !important; /* 保持比例,不留黑边或裁剪 */
}

.media-wrapper audio {
  width: 80% !important; /* 音频播放器通常不需要占满全宽,留点边距好看 */
  max-width: 600px !important;
}
</style>

完结~

相关推荐
拾贰_C2 小时前
【Vue | vue3 | spring boot】前端前台项目搭建
前端·vue.js·spring boot
Irene19912 小时前
flush 是 Vue3 中控制副作用函数执行时机的配置选项,用于决定响应式数据变化后,副作用(watch、watchEffect、组件渲染)在何时执行
vue.js
蓝黑20202 小时前
Vue SFC Playground
前端·javascript·vue.js
qq_406176142 小时前
React与Vue异同点及优缺点深度解析
前端·vue.js·react.js
SuperEugene11 小时前
Axios 接口请求规范实战:请求参数 / 响应处理 / 异常兜底,避坑中后台 API 调用混乱|API 与异步请求规范篇
开发语言·前端·javascript·vue.js·前端框架·axios
英俊潇洒美少年13 小时前
vue如何实现react useDeferredvalue和useTransition的效果
前端·vue.js·react.js
英俊潇洒美少年13 小时前
ref 底层到底是怎么变成响应式的?
vue.js
英俊潇洒美少年13 小时前
react19和vue3的优缺点 对比
前端·javascript·vue.js·react.js
多看书少吃饭15 小时前
Vue + Java + Python 打造企业级 AI 知识库与任务分发系统(RAG架构全解析)
java·vue.js·笔记