171 lines
6.3 KiB
TypeScript
171 lines
6.3 KiB
TypeScript
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||
import { App, Dropdown, Form, Input, Modal, Space, theme } from 'antd';
|
||
import {
|
||
DashboardOutlined,
|
||
HomeOutlined,
|
||
ControlOutlined,
|
||
BellOutlined,
|
||
VideoCameraOutlined,
|
||
FileTextOutlined,
|
||
BookOutlined,
|
||
ProfileOutlined,
|
||
ExperimentOutlined,
|
||
ShoppingOutlined,
|
||
SettingOutlined,
|
||
LogoutOutlined,
|
||
UserOutlined,
|
||
ThunderboltOutlined,
|
||
ApiOutlined,
|
||
ControlOutlined as ControlOutlinedIcon,
|
||
KeyOutlined,
|
||
} from '@ant-design/icons';
|
||
import { ProLayout } from '@ant-design/pro-components';
|
||
import { useState } from 'react';
|
||
import { authService } from '../services/auth';
|
||
|
||
const menuData = [
|
||
{ path: '/dashboard', name: '仪表盘', icon: <DashboardOutlined />, permission: 'dashboard:view' },
|
||
{ path: '/houses', name: '蚕房管理', icon: <HomeOutlined />, permission: 'room:read' },
|
||
{ path: '/devices', name: '设备管理', icon: <ControlOutlined />, permission: 'device:read' },
|
||
{ path: '/energy', name: '能耗管理', icon: <ThunderboltOutlined />, permission: 'energy:view' },
|
||
{ path: '/control', name: '设备控制', icon: <ApiOutlined />, permission: 'device:control' },
|
||
{ path: '/ir', name: '红外遥控', icon: <ControlOutlinedIcon />, permission: 'device:control' },
|
||
{ path: '/thresholds', name: '阈值配置', icon: <SettingOutlined />, permission: 'threshold:read' },
|
||
{ path: '/alerts', name: '告警中心', icon: <BellOutlined />, permission: 'alarm:read' },
|
||
{ path: '/videos', name: '视频监控', icon: <VideoCameraOutlined />, permission: 'video:read' },
|
||
{ path: '/logs', name: '控制日志', icon: <FileTextOutlined />, permission: 'log:read' },
|
||
{ path: '/knowledge', name: '知识库', icon: <BookOutlined />, permission: 'knowledge:read' },
|
||
{ path: '/batches', name: '批次管理', icon: <ProfileOutlined />, permission: 'batch:read' },
|
||
{ path: '/lamp-tests', name: 'LAMP 检测', icon: <ExperimentOutlined />, permission: 'lamp:read' },
|
||
{ path: '/consumables', name: '耗材管理', icon: <ShoppingOutlined />, permission: 'consumable:read' },
|
||
];
|
||
|
||
// 角色中文名
|
||
const roleNames: Record<string, string> = {
|
||
admin: '管理员',
|
||
operator: '操作员',
|
||
viewer: '查看者',
|
||
farmer: '养殖员',
|
||
};
|
||
|
||
export const BasicLayout = () => {
|
||
const location = useLocation();
|
||
const navigate = useNavigate();
|
||
const { message } = App.useApp();
|
||
const {
|
||
token: { colorBgContainer, borderRadiusLG },
|
||
} = theme.useToken();
|
||
const [pwdOpen, setPwdOpen] = useState(false);
|
||
const [pwdForm] = Form.useForm<{ oldPassword: string; newPassword: string; confirm: string }>();
|
||
const [pwdLoading, setPwdLoading] = useState(false);
|
||
|
||
const handleChangePassword = async () => {
|
||
const v = await pwdForm.validateFields();
|
||
if (v.newPassword !== v.confirm) {
|
||
message.error('两次输入的新密码不一致');
|
||
return;
|
||
}
|
||
setPwdLoading(true);
|
||
try {
|
||
await authService.changePassword({ oldPassword: v.oldPassword, newPassword: v.newPassword });
|
||
message.success('密码修改成功,请重新登录');
|
||
setPwdOpen(false);
|
||
pwdForm.resetFields();
|
||
await authService.logout();
|
||
navigate('/login');
|
||
} catch (err: any) {
|
||
const msg = err?.response?.data?.error || '密码修改失败';
|
||
message.error(typeof msg === 'string' ? msg : '密码修改失败');
|
||
} finally {
|
||
setPwdLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<ProLayout
|
||
title="蚕房环境监控"
|
||
layout="mix"
|
||
navTheme="light"
|
||
contentStyle={{ margin: 0, padding: 0 }}
|
||
menuItemRender={(item, dom) => (
|
||
<div
|
||
onClick={() => item.path && navigate(item.path)}
|
||
style={{ cursor: 'pointer' }}
|
||
>
|
||
{dom}
|
||
</div>
|
||
)}
|
||
menuDataRender={() => menuData.filter((m) => authService.hasPermission(m.permission))}
|
||
location={{ pathname: location.pathname }}
|
||
headerTitleRender={(logo, title) => (
|
||
<a onClick={() => navigate('/dashboard')}>
|
||
<Space>{logo}{title}</Space>
|
||
</a>
|
||
)}
|
||
avatarProps={{
|
||
icon: <UserOutlined />,
|
||
size: 'small',
|
||
title: (() => {
|
||
const user = authService.getUser();
|
||
return user ? `${user.username}(${roleNames[user.role ?? ''] ?? user.role})` : 'admin';
|
||
})(),
|
||
render: (_, dom) => (
|
||
<Dropdown
|
||
menu={{
|
||
items: [
|
||
{ key: 'changePassword', icon: <KeyOutlined />, label: '修改密码' },
|
||
{ type: 'divider' },
|
||
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录' },
|
||
],
|
||
onClick: async ({ key }) => {
|
||
if (key === 'logout') {
|
||
await authService.logout();
|
||
navigate('/login');
|
||
} else if (key === 'changePassword') {
|
||
pwdForm.resetFields();
|
||
setPwdOpen(true);
|
||
}
|
||
},
|
||
}}
|
||
>
|
||
{dom}
|
||
</Dropdown>
|
||
),
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
margin: 16,
|
||
padding: 16,
|
||
background: colorBgContainer,
|
||
borderRadius: borderRadiusLG,
|
||
minHeight: 280,
|
||
}}
|
||
>
|
||
<Outlet />
|
||
</div>
|
||
<Modal
|
||
title="修改密码"
|
||
open={pwdOpen}
|
||
onCancel={() => setPwdOpen(false)}
|
||
onOk={handleChangePassword}
|
||
confirmLoading={pwdLoading}
|
||
okText="提交"
|
||
cancelText="取消"
|
||
>
|
||
<Form form={pwdForm} layout="vertical">
|
||
<Form.Item label="原密码" name="oldPassword" rules={[{ required: true, message: '请输入原密码' }]}>
|
||
<Input.Password autoComplete="current-password" placeholder="请输入原密码" />
|
||
</Form.Item>
|
||
<Form.Item label="新密码" name="newPassword" rules={[{ required: true, message: '请输入新密码' }, { min: 6, message: '至少 6 位' }]}>
|
||
<Input.Password autoComplete="new-password" placeholder="至少 6 位" />
|
||
</Form.Item>
|
||
<Form.Item label="确认新密码" name="confirm" rules={[{ required: true, message: '请再次输入新密码' }]}>
|
||
<Input.Password autoComplete="new-password" placeholder="再次输入新密码" />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</ProLayout>
|
||
);
|
||
};
|