新增: 依赖图环形检测 + PNG 导出 + 任务列表真实总数

- 后端 detect_module_cycles IPC(DFS 三色标记法检测环形依赖)
- 前端环检测按钮:高亮参与环的节点(红色边框)
- 图导出 PNG(X6 toPNG 回调模式)
- 后端 count_tasks IPC + 前端 taskApi.count()
- 任务列表所有筛选/搜索/排序/翻页均拉真实 total
This commit is contained in:
2026-07-01 11:30:19 +08:00
parent 89912155a6
commit 36ea090d9b
7 changed files with 151 additions and 5 deletions

View File

@@ -168,6 +168,11 @@ export const moduleApi = {
return invoke('list_branches', { moduleId })
},
/** 检测工程依赖环形依赖,返回参与环的 module_id 列表(空=无环)。 */
detectModuleCycles(projectId: string): Promise<string[]> {
return invoke('detect_module_cycles', { projectId })
},
/** 列出项目全部工程依赖。 */
listModuleDependencies(projectId: string): Promise<ModuleDependencyRecord[]> {
return invoke('list_module_dependencies', { projectId })

View File

@@ -18,6 +18,11 @@ export const taskApi = {
return invoke('list_tasks', { projectId: null, query: queryOrProjectId ?? null })
},
/** 按条件计数任务(分页 total 用)。 */
count(query?: TaskQuery): Promise<number> {
return invoke('count_tasks', { query: query ?? null })
},
get(id: string): Promise<TaskRecord> {
return invoke('get_task_by_id', { id })
},

View File

@@ -4,6 +4,8 @@
<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 v-if="dependencies.length > 0" class="btn btn-ghost btn-sm" @click="checkCycles">🔍 环检测</button>
<button class="btn btn-ghost btn-sm" @click="exportPNG">📷 导出</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>
@@ -211,6 +213,43 @@ function zoomOut() {
graph?.zoom(-0.1)
}
/** 环形依赖检测。 */
const cycleNodes = ref<Set<string>>(new Set())
async function checkCycles() {
try {
const cycles = await moduleApi.detectModuleCycles(props.projectId)
cycleNodes.value = new Set(cycles)
if (cycles.length > 0) {
// 高亮环节点(加红色边框)
for (const id of cycles) {
const cell = graph?.getCellById(id)
if (cell) {
cell.attr('body/stroke', '#e05050')
cell.attr('body/strokeWidth', 3)
}
}
}
renderGraph()
} catch (e) {
console.error('[DependencyGraph] 环检测失败:', e)
}
}
/** 导出 PNG。 */
async function exportPNG() {
if (!graph) return
try {
graph.toPNG((dataUri: string) => {
const a = document.createElement('a')
a.href = dataUri
a.download = `dependency-graph-${props.projectId}.png`
a.click()
})
} catch (e) {
console.error('[DependencyGraph] 导出 PNG 失败:', e)
}
}
/** 依赖类型 → 边颜色 */
function depTypeColor(depType: string): string {
switch (depType) {

View File

@@ -252,7 +252,10 @@ watch(searchKeyword, () => {
_searchTimer = setTimeout(() => {
page.value = 1
loading.value = true
store.loadTasks(buildTaskQuery()).finally(() => { loading.value = false })
Promise.all([
store.loadTasks(buildTaskQuery()),
taskApi.count(buildTaskQuery()).then(n => totalTasks.value = n),
]).finally(() => { loading.value = false })
}, 300)
})
@@ -383,20 +386,29 @@ watch(activeProject, () => {
store.setActiveTaskProject(activeProject.value)
page.value = 1
loading.value = true
store.loadTasks(buildTaskQuery()).finally(() => { loading.value = false })
Promise.all([
store.loadTasks(buildTaskQuery()),
taskApi.count(buildTaskQuery()).then(n => totalTasks.value = n),
]).finally(() => { loading.value = false })
})
watch(activeStatus, () => {
store.setActiveTaskStatus(activeStatus.value)
page.value = 1
loading.value = true
store.loadTasks(buildTaskQuery()).finally(() => { loading.value = false })
Promise.all([
store.loadTasks(buildTaskQuery()),
taskApi.count(buildTaskQuery()).then(n => totalTasks.value = n),
]).finally(() => { loading.value = false })
})
watch(sortBy, () => {
page.value = 1
loading.value = true
store.loadTasks(buildTaskQuery()).finally(() => { loading.value = false })
Promise.all([
store.loadTasks(buildTaskQuery()),
taskApi.count(buildTaskQuery()).then(n => totalTasks.value = n),
]).finally(() => { loading.value = false })
})
// Ctrl+N 新建 / Ctrl+F 搜索(桌面快捷键)
@@ -415,7 +427,11 @@ onMounted(async () => {
store.setActiveTaskStatus(activeStatus.value)
loading.value = true
try {
await Promise.all([store.loadProjects(), store.loadTasks()])
await Promise.all([
store.loadProjects(),
store.loadTasks(),
taskApi.count().then(n => totalTasks.value = n),
])
} finally {
loading.value = false
}