vue中的ofd/pdf预览实现过程

 更新时间:2025年10月25日 09:05:29   作者:Qredsun  
这篇文章主要介绍了vue中的ofd/pdf预览实现过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

背景

实现预览ofd/pdf超链接功能

业务实现

1.pdf的预览

实现方式:

1.直接使用 <iframe :src="${url}#navpanes=0&toolbar=0" /> 实现pdf的预览。

  • navpanes=0 隐藏侧边栏
  • toolbar=0 隐藏顶部工具栏

2.使用pdf.js,代码先行:

<template>
  <a-tabs
    v-if="props.urls.length > 0"
    :default-active-key="activateTab"
    type="card"
    class="pdf-tabs"
    @change="tabChangeHandler"
  >
    <a-tab-pane v-for="url in props.urls" :key="url" :tab="fileName(url)">
      <div class="pdf-container">
        <canvas
          v-if="url.endsWith('.pdf')"
          class="canvas"
          :ref="(el) => (canvasRefs[url] = el)"
        ></canvas>
        <a-button class="mb-2" type="link" @click="handleDownload(url)">
          {{ fileName(url) }}
        </a-button>
      </div>
    </a-tab-pane>
  </a-tabs>
</template>

<script lang="ts" setup>
import { ref, watch, nextTick } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import { debounce } from 'lodash-es'
import { saveAs } from 'file-saver'
import EasyOFD from 'easyofd'

interface Props {
  urls?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  urls: () => [],
})
const url = ref<string>('')
const activateTab = ref<string>('')
const canvasRefs = ref<Record<string, HTMLCanvasElement | null>>({})

// 文件类型判断
const ext = ref<string>('pdf')
const isOfd = ref<boolean>(false)
const isPdf = ref<boolean>(false)

//  设置 PDF.js worker 路径(推荐方式)
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs'

//  从 URL 中提取文件名
function fileName(url: string): string {
  try {
    const decodeURL = decodeURIComponent(url).split('/')
    const lastSegment = decodeURL[decodeURL.length - 1]

    const firstIndex = lastSegment.indexOf('-')
    const lastIndex = lastSegment.lastIndexOf('-')

    if (firstIndex === -1 || lastIndex === -1 || lastIndex <= firstIndex) {
      return lastSegment.split('.')[0] // fallback 文件名
    }

    const name = lastSegment.substring(firstIndex + 1, lastIndex)
    const ext = name.split('.').pop()

    if (['pdf', 'ofd'].includes(ext ?? '')) {
      return name.substring(0, name.lastIndexOf('.'))
    }

    return name
  } catch {
    return 'unknown'
  }
}

// 获取文件类型
const getFileType = (url: string) => {
  const decodeURL = decodeURIComponent(url)
  ext.value = decodeURL.endsWith('.pdf') ? 'pdf' : 'ofd'
  isPdf.value = ext.value === 'pdf'
  isOfd.value = ext.value === 'ofd'
  isPdf.value ? loadAndRenderPdf(url) : loadAndRenderOfd(url)
}
//  下载文件
const handleDownload = debounce((url: string) => {
  saveAs(url, `${fileName(url)}.${ext.value}`)
}, 300)

//  加载并渲染 PDF
async function loadAndRenderPdf(pdfUrl: string) {
  try {
    const canvas = canvasRefs.value[pdfUrl]
    if (!canvas) return

    const loadingTask = pdfjsLib.getDocument(pdfUrl)
    const pdf = await loadingTask.promise
    const page = await pdf.getPage(1)

    const viewport = page.getViewport({ scale: 1.3 })

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

    const context = canvas.getContext('2d')
    if (!context) return

    const renderContext = {
      canvasContext: context,
      viewport,
    }

    await page.render(renderContext).promise
  } catch (error) {
    console.error('PDF 渲染失败:', error)
  }
}

//  标签页切换时加载 PDF
async function tabChangeHandler(key: string) {
  url.value = key
  activateTab.value = fileName(key)

  await nextTick() // 等待 DOM 更新
  if (key.endsWith('.pdf')) {
    await loadAndRenderPdf(key)
  }
}

//  页面初始化时自动加载第一个 PDF
watch(
  () => props.urls,
  async (newUrls) => {
    if (newUrls && newUrls.length > 0) {
      console.log('newUrls:', newUrls)
      url.value = newUrls[0]
      activateTab.value = fileName(newUrls[0])
      await nextTick()
      getFileType(newUrls[0])
    }
  },
  { immediate: true },
)
</script>

<style lang="less" scoped>
.canvas {
  border: 1px solid #000;
  width: 100%; // 响应式宽度
  border: 1px solid #000;
}
.pdf-container {
  display: flex;
  flex-direction: column;
  align-items: start;
  gap: 12px;
  max-width: 100%; // 限制最大宽度
  max-height: 400px;
  overflow: auto;
}
</style>

说一下重点:

问题一: 通过命令pnpm install pdf.js安装后,通常出现引用问题;Cannot resolve pdf.worker.entry。代码中使用的版本"pdfjs-dist": "^5.2.133"

import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';

pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;

解决方案:

ps: 上面的代码中包含了文件的下载功能,需要安装 "file-saver": "^2.0.5",

将文件从node_modules/pdfjs-dist/build/pdf.worker.min.mjs移动至项目的public/pdf.worker.min.mjs,可以使用命令 cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/pdf.worker.min.mjs

修改引用:

import * as pdfjsLib from 'pdfjs-dist';

pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs'

2.ofd的预览

实现方式:http://www.easyofd.cn/

安装的依赖:

pnpm -i jszip x2js jb2 opentype.js easyofd

业务实现:

<template>
  <div ref="containerRef" style="width: 100%; height: 800px;"></div>
</template>

<script setup>
import EasyOFD from "easyofd"
import { ref, onMounted } from 'vue'

const containerRef = ref(null)

onMounted(async () => {
  if (!containerRef.value) {
    console.error('OFD 容器不存在')
    return
  }

  const ofd = new EasyOFD('myOFD', containerRef.value)

  try {
    const response = await fetch('/files/sample.ofd')
    const blob = await response.blob()
    ofd.loadFromBlob(blob)
  } catch (e) {
    console.error('OFD 加载失败:', e)
  }
})
</script>
	
<style lang="less" scoped>
// 隐藏右侧的ppi模块,减少空白
:deep(#myOFD-ppi) {
  display: none;
}
// 增加边框
:deep(#myOFD-ofd-canvas) {
  border: 1px solid #000;
}
// 隐藏顶部按钮
:deep(.OfdButton) {
  display: none !important;
}
</style>

官网效果:(easyOfd官网手册)

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • vue 项目地址去掉 #的方法

    vue 项目地址去掉 #的方法

    vue 项目往往会搭配 vue-router 官方路由管理器,它和 vue.js 的核心深度集成,让构建单页面应用变得易如反掌。这篇文章主要介绍了vue 项目地址去掉 #的方法,需要的朋友可以参考下
    2018-10-10
  • Vue使用element plus组件的时间格式问题解决

    Vue使用element plus组件的时间格式问题解决

    这篇文章主要为大家详细介绍了Vue使用element plus组件时出现时间格式问题的解决方法,文中的示例代码讲解详细,需要的小伙伴可以参考下
    2025-07-07
  • Vue前端导出页面为PDF文件的最佳方案

    Vue前端导出页面为PDF文件的最佳方案

    这篇文章主要介绍了前端导出PDF方案,通过html2canvas和jsPDF实现单页导出,利用iframe分批处理列表页数据并打包ZIP,兼顾性能与样式还原,有效减轻服务端压力,需要的朋友可以参考下
    2025-07-07
  • vue3实现多条件搜索功能的示例代码

    vue3实现多条件搜索功能的示例代码

    搜索功能在后台管理页面中非常常见,这篇文章就着重讲一下vue3-admin-element框架中如何实现一个顶部多条件搜索功能,感兴趣的小伙伴可以了解一下
    2023-08-08
  • vue单向数据绑定和双向数据绑定方式

    vue单向数据绑定和双向数据绑定方式

    这篇文章主要介绍了vue单向数据绑定和双向数据绑定方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • Vue深入讲解数据响应式原理

    Vue深入讲解数据响应式原理

    应用会对用户的操作进行反馈,就叫响应式,数据变化会实时改变UI,就叫数据响应式,修改Vue实例中的数据时,视图会重新渲染,就是Vue的数据响应式
    2022-05-05
  • vue3使用vue-router的完整步骤记录

    vue3使用vue-router的完整步骤记录

    Vue Router是Vue.js (opens new window)官方的路由管理器,它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌,这篇文章主要给大家介绍了关于vue3使用vue-router的相关资料,需要的朋友可以参考下
    2021-06-06
  • vue插件开发之使用pdf.js实现手机端在线预览pdf文档的方法

    vue插件开发之使用pdf.js实现手机端在线预览pdf文档的方法

    这篇文章主要介绍了vue插件开发之使用pdf.js实现手机端在线预览pdf文档的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-07-07
  • vue项目中vue.config.js文件详解

    vue项目中vue.config.js文件详解

    vue.config.js 是一个可选的配置文件,如果项目的 (和 package.json 同级的) 根目录中存在这个文件,那么它会被 @vue/cli-service 自动加载,这篇文章主要介绍了vue项目中vue.config.js文件的介绍,需要的朋友可以参考下
    2024-02-02
  • 一文掌握Pinia使用及数据持久化存储超详细教程

    一文掌握Pinia使用及数据持久化存储超详细教程

    这篇文章主要介绍了Pinia安装使用及数据持久化存储的超详细教程,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-07-07

最新评论