"use client"; import { useState, useEffect, useRef, Fragment } from "react"; import { useSearchParams } from "next/navigation"; import { useQuery, useMutation } from "@tanstack/react-query"; import { Button, Chip, Modal, Spinner } from "@heroui/react"; import { ThunderboltFill, PlugConnection, CreditCard, Check, QrCode, Xmark, } from "@gravity-ui/icons"; import { api } from "@/lib/api"; // ── Status maps (same as charge-points page) ──────────────────────────────── const statusLabelMap: Record = { Available: "空闲中", Charging: "充电中", Preparing: "准备中", Finishing: "结束中", SuspendedEV: "EV 暂停", SuspendedEVSE: "EVSE 暂停", Reserved: "已预约", Faulted: "故障", Unavailable: "不可用", Occupied: "占用", }; const statusDotClass: Record = { Available: "bg-success", Charging: "bg-accent animate-pulse", Preparing: "bg-warning animate-pulse", Finishing: "bg-warning", SuspendedEV: "bg-warning", SuspendedEVSE: "bg-warning", Reserved: "bg-warning", Faulted: "bg-danger", Unavailable: "bg-danger", Occupied: "bg-warning", }; // ── Step indicator ─────────────────────────────────────────────────────────── function StepBar({ step, onGoBack }: { step: number; onGoBack: (target: number) => void }) { const labels = ["选择充电桩", "选择充电口", "选择储值卡"]; return (
{labels.map((label, i) => { const idx = i + 1; const isActive = step === idx; const isDone = idx < step; const isLast = i === labels.length - 1; return ( {!isLast && (
)}
); })}
); } // ── QR Scanner ─────────────────────────────────────────────────────────────── type ScannerProps = { onResult: (raw: string) => void; onClose: () => void; }; function QrScanner({ onResult, onClose }: ScannerProps) { const videoRef = useRef(null); const streamRef = useRef(null); const scanningRef = useRef(true); const mountedRef = useRef(true); const [error, setError] = useState(null); useEffect(() => { mountedRef.current = true; scanningRef.current = true; let detector: any = null; async function start() { let stream: MediaStream; try { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" }, }); } catch (err: any) { if (mountedRef.current) setError("无法访问摄像头:" + (err?.message ?? "未知错误")); return; } if (!mountedRef.current) { stream.getTracks().forEach((t) => t.stop()); return; } streamRef.current = stream; if (videoRef.current) { videoRef.current.srcObject = stream; try { await videoRef.current.play(); } catch (err: any) { // AbortError fires when the element is removed mid-play (e.g. Modal animation). // It is not a real error — just bail out silently. if (err?.name === "AbortError") return; if (mountedRef.current) setError("无法播放摄像头画面:" + (err?.message ?? "未知错误")); return; } } if (!mountedRef.current) return; if (!("BarcodeDetector" in window)) { if (mountedRef.current) setError("当前浏览器不支持实时扫描,请升级至最新版本"); return; } detector = new (window as any).BarcodeDetector({ formats: ["qr_code"] }); const scan = async () => { if (!scanningRef.current || !videoRef.current) return; try { const codes: Array<{ rawValue: string }> = await detector.detect(videoRef.current); if (codes.length > 0) { onResult(codes[0].rawValue); return; } } catch {} requestAnimationFrame(scan); }; requestAnimationFrame(scan); } start(); return () => { mountedRef.current = false; scanningRef.current = false; streamRef.current?.getTracks().forEach((t) => t.stop()); streamRef.current = null; }; }, [onResult]); return (
{error ? (

{error}

) : ( <> {/* eslint-disable-next-line jsx-a11y/media-has-caption */}