1. npm install pdfjs-dist@2.5.207 --save
    pdfView.vue
    
    <template>
      <div class="filePreview">
        <PdfScroll
          :highlights="highlights"
          :currentPdfPage="currentPdfPage"
          :currentPage="currentPage"
          :src="fileSrc"
        />
      </div>
    </template>
    
    <script>
    import PdfScroll from "@/views/components/PdfScroll.vue";
    
    export default {
      name: "filePreview",
      components: { PdfScroll },
      data() {
        return {
          fileSrc: "",
          currentPdfPage: 1,
          currentPage: 1,
          highlights: [],
        };
      },
      created() {
        try {
          // 从路由参数中获取文件路径,并进行URL解码
          const { fileSrc, page } = this.$route.query;
          if (fileSrc) {
            this.fileSrc = decodeURIComponent(fileSrc);
            // const bbox = JSON.parse(this.$route.query.bbox || "[]");
            // this.highlights = [
            //   {
            //     page: parseInt(page),
            //     rects: [{ x1: bbox[0], y1: bbox[1], x2: bbox[2], y2: bbox[3] }],
            //     color: "#3a903d4d", // 绿色高亮(降低透明度)
            //   },
            // ];
            // this.currentPage = parseInt(page);
            this.currentPdfPage = parseInt(page);
          } else {
            // 如果没有提供文件路径,使用默认的PDF文件
            this.fileSrc = "/static/api.pdf";
            console.warn("未提供文件路径参数,使用默认PDF文件");
          }
        } catch (error) {
          console.error("解析文件路径参数失败:", error);
          this.fileSrc = "/static/api.pdf";
        }
      },
    };
    </script>
    
    <style scoped lang="scss"></style>
    
    pdfScroll.vue
    
    <template>
      <div class="knowledge-detail-container">
        <div class="pdf-preview-section w100">
          <!-- PDF滚动容器 -->
          <div
            class="pdf-render-container"
            ref="pdfCanvasContainer"
            @scroll="onScroll"
          >
            <div
              v-for="pageNum in totalPages"
              :key="pageNum"
              class="pdf-page-wrapper"
              :data-page="pageNum"
            >
              <!-- 层包裹,便于 overlay 定位 -->
              <div class="pdf-page-layer">
                <!-- PDF底图 -->
                <canvas
                  :ref="'pdfPageCanvas' + pageNum"
                  class="pdf-canvas"
                ></canvas>
                <!-- 高亮叠加层(透明) -->
                <canvas
                  :ref="'highlightCanvas' + pageNum"
                  class="highlight-canvas"
                ></canvas>
              </div>
            </div>
            <div v-if="!pdfDoc" class="pdf-loading"><p>加载中...</p></div>
          </div>
        </div>
    
        <!-- 页码提示(可按需打开) -->
        <!-- <div class="pdf-page-indicator">
          {{ currentPdfPage }} / {{ totalPages || 0 }}
        </div> -->
      </div>
    </template>
    
    <script>
    import * as pdfjsLib from "pdfjs-dist/build/pdf.js";
    import pdfWorker from "pdfjs-dist/build/pdf.worker.entry";
    pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker;
    
    export default {
      name: "PdfScroll",
      props: {
        src: {
          type: String,
          default: "",
        },
        currentPdfPage: {
          type: Number,
          default: 1,
        },
        currentPage: {
          type: Number,
          default: 1,
        },
        highlights: {
          type: Array,
          default: () => [],
        },
      },
      data() {
        return {
          pdfDoc: null,
          totalPages: 0,
          pdfZoom: 1.0,
          // 存储每页的 viewport(用于从 PDF 坐标映射到 canvas 坐标)
          pageViewports: {}, // { [pageNum]: viewport }
        };
      },
      methods: {
        async initPdf(pdfUrl) {
          try {
            if (this.pdfDoc) {
              this.pdfDoc.destroy();
              this.pageViewports = {};
            }
            this.pdfDoc = await pdfjsLib.getDocument(pdfUrl).promise;
            this.totalPages = this.pdfDoc.numPages;
    
            // 渲染全部页面(可按需改为懒加载)
            for (let pageNum = 1; pageNum <= this.totalPages; pageNum++) {
              await this.renderPdfPage(pageNum);
            }
            // 等待DOM更新后滚动到指定页码
            this.$nextTick(() => {
              this.scrollToPage(this.currentPdfPage);
            });
          } catch (error) {
            console.error("加载PDF失败:", error);
          }
        },
    
        // 滚动到指定页码
        scrollToPage(pageNum) {
          const container = this.$refs.pdfCanvasContainer;
          const pageWrapper = container.querySelector(
            `.pdf-page-wrapper[data-page="${pageNum}"]`
          );
          if (pageWrapper) {
            container.scrollTop = pageWrapper.offsetTop;
          }
        },
    
        async renderPdfPage(pageNum) {
          try {
            const page = await this.pdfDoc.getPage(pageNum);
            const viewport = page.getViewport({ scale: this.pdfZoom });
    
            // 保存 viewport 以便后续高亮绘制使用
            this.pageViewports[pageNum] = viewport;
    
            // PDF 主 canvas
            const pdfCanvas = this.$refs[`pdfPageCanvas${pageNum}`][0];
            const pdfCtx = pdfCanvas.getContext("2d");
            pdfCanvas.height = viewport.height;
            pdfCanvas.width = viewport.width;
    
            // 高亮 overlay canvas(尺寸保持与 PDF canvas 一致)
            const highlightCanvas = this.$refs[`highlightCanvas${pageNum}`][0];
            const highlightCtx = highlightCanvas.getContext("2d");
            highlightCanvas.height = viewport.height;
            highlightCanvas.width = viewport.width;
    
            // 渲染 PDF 页面到底图 canvas
            const renderContext = { canvasContext: pdfCtx, viewport };
            await page.render(renderContext).promise;
    
            // 绘制该页(如果有)的高亮到 overlay(不会影响底图)
            this.renderHighlights(pageNum);
          } catch (error) {
            console.error("渲染PDF页面失败:", error);
          }
        },
    
        // 渲染某页的高亮(只绘制 overlay canvas)
        renderHighlights(pageNum) {
          const viewport = this.pageViewports[pageNum];
          if (!viewport) return; // 如果 viewport 还没准备好则跳过
    
          const highlightCanvas = this.$refs[`highlightCanvas${pageNum}`]?.[0];
          if (!highlightCanvas) return;
          const ctx = highlightCanvas.getContext("2d");
    
          // 清空 overlay(只清除高亮,不触碰底图)
          ctx.clearRect(0, 0, highlightCanvas.width, highlightCanvas.height);
    
          // 取出该页的高亮信息
          const pageHighlights = this.highlights.filter((h) => h.page === pageNum);
          if (pageHighlights.length === 0) return;
    
          ctx.save();
          pageHighlights.forEach((highlight) => {
            if (highlight.rects) {
              highlight.rects.forEach((rect) => {
                const canvasRect = this.convertPdfRectToCanvas(rect, viewport);
                ctx.fillStyle = highlight.color || "#ffeb3b";
                ctx.globalAlpha = 0.4;
                ctx.fillRect(
                  canvasRect.x,
                  canvasRect.y,
                  canvasRect.width,
                  canvasRect.height
                );
                ctx.globalAlpha = 1.0;
              });
            } else if (highlight.text) {
              // 文本搜索高亮逻辑可在此扩展
              console.warn("文本搜索高亮功能尚未实现");
            }
          });
          ctx.restore();
        },
    
        // 将PDF坐标转换为Canvas坐标
        convertPdfRectToCanvas(rect, viewport) {
          // rect 假设为 {x1, y1, x2, y2}
          const scale = viewport.scale;
          return {
            x: rect.x1 * scale,
            y: rect.y1 * scale,
            width: (rect.x2 - rect.x1) * scale,
            height: (rect.y2 - rect.y1) * scale,
          };
        },
    
        // 滚动监听,计算当前页码
        onScroll() {
          const container = this.$refs.pdfCanvasContainer;
          const pageWrappers = container.querySelectorAll(".pdf-page-wrapper");
          let currentPage = 1;
          for (let i = 0; i < pageWrappers.length; i++) {
            const el = pageWrappers[i];
            if (el.offsetTop - container.scrollTop <= container.clientHeight / 2) {
              currentPage = i + 1;
            }
          }
          if (currentPage !== this.currentPdfPage) {
            this.$emit("update:currentPdfPage", currentPage);
            this.$emit("handleUpdateCurrentPdfPage", currentPage);
          }
        },
      },
    
      watch: {
        src: {
          handler(newSrc, oldSrc) {
            if (newSrc && newSrc !== oldSrc) {
              this.initPdf(newSrc);
            } else if (!newSrc) {
              this.pdfDoc = null;
              this.totalPages = 0;
              this.pageViewports = {};
              this.$emit("update:currentPdfPage", 1);
            }
          },
          immediate: true,
        },
    
        // 当高亮信息变化时:清除其他页 overlay,仅重绘当前页 overlay
        highlights: {
          handler() {
            if (!this.pdfDoc) return;
            this.$nextTick(() => {
              const container = this.$refs.pdfCanvasContainer;
              // 清除所有非当前页的高亮 overlay(只影响 overlay canvas,不重绘 pdf)
              for (let pageNum = 1; pageNum <= this.totalPages; pageNum++) {
                const highlightCanvas =
                  this.$refs[`highlightCanvas${pageNum}`]?.[0];
                if (highlightCanvas) {
                  const ctx = highlightCanvas.getContext("2d");
                  if (pageNum !== this.currentPage) {
                    ctx.clearRect(
                      0,
                      0,
                      highlightCanvas.width,
                      highlightCanvas.height
                    );
                  }
                }
              }
    
              // 仅重绘当前页的高亮(如果有)
              this.renderHighlights(this.currentPage);
            });
          },
          deep: true,
        },
      },
    
      mounted() {
        // 初始化已移至 watch(src) 中处理
      },
    };
    </script>
    
    <style scoped lang="scss">
    .pdf-render-container {
      height: 100%;
      overflow-y: auto;
      background: #fafafa;
      position: relative;
    }
    .pdf-page-wrapper {
      margin-bottom: 14px;
      display: flex;
      justify-content: center;
      &:last-child {
        margin-bottom: 0;
      }
    }
    
    /* 包裹层,便于 overlay 绝对定位 */
    .pdf-page-layer {
      position: relative;
      display: inline-block;
    }
    
    /* PDF 底图 canvas */
    .pdf-canvas {
      display: block;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    }
    
    /* 高亮 overlay canvas,和 pdf-canvas 同尺寸,绝对覆盖 */
    .highlight-canvas {
      position: absolute;
      top: 0;
      left: 0;
      pointer-events: none; /* 不阻塞交互 */
      z-index: 2;
    }
    
    /* 其它样式保持不变 */
    .pdf-page-indicator {
      position: fixed;
      right: 20px;
      bottom: 40px;
      background: rgba(0, 0, 0, 0.6);
      color: white;
      padding: 4px 8px;
      border-radius: 4px;
    }
    
    /* 美化PDF加载动画 */
    .pdf-loading {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100%;
      min-height: 400px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      background-color: #f8f9fa;
      z-index: 10;
      transition: opacity 0.3s ease;
    }
    
    .pdf-loading p {
      margin-top: 20px;
      font-size: 16px;
      color: #666;
      font-weight: 500;
      letter-spacing: 0.5px;
    }
    
    /* 旋转加载动画 */
    .pdf-loading::before {
      content: "";
      width: 48px;
      height: 48px;
      border: 3px solid #f3f3f3;
      border-top: 3px solid #4caf50;
      border-radius: 50%;
      animation: spin 1s linear infinite;
      box-shadow: 0 0 15px rgba(76, 175, 80, 0.1);
    }
    
    /* 加载文本的淡入效果 */
    .pdf-loading p {
      opacity: 0;
      animation: fadeIn 0.5s ease 0.3s forwards;
    }
    
    @keyframes spin {
      0% {
        transform: rotate(0deg);
      }
      100% {
        transform: rotate(360deg);
      }
    }
    
    @keyframes fadeIn {
      from {
        opacity: 0;
        transform: translateY(10px);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }
    </style>
    

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐