<template>
    <div class="document-search">
        <div class="search-toolbar">
            <el-input v-model="keyword" class="search-input" placeholder="输入关键字搜索 PDF" clearable
                @keyup.enter.native="handleSearch" @clear="resetSearch">
                <el-button slot="append" icon="el-icon-search" @click="handleSearch" :loading="searching" :disabled="searching">
                    搜索
                </el-button>
            </el-input>
            <div class="search-status" v-if="searchResults.length">
                <span class="result-indicator">{{ currentResultIndex + 1 }}/{{ searchResults.length }}</span>
                <el-button class="nav-btn" type="text" icon="el-icon-arrow-up" :disabled="!searchResults.length"
                    @click="goToPrevious">
                    上一处
                </el-button>
                <el-button class="nav-btn" type="text" icon="el-icon-arrow-down" :disabled="!searchResults.length"
                    @click="goToNext">
                    下一处
                </el-button>
            </div>
        </div>

        <div class="viewer-container">
            <div ref="pdfWrapper" class="pdf-wrapper"></div>

            <transition name="fade">
                <div v-if="overlayType === 'loading'" key="loading" class="state-panel overlay">
                    <i class="el-icon-loading state-icon"></i>
                    <p>正在加载 PDF，请稍候...</p>
                </div>
                <div v-else-if="overlayType === 'error'" key="error" class="state-panel overlay">
                    <i class="el-icon-warning-outline state-icon"></i>
                    <p>{{ error }}</p>
                    <el-button type="primary" @click="reload">重新加载</el-button>
                </div>
                <div v-else-if="overlayType === 'no-url'" key="no-url" class="state-panel overlay">
                    <i class="el-icon-document state-icon"></i>
                    <p>暂未指定 PDF 文件，请通过路由参数 url 传入文件地址。</p>
                </div>
            </transition>
        </div>
    </div>
</template>

<script>
import debounce from 'lodash/debounce'
import 'pdfjs-dist/legacy/web/pdf_viewer.css'
import { EventBus, TextLayerBuilder } from 'pdfjs-dist/legacy/web/pdf_viewer'
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf'
import pdfWorker from 'pdfjs-dist/legacy/build/pdf.worker.entry'

const resolvedWorkerSrc =
    typeof pdfWorker === 'string'
        ? pdfWorker
        : pdfWorker && pdfWorker.default
            ? pdfWorker.default
            : (pdfWorker && pdfWorker.workerSrc) || null

if (resolvedWorkerSrc) {
    pdfjsLib.GlobalWorkerOptions.workerSrc = resolvedWorkerSrc
} else {
    console.warn('未能解析 PDF.js Worker 地址，将在主线程解析 PDF')
    pdfjsLib.GlobalWorkerOptions.workerSrc = null
}

const DEFAULT_SCALE = 1

export default {
    name: 'DocumentSearch',
    computed: {
        overlayType() {
            if (this.loading) {
                return 'loading'
            }
            if (this.error) {
                return 'error'
            }
            if (!this.pdfUrl) {
                return 'no-url'
            }
            return null
        },
    },
    data() {
        return {
            pdfUrl: '',
            pdfDoc: null,
            keyword: '',
            loading: false,
            error: null,
            scale: DEFAULT_SCALE,
            eventBus: new EventBus(),
            pageTextDivs: [],
            searchResults: [],
            currentResultIndex: -1,
            renderedPages: new Map(),
            observer: null,
            pageContainers: [],
            totalPages: 0,
            isDocumentReady: false,
            renderQueue: [],
            isProcessingQueue: false,
            pageCache: new Map(),
            isPrefetching: false,
            prefetchHandle: null,
            prefetchScheduled: false,
            initialPreloadedCount: 0,
            cMapUrl: '',
            standardFontDataUrl: '',
            pdfAssetsBase: '',
            searching: false,
        }
    },
    mounted() {
        const base = (process.env.BASE_URL || '/').replace(/\/+$/, '/')
        this.pdfAssetsBase = `${base}pdfjs/`
        this.cMapUrl = `${this.pdfAssetsBase}cmaps/`
        this.standardFontDataUrl = `${this.pdfAssetsBase}standard_fonts/`

        pdfjsLib.GlobalCMapOptions = {
            url: this.cMapUrl,
            packed: true,
        }
        if ('GlobalStandardFontDataUrl' in pdfjsLib) {
            pdfjsLib.GlobalStandardFontDataUrl = this.standardFontDataUrl
        }
        if (pdfjsLib.GlobalOptions) {
            pdfjsLib.GlobalOptions.disableFontFace = false
            if ('useSystemFonts' in pdfjsLib.GlobalOptions) {
                pdfjsLib.GlobalOptions.useSystemFonts = true
            }
        }

        this.pdfUrl = this.$route.query.url || 'http://192.168.0.14:9090/smart-bid/technicalSolutionDatabase/2025/10/30/fe5b46ea37554516a71e7c0c486d3715.pdf'
        if (this.pdfUrl) {
            this.loadDocument()
        }
    },
    beforeDestroy() {
        this.disconnectObserver()
    },
    watch: {
        '$route.query.url'(newUrl, oldUrl) {
            if (newUrl !== oldUrl) {
                this.pdfUrl = newUrl || ''
                this.resetViewerState()
                if (this.pdfUrl) {
                    this.loadDocument()
                }
            }
        },
    },
    methods: {
        async loadDocument() {
            if (!this.pdfUrl) return
            this.loading = true
            this.error = null
            this.resetViewerState()
            await this.$nextTick()

            try {
                const headers = {}
                const token = this.$store?.getters?.token || window?.sessionStorage?.getItem('token')
                if (token) {
                    headers.Authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`
                }

                const loadingTask = pdfjsLib.getDocument({
                    url: this.pdfUrl,
                    withCredentials: true,
                    httpHeaders: headers,
                    disableWorker: !resolvedWorkerSrc,
                    useWorkerFetch: !!resolvedWorkerSrc,
                    cMapUrl: this.cMapUrl,
                    cMapPacked: true,
                    standardFontDataUrl: this.standardFontDataUrl,
                    useSystemFonts: true,
                    fontExtraProperties: true,
                })
                this.pdfDoc = await loadingTask.promise
                console.log('PDF 文档加载成功', this.pdfDoc)
                await this.renderAllPages()
                if (this.keyword) {
                    this.highlightMatches()
                }
            } catch (err) {
                console.error('加载 PDF 失败:', err)
                this.error = err?.message || 'PDF 文件加载失败，请稍后再试'
            } finally {
                this.loading = false
            }
        },

        async renderAllPages() {
            if (!this.pdfDoc) return
            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
            this.pageCache.clear()
            this.isPrefetching = false
            this.initialPreloadedCount = 0

            const fragment = document.createDocumentFragment()
            for (let pageNumber = 1; pageNumber <= this.totalPages; pageNumber += 1) {
                const placeholder = document.createElement('div')
                placeholder.className = 'pdf-page placeholder'
                placeholder.style.margin = '0px auto 10px'
                placeholder.style.position= 'relative';
                placeholder.dataset.page = pageNumber
                placeholder.dataset.status = 'placeholder'
                fragment.appendChild(placeholder)
                this.pageContainers.push(placeholder)
            }
            container.appendChild(fragment)
            container.scrollTop = 0

            this.setupIntersectionObserver()
            await this.preloadInitialPages()
            this.observeInitialPages()
            this.schedulePrefetch()
            this.isDocumentReady = true
        },

        ensureContainerDimensions(pageNumber, viewport) {
            const index = pageNumber - 1
            const container = this.pageContainers[index]
            if (!container) return

            container.style.width = `${viewport.width}px`
            container.style.height = `${viewport.height}px`
            container.style.margin = '0px auto 10px'
            container.style.backgroundColor = '#fff';
            if (!container.dataset.status || container.dataset.status === 'placeholder') {
                container.dataset.status = 'prefetched'
            }
            container.classList.remove('placeholder')
            if (container.dataset.status !== 'rendered') {
                container.classList.add('prefetched')
            }

            if (this.$refs.pdfWrapper && !this.$refs.pdfWrapper.style.minWidth) {
                this.$refs.pdfWrapper.style.minWidth = `${Math.ceil(viewport.width)}px`
            }
        },

        async ensurePageCached(pageNumber) {
            if (!this.pdfDoc) return null
            if (this.pageCache.has(pageNumber)) {
                return this.pageCache.get(pageNumber)
            }
            const page = await this.pdfDoc.getPage(pageNumber)
            this.pageCache.set(pageNumber, page)
            return page
        },

        async renderCanvas(pageNumber) {
            const page = await this.ensurePageCached(pageNumber)
            if (!page) return null

            const index = pageNumber - 1
            const container = this.pageContainers[index]
            if (!container) return null

            const viewport = page.getViewport({ scale: this.scale })
            this.ensureContainerDimensions(pageNumber, viewport)
            container.classList.add('is-loading')

            const oldCanvas = container.querySelector('.pdf-canvas')
            if (oldCanvas) {
                oldCanvas.remove()
            }

            const canvas = document.createElement('canvas')
            canvas.className = 'pdf-canvas'
            const deviceScale = window.devicePixelRatio || 1
            const outputScale = deviceScale > 1 ? deviceScale : 1
            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)

            const canvasContext = canvas.getContext('2d')
            if (canvasContext && 'imageSmoothingEnabled' in canvasContext) {
                canvasContext.imageSmoothingEnabled = false
            }
            const renderContext = {
                canvasContext,
                viewport,
            }
            if (outputScale !== 1) {
                renderContext.transform = [outputScale, 0, 0, outputScale, 0, 0]
            }
            await page.render(renderContext).promise

            container.dataset.status = 'rendered'
            container.classList.remove('prefetched')

            const textLayerDiv = container.querySelector('.textLayer')
            if (textLayerDiv) {
                textLayerDiv.style.display = ''
            }

            this.renderedPages.set(pageNumber, { container, viewport })
            container.classList.remove('is-loading')
            return { page, viewport, container }
        },

        async renderTextLayer(pageNumber, { visible = true, force = false } = {}) {
            const page = await this.ensurePageCached(pageNumber)
            if (!page) return

            const index = pageNumber - 1
            const container = this.pageContainers[index]
            if (!container) return

            const viewport = page.getViewport({ scale: this.scale })
            this.ensureContainerDimensions(pageNumber, viewport)
            container.classList.add('is-loading-text')

            const existing = container.querySelector('.textLayer')
            if (existing && !force && this.pageTextDivs[index]?.length) {
                existing.style.display = ''
                container.classList.remove('is-loading-text')
                return
            }
            if (existing) {
                existing.remove()
            }

            const textLayerDiv = document.createElement('div')
            textLayerDiv.className = 'textLayer'
            textLayerDiv.style.width = `${viewport.width}px`
            textLayerDiv.style.height = `${viewport.height}px`
            textLayerDiv.style.display = ''
            container.appendChild(textLayerDiv)
            const textLayer = new TextLayerBuilder({
                textLayerDiv,
                pageIndex: index,
                viewport,
                eventBus: this.eventBus,
            })

            try {
                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
            } catch (error) {
                console.warn(`加载第 ${pageNumber} 页文本失败`, error)
            } finally {
                container.classList.remove('is-loading-text')
            }

            if (container.dataset.status !== 'rendered') {
                container.dataset.status = visible ? 'rendered' : 'text-ready'
                if (!visible) {
                    container.classList.add('prefetched')
                }
            }
        },

        async renderSinglePage(pageNumber) {
            const container = this.pageContainers[pageNumber - 1]
            if (container) {
                container.classList.add('is-loading', 'is-loading-text')
            }
            try {
                await this.renderTextLayer(pageNumber, { visible: true, force: true })
                await this.renderCanvas(pageNumber)
            } finally {
                if (container) {
                    container.classList.remove('is-loading', 'is-loading-text')
                }
            }
        },

        scheduleRender(pageNumber, { priority = false } = {}) {
            if (!pageNumber || pageNumber > this.totalPages || this.renderedPages.has(pageNumber)) return
            if (this.renderQueue.includes(pageNumber)) return
            if (priority) {
                this.renderQueue.unshift(pageNumber)
            } else {
                this.renderQueue.push(pageNumber)
            }
            this.processRenderQueue()
        },

        async processRenderQueue() {
            if (this.isProcessingQueue) return
            this.isProcessingQueue = true
            try {
                while (this.renderQueue.length) {
                    const pageNumber = this.renderQueue.shift()
                    await new Promise((resolve) => requestAnimationFrame(resolve))
                    await this.renderSinglePage(pageNumber)
                }
            } finally {
                this.isProcessingQueue = false
            }
        },

        schedulePrefetch() {
            this.cancelPrefetch()
            if (!this.totalPages) return
            const startPage = Math.min(this.totalPages, Math.max(1, this.initialPreloadedCount + 1))
            let pageNumber = startPage

            const fetchNext = async () => {
                this.prefetchScheduled = false
                this.prefetchHandle = null
                if (pageNumber > this.totalPages) {
                    this.isPrefetching = false
                    return
                }
                if (!this.pageCache.has(pageNumber)) {
                    try {
                        const page = await this.pdfDoc.getPage(pageNumber)
                        this.pageCache.set(pageNumber, page)
                        const viewport = page.getViewport({ scale: this.scale })
                        this.ensureContainerDimensions(pageNumber, viewport)
                    } catch (error) {
                        console.warn(`预取第 ${pageNumber} 页失败`, error)
                    }
                }
                pageNumber += 1
                if (pageNumber <= this.totalPages) {
                    const schedule = () => {
                        this.prefetchScheduled = true
                        if (typeof window.requestIdleCallback === 'function') {
                            this.prefetchHandle = window.requestIdleCallback(fetchNext, { timeout: 1000 })
                        } else {
                            this.prefetchHandle = window.setTimeout(fetchNext, 80)
                        }
                    }
                    schedule()
                } else {
                    this.isPrefetching = false
                }
            }

            this.isPrefetching = true
            if (typeof window.requestIdleCallback === 'function') {
                this.prefetchScheduled = true
                this.prefetchHandle = window.requestIdleCallback(fetchNext, { timeout: 800 })
            } else {
                this.prefetchScheduled = true
                this.prefetchHandle = window.setTimeout(fetchNext, 120)
            }
        },

        cancelPrefetch() {
            if (this.prefetchHandle) {
                if (typeof window.cancelIdleCallback === 'function' && this.prefetchScheduled) {
                    window.cancelIdleCallback(this.prefetchHandle)
                } else {
                    clearTimeout(this.prefetchHandle)
                }
            }
            this.prefetchHandle = null
            this.prefetchScheduled = false
            this.isPrefetching = false
        },

        async preloadInitialPages() {
            const preloadCount = Math.min(3, this.totalPages)
            if (!preloadCount) {
                this.initialPreloadedCount = 0
                return
            }

            for (let pageNumber = 1; pageNumber <= preloadCount; pageNumber += 1) {
                await this.renderTextLayer(pageNumber, { visible: pageNumber === 1 })
            }

            if (preloadCount >= 1) {
                await this.renderCanvas(1)
            }

            this.initialPreloadedCount = preloadCount
        },

        unloadPage(pageNumber) {
            if (!pageNumber) return
            const index = pageNumber - 1
            const container = this.pageContainers[index]
            if (!container || container.dataset.status !== 'rendered') return

            const canvas = container.querySelector('.pdf-canvas')
            if (canvas) {
                canvas.remove()
            }
            const textLayer = container.querySelector('.textLayer')
            if (textLayer) {
                textLayer.style.display = 'none'
            }

            container.classList.remove('is-loading')
            this.renderedPages.delete(pageNumber)
            container.dataset.status = this.pageTextDivs[index]?.length ? 'text-ready' : 'prefetched'
            container.classList.add('prefetched')
        },

        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, '\\$&')
            const pattern = new RegExp(`(${escapedKeyword})`, 'gi')
            const results = []

            this.pageTextDivs.forEach((textDivs, pageIndex) => {
                if (!textDivs || !textDivs.length) {
                    return
                }

                const segments = []
                let position = 0
                textDivs.forEach((div) => {
                    const original = div.dataset.originalText || div.textContent || ''
                    segments.push({
                        div,
                        text: original,
                        start: position,
                        end: position + original.length,
                    })
                    position += original.length
                })

                if (!segments.length) {
                    return
                }

                const pageText = segments.map((seg) => seg.text).join('')
                if (!pageText) {
                    return
                }

                pattern.lastIndex = 0
                const perDivHighlights = new Map()
                const matchRecords = []

                let match
                while ((match = pattern.exec(pageText)) !== null) {
                    if (!match[0]) continue
                    const start = match.index
                    const end = start + match[0].length
                    const matchIndex = matchRecords.length
                    matchRecords.push({
                        pageIndex,
                        start,
                        end,
                        elements: [],
                    })

                    let segIndex = segments.findIndex((seg) => start < seg.end && end > seg.start)
                    if (segIndex === -1) continue

                    let currentStart = start
                    while (segIndex < segments.length && currentStart < end) {
                        const seg = segments[segIndex]
                        const highlightStart = Math.max(0, currentStart - seg.start)
                        const highlightEnd = Math.min(seg.text.length, end - seg.start)
                        if (highlightEnd > highlightStart) {
                            const ranges = perDivHighlights.get(seg.div) || []
                            ranges.push({
                                start: highlightStart,
                                end: highlightEnd,
                                matchIndex,
                            })
                            perDivHighlights.set(seg.div, ranges)
                        }
                        if (end <= seg.end) break
                        currentStart = seg.end
                        segIndex += 1
                    }
                }

                if (!matchRecords.length) {
                    return
                }

                perDivHighlights.forEach((ranges, div) => {
                    const original = div.dataset.originalText || div.textContent || ''
                    if (!original) return

                    const sorted = ranges
                        .slice()
                        .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
                })

                perDivHighlights.forEach((_ranges, div) => {
                    const marks = div.querySelectorAll('mark.search-highlight')
                    marks.forEach((mark) => {
                        const matchIndex = Number(mark.dataset.matchIndex)
                        if (!Number.isNaN(matchIndex) && matchRecords[matchIndex]) {
                            matchRecords[matchIndex].elements.push(mark)
                            mark.dataset.pageIndex = String(pageIndex)
                        }
                    })
                })

                matchRecords.forEach((record) => {
                    if (!record.elements.length) return
                    const newIndex = results.length
                    record.elements.forEach((mark) => {
                        mark.dataset.matchIndex = String(newIndex)
                    })
                    results.push({
                        pageIndex,
                        element: record.elements[0],
                        elements: record.elements,
                    })
                })
            })

            this.searchResults = results
            if (!results.length) {
                this.currentResultIndex = -1
                this.$message.info(`未找到“${keyword}”相关内容`)
                return
            }

            if (preserveIndex && previousIndex < results.length && previousIndex >= 0) {
                this.currentResultIndex = previousIndex
            } else {
                this.currentResultIndex = 0
            }

            if (!skipNavigate) {
                this.navigateToResult(this.currentResultIndex, false)
            }
        },

        focusCurrentResult() {
            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 (!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
            if (!target) return
            const wrapper = this.$refs.pdfWrapper
            if (!wrapper) return

            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
            const idx = (this.currentResultIndex - 1 + this.searchResults.length) % this.searchResults.length
            this.navigateToResult(idx)
        },

        goToNext() {
            if (!this.searchResults.length) return
            const idx = (this.currentResultIndex + 1) % this.searchResults.length
            this.navigateToResult(idx)
        },

        resetSearch() {
            this.keyword = ''
            this.clearHighlights()
            this.searchResults = []
            this.currentResultIndex = -1
        },

        clearHighlights() {
            this.pageTextDivs.forEach((textDivs) => {
                if (!textDivs || !textDivs.length) return
                textDivs.forEach((div) => {
                    const original = div.dataset.originalText
                    if (typeof original !== 'undefined') {
                        div.textContent = original
                    }
                })
            })
        },

        resetViewerState() {
            this.clearHighlights()
            this.searchResults = []
            this.currentResultIndex = -1
            this.pageTextDivs = []
            this.renderedPages.clear()
            this.pageContainers = []
            this.totalPages = 0
            this.isDocumentReady = false
            this.disconnectObserver()
            this.cancelPrefetch()
            this.pageCache.clear()
            const wrapper = this.$refs.pdfWrapper
            if (wrapper) {
                wrapper.innerHTML = ''
                wrapper.scrollTop = 0
            }
        },

        escapeForHtml(text = '') {
            return text
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#39;')
        },

        reload() {
            if (!this.pdfUrl) return
            this.loadDocument()
        },

        setupIntersectionObserver() {
            this.disconnectObserver()
            if (!this.$refs.pdfWrapper) return

            const options = {
                root: this.$refs.pdfWrapper,
                rootMargin: '120px 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.renderTextLayer(page, { visible: true, force: true })
                this.scheduleRender(page, { priority: true })
            }
                    } else {
                        const page = Number(entry.target.dataset.page)
                        if (page) {
                            this.unloadPage(page)
                        }
                    }
                })
            }, options)

            this.pageContainers.forEach((container) => this.observer.observe(container))
        },

        observeInitialPages() {
            if (!this.pageContainers.length) return
            const initial = this.pageContainers.slice(0, 3)
            initial.forEach((container) => {
                const page = Number(container.dataset.page)
                if (page) {
                        if (page === 1) {
                            // 已在预处理中完成
                            return
                        }
                        if (page <= this.initialPreloadedCount) {
                            this.scheduleRender(page, { priority: true })
                        } else {
                            this.scheduleRender(page)
                        }
                }
            })
        },

        disconnectObserver() {
            if (this.observer) {
                this.observer.disconnect()
                this.observer = null
            }
            this.renderQueue = []
            this.isProcessingQueue = false
            this.cancelPrefetch()
        },
    },
}
</script>

<style scoped lang="scss">
.document-search {
    display: flex;
    flex-direction: column;
  height: calc(100vh - 84px);
  overflow: hidden;
    background: #f4f7ff;
    padding: 16px;
    box-sizing: border-box;
}

.search-toolbar {
    display: flex;
    align-items: center;
    gap: 16px;
    margin-bottom: 16px;
    flex-wrap: wrap;
}

.search-input {
    flex: 1;
    min-width: 240px;
}

.search-status {
    display: flex;
    align-items: center;
    gap: 8px;
    color: #506dff;
}

.result-indicator {
    font-weight: 600;
}

.nav-btn {
    padding: 0 8px;
}

.search-preparing {
    display: flex;
    align-items: center;
    gap: 8px;
    color: #506dff;
    font-size: 14px;
    margin-top: 10px;
}

.viewer-container {
    flex: 1;
    background: #ffffff;
    border-radius: 12px;
    overflow: hidden;
    position: relative;
    display: flex;
    min-height: 480px;
}

.pdf-wrapper {
    flex: 1;
    overflow: auto;
    padding: 24px;
    // background: linear-gradient(180deg, #eef3ff 0%, #ffffff 100%);
    background: #eaeaea;
    position: relative;
}

.pdf-page {
    position: relative;
    margin: 0px auto 10px !important;
    box-shadow: 0 10px 30px rgba(25, 64, 158, 0.12);
    border-radius: 8px;
    overflow: hidden;
    background: #ffffff;
    display: block;
}

.pdf-page:first-of-type::before,
.pdf-page:last-of-type::after {
    display: none;
}

.pdf-page::before,
.pdf-page::after {
    content: '';
    position: absolute;
    left: 5%;
    right: 5%;
    height: 12px;
    height: 8px;
    border-radius: 6px;
    background: linear-gradient(180deg, rgba(206, 216, 232, 0.65) 0%, rgba(208, 216, 230, 0.25) 100%);
    box-shadow: 0 2px 6px rgba(102, 125, 160, 0.25);
}

.pdf-page::before {
    top: -20px;
}

.pdf-page::after {
    bottom: -28px;
}

.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-page.prefetched {
    background: #fdfdfd;
    box-shadow: inset 0 0 0 1px rgba(112, 143, 255, 0.2);
    position: relative;
}

.pdf-page.prefetched::after {
    content: '滚动到此处以加载内容';
    color: rgba(112, 143, 255, 0.7);
    font-size: 12px;
    letter-spacing: 0.4px;
}

.pdf-canvas {
    width: 100%;
    display: block;
    margin: 0 auto;
}

.textLayer {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    pointer-events: auto;
    user-select: text;
    -webkit-user-select: text;
}

.textLayer>span {
    cursor: text;
    user-select: text;
    -webkit-user-select: text;
}

.pdf-page.is-loading::after {
    opacity: 0.5;
    background: rgba(255, 255, 255, 0.7);
}

.pdf-page.is-loading .textLayer,
.pdf-page.is-loading .pdf-canvas {
    opacity: 0.35;
}

@keyframes pdf-page-spin {
    0% {
        transform: rotate(0deg);
    }
    100% {
        transform: rotate(360deg);
    }
}

.pdf-page > canvas.pdf-canvas {
    position: relative;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
}

.pdf-page > .textLayer {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
}

.search-highlight {
    background: rgba(255, 241, 168, 0.9);
    padding: 0 2px;
    border-radius: 2px;
}

.search-highlight.is-active {
    background: rgba(32, 109, 255, 0.85);
    color: #ffffff;
}

.state-panel {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    color: #6072a1;
    gap: 12px;
    font-size: 15px;
    text-align: center;
}

.state-icon {
    font-size: 28px;
    color: #1f72ea;
}

.overlay {
    position: absolute;
    inset: 0;
    z-index: 2;
    background: linear-gradient(180deg, rgba(238, 243, 255, 0.92) 0%, rgba(255, 255, 255, 0.96) 100%);
}

.fade-enter-active,
.fade-leave-active {
    transition: opacity 0.25s ease;
}

.fade-enter,
.fade-leave-to {
    opacity: 0;
}
</style>