feat: add ToolsLayout component and update routing for tools navigation

This commit is contained in:
2026-02-24 11:06:36 +08:00
parent a195299e91
commit 0bbc21bfa0
4 changed files with 161 additions and 10 deletions
+6 -1
View File
@@ -7,7 +7,12 @@
"navigation": {
"home": "Home",
"about": "About",
"contact": "Contact"
"contact": "Contact",
"tools": "Tools",
"jsonProcessing": "JSON Processing",
"dailyTools": "Daily Tools",
"expandToolsMenu": "Expand tools menu",
"collapseToolsMenu": "Collapse tools menu"
},
"language": {
"switch": "Switch Language",
+6 -1
View File
@@ -7,7 +7,12 @@
"navigation": {
"home": "首页",
"about": "关于",
"contact": "联系"
"contact": "联系",
"tools": "工具",
"jsonProcessing": "JSON 处理",
"dailyTools": "日常小工具",
"expandToolsMenu": "展开工具菜单",
"collapseToolsMenu": "收起工具菜单"
},
"language": {
"switch": "切换语言",
+133
View File
@@ -0,0 +1,133 @@
import { useMemo, useState } from "react"
import { Link, NavLink, Outlet } from "react-router-dom"
import { useTranslation } from "react-i18next"
import dayjs from "dayjs"
import LanguageSwitcher from "@/components/language-switcher"
export default function ToolsLayout() {
const today = useMemo(() => dayjs(), [])
const { t } = useTranslation()
const [collapsed, setCollapsed] = useState(false)
const toolGroups = useMemo(
() => [
{
title: t("navigation.jsonProcessing"),
items: [
{ to: "/json-viewer", label: t("home.jsonViewer"), shortLabel: "JV" },
{ to: "/json-grid", label: t("home.jsonGrid"), shortLabel: "JG" },
],
},
{
title: t("navigation.dailyTools"),
items: [{ to: "/bmi-calculator", label: t("home.bmiCalculator"), shortLabel: "BMI" }],
},
],
[t]
)
return (
<div className="h-screen bg-gray-50 flex flex-col overflow-hidden">
<header className="bg-white shadow-sm border-b">
<div className="px-4">
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
<h1 className="text-xl font-semibold text-gray-900">{t("app.title")}</h1>
</div>
<div className="flex items-center gap-4">
<nav className="flex space-x-8">
<Link
to="/"
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
{t("navigation.home")}
</Link>
<Link
to="/about"
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
{t("navigation.about")}
</Link>
<Link
to="/contact"
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
{t("navigation.contact")}
</Link>
</nav>
<LanguageSwitcher />
</div>
</div>
</div>
</header>
<main className="flex-1 p-4 overflow-hidden min-h-0">
<div className="h-full flex gap-4 overflow-hidden">
<aside
className={`bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col transition-all duration-200 ${
collapsed ? "w-16" : "w-56"
}`}
>
<div className="px-3 py-3 border-b border-slate-200 flex items-center justify-between">
{!collapsed && (
<span className="text-xs font-semibold uppercase tracking-wider text-slate-500">
{t("navigation.tools")}
</span>
)}
<button
type="button"
onClick={() => setCollapsed((value) => !value)}
className="w-8 h-8 rounded-md border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colours text-sm"
aria-label={
collapsed ? t("navigation.expandToolsMenu") : t("navigation.collapseToolsMenu")
}
>
{collapsed ? "»" : "«"}
</button>
</div>
<nav className="p-2 flex-1 overflow-auto">
{toolGroups.map((group) => (
<div key={group.title} className="mb-3 last:mb-0">
{!collapsed && (
<div className="px-3 py-1 text-[11px] font-semibold uppercase tracking-wider text-slate-400">
{group.title}
</div>
)}
{group.items.map((tool) => (
<NavLink
key={tool.to}
to={tool.to}
className={({ isActive }) =>
`w-full flex items-center ${collapsed ? "justify-center" : "justify-start"} rounded-lg px-3 py-2 text-sm font-medium mb-1 transition-colours ${
isActive
? "bg-indigo-100 text-indigo-700"
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900"
}`
}
title={collapsed ? `${group.title} · ${tool.label}` : tool.label}
>
{collapsed ? tool.shortLabel : tool.label}
</NavLink>
))}
</div>
))}
</nav>
</aside>
<section className="flex-1 overflow-hidden min-h-0">
<Outlet />
</section>
</div>
</main>
<footer className="bg-white border-t shrink-0">
<div className="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
<p className="text-center text-sm text-gray-500">
{t("app.copyright", { year: today.year() })}
</p>
</div>
</footer>
</div>
)
}
+16 -8
View File
@@ -2,6 +2,7 @@ import { ComponentType } from "react"
import { createBrowserRouter } from "react-router-dom"
import ErrorPage from "@/components/error-page"
import HeroLayout from "@/layout/hero-layout"
import ToolsLayout from "@/layout/tools-layout"
function lazy<T extends { default: ComponentType<unknown> }>(importer: () => Promise<T>) {
return async () => {
@@ -27,6 +28,21 @@ const router = createBrowserRouter(
index: true,
lazy: lazy(() => import("@/page/home")),
},
{
path: "about",
lazy: lazy(() => import("@/page/about")),
},
{
path: "contact",
lazy: lazy(() => import("@/page/contact")),
},
],
},
{
path: "/",
element: <ToolsLayout />,
errorElement: <ErrorPage />,
children: [
{
path: "json-viewer",
lazy: lazy(() => import("@/page/json-viewer")),
@@ -39,14 +55,6 @@ const router = createBrowserRouter(
path: "bmi-calculator",
lazy: lazy(() => import("@/page/bmi-calculator")),
},
{
path: "about",
lazy: lazy(() => import("@/page/about")),
},
{
path: "contact",
lazy: lazy(() => import("@/page/contact")),
},
],
},
],