- 登录页凭据/网络/HTTP 错误提示改为英文 - 博客页改为:通栏顶部彩虹栏 + 通栏 Hero(彩虹光标动效)+ 图片·内容·图片三栏 - 两侧图片用 import.meta.glob 自动索引 src/assets/sides/,按高度自适应、居中,叠加液态玻璃覆层 - 背景改为 Lunada 渐变极光 - 性能优化:极光改静止、中间纸/顶栏去掉 backdrop-filter,降低多层毛玻璃每帧重算开销 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
107 lines
3.3 KiB
TypeScript
107 lines
3.3 KiB
TypeScript
import { useState } from 'react'
|
|
import type { FormEvent } from 'react'
|
|
import { motion } from 'framer-motion'
|
|
import { fadeUp, stagger } from './motion'
|
|
import { TextField } from './ui/TextField'
|
|
import { Button } from './ui/Button'
|
|
|
|
interface LoginFormProps {
|
|
/** 校验凭据;失败时抛错 */
|
|
onSignIn: (username: string, password: string, remember: boolean) => Promise<void>
|
|
}
|
|
|
|
// 右侧登录表单:接 Rust 后端鉴权。无注册入口(用户由后端写死)。
|
|
export function LoginForm({ onSignIn }: LoginFormProps) {
|
|
const [username, setUsername] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const [remember, setRemember] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
|
|
const handleSubmit = async (e: FormEvent) => {
|
|
e.preventDefault()
|
|
if (submitting) return
|
|
setError(null)
|
|
setSubmitting(true)
|
|
try {
|
|
await onSignIn(username.trim(), password, remember)
|
|
// 成功后由上层切换到博客页,本组件随之卸载,无需额外处理。
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Sign-in failed')
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<motion.div
|
|
variants={stagger(0.5, 0.1)}
|
|
initial="hidden"
|
|
animate="show"
|
|
className="flex w-full max-w-md flex-col justify-center px-8 py-14 sm:px-12 lg:px-16"
|
|
>
|
|
<motion.div variants={fadeUp} className="mb-10">
|
|
<h2 className="font-display text-3xl font-medium text-text">Sign in</h2>
|
|
<p className="mt-2 text-sm text-muted">Enter your details to continue.</p>
|
|
</motion.div>
|
|
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-7">
|
|
<motion.div variants={fadeUp}>
|
|
<TextField
|
|
label="Username"
|
|
type="text"
|
|
name="username"
|
|
autoComplete="username"
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
required
|
|
/>
|
|
</motion.div>
|
|
|
|
<motion.div variants={fadeUp}>
|
|
<TextField
|
|
label="Password"
|
|
type="password"
|
|
name="password"
|
|
autoComplete="current-password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
/>
|
|
</motion.div>
|
|
|
|
{error && (
|
|
<motion.p
|
|
initial={{ opacity: 0, y: -4 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="text-sm text-[#e07a5f]"
|
|
role="alert"
|
|
>
|
|
{error}
|
|
</motion.p>
|
|
)}
|
|
|
|
<motion.div
|
|
variants={fadeUp}
|
|
className="flex items-center justify-between text-sm"
|
|
>
|
|
<label className="flex cursor-pointer items-center gap-2 text-muted">
|
|
<input
|
|
type="checkbox"
|
|
checked={remember}
|
|
onChange={(e) => setRemember(e.target.checked)}
|
|
className="h-4 w-4 accent-[var(--color-accent)]"
|
|
/>
|
|
Remember me
|
|
</label>
|
|
</motion.div>
|
|
|
|
<motion.div variants={fadeUp} className="mt-2">
|
|
<Button type="submit" disabled={submitting}>
|
|
{submitting ? 'Signing in…' : 'Sign in'}
|
|
</Button>
|
|
</motion.div>
|
|
</form>
|
|
</motion.div>
|
|
)
|
|
}
|