From 148c189a0a448ca3e780f347c43e2044c788a937 Mon Sep 17 00:00:00 2001
From: cwchen <1048842385@qq.com>
Date: Mon, 10 Nov 2025 16:33:22 +0800
Subject: [PATCH] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/views/common/DocumentSearch.vue | 127 ++++++++++++++++++++--------
1 file changed, 94 insertions(+), 33 deletions(-)
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;