fix: 优化知识库文档列表及问答消息气泡markdown表格样式

This commit is contained in:
wenjinbo 2025-09-29 17:42:00 +08:00
parent e29ac67a91
commit 9f950db862
4 changed files with 417 additions and 68 deletions

View File

@ -944,9 +944,10 @@ const hasAnySources = (sources: TraceData | null | undefined) => {
overflow: auto;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
background: white;
font-size: 0.83rem; /* 比正文(0.95rem)小2px约为0.83rem */
line-height: 1.4;
font-size: 0.75rem; /* 12px */
line-height: 1.3;
border: 1px solid #e5e7eb;
table-layout: auto; /* 允许列宽根据内容自适应 */
}
::v-deep .table-wrapper summary {
@ -971,15 +972,15 @@ const hasAnySources = (sources: TraceData | null | undefined) => {
.answer-content :deep(th),
.answer-content :deep(td) {
padding: 0.75rem 1rem;
padding: 0.5rem 0.75rem; /* 减少内边距,使表格更紧凑 */
border: 1px solid #e5e7eb;
text-align: left;
vertical-align: top;
word-wrap: break-word;
max-width: 200px;
max-width: none; /* 移除最大宽度限制,允许列宽自适应 */
word-break: break-word;
white-space: normal;
font-size: 0.83rem; /* 确保表格单元格字体也比正文小2px */
font-size: 0.75rem; /* 12px */
}
.answer-content :deep(th) {
@ -987,7 +988,7 @@ const hasAnySources = (sources: TraceData | null | undefined) => {
background: #f8fafc;
font-weight: 600;
color: #374151;
font-size: 0.83rem; /* 与表格整体字体大小保持一致比正文小2px */
font-size: 0.8125rem; /* 13px */
text-transform: uppercase;
letter-spacing: 0.025em;
position: sticky;
@ -1043,21 +1044,22 @@ const hasAnySources = (sources: TraceData | null | undefined) => {
@media (max-width: 768px) {
.answer-content :deep(table) {
min-width: 600px;
font-size: 0.75rem; /* 移动端保持比正文小的比例 */
font-size: 0.6875rem; /* 11px 移动端 */
margin: 1rem 0;
border: 1px solid #e5e7eb;
line-height: 1.2; /* 移动端更紧凑的行高 */
}
.answer-content :deep(th),
.answer-content :deep(td) {
padding: 0.5rem 0.75rem;
max-width: 150px;
padding: 0.375rem 0.5rem; /* 移动端更紧凑的内边距 */
max-width: none; /* 移动端也允许列宽自适应 */
border: 1px solid #e5e7eb;
font-size: 0.75rem; /* 移动端表格单元格字体 */
font-size: 0.6875rem; /* 11px 移动端表格内容 */
}
.answer-content :deep(th) {
font-size: 0.75rem; /* 移动端表头字体 */
font-size: 0.75rem; /* 12px 移动端表头 */
border-bottom: 2px solid #e5e7eb;
}
}

View File

@ -77,7 +77,7 @@ export function useMessageHandlers() {
/<table>/g,
`<details class="table-wrapper" open>
<summary>
Expand / Collapse table
/
</summary>
<div class="table-container"><table class="excel-table" border="1" width="auto" cellspacing="0" cellpadding="0">`
).replace(

View File

@ -1,14 +1,27 @@
<template>
<div class="main-container">
<!-- 搜索头部 -->
<div class="search-header">
<div class="search-header" :class="{ 'collapsed': headerCollapsed }">
<div class="search-logo">
<h1>智能检索</h1>
<!-- 折叠/展开按钮 -->
<button
v-if="hasSearched"
class="collapse-toggle-btn"
@click="toggleHeaderCollapse"
:class="{ 'collapsed': headerCollapsed }"
>
<svg v-if="!headerCollapsed" viewBox="0 0 24 24" width="20" height="20">
<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/>
</svg>
<svg v-else viewBox="0 0 24 24" width="20" height="20">
<path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/>
</svg>
</button>
</div>
<!-- 搜索框 -->
<div class="search-box-container">
<div class="search-box-container" v-show="!headerCollapsed">
<div class="search-box-wrapper">
<div class="search-box">
<input
@ -79,12 +92,15 @@
找到约 {{ totalResults }} 条结果 (用时 {{ searchTime }})
</div>
<!-- 结果列表 -->
<div class="results-list">
<div
v-for="(result, index) in paginatedResults"
:key="index"
<!-- 结果容器 -->
<div class="results-container" :class="{ 'preview-mode': previewDrawerVisible }">
<!-- 左侧结果列表 -->
<div class="results-list-left">
<div
v-for="(result, index) in leftColumnResults"
:key="`left-${index}`"
class="result-item"
:class="{ 'preview-selected': selectedPreviewIndex === getGlobalIndex(index) }"
>
<!-- 文件名和置信度 -->
<div class="result-header">
@ -151,51 +167,93 @@
</div>
</div>
<!-- 分页组件 -->
<div class="pagination-container" v-if="totalResults > pageSize">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[5, 10, 20, 50]"
:total="totalResults"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<!-- 右侧结果列表 -->
<div class="results-list-right" v-if="!previewDrawerVisible">
<div
v-for="(result, index) in rightColumnResults"
:key="`right-${index}`"
class="result-item"
>
<!-- 文件名和置信度 -->
<div class="result-header">
<div class="file-info">
<img
:src="getFileTypeIcon(getFileExtension(result.retrievalDto?.name || ''))"
:alt="getFileExtension(result.retrievalDto?.name || '')"
class="file-type-icon"
/>
<h3 class="result-title" @click="handleTitleClick(result)">
{{ result.retrievalDto?.name || "未知文档" }}
</h3>
</div>
<div class="confidence-score">
<span class="confidence-label">置信度:</span>
<span class="confidence-value" :class="getConfidenceClass(result.score)">
{{ (parseFloat(result.score) * 100).toFixed(1) }}%
</span>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="!hasSearched">
<div class="empty-icon">
<svg viewBox="0 0 24 24">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
<!-- 文档内容摘要 -->
<div class="result-content">
<p class="content-snippet" v-html="getHighlightedContent(result.retrievalDto?.content || '暂无内容', getGlobalIndex(index + leftColumnResults.length))">
</p>
<div v-if="(result.retrievalDto?.content || '').length > 200" class="expand-btn" @click="toggleExpand(getGlobalIndex(index + leftColumnResults.length))">
{{ expandedItems.includes(getGlobalIndex(index + leftColumnResults.length)) ? '收起' : '展开更多' }}
</div>
</div>
<!-- 文档信息 -->
<div class="result-meta">
<span class="meta-item">
<svg class="meta-icon" viewBox="0 0 24 24">
<path d="M12,2A2,2 0 0,1 14,4C14,4.74 13.6,5.39 13,5.73V7H14A7,7 0 0,1 21,14H22A1,1 0 0,1 23,15V18A1,1 0 0,1 22,19H21V20A2,2 0 0,1 19,22H5A2,2 0 0,1 3,20V19H2A1,1 0 0,1 1,18V15A1,1 0 0,1 2,14H3A7,7 0 0,1 10,7H11V5.73C10.4,5.39 10,4.74 10,4A2,2 0 0,1 12,2M7.5,13A2.5,2.5 0 0,0 5,15.5A2.5,2.5 0 0,0 7.5,18A2.5,2.5 0 0,0 10,15.5A2.5,2.5 0 0,0 16.5,18A2.5,2.5 0 0,0 19,15.5A2.5,2.5 0 0,0 16.5,13Z" />
</svg>
知识库: {{ result.retrievalDto?.datasetName || '未知知识库' }}
</span>
</div>
<h3>开始您的智能检索</h3>
<p>输入关键词从海量文档中快速找到您需要的信息</p>
<!-- 操作按钮 -->
<div class="result-actions">
<el-button
type="info"
:icon="View"
text
class="action-btn preview-btn"
@click="handlePreview(result)"
v-if="result.retrievalDto?.sourceUrl && canPreview(result.retrievalDto?.name)"
>
预览
</el-button>
<el-button
type="success"
:icon="Download"
text
class="action-btn"
@click="handleDownload(result)"
v-if="result.retrievalDto?.sourceUrl"
>
下载
</el-button>
</div>
</div>
</div>
<!-- 无结果状态 -->
<div class="no-results" v-if="hasSearched && allResults.length === 0 && !loading">
<div class="no-results-icon">
<svg viewBox="0 0 24 24">
<path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z" />
</svg>
</div>
<h3>未找到相关结果</h3>
<p>请尝试使用不同的关键词或搜索方法</p>
</div>
<!-- 预览抽屉 -->
<el-drawer
v-model="previewDrawerVisible"
title="文件预览"
:direction="'rtl'"
size="50%"
class="preview-drawer"
>
<div v-loading="previewLoading">
<!-- 右侧预览窗口 -->
<div class="preview-panel" v-if="previewDrawerVisible">
<div class="preview-header">
<h3>文件预览</h3>
<el-button
type="text"
@click="closePreview"
class="close-preview-btn"
>
<svg viewBox="0 0 24 24" width="16" height="16">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</el-button>
</div>
<div class="preview-content" v-loading="previewLoading">
<!-- TXT 文件预览 -->
<div v-if="previewFileType === 'txt'" class="text-preview-container">
<pre class="text-preview">{{ previewTextContent }}</pre>
@ -297,7 +355,45 @@
<p>您可以下载文件后使用相应的应用程序打开</p>
</div>
</div>
</el-drawer>
</div>
</div>
<!-- 分页组件 -->
<div class="pagination-container" v-if="totalResults > pageSize">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[5, 10, 20, 50]"
:total="totalResults"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<!-- 空状态 -->
<div class="empty-state" v-if="!hasSearched">
<div class="empty-icon">
<svg viewBox="0 0 24 24">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>
</div>
<h3>开始您的智能检索</h3>
<p>输入关键词从海量文档中快速找到您需要的信息</p>
</div>
<!-- 无结果状态 -->
<div class="no-results" v-if="hasSearched && allResults.length === 0 && !loading">
<div class="no-results-icon">
<svg viewBox="0 0 24 24">
<path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z" />
</svg>
</div>
<h3>未找到相关结果</h3>
<p>请尝试使用不同的关键词或搜索方法</p>
</div>
<div>
@ -378,6 +474,8 @@ const previewFileUrl = ref('')
const previewTextContent = ref('')
const previewMarkdownContent = ref('')
const dialogVisible = ref(false)
const selectedPreviewIndex = ref(-1) //
const headerCollapsed = ref(false) //
const knowledgeList = ref([])
@ -423,6 +521,11 @@ const removeKnowledge = (kbId: string) => {
const handleSelectKnowledge = () => {
dialogVisible.value = true
}
//
const toggleHeaderCollapse = () => {
headerCollapsed.value = !headerCollapsed.value
}
//
const handleSearch = async () => {
if (!searchQuery.value.trim()) {
@ -457,6 +560,13 @@ const handleSearch = async () => {
totalResults.value = allResults.value.length
currentPage.value = 1 //
searchTime.value = ((Date.now() - startTime) / 1000).toFixed(2)
//
if (allResults.value.length > 0) {
setTimeout(() => {
headerCollapsed.value = true
}, 500) // 500ms
}
} catch (error) {
console.error('搜索失败:', error)
ElMessage.error('搜索失败,请稍后重试')
@ -572,6 +682,24 @@ const paginatedResults = computed(() => {
return allResults.value.slice(start, end)
})
//
const leftColumnResults = computed(() => {
if (previewDrawerVisible.value) {
return paginatedResults.value
}
// 1,3,5...
return paginatedResults.value.filter((_, index) => index % 2 === 0)
})
//
const rightColumnResults = computed(() => {
if (previewDrawerVisible.value) {
return []
}
// 2,4,6...
return paginatedResults.value.filter((_, index) => index % 2 === 1)
})
//
const handleSizeChange = (newSize: number) => {
pageSize.value = newSize
@ -657,8 +785,13 @@ const handlePreview = async (result: any) => {
return
}
//
previewDrawerVisible.value = true
previewLoading.value = true
//
const resultIndex = paginatedResults.value.findIndex(r => r === result)
selectedPreviewIndex.value = getGlobalIndex(resultIndex)
// Excel
excelRowLimited.value = false
@ -725,6 +858,17 @@ const handlePreview = async (result: any) => {
}
}
//
const closePreview = () => {
previewDrawerVisible.value = false
selectedPreviewIndex.value = -1
previewLoading.value = false
previewFileType.value = ''
previewFileUrl.value = ''
previewTextContent.value = ''
previewMarkdownContent.value = ''
}
//
const handleDownloadFromPreview = async () => {
// URL
@ -829,6 +973,16 @@ onMounted(() => {
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
position: relative;
z-index: 2;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.search-header.collapsed {
padding: 15px 20px;
}
.search-header.collapsed .search-logo h1 {
font-size: 24px;
margin-bottom: 0;
}
.search-logo h1 {
@ -856,6 +1010,41 @@ onMounted(() => {
border-radius: 2px;
}
/* 折叠按钮样式 */
.collapse-toggle-btn {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.1);
color: #667eea;
}
.collapse-toggle-btn:hover {
background: rgba(102, 126, 234, 0.1);
border-color: rgba(102, 126, 234, 0.5);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.2);
transform: translateY(-50%) scale(1.05);
}
.collapse-toggle-btn svg {
transition: transform 0.3s ease;
}
.collapse-toggle-btn.collapsed svg {
transform: rotate(180deg);
}
/* 搜索框容器 */
.search-box-container {
max-width: 600px;
@ -1005,7 +1194,7 @@ onMounted(() => {
.search-results {
flex: 1;
overflow-y: auto;
padding: 20px;
padding: 20px 0;
position: relative;
z-index: 1;
background: rgba(255, 255, 255, 0.05);
@ -1016,12 +1205,99 @@ onMounted(() => {
color: #70757a;
font-size: 14px;
margin-bottom: 20px;
padding-left: 8px;
padding: 0 20px;
}
.results-list {
max-width: 800px;
margin-bottom: 20px;
/* 结果容器 */
.results-container {
display: flex;
gap: 20px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
width: 100%;
padding: 0 20px;
}
.results-container.preview-mode {
gap: 10px;
padding: 0 10px;
}
/* 左侧结果列表 */
.results-list-left {
flex: 1;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.results-container.preview-mode .results-list-left {
flex: 0 0 50%;
max-width: 50%;
}
/* 右侧结果列表 */
.results-list-right {
flex: 1;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
opacity: 1;
transform: translateX(0);
}
.results-container.preview-mode .results-list-right {
opacity: 0;
transform: translateX(100%);
pointer-events: none;
}
/* 预览面板 */
.preview-panel {
flex: 0 0 50%;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
opacity: 0;
transform: translateX(100%);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.results-container.preview-mode .preview-panel {
opacity: 1;
transform: translateX(0);
}
.preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid rgba(102, 126, 234, 0.1);
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
}
.preview-header h3 {
margin: 0;
color: #667eea;
font-size: 18px;
font-weight: 600;
}
.close-preview-btn {
color: #909399;
transition: all 0.2s ease;
}
.close-preview-btn:hover {
color: #667eea;
background-color: rgba(102, 126, 234, 0.1);
}
.preview-content {
flex: 1;
overflow: auto;
padding: 0;
}
/* 结果项 */
@ -1058,6 +1334,19 @@ onMounted(() => {
border-color: rgba(102, 126, 234, 0.3);
}
/* 预览选中状态 */
.result-item.preview-selected {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
border-color: rgba(102, 126, 234, 0.5);
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.2);
transform: translateY(-2px);
}
.result-item.preview-selected::before {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 4px;
}
@keyframes slideInUp {
from {
opacity: 0;
@ -1257,7 +1546,7 @@ onMounted(() => {
.pagination-container {
display: flex;
justify-content: center;
padding: 20px 0;
padding: 20px;
border-top: 1px solid #e8eaed;
margin-top: 20px;
}
@ -1302,6 +1591,21 @@ onMounted(() => {
}
/* 响应式设计 */
@media (max-width: 1200px) {
.results-container {
padding: 0 15px;
}
.results-container.preview-mode {
padding: 0 8px;
}
.results-container.preview-mode .results-list-left,
.results-container.preview-mode .preview-panel {
flex: 0 0 50%;
}
}
@media (max-width: 768px) {
.search-header {
padding: 16px;
@ -1311,6 +1615,16 @@ onMounted(() => {
font-size: 24px;
}
.collapse-toggle-btn {
right: 10px;
width: 36px;
height: 36px;
}
.search-header.collapsed .search-logo h1 {
font-size: 20px;
}
.search-box-container {
max-width: 100%;
}
@ -1319,6 +1633,28 @@ onMounted(() => {
gap: 12px;
}
.results-container {
flex-direction: column;
gap: 16px;
padding: 0 10px;
}
.results-container.preview-mode {
flex-direction: column;
padding: 0 5px;
}
.results-container.preview-mode .results-list-left {
flex: 1;
max-width: 100%;
}
.results-container.preview-mode .preview-panel {
flex: 1;
max-width: 100%;
margin-top: 16px;
}
.result-header {
flex-direction: column;
align-items: flex-start;
@ -1354,6 +1690,14 @@ onMounted(() => {
.pagination-container :deep(.el-pagination .el-pagination__jump) {
display: none;
}
.preview-header {
padding: 16px 20px;
}
.preview-header h3 {
font-size: 16px;
}
}
/* 操作按钮样式 */

View File

@ -98,7 +98,7 @@
type="index"
:index="indexMethod"
/>
<el-table-column prop="fileName" :label="t('vabI18n.knowledge.document.table.fileName')" min-width="250" sortable="custom" :sort-orders="['ascending', 'descending']">
<el-table-column prop="fileName" :label="t('vabI18n.knowledge.document.table.fileName')" min-width="200" sortable="custom" :sort-orders="['ascending', 'descending']">
<template #default="{ row }">
<div class="file-name-cell">
<img
@ -134,13 +134,13 @@
</template>
</el-table-column>
<el-table-column prop="createDate" :label="t('vabI18n.knowledge.document.table.createDate')" width="180" sortable="custom" :sort-orders="['ascending', 'descending']" />
<el-table-column prop="charCount" :label="t('vabI18n.knowledge.document.table.charCount')" width="120" align="right" sortable="custom" :sort-orders="['ascending', 'descending']">
<el-table-column prop="charCount" :label="t('vabI18n.knowledge.document.table.fileSize', '文件大小')" width="120" align="right" sortable="custom" :sort-orders="['ascending', 'descending']">
<template #default="{ row }">
<span class="char-count" v-if="row.type === 'file'">{{ formatFileSize(row.charCount) }}</span>
<span v-else class="folder-indicator">--</span>
</template>
</el-table-column>
<el-table-column :label="t('vabI18n.knowledge.document.table.actions')" width="380" fixed="right">
<el-table-column :label="t('vabI18n.knowledge.document.table.actions')" width="320" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<!-- 文件操作 -->
@ -1594,6 +1594,7 @@ const handleClosePreviewDialog = () => {
gap: 20px;
position: relative;
transition: all 0.3s ease;
width: 100%;
}
@ -1603,6 +1604,8 @@ const handleClosePreviewDialog = () => {
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
width: 100%;
flex: 1;
}
.action-bar {