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

View File

@@ -1,17 +1,6 @@
import { JackpotSubNav } from "@/modules/jackpot/jackpot-subnav";
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,
};
import { redirect } from "next/navigation";
/** 旧路径保留跳转,避免书签失效 */
export default function AdminConfigJackpotRecordsPage() {
return (
<div className="w-full max-w-none px-1">
<JackpotSubNav />
<JackpotRecordsConsole />
</div>
);
redirect("/admin/config/jackpot#records");
}

View File

@@ -1,5 +1,5 @@
import { redirect } from "next/navigation";
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",
"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",
"saveSuccess": "Saved",
"saveFailed": "Save failed",
@@ -35,9 +39,7 @@
"title": "Jackpot records",
"description": "Payout records and pool contribution flows"
},
"subnavLabel": "Jackpot sub navigation",
"subnavPools": "Pool configuration",
"subnavRecords": "Records",
"poolsSectionTitle": "Per-currency pool parameters",
"payoutLoadFailed": "Failed to load payout records",
"contributionLoadFailed": "Failed to load contribution records",
"trigger": "Trigger",

View File

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

View File

@@ -1,6 +1,10 @@
{
"title": "奖池",
"configTitle": "奖池配置",
"pageDescription": "维护各币种奖池参数,下方可查询蓄水与派彩流水。",
"poolsSectionDescription": "蓄水比例、爆池阈值、开关与手动爆池。",
"recordsSectionTitle": "蓄水与派彩流水",
"recordsSectionDescription": "按条件筛选派彩记录与蓄水明细,只读查询。",
"loadFailed": "加载失败",
"saveSuccess": "已保存",
"saveFailed": "保存失败",
@@ -35,9 +39,7 @@
"title": "奖池记录",
"description": "派彩记录与奖池蓄水流水"
},
"subnavLabel": "奖池子导航",
"subnavPools": "奖池配置",
"subnavRecords": "记录",
"poolsSectionTitle": "各币种奖池参数",
"payoutLoadFailed": "派彩记录加载失败",
"contributionLoadFailed": "蓄水记录加载失败",
"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,
putAdminRolePermissions,
} from "@/api/admin-users";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { Badge } from "@/components/ui/badge";
import { resolveRoleStatusTone } from "@/lib/admin-status-tone";
import { AdminTableExportButton } from "@/components/admin/admin-table-export-button";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -363,18 +365,9 @@ export function AdminRolesConsole(): React.ReactElement {
)}
</TableCell>
<TableCell>
{role.status === 1 ? (
<Badge variant="secondary" className="font-normal">
{t("status.enabled")}
</Badge>
) : (
<Badge
variant="outline"
className="border-amber-600/50 text-amber-800 dark:text-amber-400"
>
{t("status.disabled")}
</Badge>
)}
<AdminStatusBadge status={role.status} tone={resolveRoleStatusTone(role.status)}>
{role.status === 1 ? t("status.enabled") : t("status.disabled")}
</AdminStatusBadge>
</TableCell>
<TableCell className="tabular-nums">{role.user_count}</TableCell>
<TableCell className="tabular-nums">{role.permission_slugs.length}</TableCell>

View File

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

View File

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

View File

@@ -8,6 +8,8 @@ type ConfigSectionProps = {
actions?: ReactNode;
children: ReactNode;
className?: string;
/** 页内锚点,供旧链接跳转(如 #records */
id?: string;
};
export function ConfigSection({
@@ -16,9 +18,10 @@ export function ConfigSection({
actions,
children,
className,
id,
}: ConfigSectionProps) {
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="min-w-0 space-y-1">
<h3 className="text-base font-semibold text-foreground">{title}</h3>

View File

@@ -1,20 +1,15 @@
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 }) {
const { t } = useTranslation("config");
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 (
<Badge variant="outline" className={cn("font-medium tabular-nums", className)}>
<AdminStatusBadge status={status} tone={resolveAdminStatusTone(status)}>
{label}
</Badge>
</AdminStatusBadge>
);
}

View File

@@ -37,6 +37,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { ConfigReadonlyValue } from "@/modules/config/config-readonly-value";
import { ConfigVersionActions } from "@/modules/config/config-version-actions";
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 })}
/>
) : (
<ConfigReadonlyValue className="justify-center">
{row.is_enabled
? t("play.states.enabled", { ns: "config" })
: t("play.states.disabled", { ns: "config" })}
</ConfigReadonlyValue>
<div className="flex justify-center">
<AdminStatusBadge status={row.is_enabled ? "enabled" : "disabled"}>
{row.is_enabled
? t("play.states.enabled", { ns: "config" })
: t("play.states.disabled", { ns: "config" })}
</AdminStatusBadge>
</div>
)}
</TableCell>
<TableCell>
@@ -595,7 +598,9 @@ export function PlayConfigDocScreen() {
{t("play.actions.ruleText", { ns: "config" })}
</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>
</TableRow>

View File

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

View File

@@ -7,7 +7,9 @@ import { useTranslation } from "react-i18next";
import { getAdminDrawFinanceSummary } from "@/api/admin-draws";
import { postAdminRunDrawSettlement } from "@/api/admin-settlement";
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 { DrawStatusBadge } from "@/modules/draws/draw-status-badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Table,
@@ -104,7 +106,9 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE
</div>
<div>
<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>
<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) => (
<TableRow key={b.id}>
<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">
{b.total_ticket_count}
</TableCell>

View File

@@ -1,14 +1,5 @@
import { Badge } from "@/components/ui/badge";
const emphasis: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
open: "default",
closing: "destructive",
closed: "secondary",
drawing: "secondary",
review: "outline",
cooldown: "secondary",
pending: "outline",
};
import { AdminStatusBadge } from "@/components/admin/admin-status-badge";
import { resolveAdminStatusTone } from "@/lib/admin-status-tone";
export function DrawStatusBadge({
status,
@@ -18,6 +9,9 @@ export function DrawStatusBadge({
/** 可与 DB 不同时展示预览态文案 */
label?: string;
}) {
const v = emphasis[status] ?? "outline";
return <Badge variant={v}>{label ?? status}</Badge>;
return (
<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.draw_time)}</TableCell>
<TableCell>
<DrawStatusBadge status={row.status} />
<DrawStatusBadge
status={row.status}
label={drawAdminStatusSelectLabel(row.status, t)}
/>
</TableCell>
<TableCell className="text-right font-mono text-xs tabular-nums">
{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 [items, setItems] = useState<AdminJackpotPoolRow[]>([]);
const [drafts, setDrafts] = useState<Record<number, Draft>>({});
@@ -146,13 +151,14 @@ export function JackpotPoolsConsole() {
}
};
return (
<ModuleScaffold>
<Card>
const body = (
<Card className={embedded ? "border-border/60 shadow-none" : undefined}>
{!embedded ? (
<CardHeader>
<CardTitle className="text-base">{t("configTitle")}</CardTitle>
</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 && items.length === 0 ? (
<p className="text-muted-foreground text-sm">{t("noPoolData")}</p>
@@ -288,8 +294,13 @@ export function JackpotPoolsConsole() {
</div>
);
})}
</CardContent>
</Card>
</ModuleScaffold>
</CardContent>
</Card>
);
if (embedded) {
return body;
}
return <ModuleScaffold>{body}</ModuleScaffold>;
}

View File

@@ -27,7 +27,11 @@ import type {
AdminJackpotPayoutLogsData,
} from "@/types/api/admin-jackpot";
export function JackpotRecordsConsole() {
type JackpotRecordsConsoleProps = {
embedded?: boolean;
};
export function JackpotRecordsConsole({ embedded = false }: JackpotRecordsConsoleProps) {
const { t } = useTranslation(["jackpot", "common"]);
const formatDt = useAdminDateTimeFormatter();
const [drawNo, setDrawNo] = useState("");
@@ -101,13 +105,8 @@ export function JackpotRecordsConsole() {
return translated === key ? value : translated;
};
return (
<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>
const content = (
<>
<Card className="mb-6">
<CardHeader className="pb-3">
<CardTitle className="text-base">{t("filter")}</CardTitle>
@@ -260,6 +259,12 @@ export function JackpotRecordsConsole() {
) : null}
</CardContent>
</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 = {
title: "奖池记录",
title: "奖池配置",
description: "",
} as const;

View File

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

View File

@@ -12,7 +12,7 @@ import {
import { getAdminPlayers } from "@/api/admin-player";
import { AdminDateRangeField } from "@/components/admin/admin-date-range-field";
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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@@ -379,7 +379,9 @@ export function ReconcileConsole(): React.ReactElement {
</TableCell>
<TableCell className="text-sm">{reconcileTypeLabel(row.reconcile_type, t)}</TableCell>
<TableCell>
<Badge variant="secondary">{jobStatusLabel(row.status, t)}</Badge>
<AdminStatusBadge status={row.status}>
{jobStatusLabel(row.status, t)}
</AdminStatusBadge>
</TableCell>
<TableCell className="max-w-[16rem] text-xs text-muted-foreground">
<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">
<span>{t("jobNo")} {items.job_no}</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>{t("period")} {selectedJob ? `${selectedJob.period_start ? formatTs(selectedJob.period_start) : "—"} ~ ${selectedJob.period_end ? formatTs(selectedJob.period_end) : "—"}` : "—"}</span>
</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_b_ref ?? "—"}</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>
))
)}

View File

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

View File

@@ -12,6 +12,7 @@ import {
postAdminRejectSettlementBatch,
} from "@/api/admin-settlement";
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 { ModuleScaffold } from "@/components/admin/module-scaffold";
import { Button, buttonVariants } from "@/components/ui/button";
@@ -270,23 +271,19 @@ export function SettlementBatchesConsole() {
>
{formatAdminMinorUnits(row.platform_profit, row.currency_code ?? "NPR")}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{settlementReviewStatusText(row.review_status, t)}
<TableCell>
{row.review_status ? (
<AdminStatusBadge status={row.review_status}>
{settlementReviewStatusText(row.review_status, t)}
</AdminStatusBadge>
) : (
<span className="text-sm text-muted-foreground"></span>
)}
</TableCell>
<TableCell>
<span
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",
)}
>
<AdminStatusBadge status={row.status}>
{settlementStatusText(row.status, t)}
</span>
</AdminStatusBadge>
</TableCell>
<TableCell>
<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 { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
@@ -77,15 +77,6 @@ function ticketStatusSummary(statuses: string[], t: (key: string) => string): st
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 {
const { t } = useTranslation(["tickets", "common"]);
const formatTs = useAdminDateTimeFormatter();
@@ -344,9 +335,9 @@ export function PlayerTicketsConsole(): React.ReactElement {
{row.actual_deduct_amount_formatted}
</TableCell>
<TableCell className="text-xs">
<Badge variant={ticketStatusVariant(row.status)}>
<AdminStatusBadge status={row.status}>
{ticketStatusText(row.status, t)}
</Badge>
</AdminStatusBadge>
</TableCell>
<TableCell className="max-w-[14rem] text-xs text-muted-foreground">
{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 { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
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 {
switch (status) {
case "processing":
@@ -476,7 +466,7 @@ export function TransferOrdersPanel(): React.ReactElement {
{formatAdminMinorUnits(row.amount, row.currency_code)}
</TableCell>
<TableCell>
<Badge variant={statusBadgeVariant(row.status)}>{statusLabelT(row.status, t)}</Badge>
<AdminStatusBadge status={row.status}>{statusLabelT(row.status, t)}</AdminStatusBadge>
</TableCell>
<TableCell className="max-w-[14rem] whitespace-normal break-words text-xs text-muted-foreground">
{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")})
</TableCell>
<TableCell>
<Badge variant={statusBadgeVariant(row.status)}>{statusLabelT(row.status, t)}</Badge>
<AdminStatusBadge status={row.status}>{statusLabelT(row.status, t)}</AdminStatusBadge>
</TableCell>
<TableCell className="min-w-0 whitespace-normal font-mono text-[11px] leading-snug text-muted-foreground">
{formatTs(row.created_at)}