feat: 更新 HeaderComponent 和 KnowledgeGraphViewer 组件,优化样式和功能,添加文件详情弹窗,增强用户交互体验

This commit is contained in:
Wenjie Zhang 2025-07-23 03:52:22 +08:00
parent 50931b0456
commit 21a9a3236f
3 changed files with 1238 additions and 743 deletions

View File

@ -5,7 +5,10 @@
<slot name="left"></slot>
</div>
<div class="header-title">
<h1>{{ title }}</h1>
<div class="header-title-block">
<h1>{{ title }}</h1>
<slot name="behind-title"></slot>
</div>
<slot name="description">
<p v-if="description">{{ description }}</p>
</slot>
@ -38,9 +41,9 @@ const props = defineProps({
<style scoped lang="less">
.header-container {
background-color: rgba(255, 255, 255, 0.8);
background-color: var(--bg-sider);
backdrop-filter: blur(10px);
padding: 16px 24px;
padding: 10px 24px;
border-bottom: 1px solid #f0f0f0;
position: sticky;
top: 0;
@ -60,6 +63,12 @@ const props = defineProps({
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
.header-title-block {
display: flex;
align-items: baseline;
gap: 10px;
}
h1 {
margin: 0;
font-size: 20px;

View File

@ -7,7 +7,8 @@
v-if="!hideDbSelector"
v-model:value="selectedDatabase"
placeholder="选择数据库"
style="width: 200px; margin-right: 8px"
size="small"
style="width: 160px; margin-right: 6px"
:loading="loadingDatabases"
@change="onDatabaseChange"
>
@ -23,7 +24,8 @@
<a-select
v-model:value="selectedLabel"
placeholder="选择标签/实体类型"
style="width: 180px"
size="small"
style="width: 140px"
:loading="loadingLabels"
:disabled="!selectedDatabase"
allow-clear
@ -46,50 +48,35 @@
:min="10"
:max="1000"
:step="10"
addon-before="最大节点数"
style="width: 150px"
size="small"
addon-before="limit"
style="width: 140px"
/>
<a-input-number
v-model:value="searchParams.max_depth"
:min="1"
:max="5"
:step="1"
addon-before="最大深度"
style="width: 130px; margin-left: 8px"
size="small"
addon-before="depth"
style="width: 120px; margin-left: 6px"
/>
<a-button
type="primary"
size="small"
:loading="loading"
@click="loadGraphData"
:disabled="!selectedDatabase"
style="margin-left: 8px"
style="margin-left: 6px"
>
<SearchOutlined v-if="!loading" /> 加载图谱
</a-button>
</div>
<div class="filter-section">
<a-button
@click="loadFullGraph"
:loading="loading"
:disabled="!selectedDatabase"
style="margin-left: 8px"
>
<ReloadOutlined /> 刷新全图
</a-button>
<a-button
@click="clearGraph"
:disabled="!selectedDatabase"
style="margin-left: 8px"
>
<ClearOutlined /> 清空图谱
</a-button>
</div>
<div class="stats-section">
<a-tag color="blue">节点: {{ stats.displayed_nodes || 0 }}</a-tag>
<a-tag color="green">: {{ stats.displayed_edges || 0 }}</a-tag>
<a-tag v-if="stats.is_truncated" color="red">已截断</a-tag>
<div v-if="!props.hideStats" class="stats-section">
<a-tag color="blue" size="small">节点: {{ stats.displayed_nodes || 0 }}</a-tag>
<a-tag color="green" size="small">: {{ stats.displayed_edges || 0 }}</a-tag>
<a-tag v-if="stats.is_truncated" color="red" size="small">已截断</a-tag>
</div>
</div>
@ -261,9 +248,16 @@ const props = defineProps({
hideDbSelector: {
type: Boolean,
default: false
},
hideStats: {
type: Boolean,
default: false
}
})
// emits
const emit = defineEmits(['update:stats', 'refresh-graph', 'clear-graph'])
//
const graphStore = useGraphStore()
@ -277,8 +271,8 @@ const layoutRunning = ref(false)
const loadingMessage = ref('加载图数据中...')
//
const nodePanelPosition = ref({ x: 16, y: 80 })
const edgePanelPosition = ref({ x: 16, y: 370 })
const nodePanelPosition = ref({ x: 12, y: 60 })
const edgePanelPosition = ref({ x: 12, y: 320 })
const dragging = ref({ active: false, type: '', startX: 0, startY: 0, initialX: 0, initialY: 0 })
//
@ -688,6 +682,7 @@ const loadGraphData = async () => {
const loadFullGraph = async () => {
selectedLabel.value = '*'
await loadGraphData()
emit('refresh-graph')
}
//
@ -698,6 +693,7 @@ const clearGraph = () => {
sigmaInstance.refresh()
}
graphStore.reset()
emit('clear-graph')
}
//
@ -970,7 +966,7 @@ const onDragPanel = (event) => {
const deltaX = currentRelativeX - dragging.value.startX
const deltaY = currentRelativeY - dragging.value.startY
const maxX = containerRect.width - 320 // 300px +
const maxX = containerRect.width - 260 // 240px +
const maxY = containerRect.height - 200 // 200px
const newPosition = {
@ -1092,7 +1088,18 @@ watch(() => graphStore.selectedNode, (nodeId) => {
//
graphStore.moveToSelectedNode = false
}
}
}
})
//
watch(stats, (newStats) => {
emit('update:stats', newStats)
}, { deep: true, immediate: true })
//
defineExpose({
loadFullGraph,
clearGraph
})
</script>
@ -1100,7 +1107,8 @@ watch(() => graphStore.selectedNode, (nodeId) => {
.knowledge-graph-viewer {
position: relative;
width: 100%;
height: 100vh;
// height: 100vh;
height: 100%;
display: flex;
flex-direction: column;
background: #f5f5f5;
@ -1108,10 +1116,10 @@ watch(() => graphStore.selectedNode, (nodeId) => {
.control-panel {
background: white;
padding: 16px 0;
padding: 8px 0; /* Reduced from 16px */
border-bottom: none;
display: flex;
gap: 16px;
gap: 12px; /* Reduced from 16px */
align-items: center;
flex-wrap: wrap;
z-index: 1000;
@ -1123,7 +1131,7 @@ watch(() => graphStore.selectedNode, (nodeId) => {
.stats-section {
display: flex;
align-items: center;
gap: 8px;
gap: 6px; /* Reduced from 8px */
}
.stats-section {
@ -1145,17 +1153,17 @@ watch(() => graphStore.selectedNode, (nodeId) => {
.detail-panel {
position: absolute;
width: 300px;
width: 240px; /* Reduced from 300px */
background: white;
border: 1px solid #e8e8e8;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
border-radius: 6px; /* Reduced from 8px */
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.08); /* Reduced shadow */
transition: box-shadow 0.2s ease;
cursor: move;
z-index: 1000;
&:hover {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); /* Reduced shadow */
}
&.node-panel {
@ -1170,15 +1178,15 @@ watch(() => graphStore.selectedNode, (nodeId) => {
.detail-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
gap: 6px; /* Reduced from 8px */
padding: 8px 12px; /* Reduced from 12px 16px */
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
border-radius: 8px 8px 0 0;
border-radius: 6px 6px 0 0; /* Reduced from 8px */
h4 {
margin: 0;
font-size: 14px;
font-size: 12px; /* Reduced from 14px */
font-weight: 600;
flex: 1;
color: #262626;
@ -1186,8 +1194,8 @@ watch(() => graphStore.selectedNode, (nodeId) => {
}
.panel-type-indicator {
width: 8px;
height: 8px;
width: 6px; /* Reduced from 8px */
height: 6px;
border-radius: 50%;
flex-shrink: 0;
@ -1201,14 +1209,14 @@ watch(() => graphStore.selectedNode, (nodeId) => {
}
.detail-content {
padding: 16px;
max-height: 400px;
padding: 10px; /* Reduced from 16px */
max-height: 300px; /* Reduced from 400px */
overflow-y: auto;
}
.detail-item {
display: flex;
margin-bottom: 12px;
margin-bottom: 8px; /* Reduced from 12px */
&:last-child {
margin-bottom: 0;
@ -1216,30 +1224,30 @@ watch(() => graphStore.selectedNode, (nodeId) => {
}
.detail-label {
min-width: 60px;
min-width: 50px; /* Reduced from 60px */
font-weight: 600;
color: #595959;
font-size: 12px;
font-size: 11px; /* Reduced from 12px */
flex-shrink: 0;
}
.detail-value {
color: #262626;
font-size: 12px;
font-size: 11px; /* Reduced from 12px */
word-break: break-word;
line-height: 1.4;
line-height: 1.3; /* Reduced from 1.4 */
}
.detail-actions {
margin-top: 16px;
padding-top: 12px;
margin-top: 12px; /* Reduced from 16px */
padding-top: 8px; /* Reduced from 12px */
border-top: 1px solid #f0f0f0;
}
.graph-controls {
position: absolute;
bottom: 20px;
right: 20px;
bottom: 16px; /* Reduced from 20px */
right: 16px;
z-index: 1000;
}
@ -1247,17 +1255,17 @@ watch(() => graphStore.selectedNode, (nodeId) => {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
border: 2px solid white;
border-radius: 20px;
padding: 4px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
border-radius: 16px; /* Reduced from 20px */
padding: 2px; /* Reduced from 4px */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); /* Reduced shadow */
display: flex;
gap: 2px;
gap: 1px; /* Reduced from 2px */
}
.control-btn {
width: 32px;
height: 32px;
border-radius: 16px;
width: 28px; /* Reduced from 32px */
height: 28px;
border-radius: 14px; /* Reduced from 16px */
display: flex;
align-items: center;
justify-content: center;
@ -1274,41 +1282,41 @@ watch(() => graphStore.selectedNode, (nodeId) => {
.legend {
position: absolute;
right: 16px;
top: 80px;
right: 12px; /* Reduced from 16px */
top: 60px; /* Reduced from 80px */
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
border: 2px solid white;
border-radius: 8px;
width: 180px;
max-height: 300px;
border-radius: 6px; /* Reduced from 8px */
width: 150px; /* Reduced from 180px */
max-height: 250px; /* Reduced from 300px */
z-index: 1000;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.08); /* Reduced shadow */
overflow: hidden;
}
.legend-header {
background: #fafafa;
padding: 8px 12px;
padding: 6px 10px; /* Reduced from 8px 12px */
border-bottom: 1px solid #f0f0f0;
h4 {
margin: 0;
font-size: 13px;
font-size: 12px; /* Reduced from 13px */
font-weight: 600;
color: #262626;
}
}
.legend-content {
padding: 8px 12px;
max-height: 240px;
padding: 6px 10px; /* Reduced from 8px 12px */
max-height: 200px; /* Reduced from 240px */
overflow-y: auto;
overflow-x: hidden;
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
width: 4px;
width: 3px; /* Reduced from 4px */
}
&::-webkit-scrollbar-track {
@ -1328,11 +1336,11 @@ watch(() => graphStore.selectedNode, (nodeId) => {
.legend-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px;
margin: 2px 0;
border-radius: 4px;
font-size: 12px;
gap: 6px; /* Reduced from 8px */
padding: 2px 4px; /* Reduced from 4px 6px */
margin: 1px 0; /* Reduced from 2px */
border-radius: 3px; /* Reduced from 4px */
font-size: 11px; /* Reduced from 12px */
min-width: 0;
transition: background-color 0.2s ease;
@ -1351,8 +1359,8 @@ watch(() => graphStore.selectedNode, (nodeId) => {
}
.legend-color {
width: 10px;
height: 10px;
width: 8px; /* Reduced from 10px */
height: 8px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.1);
flex-shrink: 0;
@ -1372,8 +1380,9 @@ watch(() => graphStore.selectedNode, (nodeId) => {
text-align: center;
p {
margin-top: 16px;
margin-top: 12px; /* Reduced from 16px */
color: #666;
font-size: 12px; /* Added smaller font size */
}
}
</style>

File diff suppressed because it is too large Load Diff