vue3实现pdf文件预览 - vue-pdf-embed

发布于:2025-07-11 ⋅ 阅读:(44) ⋅ 点赞:(0)

参考地址:https://juejin.cn/post/7105933034771185701
这个参考文章的代码直接可以复制使用,样式也是给到的,但是实现的是一页一页的显示pdf内容,我的需求是要全部展示出来,页码切换时是做一个滚动定位操作。

思路:

  1. 通过vue3-pdfjs插件提供的方法createLoadingTask获取到总页码numPages,通过循环将所有页面渲染
  2. 页码定位通过锚点定位方式实现。

安装插件:

 npm install vue-pdf-embed@1.1.6
 npm install vue3-pdfjs@0.1.6

注意:vue-pdf-embed使用1.1.6版本,之前未固定版本出现内容不显示问题(大概1版本的可以,2版本的不行,我这边使用的是1.1.6版本)。vue3-pdfjs安装就是0.1.6版本的,记录加上版本好,防止后期有更新出现问题不知道原因。

完整代码:

<template>
  <div class="pdf-preview-container">
    <!-- 头部导航栏 -->
    <div class="pdf-header">
      <div class="page-navigation">
        <div class="page-controls">
          <!-- 上一页按钮 -->
          <el-button link :disabled="state.pageNum <= 1" @click="prevPage">
            <el-icon>
              <ArrowUp />
            </el-icon>
          </el-button>

          <!-- 页码输入框 -->
          <el-input v-model.number="state.pageNum" class="page-input" @keyup.enter="goToPage" @blur="goToPage" />

          <!-- 下一页按钮 :disabled="currentPage >= totalPages" -->
          <el-button link @click="nextPage">
            <el-icon>
              <ArrowDown />
            </el-icon>
          </el-button>
        </div>
        <!-- 总页数 -->
        <span class="total-pages">/&nbsp;&nbsp;{{ state.numPages }}</span>
      </div>
    </div>

    <!-- PDF预览区域 -->
    <div class="pdf-content">
      <!-- PDF内容显示区域 -->
      <div class="pdf-display-area">
        <div v-for="item in state.numPages" :key="item" class='pdf-item' :id="`pdf-page-${item}`">
          <vue-pdf-embed :source="state.source" class="vue-pdf-embed" :page="item" />
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from 'vue';
import { Close, ArrowUp, ArrowDown } from '@element-plus/icons-vue';
import VuePdfEmbed from "vue-pdf-embed";
import { createLoadingTask } from "vue3-pdfjs";


const pdfFile = 'https://pic2-cdn.trytalks.com/pic2/202507091052/files_1a1b765ec00125e0FfuRNLZz.pdf?Expires=1754621583&OSSAccessKeyId=LTAI5tQd3SECCekgpXLBnQSo&Signature=nUn9kW375rdiVNHi2wdDLDm0gAM%3D'

const state = reactive({
  source: pdfFile, // 预览pdf文件地址
  pageNum: 1, // 当前页面
  scale: 1, // 缩放比例
  numPages: 0, // 总页数
});

const loadingTask: any = ref(null)

const pdfInit = () => {
  loadingTask.value = createLoadingTask(state.source);
  loadingTask.value.promise.then((pdf: { numPages: number }) => {
    state.numPages = pdf.numPages;
  });
}

// 滚动到指定页面
let isManualNavigation = false;
const scrollToPage = (page: number) => {
  // 设置手动导航标志
  isManualNavigation = true;

  // 先立即更新页码状态
  const validPage = Math.max(1, Math.min(page, state.numPages));
  state.pageNum = validPage;

  // 执行滚动
  const element = document.getElementById(`pdf-page-${validPage}`);
  if (element) {
    element.scrollIntoView({
      behavior: 'smooth',
      block: 'start'
    });
  }

  // 滚动结束后清除标志
  setTimeout(() => {
    isManualNavigation = false;
  }, 1000);
};

// 上一页
const prevPage = (): void => {
  if (state.pageNum > 1) {
    scrollToPage(state.pageNum - 1);
  }
};

// 下一页
const nextPage = (): void => {
  const nextPage = state.pageNum + 1;
  if (nextPage <= state.numPages) {
    scrollToPage(nextPage);
  }
};

// 跳转到指定页
const goToPage = (): void => {
  // 确保输入是有效数字
  let page = parseInt(String(state.pageNum));
  if (isNaN(page) || page < 1) {
    page = 1;
  } else if (page > state.numPages) {
    page = state.numPages;
  }

  // 更新输入框显示正确的页码
  state.pageNum = page;
  scrollToPage(page);
};

// 处理滚动事件,更新当前页码(带防抖)
let scrollTimeout: number | null = null;
const handleScroll = () => {
  // 如果是手动导航触发的滚动,则不更新页码
  if (isManualNavigation) return;

  if (scrollTimeout) {
    clearTimeout(scrollTimeout);
  }

  scrollTimeout = setTimeout(() => {
    const container = document.querySelector('.pdf-content');
    if (!container) return;

    const scrollPosition = container.scrollTop;
    const items = document.querySelectorAll('.pdf-item');
    const containerHeight = container.clientHeight;
    const threshold = containerHeight * 0.3; // 30%视口高度作为阈值

    let currentPage = state.pageNum;

    // 精确检测当前可见页面
    items.forEach((item) => {
      const pageNum = parseInt(item.id.replace('pdf-page-', ''));
      const rect = item.getBoundingClientRect();

      // 如果页面顶部在视口阈值范围内,则认为是当前页
      if (rect.top >= -threshold && rect.top <= threshold) {
        currentPage = pageNum;
      }
    });

    // 只有当页码确实变化时才更新
    if (currentPage !== state.pageNum) {
      state.pageNum = currentPage;
    }
  }, 150) as unknown as number; // 适当增加防抖时间
};



// 组件挂载时
onMounted(() => {
  // 检查浏览器PDF支持
  const isPdfSupported = 'application/pdf' in navigator.mimeTypes;
  console.log('浏览器原生支持PDF:', isPdfSupported);

  // 添加滚动事件监听
  const contentEl = document.querySelector('.pdf-content');
  if (contentEl) {
    contentEl.addEventListener('scroll', handleScroll);
  }

  pdfInit();
});

onBeforeUnmount(() => {
  // 移除滚动事件监听
  const contentEl = document.querySelector('.pdf-content');
  if (contentEl) {
    contentEl.removeEventListener('scroll', handleScroll);
  }
});
</script>

<style lang="scss" scoped>
.pdf-preview-container {
  display: flex;
  flex-direction: column;
  height: 100%;
  background-color: #fff;
  border-radius: 4px;
  overflow: hidden;

  .pdf-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 16px;
    background-color: #fff;
    border-bottom: 1px solid #e0e0e0;

    .back-button {
      cursor: pointer;
      display: inline-block;
      padding: 5px 5px 0px;
      border-radius: 4px;
      transition: background-color 0.2s;

      &:hover {
        background-color: #f0f0f0;
      }
    }

    .pdf-actions {
      display: flex;
      gap: 10px;
    }

    .page-navigation {
      display: flex;
      align-items: center;

      .page-controls {
        display: flex;
        align-items: center;
        padding: 2px 8px;


        .page-input {
          width: 50px;
          margin: 0 5px;

          :deep(.el-input__inner) {
            text-align: center;
            padding: 0 5px;
          }
        }
      }

      .total-pages {
        margin-left: 5px;
        color: #606266;
      }
    }
  }

  .pdf-content {
    flex: 1;
    overflow: auto;

    /* 自定义滚动条样式 */
    &::-webkit-scrollbar {
      width: 6px;
      height: 6px;
    }

    &::-webkit-scrollbar-track {
      background: rgba(0, 0, 0, 0.05);
      border-radius: 3px;
    }

    &::-webkit-scrollbar-thumb {
      background: rgba(0, 0, 0, 0.15);
      border-radius: 3px;
      transition: background 0.2s;

      &:hover {
        background: rgba(0, 0, 0, 0.25);
      }
    }

    .pdf-loading,
    .pdf-error {
      height: 100%;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }

    .pdf-display-area {
      .pdf-item {
        margin: 16px 0px;
      }
    }
  }
}
</style>

网站公告

今日签到

点亮在社区的每一天
去签到