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 pdfjsLib.GlobalWorkerOptions.workerSrc = null
} }
const DEFAULT_SCALE = 1.25 const DEFAULT_SCALE = 1
export default { export default {
name: 'DocumentSearch', name: 'DocumentSearch',
@ -91,6 +91,13 @@ export default {
pageTextDivs: [], pageTextDivs: [],
searchResults: [], searchResults: [],
currentResultIndex: -1, currentResultIndex: -1,
renderedPages: new Map(),
observer: null,
pageContainers: [],
totalPages: 0,
isDocumentReady: false,
renderQueue: [],
isProcessingQueue: false,
} }
}, },
mounted() { mounted() {
@ -99,6 +106,9 @@ export default {
this.loadDocument() this.loadDocument()
} }
}, },
beforeDestroy() {
this.disconnectObserver()
},
watch: { watch: {
'$route.query.url'(newUrl, oldUrl) { '$route.query.url'(newUrl, oldUrl) {
if (newUrl !== oldUrl) { if (newUrl !== oldUrl) {
@ -147,75 +157,129 @@ export default {
async renderAllPages() { async renderAllPages() {
if (!this.pdfDoc) return if (!this.pdfDoc) return
console.log('解析的pdf总数', this.pdfDoc.numPages)
const container = this.$refs.pdfWrapper const container = this.$refs.pdfWrapper
if (!container) return if (!container) return
container.innerHTML = '' container.innerHTML = ''
container.style.minWidth = ''
this.pageTextDivs = [] this.pageTextDivs = []
this.renderedPages.clear()
this.pageContainers = []
this.totalPages = this.pdfDoc.numPages
this.renderQueue = []
this.isProcessingQueue = false
const pageCount = this.pdfDoc.numPages const fragment = document.createDocumentFragment()
for (let pageNumber = 1; pageNumber <= pageCount; pageNumber += 1) { for (let pageNumber = 1; pageNumber <= this.totalPages; pageNumber += 1) {
const pageViewport = await this.renderSinglePage(pageNumber, container) const placeholder = document.createElement('div')
// placeholder.className = 'pdf-page placeholder'
container.style.minWidth = `${Math.ceil(pageViewport.width)}px` placeholder.dataset.page = pageNumber
fragment.appendChild(placeholder)
this.pageContainers.push(placeholder)
} }
container.appendChild(fragment)
container.scrollTop = 0 container.scrollTop = 0
this.setupIntersectionObserver()
this.observeInitialPages()
this.isDocumentReady = true
}, },
async renderSinglePage(pageNumber, container) { async renderSinglePage(pageNumber) {
const page = await this.pdfDoc.getPage(pageNumber) if (!this.pdfDoc) return
const viewport = page.getViewport({ scale: this.scale }) if (this.renderedPages.has(pageNumber)) return
const pageWrapper = document.createElement('div') const index = pageNumber - 1
pageWrapper.className = 'pdf-page' const container = this.pageContainers[index]
pageWrapper.style.width = `${viewport.width}px` if (!container || container.classList.contains('rendering')) return
pageWrapper.style.height = `${viewport.height}px`
container.appendChild(pageWrapper)
const canvas = document.createElement('canvas') container.classList.add('rendering')
canvas.className = 'pdf-canvas' try {
const outputScale = window.devicePixelRatio || 1 const page = await this.pdfDoc.getPage(pageNumber)
canvas.width = viewport.width * outputScale const viewport = page.getViewport({ scale: this.scale })
canvas.height = viewport.height * outputScale container.classList.remove('placeholder')
canvas.style.width = `${viewport.width}px` container.style.width = `${viewport.width}px`
canvas.style.height = `${viewport.height}px` container.style.height = `${viewport.height}px`
pageWrapper.appendChild(canvas)
const canvasContext = canvas.getContext('2d') const canvas = document.createElement('canvas')
const renderContext = { canvas.className = 'pdf-canvas'
canvasContext, const deviceScale = window.devicePixelRatio || 1
viewport, 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`
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 = {
canvasContext,
viewport,
}
if (outputScale !== 1) {
renderContext.transform = [outputScale, 0, 0, outputScale, 0, 0]
}
await page.render(renderContext).promise
const textLayerDiv = document.createElement('div')
textLayerDiv.className = 'textLayer'
textLayerDiv.style.width = `${viewport.width}px`
textLayerDiv.style.height = `${viewport.height}px`
container.appendChild(textLayerDiv)
const textLayer = new TextLayerBuilder({
textLayerDiv,
pageIndex: index,
viewport,
eventBus: this.eventBus,
})
const textContent = await page.getTextContent()
textLayer.setTextContent(textContent)
textLayer.render()
const textDivs = [...textLayer.textDivs]
textDivs.forEach((div) => {
div.dataset.originalText = div.textContent
})
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')
} }
if (outputScale !== 1) { },
renderContext.transform = [outputScale, 0, 0, outputScale, 0, 0]
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)
} }
await page.render(renderContext).promise this.processRenderQueue()
},
const textLayerDiv = document.createElement('div') async processRenderQueue() {
textLayerDiv.className = 'textLayer' if (this.isProcessingQueue) return
textLayerDiv.style.width = `${viewport.width}px` this.isProcessingQueue = true
textLayerDiv.style.height = `${viewport.height}px` try {
pageWrapper.appendChild(textLayerDiv) while (this.renderQueue.length) {
const pageNumber = this.renderQueue.shift()
const textLayer = new TextLayerBuilder({ await this.renderSinglePage(pageNumber)
textLayerDiv, }
pageIndex: pageNumber - 1, } finally {
viewport, this.isProcessingQueue = false
eventBus: this.eventBus, }
})
const textContent = await page.getTextContent()
textLayer.setTextContent(textContent)
textLayer.render()
const textDivs = [...textLayer.textDivs]
textDivs.forEach((div) => {
div.dataset.originalText = div.textContent
})
this.pageTextDivs.push(textDivs)
return viewport
}, },
handleSearch: debounce(function () { handleSearch: debounce(function () {
@ -223,8 +287,9 @@ export default {
this.resetSearch() this.resetSearch()
return return
} }
if (!this.pageTextDivs.length) { const hasRenderedContent = this.pageTextDivs.some((divs) => divs && divs.length)
this.$message.warning('PDF 正在加载,请稍候再试') if (!hasRenderedContent) {
this.$message.warning('页面正在准备内容,请稍后或滚动加载后再试')
return return
} }
this.highlightMatches() this.highlightMatches()
@ -243,6 +308,9 @@ export default {
const results = [] const results = []
this.pageTextDivs.forEach((textDivs, pageIndex) => { this.pageTextDivs.forEach((textDivs, pageIndex) => {
if (!textDivs || !textDivs.length) {
return
}
textDivs.forEach((div) => { textDivs.forEach((div) => {
const original = div.dataset.originalText || div.textContent || '' const original = div.dataset.originalText || div.textContent || ''
if (!original) return if (!original) return
@ -324,6 +392,7 @@ export default {
clearHighlights() { clearHighlights() {
this.pageTextDivs.forEach((textDivs) => { this.pageTextDivs.forEach((textDivs) => {
if (!textDivs || !textDivs.length) return
textDivs.forEach((div) => { textDivs.forEach((div) => {
const original = div.dataset.originalText const original = div.dataset.originalText
if (typeof original !== 'undefined') { if (typeof original !== 'undefined') {
@ -338,6 +407,11 @@ export default {
this.searchResults = [] this.searchResults = []
this.currentResultIndex = -1 this.currentResultIndex = -1
this.pageTextDivs = [] this.pageTextDivs = []
this.renderedPages.clear()
this.pageContainers = []
this.totalPages = 0
this.isDocumentReady = false
this.disconnectObserver()
const wrapper = this.$refs.pdfWrapper const wrapper = this.$refs.pdfWrapper
if (wrapper) { if (wrapper) {
wrapper.innerHTML = '' wrapper.innerHTML = ''
@ -349,6 +423,50 @@ export default {
if (!this.pdfUrl) return if (!this.pdfUrl) return
this.loadDocument() 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> </script>
@ -407,16 +525,45 @@ export default {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: 24px; padding: 24px;
background: linear-gradient(180deg, #eef3ff 0%, #ffffff 100%); // background: linear-gradient(180deg, #eef3ff 0%, #ffffff 100%);
background: #eaeaea;
position: relative; position: relative;
} }
.pdf-page { .pdf-page {
position: relative; 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); box-shadow: 0 10px 30px rgba(25, 64, 158, 0.12);
border-radius: 8px; border-radius: 8px;
overflow: hidden; 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 { .pdf-canvas {