feat: 重构仪表盘逻辑,整合管理员仪表盘API,移除冗余代码,优化数据加载和状态管理

This commit is contained in:
2026-05-11 17:02:37 +08:00
parent d0f75fcec8
commit 217ed7c02f
3 changed files with 108 additions and 140 deletions

View File

@@ -0,0 +1,12 @@
import { adminRequest } from "@/lib/admin-http";
import { API_V1_PREFIX } from "./paths";
import type { AdminDashboardData } from "@/types/api/admin-dashboard";
const A = `${API_V1_PREFIX}/admin`;
/** 首页仪表盘聚合(大厅 + 当期财务/风控/异常转账等,按账号权限填充各块) */
export async function getAdminDashboard(): Promise<AdminDashboardData> {
return adminRequest.get<AdminDashboardData>(`${A}/dashboard`);
}

View File

@@ -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 ? (

View File

@@ -0,0 +1,62 @@
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";
export type AdminDashboardWarning = {
code: string;
message: string;
};
export type AdminDashboardResolvedDraw = {
id: number;
draw_no: string;
};
export type AdminDashboardDrawPanel = {
id: number;
draw_no: string;
business_date: string;
sequence_no: number;
status: string;
hall_preview_status: string;
result_batch_counts: {
total: number;
pending_review: number;
published: number;
};
};
export type AdminDashboardSoldOutBuckets = {
d4: number;
d3: number;
d2: number;
special: number;
other: number;
};
export type AdminDashboardRiskSnapshot = {
draw_id: number;
draw_no: string;
locked_amount: number;
cap_amount: number;
usage_percent: number;
hot_pool_rows: AdminRiskPoolRow[];
sold_out_buckets: AdminDashboardSoldOutBuckets;
};
export type AdminDashboardCapabilities = {
draw_finance_risk: boolean;
wallet_transfer_view: boolean;
};
/** `GET /api/v1/admin/dashboard` */
export type AdminDashboardData = {
hall: DrawCurrentSnapshot | null;
resolved_draw: AdminDashboardResolvedDraw | null;
finance: AdminDrawFinanceSummaryData | null;
draw: AdminDashboardDrawPanel | null;
risk: AdminDashboardRiskSnapshot | null;
abnormal_transfer_total: number | null;
warnings: AdminDashboardWarning[];
capabilities: AdminDashboardCapabilities;
};