refactor: 合并多语言支持的显示名称字段,优化奖池手动爆发功能的返回数据结构,增强管理端权限控制

This commit is contained in:
2026-05-25 14:31:24 +08:00
parent 7d01e5c47e
commit ddedef824e
101 changed files with 3033 additions and 641 deletions

View File

@@ -0,0 +1,44 @@
import type { ReactNode } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";
type AdminPageCardProps = {
title: string;
description?: string;
actions?: ReactNode;
children: ReactNode;
className?: string;
contentClassName?: string;
/** 页内锚点(如 #records */
id?: string;
};
/** 与列表/运营台一致的 admin-list-card 外层,用于设置、奖池等多区块页。 */
export function AdminPageCard({
title,
description,
actions,
children,
className,
contentClassName,
id,
}: AdminPageCardProps) {
return (
<Card id={id} className={cn("admin-list-card scroll-mt-24", className)}>
<CardHeader
className={cn(
"admin-list-header",
actions != null && "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between",
)}
>
<div className="min-w-0 space-y-1">
<CardTitle className="admin-list-title">{title}</CardTitle>
{description ? <CardDescription className="text-sm">{description}</CardDescription> : null}
</div>
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}
</CardHeader>
<CardContent className={cn("admin-list-content", contentClassName)}>{children}</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,30 @@
import type { ReactNode } from "react";
import { AdminSectionHeader } from "@/components/admin/admin-section-header";
import { cn } from "@/lib/utils";
type AdminPageSectionProps = {
title: string;
description?: string;
actions?: ReactNode;
children: ReactNode;
className?: string;
id?: string;
};
/** 无 Card 的页内分区,标题样式与 ConfigSection 相同。 */
export function AdminPageSection({
title,
description,
actions,
children,
className,
id,
}: AdminPageSectionProps) {
return (
<section id={id} className={cn("scroll-mt-24 space-y-4", className)}>
<AdminSectionHeader title={title} description={description} actions={actions} />
{children}
</section>
);
}

View File

@@ -0,0 +1,40 @@
"use client";
import type { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { useAdminProfile } from "@/stores/admin-session";
type AdminPermissionGateProps = {
requiredAny: readonly string[];
children: ReactNode;
className?: string;
};
/** 深链进入无权限页面时展示拒绝说明,避免空白或反复 403。 */
export function AdminPermissionGate({
requiredAny,
children,
className,
}: AdminPermissionGateProps): React.ReactElement {
const { t } = useTranslation("common");
const profile = useAdminProfile();
const allowed = adminHasAnyPermission(profile?.permissions, [...requiredAny]);
if (allowed) {
return <>{children}</>;
}
return (
<Card className={className ?? "admin-list-card"}>
<CardHeader>
<CardTitle className="text-base">{t("permission.deniedTitle")}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{t("permission.deniedDescription")}</p>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,33 @@
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
type AdminSectionHeaderProps = {
title: string;
description?: string;
actions?: ReactNode;
className?: string;
};
/** 页内区块标题(与 ConfigSection 一致),用于 Card 内子分区或配置文档。 */
export function AdminSectionHeader({
title,
description,
actions,
className,
}: AdminSectionHeaderProps) {
return (
<div
className={cn(
"flex flex-wrap items-start justify-between gap-3 border-b border-border/60 pb-3",
className,
)}
>
<div className="min-w-0 space-y-1">
<h3 className="text-base font-semibold text-foreground">{title}</h3>
{description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
</div>
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}
</div>
);
}

View File

@@ -18,7 +18,7 @@ import {
SidebarRail,
SidebarSeparator,
} from "@/components/ui/sidebar";
import { adminNavIconBySegment } from "@/modules/_config/admin-nav-icons";
import { resolveAdminNavIcon } from "@/modules/_config/admin-nav-icons";
import { ADMIN_BASE } from "@/modules/_config/admin-nav";
import { useAdminProfile } from "@/stores/admin-session";
@@ -82,7 +82,7 @@ export function AdminAppSidebar() {
<SidebarGroupContent>
<SidebarMenu>
{visibleNav.map((item) => {
const Icon = adminNavIconBySegment[item.segment];
const Icon = resolveAdminNavIcon(item.segment);
return (
<SidebarMenuItem key={item.segment}>
<SidebarMenuButton

View File

@@ -0,0 +1,63 @@
"use client";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
type ConfirmActionDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description: string;
confirmLabel: string;
cancelLabel: string;
confirmVariant?: "default" | "destructive";
busy?: boolean;
onConfirm: () => void;
};
export function ConfirmActionDialog({
open,
onOpenChange,
title,
description,
confirmLabel,
cancelLabel,
confirmVariant = "destructive",
busy = false,
onConfirm,
}: ConfirmActionDialogProps) {
const { t } = useTranslation("common");
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button type="button" variant="outline" disabled={busy} onClick={() => onOpenChange(false)}>
{cancelLabel}
</Button>
<Button
type="button"
variant={confirmVariant}
disabled={busy}
onClick={onConfirm}
>
{busy ? t("actions.submitting") : confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -5,7 +5,6 @@ import {
LogOutIcon,
UserRoundIcon,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
@@ -67,11 +66,12 @@ export function ShellToolbar() {
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link href="/admin/account" className="flex items-center gap-2 cursor-pointer">
<UserRoundIcon className="size-4" />
{t("toolbar.accountSettings")}
</Link>
<DropdownMenuItem
className="flex cursor-pointer items-center gap-2"
onClick={() => router.push("/admin/account")}
>
<UserRoundIcon className="size-4" />
{t("toolbar.accountSettings")}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />