新增: 依赖图环形检测 + 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

@@ -1198,3 +1198,72 @@ pub async fn list_module_dependencies(
.await
.map_err(err_str)
}
/// 检测工程依赖图中的环形依赖(DFS 三色标记法)。
/// 返回参与环的 module_id 列表(空 = 无环)。
#[tauri::command]
pub async fn detect_module_cycles(
state: State<'_, AppState>,
project_id: String,
) -> Result<Vec<String>, String> {
let project_id = project_id.trim().to_string();
if project_id.is_empty() {
return Err("project_id 不能为空".to_string());
}
let deps = state
.module_dependencies
.list_by_field("project_id", &project_id)
.await
.map_err(err_str)?;
// 构建邻接表
let mut adj: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
for d in &deps {
adj.entry(d.from_module_id.clone())
.or_default()
.push(d.to_module_id.clone());
adj.entry(d.to_module_id.clone()).or_default();
}
// DFS 三色:0=白(未访问) 1=灰(栈中) 2=黑(完成)
let mut color: std::collections::HashMap<String, u8> = std::collections::HashMap::new();
let mut cycle_nodes: std::collections::HashSet<String> = std::collections::HashSet::new();
fn dfs(
node: &str,
adj: &std::collections::HashMap<String, Vec<String>>,
color: &mut std::collections::HashMap<String, u8>,
cycle_nodes: &mut std::collections::HashSet<String>,
path: &mut Vec<String>,
) {
let c = color.get(node).copied().unwrap_or(0);
if c == 1 {
// 发现环:path 中从当前节点开始的都参与环
let in_cycle = path.iter().position(|n| n == node);
if let Some(start) = in_cycle {
for n in &path[start..] {
cycle_nodes.insert(n.clone());
}
}
cycle_nodes.insert(node.to_string());
return;
}
if c == 2 {
return;
}
color.insert(node.to_string(), 1);
path.push(node.to_string());
if let Some(neighbors) = adj.get(node) {
for next in neighbors {
dfs(next, adj, color, cycle_nodes, path);
}
}
path.pop();
color.insert(node.to_string(), 2);
}
let nodes: Vec<String> = adj.keys().cloned().collect();
let mut path: Vec<String> = Vec::new();
for n in &nodes {
if color.get(n).copied().unwrap_or(0) == 0 {
dfs(n, &adj, &mut color, &mut cycle_nodes, &mut path);
}
}
Ok(cycle_nodes.into_iter().collect())
}

View File

@@ -203,6 +203,16 @@ pub async fn list_tasks(
Ok(tasks)
}
/// 按条件计数任务(分页 total 用)。
#[tauri::command]
pub async fn count_tasks(
state: State<'_, AppState>,
query: Option<TaskQuery>,
) -> Result<i64, String> {
let q = query.unwrap_or_default();
state.tasks.count_by_query(&q).await.map_err(err_str)
}
/// 按 id 查任务,找不到返回 Err(供前端详情页)
#[tauri::command]
pub async fn get_task_by_id(

View File

@@ -300,6 +300,7 @@ pub fn run() {
commands::project::import_projects_batch,
// 任务
commands::task::list_tasks,
commands::task::count_tasks,
commands::task::create_task,
commands::task::update_task,
commands::task::delete_task,
@@ -337,6 +338,7 @@ pub fn run() {
commands::module::add_module_dependency,
commands::module::remove_module_dependency,
commands::module::list_module_dependencies,
commands::module::detect_module_cycles,
// 灵感
commands::idea::list_ideas,
commands::idea::create_idea,