mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 20:14:36 +00:00
feat: login and register page
This commit is contained in:
8
web/package-lock.json
generated
8
web/package-lock.json
generated
@@ -30,7 +30,7 @@
|
||||
"postcss": "^8.5.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.2",
|
||||
"react-hook-form": "^7.56.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.5",
|
||||
"uuidjs": "^5.1.0",
|
||||
@@ -6863,9 +6863,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.56.2",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.2.tgz",
|
||||
"integrity": "sha512-vpfuHuQMF/L6GpuQ4c3ZDo+pRYxIi40gQqsCmmfUBwm+oqvBhKhwghCuj2o00YCgSfU6bR9KC/xnQGWm3Gr08A==",
|
||||
"version": "7.56.3",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.3.tgz",
|
||||
"integrity": "sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"postcss": "^8.5.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.56.2",
|
||||
"react-hook-form": "^7.56.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.5",
|
||||
"uuidjs": "^5.1.0",
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -8,7 +8,7 @@ import {
|
||||
} from '@/app/home/components/home-sidebar/HomeSidebarChild';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConfigList';
|
||||
import langbotIcon from '../../assets/langbot-logo.webp';
|
||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
// TODO 侧边导航栏要加动画
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import '@ant-design/v5-patch-for-react-19';
|
||||
import styles from './layout.module.css';
|
||||
import HomeSidebar from '@/app/home/components/home-sidebar/HomeSidebar';
|
||||
import HomeTitleBar from '@/app/home/components/home-titlebar/HomeTitleBar';
|
||||
import React, { useState } from 'react';
|
||||
import { SidebarChildVO } from '@/app/home/components/home-sidebar/HomeSidebarChild';
|
||||
// import { Layout } from 'antd';
|
||||
|
||||
// const { Sider, Content } = Layout;
|
||||
|
||||
export default function HomeLayout({
|
||||
children,
|
||||
|
||||
@@ -129,7 +129,13 @@ class HttpClient {
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
window.location.href = '/login';
|
||||
|
||||
console.log('401 error: ', errMessage, error.request);
|
||||
console.log('responseURL', error.request.responseURL)
|
||||
localStorage.removeItem('token');
|
||||
if (!error.request.responseURL.includes('/check-token')) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
break;
|
||||
case 403:
|
||||
console.error('Permission denied:', errMessage);
|
||||
|
||||
@@ -2,8 +2,8 @@ import './global.css';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
title: 'LangBot',
|
||||
description: 'LangBot 是大模型原生即时通信机器人平台',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { ConfigProvider, theme } from 'antd';
|
||||
|
||||
export default function LoginLayout({
|
||||
children,
|
||||
@@ -9,18 +8,8 @@ export default function LoginLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: '#2288ee',
|
||||
borderRadius: 6,
|
||||
},
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
}}
|
||||
>
|
||||
<div style={{ width: '100%', height: '100%' }}>
|
||||
<main style={{ width: '100%', height: '100%' }}>{children}</main>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
<div className="min-h-screen bg-background">
|
||||
<main className="min-h-screen">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.login {
|
||||
display: flex;
|
||||
width: 30%;
|
||||
height: 80vh;
|
||||
background-color: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.loginForm {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loginButton {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 1.5rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.socialLogin {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.socialButton {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rememberMe {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.forgetPassword {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 修改Logo样式,调整位置确保正确对齐 */
|
||||
.logoContainer {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
@@ -1,59 +1,75 @@
|
||||
'use client';
|
||||
import { Button, Input, Form, Checkbox, Divider } from 'antd';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import {
|
||||
GoogleOutlined,
|
||||
LockOutlined,
|
||||
UserOutlined,
|
||||
QqOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import styles from './login.module.css';
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import '@ant-design/v5-patch-for-react-19';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Mail, Lock } from "lucide-react";
|
||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||
|
||||
export default function Home() {
|
||||
const formSchema = z.object({
|
||||
email: z.string().email("请输入有效的邮箱地址"),
|
||||
password: z.string().min(1, "请输入密码"),
|
||||
});
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const [form] = Form.useForm<LoginField>();
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [isRegisterMode, setIsRegisterMode] = useState(false);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getIsInitialized();
|
||||
checkIfAlreadyLoggedIn();
|
||||
}, []);
|
||||
|
||||
// 检查是否为首次启动项目,只为首次启动的用户提供注册资格
|
||||
function getIsInitialized() {
|
||||
httpClient
|
||||
.checkIfInited()
|
||||
.then((res) => {
|
||||
setIsInitialized(res.initialized);
|
||||
if (!res.initialized) {
|
||||
router.push('/register');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('error at getIsInitialized: ', err);
|
||||
});
|
||||
}
|
||||
|
||||
function handleFormSubmit(formField: LoginField) {
|
||||
if (isRegisterMode) {
|
||||
handleRegister(formField.email, formField.password);
|
||||
} else {
|
||||
handleLogin(formField.email, formField.password);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRegister(username: string, password: string) {
|
||||
httpClient
|
||||
.initUser(username, password)
|
||||
function checkIfAlreadyLoggedIn() {
|
||||
httpClient.checkUserToken()
|
||||
.then((res) => {
|
||||
console.log('init user success: ', res);
|
||||
if (res.token) {
|
||||
localStorage.setItem('token', res.token);
|
||||
router.push('/home');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('init user error: ', err);
|
||||
console.log('error at checkIfAlreadyLoggedIn: ', err);
|
||||
});
|
||||
}
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
handleLogin(values.email, values.password);
|
||||
}
|
||||
|
||||
function handleLogin(username: string, password: string) {
|
||||
httpClient
|
||||
@@ -69,136 +85,73 @@ export default function Home() {
|
||||
}
|
||||
|
||||
return (
|
||||
// 使用 Ant Design 的组件库,使用 antd 的样式
|
||||
// 仅前端样式,无交互功能。
|
||||
|
||||
<div className={styles.container}>
|
||||
{/* login 类是整个 container,使用 flex 左右布局 */}
|
||||
<div className={styles.login}>
|
||||
{/* left 为注册的表单,需要填入的内容有:邮箱,密码 */}
|
||||
<div className={styles.left}>
|
||||
<div className={styles.loginForm}>
|
||||
{isRegisterMode && (
|
||||
<h1 className={styles.title}>注册 LangBot 账号</h1>
|
||||
)}
|
||||
{!isRegisterMode && (
|
||||
<h1 className={styles.title}>欢迎回到 LangBot</h1>
|
||||
)}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={(values) => {
|
||||
handleFormSubmit(values);
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<Card className="w-[360px]">
|
||||
<CardHeader>
|
||||
<img src={langbotIcon.src} alt="LangBot" className="w-16 h-16 mb-4 mx-auto" />
|
||||
<CardTitle className="text-2xl text-center">
|
||||
欢迎回到 LangBot 👋
|
||||
</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
登录以继续
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱!' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址!' },
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="输入邮箱地址"
|
||||
size="large"
|
||||
prefix={<UserOutlined />}
|
||||
/>
|
||||
</Form.Item>
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>邮箱</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="输入邮箱地址"
|
||||
className="pl-10"
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码!' }]}
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="输入密码"
|
||||
size="large"
|
||||
prefix={<LockOutlined />}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<div className={styles.rememberMe}>
|
||||
<Checkbox
|
||||
checked={rememberMe}
|
||||
onChange={(e) => setRememberMe(e.target.checked)}
|
||||
>
|
||||
30天内自动登录
|
||||
</Checkbox>
|
||||
<span>
|
||||
<a href="#" className={`${styles.forgetPassword}`}>
|
||||
忘记密码?
|
||||
</a>
|
||||
{!isRegisterMode && (
|
||||
<a
|
||||
href=""
|
||||
onClick={(event) => {
|
||||
setIsRegisterMode(true);
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
去注册?
|
||||
</a>
|
||||
)}
|
||||
{isRegisterMode && (
|
||||
<a
|
||||
href=""
|
||||
onClick={(event) => {
|
||||
setIsRegisterMode(false);
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
去登录
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>密码</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="输入密码"
|
||||
className="pl-10"
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className={styles.loginButton}
|
||||
block
|
||||
htmlType="submit"
|
||||
disabled={isRegisterMode && isInitialized}
|
||||
type="submit"
|
||||
className="w-full mt-4 cursor-pointer"
|
||||
>
|
||||
{isRegisterMode
|
||||
? isInitialized
|
||||
? '暂不提供注册'
|
||||
: '注册'
|
||||
: '登录'}
|
||||
登录
|
||||
</Button>
|
||||
|
||||
<Divider className={styles.divider}>或</Divider>
|
||||
|
||||
<div className={styles.socialLogin}>
|
||||
<Button
|
||||
className={styles.socialButton}
|
||||
icon={<GoogleOutlined />}
|
||||
size="large"
|
||||
disabled={true}
|
||||
>
|
||||
使用谷歌账号登录
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ height: '10px' }}></div>
|
||||
<div className={styles.socialLogin}>
|
||||
<Button
|
||||
className={styles.socialButton}
|
||||
icon={<QqOutlined />}
|
||||
size="large"
|
||||
disabled={true}
|
||||
>
|
||||
使用QQ账号登录
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface LoginField {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
router.push('/login');
|
||||
}, []);
|
||||
return <div className={``}></div>;
|
||||
}
|
||||
|
||||
15
web/src/app/register/layout.tsx
Normal file
15
web/src/app/register/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default function RegisterLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<main className="min-h-screen">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
146
web/src/app/register/page.tsx
Normal file
146
web/src/app/register/page.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
'use client';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormDescription,
|
||||
} from "@/components/ui/form";
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Mail, Lock } from "lucide-react";
|
||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.string().email("请输入有效的邮箱地址"),
|
||||
password: z.string().min(1, "请输入密码"),
|
||||
});
|
||||
|
||||
export default function Register() {
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getIsInitialized();
|
||||
}, []);
|
||||
|
||||
function getIsInitialized() {
|
||||
httpClient
|
||||
.checkIfInited()
|
||||
.then((res) => {
|
||||
if (res.initialized) {
|
||||
router.push('/login');
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('error at getIsInitialized: ', err);
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
handleRegister(values.email, values.password);
|
||||
}
|
||||
|
||||
function handleRegister(username: string, password: string) {
|
||||
httpClient
|
||||
.initUser(username, password)
|
||||
.then((res) => {
|
||||
console.log('init user success: ', res);
|
||||
router.push('/login');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('init user error: ', err);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<Card className="w-[360px]">
|
||||
<CardHeader>
|
||||
<img src={langbotIcon.src} alt="LangBot" className="w-16 h-16 mb-4 mx-auto" />
|
||||
<CardTitle className="text-2xl text-center">
|
||||
初始化 LangBot 👋
|
||||
</CardTitle>
|
||||
<CardDescription className="text-center">
|
||||
这是您首次启动 LangBot
|
||||
<br />
|
||||
您填写的邮箱和密码将作为初始管理员账号
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>邮箱</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="输入邮箱地址"
|
||||
className="pl-10"
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>密码</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="输入密码"
|
||||
className="pl-10"
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full mt-4 cursor-pointer"
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
92
web/src/components/ui/card.tsx
Normal file
92
web/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
Reference in New Issue
Block a user