特大pdf文件在线预览技术方案

特大pdf文件在线预览技术方案

1 技术背景

在一些信息化系统中,比如传统建筑工程领域的招投标,标书体积特别大,动则几百兆、几千页的pdf。这给在线预览评标等工作提出了技术挑战。解决这个问题的一般思路有两种,一是下载文件到本地,使用wps,office等办公软件打开来预览,二是直接在浏览器上打开预览。
但是,不论下载还是浏览器直接预览,都需要全量下载整个pdf文件,下载过程漫长,体验较差;如果浏览器直接预览,除了下载文件时间长问题,还需面临一次性加载pdf文件到浏览器导致浏览器内存吃紧而可能出现的卡顿和崩溃问题。

2 解决方案

解决问题的思路是按需请求 + 懒加载 + 内存换页 + 网络请求防抖。

  • 按需请求
    不要一次性下载整个大几百兆的pdf文件,而是根据需求下载预览目标页的“片”,一次只下载小几百k的数据,预览哪里就下载哪里的片。

  • 懒加载
    在前端设计中,只有进入可视区的页内容才被渲染显示。

  • 内存换页
    被滑出可视区的页码内容将从内存中移除,减少内存使用。

  • 防抖
    当用户快速滚动鼠标进行翻页时,对于停留在可视区不超过200毫秒的页面的数据请求,需要取消http的请求。

3 技术选型

  • 前端技术选型
    我们无需手戳原始码,我们遇到的问题都是前人所遇到的,他们已经走完了我们现在面临的崎岖道路。pdf.js库是一个伟大的库,它可以做到我们上面讨论中所想要的效果。

  • 后端技术选型
    后端实现上无需编写任何源代码,只需在网关nginx上添加一些支持http range的配置。

4 实现

  • 前端实现
    示例代码以vue2.5.2和pdfjs-dist2.6.347为例
"dependencies": {
   
   
    "pdfjs-dist": "^2.6.347",
    "vue": "^2.5.2",
    "vue-router": "^3.0.1"
  }

组件代码如下:

  • 目录组件
<template>
  <div class="pdf-outline-wrapper">
    <div v-if="!outline || outline.length === 0" class="empty-tip">無目錄信息</div>

    <div class="outline-tree-root" v-else>
      <div v-for="(item, index) in outline" :key="index" class="outline-node">

        <div class="outline-item-row" @click="handleRowClick(item)">
          <span
            v-if="item.items && item.items.length > 0"
            class="toggle-arrow"
            :class="{ 'is-open': openStates[index] }"
            @click.stop="toggleNode(index)"
          ></span>
          <span v-else class="toggle-placeholder"></span>

          <span class="outline-title" :title="item.title">{
   
   {
   
    item.title }}</span>
        </div>

        <div v-if="item.items && item.items.length > 0" v-show="openStates[index]" class="outline-children">
          <pdf-outline :outline="item.items" @on-click="forwardClick" />
        </div>

      </div>
    </div>
  </div>
</template>

<script>
export default {
   
   
  name: 'PdfOutline',
  props: {
   
   
    // 接收來自父組件的 PDF 原生目錄樹數據
    outline: {
   
   
      type: Array,
      default: () => []
    }
  },
  data () {
   
   
    return {
   
   
      // 記錄當前層級下各個節點的展開/折疊狀態
      openStates: {
   
   }
    }
  },
  methods: {
   
   
    // 切換展開狀態
    toggleNode (index) {
   
   
      this.$set(this.openStates, index, !this.openStates[index])
    },
    // 點擊當前行的跳轉邏輯
    handleRowClick (item) {
   
   
      if (item.dest) {
   
   
        this.$emit('on-click', item.dest)
      }
    },
    // 🚀 關鍵:將底層遞歸組件冒泡上來的點擊事件,繼續向上拋給最外層的主組件
    forwardClick (dest) {
   
   
      this.$emit('on-click', dest)
    }
  }
}
</script>

<style scoped>
.pdf-outline-wrapper {
   
   
  width: 100%;
  box-sizing: border-box;
  padding: 4px;
}

.empty-tip {
   
   
  color: #909399;
  text-align: center;
  margin-top: 30px;
  font-size: 13px;
}

.outline-node {
   
   
  display: flex;
  flex-direction: column;
  width: 100%;
}

/* 🚀 核心改造:整行改為 Flex 佈局,強制靠左對齊,保證視覺起點統一 */
.outline-item-row {
   
   
  display: flex;
  align-items: center;
  justify-content: flex-start; /* 確保所有內容從最左側開始排 */
  padding: 8px 6px;
  cursor: pointer;
  font-size: 13px;
  color: #303133;
  border-radius: 4px;
  transition: all 0.15s ease;
  user-select: none;
}

/* 鼠標懸停效果:背景微灰,文字變藍 */
.outline-item-row:hover {
   
   
  background-color: #f2f6fc;
  color: #409eff;
}

/* 展開/折疊小箭頭:縮小尺寸,改用精緻的幾何箭頭,並固定寬度 */
.toggle-arrow {
   
   
  font-size: 10px;
  color: #a8abb2;
  margin-right: 6px;
  cursor: pointer;
  transition: transform 0.2s ease;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  flex-shrink: 0; /* 防止被擠壓 */
}

/* 展開時旋轉 90 度 */
.toggle-arrow.is-open {
   
   
  transform: rotate(90deg);
  color: #606266;
}

/* 沒有子節點時的佔位符:寬度必須與小箭頭完全一致(14px + 6px 邊距 = 20px) */
.toggle-placeholder {
   
   
  width: 20px;
  flex-shrink: 0;
}

/* 🚀 文字部分優化:強制左對齊、禁止換行、超出長度自動省略號 */
.outline-title {
   
   
  flex: 1;
  text-align: left; /* 強制文字左對齊 */
  white-space: nowrap; /* 絕對不換行 */
  overflow: hidden; /* 超出部分隱藏 */
  text-overflow: ellipsis; /* 超出時顯示 ... */
  padding-right: 4px;
}

/* 🚀 核心改造:去掉原本雜亂的虛線,改用乾淨的空白縮進 */
/* 子級目錄向右精準縮進 16px */
.outline-children {
   
   
  padding-left: 16px;
  margin-left: 6px;
  /* 💡 去掉了 border-left 的 dashed 虛線,因為虛線在多層級時會顯得很碎很亂。
        改用純粹的留白(Whitespace),大廠主流(如 Adobe, Notion, VSCode)的樹狀目錄都是靠純留白來展現高級感的。 */
}
</style>
  • pdf预览主组件
<template>
  <div class="pdf-viewer-container" ref="pdfViewerContainer" v-loading="sLoading">
    <div class="pdf-sidebar">
      <div class="sidebar-tabs">
        <button :class="{ active: activeTab === 'outline' }" @click="activeTab = 'outline'">目錄</button>
        <button :class="{ active: activeTab === 'search' }" @click="activeTab = 'search'">搜索</button>
      </div>

      <div v-show="activeTab === 'outline'" class="sidebar-content outline-tree">
        <pdf-outline :outline="outline" @on-click="scrollToDest" />
      </div>

      <div v-show="activeTab === 'search'" class="sidebar-content search-panel">
        <div class="search-box">
          <div class="search-input-wrapper">
            <input
              v-model="searchQuery"
              type="text"
              placeholder="輸入關鍵字,回車搜索..."
              @keyup.enter="handleSearch"
            />
            <span
              v-if="searchQuery"
              class="clear-icon"
              @click="clearSearch"
              title="清除搜索"
            ></span>
          </div>
          <button @click="handleSearch">搜索</button>
        </div>

        <div class="search-status" v-if="searchStatusText">
          {
   
   {
   
    searchStatusText }}
        </div>

        <div class="search-results">
          <div
            v-for="(res, index) in searchResults"
            :key="index"
            class="result-item"
            @click="highlightAndScrollToPage(res.pageNo)"
          >
            <div class="result-page-badge">大約 第 {
   
   {
   
    res.pageNo }}</div>
            <div class="result-context" v-html="res.highLightText"></div>
          </div>
        </div>
      </div>
    </div>

    <div class="pdf-main-content" ref="scrollContainer" @scroll
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值