make sure pdf viewer builds

This commit is contained in:
polv committed 2026-03-26 00:10:58 +07:00
1 parent 32ae81cb20
commit 1dae31e2bc
6 files changed
+292 -40

No files matched your search

+6
View File
@@ -62,4 +62,10 @@ export default defineConfig({
md.use(mdInline);
},
},
vite: {
ssr: {
noExternal: [],
external: ["pdfjs-dist"], // don't bundle it for SSR — leave it browser-only
},
},
});
+268 -11
View File
@@ -1,22 +1,279 @@
<template>
<div :class="$style.container">
<VuePdf v-for="page in pages" :key="page" :pdf="pdf" :page="page" />
<div class="pdf-viewer">
<!-- Toolbar -->
<div class="pdf-toolbar">
<span class="pdf-toolbar__name">{{ fileName || src }}</span>
<div class="pdf-toolbar__right">
<span class="pdf-toolbar__pages" v-if="totalPages">
{{ totalPages }} pages
</span>
<button class="pdf-zoom-btn" @click="zoom(-0.25)" title="Zoom out">
−
</button>
<span class="pdf-zoom-label">{{ Math.round(scale * 100) }}%</span>
<button class="pdf-zoom-btn" @click="zoom(+0.25)" title="Zoom in">
+
</button>
</div>
</div>
<!-- Scroll container — all pages rendered vertically -->
<div class="pdf-scroll" ref="scrollEl">
<div v-if="loading" class="pdf-msg">Loading…</div>
<div v-else-if="error" class="pdf-msg pdf-msg--err">{{ error }}</div>
<div v-else class="pdf-pages">
<canvas
v-for="n in totalPages"
:key="n"
:ref="(el) => setCanvasRef(el, n)"
class="pdf-page"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ src: string }>();
<script setup>
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from "vue";
import { VuePdf, usePDF } from "@vue3-office/vue-pdf";
import "@vue3-office/vue-pdf/style.css";
const props = defineProps({
/** URL string, Uint8Array, or ArrayBuffer */
src: { type: [String, Object], required: true },
fileName: { type: String },
initialScale: { type: Number, default: 1.25 },
});
const { pdf, pages, info } = usePDF(props.src);
// ── state ──────────────────────────────────────────────────────
const scrollEl = ref(null);
const loading = ref(true);
const error = ref(null);
const totalPages = ref(0);
const scale = ref(props.initialScale);
let pdfjsLib = null;
let pdfDoc = null;
const canvasMap = {}; // pageNum → <canvas> element
const taskMap = {}; // pageNum → active renderTask
// ── canvas ref collector ───────────────────────────────────────
function setCanvasRef(el, n) {
if (el) canvasMap[n] = el;
}
// ── load ───────────────────────────────────────────────────────
async function loadPdf(src) {
loading.value = true;
error.value = null;
totalPages.value = 0;
try {
if (!pdfjsLib) {
// Dynamic import — never runs in Node/SSR
pdfjsLib = await import("pdfjs-dist");
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.mjs",
import.meta.url,
).toString();
}
if (pdfDoc) {
pdfDoc.destroy();
pdfDoc = null;
}
pdfDoc = await pdfjsLib.getDocument(src).promise;
totalPages.value = pdfDoc.numPages;
loading.value = false;
// Wait for Vue to stamp out all the <canvas> elements
await nextTick();
await renderAll();
} catch (e) {
error.value = `Failed to load PDF: ${e.message}`;
loading.value = false;
}
}
// ── render all pages ───────────────────────────────────────────
async function renderAll() {
// Cancel any in-flight tasks first
for (const task of Object.values(taskMap)) {
try {
task.cancel();
} catch {}
}
// Render pages sequentially — avoids memory thrash on mobile/Android
for (let n = 1; n <= totalPages.value; n++) {
await renderPage(n);
}
}
async function renderPage(n) {
if (!pdfDoc) return;
const canvas = canvasMap[n];
if (!canvas) return;
try {
const page = await pdfDoc.getPage(n);
const viewport = page.getViewport({ scale: scale.value });
canvas.width = viewport.width;
canvas.height = viewport.height;
const task = page.render({
canvasContext: canvas.getContext("2d"),
viewport,
});
taskMap[n] = task;
await task.promise;
delete taskMap[n];
} catch (e) {
if (e?.name !== "RenderingCancelledException") {
console.warn(`Page ${n} render error:`, e.message);
}
}
}
// ── zoom ───────────────────────────────────────────────────────
async function zoom(delta) {
scale.value = Math.min(4, Math.max(0.5, +(scale.value + delta).toFixed(2)));
await nextTick();
await renderAll();
}
// ── lifecycle ──────────────────────────────────────────────────
onMounted(() => loadPdf(props.src));
watch(
() => props.src,
(src) => loadPdf(src),
);
onBeforeUnmount(() => {
for (const task of Object.values(taskMap)) {
try {
task.cancel();
} catch {}
}
if (pdfDoc) pdfDoc.destroy();
});
</script>
<style module>
.container {
width: 100%;
<style scoped>
/* ── root ── */
.pdf-viewer {
--bar-bg: #141414;
--bg: #1e1e1e;
--border: #2e2e2e;
--text: #d4d4d4;
--muted: #666;
--accent: #5ba3f5;
display: flex;
flex-direction: column;
height: 100%; /* parent must supply a bounded height */
max-height: 80vh;
overflow: scroll;
background: var(--bg);
color: var(--text);
/* font-family: "JetBrains Mono", "Fira Mono", monospace; */
border-radius: 6px;
}
/* ── toolbar ── */
.pdf-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: 0 14px;
height: 40px;
background: var(--bar-bg);
border-bottom: 1px solid var(--border);
gap: 12px;
}
.pdf-toolbar__name {
font-size: 12px;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pdf-toolbar__right {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.pdf-toolbar__pages {
font-size: 11px;
color: var(--muted);
}
.pdf-zoom-btn {
background: none;
border: 1px solid var(--border);
color: var(--text);
border-radius: 4px;
width: 26px;
height: 26px;
font-size: 16px;
line-height: 1;
cursor: pointer;
transition:
border-color 0.15s,
color 0.15s;
}
.pdf-zoom-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.pdf-zoom-label {
font-size: 11px;
color: var(--muted);
min-width: 36px;
text-align: center;
}
/* ── scroll area — the fix lives here ── */
.pdf-scroll {
flex: 1;
min-height: 0; /* lets the flex child shrink so overflow-y activates */
overflow-y: auto;
overflow-x: auto;
padding: 20px 0;
background: var(--bg);
}
/* ── pages stack ── */
.pdf-pages {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.pdf-page {
display: block;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.6);
border-radius: 2px;
}
/* ── status ── */
.pdf-msg {
text-align: center;
padding: 60px 20px;
font-size: 13px;
color: var(--muted);
}
.pdf-msg--err {
color: #e06c6c;
}
</style>