diff --git a/src/api/admin-dashboard.ts b/src/api/admin-dashboard.ts new file mode 100644 index 0000000..1f84641 --- /dev/null +++ b/src/api/admin-dashboard.ts @@ -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 { + return adminRequest.get(`${A}/dashboard`); +} diff --git a/src/modules/dashboard/dashboard-console.tsx b/src/modules/dashboard/dashboard-console.tsx index cb51a5b..f452788 100644 --- a/src/modules/dashboard/dashboard-console.tsx +++ b/src/modules/dashboard/dashboard-console.tsx @@ -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 { - 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(null); + const [notice, setNotice] = useState(null); const [hall, setHall] = useState(null); const [drawId, setDrawId] = useState(null); const [finance, setFinance] = useState(null); - const [drawDetail, setDrawDetail] = useState(null); + const [pendingReview, setPendingReview] = useState(null); const [riskLocked, setRiskLocked] = useState(0); const [riskCap, setRiskCap] = useState(0); const [hotPoolSample, setHotPoolSample] = useState([]); @@ -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 { ) : null} + {notice && !error ? ( + + 说明 + {notice} + + ) : null} + {/* Row 1 — 核心财务 KPI */}
{loading ? ( diff --git a/src/types/api/admin-dashboard.ts b/src/types/api/admin-dashboard.ts new file mode 100644 index 0000000..782da8b --- /dev/null +++ b/src/types/api/admin-dashboard.ts @@ -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; +};