修复: 项目详情批量(任务列表联动刷新/工作流降级标注/中文git乱码/子目录错误行内/零工程空态/切tab保活) + 依赖图批量(节点渲染register/环高亮/防环防重/点击选中) + 销账
This commit is contained in:
@@ -46,7 +46,13 @@
|
||||
<div class="dep-graph__modal-field">
|
||||
<label>{{ $t('dependencyGraph.targetLabel') }}</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>
|
||||
<!-- G6:已存在 from→to 组合或选中自身作为目标时禁用该候选 -->
|
||||
<option
|
||||
v-for="m in modules"
|
||||
:key="m.id"
|
||||
:value="m.id"
|
||||
:disabled="depFrom === m.id || (depFrom !== '' && existingDepKeys.has(`${depFrom}->${m.id}`))"
|
||||
>{{ m.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="dep-graph__modal-field">
|
||||
@@ -61,7 +67,7 @@
|
||||
</div>
|
||||
<div class="dep-graph__modal-actions">
|
||||
<button class="btn btn-ghost btn-sm" @click="showAddDep = false">{{ $t('common.cancel') }}</button>
|
||||
<button class="btn btn-primary btn-sm" :disabled="!depFrom || !depTo || depFrom === depTo" @click="onAddDep">{{ $t('dependencyGraph.confirm') }}</button>
|
||||
<button class="btn btn-primary btn-sm" :disabled="!depFrom || !depTo || depFrom === depTo || existingDepKeys.has(`${depFrom}->${depTo}`)" @click="onAddDep">{{ $t('dependencyGraph.confirm') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,11 +80,11 @@
|
||||
*
|
||||
* 节点用 Vue 组件(ModuleNode.vue)渲染,通过 @antv/x6-vue-shape 注册。
|
||||
*/
|
||||
import { onMounted, onBeforeUnmount, ref, watch, markRaw } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { onMounted, onBeforeUnmount, ref, watch, computed, markRaw } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Graph, Selection, Snapline, History, Scroller, MiniMap } from '@antv/x6'
|
||||
import dagre from 'dagre'
|
||||
import { register } from '@antv/x6-vue-shape'
|
||||
import '@antv/x6-vue-shape'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { moduleApi, type ProjectModuleRecord, type ModuleDependencyRecord } from '@/api/module'
|
||||
@@ -86,16 +92,16 @@ import { useConfirm } from '@/composables/useConfirm'
|
||||
import ModuleNode from './ModuleNode.vue'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
||||
|
||||
// vue-shape 注册:把节点 Vue 组件绑定到 'vue-shape' 形状。
|
||||
// 库内 shapeMaps 仅经 register 填充(view.js:48 renderVueComponent 读 shapeMaps[node.shape]),
|
||||
// 单纯 import '@antv/x6-vue-shape' 只注册 shape 不绑组件 → 节点渲染空/报错(P0)。
|
||||
// 幂等:Graph.registerNode 对同名 shape 重复注册会覆盖,重复进组件也安全。
|
||||
register({ shape: 'vue-shape', component: ModuleNode })
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: string
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-module', moduleId: string): void
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const modules = ref<ProjectModuleRecord[]>([])
|
||||
const dependencies = ref<ModuleDependencyRecord[]>([])
|
||||
@@ -115,8 +121,23 @@ const depFrom = ref('')
|
||||
const depTo = ref('')
|
||||
const depType = ref('library')
|
||||
|
||||
// 已存在的依赖组合(from→to),用于弹窗候选禁用重复添加(G6)。
|
||||
const existingDepKeys = computed(
|
||||
() => new Set(dependencies.value.map(d => `${d.from_module_id}->${d.to_module_id}`)),
|
||||
)
|
||||
|
||||
// 当前选中高亮的节点 id(点击节点组件内高亮,不导航;G4)。
|
||||
const selectedId = ref<string | null>(null)
|
||||
|
||||
async function onAddDep() {
|
||||
if (!depFrom.value || !depTo.value || depFrom.value === depTo.value) return
|
||||
// 重复依赖拦截(G6):已存在组合,弹窗候选已禁用,此处双保险防直达。
|
||||
if (existingDepKeys.value.has(`${depFrom.value}->${depTo.value}`)) return
|
||||
// 防环(G7):本地 DFS 校验,新增 source→target 会成环则阻止。
|
||||
if (wouldCreateCycle(depFrom.value, depTo.value)) {
|
||||
Message.warning(t('dependencyGraph.cycleAddError'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
await moduleApi.addModuleDependency(props.projectId, depFrom.value, depTo.value, depType.value)
|
||||
showAddDep.value = false
|
||||
@@ -127,11 +148,40 @@ async function onAddDep() {
|
||||
renderGraph()
|
||||
} catch (e) {
|
||||
console.error('[DependencyGraph] 添加依赖失败:', e)
|
||||
// G8:对齐删除依赖的失败反馈,补用户可见错误提示(common.unknownError 通用兜底)。
|
||||
Message.error(t('common.unknownError'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地防环:在现有依赖图上,若 target 已能到达 source(DFS),则新增 source→target 会成环。
|
||||
* 复用 detectModuleCycles 的 DFS 思路的最小前端校验(纯本地,不请求后端)。
|
||||
*/
|
||||
function wouldCreateCycle(sourceId: string, targetId: string): boolean {
|
||||
const adj = new Map<string, string[]>()
|
||||
for (const d of dependencies.value) {
|
||||
const list = adj.get(d.from_module_id) ?? []
|
||||
list.push(d.to_module_id)
|
||||
adj.set(d.from_module_id, list)
|
||||
}
|
||||
const visited = new Set<string>()
|
||||
const stack = [targetId]
|
||||
while (stack.length > 0) {
|
||||
const cur = stack.pop()!
|
||||
if (cur === sourceId) return true
|
||||
if (visited.has(cur)) continue
|
||||
visited.add(cur)
|
||||
for (const next of adj.get(cur) ?? []) {
|
||||
if (!visited.has(next)) stack.push(next)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function loadModules() {
|
||||
loading.value = true
|
||||
// 数据变更后旧环高亮失效,复位待重新检测(否则残留上一批环节点红框,G5)。
|
||||
cycleNodes.value = new Set()
|
||||
try {
|
||||
const [mods, deps] = await Promise.all([
|
||||
moduleApi.listProjectModules(props.projectId),
|
||||
@@ -147,8 +197,10 @@ async function loadModules() {
|
||||
}
|
||||
}
|
||||
|
||||
function renderGraph() {
|
||||
function renderGraph(opts?: { center?: boolean }) {
|
||||
if (!graph) return
|
||||
// 点选刷新(节点点击)时跳过 centerContent,避免视图跳动;默认(加载/删除/环检测后)居中。
|
||||
const shouldCenter = opts?.center !== false
|
||||
|
||||
const nodeWidth = 200
|
||||
const nodeHeight = 80
|
||||
@@ -168,7 +220,6 @@ function renderGraph() {
|
||||
// 映射回 X6 节点格式
|
||||
const nodes = modules.value.map(m => {
|
||||
const pos = g.node(m.id)
|
||||
const isCycle = cycleNodes.value.has(m.id)
|
||||
return {
|
||||
id: m.id,
|
||||
shape: 'vue-shape',
|
||||
@@ -182,12 +233,12 @@ function renderGraph() {
|
||||
path: m.path,
|
||||
stack: m.stack,
|
||||
gitUrl: m.git_url,
|
||||
// 环节点高亮(G2):vue-shape markup 无 body 选择器,attrs.body 渲染无效,
|
||||
// 改由 isCycle 注入节点 data,ModuleNode 根元素按类描红框。
|
||||
isCycle: cycleNodes.value.has(m.id),
|
||||
// 选中高亮(G4):当前点选节点组件内高亮。
|
||||
selected: selectedId.value === m.id,
|
||||
},
|
||||
// 环节点高亮(红框);renderGraph 是画布唯一渲染入口,高亮必须在这里注入,
|
||||
// 否则会被 graph.fromJSON 整体替换抹除。
|
||||
attrs: isCycle ? {
|
||||
body: { stroke: '#e05050', strokeWidth: 3 },
|
||||
} : undefined,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -207,7 +258,7 @@ function renderGraph() {
|
||||
}))
|
||||
|
||||
graph.fromJSON({ nodes, edges })
|
||||
graph.centerContent()
|
||||
if (shouldCenter) graph.centerContent()
|
||||
}
|
||||
|
||||
function buildGraph() {
|
||||
@@ -228,8 +279,10 @@ function buildGraph() {
|
||||
graph.use(new MiniMap({ width: 200, height: 120, padding: 10 }))
|
||||
|
||||
graph.on('node:click', ({ node }) => {
|
||||
emit('select-module', String(node.id))
|
||||
router.push(`/projects/${props.projectId}`)
|
||||
// 组件内选中高亮(G4):点击节点仅高亮,不 emit 不导航(依赖图是查看/编辑关系,
|
||||
// 跳工程详情与 Tab 语义冲突,原 emit+router.push 是无意义死代码)。
|
||||
selectedId.value = String(node.id)
|
||||
renderGraph({ center: false })
|
||||
})
|
||||
|
||||
renderGraph()
|
||||
|
||||
Reference in New Issue
Block a user