import { useEffect, useMemo, useState } from 'react'; import { Alert, Button, Card, Col, Divider, Form, Input, Layout, Row, Space, Spin, Tabs, Tag, Typography, message, theme, } from 'antd'; import { BookOutlined, LockOutlined, MailOutlined, RobotOutlined, SafetyCertificateOutlined, TeamOutlined, ThunderboltOutlined, UserOutlined, } from '@ant-design/icons'; import { authApi } from '../services/api'; import { useNavigate, useSearchParams } from 'react-router-dom'; import AnnouncementModal from '../components/AnnouncementModal'; import ThemeSwitch from '../components/ThemeSwitch'; const { Title, Paragraph, Text } = Typography; interface AuthConfig { local_auth_enabled: boolean; linuxdo_enabled: boolean; email_auth_enabled: boolean; email_register_enabled: boolean; } interface LocalLoginValues { username: string; password: string; } interface EmailLoginValues { email: string; code: string; } interface EmailRegisterValues { email: string; code: string; password: string; confirmPassword: string; display_name?: string; } interface ResetPasswordValues { email: string; code: string; new_password: string; confirmNewPassword: string; } export default function Login() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [loading, setLoading] = useState(false); const [checking, setChecking] = useState(true); const [authConfig, setAuthConfig] = useState({ local_auth_enabled: false, linuxdo_enabled: false, email_auth_enabled: false, email_register_enabled: false, }); const [localForm] = Form.useForm(); const [emailLoginForm] = Form.useForm(); const [emailRegisterForm] = Form.useForm(); const [resetPasswordForm] = 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 [loginCodeSending, setLoginCodeSending] = useState(false); const [registerCodeSending, setRegisterCodeSending] = useState(false); const [resetCodeSending, setResetCodeSending] = useState(false); const [loginCountdown, setLoginCountdown] = useState(0); const [registerCountdown, setRegisterCountdown] = useState(0); const [resetCountdown, setResetCountdown] = useState(0); const [showResetPassword, setShowResetPassword] = useState(false); const localAuthEnabled = authConfig.local_auth_enabled; const linuxdoEnabled = authConfig.linuxdo_enabled; const emailAuthEnabled = authConfig.email_auth_enabled; const emailRegisterEnabled = authConfig.email_register_enabled; useEffect(() => { const timers = [ { value: loginCountdown, setter: setLoginCountdown }, { value: registerCountdown, setter: setRegisterCountdown }, { value: resetCountdown, setter: setResetCountdown }, ].map(({ value, setter }) => { if (value <= 0) { return null; } return window.setInterval(() => { setter((prev) => { if (prev <= 1) { return 0; } return prev - 1; }); }, 1000); }); return () => { timers.forEach((timer) => { if (timer) { window.clearInterval(timer); } }); }; }, [loginCountdown, registerCountdown, resetCountdown]); useEffect(() => { const checkAuth = async () => { try { await authApi.getCurrentUser(); const redirect = searchParams.get('redirect') || '/'; navigate(redirect); } catch { try { const config = await authApi.getAuthConfig(); setAuthConfig(config); } catch (error) { console.error('获取认证配置失败:', error); setAuthConfig({ local_auth_enabled: false, linuxdo_enabled: true, email_auth_enabled: false, email_register_enabled: false, }); } setChecking(false); } }; checkAuth(); }, [navigate, searchParams]); const handleLoginSuccess = () => { message.success('登录成功!'); const hideForever = localStorage.getItem('announcement_hide_forever'); const hideToday = localStorage.getItem('announcement_hide_today'); const today = new Date().toDateString(); if (hideForever === 'true' || hideToday === today) { const redirect = searchParams.get('redirect') || '/'; navigate(redirect); } else { setShowAnnouncement(true); } }; const handleLocalLogin = async (values: LocalLoginValues) => { try { setLoading(true); const response = await authApi.localLogin(values.username, values.password); if (response.success) { handleLoginSuccess(); } } catch (error) { console.error('本地登录失败:', error); } finally { setLoading(false); } }; const handleEmailLogin = async (values: EmailLoginValues) => { try { setLoading(true); const response = await authApi.emailLogin({ email: values.email, code: values.code, }); if (response.success) { handleLoginSuccess(); } } catch (error) { console.error('邮箱验证码登录失败:', error); } finally { setLoading(false); } }; const sendLoginCode = async () => { try { const values = await emailLoginForm.validateFields(['email']); setLoginCodeSending(true); const result = await authApi.sendEmailCode({ email: values.email, scene: 'login' }); message.success(result.message || '验证码已发送'); setLoginCountdown(result.resend_interval_seconds || 60); } catch (error) { console.error('发送 login 验证码失败:', error); } finally { setLoginCodeSending(false); } }; const sendRegisterCode = async () => { try { const values = await emailRegisterForm.validateFields(['email']); setRegisterCodeSending(true); const result = await authApi.sendEmailCode({ email: values.email, scene: 'register' }); message.success(result.message || '验证码已发送'); setRegisterCountdown(result.resend_interval_seconds || 60); } catch (error) { console.error('发送 register 验证码失败:', error); } finally { setRegisterCodeSending(false); } }; const sendResetCode = async () => { try { const values = await resetPasswordForm.validateFields(['email']); setResetCodeSending(true); const result = await authApi.sendEmailCode({ email: values.email, scene: 'reset_password' }); message.success(result.message || '验证码已发送'); setResetCountdown(result.resend_interval_seconds || 60); } catch (error) { console.error('发送 reset_password 验证码失败:', error); } finally { setResetCodeSending(false); } }; const handleEmailRegister = async (values: EmailRegisterValues) => { try { setLoading(true); const response = await authApi.emailRegister({ email: values.email, code: values.code, password: values.password, display_name: values.display_name?.trim() || undefined, }); if (response.success) { message.success('注册成功,已自动登录'); emailRegisterForm.resetFields(['code', 'password', 'confirmPassword']); setRegisterCountdown(0); handleLoginSuccess(); } } catch (error) { console.error('邮箱注册失败:', error); } finally { setLoading(false); } }; const handleResetPassword = async (values: ResetPasswordValues) => { try { setLoading(true); const result = await authApi.resetEmailPassword({ email: values.email, code: values.code, new_password: values.new_password, }); message.success(result.message || '密码重置成功'); resetPasswordForm.resetFields(['code', 'new_password', 'confirmNewPassword']); setResetCountdown(0); setShowResetPassword(false); } catch (error) { console.error('重置密码失败:', error); } finally { setLoading(false); } }; const handleLinuxDOLogin = async () => { try { setLoading(true); const response = await authApi.getLinuxDOAuthUrl(); const redirect = searchParams.get('redirect'); if (redirect) { sessionStorage.setItem('login_redirect', redirect); } window.location.href = response.auth_url; } catch (error) { console.error('获取授权地址失败:', error); message.error('获取授权地址失败,请稍后重试'); setLoading(false); } }; const handleAnnouncementClose = () => { setShowAnnouncement(false); const redirect = searchParams.get('redirect') || '/'; navigate(redirect); }; const handleDoNotShowToday = () => { const today = new Date().toDateString(); localStorage.setItem('announcement_hide_today', today); }; const handleNeverShow = () => { localStorage.setItem('announcement_hide_forever', 'true'); }; const loginTips = useMemo(() => { const tips = [ '首次 LinuxDO 登录会自动创建账号。', ]; if (localAuthEnabled) { tips.unshift('本地登录默认账号:admin / admin123'); } if (emailAuthEnabled) { tips.push('邮箱注册用户支持通过邮箱验证码重置密码。'); } return tips; }, [emailAuthEnabled, localAuthEnabled]); const featureItems = [ { icon: , title: '多 AI 模型协同', description: '支持 OpenAI、Gemini、Claude 等主流模型,按场景灵活切换。', }, { icon: , title: '智能向导驱动', description: '自动生成大纲、角色与世界观,快速搭建完整故事骨架。', }, { icon: , title: '角色组织管理', description: '人物关系、组织架构可视化管理,复杂设定也能清晰掌控。', }, { icon: , title: '章节创作闭环', description: '支持章节生成、编辑、重写与润色,持续提升内容质量。', }, ]; const renderLocalLogin = () => ( <>
} placeholder="请输入管理账号/邮箱" autoComplete="username" style={{ height: 46, borderRadius: 12 }} /> } placeholder="请输入访问密钥" autoComplete="current-password" style={{ height: 46, borderRadius: 12 }} />
{linuxdoEnabled ? ( <> 第三方登录 {renderLinuxDOLogin()} ) : null} ); const renderEmailLogin = () => { if (showResetPassword) { return (
忘记密码 / 重置密码
} placeholder="请输入注册邮箱" /> } placeholder="请输入新密码" /> ({ validator(_, value) { if (!value || getFieldValue('new_password') === value) { return Promise.resolve(); } return Promise.reject(new Error('两次输入的新密码不一致')); }, }), ]} > } placeholder="请再次输入新密码" />
); } return (
} placeholder="请输入已注册邮箱" autoComplete="email" style={{ height: 46, borderRadius: 12 }} /> } placeholder="请输入 6 位登录验证码" maxLength={6} style={{ height: 46, borderRadius: '12px 0 0 12px' }} />
); }; const renderEmailRegister = () => (
} placeholder="请输入注册邮箱" autoComplete="email" style={{ height: 46, borderRadius: 12 }} /> } placeholder="请输入 6 位验证码" maxLength={6} style={{ height: 46, borderRadius: '12px 0 0 12px' }} /> } placeholder="选填,默认使用邮箱前缀" autoComplete="nickname" style={{ height: 46, borderRadius: 12 }} /> } placeholder="请输入登录密码" autoComplete="new-password" style={{ height: 46, borderRadius: 12 }} /> ({ validator(_, value) { if (!value || getFieldValue('password') === value) { return Promise.resolve(); } return Promise.reject(new Error('两次输入的密码不一致')); }, }), ]} > } placeholder="请再次输入登录密码" autoComplete="new-password" style={{ height: 46, borderRadius: 12 }} /> 验证码将发送到你填写的邮箱,若未收到请检查垃圾箱或稍后重试。注册后可通过邮箱验证码登录,也支持邮箱重置密码。
); const renderLinuxDOLogin = () => (
); const authTabs = [ ...(localAuthEnabled ? [ { key: 'local-login', label: '本地登录', children: renderLocalLogin(), }, ] : []), ...(emailAuthEnabled ? [ { key: 'email-login', label: '邮箱登录', children: renderEmailLogin(), }, ] : []), ...(emailAuthEnabled && emailRegisterEnabled ? [ { key: 'email-register', label: '邮箱注册', children: renderEmailRegister(), }, ] : []), ]; if (checking) { return (
); } return ( <>
墨木灵思
墨木灵思
基于 AI 的 <br /> <span style={{ backgroundImage: `linear-gradient(90deg, ${token.colorPrimary} 0%, #d946ef 100%)`, WebkitBackgroundClip: 'text', backgroundClip: 'text', WebkitTextFillColor: 'transparent', color: token.colorPrimary, }} > 智能小说创作助手 </span> 从灵感到成稿,围绕「多模型协同、创作流程自动化、角色关系管理、章节精修」构建一体化创作工作台。
{featureItems.map((item) => ( {item.icon} {item.title} {item.description} ))}
OpenAI Gemini Claude LinuxDO OAuth Docker Compose PostgreSQL
© 2026 墨木灵思 · GPLv3 License
欢迎回来 登录 墨木灵思,继续你的小说创作项目。
{authTabs.length > 0 ? ( ) : null} {!localAuthEnabled && !linuxdoEnabled && !emailAuthEnabled ? ( ) : null} {emailAuthEnabled && !emailRegisterEnabled ? ( ) : null} } style={{ background: alphaColor(token.colorPrimary, 0.06), borderRadius: 12 }} message="登录说明" description={(
    {loginTips.map((tip) => (
  • {tip}
  • ))}
)} />
); }