@l1yp/file-viewer 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"JsonlViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/JsonlViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { parseJsonl, type JsonlRecord } from '@/lib/jsonl'\nimport { highlightCode } from '@/lib/highlight'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer JSON Lines (`.jsonl` / `.ndjson`) renderer (render mode). Receives the fetched text (the panel\n * loads it, like CsvViewer / SubtitleViewer), parses it into per-line records (see lib/jsonl.ts), and shows a\n * compact LIST — one truncated single-line preview per record. Clicking a row expands it to the pretty-printed,\n * syntax-highlighted value. Big files stay responsive two ways: each row's preview is a short single line, and\n * only an initial window of rows is in the DOM (a \"load more\" button grows it). The source toggle (header) shows\n * the raw file highlighted as JSON.\n */\nconst props = defineProps<{ text: string; name: string }>()\n\nconst PREVIEW_CHARS = 200 // truncate each row's inline preview so even a huge record is one short line\nconst INITIAL_ROWS = 200 // rows mounted initially; \"load more\" grows the window\nconst ROW_STEP = 400\n\nconst doc = computed(() => parseJsonl(props.text))\nconst shown = ref(INITIAL_ROWS)\nconst expanded = ref<Set<number>>(new Set())\n\n// A new file resets the window + any open rows.\nwatch(\n () => props.text,\n () => {\n shown.value = INITIAL_ROWS\n expanded.value = new Set()\n },\n)\n\nconst visible = computed(() => doc.value.records.slice(0, shown.value))\nconst remaining = computed(() => Math.max(0, doc.value.records.length - shown.value))\n\nfunction loadMore(): void {\n shown.value += ROW_STEP\n}\n\nfunction toggle(line: number): void {\n const s = new Set(expanded.value)\n if (s.has(line)) s.delete(line)\n else s.add(line)\n expanded.value = s\n}\n\n// Per-row copy: write the record's RAW line (the faithful on-disk text, not the re-stringified value) to the\n// clipboard, and flash a ✓ on that row. Mirrors the CodeBlock copy pattern (best-effort, cleared on a timer).\nconst copiedLine = ref<number | null>(null)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyRow(r: JsonlRecord): Promise<void> {\n try {\n await navigator.clipboard?.writeText(r.raw)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copiedLine.value = r.line\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copiedLine.value = null), 1400)\n}\nonBeforeUnmount(() => clearTimeout(copyTimer))\n\n// Compact one-line preview: stringify the parsed value (whitespace collapsed) or, for an error row, the raw\n// line — then hard-truncate. Building it from the value normalizes spacing so the list reads cleanly.\nfunction preview(r: JsonlRecord): string {\n let s: string\n if (r.error) s = r.raw\n else {\n try {\n s = JSON.stringify(r.value) ?? String(r.value)\n } catch {\n s = r.raw\n }\n }\n s = s.replace(/\\s+/g, ' ').trim()\n return s.length > PREVIEW_CHARS ? s.slice(0, PREVIEW_CHARS) + '…' : s\n}\n\n// Pretty value, highlighted as JSON — built lazily, only for an expanded row.\nfunction expandedHtml(r: JsonlRecord): string {\n if (r.error) return highlightCode(r.raw, 'json')\n try {\n return highlightCode(JSON.stringify(r.value, null, 2) ?? '', 'json')\n } catch {\n return highlightCode(r.raw, 'json')\n }\n}\n</script>\n\n<template>\n <div class=\"jl\">\n <div class=\"jl__summary\">\n <span class=\"jl__count\">{{ doc.records.length }} 条记录</span>\n <span v-if=\"doc.errorCount\" class=\"jl__errtag\">{{ doc.errorCount }} 行无法解析</span>\n </div>\n\n <div class=\"jl__list\">\n <p v-if=\"!doc.records.length\" class=\"jl__msg\">(空文件,无 JSON 记录)</p>\n\n <div v-for=\"r in visible\" :key=\"r.line\" class=\"jl__rowwrap\">\n <div\n class=\"jl__rowhead\"\n :class=\"{ 'jl__rowhead--open': expanded.has(r.line), 'jl__rowhead--err': r.error }\"\n >\n <button\n type=\"button\"\n class=\"jl__row\"\n :title=\"r.error ? `第 ${r.line} 行:JSON 无效` : `第 ${r.line} 行 —— 点击展开`\"\n @click=\"toggle(r.line)\"\n >\n <span class=\"jl__caret\" :class=\"{ 'jl__caret--open': expanded.has(r.line) }\">▶</span>\n <span class=\"jl__ln\">{{ r.line }}</span>\n <span v-if=\"r.error\" class=\"jl__badge\">无效</span>\n <span class=\"jl__preview\">{{ preview(r) }}</span>\n </button>\n <button\n type=\"button\"\n class=\"jl__copy\"\n :class=\"{ 'jl__copy--done': copiedLine === r.line }\"\n :title=\"copiedLine === r.line ? '已复制' : '复制此行'\"\n @click=\"copyRow(r)\"\n >\n <Icon :name=\"copiedLine === r.line ? 'check' : 'copy'\" :size=\"13\" />\n </button>\n </div>\n <pre\n v-if=\"expanded.has(r.line)\"\n class=\"jl__full\"\n ><code class=\"hljs\" v-html=\"expandedHtml(r)\" /></pre>\n </div>\n\n <button v-if=\"remaining\" type=\"button\" class=\"jl__more\" @click=\"loadMore\">\n 显示更多(还有 {{ remaining }} 条)\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.jl__summary {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 6px 12px;\n flex-wrap: wrap;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.jl__count {\n font-weight: 600;\n color: var(--accent);\n}\n.jl__errtag {\n padding: 1px 7px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--danger) 14%, var(--surface));\n color: var(--danger);\n font-size: 10.5px;\n}\n\n.jl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n\n.jl__list {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: scroll; /* always reserve the gutter so the draggable bar is present, not wheel-only */\n overscroll-behavior: contain;\n padding: 4px 0;\n /* Always-visible, draggable scrollbar — the record list gets long and an auto-hidden overlay bar reads\n * as \"no scrollbar\". Thin styled bar in Firefox; a classic (gutter-reserving) bar in Chromium/WebKit. */\n scrollbar-width: thin;\n scrollbar-color: var(--border-2) transparent;\n}\n.jl__list::-webkit-scrollbar {\n width: 12px;\n}\n.jl__list::-webkit-scrollbar-thumb {\n background: var(--border-2);\n background-clip: padding-box;\n border: 3px solid transparent;\n border-radius: 999px;\n}\n.jl__list::-webkit-scrollbar-thumb:hover {\n background: var(--muted);\n background-clip: padding-box;\n border: 3px solid transparent;\n}\n.jl__rowwrap {\n border-bottom: 1px solid var(--border);\n}\n\n/* Row header = the clickable preview button + a trailing copy button (a button can't nest inside a button,\n * so they're siblings in this flex row; the hover/open/error tint lives here, spanning both). */\n.jl__rowhead {\n display: flex;\n align-items: stretch;\n}\n.jl__rowhead:hover,\n.jl__rowhead--open {\n background: var(--surface-2);\n}\n\n.jl__row {\n display: flex;\n align-items: baseline;\n gap: 9px;\n flex: 1 1 auto;\n min-width: 0;\n padding: 6px 14px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n/* Trailing per-row copy button: faint by default, brightening on hover; accent flash when just copied. */\n.jl__copy {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n background: transparent;\n border: none;\n color: var(--faint);\n cursor: pointer;\n transition: color 0.15s;\n}\n.jl__copy:hover {\n color: var(--accent);\n}\n.jl__copy--done {\n color: var(--accent);\n}\n.jl__caret {\n flex-shrink: 0;\n width: 9px;\n font-size: 9px;\n color: var(--faint);\n transition: transform 0.12s;\n transform: rotate(0deg);\n align-self: center;\n}\n.jl__caret--open {\n transform: rotate(90deg);\n}\n.jl__ln {\n flex-shrink: 0;\n min-width: 30px;\n text-align: right;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n.jl__badge {\n flex-shrink: 0;\n padding: 0 6px;\n border-radius: 5px;\n background: color-mix(in srgb, var(--danger) 16%, var(--surface));\n color: var(--danger);\n font-size: 9.5px;\n font-weight: 700;\n letter-spacing: 0.04em;\n align-self: center;\n}\n.jl__preview {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text);\n}\n.jl__rowhead--err .jl__preview {\n color: var(--muted);\n}\n\n.jl__full {\n margin: 0;\n padding: 8px 14px 12px 32px;\n overflow-x: auto;\n background: var(--code-bg);\n font-family: var(--font-mono);\n font-size: 12px;\n line-height: 1.55;\n white-space: pre;\n tab-size: 2;\n}\n.jl__full code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n\n.jl__more {\n display: block;\n width: 100%;\n padding: 10px 16px;\n font-family: inherit;\n font-size: 12px;\n color: var(--accent);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n.jl__more:hover {\n background: var(--surface-2);\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"JsonlViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/JsonlViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { parseJsonl, type JsonlRecord } from '@/lib/jsonl'\nimport { highlightCode } from '@/lib/highlight'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer JSON Lines (`.jsonl` / `.ndjson`) renderer (render mode). Receives the fetched text (the panel\n * loads it, like CsvViewer / SubtitleViewer), parses it into per-line records (see lib/jsonl.ts), and shows a\n * compact LIST — one truncated single-line preview per record. Clicking a row expands it to the pretty-printed,\n * syntax-highlighted value. Big files stay responsive two ways: each row's preview is a short single line, and\n * only an initial window of rows is in the DOM (a \"load more\" button grows it). The source toggle (header) shows\n * the raw file highlighted as JSON.\n */\nconst props = defineProps<{ text: string; name: string }>()\n\nconst PREVIEW_CHARS = 200 // truncate each row's inline preview so even a huge record is one short line\nconst INITIAL_ROWS = 200 // rows mounted initially; \"load more\" grows the window\nconst ROW_STEP = 400\n\nconst doc = computed(() => parseJsonl(props.text))\nconst shown = ref(INITIAL_ROWS)\nconst expanded = ref<Set<number>>(new Set())\n\n// A new file resets the window + any open rows.\nwatch(\n () => props.text,\n () => {\n shown.value = INITIAL_ROWS\n expanded.value = new Set()\n },\n)\n\nconst visible = computed(() => doc.value.records.slice(0, shown.value))\nconst remaining = computed(() => Math.max(0, doc.value.records.length - shown.value))\n\nfunction loadMore(): void {\n shown.value += ROW_STEP\n}\n\nfunction toggle(line: number): void {\n const s = new Set(expanded.value)\n if (s.has(line)) s.delete(line)\n else s.add(line)\n expanded.value = s\n}\n\n// Per-row copy: write the record's RAW line (the faithful on-disk text, not the re-stringified value) to the\n// clipboard, and flash a ✓ on that row. Mirrors the CodeBlock copy pattern (best-effort, cleared on a timer).\nconst copiedLine = ref<number | null>(null)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nasync function copyRow(r: JsonlRecord): Promise<void> {\n try {\n await navigator.clipboard?.writeText(r.raw)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copiedLine.value = r.line\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copiedLine.value = null), 1400)\n}\nonBeforeUnmount(() => clearTimeout(copyTimer))\n\n// Compact one-line preview: stringify the parsed value (whitespace collapsed) or, for an error row, the raw\n// line — then hard-truncate. Building it from the value normalizes spacing so the list reads cleanly.\nfunction preview(r: JsonlRecord): string {\n let s: string\n if (r.error) s = r.raw\n else {\n try {\n s = JSON.stringify(r.value) ?? String(r.value)\n } catch {\n s = r.raw\n }\n }\n s = s.replace(/\\s+/g, ' ').trim()\n return s.length > PREVIEW_CHARS ? s.slice(0, PREVIEW_CHARS) + '…' : s\n}\n\n// Pretty value, highlighted as JSON — built lazily, only for an expanded row.\nfunction expandedHtml(r: JsonlRecord): string {\n if (r.error) return highlightCode(r.raw, 'json')\n try {\n return highlightCode(JSON.stringify(r.value, null, 2) ?? '', 'json')\n } catch {\n return highlightCode(r.raw, 'json')\n }\n}\n</script>\n\n<template>\n <div class=\"jl\">\n <div class=\"jl__summary\">\n <span class=\"jl__count\">{{ doc.records.length }} 条记录</span>\n <span v-if=\"doc.errorCount\" class=\"jl__errtag\">{{ doc.errorCount }} 行无法解析</span>\n </div>\n\n <div class=\"jl__list\">\n <p v-if=\"!doc.records.length\" class=\"jl__msg\">(空文件,无 JSON 记录)</p>\n\n <div v-for=\"r in visible\" :key=\"r.line\" class=\"jl__rowwrap\">\n <div\n class=\"jl__rowhead\"\n :class=\"{ 'jl__rowhead--open': expanded.has(r.line), 'jl__rowhead--err': r.error }\"\n >\n <button\n type=\"button\"\n class=\"jl__row\"\n :title=\"r.error ? `第 ${r.line} 行:JSON 无效` : `第 ${r.line} 行 —— 点击展开`\"\n @click=\"toggle(r.line)\"\n >\n <span class=\"jl__caret\" :class=\"{ 'jl__caret--open': expanded.has(r.line) }\">▶</span>\n <span class=\"jl__ln\">{{ r.line }}</span>\n <span v-if=\"r.error\" class=\"jl__badge\">无效</span>\n <span class=\"jl__preview\">{{ preview(r) }}</span>\n </button>\n <button\n type=\"button\"\n class=\"jl__copy\"\n :class=\"{ 'jl__copy--done': copiedLine === r.line }\"\n :title=\"copiedLine === r.line ? '已复制' : '复制此行'\"\n @click=\"copyRow(r)\"\n >\n <Icon :name=\"copiedLine === r.line ? 'check' : 'copy'\" :size=\"13\" />\n </button>\n </div>\n <pre\n v-if=\"expanded.has(r.line)\"\n class=\"jl__full\"\n ><code class=\"hljs\" v-html=\"expandedHtml(r)\" /></pre>\n </div>\n\n <button v-if=\"remaining\" type=\"button\" class=\"jl__more\" @click=\"loadMore\">\n 显示更多(还有 {{ remaining }} 条)\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.jl {\n width: 100%;\n min-width: 0;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background: var(--surface);\n}\n\n.jl__summary {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 6px 12px;\n flex-wrap: wrap;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.jl__count {\n font-weight: 600;\n color: var(--accent);\n}\n.jl__errtag {\n padding: 1px 7px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--danger) 14%, var(--surface));\n color: var(--danger);\n font-size: 10.5px;\n}\n\n.jl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n\n.jl__list {\n width: 100%;\n min-width: 0;\n flex: 1 1 auto;\n min-height: 0;\n box-sizing: border-box;\n overflow-x: hidden;\n overflow-y: scroll; /* always reserve the gutter so the draggable bar is present, not wheel-only */\n overscroll-behavior: contain;\n padding: 4px 0;\n /* Always-visible, draggable scrollbar — the record list gets long and an auto-hidden overlay bar reads\n * as \"no scrollbar\". Thin styled bar in Firefox; a classic (gutter-reserving) bar in Chromium/WebKit. */\n scrollbar-width: thin;\n scrollbar-color: var(--border-2) transparent;\n}\n.jl__list::-webkit-scrollbar {\n width: 12px;\n}\n.jl__list::-webkit-scrollbar-thumb {\n background: var(--border-2);\n background-clip: padding-box;\n border: 3px solid transparent;\n border-radius: 999px;\n}\n.jl__list::-webkit-scrollbar-thumb:hover {\n background: var(--muted);\n background-clip: padding-box;\n border: 3px solid transparent;\n}\n.jl__rowwrap {\n width: 100%;\n min-width: 0;\n max-width: 100%;\n overflow: hidden;\n box-sizing: border-box;\n border-bottom: 1px solid var(--border);\n}\n\n/* Row header = the clickable preview button + a trailing copy button (a button can't nest inside a button,\n * so they're siblings in this flex row; the hover/open/error tint lives here, spanning both). */\n.jl__rowhead {\n min-width: 0;\n display: flex;\n align-items: stretch;\n}\n.jl__rowhead:hover,\n.jl__rowhead--open {\n background: var(--surface-2);\n}\n\n.jl__row {\n display: flex;\n align-items: baseline;\n gap: 9px;\n flex: 1 1 0;\n min-width: 0;\n padding: 6px 14px;\n background: transparent;\n border: none;\n font-family: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n/* Trailing per-row copy button: faint by default, brightening on hover; accent flash when just copied. */\n.jl__copy {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n background: transparent;\n border: none;\n color: var(--faint);\n cursor: pointer;\n transition: color 0.15s;\n}\n.jl__copy:hover {\n color: var(--accent);\n}\n.jl__copy--done {\n color: var(--accent);\n}\n.jl__caret {\n flex-shrink: 0;\n width: 9px;\n font-size: 9px;\n color: var(--faint);\n transition: transform 0.12s;\n transform: rotate(0deg);\n align-self: center;\n}\n.jl__caret--open {\n transform: rotate(90deg);\n}\n.jl__ln {\n flex-shrink: 0;\n min-width: 30px;\n text-align: right;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n.jl__badge {\n flex-shrink: 0;\n padding: 0 6px;\n border-radius: 5px;\n background: color-mix(in srgb, var(--danger) 16%, var(--surface));\n color: var(--danger);\n font-size: 9.5px;\n font-weight: 700;\n letter-spacing: 0.04em;\n align-self: center;\n}\n.jl__preview {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-family: var(--font-mono);\n font-size: 12.5px;\n color: var(--text);\n}\n.jl__rowhead--err .jl__preview {\n color: var(--muted);\n}\n\n.jl__full {\n width: 100%;\n min-width: 0;\n max-width: 100%;\n box-sizing: border-box;\n margin: 0;\n padding: 8px 14px 12px 32px;\n overflow: auto;\n background: var(--code-bg);\n font-family: var(--font-mono);\n font-size: 12px;\n line-height: 1.55;\n white-space: pre;\n tab-size: 2;\n}\n.jl__full code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n\n.jl__more {\n display: block;\n width: 100%;\n padding: 10px 16px;\n font-family: inherit;\n font-size: 12px;\n color: var(--accent);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n.jl__more:hover {\n background: var(--surface-2);\n}\n</style>\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import e from "../_virtual/_plugin-vue_export-helper.js";
2
+ import t from "./PdfNavigationPane.vue_vue_type_script_setup_true_lang.js";
3
+ /* empty css */
4
+ //#region src/components/PdfNavigationPane.vue
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-68918933"]]);
6
+ //#endregion
7
+ export { n as default };
8
+
9
+ //# sourceMappingURL=PdfNavigationPane.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PdfNavigationPane.js","names":[],"sources":["../../src/components/PdfNavigationPane.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport Icon from '@/components/icons/Icon.vue'\nimport type { PdfNavigationItem } from '../lib/pdfNavigation'\n\nconst props = defineProps<{\n outlineItems: readonly PdfNavigationItem[]\n pageCount: number\n pageLabels: readonly string[]\n pageThumbnails: readonly (string | null)[]\n activePage: number\n}>()\n\nconst collapsed = defineModel<boolean>('collapsed', { default: false })\nconst view = defineModel<'outline' | 'pages'>('view', { default: 'outline' })\nconst emit = defineEmits<{\n navigate: [pageNumber: number]\n}>()\n\nconst listEl = useTemplateRef<HTMLElement>('listEl')\nconst autoScrollPending = shallowRef(false)\nconst hasOutline = computed(() => props.outlineItems.length > 0)\nconst hasThumbnails = computed(() => props.pageThumbnails.some(Boolean))\nconst panelClasses = computed(() => ({\n 'pn--collapsed': collapsed.value,\n 'pn--outline': hasOutline.value && view.value === 'outline',\n 'pn--compact': view.value === 'pages' && !hasThumbnails.value,\n}))\n\nconst activeOutlineId = computed(() => {\n let active: PdfNavigationItem | undefined\n for (const item of props.outlineItems) {\n if (item.pageNumber === null || item.pageNumber > props.activePage) continue\n const activePageNumber = active?.pageNumber ?? 0\n if (\n !active ||\n item.pageNumber > activePageNumber ||\n (item.pageNumber === activePageNumber && item.level > active.level)\n )\n active = item\n }\n return active?.id ?? ''\n})\n\nfunction pageLabel(pageNumber: number): string {\n return props.pageLabels[pageNumber - 1] || `第 ${pageNumber} 页`\n}\n\nfunction navigate(pageNumber: number | null): void {\n if (pageNumber !== null) emit('navigate', pageNumber)\n}\n\nasync function keepActiveItemVisible(): Promise<void> {\n if (collapsed.value || autoScrollPending.value) return\n autoScrollPending.value = true\n await nextTick()\n listEl.value\n ?.querySelector<HTMLElement>('.pn__item--active')\n ?.scrollIntoView({ block: 'nearest' })\n autoScrollPending.value = false\n}\n\nwatch(\n () => props.outlineItems.length,\n (count) => {\n if (!count && view.value === 'outline') view.value = 'pages'\n },\n { immediate: true },\n)\nwatch(() => [props.activePage, view.value, collapsed.value], keepActiveItemVisible)\n</script>\n\n<template>\n <aside class=\"pn\" :class=\"panelClasses\" aria-label=\"PDF 导航\">\n <button\n v-if=\"collapsed\"\n type=\"button\"\n class=\"pn__reopen\"\n aria-label=\"展开 PDF 导航\"\n title=\"展开 PDF 导航\"\n @click=\"collapsed = false\"\n >\n <Icon name=\"chevronRight\" :size=\"15\" />\n </button>\n\n <template v-else>\n <header class=\"pn__header\">\n <div v-if=\"hasOutline\" class=\"pn__tabs\" role=\"tablist\" aria-label=\"导航类型\">\n <button\n class=\"pn__tab\"\n :class=\"{ 'pn__tab--active': view === 'outline' }\"\n type=\"button\"\n role=\"tab\"\n :aria-selected=\"view === 'outline'\"\n @click=\"view = 'outline'\"\n >\n 目录\n </button>\n <button\n class=\"pn__tab\"\n :class=\"{ 'pn__tab--active': view === 'pages' }\"\n type=\"button\"\n role=\"tab\"\n :aria-selected=\"view === 'pages'\"\n @click=\"view = 'pages'\"\n >\n 页面\n </button>\n </div>\n <div v-else class=\"pn__meta\">\n <strong class=\"pn__title\">页面</strong>\n <span class=\"pn__count\">{{ pageCount }} 页</span>\n </div>\n <button\n class=\"pn__collapse\"\n type=\"button\"\n title=\"收起 PDF 导航\"\n aria-label=\"收起 PDF 导航\"\n @click=\"collapsed = true\"\n >\n <Icon name=\"chevronLeft\" :size=\"15\" />\n </button>\n </header>\n\n <nav ref=\"listEl\" class=\"pn__list\" aria-label=\"PDF 导航列表\">\n <template v-if=\"view === 'outline' && hasOutline\">\n <button\n v-for=\"item in outlineItems\"\n :key=\"item.id\"\n class=\"pn__item pn__outline\"\n :class=\"{\n 'pn__item--active': item.id === activeOutlineId,\n 'pn__item--disabled': item.pageNumber === null,\n }\"\n :style=\"{ '--pn-level': item.level }\"\n type=\"button\"\n :disabled=\"item.pageNumber === null\"\n :title=\"item.title\"\n :aria-current=\"item.id === activeOutlineId ? 'location' : undefined\"\n @click=\"navigate(item.pageNumber)\"\n >\n <span class=\"pn__outline-title\">{{ item.title }}</span>\n <span v-if=\"item.pageNumber !== null\" class=\"pn__outline-page\">{{\n item.pageNumber\n }}</span>\n </button>\n </template>\n\n <template v-else>\n <button\n v-for=\"pageNumber in pageCount\"\n :key=\"pageNumber\"\n class=\"pn__item pn__page\"\n :class=\"{\n 'pn__item--active': pageNumber === activePage,\n 'pn__page--fallback': !pageThumbnails[pageNumber - 1],\n }\"\n type=\"button\"\n :data-nav-page-number=\"pageNumber\"\n :aria-current=\"pageNumber === activePage ? 'page' : undefined\"\n :title=\"`跳转到第 ${pageNumber} 页`\"\n @click=\"navigate(pageNumber)\"\n >\n <template v-if=\"pageThumbnails[pageNumber - 1]\">\n <span class=\"pn__preview-wrap\">\n <img\n class=\"pn__preview\"\n :src=\"pageThumbnails[pageNumber - 1] || ''\"\n alt=\"\"\n loading=\"lazy\"\n draggable=\"false\"\n />\n </span>\n <span class=\"pn__number\">{{ pageNumber }}</span>\n </template>\n <span v-else class=\"pn__sheet\">{{ pageNumber }}</span>\n <span class=\"pn__page-label\">{{ pageLabel(pageNumber) }}</span>\n </button>\n </template>\n </nav>\n </template>\n </aside>\n</template>\n\n<style scoped>\n.pn {\n position: relative;\n width: 142px;\n min-width: 0;\n display: flex;\n flex: 0 0 142px;\n flex-direction: column;\n color: var(--muted);\n background: color-mix(in srgb, var(--surface) 94%, var(--surface-2));\n border-right: 1px solid var(--border);\n transition:\n flex-basis 160ms ease,\n width 160ms ease;\n}\n\n.pn--outline {\n width: 190px;\n flex-basis: 190px;\n}\n\n.pn--compact {\n width: 96px;\n flex-basis: 96px;\n}\n\n.pn--collapsed {\n width: 0;\n flex-basis: 0;\n border-right: 0;\n}\n\n.pn__header {\n min-height: 45px;\n padding: 7px 7px 7px 10px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 6px;\n border-bottom: 1px solid var(--border);\n}\n\n.pn__tabs {\n min-width: 0;\n display: flex;\n flex: 1;\n gap: 3px;\n}\n\n.pn__tab,\n.pn__collapse,\n.pn__reopen,\n.pn__item {\n color: inherit;\n font: inherit;\n cursor: pointer;\n background: transparent;\n border: 0;\n}\n\n.pn__tab {\n position: relative;\n min-width: 0;\n padding: 7px 5px;\n flex: 1;\n color: var(--faint);\n font-size: 11.5px;\n border-radius: 6px;\n}\n\n.pn__tab:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n\n.pn__tab--active {\n color: var(--text-strong);\n background: var(--accent-soft);\n}\n\n.pn__tab--active::after {\n position: absolute;\n right: 10px;\n bottom: 2px;\n left: 10px;\n height: 2px;\n content: '';\n background: var(--accent);\n border-radius: 2px;\n}\n\n.pn__meta {\n min-width: 0;\n}\n\n.pn__title {\n display: block;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 650;\n}\n\n.pn__count {\n display: block;\n margin-top: 1px;\n color: var(--faint);\n font-size: 10.5px;\n}\n\n.pn__collapse {\n width: 27px;\n height: 27px;\n display: grid;\n flex: none;\n place-items: center;\n border-radius: 7px;\n}\n\n.pn__collapse:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n\n.pn__reopen {\n position: absolute;\n z-index: 3;\n top: 12px;\n right: -28px;\n width: 28px;\n height: 36px;\n padding: 0;\n display: grid;\n place-items: center;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 0 9px 9px 0;\n box-shadow: 2px 1px 6px rgba(0, 0, 0, 0.12);\n}\n\n.pn__reopen:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n\n.pn__list {\n min-height: 0;\n padding: 8px 7px 22px;\n flex: 1 1 auto;\n overflow: auto;\n scrollbar-width: thin;\n}\n\n.pn__item {\n width: 100%;\n min-width: 0;\n color: var(--muted);\n text-align: left;\n border: 1px solid transparent;\n border-radius: 8px;\n}\n\n.pn__item:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n\n.pn__item--active {\n color: var(--text-strong);\n background: var(--accent-soft);\n border-color: color-mix(in srgb, var(--accent) 70%, transparent);\n}\n\n.pn__item--disabled {\n cursor: default;\n opacity: 0.52;\n}\n\n.pn__item--disabled:hover {\n color: var(--muted);\n background: transparent;\n}\n\n.pn__outline {\n gap: 8px;\n padding: 7px 8px 7px calc(9px + min(var(--pn-level), 5) * 14px);\n display: flex;\n align-items: center;\n margin: 1px 0;\n font-size: 12px;\n line-height: 1.35;\n}\n\n.pn__outline-title {\n min-width: 0;\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.pn__outline-page {\n color: var(--faint);\n font-size: 10px;\n font-variant-numeric: tabular-nums;\n}\n\n.pn__page {\n position: relative;\n padding: 5px;\n display: block;\n margin: 0 0 8px;\n}\n\n.pn__preview-wrap {\n min-width: 0;\n min-height: 62px;\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n background: #fff;\n border: 1px solid var(--border-2);\n border-radius: 4px;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);\n}\n\n.pn__item--active .pn__preview-wrap {\n border-color: var(--accent);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent);\n}\n\n.pn__preview {\n width: 104px;\n max-width: 100%;\n max-height: 142px;\n display: block;\n object-fit: contain;\n pointer-events: none;\n}\n\n.pn__number {\n position: absolute;\n z-index: 2;\n top: 9px;\n left: 9px;\n min-width: 20px;\n height: 20px;\n padding: 0 5px;\n display: grid;\n place-items: center;\n color: #fff;\n font-size: 9.5px;\n font-weight: 650;\n font-variant-numeric: tabular-nums;\n background: rgba(20, 20, 20, 0.68);\n border-radius: 5px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n backdrop-filter: blur(4px);\n}\n\n.pn__item--active .pn__number {\n background: var(--accent);\n}\n\n.pn__page-label {\n min-width: 0;\n margin-top: 5px;\n overflow: hidden;\n display: -webkit-box;\n color: inherit;\n font-size: 10.5px;\n line-height: 1.35;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n}\n\n.pn__page--fallback {\n padding: 5px;\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 5px;\n}\n\n.pn__sheet {\n width: 28px;\n height: 38px;\n display: grid;\n flex: none;\n place-items: center;\n color: #526072;\n font-size: 9.5px;\n font-variant-numeric: tabular-nums;\n background: #fff;\n border: 1px solid var(--border-2);\n border-radius: 3px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);\n}\n\n.pn__item--active .pn__sheet {\n color: var(--accent);\n border-color: var(--accent);\n}\n\n.pn__page--fallback .pn__page-label {\n margin-top: 0;\n flex: 1;\n}\n\n@container (max-width: 500px) {\n .pn {\n width: 124px;\n flex-basis: 124px;\n }\n\n .pn--outline {\n width: 158px;\n flex-basis: 158px;\n }\n\n .pn--compact {\n width: 84px;\n flex-basis: 84px;\n }\n\n .pn--collapsed {\n width: 0;\n flex-basis: 0;\n }\n\n .pn__preview {\n max-height: 126px;\n }\n}\n</style>\n"],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import type { PdfNavigationItem } from '../lib/pdfNavigation';
2
+ type __VLS_Props = {
3
+ outlineItems: readonly PdfNavigationItem[];
4
+ pageCount: number;
5
+ pageLabels: readonly string[];
6
+ pageThumbnails: readonly (string | null)[];
7
+ activePage: number;
8
+ };
9
+ type __VLS_ModelProps = {
10
+ 'collapsed'?: boolean;
11
+ 'view'?: 'outline' | 'pages';
12
+ };
13
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
14
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
+ navigate: (pageNumber: number) => any;
16
+ "update:collapsed": (value: boolean) => any;
17
+ "update:view": (value: "outline" | "pages") => any;
18
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
19
+ onNavigate?: ((pageNumber: number) => any) | undefined;
20
+ "onUpdate:collapsed"?: ((value: boolean) => any) | undefined;
21
+ "onUpdate:view"?: ((value: "outline" | "pages") => any) | undefined;
22
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
+ declare const _default: typeof __VLS_export;
24
+ export default _default;
@@ -0,0 +1,149 @@
1
+ import e from "./icons/Icon.js";
2
+ import { Fragment as t, computed as n, createCommentVNode as r, createElementBlock as i, createElementVNode as a, createVNode as o, defineComponent as s, mergeModels as c, nextTick as l, normalizeClass as u, normalizeStyle as d, openBlock as f, renderList as p, shallowRef as m, toDisplayString as h, useModel as g, useTemplateRef as _, watch as v } from "vue";
3
+ //#region src/components/PdfNavigationPane.vue?vue&type=script&setup=true&lang.ts
4
+ var y = { class: "pn__header" }, b = {
5
+ key: 0,
6
+ class: "pn__tabs",
7
+ role: "tablist",
8
+ "aria-label": "导航类型"
9
+ }, x = ["aria-selected"], S = ["aria-selected"], C = {
10
+ key: 1,
11
+ class: "pn__meta"
12
+ }, w = { class: "pn__count" }, T = [
13
+ "disabled",
14
+ "title",
15
+ "aria-current",
16
+ "onClick"
17
+ ], E = { class: "pn__outline-title" }, D = {
18
+ key: 0,
19
+ class: "pn__outline-page"
20
+ }, O = [
21
+ "data-nav-page-number",
22
+ "aria-current",
23
+ "title",
24
+ "onClick"
25
+ ], k = { class: "pn__preview-wrap" }, A = ["src"], j = { class: "pn__number" }, M = {
26
+ key: 1,
27
+ class: "pn__sheet"
28
+ }, N = { class: "pn__page-label" }, P = /*@__PURE__*/ s({
29
+ __name: "PdfNavigationPane",
30
+ props: /*@__PURE__*/ c({
31
+ outlineItems: {},
32
+ pageCount: {},
33
+ pageLabels: {},
34
+ pageThumbnails: {},
35
+ activePage: {}
36
+ }, {
37
+ collapsed: {
38
+ type: Boolean,
39
+ default: !1
40
+ },
41
+ collapsedModifiers: {},
42
+ view: { default: "outline" },
43
+ viewModifiers: {}
44
+ }),
45
+ emits: /*@__PURE__*/ c(["navigate"], ["update:collapsed", "update:view"]),
46
+ setup(s, { emit: c }) {
47
+ let P = s, F = g(s, "collapsed"), I = g(s, "view"), L = c, R = _("listEl"), z = m(!1), B = n(() => P.outlineItems.length > 0), V = n(() => P.pageThumbnails.some(Boolean)), H = n(() => ({
48
+ "pn--collapsed": F.value,
49
+ "pn--outline": B.value && I.value === "outline",
50
+ "pn--compact": I.value === "pages" && !V.value
51
+ })), U = n(() => {
52
+ let e;
53
+ for (let t of P.outlineItems) {
54
+ if (t.pageNumber === null || t.pageNumber > P.activePage) continue;
55
+ let n = e?.pageNumber ?? 0;
56
+ (!e || t.pageNumber > n || t.pageNumber === n && t.level > e.level) && (e = t);
57
+ }
58
+ return e?.id ?? "";
59
+ });
60
+ function W(e) {
61
+ return P.pageLabels[e - 1] || `第 ${e} 页`;
62
+ }
63
+ function G(e) {
64
+ e !== null && L("navigate", e);
65
+ }
66
+ async function K() {
67
+ F.value || z.value || (z.value = !0, await l(), R.value?.querySelector(".pn__item--active")?.scrollIntoView({ block: "nearest" }), z.value = !1);
68
+ }
69
+ return v(() => P.outlineItems.length, (e) => {
70
+ !e && I.value === "outline" && (I.value = "pages");
71
+ }, { immediate: !0 }), v(() => [
72
+ P.activePage,
73
+ I.value,
74
+ F.value
75
+ ], K), (n, c) => (f(), i("aside", {
76
+ class: u(["pn", H.value]),
77
+ "aria-label": "PDF 导航"
78
+ }, [F.value ? (f(), i("button", {
79
+ key: 0,
80
+ type: "button",
81
+ class: "pn__reopen",
82
+ "aria-label": "展开 PDF 导航",
83
+ title: "展开 PDF 导航",
84
+ onClick: c[0] ||= (e) => F.value = !1
85
+ }, [o(e, {
86
+ name: "chevronRight",
87
+ size: 15
88
+ })])) : (f(), i(t, { key: 1 }, [a("header", y, [B.value ? (f(), i("div", b, [a("button", {
89
+ class: u(["pn__tab", { "pn__tab--active": I.value === "outline" }]),
90
+ type: "button",
91
+ role: "tab",
92
+ "aria-selected": I.value === "outline",
93
+ onClick: c[1] ||= (e) => I.value = "outline"
94
+ }, " 目录 ", 10, x), a("button", {
95
+ class: u(["pn__tab", { "pn__tab--active": I.value === "pages" }]),
96
+ type: "button",
97
+ role: "tab",
98
+ "aria-selected": I.value === "pages",
99
+ onClick: c[2] ||= (e) => I.value = "pages"
100
+ }, " 页面 ", 10, S)])) : (f(), i("div", C, [c[4] ||= a("strong", { class: "pn__title" }, "页面", -1), a("span", w, h(s.pageCount) + " 页", 1)])), a("button", {
101
+ class: "pn__collapse",
102
+ type: "button",
103
+ title: "收起 PDF 导航",
104
+ "aria-label": "收起 PDF 导航",
105
+ onClick: c[3] ||= (e) => F.value = !0
106
+ }, [o(e, {
107
+ name: "chevronLeft",
108
+ size: 15
109
+ })])]), a("nav", {
110
+ ref_key: "listEl",
111
+ ref: R,
112
+ class: "pn__list",
113
+ "aria-label": "PDF 导航列表"
114
+ }, [I.value === "outline" && B.value ? (f(!0), i(t, { key: 0 }, p(s.outlineItems, (e) => (f(), i("button", {
115
+ key: e.id,
116
+ class: u(["pn__item pn__outline", {
117
+ "pn__item--active": e.id === U.value,
118
+ "pn__item--disabled": e.pageNumber === null
119
+ }]),
120
+ style: d({ "--pn-level": e.level }),
121
+ type: "button",
122
+ disabled: e.pageNumber === null,
123
+ title: e.title,
124
+ "aria-current": e.id === U.value ? "location" : void 0,
125
+ onClick: (t) => G(e.pageNumber)
126
+ }, [a("span", E, h(e.title), 1), e.pageNumber === null ? r("", !0) : (f(), i("span", D, h(e.pageNumber), 1))], 14, T))), 128)) : (f(!0), i(t, { key: 1 }, p(s.pageCount, (e) => (f(), i("button", {
127
+ key: e,
128
+ class: u(["pn__item pn__page", {
129
+ "pn__item--active": e === s.activePage,
130
+ "pn__page--fallback": !s.pageThumbnails[e - 1]
131
+ }]),
132
+ type: "button",
133
+ "data-nav-page-number": e,
134
+ "aria-current": e === s.activePage ? "page" : void 0,
135
+ title: `跳转到第 ${e} 页`,
136
+ onClick: (t) => G(e)
137
+ }, [s.pageThumbnails[e - 1] ? (f(), i(t, { key: 0 }, [a("span", k, [a("img", {
138
+ class: "pn__preview",
139
+ src: s.pageThumbnails[e - 1] || "",
140
+ alt: "",
141
+ loading: "lazy",
142
+ draggable: "false"
143
+ }, null, 8, A)]), a("span", j, h(e), 1)], 64)) : (f(), i("span", M, h(e), 1)), a("span", N, h(W(e)), 1)], 10, O))), 128))], 512)], 64))], 2));
144
+ }
145
+ });
146
+ //#endregion
147
+ export { P as default };
148
+
149
+ //# sourceMappingURL=PdfNavigationPane.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PdfNavigationPane.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/PdfNavigationPane.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport Icon from '@/components/icons/Icon.vue'\nimport type { PdfNavigationItem } from '../lib/pdfNavigation'\n\nconst props = defineProps<{\n outlineItems: readonly PdfNavigationItem[]\n pageCount: number\n pageLabels: readonly string[]\n pageThumbnails: readonly (string | null)[]\n activePage: number\n}>()\n\nconst collapsed = defineModel<boolean>('collapsed', { default: false })\nconst view = defineModel<'outline' | 'pages'>('view', { default: 'outline' })\nconst emit = defineEmits<{\n navigate: [pageNumber: number]\n}>()\n\nconst listEl = useTemplateRef<HTMLElement>('listEl')\nconst autoScrollPending = shallowRef(false)\nconst hasOutline = computed(() => props.outlineItems.length > 0)\nconst hasThumbnails = computed(() => props.pageThumbnails.some(Boolean))\nconst panelClasses = computed(() => ({\n 'pn--collapsed': collapsed.value,\n 'pn--outline': hasOutline.value && view.value === 'outline',\n 'pn--compact': view.value === 'pages' && !hasThumbnails.value,\n}))\n\nconst activeOutlineId = computed(() => {\n let active: PdfNavigationItem | undefined\n for (const item of props.outlineItems) {\n if (item.pageNumber === null || item.pageNumber > props.activePage) continue\n const activePageNumber = active?.pageNumber ?? 0\n if (\n !active ||\n item.pageNumber > activePageNumber ||\n (item.pageNumber === activePageNumber && item.level > active.level)\n )\n active = item\n }\n return active?.id ?? ''\n})\n\nfunction pageLabel(pageNumber: number): string {\n return props.pageLabels[pageNumber - 1] || `第 ${pageNumber} 页`\n}\n\nfunction navigate(pageNumber: number | null): void {\n if (pageNumber !== null) emit('navigate', pageNumber)\n}\n\nasync function keepActiveItemVisible(): Promise<void> {\n if (collapsed.value || autoScrollPending.value) return\n autoScrollPending.value = true\n await nextTick()\n listEl.value\n ?.querySelector<HTMLElement>('.pn__item--active')\n ?.scrollIntoView({ block: 'nearest' })\n autoScrollPending.value = false\n}\n\nwatch(\n () => props.outlineItems.length,\n (count) => {\n if (!count && view.value === 'outline') view.value = 'pages'\n },\n { immediate: true },\n)\nwatch(() => [props.activePage, view.value, collapsed.value], keepActiveItemVisible)\n</script>\n\n<template>\n <aside class=\"pn\" :class=\"panelClasses\" aria-label=\"PDF 导航\">\n <button\n v-if=\"collapsed\"\n type=\"button\"\n class=\"pn__reopen\"\n aria-label=\"展开 PDF 导航\"\n title=\"展开 PDF 导航\"\n @click=\"collapsed = false\"\n >\n <Icon name=\"chevronRight\" :size=\"15\" />\n </button>\n\n <template v-else>\n <header class=\"pn__header\">\n <div v-if=\"hasOutline\" class=\"pn__tabs\" role=\"tablist\" aria-label=\"导航类型\">\n <button\n class=\"pn__tab\"\n :class=\"{ 'pn__tab--active': view === 'outline' }\"\n type=\"button\"\n role=\"tab\"\n :aria-selected=\"view === 'outline'\"\n @click=\"view = 'outline'\"\n >\n 目录\n </button>\n <button\n class=\"pn__tab\"\n :class=\"{ 'pn__tab--active': view === 'pages' }\"\n type=\"button\"\n role=\"tab\"\n :aria-selected=\"view === 'pages'\"\n @click=\"view = 'pages'\"\n >\n 页面\n </button>\n </div>\n <div v-else class=\"pn__meta\">\n <strong class=\"pn__title\">页面</strong>\n <span class=\"pn__count\">{{ pageCount }} 页</span>\n </div>\n <button\n class=\"pn__collapse\"\n type=\"button\"\n title=\"收起 PDF 导航\"\n aria-label=\"收起 PDF 导航\"\n @click=\"collapsed = true\"\n >\n <Icon name=\"chevronLeft\" :size=\"15\" />\n </button>\n </header>\n\n <nav ref=\"listEl\" class=\"pn__list\" aria-label=\"PDF 导航列表\">\n <template v-if=\"view === 'outline' && hasOutline\">\n <button\n v-for=\"item in outlineItems\"\n :key=\"item.id\"\n class=\"pn__item pn__outline\"\n :class=\"{\n 'pn__item--active': item.id === activeOutlineId,\n 'pn__item--disabled': item.pageNumber === null,\n }\"\n :style=\"{ '--pn-level': item.level }\"\n type=\"button\"\n :disabled=\"item.pageNumber === null\"\n :title=\"item.title\"\n :aria-current=\"item.id === activeOutlineId ? 'location' : undefined\"\n @click=\"navigate(item.pageNumber)\"\n >\n <span class=\"pn__outline-title\">{{ item.title }}</span>\n <span v-if=\"item.pageNumber !== null\" class=\"pn__outline-page\">{{\n item.pageNumber\n }}</span>\n </button>\n </template>\n\n <template v-else>\n <button\n v-for=\"pageNumber in pageCount\"\n :key=\"pageNumber\"\n class=\"pn__item pn__page\"\n :class=\"{\n 'pn__item--active': pageNumber === activePage,\n 'pn__page--fallback': !pageThumbnails[pageNumber - 1],\n }\"\n type=\"button\"\n :data-nav-page-number=\"pageNumber\"\n :aria-current=\"pageNumber === activePage ? 'page' : undefined\"\n :title=\"`跳转到第 ${pageNumber} 页`\"\n @click=\"navigate(pageNumber)\"\n >\n <template v-if=\"pageThumbnails[pageNumber - 1]\">\n <span class=\"pn__preview-wrap\">\n <img\n class=\"pn__preview\"\n :src=\"pageThumbnails[pageNumber - 1] || ''\"\n alt=\"\"\n loading=\"lazy\"\n draggable=\"false\"\n />\n </span>\n <span class=\"pn__number\">{{ pageNumber }}</span>\n </template>\n <span v-else class=\"pn__sheet\">{{ pageNumber }}</span>\n <span class=\"pn__page-label\">{{ pageLabel(pageNumber) }}</span>\n </button>\n </template>\n </nav>\n </template>\n </aside>\n</template>\n\n<style scoped>\n.pn {\n position: relative;\n width: 142px;\n min-width: 0;\n display: flex;\n flex: 0 0 142px;\n flex-direction: column;\n color: var(--muted);\n background: color-mix(in srgb, var(--surface) 94%, var(--surface-2));\n border-right: 1px solid var(--border);\n transition:\n flex-basis 160ms ease,\n width 160ms ease;\n}\n\n.pn--outline {\n width: 190px;\n flex-basis: 190px;\n}\n\n.pn--compact {\n width: 96px;\n flex-basis: 96px;\n}\n\n.pn--collapsed {\n width: 0;\n flex-basis: 0;\n border-right: 0;\n}\n\n.pn__header {\n min-height: 45px;\n padding: 7px 7px 7px 10px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 6px;\n border-bottom: 1px solid var(--border);\n}\n\n.pn__tabs {\n min-width: 0;\n display: flex;\n flex: 1;\n gap: 3px;\n}\n\n.pn__tab,\n.pn__collapse,\n.pn__reopen,\n.pn__item {\n color: inherit;\n font: inherit;\n cursor: pointer;\n background: transparent;\n border: 0;\n}\n\n.pn__tab {\n position: relative;\n min-width: 0;\n padding: 7px 5px;\n flex: 1;\n color: var(--faint);\n font-size: 11.5px;\n border-radius: 6px;\n}\n\n.pn__tab:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n\n.pn__tab--active {\n color: var(--text-strong);\n background: var(--accent-soft);\n}\n\n.pn__tab--active::after {\n position: absolute;\n right: 10px;\n bottom: 2px;\n left: 10px;\n height: 2px;\n content: '';\n background: var(--accent);\n border-radius: 2px;\n}\n\n.pn__meta {\n min-width: 0;\n}\n\n.pn__title {\n display: block;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 650;\n}\n\n.pn__count {\n display: block;\n margin-top: 1px;\n color: var(--faint);\n font-size: 10.5px;\n}\n\n.pn__collapse {\n width: 27px;\n height: 27px;\n display: grid;\n flex: none;\n place-items: center;\n border-radius: 7px;\n}\n\n.pn__collapse:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n\n.pn__reopen {\n position: absolute;\n z-index: 3;\n top: 12px;\n right: -28px;\n width: 28px;\n height: 36px;\n padding: 0;\n display: grid;\n place-items: center;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 0 9px 9px 0;\n box-shadow: 2px 1px 6px rgba(0, 0, 0, 0.12);\n}\n\n.pn__reopen:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n\n.pn__list {\n min-height: 0;\n padding: 8px 7px 22px;\n flex: 1 1 auto;\n overflow: auto;\n scrollbar-width: thin;\n}\n\n.pn__item {\n width: 100%;\n min-width: 0;\n color: var(--muted);\n text-align: left;\n border: 1px solid transparent;\n border-radius: 8px;\n}\n\n.pn__item:hover {\n color: var(--text-strong);\n background: var(--bubble);\n}\n\n.pn__item--active {\n color: var(--text-strong);\n background: var(--accent-soft);\n border-color: color-mix(in srgb, var(--accent) 70%, transparent);\n}\n\n.pn__item--disabled {\n cursor: default;\n opacity: 0.52;\n}\n\n.pn__item--disabled:hover {\n color: var(--muted);\n background: transparent;\n}\n\n.pn__outline {\n gap: 8px;\n padding: 7px 8px 7px calc(9px + min(var(--pn-level), 5) * 14px);\n display: flex;\n align-items: center;\n margin: 1px 0;\n font-size: 12px;\n line-height: 1.35;\n}\n\n.pn__outline-title {\n min-width: 0;\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.pn__outline-page {\n color: var(--faint);\n font-size: 10px;\n font-variant-numeric: tabular-nums;\n}\n\n.pn__page {\n position: relative;\n padding: 5px;\n display: block;\n margin: 0 0 8px;\n}\n\n.pn__preview-wrap {\n min-width: 0;\n min-height: 62px;\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n background: #fff;\n border: 1px solid var(--border-2);\n border-radius: 4px;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);\n}\n\n.pn__item--active .pn__preview-wrap {\n border-color: var(--accent);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent);\n}\n\n.pn__preview {\n width: 104px;\n max-width: 100%;\n max-height: 142px;\n display: block;\n object-fit: contain;\n pointer-events: none;\n}\n\n.pn__number {\n position: absolute;\n z-index: 2;\n top: 9px;\n left: 9px;\n min-width: 20px;\n height: 20px;\n padding: 0 5px;\n display: grid;\n place-items: center;\n color: #fff;\n font-size: 9.5px;\n font-weight: 650;\n font-variant-numeric: tabular-nums;\n background: rgba(20, 20, 20, 0.68);\n border-radius: 5px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n backdrop-filter: blur(4px);\n}\n\n.pn__item--active .pn__number {\n background: var(--accent);\n}\n\n.pn__page-label {\n min-width: 0;\n margin-top: 5px;\n overflow: hidden;\n display: -webkit-box;\n color: inherit;\n font-size: 10.5px;\n line-height: 1.35;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n}\n\n.pn__page--fallback {\n padding: 5px;\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 5px;\n}\n\n.pn__sheet {\n width: 28px;\n height: 38px;\n display: grid;\n flex: none;\n place-items: center;\n color: #526072;\n font-size: 9.5px;\n font-variant-numeric: tabular-nums;\n background: #fff;\n border: 1px solid var(--border-2);\n border-radius: 3px;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);\n}\n\n.pn__item--active .pn__sheet {\n color: var(--accent);\n border-color: var(--accent);\n}\n\n.pn__page--fallback .pn__page-label {\n margin-top: 0;\n flex: 1;\n}\n\n@container (max-width: 500px) {\n .pn {\n width: 124px;\n flex-basis: 124px;\n }\n\n .pn--outline {\n width: 158px;\n flex-basis: 158px;\n }\n\n .pn--compact {\n width: 84px;\n flex-basis: 84px;\n }\n\n .pn--collapsed {\n width: 0;\n flex-basis: 0;\n }\n\n .pn__preview {\n max-height: 126px;\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAMA,IAAM,IAAQ,GAQR,IAAY,EAAoB,GAAC,WAA+B,GAChE,IAAO,EAAgC,GAAC,MAA8B,GACtE,IAAO,GAIP,IAAS,EAA4B,QAAQ,GAC7C,IAAoB,EAAW,EAAK,GACpC,IAAa,QAAe,EAAM,aAAa,SAAS,CAAC,GACzD,IAAgB,QAAe,EAAM,eAAe,KAAK,OAAO,CAAC,GACjE,IAAe,SAAgB;GACnC,iBAAiB,EAAU;GAC3B,eAAe,EAAW,SAAS,EAAK,UAAU;GAClD,eAAe,EAAK,UAAU,WAAW,CAAC,EAAc;EAC1D,EAAE,GAEI,IAAkB,QAAe;GACrC,IAAI;GACJ,KAAK,IAAM,KAAQ,EAAM,cAAc;IACrC,IAAI,EAAK,eAAe,QAAQ,EAAK,aAAa,EAAM,YAAY;IACpE,IAAM,IAAmB,GAAQ,cAAc;IAC/C,CACE,CAAC,KACD,EAAK,aAAa,KACjB,EAAK,eAAe,KAAoB,EAAK,QAAQ,EAAO,WAE7D,IAAS;GACb;GACA,OAAO,GAAQ,MAAM;EACvB,CAAC;EAED,SAAS,EAAU,GAA4B;GAC7C,OAAO,EAAM,WAAW,IAAa,MAAM,KAAK,EAAW;EAC7D;EAEA,SAAS,EAAS,GAAiC;GACjD,AAAI,MAAe,QAAM,EAAK,YAAY,CAAU;EACtD;EAEA,eAAe,IAAuC;GAChD,EAAU,SAAS,EAAkB,UACzC,EAAkB,QAAQ,IAC1B,MAAM,EAAS,GACf,EAAO,OACH,cAA2B,mBAAmB,CAAA,EAC9C,eAAe,EAAE,OAAO,UAAU,CAAC,GACvC,EAAkB,QAAQ;EAC5B;SAEA,QACQ,EAAM,aAAa,SACxB,MAAU;GACT,AAAI,CAAC,KAAS,EAAK,UAAU,cAAW,EAAK,QAAQ;EACvD,GACA,EAAE,WAAW,GAAK,CACpB,GACA,QAAY;GAAC,EAAM;GAAY,EAAK;GAAO,EAAU;EAAK,GAAG,CAAqB,mBAIhF,EA4GQ,SAAA;GA5GD,OAAK,EAAA,CAAC,MAAa,EAAA,KAAY,CAAA;GAAE,cAAW;MAEzC,EAAA,SAAA,EAAA,GADR,EASS,UAAA;;GAPP,MAAK;GACL,OAAM;GACN,cAAW;GACX,OAAM;GACL,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,QAAS;MAEjB,EAAuC,GAAA;GAAjC,MAAK;GAAgB,MAAM;gBAGnC,EA+FW,GAAA,EAAA,KAAA,EAAA,GAAA,CA9FT,EAoCS,UApCT,GAoCS,CAnCI,EAAA,SAAA,EAAA,GAAX,EAqBM,OArBN,GAqBM,CApBJ,EASS,UAAA;GARP,OAAK,EAAA,CAAC,WAAS,EAAA,mBACc,EAAA,UAAI,UAAA,CAAA,CAAA;GACjC,MAAK;GACL,MAAK;GACJ,iBAAe,EAAA,UAAI;GACnB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,QAAI;KACb,QAED,IAAA,CAAA,GACA,EASS,UAAA;GARP,OAAK,EAAA,CAAC,WAAS,EAAA,mBACc,EAAA,UAAI,QAAA,CAAA,CAAA;GACjC,MAAK;GACL,MAAK;GACJ,iBAAe,EAAA,UAAI;GACnB,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,QAAI;KACb,QAED,IAAA,CAAA,CAAA,CAAA,MAAA,EAAA,GAEF,EAGM,OAHN,GAGM,CAAA,AAAA,EAAA,OAFJ,EAAqC,UAAA,EAA7B,OAAM,YAAW,GAAC,MAAE,EAAA,GAC5B,EAAgD,QAAhD,GAAgD,EAArB,EAAA,SAAS,IAAG,MAAE,CAAA,CAAA,CAAA,IAE3C,EAQS,UAAA;GAPP,OAAM;GACN,MAAK;GACL,OAAM;GACN,cAAW;GACV,SAAK,AAAA,EAAA,QAAA,MAAE,EAAA,QAAS;MAEjB,EAAsC,GAAA;GAAhC,MAAK;GAAe,MAAM;UAIpC,EAuDM,OAAA;YAvDG;GAAJ,KAAI;GAAS,OAAM;GAAW,cAAW;MAC5B,EAAA,UAAI,aAAkB,EAAA,SAAA,EAAA,EAAA,GACpC,EAmBS,GAAA,EAAA,KAAA,EAAA,GAAA,EAlBQ,EAAA,eAAR,YADT,EAmBS,UAAA;GAjBN,KAAK,EAAK;GACX,OAAK,EAAA,CAAC,wBAAsB;wBACgB,EAAK,OAAO,EAAA;0BAAqD,EAAK,eAAU;;GAI3H,OAAK,EAAA,EAAA,cAAkB,EAAK,MAAK,CAAA;GAClC,MAAK;GACJ,UAAU,EAAK,eAAU;GACzB,OAAO,EAAK;GACZ,gBAAc,EAAK,OAAO,EAAA,QAAe,aAAgB,KAAA;GACzD,UAAK,MAAE,EAAS,EAAK,UAAU;MAEhC,EAAuD,QAAvD,GAAuD,EAApB,EAAK,KAAK,GAAA,CAAA,GACjC,EAAK,eAAU,OACV,EAAA,IAAA,EAAA,KADU,EAAA,GAA3B,EAES,QAFT,GAES,EADP,EAAK,UAAU,GAAA,CAAA,EAAA,GAAA,IAAA,CAAA,qBAMnB,EA4BS,GAAA,EAAA,KAAA,EAAA,GAAA,EA3Bc,EAAA,YAAd,YADT,EA4BS,UAAA;GA1BN,KAAK;GACN,OAAK,EAAA,CAAC,qBAAmB;wBACmB,MAAe,EAAA;2BAAiD,EAAA,eAAe,IAAU;;GAIrI,MAAK;GACJ,wBAAsB;GACtB,gBAAc,MAAe,EAAA,aAAU,SAAY,KAAA;GACnD,OAAK,QAAU,EAAU;GACzB,UAAK,MAAE,EAAS,CAAU;MAEX,EAAA,eAAe,IAAU,MAAA,EAAA,GAAzC,EAWW,GAAA,EAAA,KAAA,EAAA,GAAA,CAVT,EAQO,QARP,GAQO,CAPL,EAME,OAAA;GALA,OAAM;GACL,KAAK,EAAA,eAAe,IAAU,MAAA;GAC/B,KAAI;GACJ,SAAQ;GACR,WAAU;oBAGd,EAAgD,QAAhD,GAAgD,EAApB,CAAU,GAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAExC,EAAsD,QAAtD,GAAsD,EAApB,CAAU,GAAA,CAAA,IAC5C,EAA+D,QAA/D,GAA+D,EAA/B,EAAU,CAAU,CAAA,GAAA,CAAA,CAAA,GAAA,IAAA,CAAA"}
@@ -2,7 +2,7 @@ import e from "../_virtual/_plugin-vue_export-helper.js";
2
2
  import t from "./PdfViewer.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/components/PdfViewer.vue
5
- var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-2a30ef87"]]);
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-7de14862"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"PdfViewer.js","names":[],"sources":["../../src/components/PdfViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport * as pdfjsLib from 'pdfjs-dist'\nimport type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'\n// pdfjs runs its parser/rasterizer in a web worker. Vite's `?url` emits the real worker asset and returns a\n// servable URL (root-relative in the same-origin prod build, a dev URL under `pnpm dev`); we point pdfjs at\n// it. The `.mjs` is served as text/javascript by Ktor (see AGENTS.md) so the module worker loads in prod.\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\n\npdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerUrl\n\n/**\n * Side-viewer PDF (`.pdf`) renderer. Fetches the document bytes from the download URL and rasterizes each\n * page to a <canvas> entirely in-browser via pdfjs-dist (https://github.com/mozilla/pdf.js) — a vertically\n * scrollable list of pages (white sheets on a neutral \"desk\", like the docx/pptx viewers). No CDN / network\n * beyond the file fetch: pdfjs bundles its own worker, fetched lazily as a same-origin asset (see above).\n *\n * Two entry shapes mirror the other viewers (docx/xlsx/pptx/zip): a real download URL for a held attachment,\n * or a blob URL minted by the archive browsers from already-extracted bytes. Pages re-rasterize when the pane\n * is resized (debounced) so text stays crisp at the new width.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .pdf bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst pageCount = ref(0)\n// The scroll container the page canvases render into. Always mounted (the loading/error states overlay it).\nconst docEl = ref<HTMLElement | null>(null)\n\n// Live document proxy + in-flight render tasks — torn down on re-render / unmount (the worker and page\n// rasterizers don't GC on their own).\nlet pdfDoc: PDFDocumentProxy | null = null\nlet renderTasks: RenderTask[] = []\n// Bumped on every (re)render so a stale async pass (older file, or a superseded resize) can bail out.\nlet renderToken = 0\n// Cap the backing-store resolution: device pixels × this, clamped, keeps big/high-DPI pages from minting\n// multi-hundred-MB canvases while staying crisp.\nconst MAX_SCALE = 2\n\nfunction cancelRenders(): void {\n for (const t of renderTasks) {\n try {\n t.cancel()\n } catch {\n /* already settled — ignore */\n }\n }\n renderTasks = []\n}\n\nfunction disposeDoc(): void {\n cancelRenders()\n if (pdfDoc) {\n try {\n pdfDoc.destroy()\n } catch {\n /* already torn down — ignore */\n }\n pdfDoc = null\n }\n}\n\n// Rasterize every page of the live document into the container at its current width. Called on load and on\n// resize; guarded by `renderToken` so an outdated pass (file switched, or a newer resize started) stops early.\nasync function renderPages(): Promise<void> {\n const el = docEl.value\n const doc = pdfDoc\n if (!el || !doc) return\n const token = ++renderToken\n cancelRenders()\n el.innerHTML = ''\n // Available CSS width for a page (minus the container's horizontal padding). Floor at a sane minimum so a\n // momentarily-zero clientWidth (not yet laid out) doesn't render a 0-px canvas.\n const style = getComputedStyle(el)\n const padX = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)\n const cssWidth = Math.max(120, el.clientWidth - padX)\n const dpr = Math.min(typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1, MAX_SCALE)\n for (let n = 1; n <= doc.numPages; n++) {\n let page\n try {\n page = await doc.getPage(n)\n } catch {\n if (token !== renderToken) return\n continue // skip an unreadable page rather than failing the whole doc\n }\n if (token !== renderToken) return\n const base = page.getViewport({ scale: 1 })\n const cssScale = cssWidth / base.width\n const viewport = page.getViewport({ scale: cssScale * dpr })\n const canvas = document.createElement('canvas')\n canvas.className = 'pf__page'\n canvas.width = Math.ceil(viewport.width)\n canvas.height = Math.ceil(viewport.height)\n // Display at the CSS width; the extra backing-store pixels (× dpr) make text sharp on HiDPI screens.\n canvas.style.width = `${Math.round(cssWidth)}px`\n canvas.style.height = `${Math.round(cssWidth * (base.height / base.width))}px`\n if (!canvas.getContext('2d')) continue // no 2D context available — skip rather than crash the render\n el.appendChild(canvas)\n // Pass `canvas` (the preferred param in pdfjs v5); it derives the 2D context itself.\n const task = page.render({ canvas, viewport })\n renderTasks.push(task)\n try {\n await task.promise\n } catch {\n // RenderingCancelledException on supersede/unmount is expected — only a live token cares about failures.\n if (token !== renderToken) return\n }\n if (token !== renderToken) return\n }\n}\n\nasync function load(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n status.value = 'loading'\n errorMsg.value = ''\n disposeDoc()\n el.innerHTML = ''\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer load may have started while we awaited the fetch.\n if (props.url !== url || docEl.value !== el) return\n const doc = await pdfjsLib.getDocument({ data: new Uint8Array(buf) }).promise\n if (props.url !== url || docEl.value !== el) {\n try {\n doc.destroy()\n } catch {\n /* ignore */\n }\n return\n }\n pdfDoc = doc\n pageCount.value = doc.numPages\n await renderPages()\n if (props.url !== url || docEl.value !== el) return\n status.value = 'ready'\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// Re-rasterize on width changes (debounced) so pages stay crisp as the pane is dragged wider/narrower. Only\n// the width matters; height changes don't affect page scale.\nlet resizeTimer: ReturnType<typeof setTimeout> | undefined\nlet lastWidth = 0\nlet ro: ResizeObserver | null = null\nfunction onResize(): void {\n const el = docEl.value\n if (!el || !pdfDoc) return\n const w = el.clientWidth\n if (Math.abs(w - lastWidth) < 8) return // ignore sub-pixel / negligible jitter\n lastWidth = w\n clearTimeout(resizeTimer)\n resizeTimer = setTimeout(() => {\n if (pdfDoc) void renderPages()\n }, 180)\n}\n\nonMounted(() => {\n load()\n if (typeof ResizeObserver !== 'undefined' && docEl.value) {\n lastWidth = docEl.value.clientWidth\n ro = new ResizeObserver(onResize)\n ro.observe(docEl.value)\n }\n})\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n clearTimeout(resizeTimer)\n ro?.disconnect()\n renderToken++ // invalidate any in-flight render pass\n disposeDoc()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"pf\">\n <!-- Always mounted so the renderer has a sized, scrollable container; states overlay it. -->\n <div ref=\"docEl\" class=\"pf__doc\" />\n <div v-if=\"status === 'loading'\" class=\"pf__overlay\">\n <p class=\"pf__msg\">正在渲染 PDF…</p>\n </div>\n <div v-else-if=\"status === 'error'\" class=\"pf__overlay\">\n <div class=\"pf__msg pf__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"pf__retry\" @click=\"load\">重试</button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.pf {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n/* Scroll region holding the rendered pages — white sheets on a neutral \"desk\", like the docx/pptx viewers. */\n.pf__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n padding: 18px 16px 40px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n}\n/* Each page canvas: a white sheet with a soft shadow. Width is set inline (the rasterized CSS width); the\n * canvas paints its own white, so no background needed here. */\n.pf__doc :deep(.pf__page) {\n display: block;\n max-width: 100%;\n box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);\n border-radius: 2px;\n background: #fff;\n}\n/* Overlay covers the (empty) container while loading or on error, without unmounting it. */\n.pf__overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: flex-start;\n justify-content: flex-start;\n background: var(--surface);\n}\n.pf__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.pf__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.pf__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.pf__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"PdfViewer.js","names":[],"sources":["../../src/components/PdfViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, shallowRef, useTemplateRef, watch } from 'vue'\nimport * as pdfjsLib from 'pdfjs-dist'\nimport type { PDFDocumentProxy, RenderTask } from 'pdfjs-dist'\n// pdfjs runs its parser/rasterizer in a web worker. Vite's `?url` emits the real worker asset and returns a\n// servable URL (root-relative in the same-origin prod build, a dev URL under `pnpm dev`).\nimport pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'\n\nimport PdfNavigationPane from './PdfNavigationPane.vue'\nimport { loadPdfNavigation, type PdfNavigationItem } from '../lib/pdfNavigation'\n\npdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerUrl\n\nconst props = defineProps<{\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\nconst pageCount = shallowRef(0)\nconst activePage = shallowRef(1)\nconst outlineItems = shallowRef<PdfNavigationItem[]>([])\nconst pageLabels = shallowRef<string[]>([])\nconst pageThumbnails = shallowRef<Array<string | null>>([])\nconst navigationCollapsed = shallowRef(false)\nconst navigationView = shallowRef<'outline' | 'pages'>('outline')\nconst docEl = useTemplateRef<HTMLElement>('docEl')\n\nlet pdfDoc: PDFDocumentProxy | null = null\nlet renderTasks: RenderTask[] = []\nlet renderToken = 0\nlet resizeTimer: ReturnType<typeof setTimeout> | undefined\nlet lastSize = ''\nlet resizeObserver: ResizeObserver | null = null\nlet scrollFrame = 0\nconst MAX_SCALE = 2\nconst PAGE_GUTTER = 24\nconst THUMBNAIL_WIDTH = 104\n\nfunction cancelRenders(): void {\n for (const task of renderTasks) {\n try {\n task.cancel()\n } catch {\n // The task may already have settled.\n }\n }\n renderTasks = []\n}\n\nfunction disposeDoc(): void {\n cancelRenders()\n if (!pdfDoc) return\n try {\n pdfDoc.destroy()\n } catch {\n // The worker may already have been torn down.\n }\n pdfDoc = null\n}\n\nfunction createPageThumbnail(canvas: HTMLCanvasElement): string | null {\n if (!canvas.width || !canvas.height) return null\n try {\n const thumbnail = document.createElement('canvas')\n thumbnail.width = THUMBNAIL_WIDTH\n thumbnail.height = Math.max(1, Math.round(THUMBNAIL_WIDTH * (canvas.height / canvas.width)))\n const context = thumbnail.getContext('2d')\n if (!context) return null\n context.fillStyle = '#fff'\n context.fillRect(0, 0, thumbnail.width, thumbnail.height)\n context.drawImage(canvas, 0, 0, thumbnail.width, thumbnail.height)\n return thumbnail.toDataURL('image/jpeg', 0.78)\n } catch {\n return null\n }\n}\n\nfunction scrollToPage(pageNumber: number, behavior: ScrollBehavior = 'smooth'): void {\n const el = docEl.value\n const target = el?.querySelector<HTMLElement>(`[data-page-number=\"${pageNumber}\"]`)\n if (!el || !target) return\n activePage.value = pageNumber\n const rootRect = el.getBoundingClientRect()\n const targetRect = target.getBoundingClientRect()\n const paddingTop = Number.parseFloat(getComputedStyle(el).paddingTop)\n el.scrollTo({\n top: el.scrollTop + targetRect.top - rootRect.top - paddingTop,\n behavior,\n })\n}\n\nfunction syncActivePage(): void {\n if (scrollFrame) return\n scrollFrame = window.requestAnimationFrame(() => {\n scrollFrame = 0\n const el = docEl.value\n if (!el) return\n const rootRect = el.getBoundingClientRect()\n const targetY = rootRect.top + Math.min(el.clientHeight * 0.36, 220)\n let nearestPage = activePage.value\n let nearestDistance = Number.POSITIVE_INFINITY\n for (const page of el.querySelectorAll<HTMLElement>('[data-page-number]')) {\n const rect = page.getBoundingClientRect()\n const pageTarget = Math.min(Math.max(targetY, rect.top), rect.bottom)\n const distance = Math.abs(targetY - pageTarget)\n if (distance >= nearestDistance) continue\n nearestDistance = distance\n nearestPage = Number(page.dataset.pageNumber) || nearestPage\n }\n activePage.value = nearestPage\n })\n}\n\nasync function renderPages(): Promise<void> {\n const el = docEl.value\n const doc = pdfDoc\n if (!el || !doc) return\n const token = ++renderToken\n const preservedPage = activePage.value\n const preserveScroll = el.scrollTop > 0\n cancelRenders()\n el.innerHTML = ''\n\n const style = getComputedStyle(el)\n const padX = Number.parseFloat(style.paddingLeft) + Number.parseFloat(style.paddingRight)\n const padY = Number.parseFloat(style.paddingTop) + Number.parseFloat(style.paddingBottom)\n const availableWidth = Math.max(120, el.clientWidth - padX - PAGE_GUTTER)\n const availableHeight = Math.max(160, el.clientHeight - padY - PAGE_GUTTER)\n const outputScale = Math.min(window.devicePixelRatio || 1, MAX_SCALE)\n const thumbnails = Array<string | null>(doc.numPages).fill(null)\n\n for (let pageNumber = 1; pageNumber <= doc.numPages; pageNumber += 1) {\n let page\n try {\n page = await doc.getPage(pageNumber)\n } catch {\n if (token !== renderToken) return\n continue\n }\n if (token !== renderToken) return\n\n const base = page.getViewport({ scale: 1 })\n const cssScale = Math.min(availableWidth / base.width, availableHeight / base.height)\n const cssWidth = Math.round(base.width * cssScale)\n const cssHeight = Math.round(base.height * cssScale)\n const viewport = page.getViewport({ scale: cssScale * outputScale })\n\n const pageWrap = document.createElement('div')\n pageWrap.className = 'pf__page-wrap'\n pageWrap.dataset.pageNumber = `${pageNumber}`\n const canvas = document.createElement('canvas')\n canvas.className = 'pf__page'\n canvas.width = Math.ceil(viewport.width)\n canvas.height = Math.ceil(viewport.height)\n canvas.style.width = `${cssWidth}px`\n canvas.style.height = `${cssHeight}px`\n canvas.setAttribute('aria-label', `PDF 第 ${pageNumber} 页`)\n if (!canvas.getContext('2d')) continue\n pageWrap.appendChild(canvas)\n el.appendChild(pageWrap)\n\n const task = page.render({ canvas, viewport })\n renderTasks.push(task)\n let rendered = true\n try {\n await task.promise\n } catch {\n if (token !== renderToken) return\n rendered = false\n }\n if (token !== renderToken) return\n if (rendered) thumbnails[pageNumber - 1] = createPageThumbnail(canvas)\n }\n\n pageThumbnails.value = thumbnails\n if (preserveScroll) scrollToPage(preservedPage, 'auto')\n syncActivePage()\n}\n\nasync function load(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n status.value = 'loading'\n errorMsg.value = ''\n outlineItems.value = []\n pageLabels.value = []\n pageThumbnails.value = []\n disposeDoc()\n el.innerHTML = ''\n const url = props.url\n\n try {\n const response = await fetch(url)\n if (!response.ok) throw new Error(`HTTP ${response.status}`)\n const bytes = new Uint8Array(await response.arrayBuffer())\n if (props.url !== url || docEl.value !== el) return\n\n const document = await pdfjsLib.getDocument({ data: bytes }).promise\n if (props.url !== url || docEl.value !== el) {\n document.destroy()\n return\n }\n pdfDoc = document\n pageCount.value = document.numPages\n activePage.value = 1\n const navigation = await loadPdfNavigation(document)\n if (props.url !== url || docEl.value !== el) return\n outlineItems.value = navigation.outline\n pageLabels.value = navigation.pageLabels\n navigationView.value = navigation.outline.length ? 'outline' : 'pages'\n await renderPages()\n if (props.url !== url || docEl.value !== el) return\n status.value = 'ready'\n } catch (cause) {\n errorMsg.value = cause instanceof Error ? cause.message : String(cause)\n status.value = 'error'\n }\n}\n\nfunction onResize(): void {\n const el = docEl.value\n if (!el || !pdfDoc) return\n const size = `${el.clientWidth}x${el.clientHeight}`\n if (!el.clientWidth || !el.clientHeight || size === lastSize) return\n lastSize = size\n clearTimeout(resizeTimer)\n resizeTimer = setTimeout(() => {\n if (pdfDoc) void renderPages()\n }, 180)\n}\n\nonMounted(() => {\n void load()\n if (typeof ResizeObserver !== 'undefined' && docEl.value) {\n lastSize = `${docEl.value.clientWidth}x${docEl.value.clientHeight}`\n resizeObserver = new ResizeObserver(onResize)\n resizeObserver.observe(docEl.value)\n }\n})\nwatch(() => props.url, load)\nonBeforeUnmount(() => {\n clearTimeout(resizeTimer)\n resizeObserver?.disconnect()\n if (scrollFrame) window.cancelAnimationFrame(scrollFrame)\n renderToken += 1\n disposeDoc()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"pf\">\n <PdfNavigationPane\n v-if=\"status === 'ready'\"\n v-model:collapsed=\"navigationCollapsed\"\n v-model:view=\"navigationView\"\n :outline-items=\"outlineItems\"\n :page-count=\"pageCount\"\n :page-labels=\"pageLabels\"\n :page-thumbnails=\"pageThumbnails\"\n :active-page=\"activePage\"\n @navigate=\"scrollToPage\"\n />\n <div class=\"pf__stage\">\n <div ref=\"docEl\" class=\"pf__doc\" @scroll.passive=\"syncActivePage\" />\n <div v-if=\"status === 'loading'\" class=\"pf__overlay\">\n <p class=\"pf__msg\">正在渲染 PDF…</p>\n </div>\n <div v-else-if=\"status === 'error'\" class=\"pf__overlay\">\n <div class=\"pf__msg pf__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"pf__retry\" @click=\"load\">重试</button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.pf {\n position: relative;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex: 1 1 auto;\n overflow: hidden;\n container-type: inline-size;\n background: var(--surface);\n}\n\n.pf__stage {\n position: relative;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex: 1 1 0;\n overflow: hidden;\n}\n\n.pf__doc {\n width: 100%;\n min-width: 0;\n min-height: 0;\n flex: 1 1 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 24px 20px 42px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n box-sizing: border-box;\n background: var(--surface-2);\n scrollbar-color: #555961 transparent;\n scrollbar-width: thin;\n}\n\n.pf__doc :deep(.pf__page-wrap) {\n max-width: 100%;\n display: flex;\n flex: none;\n justify-content: center;\n}\n\n.pf__doc :deep(.pf__page) {\n max-width: 100%;\n display: block;\n background: #fff;\n border-radius: 2px;\n box-shadow: 0 4px 18px rgba(0, 0, 0, 0.28);\n}\n\n.pf__overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: flex-start;\n justify-content: flex-start;\n background: var(--surface);\n}\n\n.pf__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n\n.pf__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n\n.pf__retry {\n padding: 5px 12px;\n color: var(--text);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n}\n\n.pf__retry:hover {\n color: var(--accent);\n border-color: var(--accent);\n}\n</style>\n"],"mappings":""}
@@ -1,15 +1,4 @@
1
- /**
2
- * Side-viewer PDF (`.pdf`) renderer. Fetches the document bytes from the download URL and rasterizes each
3
- * page to a <canvas> entirely in-browser via pdfjs-dist (https://github.com/mozilla/pdf.js) — a vertically
4
- * scrollable list of pages (white sheets on a neutral "desk", like the docx/pptx viewers). No CDN / network
5
- * beyond the file fetch: pdfjs bundles its own worker, fetched lazily as a same-origin asset (see above).
6
- *
7
- * Two entry shapes mirror the other viewers (docx/xlsx/pptx/zip): a real download URL for a held attachment,
8
- * or a blob URL minted by the archive browsers from already-extracted bytes. Pages re-rasterize when the pane
9
- * is resized (debounced) so text stays crisp at the new width.
10
- */
11
1
  type __VLS_Props = {
12
- /** Download/blob URL of the .pdf bytes. */
13
2
  url?: string;
14
3
  name: string;
15
4
  size?: number;