新增: 文件浏览器增强(窗口分离/Git 变更/行号/Diff/分页提交历史)
- 窗口分离: FileExplorer 可弹出独立 Tauri 窗口 - 行号显示: 文件预览左侧显示行号列 - 按后缀图标: rs/ts/js/vue/css/html 等 20+ 类型彩色图标 - Diff 视图: 有 Git 变更的文件可切换 diff 红绿视图 - Git 变更面板: 变更文件列表(按目录缩进分组)+提交历史(分页加载) - 提交详情: 点击提交查看变更文件列表及文件级 diff - 时间显示: 提交历史显示相对时间/具体日期 - 面包屑导航不丢预览: 导航时不改变当前预览文件 - 刷新保留选中文件: 只清树缓存不清预览 - 中文编码修复: git 命令注入 LANG/LC_ALL 环境变量 - 自适应布局: 弹性 flex 布局,窄窗口适配 - 滚动条优化: 内容区内部滚动,不溢出页面层 - 多工程管理: 工具栏添加工程按钮+弹窗表单 - 审批加载超时缩短: 130s 降至 30s - 文件变更自动刷新: write_file/patch_file 触发 df-data-changed
This commit is contained in:
@@ -30,6 +30,7 @@ pub(super) fn data_change_for_tool(name: &str) -> Option<(&'static str, &'static
|
||||
"delete_idea" => ("idea", "delete"),
|
||||
"restore_project" => ("project", "update"),
|
||||
"bind_directory" => ("project", "update"),
|
||||
"write_file" | "patch_file" => ("file", "update"),
|
||||
_ => return None,
|
||||
};
|
||||
Some((entity, action))
|
||||
|
||||
@@ -206,11 +206,44 @@ pub async fn list_project_modules(
|
||||
if project_id.is_empty() {
|
||||
return Err("project_id 不能为空".to_string());
|
||||
}
|
||||
state
|
||||
let modules = state
|
||||
.project_modules
|
||||
.list_by_project(&project_id)
|
||||
.await
|
||||
.map_err(err_str)
|
||||
.map_err(err_str)?;
|
||||
// 老项目兼容:V34 迁移前创建的项目无工程记录(project_modules 表为空)。
|
||||
// 自动从 projects.path 补建一个工程,使文件浏览器直接可用(单仓库退化场景)。
|
||||
if modules.is_empty() {
|
||||
if let Ok(Some(project)) = state.projects.get_by_id(&project_id).await {
|
||||
if let Some(ref path) = project.path {
|
||||
if !path.trim().is_empty() {
|
||||
let now_str = df_types::now_millis().to_string();
|
||||
let module = ProjectModuleRecord {
|
||||
id: df_types::types::new_id(),
|
||||
project_id: project_id.clone(),
|
||||
name: project.name.clone(),
|
||||
path: path.clone(),
|
||||
git_url: None,
|
||||
stack: project.stack.clone(),
|
||||
auto_detected: true,
|
||||
sort_order: 0,
|
||||
created_at: now_str.clone(),
|
||||
updated_at: now_str,
|
||||
};
|
||||
if let Err(e) = state.project_modules.insert(module.clone()).await {
|
||||
tracing::warn!("老项目自动补建工程失败(非阻断): {}", e);
|
||||
}
|
||||
// 补建后重查返回(确保前端拿到刚建的工程)
|
||||
return state
|
||||
.project_modules
|
||||
.list_by_project(&project_id)
|
||||
.await
|
||||
.map_err(err_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(modules)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -231,6 +264,8 @@ struct GitChangedFile {
|
||||
struct GitRecentCommit {
|
||||
hash: String,
|
||||
subject: String,
|
||||
/// Unix 时间戳(秒),供前端格式化显示。
|
||||
timestamp: i64,
|
||||
}
|
||||
|
||||
/// Git 状态返回结构(无 .git 时各字段空,前端据此显示"非 Git 仓库")。
|
||||
@@ -332,19 +367,27 @@ fn run_git_status(dir: &str) -> GitStatus {
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 最近提交:`git log -10 --oneline`(每行 "hash subject")
|
||||
let mut recent_commits = Vec::new();
|
||||
if let Some(out) = run_git_cmd(path, &["log", "-10", "--oneline"], timeout) {
|
||||
for line in out.lines() {
|
||||
// oneline 格式:"<short-hash> <subject>"
|
||||
if let Some((hash, subject)) = line.split_once(' ') {
|
||||
recent_commits.push(GitRecentCommit {
|
||||
hash: hash.to_string(),
|
||||
subject: subject.to_string(),
|
||||
});
|
||||
// 3) 最近提交:`git log -50 --format="%h %ct %s"`(hash + unix 时间戳 + subject)
|
||||
let mut recent_commits = Vec::new();
|
||||
if let Some(out) = run_git_cmd(path, &["log", "-50", "--format=%h %ct %s"], timeout) {
|
||||
for line in out.lines() {
|
||||
// format:"<hash> <unix_ts> <subject>"
|
||||
let line = line.trim();
|
||||
if line.is_empty() { continue; }
|
||||
let mut parts = line.splitn(3, ' ');
|
||||
let hash = parts.next().unwrap_or("").to_string();
|
||||
let ts_str = parts.next().unwrap_or("0");
|
||||
let timestamp: i64 = ts_str.parse().unwrap_or(0);
|
||||
let subject = parts.next().unwrap_or("").to_string();
|
||||
if !hash.is_empty() {
|
||||
recent_commits.push(GitRecentCommit {
|
||||
hash,
|
||||
subject,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GitStatus {
|
||||
branch,
|
||||
@@ -361,6 +404,7 @@ fn run_git_cmd(cwd: &std::path::Path, args: &[&str], timeout: std::time::Duratio
|
||||
use std::sync::mpsc;
|
||||
|
||||
// 在独立线程跑子进程,主线程 select 超时,避免标准库无超时 API 的痛点。
|
||||
// 编码:设置环境变量强制 git 输出 UTF-8(Windows 系统编码可能是 GBK,直接读会乱码)。
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let args = args.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
@@ -368,6 +412,9 @@ fn run_git_cmd(cwd: &std::path::Path, args: &[&str], timeout: std::time::Duratio
|
||||
let out = Command::new("git")
|
||||
.args(&args)
|
||||
.current_dir(&cwd)
|
||||
.env("LANG", "en_US.UTF-8")
|
||||
.env("LC_ALL", "en_US.UTF-8")
|
||||
.env("GIT_PAGER", "cat")
|
||||
.output();
|
||||
let _ = tx.send(out);
|
||||
});
|
||||
@@ -619,6 +666,108 @@ pub async fn read_module_file(
|
||||
}))
|
||||
}
|
||||
|
||||
/// 查询工程内文件元信息(不读内容,轻量检测变化用)。
|
||||
/// 返回 { path, size, modified_at }。modified_at 是毫秒级时间戳。
|
||||
#[tauri::command]
|
||||
pub async fn get_module_file_meta(
|
||||
state: State<'_, AppState>,
|
||||
module_id: String,
|
||||
file_path: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let module_id = module_id.trim().to_string();
|
||||
let file_path = file_path.trim().to_string();
|
||||
if module_id.is_empty() {
|
||||
return Err("module_id 不能为空".to_string());
|
||||
}
|
||||
if file_path.is_empty() {
|
||||
return Err("file_path 不能为空".to_string());
|
||||
}
|
||||
if file_path.contains("..") {
|
||||
return Err("file_path 不允许包含 ..".to_string());
|
||||
}
|
||||
let module = state
|
||||
.project_modules
|
||||
.get_by_id(&module_id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("工程 {module_id} 不存在"))?;
|
||||
let root = std::path::PathBuf::from(&module.path);
|
||||
let abs = root.join(&file_path);
|
||||
if !abs.is_file() {
|
||||
return Err(format!("文件不存在或不是普通文件: {}", file_path));
|
||||
}
|
||||
let meta = std::fs::metadata(&abs).map_err(|e| format!("读取文件元信息失败: {e}"))?;
|
||||
// 系统级 modified_at 需跨平台转换;用 std::time::UNIX_EPOCH 算毫秒
|
||||
let modified_ms = meta
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
Ok(serde_json::json!({
|
||||
"path": file_path.replace('\\', "/"),
|
||||
"size": meta.len(),
|
||||
"modified_at": modified_ms,
|
||||
}))
|
||||
}
|
||||
|
||||
/// 查询工程内文件的 git diff(相对 staged 或 working tree 的变更)。
|
||||
/// 返回 { path, diff }。diff 为空串表示无变更。
|
||||
#[tauri::command]
|
||||
pub async fn get_module_file_diff(
|
||||
state: State<'_, AppState>,
|
||||
module_id: String,
|
||||
file_path: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let module_id = module_id.trim().to_string();
|
||||
let file_path = file_path.trim().to_string();
|
||||
if module_id.is_empty() {
|
||||
return Err("module_id 不能为空".to_string());
|
||||
}
|
||||
if file_path.is_empty() {
|
||||
return Err("file_path 不能为空".to_string());
|
||||
}
|
||||
if file_path.contains("..") {
|
||||
return Err("file_path 不允许包含 ..".to_string());
|
||||
}
|
||||
let module = state
|
||||
.project_modules
|
||||
.get_by_id(&module_id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("工程 {module_id} 不存在"))?;
|
||||
let root = std::path::PathBuf::from(&module.path);
|
||||
if !root.join(".git").exists() {
|
||||
return Ok(serde_json::json!({"path": file_path, "diff": ""}));
|
||||
}
|
||||
// 跑 git diff <path>(工作树 vs 索引的 unstaged 变更+staged 变更)
|
||||
// 设置环境变量强制 UTF-8 输出,防中文乱码。
|
||||
let cmd_dir = root.clone();
|
||||
let path_arg = file_path.replace('\\', "/");
|
||||
let diff = std::thread::spawn(move || -> Option<String> {
|
||||
let out = std::process::Command::new("git")
|
||||
.args(["diff", "--", &path_arg])
|
||||
.current_dir(&cmd_dir)
|
||||
.env("LANG", "en_US.UTF-8")
|
||||
.env("LC_ALL", "en_US.UTF-8")
|
||||
.env("GIT_PAGER", "cat")
|
||||
.output()
|
||||
.ok()?;
|
||||
if out.status.success() && !out.stdout.is_empty() {
|
||||
Some(String::from_utf8_lossy(&out.stdout).to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.join()
|
||||
.map_err(|_| "git diff 线程 Join 失败".to_string())?
|
||||
.unwrap_or_default();
|
||||
Ok(serde_json::json!({
|
||||
"path": file_path.replace('\\', "/"),
|
||||
"diff": diff,
|
||||
}))
|
||||
}
|
||||
|
||||
/// 采集工程目录 git status --porcelain 的 {posix_rel_path: status} 映射。
|
||||
/// 非 git 仓库/命令失败 → 空映射(不阻断文件树展示,仅 git_status 字段全 None)。
|
||||
fn collect_git_status_map(dir: &str) -> HashMap<String, String> {
|
||||
@@ -655,3 +804,153 @@ fn collect_git_status_map(dir: &str) -> HashMap<String, String> {
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
/// 查询工程 Git 提交历史(分页,按时间倒序)。
|
||||
/// 返回 { commits: [{ hash, subject, timestamp }], has_more: bool }。
|
||||
#[tauri::command]
|
||||
pub async fn get_module_commits(
|
||||
state: State<'_, AppState>,
|
||||
module_id: String,
|
||||
skip: Option<u32>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let module_id = module_id.trim().to_string();
|
||||
if module_id.is_empty() {
|
||||
return Err("module_id 不能为空".to_string());
|
||||
}
|
||||
let module = state
|
||||
.project_modules
|
||||
.get_by_id(&module_id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("工程 {module_id} 不存在"))?;
|
||||
let path = std::path::Path::new(&module.path);
|
||||
if !path.join(".git").exists() {
|
||||
return Ok(serde_json::json!({ "commits": [], "has_more": false }));
|
||||
}
|
||||
let skip = skip.unwrap_or(0);
|
||||
let fetch = limit.unwrap_or(50);
|
||||
// 多取一条以判断 has_more
|
||||
let fetch_plus = fetch + 1;
|
||||
let dir = module.path.clone();
|
||||
let dir_for_git = dir.clone();
|
||||
let commits: Vec<serde_json::Value> = std::thread::spawn(move || -> Vec<serde_json::Value> {
|
||||
let out = std::process::Command::new("git")
|
||||
.args([
|
||||
"log",
|
||||
&format!("--skip={}", skip),
|
||||
&format!("-{}", fetch_plus),
|
||||
"--format=%h %ct %s",
|
||||
])
|
||||
.current_dir(&dir_for_git)
|
||||
.env("LANG", "en_US.UTF-8")
|
||||
.env("LC_ALL", "en_US.UTF-8")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| if o.status.success() { Some(o.stdout) } else { None })
|
||||
.map(|b| String::from_utf8_lossy(&b).to_string())
|
||||
.unwrap_or_default();
|
||||
let mut commits: Vec<serde_json::Value> = Vec::new();
|
||||
for line in out.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() { continue; }
|
||||
if commits.len() >= fetch as usize { break; }
|
||||
let mut parts = line.splitn(3, ' ');
|
||||
let hash = parts.next().unwrap_or("").to_string();
|
||||
let ts_str = parts.next().unwrap_or("0");
|
||||
let timestamp: i64 = ts_str.parse().unwrap_or(0);
|
||||
let subject = parts.next().unwrap_or("").to_string();
|
||||
if !hash.is_empty() {
|
||||
commits.push(serde_json::json!({
|
||||
"hash": hash,
|
||||
"subject": subject,
|
||||
"timestamp": timestamp,
|
||||
}));
|
||||
}
|
||||
}
|
||||
commits
|
||||
})
|
||||
.join()
|
||||
.map_err(|_| "提交历史查询线程 Join 失败".to_string())?;
|
||||
|
||||
// 判断 has_more:取了 N+1 条但只返回 N 条,说明有更多
|
||||
let total_fetched = commits.len();
|
||||
let has_more = total_fetched == fetch_plus as usize;
|
||||
let returned: Vec<_> = commits.into_iter().take(fetch as usize).collect();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"commits": returned,
|
||||
"has_more": has_more,
|
||||
}))
|
||||
}
|
||||
|
||||
/// 查询某次提交的变更文件列表及全量 diff。
|
||||
/// 返回 { files: [{ status, path }], diff: string }。
|
||||
#[tauri::command]
|
||||
pub async fn get_commit_detail(
|
||||
state: State<'_, AppState>,
|
||||
module_id: String,
|
||||
commit_hash: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let module_id = module_id.trim().to_string();
|
||||
let commit_hash = commit_hash.trim().to_string();
|
||||
if module_id.is_empty() {
|
||||
return Err("module_id 不能为空".to_string());
|
||||
}
|
||||
if commit_hash.is_empty() {
|
||||
return Err("commit_hash 不能为空".to_string());
|
||||
}
|
||||
let module = state
|
||||
.project_modules
|
||||
.get_by_id(&module_id)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.ok_or_else(|| format!("工程 {module_id} 不存在"))?;
|
||||
let path = std::path::Path::new(&module.path);
|
||||
if !path.join(".git").exists() {
|
||||
return Ok(serde_json::json!({ "files": [], "diff": "" }));
|
||||
}
|
||||
let dir = module.path.clone();
|
||||
let (files, diff) = std::thread::spawn(move || -> (Vec<serde_json::Value>, String) {
|
||||
// 1) 获取变更文件列表:`git diff-tree --no-commit-id -r --name-status <hash>`
|
||||
let files_out = std::process::Command::new("git")
|
||||
.args(["diff-tree", "--no-commit-id", "-r", "--name-status", &commit_hash])
|
||||
.current_dir(&dir)
|
||||
.env("LANG", "en_US.UTF-8")
|
||||
.env("LC_ALL", "en_US.UTF-8")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| if o.status.success() { Some(String::from_utf8_lossy(&o.stdout).to_string()) } else { None })
|
||||
.unwrap_or_default();
|
||||
let mut files: Vec<serde_json::Value> = Vec::new();
|
||||
for line in files_out.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() { continue; }
|
||||
// format:"<status>\t<path>"
|
||||
if let Some((status, fpath)) = line.split_once('\t') {
|
||||
files.push(serde_json::json!({
|
||||
"status": status.trim(),
|
||||
"path": fpath.trim(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
// 2) 获取全量 diff:`git show <hash>`
|
||||
let diff = std::process::Command::new("git")
|
||||
.args(["show", "--format=", &commit_hash])
|
||||
.current_dir(&dir)
|
||||
.env("LANG", "en_US.UTF-8")
|
||||
.env("LC_ALL", "en_US.UTF-8")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| if o.status.success() { Some(String::from_utf8_lossy(&o.stdout).to_string()) } else { None })
|
||||
.unwrap_or_default();
|
||||
(files, diff)
|
||||
})
|
||||
.join()
|
||||
.map_err(|_| "提交详情查询线程 Join 失败".to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"files": files,
|
||||
"diff": diff,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -328,6 +328,10 @@ pub fn run() {
|
||||
// 工程文件浏览(Batch 10):文件树 + 单文件预览
|
||||
commands::module::get_module_file_tree,
|
||||
commands::module::read_module_file,
|
||||
commands::module::get_module_file_meta,
|
||||
commands::module::get_module_file_diff,
|
||||
commands::module::get_module_commits,
|
||||
commands::module::get_commit_detail,
|
||||
// 灵感
|
||||
commands::idea::list_ideas,
|
||||
commands::idea::create_idea,
|
||||
|
||||
Reference in New Issue
Block a user