import { useEffect, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { useAuth } from "../App"; import { Logo } from "../components/icons"; import { Button, Field, InlineAlert } from "../components/ui"; import { ApiError, api, session, type Tokens } from "../lib/client"; import type { User } from "../lib/types"; const EMAIL_RE = /^[^@\s]{1,64}@[^@\s]{1,190}\.[A-Za-z]{2,}$/; /** 跟后端 `password_problem` 同一套规则;本地只做"别白跑一趟"的提示,真判定在服务端。 */ function passwordHint(password: string): string | null { if (password.length < 8) return "至少 8 位"; if (password.length > 64) return "最多 64 位"; if (!/[A-Za-z]/.test(password) || !/\d/.test(password)) return "要同时有字母和数字"; return null; } /** * 注册:邮箱 + 验证码 + 密码。 * * 为什么必须先验一次邮箱:短信登录关掉以后,邮箱是账号名也是唯一联系方式, * "随手填一个" 会让账号变成没人能找回的孤岛。 */ export function RegisterPage() { const { reload } = useAuth(); const navigate = useNavigate(); const [email, setEmail] = useState(""); const [code, setCode] = useState(""); const [password, setPassword] = useState(""); const [confirm, setConfirm] = useState(""); const [nickname, setNickname] = useState(""); const [countdown, setCountdown] = useState(0); const [sent, setSent] = useState(false); const [devCode, setDevCode] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [open, setOpen] = useState(true); useEffect(() => { let alive = true; api .get<{ email_register: boolean }>("/auth/methods") .then((data) => { if (alive) setOpen(data.email_register); }) .catch(() => { /* 拿不到就按开着渲染,提交时服务端会给出真答案 */ }); return () => { alive = false; }; }, []); useEffect(() => { if (countdown <= 0) return; const timer = setTimeout(() => setCountdown((v) => v - 1), 1000); return () => clearTimeout(timer); }, [countdown]); const emailValid = EMAIL_RE.test(email.trim()); const weak = password ? passwordHint(password) : null; const mismatch = confirm.length > 0 && confirm !== password; const canSubmit = !busy && emailValid && code.length >= 4 && !weak && password.length > 0 && confirm === password; async function sendCode() { setError(null); setBusy(true); try { const data = await api.post<{ dev_code?: string }>("/auth/email/code", { email: email.trim(), }); setDevCode(data.dev_code ?? null); setSent(true); setCountdown(60); } catch (err) { setError(err instanceof ApiError ? err.message : "发送失败"); } finally { setBusy(false); } } async function submit(event: React.FormEvent) { event.preventDefault(); if (!canSubmit) return; setError(null); setBusy(true); try { const data = await api.post<{ user: User; tokens: Tokens }>("/auth/register", { email: email.trim(), code, password, ...(nickname.trim() ? { nickname: nickname.trim() } : {}), }); session.save(data.tokens); await reload(); navigate("/", { replace: true }); } catch (err) { setError(err instanceof ApiError ? err.message : "注册失败"); } finally { setBusy(false); } } return (
达道Task
任务 · 工单 · 知识

创建账号

用邮箱注册:先收一个验证码,再设一个密码。邮箱就是你的登录账号。

{!open && (
这台服务暂未开放自助注册,请联系管理员开通账号。
)}
setEmail(e.target.value)} maxLength={255} disabled={!open} />
setCode(e.target.value.trim())} maxLength={6} disabled={!open} />
setPassword(e.target.value)} maxLength={64} disabled={!open} /> {weak && {weak}} setConfirm(e.target.value)} maxLength={64} disabled={!open} /> {mismatch && 两次输入不一致} setNickname(e.target.value)} maxLength={32} disabled={!open} /> {devCode && (
开发环境验证码:{devCode} (生产环境只发到邮箱,不回显)
)} {error && {error}}

已经有账号了? 去登录

); }