refactor: 重构奖池配置页面,移除冗余组件,优化加载体验与国际化支持
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
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",
|
||||
"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",
|
||||
|
||||
@@ -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": "ट्रिगर",
|
||||
|
||||
@@ -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": "触发",
|
||||
|
||||
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,
|
||||
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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 ?? "—"}
|
||||
|
||||
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 [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>;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
title: "奖池记录",
|
||||
title: "奖池配置",
|
||||
description: "",
|
||||
} as const;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -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>{" "}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 ?? "—"}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user