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,10 +2,12 @@
import Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { getAdminDrawResultBatches } from "@/api/admin-draws";
import { buttonVariants } from "@/components/ui/button";
import { getAdminDrawResultBatches, postAdminCreateManualResultBatch } from "@/api/admin-draws";
import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
@@ -23,6 +25,22 @@ import type { AdminDrawBatchesData } from "@/types/api/admin-draws";
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
import { DrawStatusBadge } from "./draw-status-badge";
const RESULT_SLOTS = [
{ prize_type: "first", prize_index: 0, label: "头奖" },
{ prize_type: "second", prize_index: 0, label: "二奖" },
{ prize_type: "third", prize_index: 0, label: "三奖" },
...Array.from({ length: 10 }, (_, i) => ({
prize_type: "starter",
prize_index: i,
label: `特别奖 ${i + 1}`,
})),
...Array.from({ length: 10 }, (_, i) => ({
prize_type: "consolation",
prize_index: i,
label: `安慰奖 ${i + 1}`,
})),
] as const;
export function DrawReviewConsole({ drawId }: { drawId: string }) {
const profile = useAdminProfile();
const canManageDraw = adminHasAnyPermission(profile?.permissions, [
@@ -32,6 +50,10 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
const [data, setData] = useState<AdminDrawBatchesData | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [savingManual, setSavingManual] = useState(false);
const [manualNumbers, setManualNumbers] = useState<string[]>(
() => RESULT_SLOTS.map(() => ""),
);
const load = useCallback(async () => {
if (!Number.isFinite(idNum)) {
@@ -62,6 +84,33 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
data,
]);
async function saveManualDraft(): Promise<void> {
if (!Number.isFinite(idNum)) return;
const invalid = manualNumbers.some((n) => !/^[0-9]{4}$/.test(n));
if (invalid) {
toast.error("请完整输入 23 组 4 位数字");
return;
}
setSavingManual(true);
try {
const res = await postAdminCreateManualResultBatch(idNum, {
items: RESULT_SLOTS.map((slot, i) => ({
prize_type: slot.prize_type,
prize_index: slot.prize_index,
number_4d: manualNumbers[i],
})),
});
toast.success(`已保存草稿 v${res.batch.result_version},等待确认发布`);
setManualNumbers(RESULT_SLOTS.map(() => ""));
await load();
} catch (e) {
toast.error(e instanceof LotteryApiBizError ? e.message : "保存失败");
} finally {
setSavingManual(false);
}
}
if (loading && !data) {
return <p className="text-sm text-muted-foreground"></p>;
}
@@ -71,14 +120,59 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
}
return (
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<p className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<DrawStatusBadge status={data.draw_status} />
</p>
</CardHeader>
<CardContent>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<p className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<DrawStatusBadge status={data.draw_status} /> ·
</p>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{RESULT_SLOTS.map((slot, i) => (
<label key={`${slot.prize_type}-${slot.prize_index}`} className="space-y-1.5">
<span className="text-xs font-medium text-muted-foreground">{slot.label}</span>
<Input
inputMode="numeric"
maxLength={4}
value={manualNumbers[i]}
disabled={!canManageDraw || savingManual}
placeholder="0000"
className="font-mono"
onChange={(e) => {
const next = e.target.value.replace(/\D/g, "").slice(0, 4);
setManualNumbers((old) => old.map((v, idx) => (idx === i ? next : v)));
}}
/>
</label>
))}
</div>
<div className="flex flex-wrap items-center justify-end gap-2">
<Button
type="button"
variant="outline"
disabled={savingManual}
onClick={() => setManualNumbers(RESULT_SLOTS.map(() => ""))}
>
</Button>
<Button
type="button"
disabled={!canManageDraw || savingManual || !["closed", "review"].includes(data.draw_status)}
onClick={() => void saveManualDraft()}
>
{savingManual ? "保存中…" : "保存草稿"}
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent>
{pending.length === 0 ? (
<p className="text-sm text-muted-foreground py-6 text-center">
pending_review
@@ -116,7 +210,8 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
</TableBody>
</Table>
)}
</CardContent>
</Card>
</CardContent>
</Card>
</div>
);
}