feat: 更新组件样式和逻辑,优化用户体验

This commit is contained in:
Wenjie Zhang 2025-11-06 21:59:48 +08:00
parent 3a6e76e06e
commit f0275aaadc
7 changed files with 279 additions and 314 deletions

View File

@ -46,7 +46,7 @@
</div>
</div>
<div class="batch-actions-compact" v-if="selectedRowKeys.length > 0">
<div class="batch-actions" v-if="selectedRowKeys.length > 0">
<div class="batch-info">
<span>{{ selectedRowKeys.length }} </span>
<a-button
@ -74,7 +74,7 @@
:columns="columnsCompact"
:data-source="filteredFiles"
row-key="file_id"
class="my-table-compact"
class="my-table"
size="small"
:pagination="paginationCompact"
:row-selection="{
@ -373,7 +373,8 @@ import { parseToShanghai } from '@/utils/time';
.file-table-container {
display: flex;
flex-direction: column;
height: 100%;
max-height: 100%;
overflow: hidden;
border-radius: 12px;
border: 1px solid var(--gray-150);
padding-top: 6px;
@ -416,12 +417,12 @@ import { parseToShanghai } from '@/utils/time';
}
}
.batch-actions-compact {
.batch-actions {
display: flex;
align-items: center;
justify-content: space-between;
padding: 2px 4px;
background-color: var(--main-5);
padding: 2px 12px;
background-color: var(--main-10);
border-radius: 4px;
margin-bottom: 4px;
flex-shrink: 0;
@ -434,31 +435,31 @@ import { parseToShanghai } from '@/utils/time';
}
.batch-info span {
font-size: 11px;
font-size: 12px;
font-weight: 500;
color: var(--gray-700);
}
.batch-actions-compact .ant-btn {
font-size: 11px;
.batch-actions .ant-btn {
font-size: 12px;
padding: 0 6px;
height: 22px;
border-radius: 3px;
}
.batch-actions-compact .ant-btn:hover {
.batch-actions .ant-btn:hover {
background-color: var(--main-20);
color: var(--main-color);
}
.my-table-compact {
.my-table {
flex: 1;
overflow: auto;
background-color: transparent;
min-height: 0; /* 让 flex 子项可以正确缩小 */
}
.my-table-compact .main-btn {
.my-table .main-btn {
padding: 0;
height: auto;
line-height: 1.4;
@ -468,32 +469,32 @@ import { parseToShanghai } from '@/utils/time';
text-decoration: none;
}
.my-table-compact .main-btn:hover {
.my-table .main-btn:hover {
cursor: pointer;
color: var(--main-color);
}
.my-table-compact .del-btn {
.my-table .del-btn {
color: var(--gray-500);
}
.my-table-compact .download-btn {
.my-table .download-btn {
color: var(--gray-500);
}
.my-table-compact .download-btn:hover {
.my-table .download-btn:hover {
color: var(--main-color);
}
.my-table-compact .del-btn:hover {
.my-table .del-btn:hover {
color: var(--error-color);
}
.my-table-compact .del-btn:disabled {
.my-table .del-btn:disabled {
cursor: not-allowed;
}
.my-table-compact .span-type {
.my-table .span-type {
display: inline-block;
padding: 1px 5px;
font-size: 10px;

View File

@ -21,12 +21,12 @@
<div class="add-files-content">
<div class="upload-header">
<div class="source-selector">
<div class="upload-mode-selector" @click="uploadMode = 'file'" :class="{ active: uploadMode === 'file' }">
<FileOutlined /> 上传文件
</div>
<div class="upload-mode-selector" @click="uploadMode = 'url'" :class="{ active: uploadMode === 'url' }">
<LinkOutlined /> 输入网址
</div>
<a-segmented
v-model:value="uploadMode"
:options="uploadModeOptions"
size="large"
class="source-segmented"
/>
</div>
<div class="config-controls">
<a-button type="dashed" @click="showChunkConfigModal" v-if="!isGraphBased">
@ -35,7 +35,7 @@
</div>
</div>
<div class="ocr-config">
<div class="ocr-config" v-if="uploadMode === 'file'">
<a-form layout="horizontal">
<a-form-item label="使用OCR" name="enable_ocr">
<div class="ocr-controls">
@ -67,36 +67,6 @@
</a-form>
</div>
<div class="qa-split-config" v-if="isQaSplitSupported">
<a-form layout="horizontal">
<a-form-item label="QA分割模式" name="use_qa_split">
<div class="toggle-controls">
<a-switch
v-model:checked="chunkParams.use_qa_split"
style="margin-right: 12px;"
/>
<span class="param-description">
{{ chunkParams.use_qa_split ? '启用QA分割忽略chunk大小设置' : '使用普通分割模式' }}
</span>
</div>
</a-form-item>
<a-form-item
v-if="chunkParams.use_qa_split"
label="QA分隔符"
name="qa_separator"
>
<a-input
v-model:value="chunkParams.qa_separator"
placeholder="输入QA分隔符"
style="width: 200px; margin-right: 12px;"
/>
<span class="param-description">
用于分割不同QA对的分隔符默认为3个换行符
</span>
</a-form-item>
</a-form>
</div>
<!-- PDF/图片OCR提醒 -->
<div v-if="uploadMode === 'file' && hasPdfOrImageFiles && !isOcrEnabled" class="ocr-warning-alert">
检测到PDF或图片文件请启用OCR功能以提取文本内容
@ -181,7 +151,7 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, watch } from 'vue';
import { message, Upload } from 'ant-design-vue';
import { useUserStore } from '@/stores/user';
import { useDatabaseStore } from '@/stores/database';
@ -295,6 +265,24 @@ const chunkLoading = computed(() => store.state.chunkLoading);
//
const uploadMode = ref('file');
const previousOcrSelection = ref('disable');
const uploadModeOptions = computed(() => [
{
value: 'file',
label: h('div', { class: 'segmented-option' }, [
h(FileOutlined, { class: 'option-icon' }),
h('span', { class: 'option-text' }, '上传文件'),
]),
},
{
value: 'url',
label: h('div', { class: 'segmented-option' }, [
h(LinkOutlined, { class: 'option-icon' }),
h('span', { class: 'option-text' }, '输入网址'),
]),
},
]);
//
const fileList = ref([]);
@ -349,6 +337,15 @@ const isOcrEnabled = computed(() => {
return chunkParams.value.enable_ocr !== 'disable';
});
watch(uploadMode, (mode, previous) => {
if (mode === 'url') {
previousOcrSelection.value = chunkParams.value.enable_ocr;
chunkParams.value.enable_ocr = 'disable';
} else if (mode === 'file' && previous === 'url') {
chunkParams.value.enable_ocr = previousOcrSelection.value || 'disable';
}
});
// PDF
const hasPdfOrImageFiles = computed(() => {
if (fileList.value.length === 0) {
@ -647,25 +644,7 @@ const chunkData = async () => {
.source-selector {
display: flex;
gap: 12px;
}
.upload-mode-selector {
padding: 8px 16px;
border: 1px solid var(--gray-300);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
}
.upload-mode-selector:hover {
border-color: var(--main-color);
}
.upload-mode-selector.active {
border-color: var(--main-color);
background-color: var(--main-30);
color: var(--main-color);
align-items: center;
}
.config-controls {
@ -673,19 +652,52 @@ const chunkData = async () => {
align-items: center;
}
.ocr-config,
.qa-split-config {
.source-segmented {
background-color: var(--gray-100);
border-radius: 999px;
padding: 4px;
border: 1px solid var(--gray-200);
}
.source-segmented :deep(.ant-segmented-item) {
border-radius: 999px;
}
.source-segmented :deep(.ant-segmented-item-label) {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
font-weight: 500;
color: var(--gray-600);
}
.source-segmented :deep(.ant-segmented-thumb) {
background-color: var(--main-30);
border-radius: 999px;
}
.source-segmented :deep(.ant-segmented-item-selected .ant-segmented-item-label) {
color: var(--main-color);
}
.source-segmented :deep(.segmented-option) {
display: flex;
align-items: center;
gap: 8px;
}
.source-segmented :deep(.option-icon) {
font-size: 16px;
}
.ocr-config {
margin-bottom: 20px;
padding: 16px;
background-color: var(--gray-50);
border-radius: 6px;
}
.toggle-controls {
display: flex;
align-items: center;
}
.param-description {
font-size: 12px;
color: var(--gray-600);

View File

@ -113,17 +113,25 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, nextTick, onUnmounted } from 'vue';
import { useDatabaseStore } from '@/stores/database';
import { ReloadOutlined, ExpandOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import KnowledgeGraphViewer from '@/components/KnowledgeGraphViewer.vue';
import { h } from 'vue';
import { getKbTypeLabel } from '@/utils/kb_utils';
const props = defineProps({
active: {
type: Boolean,
default: false,
},
});
const store = useDatabaseStore();
const databaseId = computed(() => store.databaseId);
const kbType = computed(() => store.database.kb_type);
const kbTypeLabel = computed(() => getKbTypeLabel(kbType.value || 'lightrag'));
const graphStats = computed({
get: () => store.graphStats,
set: (stats) => store.graphStats = stats
@ -146,10 +154,9 @@ const isGraphSupported = computed(() => {
return type === 'lightrag';
});
let pendingLoadTimer = null;
const loadGraph = () => {
if (!(Object.keys(store.database?.files).length > 0)) {
return;
}
if (graphViewerRef.value && typeof graphViewerRef.value.loadFullGraph === 'function') {
graphViewerRef.value.loadFullGraph();
}
@ -220,12 +227,45 @@ const applySettings = () => {
// }
// };
watch(isGraphSupported, (supported) => {
if (supported) {
setTimeout(() => {
loadGraph();
}, 800);
const scheduleGraphLoad = (delay = 200) => {
if (!props.active || !isGraphSupported.value) {
return;
}
if (pendingLoadTimer) {
clearTimeout(pendingLoadTimer);
}
pendingLoadTimer = setTimeout(() => {
nextTick(() => {
loadGraph();
});
}, delay);
};
watch(
() => props.active,
(active) => {
if (active) {
scheduleGraphLoad();
}
},
{ immediate: true }
);
watch(databaseId, () => {
scheduleGraphLoad(300);
});
watch(isGraphSupported, (supported) => {
if (supported) {
scheduleGraphLoad(200);
}
});
onUnmounted(() => {
if (pendingLoadTimer) {
clearTimeout(pendingLoadTimer);
pendingLoadTimer = null;
}
});
</script>

View File

@ -444,7 +444,7 @@ onBeforeUnmount(() => {
}
&:disabled {
background-color: var(--gray-400);
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;

View File

@ -3,63 +3,37 @@
<div class="query-section-layout">
<!-- 主内容区域 -->
<div class="query-main">
<div class="query-header">
<div class="header-status">
<a-switch
v-model:checked="showRawData"
size="small"
checked-children="原始数据"
un-checked-children="格式化"
/>
<span v-if="searchLoading" class="search-status">
<a-spin size="small" />
搜索中...
</span>
<span v-else-if="queryResult" class="result-count">
找到 {{ resultCount }} 条结果
</span>
</div>
</div>
<div class="query-input-container">
<a-textarea
v-model:value="queryText"
placeholder="输入查询内容"
:auto-size="{ minRows: 3, maxRows: 6 }"
class="compact-query-textarea"
/>
<div class="query-actions-row">
<a-button
@click="onQuery"
:loading="searchLoading"
class="search-button"
type="primary"
>
<template #icon>
<SearchOutlined />
</template>
搜索
</a-button>
<div class="query-examples-compact">
<span class="examples-label">示例</span>
<div class="examples-container">
<a-button
type="text"
:key="currentExampleIndex"
@click="useQueryExample(queryExamples[currentExampleIndex])"
size="small"
class="example-btn"
>
{{ queryExamples[currentExampleIndex] }}
</a-button>
</div>
<div class="search-input-wrapper">
<a-textarea
v-model:value="queryText"
placeholder="输入查询内容..."
:auto-size="{ minRows: 2, maxRows: 6 }"
class="search-textarea"
@press-enter.prevent="onQuery"
/>
<div class="search-actions">
<a-switch
v-model:checked="showRawData"
checked-children="格式化"
un-checked-children="原始"
/>
<a-button
@click="onQuery"
:loading="searchLoading"
class="search-button"
type="primary"
:disabled="!queryText.trim()"
:icon=h(SearchOutlined)
shape="circle"
/>
</div>
</div>
</div>
<div class="query-results" v-if="queryResult">
<!-- 原始数据显示 -->
<div v-if="showRawData" class="result-raw">
<div v-if="!showRawData" class="result-raw">
<pre>{{ JSON.stringify(queryResult, null, 2) }}</pre>
</div>
@ -128,16 +102,13 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { ref, computed, onMounted, h } from 'vue';
import { useDatabaseStore } from '@/stores/database';
import { message } from 'ant-design-vue';
import { queryApi } from '@/apis/knowledge_api';
import {
SearchOutlined,
SettingOutlined,
CloseOutlined,
} from '@ant-design/icons-vue';
import { h } from 'vue';
const store = useDatabaseStore();
@ -155,30 +126,11 @@ const props = defineProps({
const searchLoading = computed(() => store.state.searchLoading);
const queryResult = ref('');
const showRawData = ref(false);
const resultCount = ref(0);
const showRawData = ref(true);
//
const queryText = ref('');
//
const queryExamples = ref([
'孕妇应该避免吃哪些水果?',
'荔枝应该怎么清洗?',
'如何判断西瓜是否成熟?',
'苹果有哪些营养价值?',
'什么季节最适合吃梨?',
'如何保存草莓以延长保质期?',
'香蕉变黑后还能吃吗?',
'橙子皮可以用来做什么?'
]);
//
const currentExampleIndex = ref(0);
//
let exampleCarouselInterval = null;
const onQuery = async () => {
if (!queryText.value.trim()) {
@ -195,57 +147,19 @@ const onQuery = async () => {
const data = await queryApi.queryTest(store.database.db_id, queryText.value.trim(), queryMeta);
queryResult.value = data;
//
if (data?.data && Array.isArray(data.data)) {
resultCount.value = data.data.length;
} else if (data) {
resultCount.value = 1;
}
} catch (error) {
console.error(error);
message.error(error.message);
queryResult.value = '';
resultCount.value = 0;
} finally {
store.state.searchLoading = false;
}
};
const useQueryExample = (example) => {
queryText.value = example;
onQuery();
};
const startExampleCarousel = () => {
if (exampleCarouselInterval) return;
exampleCarouselInterval = setInterval(() => {
currentExampleIndex.value = (currentExampleIndex.value + 1) % queryExamples.value.length;
}, 6000); // 6
};
const stopExampleCarousel = () => {
if (exampleCarouselInterval) {
clearInterval(exampleCarouselInterval);
exampleCarouselInterval = null;
}
};
//
//
onMounted(() => {
//
startExampleCarousel();
//
store.loadQueryParams();
});
//
onUnmounted(() => {
//
stopExampleCarousel();
});
</script>
@ -267,98 +181,92 @@ onUnmounted(() => {
flex-direction: column;
overflow: hidden;
min-width: 0;
}
.query-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--gray-200);
background-color: #fff;
.header-status {
display: flex;
align-items: center;
gap: 16px;
font-size: 13px;
color: var(--gray-600);
.search-status {
display: flex;
align-items: center;
gap: 6px;
}
.result-count {
font-weight: 500;
}
}
max-height: 100%;
}
.query-input-container {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
background-color: #fff;
border-bottom: 1px solid var(--gray-200);
gap: 12px;
background-color: var(--gray-0);
}
.compact-query-textarea {
flex: 1;
.search-input-wrapper {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px 16px 8px;
border-radius: 12px;
border: 1px solid var(--gray-200);
background-color: var(--gray-0);
box-shadow: 0 1px 3px rgba(23, 23, 23, 0.05);
transition: border-color 0.5s ease, box-shadow 0.5s ease;
&:focus {
&:hover {
border-color: var(--main-400);
box-shadow: 0 4px 12px rgba(1, 97, 121, 0.08);
}
:deep(.ant-input) {
border-radius: 8px;
background-color: var(--gray-0);
color: var(--gray-1000);
outline: none;
border: none;
box-shadow: none;
padding: 0;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
&:focus {
outline: none;
border: none;
}
&::placeholder {
color: var(--gray-500);
}
}
}
.query-actions-row {
.search-actions {
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.search-button {
flex-shrink: 0;
}
background-color: var(--main-color);
border-color: var(--main-color);
box-shadow: 0 2px 4px rgba(1, 97, 121, 0.15);
transition: all 0.2s ease;
.query-examples-compact {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
&:hover:not(:disabled) {
background-color: var(--main-bright);
border-color: var(--main-bright);
box-shadow: 0 4px 8px rgba(1, 136, 166, 0.25);
transform: translateY(-1px);
}
.examples-label {
font-size: 12px;
color: #8c8c8c;
white-space: nowrap;
}
.examples-container {
min-height: 24px;
display: flex;
}
.example-btn {
text-align: left;
white-space: normal;
height: auto;
padding: 4px 8px;
font-size: 12px;
&:disabled {
opacity: 0.5;
color: var(--gray-0);
cursor: not-allowed;
box-shadow: none;
transform: none;
}
}
.query-results {
flex: 1;
overflow-y: auto;
padding: 20px;
background-color: #fafafa;
padding: 16px;
background-color: var(--gray-25);
min-height: 0;
.result-raw {
background-color: #f8f9fa;
background-color: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: 6px;
padding: 16px;
@ -369,6 +277,7 @@ onUnmounted(() => {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
line-height: 1.5;
color: var(--gray-1000);
white-space: pre-wrap;
word-break: break-word;
}
@ -378,32 +287,36 @@ onUnmounted(() => {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.6;
color: var(--gray-1000);
}
.result-list {
.no-results {
text-align: center;
padding: 32px;
color: #999;
color: var(--gray-500);
}
.result-summary {
margin-bottom: 12px;
padding: 8px 12px;
background-color: #e6f7ff;
background-color: var(--main-50);
border-left: 3px solid var(--main-color);
border-radius: 2px;
color: var(--gray-800);
}
.result-item {
background-color: white;
border: 1px solid #e8e8e8;
background-color: var(--gray-0);
border: 1px solid var(--gray-200);
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
transition: all 0.2s ease;
&:hover {
border-color: var(--gray-400);
border-color: var(--main-300);
box-shadow: 0 2px 8px rgba(1, 97, 121, 0.08);
}
&:last-child {
@ -416,7 +329,7 @@ onUnmounted(() => {
gap: 12px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
border-bottom: 1px solid var(--gray-150);
.result-index {
font-weight: 600;
@ -429,20 +342,20 @@ onUnmounted(() => {
font-size: 12px;
padding: 2px 8px;
border-radius: 12px;
background-color: #f0f0f0;
color: #666;
background-color: var(--gray-100);
color: var(--gray-700);
}
.result-rerank-score {
background-color: #fff7e6;
color: #fa8c16;
background-color: var(--stats-warning-bg);
color: var(--stats-warning-color);
}
}
.result-content {
padding: 8px 0;
line-height: 1.6;
color: #333;
color: var(--gray-900);
white-space: pre-wrap;
word-break: break-word;
}
@ -453,14 +366,14 @@ onUnmounted(() => {
gap: 12px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
border-top: 1px solid var(--gray-150);
.metadata-item {
font-size: 12px;
color: #666;
color: var(--gray-700);
strong {
color: #999;
color: var(--gray-500);
font-weight: 500;
margin-right: 4px;
}
@ -471,13 +384,15 @@ onUnmounted(() => {
.result-unknown {
pre {
background-color: white;
background-color: var(--gray-0);
border: 1px solid var(--gray-200);
padding: 12px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
color: var(--gray-1000);
}
}
}
</style>
</style>

View File

@ -56,6 +56,7 @@
<a-tab-pane key="graph" tab="知识图谱" v-if="isGraphSupported">
<KnowledgeGraphSection
:visible="true"
:active="activeTab === 'graph'"
@toggle-visible="() => {}"
/>
</a-tab-pane>
@ -108,19 +109,27 @@ const isGraphSupported = computed(() => {
// Tab -
const activeTab = ref('query');
const previousDatabaseId = ref(null);
// IDTab
watch(databaseId, (newDbId, oldDbId) => {
if (newDbId && newDbId !== oldDbId) {
//
// 使setTimeouttick
setTimeout(() => {
activeTab.value = isGraphSupported.value ? 'graph' : 'query';
}, 0);
previousDatabaseId.value = newDbId;
}
}, { immediate: true });
// LightRAG
watch(
() => [databaseId.value, isGraphSupported.value],
([newDbId, supported], oldValue = []) => {
const [oldDbId, previouslySupported] = oldValue;
if (!newDbId) {
return;
}
if (supported && (newDbId !== oldDbId || previouslySupported === false || previouslySupported === undefined)) {
activeTab.value = 'graph';
return;
}
if (!supported && activeTab.value === 'graph') {
activeTab.value = 'query';
}
},
{ immediate: true }
);
// /
const toggleRightPanel = () => {
@ -261,10 +270,12 @@ const handleMouseUp = () => {
}
.left-panel {
display: flex;
flex-shrink: 0;
flex-grow: 1;
background-color: var(--gray-0);
padding-right: 0;
// max-height: calc(100% - 16px);
}
.right-panel {
@ -315,7 +326,6 @@ const handleMouseUp = () => {
}
}
/* Simplify resize handle */
.resize-handle {
opacity: 0.8;

View File

@ -163,15 +163,6 @@
<!-- 数据库列表 -->
<div v-else class="databases">
<div class="new-database dbcard" @click="state.openNewDatabaseModel=true">
<div class="top">
<div class="icon"><BookPlus /></div>
<div class="info">
<h3>新建知识库</h3>
</div>
</div>
<p>导入您自己的文本数据或通过Webhook实时写入数据以增强 LLM 的上下文</p>
</div>
<div
v-for="database in databases"
:key="database.db_id"
@ -224,7 +215,7 @@ import { ref, onMounted, reactive, watch, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router';
import { useConfigStore } from '@/stores/config';
import { message } from 'ant-design-vue'
import { BookPlus, Database, Zap, FileDigit, Waypoints, Building2 } from 'lucide-vue-next';
import { Database, Zap, FileDigit, Waypoints, Building2 } from 'lucide-vue-next';
import { LockOutlined, InfoCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons-vue';
import { databaseApi, typeApi } from '@/apis/knowledge_api';
import HeaderComponent from '@/components/HeaderComponent.vue';
@ -877,10 +868,6 @@ onMounted(() => {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
.new-database {
background-color: #F0F3F4;
}
}
.database, .graphbase {