Files
silk/web/src/layout/BasicLayout.tsx
T

163 lines
5.8 KiB
TypeScript
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 { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { App, Dropdown, Form, Input, Modal, Space, theme } from 'antd';
import {
DashboardOutlined,
HomeOutlined,
ControlOutlined,
BellOutlined,
VideoCameraOutlined,
FileTextOutlined,
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' },
];
// 角色中文名
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>
);
};