refactor(energy): 氢能总览参照 BI 重构 + 月度趋势 + 高密度 KPI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
参考 https://bi.lnh2e.com/lingniu/decision/link/0iqP 重新设计: - 4 张高密度 KPI 卡:累计加氢量 / 累计加氢费 / 本月加氢 / 本日加氢 每张含主指标 + 2 行明细(我司/客户、加氢费/占比) - 新增年内月度加氢量柱图(缺失月份补 0) - 数字格式化:万元/亿元/T 单位自动切换,tabular-nums 对齐 - 后端 /hydrogen/overview 增加 monthly 字段 - 骨架屏同步更新 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,17 @@
|
|||||||
import { useEffect, useState } from 'react';
|
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 { fetchHydrogenOverview, type HydrogenOverviewResponse } from './api';
|
||||||
import RotatingFooterHint from '../../components/RotatingFooterHint';
|
import RotatingFooterHint from '../../components/RotatingFooterHint';
|
||||||
|
|
||||||
|
const REGION_COLORS = [
|
||||||
|
'#3b82f6', '#22d3ee', '#a855f7', '#f59e0b',
|
||||||
|
'#10b981', '#ef4444', '#6366f1', '#14b8a6',
|
||||||
|
'#94a3b8',
|
||||||
|
];
|
||||||
|
|
||||||
interface YAxisTickProps {
|
interface YAxisTickProps {
|
||||||
x?: number;
|
x?: number;
|
||||||
y?: number;
|
y?: number;
|
||||||
@@ -24,13 +33,55 @@ function RankYAxisTick({ x = 0, y = 0, index = 0, payload }: YAxisTickProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const REGION_COLORS = [
|
// ---------- 数字格式化 ----------
|
||||||
'#3b82f6', '#22d3ee', '#a855f7', '#f59e0b',
|
function fmtKg(kg: number): { value: string; unit: string } {
|
||||||
'#10b981', '#ef4444', '#6366f1', '#14b8a6',
|
if (kg >= 1000) return { value: (kg / 1000).toFixed(2), unit: 'T' };
|
||||||
'#94a3b8',
|
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 (
|
||||||
|
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-3 md:p-4 flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={`w-7 h-7 rounded-xl flex items-center justify-center ${iconBg}`}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-bold text-slate-500">{label}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-baseline gap-1">
|
||||||
|
<span className={`text-xl md:text-2xl font-black tabular-nums leading-none ${accentClass}`}>{hero.value}</span>
|
||||||
|
<span className="text-[11px] text-slate-400 font-bold">{hero.unit}</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5 pt-1 border-t border-slate-50">
|
||||||
|
{rows.map((r, i) => (
|
||||||
|
<div key={i} className="flex items-center justify-between text-[11px] font-bold">
|
||||||
|
<span className="text-slate-400">{r.label}</span>
|
||||||
|
<span className="text-slate-700 tabular-nums">{r.value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
export default function HydrogenOverview() {
|
export default function HydrogenOverview() {
|
||||||
const [data, setData] = useState<HydrogenOverviewResponse | null>(null);
|
const [data, setData] = useState<HydrogenOverviewResponse | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -52,14 +103,120 @@ export default function HydrogenOverview() {
|
|||||||
const k = data.kpi;
|
const k = data.kpi;
|
||||||
const top5 = data.top5;
|
const top5 = data.top5;
|
||||||
const regions = data.regions;
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-3">
|
<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 className="bg-white rounded-xl border border-slate-100 px-3 py-1.5 text-[11px] text-slate-400 flex items-center justify-between">
|
||||||
|
<span>数据自 2025-01-01 起 · 每分钟刷新</span>
|
||||||
|
<span className="hidden md:inline text-slate-300">{todayYear} 年累计口径</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* KPI 4 卡 */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 md:gap-3">
|
||||||
|
<KpiCard
|
||||||
|
icon={<Fuel size={14} className="text-cyan-600" strokeWidth={2.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}` },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<KpiCard
|
||||||
|
icon={<Wallet size={14} className="text-blue-600" strokeWidth={2.4} />}
|
||||||
|
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}` },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<KpiCard
|
||||||
|
icon={<CalendarDays size={14} className="text-amber-600" strokeWidth={2.4} />}
|
||||||
|
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'}%` },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<KpiCard
|
||||||
|
icon={<Sparkles size={14} className="text-violet-600" strokeWidth={2.4} />}
|
||||||
|
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'}%` },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 月度趋势:年内每月 */}
|
||||||
|
{monthly.length > 0 && (
|
||||||
|
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-3 md:p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm font-bold text-slate-700">{todayYear} 年月度加氢量</span>
|
||||||
|
<span className="text-[11px] text-slate-400 font-bold">单位 Kg</span>
|
||||||
|
</div>
|
||||||
|
<ResponsiveContainer width="100%" height={140}>
|
||||||
|
<BarChart data={monthly} margin={{ top: 8, right: 4, bottom: 0, left: 0 }}>
|
||||||
|
<XAxis
|
||||||
|
dataKey="month"
|
||||||
|
tickFormatter={(v: string) => v.slice(5).replace(/^0/, '') + '月'}
|
||||||
|
tick={{ fontSize: 10, fill: '#94a3b8' }}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
interval={0}
|
||||||
|
/>
|
||||||
|
<YAxis hide />
|
||||||
|
<Tooltip
|
||||||
|
formatter={(v) => [`${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)' }}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="kg" radius={[4, 4, 0, 0]}>
|
||||||
|
{monthly.map((_, i) => (
|
||||||
|
<Cell key={i} fill="url(#monthlyBarGrad)" />
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="monthlyBarGrad" x1="0" x2="0" y1="0" y2="1">
|
||||||
|
<stop offset="0%" stopColor="#22d3ee" />
|
||||||
|
<stop offset="100%" stopColor="#3b82f6" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Top5 + 区域占比 */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
{/* Top5 加氢站 */}
|
{/* Top5 加氢站 */}
|
||||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-4">
|
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-3 md:p-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<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>
|
||||||
@@ -81,7 +238,7 @@ export default function HydrogenOverview() {
|
|||||||
/>
|
/>
|
||||||
<Bar dataKey="kg" radius={[6, 6, 6, 6]}>
|
<Bar dataKey="kg" radius={[6, 6, 6, 6]}>
|
||||||
{top5.map((_, i) => (
|
{top5.map((_, i) => (
|
||||||
<Cell key={i} fill={`url(#topBarGrad)`} />
|
<Cell key={i} fill="url(#topBarGrad)" />
|
||||||
))}
|
))}
|
||||||
<LabelList
|
<LabelList
|
||||||
dataKey="kg"
|
dataKey="kg"
|
||||||
@@ -101,8 +258,9 @@ export default function HydrogenOverview() {
|
|||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
{/* 区域占比环 */}
|
|
||||||
<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-3 md: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">
|
||||||
<div className="relative w-1/2 h-[200px]">
|
<div className="relative w-1/2 h-[200px]">
|
||||||
@@ -131,15 +289,16 @@ export default function HydrogenOverview() {
|
|||||||
<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]">
|
||||||
{regions.map((r, i) => (
|
{regions.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">
|
||||||
<span className="w-2 h-2 rounded-full" style={{ background: REGION_COLORS[i % REGION_COLORS.length] }} />
|
<span className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: REGION_COLORS[i % REGION_COLORS.length] }} />
|
||||||
<span className="text-slate-600">{r.region}</span>
|
<span className="text-slate-600 truncate">{r.region}</span>
|
||||||
<span className="text-slate-400 ml-auto font-bold">{(r.share * 100).toFixed(1)}%</span>
|
<span className="text-slate-400 ml-auto font-bold flex-shrink-0">{(r.share * 100).toFixed(1)}%</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RotatingFooterHint />
|
<RotatingFooterHint />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -148,13 +307,41 @@ export default function HydrogenOverview() {
|
|||||||
function HydrogenOverviewSkeleton() {
|
function HydrogenOverviewSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 animate-pulse">
|
<div className="flex flex-col gap-3 animate-pulse">
|
||||||
{/* 顶部说明条 */}
|
|
||||||
<div className="bg-white rounded-xl border border-slate-100 px-3 py-2">
|
<div className="bg-white rounded-xl border border-slate-100 px-3 py-2">
|
||||||
<div className="h-3 w-44 bg-slate-100 rounded" />
|
<div className="h-3 w-44 bg-slate-100 rounded" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 4 卡占位 */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 md:gap-3">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<div key={i} className="bg-white rounded-2xl border border-slate-100 shadow-sm p-3 md:p-4 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-7 h-7 rounded-xl bg-slate-100" />
|
||||||
|
<div className="h-3 w-16 bg-slate-100 rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="h-7 w-24 bg-slate-200 rounded" />
|
||||||
|
<div className="space-y-1.5 pt-1 border-t border-slate-50">
|
||||||
|
<div className="flex justify-between"><div className="h-2.5 w-10 bg-slate-100 rounded" /><div className="h-2.5 w-16 bg-slate-100 rounded" /></div>
|
||||||
|
<div className="flex justify-between"><div className="h-2.5 w-10 bg-slate-100 rounded" /><div className="h-2.5 w-16 bg-slate-100 rounded" /></div>
|
||||||
|
</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-3">
|
||||||
|
<div className="h-4 w-32 bg-slate-100 rounded" />
|
||||||
|
<div className="h-3 w-12 bg-slate-100 rounded" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end gap-2 h-[120px]">
|
||||||
|
{[60, 75, 50, 80, 35, 90, 45].map((h, i) => (
|
||||||
|
<div key={i} className="flex-1 bg-slate-100 rounded-t" style={{ height: `${h}%` }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<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="bg-white rounded-2xl border border-slate-100 shadow-sm p-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="h-4 w-32 bg-slate-100 rounded" />
|
<div className="h-4 w-32 bg-slate-100 rounded" />
|
||||||
@@ -171,8 +358,6 @@ function HydrogenOverviewSkeleton() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 区域占比环 占位 */}
|
|
||||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-4 flex flex-col gap-3">
|
<div className="bg-white rounded-2xl border border-slate-100 shadow-sm p-4 flex flex-col gap-3">
|
||||||
<div className="h-4 w-28 bg-slate-100 rounded" />
|
<div className="h-4 w-28 bg-slate-100 rounded" />
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { fetchJson } from '../../auth/api-client';
|
import { fetchJson } from '../../auth/api-client';
|
||||||
import type {
|
import type {
|
||||||
HydrogenKpi, HydrogenStationTop, HydrogenRegionShare, HydrogenDailyRow,
|
HydrogenKpi, HydrogenStationTop, HydrogenRegionShare, HydrogenMonthlyPoint, HydrogenDailyRow,
|
||||||
ElectricKpi, ElectricDailyRow, ElectricMonthGroup,
|
ElectricKpi, ElectricDailyRow, ElectricMonthGroup,
|
||||||
CustomerType, DateQuickPick,
|
CustomerType, DateQuickPick,
|
||||||
} from './types';
|
} from './types';
|
||||||
@@ -11,6 +11,7 @@ export interface HydrogenOverviewResponse {
|
|||||||
kpi: HydrogenKpi;
|
kpi: HydrogenKpi;
|
||||||
top5: HydrogenStationTop[];
|
top5: HydrogenStationTop[];
|
||||||
regions: HydrogenRegionShare[];
|
regions: HydrogenRegionShare[];
|
||||||
|
monthly: HydrogenMonthlyPoint[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchHydrogenOverview(): Promise<HydrogenOverviewResponse> {
|
export function fetchHydrogenOverview(): Promise<HydrogenOverviewResponse> {
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ export interface HydrogenRegionShare {
|
|||||||
share: number;
|
share: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HydrogenMonthlyPoint {
|
||||||
|
month: string; // YYYY-MM
|
||||||
|
kg: number;
|
||||||
|
fee: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HydrogenStationRow {
|
export interface HydrogenStationRow {
|
||||||
name: string;
|
name: string;
|
||||||
pricePerKg: number;
|
pricePerKg: number;
|
||||||
|
|||||||
@@ -165,7 +165,33 @@ app.get('/hydrogen/overview', async (c) => {
|
|||||||
...(restKg > 0 ? [{ region: '其他', kg: restKg, share: restKg / totalKg }] : []),
|
...(restKg > 0 ? [{ region: '其他', kg: restKg, share: restKg / totalKg }] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return { kpi, top5, regions };
|
// 月度趋势(本年内 12 个月,缺失月补 0)
|
||||||
|
const [monthRows] = await pool.query<RowDataPacket[]>(
|
||||||
|
`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<string, { kg: number; fee: number }>();
|
||||||
|
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);
|
return c.json(data);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user