feat: add refresh button to various dashboard pages for improved data fetching

This commit is contained in:
2026-03-11 00:29:04 +08:00
parent 4d0c429d5f
commit a84393590e
5 changed files with 72 additions and 30 deletions

View File

@@ -16,7 +16,7 @@ import {
Table, Table,
TextField, TextField,
} from "@heroui/react"; } from "@heroui/react";
import { ArrowLeft, Pencil, PlugConnection } from "@gravity-ui/icons"; import { ArrowLeft, Pencil, PlugConnection, ArrowRotateRight } from "@gravity-ui/icons";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
@@ -107,7 +107,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
feePerKwh: "0", feePerKwh: "0",
}); });
const cpQuery = useQuery({ const { isFetching: refreshing, ...cpQuery } = useQuery({
queryKey: ["chargePoint", id], queryKey: ["chargePoint", id],
queryFn: () => api.chargePoints.get(id), queryFn: () => api.chargePoints.get(id),
refetchInterval: 3_000, refetchInterval: 3_000,
@@ -227,12 +227,17 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
</p> </p>
)} )}
</div> </div>
{isAdmin && ( <div className="flex items-center gap-2">
<Button size="sm" variant="secondary" onPress={openEdit}> <Button isIconOnly size="sm" variant="ghost" isDisabled={refreshing} onPress={() => cpQuery.refetch()} aria-label="刷新">
<Pencil className="size-4" /> <ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
</Button> </Button>
)} {isAdmin && (
<Button size="sm" variant="secondary" onPress={openEdit}>
<Pencil className="size-4" />
</Button>
)}
</div>
</div> </div>
{/* Info grid */} {/* Info grid */}

View File

@@ -14,7 +14,7 @@ import {
Table, Table,
TextField, TextField,
} from "@heroui/react"; } from "@heroui/react";
import { Plus, Pencil, PlugConnection, TrashBin } from "@gravity-ui/icons"; import { Plus, Pencil, PlugConnection, TrashBin, ArrowRotateRight } from "@gravity-ui/icons";
import Link from "next/link"; import Link from "next/link";
import { api, type ChargePoint } from "@/lib/api"; import { api, type ChargePoint } from "@/lib/api";
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
@@ -74,7 +74,7 @@ export default function ChargePointsPage() {
const [formBusy, setFormBusy] = useState(false); const [formBusy, setFormBusy] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<ChargePoint | null>(null); const [deleteTarget, setDeleteTarget] = useState<ChargePoint | null>(null);
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const { data: chargePoints = [], refetch: refetchList } = useQuery({ const { data: chargePoints = [], refetch: refetchList, isFetching: refreshing } = useQuery({
queryKey: ["chargePoints"], queryKey: ["chargePoints"],
queryFn: () => api.chargePoints.list().catch(() => []), queryFn: () => api.chargePoints.list().catch(() => []),
refetchInterval: 3_000, refetchInterval: 3_000,
@@ -152,12 +152,17 @@ export default function ChargePointsPage() {
<h1 className="text-xl font-semibold text-foreground"></h1> <h1 className="text-xl font-semibold text-foreground"></h1>
<p className="mt-0.5 text-sm text-muted"> {chargePoints.length} </p> <p className="mt-0.5 text-sm text-muted"> {chargePoints.length} </p>
</div> </div>
{isAdmin && ( <div className="flex items-center gap-2">
<Button size="sm" variant="secondary" onPress={openCreate}> <Button isIconOnly size="sm" variant="ghost" isDisabled={refreshing} onPress={() => refetchList()} aria-label="刷新">
<Plus className="size-4" /> <ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
</Button> </Button>
)} {isAdmin && (
<Button size="sm" variant="secondary" onPress={openCreate}>
<Plus className="size-4" />
</Button>
)}
</div>
</div> </div>
{/* Create / Edit modal — admin only */} {/* Create / Edit modal — admin only */}

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Card, Spinner } from "@heroui/react"; import { Button, Card, Spinner } from "@heroui/react";
import { import {
Thunderbolt, Thunderbolt,
PlugConnection, PlugConnection,
@@ -9,6 +9,7 @@ import {
ChartColumn, ChartColumn,
TagDollar, TagDollar,
Person, Person,
ArrowRotateRight,
} from "@gravity-ui/icons"; } from "@gravity-ui/icons";
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
import { api, type Stats, type UserStats, type Transaction, type ChargePoint } from "@/lib/api"; import { api, type Stats, type UserStats, type Transaction, type ChargePoint } from "@/lib/api";
@@ -187,7 +188,7 @@ export default function DashboardPage() {
const { data: sessionData, isPending } = useSession(); const { data: sessionData, isPending } = useSession();
const isAdmin = sessionData?.user?.role === "admin"; const isAdmin = sessionData?.user?.role === "admin";
const { data, isPending: queryPending } = useQuery({ const { data, isPending: queryPending, isFetching: refreshing, refetch } = useQuery({
queryKey: ["dashboard", isAdmin], queryKey: ["dashboard", isAdmin],
queryFn: async () => { queryFn: async () => {
const [statsRes, txRes, cpsData] = await Promise.all([ const [statsRes, txRes, cpsData] = await Promise.all([
@@ -225,9 +226,14 @@ export default function DashboardPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div className="flex items-start justify-between gap-3">
<h1 className="text-xl font-semibold text-foreground"></h1> <div>
<p className="mt-0.5 text-sm text-muted"></p> <h1 className="text-xl font-semibold text-foreground"></h1>
<p className="mt-0.5 text-sm text-muted"></p>
</div>
<Button isIconOnly size="sm" variant="ghost" isDisabled={refreshing} onPress={() => refetch()} aria-label="刷新">
<ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
</Button>
</div> </div>
{/* Today's live metrics */} {/* Today's live metrics */}
@@ -326,11 +332,16 @@ export default function DashboardPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div className="flex items-start justify-between gap-3">
<h1 className="text-xl font-semibold text-foreground"></h1> <div>
<p className="mt-0.5 text-sm text-muted"> <h1 className="text-xl font-semibold text-foreground"></h1>
{sessionData?.user?.name ?? sessionData?.user?.email} <p className="mt-0.5 text-sm text-muted">
</p> {sessionData?.user?.name ?? sessionData?.user?.email}
</p>
</div>
<Button isIconOnly size="sm" variant="ghost" isDisabled={refreshing} onPress={() => refetch()} aria-label="刷新">
<ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
</Button>
</div> </div>
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4"> <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">

View File

@@ -3,7 +3,7 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Button, Chip, Modal, Pagination, Spinner, Table } from "@heroui/react"; import { Button, Chip, Modal, Pagination, Spinner, Table } from "@heroui/react";
import { TrashBin } from "@gravity-ui/icons"; import { TrashBin, ArrowRotateRight } from "@gravity-ui/icons";
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
@@ -30,6 +30,7 @@ export default function TransactionsPage() {
const { const {
data, data,
isPending: loading, isPending: loading,
isFetching: refreshing,
refetch, refetch,
} = useQuery({ } = useQuery({
queryKey: ["transactions", page, status], queryKey: ["transactions", page, status],
@@ -76,7 +77,11 @@ export default function TransactionsPage() {
<h1 className="text-xl font-semibold text-foreground"></h1> <h1 className="text-xl font-semibold text-foreground"></h1>
<p className="mt-0.5 text-sm text-muted"> {data?.total ?? "—"} </p> <p className="mt-0.5 text-sm text-muted"> {data?.total ?? "—"} </p>
</div> </div>
<div className="flex gap-1.5 rounded-xl bg-surface-secondary p-1"> <div className="flex items-center gap-2">
<Button isIconOnly size="sm" variant="ghost" isDisabled={refreshing} onPress={() => refetch()} aria-label="刷新">
<ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
</Button>
<div className="flex gap-1.5 rounded-xl bg-surface-secondary p-1">
{(["all", "active", "completed"] as const).map((s) => ( {(["all", "active", "completed"] as const).map((s) => (
<button <button
key={s} key={s}
@@ -89,8 +94,7 @@ export default function TransactionsPage() {
> >
{s === "all" ? "全部" : s === "active" ? "进行中" : "已完成"} {s === "all" ? "全部" : s === "active" ? "进行中" : "已完成"}
</button> </button>
))} ))} </div> </div>
</div>
</div> </div>
<Table> <Table>

View File

@@ -17,7 +17,7 @@ import {
TextField, TextField,
useFilter, useFilter,
} from "@heroui/react"; } from "@heroui/react";
import { CreditCard, Pencil } from "@gravity-ui/icons"; import { CreditCard, Pencil, ArrowRotateRight } from "@gravity-ui/icons";
import { api, type IdTag, type UserRow } from "@/lib/api"; import { api, type IdTag, type UserRow } from "@/lib/api";
import { useSession } from "@/lib/auth-client"; import { useSession } from "@/lib/auth-client";
@@ -140,6 +140,7 @@ export default function UsersPage() {
const [users, setUsers] = useState<UserRow[]>([]); const [users, setUsers] = useState<UserRow[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [updating, setUpdating] = useState<string | null>(null); const [updating, setUpdating] = useState<string | null>(null);
const [createForm, setCreateForm] = useState<CreateForm>(emptyCreate); const [createForm, setCreateForm] = useState<CreateForm>(emptyCreate);
@@ -168,6 +169,17 @@ export default function UsersPage() {
} }
}; };
const handleRefresh = async () => {
setRefreshing(true);
try {
setUsers(await api.users.list());
} catch {
// ignore
} finally {
setRefreshing(false);
}
};
useEffect(() => { useEffect(() => {
load(); load();
}, []); }, []);
@@ -296,7 +308,11 @@ export default function UsersPage() {
<h1 className="text-xl font-semibold text-foreground"></h1> <h1 className="text-xl font-semibold text-foreground"></h1>
<p className="mt-0.5 text-sm text-muted"> {users.length} </p> <p className="mt-0.5 text-sm text-muted"> {users.length} </p>
</div> </div>
<Modal> <div className="flex items-center gap-2">
<Button isIconOnly size="sm" variant="ghost" isDisabled={refreshing} onPress={handleRefresh} aria-label="刷新">
<ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
</Button>
<Modal>
<Button variant="secondary" onPress={() => setCreateForm(emptyCreate)}> <Button variant="secondary" onPress={() => setCreateForm(emptyCreate)}>
</Button> </Button>
@@ -386,6 +402,7 @@ export default function UsersPage() {
</Modal.Container> </Modal.Container>
</Modal.Backdrop> </Modal.Backdrop>
</Modal> </Modal>
</div>
</div> </div>
<Table> <Table>