当前位置: 首页 > article >正文

vue2 多页面pdf预览

        使用pdfjs-dist预览pdf,实现预加载,滚动条翻页。pdfjs的版本很重要,换了好多版本,终于有一个能用的

node 20.18.1
"pdfjs-dist": "^2.2.228",

        vue页面代码如下

<template>
  <div v-loading="loading">
    <div class="fixed-toolbar">
      <div class="flex flex-direction">
        <div class="mb4">
          <el-button @click="onClose()" size="mini"
            type="warning">返&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;回</el-button>
        </div>
        <div class="mb4">
          <el-button @click="switchViewMode()" size="mini" type="success">切换模式</el-button>
        </div>
        <div class="mb4">
          <el-button @click="scalBig()" size="mini"
            type="primary">放&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;大</el-button>
        </div>
        <!-- <div>
          <el-button class="mb4" @click="renderPdf(1)" size="mini" type="primary">默认</el-button>
        </div> -->
        <div>
          <el-button @click="scalSmall()" size="mini"
            type="primary">缩&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;小</el-button>
        </div>
        <el-dropdown size="mini" trigger="click" class="more-dropdown" style="line-height: 35px;">
          <el-button size="mini" type="primary">缩&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;放</el-button>
          <el-dropdown-menu slot="dropdown">
            <el-dropdown-item><span class="el-button--text-" @click="scale = 1">1X</span></el-dropdown-item>
            <el-dropdown-item><span class="el-button--text-" @click="scale = 2">2X</span></el-dropdown-item>
            <el-dropdown-item><span class="el-button--text-" @click="scale = 3">3X</span></el-dropdown-item>
          </el-dropdown-menu>
        </el-dropdown>
      </div>
    </div>
    <!-- PDF容器 -->
    <div ref="pdfContainer" class="pdf-container" @scroll="handleScroll">
      <!-- 占位符,用于撑开滚动区域 -->
      <div :style="{ height: totalHeight + 'px' }"></div>

      <!-- 渲染可见页面 -->
      <div v-for="page in visiblePages" :key="page.pageNumber" :ref="`page-${page.pageNumber}`" class="page-container"
        :style="{ top: getPageTop(page.pageNumber) + 'px' }">
        <canvas :ref="`canvas-${page.pageNumber}`"></canvas>
        <div v-if="page.loading" class="loading-indicator">加载中...</div>
      </div>
    </div>
  </div>
</template>

<script>
import pdfjsLib from 'pdfjs-dist/build/pdf';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
import { debounce } from 'lodash';

pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;

export default {
  data() {
    return {
      pdfDoc: null,
      loading: true,
      totalPages: 0,
      pageHeights: [],
      totalHeight: 0,
      visiblePages: [],
      scale: 1,
      baseScale: 1, // 移动端基准缩放比例
      scrollTop: 0,
      viewport: null,
      renderTasks: {},
      loadedPages: {}, // 新增:记录已加载的页面
      desiredTotal: 5, // 每次加载的页面数量
      canvasPool: []
    };
  },
  mounted() {
    const pageSize = this.$route.query.pageSize
    this.desiredTotal = pageSize?pageSize:this.desiredTotal
    console.log('预加载页面数量: -->', this.desiredTotal);
    this.loadPdf(this.pdfUrl);
    this.debouncedHandleScroll = debounce(this.updateVisiblePages, 100);
    this.debouncedHandleResize = debounce(() => {
      this.calculatePageHeights();
      this.updateVisiblePages();
    }, 100);
    window.addEventListener('resize', this.handleResize);
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.handleResize);
    this.debouncedHandleScroll.cancel();
    this.debouncedHandleResize.cancel();
    Object.values(this.renderTasks).forEach((task) => task.cancel());
  },
  computed: {
    pdfUrl() {
      return this.$route.query.pdfUrl;
    },
  },
  watch: {
    scale(newVal) {
      this.loadedPages = {}
      this.updateVisiblePages()
    }
  },
  methods: {
    switchViewMode() {
      this.$router.replace({
        name: 'pagePreviewPdf',
        query: {
          pdfUrl: this.pdfUrl
        }
      })
    },
    onClose() {
      this.$router.go(-1)
    },
    // 放大
    scalBig() {
      this.scale = this.scale + 0.2
    },
    // 缩小
    scalSmall() {
      if (this.scale > 1.2) {
        this.scale = this.scale - 0.2
      }
    },
    async loadPdf(url) {
      try {
        const loadingTask = pdfjsLib.getDocument(url);
        this.pdfDoc = await loadingTask.promise;
        this.totalPages = this.pdfDoc.numPages;
        await this.calculatePageHeights();
        this.updateVisiblePages();
        this.loading = false;
      } catch (error) {
        console.error('加载PDF失败:', error);
        this.loading = false;
        this.$message.error('加载PDF失败:' + error);
      }
    },
    handleScroll() {
      this.debouncedHandleScroll();
    },
    handleResize() {
      this.debouncedHandleResize();
    },

    async calculatePageHeights() {
      this.pageHeights = [];
      let page = await this.pdfDoc.getPage(1);
      let viewport = page.getViewport({ scale: 1 });
      this.baseScale = this.isMobile ? window.innerWidth / viewport.width : 1
      for (let i = 1; i <= this.totalPages; i++) {
        const page = await this.pdfDoc.getPage(i);
        const viewport = page.getViewport({ scale: this.scale*this.baseScale });
        this.pageHeights.push(viewport.height);
        console.log(i, ' 页面高度:', viewport.height, 'px,页面宽度:', viewport.width, 'px')
      }

      this.totalHeight = this.pageHeights.reduce((sum, height) => sum + height, 0);
    },

    getPageTop(pageNumber) {
      const top = this.pageHeights.slice(0, pageNumber - 1).reduce((sum, height) => sum + height, 0)
      // console.log('当前显示页面top:', top)
      return top;
    },

    updateVisiblePages() {
      const container = this.$refs.pdfContainer;
      const scrollTop = container.scrollTop;
      const containerHeight = container.clientHeight;

      // 计算初始可见范围
      let startPage = 1;
      let endPage = this.totalPages;
      let currentHeight = 0;

      // 计算起始页
      for (let i = 0; i < this.pageHeights.length; i++) {
        currentHeight += this.pageHeights[i];
        if (currentHeight > scrollTop) {
          startPage = i + 1;
          break;
        }
      }

      // 计算结束页
      currentHeight = 0;
      for (let i = 0; i < this.pageHeights.length; i++) {
        currentHeight += this.pageHeights[i];
        if (currentHeight > scrollTop + containerHeight) {
          endPage = i + 1;
          break;
        }
      }

      // 扩展预加载范围,总页数不超过10
      const desiredTotal = this.desiredTotal;
      const visibleCount = endPage - startPage + 1;
      let remaining = desiredTotal - visibleCount;

      if (remaining > 0) {
        let preloadBefore = Math.floor(remaining / 2);
        let preloadAfter = remaining - preloadBefore;

        let newStart = Math.max(1, startPage - preloadBefore);
        let newEnd = Math.min(this.totalPages, endPage + preloadAfter);

        // 边界调整
        const addedBefore = startPage - newStart;
        const addedAfter = newEnd - endPage;

        if (addedBefore < preloadBefore) {
          newEnd = Math.min(this.totalPages, newEnd + (preloadBefore - addedBefore));
        } else if (addedAfter < preloadAfter) {
          newStart = Math.max(1, newStart - (preloadAfter - addedAfter));
        }

        startPage = newStart;
        endPage = newEnd;

        // 确保不超过总页数限制
        if (endPage - startPage + 1 > desiredTotal) {
          endPage = startPage + desiredTotal - 1;
          if (endPage > this.totalPages) endPage = this.totalPages;
        }
      } else {
        // 可见页数超过10时调整
        endPage = startPage + desiredTotal - 1;
        if (endPage > this.totalPages) {
          endPage = this.totalPages;
          startPage = Math.max(1, endPage - desiredTotal + 1);
        }
      }

      // 生成可见页面数组
      this.visiblePages = [];
      console.log('渲染显示范围:', startPage, ' --- ', endPage)
      const pages = []
      for (let j = startPage; j <= endPage; j++) {
        pages.push(j + '')
      }
      console.log('>>>>loadedPages:', JSON.stringify(this.loadedPages))
      // 移除不在显示区的页面
      Object.keys(this.loadedPages).filter(k => !pages.includes(k)).forEach(pageNumber => {
        this.$delete(this.loadedPages, pageNumber);
      })
      console.log('>>>>cleaned loadedPages:', JSON.stringify(this.loadedPages))

      for (let i = startPage; i <= endPage; i++) {
        const isLoaded = !!this.loadedPages[i];
        this.visiblePages.push({
          pageNumber: i,
          loading: !isLoaded,
        });
        if (!isLoaded) {
          this.renderPage(i);
          console.log('渲染', i)
        } else {
          console.log('DONE:', i)
        }
      }
    },

    async renderPage(pageNumber) {
      console.log('>>>>loadedPages:', JSON.stringify(this.loadedPages))
      if (this.loadedPages[pageNumber]) {
        const index = this.visiblePages.findIndex((p) => p.pageNumber === pageNumber);
        if (index !== -1) this.$set(this.visiblePages[index], 'loading', false);
        return;
      }

      if (this.renderTasks[pageNumber]) this.renderTasks[pageNumber].cancel();

      try {
        const page = await this.pdfDoc.getPage(pageNumber);
        const canvas = this.$refs[`canvas-${pageNumber}`][0];
        const context = canvas.getContext('2d');
        const viewport = page.getViewport({ scale: this.scale*this.baseScale });

        canvas.height = viewport.height;
        canvas.width = viewport.width;

        const renderTask = page.render({ canvasContext: context, viewport });
        this.renderTasks[pageNumber] = renderTask;

        await renderTask.promise;
        this.$set(this.loadedPages, pageNumber, true); // 标记为已加载

        const index = this.visiblePages.findIndex((p) => p.pageNumber === pageNumber);
        if (index !== -1) this.$set(this.visiblePages[index], 'loading', false);
      } catch (error) {
        if (error.name !== 'RenderingCancelledException') console.error('渲染失败:', error);
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.pdf-container {
  height: 100vh;
  overflow-y: auto;
  width: 100%;
  position: relative;
}

.page-container {
  position: absolute;
  width: 100%;
  background: #f5f5f5;
  margin-bottom: 20px;
  display: flex;
  justify-content: center;
}

.loading-indicator {
  text-align: center;
  padding: 20px;
}


.fixed-toolbar {
  position: fixed;
  bottom: 50%;
  right: 0;
  background-color: white;
  /* 可选:设置背景颜色 */
  opacity: 0.7;
  z-index: 1000;
  /* 确保工具栏在其他内容之上 */
  padding: 10px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  /* 可选:添加阴影效果 */
  margin-bottom: 10px;
  flex-wrap: wrap;
}

.mb4 {
  margin-bottom: 4px;
}

@media screen and (max-width: 768px) {}
</style>


http://www.kler.cn/a/543099.html

相关文章:

  • 办公用品管理系统需求说明
  • Spring:Spring实现AOP的通俗理解(有源码跟踪)
  • uniapp中对于文件和文件夹的处理,内存的查询
  • C++智能指针的使用
  • 网络工程师 (32)TRUNK
  • 计算机毕业设计——Springboot餐厅点餐系统
  • 2025年02月12日Github流行趋势
  • maven项目如何部署构建输出(如 JAR、WAR 文件等)到远程仓库中
  • 基于 Python(Flask)、JavaScript、HTML 和 CSS 实现前后端交互的详细开发过程
  • 集成学习(一):从理论到实战(附代码)
  • vue 项目使用vue-watermark组件给页面添加满屏水印
  • 计算机组成原理——中央处理器(九)
  • tp whereOr用法2
  • 链表的‘跑酷’:C++ list 如何在数据中自由穿梭?
  • IGBT工作原理
  • Barra多因子模型
  • 回归新系列——网络安全实操干货系列——Kali Linux新版本——Kali Purple实操指南——信息收集篇1——Nmap(其一)
  • AI赋能前端开发:加速你的职业晋升之路
  • 大前端之前端开发接口测试工具postman的使用方法-简单get接口请求测试的使用方法-简单教学一看就会-以实际例子来说明-优雅草卓伊凡
  • 玩转状态模式
  • Linux下的进程切换与调度
  • Spark商品销售数据可视化分析系统 机器学习预测算法 讲解视频 论文 大数据毕业设计 Hadoop和Hive 销量预测✅
  • 【github】docker realtime
  • 探索RDMA技术:从基础到实践
  • 【Qt】定期清理程序
  • AI写代码工具赋能前端工程师,加速职业晋升