feat: 扩展开奖与结算管理,支持手动操作、导出和版本展示

This commit is contained in:
2026-05-16 18:00:57 +08:00
parent 34f9175304
commit fae8c1ae01
21 changed files with 1148 additions and 410 deletions

View File

@@ -2,8 +2,16 @@
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { getAdminSettlementBatch, getAdminSettlementBatchDetails } from "@/api/admin-settlement";
import {
downloadAdminSettlementBatchExport,
getAdminSettlementBatch,
getAdminSettlementBatchDetails,
postAdminApproveSettlementBatch,
postAdminPayoutSettlementBatch,
postAdminRejectSettlementBatch,
} from "@/api/admin-settlement";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { Button, buttonVariants } from "@/components/ui/button";
@@ -37,6 +45,7 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
const [err, setErr] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(25);
const [acting, setActing] = useState<string | null>(null);
const load = useCallback(async () => {
setLoading(true);
@@ -57,6 +66,38 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
}
}, [batchId, page, perPage]);
async function runAction(label: string, action: () => Promise<unknown>): Promise<void> {
setActing(label);
try {
await action();
toast.success(`${label}成功`);
await load();
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : `${label}失败`);
} finally {
setActing(null);
}
}
async function exportCsv(): Promise<void> {
setActing("导出");
try {
const blob = await downloadAdminSettlementBatchExport(batchId);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `settlement-${batchId}.csv`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : "导出失败");
} finally {
setActing(null);
}
}
useEffect(() => {
const t = window.setTimeout(() => void load(), 0);
return () => window.clearTimeout(t);
@@ -98,6 +139,10 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
<span className="text-muted-foreground"></span>{" "}
<span className="font-mono">{summary.status}</span>
</p>
<p>
<span className="text-muted-foreground"></span>{" "}
<span className="font-mono">{summary.review_status ?? "—"}</span>
</p>
<p>
<span className="text-muted-foreground"></span>{" "}
<span className="tabular-nums">{summary.total_ticket_count}</span>
@@ -122,6 +167,37 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
<p>
<span className="text-muted-foreground"></span> {formatDt(summary.finished_at)}
</p>
<div className="flex flex-wrap gap-2 sm:col-span-2">
<Button
type="button"
size="sm"
variant="outline"
disabled={acting !== null || summary.status !== "pending_review"}
onClick={() => void runAction("审核通过", () => postAdminApproveSettlementBatch(batchId))}
>
</Button>
<Button
type="button"
size="sm"
variant="outline"
disabled={acting !== null || summary.status !== "pending_review"}
onClick={() => void runAction("驳回", () => postAdminRejectSettlementBatch(batchId))}
>
</Button>
<Button
type="button"
size="sm"
disabled={acting !== null || summary.status !== "approved"}
onClick={() => void runAction("执行派彩", () => postAdminPayoutSettlementBatch(batchId))}
>
</Button>
<Button type="button" size="sm" variant="secondary" disabled={acting !== null} onClick={() => void exportCsv()}>
</Button>
</div>
</CardContent>
</Card>
) : loading ? (

View File

@@ -2,8 +2,15 @@
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { getAdminSettlementBatches } from "@/api/admin-settlement";
import {
downloadAdminSettlementBatchExport,
getAdminSettlementBatches,
postAdminApproveSettlementBatch,
postAdminPayoutSettlementBatch,
postAdminRejectSettlementBatch,
} from "@/api/admin-settlement";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { Button, buttonVariants } from "@/components/ui/button";
@@ -52,6 +59,7 @@ export function SettlementBatchesConsole() {
const [appliedStatus, setAppliedStatus] = useState(STATUS_ALL);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(20);
const [actingId, setActingId] = useState<number | null>(null);
const load = useCallback(async () => {
setLoading(true);
@@ -86,6 +94,38 @@ export function SettlementBatchesConsole() {
setPage(1);
};
async function runBatchAction(batchId: number, label: string, action: () => Promise<unknown>): Promise<void> {
setActingId(batchId);
try {
await action();
toast.success(`${label}成功`);
await load();
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : `${label}失败`);
} finally {
setActingId(null);
}
}
async function exportBatch(batchId: number): Promise<void> {
setActingId(batchId);
try {
const blob = await downloadAdminSettlementBatchExport(batchId);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `settlement-${batchId}.csv`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : "导出失败");
} finally {
setActingId(null);
}
}
return (
<ModuleScaffold>
<div className="mb-6">
@@ -142,6 +182,7 @@ export function SettlementBatchesConsole() {
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
<TableHead className="text-right"></TableHead>
@@ -157,6 +198,9 @@ export function SettlementBatchesConsole() {
<TableCell className="font-mono text-xs">{row.id}</TableCell>
<TableCell className="font-mono text-sm">{row.draw_no ?? "—"}</TableCell>
<TableCell className="font-mono text-xs">v{row.settle_version}</TableCell>
<TableCell className="text-xs text-muted-foreground">
{row.review_status ?? "—"}
</TableCell>
<TableCell>
<span
className={cn(
@@ -181,12 +225,49 @@ export function SettlementBatchesConsole() {
{formatDt(row.finished_at ?? row.started_at)}
</TableCell>
<TableCell>
<Link
href={`/admin/settlement-batches/${row.id}/details`}
className={cn(buttonVariants({ variant: "link", size: "sm" }), "px-0")}
>
</Link>
<div className="flex flex-wrap justify-end gap-1.5">
<Link
href={`/admin/settlement-batches/${row.id}/details`}
className={cn(buttonVariants({ variant: "link", size: "sm" }), "px-0")}
>
</Link>
<Button
type="button"
size="sm"
variant="outline"
disabled={actingId !== null || row.status !== "pending_review"}
onClick={() => void runBatchAction(row.id, "审核通过", () => postAdminApproveSettlementBatch(row.id))}
>
</Button>
<Button
type="button"
size="sm"
variant="outline"
disabled={actingId !== null || row.status !== "pending_review"}
onClick={() => void runBatchAction(row.id, "驳回", () => postAdminRejectSettlementBatch(row.id))}
>
</Button>
<Button
type="button"
size="sm"
disabled={actingId !== null || row.status !== "approved"}
onClick={() => void runBatchAction(row.id, "执行派彩", () => postAdminPayoutSettlementBatch(row.id))}
>
</Button>
<Button
type="button"
size="sm"
variant="secondary"
disabled={actingId !== null}
onClick={() => void exportBatch(row.id)}
>
</Button>
</div>
</TableCell>
</TableRow>
))}