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 ( + + +
+

{title}

+ {icon} +
+

{value}

+ {footer ? ( +
+ {footer} +
+ ) : ( + hint &&
{hint}
+ )} +
+
+ ); +}