Files
Sub-Router/src/pages/Login.jsx
T
abingyydsandClaude Sonnet 4.6 01405ca8cf feat: dist-site 多主题模板 + 中英双语 i18n
- 新增 4 套主题模板:starter/default/dark/minimal
  - Starter: 毛玻璃 + GradientText + SpotlightCard (原有风格)
  - Default: Aurora 极光背景 + SplitText + TiltedCard
  - Dark: Particles 粒子 + DecryptedText 赛博朋克风格
  - Minimal: 极简优雅 + FadeContent 淡入动画
- 新增 i18n 中英双语支持 (i18next + browser-languagedetector)
- 新增 ThemeProvider 运行时主题切换 (根据 theme_template 字段)
- 新增 11 个 react-bits 动画组件
- 所有页面文案国际化 (Login/Register/Dashboard/Tokens/Pricing/Packages)
- 新增 README.md + README.zh.md 含主题切换指南

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:04:25 +09:00

108 lines
3.8 KiB
React

import React, { useState } from 'react';
import { Link, Navigate, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../context/AuthContext';
import { useSite } from '../context/SiteContext';
import toast from 'react-hot-toast';
export default function Login() {
const { t } = useTranslation();
const { login, user } = useAuth();
const { site } = useSite();
const navigate = useNavigate();
const location = useLocation();
const [form, setForm] = useState({ username: '', password: '' });
const [loading, setLoading] = useState(false);
// If already logged in, redirect via component (not navigate in render)
if (user) {
const from = location.state?.from?.pathname || '/dashboard';
return <Navigate to={from} replace />;
}
const handleSubmit = async (e) => {
e.preventDefault();
if (!form.username || !form.password) {
toast.error(t('login.fillAllFields'));
return;
}
setLoading(true);
try {
const result = await login(form.username, form.password);
if (result.success) {
toast.success(t('login.welcomeBackToast'));
const from = location.state?.from?.pathname || '/dashboard';
navigate(from, { replace: true });
return; // component may unmount — skip setLoading
}
// error toast is handled by api interceptor for success:false
} catch (err) {
// Network error handled by interceptor
}
setLoading(false);
};
return (
<div className="min-h-[70vh] flex items-center justify-center px-6">
<div className="w-full max-w-md">
<div className="glass rounded-2xl p-8">
<div className="text-center mb-8">
<h1 className="text-2xl font-heading font-bold text-white mb-2">{t('login.welcomeBack')}</h1>
<p className="text-sm text-neutral-400">
{site?.name ? t('login.signInTo', { name: site.name }) : t('login.signInToDefault')}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-neutral-300 mb-1.5">{t('login.username')}</label>
<input
type="text"
value={form.username}
onChange={(e) => setForm({ ...form, username: e.target.value })}
className="input"
placeholder={t('login.enterUsername')}
autoComplete="username"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-neutral-300 mb-1.5">{t('login.password')}</label>
<input
type="password"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
className="input"
placeholder={t('login.enterPassword')}
autoComplete="current-password"
required
/>
</div>
<button
type="submit"
disabled={loading}
className="btn-primary w-full flex items-center justify-center gap-2"
>
{loading && (
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
)}
{loading ? t('login.signingIn') : t('login.signInBtn')}
</button>
</form>
<div className="mt-6 text-center">
<p className="text-sm text-neutral-400">
{t('login.noAccount')}{' '}
<Link to="/register" className="text-brand-400 hover:text-brand-300 transition-colors font-medium">
{t('login.createOne')}
</Link>
</p>
</div>
</div>
</div>
</div>
);
}