帖子分类
Change-Id: I17bafbfe3c1c8fd26c1e12499cb3c17cd1738e23
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..f15b3e1
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,15 @@
+server {
+ listen 80;
+ server_name localhost;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html;
+ try_files $uri $uri/ /index.html; # 关键:支持前端路由
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+}
\ No newline at end of file
diff --git a/src/api/auth.ts b/src/api/auth.ts
index bc6d413..cba34b3 100644
--- a/src/api/auth.ts
+++ b/src/api/auth.ts
@@ -1,3 +1,4 @@
export const postUserLogin= '/auth/login';
export const postUserRegister= '/auth/register';
-export const postVerifivateCode="/auth/send_verification_code";
\ No newline at end of file
+export const postVerifivateCode="/auth/send_verification_code";
+export const getRefreshToken="/auth/refresh_token";
\ No newline at end of file
diff --git a/src/api/comment.ts b/src/api/comment.ts
new file mode 100644
index 0000000..1809ad2
--- /dev/null
+++ b/src/api/comment.ts
@@ -0,0 +1 @@
+export const getPostComments = '/api/comments/post'
\ No newline at end of file
diff --git a/src/api/post.ts b/src/api/post.ts
index 99dc8fa..b211bdd 100644
--- a/src/api/post.ts
+++ b/src/api/post.ts
@@ -1 +1,6 @@
-export const hotPosts='/post/hot'
\ No newline at end of file
+export const getHotPosts='/post/recommended'
+export const getLikePosts='/post/recommended-by-tags'
+export const getPosts='/post/search'
+export const getPostDetail='/post'
+export const getPostComments='/comments/post'
+export const unknownAPI='/api/unknown'
\ No newline at end of file
diff --git a/src/api/user.ts b/src/api/user.ts
index 30fbfff..ecea4d8 100644
--- a/src/api/user.ts
+++ b/src/api/user.ts
@@ -1 +1,3 @@
-export const getUserInfo="/user"
\ No newline at end of file
+export const getUserInfo="/user"
+
+export const postFollowUser="/user/follow"
\ No newline at end of file
diff --git a/src/components/navbar/navbar.tsx b/src/components/navbar/navbar.tsx
index a6c1fe7..a305521 100644
--- a/src/components/navbar/navbar.tsx
+++ b/src/components/navbar/navbar.tsx
@@ -1,4 +1,4 @@
-import React, { use } from "react";
+import React, { use, useEffect } from "react";
import Icon, {
HomeOutlined,
} from "@ant-design/icons";
@@ -8,12 +8,12 @@
import { Menu } from 'antd';
import { useState } from "react";
import style from './navbar.module.css'
-
+import { set } from "lodash";
+import { useNavigate } from "react-router";
type CustomIconComponentProps = GetProps<typeof Icon>;
type MenuItem = Required<MenuProps>['items'][number];
const web_base_url = process.env.WEB_BASE_URL || 'http://localhost:3000';
-
const VideoSvg = () => (
<svg width="1em" height="1em" fill="currentColor" xmlns="http://www.w3.org/2000/svg" p-id="7331" viewBox="0 0 1024 1024">
<title>video icon</title>
@@ -63,7 +63,6 @@
<Icon component={ChatSvg} {...props} />
);
-
const items: MenuItem[] = [
{
key: 'home',
@@ -78,7 +77,7 @@
key: 'video',
icon: <VideoIcon />,
label: (
- <a href={{web_base_url}+'/posts?type=video'}>
+ <a href={'/posts?type=video'}>
影视
</a>
),
@@ -87,7 +86,7 @@
key: 'music',
icon: <MusicIcon />,
label: (
- <a href={{web_base_url}+'/posts?type=music'}>
+ <a href={'/posts?type=music'}>
音乐
</a>
),
@@ -96,7 +95,7 @@
key: 'game',
icon: <GameIcon />,
label: (
- <a href={{web_base_url}+'/posts?type=game'}>
+ <a href={'/posts?type=game'}>
游戏
</a>
),
@@ -105,7 +104,7 @@
key: 'software',
icon: <SoftwareIcon />,
label: (
- <a href={{web_base_url}+'/posts?type=software'}>
+ <a href={'/posts?type=software'}>
软件
</a>
),
@@ -114,20 +113,27 @@
key: 'chat',
icon: <ChatIcon />,
label: (
- <a href={{web_base_url}+'/posts?type=chat'}>
+ <a href={'/posts?type=chat'}>
聊天
</a>
),
},
];
+interface NavbarProps {
+ current?: string;
+}
-
-const Navbar: React.FC = () => {
+const Navbar: React.FC<NavbarProps> = (props) => {
const [current, setCurrent] = useState('home');
-
+ useEffect(() => {
+ console.log('current:', props.current);
+ if (props.current) {
+ setCurrent(props.current);
+ }
+ },[props.current]);
const onClick: MenuProps['onClick'] = (e) => {
console.log('click ', e);
setCurrent(e.key);
diff --git a/src/components/postsPanel/postsPanel.module.css b/src/components/postsPanel/postsPanel.module.css
index dc10683..b4b75ce 100644
--- a/src/components/postsPanel/postsPanel.module.css
+++ b/src/components/postsPanel/postsPanel.module.css
@@ -2,6 +2,7 @@
background-color: var(--card-bg);
height:100%;
width:100%;
+ padding:0px 5px;
}
.header{
@@ -27,7 +28,16 @@
.content .item{
display:flex;
justify-content:space-between;
+ align-items:center;
+ padding-top:10px;
+ padding-bottom:10px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04); /* 添加边框阴影 */
+ border:1px solid rgba(0,0,0,0.04);
+}
+.content .item:hover {
+ background:var(--select-bg-color);
+ cursor:pointer
}
.content .item .text{
width:70%;
diff --git a/src/components/postsPanel/postsPanel.tsx b/src/components/postsPanel/postsPanel.tsx
index e4f87c0..2916753 100644
--- a/src/components/postsPanel/postsPanel.tsx
+++ b/src/components/postsPanel/postsPanel.tsx
@@ -2,6 +2,7 @@
import React, { useCallback } from 'react';
import request from '@/utils/request'
import style from './postsPanel.module.css'
+import { useNavigate } from 'react-router';
interface panelProps{
@@ -11,23 +12,24 @@
}
const PostsPanel:React.FC<panelProps> = (props) => {
- const fenchData = useCallback(() => request.get(props.url), [props.url])
+ const nav = useNavigate();
+ const fenchData = useCallback(() => request.get(`${props.url}?page=1&size=5`), [props.url])
const {data} = useApi(fenchData, true);
-
-
-
+ const handlePostCheck =(postId:string) =>{
+ nav('/postDetail?postId=' + postId);
+ }
return (
<div className={style.panel}>
<div className={style.header}>
<span className={style.title}>{props.name}</span>
<span className={style.more}>更多</span>
</div>
- <div className={style.content}>
+ <div className={style.content} >
{data && data.length > 0 ?
- data?.map((item: { title: string; date: string }, index: number) => (
- <div key={index} className={style.item}>
- <span className={style.text}>{item.title}</span>
- <span>{item.date}</span>
+ data?.map((item: {postId:string, postTitle: string; createdAt: string }, index: number) => (
+ <div key={index} className={style.item} onClick={()=> handlePostCheck(item.postId)} >
+ <span className={style.text}>{item.postTitle}</span>
+ <span>{item.createdAt}</span>
</div>
)) :(
<div>未查询到相关记录</div>
diff --git a/src/components/selfStatus/selfStatus.tsx b/src/components/selfStatus/selfStatus.tsx
index a5156f4..834e841 100644
--- a/src/components/selfStatus/selfStatus.tsx
+++ b/src/components/selfStatus/selfStatus.tsx
@@ -1,7 +1,10 @@
-import React from "react";
+import React, { useEffect } from "react";
import { useAppSelector } from "../../hooks/store";
import style from "./style.module.css"
-
+import { useApi } from "@/hooks/request";
+import request from "@/utils/request";
+import { getUserInfo } from "@/api/user";
+import { useAppDispatch } from "@/hooks/store";
interface SelfStatusProps {
className?: string;
}
@@ -13,11 +16,25 @@
const downloadTraffic = useAppSelector(state => state.user.downloadTraffic);
const downloadPoints = useAppSelector(state => state.user.downloadPoints);
const avatar = useAppSelector(state => state.user.avatar);
+ const dispatch = useAppDispatch();
+ const { data, refresh } = useApi(() => request.get(getUserInfo), false);
+ useEffect(() => {
+ if (avatar.length === 0) {
+ refresh(); // 触发 API 请求
+ }
+ }, [avatar, refresh]);
+
+ useEffect(() => {
+ if (data) {
+ dispatch({ type: "user/getUserInfo", payload: data });
+ }
+ }, [data, dispatch]);
return (
<div className={style.container}>
<div className={style.left}>
- <img className={style.avatar} src={avatar} alt="User Avatar" />
+ {avatar && avatar.length > 0 ? (
+ <img className={style.avatar} src={avatar} alt="User Avatar" />):null}
</div>
<div className={style.right}>
<div className={style.info}>
diff --git a/src/components/selfStatus/style.module.css b/src/components/selfStatus/style.module.css
index 8ed86a7..858e88a 100644
--- a/src/components/selfStatus/style.module.css
+++ b/src/components/selfStatus/style.module.css
@@ -4,7 +4,6 @@
align-items: flex-start;
justify-content: space-between;
padding: 20px;
- border: 1px solid #ccc;
border-radius: 10px;
background-color: var(--card-bg);
width: 100%;
diff --git a/src/global.css b/src/global.css
index b49490a..9c38334 100644
--- a/src/global.css
+++ b/src/global.css
@@ -28,6 +28,7 @@
--text-color: #000000;
--card-bg: #ffffff;
--border-color: #e0e0e0;
+ --select-bg-color: #b3d8fd;
}
body.dark {
@@ -35,5 +36,6 @@
--text-color: #f1f1f1;
--card-bg: #1e1e1e;
--border-color: #444444;
+ --select-bg-color:#3a466b;
}
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
index 69441df..9205b1b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,10 +1,13 @@
-import React from "react";
+import React, { use } from "react";
import {createRoot} from "react-dom/client"
import { Provider } from "react-redux";
import router from "./route";
import store from "./store/index";
import { RouterProvider } from "react-router";
import './global.css';
+import { setupMock } from "./mock/index";
+import { useEffect } from "react";
+import { checkAndRefreshToken } from "./utils/jwt";
if(localStorage.getItem("theme") === null) {
localStorage.setItem("theme", "light");
@@ -13,7 +16,9 @@
document.body.className=localStorage.getItem("theme")!;
}
+setupMock()
const root = createRoot(document.getElementById('root')!)
+
root.render(
<Provider store={store}>
<RouterProvider router={router}/>
diff --git a/src/mock/auth.js b/src/mock/auth.js
index 3cc837b..272d8b7 100644
--- a/src/mock/auth.js
+++ b/src/mock/auth.js
@@ -1,16 +1,63 @@
import Mock from 'mockjs';
import MockAdapter from 'axios-mock-adapter';
-import {postUserLogin} from '@/api/auth'; // Import the API endpoint
+import {postUserLogin, getRefreshToken} from '@/api/auth'; // Import the API endpoint
+
+
+function generateToken(userId, role) {
+ const exp = Math.floor(Date.now() / 1000) + 60 * 60;
+
+ // 生成 JWT Header
+ const header = {
+ alg: 'HS256', // 签名算法
+ typ: 'JWT', // 类型
+ };
+
+ // 生成 JWT Payload
+ const payload = {
+ userId,
+ role,
+ exp,
+ };
+
+ // Base64 编码 Header 和 Payload
+ const base64Header = btoa(JSON.stringify(header));
+ const base64Payload = btoa(JSON.stringify(payload));
+
+ // 模拟 Signature(实际应使用密钥进行 HMAC-SHA256 签名)
+ const signature = btoa('mock-signature'); // 简单模拟签名
+
+ // 拼接 JWT Token
+ const token = `${base64Header}.${base64Payload}.${signature}`;
+ return token;
+}
/**
* 设置用户相关的 Mock 接口
* @param {MockAdapter} mock
*/
-export function setupAuthMock(mock){
- mock.onPost(postUserLogin).reply(config => {
- let data = Mock.mock({
- "token": '11111111111111111',
- });
- return [200, data];
+export function setupAuthMock(mock) {
+ mock.onPost(postUserLogin).reply(config => {
+ const data = JSON.parse(config.data);
+ if(data.email === 'admin@1' && data.password === '123456'){
+ // 模拟用户登录数据
+ const token = generateToken(1, 'admin');
+
+ // 返回模拟的 Token
+ let data = Mock.mock({
+ token,
});
-}
+ return [200, data];
+ }else{
+ return [401, {message: '用户名或密码错误'}];
+ }
+ });
+ mock.onGet(getRefreshToken).reply(config => {
+ // 模拟用户登录数据
+ const token = generateToken(1, 'admin'); // 这里的 1 和 'admin' 可以根据需要修改
+ // 返回模拟的 Token
+ let data = Mock.mock({
+ token,
+ });
+ return [200, data];
+ })
+}
diff --git a/src/mock/comment.d.ts b/src/mock/comment.d.ts
new file mode 100644
index 0000000..2ed6cca
--- /dev/null
+++ b/src/mock/comment.d.ts
@@ -0,0 +1,3 @@
+import type MockAdapter from 'axios-mock-adapter';
+
+export declare function setupCommentMock(mock: MockAdapter): void;
\ No newline at end of file
diff --git a/src/mock/comment.js b/src/mock/comment.js
new file mode 100644
index 0000000..695ccc4
--- /dev/null
+++ b/src/mock/comment.js
@@ -0,0 +1,34 @@
+import mockjs from "mockjs";
+import MockAdapter from "axios-mock-adapter";
+import { getPostComments} from "@/api/comment";
+
+export function setupCommentMock(mock) {
+ const getPostCommentsPattern = new RegExp(`^${getPostComments}/\\d+$`);
+ mock.onGet(getPostCommentsPattern).reply(config => {
+ let data = mockjs.mock({
+ [`list|5`]: [
+ {
+ "commentId|+1": 1, // 自增评论 ID
+ "content": "@cparagraph(1, 3)", // 随机生成 1-3 段评论内容
+ "createdAt": "@datetime('T')", // 随机生成时间戳
+ "parentCommentId": null, // 顶级评论的父评论 ID 为 null
+ "postId|1-100": 1, // 随机生成帖子 ID
+ "userId|1-100": 1, // 随机生成用户 ID
+ "replies|0-3": [ // 随机生成 0-3 条子评论
+ {
+ "commentId|+1": 100, // 子评论的 ID 从 100 开始自增
+ "content": "@cparagraph(1, 2)", // 随机生成 1-2 段子评论内容
+ "createdAt": "@datetime('T')", // 随机生成时间戳
+ "parentCommentId": "@increment(1)", // 父评论 ID
+ "postId|1-100": 1, // 随机生成帖子 ID
+ "userId|1-100": 1, // 随机生成用户 ID
+ "replies": [] // 子评论的子评论为空
+ }
+ ]
+ },
+ ],
+ });
+ return [200, data.list];
+ });
+
+}
\ No newline at end of file
diff --git a/src/mock/index.ts b/src/mock/index.ts
index fabb34e..4429216 100644
--- a/src/mock/index.ts
+++ b/src/mock/index.ts
@@ -3,6 +3,7 @@
import {setupAuthMock} from './auth'
import { setupUserMock } from './user';
import { setupPostMock } from './post';
+import { setupCommentMock } from './comment';
// 创建 Mock 实例
export const mock = new MockAdapter(instance, {
@@ -18,9 +19,8 @@
setupAuthMock(mock)
setupUserMock(mock)
setupPostMock(mock)
+ setupCommentMock(mock)
console.log('Mock 模块已加载')
}
-// 自动执行
-setupMock()
\ No newline at end of file
diff --git a/src/mock/post.js b/src/mock/post.js
index abdfc6a..63f4413 100644
--- a/src/mock/post.js
+++ b/src/mock/post.js
@@ -1,17 +1,83 @@
import Mock from 'mockjs';
import MockAdapter from 'axios-mock-adapter';
-import {hotPosts} from '@/api/post'
+import {getHotPosts, getLikePosts, getPosts, getPostDetail} from '@/api/post'
/**
* 设置用户相关的 Mock 接口
* @param {MockAdapter} mock
*/
export function setupPostMock(mock){
- mock.onGet(hotPosts).reply(config => {
- let data = Mock.mock([{
- 'title':'test title',
- 'date':'2025-4-20'
- }]);
+ const hotPostsPattern = new RegExp(`^${getHotPosts}(\\?page=\\d+&size=\\d+)?$`);
+ const LikePostsPattern = new RegExp(`^${getLikePosts}(\\?page=\\d+&size=\\d+)?$`);
+ const searchPostsPattern = new RegExp(`^${getPosts}\\?((keyword=[^&]+&?)|(tags=[^&]+&?)|(author=[^&]+&?)|)+(page=\\d+)&(pageSize=\\d+)$`);
+ mock.onGet(hotPostsPattern).reply(config => {
+ const urlParams = new URLSearchParams(config.url.split('?')[1]);
+ const size = parseInt(urlParams.get('size')) || 10;
+ let data = Mock.mock({
+ [`list|${size}`]: [
+ {
+ 'postId|+1': 1,
+ 'postTitle': '@ctitle(5, 10)',
+ 'postContent': '@cparagraph(1, 3)',
+ 'author': '@cname()',
+ 'createdAt': '@date("yyyy-MM-dd")',
+ 'viewCount|1-100': 1,
+ },
+ ],
+ });
+ return [200, data.list];
+ });
+ mock.onGet(LikePostsPattern).reply(config => {
+ const urlParams = new URLSearchParams(config.url.split('?')[1]);
+ const size = parseInt(urlParams.get('size')) || 10;
+ let data = Mock.mock({
+ [`list|${size}`]: [
+ {
+ 'postId|+1': 1,
+ 'postTitle': '@ctitle(5, 10)',
+ 'postContent': '@cparagraph(1, 3)',
+ 'author': '@cname()',
+ 'createdAt': '@date("yyyy-MM-dd")',
+ 'viewCount|1-100': 1,
+ },
+ ],
+ });
+ return [200, data.list];
+ });
+ mock.onGet(searchPostsPattern).reply(config => {
+ const urlParams = new URLSearchParams(config.url.split('?')[1]);
+ const tags = urlParams.get('tags')?.split(',') || []; // 将 tags 参数解析为数组
+ const page = parseInt(urlParams.get('page')) || 1; // 默认 page 为 1
+ const size = parseInt(urlParams.get('size')) || 10; // 默认 size 为 10
+ let data = Mock.mock({
+ [`list|${size}`]: [
+ {
+ 'postId|+1': 1,
+ 'postTitle': '@ctitle(5, 10)',
+ 'postContent': '@cparagraph(1, 3)',
+ 'author': '@cname()',
+ 'createdAt': '@date("yyyy-MM-dd")',
+ 'viewCount|1-100': 1,
+ },
+ ],
+ });
+ return [200, data.list];
+ });
+
+
+ const getPostDetailPattern = new RegExp(`^${getPostDetail}/[0-9]+$`);
+ mock.onGet(getPostDetailPattern).reply(config => {
+ const postId = config.url.split('/').pop();
+ let data = Mock.mock({
+ 'postId': postId,
+ 'postTitle': '@ctitle(5, 10)',
+ 'postContent': '@cparagraph(1, 3)',
+ 'author': '@cname()',
+ 'createdAt': '@date("yyyy-MM-dd")',
+ 'viewCount|1-100': 1,
+ });
return [200, data];
});
+
+
}
diff --git a/src/route/index.tsx b/src/route/index.tsx
index c56ada3..93e6cf9 100644
--- a/src/route/index.tsx
+++ b/src/route/index.tsx
@@ -6,6 +6,8 @@
import React from 'react'
import Forum from '../views/forum'
import { RootState } from '@/store'
+import PostList from '../views/postList/postList'
+import PostDetail from '../views/postDetail/postDetail'
const router = createBrowserRouter([
{
@@ -24,6 +26,14 @@
element:<Forum/>
},
+ {
+ path: '/posts',
+ element: <PostList/>
+ },
+ {
+ path: '/postsDetail',
+ element: <PostDetail/>
+ }
]
},
]
diff --git a/src/store/userReducer.ts b/src/store/userReducer.ts
index bbed95e..b677263 100644
--- a/src/store/userReducer.ts
+++ b/src/store/userReducer.ts
@@ -1,4 +1,5 @@
import { createSlice } from '@reduxjs/toolkit';
+import { isTokenExpired } from '@/utils/jwt';
interface UserState {
userId: string;
@@ -15,7 +16,7 @@
userId: '',
userName: '',
role: '',
- isLogin: false,
+ isLogin: localStorage.getItem('token')&& isTokenExpired(localStorage.getItem('token') as string) ==='0' ? true : false,
uploadTraffic: 0,
downloadTraffic: 0,
downloadPoints: 0,
@@ -39,7 +40,6 @@
state.downloadTraffic = action.payload.downloadTraffic;
state.downloadPoints = action.payload.downloadPoints;
state.avatar = action.payload.avatar;
- console.log(state);
},
logout: (state) => {
state.userId = '';
diff --git a/src/utils/axios.ts b/src/utils/axios.ts
index 1eaad87..273a9c4 100644
--- a/src/utils/axios.ts
+++ b/src/utils/axios.ts
@@ -12,6 +12,7 @@
// 请求拦截器
instance.interceptors.request.use(
(config) => {
+ console.log('Request Config:', config)
// 添加认证 token
const token = localStorage.getItem('token')
if (token) {
@@ -27,7 +28,6 @@
// 响应拦截器
instance.interceptors.response.use(
(response) => {
- console.log('Response:', response)
// 统一处理响应数据格式
if (response.status === 200) {
return response.data
diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts
new file mode 100644
index 0000000..42308dc
--- /dev/null
+++ b/src/utils/jwt.ts
@@ -0,0 +1,61 @@
+import { getRefreshToken } from '@/api/auth';
+import axios from 'axios';
+
+function parseJwt(token: string) {
+ console.log('Parsing JWT token:', token);
+ try {
+ const base64Url = token.split('.')[1];
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+ const jsonPayload = decodeURIComponent(
+ atob(base64)
+ .split('')
+ .map(c => `%${('00' + c.charCodeAt(0).toString(16)).slice(-2)}`)
+ .join('')
+ );
+ return JSON.parse(jsonPayload);
+ } catch (error) {
+ console.error('Invalid JWT token:', error);
+ return null;
+ }
+}
+
+/**
+ * 检查token是否过期
+ * @returns {string} -1: token无效 0: token未过期 1: token已过期
+**/
+export function isTokenExpired(token: string): string {
+ if (token === null || token === undefined) {
+ return '-1'; // 如果没有token,认为token无效
+ }
+ const decoded = parseJwt(token);
+ if (!decoded || !decoded.exp) {
+ return '-1'; // 如果解析失败或没有 `exp` 字段,认为token无效
+ }
+ const currentTime = Math.floor(Date.now() / 1000); // 当前时间(秒)
+ const bufferTime = 60 * 10; // 过期前的缓冲时间(秒)
+ return decoded.exp < currentTime ?decoded.exp < currentTime - bufferTime ? '-1' : '1' :"0";
+}
+
+async function refreshToken() {
+ try {
+ const response = await axios.post(getRefreshToken, {
+ token: localStorage.getItem('token'),
+ });
+ const newToken = response.data.token;
+ localStorage.setItem('token', newToken);
+ return newToken;
+ } catch (error) {
+ console.error('Failed to refresh token:', error);
+ return null;
+ }
+}
+
+export async function checkAndRefreshToken() {
+ const token = localStorage.getItem('token');
+ if (token && isTokenExpired(token)=='1') {
+ const newToken = await refreshToken();
+ if (!newToken) {
+ localStorage.removeItem('token');
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/views/forum/index.tsx b/src/views/forum/index.tsx
index 345f880..83d791f 100644
--- a/src/views/forum/index.tsx
+++ b/src/views/forum/index.tsx
@@ -5,7 +5,7 @@
import style from "./index.module.css";
import Navbar from "@/components/navbar/navbar";
import PostsPanel from "@/components/postsPanel/postsPanel";
-import { hotPosts } from "@/api/post";
+import { getHotPosts, getLikePosts} from "@/api/post";
import { Carousel } from 'antd';
import ad1 from '&/assets/ad1.png'
import ad2 from '&/assets/ad2.png'
@@ -42,7 +42,7 @@
</Carousel>
</div>
<div className={style.hotPosts}>
- <PostsPanel name='热门种子' url={hotPosts} limit={5}/>
+ <PostsPanel name='热门种子' url={getHotPosts} limit={5}/>
</div>
</div>
</div>
@@ -54,13 +54,13 @@
</div>
<div className={style.down}>
<div className={style.newPost}>
- <PostsPanel name='最新发布' url={hotPosts} limit={5}/>
+ <PostsPanel name='最新发布' url={getHotPosts} limit={5}/>
</div>
<div className={style.likePost}>
- <PostsPanel name='猜你喜欢' url={hotPosts} limit={5}/>
+ <PostsPanel name='猜你喜欢' url={getLikePosts} limit={5}/>
</div>
<div className={style.forsalePost}>
- <PostsPanel name='促销种子' url={hotPosts} limit={5}/>
+ <PostsPanel name='促销种子' url={getHotPosts} limit={5}/>
</div>
</div>
</div>
diff --git a/src/views/frame/frame.tsx b/src/views/frame/frame.tsx
index c0c2e0e..f41e53f 100644
--- a/src/views/frame/frame.tsx
+++ b/src/views/frame/frame.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { use } from "react";
import { Outlet } from "react-router";
import { useEffect, useState } from "react";
import {
@@ -12,8 +12,13 @@
import logo from "&/assets/logo.png";
import { useAppDispatch } from "@/hooks/store";
import { useSelector } from "react-redux";
+
+import { checkAndRefreshToken } from "@/utils/jwt";
const Frame:React.FC = () => {
+ useEffect(() => {
+ checkAndRefreshToken();
+ }, []);
const dispatch = useAppDispatch();
const showSearch = useSelector((state: any) => state.setting.showSearch);
diff --git a/src/views/login/login.tsx b/src/views/login/login.tsx
index bd429c7..562f178 100644
--- a/src/views/login/login.tsx
+++ b/src/views/login/login.tsx
@@ -11,38 +11,39 @@
import logo from '&/assets/logo.png';
import { getUserInfo } from '@/api/user';
import debounce from 'lodash/debounce';
+import {message} from 'antd';
+
const Login: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const dispatch = useAppDispatch();
- const { refresh: postUserLoginRefresh } = useApi(() => request.post(postUserLogin, {}), false);
+ const { refresh: postUserLoginRefresh } = useApi(() => request.post(postUserLogin, {email, password}), false);
const { refresh: getUserInfoRefresh } = useApi(() => request.get(getUserInfo), false);
+ const [messageApi, contextHolder] = message.useMessage();
const nav = useNavigate();
+ const showErrorMessage = async (message: string) => {
+ messageApi.error(message);
+ };
const handleLogin = debounce(async () => {
try {
- const res =await postUserLoginRefresh();
- if (res==null ||(res as any).error) {
- alert('Login failed. Please check your credentials.');
- return;
+ const res = await postUserLoginRefresh();
+ console.log(res);
+ if (res == null || (res as any).error) {
+ throw new Error('登录失败');
}
dispatch({ type: "user/login", payload: res });
-
+
const userInfo = await getUserInfoRefresh();
- if (userInfo==null || (userInfo as any).error) {
- alert('Failed to fetch user information.');
- return;
+ if (userInfo == null || (userInfo as any).error) {
+ throw new Error('获取用户信息失败');
}
dispatch({ type: "user/getUserInfo", payload: userInfo });
nav('/');
} catch (error) {
- alert('An unexpected error occurred. Please try again later.');
- if (error instanceof Error) {
- console.error(error.message); // 明确访问 message 属性
- } else {
- console.error('Unknown error occurred');
- }
+ // 将错误信息传递给一个异步函数
+ showErrorMessage('登录失败,请检查您的用户名和密码');
}
}, 1000) as () => void;
@@ -51,6 +52,7 @@
}
return (
<div className={style.form}>
+ {contextHolder}
<img className={style.logo} src={logo} alt="logo" onClick={handleLogoClick}></img>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className={style.email} placeholder="Enter your email" />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className={style.password} placeholder="Enter your password" />
diff --git a/src/views/postDetail/postDetail.module.css b/src/views/postDetail/postDetail.module.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/views/postDetail/postDetail.module.css
diff --git a/src/views/postDetail/postDetail.tsx b/src/views/postDetail/postDetail.tsx
new file mode 100644
index 0000000..a40fb68
--- /dev/null
+++ b/src/views/postDetail/postDetail.tsx
@@ -0,0 +1,114 @@
+import React, { useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import styles from './PostDetail.module.css';
+import { Card, List, Typography, Button, Input, Spin, Empty } from 'antd';
+type CommentProps = {
+ children?: React.ReactNode;
+};
+import { getPostDetail } from '@/api/post';
+import { getPostComments } from '@/api/comment';
+import { useSearchParams } from 'react-router-dom';
+import request from '@/utils/request';
+import { useApi } from '@/hooks/request';
+import Navbar from '@/components/navbar/navbar';
+
+const { Title, Text, Paragraph } = Typography;
+const { TextArea } = Input;
+
+export interface PostResponse {
+ createdAt?: number;
+ hotScore?: number;
+ lastCalculated?: number;
+ postContent?: string;
+ postId?: number;
+ postTitle?: string;
+ postType?: string;
+ userId?: number;
+ viewCount?: number;
+ [property: string]: any;
+}
+
+export interface CommentResponse {
+ commentId?: number;
+ content?: string;
+ createdAt?: number;
+ parentCommentId?: number | null;
+ postId?: number;
+ replies?: CommentResponse[];
+ userId?: number;
+ [property: string]: any;
+}
+
+const PostDetail: React.FC = () => {
+ const [searchParams] = useSearchParams();
+ const postId = searchParams.get('postId');
+ const { refresh: getPostDetailRefresh } = useApi(() => request.get(getPostDetail + `/${postId}`), false);
+ const { refresh: getPostCommentsRefresh } = useApi(() => request.get(getPostComments + `/${postId}`), false);
+ const [post, setPost] = useState<PostResponse | null>(null);
+ const [comments, setComments] = useState<CommentResponse[]>([]);
+ const [newComment, setNewComment] = useState<string>('');
+ const [loading, setLoading] = useState<boolean>(true);
+
+ useEffect(() => {
+ console.log('postId', postId);
+ if (!postId) return;
+ const fetchData = async () => {
+ setLoading(true);
+ const res = await getPostDetailRefresh();
+ if (res == null || (res as any).error) {
+ setLoading(false);
+ return;
+ }
+ setPost(res as PostResponse);
+ await getPostCommentsRefresh();
+ setComments(res as CommentResponse[]);
+ setLoading(false);
+ };
+ fetchData();
+ }, [postId]);
+
+ if (loading) return <div className={styles.center}><Spin /></div>;
+ if (!post) return <div className={styles.center}><Empty description="未找到帖子" /></div>;
+
+ return (
+ <div className={styles.container}>
+ <div className={styles.nav}>
+ <Navbar current={post.postType} />
+ </div>
+ <div className={styles.content}>
+ <div className={styles.postDetail}>
+
+ </div >
+ <Card title={post.postTitle} className={styles.card}>
+ <Paragraph>{post.postContent}</Paragraph>
+ <div className={styles.actions}>
+ <Button type="primary" onClick={() => setNewComment('')}>评论</Button>
+ </div>
+ </Card>
+
+ <List
+ className={styles.commentList}
+ header={<Title level={4}>评论区</Title>}
+ dataSource={comments}
+ renderItem={(item) => (
+ <List.Item key={item.commentId}>
+ <List.Item.Meta
+ title={<Text strong>{item.userId}</Text>}
+ description={<Text>{item.content}</Text>}
+ />
+ </List.Item>
+ )}
+ />
+
+ <TextArea
+ rows={4}
+ value={newComment}
+ onChange={(e) => setNewComment(e.target.value)}
+ placeholder="写下你的评论..."
+ />
+ </div>
+ </div>
+ );
+};
+
+export default PostDetail;
\ No newline at end of file
diff --git a/src/views/postDetail/posterInfo.tsx b/src/views/postDetail/posterInfo.tsx
new file mode 100644
index 0000000..8fe381c
--- /dev/null
+++ b/src/views/postDetail/posterInfo.tsx
@@ -0,0 +1,37 @@
+import { PositionType } from 'antd/es/image/style';
+import request from '@/utils/request';
+import { useApi } from '@/hooks/request';
+import React from 'react';
+import { useNavigate } from 'react-router';
+import { postFollowUser } from '@/api/user';
+interface PosterInfoProps {
+ userId: number;
+ userName: string;
+ role:string;
+ avatar: string;
+}
+
+const PosterInfo: React.FC<PosterInfoProps> = (prop:PosterInfoProps) => {
+ const nav = useNavigate();
+ const { refresh } = useApi(()=>request.post(postFollowUser+`userId=${prop.userId}`), false);
+ const handleClick = () => {
+ nav(`/homepage?userId=${prop.userId}`);
+ }
+
+ const handleFollow = () => {
+ refresh()
+ }
+ const { userId, userName, role, avatar } = prop;
+ return (
+ <>
+ <div className="poster-info" onClick={handleClick}>
+ <img src={avatar}></img>
+ <p>{userName}</p>
+ <p>{role}</p>
+ </div>
+ <button className="poster-info-button" />关注
+
+ </>
+
+ );
+}
\ No newline at end of file
diff --git a/src/views/postList/postList.module.css b/src/views/postList/postList.module.css
new file mode 100644
index 0000000..6290106
--- /dev/null
+++ b/src/views/postList/postList.module.css
@@ -0,0 +1,88 @@
+.container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ position: relative;
+}
+
+.left {
+ flex: 3;
+ display: flex;
+ flex-direction: column;
+ margin: 5px;
+}
+
+.navbar {
+ height: 60px;
+ background-color: #f5f5f5;
+ border-bottom: 1px solid #ddd;
+ border-radius: 8px;
+}
+
+.content {
+ flex: 1;
+ overflow-y: auto; /* 允许垂直滚动 */
+ padding: 10px;
+ background-color: #fff;
+ border-radius: 8px;
+ border: 1px solid #ddd;
+}
+
+.contentItem {
+ position: relative;
+ margin-bottom: 15px;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ background-color: #f9f9f9;
+}
+
+.contentItem h3 {
+ margin: 0 0 5px;
+ font-size: 18px;
+ color: #333;
+}
+
+.contentItem p {
+ margin: 0;
+ font-size: 14px;
+ color: #666;
+}
+
+.contentItem .createDate {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ font-size: 12px;
+ color: #999;
+}
+
+.noData {
+ text-align: center;
+ color: #999;
+ margin-top: 20px;
+}
+
+.right {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ margin: 5px;
+}
+
+.selfStatus {
+ margin-bottom: 20px;
+ background-color: #fff;
+ border-radius: 8px;
+ border: 1px solid #ddd;
+ padding: 10px;
+}
+
+.filter {
+ flex: 1;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ padding: 10px;
+}
\ No newline at end of file
diff --git a/src/views/postList/postList.tsx b/src/views/postList/postList.tsx
new file mode 100644
index 0000000..c08b7cf
--- /dev/null
+++ b/src/views/postList/postList.tsx
@@ -0,0 +1,82 @@
+import React from "react";
+import style from "./postList.module.css";
+import SelfStatus from "@/components/selfStatus/selfStatus";
+import Navbar from "@/components/navbar/navbar";
+import PostsPanel from "@/components/postsPanel/postsPanel";
+import { getPosts, unknownAPI } from "@/api/post";
+import { Form } from "antd"
+import { useApi } from "@/hooks/request";
+import request from "@/utils/request";
+import { Pagination, PaginationProps } from "antd";
+import { set } from "lodash";
+import { useEffect } from "react";
+import { useNavigate, useSearchParams } from "react-router";
+
+
+const PostList:React.FC = () => {
+ const [searchParams] = useSearchParams();
+ const type = searchParams.get("type") || "";
+ const nav = useNavigate();
+
+ if(type in ['video', 'music', 'game', 'software']) {
+ nav('/')
+ }
+
+ const {data:postList, refresh:getPostList} = useApi(() => request.get(getPosts + `?tags=${[type]}&page=${currentPage}&pageSize=${pageSize}`), false);
+ const [currentPage, setCurrentPage] = React.useState(1);
+ const [pageSize, setPageSize] = React.useState(10);
+ const handlePageChange = (page:number, size?:number) => {
+ setCurrentPage(page);
+ if(size) setPageSize(size);
+ console.log(page, size);
+ };
+
+ const handlePostClick = (postId:number) => {
+ nav(`/postsDetail?postId=${postId}`);
+ }
+
+ useEffect(() => {
+ getPostList();
+ },[currentPage, pageSize]);
+
+ return (
+ <div className={style.container}>
+ <div className={style.left}>
+ <div className={style.navbar}>
+ <Navbar current={type}/>
+ </div>
+ <div className={style.content}>
+ {postList && postList.length > 0 ? (
+ postList.map((post: { postId: number; postTitle: string; postContent: string; createdAt:string }) => (
+ <div key={post.postId} className={style.contentItem} onClick={() => handlePostClick(post.postId)}>
+ <h3>{post.postTitle}</h3>
+ <p>{post.postContent.substring(0, 20)}</p>
+ <p className={style.createDate}>{post.createdAt}</p>
+ </div>
+ ))
+ ) : (
+ <div className={style.noData}>未查询到相关帖子</div>
+ )}
+
+ <Pagination
+ showSizeChanger
+ onShowSizeChange={handlePageChange}
+ defaultCurrent={1}
+ total={500}
+ onChange={handlePageChange}
+ />
+ </div>
+ </div>
+ <div className={style.right}>
+ <div className={style.selfStatus}>
+ <SelfStatus/>
+ </div>
+ <div className={style.filter}>
+
+ </div>
+ </div>
+ </div>
+ )
+}
+
+export default PostList;
\ No newline at end of file
diff --git a/test/login.test.tsx b/test/login.test.tsx
deleted file mode 100644
index b342387..0000000
--- a/test/login.test.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import React from 'react';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-import '@testing-library/jest-dom';
-import Login from '../src/views/login/login';
-import { useApi } from '@/hooks/request';
-import { useAppDispatch } from '@/hooks/store';
-import { useNavigate } from 'react-router';
-import { useSelector } from 'react-redux';
-
-
-jest.mock('@/hooks/request', () => ({
- useApi: jest.fn(),
-}));
-// 模拟所有外部依赖
-jest.mock('@/hooks/store', () => ({
- useAppDispatch: jest.fn(),
-}));
-
-jest.mock('react-router', () => ({
- useNavigate: jest.fn(),
-}));
-
-jest.mock('react-redux', () => ({
- useSelector: jest.fn(),
-}));
-
-// 类型安全:明确模拟函数的返回类型
-const mockUseApi = useApi as jest.MockedFunction<typeof useApi>;
-const mockUseAppDispatch = useAppDispatch as jest.MockedFunction<typeof useAppDispatch>;
-const mockUseNavigate = useNavigate as jest.MockedFunction<typeof useNavigate>;
-const mockUseSelector = useSelector as jest.MockedFunction<typeof useSelector>;
-
-describe('Login Component', () => {
- const mockDispatch = jest.fn();
- const mockNavigate = jest.fn();
- const mockPostRefresh = jest.fn();
- const mockGetRefresh = jest.fn();
-
- beforeEach(() => {
- // 初始化模拟函数返回值
- mockUseAppDispatch.mockReturnValue(mockDispatch);
- mockUseNavigate.mockReturnValue(mockNavigate);
- mockUseSelector.mockImplementation((selector) => selector({ user: { userName: '' } }));
-
- // 默认模拟 useApi 返回正常状态
- mockUseApi
- .mockReturnValueOnce({
- data: { token: 'mock-token' },
- loading: false,
- error: null,
- refresh: mockPostRefresh,
- })
- .mockReturnValueOnce({
- data:{
- 'userId' : '001',
- 'userName' : 'san3yuan',
- 'role' : 'manager',
- 'uploadTraffic' : 0,
- 'downloadTraffic': 0,
- 'downloadPoints' : 0,
- 'avatar' : 'https://www.w3school.com.cn/i/photo/tulip.jpg',
- },
- loading: false,
- error: null,
- refresh: mockGetRefresh,
- })
- ;
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- // 测试 1: 基础渲染
- it('渲染所有输入框和按钮', () => {
- render(<Login />);
- expect(screen.getByPlaceholderText('Enter your email')).toBeInTheDocument();
- expect(screen.getByPlaceholderText('Enter your password')).toBeInTheDocument();
- expect(screen.getByText('登录')).toBeInTheDocument();
- expect(screen.getByText('注册')).toBeInTheDocument();
- expect(screen.getByText('忘记密码')).toBeInTheDocument();
- });
-
- // 测试 2: 登录成功流程
- it('点击登录按钮触发API请求、Redux更新和导航', async () => {
- // 模拟 API 返回有效数据
- mockPostRefresh.mockResolvedValue({ token: 'mock-token' });
- mockGetRefresh.mockResolvedValue({
- 'userId' : '001',
- 'userName' : 'san3yuan',
- 'role' : 'manager',
- 'uploadTraffic' : 0,
- 'downloadTraffic': 0,
- 'downloadPoints' : 0,
- 'avatar' : 'https://www.w3school.com.cn/i/photo/tulip.jpg',
- });
-
- render(<Login />);
- jest.useFakeTimers();
- fireEvent.click(screen.getByText('登录'));
- jest.runAllTimers();
- await waitFor(() => {
- // 验证 dispatch 调用
- expect(mockPostRefresh).toHaveBeenCalled();
-
- // 验证Redux更新
- expect(mockDispatch).toHaveBeenNthCalledWith(1, {
- type: 'user/login',
- payload: { token: 'mock-token' }
- });
-
- // 验证第二次API调用(用户信息)
- expect(mockGetRefresh).toHaveBeenCalled();
-
- // 验证用户信息更新
- expect(mockDispatch).toHaveBeenNthCalledWith(2, {
- type: 'user/getUserInfo',
- payload: {
- 'userId' : '001',
- 'userName' : 'san3yuan',
- 'role' : 'manager',
- 'uploadTraffic' : 0,
- 'downloadTraffic': 0,
- 'downloadPoints' : 0,
- 'avatar' : 'https://www.w3school.com.cn/i/photo/tulip.jpg',
- }
- });
- });
- });
- });
\ No newline at end of file
diff --git a/test/postsPanel.test.tsx b/test/postsPanel.test.tsx
deleted file mode 100644
index cd753fc..0000000
--- a/test/postsPanel.test.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import React from 'react';
-import { render, screen, waitFor } from '@testing-library/react';
-import '@testing-library/jest-dom';
-import { useApi } from '@/hooks/request';
-import PostsPanel from '@/components/postsPanel/postsPanel';
-
-// 模拟 useApi
-jest.mock('@/hooks/request', () => ({
- useApi: jest.fn(() => ({
- data: [], // 默认返回空数组
- loading: false,
- error: null,
- })),
-}));
-describe('PostsPanel Component', () => {
- const mockUseApi = useApi as jest.MockedFunction<typeof useApi>;
-
- it('renders the component with a title', () => {
- // 渲染组件
- render(<PostsPanel name="热门帖子" url="/api/posts" limit={5} />);
-
- // 验证标题是否正确渲染
- expect(screen.getByText('热门帖子')).toBeInTheDocument();
- expect(screen.getByText('更多')).toBeInTheDocument();
- });
-
- it('renders posts when data is available', async () => {
- // 模拟 API 返回数据
- mockUseApi.mockReturnValue({
- data: [
- { title: 'Post 1', date: '2025-04-01' },
- { title: 'Post 2', date: '2025-04-02' },
- ],
- });
-
- // 渲染组件
- render(<PostsPanel name="热门帖子" url="/api/posts" limit={5} />);
-
- // 验证数据是否正确渲染
- await waitFor(() => {
- expect(screen.getByText('Post 1')).toBeInTheDocument();
- expect(screen.getByText('2025-04-01')).toBeInTheDocument();
- expect(screen.getByText('Post 2')).toBeInTheDocument();
- expect(screen.getByText('2025-04-02')).toBeInTheDocument();
- });
- });
-
- it('renders a message when no data is available', async () => {
- // 模拟 API 返回空数据
- mockUseApi.mockReturnValue({
- data: [],
- });
-
- // 渲染组件
- render(<PostsPanel name="热门帖子" url="/api/posts" limit={5} />);
-
- // 验证无数据时的提示信息
- await waitFor(() => {
- expect(screen.getByText('未查询到相关记录')).toBeInTheDocument();
- });
- });
-
- it('handles loading state', () => {
- // 模拟加载状态
- mockUseApi.mockReturnValue({
- data: null,
- });
-
- // 渲染组件
- render(<PostsPanel name="热门帖子" url="/api/posts" limit={5} />);
-
- // 验证组件是否正确渲染(可以根据需求添加加载状态的测试)
- expect(screen.getByText('未查询到相关记录')).toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/test/selfStatus.test.tsx b/test/selfStatus.test.tsx
deleted file mode 100644
index 4bd690e..0000000
--- a/test/selfStatus.test.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import SelfStatus from '@/components/selfStatus/selfStatus';
-import { render, screen } from '@testing-library/react';
-import { useSelector } from 'react-redux';
-import React from 'react';
-import '@testing-library/jest-dom';
-import { useAppSelector } from '@/hooks/store';
-
-jest.mock('@/hooks/request', () => ({
- useApi: jest.fn(),
- }));
- // 模拟所有外部依赖
- jest.mock('@/hooks/store', () => ({
- useAppDispatch: jest.fn(),
- useAppSelector: jest.fn(),
- }));
-
- jest.mock('react-router', () => ({
- useNavigate: jest.fn(),
- }));
-
- jest.mock('react-redux', () => ({
- useSelector: jest.fn(),
- }));
-
-
-describe('SelfStatus Component', () => {
- it('renders correctly', () => {
- (useAppSelector as jest.Mock).mockImplementation((selector) => selector({
- user: {
- userId: '001',
- userName: 'san3yuan',
- role: 'manager',
- uploadTraffic: 0,
- downloadTraffic: 0,
- downloadPoints: 0,
- },
- setting: {
- theme: 'light',
- },
- }));
-
- render(<SelfStatus />);
-
- expect(screen.getByText('san3yuan')).toBeInTheDocument();
- expect(screen.getByText('用户组: manager')).toBeInTheDocument();
- expect(screen.getByText('上传量: 0')).toBeInTheDocument();
- expect(screen.getByText('下载量: 0')).toBeInTheDocument();
- expect(screen.getByText('下载积分: 0')).toBeInTheDocument();
- })
- it('calculates and displays share ratio correctly', () => {
- (useAppSelector as jest.Mock).mockImplementation((selector) => selector({
- user: {
- uploadTraffic: 100,
- downloadTraffic: 50,
- },
- }));
- render(<SelfStatus />);
- expect(screen.getByText('分享率: 2.00')).toBeInTheDocument();
- });
- it('handles empty data gracefully', () => {
- (useAppSelector as jest.Mock).mockImplementation((selector) => selector({
- user: {
- userName: '',
- role: '',
- uploadTraffic: null,
- downloadTraffic: null,
- downloadPoints: null,
- },
- }));
- render(<SelfStatus />);
- expect(screen.getByText('用户组: N/A')).toBeInTheDocument();
- expect(screen.getByText('上传量: 0')).toBeInTheDocument();
- expect(screen.getByText('下载量: 0')).toBeInTheDocument();
- expect(screen.getByText('分享率: N/A')).toBeInTheDocument();
- expect(screen.getByText('下载积分: 0')).toBeInTheDocument();
- });
-});
\ No newline at end of file