"use client" import type { FormEvent } from "react" import { useEffect, useMemo, useRef, useState } from "react" import { ArrowRight, Building2, CheckCircle2, Eye, EyeOff, LockKeyhole, UserRound, } from "lucide-react" import { AnimatedLoginCharacters, type LoginCharacterMood } from "@/components/login/animated-login-characters" import { OasisCanvas } from "@/components/login/oasis-canvas" type LoginStatus = "idle" | "loading" | "success" type AuthConfig = { auth_configured?: boolean password_enabled?: boolean feishu_enabled?: boolean } function normalizeNextPath(value: string | null | undefined) { const next = (value || "/").trim() || "/" if (!next.startsWith("/") || next.startsWith("//")) return "/" return next } function loginNextPath() { if (typeof window === "undefined") return "/" return normalizeNextPath(new URLSearchParams(window.location.search).get("next")) } export default function LoginPage() { const [authConfig, setAuthConfig] = useState(null) const [nextPath] = useState(loginNextPath) const [username, setUsername] = useState("") const [password, setPassword] = useState("") const [remember, setRemember] = useState(true) const [showPassword, setShowPassword] = useState(false) const [activeField, setActiveField] = useState<"username" | "password" | null>(null) const [hasError, setHasError] = useState(false) const [status, setStatus] = useState("idle") const [eyeOffset, setEyeOffset] = useState({ x: 0, y: 0 }) const autoFeishuAttemptedRef = useRef(false) useEffect(() => { let cancelled = false fetch("/api/auth/config", { cache: "no-store", credentials: "include" }) .then((res) => res.ok ? res.json() : null) .then((data) => { if (!cancelled && data) setAuthConfig(data) }) .catch(() => { if (!cancelled) setAuthConfig(null) }) return () => { cancelled = true } }, []) useEffect(() => { // skip touch / coarse pointers — the eye-follow effect is pointless there and // would thrash state (and battery) on scroll-driven pointer events if (typeof window !== "undefined" && window.matchMedia?.("(pointer: coarse)").matches) return let frame = 0 const onPointerMove = (event: PointerEvent) => { if (frame) return // coalesce to at most one state update per animation frame frame = window.requestAnimationFrame(() => { frame = 0 const centerX = window.innerWidth / 2 const centerY = window.innerHeight / 2 const nextX = Math.max(-1, Math.min(1, (event.clientX - centerX) / centerX)) const nextY = Math.max(-1, Math.min(1, (event.clientY - centerY) / centerY)) setEyeOffset({ x: nextX * 8, y: nextY * 5.5 }) }) } window.addEventListener("pointermove", onPointerMove) return () => { window.removeEventListener("pointermove", onPointerMove) if (frame) window.cancelAnimationFrame(frame) } }, []) const disabled = status === "loading" || status === "success" const feishuEnabled = Boolean(authConfig?.feishu_enabled) const passwordEnabled = authConfig?.password_enabled ?? true useEffect(() => { if (!feishuEnabled || status !== "idle" || autoFeishuAttemptedRef.current) return const attemptKey = `skg-feishu-auto-login:${nextPath}` if (window.sessionStorage.getItem(attemptKey) === "1") return window.sessionStorage.setItem(attemptKey, "1") autoFeishuAttemptedRef.current = true setStatus("loading") window.location.href = `/api/auth/feishu/start?next=${encodeURIComponent(nextPath)}` }, [feishuEnabled, nextPath, status]) const mood: LoginCharacterMood = useMemo(() => { if (status === "success") return "success" if (hasError) return "error" if (showPassword && activeField === "password") return "peek" if (activeField || username || password) return "typing" return "idle" }, [activeField, hasError, password, showPassword, status, username]) async function onSubmit(event: FormEvent) { event.preventDefault() setHasError(false) if (!passwordEnabled) return if (!username.trim() || !password) { setHasError(true) return } setStatus("loading") try { const res = await fetch("/api/auth/login", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password, remember }), }) if (!res.ok) { throw new Error() } setStatus("success") window.setTimeout(() => { window.location.href = nextPath }, 420) } catch { setStatus("idle") setHasError(true) } } function onFeishuLogin() { setStatus("loading") window.location.href = `/api/auth/feishu/start?next=${encodeURIComponent(nextPath)}` } return (
{feishuEnabled ? ( ) : null} {feishuEnabled && passwordEnabled ? (
备用账号
) : null} {passwordEnabled ? (
) : null} {passwordEnabled ? ( ) : null} {status === "success" ? (
) : null} {passwordEnabled ? ( ) : null}
) }