优化: 项目文件树/依赖图组件 + 灵感对抗校验 + 小程序脚本 + 模块管理
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user