Files
dms/client/src/App.jsx
T
2026-08-11 12:19:20 +08:00

373 lines
13 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react'
import { Layout, Menu, theme, Button, Space, Dropdown, message, Drawer, Avatar, Modal, Form, Input, Badge, Tooltip } from 'antd'
import {
DashboardOutlined,
DesktopOutlined,
TeamOutlined,
BellOutlined,
InfoCircleOutlined,
BookOutlined,
MenuOutlined,
LogoutOutlined,
UserOutlined,
DownOutlined,
UserSwitchOutlined,
KeyOutlined,
MessageOutlined
} 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 AccountManagement from './pages/AccountManagement'
import Messages from './pages/Messages'
import About from './pages/About'
import Guide from './pages/Guide'
import Download from './pages/Download'
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 [pwdModalVisible, setPwdModalVisible] = useState(false)
const [pwdForm] = Form.useForm()
const [unreadTotal, setUnreadTotal] = useState(0)
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)
}, [])
// 轮询消息未读总数(侧边栏徽标)
useEffect(() => {
if (!user) return
const fetchUnread = () => {
axios.get('/api/chat/conversations')
.then(res => {
if (res.data.success) {
setUnreadTotal(res.data.data.reduce((s, c) => s + (c.unread || 0), 0))
}
})
.catch(() => {})
}
fetchUnread()
const timer = setInterval(fetchUnread, 10000)
return () => clearInterval(timer)
}, [user?.id])
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: '系统首页' },
{ type: 'divider' },
{ key: 'devices', icon: <DesktopOutlined />, label: '设备管理' },
{ key: 'personnel', icon: <TeamOutlined />, label: '人员管理' },
...(user?.role === 'admin' ? [{ key: 'accounts', icon: <UserSwitchOutlined />, label: '账户管理' }] : []),
{ key: 'notifications', icon: <BellOutlined />, label: '通知管理' },
{ key: 'download', icon: <InfoCircleOutlined />, label: '软件下载' }
]
// 移动端底部导航(5个核心入口)
const bottomNavItems = [
{ key: 'dashboard', icon: <DashboardOutlined />, label: '首页' },
{ key: 'devices', icon: <DesktopOutlined />, label: '设备' },
{ key: 'personnel', icon: <TeamOutlined />, label: '人员' },
{ key: 'notifications', icon: <BellOutlined />, label: '通知' },
{ key: 'messages', icon: <MessageOutlined />, 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 'accounts': return user?.role === 'admin' ? <AccountManagement user={user} /> : <Dashboard onNavigate={setCurrentPage} />
case 'notifications': return <NotificationManagement user={user} />
case 'messages': return <Messages user={user} />
case 'download': return <Download />
default: return <Dashboard onNavigate={setCurrentPage} />
}
}
const getPageTitle = () => {
const titles = {
dashboard: '系统首页', about: '软件简介', guide: '使用说明',download: '软件下载',
devices: '设备管理', personnel: '人员管理', accounts: '账户管理',
notifications: '通知管理', messages: '消息对话'
}
return titles[currentPage] || '系统首页'
}
// 修改自己的密码(所有用户可用,成功后需用新密码重新登录)
const handleChangePassword = async () => {
try {
const values = await pwdForm.validateFields()
const res = await axios.patch('/api/auth/password', {
oldPassword: values.oldPassword,
newPassword: values.newPassword
})
if (res.data.success) {
setPwdModalVisible(false)
pwdForm.resetFields()
// 后端已使当前会话失效,清除本地登录态回到登录页
localStorage.removeItem('token')
localStorage.removeItem('user')
setUser(null)
message.success(res.data.message)
}
} catch (err) {
if (err.response) message.error(err.response.data?.message || '修改失败')
}
}
const pwdModal = (
<Modal
title="修改密码"
open={pwdModalVisible}
onOk={handleChangePassword}
onCancel={() => setPwdModalVisible(false)}
okText="确定"
cancelText="取消"
width={420}
>
<Form form={pwdForm} layout="vertical" style={{ marginTop: 12 }}>
<Form.Item name="oldPassword" label="原密码" rules={[{ required: true, message: '请输入原密码' }]}>
<Input.Password placeholder="请输入原密码" />
</Form.Item>
<Form.Item
name="newPassword"
label="新密码"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码不少于6位' }
]}
>
<Input.Password placeholder="请输入新密码" />
</Form.Item>
<Form.Item
name="confirmPassword"
label="确认新密码"
dependencies={['newPassword']}
rules={[
{ required: true, message: '请再次输入新密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) return Promise.resolve()
return Promise.reject(new Error('两次输入的密码不一致'))
}
})
]}
>
<Input.Password placeholder="请再次输入新密码" />
</Form.Item>
</Form>
</Modal>
)
const userMenuItems = [
{ key: 'role', label: `角色:${user?.role === 'admin' ? '管理员' : '普通用户'}`, icon: <UserOutlined /> },
{ type: 'divider' },
{ key: 'changePwd', label: '修改密码', icon: <KeyOutlined />, onClick: () => setPwdModalVisible(true) },
{ 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>
{pwdModal}
</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)}
/>
<div className="sider-footer">
<Tooltip title={collapsed ? '消息对话' : ''} placement="right">
<Button
type="text"
block
className={currentPage === 'messages' ? 'sider-footer-active' : ''}
onClick={() => setCurrentPage('messages')}
>
<Badge count={unreadTotal} size="small" offset={collapsed ? [12, -8] : [8, 0]}>
<MessageOutlined style={{ fontSize: 16 }} />
</Badge>
{!collapsed && <span style={{ marginLeft: 10 }}>消息对话</span>}
</Button>
</Tooltip>
</div>
</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>
<Button type="text" icon={<KeyOutlined />} onClick={() => setPwdModalVisible(true)}>
修改密码
</Button>
<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>
{pwdModal}
</Layout>
)
}
export default App