AppShell.tsx
4.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { useState } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'
import { useAppSelector, useAppDispatch } from '../store/hooks'
import { closeTab, activateTab } from '../store/slices/tabsSlice'
import NavOverlay from './NavOverlay'
const ANTLER_PATHS = [
'M14 10c2 4 1 8-1 11 3-1 7 0 10 3 1-4 4-7 8-7-3 3-4 7-3 11l4 1c-1 3 0 6 3 8-3 0-6 1-8 4-1-3-4-5-8-5 2-3 2-7 0-10-3 1-7 0-10-3 3 0 5-2 6-5l-1-8z',
'M48 14c-2 3-2 6-1 9-2-2-5-2-8-1 1 3 1 6-1 9 3 0 5 2 6 5 1-3 4-5 7-5-2-3-2-6 0-9 2 1 5 1 7-1-2 0-4-1-5-3-1-2-3-4-5-4z',
'M28 38c2 3 5 5 9 5 1 4 4 7 8 8-3 2-5 5-5 9-3-2-7-3-11-2 1-3 1-7-1-10-3 0-6-1-8-4 3-1 6-3 8-6z',
]
export default function AppShell() {
const dispatch = useAppDispatch()
const navigate = useNavigate()
const { tabs, activeId } = useAppSelector(s => s.tabs)
const userInfo = useAppSelector(s => s.auth.userInfo)
const [navOpen, setNavOpen] = useState(false)
function handleTabClick(id: string, path: string) {
dispatch(activateTab(id))
navigate(path)
}
function handleTabClose(e: React.MouseEvent, id: string) {
e.stopPropagation()
const idx = tabs.findIndex(t => t.id === id)
dispatch(closeTab(id))
const remaining = tabs.filter(t => t.id !== id)
const newIdx = Math.min(idx, remaining.length - 1)
if (remaining[newIdx]) navigate(remaining[newIdx].path)
}
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden' }}>
{/* Topbar */}
<div style={{ display: 'flex', alignItems: 'stretch', height: 44, background: 'var(--color-topbar-bg)', color: '#fff', position: 'relative', zIndex: 30, flexShrink: 0 }}>
{/* Logo */}
<div style={{ width: 54, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg viewBox="0 0 64 64" width={30} height={30} fill="#0e1216">
{ANTLER_PATHS.map((d, i) => <path key={i} d={d} />)}
</svg>
</div>
{/* Nav toggle */}
<button
aria-label="全部导航"
onClick={() => setNavOpen(v => !v)}
style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '0 18px', color: '#fff', cursor: 'pointer', fontSize: 14, border: 'none', background: navOpen ? 'var(--color-primary)' : 'transparent', height: '100%', fontFamily: 'inherit' }}
>
<svg width={18} height={18} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<line x1={4} y1={7} x2={20} y2={7} /><line x1={4} y1={12} x2={20} y2={12} /><line x1={4} y1={17} x2={20} y2={17} />
</svg>
全部导航
</button>
{/* Tab list */}
<div style={{ display: 'flex', alignItems: 'stretch', flex: 1 }}>
{tabs.map(tab => (
<div
key={tab.id}
role="tab"
aria-selected={tab.id === activeId}
onClick={() => handleTabClick(tab.id, tab.path)}
style={{
display: 'flex', alignItems: 'center', gap: 8, padding: '0 18px', cursor: 'pointer', fontSize: 14, height: '100%',
color: tab.id === activeId ? 'var(--color-tab-active)' : 'var(--color-tab-text)',
borderBottom: tab.id === activeId ? '2px solid var(--color-tab-active)' : 'none',
}}
>
{tab.title}
{tab.closable && (
<button
aria-label={`关闭 ${tab.title}`}
onClick={e => handleTabClose(e, tab.id)}
style={{ marginLeft: 6, width: 14, height: 14, borderRadius: '50%', border: 'none', background: 'transparent', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontSize: 11, color: 'var(--color-tab-text)', cursor: 'pointer', padding: 0 }}
>
✕
</button>
)}
</div>
))}
</div>
{/* User info */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, paddingRight: 14, fontSize: 14 }}>
{userInfo?.username}({userInfo?.userType})
</div>
</div>
{/* Stage */}
<div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
{navOpen && <NavOverlay onClose={() => setNavOpen(false)} />}
<div style={{ position: 'absolute', inset: 0, overflow: 'auto' }}>
<Outlet />
</div>
</div>
</div>
)
}