优化: 所有剩余UI/UX待办一批完成(持久化+AuditLog+解耦+total+原12大改+P2)

持久化(P1-c):新建 usePersistedRef composable,Tasks/AuditLog/ProjectDetail 等接入 localStorage

AuditLog(P1-d):后端 list_tool_executions 加 WHERE 筛选+返 {items,total,has_more},前端对接+长列折叠+筛选持久化

数据源解耦(P1-g):ProjectDetail projectTasks 按 project_id 独立加载 + ChatInput @项目联想独立加载(不读 store.tasks 当前页)

GitChanges(12a):后端 get_module_commits 加 git rev-list --count 返 total,前端显真实总数

原12大改:Dashboard 统计卡压底行(1)/Projects 列表卡片视图(2)/project_event 埋点排序(3)/TaskDetail 重设计(4)/IdeaDetail 重设计(5)/KnowledgeDetail 重设计(6)/界面持久化+侧栏Ctrl+B+审批数字键(7)/ProjectDetail 三栏改两栏(10)

P2打磨:文件浏览器(FileTree去重/FilePreview行号.md Diff/selectedFilePath归位)/settings反馈(假保存/端口校验)/AI会话(try-catch/scrollIntoView)/后端计数(move_queue事件/timeline total/workflow分页/import_batch分块)/杂项(TopBar/ConfirmDialog键盘/CIStatus i18n/ToolResultBody/ModuleNode/ApprovalDialog全选)
This commit is contained in:
lxy
2026-08-02 13:11:06 +08:00
parent caaabf0c15
commit f736f435bc
70 changed files with 2645 additions and 576 deletions
@@ -184,6 +184,32 @@ impl_repo!(
}
);
// ============================================================
// AuditQuery — 审批历史多条件查询入参(status / risk / 工具名关键词)
// ============================================================
/// 审批历史多条件查询入参(对标 [`IdeaQuery`] 的可选字段 struct 设计)。
///
/// 所有字段可选;全 None → 等价 `list_recent`(向后兼容)。设计对齐 `查询能力补全方案`:
/// 可选字段 struct 而非逐个加 IPC 参数,复用 [`IdeaRepo::list_by_query`] 的动态 WHERE 拼接
/// 模式(if-let 分支拼 SQL + 分支化参数绑定)。
///
/// - `status`:状态精确匹配(pending/approved/rejected/executing/completed/failed/interrupted)
/// - `risk_level`:风险等级精确匹配(low/medium/high)
/// - `tool_keyword`:`tool_name LIKE %kw%`(对齐 idea_repo 关键词 LIKE 检索,不上 FTS5)
/// - `limit`/`offset`:钳制上限 200(对齐 [`AiToolExecutionRepo::list_recent`])
///
/// `Deserialize`:Tauri IPC 从前端 JSON 反序列化为命令参数。
/// `Default`:命令层兼容旧全量调用(`AuditQuery::default()` 等价无条件)。
#[derive(Debug, Clone, Default, serde::Deserialize)]
pub struct AuditQuery {
pub status: Option<String>,
pub risk_level: Option<String>,
pub tool_keyword: Option<String>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
impl_repo!(
/// AI 工具执行审计表 CRUD
AiToolExecutionRepo,
@@ -318,6 +344,125 @@ impl AiToolExecutionRepo {
.await
.map_err(storage_err)?
}
/// 多条件查询:动态 WHERE 拼接(status / risk_level / 工具名关键词) + 分页。
///
/// 复用 [`IdeaRepo::list_by_query`] 的动态 WHERE 模式:if-let 分支按可选条件拼 SQL 片段,
/// 各分支化参数绑定到 `?N` 占位符。limit 钳制上限 200(对齐 [`Self::list_recent`])。
///
/// **向后兼容**:空 query(全 None)→ 无 WHERE 子句,等价 `list_recent`。
/// 与 list_pending/list_recent 同理走专用 SELECT,绕过通用 query 宏(后者硬编码
/// ORDER BY created_at,本表无该列)。
pub async fn list_by_query(&self, q: &AuditQuery) -> Result<Vec<AiToolExecutionRecord>> {
let conn = self.conn.clone();
let status = q.status.clone();
let risk = q.risk_level.clone();
let kw = q.tool_keyword.clone();
let limit_i: i64 = q.limit.unwrap_or(50).min(200) as i64;
let offset_i: i64 = q.offset.unwrap_or(0) as i64;
tokio::task::spawn_blocking(move || {
let guard = conn.blocking_lock();
let mut where_clauses: Vec<String> = Vec::new();
let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
if let Some(s) = &status {
where_clauses.push(format!("status = ?{}", params_vec.len() + 1));
params_vec.push(Box::new(s.clone()));
}
if let Some(r) = &risk {
where_clauses.push(format!("risk_level = ?{}", params_vec.len() + 1));
params_vec.push(Box::new(r.clone()));
}
if let Some(k) = &kw {
let escaped = k.replace('%', "\\%").replace('_', "\\_");
let pat = format!("%{escaped}%");
where_clauses.push(format!("tool_name LIKE ?{} ESCAPE '\\'", params_vec.len() + 1));
params_vec.push(Box::new(pat));
}
let where_sql = if where_clauses.is_empty() {
String::new()
} else {
format!(" WHERE {}", where_clauses.join(" AND "))
};
let where_param_count = params_vec.len();
let sql = format!(
"SELECT * FROM ai_tool_executions{where_sql} \
ORDER BY requested_at DESC LIMIT ?{lim} OFFSET ?{off}",
lim = where_param_count + 1,
off = where_param_count + 2,
);
let mut stmt = guard.prepare(&sql).map_err(storage_err)?;
params_vec.push(Box::new(limit_i));
params_vec.push(Box::new(offset_i));
let param_refs: Vec<&dyn rusqlite::ToSql> =
params_vec.iter().map(|p| p.as_ref()).collect();
let rows = stmt
.query_map(param_refs.as_slice(), |row| ai_tool_execution_from_row(row))
.map_err(storage_err)?;
let mut results = Vec::new();
for r in rows {
results.push(r.map_err(storage_err)?);
}
Ok(results)
})
.await
.map_err(storage_err)?
}
/// 按 [`AuditQuery`] 条件计数(不含 limit/offset,用于分页 total)。
///
/// 复用 [`Self::list_by_query`] 的 WHERE 构造逻辑(仅 WHERE,无 ORDER BY/LIMIT),
/// 返回满足条件的总行数(忽略分页裁剪)。对标 [`TaskRepo::count_by_query`]。
pub async fn count_by_query(&self, q: &AuditQuery) -> Result<i64> {
let conn = self.conn.clone();
let status = q.status.clone();
let risk = q.risk_level.clone();
let kw = q.tool_keyword.clone();
tokio::task::spawn_blocking(move || {
let guard = conn.blocking_lock();
let mut where_clauses: Vec<String> = Vec::new();
let mut params_vec: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
if let Some(s) = &status {
where_clauses.push(format!("status = ?{}", params_vec.len() + 1));
params_vec.push(Box::new(s.clone()));
}
if let Some(r) = &risk {
where_clauses.push(format!("risk_level = ?{}", params_vec.len() + 1));
params_vec.push(Box::new(r.clone()));
}
if let Some(k) = &kw {
let escaped = k.replace('%', "\\%").replace('_', "\\_");
let pat = format!("%{escaped}%");
where_clauses.push(format!("tool_name LIKE ?{} ESCAPE '\\'", params_vec.len() + 1));
params_vec.push(Box::new(pat));
}
let sql = if where_clauses.is_empty() {
"SELECT COUNT(*) FROM ai_tool_executions".to_string()
} else {
format!(
"SELECT COUNT(*) FROM ai_tool_executions WHERE {}",
where_clauses.join(" AND ")
)
};
let param_refs: Vec<&dyn rusqlite::ToSql> =
params_vec.iter().map(|p| p.as_ref()).collect();
let count: i64 = guard
.query_row(&sql, param_refs.as_slice(), |row| row.get(0))
.map_err(storage_err)?;
Ok(count)
})
.await
.map_err(storage_err)?
}
}
// AiConversationRepo 的整体更新已由 impl_repo! 宏统一生成的 update_full 提供。