diff --git a/docs/superpowers/plans/2026-04-28-energy-module.md b/docs/superpowers/plans/2026-04-28-energy-module.md new file mode 100644 index 0000000..dc9db61 --- /dev/null +++ b/docs/superpowers/plans/2026-04-28-energy-module.md @@ -0,0 +1,1242 @@ +# 能源管理模块实施计划 + +> **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 + +**选哪种?**