chore: checkpoint local changes
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import React, { useState } from 'react';
|
||||
import './styles/energy-bi-board.css';
|
||||
import { isOssGateSessionAuthed } from '../../common/oss-access-gate';
|
||||
|
||||
export const ENERGY_BI_PASSWORD = 'lingniu';
|
||||
export const ENERGY_BI_AUTH_KEY = 'energy-h2-bi-board-auth-v1';
|
||||
|
||||
export function isEnergyBiAuthed(): boolean {
|
||||
return isOssGateSessionAuthed(ENERGY_BI_AUTH_KEY);
|
||||
}
|
||||
|
||||
export function setEnergyBiAuthed(ok: boolean): void {
|
||||
try {
|
||||
if (ok) sessionStorage.setItem(ENERGY_BI_AUTH_KEY, '1');
|
||||
else sessionStorage.removeItem(ENERGY_BI_AUTH_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
interface EnergyBiAccessGateProps {
|
||||
onOk: () => void;
|
||||
}
|
||||
|
||||
/** 轻门禁:口令 lingniu · 本会话记住(与汇报舱 / 作战室同口径) */
|
||||
export const EnergyBiAccessGate: React.FC<EnergyBiAccessGateProps> = ({ onOk }) => {
|
||||
const [pwd, setPwd] = useState('');
|
||||
const [err, setErr] = useState('');
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (pwd.trim() === ENERGY_BI_PASSWORD) {
|
||||
setEnergyBiAuthed(true);
|
||||
setErr('');
|
||||
onOk();
|
||||
return;
|
||||
}
|
||||
setErr('口令不对,请重试');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ehb-gate">
|
||||
<form className="ehb-gate-card" onSubmit={submit}>
|
||||
<p className="ehb-gate-kicker">ONEOS · 能源 BI</p>
|
||||
<h1 className="ehb-gate-title">氢能经营看板</h1>
|
||||
<p className="ehb-gate-sub">我司成本 · 按日 / 总览 · 单站日报</p>
|
||||
<label className="ehb-gate-label" htmlFor="ehb-pwd">
|
||||
访问口令
|
||||
</label>
|
||||
<input
|
||||
id="ehb-pwd"
|
||||
className="ehb-gate-input"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
autoFocus
|
||||
value={pwd}
|
||||
onChange={(e) => {
|
||||
setPwd(e.target.value);
|
||||
if (err) setErr('');
|
||||
}}
|
||||
placeholder="请输入口令"
|
||||
/>
|
||||
<p className="ehb-gate-error" role="alert">
|
||||
{err}
|
||||
</p>
|
||||
<button type="submit" className="ehb-gate-btn">
|
||||
进入看板
|
||||
</button>
|
||||
<p className="ehb-gate-foot">内部文件 · 请勿外传</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"directory": {
|
||||
"title": "能源氢费经营看板",
|
||||
"nodes": [
|
||||
{
|
||||
"id": "overview",
|
||||
"title": "产品需求说明(PRD)",
|
||||
"type": "markdown",
|
||||
"path": ".spec/requirements-prd.md",
|
||||
"description": "口令 lingniu · 顶栏全局/单站 · 无关联入口卡 · 默认全部车辆·KPI/图表/钻取跟随筛选 · 无车牌归外部车辆 · 外部主数据见 energy-h2-external-* · 分流 own→氢费明细 external→仅 BI"
|
||||
},
|
||||
{
|
||||
"id": "board-app",
|
||||
"title": "氢能经营看板",
|
||||
"type": "route",
|
||||
"path": "/prototypes/energy-h2-bi-board",
|
||||
"description": "顶栏全局/单站;单站=日报起止查询·行内加氢量占比·按量降序"
|
||||
},
|
||||
{
|
||||
"id": "energy-board-plan",
|
||||
"title": "能源 BI 看板方案",
|
||||
"type": "markdown",
|
||||
"path": "../../resources/prd/energy-board-plan-20260806.md"
|
||||
},
|
||||
{
|
||||
"id": "host-ref",
|
||||
"title": "宿主 overview 视觉参考",
|
||||
"type": "markdown",
|
||||
"path": "../../resources/prd/energy-bi-host-ref/content.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import type {
|
||||
CostDim,
|
||||
FleetScope,
|
||||
H2OrderRow,
|
||||
LeaseKind,
|
||||
OpsKind,
|
||||
} from '../types';
|
||||
import { COST_DIM_LABEL, LEASE_KIND_LABEL, OPS_KIND_LABEL } from '../types';
|
||||
|
||||
export function filterOrders(
|
||||
rows: H2OrderRow[],
|
||||
year: number,
|
||||
verifyScope: 'all' | 'verified',
|
||||
fleetScope: FleetScope,
|
||||
): H2OrderRow[] {
|
||||
return rows.filter((r) => {
|
||||
if (!r.occurredAt.startsWith(String(year))) return false;
|
||||
if (verifyScope === 'verified' && r.verifyStatus !== 'verified') return false;
|
||||
if (fleetScope === 'own' && r.fleet !== 'own') return false;
|
||||
if (fleetScope === 'external' && r.fleet !== 'external') return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function sumAmount(rows: H2OrderRow[]): number {
|
||||
return rows.reduce((s, r) => s + r.amount, 0);
|
||||
}
|
||||
|
||||
export function sumKg(rows: H2OrderRow[]): number {
|
||||
return rows.reduce((s, r) => s + r.quantityKg, 0);
|
||||
}
|
||||
|
||||
export function companyCostRows(rows: H2OrderRow[]): H2OrderRow[] {
|
||||
return rows.filter((r) => r.borneBy === 'company');
|
||||
}
|
||||
|
||||
export function dimAmount(rows: H2OrderRow[], dim: CostDim): number {
|
||||
return sumAmount(companyCostRows(rows).filter((r) => r.costDim === dim));
|
||||
}
|
||||
|
||||
export function leaseSubAmount(rows: H2OrderRow[], kind: LeaseKind): number {
|
||||
return sumAmount(
|
||||
companyCostRows(rows).filter((r) => r.costDim === 'lease' && r.leaseKind === kind),
|
||||
);
|
||||
}
|
||||
|
||||
export function opsSubAmount(rows: H2OrderRow[], kind: OpsKind): number {
|
||||
return sumAmount(
|
||||
companyCostRows(rows).filter((r) => r.costDim === 'ops' && r.opsKind === kind),
|
||||
);
|
||||
}
|
||||
|
||||
export interface DimCard {
|
||||
key: CostDim;
|
||||
label: string;
|
||||
amount: number;
|
||||
subs: { key: string; label: string; amount: number }[];
|
||||
}
|
||||
|
||||
export function costDimCards(rows: H2OrderRow[]): DimCard[] {
|
||||
return [
|
||||
{
|
||||
key: 'lease',
|
||||
label: COST_DIM_LABEL.lease,
|
||||
amount: dimAmount(rows, 'lease'),
|
||||
subs: [
|
||||
{ key: 'company_borne', label: LEASE_KIND_LABEL.company_borne, amount: leaseSubAmount(rows, 'company_borne') },
|
||||
{ key: 'package_h2', label: LEASE_KIND_LABEL.package_h2, amount: leaseSubAmount(rows, 'package_h2') },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'logistics',
|
||||
label: COST_DIM_LABEL.logistics,
|
||||
amount: dimAmount(rows, 'logistics'),
|
||||
subs: [],
|
||||
},
|
||||
{
|
||||
key: 'ops',
|
||||
label: COST_DIM_LABEL.ops,
|
||||
amount: dimAmount(rows, 'ops'),
|
||||
subs: [
|
||||
{ key: 'abnormal', label: OPS_KIND_LABEL.abnormal, amount: opsSubAmount(rows, 'abnormal') },
|
||||
{ key: 'transfer', label: OPS_KIND_LABEL.transfer, amount: opsSubAmount(rows, 'transfer') },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function pendingAmount(rows: H2OrderRow[]): number {
|
||||
return dimAmount(rows, 'pending');
|
||||
}
|
||||
|
||||
export function unverified(rows: H2OrderRow[]): { amount: number; count: number } {
|
||||
const list = rows.filter((r) => r.verifyStatus === 'unverified');
|
||||
return { amount: sumAmount(list), count: list.length };
|
||||
}
|
||||
|
||||
export function formatYuan(n: number): string {
|
||||
return `¥${n.toLocaleString('zh-CN', { maximumFractionDigits: 0 })}`;
|
||||
}
|
||||
|
||||
export function formatKg(n: number): string {
|
||||
return `${n.toLocaleString('zh-CN', { maximumFractionDigits: 2 })} kg`;
|
||||
}
|
||||
|
||||
export function costDimLabel(row: H2OrderRow): string {
|
||||
if (row.costDim === 'lease' && row.leaseKind) {
|
||||
return `${COST_DIM_LABEL.lease} · ${LEASE_KIND_LABEL[row.leaseKind]}`;
|
||||
}
|
||||
if (row.costDim === 'ops' && row.opsKind) {
|
||||
return `${COST_DIM_LABEL.ops} · ${OPS_KIND_LABEL[row.opsKind]}`;
|
||||
}
|
||||
return COST_DIM_LABEL[row.costDim];
|
||||
}
|
||||
|
||||
export type DimFilter =
|
||||
| { dim: CostDim; sub?: string }
|
||||
| null;
|
||||
|
||||
export function applyDimFilter(rows: H2OrderRow[], filter: DimFilter): H2OrderRow[] {
|
||||
if (!filter) return rows;
|
||||
return rows.filter((r) => {
|
||||
if (r.borneBy !== 'company') return false;
|
||||
if (r.costDim !== filter.dim) return false;
|
||||
if (!filter.sub) return true;
|
||||
if (filter.dim === 'lease') return r.leaseKind === filter.sub;
|
||||
if (filter.dim === 'ops') return r.opsKind === filter.sub;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** 统计/明细共用:维度筛后的我司成本行;无维度筛则全部我司行 */
|
||||
export function companyRowsForStats(rows: H2OrderRow[], filter: DimFilter): H2OrderRow[] {
|
||||
if (!filter) return companyCostRows(rows);
|
||||
return applyDimFilter(rows, filter);
|
||||
}
|
||||
|
||||
export interface StationMonthRow {
|
||||
stationId: string;
|
||||
stationName: string;
|
||||
month: string;
|
||||
amount: number;
|
||||
quantityKg: number;
|
||||
unverifiedAmount: number;
|
||||
}
|
||||
|
||||
export function stationMonthAgg(rows: H2OrderRow[]): StationMonthRow[] {
|
||||
const map = new Map<string, H2OrderRow[]>();
|
||||
rows.forEach((r) => {
|
||||
const month = r.occurredAt.slice(0, 7);
|
||||
const key = `${r.stationId}|${month}`;
|
||||
const list = map.get(key) ?? [];
|
||||
list.push(r);
|
||||
map.set(key, list);
|
||||
});
|
||||
return Array.from(map.entries())
|
||||
.map(([key, list]) => {
|
||||
const [stationId, month] = key.split('|');
|
||||
return {
|
||||
stationId,
|
||||
stationName: list[0].stationName,
|
||||
month,
|
||||
amount: sumAmount(list),
|
||||
quantityKg: sumKg(list),
|
||||
unverifiedAmount: sumAmount(list.filter((x) => x.verifyStatus === 'unverified')),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.amount - a.amount);
|
||||
}
|
||||
|
||||
export interface CustomerAttrRow {
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
borneLabel: string;
|
||||
quantityKg: number;
|
||||
companyCost: number;
|
||||
unverifiedAmount: number;
|
||||
}
|
||||
|
||||
export function customerAttrAgg(rows: H2OrderRow[]): CustomerAttrRow[] {
|
||||
const map = new Map<string, H2OrderRow[]>();
|
||||
rows.forEach((r) => {
|
||||
const list = map.get(r.customerId) ?? [];
|
||||
list.push(r);
|
||||
map.set(r.customerId, list);
|
||||
});
|
||||
return Array.from(map.entries())
|
||||
.map(([customerId, list]) => {
|
||||
const company = list.filter((x) => x.borneBy === 'company');
|
||||
const customer = list.filter((x) => x.borneBy === 'customer');
|
||||
let borneLabel = '混合';
|
||||
if (company.length && !customer.length) borneLabel = '我司';
|
||||
else if (customer.length && !company.length) borneLabel = '客户';
|
||||
return {
|
||||
customerId,
|
||||
customerName: list[0].customerName,
|
||||
borneLabel,
|
||||
quantityKg: sumKg(list),
|
||||
companyCost: sumAmount(company),
|
||||
unverifiedAmount: sumAmount(list.filter((x) => x.verifyStatus === 'unverified')),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.companyCost - a.companyCost || b.quantityKg - a.quantityKg);
|
||||
}
|
||||
|
||||
export const SOURCE_LABEL: Record<H2OrderRow['source'], string> = {
|
||||
api: 'API',
|
||||
manual: '补录',
|
||||
fence: '围栏',
|
||||
};
|
||||
|
||||
/** 总览 KPI:在宿主示意量级上按当前筛选(年/车辆/核对)等比缩放,保证卡片跟随顶栏筛选 */
|
||||
export function computeHostKpi(
|
||||
filtered: H2OrderRow[],
|
||||
year: number,
|
||||
allOrders: H2OrderRow[],
|
||||
base: {
|
||||
totalKgT: number;
|
||||
companyKgT: number;
|
||||
customerKgT: number;
|
||||
totalFeeWan: number;
|
||||
companyFeeWan: number;
|
||||
customerFeeWan: number;
|
||||
profitWan: number;
|
||||
incomeWan: number;
|
||||
costWan: number;
|
||||
monthKgT: number;
|
||||
monthFeeWan: number;
|
||||
monthYearPct: number;
|
||||
dayKg: number;
|
||||
dayFee: number;
|
||||
dayMonthPct: number;
|
||||
},
|
||||
) {
|
||||
const round2 = (n: number) => Math.round(n * 100) / 100;
|
||||
const baseline = filterOrders(allOrders, year, 'all', 'all');
|
||||
const baseKg = sumKg(baseline) || 1;
|
||||
const fKg = sumKg(filtered);
|
||||
const ratio = fKg / baseKg;
|
||||
|
||||
const companyKg = sumKg(filtered.filter((r) => r.borneBy === 'company'));
|
||||
const customerKg = sumKg(filtered.filter((r) => r.borneBy === 'customer'));
|
||||
const split = companyKg + customerKg || 1;
|
||||
const companyShare = companyKg / split;
|
||||
const customerShare = customerKg / split;
|
||||
|
||||
const monthRows = filtered.filter((r) => r.occurredAt.startsWith(`${year}-08`));
|
||||
const dayRows = filtered.filter((r) => r.occurredAt.startsWith(`${year}-08-08`));
|
||||
const monthKg = sumKg(monthRows);
|
||||
const dayKgVal = sumKg(dayRows);
|
||||
const monthAmt = sumAmount(monthRows);
|
||||
const dayAmt = sumAmount(dayRows);
|
||||
const yearKg = fKg || 1;
|
||||
const monthKgShare = monthKg / yearKg;
|
||||
const dayMonthShare = monthKg > 0 ? dayKgVal / monthKg : 0;
|
||||
|
||||
const totalKgT = round2(base.totalKgT * ratio);
|
||||
const totalFeeWan = round2(base.totalFeeWan * ratio);
|
||||
const incomeWan = round2(base.incomeWan * ratio);
|
||||
const costWan = round2(base.costWan * ratio);
|
||||
const profitWan = round2(base.profitWan * ratio);
|
||||
const monthKgT = round2(totalKgT * monthKgShare);
|
||||
const monthFeeWan = round2(totalFeeWan * monthKgShare);
|
||||
|
||||
return {
|
||||
totalKgT,
|
||||
companyKgT: round2(totalKgT * companyShare),
|
||||
customerKgT: round2(totalKgT * customerShare),
|
||||
totalFeeWan,
|
||||
companyFeeWan: round2(totalFeeWan * companyShare),
|
||||
customerFeeWan: round2(totalFeeWan * customerShare),
|
||||
profitWan,
|
||||
incomeWan,
|
||||
costWan,
|
||||
monthKgT,
|
||||
monthFeeWan,
|
||||
monthYearPct: round2(monthKgShare * 100),
|
||||
dayKg: round2(dayKgVal > 0 ? dayKgVal : base.dayKg * ratio * Math.max(dayMonthShare, 0.01)),
|
||||
dayFee: Math.round(dayAmt > 0 ? dayAmt : base.dayFee * ratio * Math.max(dayMonthShare, 0.01)),
|
||||
dayMonthPct: round2((dayMonthShare || base.dayMonthPct / 100) * 100),
|
||||
profitRatePct: incomeWan > 0 ? round2((profitWan / incomeWan) * 100) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import type { H2OrderRow, StationPrepaid } from '../types';
|
||||
|
||||
/** 辅助生成 200 条逼真高质量订单明细假数据 */
|
||||
function generate200MockOrders(): H2OrderRow[] {
|
||||
const stations = [
|
||||
{ id: 'st-ln', name: '佛山南海羚牛加氢站' },
|
||||
{ id: 'st-dp', name: '东鹏大道甲醇制氢一体站' },
|
||||
{ id: 'st-jx', name: '嘉兴中石化滨海加氢站' },
|
||||
{ id: 'st-jj', name: '嘉兴嘉锦加氢站' },
|
||||
{ id: 'st-cd', name: '成都中石化天府机场高速北站加氢站' },
|
||||
{ id: 'st-gz', name: '广州黄埔高新区氢能示范加氢站' },
|
||||
{ id: 'st-sh', name: '上海安亭加氢站' },
|
||||
];
|
||||
|
||||
const ownPlates = [
|
||||
'粤A99887', '粤B12001', '浙A52088', '浙F77881', '浙A88888F',
|
||||
'浙A66666', '浙F11223', '浙F33445', '川A77889', '川B99001',
|
||||
'沪A33219', '粤B88102', '浙F99812', '浙F66521', '粤A11029',
|
||||
];
|
||||
|
||||
const extPlates = [
|
||||
'粤A77661', '浙F33211', '川A55432', '沪B98765', '粤B66554',
|
||||
'浙A22334', '粤A99102', '川B88761', '无车牌(散车)',
|
||||
];
|
||||
|
||||
const customers = [
|
||||
{ id: 'c-ln', name: '羚牛自营', type: 'internal', deptId: 'd-ops', deptName: '运维中心' },
|
||||
{ id: 'c-bao', name: '包氢专线项目', type: 'internal', deptId: 'd-lease', deptName: '租赁业务一部' },
|
||||
{ id: 'c-lease-a', name: '嘉兴智奇供应链', type: 'internal', deptId: 'd-lease', deptName: '租赁业务一部' },
|
||||
{ id: 'c-log', name: '嘉兴益顺冷链', type: 'internal', deptId: 'd-log', deptName: '物流中心' },
|
||||
{ id: 'c-log2', name: '四川群彬物流', type: 'internal', deptId: 'd-log', deptName: '物流中心' },
|
||||
{ id: 'c-lease-b', name: '无锡铭康物流', type: 'internal', deptId: 'd-lease', deptName: '租赁业务二部' },
|
||||
{ id: 'c-ops', name: '运维调拨车辆', type: 'internal', deptId: 'd-ops', deptName: '运维中心' },
|
||||
{ id: 'c-pend', name: '待归属样本', type: 'internal', deptId: 'd-lease', deptName: '租赁业务一部' },
|
||||
{ id: 'c-ext-a', name: '广东氢动力', type: 'external', deptId: 'd-sales', deptName: '能源销售' },
|
||||
{ id: 'c-ext-b', name: '广东清运专线', type: 'external', deptId: 'd-sales', deptName: '能源销售' },
|
||||
{ id: 'c-ext-c', name: '东展供应链', type: 'external', deptId: 'd-sales', deptName: '能源销售' },
|
||||
{ id: 'c-ext-d', name: '顺丰冷运专线', type: 'external', deptId: 'd-sales', deptName: '能源销售' },
|
||||
{ id: 'c-ext-e', name: '极兔速递冷链', type: 'external', deptId: 'd-sales', deptName: '能源销售' },
|
||||
];
|
||||
|
||||
const list: H2OrderRow[] = [];
|
||||
|
||||
// 生成 200 条记录
|
||||
for (let i = 1; i <= 200; i++) {
|
||||
const padId = String(i).padStart(3, '0');
|
||||
|
||||
// 年份分配: 1~145 (2026年), 146~185 (2025年), 186~200 (2024年)
|
||||
let year = 2026;
|
||||
let month = Math.floor((i % 8)) + 1; // 1~8月
|
||||
if (i > 145 && i <= 185) {
|
||||
year = 2025;
|
||||
month = Math.floor((i % 12)) + 1;
|
||||
} else if (i > 185) {
|
||||
year = 2024;
|
||||
month = Math.floor((i % 12)) + 1;
|
||||
}
|
||||
|
||||
const day = (i * 7 % 28) + 1;
|
||||
const hour = (i * 3 % 14) + 7;
|
||||
const minute = (i * 11 % 50) + 5;
|
||||
|
||||
const mm = month < 10 ? `0${month}` : `${month}`;
|
||||
const dd = day < 10 ? `0${day}` : `${day}`;
|
||||
const hh = hour < 10 ? `0${hour}` : `${hour}`;
|
||||
const min = minute < 10 ? `0${minute}` : `${minute}`;
|
||||
|
||||
const occurredAt = `${year}-${mm}-${dd} ${hh}:${min}`;
|
||||
const station = stations[i % stations.length];
|
||||
const customer = customers[i % customers.length];
|
||||
|
||||
const isOwn = customer.type === 'internal';
|
||||
const fleet = isOwn ? 'own' : 'external';
|
||||
const plateNo = isOwn
|
||||
? ownPlates[i % ownPlates.length]
|
||||
: extPlates[i % extPlates.length];
|
||||
|
||||
// 单价 & 加氢量
|
||||
const unitPrice = [28, 30, 32, 35][i % 4];
|
||||
const quantityKg = Math.round((18 + (i * 3.7 % 85)) * 100) / 100;
|
||||
const amount = Math.round(quantityKg * unitPrice);
|
||||
|
||||
// 成本维度与费用承担
|
||||
let borneBy: 'company' | 'customer' = 'company';
|
||||
let costDim: 'lease' | 'logistics' | 'ops' | 'pending' = 'lease';
|
||||
let leaseKind: 'company_borne' | 'package_h2' | undefined = undefined;
|
||||
let opsKind: 'abnormal' | 'transfer' | undefined = undefined;
|
||||
|
||||
if (customer.id === 'c-ext-a' || customer.id === 'c-ext-d') {
|
||||
borneBy = 'customer';
|
||||
}
|
||||
|
||||
const dimType = i % 5;
|
||||
if (dimType === 0) {
|
||||
costDim = 'lease';
|
||||
leaseKind = 'company_borne';
|
||||
} else if (dimType === 1) {
|
||||
costDim = 'lease';
|
||||
leaseKind = 'package_h2';
|
||||
} else if (dimType === 2) {
|
||||
costDim = 'logistics';
|
||||
} else if (dimType === 3) {
|
||||
costDim = 'ops';
|
||||
opsKind = i % 2 === 0 ? 'abnormal' : 'transfer';
|
||||
} else {
|
||||
costDim = 'pending';
|
||||
}
|
||||
|
||||
// 核对状态与数据来源
|
||||
const verifyStatus = isOwn ? (i % 4 === 0 ? 'unverified' : 'verified') : 'unverified';
|
||||
const source = isOwn
|
||||
? (i % 3 === 0 ? 'manual' : i % 3 === 1 ? 'fence' : 'api')
|
||||
: (i % 2 === 0 ? 'api' : 'manual');
|
||||
|
||||
list.push({
|
||||
id: `HO-${String(year).slice(2)}${mm}-${padId}`,
|
||||
occurredAt,
|
||||
stationId: station.id,
|
||||
stationName: station.name,
|
||||
plateNo,
|
||||
customerId: customer.id,
|
||||
customerName: customer.name,
|
||||
deptId: customer.deptId,
|
||||
deptName: customer.deptName,
|
||||
amount,
|
||||
quantityKg,
|
||||
unitPrice,
|
||||
borneBy,
|
||||
costDim,
|
||||
leaseKind,
|
||||
opsKind,
|
||||
verifyStatus,
|
||||
source,
|
||||
fleet,
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 假数:200 条订单明细(三维度成本拆分) */
|
||||
export const MOCK_ORDERS: H2OrderRow[] = generate200MockOrders();
|
||||
|
||||
export const MOCK_PREPAID: StationPrepaid[] = [
|
||||
{
|
||||
stationId: 'st-jx',
|
||||
stationName: '嘉兴中石化滨海加氢站',
|
||||
openingBalance: 120000,
|
||||
openingAnchorLabel: '2025 年末财务期末',
|
||||
recharge: 80000,
|
||||
consume: 95000,
|
||||
},
|
||||
{
|
||||
stationId: 'st-jj',
|
||||
stationName: '嘉兴嘉锦加氢站',
|
||||
openingBalance: null,
|
||||
openingAnchorLabel: null,
|
||||
recharge: 40000,
|
||||
consume: 28000,
|
||||
},
|
||||
];
|
||||
|
||||
/** 宿主总览 KPI(与 zip content 同量级示意,只读壳) */
|
||||
export const HOST_KPI = {
|
||||
totalKgT: 697.16,
|
||||
companyKgT: 468.28,
|
||||
customerKgT: 228.88,
|
||||
totalFeeWan: 2093.71,
|
||||
companyFeeWan: 1362.85,
|
||||
customerFeeWan: 730.87,
|
||||
profitWan: 13.01,
|
||||
incomeWan: 743.88,
|
||||
costWan: 2093.71,
|
||||
monthKgT: 16.67,
|
||||
monthFeeWan: 50.36,
|
||||
monthYearPct: 2.4,
|
||||
dayKg: 181.78,
|
||||
dayFee: 6533,
|
||||
dayMonthPct: 1.1,
|
||||
};
|
||||
|
||||
export const DEFAULT_YEAR = 2026;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @name 能源氢费经营看板
|
||||
* @description 嵌入 bi-next #hydrogen/overview · 我司成本三维度(非 OneOS V2) · 口令 lingniu
|
||||
*/
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
type AnnotationSourceDocument,
|
||||
type AnnotationViewerOptions,
|
||||
} from '@axhub/annotation';
|
||||
import { PrototypeAnnotationHost } from '../../common/prototype-annotation-host';
|
||||
import { clearHostPrototypeRouteInfo } from '../../common/useHashPage';
|
||||
import { EnergyBiAccessGate, isEnergyBiAuthed } from './EnergyBiAccessGate';
|
||||
import { EnergyBiBoardApp } from './EnergyBiBoardApp';
|
||||
import annotationSourceDocument from './annotation-source.json';
|
||||
|
||||
function AuthedEnergyBiBoard() {
|
||||
const [ok, setOk] = useState(() => isEnergyBiAuthed());
|
||||
if (!ok) return <EnergyBiAccessGate onOk={() => setOk(true)} />;
|
||||
return <EnergyBiBoardApp />;
|
||||
}
|
||||
|
||||
export default function EnergyH2BiBoardEntry() {
|
||||
useEffect(() => {
|
||||
clearHostPrototypeRouteInfo();
|
||||
}, []);
|
||||
|
||||
const annotationOptions = useMemo<AnnotationViewerOptions>(
|
||||
() => ({ title: '能源氢费经营看板' }),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<PrototypeAnnotationHost
|
||||
source={annotationSourceDocument as unknown as AnnotationSourceDocument}
|
||||
options={annotationOptions}
|
||||
>
|
||||
<AuthedEnergyBiBoard />
|
||||
</PrototypeAnnotationHost>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const container = document.getElementById('root');
|
||||
if (container && !container.dataset.energyH2BiBoardMounted) {
|
||||
container.dataset.energyH2BiBoardMounted = '1';
|
||||
const root = createRoot(container);
|
||||
root.render(<EnergyH2BiBoardEntry />);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"version": 1,
|
||||
"format": "axhub-published-source",
|
||||
"sourceRoot": "source",
|
||||
"entry": "index.tsx",
|
||||
"files": [
|
||||
{
|
||||
"path": "annotation-source.json",
|
||||
"kind": "source"
|
||||
},
|
||||
{
|
||||
"path": "data/aggregates.ts",
|
||||
"kind": "source"
|
||||
},
|
||||
{
|
||||
"path": "data/mockBoard.ts",
|
||||
"kind": "source"
|
||||
},
|
||||
{
|
||||
"path": "data/mockDaily.ts",
|
||||
"kind": "source"
|
||||
},
|
||||
{
|
||||
"path": "EnergyBiAccessGate.tsx",
|
||||
"kind": "source"
|
||||
},
|
||||
{
|
||||
"path": "EnergyBiBoardApp.tsx",
|
||||
"kind": "source"
|
||||
},
|
||||
{
|
||||
"path": "index.tsx",
|
||||
"kind": "entry"
|
||||
},
|
||||
{
|
||||
"path": "styles/energy-bi-board.css",
|
||||
"kind": "source"
|
||||
},
|
||||
{
|
||||
"path": "types.ts",
|
||||
"kind": "source"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
/** 能源 BI · 氢能总览嵌入功能 · 类型(宿主 bi-next #hydrogen/overview,非 OneOS V2) */
|
||||
|
||||
/** 我司成本三维度(本尊 2026-08-07) */
|
||||
export type CostDim = 'lease' | 'logistics' | 'ops' | 'pending';
|
||||
|
||||
/** 租赁成本二级 */
|
||||
export type LeaseKind = 'company_borne' | 'package_h2';
|
||||
|
||||
/** 运维成本二级 */
|
||||
export type OpsKind = 'abnormal' | 'transfer';
|
||||
|
||||
export type VerifyStatus = 'verified' | 'unverified';
|
||||
export type BorneBy = 'company' | 'customer';
|
||||
export type FleetScope = 'own' | 'external' | 'all';
|
||||
/** 按日 | 总览(站日报 / 现结登记已拆独立模块) */
|
||||
export type HostView = 'daily' | 'overview';
|
||||
|
||||
export interface H2OrderRow {
|
||||
id: string;
|
||||
occurredAt: string;
|
||||
stationId: string;
|
||||
stationName: string;
|
||||
plateNo: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
deptId: string;
|
||||
deptName: string;
|
||||
amount: number;
|
||||
quantityKg: number;
|
||||
unitPrice: number;
|
||||
borneBy: BorneBy;
|
||||
costDim: CostDim;
|
||||
leaseKind?: LeaseKind;
|
||||
opsKind?: OpsKind;
|
||||
verifyStatus: VerifyStatus;
|
||||
source: 'api' | 'manual' | 'fence';
|
||||
fleet: 'own' | 'external';
|
||||
}
|
||||
|
||||
export interface StationPrepaid {
|
||||
stationId: string;
|
||||
stationName: string;
|
||||
openingBalance: number | null;
|
||||
openingAnchorLabel: string | null;
|
||||
recharge: number;
|
||||
consume: number;
|
||||
}
|
||||
|
||||
export const COST_DIM_LABEL: Record<CostDim, string> = {
|
||||
lease: '租赁成本',
|
||||
logistics: '物流成本',
|
||||
ops: '运维成本',
|
||||
pending: '待归属',
|
||||
};
|
||||
|
||||
export const LEASE_KIND_LABEL: Record<LeaseKind, string> = {
|
||||
company_borne: '我司承担',
|
||||
package_h2: '包氢项目',
|
||||
};
|
||||
|
||||
export const OPS_KIND_LABEL: Record<OpsKind, string> = {
|
||||
abnormal: '异动',
|
||||
transfer: '调拨',
|
||||
};
|
||||
Reference in New Issue
Block a user