From 32fd3c30f8fa71ade06f9a561d58ec7a15e1ee25 Mon Sep 17 00:00:00 2001
From: cs10086 <1291454411@qq.com>
Date: Sat, 11 Apr 2026 05:34:33 +0800
Subject: [PATCH] 1
---
package.json | 2 +-
pnpm-lock.yaml | 2 +-
src/components/Navbar/Navbar.tsx | 56 +++--
src/features/login/authSlice.ts | 263 -----------------------
src/features/login/authSlice.tsx | 279 ++++++++++++++++++++++++
src/global.css | 46 +++-
src/pages/Home/Home.tsx | 137 ++++++------
src/pages/Profile/Profile.tsx | 358 +++++++++++++++++++++++++++++--
tsconfig.json | 7 -
vite.config.ts | 3 +-
10 files changed, 771 insertions(+), 382 deletions(-)
delete mode 100644 src/features/login/authSlice.ts
create mode 100644 src/features/login/authSlice.tsx
diff --git a/package.json b/package.json
index 8a71b72..5111eb3 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
- "@ant-design/icons": "^6.1.1",
+ "@ant-design/icons": "6.x",
"@chakra-ui/react": "^3.34.0",
"@emotion/react": "^11.14.0",
"@reduxjs/toolkit": "^2.11.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 59794e7..75b0eb5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,7 +9,7 @@ importers:
.:
dependencies:
'@ant-design/icons':
- specifier: ^6.1.1
+ specifier: 6.x
version: 6.1.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@chakra-ui/react':
specifier: ^3.34.0
diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx
index e8f63ab..a6cbfb5 100644
--- a/src/components/Navbar/Navbar.tsx
+++ b/src/components/Navbar/Navbar.tsx
@@ -1,3 +1,4 @@
+import "./Navbar.css";
import {
HomeOutlined,
SettingOutlined,
@@ -16,7 +17,7 @@ import {
import type { MenuProps } from "antd";
import { Menu, Modal } from "antd";
import React, { useState } from "react";
-import { Input, Avatar } from "antd";
+import { Input, Avatar, Spin } from "antd";
import { SearchOutlined } from "@ant-design/icons";
@@ -24,7 +25,7 @@ import { PasswordInput } from "@/components/ui/password-input";
import { Input as ChakraInput, InputGroup, Box } from "@chakra-ui/react";
-import { QRCode, Button } from "antd";
+import { QRCode, Button } from "antd";
// import { PasswordInput } from "@/components/ui/password-input"
import { Tabs } from "antd";
@@ -43,14 +44,17 @@ import {
selectRefreshToken,
selectError,
selectTokenType,
- selectExpiresIn,
+ selectExpiresAtTimestamp,
selectLoading,
emailCodeLogin,
sendEmailCode,
+ logout,
} from "@/features/login";
import { useAppDispatch } from "@/app/store";
+import { DataList } from "@chakra-ui/react"
+
import { useSelector } from "react-redux";
const Navbar: React.FC = () => {
@@ -62,7 +66,7 @@ const Navbar: React.FC = () => {
const refreshToken = useSelector(selectRefreshToken);
const tokenType = useSelector(selectTokenType);
const error = useSelector(selectError);
- const expiresIn = useSelector(selectExpiresIn);
+ const expiresAtTimestamp = useSelector(selectExpiresAtTimestamp);
const loading = useSelector(selectLoading);
const dispatch = useAppDispatch();
@@ -101,6 +105,8 @@ const Navbar: React.FC = () => {
setLoginTabValue("1");
};
+ const navigation = useNavigate();
+
const items: MenuProps["items"] = [
{
label: "主页",
@@ -238,6 +244,10 @@ const Navbar: React.FC = () => {
label: "退出登录",
key: "logout",
icon: ,
+ onClick: async () => {
+ dispatch(logout());
+ navigation("/");
+ },
},
],
},
@@ -337,8 +347,6 @@ const Navbar: React.FC = () => {
],
},
];
-
- const navigation = useNavigate();
const onClick: MenuProps["onClick"] = (e) => {
console.log("click ", e);
setCurrentMenuItem(e.key);
@@ -398,13 +406,17 @@ const Navbar: React.FC = () => {
-
-
扫描二维码登录
+
+ 扫描二维码登录
+
@@ -611,25 +623,27 @@ const Navbar: React.FC = () => {
gap="medium"
vertical={false}
justify="center"
- className="margin-top-30"
>
+
-
+
{
+
);
};
diff --git a/src/features/login/authSlice.ts b/src/features/login/authSlice.ts
deleted file mode 100644
index cac4b19..0000000
--- a/src/features/login/authSlice.ts
+++ /dev/null
@@ -1,263 +0,0 @@
-// src/features/auth/authSlice.ts
-import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
-
-import { api_request } from '@/utils/request';
-
-// 用户类型定义
-export interface User {
- id: number;
- username: string;
- email: string;
- is_active: boolean;
- is_staff: boolean;
- is_superuser: boolean;
- last_login: string | null;
- date_joined: string;
-}
-
-// 登录响应类型
-export interface AuthResponse {
- user: User;
- refresh: string;
- access: string;
- token_type: string;
- expires_in: number;
-}
-
-export interface EmailLoginRequests {
- to_email: string;
- code?: string;
-}
-
-export interface PasswordLoginRequests {
- account: string;
- password: string;
-}
-
-// 状态类型
-export interface AuthState {
- user: User | null;
- accessToken: string | null;
- refreshToken: string | null;
- tokenType: string | null;
- isLogin: boolean;
- error: string | null;
- loading: boolean;
- expiresIn: number | null;
-
-}
-
-export interface ErrorResponse {
- code: string,
- message: string
-}
-
-// 基础响应类型
-export interface BaseResponse {
- success: boolean;
- message?: string;
- data?: T;
- code?: string | number;
- timestamp?: number;
-}
-
-// 初始状态
-const initialState: AuthState = {
- user: JSON.parse(localStorage.getItem('user') || 'null'),
- accessToken: localStorage.getItem('access_token'),
- refreshToken: localStorage.getItem('refresh_token'),
- tokenType: localStorage.getItem('token_type'),
- isLogin: !!localStorage.getItem('access_token'),
- error: null,
- loading: false,
- expiresIn: localStorage.getItem('expires_in') ? parseInt(localStorage.getItem('expires_in')!, 10) : null,
-};
-
-export const sendEmailCode = createAsyncThunk<
- BaseResponse,
- EmailLoginRequests, // 参数类型
- {
- rejectValue: ErrorResponse;
- }
->('/user/emailCodeLogin', async (credentials, { rejectWithValue }) => {
- try {
- console.log('发送邮箱验证码请求');
-
- const response = await api_request.login.auth.send_user_email({
- to_email: credentials.to_email,
- });
-
- const responseData = response.data as BaseResponse;
-
- // 2. 检查响应是否有效
- if (!response || !responseData) {
- return rejectWithValue({
- code: '1001',
- message: '服务器返回空响应',
- });
- }
-
- // const user = responseData?.user || null;
- // const refresh= responseData?.refresh || null;
- // const access= responseData?.access || null;
- // const token_type= responseData?.token_type || null;
- // const expires_in= responseData?.expires_in ? Number(responseData?.expires_in) * 1000 + Date.now() : null;
-
- // localStorage.setItem('user', JSON.stringify(user));
- // localStorage.setItem('access_token', JSON.stringify(access));
- // localStorage.setItem('refresh_token', JSON.stringify(refresh));
- // localStorage.setItem('token_type', JSON.stringify(token_type));
- // if (expires_in) {
- // localStorage.setItem('expires_in', JSON.stringify(expires_in));
- // }
-
- return responseData;
-
- } catch (error: any) {
- console.error('邮箱验证码发送失败:', error);
-
- const errorCode = error.response?.status?.toString() || '1000';
- const errorMessage = error.response?.data?.message
- || error.message
- || '邮箱登录请求失败';
-
- return rejectWithValue({
- code: errorCode,
- message: errorMessage,
- });
- }
-});
-
-
-export const emailCodeLogin = createAsyncThunk<
- AuthResponse,
- EmailLoginRequests, // 参数类型
- {
- rejectValue: ErrorResponse;
- }
->('/user/user_login_or_register/', async (credentials, { rejectWithValue }) => {
- try {
- console.log('发送邮箱登录请求');
-
- const response = await api_request.login.auth.user_login_or_register({
- email: credentials.to_email,
- code: credentials.code,
- });
-
- const responseData = response.data as AuthResponse;
-
- // 2. 检查响应是否有效
- if (!response || !responseData) {
- return rejectWithValue({
- code: '1001',
- message: '服务器返回空响应',
- });
- }
-
- const user = responseData?.user || null;
- const refresh= responseData?.refresh || null;
- const access= responseData?.access || null;
- const token_type= responseData?.token_type || null;
- const expires_in= responseData?.expires_in ? Number(responseData?.expires_in) * 1000 + Date.now() : null;
-
- localStorage.setItem('user', JSON.stringify(user));
- localStorage.setItem('access_token', JSON.stringify(access));
- localStorage.setItem('refresh_token', JSON.stringify(refresh));
- localStorage.setItem('token_type', JSON.stringify(token_type));
- if (expires_in) {
- localStorage.setItem('expires_in', JSON.stringify(expires_in));
- }
-
- return responseData;
-
- } catch (error: any) {
-
- const errorCode = error.response?.status?.toString() || '1000';
- const errorMessage = error.response?.data?.message
- || error.message
- || '邮箱登录请求失败';
-
- return rejectWithValue({
- code: errorCode,
- message: errorMessage,
- });
- }
-});
-
-const authSlice = createSlice({
- name: 'auth',
- initialState,
- reducers: {
- logout: (state) => {
- state.user = null;
- state.accessToken = null;
- state.refreshToken = null;
- state.tokenType = null;
- state.isLogin = false;
- state.error = null;
- state.expiresIn = null;
-
- // 删除 localStorage 中的 token
- localStorage.removeItem('access_token');
- localStorage.removeItem('refresh_token');
- localStorage.removeItem('token_type');
- localStorage.removeItem('user');
- localStorage.removeItem('expires_in');
- localStorage.removeItem('user');
-
- }
- },
- extraReducers: (builder) => {
- builder
- // 处理登录
- .addCase(emailCodeLogin.pending, (state) => {
- state.loading = true;
- state.error = null;
- })
- .addCase(emailCodeLogin.fulfilled, (state, action) => {
- state.loading = false;
- state.error = null;
- state.user = action.payload.user;
- state.accessToken = action.payload.access;
- state.refreshToken = action.payload.refresh;
- state.tokenType = action.payload.token_type;
- state.expiresIn = action.payload.expires_in;
- state.isLogin = true;
- })
- .addCase(emailCodeLogin.rejected, (state, action) => {
- state.loading = false;
- state.error = action.payload?.message || '登录失败';
- })
- // 处理发送邮箱验证码
- .addCase(sendEmailCode.pending, (state) => {
- state.loading = true;
- state.error = null;
- })
- .addCase(sendEmailCode.fulfilled, (state, action) => {
- state.loading = false;
- state.error = null;
- })
- .addCase(sendEmailCode.rejected, (state, action) => {
- state.loading = false;
- state.error = action.payload?.message || '发送邮箱验证码失败';
- })
- },
-});
-
-
-// 导出 actions
-export const { logout } = authSlice.actions;
-
-// 导出 reducer
-export default authSlice.reducer;
-
-// 导出选择器
-export const selectAuth = (state: { auth: AuthState }) => state.auth;
-export const selectUser = (state: { auth: AuthState }) => state.auth.user;
-export const selectError = (state: { auth: AuthState }) => state.auth.error;
-export const selectAccessToken = (state: { auth: AuthState }) => state.auth.accessToken;
-export const selectRefreshToken = (state: { auth: AuthState }) => state.auth.refreshToken;
-export const selectTokenType = (state: { auth: AuthState }) => state.auth.tokenType;
-export const selectIsLogin = (state: { auth: AuthState }) => state.auth.isLogin;
-export const selectExpiresIn = (state: { auth: AuthState }) => state.auth.expiresIn;
-export const selectLoading = (state: { auth: AuthState }) => state.auth.loading;
\ No newline at end of file
diff --git a/src/features/login/authSlice.tsx b/src/features/login/authSlice.tsx
new file mode 100644
index 0000000..f427a26
--- /dev/null
+++ b/src/features/login/authSlice.tsx
@@ -0,0 +1,279 @@
+// src/features/auth/authSlice.ts
+import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
+import { notification } from "antd";
+import { api_request } from "@/utils/request";
+
+import { CloseCircleFilled } from '@ant-design/icons';
+
+// 用户类型定义
+export interface User {
+ id: number;
+ username: string;
+ email: string;
+ is_active: boolean;
+ is_staff: boolean;
+ is_superuser: boolean;
+ last_login: string | null;
+ date_joined: string;
+}
+
+// 登录响应类型
+export interface AuthResponse {
+ user: User;
+ refresh: string;
+ access: string;
+ token_type: string;
+ expires_at_timestamp: number;
+}
+
+export interface EmailLoginRequests {
+ to_email: string;
+ code?: string;
+}
+
+export interface PasswordLoginRequests {
+ account: string;
+ password: string;
+}
+
+// 状态类型
+export interface AuthState {
+ user: User | null;
+ accessToken: string | null;
+ refreshToken: string | null;
+ tokenType: string | null;
+ isLogin: boolean;
+ error: string | null;
+ loading: boolean;
+ expiresAtTimestamp: number | null;
+}
+
+export interface ErrorResponse {
+ code: string;
+ message: string;
+}
+
+// 基础响应类型
+export interface BaseResponse {
+ success: boolean;
+ message?: string;
+ data?: T;
+ code?: string | number;
+ timestamp?: number;
+}
+
+// 初始状态
+const initialState: AuthState = {
+ user: JSON.parse(localStorage.getItem("user") || "null"),
+ accessToken: localStorage.getItem("access_token"),
+ refreshToken: localStorage.getItem("refresh_token"),
+ tokenType: localStorage.getItem("token_type"),
+ isLogin: !!localStorage.getItem("access_token"),
+ error: null,
+ loading: false,
+ expiresIn: localStorage.getItem("expires_at_timestamp")
+ ? parseInt(localStorage.getItem("expires_at_timestamp")!, 10)
+ : null,
+};
+
+export const sendEmailCode = createAsyncThunk<
+ BaseResponse,
+ EmailLoginRequests, // 参数类型
+ {
+ rejectValue: ErrorResponse;
+ }
+>("/user/emailCodeLogin", async (credentials, { rejectWithValue }) => {
+ try {
+ console.log("发送邮箱验证码请求");
+
+ const response = await api_request.login.auth.send_user_email({
+ to_email: credentials.to_email,
+ });
+
+ const responseData = response.data as BaseResponse;
+
+ // 2. 检查响应是否有效
+ if (!response || !responseData) {
+ return rejectWithValue({
+ code: "1001",
+ message: "服务器返回空响应",
+ });
+ }
+
+ // const user = responseData?.user || null;
+ // const refresh= responseData?.refresh || null;
+ // const access= responseData?.access || null;
+ // const token_type= responseData?.token_type || null;
+ // const expires_in= responseData?.expires_in ? Number(responseData?.expires_in) * 1000 + Date.now() : null;
+
+ // localStorage.setItem('user', JSON.stringify(user));
+ // localStorage.setItem('access_token', JSON.stringify(access));
+ // localStorage.setItem('refresh_token', JSON.stringify(refresh));
+ // localStorage.setItem('token_type', JSON.stringify(token_type));
+ // if (expires_in) {
+ // localStorage.setItem('expires_in', JSON.stringify(expires_in));
+ // }
+
+ return responseData;
+ } catch (error: any) {
+ console.error("邮箱验证码发送失败:", error);
+
+ const errorCode = error.response?.status?.toString() || "1000";
+ const errorMessage =
+ error.response?.data?.message || error.message || "邮箱登录请求失败";
+
+ return rejectWithValue({
+ code: errorCode,
+ message: errorMessage,
+ });
+ }
+});
+
+export const emailCodeLogin = createAsyncThunk<
+ AuthResponse,
+ EmailLoginRequests, // 参数类型
+ {
+ rejectValue: ErrorResponse;
+ }
+>("/user/user_login_or_register/", async (credentials, { rejectWithValue }) => {
+ try {
+ console.log("发送邮箱登录请求");
+
+ const response = await api_request.login.auth.user_login_or_register({
+ email: credentials.to_email,
+ code: credentials.code,
+ });
+
+ const responseData = response.data as AuthResponse;
+
+ // 2. 检查响应是否有效
+ if (!response || !responseData) {
+ return rejectWithValue({
+ code: "1001",
+ message: "服务器返回空响应",
+ });
+ }
+
+ const user = responseData?.user || null;
+ const refresh = responseData?.refresh || null;
+ const access = responseData?.access || null;
+ const token_type = responseData?.token_type || null;
+ const expires_at_timestamp = responseData?.expires_at_timestamp || null;
+
+ localStorage.setItem("user", JSON.stringify(user));
+ localStorage.setItem("access_token", JSON.stringify(access));
+ localStorage.setItem("refresh_token", JSON.stringify(refresh));
+ localStorage.setItem("token_type", JSON.stringify(token_type));
+ if (expires_at_timestamp) {
+ localStorage.setItem(
+ "expires_at_timestamp",
+ JSON.stringify(expires_at_timestamp),
+ );
+ }
+
+ return responseData;
+ } catch (error: any) {
+ const errorCode = error.response?.status?.toString() || "1000";
+ const errorMessage =
+ error.response?.data?.message || error.message || "邮箱登录请求失败";
+
+ return rejectWithValue({
+ code: errorCode,
+ message: errorMessage,
+ });
+ }
+});
+
+const authSlice = createSlice({
+ name: "auth",
+ initialState,
+ reducers: {
+ logout: (state) => {
+ state.user = null;
+ state.accessToken = null;
+ state.refreshToken = null;
+ state.tokenType = null;
+ state.isLogin = false;
+ state.error = null;
+ state.expiresAtTimestamp = null;
+
+ // 删除 localStorage 中的 token
+ localStorage.removeItem("access_token");
+ localStorage.removeItem("refresh_token");
+ localStorage.removeItem("token_type");
+ localStorage.removeItem("user");
+ localStorage.removeItem("expires_in");
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ // 处理登录
+ .addCase(emailCodeLogin.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(emailCodeLogin.fulfilled, (state, action) => {
+ state.loading = false;
+ state.error = null;
+ state.user = action.payload.user;
+ state.accessToken = action.payload.access;
+ state.refreshToken = action.payload.refresh;
+ state.tokenType = action.payload.token_type;
+ state.expiresAtTimestamp = action.payload.expires_at_timestamp;
+ state.isLogin = true;
+ })
+ .addCase(emailCodeLogin.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload?.message || "登录失败";
+ notification.error({
+ title: "登录失败",
+ type: "error",
+ showProgress: true,
+ description: state.error,
+ placement: "top",
+ });
+ })
+ // 处理发送邮箱验证码
+ .addCase(sendEmailCode.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(sendEmailCode.fulfilled, (state, action) => {
+ state.loading = false;
+ state.error = null;
+ })
+ .addCase(sendEmailCode.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.payload?.message || "发送邮箱验证码失败";
+ notification.open({
+ title: "发送验证码失败",
+ type: "error",
+ showProgress: true,
+ description: state.error,
+ placement: "top",
+ icon: ,
+ });
+ });
+ },
+});
+
+// 导出 actions
+export const { logout } = authSlice.actions;
+
+// 导出 reducer
+export default authSlice.reducer;
+
+// 导出选择器
+export const selectAuth = (state: { auth: AuthState }) => state.auth;
+export const selectUser = (state: { auth: AuthState }) => state.auth.user;
+export const selectError = (state: { auth: AuthState }) => state.auth.error;
+export const selectAccessToken = (state: { auth: AuthState }) =>
+ state.auth.accessToken;
+export const selectRefreshToken = (state: { auth: AuthState }) =>
+ state.auth.refreshToken;
+export const selectTokenType = (state: { auth: AuthState }) =>
+ state.auth.tokenType;
+export const selectIsLogin = (state: { auth: AuthState }) => state.auth.isLogin;
+export const selectExpiresAtTimestamp = (state: { auth: AuthState }) =>
+ state.auth.expiresAtTimestamp;
+export const selectLoading = (state: { auth: AuthState }) => state.auth.loading;
diff --git a/src/global.css b/src/global.css
index 5906c26..6cb7d43 100644
--- a/src/global.css
+++ b/src/global.css
@@ -104,17 +104,23 @@
margin: 0 5px;
}
+
/* Reusable layout utilities */
+
.width_100 {
width: 100%;
}
+
/* Modal and dialog utilities */
+
.modal-width {
width: 852px;
}
+
/* Text and color utilities */
+
.text-black {
color: #000;
}
@@ -155,12 +161,16 @@
margin-bottom: 30px;
}
+
/* Button and interactive elements */
+
.button-padding-h60 {
padding: 0 60px;
}
+
/* Margin and spacing utilities */
+
.margin-right-10 {
margin-right: 10px;
}
@@ -173,11 +183,9 @@
margin: 10px 10px;
}
-.margin-top-30 {
- margin-top: 30px;
-}
/* Layout and spacing utilities for Home page */
+
.width_100 {
width: 100%;
}
@@ -256,7 +264,9 @@
margin-top: 0;
}
+
/* Document page specific utilities */
+
.width_1000 {
width: 1000px;
}
@@ -305,7 +315,9 @@
margin-bottom: 10px;
}
+
/* Additional margin utilities */
+
.margin-left-16 {
margin-left: 16px;
}
@@ -322,7 +334,9 @@
margin: 10px;
}
+
/* Settings page specific utilities */
+
.width-256 {
width: 256px;
}
@@ -339,6 +353,10 @@
font-size: 24px;
}
+.font-size-20 {
+ font-size: 20px;
+}
+
.font-size-16 {
font-size: 16px;
}
@@ -351,7 +369,9 @@
flex: 1;
}
+
/* Additional utilities */
+
.background-color-f0f0f0 {
background-color: #f0f0f0;
}
@@ -364,7 +384,9 @@
border-bottom: 1px solid #d9d9d9;
}
+
/* Markdown component utilities */
+
.markdown-margin-16 {
margin: 16px 0;
}
@@ -393,7 +415,9 @@
font-size: 85%;
}
+
/* Additional spacing utilities */
+
.margin-top-20 {
margin-top: 20px;
}
@@ -406,7 +430,9 @@
border-radius: 5px;
}
+
/* Additional layout utilities for Navbar */
+
.width_500 {
width: 500px;
}
@@ -419,16 +445,16 @@
margin-right: 40px;
}
+
/* Additional margin utilities */
-.margin-top-30 {
- margin-top: 30px;
-}
.margin-top-20 {
margin-top: 20px;
}
+
/* Text utilities */
+
.text-center {
text-align: center;
}
@@ -494,10 +520,6 @@
margin-bottom: 15px;
}
-.margin-top-30 {
- margin-top: 30px;
-}
-
.border-bottom-blue {
border-bottom: 2px solid #3498db;
}
@@ -546,7 +568,9 @@
border-radius: 0 4px 4px 0;
}
+
/* Flex utilities */
+
.display-flex {
display: flex;
}
@@ -581,7 +605,9 @@
margin-left: 600px;
}
+
/* Reusable utility classes */
+
.right-align-container {
margin-left: auto;
display: flex;
diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx
index 88a5c16..422fe2e 100644
--- a/src/pages/Home/Home.tsx
+++ b/src/pages/Home/Home.tsx
@@ -18,10 +18,10 @@ import { Flex, Radio } from "antd";
import { Card } from "antd";
import { Image, Button } from "antd";
-import { EyeOutlined } from '@ant-design/icons';
+import { EyeOutlined } from "@ant-design/icons";
const { Meta } = Card;
-import { useNavigate } from 'react-router-dom';
+import { useNavigate } from "react-router-dom";
const Home: React.FC = () => {
const [value, setValue] = useState(1);
@@ -37,49 +37,51 @@ const Home: React.FC = () => {
key: "all",
label: "全部分类",
children: (
-
- 综合排序
-
- ),
- },
- {
- value: 2,
- className: "option-2",
- label: (
-
- 最多使用
-
- ),
- },
- {
- value: 3,
- className: "option-3",
- label: (
-
- 最新发布
-
- ),
- },
- {
- value: 4,
- className: "option-4",
- label: (
-
- 最多收藏
-
- ),
- },
- ]}
- />
+
+
+ 综合排序
+
+ ),
+ },
+ {
+ value: 2,
+ className: "option-2",
+ label: (
+
+ 最多使用
+
+ ),
+ },
+ {
+ value: 3,
+ className: "option-3",
+ label: (
+
+ 最新发布
+
+ ),
+ },
+ {
+ value: 4,
+ className: "option-4",
+ label: (
+
+ 最多收藏
+
+ ),
+ },
+ ]}
+ />
+
),
},
{
@@ -98,12 +100,6 @@ const Home: React.FC = () => {
children: "Content of Tab Pane 3",
},
];
-
- const baseStyle: React.CSSProperties = {
- width: "25%",
- height: 350,
- };
-
return (
<>
@@ -118,9 +114,8 @@ const Home: React.FC = () => {
defaultActiveKey="all"
items={items}
centered
- className="margin-30 margin-left-100 margin-right-100"
+ className="margin-30 margin-left-100 margin-right-100 margin-bottom-30"
/>
-
{Array.from({ length: 5 }).map((_, rowIndex) => (
{
className="margin-bottom-30 margin-left-50 margin-right-50"
justify="center"
wrap="nowrap"
- gap="middle"
+ gap="large"
>
{Array.from({ length: 5 }).map((_, colIndex) => {
const cardIndex = rowIndex * 5 + colIndex;
@@ -143,24 +138,30 @@ const Home: React.FC = () => {
hoverable
className="card-width-300"
cover={
-
- )} description="详细介绍..........." />
+
+ 功能描述
+
+
+ 1111
+
+
+ }
+ description="详细介绍..........."
+ />
{
gap="middle"
className="margin-top-20"
>
-
-
+
+
diff --git a/src/pages/Profile/Profile.tsx b/src/pages/Profile/Profile.tsx
index 7b169da..fc5783a 100644
--- a/src/pages/Profile/Profile.tsx
+++ b/src/pages/Profile/Profile.tsx
@@ -6,7 +6,7 @@ import Navbar from "@/components/Navbar/Navbar";
import React, { useState } from "react";
-import { Card } from "antd";
+import { useNavigate } from "react-router-dom";
import {
AppstoreOutlined,
@@ -16,39 +16,110 @@ import {
MenuFoldOutlined,
MenuUnfoldOutlined,
PieChartOutlined,
+ EditOutlined,
} from "@ant-design/icons";
import type { MenuProps } from "antd";
-import { Button, Menu, Flex, Layout, Typography } from "antd";
-import { Float } from "@chakra-ui/react";
+import { Button, Menu, Flex, Typography, Steps, Input } from "antd";
+import { Select, Space, Result } from "antd";
+import { useSelector } from "react-redux";
+
+import { Tooltip } from "antd";
+
+import {
+ selectUser,
+ selectAccessToken,
+ selectRefreshToken,
+ selectTokenType,
+ selectExpiresAtTimestamp,
+} from "@/features/login";
+
+interface MenuInfo {
+ key: string;
+ children?: MenuInfo[];
+}
const { Text } = Typography;
-const Home: React.FC = () => {
- const [collapsed, setCollapsed] = useState(false);
+const content = "This is a content.";
+const Home: React.FC = () => {
+ const user = useSelector(selectUser);
+ const accessToken = useSelector(selectAccessToken);
+ const refreshToken = useSelector(selectRefreshToken);
+ const tokenType = useSelector(selectTokenType);
+ const expiresAtTimestamp = useSelector(selectExpiresAtTimestamp);
+
+ const [hoveredKey, setHoveredKey] = useState("");
+
+ const [collapsed, setCollapsed] = useState(false);
+ const [MenuKey, setMenuKey] = useState("user_info");
+ const [change_password_step, setChange_password_step] = useState(0);
+ const navigation = useNavigate();
+ const [stepItems, setStepItems] = useState([
+ {
+ title: "发送邮件或者手机号验证码",
+ content: "验证完成或者未完成或者正在验证码",
+ },
+ {
+ title: "修改密码",
+ content: "验证完成或者未完成或者正在验证码",
+ },
+ {
+ title: "修改完成",
+ content: "验证完成或者未完成或者正在验证码",
+ },
+ ]);
+ const options = [
+ {
+ value: "邮箱",
+ label: "邮箱",
+ },
+ {
+ value: "手机号",
+ label: "手机号",
+ },
+ ];
const toggleCollapsed = () => {
setCollapsed(!collapsed);
};
+ const handleMenuClick = (info: MenuInfo) => {
+ setMenuKey(info.key);
+ console.log(MenuKey);
+ console.log("菜单点击", { key: info.key });
+ };
+
type MenuItem = Required["items"][number];
const items: MenuItem[] = [
- { key: "1", icon: , label: "用户信息" },
- { key: "2", icon: , label: "个人安全" },
- { key: "3", icon: , label: "修改密码" },
{
- key: "sub1",
+ key: "user_info",
+ icon: ,
+ label: "用户信息",
+ },
+ {
+ key: "personal_security",
+ icon: ,
+ label: "安全设置",
+ },
+ {
+ key: "change_password",
+ icon: ,
+ label: "修改密码",
+ },
+ {
+ key: "blacklist",
label: "黑名单管理",
icon: ,
},
{
- key: "sub2",
+ key: "points_management",
label: "积分管理",
icon: ,
},
{
- key: "sub3",
+ key: "recent_login",
label: "最近登录记录",
icon: ,
},
@@ -75,10 +146,11 @@ const Home: React.FC = () => {
>
@@ -103,7 +175,267 @@ const Home: React.FC = () => {
- 个人信息等,主内容
+ {MenuKey === "user_info" && (
+
+
+ 基本信息
+
+
+
+
+ 昵称:
+
+ {user?.username}
+
+
+
+ 用户id:
+
+ {user?.id}
+
+
+
+ 性别:
+
+ 男
+
+
+ setHoveredKey("bio")}
+ onMouseLeave={() => setHoveredKey("")}
+ >
+
+ 简介:
+
+ 未填写
+
+
+ {hoveredKey === "bio" && (
+
+ )}
+
+
+
+
+ 所在地区:
+
+ 未填写
+
+
+
+ 邮箱:
+
+ {user?.email}
+
+
+
+ 手机号:
+
+ 未绑定
+
+
+
+ 账号创建时间:
+
+ {user?.date_joined}
+
+
+ )}
+ {MenuKey === "personal_security" && (
+
+
+ 安全设置
+
+
+ )}
+ {MenuKey === "change_password" && (
+ <>
+
+
+ {change_password_step === 0 && (
+
+
+
+
+
+
+
+ {/* 当验证码发送成功时,按钮可用,否则禁用 */}
+
+
+
+ )}
+ {change_password_step === 1 && (
+
+
+ 修改密码
+
+
+
+ 确认密码
+
+
+
+ 验证码
+
+
+
+
+
+
+
+ )}
+ {change_password_step === 2 && (
+ {
+ navigation("/");
+ }}
+ >
+ 回到首页
+ ,
+ ,
+ ]}
+ />
+ )}
+
+ >
+ )}
+ {MenuKey === "blacklist" && (
+
+
+ 黑名单管理
+
+
+ )}
+ {MenuKey === "profile" && (
+
+
+ 积分管理
+
+
+ )}
+ {MenuKey === "recent_login" && (
+
+
+ 最近登录记录
+
+
+ )}
>
diff --git a/tsconfig.json b/tsconfig.json
index 59f24a6..a7f043a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,10 +1,3 @@
-// {
-// "files": [],
-// "references": [
-// { "path": "./tsconfig.app.json" },
-// { "path": "./tsconfig.node.json" }
-// ]
-// }
{
"compilerOptions": {
"types": ["node"],
diff --git a/vite.config.ts b/vite.config.ts
index 4231c50..6fa25cb 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -5,6 +5,7 @@ import path from 'path';
// https://vite.dev/config/
export default defineConfig({
server: {
+ hmr: true,
host: '::',
allowedHosts: [
'.mymoyu.top', // 注意是点开头,不是星号
@@ -16,7 +17,7 @@ export default defineConfig({
plugins: [react()],
resolve: {
alias: {
- '@': path.resolve(__dirname, './src'), // 实际上使用了,但可能没被检测到
+ '@': path.resolve(__dirname, './src'),
},
},
})