"use client"; import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Button, Card, Spinner } from "@heroui/react"; import Link from "next/link"; import { Thunderbolt, PlugConnection, CreditCard, ChartColumn, TagDollar, Person, ArrowRotateRight, TriangleExclamation, } from "@gravity-ui/icons"; import { AreaChart } from "@tremor/react"; import dayjs from "@/lib/dayjs"; import { useSession } from "@/lib/auth-client"; import { api, type Stats, type UserStats, type Transaction, type ChargePoint, type ChartRange, type ChartDataPoint, } from "@/lib/api"; // ── Helpers ──────────────────────────────────────────────────────────────── function timeAgo(dateStr: string | null | undefined): string { if (!dateStr) return "—"; return dayjs(dateStr).fromNow(); } function cpOnline(cp: ChargePoint): boolean { if (!cp.lastHeartbeatAt) return false; return dayjs().diff(dayjs(cp.lastHeartbeatAt), "second") < 120; } // ── StatCard ─────────────────────────────────────────────────────────────── type CardColor = "accent" | "success" | "warning" | "default"; const colorStyles: Record = { accent: { border: "border-accent", bg: "bg-accent/10", icon: "text-accent" }, success: { border: "border-success", bg: "bg-success/10", icon: "text-success" }, warning: { border: "border-warning", bg: "bg-warning/10", icon: "text-warning" }, default: { border: "border-border", bg: "bg-default", icon: "text-muted" }, }; function StatusDot({ color }: { color: "success" | "warning" | "muted" }) { const cls = color === "success" ? "bg-success" : color === "warning" ? "bg-warning" : "bg-muted/40"; return ; } function StatCard({ title, value, footer, icon: Icon, color = "default", }: { title: string; value: string | number; footer?: React.ReactNode; icon?: React.ComponentType<{ className?: string }>; color?: CardColor; }) { const s = colorStyles[color]; return (

{title}

{Icon && (
)}

{value}

{footer && (
{footer}
)}
); } // ── Panel wrapper ────────────────────────────────────────────────────────── function Panel({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } // ── TrendChart ───────────────────────────────────────────────────────────── function formatBucket(bucket: string, range: ChartRange): string { const d = dayjs(bucket); return range === "24h" ? d.format("HH:mm") : d.format("MM/DD"); } const RANGES: { label: string; value: ChartRange }[] = [ { label: "24 小时", value: "24h" }, { label: "7 天", value: "7d" }, { label: "30 天", value: "30d" }, ]; type ChartView = "economy" | "activity"; const SERIES_CONFIG = { revenue: { color: "sky" as const, label: "营收", unit: "¥", fmt: (v: number) => (v === 0 ? "¥0" : `¥${v.toFixed(v < 10 ? 2 : 1)}`), }, energyKwh: { color: "emerald" as const, label: "充电量", unit: "kWh", fmt: (v: number) => (v === 0 ? "0 kWh" : `${v.toFixed(v < 10 ? 2 : 1)} kWh`), }, transactions: { color: "violet" as const, label: "充电次数", unit: "次", fmt: (v: number) => `${v} 次`, }, utilizationPct: { color: "amber" as const, label: "利用率", unit: "%", fmt: (v: number) => `${v.toFixed(1)}%`, }, } as const; const VIEW_SERIES: Record = { economy: ["revenue", "energyKwh"], activity: ["transactions", "utilizationPct"], }; function TrendChart() { const [range, setRange] = useState("7d"); const [view, setView] = useState("economy"); const [hidden, setHidden] = useState>(new Set()); const toggle = (key: string) => setHidden((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); const switchView = (v: ChartView) => { setView(v); setHidden(new Set()); }; const allSeriesForView = VIEW_SERIES[view]; const visibleSeries = allSeriesForView.filter((k) => !hidden.has(k)); const visibleColors = visibleSeries.map((k) => SERIES_CONFIG[k].color); const { data, isPending } = useQuery({ queryKey: ["stats-chart", range], queryFn: () => api.stats.chart(range), staleTime: 60_000, }); return (
{/* Header */}
{/* View toggle */}
{(["economy", "activity"] as ChartView[]).map((v) => ( ))}
{/* Range toggle */}
{RANGES.map((r) => ( ))}
{/* Chart */}
{isPending ? (
) : ( ({ ...p, label: formatBucket(p.bucket, range) }))} index="label" categories={visibleSeries} colors={visibleColors} valueFormatter={(v) => `${v}`} customTooltip={(props) => { if (!props.active || !props.payload?.length) return null; return (

{props.label}

{props.payload.map((entry) => { const key = entry.dataKey as string; const cfg = SERIES_CONFIG[key as keyof typeof SERIES_CONFIG]; if (!cfg) return null; return (
{cfg.label} {cfg.fmt(entry.value as number)}
); })}
); }} showLegend={false} showGridLines={true} showYAxis={true} showXAxis={true} curveType="monotone" showAnimation className="h-56" /> )} {/* Legend */}
{allSeriesForView.map((key) => { const cfg = SERIES_CONFIG[key]; return ( ); })}
); } // ── RecentTransactions ──────────────────────────────────────────────────── function RecentTransactions({ txns }: { txns: Transaction[] }) { if (txns.length === 0) { return
暂无充电记录
; } return (
    {txns.map((tx) => { const active = !tx.stopTimestamp; const kwh = tx.energyWh != null ? (tx.energyWh / 1000).toFixed(2) : active ? "充电中…" : "—"; const amount = tx.chargeAmount != null && tx.chargeAmount > 0 ? `¥${(tx.chargeAmount / 100).toFixed(2)}` : active ? "—" : "免费"; return (
  • {tx.chargePointIdentifier ?? "—"} {tx.connectorNumber != null && ( #{tx.connectorNumber} )}

    {tx.idTag} {tx.idTagUserId && ( <> · {tx.idTagUserName ?? tx.idTagUserId} ({tx.idTagUserId.slice(0, 8)}) )}

    {kwh} kWh

    {amount}

    {timeAgo(tx.startTimestamp)}

  • ); })}
); } // ── ChargePointStatus ───────────────────────────────────────────────────── function ChargePointStatus({ cps }: { cps: ChargePoint[] }) { if (cps.length === 0) { return
暂无充电桩
; } return (
    {cps.map((cp) => { const online = cpOnline(cp); const chargingCount = cp.connectors.filter((c) => c.status === "Charging").length; const availableCount = cp.connectors.filter((c) => c.status === "Available").length; const faultedCount = cp.connectors.filter((c) => c.status === "Faulted").length; return (
  • {cp.chargePointIdentifier}

    {cp.chargePointModel ?? cp.chargePointVendor ?? "未知型号"}

    {online ? (
    {/* Charge point status */} {cp.chargePointStatus === "Faulted" && (
    整桩故障
    )} {/* Status summary */}
    {faultedCount > 0 && (
    {faultedCount}
    )} {chargingCount > 0 && (
    {chargingCount}
    )}
    {availableCount}
    ) : (
    离线
    )}
  • ); })}
); } // ── Page ─────────────────────────────────────────────────────────────────── export default function DashboardPage() { const { data: sessionData, isPending } = useSession(); const isAdmin = sessionData?.user?.role === "admin"; const { data, isPending: queryPending, isFetching: refreshing, refetch, } = useQuery({ queryKey: ["dashboard", isAdmin], queryFn: async () => { const [statsRes, txRes, cpsData] = await Promise.all([ api.stats.get(), api.transactions.list({ limit: 6 }), isAdmin ? api.chargePoints.list() : Promise.resolve([] as ChargePoint[]), ]); return { stats: statsRes, txns: txRes.data, cps: cpsData }; }, refetchInterval: 3_000, enabled: !isPending, }); if (isPending || queryPending) { return (

概览

加载中…

); } // ── Admin view ──────────────────────────────────────────────────────────── if (isAdmin) { const s = data?.stats as Stats | undefined; const todayKwh = s ? (s.todayEnergyWh / 1000).toFixed(1) : "—"; const todayRevenue = s ? `¥${(s.todayRevenue / 100).toFixed(2)}` : "—"; const offlineCount = (s?.totalChargePoints ?? 0) - (s?.onlineChargePoints ?? 0); return (

概览

实时运营状态

{/* Today's live metrics */}
当日 00:00 起累计} /> 当日累计输出电能} /> 已完成订单金额合计} /> {s?.activeTransactions ? "充电进行中" : "当前空闲"} } />
{/* Infrastructure metrics */}
{s?.onlineChargePoints ?? 0} 在线 · {offlineCount} 离线 } /> 已注册卡片总量} /> 系统用户总数} />
{/* Trend chart */} {isAdmin && } {/* Detail panels */}
); } // ── User view ───────────────────────────────────────────────────────────── const s = data?.stats as UserStats | undefined; const totalYuan = s ? (s.totalBalance / 100).toFixed(2) : "—"; const todayKwh = s ? (s.todayEnergyWh / 1000).toFixed(2) : "—"; return (

概览

{sessionData?.user?.name ?? sessionData?.user?.email} 的账户概览

已绑定的储值卡数量} /> 所有储值卡余额合计} /> {s?.activeTransactions ? "充电中" : "当前空闲"} } /> 今日 {s?.todayTransactions ?? 0} 次} />
); }