Explorar el Código

Update:更新移动端样式

XieXing hace 3 meses
padre
commit
b5da40438e

+ 167 - 0
shudao-vue-frontend/src/components/MobileFileReportCard.vue

@@ -0,0 +1,167 @@
+<template>
+  <div class="mobile-file-report-card" @click="openFile">
+    <!-- 文件信息头部 -->
+    <div class="card-header">
+      <div class="file-icon-wrapper">
+        <svg class="file-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
+          <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />
+        </svg>
+      </div>
+      <div class="file-details">
+        <div class="file-name">{{ report.report?.display_name || report.source_file }}</div>
+        <div class="file-meta-row">
+          <span v-if="report.metadata?.primary_category" class="category-tag">
+            <svg class="tag-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
+              <path d="M5.5,7A1.5,1.5 0 0,1 4,5.5A1.5,1.5 0 0,1 5.5,4A1.5,1.5 0 0,1 7,5.5A1.5,1.5 0 0,1 5.5,7M21.41,11.58L12.41,2.58C12.05,2.22 11.55,2 11,2H4C2.89,2 2,2.89 2,4V11C2,11.55 2.22,12.05 2.59,12.41L11.58,21.41C11.95,21.77 12.45,22 13,22C13.55,22 14.05,21.77 14.41,21.41L21.41,14.41C21.78,14.05 22,13.55 22,13C22,12.44 21.77,11.94 21.41,11.58Z" />
+            </svg>
+            {{ report.metadata.primary_category }}
+          </span>
+          <span class="view-count">
+            <svg class="view-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
+              <path d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z" />
+            </svg>
+            {{ report.metadata?.view_count || 0 }} 次查看
+          </span>
+          <span class="view-detail">
+            查看详情 &gt;
+          </span>
+        </div>
+      </div>
+      <div class="file-date">{{ formatDate(report.metadata?.upload_date) }}</div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+  report: {
+    type: Object,
+    required: true
+  }
+})
+
+const emit = defineEmits(['preview-file'])
+
+const formatDate = (dateStr) => {
+  if (!dateStr) return ''
+  const date = new Date(dateStr)
+  if (isNaN(date.getTime())) return dateStr
+  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
+}
+
+const openFile = () => {
+  if (props.report.file_path) {
+    const fileName = props.report.report?.display_name || props.report.source_file || '未命名文件'
+    emit('preview-file', {
+      filePath: props.report.file_path,
+      fileName: fileName
+    })
+  }
+}
+</script>
+
+<style scoped>
+.mobile-file-report-card {
+  background: white;
+  border-radius: 12px;
+  padding: 16px;
+  margin-bottom: 12px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.mobile-file-report-card:active {
+  background: #f8f9fa;
+}
+
+.card-header {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+}
+
+.file-icon-wrapper {
+  flex-shrink: 0;
+  width: 40px;
+  height: 40px;
+  background: #fee2e2;
+  border-radius: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.file-icon {
+  width: 24px;
+  height: 24px;
+  color: #dc2626;
+}
+
+.file-details {
+  flex: 1;
+  min-width: 0;
+}
+
+.file-name {
+  font-size: 15px;
+  font-weight: 600;
+  color: #1f2937;
+  line-height: 1.4;
+  margin-bottom: 8px;
+  word-break: break-word;
+}
+
+.file-meta-row {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+
+.category-tag {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  padding: 4px 10px;
+  background: #dbeafe;
+  color: #1d4ed8;
+  border-radius: 12px;
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.tag-icon {
+  width: 12px;
+  height: 12px;
+}
+
+.view-count {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  color: #6b7280;
+  font-size: 12px;
+}
+
+.view-icon {
+  width: 14px;
+  height: 14px;
+}
+
+.view-detail {
+  color: #3b82f6;
+  font-size: 12px;
+  font-weight: 500;
+  margin-left: auto;
+}
+
+.file-date {
+  flex-shrink: 0;
+  color: #9ca3af;
+  font-size: 12px;
+  white-space: nowrap;
+}
+</style>

+ 83 - 7
shudao-vue-frontend/src/components/MobilePdfViewer.vue

@@ -17,13 +17,13 @@
     </div>
 
     <div ref="pdfContainer" class="pdf-container">
-      <!-- Canvas 元素将在这里动态生成 -->
+      <!-- Canvas 元素将在这里动态生成,每个页面包含水印覆盖层 -->
     </div>
   </div>
 </template>
 
 <script setup>
-import { ref, onMounted, watch, onUnmounted } from 'vue';
+import { ref, computed, onMounted, watch, onUnmounted } from 'vue';
 import * as pdfjsLib from 'pdfjs-dist';
 
 // 设置 worker
@@ -39,17 +39,29 @@ const props = defineProps({
   url: {
     type: String,
     required: true
+  },
+  watermarkConfig: {
+    type: Object,
+    default: null
+    // { username: string, account: string, date: string }
   }
 });
 
+// 计算水印文本
+const watermarkText = computed(() => {
+  if (!props.watermarkConfig) return ''
+  const { username, account, date } = props.watermarkConfig
+  return `${username || ''} ${account || ''} ${date || ''}`.trim()
+})
+
 const loading = ref(true);
 const error = ref(null);
 const progress = ref(0);
 const pdfContainer = ref(null);
 const currentUrl = ref('');
-const isLoading = ref(false); // 防止重复加载的标志
-const retryCount = ref(0); // 重试次数
-const maxRetries = 3; // 最大重试次数
+const isLoading = ref(false);
+const retryCount = ref(0);
+const maxRetries = 3;
 let pdfDoc = null;
 
 const loadPdf = async (isRetry = false) => {
@@ -98,6 +110,8 @@ const loadPdf = async (isRetry = false) => {
     if (pdfDoc.numPages > 0) {
       // 第一页优先加载
       await renderPage(1);
+      // 第一页渲染完成后立即隐藏加载提示
+      loading.value = false;
       progress.value = Math.floor((1 / pdfDoc.numPages) * 100);
       
       // 加载剩余页面
@@ -141,6 +155,12 @@ const renderPage = async (pageNum) => {
     const page = await pdfDoc.getPage(pageNum);
     const viewport = page.getViewport({ scale: 1.5 });
     
+    // 创建页面容器
+    const pageWrapper = document.createElement('div');
+    pageWrapper.className = 'page-wrapper';
+    pageWrapper.style.position = 'relative';
+    pageWrapper.style.marginBottom = '10px';
+    
     const canvas = document.createElement('canvas');
     const context = canvas.getContext('2d');
     
@@ -149,11 +169,27 @@ const renderPage = async (pageNum) => {
     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)';
     
+    pageWrapper.appendChild(canvas);
+    
+    // 添加水印覆盖层
+    if (props.watermarkConfig && watermarkText.value) {
+      console.log('📄 [MobilePdfViewer] 添加水印:', watermarkText.value);
+      const watermarkOverlay = document.createElement('div');
+      watermarkOverlay.className = 'watermark-overlay';
+      watermarkOverlay.innerHTML = `
+        <div class="watermark-grid">
+          ${Array(15).fill(`<span class="watermark-item">${watermarkText.value}</span>`).join('')}
+        </div>
+      `;
+      pageWrapper.appendChild(watermarkOverlay);
+    } else {
+      console.log('📄 [MobilePdfViewer] 无水印配置:', { watermarkConfig: props.watermarkConfig, watermarkText: watermarkText.value });
+    }
+    
     if (pdfContainer.value) {
-      pdfContainer.value.appendChild(canvas);
+      pdfContainer.value.appendChild(pageWrapper);
     }
     
     const renderContext = {
@@ -319,3 +355,43 @@ onUnmounted(() => {
   100% { transform: rotate(360deg); }
 }
 </style>
+
+<style>
+/* 水印覆盖层样式 - 非scoped以支持动态创建的DOM */
+.mobile-pdf-viewer .page-wrapper {
+  position: relative;
+}
+
+.mobile-pdf-viewer .watermark-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+  z-index: 10;
+  overflow: hidden;
+}
+
+.mobile-pdf-viewer .watermark-grid {
+  width: 100%;
+  height: 100%;
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  grid-template-rows: repeat(5, 1fr);
+  gap: 20px;
+  padding: 30px;
+  transform: rotate(-30deg) scale(1.5);
+  transform-origin: center center;
+}
+
+.mobile-pdf-viewer .watermark-item {
+  color: rgba(120, 120, 120, 0.15);
+  font-size: 13px;
+  white-space: nowrap;
+  user-select: none;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+</style>

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

@@ -432,6 +432,7 @@
           <MobilePdfViewer
             v-else-if="previewFilePath"
             :url="previewFilePath"
+            :watermark-config="previewWatermarkConfig"
           />
           <div v-else class="file-empty">
             <svg class="empty-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
@@ -469,7 +470,7 @@ import { createSSEConnection, closeSSEConnection } from '@/utils/sse'
 import { getApiPrefix, getReportApiPrefix, BACKEND_API_PREFIX } from '@/utils/apiConfig'
 import { renderMarkdown } from '@/utils/markdown'
 import { stopSSEStream, updateAIMessageContent } from '@/utils/api.js'
-import { getToken, getTokenType } from '@/utils/auth.js'
+import { getToken, getTokenType, getUserName, getAccountId } from '@/utils/auth.js'
 import { initNativeNavForSubPage } from '@/utils/nativeBridge.js'
 import Vditor from 'vditor'
 import 'vditor/dist/index.css'
@@ -549,6 +550,7 @@ const previewTitle = ref('')
 const showFilePreview = ref(false)
 const previewFilePath = ref('')
 const previewFileName = ref('')
+const previewWatermarkConfig = ref(null)
 const fileLoading = ref(false)
 const fileError = ref('')
 
@@ -2107,14 +2109,13 @@ const openInNewTab = () => {
 const handleFilePreview = (data) => {
   // 重置状态
   fileError.value = ''
-  fileLoading.value = false  // 先设为false,让组件能够渲染
+  fileLoading.value = false
 
   // 处理不同类型的输入参数
   if (typeof data === 'string') {
     previewFilePath.value = data
     previewFileName.value = data
   } else if (data && data.filePath) {
-    // file_path 本身就是加密的URL,可以直接使用
     previewFilePath.value = data.filePath
     previewFileName.value = data.fileName || data.filePath
   } else {
@@ -2123,6 +2124,14 @@ const handleFilePreview = (data) => {
     previewFileName.value = ''
   }
   
+  // 设置水印配置
+  const now = new Date()
+  previewWatermarkConfig.value = {
+    username: getUserName() || '用户',
+    account: getAccountId() || '',
+    date: `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`
+  }
+  
   showFilePreview.value = true
 }
 

+ 48 - 87
shudao-vue-frontend/src/views/mobile/m-PolicyDocument.vue

@@ -121,35 +121,20 @@
                                 />
                                 {{ tag }}
                             </span>
-                        </div>
-                        <p class="doc-description">{{ file.policy_content }}</p>
-                        <div class="doc-footer">
-                            <div class="doc-info">
-                                <span class="info-item">
-                                    <img
-                                        src="@/assets/Policy/5.png"
-                                        alt="部门"
-                                        class="info-icon"
-                                    />
-                                    {{ file.policy_department }}
-                                </span>
-                                <span class="info-item">
-                                    <img
-                                        src="@/assets/Policy/6.png"
-                                        alt="次数"
-                                        class="info-icon"
-                                    />
-                                    {{ file.view_count }} 次查看
-                                </span>
-                            </div>
-                            <div class="doc-actions">
-                                <button
-                                    class="action-btn view-btn"
-                                    @click="viewPolicy(file)"
-                                >
-                                    查看详情 &gt;
-                                </button>
-                            </div>
+                            <span class="info-item view-count">
+                                <img
+                                    src="@/assets/Policy/6.png"
+                                    alt="次数"
+                                    class="info-icon"
+                                />
+                                {{ file.view_count }} 次查看
+                            </span>
+                            <button
+                                class="action-btn view-btn"
+                                @click="viewPolicy(file)"
+                            >
+                                查看详情 &gt;
+                            </button>
                         </div>
                     </div>
                 </div>
@@ -175,6 +160,7 @@
                     <MobilePdfViewer
                         v-if="previewUrl && isPdfType"
                         :url="previewUrl"
+                        :watermark-config="previewWatermarkConfig"
                         class="preview-frame"
                     />
                     <!-- Office文档使用微软在线预览 -->
@@ -199,6 +185,7 @@ import MobileHeader from "@/components/MobileHeader.vue";
 import MobilePdfViewer from "@/components/MobilePdfViewer.vue";
 import { apis } from "@/request/apis.js";
 import { initNativeNavForSubPage } from '@/utils/nativeBridge.js';
+import { getUserName, getAccountId } from '@/utils/auth.js';
 
 const router = useRouter();
 
@@ -214,6 +201,16 @@ const documentList = ref(null);
 const previewVisible = ref(false);
 const previewFile = ref(null);
 
+// 水印配置
+const previewWatermarkConfig = computed(() => {
+    const now = new Date();
+    return {
+        username: getUserName() || '用户',
+        account: getAccountId() || '',
+        date: `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`
+    };
+});
+
 let searchTimer = null;
 let scrollTimer = null;
 
@@ -622,67 +619,31 @@ onUnmounted(() => {
                     flex-shrink: 0;
                 }
             }
-        }
-
-        .doc-description {
-            font-size: 13px;
-            color: #6b7280;
-            line-height: 1.5;
-            margin: 0 0 6px 0;
-            display: -webkit-box;
-            line-clamp: 2;
-            -webkit-line-clamp: 2;
-            -webkit-box-orient: vertical;
-            overflow: hidden;
-        }
-
-        .doc-footer {
-            display: flex;
-            justify-content: space-between;
-            align-items: center;
-
-            .doc-info {
+            
+            .view-count {
                 display: flex;
-                gap: 16px;
-
-                .info-item {
-                    display: flex;
-                    align-items: center;
-                    gap: 6px;
-                    font-size: 12px;
-                    color: #6b7280;
-
-                    .info-icon {
-                        width: 14px;
-                        height: 14px;
-                        opacity: 0.6;
-                    }
+                align-items: center;
+                gap: 4px;
+                font-size: 12px;
+                color: #6b7280;
+                margin-left: auto;
+                
+                .info-icon {
+                    width: 14px;
+                    height: 14px;
+                    opacity: 0.6;
                 }
             }
-
-            .doc-actions {
-                display: flex;
-
-                .action-btn:first-child {
-                    margin-right: 6px;
-                }
-
-                .action-btn {
-                    padding: 4px 0;
-                    border: none;
-                    border-radius: 6px;
-                    font-size: 13px;
-                    cursor: pointer;
-                    transition: all 0.3s ease;
-                    display: flex;
-                    align-items: center;
-                    gap: 6px;
-
-                    &.view-btn {
-                        color: #3b82f6;
-                        background: transparent;
-                    }
-                }
+            
+            .view-btn {
+                padding: 4px 0;
+                border: none;
+                font-size: 13px;
+                cursor: pointer;
+                color: #3b82f6;
+                background: transparent;
+                white-space: nowrap;
+                margin-left: 12px;
             }
         }
     }