feat(api): add stats chart endpoint for admin access with time series data

feat(dayjs): integrate dayjs for date handling and formatting across the application
refactor(routes): update date handling in id-tags, transactions, users, and dashboard routes to use dayjs
style(globals): improve CSS variable definitions for better readability and consistency
deps: add dayjs as a dependency for date manipulation
This commit is contained in:
2026-03-11 21:34:21 +08:00
parent 73f0c6243a
commit 02a361488b
27 changed files with 502 additions and 110 deletions

View File

@@ -19,6 +19,7 @@ import {
import { ArrowLeft, Pencil, PlugConnection, ArrowRotateRight } from "@gravity-ui/icons";
import { api } from "@/lib/api";
import { useSession } from "@/lib/auth-client";
import dayjs from "@/lib/dayjs";
// ── Status maps ────────────────────────────────────────────────────────────
@@ -60,8 +61,7 @@ const TX_LIMIT = 10;
function formatDuration(start: string, stop: string | null): string {
if (!stop) return "进行中";
const ms = new Date(stop).getTime() - new Date(start).getTime();
const min = Math.floor(ms / 60000);
const min = dayjs(stop).diff(dayjs(start), "minute");
if (min < 60) return `${min} 分钟`;
const h = Math.floor(min / 60);
const m = min % 60;
@@ -69,15 +69,7 @@ function formatDuration(start: string, stop: string | null): string {
}
function relativeTime(iso: string): string {
const diff = Date.now() - new Date(iso).getTime();
const s = Math.floor(diff / 1000);
if (s < 60) return `${s} 秒前`;
const m = Math.floor(s / 60);
if (m < 60) return `${m} 分钟前`;
const h = Math.floor(m / 60);
if (h < 24) return `${h} 小时前`;
const d = Math.floor(h / 24);
return `${d} 天前`;
return dayjs(iso).fromNow();
}
// ── Edit form type ─────────────────────────────────────────────────────────
@@ -155,7 +147,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
// Online if last heartbeat within 3× interval
const isOnline =
cp?.lastHeartbeatAt != null &&
Date.now() - new Date(cp.lastHeartbeatAt).getTime() < (cp.heartbeatInterval ?? 60) * 3 * 1000;
dayjs().diff(dayjs(cp.lastHeartbeatAt), "second") < (cp.heartbeatInterval ?? 60) * 3;
const { data: sessionData } = useSession();
const isAdmin = sessionData?.user?.role === "admin";
@@ -343,7 +335,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
<dt className="shrink-0 text-sm text-muted"></dt>
<dd className="text-right text-sm text-foreground">
{cp.lastHeartbeatAt ? (
<span title={new Date(cp.lastHeartbeatAt).toLocaleString("zh-CN")}>
<span title={dayjs(cp.lastHeartbeatAt).format("YYYY/M/D HH:mm:ss")}>
{relativeTime(cp.lastHeartbeatAt)}
</span>
) : (
@@ -355,7 +347,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
<dt className="shrink-0 text-sm text-muted"></dt>
<dd className="text-right text-sm text-foreground">
{cp.lastBootNotificationAt ? (
<span title={new Date(cp.lastBootNotificationAt).toLocaleString("zh-CN")}>
<span title={dayjs(cp.lastBootNotificationAt).format("YYYY/M/D HH:mm:ss")}>
{relativeTime(cp.lastBootNotificationAt)}
</span>
) : (
@@ -366,7 +358,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
<div className="flex items-center justify-between gap-4 py-2">
<dt className="shrink-0 text-sm text-muted"></dt>
<dd className="text-sm text-foreground">
{new Date(cp.createdAt).toLocaleDateString("zh-CN")}
{dayjs(cp.createdAt).format("YYYY/M/D")}
</dd>
</div>
</dl>
@@ -437,12 +429,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
{conn.info && <p className="text-xs text-muted">{conn.info}</p>}
<p className="text-xs text-muted">
{" "}
{new Date(conn.lastStatusAt).toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})}
{dayjs(conn.lastStatusAt).format("MM/DD HH:mm")}
</p>
</div>
))}
@@ -489,12 +476,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
</Table.Cell>
<Table.Cell className="font-mono">{tx.idTag}</Table.Cell>
<Table.Cell className="tabular-nums text-sm">
{new Date(tx.startTimestamp).toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})}
{dayjs(tx.startTimestamp).format("MM/DD HH:mm")}
</Table.Cell>
<Table.Cell>{formatDuration(tx.startTimestamp, tx.stopTimestamp)}</Table.Cell>
<Table.Cell className="tabular-nums">

View File

@@ -27,6 +27,7 @@ import Link from "next/link";
import { ScrollFade } from "@/components/scroll-fade";
import { api, type ChargePoint } from "@/lib/api";
import { useSession } from "@/lib/auth-client";
import dayjs from "@/lib/dayjs";
const statusLabelMap: Record<string, string> = {
Available: "空闲中",
@@ -473,7 +474,7 @@ export default function ChargePointsPage() {
</Table.Cell>
<Table.Cell>
{cp.lastHeartbeatAt ? (
new Date(cp.lastHeartbeatAt).toLocaleString("zh-CN")
dayjs(cp.lastHeartbeatAt).format("YYYY/M/D HH:mm:ss")
) : (
<span className="text-muted"></span>
)}