# 渠道统计 — 前端盈亏展示 ## 📋 实施计划:渠道统计页面新增成本/盈亏展示 ### 任务类型 - [x] 前端 - [ ] 后端 - [ ] 全栈 ### 需求概述 后端 `/admin/channels/:id/stats` 接口已新增以下字段: **Overview 新增**: | 字段 | 类型 | 说明 | |------|------|------| | `total_cost_cents` | number | 总成本(分) | | `total_profit_cents` | number | 盈亏(分) = paid - cost | | `total_cost` | number | 总成本(元) | | `total_profit` | number | 盈亏(元) | **趋势图每日新增**: | 字段 | 类型 | 说明 | |------|------|------| | `cost_cents` | number | 当日成本(分) | | `profit_cents` | number | 当日盈亏(分) | ### 技术方案 #### UI 设计 **Overview 区域**: - 现有 3 个卡片(用户、订单、实付金额)→ 扩展为 **5 个卡片** - 新增:**总成本** 卡片 + **盈亏** 卡片 - 布局:从 `grid-cols-3` 改为 `grid-cols-5`(或在移动端自适应 `grid-cols-2 md:grid-cols-5`) - 盈亏卡片需根据正/负值显示不同颜色(盈利=绿色,亏损=红色) **趋势图区域**: - 现有 2 个 Tab(用户增长、付费数据)→ 新增第 3 个 Tab:**盈亏分析** - 盈亏分析 Tab 包含 3 条曲线:实付金额、成本、盈亏 - 盈亏曲线可使用虚线区分 ### 实施步骤 #### Step 1: 更新 TypeScript 类型定义 **文件**:`web/admin/src/api/channels.ts` 在 `StatsOverview` 接口新增: ```typescript export interface StatsOverview { total_users: number total_orders: number total_gmv: number total_paid_cents?: number // 新增 total_cost_cents?: number // 总成本(分) total_profit_cents?: number // 盈亏(分) total_cost?: number // 总成本(元) total_profit?: number // 盈亏(元) } ``` 在 `StatsDailyItem` 接口新增: ```typescript export interface StatsDailyItem { date: string user_count: number order_count: number gmv: number paid_cents?: number // 新增 cost_cents?: number // 当日成本(分) profit_cents?: number // 当日盈亏(分) } ``` #### Step 2: 更新 Overview 卡片区域 **文件**:`web/admin/src/views/operations/channels/index.vue` **2.1** 布局从 `grid-cols-3` 改为 `grid-cols-5` **2.2** 新增两个 `ArtStatsCard`: ```vue ``` **2.3** 新增 computed 属性: ```typescript const totalCostYuan = computed(() => { const cents = statsData.value.overview.total_cost_cents if (typeof cents === 'number') { return Number((cents / 100).toFixed(2)) } return 0 }) const totalProfitYuan = computed(() => { const cents = statsData.value.overview.total_profit_cents if (typeof cents === 'number') { return Number((cents / 100).toFixed(2)) } return 0 }) const profitCardStyle = computed(() => totalProfitYuan.value >= 0 ? 'bg-green-50' : 'bg-red-50' ) const profitTextColor = computed(() => totalProfitYuan.value >= 0 ? '#10B981' : '#EF4444' ) const profitIconStyle = computed(() => totalProfitYuan.value >= 0 ? 'bg-green-500' : 'bg-red-500' ) const profitDescription = computed(() => totalProfitYuan.value >= 0 ? '盈利' : '亏损' ) ``` #### Step 3: 更新趋势图 Tab **文件**:`web/admin/src/views/operations/channels/index.vue` **3.1** 在 `el-radio-group` 新增 Tab: ```vue 用户增长 付费数据 盈亏分析 ``` **3.2** 在 `chartData` computed 中新增 `profit` 分支: ```typescript const chartData = computed(() => { if (statsTab.value === 'growth') { return [ { name: '新增用户', data: statsData.value.daily.map(i => i.user_count), smooth: true, color: '#409EFF' } ] } else if (statsTab.value === 'revenue') { return [ { name: '订单数', data: statsData.value.daily.map(i => i.order_count), smooth: true, color: '#67C23A' }, { name: '实付金额', data: statsData.value.daily.map(i => getDailyPaidYuan(i)), smooth: true, color: '#E6A23C' } ] } else { // profit tab return [ { name: '实付(元)', data: statsData.value.daily.map(i => getDailyPaidYuan(i)), smooth: true, color: '#E6A23C' }, { name: '成本(元)', data: statsData.value.daily.map(i => getDailyCostYuan(i)), smooth: true, color: '#7C3AED' }, { name: '盈亏(元)', data: statsData.value.daily.map(i => getDailyProfitYuan(i)), smooth: true, color: '#10B981' } ] } }) ``` **3.3** 新增辅助函数: ```typescript function getDailyCostYuan(item: { cost_cents?: number }) { if (typeof item.cost_cents === 'number') { return Number((item.cost_cents / 100).toFixed(2)) } return 0 } function getDailyProfitYuan(item: { profit_cents?: number }) { if (typeof item.profit_cents === 'number') { return Number((item.profit_cents / 100).toFixed(2)) } return 0 } ``` #### Step 4: 更新 statsData 初始值 **文件**:`web/admin/src/views/operations/channels/index.vue` ```typescript const statsData = ref({ overview: { total_users: 0, total_orders: 0, total_gmv: 0, total_paid_cents: 0, total_cost_cents: 0, total_profit_cents: 0, total_cost: 0, total_profit: 0 }, daily: [] }) ``` ### 关键文件 | 文件 | 操作 | 说明 | |------|------|------| | `web/admin/src/api/channels.ts:L68-86` | 修改 | 扩展 StatsOverview 和 StatsDailyItem 接口 | | `web/admin/src/views/operations/channels/index.vue:L155-184` | 修改 | Overview 卡片区域新增成本/盈亏卡 | | `web/admin/src/views/operations/channels/index.vue:L202-205` | 修改 | 趋势图新增盈亏分析 Tab | | `web/admin/src/views/operations/channels/index.vue:L482-558` | 修改 | 新增 computed 属性和辅助函数 | ### 风险与缓解 | 风险 | 严重程度 | 缓解措施 | |------|---------|----------| | 5列卡片在窄屏溢出 | 低 | 使用响应式 `grid-cols-2 md:grid-cols-5`,必要时改为 `grid-cols-3` + 第二行 `grid-cols-2` | | 后端字段为空(旧数据) | 已解决 | 所有新字段使用 `?` 可选,computed 中做 `typeof` 检查,默认 0 | | ArtStatsCard 不支持负数展示 | 低 | ArtCountTo 组件底层支持负数(基于 countUp.js),无需额外处理 | | 盈亏曲线可能有负值 | 低 | ECharts 原生支持负值 Y 轴,图表会自动适配 | ### 验收标准 - [ ] TypeScript 类型定义包含新字段 - [ ] Overview 展示 5 个卡片(用户、订单、实付、成本、盈亏) - [ ] 盈亏卡片根据正/负值动态切换颜色(绿/红) - [ ] 趋势图新增"盈亏分析"Tab - [ ] 盈亏分析 Tab 展示 3 条曲线(实付、成本、盈亏) - [ ] `pnpm build` 编译通过 - [ ] `pnpm type-check` 类型检查通过 ### SESSION_ID(供 /ccg:execute 使用) - CODEX_SESSION: N/A - GEMINI_SESSION: N/A