# 能源管理模块实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 在底部导航新增「能源管理」入口,含氢能(总览 + 每日明细)与电能(每日明细)两个 Tab,全前端 mock 数据,移动 / 桌面双端响应。 **Architecture:** React 19 + Tailwind 4 模块,沿用 `src/modules/mileage/` 的目录骨架与 sub-nav 风格;图表用 recharts;状态全在组件内 `useState`;后端 0 改动;UI 双端响应通过 Tailwind `md:` / `lg:` 断点完成。 **Tech Stack:** React 19, TypeScript, Tailwind 4, recharts (已有), lucide-react (已有), motion (已有)。**无单元测试框架**——本项目历史模块均无测试,验证靠 `npm run lint` 通过 + chrome-devtools MCP 浏览器视觉确认。 **Spec:** `docs/superpowers/specs/2026-04-28-energy-module-design.md` --- ## File Structure | 文件 | 责任 | 任务 | |------|------|------| | `src/modules/energy/types.ts` | 共享类型定义 | T1 | | `src/modules/energy/mock.ts` | 所有 mock 数据 | T1 | | `src/modules/energy/TrendBadge.tsx` | 环比 pill 组件,全模块复用 | T2 | | `src/modules/energy/EnergyModule.tsx` | 顶级容器 + 氢能/电能 切换 | T2 | | `src/modules/energy/HydrogenView.tsx` | 氢能 Tab 内 总览/每日 切换 | T3 | | `src/modules/energy/HydrogenOverview.tsx` | KPI 卡 + Top5 横柱 + 区域占比环 | T3, T4 | | `src/modules/energy/HydrogenDaily.tsx` | 加氢量明细表 + 站点级下钻 | T5 | | `src/modules/energy/ElectricView.tsx` | mini KPI 头 + 月份分组充电表 | T6 | | `src/App.tsx` (modify) | 在 BASE_MODULES 注册 energy | T2 | | `src/components/Shell.tsx` (modify) | PATH_MAP 加 `/energy` | T2 | --- ## Task 1: 类型与 mock 数据 **Files:** - Create: `src/modules/energy/types.ts` - Create: `src/modules/energy/mock.ts` - [ ] **Step 1.1: 创建类型文件** 写入 `src/modules/energy/types.ts`: ```ts export type CustomerType = 'external' | 'lingniu'; export type DateQuickPick = 'today' | 'thisWeek' | 'thisMonth' | 'thisQuarter' | 'last7' | 'last30'; export interface HydrogenKpi { yearKg: number; yearFee: number; ourYearKg: number; ourYearFee: number; customerYearKg: number; monthKg: number; monthFee: number; todayKg: number; todayFee: number; lingniuBornKg: number; lingniuBornFee: number; } export interface HydrogenStationTop { rank: number; name: string; kg: number; fee: number; share: number; } export interface HydrogenRegionShare { region: string; kg: number; share: number; } export interface HydrogenStationRow { name: string; pricePerKg: number; kg: number; chainPct: number; } export interface HydrogenDailyRow { date: string; totalKg: number; chainPct: number; customerType: CustomerType; stations: HydrogenStationRow[]; } export interface ElectricKpi { totalKwh: number; totalFee: number; monthKwh: number; monthFee: number; todayKwh: number; todayFee: number; todayChainPct: number; } export interface ElectricDailyRow { date: string; kwh: number; fee: number; chainPct: number; } export interface ElectricMonthGroup { month: string; kwh: number; fee: number; rows: ElectricDailyRow[]; } ``` - [ ] **Step 1.2: 创建 mock 数据** 写入 `src/modules/energy/mock.ts`: ```ts import type { HydrogenKpi, HydrogenStationTop, HydrogenRegionShare, HydrogenDailyRow, ElectricKpi, ElectricMonthGroup, } from './types'; export const HYDROGEN_KPI: HydrogenKpi = { yearKg: 362_430, yearFee: 10_664_600, ourYearKg: 245_960, ourYearFee: 6_955_200, customerYearKg: 116_470, monthKg: 85_410, monthFee: 2_612_300, todayKg: 0, todayFee: 0, lingniuBornKg: 302_620, lingniuBornFee: 100_300, }; export const HYDROGEN_STATIONS_TOP5: HydrogenStationTop[] = [ { rank: 1, name: '佛山豪汇石油加氢站', kg: 78_421.30, fee: 2_744_745, share: 0.216 }, { rank: 2, name: '嘉兴嘉燃经开站', kg: 65_028.80, fee: 2_275_988, share: 0.179 }, { rank: 3, name: '广州新锋交通联新加氢站', kg: 54_882.50, fee: 2_195_300, share: 0.151 }, { rank: 4, name: '北京京辉加氢站', kg: 43_127.40, fee: 1_596_714, share: 0.119 }, { rank: 5, name: '新疆乌鲁木齐加氢站', kg: 38_601.20, fee: 1_351_042, share: 0.106 }, ]; export const HYDROGEN_REGION_SHARE: HydrogenRegionShare[] = [ { region: '广东', kg: 148_400, share: 0.409 }, { region: '浙江', kg: 72_500, share: 0.200 }, { region: '北京', kg: 43_500, share: 0.120 }, { region: '新疆', kg: 39_000, share: 0.108 }, { region: '上海', kg: 21_800, share: 0.060 }, { region: '四川', kg: 16_300, share: 0.045 }, { region: '河北', kg: 10_900, share: 0.030 }, { region: '山东', kg: 7_300, share: 0.020 }, { region: '其他', kg: 2_730, share: 0.008 }, ]; const HD_STATION_NAMES = [ { name: '佛山豪汇石油加氢站', pricePerKg: 35 }, { name: '嘉兴嘉燃经开站', pricePerKg: 35 }, { name: '广州新锋交通联新加氢站', pricePerKg: 40 }, { name: '北京京辉加氢站', pricePerKg: 38 }, { name: '新疆乌鲁木齐加氢站', pricePerKg: 35 }, ]; function makeStations(seed: number): HydrogenDailyRow['stations'] { const count = 2 + (seed % 3); return HD_STATION_NAMES.slice(0, count).map((s, i) => ({ name: s.name, pricePerKg: s.pricePerKg, kg: Math.round(((seed * 13 + i * 17) % 1500 + 80) * 100) / 100, chainPct: ((seed * 7 + i * 11) % 200 - 100) / 100 / 2, })); } export const HYDROGEN_DAILY: HydrogenDailyRow[] = Array.from({ length: 30 }, (_, i) => { const day = 28 - i; const month = day > 0 ? 4 : 3; const realDay = day > 0 ? day : day + 31; const date = `2026-${String(month).padStart(2, '0')}-${String(realDay).padStart(2, '0')}`; const stations = makeStations(i + 1); const totalKg = stations.reduce((a, b) => a + b.kg, 0); const chainPct = ((i * 23) % 100 - 50) / 100; return { date, totalKg: Math.round(totalKg * 100) / 100, chainPct, customerType: i % 3 === 0 ? 'lingniu' : 'external', stations, }; }); export const ELECTRIC_KPI: ElectricKpi = { totalKwh: 817_632.24, totalFee: 151_542.92, monthKwh: 42_318.56, monthFee: 8_437.12, todayKwh: 510.91, todayFee: 184.82, todayChainPct: -0.821, }; const APR_DAYS: Array<[string, number, number]> = [ ['2026-04-26', 510.91, 184.82], ['2026-04-25', 2859.61, 314.20], ['2026-04-24', 802.64, 437.83], ['2026-04-23', 2520.22, 495.05], ['2026-04-22', 2234.23, 653.73], ['2026-04-21', 3520.86, 510.06], ['2026-04-20', 527.65, 295.05], ['2026-04-19', 3151.97, 593.55], ['2026-04-18', 1616.38, 183.84], ['2026-04-17', 1069.09, 597.73], ['2026-04-16', 2186.34, 396.63], ['2026-04-15', 2568.16, 572.27], ['2026-04-14', 2315.38, 489.82], ['2026-04-13', 2274.88, 423.15], ['2026-04-12', 2742.85, 248.52], ['2026-04-11', 599.67, 299.13], ['2026-04-10', 2576.59, 806.44], ['2026-04-09', 2627.30, 814.80], ['2026-04-08', 2058.35, 573.11], ['2026-04-07', 2739.61, 261.56], ]; function buildElectricRows(days: Array<[string, number, number]>) { return days.map(([date, kwh, fee], i) => { const prev = days[i + 1]?.[1]; const chainPct = prev ? (kwh - prev) / prev : 0; return { date, kwh, fee, chainPct }; }); } export const ELECTRIC_MONTHLY: ElectricMonthGroup[] = [ { month: '2026-04', kwh: APR_DAYS.reduce((a, [, k]) => a + k, 0), fee: APR_DAYS.reduce((a, [, , f]) => a + f, 0), rows: buildElectricRows(APR_DAYS), }, ]; ``` - [ ] **Step 1.3: 类型检查通过** ```bash npm run lint ``` 期望:无错误退出。 - [ ] **Step 1.4: 提交** ```bash git add src/modules/energy/types.ts src/modules/energy/mock.ts git commit -m "feat(energy): add types and mock data for new module Co-Authored-By: Claude Opus 4.7 (1M context) " ``` --- ## Task 2: 模块容器 + 注册到 App **Files:** - Create: `src/modules/energy/TrendBadge.tsx` - Create: `src/modules/energy/EnergyModule.tsx` - Modify: `src/App.tsx` - Modify: `src/components/Shell.tsx` - [ ] **Step 2.1: 共用 TrendBadge 组件** 写入 `src/modules/energy/TrendBadge.tsx`: ```tsx import { ArrowUp, ArrowDown, Minus } from 'lucide-react'; export interface TrendBadgeProps { value: number; // -1..+1, 0 表示持平 className?: string; } export default function TrendBadge({ value, className = '' }: TrendBadgeProps) { const isUp = value > 0.0001; const isDown = value < -0.0001; const cls = isUp ? 'bg-emerald-50 text-emerald-600' : isDown ? 'bg-red-50 text-red-600' : 'bg-slate-100 text-slate-500'; const Icon = isUp ? ArrowUp : isDown ? ArrowDown : Minus; const sign = isUp ? '+' : ''; return ( {sign}{(value * 100).toFixed(2)}% ); } ``` - [ ] **Step 2.2: EnergyModule 顶层容器** 写入 `src/modules/energy/EnergyModule.tsx`: ```tsx import { useState } from 'react'; import { Fuel, BatteryCharging } from 'lucide-react'; import { motion } from 'motion/react'; import HydrogenView from './HydrogenView'; import ElectricView from './ElectricView'; type TopTab = 'hydrogen' | 'electric'; export default function EnergyModule() { const [activeTab, setActiveTab] = useState('hydrogen'); return (
{activeTab === 'hydrogen' ? : }
); } ``` - [ ] **Step 2.3: 占位的 HydrogenView / ElectricView** 写入 `src/modules/energy/HydrogenView.tsx`: ```tsx export default function HydrogenView() { return (
氢能视图占位 — 将在 Task 3-5 实现
); } ``` 写入 `src/modules/energy/ElectricView.tsx`: ```tsx export default function ElectricView() { return (
电能视图占位 — 将在 Task 6 实现
); } ``` - [ ] **Step 2.4: 注册到 App.tsx** 修改 `src/App.tsx`:在 import 区加 `Zap`、`EnergyModule`,并把 `BASE_MODULES` 改为: ```tsx import { Truck, Route, Activity, Zap } from 'lucide-react'; import EnergyModule from './modules/energy/EnergyModule'; // ... 其他 import 不变 ... const BASE_MODULES: ModuleConfig[] = [ { id: 'assets', label: '资产管理', icon: Truck, component: AssetsModule }, { id: 'mileage', label: '里程管理', icon: Route, component: MileageModule }, { id: 'energy', label: '能源管理', icon: Zap, component: EnergyModule }, ]; ``` - [ ] **Step 2.5: 注册 path 到 Shell.tsx** 修改 `src/components/Shell.tsx` 的 `PATH_MAP`,加一条 `'/energy': 'energy'`: ```tsx const PATH_MAP: Record = { '/vehicle': 'assets', '/assets': 'assets', '/mileage': 'mileage', '/scheduling': 'scheduling', '/energy': 'energy', }; ``` - [ ] **Step 2.6: 类型检查 + 视觉验证** ```bash npm run lint ``` 期望:无错误。 ```bash npm run dev ``` 后台启动后用 chrome-devtools MCP 打开 `http://localhost:3000/#energy`: - 移动尺寸(375×812)和桌面尺寸(1440×900)下都应能看到底部 nav(移动)/ 左侧 nav(桌面)多了「能源管理」图标 - 点进去顶部应有「氢能 / 电能」sub-nav,下划线滑块在切换时移动 - 默认显示氢能视图占位文字 - [ ] **Step 2.7: 提交** ```bash git add src/modules/energy/TrendBadge.tsx src/modules/energy/EnergyModule.tsx \ src/modules/energy/HydrogenView.tsx src/modules/energy/ElectricView.tsx \ src/App.tsx src/components/Shell.tsx git commit -m "feat(energy): add module shell, register in nav Co-Authored-By: Claude Opus 4.7 (1M context) " ``` --- ## Task 3: 氢能 sub-nav + 总览 KPI 卡 **Files:** - Modify: `src/modules/energy/HydrogenView.tsx` - Create: `src/modules/energy/HydrogenOverview.tsx` - [ ] **Step 3.1: HydrogenView 加二级 Tab** 覆盖 `src/modules/energy/HydrogenView.tsx`: ```tsx import { useState } from 'react'; import { LayoutDashboard, CalendarDays } from 'lucide-react'; import { motion } from 'motion/react'; import HydrogenOverview from './HydrogenOverview'; import HydrogenDaily from './HydrogenDaily'; type SubTab = 'overview' | 'daily'; export default function HydrogenView() { const [sub, setSub] = useState('overview'); return ( <>
{sub === 'overview' ? : } ); } ``` 注:因为 Task 5 才创建 `HydrogenDaily`,本步骤需要同时建一个临时占位文件以让 TS 编译通过。 - [ ] **Step 3.2: HydrogenDaily 临时占位** 写入 `src/modules/energy/HydrogenDaily.tsx`: ```tsx export default function HydrogenDaily() { return (
每日氢能视图占位 — 将在 Task 5 实现
); } ``` - [ ] **Step 3.3: HydrogenOverview KPI 4 卡** 写入 `src/modules/energy/HydrogenOverview.tsx`: ```tsx import { Fuel, Wallet, Coins, CalendarClock } from 'lucide-react'; import { HYDROGEN_KPI } from './mock'; function fmtKg(kg: number) { if (kg >= 1000) return `${(kg / 1000).toFixed(2)}T`; return `${kg.toFixed(2)}Kg`; } function fmtYuanWan(yuan: number) { return `¥${(yuan / 10_000).toFixed(2)}万`; } function fmtYuan(yuan: number) { return `¥${yuan.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`; } export default function HydrogenOverview() { const k = HYDROGEN_KPI; return (
数据自 2025-01-01 起,每 5 分钟更新
{/* 卡 1:年加氢量 */}
年加氢量
{fmtKg(k.yearKg)}
我方 {fmtKg(k.ourYearKg)}
客户产生 {fmtKg(k.customerYearKg)}
{/* 卡 2:年加氢费 */}
年加氢费
{fmtYuanWan(k.yearFee)}
我方 {fmtYuanWan(k.ourYearFee)}
{/* 卡 3:累计羚牛承担 */}
累计羚牛承担
{fmtYuanWan(k.lingniuBornFee)}
{fmtKg(k.lingniuBornKg)}
{/* 卡 4:本月 / 今日 */}
本月 / 今日
本月
{fmtKg(k.monthKg)}
{fmtYuanWan(k.monthFee)}
今日
{fmtKg(k.todayKg)}
{fmtYuan(k.todayFee)}
); } ``` - [ ] **Step 3.4: 类型检查 + 视觉验证** ```bash npm run lint ``` 用 chrome-devtools 打开 `#energy`: - 顶部「氢能 / 电能」下面应有第二行「总览 / 每日」sub-nav - 4 张 KPI 卡:移动 2×2,桌面 1×4 - 卡 1:青蓝渐变,主数 `362.43T`,下面两行分解 - 卡 2:蓝紫渐变,主数 `¥1066.46万` - 卡 3:蜜橙渐变,主数 `¥10.03万` - 卡 4:白底,左本月右今日两栏 - 切到「每日」显示占位文字 - [ ] **Step 3.5: 提交** ```bash git add src/modules/energy/HydrogenView.tsx src/modules/energy/HydrogenDaily.tsx \ src/modules/energy/HydrogenOverview.tsx git commit -m "feat(energy): hydrogen overview KPI cards (4-card grid) Co-Authored-By: Claude Opus 4.7 (1M context) " ``` --- ## Task 4: 氢能总览 — Top5 横柱 + 区域占比环 **Files:** - Modify: `src/modules/energy/HydrogenOverview.tsx` - [ ] **Step 4.1: 增加图表区** 在 `HydrogenOverview.tsx` 顶部追加 import: ```tsx import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, PieChart, Pie, Tooltip } from 'recharts'; import { HYDROGEN_KPI, HYDROGEN_STATIONS_TOP5, HYDROGEN_REGION_SHARE } from './mock'; ``` 并在 KPI 网格 `` 之后、组件最外层 `` 之前,插入: ```tsx
{/* Top5 加氢站 */}
加氢站加注量 Top5 单位 Kg
`${v.toLocaleString('zh-CN')} Kg`} contentStyle={{ borderRadius: 12, fontSize: 12 }} /> {HYDROGEN_STATIONS_TOP5.map((_, i) => ( ))}
{/* 区域占比环 */}
各区域加氢占比
{HYDROGEN_REGION_SHARE.map((_, i) => ( ))} `${(v / 1000).toFixed(2)}T`} contentStyle={{ borderRadius: 12, fontSize: 12 }} />
{HYDROGEN_REGION_SHARE.map((r, i) => (
{r.region} {(r.share * 100).toFixed(1)}%
))}
年合计 {(HYDROGEN_KPI.yearKg / 1000).toFixed(2)}T
``` 并在文件顶部(import 之下、export default 之上)加颜色常量: ```tsx const REGION_COLORS = [ '#3b82f6', '#22d3ee', '#a855f7', '#f59e0b', '#10b981', '#ef4444', '#6366f1', '#14b8a6', '#94a3b8', ]; ``` - [ ] **Step 4.2: 类型检查 + 视觉验证** ```bash npm run lint ``` 打开 `#energy` → 氢能 → 总览: - KPI 区下方应有两块图: - 移动尺寸:上下叠(柱图在上、环图在下) - 桌面尺寸:左右并排 - Top5 横柱站名完整(不被截断),柱条蓝→青渐变 - 环形图旁有图例,鼠标 hover 柱/扇区时 Tooltip 出现 - 环图下方居中显示「年合计 362.43T」 - [ ] **Step 4.3: 提交** ```bash git add src/modules/energy/HydrogenOverview.tsx git commit -m "feat(energy): hydrogen overview Top5 bar + region donut Co-Authored-By: Claude Opus 4.7 (1M context) " ``` --- ## Task 5: 氢能每日明细表 + 站点级下钻 **Files:** - Modify: `src/modules/energy/HydrogenDaily.tsx` - [ ] **Step 5.1: 完整重写 HydrogenDaily** 覆盖 `src/modules/energy/HydrogenDaily.tsx`: ```tsx import { useMemo, useState } from 'react'; import { ChevronRight } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import TrendBadge from './TrendBadge'; import { HYDROGEN_DAILY } from './mock'; import type { CustomerType, DateQuickPick, HydrogenDailyRow } from './types'; const TODAY = new Date('2026-04-28'); const QUICK_PICK_OPTIONS: Array<{ id: DateQuickPick; label: string }> = [ { id: 'today', label: '当天' }, { id: 'thisWeek', label: '本周' }, { id: 'thisMonth', label: '本月' }, { id: 'thisQuarter', label: '本季度' }, { id: 'last7', label: '最近7天' }, { id: 'last30', label: '最近30天' }, ]; function isInPick(date: string, pick: DateQuickPick): boolean { const d = new Date(date); switch (pick) { case 'today': { return d.toISOString().slice(0, 10) === TODAY.toISOString().slice(0, 10); } case 'thisWeek': { const day = TODAY.getDay() || 7; const start = new Date(TODAY); start.setDate(TODAY.getDate() - day + 1); return d >= start && d <= TODAY; } case 'thisMonth': return d.getFullYear() === TODAY.getFullYear() && d.getMonth() === TODAY.getMonth(); case 'thisQuarter': { const q = Math.floor(TODAY.getMonth() / 3); const dq = Math.floor(d.getMonth() / 3); return d.getFullYear() === TODAY.getFullYear() && dq === q; } case 'last7': { const c = new Date(TODAY); c.setDate(TODAY.getDate() - 6); return d >= c && d <= TODAY; } case 'last30': { const c = new Date(TODAY); c.setDate(TODAY.getDate() - 29); return d >= c && d <= TODAY; } } } export default function HydrogenDaily() { const [pick, setPick] = useState('last30'); const [customer, setCustomer] = useState('external'); const [expanded, setExpanded] = useState>(new Set()); const rows = useMemo(() => { return HYDROGEN_DAILY .filter(r => r.customerType === customer) .filter(r => isInPick(r.date, pick)) .sort((a, b) => b.date.localeCompare(a.date)); }, [pick, customer]); const totalKg = rows.reduce((a, r) => a + r.totalKg, 0); const toggle = (date: string) => setExpanded(prev => { const next = new Set(prev); next.has(date) ? next.delete(date) : next.add(date); return next; }); return (
{/* 日期速选 */}
{QUICK_PICK_OPTIONS.map(opt => ( ))}
{/* 客户类型 segmented */}
{(['external', 'lingniu'] as const).map(c => ( ))}
{/* 表格 */}
{/* 表头 */}
日期 / 加氢站带价格 单价 加氢量(Kg) 环比
{/* 合计行 */}
合计 {totalKg.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}
{/* 主行 + 子行 */} {rows.length === 0 ? (
暂无数据
) : rows.map(r => { const open = expanded.has(r.date); return (
{open && ( {r.stations.map(s => (
{s.name} · {s.pricePerKg} 元/Kg {s.pricePerKg} 元/Kg {s.kg.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}
))}
)}
); })}
); } ``` - [ ] **Step 5.2: 类型检查 + 视觉验证** ```bash npm run lint ``` 用 chrome-devtools 打开 `#energy` → 氢能 → 每日: - 顶部:6 个日期速选 pill 横排(窄屏可横滚),默认「最近30天」选中 - 第二行:外部 / 羚牛 双段开关,默认「外部」 - 表头:日期 / (md+ 显示单价) / 加氢量 / 环比 - 合计行:蓝底,显示总量 - 主行:可点击展开 → 子行内缩进、显示站名/单价/量/环比 - 切「羚牛」后行数变化、合计跟随 - 切「当天」后大概率显示「暂无数据」(mock 中 4-28 不一定有 lingniu 数据);切「最近7天」应有数据 - 环比 pill:上绿、下红、持平灰 - [ ] **Step 5.3: 提交** ```bash git add src/modules/energy/HydrogenDaily.tsx git commit -m "feat(energy): hydrogen daily table with station drilldown Co-Authored-By: Claude Opus 4.7 (1M context) " ``` --- ## Task 6: 电能视图(mini KPI + 月份分组表) **Files:** - Modify: `src/modules/energy/ElectricView.tsx` - [ ] **Step 6.1: 完整重写 ElectricView** 覆盖 `src/modules/energy/ElectricView.tsx`: ```tsx import { useMemo, useState } from 'react'; import { ChevronRight, Wallet, BatteryCharging, CalendarClock } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import TrendBadge from './TrendBadge'; import { ELECTRIC_KPI, ELECTRIC_MONTHLY } from './mock'; import type { CustomerType } from './types'; function fmtYuan(yuan: number) { return `¥${yuan.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`; } function fmtKwh(kwh: number) { return `${kwh.toLocaleString('zh-CN', { maximumFractionDigits: 2 })} 度`; } export default function ElectricView() { const [customer, setCustomer] = useState('external'); const [openMonths, setOpenMonths] = useState>(new Set([ELECTRIC_MONTHLY[0]?.month])); const months = useMemo(() => { // mock 暂不区分客户类型,customer 切换不影响数据;保留 UI 切换以与 BI 一致 void customer; return ELECTRIC_MONTHLY; }, [customer]); const k = ELECTRIC_KPI; const toggleMonth = (m: string) => setOpenMonths(prev => { const next = new Set(prev); next.has(m) ? next.delete(m) : next.add(m); return next; }); return (
龙王路停车场充电站,期初 2025-01-01,手工导入每日更新
{/* 横向 mini KPI 头 */}
累计
{fmtYuan(k.totalFee)}
{fmtKwh(k.totalKwh)}
本月
{fmtYuan(k.monthFee)}
{fmtKwh(k.monthKwh)}
今日
{fmtYuan(k.todayFee)}
{fmtKwh(k.todayKwh)}
{/* 客户类型 */}
{(['external', 'lingniu'] as const).map(c => ( ))}
{/* 月份分组表 */}
月份 / 日期 充电电量 充电费用(元) 环比
{months.map(m => { const open = openMonths.has(m.month); return (
{open && ( {m.rows.map(d => (
{d.date} {d.kwh.toLocaleString('zh-CN', { maximumFractionDigits: 2 })} {d.fee.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}
))}
)}
); })}
); } ``` - [ ] **Step 6.2: 类型检查 + 视觉验证** ```bash npm run lint ``` 打开 `#energy` → 电能: - 顶部说明条 → 3 张 mini KPI 卡(累计 / 本月 / 今日) - 今日卡右侧带环比 pill - 客户类型双段开关 - 表头:月份/日期 + 充电电量 + 充电费用(md+ 还有环比列) - 默认展开 `2026-04`,能看到 2026-04-26 等若干日级行 - 点月份行能折叠收起 - [ ] **Step 6.3: 提交** ```bash git add src/modules/energy/ElectricView.tsx git commit -m "feat(energy): electric view with mini KPI + month grouping Co-Authored-By: Claude Opus 4.7 (1M context) " ``` --- ## Task 7: 双端响应 / 横屏 / 终验 **Files:** (不写代码,只校验) - [ ] **Step 7.1: 移动尺寸完整走查** 启动开发服务器(若未启动): ```bash npm run dev ``` 用 chrome-devtools `resize_page width=375 height=812`,依次访问并截图保存: - `#energy` → 氢能 → 总览:4 KPI 卡 2×2,下方 Top5 在上、环图在下 - `#energy` → 氢能 → 每日:日期 pill 可横滚,行可展开 - `#energy` → 电能:3 mini KPI 横排不堆叠 ```bash # 在 chrome-devtools 中 take_screenshot fullPage=true filePath=/tmp/energy-mobile-overview.png take_screenshot fullPage=true filePath=/tmp/energy-mobile-daily.png take_screenshot fullPage=true filePath=/tmp/energy-mobile-electric.png ``` 确认底部 nav 在所有页面都不遮挡内容(`pb-16` 起作用)。 - [ ] **Step 7.2: 桌面尺寸完整走查** `resize_page width=1440 height=900`,访问同样三页,确认: - 总览 KPI 1×4 横排 - Top5 + 环图 左右并排 - 表格出现 `单价` 列 / 环比列 ```bash take_screenshot fullPage=true filePath=/tmp/energy-desktop-overview.png take_screenshot fullPage=true filePath=/tmp/energy-desktop-daily.png take_screenshot fullPage=true filePath=/tmp/energy-desktop-electric.png ``` - [ ] **Step 7.3: 横屏尺寸** `resize_page width=812 height=375`(landscape 触发)。确认: - `landscape:overflow-hidden` 起作用,没有底部 nav 重复占位 - 内容可正常滚动 - [ ] **Step 7.4: 完整 lint** ```bash npm run lint ``` 期望:无错误。 - [ ] **Step 7.5: 终结提交(如有调整)** 如果 7.1-7.3 中发现需要调整 className,修复后: ```bash git add -p # 选择性 stage git commit -m "fix(energy): responsive polish from full-screen review Co-Authored-By: Claude Opus 4.7 (1M context) " ``` 如果完全无需调整,跳过此步。 --- ## Self-Review (作者已做) **Spec coverage:** | Spec 要点 | 实现位置 | |----------|---------| | BASE_MODULES 注册 + Zap icon | T2 Step 2.4 | | Shell PATH_MAP 加 /energy | T2 Step 2.5 | | 顶部「氢能/电能」sub-nav + motion 滑块 | T2 Step 2.2 | | 氢能内层「总览/每日」sub-tab | T3 Step 3.1 | | 氢能总览 4 KPI 卡(含渐变色 / icon) | T3 Step 3.3 | | Top5 横柱(蓝→青渐变) | T4 Step 4.1 | | 各区域占比环 + 中心年合计 + 双列图例 | T4 Step 4.1 | | 氢能每日:6 速选 + 客户类型 + 合计行 | T5 Step 5.1 | | 氢能日期行可下钻到站点级(站名+单价 元/Kg) | T5 Step 5.1 | | 环比 pill(绿/红/灰,复用) | T2 Step 2.1 + 全模块 import | | 电能 3 mini KPI 横排 | T6 Step 6.1 | | 电能月份分组 + 日级展开 | T6 Step 6.1 | | 移动 2×2 / 桌面 1×4 KPI 网格 | T3 + 验收在 T7 | | Top5 / 环图 移动叠 / 桌面并排 | T4 + 验收在 T7 | | 横屏 landscape:overflow-hidden 保留 | T2 容器层 + T7.3 验收 | | 验收时 npm run lint 通过 | 每个 task | | 不引入新依赖 | 已确认(recharts/lucide-react/motion 全已存在) | **Placeholder scan:** 无 TBD/TODO;每步都有完整代码或具体校验命令。 **Type consistency:** - `HydrogenDailyRow.customerType` 在 mock 与 HydrogenDaily filter 中均使用 `external`/`lingniu` - `TrendBadge` 接口定义在 T2,全模块复用,无重名 - `HYDROGEN_*` mock 常量名(全大写下划线)一致 **已添加任务:** spec 提到的「icon 选型 Zap / lucide 大小 size={20}」由 T2 Step 2.4 满足(Zap import + 现有 Shell 已 size 20)。 --- **Plan complete and saved to `docs/superpowers/plans/2026-04-28-energy-module.md`. Two execution options:** **1. Subagent-Driven (recommended)** — 我每个 task 派一个干净的 subagent,task 间我做 review,节奏快、上下文不污染 **2. Inline Execution** — 当前会话里串起来跑,期间设几个 checkpoint 给你 review **选哪种?**