chore: happy 出口通初始提交(前端 React + 后端 Express,采购管理已原生化)
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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 toggleGroup = React.useCallback((id) => {
|
||||
setCollapsedGroups(current => current.includes(id) ? current.filter(x => x !== id) : [...current, id])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<div className="app-container">
|
||||
<aside inert={settingsOpen || assistantOpen ? "" : undefined} className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
|
||||
<div className="sidebar-header">
|
||||
<h1>{collapsed ? 'HET' : 'happy出口通'}</h1>
|
||||
<button
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="collapse-btn"
|
||||
aria-label={collapsed ? '展开导航' : '收起导航'}
|
||||
title={collapsed ? '展开导航' : '收起导航'}
|
||||
>
|
||||
<SidebarLayoutIcon size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="sidebar-nav">
|
||||
{/* 工作台:独立置顶,不属于任何分组 */}
|
||||
<NavLink aria-label="工作台" title="工作台" to="/" end className={navClass}>
|
||||
<span className="icon"><DashboardIcon size={18} /></span>
|
||||
{!collapsed && <span>工作台</span>}
|
||||
</NavLink>
|
||||
|
||||
{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 inert={settingsOpen || assistantOpen ? "" : undefined} className="main-content">
|
||||
<GlobalHeader onOpenAssistant={() => setAssistantOpen(true)} />
|
||||
<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>
|
||||
|
||||
<AIAssistant isOpen={assistantOpen} onClose={closeAssistant} />
|
||||
<Settings isOpen={settingsOpen} onClose={closeSettings} theme={theme} onThemeChange={setTheme} reducedMotion={reducedMotion} onReducedMotionChange={setReducedMotion} />
|
||||
</div>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user