devperf/frontend/src/components/roi/RevenuePieChart.vue
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

116 lines
3.0 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue';
import { useECharts, CHART_COLORS } from '@/composables/useECharts';
const props = defineProps<{
byCategory: Record<string, { totalRevenue: number }>;
}>();
const CATEGORY_LABELS: Record<string, string> = {
cash_cow: '现金牛',
efficiency_tool: '效能工具',
moat: '资本护城河',
composite: '复合型',
uncategorized: '未打标',
};
function fmtCurrency(n: number): string {
if (n >= 10000) return `¥${(n / 10000).toFixed(1)}`;
return `¥${Math.round(n).toLocaleString()}`;
}
const total = computed(() =>
Object.values(props.byCategory).reduce((s, v) => s + (v.totalRevenue > 0 ? v.totalRevenue : 0), 0)
);
const option = computed(() => {
const data = Object.entries(props.byCategory)
.filter(([, v]) => v.totalRevenue > 0)
.map(([k, v]) => ({ name: CATEGORY_LABELS[k] || k, value: Math.round(v.totalRevenue) }));
return {
color: CHART_COLORS,
textStyle: { fontFamily: "'Geist', 'PingFang SC', sans-serif", color: '#4d5258' },
tooltip: {
trigger: 'item',
backgroundColor: '#ffffff',
borderColor: '#dfe2e6',
borderWidth: 1,
textStyle: { color: '#2d3033', fontSize: 12 },
extraCssText: 'box-shadow: 0 8px 24px rgba(34,40,42,0.06); border-radius: 10px; padding: 8px 12px;',
formatter: (params: any) => `
<div style="font-weight:600;margin-bottom:4px">${params.name}</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:13px">¥${params.value.toLocaleString()} <span style="color:#7a8085">(${params.percent}%)</span></div>
`,
},
legend: {
orient: 'horizontal',
bottom: 0,
left: 'center',
itemGap: 18,
itemWidth: 8,
itemHeight: 8,
icon: 'circle',
textStyle: { fontSize: 12, color: '#4d5258' },
},
graphic: [
{
type: 'text',
left: 'center',
top: '38%',
style: {
text: '总产出',
fill: '#7a8085',
fontSize: 11,
fontFamily: "'Geist', 'PingFang SC', sans-serif",
fontWeight: 500,
},
},
{
type: 'text',
left: 'center',
top: '46%',
style: {
text: fmtCurrency(total.value),
fill: '#2d3033',
fontSize: 22,
fontWeight: 600,
fontFamily: "'JetBrains Mono', monospace",
},
},
],
series: [{
type: 'pie',
radius: ['58%', '78%'],
center: ['50%', '45%'],
avoidLabelOverlap: true,
itemStyle: {
borderColor: '#ffffff',
borderWidth: 2,
},
label: { show: false },
labelLine: { show: false },
emphasis: {
scale: true,
scaleSize: 6,
itemStyle: {
shadowBlur: 12,
shadowColor: 'rgba(0,0,0,0.10)',
},
},
data,
}],
};
});
const { chartRef } = useECharts(option);
</script>
<template>
<div ref="chartRef" class="revenue-pie"></div>
</template>
<style scoped>
.revenue-pie { width: 100%; height: 320px; }
</style>