/** * LoginPage - 全屏登录页面 * * 强制 SaaS 登录门禁页面。用户必须登录后才能进入系统。 * 深色渐变背景 + 毛玻璃卡片设计。 * 复用 saasStore 的 login/register/loginWithTotp 逻辑。 */ import { useState, useEffect, useRef } from 'react'; import { motion } from 'framer-motion'; import { LogIn, UserPlus, Eye, EyeOff, Loader2, AlertCircle, Mail, Shield, ShieldCheck, ArrowLeft, User, } from 'lucide-react'; import { useSaaSStore } from '../store/saasStore'; import { cn } from '../lib/utils'; import { isTauriRuntime } from '../lib/tauri-gateway'; // === Constants === const PRODUCTION_SAAS_URL = 'https://saas.zclaw.com'; const DEV_SAAS_URL = 'http://127.0.0.1:8080'; // === Animation Variants === const cardVariants = { hidden: { opacity: 0, y: 20, scale: 0.96 }, visible: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.5, ease: [0.16, 1, 0.3, 1] as const }, }, }; const fieldVariants = { hidden: { opacity: 0, y: 10 }, visible: (i: number) => ({ opacity: 1, y: 0, transition: { delay: 0.15 + i * 0.05, duration: 0.35, ease: 'easeOut' as const }, }), }; // === ZCLAW Logo SVG === function ZclawLogo({ className }: { className?: string }) { return ( Z ); } // === Helpers === /** 根据运行环境自动选择 SaaS 服务器地址 */ function getSaasUrl(): string { if (import.meta.env.DEV) return DEV_SAAS_URL; return isTauriRuntime() ? PRODUCTION_SAAS_URL : DEV_SAAS_URL; } // === Component === export function LoginPage() { const { login, loginWithTotp, register, isLoading, error: storeError, totpRequired, clearError, } = useSaaSStore(); // Form state const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [email, setEmail] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [displayName, setDisplayName] = useState(''); const [showPassword, setShowPassword] = useState(false); const [isRegister, setIsRegister] = useState(false); const [localError, setLocalError] = useState(null); const [totpCode, setTotpCode] = useState(''); const [showTotpStep, setShowTotpStep] = useState(false); // Sync TOTP required from store & clear stale errors on mount const mountedRef = useRef(false); useEffect(() => { clearError(); mountedRef.current = true; }, [clearError]); // Sync TOTP required from store useEffect(() => { if (totpRequired && !showTotpStep) { setShowTotpStep(true); } }, [totpRequired, showTotpStep]); // 屏蔽首次挂载时从 bootstrap/connectionStore 残留的 storeError const displayError = mountedRef.current ? (storeError || localError) : localError; const clearErrors = () => setLocalError(null); // === Login Handler === const handleLoginSubmit = async (e: React.FormEvent) => { e.preventDefault(); clearErrors(); if (!username.trim()) { setLocalError('请输入用户名'); return; } if (!password) { setLocalError('请输入密码'); return; } try { await login(getSaasUrl(), username.trim(), password); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); setLocalError(message); } }; // === Register Handler === const handleRegisterSubmit = async (e: React.FormEvent) => { e.preventDefault(); clearErrors(); if (!username.trim()) { setLocalError('请输入用户名'); return; } if (!email.trim()) { setLocalError('请输入邮箱地址'); return; } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) { setLocalError('邮箱格式不正确'); return; } if (password.length < 6) { setLocalError('密码长度至少 6 个字符'); return; } if (password !== confirmPassword) { setLocalError('两次输入的密码不一致'); return; } try { await register( getSaasUrl(), username.trim(), email.trim(), password, displayName.trim() || undefined, ); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); setLocalError(message); } }; // === TOTP Handler === const handleTotpSubmit = async () => { if (totpCode.length !== 6) return; clearErrors(); try { await loginWithTotp(getSaasUrl(), username.trim(), password, totpCode); setTotpCode(''); setShowTotpStep(false); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); setLocalError(message); } }; // === Tab Switch === const handleTabSwitch = (registerMode: boolean) => { setIsRegister(registerMode); clearErrors(); setConfirmPassword(''); setEmail(''); setDisplayName(''); }; const handleBackToLogin = () => { setShowTotpStep(false); setTotpCode(''); clearErrors(); }; // === Input class helper === const inputClass = cn( 'w-full px-4 py-3 rounded-lg text-sm text-white placeholder-white/40', 'bg-white/5 border border-white/10', 'focus:outline-none focus:ring-2 focus:ring-emerald-400/30 focus:border-emerald-400/50', 'transition-all duration-200', 'disabled:opacity-50 disabled:cursor-not-allowed', ); return (
{/* Animated gradient background */}
{/* Radial glow */}
{/* Card */}
{/* TOTP Verification Step */} {showTotpStep ? (

双因素认证

此账号已启用双因素认证,请输入验证码

setTotpCode(e.target.value.replace(/\D/g, ''))} placeholder="000000" autoComplete="one-time-code" autoFocus disabled={isLoading} className={cn(inputClass, 'text-center font-mono text-2xl tracking-[0.5em] py-4')} onKeyDown={(e) => { if (e.key === 'Enter' && totpCode.length === 6) handleTotpSubmit(); }} /> {displayError && ( {displayError} )}
) : ( <> {/* Logo & Brand */}

ZCLAW

{isRegister ? '创建账号,开始使用' : '登录以继续'}

{/* Tab Switcher */} {/* Form */}
{/* Username */}
setUsername(e.target.value)} placeholder="用户名" autoComplete="username" disabled={isLoading} className={cn(inputClass, 'pl-10')} />
{/* Email (Register only) */} {isRegister && (
setEmail(e.target.value)} placeholder="邮箱地址" autoComplete="email" disabled={isLoading} className={cn(inputClass, 'pl-10')} />
)} {/* Display Name (Register only, optional) */} {isRegister && ( setDisplayName(e.target.value)} placeholder="显示名称(可选)" autoComplete="name" disabled={isLoading} className={inputClass} /> )} {/* Password */}
setPassword(e.target.value)} placeholder={isRegister ? '密码(至少 6 位)' : '密码'} autoComplete={isRegister ? 'new-password' : 'current-password'} disabled={isLoading} className={cn(inputClass, 'pr-10')} />
{/* Confirm Password (Register only) */} {isRegister && ( setConfirmPassword(e.target.value)} placeholder="确认密码" autoComplete="new-password" disabled={isLoading} className={inputClass} /> )} {/* Error Display */} {displayError && ( {displayError} )} {/* Submit Button */}
{/* Version footer */} ZCLAW AI Agent Platform )}
); }