优化: 所有剩余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
+56 -4
View File
@@ -110,6 +110,7 @@ import SkillMention from './SkillMention.vue'
import ImageInput from './ImageInput.vue'
import MentionPopover from './MentionPopover.vue'
import EnrichmentPanel from './EnrichmentPanel.vue'
import { taskApi } from '../../api/task'
import type { AiMessage, ContentPart, SkillInfo, ProjectRecord, TaskRecord, IdeaRecord, MentionSpan } from '../../api/types'
import type { MentionItem } from './MentionItem'
@@ -323,7 +324,8 @@ function clearSkill() {
// ── UX-10 §1.4: @ 实体引用(@ 触发 popover,选中插入 [类型:名] 标记,后端 system prompt 注入摘要) ──
// 架构复用 / 技能联想:独立状态(mentionOpen/mentionIndex/mentionQuery),不与 skill 混。
// 联想源只读 projectsStore.projects / projectsStore.tasks(前端 store 现有数据,不新增 IPC)。
// 联想源:projects/ideas 读 store;tasks 联想候选读 store.tasks,但 @项目 enrichment
// 反查任务走 projectTasksCache(独立 list_tasks,隔离 Tasks 视图筛选,见 P1-g)。
// 触发:输入框光标前最近的 @ 后无空白/换行 → 激活;Esc/光标移开/选中插入 → 关闭。
// Enter 不冲突:popover 开时 Enter 插入选中(覆盖发送),关时 Enter 发送(原逻辑)。
// MentionItem 接口已抽至 ./MentionItem.ts(MentionPopover 子组件共用)。
@@ -340,6 +342,32 @@ const pendingMentionSpans = ref<MentionSpan[]>([])
// ── ⑥.4 Phase 4: @项目展开摘要(输入区 enrichment 预览) ──
// 当用户 @[项目:xxx] 后,从 projectsStore 反查项目+关联任务/灵感,
// 在输入区下方显式 badge + 可展开的上下文参考面板。
// P1-g: 任务联想源隔离 store.tasks(后者反映 Tasks 视图当前筛选+分页,B-29 契约)。
// @项目 enrichment 按项目反查时读 store.tasks 会被当前页筛选污染(只见到筛进来的子集)。
// 改为按 project_id 独立 invoke list_tasks({project_id, limit}) + 本地 Map 缓存,
// 切项目(projectsStore.projects 列表重载/项目删除)清空缓存防陈旧。
const MENTION_TASK_LIMIT = 50
const projectTasksCache = ref<Map<string, TaskRecord[]>>(new Map())
/** 拉取指定项目的任务并缓存(已缓存则跳过);失败静默降级为空数组(不阻塞 enrichment 渲染) */
async function ensureProjectTasks(projectId: string): Promise<void> {
if (projectTasksCache.value.has(projectId)) return
try {
const list = await taskApi.list({ project_id: projectId, limit: MENTION_TASK_LIMIT })
// 写新 Map(响应式触发:Map 重赋值让 computed 重算,mutate 旧 Map 不可靠)
const next = new Map(projectTasksCache.value)
next.set(projectId, list)
projectTasksCache.value = next
} catch (e) {
// 失败占位空数组避免反复重试,下次切项目自然清缓存重拉
const next = new Map(projectTasksCache.value)
next.set(projectId, [])
projectTasksCache.value = next
console.error('[ChatInput] 加载项目任务失败:', e)
}
}
interface ProjectEnrichment {
id: string
name: string
@@ -358,13 +386,14 @@ const projectEnrichments = computed<ProjectEnrichment[]>(() => {
const projectSpans = pendingMentionSpans.value.filter(sp => sp.kind === 'project')
if (projectSpans.length === 0) return []
const projects = (projectsStore.projects || []) as ProjectRecord[]
const tasks = (projectsStore.tasks || []) as TaskRecord[]
// P1-g: 任务从 projectTasksCache 读(按 project_id 独立拉取),
// 不读 store.tasks(后者反映 Tasks 视图筛选+分页,会污染 @项目 enrichment)。
const ideas = (projectsStore.ideas || []) as IdeaRecord[]
const results: ProjectEnrichment[] = []
for (const span of projectSpans) {
const project = projects.find(p => p.id === span.refId)
if (!project) continue
const relTasks = tasks.filter(t => t.project_id === project.id)
const relTasks = (projectTasksCache.value.get(project.id) || []) as TaskRecord[]
// ideas linking to this project via promoted_to (project name matching)
const relIdeas = ideas.filter(i => i.promoted_to === project.name || i.promoted_to === project.id)
const running = relTasks.filter(t => t.status === 'in_progress').slice(0, 20)
@@ -430,7 +459,9 @@ function dismissEnrichment() {
for (const span of projectSpans) {
const label = text.slice(span.start, span.start + span.length)
// 替换标记 + 前后可能的空格为单个空格
const pattern = new RegExp(`\\s*${escapeRegex(label)}\s*`)
// M2: 原尾段 \s* 在模板字符串里是单反斜杠 → 解析成字面 s(零或多个 s 字母),
// 应为 \\s*(双反斜杠 → 正则 \s*),与首段 \s* 对称匹配尾部空白。
const pattern = new RegExp(`\\s*${escapeRegex(label)}\\s*`)
text = text.replace(pattern, ' ').trim()
}
inputText.value = text
@@ -443,6 +474,27 @@ function escapeRegex(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
// P1-g: @项目 mention 选中后懒加载该项目的任务(独立 list_tasks 通道,隔离 store.tasks 筛选)。
// 选中即触发,首次拉取后入缓存,后续同一项目不重复拉。
watch(
() => pendingMentionSpans.value.filter(sp => sp.kind === 'project').map(sp => sp.refId),
(projectIds) => {
for (const pid of projectIds) {
if (pid) void ensureProjectTasks(pid)
}
},
{ flush: 'post' },
)
// P1-g: 切项目清缓存 —— projectsStore.projects 列表重载/增删会让缓存 id 失效(项目删除后
// 缓存条目悬空)。监听 projects 引用变化即清空,下次 @项目 自然重拉最新。
watch(
() => projectsStore.projects,
() => {
if (projectTasksCache.value.size > 0) projectTasksCache.value = new Map()
},
)
const mentionGroupLabel = computed(() => ({
project: t('aiChat.mentionGroupProject'),
task: t('aiChat.mentionGroupTask'),