新增: 依赖图增强(添加依赖弹窗 + dagre 自动布局)
- 添加依赖入口:工具栏"+ 依赖"按钮,弹窗选择源/目标工程 + 依赖类型 - dagre 层次布局替换网格布局(LR 方向,自动排布工程节点) - 依赖边按类型着色(library/api/mq/shared/custom) - 小地图导航 + 缩放/适应内容
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
<div class="dep-graph__toolbar">
|
||||
<span class="dep-graph__title">{{ $t('dependencyGraph.title') }}</span>
|
||||
<div class="dep-graph__actions">
|
||||
<button v-if="modules.length >= 2" class="btn btn-ghost btn-sm" @click="showAddDep = true">+ 依赖</button>
|
||||
<button class="btn btn-ghost btn-sm" @click="fitContent">适应内容</button>
|
||||
<button class="btn btn-ghost btn-sm" @click="zoomIn">+</button>
|
||||
<button class="btn btn-ghost btn-sm" @click="zoomOut">-</button>
|
||||
@@ -13,6 +14,39 @@
|
||||
<div class="empty-icon">📦</div>
|
||||
<div>{{ $t('dependencyGraph.empty') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加依赖弹窗 -->
|
||||
<div v-if="showAddDep" class="dep-graph__modal-overlay" @click.self="showAddDep = false">
|
||||
<div class="dep-graph__modal">
|
||||
<h3>添加工程依赖</h3>
|
||||
<div class="dep-graph__modal-field">
|
||||
<label>源工程(依赖方)</label>
|
||||
<select v-model="depFrom" class="dep-graph__modal-input">
|
||||
<option v-for="m in modules" :key="m.id" :value="m.id">{{ m.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="dep-graph__modal-field">
|
||||
<label>目标工程(被依赖)</label>
|
||||
<select v-model="depTo" class="dep-graph__modal-input">
|
||||
<option v-for="m in modules" :key="m.id" :value="m.id">{{ m.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="dep-graph__modal-field">
|
||||
<label>依赖类型</label>
|
||||
<select v-model="depType" class="dep-graph__modal-input">
|
||||
<option value="library">类库</option>
|
||||
<option value="api">API 调用</option>
|
||||
<option value="mq">消息队列</option>
|
||||
<option value="shared">共享资源</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="dep-graph__modal-actions">
|
||||
<button class="btn btn-ghost btn-sm" @click="showAddDep = false">取消</button>
|
||||
<button class="btn btn-primary btn-sm" :disabled="!depFrom || !depTo || depFrom === depTo" @click="onAddDep">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,6 +59,7 @@
|
||||
import { onMounted, onBeforeUnmount, ref, watch, markRaw } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Graph, Selection, Snapline, History, Scroller, MiniMap } from '@antv/x6'
|
||||
import dagre from 'dagre'
|
||||
import '@antv/x6-vue-shape'
|
||||
import { moduleApi, type ProjectModuleRecord, type ModuleDependencyRecord } from '@/api/module'
|
||||
import ModuleNode from './ModuleNode.vue'
|
||||
@@ -45,6 +80,27 @@ const dependencies = ref<ModuleDependencyRecord[]>([])
|
||||
const loading = ref(true)
|
||||
let graph: Graph | null = null
|
||||
|
||||
// 添加依赖弹窗
|
||||
const showAddDep = ref(false)
|
||||
const depFrom = ref('')
|
||||
const depTo = ref('')
|
||||
const depType = ref('library')
|
||||
|
||||
async function onAddDep() {
|
||||
if (!depFrom.value || !depTo.value || depFrom.value === depTo.value) return
|
||||
try {
|
||||
await moduleApi.addModuleDependency(props.projectId, depFrom.value, depTo.value, depType.value)
|
||||
showAddDep.value = false
|
||||
depFrom.value = ''
|
||||
depTo.value = ''
|
||||
depType.value = 'library'
|
||||
await loadModules()
|
||||
renderGraph()
|
||||
} catch (e) {
|
||||
console.error('[DependencyGraph] 添加依赖失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadModules() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -65,20 +121,29 @@ async function loadModules() {
|
||||
function renderGraph() {
|
||||
if (!graph) return
|
||||
|
||||
const cols = 3
|
||||
const nodeWidth = 200
|
||||
const nodeHeight = 80
|
||||
const gapX = 50
|
||||
const gapY = 40
|
||||
|
||||
const nodes = modules.value.map((m, i) => {
|
||||
const col = i % cols
|
||||
const row = Math.floor(i / cols)
|
||||
// dagre 层次布局
|
||||
const g = new dagre.graphlib.Graph()
|
||||
g.setGraph({ rankdir: 'LR', nodesep: 30, ranksep: 60 })
|
||||
g.setDefaultEdgeLabel(() => ({}))
|
||||
for (const m of modules.value) {
|
||||
g.setNode(m.id, { width: nodeWidth, height: nodeHeight })
|
||||
}
|
||||
for (const d of dependencies.value) {
|
||||
g.setEdge(d.from_module_id, d.to_module_id)
|
||||
}
|
||||
dagre.layout(g)
|
||||
|
||||
// 映射回 X6 节点格式
|
||||
const nodes = modules.value.map(m => {
|
||||
const pos = g.node(m.id)
|
||||
return {
|
||||
id: m.id,
|
||||
shape: 'vue-shape',
|
||||
x: col * (nodeWidth + gapX) + 40,
|
||||
y: row * (nodeHeight + gapY) + 40,
|
||||
x: (pos?.x ?? 0) - nodeWidth / 2,
|
||||
y: (pos?.y ?? 0) - nodeHeight / 2,
|
||||
width: nodeWidth,
|
||||
height: nodeHeight,
|
||||
component: markRaw(ModuleNode),
|
||||
@@ -257,4 +322,41 @@ onBeforeUnmount(() => {
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 添加依赖弹窗 */
|
||||
.dep-graph__modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
.dep-graph__modal {
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
padding: 20px;
|
||||
min-width: 360px;
|
||||
}
|
||||
.dep-graph__modal h3 { font-size: 16px; color: var(--df-text); margin-bottom: 16px; }
|
||||
.dep-graph__modal-field { margin-bottom: 12px; }
|
||||
.dep-graph__modal-field label {
|
||||
display: block; font-size: 12px; color: var(--df-text-secondary); margin-bottom: 4px;
|
||||
}
|
||||
.dep-graph__modal-input {
|
||||
width: 100%; box-sizing: border-box;
|
||||
padding: 6px 10px;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
background: var(--df-bg); color: var(--df-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
.dep-graph__modal-actions {
|
||||
display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px;
|
||||
}
|
||||
.btn-primary { background: var(--df-accent); color: #fff; }
|
||||
.btn-primary:hover { filter: brightness(1.1); }
|
||||
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user