优化: 所有剩余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:
@@ -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 提供。
|
||||
|
||||
Reference in New Issue
Block a user