refactor(energy): visual polish + KPI/table self-consistency
- mock: derive ELECTRIC_KPI month/today from APR_DAYS so card and table totals always agree (previously ¥8,437 vs ¥9,151 mismatch) - overview: Top5 bar chart now shows rank badges (1-5) and inline value labels at bar ends — readable without hover - overview: donut "年合计 362.43T" moves into the chart center (previously below as a separate line, defeating the donut hole) - daily: rows with |chainPct| ≥ 30% get a tinted background (green for spikes, red for drops) for at-a-glance abnormal-day spotting Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,11 +120,15 @@ export default function HydrogenDaily() {
|
|||||||
<div className="px-3 py-10 text-center text-slate-400 text-[12px] font-bold">暂无数据</div>
|
<div className="px-3 py-10 text-center text-slate-400 text-[12px] font-bold">暂无数据</div>
|
||||||
) : rows.map(r => {
|
) : rows.map(r => {
|
||||||
const open = expanded.has(r.date);
|
const open = expanded.has(r.date);
|
||||||
|
const isAbnormal = Math.abs(r.chainPct) >= 0.3;
|
||||||
|
const abnormalBg = isAbnormal
|
||||||
|
? r.chainPct > 0 ? 'bg-emerald-50/40' : 'bg-red-50/40'
|
||||||
|
: '';
|
||||||
return (
|
return (
|
||||||
<div key={r.date} className="border-t border-slate-100">
|
<div key={r.date} className={`border-t border-slate-100 ${abnormalBg}`}>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggle(r.date)}
|
onClick={() => toggle(r.date)}
|
||||||
className="w-full grid grid-cols-[1fr_auto_auto] md:grid-cols-[1fr_140px_120px_120px] gap-2 px-3 py-2.5 text-left hover:bg-slate-50 transition-colors"
|
className="w-full grid grid-cols-[1fr_auto_auto] md:grid-cols-[1fr_140px_120px_120px] gap-2 px-3 py-2.5 text-left hover:bg-slate-50/60 transition-colors"
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-1 text-[12px] text-slate-700 font-bold">
|
<span className="flex items-center gap-1 text-[12px] text-slate-700 font-bold">
|
||||||
<ChevronRight size={14} className={`transition-transform ${open ? 'rotate-90' : ''} text-slate-400`} />
|
<ChevronRight size={14} className={`transition-transform ${open ? 'rotate-90' : ''} text-slate-400`} />
|
||||||
|
|||||||
@@ -1,7 +1,28 @@
|
|||||||
import { Fuel, Wallet, Coins, CalendarClock } from 'lucide-react';
|
import { Fuel, Wallet, Coins, CalendarClock } from 'lucide-react';
|
||||||
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, PieChart, Pie, Tooltip } from 'recharts';
|
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, PieChart, Pie, Tooltip, LabelList } from 'recharts';
|
||||||
import { HYDROGEN_KPI, HYDROGEN_STATIONS_TOP5, HYDROGEN_REGION_SHARE } from './mock';
|
import { HYDROGEN_KPI, HYDROGEN_STATIONS_TOP5, HYDROGEN_REGION_SHARE } from './mock';
|
||||||
|
|
||||||
|
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 = [
|
const REGION_COLORS = [
|
||||||
'#3b82f6', '#22d3ee', '#a855f7', '#f59e0b',
|
'#3b82f6', '#22d3ee', '#a855f7', '#f59e0b',
|
||||||
'#10b981', '#ef4444', '#6366f1', '#14b8a6',
|
'#10b981', '#ef4444', '#6366f1', '#14b8a6',
|
||||||
@@ -84,14 +105,14 @@ export default function HydrogenOverview() {
|
|||||||
<span className="text-sm font-bold text-slate-700">加氢站加注量 Top5</span>
|
<span className="text-sm font-bold text-slate-700">加氢站加注量 Top5</span>
|
||||||
<span className="text-[11px] text-slate-400 font-bold">单位 Kg</span>
|
<span className="text-[11px] text-slate-400 font-bold">单位 Kg</span>
|
||||||
</div>
|
</div>
|
||||||
<ResponsiveContainer width="100%" height={240}>
|
<ResponsiveContainer width="100%" height={260}>
|
||||||
<BarChart data={HYDROGEN_STATIONS_TOP5} layout="vertical" margin={{ top: 0, right: 60, bottom: 0, left: 0 }}>
|
<BarChart data={HYDROGEN_STATIONS_TOP5} layout="vertical" margin={{ top: 4, right: 80, bottom: 4, left: 0 }}>
|
||||||
<XAxis type="number" hide />
|
<XAxis type="number" hide />
|
||||||
<YAxis
|
<YAxis
|
||||||
type="category"
|
type="category"
|
||||||
dataKey="name"
|
dataKey="name"
|
||||||
width={150}
|
width={170}
|
||||||
tick={{ fontSize: 11, fill: '#475569' }}
|
tick={<RankYAxisTick />}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
/>
|
/>
|
||||||
@@ -103,6 +124,14 @@ export default function HydrogenOverview() {
|
|||||||
{HYDROGEN_STATIONS_TOP5.map((_, i) => (
|
{HYDROGEN_STATIONS_TOP5.map((_, i) => (
|
||||||
<Cell key={i} fill={`url(#topBarGrad)`} />
|
<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>
|
</Bar>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="topBarGrad" x1="0" x2="1" y1="0" y2="0">
|
<linearGradient id="topBarGrad" x1="0" x2="1" y1="0" y2="0">
|
||||||
@@ -117,23 +146,29 @@ export default function HydrogenOverview() {
|
|||||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-4 flex flex-col gap-2">
|
<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>
|
<span className="text-sm font-bold text-slate-700">各区域加氢占比</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ResponsiveContainer width="50%" height={200}>
|
<div className="relative w-1/2 h-[200px]">
|
||||||
<PieChart>
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<Pie
|
<PieChart>
|
||||||
data={HYDROGEN_REGION_SHARE}
|
<Pie
|
||||||
dataKey="kg"
|
data={HYDROGEN_REGION_SHARE}
|
||||||
nameKey="region"
|
dataKey="kg"
|
||||||
innerRadius={48}
|
nameKey="region"
|
||||||
outerRadius={80}
|
innerRadius={48}
|
||||||
paddingAngle={1}
|
outerRadius={80}
|
||||||
>
|
paddingAngle={1}
|
||||||
{HYDROGEN_REGION_SHARE.map((_, i) => (
|
>
|
||||||
<Cell key={i} fill={REGION_COLORS[i % REGION_COLORS.length]} />
|
{HYDROGEN_REGION_SHARE.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 }} />
|
</Pie>
|
||||||
</PieChart>
|
<Tooltip formatter={(v) => `${(Number(v ?? 0) / 1000).toFixed(2)}T`} contentStyle={{ borderRadius: 12, fontSize: 12 }} />
|
||||||
</ResponsiveContainer>
|
</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">{(HYDROGEN_KPI.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]">
|
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-x-3 gap-y-1 text-[11px]">
|
||||||
{HYDROGEN_REGION_SHARE.map((r, i) => (
|
{HYDROGEN_REGION_SHARE.map((r, i) => (
|
||||||
<div key={r.region} className="flex items-center gap-1.5">
|
<div key={r.region} className="flex items-center gap-1.5">
|
||||||
@@ -144,7 +179,6 @@ export default function HydrogenOverview() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center text-[11px] text-slate-400 font-bold pt-1">年合计 {(HYDROGEN_KPI.yearKg / 1000).toFixed(2)}T</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -72,16 +72,6 @@ export const HYDROGEN_DAILY: HydrogenDailyRow[] = Array.from({ length: 30 }, (_,
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
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]> = [
|
const APR_DAYS: Array<[string, number, number]> = [
|
||||||
['2026-04-26', 510.91, 184.82],
|
['2026-04-26', 510.91, 184.82],
|
||||||
['2026-04-25', 2859.61, 314.20],
|
['2026-04-25', 2859.61, 314.20],
|
||||||
@@ -113,6 +103,22 @@ function buildElectricRows(days: Array<[string, number, number]>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const APR_KWH_SUM = APR_DAYS.reduce((a, [, k]) => a + k, 0);
|
||||||
|
const APR_FEE_SUM = APR_DAYS.reduce((a, [, , f]) => a + f, 0);
|
||||||
|
const [TODAY_DATE, TODAY_KWH, TODAY_FEE] = APR_DAYS[0];
|
||||||
|
const [, PREV_KWH] = APR_DAYS[1];
|
||||||
|
void TODAY_DATE;
|
||||||
|
|
||||||
|
export const ELECTRIC_KPI: ElectricKpi = {
|
||||||
|
totalKwh: 817_632.24,
|
||||||
|
totalFee: 151_542.92,
|
||||||
|
monthKwh: APR_KWH_SUM,
|
||||||
|
monthFee: APR_FEE_SUM,
|
||||||
|
todayKwh: TODAY_KWH,
|
||||||
|
todayFee: TODAY_FEE,
|
||||||
|
todayChainPct: (TODAY_KWH - PREV_KWH) / PREV_KWH,
|
||||||
|
};
|
||||||
|
|
||||||
export const ELECTRIC_MONTHLY: ElectricMonthGroup[] = [
|
export const ELECTRIC_MONTHLY: ElectricMonthGroup[] = [
|
||||||
{
|
{
|
||||||
month: '2026-04',
|
month: '2026-04',
|
||||||
|
|||||||
Reference in New Issue
Block a user