chore: drop leftover login/register template drafts and debug scripts; track Register pages and icon variants
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
/* ============ Deep Space Register - 共享样式 ============ */
|
||||
|
||||
.register-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #0a0e1a;
|
||||
}
|
||||
|
||||
/* 星空背景 */
|
||||
.register-stars {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.register-star {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
animation: regTwinkle 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes regTwinkle {
|
||||
0%, 100% { opacity: 0.15; }
|
||||
50% { opacity: 0.9; }
|
||||
}
|
||||
|
||||
/* 浮动光球 */
|
||||
.register-orb {
|
||||
position: fixed;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
.register-orb-1 {
|
||||
width: 300px; height: 300px;
|
||||
background: #667eea;
|
||||
top: -80px; right: -60px;
|
||||
animation: regOrbFloat1 8s ease-in-out infinite;
|
||||
}
|
||||
.register-orb-2 {
|
||||
width: 250px; height: 250px;
|
||||
background: #764ba2;
|
||||
bottom: 80px; left: -80px;
|
||||
animation: regOrbFloat2 10s ease-in-out infinite;
|
||||
}
|
||||
.register-orb-3 {
|
||||
width: 180px; height: 180px;
|
||||
background: #f093fb;
|
||||
top: 50%; right: -30px;
|
||||
animation: regOrbFloat3 12s ease-in-out infinite;
|
||||
}
|
||||
@keyframes regOrbFloat1 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(-30px, 40px) scale(1.1); }
|
||||
}
|
||||
@keyframes regOrbFloat2 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(40px, -30px) scale(1.15); }
|
||||
}
|
||||
@keyframes regOrbFloat3 {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(-20px, -40px) scale(0.9); }
|
||||
}
|
||||
|
||||
/* 琉璃卡片 */
|
||||
.register-glass-card {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.register-glass-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 28px;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.12), rgba(255,255,255,0.01));
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 轨道 Logo */
|
||||
.register-orbit-ring {
|
||||
width: 76px; height: 76px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(129, 140, 248, 0.2);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
animation: regOrbitSpin 12s linear infinite;
|
||||
}
|
||||
.register-orbit-ring .register-planet {
|
||||
width: 34px; height: 34px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: regOrbitSpin 12s linear infinite reverse;
|
||||
}
|
||||
.register-orbit-ring .register-planet svg {
|
||||
width: 18px; height: 18px; color: white;
|
||||
}
|
||||
.register-orbit-dot {
|
||||
position: absolute;
|
||||
width: 6px; height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #818cf8;
|
||||
top: -3px; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
box-shadow: 0 0 10px rgba(129, 140, 248, 0.6);
|
||||
}
|
||||
@keyframes regOrbitSpin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 玻璃输入框 */
|
||||
.register-glass-input-wrap {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.register-glass-input {
|
||||
width: 100%;
|
||||
padding: 14px 16px 14px 48px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 14px;
|
||||
color: #f1f5f9;
|
||||
font-size: 15px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.register-glass-input::placeholder {
|
||||
color: #64748b;
|
||||
}
|
||||
.register-glass-input:focus {
|
||||
border-color: rgba(102, 126, 234, 0.5);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12), inset 0 0 20px rgba(102, 126, 234, 0.04);
|
||||
}
|
||||
.register-glass-input-icon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #64748b;
|
||||
pointer-events: none;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.register-glass-input:focus ~ .register-glass-input-icon {
|
||||
color: #818cf8;
|
||||
}
|
||||
|
||||
/* 验证码按钮 */
|
||||
.register-verify-btn {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 7px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(129, 140, 248, 0.3);
|
||||
background: rgba(129, 140, 248, 0.1);
|
||||
color: #818cf8;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: all 0.3s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.register-verify-btn:hover:not(:disabled) {
|
||||
background: rgba(129, 140, 248, 0.2);
|
||||
}
|
||||
.register-verify-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 主按钮 */
|
||||
.register-btn-primary {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
.register-btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 28px rgba(102, 126, 234, 0.5);
|
||||
}
|
||||
.register-btn-primary:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
.register-btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.register-btn-primary::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.18) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 社交登录行 */
|
||||
.register-social-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.register-social-btn {
|
||||
width: 52px; height: 52px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.register-social-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.register-social-btn svg {
|
||||
width: 22px; height: 22px;
|
||||
}
|
||||
|
||||
/* 密码强度条 */
|
||||
.register-strength-bar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.register-strength-bar span {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.register-strength-bar.weak span:nth-child(1) { background: #f87171; }
|
||||
.register-strength-bar.fair span:nth-child(-n+2) { background: #fbbf24; }
|
||||
.register-strength-bar.strong span:nth-child(-n+3) { background: #34d399; }
|
||||
.register-strength-bar.very-strong span { background: #818cf8; }
|
||||
|
||||
/* 协议 */
|
||||
.register-agreement {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-top: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.register-agreement a {
|
||||
color: #818cf8;
|
||||
text-decoration: none;
|
||||
}
|
||||
.register-agreement a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.register-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 24px 0;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
.register-divider::before,
|
||||
.register-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);
|
||||
}
|
||||
|
||||
/* 自定义复选框 */
|
||||
.register-checkbox {
|
||||
width: 20px; height: 20px;
|
||||
border-radius: 6px;
|
||||
border: 1.5px solid rgba(255, 255, 255, 0.15);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.register-checkbox.checked {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-color: transparent;
|
||||
}
|
||||
.register-checkbox svg {
|
||||
width: 12px; height: 12px;
|
||||
color: white;
|
||||
display: none;
|
||||
}
|
||||
.register-checkbox.checked svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 错误提示 */
|
||||
.register-error-msg {
|
||||
color: #f87171;
|
||||
font-size: 13px;
|
||||
margin-top: -8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 动画 */
|
||||
@keyframes regSlideUp {
|
||||
from { opacity: 0; transform: translateY(24px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.reg-animate-in {
|
||||
animation: regSlideUp 0.5s ease forwards;
|
||||
}
|
||||
.reg-animate-delay {
|
||||
animation: regSlideUp 0.5s ease 0.1s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
.reg-animate-delay2 {
|
||||
animation: regSlideUp 0.5s ease 0.2s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ============ 桌面端 ============ */
|
||||
.register-desktop {
|
||||
width: 460px;
|
||||
}
|
||||
|
||||
/* ============ 移动端 ============ */
|
||||
.register-mobile {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
/* ============ 暗色模式适配(琉璃拟态在亮色模式下调整) ============ */
|
||||
[data-theme="light"] .register-page {
|
||||
background: #0f172a;
|
||||
}
|
||||
[data-theme="light"] .register-glass-card {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
[data-theme="light"] .register-glass-input {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #f1f5f9;
|
||||
}
|
||||
[data-theme="light"] .register-glass-input::placeholder {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* ============ 移动端适配 ============ */
|
||||
@media (max-width: 480px) {
|
||||
.register-desktop {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.register-glass-card {
|
||||
border-radius: 24px;
|
||||
}
|
||||
.register-orb-1 { width: 200px; height: 200px; }
|
||||
.register-orb-2 { width: 160px; height: 160px; }
|
||||
.register-orb-3 { width: 120px; height: 120px; }
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Spin, App as AntdApp } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch } from "@/app/store";
|
||||
import { selectLoading, emailCodeLogin, sendEmailCode } from "@/features/login";
|
||||
import { useSelector } from "react-redux";
|
||||
import "./Register.css";
|
||||
|
||||
/** 从 URL 获取邀请码 */
|
||||
const getInviteCodeFromUrl = (): string | null => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const inviteParam = params.get("invite");
|
||||
if (inviteParam) return inviteParam;
|
||||
const pathParts = window.location.pathname.split("/");
|
||||
const inviteIndex = pathParts.indexOf("invite");
|
||||
if (inviteIndex !== -1 && pathParts[inviteIndex + 1]) {
|
||||
return pathParts[inviteIndex + 1];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** 生成随机星星 */
|
||||
const generateStars = (count: number) =>
|
||||
Array.from({ length: count }, (_, i) => ({
|
||||
id: i,
|
||||
left: Math.random() * 100,
|
||||
top: Math.random() * 100,
|
||||
delay: Math.random() * 3,
|
||||
duration: 2 + Math.random() * 3,
|
||||
size: 1 + Math.random() * 2,
|
||||
}));
|
||||
|
||||
const Register: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = AntdApp.useApp();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const loading = useSelector(selectLoading);
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [codeSent, setCodeSent] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const stars = useMemo(() => generateStars(80), []);
|
||||
const codeInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const inviteCode = getInviteCodeFromUrl();
|
||||
if (inviteCode) {
|
||||
localStorage.setItem("invite_code", inviteCode);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/** 密码强度 */
|
||||
const passwordStrength = useMemo(() => {
|
||||
if (!password) return "";
|
||||
let score = 0;
|
||||
if (password.length >= 8) score++;
|
||||
if (password.length >= 12) score++;
|
||||
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++;
|
||||
if (/\d/.test(password)) score++;
|
||||
if (/[^a-zA-Z0-9]/.test(password)) score++;
|
||||
if (score <= 1) return "weak";
|
||||
if (score <= 2) return "fair";
|
||||
if (score <= 3) return "strong";
|
||||
return "very-strong";
|
||||
}, [password]);
|
||||
|
||||
/** 发送验证码 */
|
||||
const handleSendCode = async () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!email) {
|
||||
newErrors.email = t("register.warnEmailRequired", "请输入邮箱");
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
newErrors.email = t("register.warnEmailFormat", "邮箱格式不正确");
|
||||
}
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
setErrors({});
|
||||
|
||||
try {
|
||||
const res = await dispatch(sendEmailCode({ to_email: email }));
|
||||
if (res.meta.requestStatus === "fulfilled") {
|
||||
setCodeSent(true);
|
||||
setCountdown(60);
|
||||
message.success(t("register.codeSent", "验证码已发送"));
|
||||
setTimeout(() => codeInputRef.current?.focus(), 100);
|
||||
const intervalId = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(intervalId);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
} catch {
|
||||
// error handled by redux
|
||||
}
|
||||
};
|
||||
|
||||
/** 提交注册 */
|
||||
const handleSubmit = async () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!email) newErrors.email = t("register.warnEmailRequired", "请输入邮箱");
|
||||
if (!code) newErrors.code = t("register.warnCodeRequired", "请输入验证码");
|
||||
if (password && password !== confirmPassword) {
|
||||
newErrors.confirmPassword = t("register.warnPasswordMismatch", "两次密码不一致");
|
||||
}
|
||||
if (password && password.length < 8) {
|
||||
newErrors.password = t("register.warnPasswordLength", "密码至少8位");
|
||||
}
|
||||
if (!agreed) {
|
||||
newErrors.agreed = t("register.warnAgreement", "请同意用户协议");
|
||||
}
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
setErrors({});
|
||||
|
||||
const inviteCode = localStorage.getItem("invite_code");
|
||||
try {
|
||||
const result = await dispatch(
|
||||
emailCodeLogin({
|
||||
to_email: email,
|
||||
code,
|
||||
invite_code: inviteCode || undefined,
|
||||
})
|
||||
);
|
||||
if (result.meta.requestStatus === "fulfilled" && result.payload && "access" in result.payload) {
|
||||
localStorage.removeItem("invite_code");
|
||||
message.success(t("register.success", "注册成功"));
|
||||
navigate("/");
|
||||
}
|
||||
} catch {
|
||||
// error handled by redux
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !loading) {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="register-page" onKeyDown={handleKeyDown}>
|
||||
{/* 星空背景 */}
|
||||
<div className="register-stars">
|
||||
{stars.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className="register-star"
|
||||
style={{
|
||||
left: `${s.left}%`,
|
||||
top: `${s.top}%`,
|
||||
animationDelay: `${s.delay}s`,
|
||||
animationDuration: `${s.duration}s`,
|
||||
width: `${s.size}px`,
|
||||
height: `${s.size}px`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 浮动光球 */}
|
||||
<div className="register-orb register-orb-1" />
|
||||
<div className="register-orb register-orb-2" />
|
||||
<div className="register-orb register-orb-3" />
|
||||
|
||||
{/* 琉璃卡片 */}
|
||||
<div className="register-glass-card register-desktop reg-animate-in" style={{ padding: "40px 32px" }}>
|
||||
{/* 轨道 Logo */}
|
||||
<div style={{ textAlign: "center", marginBottom: 28 }} className="reg-animate-in">
|
||||
<div className="register-orbit-ring">
|
||||
<div className="register-planet">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="register-orbit-dot" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style={{
|
||||
textAlign: "center",
|
||||
fontSize: 24,
|
||||
fontWeight: 700,
|
||||
marginBottom: 6,
|
||||
color: "#f1f5f9",
|
||||
lineHeight: 1.3,
|
||||
}} className="reg-animate-in">
|
||||
{t("register.title", "创建你的账号")}
|
||||
</h2>
|
||||
<p style={{
|
||||
textAlign: "center",
|
||||
fontSize: 14,
|
||||
color: "#64748b",
|
||||
marginBottom: 32,
|
||||
}} className="reg-animate-delay">
|
||||
{t("register.subtitle", "加入我们,开启全新体验")}
|
||||
</p>
|
||||
|
||||
{/* 邮箱 */}
|
||||
<div className="register-glass-input-wrap reg-animate-delay">
|
||||
<input
|
||||
type="email"
|
||||
className="register-glass-input"
|
||||
placeholder={t("register.emailPlaceholder", "邮箱地址")}
|
||||
value={email}
|
||||
onChange={(e) => { setEmail(e.target.value); setErrors((p) => ({ ...p, email: "" })); }}
|
||||
style={{ paddingRight: codeSent ? 110 : 16 }}
|
||||
/>
|
||||
<span className="register-glass-input-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect width="20" height="16" x="2" y="4" rx="2" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</svg>
|
||||
</span>
|
||||
{codeSent && (
|
||||
<button
|
||||
className="register-verify-btn"
|
||||
disabled={countdown > 0}
|
||||
onClick={handleSendCode}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}s` : t("register.resend", "重新发送")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{errors.email && <div className="register-error-msg">{errors.email}</div>}
|
||||
|
||||
{/* 验证码 */}
|
||||
<div className="register-glass-input-wrap reg-animate-delay">
|
||||
<input
|
||||
ref={codeInputRef}
|
||||
type="text"
|
||||
className="register-glass-input"
|
||||
placeholder={t("register.codePlaceholder", "验证码")}
|
||||
value={code}
|
||||
onChange={(e) => { setCode(e.target.value); setErrors((p) => ({ ...p, code: "" })); }}
|
||||
maxLength={8}
|
||||
/>
|
||||
<span className="register-glass-input-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
</span>
|
||||
{!codeSent && (
|
||||
<button
|
||||
className="register-verify-btn"
|
||||
disabled={!email || loading}
|
||||
onClick={handleSendCode}
|
||||
>
|
||||
{t("register.sendCode", "获取验证码")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{errors.code && <div className="register-error-msg">{errors.code}</div>}
|
||||
|
||||
{/* 用户名(选填) */}
|
||||
<div className="register-glass-input-wrap reg-animate-delay2">
|
||||
<input
|
||||
type="text"
|
||||
className="register-glass-input"
|
||||
placeholder={t("register.usernameOptional", "用户名(选填)")}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<span className="register-glass-input-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 密码 */}
|
||||
<div className="register-glass-input-wrap reg-animate-delay2">
|
||||
<input
|
||||
type="password"
|
||||
className="register-glass-input"
|
||||
placeholder={t("register.passwordPlaceholder", "设置密码(选填,至少8位)")}
|
||||
value={password}
|
||||
onChange={(e) => { setPassword(e.target.value); setErrors((p) => ({ ...p, password: "" })); }}
|
||||
/>
|
||||
<span className="register-glass-input-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
{password && (
|
||||
<div className={`register-strength-bar ${passwordStrength}`}>
|
||||
<span /><span /><span /><span />
|
||||
</div>
|
||||
)}
|
||||
{errors.password && <div className="register-error-msg">{errors.password}</div>}
|
||||
|
||||
{/* 确认密码 */}
|
||||
{password && (
|
||||
<div className="register-glass-input-wrap" style={{ animation: "regSlideUp 0.3s ease forwards" }}>
|
||||
<input
|
||||
type="password"
|
||||
className="register-glass-input"
|
||||
placeholder={t("register.confirmPasswordPlaceholder", "确认密码")}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => { setConfirmPassword(e.target.value); setErrors((p) => ({ ...p, confirmPassword: "" })); }}
|
||||
/>
|
||||
<span className="register-glass-input-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{errors.confirmPassword && <div className="register-error-msg">{errors.confirmPassword}</div>}
|
||||
|
||||
{/* 协议复选框 */}
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: 10, margin: "20px 0" }} className="reg-animate-delay2">
|
||||
<div
|
||||
className={`register-checkbox ${agreed ? "checked" : ""}`}
|
||||
onClick={() => { setAgreed(!agreed); setErrors((p) => ({ ...p, agreed: "" })); }}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: "#64748b", lineHeight: 1.5 }}>
|
||||
{t("register.agreementPrefix", "我已阅读并同意")}
|
||||
<a href="/agreement" style={{ color: "#818cf8", textDecoration: "none" }} onClick={(e) => e.stopPropagation()}>
|
||||
{t("register.userAgreement", "用户协议")}
|
||||
</a>
|
||||
{t("register.and", "和")}
|
||||
<a href="/privacy" style={{ color: "#818cf8", textDecoration: "none" }} onClick={(e) => e.stopPropagation()}>
|
||||
{t("register.privacyPolicy", "隐私政策")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{errors.agreed && <div className="register-error-msg" style={{ marginTop: -12 }}>{errors.agreed}</div>}
|
||||
|
||||
{/* 注册按钮 */}
|
||||
<button
|
||||
className="register-btn-primary reg-animate-delay2"
|
||||
disabled={loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{loading ? t("register.submitting", "注册中...") : t("register.submit", "创建账号")}
|
||||
</button>
|
||||
|
||||
{/* 分割线 */}
|
||||
<div className="register-divider reg-animate-delay2">
|
||||
{t("register.orDivider", "或")}
|
||||
</div>
|
||||
|
||||
{/* 社交登录 */}
|
||||
<div className="register-social-row reg-animate-delay2">
|
||||
<div className="register-social-btn" onClick={() => message.info(t("loginModal.qqNotAvailable", "QQ登录暂未开放"))}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15h-2v-6h2v6zm4 0h-2v-8h2v8zm-4-8h-2V7h2v2z" /></svg>
|
||||
</div>
|
||||
<div className="register-social-btn" onClick={() => message.info(t("loginModal.wechatNotAvailable", "微信登录暂未开放"))}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15h-2v-6h2v6zm4 0h-2v-8h2v8zm-4-8h-2V7h2v2z" /></svg>
|
||||
</div>
|
||||
<div className="register-social-btn" onClick={() => message.info(t("loginModal.weiboNotAvailable", "微博登录暂未开放"))}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 已有账号 */}
|
||||
<div className="register-agreement reg-animate-delay2">
|
||||
{t("register.hasAccount", "已有账号?")}
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => { e.preventDefault(); navigate("/"); }}
|
||||
style={{ fontWeight: 500 }}
|
||||
>
|
||||
{t("register.goLogin", "立即登录")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Spin spinning={loading} percent="auto" fullscreen />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
@@ -0,0 +1,403 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Spin, App as AntdApp } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppDispatch } from "@/app/store";
|
||||
import { selectLoading, emailCodeLogin, sendEmailCode } from "@/features/login";
|
||||
import { useSelector } from "react-redux";
|
||||
import "./Register.css";
|
||||
|
||||
/** 从 URL 获取邀请码 */
|
||||
const getInviteCodeFromUrl = (): string | null => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const inviteParam = params.get("invite");
|
||||
if (inviteParam) return inviteParam;
|
||||
const pathParts = window.location.pathname.split("/");
|
||||
const inviteIndex = pathParts.indexOf("invite");
|
||||
if (inviteIndex !== -1 && pathParts[inviteIndex + 1]) {
|
||||
return pathParts[inviteIndex + 1];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/** 生成随机星星 */
|
||||
const generateStars = (count: number) =>
|
||||
Array.from({ length: count }, (_, i) => ({
|
||||
id: i,
|
||||
left: Math.random() * 100,
|
||||
top: Math.random() * 100,
|
||||
delay: Math.random() * 3,
|
||||
duration: 2 + Math.random() * 3,
|
||||
size: 1 + Math.random() * 2,
|
||||
}));
|
||||
|
||||
const RegisterMobile: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = AntdApp.useApp();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const loading = useSelector(selectLoading);
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [codeSent, setCodeSent] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const stars = useMemo(() => generateStars(50), []);
|
||||
const codeInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const inviteCode = getInviteCodeFromUrl();
|
||||
if (inviteCode) {
|
||||
localStorage.setItem("invite_code", inviteCode);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/** 密码强度 */
|
||||
const passwordStrength = useMemo(() => {
|
||||
if (!password) return "";
|
||||
let score = 0;
|
||||
if (password.length >= 8) score++;
|
||||
if (password.length >= 12) score++;
|
||||
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++;
|
||||
if (/\d/.test(password)) score++;
|
||||
if (/[^a-zA-Z0-9]/.test(password)) score++;
|
||||
if (score <= 1) return "weak";
|
||||
if (score <= 2) return "fair";
|
||||
if (score <= 3) return "strong";
|
||||
return "very-strong";
|
||||
}, [password]);
|
||||
|
||||
/** 发送验证码 */
|
||||
const handleSendCode = async () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!email) {
|
||||
newErrors.email = t("register.warnEmailRequired", "请输入邮箱");
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
newErrors.email = t("register.warnEmailFormat", "邮箱格式不正确");
|
||||
}
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
setErrors({});
|
||||
|
||||
try {
|
||||
const res = await dispatch(sendEmailCode({ to_email: email }));
|
||||
if (res.meta.requestStatus === "fulfilled") {
|
||||
setCodeSent(true);
|
||||
setCountdown(60);
|
||||
message.success(t("register.codeSent", "验证码已发送"));
|
||||
setTimeout(() => codeInputRef.current?.focus(), 100);
|
||||
const intervalId = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(intervalId);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
} catch {
|
||||
// error handled by redux
|
||||
}
|
||||
};
|
||||
|
||||
/** 提交注册 */
|
||||
const handleSubmit = async () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!email) newErrors.email = t("register.warnEmailRequired", "请输入邮箱");
|
||||
if (!code) newErrors.code = t("register.warnCodeRequired", "请输入验证码");
|
||||
if (password && password !== confirmPassword) {
|
||||
newErrors.confirmPassword = t("register.warnPasswordMismatch", "两次密码不一致");
|
||||
}
|
||||
if (password && password.length < 8) {
|
||||
newErrors.password = t("register.warnPasswordLength", "密码至少8位");
|
||||
}
|
||||
if (!agreed) {
|
||||
newErrors.agreed = t("register.warnAgreement", "请同意用户协议");
|
||||
}
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
setErrors({});
|
||||
|
||||
const inviteCode = localStorage.getItem("invite_code");
|
||||
try {
|
||||
const result = await dispatch(
|
||||
emailCodeLogin({
|
||||
to_email: email,
|
||||
code,
|
||||
invite_code: inviteCode || undefined,
|
||||
})
|
||||
);
|
||||
if (result.meta.requestStatus === "fulfilled" && result.payload && "access" in result.payload) {
|
||||
localStorage.removeItem("invite_code");
|
||||
message.success(t("register.success", "注册成功"));
|
||||
navigate("/");
|
||||
}
|
||||
} catch {
|
||||
// error handled by redux
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !loading) {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="register-page" onKeyDown={handleKeyDown}>
|
||||
{/* 星空背景 */}
|
||||
<div className="register-stars">
|
||||
{stars.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className="register-star"
|
||||
style={{
|
||||
left: `${s.left}%`,
|
||||
top: `${s.top}%`,
|
||||
animationDelay: `${s.delay}s`,
|
||||
animationDuration: `${s.duration}s`,
|
||||
width: `${s.size}px`,
|
||||
height: `${s.size}px`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 浮动光球 */}
|
||||
<div className="register-orb register-orb-1" />
|
||||
<div className="register-orb register-orb-2" />
|
||||
|
||||
{/* 琉璃卡片 - 移动端 */}
|
||||
<div className="register-glass-card register-mobile reg-animate-in" style={{ padding: "32px 24px" }}>
|
||||
{/* 轨道 Logo */}
|
||||
<div style={{ textAlign: "center", marginBottom: 24 }} className="reg-animate-in">
|
||||
<div className="register-orbit-ring">
|
||||
<div className="register-planet">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="register-orbit-dot" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style={{
|
||||
textAlign: "center",
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
marginBottom: 6,
|
||||
color: "#f1f5f9",
|
||||
lineHeight: 1.3,
|
||||
}} className="reg-animate-in">
|
||||
{t("register.title", "创建你的账号")}
|
||||
</h2>
|
||||
<p style={{
|
||||
textAlign: "center",
|
||||
fontSize: 13,
|
||||
color: "#64748b",
|
||||
marginBottom: 28,
|
||||
}} className="reg-animate-delay">
|
||||
{t("register.subtitle", "加入我们,开启全新体验")}
|
||||
</p>
|
||||
|
||||
{/* 邮箱 */}
|
||||
<div className="register-glass-input-wrap reg-animate-delay">
|
||||
<input
|
||||
type="email"
|
||||
className="register-glass-input"
|
||||
placeholder={t("register.emailPlaceholder", "邮箱地址")}
|
||||
value={email}
|
||||
onChange={(e) => { setEmail(e.target.value); setErrors((p) => ({ ...p, email: "" })); }}
|
||||
style={{ paddingRight: codeSent ? 100 : 16 }}
|
||||
/>
|
||||
<span className="register-glass-input-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect width="20" height="16" x="2" y="4" rx="2" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</svg>
|
||||
</span>
|
||||
{codeSent && (
|
||||
<button
|
||||
className="register-verify-btn"
|
||||
disabled={countdown > 0}
|
||||
onClick={handleSendCode}
|
||||
style={{ fontSize: 12, padding: "6px 10px" }}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}s` : t("register.resend", "重发")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{errors.email && <div className="register-error-msg">{errors.email}</div>}
|
||||
|
||||
{/* 验证码 */}
|
||||
<div className="register-glass-input-wrap reg-animate-delay">
|
||||
<input
|
||||
ref={codeInputRef}
|
||||
type="text"
|
||||
className="register-glass-input"
|
||||
placeholder={t("register.codePlaceholder", "验证码")}
|
||||
value={code}
|
||||
onChange={(e) => { setCode(e.target.value); setErrors((p) => ({ ...p, code: "" })); }}
|
||||
maxLength={8}
|
||||
/>
|
||||
<span className="register-glass-input-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
</span>
|
||||
{!codeSent && (
|
||||
<button
|
||||
className="register-verify-btn"
|
||||
disabled={!email || loading}
|
||||
onClick={handleSendCode}
|
||||
style={{ fontSize: 12, padding: "6px 10px" }}
|
||||
>
|
||||
{t("register.sendCode", "获取验证码")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{errors.code && <div className="register-error-msg">{errors.code}</div>}
|
||||
|
||||
{/* 用户名(选填) */}
|
||||
<div className="register-glass-input-wrap reg-animate-delay2">
|
||||
<input
|
||||
type="text"
|
||||
className="register-glass-input"
|
||||
placeholder={t("register.usernameOptional", "用户名(选填)")}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<span className="register-glass-input-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 密码 */}
|
||||
<div className="register-glass-input-wrap reg-animate-delay2">
|
||||
<input
|
||||
type="password"
|
||||
className="register-glass-input"
|
||||
placeholder={t("register.passwordPlaceholder", "设置密码(选填,至少8位)")}
|
||||
value={password}
|
||||
onChange={(e) => { setPassword(e.target.value); setErrors((p) => ({ ...p, password: "" })); }}
|
||||
/>
|
||||
<span className="register-glass-input-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
{password && (
|
||||
<div className={`register-strength-bar ${passwordStrength}`}>
|
||||
<span /><span /><span /><span />
|
||||
</div>
|
||||
)}
|
||||
{errors.password && <div className="register-error-msg">{errors.password}</div>}
|
||||
|
||||
{/* 确认密码 */}
|
||||
{password && (
|
||||
<div className="register-glass-input-wrap" style={{ animation: "regSlideUp 0.3s ease forwards" }}>
|
||||
<input
|
||||
type="password"
|
||||
className="register-glass-input"
|
||||
placeholder={t("register.confirmPasswordPlaceholder", "确认密码")}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => { setConfirmPassword(e.target.value); setErrors((p) => ({ ...p, confirmPassword: "" })); }}
|
||||
/>
|
||||
<span className="register-glass-input-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{errors.confirmPassword && <div className="register-error-msg">{errors.confirmPassword}</div>}
|
||||
|
||||
{/* 协议复选框 */}
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: 10, margin: "18px 0" }} className="reg-animate-delay2">
|
||||
<div
|
||||
className={`register-checkbox ${agreed ? "checked" : ""}`}
|
||||
onClick={() => { setAgreed(!agreed); setErrors((p) => ({ ...p, agreed: "" })); }}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: "#64748b", lineHeight: 1.5 }}>
|
||||
{t("register.agreementPrefix", "我已阅读并同意")}
|
||||
<a href="/agreement" style={{ color: "#818cf8", textDecoration: "none" }} onClick={(e) => e.stopPropagation()}>
|
||||
{t("register.userAgreement", "用户协议")}
|
||||
</a>
|
||||
{t("register.and", "和")}
|
||||
<a href="/privacy" style={{ color: "#818cf8", textDecoration: "none" }} onClick={(e) => e.stopPropagation()}>
|
||||
{t("register.privacyPolicy", "隐私政策")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{errors.agreed && <div className="register-error-msg" style={{ marginTop: -12 }}>{errors.agreed}</div>}
|
||||
|
||||
{/* 注册按钮 */}
|
||||
<button
|
||||
className="register-btn-primary reg-animate-delay2"
|
||||
disabled={loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{loading ? t("register.submitting", "注册中...") : t("register.submit", "创建账号")}
|
||||
</button>
|
||||
|
||||
{/* 分割线 */}
|
||||
<div className="register-divider reg-animate-delay2">
|
||||
{t("register.orDivider", "或")}
|
||||
</div>
|
||||
|
||||
{/* 社交登录 */}
|
||||
<div className="register-social-row reg-animate-delay2">
|
||||
<div className="register-social-btn" style={{ width: 48, height: 48, borderRadius: 14 }}
|
||||
onClick={() => message.info(t("loginModal.qqNotAvailable", "QQ登录暂未开放"))}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15h-2v-6h2v6zm4 0h-2v-8h2v8zm-4-8h-2V7h2v2z" /></svg>
|
||||
</div>
|
||||
<div className="register-social-btn" style={{ width: 48, height: 48, borderRadius: 14 }}
|
||||
onClick={() => message.info(t("loginModal.wechatNotAvailable", "微信登录暂未开放"))}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15h-2v-6h2v6zm4 0h-2v-8h2v8zm-4-8h-2V7h2v2z" /></svg>
|
||||
</div>
|
||||
<div className="register-social-btn" style={{ width: 48, height: 48, borderRadius: 14 }}
|
||||
onClick={() => message.info(t("loginModal.weiboNotAvailable", "微博登录暂未开放"))}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 已有账号 */}
|
||||
<div className="register-agreement reg-animate-delay2" style={{ fontSize: 13 }}>
|
||||
{t("register.hasAccount", "已有账号?")}
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => { e.preventDefault(); navigate("/"); }}
|
||||
style={{ fontWeight: 500 }}
|
||||
>
|
||||
{t("register.goLogin", "立即登录")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Spin spinning={loading} percent="auto" fullscreen />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterMobile;
|
||||
Reference in New Issue
Block a user