调整前端目录结构

This commit is contained in:
2025-05-10 12:08:04 +08:00
parent 3c7d84165c
commit 8e09da9912
41 changed files with 2909 additions and 355 deletions

View File

@@ -1,7 +0,0 @@
export default function Account() {
return (
<div>
</div>
)
}

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 />
</>
)
}

View File

@@ -1,21 +1,52 @@
'use client';
import { AppSidebar } from "@/components/app-sidebar"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { LoginForm, LoginFormData } from "@/components/login-form";
const handleSubmit = async (data: LoginFormData) => {
console.log(data)
}
const handleSendCode = async (data: LoginFormData) => {
console.log(data)
}
export default function Console() {
export default function Page() {
return (
<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">
<LoginForm onSubmit={handleSubmit} onSendCode={handleSendCode} />
</div>
</div>
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
</SidebarInset>
</SidebarProvider>
)
}
}