Преглед изворни кода

Merge branch 'dev-xym-add-pdf' of algorithm/rag-web into master

xueyiming пре 1 месец
родитељ
комит
dd7dd3156d
4 измењених фајлова са 841 додато и 170 уклоњено
  1. 205 11
      src/views/KnowledgeContent.vue
  2. 238 25
      src/views/QAndA.vue
  3. 157 102
      src/views/QAndAHistory.vue
  4. 241 32
      src/views/SearchPage.vue

+ 205 - 11
src/views/KnowledgeContent.vue

@@ -65,6 +65,8 @@
               </div>
               <div class="document-info">
                 <span class="document-name">{{ item.title || item.text }}</span>
+                <span v-if="item.textType === 3" class="document-type">PDF文档</span>
+                <span v-else class="document-type">文本文档</span>
               </div>
               <div class="document-actions">
                 <el-tooltip content="删除文档" placement="top">
@@ -104,13 +106,16 @@
           <div class="content-header">
             <div class="content-title">
               <h2>{{ selectedTitle || '选择文档查看内容' }}</h2>
-              <span v-if="statusDesc" class="content-status"
-                    :class="statusDesc === '可用' ? 'available' : 'unavailable'">
-                {{ statusDesc }}
-              </span>
+              <div class="content-meta">
+                <span v-if="statusDesc" class="content-status"
+                      :class="statusDesc === '可用' ? 'available' : 'unavailable'">
+                  {{ statusDesc }}
+                </span>
+                <span v-if="selectedTextType === 3" class="content-type pdf-type">PDF文档</span>
+                <span v-else class="content-type text-type">文本文档</span>
+              </div>
             </div>
             <el-button
-                v-if="selectedContent"
                 type="primary"
                 class="chunk-btn"
                 @click="openChunkDialog"
@@ -118,12 +123,44 @@
               <span class="button-icon">📖</span>
               查看分段
             </el-button>
+            <el-button
+                v-if="selectedTextType === 3 && selectedUrl"
+                type="primary"
+                class="view-pdf-btn"
+                @click="openPdfInNewTab"
+            >
+              <span class="button-icon">👀</span>
+              在新窗口查看PDF
+            </el-button>
           </div>
 
           <div class="content-body">
-            <div v-if="selectedContent" class="content-text">
+            <!-- PDF 内容展示 -->
+            <div v-if="selectedTextType === 3" class="pdf-content">
+              <div v-if="selectedUrl" class="pdf-viewer-container">
+                <iframe
+                    :src="selectedUrl"
+                    class="pdf-viewer"
+                    frameborder="0"
+                    @load="onPdfLoad"
+                    @error="onPdfError"
+                ></iframe>
+                <!--                <div v-if="pdfLoading" class="pdf-loading">-->
+                <!--                  <div class="loading-spinner"></div>-->
+                <!--                  <p>正在加载PDF文档...</p>-->
+                <!--                </div>-->
+              </div>
+              <div v-else class="pdf-empty">
+                <div class="empty-icon">📄</div>
+                <h3>PDF文档不可用</h3>
+                <p>无法加载PDF文档,请检查文件地址是否正确</p>
+              </div>
+            </div>
+            <!-- 文本内容展示 -->
+            <div v-else-if="selectedContent && selectedTextType !== 3" class="content-text">
               <pre>{{ selectedContent }}</pre>
             </div>
+            <!-- 空状态 -->
             <div v-else class="empty-content">
               <div class="empty-icon">📄</div>
               <h3>选择左侧文档查看内容</h3>
@@ -362,6 +399,8 @@ interface Item {
   title: string | null;
   text: string;
   statusDesc: string;
+  textType: number; // 新增字段:文档类型
+  url: string; // 新增字段:PDF文件URL
 }
 
 export default defineComponent({
@@ -373,7 +412,10 @@ export default defineComponent({
     const activeItem = ref('');
     const selectedTitle = ref('');
     const selectedContent = ref('');
+    const selectedTextType = ref<number>(1); // 新增:选中的文档类型
+    const selectedUrl = ref(''); // 新增:选中的PDF URL
     const statusDesc = ref('');
+    const pdfLoading = ref(false); // PDF加载状态
 
     // 页码控制
     const pageIndex = ref(1);
@@ -398,7 +440,6 @@ export default defineComponent({
     // 上传相关配置
     const uploadAction = `${API_BASE_URL}/upload/file`;
 
-
     const dialogVisible = ref(false);
     const formRef = ref();
 
@@ -447,7 +488,32 @@ export default defineComponent({
       if (selected) {
         selectedTitle.value = selected.title || '';
         selectedContent.value = selected.text;
+        selectedTextType.value = selected.textType || 1;
+        selectedUrl.value = selected.url || '';
         statusDesc.value = selected.statusDesc;
+
+        // 如果是PDF类型,显示加载状态
+        if (selected.textType === 3 && selected.url) {
+          pdfLoading.value = true;
+        }
+      }
+    };
+
+    // PDF加载完成处理
+    const onPdfLoad = () => {
+      pdfLoading.value = false;
+    };
+
+    // PDF加载错误处理
+    const onPdfError = () => {
+      pdfLoading.value = false;
+      ElMessage.error('PDF文档加载失败,请检查网络连接或文件地址');
+    };
+
+    // 在新窗口打开PDF
+    const openPdfInNewTab = () => {
+      if (selectedUrl.value) {
+        window.open(selectedUrl.value, '_blank');
       }
     };
 
@@ -505,6 +571,11 @@ export default defineComponent({
       // 根据实际返回数据结构判断
       if (response.status_code === 200) {
         ElMessage.success('文件上传成功!');
+        // 关闭弹窗并刷新列表
+        dialogVisible.value = false;
+        if (datasetId.value !== null) {
+          fetchData(datasetId.value);
+        }
       } else {
         ElMessage.error(response.detail || '文件上传失败');
       }
@@ -523,6 +594,7 @@ export default defineComponent({
           title: formData.value.title,
           text: formData.value.text,
           dont_chunk: !formData.value.isChunk,
+          is_web: true,
         });
 
         console.log('提交成功:', response.data);
@@ -534,8 +606,12 @@ export default defineComponent({
           console.error('datasetId is null');
         }
 
-        ElMessage.success('文档添加成功!');
-        resetForm();
+        if (response.data.error != null) {
+          ElMessage.error(response.data.error);
+        } else {
+          ElMessage.success('文档添加成功!');
+          resetForm();
+        }
 
       } catch (error) {
         console.error('提交失败:', error);
@@ -621,6 +697,8 @@ export default defineComponent({
       activeItem,
       selectedTitle,
       selectedContent,
+      selectedTextType,
+      selectedUrl,
       pageIndex,
       pageSize,
       totalItems,
@@ -653,6 +731,11 @@ export default defineComponent({
       uploadAction,
       beforeUpload,
       handleUploadSuccess,
+      // PDF相关
+      pdfLoading,
+      onPdfLoad,
+      onPdfError,
+      openPdfInNewTab,
     };
   },
 });
@@ -880,6 +963,16 @@ export default defineComponent({
   white-space: nowrap;
 }
 
+.document-type {
+  display: block;
+  font-size: 0.75rem;
+  color: #718096;
+  padding: 2px 6px;
+  background: #f7fafc;
+  border-radius: 4px;
+  width: fit-content;
+}
+
 .document-actions {
   display: flex;
   align-items: center;
@@ -958,6 +1051,12 @@ export default defineComponent({
   line-height: 1.4;
 }
 
+.content-meta {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
 .content-status {
   font-size: 0.85rem;
   font-weight: 500;
@@ -977,7 +1076,26 @@ export default defineComponent({
   border: 1px solid #feb2b2;
 }
 
-.chunk-btn {
+.content-type {
+  font-size: 0.85rem;
+  font-weight: 500;
+  padding: 4px 12px;
+  border-radius: 12px;
+}
+
+.content-type.pdf-type {
+  background: #fff5f5;
+  color: #c53030;
+  border: 1px solid #fed7d7;
+}
+
+.content-type.text-type {
+  background: #f0fff4;
+  color: #2f855a;
+  border: 1px solid #c6f6d5;
+}
+
+.chunk-btn, .view-pdf-btn {
   border-radius: 12px;
   padding: 10px 20px;
   font-weight: 500;
@@ -989,6 +1107,68 @@ export default defineComponent({
   flex-direction: column;
 }
 
+/* PDF内容展示样式 */
+.pdf-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.pdf-viewer-container {
+  flex: 1;
+  position: relative;
+  border: 1px solid #e2e8f0;
+  border-radius: 12px;
+  overflow: hidden;
+  background: #f7fafc;
+}
+
+.pdf-viewer {
+  width: 100%;
+  height: 100%;
+  min-height: 600px;
+  background: white;
+}
+
+.pdf-loading {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(255, 255, 255, 0.9);
+}
+
+.loading-spinner {
+  width: 40px;
+  height: 40px;
+  border: 4px solid #e2e8f0;
+  border-top: 4px solid #4299e1;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 16px;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.pdf-loading p {
+  margin: 0;
+  color: #718096;
+  font-size: 1rem;
+}
+
+/* 文本内容展示样式 */
 .content-text {
   background: #f7fafc;
   border-radius: 12px;
@@ -1264,6 +1444,10 @@ export default defineComponent({
   font-weight: 500;
 }
 
+.icon-pdf {
+  color: #e53e3e;
+}
+
 .empty-chunks {
   padding: 60px 20px;
   text-align: center;
@@ -1355,7 +1539,7 @@ export default defineComponent({
     align-items: stretch;
   }
 
-  .chunk-btn {
+  .chunk-btn, .view-pdf-btn {
     width: 100%;
   }
 
@@ -1381,6 +1565,10 @@ export default defineComponent({
   .upload-icon {
     font-size: 2rem;
   }
+
+  .pdf-viewer {
+    min-height: 400px;
+  }
 }
 
 @media (max-width: 480px) {
@@ -1395,6 +1583,12 @@ export default defineComponent({
   .sidebar {
     width: 100%;
   }
+
+  .content-meta {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 8px;
+  }
 }
 
 /* 侧边栏滚动条样式 */

+ 238 - 25
src/views/QAndA.vue

@@ -187,13 +187,13 @@
               <h3>最终回答</h3>
               <p>&nbsp;&nbsp;&nbsp;基于知识库内容生成的完整答案</p>
             </div>
-<!--            <div class="header-actions">-->
-<!--              <el-tooltip content="复制回答" placement="top">-->
-<!--                <el-button text class="action-btn" @click="copyToClipboard(chatSummary)">-->
-<!--                  📋-->
-<!--                </el-button>-->
-<!--              </el-tooltip>-->
-<!--            </div>-->
+            <!--            <div class="header-actions">-->
+            <!--              <el-tooltip content="复制回答" placement="top">-->
+            <!--                <el-button text class="action-btn" @click="copyToClipboard(chatSummary)">-->
+            <!--                  📋-->
+            <!--                </el-button>-->
+            <!--              </el-tooltip>-->
+            <!--            </div>-->
           </div>
           <div class="card-body">
             <div v-html="parsedChatSummary" class="thinking-content"></div>
@@ -286,6 +286,8 @@
                         {{ (result.score * 100).toFixed(1) }}%
                       </span>
                       <span class="source-tag">{{ result.datasetName }}</span>
+                      <span v-if="result.textType === 3" class="file-type pdf-type">PDF</span>
+                      <span v-else class="file-type text-type">文本</span>
                     </div>
                   </div>
                   <p class="result-preview">{{ result.content.substring(0, 150) }}...</p>
@@ -348,7 +350,9 @@
       <template #header>
         <div class="dialog-header">
           <div class="dialog-title">
-            <div class="title-icon">📄</div>
+            <div class="title-icon" :class="selectedResult.textType === 3 ? 'pdf-icon' : 'text-icon'">
+              {{ selectedResult.textType === 3 ? '📄' : '📝' }}
+            </div>
             <div class="title-content">
               <h3>{{ selectedResult.contentSummary }}</h3>
               <div class="title-meta">
@@ -360,6 +364,14 @@
                   <span class="badge-icon">🎯</span>
                   相关度: {{ (selectedResult.score * 100).toFixed(1) }}%
                 </span>
+                <span v-if="selectedResult.textType === 3" class="meta-badge file-type-badge pdf-badge">
+                  <span class="badge-icon">📄</span>
+                  PDF文档
+                </span>
+                <span v-else class="meta-badge file-type-badge text-badge">
+                  <span class="badge-icon">📝</span>
+                  文本文档
+                </span>
               </div>
             </div>
           </div>
@@ -377,6 +389,7 @@
             摘要内容
           </div>
           <div
+              v-if="selectedResult.textType !== 3"
               class="tab-item"
               :class="{ active: activeTab === 'original' }"
               @click="activeTab = 'original'"
@@ -384,19 +397,21 @@
             <span class="tab-icon">📖</span>
             完整原文
           </div>
+          <div
+              v-if="selectedResult.textType === 3"
+              class="tab-item"
+              :class="{ active: activeTab === 'pdf' }"
+              @click="activeTab = 'pdf'"
+          >
+            <span class="tab-icon">📄</span>
+            PDF文档
+          </div>
         </div>
 
         <div class="tab-content">
           <div v-show="activeTab === 'summary'" class="content-section summary-section">
             <div class="section-header">
               <h4>内容摘要</h4>
-<!--              <el-button-->
-<!--                  text-->
-<!--                  class="copy-btn"-->
-<!--                  @click="copyToClipboard(selectedResult.content)"-->
-<!--              >-->
-<!--                📋 复制内容-->
-<!--              </el-button>-->
             </div>
             <div class="summary-content">
               {{ selectedResult.content }}
@@ -406,18 +421,47 @@
           <div v-show="activeTab === 'original'" class="content-section original-section">
             <div class="section-header">
               <h4>完整原文</h4>
-<!--              <el-button-->
-<!--                  text-->
-<!--                  class="copy-btn"-->
-<!--                  @click="copyToClipboard(originalContent)"-->
-<!--              >-->
-<!--                📋 复制原文-->
-<!--              </el-button>-->
             </div>
             <div class="original-content">
               <pre>{{ originalContent }}</pre>
             </div>
           </div>
+
+          <!-- PDF 内容展示 -->
+          <div v-show="activeTab === 'pdf'" class="content-section pdf-section">
+            <div class="section-header">
+              <h4>PDF文档</h4>
+              <el-button
+                  v-if="selectedResult.url"
+                  type="primary"
+                  class="view-pdf-btn"
+                  @click="openPdfInNewTab"
+              >
+                <span class="button-icon">👀</span>
+                在新窗口查看
+              </el-button>
+            </div>
+            <div class="pdf-content" v-if="selectedResult.url">
+              <div class="pdf-viewer-container">
+                <iframe
+                    :src="selectedResult.url"
+                    class="pdf-viewer"
+                    frameborder="0"
+                    @load="onPdfLoad"
+                    @error="onPdfError"
+                ></iframe>
+                <div v-if="pdfLoading" class="pdf-loading">
+                  <div class="loading-spinner"></div>
+                  <p>正在加载PDF文档...</p>
+                </div>
+              </div>
+            </div>
+            <div v-else class="pdf-empty">
+              <div class="empty-icon">📄</div>
+              <h3>PDF文档不可用</h3>
+              <p>无法加载PDF文档,请检查文件地址是否正确</p>
+            </div>
+          </div>
         </div>
       </div>
 
@@ -465,6 +509,7 @@ const loading = ref(false);
 const refreshing = ref(false);
 const hasSearched = ref(false);
 const activeTab = ref('summary');
+const pdfLoading = ref(false);
 
 // 可折叠区域的展开状态
 const expandedSections = ref({
@@ -521,6 +566,22 @@ const copyToClipboard = async (text) => {
   }
 };
 
+// PDF相关方法
+const onPdfLoad = () => {
+  pdfLoading.value = false;
+};
+
+const onPdfError = () => {
+  pdfLoading.value = false;
+  ElMessage.error('PDF文档加载失败,请检查网络连接或文件地址');
+};
+
+const openPdfInNewTab = () => {
+  if (selectedResult.value.url) {
+    window.open(selectedResult.value.url, '_blank');
+  }
+};
+
 // 切换可折叠区域
 const toggleSection = (section) => {
   expandedSections.value[section] = !expandedSections.value[section];
@@ -611,13 +672,28 @@ const chat = async () => {
 const handleDetails = async (result) => {
   selectedResult.value = result;
   dialogVisible.value = true;
-  activeTab.value = 'summary';
+
+  // 根据文档类型设置默认标签页
+  if (result.textType === 3) {
+    activeTab.value = 'summary';
+    if (result.url) {
+      pdfLoading.value = true;
+    }
+  } else {
+    activeTab.value = 'summary';
+  }
 
   try {
     const response = await fetch(`${API_BASE_URL}/content/get?docId=${result.docId}`);
     const data = await response.json();
     if (data.status_code === 200) {
       originalContent.value = data.data.text;
+      // 更新选中结果的数据,确保包含完整的文档信息
+      selectedResult.value = {
+        ...selectedResult.value,
+        textType: data.data.textType,
+        url: data.data.url
+      };
     } else {
       ElMessage.error('获取原文内容失败');
     }
@@ -775,10 +851,14 @@ onMounted(() => {
 
 .knowledge-card {
   background: white;
+  padding: 16px;
   border-radius: 16px;
   cursor: pointer;
   transition: all 0.3s ease;
   border: 2px solid transparent;
+  display: flex;
+  align-items: center;
+  gap: 12px;
   box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
   overflow: hidden;
 }
@@ -1410,6 +1490,7 @@ onMounted(() => {
   display: flex;
   gap: 8px;
   align-items: center;
+  flex-wrap: wrap;
 }
 
 .score-badge {
@@ -1441,6 +1522,22 @@ onMounted(() => {
   border: 1px solid #e2e8f0;
 }
 
+.file-type {
+  padding: 4px 10px;
+  border-radius: 12px;
+  font-size: 0.8rem;
+  font-weight: 600;
+  color: white;
+}
+
+.file-type.pdf-type {
+  background: #e53e3e;
+}
+
+.file-type.text-type {
+  background: #48bb78;
+}
+
 .result-preview {
   margin: 0 0 16px 0;
   color: #718096;
@@ -1622,12 +1719,19 @@ onMounted(() => {
   display: flex;
   align-items: center;
   justify-content: center;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   border-radius: 16px;
   color: white;
   flex-shrink: 0;
 }
 
+.title-icon.pdf-icon {
+  background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%);
+}
+
+.title-icon.text-icon {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
 .title-content {
   flex: 1;
   min-width: 0;
@@ -1681,6 +1785,18 @@ onMounted(() => {
   border: 1px solid #fed7d7;
 }
 
+.file-type-badge.pdf-badge {
+  background: #fff5f5;
+  color: #c53030;
+  border: 1px solid #fed7d7;
+}
+
+.file-type-badge.text-badge {
+  background: #f0fff4;
+  color: #2f855a;
+  border: 1px solid #c6f6d5;
+}
+
 .badge-icon {
   font-size: 0.8rem;
 }
@@ -1743,7 +1859,7 @@ onMounted(() => {
   font-weight: 600;
 }
 
-.copy-btn {
+.copy-btn, .view-pdf-btn {
   font-size: 0.9rem;
   padding: 6px 12px;
   border-radius: 8px;
@@ -1777,6 +1893,86 @@ onMounted(() => {
   word-wrap: break-word;
 }
 
+/* PDF内容展示样式 */
+.pdf-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.pdf-viewer-container {
+  flex: 1;
+  position: relative;
+  border: 1px solid #e2e8f0;
+  border-radius: 12px;
+  overflow: hidden;
+  background: #f7fafc;
+  min-height: 500px;
+}
+
+.pdf-viewer {
+  width: 100%;
+  height: 100%;
+  min-height: 500px;
+  background: white;
+}
+
+.pdf-loading {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(255, 255, 255, 0.9);
+}
+
+.pdf-loading .loading-spinner {
+  width: 40px;
+  height: 40px;
+  border: 4px solid #e2e8f0;
+  border-top: 4px solid #4299e1;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 16px;
+}
+
+.pdf-loading p {
+  margin: 0;
+  color: #718096;
+  font-size: 1rem;
+}
+
+.pdf-empty {
+  text-align: center;
+  padding: 60px 20px;
+  background: #f7fafc;
+  border-radius: 12px;
+  border: 1px solid #e2e8f0;
+}
+
+.pdf-empty .empty-icon {
+  font-size: 3rem;
+  margin-bottom: 16px;
+  opacity: 0.6;
+}
+
+.pdf-empty h3 {
+  margin: 0 0 8px 0;
+  color: #2d3748;
+  font-size: 1.25rem;
+  font-weight: 600;
+}
+
+.pdf-empty p {
+  margin: 0;
+  color: #718096;
+  font-size: 0.95rem;
+}
+
 .dialog-footer {
   padding: 0 32px 24px;
 }
@@ -1939,11 +2135,16 @@ onMounted(() => {
 
   .content-tabs {
     padding: 0 20px;
+    flex-wrap: wrap;
   }
 
   .tab-content {
     padding: 0 20px 20px;
   }
+
+  .pdf-viewer {
+    min-height: 400px;
+  }
 }
 
 @media (max-width: 480px) {
@@ -1972,6 +2173,18 @@ onMounted(() => {
     width: 95% !important;
     margin: 20px auto;
   }
+
+  .title-meta {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 8px;
+  }
+
+  .result-meta {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 8px;
+  }
 }
 
 /* 侧边栏滚动条样式 */

+ 157 - 102
src/views/QAndAHistory.vue

@@ -45,33 +45,6 @@
         </div>
       </div>
 
-      <!-- 搜索和筛选 -->
-<!--      <div class="filter-section">-->
-<!--        <div class="search-input">-->
-<!--          <el-input-->
-<!--              v-model="searchQuery"-->
-<!--              placeholder="搜索问题内容..."-->
-<!--              size="large"-->
-<!--              clearable-->
-<!--              @input="handleSearch"-->
-<!--          >-->
-<!--            <template #prefix>-->
-<!--              <span class="search-icon">🔍</span>-->
-<!--            </template>-->
-<!--          </el-input>-->
-<!--        </div>-->
-<!--        <div class="filter-actions">-->
-<!--          <el-button-->
-<!--              class="refresh-btn"-->
-<!--              @click="refreshData"-->
-<!--              :loading="loading"-->
-<!--          >-->
-<!--            <span class="btn-icon">🔄</span>-->
-<!--            刷新-->
-<!--          </el-button>-->
-<!--        </div>-->
-<!--      </div>-->
-
       <!-- 加载状态 -->
       <div v-if="loading" class="loading-section">
         <div class="loading-content">
@@ -259,6 +232,11 @@
                   <span class="badge-icon">🎯</span>
                   相关度: {{ (selectedResult.score * 100).toFixed(1) }}%
                 </span>
+                <!-- PDF标签 -->
+                <span v-if="isPdfDocument" class="meta-badge pdf-badge">
+                  <span class="badge-icon">📎</span>
+                  PDF文档
+                </span>
               </div>
             </div>
           </div>
@@ -283,19 +261,22 @@
             <span class="tab-icon">📖</span>
             完整原文
           </div>
+          <!-- PDF预览标签 -->
+          <div
+              v-if="isPdfDocument"
+              class="tab-item"
+              :class="{ active: activeTab === 'pdf' }"
+              @click="activeTab = 'pdf'"
+          >
+            <span class="tab-icon">📎</span>
+            PDF预览
+          </div>
         </div>
 
         <div class="tab-content">
           <div v-show="activeTab === 'summary'" class="content-section summary-section">
             <div class="section-header">
               <h4>内容摘要</h4>
-<!--              <el-button-->
-<!--                  text-->
-<!--                  class="copy-btn"-->
-<!--                  @click="copyToClipboard(selectedResult.content)"-->
-<!--              >-->
-<!--                📋 复制内容-->
-<!--              </el-button>-->
             </div>
             <div class="summary-content">
               {{ selectedResult.content }}
@@ -305,18 +286,41 @@
           <div v-show="activeTab === 'original'" class="content-section original-section">
             <div class="section-header">
               <h4>完整原文</h4>
-<!--              <el-button-->
-<!--                  text-->
-<!--                  class="copy-btn"-->
-<!--                  @click="copyToClipboard(originalContent)"-->
-<!--              >-->
-<!--                📋 复制原文-->
-<!--              </el-button>-->
             </div>
             <div class="original-content">
               <pre>{{ originalContent }}</pre>
             </div>
           </div>
+
+          <!-- PDF预览内容 -->
+          <div v-show="activeTab === 'pdf'" class="content-section pdf-section">
+            <div class="section-header">
+              <h4>PDF文档预览</h4>
+              <el-button
+                  v-if="pdfUrl"
+                  type="primary"
+                  class="download-btn"
+                  @click="downloadPdf"
+              >
+                <span class="button-icon">👀</span>
+                在新窗口查看PDF
+              </el-button>
+            </div>
+            <div class="pdf-preview">
+              <div v-if="!pdfUrl" class="no-pdf-content">
+                <div class="no-pdf-icon">📄</div>
+                <h3>无法预览PDF文档</h3>
+                <p>该文档的PDF文件暂时无法访问</p>
+              </div>
+              <iframe
+                  v-else
+                  :src="pdfUrl"
+                  class="pdf-iframe"
+                  frameborder="0"
+                  @load="pdfLoading = false"
+              ></iframe>
+            </div>
+          </div>
         </div>
       </div>
 
@@ -348,8 +352,6 @@ import { ref, onMounted, computed } from 'vue';
 import { ElMessage } from 'element-plus';
 import { marked } from 'marked';
 import { API_BASE_URL } from "@/config";
-import dayjs from 'dayjs';
-
 
 // 响应式数据
 const historyList = ref([]);
@@ -364,6 +366,9 @@ const resultDialogVisible = ref(false);
 const selectedResult = ref({});
 const originalContent = ref('');
 const activeTab = ref('summary');
+const pdfUrl = ref('');
+const pdfLoading = ref(false);
+const textType = ref(0); // 存储文档类型
 
 // 计算属性
 const filteredHistory = computed(() => {
@@ -379,6 +384,11 @@ const filteredHistory = computed(() => {
   );
 });
 
+// 判断是否为PDF文档
+const isPdfDocument = computed(() => {
+  return textType.value === 3;
+});
+
 // 方法
 const parseMarkdown = (content) => {
   if (!content) return '';
@@ -389,25 +399,19 @@ const parseMarkdown = (content) => {
 
 const formatTime = (timeString) => {
   try {
-    // 处理 GMT 时间格式 "Tue, 30 Sep 2025 14:02:21 GMT"
     if (timeString.includes('GMT')) {
-      // 使用 UTC 方法获取时间组件,避免时区转换
       const date = new Date(timeString);
-
       if (!isNaN(date.getTime())) {
-        // 使用 UTC 相关的方法获取时间组件
         const year = date.getUTCFullYear();
         const month = String(date.getUTCMonth() + 1).padStart(2, '0');
         const day = String(date.getUTCDate()).padStart(2, '0');
         const hours = String(date.getUTCHours()).padStart(2, '0');
         const minutes = String(date.getUTCMinutes()).padStart(2, '0');
         const seconds = String(date.getUTCSeconds()).padStart(2, '0');
-
         return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
       }
     }
 
-    // 对于其他格式,使用原来的逻辑
     const date = new Date(timeString);
     if (!isNaN(date.getTime())) {
       return date.toLocaleString('zh-CN', {
@@ -453,13 +457,11 @@ const getDisplayedResults = (item) => {
 };
 
 const processHistoryItem = (item) => {
-  // 确保所有字段都有值,如果没有则从其他字段获取
   return {
     ...item,
     final_result: item.final_result || item.chat_res || item.ai_answer || '',
     chat_res: item.chat_res || '',
     ai_answer: item.ai_answer || '',
-    // 添加展开状态
     isResultsExpanded: false
   };
 };
@@ -481,26 +483,45 @@ const handleResultDetails = async (result) => {
   selectedResult.value = result;
   resultDialogVisible.value = true;
   activeTab.value = 'summary';
+  pdfUrl.value = '';
+  pdfLoading.value = false;
+  textType.value = 0;
 
   try {
     const response = await fetch(`${API_BASE_URL}/content/get?docId=${result.docId}`);
     const data = await response.json();
+
     if (data.status_code === 200) {
-      originalContent.value = data.data.text;
+      textType.value = data.data.textType || 0;
+
+      if (textType.value === 3) {
+        pdfUrl.value = data.data.url;
+        originalContent.value = '此文档为PDF格式,请在PDF预览中查看完整内容';
+        pdfLoading.value = true;
+      } else {
+        originalContent.value = data.data.text || '';
+      }
     } else {
-      ElMessage.error('获取原文内容失败');
+      ElMessage.error('获取文内容失败');
     }
   } catch (error) {
-    ElMessage.error('请求原文内容失败');
+    console.error('请求文档内容失败:', error);
+    ElMessage.error('请求文档内容失败');
   }
 };
 
-const copyToClipboard = async (text) => {
-  try {
-    await navigator.clipboard.writeText(text);
-    ElMessage.success('已复制到剪贴板');
-  } catch (err) {
-    ElMessage.error('复制失败');
+// 下载PDF
+const downloadPdf = () => {
+  if (pdfUrl.value) {
+    const link = document.createElement('a');
+    link.href = pdfUrl.value;
+    link.download = selectedResult.value.fileName || 'document.pdf';
+    link.target = '_blank';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+  } else {
+    ElMessage.warning('无法下载PDF文件');
   }
 };
 
@@ -533,7 +554,6 @@ const fetchHistory = async () => {
     const data = await response.json();
 
     if (data.status_code === 200) {
-      // 处理每个历史记录项,确保字段正确
       historyList.value = (data.data.entities || []).map(processHistoryItem);
       totalCount.value = data.data.total_count || 0;
       totalPages.value = data.data.total_pages || 0;
@@ -555,7 +575,6 @@ onMounted(() => {
 </script>
 
 <style scoped>
-/* 保持原有的所有样式不变 */
 .history-container {
   min-height: 100vh;
   background: linear-gradient(135deg, rgba(102, 126, 234, 0.6) 0%, rgba(118, 75, 162, 0.4) 100%);
@@ -681,32 +700,6 @@ onMounted(() => {
   font-weight: 600;
 }
 
-.filter-section {
-  background: white;
-  border-radius: 16px;
-  padding: 20px;
-  margin-bottom: 24px;
-  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
-  display: flex;
-  gap: 16px;
-  align-items: center;
-}
-
-.search-input {
-  flex: 1;
-}
-
-.filter-actions {
-  display: flex;
-  gap: 12px;
-}
-
-.refresh-btn {
-  border-radius: 12px;
-  padding: 10px 20px;
-  font-weight: 600;
-}
-
 .loading-section {
   background: white;
   border-radius: 16px;
@@ -732,8 +725,12 @@ onMounted(() => {
 }
 
 @keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
 }
 
 .history-list {
@@ -1090,7 +1087,7 @@ onMounted(() => {
   margin-right: 6px;
 }
 
-/* 弹窗样式 - 从主页面复制并调整 */
+/* 弹窗样式 */
 .content-dialog :deep(.el-dialog) {
   border-radius: 24px;
   overflow: hidden;
@@ -1192,6 +1189,12 @@ onMounted(() => {
   border: 1px solid #fed7d7;
 }
 
+.pdf-badge {
+  background: #fff5f5;
+  color: #c53030;
+  border: 1px solid #fed7d7;
+}
+
 .badge-icon {
   font-size: 0.8rem;
 }
@@ -1254,12 +1257,6 @@ onMounted(() => {
   font-weight: 600;
 }
 
-.copy-btn {
-  font-size: 0.9rem;
-  padding: 6px 12px;
-  border-radius: 8px;
-}
-
 .summary-content {
   background: #f7fafc;
   padding: 20px;
@@ -1288,6 +1285,55 @@ onMounted(() => {
   word-wrap: break-word;
 }
 
+/* PDF预览样式 */
+.pdf-preview {
+  width: 100%;
+  height: 600px;
+  border: 1px solid #e2e8f0;
+  border-radius: 12px;
+  overflow: hidden;
+  background: #f7fafc;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.pdf-iframe {
+  width: 100%;
+  height: 100%;
+  border: none;
+}
+
+.no-pdf-content {
+  text-align: center;
+  padding: 60px 20px;
+  color: #718096;
+}
+
+.no-pdf-icon {
+  font-size: 4rem;
+  margin-bottom: 20px;
+  opacity: 0.6;
+}
+
+.no-pdf-content h3 {
+  margin: 0 0 12px 0;
+  color: #2d3748;
+  font-size: 1.25rem;
+  font-weight: 600;
+}
+
+.no-pdf-content p {
+  margin: 0;
+  font-size: 0.95rem;
+}
+
+.download-btn {
+  border-radius: 8px;
+  padding: 8px 16px;
+  font-weight: 500;
+}
+
 .dialog-footer {
   padding: 0 32px 24px;
 }
@@ -1349,11 +1395,6 @@ onMounted(() => {
     grid-template-columns: 1fr;
   }
 
-  .filter-section {
-    flex-direction: column;
-    align-items: stretch;
-  }
-
   .item-header {
     flex-direction: column;
     align-items: stretch;
@@ -1374,5 +1415,19 @@ onMounted(() => {
     width: 95% !important;
     margin: 20px auto;
   }
+
+  .pdf-preview {
+    height: 400px;
+  }
+
+  .dialog-title {
+    flex-direction: column;
+    text-align: center;
+    gap: 12px;
+  }
+
+  .title-meta {
+    justify-content: center;
+  }
 }
 </style>

+ 241 - 32
src/views/SearchPage.vue

@@ -101,20 +101,20 @@
             </div>
 
             <!-- 快速搜索示例 -->
-<!--            <div class="quick-questions" v-if="!loading">-->
-<!--              <p class="quick-title">💡 试试搜索这些内容:</p>-->
-<!--              <div class="question-chips">-->
-<!--                <el-tag-->
-<!--                    v-for="question in quickQuestions"-->
-<!--                    :key="question"-->
-<!--                    class="question-chip"-->
-<!--                    @click="query = question; search()"-->
-<!--                    :disabled="loading"-->
-<!--                >-->
-<!--                  {{ question }}-->
-<!--                </el-tag>-->
-<!--              </div>-->
-<!--            </div>-->
+            <!--            <div class="quick-questions" v-if="!loading">-->
+            <!--              <p class="quick-title">💡 试试搜索这些内容:</p>-->
+            <!--              <div class="question-chips">-->
+            <!--                <el-tag-->
+            <!--                    v-for="question in quickQuestions"-->
+            <!--                    :key="question"-->
+            <!--                    class="question-chip"-->
+            <!--                    @click="query = question; search()"-->
+            <!--                    :disabled="loading"-->
+            <!--                >-->
+            <!--                  {{ question }}-->
+            <!--                </el-tag>-->
+            <!--              </div>-->
+            <!--            </div>-->
           </div>
         </div>
 
@@ -156,6 +156,8 @@
                     {{ (result.score * 100).toFixed(1) }}%
                   </span>
                   <span class="source-tag">{{ result.datasetName }}</span>
+                  <span v-if="result.textType === 3" class="file-type pdf-type">PDF</span>
+                  <span v-else class="file-type text-type">文本</span>
                 </div>
               </div>
               <p class="result-preview">{{ result.content.substring(0, 150) }}...</p>
@@ -216,7 +218,9 @@
       <template #header>
         <div class="dialog-header">
           <div class="dialog-title">
-            <div class="title-icon">📄</div>
+            <div class="title-icon" :class="selectedResult.textType === 3 ? 'pdf-icon' : 'text-icon'">
+              {{ selectedResult.textType === 3 ? '📄' : '📝' }}
+            </div>
             <div class="title-content">
               <h3>{{ selectedResult.contentSummary }}</h3>
               <div class="title-meta">
@@ -228,6 +232,14 @@
                   <span class="badge-icon">🎯</span>
                   相关度: {{ (selectedResult.score * 100).toFixed(1) }}%
                 </span>
+                <span v-if="selectedResult.textType === 3" class="meta-badge file-type-badge pdf-badge">
+                  <span class="badge-icon">📄</span>
+                  PDF文档
+                </span>
+                <span v-else class="meta-badge file-type-badge text-badge">
+                  <span class="badge-icon">📝</span>
+                  文本文档
+                </span>
               </div>
             </div>
           </div>
@@ -245,6 +257,7 @@
             摘要内容
           </div>
           <div
+              v-if="selectedResult.textType !== 3"
               class="tab-item"
               :class="{ active: activeTab === 'original' }"
               @click="activeTab = 'original'"
@@ -252,19 +265,21 @@
             <span class="tab-icon">📖</span>
             完整原文
           </div>
+          <div
+              v-if="selectedResult.textType === 3"
+              class="tab-item"
+              :class="{ active: activeTab === 'pdf' }"
+              @click="activeTab = 'pdf'"
+          >
+            <span class="tab-icon">📄</span>
+            PDF文档
+          </div>
         </div>
 
         <div class="tab-content">
           <div v-show="activeTab === 'summary'" class="content-section summary-section">
             <div class="section-header">
               <h4>内容摘要</h4>
-<!--              <el-button-->
-<!--                  text-->
-<!--                  class="copy-btn"-->
-<!--                  @click="copyToClipboard(selectedResult.content)"-->
-<!--              >-->
-<!--                📋 复制内容-->
-<!--              </el-button>-->
             </div>
             <div class="summary-content">
               {{ selectedResult.content }}
@@ -274,18 +289,47 @@
           <div v-show="activeTab === 'original'" class="content-section original-section">
             <div class="section-header">
               <h4>完整原文</h4>
-<!--              <el-button-->
-<!--                  text-->
-<!--                  class="copy-btn"-->
-<!--                  @click="copyToClipboard(originalContent)"-->
-<!--              >-->
-<!--                📋 复制原文-->
-<!--              </el-button>-->
             </div>
             <div class="original-content">
               <pre>{{ originalContent }}</pre>
             </div>
           </div>
+
+          <!-- PDF 内容展示 -->
+          <div v-show="activeTab === 'pdf'" class="content-section pdf-section">
+            <div class="section-header">
+              <h4>PDF文档</h4>
+              <el-button
+                  v-if="selectedResult.url"
+                  type="primary"
+                  class="view-pdf-btn"
+                  @click="openPdfInNewTab"
+              >
+                <span class="button-icon">👀</span>
+                在新窗口查看
+              </el-button>
+            </div>
+            <div class="pdf-content" v-if="selectedResult.url">
+              <div class="pdf-viewer-container">
+                <iframe
+                    :src="selectedResult.url"
+                    class="pdf-viewer"
+                    frameborder="0"
+                    @load="onPdfLoad"
+                    @error="onPdfError"
+                ></iframe>
+<!--                <div v-if="pdfLoading" class="pdf-loading">-->
+<!--                  <div class="loading-spinner"></div>-->
+<!--                  <p>正在加载PDF文档...</p>-->
+<!--                </div>-->
+              </div>
+            </div>
+            <div v-else class="pdf-empty">
+              <div class="empty-icon">📄</div>
+              <h3>PDF文档不可用</h3>
+              <p>无法加载PDF文档,请检查文件地址是否正确</p>
+            </div>
+          </div>
         </div>
       </div>
 
@@ -328,6 +372,7 @@ const originalContent = ref('');
 const loading = ref(false);
 const hasSearched = ref(false);
 const activeTab = ref('summary');
+const pdfLoading = ref(false);
 
 // 快速搜索示例
 const quickQuestions = ref([
@@ -362,6 +407,22 @@ const copyToClipboard = async (text) => {
   }
 };
 
+// PDF相关方法
+const onPdfLoad = () => {
+  pdfLoading.value = false;
+};
+
+const onPdfError = () => {
+  pdfLoading.value = false;
+  ElMessage.error('PDF文档加载失败,请检查网络连接或文件地址');
+};
+
+const openPdfInNewTab = () => {
+  if (selectedResult.value.url) {
+    window.open(selectedResult.value.url, '_blank');
+  }
+};
+
 // 方法
 const getKnowledgeBaseList = async () => {
   try {
@@ -416,13 +477,28 @@ const search = async () => {
 const handleDetails = async (result) => {
   selectedResult.value = result;
   dialogVisible.value = true;
-  activeTab.value = 'summary';
+
+  // 根据文档类型设置默认标签页
+  if (result.textType === 3) {
+    activeTab.value = 'summary';
+    if (result.url) {
+      pdfLoading.value = true;
+    }
+  } else {
+    activeTab.value = 'summary';
+  }
 
   try {
     const response = await fetch(`${API_BASE_URL}/content/get?docId=${result.docId}`);
     const data = await response.json();
     if (data.status_code === 200) {
       originalContent.value = data.data.text;
+      // 更新选中结果的数据,确保包含完整的文档信息
+      selectedResult.value = {
+        ...selectedResult.value,
+        textType: data.data.textType,
+        url: data.data.url
+      };
     } else {
       ElMessage.error('获取原文内容失败');
     }
@@ -956,6 +1032,7 @@ onMounted(() => {
   display: flex;
   gap: 8px;
   align-items: center;
+  flex-wrap: wrap;
 }
 
 .score-badge {
@@ -987,6 +1064,22 @@ onMounted(() => {
   border: 1px solid #e2e8f0;
 }
 
+.file-type {
+  padding: 4px 10px;
+  border-radius: 12px;
+  font-size: 0.8rem;
+  font-weight: 600;
+  color: white;
+}
+
+.file-type.pdf-type {
+  background: #e53e3e;
+}
+
+.file-type.text-type {
+  background: #48bb78;
+}
+
 .result-preview {
   margin: 0 0 16px 0;
   color: #718096;
@@ -1168,12 +1261,19 @@ onMounted(() => {
   display: flex;
   align-items: center;
   justify-content: center;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   border-radius: 16px;
   color: white;
   flex-shrink: 0;
 }
 
+.title-icon.pdf-icon {
+  background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%);
+}
+
+.title-icon.text-icon {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
 .title-content {
   flex: 1;
   min-width: 0;
@@ -1227,6 +1327,18 @@ onMounted(() => {
   border: 1px solid #fed7d7;
 }
 
+.file-type-badge.pdf-badge {
+  background: #fff5f5;
+  color: #c53030;
+  border: 1px solid #fed7d7;
+}
+
+.file-type-badge.text-badge {
+  background: #f0fff4;
+  color: #2f855a;
+  border: 1px solid #c6f6d5;
+}
+
 .badge-icon {
   font-size: 0.8rem;
 }
@@ -1289,7 +1401,7 @@ onMounted(() => {
   font-weight: 600;
 }
 
-.copy-btn {
+.copy-btn, .view-pdf-btn {
   font-size: 0.9rem;
   padding: 6px 12px;
   border-radius: 8px;
@@ -1323,6 +1435,86 @@ onMounted(() => {
   word-wrap: break-word;
 }
 
+/* PDF内容展示样式 */
+.pdf-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.pdf-viewer-container {
+  flex: 1;
+  position: relative;
+  border: 1px solid #e2e8f0;
+  border-radius: 12px;
+  overflow: hidden;
+  background: #f7fafc;
+  min-height: 500px;
+}
+
+.pdf-viewer {
+  width: 100%;
+  height: 100%;
+  min-height: 500px;
+  background: white;
+}
+
+.pdf-loading {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(255, 255, 255, 0.9);
+}
+
+.pdf-loading .loading-spinner {
+  width: 40px;
+  height: 40px;
+  border: 4px solid #e2e8f0;
+  border-top: 4px solid #4299e1;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 16px;
+}
+
+.pdf-loading p {
+  margin: 0;
+  color: #718096;
+  font-size: 1rem;
+}
+
+.pdf-empty {
+  text-align: center;
+  padding: 60px 20px;
+  background: #f7fafc;
+  border-radius: 12px;
+  border: 1px solid #e2e8f0;
+}
+
+.pdf-empty .empty-icon {
+  font-size: 3rem;
+  margin-bottom: 16px;
+  opacity: 0.6;
+}
+
+.pdf-empty h3 {
+  margin: 0 0 8px 0;
+  color: #2d3748;
+  font-size: 1.25rem;
+  font-weight: 600;
+}
+
+.pdf-empty p {
+  margin: 0;
+  color: #718096;
+  font-size: 0.95rem;
+}
+
 .dialog-footer {
   padding: 0 32px 24px;
 }
@@ -1419,6 +1611,7 @@ onMounted(() => {
 
   .content-tabs {
     padding: 0 20px;
+    flex-wrap: wrap;
   }
 
   .tab-content {
@@ -1428,6 +1621,10 @@ onMounted(() => {
   .header-stats {
     gap: 32px;
   }
+
+  .pdf-viewer {
+    min-height: 400px;
+  }
 }
 
 @media (max-width: 480px) {
@@ -1447,6 +1644,18 @@ onMounted(() => {
     width: 95% !important;
     margin: 20px auto;
   }
+
+  .title-meta {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 8px;
+  }
+
+  .result-meta {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 8px;
+  }
 }
 
 /* 侧边栏滚动条样式 */