Files
20260512-skg-tk/web/app/login/page.tsx
2026-05-15 19:02:43 +08:00

249 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import type { FormEvent } from "react"
import { useEffect, useMemo, useState } from "react"
import {
AlertCircle,
ArrowRight,
CheckCircle2,
Eye,
EyeOff,
LockKeyhole,
ShieldCheck,
Sparkles,
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"
export default function LoginPage() {
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 [error, setError] = useState("")
const [status, setStatus] = useState<LoginStatus>("idle")
const [eyeOffset, setEyeOffset] = useState({ x: 0, y: 0 })
useEffect(() => {
const onPointerMove = (event: PointerEvent) => {
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)
}, [])
const disabled = status === "loading" || status === "success"
const mood: LoginCharacterMood = useMemo(() => {
if (status === "success") return "success"
if (error) return "error"
if (showPassword && activeField === "password") return "peek"
if (activeField || username || password) return "typing"
return "idle"
}, [activeField, error, password, showPassword, status, username])
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
setError("")
if (!username.trim() || !password) {
setError("请输入访问账号和访问密钥")
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) {
let message = "访问账号或密钥不正确"
try {
const data = await res.json()
message = data?.detail || data?.error || message
} catch {
// keep default message
}
throw new Error(message)
}
setStatus("success")
window.setTimeout(() => {
window.location.href = "/"
}, 420)
} catch (err) {
setStatus("idle")
setError(err instanceof Error ? err.message : "验证失败,请稍后再试")
}
}
return (
<main className="login-page login-page--oasis relative min-h-screen overflow-hidden px-5 py-6 text-white sm:px-8 lg:px-10">
<OasisCanvas />
<div className="login-oasis-shade" />
<div className="relative z-10 mx-auto flex min-h-[calc(100vh-3rem)] w-full max-w-7xl items-center">
<div className="grid w-full gap-5 lg:grid-cols-[minmax(0,1fr)_minmax(300px,340px)] lg:items-center">
<section className="login-hero login-oasis-hero order-2 relative min-h-[540px] overflow-hidden p-1 text-white sm:p-2 lg:order-1 lg:min-h-[620px]">
<div className="relative z-10 flex h-full flex-col">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="login-wordmark">
<span className="login-wordmark__logo">SKG</span>
<span className="login-wordmark__sub">Creative OS</span>
</div>
<div className="login-secure-pill">
<ShieldCheck className="h-4 w-4" />
<span>Internal Access</span>
</div>
</div>
<div className="mt-10 max-w-[620px]">
<p className="login-kicker">CONTENT PRODUCTION SYSTEM</p>
<h1 className="login-premium-title"></h1>
<p className="login-premium-copy"></p>
</div>
<div className="login-creative-stage" aria-label="Creative workflow visual">
<div className="login-creative-caption">
<span>Creative Pipeline</span>
<b>Pipeline ready</b>
</div>
<div className="login-dynamic-dock">
<span className="login-dynamic-dock__label">Live Creative Modules</span>
<AnimatedLoginCharacters mood={mood} eyeOffset={eyeOffset} />
</div>
<div className="login-studio-chip login-studio-chip--visual">
<Sparkles className="h-4 w-4" />
<span>Pipeline Online</span>
</div>
</div>
<div className="login-premium-metrics">
{[
["Frame Lab", "画面"],
["Sound Lab", "声音"],
["Final Cut", "成片"],
].map(([label, value]) => (
<div key={label} className="login-premium-metric">
<span>{label}</span>
<b>{value}</b>
</div>
))}
</div>
</div>
</section>
<section className="login-auth-panel order-1 flex items-center rounded-[8px] p-5 sm:p-6 lg:order-2 lg:p-6">
<form className="w-full" onSubmit={onSubmit}>
<div className="mb-5">
<div className="login-auth-icon mb-3 inline-flex h-9 w-9 items-center justify-center rounded-[8px] text-white">
<LockKeyhole className="h-5 w-5" />
</div>
<h2 className="text-xl font-semibold text-white"></h2>
<p className="mt-2 text-sm leading-6 text-white/55"></p>
</div>
<div className="space-y-3">
<label className="block">
<span className="mb-2 block text-sm font-medium text-white/70">访</span>
<span className="flex h-11 items-center gap-3 rounded-[8px] border border-white/10 bg-black/25 px-3 text-white transition focus-within:border-[#d6b36a] focus-within:bg-black/35 focus-within:ring-2 focus-within:ring-[#d6b36a]/25">
<UserRound className="h-4 w-4 text-white/45" />
<input
className="h-full min-w-0 flex-1 bg-transparent text-base text-white outline-none placeholder:text-white/30"
value={username}
disabled={disabled}
autoComplete="username"
placeholder="输入访问账号"
onFocus={() => setActiveField("username")}
onBlur={() => setActiveField(null)}
onChange={(event) => {
setUsername(event.target.value)
if (error) setError("")
}}
/>
</span>
</label>
<label className="block">
<span className="mb-2 block text-sm font-medium text-white/70">访</span>
<span className="flex h-11 items-center gap-3 rounded-[8px] border border-white/10 bg-black/25 px-3 text-white transition focus-within:border-[#d6b36a] focus-within:bg-black/35 focus-within:ring-2 focus-within:ring-[#d6b36a]/25">
<LockKeyhole className="h-4 w-4 text-white/45" />
<input
className="h-full min-w-0 flex-1 bg-transparent text-base text-white outline-none placeholder:text-white/30"
value={password}
disabled={disabled}
type={showPassword ? "text" : "password"}
autoComplete="current-password"
placeholder="输入访问密钥"
onFocus={() => setActiveField("password")}
onBlur={() => setActiveField(null)}
onChange={(event) => {
setPassword(event.target.value)
if (error) setError("")
}}
/>
<button
className="grid h-9 w-9 place-items-center rounded-[8px] text-white/55 transition hover:bg-white/10 hover:text-white focus:outline-none focus:ring-2 focus:ring-[#d6b36a]/45 disabled:opacity-50"
type="button"
disabled={disabled}
aria-label={showPassword ? "隐藏密码" : "显示密码"}
onMouseDown={(event) => event.preventDefault()}
onClick={() => setShowPassword((value) => !value)}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</span>
</label>
</div>
<div className="mt-3 flex items-center justify-between gap-4">
<label className="flex cursor-pointer items-center gap-2 text-sm text-white/60">
<input
className="h-4 w-4 rounded border-white/20 bg-black/30 accent-[#c89b3c]"
type="checkbox"
checked={remember}
disabled={disabled}
onChange={(event) => setRemember(event.target.checked)}
/>
<span></span>
</label>
<span className="text-xs text-white/35">marketing.skg.com</span>
</div>
<div className="mt-4 min-h-9" aria-live="polite">
{error ? (
<div className="flex items-start gap-2 rounded-[8px] border border-red-400/30 bg-red-500/10 px-3 py-2 text-sm text-red-100">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{error}</span>
</div>
) : status === "success" ? (
<div className="flex items-start gap-2 rounded-[8px] border border-emerald-300/30 bg-emerald-400/10 px-3 py-2 text-sm text-emerald-100">
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" />
<span></span>
</div>
) : null}
</div>
<button
className="mt-1 flex h-11 w-full items-center justify-center gap-2 rounded-[8px] bg-white px-4 text-base font-semibold text-black shadow-xl shadow-black/25 transition hover:bg-[#f5efe3] focus:outline-none focus:ring-2 focus:ring-[#d6b36a]/60 disabled:cursor-wait disabled:opacity-70"
type="submit"
disabled={disabled}
>
<span>{status === "loading" ? "验证中" : status === "success" ? "已验证" : "打开工作台"}</span>
<ArrowRight className="h-4 w-4" />
</button>
</form>
</section>
</div>
</div>
</main>
)
}