This commit is contained in:
2026-08-27 16:06:23 +08:00
parent 4de68f78cd
commit 2d19c736a6
8 changed files with 501 additions and 4 deletions
+63 -4
View File
@@ -1,10 +1,11 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { BrainCircuit, Calculator, Network, Boxes, ReceiptText, CalendarClock, Sparkles } from 'lucide-react';
import { BrainCircuit, Calculator, Network, Boxes, ReceiptText, CalendarClock, Sparkles, LogOut } from 'lucide-react';
import { useTheme } from '@/shared/context/ThemeContext';
import StarBackground from '@/shared/components/StarBackground';
import ThemeToggle from '@/shared/components/ThemeToggle';
import { modules } from '@/modules/registry';
import { getUserInfo, clearSession, type SessionUserInfo } from '@/shared/services/iamAuth';
const ICON_MAP: Record<string, React.ComponentType<{ size?: number; color?: string }>> = {
BrainCircuit,
@@ -21,6 +22,15 @@ export default function HomePage() {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [searchParams] = useSearchParams();
const [userInfo, setUserInfo] = useState<SessionUserInfo | null>(() => getUserInfo());
// 登录守卫:未登录跳转 /login
useEffect(() => {
const token = sessionStorage.getItem('userToken');
if (!token) {
navigate('/login', { replace: true });
}
}, [navigate]);
// 首页模块多时需纵向滚动;离开后恢复全局 overflow:hidden(其他模块自管布局)
useEffect(() => {
@@ -36,6 +46,12 @@ export default function HomePage() {
}
}, [searchParams]);
const handleLogout = () => {
clearSession();
setUserInfo(null);
navigate('/login', { replace: true });
};
return (
<div style={{
width: '100vw', minHeight: '100vh',
@@ -112,8 +128,51 @@ export default function HomePage() {
</div>
</div>
{/* 右上角主题切换 */}
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center' }}>
{/* 右上角:用户信息 + 主题切换 */}
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 16 }}>
{userInfo && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{
width: 28, height: 28, borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 13, fontWeight: 700,
color: '#04121f',
background: 'linear-gradient(135deg, #00d4ff, #4f9eff)',
}}>
{userInfo.userId.charAt(0).toUpperCase()}
</div>
<span style={{
fontSize: 13,
...(isDark ? { color: 'rgba(168,216,255,0.85)' } : { color: '#334155' }),
}}>
{userInfo.userId}
</span>
<button
onClick={handleLogout}
title="退出登录"
style={{
marginLeft: 4,
display: 'flex', alignItems: 'center', gap: 4,
padding: '5px 12px',
borderRadius: 8,
border: `1px solid ${isDark ? 'rgba(0,212,255,0.3)' : 'rgba(0,0,0,0.12)'}`,
background: isDark ? 'rgba(0,212,255,0.06)' : '#fff',
color: isDark ? 'rgba(168,216,255,0.8)' : '#475569',
fontSize: 12,
cursor: 'pointer',
transition: 'border-color 0.2s, color 0.2s',
}}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = '#ef4444'; e.currentTarget.style.color = '#ef4444'; }}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = isDark ? 'rgba(0,212,255,0.3)' : 'rgba(0,0,0,0.12)';
e.currentTarget.style.color = isDark ? 'rgba(168,216,255,0.8)' : '#475569';
}}
>
<LogOut size={13} />
退出
</button>
</div>
)}
<ThemeToggle />
</div>
</div>
+231
View File
@@ -0,0 +1,231 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { User, Lock, Building2, LogIn, AlertCircle, Loader2 } from 'lucide-react';
import StarBackground from '@/shared/components/StarBackground';
import { iamLogin, saveSession } from '@/shared/services/iamAuth';
import { useTheme } from '@/shared/context/ThemeContext';
const inputStyle = (isDark: boolean): React.CSSProperties => ({
width: '100%',
boxSizing: 'border-box',
padding: '12px 14px 12px 42px',
borderRadius: 10,
border: `1px solid ${isDark ? 'rgba(0,212,255,0.25)' : 'rgba(0,0,0,0.12)'}`,
background: isDark ? 'rgba(0,212,255,0.06)' : '#fff',
color: isDark ? '#e8f4ff' : '#1e293b',
fontSize: 14,
outline: 'none',
transition: 'border-color 0.2s, box-shadow 0.2s',
});
const iconStyle = (isDark: boolean): React.CSSProperties => ({
position: 'absolute',
left: 14,
top: '50%',
transform: 'translateY(-50%)',
color: isDark ? 'rgba(0,212,255,0.6)' : '#94a3b8',
});
export default function LoginPage() {
const navigate = useNavigate();
const { theme } = useTheme();
const isDark = theme === 'dark';
const [userId, setUserId] = useState('');
const [password, setPassword] = useState('');
const [tenantId, setTenantId] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
document.body.style.overflow = 'auto';
return () => { document.body.style.overflow = ''; };
}, []);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!userId.trim() || !password) {
setError('请输入账号和密码');
return;
}
setLoading(true);
setError('');
try {
const result = await iamLogin({ userId: userId.trim(), password, tenantId: tenantId.trim() || undefined });
saveSession(result.token, {
userId: result.userId,
tenantId: result.tenantId,
loginTime: new Date().toLocaleString('zh-CN'),
});
navigate('/', { replace: true });
} catch (ex) {
setError(ex instanceof Error ? ex.message : '登录失败,请稍后重试');
} finally {
setLoading(false);
}
};
return (
<div style={{
width: '100vw',
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
boxSizing: 'border-box',
fontFamily: "-apple-system, 'SF Pro Display', 'SF Pro Text', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif",
color: isDark ? '#a8d8ff' : '#1e3a6e',
}}>
{isDark ? <StarBackground theme="dark" /> : (
<div style={{
position: 'fixed', top: 0, left: 0, width: '100%', height: '100%',
background: 'linear-gradient(160deg, #f8f8fb 0%, #f2f3f7 50%, #eef1f7 100%)',
zIndex: 0,
}} />
)}
<div style={{
position: 'relative', zIndex: 1,
width: 400, maxWidth: '92vw',
padding: '40px 36px 32px',
borderRadius: 20,
background: isDark ? 'rgba(10,22,40,0.75)' : 'rgba(255,255,255,0.9)',
backdropFilter: 'blur(16px)',
border: `1px solid ${isDark ? 'rgba(0,212,255,0.2)' : 'rgba(0,0,0,0.06)'}`,
boxShadow: isDark ? '0 24px 64px rgba(0,0,0,0.5), 0 0 48px rgba(0,212,255,0.08)' : '0 24px 64px rgba(30,58,110,0.12)',
}}>
{/* 品牌区 */}
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<img
src="/digiwin.svg"
alt="Digiwin"
style={{ height: 36, objectFit: 'contain', filter: isDark ? 'drop-shadow(0 0 8px rgba(0,212,255,0.4))' : 'none' }}
/>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10, marginTop: 14 }}>
<h1 style={{
fontSize: 26, fontWeight: 800, letterSpacing: '0.04em', margin: 0,
...(isDark
? { color: '#e8f4ff', textShadow: '0 0 16px rgba(0,212,255,0.5)' }
: { color: '#111827' }),
}}>
EIOSpace
</h1>
</div>
<p style={{ fontSize: 13, marginTop: 6, ...(isDark ? { color: 'rgba(168,216,255,0.6)' } : { color: '#6b7280' }) }}>
企业智能 · 数据驱动 · 产业赋能
</p>
</div>
{/* 登录表单 */}
<form onSubmit={handleLogin}>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 6, color: isDark ? 'rgba(168,216,255,0.8)' : '#475569' }}>
账号(用户ID / 手机号 / 邮箱)
</label>
<div style={{ position: 'relative' }}>
<User size={16} style={iconStyle(isDark)} />
<input
type="text"
value={userId}
onChange={(e) => setUserId(e.target.value)}
placeholder="请输入账号"
autoComplete="username"
style={inputStyle(isDark)}
onFocus={(e) => { e.currentTarget.style.borderColor = '#00d4ff'; }}
onBlur={(e) => { e.currentTarget.style.borderColor = isDark ? 'rgba(0,212,255,0.25)' : 'rgba(0,0,0,0.12)'; }}
/>
</div>
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 6, color: isDark ? 'rgba(168,216,255,0.8)' : '#475569' }}>
密码
</label>
<div style={{ position: 'relative' }}>
<Lock size={16} style={iconStyle(isDark)} />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="请输入密码"
autoComplete="current-password"
style={inputStyle(isDark)}
onFocus={(e) => { e.currentTarget.style.borderColor = '#00d4ff'; }}
onBlur={(e) => { e.currentTarget.style.borderColor = isDark ? 'rgba(0,212,255,0.25)' : 'rgba(0,0,0,0.12)'; }}
/>
</div>
</div>
<div style={{ marginBottom: 20 }}>
<label style={{ display: 'block', fontSize: 13, marginBottom: 6, color: isDark ? 'rgba(168,216,255,0.8)' : '#475569' }}>
租户 ID(可选)
</label>
<div style={{ position: 'relative' }}>
<Building2 size={16} style={iconStyle(isDark)} />
<input
type="text"
value={tenantId}
onChange={(e) => setTenantId(e.target.value)}
placeholder="不填则自动匹配已授权租户"
style={inputStyle(isDark)}
onFocus={(e) => { e.currentTarget.style.borderColor = '#00d4ff'; }}
onBlur={(e) => { e.currentTarget.style.borderColor = isDark ? 'rgba(0,212,255,0.25)' : 'rgba(0,0,0,0.12)'; }}
/>
</div>
</div>
{error && (
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '10px 12px', marginBottom: 16,
borderRadius: 8, fontSize: 13,
background: 'rgba(239,68,68,0.12)',
border: '1px solid rgba(239,68,68,0.4)',
color: '#fca5a5',
}}>
<AlertCircle size={15} style={{ flexShrink: 0 }} />
{error}
</div>
)}
<button
type="submit"
disabled={loading}
style={{
width: '100%',
padding: '13px 0',
borderRadius: 10,
border: 'none',
cursor: loading ? 'wait' : 'pointer',
fontSize: 15,
fontWeight: 600,
letterSpacing: '0.08em',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
color: '#04121f',
background: 'linear-gradient(90deg, #00d4ff, #4f9eff)',
boxShadow: '0 8px 24px rgba(0,212,255,0.35)',
transition: 'transform 0.15s, box-shadow 0.15s, opacity 0.15s',
opacity: loading ? 0.7 : 1,
}}
onMouseEnter={(e) => { if (!loading) e.currentTarget.style.transform = 'translateY(-1px)'; }}
onMouseLeave={(e) => { e.currentTarget.style.transform = 'none'; }}
>
{loading ? <Loader2 size={17} style={{ animation: 'spin 1s linear infinite' }} /> : <LogIn size={17} />}
{loading ? '登录中...' : '登 录'}
</button>
</form>
<p style={{
textAlign: 'center', fontSize: 12, marginTop: 24,
...(isDark ? { color: 'rgba(168,216,255,0.45)' } : { color: '#94a3b8' }),
}}>
鼎捷云统一身份认证 · IAM
</p>
</div>
</div>
);
}