Files
happy-export/frontend/src/App.jsx
T
Drsakura d650ff9c4d refactor(layout): 顶栏横跨整宽并压在侧边栏之上,侧栏去标题、按钮移到工作台右侧
- App.jsx: 新增 .app-shell / .app-body 两层结构,GlobalHeader 提到侧边栏之前;
  inert 上移到 .app-shell,行为不变
- 侧边栏删除带 happy出口通 的 .sidebar-header(logo 与名字统一交给顶栏,且更大)
- 折叠/展开按钮从侧栏标题栏移到「工作台」右侧(.sidebar-top),收起时自动落到工作台下方
- 新增 --header-height 变量(80px,≤680px 时 62px),顶栏 min-height 与侧栏 sticky top/
  height、route-content min-height 全部据此计算
- 移除已失效的 .sidebar-header 规则(含 ≤900px 媒体查询)
2026-09-10 17:04:05 +08:00

205 lines
8.6 KiB
React

import React from 'react'
import { BrowserRouter as Router, Routes, Route, NavLink, Navigate } from 'react-router-dom'
import Dashboard from './pages/Dashboard'
import EmbeddedSku from './pages/EmbeddedSku'
import ProductManagement from './pages/ProductManagement'
import SupplierManagement from './pages/SupplierManagement'
import CustomerManagement from './pages/CustomerManagement'
import OrderManagement from './pages/OrderManagement'
import ProspectManagement from './pages/ProspectManagement'
import QuoteManagement from './pages/QuoteManagement'
import PiContractManagement from './pages/PiContractManagement'
import PurchaseOrderManagement from './pages/PurchaseOrderManagement'
import Settings from './components/Settings'
import GlobalHeader from './components/GlobalHeader'
import AIAssistant from './components/AIAssistant'
import { readPreference } from './preferences'
import {
DashboardIcon,
FactoryIcon,
PackageIcon,
UsersIcon,
TargetIcon,
ClipboardIcon,
DownloadIcon,
QuoteIcon,
ContractIcon,
PurchaseIcon,
ChevronDownIcon,
TuneIcon,
SunIcon,
MoonIcon,
SidebarLayoutIcon
} from './components/Icons'
/* 侧边栏分组配置:工作台独立置顶,不归属任何分组 */
const NAV_GROUPS = [
{
id: 'business',
title: '业务管理',
items: [
{ to: '/quotes', label: '客户报价单管理', icon: QuoteIcon },
{ to: '/pi-contracts', label: 'PI合同管理', icon: ContractIcon },
{ to: '/purchase-orders', label: '采购单管理', icon: PurchaseIcon }
]
},
{
id: 'customer',
title: '客户管理',
items: [
{ to: '/customers', label: '客户管理', icon: UsersIcon },
{ to: '/prospects', label: '新客开发', icon: TargetIcon },
{ to: '/orders', label: '订单管理', icon: ClipboardIcon }
]
},
{
id: 'procurement',
title: '采购管理',
items: [
{ to: '/products', label: '产品管理', icon: PackageIcon },
{ to: '/suppliers', label: '供应商管理', icon: FactoryIcon },
{ to: '/procurement/import', label: '合同导入', icon: DownloadIcon }
]
}
]
function App() {
const [collapsed, setCollapsed] = React.useState(false)
const [collapsedGroups, setCollapsedGroups] = React.useState([])
const [settingsOpen, setSettingsOpen] = React.useState(false)
const [assistantOpen, setAssistantOpen] = React.useState(false)
const [theme, setTheme] = React.useState(() => readPreference('tms-theme', ['dark', 'light'], 'dark'))
const [reducedMotion, setReducedMotion] = React.useState(() => readPreference('tms-motion', ['full', 'reduced'], 'full') === 'reduced')
const closeSettings = React.useCallback(() => setSettingsOpen(false), [])
const closeAssistant = React.useCallback(() => setAssistantOpen(false), [])
React.useEffect(() => {
document.documentElement.dataset.theme = theme
document.documentElement.style.colorScheme = theme
try { localStorage.setItem('tms-theme', theme) } catch { /* Session-only when storage is unavailable. */ }
}, [theme])
React.useEffect(() => {
document.documentElement.dataset.motion = reducedMotion ? 'reduced' : 'full'
try { localStorage.setItem('tms-motion', reducedMotion ? 'reduced' : 'full') } catch { /* Session-only. */ }
}, [reducedMotion])
const navClass = ({ isActive }) => `nav-item${isActive ? ' active' : ''}`
/* 工作台额外带一个类,供「展开/收起导航按钮挂在它右侧」定位 */
const homeNavClass = ({ isActive }) => `nav-item nav-item-home${isActive ? ' active' : ''}`
const toggleGroup = React.useCallback((id) => {
setCollapsedGroups(current => current.includes(id) ? current.filter(x => x !== id) : [...current, id])
}, [])
return (
<Router>
<div className="app-container">
<div className="app-shell" inert={settingsOpen || assistantOpen ? "" : undefined}>
{/* 顶栏横跨整宽,压在侧边栏之上 */}
<GlobalHeader onOpenAssistant={() => setAssistantOpen(true)} />
<div className="app-body">
<aside className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
{/* 工作台置顶;展开/收起导航按钮挂在它右侧 */}
<div className="sidebar-top">
<NavLink aria-label="工作台" title="工作台" to="/" end className={homeNavClass}>
<span className="icon"><DashboardIcon size={18} /></span>
{!collapsed && <span>工作台</span>}
</NavLink>
<button
onClick={() => setCollapsed(!collapsed)}
className="collapse-btn"
aria-label={collapsed ? '展开导航' : '收起导航'}
title={collapsed ? '展开导航' : '收起导航'}
>
<SidebarLayoutIcon size={18} />
</button>
</div>
<nav className="sidebar-nav">
{NAV_GROUPS.map(group => {
/* 侧栏整体收起时忽略分组的折叠状态,保证图标仍可点 */
const open = collapsed || !collapsedGroups.includes(group.id)
return (
<div className="nav-group" key={group.id}>
<div className="nav-group-header">
<span className="nav-group-title">{group.title}</span>
{!collapsed && (
<button
type="button"
className={`nav-group-toggle${open ? ' open' : ''}`}
onClick={() => toggleGroup(group.id)}
aria-expanded={open}
aria-label={`${open ? '收起' : '展开'}${group.title}`}
title={`${open ? '收起' : '展开'}${group.title}`}
>
<ChevronDownIcon size={14} />
</button>
)}
</div>
{open && group.items.map(item => {
const ItemIcon = item.icon
return (
<NavLink key={item.to} aria-label={item.label} title={item.label} to={item.to} className={navClass}>
<span className="icon"><ItemIcon size={18} /></span>
{!collapsed && <span>{item.label}</span>}
</NavLink>
)
})}
</div>
)
})}
</nav>
<div className="sidebar-footer">
<div className="user-info">管理员</div>
<button
className="theme-toggle-btn"
onClick={() => setTheme(current => current === 'light' ? 'dark' : 'light')}
title={theme === 'light' ? '切换至夜间模式' : '切换至日间模式'}
aria-label={theme === 'light' ? '切换至夜间模式' : '切换至日间模式'}
>
{theme === 'light' ? <MoonIcon size={18} /> : <SunIcon size={18} />}
</button>
<button className="settings-btn" onClick={() => setSettingsOpen(true)} title="设置" aria-label="设置">
<TuneIcon size={18} />
</button>
</div>
</aside>
<main className="main-content">
<div className="route-content">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/quotes" element={<QuoteManagement />} />
<Route path="/pi-contracts" element={<PiContractManagement />} />
<Route path="/purchase-orders" element={<PurchaseOrderManagement />} />
<Route path="/products" element={<ProductManagement />} />
<Route path="/suppliers" element={<SupplierManagement />} />
<Route path="/procurement/import" element={<EmbeddedSku view="import" />} />
<Route path="/customers" element={<CustomerManagement />} />
<Route path="/orders" element={<OrderManagement />} />
<Route path="/prospects" element={<ProspectManagement />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</div>
</main>
</div>
</div>
<AIAssistant isOpen={assistantOpen} onClose={closeAssistant} />
<Settings isOpen={settingsOpen} onClose={closeSettings} theme={theme} onThemeChange={setTheme} reducedMotion={reducedMotion} onReducedMotionChange={setReducedMotion} />
</div>
</Router>
)
}
export default App