最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue3+PDF.js實(shí)現(xiàn)高清PDF預(yù)覽組件開發(fā)(移動(dòng)端)

 更新時(shí)間:2026年05月13日 08:30:05   作者:ZHIS  
本文基于Vue3和PDF.js封裝了一套移動(dòng)端PDF預(yù)覽組件,其核心功能包括高清渲染,雙指手勢縮放,按鈕縮放等內(nèi)容,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下

大家好 在移動(dòng)端 H5 業(yè)務(wù)中,PDF 預(yù)覽是高頻需求(培訓(xùn)文檔、合同、試卷、資料預(yù)覽等)。原生瀏覽器 PDF 預(yù)覽體驗(yàn)差、無法自定義、不支持手勢縮放,因此基于 Vue3 + PDF.js 封裝了一套生產(chǎn)可用、高性能、高清渲染的移動(dòng)端 PDF 預(yù)覽組件。

本文完整實(shí)現(xiàn):

  • ? 高清渲染(適配移動(dòng)端高分屏)
  • ? 雙指手勢縮放
  • ? 按鈕縮放 + 重置
  • ? 加載進(jìn)度條展示
  • ? 錯(cuò)誤處理 + 重新加載
  • ? 內(nèi)存安全釋放
  • ? 樣式美觀,直接用于生產(chǎn)

一、核心技術(shù)棧

  • Vue3 + <script setup>
  • PDF.js(Mozilla 官方 PDF 解析庫)
  • Vant(加載、空狀態(tài)、按鈕)
  • Axios(文件流下載 + 進(jìn)度監(jiān)聽)

二、核心設(shè)計(jì)思路(重點(diǎn))

為了保證清晰度 + 流暢縮放 + 低性能消耗,本組件采用分離設(shè)計(jì):

  1. 渲染縮放(固定高清):使用設(shè)備像素比 DPR 做高清渲染,保證文字不模糊,只渲染一次。
  2. 視覺縮放(CSS transform):縮放不重新渲染,僅用 CSS scale 變換,極致流暢。
  3. 雙指手勢計(jì)算:監(jiān)聽兩指距離變化,動(dòng)態(tài)計(jì)算縮放比例。
  4. 狀態(tài)管理:加載中 / 加載失敗 / 渲染完成 三狀態(tài)控制。

三、完整代碼實(shí)現(xiàn)(可直接復(fù)制使用)

1. Template 視圖結(jié)構(gòu)

<template>
  <!-- PDF預(yù)覽外層容器 -->
  <div class="pdf-preview-box">
    <!-- 加載中狀態(tài):顯示加載動(dòng)畫和進(jìn)度 -->
    <div v-if="loading" class="loading-wrapper">
      <van-loading type="spinner" size="32px">加載中... ({{ progress }}%)</van-loading>
    </div>
    <!-- 加載失敗狀態(tài):顯示錯(cuò)誤提示和重試按鈕 -->
    <div v-else-if="error" class="error-wrapper">
      <van-empty :description="errorMessage" />
      <van-button type="primary" size="small" @click="retry">
        重新加載
      </van-button>
    </div>
    <!-- PDF內(nèi)容滾動(dòng)容器:綁定手勢 -->
    <div 
      v-show="!loading && !error" 
      class="pdf-scroll-wrapper" 
      :style="scrollWrapperStyle"
      @touchstart="handleTouchStart"
      @touchmove="handleTouchMove"
      @touchend="handleTouchEnd"
    >
      <!-- PDF渲染容器 -->
      <div 
        class="pdf-container" 
        ref="containerRef"
        :style="containerStyle"
      />
    </div>
    <!-- 縮放控制面板 -->
    <div v-if="!loading && !error" class="zoom-controls">
      <div class="zoom-btn" @click="zoomOut">
        <van-icon name="minus" />
      </div>
      <div class="zoom-info">{{ Math.round(visualScale * 100) }}%</div>
      <div class="zoom-btn" @click="zoomIn">
        <van-icon name="plus" />
      </div>
      <div class="zoom-btn reset-btn" @click="resetZoom">
        重置
      </div>
    </div>
  </div>
</template>

2. Script Setup 核心邏輯

<script setup>
import { ref, shallowRef, defineProps, watch, onMounted, onUnmounted, computed } from 'vue';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
import { commonProps } from './commonProps';
import request from '@/utils/request';

// 配置 Worker
import pdfjsWorker from 'pdfjs-dist/legacy/build/pdf.worker.mjs?url';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;

const props = defineProps(commonProps());

// 狀態(tài)
const loading = ref(true);
const error = ref(false);
const errorMessage = ref('加載失敗');
const progress = ref(0);
const containerRef = ref(null);
const pdfDoc = shallowRef(null);

// 高清渲染倍率
const renderScale = computed(() => {
  const dpr = window.devicePixelRatio || 1;
  return Math.min(Math.max(dpr, 2.5), 4.0);
});

const contentWidth = ref(0);
const contentHeight = ref(0);

// 手勢縮放狀態(tài)
const touchState = ref({
  startDistance: 0,
  isZooming: false,
  baseScale: 1.0,
});

// 視覺縮放系數(shù)(CSS scale)
const visualScale = ref(1.0);

// 滾動(dòng)容器樣式(動(dòng)態(tài)計(jì)算縮放后寬高)
const scrollWrapperStyle = computed(() => ({
  width: contentWidth.value ? `${contentWidth.value * visualScale.value}px` : '100%',
  height: contentHeight.value ? `${contentHeight.value * visualScale.value}px` : 'auto',
  minWidth: '100%',
  minHeight: '100%',
  position: 'relative',
  overflow: 'hidden',
}));

// PDF 容器樣式(縮放變換)
const containerStyle = computed(() => ({
  transform: `scale(${visualScale.value})`,
  transformOrigin: '0 0',
  transition: touchState.value.isZooming ? 'none' : 'transform 0.2s ease',
  position: 'absolute',
  top: 0,
  left: 0,
  width: contentWidth.value ? `${contentWidth.value}px` : '100%',
  height: contentHeight.value ? `${contentHeight.value}px` : 'auto',
}));

// 計(jì)算雙指距離
const getDistance = (touches) => {
  return Math.hypot(
    touches[0].clientX - touches[1].clientX,
    touches[0].clientY - touches[1].clientY
  );
};

// 觸摸開始
const handleTouchStart = (e) => {
  if (e.touches.length === 2) {
    touchState.value.isZooming = true;
    touchState.value.startDistance = getDistance(e.touches);
    touchState.value.baseScale = visualScale.value;
  }
};

// 觸摸移動(dòng)(雙指縮放)
const handleTouchMove = (e) => {
  if (e.touches.length === 2 && touchState.value.isZooming) {
    e.preventDefault();
    const currentDistance = getDistance(e.touches);
    const ratio = currentDistance / touchState.value.startDistance;
    let newScale = touchState.value.baseScale * ratio;
    newScale = Math.min(Math.max(newScale, 0.5), 4.0);
    visualScale.value = newScale;
  }
};

// 觸摸結(jié)束
const handleTouchEnd = () => {
  touchState.value.isZooming = false;
};

// 渲染單頁 PDF
const renderPage = async (num) => {
  if (!pdfDoc.value || !containerRef.value) return;
  try {
    const page = await pdfDoc.value.getPage(num);
    const screenWidth = window.innerWidth;
    const originalViewport = page.getViewport({ scale: 1 });
    const baseScale = screenWidth / originalViewport.width;
    const finalScale = baseScale * renderScale.value;
    const viewport = page.getViewport({ scale: finalScale });

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = viewport.width;
    canvas.height = viewport.height;

    canvas.style.width = `${screenWidth}px`;
    canvas.style.height = 'auto';
    canvas.style.marginBottom = '10px';
    canvas.style.display = 'block';

    await page.render({ canvasContext: ctx, viewport }).promise;
    containerRef.value.appendChild(canvas);
  } catch (err) {
    console.error(`第 ${num} 頁渲染失敗`, err);
  }
};

// 放大
const zoomIn = () => {
  if (visualScale.value >= 4) return;
  visualScale.value = parseFloat((visualScale.value + 0.2).toFixed(1));
};

// 縮小
const zoomOut = () => {
  if (visualScale.value <= 0.5) return;
  visualScale.value = parseFloat((visualScale.value - 0.2).toFixed(1));
};

// 重置縮放
const resetZoom = () => {
  visualScale.value = 1.0;
};

// 加載 PDF 主邏輯
const loadPdf = async () => {
  if (!props.url) return;
  loading.value = true;
  error.value = false;
  visualScale.value = 1.0;
  containerRef.value?.innerHTML = '';

  try {
    let url = props.url;
    if (url.startsWith(window.location.origin)) url = url.replace(window.location.origin, '');

    const res = await request({
      url,
      method: 'get',
      responseType: 'arraybuffer',
      onDownloadProgress: (e) => {
        if (e.total) progress.value = Math.floor((e.loaded / e.total) * 100);
      },
      loading: false,
    });

    const data = res.data;
    if (!data || !data.byteLength) throw new Error('文件內(nèi)容為空');

    const loadingTask = pdfjsLib.getDocument({
      data,
      cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@5.7.284/cmaps/',
      cMapPacked: true,
    });

    pdfDoc.value = await loadingTask.promise;
    for (let i = 1; i <= pdfDoc.value.numPages; i++) await renderPage(i);

    setTimeout(() => {
      contentWidth.value = containerRef.value.scrollWidth || window.innerWidth;
      contentHeight.value = containerRef.value.scrollHeight;
    }, 300);

    loading.value = false;
    props.onLoad?.();
  } catch (err) {
    loading.value = false;
    error.value = true;
    if (err.message === '文件內(nèi)容為空') {
      errorMessage.value = '文件為空,無法預(yù)覽';
    } else if (err.name === 'InvalidPDFException') {
      errorMessage.value = 'PDF 格式損壞或無效';
    } else {
      errorMessage.value = '預(yù)覽失敗,請重試';
    }
  }
};

// 重試
const retry = () => loadPdf();

watch(() => props.url, loadPdf);
onMounted(loadPdf);
onUnmounted(() => pdfDoc.value?.destroy());
</script>

3. Style 樣式(移動(dòng)端友好)

<style scoped>
.pdf-preview-box {
  width: 100%;
  height: calc(100vh - 100px);
  overflow: auto;
  background: #f5f5f5;
  -webkit-overflow-scrolling: touch;
  position: relative;
}
.loading-wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}
.error-wrapper {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  gap: 20px;
}
.pdf-container {
  display: flex;
  flex-direction: column;
  will-change: transform;
}
.zoom-controls {
  position: fixed;
  bottom: 80px;
  right: 20px;
  background: rgba(255,255,255,0.92);
  border-radius: 24px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.12);
  padding: 10px 6px;
  z-index: 100;
}
.zoom-btn {
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  cursor: pointer;
}
.zoom-info {
  font-size: 12px;
  color: #666;
  text-align: center;
  margin: 4px 0;
}
.reset-btn {
  font-size: 12px;
  border-top: 1px solid #eee;
  padding-top: 6px;
}
:deep(canvas) {
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
  max-width: none !important;
}
</style>
<script>
export default { name: 'PdfPreview' }
</script>

四、核心亮點(diǎn)技術(shù)解析

1. 高清渲染機(jī)制

const renderScale = computed(() => {
  const dpr = window.devicePixelRatio || 1;
  return Math.min(Math.max(dpr, 2.5), 4.0);
});
  • 自動(dòng)適配手機(jī)像素比
  • 最低 2.5 倍清晰度,解決移動(dòng)端文字模糊
  • 上限 4 倍,避免內(nèi)存占用過大

2. 高性能縮放(不重渲染)

transform: `scale(${visualScale.value})`
  • 純 CSS 縮放,流暢不卡頓
  • 手勢 / 按鈕雙控制
  • 縮放區(qū)間 0.5 ~ 4.0 倍

3. 雙指手勢實(shí)現(xiàn)

  • 監(jiān)聽 touchstart 記錄兩指初始距離
  • touchmove 計(jì)算比例動(dòng)態(tài)縮放
  • e.preventDefault() 防止瀏覽器默認(rèn)行為沖突

4. 完整異常處理

  • 文件為空
  • PDF 格式損壞
  • 網(wǎng)絡(luò)請求失敗
  • 支持重新加載

五、組件使用方式

<PdfPreview
  url="/api/file/xxx.pdf或二進(jìn)制流"
  @load="handleLoad"
/>

六、總結(jié)

這套 Vue3 移動(dòng)端 PDF 預(yù)覽組件已經(jīng)在生產(chǎn)環(huán)境穩(wěn)定運(yùn)行,具備:

  • 高清渲染不模糊
  • 手勢 / 按鈕縮放流暢
  • 加載進(jìn)度、錯(cuò)誤、重試齊全
  • 內(nèi)存安全,無泄漏
  • 樣式美觀,開箱即用

非常適合:

  • 培訓(xùn)文檔預(yù)覽
  • 合同 / 協(xié)議預(yù)覽
  • 學(xué)習(xí)資料預(yù)覽
  • 考試試卷預(yù)覽

到此這篇關(guān)于Vue3+PDF.js實(shí)現(xiàn)高清PDF預(yù)覽組件開發(fā)(移動(dòng)端)的文章就介紹到這了,更多相關(guān)Vue3 PDF預(yù)覽組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • vue3調(diào)度器scheduler功能和用法詳解

    vue3調(diào)度器scheduler功能和用法詳解

    調(diào)度器是vue3響應(yīng)式系統(tǒng)中一個(gè)非常重要的特性,可調(diào)度性指的是當(dāng)trigger 動(dòng)作觸發(fā)副作用函數(shù)重新執(zhí)行時(shí),有能力決定副作用函數(shù)執(zhí)行的時(shí)機(jī)、次數(shù)以及方式,本文通過代碼示例給大家介紹調(diào)度器是什么,有什么功能,怎么使用,感興趣的同學(xué)可以借鑒閱讀
    2023-06-06
  • 解決ElementUI組件中el-upload上傳圖片不顯示問題

    解決ElementUI組件中el-upload上傳圖片不顯示問題

    這篇文章主要介紹了解決ElementUI組件中el-upload上傳圖片不顯示問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-10-10
  • Vue實(shí)現(xiàn)DOM元素拖放互換位置示例

    Vue實(shí)現(xiàn)DOM元素拖放互換位置示例

    本文主要介紹了Vue實(shí)現(xiàn)DOM元素拖放互換位置示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-01-01
  • vue+springboot圖片上傳和顯示的示例代碼

    vue+springboot圖片上傳和顯示的示例代碼

    這篇文章主要介紹了vue+springboot圖片上傳和顯示的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-02-02
  • uniapp微信小程序使用webview嵌套u(yù)niappH5并實(shí)現(xiàn)通信詳細(xì)步驟

    uniapp微信小程序使用webview嵌套u(yù)niappH5并實(shí)現(xiàn)通信詳細(xì)步驟

    在開發(fā)微信小程序的時(shí)候,我們有時(shí)候會(huì)遇到將H5頁面嵌入到小程序頁面中的情況,這篇文章主要給大家介紹了關(guān)于uniapp微信小程序使用webview嵌套u(yù)niappH5并實(shí)現(xiàn)通信的詳細(xì)步驟,需要的朋友可以參考下
    2024-05-05
  • vue中的按鈕綁定事件小案例

    vue中的按鈕綁定事件小案例

    這篇文章主要介紹了vue中的按鈕綁定事件小案例,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-11-11
  • 教你在vue項(xiàng)目中使用svg圖標(biāo)的方法

    教你在vue項(xiàng)目中使用svg圖標(biāo)的方法

    本文給大家介紹了在vue項(xiàng)目中使用svg圖標(biāo)的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2022-04-04
  • element-plus的el-tree的雙向綁定示例代碼

    element-plus的el-tree的雙向綁定示例代碼

    這篇文章主要介紹了element-plus的el-tree的雙向綁定的示例代碼,代碼簡單易懂,結(jié)合圖文給大家展示,感興趣的朋友跟隨小編一起看看吧
    2024-12-12
  • 巧妙運(yùn)用v-model實(shí)現(xiàn)父子組件傳值的方法示例

    巧妙運(yùn)用v-model實(shí)現(xiàn)父子組件傳值的方法示例

    這篇文章主要介紹了巧妙運(yùn)用v-model實(shí)現(xiàn)父子組件傳值的方法示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2019-04-04
  • vue導(dǎo)出excel和echart圖形分別在不同工作表的實(shí)現(xiàn)方法

    vue導(dǎo)出excel和echart圖形分別在不同工作表的實(shí)現(xiàn)方法

    這篇文章主要給大家介紹了如何使用vue實(shí)現(xiàn)導(dǎo)出excel和echart圖形分別在不同工作表,文中有詳細(xì)的代碼示例供大家參考,對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下
    2023-10-10

最新評論

大竹县| 彩票| 永善县| 二连浩特市| 封丘县| 吉安市| 锦州市| 达拉特旗| 进贤县| 明水县| 长丰县| 财经| 会宁县| 岱山县| 长乐市| 滨海县| 广丰县| 南皮县| 大同县| 朝阳市| 南京市| 乌兰察布市| 黄梅县| 文成县| 泽库县| 宁津县| 淮北市| 托里县| 景宁| 六枝特区| 桐柏县| 石河子市| 荥阳市| 江山市| 万山特区| 安吉县| 岳池县| 温宿县| 孝昌县| 石柱| 抚远县|