All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- ElectricView 改为受控组件接收 sub prop(与 HydrogenView 对齐) - EnergyModule sticky 头部统一显示 sub-tabs:氢能、电能都给 每日 / 总览 ETC 仍不显示子 tab(建设中页) - 共享 sub state 抽 helper:activeTab 切换时自动用对应的 sub - 龙王路停车场充电站信息条移入 ElectricOverview 顶部(同氢能"数据自...") 进入电能默认显示「每日」(与氢能一致),切换「总览」看 KPI + 柱图 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
106 lines
4.7 KiB
TypeScript
106 lines
4.7 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { Wallet, CalendarClock } from 'lucide-react';
|
||
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, Tooltip } from 'recharts';
|
||
import { fetchElectricOverview, type ElectricOverviewResponse } from './api';
|
||
import RotatingFooterHint from '../../components/RotatingFooterHint';
|
||
|
||
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 ElectricOverview() {
|
||
const [data, setData] = useState<ElectricOverviewResponse | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
fetchElectricOverview()
|
||
.then(d => { if (!cancelled) setData(d); })
|
||
.catch(e => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); });
|
||
return () => { cancelled = true; };
|
||
}, []);
|
||
|
||
if (error) {
|
||
return <div className="bg-red-50 text-red-600 rounded-2xl border border-red-100 p-4 text-sm">加载失败:{error}</div>;
|
||
}
|
||
if (!data) {
|
||
return <div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-6 text-center text-slate-400 text-sm">加载中…</div>;
|
||
}
|
||
const k = data.kpi;
|
||
const trendData = data.trend;
|
||
// 当电能数据滞后(本月无数据走 fallback)时,柱图标题显示实际月份
|
||
const trendMonthLabel = trendData[0]?.date.slice(0, 7);
|
||
const currentMonth = new Date().toISOString().slice(0, 7);
|
||
const chartTitle = trendMonthLabel && trendMonthLabel !== currentMonth
|
||
? `${trendMonthLabel} 每日充电`
|
||
: '本月每日充电';
|
||
|
||
return (
|
||
<div className="flex flex-col gap-3">
|
||
<div className="bg-white rounded-xl border border-slate-100 px-3 py-1.5 text-[11px] text-slate-400">
|
||
龙王路停车场充电站,期初 2025-01-01,手工导入每日更新
|
||
</div>
|
||
{/* 横向 mini KPI 头 */}
|
||
<div className="grid grid-cols-2 gap-2 md:gap-3">
|
||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-3 md:p-4">
|
||
<div className="flex items-center gap-1 text-[11px] text-slate-500 font-bold mb-1">
|
||
<Wallet size={11} className="text-blue-600" />累计
|
||
</div>
|
||
<div className="text-base md:text-2xl font-bold text-slate-800">{fmtYuan(k.totalFee)}</div>
|
||
<div className="text-[11px] text-slate-500 font-bold mt-0.5">{fmtKwh(k.totalKwh)}</div>
|
||
</div>
|
||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-3 md:p-4">
|
||
<div className="flex items-center gap-1 text-[11px] text-slate-500 font-bold mb-1">
|
||
<CalendarClock size={11} className="text-blue-600" />本月
|
||
</div>
|
||
<div className="text-base md:text-2xl font-bold text-slate-800">{fmtYuan(k.monthFee)}</div>
|
||
<div className="text-[11px] text-slate-500 font-bold mt-0.5">{fmtKwh(k.monthKwh)}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 本月每日充电柱图 */}
|
||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-4">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="text-sm font-bold text-slate-700">{chartTitle}</span>
|
||
<span className="text-[11px] text-slate-400 font-bold">单位 元</span>
|
||
</div>
|
||
<ResponsiveContainer width="100%" height={160}>
|
||
<BarChart data={trendData} margin={{ top: 8, right: 4, bottom: 0, left: 0 }}>
|
||
<XAxis
|
||
dataKey="date"
|
||
tickFormatter={(v: string) => v.slice(5)}
|
||
tick={{ fontSize: 10, fill: '#94a3b8' }}
|
||
tickLine={false}
|
||
axisLine={false}
|
||
interval="preserveStartEnd"
|
||
minTickGap={8}
|
||
/>
|
||
<YAxis hide />
|
||
<Tooltip
|
||
formatter={(v) => [`¥${Number(v ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`, '充电费用']}
|
||
labelFormatter={(d) => `日期 ${d}`}
|
||
contentStyle={{ borderRadius: 12, fontSize: 12 }}
|
||
cursor={{ fill: 'rgba(59, 130, 246, 0.06)' }}
|
||
/>
|
||
<Bar dataKey="fee" radius={[4, 4, 0, 0]}>
|
||
{trendData.map((_, i) => (
|
||
<Cell key={i} fill="url(#electricBarGrad)" />
|
||
))}
|
||
</Bar>
|
||
<defs>
|
||
<linearGradient id="electricBarGrad" x1="0" x2="0" y1="0" y2="1">
|
||
<stop offset="0%" stopColor="#3b82f6" />
|
||
<stop offset="100%" stopColor="#22d3ee" />
|
||
</linearGradient>
|
||
</defs>
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
<RotatingFooterHint />
|
||
</div>
|
||
);
|
||
}
|