refactor: 合并多语言支持的显示名称字段,优化奖池手动爆发功能的返回数据结构,增强管理端权限控制
This commit is contained in:
@@ -3,10 +3,11 @@
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AdminPageCard } from "@/components/admin/admin-page-card";
|
||||
import { JackpotPoolsConsole } from "@/modules/jackpot/jackpot-pools-console";
|
||||
import { JackpotRecordsConsole } from "@/modules/jackpot/jackpot-records-console";
|
||||
|
||||
/** 奖池单页:池参数 + 流水记录,避免 ConfigDocPage / 内层 Card 重复套娃。 */
|
||||
/** 奖池单页:池参数 + 流水记录,与列表/设置页共用 admin-list-card 布局。 */
|
||||
export function JackpotConfigScreen() {
|
||||
const { t } = useTranslation("jackpot");
|
||||
|
||||
@@ -23,20 +24,14 @@ export function JackpotConfigScreen() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<section className="space-y-4">
|
||||
<h2 className="border-b border-border/60 pb-3 text-base font-semibold text-foreground">
|
||||
{t("poolsSectionTitle")}
|
||||
</h2>
|
||||
<div className="flex w-full max-w-none flex-col gap-6">
|
||||
<AdminPageCard title={t("poolsSectionTitle")}>
|
||||
<JackpotPoolsConsole embedded />
|
||||
</section>
|
||||
</AdminPageCard>
|
||||
|
||||
<section id="jackpot-records" className="scroll-mt-24 space-y-4">
|
||||
<h2 className="border-b border-border/60 pb-3 text-base font-semibold text-foreground">
|
||||
{t("recordsSectionTitle")}
|
||||
</h2>
|
||||
<AdminPageCard id="jackpot-records" title={t("recordsSectionTitle")}>
|
||||
<JackpotRecordsConsole embedded />
|
||||
</section>
|
||||
</AdminPageCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
postAdminJackpotManualBurst,
|
||||
putAdminJackpotPool,
|
||||
} from "@/api/admin-jackpot";
|
||||
import { useConfirmAction } from "@/hooks/use-confirm-action";
|
||||
import { adminHasAnyPermission } from "@/lib/admin-permissions";
|
||||
import { PRD_JACKPOT_MANAGE, PRD_JACKPOT_MANUAL_BURST } from "@/lib/admin-prd";
|
||||
import { ModuleScaffold } from "@/components/admin/module-scaffold";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
@@ -20,7 +23,16 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { toast } from "sonner";
|
||||
import { useAdminProfile } from "@/stores/admin-session";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type { AdminJackpotPoolRow } from "@/types/api/admin-jackpot";
|
||||
|
||||
@@ -34,7 +46,6 @@ type Draft = {
|
||||
combo_trigger_play_codes: string;
|
||||
status: string;
|
||||
manual_burst_draw_id: string;
|
||||
manual_burst_amount: string;
|
||||
};
|
||||
|
||||
function toDraft(p: AdminJackpotPoolRow): Draft {
|
||||
@@ -48,7 +59,6 @@ function toDraft(p: AdminJackpotPoolRow): Draft {
|
||||
combo_trigger_play_codes: p.combo_trigger_play_codes.join(","),
|
||||
status: String(p.status),
|
||||
manual_burst_draw_id: "",
|
||||
manual_burst_amount: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,11 +69,16 @@ type JackpotPoolsConsoleProps = {
|
||||
|
||||
export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsoleProps) {
|
||||
const { t } = useTranslation(["jackpot", "common"]);
|
||||
const profile = useAdminProfile();
|
||||
const canManageJackpot = adminHasAnyPermission(profile?.permissions, [PRD_JACKPOT_MANAGE]);
|
||||
const canManualBurst = adminHasAnyPermission(profile?.permissions, [PRD_JACKPOT_MANUAL_BURST]);
|
||||
const { request: requestConfirm, ConfirmDialog: ConfirmActionDialog } = useConfirmAction();
|
||||
const [items, setItems] = useState<AdminJackpotPoolRow[]>([]);
|
||||
const [drafts, setDrafts] = useState<Record<number, Draft>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [savingId, setSavingId] = useState<number | null>(null);
|
||||
const [burstingId, setBurstingId] = useState<number | null>(null);
|
||||
const [confirmBurstPoolId, setConfirmBurstPoolId] = useState<number | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -131,22 +146,18 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = d.manual_burst_amount.trim()
|
||||
? Number.parseInt(d.manual_burst_amount, 10)
|
||||
: undefined;
|
||||
|
||||
setBurstingId(p.id);
|
||||
try {
|
||||
await postAdminJackpotManualBurst(p.id, {
|
||||
draw_id: drawId,
|
||||
amount: amount !== undefined && Number.isFinite(amount) ? amount : undefined,
|
||||
});
|
||||
toast.success(t("manualBurstSuccess"));
|
||||
const res = await postAdminJackpotManualBurst(p.id, { draw_id: drawId });
|
||||
toast.success(
|
||||
`${t("manualBurstSuccess")} · ${res.draw_no} · ${res.winner_count} ${t("winnerCount")}`,
|
||||
);
|
||||
await load();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : t("manualBurstFailed"));
|
||||
} finally {
|
||||
setBurstingId(null);
|
||||
setConfirmBurstPoolId(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -164,7 +175,7 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
||||
className="space-y-4 rounded-xl border border-border/60 bg-muted/10 p-4"
|
||||
>
|
||||
<h3 className="font-mono text-sm font-semibold">{p.currency_code}</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<fieldset disabled={!canManageJackpot} className="grid gap-4 border-0 p-0 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`amt-${p.id}`}>{t("currentAmount")}</Label>
|
||||
<Input
|
||||
@@ -244,16 +255,31 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-border/60 pt-3">
|
||||
<Button type="button" disabled={savingId === p.id} onClick={() => void save(p)}>
|
||||
{savingId === p.id ? t("saving") : t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
{canManageJackpot ? (
|
||||
<div className="flex justify-end border-t border-border/60 pt-3">
|
||||
<Button
|
||||
type="button"
|
||||
disabled={savingId === p.id}
|
||||
onClick={() =>
|
||||
requestConfirm({
|
||||
title: t("confirmSavePoolTitle"),
|
||||
description: t("confirmSavePoolDescription"),
|
||||
confirmLabel: t("confirm.confirmSave", { ns: "common" }),
|
||||
onConfirm: () => save(p),
|
||||
})
|
||||
}
|
||||
>
|
||||
{savingId === p.id ? t("saving") : t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{canManualBurst ? (
|
||||
<div className="rounded-lg border border-amber-200/80 bg-amber-50/80 p-4 dark:border-amber-900/50 dark:bg-amber-950/30">
|
||||
<p className="mb-3 text-xs font-medium text-amber-900 dark:text-amber-200">
|
||||
<p className="mb-1 text-xs font-medium text-amber-900 dark:text-amber-200">
|
||||
{t("manualBurst")}
|
||||
</p>
|
||||
<p className="mb-3 text-xs text-amber-800/90 dark:text-amber-300/90">{t("manualBurstHint")}</p>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end">
|
||||
<div className="min-w-0 flex-1 space-y-1.5 sm:max-w-xs">
|
||||
<Label htmlFor={`burst-draw-${p.id}`}>{t("manualBurstDrawId")}</Label>
|
||||
@@ -264,34 +290,63 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
||||
onChange={(e) => updateDraft(p.id, { manual_burst_draw_id: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 space-y-1.5 sm:max-w-xs">
|
||||
<Label htmlFor={`burst-amount-${p.id}`}>{t("manualBurstAmount")}</Label>
|
||||
<Input
|
||||
id={`burst-amount-${p.id}`}
|
||||
className="font-mono"
|
||||
value={d.manual_burst_amount}
|
||||
onChange={(e) => updateDraft(p.id, { manual_burst_amount: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
className="shrink-0 sm:ml-auto"
|
||||
disabled={burstingId === p.id}
|
||||
onClick={() => void manualBurst(p)}
|
||||
onClick={() => setConfirmBurstPoolId(p.id)}
|
||||
>
|
||||
{burstingId === p.id ? t("processing") : t("manualBurst")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
const confirmPool = confirmBurstPoolId !== null ? items.find((p) => p.id === confirmBurstPoolId) : null;
|
||||
const confirmDraft = confirmPool ? drafts[confirmPool.id] : null;
|
||||
|
||||
const confirmDialog = (
|
||||
<Dialog open={confirmBurstPoolId !== null} onOpenChange={(open) => !open && setConfirmBurstPoolId(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("manualBurstConfirmTitle")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("manualBurstConfirmDescription", {
|
||||
drawId: confirmDraft?.manual_burst_draw_id ?? "—",
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setConfirmBurstPoolId(null)}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
disabled={confirmPool === undefined || burstingId !== null}
|
||||
onClick={() => confirmPool && void manualBurst(confirmPool)}
|
||||
>
|
||||
{t("manualBurstConfirm")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
if (embedded) {
|
||||
return poolList;
|
||||
return (
|
||||
<>
|
||||
{poolList}
|
||||
{confirmDialog}
|
||||
<ConfirmActionDialog />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -302,6 +357,8 @@ export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsolePro
|
||||
</CardHeader>
|
||||
<CardContent>{poolList}</CardContent>
|
||||
</Card>
|
||||
{confirmDialog}
|
||||
<ConfirmActionDialog />
|
||||
</ModuleScaffold>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,8 +155,8 @@ export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsol
|
||||
};
|
||||
|
||||
const filterBlock = embedded ? (
|
||||
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<div className="flex max-w-xs flex-1 flex-col gap-1.5">
|
||||
<div className="admin-list-toolbar mb-0 border-t-0 pt-0">
|
||||
<div className="admin-list-field max-w-xs flex-1">
|
||||
<Label htmlFor="jk-draw">{t("drawNo")}</Label>
|
||||
<Input
|
||||
id="jk-draw"
|
||||
@@ -166,9 +166,11 @@ export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsol
|
||||
placeholder={t("optional")}
|
||||
/>
|
||||
</div>
|
||||
<Button type="button" onClick={applyDraw}>
|
||||
{t("apply")}
|
||||
</Button>
|
||||
<div className="admin-list-actions">
|
||||
<Button type="button" onClick={applyDraw}>
|
||||
{t("apply")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="mb-6">
|
||||
|
||||
Reference in New Issue
Block a user