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>
90 lines
1.7 KiB
Vue
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>
|