优化: 项目文件树/依赖图组件 + 灵感对抗校验 + 小程序脚本 + 模块管理

This commit is contained in:
lxy
2026-08-10 00:00:31 +08:00
parent 6bafdcd5a5
commit cf00345c35
19 changed files with 521 additions and 185 deletions
+75 -16
View File
@@ -80,9 +80,9 @@
*
* 节点用 Vue 组件(ModuleNode.vue)渲染,通过 @antv/x6-vue-shape 注册。
*/
import { onMounted, onBeforeUnmount, ref, watch, computed, markRaw } from 'vue'
import { onMounted, onBeforeUnmount, onActivated, ref, watch, computed, markRaw } from 'vue'
import { useI18n } from 'vue-i18n'
import { Graph, Selection, Snapline, History, Scroller, MiniMap } from '@antv/x6'
import { Graph, Selection, Snapline, History, Scroller, MiniMap, Export } from '@antv/x6'
import dagre from 'dagre'
import { register } from '@antv/x6-vue-shape'
import '@antv/x6-vue-shape'
@@ -261,13 +261,38 @@ function renderGraph(opts?: { center?: boolean }) {
if (shouldCenter) graph.centerContent()
}
/**
* F4:局部更新节点选中态(替代整图 fromJSON 重建,保留平移/缩放态)。
* 只刷新被点节点 + 上次选中节点两个 cell:更新其 data.selected,并手动触发该 node view 的
* 'vue' action 重挂载 Vue 组件。原因:vue-shape 下 node.setData 不触发组件重渲染
* (NodeView/VueShapeView 的 actions 映射无 data→render,'component' 变化才触发 'vue' action)。
*/
function updateNodeSelection(nextId: string) {
if (!graph) return
const prevId = selectedId.value
selectedId.value = nextId
const ids = new Set<string>()
if (nextId) ids.add(nextId)
if (prevId) ids.add(prevId)
for (const id of ids) {
const cell = graph.getCellById(id)
if (!cell) continue
const d = cell.getData() ?? {}
cell.setData({ ...d, selected: id === nextId })
// 'vue' 不在库导出 FlagManagerAction 联合类型内(vue-shape 扩展),故以 any 访问。
const view = graph.findViewByCell(cell) as any
if (view?.confirmUpdate) view.confirmUpdate(view.getFlag('vue'))
}
}
function buildGraph() {
if (!containerRef.value) return
graph = new Graph({
container: containerRef.value,
background: { color: '#1a1a2e' },
grid: { visible: true, size: 10, type: 'dot', args: { color: '#2a2a4e' } },
// U1:画布/grid 用主题 token 实际色值(X6 background/grid 需具体色值,非 CSS 变量),深色硬编码移除。
background: { color: themeColor('--df-bg', '#0c0e1a') },
grid: { visible: true, size: 10, type: 'dot', args: { color: themeColor('--df-border-strong', '#2a2a4e') } },
mousewheel: { enabled: true, modifiers: ['ctrl'], minScale: 0.3, maxScale: 3 },
interacting: { nodeMovable: true },
})
@@ -277,18 +302,31 @@ function buildGraph() {
graph.use(new History({ enabled: true }))
graph.use(new Scroller({ enabled: true, pannable: true }))
graph.use(new MiniMap({ width: 200, height: 120, padding: 10 }))
// F3:注册 Export 插件。X6 的 toPNG 依赖 graph.use(new Export()) 挂载 'export' 插件,
// 缺注册时 Graph.prototype.toPNG 静默 no-op(回调永不触发、无报错),导出按钮失效。
graph.use(new Export())
graph.on('node:click', ({ node }) => {
// 组件内选中高亮(G4):点击节点仅高亮,不 emit 不导航(依赖图是查看/编辑关系,
// 跳工程详情与 Tab 语义冲突,原 emit+router.push 是无意义死代码)。
selectedId.value = String(node.id)
renderGraph({ center: false })
// F4:局部刷新选中态(仅被点节点+上次选中节点),不再整图 fromJSON 重建,保留平移/缩放态。
updateNodeSelection(String(node.id))
})
renderGraph()
bindEdgeDeleteButtons()
}
/**
* U1:读取 CSS 主题 token 的实际色值(X6 画布 background/grid 需要具体颜色字符串,不支持 CSS 变量)。
* 取不到时回退传入的默认色值。buildGraph 在挂载后执行,此时 data-theme 已生效,读到当前主题色。
*/
function themeColor(varName: string, fallback: string): string {
if (typeof window === 'undefined') return fallback
const v = getComputedStyle(document.documentElement).getPropertyValue(varName).trim()
return v || fallback
}
function fitContent() {
graph?.zoomToFit({ padding: 20, maxScale: 1.5 })
}
@@ -320,19 +358,25 @@ async function checkCycles() {
}
}
/** 导出 PNG。 */
async function exportPNG() {
/**
* 导出 PNG。
* F3:toPNG 回调是异步触发,外层 try/catch 捕不到回调内错误;且 X6 的 toPNGAsync 在导出失败时
* 吞错永不 resolve(会挂死),故沿用回调风格,错误处理放入回调体(try/catch 包下载逻辑),
* 失败 console.error + Message.error 用户可见提示。
*/
function exportPNG() {
if (!graph) return
try {
graph.toPNG((dataUri: string) => {
graph.toPNG((dataUri: string) => {
try {
const a = document.createElement('a')
a.href = dataUri
a.download = `dependency-graph-${props.projectId}.png`
a.click()
})
} catch (e) {
console.error('[DependencyGraph] 导出 PNG 失败:', e)
}
} catch (e) {
console.error('[DependencyGraph] 导出 PNG 失败:', e)
Message.error(t('common.unknownError'))
}
})
}
/** 删除依赖(二次确认 + 重绘)。 */
@@ -384,12 +428,15 @@ function hideEdgeDeleteBtn() {
deleteDepTarget = null
}
// F2:edge hover 处理器提升为 setup 作用域稳定函数引用(非每次 bind 新建对象),
// 使 graph.off 能删掉旧 handler,避免 edge:mouseenter/leave 监听累积导致 showEdgeDeleteBtn 重复触发。
function onEdgeEnter(e: any) { showEdgeDeleteBtn(e.edge) }
function onEdgeLeave() { hideEdgeDeleteBtn() }
/** 给所有 edge 绑 hover 显示删除按钮的监听。 */
function bindEdgeDeleteButtons() {
if (!graph) return
try { graph.off('edge:mouseenter', onEdgeEnter); graph.off('edge:mouseleave', onEdgeLeave) } catch { /* ignore */ }
function onEdgeEnter(e: any) { showEdgeDeleteBtn(e.edge) }
function onEdgeLeave() { hideEdgeDeleteBtn() }
graph.on('edge:mouseenter', onEdgeEnter)
graph.on('edge:mouseleave', onEdgeLeave)
}
@@ -410,6 +457,18 @@ onMounted(async () => {
buildGraph()
})
// KeepAlive 保活:本组件被 KeepAlive 包裹不重挂载,缺 onActivated 时切回依赖图 tab 不刷新(F1)。
// 切回 tab 时重新拉模块+依赖并重绘(覆盖概览页增删模块后切回的场景)。
// 首次挂载 KeepAlive 激活也会触发,此时 onMounted 已 loadModules+renderGraph,守卫跳过避免重复拉。
let activatedOnce = false
onActivated(() => {
if (!activatedOnce) {
activatedOnce = true
return
}
void loadModules().then(() => renderGraph())
})
watch(() => props.projectId, async () => {
await loadModules()
renderGraph()