XieXing 4 месяцев назад
Родитель
Сommit
6db10793d9

+ 219 - 0
shudao-vue-frontend/src/components/MobilePdfViewer.vue

@@ -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>

+ 3 - 3
shudao-vue-frontend/src/views/mobile/m-HazardDetection.vue

@@ -1377,16 +1377,16 @@ const openExampleModal = async (hazardInfo) => {
       const exampleData = response.data;
       
       // 检查是否有示例图数据
-      if (exampleData && (exampleData.correct_example_image || exampleData.error_example_image)) {
+      if (exampleData && (exampleData.correct_example_image || exampleData.wrong_example_image)) {
         exampleImages.value = {
           correctImageUrl: exampleData.correct_example_image || '',
-          errorImageUrl: exampleData.error_example_image || ''
+          errorImageUrl: exampleData.wrong_example_image || ''
         };
         
         // 设置图片加载状态
         imageLoadingStates.value = {
           correct: !!exampleData.correct_example_image,
-          error: !!exampleData.error_example_image
+          error: !!exampleData.wrong_example_image
         };
         
         showExampleModal.value = true;

+ 16 - 1
shudao-vue-frontend/src/views/mobile/m-PolicyDocument.vue

@@ -171,8 +171,15 @@
                     <button class="close-btn" @click="closePreview">×</button>
                 </div>
                 <div class="preview-body">
+                    <!-- PDF/TXT/其他文件使用自定义渲染器 -->
+                    <MobilePdfViewer
+                        v-if="previewUrl && isPdfType"
+                        :url="previewUrl"
+                        class="preview-frame"
+                    />
+                    <!-- Office文档使用微软在线预览 -->
                     <iframe
-                        v-if="previewUrl"
+                        v-else-if="previewUrl"
                         :src="previewUrl"
                         class="preview-frame"
                         frameborder="0"
@@ -189,6 +196,7 @@
 import { ref, computed, onMounted, onUnmounted } from "vue";
 import { useRouter } from "vue-router";
 import MobileHeader from "@/components/MobileHeader.vue";
+import MobilePdfViewer from "@/components/MobilePdfViewer.vue";
 import { apis } from "@/request/apis.js";
 
 const router = useRouter();
@@ -347,6 +355,13 @@ const previewTitle = computed(
     () => previewFile.value?.policy_name || "政策文件预览"
 );
 
+const isPdfType = computed(() => {
+    if (!previewFile.value) return false;
+    const fileType = previewFile.value.file_type;
+    // 0: PDF, 4: TXT, 5: 其他 (通常也是PDF或图片)
+    return fileType === 0 || fileType === 4 || fileType === 5;
+});
+
 const closePreview = () => {
     previewVisible.value = false;
     previewFile.value = null;