make sure pdf viewer builds
This commit is contained in:
1 parent
32ae81cb20
commit
1dae31e2bc
6 files changed
+292
-40
No files matched your search
@@ -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>
|
||||
Reference in new issue
Block a user