feat: 添加财务摘要接口,更新管理员抽奖模块和导航,优化权限管理逻辑
This commit is contained in:
@@ -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);
|
||||
|
||||
63
src/modules/wallet/wallet-subnav.tsx
Normal file
63
src/modules/wallet/wallet-subnav.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user