207 lines
8.0 KiB
TypeScript
Raw Normal View History

2026-06-22 14:43:46 +08:00
'use client';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { adminApi } from '@/lib/admin-services';
import { useAdminStore } from '@/store/adminStore';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Building2,
CheckCircle2,
User,
Lock,
Loader2,
} from 'lucide-react';
const schema = z.object({
username: z.string().min(1, '请输入账号'),
password: z.string().min(1, '请输入密码'),
});
type FormData = z.infer<typeof schema>;
export default function AdminLoginPage() {
const router = useRouter();
const search = useSearchParams();
const { setLogin, token } = useAdminStore();
const [serverError, setServerError] = useState<string | null>(null);
// 已登录则直接跳到后台
useEffect(() => {
if (token) {
const redirect = search.get('redirect');
router.replace(redirect || '/admin/dashboard');
}
}, [token, router, search]);
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { username: 'admin', password: '' },
});
const onSubmit = async (data: FormData) => {
setServerError(null);
try {
const res = await adminApi.login(data);
setLogin(res.token, res.admin);
const redirect = search.get('redirect');
router.replace(redirect || '/admin/dashboard');
} catch (e) {
setServerError((e as Error).message);
}
};
return (
<div className="grid min-h-screen lg:grid-cols-2">
{/* ===== 左侧:品牌展示区(深色,呼应首页 Hero===== */}
<div className="relative hidden overflow-hidden bg-[#0a0e27] lg:flex lg:flex-col lg:justify-between lg:p-12">
{/* 渐变底 + 辉光 + 几何图形 */}
<div className="pointer-events-none absolute inset-0">
<div className="absolute inset-0 bg-gradient-to-br from-[#0a0e27] via-[#0d1130] to-[#0a0e27]" />
<div className="absolute -left-20 top-0 h-72 w-72 rounded-full bg-brand-600/15 blur-[120px]" />
<div className="absolute bottom-0 right-0 h-80 w-80 rounded-full bg-brand-700/15 blur-[120px]" />
<div
className="hero-anim-spin absolute right-10 top-16 h-32 w-32 rounded-full border-2 border-dashed border-white/10"
style={{ animationDuration: '24s' }}
/>
<div className="hero-anim-float absolute bottom-20 left-[8%] h-12 w-12 rounded-lg border border-white/10 bg-white/[0.03]" />
<div className="hero-anim-pulse absolute left-[18%] top-[28%] h-2.5 w-2.5 rounded-full bg-brand-400/40" />
<div className="hero-anim-float-rev absolute right-[14%] bottom-[24%] rotate-45 h-8 w-8 rounded-sm border border-white/10 bg-white/[0.03]" />
<div className="hero-anim-float absolute right-[22%] top-[20%] text-2xl font-light text-white/10">+</div>
</div>
{/* Logo */}
<div className="relative z-10 flex items-center gap-2">
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-brand-500 to-brand-700 text-white shadow-lg shadow-brand-600/30 ring-1 ring-inset ring-white/10">
<Building2 className="h-5 w-5" />
</span>
<span className="text-lg font-bold text-white"></span>
</div>
{/* 标语 */}
<div className="relative z-10">
<h2 className="text-3xl font-bold leading-tight text-white xl:text-4xl">
<br />
<span className="bg-gradient-to-r from-white to-slate-400 bg-clip-text text-transparent">
</span>
</h2>
<p className="mt-4 text-lg text-slate-400">&ldquo;&rdquo;</p>
<div className="mt-10 space-y-3">
{[
'一站式物业管理 SaaS 平台',
'覆盖缴费、报修、公告、巡检全流程',
'三端协同,助力物业降本增效',
].map((t) => (
<div key={t} className="flex items-center gap-2.5 text-sm text-slate-300">
<CheckCircle2 className="h-4 w-4 shrink-0 text-brand-400" />
{t}
</div>
))}
</div>
</div>
{/* 版权 */}
<div className="relative z-10 text-xs text-slate-500">
© {new Date().getFullYear()} · SaaS
</div>
</div>
{/* ===== 右侧:表单区(干净浅色)===== */}
<div className="flex min-h-screen items-center justify-center bg-white px-6 py-12">
<div className="w-full max-w-sm">
{/* 移动端 Logo左侧面板在小屏隐藏*/}
<div className="mb-10 flex items-center gap-2 lg:hidden">
<span className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-slate-800 to-slate-900 text-white shadow-md shadow-slate-900/25 ring-1 ring-inset ring-white/10">
<Building2 className="h-5 w-5" />
</span>
<span className="text-lg font-bold text-slate-900"></span>
</div>
<div className="mb-8">
<span className="text-xs font-semibold uppercase tracking-widest text-brand-600">
Admin
</span>
<h1 className="mt-2 text-2xl font-bold text-slate-900"></h1>
<p className="mt-1.5 text-sm text-slate-500"></p>
</div>
{search.get('expired') && (
<div className="mb-5 rounded-lg border border-yellow-200 bg-yellow-50 px-3.5 py-2.5 text-sm text-yellow-700">
</div>
)}
<form
onSubmit={(e) => {
e.preventDefault();
void handleSubmit(onSubmit)(e);
}}
className="space-y-5"
>
<div className="space-y-1.5">
<Label htmlFor="username"></Label>
<div className="relative">
<User className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
id="username"
className="pl-9"
placeholder="请输入账号"
{...register('username')}
/>
</div>
{errors.username && (
<p className="text-xs text-red-600">{errors.username.message}</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="password"></Label>
<div className="relative">
<Lock className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input
id="password"
type="password"
className="pl-9"
placeholder="请输入密码"
{...register('password')}
/>
</div>
{errors.password && (
<p className="text-xs text-red-600">{errors.password.message}</p>
)}
</div>
{serverError && (
<p className="text-sm text-red-600">{serverError}</p>
)}
<Button
type="submit"
className="w-full bg-slate-900 text-white hover:bg-black"
disabled={isSubmitting}
>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</Button>
</form>
2026-06-22 18:05:27 +08:00
{/* <p className="mt-8 text-center text-xs text-slate-400">
2026-06-22 14:43:46 +08:00
admin / 123456
2026-06-22 18:05:27 +08:00
</p> */}
2026-06-22 14:43:46 +08:00
</div>
</div>
</div>
);
}