feat: 添加财务摘要接口,更新管理员抽奖模块和导航,优化权限管理逻辑

This commit is contained in:
2026-05-11 16:21:22 +08:00
parent f083b28fc6
commit b539bf0660
57 changed files with 2134 additions and 108 deletions

View File

@@ -32,7 +32,6 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { LotteryApiBizError } from "@/types/api/errors";
import type {
@@ -183,28 +182,7 @@ function walletAdminSelectDisplayedLabel(
return options.find((o) => o.value === v)?.label ?? v;
}
export function WalletConsole(): React.ReactElement {
return (
<Tabs defaultValue="txns" className="w-full">
<TabsList variant="line" className="mb-6 w-full justify-start">
<TabsTrigger value="txns"></TabsTrigger>
<TabsTrigger value="orders"></TabsTrigger>
<TabsTrigger value="player"></TabsTrigger>
</TabsList>
<TabsContent value="orders">
<TransferOrdersPanel />
</TabsContent>
<TabsContent value="txns">
<WalletTxnsPanel />
</TabsContent>
<TabsContent value="player">
<PlayerWalletPanel />
</TabsContent>
</Tabs>
);
}
function TransferOrdersPanel(): React.ReactElement {
export function TransferOrdersPanel(): React.ReactElement {
const formatTs = useAdminDateTimeFormatter();
const [data, setData] = useState<AdminTransferOrderListData | null>(null);
const [loading, setLoading] = useState(true);
@@ -267,7 +245,8 @@ function TransferOrdersPanel(): React.ReactElement {
<CardTitle></CardTitle>
<CardDescription>
§5.11 {" "}
<code className="rounded bg-muted px-1">player_id</code> ID
<code className="rounded bg-muted px-1">player_id</code> ID PRD §15.4{" "}
<code className="rounded bg-muted px-1">status=failed</code>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
@@ -393,6 +372,7 @@ function TransferOrdersPanel(): React.ReactElement {
<TableHead className="w-14"></TableHead>
<TableHead className="whitespace-nowrap"></TableHead>
<TableHead className="whitespace-nowrap"></TableHead>
<TableHead className="min-w-0 max-w-[14rem]"></TableHead>
<TableHead className="min-w-0 whitespace-normal leading-tight">
</TableHead>
@@ -405,7 +385,7 @@ function TransferOrdersPanel(): React.ReactElement {
<TableBody>
{data.items.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="text-muted-foreground">
<TableCell colSpan={10} className="text-muted-foreground">
</TableCell>
</TableRow>
@@ -432,6 +412,9 @@ function TransferOrdersPanel(): React.ReactElement {
<TableCell>
<Badge variant={statusBadgeVariant(row.status)}>{row.status}</Badge>
</TableCell>
<TableCell className="max-w-[14rem] whitespace-normal break-words text-xs text-muted-foreground">
{row.fail_reason?.trim() ? row.fail_reason : "—"}
</TableCell>
<TableCell className="min-w-0 whitespace-normal font-mono text-[11px] leading-snug text-muted-foreground">
{formatTs(row.created_at)}
</TableCell>
@@ -465,7 +448,7 @@ function TransferOrdersPanel(): React.ReactElement {
);
}
function WalletTxnsPanel(): React.ReactElement {
export function WalletTxnsPanel(): React.ReactElement {
const formatTs = useAdminDateTimeFormatter();
const [data, setData] = useState<AdminWalletTxnListData | null>(null);
const [loading, setLoading] = useState(true);
@@ -758,7 +741,7 @@ function WalletTxnsPanel(): React.ReactElement {
);
}
function PlayerWalletPanel(): React.ReactElement {
export function PlayerWalletPanel(): React.ReactElement {
const [playerId, setPlayerId] = useState("");
const [result, setResult] = useState<AdminPlayerWalletsData | null>(null);
const [err, setErr] = useState<string | null>(null);

View File

@@ -0,0 +1,63 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { useAdminProfile } from "@/stores/admin-session";
const RECONCILE_PERMS = [
"prd.wallet_reconcile.manage",
"prd.wallet_reconcile.view",
"prd.wallet_reconcile.view_cs",
] as const;
const USER_PERMS = [
"prd.users.manage",
"prd.users.view_finance",
"prd.users.view_cs",
] as const;
const tabs: { href: string; label: string; requiredAny: readonly string[] }[] = [
{ href: "/admin/wallet/transactions", label: "钱包流水", requiredAny: RECONCILE_PERMS },
{ href: "/admin/wallet/transfer-orders", label: "转账单", requiredAny: RECONCILE_PERMS },
{ href: "/admin/wallet/player", label: "玩家钱包", requiredAny: USER_PERMS },
];
export function WalletSubnav(): React.ReactElement {
const pathname = usePathname();
const profile = useAdminProfile();
const perms = profile?.permissions;
return (
<nav
aria-label="钱包子页"
className="mb-6 flex flex-wrap gap-2 border-b border-border pb-3"
>
{tabs.map((t) => {
const allowed = adminHasAnyPermission(perms, [...t.requiredAny]);
const active = pathname === t.href || pathname.startsWith(`${t.href}/`);
const className = cn(
"rounded-lg px-3 py-1.5 text-sm font-medium transition-colors",
active
? "bg-primary text-primary-foreground"
: "bg-muted/60 text-foreground hover:bg-muted",
!allowed && "cursor-not-allowed opacity-45",
);
if (!allowed) {
return (
<span key={t.href} className={className} title="当前账号无访问该页的权限">
{t.label}
</span>
);
}
return (
<Link key={t.href} href={t.href} className={className}>
{t.label}
</Link>
);
})}
</nav>
);
}