diff --git a/apps/web/app/dashboard/charge-points/page.tsx b/apps/web/app/dashboard/charge-points/page.tsx index a88d00f..687e8c8 100644 --- a/apps/web/app/dashboard/charge-points/page.tsx +++ b/apps/web/app/dashboard/charge-points/page.tsx @@ -16,6 +16,7 @@ import { } from "@heroui/react"; import { Plus, Pencil, PlugConnection, TrashBin, ArrowRotateRight } from "@gravity-ui/icons"; import Link from "next/link"; +import { ScrollFade } from "@/components/scroll-fade"; import { api, type ChargePoint } from "@/lib/api"; import { useSession } from "@/lib/auth-client"; @@ -45,6 +46,39 @@ const statusDotClass: Record = { Occupied: "bg-warning", }; +function ConnectorCell({ connectors }: { connectors: ChargePoint["connectors"] }) { + return ( + + {connectors.length === 0 ? ( + + ) : ( + [...connectors] + .sort((a, b) => a.connectorId - b.connectorId) + .map((conn) => ( +
+ + + #{conn.connectorId} + + + + + {statusLabelMap[conn.status] ?? conn.status} + +
+ )) + )} +
+ ); +} + const registrationColorMap: Record = { Accepted: "success", Pending: "warning", @@ -74,7 +108,11 @@ export default function ChargePointsPage() { const [formBusy, setFormBusy] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); - const { data: chargePoints = [], refetch: refetchList, isFetching: refreshing } = useQuery({ + const { + data: chargePoints = [], + refetch: refetchList, + isFetching: refreshing, + } = useQuery({ queryKey: ["chargePoints"], queryFn: () => api.chargePoints.list().catch(() => []), refetchInterval: 3_000, @@ -153,7 +191,14 @@ export default function ChargePointsPage() {

共 {chargePoints.length} 台设备

- {isAdmin && ( @@ -252,7 +297,7 @@ export default function ChargePointsPage() { {!isEdit && (

- 手动创建的充电桩默认注册状态为 Pending,需手动改为 Accepted 后才可正常充电。 + 自动注册的充电桩默认状态为 Pending,需手动改为 Accepted 后才可正常上线。

)} @@ -303,19 +348,28 @@ export default function ChargePointsPage() { )} {chargePoints.map((cp) => ( - + {cp.chargePointIdentifier} {isAdmin && ( - {cp.chargePointVendor && cp.chargePointModel ? ( - `${cp.chargePointVendor} / ${cp.chargePointModel}` + {cp.chargePointVendor || cp.chargePointModel ? ( +
+ {cp.chargePointVendor && ( + + {cp.chargePointVendor} + + )} + {cp.chargePointModel && ( + {cp.chargePointModel} + )} +
) : ( )} @@ -368,34 +422,7 @@ export default function ChargePointsPage() { )}
-
- {cp.connectors.length === 0 ? ( - - ) : ( - [...cp.connectors] - .sort((a, b) => a.connectorId - b.connectorId) - .map((conn) => ( -
- - - #{conn.connectorId} - - - - - {statusLabelMap[conn.status] ?? conn.status} - -
- )) - )} -
+
{isAdmin && ( diff --git a/apps/web/components/scroll-fade.tsx b/apps/web/components/scroll-fade.tsx new file mode 100644 index 0000000..5c6aea6 --- /dev/null +++ b/apps/web/components/scroll-fade.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useRef, useState, useEffect, type ReactNode } from "react"; + +interface ScrollFadeProps { + children: ReactNode; + className?: string; + /** Tailwind max-width class, e.g. "max-w-2xs". Defaults to none. */ + maxWidth?: string; +} + +/** + * Wraps children in a horizontally-scrollable container that shows left/right + * gradient fade indicators when there is overflowed content in that direction. + */ +export function ScrollFade({ children, className, maxWidth }: ScrollFadeProps) { + const scrollRef = useRef(null); + const [showLeft, setShowLeft] = useState(false); + const [showRight, setShowRight] = useState(false); + + const update = () => { + const el = scrollRef.current; + if (!el) return; + setShowLeft(el.scrollLeft > 0); + setShowRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1); + }; + + useEffect(() => { + update(); + const el = scrollRef.current; + if (!el) return; + const ro = new ResizeObserver(update); + ro.observe(el); + return () => ro.disconnect(); + }, [children]); + + return ( +
+
+ {children} +
+ {showLeft && ( +
+ )} + {showRight && ( +
+ )} +
+ ); +}