feat(web): add remote start transaction feature and QR code scanning for charging

This commit is contained in:
2026-03-11 18:09:00 +08:00
parent 8ee2378c78
commit 73f0c6243a
6 changed files with 822 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import {
Button,
@@ -14,7 +14,15 @@ import {
Table,
TextField,
} from "@heroui/react";
import { Plus, Pencil, PlugConnection, TrashBin, ArrowRotateRight } from "@gravity-ui/icons";
import {
Plus,
Pencil,
PlugConnection,
TrashBin,
ArrowRotateRight,
QrCode,
} 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";
@@ -108,6 +116,7 @@ export default function ChargePointsPage() {
const [formBusy, setFormBusy] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<ChargePoint | null>(null);
const [deleting, setDeleting] = useState(false);
const [qrTarget, setQrTarget] = useState<ChargePoint | null>(null);
const {
data: chargePoints = [],
refetch: refetchList,
@@ -183,6 +192,11 @@ export default function ChargePointsPage() {
const { data: sessionData } = useSession();
const isAdmin = sessionData?.user?.role === "admin";
const [qrOrigin, setQrOrigin] = useState("");
useEffect(() => {
setQrOrigin(window.location.origin);
}, []);
return (
<div className="space-y-4">
<div className="flex flex-wrap items-start justify-between gap-3">
@@ -319,6 +333,65 @@ export default function ChargePointsPage() {
</>
)}
{/* QR Code Modal */}
{isAdmin && (
<Modal
isOpen={qrTarget !== null}
onOpenChange={(open) => {
if (!open) setQrTarget(null);
}}
>
<Modal.Backdrop>
<Modal.Container scroll="outside">
<Modal.Dialog className="sm:max-w-lg">
<Modal.CloseTrigger />
<Modal.Header>
<Modal.Heading>{qrTarget?.chargePointIdentifier} </Modal.Heading>
</Modal.Header>
<Modal.Body className="space-y-4">
<p className="text-sm text-muted">
</p>
{qrTarget &&
qrTarget.connectors.filter((c) => c.connectorId > 0).length === 0 && (
<p className="text-sm text-muted">
线
</p>
)}
<div className="grid gap-4 grid-cols-2 sm:grid-cols-3">
{qrTarget?.connectors
.filter((c) => c.connectorId > 0)
.sort((a, b) => a.connectorId - b.connectorId)
.map((conn) => {
const url = `${qrOrigin}/dashboard/charge?cpId=${qrTarget.id}&connector=${conn.connectorId}`;
return (
<div
key={conn.id}
className="flex flex-col items-center gap-2 rounded-xl border border-border p-3"
>
<p className="text-xs font-medium text-foreground">
#{conn.connectorId}
</p>
<QRCodeSVG value={url} size={120} className="rounded" />
<p className="break-all text-center font-mono text-[9px] text-muted leading-tight">
{url}
</p>
</div>
);
})}
</div>
</Modal.Body>
<Modal.Footer className="flex justify-end">
<Button variant="ghost" onPress={() => setQrTarget(null)}>
</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Backdrop>
</Modal>
)}
<Table>
<Table.ScrollContainer>
<Table.Content aria-label="充电桩列表" className="min-w-200">
@@ -435,6 +508,15 @@ export default function ChargePointsPage() {
>
<Pencil className="size-4" />
</Button>
<Button
isIconOnly
size="sm"
variant="tertiary"
onPress={() => setQrTarget(cp)}
aria-label="查看二维码"
>
<QrCode className="size-4" />
</Button>
<Modal>
<Button
isIconOnly