haixunMaster/haixun-backend/web/src/components/Layout.tsx

110 lines
4.0 KiB
TypeScript
Raw Normal View History

2026-06-23 09:54:27 +00:00
import { NavLink, Outlet } from 'react-router-dom'
import { useAuth } from '../auth/AuthContext'
import { MobileBottomNav } from './MobileBottomNav'
import { ThemeToggle } from './ThemeToggle'
const navMain = [
{ to: '/', label: '總覽' },
{ to: '/jobs', label: '背景任務' },
{ to: '/job-schedules', label: '排程' },
{ to: '/ai', label: 'AI' },
]
const navMore = [
{ to: '/job-templates', label: '模板' },
{ to: '/settings', label: '設定' },
{ to: '/profile', label: '會員' },
{ to: '/permissions', label: '權限' },
]
function NavItem({ to, label, end }: { to: string; label: string; end?: boolean }) {
return (
<NavLink
to={to}
end={end}
className={({ isActive }) =>
`rounded-[var(--radius-pill)] px-4 py-2 text-sm font-semibold transition ${
isActive
? 'bg-brand text-white shadow-soft'
: 'text-ink hover:bg-brand-soft hover:text-brand'
}`
}
>
{label}
</NavLink>
)
}
export function Layout() {
const { member, uid, logout } = useAuth()
return (
<div className="flex min-h-screen bg-canvas">
<aside className="hidden w-60 shrink-0 flex-col border-r border-line bg-surface/90 p-4 backdrop-blur-md lg:flex lg:w-64 lg:p-5">
<div className="rounded-[var(--radius-xl)] border border-line bg-surface-muted px-4 py-4">
<div className="display-en text-[10px] font-bold tracking-[0.22em] text-brand uppercase">Haixun</div>
<div className="mt-1 text-2xl font-black text-brand"></div>
</div>
<nav className="mt-6 flex flex-1 flex-col gap-5">
<div>
<p className="mb-2 px-2 text-xs font-bold tracking-wide text-ink-secondary uppercase"></p>
<div className="flex flex-col gap-1">
{navMain.map((item) => (
<NavItem key={item.to} to={item.to} label={item.label} end={item.to === '/'} />
))}
</div>
</div>
<div>
<p className="mb-2 px-2 text-xs font-bold tracking-wide text-ink-secondary uppercase"></p>
<div className="flex flex-col gap-1">
{navMore.map((item) => (
<NavItem key={item.to} to={item.to} label={item.label} />
))}
</div>
</div>
</nav>
<div className="rounded-[var(--radius-lg)] border border-line bg-surface-muted p-3 text-sm">
<div className="truncate font-semibold text-ink">{member?.email}</div>
<div className="mt-1.5">
<BadgePill>{member?.roles?.join(', ') || 'user'}</BadgePill>
</div>
<button
type="button"
onClick={() => logout()}
className="mt-3 w-full rounded-[var(--radius-pill)] border border-line bg-surface px-3 py-2 font-semibold text-ink transition hover:border-brand/30 hover:text-brand"
>
</button>
</div>
</aside>
<div className="flex min-w-0 flex-1 flex-col">
<header className="sticky top-0 z-10 flex items-center justify-between border-b border-line bg-canvas/85 px-4 py-3 backdrop-blur-md sm:px-5 lg:px-8">
<div className="min-w-0 lg:hidden">
<p className="text-lg font-black text-brand"></p>
<p className="truncate font-mono text-[11px] text-ink-secondary">{uid || member?.uid}</p>
</div>
<p className="hidden truncate text-sm text-ink-secondary lg:block">
<span className="font-mono text-xs text-ink">{uid || member?.uid}</span>
</p>
<ThemeToggle compact className="shrink-0" />
</header>
<main className="layout-main flex-1 overflow-auto px-4 py-5 sm:px-5 md:px-8 md:py-8">
<Outlet />
</main>
</div>
<MobileBottomNav />
</div>
)
}
function BadgePill({ children }: { children: React.ReactNode }) {
return (
<span className="inline-block rounded-[var(--radius-pill)] bg-brand-soft px-2.5 py-0.5 text-xs font-bold text-brand">
{children}
</span>
)
}