PersonalWebApplication/Client/src/components/LoginForm.tsx
Ryan c61b8de776 重构登录后博客页:Lunada 渐变背景 + 两侧图片液态玻璃;登录报错改英文
- 登录页凭据/网络/HTTP 错误提示改为英文
- 博客页改为:通栏顶部彩虹栏 + 通栏 Hero(彩虹光标动效)+ 图片·内容·图片三栏
- 两侧图片用 import.meta.glob 自动索引 src/assets/sides/,按高度自适应、居中,叠加液态玻璃覆层
- 背景改为 Lunada 渐变极光
- 性能优化:极光改静止、中间纸/顶栏去掉 backdrop-filter,降低多层毛玻璃每帧重算开销

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:57:00 +08:00

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>
)
}