|
|
@@ -0,0 +1,219 @@
|
|
|
+<template>
|
|
|
+ <div class="mobile-pdf-viewer">
|
|
|
+ <div v-if="loading" class="loading-container">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <div class="loading-text">正在加载文档 {{ Math.floor(progress * 100) }}%...</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="error" class="error-container">
|
|
|
+ <div class="error-icon">!</div>
|
|
|
+ <div class="error-text">{{ error }}</div>
|
|
|
+ <button class="retry-btn" @click="loadPdf">重试</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div ref="pdfContainer" class="pdf-container">
|
|
|
+ <!-- Canvas 元素将在这里动态生成 -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted, watch, onUnmounted } from 'vue';
|
|
|
+import * as pdfjsLib from 'pdfjs-dist';
|
|
|
+
|
|
|
+// 设置 worker
|
|
|
+// 注意:在 Vite 中,我们需要正确引入 worker
|
|
|
+// 使用 CDN 或者本地构建的 worker
|
|
|
+// 这里尝试使用 import 方式,如果失败则回退到 CDN
|
|
|
+try {
|
|
|
+ // 尝试动态导入 worker
|
|
|
+ const workerUrl = new URL('pdfjs-dist/build/pdf.worker.min.js', import.meta.url).href;
|
|
|
+ pdfjsLib.GlobalWorkerOptions.workerSrc = workerUrl;
|
|
|
+} catch (e) {
|
|
|
+ console.warn('无法本地加载 PDF worker,尝试使用 CDN');
|
|
|
+ pdfjsLib.GlobalWorkerOptions.workerSrc = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;
|
|
|
+}
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ url: {
|
|
|
+ type: String,
|
|
|
+ required: true
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+const loading = ref(true);
|
|
|
+const error = ref(null);
|
|
|
+const progress = ref(0);
|
|
|
+const pdfContainer = ref(null);
|
|
|
+let pdfDoc = null;
|
|
|
+
|
|
|
+const loadPdf = async () => {
|
|
|
+ if (!props.url) return;
|
|
|
+
|
|
|
+ loading.value = true;
|
|
|
+ error.value = null;
|
|
|
+ progress.value = 0;
|
|
|
+
|
|
|
+ // 清空容器
|
|
|
+ if (pdfContainer.value) {
|
|
|
+ pdfContainer.value.innerHTML = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const loadingTask = pdfjsLib.getDocument(props.url);
|
|
|
+
|
|
|
+ loadingTask.onProgress = (p) => {
|
|
|
+ if (p.total > 0) {
|
|
|
+ progress.value = p.loaded / p.total;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ pdfDoc = await loadingTask.promise;
|
|
|
+ console.log(`PDF 加载成功,共 ${pdfDoc.numPages} 页`);
|
|
|
+
|
|
|
+ // 渲染所有页面
|
|
|
+ for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) {
|
|
|
+ await renderPage(pageNum);
|
|
|
+ }
|
|
|
+
|
|
|
+ loading.value = false;
|
|
|
+ } catch (err) {
|
|
|
+ console.error('PDF 加载失败:', err);
|
|
|
+ error.value = '文档加载失败,请稍后重试或尝试下载查看';
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const renderPage = async (pageNum) => {
|
|
|
+ try {
|
|
|
+ const page = await pdfDoc.getPage(pageNum);
|
|
|
+
|
|
|
+ // 计算缩放比例,使页面宽度适应屏幕
|
|
|
+ const containerWidth = pdfContainer.value ? pdfContainer.value.clientWidth : window.innerWidth;
|
|
|
+ // 默认使用 1.5 倍缩放以获得更好的清晰度,然后通过 CSS 缩小
|
|
|
+ const viewport = page.getViewport({ scale: 1.5 });
|
|
|
+
|
|
|
+ // 创建 canvas
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
+ const context = canvas.getContext('2d');
|
|
|
+
|
|
|
+ // 设置 canvas 尺寸
|
|
|
+ canvas.height = viewport.height;
|
|
|
+ canvas.width = viewport.width;
|
|
|
+
|
|
|
+ // 设置 CSS 样式以适应容器宽度
|
|
|
+ canvas.style.width = '100%';
|
|
|
+ canvas.style.height = 'auto';
|
|
|
+ canvas.style.display = 'block';
|
|
|
+ canvas.style.marginBottom = '10px';
|
|
|
+ canvas.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
|
|
|
+
|
|
|
+ if (pdfContainer.value) {
|
|
|
+ pdfContainer.value.appendChild(canvas);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染页面
|
|
|
+ const renderContext = {
|
|
|
+ canvasContext: context,
|
|
|
+ viewport: viewport
|
|
|
+ };
|
|
|
+
|
|
|
+ await page.render(renderContext).promise;
|
|
|
+ } catch (err) {
|
|
|
+ console.error(`渲染第 ${pageNum} 页失败:`, err);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+watch(() => props.url, () => {
|
|
|
+ loadPdf();
|
|
|
+});
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ // 稍微延迟加载,确保容器已准备好
|
|
|
+ setTimeout(() => {
|
|
|
+ loadPdf();
|
|
|
+ }, 100);
|
|
|
+});
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (pdfDoc) {
|
|
|
+ pdfDoc.destroy();
|
|
|
+ }
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.mobile-pdf-viewer {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 300px;
|
|
|
+ background-color: #f3f4f6;
|
|
|
+ position: relative;
|
|
|
+ overflow-y: auto;
|
|
|
+ -webkit-overflow-scrolling: touch;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-container, .error-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 40px 20px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ border: 4px solid #e5e7eb;
|
|
|
+ border-top: 4px solid #3b82f6;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-text {
|
|
|
+ color: #6b7280;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.error-icon {
|
|
|
+ width: 48px;
|
|
|
+ height: 48px;
|
|
|
+ background-color: #fee2e2;
|
|
|
+ color: #ef4444;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.error-text {
|
|
|
+ color: #374151;
|
|
|
+ font-size: 15px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.retry-btn {
|
|
|
+ padding: 8px 24px;
|
|
|
+ background-color: #3b82f6;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.pdf-container {
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+</style>
|