LAPTOP-1LHD2FP6\joeny пре 5 месеци
родитељ
комит
c2cff7b774
2 измењених фајлова са 505 додато и 22 уклоњено
  1. 390 0
      src/components/DocumentPreview/index.vue
  2. 115 22
      src/views/report.vue

+ 390 - 0
src/components/DocumentPreview/index.vue

@@ -0,0 +1,390 @@
+<template>
+  <div class="document-preview">
+    <!-- 文档列表 -->
+    <div class="document-list">
+      <div v-if="documents.length === 0" class="empty-state">
+        <FileOutlined class="empty-icon" />
+        <p>暂无文档</p>
+      </div>
+      <div v-else v-for="doc in documents" :key="doc.name" class="document-item">
+        <div class="document-info" @click="handleSelect(doc)">
+          <!-- 文档图标 -->
+          <div class="doc-icon">
+            <img 
+              v-if="isImageFile(doc)"
+              :src="doc.url" 
+              class="preview-thumbnail"
+              alt="preview"
+            />
+            <video
+              v-else-if="isVideoFile(doc)"
+              :src="doc.url"
+              class="preview-thumbnail"
+              preload="metadata"
+            />
+            <img 
+              v-else 
+              :src="getFileIcon(doc)" 
+              alt="file"
+              class="file-icon"
+            />
+          </div>
+          
+          <!-- 文档信息 -->
+          <div class="info">
+            <div class="name" :title="doc.name">{{ doc.name }}</div>
+            <div class="size">{{ doc.sizeFormatted || formatFileSize(doc.size) }}</div>
+          </div>
+        </div>
+
+        <!-- 操作按钮 -->
+        <div class="actions">
+          <button class="action-btn" @click="handlePreview(doc)" title="预览">
+            <EyeOutlined />
+          </button>
+          <button class="action-btn" @click="handleDownload(doc)" title="下载">
+            <DownloadOutlined />
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 预览弹窗 -->
+    <div v-if="previewVisible" class="preview-modal" @click="closePreview">
+      <div class="preview-content" @click.stop>
+        <div class="preview-header">
+          <span>{{ currentDoc?.name }}</span>
+          <button class="close-btn" @click="closePreview">
+            <CloseOutlined />
+          </button>
+        </div>
+        <div class="preview-body">
+          <!-- 图片预览 -->
+          <img 
+            v-if="isImageFile(currentDoc)"
+            :src="currentDoc?.url" 
+            class="preview-image"
+            alt="preview"
+          />
+          <!-- 视频预览 -->
+          <video
+            v-else-if="isVideoFile(currentDoc)"
+            :src="currentDoc?.url"
+            controls
+            class="preview-video"
+          />
+          <!-- 其他文件预览 -->
+          <div v-else class="file-preview">
+            <FileOutlined class="large-icon" />
+            <p>该文件类型暂不支持预览</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { defineComponent, ref } from 'vue';
+import { 
+  FileOutlined, 
+  EyeOutlined, 
+  DownloadOutlined,
+  CloseOutlined
+} from '@ant-design/icons-vue';
+
+export default defineComponent({
+  name: 'DocumentPreview',
+  components: {
+    FileOutlined,
+    EyeOutlined,
+    DownloadOutlined,
+    CloseOutlined
+  },
+  props: {
+    documents: {
+      type: Array,
+      default: () => []
+    }
+  },
+  emits: ['select'],
+  setup(props, { emit }) {
+    const previewVisible = ref(false);
+    const currentDoc = ref(null);
+
+    const isImageFile = (file) => {
+      if (!file) return false;
+      const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
+      const ext = file.name?.split('.')?.pop()?.toLowerCase() || '';
+      return imageTypes.includes(ext);
+    };
+
+    const isVideoFile = (file) => {
+      if (!file) return false;
+      const videoTypes = ['mp4', 'webm', 'ogg'];
+      const ext = file.name?.split('.')?.pop()?.toLowerCase() || '';
+      return videoTypes.includes(ext);
+    };
+
+    const getFileIcon = (file) => {
+      // 根据文件类型返回对应的图标
+      return '/src/assets/svg/word.svg'; // 默认文档图标
+    };
+
+    const formatFileSize = (bytes) => {
+      if (!bytes || isNaN(bytes)) return '0 B';
+      const k = 1024;
+      const sizes = ['B', 'KB', 'MB', 'GB'];
+      const i = Math.floor(Math.log(bytes) / Math.log(k));
+      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+    };
+
+    const handleSelect = (doc) => {
+      emit('select', doc);
+    };
+
+    const handlePreview = (doc) => {
+      currentDoc.value = doc;
+      previewVisible.value = true;
+    };
+
+    const handleDownload = (doc) => {
+      if (doc.url) {
+        const link = document.createElement('a');
+        link.href = doc.url;
+        link.download = doc.name;
+        document.body.appendChild(link);
+        link.click();
+        document.body.removeChild(link);
+      }
+    };
+
+    const closePreview = () => {
+      previewVisible.value = false;
+      currentDoc.value = null;
+    };
+
+    return {
+      previewVisible,
+      currentDoc,
+      isImageFile,
+      isVideoFile,
+      getFileIcon,
+      formatFileSize,
+      handleSelect,
+      handlePreview,
+      handleDownload,
+      closePreview
+    };
+  }
+});
+</script>
+
+<style lang="less" scoped>
+.document-preview {
+  height: 100%;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.document-list {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 48px 0;
+  color: rgba(0, 0, 0, 0.45);
+
+  .empty-icon {
+    font-size: 48px;
+    margin-bottom: 16px;
+  }
+
+  p {
+    font-size: 14px;
+    margin: 0;
+  }
+}
+
+.document-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px;
+  border-radius: 8px;
+  margin-bottom: 8px;
+  background: #fff;
+  transition: all 0.3s;
+
+  &:hover {
+    background: #f5f5f5;
+  }
+}
+
+.document-info {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex: 1;
+  cursor: pointer;
+}
+
+.doc-icon {
+  width: 40px;
+  height: 40px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.preview-thumbnail {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  border-radius: 4px;
+}
+
+.file-icon {
+  width: 24px;
+  height: 24px;
+}
+
+.info {
+  flex: 1;
+  min-width: 0;
+
+  .name {
+    font-size: 14px;
+    color: rgba(0, 0, 0, 0.85);
+    margin-bottom: 4px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+  }
+
+  .size {
+    font-size: 12px;
+    color: rgba(0, 0, 0, 0.45);
+  }
+}
+
+.actions {
+  display: flex;
+  gap: 8px;
+}
+
+.action-btn {
+  width: 32px;
+  height: 32px;
+  border: none;
+  background: transparent;
+  border-radius: 4px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: rgba(0, 0, 0, 0.45);
+  transition: all 0.3s;
+
+  &:hover {
+    background: rgba(0, 0, 0, 0.04);
+    color: #1677ff;
+  }
+}
+
+.preview-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.preview-content {
+  background: #fff;
+  border-radius: 8px;
+  width: 90%;
+  max-width: 800px;
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.preview-header {
+  padding: 16px;
+  border-bottom: 1px solid #f0f0f0;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  span {
+    font-size: 16px;
+    font-weight: 500;
+  }
+
+  .close-btn {
+    border: none;
+    background: transparent;
+    cursor: pointer;
+    color: rgba(0, 0, 0, 0.45);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 8px;
+    border-radius: 4px;
+    transition: all 0.3s;
+
+    &:hover {
+      background: rgba(0, 0, 0, 0.04);
+      color: rgba(0, 0, 0, 0.88);
+    }
+  }
+}
+
+.preview-body {
+  flex: 1;
+  overflow: auto;
+  padding: 24px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.preview-image {
+  max-width: 100%;
+  max-height: 70vh;
+  object-fit: contain;
+}
+
+.preview-video {
+  max-width: 100%;
+  max-height: 70vh;
+}
+
+.file-preview {
+  text-align: center;
+  color: rgba(0, 0, 0, 0.45);
+
+  .large-icon {
+    font-size: 64px;
+    margin-bottom: 16px;
+  }
+
+  p {
+    margin: 0;
+    font-size: 14px;
+  }
+}
+</style> 

+ 115 - 22
src/views/report.vue

@@ -438,30 +438,64 @@
         />
       </div>
       <div class="right-menu-content">
-        <div class="search-header">
-          <span>搜索结果</span>
-          <div class="web-search-toggle">
-            <input
-              type="checkbox"
-              id="searchToggle"
-              v-model="enableWebSearch"
-              class="toggle-input"
-              @change="handleSearchToggle"
-            />
-            <label for="searchToggle" class="toggle-label">
-              {{ enableWebSearch ? '搜索已开启' : '搜索已关闭' }}
-            </label>
+        <!-- 添加 Tab 切换 -->
+        <div class="right-menu-tabs">
+          <div 
+            class="tab-item" 
+            :class="{ active: activeTab === 'search' }"
+            @click="activeTab = 'search'"
+          >
+            <FileSearchOutlined />
+            <span>搜索</span>
+          </div>
+          <div 
+            class="tab-item" 
+            :class="{ active: activeTab === 'preview' }"
+            @click="activeTab = 'preview'"
+          >
+            <FileOutlined />
+            <span>文档预览</span>
           </div>
         </div>
-        <SearchResults
-          v-if="enableWebSearch"
-          :searchResults="searchResults"
-          :loading="isSearchLoading"
-          :total="searchTotal"
-          :enableWebSearch="enableWebSearch"
-          :message="currentSearchQuery"
-          @update:enableWebSearch="handleSearchToggle"
-        />
+
+        <!-- 搜索面板 -->
+        <div v-show="activeTab === 'search'" class="panel-container">
+          <div class="search-header">
+            <span>搜索结果</span>
+            <div class="web-search-toggle">
+              <input
+                type="checkbox"
+                id="searchToggle"
+                v-model="enableWebSearch"
+                class="toggle-input"
+                @change="handleSearchToggle"
+              />
+              <label for="searchToggle" class="toggle-label">
+                {{ enableWebSearch ? '搜索已开启' : '搜索已关闭' }}
+              </label>
+            </div>
+          </div>
+          <SearchResults
+            v-if="enableWebSearch"
+            :searchResults="searchResults"
+            :loading="isSearchLoading"
+            :total="searchTotal"
+            :enableWebSearch="enableWebSearch"
+            :message="currentSearchQuery"
+            @update:enableWebSearch="handleSearchToggle"
+          />
+        </div>
+
+        <!-- 文档预览面板 -->
+        <div v-show="activeTab === 'preview'" class="panel-container">
+          <div class="preview-header">
+            <span>文档预览</span>
+          </div>
+          <DocumentPreview 
+            :documents="attachedFiles"
+            @select="handleDocumentSelect"
+          />
+        </div>
       </div>
     </div>
   </div>
@@ -507,6 +541,7 @@ import VoiceConfig from "../components/VoiceConfig/index.vue";
 import KnowledgeConfig from "../components/KnowledgeConfig/index.vue";
 import ExportButton from "../components/ExportButton/index.vue";
 import dayjs from 'dayjs';
+import DocumentPreview from "../components/DocumentPreview/index.vue";
 
 // 初始化 markdown-it
 const md = new MarkdownIt({
@@ -1317,6 +1352,14 @@ const handleSearchToggle = (value) => {
   }
 };
 
+// 添加新的响应式变量
+const activeTab = ref('search'); // 默认显示搜索面板
+
+// 处理文档选择
+const handleDocumentSelect = (document) => {
+  // 处理文档选择逻辑
+  console.log('Selected document:', document);
+};
 </script>
 
 <style scoped>
@@ -2987,6 +3030,56 @@ button,
   color: rgba(0, 0, 0, 0.88);
   cursor: pointer;
 }
+
+/* 添加 Tab 样式 */
+.right-menu-tabs {
+  display: flex;
+  border-bottom: 1px solid #f0f0f0;
+  background: #fff;
+}
+
+.tab-item {
+  flex: 1;
+  padding: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  cursor: pointer;
+  transition: all 0.3s;
+  border-bottom: 2px solid transparent;
+  color: rgba(0, 0, 0, 0.65);
+}
+
+.tab-item:hover {
+  color: #1677ff;
+}
+
+.tab-item.active {
+  color: #1677ff;
+  border-bottom-color: #1677ff;
+  background: #f6f9fe;
+}
+
+.panel-container {
+  height: calc(100% - 45px); /* 减去 tab 的高度 */
+  overflow-y: auto;
+}
+
+/* 修改右侧菜单内容样式 */
+.right-menu-content {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 预览面板样式 */
+.preview-header {
+  padding: 16px;
+  border-bottom: 1px solid #f0f0f0;
+  font-size: 16px;
+  font-weight: bold;
+}
 </style>
 <style lang="less" scoped>
 ::v-deep .message-content {