Files
helios-evcs/apps/web/app/dashboard/page.tsx
2026-03-10 17:59:44 +08:00

214 lines
6.9 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { Card } from "@heroui/react";
import { Thunderbolt, PlugConnection, CreditCard, ChartColumn, TagDollar } from "@gravity-ui/icons";
import { useSession } from "@/lib/auth-client";
import { api, type Stats, type UserStats } from "@/lib/api";
type CardColor = "accent" | "success" | "warning" | "default";
const colorStyles: Record<CardColor, { border: string; bg: string; icon: string }> = {
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 <span className={`inline-block h-1.5 w-1.5 shrink-0 rounded-full ${cls}`} />;
}
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 (
<Card className={`border-t-2 ${s.border}`}>
<Card.Content className="flex flex-col gap-3">
<div className="flex items-start justify-between gap-2">
<p className="text-sm text-muted">{title}</p>
{Icon && (
<div className={`flex size-9 shrink-0 items-center justify-center rounded-xl ${s.bg}`}>
<Icon className={`size-4.5 ${s.icon}`} />
</div>
)}
</div>
<p className="text-3xl font-bold tabular-nums leading-none text-foreground">{value}</p>
{footer && (
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted">
{footer}
</div>
)}
</Card.Content>
</Card>
);
}
export default function DashboardPage() {
const { data: sessionData, isPending } = useSession();
const isAdmin = sessionData?.user?.role === "admin";
const [adminStats, setAdminStats] = useState<Stats | null>(null);
const [userStats, setUserStats] = useState<UserStats | null>(null);
useEffect(() => {
if (isPending) return;
api.stats
.get()
.then((data) => {
if ("todayEnergyWh" in data) {
setAdminStats(data);
return;
}
setUserStats(data);
})
.catch(() => {});
}, [isPending, isAdmin]);
if (isPending) {
return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-semibold text-foreground"></h1>
<p className="mt-0.5 text-sm text-muted"></p>
</div>
</div>
);
}
if (isAdmin) {
const stats = adminStats;
const todayKwh = stats ? (stats.todayEnergyWh / 1000).toFixed(1) : "—";
const offlineCount = (stats?.totalChargePoints ?? 0) - (stats?.onlineChargePoints ?? 0);
return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-semibold text-foreground"></h1>
<p className="mt-0.5 text-sm text-muted"></p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-5">
<StatCard
title="充电桩总数"
value={stats?.totalChargePoints ?? "—"}
icon={PlugConnection}
color="accent"
footer={
<>
<StatusDot color="success" />
<span className="font-medium text-success">
{stats?.onlineChargePoints ?? 0} 线
</span>
<span className="text-border">·</span>
<span>{offlineCount} 线</span>
</>
}
/>
<StatCard
title="在线充电桩"
value={stats?.onlineChargePoints ?? "—"}
icon={PlugConnection}
color="success"
footer={<span> 2 </span>}
/>
<StatCard
title="进行中充电"
value={stats?.activeTransactions ?? "—"}
icon={Thunderbolt}
color={stats?.activeTransactions ? "warning" : "default"}
footer={
<>
<StatusDot color={stats?.activeTransactions ? "success" : "muted"} />
<span className={stats?.activeTransactions ? "font-medium text-success" : ""}>
{stats?.activeTransactions ? "活跃中" : "当前空闲"}
</span>
</>
}
/>
<StatCard
title="储值卡总数"
value={stats?.totalIdTags ?? "—"}
icon={CreditCard}
color="default"
footer={<span></span>}
/>
<StatCard
title="今日充电量"
value={`${todayKwh} kWh`}
icon={ChartColumn}
color="accent"
footer={<span> 00:00 </span>}
/>
</div>
</div>
);
}
// User view
const stats = userStats;
const totalYuan = stats ? (stats.totalBalance / 100).toFixed(2) : "—";
return (
<div className="space-y-6">
<div>
<h1 className="text-xl font-semibold text-foreground"></h1>
<p className="mt-0.5 text-sm text-muted">
{sessionData?.user?.name ?? sessionData?.user?.email}
</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<StatCard
title="我的储值卡"
value={stats?.totalIdTags ?? "—"}
icon={CreditCard}
color="accent"
footer={<span></span>}
/>
<StatCard
title="账户总余额"
value={`¥${totalYuan}`}
icon={TagDollar}
color="success"
footer={<span></span>}
/>
<StatCard
title="进行中充电"
value={stats?.activeTransactions ?? "—"}
icon={Thunderbolt}
color={stats?.activeTransactions ? "warning" : "default"}
footer={
<>
<StatusDot color={stats?.activeTransactions ? "success" : "muted"} />
<span className={stats?.activeTransactions ? "font-medium text-success" : ""}>
{stats?.activeTransactions ? "充电中" : "当前空闲"}
</span>
</>
}
/>
<StatCard
title="累计充电次数"
value={stats?.totalTransactions ?? "—"}
icon={ChartColumn}
color="default"
footer={<span></span>}
/>
</div>
</div>
);
}