refactor(frontend): 重构状态管理使用Zustand替代Context API
feat(auth): 添加账户锁定功能及自动解锁机制 feat(user): 在用户模型中添加lockedUntil字段 feat(api): 实现账户锁定逻辑和剩余尝试次数提示 perf(3d): 优化3D场景状态管理性能 perf(floorplan): 优化平面图状态管理性能 chore(deps): 添加zustand依赖 chore(config): 更新安全配置锁定时间为3分钟 docs: 更新部分组件注释 style: 调整登录页面样式
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+276
-548
@@ -1,16 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { version as appVersion } from '../../package.json';
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Card,
|
||||
message,
|
||||
Typography,
|
||||
Divider,
|
||||
Space,
|
||||
Alert,
|
||||
Row,
|
||||
Col,
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
@@ -20,18 +16,20 @@ import {
|
||||
SafetyCertificateOutlined,
|
||||
CloudServerOutlined,
|
||||
ArrowLeftOutlined,
|
||||
DashboardOutlined,
|
||||
SecurityScanOutlined,
|
||||
ApiOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { authAPI } from '../api';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import './Login.css';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const Login = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isFirstUser, setIsFirstUser] = useState(false);
|
||||
const [registerMode, setRegisterMode] = useState(false);
|
||||
const [unlockMode, setUnlockMode] = useState(false);
|
||||
const { login, register, checkAdmin } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -44,61 +42,30 @@ const Login = () => {
|
||||
const response = await checkAdmin();
|
||||
if (response.success) {
|
||||
setIsFirstUser(!response.data.hasAdmin);
|
||||
if (!response.data.hasAdmin) {
|
||||
setRegisterMode(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 静默处理:登录页调用 check-admin 失败是正常的(未登录状态)
|
||||
// 不显示错误信息,避免触发 401 重定向循环
|
||||
console.log('检查管理员状态:', error);
|
||||
console.error('检查管理员状态失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const onFinishLogin = async values => {
|
||||
const onFinishLogin = async (values) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await login(values.username, values.password);
|
||||
if (result.success) {
|
||||
message.success('登录成功');
|
||||
navigate('/');
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
if (result.code === 'PENDING_APPROVAL') {
|
||||
message.warning('账户待审核,请联系管理员激活');
|
||||
} else {
|
||||
message.error(result.message || '登录失败');
|
||||
}
|
||||
message.error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error || '登录失败');
|
||||
message.error('登录失败,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onFinishUnlock = async values => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await authAPI.unlock(values);
|
||||
if (response.success) {
|
||||
message.success('解锁成功,请重新登录');
|
||||
setUnlockMode(false);
|
||||
} else {
|
||||
message.error(response.message || '解锁失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error || '解锁失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onFinishRegister = async values => {
|
||||
if (values.password !== values.confirmPassword) {
|
||||
message.error('两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
const onFinishRegister = async (values) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await register({
|
||||
@@ -108,552 +75,313 @@ const Login = () => {
|
||||
phone: values.phone,
|
||||
realName: values.realName,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
message.success(result.isFirstUser ? '管理员账号创建成功' : '注册成功,请等待管理员审核');
|
||||
if (result.isFirstUser) {
|
||||
message.success('注册成功,已为您创建管理员账户');
|
||||
navigate('/');
|
||||
} else if (result.pendingApproval) {
|
||||
message.success('注册成功,请等待管理员审核');
|
||||
setRegisterMode(false);
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
message.success('注册成功');
|
||||
navigate('/');
|
||||
setRegisterMode(false);
|
||||
}
|
||||
} else {
|
||||
message.error(result.message || '注册失败');
|
||||
message.error(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error || '注册失败');
|
||||
message.error('注册失败,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 左侧宣传区域组件
|
||||
const LeftPanel = () => (
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
padding: '60px',
|
||||
color: '#fff',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* 背景装饰 */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '600px',
|
||||
height: '600px',
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
top: '-200px',
|
||||
left: '-200px',
|
||||
filter: 'blur(60px)',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '400px',
|
||||
height: '400px',
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
bottom: '-100px',
|
||||
right: '-100px',
|
||||
filter: 'blur(40px)',
|
||||
}}
|
||||
/>
|
||||
const features = [
|
||||
{ icon: <DashboardOutlined />, title: '实时监控', desc: '3D可视化机房全景', color: '#14b8a6' },
|
||||
{ icon: <SecurityScanOutlined />, title: '安全可靠', desc: '多重权限防护', color: '#3b82f6' },
|
||||
{ icon: <ApiOutlined />, title: '极速响应', desc: '毫秒级数据采集', color: '#8b5cf6' },
|
||||
];
|
||||
|
||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
borderRadius: '20px',
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '40px',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255,255,255,0.3)',
|
||||
}}
|
||||
>
|
||||
<CloudServerOutlined style={{ fontSize: '40px', color: '#fff' }} />
|
||||
</div>
|
||||
const stats = [
|
||||
{ value: '99.9%', label: '可用性', icon: '↑' },
|
||||
{ value: '24/7', label: '监控', icon: '●' },
|
||||
{ value: '<100ms', label: '响应', icon: '⚡' },
|
||||
];
|
||||
|
||||
<Title
|
||||
level={1}
|
||||
style={{
|
||||
color: '#fff',
|
||||
fontSize: '48px',
|
||||
fontWeight: 700,
|
||||
marginBottom: '24px',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
IDC设备
|
||||
<br />
|
||||
管理系统
|
||||
</Title>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
fontSize: '18px',
|
||||
lineHeight: 1.8,
|
||||
maxWidth: '480px',
|
||||
marginBottom: '48px',
|
||||
}}
|
||||
>
|
||||
专业的数据中心设备管理平台,提供机房、机柜、设备的全生命周期管理,
|
||||
助力企业实现高效的IT资产管理。
|
||||
</Paragraph>
|
||||
|
||||
<Row gutter={[24, 24]}>
|
||||
<Col xs={8} sm={8} md={8}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '32px', fontWeight: 700, marginBottom: '8px' }}>99.9%</div>
|
||||
<div style={{ fontSize: '13px', opacity: 0.8 }}>系统稳定性</div>
|
||||
const RackVisual = () => (
|
||||
<div className="rack-visual">
|
||||
<div className="rack-visual__header">
|
||||
<span className="rack-visual__title">机房概览</span>
|
||||
<span className="rack-visual__badge">3 在线</span>
|
||||
</div>
|
||||
<div className="rack-visual__body">
|
||||
{[1, 2, 3].map((rack) => (
|
||||
<div key={rack} className="rack-unit" style={{ animationDelay: `${rack * 0.1}s` }}>
|
||||
<div className="rack-unit__header">
|
||||
<span className="rack-unit__id">{rack.toString().padStart(2, '0')}</span>
|
||||
<span className="rack-unit__status" />
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={8} sm={8} md={8}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '32px', fontWeight: 700, marginBottom: '8px' }}>24/7</div>
|
||||
<div style={{ fontSize: '13px', opacity: 0.8 }}>全天候监控</div>
|
||||
<div className="rack-unit__slots">
|
||||
{[1, 2, 3, 4, 5].map((slot) => (
|
||||
<div
|
||||
key={slot}
|
||||
className={`rack-unit__slot rack-unit__slot--${slot <= (4 - rack + 1) ? 'on' : 'off'}`}
|
||||
style={{ animationDelay: `${(rack * 5 + slot) * 0.03}s` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={8} sm={8} md={8}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '32px', fontWeight: 700, marginBottom: '8px' }}>100%</div>
|
||||
<div style={{ fontSize: '13px', opacity: 0.8 }}>数据安全</div>
|
||||
<div className="rack-unit__load">
|
||||
<div className="rack-unit__load-bar" style={{ width: `${25 + rack * 18}%` }} />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 获取标题和副标题
|
||||
const getHeaderContent = () => {
|
||||
if (isFirstUser) {
|
||||
return {
|
||||
title: '创建管理员账户',
|
||||
subtitle: '首次使用,请创建系统管理员账户',
|
||||
};
|
||||
}
|
||||
if (unlockMode) {
|
||||
return {
|
||||
title: '账户解锁',
|
||||
subtitle: '输入账户信息以解锁账户',
|
||||
};
|
||||
}
|
||||
if (registerMode) {
|
||||
return {
|
||||
title: '注册新账户',
|
||||
subtitle: '填写信息完成账户注册',
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: '欢迎回来',
|
||||
subtitle: '请登录您的账户以继续',
|
||||
};
|
||||
};
|
||||
|
||||
const headerContent = getHeaderContent();
|
||||
|
||||
return (
|
||||
<Row style={{ minHeight: '100vh', overflow: 'hidden' }}>
|
||||
{/* 左侧区域 - 桌面端显示 */}
|
||||
<Col
|
||||
xs={0}
|
||||
sm={0}
|
||||
md={0}
|
||||
lg={12}
|
||||
xl={14}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #6B8DD6 100%)',
|
||||
}}
|
||||
>
|
||||
<LeftPanel />
|
||||
</Col>
|
||||
<div className="login-container">
|
||||
<div className="login-bg-decoration">
|
||||
<div className="decoration-circle decoration-circle--1" />
|
||||
<div className="decoration-circle decoration-circle--2" />
|
||||
<div className="decoration-circle decoration-circle--3" />
|
||||
<div className="decoration-grid" />
|
||||
<div className="decoration-wave" />
|
||||
</div>
|
||||
|
||||
{/* 右侧登录区域 */}
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={24}
|
||||
lg={12}
|
||||
xl={10}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#f8fafc',
|
||||
padding: '24px',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* 移动端背景 */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '200px',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'none',
|
||||
}}
|
||||
className="mobile-bg"
|
||||
/>
|
||||
<div className="login-wrapper">
|
||||
<div className="login-card">
|
||||
<div className="login-card__brand">
|
||||
<div className="brand-header">
|
||||
<div className="brand-logo">
|
||||
<CloudServerOutlined />
|
||||
<div className="logo-pulse" />
|
||||
</div>
|
||||
<div className="brand-title-section">
|
||||
<h1 className="brand-title">
|
||||
<span className="brand-title__main">IDC</span>
|
||||
<span className="brand-title__sub">机柜管理系统</span>
|
||||
</h1>
|
||||
<p className="brand-desc">智能数据中心基础设施管理平台</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: registerMode ? 520 : 440,
|
||||
borderRadius: '24px',
|
||||
boxShadow: '0 25px 80px rgba(0,0,0,0.15), 0 10px 30px rgba(0,0,0,0.1)',
|
||||
background: '#fff',
|
||||
border: 'none',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}}
|
||||
bodyStyle={{ padding: '48px' }}
|
||||
>
|
||||
{/* 返回按钮 */}
|
||||
{(registerMode || unlockMode) && !isFirstUser && (
|
||||
<Button
|
||||
type="link"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => {
|
||||
setRegisterMode(false);
|
||||
setUnlockMode(false);
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '24px',
|
||||
left: '24px',
|
||||
color: '#667eea',
|
||||
padding: '4px 8px',
|
||||
}}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
)}
|
||||
<RackVisual />
|
||||
|
||||
{/* 头部 */}
|
||||
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '64px',
|
||||
height: '64px',
|
||||
borderRadius: '16px',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto 20px',
|
||||
boxShadow: '0 8px 24px rgba(102, 126, 234, 0.35)',
|
||||
}}
|
||||
>
|
||||
<CloudServerOutlined style={{ fontSize: '32px', color: '#fff' }} />
|
||||
<div className="brand-stats">
|
||||
{stats.map((stat, index) => (
|
||||
<div key={index} className="stat-card">
|
||||
<span className="stat-value">{stat.value}</span>
|
||||
<span className="stat-label">{stat.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="brand-features">
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="brand-feature"
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
>
|
||||
<div
|
||||
className="feature-icon"
|
||||
style={{ background: `linear-gradient(135deg, ${feature.color}15, ${feature.color}10)`, color: feature.color }}
|
||||
>
|
||||
{feature.icon}
|
||||
</div>
|
||||
<div className="feature-content">
|
||||
<span className="feature-title">{feature.title}</span>
|
||||
<span className="feature-desc">{feature.desc}</span>
|
||||
</div>
|
||||
<div className="feature-arrow">→</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="brand-footer">
|
||||
<span className="footer-trust">
|
||||
<span className="trust-icon">🛡️</span>
|
||||
企业级安全认证
|
||||
</span>
|
||||
<span className="footer-version">v{appVersion}</span>
|
||||
</div>
|
||||
<Title
|
||||
level={3}
|
||||
style={{
|
||||
fontSize: '28px',
|
||||
fontWeight: 700,
|
||||
color: '#1e293b',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>
|
||||
{headerContent.title}
|
||||
</Title>
|
||||
<Text style={{ fontSize: '15px', color: '#64748b' }}>{headerContent.subtitle}</Text>
|
||||
</div>
|
||||
|
||||
{/* 首次使用提示 */}
|
||||
{isFirstUser && (
|
||||
<Alert
|
||||
message="欢迎使用IDC设备管理系统"
|
||||
description="您是第一个用户,系统将自动为您分配管理员权限。"
|
||||
type="success"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px', borderRadius: '12px' }}
|
||||
/>
|
||||
)}
|
||||
<div className="login-card__divider" />
|
||||
|
||||
{/* 解锁模式提示 */}
|
||||
{unlockMode && (
|
||||
<Alert
|
||||
message="账户解锁说明"
|
||||
description="当您的账户连续5次登录失败后会被锁定,请输入正确的用户名和密码进行解锁。"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px', borderRadius: '12px' }}
|
||||
/>
|
||||
)}
|
||||
<div className="login-card__form">
|
||||
<div className="form-header">
|
||||
<Title level={2} className="form-title">
|
||||
{isFirstUser ? '初始化系统' : '欢迎回来'}
|
||||
</Title>
|
||||
<Text className="form-subtitle">
|
||||
{isFirstUser
|
||||
? '创建管理员账号以开始使用系统'
|
||||
: '请登录您的账号继续访问'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 表单 */}
|
||||
<Form
|
||||
name={unlockMode ? 'unlock' : registerMode ? 'register' : 'login'}
|
||||
size="large"
|
||||
onFinish={unlockMode ? onFinishUnlock : registerMode ? onFinishRegister : onFinishLogin}
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
>
|
||||
{registerMode ? (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' },
|
||||
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
||||
placeholder="请输入用户名"
|
||||
style={{ borderRadius: '12px', height: '48px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="realName"
|
||||
label="真实姓名"
|
||||
rules={[{ required: true, message: '请输入真实姓名' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={
|
||||
<SafetyCertificateOutlined
|
||||
style={{ color: '#94a3b8', fontSize: '18px' }}
|
||||
/>
|
||||
}
|
||||
placeholder="请输入真实姓名"
|
||||
style={{ borderRadius: '12px', height: '48px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="邮箱"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={<MailOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
||||
placeholder="请输入邮箱"
|
||||
style={{ borderRadius: '12px', height: '48px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="phone" label="手机号">
|
||||
<Input
|
||||
prefix={<PhoneOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
||||
placeholder="请输入手机号(可选)"
|
||||
style={{ borderRadius: '12px', height: '48px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码长度不能少于6个字符' },
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
||||
placeholder="请输入密码"
|
||||
style={{ borderRadius: '12px', height: '48px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
label="确认密码"
|
||||
dependencies={['password']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('password') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
||||
placeholder="请再次输入密码"
|
||||
style={{ borderRadius: '12px', height: '48px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
||||
placeholder="请输入用户名"
|
||||
style={{ borderRadius: '12px', height: '52px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
style={{ marginBottom: '8px' }}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined style={{ color: '#94a3b8', fontSize: '18px' }} />}
|
||||
placeholder="请输入密码"
|
||||
style={{ borderRadius: '12px', height: '52px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{!registerMode && !unlockMode && (
|
||||
<div style={{ textAlign: 'right', marginBottom: '24px' }}>
|
||||
<Button type="link" style={{ color: '#667eea', padding: 0 }}>
|
||||
忘记密码?
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
{isFirstUser && (
|
||||
<Alert
|
||||
className="form-alert"
|
||||
message="系统初始化"
|
||||
description="首次使用需要创建管理员账号"
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form.Item style={{ marginTop: '32px', marginBottom: '16px' }}>
|
||||
{registerMode && !isFirstUser && (
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
style={{
|
||||
height: '52px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
borderRadius: '12px',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
border: 'none',
|
||||
boxShadow: '0 8px 24px rgba(102, 126, 234, 0.35)',
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => {
|
||||
setRegisterMode(false);
|
||||
}}
|
||||
className="back-button"
|
||||
>
|
||||
{registerMode ? '立即注册' : unlockMode ? '立即解锁' : '登 录'}
|
||||
返回登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{/* 底部切换 */}
|
||||
{!isFirstUser && (
|
||||
<div style={{ textAlign: 'center', marginTop: '24px' }}>
|
||||
{!unlockMode && !registerMode && (
|
||||
<Space split={<Divider type="vertical" />} size="large">
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => setRegisterMode(true)}
|
||||
style={{ color: '#64748b', fontWeight: 500 }}
|
||||
<Form
|
||||
name={registerMode ? 'register' : 'login'}
|
||||
layout="vertical"
|
||||
onFinish={registerMode ? onFinishRegister : onFinishLogin}
|
||||
size="large"
|
||||
className="login-form"
|
||||
>
|
||||
{registerMode && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="realName"
|
||||
label="真实姓名"
|
||||
rules={[{ required: true, message: '请输入真实姓名' }]}
|
||||
>
|
||||
注册新账户
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => setUnlockMode(true)}
|
||||
style={{ color: '#64748b', fontWeight: 500 }}
|
||||
<Input
|
||||
placeholder="请输入真实姓名"
|
||||
className="login-input"
|
||||
prefix={<UserOutlined className="input-icon" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
账户解锁
|
||||
</Button>
|
||||
</Space>
|
||||
<Input
|
||||
placeholder="请输入用户名"
|
||||
className="login-input"
|
||||
prefix={<UserOutlined className="input-icon" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="邮箱"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入邮箱"
|
||||
className="login-input"
|
||||
prefix={<MailOutlined className="input-icon" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="phone"
|
||||
label="手机号"
|
||||
rules={[{ required: true, message: '请输入手机号' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入手机号"
|
||||
className="login-input"
|
||||
prefix={<PhoneOutlined className="input-icon" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(registerMode || unlockMode) && (
|
||||
<Text style={{ color: '#64748b' }}>
|
||||
已有账户?{' '}
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
setRegisterMode(false);
|
||||
setUnlockMode(false);
|
||||
}}
|
||||
style={{ color: '#667eea', fontWeight: 600, padding: 0 }}
|
||||
>
|
||||
立即登录
|
||||
</Button>
|
||||
</Text>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label={isFirstUser ? '管理员账号' : '用户名'}
|
||||
rules={[{ required: true, message: isFirstUser ? '请设置管理员账号' : '请输入用户名' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined className="input-icon" />}
|
||||
placeholder={isFirstUser ? '请设置管理员账号' : '请输入用户名'}
|
||||
className="login-input"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={isFirstUser ? '管理员密码' : '密码'}
|
||||
rules={[
|
||||
{ required: true, message: isFirstUser ? '请设置管理员密码' : '请输入密码' },
|
||||
{ min: 6, message: '密码长度不能少于6位' },
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined className="input-icon" />}
|
||||
placeholder={isFirstUser ? '请设置管理员密码' : '请输入密码'}
|
||||
className="login-input"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{registerMode && (
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
label="确认密码"
|
||||
dependencies={['password']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('password') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined className="input-icon" />}
|
||||
placeholder="请确认密码"
|
||||
className="login-input"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item className="submit-item">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
className="submit-button"
|
||||
>
|
||||
{registerMode ? '立即注册' : '登 录'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{!isFirstUser && !registerMode && (
|
||||
<div className="form-actions">
|
||||
<Button type="link" className="action-link" onClick={() => setRegisterMode(true)}>
|
||||
<SafetyCertificateOutlined /> 立即注册
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-footer">
|
||||
<span>IDC Management System</span>
|
||||
<span className="footer-dot" />
|
||||
<span>v{appVersion}</span>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 移动端底部版权 */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '24px',
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: 'center',
|
||||
color: '#94a3b8',
|
||||
fontSize: '13px',
|
||||
display: 'none',
|
||||
}}
|
||||
className="mobile-footer"
|
||||
>
|
||||
© 2024 IDC设备管理系统. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
{/* 响应式样式 */}
|
||||
<style>{`
|
||||
@media (max-width: 991px) {
|
||||
.mobile-bg {
|
||||
display: block !important;
|
||||
}
|
||||
.mobile-footer {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
@media (max-width: 575px) {
|
||||
.ant-card-body {
|
||||
padding: 32px 24px !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
export default Login;
|
||||
@@ -35,7 +35,7 @@ import DeviceDetailDrawer from '../components/DeviceDetailDrawer';
|
||||
import CloseButton from '../components/CloseButton';
|
||||
import RackSelectorHeader from '../components/3d/RackSelectorHeader';
|
||||
import { Layout } from 'antd';
|
||||
import { useScene3D } from '../context/Scene3DContext';
|
||||
import { useScene3D } from '../hooks/useScene3D';
|
||||
import { useSortedRacks } from '../hooks/useSortedRacks';
|
||||
|
||||
const { Content } = Layout;
|
||||
@@ -46,7 +46,7 @@ const Rack3DVisualization = () => {
|
||||
// Scene 组件的 ref,用于调用重置视角方法
|
||||
const sceneRef = useRef(null);
|
||||
|
||||
// 使用 Scene3DContext 管理3D场景状态
|
||||
// 使用 Zustand Store 管理3D场景状态
|
||||
const {
|
||||
devices,
|
||||
setDevices,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useRef, useCallback, useState, useEffect } from 'react';
|
||||
import { Spin, Empty, message, Button } from 'antd';
|
||||
import { HomeOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { FloorPlanProvider } from '../context/FloorPlanContext';
|
||||
import useFloorPlanContext from '../hooks/floorplan/useFloorPlanContext';
|
||||
import useFloorPlanData from '../hooks/floorplan/useFloorPlanData';
|
||||
import { FloorPlanCanvas, FloorPlanToolbar, RackDetailPanel } from '../components/floorplan';
|
||||
@@ -48,28 +47,20 @@ const DeviceTooltip = ({ device, rack, x, y }) => {
|
||||
</DeviceInfoRow>
|
||||
<DeviceInfoRow>
|
||||
<DeviceInfoLabel>状态:</DeviceInfoLabel>
|
||||
<DeviceInfoValue $color={statusColor}>{DEVICE_STATUS_NAMES[device.status] || device.status}</DeviceInfoValue>
|
||||
</DeviceInfoRow>
|
||||
<DeviceInfoRow>
|
||||
<DeviceInfoLabel>位置:</DeviceInfoLabel>
|
||||
<DeviceInfoValue>{rack?.name} - {device.position}U</DeviceInfoValue>
|
||||
<DeviceInfoValue style={{ color: statusColor }}>
|
||||
{DEVICE_STATUS_NAMES[device.status] || device.status}
|
||||
</DeviceInfoValue>
|
||||
</DeviceInfoRow>
|
||||
{device.ipAddress && (
|
||||
<DeviceInfoRow>
|
||||
<DeviceInfoLabel>IP:</DeviceInfoLabel>
|
||||
<DeviceInfoValue $mono>{device.ipAddress}</DeviceInfoValue>
|
||||
<DeviceInfoValue>{device.ipAddress}</DeviceInfoValue>
|
||||
</DeviceInfoRow>
|
||||
)}
|
||||
{device.model && (
|
||||
{rack && (
|
||||
<DeviceInfoRow>
|
||||
<DeviceInfoLabel>型号:</DeviceInfoLabel>
|
||||
<DeviceInfoValue>{device.model}</DeviceInfoValue>
|
||||
</DeviceInfoRow>
|
||||
)}
|
||||
{device.height > 1 && (
|
||||
<DeviceInfoRow>
|
||||
<DeviceInfoLabel>高度:</DeviceInfoLabel>
|
||||
<DeviceInfoValue>{device.height}U</DeviceInfoValue>
|
||||
<DeviceInfoLabel>位置:</DeviceInfoLabel>
|
||||
<DeviceInfoValue>{rack.name} U{device.position}</DeviceInfoValue>
|
||||
</DeviceInfoRow>
|
||||
)}
|
||||
</DeviceInfoContent>
|
||||
@@ -77,26 +68,7 @@ const DeviceTooltip = ({ device, rack, x, y }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyState = ({ onRefresh }) => (
|
||||
<EmptyStateContainer>
|
||||
<Empty
|
||||
image={<HomeOutlined style={{ fontSize: 64, color: '#d9d9d9' }} />}
|
||||
description={
|
||||
<>
|
||||
<EmptyStateTitle>暂无机房数据</EmptyStateTitle>
|
||||
<EmptyStateSubtitle>请先在机房管理中创建机房,或检查网络连接</EmptyStateSubtitle>
|
||||
{onRefresh && (
|
||||
<Button icon={<ReloadOutlined />} onClick={onRefresh}>
|
||||
刷新
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</EmptyStateContainer>
|
||||
);
|
||||
|
||||
const FloorPlanContent = () => {
|
||||
const RoomFloorPlanContent = () => {
|
||||
const {
|
||||
selectedRoomId,
|
||||
setSelectedRoom,
|
||||
@@ -160,113 +132,99 @@ const FloorPlanContent = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleRackClick = useCallback((rack) => {
|
||||
if (rack) {
|
||||
showDetail(rack);
|
||||
}
|
||||
}, [showDetail]);
|
||||
|
||||
const handleRackDoubleClick = useCallback((rack) => {
|
||||
if (rack) {
|
||||
showDetail(rack);
|
||||
}
|
||||
}, [showDetail]);
|
||||
|
||||
const handleDeviceHover = useCallback((device, rack, x, y) => {
|
||||
const handleDeviceHover = useCallback((device, rack, event) => {
|
||||
if (device) {
|
||||
const rect = containerRef.current?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
setTooltipPosition({
|
||||
x: event.clientX - rect.left + 15,
|
||||
y: event.clientY - rect.top + 15,
|
||||
});
|
||||
}
|
||||
setHoveredDevice(device);
|
||||
setHoveredDeviceRack(rack);
|
||||
setTooltipPosition({ x, y });
|
||||
} else {
|
||||
setHoveredDevice(null);
|
||||
setHoveredDeviceRack(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleViewChange = useCallback((viewState) => {
|
||||
setCurrentZoom(viewState.zoom);
|
||||
}, []);
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
if (!canvasRef.current || !layoutData?.room) {
|
||||
message.warning('请先选择机房');
|
||||
return;
|
||||
const handleRackClick = useCallback((rack) => {
|
||||
if (rack) {
|
||||
showDetail(rack);
|
||||
}
|
||||
}, [showDetail]);
|
||||
|
||||
const dataUrl = canvasRef.current.exportImage(layoutData.room.name);
|
||||
if (!dataUrl) {
|
||||
message.error('导出失败,请稍后重试');
|
||||
return;
|
||||
}
|
||||
if (loading) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<LoadingOverlay>
|
||||
<Spin size="large" />
|
||||
<span>加载中...</span>
|
||||
</LoadingOverlay>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.download = `${layoutData.room.name || '机房平面图'}_${new Date().toISOString().slice(0, 10)}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
message.success('导出成功');
|
||||
}, [layoutData]);
|
||||
if (!layoutData || layoutData.racks.length === 0) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<EmptyStateContainer>
|
||||
<Empty
|
||||
description={selectedRoomId ? '该机房暂无机柜数据' : '请先选择机房'}
|
||||
/>
|
||||
</EmptyStateContainer>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentWrapper>
|
||||
<PageContainer>
|
||||
<FloorPlanToolbar
|
||||
selectedRoomId={selectedRoomId}
|
||||
roomName={layoutData.roomName}
|
||||
roomId={selectedRoomId}
|
||||
onRoomChange={setSelectedRoom}
|
||||
zoom={currentZoom}
|
||||
onZoomIn={() => canvasRef.current?.zoomIn()}
|
||||
onZoomOut={() => canvasRef.current?.zoomOut()}
|
||||
onZoomReset={() => canvasRef.current?.zoomReset()}
|
||||
onZoomChange={setCurrentZoom}
|
||||
onFullscreen={handleToggleFullscreen}
|
||||
isFullscreen={isFullscreen}
|
||||
onToggleFullscreen={handleToggleFullscreen}
|
||||
onRefresh={refetch}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
|
||||
<CanvasContainer ref={containerRef}>
|
||||
{loading && (
|
||||
<LoadingOverlay>
|
||||
<Spin tip="加载中..." />
|
||||
</LoadingOverlay>
|
||||
)}
|
||||
|
||||
{!selectedRoomId && <EmptyState onRefresh={refetch} />}
|
||||
|
||||
{selectedRoomId && layoutData && (
|
||||
|
||||
<ContentWrapper ref={containerRef}>
|
||||
<CanvasContainer>
|
||||
<FloorPlanCanvas
|
||||
ref={canvasRef}
|
||||
room={layoutData.room}
|
||||
racks={layoutData.racks}
|
||||
rooms={layoutData.rooms}
|
||||
selectedRoomId={selectedRoomId}
|
||||
zoom={currentZoom}
|
||||
onRackClick={handleRackClick}
|
||||
onRackDoubleClick={handleRackDoubleClick}
|
||||
onDeviceHover={handleDeviceHover}
|
||||
onViewChange={handleViewChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<DeviceTooltip
|
||||
device={hoveredDevice}
|
||||
rack={hoveredDeviceRack}
|
||||
x={tooltipPosition.x}
|
||||
y={tooltipPosition.y}
|
||||
/>
|
||||
</CanvasContainer>
|
||||
|
||||
{hoveredDevice && (
|
||||
<DeviceTooltip
|
||||
device={hoveredDevice}
|
||||
rack={hoveredDeviceRack}
|
||||
x={tooltipPosition.x}
|
||||
y={tooltipPosition.y}
|
||||
/>
|
||||
)}
|
||||
</CanvasContainer>
|
||||
</ContentWrapper>
|
||||
|
||||
<RackDetailPanel
|
||||
rack={detailRack}
|
||||
visible={detailVisible}
|
||||
onClose={hideDetail}
|
||||
/>
|
||||
</ContentWrapper>
|
||||
{detailVisible && detailRack && (
|
||||
<RackDetailPanel
|
||||
rack={detailRack}
|
||||
onClose={hideDetail}
|
||||
/>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const RoomFloorPlan = () => {
|
||||
return (
|
||||
<FloorPlanProvider>
|
||||
<PageContainer>
|
||||
<FloorPlanContent />
|
||||
</PageContainer>
|
||||
</FloorPlanProvider>
|
||||
);
|
||||
return <RoomFloorPlanContent />;
|
||||
};
|
||||
|
||||
export default RoomFloorPlan;
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
MenuUnfoldOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import axios from 'axios';
|
||||
import { useConfig } from '../context/ConfigContext';
|
||||
import { useConfig } from '../hooks/useConfig';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
Reference in New Issue
Block a user