添加了postsPanel作为通用帖子显示板,增加了对jest测试的配置,添加了论坛主页,设定了论坛全局框架,设定了论坛基础主题色及主题切换、字号切换逻辑

Change-Id: I9fad0cf577088adb00c9850d405ccd23e6072413
diff --git a/src/api/post.ts b/src/api/post.ts
new file mode 100644
index 0000000..99dc8fa
--- /dev/null
+++ b/src/api/post.ts
@@ -0,0 +1 @@
+export const hotPosts='/post/hot'
\ No newline at end of file
diff --git a/src/components/navbar/navbar.module.css b/src/components/navbar/navbar.module.css
new file mode 100644
index 0000000..b531f39
--- /dev/null
+++ b/src/components/navbar/navbar.module.css
@@ -0,0 +1,12 @@
+.menu{
+    background-color: var(--card-bg);
+    font-size: 1rem;
+    
+    display:flex;
+    justify-content:space-between;
+}
+
+.menu *{
+    color:var(--text-color);
+    transition:none;
+}
\ No newline at end of file
diff --git a/src/components/navbar/navbar.tsx b/src/components/navbar/navbar.tsx
new file mode 100644
index 0000000..a6c1fe7
--- /dev/null
+++ b/src/components/navbar/navbar.tsx
@@ -0,0 +1,139 @@
+import React, { use } from "react";
+import Icon, {
+    HomeOutlined,
+} from "@ant-design/icons";
+import { Space } from 'antd';
+import type { GetProps } from 'antd';
+import type { MenuProps } from 'antd';
+import { Menu } from 'antd';
+import { useState } from "react";
+import style from './navbar.module.css'
+
+
+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>
+      <path d="M85.333333 348.091733V764.586667a153.6 153.6 0 0 0 153.6 153.6h561.578667a153.6 153.6 0 0 0 153.6-153.6V348.091733a153.6 153.6 0 0 0-153.6-153.6H238.933333a153.6 153.6 0 0 0-153.6 153.6z m68.266667 0a85.333333 85.333333 0 0 1 85.333333-85.333333h561.578667a85.333333 85.333333 0 0 1 85.333333 85.333333V764.586667a85.333333 85.333333 0 0 1-85.333333 85.333333H238.933333a85.333333 85.333333 0 0 1-85.333333-85.333333V348.091733z" fill="#000000" p-id="7332"></path><path d="M401.885867 182.254933l-65.467734-69.188266a34.133333 34.133333 0 1 0-49.578666 46.933333l65.450666 69.1712a34.133333 34.133333 0 0 0 49.595734-46.916267zM635.989333 182.254933l65.467734-69.188266a34.133333 34.133333 0 1 1 49.578666 46.933333l-65.467733 69.1712a34.133333 34.133333 0 0 1-49.578667-46.916267zM383.556267 472.405333v167.168a85.333333 85.333333 0 0 0 128.733866 73.454934l141.482667-83.575467a85.333333 85.333333 0 0 0 0-146.944l-141.482667-83.575467a85.333333 85.333333 0 0 0-128.733866 73.454934z m68.266666 0a17.066667 17.066667 0 0 1 25.7536-14.711466l141.4656 83.592533a17.066667 17.066667 0 0 1 0 29.3888l-141.482666 83.575467a17.066667 17.066667 0 0 1-25.736534-14.677334v-167.185066z" fill="#000000" p-id="7333"></path>
+    </svg>
+);
+const MusicSvg = () => (
+    <svg width="1em" height="1em" fill="currentColor" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2776" viewBox="0 0 1024 1024">
+        <title>music icon</title>
+        <path d="M895.456 770.56C895.552 769.696 896 768.896 896 768L896 160c0-1.056-0.512-1.984-0.608-3.008-0.032-1.664 0.448-3.232 0.16-4.928-2.88-17.408-19.328-29.184-36.8-26.304l-480 80c-17.408 2.912-29.216 19.392-26.304 36.832 0.256 1.472 1.024 2.656 1.44 4.032C352.96 249.664 352 252.672 352 256l0 429.6c-20.128-9.376-43.648-14.784-69.408-14.784-21.312 0-42.816 3.456-63.968 10.336-39.616 12.8-73.536 36.224-95.584 65.984-24.064 32.512-31.68 70.24-20.864 103.648 15.648 48.288 66.656 79.456 129.92 79.456 21.248 0 42.72-3.456 63.904-10.304 58.656-19.04 100.288-59.2 115.04-103.808C413.888 811.328 416 806.016 416 800l0-5.312c1.056-8.48 1.056-16.96 0-25.472L416 264.448l416-69.344 0 490.88c-20.32-9.632-44.096-15.2-70.176-15.2-21.28 0-42.816 3.456-63.968 10.336-39.584 12.8-73.568 36.224-95.584 65.984-24.096 32.512-31.68 70.24-20.864 103.648 15.648 48.288 66.656 79.456 129.92 79.456 21.248 0 42.752-3.456 63.904-10.304C853.472 894.56 902.176 831.68 895.456 770.56z" fill="#5D646F" p-id="2777"></path>
+      
+    </svg>
+);
+const GameSvg = () => (
+    <svg width="1em" height="1em" fill="currentColor" xmlns="http://www.w3.org/2000/svg" p-id="8400" viewBox="0 0 1024 1024">
+        <title>game icon</title>
+        <path d="M683.2 588.8m-43.2 0a43.2 43.2 0 1 0 86.4 0 43.2 43.2 0 1 0-86.4 0Z" p-id="8401"></path><path d="M942.4 358.4c-30.4-17.6-64-25.6-97.6-25.6H555.2v-256c0-25.6-17.6-43.2-43.2-43.2s-43.2 17.6-43.2 43.2v256H174.4C81.6 332.8 0 414.4 0 512v299.2c0 97.6 81.6 179.2 174.4 179.2 84.8 0 158.4-64 174.4-145.6h324.8c17.6 81.6 84.8 145.6 174.4 145.6 97.6 0 174.4-81.6 174.4-179.2V512c1.6-59.2-28.8-120-80-153.6z m-4.8 456c0 51.2-36.8 94.4-88 94.4s-89.6-43.2-89.6-94.4c0-30.4-20.8-56-56-56H353.6c-64 0-84.8 25.6-84.8 56 0 51.2-43.2 89.6-89.6 89.6-51.2 0-89.6-43.2-89.6-94.4V507.2c-4.8-46.4 38.4-89.6 84.8-89.6H848c25.6 0 46.4 8 64 30.4 17.6 17.6 25.6 38.4 25.6 64v302.4z" p-id="8402"></path><path d="M340.8 545.6h-43.2v-43.2c0-25.6-17.6-43.2-43.2-43.2s-43.2 17.6-43.2 43.2v43.2h-43.2c-25.6 0-43.2 17.6-43.2 43.2s17.6 43.2 43.2 43.2h43.2v43.2c0 25.6 17.6 43.2 43.2 43.2s43.2-17.6 43.2-43.2v-43.2h43.2c25.6 0 43.2-17.6 43.2-43.2s-17.6-43.2-43.2-43.2z" p-id="8403"></path><path d="M852.8 588.8m-43.2 0a43.2 43.2 0 1 0 86.4 0 43.2 43.2 0 1 0-86.4 0Z" p-id="8404"></path><path d="M768 504m-43.2 0a43.2 43.2 0 1 0 86.4 0 43.2 43.2 0 1 0-86.4 0Z" p-id="8405"></path><path d="M768 673.6m-43.2 0a43.2 43.2 0 1 0 86.4 0 43.2 43.2 0 1 0-86.4 0Z" p-id="8406"></path>
+    </svg>
+);
+const SoftwareSvg = () => (
+    <svg width="1em" height="1em" fill="currentColor" xmlns="http://www.w3.org/2000/svg" p-id="10283" viewBox="0 0 1024 1024">
+        <title>software icon</title>
+        <path d="M398.208 113.792v284.448H113.76V113.792h284.448z m0-56.896H113.76c-31.296 0-56.896 25.6-56.896 56.896v284.448c0 31.296 25.6 56.896 56.896 56.896h284.448c31.296 0 56.896-25.6 56.896-56.896V113.792c0-31.296-25.6-56.896-56.896-56.896z m0 568.896v284.448H113.76v-284.448h284.448z m0-56.896H113.76c-31.296 0-56.896 25.6-56.896 56.896v284.448c0 31.296 25.6 56.896 56.896 56.896h284.448c31.296 0 56.896-25.6 56.896-56.896v-284.448c0-31.296-25.6-56.896-56.896-56.896z m512-455.104v284.448h-284.448V113.792h284.448z m0-56.896h-284.448c-31.296 0-56.896 25.6-56.896 56.896v284.448c0 31.296 25.6 56.896 56.896 56.896h284.448c31.296 0 56.896-25.6 56.896-56.896V113.792c0-31.296-25.6-56.896-56.896-56.896z m0 568.896v284.448h-284.448v-284.448h284.448z m0-56.896h-284.448c-31.296 0-56.896 25.6-56.896 56.896v284.448c0 31.296 25.6 56.896 56.896 56.896h284.448c31.296 0 56.896-25.6 56.896-56.896v-284.448c0-31.296-25.6-56.896-56.896-56.896z" p-id="10284"></path>
+    </svg>
+);
+const ChatSvg = () => (
+    <svg width="1em" height="1em" fill="currentColor" xmlns="http://www.w3.org/2000/svg" p-id="15809" viewBox="0 0 1024 1024">
+        <title>chat icon</title>
+        <path d="M920.642991 1.684336h-583.775701c-48.08972 0-87.327103 39.428785-87.327103 87.738617v88.217122H103.596262c-48.328972 0-87.566355 39.419215-87.566355 87.977869V675.935701c0 48.558654 39.237383 87.977869 87.566355 87.977869H133.024299v229.31858a28.901682 28.901682 0 0 0 18.42243 27.159925c3.588785 1.435514 7.17757 2.162841 10.766355 2.162841a29.284486 29.284486 0 0 0 21.293458-9.129869L418.691589 763.674318h268.201869c23.685981 0 44.740187-10.335701 60.770093-26.202916l93.069159 98.552822c5.742056 6.010019 13.398131 9.139439 21.293458 9.13944 3.588785 0 7.17757-0.727327 10.766355-2.162842a29.265346 29.265346 0 0 0 18.42243-27.169495V587.718579H920.642991c48.08972 0 87.327103-39.428785 87.327102-87.738616v-410.55701C1007.730841 41.103551 968.73271 1.684336 920.642991 1.684336zM686.893458 705.019215h-281.839252c-9.809346 0-18.183178 5.292262-23.446729 12.737794L191.401869 919.437159V735.547813c0-0.239252-0.239252-0.478505-0.239252-0.717757 0-0.239252 0.239252-0.478505 0.239252-0.727327 0-16.096897-13.158879-29.322766-29.188785-29.322766H103.596262c-16.029907 0-29.188785-13.216299-29.188785-29.322767V265.617944c0-16.106467 13.158879-29.332336 29.188785-29.332337h145.943925v263.943178c0 48.309832 39.237383 87.729047 87.327103 87.729047h269.876635l101.442991 107.453009c-5.502804 5.761196-12.919626 9.608374-21.293458 9.608374z m262.699065-204.8c0 16.106467-12.919626 29.093084-28.949532 29.093084h-58.616823c-16.029907 0-29.188785 13.206729-29.188785 29.322766v183.889346l-192.358878-204.082243-0.239253-0.239252c-1.914019-1.923589-4.06729-3.129421-6.459813-4.564935-0.957009-0.727327-1.914019-1.684336-3.11028-1.923588-0.957009-0.478505-1.914019-0.239252-2.871028-0.727328a24.757832 24.757832 0 0 0-8.373832-1.684336H336.86729a28.968673 28.968673 0 0 1-28.949533-29.083514V89.422953c0-16.106467 12.919626-29.093084 28.949533-29.093084h583.775701a28.968673 28.968673 0 0 1 28.949532 29.093084v410.796262z" fill="#2E323F" p-id="15810"></path>
+    </svg>
+);
+
+
+const MusicIcon = (props: Partial<CustomIconComponentProps>) => (
+    <Icon component={MusicSvg} {...props} />
+);
+const VideoIcon = (props: Partial<CustomIconComponentProps>) => (
+    <Icon component={VideoSvg} {...props} />
+);
+const GameIcon = (props: Partial<CustomIconComponentProps>) => (
+    <Icon component={GameSvg} {...props} />
+);
+const SoftwareIcon = (props: Partial<CustomIconComponentProps>) => (
+    <Icon component={SoftwareSvg} {...props} />
+);
+const ChatIcon = (props: Partial<CustomIconComponentProps>) => (
+    <Icon component={ChatSvg} {...props} />
+);
+
+
+const items: MenuItem[] = [
+{
+    key: 'home',
+    icon: <HomeOutlined />,
+    label: (
+    <a href={{web_base_url}+'/'}>
+        首页
+    </a>
+    ),
+},
+{
+    key: 'video',
+    icon: <VideoIcon />,
+    label: (
+    <a href={{web_base_url}+'/posts?type=video'}>
+        影视
+    </a>
+    ),
+},
+{
+    key: 'music',
+    icon: <MusicIcon />,
+    label: (
+    <a href={{web_base_url}+'/posts?type=music'}>
+        音乐
+    </a>
+    ),
+},
+{
+    key: 'game',
+    icon: <GameIcon />,
+    label: (
+    <a href={{web_base_url}+'/posts?type=game'}>
+        游戏
+    </a>
+    ),
+},
+{
+    key: 'software',
+    icon: <SoftwareIcon />,
+    label: (
+    <a href={{web_base_url}+'/posts?type=software'}>
+        软件
+    </a>
+    ),
+},
+{
+    key: 'chat',
+    icon: <ChatIcon />,
+    label: (
+    <a href={{web_base_url}+'/posts?type=chat'}>
+        聊天
+    </a>
+    ),
+},
+];
+
+
+
+
+
+const Navbar: React.FC = () => {
+    const [current, setCurrent] = useState('home');
+
+    const onClick: MenuProps['onClick'] = (e) => {
+        console.log('click ', e);
+        setCurrent(e.key);
+    };
+
+    return <Menu className={style.menu} onClick={onClick} selectedKeys={[current]} mode="horizontal" items={items} />;
+}
+
+export default Navbar;
diff --git a/src/components/postsPanel/postsPanel.module.css b/src/components/postsPanel/postsPanel.module.css
new file mode 100644
index 0000000..dc10683
--- /dev/null
+++ b/src/components/postsPanel/postsPanel.module.css
@@ -0,0 +1,38 @@
+.panel{
+    background-color: var(--card-bg);
+    height:100%;
+    width:100%;
+}
+
+.header{
+    border-bottom:1px solid var(--border-color);
+    box-shadow: 1px;
+    height:10%;
+    width:100%;
+    display:flex;
+    justify-content: space-between;
+}
+
+.header .title{
+    font:bold ;
+    color:var(--text-color);
+    
+}
+
+.header .more{
+    font:bold ;
+    color:var(--text-color);
+}
+
+.content .item{
+    display:flex;
+    justify-content:space-between;
+
+}
+.content .item .text{
+    width:70%;
+    overflow-x:hidden;
+    color:var(--text-color);
+}
+
+
diff --git a/src/components/postsPanel/postsPanel.tsx b/src/components/postsPanel/postsPanel.tsx
new file mode 100644
index 0000000..e4f87c0
--- /dev/null
+++ b/src/components/postsPanel/postsPanel.tsx
@@ -0,0 +1,40 @@
+import { useApi } from '@/hooks/request';
+import React, { useCallback } from  'react';
+import request from '@/utils/request'
+import style from './postsPanel.module.css'
+
+
+interface panelProps{
+    name:string,
+    url:string,
+    limit:number
+}
+
+const PostsPanel:React.FC<panelProps> = (props) => {
+    const fenchData = useCallback(() => request.get(props.url), [props.url])
+    const {data} = useApi(fenchData, true);
+
+
+
+    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}>
+                {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>
+                    </div>
+                ))  :(
+                    <div>未查询到相关记录</div>
+                )}
+            </div>
+        </div>
+    )
+}
+
+export default PostsPanel;
\ No newline at end of file
diff --git a/src/components/selfStatus/selfStatus.tsx b/src/components/selfStatus/selfStatus.tsx
index be2a3bc..a5156f4 100644
--- a/src/components/selfStatus/selfStatus.tsx
+++ b/src/components/selfStatus/selfStatus.tsx
@@ -13,7 +13,6 @@
     const downloadTraffic = useAppSelector(state => state.user.downloadTraffic);
     const downloadPoints = useAppSelector(state => state.user.downloadPoints);
     const avatar = useAppSelector(state => state.user.avatar);
-    console.log(avatar)
 
     return (
         <div className={style.container}>
@@ -23,14 +22,14 @@
             <div className={style.right}>
                 <div className={style.info}>
                     <p className={style.userName}>{userName}</p>
-                    <p className={style.role}>角色: {role}</p>
-                    <p className={style.uploadTraffic}>上传量: {uploadTraffic}</p>
-                    <p className={style.downloadTraffic}>下载量: {downloadTraffic}</p>
+                    <p className={style.role}>用户组: {role && role.trim().length? role:'N/A'}</p>
+                    <p className={style.uploadTraffic}>上传量: {uploadTraffic ? uploadTraffic : 0}</p>
+                    <p className={style.downloadTraffic}>下载量: {downloadTraffic ? downloadTraffic : 0}</p>
 
                     <p className={style.shareRatio}>
                         分享率: {uploadTraffic && downloadTraffic ? (uploadTraffic / downloadTraffic).toFixed(2) : "N/A"}
                     </p>
-                    <p className={style.downloadPoints}>下载积分: {downloadPoints}</p>
+                    <p className={style.downloadPoints}>下载积分: {downloadPoints ? downloadPoints : 0}</p>
                 </div>
                 <button className={style.signInButton}>签到</button>
             </div>
diff --git a/src/components/selfStatus/style.module.css b/src/components/selfStatus/style.module.css
index bcf883c..8ed86a7 100644
--- a/src/components/selfStatus/style.module.css
+++ b/src/components/selfStatus/style.module.css
@@ -6,7 +6,7 @@
     padding: 20px;
     border: 1px solid #ccc;
     border-radius: 10px;
-    background-color: #f9f9f9;
+    background-color: var(--card-bg);
     width: 100%;
     height: 100%; /* Adjust height as needed */
     box-sizing: border-box;
@@ -48,6 +48,7 @@
 
 .userName {
     font-size: 18px;
+    color:var(--text-color);
     font-weight: bold;
     margin-bottom: 5px;
 }
@@ -56,6 +57,8 @@
 .downloadTraffic,
 .downloadPoints,
 .shareRatio {
+    color:var(--text-color);
+    margin-top:5px;
     font-size: 14px;
     margin-bottom: 5px;
 }
diff --git a/src/global.css b/src/global.css
index 8d1922e..b49490a 100644
--- a/src/global.css
+++ b/src/global.css
@@ -4,7 +4,7 @@
     height: 100vh;
     width: 100vw;
     background-color: #e6f7ff; /* Light blue background */
-    display: flex;
+    /* display: flex; */
     justify-content: center;
     align-items: center;
     font-family: Arial, sans-serif; /* Optional: Set a global font */
@@ -16,9 +16,24 @@
     height: 100vh;
     width: 100vw;
     background-color: #e6f7ff; /* Light blue background */
-    display: flex;
+    /* display: flex; */
     justify-content: center;
     align-items: center;
     font-family: Arial, sans-serif; /* Optional: Set a global font */
     box-sizing: border-box;
-}
\ No newline at end of file
+}
+
+body.light {
+    --bg-color: #f9f9f9;
+    --text-color: #000000;
+    --card-bg: #ffffff;
+    --border-color: #e0e0e0;
+  }
+  
+body.dark {
+    --bg-color: #2b2b2b;
+    --text-color: #f1f1f1;
+    --card-bg: #1e1e1e;
+    --border-color: #444444;
+  }
+  
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
index 4bd29bd..69441df 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -6,6 +6,13 @@
 import { RouterProvider } from "react-router";
 import './global.css';
 
+if(localStorage.getItem("theme") === null) {
+    localStorage.setItem("theme", "light");
+    document.body.className="light";
+}else{
+    document.body.className=localStorage.getItem("theme")!;
+}
+
 const root = createRoot(document.getElementById('root')!)
 root.render(
     <Provider store={store}>
diff --git a/src/mock/auth.d.ts b/src/mock/auth.d.ts
index 70d0d39..0f7494a 100644
--- a/src/mock/auth.d.ts
+++ b/src/mock/auth.d.ts
@@ -1,3 +1,3 @@
 import type MockAdapter from 'axios-mock-adapter';
 
-export declare function setupAuthMock(mock: MockAdapter): void;
\ No newline at end of file
+export declare function setupAuthMock(mock: MockAdapter): void;
diff --git a/src/mock/index.ts b/src/mock/index.ts
index b07a1be..fabb34e 100644
--- a/src/mock/index.ts
+++ b/src/mock/index.ts
@@ -1,6 +1,8 @@
 import MockAdapter from 'axios-mock-adapter';
 import instance from '@/utils/axios'
 import {setupAuthMock}  from './auth'
+import { setupUserMock } from './user';
+import { setupPostMock } from './post';
 
 // 创建 Mock 实例
 export const mock = new MockAdapter(instance, { 
@@ -14,6 +16,8 @@
 
   // 加载各模块 Mock
   setupAuthMock(mock)
+  setupUserMock(mock)
+  setupPostMock(mock)
   
   console.log('Mock 模块已加载')
 }
diff --git a/src/mock/post.d.ts b/src/mock/post.d.ts
new file mode 100644
index 0000000..9d00ff3
--- /dev/null
+++ b/src/mock/post.d.ts
@@ -0,0 +1,3 @@
+
+import type MockAdapter from 'axios-mock-adapter';
+export declare function setupPostMock(mock: MockAdapter): void;
\ No newline at end of file
diff --git a/src/mock/post.js b/src/mock/post.js
new file mode 100644
index 0000000..abdfc6a
--- /dev/null
+++ b/src/mock/post.js
@@ -0,0 +1,17 @@
+import Mock from 'mockjs';
+import MockAdapter from 'axios-mock-adapter';
+import {hotPosts} 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'
+        }]);
+        return [200, data];
+      });
+}    
diff --git a/src/mock/user.d.ts b/src/mock/user.d.ts
index 0f12476..9d72eb0 100644
--- a/src/mock/user.d.ts
+++ b/src/mock/user.d.ts
@@ -1,3 +1,2 @@
 import type MockAdapter from 'axios-mock-adapter';
-
 export declare function setupUserMock(mock: MockAdapter): void;
\ No newline at end of file
diff --git a/src/mock/user.js b/src/mock/user.js
index e3ae653..c0e1321 100644
--- a/src/mock/user.js
+++ b/src/mock/user.js
@@ -15,7 +15,7 @@
             'uploadTraffic' : 0,
             'downloadTraffic': 0,
             'downloadPoints' : 0,
-            'avatar' : 0,
+            'avatar' : 'https://www.w3school.com.cn/i/photo/tulip.jpg',
         });
         return [200, data];
       });
diff --git a/src/route/index.tsx b/src/route/index.tsx
index d857a36..c56ada3 100644
--- a/src/route/index.tsx
+++ b/src/route/index.tsx
@@ -2,6 +2,7 @@
 import PrivateRoute from './privateRoute'
 import { useSelector } from 'react-redux'
 import Login from '../views/login/login'
+import Frame from '../views/frame/frame'
 import React from 'react'
 import Forum from '../views/forum'
 import { RootState } from '@/store'
@@ -15,8 +16,15 @@
         redirectPath="/login"/>, 
         children: [
             {
-                index: true,
-                element: <Forum /> // 论坛主页面
+                path:'/',
+                element: <Frame/>,
+                children: [
+                    {
+                        index: true,
+                        element:<Forum/>
+
+                    },
+                ]
             },
         ]
     },
diff --git a/src/store/index.ts b/src/store/index.ts
index 5501ef3..57b060a 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,8 +1,10 @@
 import {configureStore} from '@reduxjs/toolkit';
-import userReducer from './userReducer';
+import  userReducer from './userReducer';
+import  settingReducer  from './settingReducer';
 const store = configureStore({
   reducer: {
     user: userReducer,
+    setting:settingReducer,
   }
 });
 
diff --git a/src/store/settingReducer.ts b/src/store/settingReducer.ts
new file mode 100644
index 0000000..ef519b2
--- /dev/null
+++ b/src/store/settingReducer.ts
@@ -0,0 +1,50 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+interface SettingState {
+    theme: string;
+    fontSize: string;
+    showSearch: boolean;
+}
+
+const initialState: SettingState = {
+    theme: localStorage.getItem("theme") || "light",
+    fontSize: localStorage.getItem("font") || "medium",
+    showSearch: false,
+};
+
+const fontSizeMap: Record<string, string> = {
+    small: "12px",
+    medium: "16px",
+    large: "20px",
+};
+document.body.className = initialState.theme; // 设置初始主题
+document.documentElement.style.fontSize = fontSizeMap[initialState.fontSize];
+export const settingSlice = createSlice({
+    name:"setting",
+    initialState,
+    reducers:{
+        toggleSearch: (state) => {
+            state.showSearch = !state.showSearch;
+        },
+        toggleFontSize: (state) => {
+            state.fontSize =
+                state.fontSize === "small"
+                    ? "medium"
+                    : state.fontSize === "medium"
+                    ? "large"
+                    : "small";
+            document.documentElement.style.fontSize = fontSizeMap[state.fontSize]; // 切换字体大小
+            localStorage.setItem("font", state.fontSize); // 保存到 localStorage
+        },
+        toggleTheme: (state) => {
+            const nextTheme = state.theme === "light" ? "dark" : "light";
+            state.theme = nextTheme;
+            document.body.className = nextTheme; // 切换 body 类名用于全局主题
+            localStorage.setItem("theme", nextTheme); // 保存到 localStorage
+            console.log("theme:", nextTheme)
+        },
+    },
+})
+
+export default settingSlice.reducer;
+export const { toggleSearch, toggleFontSize, toggleTheme } = settingSlice.actions;
\ No newline at end of file
diff --git a/src/store/userReducer.ts b/src/store/userReducer.ts
index 61cacb8..bbed95e 100644
--- a/src/store/userReducer.ts
+++ b/src/store/userReducer.ts
@@ -32,7 +32,6 @@
             state.isLogin = true;
         },
         getUserInfo: (state, action) => {
-            
             state.userId = action.payload.userId;
             state.userName = action.payload.userName;
             state.role = action.payload.role;
@@ -40,6 +39,7 @@
             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/views/forum/index.module.css b/src/views/forum/index.module.css
index 53cf91c..5cd586a 100644
--- a/src/views/forum/index.module.css
+++ b/src/views/forum/index.module.css
@@ -1,12 +1,80 @@
-.selfStatus{
-    width:25%;
-    height:45%;
-    position: absolute;
-    right:0%;
-    top:0%;
-    
+/* index.module.css */
+.container {
+    width: 100%;
+    height: 100%;
+    position: relative; /* 新增 */
 }
-.container{
+.up{
+    width: 100%;
+    height: 50%;
+    position: relative; /* 新增 */
+    top: 0%;
+    display: flex;
+    flex-direction: row;
+    border-radius: 8px;
+}
+
+.down{
+    height:48%;
+    width:100%;
+    bottom:0%;
+    display:flex;
+    flex-direction:row;
+}
+
+
+.upright{
+    width:25%;
+}
+.upleft{
+    height:100%;
+    width:75%;
+    margin:5px;
+    display:flex;
+    flex-direction:column;
+}
+
+.upcontent{
+    display:flex;
+    flex-direction: row;
     width:100%;
     height:100%;
+}
+
+.advertisements {
+    width:40%;
+    margin: 5px;
+    border-radius: 8px;
+    overflow: hidden;
+}
+
+.adImage {
+    width: 100%;
+    height: 99%;
+    object-fit: fill;
+    border-radius: 8px;
+}
+
+
+.hotPosts{
+    width:60%;
+    margin:5px;
+}
+
+
+.selfStatus {
+    width: 100%;
+    height: 100%;
+    position: relative; /* 改为绝对定位 */
+    
+    border-radius: 8px;
+}
+
+.newPost,
+.likePost,
+.forsalePost {
+
+    flex:1;
+    margin:5px;
+    border-radius: 8px;
 }
\ No newline at end of file
diff --git a/src/views/forum/index.tsx b/src/views/forum/index.tsx
index dbceffd..345f880 100644
--- a/src/views/forum/index.tsx
+++ b/src/views/forum/index.tsx
@@ -3,14 +3,65 @@
 import SelfStatus from "@/components/selfStatus/selfStatus";
 
 import style from "./index.module.css";
+import Navbar from "@/components/navbar/navbar";
+import PostsPanel from "@/components/postsPanel/postsPanel";
+import { hotPosts } from "@/api/post";
+import { Carousel } from 'antd';
+import ad1 from '&/assets/ad1.png'
+import ad2 from '&/assets/ad2.png'
+import { useEffect } from "react";
 
 export default function Forum() {
+    useEffect(() => {
+        // 禁止滚动
+        document.body.style.overflow = 'hidden';
 
-
+        // 组件卸载时恢复滚动
+        return () => {
+            document.body.style.overflow = 'auto';
+        };
+    }, []);
+    
+    
     return (
         <div className={style.container}>
-            <div className={style.selfStatus}>
-                <SelfStatus />
+            <div className={style.up}>
+               <div className={style.upleft}>
+                <div className={style.navbar}>
+                    <Navbar/>
+                </div>
+                <div className={style.upcontent}>
+                    <div className={style.advertisements}>
+                        <Carousel arrows infinite={false}>
+                            <div>
+                                <img src={ad1} alt="广告1" className={style.adImage} />
+                            </div>
+                            <div>
+                                <img src={ad2} alt="广告2" className={style.adImage} />
+                            </div>
+                        </Carousel>
+                    </div>
+                    <div className={style.hotPosts}>
+                        <PostsPanel name='热门种子' url={hotPosts} limit={5}/>
+                    </div>
+                </div>
+               </div>
+                <div className={style.upright}>
+                    <div className={style.selfStatus}>
+                        <SelfStatus />
+                    </div>
+                </div> 
+            </div>
+            <div className={style.down}>
+                <div className={style.newPost}>
+                    <PostsPanel name='最新发布' url={hotPosts} limit={5}/>
+                </div>
+                <div className={style.likePost}>
+                    <PostsPanel name='猜你喜欢' url={hotPosts} limit={5}/>
+                </div>
+                <div className={style.forsalePost}>
+                    <PostsPanel name='促销种子' url={hotPosts} limit={5}/>
+                </div>
             </div>
         </div>
     );
diff --git a/src/views/frame/frame.module.css b/src/views/frame/frame.module.css
new file mode 100644
index 0000000..91967f8
--- /dev/null
+++ b/src/views/frame/frame.module.css
@@ -0,0 +1,63 @@
+
+
+.header{
+    display:flex;
+    justify-content: space-between;
+    align-items: center;
+    height:8%;
+    background-color: var(--bg-color);
+    width: 100%;
+    overflow:hidden;
+}
+
+.header .logo{
+    width: 100px; /* Set a fixed width for the logo */
+    height: auto; /* Maintain aspect ratio */
+    left:0;
+    margin:10px;
+}
+
+.header .searchInput{
+    border:1px solid rgb(197, 193, 194);
+    width: 60%;
+    height:50%;
+    padding: 5px 10px;
+    border-radius: 5px;
+    font-size: 1rem;
+    color: var(--text-color);
+    background-color: var(--bg-color);
+    transition: all 0.1s ease-in-out;
+}
+
+.header .toollist{
+    align-content: center;
+    right:0;
+    gap:10px;
+    margin-right:10px;
+}
+
+.header .toollist > *{
+    padding: 5px 10px;
+    width:auto;
+    font-size:150%;
+    user-select: none;
+    color: var(--text-color);
+}
+
+.header .toollist > *:hover{
+    background-color: rgb(197, 193, 194);
+    border-radius: 5px;
+    cursor: pointer;
+    font-size:160%;
+    transition: all 0.1s ease-in-out;
+    color: var(--text-color);
+}
+
+.container{
+    height:92vh;
+    background-color:var(--bg-color);
+    display:flex;
+    flex-direction:column;
+    overflow: auto;
+    width:100%;
+}
\ No newline at end of file
diff --git a/src/views/frame/frame.tsx b/src/views/frame/frame.tsx
new file mode 100644
index 0000000..c0c2e0e
--- /dev/null
+++ b/src/views/frame/frame.tsx
@@ -0,0 +1,55 @@
+import React from "react";
+import { Outlet } from "react-router";
+import { useEffect, useState } from "react";
+import { 
+    SearchOutlined, 
+    FontSizeOutlined, 
+    MessageOutlined, 
+    SunOutlined, 
+    MoonOutlined
+ } from "@ant-design/icons";
+import style from "./frame.module.css";
+import logo from "&/assets/logo.png";
+import { useAppDispatch } from "@/hooks/store";
+import { useSelector } from "react-redux";
+const Frame:React.FC = () => {
+
+    const dispatch = useAppDispatch();
+
+    const showSearch = useSelector((state: any) => state.setting.showSearch); 
+    const theme= useSelector((state: any) => state.setting.theme);
+    const toggleSearch = () => {
+        dispatch({ type: "setting/toggleSearch" });
+    }
+
+    const toggleFontSize = () => {
+        dispatch({ type: "setting/toggleFontSize" });
+    };
+
+    const toggleTheme = () => {
+        dispatch({ type: "setting/toggleTheme" });
+    };
+
+
+    return (
+        <div style={{ display: 'block', height: '100vh' }}>
+            <header className={style.header}>
+                <img className={style.logo} src={logo} alt="website logo"></img>
+                {showSearch && (<input className={style.searchInput} placeholder="输入关键词进行搜索"/>)}
+                <div className={style.toollist}>
+                    <SearchOutlined onClick={toggleSearch}/>
+                    <FontSizeOutlined onClick={toggleFontSize}/>
+                    <MessageOutlined />
+                    {theme === 'dark' ? <MoonOutlined onClick={toggleTheme}/> : <SunOutlined onClick={toggleTheme}/>}
+                </div>
+            </header>
+            <div className={style.container}>
+                <Outlet/>
+            </div>
+            
+            
+        </div>
+    );
+}
+
+export default Frame;
\ No newline at end of file
diff --git a/src/views/login/login.tsx b/src/views/login/login.tsx
index 26b7842..bd429c7 100644
--- a/src/views/login/login.tsx
+++ b/src/views/login/login.tsx
@@ -8,21 +8,21 @@
 import { useState } from 'react';
 import { useSelector } from 'react-redux';
 import { useNavigate } from 'react-router';
-import logo from '&/asserts/logo.png';
+import logo from '&/assets/logo.png';
 import { getUserInfo } from '@/api/user';
-
+import debounce from 'lodash/debounce';
 
 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: getUserInfoRefresh } = useApi(async () => await request.get(getUserInfo), false);
+    const { refresh: getUserInfoRefresh } = useApi(() => request.get(getUserInfo), false);
 
     const nav = useNavigate();
-    const handleLogin = async () => {
+    const handleLogin = debounce(async () => {
         try {
-            const res = await postUserLoginRefresh();
+            const res =await postUserLoginRefresh();
             if (res==null ||(res as any).error) {
                 alert('Login failed. Please check your credentials.');
                 return;
@@ -44,14 +44,17 @@
               console.error('Unknown error occurred');
             }
         }
-    };
+    }, 1000) as () => void;
 
+    const handleLogoClick = () => {
+        nav('/');
+    }
     return (
         <div className={style.form}>
-            <img className={style.logo} src={logo} alt="logo"></img>
+            <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.submit} onClick={() => handleLogin()}>登录</button>
             <button className={style.register}>注册</button>
             <button className={style.forget}> 忘记密码</button>
         </div>