|
|
@@ -1,24 +1,66 @@
|
|
|
<template>
|
|
|
- <div class="pdf-view" v-if="props.filesList.length">
|
|
|
+ <div v-if="props.filesList.length" ref="containerRef" class="pdf-view">
|
|
|
<div v-if="pdfError" class="pdf-empty">{{ pdfError }}</div>
|
|
|
- <VuePdfEmbed
|
|
|
+ <DynamicScroller
|
|
|
v-else
|
|
|
- class="pdf-embed"
|
|
|
- :source="pdfSource"
|
|
|
- @loaded="handleLoaded"
|
|
|
- @loading-failed="handleLoadFailed"
|
|
|
- @rendering-failed="handleRenderFailed"
|
|
|
- />
|
|
|
+ :key="pdfUrl"
|
|
|
+ class="pdf-scroller"
|
|
|
+ :items="pageItems"
|
|
|
+ :min-item-size="estimatedPageHeight"
|
|
|
+ :buffer="560"
|
|
|
+ :prerender="1"
|
|
|
+ :update-interval="80"
|
|
|
+ key-field="id"
|
|
|
+ v-slot="{ item, active }"
|
|
|
+ >
|
|
|
+ <DynamicScrollerItem
|
|
|
+ :item="item"
|
|
|
+ :active="active"
|
|
|
+ :size-dependencies="[pageWidth]"
|
|
|
+ class="pdf-page-item"
|
|
|
+ >
|
|
|
+ <PdfVirtualPage
|
|
|
+ :key="item.id"
|
|
|
+ :active="canRenderPage(item, active)"
|
|
|
+ :doc="pdfDocument"
|
|
|
+ :page-number="item.pageNumber"
|
|
|
+ :page-width="pageWidth"
|
|
|
+ :placeholder-height="estimatedPageHeight"
|
|
|
+ @rendered="handlePageRendered"
|
|
|
+ @error="handleRenderFailed"
|
|
|
+ />
|
|
|
+ </DynamicScrollerItem>
|
|
|
+ </DynamicScroller>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { computed, ref } from "vue";
|
|
|
-import VuePdfEmbed from "vue-pdf-embed";
|
|
|
+import {
|
|
|
+ computed,
|
|
|
+ markRaw,
|
|
|
+ nextTick,
|
|
|
+ onBeforeUnmount,
|
|
|
+ onMounted,
|
|
|
+ ref,
|
|
|
+ shallowRef,
|
|
|
+ watch,
|
|
|
+} from "vue";
|
|
|
+import { onBeforeRouteLeave } from "vue-router";
|
|
|
+import {
|
|
|
+ getDocument,
|
|
|
+ GlobalWorkerOptions,
|
|
|
+} from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
|
+import { DynamicScroller, DynamicScrollerItem } from "vue-virtual-scroller";
|
|
|
+import PdfWorker from "pdfjs-dist/legacy/build/pdf.worker.min.mjs?url";
|
|
|
import { getThumb } from "@/utils/file";
|
|
|
+import PdfVirtualPage from "@/components/FileReader/PdfVirtualPage.vue";
|
|
|
|
|
|
-import "vue-pdf-embed/dist/styles/annotationLayer.css";
|
|
|
-import "vue-pdf-embed/dist/styles/textLayer.css";
|
|
|
+import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
|
|
+
|
|
|
+const RANGE_CHUNK_SIZE = 32 * 1024;
|
|
|
+const DEFAULT_PAGE_RATIO = 1.414;
|
|
|
+
|
|
|
+GlobalWorkerOptions.workerSrc = PdfWorker;
|
|
|
|
|
|
const props = defineProps({
|
|
|
url: {
|
|
|
@@ -32,31 +74,186 @@ const props = defineProps({
|
|
|
});
|
|
|
|
|
|
const emit = defineEmits(["ready", "error"]);
|
|
|
+
|
|
|
+const containerRef = ref(null);
|
|
|
const pdfError = ref("");
|
|
|
+const pageWidth = ref(0);
|
|
|
+const pageRatio = ref(DEFAULT_PAGE_RATIO);
|
|
|
+const firstRenderedPage = ref(false);
|
|
|
+const pdfDocument = shallowRef(null);
|
|
|
+let resizeObserver = null;
|
|
|
+let loadingTask = null;
|
|
|
+let loadToken = 0;
|
|
|
|
|
|
-const pdfSource = computed(() => getThumb(props.filesList[0]) || "");
|
|
|
+const pdfUrl = computed(() => getThumb(props.filesList[0]) || "");
|
|
|
+const pdfSource = computed(() => {
|
|
|
+ if (!pdfUrl.value) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
-const handleLoaded = () => {
|
|
|
- pdfError.value = "";
|
|
|
- emit("ready");
|
|
|
-};
|
|
|
+ return {
|
|
|
+ url: pdfUrl.value,
|
|
|
+ disableRange: false,
|
|
|
+ disableStream: true,
|
|
|
+ disableAutoFetch: true,
|
|
|
+ rangeChunkSize: RANGE_CHUNK_SIZE,
|
|
|
+ };
|
|
|
+});
|
|
|
|
|
|
const handleLoadFailed = (error) => {
|
|
|
pdfError.value = error?.message || "PDF 加载失败";
|
|
|
emit("error", error);
|
|
|
};
|
|
|
|
|
|
+const destroyLoadingTask = () => {
|
|
|
+ loadToken += 1;
|
|
|
+ pdfDocument.value = null;
|
|
|
+ firstRenderedPage.value = false;
|
|
|
+
|
|
|
+ if (loadingTask) {
|
|
|
+ const task = loadingTask;
|
|
|
+ loadingTask = null;
|
|
|
+ task.destroy().catch(() => {});
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const loadPdfDocument = async (source) => {
|
|
|
+ destroyLoadingTask();
|
|
|
+ pdfDocument.value = null;
|
|
|
+ pdfError.value = "";
|
|
|
+ firstRenderedPage.value = false;
|
|
|
+
|
|
|
+ if (!source) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentToken = loadToken;
|
|
|
+ loadingTask = getDocument(source);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const doc = await loadingTask.promise;
|
|
|
+ if (currentToken !== loadToken) {
|
|
|
+ doc.destroy?.();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ pdfDocument.value = markRaw(doc);
|
|
|
+ } catch (error) {
|
|
|
+ if (currentToken !== loadToken) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ handleLoadFailed(error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const pageItems = computed(() => {
|
|
|
+ const total = pdfDocument.value?.numPages || 0;
|
|
|
+ return Array.from({ length: total }, (_, index) => ({
|
|
|
+ id: index + 1,
|
|
|
+ pageNumber: index + 1,
|
|
|
+ }));
|
|
|
+});
|
|
|
+
|
|
|
+const estimatedPageHeight = computed(() => {
|
|
|
+ const width = pageWidth.value || 690;
|
|
|
+ return Math.max(560, Math.round(width * pageRatio.value) + 20);
|
|
|
+});
|
|
|
+
|
|
|
+const updatePageWidth = () => {
|
|
|
+ const nextWidth = containerRef.value?.clientWidth || 0;
|
|
|
+ if (nextWidth) {
|
|
|
+ pageWidth.value = nextWidth;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handlePageRendered = (pageNumber) => {
|
|
|
+ if (firstRenderedPage.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ firstRenderedPage.value = true;
|
|
|
+ pdfError.value = "";
|
|
|
+ emit("ready", pageNumber);
|
|
|
+};
|
|
|
+
|
|
|
+const canRenderPage = (item, active) => {
|
|
|
+ if (!active) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return firstRenderedPage.value || item.pageNumber === 1;
|
|
|
+};
|
|
|
+
|
|
|
const handleRenderFailed = (error) => {
|
|
|
pdfError.value = error?.message || "PDF 渲染失败";
|
|
|
emit("error", error);
|
|
|
};
|
|
|
+
|
|
|
+watch(
|
|
|
+ pdfUrl,
|
|
|
+ () => {
|
|
|
+ pdfError.value = "";
|
|
|
+ firstRenderedPage.value = false;
|
|
|
+ },
|
|
|
+ { immediate: true },
|
|
|
+);
|
|
|
+
|
|
|
+watch(pdfSource, loadPdfDocument, { immediate: true });
|
|
|
+
|
|
|
+watch(
|
|
|
+ pdfDocument,
|
|
|
+ async (doc) => {
|
|
|
+ if (!doc) {
|
|
|
+ pageRatio.value = DEFAULT_PAGE_RATIO;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const firstPage = await doc.getPage(1);
|
|
|
+ const viewport = firstPage.getViewport({ scale: 1 });
|
|
|
+ if (viewport.width && viewport.height) {
|
|
|
+ pageRatio.value = viewport.height / viewport.width;
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ pageRatio.value = DEFAULT_PAGE_RATIO;
|
|
|
+ }
|
|
|
+
|
|
|
+ await nextTick();
|
|
|
+ updatePageWidth();
|
|
|
+ },
|
|
|
+ { immediate: true },
|
|
|
+);
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ updatePageWidth();
|
|
|
+
|
|
|
+ if (typeof ResizeObserver !== "undefined" && containerRef.value) {
|
|
|
+ resizeObserver = new ResizeObserver(updatePageWidth);
|
|
|
+ resizeObserver.observe(containerRef.value);
|
|
|
+ } else if (typeof window !== "undefined") {
|
|
|
+ window.addEventListener("resize", updatePageWidth);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeRouteLeave(() => {
|
|
|
+ destroyLoadingTask();
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ destroyLoadingTask();
|
|
|
+ resizeObserver?.disconnect();
|
|
|
+ if (typeof window !== "undefined") {
|
|
|
+ window.removeEventListener("resize", updatePageWidth);
|
|
|
+ }
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.pdf-view {
|
|
|
width: 100%;
|
|
|
- min-height: calc(100vh - 132px);
|
|
|
- overflow: auto;
|
|
|
+ height: calc(100vh - 132px);
|
|
|
+ overflow: hidden;
|
|
|
padding: 20px 14px 32px;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
@@ -70,16 +267,16 @@ const handleRenderFailed = (error) => {
|
|
|
color: #8a8a8a;
|
|
|
}
|
|
|
|
|
|
-.pdf-embed {
|
|
|
- width: 100%;
|
|
|
- border-radius: 10px;
|
|
|
- overflow: hidden;
|
|
|
- background: rgba(255, 255, 255, 0.95);
|
|
|
- box-shadow: 0 8px 24px rgba(76, 48, 34, 0.08);
|
|
|
+.pdf-scroller {
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.pdf-page-item {
|
|
|
+ padding-bottom: 14px;
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
:deep(.vue-pdf-embed__page) {
|
|
|
- margin-bottom: 14px;
|
|
|
box-shadow: 0 8px 20px rgba(75, 50, 40, 0.1);
|
|
|
}
|
|
|
</style>
|