feat: implement user authentication with login functionality

- Add auth-api for handling login requests.
- Update index.ts to export AuthApi.
- Modify HeroLayout to display username or login link based on authentication state.
- Create LoginPage component for user login.
- Update router to include login route with EmptyLayout.
- Configure WebClient to include credentials in requests.
- Add auth-slice for managing authentication state in Redux.
- Update Redux store to include auth reducer.
- Define LoginRequest and User types in types/index.ts.
- Configure Vite to proxy API requests to the backend server.
This commit is contained in:
2026-04-14 11:17:31 +08:00
parent b000336d22
commit ac76150915
12 changed files with 249 additions and 98 deletions
+9
View File
@@ -0,0 +1,9 @@
import { LoginRequest, User } from "@/types"
import { WebClient } from "@/shared/web-client"
export async function login(loginRequest: LoginRequest): Promise<User> {
const { data } = await WebClient.post<User>("/auth/login", {
...loginRequest,
})
return data
}
+1
View File
@@ -1,3 +1,4 @@
export * as FirearmApi from "./firearm-api"
export * as ModificationApi from "./modification-api"
export * as TagApi from "./tag-api"
export * as AuthApi from "./auth-api"
+14
View File
@@ -1,6 +1,7 @@
import { Outlet, Link } from "react-router-dom"
import { useMemo } from "react"
import dayjs from "dayjs"
import { useAppSelector } from "@/store"
/**
* Main application component that serves as the root layout.
@@ -8,6 +9,7 @@ import dayjs from "dayjs"
*/
export default function HeroLayout() {
const today = useMemo(() => dayjs(), [])
const user = useAppSelector((state) => state.auth.user)
return (
<div className="bg-gray-50">
@@ -33,6 +35,18 @@ export default function HeroLayout() {
>
</Link>
{user ? (
<span className="text-gray-700 px-3 py-2 rounded-md text-sm font-medium">
{user.username}
</span>
) : (
<Link
to="/login"
className="text-gray-500 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
</Link>
)}
<a
href="https://github.com/zihluwang/delta-force-firearm-modification-codes"
target="_blank"
+67
View File
@@ -0,0 +1,67 @@
import { useState } from "react"
import { useNavigate } from "react-router-dom"
import { App, Button, Card, Form, Input, Typography } from "antd"
import { AuthApi } from "@/api"
import { useAppDispatch } from "@/store"
import { setCurrentUser } from "@/store/auth-slice"
import { LoginRequest } from "@/types"
export default function LoginPage() {
const navigate = useNavigate()
const { message } = App.useApp()
const dispatch = useAppDispatch()
const [loading, setLoading] = useState(false)
async function onFinish(values: LoginRequest) {
setLoading(true)
try {
const user = await AuthApi.login(values)
dispatch(setCurrentUser(user))
message.success(`欢迎回来,${user.username}`)
navigate("/firearms")
} catch {
message.error("登录失败,请检查帐号或密码")
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-gray-100 px-4 py-10 sm:py-16">
<div className="mx-auto max-w-md">
<Card bordered={false} className="shadow-sm">
<Typography.Title level={3} className="!mb-2 text-center">
</Typography.Title>
<Typography.Paragraph className="!mb-6 text-center !text-gray-500">
使
</Typography.Paragraph>
<Form<LoginRequest> layout="vertical" onFinish={onFinish} requiredMark={false}>
<Form.Item<LoginRequest>
name="principle"
label="帐号"
rules={[{ required: true, message: "请输入帐号" }]}
>
<Input autoComplete="username" placeholder="请输入帐号" />
</Form.Item>
<Form.Item<LoginRequest>
name="credential"
label="密码"
rules={[{ required: true, message: "请输入密码" }]}
>
<Input.Password autoComplete="current-password" placeholder="请输入密码" />
</Form.Item>
<Form.Item className="!mb-0">
<Button type="primary" htmlType="submit" loading={loading} block>
</Button>
</Form.Item>
</Form>
</Card>
</div>
</div>
)
}
+11
View File
@@ -1,6 +1,7 @@
import { ComponentType } from "react"
import { createBrowserRouter } from "react-router-dom"
import ErrorPage from "@/components/error-page"
import EmptyLayout from "@/layout/empty-layout"
import HeroLayout from "@/layout/hero-layout"
function lazy<T extends { default: ComponentType<unknown> }>(importer: () => Promise<T>) {
@@ -37,6 +38,16 @@ const router = createBrowserRouter(
},
],
},
{
element: <EmptyLayout />,
errorElement: <ErrorPage />,
children: [
{
path: "login",
lazy: lazy(() => import("@/page/login")),
},
],
},
],
{
basename: "/",
+1
View File
@@ -4,6 +4,7 @@ import dayjs from "dayjs"
const WebClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: dayjs.duration({ seconds: 10 }).asMilliseconds(),
withCredentials: true
})
export { WebClient }
+26
View File
@@ -0,0 +1,26 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
import { User } from "@/types"
interface AuthState {
user: User | null
}
const initialState: AuthState = {
user: null,
}
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
setCurrentUser(state, action: PayloadAction<User>) {
state.user = action.payload
},
clearCurrentUser(state) {
state.user = null
},
},
})
export const { setCurrentUser, clearCurrentUser } = authSlice.actions
export const authReducer = authSlice.reducer
+3 -1
View File
@@ -11,6 +11,7 @@ import {
REGISTER,
} from "redux-persist"
import createWebStorage from "redux-persist/es/storage/createWebStorage"
import { authReducer } from "./auth-slice"
import { firearmsReducer } from "./firearms-slice"
const storage = createWebStorage(import.meta.env.VITE_REDUX_STORAGE ?? "local")
@@ -18,10 +19,11 @@ const storage = createWebStorage(import.meta.env.VITE_REDUX_STORAGE ?? "local")
const persistConfig = {
key: "root",
storage,
whitelist: ["firearms"],
whitelist: ["auth", "firearms"],
}
const rootReducer = combineReducers({
auth: authReducer,
firearms: firearmsReducer
})
+11
View File
@@ -47,3 +47,14 @@ export interface PageQueryParams {
sortBy?: string
direction?: Direction
}
export interface LoginRequest {
principle: string
credential: string
}
export interface User {
id: number
username: string
email: string
}