253 lines
8.6 KiB
React
253 lines
8.6 KiB
React
import React, { useState, useEffect } from 'react'
|
||||
|
|
import { Layout, Menu, theme, Button, Space, Dropdown, message, Drawer, Avatar } from 'antd'
|
|||
|
|
import {
|
|||
|
|
DashboardOutlined,
|
|||
|
|
DesktopOutlined,
|
|||
|
|
TeamOutlined,
|
|||
|
|
BellOutlined,
|
|||
|
|
InfoCircleOutlined,
|
|||
|
|
BookOutlined,
|
|||
|
|
MenuOutlined,
|
|||
|
|
LogoutOutlined,
|
|||
|
|
UserOutlined,
|
|||
|
|
DownOutlined
|
|||
|
|
} from '@ant-design/icons'
|
|||
|
|
import axios from 'axios'
|
|||
|
|
import Login from './pages/Login'
|
|||
|
|
import Weather from './components/Weather'
|
|||
|
|
import Dashboard from './pages/Dashboard'
|
|||
|
|
import DeviceManagement from './pages/DeviceManagement'
|
|||
|
|
import PersonnelManagement from './pages/PersonnelManagement'
|
|||
|
|
import NotificationManagement from './pages/NotificationManagement'
|
|||
|
|
import About from './pages/About'
|
|||
|
|
import Guide from './pages/Guide'
|
|||
|
|
|
|||
|
|
const { Header, Sider, Content } = Layout
|
|||
|
|
|
|||
|
|
// 移动端检测 Hook
|
|||
|
|
function useIsMobile() {
|
|||
|
|
const [isMobile, setIsMobile] = useState(window.innerWidth < 768)
|
|||
|
|
useEffect(() => {
|
|||
|
|
const onResize = () => setIsMobile(window.innerWidth < 768)
|
|||
|
|
window.addEventListener('resize', onResize)
|
|||
|
|
return () => window.removeEventListener('resize', onResize)
|
|||
|
|
}, [])
|
|||
|
|
return isMobile
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function App() {
|
|||
|
|
const isMobile = useIsMobile()
|
|||
|
|
const [collapsed, setCollapsed] = useState(false)
|
|||
|
|
const [drawerVisible, setDrawerVisible] = useState(false)
|
|||
|
|
const [currentPage, setCurrentPage] = useState('dashboard')
|
|||
|
|
const [user, setUser] = useState(null)
|
|||
|
|
const [checkingAuth, setCheckingAuth] = useState(true)
|
|||
|
|
|
|||
|
|
const {
|
|||
|
|
token: { colorBgContainer, borderRadiusLG }
|
|||
|
|
} = theme.useToken()
|
|||
|
|
|
|||
|
|
// 初始化时恢复登录态
|
|||
|
|
useEffect(() => {
|
|||
|
|
const token = localStorage.getItem('token')
|
|||
|
|
const savedUser = localStorage.getItem('user')
|
|||
|
|
if (token && savedUser) {
|
|||
|
|
axios.get('/api/auth/me')
|
|||
|
|
.then(res => {
|
|||
|
|
if (res.data.success) setUser(res.data.data)
|
|||
|
|
else handleLogout(false)
|
|||
|
|
})
|
|||
|
|
.catch(() => handleLogout(false))
|
|||
|
|
.finally(() => setCheckingAuth(false))
|
|||
|
|
} else {
|
|||
|
|
setCheckingAuth(false)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听登录过期
|
|||
|
|
const onExpired = () => handleLogout(false)
|
|||
|
|
window.addEventListener('auth-expired', onExpired)
|
|||
|
|
return () => window.removeEventListener('auth-expired', onExpired)
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
const handleLogin = (userData) => {
|
|||
|
|
setUser(userData)
|
|||
|
|
setCurrentPage('dashboard')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleLogout = async (callApi = true) => {
|
|||
|
|
if (callApi) {
|
|||
|
|
try { await axios.post('/api/auth/logout') } catch (err) { /* ignore */ }
|
|||
|
|
}
|
|||
|
|
localStorage.removeItem('token')
|
|||
|
|
localStorage.removeItem('user')
|
|||
|
|
setUser(null)
|
|||
|
|
message.success('已安全退出')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getGreeting = () => {
|
|||
|
|
const hour = new Date().getHours()
|
|||
|
|
if (hour < 6) return '晚上好'
|
|||
|
|
if (hour < 9) return '早上好'
|
|||
|
|
if (hour < 12) return '上午好'
|
|||
|
|
if (hour < 14) return '中午好'
|
|||
|
|
if (hour < 18) return '下午好'
|
|||
|
|
return '晚上好'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const menuItems = [
|
|||
|
|
{ key: 'dashboard', icon: <DashboardOutlined />, label: '系统首页' },
|
|||
|
|
{ key: 'about', icon: <InfoCircleOutlined />, label: '软件简介' },
|
|||
|
|
{ key: 'guide', icon: <BookOutlined />, label: '使用说明' },
|
|||
|
|
{ type: 'divider' },
|
|||
|
|
{ key: 'devices', icon: <DesktopOutlined />, label: '设备管理' },
|
|||
|
|
{ key: 'personnel', icon: <TeamOutlined />, label: '人员管理' },
|
|||
|
|
{ key: 'notifications', icon: <BellOutlined />, label: '通知管理' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
// 移动端底部导航(4个核心入口)
|
|||
|
|
const bottomNavItems = [
|
|||
|
|
{ key: 'dashboard', icon: <DashboardOutlined />, label: '首页' },
|
|||
|
|
{ key: 'devices', icon: <DesktopOutlined />, label: '设备' },
|
|||
|
|
{ key: 'personnel', icon: <TeamOutlined />, label: '人员' },
|
|||
|
|
{ key: 'notifications', icon: <BellOutlined />, label: '通知' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const renderPage = () => {
|
|||
|
|
switch (currentPage) {
|
|||
|
|
case 'dashboard': return <Dashboard onNavigate={setCurrentPage} />
|
|||
|
|
case 'about': return <About />
|
|||
|
|
case 'guide': return <Guide />
|
|||
|
|
case 'devices': return <DeviceManagement user={user} />
|
|||
|
|
case 'personnel': return <PersonnelManagement user={user} />
|
|||
|
|
case 'notifications': return <NotificationManagement user={user} />
|
|||
|
|
default: return <Dashboard onNavigate={setCurrentPage} />
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getPageTitle = () => {
|
|||
|
|
const titles = {
|
|||
|
|
dashboard: '系统首页', about: '软件简介', guide: '使用说明',
|
|||
|
|
devices: '设备管理', personnel: '人员管理', notifications: '通知管理'
|
|||
|
|
}
|
|||
|
|
return titles[currentPage] || '系统首页'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const userMenuItems = [
|
|||
|
|
{ key: 'role', label: `角色:${user?.role === 'admin' ? '管理员' : '普通用户'}`, icon: <UserOutlined /> },
|
|||
|
|
{ type: 'divider' },
|
|||
|
|
{ key: 'logout', label: '退出登录', icon: <LogoutOutlined />, danger: true, onClick: () => handleLogout() }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
if (checkingAuth) {
|
|||
|
|
return <div style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>加载中...</div>
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!user) {
|
|||
|
|
return <Login onLogin={handleLogin} />
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const menuContent = (
|
|||
|
|
<>
|
|||
|
|
<div className="logo-area">
|
|||
|
|
<img src="/assets/logo-white.png" alt="影视星河" className="logo-img" />
|
|||
|
|
</div>
|
|||
|
|
<Menu
|
|||
|
|
theme="dark"
|
|||
|
|
mode="inline"
|
|||
|
|
selectedKeys={[currentPage]}
|
|||
|
|
items={menuItems}
|
|||
|
|
onClick={({ key }) => { setCurrentPage(key); setDrawerVisible(false) }}
|
|||
|
|
/>
|
|||
|
|
</>
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// ============ 移动端布局 ============
|
|||
|
|
if (isMobile) {
|
|||
|
|
return (
|
|||
|
|
<Layout style={{ minHeight: '100vh' }}>
|
|||
|
|
<Header className="mobile-header">
|
|||
|
|
<Button type="text" icon={<MenuOutlined />} onClick={() => setDrawerVisible(true)} style={{ color: '#fff' }} />
|
|||
|
|
<span className="mobile-title">{getPageTitle()}</span>
|
|||
|
|
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight" trigger={['click']}>
|
|||
|
|
<Avatar style={{ backgroundColor: '#7c3aed' }} icon={<UserOutlined />} />
|
|||
|
|
</Dropdown>
|
|||
|
|
</Header>
|
|||
|
|
|
|||
|
|
<div className="mobile-info-bar">
|
|||
|
|
<span className="mobile-greeting">{user.display_name || user.username},{getGreeting()}!</span>
|
|||
|
|
<Weather />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Drawer
|
|||
|
|
placement="left"
|
|||
|
|
open={drawerVisible}
|
|||
|
|
onClose={() => setDrawerVisible(false)}
|
|||
|
|
width={260}
|
|||
|
|
styles={{ body: { padding: 0, background: '#001529' }, header: { display: 'none' } }}
|
|||
|
|
>
|
|||
|
|
<div style={{ background: '#001529', height: '100%' }}>{menuContent}</div>
|
|||
|
|
</Drawer>
|
|||
|
|
|
|||
|
|
<Content className="mobile-content">
|
|||
|
|
{renderPage()}
|
|||
|
|
</Content>
|
|||
|
|
|
|||
|
|
<nav className="bottom-nav">
|
|||
|
|
{bottomNavItems.map(item => (
|
|||
|
|
<button
|
|||
|
|
key={item.key}
|
|||
|
|
className={`bottom-nav-item ${currentPage === item.key ? 'active' : ''}`}
|
|||
|
|
onClick={() => setCurrentPage(item.key)}
|
|||
|
|
>
|
|||
|
|
{item.icon}
|
|||
|
|
<span>{item.label}</span>
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</nav>
|
|||
|
|
</Layout>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============ 桌面端布局 ============
|
|||
|
|
return (
|
|||
|
|
<Layout style={{ minHeight: '100vh' }}>
|
|||
|
|
<Sider trigger={null} collapsible collapsed={collapsed} theme="dark">
|
|||
|
|
<div className="logo-area">
|
|||
|
|
{collapsed ? (
|
|||
|
|
<img src="/assets/logo-icon.png" alt="logo" className="logo-icon" />
|
|||
|
|
) : (
|
|||
|
|
<img src="/assets/logo-white.png" alt="影视星河" className="logo-img" />
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<Menu
|
|||
|
|
theme="dark"
|
|||
|
|
mode="inline"
|
|||
|
|
selectedKeys={[currentPage]}
|
|||
|
|
items={menuItems}
|
|||
|
|
onClick={({ key }) => setCurrentPage(key)}
|
|||
|
|
/>
|
|||
|
|
</Sider>
|
|||
|
|
<Layout>
|
|||
|
|
<Header style={{ padding: '0 24px', background: colorBgContainer, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}>
|
|||
|
|
<span style={{ fontSize: 18, fontWeight: 500 }}>{getPageTitle()}</span>
|
|||
|
|
<Space size="large" align="center">
|
|||
|
|
<Weather />
|
|||
|
|
<div className="greeting-text">{user.display_name || user.username},{getGreeting()}!</div>
|
|||
|
|
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
|||
|
|
<Button type="text" icon={<UserOutlined />} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|||
|
|
{user.display_name || user.username}
|
|||
|
|
<DownOutlined style={{ fontSize: 10 }} />
|
|||
|
|
</Button>
|
|||
|
|
</Dropdown>
|
|||
|
|
</Space>
|
|||
|
|
</Header>
|
|||
|
|
<Content style={{ margin: 24, padding: 24, background: colorBgContainer, borderRadius: borderRadiusLG, minHeight: 280 }}>
|
|||
|
|
{renderPage()}
|
|||
|
|
</Content>
|
|||
|
|
</Layout>
|
|||
|
|
</Layout>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default App
|