将 AdminRiskPage 改名为 AdminRiskIndexPage,并接入 RiskIndexConsole 组件。

This commit is contained in:
2026-05-11 11:52:53 +08:00
parent 78045de9a3
commit 0103d25426
16 changed files with 1033 additions and 15 deletions

View File

@@ -0,0 +1,195 @@
"use client";
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { getAdminRiskPoolDetail } from "@/api/admin-risk";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { formatAdminMinorUnits } from "@/lib/money";
import { cn } from "@/lib/utils";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminRiskPoolDetailLogRow, AdminRiskPoolShowData } from "@/types/api/admin-risk";
export function RiskPoolDetailConsole({
drawId,
number4d,
}: {
drawId: number;
number4d: string;
}) {
const formatDt = useAdminDateTimeFormatter();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(20);
const [data, setData] = useState<AdminRiskPoolShowData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const d = await getAdminRiskPoolDetail(drawId, number4d, { page, per_page: perPage });
setData(d);
} catch (e) {
const msg =
e instanceof LotteryApiBizError ? e.message : "加载风险池详情失败";
setError(msg);
setData(null);
} finally {
setLoading(false);
}
}, [drawId, number4d, page, perPage]);
useEffect(() => {
queueMicrotask(() => {
void load();
});
}, [load]);
if (error && !data) {
return (
<Card className="border-destructive/40">
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription>{error}</CardDescription>
</CardHeader>
<CardContent>
<Link
href={`/admin/risk/draws/${drawId}/pools`}
className={cn(buttonVariants({ variant: "outline", size: "sm" }))}
>
</Link>
</CardContent>
</Card>
);
}
if (loading && !data) {
return <p className="text-sm text-muted-foreground"></p>;
}
if (!data) {
return null;
}
const { pool, logs } = data;
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-2">
<Link
href={`/admin/risk/draws/${drawId}/pools`}
className={cn(buttonVariants({ variant: "ghost", size: "sm" }))}
>
</Link>
</div>
<Card>
<CardHeader>
<CardTitle className="text-lg">
<span className="font-mono">{pool.normalized_number}</span>
</CardTitle>
<CardDescription>
{data.draw_no} · `risk_pools` 0 §6.4
</CardDescription>
</CardHeader>
<CardContent className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<div className="rounded-lg border bg-muted/40 p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 font-mono text-sm font-medium tabular-nums">
{formatAdminMinorUnits(pool.total_cap_amount)}
</p>
</div>
<div className="rounded-lg border bg-muted/40 p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 font-mono text-sm font-medium tabular-nums">
{formatAdminMinorUnits(pool.locked_amount)}
</p>
</div>
<div className="rounded-lg border bg-muted/40 p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 font-mono text-sm font-medium tabular-nums">
{formatAdminMinorUnits(pool.remaining_amount)}
</p>
</div>
<div className="rounded-lg border bg-muted/40 p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="mt-1 text-sm font-medium">{pool.is_sold_out ? "是" : "否"}</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{" "}
{pool.usage_ratio != null ? `${(pool.usage_ratio * 100).toFixed(2)}%` : "—"}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> / </CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="overflow-x-auto rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.items.map((row: AdminRiskPoolDetailLogRow) => (
<TableRow key={row.id}>
<TableCell className="whitespace-nowrap text-xs text-muted-foreground">
{row.created_at ? formatDt(row.created_at) : "—"}
</TableCell>
<TableCell className="text-sm">{row.action_type}</TableCell>
<TableCell className="text-right text-sm tabular-nums">
{formatAdminMinorUnits(row.amount)}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{row.source_reason ?? "—"}
</TableCell>
<TableCell className="font-mono text-xs">{row.ticket_no ?? "—"}</TableCell>
<TableCell className="text-xs">{row.play_code ?? "—"}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<AdminListPaginationFooter
selectId={`risk-pool-detail-${drawId}-${number4d}`}
total={logs.meta.total}
page={logs.meta.current_page}
lastPage={logs.meta.last_page}
perPage={logs.meta.per_page}
loading={loading}
onPerPageChange={(n) => {
setPerPage(n);
setPage(1);
}}
onPageChange={setPage}
/>
</CardContent>
</Card>
</div>
);
}