diff --git a/apps/web/app/dashboard/charge-points/[id]/page.tsx b/apps/web/app/dashboard/charge-points/[id]/page.tsx
index 4ea3063..a5314dc 100644
--- a/apps/web/app/dashboard/charge-points/[id]/page.tsx
+++ b/apps/web/app/dashboard/charge-points/[id]/page.tsx
@@ -16,10 +16,11 @@ import {
Table,
TextField,
} from "@heroui/react";
-import { ArrowLeft, Pencil, PlugConnection, ArrowRotateRight } from "@gravity-ui/icons";
+import { ArrowLeft, Pencil, ArrowRotateRight } from "@gravity-ui/icons";
import { api } from "@/lib/api";
import { useSession } from "@/lib/auth-client";
import dayjs from "@/lib/dayjs";
+import InfoSection from "@/components/info-section";
import { Plug } from "lucide-react";
// ── Status maps ────────────────────────────────────────────────────────────
@@ -270,8 +271,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
{/* Device info — admin only */}
{isAdmin && (
-
-
设备信息
+
{[
{ label: "品牌", value: cp.chargePointVendor },
@@ -291,13 +291,12 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
))}
-
+
)}
{/* Operation info — admin only */}
{isAdmin && (
-
-
运行配置
+
- 注册状态
@@ -369,13 +368,12 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
-
+
)}
{/* Fee info — user only */}
{!isAdmin && (
-
-
电价信息
+
- 单位电价
@@ -404,7 +402,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
-
+
)}
diff --git a/apps/web/app/dashboard/page.tsx b/apps/web/app/dashboard/page.tsx
index 56def03..42f7418 100644
--- a/apps/web/app/dashboard/page.tsx
+++ b/apps/web/app/dashboard/page.tsx
@@ -2,15 +2,13 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
-import { Button, Card, Spinner } from "@heroui/react";
+import { Button, Spinner } from "@heroui/react";
import Link from "next/link";
import {
Thunderbolt,
- PlugConnection,
CreditCard,
ChartColumn,
TagDollar,
- Person,
ArrowRotateRight,
TriangleExclamation,
} from "@gravity-ui/icons";
@@ -27,6 +25,7 @@ import {
type ChartDataPoint,
} from "@/lib/api";
import { BanknoteArrowDown, EvCharger, Plug, Users } from "lucide-react";
+import MetricIndicator from "@/components/metric-indicator";
// ── Helpers ────────────────────────────────────────────────────────────────
@@ -72,24 +71,20 @@ function StatCard({
}) {
const s = colorStyles[color];
return (
-
-
-
-
{title}
- {Icon && (
-
-
-
- )}
-
- {value}
- {footer && (
-
- {footer}
+
+
- )}
-
-
+ ) : undefined
+ }
+ footer={footer}
+ />
);
}
diff --git a/apps/web/app/dashboard/transactions/[id]/page.tsx b/apps/web/app/dashboard/transactions/[id]/page.tsx
index 4a40a69..b2d969d 100644
--- a/apps/web/app/dashboard/transactions/[id]/page.tsx
+++ b/apps/web/app/dashboard/transactions/[id]/page.tsx
@@ -4,11 +4,35 @@ import { use, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
-import { Button, Chip, Modal, Spinner } from "@heroui/react";
+import { Alert, Button, Chip, Modal, Spinner } from "@heroui/react";
import { ArrowLeft, ArrowRotateRight, TrashBin } from "@gravity-ui/icons";
import { APIError, api } from "@/lib/api";
import { useSession } from "@/lib/auth-client";
import dayjs from "@/lib/dayjs";
+import InfoSection from "@/components/info-section";
+import MetricIndicator from "@/components/metric-indicator";
+import { BanknoteArrowUp, Clock, EvCharger } from "lucide-react";
+
+const stopReasonLabelMap: Record = {
+ EmergencyStop: "紧急停止",
+ EVDisconnected: "车辆断开",
+ HardReset: "硬重启",
+ Local: "本地结束",
+ Other: "其他原因",
+ PowerLoss: "断电结束",
+ Reboot: "重启结束",
+ Remote: "远程结束",
+ SoftReset: "软重启",
+ UnlockCommand: "解锁结束",
+ DeAuthorized: "鉴权拒绝",
+};
+
+const idTagRejectLabelMap: Record = {
+ Blocked: "卡片不可用或余额不足",
+ Expired: "卡片已过期",
+ Invalid: "卡片无效",
+ ConcurrentTx: "该卡已有进行中的订单",
+};
function formatDuration(start: string, stop: string | null): string {
if (!stop) return "进行中";
@@ -128,6 +152,14 @@ export default function TransactionDetailPage({ params }: { params: Promise<{ id
const amountFen = tx.chargeAmount ?? tx.estimatedCost;
const isEstimatedEnergy = tx.energyWh == null && tx.liveEnergyWh != null;
const isEstimatedAmount = tx.chargeAmount == null && tx.estimatedCost != null;
+ const isRejected = tx.stopReason === "DeAuthorized";
+ const stopReasonLabel = tx.stopReason
+ ? (stopReasonLabelMap[tx.stopReason] ?? tx.stopReason)
+ : "—";
+ const rejectReason =
+ tx.idTagStatus && tx.idTagStatus !== "Accepted"
+ ? (idTagRejectLabelMap[tx.idTagStatus] ?? tx.idTagStatus)
+ : "鉴权失败";
return (
@@ -142,8 +174,12 @@ export default function TransactionDetailPage({ params }: { params: Promise<{ id
-
充电订单#{tx.id}
- {tx.stopTimestamp ? (
+ 订单 #{tx.id}
+ {isRejected ? (
+
+ 已拒绝
+
+ ) : tx.stopTimestamp ? (
已完成
@@ -157,6 +193,11 @@ export default function TransactionDetailPage({ params }: { params: Promise<{ id
费用预估
)}
+ {tx.stopReason && !isRejected && (
+
+ {stopReasonLabel}
+
+ )}
开始于 {formatDateTime(tx.startTimestamp)} · {tx.stopTimestamp ? "时长 " : ""}
@@ -251,118 +292,156 @@ export default function TransactionDetailPage({ params }: { params: Promise<{ id
+
+
+
+
+ {isRejected ? "交易已被系统拒绝" : tx.stopTimestamp ? "本次交易已结束" : "充电进行中"}
+
+
+ {isRejected
+ ? rejectReason
+ : tx.stopTimestamp
+ ? `结束原因:${stopReasonLabel}`
+ : "充电进行中,实际充电量和费用以结束时系统计算为准。"}
+
+
+
+
-
-
充电量
-
-
{formatEnergy(energyWh)}
- {isEstimatedEnergy && (
-
- 预估
-
- )}
-
-
-
-
总费用
-
-
{formatAmount(amountFen)}
- {isEstimatedAmount && (
-
- 预估
-
- )}
-
-
-
-
状态
-
- {tx.stopTimestamp ? "已完成" : "进行中"}
-
-
-
-
停止原因
-
{tx.stopReason ?? "—"}
-
+
}
+ value={
+
+ {formatEnergy(energyWh)}
+ {isEstimatedEnergy && (
+
+ 预估
+
+ )}
+
+ }
+ />
+
}
+ value={
+
+ {formatAmount(amountFen)}
+ {isEstimatedAmount && (
+
+ 预估
+
+ )}
+
+ }
+ />
+
}
+ color={
+ isRejected ? "border-danger" : tx.stopTimestamp ? "border-success" : "border-warning"
+ }
+ value={isRejected ? "已拒绝" : tx.stopTimestamp ? "已完成" : "进行中"}
+ />
+
-
-
-
- - 交易编号
- - #{tx.id}
-
- - 储值卡
- - {tx.idTag}
-
- - 桩编号
- - {tx.chargePointIdentifier ?? "—"}
-
- - 连接器
- - {tx.connectorNumber ?? "—"}
-
- - 开始时间
- - {formatDateTime(tx.startTimestamp)}
-
- - 结束时间
- - {formatDateTime(tx.stopTimestamp)}
-
- - 持续时长
- -
- {formatDuration(tx.startTimestamp, tx.stopTimestamp)}
-
+
+
+
+
- 交易编号
+ - #{tx.id}
+
+
+
- 储值卡
+ - {tx.idTag}
+
+
+
- 桩编号
+ -
+ {tx.chargePointIdentifier ?? "—"}
+
+
+
+
- 连接器
+ - {tx.connectorNumber ?? "—"}
+
+
+
- 开始时间
+ -
+ {formatDateTime(tx.startTimestamp)}
+
+
+
+
- 结束时间
+ -
+ {formatDateTime(tx.stopTimestamp)}
+
+
+
+
- 持续时长
+ -
+ {formatDuration(tx.startTimestamp, tx.stopTimestamp)}
+
+
-
+
-
-
-
- - 起始表计
- -
- {tx.startMeterValue != null ? `${tx.startMeterValue} Wh` : "—"}
-
-
- - 结束表计
- -
- {tx.stopMeterValue != null ? `${tx.stopMeterValue} Wh` : "—"}
-
-
- - 消耗电量
- -
-
- {formatEnergy(energyWh)}
- {isEstimatedEnergy && (
-
- 预估
-
- )}
-
-
-
- - 电费
- - {formatAmount(tx.electricityFee)}
-
- - 服务费
- - {formatAmount(tx.serviceFee)}
-
- - 总费用
- -
-
- {formatAmount(amountFen)}
- {isEstimatedAmount && (
-
- 预估
-
- )}
-
-
+
+
+
+
- 起始表计
+ -
+ {tx.startMeterValue != null ? `${tx.startMeterValue} Wh` : "—"}
+
+
+
+
- 结束表计
+ -
+ {tx.stopMeterValue != null ? `${tx.stopMeterValue} Wh` : "—"}
+
+
+
+
- 消耗电量
+ -
+
+ {formatEnergy(energyWh)}
+ {isEstimatedEnergy && (
+
+ 预估
+
+ )}
+
+
+
+
+
- 电费
+ - {formatAmount(tx.electricityFee)}
+
+
+
- 服务费
+ - {formatAmount(tx.serviceFee)}
+
+
+
- 总费用
+ -
+
+ {formatAmount(amountFen)}
+ {isEstimatedAmount && (
+
+ 预估
+
+ )}
+
+
+
-
+
);
diff --git a/apps/web/components/info-section.tsx b/apps/web/components/info-section.tsx
new file mode 100644
index 0000000..f9a046c
--- /dev/null
+++ b/apps/web/components/info-section.tsx
@@ -0,0 +1,15 @@
+import type { ReactNode } from "react";
+
+type InfoSectionProps = {
+ title: string;
+ children: ReactNode;
+};
+
+export default function InfoSection({ title, children }: InfoSectionProps) {
+ return (
+
+
{title}
+ {children}
+
+ );
+}
diff --git a/apps/web/components/metric-indicator.tsx b/apps/web/components/metric-indicator.tsx
new file mode 100644
index 0000000..4ea6dbc
--- /dev/null
+++ b/apps/web/components/metric-indicator.tsx
@@ -0,0 +1,41 @@
+import type { ReactNode } from "react";
+import { Card } from "@heroui/react";
+
+type MetricIndicatorProps = {
+ title: string;
+ value: ReactNode;
+ hint?: ReactNode;
+ footer?: ReactNode;
+ icon?: ReactNode;
+ color?: string;
+ valueClassName?: string;
+};
+
+export default function MetricIndicator({
+ title,
+ value,
+ hint,
+ footer,
+ icon,
+ color = "border-border",
+ valueClassName = "text-xl font-semibold text-foreground tabular-nums",
+}: MetricIndicatorProps) {
+ return (
+
+
+
+ {value}
+ {footer ? (
+
+ {footer}
+
+ ) : (
+ hint && {hint}
+ )}
+
+
+ );
+}