Files
ln-bi/src/modules/energy/hydrogen/api.ts
T
dsh-agent 7556dbcebc refactor(stage2): 生产代码移出 src/vendor,氢能收敛为分层 feature
src/vendor 语义是'第三方/参考',却装着线上氢能看板并反向 import
src/modules,依赖方向倒挂。本次把生产代码归位并建立氢能分层:

  src/modules/energy/hydrogen/
    index.tsx        导航入口(原 HydrogenModule.tsx)
    api.ts types.ts  HTTP 与 DTO
    dev-mock-api.ts  仅 dev 的 vite 插件(仍由 vite.config.ts 引用)
    model/           纯逻辑(format / bearing-labels / daily-detail-format)
    board/           经营看板 UI(原 vendor energy-h2-bi-board)
    station-daily/   单站日报 UI(原 vendor energy-h2-station-daily)
    drill/           下钻弹层 UI(原 hydrogen-bi-v2 的 UI 部分)
    common/          工具与共享组件(原 vendor common + prototype-download)
    fonts/           JetBrains Mono(原 vendor resources/design-system)

- UI 与 CSS 逐字节保留,仅移动位置与改写相对 import。
- 删除自挂载原型入口后遗留的 2 个 annotation-source.json。
- vite.config.ts 与 tsconfig 的路径/exclude 同步清理。
- independent-entry.test.ts 不再硬编码路径,改为断言'两个入口解析到同一文件'。

导入改写用一次性 codemod 完成(按旧位置解析、按新位置重写),
lint / test(131) / build 全绿,可达性分析仍为 0 未引用文件。
2026-09-11 10:14:37 +08:00

183 lines
6.0 KiB
TypeScript

import { fetchJson } from '../../../auth/api-client';
import type {
H2BiDailyResponse,
H2BiDailyTreeQuery,
H2BiDailyTreeResponse,
H2BiDrillReadOptions,
H2BiFullDrillOptions,
H2BiFullDrillResponse,
H2BiDrillQuery,
H2BiDrillResponse,
H2BiMetaResponse,
H2BiOverviewResponse,
H2BiQuery,
} from './types';
const BASE = '/api/energy/h2/v2';
function queryString(query: Record<string, unknown>) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value !== undefined && value !== null && value !== '') params.set(key, String(value));
}
return params.toString();
}
function request<T>(path: string, query: object = {}, options?: RequestInit) {
const qs = queryString(query as Record<string, unknown>);
return fetchJson<T>(`${BASE}/${path}${qs ? `?${qs}` : ''}`, options);
}
export function fetchH2BiMeta() {
return request<H2BiMetaResponse>('meta');
}
export function fetchH2BiOverview(query: H2BiQuery) {
return request<H2BiOverviewResponse>('overview', query);
}
export function fetchH2BiDaily(query: H2BiQuery) {
return request<H2BiDailyResponse>('daily', query).catch(async () => {
// The date-group drill is backed by the same read-only ledger and has a
// simpler query plan. Keep the date view usable when the aggregate daily
// endpoint times out/fails, without substituting mock data.
const drill = await request<H2BiDrillResponse>('drill', {
...query,
groupBy: 'date',
pageSize: 400,
});
const startDate = query.startDate ?? `${query.year}-01-01`;
const endDate = query.endDate ?? new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(new Date());
const byDate = new Map(drill.groups.map((row) => [row.name, row]));
const days = [] as H2BiDailyResponse['trend'];
for (let cursor = new Date(`${startDate}T00:00:00Z`); cursor <= new Date(`${endDate}T00:00:00Z`); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
const date = cursor.toISOString().slice(0, 10);
const row = byDate.get(date);
days.push({
date,
kg: Number(row?.kg ?? 0),
lingniuKg: Number(row?.lingniuKg ?? 0),
externalKg: Number(row?.externalKg ?? 0),
cost: Number(row?.cost ?? 0),
recordCount: Number(row?.recordCount ?? 0),
});
}
const totalKg = days.reduce((sum, row) => sum + row.kg, 0);
const totalCost = days.reduce((sum, row) => sum + row.cost, 0);
return {
range: { startDate, endDate },
watermark: { ledgerAt: null, paymentAt: null },
filters: query,
kpis: {
totalKg,
totalCost,
averageDailyKg: totalKg / Math.max(1, days.length),
stationCount: Number(drill.summary.stationCount ?? 0),
activeDays: days.filter((row) => row.kg > 0).length,
},
trend: days,
days: [...days].reverse(),
};
});
}
export function fetchH2BiDailyTree(date: string, query: H2BiDailyTreeQuery) {
return request<H2BiDailyTreeResponse>('daily-tree', { date, ...query });
}
export async function fetchH2BiDrill(
query: H2BiDrillQuery,
options?: H2BiDrillReadOptions,
) {
const page = query.page ?? 1;
const pageSize = Math.min(Math.max(1, query.pageSize ?? 100), 200);
const response = await request<H2BiDrillResponse>(
'drill',
{ ...query, page, pageSize },
options,
);
const itemCount = query.groupBy === 'record'
? response.records.length
: response.groups.length;
// Older servers only set hasMore for record pages. A full page is therefore
// also treated as potentially incomplete; the final empty/short page proves
// completion without silently dropping grouped rows.
return {
...response,
page: {
...response.page,
page,
pageSize,
itemCount,
hasMore: Boolean(response.page?.hasMore) || itemCount === pageSize,
},
} satisfies H2BiDrillResponse;
}
function abortError() {
const error = new Error('已取消全量读取');
error.name = 'AbortError';
return error;
}
/**
* Reads every page only after a caller explicitly asks for a complete result.
* It never returns a partial collection: server errors, cancellation, and the
* page guard all reject before an export can be created.
*/
export async function fetchAllH2BiDrill(
query: Omit<H2BiDrillQuery, 'page' | 'pageSize'>,
options: H2BiFullDrillOptions = {},
): Promise<H2BiFullDrillResponse> {
const pageSize = Math.min(Math.max(1, options.pageSize ?? 200), 200);
const maxPages = Math.max(1, options.maxPages ?? 250);
const maxRows = Math.max(1, options.maxRows ?? 50_000);
const groups: H2BiDrillResponse['groups'] = [];
const records: H2BiDrillResponse['records'] = [];
let first: H2BiDrillResponse | null = null;
for (let page = 1; page <= maxPages; page += 1) {
if (options.signal?.aborted) throw abortError();
const result = await fetchH2BiDrill(
{ ...query, page, pageSize },
{ signal: options.signal },
);
if (!first) first = result;
groups.push(...result.groups);
records.push(...result.records);
const itemCount = query.groupBy === 'record' ? records.length : groups.length;
if (itemCount > maxRows)
throw new Error(`全量读取超过 ${maxRows} 条保护上限,未生成不完整结果;请缩小日期范围后重新导出`);
if (!result.page.hasMore) {
return {
...first,
groups,
records,
page: {
...result.page,
page: 1,
pageSize,
itemCount,
hasMore: false,
pagesRead: page,
complete: true,
},
};
}
}
throw new Error(`全量读取超过 ${maxPages} 页保护上限,未生成不完整结果;请缩小日期范围后重新导出`);
}
/** Reusable contract for station-detail consumers that need every raw record. */
export function fetchAllH2BiDrillRecords(
query: Omit<H2BiDrillQuery, 'groupBy' | 'page' | 'pageSize'>,
options?: H2BiFullDrillOptions,
) {
return fetchAllH2BiDrill({ ...query, groupBy: 'record' }, options);
}