zyc 4a2ed8d414 feat(ui+perf): Editorial Data Console 重设计 + 接口性能 + ROI 权限锁
UI 重设计 (Editorial Data Console 风):
- 设计令牌系统: OKLCH 色彩 + Newsreader/Geist/JetBrains Mono 字体 + exp easing
- 全局表格基线 (.n-data-table 统一 editorial 风 + .table-shell 卡片容器)
- DataCard / Naive UI 主题对齐新 token (深墨青主色 + 暖琥珀强调)
- RoiDashboard: 3 KPI 卡片同字号 + chip 多色筛选 + section editorial 节奏
- ProjectRoiBoard: hero 卡 highlight + ytd-strip 节奏化 (10/13/15px 三层字号)
- ProjectList: 自适应卡片 + 产品线 NSelect 筛选 + 拆出独立"类型"列 + 文本链接操作
- RevenuePieChart 重设计: donut + 中心总额 + 底部水平图例 (替代外部 callout 截断)
- 全部页面 width:100% + clamp() 流体 padding,断点驱动 auto-fit 网格
- AppSidebar 项目子菜单按产品线分组 + 可折叠 + localStorage 持久化

接口性能优化 (N+1 → 批量 + Map 索引):
- /api/overview: 8.5s → 0.5s (17×) - 消除 3 处循环 SQL 查询
- /api/okr:     11.3s → 0.3s (37×) - getOKRByPeriod 一次性 inArray 批量
- ROI 三处时间窗 (aggregate/timeseries/events) launchedAt 截断对齐

ROI 权限锁:
- 全部 ROI 端点统一 admin (roiRoutes 全局 requireRole)
- 路由 /roi + /projects/:id/roi meta.roles=['admin']
- 侧边栏 ROI 入口 + 项目详情打标按钮/分类标签全部 v-if isAdmin

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:28:48 +08:00

90 lines
1.7 KiB
Vue

<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import AppSidebar from './AppSidebar.vue';
import AppHeader from './AppHeader.vue';
import { useDashboardStore } from '@/stores/dashboard';
const dashStore = useDashboardStore();
onMounted(() => {
dashStore.initResize();
});
onUnmounted(() => {
dashStore.destroyResize();
});
</script>
<template>
<div class="app-layout">
<!-- Mobile overlay backdrop -->
<div
v-if="dashStore.isMobile && dashStore.mobileSidebarOpen"
class="sidebar-overlay"
@click="dashStore.closeMobileSidebar"
/>
<AppSidebar />
<div
class="main-container"
:class="{
collapsed: !dashStore.isMobile && dashStore.sidebarCollapsed,
mobile: dashStore.isMobile,
}"
>
<AppHeader />
<main class="main-content">
<router-view />
</main>
</div>
</div>
</template>
<style scoped>
.app-layout {
display: flex;
min-height: 100vh;
}
.main-container {
flex: 1;
margin-left: var(--sidebar-width);
transition: margin-left var(--duration-collapse) var(--ease-default);
display: flex;
flex-direction: column;
min-width: 0;
overflow-x: hidden;
}
.main-container.collapsed {
margin-left: var(--sidebar-collapsed-width);
}
/* Mobile: main content takes full width, no margin for sidebar */
.main-container.mobile {
margin-left: 0;
}
.main-content {
flex: 1;
padding: var(--space-6);
overflow-y: auto;
background: var(--color-bg);
}
/* Overlay backdrop for mobile sidebar */
.sidebar-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: calc(var(--z-sticky) + 1);
}
@media (max-width: 768px) {
.main-content {
padding: var(--space-4);
}
}
</style>