193 lines
6.7 KiB
TypeScript
193 lines
6.7 KiB
TypeScript
"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, 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>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2">
|
||
<p className="text-sm text-destructive">{error}</p>
|
||
<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>
|
||
<p className="text-sm text-muted-foreground">期号 {data.draw_no}</p>
|
||
</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>
|
||
</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>
|
||
);
|
||
}
|