first commit

This commit is contained in:
mei
2026-08-10 22:56:38 +08:00
commit 2086bd79cc
32 changed files with 14110 additions and 0 deletions
+252
View File
@@ -0,0 +1,252 @@
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