fix(auth): 登录主题 - 跟随应用外观并补齐回归
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { ConfigProvider, Tabs } from "antd";
|
||||
import { Tabs } from "antd";
|
||||
import { ArrowLeft, Play } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Link, Outlet, useLocation, useNavigate } from "react-router";
|
||||
@@ -7,8 +7,8 @@ import { Link, Outlet, useLocation, useNavigate } from "react-router";
|
||||
import { BrandLogo } from "@/components/brand/brand-logo";
|
||||
import { SiteComplianceFooter } from "@/components/layout/site-compliance-footer";
|
||||
import { aceternityMotion } from "@/lib/aceternity-motion";
|
||||
import { getAntThemeConfig } from "@/lib/app-theme";
|
||||
import { brandStudioLabel, useAppearanceStore } from "@/stores/use-appearance-store";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
|
||||
const AUTH_TABS = [
|
||||
{ key: "login", label: "登录" },
|
||||
@@ -48,6 +48,7 @@ export function LinuxDOIcon() {
|
||||
|
||||
export function AuthScene() {
|
||||
const appearance = useAppearanceStore((state) => state.appearance);
|
||||
const theme = useThemeStore((state) => state.theme);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const reducedMotion = useReducedMotion();
|
||||
@@ -74,18 +75,18 @@ export function AuthScene() {
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="auth-scene h-dvh min-h-0 overflow-y-auto text-white lg:overflow-hidden">
|
||||
<main className="auth-scene h-dvh min-h-0 overflow-y-auto lg:overflow-hidden">
|
||||
<div className="grid min-h-full lg:h-full lg:grid-cols-[minmax(0,1.32fr)_minmax(520px,1fr)]">
|
||||
<section className="relative min-h-[250px] overflow-hidden sm:min-h-[320px] lg:min-h-0" aria-label={`${appearance.brandName}品牌影片`}>
|
||||
<section className="auth-scene-hero relative min-h-[250px] overflow-hidden sm:min-h-[320px] lg:min-h-0" aria-label={`${appearance.brandName}品牌影片`}>
|
||||
{videoActive && appearance.authVideoUrl ? <video ref={videoRef} className="absolute inset-0 size-full object-cover" src={appearance.authVideoUrl} poster={appearance.authVideoPosterUrl || undefined} autoPlay muted loop playsInline preload="metadata" onPlay={() => setVideoPlaying(true)} onPause={() => setVideoPlaying(false)} /> : appearance.authVideoPosterUrl && failedPosterURL !== appearance.authVideoPosterUrl ? <img className="absolute inset-0 size-full object-cover" src={appearance.authVideoPosterUrl} alt="" decoding="async" onError={() => setFailedPosterURL(appearance.authVideoPosterUrl)} /> : null}
|
||||
<div aria-hidden className="absolute inset-0 bg-[linear-gradient(180deg,rgba(4,5,8,.58),transparent_42%,rgba(4,5,8,.74))]" />
|
||||
<div aria-hidden className="auth-scene-hero-overlay absolute inset-0" />
|
||||
<div aria-hidden className="auth-scene-video-blend absolute inset-y-0 right-0 hidden w-[clamp(120px,14vw,240px)] lg:block" />
|
||||
<div className="absolute inset-x-0 top-0 flex items-center justify-between gap-4 p-5 sm:p-7 lg:p-9">
|
||||
<Link to="/" className="inline-flex items-center gap-2.5 text-sm font-semibold text-white drop-shadow-sm transition-opacity hover:opacity-80">
|
||||
<BrandLogo theme="dark" className="size-7" alt="" fallback={<span className="size-7 bg-current" style={{ mask: "url(/logo.svg) center / contain no-repeat", WebkitMask: "url(/logo.svg) center / contain no-repeat" }} />} />
|
||||
<Link to="/" className="auth-scene-brand inline-flex items-center gap-2.5 text-sm font-semibold drop-shadow-sm transition-opacity hover:opacity-80">
|
||||
<BrandLogo theme={theme} className="size-7" alt="" fallback={<span className="size-7 bg-current" style={{ mask: "url(/logo.svg) center / contain no-repeat", WebkitMask: "url(/logo.svg) center / contain no-repeat" }} />} />
|
||||
{appearance.brandName}
|
||||
</Link>
|
||||
<button type="button" className="inline-flex items-center gap-2 rounded-full border border-white/16 bg-black/20 px-3 py-1.5 text-[var(--fs-label)] text-white/76 backdrop-blur-xl transition hover:bg-black/35 disabled:cursor-default" onClick={playVideo} disabled={videoPlaying || !appearance.authVideoUrl} aria-pressed={videoPlaying}>
|
||||
<button type="button" className="auth-scene-hero-control inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-[var(--fs-label)] backdrop-blur-xl transition disabled:cursor-default" onClick={playVideo} disabled={videoPlaying || !appearance.authVideoUrl} aria-pressed={videoPlaying}>
|
||||
<Play className="size-3 fill-current" />
|
||||
{videoPlaying ? "创作正在发生" : "播放品牌影片"}
|
||||
</button>
|
||||
@@ -96,9 +97,9 @@ export function AuthScene() {
|
||||
transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }}
|
||||
className="absolute inset-x-0 bottom-0 max-w-2xl p-5 sm:p-7 lg:p-10"
|
||||
>
|
||||
<p className="text-xs font-semibold tracking-[0.18em] text-white/58">{brandStudioLabel(appearance)}</p>
|
||||
<h1 className="mt-3 max-w-xl whitespace-pre-line text-3xl font-semibold leading-tight sm:text-4xl lg:text-5xl">{appearance.authHeroTitle}</h1>
|
||||
{appearance.authHeroDescription ? <p className="mt-4 max-w-xl whitespace-pre-line text-sm leading-6 text-white/62 sm:text-base sm:leading-7">{appearance.authHeroDescription}</p> : null}
|
||||
<p className="auth-scene-hero-muted text-xs font-semibold tracking-[0.18em]">{brandStudioLabel(appearance)}</p>
|
||||
<h1 className="auth-scene-hero-title mt-3 max-w-xl whitespace-pre-line text-3xl font-semibold leading-tight sm:text-4xl lg:text-5xl">{appearance.authHeroTitle}</h1>
|
||||
{appearance.authHeroDescription ? <p className="auth-scene-hero-muted mt-4 max-w-xl whitespace-pre-line text-sm leading-6 sm:text-base sm:leading-7">{appearance.authHeroDescription}</p> : null}
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
@@ -115,25 +116,23 @@ export function AuthScene() {
|
||||
transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }}
|
||||
className="my-auto w-full max-w-[460px]"
|
||||
>
|
||||
<ConfigProvider theme={getAntThemeConfig(true, appearance.activeSkin)}>
|
||||
<div className="auth-card-dark auth-scene-card h-auto overflow-hidden rounded-lg backdrop-blur-2xl">
|
||||
<section aria-label={copy.title} className={`flex flex-col ${recovery ? "min-h-[600px]" : activeTab === "login" ? "min-h-[500px]" : "min-h-[620px] sm:min-h-[640px]"}`}>
|
||||
<header className="px-6 pb-5 pt-6 sm:px-8 sm:pt-7">
|
||||
<p className="auth-scene-eyebrow text-xs font-semibold tracking-[0.18em]">{copy.eyebrow}</p>
|
||||
<h2 className="mt-2 text-3xl font-semibold">{copy.title}</h2>
|
||||
<p className="auth-scene-muted mt-2 text-sm leading-6">{copy.description}</p>
|
||||
</header>
|
||||
{!recovery ? (
|
||||
<div className="px-6 sm:px-8">
|
||||
<Tabs className="auth-card-tabs" activeKey={activeTab} items={AUTH_TABS} onChange={(key) => navigate({ pathname: key === "register" ? "/register" : "/login", search: location.search })} />
|
||||
</div>
|
||||
) : null}
|
||||
<div key={location.pathname} className="flex-1 px-6 py-6 sm:px-8 sm:py-7">
|
||||
<Outlet />
|
||||
<div className="auth-scene-card h-auto overflow-hidden rounded-lg backdrop-blur-2xl">
|
||||
<section aria-label={copy.title} className={`flex flex-col ${recovery ? "min-h-[600px]" : activeTab === "login" ? "" : "min-h-[620px] sm:min-h-[640px]"}`}>
|
||||
<header className="px-6 pb-5 pt-6 sm:px-8 sm:pt-7">
|
||||
<p className="auth-scene-eyebrow text-xs font-semibold tracking-[0.18em]">{copy.eyebrow}</p>
|
||||
<h2 className="mt-2 text-3xl font-semibold">{copy.title}</h2>
|
||||
<p className="auth-scene-muted mt-2 text-sm leading-6">{copy.description}</p>
|
||||
</header>
|
||||
{!recovery ? (
|
||||
<div className="px-6 sm:px-8">
|
||||
<Tabs className="auth-card-tabs" activeKey={activeTab} items={AUTH_TABS} onChange={(key) => navigate({ pathname: key === "register" ? "/register" : "/login", search: location.search })} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
) : null}
|
||||
<div key={location.pathname} className={`${!recovery && activeTab === "login" ? "" : "flex-1"} px-6 py-6 sm:px-8 sm:py-7`}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</motion.div>
|
||||
<SiteComplianceFooter variant="auth" className="absolute inset-x-0 bottom-0" />
|
||||
</section>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function ForgotPasswordPage() {
|
||||
<Input
|
||||
id="recovery-email"
|
||||
size="large"
|
||||
prefix={<Mail className="size-4 text-white/35" />}
|
||||
prefix={<Mail className="auth-scene-icon size-4" />}
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
placeholder="请输入绑定邮箱"
|
||||
@@ -120,7 +120,7 @@ export default function ForgotPasswordPage() {
|
||||
<form onSubmit={submitReset} className="space-y-4">
|
||||
<AuthField label="账号邮箱" htmlFor="recovery-email-confirm">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
|
||||
<Input id="recovery-email-confirm" size="large" prefix={<Mail className="size-4 text-white/35" />} value={email} readOnly autoComplete="email" />
|
||||
<Input id="recovery-email-confirm" size="large" prefix={<Mail className="auth-scene-icon size-4" />} value={email} readOnly autoComplete="email" />
|
||||
<Button htmlType="button" size="large" onClick={editEmail}>
|
||||
修改邮箱
|
||||
</Button>
|
||||
@@ -131,7 +131,7 @@ export default function ForgotPasswordPage() {
|
||||
<Input
|
||||
id="recovery-code"
|
||||
size="large"
|
||||
prefix={<ShieldCheck className="size-4 text-white/35" />}
|
||||
prefix={<ShieldCheck className="auth-scene-icon size-4" />}
|
||||
value={emailCode}
|
||||
onChange={(event) => setEmailCode(event.target.value.replace(/\D/g, "").slice(0, 6))}
|
||||
placeholder="6 位验证码"
|
||||
@@ -149,7 +149,7 @@ export default function ForgotPasswordPage() {
|
||||
<Input.Password
|
||||
id="recovery-password"
|
||||
size="large"
|
||||
prefix={<LockKeyhole className="size-4 text-white/35" />}
|
||||
prefix={<LockKeyhole className="auth-scene-icon size-4" />}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="至少 8 位"
|
||||
@@ -162,7 +162,7 @@ export default function ForgotPasswordPage() {
|
||||
<Input.Password
|
||||
id="recovery-confirm-password"
|
||||
size="large"
|
||||
prefix={<LockKeyhole className="size-4 text-white/35" />}
|
||||
prefix={<LockKeyhole className="auth-scene-icon size-4" />}
|
||||
value={confirmPassword}
|
||||
onChange={(event) => setConfirmPassword(event.target.value)}
|
||||
placeholder="再次输入密码"
|
||||
@@ -183,7 +183,7 @@ export default function ForgotPasswordPage() {
|
||||
function AuthField({ label, htmlFor, children }: { label: string; htmlFor: string; children: ReactNode }) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label htmlFor={htmlFor} className="block text-xs font-medium text-white/62">
|
||||
<label htmlFor={htmlFor} className="auth-scene-label block text-xs font-medium">
|
||||
{label}
|
||||
</label>
|
||||
{children}
|
||||
@@ -194,7 +194,7 @@ function AuthField({ label, htmlFor, children }: { label: string; htmlFor: strin
|
||||
function BackToLogin({ to }: { to: string }) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<Link to={to} className="inline-flex min-h-8 items-center gap-1.5 rounded-sm text-xs text-white/50 transition-colors hover:text-white/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-300/45">
|
||||
<Link to={to} className="auth-scene-link inline-flex min-h-8 items-center gap-1.5 rounded-sm text-xs transition-colors">
|
||||
<ArrowLeft className="size-3.5" />
|
||||
返回登录
|
||||
</Link>
|
||||
@@ -204,7 +204,7 @@ function BackToLogin({ to }: { to: string }) {
|
||||
|
||||
function Notice({ icon, children }: { icon: ReactNode; children: ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-start gap-2 rounded-lg border border-amber-300/15 bg-amber-300/[0.06] px-3 py-2.5 text-xs leading-5 text-amber-100/78">
|
||||
<div className="auth-scene-notice flex items-start gap-2 rounded-lg border px-3 py-2.5 text-xs leading-5">
|
||||
<span className="mt-0.5 shrink-0">{icon}</span>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function LoginPage() {
|
||||
{methods.length > 0 && <Segmented block aria-label="登录方式" value={method} disabled={submitting} options={[{ value: "password", label: "密码登录" }, ...methods.map((value) => ({ value, label: methodLabels[value] }))]} onChange={(value) => { setMethod(value as typeof method); setVerification({ ...emptyVerification }); }} />}
|
||||
{method !== "password" ? <VerificationFields key={method} purpose="login" method={method} value={verification} onChange={setVerification} disabled={submitting} /> : <>
|
||||
<AuthField label="用户名 / 邮箱" htmlFor="login-account">
|
||||
<Input id="login-account" size="large" prefix={<UserRound className="size-4 text-white/35" />} value={username} onChange={(event) => setUsername(event.target.value)} placeholder="用户名或邮箱" autoComplete="username" required />
|
||||
<Input id="login-account" size="large" prefix={<UserRound className="auth-scene-icon size-4" />} value={username} onChange={(event) => setUsername(event.target.value)} placeholder="用户名或邮箱" autoComplete="username" required />
|
||||
</AuthField>
|
||||
<AuthField
|
||||
label="密码"
|
||||
@@ -77,7 +77,7 @@ export default function LoginPage() {
|
||||
action={
|
||||
<Link
|
||||
to={forgotPasswordURL}
|
||||
className="-my-2 inline-flex min-h-8 items-center rounded-sm text-xs font-medium text-blue-300/80 transition-colors hover:text-blue-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-300/45"
|
||||
className="auth-scene-link -my-2 inline-flex min-h-8 items-center rounded-sm text-xs font-medium transition-colors"
|
||||
>
|
||||
忘记密码?
|
||||
</Link>
|
||||
@@ -86,7 +86,7 @@ export default function LoginPage() {
|
||||
<Input.Password
|
||||
id="login-password"
|
||||
size="large"
|
||||
prefix={<LockKeyhole className="size-4 text-white/35" />}
|
||||
prefix={<LockKeyhole className="auth-scene-icon size-4" />}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="请输入密码"
|
||||
@@ -100,7 +100,7 @@ export default function LoginPage() {
|
||||
</Button>
|
||||
{linuxdoEnabled ? (
|
||||
<>
|
||||
<Divider plain className="!border-white/10 !text-white/30">
|
||||
<Divider plain className="auth-scene-divider">
|
||||
或
|
||||
</Divider>
|
||||
<Button size="large" block icon={<LinuxDOIcon />} href={linuxDOLoginURL(next)}>
|
||||
@@ -116,7 +116,7 @@ function AuthField({ label, htmlFor, action, children }: { label: string; htmlFo
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<label htmlFor={htmlFor} className="text-xs font-medium text-white/62">
|
||||
<label htmlFor={htmlFor} className="auth-scene-label text-xs font-medium">
|
||||
{label}
|
||||
</label>
|
||||
{action}
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function RegisterPage() {
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<AuthField label="用户名">
|
||||
<Input size="large" prefix={<UserRound className="size-4 text-white/35" />} value={username} onChange={(event) => setUsername(event.target.value)} placeholder="3-32 位字符" autoComplete="username" required disabled={disabled} />
|
||||
<Input size="large" prefix={<UserRound className="auth-scene-icon size-4" />} value={username} onChange={(event) => setUsername(event.target.value)} placeholder="3-32 位字符" autoComplete="username" required disabled={disabled} />
|
||||
</AuthField>
|
||||
<AuthField label="显示名称">
|
||||
<Input size="large" value={displayName} onChange={(event) => setDisplayName(event.target.value)} placeholder="不填则使用用户名" disabled={disabled} />
|
||||
@@ -114,7 +114,7 @@ export default function RegisterPage() {
|
||||
{settings?.firstUser ? <AuthField label="邮箱(可选)">
|
||||
<Input
|
||||
size="large"
|
||||
prefix={<Mail className="size-4 text-white/35" />}
|
||||
prefix={<Mail className="auth-scene-icon size-4" />}
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
placeholder="用于登录与安全验证"
|
||||
@@ -131,7 +131,7 @@ export default function RegisterPage() {
|
||||
<AuthField label="密码">
|
||||
<Input.Password
|
||||
size="large"
|
||||
prefix={<LockKeyhole className="size-4 text-white/35" />}
|
||||
prefix={<LockKeyhole className="auth-scene-icon size-4" />}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
placeholder="至少 8 位"
|
||||
@@ -143,7 +143,7 @@ export default function RegisterPage() {
|
||||
<AuthField label="确认密码">
|
||||
<Input.Password
|
||||
size="large"
|
||||
prefix={<LockKeyhole className="size-4 text-white/35" />}
|
||||
prefix={<LockKeyhole className="auth-scene-icon size-4" />}
|
||||
value={confirmPassword}
|
||||
onChange={(event) => setConfirmPassword(event.target.value)}
|
||||
placeholder="再次输入密码"
|
||||
@@ -154,11 +154,11 @@ export default function RegisterPage() {
|
||||
</AuthField>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2 text-xs leading-5 text-white/55">
|
||||
<div className="auth-scene-muted flex items-start gap-2 text-xs leading-5">
|
||||
<Checkbox checked={agreementAccepted} onChange={(event) => setAgreementAccepted(event.target.checked)}>
|
||||
<span className="text-xs leading-5 text-white/55">我已阅读并同意</span>
|
||||
<span className="auth-scene-muted text-xs leading-5">我已阅读并同意</span>
|
||||
</Checkbox>
|
||||
<button type="button" className="-ml-1 text-xs leading-5 text-blue-300/85 transition hover:text-blue-200" onClick={() => setAgreementOpen(true)}>
|
||||
<button type="button" className="auth-scene-link -ml-1 text-xs leading-5 transition" onClick={() => setAgreementOpen(true)}>
|
||||
《影策服务协议》
|
||||
</button>
|
||||
</div>
|
||||
@@ -168,7 +168,7 @@ export default function RegisterPage() {
|
||||
</Button>
|
||||
{settings?.linuxdoEnabled && !settings.smsAndEmailRegistration ? (
|
||||
<>
|
||||
<Divider plain className="!border-white/10 !text-white/30">
|
||||
<Divider plain className="auth-scene-divider">
|
||||
或
|
||||
</Divider>
|
||||
<Button size="large" block disabled={!agreementAccepted} icon={<LinuxDOIcon />} href={agreementAccepted ? linuxDOLoginURL(next, true) : undefined}>
|
||||
@@ -194,7 +194,7 @@ export default function RegisterPage() {
|
||||
function AuthField({ label, children }: { label: string; children: ReactNode }) {
|
||||
return (
|
||||
<label className="block space-y-2">
|
||||
<span className="text-xs font-medium text-white/62">{label}</span>
|
||||
<span className="auth-scene-label text-xs font-medium">{label}</span>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
@@ -202,7 +202,7 @@ function AuthField({ label, children }: { label: string; children: ReactNode })
|
||||
|
||||
function Notice({ icon, tone, children }: { icon: ReactNode; tone: "blue" | "amber"; children: ReactNode }) {
|
||||
return (
|
||||
<div className={`flex items-start gap-2 rounded-lg border px-3 py-2.5 text-xs leading-5 ${tone === "blue" ? "border-blue-300/15 bg-blue-300/[0.06] text-blue-100/78" : "border-amber-300/15 bg-amber-300/[0.06] text-amber-100/78"}`}>
|
||||
<div data-tone={tone} className="auth-scene-notice flex items-start gap-2 rounded-lg border px-3 py-2.5 text-xs leading-5">
|
||||
<span className="mt-0.5 shrink-0">{icon}</span>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user