415 lines
10 KiB
TypeScript
415 lines
10 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|||
|
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
|
||
|
|
import {
|
||
|
|
Layout,
|
||
|
|
Menu,
|
||
|
|
Button,
|
||
|
|
Avatar,
|
||
|
|
Dropdown,
|
||
|
|
Typography,
|
||
|
|
Space,
|
||
|
|
Badge,
|
||
|
|
Drawer,
|
||
|
|
Modal,
|
||
|
|
theme
|
||
|
|
} from 'antd'
|
||
|
|
import {
|
||
|
|
DashboardOutlined,
|
||
|
|
ProjectOutlined,
|
||
|
|
DollarOutlined,
|
||
|
|
FileTextOutlined,
|
||
|
|
BarChartOutlined,
|
||
|
|
UserOutlined,
|
||
|
|
LogoutOutlined,
|
||
|
|
SettingOutlined,
|
||
|
|
BellOutlined,
|
||
|
|
MenuFoldOutlined,
|
||
|
|
MenuUnfoldOutlined,
|
||
|
|
CalculatorOutlined,
|
||
|
|
ToolOutlined,
|
||
|
|
WalletOutlined,
|
||
|
|
MoneyCollectOutlined,
|
||
|
|
AuditOutlined,
|
||
|
|
FileSearchOutlined,
|
||
|
|
ShoppingCartOutlined,
|
||
|
|
TeamOutlined,
|
||
|
|
ShopOutlined,
|
||
|
|
SolutionOutlined,
|
||
|
|
HomeOutlined,
|
||
|
|
SafetyOutlined,
|
||
|
|
FileDoneOutlined,
|
||
|
|
AppstoreOutlined,
|
||
|
|
CheckCircleOutlined
|
||
|
|
} from '@ant-design/icons'
|
||
|
|
import { useAuthStore } from '../../store/authStore'
|
||
|
|
import { useLanguageStore } from '../../store/languageStore'
|
||
|
|
import CompanyLogo from '../common/CompanyLogo'
|
||
|
|
import LanguageSelector from '../common/LanguageSelector'
|
||
|
|
|
||
|
|
const { Header, Sider, Content } = Layout
|
||
|
|
const { Text } = Typography
|
||
|
|
|
||
|
|
const MainLayout: React.FC = () => {
|
||
|
|
const navigate = useNavigate()
|
||
|
|
const location = useLocation()
|
||
|
|
const [collapsed, setCollapsed] = useState(false)
|
||
|
|
const [isMobile, setIsMobile] = useState(false)
|
||
|
|
const [mobileMenuVisible, setMobileMenuVisible] = useState(false)
|
||
|
|
const [settingsVisible, setSettingsVisible] = useState(false)
|
||
|
|
const { user, logout } = useAuthStore()
|
||
|
|
const { t } = useLanguageStore()
|
||
|
|
|
||
|
|
const {
|
||
|
|
token: { colorBgContainer, borderRadiusLG },
|
||
|
|
} = theme.useToken()
|
||
|
|
|
||
|
|
// 检测屏幕尺寸
|
||
|
|
useEffect(() => {
|
||
|
|
const checkMobile = () => {
|
||
|
|
const mobile = window.innerWidth <= 768
|
||
|
|
setIsMobile(mobile)
|
||
|
|
if (mobile) {
|
||
|
|
setCollapsed(true)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
checkMobile()
|
||
|
|
window.addEventListener('resize', checkMobile)
|
||
|
|
return () => window.removeEventListener('resize', checkMobile)
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
// 完整菜单项
|
||
|
|
const menuItems = [
|
||
|
|
{
|
||
|
|
key: '/dashboard',
|
||
|
|
icon: <DashboardOutlined />,
|
||
|
|
label: '工作台'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: '/projects',
|
||
|
|
icon: <ProjectOutlined />,
|
||
|
|
label: '项目管理'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: '/budget-projects',
|
||
|
|
icon: <CalculatorOutlined />,
|
||
|
|
label: '预算报价'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: '/construction',
|
||
|
|
icon: <ToolOutlined />,
|
||
|
|
label: '施工管理'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'approval',
|
||
|
|
icon: <SolutionOutlined />,
|
||
|
|
label: '审批管理',
|
||
|
|
children: [
|
||
|
|
{
|
||
|
|
key: '/approval',
|
||
|
|
icon: <CheckCircleOutlined />,
|
||
|
|
label: '待审批'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: '/execution',
|
||
|
|
icon: <DollarOutlined />,
|
||
|
|
label: '待执行'
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'finance-docs',
|
||
|
|
icon: <FileDoneOutlined />,
|
||
|
|
label: '财务申请',
|
||
|
|
children: [
|
||
|
|
{
|
||
|
|
key: '/advances',
|
||
|
|
icon: <WalletOutlined />,
|
||
|
|
label: '预支申请'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: '/reimbursements',
|
||
|
|
icon: <FileTextOutlined />,
|
||
|
|
label: '报销申请'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: '/payment-requests',
|
||
|
|
icon: <MoneyCollectOutlined />,
|
||
|
|
label: '付款申请'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: '/verification',
|
||
|
|
icon: <AuditOutlined />,
|
||
|
|
label: '核销申请'
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'finance-group',
|
||
|
|
icon: <BarChartOutlined />,
|
||
|
|
label: '财务管理',
|
||
|
|
children: [
|
||
|
|
{
|
||
|
|
key: '/finance',
|
||
|
|
label: '财务概览'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: '/exchange-rates',
|
||
|
|
icon: <DollarOutlined />,
|
||
|
|
label: '汇率管理'
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: '/reports',
|
||
|
|
icon: <FileSearchOutlined />,
|
||
|
|
label: '报表分析'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'procurement',
|
||
|
|
icon: <ShoppingCartOutlined />,
|
||
|
|
label: '采购管理',
|
||
|
|
children: [
|
||
|
|
{
|
||
|
|
key: '/products',
|
||
|
|
icon: <AppstoreOutlined />,
|
||
|
|
label: '商品管理'
|
||
|
|
}
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'partners',
|
||
|
|
icon: <TeamOutlined />,
|
||
|
|
label: '合作伙伴',
|
||
|
|
children: [
|
||
|
|
{
|
||
|
|
key: '/suppliers',
|
||
|
|
icon: <ShopOutlined />,
|
||
|
|
label: '供应商管理'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: '/subcontractors',
|
||
|
|
icon: <SolutionOutlined />,
|
||
|
|
label: '分包商管理'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: '/customers',
|
||
|
|
icon: <HomeOutlined />,
|
||
|
|
label: '客户管理'
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
// 用户下拉菜单
|
||
|
|
const userMenuItems = [
|
||
|
|
{
|
||
|
|
key: 'profile',
|
||
|
|
icon: <UserOutlined />,
|
||
|
|
label: '个人信息'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'settings',
|
||
|
|
icon: <SettingOutlined />,
|
||
|
|
label: '系统设置'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
type: 'divider' as const
|
||
|
|
},
|
||
|
|
{
|
||
|
|
key: 'logout',
|
||
|
|
icon: <LogoutOutlined />,
|
||
|
|
label: '退出登录'
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
// 处理菜单点击
|
||
|
|
const handleMenuClick = ({ key }: { key: string }) => {
|
||
|
|
if (key === 'logout') {
|
||
|
|
logout()
|
||
|
|
navigate('/login')
|
||
|
|
} else if (key === 'settings') {
|
||
|
|
setSettingsVisible(true)
|
||
|
|
} else if (key.startsWith('/')) {
|
||
|
|
navigate(key)
|
||
|
|
if (isMobile) {
|
||
|
|
setMobileMenuVisible(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取当前选中的菜单项
|
||
|
|
const getSelectedKey = () => {
|
||
|
|
return location.pathname
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取当前展开的菜单项
|
||
|
|
const getOpenKeys = () => {
|
||
|
|
const path = location.pathname
|
||
|
|
if (path.startsWith('/suppliers') ||
|
||
|
|
path.startsWith('/subcontractors') ||
|
||
|
|
path.startsWith('/customers')) {
|
||
|
|
return ['partners']
|
||
|
|
}
|
||
|
|
if (path.startsWith('/advances') ||
|
||
|
|
path.startsWith('/reimbursements') ||
|
||
|
|
path.startsWith('/payment-requests') ||
|
||
|
|
path.startsWith('/verification')) {
|
||
|
|
return ['finance-docs']
|
||
|
|
}
|
||
|
|
if (path.startsWith('/approval') || path.startsWith('/execution')) {
|
||
|
|
return ['approval']
|
||
|
|
}
|
||
|
|
if (path.startsWith('/products')) {
|
||
|
|
return ['procurement']
|
||
|
|
}
|
||
|
|
return []
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Layout style={{ minHeight: '100vh' }}>
|
||
|
|
{/* 桌面端侧边栏 */}
|
||
|
|
{!isMobile && (
|
||
|
|
<Sider
|
||
|
|
trigger={null}
|
||
|
|
collapsible
|
||
|
|
collapsed={collapsed}
|
||
|
|
style={{
|
||
|
|
overflow: 'auto',
|
||
|
|
height: '100vh',
|
||
|
|
position: 'fixed',
|
||
|
|
left: 0,
|
||
|
|
top: 0,
|
||
|
|
bottom: 0,
|
||
|
|
background: colorBgContainer,
|
||
|
|
borderRight: '1px solid #f0f0f0'
|
||
|
|
}}
|
||
|
|
width={220}
|
||
|
|
collapsedWidth={80}
|
||
|
|
>
|
||
|
|
{/* Logo */}
|
||
|
|
<div style={{
|
||
|
|
height: 64,
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: collapsed ? 'center' : 'flex-start',
|
||
|
|
padding: collapsed ? 0 : '0 20px',
|
||
|
|
borderBottom: '1px solid #f0f0f0'
|
||
|
|
}}>
|
||
|
|
<CompanyLogo collapsed={collapsed} />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 菜单 */}
|
||
|
|
<Menu
|
||
|
|
mode="inline"
|
||
|
|
selectedKeys={[getSelectedKey()]}
|
||
|
|
defaultOpenKeys={getOpenKeys()}
|
||
|
|
items={menuItems}
|
||
|
|
onClick={handleMenuClick}
|
||
|
|
style={{ borderRight: 0 }}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{/* 折叠按钮 */}
|
||
|
|
<div style={{
|
||
|
|
position: 'absolute',
|
||
|
|
bottom: 0,
|
||
|
|
width: '100%',
|
||
|
|
padding: 16,
|
||
|
|
borderTop: '1px solid #f0f0f0'
|
||
|
|
}}>
|
||
|
|
<Button
|
||
|
|
type="text"
|
||
|
|
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||
|
|
onClick={() => setCollapsed(!collapsed)}
|
||
|
|
style={{ width: '100%' }}
|
||
|
|
>
|
||
|
|
{!collapsed && '收起菜单'}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</Sider>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 移动端抽屉菜单 */}
|
||
|
|
{isMobile && (
|
||
|
|
<Drawer
|
||
|
|
placement="left"
|
||
|
|
onClose={() => setMobileMenuVisible(false)}
|
||
|
|
open={mobileMenuVisible}
|
||
|
|
width={280}
|
||
|
|
styles={{ body: { padding: 0 } }}
|
||
|
|
>
|
||
|
|
<div style={{ height: 64, padding: '0 20px', display: 'flex', alignItems: 'center', borderBottom: '1px solid #f0f0f0' }}>
|
||
|
|
<CompanyLogo collapsed={false} />
|
||
|
|
</div>
|
||
|
|
<Menu
|
||
|
|
mode="inline"
|
||
|
|
selectedKeys={[getSelectedKey()]}
|
||
|
|
defaultOpenKeys={getOpenKeys()}
|
||
|
|
items={menuItems}
|
||
|
|
onClick={handleMenuClick}
|
||
|
|
style={{ borderRight: 0 }}
|
||
|
|
/>
|
||
|
|
</Drawer>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<Layout style={{ marginLeft: isMobile ? 0 : (collapsed ? 80 : 220), transition: 'margin-left 0.2s' }}>
|
||
|
|
{/* 顶部导航 */}
|
||
|
|
<Header style={{
|
||
|
|
padding: '0 24px',
|
||
|
|
background: colorBgContainer,
|
||
|
|
position: 'sticky',
|
||
|
|
top: 0,
|
||
|
|
zIndex: 1,
|
||
|
|
borderBottom: '1px solid #f0f0f0',
|
||
|
|
display: 'flex',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'space-between'
|
||
|
|
}}>
|
||
|
|
{/* 移动端菜单按钮 */}
|
||
|
|
{isMobile && (
|
||
|
|
<Button
|
||
|
|
type="text"
|
||
|
|
icon={<MenuFoldOutlined />}
|
||
|
|
onClick={() => setMobileMenuVisible(true)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div style={{ flex: 1 }} />
|
||
|
|
|
||
|
|
<Space size="middle">
|
||
|
|
<LanguageSelector />
|
||
|
|
|
||
|
|
<Dropdown menu={{ items: userMenuItems, onClick: handleMenuClick }} placement="bottomRight">
|
||
|
|
<Space style={{ cursor: 'pointer' }}>
|
||
|
|
<Avatar icon={<UserOutlined />} style={{ backgroundColor: '#1890ff' }} />
|
||
|
|
{!isMobile && <Text>{user?.name || user?.username || '用户'}</Text>}
|
||
|
|
</Space>
|
||
|
|
</Dropdown>
|
||
|
|
</Space>
|
||
|
|
</Header>
|
||
|
|
|
||
|
|
{/* 内容区域 */}
|
||
|
|
<Content style={{
|
||
|
|
margin: 0,
|
||
|
|
minHeight: 280,
|
||
|
|
background: '#f5f5f5'
|
||
|
|
}}>
|
||
|
|
<Outlet />
|
||
|
|
</Content>
|
||
|
|
</Layout>
|
||
|
|
|
||
|
|
{/* 设置弹窗 */}
|
||
|
|
<Modal
|
||
|
|
title="系统设置"
|
||
|
|
open={settingsVisible}
|
||
|
|
onCancel={() => setSettingsVisible(false)}
|
||
|
|
footer={null}
|
||
|
|
>
|
||
|
|
<p>系统设置功能开发中...</p>
|
||
|
|
</Modal>
|
||
|
|
</Layout>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default MainLayout
|