优化: 所有剩余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
+64 -2
View File
@@ -24,18 +24,80 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAiStore } from '@/stores/ai'
import { useAppSettingsStore } from '@/stores/appSettings'
import AppSidebar from './AppSidebar.vue'
defineProps<{
const props = defineProps<{
isDetached: boolean
}>()
const aiStore = useAiStore()
const appSettings = useAppSettingsStore()
// ── 7a:路由路径+查询启动恢复(仅主窗口)──
// 持久化到 localStorage(轻量、同步、读早于 appSettings.loadAll 完成,避免闪烁)。
// 各列表页(Tasks/Ideas/...)的筛选恢复由各自 agent 读写各自 localStorage key;
// 此处只负责「上次所在页面 + query」级别的恢复(如 /tasks?status=done)。
// 分离窗口(ai-detached/file-explorer-detached/approval-popup)不参与:它们有专属
// 入口路由,且会被 watch 误覆盖主窗口记录。
const ROUTE_STORAGE_KEY = 'df-last-route'
// 这些路径不持久化(过渡/分离/弹窗/根跳转):避免分离窗口覆盖、避免 '/' redirect 噪音
const NON_PERSIST_PATHS = new Set(['/', '/ai-detached', '/file-explorer-detached', '/approval-popup'])
const route = useRoute()
const router = useRouter()
/** 写入当前 path + query 到 localStorage(忽略 NON_PERSIST 路径) */
function persistRoute(path: string, query: Record<string, string>) {
if (props.isDetached || NON_PERSIST_PATHS.has(path)) return
try {
localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify({ path, query }))
} catch {
/* 配额溢出/隐私模式,静默 */
}
}
onMounted(() => {
// 分离窗口不恢复(它们的入口路由由打开方指定)
if (props.isDetached) return
// 首屏若落在根 redirect('/') 或 ai-home,尝试恢复上次路由
try {
const raw = localStorage.getItem(ROUTE_STORAGE_KEY)
if (!raw) return
const saved = JSON.parse(raw) as { path?: string; query?: Record<string, string> }
const targetPath = saved.path
if (!targetPath || NON_PERSIST_PATHS.has(targetPath)) return
// 仅在当前确实处于根/首页时恢复(避免覆盖用户深链接或刷新某具体页)
if (route.path !== '/' && route.path !== '/ai-home') {
// 用户已在某具体页:仅补持久化当前页,不抢断
persistRoute(route.path, { ...route.query } as Record<string, string>)
return
}
// 恢复:仅当目标路由存在(catch-all 会 redirect,这里 router.replace 自身已容错)
const targetQuery = saved.query || {}
// 防自循环:目标与当前位置完全一致则不 replace
if (targetPath === route.path && Object.keys(targetQuery).length === 0) return
router.replace({ path: targetPath, query: targetQuery })
} catch {
/* 解析失败:数据损坏,忽略(下次 watch 会覆写正确值) */
}
})
// watch 路由变化:持久化最新 path + query(主窗口)
watch(
() => route.path,
(path) => persistRoute(path, { ...route.query } as Record<string, string>),
)
// query 变化(同 path 不同 query,如筛选)也要持久化
watch(
() => route.query,
(q) => persistRoute(route.path, { ...(q as Record<string, string>) }),
{ deep: true },
)
// ── AI 面板拖拽调宽 ──
// useSetting 监听 appSettings 缓存:loadAll 异步填充后会自动刷新 ref
const panelWidth = appSettings.useSetting<number>('df-ai-width', 500)
+34 -39
View File
@@ -1,6 +1,6 @@
<template>
<aside class="df-sidebar" :class="{ 'df-sidebar--collapsed': collapsed }">
<!-- Logo -->
<!-- Logo(7b:brand 区只保留品牌标识,收缩按钮已移到底部操作区) -->
<div class="sidebar-brand">
<div class="brand-mark">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
@@ -12,16 +12,8 @@
</div>
<span v-if="!collapsed" class="brand-text">DevFlow</span>
<span v-if="!collapsed" class="brand-version">v0.1</span>
<button v-if="!collapsed" class="collapse-btn" @click="toggleCollapsed" :title="$t('nav.collapse')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
</button>
</div>
<!-- 收缩态展开按钮(底部固定位置,仅在 collapsed 时显示) -->
<button v-if="collapsed" class="expand-btn" @click="toggleCollapsed" :title="$t('nav.expand')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</button>
<!-- Navigation -->
<nav class="sidebar-nav">
<div class="nav-group">
@@ -63,6 +55,16 @@
<span class="status-dot"></span>
<span class="status-text">{{ $t('nav.systemReady') }}</span>
</div>
<!-- 7b:收缩/展开切换按钮(统一置于底部操作区,与 AI 面板/设置同列)。
collapsed 时显展开箭头(→),展开时显收起箭头(←),快捷键 Ctrl+B 同效。 -->
<button class="nav-link collapse-toggle-btn" @click="toggleCollapsed" :title="collapsed ? $t('nav.expand') : $t('nav.collapse')">
<span class="nav-link-icon">
<svg v-if="!collapsed" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</span>
<span v-if="!collapsed" class="nav-link-text">{{ $t('nav.collapse') }}</span>
<span v-if="!collapsed" class="ai-toggle-shortcut">Ctrl+B</span>
</button>
<!-- AI 面板切换按钮 -->
<button class="nav-link ai-toggle-btn" :class="{ 'ai-toggle-btn--active': aiStore.state.panelOpen }" @click="aiStore.togglePanel()" :title="collapsed ? $t('nav.aiPanel') : undefined">
<span class="nav-link-icon ai-spark-icon">
@@ -111,16 +113,28 @@ function checkWindowWidth() {
isNarrowWindow.value = window.innerWidth < COLLAPSE_THRESHOLD
}
// 7b:Ctrl+B / Cmd+B 切换侧边栏收缩(与 AI 面板 Ctrl+I 同款全局快捷键风格)
// 仅响应 ctrl 或 meta 任一修饰键(Cmd+I 同款),忽略纯 'b'(避免拦截正常输入)
function handleCollapseKeydown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && (e.key === 'b' || e.key === 'B')) {
e.preventDefault()
toggleCollapsed()
}
}
onMounted(async () => {
// 恢复用户偏好(默认展开)
userCollapsed.value = appSettings.get('df-sidebar-collapsed', false)
// 检查当前窗口宽度
checkWindowWidth()
window.addEventListener('resize', checkWindowWidth)
// Ctrl+B 切换快捷键
window.addEventListener('keydown', handleCollapseKeydown)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', checkWindowWidth)
window.removeEventListener('keydown', handleCollapseKeydown)
})
// 知识库候选数量(侧栏 badge)— 解包 computed 取值保持响应式
@@ -199,39 +213,20 @@ const secondaryNav = [
border-radius: var(--df-radius-xs);
}
/* 收缩按钮(展开态 brand 区右侧) */
.collapse-btn {
margin-left: auto;
display: flex;
align-items: center;
justify-content: center;
width: 22px; height: 22px;
border: none;
border-radius: var(--df-radius-sm);
background: transparent;
color: var(--df-text-dim);
/* 7b:收缩/展开切换按钮(底部操作区,复用 nav-link 样式 + button 重置)
与 .ai-toggle-btn 同款 button-over-nav-link 模式:cursor/border/background/font 重置 */
.collapse-toggle-btn {
cursor: pointer;
transition: all 0.15s var(--df-ease);
}
.collapse-btn:hover {
background: var(--df-sidebar-hover);
color: var(--df-text);
}
/* 展开按钮(收缩态 brand 区下方) */
.expand-btn {
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
width: 100%;
padding: 6px 0;
border: none;
background: transparent;
color: var(--df-text-dim);
cursor: pointer;
transition: all 0.15s var(--df-ease);
font: inherit;
text-align: left;
appearance: none;
-webkit-appearance: none;
color: var(--df-text-secondary);
}
.expand-btn:hover {
.collapse-toggle-btn:hover {
background: var(--df-sidebar-hover);
color: var(--df-text);
}