add mainView, reward, community pages
Change-Id: I70da6ed3e91ebf4124c2074b6508192a19ed9909
diff --git "a/src/app/reward/reward-detail/\133rewardId\135/page.tsx" "b/src/app/reward/reward-detail/\133rewardId\135/page.tsx"
new file mode 100644
index 0000000..fb8f8b1
--- /dev/null
+++ "b/src/app/reward/reward-detail/\133rewardId\135/page.tsx"
@@ -0,0 +1,364 @@
+'use client';
+
+import React, { useEffect, useState, useRef } from 'react';
+import { Image } from 'primereact/image';
+import { Avatar } from 'primereact/avatar';
+import { Button } from 'primereact/button';
+import { InputText } from "primereact/inputtext";
+// 页面跳转
+import { useParams } from 'next/navigation'
+// 接口传输
+import axios from 'axios';
+// 回复评论
+import { OverlayPanel } from 'primereact/overlaypanel';
+import { Sidebar } from 'primereact/sidebar';
+// 分页
+import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator';
+// 消息提醒
+import { Toast } from 'primereact/toast';
+// 样式
+import './reward-detail.scss';
+
+
+// 评论信息
+interface Comment {
+ commentId: number;
+ userId: number | null;
+ replyId: number;
+ resourceId: number;
+ reawardId: number;
+ content: string;
+ createAt: string;
+}
+// 评论列表
+interface CommentList {
+ total: number; // 评论总数
+ records: Comment[]; // 当前页评论数组
+}
+// 悬赏信息
+interface RewardInfo {
+ rewardId: number;
+ userId: number;
+ rewardPicture: string;
+ rewardName: string;
+ rewardDescription: string;
+ price: number;
+ createAt: string;
+ lastUpdateAt: string;
+ commentNumber: number;
+ communityId: number;
+}
+// 用户信息
+interface UserInfo {
+ userId: number;
+ username: string;
+ avatar: string;
+ signature: string;
+}
+// 新评论接口
+interface NewComment {
+ userId: number;
+ threadId: number;
+ resourceId: number;
+ rewardId: number;
+ replyId: number;
+ content: string;
+ createAt: string;
+}
+
+
+//帖子详情界面
+export default function RewardDetailPage() {
+ // 获取URL参数,页面跳转
+ const params = useParams<{ rewardId: string }>()
+ const rewardId = decodeURIComponent(params.rewardId); // 防止中文路径乱码
+ // 消息提醒
+ const toast = useRef<Toast>(null);
+ // 帖子信息
+ const [rewardInfo, setRewardInfo] = useState<RewardInfo | null>(null);
+ // 发帖人信息
+ const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
+ // 评论人信息
+ const [commentUserInfos, setCommentUserInfos] = useState<Map<number, UserInfo>>(new Map());
+ //评论
+ const [comments, setComments] = useState<Comment[]>([]);
+ const [commentValue, setCommentValue] = useState<string>('');
+ const [totalComments, setTotalComments] = useState<number>(0);
+ // 回复
+ const [replyValue, setReplyValue] = useState<string>('');
+ const [visibleReply, setVisibleReply] = useState<boolean>(false);// 回复评论可视
+ // 评论选择框
+ const ops = useRef<OverlayPanel[]>([]);
+
+ // 分页
+ const [first, setFirst] = useState<number>(0);
+ const [rows, setRows] = useState<number>(5);
+ const onPageChange = (event: PaginatorPageChangeEvent) => {
+ setFirst(event.first);
+ setRows(event.rows);
+ };
+
+ // 获取帖子信息
+ useEffect(() => {
+ fetchRewardInfo();
+ }, [rewardId, rewardInfo]);
+
+ const fetchRewardInfo = async () => {
+ try {
+ const { data } = await axios.get(process.env.PUBLIC_URL +`/reward/info?rewardId=${rewardId}`);
+ setRewardInfo(data);
+ } catch (err) {
+ console.error(err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取悬赏信息失败' });
+ }
+ };
+
+ // 获取发帖人
+ useEffect(() => {
+ if (!rewardInfo) return;
+ // 发帖人
+ axios.get(process.env.PUBLIC_URL +`/user/info?userId=${rewardInfo.userId}`)
+ .then(res => setUserInfo(res.data))
+ .catch(console.error);
+ }, [rewardInfo]);
+
+
+ // 当 rewardId 或分页参数变化时重新拉评论
+ useEffect(() => {
+ if (!rewardId) return;
+
+ fetchComments();
+ }, [rewardId, first, rows]);
+
+
+ //通过评论ID获取评论人信息
+ const getReplyUserName = (replyId: number) => {
+ if (replyId == null || replyId == 0) return '';
+ const replyComment = comments.find(comment => comment.commentId === replyId);
+ if (!replyComment?.userId) return '匿名用户';
+ return "回复 " + commentUserInfos.get(replyComment.userId)?.username || '匿名用户';
+ };
+
+ // 获取评论列表
+ const fetchComments = async () => {
+ try {
+ const pageNumber = first / rows + 1;
+ console.log("当前页" + pageNumber + "size" + rows);
+ const response = await axios.get<CommentList>(
+ process.env.PUBLIC_URL +`/comments`, {
+ params: { id: rewardId, pageNumber, rows, type: 'reward' }
+ }
+ );
+ console.log('获取评论列表:', response.data.records);
+ setComments(response.data.records);
+ setTotalComments(response.data.total);
+ // 拉取评论对应用户信息
+ response.data.records.forEach(comment => {
+ if (comment.userId != null && !commentUserInfos.has(comment.userId)) {
+ axios.get<UserInfo>(
+ process.env.PUBLIC_URL +`/user/info`,
+ { params: { userId: comment.userId } }
+ ).then(res => {
+ setCommentUserInfos(prev => new Map(prev).set(comment.userId!, res.data));
+ });
+ }
+ });
+ } catch (err) {
+ console.error('获取评论失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取评论失败' });
+ }
+ };
+
+ // 回复评论接口
+ const publishReply = async (commentId: number) => {
+ if (!replyValue.trim() || !rewardInfo) return;
+ console.log('发布评论:', commentId);
+ try {
+ const newComment: NewComment = {
+ userId: 22301145,
+ rewardId: rewardInfo.rewardId,
+ threadId: 0,
+ resourceId: 0,
+ replyId: commentId,
+ content: commentValue,
+ createAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ };
+
+ const response = await axios.post(process.env.PUBLIC_URL +'/comment', newComment);
+
+ if (response.status === 200) {
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '回复成功' });
+ // 更新评论列表
+ fetchComments();
+ setVisibleReply(false)
+ // 清空输入框
+ setReplyValue('');
+ }
+ } catch (error) {
+ console.error('发布评论失败:', error);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '回复失败' });
+ }
+ };
+
+ // 发布评论接口
+ const publishComment = async () => {
+ if (!commentValue.trim() || !rewardInfo) return;
+
+ try {
+ const newComment: NewComment = {
+ userId: 22301145,
+ rewardId: rewardInfo.rewardId,
+ threadId: 0,
+ resourceId: 0,
+ replyId: 0, // 直接评论,不是回复
+ content: commentValue,
+ createAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ };
+
+ const response = await axios.post(process.env.PUBLIC_URL +'/comment', newComment);
+
+ if (response.status === 200) {
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '评论成功' });
+ // 更新评论列表
+ fetchComments();
+ // 清空输入框
+ setCommentValue('');
+ }
+ } catch (error) {
+ console.error('发布评论失败:', error);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '发布评论失败' });
+ }
+ };
+
+ // 删除评论接口
+ const deleteComment = async (commentId: number) => {
+ if (!rewardInfo) return;
+
+ try {
+ // 调用 DELETE 接口,URL 中最后一段是要删除的 commentId
+ const response = await axios.delete(
+ process.env.PUBLIC_URL +`/comment?commentId=${commentId}`
+ );
+
+ if (response.status === 200) {
+ fetchComments();
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '删除评论成功' });
+ } else {
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '删除评论失败' });
+ console.error('删除评论失败,状态码:', response.status);
+ }
+ } catch (error) {
+ console.error('删除评论接口报错:', error);
+ }
+ };
+
+ const ReplyHeader = (
+ <div className="flex align-items-center gap-1">
+ <h3>回复评论</h3>
+ </div>
+ );
+ if (!rewardInfo || !userInfo) return <div>Loading...</div>;
+ return (
+ <div className="reward-detail">
+ <Toast ref={toast}></Toast>
+ {/* 帖子头部 */}
+ <div className="reward-header">
+ <h1>{rewardInfo.rewardName}</h1>
+ <span className="post-time">{"最新更新时间:" + rewardInfo.lastUpdateAt}</span>
+ </div>
+
+ {/* 帖子内容 */}
+ <div className="reward-content">
+ <div className="reward-info-container">
+ <div className="user-info">
+ <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <div className="user-meta">
+ <h3>{userInfo.username}</h3>
+ <span>{userInfo.signature}</span>
+ </div>
+ </div>
+
+ {/* 左侧文字内容 */}
+ <div className="reward-info">
+ <p>{rewardInfo.rewardDescription}</p>
+ </div>
+ </div>
+
+
+ {/* 右侧图片+价格+按钮 */}
+ <div className="reward-media">
+ <Image
+ src={process.env.NEXT_PUBLIC_NGINX_URL + "rewards/" + rewardInfo.rewardPicture}
+ alt={rewardInfo.rewardName}
+ width="500"
+ height="400"
+ />
+ <div className="reward-actions">
+ <span className="reward-price">¥{rewardInfo.price}</span>
+ <Button className="submit-bounty">提交悬赏</Button>
+ </div>
+ </div>
+ </div>
+ {/* 评论列表 */}
+ <div className="comments-section">
+ <div className="comments-header">
+ <h2>评论 ({totalComments})</h2>
+ </div>
+ <div className="comments-input">
+ <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <InputText value={commentValue} placeholder="发布你的评论" onChange={(e) => setCommentValue(e.target.value)} />
+ <Button label="发布评论" onClick={publishComment} disabled={!commentValue.trim()} />
+ </div>
+ <div className="comments-list">
+ {comments.map((comment, index) => (
+ <div key={comment.commentId} className="comment-item">
+ <div className="comment-user">
+ <Avatar
+ image={comment.userId ? process.env.NEXT_PUBLIC_NGINX_URL + "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
+ size="normal"
+ shape="circle"
+ />
+ <div className="comment-meta">
+ <span className="username">
+ {comment.userId ? commentUserInfos.get(comment.userId)?.username : '匿名用户'}
+ </span>
+ <div className="comment-time">
+ <span className="floor">#{index + 1}楼</span>
+ <span className="time">{comment.createAt}</span>
+ </div>
+ </div>
+ <i className='pi pi-ellipsis-v' onClick={(e) => ops.current[index].toggle(e)} />
+ </div>
+ <div className="comment-content">
+ {<span className="reply-to">{getReplyUserName(comment.replyId)}</span>}
+ <p>{comment.content}</p>
+ </div>
+ <OverlayPanel // 回调 ref:把实例放到 ops.current 对应的位置
+ ref={el => {
+ if (el) ops.current[index] = el;
+ }}>
+ <Button label="回复" text size="small" onClick={() => setVisibleReply(true)} />
+ {comment.userId === 22301145 &&
+ <Button
+ label="删除"
+ text
+ size="small"
+ onClick={() => { console.log('Deleting comment:', comment.commentId, 'by user:', comment.userId); deleteComment(comment.commentId) }}
+ />
+ }
+ </OverlayPanel>
+ <Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}>
+ <div className="reply-input">
+ <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <InputText value={replyValue} placeholder="发布你的评论" onChange={(e) => setReplyValue(e.target.value)} />
+ <Button label="发布评论" onClick={() => publishReply(comment.commentId)} disabled={!replyValue.trim()} />
+ </div>
+ </Sidebar>
+ </div>
+ ))}
+ {totalComments > 5 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalComments} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />)}
+ </div>
+ </div>
+ </div>
+ );
+}
\ No newline at end of file
diff --git "a/src/app/reward/reward-detail/\133rewardId\135/reward-detail.scss" "b/src/app/reward/reward-detail/\133rewardId\135/reward-detail.scss"
new file mode 100644
index 0000000..a38cd0a
--- /dev/null
+++ "b/src/app/reward/reward-detail/\133rewardId\135/reward-detail.scss"
@@ -0,0 +1,93 @@
+.reward-detail {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem;
+
+ // 把标题和用户信息、发布时间都放到同一个 flex 容器里
+ .reward-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+
+ h1 {
+ order: 1;
+ font-size: 2rem;
+ color: #1a202c;
+ margin: 0;
+ }
+
+ .post-time {
+ order: 3;
+ color: #718096;
+ font-size: 0.875rem;
+ }
+ }
+
+ // 贴子正文,文本在左,图片 + 价格 + 按钮在右
+ .reward-content {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ margin-top: 1rem;
+
+
+ .reward-info-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ width: 50%;
+
+ .user-info {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ }
+
+ // 文本部分
+ .reward-info {
+
+ p {
+ font-size: 1rem;
+ line-height: 1.75;
+ color: #4a5568;
+ margin-bottom: 2rem;
+ }
+ }
+ }
+
+
+
+
+ // 右侧媒体区:图片、价格、提交按钮
+ .reward-media {
+ display: flex;
+ width: 50%;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: flex-end;
+ gap: 2rem;
+
+ img {
+ margin-top: 1rem;
+ border-radius: 0.5rem 0.5rem 0.5rem 0.5rem;
+ }
+
+ .reward-actions {
+ min-width: 120px;
+ display: flex;
+ flex-direction: row;
+ gap: 1rem;
+ margin-bottom: 2rem;
+
+ .reward-price {
+ font-size: 2rem;
+ font-weight: bold;
+ color: #2c3e50;
+ margin-bottom: 0.5rem;
+ }
+
+ }
+ }
+ }
+}
\ No newline at end of file