diff --git a/public/icons-variants/icon-c-letter-v2.svg b/public/icons-variants/icon-c-letter-v2.svg new file mode 100644 index 0000000..87d7a04 --- /dev/null +++ b/public/icons-variants/icon-c-letter-v2.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/icons-variants/icon-colared-wrench.svg b/public/icons-variants/icon-colared-wrench.svg new file mode 100644 index 0000000..ba5902d --- /dev/null +++ b/public/icons-variants/icon-colared-wrench.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/icons-variants/icon-ring-colared.svg b/public/icons-variants/icon-ring-colared.svg new file mode 100644 index 0000000..5a95c5e --- /dev/null +++ b/public/icons-variants/icon-ring-colared.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/icons-variants/icon-wave-square.svg b/public/icons-variants/icon-wave-square.svg new file mode 100644 index 0000000..a2f9819 --- /dev/null +++ b/public/icons-variants/icon-wave-square.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 99a6c42..44a07cd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -110,7 +110,6 @@ const QRCodeGeneratorApiDetail = lazy(() => import("@/pages/QRCodeGenerator/QRCo const IPLocationApiDetail = lazy(() => import("@/pages/IPLocationApiDetail/IPLocationApiDetail")); const PasswordGeneratorApiDetail = lazy(() => import("@/pages/PasswordGenerator/PasswordGeneratorApiDetail")); const BaiduTranslateDetail = lazy(() => import("@/pages/BaiduTranslateDetail/BaiduTranslateDetail")); -const LoginMobile = lazy(() => import("@/pages/LoginMobile/LoginMobile")); const PageLoading = () => (
diff --git a/src/pages/Register/Register.css b/src/pages/Register/Register.css new file mode 100644 index 0000000..d4e9ca0 --- /dev/null +++ b/src/pages/Register/Register.css @@ -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; } +} diff --git a/src/pages/Register/Register.tsx b/src/pages/Register/Register.tsx new file mode 100644 index 0000000..9696eea --- /dev/null +++ b/src/pages/Register/Register.tsx @@ -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>({}); + + const stars = useMemo(() => generateStars(80), []); + const codeInputRef = useRef(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 = {}; + 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 = {}; + 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 ( +
+ {/* 星空背景 */} +
+ {stars.map((s) => ( +
+ ))} +
+ + {/* 浮动光球 */} +
+
+
+ + {/* 琉璃卡片 */} +
+ {/* 轨道 Logo */} +
+
+
+ + + + +
+
+
+
+ +

+ {t("register.title", "创建你的账号")} +

+

+ {t("register.subtitle", "加入我们,开启全新体验")} +

+ + {/* 邮箱 */} +
+ { setEmail(e.target.value); setErrors((p) => ({ ...p, email: "" })); }} + style={{ paddingRight: codeSent ? 110 : 16 }} + /> + + + + + + + {codeSent && ( + + )} +
+ {errors.email &&
{errors.email}
} + + {/* 验证码 */} +
+ { setCode(e.target.value); setErrors((p) => ({ ...p, code: "" })); }} + maxLength={8} + /> + + + + + + {!codeSent && ( + + )} +
+ {errors.code &&
{errors.code}
} + + {/* 用户名(选填) */} +
+ setUsername(e.target.value)} + /> + + + + + + +
+ + {/* 密码 */} +
+ { setPassword(e.target.value); setErrors((p) => ({ ...p, password: "" })); }} + /> + + + + + + +
+ {password && ( +
+ +
+ )} + {errors.password &&
{errors.password}
} + + {/* 确认密码 */} + {password && ( +
+ { setConfirmPassword(e.target.value); setErrors((p) => ({ ...p, confirmPassword: "" })); }} + /> + + + + + +
+ )} + {errors.confirmPassword &&
{errors.confirmPassword}
} + + {/* 协议复选框 */} +
+
{ setAgreed(!agreed); setErrors((p) => ({ ...p, agreed: "" })); }} + > + + + +
+ +
+ {errors.agreed &&
{errors.agreed}
} + + {/* 注册按钮 */} + + + {/* 分割线 */} +
+ {t("register.orDivider", "或")} +
+ + {/* 社交登录 */} +
+
message.info(t("loginModal.qqNotAvailable", "QQ登录暂未开放"))}> + +
+
message.info(t("loginModal.wechatNotAvailable", "微信登录暂未开放"))}> + +
+
message.info(t("loginModal.weiboNotAvailable", "微博登录暂未开放"))}> + +
+
+ + {/* 已有账号 */} + +
+ + +
+ ); +}; + +export default Register; diff --git a/src/pages/Register/RegisterMobile.tsx b/src/pages/Register/RegisterMobile.tsx new file mode 100644 index 0000000..b326e6e --- /dev/null +++ b/src/pages/Register/RegisterMobile.tsx @@ -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>({}); + + const stars = useMemo(() => generateStars(50), []); + const codeInputRef = useRef(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 = {}; + 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 = {}; + 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 ( +
+ {/* 星空背景 */} +
+ {stars.map((s) => ( +
+ ))} +
+ + {/* 浮动光球 */} +
+
+ + {/* 琉璃卡片 - 移动端 */} +
+ {/* 轨道 Logo */} +
+
+
+ + + + +
+
+
+
+ +

+ {t("register.title", "创建你的账号")} +

+

+ {t("register.subtitle", "加入我们,开启全新体验")} +

+ + {/* 邮箱 */} +
+ { setEmail(e.target.value); setErrors((p) => ({ ...p, email: "" })); }} + style={{ paddingRight: codeSent ? 100 : 16 }} + /> + + + + + + + {codeSent && ( + + )} +
+ {errors.email &&
{errors.email}
} + + {/* 验证码 */} +
+ { setCode(e.target.value); setErrors((p) => ({ ...p, code: "" })); }} + maxLength={8} + /> + + + + + + {!codeSent && ( + + )} +
+ {errors.code &&
{errors.code}
} + + {/* 用户名(选填) */} +
+ setUsername(e.target.value)} + /> + + + + + + +
+ + {/* 密码 */} +
+ { setPassword(e.target.value); setErrors((p) => ({ ...p, password: "" })); }} + /> + + + + + + +
+ {password && ( +
+ +
+ )} + {errors.password &&
{errors.password}
} + + {/* 确认密码 */} + {password && ( +
+ { setConfirmPassword(e.target.value); setErrors((p) => ({ ...p, confirmPassword: "" })); }} + /> + + + + + +
+ )} + {errors.confirmPassword &&
{errors.confirmPassword}
} + + {/* 协议复选框 */} +
+
{ setAgreed(!agreed); setErrors((p) => ({ ...p, agreed: "" })); }} + > + + + +
+ +
+ {errors.agreed &&
{errors.agreed}
} + + {/* 注册按钮 */} + + + {/* 分割线 */} +
+ {t("register.orDivider", "或")} +
+ + {/* 社交登录 */} +
+
message.info(t("loginModal.qqNotAvailable", "QQ登录暂未开放"))}> + +
+
message.info(t("loginModal.wechatNotAvailable", "微信登录暂未开放"))}> + +
+
message.info(t("loginModal.weiboNotAvailable", "微博登录暂未开放"))}> + +
+
+ + {/* 已有账号 */} + +
+ + +
+ ); +}; + +export default RegisterMobile; diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 53579d6..a154565 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/css.t.ts","./src/i18n.ts","./src/jszip.d.ts","./src/main.tsx","./src/app/store.ts","./src/components/authguard/authguard.tsx","./src/components/bdcloudvideoview/bdcloudvideoview.tsx","./src/components/bdcloudvideoview/index.ts","./src/components/captchainput/captchainput.tsx","./src/components/languageswitcher/languageswitcher.tsx","./src/components/markdown/markdown.tsx","./src/components/markdownrenderer/markdownrenderer.tsx","./src/components/mobileguard/mobileguard.tsx","./src/components/navbar/navbar.tsx","./src/components/slidercaptcha/slidercaptcha.tsx","./src/components/slidercaptcha/captchaimages.ts","./src/components/slidercaptcha/captchautils.ts","./src/components/slidercaptcha/index.ts","./src/components/themetoggle/themetoggle.tsx","./src/components/toolfavoritebutton/toolfavoritebutton.tsx","./src/components/test/test.tsx","./src/components/ui/color-mode.tsx","./src/components/ui/password-input.tsx","./src/components/ui/provider.tsx","./src/components/ui/toaster.tsx","./src/components/ui/tooltip.tsx","./src/contexts/fontsizecontext.tsx","./src/contexts/languagecontext.tsx","./src/contexts/loginmodalcontext.tsx","./src/contexts/themecontext.tsx","./src/features/login/authslice.tsx","./src/features/login/index.ts","./src/hooks/useismobile.ts","./src/hooks/uselearntimer.ts","./src/hooks/userecordhistory.ts","./src/hooks/useroutetracker.ts","./src/locales/index.ts","./src/pages/aqidetails/aqidetails.tsx","./src/pages/aqidetails/aqidetailsmobile.tsx","./src/pages/apidetail/apidetail.tsx","./src/pages/apidetail/apidetailmobile.tsx","./src/pages/apidirectory/apidirectory.tsx","./src/pages/apidirectory/apidirectorydesktop.tsx","./src/pages/apidirectory/apidirectorymobile.tsx","./src/pages/apidocs/apidocs.tsx","./src/pages/articledetail/articledetail.tsx","./src/pages/articleeditor/articleeditor.tsx","./src/pages/articlemanage/articlemanage.tsx","./src/pages/articles/articles.tsx","./src/pages/articles/articlesmobile.tsx","./src/pages/baidutranslate/baidutranslate.tsx","./src/pages/baidutranslate/baidutranslatemobile.tsx","./src/pages/baidutranslatedetail/baidutranslatedetail.tsx","./src/pages/base64tool/base64tool.tsx","./src/pages/base64tool/base64toolmobile.tsx","./src/pages/bug/bug.tsx","./src/pages/bug/bugmobile.tsx","./src/pages/bugdetail/bugdetail.tsx","./src/pages/calculator/calculator.tsx","./src/pages/calculator/calculatormobile.tsx","./src/pages/changelog/changelog.tsx","./src/pages/changelog/changelogmobile.tsx","./src/pages/charsetconverter/charsetconverter.tsx","./src/pages/charsetconverter/charsetconvertermobile.tsx","./src/pages/chat/chat.tsx","./src/pages/chat/chatmobile.tsx","./src/pages/codeformatter/codeformatter.tsx","./src/pages/codeformatter/codeformattermobile.tsx","./src/pages/colorconverter/colorconverter.tsx","./src/pages/colorconverter/colorconvertermobile.tsx","./src/pages/colorpicker/colorpicker.tsx","./src/pages/console/console.tsx","./src/pages/courselearn/courselearn.tsx","./src/pages/currencydetail/currencydetail.tsx","./src/pages/datecalculator/datecalculator.tsx","./src/pages/document/document.tsx","./src/pages/encodingconverter/encodingconverter.tsx","./src/pages/encodingconverter/encodingconvertermobile.tsx","./src/pages/favorites/favorites.tsx","./src/pages/forgotpassword/forgotpassword.tsx","./src/pages/history/history.tsx","./src/pages/home/home.tsx","./src/pages/home/homemobile.tsx","./src/pages/iplocation/iplocation.tsx","./src/pages/iplocationapidetail/iplocationapidetail.tsx","./src/pages/imagecompressor/imagecompressor.tsx","./src/pages/imagecompressor/imagecompressormobile.tsx","./src/pages/imagecompressor/components/batchprocessing.tsx","./src/pages/imagecompressor/components/comparisonpreview.tsx","./src/pages/imagecompressor/components/compressionconfig.tsx","./src/pages/imagecompressor/components/compressionhistory.tsx","./src/pages/imagecompressor/components/uploadarea.tsx","./src/pages/imageeditor/imageeditor.tsx","./src/pages/inviteredirect/inviteredirect.tsx","./src/pages/jsonformatter/jsonformatter.tsx","./src/pages/jsonformatter/jsonformattermobile.tsx","./src/pages/learn/learn.tsx","./src/pages/learn/learnmobile.tsx","./src/pages/learneditor/learneditor.tsx","./src/pages/learnmanage/learnmanage.tsx","./src/pages/manual/manual.tsx","./src/pages/messages/messages.tsx","./src/pages/messages/messagesmobile.tsx","./src/pages/notfound/notfound.tsx","./src/pages/passwordgenerator/passwordgeneratorapidetail.tsx","./src/pages/privacy/privacy.tsx","./src/pages/profile/profile.tsx","./src/pages/profile/profilechangepassword.tsx","./src/pages/profile/profileemail.tsx","./src/pages/profile/profilephone.tsx","./src/pages/profile/components/biosection.tsx","./src/pages/profile/components/blacklistcard.tsx","./src/pages/profile/components/changepasswordcard.tsx","./src/pages/profile/components/devicemanagementcard.tsx","./src/pages/profile/components/editableitem.tsx","./src/pages/profile/components/emailsettingcard.tsx","./src/pages/profile/components/infoitem.tsx","./src/pages/profile/components/notificationsettingcard.tsx","./src/pages/profile/components/phonesettingcard.tsx","./src/pages/profile/components/privacysettingcard.tsx","./src/pages/profile/components/profilecomponents.tsx","./src/pages/profile/components/recentlogincard.tsx","./src/pages/profile/components/settingcard.tsx","./src/pages/profile/components/useridcard.tsx","./src/pages/profile/components/userinfocard.tsx","./src/pages/qrcodegenerator/qrcodegenerator.tsx","./src/pages/qrcodegenerator/qrcodegeneratorapidetail.tsx","./src/pages/qrcodegenerator/qrcodegeneratormobile.tsx","./src/pages/regextool/regextool.tsx","./src/pages/regextool/regextoolmobile.tsx","./src/pages/search/search.tsx","./src/pages/settings/settings.tsx","./src/pages/shorturldetail/shorturldetail.tsx","./src/pages/shorturldetail/shorturldetailmobile.tsx","./src/pages/slidercaptchademo/slidercaptchademo.tsx","./src/pages/systemmessage/systemmessage.tsx","./src/pages/taskcenter/taskcenter.tsx","./src/pages/textdiff/textdiff.tsx","./src/pages/textdiff/textdiffmobile.tsx","./src/pages/timestamptool/timestamptool.tsx","./src/pages/timestamptool/timestamptoolmobile.tsx","./src/pages/tooldetail/tooldetail.tsx","./src/pages/use/use.tsx","./src/pages/useragreement/useragreement.tsx","./src/pages/useragreement/useragreementmobile.tsx","./src/pages/userhome/userhome.tsx","./src/pages/userhome/components/activitytimeline.tsx","./src/pages/userhome/components/courseprogresscard.tsx","./src/pages/utility/utility.tsx","./src/pages/utility/utilitymobile.tsx","./src/pages/wallet/coins.tsx","./src/pages/wallet/invite.tsx","./src/pages/wallet/points.tsx","./src/pages/wallet/recharge.tsx","./src/pages/weatherdetail/weatherdetail.tsx","./src/pages/weatherdetail/weatherdetailmobile.tsx","./src/types/bdcloud.d.ts","./src/types/image.ts","./src/utils/apiimageutils.ts","./src/utils/axios.ts","./src/utils/charencoding.ts","./src/utils/colorhistorystorage.ts","./src/utils/encoding.ts","./src/utils/imageprocessor.ts","./src/utils/message.ts","./src/utils/qrrenderer.ts","./src/utils/request.ts","./src/utils/tasktrack.ts","./src/utils/time.ts","./src/utils/toolusagelogger.ts"],"errors":true,"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/css.t.ts","./src/i18n.ts","./src/jszip.d.ts","./src/main.tsx","./src/app/store.ts","./src/components/authguard/authguard.tsx","./src/components/bdcloudvideoview/bdcloudvideoview.tsx","./src/components/bdcloudvideoview/index.ts","./src/components/captchainput/captchainput.tsx","./src/components/languageswitcher/languageswitcher.tsx","./src/components/markdown/markdown.tsx","./src/components/markdownrenderer/markdownrenderer.tsx","./src/components/mobilebottomnav/mobilebottomnav.tsx","./src/components/mobileguard/mobileguard.tsx","./src/components/mobiletopnav/mobiletopnav.tsx","./src/components/navbar/navbar.tsx","./src/components/slidercaptcha/slidercaptcha.tsx","./src/components/slidercaptcha/captchaimages.ts","./src/components/slidercaptcha/captchautils.ts","./src/components/slidercaptcha/index.ts","./src/components/themetoggle/themetoggle.tsx","./src/components/toolfavoritebutton/toolfavoritebutton.tsx","./src/components/test/test.tsx","./src/components/ui/color-mode.tsx","./src/components/ui/password-input.tsx","./src/components/ui/provider.tsx","./src/components/ui/toaster.tsx","./src/components/ui/tooltip.tsx","./src/contexts/fontsizecontext.tsx","./src/contexts/languagecontext.tsx","./src/contexts/loginmodalcontext.tsx","./src/contexts/themecontext.tsx","./src/features/login/authslice.tsx","./src/features/login/index.ts","./src/hooks/useismobile.ts","./src/hooks/uselearntimer.ts","./src/hooks/userecordhistory.ts","./src/hooks/useroutetracker.ts","./src/locales/index.ts","./src/pages/aqidetails/aqidetails.tsx","./src/pages/aqidetails/aqidetailsmobile.tsx","./src/pages/apidetail/apidetail.tsx","./src/pages/apidetail/apidetailmobile.tsx","./src/pages/apidirectory/apidirectory.tsx","./src/pages/apidirectory/apidirectorydesktop.tsx","./src/pages/apidirectory/apidirectorymobile.tsx","./src/pages/apidocs/apidocs.tsx","./src/pages/articledetail/articledetail.tsx","./src/pages/articleeditor/articleeditor.tsx","./src/pages/articlemanage/articlemanage.tsx","./src/pages/articles/articles.tsx","./src/pages/articles/articlesmobile.tsx","./src/pages/baidutranslate/baidutranslate.tsx","./src/pages/baidutranslate/baidutranslatemobile.tsx","./src/pages/baidutranslatedetail/baidutranslatedetail.tsx","./src/pages/base64tool/base64tool.tsx","./src/pages/base64tool/base64toolmobile.tsx","./src/pages/bug/bug.tsx","./src/pages/bug/bugmobile.tsx","./src/pages/bugdetail/bugdetail.tsx","./src/pages/calculator/calculator.tsx","./src/pages/calculator/calculatormobile.tsx","./src/pages/changelog/changelog.tsx","./src/pages/changelog/changelogmobile.tsx","./src/pages/charsetconverter/charsetconverter.tsx","./src/pages/charsetconverter/charsetconvertermobile.tsx","./src/pages/chat/chat.tsx","./src/pages/chat/chatmobile.tsx","./src/pages/codeformatter/codeformatter.tsx","./src/pages/codeformatter/codeformattermobile.tsx","./src/pages/colorconverter/colorconverter.tsx","./src/pages/colorconverter/colorconvertermobile.tsx","./src/pages/colorpicker/colorpicker.tsx","./src/pages/console/console.tsx","./src/pages/courselearn/courselearn.tsx","./src/pages/currencydetail/currencydetail.tsx","./src/pages/datecalculator/datecalculator.tsx","./src/pages/document/document.tsx","./src/pages/encodingconverter/encodingconverter.tsx","./src/pages/encodingconverter/encodingconvertermobile.tsx","./src/pages/favorites/favorites.tsx","./src/pages/forgotpassword/forgotpassword.tsx","./src/pages/history/history.tsx","./src/pages/home/home.tsx","./src/pages/home/homemobile.tsx","./src/pages/iplocation/iplocation.tsx","./src/pages/iplocationapidetail/iplocationapidetail.tsx","./src/pages/imagecompressor/imagecompressor.tsx","./src/pages/imagecompressor/imagecompressormobile.tsx","./src/pages/imagecompressor/components/batchprocessing.tsx","./src/pages/imagecompressor/components/comparisonpreview.tsx","./src/pages/imagecompressor/components/compressionconfig.tsx","./src/pages/imagecompressor/components/compressionhistory.tsx","./src/pages/imagecompressor/components/uploadarea.tsx","./src/pages/imageeditor/imageeditor.tsx","./src/pages/inviteredirect/inviteredirect.tsx","./src/pages/jsonformatter/jsonformatter.tsx","./src/pages/jsonformatter/jsonformattermobile.tsx","./src/pages/learn/learn.tsx","./src/pages/learn/learnmobile.tsx","./src/pages/learneditor/learneditor.tsx","./src/pages/learnmanage/learnmanage.tsx","./src/pages/manual/manual.tsx","./src/pages/messages/messages.tsx","./src/pages/messages/messagesmobile.tsx","./src/pages/notfound/notfound.tsx","./src/pages/passwordgenerator/passwordgeneratorapidetail.tsx","./src/pages/privacy/privacy.tsx","./src/pages/profile/profile.tsx","./src/pages/profile/profilechangepassword.tsx","./src/pages/profile/profileemail.tsx","./src/pages/profile/profilephone.tsx","./src/pages/profile/components/biosection.tsx","./src/pages/profile/components/blacklistcard.tsx","./src/pages/profile/components/changepasswordcard.tsx","./src/pages/profile/components/devicemanagementcard.tsx","./src/pages/profile/components/editableitem.tsx","./src/pages/profile/components/emailsettingcard.tsx","./src/pages/profile/components/infoitem.tsx","./src/pages/profile/components/notificationsettingcard.tsx","./src/pages/profile/components/phonesettingcard.tsx","./src/pages/profile/components/privacysettingcard.tsx","./src/pages/profile/components/profilecomponents.tsx","./src/pages/profile/components/recentlogincard.tsx","./src/pages/profile/components/settingcard.tsx","./src/pages/profile/components/useridcard.tsx","./src/pages/profile/components/userinfocard.tsx","./src/pages/qrcodegenerator/qrcodegenerator.tsx","./src/pages/qrcodegenerator/qrcodegeneratorapidetail.tsx","./src/pages/qrcodegenerator/qrcodegeneratormobile.tsx","./src/pages/regextool/regextool.tsx","./src/pages/regextool/regextoolmobile.tsx","./src/pages/register/register.tsx","./src/pages/register/registermobile.tsx","./src/pages/search/search.tsx","./src/pages/settings/settings.tsx","./src/pages/shorturldetail/shorturldetail.tsx","./src/pages/shorturldetail/shorturldetailmobile.tsx","./src/pages/slidercaptchademo/slidercaptchademo.tsx","./src/pages/systemmessage/systemmessage.tsx","./src/pages/taskcenter/taskcenter.tsx","./src/pages/textdiff/textdiff.tsx","./src/pages/textdiff/textdiffmobile.tsx","./src/pages/timestamptool/timestamptool.tsx","./src/pages/timestamptool/timestamptoolmobile.tsx","./src/pages/tooldetail/tooldetail.tsx","./src/pages/use/use.tsx","./src/pages/useragreement/useragreement.tsx","./src/pages/useragreement/useragreementmobile.tsx","./src/pages/userhome/userhome.tsx","./src/pages/userhome/components/activitytimeline.tsx","./src/pages/userhome/components/courseprogresscard.tsx","./src/pages/utility/utility.tsx","./src/pages/utility/utilitymobile.tsx","./src/pages/wallet/coins.tsx","./src/pages/wallet/invite.tsx","./src/pages/wallet/points.tsx","./src/pages/wallet/recharge.tsx","./src/pages/weatherdetail/weatherdetail.tsx","./src/pages/weatherdetail/weatherdetailmobile.tsx","./src/types/bdcloud.d.ts","./src/types/image.ts","./src/utils/apiimageutils.ts","./src/utils/axios.ts","./src/utils/charencoding.ts","./src/utils/colorhistorystorage.ts","./src/utils/encoding.ts","./src/utils/htmlsanitizer.ts","./src/utils/imageprocessor.ts","./src/utils/message.ts","./src/utils/qrrenderer.ts","./src/utils/request.ts","./src/utils/tasktrack.ts","./src/utils/time.ts","./src/utils/toolusagelogger.ts"],"version":"5.9.3"} \ No newline at end of file