优化: 所有剩余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
+47 -9
View File
@@ -276,31 +276,69 @@ async function deleteConversation(id: string) {
notifyConversationChanged()
}
/** 重命名会话(后端 + 本地侧栏摘要同步) */
/** M30:写后端 IPC 失败时推送错误气泡(对齐 loadConversations/switchConversation 的失败反馈),
* 保持侧栏已有交互不被无声吞掉(原仅向上抛,调用方 store 直调无 try → 异常进 unhandledrejection)。 */
function pushConvOpFail(key: 'renameConvFail' | 'archiveConvFail' | 'pinConvFail', params?: Record<string, string | number>) {
state.messages.push({
id: `conv-op-fail-${nextMsgId()}`,
role: 'assistant',
content: t(`ai.${key}`, params),
isError: true,
timestamp: Date.now(),
} as AiMessage)
notifyConversationChanged()
}
/** 重命名会话(后端 + 本地侧栏摘要同步)。
* M30:乐观更新(本地先写 → IPC → 失败回滚 + 提示)。原逻辑 IPC 成功才写本地,IPC 失败时异常
* 向上抛但 store 直调处无 try/catch → unhandledrejection,用户无感。改乐观更新让失败可见可回滚。 */
async function renameConversation(id: string, title: string) {
await aiApi.renameConversation(id, title)
// 本地同步更新侧栏摘要标题
const conv = state.conversations.find(c => c.id === id)
const prevTitle = conv?.title ?? null
if (conv) conv.title = title
notifyConversationChanged()
try {
await aiApi.renameConversation(id, title)
} catch (e) {
console.error('[AI] 重命名会话失败:', e)
if (conv) conv.title = prevTitle
notifyConversationChanged()
pushConvOpFail('renameConvFail')
}
}
/** 归档/取消归档(后端 + 本地侧栏分组同步) */
/** 归档/取消归档(后端 + 本地侧栏分组同步)
* M30:乐观更新 + 失败回滚 + 提示(同 renameConversation)。 */
async function archiveConversation(id: string, archived: boolean) {
await aiApi.archiveConversation(id, archived)
// 本地同步归档态(侧栏立即移入/移出归档分组)
const conv = state.conversations.find(c => c.id === id)
const prevArchived = conv?.archived ?? false
if (conv) conv.archived = archived
notifyConversationChanged()
try {
await aiApi.archiveConversation(id, archived)
} catch (e) {
console.error('[AI] 归档会话失败:', e)
if (conv) conv.archived = prevArchived
notifyConversationChanged()
pushConvOpFail('archiveConvFail', { action: archived ? t('ai.archiveAction') : t('ai.unarchiveAction') })
}
}
/** 置顶/取消置顶(后端 + 本地侧栏排序同步;UX-17) */
/** 置顶/取消置顶(后端 + 本地侧栏排序同步;UX-17)
* M30:乐观更新 + 失败回滚 + 提示(同 renameConversation)。 */
async function setPinnedConversation(id: string, pinned: boolean) {
await aiApi.setPinnedConversation(id, pinned)
// 本地同步置顶态(排序 computed 读 pinned,立即重排)
const conv = state.conversations.find(c => c.id === id)
const prevPinned = conv?.pinned ?? false
if (conv) conv.pinned = pinned
notifyConversationChanged()
try {
await aiApi.setPinnedConversation(id, pinned)
} catch (e) {
console.error('[AI] 置顶会话失败:', e)
if (conv) conv.pinned = prevPinned
notifyConversationChanged()
pushConvOpFail('pinConvFail', { action: pinned ? t('ai.pinAction') : t('ai.unpinAction') })
}
}
/** 折叠/展开归档分组 */
+56
View File
@@ -0,0 +1,56 @@
//! localStorage 持久化 ref — 读初始化 + 写回 localStorage(JSON)
//!
//! 背景:Tasks/Projects/Ideas 等列表页的筛选/分页状态散布在多个 ref,各自手写
//! localStorage.getItem/setItem 易漏且风格不一。本 composable 收敛为声明式一行:
//! const page = usePersistedRef('tasks.page', 1)
//!
//! 行为:
//! - 初始化时读 localStorage,存在且 JSON 解析成功则用其值,否则用 default
//! - watch ref 深度变化,JSON.stringify 写回 localStorage(try/catch 防脏数据/配额)
//! - SSR/无 localStorage 环境(SSR 不适用于 Tauri webview,但防御性兜底)返回纯 ref
//!
//! 设计取舍:
//! - 不做跨 tab 同步(storage 事件)——单实例 Tauri 应用无此需求
//! - 不做类型校验(解析出的值类型可能漂移)——调用方 default 决定类型,JSON 解析成功即采用,
//! 若需严格类型校验可在调用方业务层判断(当前所有调用点均为 string/number,无需求)
import { ref, watch, type Ref } from 'vue'
/**
* 创建一个持久化到 localStorage 的 ref。
*
* @param key localStorage 键名(建议带模块前缀,如 'tasks.page')
* @param defaultValue 默认值(localStorage 无值或解析失败时使用)
* @returns 与普通 ref 行为一致的 Ref<T>
*/
export function usePersistedRef<T>(key: string, defaultValue: T): Ref<T> {
// 初始化:读 localStorage,失败/不存在回落到 default
const initial = readPersisted(key, defaultValue)
const r = ref(initial) as Ref<T>
// 写回:深度 watch,JSON.stringify 失败静默(防循环引用等异常)
watch(
r,
(val) => {
try {
localStorage.setItem(key, JSON.stringify(val))
} catch {
/* 忽略:配额溢出 / 序列化失败 / 隐私模式禁用 localStorage */
}
},
{ deep: true },
)
return r
}
/** 读 localStorage 并 JSON 解析,失败/不存在返回 defaultValue。仅本模块内部使用。 */
function readPersisted<T>(key: string, defaultValue: T): T {
try {
const raw = localStorage.getItem(key)
if (raw === null) return defaultValue
return JSON.parse(raw) as T
} catch {
return defaultValue
}
}