AppShell.tsx 4.35 KB
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>
  )
}