diff --git a/src/modules/energy/HydrogenOverview.tsx b/src/modules/energy/HydrogenOverview.tsx
index 8791ead..d2104af 100644
--- a/src/modules/energy/HydrogenOverview.tsx
+++ b/src/modules/energy/HydrogenOverview.tsx
@@ -1,8 +1,17 @@
import { useEffect, useState } from 'react';
-import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, PieChart, Pie, Tooltip, LabelList } from 'recharts';
+import {
+ BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, PieChart, Pie, Tooltip, LabelList,
+} from 'recharts';
+import { Fuel, Wallet, CalendarDays, Sparkles } from 'lucide-react';
import { fetchHydrogenOverview, type HydrogenOverviewResponse } from './api';
import RotatingFooterHint from '../../components/RotatingFooterHint';
+const REGION_COLORS = [
+ '#3b82f6', '#22d3ee', '#a855f7', '#f59e0b',
+ '#10b981', '#ef4444', '#6366f1', '#14b8a6',
+ '#94a3b8',
+];
+
interface YAxisTickProps {
x?: number;
y?: number;
@@ -24,13 +33,55 @@ function RankYAxisTick({ x = 0, y = 0, index = 0, payload }: YAxisTickProps) {
);
}
-const REGION_COLORS = [
- '#3b82f6', '#22d3ee', '#a855f7', '#f59e0b',
- '#10b981', '#ef4444', '#6366f1', '#14b8a6',
- '#94a3b8',
-];
+// ---------- 数字格式化 ----------
+function fmtKg(kg: number): { value: string; unit: string } {
+ if (kg >= 1000) return { value: (kg / 1000).toFixed(2), unit: 'T' };
+ return { value: kg.toFixed(2), unit: 'Kg' };
+}
+function fmtYuan(yuan: number): { value: string; unit: string } {
+ if (yuan >= 100_000_000) return { value: (yuan / 100_000_000).toFixed(2), unit: '亿元' };
+ if (yuan >= 10_000) {
+ const w = yuan / 10_000;
+ return { value: w.toLocaleString('zh-CN', { maximumFractionDigits: 2 }), unit: '万元' };
+ }
+ return { value: yuan.toLocaleString('zh-CN', { maximumFractionDigits: 0 }), unit: '元' };
+}
+// ---------- KPI 卡 ----------
+interface KpiCardProps {
+ icon: React.ReactNode;
+ label: string;
+ hero: { value: string; unit: string };
+ rows: { label: string; value: string }[];
+ accentClass: string;
+ iconBg: string;
+}
+function KpiCard({ icon, label, hero, rows, accentClass, iconBg }: KpiCardProps) {
+ return (
+
+
+
+ {hero.value}
+ {hero.unit}
+
+
+ {rows.map((r, i) => (
+
+ {r.label}
+ {r.value}
+
+ ))}
+
+
+ );
+}
+// ============================================================
export default function HydrogenOverview() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
@@ -52,14 +103,120 @@ export default function HydrogenOverview() {
const k = data.kpi;
const top5 = data.top5;
const regions = data.regions;
+ const monthly = data.monthly;
+
+ const yearKgFmt = fmtKg(k.yearKg);
+ const yearFeeFmt = fmtYuan(k.yearFee);
+ const ourYearKgFmt = fmtKg(k.ourYearKg);
+ const customerYearKgFmt = fmtKg(k.customerYearKg);
+ const monthKgFmt = fmtKg(k.monthKg);
+ const monthFeeFmt = fmtYuan(k.monthFee);
+ const todayKgFmt = fmtKg(k.todayKg);
+ const todayFeeFmt = fmtYuan(k.todayFee);
+ const customerYearFee = Math.max(0, k.yearFee - k.ourYearFee);
+ const customerYearFeeFmt = fmtYuan(customerYearFee);
+ const todayYear = new Date().getFullYear();
+
return (
-
- 数据自 2025-01-01 起,每 1 分钟更新
+ {/* 顶部说明条 */}
+
+ 数据自 2025-01-01 起 · 每分钟刷新
+ {todayYear} 年累计口径
+
+ {/* KPI 4 卡 */}
+
+ }
+ iconBg="bg-cyan-50"
+ accentClass="text-slate-800"
+ label="累计加氢量"
+ hero={yearKgFmt}
+ rows={[
+ { label: '我司', value: `${ourYearKgFmt.value} ${ourYearKgFmt.unit}` },
+ { label: '客户', value: `${customerYearKgFmt.value} ${customerYearKgFmt.unit}` },
+ ]}
+ />
+ }
+ iconBg="bg-blue-50"
+ accentClass="text-slate-800"
+ label="累计加氢费"
+ hero={{ value: `¥${yearFeeFmt.value}`, unit: yearFeeFmt.unit }}
+ rows={[
+ { label: '我司承担', value: `¥${fmtYuan(k.ourYearFee).value} ${fmtYuan(k.ourYearFee).unit}` },
+ { label: '客户承担', value: `¥${customerYearFeeFmt.value} ${customerYearFeeFmt.unit}` },
+ ]}
+ />
+ }
+ iconBg="bg-amber-50"
+ accentClass="text-amber-600"
+ label="本月加氢"
+ hero={monthKgFmt}
+ rows={[
+ { label: '加氢费', value: `¥${monthFeeFmt.value} ${monthFeeFmt.unit}` },
+ { label: '占年比', value: `${k.yearKg > 0 ? (k.monthKg / k.yearKg * 100).toFixed(1) : '0.0'}%` },
+ ]}
+ />
+ }
+ iconBg="bg-violet-50"
+ accentClass="text-violet-600"
+ label="本日加氢"
+ hero={todayKgFmt}
+ rows={[
+ { label: '加氢费', value: `¥${todayFeeFmt.value} ${todayFeeFmt.unit}` },
+ { label: '占月比', value: `${k.monthKg > 0 ? (k.todayKg / k.monthKg * 100).toFixed(1) : '0.0'}%` },
+ ]}
+ />
+
+
+ {/* 月度趋势:年内每月 */}
+ {monthly.length > 0 && (
+
+
+ {todayYear} 年月度加氢量
+ 单位 Kg
+
+
+
+ v.slice(5).replace(/^0/, '') + '月'}
+ tick={{ fontSize: 10, fill: '#94a3b8' }}
+ tickLine={false}
+ axisLine={false}
+ interval={0}
+ />
+
+ [`${Number(v ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 0 })} Kg`, '加氢量']}
+ labelFormatter={(d) => `${d}`}
+ contentStyle={{ borderRadius: 12, fontSize: 12 }}
+ cursor={{ fill: 'rgba(34, 211, 238, 0.06)' }}
+ />
+
+ {monthly.map((_, i) => (
+ |
+ ))}
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Top5 + 区域占比 */}
{/* Top5 加氢站 */}
-
+
加氢站加注量 Top5
单位 Kg
@@ -81,7 +238,7 @@ export default function HydrogenOverview() {
/>
{top5.map((_, i) => (
- |
+ |
))}
- {/* 区域占比环 */}
-
+
+ {/* 区域占比 */}
+
各区域加氢占比
@@ -131,15 +289,16 @@ export default function HydrogenOverview() {
{regions.map((r, i) => (
-
- {r.region}
- {(r.share * 100).toFixed(1)}%
+
+ {r.region}
+ {(r.share * 100).toFixed(1)}%
))}
+
);
@@ -148,13 +307,41 @@ export default function HydrogenOverview() {
function HydrogenOverviewSkeleton() {
return (
- {/* 顶部说明条 */}
+ {/* 4 卡占位 */}
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+ {/* 月度柱图占位 */}
+
+
+
+ {[60, 75, 50, 80, 35, 90, 45].map((h, i) => (
+
+ ))}
+
+
+
- {/* Top5 占位 */}
@@ -171,8 +358,6 @@ function HydrogenOverviewSkeleton() {
))}
-
- {/* 区域占比环 占位 */}
diff --git a/src/modules/energy/api.ts b/src/modules/energy/api.ts
index 2ee72ea..dc14282 100644
--- a/src/modules/energy/api.ts
+++ b/src/modules/energy/api.ts
@@ -1,6 +1,6 @@
import { fetchJson } from '../../auth/api-client';
import type {
- HydrogenKpi, HydrogenStationTop, HydrogenRegionShare, HydrogenDailyRow,
+ HydrogenKpi, HydrogenStationTop, HydrogenRegionShare, HydrogenMonthlyPoint, HydrogenDailyRow,
ElectricKpi, ElectricDailyRow, ElectricMonthGroup,
CustomerType, DateQuickPick,
} from './types';
@@ -11,6 +11,7 @@ export interface HydrogenOverviewResponse {
kpi: HydrogenKpi;
top5: HydrogenStationTop[];
regions: HydrogenRegionShare[];
+ monthly: HydrogenMonthlyPoint[];
}
export function fetchHydrogenOverview(): Promise {
diff --git a/src/modules/energy/types.ts b/src/modules/energy/types.ts
index 304c57a..7345394 100644
--- a/src/modules/energy/types.ts
+++ b/src/modules/energy/types.ts
@@ -29,6 +29,12 @@ export interface HydrogenRegionShare {
share: number;
}
+export interface HydrogenMonthlyPoint {
+ month: string; // YYYY-MM
+ kg: number;
+ fee: number;
+}
+
export interface HydrogenStationRow {
name: string;
pricePerKg: number;
diff --git a/src/server/routes/energy/index.ts b/src/server/routes/energy/index.ts
index 6066c4a..e678a7c 100644
--- a/src/server/routes/energy/index.ts
+++ b/src/server/routes/energy/index.ts
@@ -165,7 +165,33 @@ app.get('/hydrogen/overview', async (c) => {
...(restKg > 0 ? [{ region: '其他', kg: restKg, share: restKg / totalKg }] : []),
];
- return { kpi, top5, regions };
+ // 月度趋势(本年内 12 个月,缺失月补 0)
+ const [monthRows] = await pool.query(
+ `SELECT DATE_FORMAT(hydrogen_time, '%Y-%m') AS m,
+ ROUND(SUM(hydrogen_quantity), 2) AS kg,
+ ROUND(SUM(cost_expense), 2) AS fee
+ FROM tab_energy_hydrogen_bill
+ WHERE is_deleted = 0
+ AND hydrogen_time >= ?
+ AND YEAR(hydrogen_time) = YEAR(CURDATE())
+ GROUP BY m
+ ORDER BY m`,
+ [HYDROGEN_MIN_DATE],
+ );
+ const monthMap = new Map();
+ for (const r of monthRows) {
+ monthMap.set(r.m as string, { kg: Number(r.kg) || 0, fee: Number(r.fee) || 0 });
+ }
+ const year = new Date().getFullYear();
+ const currentMonth = new Date().getMonth() + 1;
+ const monthly: { month: string; kg: number; fee: number }[] = [];
+ for (let mi = 1; mi <= currentMonth; mi++) {
+ const key = `${year}-${String(mi).padStart(2, '0')}`;
+ const v = monthMap.get(key) || { kg: 0, fee: 0 };
+ monthly.push({ month: key, kg: v.kg, fee: v.fee });
+ }
+
+ return { kpi, top5, regions, monthly };
});
return c.json(data);
});