feat(graph): add back-to-panorama button, legend component, and wire detail panel props (GAP-F3, GAP-F4)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
openclaw 2026-03-24 09:03:48 +00:00
parent ca9f199d53
commit f7f54a56cc
2 changed files with 128 additions and 2 deletions

View File

@ -0,0 +1,91 @@
<template>
<div class="graph-legend" :class="{ collapsed: !expanded }">
<div class="legend-header" @click="expanded = !expanded">
<span>图例</span>
<span class="toggle">{{ expanded ? '▼' : '▶' }}</span>
</div>
<div v-if="expanded" class="legend-body">
<div class="legend-section">
<div class="legend-title">形状</div>
<div class="legend-item">
<svg width="20" height="20"><circle cx="10" cy="10" r="8" fill="#9E9E9E"/></svg>
<span>Capability</span>
</div>
<div class="legend-item">
<svg width="20" height="20"><rect x="2" y="2" width="16" height="16" rx="2" fill="#9E9E9E"/></svg>
<span>Module</span>
</div>
<div class="legend-item">
<svg width="20" height="20"><polygon points="10,2 18,10 10,18 2,10" fill="#9E9E9E"/></svg>
<span>Entity</span>
</div>
<div class="legend-item">
<svg width="20" height="20"><circle cx="10" cy="10" r="6" fill="#9E9E9E"/></svg>
<span>Runtime</span>
</div>
<div class="legend-item">
<svg width="20" height="20"><rect x="1" y="4" width="18" height="12" rx="2" fill="none" stroke="#9E9E9E" stroke-width="2"/></svg>
<span>Document</span>
</div>
</div>
<div class="legend-section">
<div class="legend-title">状态</div>
<div class="legend-item" v-for="s in statuses" :key="s.label">
<svg width="20" height="20"><circle cx="10" cy="10" r="6" :fill="s.color"/></svg>
<span>{{ s.label }}</span>
</div>
</div>
<div class="legend-section">
<div class="legend-title">边线</div>
<div class="legend-item" v-for="e in edgeTypes" :key="e.label">
<svg width="40" height="12">
<line x1="0" y1="6" x2="40" y2="6" :stroke="e.color" stroke-width="2" :stroke-dasharray="e.dash" />
</svg>
<span>{{ e.label }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const expanded = ref(true)
const statuses = [
{ label: 'OK', color: '#4CAF50' },
{ label: 'Sparse', color: '#FFC107' },
{ label: 'Missing', color: '#F44336' },
{ label: 'Template Residue', color: '#FF9800' },
{ label: 'Placeholder Heavy', color: '#9C27B0' },
{ label: 'Unknown', color: '#9E9E9E' },
]
const edgeTypes = [
{ label: 'traces_to', color: '#666', dash: '0' },
{ label: 'depends_on', color: '#999', dash: '6,3' },
{ label: 'documents', color: '#42A5F5', dash: '0' },
{ label: 'integrates_with', color: '#AB47BC', dash: '4,2' },
]
</script>
<style scoped>
.graph-legend {
position: absolute; bottom: 16px; right: 16px;
background: rgba(255,255,255,0.92); border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
padding: 8px 12px; z-index: 10; min-width: 180px;
font-size: 12px; pointer-events: auto;
}
.legend-header {
display: flex; justify-content: space-between; align-items: center;
cursor: pointer; font-weight: 600; font-size: 13px; padding-bottom: 4px;
}
.toggle { font-size: 10px; color: #999; }
.legend-body { display: flex; flex-direction: column; gap: 8px; padding-top: 4px; }
.legend-section { display: flex; flex-direction: column; gap: 2px; }
.legend-title { font-weight: 600; color: #666; margin-bottom: 2px; }
.legend-item { display: flex; align-items: center; gap: 6px; }
.legend-item span { color: #333; }
</style>

View File

@ -9,7 +9,7 @@
<div class="summary-item" style="color: var(--color-missing)">Missing <strong>{{ scanResult.summary.missing }}</strong></div>
</div>
<div class="toolbar">
<div v-if="!isDrillDown" class="toolbar">
<button class="toggle-btn" :class="{ active: showDocumentView }" @click="toggleDocumentView">
{{ showDocumentView ? '默认视图' : '文档视图' }}
</button>
@ -17,7 +17,20 @@
<svg ref="svgRef" class="graph-svg"></svg>
<GraphDetail :node="selectedNode" @close="clearSelection" />
<GraphLegend />
<div v-if="isDrillDown" class="drill-down-bar">
<button @click="returnToPanorama" class="back-btn">&larr; 返回全景图</button>
<span class="drill-down-label">当前: {{ drillDownNodeLabel }}</span>
</div>
<GraphDetail
:node="selectedNode"
:graphView="graphView"
:projectId="(route.params.id as string)"
@close="clearSelection"
@selectNode="store.selectNode"
/>
</div>
</template>
@ -28,6 +41,7 @@ import { storeToRefs } from 'pinia'
import * as d3 from 'd3'
import { useGraphStore } from '../composables/useGraph'
import GraphDetail from './GraphDetail.vue'
import GraphLegend from './GraphLegend.vue'
import type { GraphNode, GraphEdge } from '@/shared/types/api'
const route = useRoute()
@ -79,6 +93,13 @@ function toggleDocumentView() {
drawGraph()
}
async function returnToPanorama() {
const projectId = route.params.id as string
isDrillDown.value = false
drillDownNodeLabel.value = ''
await store.loadGraph(projectId)
}
function drawGraph() {
if (!svgRef.value || !graphView.value) return
@ -247,6 +268,8 @@ function drawGraph() {
// Double-click -> drill down
node.on('dblclick', (_event: any, d: any) => {
const projectId = route.params.id as string
isDrillDown.value = true
drillDownNodeLabel.value = d.label
store.loadNeighbors(projectId, d.id)
})
@ -321,4 +344,16 @@ watch(graphView, () => { drawGraph() })
color: white;
border-color: #42A5F5;
}
.drill-down-bar {
position: absolute; top: 12px; left: 12px;
display: flex; align-items: center; gap: 12px;
background: white; padding: 8px 16px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1); z-index: 10;
}
.back-btn {
background: #1976D2; color: white; border: none;
padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px;
}
.back-btn:hover { background: #1565C0; }
.drill-down-label { font-size: 13px; color: #666; }
</style>