diff --git a/src/views/common/DocumentSearch.vue b/src/views/common/DocumentSearch.vue index cafc390..e8e45a3 100644 --- a/src/views/common/DocumentSearch.vue +++ b/src/views/common/DocumentSearch.vue @@ -93,6 +93,7 @@ export default { pageTextDivs: [], searchResults: [], currentResultIndex: -1, + pageMatchRanges: [], renderedPages: new Map(), observer: null, pageContainers: [], @@ -364,6 +365,10 @@ export default { container.classList.remove('is-loading-text') } + if (this.keyword.trim() && this.pageMatchRanges && this.pageMatchRanges.length) { + this.applyHighlightsToPage(index) + } + if (container.dataset.status !== 'rendered') { container.dataset.status = visible ? 'rendered' : 'text-ready' if (!visible) { @@ -532,6 +537,8 @@ export default { console.error('全文预处理失败', error) } } + + this.highlightMatches() } finally { this.searching = false @@ -564,6 +571,7 @@ export default { const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const pattern = new RegExp(`(${escapedKeyword})`, 'gi') const results = [] + this.pageMatchRanges = Array.from({ length: this.totalPages || 0 }, () => []) this.pageTextDivs.forEach((textDivs, pageIndex) => { if (!textDivs || !textDivs.length) { @@ -572,10 +580,11 @@ export default { const segments = [] let position = 0 - textDivs.forEach((div) => { + textDivs.forEach((div, divIndex) => { const original = div.dataset.originalText || div.textContent || '' segments.push({ div, + divIndex, text: original, start: position, end: position + original.length, @@ -607,6 +616,7 @@ export default { start, end, elements: [], + segments: [], }) let segIndex = segments.findIndex((seg) => start < seg.end && end > seg.start) @@ -625,6 +635,11 @@ export default { matchIndex, }) perDivHighlights.set(seg.div, ranges) + matchRecords[matchIndex].segments.push({ + divIndex: seg.divIndex, + start: highlightStart, + end: highlightEnd, + }) } if (end <= seg.end) break currentStart = seg.end @@ -671,6 +686,8 @@ export default { }) }) + const pageMatches = this.pageMatchRanges[pageIndex] || [] + matchRecords.forEach((record) => { if (!record.elements.length) return const newIndex = results.length @@ -682,7 +699,13 @@ export default { element: record.elements[0], elements: record.elements, }) + pageMatches.push({ + matchIndex: newIndex, + segments: record.segments.map((item) => ({ ...item })), + }) }) + + this.pageMatchRanges[pageIndex] = pageMatches }) this.searchResults = results @@ -700,6 +723,8 @@ export default { if (!skipNavigate) { this.navigateToResult(this.currentResultIndex, false) + } else { + this.updateCurrentResultActiveState() } }, @@ -720,18 +745,7 @@ export default { } this.currentResultIndex = index - this.searchResults.forEach((item, idx) => { - 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') - } - }) - }) + this.updateCurrentResultActiveState() const target = this.searchResults[this.currentResultIndex]?.element 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 += `${this.escapeForHtml(text)}` + 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() { if (!this.searchResults.length) return const idx = (this.currentResultIndex - 1 + this.searchResults.length) % this.searchResults.length @@ -766,6 +873,7 @@ export default { this.clearHighlights() this.searchResults = [] this.currentResultIndex = -1 + this.pageMatchRanges = [] }, clearHighlights() { @@ -784,6 +892,7 @@ export default { this.clearHighlights() this.searchResults = [] this.currentResultIndex = -1 + this.pageMatchRanges = [] this.pageTextDivs = [] this.renderedPages.clear() this.pageContainers = []