refactor: 更新管理端页面元数据,统一国际化支持,移除冗余代码

This commit is contained in:
2026-05-21 17:27:52 +08:00
parent 26feed3c4f
commit e8a5507411
77 changed files with 1669 additions and 732 deletions

View File

@@ -29,7 +29,10 @@ const NAV_TRANSLATION_KEYS: Record<string, string> = {
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<string, string> = {
settings: "settings",
};
const RULES_ROUTE_LABELS: Record<string, string> = {
plays: "nav.items.plays",
odds: "nav.rulesOddsTitle",
};
const RISK_ROUTE_LABELS: Record<string, string> = {
cap: "nav.riskCapTitle",
};
const SETTINGS_ROUTE_LABELS: Record<string, string> = {
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",

View File

@@ -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;
}

View File

@@ -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 (
<SidebarProvider defaultOpen>
<AdminDocumentTitle />
<AdminAppSidebar />
<SidebarInset className="min-w-0 overflow-x-clip max-md:overflow-x-clip">
<header className="sticky top-0 z-30 flex h-14 shrink-0 items-center gap-3 border-b border-border bg-card/90 px-4 shadow-[0_1px_0_rgb(216_230_251_/_45%)] backdrop-blur-md">

View File

@@ -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 (
<Button type="button" size="sm" variant="secondary" onClick={onExport}>
<Download className="size-4" />
{label ?? t("actions.exportExcel", { defaultValue: "导出 Excel" })}
{label ?? t("actions.exportExcel")}
</Button>
);
}