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

View File

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

File diff suppressed because it is too large Load Diff