From c36ed499b64d11d01dbdbb7e784f656b5aff183d Mon Sep 17 00:00:00 2001 From: cwchen <1048842385@qq.com> Date: Mon, 10 Nov 2025 11:12:13 +0800 Subject: [PATCH] =?UTF-8?q?pdf=20=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/common/DocumentSearch.vue | 265 +++++++++++++++++++++------- 1 file changed, 206 insertions(+), 59 deletions(-) diff --git a/src/views/common/DocumentSearch.vue b/src/views/common/DocumentSearch.vue index fc2b6a4..c07b6c3 100644 --- a/src/views/common/DocumentSearch.vue +++ b/src/views/common/DocumentSearch.vue @@ -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,75 +157,129 @@ 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) { - const page = await this.pdfDoc.getPage(pageNumber) - const viewport = page.getViewport({ scale: this.scale }) + async renderSinglePage(pageNumber) { + if (!this.pdfDoc) return + if (this.renderedPages.has(pageNumber)) return - const pageWrapper = document.createElement('div') - pageWrapper.className = 'pdf-page' - pageWrapper.style.width = `${viewport.width}px` - pageWrapper.style.height = `${viewport.height}px` - container.appendChild(pageWrapper) + const index = pageNumber - 1 + const container = this.pageContainers[index] + if (!container || container.classList.contains('rendering')) return - const canvas = document.createElement('canvas') - canvas.className = 'pdf-canvas' - const outputScale = window.devicePixelRatio || 1 - 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.classList.add('rendering') + try { + const page = await this.pdfDoc.getPage(pageNumber) + const viewport = page.getViewport({ scale: this.scale }) + container.classList.remove('placeholder') + container.style.width = `${viewport.width}px` + container.style.height = `${viewport.height}px` - const canvasContext = canvas.getContext('2d') - const renderContext = { - canvasContext, - viewport, + const canvas = document.createElement('canvas') + canvas.className = 'pdf-canvas' + 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` + 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') - textLayerDiv.className = 'textLayer' - textLayerDiv.style.width = `${viewport.width}px` - textLayerDiv.style.height = `${viewport.height}px` - pageWrapper.appendChild(textLayerDiv) - - const textLayer = new TextLayerBuilder({ - textLayerDiv, - pageIndex: pageNumber - 1, - 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.push(textDivs) - - return viewport + 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 + }, }, } @@ -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 {