128 lines
5.4 KiB
TypeScript
128 lines
5.4 KiB
TypeScript
import {
|
|
Bar,
|
|
BarChart,
|
|
LabelList,
|
|
Legend,
|
|
ResponsiveContainer,
|
|
Tooltip,
|
|
XAxis,
|
|
YAxis,
|
|
} from 'recharts';
|
|
import { useState } from 'react';
|
|
import type { HydrogenMonthlyPoint } from '../../types';
|
|
import {
|
|
buildMonthDrillPayload,
|
|
formatYuan as fmtYuan,
|
|
type OverviewDrillPayload,
|
|
type OverviewDrillRequest,
|
|
type OverviewScope,
|
|
} from '../model';
|
|
import { OverviewDrillDialog } from './OverviewDrillDialog';
|
|
|
|
type MonthlyChartPoint = HydrogenMonthlyPoint & { monthLabel: string };
|
|
|
|
interface MonthlyChartsProps {
|
|
activeYear: number;
|
|
monthly: HydrogenMonthlyPoint[];
|
|
monthlyDual: MonthlyChartPoint[];
|
|
scope?: OverviewScope;
|
|
scopeLabel?: string | null;
|
|
onDrillRequest?: (request: OverviewDrillRequest) => void;
|
|
}
|
|
|
|
export function MonthlyCharts({ activeYear, monthly, monthlyDual, scope = 'global', scopeLabel, onDrillRequest }: MonthlyChartsProps) {
|
|
const [drill, setDrill] = useState<OverviewDrillPayload | null>(null);
|
|
const openMonthPoint = (point: MonthlyChartPoint | undefined) => {
|
|
if (!point) return;
|
|
if (onDrillRequest) onDrillRequest({ kind: 'month', key: point.month, label: `${point.month} 月度经营明细` });
|
|
else setDrill(buildMonthDrillPayload(point));
|
|
};
|
|
const openMonthFromChart = (state: unknown) => {
|
|
openMonthPoint((state as { activePayload?: { payload?: MonthlyChartPoint }[] } | undefined)?.activePayload?.[0]?.payload);
|
|
};
|
|
const openMonthFromBar = (entry: unknown) => {
|
|
openMonthPoint((entry as { payload?: MonthlyChartPoint } | undefined)?.payload);
|
|
};
|
|
const scopeText = scope === 'station' && scopeLabel ? ` · ${scopeLabel}` : '';
|
|
return (
|
|
<>
|
|
{monthly.length > 0 && (
|
|
<div className="ehb-chart-box">
|
|
<div className="ehb-chart-box-head">
|
|
<div className="ehb-chart-box-title">{activeYear} 年月度加氢量{scopeText}</div>
|
|
<div className="ehb-chart-box-meta">单位:Kg</div>
|
|
</div>
|
|
<div className="ehb-chart-box-body">
|
|
<ResponsiveContainer width="100%" height="100%" minWidth={0} initialDimension={{ width: 1, height: 200 }}>
|
|
<BarChart data={monthlyDual} margin={{ top: 8, right: 4, bottom: 0, left: 0 }} onClick={openMonthFromChart} className="cursor-pointer">
|
|
<XAxis
|
|
dataKey="monthLabel"
|
|
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)' }}
|
|
/>
|
|
<Legend verticalAlign="top" height={24} iconSize={8} wrapperStyle={{ fontSize: 11, paddingBottom: 4 }} />
|
|
<Bar dataKey="lingniuKg" name="羚牛车辆" stackId="kg" fill="#4ba3df" radius={[0, 0, 0, 0]} onClick={openMonthFromBar} />
|
|
<Bar dataKey="externalKg" name="外部车辆" stackId="kg" fill="#f4bb45" radius={[4, 4, 0, 0]} onClick={openMonthFromBar}>
|
|
<LabelList dataKey="kg" position="top" formatter={value => {
|
|
const total = Number(value ?? 0);
|
|
return total >= 1000 ? `${(total / 1000).toFixed(1)}k` : total.toFixed(0);
|
|
}} fill="#475569" fontSize={10} fontWeight={700} />
|
|
</Bar>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{monthly.length > 0 && (
|
|
<div className="ehb-chart-box">
|
|
<div className="ehb-chart-box-head">
|
|
<div className="ehb-chart-box-title">{activeYear} 年月度收支对比{scopeText}</div>
|
|
<div className="ehb-chart-box-meta">单位:元</div>
|
|
</div>
|
|
<div className="ehb-chart-box-body">
|
|
<ResponsiveContainer width="100%" height="100%" minWidth={0} initialDimension={{ width: 1, height: 200 }}>
|
|
<BarChart data={monthlyDual} margin={{ top: 8, right: 4, bottom: 0, left: 0 }} onClick={openMonthFromChart} className="cursor-pointer">
|
|
<XAxis
|
|
dataKey="monthLabel"
|
|
tick={{ fontSize: 10, fill: '#94a3b8' }}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
interval={0}
|
|
/>
|
|
<YAxis hide />
|
|
<Legend
|
|
verticalAlign="top"
|
|
height={20}
|
|
iconSize={8}
|
|
wrapperStyle={{ fontSize: 11, paddingBottom: 4 }}
|
|
/>
|
|
<Tooltip
|
|
formatter={(v, name) => {
|
|
const f = fmtYuan(Number(v ?? 0));
|
|
return [`¥${f.value} ${f.unit}`, name];
|
|
}}
|
|
contentStyle={{ borderRadius: 12, fontSize: 12 }}
|
|
cursor={{ fill: 'rgba(148, 163, 184, 0.06)' }}
|
|
/>
|
|
<Bar dataKey="fee" name="成本支出" fill="#f59e0b" radius={[3, 3, 0, 0]} onClick={openMonthFromBar} />
|
|
<Bar dataKey="revenue" name="客户收入" fill="#10b981" radius={[3, 3, 0, 0]} onClick={openMonthFromBar} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<OverviewDrillDialog payload={drill} onClose={() => setDrill(null)} />
|
|
</>
|
|
);
|
|
}
|