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,19 +2,30 @@
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { getAdminDraw } from "@/api/admin-draws";
import { buttonVariants } from "@/components/ui/button";
import {
getAdminDraw,
postAdminCancelDraw,
postAdminManualCloseDraw,
postAdminReopenDraw,
postAdminRunDrawRng,
} from "@/api/admin-draws";
import { postAdminRunDrawSettlement } from "@/api/admin-settlement";
import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawShowData } from "@/types/api/admin-draws";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { useAdminProfile } from "@/stores/admin-session";
import { cn } from "@/lib/utils";
import { DrawStatusBadge } from "./draw-status-badge";
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
@@ -27,10 +38,14 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
export function DrawDetailConsole({ drawId }: { drawId: string }) {
const idNum = Number(drawId);
const profile = useAdminProfile();
const canManageDraw = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]);
const isSuperAdmin = profile?.permissions?.includes("prd.admin_user.manage") ?? false;
const formatDt = useAdminDateTimeFormatter();
const [data, setData] = useState<AdminDrawShowData | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [acting, setActing] = useState<string | null>(null);
const load = useCallback(async () => {
if (!Number.isFinite(idNum)) {
@@ -50,6 +65,20 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
}
}, [idNum]);
async function runAction(name: string, action: () => Promise<unknown>): Promise<void> {
if (!Number.isFinite(idNum)) return;
setActing(name);
try {
await action();
toast.success(`${name}成功`);
await load();
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : `${name}失败`);
} finally {
setActing(null);
}
}
useEffect(() => {
const timer = window.setTimeout(() => {
void load();
@@ -124,6 +153,58 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
<p className="text-sm text-muted-foreground">
/ / RNG / /
</p>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
<Button
type="button"
variant="secondary"
disabled={!canManageDraw || acting !== null || !["pending", "open"].includes(data.status)}
onClick={() => void runAction("手动封盘", () => postAdminManualCloseDraw(idNum))}
>
{acting === "手动封盘" ? "处理中…" : "手动封盘"}
</Button>
<Button
type="button"
variant="outline"
disabled={!canManageDraw || acting !== null || !["pending", "open", "closing", "closed"].includes(data.status)}
onClick={() => void runAction("取消期号", () => postAdminCancelDraw(idNum))}
>
{acting === "取消期号" ? "处理中…" : "未开奖前取消"}
</Button>
<Button
type="button"
disabled={!canManageDraw || acting !== null || data.status !== "closed"}
onClick={() => void runAction("RNG开奖", () => postAdminRunDrawRng(idNum))}
>
{acting === "RNG开奖" ? "生成中…" : "RNG 自动生成"}
</Button>
{isSuperAdmin ? (
<Button
type="button"
variant="destructive"
disabled={acting !== null || data.status !== "cooldown"}
onClick={() => void runAction("重开", () => postAdminReopenDraw(idNum))}
>
{acting === "重开" ? "处理中…" : "冷静期重开"}
</Button>
) : null}
<Button
type="button"
variant="outline"
disabled={acting !== null || !["settling", "cooldown"].includes(data.status)}
onClick={() => void runAction("触发结算", () => postAdminRunDrawSettlement(idNum))}
>
{acting === "触发结算" ? "处理中…" : "触发结算"}
</Button>
</CardContent>
</Card>
</div>
);
}