Files
new-api-oiss/web/src/components/auth/RegisterForm.jsx
lichao e4a3b1bd29 feat: customize frontend with OasisRelay branding and UI enhancements
- Rebrand hero section with OasisRelay title, gradient text, and uptime counter
- Add transparent-to-frosted-glass nav bar effect on home page scroll
- Add grid square background pattern on home page
- Simplify nav items to: 首页, 控制台, 定价, 文档
- Change 分组监控 to Activity icon button opening external status page
- Switch font family to PingFang SC with cross-platform fallbacks
- Fix username visibility in light mode nav bar
- Restyle login/register forms and home page sections

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 23:40:56 +08:00

697 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import {
API,
getLogo,
showError,
showInfo,
showSuccess,
updateAPI,
getSystemName,
getOAuthProviderIcon,
setUserData,
onDiscordOAuthClicked,
onCustomOAuthClicked,
} from '../../helpers';
import Turnstile from 'react-turnstile';
import {
Button,
Checkbox,
Form,
Icon,
Modal,
} from '@douyinfe/semi-ui';
import {
IconGithubLogo,
IconMail,
} from '@douyinfe/semi-icons';
import {
onGitHubOAuthClicked,
onLinuxDOOAuthClicked,
onOIDCClicked,
} from '../../helpers';
import OIDCIcon from '../common/logo/OIDCIcon';
import LinuxDoIcon from '../common/logo/LinuxDoIcon';
import WeChatIcon from '../common/logo/WeChatIcon';
import TelegramLoginButton from 'react-telegram-login/src';
import { UserContext } from '../../context/User';
import { StatusContext } from '../../context/Status';
import { useTranslation } from 'react-i18next';
import { SiDiscord } from 'react-icons/si';
const RegisterForm = () => {
let navigate = useNavigate();
const { t } = useTranslation();
const githubButtonTextKeyByState = {
idle: '使用 GitHub 继续',
redirecting: '正在跳转 GitHub...',
timeout: '请求超时,请刷新页面后重新发起 GitHub 登录',
};
const [inputs, setInputs] = useState({
username: '',
password: '',
password2: '',
email: '',
verification_code: '',
wechat_verification_code: '',
});
const { username, password, password2 } = inputs;
const [userState, userDispatch] = useContext(UserContext);
const [statusState] = useContext(StatusContext);
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState('');
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
const [showEmailRegister, setShowEmailRegister] = useState(false);
const [wechatLoading, setWechatLoading] = useState(false);
const [githubLoading, setGithubLoading] = useState(false);
const [discordLoading, setDiscordLoading] = useState(false);
const [oidcLoading, setOidcLoading] = useState(false);
const [linuxdoLoading, setLinuxdoLoading] = useState(false);
const [emailRegisterLoading, setEmailRegisterLoading] = useState(false);
const [registerLoading, setRegisterLoading] = useState(false);
const [verificationCodeLoading, setVerificationCodeLoading] = useState(false);
const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] =
useState(false);
const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false);
const [customOAuthLoading, setCustomOAuthLoading] = useState({});
const [disableButton, setDisableButton] = useState(false);
const [countdown, setCountdown] = useState(30);
const [agreedToTerms, setAgreedToTerms] = useState(false);
const [hasUserAgreement, setHasUserAgreement] = useState(false);
const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false);
const [githubButtonState, setGithubButtonState] = useState('idle');
const [githubButtonDisabled, setGithubButtonDisabled] = useState(false);
const githubTimeoutRef = useRef(null);
const githubButtonText = t(githubButtonTextKeyByState[githubButtonState]);
const logo = getLogo();
const systemName = getSystemName();
let affCode = new URLSearchParams(window.location.search).get('aff');
if (affCode) {
localStorage.setItem('aff', affCode);
}
const status = useMemo(() => {
if (statusState?.status) return statusState.status;
const savedStatus = localStorage.getItem('status');
if (!savedStatus) return {};
try {
return JSON.parse(savedStatus) || {};
} catch (err) {
return {};
}
}, [statusState?.status]);
const hasCustomOAuthProviders =
(status.custom_oauth_providers || []).length > 0;
const hasOAuthRegisterOptions = Boolean(
status.github_oauth ||
status.discord_oauth ||
status.oidc_enabled ||
status.wechat_login ||
status.linuxdo_oauth ||
status.telegram_oauth ||
hasCustomOAuthProviders,
);
const [showEmailVerification, setShowEmailVerification] = useState(false);
useEffect(() => {
setShowEmailVerification(!!status?.email_verification);
if (status?.turnstile_check) {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
// 从 status 获取用户协议和隐私政策的启用状态
setHasUserAgreement(status?.user_agreement_enabled || false);
setHasPrivacyPolicy(status?.privacy_policy_enabled || false);
}, [status]);
useEffect(() => {
let countdownInterval = null;
if (disableButton && countdown > 0) {
countdownInterval = setInterval(() => {
setCountdown(countdown - 1);
}, 1000);
} else if (countdown === 0) {
setDisableButton(false);
setCountdown(30);
}
return () => clearInterval(countdownInterval); // Clean up on unmount
}, [disableButton, countdown]);
useEffect(() => {
return () => {
if (githubTimeoutRef.current) {
clearTimeout(githubTimeoutRef.current);
}
};
}, []);
const onWeChatLoginClicked = () => {
setWechatLoading(true);
setShowWeChatLoginModal(true);
setWechatLoading(false);
};
const onSubmitWeChatVerificationCode = async () => {
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试Turnstile 正在检查用户环境!');
return;
}
setWechatCodeSubmitLoading(true);
try {
const res = await API.get(
`/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
);
const { success, message, data } = res.data;
if (success) {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
setUserData(data);
updateAPI();
navigate('/');
showSuccess('登录成功!');
setShowWeChatLoginModal(false);
} else {
showError(message);
}
} catch (error) {
showError('登录失败,请重试');
} finally {
setWechatCodeSubmitLoading(false);
}
};
function handleChange(name, value) {
setInputs((inputs) => ({ ...inputs, [name]: value }));
}
async function handleSubmit(e) {
if (password.length < 8) {
showInfo('密码长度不得小于 8 位!');
return;
}
if (password !== password2) {
showInfo('两次输入的密码不一致');
return;
}
if (username && password) {
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试Turnstile 正在检查用户环境!');
return;
}
setRegisterLoading(true);
try {
if (!affCode) {
affCode = localStorage.getItem('aff');
}
inputs.aff_code = affCode;
const res = await API.post(
`/api/user/register?turnstile=${turnstileToken}`,
inputs,
);
const { success, message } = res.data;
if (success) {
navigate('/login');
showSuccess('注册成功!');
} else {
showError(message);
}
} catch (error) {
showError('注册失败,请重试');
} finally {
setRegisterLoading(false);
}
}
}
const sendVerificationCode = async () => {
if (inputs.email === '') return;
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试Turnstile 正在检查用户环境!');
return;
}
setVerificationCodeLoading(true);
try {
const res = await API.get(
`/api/verification?email=${encodeURIComponent(inputs.email)}&turnstile=${turnstileToken}`,
);
const { success, message } = res.data;
if (success) {
showSuccess('验证码发送成功,请检查你的邮箱!');
setDisableButton(true); // 发送成功后禁用按钮,开始倒计时
} else {
showError(message);
}
} catch (error) {
showError('发送验证码失败,请重试');
} finally {
setVerificationCodeLoading(false);
}
};
const handleGitHubClick = () => {
if (githubButtonDisabled) {
return;
}
setGithubLoading(true);
setGithubButtonDisabled(true);
setGithubButtonState('redirecting');
if (githubTimeoutRef.current) {
clearTimeout(githubTimeoutRef.current);
}
githubTimeoutRef.current = setTimeout(() => {
setGithubLoading(false);
setGithubButtonState('timeout');
setGithubButtonDisabled(true);
}, 20000);
try {
onGitHubOAuthClicked(status.github_client_id, { shouldLogout: true });
} finally {
setTimeout(() => setGithubLoading(false), 3000);
}
};
const handleDiscordClick = () => {
setDiscordLoading(true);
try {
onDiscordOAuthClicked(status.discord_client_id, { shouldLogout: true });
} finally {
setTimeout(() => setDiscordLoading(false), 3000);
}
};
const handleOIDCClick = () => {
setOidcLoading(true);
try {
onOIDCClicked(
status.oidc_authorization_endpoint,
status.oidc_client_id,
false,
{ shouldLogout: true },
);
} finally {
setTimeout(() => setOidcLoading(false), 3000);
}
};
const handleLinuxDOClick = () => {
setLinuxdoLoading(true);
try {
onLinuxDOOAuthClicked(status.linuxdo_client_id, { shouldLogout: true });
} finally {
setTimeout(() => setLinuxdoLoading(false), 3000);
}
};
const handleCustomOAuthClick = (provider) => {
setCustomOAuthLoading((prev) => ({ ...prev, [provider.slug]: true }));
try {
onCustomOAuthClicked(provider, { shouldLogout: true });
} finally {
setTimeout(() => {
setCustomOAuthLoading((prev) => ({ ...prev, [provider.slug]: false }));
}, 3000);
}
};
const handleEmailRegisterClick = () => {
setEmailRegisterLoading(true);
setShowEmailRegister(true);
setEmailRegisterLoading(false);
};
const handleOtherRegisterOptionsClick = () => {
setOtherRegisterOptionsLoading(true);
setShowEmailRegister(false);
setOtherRegisterOptionsLoading(false);
};
const onTelegramLoginClicked = async (response) => {
const fields = [
'id',
'first_name',
'last_name',
'username',
'photo_url',
'auth_date',
'hash',
'lang',
];
const params = {};
fields.forEach((field) => {
if (response[field]) {
params[field] = response[field];
}
});
try {
const res = await API.get(`/api/oauth/telegram/login`, { params });
const { success, message, data } = res.data;
if (success) {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
showSuccess('登录成功!');
setUserData(data);
updateAPI();
navigate('/');
} else {
showError(message);
}
} catch (error) {
showError('登录失败,请重试');
}
};
const renderOAuthOptions = () => {
return (
<div className='lp-auth-card'>
<div className='lp-auth-logo-row'>
<img src={logo} alt='Logo' style={{width:'32px',height:'32px',borderRadius:'8px',objectFit:'cover'}} />
<span className='lp-auth-brand'>{systemName}</span>
</div>
<h1 className='lp-auth-title'>{t('创建新账号')}</h1>
<p className='lp-auth-sub'> <Link to='/login' className='lp-auth-link'>{t('登录已有账号')}</Link></p>
<div className='lp-auth-oauth-list'>
{status.wechat_login && (
<Button
theme='outline'
className='lp-auth-oauth-btn'
type='tertiary'
icon={<Icon svg={<WeChatIcon />} style={{ color: '#07C160' }} />}
onClick={onWeChatLoginClicked}
loading={wechatLoading}
>
<span className='ml-3'>{t('使用 微信 继续')}</span>
</Button>
)}
{status.github_oauth && (
<Button
theme='outline'
className='lp-auth-oauth-btn'
type='tertiary'
icon={<IconGithubLogo size='large' />}
onClick={handleGitHubClick}
loading={githubLoading}
disabled={githubButtonDisabled}
>
<span className='ml-3'>{githubButtonText}</span>
</Button>
)}
{status.discord_oauth && (
<Button
theme='outline'
className='lp-auth-oauth-btn'
type='tertiary'
icon={<SiDiscord style={{ color: '#5865F2', width: '20px', height: '20px' }} />}
onClick={handleDiscordClick}
loading={discordLoading}
>
<span className='ml-3'>{t('使用 Discord 继续')}</span>
</Button>
)}
{status.oidc_enabled && (
<Button
theme='outline'
className='lp-auth-oauth-btn'
type='tertiary'
icon={<OIDCIcon style={{ color: '#1877F2' }} />}
onClick={handleOIDCClick}
loading={oidcLoading}
>
<span className='ml-3'>{t('使用 OIDC 继续')}</span>
</Button>
)}
{status.linuxdo_oauth && (
<Button
theme='outline'
className='lp-auth-oauth-btn'
type='tertiary'
icon={<LinuxDoIcon style={{ color: '#E95420', width: '20px', height: '20px' }} />}
onClick={handleLinuxDOClick}
loading={linuxdoLoading}
>
<span className='ml-3'>{t('使用 LinuxDO 继续')}</span>
</Button>
)}
{status.custom_oauth_providers &&
status.custom_oauth_providers.map((provider) => (
<Button
key={provider.slug}
theme='outline'
className='lp-auth-oauth-btn'
type='tertiary'
icon={getOAuthProviderIcon(provider.icon || '', 20)}
onClick={() => handleCustomOAuthClick(provider)}
loading={customOAuthLoading[provider.slug]}
>
<span className='ml-3'>{t('使用 {{name}} 继续', { name: provider.name })}</span>
</Button>
))}
{status.telegram_oauth && (
<div className='flex justify-center my-2'>
<TelegramLoginButton
dataOnauth={onTelegramLoginClicked}
botName={status.telegram_bot_name}
/>
</div>
)}
<div className='lp-auth-divider'><span>{t('或')}</span></div>
<Button
theme='solid'
type='primary'
className='lp-auth-oauth-btn'
icon={<IconMail size='large' />}
onClick={handleEmailRegisterClick}
loading={emailRegisterLoading}
>
<span className='ml-3'>{t('使用 用户名 注册')}</span>
</Button>
</div>
</div>
);
};
const renderEmailRegisterForm = () => {
return (
<div className='lp-auth-card'>
<div className='lp-auth-logo-row'>
<img src={logo} alt='Logo' style={{width:'32px',height:'32px',borderRadius:'8px',objectFit:'cover'}} />
<span className='lp-auth-brand'>{systemName}</span>
</div>
<h1 className='lp-auth-title'>{t('创建新账号')}</h1>
<p className='lp-auth-sub'> <Link to='/login' className='lp-auth-link'>{t('登录已有账号')}</Link></p>
<div className='lp-form-group'>
<span className='lp-form-icon'>👤</span>
<input
type='text'
placeholder={t('请输入用户名')}
name='username'
autoComplete='nickname'
onChange={(e) => handleChange('username', e.target.value)}
className='lp-input'
/>
</div>
<div className='lp-form-group'>
<span className='lp-form-icon'>🔒</span>
<input
type='password'
placeholder={t('输入密码,最短 8 位,最长 20 位')}
name='password'
autoComplete='new-password'
onChange={(e) => handleChange('password', e.target.value)}
className='lp-input'
/>
</div>
<div className='lp-form-group'>
<span className='lp-form-icon'>🔒</span>
<input
type='password'
placeholder={t('确认密码')}
name='password2'
autoComplete='new-password'
onChange={(e) => handleChange('password2', e.target.value)}
className='lp-input'
/>
</div>
{showEmailVerification && (
<>
<div className='lp-form-group'>
<span className='lp-form-icon'></span>
<input
type='email'
placeholder={t('输入邮箱地址')}
name='email'
autoComplete='email'
onChange={(e) => handleChange('email', e.target.value)}
className='lp-input'
/>
</div>
<div className='lp-code-row'>
<div className='lp-form-group' style={{flex:1,margin:0}}>
<span className='lp-form-icon'>🔑</span>
<input
type='text'
placeholder={t('输入验证码')}
name='verification_code'
onChange={(e) => handleChange('verification_code', e.target.value)}
className='lp-input'
/>
</div>
<button
className='lp-btn-send-code'
onClick={sendVerificationCode}
disabled={disableButton || verificationCodeLoading}
>
{disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')}
</button>
</div>
</>
)}
{(hasUserAgreement || hasPrivacyPolicy) && (
<div className='lp-terms-row'>
<Checkbox checked={agreedToTerms} onChange={(e) => setAgreedToTerms(e.target.checked)}>
<span className='lp-auth-terms-text'>
{t('我已阅读并同意')}
{hasUserAgreement && (
<a href='/user-agreement' target='_blank' rel='noopener noreferrer' className='lp-auth-link'> {t('用户协议')}</a>
)}
{hasUserAgreement && hasPrivacyPolicy && t('和')}
{hasPrivacyPolicy && (
<a href='/privacy-policy' target='_blank' rel='noopener noreferrer' className='lp-auth-link'> {t('隐私政策')}</a>
)}
</span>
</Checkbox>
</div>
)}
<button
className='lp-btn-submit'
onClick={handleSubmit}
disabled={registerLoading || ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms)}
>
{registerLoading ? t('注册中...') : t('下一步')}
</button>
{hasOAuthRegisterOptions && (
<>
<div className='lp-auth-divider'><span>{t('或')}</span></div>
<button className='lp-btn-other-options' onClick={handleOtherRegisterOptionsClick}>
{t('其他注册选项')}
</button>
</>
)}
<div className='lp-benefits'>
<div className='lp-benefits-title'>注册后您将获得</div>
<div className='lp-benefit-item'>使用量统计面板</div>
<div className='lp-benefit-item'>灵活的订阅方案</div>
<div className='lp-benefit-item'>200+ 模型一键接入</div>
<div className='lp-benefit-item'>专属 API Token</div>
</div>
</div>
);
};
const renderWeChatLoginModal = () => {
return (
<Modal
title={t('微信扫码登录')}
visible={showWeChatLoginModal}
maskClosable={true}
onOk={onSubmitWeChatVerificationCode}
onCancel={() => setShowWeChatLoginModal(false)}
okText={t('登录')}
centered={true}
okButtonProps={{
loading: wechatCodeSubmitLoading,
}}
>
<div className='flex flex-col items-center'>
<img src={status.wechat_qrcode} alt='微信二维码' className='mb-4' />
</div>
<div className='text-center mb-4'>
<p>
{t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
</p>
</div>
<Form>
<Form.Input
field='wechat_verification_code'
placeholder={t('验证码')}
label={t('验证码')}
value={inputs.wechat_verification_code}
onChange={(value) =>
handleChange('wechat_verification_code', value)
}
/>
</Form>
</Modal>
);
};
return (
<div className='lp-auth-page' style={{ minHeight: 'calc(100vh - 56px)' }}>
<div className='lp-auth-main'>
{showEmailRegister ||
!hasOAuthRegisterOptions
? renderEmailRegisterForm()
: renderOAuthOptions()}
{renderWeChatLoginModal()}
{turnstileEnabled && (
<div className='flex justify-center mt-6'>
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
</div>
)}
</div>
</div>
);
};
export default RegisterForm;