refactor: 重构奖池配置页面,移除冗余组件,优化加载体验与国际化支持

This commit is contained in:
2026-05-21 16:46:48 +08:00
parent 3ce84af39c
commit 26feed3c4f
29 changed files with 393 additions and 213 deletions

View File

@@ -1,20 +1,19 @@
import { JackpotSubNav } from "@/modules/jackpot/jackpot-subnav"; import { Suspense } from "react";
import { JackpotPoolsConsole } from "@/modules/jackpot/jackpot-pools-console";
import { JackpotConfigScreen } from "@/modules/jackpot/jackpot-config-screen";
import { jackpotModuleMeta } from "@/modules/jackpot/meta"; import { jackpotModuleMeta } from "@/modules/jackpot/meta";
import type { Metadata } from "next"; import type { Metadata } from "next";
export const metadata: Metadata = { export const metadata: Metadata = {
title: `奖池配置 · ${jackpotModuleMeta.title}`, title: jackpotModuleMeta.title,
}; };
export default function AdminConfigJackpotPage() { export default function AdminConfigJackpotPage() {
return ( return (
<div className="w-full max-w-none px-1"> <Suspense
<JackpotSubNav /> fallback={<p className="text-sm text-muted-foreground">Loading</p>}
<div className="mx-auto mb-6 max-w-5xl"> >
<h1 className="text-lg font-semibold tracking-tight">{jackpotModuleMeta.title}</h1> <JackpotConfigScreen />
</div> </Suspense>
<JackpotPoolsConsole />
</div>
); );
} }

View File

@@ -1,17 +1,6 @@
import { JackpotSubNav } from "@/modules/jackpot/jackpot-subnav"; import { redirect } from "next/navigation";
import { JackpotRecordsConsole } from "@/modules/jackpot/jackpot-records-console";
import { jackpotModuleMeta } from "@/modules/jackpot/meta";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: jackpotModuleMeta.title,
};
/** 旧路径保留跳转,避免书签失效 */
export default function AdminConfigJackpotRecordsPage() { export default function AdminConfigJackpotRecordsPage() {
return ( redirect("/admin/config/jackpot#records");
<div className="w-full max-w-none px-1">
<JackpotSubNav />
<JackpotRecordsConsole />
</div>
);
} }

View File

@@ -1,5 +1,5 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
export default function AdminJackpotRecordsRedirectPage() { export default function AdminJackpotRecordsRedirectPage() {
redirect("/admin/config/jackpot/records"); redirect("/admin/config/jackpot#records");
} }

View File

@@ -0,0 +1,39 @@
import type { ReactNode } from "react";
import { Badge } from "@/components/ui/badge";
import {
adminStatusBadgeClassName,
resolveAdminStatusTone,
type AdminStatusTone,
} from "@/lib/admin-status-tone";
import { cn } from "@/lib/utils";
type AdminStatusBadgeProps = {
children: ReactNode;
/** 用于自动解析色调;也可仅用 `tone` 显式指定 */
status?: string | number | boolean | null;
tone?: AdminStatusTone;
className?: string;
};
export function AdminStatusBadge({
children,
status,
tone,
className,
}: AdminStatusBadgeProps) {
const resolvedTone = tone ?? resolveAdminStatusTone(status);
return (
<Badge
variant="outline"
className={cn(
"font-medium whitespace-nowrap",
adminStatusBadgeClassName(resolvedTone),
className,
)}
>
{children}
</Badge>
);
}

View File

@@ -1,6 +1,10 @@
{ {
"title": "Jackpot", "title": "Jackpot",
"configTitle": "Jackpot pool configuration", "configTitle": "Jackpot pool configuration",
"pageDescription": "Maintain per-currency pool parameters; contribution and payout logs are below.",
"poolsSectionDescription": "Contribution rate, burst threshold, switch, and manual burst.",
"recordsSectionTitle": "Contribution & payout logs",
"recordsSectionDescription": "Filter payout and contribution entries (read-only).",
"loadFailed": "Failed to load", "loadFailed": "Failed to load",
"saveSuccess": "Saved", "saveSuccess": "Saved",
"saveFailed": "Save failed", "saveFailed": "Save failed",
@@ -35,9 +39,7 @@
"title": "Jackpot records", "title": "Jackpot records",
"description": "Payout records and pool contribution flows" "description": "Payout records and pool contribution flows"
}, },
"subnavLabel": "Jackpot sub navigation", "poolsSectionTitle": "Per-currency pool parameters",
"subnavPools": "Pool configuration",
"subnavRecords": "Records",
"payoutLoadFailed": "Failed to load payout records", "payoutLoadFailed": "Failed to load payout records",
"contributionLoadFailed": "Failed to load contribution records", "contributionLoadFailed": "Failed to load contribution records",
"trigger": "Trigger", "trigger": "Trigger",

View File

@@ -1,6 +1,10 @@
{ {
"title": "Jackpot", "title": "Jackpot",
"configTitle": "Jackpot पूल कन्फिगरेसन", "configTitle": "Jackpot पूल कन्फिगरेसन",
"pageDescription": "मुद्रा अनुसार पूल प्यारामिटर; तल योगदान र पेआउट लग देख्नुहोस्।",
"poolsSectionDescription": "योगदान दर, बर्स्ट थ्रेसहोल्ड, स्विच र म्यानुअल बर्स्ट।",
"recordsSectionTitle": "योगदान र पेआउट लग",
"recordsSectionDescription": "पेआउट र योगदान प्रविष्टि फिल्टर (पढ्न मात्र)।",
"loadFailed": "लोड असफल भयो", "loadFailed": "लोड असफल भयो",
"saveSuccess": "सुरक्षित भयो", "saveSuccess": "सुरक्षित भयो",
"saveFailed": "सुरक्षित गर्न असफल", "saveFailed": "सुरक्षित गर्न असफल",
@@ -35,9 +39,7 @@
"title": "Jackpot रेकर्ड", "title": "Jackpot रेकर्ड",
"description": "भुक्तानी रेकर्ड र पूल योगदान प्रवाह" "description": "भुक्तानी रेकर्ड र पूल योगदान प्रवाह"
}, },
"subnavLabel": "Jackpot उपनेभिगेसन", "poolsSectionTitle": "मुद्रा अनुसार पूल प्यारामिटर",
"subnavPools": "पूल कन्फिगरेसन",
"subnavRecords": "रेकर्ड",
"payoutLoadFailed": "भुक्तानी रेकर्ड लोड असफल भयो", "payoutLoadFailed": "भुक्तानी रेकर्ड लोड असफल भयो",
"contributionLoadFailed": "योगदान रेकर्ड लोड असफल भयो", "contributionLoadFailed": "योगदान रेकर्ड लोड असफल भयो",
"trigger": "ट्रिगर", "trigger": "ट्रिगर",

View File

@@ -1,6 +1,10 @@
{ {
"title": "奖池", "title": "奖池",
"configTitle": "奖池配置", "configTitle": "奖池配置",
"pageDescription": "维护各币种奖池参数,下方可查询蓄水与派彩流水。",
"poolsSectionDescription": "蓄水比例、爆池阈值、开关与手动爆池。",
"recordsSectionTitle": "蓄水与派彩流水",
"recordsSectionDescription": "按条件筛选派彩记录与蓄水明细,只读查询。",
"loadFailed": "加载失败", "loadFailed": "加载失败",
"saveSuccess": "已保存", "saveSuccess": "已保存",
"saveFailed": "保存失败", "saveFailed": "保存失败",
@@ -35,9 +39,7 @@
"title": "奖池记录", "title": "奖池记录",
"description": "派彩记录与奖池蓄水流水" "description": "派彩记录与奖池蓄水流水"
}, },
"subnavLabel": "奖池子导航", "poolsSectionTitle": "各币种奖池参数",
"subnavPools": "奖池配置",
"subnavRecords": "记录",
"payoutLoadFailed": "派彩记录加载失败", "payoutLoadFailed": "派彩记录加载失败",
"contributionLoadFailed": "蓄水记录加载失败", "contributionLoadFailed": "蓄水记录加载失败",
"trigger": "触发", "trigger": "触发",

View File

@@ -0,0 +1,133 @@
/**
* 后台状态色标:统一映射到可读色调,列表/详情一眼可辨。
*/
export type AdminStatusTone =
| "success"
| "info"
| "warning"
| "danger"
| "neutral"
| "indigo"
| "draft"
| "active"
| "archived";
export const ADMIN_STATUS_TONE_CLASSES: Record<AdminStatusTone, string> = {
success:
"border-emerald-500/30 bg-emerald-500/12 text-emerald-800 dark:text-emerald-300",
info: "border-primary/30 bg-primary/10 text-primary",
warning:
"border-amber-500/30 bg-amber-500/12 text-amber-900 dark:text-amber-300",
danger:
"border-destructive/35 bg-destructive/10 text-destructive dark:text-red-300",
neutral: "border-border/80 bg-muted text-muted-foreground",
indigo:
"border-indigo-500/30 bg-indigo-500/12 text-indigo-800 dark:text-indigo-300",
draft:
"border-amber-500/30 bg-amber-500/12 text-amber-900 dark:text-amber-300",
active:
"border-emerald-500/30 bg-emerald-500/12 text-emerald-800 dark:text-emerald-300",
archived:
"border-slate-400/35 bg-slate-500/10 text-slate-600 dark:text-slate-300",
};
const STATUS_TONE_MAP: Record<string, AdminStatusTone> = {
// 配置版本
draft: "draft",
active: "active",
archived: "archived",
// 期次
open: "info",
closing: "warning",
closed: "neutral",
drawing: "indigo",
review: "warning",
cooldown: "neutral",
pending: "info",
settling: "indigo",
settled: "success",
cancelled: "danger",
// 结算批次
running: "warning",
pending_review: "info",
approved: "indigo",
rejected: "neutral",
paid: "success",
completed: "success",
failed: "danger",
// 注单
pending_confirm: "info",
partial_pending_confirm: "warning",
success: "success",
pending_payout: "warning",
settled_win: "success",
settled_lose: "neutral",
// 钱包 / 划转
processing: "info",
posted: "success",
pending_reconcile: "warning",
reversed: "neutral",
manually_processed: "indigo",
// 对账
mismatch: "danger",
matched: "success",
pending_check: "warning",
// 开关 / 布尔
enabled: "success",
disabled: "neutral",
true: "success",
false: "neutral",
// 玩家 status 数值
"0": "success",
"1": "warning",
"2": "danger",
// 管理员/角色1=启用 0=停用)
"role_enabled": "success",
"role_disabled": "neutral",
};
/** 玩家0 正常 1 冻结 2 封禁 */
export function resolvePlayerStatusTone(status: number): AdminStatusTone {
if (status === 0) return "success";
if (status === 1) return "warning";
if (status === 2) return "danger";
return "neutral";
}
/** 后台用户0 启用 1 停用 */
export function resolveAdminUserStatusTone(status: number): AdminStatusTone {
return status === 0 ? "success" : "neutral";
}
/** 角色1 启用 0 停用 */
export function resolveRoleStatusTone(status: number): AdminStatusTone {
return status === 1 ? "success" : "neutral";
}
export function resolveAdminStatusTone(
status: string | number | boolean | null | undefined,
): AdminStatusTone {
if (status === null || status === undefined || status === "") {
return "neutral";
}
if (typeof status === "boolean") {
return status ? "success" : "neutral";
}
const key = String(status).trim().toLowerCase();
return STATUS_TONE_MAP[key] ?? "neutral";
}
export function adminStatusBadgeClassName(
tone: AdminStatusTone,
className?: string,
): string {
return `${ADMIN_STATUS_TONE_CLASSES[tone]} ${className ?? ""}`.trim();
}

View File

@@ -13,7 +13,9 @@ import {
putAdminRole, putAdminRole,
putAdminRolePermissions, putAdminRolePermissions,
} from "@/api/admin-users"; } from "@/api/admin-users";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -363,18 +365,9 @@ export function AdminRolesConsole(): React.ReactElement {
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell>
{role.status === 1 ? ( <AdminStatusBadge status={role.status} tone={resolveRoleStatusTone(role.status)}>
<Badge variant="secondary" className="font-normal"> {role.status === 1 ? t("status.enabled") : t("status.disabled")}
{t("status.enabled")} </AdminStatusBadge>
</Badge>
) : (
<Badge
variant="outline"
className="border-amber-600/50 text-amber-800 dark:text-amber-400"
>
{t("status.disabled")}
</Badge>
)}
</TableCell> </TableCell>
<TableCell className="tabular-nums">{role.user_count}</TableCell> <TableCell className="tabular-nums">{role.user_count}</TableCell>
<TableCell className="tabular-nums">{role.permission_slugs.length}</TableCell> <TableCell className="tabular-nums">{role.permission_slugs.length}</TableCell>

View File

@@ -14,7 +14,9 @@ import {
} from "@/api/admin-users"; } from "@/api/admin-users";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { resolveAdminUserStatusTone } from "@/lib/admin-status-tone";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
@@ -387,18 +389,9 @@ export function AdminUsersConsole(): React.ReactElement {
</TableCell> </TableCell>
<TableCell>{row.nickname ?? ""}</TableCell> <TableCell>{row.nickname ?? ""}</TableCell>
<TableCell> <TableCell>
{row.status === 0 ? ( <AdminStatusBadge status={row.status} tone={resolveAdminUserStatusTone(row.status)}>
<Badge variant="secondary" className="font-normal"> {row.status === 0 ? t("status.enabled") : t("status.disabled")}
{t("status.enabled")} </AdminStatusBadge>
</Badge>
) : (
<Badge
variant="outline"
className="border-amber-600/50 text-amber-800 dark:text-amber-400"
>
{t("status.disabled")}
</Badge>
)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">

View File

@@ -1,6 +1,12 @@
/** /**
* Single source of truth for config sub-navigation and breadcrumb routes. * Single source of truth for config sub-navigation and breadcrumb routes.
* Add new config pages here and create the matching `app/admin/(shell)/config/.../page.tsx`. * Add new config pages here and create the matching `app/admin/(shell)/config/.../page.tsx`.
*
* ## 导航层级约定(避免「侧栏 + 顶栏 + 页内 Tab」叠三层
* 1. **侧栏**:模块入口(如「运营配置」)— 全站唯一一级。
* 2. **本文件顶栏ConfigSubNav**:运营配置下的「业务子域」切换(玩法、赔率、奖池…)— 最多一层。
* 3. **页面内**:默认 **禁止再建 Tab/子路由**;改参数与查流水用 **同页分区ConfigSection** 或锚点 `#records`。
* 仅当子域体量极大、且与配置完全无关时再考虑独立路由,且不得与顶栏同名。
*/ */
export type ConfigNavGroup = { export type ConfigNavGroup = {

View File

@@ -8,6 +8,8 @@ type ConfigSectionProps = {
actions?: ReactNode; actions?: ReactNode;
children: ReactNode; children: ReactNode;
className?: string; className?: string;
/** 页内锚点,供旧链接跳转(如 #records */
id?: string;
}; };
export function ConfigSection({ export function ConfigSection({
@@ -16,9 +18,10 @@ export function ConfigSection({
actions, actions,
children, children,
className, className,
id,
}: ConfigSectionProps) { }: ConfigSectionProps) {
return ( return (
<section className={cn("space-y-4", className)}> <section id={id} className={cn("scroll-mt-24 space-y-4", className)}>
<div className="flex flex-wrap items-start justify-between gap-3 border-b border-border/60 pb-3"> <div className="flex flex-wrap items-start justify-between gap-3 border-b border-border/60 pb-3">
<div className="min-w-0 space-y-1"> <div className="min-w-0 space-y-1">
<h3 className="text-base font-semibold text-foreground">{title}</h3> <h3 className="text-base font-semibold text-foreground">{title}</h3>

View File

@@ -1,20 +1,15 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { resolveAdminStatusTone } from "@/lib/admin-status-tone";
export function ConfigStatusBadge({ status }: { status: string }) { export function ConfigStatusBadge({ status }: { status: string }) {
const { t } = useTranslation("config"); const { t } = useTranslation("config");
const label = t(`versionStatus.${status}`, { defaultValue: status }); const label = t(`versionStatus.${status}`, { defaultValue: status });
const className =
status === "active"
? "border-primary/30 bg-primary/10 text-primary"
: status === "draft"
? "border-border bg-secondary text-secondary-foreground"
: "border-border/80 bg-muted text-muted-foreground";
return ( return (
<Badge variant="outline" className={cn("font-medium tabular-nums", className)}> <AdminStatusBadge status={status} tone={resolveAdminStatusTone(status)}>
{label} {label}
</Badge> </AdminStatusBadge>
); );
} }

View File

@@ -37,6 +37,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value"; import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
import { ConfigVersionActions } from "@/modules/config/config-version-actions"; import { ConfigVersionActions } from "@/modules/config/config-version-actions";
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher"; import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
@@ -499,11 +500,13 @@ export function PlayConfigDocScreen() {
aria-label={t("play.aria.enablePlay", { ns: "config", playCode: row.play_code })} aria-label={t("play.aria.enablePlay", { ns: "config", playCode: row.play_code })}
/> />
) : ( ) : (
<ConfigReadonlyValue className="justify-center"> <div className="flex justify-center">
{row.is_enabled <AdminStatusBadge status={row.is_enabled ? "enabled" : "disabled"}>
? t("play.states.enabled", { ns: "config" }) {row.is_enabled
: t("play.states.disabled", { ns: "config" })} ? t("play.states.enabled", { ns: "config" })
</ConfigReadonlyValue> : t("play.states.disabled", { ns: "config" })}
</AdminStatusBadge>
</div>
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -595,7 +598,9 @@ export function PlayConfigDocScreen() {
{t("play.actions.ruleText", { ns: "config" })} {t("play.actions.ruleText", { ns: "config" })}
</Button> </Button>
) : ( ) : (
<span className="text-sm text-muted-foreground">{t("play.states.readOnly", { ns: "config" })}</span> <AdminStatusBadge status="disabled" className="mx-auto w-fit">
{t("play.states.readOnly", { ns: "config" })}
</AdminStatusBadge>
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -125,10 +125,12 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
status={data.status} status={data.status}
label={drawStatusText(data.status, t)} label={drawStatusText(data.status, t)}
/> />
<p className="text-sm text-muted-foreground"> <p className="flex flex-wrap items-center justify-end gap-2 text-sm text-muted-foreground">
{t("hallPreviewStatus", { <span>{t("hallPreviewStatus", { status: "" }).replace(/\{\{status\}\}/, "").replace(/\s+$/, "")}</span>
status: drawStatusText(data.hall_preview_status, t), <DrawStatusBadge
})} status={data.hall_preview_status}
label={drawStatusText(data.hall_preview_status, t)}
/>
</p> </p>
</div> </div>
</div> </div>

View File

@@ -7,7 +7,9 @@ import { useTranslation } from "react-i18next";
import { getAdminDrawFinanceSummary } from "@/api/admin-draws"; import { getAdminDrawFinanceSummary } from "@/api/admin-draws";
import { postAdminRunDrawSettlement } from "@/api/admin-settlement"; import { postAdminRunDrawSettlement } from "@/api/admin-settlement";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { DrawStatusBadge } from "@/modules/draws/draw-status-badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Table, Table,
@@ -104,7 +106,9 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
</div> </div>
<div> <div>
<span className="text-muted-foreground">{t("status")}</span> <span className="text-muted-foreground">{t("status")}</span>
<p>{drawStatusText(data.draw_status, t)}</p> <p className="mt-1">
<DrawStatusBadge status={data.draw_status} label={drawStatusText(data.draw_status, t)} />
</p>
</div> </div>
<div> <div>
<span className="text-muted-foreground">{t("orderAndItemCount")}</span> <span className="text-muted-foreground">{t("orderAndItemCount")}</span>
@@ -186,7 +190,9 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
{data.settlement_batches.map((b) => ( {data.settlement_batches.map((b) => (
<TableRow key={b.id}> <TableRow key={b.id}>
<TableCell className="font-mono text-xs">{b.id}</TableCell> <TableCell className="font-mono text-xs">{b.id}</TableCell>
<TableCell className="text-xs">{drawStatusText(b.status, t)}</TableCell> <TableCell>
<AdminStatusBadge status={b.status}>{drawStatusText(b.status, t)}</AdminStatusBadge>
</TableCell>
<TableCell className="text-right tabular-nums text-xs"> <TableCell className="text-right tabular-nums text-xs">
{b.total_ticket_count} {b.total_ticket_count}
</TableCell> </TableCell>

View File

@@ -1,14 +1,5 @@
import { Badge } from "@/components/ui/badge"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { resolveAdminStatusTone } from "@/lib/admin-status-tone";
const emphasis: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
open: "default",
closing: "destructive",
closed: "secondary",
drawing: "secondary",
review: "outline",
cooldown: "secondary",
pending: "outline",
};
export function DrawStatusBadge({ export function DrawStatusBadge({
status, status,
@@ -18,6 +9,9 @@ export function DrawStatusBadge({
/** 可与 DB 不同时展示预览态文案 */ /** 可与 DB 不同时展示预览态文案 */
label?: string; label?: string;
}) { }) {
const v = emphasis[status] ?? "outline"; return (
return <Badge variant={v}>{label ?? status}</Badge>; <AdminStatusBadge status={status} tone={resolveAdminStatusTone(status)}>
{label ?? status}
</AdminStatusBadge>
);
} }

View File

@@ -267,7 +267,10 @@ export function DrawsIndexConsole() {
<TableCell className="text-sm">{formatDt(row.close_time)}</TableCell> <TableCell className="text-sm">{formatDt(row.close_time)}</TableCell>
<TableCell className="text-sm">{formatDt(row.draw_time)}</TableCell> <TableCell className="text-sm">{formatDt(row.draw_time)}</TableCell>
<TableCell> <TableCell>
<DrawStatusBadge status={row.status} /> <DrawStatusBadge
status={row.status}
label={drawAdminStatusSelectLabel(row.status, t)}
/>
</TableCell> </TableCell>
<TableCell className="text-right font-mono text-xs tabular-nums"> <TableCell className="text-right font-mono text-xs tabular-nums">
{row.total_bet_minor ?? "—"} {row.total_bet_minor ?? "—"}

View File

@@ -0,0 +1,43 @@
"use client";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { ConfigDocPage } from "@/modules/config/config-doc-page";
import { ConfigSection } from "@/modules/config/config-section";
import { JackpotPoolsConsole } from "@/modules/jackpot/jackpot-pools-console";
import { JackpotRecordsConsole } from "@/modules/jackpot/jackpot-records-console";
/**
* 奖池:仅保留「侧栏 + 运营配置顶栏」两层导航;池参数与流水在同一页用分区展示。
*/
export function JackpotConfigScreen() {
const { t } = useTranslation("jackpot");
useEffect(() => {
const scrollToRecords = () => {
if (window.location.hash !== "#records") {
return;
}
document.getElementById("jackpot-records")?.scrollIntoView({ behavior: "smooth", block: "start" });
};
scrollToRecords();
window.addEventListener("hashchange", scrollToRecords);
return () => window.removeEventListener("hashchange", scrollToRecords);
}, []);
return (
<ConfigDocPage title={t("configTitle")} description={t("pageDescription")}>
<ConfigSection title={t("poolsSectionTitle")} description={t("poolsSectionDescription")}>
<JackpotPoolsConsole embedded />
</ConfigSection>
<ConfigSection
id="jackpot-records"
title={t("recordsSectionTitle")}
description={t("recordsSectionDescription")}
>
<JackpotRecordsConsole embedded />
</ConfigSection>
</ConfigDocPage>
);
}

View File

@@ -53,7 +53,12 @@ function toDraft(p: AdminJackpotPoolRow): Draft {
}; };
} }
export function JackpotPoolsConsole() { type JackpotPoolsConsoleProps = {
/** 嵌入运营配置单页时去掉外层脚手架与重复标题 */
embedded?: boolean;
};
export function JackpotPoolsConsole({ embedded = false }: JackpotPoolsConsoleProps) {
const { t } = useTranslation(["jackpot", "common"]); const { t } = useTranslation(["jackpot", "common"]);
const [items, setItems] = useState<AdminJackpotPoolRow[]>([]); const [items, setItems] = useState<AdminJackpotPoolRow[]>([]);
const [drafts, setDrafts] = useState<Record<number, Draft>>({}); const [drafts, setDrafts] = useState<Record<number, Draft>>({});
@@ -146,13 +151,14 @@ export function JackpotPoolsConsole() {
} }
}; };
return ( const body = (
<ModuleScaffold> <Card className={embedded ? "border-border/60 shadow-none" : undefined}>
<Card> {!embedded ? (
<CardHeader> <CardHeader>
<CardTitle className="text-base">{t("configTitle")}</CardTitle> <CardTitle className="text-base">{t("configTitle")}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-8"> ) : null}
<CardContent className="space-y-8">
{loading ? <p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p> : null} {loading ? <p className="text-muted-foreground text-sm">{t("states.loading", { ns: "common" })}</p> : null}
{!loading && items.length === 0 ? ( {!loading && items.length === 0 ? (
<p className="text-muted-foreground text-sm">{t("noPoolData")}</p> <p className="text-muted-foreground text-sm">{t("noPoolData")}</p>
@@ -288,8 +294,13 @@ export function JackpotPoolsConsole() {
</div> </div>
); );
})} })}
</CardContent> </CardContent>
</Card> </Card>
</ModuleScaffold>
); );
if (embedded) {
return body;
}
return <ModuleScaffold>{body}</ModuleScaffold>;
} }

View File

@@ -27,7 +27,11 @@ import type {
AdminJackpotPayoutLogsData, AdminJackpotPayoutLogsData,
} from "@/types/api/admin-jackpot"; } from "@/types/api/admin-jackpot";
export function JackpotRecordsConsole() { type JackpotRecordsConsoleProps = {
embedded?: boolean;
};
export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsoleProps) {
const { t } = useTranslation(["jackpot", "common"]); const { t } = useTranslation(["jackpot", "common"]);
const formatDt = useAdminDateTimeFormatter(); const formatDt = useAdminDateTimeFormatter();
const [drawNo, setDrawNo] = useState(""); const [drawNo, setDrawNo] = useState("");
@@ -101,13 +105,8 @@ export function JackpotRecordsConsole() {
return translated === key ? value : translated; return translated === key ? value : translated;
}; };
return ( const content = (
<ModuleScaffold> <>
<div className="mb-6">
<h1 className="text-lg font-semibold tracking-tight">{t("recordsPage.title")}</h1>
<p className="mt-1 text-sm text-muted-foreground">{t("recordsPage.description")}</p>
</div>
<Card className="mb-6"> <Card className="mb-6">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-base">{t("filter")}</CardTitle> <CardTitle className="text-base">{t("filter")}</CardTitle>
@@ -260,6 +259,12 @@ export function JackpotRecordsConsole() {
) : null} ) : null}
</CardContent> </CardContent>
</Card> </Card>
</ModuleScaffold> </>
); );
if (embedded) {
return content;
}
return <ModuleScaffold>{content}</ModuleScaffold>;
} }

View File

@@ -1,39 +0,0 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
const LINKS: { href: string; label: string }[] = [
{ href: "/admin/config/jackpot", label: "subnavPools" },
{ href: "/admin/config/jackpot/records", label: "subnavRecords" },
];
export function JackpotSubNav() {
const { t } = useTranslation("jackpot");
const pathname = usePathname();
return (
<nav className="mb-6 flex flex-wrap gap-2 border-b border-border pb-3" aria-label={t("subnavLabel")}>
{LINKS.map(({ href, label }) => {
const active = pathname === href || pathname.startsWith(`${href}/`);
return (
<Link
key={href}
href={href}
className={cn(
"rounded-md px-3 py-1.5 text-sm transition-colors",
active
? "bg-primary text-primary-foreground"
: "bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground",
)}
>
{t(label)}
</Link>
);
})}
</nav>
);
}

View File

@@ -1,4 +1,4 @@
export const jackpotModuleMeta = { export const jackpotModuleMeta = {
title: "奖池记录", title: "奖池配置",
description: "", description: "",
} as const; } as const;

View File

@@ -12,7 +12,8 @@ import {
} from "@/api/admin-player"; } from "@/api/admin-player";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { Badge } from "@/components/ui/badge"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { resolvePlayerStatusTone } from "@/lib/admin-status-tone";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
@@ -53,15 +54,6 @@ function playerStatusLabelT(status: number, t: (key: string) => string): string
return String(status); return String(status);
} }
function playerStatusVariant(
status: number,
): "default" | "secondary" | "destructive" | "outline" {
if (status === 0) return "secondary";
if (status === 1) return "outline";
if (status === 2) return "destructive";
return "default";
}
const PLAYER_STATUS_OPTIONS = [ const PLAYER_STATUS_OPTIONS = [
{ value: 0, label: "statusNormal" }, { value: 0, label: "statusNormal" },
{ value: 1, label: "statusFrozen" }, { value: 1, label: "statusFrozen" },
@@ -354,9 +346,9 @@ export function PlayersConsole(): React.ReactElement {
: "—"} : "—"}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant={playerStatusVariant(row.status)} className="font-normal"> <AdminStatusBadge status={row.status} tone={resolvePlayerStatusTone(row.status)}>
{playerStatusLabelT(row.status, t)} {playerStatusLabelT(row.status, t)}
</Badge> </AdminStatusBadge>
</TableCell> </TableCell>
<TableCell className="whitespace-nowrap text-xs text-muted-foreground"> <TableCell className="whitespace-nowrap text-xs text-muted-foreground">
{row.last_login_at {row.last_login_at

View File

@@ -12,7 +12,7 @@ import {
import { getAdminPlayers } from "@/api/admin-player"; import { getAdminPlayers } from "@/api/admin-player";
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field"; import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { Badge } from "@/components/ui/badge"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@@ -379,7 +379,9 @@ export function ReconcileConsole(): React.ReactElement {
</TableCell> </TableCell>
<TableCell className="text-sm">{reconcileTypeLabel(row.reconcile_type, t)}</TableCell> <TableCell className="text-sm">{reconcileTypeLabel(row.reconcile_type, t)}</TableCell>
<TableCell> <TableCell>
<Badge variant="secondary">{jobStatusLabel(row.status, t)}</Badge> <AdminStatusBadge status={row.status}>
{jobStatusLabel(row.status, t)}
</AdminStatusBadge>
</TableCell> </TableCell>
<TableCell className="max-w-[16rem] text-xs text-muted-foreground"> <TableCell className="max-w-[16rem] text-xs text-muted-foreground">
<span className="line-clamp-2"> <span className="line-clamp-2">
@@ -459,7 +461,16 @@ export function ReconcileConsole(): React.ReactElement {
<div className="mb-3 flex flex-wrap items-center gap-2 text-sm text-muted-foreground"> <div className="mb-3 flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<span>{t("jobNo")} {items.job_no}</span> <span>{t("jobNo")} {items.job_no}</span>
<span>·</span> <span>·</span>
<span>{t("status")} {selectedJob ? jobStatusLabel(selectedJob.status, t) : "—"}</span> <span className="inline-flex items-center gap-1.5">
{t("status")}
{selectedJob ? (
<AdminStatusBadge status={selectedJob.status}>
{jobStatusLabel(selectedJob.status, t)}
</AdminStatusBadge>
) : (
"—"
)}
</span>
<span>·</span> <span>·</span>
<span>{t("period")} {selectedJob ? `${selectedJob.period_start ? formatTs(selectedJob.period_start) : "—"} ~ ${selectedJob.period_end ? formatTs(selectedJob.period_end) : "—"}` : "—"}</span> <span>{t("period")} {selectedJob ? `${selectedJob.period_start ? formatTs(selectedJob.period_start) : "—"} ~ ${selectedJob.period_end ? formatTs(selectedJob.period_end) : "—"}` : "—"}</span>
</div> </div>
@@ -488,7 +499,11 @@ export function ReconcileConsole(): React.ReactElement {
<TableCell className="font-mono text-xs">{r.side_a_ref ?? "—"}</TableCell> <TableCell className="font-mono text-xs">{r.side_a_ref ?? "—"}</TableCell>
<TableCell className="font-mono text-xs">{r.side_b_ref ?? "—"}</TableCell> <TableCell className="font-mono text-xs">{r.side_b_ref ?? "—"}</TableCell>
<TableCell className="tabular-nums">{r.difference_amount}</TableCell> <TableCell className="tabular-nums">{r.difference_amount}</TableCell>
<TableCell className="text-sm">{itemStatusLabel(r.status, t)}</TableCell> <TableCell>
<AdminStatusBadge status={r.status}>
{itemStatusLabel(r.status, t)}
</AdminStatusBadge>
</TableCell>
</TableRow> </TableRow>
)) ))
)} )}

View File

@@ -14,6 +14,7 @@ import {
postAdminRejectSettlementBatch, postAdminRejectSettlementBatch,
} from "@/api/admin-settlement"; } from "@/api/admin-settlement";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -208,13 +209,21 @@ export function SettlementBatchDetailsConsole({ batchId }: Props) {
</p> </p>
</CardHeader> </CardHeader>
<CardContent className="grid gap-2 text-sm sm:grid-cols-2"> <CardContent className="grid gap-2 text-sm sm:grid-cols-2">
<p> <p className="flex flex-wrap items-center gap-2">
<span className="text-muted-foreground">{t("settlementStatus")}</span>{" "} <span className="text-muted-foreground">{t("settlementStatus")}</span>
<span className="font-mono">{settlementStatusText(summary.status, t)}</span> <AdminStatusBadge status={summary.status}>
{settlementStatusText(summary.status, t)}
</AdminStatusBadge>
</p> </p>
<p> <p className="flex flex-wrap items-center gap-2">
<span className="text-muted-foreground">{t("reviewState")}</span>{" "} <span className="text-muted-foreground">{t("reviewState")}</span>
<span className="font-mono">{settlementReviewStatusText(summary.review_status, t)}</span> {summary.review_status ? (
<AdminStatusBadge status={summary.review_status}>
{settlementReviewStatusText(summary.review_status, t)}
</AdminStatusBadge>
) : (
<span></span>
)}
</p> </p>
<p> <p>
<span className="text-muted-foreground">{t("ticketTotal")}</span>{" "} <span className="text-muted-foreground">{t("ticketTotal")}</span>{" "}

View File

@@ -12,6 +12,7 @@ import {
postAdminRejectSettlementBatch, postAdminRejectSettlementBatch,
} from "@/api/admin-settlement"; } from "@/api/admin-settlement";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { ModuleScaffold } from "@/components/admin/module-scaffold";
import { Button, buttonVariants } from "@/components/ui/button"; import { Button, buttonVariants } from "@/components/ui/button";
@@ -270,23 +271,19 @@ export function SettlementBatchesConsole() {
> >
{formatAdminMinorUnits(row.platform_profit, row.currency_code ?? "NPR")} {formatAdminMinorUnits(row.platform_profit, row.currency_code ?? "NPR")}
</TableCell> </TableCell>
<TableCell className="text-xs text-muted-foreground"> <TableCell>
{settlementReviewStatusText(row.review_status, t)} {row.review_status ? (
<AdminStatusBadge status={row.review_status}>
{settlementReviewStatusText(row.review_status, t)}
</AdminStatusBadge>
) : (
<span className="text-sm text-muted-foreground"></span>
)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<span <AdminStatusBadge status={row.status}>
className={cn(
"rounded px-1.5 py-0.5 text-xs font-medium",
["completed", "paid"].includes(row.status) && "bg-emerald-500/15 text-emerald-800",
row.status === "running" && "bg-amber-500/15 text-amber-900",
row.status === "pending_review" && "bg-blue-500/15 text-blue-800",
row.status === "approved" && "bg-indigo-500/15 text-indigo-800",
row.status === "rejected" && "bg-muted text-muted-foreground",
row.status === "failed" && "bg-destructive/15 text-destructive",
)}
>
{settlementStatusText(row.status, t)} {settlementStatusText(row.status, t)}
</span> </AdminStatusBadge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex flex-wrap justify-end gap-1.5"> <div className="flex flex-wrap justify-end gap-1.5">

View File

@@ -7,7 +7,7 @@ import { getAdminTicketItems } from "@/api/admin-tickets";
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field"; import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { Badge } from "@/components/ui/badge"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
@@ -77,15 +77,6 @@ function ticketStatusSummary(statuses: string[], t: (key: string) => string): st
return t("statusSelectedCount", { count: statuses.length, defaultValue: `已选 ${statuses.length}` }); return t("statusSelectedCount", { count: statuses.length, defaultValue: `已选 ${statuses.length}` });
} }
function ticketStatusVariant(
value: string,
): "default" | "secondary" | "destructive" | "outline" {
if (value === "settled_win") return "secondary";
if (value === "failed") return "destructive";
if (value === "pending_payout") return "default";
return "outline";
}
export function PlayerTicketsConsole(): React.ReactElement { export function PlayerTicketsConsole(): React.ReactElement {
const { t } = useTranslation(["tickets", "common"]); const { t } = useTranslation(["tickets", "common"]);
const formatTs = useAdminDateTimeFormatter(); const formatTs = useAdminDateTimeFormatter();
@@ -344,9 +335,9 @@ export function PlayerTicketsConsole(): React.ReactElement {
{row.actual_deduct_amount_formatted} {row.actual_deduct_amount_formatted}
</TableCell> </TableCell>
<TableCell className="text-xs"> <TableCell className="text-xs">
<Badge variant={ticketStatusVariant(row.status)}> <AdminStatusBadge status={row.status}>
{ticketStatusText(row.status, t)} {ticketStatusText(row.status, t)}
</Badge> </AdminStatusBadge>
</TableCell> </TableCell>
<TableCell className="max-w-[14rem] text-xs text-muted-foreground"> <TableCell className="max-w-[14rem] text-xs text-muted-foreground">
{row.fail_reason_text ?? row.fail_reason_code ?? "—"} {row.fail_reason_text ?? row.fail_reason_code ?? "—"}

View File

@@ -15,7 +15,7 @@ import {
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field"; import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button"; import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { Badge } from "@/components/ui/badge"; import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
@@ -93,16 +93,6 @@ function CellMonoId({
); );
} }
function statusBadgeVariant(
status: string,
): "default" | "secondary" | "destructive" | "outline" {
if (status === "success" || status === "posted") return "secondary";
if (status === "failed") return "destructive";
if (status === "pending_reconcile") return "outline";
if (status === "reversed" || status === "manually_processed") return "outline";
return "default";
}
function statusLabelT(status: string, t: (key: string) => string): string { function statusLabelT(status: string, t: (key: string) => string): string {
switch (status) { switch (status) {
case "processing": case "processing":
@@ -476,7 +466,7 @@ export function TransferOrdersPanel(): React.ReactElement {
{formatAdminMinorUnits(row.amount, row.currency_code)} {formatAdminMinorUnits(row.amount, row.currency_code)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant={statusBadgeVariant(row.status)}>{statusLabelT(row.status, t)}</Badge> <AdminStatusBadge status={row.status}>{statusLabelT(row.status, t)}</AdminStatusBadge>
</TableCell> </TableCell>
<TableCell className="max-w-[14rem] whitespace-normal break-words text-xs text-muted-foreground"> <TableCell className="max-w-[14rem] whitespace-normal break-words text-xs text-muted-foreground">
{row.fail_reason?.trim() ? row.fail_reason : "—"} {row.fail_reason?.trim() ? row.fail_reason : "—"}
@@ -798,7 +788,7 @@ export function WalletTxnsPanel(): React.ReactElement {
{row.amount} ({row.direction === 1 ? t("in") : t("out")}) {row.amount} ({row.direction === 1 ? t("in") : t("out")})
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant={statusBadgeVariant(row.status)}>{statusLabelT(row.status, t)}</Badge> <AdminStatusBadge status={row.status}>{statusLabelT(row.status, t)}</AdminStatusBadge>
</TableCell> </TableCell>
<TableCell className="min-w-0 whitespace-normal font-mono text-[11px] leading-snug text-muted-foreground"> <TableCell className="min-w-0 whitespace-normal font-mono text-[11px] leading-snug text-muted-foreground">
{formatTs(row.created_at)} {formatTs(row.created_at)}