refactor: 重构组件和样式以改善UI/UX

- 重构FileDetailModal.vue,增加了下载选项下拉菜单并优化了布局。
- 移除部分页面的响应式媒体查询,不做响应式适配,仅适配大众尺寸
- 改进了ChunkParamsConfig.vue的布局,更好地分组参数。
- 调整了FileTable.vue,改善排序和筛选UI,包括状态筛选逻辑。
- 优化了FileUploadModal.vue的布局,提高OCR引擎配置的可见性。
- 其他  AiTextarea.vue / DataBaseView.vue / AppLayout.vue / file_utils.js / DataBaseInfoView.vue / FeedbackModalComponent.vue / FileTable.vue
This commit is contained in:
Wenjie Zhang 2026-01-15 04:54:53 +08:00
parent 55dbbf1846
commit 62685b7ffe
14 changed files with 485 additions and 313 deletions

View File

@ -1,5 +1,5 @@
<template>
<div class="chat-container" ref="chatContainerRef">
<div class="chat-container">
<ChatSidebarComponent
:current-chat-id="currentChatId"
:chats-list="chatsList"
@ -16,13 +16,10 @@
@toggle-sidebar="toggleSidebar"
@open-agent-modal="openAgentModal"
:class="{
'floating-sidebar': isSmallContainer,
'sidebar-open': chatUIStore.isSidebarOpen,
'no-transition': localUIState.isInitialRender,
'collapsed': isSmallContainer && !chatUIStore.isSidebarOpen
'no-transition': localUIState.isInitialRender
}"
/>
<div class="sidebar-backdrop" v-if="chatUIStore.isSidebarOpen && isSmallContainer" @click="toggleSidebar"></div>
<div class="chat">
<div class="chat-header">
<div class="header__left">
@ -39,7 +36,7 @@
>
<LoaderCircle v-if="chatUIStore.creatingNewChat" class="nav-btn-icon loading-icon" size="18"/>
<MessageCirclePlus v-else class="nav-btn-icon" size="18"/>
<span class="text" :class="{'hide-text': isMediumContainer}">新对话</span>
<span class="text">新对话</span>
</div>
<div
v-if="!props.singleMode"
@ -68,7 +65,7 @@
<span v-if="hasAgentStateContent" class="text">状态</span>
</div>
</AgentPopover>
<slot name="header-right" :is-medium-container="isMediumContainer"></slot>
<slot name="header-right"></slot>
</div>
</div>
@ -239,7 +236,6 @@ const threadMessages = ref({});
// UI 使
const localUIState = reactive({
isInitialRender: true,
containerWidth: 0,
});
// AgentState Popover
@ -367,25 +363,12 @@ const isStreaming = computed(() => {
return threadState ? threadState.isStreaming : false;
});
const isProcessing = computed(() => isStreaming.value);
const isSmallContainer = computed(() => localUIState.containerWidth <= 520);
const isMediumContainer = computed(() => localUIState.containerWidth <= 768);
// ==================== SCROLL & RESIZE HANDLING ====================
const chatContainerRef = ref(null);
const scrollController = new ScrollController('.chat');
let resizeObserver = null;
onMounted(() => {
nextTick(() => {
if (chatContainerRef.value) {
localUIState.containerWidth = chatContainerRef.value.offsetWidth;
resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
localUIState.containerWidth = entry.contentRect.width;
}
});
resizeObserver.observe(chatContainerRef.value);
}
const chatContainer = document.querySelector('.chat');
if (chatContainer) {
chatContainer.addEventListener('scroll', scrollController.handleScroll, { passive: true });
@ -395,7 +378,6 @@ onMounted(() => {
});
onUnmounted(() => {
if (resizeObserver) resizeObserver.disconnect();
scrollController.cleanup();
// 线
resetOnGoingConv();
@ -1029,38 +1011,6 @@ watch(conversations, () => {
position: relative;
}
.sidebar-backdrop {
display: none; /* 默认隐藏通过v-if控制显示 */
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
z-index: 99;
animation: fadeIn 0.3s ease;
}
.floating-sidebar {
position: absolute !important;
z-index: 1001;
height: 100%;
left: 0;
top: 0;
transform: translateX(0);
transition: transform 0.3s ease;
width: 80% !important;
max-width: 300px;
&.no-transition {
transition: none !important;
}
&.collapsed {
transform: translateX(-100%);
}
}
.chat {
position: relative;
flex: 1;
@ -1332,14 +1282,7 @@ watch(conversations, () => {
}
}
@media (max-width: 768px) {
.chat-sidebar.collapsed {
width: 0;
border: none;
}
.chat-header {
.header__left {
.text {
@ -1348,56 +1291,17 @@ watch(conversations, () => {
}
}
}
@media (max-width: 520px) {
.sidebar-backdrop {
display: block;
}
.chat-box {
padding: 1rem 1rem;
}
.bottom {
padding: 0.5rem 0.5rem;
}
.chat-header {
padding: 0.5rem 0 !important;
}
.floating-sidebar {
position: fixed;
z-index: 100;
height: 100%;
left: 0;
top: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateX(0);
transition: transform 0.3s ease;
width: 80% !important;
max-width: 300px;
&.collapsed {
transform: translateX(-100%);
}
}
}
.hide-text {
display: none;
}
</style>
<style lang="less">
.agent-nav-btn {
display: flex;
gap: 10px;
padding: 6px 14px;
padding: 6px 8px;
height: 32px;
justify-content: center;
align-items: center;
border-radius: 12px;
border-radius: 8px;
color: var(--gray-900);
cursor: pointer;
width: auto;
@ -1407,7 +1311,7 @@ watch(conversations, () => {
background: transparent;
&:hover:not(.is-disabled) {
background-color: var(--gray-50);
background-color: var(--gray-100);
}
&.is-disabled {
@ -1417,7 +1321,7 @@ watch(conversations, () => {
}
.nav-btn-icon {
height: 24px;
height: 18px;
}
.loading-icon {

View File

@ -643,6 +643,7 @@ const resetConfig = async () => {
.save-btn {
flex: 1;
height: 42px;
background-color: var(--gray-100);
border: none;
border-radius: 6px;
@ -1133,7 +1134,7 @@ const resetConfig = async () => {
//
@media (max-width: 768px) {
.agent-config-sidebar.open {
width: 100vw;
width: 100%;
}
}
</style>

View File

@ -16,9 +16,9 @@
@click="generateDescription"
>
<template #icon>
<Sparkles size="14" />
<WandSparkles size="14" />
</template>
<span v-if="!loading" class="ai-text">润色</span>
<span v-if="!loading" class="ai-text">{{ modelValue?.trim() ? '润色' : '生成' }}</span>
</a-button>
</a-tooltip>
</div>
@ -28,7 +28,7 @@
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import { databaseApi } from '@/apis/knowledge_api'
import { Sparkles } from 'lucide-vue-next'
import { WandSparkles } from 'lucide-vue-next'
const props = defineProps({
modelValue: {
@ -97,7 +97,7 @@ const generateDescription = async () => {
z-index: 1;
display: flex;
align-items: center;
gap: 2px;
gap: 4px;
padding: 2px 6px;
height: 24px;
color: var(--main-color);

View File

@ -243,12 +243,6 @@ const toggleCollapse = () => {
transform: translateX(-12px);
}
&.floating-sidebar .sidebar-content {
width: 100%;
min-width: 0;
max-width: 300px;
}
&.no-transition {
transition: none !important;
}

View File

@ -9,15 +9,17 @@
autocomplete="off"
layout="vertical"
>
<a-form-item label="Chunk Size" name="chunk_size">
<a-input-number v-model:value="tempChunkParams.chunk_size" :min="100" :max="10000" style="width: 100%;" />
<p class="param-description">每个文本片段的最大字符数</p>
</a-form-item>
<a-form-item label="Chunk Overlap" name="chunk_overlap">
<a-input-number v-model:value="tempChunkParams.chunk_overlap" :min="0" :max="1000" style="width: 100%;" />
<p class="param-description">相邻文本片段间的重叠字符数</p>
</a-form-item>
<a-form-item v-if="showQaSplit" label="分隔符 (Separator)" name="qa_separator">
<div class="chunk-row">
<a-form-item label="Chunk Size" name="chunk_size">
<a-input-number v-model:value="tempChunkParams.chunk_size" :min="100" :max="10000" style="width: 100%;" />
<p class="param-description">每个文本片段的最大字符数</p>
</a-form-item>
<a-form-item label="Chunk Overlap" name="chunk_overlap">
<a-input-number v-model:value="tempChunkParams.chunk_overlap" :min="0" :max="1000" style="width: 100%;" />
<p class="param-description">相邻文本片段间的重叠字符数</p>
</a-form-item>
</div>
<a-form-item v-if="showQaSplit" class="qa-separator-item" label="分隔符 (Separator)" name="qa_separator">
<a-input
v-model:value="tempChunkParams.qa_separator"
placeholder="输入分隔符,例如 \n\n\n 或 ---"
@ -59,10 +61,26 @@ defineProps({
line-height: 1.5;
}
.chunk-row {
display: flex;
gap: 16px;
margin-bottom: 8px;
}
.chunk-row > .ant-form-item {
flex: 1;
margin-bottom: 0;
}
.qa-separator-item {
margin-top: 8px;
margin-bottom: 0;
}
.param-description {
font-size: 12px;
color: var(--gray-400);
margin: 4px 0 0 0;
line-height: 1.4;
}
</style>
</style>

View File

@ -3,63 +3,118 @@
v-model:open="visible"
width="1200px"
:footer="null"
:closable="false"
wrap-class-name="file-detail"
@after-open-change="afterOpenChange"
:bodyStyle="{ height: '80vh', padding: '0' }"
>
<template #title>
<div class="modal-title-wrapper">
<span>{{ file?.filename || '文件详情' }}</span>
<div class="download-buttons" v-if="file">
<a-button
type="text"
size="small"
@click="handleDownloadOriginal"
:loading="downloadingOriginal"
:disabled="!file.file_id"
:icon="h(DownloadOutlined)"
>
下载原文
</a-button>
<a-button
type="text"
size="small"
@click="handleDownloadMarkdown"
:loading="downloadingMarkdown"
:disabled="!((file.lines && file.lines.length > 0) || file.content)"
:icon="h(DownloadOutlined)"
>
下载 Markdown
</a-button>
<!-- 左侧文件名和图标 -->
<div class="file-title">
<component :is="fileIcon" :style="{ color: fileIconColor, fontSize: '18px' }" />
<span class="file-name">{{ file?.filename || '文件详情' }}</span>
</div>
<div class="header-controls">
<!-- 字符数/片段数显示在 segment 左边 -->
<span class="view-info">
{{ viewMode === 'chunks' ? chunkCount + ' 个片段' : formatTextLength(charCount) + ' 字符' }}
</span>
<!-- 视图模式切换 -->
<div class="view-controls" v-if="file && hasContent">
<a-segmented
v-model:value="viewMode"
:options="viewModeOptions"
/>
</div>
<!-- 下载按钮下拉菜单 -->
<a-dropdown trigger="click" v-if="file">
<a-button type="default" class="download-btn">
<template #icon><Download :size="16" /></template>
下载
<ChevronDown :size="16" style="margin-left: 4px;" />
</a-button>
<template #overlay>
<a-menu @click="handleDownloadMenuClick">
<a-menu-item key="original" :disabled="!file.file_id">
<template #icon><Download :size="16" /></template>
下载原文
</a-menu-item>
<a-menu-item key="markdown" :disabled="!((file.lines && file.lines.length > 0) || file.content)">
<template #icon><FileText :size="16" /></template>
下载 Markdown
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<!-- 自定义关闭按钮 -->
<button class="custom-close-btn" @click="visible = false">
<X :size="16" />
</button>
</div>
</div>
</template>
<div class="file-detail-content" v-if="file">
<div class="file-content-section" v-if="(file.lines && file.lines.length > 0) || file.content">
<MarkdownContentViewer :chunks="file.lines" :content="file.content" />
<!-- Markdown 模式 -->
<div v-if="viewMode === 'markdown'" class="content-panel">
<MdPreview
v-if="mergedContent"
:modelValue="mergedContent"
:theme="theme"
previewTheme="github"
class="markdown-content"
/>
<div v-else class="empty-content">
<p>暂无文件内容</p>
</div>
</div>
<div v-else-if="loading" class="loading-container">
<a-spin />
<!-- Chunks 模式使用 Grid 布局 -->
<div v-else-if="viewMode === 'chunks'" class="chunks-panel">
<div class="chunk-grid">
<div
v-for="chunk in mappedChunks"
:key="chunk.id"
class="chunk-card"
>
<div class="chunk-card-header">
<span class="chunk-order">#{{ chunk.chunk_order_index }}</span>
</div>
<div class="chunk-card-content">
{{ chunk.content.replace(/\n+/g, ' ') }}
</div>
</div>
</div>
<div v-if="mappedChunks.length === 0" class="empty-content">
<p>暂无分块信息</p>
</div>
</div>
</div>
<div v-else class="empty-content">
<p>暂无文件内容</p>
</div>
<div v-else-if="loading" class="loading-container">
<a-spin />
</div>
</a-modal>
</template>
<script setup>
import { computed, ref, h } from 'vue';
import { computed, ref, watch } from 'vue';
import { useDatabaseStore } from '@/stores/database';
import { useThemeStore } from '@/stores/theme';
import { message } from 'ant-design-vue';
import { DownloadOutlined } from '@ant-design/icons-vue';
import { documentApi } from '@/apis/knowledge_api';
import { mergeChunks } from '@/utils/chunkUtils';
import MarkdownContentViewer from './MarkdownContentViewer.vue';
import { getFileIcon, getFileIconColor } from '@/utils/file_utils';
import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/preview.css';
import { Download, ChevronDown, FileText, X } from 'lucide-vue-next';
const store = useDatabaseStore();
const themeStore = useThemeStore();
const visible = computed({
get: () => store.state.fileDetailModalVisible,
@ -68,12 +123,70 @@ const visible = computed({
const file = computed(() => store.selectedFile);
const loading = computed(() => store.state.fileDetailLoading);
//
const fileIcon = computed(() => getFileIcon(file.value?.filename));
const fileIconColor = computed(() => getFileIconColor(file.value?.filename));
const downloadingOriginal = ref(false);
const downloadingMarkdown = ref(false);
//
const theme = computed(() => themeStore.isDark ? 'dark' : 'light');
//
const viewMode = ref('markdown');
const hasIndexed = computed(() => ['done', 'indexed'].includes(file.value?.status));
const hasContent = computed(() => (file.value?.lines && file.value?.lines.length > 0) || file.value?.content);
const viewModeOptions = computed(() => {
const options = [{ label: 'Markdown', value: 'markdown' }];
if (hasIndexed.value) {
options.push({ label: 'Chunks', value: 'chunks' });
}
return options;
});
//
watch(file, (newFile) => {
if (newFile) {
if (!hasIndexed.value) {
viewMode.value = 'markdown';
}
}
});
//
const mergeResult = computed(() => mergeChunks(file.value?.lines || []));
const mappedChunks = computed(() => mergeResult.value.chunks);
const mergedContent = computed(() => file.value?.content || mergeResult.value.content || '');
const charCount = computed(() => mergedContent.value.length);
const chunkCount = computed(() => mappedChunks.value.length || file.value?.lines?.length || 0);
//
function formatTextLength(length) {
if (!length && length !== 0) return '0 字符';
if (length < 1000) {
return `${length}`;
} else {
return `${(length / 1000).toFixed(1)}k`;
}
}
const afterOpenChange = (open) => {
if (!open) {
store.selectedFile = null;
viewMode.value = 'markdown';
}
};
//
const handleDownloadMenuClick = ({ key }) => {
if (key === 'original') {
handleDownloadOriginal();
} else if (key === 'markdown') {
handleDownloadMarkdown();
}
};
@ -143,12 +256,7 @@ const handleDownloadOriginal = async () => {
// Markdown
const handleDownloadMarkdown = () => {
let content = '';
if (file.value.content) {
content = file.value.content;
} else if (file.value.lines && file.value.lines.length > 0) {
content = mergeChunks(file.value.lines).content;
}
const content = mergedContent.value;
if (!content) {
message.error('没有可下载的 Markdown 内容');
@ -198,10 +306,17 @@ const handleDownloadMarkdown = () => {
flex-direction: column;
}
.file-content-section h4 {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
.content-panel,
.chunks-panel {
flex: 1;
overflow-y: auto;
padding: 16px 0;
background: var(--gray-0);
min-height: 0;
}
.markdown-content {
min-height: 100%;
}
.loading-container {
@ -215,6 +330,50 @@ const handleDownloadMarkdown = () => {
text-align: center;
padding: 40px 0;
color: var(--gray-400);
width: 100%;
}
/* Chunks 面板样式 */
.chunk-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
}
.chunk-card {
background: var(--gray-0);
border: 1px solid var(--gray-200);
border-radius: 8px;
padding: 12px;
transition: all 0.2s ease;
}
.chunk-card:hover {
border-color: var(--main-color);
box-shadow: 0 2px 8px rgba(1, 97, 121, 0.1);
}
.chunk-card-header {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.chunk-order {
font-weight: 600;
color: var(--main-color);
font-size: 12px;
}
.chunk-card-content {
font-size: 12px;
color: var(--gray-600);
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
}
</style>
@ -239,10 +398,146 @@ const handleDownloadMarkdown = () => {
padding-right: 8px;
}
.download-buttons {
/* 文件标题样式 */
.file-title {
display: flex;
align-items: center;
gap: 8px;
margin: 0 16px;
flex-shrink: 0;
}
</style>
.file-name {
font-weight: 600;
font-size: 15px;
color: var(--gray-900);
}
.title-info {
font-size: 13px;
color: var(--gray-600);
font-weight: 500;
}
.header-controls {
display: flex;
align-items: center;
gap: 12px;
margin-left: auto;
}
/* 下载按钮样式 */
.download-btn {
display: inline-flex;
align-items: center;
padding: 4px 8px;
height: 28px;
font-size: 13px;
line-height: 1;
border-radius: 6px;
gap: 4px;
svg {
vertical-align: middle;
}
}
/* 自定义关闭按钮 */
.custom-close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
color: var(--gray-500);
transition: all 0.2s;
&:hover {
background: var(--gray-100);
color: var(--gray-700);
}
}
/* 视图切换控件 */
.view-controls {
display: flex;
align-items: center;
gap: 8px;
}
.view-info {
font-size: 12px;
color: var(--gray-500);
white-space: nowrap;
}
/* 下拉菜单样式 */
.ant-dropdown-menu {
border-radius: 8px;
padding: 4px;
}
.ant-dropdown-menu-item {
border-radius: 6px;
display: flex;
align-items: center;
padding: 8px 12px;
svg {
margin-right: 8px;
}
}
</style>
<style lang="less">
/* MdPreview 覆盖样式 - 非 scoped */
.markdown-content {
.md-editor-preview-wrapper {
padding: 0;
}
#md-editor-v-2-preview-wrapper {
font-size: 14px;
line-height: 1.75;
color: var(--gray-1000);
}
#md-editor-v-2-preview-wrapper h1 {
font-size: 1.5rem;
margin: 16px 0 12px;
font-weight: 600;
}
#md-editor-v-2-preview-wrapper h2 {
font-size: 1.4rem;
margin: 16px 0 12px;
font-weight: 600;
}
#md-editor-v-2-preview-wrapper h3 {
font-size: 1.3rem;
margin: 14px 0 10px;
font-weight: 600;
}
#md-editor-v-2-preview-wrapper h4 {
font-size: 1.2rem;
margin: 14px 0 10px;
font-weight: 600;
}
#md-editor-v-2-preview-wrapper h5 {
font-size: 1.1rem;
margin: 12px 0 8px;
font-weight: 600;
}
#md-editor-v-2-preview-wrapper h6 {
font-size: 1rem;
margin: 12px 0 8px;
font-weight: 600;
}
}
</style>

View File

@ -39,7 +39,7 @@
</a-input>
<a-dropdown trigger="click">
<a-button type="text" class="panel-action-btn" title="排序">
<a-button type="text" class="panel-action-btn" :class="{ 'active': sortField !== 'filename' }" title="排序">
<template #icon><ArrowUpDown size="16" /></template>
</a-button>
<template #overlay>
@ -52,11 +52,11 @@
</a-dropdown>
<a-dropdown trigger="click">
<a-button type="text" class="panel-action-btn" :class="{ 'active': statusFilter }" title="筛选状态">
<a-button type="text" class="panel-action-btn" :class="{ 'active': statusFilter !== 'all' }" title="筛选状态">
<template #icon><Filter size="16" /></template>
</a-button>
<template #overlay>
<a-menu :selectedKeys="statusFilter ? [statusFilter] : []" @click="handleStatusMenuClick">
<a-menu :selectedKeys="[statusFilter]" @click="handleStatusMenuClick">
<a-menu-item key="all">全部状态</a-menu-item>
<a-menu-item v-for="opt in statusOptions" :key="opt.value">
{{ opt.label }}
@ -114,7 +114,7 @@
/>
<span>{{ selectedRowKeys.length }} </span>
</div>
<div style="display: flex; gap: 8px;">
<div style="display: flex; gap: 2px;">
<a-button
type="link"
@click="handleBatchParse"
@ -202,7 +202,7 @@
<div v-if="column.key === 'filename'">
<template v-if="record.is_folder">
<span class="folder-row" @click="toggleExpand(record)">
<component :is="expandedRowKeys.includes(record.file_id) ? h(FolderOpenFilled) : h(FolderFilled)" style="margin-right: 12px; color: #ffb800;" />
<component :is="expandedRowKeys.includes(record.file_id) ? h(FolderOpenFilled) : h(FolderFilled)" style="margin-right: 8px; color: #ffb800; font-size: 16px;" />
{{ record.filename }}
</span>
</template>
@ -216,7 +216,7 @@
</div>
</template>
<a-button class="main-btn" type="link" @click="openFileDetail(record)">
<component :is="getFileIcon(record.displayName || text)" :style="{ marginRight: '6px', color: getFileIconColor(record.displayName || text) }" />
<component :is="getFileIcon(record.displayName || text)" :style="{ marginRight: '0', color: getFileIconColor(record.displayName || text), fontSize: '16px' }" />
{{ record.displayName || text }}
</a-button>
</a-popover>
@ -343,11 +343,7 @@ const handleSortMenuClick = (e) => {
};
const handleStatusMenuClick = (e) => {
if (e.key === 'all') {
statusFilter.value = null;
} else {
statusFilter.value = e.key;
}
statusFilter.value = e.key;
//
paginationConfig.value.current = 1;
};
@ -406,9 +402,9 @@ const allSelectableFiles = computed(() => {
// Follow getCheckboxProps logic
if (lock.value || file.status === 'processing' || file.status === 'waiting') return false;
if (nameFilter || status) {
if (nameFilter || status !== 'all') {
const nameMatch = !nameFilter || (file.filename && file.filename.toLowerCase().includes(nameFilter));
const statusMatch = !status || file.status === status ||
const statusMatch = status === 'all' || file.status === status ||
(status === 'indexed' && file.status === 'done') ||
(status === 'error_indexing' && file.status === 'failed');
return nameMatch && statusMatch;
@ -625,7 +621,7 @@ const handleTableChange = (pagination) => {
//
const filenameFilter = ref('');
const statusFilter = ref(null);
const statusFilter = ref('all');
const statusOptions = [
{ label: '已上传', value: 'uploaded' },
{ label: '解析中', value: 'parsing' },
@ -796,11 +792,11 @@ const filteredFiles = computed(() => {
const status = statusFilter.value;
//
if (nameFilter || status) {
if (nameFilter || status !== 'all') {
// /使
return files.value.filter(file => {
const nameMatch = !nameFilter || (file.filename && file.filename.toLowerCase().includes(nameFilter));
const statusMatch = !status || file.status === status ||
const statusMatch = status === 'all' || file.status === status ||
(status === 'indexed' && file.status === 'done') ||
(status === 'error_indexing' && file.status === 'failed');
return nameMatch && statusMatch;
@ -1134,7 +1130,7 @@ import ChunkParamsConfig from '@/components/ChunkParamsConfig.vue';
.panel-actions {
display: flex;
align-items: center;
gap: 4px;
gap: 6px;
.action-searcher {
width: 120px;
@ -1142,6 +1138,7 @@ import ChunkParamsConfig from '@/components/ChunkParamsConfig.vue';
border-radius: 6px;
padding: 4px 8px;
border: none;
box-shadow: 0 0 0 1px var(--shadow-1);
}
}
@ -1188,7 +1185,9 @@ import ChunkParamsConfig from '@/components/ChunkParamsConfig.vue';
flex: 1;
overflow: auto;
background-color: transparent;
min-height: 0; /* 让 flex 子项可以正确缩小 */
min-height: 0;
table-layout: fixed;
padding-left: 4px;
}
.my-table .main-btn {
@ -1262,6 +1261,12 @@ import ChunkParamsConfig from '@/components/ChunkParamsConfig.vue';
opacity: 0.9;
}
.my-table .span-type.md,
.my-table .span-type.markdown {
background-color: var(--gray-200);
color: var(--gray-800);
}
.auto-refresh-btn {
height: 24px;
padding: 0 8px;
@ -1272,15 +1277,13 @@ import ChunkParamsConfig from '@/components/ChunkParamsConfig.vue';
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
padding: 0 4px;
color: var(--gray-700);
border-radius: 8px;
padding: 4px;
color: var(--gray-600);
transition: all 0.1s ease;
font-size: 12px;
svg {
height: 16px;
}
width: auto;
height: auto;
&.expand {
transform: scaleX(-1);
@ -1307,6 +1310,7 @@ import ChunkParamsConfig from '@/components/ChunkParamsConfig.vue';
color: var(--main-color);
background-color: var(--gray-100);
font-weight: 600;
box-shadow: 0 0 0 1px var(--shadow-1);
}
.action-trigger-btn {

View File

@ -39,28 +39,26 @@
<!-- 2. 配置面板 -->
<div class="settings-panel">
<!-- 第一行存储位置 -->
<div class="setting-row">
<div class="setting-label">存储位置</div>
<div class="setting-content flex-row">
<a-tree-select
v-model:value="selectedFolderId"
show-search
class="folder-select"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
placeholder="选择目标文件夹(默认为根目录)"
allow-clear
tree-default-expand-all
:tree-data="folderTreeData"
tree-node-filter-prop="title"
>
</a-tree-select>
<!-- 第一行存储位置 + OCR 引擎 -->
<div class="setting-row two-cols">
<div class="col-item">
<div class="setting-label">存储位置</div>
<div class="setting-content flex-row">
<a-tree-select
v-model:value="selectedFolderId"
show-search
class="folder-select"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
placeholder="选择目标文件夹(默认为根目录)"
allow-clear
tree-default-expand-all
:tree-data="folderTreeData"
tree-node-filter-prop="title"
>
</a-tree-select>
</div>
<p class="param-description">选择文件保存的目标文件夹</p>
</div>
</div>
<!-- 第二行OCR (单列布局) -->
<div class="setting-row">
<!-- OCR 配置 -->
<div class="col-item">
<div class="setting-label">
OCR 引擎
@ -80,15 +78,20 @@
:disabled="ocrHealthChecking"
class="ocr-select"
/>
<!-- 紧凑的状态提示 -->
<div class="status-mini-tip" v-if="chunkParams.enable_ocr !== 'disable'">
<span v-if="selectedOcrStatus === 'healthy'" class="text-success">
<CheckCircleFilled /> {{ selectedOcrMessage || '服务正常' }}
</span>
<span v-else-if="selectedOcrStatus && selectedOcrStatus !== 'unknown'" class="text-warning">
{{ selectedOcrMessage || '服务异常' }}
</span>
</div>
<p class="param-description">
<template v-if="!isOcrEnabled">
不启用 OCR仅处理文本文件
</template>
<template v-else-if="selectedOcrStatus === 'healthy'">
{{ selectedOcrMessage || '服务正常' }}
</template>
<template v-else-if="selectedOcrStatus === 'unknown'">
点击刷新图标检查服务状态
</template>
<template v-else>
{{ selectedOcrMessage || '服务异常' }}
</template>
</p>
</div>
</div>
</div>
@ -878,7 +881,6 @@ const chunkData = async () => {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px;
}
.help-link-btn {
@ -934,7 +936,7 @@ const chunkData = async () => {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
gap: 4px;
min-width: 0; // Fix flex overflow
}
}
@ -983,15 +985,28 @@ const chunkData = async () => {
white-space: nowrap;
}
.status-mini-tip {
margin-top: 6px;
.param-description {
font-size: 12px;
color: var(--gray-400);
margin: 4px 0 0 0;
line-height: 1.4;
display: flex;
align-items: center;
gap: 4px;
.text-success { color: var(--color-success-500); }
.text-warning { color: var(--color-warning-500); }
.text-success {
color: var(--color-success-500);
display: flex;
align-items: center;
gap: 4px;
}
.text-warning {
color: var(--color-warning-500);
display: flex;
align-items: center;
gap: 4px;
}
}
/* Chunk Display Card */

View File

@ -13,7 +13,8 @@
@click="toggleChunkPanel"
:title="chunkPanelVisible ? '隐藏片段列表' : '显示片段列表'"
>
{{ chunkPanelVisible ? '«' : '»' }}
<ChevronLeft v-if="chunkPanelVisible" :size="14" />
<ChevronRight v-else :size="14" />
</button>
</div>
</div>
@ -98,6 +99,7 @@ import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/preview.css';
import { mergeChunks, getChunkPreview } from '@/utils/chunkUtils';
import { useThemeStore } from '@/stores/theme';
import { ChevronRight, ChevronLeft } from 'lucide-vue-next';
const props = defineProps({
chunks: {
@ -209,16 +211,18 @@ onUnmounted(() => {
}
.toggle-btn {
display: flex;
align-items: center;
justify-content: center;
background: var(--gray-100);
/* color: var(--gray-0); */
border: none;
border-radius: 4px;
width: 28px;
width: 24px;
height: 24px;
padding: 0;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.2s ease;
color: var(--gray-700);
}
.toggle-btn:hover {

View File

@ -460,6 +460,7 @@ watch(() => props.agentId, () => {
//
.empty-state {
grid-column: 1 / -1;
display: flex;
justify-content: center;
align-items: center;

View File

@ -192,10 +192,6 @@ provide('settingsModal', {
<UserInfoComponent />
</div>
</div>
<div class="header-mobile">
<RouterLink to="/agent" class="nav-item" active-class="active">对话</RouterLink>
<RouterLink to="/database" class="nav-item" active-class="active">知识</RouterLink>
</div>
<router-view v-slot="{ Component, route }" id="app-router-view">
<keep-alive v-if="route.meta.keepAlive !== false">
<component :is="Component" />
@ -235,10 +231,6 @@ provide('settingsModal', {
height: 100vh;
min-width: var(--min-width);
.header-mobile {
display: none;
}
.debug-panel {
position: absolute;
z-index: 100;
@ -293,7 +285,7 @@ div.header, #app-router-view {
.logo {
width: 34px;
height: 34px;
margin: 12px 0 20px 0;
margin: 6px 0 20px 0;
img {
width: 100%;
@ -417,49 +409,6 @@ div.header, #app-router-view {
}
@media (max-width: 520px) {
.app-layout {
flex-direction: column-reverse;
div.header {
display: none;
}
.debug-panel {
bottom: 10rem;
}
}
.app-layout div.header-mobile {
display: flex;
flex-direction: row;
width: 100%;
padding: 0 20px;
justify-content: space-around;
align-items: center;
flex: 0 0 60px;
border-right: none;
height: 40px;
.nav-item {
text-decoration: none;
width: 40px;
color: var(--gray-900);
font-size: 1rem;
font-weight: bold;
transition: color 0.1s ease-in-out, font-size 0.1s ease-in-out;
&.active {
color: var(--gray-10000);
font-size: 1.1rem;
}
}
}
.app-layout .chat-box::webkit-scrollbar {
width: 0;
}
}
.app-layout.use-top-bar {
flex-direction: column;
}

View File

@ -63,9 +63,9 @@ export const getFileIconColor = (filename) => {
'text': '#1890ff',
'log': '#1890ff',
// Markdown文件 - 深
'md': '#0050b3',
'markdown': '#0050b3',
// Markdown文件 - 深
'md': '#595959',
'markdown': '#595959',
// PDF文件 - 红色
'pdf': '#ff4d4f',

View File

@ -39,6 +39,7 @@
@click="openSearchConfigModal"
>
<SettingOutlined />
<span class="config-text">检索配置</span>
</a-button>
</a-tooltip>
</template>
@ -540,12 +541,13 @@ const handleMouseUp = () => {
}
.config-btn {
color: var(--gray-500);
color: var(--gray-600);
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
gap: 4px;
padding: 4px 8px;
height: 32px;
border-radius: 6px;
transition: all 0.2s;
@ -554,24 +556,9 @@ const handleMouseUp = () => {
color: var(--main-color);
background-color: var(--gray-100);
}
}
/* Responsive design for smaller screens */
@media (max-width: 768px) {
.unified-layout {
flex-direction: column;
}
.unified-layout .left-panel {
border-right: none;
border-bottom: 1px solid var(--gray-200);
}
.unified-layout .resize-handle {
width: 100%;
height: 2px;
cursor: row-resize;
.config-text {
font-size: 13px;
}
}

View File

@ -90,7 +90,7 @@
size="default"
/>
<span style="margin-left: 12px;">设置为私有知识库</span>
<a-tooltip title="在部分智能体的设计中,可以根据隐私标志来决定启用什么模型和策略。例如,对于私有知识库,可以选择更严格的数据处理和访问控制策略,以保护敏感信息的安全性和隐私性。">
<a-tooltip title="当前未使用此属性。在部分智能体的设计中,可以根据隐私标志来决定启用什么模型和策略。例如,对于私有知识库,可以选择更严格的数据处理和访问控制策略,以保护敏感信息的安全性和隐私性。">
<InfoCircleOutlined style="margin-left: 8px; color: var(--gray-500); cursor: help;" />
</a-tooltip>
</div>