优化: 所有剩余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
+32 -4
View File
@@ -125,12 +125,40 @@ function onResize() {
// ------------------------------------------------------------
// SettingsNav 搜索命中首匹配项时上抛 scroll-target(key)。
// 此处据 key 查 SETTINGS_INDEX 取 labelKey → t() 得当前 locale 文本,
// 在右侧内容 DOM 中找匹配文本的 .setting-label(表单型 Section)或
// 在右侧内容 DOM 中找匹配的 .setting-label(表单型 Section)或
// .panel-header h2(列表型面板如 ProviderPanel),scrollIntoView 平滑定位。
// 用文本匹配而非 data-* 属性:避免改动各 Section(严守"仅改 plan 列出文件")。
//
// 匹配策略(2026-08-02 加固):
// 旧实现用 textContent.trim() === labelText 严格相等,SettingRow 的 .setting-label
// 常带 next-round-badge 子 span(「下次对话生效」) → textContent 拼接出
// 「主题 下次对话生效」≠「主题」,严格相等失效,搜索定位静默无命中。
// 现改读 .setting-label 的"首子文本节点"(纯 label,排除 badge/desc 等子元素),
// 与 labelText trim 后 includes 匹配(双向 includes 兜底首尾空白/标点)。
// h2 同理改 includes 提升容错。理想方案是 .setting-label 加 data-setting-key,
// 但那需改 SettingRow.vue + 6 个 Section(越"仅改指定文件"范围),故此处加固文本匹配。
// ============================================================
const contentRef = ref<HTMLElement | null>(null)
/** 取元素"首文本节点"内容(跳过子元素,排除 SettingRow 的 next-round-badge 拼接干扰) */
function firstTextNodeText(el: HTMLElement | null): string {
if (!el) return ''
// 优先 childNodes 中的首个 text node;退而 textContent(无子元素场景)
for (const node of Array.from(el.childNodes)) {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent || ''
}
}
return el.textContent || ''
}
/** 双向 includes 匹配(容错首尾空白/标点差异;空串不命中) */
function fuzzyMatch(a: string, b: string): boolean {
const sa = a.trim()
const sb = b.trim()
if (!sa || !sb) return false
return sa.includes(sb) || sb.includes(sa)
}
function scrollToSettingItem(itemKey: string) {
const entry = SETTINGS_INDEX.find((e) => e.key === itemKey)
if (!entry) return
@@ -145,7 +173,7 @@ function scrollToSettingItem(itemKey: string) {
const labels = root.querySelectorAll<HTMLElement>('.setting-label')
let target: HTMLElement | null = null
for (const el of labels) {
if (el.textContent?.trim() === labelText.trim()) {
if (fuzzyMatch(firstTextNodeText(el), labelText)) {
target = el.closest<HTMLElement>('.setting-row') ?? el
break
}
@@ -153,7 +181,7 @@ function scrollToSettingItem(itemKey: string) {
if (!target) {
const headers = root.querySelectorAll<HTMLElement>('.panel-header h2')
for (const el of headers) {
if (el.textContent?.trim() === labelText.trim()) {
if (fuzzyMatch(firstTextNodeText(el), labelText)) {
target = el
break
}