feat: 扩展开奖与结算管理,支持手动操作、导出和版本展示
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user