diff --git a/src/app/admin/(shell)/admin-roles/page.tsx b/src/app/admin/(shell)/admin-roles/page.tsx index 0fac604..a30a8be 100644 --- a/src/app/admin/(shell)/admin-roles/page.tsx +++ b/src/app/admin/(shell)/admin-roles/page.tsx @@ -1,11 +1,9 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { AdminRolesConsole } from "@/modules/admin-roles/admin-roles-console"; -import { adminRolesModuleMeta } from "@/modules/admin-roles/meta"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: adminRolesModuleMeta.title, -}; +export const metadata: Metadata = buildPageMetadata("adminRoles", "title"); export default function AdminRolesPage() { return ( diff --git a/src/app/admin/(shell)/admin-users/page.tsx b/src/app/admin/(shell)/admin-users/page.tsx index 56f030e..97b6f67 100644 --- a/src/app/admin/(shell)/admin-users/page.tsx +++ b/src/app/admin/(shell)/admin-users/page.tsx @@ -1,11 +1,9 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { AdminUsersConsole } from "@/modules/admin-users/admin-users-console"; -import { adminUsersModuleMeta } from "@/modules/admin-users/meta"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: adminUsersModuleMeta.title, -}; +export const metadata: Metadata = buildPageMetadata("adminUsers", "title"); export default function AdminUsersPage() { return ( diff --git a/src/app/admin/(shell)/audit-logs/page.tsx b/src/app/admin/(shell)/audit-logs/page.tsx index 520eada..6b2923b 100644 --- a/src/app/admin/(shell)/audit-logs/page.tsx +++ b/src/app/admin/(shell)/audit-logs/page.tsx @@ -1,11 +1,9 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { auditLogsModuleMeta } from "@/modules/audit/meta"; import { AuditLogsConsole } from "@/modules/audit/audit-logs-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: auditLogsModuleMeta.title, -}; +export const metadata: Metadata = buildPageMetadata("audit", "title"); export default function AdminAuditLogsPage() { return ( diff --git a/src/app/admin/(shell)/config/jackpot/page.tsx b/src/app/admin/(shell)/config/jackpot/page.tsx index 527d678..6a8ad54 100644 --- a/src/app/admin/(shell)/config/jackpot/page.tsx +++ b/src/app/admin/(shell)/config/jackpot/page.tsx @@ -1,19 +1,5 @@ -import { Suspense } from "react"; +import { redirect } from "next/navigation"; -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, -}; - -export default function AdminConfigJackpotPage() { - return ( - Loading…

} - > - -
- ); +export default function AdminConfigJackpotRedirectPage() { + redirect("/admin/jackpot"); } diff --git a/src/app/admin/(shell)/config/jackpot/records/page.tsx b/src/app/admin/(shell)/config/jackpot/records/page.tsx index 2dcd254..8193246 100644 --- a/src/app/admin/(shell)/config/jackpot/records/page.tsx +++ b/src/app/admin/(shell)/config/jackpot/records/page.tsx @@ -1,6 +1,5 @@ import { redirect } from "next/navigation"; -/** 旧路径保留跳转,避免书签失效 */ -export default function AdminConfigJackpotRecordsPage() { - redirect("/admin/config/jackpot#records"); +export default function AdminConfigJackpotRecordsRedirectPage() { + redirect("/admin/jackpot#records"); } diff --git a/src/app/admin/(shell)/config/layout.tsx b/src/app/admin/(shell)/config/layout.tsx index 9e0762b..c1c439b 100644 --- a/src/app/admin/(shell)/config/layout.tsx +++ b/src/app/admin/(shell)/config/layout.tsx @@ -1,7 +1,6 @@ import type { ReactNode } from "react"; -import { ConfigWorkspaceShell } from "@/modules/config/config-workspace-shell"; - +/** 配置总览仅展示卡片入口,不再挂载横向子导航。 */ export default function AdminConfigLayout({ children }: { children: ReactNode }) { - return {children}; + return children; } diff --git a/src/app/admin/(shell)/config/odds/page.tsx b/src/app/admin/(shell)/config/odds/page.tsx index 830b2d0..eaea2ba 100644 --- a/src/app/admin/(shell)/config/odds/page.tsx +++ b/src/app/admin/(shell)/config/odds/page.tsx @@ -1,11 +1,5 @@ -import { OddsConfigDocScreen } from "@/modules/config/doc/odds-config-doc-screen"; -import { configOddsMeta } from "@/modules/config/meta"; -import type { Metadata } from "next"; +import { redirect } from "next/navigation"; -export const metadata: Metadata = { - title: configOddsMeta.title, -}; - -export default function AdminConfigOddsPage() { - return ; +export default function AdminConfigOddsRedirectPage() { + redirect("/admin/rules/odds"); } diff --git a/src/app/admin/(shell)/config/page.tsx b/src/app/admin/(shell)/config/page.tsx index b778389..5aabd77 100644 --- a/src/app/admin/(shell)/config/page.tsx +++ b/src/app/admin/(shell)/config/page.tsx @@ -1,11 +1,9 @@ -import { configHubMeta } from "@/modules/config/meta"; -import { redirect } from "next/navigation"; +import { ConfigHubScreen } from "@/modules/config/config-hub-screen"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: configHubMeta.title, -}; +export const metadata: Metadata = buildPageMetadata("config", "hub.title"); export default function AdminConfigHubPage() { - redirect("/admin/config/plays"); + return ; } diff --git a/src/app/admin/(shell)/config/plays/page.tsx b/src/app/admin/(shell)/config/plays/page.tsx index e2d9627..5ae5c30 100644 --- a/src/app/admin/(shell)/config/plays/page.tsx +++ b/src/app/admin/(shell)/config/plays/page.tsx @@ -1,11 +1,5 @@ -import { PlayConfigDocScreen } from "@/modules/config/doc/play-config-doc-screen"; -import { configPlayConfigMeta } from "@/modules/config/meta"; -import type { Metadata } from "next"; +import { redirect } from "next/navigation"; -export const metadata: Metadata = { - title: configPlayConfigMeta.title, -}; - -export default function AdminConfigPlaysPage() { - return ; +export default function AdminConfigPlaysRedirectPage() { + redirect("/admin/rules/plays"); } diff --git a/src/app/admin/(shell)/config/rebate/page.tsx b/src/app/admin/(shell)/config/rebate/page.tsx index 7bfe5f3..6df3bca 100644 --- a/src/app/admin/(shell)/config/rebate/page.tsx +++ b/src/app/admin/(shell)/config/rebate/page.tsx @@ -1,11 +1,5 @@ -import { RebateConfigDocScreen } from "@/modules/config/doc/rebate-config-doc-screen"; -import { configRebateMeta } from "@/modules/config/meta"; -import type { Metadata } from "next"; +import { redirect } from "next/navigation"; -export const metadata: Metadata = { - title: configRebateMeta.title, -}; - -export default function AdminConfigRebateDedicatedPage() { - return ; +export default function AdminConfigRebateRedirectPage() { + redirect("/admin/rules/odds#rebate"); } diff --git a/src/app/admin/(shell)/config/risk-cap/page.tsx b/src/app/admin/(shell)/config/risk-cap/page.tsx index d1520f2..4e70a02 100644 --- a/src/app/admin/(shell)/config/risk-cap/page.tsx +++ b/src/app/admin/(shell)/config/risk-cap/page.tsx @@ -1,11 +1,5 @@ -import { configRiskCapMeta } from "@/modules/config/meta"; -import { RiskCapDocScreen } from "@/modules/config/doc/risk-cap-doc-screen"; -import type { Metadata } from "next"; +import { redirect } from "next/navigation"; -export const metadata: Metadata = { - title: configRiskCapMeta.title, -}; - -export default function AdminConfigRiskCapPage() { - return ; +export default function AdminConfigRiskCapRedirectPage() { + redirect("/admin/risk/cap"); } diff --git a/src/app/admin/(shell)/currencies/page.tsx b/src/app/admin/(shell)/currencies/page.tsx index e891fbb..8452f69 100644 --- a/src/app/admin/(shell)/currencies/page.tsx +++ b/src/app/admin/(shell)/currencies/page.tsx @@ -1,10 +1,9 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { CurrencyManagementScreen } from "@/modules/settings/currency-management-screen"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: "币种管理", -}; +export const metadata: Metadata = buildPageMetadata("config", "currencies.title"); export default function AdminCurrenciesPage() { return ( diff --git a/src/app/admin/(shell)/draws/[drawId]/finance/page.tsx b/src/app/admin/(shell)/draws/[drawId]/finance/page.tsx index a5471b8..196f60f 100644 --- a/src/app/admin/(shell)/draws/[drawId]/finance/page.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/finance/page.tsx @@ -1,9 +1,8 @@ import { DrawFinanceConsole } from "@/modules/draws/draw-finance-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: "期号收支", -}; +export const metadata: Metadata = buildPageMetadata("draws", "subnav.finance"); export default async function AdminDrawFinancePage(props: { params: Promise<{ drawId: string }>; diff --git a/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx b/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx index 5a3700e..bdd5cd3 100644 --- a/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx +++ b/src/app/admin/(shell)/draws/[drawId]/publish/[batchId]/page.tsx @@ -1,9 +1,8 @@ import { DrawPublishConsole } from "@/modules/draws/draw-publish-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: "开奖结果发布", -}; +export const metadata: Metadata = buildPageMetadata("draws", "publishTitle"); export default async function AdminDrawPublishBatchPage(props: { params: Promise<{ drawId: string; batchId: string }>; diff --git a/src/app/admin/(shell)/draws/page.tsx b/src/app/admin/(shell)/draws/page.tsx index 89096ef..8061e61 100644 --- a/src/app/admin/(shell)/draws/page.tsx +++ b/src/app/admin/(shell)/draws/page.tsx @@ -1,11 +1,9 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { drawsModuleMeta } from "@/modules/draws/meta"; import { DrawsIndexConsole } from "@/modules/draws/draws-index-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: drawsModuleMeta.title, -}; +export const metadata: Metadata = buildPageMetadata("draws", "statusListTitle"); export default function AdminDrawsPage() { return ( diff --git a/src/app/admin/(shell)/jackpot/page.tsx b/src/app/admin/(shell)/jackpot/page.tsx new file mode 100644 index 0000000..ad8330e --- /dev/null +++ b/src/app/admin/(shell)/jackpot/page.tsx @@ -0,0 +1,18 @@ +import { Suspense } from "react"; + +import { JackpotConfigScreen } from "@/modules/jackpot/jackpot-config-screen"; +import { RulesPageShell } from "@/modules/rules/rules-page-shell"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import type { Metadata } from "next"; + +export const metadata: Metadata = buildPageMetadata("jackpot", "configTitle"); + +export default function AdminJackpotPage() { + return ( + + Loading…

}> + +
+
+ ); +} diff --git a/src/app/admin/(shell)/jackpot/pools/page.tsx b/src/app/admin/(shell)/jackpot/pools/page.tsx index 8b06059..4a8fd5d 100644 --- a/src/app/admin/(shell)/jackpot/pools/page.tsx +++ b/src/app/admin/(shell)/jackpot/pools/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default function AdminJackpotPoolsRedirectPage() { - redirect("/admin/config/jackpot"); + redirect("/admin/jackpot"); } diff --git a/src/app/admin/(shell)/jackpot/records/page.tsx b/src/app/admin/(shell)/jackpot/records/page.tsx index 1b086c5..41158f7 100644 --- a/src/app/admin/(shell)/jackpot/records/page.tsx +++ b/src/app/admin/(shell)/jackpot/records/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default function AdminJackpotRecordsRedirectPage() { - redirect("/admin/config/jackpot#records"); + redirect("/admin/jackpot#records"); } diff --git a/src/app/admin/(shell)/page.tsx b/src/app/admin/(shell)/page.tsx index e3586df..978c2dc 100644 --- a/src/app/admin/(shell)/page.tsx +++ b/src/app/admin/(shell)/page.tsx @@ -1,10 +1,9 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { DashboardConsole } from "@/modules/dashboard/dashboard-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: "仪表盘", -}; +export const metadata: Metadata = buildPageMetadata("dashboard", "title"); export default function AdminDashboardPage() { return ( diff --git a/src/app/admin/(shell)/players/page.tsx b/src/app/admin/(shell)/players/page.tsx index a20a2f7..a905725 100644 --- a/src/app/admin/(shell)/players/page.tsx +++ b/src/app/admin/(shell)/players/page.tsx @@ -1,11 +1,9 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { playersModuleMeta } from "@/modules/players/meta"; import { PlayersConsole } from "@/modules/players/players-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: playersModuleMeta.title, -}; +export const metadata: Metadata = buildPageMetadata("players", "title"); export default function AdminPlayersPage() { return ( diff --git a/src/app/admin/(shell)/reconcile/page.tsx b/src/app/admin/(shell)/reconcile/page.tsx index de64a97..eaedaa3 100644 --- a/src/app/admin/(shell)/reconcile/page.tsx +++ b/src/app/admin/(shell)/reconcile/page.tsx @@ -1,11 +1,9 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { reconcileModuleMeta } from "@/modules/reconcile/meta"; import { ReconcileConsole } from "@/modules/reconcile/reconcile-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: reconcileModuleMeta.title, -}; +export const metadata: Metadata = buildPageMetadata("reconcile", "title"); export default function AdminReconcilePage() { return ( diff --git a/src/app/admin/(shell)/risk/cap/page.tsx b/src/app/admin/(shell)/risk/cap/page.tsx new file mode 100644 index 0000000..31b44dd --- /dev/null +++ b/src/app/admin/(shell)/risk/cap/page.tsx @@ -0,0 +1,14 @@ +import { RiskCapDocScreen } from "@/modules/config/doc/risk-cap-doc-screen"; +import { RulesPageShell } from "@/modules/rules/rules-page-shell"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import type { Metadata } from "next"; + +export const metadata: Metadata = buildPageMetadata("config", "nav.riskCapTitle"); + +export default function AdminRiskCapPage() { + return ( + + + + ); +} diff --git a/src/app/admin/(shell)/rules/odds/page.tsx b/src/app/admin/(shell)/rules/odds/page.tsx new file mode 100644 index 0000000..38280db --- /dev/null +++ b/src/app/admin/(shell)/rules/odds/page.tsx @@ -0,0 +1,9 @@ +import { RulesOddsConfigScreen } from "@/modules/rules/rules-odds-config-screen"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import type { Metadata } from "next"; + +export const metadata: Metadata = buildPageMetadata("config", "nav.rulesOddsTitle"); + +export default function AdminRulesOddsPage() { + return ; +} diff --git a/src/app/admin/(shell)/rules/plays/page.tsx b/src/app/admin/(shell)/rules/plays/page.tsx new file mode 100644 index 0000000..4055146 --- /dev/null +++ b/src/app/admin/(shell)/rules/plays/page.tsx @@ -0,0 +1,14 @@ +import { PlayConfigDocScreen } from "@/modules/config/doc/play-config-doc-screen"; +import { RulesPageShell } from "@/modules/rules/rules-page-shell"; +import { buildPageMetadata } from "@/lib/page-metadata"; +import type { Metadata } from "next"; + +export const metadata: Metadata = buildPageMetadata("config", "nav.rulesPlaysTitle"); + +export default function AdminRulesPlaysPage() { + return ( + + + + ); +} diff --git a/src/app/admin/(shell)/settings/currencies/page.tsx b/src/app/admin/(shell)/settings/currencies/page.tsx index 6c7bca9..3231a74 100644 --- a/src/app/admin/(shell)/settings/currencies/page.tsx +++ b/src/app/admin/(shell)/settings/currencies/page.tsx @@ -1,9 +1,8 @@ import { redirect } from "next/navigation"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: "币种管理", -}; +export const metadata: Metadata = buildPageMetadata("config", "currencies.title"); export default function AdminCurrencySettingsPage() { redirect("/admin/currencies"); diff --git a/src/app/admin/(shell)/settings/page.tsx b/src/app/admin/(shell)/settings/page.tsx index 88eec74..d9a8ce2 100644 --- a/src/app/admin/(shell)/settings/page.tsx +++ b/src/app/admin/(shell)/settings/page.tsx @@ -1,11 +1,9 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { settingsModuleMeta } from "@/modules/settings/meta"; import { SystemSettingsScreen } from "@/modules/settings/system-settings-screen"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: settingsModuleMeta.title, -}; +export const metadata: Metadata = buildPageMetadata("common", "nav.settings"); export default function AdminSettingsPage() { return ( diff --git a/src/app/admin/(shell)/settlement-batches/[batchId]/details/page.tsx b/src/app/admin/(shell)/settlement-batches/[batchId]/details/page.tsx index 8d2b1bc..5f72ed8 100644 --- a/src/app/admin/(shell)/settlement-batches/[batchId]/details/page.tsx +++ b/src/app/admin/(shell)/settlement-batches/[batchId]/details/page.tsx @@ -1,11 +1,9 @@ import { InvalidSettlementBatchId } from "@/modules/settlement/invalid-settlement-batch-id"; import { SettlementBatchDetailsConsole } from "@/modules/settlement/settlement-batch-details-console"; -import { settlementModuleMeta } from "@/modules/settlement/meta"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: `结算明细 · ${settlementModuleMeta.title}`, -}; +export const metadata: Metadata = buildPageMetadata("settlement", "details"); export default async function AdminSettlementBatchDetailsPage(props: { params: Promise<{ batchId: string }>; diff --git a/src/app/admin/(shell)/settlement-batches/page.tsx b/src/app/admin/(shell)/settlement-batches/page.tsx index 21770f3..09155dc 100644 --- a/src/app/admin/(shell)/settlement-batches/page.tsx +++ b/src/app/admin/(shell)/settlement-batches/page.tsx @@ -1,10 +1,8 @@ import { SettlementBatchesConsole } from "@/modules/settlement/settlement-batches-console"; -import { settlementModuleMeta } from "@/modules/settlement/meta"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: settlementModuleMeta.title, -}; +export const metadata: Metadata = buildPageMetadata("settlement", "batchList"); export default function AdminSettlementBatchesPage() { return ; diff --git a/src/app/admin/(shell)/tickets/page.tsx b/src/app/admin/(shell)/tickets/page.tsx index c717c08..3cb1a55 100644 --- a/src/app/admin/(shell)/tickets/page.tsx +++ b/src/app/admin/(shell)/tickets/page.tsx @@ -1,11 +1,9 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; -import { ticketsModuleMeta } from "@/modules/tickets/meta"; import { PlayerTicketsConsole } from "@/modules/tickets/player-tickets-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: ticketsModuleMeta.title, -}; +export const metadata: Metadata = buildPageMetadata("tickets", "title"); export default function AdminTicketsPage() { return ( diff --git a/src/app/admin/(shell)/wallet/player/page.tsx b/src/app/admin/(shell)/wallet/player/page.tsx index 3e5fc9d..2a04d61 100644 --- a/src/app/admin/(shell)/wallet/player/page.tsx +++ b/src/app/admin/(shell)/wallet/player/page.tsx @@ -1,10 +1,8 @@ -import { walletModuleMeta } from "@/modules/wallet/meta"; import { PlayerWalletPanel } from "@/modules/wallet/wallet-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: `${walletModuleMeta.title} · 玩家钱包`, -}; +export const metadata: Metadata = buildPageMetadata("wallet", "playerWalletQuery"); export default function AdminWalletPlayerPage() { return ; diff --git a/src/app/admin/(shell)/wallet/transactions/page.tsx b/src/app/admin/(shell)/wallet/transactions/page.tsx index 569710a..5dcbcd8 100644 --- a/src/app/admin/(shell)/wallet/transactions/page.tsx +++ b/src/app/admin/(shell)/wallet/transactions/page.tsx @@ -1,10 +1,8 @@ -import { walletModuleMeta } from "@/modules/wallet/meta"; import { WalletTxnsPanel } from "@/modules/wallet/wallet-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: `${walletModuleMeta.title} · 钱包流水`, -}; +export const metadata: Metadata = buildPageMetadata("wallet", "walletTransactions"); export default function AdminWalletTransactionsPage() { return ; diff --git a/src/app/admin/(shell)/wallet/transfer-orders/page.tsx b/src/app/admin/(shell)/wallet/transfer-orders/page.tsx index 18c0325..5565467 100644 --- a/src/app/admin/(shell)/wallet/transfer-orders/page.tsx +++ b/src/app/admin/(shell)/wallet/transfer-orders/page.tsx @@ -1,10 +1,8 @@ -import { walletModuleMeta } from "@/modules/wallet/meta"; import { TransferOrdersPanel } from "@/modules/wallet/wallet-console"; +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; -export const metadata: Metadata = { - title: `${walletModuleMeta.title} · 转账单`, -}; +export const metadata: Metadata = buildPageMetadata("wallet", "transferOrders"); export default function AdminWalletTransferOrdersPage() { return ; diff --git a/src/app/admin/login/page.tsx b/src/app/admin/login/page.tsx index d6dccc5..d2b197b 100644 --- a/src/app/admin/login/page.tsx +++ b/src/app/admin/login/page.tsx @@ -1,11 +1,9 @@ +import { buildPageMetadata } from "@/lib/page-metadata"; import type { Metadata } from "next"; import { LoginForm } from "@/components/admin/login-form"; -import { authModuleMeta } from "@/modules/auth/meta"; -export const metadata: Metadata = { - title: authModuleMeta.title, -}; +export const metadata: Metadata = buildPageMetadata("auth", "title"); export default function AdminLoginPage() { return ; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1e99a8c..5fb6af2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -16,10 +16,10 @@ const geistMono = Geist_Mono({ export const metadata: Metadata = { title: { - template: "%s · 彩票后台", - default: "彩票后台", + template: "%s · Lottery Admin", + default: "Lottery Admin", }, - description: "彩票后台管理端", + description: "Lottery administration console", }; export default function RootLayout({ diff --git a/src/components/admin/admin-breadcrumb.tsx b/src/components/admin/admin-breadcrumb.tsx index c8e97b9..c57e8cb 100644 --- a/src/components/admin/admin-breadcrumb.tsx +++ b/src/components/admin/admin-breadcrumb.tsx @@ -29,7 +29,10 @@ const NAV_TRANSLATION_KEYS: Record = { currencies: "currencies", wallet: "wallet", draws: "draws", - config: "config", + rules_plays: "rules_plays", + rules_odds: "rules_odds", + jackpot: "jackpot", + risk_cap: "risk_cap", risk: "risk", settlement: "settlement", reconcile: "reconcile", @@ -38,6 +41,15 @@ const NAV_TRANSLATION_KEYS: Record = { settings: "settings", }; +const RULES_ROUTE_LABELS: Record = { + plays: "nav.items.plays", + odds: "nav.rulesOddsTitle", +}; + +const RISK_ROUTE_LABELS: Record = { + cap: "nav.riskCapTitle", +}; + const SETTINGS_ROUTE_LABELS: Record = { currencies: "currencies.title", }; @@ -76,9 +88,16 @@ export function AdminBreadcrumb() { if (pathname !== ADMIN_BASE) { const businessSegment = segments[1]; if (businessSegment) { - const navItem = navItems.find((item) => { - return item.segment === businessSegment || item.href.includes(businessSegment); - }); + const navItem = navItems + .filter( + (item) => + pathname === item.href || + pathname.startsWith(`${item.href}/`) || + (item.activeMatchPrefix != null && + (pathname === item.activeMatchPrefix || + pathname.startsWith(`${item.activeMatchPrefix}/`))), + ) + .sort((a, b) => b.href.length - a.href.length)[0]; if (navItem && navItem.href !== ADMIN_BASE) { const translatedNavLabel = @@ -104,11 +123,23 @@ export function AdminBreadcrumb() { }); } - if (segments.length > 2) { + const navCoversPath = + navItem != null && + (pathname === navItem.href || pathname.startsWith(`${navItem.href}/`)); + + if (segments.length > 2 && !navCoversPath) { const subSegment = segments[2]; let subLabel = ""; - if (businessSegment === "config" && subSegment) { - subLabel = t(`nav.items.${subSegment}`, { ns: "config", defaultValue: titleCase(subSegment) }); + if (businessSegment === "rules" && subSegment) { + const key = RULES_ROUTE_LABELS[subSegment]; + subLabel = key + ? t(key, { ns: "config", defaultValue: titleCase(subSegment) }) + : titleCase(subSegment); + } else if (businessSegment === "risk" && subSegment) { + const key = RISK_ROUTE_LABELS[subSegment]; + subLabel = key + ? t(key, { ns: "config", defaultValue: titleCase(subSegment) }) + : titleCase(subSegment); } else if (businessSegment === "settings" && subSegment) { subLabel = t(SETTINGS_ROUTE_LABELS[subSegment] ?? `settings.${subSegment}`, { ns: "config", diff --git a/src/components/admin/admin-document-title.tsx b/src/components/admin/admin-document-title.tsx new file mode 100644 index 0000000..2c1d6f3 --- /dev/null +++ b/src/components/admin/admin-document-title.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname } from "next/navigation"; +import { useTranslation } from "react-i18next"; + +import { resolveAdminPageTitle } from "@/lib/admin-page-title"; + +/** 随界面语言更新 `document.title`(弥补 Next 静态 metadata 无法按用户语言切换)。 */ +export function AdminDocumentTitle() { + const pathname = usePathname(); + const { t, i18n } = useTranslation(); + + useEffect(() => { + const spec = resolveAdminPageTitle(pathname); + const appTitle = t("app.title", { ns: "common" }); + + if (!spec) { + document.title = appTitle; + return; + } + + const pageTitle = t(spec.key, { ns: spec.ns, ...(spec.params ?? {}) }); + document.title = `${pageTitle} · ${appTitle}`; + }, [pathname, i18n.language, t]); + + return null; +} diff --git a/src/components/admin/admin-shell.tsx b/src/components/admin/admin-shell.tsx index 2a65f14..095d0f8 100644 --- a/src/components/admin/admin-shell.tsx +++ b/src/components/admin/admin-shell.tsx @@ -5,6 +5,7 @@ import type { ReactNode } from "react"; import { AdminAppSidebar } from "@/components/admin/admin-sidebar"; import { ShellToolbar } from "@/components/admin/toolbar"; import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb"; +import { AdminDocumentTitle } from "@/components/admin/admin-document-title"; import { SidebarInset, SidebarProvider, @@ -15,6 +16,7 @@ import { Separator } from "@/components/ui/separator"; export function AdminShell({ children }: { children: ReactNode }) { return ( +
diff --git a/src/components/admin/admin-table-export-button.tsx b/src/components/admin/admin-table-export-button.tsx index 6ed02a3..27d383e 100644 --- a/src/components/admin/admin-table-export-button.tsx +++ b/src/components/admin/admin-table-export-button.tsx @@ -72,7 +72,7 @@ export function AdminTableExportButton({ const onExport = () => { const table = document.getElementById(tableId); if (!(table instanceof HTMLTableElement)) { - toast.error(t("errors.loadFailed", { defaultValue: "导出失败" })); + toast.error(t("toast.exportFailed")); return; } @@ -84,16 +84,16 @@ export function AdminTableExportButton({ }); const safeName = filename.endsWith(".xlsx") ? filename : `${filename}.xlsx`; XLSX.writeFile(workbook, safeName); - toast.success(t("actions.done", { defaultValue: "已导出" })); + toast.success(t("toast.exportDone")); } catch { - toast.error(t("errors.loadFailed", { defaultValue: "导出失败" })); + toast.error(t("toast.exportFailed")); } }; return ( ); } diff --git a/src/hooks/use-export-labels.ts b/src/hooks/use-export-labels.ts new file mode 100644 index 0000000..9fd28ce --- /dev/null +++ b/src/hooks/use-export-labels.ts @@ -0,0 +1,37 @@ +"use client"; + +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; + +/** 与 `common.export.*` 键一致 */ +export type ExportLabelKey = + | "drawsList" + | "drawFinance" + | "adminRoles" + | "adminUsers" + | "players" + | "walletTransferOrders" + | "walletTransactions" + | "playerWallets" + | "tickets" + | "settlementBatches" + | "jackpotPayouts" + | "jackpotContributions" + | "riskLockLogs" + | "riskPools" + | "riskIndex" + | "riskPoolDetail" + | "auditLogs" + | "currencies"; + +export function useExportLabels(key: ExportLabelKey, params?: Record) { + const { t } = useTranslation("common"); + + return useMemo( + () => ({ + filename: t(`export.${key}.filename`, params), + sheetName: t(`export.${key}.sheetName`, params), + }), + [key, params, t], + ); +} diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 7afec99..0621573 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -14,6 +14,7 @@ "download": "Download", "search": "Search", "apply": "Apply", + "reset": "Reset", "loading": "Loading...", "submitting": "Submitting...", "logout": "Log out", @@ -24,7 +25,36 @@ "create": "Create", "createTask": "Create task", "clear": "Clear", - "done": "Done" + "done": "Done", + "exportExcel": "Export Excel" + }, + "aria": { + "expand": "Expand", + "collapse": "Collapse" + }, + "export": { + "drawsList": { "filename": "draws-list", "sheetName": "Draws" }, + "drawFinance": { "filename": "draw-finance-{{drawNo}}", "sheetName": "Draw finance" }, + "adminRoles": { "filename": "admin-roles", "sheetName": "Roles" }, + "adminUsers": { "filename": "admin-users", "sheetName": "Admin users" }, + "players": { "filename": "players", "sheetName": "Players" }, + "walletTransferOrders": { "filename": "transfer-orders", "sheetName": "Transfer orders" }, + "walletTransactions": { "filename": "wallet-transactions", "sheetName": "Transactions" }, + "playerWallets": { "filename": "player-wallets", "sheetName": "Player wallets" }, + "tickets": { "filename": "tickets", "sheetName": "Tickets" }, + "settlementBatches": { "filename": "settlement-batches", "sheetName": "Settlement" }, + "jackpotPayouts": { "filename": "jackpot-payouts", "sheetName": "Jackpot payouts" }, + "jackpotContributions": { "filename": "jackpot-contributions", "sheetName": "Contributions" }, + "riskLockLogs": { "filename": "risk-lock-logs", "sheetName": "Risk lock logs" }, + "riskPools": { "filename": "risk-pools", "sheetName": "Risk pools" }, + "riskIndex": { "filename": "risk-draws", "sheetName": "Risk center" }, + "riskPoolDetail": { "filename": "risk-pool-{{number}}", "sheetName": "Risk pool detail" }, + "auditLogs": { "filename": "audit-logs", "sheetName": "Audit logs" }, + "currencies": { "filename": "currencies", "sheetName": "Currencies" } + }, + "toast": { + "exportDone": "Exported", + "exportFailed": "Export failed" }, "date": { "placeholder": "Select date", @@ -65,8 +95,10 @@ "currencies": "Currencies", "wallet": "Wallet", "draws": "Draws", - "config": "Configuration", - "risk": "Risk", + "rules_plays": "Play rules", + "rules_odds": "Odds & rebate", + "risk_cap": "Risk cap rules", + "risk": "Risk center", "settlement": "Settlement", "jackpot": "Jackpot", "reconcile": "Reconcile", diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json index eb378d5..ed44be1 100644 --- a/src/i18n/locales/en/config.json +++ b/src/i18n/locales/en/config.json @@ -13,7 +13,23 @@ "rebate": "Commission / rebate", "jackpot": "Jackpot pool", "risk-cap": "Payout caps" - } + }, + "rulesPlaysTitle": "Play rules", + "rulesOddsTitle": "Odds & rebate", + "rulesOddsDescription": "Odds matrix and rebate rates on one page, sharing the same odds version line.", + "riskCapTitle": "Risk cap rules" + }, + "hub": { + "title": "Operations configuration", + "description": "Jump to play rules, odds & rebate, jackpot, and risk cap by domain. The sidebar provides direct links; this page is an overview.", + "playsTitle": "Play rules", + "playsDesc": "Play switches, limits, and rule copy", + "oddsTitle": "Odds & rebate", + "oddsDesc": "Odds matrix and rebate rates in one version stream", + "jackpotTitle": "Jackpot", + "jackpotDesc": "Pool parameters and ledger records", + "riskCapTitle": "Risk cap rules", + "riskCapDesc": "Per-number payout caps and occupancy" }, "versionStatus": { "active": "Active", @@ -198,6 +214,7 @@ } }, "odds": { + "sectionHint": "Pick a version to edit prize-tier odds; publishing applies to new tickets immediately.", "tabs": { "all": "All" }, @@ -235,6 +252,8 @@ } }, "rebate": { + "sectionHint": "Rebate rates are stored in the odds version; select or create an odds draft in the section above first.", + "embeddedVersionHint": "Rebate shares the odds version line—switch versions in the Odds section above.", "sheetDescription": "Rebate is stored in the odds draft version and shares the same version set as odds.", "publishLabel": "Publish", "publishSuccess": "Published odds version with rebate", diff --git a/src/i18n/locales/en/dashboard.json b/src/i18n/locales/en/dashboard.json index 3ced7a3..8302b94 100644 --- a/src/i18n/locales/en/dashboard.json +++ b/src/i18n/locales/en/dashboard.json @@ -3,11 +3,26 @@ "refresh": "Refresh", "notice": "Notice", "todayBetTotal": "Current draw total bet", - "currentDrawFinanceSummary": "Finance summary for the current hall draw", + "drawNoHint": "Draw {{drawNo}}", + "orderAndTicket": "{{orders}} orders · {{tickets}} items", + "marginRate": "Gross margin ~{{rate}}%", + "financeStructure": "Bet fund structure", + "payoutComposition": "Payout composition", + "winPayout": "Win payout", + "jackpotPayout": "Jackpot payout", + "houseGross": "House gross", + "payoutRateOfBet": "Payout / bet {{rate}}%", + "noFinanceActivity": "No bets this draw", + "noPayoutYet": "No payout this draw", + "resultBatches": "Result batch progress", + "batchPending": "Pending review", + "batchPublished": "Published", + "batchTotal": "Total batches", + "settlementOverview": "Settlement batches", + "noSettlementBatches": "No settlement batches", + "quickLinksTitle": "Quick links", "currentPayout": "Current payout", - "payoutSummary": "Winning payout + Jackpot", "currentProfit": "Current platform profit", - "profitFormula": "Bet - payout (approx.)", "currentDraw": "Current draw", "drawSequence": "Round {{sequence}}", "drawDetails": "Draw details", diff --git a/src/i18n/locales/ne/common.json b/src/i18n/locales/ne/common.json index a550d93..1ed9c9f 100644 --- a/src/i18n/locales/ne/common.json +++ b/src/i18n/locales/ne/common.json @@ -14,6 +14,7 @@ "download": "डाउनलोड", "search": "खोज", "apply": "लागू गर्नुहोस्", + "reset": "रिसेट", "loading": "लोड हुँदैछ...", "submitting": "पेश हुँदैछ...", "logout": "लगआउट", @@ -24,7 +25,36 @@ "create": "सिर्जना गर्नुहोस्", "createTask": "टास्क सिर्जना गर्नुहोस्", "clear": "खाली गर्नुहोस्", - "done": "समाप्त" + "done": "समाप्त", + "exportExcel": "Excel निर्यात" + }, + "aria": { + "expand": "खोल्नुहोस्", + "collapse": "बन्द गर्नुहोस्" + }, + "export": { + "drawsList": { "filename": "draw-suchi", "sheetName": "Draw" }, + "drawFinance": { "filename": "draw-arth-{{drawNo}}", "sheetName": "Draw finance" }, + "adminRoles": { "filename": "bhumika-suchi", "sheetName": "Bhumika" }, + "adminUsers": { "filename": "admin-prayogkarta", "sheetName": "Admin users" }, + "players": { "filename": "khiladi-suchi", "sheetName": "Khiladi" }, + "walletTransferOrders": { "filename": "transfer-ades", "sheetName": "Transfer" }, + "walletTransactions": { "filename": "wallet-len-den", "sheetName": "Len-den" }, + "playerWallets": { "filename": "khiladi-wallet", "sheetName": "Wallet" }, + "tickets": { "filename": "ticket-suchi", "sheetName": "Ticket" }, + "settlementBatches": { "filename": "settlement-batch", "sheetName": "Settlement" }, + "jackpotPayouts": { "filename": "jackpot-bhugtan", "sheetName": "Bhugtan" }, + "jackpotContributions": { "filename": "jackpot-yogdan", "sheetName": "Yogdan" }, + "riskLockLogs": { "filename": "jokhim-lock", "sheetName": "Lock log" }, + "riskPools": { "filename": "jokhim-pool", "sheetName": "Risk pool" }, + "riskIndex": { "filename": "jokhim-draw", "sheetName": "Jokhim" }, + "riskPoolDetail": { "filename": "pool-{{number}}", "sheetName": "Pool detail" }, + "auditLogs": { "filename": "audit-log", "sheetName": "Audit" }, + "currencies": { "filename": "mudra", "sheetName": "Mudra" } + }, + "toast": { + "exportDone": "निर्यात भयो", + "exportFailed": "निर्यात असफल" }, "date": { "placeholder": "मिति छान्नुहोस्", @@ -65,8 +95,10 @@ "currencies": "मुद्रा व्यवस्थापन", "wallet": "वालेट", "draws": "ड्रअहरू", - "config": "कन्फिगरेसन", - "risk": "जोखिम", + "rules_plays": "खेल नियम", + "rules_odds": "बाधा र रिबेट", + "risk_cap": "जोखिम क्याप संस्करण", + "risk": "जोखिम केन्द्र", "settlement": "सेटलमेन्ट", "jackpot": "Jackpot", "reconcile": "मिलान", diff --git a/src/i18n/locales/ne/config.json b/src/i18n/locales/ne/config.json index 4002bee..b5401da 100644 --- a/src/i18n/locales/ne/config.json +++ b/src/i18n/locales/ne/config.json @@ -13,7 +13,23 @@ "rebate": "कमिसन / रिबेट", "jackpot": "Jackpot पूल", "risk-cap": "पेमेन्ट क्याप" - } + }, + "rulesPlaysTitle": "खेल नियम", + "rulesOddsTitle": "बाधा र रिबेट", + "rulesOddsDescription": "बाधा म्याट्रिक्स र रिबेट दर एउटै पृष्ठमा, एउटै बाधा संस्करण लाइनमा।", + "riskCapTitle": "जोखिम क्याप संस्करण" + }, + "hub": { + "title": "सञ्चालन कन्फिगरेसन अवलोकन", + "description": "खेल नियम, बाधा/रिबेट, Jackpot र क्याप क्षेत्रअनुसार खोल्नुहोस्। साइडबारबाट सिधा जान सकिन्छ; यो पृष्ठ सारांश हो।", + "playsTitle": "खेल नियम", + "playsDesc": "खेल स्विच, सीमा र नियम पाठ", + "oddsTitle": "बाधा र रिबेट", + "oddsDesc": "बाधा म्याट्रिक्स र रिबेट, एउटै संस्करण", + "jackpotTitle": "Jackpot", + "jackpotDesc": "पूल प्यारामिटर र लेजर", + "riskCapTitle": "जोखिम क्याप", + "riskCapDesc": "नम्बर क्याप र ओगट उपस्थिति" }, "versionStatus": { "active": "सक्रिय", @@ -198,6 +214,7 @@ } }, "odds": { + "sectionHint": "संस्करण छानेर पुरस्कार-स्तर बाधा सम्पादन गर्नुहोस्; प्रकाशनपछि नयाँ टिकटमा लागू हुन्छ।", "tabs": { "all": "सबै" }, @@ -235,6 +252,8 @@ } }, "rebate": { + "sectionHint": "रिबेट दर अड्स संस्करणमा लेखिन्छ; पहिले माथिको «बाधा» खण्डमा ड्राफ्ट छान्नुहोस्।", + "embeddedVersionHint": "रिबेट माथिको बाधा संस्करण लाइन साझा गर्छ—संस्करण त्यहीँबाट बदल्नुहोस्।", "sheetDescription": "रिबेट अड्स ड्राफ्ट संस्करणमा राखिन्छ र अड्ससँग एउटै संस्करण सेट साझा गर्छ।", "publishLabel": "प्रकाशन", "publishSuccess": "रिबेटसहितको अड्स संस्करण प्रकाशित भयो", diff --git a/src/i18n/locales/ne/dashboard.json b/src/i18n/locales/ne/dashboard.json index ff90def..921ebd3 100644 --- a/src/i18n/locales/ne/dashboard.json +++ b/src/i18n/locales/ne/dashboard.json @@ -3,11 +3,26 @@ "refresh": "रिफ्रेस", "notice": "सूचना", "todayBetTotal": "हालको ड्रअ कुल बेट", - "currentDrawFinanceSummary": "हालको हल ड्रअको वित्तीय सारांश", + "drawNoHint": "ड्रअ {{drawNo}}", + "orderAndTicket": "{{orders}} अर्डर · {{tickets}} वस्तु", + "marginRate": "सकल मार्जिन ~{{rate}}%", + "financeStructure": "बेट कोष संरचना", + "payoutComposition": "भुक्तानी संरचना", + "winPayout": "जित भुक्तानी", + "jackpotPayout": "Jackpot भुक्तानी", + "houseGross": "प्लेटफर्म बाँकी", + "payoutRateOfBet": "भुक्तानी/बेट {{rate}}%", + "noFinanceActivity": "यस ड्रअमा बेट छैन", + "noPayoutYet": "यस ड्रअमा भुक्तानी छैन", + "resultBatches": "परिणाम ब्याच प्रगति", + "batchPending": "समीक्षा बाँकी", + "batchPublished": "प्रकाशित", + "batchTotal": "कुल ब्याच", + "settlementOverview": "सेटलमेन्ट ब्याच", + "noSettlementBatches": "सेटलमेन्ट ब्याच छैन", + "quickLinksTitle": "छिटो लिङ्क", "currentPayout": "हालको भुक्तानी", - "payoutSummary": "जितेको भुक्तानी + Jackpot", "currentProfit": "हालको प्लेटफर्म नाफा", - "profitFormula": "बेट - भुक्तानी (अनुमानित)", "currentDraw": "हालको ड्रअ", "drawSequence": "राउन्ड {{sequence}}", "drawDetails": "ड्रअ विवरण", diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index 0e1881f..b3ba913 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -14,6 +14,7 @@ "download": "下载", "search": "搜索", "apply": "应用", + "reset": "重置", "loading": "加载中...", "submitting": "提交中...", "logout": "退出登录", @@ -24,7 +25,36 @@ "create": "创建", "createTask": "创建任务", "clear": "清除", - "done": "完成" + "done": "完成", + "exportExcel": "导出 Excel" + }, + "aria": { + "expand": "展开", + "collapse": "收起" + }, + "export": { + "drawsList": { "filename": "期号列表", "sheetName": "期号列表" }, + "drawFinance": { "filename": "期号收支-{{drawNo}}", "sheetName": "期号收支" }, + "adminRoles": { "filename": "角色列表", "sheetName": "角色列表" }, + "adminUsers": { "filename": "后台用户列表", "sheetName": "后台用户" }, + "players": { "filename": "玩家列表", "sheetName": "玩家列表" }, + "walletTransferOrders": { "filename": "钱包转账订单", "sheetName": "转账订单" }, + "walletTransactions": { "filename": "钱包流水", "sheetName": "钱包流水" }, + "playerWallets": { "filename": "玩家钱包", "sheetName": "玩家钱包" }, + "tickets": { "filename": "注单列表", "sheetName": "注单列表" }, + "settlementBatches": { "filename": "结算批次", "sheetName": "结算批次" }, + "jackpotPayouts": { "filename": "奖池派彩记录", "sheetName": "奖池派彩" }, + "jackpotContributions": { "filename": "奖池注入记录", "sheetName": "奖池注入" }, + "riskLockLogs": { "filename": "风险占用流水", "sheetName": "风险占用流水" }, + "riskPools": { "filename": "风险池", "sheetName": "风险池" }, + "riskIndex": { "filename": "风控中心期号列表", "sheetName": "风控中心" }, + "riskPoolDetail": { "filename": "风险池详情-{{number}}", "sheetName": "风险池详情" }, + "auditLogs": { "filename": "审计日志", "sheetName": "审计日志" }, + "currencies": { "filename": "币种管理", "sheetName": "币种管理" } + }, + "toast": { + "exportDone": "已导出", + "exportFailed": "导出失败" }, "date": { "placeholder": "选择日期", @@ -65,8 +95,10 @@ "currencies": "币种管理", "wallet": "钱包流水", "draws": "期号列表", - "config": "运营配置", - "risk": "风控", + "rules_plays": "投注规则", + "rules_odds": "赔率与回水", + "risk_cap": "限额版本", + "risk": "风控中心", "settlement": "结算", "jackpot": "奖池", "reconcile": "对账", diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json index 4050df1..0773347 100644 --- a/src/i18n/locales/zh/config.json +++ b/src/i18n/locales/zh/config.json @@ -13,7 +13,23 @@ "rebate": "佣金 / 回水", "jackpot": "奖池配置", "risk-cap": "赔付封顶" - } + }, + "rulesPlaysTitle": "投注规则", + "rulesOddsTitle": "赔率与回水", + "rulesOddsDescription": "赔率矩阵与回水比例在同一页维护,共用赔率版本线。", + "riskCapTitle": "限额版本" + }, + "hub": { + "title": "运营配置总览", + "description": "按业务域进入玩法、赔率回水、奖池与限额配置。侧栏已提供直达入口,本页为汇总导航。", + "playsTitle": "投注规则", + "playsDesc": "玩法开关、限额与规则说明", + "oddsTitle": "赔率与回水", + "oddsDesc": "赔率矩阵与回水比例,版本一体发布", + "jackpotTitle": "奖池", + "jackpotDesc": "奖池参数与进账流水", + "riskCapTitle": "限额版本", + "riskCapDesc": "号码赔付封顶与占用视图" }, "versionStatus": { "active": "生效中", @@ -198,6 +214,7 @@ } }, "odds": { + "sectionHint": "选择版本后可编辑各奖级赔率;发布后立即作用于新注单。", "tabs": { "all": "全部" }, @@ -235,6 +252,8 @@ } }, "rebate": { + "sectionHint": "回水比例写入赔率版本;请先在上方选择或创建赔率草稿。", + "embeddedVersionHint": "回水与上方赔率共用版本线,请在「赔率」区块切换版本。", "sheetDescription": "回水配置存放在赔率草稿版本中,与赔率共用同一套版本记录。", "publishLabel": "发布", "publishSuccess": "已发布带回水的赔率版本", diff --git a/src/i18n/locales/zh/dashboard.json b/src/i18n/locales/zh/dashboard.json index 0e45b4b..8dca547 100644 --- a/src/i18n/locales/zh/dashboard.json +++ b/src/i18n/locales/zh/dashboard.json @@ -3,11 +3,26 @@ "refresh": "刷新", "notice": "提示", "todayBetTotal": "当期投注总额", - "currentDrawFinanceSummary": "当前大厅期财务汇总", + "drawNoHint": "期号 {{drawNo}}", + "orderAndTicket": "{{orders}} 单 · {{tickets}} 笔", + "marginRate": "毛利率约 {{rate}}%", + "financeStructure": "投注资金结构", + "payoutComposition": "派彩构成", + "winPayout": "中奖派彩", + "jackpotPayout": "奖池派彩", + "houseGross": "平台结余", + "payoutRateOfBet": "派彩占投注 {{rate}}%", + "noFinanceActivity": "本期暂无投注", + "noPayoutYet": "本期暂无派彩", + "resultBatches": "开奖批次进度", + "batchPending": "待审核", + "batchPublished": "已发布", + "batchTotal": "批次合计", + "settlementOverview": "结算批次分布", + "noSettlementBatches": "暂无结算批次", + "quickLinksTitle": "快捷入口", "currentPayout": "当期派彩", - "payoutSummary": "中奖派彩 + 奖池", "currentProfit": "当期平台盈亏", - "profitFormula": "投注 − 派彩(近似)", "currentDraw": "当前期号", "drawSequence": "第 {{sequence}} 期", "drawDetails": "期号详情", diff --git a/src/lib/admin-page-title.ts b/src/lib/admin-page-title.ts new file mode 100644 index 0000000..5de113d --- /dev/null +++ b/src/lib/admin-page-title.ts @@ -0,0 +1,105 @@ +export type PageTitleSpec = { + ns: string; + key: string; + params?: Record; +}; + +const EXACT_ROUTES: Record = { + "/admin": { ns: "dashboard", key: "title" }, + "/admin/players": { ns: "players", key: "title" }, + "/admin/draws": { ns: "draws", key: "statusListTitle" }, + "/admin/tickets": { ns: "tickets", key: "title" }, + "/admin/settlement-batches": { ns: "settlement", key: "batchList" }, + "/admin/reconcile": { ns: "reconcile", key: "title" }, + "/admin/audit-logs": { ns: "audit", key: "title" }, + "/admin/admin-users": { ns: "adminUsers", key: "title" }, + "/admin/admin-roles": { ns: "adminRoles", key: "title" }, + "/admin/wallet": { ns: "wallet", key: "title" }, + "/admin/wallet/transactions": { ns: "wallet", key: "walletTransactions" }, + "/admin/wallet/transfer-orders": { ns: "wallet", key: "transferOrders" }, + "/admin/wallet/player": { ns: "wallet", key: "playerWalletQuery" }, + "/admin/risk": { ns: "risk", key: "center" }, + "/admin/settings": { ns: "common", key: "nav.settings" }, + "/admin/settings/currencies": { ns: "config", key: "currencies.title" }, + "/admin/currencies": { ns: "config", key: "currencies.title" }, + "/admin/config": { ns: "config", key: "hub.title" }, + "/admin/rules/plays": { ns: "config", key: "nav.rulesPlaysTitle" }, + "/admin/rules/odds": { ns: "config", key: "nav.rulesOddsTitle" }, + "/admin/jackpot": { ns: "jackpot", key: "configTitle" }, + "/admin/risk/cap": { ns: "config", key: "nav.riskCapTitle" }, + "/admin/login": { ns: "auth", key: "title" }, +}; + +type RoutePattern = { + test: (pathname: string) => boolean; + resolve: (pathname: string) => PageTitleSpec; +}; + +const ROUTE_PATTERNS: RoutePattern[] = [ + { + test: (p) => /^\/admin\/draws\/\d+\/finance$/.test(p), + resolve: () => ({ ns: "draws", key: "subnav.finance" }), + }, + { + test: (p) => /^\/admin\/draws\/\d+\/results$/.test(p), + resolve: () => ({ ns: "draws", key: "subnav.results" }), + }, + { + test: (p) => /^\/admin\/draws\/\d+\/review$/.test(p), + resolve: () => ({ ns: "draws", key: "subnav.review" }), + }, + { + test: (p) => /^\/admin\/draws\/\d+\/publish\/\d+$/.test(p), + resolve: () => ({ ns: "draws", key: "publishTitle" }), + }, + { + test: (p) => /^\/admin\/draws\/\d+\/risk\/occupancy$/.test(p) || /^\/admin\/risk\/draws\/\d+\/occupancy$/.test(p), + resolve: () => ({ ns: "draws", key: "subnav.riskOccupancy" }), + }, + { + test: (p) => /^\/admin\/draws\/\d+\/risk\/hot$/.test(p) || /^\/admin\/risk\/draws\/\d+\/hot$/.test(p), + resolve: () => ({ ns: "draws", key: "subnav.riskHot" }), + }, + { + test: (p) => /^\/admin\/draws\/\d+\/risk\/sold-out$/.test(p) || /^\/admin\/risk\/draws\/\d+\/sold-out$/.test(p), + resolve: () => ({ ns: "draws", key: "subnav.riskSoldOut" }), + }, + { + test: (p) => /^\/admin\/draws\/\d+\/risk\/pools$/.test(p) || /^\/admin\/risk\/draws\/\d+\/pools$/.test(p), + resolve: () => ({ ns: "draws", key: "subnav.riskPools" }), + }, + { + test: (p) => /^\/admin\/draws\/\d+\/risk\/pools\/[^/]+$/.test(p) || /^\/admin\/risk\/draws\/\d+\/pools\/[^/]+$/.test(p), + resolve: (p) => { + const number = decodeURIComponent(p.split("/").pop() ?? ""); + return { ns: "risk", key: "numberTitle", params: { number } }; + }, + }, + { + test: (p) => /^\/admin\/draws\/\d+\/risk$/.test(p), + resolve: () => ({ ns: "risk", key: "title" }), + }, + { + test: (p) => /^\/admin\/draws\/\d+$/.test(p), + resolve: () => ({ ns: "draws", key: "drawDetail" }), + }, + { + test: (p) => /^\/admin\/settlement-batches\/\d+\/details$/.test(p), + resolve: () => ({ ns: "settlement", key: "details" }), + }, +]; + +/** 根据路径解析浏览器标题用的 i18n 规格。 */ +export function resolveAdminPageTitle(pathname: string): PageTitleSpec | null { + if (EXACT_ROUTES[pathname]) { + return EXACT_ROUTES[pathname]; + } + + for (const pattern of ROUTE_PATTERNS) { + if (pattern.test(pathname)) { + return pattern.resolve(pathname); + } + } + + return null; +} diff --git a/src/lib/page-metadata.ts b/src/lib/page-metadata.ts new file mode 100644 index 0000000..a4ac413 --- /dev/null +++ b/src/lib/page-metadata.ts @@ -0,0 +1,57 @@ +import type { Metadata } from "next"; + +import enAdminRoles from "@/i18n/locales/en/adminRoles.json"; +import enAdminUsers from "@/i18n/locales/en/adminUsers.json"; +import enAudit from "@/i18n/locales/en/audit.json"; +import enAuth from "@/i18n/locales/en/auth.json"; +import enConfig from "@/i18n/locales/en/config.json"; +import enDashboard from "@/i18n/locales/en/dashboard.json"; +import enDraws from "@/i18n/locales/en/draws.json"; +import enJackpot from "@/i18n/locales/en/jackpot.json"; +import enPlayers from "@/i18n/locales/en/players.json"; +import enReconcile from "@/i18n/locales/en/reconcile.json"; +import enRisk from "@/i18n/locales/en/risk.json"; +import enSettlement from "@/i18n/locales/en/settlement.json"; +import enCommon from "@/i18n/locales/en/common.json"; +import enTickets from "@/i18n/locales/en/tickets.json"; +import enWallet from "@/i18n/locales/en/wallet.json"; + +const EN_FLAT: Record> = { + dashboard: enDashboard, + players: enPlayers, + draws: enDraws, + tickets: enTickets, + settlement: enSettlement, + reconcile: enReconcile, + audit: enAudit, + adminUsers: enAdminUsers, + adminRoles: enAdminRoles, + wallet: enWallet, + risk: enRisk, + jackpot: enJackpot, + config: enConfig, + common: enCommon, + auth: enAuth, +}; + +function getByPath(obj: Record, path: string): string | undefined { + const parts = path.split("."); + let cur: unknown = obj; + for (const part of parts) { + if (cur == null || typeof cur !== "object") { + return undefined; + } + cur = (cur as Record)[part]; + } + return typeof cur === "string" ? cur : undefined; +} + +/** SSR 默认英文标题;客户端由 {@link AdminDocumentTitle} 按用户语言覆盖。 */ +export function buildPageMetadata(ns: string, key: string): Metadata { + const bundle = EN_FLAT[ns]; + const title = bundle ? getByPath(bundle as Record, key) : undefined; + + return { + title: title ?? key, + }; +} diff --git a/src/modules/_config/admin-nav-icons.tsx b/src/modules/_config/admin-nav-icons.tsx index dad3077..23003b3 100644 --- a/src/modules/_config/admin-nav-icons.tsx +++ b/src/modules/_config/admin-nav-icons.tsx @@ -24,7 +24,10 @@ export const adminNavIconBySegment: Record dashboard: LayoutDashboard, players: Users, draws: CalendarClock, - config: SlidersHorizontal, + rules_plays: SlidersHorizontal, + rules_odds: SlidersHorizontal, + jackpot: CircleDollarSign, + risk_cap: ShieldAlert, tickets: Ticket, wallet: Wallet, risk: ShieldAlert, diff --git a/src/modules/_config/admin-nav.ts b/src/modules/_config/admin-nav.ts index e080a90..157f074 100644 --- a/src/modules/_config/admin-nav.ts +++ b/src/modules/_config/admin-nav.ts @@ -4,7 +4,10 @@ export type AdminNavSegment = | "dashboard" | "players" | "draws" - | "config" + | "rules_plays" + | "rules_odds" + | "jackpot" + | "risk_cap" | "tickets" | "wallet" | "risk" diff --git a/src/modules/admin-roles/admin-roles-console.tsx b/src/modules/admin-roles/admin-roles-console.tsx index 59510e0..66544c5 100644 --- a/src/modules/admin-roles/admin-roles-console.tsx +++ b/src/modules/admin-roles/admin-roles-console.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { ChevronDown } from "lucide-react"; +import { useExportLabels } from "@/hooks/use-export-labels"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; @@ -52,6 +53,7 @@ function permissionLabel(slug: string, fallback: string, t: (key: string) => str export function AdminRolesConsole(): React.ReactElement { const { t } = useTranslation(["adminUsers", "common"]); + const exportLabels = useExportLabels("adminRoles"); const [catalog, setCatalog] = useState(null); const [roles, setRoles] = useState([]); const [loading, setLoading] = useState(true); @@ -312,8 +314,8 @@ export function AdminRolesConsole(): React.ReactElement {
diff --git a/src/modules/config/config-hub-screen.tsx b/src/modules/config/config-hub-screen.tsx new file mode 100644 index 0000000..c7f3c8a --- /dev/null +++ b/src/modules/config/config-hub-screen.tsx @@ -0,0 +1,77 @@ +"use client"; + +import Link from "next/link"; +import { useTranslation } from "react-i18next"; +import { ChevronRight } from "lucide-react"; + +import { ModuleScaffold } from "@/components/admin/module-scaffold"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { useAdminProfile } from "@/stores/admin-session"; +import { adminHasAnyPermission } from "@/lib/admin-permissions"; + +type HubCard = { + href: string; + titleKey: string; + descKey: string; + requiredAny: readonly string[]; +}; + +const HUB_CARDS: HubCard[] = [ + { + href: "/admin/rules/plays", + titleKey: "hub.playsTitle", + descKey: "hub.playsDesc", + requiredAny: ["prd.play_switch.manage", "prd.odds.manage"], + }, + { + href: "/admin/rules/odds", + titleKey: "hub.oddsTitle", + descKey: "hub.oddsDesc", + requiredAny: ["prd.odds.manage", "prd.rebate.manage", "prd.rebate.view"], + }, + { + href: "/admin/jackpot", + titleKey: "hub.jackpotTitle", + descKey: "hub.jackpotDesc", + requiredAny: ["prd.jackpot.manage", "prd.jackpot.view"], + }, + { + href: "/admin/risk/cap", + titleKey: "hub.riskCapTitle", + descKey: "hub.riskCapDesc", + requiredAny: ["prd.risk_cap.manage", "prd.risk_cap.view"], + }, +]; + +export function ConfigHubScreen() { + const { t } = useTranslation("config"); + const profile = useAdminProfile(); + const visible = HUB_CARDS.filter((card) => + adminHasAnyPermission(profile?.permissions, card.requiredAny), + ); + + return ( + +
+

{t("hub.title")}

+

{t("hub.description")}

+
+
+ {visible.map((card) => ( + + + + + {t(card.titleKey)} + + + {t(card.descKey)} + + + + + ))} +
+
+ ); +} diff --git a/src/modules/config/config-version-switcher.tsx b/src/modules/config/config-version-switcher.tsx index 40d12fb..979ca86 100644 --- a/src/modules/config/config-version-switcher.tsx +++ b/src/modules/config/config-version-switcher.tsx @@ -158,9 +158,11 @@ export function ConfigVersionSwitcher({ {resolvedSheetTitle} - - {resolvedSheetDescription} - + {resolvedSheetDescription ? ( + + {resolvedSheetDescription} + + ) : null}
diff --git a/src/modules/config/doc/odds-config-doc-screen.tsx b/src/modules/config/doc/odds-config-doc-screen.tsx index b87141f..d6fd4f2 100644 --- a/src/modules/config/doc/odds-config-doc-screen.tsx +++ b/src/modules/config/doc/odds-config-doc-screen.tsx @@ -61,7 +61,12 @@ function filterTypes(tab: CatTab, types: AdminPlayTypeRow[]): AdminPlayTypeRow[] return types.filter((t) => t.dimension === dim); } -export function OddsConfigDocScreen() { +type OddsConfigDocScreenProps = { + /** 嵌入「赔率与回水」合并页时去掉外层 ConfigDocPage */ + embedded?: boolean; +}; + +export function OddsConfigDocScreen({ embedded = false }: OddsConfigDocScreenProps) { const { t } = useTranslation(["config", "adminUsers", "common"]); const formatDt = useAdminDateTimeFormatter(); const [types, setTypes] = useState([]); @@ -395,10 +400,7 @@ export function OddsConfigDocScreen() { { id: "d2", label: "2D" }, ]; - return ( - {catTabs.map((tab) => ( @@ -427,8 +429,9 @@ export function OddsConfigDocScreen() { )}
- } - toolbar={ + ); + + const toolbarBlock = ( } /> - } - context={ - detail ? ( - - {t("odds.activeVersionPrefix", { ns: "config" })} - {activeHead ? ( - <> - v{activeHead.version_no} - {activeHead.effective_at ? ` · ${formatDt(activeHead.effective_at)}` : ""} - - ) : ( - "—" - )} - {!isDraft ? ( - <> - {" "} - — {t("odds.readOnlyHint", { ns: "config" })} - - ) : null} - - ) : null - } - > + ); + + const contextBlock = + embedded || !detail ? null : ( + + {t("odds.activeVersionPrefix", { ns: "config" })} + {activeHead ? ( + <> + v{activeHead.version_no} + {activeHead.effective_at ? ` · ${formatDt(activeHead.effective_at)}` : ""} + + ) : ( + "—" + )} + {!isDraft ? ( + <> + {" "} + — {t("odds.readOnlyHint", { ns: "config" })} + + ) : null} + + ); + + const mainBlock = ( + <> {error ?

{error}

: null} {loadingDetail || loadingTypes ? ( @@ -489,7 +494,7 @@ export function OddsConfigDocScreen() {
{PRIZE_SCOPE_ORDER.map((scope) => { const row = scopeRows[scope]; - const hint = PRIZE_SCOPE_MULTIPLIER_HINT[scope]; + const hint = embedded ? null : PRIZE_SCOPE_MULTIPLIER_HINT[scope]; const idx = row ? rowIndex(resolvedPlayCode, scope) : -1; return (
@@ -517,13 +522,15 @@ export function OddsConfigDocScreen() { {row.odds_value} )} - - {t("odds.multiplier", { - ns: "config", - value: oddsMultiplierLabel(row.odds_value), - currency: row.currency_code, - })} - + {!embedded ? ( + + {t("odds.multiplier", { + ns: "config", + value: oddsMultiplierLabel(row.odds_value), + currency: row.currency_code, + })} + + ) : null}
) : (

{t("odds.missingScopeRow", { ns: "config", scope })}

@@ -547,11 +554,17 @@ export function OddsConfigDocScreen() { {rebatePercentUi} )} -

{t("odds.rebateRateHint", { ns: "config" })}

+ {!embedded ? ( +

{t("odds.rebateRateHint", { ns: "config" })}

+ ) : null}
) : null} + + ); + const dialogs = ( + <> @@ -612,6 +625,30 @@ export function OddsConfigDocScreen() { + + ); + + if (embedded) { + return ( +
+ {filtersBlock} + {toolbarBlock} + {contextBlock} + {mainBlock} + {dialogs} +
+ ); + } + + return ( + + {mainBlock} + {dialogs} ); } diff --git a/src/modules/config/doc/rebate-config-doc-screen.tsx b/src/modules/config/doc/rebate-config-doc-screen.tsx index e941324..ea0df81 100644 --- a/src/modules/config/doc/rebate-config-doc-screen.tsx +++ b/src/modules/config/doc/rebate-config-doc-screen.tsx @@ -48,7 +48,11 @@ function inferPercentFrom(dim: 2 | 3 | 4, rows: OddsItemRow[], typeList: AdminPl return hit ? rateToPercentUi(String(hit.rebate_rate)) : "0"; } -export function RebateConfigDocScreen() { +type RebateConfigDocScreenProps = { + embedded?: boolean; +}; + +export function RebateConfigDocScreen({ embedded = false }: RebateConfigDocScreenProps) { const { t } = useTranslation(["config", "common"]); const formatDt = useAdminDateTimeFormatter(); const [types, setTypes] = useState([]); @@ -266,60 +270,59 @@ export function RebateConfigDocScreen() { } } - return ( - - } - actions={ - void refreshList()} - onNewDraft={() => void handleNewDraft()} - onSaveDraft={() => void handleSave()} - onPublish={() => void handlePublish()} - /> - } + const toolbarBlock = embedded ? null : ( + } - context={ - detail ? ( - - {t("rebate.editingVersion", { - ns: "config", - version: detail.version_no, - status: - detail.status === "draft" - ? t("versionStatus.draft", { ns: "config" }) - : detail.status === "active" - ? t("versionStatus.active", { ns: "config" }) - : t("versionStatus.archived", { ns: "config" }), - })} - {!isDraft ? ( - <> - {" "} - — {t("rebate.readOnlyHint", { ns: "config" })} - - ) : null} - - ) : null + actions={ + void refreshList()} + onNewDraft={() => void handleNewDraft()} + onSaveDraft={() => void handleSave()} + onPublish={() => void handlePublish()} + /> } - > + /> + ); + + const contextBlock = + embedded || !detail ? null : ( + + {t("rebate.editingVersion", { + ns: "config", + version: detail.version_no, + status: + detail.status === "draft" + ? t("versionStatus.draft", { ns: "config" }) + : detail.status === "active" + ? t("versionStatus.active", { ns: "config" }) + : t("versionStatus.archived", { ns: "config" }), + })} + {!isDraft ? ( + <> + {" "} + — {t("rebate.readOnlyHint", { ns: "config" })} + + ) : null} + + ); + + const fieldsBlock = ( + <>
@@ -386,16 +389,37 @@ export function RebateConfigDocScreen() {
+ {!embedded ? (
{t("rebate.effectiveTime", { ns: "config" })} {activeHead?.effective_at ? formatDt(activeHead.effective_at) : "—"}
+ ) : null} {loading || loadingDetail ? (

{t("states.loading", { ns: "common" })}

) : null} + + ); + + if (embedded) { + return ( +
+ {contextBlock} + {fieldsBlock} +
+ ); + } + + return ( + + {fieldsBlock} ); } diff --git a/src/modules/dashboard/dashboard-console.tsx b/src/modules/dashboard/dashboard-console.tsx index 2be2f27..fcc3f3a 100644 --- a/src/modules/dashboard/dashboard-console.tsx +++ b/src/modules/dashboard/dashboard-console.tsx @@ -24,10 +24,21 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; +import { + CapUsageBar, + FinanceStructureChart, + HotUsageBars, + PayoutCompositionChart, + ResultBatchProgress, + SettlementStatusChart, + SoldOutRing, + StatCard, +} from "@/modules/dashboard/dashboard-visuals"; import { useAdminCurrencyCatalog } from "@/hooks/use-admin-currency-catalog"; import { formatAdminMinorUnits, getAdminCurrencyDecimalPlaces } from "@/lib/money"; import { cn } from "@/lib/utils"; import { LotteryApiBizError } from "@/types/api/errors"; +import type { AdminDashboardDrawPanel } from "@/types/api/admin-dashboard"; import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance"; import type { AdminRiskPoolRow } from "@/types/api/admin-risk"; import type { DrawCurrentSnapshot } from "@/types/api/public-draw"; @@ -66,7 +77,6 @@ function formatSignedMoneyMinor(minor: number, currencyCode: string | null): str return `${s}${formatMoneyMinor(Math.abs(minor), currencyCode)}`; } -/** Aligned with the bucket dimensions used by AdminDashboardSnapshotBuilder::soldOutBucketKey. */ function poolPlayCategory(normalizedNumber: string): HotPlayTab | "other" { const raw = normalizedNumber.trim(); const digits = raw.replace(/\D/g, ""); @@ -98,156 +108,6 @@ function topPoolsForTab(pools: AdminRiskPoolRow[], tab: HotPlayTab): AdminRiskPo .slice(0, 10); } -function RiskSemiGauge({ pct }: { pct: number }): ReactElement { - const { t } = useTranslation("dashboard"); - const v = Math.min(100, Math.max(0, pct)); - const r = 76; - const arcLen = Math.PI * r; - - return ( -
- - - - -
-

{v.toFixed(2)}%

-

{t("capUsage")}

-
-
- ); -} - -function HotBarChart({ rows }: { rows: AdminRiskPoolRow[] }): ReactElement { - const { t } = useTranslation("dashboard"); - const maxU = Math.max(0.0001, ...rows.map((r) => r.usage_ratio ?? 0)); - - return ( -
-
- - {t("capUsage")} - - {rows.length === 0 ? ( -

{t("noPoolData")}

- ) : ( - rows.map((row) => { - const u = row.usage_ratio ?? 0; - const h = Math.max(8, (u / maxU) * 100); - return ( -
-
-
-
- - {row.normalized_number.trim()} - -
- ); - }) - )} -
-

{t("numbersByUsage")}

-
- ); -} - -function SoldOutDonut({ buckets }: { buckets: SoldOutBuckets }): ReactElement { - const { t } = useTranslation("dashboard"); - const entries: { key: keyof SoldOutBuckets; label: string; color: string }[] = [ - { key: "d4", label: t("soldOutBuckets.d4"), color: "oklch(0.32 0.08 260)" }, - { key: "d3", label: t("soldOutBuckets.d3"), color: "oklch(0.48 0.12 250)" }, - { key: "d2", label: t("soldOutBuckets.d2"), color: "oklch(0.78 0.14 95)" }, - { key: "special", label: t("soldOutBuckets.special"), color: "oklch(0.55 0.22 25)" }, - { key: "other", label: t("soldOutBuckets.other"), color: "oklch(0.62 0.16 145)" }, - ]; - const total = entries.reduce((s, e) => s + buckets[e.key], 0); - - if (total === 0) { - return ( -
-

{t("noSoldOutNumbers")}

-
- ); - } - - let acc = 0; - const parts = entries - .filter((e) => buckets[e.key] > 0) - .map((e) => { - const frac = buckets[e.key] / total; - const start = acc; - acc += frac; - return { ...e, frac, start }; - }); - - const gradientStops = - parts.length === 1 - ? `${parts[0].color} 0deg 360deg` - : parts - .map((p) => { - const a0 = p.start * 360; - const a1 = (p.start + p.frac) * 360; - return `${p.color} ${a0}deg ${a1}deg`; - }) - .join(", "); - - return ( -
-
-
-
-

{total}

-

{t("soldOutTotal")}

-
-
-
    - {entries.map((e) => ( -
  • - - - {e.label} - - {buckets[e.key]} -
  • - ))} -
-
- ); -} - export function DashboardConsole(): ReactElement { const { t } = useTranslation(["dashboard", "common"]); useAdminCurrencyCatalog(); @@ -259,6 +119,7 @@ export function DashboardConsole(): ReactElement { const [hall, setHall] = useState(null); const [drawId, setDrawId] = useState(null); + const [drawPanel, setDrawPanel] = useState(null); const [finance, setFinance] = useState(null); const [pendingReview, setPendingReview] = useState(null); const [riskLocked, setRiskLocked] = useState(0); @@ -277,6 +138,7 @@ export function DashboardConsole(): ReactElement { setError(null); setNotice(null); setFinance(null); + setDrawPanel(null); setPendingReview(null); setDrawId(null); setRiskLocked(0); @@ -297,6 +159,7 @@ export function DashboardConsole(): ReactElement { setFinance(d.finance); } if (d.draw != null) { + setDrawPanel(d.draw); setPendingReview(d.draw.result_batch_counts.pending_review); } if (d.risk != null) { @@ -326,10 +189,10 @@ export function DashboardConsole(): ReactElement { }, [t]); useEffect(() => { - const t = window.setTimeout(() => { + const timer = window.setTimeout(() => { void load(false); }, 0); - return () => window.clearTimeout(t); + return () => window.clearTimeout(timer); }, [load]); const currency = finance?.currency_code ?? null; @@ -355,19 +218,26 @@ export function DashboardConsole(): ReactElement { { href: "/admin/audit-logs", label: t("quickLinks.auditLogs"), icon: }, ]; + const kpiSkeleton = ( +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ +
+ ))} +
+ ); + return (
-
-

{t("title")}

-
+

{t("title")}

{todayLabel}