From eac81d2fab5b205dd13f0fbc11f8ec61fad8e668 Mon Sep 17 00:00:00 2001 From: Timothy Yin Date: Wed, 11 Mar 2026 00:15:07 +0800 Subject: [PATCH] feat: integrate React Query for data fetching and state management across dashboard components --- .../app/dashboard/charge-points/[id]/page.tsx | 67 ++++++------------- apps/web/app/dashboard/charge-points/page.tsx | 35 +++------- apps/web/app/dashboard/id-tags/page.tsx | 62 +++++++++-------- apps/web/app/dashboard/layout.tsx | 5 +- apps/web/app/dashboard/page.tsx | 50 +++++--------- apps/web/app/dashboard/transactions/page.tsx | 34 ++++------ apps/web/components/query-provider.tsx | 18 +++++ apps/web/package.json | 1 + package.json | 3 +- 9 files changed, 121 insertions(+), 154 deletions(-) create mode 100644 apps/web/components/query-provider.tsx diff --git a/apps/web/app/dashboard/charge-points/[id]/page.tsx b/apps/web/app/dashboard/charge-points/[id]/page.tsx index 06bebf2..23d2d79 100644 --- a/apps/web/app/dashboard/charge-points/[id]/page.tsx +++ b/apps/web/app/dashboard/charge-points/[id]/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { use, useCallback, useEffect, useState } from "react"; +import { use, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import Link from "next/link"; import { Button, @@ -93,14 +94,8 @@ type EditForm = { export default function ChargePointDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params); - const [cp, setCp] = useState(null); - const [notFound, setNotFound] = useState(false); - const [loading, setLoading] = useState(true); - // transactions - const [txData, setTxData] = useState(null); const [txPage, setTxPage] = useState(1); - const [txLoading, setTxLoading] = useState(true); // edit modal const [editOpen, setEditOpen] = useState(false); @@ -112,42 +107,22 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id feePerKwh: "0", }); - const loadCp = useCallback(async () => { - setLoading(true); - try { - const data = await api.chargePoints.get(id); - setCp(data); - } catch { - setNotFound(true); - } finally { - setLoading(false); - } - }, [id]); + const cpQuery = useQuery({ + queryKey: ["chargePoint", id], + queryFn: () => api.chargePoints.get(id), + refetchInterval: 3_000, + retry: false, + }); - const loadTx = useCallback( - async (p: number) => { - setTxLoading(true); - try { - const data = await api.transactions.list({ - page: p, - limit: TX_LIMIT, - chargePointId: id, - }); - setTxData(data); - } finally { - setTxLoading(false); - } - }, - [id], - ); + const txQuery = useQuery({ + queryKey: ["chargePointTransactions", id, txPage], + queryFn: () => + api.transactions.list({ page: txPage, limit: TX_LIMIT, chargePointId: id }), + refetchInterval: 3_000, + }); - useEffect(() => { - loadCp(); - }, [loadCp]); - - useEffect(() => { - loadTx(txPage); - }, [txPage, loadTx]); + const cp = cpQuery.data; + const txData = txQuery.data; const openEdit = () => { if (!cp) return; @@ -165,13 +140,13 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id setEditBusy(true); try { const fee = Math.max(0, Math.round(Number(editForm.feePerKwh) || 0)); - const updated = await api.chargePoints.update(cp.id, { + await api.chargePoints.update(cp.id, { chargePointVendor: editForm.chargePointVendor, chargePointModel: editForm.chargePointModel, registrationStatus: editForm.registrationStatus, feePerKwh: fee, }); - setCp((prev) => (prev ? { ...prev, ...updated, connectors: prev.connectors } : prev)); + await cpQuery.refetch(); setEditOpen(false); } finally { setEditBusy(false); @@ -188,7 +163,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id // ── Render: loading / not found ────────────────────────────────────────── - if (loading) { + if (cpQuery.isPending) { return (
@@ -196,7 +171,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id ); } - if (notFound || !cp) { + if (!cp) { return (
(
- {txLoading ? "加载中…" : "暂无充电记录"} + {txQuery.isPending ? "加载中…" : "暂无充电记录"}
)} > diff --git a/apps/web/app/dashboard/charge-points/page.tsx b/apps/web/app/dashboard/charge-points/page.tsx index 3e07c57..22db7f2 100644 --- a/apps/web/app/dashboard/charge-points/page.tsx +++ b/apps/web/app/dashboard/charge-points/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import { Button, Chip, @@ -67,26 +68,17 @@ const EMPTY_FORM: FormData = { }; export default function ChargePointsPage() { - const [chargePoints, setChargePoints] = useState([]); const [formOpen, setFormOpen] = useState(false); const [formTarget, setFormTarget] = useState(null); const [formData, setFormData] = useState(EMPTY_FORM); const [formBusy, setFormBusy] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); - const hasFetched = useRef(false); - - const load = useCallback(async () => { - const data = await api.chargePoints.list().catch(() => []); - setChargePoints(data); - }, []); - - useEffect(() => { - if (!hasFetched.current) { - hasFetched.current = true; - load(); - } - }, [load]); + const { data: chargePoints = [], refetch: refetchList } = useQuery({ + queryKey: ["chargePoints"], + queryFn: () => api.chargePoints.list().catch(() => []), + refetchInterval: 3_000, + }); const openCreate = () => { setFormTarget(null); @@ -113,28 +105,23 @@ export default function ChargePointsPage() { const fee = Math.max(0, Math.round(Number(formData.feePerKwh) || 0)); if (formTarget) { // Edit - const updated = await api.chargePoints.update(String(formTarget.id), { + await api.chargePoints.update(String(formTarget.id), { chargePointVendor: formData.chargePointVendor, chargePointModel: formData.chargePointModel, registrationStatus: formData.registrationStatus, feePerKwh: fee, }); - setChargePoints((prev) => - prev.map((cp) => - cp.id === formTarget.id ? { ...updated, connectors: cp.connectors } : cp, - ), - ); } else { // Create - const created = await api.chargePoints.create({ + await api.chargePoints.create({ chargePointIdentifier: formData.chargePointIdentifier.trim(), chargePointVendor: formData.chargePointVendor.trim() || undefined, chargePointModel: formData.chargePointModel.trim() || undefined, registrationStatus: formData.registrationStatus, feePerKwh: fee, }); - setChargePoints((prev) => [created, ...prev]); } + await refetchList(); setFormOpen(false); } finally { setFormBusy(false); @@ -146,7 +133,7 @@ export default function ChargePointsPage() { setDeleting(true); try { await api.chargePoints.delete(String(deleteTarget.id)); - setChargePoints((prev) => prev.filter((cp) => cp.id !== deleteTarget.id)); + await refetchList(); setDeleteTarget(null); } finally { setDeleting(false); diff --git a/apps/web/app/dashboard/id-tags/page.tsx b/apps/web/app/dashboard/id-tags/page.tsx index f030aec..2f2fe0e 100644 --- a/apps/web/app/dashboard/id-tags/page.tsx +++ b/apps/web/app/dashboard/id-tags/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import { Autocomplete, Button, @@ -22,7 +23,7 @@ import { useFilter, } from "@heroui/react"; import { parseDate } from "@internationalized/date"; -import { Pencil, Plus, TrashBin } from "@gravity-ui/icons"; +import { ArrowRotateRight, Pencil, Plus, TrashBin } from "@gravity-ui/icons"; import { api, type IdTag, type UserRow } from "@/lib/api"; import { useSession } from "@/lib/auth-client"; @@ -330,43 +331,36 @@ function TagFormBody({ export default function IdTagsPage() { const { data: sessionData } = useSession(); const isAdmin = sessionData?.user?.role === "admin"; - const [tags, setTags] = useState([]); - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(null); const [form, setForm] = useState(emptyForm); const [saving, setSaving] = useState(false); const [deletingTag, setDeletingTag] = useState(null); const [claiming, setClaiming] = useState(false); - const handleClaim = async () => { - setClaiming(true); - try { - await api.idTags.claim(); - await load(); - } finally { - setClaiming(false); - } - }; - - const load = async () => { - setLoading(true); - try { + const { data: idTagsData, isPending: loading, isFetching: refreshing, refetch } = useQuery({ + queryKey: ["idTags"], + queryFn: async () => { const [tagList, userList] = await Promise.all([ api.idTags.list(), api.users.list().catch(() => [] as UserRow[]), ]); - setTags(tagList); - setUsers(userList); + return { tags: tagList, users: userList }; + }, + }); + + const tags = idTagsData?.tags ?? []; + const users = idTagsData?.users ?? []; + + const handleClaim = async () => { + setClaiming(true); + try { + await api.idTags.claim(); + await refetch(); } finally { - setLoading(false); + setClaiming(false); } }; - useEffect(() => { - load(); - }, []); - const openCreate = () => { setEditing(null); setForm(emptyForm); @@ -405,7 +399,7 @@ export default function IdTagsPage() { balance: yuanToFen(form.balance), }); } - await load(); + await refetch(); } finally { setSaving(false); } @@ -415,7 +409,7 @@ export default function IdTagsPage() { setDeletingTag(idTag); try { await api.idTags.delete(idTag); - await load(); + await refetch(); } finally { setDeletingTag(null); } @@ -429,7 +423,18 @@ export default function IdTagsPage() {

共 {tags.length} 张

{isAdmin ? ( - +
+ +
) : (