新增: 多工程管理(编辑/删除确认/记忆选中/自动扫描子仓库)

- 工程编辑入口:工具栏编辑按钮+弹窗(名称/路径/Git地址)
- 工程删除二次确认:防误删
- 记住上次选中工程:localStorage 持久化
- 自动扫描子仓库:下拉菜单加扫描入口,发现 .git 子目录自动建工程
This commit is contained in:
2026-06-30 20:49:58 +08:00
parent 6b131f3dc1
commit abc6880936
6 changed files with 261 additions and 39 deletions

View File

@@ -768,6 +768,88 @@ pub async fn get_module_file_diff(
}))
}
/// 扫描项目绑定目录下的子仓库(含 .git 的直接子目录),自动创建工程记录。
/// 返回新创建的工程数量。幂等:已存在的路径不重复创建。
#[tauri::command]
pub async fn scan_project_modules(
state: State<'_, AppState>,
project_id: String,
) -> Result<i64, String> {
let project_id = project_id.trim().to_string();
if project_id.is_empty() {
return Err("project_id 不能为空".to_string());
}
// 取项目记录拿 path
let project = state
.projects
.get_by_id(&project_id)
.await
.map_err(err_str)?
.ok_or_else(|| format!("项目 {project_id} 不存在"))?;
let root_path = project
.path
.as_ref()
.filter(|p| !p.trim().is_empty())
.ok_or_else(|| "项目未绑定目录".to_string())?;
let root = PathBuf::from(root_path);
// 取已有工程路径集(避免重复创建)
let existing = state
.project_modules
.list_by_project(&project_id)
.await
.map_err(err_str)?;
let existing_paths: std::collections::HashSet<String> = existing
.iter()
.map(|m| m.path.to_lowercase().replace("\\", "/"))
.collect();
// 扫描一级子目录(含 .git)
let mut new_count = 0i64;
let entries = std::fs::read_dir(&root)
.map_err(|e| format!("读取项目目录失败: {e}"))?;
for entry in entries.flatten() {
let ft = match entry.file_type() { Ok(t) => t, Err(_) => continue };
if !ft.is_dir() { continue; }
let name = entry.file_name().to_string_lossy().to_string();
// 跳过隐藏目录和噪音目录
if name.starts_with('.') || name == "node_modules" || name == "target" || name == "__pycache__" || name == "dist" {
continue;
}
let child_path = entry.path();
// 必须含 .git(独立仓库)
if !child_path.join(".git").exists() { continue; }
let child_path_str = child_path.to_string_lossy().replace("\\", "/");
if existing_paths.contains(&child_path_str.to_lowercase()) { continue; }
// 获取远程地址(失败忽略)
let git_url = std::process::Command::new("git")
.args(["remote", "get-url", "origin"])
.current_dir(&child_path)
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|s| !s.is_empty());
let now_str = now_millis();
let record = ProjectModuleRecord {
id: new_id(),
project_id: project_id.clone(),
name: name.clone(),
path: child_path.to_string_lossy().to_string(),
git_url,
stack: None,
auto_detected: true,
sort_order: (existing.len() + new_count as usize) as i32,
created_at: now_str.clone(),
updated_at: now_str,
};
if let Err(e) = state.project_modules.insert(record).await {
tracing::warn!("自动探测工程 {} 失败: {}", child_path_str, e);
continue;
}
new_count += 1;
}
Ok(new_count)
}
/// 采集工程目录 git status --porcelain 的 {posix_rel_path: status} 映射。
/// 非 git 仓库/命令失败 → 空映射(不阻断文件树展示,仅 git_status 字段全 None)。
fn collect_git_status_map(dir: &str) -> HashMap<String, String> {

View File

@@ -324,6 +324,7 @@ pub fn run() {
commands::module::update_project_module,
commands::module::remove_project_module,
commands::module::list_project_modules,
commands::module::scan_project_modules,
commands::module::get_module_git_status,
// 工程文件浏览(Batch 10):文件树 + 单文件预览
commands::module::get_module_file_tree,