139 lines
3.9 KiB
TypeScript
139 lines
3.9 KiB
TypeScript
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>
|
|
)
|
|
}
|