import { useEffect, useRef, useState } from "react"; import type { FormEvent } from "react"; import { ArrowRight, Eye, EyeOff, Info, LockKeyhole, ScanLine, ShieldCheck, Ticket, UserRound, Users } from "lucide-react"; import { api, getRemember, setRemember as persistRemember } from "../api"; import type { Team, User } from "../types"; import type { AuthMode } from "./route-config"; type LoginProgress = "checking" | "entering"; const LOGIN_PROGRESS_COPY: Record = { checking: "正在验证用户名和密码…", entering: "登录成功,正在进入工作台…" }; type FieldErrors = { username?: string; password?: string; }; type RegisterErrors = { team?: string; username?: string; password?: string; password2?: string; agree?: string; }; // 注册码优先:先验码,验过才展开对应表单(create_team 开团队 / join_team 加入已有团队)。 type InviteState = | { status: "idle" } | { status: "checking" } | { status: "valid"; kind: "join_team" | "create_team"; teamName: string | null; role?: string } | { status: "invalid"; detail: string }; export function AuthScreen({ initialMode, onModeChange, onAuthed }: { initialMode: AuthMode; onModeChange: (mode: AuthMode) => void; onAuthed: (payload: { token: string; user: User; team: Team; role?: string; remember?: boolean }) => void | Promise; }) { const remembered = getRemember(); const [mode, setMode] = useState(initialMode); // ── 登录态 ── const [username, setUsername] = useState(remembered?.username || ""); const [password, setPassword] = useState(""); const [remember, setRemember] = useState(Boolean(remembered)); const [showPwd, setShowPwd] = useState(false); const [fieldErrors, setFieldErrors] = useState({}); // ── 注册态 ── const [regTeam, setRegTeam] = useState(""); const [regUsername, setRegUsername] = useState(""); const [regPassword, setRegPassword] = useState(""); const [regPassword2, setRegPassword2] = useState(""); const [regInvite, setRegInvite] = useState(""); const [inviteState, setInviteState] = useState({ status: "idle" }); const [regAgree, setRegAgree] = useState(true); const [regShowPwd, setRegShowPwd] = useState(false); const [regShowPwd2, setRegShowPwd2] = useState(false); const [regErrors, setRegErrors] = useState({}); // ── 共用 ── const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const [toast, setToast] = useState<{ title: string; sub: string } | null>(null); const [loginProgress, setLoginProgress] = useState(null); const toastTimer = useRef | null>(null); // initialMode 由路由驱动(/login ↔ /register),外部回退/前进时跟随 useEffect(() => { setMode(initialMode); }, [initialMode]); useEffect(() => () => { if (toastTimer.current) clearTimeout(toastTimer.current); }, []); // 邀请链接 ?invite=CODE:进注册页自动回填 + 验码(团管/超管发的链接点开即识别团队) useEffect(() => { if (mode !== "register") return; const code = new URLSearchParams(window.location.search).get("invite"); if (code && !regInvite) { setRegInvite(code); void checkInvite(code); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [mode]); // 验码:验过才展开对应表单;未验/无效不暴露团队信息 async function checkInvite(rawCode?: string) { const code = (rawCode ?? regInvite).trim().toUpperCase(); setError(""); if (!code) { setInviteState({ status: "invalid", detail: "请填写邀请码" }); return; } setInviteState({ status: "checking" }); try { const res = await api.validateInvite(code); if (res.valid && res.kind) { setInviteState({ status: "valid", kind: res.kind, teamName: res.team_name ?? null, role: res.role }); if (res.kind === "join_team") setRegTeam(res.team_name || ""); } else { setInviteState({ status: "invalid", detail: res.detail || "邀请码无效或已过期" }); } } catch { setInviteState({ status: "invalid", detail: "邀请码校验失败,请重试" }); } } // 改码即重置验证态:必须重新验证(避免拿旧验证结果提交) function onInviteChange(value: string) { setRegInvite(value); setError(""); if (inviteState.status !== "idle") setInviteState({ status: "idle" }); } function showToast(title: string, sub: string) { setToast({ title, sub }); if (toastTimer.current) clearTimeout(toastTimer.current); toastTimer.current = setTimeout(() => setToast(null), 2800); } function switchMode(next: AuthMode) { if (next === mode) return; setMode(next); setError(""); setLoginProgress(null); setFieldErrors({}); setRegErrors({}); onModeChange(next); } // ── 登录 ── function setFieldValue(field: keyof FieldErrors, value: string) { if (field === "username") setUsername(value); if (field === "password") setPassword(value); setError(""); setLoginProgress(null); setFieldErrors((current) => ({ ...current, [field]: undefined })); } function validateLogin() { const next: FieldErrors = {}; if (!username.trim()) next.username = "请输入用户名"; if (!password) next.password = "请输入密码"; setFieldErrors(next); if (Object.keys(next).length > 0) { setError("请补全必填字段后再登录"); return false; } return true; } async function submitLogin(event?: FormEvent) { event?.preventDefault(); if (busy) return; if (!validateLogin()) return; setBusy(true); setError(""); setLoginProgress("checking"); try { const payload = await api.login({ username: username.trim(), password }); // 记住我:勾选则存用户名 + 7 天有效期,token 落 localStorage;否则只存当前会话 persistRemember(username.trim(), remember); // 凭证验过即进:不再做任何人为延迟动画。"进入"态由真实的 onAuthed(身份+数据水合)驱动。 setLoginProgress("entering"); await onAuthed({ ...payload, remember }); } catch (err) { setLoginProgress(null); const raw = err instanceof Error ? err.message : ""; const friendly = /invalid credentials|unable to log in|non_field_errors/i.test(raw) ? "用户名或密码不正确" : raw || "登录失败,请稍后重试"; setError(friendly); } finally { setBusy(false); } } // ── 注册 ── function setRegValue(field: keyof RegisterErrors, value: string) { if (field === "team") setRegTeam(value); if (field === "username") setRegUsername(value); if (field === "password") setRegPassword(value); if (field === "password2") setRegPassword2(value); setError(""); setRegErrors((current) => ({ ...current, [field]: undefined })); } function validateRegister() { if (inviteState.status !== "valid") { setError("请先验证邀请码"); return false; } const next: RegisterErrors = {}; // 仅开团队码需要填团队名;加入码用只读团队名 if (inviteState.kind === "create_team" && !regTeam.trim()) next.team = "请输入团队名"; if (!regUsername.trim()) next.username = "请输入用户名"; if (!regPassword) next.password = "请输入密码"; else if (regPassword.length < 8) next.password = "密码至少 8 位"; if (!regPassword2) next.password2 = "请再次输入密码"; else if (regPassword && regPassword2 !== regPassword) next.password2 = "两次密码不一致"; if (!regAgree) next.agree = "请阅读并同意用户协议"; setRegErrors(next); if (Object.keys(next).length > 0) { setError("请补全必填字段后再继续"); return false; } return true; } async function submitRegister(event?: FormEvent) { event?.preventDefault(); if (busy) return; if (!validateRegister() || inviteState.status !== "valid") return; setBusy(true); setError(""); setLoginProgress("entering"); try { const payload = await api.register({ username: regUsername.trim(), password: regPassword, invite_code: regInvite.trim().toUpperCase(), // 开团队码才带团队名;加入码服务端按邀请所属团队入伙 ...(inviteState.kind === "create_team" ? { team_name: regTeam.trim() } : {}) }); // 注册即登录:记住新账号用户名,与登录一致地交给 onAuthed 完成身份+数据水合 persistRemember(regUsername.trim(), true); await onAuthed({ ...payload, remember: true }); } catch (err) { setLoginProgress(null); const raw = err instanceof Error ? err.message : ""; const friendly = /already exists|unique|taken/i.test(raw) ? "该用户名已被占用,换一个试试" : /invite|invitation|邀请码/i.test(raw) ? "邀请码无效或已过期,请重新验证" : raw || "提交失败,请稍后重试"; setError(friendly); } finally { setBusy(false); } } const brand = ( ); if (mode === "register") { return (
{brand}

注册

// /auth/register

{inviteState.status === "valid" ? inviteState.kind === "create_team" ? "邀请码有效 · 开新团队,你将成为团队超管。" : `邀请码有效 · 加入「${inviteState.teamName || "团队"}」。` : "输入邀请码开通账户 —— 没有码请联系团队管理员或平台。"}

{/* 第一步:邀请码(必填)· 验过才展开后续表单 */}
{inviteState.status === "invalid" &&

{inviteState.detail}

} {inviteState.status === "valid" && (

)}
{/* 第二步:验过码后展开 —— 不暴露任何团队列表 */} {inviteState.status === "valid" && ( <> {inviteState.kind === "create_team" ? (
) : (

邀请码已绑定该团队,你将作为成员加入。

)}
{loginProgress && !error && (
{inviteState.kind === "create_team" ? "正在创建团队,进入工作台…" : "正在加入团队,进入工作台…"}
)} {error &&
{error}
} )} {inviteState.status !== "valid" && error &&
{error}
}
{toast && (
{toast.title}// {toast.sub}
)}
); } return (
{brand}

登录

// /auth/login

用团队账号登录,进入你的工作台与生产管线。

{loginProgress && !error && (
{LOGIN_PROGRESS_COPY[loginProgress]}
)} {error &&
{error}
}
OR
{toast && (
{toast.title}// {toast.sub}
)}
); }