- Node.js backend server - Frontend application - Backup script - Project specification Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
71 lines
2.5 KiB
JavaScript
71 lines
2.5 KiB
JavaScript
import { useMemo } from 'react';
|
|
import { useRouter } from 'next/router';
|
|
import { Icon } from './icons';
|
|
|
|
const tabs = [
|
|
{ href: '/', label: '首页', icon: 'home' },
|
|
{ href: '/stats', label: '统计', icon: 'chart' },
|
|
{ href: '/recurring', label: '周期', icon: 'repeat' },
|
|
{ href: '/categories', label: '分类', icon: 'tags' },
|
|
];
|
|
|
|
export default function AppShell({ title, subtitle, actions, headerContent, children, contentClassName = '', hideNav = false, compactHeader = false }) {
|
|
const router = useRouter();
|
|
|
|
const activeTab = useMemo(() => {
|
|
const matched = tabs.find((tab) => tab.href === router.pathname);
|
|
return matched?.href || null;
|
|
}, [router.pathname]);
|
|
|
|
const headerNode = headerContent || (title ? (
|
|
<div className="app-header__text">
|
|
<h1>{title}</h1>
|
|
{subtitle ? <p>{subtitle}</p> : null}
|
|
</div>
|
|
) : null);
|
|
|
|
const headerClassName = [
|
|
'app-header',
|
|
compactHeader ? 'app-header--compact' : '',
|
|
headerNode ? 'app-header--with-content' : 'app-header--actions-only',
|
|
actions ? 'app-header--with-actions' : '',
|
|
].filter(Boolean).join(' ');
|
|
|
|
return (
|
|
<div className={`app-shell${hideNav ? ' app-shell--standalone' : ''}`}>
|
|
<header className={headerClassName}>
|
|
<div className="app-shell__container app-header__inner">
|
|
{headerNode ? <div className="app-header__main">{headerNode}</div> : <div className="app-header__spacer" aria-hidden="true" />}
|
|
{actions ? <div className="app-header__actions">{actions}</div> : null}
|
|
</div>
|
|
</header>
|
|
|
|
<main className={`app-content ${contentClassName}`.trim()}>
|
|
<div className="app-shell__container app-content__inner">{children}</div>
|
|
</main>
|
|
|
|
{!hideNav ? (
|
|
<nav className="bottom-nav" aria-label="主导航">
|
|
<div className="app-shell__container bottom-nav__inner">
|
|
{tabs.map((tab) => {
|
|
const isActive = activeTab === tab.href;
|
|
return (
|
|
<button
|
|
key={tab.href}
|
|
type="button"
|
|
className={`bottom-nav__item${isActive ? ' is-active' : ''}`}
|
|
onClick={() => router.push(tab.href)}
|
|
aria-current={isActive ? 'page' : undefined}
|
|
>
|
|
<Icon name={tab.icon} size={20} />
|
|
<span>{tab.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</nav>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|