feature: 新增全新登录页布局与主题切换入口

This commit is contained in:
xiamuceer-j
2026-03-06 14:14:09 +08:00
parent fc03fe958f
commit 80bda8c021
+278 -159
View File
@@ -1,10 +1,12 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Button, Card, Space, Typography, message, Spin, Form, Input, Tabs } from 'antd'; import { Alert, Button, Card, Col, Divider, Form, Input, Layout, Row, Space, Spin, Tag, Typography, message, theme } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons'; import { BookOutlined, LockOutlined, RobotOutlined, SafetyCertificateOutlined, TeamOutlined, ThunderboltOutlined, UserOutlined } from '@ant-design/icons';
import { authApi } from '../services/api'; import { authApi } from '../services/api';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import AnnouncementModal from '../components/AnnouncementModal'; import AnnouncementModal from '../components/AnnouncementModal';
import ThemeSwitch from '../components/ThemeSwitch';
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;
export default function Login() { export default function Login() {
@@ -15,8 +17,11 @@ export default function Login() {
const [localAuthEnabled, setLocalAuthEnabled] = useState(false); const [localAuthEnabled, setLocalAuthEnabled] = useState(false);
const [linuxdoEnabled, setLinuxdoEnabled] = useState(false); const [linuxdoEnabled, setLinuxdoEnabled] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
const { token } = theme.useToken();
const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`;
const primaryButtonShadow = `0 8px 20px ${alphaColor(token.colorPrimary, 0.28)}`;
const hoverButtonShadow = `0 12px 28px ${alphaColor(token.colorPrimary, 0.36)}`;
const [showAnnouncement, setShowAnnouncement] = useState(false); const [showAnnouncement, setShowAnnouncement] = useState(false);
const [activeLoginMethod, setActiveLoginMethod] = useState<'local' | 'linuxdo'>('local');
// 检查是否已登录和获取认证配置 // 检查是否已登录和获取认证配置
useEffect(() => { useEffect(() => {
@@ -97,9 +102,9 @@ export default function Login() {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
minHeight: '100vh', minHeight: '100vh',
background: 'var(--color-bg-base)', background: token.colorBgLayout,
}}> }}>
<Spin size="large" style={{ color: 'var(--color-primary)' }} /> <Spin size="large" style={{ color: token.colorPrimary }} />
</div> </div>
); );
} }
@@ -108,47 +113,52 @@ export default function Login() {
const renderLocalLogin = () => ( const renderLocalLogin = () => (
<Form <Form
form={form} form={form}
layout="vertical"
onFinish={handleLocalLogin} onFinish={handleLocalLogin}
size="large" size="large"
style={{ marginTop: '24px' }} style={{ marginTop: '16px' }}
> >
<Form.Item <Form.Item
name="username" name="username"
rules={[{ required: true, message: '请输入用户名' }]} label="管理账号"
rules={[{ required: true, message: '请输入管理账号' }]}
> >
<Input <Input
prefix={<UserOutlined style={{ color: '#999' }} />} prefix={<UserOutlined style={{ color: token.colorTextTertiary }} />}
placeholder="用户名" placeholder="请输入管理账号"
autoComplete="username" autoComplete="username"
style={{ height: 46, borderRadius: 12 }}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="password" name="password"
rules={[{ required: true, message: '请输入密码' }]} label="访问密钥"
rules={[{ required: true, message: '请输入访问密钥' }]}
> >
<Input.Password <Input.Password
prefix={<LockOutlined style={{ color: '#999' }} />} prefix={<LockOutlined style={{ color: token.colorTextTertiary }} />}
placeholder="密码" placeholder="请输入访问密钥"
autoComplete="current-password" autoComplete="current-password"
style={{ height: 46, borderRadius: 12 }}
/> />
</Form.Item> </Form.Item>
<Form.Item style={{ marginBottom: 0 }}> <Form.Item style={{ marginBottom: 0, marginTop: 8 }}>
<Button <Button
type="primary" type="primary"
htmlType="submit" htmlType="submit"
loading={loading} loading={loading}
block block
style={{ style={{
height: 48, height: 46,
fontSize: 16, fontSize: 16,
fontWeight: 600, fontWeight: 600,
background: 'var(--color-primary)', background: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${alphaColor(token.colorPrimary, 0.86)} 100%)`,
border: 'none', border: 'none',
borderRadius: '12px', borderRadius: '12px',
boxShadow: 'var(--shadow-primary)', boxShadow: primaryButtonShadow,
}} }}
> >
</Button> </Button>
</Form.Item> </Form.Item>
</Form> </Form>
@@ -156,7 +166,7 @@ export default function Login() {
// 渲染LinuxDO登录 // 渲染LinuxDO登录
const renderLinuxDOLogin = () => ( const renderLinuxDOLogin = () => (
<div style={{ padding: '24px 0 8px' }}> <div>
<Button <Button
type="primary" type="primary"
size="large" size="large"
@@ -176,25 +186,25 @@ export default function Login() {
onClick={handleLinuxDOLogin} onClick={handleLinuxDOLogin}
block block
style={{ style={{
height: 52, height: 46,
fontSize: 16, fontSize: 16,
fontWeight: 600, fontWeight: 600,
background: 'var(--color-primary)', background: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${alphaColor(token.colorPrimary, 0.86)} 100%)`,
border: 'none', border: 'none',
borderRadius: '12px', borderRadius: '12px',
boxShadow: 'var(--shadow-primary)', boxShadow: primaryButtonShadow,
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = 'var(--shadow-elevated)'; e.currentTarget.style.boxShadow = hoverButtonShadow;
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'var(--shadow-primary)'; e.currentTarget.style.boxShadow = primaryButtonShadow;
}} }}
> >
使 LinuxDO 使 LinuxDO OAuth
</Button> </Button>
</div> </div>
); );
@@ -216,11 +226,34 @@ export default function Login() {
localStorage.setItem('announcement_hide_forever', 'true'); localStorage.setItem('announcement_hide_forever', 'true');
}; };
const currentLoginMethod = localAuthEnabled && linuxdoEnabled const loginTips = [
? activeLoginMethod '本地登录默认账号:admin / admin123',
: localAuthEnabled '首次 LinuxDO 登录会自动创建账号',
? 'local' '系统采用多用户数据隔离机制,每位用户拥有独立的创作空间与配置。',
: 'linuxdo'; ];
const featureItems = [
{
icon: <RobotOutlined />,
title: '多 AI 模型协同',
description: '支持 OpenAI、Gemini、Claude 等主流模型,按场景灵活切换。',
},
{
icon: <ThunderboltOutlined />,
title: '智能向导驱动',
description: '自动生成大纲、角色与世界观,快速搭建完整故事骨架。',
},
{
icon: <TeamOutlined />,
title: '角色组织管理',
description: '人物关系、组织架构可视化管理,复杂设定也能清晰掌控。',
},
{
icon: <BookOutlined />,
title: '章节创作闭环',
description: '支持章节生成、编辑、重写与润色,持续提升内容质量。',
},
];
return ( return (
<> <>
@@ -230,155 +263,241 @@ export default function Login() {
onDoNotShowToday={handleDoNotShowToday} onDoNotShowToday={handleDoNotShowToday}
onNeverShow={handleNeverShow} onNeverShow={handleNeverShow}
/> />
<div style={{ <Layout style={{ minHeight: '100vh', background: token.colorBgLayout }}>
display: 'flex', <div
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
background: 'var(--color-bg-base)',
padding: '20px',
position: 'relative',
overflow: 'hidden',
}}>
{/* 装饰性背景元素 */}
<div style={{
position: 'absolute',
top: '-10%',
right: '-5%',
width: '400px',
height: '400px',
background: 'var(--color-primary)',
opacity: 0.1,
borderRadius: '50%',
filter: 'blur(60px)',
}} />
<div style={{
position: 'absolute',
bottom: '-10%',
left: '-5%',
width: '350px',
height: '350px',
background: 'var(--color-success)',
opacity: 0.08,
borderRadius: '50%',
filter: 'blur(60px)',
}} />
<Card
style={{ style={{
width: '100%', position: 'fixed',
maxWidth: 420, top: 20,
background: 'var(--color-bg-container)', right: 20,
backdropFilter: 'blur(20px)', zIndex: 10,
WebkitBackdropFilter: 'blur(20px)', padding: '8px 10px',
boxShadow: 'var(--shadow-card)', borderRadius: 12,
border: '1px solid var(--color-border)', background: alphaColor(token.colorBgContainer, 0.9),
borderRadius: '16px', border: `1px solid ${token.colorBorderSecondary}`,
position: 'relative', backdropFilter: 'blur(6px)',
zIndex: 1,
}}
bodyStyle={{
padding: '40px 32px',
}} }}
> >
<Space direction="vertical" size="large" style={{ width: '100%', textAlign: 'center' }}> <ThemeSwitch size="small" />
{/* Logo区域 */} </div>
<div style={{ marginBottom: '8px' }}> <Row style={{ minHeight: '100vh' }}>
<div style={{ <Col xs={0} lg={11}>
width: '72px', <section
height: '72px', style={{
margin: '0 auto 20px', height: '100%',
background: 'var(--color-primary)', padding: '44px 64px 88px',
borderRadius: '20px', display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
position: 'relative',
overflow: 'hidden',
backgroundColor: alphaColor(token.colorBgContainer, 0.78),
backgroundImage: `linear-gradient(${alphaColor(token.colorTextSecondary, 0.06)} 1px, transparent 1px), linear-gradient(90deg, ${alphaColor(token.colorTextSecondary, 0.06)} 1px, transparent 1px)`,
backgroundSize: '68px 68px',
}}
>
<div
style={{
position: 'absolute',
inset: 0,
background: `radial-gradient(circle at 25% 20%, ${alphaColor(token.colorPrimary, 0.12)} 0%, transparent 50%)`,
pointerEvents: 'none',
}}
/>
<div
style={{
position: 'relative',
zIndex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
gap: 34,
width: '100%',
// flex: 1,
}}
>
<Space align="center" size={14}>
<div
style={{
width: 46,
height: 46,
borderRadius: 14,
background: `linear-gradient(135deg, ${token.colorPrimary} 0%, ${alphaColor(token.colorPrimary, 0.7)} 100%)`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
boxShadow: 'var(--shadow-primary)', boxShadow: primaryButtonShadow,
}}> }}
>
<img <img
src="/logo.svg" src="/logo.svg"
alt="Logo" alt="MuMuAINovel"
style={{ style={{ width: 26, height: 26, filter: 'brightness(0) invert(1)' }}
width: '48px',
height: '48px',
filter: 'brightness(0) invert(1)',
}}
/> />
</div> </div>
<Title level={2} style={{ <Title level={3} style={{ margin: 0, color: token.colorText }}>
marginBottom: 8, MuMuAINovel
color: 'var(--color-primary)',
fontWeight: 700,
}}>
AI小说创作助手
</Title> </Title>
<Paragraph style={{ </Space>
color: 'var(--color-text-secondary)',
fontSize: '14px', <Space direction="vertical" size={32} style={{ width: '100%' }}>
<div style={{ maxWidth: 'min(860px, 100%)' }}>
<Title
level={1}
style={{
marginBottom: 22,
color: token.colorText,
lineHeight: 1.12,
fontWeight: 800,
fontSize: 'clamp(52px, 3vw, 78px)',
}}
>
AI
<br />
<span
style={{
backgroundImage: `linear-gradient(90deg, ${token.colorPrimary} 0%, #d946ef 100%)`,
WebkitBackgroundClip: 'text',
backgroundClip: 'text',
WebkitTextFillColor: 'transparent',
color: token.colorPrimary,
}}
>
</span>
</Title>
<Paragraph
style={{
fontSize: 'clamp(18px, 1vw, 22px)',
lineHeight: 1.85,
color: token.colorTextSecondary,
marginBottom: 0, marginBottom: 0,
}}> maxWidth: 800,
{localAuthEnabled && linuxdoEnabled ? '选择登录方式' : }}
localAuthEnabled ? '使用账户密码登录' : >
'使用 LinuxDO 账号登录'} 稿
</Paragraph> </Paragraph>
</div> </div>
{/* 登录方式 */} <Row gutter={[20, 20]} style={{ width: '100%', maxWidth: 'min(920px, 100%)' }}>
{localAuthEnabled && linuxdoEnabled ? ( {featureItems.map((item) => (
<Tabs <Col span={12} key={item.title}>
activeKey={activeLoginMethod} <Card
onChange={(key) => setActiveLoginMethod(key as 'local' | 'linuxdo')} size="small"
centered bordered={false}
items={[ style={{
{ height: '100%',
key: 'local', minHeight: 120,
label: '账户密码', borderRadius: 16,
children: renderLocalLogin(), background: alphaColor(token.colorBgContainer, 0.9),
}, }}
{ bodyStyle={{ padding: 16 }}
key: 'linuxdo', >
label: 'LinuxDO', <Space direction="vertical" size={8}>
children: renderLinuxDOLogin(), <Space size={10} style={{ color: token.colorPrimary, fontWeight: 700, fontSize: 15 }}>
}, {item.icon}
]} <span>{item.title}</span>
/> </Space>
) : localAuthEnabled ? ( <Paragraph style={{ marginBottom: 0, color: token.colorTextSecondary, fontSize: 14, lineHeight: 1.65 }}>
renderLocalLogin() {item.description}
) : (
renderLinuxDOLogin()
)}
{/* 提示信息 */}
<div style={{
padding: '16px',
background: 'rgba(77, 128, 136, 0.08)',
borderRadius: '12px',
border: '1px solid var(--color-border)',
}}>
<Paragraph style={{
fontSize: 13,
color: 'var(--color-text-secondary)',
marginBottom: 0,
lineHeight: 1.6,
}}>
{currentLoginMethod === 'linuxdo' ? (
<>
🎉
<br />
🔒
</>
) : (
<>
🧪 admin / admin123
<br />
🔒
</>
)}
</Paragraph> </Paragraph>
</div>
</Space> </Space>
</Card> </Card>
</Col>
))}
</Row>
</Space>
<Space size={[10, 14]} wrap style={{ maxWidth: 'min(860px, 100%)' }}>
<Tag color="blue">OpenAI</Tag>
<Tag color="geekblue">Gemini</Tag>
<Tag color="purple">Claude</Tag>
<Tag color="cyan">LinuxDO OAuth</Tag>
<Tag color="green">Docker Compose</Tag>
<Tag color="gold">PostgreSQL</Tag>
</Space>
</div> </div>
<Paragraph
style={{
marginBottom: 0,
fontSize: 12,
color: token.colorTextTertiary,
position: 'relative',
zIndex: 1,
letterSpacing: 0.4,
}}
>
© 2026 MuMuAINovel · GPLv3 License
</Paragraph>
</section>
</Col>
<Col xs={24} lg={13}>
<section
style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '48px min(7vw, 72px)',
background: token.colorBgLayout,
}}
>
<div style={{ width: '100%', maxWidth: 480 }}>
<Space direction="vertical" size={4}>
<Title level={2} style={{ marginBottom: 0, fontWeight: 700, color: token.colorText }}>
</Title>
<Paragraph style={{ marginBottom: 0, color: token.colorTextSecondary }}>
MuMuAINovel
</Paragraph>
</Space>
<div style={{ marginTop: 22 }}>
{localAuthEnabled ? renderLocalLogin() : null}
{linuxdoEnabled && localAuthEnabled ? (
<>
<Divider style={{ margin: '18px 0 16px' }}></Divider>
{renderLinuxDOLogin()}
</>
) : null}
{!localAuthEnabled && linuxdoEnabled ? renderLinuxDOLogin() : null}
{!localAuthEnabled && !linuxdoEnabled ? (
<Alert
type="warning"
showIcon
message="当前未启用可用登录方式"
description="请联系管理员在系统配置中启用本地登录或 LinuxDO OAuth 登录。"
/>
) : null}
<Divider style={{ margin: '20px 0 14px' }} />
<Alert
type="info"
showIcon
icon={<SafetyCertificateOutlined />}
style={{ background: alphaColor(token.colorPrimary, 0.06), borderRadius: 12 }}
message="登录说明"
description={(
<ul style={{ margin: 0, paddingLeft: 18 }}>
{loginTips.map((tip) => (
<li key={tip} style={{ marginBottom: 4 }}>
{tip}
</li>
))}
</ul>
)}
/>
</div>
</div>
</section>
</Col>
</Row>
</Layout>
</> </>
); );
} }