auto-save 2026-05-15 17:11 (~6)
This commit is contained in:
@@ -296,6 +296,50 @@
|
||||
font-weight: 600;
|
||||
line-height: 1.05;
|
||||
}
|
||||
.login-dynamic-dock {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 118px;
|
||||
width: 252px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.64);
|
||||
padding: 12px;
|
||||
box-shadow: 0 22px 52px rgba(40, 40, 40, 0.12);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
.login-dynamic-dock__label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(40, 40, 40, 0.46);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.login-dynamic-dock .login-character-stage {
|
||||
min-height: 116px;
|
||||
border-color: rgba(40, 40, 40, 0.08);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(40, 40, 40, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(180deg, rgba(40, 40, 40, 0.04) 1px, transparent 1px),
|
||||
rgba(255, 255, 255, 0.5);
|
||||
background-size: 22px 22px, 22px 22px, auto;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.84);
|
||||
}
|
||||
.login-dynamic-dock .login-character-stage::after {
|
||||
height: 48%;
|
||||
background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.7));
|
||||
}
|
||||
.login-dynamic-dock .login-stage-grid {
|
||||
inset: 12px;
|
||||
border-color: rgba(40, 40, 40, 0.08);
|
||||
}
|
||||
.login-dynamic-dock .login-characters-container {
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translateX(-50%) scale(0.28);
|
||||
}
|
||||
.login-studio-chip {
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
@@ -816,6 +860,18 @@
|
||||
.login-product-caption b {
|
||||
font-size: 22px;
|
||||
}
|
||||
.login-dynamic-dock {
|
||||
left: 0;
|
||||
bottom: 132px;
|
||||
width: 210px;
|
||||
padding: 10px;
|
||||
}
|
||||
.login-dynamic-dock .login-character-stage {
|
||||
min-height: 94px;
|
||||
}
|
||||
.login-dynamic-dock .login-characters-container {
|
||||
transform: translateX(-50%) scale(0.22);
|
||||
}
|
||||
.login-studio-chip {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import type { FormEvent } from "react"
|
||||
import { useState } from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import {
|
||||
AlertCircle,
|
||||
ArrowRight,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Sparkles,
|
||||
UserRound,
|
||||
} from "lucide-react"
|
||||
import { AnimatedLoginCharacters, type LoginCharacterMood } from "@/components/login/animated-login-characters"
|
||||
|
||||
type LoginStatus = "idle" | "loading" | "success"
|
||||
|
||||
@@ -25,11 +26,33 @@ export default function LoginPage() {
|
||||
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("")
|
||||
@@ -95,6 +118,10 @@ export default function LoginPage() {
|
||||
<span>G7 Pro Fold</span>
|
||||
<b>Neck Massager</b>
|
||||
</div>
|
||||
<div className="login-dynamic-dock">
|
||||
<span className="login-dynamic-dock__label">Live Studio Modules</span>
|
||||
<AnimatedLoginCharacters mood={mood} eyeOffset={eyeOffset} />
|
||||
</div>
|
||||
<div className="login-studio-chip login-studio-chip--visual">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
<span>Visual Asset Flow</span>
|
||||
@@ -141,6 +168,8 @@ export default function LoginPage() {
|
||||
disabled={disabled}
|
||||
autoComplete="username"
|
||||
placeholder="请输入账号"
|
||||
onFocus={() => setActiveField("username")}
|
||||
onBlur={() => setActiveField(null)}
|
||||
onChange={(event) => {
|
||||
setUsername(event.target.value)
|
||||
if (error) setError("")
|
||||
@@ -160,6 +189,8 @@ export default function LoginPage() {
|
||||
type={showPassword ? "text" : "password"}
|
||||
autoComplete="current-password"
|
||||
placeholder="请输入密码"
|
||||
onFocus={() => setActiveField("password")}
|
||||
onBlur={() => setActiveField(null)}
|
||||
onChange={(event) => {
|
||||
setPassword(event.target.value)
|
||||
if (error) setError("")
|
||||
|
||||
Reference in New Issue
Block a user