blob: 0d9230372e8babd5f43cea0d0e91cb8cd30beac2 [file] [log] [blame]
// FriendMoments.js
import React, { useContext, useState, useEffect } from 'react';
import axios from 'axios';
import './FriendMoments.css';
import Header from '../../components/Header';
import { Edit, GoodTwo, Comment } from '@icon-park/react';
import { UserContext } from '../../context/UserContext'; // 引入用户上下文
// 修改后的封面图 URL 拼接函数
const formatImageUrl = (url) => {
if (!url) return '';
const filename = url.split('/').pop(); // 提取文件名部分
return `http://localhost:5011/uploads/dynamic/${filename}`;
};
const FriendMoments = () => {
const [feeds, setFeeds] = useState([]);
const [filteredFeeds, setFilteredFeeds] = useState([]);
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [commentBoxVisibleId, setCommentBoxVisibleId] = useState(null); // 当前显示评论框的动态ID
const [replyToCommentId, setReplyToCommentId] = useState(null); // 当前回复的评论ID
const [replyToUsername, setReplyToUsername] = useState(''); // 当前回复的用户名
const [commentInput, setCommentInput] = useState(''); // 当前输入的评论内容
// 从上下文中获取用户信息
const { user } = useContext(UserContext);
const userId = user?.userId || null; // 从用户上下文中获取userId
const username = user?.username || '未知用户'; // 获取用户名
// Modal state & form fields
const [showModal, setShowModal] = useState(false);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [selectedImages, setSelectedImages] = useState([]);
const [previewUrls, setPreviewUrls] = useState([]);
// 检查用户是否已登录
const isLoggedIn = !!userId;
// 拉取好友动态列表
const fetchFeeds = async () => {
if (!isLoggedIn) {
setLoading(false);
setError('请先登录');
return;
}
setLoading(true);
setError(null);
try {
// 注意这里修改了API路径,使用getAllDynamics接口
const res = await axios.get(`/echo/dynamic/${userId}/getAllDynamics`);
// 检查API返回的数据结构
console.log('API响应数据:', res.data);
// 从响应中提取dynamic数组
const dynamicList = res.data.dynamic || [];
// 将API返回的数据结构转换为前端期望的格式
const formattedFeeds = dynamicList.map(item => ({
postNo: item.dynamic_id, // 使用API返回的dynamic_id作为帖子ID
title: item.title,
postContent: item.content,
imageUrl: item.images, // 使用API返回的images字段
postTime: item.time, // 使用API返回的time字段
postLikeNum: item.likes?.length || 0, // 点赞数
liked: item.likes?.some(like => like.user_id === userId), // 当前用户是否已点赞
user_id: item.user_id, // 发布者ID
username: item.username, // 发布者昵称
avatar_url: item.avatar_url, // 发布者头像
comments: item.comments || [] // 评论列表
}));
setFeeds(formattedFeeds);
setFilteredFeeds(formattedFeeds);
} catch (err) {
console.error('获取动态列表失败:', err);
setError('获取动态列表失败,请稍后重试');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchFeeds();
}, [userId]);
// 搜索处理
const handleSearch = () => {
const q = query.trim().toLowerCase();
if (!q) {
setFilteredFeeds(feeds);
return;
}
setFilteredFeeds(
feeds.filter(f =>
(f.title || '').toLowerCase().includes(q) ||
(f.postContent || '').toLowerCase().includes(q)
)
);
};
const handleReset = () => {
setQuery('');
setFilteredFeeds(feeds);
};
// 对话框内:处理图片选择
const handleImageChange = (e) => {
const files = Array.from(e.target.files);
if (!files.length) return;
const previewUrls = files.map(file => URL.createObjectURL(file));
setSelectedImages(files);
setPreviewUrls(previewUrls);
};
// 对话框内:提交新动态
const handleSubmit = async () => {
if (!isLoggedIn) {
alert('请先登录');
return;
}
if (!content.trim()) {
alert('内容不能为空');
return;
}
try {
// 使用formData格式提交
const formData = new FormData();
formData.append('title', title.trim() || '');
formData.append('content', content.trim());
// 添加图片文件
selectedImages.forEach((file, index) => {
formData.append('image_url', file);
});
// 调用创建动态API
await axios.post(`/echo/dynamic/${userId}/createDynamic`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
// 重置表单
setTitle('');
setContent('');
setSelectedImages([]);
setPreviewUrls([]);
setShowModal(false);
fetchFeeds();
alert('发布成功');
} catch (err) {
console.error('发布失败', err);
alert('发布失败,请稍后重试');
}
};
// 删除动态 - 注意:API文档中未提供删除接口,这里保留原代码
const handleDelete = async (dynamicId) => {
if (!isLoggedIn) {
alert('请先登录');
return;
}
if (!window.confirm('确定要删除这条动态吗?')) return;
try {
// 注意:API文档中未提供删除接口,这里使用原代码中的路径
await axios.delete(`/echo/dynamic/me/deleteDynamic/${dynamicId}`);
fetchFeeds();
alert('删除成功');
} catch (err) {
console.error('删除失败', err);
alert('删除失败,请稍后重试');
}
};
// 点赞动态
const handleLike = async (dynamicId, islike) => {
if (islike) {
handleUnlike(dynamicId);
return
}
if (!isLoggedIn) {
alert('请先登录');
return;
}
// 验证dynamicId是否有效
if (!dynamicId) {
console.error('无效的dynamicId:', dynamicId);
alert('点赞失败:动态ID无效');
return;
}
console.log('当前用户ID:', userId);
console.log('即将点赞的动态ID:', dynamicId);
try {
// 确保参数是整数类型
const requestData = {
userId: parseInt(userId),
dynamicId: parseInt(dynamicId)
};
// 验证参数是否为有效数字
if (isNaN(requestData.userId) || isNaN(requestData.dynamicId)) {
console.error('无效的参数:', requestData);
alert('点赞失败:参数格式错误');
return;
}
console.log('点赞请求数据:', requestData);
const res = await axios.post(`/echo/dynamic/like`, requestData, {
headers: {
'Content-Type': 'application/json' // 明确指定JSON格式
}
});
console.log('点赞API响应:', res.data);
if (res.status === 200) {
// 更新本地状态
feeds.forEach(feed => {
if (feed.postNo === dynamicId) {
feed.postLikeNum = (feed.postLikeNum || 0) + 1;
feed.liked = true;
}
});
setFeeds([...feeds]); // 更新状态以触发重新渲染
} else {
alert(res.data.message || '点赞失败');
}
} catch (err) {
console.error('点赞失败', err);
// 检查错误响应,获取更详细的错误信息
if (err.response) {
console.error('错误响应数据:', err.response.data);
console.error('错误响应状态:', err.response.status);
console.error('错误响应头:', err.response.headers);
}
alert('点赞失败,请稍后重试');
}
};
// 取消点赞
const handleUnlike = async (dynamicId) => {
if (!isLoggedIn) {
alert('请先登录');
return;
}
// 验证dynamicId是否有效
if (!dynamicId) {
console.error('无效的dynamicId:', dynamicId);
alert('取消点赞失败:动态ID无效');
return;
}
// 检查是否已经取消点赞,防止重复请求
const currentFeed = feeds.find(feed => feed.postNo === dynamicId);
if (currentFeed && !currentFeed.liked) {
console.warn('尝试重复取消点赞,已忽略');
return;
}
try {
// 确保参数是整数类型
const requestData = {
userId: parseInt(userId),
dynamicId: parseInt(dynamicId)
};
// 验证参数是否为有效数字
if (isNaN(requestData.userId) || isNaN(requestData.dynamicId)) {
console.error('无效的参数:', requestData);
alert('取消点赞失败:参数格式错误');
return;
}
console.log('取消点赞请求数据:', requestData);
const res = await axios.delete(`/echo/dynamic/unlike`, {
headers: {
'Content-Type': 'application/json' // 明确指定JSON格式
},
data: requestData // 将参数放在data属性中
});
console.log('取消点赞API响应:', res.data);
if (res.status === 200) {
// 更新本地状态
feeds.forEach(feed => {
if (feed.postNo === dynamicId) {
feed.postLikeNum = Math.max(0, (feed.postLikeNum || 0) - 1);
feed.liked = false;
}
});
setFeeds([...feeds]); // 更新状态以触发重新渲染
} else {
alert(res.data.message || '取消点赞失败');
}
} catch (err) {
console.error('取消点赞失败', err);
// 检查错误响应,获取更详细的错误信息
if (err.response) {
console.error('错误响应数据:', err.response.data);
console.error('错误响应状态:', err.response.status);
console.error('错误响应头:', err.response.headers);
}
alert('取消点赞失败,请稍后重试');
}
};
// 评论好友动态
const handleComment = async (dynamicId) => {
if (!isLoggedIn) {
alert('请先登录');
return;
}
if (!commentInput.trim()) {
alert('评论内容不能为空');
return;
}
try {
// 准备请求数据
const requestData = {
content: commentInput.trim()
};
// 如果是回复,添加parent_comment_id
if (replyToCommentId) {
requestData.parent_comment_id = replyToCommentId;
}
const res = await axios.post(`/echo/dynamic/${userId}/feeds/${dynamicId}/comments`, requestData);
if (res.status === 200 || res.status === 201) {
// 成功获取评论数据
const newComment = {
user_id: userId,
username: username,
content: commentInput.trim(),
time: new Date().toISOString(), // 使用当前时间作为评论时间
// 如果是回复,添加parent_comment_id和reply_to_username
...(replyToCommentId && { parent_comment_id: replyToCommentId }),
...(replyToUsername && { reply_to_username: replyToUsername })
};
// 更新本地状态,添加新评论
setFeeds(prevFeeds => {
return prevFeeds.map(feed => {
if (feed.postNo === dynamicId) {
// 确保comments是数组,并且正确合并新评论
const currentComments = Array.isArray(feed.comments) ? feed.comments : [];
return {
...feed,
comments: [...currentComments, newComment]
};
}
return feed;
});
});
// 更新过滤后的动态列表
setFilteredFeeds(prevFeeds => {
return prevFeeds.map(feed => {
if (feed.postNo === dynamicId) {
// 确保comments是数组,并且正确合并新评论
const currentComments = Array.isArray(feed.comments) ? feed.comments : [];
return {
...feed,
comments: [...currentComments, newComment]
};
}
return feed;
});
});
// 清空回复状态
setReplyToCommentId(null);
setReplyToUsername('');
setCommentInput('');
setCommentBoxVisibleId(null); // 关闭评论框
} else {
alert(res.data.error || '评论失败');
}
} catch (err) {
console.error('评论失败', err);
alert('评论失败,请稍后重试');
}
};
// 切换回复框显示状态
const toggleReplyBox = (dynamicId, commentId = null, username = '') => {
// 如果点击的是当前正在回复的评论,关闭回复框
if (commentBoxVisibleId === dynamicId && replyToCommentId === commentId) {
setCommentBoxVisibleId(null);
setReplyToCommentId(null);
setReplyToUsername('');
setCommentInput('');
return;
}
// 显示回复框,设置回复目标
setCommentBoxVisibleId(dynamicId);
setReplyToCommentId(commentId);
setReplyToUsername(username);
setCommentInput(username ? `回复 ${username}: ` : '');
};
return (
<div className="friend-moments-container">
<Header />
<div className="fm-header">
<button className="create-btn" onClick={() => setShowModal(true)}>
<Edit theme="outline" size="18" style={{ marginRight: '6px' }} />
创建动态
</button>
</div>
<div className="feed-list">
{loading ? (
<div className="loading-message">加载中...</div>
) : error ? (
<div className="error-message">{error}</div>
) : !isLoggedIn ? (
<div className="login-prompt">
<p>请先登录查看好友动态</p>
</div>
) : filteredFeeds.length === 0 ? (
<div className="empty-message">暂无动态</div>
) : (
filteredFeeds.map(feed => (
<div className="feed-item" key={feed.postNo || `feed-${Math.random()}`}>
{/* 显示发布者信息 */}
<div className="feed-author">
<img
style={{ width: '70px', height: '70px', borderRadius: '50%' }}
src={feed.avatar_url || 'https://example.com/default-avatar.jpg'}
alt={feed.username || '用户头像'}
/>
<div>
<div style={{ fontWeight: 'bold', fontSize: '20px', marginBottom: '5px' }}>{feed.username || '未知用户'}</div>
<span className="feed-date">{new Date(feed.postTime || Date.now()).toLocaleString()}</span>
</div>
</div>
{feed.title && <h4 style={{ fontWeight: 'bold', fontSize: '18px', margin: '15px 0' }}>{feed.title}</h4>}
<div style={{ margin: '20px 0' }}>{feed.postContent || '无内容'}</div>
{feed.imageUrl && (
<div className="feed-images">
{typeof feed.imageUrl === 'string' ? (
<img src={formatImageUrl(feed.imageUrl)} alt="动态图片" />
) : (
feed.imageUrl.map((url, i) => (
<img key={i} src={formatImageUrl(url)} alt={`动态图${i}`} />
))
)}
</div>
)}
<div className="feed-footer">
<div className="like-container">
<button className="icon-btn" onClick={() => handleLike(feed.postNo, feed.liked, feed.user_id)}>
<GoodTwo theme="outline" size="24" fill={feed.liked ? '#ffa600dd' : '#000000'} />
<span>{feed.postLikeNum || 0}</span>
</button>
<button
className="icon-btn"
onClick={() => {
toggleReplyBox(feed.postNo);
}}
>
<Comment theme="outline" size="24" fill="#333" />评论
{/* <span style={{ fontSize: '14px', color: '#333' }}>评论</span> */}
</button>
</div>
{feed.user_id === userId && (
<button className="delete-btn" onClick={() => handleDelete(feed.postNo)}>
删除
</button>
)}
</div>
{/* 评论输入框 */}
{commentBoxVisibleId === feed.postNo && (
<div className="comment-box">
<textarea
className="comment-input"
placeholder={replyToUsername ? `回复 ${replyToUsername}...` : '请输入评论内容...'}
value={commentInput}
onChange={(e) => setCommentInput(e.target.value)}
/>
<button
className="submit-comment-btn"
onClick={() => handleComment(feed.postNo)}
>
发布评论
</button>
</div>
)}
{/* 评论列表 */}
{Array.isArray(feed.comments) && feed.comments.length > 0 && (
<div className="comments-container">
<h5 style={{ fontWeight: 'bold', fontSize: '18px', marginTop: '10px', marginBottom: '20px' }}>评论 ({feed.comments.length})</h5>
<div className="comments-list">
{feed.comments.map((comment, index) => (
<div className="comment-item" key={index}>
<div className="comment-header">
<span className="comment-user">{comment.username || '用户'}</span>
<span className="comment-time">
{new Date(comment.time || Date.now()).toLocaleString()}
</span>
</div>
<p className="comment-content">
{/* 显示回复格式 */}
{/* {comment.reply_to_username ?
<span className="reply-prefix">{comment.username} 回复 {comment.reply_to_username}:</span> :
<span>{comment.username}:</span>
} */}
{comment.content}
</p>
<button
className="reply-btn"
onClick={() => toggleReplyBox(feed.postNo, comment.id || index, comment.username)}
>
回复
</button>
{/* 嵌套回复 */}
{Array.isArray(comment.replies) && comment.replies.length > 0 && (
<div className="nested-replies">
{comment.replies.map((reply, replyIndex) => (
<div className="reply-item" key={replyIndex}>
<p className="reply-content">
{reply.reply_to_username ?
<span className="reply-prefix">{reply.username} 回复 {reply.reply_to_username}:</span> :
<span>{reply.username}:</span>
}
{reply.content}
</p>
<button
className="reply-btn"
onClick={() => toggleReplyBox(feed.postNo, reply.id || `${index}-${replyIndex}`, reply.username)}
>
回复
</button>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
))
)}
</div>
{/* Modal 对话框 */}
{showModal && (
<div className="modal-overlay" onClick={() => setShowModal(false)}>
<div className="modal-dialog" onClick={e => e.stopPropagation()}>
<h3>发布新动态</h3>
<input
type="text"
placeholder="标题"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<textarea
placeholder="写下你的内容..."
value={content}
onChange={e => setContent(e.target.value)}
/>
<label className="file-label">
选择图片
<input
type="file"
accept="image/*"
multiple
onChange={handleImageChange}
style={{ display: 'none' }}
/>
</label>
<div className="cf-preview">
{previewUrls.map((url, i) => (
<img key={i} src={url} alt={`预览${i}`} />
))}
</div>
<div className="modal-actions">
<button className="btn cancel" onClick={() => setShowModal(false)}>
取消
</button>
<button className="btn submit" onClick={handleSubmit}>
发布
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default FriendMoments;