feat: 更新组件样式和逻辑,优化用户体验
This commit is contained in:
parent
3a6e76e06e
commit
f0275aaadc
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -444,7 +444,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--gray-400);
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
// 监听数据库ID变化,只在切换数据库时设置默认Tab
|
||||
watch(databaseId, (newDbId, oldDbId) => {
|
||||
if (newDbId && newDbId !== oldDbId) {
|
||||
// 切换到支持图谱的数据库时,默认显示图谱,否则显示检索
|
||||
// 使用setTimeout确保在下一个tick中设置,避免和其他响应式更新冲突
|
||||
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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user