From abc68809366863708492ff0e8d29df4ef78e8a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 30 Jun 2026 20:49:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E5=A4=9A=E5=B7=A5?= =?UTF-8?q?=E7=A8=8B=E7=AE=A1=E7=90=86(=E7=BC=96=E8=BE=91/=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E7=A1=AE=E8=AE=A4/=E8=AE=B0=E5=BF=86=E9=80=89?= =?UTF-8?q?=E4=B8=AD/=E8=87=AA=E5=8A=A8=E6=89=AB=E6=8F=8F=E5=AD=90?= =?UTF-8?q?=E4=BB=93=E5=BA=93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 工程编辑入口:工具栏编辑按钮+弹窗(名称/路径/Git地址) - 工程删除二次确认:防误删 - 记住上次选中工程:localStorage 持久化 - 自动扫描子仓库:下拉菜单加扫描入口,发现 .git 子目录自动建工程 --- src-tauri/src/commands/module.rs | 82 +++++++++++ src-tauri/src/lib.rs | 1 + src/api/module.ts | 21 +++ src/components/project/FileExplorer.vue | 188 +++++++++++++++++++----- src/i18n/en/fileExplorer.ts | 4 + src/i18n/zh-CN/fileExplorer.ts | 4 + 6 files changed, 261 insertions(+), 39 deletions(-) diff --git a/src-tauri/src/commands/module.rs b/src-tauri/src/commands/module.rs index 5e00cc5..3286c3a 100644 --- a/src-tauri/src/commands/module.rs +++ b/src-tauri/src/commands/module.rs @@ -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 { + 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 = 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 { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index edd27c3..943fbf4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, diff --git a/src/api/module.ts b/src/api/module.ts index 89da97a..2de1c56 100644 --- a/src/api/module.ts +++ b/src/api/module.ts @@ -87,6 +87,11 @@ export const moduleApi = { return invoke('add_project_module', { input }) }, + /** 扫描项目绑定目录下的子仓库,自动创建工程记录(幂等)。返回新创建数量。 */ + scanProjectModules(projectId: string): Promise { + return invoke('scan_project_modules', { projectId }) + }, + /** 列出项目全部工程(按 sort_order ASC)。 */ listProjectModules(projectId: string): Promise { return invoke('list_project_modules', { projectId }) @@ -128,6 +133,22 @@ export const moduleApi = { return invoke('get_module_file_diff', { moduleId, filePath }) }, + /** 更新工程记录。 */ + updateProjectModule(input: { + id: string + name?: string + path?: string + gitUrl?: string | null + stack?: string | null + }): Promise { + return invoke('update_project_module', { input }) + }, + + /** 删除工程记录。 */ + removeProjectModule(moduleId: string): Promise { + return invoke('remove_project_module', { id: moduleId }) + }, + /** 分页查询工程 Git 提交历史。返回 { commits, has_more }。 */ getModuleCommits(moduleId: string, skip?: number, limit?: number): Promise<{ commits: { hash: string; subject: string; timestamp: number }[] diff --git a/src/components/project/FileExplorer.vue b/src/components/project/FileExplorer.vue index 4e89ee6..3e4094c 100644 --- a/src/components/project/FileExplorer.vue +++ b/src/components/project/FileExplorer.vue @@ -25,6 +25,10 @@ {{ m.name }} {{ m.path }} +
+
+ 🔍 {{ $t('fileExplorer.scanSubmodules') }} +
@@ -41,7 +45,13 @@
- + +
- -