refactor: 重构奖池配置页面,移除冗余组件,优化加载体验与国际化支持
This commit is contained in:
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/components/admin/admin-status-badge.tsx
Normal file
39
src/components/admin/admin-status-badge.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "ट्रिगर",
|
||||||
|
|||||||
@@ -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": "触发",
|
||||||
|
|||||||
133
src/lib/admin-status-tone.ts
Normal file
133
src/lib/admin-status-tone.ts
Normal 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();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ?? "—"}
|
||||||
|
|||||||
43
src/modules/jackpot/jackpot-config-screen.tsx
Normal file
43
src/modules/jackpot/jackpot-config-screen.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export const jackpotModuleMeta = {
|
export const jackpotModuleMeta = {
|
||||||
title: "奖池记录",
|
title: "奖池配置",
|
||||||
description: "",
|
description: "",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>{" "}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 ?? "—"}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
Reference in New Issue
Block a user