397 lines
16 KiB
TypeScript
397 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import Link from "next/link";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { signIn } from "next-auth/react";
|
|
import {
|
|
ArrowRightIcon,
|
|
CheckCircle2Icon,
|
|
CoinsIcon,
|
|
Globe2Icon,
|
|
KeyRoundIcon,
|
|
Loader2Icon,
|
|
LockIcon,
|
|
MailIcon,
|
|
PieChartIcon,
|
|
TrendingUpIcon,
|
|
WalletIcon,
|
|
} from "lucide-react";
|
|
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { FluxentLogo } from "@/components/fluxent-logo";
|
|
import { ThemeToggle } from "@/components/theme-toggle";
|
|
|
|
interface LoginFormProps {
|
|
oidcEnabled: boolean;
|
|
oidcName: string;
|
|
}
|
|
|
|
export function LoginForm({ oidcEnabled, oidcName }: LoginFormProps) {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const callbackUrl = searchParams.get("callbackUrl") || "/";
|
|
const authError = searchParams.get("error");
|
|
const registered = searchParams.get("registered");
|
|
|
|
const [email, setEmail] = React.useState("");
|
|
const [password, setPassword] = React.useState("");
|
|
const [rememberMe, setRememberMe] = React.useState(false);
|
|
const [isPending, setIsPending] = React.useState(false);
|
|
const [isOidcPending, setIsOidcPending] = React.useState(false);
|
|
const [errorMessage, setErrorMessage] = React.useState<string | null>(() => {
|
|
if (authError === "CredentialsSignin") {
|
|
return "邮箱或密码错误,请核对后重试";
|
|
}
|
|
if (authError) {
|
|
return "认证过程中遇到问题,请重新登录";
|
|
}
|
|
return null;
|
|
});
|
|
|
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
setErrorMessage(null);
|
|
|
|
if (!email.trim()) {
|
|
setErrorMessage("请输入登录邮箱");
|
|
return;
|
|
}
|
|
if (!password) {
|
|
setErrorMessage("请输入登录密码");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsPending(true);
|
|
const res = await signIn("credentials", {
|
|
email: email.trim(),
|
|
password,
|
|
redirect: false,
|
|
callbackUrl,
|
|
});
|
|
|
|
if (res?.error) {
|
|
if (res.error === "CredentialsSignin" || res.code === "credentials") {
|
|
setErrorMessage("账号或密码不正确,请重新检查");
|
|
} else {
|
|
setErrorMessage("登录失败,请稍后重试");
|
|
}
|
|
return;
|
|
}
|
|
|
|
router.push(callbackUrl);
|
|
router.refresh();
|
|
} catch {
|
|
setErrorMessage("网络异常,无法连接到认证服务器");
|
|
} finally {
|
|
setIsPending(false);
|
|
}
|
|
};
|
|
|
|
const handleOidcLogin = async () => {
|
|
try {
|
|
setIsOidcPending(true);
|
|
await signIn("oidc", { callbackUrl });
|
|
} catch {
|
|
setIsOidcPending(false);
|
|
setErrorMessage("SSO 单点登录发起失败,请稍后重试");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="relative flex min-h-svh flex-col justify-between overflow-hidden bg-background">
|
|
{/* Decorative ambient background meshes */}
|
|
<div className="pointer-events-none absolute -top-40 -right-40 size-[32rem] rounded-full bg-primary/5 blur-3xl" />
|
|
<div className="pointer-events-none absolute top-1/2 -left-48 size-[34rem] rounded-full bg-primary/5 blur-3xl" />
|
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,rgba(120,119,198,0.08),rgba(255,255,255,0))]" />
|
|
|
|
{/* Top navigation / branding header */}
|
|
<header className="relative z-10 flex h-16 w-full items-center justify-between px-6 md:px-12">
|
|
<Link
|
|
href="/"
|
|
className="group flex items-center gap-2.5 outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
>
|
|
<FluxentLogo size={32} />
|
|
<div className="flex flex-col">
|
|
<span className="font-heading text-base font-semibold tracking-tight">
|
|
Fluxent
|
|
</span>
|
|
<span className="text-[10px] tracking-wider text-muted-foreground uppercase">
|
|
Financial OS
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
<div className="flex items-center gap-2">
|
|
<ThemeToggle />
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main content grid */}
|
|
<main className="relative z-10 flex flex-1 items-center justify-center px-4 py-8 sm:px-6">
|
|
<div className="grid w-full max-w-4xl gap-8 lg:grid-cols-[1.1fr_1fr] lg:items-center">
|
|
{/* Left Hero feature showcase (visible on large screen) */}
|
|
<div className="hidden flex-col gap-8 pr-4 lg:flex">
|
|
<div className="flex flex-col gap-3">
|
|
<div className="inline-flex w-fit items-center gap-2 rounded-full border border-border/80 bg-muted/60 px-3 py-1 text-xs text-muted-foreground backdrop-blur-xs">
|
|
<span className="inline-block size-1.5 rounded-full bg-primary animate-pulse" />
|
|
全球多币种 • 全场景记账 • 实时汇率
|
|
</div>
|
|
<h1 className="font-heading text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
|
掌控每一笔资产流动,
|
|
<br />
|
|
让财务管理行云流水。
|
|
</h1>
|
|
<p className="text-muted-foreground text-sm leading-relaxed">
|
|
无论是跨国多币种账户、信用卡记账,还是银行与电子钱包资金调度,Fluxent
|
|
提供银行级的精确记录与现代化的优雅交互体验。
|
|
</p>
|
|
</div>
|
|
|
|
{/* Visual value propositions */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="flex flex-col gap-1.5 rounded-xl border border-border/60 bg-card/50 p-3.5 backdrop-blur-xs transition-colors hover:border-border">
|
|
<div className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
<CoinsIcon className="size-4" />
|
|
</div>
|
|
<h3 className="text-xs font-semibold text-foreground">
|
|
全币种与汇率追踪
|
|
</h3>
|
|
<p className="text-[11px] text-muted-foreground leading-normal">
|
|
多币种入账与清算汇率对账,资产估值一目了然
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5 rounded-xl border border-border/60 bg-card/50 p-3.5 backdrop-blur-xs transition-colors hover:border-border">
|
|
<div className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
<WalletIcon className="size-4" />
|
|
</div>
|
|
<h3 className="text-xs font-semibold text-foreground">
|
|
全渠道多账户管理
|
|
</h3>
|
|
<p className="text-[11px] text-muted-foreground leading-normal">
|
|
银行账户、电子钱包与支付卡渠道统一调度
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5 rounded-xl border border-border/60 bg-card/50 p-3.5 backdrop-blur-xs transition-colors hover:border-border">
|
|
<div className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
<TrendingUpIcon className="size-4" />
|
|
</div>
|
|
<h3 className="text-xs font-semibold text-foreground">
|
|
复式记账与场景分类
|
|
</h3>
|
|
<p className="text-[11px] text-muted-foreground leading-normal">
|
|
标准借贷分录与消费场景画像,财务合规严谨
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5 rounded-xl border border-border/60 bg-card/50 p-3.5 backdrop-blur-xs transition-colors hover:border-border">
|
|
<div className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
<PieChartIcon className="size-4" />
|
|
</div>
|
|
<h3 className="text-xs font-semibold text-foreground">
|
|
资产汇总与统计
|
|
</h3>
|
|
<p className="text-[11px] text-muted-foreground leading-normal">
|
|
跨账户资金结构分布分析,收支报表一览无余
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Login Card */}
|
|
<div className="w-full">
|
|
<Card className="border-border/80 shadow-lg shadow-black/5 dark:shadow-black/25">
|
|
<CardHeader className="gap-1.5 pb-4">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-xl font-bold tracking-tight">
|
|
欢迎回到 Fluxent
|
|
</CardTitle>
|
|
<div className="flex size-7 items-center justify-center rounded-lg bg-primary/5 text-primary lg:hidden">
|
|
<FluxentLogo size={24} />
|
|
</div>
|
|
</div>
|
|
<CardDescription className="text-xs">
|
|
输入您的注册邮箱与密码以继续使用
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent className="flex flex-col gap-4">
|
|
{registered === "1" && (
|
|
<Alert className="border-border bg-muted/40">
|
|
<CheckCircle2Icon className="text-foreground" />
|
|
<AlertTitle>账号注册成功</AlertTitle>
|
|
<AlertDescription className="text-xs">
|
|
您的 Fluxent 账户已就绪,请使用新密码进行登录。
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{errorMessage && (
|
|
<Alert variant="destructive">
|
|
<AlertDescription className="text-xs">
|
|
{errorMessage}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* OIDC Single Sign On Button (if enabled) */}
|
|
{oidcEnabled && (
|
|
<div className="flex flex-col gap-3">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="lg"
|
|
onClick={handleOidcLogin}
|
|
disabled={isOidcPending || isPending}
|
|
className="w-full justify-center text-xs font-medium"
|
|
>
|
|
{isOidcPending ? (
|
|
<Loader2Icon
|
|
data-icon="inline-start"
|
|
className="animate-spin"
|
|
/>
|
|
) : (
|
|
<Globe2Icon data-icon="inline-start" />
|
|
)}
|
|
通过 {oidcName} 快速登录
|
|
</Button>
|
|
|
|
<div className="relative flex items-center justify-center">
|
|
<Separator className="w-full" />
|
|
<span className="absolute bg-card px-2 text-[11px] uppercase tracking-wider text-muted-foreground">
|
|
或者使用邮箱密码
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Main Email & Password Form */}
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-3.5">
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="email" className="text-xs font-medium">
|
|
邮箱地址
|
|
</Label>
|
|
<div className="relative flex items-center">
|
|
<Input
|
|
id="email"
|
|
name="email"
|
|
type="email"
|
|
autoComplete="email"
|
|
placeholder="name@company.com"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
required
|
|
disabled={isPending}
|
|
className="pl-8"
|
|
/>
|
|
<MailIcon className="pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="password" className="text-xs font-medium">
|
|
密码
|
|
</Label>
|
|
</div>
|
|
<div className="relative flex items-center">
|
|
<Input
|
|
id="password"
|
|
name="password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
placeholder="••••••••"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
disabled={isPending}
|
|
className="pl-8"
|
|
/>
|
|
<LockIcon className="pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-0.5">
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="rememberMe"
|
|
checked={rememberMe}
|
|
onCheckedChange={(checked) =>
|
|
setRememberMe(Boolean(checked))
|
|
}
|
|
/>
|
|
<label
|
|
htmlFor="rememberMe"
|
|
className="text-xs text-muted-foreground cursor-pointer select-none"
|
|
>
|
|
保持此设备的登录状态
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
size="lg"
|
|
disabled={isPending}
|
|
className="mt-1 w-full justify-center text-xs font-medium"
|
|
>
|
|
{isPending ? (
|
|
<Loader2Icon
|
|
data-icon="inline-start"
|
|
className="animate-spin"
|
|
/>
|
|
) : (
|
|
<KeyRoundIcon data-icon="inline-start" />
|
|
)}
|
|
{isPending ? "正在登录..." : "登录进入系统"}
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
|
|
<CardFooter className="justify-center border-t border-border/50 py-3 text-xs text-muted-foreground">
|
|
<p>
|
|
还没有 Fluxent 账号?{" "}
|
|
<Link
|
|
href="/register"
|
|
className="font-medium text-foreground underline underline-offset-4 hover:text-primary"
|
|
>
|
|
立即注册新账号
|
|
<ArrowRightIcon
|
|
data-icon="inline-end"
|
|
className="inline size-3 ml-0.5"
|
|
/>
|
|
</Link>
|
|
</p>
|
|
</CardFooter>
|
|
</Card>
|
|
|
|
<p className="mt-4 text-center text-[11px] text-muted-foreground">
|
|
登录即代表您同意 Fluxent 的服务条款与隐私政策
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<footer className="relative z-10 flex h-12 items-center justify-center border-t border-border/40 px-6 text-center text-[11px] text-muted-foreground">
|
|
<span>© {new Date().getFullYear()} Fluxent Financial. 保留所有权利。</span>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|