feat: layout components (Sidebar, Header, MobileNav, TabBar, AppLayout) with routing
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useTheme } from '../../hooks/useTheme'
|
||||
import Sidebar from './Sidebar'
|
||||
import Header from './Header'
|
||||
import MobileNav from './MobileNav'
|
||||
import TabBar from './TabBar'
|
||||
|
||||
function useMediaQuery(query: string) {
|
||||
const [matches, setMatches] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.matchMedia(query).matches
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const mql = window.matchMedia(query)
|
||||
mql.addEventListener('change', (e) => setMatches(e.matches))
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
export interface AppLayoutProps {
|
||||
children: ReactNode
|
||||
breadcrumb?: string
|
||||
onlineCount?: number
|
||||
runningCount?: number
|
||||
onSearch?: (query: string) => any
|
||||
}
|
||||
|
||||
export default function AppLayout({
|
||||
children,
|
||||
breadcrumb,
|
||||
onlineCount = 0,
|
||||
runningCount = 0,
|
||||
onSearch,
|
||||
}: AppLayoutProps) {
|
||||
const { theme, toggle } = useTheme()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const isMobile = useMediaQuery('(max-width: 767px)')
|
||||
const isTablet = useMediaQuery('(min-width: 768px) and (max-width: 1023px)')
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false)
|
||||
|
||||
const handleNavigate = (path: string) => {
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
const sidebarWidth = isTablet ? 180 : 220
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col" style={{ background: 'var(--bg-root)' }}>
|
||||
{/* Mobile header */}
|
||||
<header
|
||||
className="flex items-center justify-between px-4 shrink-0"
|
||||
style={{
|
||||
height: 'var(--header-h)',
|
||||
background: 'var(--bg-nav)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setMobileNavOpen(true)}
|
||||
className="cursor-pointer bg-transparent border-none text-[20px] p-1"
|
||||
style={{ color: 'var(--text)' }}
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
<span style={{ color: 'var(--honey)', fontWeight: 700, fontSize: 14 }}>知微蜂巢</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="cursor-pointer bg-transparent border-none text-[18px] leading-none p-1"
|
||||
title={theme === 'dark' ? '切换浅色模式' : '切换深色模式'}
|
||||
>
|
||||
{theme === 'dark' ? '☀️' : '🌙'}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* Mobile content */}
|
||||
<main className="flex-1 overflow-y-auto p-3 pb-[64px]">{children}</main>
|
||||
|
||||
{/* Mobile tab bar */}
|
||||
<TabBar activePath={location.pathname} />
|
||||
|
||||
{/* Mobile nav drawer */}
|
||||
<MobileNav
|
||||
open={mobileNavOpen}
|
||||
onClose={() => setMobileNavOpen(false)}
|
||||
activePath={location.pathname}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `${sidebarWidth}px 1fr`,
|
||||
gridTemplateRows: 'var(--header-h) 1fr',
|
||||
gridTemplateAreas: '"sidebar header" "sidebar main"',
|
||||
background: 'var(--bg-root)',
|
||||
}}
|
||||
>
|
||||
{/* Sidebar */}
|
||||
<div style={{ gridArea: 'sidebar' }}>
|
||||
<Sidebar activePath={location.pathname} onNavigate={handleNavigate} />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ gridArea: 'header' }}>
|
||||
<Header
|
||||
breadcrumb={breadcrumb}
|
||||
onlineCount={onlineCount}
|
||||
runningCount={runningCount}
|
||||
onSearch={onSearch}
|
||||
theme={theme}
|
||||
onToggleTheme={toggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main */}
|
||||
<main
|
||||
className="overflow-y-auto p-5"
|
||||
style={{ gridArea: 'main', background: 'var(--bg-root)' }}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user