From 13ae574aadb11ab4960e8848e73e88c18ed172f8 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 10 Jun 2026 13:58:31 +0800 Subject: [PATCH] feat(dashboard, risk): add SWR query hook and lazy load charts --- package-lock.json | 22 +++++++++ package.json | 1 + src/hooks/use-api-query.ts | 30 +++++++++++++ .../dashboard/dashboard-analytics-panel.tsx | 15 +++++-- src/modules/dashboard/dashboard-console.tsx | 45 ++++++++++++++----- src/modules/risk/risk-draw-header.tsx | 32 ++++--------- 6 files changed, 108 insertions(+), 37 deletions(-) create mode 100644 src/hooks/use-api-query.ts diff --git a/package-lock.json b/package-lock.json index 94b7130..f9b75cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "recharts": "^3.8.0", "shadcn": "^4.7.0", "sonner": "^2.0.7", + "swr": "^2.4.1", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0", "xlsx": "^0.18.5", @@ -4520,6 +4521,14 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://mirrors.cloud.tencent.com/npm/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", @@ -9716,6 +9725,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.4.1", + "resolved": "https://mirrors.cloud.tencent.com/npm/swr/-/swr-2.4.1.tgz", + "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tagged-tag": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz", diff --git a/package.json b/package.json index 0a583ed..1addf45 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "recharts": "^3.8.0", "shadcn": "^4.7.0", "sonner": "^2.0.7", + "swr": "^2.4.1", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0", "xlsx": "^0.18.5", diff --git a/src/hooks/use-api-query.ts b/src/hooks/use-api-query.ts new file mode 100644 index 0000000..66434e8 --- /dev/null +++ b/src/hooks/use-api-query.ts @@ -0,0 +1,30 @@ +"use client"; + +import useSWR, { type SWRConfiguration, type SWRResponse } from "swr"; + +/** + * 通用 API 查询 Hook —— 基于 SWR 的薄包装。 + * + * @param key SWR 缓存键。传 `null` 表示暂不请求(条件请求)。 + * @param fetcher 无参异步函数,通常由 `api/*.ts` 的函数包装而来。 + * @param options SWR 配置覆盖。 + * + * @example + * ```tsx + * const { data, error, isLoading } = useApiQuery( + * ["admin/draws", page, status], + * () => getAdminDraws({ page, status }), + * ); + * ``` + */ +export function useApiQuery( + key: string | readonly unknown[] | null, + fetcher: () => Promise, + options?: SWRConfiguration, +): SWRResponse { + return useSWR(key, fetcher, { + revalidateOnFocus: false, + dedupingInterval: 2_000, + ...options, + }); +} diff --git a/src/modules/dashboard/dashboard-analytics-panel.tsx b/src/modules/dashboard/dashboard-analytics-panel.tsx index 1703e2b..09cecf6 100644 --- a/src/modules/dashboard/dashboard-analytics-panel.tsx +++ b/src/modules/dashboard/dashboard-analytics-panel.tsx @@ -1,5 +1,6 @@ "use client"; +import dynamic from "next/dynamic"; import Link from "next/link"; import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; @@ -23,10 +24,6 @@ import { getAdminRequestLocale } from "@/lib/admin-locale"; import { cn } from "@/lib/utils"; import { DashboardKpiCard } from "@/modules/dashboard/dashboard-visuals"; import { DASHBOARD_CHART_COLORS } from "@/modules/dashboard/dashboard-chart-config"; -import { - DailyTrendChart, - PlayBreakdownChart, -} from "@/modules/dashboard/dashboard-trend-charts"; import { DASHBOARD_ANALYTICS_PERIODS, DASHBOARD_RANKING_METRICS, @@ -34,6 +31,16 @@ import { type DashboardAnalyticsState, } from "@/modules/dashboard/use-dashboard-analytics"; +// recharts 图表组件懒加载 +const DailyTrendChart = dynamic( + () => import("@/modules/dashboard/dashboard-trend-charts").then((m) => ({ default: m.DailyTrendChart })), + { ssr: false }, +); +const PlayBreakdownChart = dynamic( + () => import("@/modules/dashboard/dashboard-trend-charts").then((m) => ({ default: m.PlayBreakdownChart })), + { ssr: false }, +); + function computeDeltaPercent(series: number[]): string | null { if (series.length < 2) { return null; diff --git a/src/modules/dashboard/dashboard-console.tsx b/src/modules/dashboard/dashboard-console.tsx index e0648fa..1bddd1d 100644 --- a/src/modules/dashboard/dashboard-console.tsx +++ b/src/modules/dashboard/dashboard-console.tsx @@ -1,5 +1,6 @@ "use client"; +import dynamic from "next/dynamic"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState, type ReactElement, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; @@ -35,16 +36,6 @@ import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admi import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; -import { - AbnormalTransferPanelFooter, - CapUsageBar, - FinanceStructureChart, - HotUsageBars, - ResultBatchQueueSummary, - PlatformLifetimePayoutSnapshot, - DashboardPanelCard, - SettlementStatusChart, -} from "@/modules/dashboard/dashboard-visuals"; import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog"; import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime"; import { normalizeAdminLanguage } from "@/i18n"; @@ -66,6 +57,40 @@ import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance import type { AdminRiskPoolRow } from "@/types/api/admin-risk"; import type { DrawCurrentSnapshot } from "@/types/api/public-draw"; +// recharts 图表组件懒加载,避免 ~200KB 进入主 bundle +const DashboardPanelCard = dynamic( + () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.DashboardPanelCard })), + { ssr: false }, +); +const AbnormalTransferPanelFooter = dynamic( + () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.AbnormalTransferPanelFooter })), + { ssr: false }, +); +const CapUsageBar = dynamic( + () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.CapUsageBar })), + { ssr: false }, +); +const FinanceStructureChart = dynamic( + () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.FinanceStructureChart })), + { ssr: false }, +); +const HotUsageBars = dynamic( + () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.HotUsageBars })), + { ssr: false }, +); +const ResultBatchQueueSummary = dynamic( + () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.ResultBatchQueueSummary })), + { ssr: false }, +); +const PlatformLifetimePayoutSnapshot = dynamic( + () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.PlatformLifetimePayoutSnapshot })), + { ssr: false }, +); +const SettlementStatusChart = dynamic( + () => import("@/modules/dashboard/dashboard-visuals").then((m) => ({ default: m.SettlementStatusChart })), + { ssr: false }, +); + type HotPlayTab = "4D" | "3D" | "2D" | "special"; function formatMoneyMinor(minor: number, currencyCode: string | null): string { diff --git a/src/modules/risk/risk-draw-header.tsx b/src/modules/risk/risk-draw-header.tsx index 799068b..aed0edc 100644 --- a/src/modules/risk/risk-draw-header.tsx +++ b/src/modules/risk/risk-draw-header.tsx @@ -1,42 +1,28 @@ "use client"; -import { AdminLoadingState, AdminLoadingInline, AdminTableLoadingRow } from "@/components/admin/admin-loading-state"; +import { AdminLoadingInline } from "@/components/admin/admin-loading-state"; -import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useAsyncEffect } from "@/hooks/use-async-effect"; import { useTranslationRef } from "@/hooks/use-translation-ref"; +import { useApiQuery } from "@/hooks/use-api-query"; import { getAdminDraw } from "@/api/admin-draws"; import { drawStatusLabel, hallPreviewDiffersFromDbStatus } from "@/modules/draws/draw-display"; import { DrawStatusBadge } from "@/modules/draws/draw-status-badge"; import { LotteryApiBizError } from "@/types/api/errors"; -import type { AdminDrawShowData } from "@/types/api/admin-draws"; export function RiskDrawHeader({ drawId }: { drawId: number }) { const { t } = useTranslation(["risk", "draws"]); const tRef = useTranslationRef(["risk", "draws"]); - const [draw, setDraw] = useState(null); - const [error, setError] = useState(null); - const load = useCallback(async () => { - setError(null); - try { - const d = await getAdminDraw(drawId); - setDraw(d); - } catch (e) { - const msg = - e instanceof LotteryApiBizError ? e.message : tRef.current("drawInfoLoadFailed"); - setError(msg); - setDraw(null); - } - }, [drawId]); - - useAsyncEffect(() => { - void load(); - }, [drawId]); + const { data: draw, error } = useApiQuery( + ["admin/draws", drawId], + () => getAdminDraw(drawId), + ); if (error) { - return

{error}

; + const msg = + error instanceof LotteryApiBizError ? error.message : tRef.current("drawInfoLoadFailed"); + return

{msg}

; } if (!draw) {