Files
DevFlow/src/views/Dashboard.vue
T
lxy f736f435bc 优化: 所有剩余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全选)
2026-08-02 13:11:06 +08:00

220 lines
7.5 KiB
Vue

<template>
<div class="dashboard">
<!-- ═══ Header ═══ -->
<header class="dash-header">
<div class="dash-header-left">
<h1 class="dash-title">{{ $t('dashboard.title') }}</h1>
<span class="dash-subtitle">{{ $t('dashboard.subtitle') }}</span>
</div>
<div class="dash-header-right">
<button class="btn btn-ghost btn-sm dash-refresh-btn" @click="refresh" :disabled="loading" :title="$t('dashboard.refresh')">
<svg v-if="loading" class="dash-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 11-6.219-8.56"/></svg>
<svg v-else width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
</button>
<button class="btn btn-primary btn-sm" @click="quickCapture">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
{{ $t('dashboard.captureIdea') }}
</button>
</div>
</header>
<!-- ═══ 错误条:refresh/onMounted 加载失败对用户可见(样式全局 .error-banner) ═══ -->
<div v-if="errorMsg" class="error-banner" style="margin-bottom: 14px">
<span class="error-text">{{ errorMsg }}</span>
<button class="error-dismiss" @click="errorMsg = ''"></button>
</div>
<!-- ═══ Main Grid ═══ -->
<div class="dash-grid">
<!-- Left: Active Projects -->
<ActiveProjectsPanel />
<!-- Right Column -->
<div class="dash-right">
<!-- Ideas -->
<IdeasPanel />
</div>
</div>
<!-- ═══ 统计概要(底部一行小字,原独立统计卡已下沉) ═══ -->
<div class="dash-stats-inline">
<span class="dash-stats-item clickable" @click="go('/ideas')" :title="$t('dashboard.viewAll')">
<span class="dash-stats-num">{{ store.stats.ideas }}</span>{{ $t('dashboard.stats.ideas') }}
</span>
<span class="dash-stats-sep">·</span>
<span class="dash-stats-item clickable" @click="go('/projects')" :title="$t('dashboard.viewAll')">
<span class="dash-stats-num">{{ store.stats.projects }}</span>{{ $t('dashboard.stats.projects') }}
</span>
<span class="dash-stats-sep">·</span>
<span class="dash-stats-item clickable" @click="go('/tasks')" :title="$t('dashboard.viewAll')">
<span class="dash-stats-num">{{ store.stats.activeTasks }}</span>{{ $t('dashboard.stats.activeTasks') }}
</span>
<span class="dash-stats-sep">·</span>
<span class="dash-stats-item">
<span class="dash-stats-num">{{ store.stats.drafts }}</span>{{ $t('dashboard.stats.drafts') }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
// 仪表盘页壳 — 仅保留:Header(refresh/quickCapture) + 错误条 + 布局栅格 + 加载编排(loadAll) +
// 底部统计概要行(原独立统计卡 StatCardRow 已下沉为底部一行小字,空间让给项目/灵感面板)。
// 项目面板/灵感面板已抽至 components/dashboard/ 子组件,各自共享 project store(全局单例),无需 props。
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useProjectStore } from '@/stores/project'
import ActiveProjectsPanel from '@/components/dashboard/ActiveProjectsPanel.vue'
import IdeasPanel from '@/components/dashboard/IdeasPanel.vue'
const router = useRouter()
const store = useProjectStore()
const { t } = useI18n()
// 加载态(refresh/load 期间 true,驱动刷新按钮禁用) + 错误态(加载失败对用户可见)
const loading = ref(false)
const errorMsg = ref('')
// 统一加载入口(projects/ideas 并行 + 统计独立通道),管理 loading + errorMsg
// refresh 与 onMounted 共用,避免两处 catch 逻辑重复
// P1-b A0:不再调 store.loadTasks() —— Dashboard 模板不消费 state.tasks,
// 统计(activeTaskCount/projectTaskCounts)走 loadStats() 独立通道(后端 count_tasks),
// 不被 Tasks 视图分页/筛选污染。Tasks.vue 自行 loadTasks(onMounted/筛选/变更)。
// loadStats 依赖 state.projects(算 projectTaskCounts),故 loadProjects 完成后再调。
async function loadAll() {
if (loading.value) return // 防重复触发
loading.value = true
errorMsg.value = ''
try {
await Promise.all([store.loadProjects(), store.loadIdeas()])
await store.loadStats()
} catch (e: any) {
console.error('Dashboard load failed:', e)
errorMsg.value = e?.toString() ?? t('dashboard.err.loadFailed')
} finally {
loading.value = false
}
}
async function refresh() {
await loadAll()
}
function quickCapture() {
router.push('/ideas')
}
// 底部统计概要项点击跳转(可点击项:ideas/projects/tasks;drafts 不可点)
function go(link: string) {
router.push(link)
}
onMounted(() => {
loadAll()
})
</script>
<style scoped>
.dashboard { padding: 16px 20px 20px; }
/* ═══ Header ═══ */
.dash-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.dash-title {
font-size: 18px;
font-weight: 500;
letter-spacing: -0.4px;
color: var(--df-text);
line-height: 1.2;
}
.dash-subtitle {
font-size: 11px;
color: var(--df-text-dim);
margin-top: 2px;
display: flex;
align-items: center;
gap: 5px;
}
.dash-subtitle::before {
content: '';
width: 5px; height: 5px;
border-radius: 50%;
background: var(--df-success);
display: inline-block;
}
.dash-header-right { display: flex; gap: 6px; align-items: center; }
/* 刷新按钮图标旋转动画(加载中反馈) */
.dash-spinner { animation: dash-spin 0.9s linear infinite; }
@keyframes dash-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* ═══ Grid Layout ═══ */
.dash-grid {
display: grid;
grid-template-columns: 1fr 320px;
gap: 12px;
}
.dash-right { display: flex; flex-direction: column; gap: 12px; }
/* ═══ 底部统计概要(一行小字,原独立统计卡下沉) ═══ */
.dash-stats-inline {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin-top: 14px;
padding-top: 10px;
border-top: 0.5px solid var(--df-border);
font-size: 11px;
color: var(--df-text-dim);
line-height: 1.4;
}
.dash-stats-item {
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: 450;
}
.dash-stats-item.clickable {
cursor: pointer;
transition: color 0.15s var(--df-ease);
}
.dash-stats-item.clickable:hover { color: var(--df-accent); }
.dash-stats-num {
font-size: 13px;
font-weight: 600;
color: var(--df-text);
letter-spacing: -0.3px;
}
.dash-stats-sep { color: var(--df-text-dim); opacity: 0.6; }
/* ═══ Responsive ═══ */
/* 窄窗口:单栏(与 AppLayout 768px 断点对齐) */
@media (max-width: 768px) {
.dashboard { padding: 12px 14px 16px; }
.dash-grid { grid-template-columns: 1fr; }
.dash-right { flex-direction: row; flex-wrap: wrap; gap: 10px; }
.dash-right > .df-panel { flex: 1 1 280px; min-width: 0; }
}
/* 宽窗口:右栏展开 */
@media (min-width: 1201px) {
.dash-grid { grid-template-columns: 1fr 340px; }
}
/* 超宽窗口:充分利用 */
@media (min-width: 1600px) {
.dashboard { padding: 20px 28px 24px; }
.dash-grid { grid-template-columns: 1fr 400px; gap: 16px; }
}
</style>