207 lines
8.0 KiB
TypeScript
207 lines
8.0 KiB
TypeScript
|
|
'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">懂物业,更懂“省”心</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>
|
|||
|
|
|
|||
|
|
<p className="mt-8 text-center text-xs text-slate-400">
|
|||
|
|
初始账号:admin / 密码:123456
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|