feat(api): 添加 MeterValue 展示支持,重置 OCPP 认证密钥二次确认
This commit is contained in:
@@ -2,7 +2,8 @@ import { Hono } from "hono";
|
||||
import { desc, eq, sql } from "drizzle-orm";
|
||||
import dayjs from "dayjs";
|
||||
import { useDrizzle } from "@/lib/db.js";
|
||||
import { chargePoint, connector } from "@/db/schema.js";
|
||||
import { chargePoint, connector, meterValue } from "@/db/schema.js";
|
||||
import type { SampledValue } from "@/db/schema.js";
|
||||
import { ocppConnections } from "@/ocpp/handler.js";
|
||||
import { generateOcppPassword, hashOcppPassword } from "@/lib/ocpp-auth.js";
|
||||
import type { HonoEnv } from "@/types/hono.ts";
|
||||
@@ -122,6 +123,7 @@ app.get("/connections", (c) => {
|
||||
app.get("/:id", async (c) => {
|
||||
const db = useDrizzle();
|
||||
const id = c.req.param("id");
|
||||
const isAdmin = c.get("user")?.role === "admin";
|
||||
|
||||
const [cp] = await db.select().from(chargePoint).where(eq(chargePoint.id, id)).limit(1);
|
||||
|
||||
@@ -130,12 +132,39 @@ app.get("/:id", async (c) => {
|
||||
const allConnectors = await db.select().from(connector).where(eq(connector.chargePointId, id));
|
||||
const cpStatus = allConnectors.find((conn) => conn.connectorId === 0);
|
||||
const displayConnectors = allConnectors.filter((conn) => conn.connectorId > 0);
|
||||
const [latestMeter] = await db
|
||||
.select({
|
||||
timestamp: meterValue.timestamp,
|
||||
sampledValues: meterValue.sampledValues,
|
||||
})
|
||||
.from(meterValue)
|
||||
.where(eq(meterValue.chargePointId, id))
|
||||
.orderBy(desc(meterValue.timestamp), desc(meterValue.receivedAt))
|
||||
.limit(1);
|
||||
|
||||
const meterHistory = isAdmin
|
||||
? (
|
||||
await db
|
||||
.select({
|
||||
connectorNumber: meterValue.connectorNumber,
|
||||
timestamp: meterValue.timestamp,
|
||||
sampledValues: meterValue.sampledValues,
|
||||
})
|
||||
.from(meterValue)
|
||||
.where(eq(meterValue.chargePointId, id))
|
||||
.orderBy(desc(meterValue.timestamp), desc(meterValue.receivedAt))
|
||||
.limit(24)
|
||||
).reverse()
|
||||
: [];
|
||||
|
||||
return c.json({
|
||||
...cp,
|
||||
connectors: displayConnectors,
|
||||
chargePointStatus: cpStatus?.status ?? null,
|
||||
chargePointErrorCode: cpStatus?.errorCode ?? null,
|
||||
latestMeterTimestamp: latestMeter?.timestamp ?? null,
|
||||
latestMeterValues: ((latestMeter?.sampledValues as SampledValue[] | undefined) ?? []),
|
||||
meterHistory,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,12 @@ import {
|
||||
Tooltip,
|
||||
} from "@heroui/react";
|
||||
import { ArrowLeft, Pencil, ArrowRotateRight, Key, Copy, Check } from "@gravity-ui/icons";
|
||||
import { api, type ChargePointPasswordReset } from "@/lib/api";
|
||||
import {
|
||||
api,
|
||||
type ChargePointPasswordReset,
|
||||
type MeterHistoryPoint,
|
||||
type MeterSampledValue,
|
||||
} from "@/lib/api";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import dayjs from "@/lib/dayjs";
|
||||
import InfoSection from "@/components/info-section";
|
||||
@@ -58,6 +63,8 @@ const registrationColorMap: Record<string, "success" | "warning" | "danger"> = {
|
||||
Rejected: "danger",
|
||||
};
|
||||
|
||||
const RESET_CONFIRM_TEXT = "我将重新配置设备";
|
||||
|
||||
const TX_LIMIT = 10;
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
@@ -75,6 +82,179 @@ function relativeTime(iso: string): string {
|
||||
return dayjs(iso).fromNow();
|
||||
}
|
||||
|
||||
function formatDateTime(iso: string | null | undefined): string {
|
||||
if (!iso) return "—";
|
||||
return dayjs(iso).format("YYYY/M/D HH:mm:ss");
|
||||
}
|
||||
|
||||
function extractMeterValue(sampledValues: MeterSampledValue[], measurands: string[]) {
|
||||
const parsedValues = sampledValues
|
||||
.map((sv) => {
|
||||
const numericValue = Number(sv.value);
|
||||
if (Number.isNaN(numericValue)) return null;
|
||||
return {
|
||||
value: numericValue,
|
||||
measurand: sv.measurand,
|
||||
phase: sv.phase,
|
||||
unit: sv.unit,
|
||||
};
|
||||
})
|
||||
.filter((sv): sv is NonNullable<typeof sv> => sv !== null);
|
||||
|
||||
const withoutPhase = parsedValues.find(
|
||||
(sv) =>
|
||||
((sv.measurand == null && measurands.includes("Energy.Active.Import.Register")) ||
|
||||
(sv.measurand != null && measurands.includes(sv.measurand))) &&
|
||||
!sv.phase,
|
||||
);
|
||||
if (withoutPhase) return withoutPhase;
|
||||
|
||||
return parsedValues.find(
|
||||
(sv) =>
|
||||
(sv.measurand == null && measurands.includes("Energy.Active.Import.Register")) ||
|
||||
(sv.measurand != null && measurands.includes(sv.measurand)),
|
||||
);
|
||||
}
|
||||
|
||||
function MeterCard({
|
||||
label,
|
||||
value,
|
||||
emphasis = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
emphasis?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-xl border border-border/70 px-3 py-2.5 ${
|
||||
emphasis ? "bg-surface-secondary" : "bg-surface"
|
||||
}`}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<dt className="text-xs font-medium tracking-wide text-muted">{label}</dt>
|
||||
<dd className="mt-1 truncate text-base font-semibold tabular-nums text-foreground">
|
||||
{value}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildMeterSnapshot(history: MeterHistoryPoint[]) {
|
||||
const latestPoint = history[history.length - 1];
|
||||
return {
|
||||
latestTimestamp: latestPoint?.timestamp ?? null,
|
||||
latestValues: latestPoint?.sampledValues ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function MeterChannelSection({
|
||||
connectorNumber,
|
||||
history,
|
||||
}: {
|
||||
connectorNumber: number;
|
||||
history: MeterHistoryPoint[];
|
||||
}) {
|
||||
const snapshot = buildMeterSnapshot(history);
|
||||
const meterVoltage = extractMeterValue(snapshot.latestValues, ["Voltage"]);
|
||||
const meterCurrent = extractMeterValue(snapshot.latestValues, ["Current.Import"]);
|
||||
const meterPower = extractMeterValue(snapshot.latestValues, ["Power.Active.Import"]);
|
||||
const meterPf = extractMeterValue(snapshot.latestValues, ["Power.Factor"]);
|
||||
const meterFrequency = extractMeterValue(snapshot.latestValues, ["Frequency"]);
|
||||
const meterTemperature = extractMeterValue(snapshot.latestValues, ["Temperature"]);
|
||||
const meterEnergyReg = extractMeterValue(snapshot.latestValues, [
|
||||
"Energy.Active.Import.Register",
|
||||
]);
|
||||
|
||||
return (
|
||||
<InfoSection title={`连接器 #${connectorNumber}`}>
|
||||
{history.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-border/70 bg-surface px-4 py-6 text-center">
|
||||
<div className="mx-auto flex size-10 items-center justify-center rounded-full bg-muted/10 text-muted">
|
||||
<Plug className="size-5" />
|
||||
</div>
|
||||
<p className="mt-3 text-sm font-medium text-foreground">
|
||||
暂未收到该连接器的 MeterValue 采样数据
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-5 text-muted">
|
||||
充电桩上报后,这里会自动显示电压、电流、功率等实时计量信息。
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-3 rounded-xl border border-border/70 bg-surface-secondary px-3 py-2">
|
||||
<div>
|
||||
<p className="text-xs font-medium tracking-wide text-muted">
|
||||
连接器 #{connectorNumber}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{formatDateTime(snapshot.latestTimestamp)}
|
||||
</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-accent/10 px-2 py-1 text-xs font-medium text-accent">
|
||||
最近 {history.length} 条
|
||||
</span>
|
||||
</div>
|
||||
<dl className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<MeterCard
|
||||
label="电压"
|
||||
value={
|
||||
meterVoltage ? `${meterVoltage.value.toFixed(1)} ${meterVoltage.unit ?? "V"}` : "—"
|
||||
}
|
||||
/>
|
||||
<MeterCard
|
||||
label="电流"
|
||||
value={
|
||||
meterCurrent ? `${meterCurrent.value.toFixed(2)} ${meterCurrent.unit ?? "A"}` : "—"
|
||||
}
|
||||
/>
|
||||
<MeterCard
|
||||
label="有功功率"
|
||||
value={meterPower ? `${meterPower.value.toFixed(0)} ${meterPower.unit ?? "W"}` : "—"}
|
||||
/>
|
||||
<MeterCard label="功率因数" value={meterPf ? meterPf.value.toFixed(3) : "—"} />
|
||||
<MeterCard
|
||||
label="频率"
|
||||
value={
|
||||
meterFrequency
|
||||
? `${meterFrequency.value.toFixed(1)} ${meterFrequency.unit ?? "Hz"}`
|
||||
: "—"
|
||||
}
|
||||
/>
|
||||
<MeterCard
|
||||
label="温度"
|
||||
value={
|
||||
meterTemperature
|
||||
? `${meterTemperature.value.toFixed(1)} ${meterTemperature.unit ?? "Celsius"}`
|
||||
: "—"
|
||||
}
|
||||
/>
|
||||
<div className="sm:col-span-2 xl:col-span-1">
|
||||
<MeterCard
|
||||
label="累计电能表读数"
|
||||
value={
|
||||
meterEnergyReg
|
||||
? `${meterEnergyReg.value.toFixed(3)} ${meterEnergyReg.unit ?? "Wh"}`
|
||||
: "—"
|
||||
}
|
||||
emphasis
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2 xl:col-span-2">
|
||||
<MeterCard
|
||||
label="采样时间"
|
||||
value={formatDateTime(snapshot.latestTimestamp)}
|
||||
emphasis
|
||||
/>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
</InfoSection>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Edit form type ─────────────────────────────────────────────────────────
|
||||
|
||||
type EditForm = {
|
||||
@@ -107,16 +287,25 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
});
|
||||
|
||||
// reset password
|
||||
const [resetConfirmOpen, setResetConfirmOpen] = useState(false);
|
||||
const [resetConfirmText, setResetConfirmText] = useState("");
|
||||
const [resetBusy, setResetBusy] = useState(false);
|
||||
const [resetResult, setResetResult] = useState<ChargePointPasswordReset | null>(null);
|
||||
const [resetCopied, setResetCopied] = useState(false);
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
const openResetConfirm = () => {
|
||||
setResetConfirmText("");
|
||||
setResetConfirmOpen(true);
|
||||
};
|
||||
|
||||
const handleConfirmResetPassword = async () => {
|
||||
if (!cp) return;
|
||||
setResetBusy(true);
|
||||
try {
|
||||
const result = await api.chargePoints.resetPassword(cp.id);
|
||||
setResetResult(result);
|
||||
setResetConfirmOpen(false);
|
||||
setResetConfirmText("");
|
||||
} finally {
|
||||
setResetBusy(false);
|
||||
}
|
||||
@@ -220,6 +409,16 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
}
|
||||
|
||||
const sortedConnectors = [...cp.connectors].sort((a, b) => a.connectorId - b.connectorId);
|
||||
const meterHistory = cp.meterHistory ?? [];
|
||||
const meterHistoryByConnector = meterHistory.reduce<Record<number, MeterHistoryPoint[]>>(
|
||||
(acc, row) => {
|
||||
if (!acc[row.connectorNumber]) acc[row.connectorNumber] = [];
|
||||
acc[row.connectorNumber].push(row);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const displayConnectors = sortedConnectors.filter((connector) => connector.connectorId > 0);
|
||||
|
||||
// ── Render ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -239,10 +438,10 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h1 className="text-2xl font-semibold text-foreground">
|
||||
{cp.deviceName ?? <span className="font-mono">{cp.chargePointIdentifier}</span>}
|
||||
{cp.deviceName ?? <span>{cp.chargePointIdentifier}</span>}
|
||||
</h1>
|
||||
{isAdmin && cp.deviceName && (
|
||||
<span className="font-mono text-sm text-muted">{cp.chargePointIdentifier}</span>
|
||||
<span className="text-sm text-muted">{cp.chargePointIdentifier}</span>
|
||||
)}
|
||||
<Chip
|
||||
color={registrationColorMap[cp.registrationStatus] ?? "warning"}
|
||||
@@ -252,9 +451,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
{cp.registrationStatus}
|
||||
</Chip>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={`size-2 rounded-full ${transportStatusDotClass}`}
|
||||
/>
|
||||
<span className={`size-2 rounded-full ${transportStatusDotClass}`} />
|
||||
<span className="text-xs text-muted">{statusLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -265,7 +462,14 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button isIconOnly size="sm" variant="ghost" isDisabled={refreshing} onPress={() => cpQuery.refetch()} aria-label="刷新">
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
isDisabled={refreshing}
|
||||
onPress={() => cpQuery.refetch()}
|
||||
aria-label="刷新"
|
||||
>
|
||||
<ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
@@ -273,7 +477,12 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
<Tooltip>
|
||||
<Tooltip.Content>重置 OCPP 认证密钥</Tooltip.Content>
|
||||
<Tooltip.Trigger>
|
||||
<Button size="sm" variant="ghost" isDisabled={resetBusy} onPress={handleResetPassword}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
isDisabled={resetBusy}
|
||||
onPress={openResetConfirm}
|
||||
>
|
||||
{resetBusy ? <Spinner size="sm" /> : <Key className="size-4" />}
|
||||
重置密钥
|
||||
</Button>
|
||||
@@ -340,6 +549,66 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isAdmin && (
|
||||
<Modal
|
||||
isOpen={resetConfirmOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!resetBusy) {
|
||||
setResetConfirmOpen(open);
|
||||
if (!open) setResetConfirmText("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Modal.Backdrop>
|
||||
<Modal.Container scroll="outside">
|
||||
<Modal.Dialog className="sm:max-w-md">
|
||||
<Modal.CloseTrigger />
|
||||
<Modal.Header>
|
||||
<Modal.Heading>重置 OCPP 认证密钥</Modal.Heading>
|
||||
</Modal.Header>
|
||||
<Modal.Body className="space-y-4">
|
||||
<p className="text-sm text-warning font-medium">
|
||||
重置后旧密钥将立即失效,请先确认设备已准备重新配置。
|
||||
</p>
|
||||
<TextField fullWidth>
|
||||
<Label className="text-sm font-normal">
|
||||
请输入 “
|
||||
<span className="font-medium cursor-text">{RESET_CONFIRM_TEXT}</span>”
|
||||
以继续
|
||||
</Label>
|
||||
<Input
|
||||
placeholder={RESET_CONFIRM_TEXT}
|
||||
value={resetConfirmText}
|
||||
onChange={(e) => setResetConfirmText(e.target.value)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</TextField>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setResetConfirmOpen(false);
|
||||
setResetConfirmText("");
|
||||
}}
|
||||
isDisabled={resetBusy}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
isDisabled={resetBusy || resetConfirmText !== RESET_CONFIRM_TEXT}
|
||||
onPress={handleConfirmResetPassword}
|
||||
>
|
||||
{resetBusy ? <Spinner size="sm" /> : "确认重置"}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog>
|
||||
</Modal.Container>
|
||||
</Modal.Backdrop>
|
||||
</Modal>
|
||||
)}
|
||||
</dl>
|
||||
</InfoSection>
|
||||
)}
|
||||
@@ -444,9 +713,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
<dt className="shrink-0 text-sm text-muted">充电桥状态</dt>
|
||||
<dd>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={`size-2 rounded-full ${transportStatusDotClass}`}
|
||||
/>
|
||||
<span className={`size-2 rounded-full ${transportStatusDotClass}`} />
|
||||
<span className="text-sm text-foreground">{statusLabel}</span>
|
||||
</div>
|
||||
</dd>
|
||||
@@ -483,10 +750,8 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
{conn.errorCode && conn.errorCode !== "NoError" && (
|
||||
<p className="text-xs text-danger">错误: {conn.errorCode}</p>
|
||||
)}
|
||||
{/* {conn.info && <p className="text-xs text-muted">{conn.info}</p>} */}
|
||||
<p className="text-xs text-muted">
|
||||
更新于{" "}
|
||||
{dayjs(conn.lastStatusAt).format("MM/DD HH:mm")}
|
||||
更新于 {dayjs(conn.lastStatusAt).format("MM/DD HH:mm")}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
@@ -494,6 +759,19 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* MeterValue */}
|
||||
{isAdmin && (
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{displayConnectors.map((connector) => (
|
||||
<MeterChannelSection
|
||||
key={connector.id}
|
||||
connectorNumber={connector.connectorId}
|
||||
history={meterHistoryByConnector[connector.connectorId] ?? []}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Transactions */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -600,7 +878,10 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
<Modal
|
||||
isOpen={resetResult !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) { setResetResult(null); setResetCopied(false); }
|
||||
if (!open) {
|
||||
setResetResult(null);
|
||||
setResetCopied(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Modal.Backdrop>
|
||||
@@ -626,9 +907,15 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onPress={() => resetResult && handleCopyResetPassword(resetResult.plainPassword)}
|
||||
onPress={() =>
|
||||
resetResult && handleCopyResetPassword(resetResult.plainPassword)
|
||||
}
|
||||
>
|
||||
{resetCopied ? <Check className="size-4 text-success" /> : <Copy className="size-4" />}
|
||||
{resetCopied ? (
|
||||
<Check className="size-4 text-success" />
|
||||
) : (
|
||||
<Copy className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
</Tooltip>
|
||||
@@ -636,7 +923,12 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="flex justify-end">
|
||||
<Button onPress={() => { setResetResult(null); setResetCopied(false); }}>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setResetResult(null);
|
||||
setResetCopied(false);
|
||||
}}
|
||||
>
|
||||
我已保存密钥
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
@@ -667,9 +959,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
<Input
|
||||
placeholder="1号楼A区01号桩"
|
||||
value={editForm.deviceName}
|
||||
onChange={(e) =>
|
||||
setEditForm((f) => ({ ...f, deviceName: e.target.value }))
|
||||
}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, deviceName: e.target.value }))}
|
||||
/>
|
||||
</TextField>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
@@ -744,17 +1034,17 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
||||
</Select>
|
||||
</div>
|
||||
{editForm.pricingMode === "fixed" && (
|
||||
<TextField fullWidth>
|
||||
<Label className="text-sm font-medium">固定电价(分/kWh)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
placeholder="0"
|
||||
value={editForm.feePerKwh}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, feePerKwh: e.target.value }))}
|
||||
/>
|
||||
</TextField>
|
||||
<TextField fullWidth>
|
||||
<Label className="text-sm font-medium">固定电价(分/kWh)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
placeholder="0"
|
||||
value={editForm.feePerKwh}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, feePerKwh: e.target.value }))}
|
||||
/>
|
||||
</TextField>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="flex justify-end gap-2">
|
||||
|
||||
@@ -74,6 +74,22 @@ export type ConnectionsStatus = {
|
||||
|
||||
export type ChargePointConnectionStatus = "online" | "unavailable" | "offline";
|
||||
|
||||
export type MeterSampledValue = {
|
||||
value: string;
|
||||
context?: string;
|
||||
format?: string;
|
||||
measurand?: string;
|
||||
phase?: string;
|
||||
location?: string;
|
||||
unit?: string;
|
||||
};
|
||||
|
||||
export type MeterHistoryPoint = {
|
||||
connectorNumber: number;
|
||||
timestamp: string;
|
||||
sampledValues: MeterSampledValue[];
|
||||
};
|
||||
|
||||
export type ChargePoint = {
|
||||
id: string;
|
||||
chargePointIdentifier: string;
|
||||
@@ -123,6 +139,9 @@ export type ChargePointDetail = {
|
||||
connectors: ConnectorDetail[];
|
||||
chargePointStatus: string | null;
|
||||
chargePointErrorCode: string | null;
|
||||
latestMeterTimestamp: string | null;
|
||||
latestMeterValues: MeterSampledValue[];
|
||||
meterHistory: MeterHistoryPoint[];
|
||||
};
|
||||
|
||||
export type Transaction = {
|
||||
|
||||
Reference in New Issue
Block a user