refactor: 更新管理端页面元数据,统一国际化支持,移除冗余代码
This commit is contained in:
@@ -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",
|
||||
|
||||
28
src/components/admin/admin-document-title.tsx
Normal file
28
src/components/admin/admin-document-title.tsx
Normal 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;
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user