feat(dashboard, risk): add SWR query hook and lazy load charts
This commit is contained in:
22
package-lock.json
generated
22
package-lock.json
generated
@@ -25,6 +25,7 @@
|
|||||||
"recharts": "^3.8.0",
|
"recharts": "^3.8.0",
|
||||||
"shadcn": "^4.7.0",
|
"shadcn": "^4.7.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
|
"swr": "^2.4.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
@@ -4520,6 +4521,14 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -9716,6 +9725,19 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/tagged-tag": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"recharts": "^3.8.0",
|
"recharts": "^3.8.0",
|
||||||
"shadcn": "^4.7.0",
|
"shadcn": "^4.7.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
|
"swr": "^2.4.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
|
|||||||
30
src/hooks/use-api-query.ts
Normal file
30
src/hooks/use-api-query.ts
Normal file
@@ -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<Data = unknown, Error = unknown>(
|
||||||
|
key: string | readonly unknown[] | null,
|
||||||
|
fetcher: () => Promise<Data>,
|
||||||
|
options?: SWRConfiguration<Data, Error>,
|
||||||
|
): SWRResponse<Data, Error> {
|
||||||
|
return useSWR<Data, Error>(key, fetcher, {
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
dedupingInterval: 2_000,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -23,10 +24,6 @@ import { getAdminRequestLocale } from "@/lib/admin-locale";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { DashboardKpiCard } from "@/modules/dashboard/dashboard-visuals";
|
import { DashboardKpiCard } from "@/modules/dashboard/dashboard-visuals";
|
||||||
import { DASHBOARD_CHART_COLORS } from "@/modules/dashboard/dashboard-chart-config";
|
import { DASHBOARD_CHART_COLORS } from "@/modules/dashboard/dashboard-chart-config";
|
||||||
import {
|
|
||||||
DailyTrendChart,
|
|
||||||
PlayBreakdownChart,
|
|
||||||
} from "@/modules/dashboard/dashboard-trend-charts";
|
|
||||||
import {
|
import {
|
||||||
DASHBOARD_ANALYTICS_PERIODS,
|
DASHBOARD_ANALYTICS_PERIODS,
|
||||||
DASHBOARD_RANKING_METRICS,
|
DASHBOARD_RANKING_METRICS,
|
||||||
@@ -34,6 +31,16 @@ import {
|
|||||||
type DashboardAnalyticsState,
|
type DashboardAnalyticsState,
|
||||||
} from "@/modules/dashboard/use-dashboard-analytics";
|
} 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 {
|
function computeDeltaPercent(series: number[]): string | null {
|
||||||
if (series.length < 2) {
|
if (series.length < 2) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback, useEffect, useMemo, useState, type ReactElement, type ReactNode } from "react";
|
import { useCallback, useEffect, useMemo, useState, type ReactElement, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -35,16 +36,6 @@ import { AdminNoResourceState, AdminTableNoResourceRow } from "@/components/admi
|
|||||||
import { Button, buttonVariants } from "@/components/ui/button";
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
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 { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog";
|
||||||
import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime";
|
import { adminWeekdayKeyForDate, formatAdminCalendarToday } from "@/lib/admin-datetime";
|
||||||
import { normalizeAdminLanguage } from "@/i18n";
|
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 { AdminRiskPoolRow } from "@/types/api/admin-risk";
|
||||||
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
|
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";
|
type HotPlayTab = "4D" | "3D" | "2D" | "special";
|
||||||
|
|
||||||
function formatMoneyMinor(minor: number, currencyCode: string | null): string {
|
function formatMoneyMinor(minor: number, currencyCode: string | null): string {
|
||||||
|
|||||||
@@ -1,42 +1,28 @@
|
|||||||
"use client";
|
"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 { useTranslation } from "react-i18next";
|
||||||
import { useAsyncEffect } from "@/hooks/use-async-effect";
|
|
||||||
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
import { useTranslationRef } from "@/hooks/use-translation-ref";
|
||||||
|
import { useApiQuery } from "@/hooks/use-api-query";
|
||||||
|
|
||||||
import { getAdminDraw } from "@/api/admin-draws";
|
import { getAdminDraw } from "@/api/admin-draws";
|
||||||
import { drawStatusLabel, hallPreviewDiffersFromDbStatus } from "@/modules/draws/draw-display";
|
import { drawStatusLabel, hallPreviewDiffersFromDbStatus } from "@/modules/draws/draw-display";
|
||||||
import { DrawStatusBadge } from "@/modules/draws/draw-status-badge";
|
import { DrawStatusBadge } from "@/modules/draws/draw-status-badge";
|
||||||
import { LotteryApiBizError } from "@/types/api/errors";
|
import { LotteryApiBizError } from "@/types/api/errors";
|
||||||
import type { AdminDrawShowData } from "@/types/api/admin-draws";
|
|
||||||
|
|
||||||
export function RiskDrawHeader({ drawId }: { drawId: number }) {
|
export function RiskDrawHeader({ drawId }: { drawId: number }) {
|
||||||
const { t } = useTranslation(["risk", "draws"]);
|
const { t } = useTranslation(["risk", "draws"]);
|
||||||
const tRef = useTranslationRef(["risk", "draws"]);
|
const tRef = useTranslationRef(["risk", "draws"]);
|
||||||
const [draw, setDraw] = useState<AdminDrawShowData | null>(null);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const { data: draw, error } = useApiQuery(
|
||||||
setError(null);
|
["admin/draws", drawId],
|
||||||
try {
|
() => getAdminDraw(drawId),
|
||||||
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]);
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <p className="text-sm text-destructive">{error}</p>;
|
const msg =
|
||||||
|
error instanceof LotteryApiBizError ? error.message : tRef.current("drawInfoLoadFailed");
|
||||||
|
return <p className="text-sm text-destructive">{msg}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!draw) {
|
if (!draw) {
|
||||||
|
|||||||
Reference in New Issue
Block a user