搜索优化

This commit is contained in:
cwchen 2025-11-10 18:41:53 +08:00
parent 10a35036f5
commit a1ebf7c660
1 changed files with 122 additions and 13 deletions

View File

@ -93,6 +93,7 @@ export default {
pageTextDivs: [], pageTextDivs: [],
searchResults: [], searchResults: [],
currentResultIndex: -1, currentResultIndex: -1,
pageMatchRanges: [],
renderedPages: new Map(), renderedPages: new Map(),
observer: null, observer: null,
pageContainers: [], pageContainers: [],
@ -364,6 +365,10 @@ export default {
container.classList.remove('is-loading-text') container.classList.remove('is-loading-text')
} }
if (this.keyword.trim() && this.pageMatchRanges && this.pageMatchRanges.length) {
this.applyHighlightsToPage(index)
}
if (container.dataset.status !== 'rendered') { if (container.dataset.status !== 'rendered') {
container.dataset.status = visible ? 'rendered' : 'text-ready' container.dataset.status = visible ? 'rendered' : 'text-ready'
if (!visible) { if (!visible) {
@ -532,6 +537,8 @@ export default {
console.error('全文预处理失败', error) console.error('全文预处理失败', error)
} }
} }
this.highlightMatches() this.highlightMatches()
} finally { } finally {
this.searching = false this.searching = false
@ -564,6 +571,7 @@ export default {
const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const pattern = new RegExp(`(${escapedKeyword})`, 'gi') const pattern = new RegExp(`(${escapedKeyword})`, 'gi')
const results = [] const results = []
this.pageMatchRanges = Array.from({ length: this.totalPages || 0 }, () => [])
this.pageTextDivs.forEach((textDivs, pageIndex) => { this.pageTextDivs.forEach((textDivs, pageIndex) => {
if (!textDivs || !textDivs.length) { if (!textDivs || !textDivs.length) {
@ -572,10 +580,11 @@ export default {
const segments = [] const segments = []
let position = 0 let position = 0
textDivs.forEach((div) => { textDivs.forEach((div, divIndex) => {
const original = div.dataset.originalText || div.textContent || '' const original = div.dataset.originalText || div.textContent || ''
segments.push({ segments.push({
div, div,
divIndex,
text: original, text: original,
start: position, start: position,
end: position + original.length, end: position + original.length,
@ -607,6 +616,7 @@ export default {
start, start,
end, end,
elements: [], elements: [],
segments: [],
}) })
let segIndex = segments.findIndex((seg) => start < seg.end && end > seg.start) let segIndex = segments.findIndex((seg) => start < seg.end && end > seg.start)
@ -625,6 +635,11 @@ export default {
matchIndex, matchIndex,
}) })
perDivHighlights.set(seg.div, ranges) perDivHighlights.set(seg.div, ranges)
matchRecords[matchIndex].segments.push({
divIndex: seg.divIndex,
start: highlightStart,
end: highlightEnd,
})
} }
if (end <= seg.end) break if (end <= seg.end) break
currentStart = seg.end currentStart = seg.end
@ -671,6 +686,8 @@ export default {
}) })
}) })
const pageMatches = this.pageMatchRanges[pageIndex] || []
matchRecords.forEach((record) => { matchRecords.forEach((record) => {
if (!record.elements.length) return if (!record.elements.length) return
const newIndex = results.length const newIndex = results.length
@ -682,7 +699,13 @@ export default {
element: record.elements[0], element: record.elements[0],
elements: record.elements, elements: record.elements,
}) })
pageMatches.push({
matchIndex: newIndex,
segments: record.segments.map((item) => ({ ...item })),
})
}) })
this.pageMatchRanges[pageIndex] = pageMatches
}) })
this.searchResults = results this.searchResults = results
@ -700,6 +723,8 @@ export default {
if (!skipNavigate) { if (!skipNavigate) {
this.navigateToResult(this.currentResultIndex, false) this.navigateToResult(this.currentResultIndex, false)
} else {
this.updateCurrentResultActiveState()
} }
}, },
@ -720,18 +745,7 @@ export default {
} }
this.currentResultIndex = index this.currentResultIndex = index
this.searchResults.forEach((item, idx) => { this.updateCurrentResultActiveState()
if (!item) return
const elements = item.elements && item.elements.length ? item.elements : [item.element]
elements.forEach((el) => {
if (!el) return
if (idx === this.currentResultIndex) {
el.classList.add('is-active')
} else {
el.classList.remove('is-active')
}
})
})
const target = this.searchResults[this.currentResultIndex]?.element const target = this.searchResults[this.currentResultIndex]?.element
if (!target) return if (!target) return
@ -749,6 +763,99 @@ export default {
}) })
}, },
applyHighlightsToPage(pageIndex) {
if (!this.keyword || !this.pageMatchRanges || !this.pageMatchRanges.length) return
const textDivs = this.pageTextDivs[pageIndex]
if (!textDivs || !textDivs.length) return
const pageMatches = this.pageMatchRanges[pageIndex]
if (!pageMatches || !pageMatches.length) return
textDivs.forEach((div) => {
const original = div.dataset.originalText
if (typeof original !== 'undefined') {
div.textContent = original
}
})
const perDivRanges = new Map()
pageMatches.forEach((match) => {
if (!match || !Array.isArray(match.segments)) return
match.segments.forEach((segment) => {
const { divIndex, start, end } = segment || {}
if (typeof divIndex !== 'number') return
const ranges = perDivRanges.get(divIndex) || []
ranges.push({
start,
end,
matchIndex: match.matchIndex,
})
perDivRanges.set(divIndex, ranges)
})
})
const updatedElements = new Map()
perDivRanges.forEach((ranges, divIndex) => {
const div = textDivs[divIndex]
if (!div) return
const original = div.dataset.originalText || ''
if (!original) return
const sorted = ranges
.slice()
.filter((item) => typeof item.start === 'number' && typeof item.end === 'number')
.sort((a, b) => (a.start === b.start ? a.end - b.end : a.start - b.start))
let cursor = 0
let html = ''
sorted.forEach(({ start, end, matchIndex }) => {
if (start > cursor) {
html += this.escapeForHtml(original.slice(cursor, start))
}
const text = original.slice(start, end)
html += `<mark class="search-highlight" data-match-index="${matchIndex}">${this.escapeForHtml(text)}</mark>`
cursor = end
})
if (cursor < original.length) {
html += this.escapeForHtml(original.slice(cursor))
}
div.innerHTML = html
const marks = div.querySelectorAll('mark.search-highlight')
marks.forEach((mark) => {
const matchIndex = Number(mark.dataset.matchIndex)
if (Number.isNaN(matchIndex)) return
const elements = updatedElements.get(matchIndex) || []
elements.push(mark)
updatedElements.set(matchIndex, elements)
})
})
updatedElements.forEach((elements, matchIndex) => {
if (!this.searchResults[matchIndex]) return
this.searchResults[matchIndex].elements = elements
this.searchResults[matchIndex].element = elements[0] || null
})
this.updateCurrentResultActiveState()
},
updateCurrentResultActiveState() {
this.searchResults.forEach((item, idx) => {
if (!item) return
const elements = item.elements && item.elements.length ? item.elements : item.element ? [item.element] : []
elements.forEach((el) => {
if (!el) return
if (idx === this.currentResultIndex) {
el.classList.add('is-active')
} else {
el.classList.remove('is-active')
}
})
})
},
goToPrevious() { goToPrevious() {
if (!this.searchResults.length) return if (!this.searchResults.length) return
const idx = (this.currentResultIndex - 1 + this.searchResults.length) % this.searchResults.length const idx = (this.currentResultIndex - 1 + this.searchResults.length) % this.searchResults.length
@ -766,6 +873,7 @@ export default {
this.clearHighlights() this.clearHighlights()
this.searchResults = [] this.searchResults = []
this.currentResultIndex = -1 this.currentResultIndex = -1
this.pageMatchRanges = []
}, },
clearHighlights() { clearHighlights() {
@ -784,6 +892,7 @@ export default {
this.clearHighlights() this.clearHighlights()
this.searchResults = [] this.searchResults = []
this.currentResultIndex = -1 this.currentResultIndex = -1
this.pageMatchRanges = []
this.pageTextDivs = [] this.pageTextDivs = []
this.renderedPages.clear() this.renderedPages.clear()
this.pageContainers = [] this.pageContainers = []