feat(web): add ScrollFade component for improved horizontal scrolling experience

This commit is contained in:
2026-03-11 11:49:03 +08:00
parent 1619ed22a0
commit ee329c7b9b
2 changed files with 116 additions and 35 deletions

View File

@@ -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<string, string> = {
Occupied: "bg-warning",
};
function ConnectorCell({ connectors }: { connectors: ChargePoint["connectors"] }) {
return (
<ScrollFade maxWidth="max-w-2xs">
{connectors.length === 0 ? (
<span className="text-muted text-sm"></span>
) : (
[...connectors]
.sort((a, b) => a.connectorId - b.connectorId)
.map((conn) => (
<div
key={conn.id}
className="flex shrink-0 items-center gap-1.5 rounded-lg border border-border bg-surface-secondary px-2 py-1"
>
<PlugConnection className="size-3 shrink-0 text-muted" />
<span className="text-xs font-medium tabular-nums text-muted">
#{conn.connectorId}
</span>
<span className="h-3 w-px bg-border" />
<span
className={`size-1.5 shrink-0 rounded-full ${
statusDotClass[conn.status] ?? "bg-warning"
}`}
/>
<span className="text-xs text-nowrap text-foreground">
{statusLabelMap[conn.status] ?? conn.status}
</span>
</div>
))
)}
</ScrollFade>
);
}
const registrationColorMap: Record<string, "success" | "warning" | "danger"> = {
Accepted: "success",
Pending: "warning",
@@ -74,7 +108,11 @@ export default function ChargePointsPage() {
const [formBusy, setFormBusy] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<ChargePoint | null>(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() {
<p className="mt-0.5 text-sm text-muted"> {chargePoints.length} </p>
</div>
<div className="flex items-center gap-2">
<Button isIconOnly size="sm" variant="ghost" isDisabled={refreshing} onPress={() => refetchList()} aria-label="刷新">
<Button
isIconOnly
size="sm"
variant="ghost"
isDisabled={refreshing}
onPress={() => refetchList()}
aria-label="刷新"
>
<ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
</Button>
{isAdmin && (
@@ -252,7 +297,7 @@ export default function ChargePointsPage() {
</TextField>
{!isEdit && (
<p className="text-xs text-muted">
Pending Accepted
Pending Accepted 线
</p>
)}
</Modal.Body>
@@ -303,19 +348,28 @@ export default function ChargePointsPage() {
</Table.Row>
)}
{chargePoints.map((cp) => (
<Table.Row key={cp.id} id={String(cp.id)}>
<Table.Row key={cp.id} id={String(cp.id)} className={"group"}>
<Table.Cell>
<Link
href={`/dashboard/charge-points/${cp.id}`}
className="font-mono font-medium text-accent hover:underline"
className="font-medium text-accent"
>
{cp.chargePointIdentifier}
</Link>
</Table.Cell>
{isAdmin && (
<Table.Cell>
{cp.chargePointVendor && cp.chargePointModel ? (
`${cp.chargePointVendor} / ${cp.chargePointModel}`
{cp.chargePointVendor || cp.chargePointModel ? (
<div className="flex flex-col">
{cp.chargePointVendor && (
<span className="text-xs text-muted font-medium">
{cp.chargePointVendor}
</span>
)}
{cp.chargePointModel && (
<span className="text-sm text-foreground">{cp.chargePointModel}</span>
)}
</div>
) : (
<span className="text-muted"></span>
)}
@@ -368,34 +422,7 @@ export default function ChargePointsPage() {
)}
</Table.Cell>
<Table.Cell>
<div className="flex flex-wrap gap-1.5">
{cp.connectors.length === 0 ? (
<span className="text-muted text-sm"></span>
) : (
[...cp.connectors]
.sort((a, b) => a.connectorId - b.connectorId)
.map((conn) => (
<div
key={conn.id}
className="flex items-center gap-1.5 rounded-lg border border-border bg-surface-secondary px-2 py-1"
>
<PlugConnection className="size-3 shrink-0 text-muted" />
<span className="text-xs font-medium tabular-nums text-muted">
#{conn.connectorId}
</span>
<span className="h-3 w-px bg-border" />
<span
className={`size-1.5 shrink-0 rounded-full ${
statusDotClass[conn.status] ?? "bg-warning"
}`}
/>
<span className="text-xs text-foreground text-nowrap">
{statusLabelMap[conn.status] ?? conn.status}
</span>
</div>
))
)}
</div>
<ConnectorCell connectors={cp.connectors} />
</Table.Cell>
{isAdmin && (
<Table.Cell>