feat(web): add ScrollFade component for improved horizontal scrolling experience
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user