231 lines
9.3 KiB
TypeScript
231 lines
9.3 KiB
TypeScript
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>
|
||
)
|
||
}
|