✨ feat: impl accounts and channels
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useActionState } from "react";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
CheckCircle2Icon,
|
||||
KeyRoundIcon,
|
||||
Loader2Icon,
|
||||
LockIcon,
|
||||
MailIcon,
|
||||
SparklesIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { registerAction, type RegisterState } from "@/lib/actions/auth";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { FluxentLogo } from "@/components/fluxent-logo";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
const initialState: RegisterState = {};
|
||||
|
||||
export function RegisterForm() {
|
||||
const router = useRouter();
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
registerAction,
|
||||
initialState
|
||||
);
|
||||
|
||||
const [password, setPassword] = React.useState("");
|
||||
const [confirmPassword, setConfirmPassword] = React.useState("");
|
||||
|
||||
// 当注册成功时,优雅倒计时或自动跳转
|
||||
React.useEffect(() => {
|
||||
if (state?.success) {
|
||||
const timer = setTimeout(() => {
|
||||
router.push("/login?registered=1");
|
||||
}, 1500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [state?.success, router]);
|
||||
|
||||
// 密码复杂度提示计算
|
||||
const hasMinLen = password.length >= 8;
|
||||
const passwordsMatch = Boolean(
|
||||
password && confirmPassword && password === confirmPassword
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-svh flex-col justify-between overflow-hidden bg-background">
|
||||
{/* Decorative ambient gradients */}
|
||||
<div className="pointer-events-none absolute -top-40 -left-40 size-[32rem] rounded-full bg-primary/5 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -bottom-40 -right-40 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))]" />
|
||||
|
||||
{/* 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 Card */}
|
||||
<main className="relative z-10 flex flex-1 items-center justify-center px-4 py-8 sm:px-6">
|
||||
<div className="w-full max-w-md">
|
||||
<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/10 text-primary">
|
||||
<SparklesIcon className="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
<CardDescription className="text-xs">
|
||||
创建主账户,开启多币种资产追踪与专业级记账
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{/* 注册成功反馈 */}
|
||||
{state?.success ? (
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-6 text-center">
|
||||
<div className="flex size-12 items-center justify-center rounded-full bg-primary/10 text-primary">
|
||||
<CheckCircle2Icon className="size-6" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold text-foreground">
|
||||
账户已成功创建!
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
系统正在为您准备控制台,即将自动跳转至登录页...
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2 text-xs"
|
||||
onClick={() => router.push("/login?registered=1")}
|
||||
>
|
||||
立即前往登录
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 全局错误提示 */}
|
||||
{state?.error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription className="text-xs">
|
||||
{state.error}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form action={formAction} className="flex flex-col gap-3.5">
|
||||
{/* 姓名 / 昵称 */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="name" className="text-xs font-medium">
|
||||
姓名 / 昵称
|
||||
</Label>
|
||||
<div className="relative flex items-center">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
autoComplete="name"
|
||||
placeholder="例如:Alex Chen"
|
||||
required
|
||||
disabled={isPending}
|
||||
className="pl-8"
|
||||
/>
|
||||
<UserIcon className="pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
{state?.fieldErrors?.name && (
|
||||
<p className="text-[11px] text-destructive">
|
||||
{state.fieldErrors.name[0]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 邮箱 */}
|
||||
<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="alex@example.com"
|
||||
required
|
||||
disabled={isPending}
|
||||
className="pl-8"
|
||||
/>
|
||||
<MailIcon className="pointer-events-none absolute left-2.5 size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
{state?.fieldErrors?.email && (
|
||||
<p className="text-[11px] text-destructive">
|
||||
{state.fieldErrors.email[0]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 密码 */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="password" className="text-xs font-medium">
|
||||
登录密码
|
||||
</Label>
|
||||
<div className="relative flex items-center">
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder="至少 8 个字符"
|
||||
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>
|
||||
{state?.fieldErrors?.password && (
|
||||
<p className="text-[11px] text-destructive">
|
||||
{state.fieldErrors.password[0]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 确认密码 */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label
|
||||
htmlFor="confirmPassword"
|
||||
className="text-xs font-medium"
|
||||
>
|
||||
再次确认密码
|
||||
</Label>
|
||||
<div className="relative flex items-center">
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder="重复输入上方设置的密码"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(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>
|
||||
{state?.fieldErrors?.confirmPassword && (
|
||||
<p className="text-[11px] text-destructive">
|
||||
{state.fieldErrors.confirmPassword[0]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 密码强度/匹配指引 */}
|
||||
<div className="flex flex-col gap-1 rounded-lg border border-border/40 bg-muted/30 p-2.5 text-[11px] text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={`size-1.5 rounded-full ${
|
||||
hasMinLen ? "bg-primary" : "bg-muted-foreground/40"
|
||||
}`}
|
||||
/>
|
||||
<span>密码长度不少于 8 位</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={`size-1.5 rounded-full ${
|
||||
passwordsMatch
|
||||
? "bg-primary"
|
||||
: "bg-muted-foreground/40"
|
||||
}`}
|
||||
/>
|
||||
<span>两次密码输入保持一致</span>
|
||||
</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="/login"
|
||||
className="font-medium text-foreground underline underline-offset-4 hover:text-primary"
|
||||
>
|
||||
<ArrowLeftIcon
|
||||
data-icon="inline-start"
|
||||
className="inline size-3 mr-0.5"
|
||||
/>
|
||||
返回直接登录
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user