feat(csms): 添加 OCPP 鉴权

This commit is contained in:
2026-03-16 16:53:33 +08:00
parent 4885cf6778
commit cf0861f8f6
8 changed files with 328 additions and 21 deletions

View File

@@ -6,6 +6,7 @@ import {
Button,
Chip,
Input,
InputGroup,
Label,
ListBox,
Modal,
@@ -22,11 +23,13 @@ import {
TrashBin,
ArrowRotateRight,
QrCode,
Copy,
Check,
} from "@gravity-ui/icons";
import { QRCodeSVG } from "qrcode.react";
import Link from "next/link";
import { ScrollFade } from "@/components/scroll-fade";
import { api, type ChargePoint } from "@/lib/api";
import { api, type ChargePoint, type ChargePointCreated } from "@/lib/api";
import { useSession } from "@/lib/auth-client";
import dayjs from "@/lib/dayjs";
@@ -123,6 +126,8 @@ export default function ChargePointsPage() {
const [deleteTarget, setDeleteTarget] = useState<ChargePoint | null>(null);
const [deleting, setDeleting] = useState(false);
const [qrTarget, setQrTarget] = useState<ChargePoint | null>(null);
const [createdCp, setCreatedCp] = useState<ChargePointCreated | null>(null);
const [copied, setCopied] = useState(false);
const {
data: chargePoints = [],
refetch: refetchList,
@@ -168,9 +173,11 @@ export default function ChargePointsPage() {
feePerKwh: formData.pricingMode === "fixed" ? fee : 0,
deviceName: formData.deviceName.trim() || null,
});
await refetchList();
setFormOpen(false);
} else {
// Create
await api.chargePoints.create({
// Create — capture plainPassword for one-time display
const created = await api.chargePoints.create({
chargePointIdentifier: formData.chargePointIdentifier.trim(),
chargePointVendor: formData.chargePointVendor.trim() || undefined,
chargePointModel: formData.chargePointModel.trim() || undefined,
@@ -179,9 +186,10 @@ export default function ChargePointsPage() {
feePerKwh: formData.pricingMode === "fixed" ? fee : 0,
deviceName: formData.deviceName.trim() || undefined,
});
await refetchList();
setFormOpen(false);
setCreatedCp(created);
}
await refetchList();
setFormOpen(false);
} finally {
setFormBusy(false);
}
@@ -201,6 +209,13 @@ export default function ChargePointsPage() {
const isEdit = formTarget !== null;
const handleCopyPassword = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
const { data: sessionData } = useSession();
const isAdmin = sessionData?.user?.role === "admin";
@@ -268,9 +283,7 @@ export default function ChargePointsPage() {
<Input
placeholder="1号楼A区01号桩"
value={formData.deviceName}
onChange={(e) =>
setFormData((f) => ({ ...f, deviceName: e.target.value }))
}
onChange={(e) => setFormData((f) => ({ ...f, deviceName: e.target.value }))}
/>
</TextField>
<div className="grid grid-cols-2 gap-3">
@@ -444,6 +457,95 @@ export default function ChargePointsPage() {
</Modal>
)}
{/* OCPP Password Modal — shown once after creation */}
{isAdmin && (
<Modal
isOpen={createdCp !== null}
onOpenChange={(open) => {
if (!open) {
setCreatedCp(null);
setCopied(false);
}
}}
>
<Modal.Backdrop>
<Modal.Container scroll="outside">
<Modal.Dialog className="sm:max-w-md">
<Modal.Header>
<Modal.Heading></Modal.Heading>
</Modal.Header>
<Modal.Body className="space-y-4">
<p className="text-sm text-warning font-medium">
</p>
<TextField fullWidth isReadOnly>
<Label className="text-sm font-medium"></Label>
<Input value={createdCp?.chargePointIdentifier ?? ""} className="font-mono" />
</TextField>
<TextField fullWidth isReadOnly>
<Label className="text-sm font-medium">OCPP Basic Auth </Label>
<InputGroup>
<InputGroup.Input
value={createdCp?.plainPassword ?? ""}
className="font-mono select-all"
/>
<InputGroup.Suffix>
<Tooltip>
<Tooltip.Content>{copied ? "已复制" : "复制密码"}</Tooltip.Content>
<Tooltip.Trigger>
<Button
isIconOnly
size="sm"
variant="ghost"
onPress={() =>
createdCp && handleCopyPassword(createdCp.plainPassword)
}
>
{copied ? (
<Check className="size-4 text-success" />
) : (
<Copy className="size-4" />
)}
</Button>
</Tooltip.Trigger>
</Tooltip>
</InputGroup.Suffix>
</InputGroup>
</TextField>
<div className="space-y-1.5">
<TextField fullWidth isReadOnly>
<Label className="text-sm font-medium"> WebSocket </Label>
<Input
value={`wss://<your-server>/ocpp/${createdCp?.chargePointIdentifier ?? ""}`}
className="font-mono text-xs"
/>
</TextField>
<p className="text-xs text-muted">
HTTP
<br />
<code className="text-foreground">
Authorization: Basic &lt;base64({createdCp?.chargePointIdentifier}
:&lt;password&gt;)&gt;
</code>
</p>
</div>
</Modal.Body>
<Modal.Footer className="flex justify-end">
<Button
onPress={() => {
setCreatedCp(null);
setCopied(false);
}}
>
</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
)}
<Table>
<Table.ScrollContainer>
<Table.Content aria-label="充电桩列表" className="min-w-200">