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,
TextField,
} 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 { useSession } from "@/lib/auth-client";
@@ -107,7 +107,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
feePerKwh: "0",
});
const cpQuery = useQuery({
const { isFetching: refreshing, ...cpQuery } = useQuery({
queryKey: ["chargePoint", id],
queryFn: () => api.chargePoints.get(id),
refetchInterval: 3_000,
@@ -227,6 +227,10 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
</p>
)}
</div>
<div className="flex items-center gap-2">
<Button isIconOnly size="sm" variant="ghost" isDisabled={refreshing} onPress={() => cpQuery.refetch()} aria-label="刷新">
<ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
</Button>
{isAdmin && (
<Button size="sm" variant="secondary" onPress={openEdit}>
<Pencil className="size-4" />
@@ -234,6 +238,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
</Button>
)}
</div>
</div>
{/* Info grid */}
<div className="grid gap-4 md:grid-cols-2">

View File

@@ -14,7 +14,7 @@ import {
Table,
TextField,
} 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 { api, type ChargePoint } from "@/lib/api";
import { useSession } from "@/lib/auth-client";
@@ -74,7 +74,7 @@ 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 } = useQuery({
const { data: chargePoints = [], refetch: refetchList, isFetching: refreshing } = useQuery({
queryKey: ["chargePoints"],
queryFn: () => api.chargePoints.list().catch(() => []),
refetchInterval: 3_000,
@@ -152,6 +152,10 @@ export default function ChargePointsPage() {
<h1 className="text-xl font-semibold text-foreground"></h1>
<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="刷新">
<ArrowRotateRight className={`size-4 ${refreshing ? "animate-spin" : ""}`} />
</Button>
{isAdmin && (
<Button size="sm" variant="secondary" onPress={openCreate}>
<Plus className="size-4" />
@@ -159,6 +163,7 @@ export default function ChargePointsPage() {
</Button>
)}
</div>
</div>
{/* Create / Edit modal — admin only */}
{isAdmin && (

View File

@@ -1,7 +1,7 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { Card, Spinner } from "@heroui/react";
import { Button, Card, Spinner } from "@heroui/react";
import {
Thunderbolt,
PlugConnection,
@@ -9,6 +9,7 @@ import {
ChartColumn,
TagDollar,
Person,
ArrowRotateRight,
} from "@gravity-ui/icons";
import { useSession } from "@/lib/auth-client";
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 isAdmin = sessionData?.user?.role === "admin";
const { data, isPending: queryPending } = useQuery({
const { data, isPending: queryPending, isFetching: refreshing, refetch } = useQuery({
queryKey: ["dashboard", isAdmin],
queryFn: async () => {
const [statsRes, txRes, cpsData] = await Promise.all([
@@ -225,10 +226,15 @@ export default function DashboardPage() {
return (
<div className="space-y-6">
<div className="flex items-start justify-between gap-3">
<div>
<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>
{/* Today's live metrics */}
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
@@ -326,12 +332,17 @@ export default function DashboardPage() {
return (
<div className="space-y-6">
<div className="flex items-start justify-between gap-3">
<div>
<h1 className="text-xl font-semibold text-foreground"></h1>
<p className="mt-0.5 text-sm text-muted">
{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 className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<StatCard

View File

@@ -3,7 +3,7 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
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 { useSession } from "@/lib/auth-client";
@@ -30,6 +30,7 @@ export default function TransactionsPage() {
const {
data,
isPending: loading,
isFetching: refreshing,
refetch,
} = useQuery({
queryKey: ["transactions", page, status],
@@ -76,6 +77,10 @@ export default function TransactionsPage() {
<h1 className="text-xl font-semibold text-foreground"></h1>
<p className="mt-0.5 text-sm text-muted"> {data?.total ?? "—"} </p>
</div>
<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) => (
<button
@@ -89,8 +94,7 @@ export default function TransactionsPage() {
>
{s === "all" ? "全部" : s === "active" ? "进行中" : "已完成"}
</button>
))}
</div>
))} </div> </div>
</div>
<Table>

View File

@@ -17,7 +17,7 @@ import {
TextField,
useFilter,
} 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 { useSession } from "@/lib/auth-client";
@@ -140,6 +140,7 @@ export default function UsersPage() {
const [users, setUsers] = useState<UserRow[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [updating, setUpdating] = useState<string | null>(null);
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(() => {
load();
}, []);
@@ -296,6 +308,10 @@ export default function UsersPage() {
<h1 className="text-xl font-semibold text-foreground"></h1>
<p className="mt-0.5 text-sm text-muted"> {users.length} </p>
</div>
<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)}>
@@ -387,6 +403,7 @@ export default function UsersPage() {
</Modal.Backdrop>
</Modal>
</div>
</div>
<Table>
<Table.ScrollContainer>