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={ -
+
{`card-${cardIndex}`}
} > - - 功能描述 - 1111 -
- )} 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'), }, }, })