Files
ln-bi/src/modules/energy/HydrogenOverview.tsx
kkfluous c3b43837fb fix(energy): update hint text to match real 1m cache TTL
The "每 5 分钟更新" copy was inherited from the BI dashboard mock and
no longer matches reality — server cache is 60s TTL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:02:41 +08:00

207 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { Fuel, Wallet, Coins, CalendarClock } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, PieChart, Pie, Tooltip, LabelList } from 'recharts';
import { fetchHydrogenOverview, type HydrogenOverviewResponse } from './api';
interface YAxisTickProps {
x?: number;
y?: number;
index?: number;
payload?: { value: string };
}
function RankYAxisTick({ x = 0, y = 0, index = 0, payload }: YAxisTickProps) {
return (
<g transform={`translate(${x},${y})`}>
<circle cx={-158} cy={0} r={9} fill="#3b82f6" />
<text x={-158} y={3} textAnchor="middle" fontSize={10} fontWeight={700} fill="#fff">
{index + 1}
</text>
<text x={-144} y={4} textAnchor="start" fontSize={11} fill="#475569">
{payload?.value}
</text>
</g>
);
}
const REGION_COLORS = [
'#3b82f6', '#22d3ee', '#a855f7', '#f59e0b',
'#10b981', '#ef4444', '#6366f1', '#14b8a6',
'#94a3b8',
];
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 [data, setData] = useState<HydrogenOverviewResponse | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
fetchHydrogenOverview()
.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 top5 = data.top5;
const regions = data.regions;
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 1
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{/* 卡 1年加氢量 */}
<div className="bg-gradient-to-br from-cyan-50 to-blue-50 rounded-2xl border border-slate-100 shadow-sm p-4 flex flex-col gap-2">
<div className="flex items-center justify-between text-[11px] text-slate-500">
<span className="flex items-center gap-1 font-bold"><Fuel size={12} className="text-cyan-600" /></span>
</div>
<div className="text-2xl lg:text-3xl font-bold text-slate-800 leading-tight">{fmtKg(k.yearKg)}</div>
<div className="text-[11px] text-slate-500 font-bold space-y-0.5">
<div> <span className="text-slate-700">{fmtKg(k.ourYearKg)}</span></div>
<div> <span className="text-slate-700">{fmtKg(k.customerYearKg)}</span></div>
</div>
</div>
{/* 卡 2年加氢费 */}
<div className="bg-gradient-to-br from-blue-50 to-violet-50 rounded-2xl border border-slate-100 shadow-sm p-4 flex flex-col gap-2">
<div className="flex items-center justify-between text-[11px] text-slate-500">
<span className="flex items-center gap-1 font-bold"><Wallet size={12} className="text-blue-600" /></span>
</div>
<div className="text-2xl lg:text-3xl font-bold text-slate-800 leading-tight">{fmtYuanWan(k.yearFee)}</div>
<div className="text-[11px] text-slate-500 font-bold">
<div> <span className="text-slate-700">{fmtYuanWan(k.ourYearFee)}</span></div>
</div>
</div>
{/* 卡 3累计羚牛承担 */}
<div className="bg-gradient-to-br from-amber-50 to-orange-50 rounded-2xl border border-slate-100 shadow-sm p-4 flex flex-col gap-2">
<div className="flex items-center justify-between text-[11px] text-slate-500">
<span className="flex items-center gap-1 font-bold"><Coins size={12} className="text-amber-600" /></span>
</div>
<div className="text-2xl lg:text-3xl font-bold text-slate-800 leading-tight">{fmtYuanWan(k.lingniuBornFee)}</div>
<div className="text-[11px] text-slate-500 font-bold space-y-0.5">
<div> <span className="text-slate-700">{fmtKg(k.lingniuBornKg)}</span></div>
</div>
</div>
{/* 卡 4本月 / 今日 */}
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-4 flex flex-col gap-2">
<div className="flex items-center justify-between text-[11px] text-slate-500">
<span className="flex items-center gap-1 font-bold"><CalendarClock size={12} className="text-slate-500" /> / </span>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<div className="text-[10px] text-slate-400 font-bold"></div>
<div className="text-base md:text-lg font-bold text-slate-800">{fmtKg(k.monthKg)}</div>
<div className="text-[11px] text-slate-500 font-bold">{fmtYuanWan(k.monthFee)}</div>
</div>
<div>
<div className="text-[10px] text-slate-400 font-bold"></div>
<div className="text-base md:text-lg font-bold text-slate-800">{fmtKg(k.todayKg)}</div>
<div className="text-[11px] text-slate-500 font-bold">{fmtYuan(k.todayFee)}</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Top5 加氢站 */}
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-bold text-slate-700"> Top5</span>
<span className="text-[11px] text-slate-400 font-bold"> Kg</span>
</div>
<ResponsiveContainer width="100%" height={260}>
<BarChart data={top5} layout="vertical" margin={{ top: 4, right: 80, bottom: 4, left: 0 }}>
<XAxis type="number" hide />
<YAxis
type="category"
dataKey="name"
width={170}
tick={<RankYAxisTick />}
tickLine={false}
axisLine={false}
/>
<Tooltip
formatter={(v) => `${Number(v ?? 0).toLocaleString('zh-CN')} Kg`}
contentStyle={{ borderRadius: 12, fontSize: 12 }}
/>
<Bar dataKey="kg" radius={[6, 6, 6, 6]}>
{top5.map((_, i) => (
<Cell key={i} fill={`url(#topBarGrad)`} />
))}
<LabelList
dataKey="kg"
position="right"
formatter={(v) => `${Number(v ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 0 })}`}
fill="#475569"
fontSize={11}
fontWeight={700}
/>
</Bar>
<defs>
<linearGradient id="topBarGrad" x1="0" x2="1" y1="0" y2="0">
<stop offset="0%" stopColor="#3b82f6" />
<stop offset="100%" stopColor="#22d3ee" />
</linearGradient>
</defs>
</BarChart>
</ResponsiveContainer>
</div>
{/* 区域占比环 */}
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-4 flex flex-col gap-2">
<span className="text-sm font-bold text-slate-700"></span>
<div className="flex items-center gap-2">
<div className="relative w-1/2 h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={regions}
dataKey="kg"
nameKey="region"
innerRadius={48}
outerRadius={80}
paddingAngle={1}
>
{regions.map((_, i) => (
<Cell key={i} fill={REGION_COLORS[i % REGION_COLORS.length]} />
))}
</Pie>
<Tooltip formatter={(v) => `${(Number(v ?? 0) / 1000).toFixed(2)}T`} contentStyle={{ borderRadius: 12, fontSize: 12 }} />
</PieChart>
</ResponsiveContainer>
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<div className="text-[10px] text-slate-400 font-bold"></div>
<div className="text-base font-bold text-slate-700 leading-tight">{(k.yearKg / 1000).toFixed(2)}T</div>
</div>
</div>
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-x-3 gap-y-1 text-[11px]">
{regions.map((r, i) => (
<div key={r.region} className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full" style={{ background: REGION_COLORS[i % REGION_COLORS.length] }} />
<span className="text-slate-600">{r.region}</span>
<span className="text-slate-400 ml-auto font-bold">{(r.share * 100).toFixed(1)}%</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}