auto-save 2026-05-15 17:00 (~3)

This commit is contained in:
2026-05-15 17:00:51 +08:00
parent 0d57081ad8
commit c53d27d811
3 changed files with 259 additions and 99 deletions

View File

@@ -129,10 +129,7 @@
============================================================ */
.login-page {
background:
radial-gradient(circle at 15% 18%, rgba(64, 93, 230, 0.2), transparent 30%),
radial-gradient(circle at 77% 26%, rgba(245, 135, 32, 0.14), transparent 30%),
linear-gradient(115deg, #f6f7f9 0 42%, #16171c 42% 100%);
background-size: auto, auto, auto;
linear-gradient(112deg, #f7f7f4 0 50%, #111214 50% 100%);
font-family: Roboto, "Geist", "Geist Fallback", sans-serif;
}
.login-page::before {
@@ -143,8 +140,8 @@
background:
linear-gradient(90deg, rgba(40, 40, 40, 0.035) 1px, transparent 1px),
linear-gradient(180deg, rgba(40, 40, 40, 0.035) 1px, transparent 1px),
linear-gradient(180deg, rgba(255, 255, 255, 0.34), transparent 34%, rgba(0, 0, 0, 0.22));
background-size: 48px 48px, 48px 48px, auto;
linear-gradient(180deg, rgba(255, 255, 255, 0.42), transparent 40%, rgba(0, 0, 0, 0.42));
background-size: 56px 56px, 56px 56px, auto;
}
.login-page::after {
content: "";
@@ -152,15 +149,22 @@
inset: 0;
pointer-events: none;
background-image:
linear-gradient(114deg, transparent 0 16%, rgba(64, 93, 230, 0.1) 16% 25%, transparent 25% 53%, rgba(245, 135, 32, 0.09) 53% 61%, transparent 61%),
linear-gradient(rgba(255, 255, 255, 0.055) 1px, transparent 1px);
background-size: 100% 6px;
opacity: 0.72;
linear-gradient(114deg, transparent 0 23%, rgba(255, 255, 255, 0.26) 23% 33%, transparent 33% 54%, rgba(214, 179, 106, 0.12) 54% 62%, transparent 62%),
linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px);
background-size: auto, 100% 6px;
opacity: 0.78;
mix-blend-mode: screen;
}
.login-hero {
isolation: isolate;
color: #282828;
border: 1px solid rgba(255, 255, 255, 0.8);
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.96), rgba(239, 239, 235, 0.9)),
#f7f7f4;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.92),
0 28px 70px rgba(15, 15, 15, 0.16);
}
.login-hero::before {
content: "";
@@ -168,24 +172,23 @@
inset: 0;
z-index: 0;
background:
radial-gradient(circle at 75% 18%, rgba(255, 255, 255, 0.95), transparent 26%),
linear-gradient(160deg, rgba(255, 255, 255, 0.82), rgba(238, 240, 244, 0.72)),
linear-gradient(20deg, transparent 40%, rgba(40, 40, 40, 0.06) 40% 54%, transparent 54%);
linear-gradient(90deg, transparent 0 48%, rgba(255, 255, 255, 0.58) 48% 100%),
linear-gradient(18deg, transparent 0 58%, rgba(214, 179, 106, 0.1) 58% 66%, transparent 66%),
linear-gradient(180deg, rgba(255, 255, 255, 0.55), transparent 48%);
}
.login-hero::after {
content: "";
position: absolute;
left: -18%;
top: 10%;
right: 10%;
bottom: 126px;
z-index: 0;
width: 88%;
height: 68%;
border-radius: 999px;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.54), rgba(240, 224, 190, 0.26), transparent);
filter: blur(18px);
transform: rotate(-11deg);
width: 46%;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(40, 40, 40, 0.16), transparent);
box-shadow: 0 28px 52px rgba(40, 40, 40, 0.22);
pointer-events: none;
}
.login-wordmark,
.login-brand-mark {
display: inline-flex;
align-items: baseline;
@@ -193,15 +196,18 @@
color: #282828;
letter-spacing: 0;
}
.login-wordmark__logo,
.login-brand-mark__logo {
font-size: 26px;
font-weight: 700;
line-height: 1;
}
.login-wordmark__sub,
.login-brand-mark__sub {
font-size: 13px;
color: rgba(40, 40, 40, 0.54);
}
.login-secure-pill,
.login-store-pill {
display: inline-flex;
align-items: center;
@@ -214,6 +220,142 @@
font-size: 13px;
box-shadow: 0 10px 24px rgba(40, 40, 40, 0.18);
}
.login-kicker {
margin: 0 0 16px;
color: rgba(40, 40, 40, 0.48);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.18em;
}
.login-premium-title {
margin: 0;
max-width: 620px;
color: #0d0d0d;
font-size: clamp(44px, 5.5vw, 74px);
font-weight: 600;
line-height: 0.98;
letter-spacing: 0;
}
.login-premium-copy {
margin: 18px 0 0;
max-width: 520px;
color: rgba(40, 40, 40, 0.58);
font-size: 16px;
line-height: 1.7;
}
.login-product-stage {
position: relative;
flex: 1;
min-height: 300px;
margin-top: 26px;
}
.login-product-stage__halo {
position: absolute;
right: 7%;
bottom: 24px;
width: 52%;
height: 42px;
border-radius: 50%;
background: rgba(40, 40, 40, 0.12);
filter: blur(18px);
transform: perspective(500px) rotateX(58deg);
}
.login-product-image {
position: absolute;
right: 1%;
bottom: 0;
width: min(54%, 440px);
max-height: 360px;
object-fit: contain;
filter:
drop-shadow(0 36px 48px rgba(40, 40, 40, 0.22))
drop-shadow(0 4px 0 rgba(255, 255, 255, 0.75));
}
.login-product-caption {
position: absolute;
left: 0;
bottom: 50px;
width: min(310px, 46%);
border-top: 1px solid rgba(40, 40, 40, 0.16);
padding-top: 18px;
color: #151515;
}
.login-product-caption span {
display: block;
color: rgba(40, 40, 40, 0.5);
font-size: 13px;
}
.login-product-caption b {
display: block;
margin-top: 5px;
font-size: 28px;
font-weight: 600;
line-height: 1.05;
}
.login-studio-chip {
position: absolute;
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 40px;
border: 1px solid rgba(255, 255, 255, 0.72);
border-radius: 999px;
background: rgba(255, 255, 255, 0.72);
padding: 0 14px;
color: rgba(40, 40, 40, 0.68);
font-size: 13px;
box-shadow: 0 18px 40px rgba(40, 40, 40, 0.1);
backdrop-filter: blur(18px);
}
.login-studio-chip--visual {
right: 3%;
top: 28px;
}
.login-studio-chip--review {
right: 35%;
bottom: 104px;
}
.login-premium-metrics {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
margin-top: 24px;
overflow: hidden;
border: 1px solid rgba(40, 40, 40, 0.1);
border-radius: 8px;
background: rgba(40, 40, 40, 0.1);
}
.login-premium-metric {
min-height: 78px;
background: rgba(255, 255, 255, 0.62);
padding: 14px 16px;
}
.login-premium-metric span {
display: block;
color: rgba(40, 40, 40, 0.48);
font-size: 12px;
}
.login-premium-metric b {
display: block;
margin-top: 8px;
color: #121212;
font-size: 20px;
font-weight: 600;
}
.login-auth-panel {
border: 1px solid rgba(255, 255, 255, 0.1);
background:
linear-gradient(180deg, rgba(22, 23, 28, 0.98), rgba(10, 11, 14, 0.98)),
#101114;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.06),
0 32px 70px rgba(0, 0, 0, 0.35);
}
.login-auth-icon {
background: linear-gradient(145deg, #2b2b2b, #111);
border: 1px solid rgba(214, 179, 106, 0.42);
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.3), 0 0 0 5px rgba(214, 179, 106, 0.06);
}
.login-product-ribbon {
display: flex;
flex-wrap: wrap;
@@ -644,6 +786,46 @@
100% { translate: 0 -8px; }
}
@media (max-width: 720px) {
.login-page {
background: linear-gradient(164deg, #f7f7f4 0 48%, #111214 48% 100%);
}
.login-premium-title {
font-size: 34px;
}
.login-premium-copy {
font-size: 14px;
}
.login-product-stage {
min-height: 340px;
}
.login-product-image {
right: -18%;
bottom: 28px;
width: 86%;
max-height: 280px;
}
.login-product-caption {
bottom: 34px;
width: 48%;
}
.login-product-caption b {
font-size: 22px;
}
.login-studio-chip {
font-size: 12px;
}
.login-studio-chip--visual {
right: 0;
top: 12px;
}
.login-studio-chip--review {
left: 0;
right: auto;
bottom: 118px;
}
.login-premium-metrics {
grid-template-columns: 1fr;
}
.login-character-stage {
min-height: 270px;
}

View File

@@ -1,10 +1,11 @@
"use client"
import type { FormEvent } from "react"
import { useEffect, useMemo, useState } from "react"
import { useState } from "react"
import {
AlertCircle,
ArrowRight,
BadgeCheck,
CheckCircle2,
Eye,
EyeOff,
@@ -13,39 +14,19 @@ import {
Sparkles,
UserRound,
} from "lucide-react"
import { AnimatedLoginCharacters, type LoginCharacterMood } from "@/components/login/animated-login-characters"
type LoginStatus = "idle" | "loading" | "success"
const PRODUCT_IMAGE =
"https://www.skg.com/cdn/shop/files/17531530438558.png?v=1754462861&width=900"
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 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])
const disabled = status === "loading" || status === "success"
@@ -85,63 +66,64 @@ export default function LoginPage() {
}
return (
<main
className="login-page relative min-h-screen overflow-hidden px-5 py-6 text-white sm:px-8 lg:px-10"
>
<main className="login-page relative min-h-screen overflow-hidden px-5 py-6 text-white sm:px-8 lg:px-10">
<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,1.08fr)_minmax(380px,460px)] lg:items-stretch">
<section className="login-hero order-2 relative min-h-[470px] overflow-hidden rounded-[8px] border border-white/55 bg-[#f3f4f6] p-6 text-[#282828] shadow-2xl shadow-black/20 sm:p-8 lg:order-1 lg:min-h-[620px]">
<div className="relative z-10 flex h-full flex-col justify-between gap-8">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<div className="login-brand-mark">
<span className="login-brand-mark__logo">SKG</span>
<span className="login-brand-mark__sub"></span>
</div>
<div className="mt-5 flex items-center gap-3">
<div className="grid h-11 w-11 place-items-center rounded-[8px] bg-[#282828] text-white">
<Sparkles className="h-5 w-5" />
</div>
<div>
<p className="text-sm text-[#666]">SKG Marketing Studio</p>
<h1 className="text-2xl font-semibold leading-tight text-[#111] sm:text-4xl"></h1>
</div>
</div>
<div className="grid w-full gap-5 lg:grid-cols-[minmax(0,1.16fr)_minmax(380px,440px)] lg:items-stretch">
<section className="login-hero order-2 relative min-h-[540px] overflow-hidden rounded-[8px] p-6 text-[#171717] sm:p-8 lg:order-1 lg:min-h-[660px]">
<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"></span>
</div>
<div className="login-store-pill">
<div className="login-secure-pill">
<ShieldCheck className="h-4 w-4" />
<span></span>
<span>Secure Studio</span>
</div>
</div>
<div className="login-product-ribbon" aria-hidden="true">
<span>Neck Relief</span>
<span>Visual Assets</span>
<span>Product Video</span>
<span>Daily Care</span>
<div className="mt-10 max-w-[620px]">
<p className="login-kicker">MARKETING CONTENT STUDIO</p>
<h1 className="login-premium-title"></h1>
<p className="login-premium-copy">SKG 广</p>
</div>
<AnimatedLoginCharacters mood={mood} eyeOffset={eyeOffset} />
<div className="login-product-stage" aria-label="SKG product visual">
<div className="login-product-stage__halo" />
<img className="login-product-image" src={PRODUCT_IMAGE} alt="SKG neck massager" />
<div className="login-product-caption">
<span>G7 Pro Fold</span>
<b>Neck Massager</b>
</div>
<div className="login-studio-chip login-studio-chip--visual">
<Sparkles className="h-4 w-4" />
<span>Visual Asset Flow</span>
</div>
<div className="login-studio-chip login-studio-chip--review">
<BadgeCheck className="h-4 w-4" />
<span>Brand Review Ready</span>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<div className="login-premium-metrics">
{[
["Shop by Need", "素材"],
["Recovery", "声音"],
["Self-Care", "成片"],
["Visual", "素材"],
["Audio", "声音"],
["Video", "成片"],
].map(([label, value]) => (
<div key={label} className="login-skg-tile rounded-[8px] px-4 py-3">
<p className="text-xs text-[#757575]">{label}</p>
<p className="mt-1 text-lg font-semibold text-[#111]">{value}</p>
<div key={label} className="login-premium-metric">
<span>{label}</span>
<b>{value}</b>
</div>
))}
</div>
</div>
</section>
<section className="order-1 flex min-h-[470px] items-center rounded-[8px] border border-white/10 bg-[#10121d]/95 p-5 shadow-2xl shadow-black/40 sm:p-8 lg:order-2 lg:min-h-[620px]">
<section className="login-auth-panel order-1 flex min-h-[470px] items-center rounded-[8px] p-5 sm:p-8 lg:order-2 lg:min-h-[660px]">
<form className="w-full" onSubmit={onSubmit}>
<div className="mb-8">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-[8px] bg-[#6c3ff5] text-white shadow-lg shadow-[#6c3ff5]/35">
<div className="login-auth-icon mb-4 inline-flex h-12 w-12 items-center justify-center rounded-[8px] text-white">
<LockKeyhole className="h-5 w-5" />
</div>
<h2 className="text-2xl font-semibold text-white"></h2>
@@ -151,7 +133,7 @@ export default function LoginPage() {
<div className="space-y-4">
<label className="block">
<span className="mb-2 block text-sm font-medium text-white/70"></span>
<span className="flex h-12 items-center gap-3 rounded-[8px] border border-white/10 bg-black/25 px-3 text-white transition focus-within:border-[#8d6cff] focus-within:bg-black/35 focus-within:ring-2 focus-within:ring-[#8d6cff]/30">
<span className="flex h-12 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"
@@ -159,8 +141,6 @@ export default function LoginPage() {
disabled={disabled}
autoComplete="username"
placeholder="请输入账号"
onFocus={() => setActiveField("username")}
onBlur={() => setActiveField(null)}
onChange={(event) => {
setUsername(event.target.value)
if (error) setError("")
@@ -171,7 +151,7 @@ export default function LoginPage() {
<label className="block">
<span className="mb-2 block text-sm font-medium text-white/70"></span>
<span className="flex h-12 items-center gap-3 rounded-[8px] border border-white/10 bg-black/25 px-3 text-white transition focus-within:border-[#8d6cff] focus-within:bg-black/35 focus-within:ring-2 focus-within:ring-[#8d6cff]/30">
<span className="flex h-12 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"
@@ -180,15 +160,13 @@ 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("")
}}
/>
<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-[#8d6cff]/45 disabled:opacity-50"
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 ? "隐藏密码" : "显示密码"}
@@ -204,7 +182,7 @@ export default function LoginPage() {
<div className="mt-4 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-[#6c3ff5]"
className="h-4 w-4 rounded border-white/20 bg-black/30 accent-[#c89b3c]"
type="checkbox"
checked={remember}
disabled={disabled}
@@ -230,7 +208,7 @@ export default function LoginPage() {
</div>
<button
className="mt-2 flex h-12 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-[#f2f0ff] focus:outline-none focus:ring-2 focus:ring-[#8d6cff]/60 disabled:cursor-wait disabled:opacity-70"
className="mt-2 flex h-12 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}
>