调整前端目录结构

This commit is contained in:
2025-05-10 12:08:04 +08:00
parent 2f86362f4b
commit d281a6c804
41 changed files with 2909 additions and 355 deletions

View File

@@ -0,0 +1,67 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
import { useState, useCallback } from "react";
import { toast } from "sonner";
import LoginHeader from "./LoginHeader";
import { SendCodeFormData } from "./types";
import { Label } from "@/components/ui/label";
export default function EmailLoginMode({ onSendCode }: { onSendCode: (data: SendCodeFormData) => Promise<boolean> }) {
const [email, setEmail] = useState("");
const handleSendCode = useCallback(() => {
if (!email.trim().match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
toast.error('请输入正确的邮箱地址');
return;
}
onSendCode({
type: 'email',
email,
})
}, [email, onSendCode]);
return (
<>
<LoginHeader />
<div className="grid gap-3">
<Label htmlFor="email"></Label>
<Input
id="email-login-mode-email"
name="email"
type="text"
placeholder="电子邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
required />
</div>
<div className="grid gap-3">
<div className="flex items-center h-4">
<Label htmlFor="code"></Label>
</div>
<div className="flex gap-5">
<InputOTP
id="email-login-mode-code"
name="code"
maxLength={6}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
required
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<Button type="button" variant="secondary" onClick={handleSendCode}></Button>
</div>
</div>
<Button type="submit" className="w-full">
</Button>
</>
)
}

View File

@@ -0,0 +1,12 @@
export default function LoginHeader() {
return (
<>
<div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold"></h1>
<p className="text-muted-foreground text-balance">
</p>
</div>
</>
)
}

View File

@@ -0,0 +1,41 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import LoginHeader from "./LoginHeader";
import { Label } from "@/components/ui/label"
export default function PasswordLoginMode({ forgetPassword }: { forgetPassword: () => void }) {
return (
<>
<LoginHeader />
<div className="grid gap-3">
<Label htmlFor="email">//</Label>
<Input
id="password-login-mode-account"
name="account"
type="text"
placeholder="电子邮箱/手机号/账号"
required
/>
</div>
<div className="grid gap-3">
<div className="flex items-center h-4">
<Label htmlFor="password"></Label>
<a
onClick={forgetPassword}
className="ml-auto text-sm underline-offset-2 hover:underline cursor-pointer"
>
</a>
</div>
<Input
id="password-login-mode-password"
name="password"
type="password"
required />
</div>
<Button type="submit" className="w-full">
</Button>
</>
)
}

View File

@@ -0,0 +1,67 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
import { useState, useCallback } from "react";
import { toast } from "sonner";
import LoginHeader from "./LoginHeader";
import { SendCodeFormData } from "./types";
import { Label } from "@/components/ui/label"
export default function PhoneLoginMode({ onSendCode }: { onSendCode: (data: SendCodeFormData) => Promise<boolean> }) {
const [phone, setPhone] = useState("");
const handleSendCode = useCallback(() => {
if (phone.trim().length !== 11) {
toast.error('请输入正确的手机号');
return;
}
onSendCode({
type: 'phone',
phone,
})
}, [phone, onSendCode]);
return (
<>
<LoginHeader />
<div className="grid gap-3">
<Label htmlFor="phone"></Label>
<Input
id="phone-login-mode-phone"
name="phone"
type="text"
placeholder="+86 手机号"
value={phone}
onChange={(e) => setPhone(e.target.value)}
required />
</div>
<div className="grid gap-3">
<div className="flex items-center h-4">
<Label htmlFor="code"></Label>
</div>
<div className="flex gap-5">
<InputOTP
id="phone-login-mode-code"
name="code"
maxLength={6}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
required
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<Button type="button" variant="secondary" onClick={handleSendCode}></Button>
</div>
</div>
<Button type="submit" className="w-full">
</Button>
</>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -0,0 +1,16 @@
export type SubmitMode = 'password' | 'phone' | 'email';
export type LoginFormData = {
type: SubmitMode;
account?: string;
password?: string;
phone?: string;
email?: string;
code?: string;
}
export type SendCodeMode = 'phone' | 'email';
export type SendCodeFormData = {
type: SendCodeMode;
phone?: string;
email?: string;
}

View File

@@ -0,0 +1,140 @@
'use client';
import { authApi, verificationApi } from "@/lib/api";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import Header from "@/app/components/Header";
import Footer from "@/app/components/Footer";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { KeyRound, Phone, Mail } from "lucide-react";
import EmailLoginMode from "./components/EmailLoginMode";
import PasswordLoginMode from "./components/PasswordLoginMode";
import PhoneLoginMode from "./components/PhoneLoginMode";
import { LoginFormData, SendCodeFormData, SubmitMode } from "./components/types";
import { useCallback, useState } from "react";
import LoginBG from './components/login-bg.jpg';
import Image from "next/image";
export default function Login() {
const router = useRouter();
const [loginMode, setLoginMode] = useState<SubmitMode>('password');
const handleForgetPassword = useCallback(() => {
toast.warning('开发中,敬请期待!暂时可通过发送邮件至网站管理员进行密码重置。');
}, []);
const handleSendCode = async (data: SendCodeFormData) => {
const res = await verificationApi.send({
type: 'login',
targetType: data.type,
phone: data.phone,
email: data.email,
})
if (res.statusCode === 200) {
toast.success('验证码已发送,请注意查收');
return true;
} else {
toast.error(res.message || '验证码发送失败,请稍后再试');
return false;
}
}
const handleSubmit = async (data: LoginFormData) => {
const res = await authApi.login({
...data,
});
if (res.statusCode === 200 && res.data) {
toast.success('登录成功');
localStorage.setItem('token', res.data.token);
router.replace('/console');
return true;
} else {
toast.error(res.message || '登录失败,请稍后再试');
return false;
}
}
return (
<>
<Header />
<div className="flex flex-1 flex-col items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm md:max-w-3xl">
<div className={cn("flex flex-col gap-6")}>
<Card className="overflow-hidden p-0">
<CardContent className="grid p-0 md:grid-cols-2">
<form className="p-6 md:p-8" onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
handleSubmit({
type: loginMode,
account: formData.get('account')?.toString(),
password: formData.get('password')?.toString(),
phone: formData.get('phone')?.toString(),
email: formData.get('email')?.toString(),
code: formData.get('code')?.toString(),
})
}}>
<div className="flex flex-col gap-6">
{loginMode === 'password' ? <PasswordLoginMode forgetPassword={handleForgetPassword} /> : null}
{loginMode === 'phone' ? <PhoneLoginMode onSendCode={handleSendCode} /> : null}
{loginMode === 'email' ? <EmailLoginMode onSendCode={handleSendCode} /> : null}
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
<span className="bg-card text-muted-foreground relative z-10 px-2">
使
</span>
</div>
<div className="grid grid-cols-3 gap-4">
<Button variant={loginMode === 'password' ? 'default' : 'outline'} type="button" className="w-full" onClick={() => setLoginMode('password')}>
<KeyRound />
</Button>
<Button variant={loginMode === 'phone' ? 'default' : 'outline'} type="button" className="w-full" onClick={() => setLoginMode('phone')}>
<Phone />
</Button>
<Button variant={loginMode === 'email' ? 'default' : 'outline'} type="button" className="w-full" onClick={() => setLoginMode('email')}>
<Mail />
</Button>
</div>
<div className="text-center text-sm">
{" "}
<a className="underline underline-offset-4 cursor-pointer" onClick={() => setLoginMode('phone')}>
</a>
</div>
</div>
</form>
<div className="bg-muted relative hidden md:block">
<Image
src={LoginBG.src}
alt="Image"
width={500}
height={500}
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
priority
quality={100}
/>
</div>
</CardContent>
</Card>
<div className="text-muted-foreground *:[a]:hover:text-primary text-center text-xs text-balance *:[a]:underline *:[a]:underline-offset-4">
{" "}
<a href="#" className="underline underline-offset-4">
</a>{" "}
{" "}
<a href="#" className="underline underline-offset-4">
</a>
</div>
</div >
</div>
</div>
<Footer />
</>
)
}