4 changed files with 61 additions and 323 deletions
@ -1,326 +1,64 @@ |
|||
<template> |
|||
<div class="pdf-preview-container"> |
|||
<!-- 头部导航栏 --> |
|||
|
|||
<div class="page-navigation"> |
|||
<div class="page-controls"> |
|||
|
|||
<div class="page-tool"> |
|||
<div class="page-tool-item" @click="prevPage">上一页</div> |
|||
<div class="page-tool-item">{{state.pageNum}}/{{state.numPages}}</div> |
|||
<div class="page-tool-item" @click="nextPage">下一页</div> |
|||
</div> |
|||
|
|||
</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" style="width: 98%;"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div id="pdfContainer" ref="pdfContainer" style="width: 100%; height: 100%; overflow: auto;"></div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from 'vue'; |
|||
import VuePdfEmbed from "vue-pdf-embed"; |
|||
import { createLoadingTask } from "vue3-pdfjs"; |
|||
<script > |
|||
import {baseUrl} from "@/config/env"; |
|||
import {useRoute} from "vue-router"; |
|||
|
|||
const pdfFile = 'http://192.168.110.36:85/profile/upload/2024/03/29/40eb1a97ca27c3369370e90632d92db0.pdf' |
|||
|
|||
const route = useRoute(); |
|||
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 pageZoomOut = (): void => { |
|||
loadingTask.value = createLoadingTask(state.source); |
|||
loadingTask.value.promise.then(pages => { |
|||
pages[0].getViewport({ scale: 1.5 }); |
|||
}); |
|||
|
|||
} |
|||
const pageZoomIn = (): void => { |
|||
loadingTask.value = createLoadingTask(state.source); |
|||
loadingTask.value.promise.then(pages => { |
|||
pages[0].getViewport({ scale: 0.5 }); |
|||
}); |
|||
} |
|||
|
|||
|
|||
|
|||
// 跳转到指定页 |
|||
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; |
|||
import * as pdfjsLib from 'pdfjs-dist'; |
|||
const screenWidth = window.innerWidth; |
|||
export default { |
|||
name: 'PdfViewer', |
|||
data() { |
|||
return { |
|||
pageNums: 0, |
|||
// 可引入网络文件或者本地文件 |
|||
pdfUrl: "http://192.168.110.36:85/profile/upload/2024/03/29/40eb1a97ca27c3369370e90632d92db0.pdf", |
|||
}; |
|||
}, |
|||
props: { |
|||
pdfSrc: { |
|||
type: String, |
|||
required: true, |
|||
}, |
|||
}, |
|||
mounted() { |
|||
this.pdfUrl =baseUrl+ this.$route.query.url; |
|||
console.log("pdf地址", this.pdfUrl) |
|||
this.loadPdf(); |
|||
}, |
|||
methods: { |
|||
async loadPdf() { |
|||
// pdfjsLib.GlobalWorkerOptions.workerSrc = '../../api/pdf.worker.js' |
|||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('../../api/pdf.worker.min.mjs', import.meta.url).href; |
|||
// getDocument 接口可以获取 url 指定的 PDF 文件,并返回一个 PDFDocumentLoadingTask 对象 |
|||
const loadingTask = pdfjsLib.getDocument(this.pdfUrl); |
|||
const pdf = await loadingTask.promise; |
|||
this.pageNums = pdf.numPages; |
|||
var pdfContainer = document.getElementById('pdfContainer'); |
|||
// 遍历PDF页面并渲染 |
|||
for (var i = 1; i <= this.pageNums; i++) { |
|||
pdf.getPage(i).then(async function (page) { |
|||
var scale = 1; |
|||
var viewport = page.getViewport({scale: scale}); |
|||
var canvas = document.createElement('canvas'); |
|||
var context = canvas.getContext('2d'); |
|||
canvas.height = viewport.height; |
|||
canvas.width = viewport.width; |
|||
var scale1 = screenWidth / viewport.width; // 根据屏幕宽度调整 scale |
|||
var adjustedViewport = page.getViewport({scale: scale1}); |
|||
|
|||
// 将canvas添加到容器中 |
|||
pdfContainer.appendChild(canvas); |
|||
|
|||
// 渲染PDF页面到canvas上 |
|||
var renderContext = { |
|||
canvasContext: context, |
|||
viewport: adjustedViewport |
|||
}; |
|||
page.render(renderContext); |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
// 只有当页码确实变化时才更新 |
|||
if (currentPage !== state.pageNum) { |
|||
state.pageNum = currentPage; |
|||
} |
|||
}, 150) as unknown as number; // 适当增加防抖时间 |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
|
|||
// 组件挂载时 |
|||
onMounted(() => { |
|||
state.source = baseUrl +route?.query?.url; |
|||
// 检查浏览器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 { |
|||
position: relative; |
|||
width: 100%; |
|||
.pdf-item { |
|||
margin: 16px 0px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
@media (max-width: 768px) { |
|||
.pdf-display-area { |
|||
width: 98%; /* 在小屏幕上稍微减小宽度 */ |
|||
} |
|||
} |
|||
|
|||
.page-tool { |
|||
position: absolute; |
|||
bottom: 35px; |
|||
padding-left: 8px; |
|||
padding-right: 8px; |
|||
display: flex; |
|||
align-items: center; |
|||
background: rgb(66, 66, 66); |
|||
color: white; |
|||
border-radius: 19px; |
|||
z-index: 100; |
|||
cursor: pointer; |
|||
margin-left: 50%; |
|||
transform: translateX(-50%); |
|||
} |
|||
.page-tool-item { |
|||
padding: 8px 15px; |
|||
padding-left: 10px; |
|||
cursor: pointer; |
|||
} |
|||
|
|||
} |
|||
</style> |
|||
</script> |
|||
Loading…
Reference in new issue