diff --git a/src/views/common/DocumentSearch.vue b/src/views/common/DocumentSearch.vue index b271ebc..7cd1338 100644 --- a/src/views/common/DocumentSearch.vue +++ b/src/views/common/DocumentSearch.vue @@ -3,7 +3,9 @@
- 搜索 + + 搜索 +
{{ currentResultIndex + 1 }}/{{ searchResults.length }} @@ -106,6 +108,7 @@ export default { cMapUrl: '', standardFontDataUrl: '', pdfAssetsBase: '', + searching: false, } }, mounted() { @@ -510,25 +513,52 @@ export default { container.classList.add('prefetched') }, - handleSearch: debounce(function () { - if (!this.keyword) { - this.resetSearch() - return - } - const hasRenderedContent = this.pageTextDivs.some((divs) => divs && divs.length) - if (!hasRenderedContent) { - this.$message.warning('页面正在准备内容,请稍后或滚动加载后再试') - return - } - this.highlightMatches() - }, 200), - - highlightMatches() { + handleSearch: debounce(async function () { const keyword = this.keyword.trim() if (!keyword) { this.resetSearch() return } + if (!this.pdfDoc) return + + if (this.searching) return + this.searching = true + try { + const allPrepared = this.pageTextDivs.length === this.totalPages && this.pageTextDivs.every(items => items && items.length) + if (!allPrepared) { + try { + await this.ensureAllTextLayersLoaded() + } catch (error) { + console.error('全文预处理失败', error) + } + } + this.highlightMatches() + } finally { + this.searching = false + } + }, 200), + + async ensureAllTextLayersLoaded() { + if (!this.pdfDoc || !this.totalPages) return + let total = this.totalPages + for (let pageNumber = 1; pageNumber <= total; pageNumber += 1) { + if (this.totalPages !== total) break + if (!this.pageTextDivs[pageNumber - 1] || !this.pageTextDivs[pageNumber - 1].length) { + await this.renderTextLayer(pageNumber, { visible: pageNumber === 1, force: true }) + await this.$nextTick() + } + } + }, + + highlightMatches(options = {}) { + const { preserveIndex = false, skipNavigate = false } = options + const keyword = this.keyword.trim() + if (!keyword) { + this.resetSearch() + return + } + + const previousIndex = preserveIndex ? this.currentResultIndex : 0 this.clearHighlights() const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') @@ -554,6 +584,7 @@ export default { div.innerHTML = highlighted const marks = div.querySelectorAll('mark.search-highlight') marks.forEach((mark) => { + mark.dataset.matchIndex = results.length results.push({ pageIndex, element: mark, @@ -569,14 +600,36 @@ export default { return } - this.currentResultIndex = 0 - this.focusCurrentResult() + if (preserveIndex && previousIndex < results.length && previousIndex >= 0) { + this.currentResultIndex = previousIndex + } else { + this.currentResultIndex = 0 + } + + if (!skipNavigate) { + this.navigateToResult(this.currentResultIndex, false) + } }, focusCurrentResult() { - if (!this.searchResults.length || this.currentResultIndex < 0) return - this.searchResults.forEach((item, index) => { - if (index === this.currentResultIndex) { + this.navigateToResult(this.currentResultIndex) + }, + + async navigateToResult(index, ensureRendered = false) { + if (!this.searchResults.length || index < 0 || index >= this.searchResults.length) return + const currentResult = this.searchResults[index] + const pageNumber = currentResult.pageIndex + 1 + + if (ensureRendered || !this.pageTextDivs[pageNumber - 1] || !this.pageTextDivs[pageNumber - 1].length) { + await this.renderTextLayer(pageNumber, { visible: true, force: true }) + await this.renderCanvas(pageNumber) + await this.$nextTick() + this.highlightMatches({ preserveIndex: true, skipNavigate: true }) + } + + this.currentResultIndex = index + this.searchResults.forEach((item, idx) => { + if (idx === this.currentResultIndex) { item.element.classList.add('is-active') } else { item.element.classList.remove('is-active') @@ -585,30 +638,30 @@ export default { const target = this.searchResults[this.currentResultIndex]?.element if (!target) return - const wrapper = this.$refs.pdfWrapper if (!wrapper) return - const targetRect = target.getBoundingClientRect() - const wrapperRect = wrapper.getBoundingClientRect() - const offset = targetRect.top - wrapperRect.top - wrapper.clientHeight / 2 - wrapper.scrollTo({ - top: wrapper.scrollTop + offset, - behavior: 'smooth', + this.$nextTick(() => { + const targetRect = target.getBoundingClientRect() + const wrapperRect = wrapper.getBoundingClientRect() + const offset = targetRect.top - wrapperRect.top - wrapper.clientHeight / 2 + wrapper.scrollTo({ + top: wrapper.scrollTop + offset, + behavior: 'smooth' + }) }) }, goToPrevious() { if (!this.searchResults.length) return - this.currentResultIndex = - (this.currentResultIndex - 1 + this.searchResults.length) % this.searchResults.length - this.focusCurrentResult() + const idx = (this.currentResultIndex - 1 + this.searchResults.length) % this.searchResults.length + this.navigateToResult(idx) }, goToNext() { if (!this.searchResults.length) return - this.currentResultIndex = (this.currentResultIndex + 1) % this.searchResults.length - this.focusCurrentResult() + const idx = (this.currentResultIndex + 1) % this.searchResults.length + this.navigateToResult(idx) }, resetSearch() { @@ -755,9 +808,17 @@ export default { padding: 0 8px; } +.search-preparing { + display: flex; + align-items: center; + gap: 8px; + color: #506dff; + font-size: 14px; + margin-top: 10px; +} + .viewer-container { flex: 1; - height: 100%; background: #ffffff; border-radius: 12px; overflow: hidden;