pdf 加载

This commit is contained in:
cwchen 2025-11-10 11:12:13 +08:00
parent c88199a40c
commit c36ed499b6
1 changed files with 206 additions and 59 deletions

View File

@ -61,7 +61,7 @@ if (resolvedWorkerSrc) {
pdfjsLib.GlobalWorkerOptions.workerSrc = null
}
const DEFAULT_SCALE = 1.25
const DEFAULT_SCALE = 1
export default {
name: 'DocumentSearch',
@ -91,6 +91,13 @@ export default {
pageTextDivs: [],
searchResults: [],
currentResultIndex: -1,
renderedPages: new Map(),
observer: null,
pageContainers: [],
totalPages: 0,
isDocumentReady: false,
renderQueue: [],
isProcessingQueue: false,
}
},
mounted() {
@ -99,6 +106,9 @@ export default {
this.loadDocument()
}
},
beforeDestroy() {
this.disconnectObserver()
},
watch: {
'$route.query.url'(newUrl, oldUrl) {
if (newUrl !== oldUrl) {
@ -147,40 +157,63 @@ export default {
async renderAllPages() {
if (!this.pdfDoc) return
console.log('解析的pdf总数', this.pdfDoc.numPages)
const container = this.$refs.pdfWrapper
if (!container) return
container.innerHTML = ''
container.style.minWidth = ''
this.pageTextDivs = []
this.renderedPages.clear()
this.pageContainers = []
this.totalPages = this.pdfDoc.numPages
this.renderQueue = []
this.isProcessingQueue = false
const pageCount = this.pdfDoc.numPages
for (let pageNumber = 1; pageNumber <= pageCount; pageNumber += 1) {
const pageViewport = await this.renderSinglePage(pageNumber, container)
//
container.style.minWidth = `${Math.ceil(pageViewport.width)}px`
const fragment = document.createDocumentFragment()
for (let pageNumber = 1; pageNumber <= this.totalPages; pageNumber += 1) {
const placeholder = document.createElement('div')
placeholder.className = 'pdf-page placeholder'
placeholder.dataset.page = pageNumber
fragment.appendChild(placeholder)
this.pageContainers.push(placeholder)
}
container.appendChild(fragment)
container.scrollTop = 0
this.setupIntersectionObserver()
this.observeInitialPages()
this.isDocumentReady = true
},
async renderSinglePage(pageNumber, container) {
async renderSinglePage(pageNumber) {
if (!this.pdfDoc) return
if (this.renderedPages.has(pageNumber)) return
const index = pageNumber - 1
const container = this.pageContainers[index]
if (!container || container.classList.contains('rendering')) return
container.classList.add('rendering')
try {
const page = await this.pdfDoc.getPage(pageNumber)
const viewport = page.getViewport({ scale: this.scale })
const pageWrapper = document.createElement('div')
pageWrapper.className = 'pdf-page'
pageWrapper.style.width = `${viewport.width}px`
pageWrapper.style.height = `${viewport.height}px`
container.appendChild(pageWrapper)
container.classList.remove('placeholder')
container.style.width = `${viewport.width}px`
container.style.height = `${viewport.height}px`
const canvas = document.createElement('canvas')
canvas.className = 'pdf-canvas'
const outputScale = window.devicePixelRatio || 1
const deviceScale = window.devicePixelRatio || 1
const outputScale = Math.min(deviceScale, 1.5)
canvas.width = viewport.width * outputScale
canvas.height = viewport.height * outputScale
canvas.style.width = `${viewport.width}px`
canvas.style.height = `${viewport.height}px`
pageWrapper.appendChild(canvas)
container.appendChild(canvas)
if (this.$refs.pdfWrapper && !this.$refs.pdfWrapper.style.minWidth) {
this.$refs.pdfWrapper.style.minWidth = `${Math.ceil(viewport.width)}px`
}
const canvasContext = canvas.getContext('2d')
const renderContext = {
@ -196,11 +229,11 @@ export default {
textLayerDiv.className = 'textLayer'
textLayerDiv.style.width = `${viewport.width}px`
textLayerDiv.style.height = `${viewport.height}px`
pageWrapper.appendChild(textLayerDiv)
container.appendChild(textLayerDiv)
const textLayer = new TextLayerBuilder({
textLayerDiv,
pageIndex: pageNumber - 1,
pageIndex: index,
viewport,
eventBus: this.eventBus,
})
@ -213,9 +246,40 @@ export default {
textDivs.forEach((div) => {
div.dataset.originalText = div.textContent
})
this.pageTextDivs.push(textDivs)
this.pageTextDivs[index] = textDivs
this.renderedPages.set(pageNumber, { container, viewport })
if (this.observer) {
this.observer.unobserve(container)
}
} catch (error) {
console.error(`渲染第 ${pageNumber} 页失败`, error)
} finally {
container.classList.remove('rendering')
}
},
return viewport
scheduleRender(pageNumber, { priority = false } = {}) {
if (!pageNumber || this.renderedPages.has(pageNumber)) return
if (this.renderQueue.includes(pageNumber)) return
if (priority) {
this.renderQueue.unshift(pageNumber)
} else {
this.renderQueue.push(pageNumber)
}
this.processRenderQueue()
},
async processRenderQueue() {
if (this.isProcessingQueue) return
this.isProcessingQueue = true
try {
while (this.renderQueue.length) {
const pageNumber = this.renderQueue.shift()
await this.renderSinglePage(pageNumber)
}
} finally {
this.isProcessingQueue = false
}
},
handleSearch: debounce(function () {
@ -223,8 +287,9 @@ export default {
this.resetSearch()
return
}
if (!this.pageTextDivs.length) {
this.$message.warning('PDF 正在加载,请稍候再试')
const hasRenderedContent = this.pageTextDivs.some((divs) => divs && divs.length)
if (!hasRenderedContent) {
this.$message.warning('页面正在准备内容,请稍后或滚动加载后再试')
return
}
this.highlightMatches()
@ -243,6 +308,9 @@ export default {
const results = []
this.pageTextDivs.forEach((textDivs, pageIndex) => {
if (!textDivs || !textDivs.length) {
return
}
textDivs.forEach((div) => {
const original = div.dataset.originalText || div.textContent || ''
if (!original) return
@ -324,6 +392,7 @@ export default {
clearHighlights() {
this.pageTextDivs.forEach((textDivs) => {
if (!textDivs || !textDivs.length) return
textDivs.forEach((div) => {
const original = div.dataset.originalText
if (typeof original !== 'undefined') {
@ -338,6 +407,11 @@ export default {
this.searchResults = []
this.currentResultIndex = -1
this.pageTextDivs = []
this.renderedPages.clear()
this.pageContainers = []
this.totalPages = 0
this.isDocumentReady = false
this.disconnectObserver()
const wrapper = this.$refs.pdfWrapper
if (wrapper) {
wrapper.innerHTML = ''
@ -349,6 +423,50 @@ export default {
if (!this.pdfUrl) return
this.loadDocument()
},
setupIntersectionObserver() {
this.disconnectObserver()
if (!this.$refs.pdfWrapper) return
const options = {
root: this.$refs.pdfWrapper,
rootMargin: '400px 0px',
threshold: 0.01,
}
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const page = Number(entry.target.dataset.page)
if (page) {
this.scheduleRender(page)
}
}
})
}, options)
this.pageContainers.forEach((container) => this.observer.observe(container))
},
observeInitialPages() {
if (!this.pageContainers.length) return
const initial = this.pageContainers.slice(0, 2)
initial.forEach((container) => {
const page = Number(container.dataset.page)
if (page) {
this.scheduleRender(page, { priority: true })
}
})
},
disconnectObserver() {
if (this.observer) {
this.observer.disconnect()
this.observer = null
}
this.renderQueue = []
this.isProcessingQueue = false
},
},
}
</script>
@ -407,16 +525,45 @@ export default {
flex: 1;
overflow: auto;
padding: 24px;
background: linear-gradient(180deg, #eef3ff 0%, #ffffff 100%);
// background: linear-gradient(180deg, #eef3ff 0%, #ffffff 100%);
background: #eaeaea;
position: relative;
}
.pdf-page {
position: relative;
margin: 0 auto 24px;
margin: 0 auto;
margin-top: 18px;
margin-bottom: 18px;
box-shadow: 0 10px 30px rgba(25, 64, 158, 0.12);
border-radius: 8px;
overflow: hidden;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
.pdf-page:first-of-type {
margin-top: 0;
}
.pdf-page:not(:last-of-type) {
margin-bottom: 26px;
}
.pdf-page.placeholder {
box-shadow: none;
background: linear-gradient(135deg, rgba(233, 239, 255, 0.7), rgba(255, 255, 255, 0.7));
border: 1px dashed rgba(112, 143, 255, 0.4);
min-height: 240px;
color: #7f8bff;
font-size: 13px;
letter-spacing: 0.6px;
}
.pdf-page.placeholder::after {
content: '正在准备页面...';
}
.pdf-canvas {