Files
mini-programs/src/other_pages/comment_reply/index.tsx
张成 1226350099 1
2025-11-14 23:14:18 +08:00

364 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from "react";
import { View, Text, ScrollView, Image, Input } from "@tarojs/components";
import { withAuth, EmptyState, GeneralNavbar } from "@/components";
import commentService, { CommentActivity } from "@/services/commentService";
import { formatShortRelativeTime } from "@/utils/timeUtils";
import Taro from "@tarojs/taro";
import "./index.scss";
// 评论/回复类型定义
interface CommentReplyItem {
id: number;
user_id: number;
user_avatar: string;
user_nickname: string;
action_type: "comment" | "reply"; // 评论了你的球局 / 回复了你的评论
time: string;
content: string;
original_comment?: string; // 被回复的评论内容
activity_image: string;
activity_id: number;
activity_title: string;
parent_id: number | null; // 父评论ID用于回复
game_id: number; // 球局ID
}
const CommentReply = () => {
const [commentList, setCommentList] = useState<CommentReplyItem[]>([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [showReplyInput, setShowReplyInput] = useState(false);
const [replyTarget, setReplyTarget] = useState<CommentReplyItem | null>(null);
const [replyContent, setReplyContent] = useState("");
const [inputFocus, setInputFocus] = useState(false);
useEffect(() => {
getCommentReplyList();
}, []);
// 获取评论和回复列表
const getCommentReplyList = async () => {
if (loading) return;
setLoading(true);
try {
const res = await commentService.getMyActivities({
page: 1,
pageSize: 20,
});
if (res.code === 0 && res.data) {
// 映射数据
const mappedList = res.data.rows.map((item: CommentActivity) => ({
id: item.id,
user_id: item.user?.id || 0,
user_avatar: item.user?.avatar_url || "",
user_nickname: item.user?.nickname || "匿名用户",
action_type: item.type === "reply" ? "reply" as const : "comment" as const,
time: item.create_time,
content: item.content || "",
original_comment: item.parent_comment?.content || "",
activity_image: item.game?.image || "",
activity_id: item.game_id,
activity_title: item.game?.title || "",
parent_id: item.parent_id,
game_id: item.game_id,
}));
setCommentList(mappedList);
}
} catch (e) {
Taro.showToast({
title: "获取列表失败",
icon: "none",
duration: 2000,
});
} finally {
setLoading(false);
}
};
// 处理回复
const handleReply = (e: any, item: CommentReplyItem) => {
e.stopPropagation(); // 阻止事件冒泡
setReplyTarget(item);
setShowReplyInput(true);
setInputFocus(true);
setReplyContent("");
};
// 处理点击评论项(跳转到球局详情)
const handleCommentClick = (item: CommentReplyItem) => {
Taro.navigateTo({
url: `/game_pages/detail/index?id=${item.activity_id}&message_id=${item.id}`,
});
};
// 处理点击用户(跳转到个人页)
const handleUserClick = (e: any, userId: number) => {
e.stopPropagation(); // 阻止事件冒泡
Taro.navigateTo({
url: `/user_pages/other/index?userid=${userId}`,
});
};
// 发送回复
const handleSendReply = async () => {
if (!replyContent.trim() || !replyTarget) {
Taro.showToast({
title: "请输入回复内容",
icon: "none",
duration: 2000,
});
return;
}
try {
// 调用回复接口
// 如果是回复评论parent_id 使用评论的 parent_id 或 id
// 如果是顶级评论parent_id 为 null则使用评论的 id 作为 parent_id
const parentId = replyTarget.parent_id || replyTarget.id;
const res = await commentService.replyComment(
parentId,
replyTarget.user_id,
replyContent.trim()
);
if (res.code === 0) {
Taro.showToast({
title: "回复成功",
icon: "success",
duration: 2000,
});
// 关闭输入框
setShowReplyInput(false);
setReplyTarget(null);
setReplyContent("");
setInputFocus(false);
// 刷新列表
getCommentReplyList();
} else {
throw new Error(res.message || "回复失败");
}
} catch (e: any) {
Taro.showToast({
title: e.message || "回复失败",
icon: "none",
duration: 2000,
});
}
};
// 取消回复
const handleCancelReply = () => {
setShowReplyInput(false);
setReplyTarget(null);
setReplyContent("");
setInputFocus(false);
};
// 输入框失去焦点
const handleInputBlur = () => {
// 延迟执行,避免点击发送按钮时输入框先失焦导致发送失败
setTimeout(() => {
handleCancelReply();
}, 200);
};
// 处理返回
const handleBack = () => {
Taro.navigateBack();
};
// 处理下拉刷新
const handleRefresh = async () => {
setRefreshing(true);
try {
const res = await commentService.getMyActivities({
page: 1,
pageSize: 20,
});
if (res.code === 0 && res.data) {
const mappedList = res.data.rows.map((item: CommentActivity) => ({
id: item.id,
user_id: item.user?.id || 0,
user_avatar: item.user?.avatar_url || "",
user_nickname: item.user?.nickname || "匿名用户",
action_type: item.type === "reply" ? "reply" as const : "comment" as const,
time: item.create_time,
content: item.content || "",
original_comment: item.parent_comment?.content || "",
activity_image: item.game?.image || "",
activity_id: item.game_id,
activity_title: item.game?.title || "",
parent_id: item.parent_id,
game_id: item.game_id,
}));
setCommentList(mappedList);
}
} catch (e) {
Taro.showToast({
title: "刷新失败",
icon: "none",
duration: 2000,
});
} finally {
setRefreshing(false);
}
};
// 渲染评论/回复项
const renderCommentItem = (item: CommentReplyItem) => {
const actionText = item.action_type === "comment" ? "评论了你的球局" : "回复了你的评论";
return (
<View
className="comment-item"
key={item.id}
onClick={() => handleCommentClick(item)}
>
<View className="comment-left">
<Image
className="user-avatar"
src={item.user_avatar || "https://img.yzcdn.cn/vant/cat.jpeg"}
mode="aspectFill"
onClick={(e) => handleUserClick(e, item.user_id)}
/>
<View className="comment-content">
<Text
className="user-nickname"
onClick={(e) => handleUserClick(e, item.user_id)}
>
{item.user_nickname}
</Text>
<View className="action-row">
<Text className="action-text">{actionText}</Text>
<Text className="time-text">{formatShortRelativeTime(item.time)}</Text>
</View>
<Text className="comment-text">{item.content}</Text>
{/* 如果是回复,显示被回复的评论 */}
{item.action_type === "reply" && item.original_comment && (
<View className="original-comment">
<View className="quote-line"></View>
<Text className="original-text">{item.original_comment}</Text>
</View>
)}
{/* 回复按钮 */}
<View className="reply-button" onClick={(e) => handleReply(e, item)}>
<Image
className="reply-icon"
src={require('@/static/message/reply-icon.svg')}
/>
<Text className="reply-text"></Text>
</View>
</View>
</View>
{/* 右侧球局图片 */}
<Image
className="activity-image"
src={item.activity_image}
mode="aspectFill"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({
url: `/game_pages/detail/index?id=${item.game_id}`,
});
}}
/>
</View>
);
};
return (
<View className="comment-reply-container">
{/* 顶部导航栏 */}
<GeneralNavbar
title="收到的评论和回复"
showBack={true}
showAvatar={false}
onBack={handleBack}
/>
{/* 评论列表 */}
<ScrollView
scrollY
className="comment-scroll"
scrollWithAnimation
enhanced
showScrollbar={false}
refresherEnabled={true}
refresherTriggered={refreshing}
onRefresherRefresh={handleRefresh}
>
{commentList.length > 0 ? (
<View className="comment-list">
{commentList.map(renderCommentItem)}
{/* 到底了提示 */}
<View className="bottom-tip">
<Text className="tip-text"></Text>
</View>
</View>
) : (
<EmptyState text="暂无评论和回复" />
)}
</ScrollView>
{/* 回复输入框 */}
<>
{/* 遮罩层 */}
<View
className={`reply-mask ${showReplyInput ? 'show' : ''}`}
onClick={handleCancelReply}
></View>
<View className={`reply-input-container ${showReplyInput ? 'show' : ''}`}>
<View className="reply-input-wrapper">
<View className="input-row">
<Input
className="reply-input"
type="text"
placeholder={`回复 ${replyTarget?.user_nickname || ''}`}
value={replyContent}
focus={inputFocus}
adjustPosition={true}
cursorSpacing={30}
holdKeyboard={false}
onInput={(e) => setReplyContent(e.detail.value)}
onBlur={handleInputBlur}
confirmType="send"
onConfirm={handleSendReply}
/>
<View className="input-actions">
<View
className={`send-btn ${replyContent.trim() ? 'active' : ''}`}
onClick={handleSendReply}
>
<Image
className="send-icon"
src={require('@/static/message/send-icon.svg')}
/>
</View>
</View>
</View>
</View>
</View>
</>
</View>
);
};
export default withAuth(CommentReply);