帖子分类

Change-Id: I17bafbfe3c1c8fd26c1e12499cb3c17cd1738e23
diff --git a/src/views/forum/index.tsx b/src/views/forum/index.tsx
index 345f880..83d791f 100644
--- a/src/views/forum/index.tsx
+++ b/src/views/forum/index.tsx
@@ -5,7 +5,7 @@
 import style from "./index.module.css";
 import Navbar from "@/components/navbar/navbar";
 import PostsPanel from "@/components/postsPanel/postsPanel";
-import { hotPosts } from "@/api/post";
+import { getHotPosts, getLikePosts} from "@/api/post";
 import { Carousel } from 'antd';
 import ad1 from '&/assets/ad1.png'
 import ad2 from '&/assets/ad2.png'
@@ -42,7 +42,7 @@
                         </Carousel>
                     </div>
                     <div className={style.hotPosts}>
-                        <PostsPanel name='热门种子' url={hotPosts} limit={5}/>
+                        <PostsPanel name='热门种子' url={getHotPosts} limit={5}/>
                     </div>
                 </div>
                </div>
@@ -54,13 +54,13 @@
             </div>
             <div className={style.down}>
                 <div className={style.newPost}>
-                    <PostsPanel name='最新发布' url={hotPosts} limit={5}/>
+                    <PostsPanel name='最新发布' url={getHotPosts} limit={5}/>
                 </div>
                 <div className={style.likePost}>
-                    <PostsPanel name='猜你喜欢' url={hotPosts} limit={5}/>
+                    <PostsPanel name='猜你喜欢' url={getLikePosts} limit={5}/>
                 </div>
                 <div className={style.forsalePost}>
-                    <PostsPanel name='促销种子' url={hotPosts} limit={5}/>
+                    <PostsPanel name='促销种子' url={getHotPosts} limit={5}/>
                 </div>
             </div>
         </div>
diff --git a/src/views/frame/frame.tsx b/src/views/frame/frame.tsx
index c0c2e0e..f41e53f 100644
--- a/src/views/frame/frame.tsx
+++ b/src/views/frame/frame.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { use } from "react";
 import { Outlet } from "react-router";
 import { useEffect, useState } from "react";
 import { 
@@ -12,8 +12,13 @@
 import logo from "&/assets/logo.png";
 import { useAppDispatch } from "@/hooks/store";
 import { useSelector } from "react-redux";
+
+import { checkAndRefreshToken } from "@/utils/jwt";
 const Frame:React.FC = () => {
 
+    useEffect(() => {
+        checkAndRefreshToken();
+    }, []);
     const dispatch = useAppDispatch();
 
     const showSearch = useSelector((state: any) => state.setting.showSearch); 
diff --git a/src/views/login/login.tsx b/src/views/login/login.tsx
index bd429c7..562f178 100644
--- a/src/views/login/login.tsx
+++ b/src/views/login/login.tsx
@@ -11,38 +11,39 @@
 import logo from '&/assets/logo.png';
 import { getUserInfo } from '@/api/user';
 import debounce from 'lodash/debounce';
+import {message} from 'antd';
+
 
 const Login: React.FC = () => {
     const [email, setEmail] = useState('');
     const [password, setPassword] = useState('');
     const dispatch = useAppDispatch();
-    const { refresh: postUserLoginRefresh } = useApi(() => request.post(postUserLogin, {}), false);
+    const { refresh: postUserLoginRefresh } = useApi(() => request.post(postUserLogin, {email, password}), false);
     const { refresh: getUserInfoRefresh } = useApi(() => request.get(getUserInfo), false);
+    const [messageApi, contextHolder] = message.useMessage();
 
     const nav = useNavigate();
+    const showErrorMessage = async (message: string) => {
+        messageApi.error(message);
+    };
     const handleLogin = debounce(async () => {
         try {
-            const res =await postUserLoginRefresh();
-            if (res==null ||(res as any).error) {
-                alert('Login failed. Please check your credentials.');
-                return;
+            const res = await postUserLoginRefresh();
+            console.log(res);
+            if (res == null || (res as any).error) {
+                throw new Error('登录失败');
             }
             dispatch({ type: "user/login", payload: res });
-            
+
             const userInfo = await getUserInfoRefresh();
-            if (userInfo==null || (userInfo as any).error) {
-                alert('Failed to fetch user information.');
-                return;
+            if (userInfo == null || (userInfo as any).error) {
+                throw new Error('获取用户信息失败');
             }
             dispatch({ type: "user/getUserInfo", payload: userInfo });
             nav('/');
         } catch (error) {
-            alert('An unexpected error occurred. Please try again later.');
-            if (error instanceof Error) {
-              console.error(error.message); // 明确访问 message 属性
-            } else {
-              console.error('Unknown error occurred');
-            }
+            // 将错误信息传递给一个异步函数
+            showErrorMessage('登录失败,请检查您的用户名和密码');
         }
     }, 1000) as () => void;
 
@@ -51,6 +52,7 @@
     }
     return (
         <div className={style.form}>
+            {contextHolder}
             <img className={style.logo} src={logo} alt="logo" onClick={handleLogoClick}></img>
             <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className={style.email} placeholder="Enter your email" />
             <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className={style.password} placeholder="Enter your password" />
diff --git a/src/views/postDetail/postDetail.module.css b/src/views/postDetail/postDetail.module.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/views/postDetail/postDetail.module.css
diff --git a/src/views/postDetail/postDetail.tsx b/src/views/postDetail/postDetail.tsx
new file mode 100644
index 0000000..a40fb68
--- /dev/null
+++ b/src/views/postDetail/postDetail.tsx
@@ -0,0 +1,114 @@
+import React, { useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import styles from './PostDetail.module.css';
+import { Card, List, Typography, Button, Input, Spin, Empty } from 'antd';
+type CommentProps = {
+	children?: React.ReactNode;
+};
+import { getPostDetail } from '@/api/post';
+import { getPostComments } from '@/api/comment';
+import { useSearchParams } from 'react-router-dom';
+import request from '@/utils/request';
+import { useApi } from '@/hooks/request';
+import Navbar from '@/components/navbar/navbar';
+
+const { Title, Text, Paragraph } = Typography;
+const { TextArea } = Input;
+
+export interface PostResponse {
+	createdAt?: number;
+	hotScore?: number;
+	lastCalculated?: number;
+	postContent?: string;
+	postId?: number;
+	postTitle?: string;
+	postType?: string;
+	userId?: number;
+	viewCount?: number;
+	[property: string]: any;
+}
+
+export interface CommentResponse {
+	commentId?: number;
+	content?: string;
+	createdAt?: number;
+	parentCommentId?: number | null;
+	postId?: number;
+	replies?: CommentResponse[];
+	userId?: number;
+	[property: string]: any;
+}
+
+const PostDetail: React.FC = () => {
+	const [searchParams] = useSearchParams();
+	const postId = searchParams.get('postId');
+	const { refresh: getPostDetailRefresh } = useApi(() => request.get(getPostDetail + `/${postId}`), false);
+	const { refresh: getPostCommentsRefresh } = useApi(() => request.get(getPostComments + `/${postId}`), false);
+	const [post, setPost] = useState<PostResponse | null>(null);
+	const [comments, setComments] = useState<CommentResponse[]>([]);
+	const [newComment, setNewComment] = useState<string>('');
+	const [loading, setLoading] = useState<boolean>(true);
+
+	useEffect(() => {
+		console.log('postId', postId);
+		if (!postId) return;
+		const fetchData = async () => {
+			setLoading(true);
+			const res = await getPostDetailRefresh();
+			if (res == null || (res as any).error) {
+				setLoading(false);
+				return;
+			}
+			setPost(res as PostResponse);
+			await getPostCommentsRefresh();
+			setComments(res as CommentResponse[]);
+			setLoading(false);
+		};
+		fetchData();
+	}, [postId]);
+
+	if (loading) return <div className={styles.center}><Spin /></div>;
+	if (!post) return <div className={styles.center}><Empty description="未找到帖子" /></div>;
+
+	return (
+		<div className={styles.container}>
+			<div className={styles.nav}>
+				<Navbar current={post.postType} />
+			</div>
+			<div className={styles.content}>
+				<div className={styles.postDetail}>
+						
+				</div >
+				<Card title={post.postTitle} className={styles.card}>
+					<Paragraph>{post.postContent}</Paragraph>
+					<div className={styles.actions}>
+						<Button type="primary" onClick={() => setNewComment('')}>评论</Button>
+					</div>
+				</Card>
+
+				<List
+					className={styles.commentList}
+					header={<Title level={4}>评论区</Title>}
+					dataSource={comments}
+					renderItem={(item) => (
+						<List.Item key={item.commentId}>
+							<List.Item.Meta
+								title={<Text strong>{item.userId}</Text>}
+								description={<Text>{item.content}</Text>}
+							/>
+						</List.Item>
+					)}
+				/>
+
+				<TextArea
+					rows={4}
+					value={newComment}
+					onChange={(e) => setNewComment(e.target.value)}
+					placeholder="写下你的评论..."
+				/>
+				</div>
+		</div>
+	);
+};
+
+export default PostDetail;
\ No newline at end of file
diff --git a/src/views/postDetail/posterInfo.tsx b/src/views/postDetail/posterInfo.tsx
new file mode 100644
index 0000000..8fe381c
--- /dev/null
+++ b/src/views/postDetail/posterInfo.tsx
@@ -0,0 +1,37 @@
+import { PositionType } from 'antd/es/image/style';
+import request from '@/utils/request';
+import { useApi } from '@/hooks/request';
+import React from 'react';
+import { useNavigate } from 'react-router';
+import { postFollowUser } from '@/api/user';
+interface PosterInfoProps {
+    userId: number;
+    userName: string;
+    role:string;
+    avatar: string;
+}
+
+const PosterInfo: React.FC<PosterInfoProps> = (prop:PosterInfoProps) => {
+    const nav = useNavigate();
+    const { refresh } = useApi(()=>request.post(postFollowUser+`userId=${prop.userId}`), false);
+    const handleClick = () => {
+        nav(`/homepage?userId=${prop.userId}`);
+    }
+
+    const handleFollow = () => {
+        refresh()
+    }
+    const { userId, userName, role, avatar } = prop;
+    return (
+        <>
+            <div className="poster-info" onClick={handleClick}>
+                <img src={avatar}></img>
+                <p>{userName}</p>
+                <p>{role}</p>
+            </div>
+            <button className="poster-info-button" />关注
+
+        </>
+
+    );
+}
\ No newline at end of file
diff --git a/src/views/postList/postList.module.css b/src/views/postList/postList.module.css
new file mode 100644
index 0000000..6290106
--- /dev/null
+++ b/src/views/postList/postList.module.css
@@ -0,0 +1,88 @@
+.container {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: row;
+    position: relative;
+}
+
+.left {
+    flex: 3;
+    display: flex;
+    flex-direction: column;
+    margin: 5px;
+}
+
+.navbar {
+    height: 60px;
+    background-color: #f5f5f5;
+    border-bottom: 1px solid #ddd;
+    border-radius: 8px;
+}
+
+.content {
+    flex: 1;
+    overflow-y: auto; /* 允许垂直滚动 */
+    padding: 10px;
+    background-color: #fff;
+    border-radius: 8px;
+    border: 1px solid #ddd;
+}
+
+.contentItem {
+    position: relative;
+    margin-bottom: 15px;
+    padding: 10px;
+    border: 1px solid #ddd;
+    border-radius: 5px;
+    background-color: #f9f9f9;
+}
+
+.contentItem h3 {
+    margin: 0 0 5px;
+    font-size: 18px;
+    color: #333;
+}
+
+.contentItem p {
+    margin: 0;
+    font-size: 14px;
+    color: #666;
+}
+
+.contentItem .createDate {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    font-size: 12px;
+    color: #999;
+}
+
+.noData {
+    text-align: center;
+    color: #999;
+    margin-top: 20px;
+}
+
+.right {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    margin: 5px;
+}
+
+.selfStatus {
+    margin-bottom: 20px;
+    background-color: #fff;
+    border-radius: 8px;
+    border: 1px solid #ddd;
+    padding: 10px;
+}
+
+.filter {
+    flex: 1;
+    background-color: #fff;
+    border: 1px solid #ddd;
+    border-radius: 8px;
+    padding: 10px;
+}
\ No newline at end of file
diff --git a/src/views/postList/postList.tsx b/src/views/postList/postList.tsx
new file mode 100644
index 0000000..c08b7cf
--- /dev/null
+++ b/src/views/postList/postList.tsx
@@ -0,0 +1,82 @@
+import React from "react";
+import style from "./postList.module.css";
+import SelfStatus from "@/components/selfStatus/selfStatus";
+import Navbar from "@/components/navbar/navbar";
+import PostsPanel from "@/components/postsPanel/postsPanel";
+import { getPosts, unknownAPI } from "@/api/post";
+import { Form } from "antd"
+import { useApi } from "@/hooks/request";
+import request from "@/utils/request";
+import { Pagination, PaginationProps } from "antd";
+import { set } from "lodash";
+import { useEffect } from "react";
+import { useNavigate, useSearchParams } from "react-router";
+
+
+const PostList:React.FC = () => {
+    const [searchParams] = useSearchParams();
+    const type = searchParams.get("type") || ""; 
+    const nav = useNavigate();
+
+    if(type in ['video', 'music', 'game', 'software']) {
+        nav('/')
+    }
+
+    const {data:postList, refresh:getPostList} = useApi(() => request.get(getPosts + `?tags=${[type]}&page=${currentPage}&pageSize=${pageSize}`), false);
+    const [currentPage, setCurrentPage] = React.useState(1);
+    const [pageSize, setPageSize] = React.useState(10);
+    const handlePageChange = (page:number, size?:number) => {
+        setCurrentPage(page);
+        if(size) setPageSize(size);
+        console.log(page, size);
+    };
+
+    const handlePostClick = (postId:number) => {
+        nav(`/postsDetail?postId=${postId}`);
+    }
+
+    useEffect(() => {
+        getPostList();
+    },[currentPage, pageSize]);
+
+    return (
+        <div className={style.container}>
+            <div className={style.left}>
+                <div className={style.navbar}>
+                    <Navbar current={type}/>
+                </div>
+                <div className={style.content}>
+                    {postList && postList.length > 0 ? (
+                        postList.map((post: { postId: number; postTitle: string; postContent: string; createdAt:string }) => (
+                            <div key={post.postId} className={style.contentItem} onClick={() => handlePostClick(post.postId)}>
+                                <h3>{post.postTitle}</h3>
+                                <p>{post.postContent.substring(0, 20)}</p>
+                                <p className={style.createDate}>{post.createdAt}</p>
+                            </div>
+                        ))
+                    ) : (
+                        <div className={style.noData}>未查询到相关帖子</div>
+                    )}
+
+                    <Pagination
+                        showSizeChanger
+                        onShowSizeChange={handlePageChange}
+                        defaultCurrent={1}
+                        total={500}
+                        onChange={handlePageChange}
+                    />
+                </div>
+            </div>
+            <div className={style.right}>
+                <div className={style.selfStatus}>
+                    <SelfStatus/>
+                </div>
+                <div className={style.filter}>
+                    
+                </div> 
+            </div>
+        </div>
+    )
+}
+
+export default PostList;
\ No newline at end of file