feat: 增加角色管理与奖池配置迁移,优化管理端权限与样式

This commit is contained in:
2026-05-19 14:40:04 +08:00
parent d625c59393
commit a1fb163f1b
45 changed files with 1080 additions and 518 deletions

View File

@@ -26,7 +26,12 @@ 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";
import {
PRD_DRAW_REOPEN_MANAGE,
PRD_DRAW_RESULT_MANAGE,
PRD_PAYOUT_MANAGE,
PRD_PAYOUT_REVIEW,
} from "./draw-prd";
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
@@ -42,7 +47,11 @@ 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 canReopenDraw = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_REOPEN_MANAGE]);
const canRunSettlement = adminHasAnyPermission(profile?.permissions, [
PRD_PAYOUT_MANAGE,
PRD_PAYOUT_REVIEW,
]);
const formatDt = useAdminDateTimeFormatter();
const [data, setData] = useState<AdminDrawShowData | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -159,17 +168,16 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">{t("drawActions")}</CardTitle>
<p className="text-sm text-muted-foreground">
{t("drawActionsDesc")}
</p>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
{(canManageDraw || canReopenDraw || canRunSettlement) ? (
<Card>
<CardHeader>
<CardTitle className="text-base">{t("drawActions")}</CardTitle>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
<Button
type="button"
variant="secondary"
variant="outline"
size="sm"
disabled={!canManageDraw || acting !== null || !["pending", "open"].includes(data.status)}
onClick={() => void runAction(t("manualClose"), () => postAdminManualCloseDraw(idNum))}
>
@@ -178,6 +186,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
<Button
type="button"
variant="outline"
size="sm"
disabled={!canManageDraw || acting !== null || !["pending", "open", "closing", "closed"].includes(data.status)}
onClick={() => void runAction(t("cancelDraw"), () => postAdminCancelDraw(idNum))}
>
@@ -185,15 +194,18 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
</Button>
<Button
type="button"
variant="outline"
size="sm"
disabled={!canManageDraw || acting !== null || data.status !== "closed"}
onClick={() => void runAction(t("rngDraw"), () => postAdminRunDrawRng(idNum))}
>
{acting === t("rngDraw") ? t("generating") : t("rngAutoGenerate")}
</Button>
{isSuperAdmin ? (
{canReopenDraw ? (
<Button
type="button"
variant="destructive"
size="sm"
disabled={acting !== null || data.status !== "cooldown"}
onClick={() => void runAction(t("reopen"), () => postAdminReopenDraw(idNum))}
>
@@ -203,13 +215,15 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
<Button
type="button"
variant="outline"
disabled={acting !== null || !["settling", "cooldown"].includes(data.status)}
size="sm"
disabled={!canRunSettlement || acting !== null || data.status !== "settling"}
onClick={() => void runAction(t("runSettlement"), () => postAdminRunDrawSettlement(idNum))}
>
{acting === t("runSettlement") ? t("processing") : t("runSettlement")}
</Button>
</CardContent>
</Card>
</CardContent>
</Card>
) : null}
</div>
);
}

View File

@@ -16,14 +16,23 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { cn } from "@/lib/utils";
import { useAdminProfile } from "@/stores/admin-session";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance";
import { toast } from "sonner";
import { PRD_PAYOUT_MANAGE, PRD_PAYOUT_REVIEW } from "./draw-prd";
export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactElement {
const { t } = useTranslation(["draws", "common"]);
const idNum = Number(drawId);
const profile = useAdminProfile();
const canRunSettlement = adminHasAnyPermission(profile?.permissions, [
PRD_PAYOUT_MANAGE,
PRD_PAYOUT_REVIEW,
]);
const [data, setData] = useState<AdminDrawFinanceSummaryData | null>(null);
const [err, setErr] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
@@ -122,7 +131,12 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
<Button type="button" variant="secondary" size="sm" onClick={() => void load()}>
{t("actions.refresh", { ns: "common" })}
</Button>
<Button type="button" size="sm" disabled={settling} onClick={() => void runSettlement()}>
<Button
type="button"
size="sm"
disabled={!canRunSettlement || settling || data.draw_status !== "settling"}
onClick={() => void runSettlement()}
>
{settling ? t("processing") : t("runSettlement")}
</Button>
<Link

View File

@@ -1,2 +1,5 @@
/** 开奖结果发布权限 slug */
export const PRD_DRAW_RESULT_MANAGE = "prd.draw_result.manage" as const;
export const PRD_DRAW_REOPEN_MANAGE = "prd.draw_reopen.manage" as const;
export const PRD_PAYOUT_MANAGE = "prd.payout.manage" as const;
export const PRD_PAYOUT_REVIEW = "prd.payout.review" as const;

View File

@@ -27,10 +27,13 @@ import {
TableRow,
} from "@/components/ui/table";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { cn } from "@/lib/utils";
import { useAdminProfile } from "@/stores/admin-session";
import { LotteryApiBizError } from "@/types/api/errors";
import type { AdminDrawListData, AdminDrawListItem } from "@/types/api/admin-draws";
import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd";
import { DrawStatusBadge } from "./draw-status-badge";
/** 下拉「不限」;请求时不传 status */
@@ -62,6 +65,8 @@ function drawAdminStatusSelectLabel(raw: unknown, t: (key: string) => string): s
export function DrawsIndexConsole() {
const { t } = useTranslation(["draws", "common"]);
const formatDt = useAdminDateTimeFormatter();
const profile = useAdminProfile();
const canManageDraw = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]);
const [data, setData] = useState<AdminDrawListData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -138,9 +143,11 @@ export function DrawsIndexConsole() {
<Card>
<CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<CardTitle className="text-lg">{t("statusListTitle")}</CardTitle>
<Button type="button" onClick={() => void generatePlan()} disabled={generating}>
{generating ? t("generating") : t("generatePlan")}
</Button>
{canManageDraw ? (
<Button type="button" onClick={() => void generatePlan()} disabled={generating}>
{generating ? t("generating") : t("generatePlan")}
</Button>
) : null}
</CardHeader>
<CardContent className="space-y-4">
{/* Grid桌面端标签一行 / 控件一行,避免 flex+items-end 与各列实际高度不一致;移动端单列自上而下 */}