持久化(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全选)
220 lines
7.5 KiB
Vue
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>
|