feat(energy): rebuild hydrogen BI board and drill-through
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
kkfluous
2026-08-20 13:59:03 +08:00
parent 37a9f303ec
commit 62efef0ab9
72 changed files with 36773 additions and 965 deletions
+224 -1
View File
@@ -1,4 +1,48 @@
import type { HydrogenOverviewResponse } from '../api';
import type {
HydrogenCustomerRow,
HydrogenKpi,
HydrogenMonthlyPoint,
HydrogenRegionShare,
HydrogenStationFull,
} from '../types';
export type OverviewScope = 'global' | 'station';
export type OverviewMetricKey = 'yearKg' | 'yearFee' | 'yearProfit' | 'monthKg' | 'todayKg';
export type OverviewDrillKind = 'metric' | 'month' | 'station' | 'customer' | 'region';
export interface OverviewDrillRequest {
kind: OverviewDrillKind;
key: string;
label: string;
entityId?: number;
}
export interface OverviewDrillMetric {
label: string;
value: string;
tone?: 'default' | 'blue' | 'green' | 'amber' | 'red';
}
export interface OverviewDrillColumn {
key: string;
label: string;
align?: 'left' | 'right';
}
export interface OverviewDrillPayload {
kind: OverviewDrillKind;
title: string;
subtitle: string;
metrics: OverviewDrillMetric[];
columns: OverviewDrillColumn[];
rows: Record<string, string | number>[];
emptyMessage?: string;
primaryActionLabel?: string;
groupBy?: 'station' | 'customer' | 'vehicle';
groupByOptions?: Array<'station' | 'customer' | 'vehicle'>;
rowActionLabel?: string;
}
export function formatKg(kg: number): { value: string; unit: string } {
if (kg >= 1000) return { value: (kg / 1000).toFixed(2), unit: 'T' };
@@ -22,6 +66,185 @@ export function formatYuan(yuan: number): { value: string; unit: string } {
};
}
function formatKgText(value: number): string {
const formatted = formatKg(value);
return `${formatted.value} ${formatted.unit}`;
}
function formatYuanText(value: number): string {
const formatted = formatYuan(value);
return `¥${formatted.value} ${formatted.unit}`;
}
const METRIC_LABELS: Record<OverviewMetricKey, string> = {
yearKg: '累计加氢量',
yearFee: '累计加氢费',
yearProfit: '客户单毛利',
monthKg: '本月加氢',
todayKg: '本日加氢',
};
/**
* 总览接口只返回聚合值。弹层先完整呈现可核验汇总,订单树由父页面在接入
* 明细接口后通过 onDrillRequest 承接,避免用推算值冒充真实订单。
*/
export function buildMetricDrillPayload(kpi: HydrogenKpi, key: OverviewMetricKey): OverviewDrillPayload {
const customerCost = Math.max(0, kpi.yearFee - kpi.ourYearFee);
const common = {
kind: 'metric' as const,
title: METRIC_LABELS[key],
subtitle: '汇总口径穿透 · 当前筛选范围',
columns: [],
rows: [],
emptyMessage: '当前总览接口未返回站点、客户、车牌及单笔订单层级;汇总值已按真实账本展示。',
};
if (key === 'yearKg') {
return {
...common,
metrics: [
{ label: '累计加氢量', value: formatKgText(kpi.yearKg), tone: 'blue' },
{ label: '我司车辆', value: formatKgText(kpi.ourYearKg) },
{ label: '客户车辆', value: formatKgText(kpi.customerYearKg) },
],
};
}
if (key === 'yearFee') {
return {
...common,
metrics: [
{ label: '累计加氢费', value: formatYuanText(kpi.yearFee), tone: 'blue' },
{ label: '我司承担', value: formatYuanText(kpi.ourYearFee) },
{ label: '客户承担', value: formatYuanText(customerCost) },
],
};
}
if (key === 'yearProfit') {
return {
...common,
metrics: [
{ label: '客户单毛利', value: formatYuanText(kpi.yearProfit), tone: kpi.yearProfit >= 0 ? 'green' : 'red' },
{ label: '客户收入', value: formatYuanText(kpi.yearRevenue) },
{ label: '客户成本', value: formatYuanText(customerCost) },
],
};
}
if (key === 'monthKg') {
return {
...common,
metrics: [
{ label: '本月加氢量', value: formatKgText(kpi.monthKg), tone: 'amber' },
{ label: '本月加氢费', value: formatYuanText(kpi.monthFee) },
{ label: '占年比', value: `${kpi.yearKg > 0 ? (kpi.monthKg / kpi.yearKg * 100).toFixed(1) : '0.0'}%` },
],
};
}
return {
...common,
metrics: [
{ label: '本日加氢量', value: formatKgText(kpi.todayKg), tone: 'blue' },
{ label: '本日加氢费', value: formatYuanText(kpi.todayFee) },
{ label: '占月比', value: `${kpi.monthKg > 0 ? (kpi.todayKg / kpi.monthKg * 100).toFixed(1) : '0.0'}%` },
],
};
}
export function buildMonthDrillPayload(month: HydrogenMonthlyPoint): OverviewDrillPayload {
const lingniuKg = month.lingniuKg ?? 0;
const externalKg = month.externalKg ?? Math.max(0, month.kg - lingniuKg);
return {
kind: 'month',
title: `${month.month} 月度经营明细`,
subtitle: '月度聚合 · 当前车辆范围',
metrics: [
{ label: '加氢总量', value: formatKgText(month.kg), tone: 'blue' },
{ label: '成本支出', value: formatYuanText(month.fee), tone: 'amber' },
{ label: '客户收入', value: formatYuanText(month.revenue), tone: 'green' },
{ label: '客户单毛利', value: formatYuanText(month.profit), tone: month.profit >= 0 ? 'green' : 'red' },
],
columns: [
{ key: 'category', label: '车辆范围' },
{ key: 'kg', label: '加氢量', align: 'right' },
{ key: 'share', label: '占比', align: 'right' },
],
rows: [
{ category: '羚牛车辆', kg: formatKgText(lingniuKg), share: `${month.kg > 0 ? (lingniuKg / month.kg * 100).toFixed(1) : '0.0'}%` },
{ category: '外部车辆', kg: formatKgText(externalKg), share: `${month.kg > 0 ? (externalKg / month.kg * 100).toFixed(1) : '0.0'}%` },
],
};
}
export function buildStationDrillPayload(station: HydrogenStationFull): OverviewDrillPayload {
return {
kind: 'station',
title: `${station.name}」加氢汇总明细`,
subtitle: `${station.province || '未归属省份'} · 当前统计周期`,
metrics: [
{ label: '加氢量', value: formatKgText(station.kg), tone: 'blue' },
{ label: '氢费收入', value: formatYuanText(station.revenue), tone: 'green' },
{ label: '加氢量占比', value: `${(station.share * 100).toFixed(1)}%` },
{ label: '收入占比', value: `${(station.revenueShare * 100).toFixed(1)}%` },
],
columns: [],
rows: [],
emptyMessage: '当前总览接口未返回该站按日、客户及车辆流水;可切换到单站视图继续查看真实数据。',
primaryActionLabel: '进入单站视图',
};
}
export function buildCustomerDrillPayload(customer: HydrogenCustomerRow): OverviewDrillPayload {
const payerLabel = customer.payer === 'lingniu' ? '羚牛承担' : customer.payer === 'mixed' ? '混合承担' : '客户承担';
return {
kind: 'customer',
title: `${customer.name}」客户账单明细`,
subtitle: `${payerLabel} · 当前统计周期`,
metrics: [
{ label: '加氢量', value: formatKgText(customer.kg), tone: 'blue' },
{ label: '成本支出', value: formatYuanText(customer.cost), tone: 'amber' },
{ label: '应收', value: formatYuanText(customer.revenue), tone: 'green' },
{ label: '价差', value: formatYuanText(customer.revenue - customer.cost), tone: customer.revenue - customer.cost >= 0 ? 'green' : 'red' },
],
columns: [],
rows: [],
emptyMessage: '当前总览接口未返回该客户按日、车牌及单笔加氢流水。',
};
}
export function buildRegionDrillPayload(
region: HydrogenRegionShare,
stations: HydrogenStationFull[],
granularity: 'province' | 'city',
): OverviewDrillPayload {
const matchedStations = granularity === 'province'
? stations.filter(station => (station.province?.trim() || '未归属') === region.region)
: [];
return {
kind: 'region',
title: `${region.region}加氢区域明细`,
subtitle: `${granularity === 'province' ? '省级' : '市级'}口径 · 当前统计周期`,
metrics: [
{ label: '区域加氢量', value: formatKgText(region.kg), tone: 'blue' },
{ label: '全局占比', value: `${(region.share * 100).toFixed(1)}%` },
{ label: '已关联站点', value: `${matchedStations.length}` },
],
columns: [
{ key: 'station', label: '加氢站' },
{ key: 'province', label: '所属省份' },
{ key: 'kg', label: '加氢量', align: 'right' },
{ key: 'share', label: '区域内占比', align: 'right' },
],
rows: matchedStations.map(station => ({
station: station.name,
province: station.province || '未归属',
kg: formatKgText(station.kg),
share: `${region.kg > 0 ? (station.kg / region.kg * 100).toFixed(1) : '0.0'}%`,
})),
emptyMessage: granularity === 'city'
? '当前站点汇总接口未返回城市字段,无法将市级汇总继续关联到具体站点。'
: '当前区域暂无可关联站点。',
};
}
export function formatRelative(timestamp: number, now = Date.now()): string {
const seconds = Math.max(0, Math.floor((now - timestamp) / 1000));
if (seconds < 5) return '刚刚';
@@ -65,7 +288,7 @@ export function deriveOverviewMetrics(data: HydrogenOverviewResponse) {
? ((latestMonth.kg - previousMonth.kg) / previousMonth.kg) * 100
: null,
top5Share: (top5.reduce((sum, item) => sum + item.kg, 0) / Math.max(1, kpi.yearKg)) * 100,
profitYield: kpi.yearRevenue > 0 ? (kpi.yearProfit / kpi.yearRevenue) * 100 : 0,
customerGrossMarginPct: kpi.yearRevenue > 0 ? (kpi.yearProfit / kpi.yearRevenue) * 100 : 0,
stationAvgKg: stations.length > 0 ? kpi.yearKg / stations.length : 0,
monthlyDual: monthly.map(month => ({
...month,