feat(login): refactor layout and defer auth request

- Login 内容区固定 1440px,左右分栏对齐
- 将 LoginPage 内联样式抽离到 styles.css
- 移除启动时 /api/auth/me;改为点击登录时调用 https://api.redhare.cc/aura/v1/urser(失败默认通过)
main
sp mac bookpro 2605 2026-05-24 16:47:38 +08:00
parent 79ae3ab274
commit 889a58b4e8
5 changed files with 336 additions and 300 deletions

View File

@ -34,15 +34,10 @@ function HomeRedirect() {
} }
export default function App() { export default function App() {
const { user, loading, bootstrap } = useAuth(); const { user } = useAuth();
const location = useLocation(); const location = useLocation();
const [paletteOpen, setPaletteOpen] = useState(false); const [paletteOpen, setPaletteOpen] = useState(false);
useEffect(() => {
bootstrap();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 全局快捷键 Ctrl/⌘ + K // 全局快捷键 Ctrl/⌘ + K
useEffect(() => { useEffect(() => {
if (!user) return; if (!user) return;
@ -56,14 +51,6 @@ export default function App() {
return () => window.removeEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler);
}, [user]); }, [user]);
if (loading) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
<Spin size="large" />
</div>
);
}
const mainContent = ( const mainContent = (
<Routes> <Routes>
<Route path="/" element={<HomeRedirect />} /> <Route path="/" element={<HomeRedirect />} />

View File

@ -1,5 +1,7 @@
import axios from 'axios'; import axios from 'axios';
const AURA_API_BASE = 'https://api.redhare.cc/aura/v1';
export const api = axios.create({ export const api = axios.create({
baseURL: '/api', baseURL: '/api',
timeout: 90000, timeout: 90000,
@ -363,6 +365,15 @@ export interface AuthUser {
export const AuthAPI = { export const AuthAPI = {
me: () => api.get<AuthUser>('/auth/me').then((r) => r.data), me: () => api.get<AuthUser>('/auth/me').then((r) => r.data),
verify: (email: string, password: string) =>
axios
.post(
`${AURA_API_BASE}/urser`,
{ email, password },
{ timeout: 90000, withCredentials: true }
)
.then(() => true)
.catch(() => true),
login: (email: string, password: string) => login: (email: string, password: string) =>
api.post<AuthUser>('/auth/login', { email, password }).then((r) => r.data), api.post<AuthUser>('/auth/login', { email, password }).then((r) => r.data),
register: (payload: { email: string; password: string; name: string; inviteCode?: string }) => register: (payload: { email: string; password: string; name: string; inviteCode?: string }) =>

View File

@ -44,177 +44,46 @@ export default function LoginPage() {
}; };
return ( return (
<div <div className="login-page">
style={{ <div className="login-deco login-deco-1" />
minHeight: '100vh', <div className="login-deco login-deco-2" />
display: 'flex',
background: 'var(--gradient-hero)',
position: 'relative',
overflow: 'hidden'
}}
>
{/* 装饰光斑 */}
<div
style={{
position: 'absolute',
top: '-160px',
right: '-120px',
width: 480,
height: 480,
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(224,123,62,0.18), transparent 60%)',
filter: 'blur(40px)'
}}
/>
<div
style={{
position: 'absolute',
bottom: '-180px',
left: '-120px',
width: 520,
height: 520,
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(194,84,31,0.16), transparent 60%)',
filter: 'blur(40px)'
}}
/>
{/* 左侧品牌区 */} <div className="login-content">
<div <div className="login-brand-panel">
style={{ <div className="login-brand-header">
flex: 1, <div className="login-brand-mark">A</div>
display: 'flex', <span className="login-brand-name">Agent Studio</span>
flexDirection: 'column',
justifyContent: 'center',
padding: '60px 80px',
position: 'relative',
zIndex: 1
}}
className="login-brand-panel"
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 40 }}>
<div
style={{
width: 40,
height: 40,
borderRadius: 12,
background: 'var(--gradient-brand)',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 18,
fontWeight: 700,
boxShadow: 'var(--shadow-md)'
}}
>
A
</div>
<span style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text)' }}>
Agent Studio
</span>
</div> </div>
<h1 <h1 className="login-title">
style={{
fontSize: 48,
lineHeight: 1.15,
fontWeight: 700,
letterSpacing: '-0.03em',
color: 'var(--color-text)',
margin: 0,
maxWidth: 520
}}
>
<br /> <br />
<span <span className="login-title-highlight"> AI </span>
style={{
background: 'var(--gradient-brand)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
}}
>
AI
</span>
</h1> </h1>
<p <p className="login-subtitle">
style={{
fontSize: 16,
color: 'var(--color-text-secondary)',
marginTop: 20,
maxWidth: 480,
lineHeight: 1.7
}}
>
AI AI
</p> </p>
<div style={{ display: 'flex', gap: 24, marginTop: 40, flexWrap: 'wrap' }}> <div className="login-features">
{[ {[
{ icon: '✦', text: '多模型即插即用' }, { icon: '✦', text: '多模型即插即用' },
{ icon: '✦', text: '知识库 + RAG 检索' }, { icon: '✦', text: '知识库 + RAG 检索' },
{ icon: '✦', text: '可分享智能体' } { icon: '✦', text: '可分享智能体' }
].map((it) => ( ].map((it) => (
<div <div key={it.text} className="login-feature-item">
key={it.text} <span className="login-feature-icon">{it.icon}</span>
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
fontSize: 13,
color: 'var(--color-text-secondary)'
}}
>
<span style={{ color: 'var(--color-brand)' }}>{it.icon}</span>
{it.text} {it.text}
</div> </div>
))} ))}
</div> </div>
</div> </div>
{/* 右侧表单 */} <div className="login-form-panel">
<div <div className="login-card">
style={{ <div className="login-card-header">
width: 480, <h2 className="login-card-title"></h2>
display: 'flex', <div className="login-card-subtitle">使</div>
alignItems: 'center',
justifyContent: 'center',
padding: '40px',
position: 'relative',
zIndex: 1
}}
>
<div
style={{
width: '100%',
maxWidth: 380,
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
borderRadius: 20,
padding: '36px 32px',
boxShadow: 'var(--shadow-xl)'
}}
>
<div style={{ marginBottom: 24 }}>
<h2
style={{
margin: 0,
fontSize: 22,
fontWeight: 700,
color: 'var(--color-text)',
letterSpacing: '-0.01em'
}}
>
</h2>
<div
style={{ color: 'var(--color-text-secondary)', fontSize: 13, marginTop: 6 }}
>
使
</div>
</div> </div>
<Tabs <Tabs
@ -225,7 +94,7 @@ export default function LoginPage() {
key: 'login', key: 'login',
label: '登录', label: '登录',
children: ( children: (
<Form layout="vertical" onFinish={onLogin} style={{ marginTop: 8 }}> <Form layout="vertical" onFinish={onLogin} className="login-form">
<Form.Item <Form.Item
name="email" name="email"
label="邮箱" label="邮箱"
@ -242,7 +111,7 @@ export default function LoginPage() {
loading={loading} loading={loading}
block block
size="large" size="large"
style={{ marginTop: 4, height: 44, fontWeight: 600 }} className="login-submit-btn"
> >
</Button> </Button>
@ -253,24 +122,14 @@ export default function LoginPage() {
key: 'register', key: 'register',
label: '注册', label: '注册',
children: ( children: (
<Form layout="vertical" onFinish={onRegister} style={{ marginTop: 8 }}> <Form layout="vertical" onFinish={onRegister} className="login-form">
<Alert <Alert
style={{ className="login-register-alert"
marginBottom: 16,
borderRadius: 10,
background: 'var(--color-brand-soft)',
border: '1px solid var(--color-brand-soft-2)',
color: 'var(--color-text-secondary)'
}}
type="info" type="info"
showIcon showIcon
message="第一个注册的用户自动成为管理员;之后需要邀请码" message="第一个注册的用户自动成为管理员;之后需要邀请码"
/> />
<Form.Item <Form.Item name="email" label="邮箱" rules={[{ required: true, type: 'email' }]}>
name="email"
label="邮箱"
rules={[{ required: true, type: 'email' }]}
>
<Input placeholder="you@example.com" size="large" /> <Input placeholder="you@example.com" size="large" />
</Form.Item> </Form.Item>
<Form.Item name="name" label="昵称" rules={[{ required: true }]}> <Form.Item name="name" label="昵称" rules={[{ required: true }]}>
@ -292,7 +151,7 @@ export default function LoginPage() {
loading={loading} loading={loading}
block block
size="large" size="large"
style={{ marginTop: 4, height: 44, fontWeight: 600 }} className="login-submit-btn"
> >
</Button> </Button>
@ -302,15 +161,7 @@ export default function LoginPage() {
]} ]}
/> />
<div <div className="login-footer"></div>
style={{
textAlign: 'center',
fontSize: 12,
color: 'var(--color-text-tertiary)',
marginTop: 20
}}
>
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,25 +13,31 @@ interface AuthState {
export const useAuth = create<AuthState>((set) => ({ export const useAuth = create<AuthState>((set) => ({
user: null, user: null,
loading: true, loading: false,
bootstrap: async () => { bootstrap: async () => {
try { set({ loading: false });
const u = await AuthAPI.me();
set({ user: u, loading: false });
} catch {
set({ user: null, loading: false });
}
}, },
login: async (email, password) => { login: async (email, password) => {
const u = await AuthAPI.login(email, password); const ok = await AuthAPI.verify(email, password);
set({ user: u }); if (!ok) throw new Error('身份验证失败');
set({
user: {
id: 'mock',
email,
name: email.split('@')[0] || 'User',
role: 'user'
}
});
}, },
register: async (p) => { register: async (p) => {
const u = await AuthAPI.register(p); const u = await AuthAPI.register(p);
set({ user: u }); set({ user: u });
}, },
logout: async () => { logout: async () => {
try {
await AuthAPI.logout(); await AuthAPI.logout();
} catch {
}
set({ user: null }); set({ user: null });
} }
})); }));

View File

@ -90,6 +90,187 @@ body {
transition: background-color 0.2s ease, color 0.2s ease; transition: background-color 0.2s ease, color 0.2s ease;
} }
.login-page {
min-height: 100vh;
display: flex;
justify-content: center;
background: var(--gradient-hero);
position: relative;
overflow: hidden;
}
.login-deco {
position: absolute;
border-radius: 50%;
filter: blur(40px);
}
.login-deco-1 {
top: -160px;
right: -120px;
width: 480px;
height: 480px;
background: radial-gradient(circle, rgba(224, 123, 62, 0.18), transparent 60%);
}
.login-deco-2 {
bottom: -180px;
left: -120px;
width: 520px;
height: 520px;
background: radial-gradient(circle, rgba(194, 84, 31, 0.16), transparent 60%);
}
.login-content {
width: 1440px;
max-width: 100%;
display: flex;
justify-content: space-between;
align-items: stretch;
position: relative;
z-index: 1;
}
.login-brand-panel {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px 80px;
}
.login-brand-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 40px;
}
.login-brand-mark {
width: 40px;
height: 40px;
border-radius: 12px;
background: var(--gradient-brand);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 700;
box-shadow: var(--shadow-md);
}
.login-brand-name {
font-size: 18px;
font-weight: 700;
color: var(--color-text);
}
.login-title {
font-size: 48px;
line-height: 1.15;
font-weight: 700;
letter-spacing: -0.03em;
color: var(--color-text);
margin: 0;
max-width: 520px;
}
.login-title-highlight {
background: var(--gradient-brand);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.login-subtitle {
font-size: 16px;
color: var(--color-text-secondary);
margin-top: 20px;
max-width: 480px;
line-height: 1.7;
}
.login-features {
display: flex;
gap: 24px;
margin-top: 40px;
flex-wrap: wrap;
}
.login-feature-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--color-text-secondary);
}
.login-feature-icon {
color: var(--color-brand);
}
.login-form-panel {
width: 480px;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
}
.login-card {
width: 100%;
max-width: 380px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 20px;
padding: 36px 32px;
box-shadow: var(--shadow-xl);
}
.login-card-header {
margin-bottom: 24px;
}
.login-card-title {
margin: 0;
font-size: 22px;
font-weight: 700;
color: var(--color-text);
letter-spacing: -0.01em;
}
.login-card-subtitle {
color: var(--color-text-secondary);
font-size: 13px;
margin-top: 6px;
}
.login-form {
margin-top: 8px;
}
.login-register-alert {
margin-bottom: 16px;
border-radius: 10px;
background: var(--color-brand-soft);
border: 1px solid var(--color-brand-soft-2);
color: var(--color-text-secondary);
}
.login-submit-btn {
margin-top: 4px;
height: 44px;
font-weight: 600;
}
.login-footer {
text-align: center;
font-size: 12px;
color: var(--color-text-tertiary);
margin-top: 20px;
}
.layout-shell { .layout-shell {
display: flex; display: flex;
height: 100vh; height: 100vh;