feat: 修复 7 个 Gap — 图分组布局、status 映射、详情面板丰富等 #2
91
frontend/src/modules/graph/components/GraphLegend.vue
Normal file
91
frontend/src/modules/graph/components/GraphLegend.vue
Normal 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>
|
||||
|
|
@ -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">← 返回全景图</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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user