From cf6076f50424d3b8fb0ece57dde98ccd7c60b4cb Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 9 May 2025 20:33:12 +0800 Subject: [PATCH] feat: login and register page --- web/package-lock.json | 8 +- web/package.json | 2 +- .../app/{home => }/assets/langbot-logo.webp | Bin .../components/home-sidebar/HomeSidebar.tsx | 2 +- web/src/app/home/layout.tsx | 4 - web/src/app/infra/http/HttpClient.ts | 8 +- web/src/app/layout.tsx | 4 +- web/src/app/login/layout.tsx | 17 +- web/src/app/login/login.module.css | 98 ------- web/src/app/login/page.tsx | 257 +++++++----------- web/src/app/page.tsx | 9 + web/src/app/register/layout.tsx | 15 + web/src/app/register/page.tsx | 146 ++++++++++ web/src/components/ui/card.tsx | 92 +++++++ 14 files changed, 385 insertions(+), 277 deletions(-) rename web/src/app/{home => }/assets/langbot-logo.webp (100%) delete mode 100644 web/src/app/login/login.module.css create mode 100644 web/src/app/register/layout.tsx create mode 100644 web/src/app/register/page.tsx create mode 100644 web/src/components/ui/card.tsx diff --git a/web/package-lock.json b/web/package-lock.json index a1ade004..69d7c822 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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" diff --git a/web/package.json b/web/package.json index 2a1105bc..d058d419 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/app/home/assets/langbot-logo.webp b/web/src/app/assets/langbot-logo.webp similarity index 100% rename from web/src/app/home/assets/langbot-logo.webp rename to web/src/app/assets/langbot-logo.webp diff --git a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx index c18e2a59..8f0ef356 100644 --- a/web/src/app/home/components/home-sidebar/HomeSidebar.tsx +++ b/web/src/app/home/components/home-sidebar/HomeSidebar.tsx @@ -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 侧边导航栏要加动画 diff --git a/web/src/app/home/layout.tsx b/web/src/app/home/layout.tsx index 2d2d7e56..f12f2973 100644 --- a/web/src/app/home/layout.tsx +++ b/web/src/app/home/layout.tsx @@ -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, diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index f7acb654..9243a1bb 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -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); diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index fe4f4284..6d6e7ad0 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -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({ diff --git a/web/src/app/login/layout.tsx b/web/src/app/login/layout.tsx index f341f627..4996a7ac 100644 --- a/web/src/app/login/layout.tsx +++ b/web/src/app/login/layout.tsx @@ -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 ( - -
-
{children}
-
-
+
+
{children}
+
); } diff --git a/web/src/app/login/login.module.css b/web/src/app/login/login.module.css deleted file mode 100644 index b6435980..00000000 --- a/web/src/app/login/login.module.css +++ /dev/null @@ -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; -} diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx index 3a8eee2c..afdafbdd 100644 --- a/web/src/app/login/page.tsx +++ b/web/src/app/login/page.tsx @@ -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(); - const [rememberMe, setRememberMe] = useState(false); - const [isRegisterMode, setIsRegisterMode] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); + + const form = useForm>({ + 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) { + handleLogin(values.email, values.password); + } function handleLogin(username: string, password: string) { httpClient @@ -69,136 +85,73 @@ export default function Home() { } return ( - // 使用 Ant Design 的组件库,使用 antd 的样式 - // 仅前端样式,无交互功能。 - -
- {/* login 类是整个 container,使用 flex 左右布局 */} -
- {/* left 为注册的表单,需要填入的内容有:邮箱,密码 */} -
-
- {isRegisterMode && ( -

注册 LangBot 账号

- )} - {!isRegisterMode && ( -

欢迎回到 LangBot

- )} -
{ - handleFormSubmit(values); - }} - > - + + + LangBot + + 欢迎回到 LangBot 👋 + + + 登录以继续 + + + + + + - } - /> - + render={({ field }) => ( + + 邮箱 + +
+ + +
+
+ +
+ )} + /> - - } - /> - - -
- setRememberMe(e.target.checked)} - > - 30天内自动登录 - - - - 忘记密码? - - {!isRegisterMode && ( - { - setIsRegisterMode(true); - event.preventDefault(); - }} - > - 去注册? - - )} - {isRegisterMode && ( - { - setIsRegisterMode(false); - event.preventDefault(); - }} - > - 去登录 - - )} - -
+ render={({ field }) => ( + + 密码 + +
+ + +
+
+ +
+ )} + /> - - - -
- -
-
-
- -
-
-
-
-
+ + + +
); } - -interface LoginField { - email: string; - password: string; -} diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 29814463..c2943fe9 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -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
; } diff --git a/web/src/app/register/layout.tsx b/web/src/app/register/layout.tsx new file mode 100644 index 00000000..c93e0bde --- /dev/null +++ b/web/src/app/register/layout.tsx @@ -0,0 +1,15 @@ +'use client'; + +import React from 'react'; + +export default function RegisterLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+
{children}
+
+ ); +} diff --git a/web/src/app/register/page.tsx b/web/src/app/register/page.tsx new file mode 100644 index 00000000..81e6ff8e --- /dev/null +++ b/web/src/app/register/page.tsx @@ -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>({ + 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) { + 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 ( +
+ + + LangBot + + 初始化 LangBot 👋 + + + 这是您首次启动 LangBot +
+ 您填写的邮箱和密码将作为初始管理员账号 +
+
+ +
+ + ( + + 邮箱 + +
+ + +
+
+ +
+ )} + /> + + ( + + 密码 + +
+ + +
+
+ +
+ )} + /> + + + + +
+
+
+ ); +} diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx new file mode 100644 index 00000000..d05bbc6c --- /dev/null +++ b/web/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}