merge

Change-Id: I5227831adac7f85854cbe7321c2a3aa39d8c1d7a
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/scripts/.env b/scripts/.env
deleted file mode 100644
index 083c815..0000000
--- a/scripts/.env
+++ /dev/null
@@ -1 +0,0 @@
-NODE_ENV=development
\ No newline at end of file
diff --git a/scripts/webpack.base.js b/scripts/webpack.base.js
index eb4f06e..5224c08 100644
--- a/scripts/webpack.base.js
+++ b/scripts/webpack.base.js
@@ -150,10 +150,6 @@
       cwd: process.cwd(),
     }),
     new webpack.DefinePlugin({
-      'process.env': JSON.stringify({
-        NODE_ENV: process.env.NODE_ENV || 'production',
-        // 这里可以添加其他你需要的环境变量
-      }),
       // 确保基础变量存在
       'process.platform': JSON.stringify(platform()),
       'process.browser': JSON.stringify(true)
diff --git a/scripts/webpack.dev.js b/scripts/webpack.dev.js
index c5feb8d..eeb400a 100644
--- a/scripts/webpack.dev.js
+++ b/scripts/webpack.dev.js
@@ -14,7 +14,7 @@
       'process.env': JSON.stringify({
         NODE_ENV: 'development',    // 等价于 mode 设置
         PUBLIC_URL: './',           // 建议使用相对路径
-        API_BASE_URL: 'http://localhost:3030/api', // 添加API路径
+        API_BASE_URL: 'http://localhost:5008', // 添加API路径
         WEB_BASE_URL: 'http://localhost:8080', // 添加WEB路径
       })
     }),
diff --git a/scripts/webpack.prod.js b/scripts/webpack.prod.js
index 0de8641..88ac0ba 100644
--- a/scripts/webpack.prod.js
+++ b/scripts/webpack.prod.js
@@ -3,4 +3,14 @@
 
 module.exports = merge(base, {
   mode: 'production', // 生产模式
+  plugins: [
+    new webpack.DefinePlugin({
+      'process.env': JSON.stringify({
+        NODE_ENV: 'production',    // 等价于 mode 设置
+        PUBLIC_URL: './',           // 建议使用相对路径
+        API_BASE_URL: 'http://localhost:5008', // 添加API路径
+        WEB_BASE_URL: 'http://localhost:8080', // 添加WEB路径
+      })
+    }),
+  ],
 })
\ 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 3745736..61d3f4e 100644
--- a/src/components/selfStatus/selfStatus.tsx
+++ b/src/components/selfStatus/selfStatus.tsx
@@ -1,8 +1,14 @@
-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";
+
 import { useNavigate } from "react-router";
 
+
 interface SelfStatusProps {
     className?: string;
 }
@@ -16,6 +22,19 @@
     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]);
 
     function handleAvatarClick(){
         nav('/homepage')
@@ -24,7 +43,8 @@
     return (
         <div className={style.container}>
             <div className={style.left}>
-                <img className={style.avatar} onClick={handleAvatarClick} 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 81a6670..a5f7396 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;
     --primary-color: #3498db; 
     --primary-hover: #2980b9;
     --primary-card: #a3d1f0;
@@ -38,6 +39,7 @@
     --text-color: #f1f1f1;
     --card-bg: #1e1e1e;
     --border-color: #444444;
+    --select-bg-color:#3a466b;
     --primary-color: #3498db; 
     --primary-hover: #2980b9;
     --primary-card:#280202;
diff --git a/src/index.tsx b/src/index.tsx
index 69441df..494de3e 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,10 +1,12 @@
-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 { useEffect } from "react";
+import { checkAndRefreshToken } from "./utils/jwt";
 
 if(localStorage.getItem("theme") === null) {
     localStorage.setItem("theme", "light");
@@ -14,6 +16,7 @@
 }
 
 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 f9c6a64..49cc44f 100644
--- a/src/mock/index.ts
+++ b/src/mock/index.ts
@@ -3,16 +3,17 @@
 import { setupAuthMock }  from './auth'
 import { setupUserMock } from './user';
 import { setupPostMock } from './post';
+import { setupCommentMock } from './comment';
 import {setupUserMessageMock} from './homepage';
 import { setupUploadMock } from './upload';
 
-// 创建 Mock 实例
-export const mock = new MockAdapter(instance, { 
-  delayResponse: process.env.NODE_ENV === 'test' ? 0 : 500 
-})
 
 // 聚合所有 Mock 模块
 export function setupMock() {
+  return;
+  const mock = new MockAdapter(instance, { 
+    delayResponse: process.env.NODE_ENV === 'test' ? 0 : 500 
+  })
   // 开发环境启用 Mock
   if (process.env.NODE_ENV !== 'development') return
 
@@ -20,11 +21,9 @@
   setupAuthMock(mock)
   setupUserMock(mock)
   setupPostMock(mock)
+  setupCommentMock(mock)
   setupUserMessageMock(mock)
   setupUploadMock(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 62bb7a5..857118e 100644
--- a/src/route/index.tsx
+++ b/src/route/index.tsx
@@ -9,6 +9,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([
     {
@@ -28,6 +30,14 @@
 
                     },
                     {
+                        path: '/posts',
+                        element: <PostList/>
+                    },
+                    {
+                        path: '/postsDetail',
+                        element: <PostDetail/>
+                    },
+                    {
                         path:'/homepage',
                         element: <Homepage/>
                     },
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 8c60d4b..3c9c122 100644
--- a/src/utils/axios.ts
+++ b/src/utils/axios.ts
@@ -2,8 +2,7 @@
 import type { AxiosRequestConfig, AxiosResponse } from 'axios'
 
 const instance = axios.create({
-    // baseURL: process.env.API_BASE_URL,
-    baseURL: 'http://localhost:8080',
+    baseURL: process.env.API_BASE_URL,
     timeout: 10000,
     headers: {
       'Content-Type': 'application/json'
@@ -13,6 +12,7 @@
   // 请求拦截器
   instance.interceptors.request.use(
     (config) => {
+      console.log('Request Config:', config)
       // 添加认证 token
       const token = localStorage.getItem('token')
       if (token) {
@@ -28,7 +28,6 @@
   // 响应拦截器
   instance.interceptors.response.use(
     (response) => {
-      console.log('Response:', response)
       // 统一处理响应数据格式
       if (response.status === 200) {
         return response.data
@@ -36,6 +35,9 @@
       return Promise.reject(response.data)
     },
     (error) => {
+      if(error.status===401){
+       // window.location.href = '/login';
+      }
       // 统一错误处理
       console.error('API Error:', error.response?.status, error.message)
       return Promise.reject(error)
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 c22a528..83106a1 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,12 +12,13 @@
 import logo from "&/assets/logo.png";
 import { useAppDispatch } from "@/hooks/store";
 import { useSelector } from "react-redux";
-
+import { checkAndRefreshToken } from "@/utils/jwt";
 import { useNavigate } from "react-router-dom";
-
-
 const Frame:React.FC = () => {
 
+    useEffect(() => {
+        checkAndRefreshToken();
+    }, []);
     const dispatch = useAppDispatch();
 
     const showSearch = useSelector((state: any) => state.setting.showSearch); 
diff --git a/src/views/login/login.module.css b/src/views/login/login.module.css
index dca1df4..a7fc6f4 100644
--- a/src/views/login/login.module.css
+++ b/src/views/login/login.module.css
@@ -74,6 +74,18 @@
     max-width: 300px;
 }
 
+.sendCode {
+    padding:5px;
+    border-radius: 5px;
+    border: none;
+    font-size: 8px;
+    background-color: #ff7300; /* Blue */
+    color: white;
+    cursor: pointer;
+    width: 60px;
+    max-width: 300px;
+}
+
 .form .register:hover {
     background-color: #0056b3; /* Darker blue */
 }
@@ -90,4 +102,20 @@
     text-decoration: underline;
     align-content: flex-end;
     color: #007BFF; /* Blue */
+}
+
+.back {
+    background: none;
+    border: none;
+    text-decoration: underline;
+    cursor: pointer;
+    margin-top: 8px;
+    font-size: 16px;
+    padding: 0;
+    transition: color 0.2s;
+}
+
+.back:hover {
+    color: #0056b3;
+    text-decoration: underline;
 }
\ No newline at end of file
diff --git a/src/views/login/login.tsx b/src/views/login/login.tsx
index 96c5485..fef6e32 100644
--- a/src/views/login/login.tsx
+++ b/src/views/login/login.tsx
@@ -1,28 +1,39 @@
-import React, { useEffect } from 'react';
+import React, { useState } from 'react';
 import { useApi } from '@/hooks/request';
 import request from '@/utils/request';
-import { postUserLogin} from '@/api/auth';
+import { postUserLogin } from '@/api/auth';
 import { useAppDispatch } from '@/hooks/store';
-import { RootState } from '@/store';
 import style from './login.module.css';
-import { useState } from 'react';
-import { useSelector } from 'react-redux';
 import { useNavigate } from 'react-router';
 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 [showRegister, setShowRegister] = useState(false);
+    const [inviteCode, setInviteCode] = useState('');
+    const [registerEmail, setRegisterEmail] = useState('');
+    const [registerPassword, setRegisterPassword] = useState('');
+    const [emailCode, setEmailCode] = useState('');
+    const [codeBtnDisabled, setCodeBtnDisabled] = useState(false);
+    const [codeBtnText, setCodeBtnText] = useState('发送验证码');
+    const [codeTimer, setCodeTimer] = useState<NodeJS.Timeout | null>(null);
 
+    const dispatch = useAppDispatch();
+    const [messageApi, contextHolder] = message.useMessage();
     const { refresh: postUserLoginRefresh } = useApi(
         () => request.post(postUserLogin, { email, password}), false);
     const { refresh: getUserInfoRefresh } = useApi(
         () => request.get(getUserInfo), false);
-
     const nav = useNavigate();
+    const showErrorMessage = async (msg: string) => {
+        messageApi.error(msg);
+    };
+
+    // 登录逻辑
     const handleLogin = debounce(async () => {
         try {
             const res =await postUserLoginRefresh({email, password});
@@ -32,35 +43,141 @@
                 return;
             }
             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;
 
+    // 发送验证码逻辑
+    const handleSendCode = async () => {
+        if (!registerEmail) {
+            showErrorMessage('请填写邮箱');
+            return;
+        }
+        setCodeBtnDisabled(true);
+        let seconds = 60;
+        setCodeBtnText(`已发送(${seconds}s)`);
+        const timer = setInterval(() => {
+            seconds -= 1;
+            setCodeBtnText(`已发送(${seconds}s)`);
+            if (seconds <= 0) {
+                clearInterval(timer);
+                setCodeBtnDisabled(false);
+                setCodeBtnText('发送验证码');
+            }
+        }, 1000);
+        setCodeTimer(timer);
+        // TODO: 调用发送验证码接口
+        message.success('验证码已发送');
+    };
+
+    // 切换回登录
+    const handleBackToLogin = () => {
+        setShowRegister(false);
+        setInviteCode('');
+        setRegisterEmail('');
+        setRegisterPassword('');
+        setEmailCode('');
+        if (codeTimer) clearInterval(codeTimer);
+        setCodeBtnDisabled(false);
+        setCodeBtnText('发送验证码');
+    };
+
+    // 注册逻辑(仅前端校验,实际应调用注册接口)
+    const handleRegister = async () => {
+        if (!inviteCode || !registerEmail || !registerPassword || !emailCode) {
+            showErrorMessage('请填写完整信息');
+            return;
+        }
+        // TODO: 调用注册接口
+        message.success('注册成功,请登录');
+        handleBackToLogin();
+    };
+
     const handleLogoClick = () => {
         nav('/');
-    }
+    };
+
     return (
         <div className={style.form}>
-            <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" />
-            <button className={style.submit} onClick={() => handleLogin()}>登录</button>
-            <button className={style.register}>注册</button>
-            <button className={style.forget}> 忘记密码</button>
+            {contextHolder}
+            <img className={style.logo} src={logo} alt="logo" onClick={handleLogoClick} />
+            {!showRegister ? (
+                <>
+                    <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"
+                    />
+                    <button className={style.submit} onClick={() => handleLogin()}>登录</button>
+                    <button className={style.register} onClick={() => setShowRegister(true)}>注册</button>
+                    <button className={style.forget}>忘记密码</button>
+                </>
+            ) : (
+                <>
+                    <input
+                        type="text"
+                        value={inviteCode}
+                        onChange={(e) => setInviteCode(e.target.value)}
+                        className={style.invite}
+                        placeholder="邀请码"
+                    />
+                    <input
+                        type="email"
+                        value={registerEmail}
+                        onChange={(e) => setRegisterEmail(e.target.value)}
+                        className={style.email}
+                        placeholder="邮箱"
+                    />
+                    <input
+                        type="password"
+                        value={registerPassword}
+                        onChange={(e) => setRegisterPassword(e.target.value)}
+                        className={style.password}
+                        placeholder="密码"
+                    />
+                    <div style={{ display: 'flex',width:'80%', alignItems: 'center', gap: 8, padding:'10px' }}>
+                        <input
+                            type="text"
+                            value={emailCode}
+                            onChange={(e) => setEmailCode(e.target.value)}
+                            className={style.code}
+                            placeholder="邮箱验证码"
+                            style={{ flex: 1 }}
+                        />
+                        <button
+                            className={style.sendCode}
+                            onClick={handleSendCode}
+                            disabled={codeBtnDisabled}
+                            style={{
+                                background: codeBtnDisabled ? '#ccc' : undefined,
+                                color: codeBtnDisabled ? '#888' : undefined,
+                                cursor: codeBtnDisabled ? 'not-allowed' : 'pointer'
+                            }}
+                        >
+                            {codeBtnText}
+                        </button>
+                    </div>
+                    <button className={style.submit} onClick={handleRegister}>注册</button>
+                    <button className={style.back} onClick={handleBackToLogin}>返回登录</button>
+                </>
+            )}
         </div>
     );
 };
diff --git a/src/views/postDetail/postDetail.module.css b/src/views/postDetail/postDetail.module.css
new file mode 100644
index 0000000..46c2176
--- /dev/null
+++ b/src/views/postDetail/postDetail.module.css
@@ -0,0 +1,89 @@
+.commentList .ant-list-item {
+    min-height: 300px;
+    height: 300px;
+    box-sizing: border-box;
+    display: flex;
+    align-items: flex-start;
+    /* 可选:让内容垂直居中可用 align-items: center; */
+}
+.contentArea {
+  width: 100%;
+  max-width: 900px;
+  margin: 32px auto 0 auto;
+  padding: 0 16px 32px 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+.card {
+  border-radius: 10px;
+  background: var(--card-bg);
+}
+
+.metaRow {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px;
+  margin-bottom: 8px;
+}
+
+.locked {
+  margin: 12px 0;
+  color: #d4380d;
+  font-weight: bold;
+}
+
+.contentText {
+  font-size: 17px;
+  color: var(--text-color);
+  line-height: 1.8;
+  margin-top: 12px;
+}
+
+.addCommentCard {
+  border-radius: 10px;
+  background: var(--card-bg);
+}
+
+.textarea {
+  font-size: 15px;
+}
+
+.commentListCard {
+  border-radius: 10px;
+  background: var(--card-bg);
+}
+
+.commentList .ant-list-item,
+.replyList .ant-list-item {
+  min-height: 120px;
+  height: auto;
+  box-sizing: border-box;
+  display: flex;
+  align-items: flex-start;
+  border-bottom: 1px solid var(--border-color);
+  padding: 24px 16px;
+}
+
+.replyList {
+  margin-left: 32px;
+  background: transparent;
+}
+
+.replyItem {
+  background: #f6f8fa;
+  border-radius: 6px;
+  margin-bottom: 8px;
+  padding: 12px 16px;
+}
+
+@media (max-width: 600px) {
+  .contentArea {
+    max-width: 100%;
+    padding: 0 4px 24px 4px;
+  }
+  .card, .addCommentCard, .commentListCard {
+    padding: 0;
+  }
+}
\ No newline at end of file
diff --git a/src/views/postDetail/postDetail.tsx b/src/views/postDetail/postDetail.tsx
new file mode 100644
index 0000000..d6029a8
--- /dev/null
+++ b/src/views/postDetail/postDetail.tsx
@@ -0,0 +1,206 @@
+import React, { useEffect, useState } from 'react';
+import styles from './PostDetail.module.css';
+import { Card, List, Typography, Button, Input, Spin, Empty, Divider } from 'antd';
+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';
+import { DownloadOutlined, LikeOutlined, LikeFilled } from '@ant-design/icons';
+
+const { Title, Text, Paragraph } = Typography;
+const { TextArea } = Input;
+
+export interface PostResponse {
+  postId?: number;
+  userId?: number;
+  postTitle?: string;
+  postContent?: string;
+  createdAt?: number;
+  postType?: string;
+  isLocked?: boolean;
+  lockedReason?: string;
+  lockedAt?: string;
+  lockedBy?: number;
+  viewCount?: number;
+  hotScore?: number;
+  lastCalculated?: 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);
+  const [liked, setLiked] = useState(false);
+
+  useEffect(() => {
+    if (!postId) return;
+    const fetchData = async () => {
+      setLoading(true);
+      const postRes = await getPostDetailRefresh();
+      if (!postRes || (postRes as any).error) {
+        setLoading(false);
+        return;
+      }
+      setPost(postRes as PostResponse);
+
+      const commentsRes = await getPostCommentsRefresh();
+      setComments(commentsRes 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.contentArea}>
+        <Card
+          title={<Title level={3} style={{ margin: 0 }}>{post.postTitle || "帖子标题"}</Title>}
+          className={styles.card}
+          bordered={false}
+          style={{ marginBottom: 24, boxShadow: '0 4px 24px rgba(0,0,0,0.08)' }}
+          extra={
+            <div style={{ display: 'flex', gap: 16 }}>
+              <Button
+                type="primary"
+                icon={<DownloadOutlined />}
+                onClick={() => {
+                  // 下载逻辑
+                  window.open(`/api/download/post/${post.postId}`, '_blank');
+                }}
+              >
+                下载
+              </Button>
+              <Button
+                type="primary"
+                icon={liked ? <LikeFilled /> : <LikeOutlined />}
+                style={liked ? { background: '#ccc', borderColor: '#ccc', color: '#888', cursor: 'not-allowed' } : {}}
+                disabled={liked}
+                onClick={() => setLiked(true)}
+              >
+                {liked ? '已点赞' : '点赞'}
+              </Button>
+            </div>
+          }
+        >
+          <div className={styles.metaRow}>
+            <Text type="secondary">作者ID: {post.userId}</Text>
+            <Text type="secondary">发布时间: {post.createdAt ? new Date(post.createdAt).toLocaleString() : "未知"}</Text>
+            <Text type="secondary">浏览量: {post.viewCount}</Text>
+            <Text type="secondary">类型: {post.postType}</Text>
+            <Text type="secondary">热度: {post.hotScore}</Text>
+            <Text type="secondary">最后计算: {post.lastCalculated ? new Date(post.lastCalculated).toLocaleString() : "无"}</Text>
+          </div>
+          {post.isLocked && (
+            <div className={styles.locked}>
+              <Text type="danger">本帖已锁定</Text>
+              {post.lockedReason && <Text type="secondary">(原因:{post.lockedReason})</Text>}
+              {post.lockedAt && <Text style={{ marginLeft: 8 }}>锁定时间: {post.lockedAt}</Text>}
+              {post.lockedBy !== 0 && <Text style={{ marginLeft: 8 }}>锁定人ID: {post.lockedBy}</Text>}
+            </div>
+          )}
+          <Divider style={{ margin: '16px 0' }} />
+          <Paragraph className={styles.contentText}>{post.postContent || "暂无内容"}</Paragraph>
+        </Card>
+
+        {/* 发布评论区域 */}
+        <Card className={styles.addCommentCard} style={{ marginBottom: 32, boxShadow: '0 2px 12px rgba(0,0,0,0.06)' }}>
+          <Title level={5} style={{ marginBottom: 12 }}>发布评论</Title>
+          <TextArea
+            rows={4}
+            value={newComment}
+            onChange={(e) => setNewComment(e.target.value)}
+            placeholder="写下你的评论..."
+            className={styles.textarea}
+          />
+          <Button
+            type="primary"
+            style={{ marginTop: 12, float: 'right' }}
+            onClick={() => setNewComment('')}
+            disabled={!newComment.trim()}
+          >
+            评论
+          </Button>
+          <div style={{ clear: 'both' }} />
+        </Card>
+
+        <Card
+          className={styles.commentListCard}
+          title={<Title level={4} style={{ margin: 0 }}>评论区</Title>}
+          bodyStyle={{ padding: 0 }}
+        >
+          <List
+            className={styles.commentList}
+            dataSource={comments}
+            locale={{ emptyText: <Empty description="暂无评论" /> }}
+            renderItem={(item) => (
+              <List.Item className={styles.commentItem} key={item.commentId}>
+                <List.Item.Meta
+                  title={<Text strong>用户ID: {item.userId}</Text>}
+                  description={
+                    <>
+                      <Text>{item.content}</Text>
+                      <div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
+                        {item.createdAt && new Date(item.createdAt).toLocaleString()}
+                      </div>
+                    </>
+                  }
+                />
+                {/* 可递归渲染子评论 */}
+                {item.replies && item.replies.length > 0 && (
+                  <List
+                    className={styles.replyList}
+                    dataSource={item.replies}
+                    renderItem={reply => (
+                      <List.Item className={styles.replyItem} key={reply.commentId}>
+                        <List.Item.Meta
+                          title={<Text strong>用户ID: {reply.userId}</Text>}
+                          description={
+                            <>
+                              <Text>{reply.content}</Text>
+                              <div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
+                                {reply.createdAt && new Date(reply.createdAt).toLocaleString()}
+                              </div>
+                            </>
+                          }
+                        />
+                      </List.Item>
+                    )}
+                  />
+                )}
+              </List.Item>
+            )}
+          />
+        </Card>
+      </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