feat(draws, i18n): 新增批量删除功能并增强多语言支持

在后台开奖管理模块中新增删除待发布结果批次的 API 接口。
更新 DrawPublishConsole 与 DrawReviewConsole 组件,新增“丢弃批次”按钮,支持删除草稿状态的结果批次。
新增删除成功与删除失败的 Toast 提示,优化用户操作反馈体验。
在英文、尼泊尔语与中文语言包中新增批量删除确认及删除成功相关翻译文案,完善多语言支持。
提升开奖管理流程的灵活性与易用性,方便管理员快速清理无效或误创建的批次数据。
This commit is contained in:
2026-06-01 15:41:28 +08:00
parent d30c135dde
commit 53bf64cc53
10 changed files with 272 additions and 43 deletions

View File

@@ -1,11 +1,16 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { getAdminDrawResultBatches, postAdminPublishResultBatch } from "@/api/admin-draws";
import {
deleteAdminPendingResultBatch,
getAdminDrawResultBatches,
postAdminPublishResultBatch,
} from "@/api/admin-draws";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
@@ -29,6 +34,7 @@ import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchId: string }) {
const { t } = useTranslation(["draws", "common"]);
const router = useRouter();
const profile = useAdminProfile();
const canManageDraw = adminHasAnyPermission(profile?.permissions, [
PRD_DRAW_RESULT_MANAGE,
@@ -39,6 +45,7 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [publishing, setPublishing] = useState(false);
const [discarding, setDiscarding] = useState(false);
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const load = useCallback(async () => {
@@ -71,6 +78,22 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
return data?.batches.find((b) => b.id === batchNum);
}, [batchNum, data]);
async function discardBatch(): Promise<void> {
if (!Number.isFinite(idNum) || !Number.isFinite(batchNum)) return;
setDiscarding(true);
try {
await deleteAdminPendingResultBatch(idNum, batchNum);
toast.success(t("discardPendingBatchSuccess"));
router.replace(`/admin/draws/${drawId}/review`);
} catch (e) {
toast.error(
e instanceof LotteryApiBizError ? e.message : t("discardPendingBatchFailed"),
);
} finally {
setDiscarding(false);
}
}
async function publish(): Promise<void> {
if (!Number.isFinite(idNum) || !Number.isFinite(batchNum)) return;
setPublishing(true);
@@ -139,10 +162,13 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
</Alert>
) : null}
{canPublish ? (
<Alert>
<AlertTitle>{t("checkBeforePublish")}</AlertTitle>
<AlertDescription>{t("checkBeforePublishDesc")}</AlertDescription>
</Alert>
<>
<Alert>
<AlertTitle>{t("checkBeforePublish")}</AlertTitle>
<AlertDescription>{t("checkBeforePublishDesc")}</AlertDescription>
</Alert>
<p className="text-sm text-muted-foreground">{t("publishReadOnlyHint")}</p>
</>
) : null}
<div className="overflow-x-auto rounded-lg border border-border">
@@ -176,16 +202,33 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
})}
</p>
</CardContent>
<CardFooter className="justify-end gap-2">
<CardFooter className="flex flex-wrap justify-end gap-2">
<Link
href={`/admin/draws/${drawId}/results`}
className={cn(buttonVariants({ variant: "outline", size: "default" }))}
>
{t("publishedView")}
</Link>
{canPublish ? (
<Button
type="button"
variant="outline"
disabled={publishing || discarding}
onClick={() =>
requestConfirm({
title: t("confirm.discardPendingBatchTitle"),
description: t("confirm.discardPendingBatchDescription"),
confirmVariant: "destructive",
onConfirm: () => discardBatch(),
})
}
>
{discarding ? t("discardingPendingBatch") : t("discardPendingBatch")}
</Button>
) : null}
<Button
type="button"
disabled={!canPublish || publishing}
disabled={!canPublish || publishing || discarding}
onClick={() =>
requestConfirm({
title: t("confirm.publishTitle"),

View File

@@ -1,11 +1,15 @@
"use client";
import { Dices, Rocket } from "lucide-react";
import { Dices, Rocket, Trash2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { getAdminDrawResultBatches, postAdminCreateManualResultBatch } from "@/api/admin-draws";
import {
deleteAdminPendingResultBatch,
getAdminDrawResultBatches,
postAdminCreateManualResultBatch,
} from "@/api/admin-draws";
import { AdminRowActionsMenu } from "@/components/admin/admin-row-actions-menu";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -61,6 +65,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [savingManual, setSavingManual] = useState(false);
const [discardingBatchId, setDiscardingBatchId] = useState<number | null>(null);
const { request: requestConfirm, ConfirmDialog } = useConfirmAction();
const [manualNumbers, setManualNumbers] = useState<string[]>(
() => RESULT_SLOTS.map(() => ""),
@@ -99,6 +104,22 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
setManualNumbers(RESULT_SLOTS.map(() => randomDrawNumber4d()));
}
async function discardPendingBatch(batchId: number): Promise<void> {
if (!Number.isFinite(idNum)) return;
setDiscardingBatchId(batchId);
try {
await deleteAdminPendingResultBatch(idNum, batchId);
toast.success(t("discardPendingBatchSuccess"));
await load();
} catch (e) {
toast.error(
e instanceof LotteryApiBizError ? e.message : t("discardPendingBatchFailed"),
);
} finally {
setDiscardingBatchId(null);
}
}
async function saveManualDraft(): Promise<void> {
if (!Number.isFinite(idNum)) return;
const invalid = manualNumbers.some((n) => !/^[0-9]{4}$/.test(n));
@@ -233,6 +254,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
<TableCell className="text-center">
{canManageDraw ? (
<AdminRowActionsMenu
busy={discardingBatchId === b.id}
actions={[
{
key: "publish",
@@ -240,6 +262,20 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) {
icon: Rocket,
href: `/admin/draws/${drawId}/publish/${b.id}`,
},
{
key: "discard",
label: t("discardPendingBatch"),
icon: Trash2,
destructive: true,
disabled: discardingBatchId !== null,
onClick: () =>
requestConfirm({
title: t("confirm.discardPendingBatchTitle"),
description: t("confirm.discardPendingBatchDescription"),
confirmVariant: "destructive",
onConfirm: () => discardPendingBatch(b.id),
}),
},
]}
/>
) : (