263 lines
6.2 KiB
Vue
263 lines
6.2 KiB
Vue
<script setup lang="ts">
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import {
|
|
GlobalWorkerOptions,
|
|
getDocument,
|
|
type PDFDocumentLoadingTask,
|
|
type PDFDocumentProxy,
|
|
type RenderTask,
|
|
} from "pdfjs-dist";
|
|
import { computed, nextTick, onBeforeUnmount, ref, shallowRef, watch } from "vue";
|
|
|
|
const props = defineProps<{
|
|
scanUrl: string;
|
|
title: string;
|
|
}>();
|
|
|
|
const show = defineModel<boolean>("show", { required: true });
|
|
|
|
GlobalWorkerOptions.workerSrc = new URL(
|
|
"pdfjs-dist/build/pdf.worker.min.mjs",
|
|
import.meta.url,
|
|
).toString();
|
|
|
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
|
const pdfDocument = shallowRef<PDFDocumentProxy | null>(null);
|
|
const loading = ref(false);
|
|
const error = ref("");
|
|
const pageNumber = ref(1);
|
|
const pageCount = ref(0);
|
|
const scale = ref(1.1);
|
|
|
|
let loadingTask: PDFDocumentLoadingTask | null = null;
|
|
let renderTask: RenderTask | null = null;
|
|
const pageInfo = computed(() => `${pageNumber.value} / ${pageCount.value}`);
|
|
const canGoBack = computed(() => pageNumber.value > 1);
|
|
const canGoForward = computed(() => pageNumber.value < pageCount.value);
|
|
|
|
async function loadDocument() {
|
|
resetDocument();
|
|
|
|
loading.value = true;
|
|
error.value = "";
|
|
|
|
try {
|
|
const bytes = await invoke<number[]>("load_remote_file", {
|
|
url: props.scanUrl,
|
|
});
|
|
|
|
if (!bytes.length) {
|
|
throw new Error("Файл пустой");
|
|
}
|
|
|
|
const task = getDocument({ data: new Uint8Array(bytes) });
|
|
loadingTask = task;
|
|
|
|
const document = await task.promise;
|
|
if (loadingTask !== task) {
|
|
await document.destroy();
|
|
return;
|
|
}
|
|
|
|
pdfDocument.value = document;
|
|
pageCount.value = document.numPages;
|
|
pageNumber.value = 1;
|
|
scale.value = 1.1;
|
|
loading.value = false;
|
|
await nextTick();
|
|
await renderPage();
|
|
} catch (err) {
|
|
error.value = err instanceof Error ? err.message : "Не удалось открыть PDF";
|
|
loading.value = false;
|
|
if (loadingTask) {
|
|
loadingTask = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function renderPage() {
|
|
const document = pdfDocument.value;
|
|
const canvas = canvasRef.value;
|
|
|
|
if (!document || !canvas) {
|
|
return;
|
|
}
|
|
|
|
renderTask?.cancel();
|
|
|
|
const page = await document.getPage(pageNumber.value);
|
|
const viewport = page.getViewport({ scale: scale.value });
|
|
const context = canvas.getContext("2d");
|
|
|
|
if (!context) {
|
|
error.value = "Не удалось создать canvas context";
|
|
return;
|
|
}
|
|
|
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
|
canvas.width = Math.floor(viewport.width * devicePixelRatio);
|
|
canvas.height = Math.floor(viewport.height * devicePixelRatio);
|
|
canvas.style.width = `${viewport.width}px`;
|
|
canvas.style.height = `${viewport.height}px`;
|
|
|
|
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
|
|
|
|
renderTask = page.render({ canvas: canvas, canvasContext: context, viewport });
|
|
|
|
try {
|
|
await renderTask.promise;
|
|
} catch (err) {
|
|
if (!(err instanceof Error) || err.name !== "RenderingCancelledException") {
|
|
error.value = err instanceof Error ? err.message : "Не удалось отрисовать PDF";
|
|
}
|
|
}
|
|
}
|
|
|
|
function resetDocument() {
|
|
renderTask?.cancel();
|
|
renderTask = null;
|
|
loadingTask?.destroy().catch(() => undefined);
|
|
loadingTask = null;
|
|
pdfDocument.value?.destroy().catch(() => undefined);
|
|
pdfDocument.value = null;
|
|
pageCount.value = 0;
|
|
pageNumber.value = 1;
|
|
loading.value = false;
|
|
error.value = "";
|
|
}
|
|
|
|
async function goBack() {
|
|
if (canGoBack.value) {
|
|
pageNumber.value -= 1;
|
|
await nextTick();
|
|
await renderPage();
|
|
}
|
|
}
|
|
|
|
async function goForward() {
|
|
if (canGoForward.value) {
|
|
pageNumber.value += 1;
|
|
await nextTick();
|
|
await renderPage();
|
|
}
|
|
}
|
|
|
|
async function zoomIn() {
|
|
scale.value = Math.min(scale.value + 0.2, 3);
|
|
await nextTick();
|
|
await renderPage();
|
|
}
|
|
|
|
async function zoomOut() {
|
|
scale.value = Math.max(scale.value - 0.2, 0.5);
|
|
await nextTick();
|
|
await renderPage();
|
|
}
|
|
|
|
watch([show, () => props.scanUrl], async ([visible]) => {
|
|
if (visible) {
|
|
await nextTick();
|
|
await loadDocument();
|
|
} else {
|
|
resetDocument();
|
|
}
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
resetDocument();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<van-popup v-model:show="show" round position="bottom" class="pdf-preview">
|
|
<div class="pdf-preview__header">
|
|
<div class="pdf-preview__title">{{ title }}</div>
|
|
<van-button size="small" plain @click="show = false">Закрыть</van-button>
|
|
</div>
|
|
|
|
<div class="pdf-preview__toolbar">
|
|
<van-button size="small" plain :disabled="!canGoBack" @click="goBack">
|
|
Назад
|
|
</van-button>
|
|
<div class="pdf-preview__page-info">{{ pageInfo }}</div>
|
|
<van-button size="small" plain :disabled="!canGoForward" @click="goForward">
|
|
Вперед
|
|
</van-button>
|
|
<van-button size="small" plain @click="zoomOut">-</van-button>
|
|
<van-button size="small" plain @click="zoomIn">+</van-button>
|
|
</div>
|
|
|
|
<div class="pdf-preview__body">
|
|
<van-loading v-if="loading">Загрузка PDF...</van-loading>
|
|
<van-empty v-else-if="error" :description="error" />
|
|
<div v-else class="pdf-preview__canvas-wrap">
|
|
<canvas ref="canvasRef" class="pdf-preview__canvas" />
|
|
</div>
|
|
</div>
|
|
</van-popup>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.pdf-preview {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 92vh;
|
|
background: #f5f7fb;
|
|
}
|
|
|
|
.pdf-preview__header,
|
|
.pdf-preview__toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 12px 16px;
|
|
background: #fff;
|
|
}
|
|
|
|
.pdf-preview__header {
|
|
justify-content: space-between;
|
|
border-bottom: 1px solid #eef0f4;
|
|
}
|
|
|
|
.pdf-preview__title {
|
|
min-width: 0;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.pdf-preview__toolbar {
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
border-bottom: 1px solid #eef0f4;
|
|
}
|
|
|
|
.pdf-preview__page-info {
|
|
min-width: 72px;
|
|
text-align: center;
|
|
color: #636b74;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.pdf-preview__body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
padding: 16px;
|
|
}
|
|
|
|
.pdf-preview__canvas-wrap {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.pdf-preview__canvas {
|
|
max-width: 100%;
|
|
height: auto;
|
|
background: #fff;
|
|
box-shadow: 0 8px 28px rgba(15, 23, 42, 0.16);
|
|
}
|
|
</style>
|