4 changed files with 61 additions and 323 deletions
@ -1,326 +1,64 @@ |
|||||
<template> |
<template> |
||||
<div class="pdf-preview-container"> |
<div id="pdfContainer" ref="pdfContainer" style="width: 100%; height: 100%; overflow: auto;"></div> |
||||
<!-- 头部导航栏 --> |
|
||||
|
|
||||
<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> |
|
||||
</template> |
</template> |
||||
|
|
||||
<script setup lang="ts"> |
<script > |
||||
import { ref, onMounted, onBeforeUnmount, watch, computed, reactive } from 'vue'; |
|
||||
import VuePdfEmbed from "vue-pdf-embed"; |
|
||||
import { createLoadingTask } from "vue3-pdfjs"; |
|
||||
import {baseUrl} from "@/config/env"; |
import {baseUrl} from "@/config/env"; |
||||
import {useRoute} from "vue-router"; |
import * as pdfjsLib from 'pdfjs-dist'; |
||||
|
const screenWidth = window.innerWidth; |
||||
const pdfFile = 'http://192.168.110.36:85/profile/upload/2024/03/29/40eb1a97ca27c3369370e90632d92db0.pdf' |
export default { |
||||
|
name: 'PdfViewer', |
||||
const route = useRoute(); |
data() { |
||||
const state = reactive({ |
return { |
||||
source: pdfFile, // 预览pdf文件地址 |
pageNums: 0, |
||||
pageNum: 1, // 当前页面 |
// 可引入网络文件或者本地文件 |
||||
scale: 1, // 缩放比例 |
pdfUrl: "http://192.168.110.36:85/profile/upload/2024/03/29/40eb1a97ca27c3369370e90632d92db0.pdf", |
||||
numPages: 0, // 总页数 |
}; |
||||
}); |
}, |
||||
|
props: { |
||||
const loadingTask: any = ref(null) |
pdfSrc: { |
||||
|
type: String, |
||||
const pdfInit = () => { |
required: true, |
||||
loadingTask.value = createLoadingTask(state.source); |
}, |
||||
loadingTask.value.promise.then((pdf: { numPages: number }) => { |
}, |
||||
state.numPages = pdf.numPages; |
mounted() { |
||||
}); |
this.pdfUrl =baseUrl+ this.$route.query.url; |
||||
} |
console.log("pdf地址", this.pdfUrl) |
||||
|
this.loadPdf(); |
||||
// 滚动到指定页面 |
}, |
||||
let isManualNavigation = false; |
methods: { |
||||
const scrollToPage = (page: number) => { |
async loadPdf() { |
||||
// 设置手动导航标志 |
// pdfjsLib.GlobalWorkerOptions.workerSrc = '../../api/pdf.worker.js' |
||||
isManualNavigation = true; |
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 validPage = Math.max(1, Math.min(page, state.numPages)); |
const pdf = await loadingTask.promise; |
||||
state.pageNum = validPage; |
this.pageNums = pdf.numPages; |
||||
|
var pdfContainer = document.getElementById('pdfContainer'); |
||||
// 执行滚动 |
// 遍历PDF页面并渲染 |
||||
const element = document.getElementById(`pdf-page-${validPage}`); |
for (var i = 1; i <= this.pageNums; i++) { |
||||
if (element) { |
pdf.getPage(i).then(async function (page) { |
||||
element.scrollIntoView({ |
var scale = 1; |
||||
behavior: 'smooth', |
var viewport = page.getViewport({scale: scale}); |
||||
block: 'start' |
var canvas = document.createElement('canvas'); |
||||
}); |
var context = canvas.getContext('2d'); |
||||
} |
canvas.height = viewport.height; |
||||
|
canvas.width = viewport.width; |
||||
// 滚动结束后清除标志 |
var scale1 = screenWidth / viewport.width; // 根据屏幕宽度调整 scale |
||||
setTimeout(() => { |
var adjustedViewport = page.getViewport({scale: scale1}); |
||||
isManualNavigation = false; |
|
||||
}, 1000); |
// 将canvas添加到容器中 |
||||
}; |
pdfContainer.appendChild(canvas); |
||||
|
|
||||
// 上一页 |
// 渲染PDF页面到canvas上 |
||||
const prevPage = (): void => { |
var renderContext = { |
||||
if (state.pageNum > 1) { |
canvasContext: context, |
||||
scrollToPage(state.pageNum - 1); |
viewport: adjustedViewport |
||||
} |
}; |
||||
}; |
page.render(renderContext); |
||||
|
}); |
||||
// 下一页 |
|
||||
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; |
|
||||
} |
} |
||||
}); |
}, |
||||
|
}, |
||||
// 只有当页码确实变化时才更新 |
|
||||
if (currentPage !== state.pageNum) { |
|
||||
state.pageNum = currentPage; |
|
||||
} |
|
||||
}, 150) as unknown as number; // 适当增加防抖时间 |
|
||||
}; |
}; |
||||
|
</script> |
||||
|
|
||||
// 组件挂载时 |
|
||||
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> |
|
||||
Loading…
Reference in new issue