Vue3使用EasyOFD.js实现ofd文件自定义展示
EasyOFD.js——一个在web端展示ofd文件的控件,该控件基于CANVAS绘制。官网提供的事例,不适合用于多页ofd文件的展示,本文基于EAYSOFD实现放大、缩小、页面跳转以及多页滚动等功能
1、安装EAYSOFD依赖和EASYOFD组件
//依赖 npm i jszip x2js jb2 opentype.js //本程序 npm i easyofd
2、vue3中的使用方法
2.1 增加使用EASYOFD的VUE组件
<template> <div id="1111111"> </div> </template>
2.2 在控件加载时初始化组件
onMounted(() => { let yourElement=document.getElementById("1111111"); let ofd=new EasyOFD('myofdID', yourElement); })
2.3 完整代码

<script setup> import EasyOFD from "easyofd"; import { onMounted } from 'vue' onMounted(() => { let yourElement=document.getElementById("1111111"); let ofd=new EasyOFD('myofdID', yourElement); }) </script> <template> <div id="1111111"> </div> </template> <style > .OfdButton{ padding: 10px 20px; background-color: #007bff; color: #fff; border: none; border-radius: 5px; cursor: pointer; margin-right: 10px; } </style>
4、按以上步骤可以轻松实现ofd为文件展示、效果如下:
5、官网给的使用步骤没有给出API,打印ofd.js对象可以看到部分按钮对应的操作方法
6、根据对API可以自定义放大、缩小、以及页面跳转,完整代码如下:(easyofd.js是利用canvas绘制,每次只显示一页,无法实现多页滚动)

<template> <div class="ofd-preview" v-loading="isShow && loading"> <div class="ofd-tool"> <div class="item"> <el-icon class="zoom-btn" @click="zoomIn" title="缩小"> <Minus /> </el-icon> | <el-icon class="zoom-btn" @click="zoomOut" title="放大"> <Plus /> </el-icon> </div> <div class="item"> <el-input v-model="nowPage" style="width: 55px; height: 28px" placeholder="Please input" class="now-page" @keydown.enter="getPage" /> <span class="line">/ {{ totalPage }}</span> </div> </div> <div id="ofdBox"></div> </div> </template> <script setup lang="ts"> import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'; import { docFilePreview, downloadPdf } from '@/api/docLib'; import { Minus, Plus } from '@element-plus/icons-vue'; import EasyOFD from 'easyofd'; const props = withDefaults( defineProps<{ docId: string; isShow: boolean; }>(), { docId: '', isShow: true } ); const loading = ref(true); let ofd: any = null; const nowPage = ref(1); // 当前页; const totalPage = ref(1); // 总页; onMounted(() => { let ofdBox = document.getElementById('ofdBox'); ofd = new EasyOFD('ofdContainer', ofdBox); }); watch( () => props.docId, async () => { loading.value = true; if (props.docId) { let res = await downloadPdf(props.docId); if (ofd) { ofd.loadFromBlob(res, () => { console.log('加载完成'); }); console.log('ofd1', ofd); const timer = setInterval(() => { if (ofd.view.AllPageNo) { // 获取总页码 loading.value = false; nowPage.value = ofd.view.pageNow; totalPage.value = ofd.view.AllPageNo; console.log('ofd2', ofd); const divEle1 = document.querySelector( '#ofdContainer > :nth-child(2)' ); if (divEle1) { // 默认一屏展示 const scale = Math.floor(((divEle1.clientHeight - 20) / ofd.height) * 100) / 100; ofd.scaleCanvas(scale); } clearInterval(timer); } }, 500); } } }, { immediate: true } ); // 缩小 const zoomIn = () => { console.log('缩小', ofd.zoomSize); if (ofd && ofd.zoomSize > 0.1) { ofd.ZoomIn(); } }; // 放大 const zoomOut = () => { console.log('放大', ofd.zoomSize); if (ofd && ofd.zoomSize <= 2) { ofd.ZoomOut(); } }; const getPage = () => { console.log('当前页', nowPage.value); if (nowPage.value < 1) { nowPage.value = 1; } else if (nowPage.value > totalPage.value) { nowPage.value = totalPage.value; } ofd.view.SetPage(nowPage.value); ofd.scaleCanvas(ofd.zoomSize); ofd.Draw(); }; </script> <style lang="scss" scoped> .ofd-preview { height: 100%; width: 100%; display: flex; flex-direction: column; .ofd-tool { height: 36px; display: flex; justify-content: flex-end; align-items: center; background-color: #f6f7fc; padding-right: 10px; color: rgb(12, 12, 13); .item { display: flex; align-items: center; } .zoom-btn { width: 28px; height: 28px; padding: 2px 6px 0; border-radius: 2px; user-select: none; cursor: default; &:hover { background-color: rgb(221, 222, 223); } } .now-page { width: 55px; height: 28px; :deep(.el-input__inner) { text-align: right; } } .line { min-width: 16px; padding: 7px; margin: 2px; border-radius: 2px; color: var(--main-color); font-size: 12px; line-height: 14px; text-align: left; } } #ofdBox { height: calc(100% - 36px); width: 100%; overflow: hidden; } } </style> <style lang="scss"> #ofdContainer { height: 100% !important; overflow-y: hidden; border-radius: 8px; > div:nth-child(1) { border-bottom: 1px solid #ddd; display: none !important; // 隐藏原按钮 } > div:nth-child(2) { background-color: #fff !important; max-width: 100% !important; height: 100% !important; max-height: none !important; box-sizing: border-box; } #ofdContainerselectButton { display: none; } .OfdButton { padding: 6px 8px; background-color: var(--el-color-primary); color: #fff; border: none; border-radius: 5px; cursor: pointer; margin-right: 10px; font-size: 12px; } } </style>
7、多页文档自定义滚动,获取总页面后循环绘制每一页的canvas,并将canvas缓存为图片,然后展示图片,从而实现多页滚动,页码过多时,可以使用IndexedDB缓存文件,完整代码如下:

<template> <div class="ofd-preview" v-loading="isShow && loading"> <div class="ofd-tool"> <div class="item"> <el-icon class="zoom-btn" @click="zoomIn" title="缩小"> <Minus /> </el-icon> | <el-icon class="zoom-btn" @click="zoomOut" title="放大"> <Plus /> </el-icon> </div> <div class="item"> <el-input v-model="nowPage" style="width: 55px; height: 28px" type="number" placeholder="Please input" class="now-page" @keydown.enter="getPage" /> <span class="line">/ {{ totalPage }}</span> </div> </div> <div id="ofdBox" v-if="loading"></div> <div id="ofdImageBox" v-if="!loading"> <img v-for="(url, i) in dataURLs" :key="url" :src="url" loading="lazy" class="ofd-image" :style="`transform: scale(${imageScale});`" :id="`ofdImage_${i}`" /> </div> </div> </template> <script setup lang="ts"> import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'; import { docFilePreview, downloadPdf } from '@/api/docLib'; import { Minus, Plus } from '@element-plus/icons-vue'; import EasyOFD from 'easyofd'; import ofdIndexedDB from '@/utils/ofdIndexedDB'; const props = withDefaults( defineProps<{ docId: string; isShow: boolean; }>(), { docId: '', isShow: true } ); const loading = ref(true); let ofd: any = null; const nowPage = ref(1); // 当前页; const totalPage = ref(1); // 总页; const dataURLs = ref<string[]>([]); const imageScale = ref(1); onMounted(() => {}); watch( () => props.docId, async () => { loading.value = true; if (props.docId) { let data = await ofdIndexedDB.getData('ofdImageCache', props.docId); console.log('ofdImageCache', data); if (data) { totalPage.value = data.totalPage; dataURLs.value = []; for (let i = 1; i <= totalPage.value; i++) { dataURLs.value.push(data[i]); } loading.value = false; nextTick(() => { observeOfdImageBoxAdd(); }); return; } let ofdBox = document.getElementById('ofdBox'); ofd = new EasyOFD('ofdContainer', ofdBox); let res = await downloadPdf(props.docId); if (ofd) { ofd.loadFromBlob(res, () => { console.log('加载完成'); }); console.log('ofd1', ofd); const timer = setInterval(() => { if (ofd.view.AllPageNo) { nowPage.value = ofd.view.pageNow; totalPage.value = ofd.view.AllPageNo; console.log('ofd2', ofd); const divEle1 = document.querySelector( '#ofdContainer > :nth-child(2)' ); dataURLs.value = []; if (divEle1) { // 默认一屏展示 const scale = Math.floor((divEle1.clientHeight / ofd.height) * 100) / 100; if (scale < 0.4) { // canvas比例太大 imageScale.value = 0.5 + scale * 2; ofd.scaleCanvas(scale * 2); } else { imageScale.value = scale; } // 把每一页的数据转换成图片展示 const ofdCacheObj = { docId: props.docId, totalPage: totalPage.value }; for (let i = 1; i <= totalPage.value; i++) { ofd.view.SetPage(i); ofd.scaleCanvas(ofd.zoomSize); ofd.Draw(); const canvas: any = document.querySelector( '#ofdContainer-ofd-canvas' ); if (canvas) { const dataURL = canvas.toDataURL('image/png'); console.log('i', i); dataURLs.value.push(dataURL); ofdCacheObj[i] = dataURL; } if (i === totalPage.value) { loading.value = false; nextTick(() => { observeOfdImageBoxAdd(); }); } } console.log('dataURLs', dataURLs.value); // 缓存数据 // const ofdCacheObj = { // docId: props.docId, // totalPage: totalPage.value, // dataURLs: dataURLs.value // }; ofdIndexedDB.addData('ofdImageCache', props.docId, ofdCacheObj); } clearInterval(timer); } }, 500); } } }, { immediate: true } ); // 缩小 const zoomIn = () => { console.log('缩小', imageScale.value); if (imageScale.value >= 0.1) { imageScale.value = imageScale.value - 0.1; } }; // 放大 const zoomOut = () => { console.log('放大', imageScale.value); if (imageScale.value <= 2) { imageScale.value = imageScale.value + 0.1; } }; const getPage = () => { console.log('当前页', nowPage.value); if (nowPage.value < 1) { nowPage.value = 1; } else if (nowPage.value > totalPage.value) { nowPage.value = totalPage.value; } const ofdImageEle = document.getElementById(`ofdImage_${nowPage.value - 1}`); if (ofdImageEle) { ofdImageEle.scrollIntoView({ behavior: 'smooth', // 平滑滚动 block: 'start' // 滚动到顶部对齐 }); } }; // 监听图片盒子滚动 const observeVisibilityChanges = ( parentElement: HTMLDivElement, callback: Function ) => { const observer = new IntersectionObserver( (entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { callback(entry.target); // 当元素进入视野时调用回调函数 } }); }, { root: parentElement } ); // 开始观察所有子元素 Array.from(parentElement.children).forEach(child => observer.observe(child)); }; const observeOfdImageBoxAdd = () => { const ofdImageBox = document.getElementById('ofdImageBox') as HTMLDivElement; if (ofdImageBox) { observeVisibilityChanges(ofdImageBox, (element: HTMLImageElement) => { console.log(`${element.id} is now in view.`); if (ofdImageBox.scrollTop > 0 && element.id && element.id.includes('_')) { nowPage.value = +element.id.split('_')[1] + 1 || 1; } }); } }; </script> <style lang="scss" scoped> .ofd-preview { height: 100%; width: 100%; display: flex; flex-direction: column; .ofd-tool { height: 36px; display: flex; justify-content: flex-end; align-items: center; background-color: #f6f7fc; padding-right: 10px; color: rgb(12, 12, 13); .item { display: flex; align-items: center; } .zoom-btn { width: 28px; height: 28px; padding: 2px 6px 0; border-radius: 2px; user-select: none; cursor: default; &:hover { background-color: rgb(221, 222, 223); } } .now-page { width: 55px; height: 28px; :deep(.el-input__inner) { text-align: right; } } .line { min-width: 16px; padding: 7px; margin: 2px; border-radius: 2px; color: var(--main-color); font-size: 12px; line-height: 14px; text-align: left; } } #ofdBox { height: calc(100% - 36px); width: 100%; overflow: hidden; } #ofdImageBox { display: flex; flex-direction: column; height: 100%; overflow: auto; > img { align-self: center; } } } </style> <style lang="scss"> #ofdContainer { height: 100% !important; overflow-y: hidden; border-radius: 8px; > div:nth-child(1) { border-bottom: 1px solid #ddd; display: none !important; } > div:nth-child(2) { background-color: #fff !important; max-width: 100% !important; height: 100% !important; max-height: none !important; box-sizing: border-box; } #ofdContainerselectButton { display: none; } .OfdButton { padding: 6px 8px; background-color: var(--el-color-primary); color: #fff; border: none; border-radius: 5px; cursor: pointer; margin-right: 10px; font-size: 12px; } } </style>
8、最终效果展示如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现