feat: 重构仪表盘逻辑,整合管理员仪表盘API,移除冗余代码,优化数据加载和状态管理
This commit is contained in:
@@ -20,10 +20,7 @@ import {
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
|
||||
import { getAdminDraw, getAdminDrawFinanceSummary, getAdminDraws } from "@/api/admin-draws";
|
||||
import { getAdminRiskPools } from "@/api/admin-risk";
|
||||
import { getAdminTransferOrders } from "@/api/admin-wallet";
|
||||
import { getDrawCurrent } from "@/api/public-draw";
|
||||
import { getAdminDashboard } from "@/api/admin-dashboard";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -31,7 +28,6 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
|
||||
import type { AdminDrawShowData } from "@/types/api/admin-draws";
|
||||
import type { AdminRiskPoolRow } from "@/types/api/admin-risk";
|
||||
import type { DrawCurrentSnapshot } from "@/types/api/public-draw";
|
||||
|
||||
@@ -97,84 +93,6 @@ function topPoolsForTab(pools: AdminRiskPoolRow[], tab: HotPlayTab): AdminRiskPo
|
||||
.slice(0, 10);
|
||||
}
|
||||
|
||||
function soldOutBucketKey(normalizedNumber: string): keyof SoldOutBuckets {
|
||||
const raw = normalizedNumber.trim();
|
||||
if (/[A-Za-z]/.test(raw)) {
|
||||
return "special";
|
||||
}
|
||||
const digits = raw.replace(/\D/g, "");
|
||||
const len = digits.length > 0 ? digits.length : raw.length;
|
||||
if (len >= 4) {
|
||||
return "d4";
|
||||
}
|
||||
if (len === 3) {
|
||||
return "d3";
|
||||
}
|
||||
if (len === 2) {
|
||||
return "d2";
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
|
||||
async function aggregateRiskPools(
|
||||
drawId: number,
|
||||
): Promise<{ locked: number; cap: number; soldOutCount: number; top10: AdminRiskPoolRow[] }> {
|
||||
let locked = 0;
|
||||
let cap = 0;
|
||||
let soldOutCount = 0;
|
||||
let top10: AdminRiskPoolRow[] = [];
|
||||
let page = 1;
|
||||
const safetyMaxPages = 200;
|
||||
|
||||
while (page <= safetyMaxPages) {
|
||||
const d = await getAdminRiskPools(drawId, {
|
||||
page,
|
||||
per_page: 100,
|
||||
sort: "usage_desc",
|
||||
});
|
||||
if (page === 1) {
|
||||
top10 = d.items.slice(0, 10);
|
||||
}
|
||||
for (const it of d.items) {
|
||||
locked += it.locked_amount;
|
||||
cap += it.total_cap_amount;
|
||||
if (it.is_sold_out) {
|
||||
soldOutCount += 1;
|
||||
}
|
||||
}
|
||||
if (page >= d.meta.last_page) {
|
||||
break;
|
||||
}
|
||||
page += 1;
|
||||
}
|
||||
|
||||
return { locked, cap, soldOutCount, top10 };
|
||||
}
|
||||
|
||||
async function aggregateSoldOutBuckets(drawId: number): Promise<SoldOutBuckets> {
|
||||
const buckets: SoldOutBuckets = { d4: 0, d3: 0, d2: 0, special: 0, other: 0 };
|
||||
let page = 1;
|
||||
const safetyMaxPages = 80;
|
||||
|
||||
while (page <= safetyMaxPages) {
|
||||
const d = await getAdminRiskPools(drawId, {
|
||||
page,
|
||||
per_page: 100,
|
||||
sold_out_only: true,
|
||||
sort: "usage_desc",
|
||||
});
|
||||
for (const it of d.items) {
|
||||
buckets[soldOutBucketKey(it.normalized_number)] += 1;
|
||||
}
|
||||
if (page >= d.meta.last_page) {
|
||||
break;
|
||||
}
|
||||
page += 1;
|
||||
}
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
function RiskSemiGauge({ pct }: { pct: number }): ReactElement {
|
||||
const v = Math.min(100, Math.max(0, pct));
|
||||
const r = 76;
|
||||
@@ -320,11 +238,12 @@ export function DashboardConsole(): ReactElement {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
|
||||
const [hall, setHall] = useState<DrawCurrentSnapshot | null>(null);
|
||||
const [drawId, setDrawId] = useState<number | null>(null);
|
||||
const [finance, setFinance] = useState<AdminDrawFinanceSummaryData | null>(null);
|
||||
const [drawDetail, setDrawDetail] = useState<AdminDrawShowData | null>(null);
|
||||
const [pendingReview, setPendingReview] = useState<number | null>(null);
|
||||
const [riskLocked, setRiskLocked] = useState(0);
|
||||
const [riskCap, setRiskCap] = useState(0);
|
||||
const [hotPoolSample, setHotPoolSample] = useState<AdminRiskPoolRow[]>([]);
|
||||
@@ -339,8 +258,9 @@ export function DashboardConsole(): ReactElement {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
setFinance(null);
|
||||
setDrawDetail(null);
|
||||
setPendingReview(null);
|
||||
setDrawId(null);
|
||||
setRiskLocked(0);
|
||||
setRiskCap(0);
|
||||
@@ -349,67 +269,35 @@ export function DashboardConsole(): ReactElement {
|
||||
setAbnormalTransferTotal(null);
|
||||
|
||||
try {
|
||||
const current = await getDrawCurrent();
|
||||
setHall(current);
|
||||
const d = await getAdminDashboard();
|
||||
setHall(d.hall);
|
||||
|
||||
if (!current) {
|
||||
return;
|
||||
if (d.resolved_draw != null) {
|
||||
setDrawId(d.resolved_draw.id);
|
||||
}
|
||||
|
||||
let resolvedId: number | null = null;
|
||||
try {
|
||||
const list = await getAdminDraws({ draw_no: current.draw_no, per_page: 1, page: 1 });
|
||||
resolvedId = list.items[0]?.id ?? null;
|
||||
} catch {
|
||||
setError("已连接大厅期号,但需登录后台后才能拉取汇总与风控数据。");
|
||||
return;
|
||||
if (d.finance != null) {
|
||||
setFinance(d.finance);
|
||||
}
|
||||
if (d.draw != null) {
|
||||
setPendingReview(d.draw.result_batch_counts.pending_review);
|
||||
}
|
||||
if (d.risk != null) {
|
||||
setRiskLocked(d.risk.locked_amount);
|
||||
setRiskCap(d.risk.cap_amount);
|
||||
setHotPoolSample(d.risk.hot_pool_rows);
|
||||
setSoldOutBuckets(d.risk.sold_out_buckets);
|
||||
}
|
||||
setAbnormalTransferTotal(d.abnormal_transfer_total);
|
||||
|
||||
if (resolvedId == null) {
|
||||
setError("大厅期号在后台期号列表中未匹配到记录(或无权查看)。");
|
||||
return;
|
||||
const noticeParts: string[] = d.warnings.map((w) => w.message);
|
||||
if (d.resolved_draw != null && !d.capabilities.draw_finance_risk) {
|
||||
noticeParts.push("当前账号无开奖查看/管理权限,财务与风控数据未返回。");
|
||||
}
|
||||
|
||||
setDrawId(resolvedId);
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
getAdminDrawFinanceSummary(resolvedId),
|
||||
getAdminDraw(resolvedId),
|
||||
aggregateRiskPools(resolvedId),
|
||||
getAdminTransferOrders({ abnormal: true, per_page: 1, page: 1 }),
|
||||
getAdminRiskPools(resolvedId, { page: 1, per_page: 100, sort: "usage_desc" }),
|
||||
aggregateSoldOutBuckets(resolvedId),
|
||||
]);
|
||||
|
||||
if (results[0].status === "fulfilled") {
|
||||
setFinance(results[0].value);
|
||||
}
|
||||
if (results[1].status === "fulfilled") {
|
||||
setDrawDetail(results[1].value);
|
||||
}
|
||||
if (results[2].status === "fulfilled") {
|
||||
const r = results[2].value;
|
||||
setRiskLocked(r.locked);
|
||||
setRiskCap(r.cap);
|
||||
}
|
||||
if (results[3].status === "fulfilled") {
|
||||
setAbnormalTransferTotal(results[3].value.total);
|
||||
}
|
||||
if (results[4].status === "fulfilled") {
|
||||
setHotPoolSample(results[4].value.items);
|
||||
}
|
||||
if (results[5].status === "fulfilled") {
|
||||
setSoldOutBuckets(results[5].value);
|
||||
}
|
||||
|
||||
const failed = results.find((r) => r.status === "rejected");
|
||||
if (failed && failed.status === "rejected") {
|
||||
const msg =
|
||||
failed.reason instanceof LotteryApiBizError
|
||||
? failed.reason.message
|
||||
: "部分数据加载失败(权限或网络)。";
|
||||
setError(msg);
|
||||
if (d.hall != null && !d.capabilities.wallet_transfer_view) {
|
||||
noticeParts.push("当前账号无钱包对账查看权限,异常转账计数未返回。");
|
||||
}
|
||||
setNotice(noticeParts.length > 0 ? noticeParts.join(" ") : null);
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e instanceof LotteryApiBizError ? e.message : "加载失败,请检查 API 与登录状态。";
|
||||
@@ -429,7 +317,6 @@ export function DashboardConsole(): ReactElement {
|
||||
|
||||
const currency = finance?.currency_code ?? null;
|
||||
const usagePct = riskCap > 0 ? (riskLocked / riskCap) * 100 : 0;
|
||||
const pendingReview = drawDetail?.result_batch_counts?.pending_review ?? null;
|
||||
|
||||
const hotRows = useMemo(() => topPoolsForTab(hotPoolSample, hotTab), [hotPoolSample, hotTab]);
|
||||
|
||||
@@ -483,6 +370,13 @@ export function DashboardConsole(): ReactElement {
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{notice && !error ? (
|
||||
<Alert className="border-sky-200 bg-sky-50 dark:border-sky-900/50 dark:bg-sky-950/30">
|
||||
<AlertTitle>说明</AlertTitle>
|
||||
<AlertDescription>{notice}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{/* Row 1 — 核心财务 KPI */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{loading ? (
|
||||
|
||||
Reference in New Issue
Block a user