Files
helios-evcs/apps/web/lib/api.ts
Timothy Yin 0118dd2e15 feat(web): 添加拓扑图页面和相关组件
feat(csms): 添加获取当前连接状态的API
feat(csms): 添加获取当前活动OCPP WebSocket连接的接口
deps(web): 添加@xyflow/react依赖
2026-03-16 12:59:05 +08:00

332 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export class APIError extends Error {
constructor(
public readonly status: number,
message: string,
) {
super(message);
this.name = "APIError";
}
}
const CSMS_URL = process.env.NEXT_PUBLIC_CSMS_URL ?? "http://localhost:3001";
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${CSMS_URL}${path}`, {
...init,
headers: {
"Content-Type": "application/json",
...init?.headers,
},
credentials: "include",
});
if (!res.ok) {
const text = await res.text().catch(() => res.statusText);
throw new APIError(res.status, `API ${path} failed (${res.status}): ${text}`);
}
return res.json() as Promise<T>;
}
// ── Types ──────────────────────────────────────────────────────────────────
export type Stats = {
totalChargePoints: number;
onlineChargePoints: number;
activeTransactions: number;
totalIdTags: number;
todayEnergyWh: number;
todayRevenue: number;
totalUsers: number;
todayTransactions: number;
};
export type UserStats = {
totalIdTags: number;
totalBalance: number;
activeTransactions: number;
totalTransactions: number;
todayEnergyWh: number;
todayTransactions: number;
};
export type ConnectorSummary = {
id: string;
connectorId: number;
status: string;
lastStatusAt: string | null;
};
export type ConnectorDetail = {
id: string;
connectorId: number;
status: string;
errorCode: string;
info: string | null;
vendorId: string | null;
vendorErrorCode: string | null;
lastStatusAt: string;
createdAt: string;
updatedAt: string;
};
export type ConnectionsStatus = {
connectedIdentifiers: string[];
};
export type ChargePoint = {
id: string;
chargePointIdentifier: string;
chargePointVendor: string | null;
chargePointModel: string | null;
registrationStatus: string;
lastHeartbeatAt: string | null;
lastBootNotificationAt: string | null;
feePerKwh: number;
pricingMode: "fixed" | "tou";
connectors: ConnectorSummary[];
chargePointStatus: string | null;
chargePointErrorCode: string | null;
};
export type ChargePointDetail = {
id: string;
chargePointIdentifier: string;
chargePointVendor: string | null;
chargePointModel: string | null;
chargePointSerialNumber: string | null;
firmwareVersion: string | null;
iccid: string | null;
imsi: string | null;
meterSerialNumber: string | null;
meterType: string | null;
registrationStatus: string;
heartbeatInterval: number | null;
lastHeartbeatAt: string | null;
lastBootNotificationAt: string | null;
feePerKwh: number;
pricingMode: "fixed" | "tou";
createdAt: string;
updatedAt: string;
connectors: ConnectorDetail[];
chargePointStatus: string | null;
chargePointErrorCode: string | null;
};
export type Transaction = {
id: number;
chargePointIdentifier: string | null;
connectorNumber: number | null;
idTag: string;
idTagStatus: string | null;
idTagUserId: string | null;
idTagUserName: string | null;
startTimestamp: string;
stopTimestamp: string | null;
startMeterValue: number | null;
stopMeterValue: number | null;
energyWh: number | null;
liveEnergyWh: number | null;
estimatedCost: number | null;
stopIdTag: string | null;
stopReason: string | null;
chargeAmount: number | null;
electricityFee: number | null;
serviceFee: number | null;
};
export type IdTag = {
idTag: string;
status: string;
expiryDate: string | null;
parentIdTag: string | null;
userId: string | null;
balance: number;
cardLayout: "center" | "around" | null;
cardSkin: "line" | "circles" | "glow" | "vip" | "redeye" | null;
createdAt: string;
};
export type UserRow = {
id: string;
name: string | null;
email: string;
emailVerified: boolean;
username: string | null;
role: string | null;
banned: boolean | null;
banReason: string | null;
createdAt: string;
};
export type PaginatedTransactions = {
data: Transaction[];
total: number;
page: number;
totalPages: number;
};
export type PriceTier = "peak" | "valley" | "flat";
export type TariffSlot = {
start: number;
end: number;
tier: PriceTier;
};
export type TierPricing = {
/** 电价(元/kWh */
electricityPrice: number;
/** 服务费(元/kWh */
serviceFee: number;
};
export type TariffConfig = {
id?: string;
slots: TariffSlot[];
prices: Record<PriceTier, TierPricing>;
createdAt?: string;
updatedAt?: string;
};
// ── API functions ──────────────────────────────────────────────────────────
export type ChartRange = "30d" | "7d" | "24h";
export type ChartDataPoint = {
bucket: string;
energyKwh: number;
revenue: number;
transactions: number;
utilizationPct: number;
};
export const api = {
stats: {
get: () => apiFetch<Stats | UserStats>("/api/stats"),
chart: (range: ChartRange) =>
apiFetch<ChartDataPoint[]>(`/api/stats/chart?range=${range}`),
},
chargePoints: {
list: () => apiFetch<ChargePoint[]>("/api/charge-points"),
get: (id: string) => apiFetch<ChargePointDetail>(`/api/charge-points/${id}`),
connections: () => apiFetch<ConnectionsStatus>("/api/charge-points/connections"),
create: (data: {
chargePointIdentifier: string;
chargePointVendor?: string;
chargePointModel?: string;
registrationStatus?: "Accepted" | "Pending" | "Rejected";
feePerKwh?: number;
pricingMode?: "fixed" | "tou";
}) =>
apiFetch<ChargePoint>("/api/charge-points", {
method: "POST",
body: JSON.stringify(data),
}),
update: (
id: string,
data: {
feePerKwh?: number;
pricingMode?: "fixed" | "tou";
registrationStatus?: "Accepted" | "Pending" | "Rejected";
chargePointVendor?: string;
chargePointModel?: string;
},
) =>
apiFetch<ChargePoint>(`/api/charge-points/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
}),
delete: (id: string) =>
apiFetch<{ success: true }>(`/api/charge-points/${id}`, { method: "DELETE" }),
},
transactions: {
list: (params?: {
page?: number;
limit?: number;
status?: "active" | "completed";
chargePointId?: string;
}) => {
const q = new URLSearchParams();
if (params?.page) q.set("page", String(params.page));
if (params?.limit) q.set("limit", String(params.limit));
if (params?.status) q.set("status", params.status);
if (params?.chargePointId) q.set("chargePointId", params.chargePointId);
const qs = q.toString();
return apiFetch<PaginatedTransactions>(`/api/transactions${qs ? "?" + qs : ""}`);
},
get: (id: number) => apiFetch<Transaction>(`/api/transactions/${id}`),
stop: (id: number) =>
apiFetch<Transaction & { online: boolean }>(`/api/transactions/${id}/stop`, {
method: "POST",
}),
remoteStart: (data: { chargePointIdentifier: string; connectorId: number; idTag: string }) =>
apiFetch<{ success: true }>("/api/transactions/remote-start", {
method: "POST",
body: JSON.stringify(data),
}),
delete: (id: number) =>
apiFetch<{ success: true }>(`/api/transactions/${id}`, { method: "DELETE" }),
},
idTags: {
list: () => apiFetch<IdTag[]>("/api/id-tags"),
get: (idTag: string) => apiFetch<IdTag>(`/api/id-tags/${idTag}`),
claim: () => apiFetch<IdTag>("/api/id-tags/claim", { method: "POST" }),
create: (data: {
idTag: string;
status?: string;
expiryDate?: string;
parentIdTag?: string;
userId?: string | null;
balance?: number;
cardLayout?: "center" | "around";
cardSkin?: "line" | "circles" | "glow" | "vip" | "redeye";
}) => apiFetch<IdTag>("/api/id-tags", { method: "POST", body: JSON.stringify(data) }),
update: (
idTag: string,
data: {
status?: string;
expiryDate?: string | null;
parentIdTag?: string | null;
userId?: string | null;
balance?: number;
cardLayout?: "center" | "around" | null;
cardSkin?: "line" | "circles" | "glow" | "vip" | "redeye" | null;
},
) => apiFetch<IdTag>(`/api/id-tags/${idTag}`, { method: "PATCH", body: JSON.stringify(data) }),
delete: (idTag: string) =>
apiFetch<{ success: true }>(`/api/id-tags/${idTag}`, { method: "DELETE" }),
},
users: {
list: () => apiFetch<UserRow[]>("/api/users"),
create: (data: {
name: string;
email: string;
password: string;
username?: string;
role?: string;
}) =>
apiFetch<{ user: UserRow }>("/api/auth/admin/create-user", {
method: "POST",
body: JSON.stringify(data),
}),
update: (
id: string,
data: {
name?: string;
username?: string | null;
role?: string;
banned?: boolean;
banReason?: string | null;
},
) => apiFetch<UserRow>(`/api/users/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
},
setup: {
create: (data: { name: string; email: string; username: string; password: string }) =>
apiFetch<{ success: boolean }>("/api/setup", { method: "POST", body: JSON.stringify(data) }),
},
tariff: {
get: () => apiFetch<TariffConfig | null>("/api/tariff"),
put: (data: { slots: TariffSlot[]; prices: Record<PriceTier, TierPricing> }) =>
apiFetch<TariffConfig>("/api/tariff", { method: "PUT", body: JSON.stringify(data) }),
},
};