Files
TurboHR/frontend/src/pages/auth/Login.tsx
T

231 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { Eye, EyeOff, Shield, Zap, Users, FileText, ArrowRight } from 'lucide-react'
import Logo from '../../components/ui/Logo'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { useAuthStore } from '../../store/authStore'
import { authApi } from '../../lib/api-services'
import { Input, Label } from '../../components/ui/Input'
import Button from '../../components/ui/Button'
const schema = z.object({
phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'),
password: z.string().min(1, '请输入密码'),
})
type FormData = z.infer<typeof schema>
export default function Login() {
const navigate = useNavigate()
const { setAuth } = useAuthStore()
const [showPassword, setShowPassword] = useState(false)
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const [remember, setRemember] = useState(false)
const { register, handleSubmit, setValue, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
phone: '13800000001',
password: '12345678',
},
})
// 读取记住的账号密码
useEffect(() => {
try {
const saved = localStorage.getItem('admin-login-remember')
if (saved) {
const { phone, password } = JSON.parse(saved)
if (phone) setValue('phone', phone)
if (password) setValue('password', password)
setRemember(true)
}
} catch {
// 忽略解析错误
}
}, [setValue])
const onSubmit = async (data: FormData) => {
setError('')
setLoading(true)
try {
const res = await authApi.login(data) as any
if (remember) {
localStorage.setItem('admin-login-remember', JSON.stringify({ phone: data.phone, password: data.password }))
} else {
localStorage.removeItem('admin-login-remember')
}
setAuth(res.user, res.accessToken, res.refreshToken)
navigate('/')
} catch (err: any) {
setError(err.response?.data?.error?.message || '登录失败,请稍后重试')
} finally {
setLoading(false)
}
}
const features = [
{ icon: Users, title: '智能花名册', desc: '员工档案全生命周期管理' },
{ icon: FileText, title: '合同自动化', desc: '电子签约 + 到期预警' },
{ icon: Shield, title: '合规风控', desc: 'AI 驱动的用工风险检测' },
{ icon: Zap, title: '薪税一键算', desc: '工资社保个税自动计算' },
]
return (
<div className="min-h-screen flex bg-surface-page">
{/* 左侧品牌展示区 */}
<div className="hidden lg:flex lg:w-[480px] xl:w-[540px] relative overflow-hidden bg-gradient-to-br from-brand-700 via-brand-600 to-brand-800">
{/* 装饰性几何图形 */}
<div className="absolute inset-0 opacity-10">
<div className="absolute top-20 left-20 w-72 h-72 rounded-full border-[3px] border-white" />
<div className="absolute bottom-32 right-10 w-48 h-48 rounded-full border-[2px] border-white" />
<div className="absolute top-1/2 left-1/3 w-96 h-96 rounded-full border-[1px] border-white" />
</div>
<div className="absolute top-0 right-0 w-40 h-40 bg-white/5 rounded-bl-[80px]" />
<div className="absolute bottom-0 left-0 w-32 h-32 bg-white/5 rounded-tr-[64px]" />
<div className="relative z-10 flex flex-col justify-between p-12 xl:p-16 text-white w-full">
{/* Logo + 品牌名 */}
<div>
<Logo className="w-20 h-20 object-contain mb-3" />
<div className="text-xl font-bold tracking-tight"></div>
<div className="text-xs text-white/60 mt-0.5"></div>
</div>
{/* 标语 */}
<div>
<h2 className="text-3xl xl:text-4xl font-bold leading-tight tracking-tight">
<br />
</h2>
<p className="mt-4 text-sm text-white/70 leading-relaxed max-w-sm">
AI
</p>
</div>
{/* 功能亮点 */}
<div className="space-y-3">
{features.map((f, i) => (
<div key={i} className="flex items-center gap-3 group">
<div className="w-9 h-9 rounded-lg bg-white/10 backdrop-blur flex items-center justify-center flex-shrink-0 group-hover:bg-white/20 transition-colors">
<f.icon className="w-4.5 h-4.5 text-white" strokeWidth={1.8} />
</div>
<div>
<div className="text-sm font-semibold text-white/95">{f.title}</div>
<div className="text-xs text-white/55 mt-0.5">{f.desc}</div>
</div>
</div>
))}
</div>
{/* 底部版权 */}
<div className="text-xs text-white/40">
© 2026
</div>
</div>
</div>
{/* 右侧登录表单区 */}
<div className="flex-1 flex items-center justify-center px-4 py-12">
<div className="w-full max-w-sm">
{/* 移动端 Logo */}
<div className="flex lg:hidden flex-col items-center mb-8">
<Logo className="w-16 h-16 object-contain mb-2" />
<span className="text-xl font-bold"></span>
</div>
{/* 欢迎语 */}
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="text-sm text-gray-500 mt-1.5"></p>
</div>
{error && (
<div className="mb-5 px-4 py-3 rounded-lg bg-red-50 border border-red-100 text-red-700 text-sm flex items-start gap-2">
<span className="flex-shrink-0 mt-0.5"></span>
<span>{error}</span>
</div>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
<div>
<Label></Label>
<Input
type="tel"
placeholder="请输入手机号"
className="h-11"
{...register('phone')}
maxLength={11}
/>
{errors.phone && <p className="text-xs text-red-500 mt-1">{errors.phone.message}</p>}
</div>
<div>
<Label></Label>
<div className="relative">
<Input
type={showPassword ? 'text' : 'password'}
placeholder="请输入密码"
className="h-11 pr-10"
{...register('password')}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
>
{showPassword ? <EyeOff className="w-4.5 h-4.5" /> : <Eye className="w-4.5 h-4.5" />}
</button>
</div>
{errors.password && <p className="text-xs text-red-500 mt-1">{errors.password.message}</p>}
</div>
<div className="flex items-center justify-between">
<label className="flex items-center gap-2 cursor-pointer select-none">
<input
type="checkbox"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary focus:ring-offset-0"
/>
<span className="text-sm text-gray-500"></span>
</label>
<Link to="/forgot-password" className="text-sm text-primary hover:text-primary-dark transition-colors"></Link>
</div>
<Button type="submit" size="lg" className="w-full h-11" disabled={loading}>
{loading ? (
<span className="flex items-center gap-2">
<span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
...
</span>
) : (
<span className="flex items-center gap-2">
<ArrowRight className="w-4 h-4" />
</span>
)}
</Button>
</form>
{/* 分割线 */}
<div className="mt-8 mb-6 flex items-center gap-4">
<div className="flex-1 h-px bg-gray-200" />
<span className="text-xs text-gray-400"></span>
<div className="flex-1 h-px bg-gray-200" />
</div>
<Link
to="/register"
className="block w-full h-11 leading-[44px] text-center rounded-md border border-gray-300 text-sm font-medium text-gray-700 hover:bg-gray-50 hover:border-gray-400 transition-colors"
>
</Link>
</div>
</div>
</div>
)
}