blob: c642772dd3df57e2d66848134e140b2aaace3ed1 [file] [log] [blame]
Krishyab5ef96d2025-06-05 13:57:05 +08001import React, { useContext, useState, useEffect } from 'react';
Krishyaf1d0ea82025-05-03 17:01:58 +08002import axios from 'axios';
3import './FriendMoments.css';
4import Header from '../../components/Header';
Krishyab5ef96d2025-06-05 13:57:05 +08005import { Edit, GoodTwo, Comment } from '@icon-park/react';
6import { UserContext } from '../../context/UserContext'; // 引入用户上下文
Krishyae71688a2025-04-10 21:25:17 +08007
22301009e68c0dd2025-06-05 15:28:07 +08008// 修改后的封面图 URL 拼接函数
9const formatImageUrl = (url) => {
10 if (!url) return '';
11 const filename = url.split('/').pop(); // 提取文件名部分
223010094158f3a2025-06-06 19:59:10 +080012 return `http://localhost:5011/uploads/dynamic/${filename}`;
22301009e68c0dd2025-06-05 15:28:07 +080013};
14
Krishyaf1d0ea82025-05-03 17:01:58 +080015const FriendMoments = () => {
16 const [feeds, setFeeds] = useState([]);
17 const [filteredFeeds, setFilteredFeeds] = useState([]);
18 const [query, setQuery] = useState('');
Krishyab5ef96d2025-06-05 13:57:05 +080019 const [loading, setLoading] = useState(true);
20 const [error, setError] = useState(null);
21 const [commentBoxVisibleId, setCommentBoxVisibleId] = useState(null); // 当前显示评论框的动态ID
22 const [commentInput, setCommentInput] = useState(''); // 当前输入的评论内容
23
24 // 从上下文中获取用户信息
25 const { user } = useContext(UserContext);
26 const userId = user?.userId || null; // 从用户上下文中获取userId
27 const username = user?.username || '未知用户'; // 获取用户名
Krishyaf1d0ea82025-05-03 17:01:58 +080028
29 // Modal state & form fields
30 const [showModal, setShowModal] = useState(false);
31 const [title, setTitle] = useState('');
32 const [content, setContent] = useState('');
Krishya8f2fec82025-06-04 21:54:46 +080033 const [selectedImages, setSelectedImages] = useState([]);
Krishyab5ef96d2025-06-05 13:57:05 +080034 const [previewUrls, setPreviewUrls] = useState([]);
35
36 // 检查用户是否已登录
37 const isLoggedIn = !!userId;
Krishyaf1d0ea82025-05-03 17:01:58 +080038
39 // 拉取好友动态列表
40 const fetchFeeds = async () => {
Krishyab5ef96d2025-06-05 13:57:05 +080041 if (!isLoggedIn) {
42 setLoading(false);
43 setError('请先登录');
44 return;
45 }
46
47 setLoading(true);
48 setError(null);
Krishyaf1d0ea82025-05-03 17:01:58 +080049 try {
Krishyab5ef96d2025-06-05 13:57:05 +080050 // 注意这里修改了API路径,使用getAllDynamics接口
Krishya8f2fec82025-06-04 21:54:46 +080051 const res = await axios.get(`/echo/dynamic/${userId}/getAllDynamics`);
Krishyab5ef96d2025-06-05 13:57:05 +080052
53 // 检查API返回的数据结构
54 console.log('API响应数据:', res.data);
55
56 // 从响应中提取dynamic数组
57 const dynamicList = res.data.dynamic || [];
58
59 // 将API返回的数据结构转换为前端期望的格式
60 const formattedFeeds = dynamicList.map(item => ({
61 postNo: item.dynamic_id, // 使用API返回的dynamic_id作为帖子ID
62 title: item.title,
63 postContent: item.content,
64 imageUrl: item.images, // 使用API返回的images字段
65 postTime: item.time, // 使用API返回的time字段
66 postLikeNum: item.likes?.length || 0, // 点赞数
67 liked: item.likes?.some(like => like.user_id === userId), // 当前用户是否已点赞
68 user_id: item.user_id, // 发布者ID
69 username: item.username, // 发布者昵称
70 avatar_url: item.avatar_url, // 发布者头像
71 comments: item.comments || [] // 评论列表
72 }));
73
74 setFeeds(formattedFeeds);
75 setFilteredFeeds(formattedFeeds);
Krishyaf1d0ea82025-05-03 17:01:58 +080076 } catch (err) {
77 console.error('获取动态列表失败:', err);
Krishyab5ef96d2025-06-05 13:57:05 +080078 setError('获取动态列表失败,请稍后重试');
79 } finally {
80 setLoading(false);
Krishyaf1d0ea82025-05-03 17:01:58 +080081 }
82 };
83
84 useEffect(() => {
85 fetchFeeds();
Krishya8f2fec82025-06-04 21:54:46 +080086 }, [userId]);
Krishyaf1d0ea82025-05-03 17:01:58 +080087
88 // 搜索处理
89 const handleSearch = () => {
90 const q = query.trim().toLowerCase();
Krishya8f2fec82025-06-04 21:54:46 +080091 if (!q) {
92 setFilteredFeeds(feeds);
93 return;
94 }
Krishyaf1d0ea82025-05-03 17:01:58 +080095 setFilteredFeeds(
Krishya8f2fec82025-06-04 21:54:46 +080096 feeds.filter(f =>
97 (f.title || '').toLowerCase().includes(q) ||
98 (f.postContent || '').toLowerCase().includes(q)
99 )
Krishyaf1d0ea82025-05-03 17:01:58 +0800100 );
101 };
Krishya8f2fec82025-06-04 21:54:46 +0800102
Krishyaf1d0ea82025-05-03 17:01:58 +0800103 const handleReset = () => {
104 setQuery('');
105 setFilteredFeeds(feeds);
106 };
107
Krishya8f2fec82025-06-04 21:54:46 +0800108 // 对话框内:处理图片选择
109 const handleImageChange = (e) => {
Krishyaf1d0ea82025-05-03 17:01:58 +0800110 const files = Array.from(e.target.files);
111 if (!files.length) return;
Krishya8f2fec82025-06-04 21:54:46 +0800112
Krishya8f2fec82025-06-04 21:54:46 +0800113 const previewUrls = files.map(file => URL.createObjectURL(file));
114
115 setSelectedImages(files);
Krishyab5ef96d2025-06-05 13:57:05 +0800116 setPreviewUrls(previewUrls);
Krishyaf1d0ea82025-05-03 17:01:58 +0800117 };
118
119 // 对话框内:提交新动态
120 const handleSubmit = async () => {
Krishyab5ef96d2025-06-05 13:57:05 +0800121 if (!isLoggedIn) {
122 alert('请先登录');
123 return;
124 }
125
Krishyaf1d0ea82025-05-03 17:01:58 +0800126 if (!content.trim()) {
127 alert('内容不能为空');
128 return;
129 }
Krishya8f2fec82025-06-04 21:54:46 +0800130
Krishyaf1d0ea82025-05-03 17:01:58 +0800131 try {
Krishyab5ef96d2025-06-05 13:57:05 +0800132 // 使用formData格式提交
Krishya8f2fec82025-06-04 21:54:46 +0800133 const formData = new FormData();
Krishya8f2fec82025-06-04 21:54:46 +0800134 formData.append('title', title.trim() || '');
135 formData.append('content', content.trim());
136
137 // 添加图片文件
138 selectedImages.forEach((file, index) => {
139 formData.append('image_url', file);
140 });
141
Krishyab5ef96d2025-06-05 13:57:05 +0800142 // 调用创建动态API
Krishya8f2fec82025-06-04 21:54:46 +0800143 await axios.post(`/echo/dynamic/${userId}/createDynamic`, formData, {
144 headers: {
145 'Content-Type': 'multipart/form-data'
146 }
147 });
148
Krishyaf1d0ea82025-05-03 17:01:58 +0800149 // 重置表单
150 setTitle('');
151 setContent('');
Krishya8f2fec82025-06-04 21:54:46 +0800152 setSelectedImages([]);
Krishyab5ef96d2025-06-05 13:57:05 +0800153 setPreviewUrls([]);
Krishyaf1d0ea82025-05-03 17:01:58 +0800154 setShowModal(false);
155 fetchFeeds();
Krishya8f2fec82025-06-04 21:54:46 +0800156 alert('发布成功');
Krishyaf1d0ea82025-05-03 17:01:58 +0800157 } catch (err) {
158 console.error('发布失败', err);
159 alert('发布失败,请稍后重试');
160 }
161 };
162
Krishyab5ef96d2025-06-05 13:57:05 +0800163 // 删除动态 - 注意:API文档中未提供删除接口,这里保留原代码
Krishya8f2fec82025-06-04 21:54:46 +0800164 const handleDelete = async (dynamicId) => {
Krishyab5ef96d2025-06-05 13:57:05 +0800165
166 if (!isLoggedIn) {
167 alert('请先登录');
168 return;
169 }
170
Krishyaf1d0ea82025-05-03 17:01:58 +0800171 if (!window.confirm('确定要删除这条动态吗?')) return;
172 try {
Krishyab5ef96d2025-06-05 13:57:05 +0800173 // 注意:API文档中未提供删除接口,这里使用原代码中的路径
Krishya8f2fec82025-06-04 21:54:46 +0800174 await axios.delete(`/echo/dynamic/me/deleteDynamic/${dynamicId}`);
Krishyaf1d0ea82025-05-03 17:01:58 +0800175 fetchFeeds();
Krishya8f2fec82025-06-04 21:54:46 +0800176 alert('删除成功');
Krishyaf1d0ea82025-05-03 17:01:58 +0800177 } catch (err) {
178 console.error('删除失败', err);
Krishya8f2fec82025-06-04 21:54:46 +0800179 alert('删除失败,请稍后重试');
180 }
181 };
182
183 // 点赞动态
Krishyab5ef96d2025-06-05 13:57:05 +0800184 const handleLike = async (dynamicId,islike) => {
185 if (islike) {
186 handleUnlike(dynamicId);
187 return
188 }
189 if (!isLoggedIn) {
190 alert('请先登录');
191 return;
192 }
193
194 // 验证dynamicId是否有效
195 if (!dynamicId) {
196 console.error('无效的dynamicId:', dynamicId);
197 alert('点赞失败:动态ID无效');
198 return;
199 }
200
Krishyab5ef96d2025-06-05 13:57:05 +0800201
202 console.log('当前用户ID:', userId);
203 console.log('即将点赞的动态ID:', dynamicId);
204
Krishya8f2fec82025-06-04 21:54:46 +0800205 try {
Krishyab5ef96d2025-06-05 13:57:05 +0800206 // 确保参数是整数类型
207 const requestData = {
208 userId: parseInt(userId),
209 dynamicId: parseInt(dynamicId)
210 };
211
212 // 验证参数是否为有效数字
213 if (isNaN(requestData.userId) || isNaN(requestData.dynamicId)) {
214 console.error('无效的参数:', requestData);
215 alert('点赞失败:参数格式错误');
216 return;
217 }
218
219 console.log('点赞请求数据:', requestData);
220
221 const res = await axios.post(`/echo/dynamic/like`, requestData, {
222 headers: {
223 'Content-Type': 'application/json' // 明确指定JSON格式
224 }
Krishya8f2fec82025-06-04 21:54:46 +0800225 });
226
Krishyab5ef96d2025-06-05 13:57:05 +0800227 console.log('点赞API响应:', res.data);
228
Krishya8f2fec82025-06-04 21:54:46 +0800229 if (res.status === 200) {
230 // 更新本地状态
Krishyab5ef96d2025-06-05 13:57:05 +0800231 feeds.forEach(feed => {
Krishya8f2fec82025-06-04 21:54:46 +0800232 if (feed.postNo === dynamicId) {
Krishyab5ef96d2025-06-05 13:57:05 +0800233 feed.postLikeNum = (feed.postLikeNum || 0) + 1;
234 feed.liked = true;
Krishya8f2fec82025-06-04 21:54:46 +0800235 }
Krishyab5ef96d2025-06-05 13:57:05 +0800236 });
237 setFeeds([...feeds]); // 更新状态以触发重新渲染
Krishya8f2fec82025-06-04 21:54:46 +0800238 } else {
239 alert(res.data.message || '点赞失败');
240 }
241 } catch (err) {
242 console.error('点赞失败', err);
Krishyab5ef96d2025-06-05 13:57:05 +0800243
244 // 检查错误响应,获取更详细的错误信息
245 if (err.response) {
246 console.error('错误响应数据:', err.response.data);
247 console.error('错误响应状态:', err.response.status);
248 console.error('错误响应头:', err.response.headers);
249 }
250
Krishya8f2fec82025-06-04 21:54:46 +0800251 alert('点赞失败,请稍后重试');
252 }
253 };
254
255 // 取消点赞
256 const handleUnlike = async (dynamicId) => {
Krishyab5ef96d2025-06-05 13:57:05 +0800257 if (!isLoggedIn) {
258 alert('请先登录');
259 return;
260 }
261
262 // 验证dynamicId是否有效
263 if (!dynamicId) {
264 console.error('无效的dynamicId:', dynamicId);
265 alert('取消点赞失败:动态ID无效');
266 return;
267 }
268
269 // 检查是否已经取消点赞,防止重复请求
270 const currentFeed = feeds.find(feed => feed.postNo === dynamicId);
271 if (currentFeed && !currentFeed.liked) {
272 console.warn('尝试重复取消点赞,已忽略');
273 return;
274 }
275
Krishya8f2fec82025-06-04 21:54:46 +0800276 try {
Krishyab5ef96d2025-06-05 13:57:05 +0800277 // 确保参数是整数类型
278 const requestData = {
279 userId: parseInt(userId),
280 dynamicId: parseInt(dynamicId)
281 };
282
283 // 验证参数是否为有效数字
284 if (isNaN(requestData.userId) || isNaN(requestData.dynamicId)) {
285 console.error('无效的参数:', requestData);
286 alert('取消点赞失败:参数格式错误');
287 return;
288 }
289
290 console.log('取消点赞请求数据:', requestData);
291
Krishya8f2fec82025-06-04 21:54:46 +0800292 const res = await axios.delete(`/echo/dynamic/unlike`, {
Krishyab5ef96d2025-06-05 13:57:05 +0800293 headers: {
294 'Content-Type': 'application/json' // 明确指定JSON格式
295 },
296 data: requestData // 将参数放在data属性中
Krishya8f2fec82025-06-04 21:54:46 +0800297 });
298
Krishyab5ef96d2025-06-05 13:57:05 +0800299 console.log('取消点赞API响应:', res.data);
300
Krishya8f2fec82025-06-04 21:54:46 +0800301 if (res.status === 200) {
302 // 更新本地状态
Krishyab5ef96d2025-06-05 13:57:05 +0800303 feeds.forEach(feed => {
Krishya8f2fec82025-06-04 21:54:46 +0800304 if (feed.postNo === dynamicId) {
Krishyab5ef96d2025-06-05 13:57:05 +0800305 feed.postLikeNum = Math.max(0, (feed.postLikeNum || 0) - 1);
306 feed.liked = false;
Krishya8f2fec82025-06-04 21:54:46 +0800307 }
Krishyab5ef96d2025-06-05 13:57:05 +0800308 });
309 setFeeds([...feeds]); // 更新状态以触发重新渲染
Krishya8f2fec82025-06-04 21:54:46 +0800310 } else {
311 alert(res.data.message || '取消点赞失败');
312 }
313 } catch (err) {
314 console.error('取消点赞失败', err);
Krishyab5ef96d2025-06-05 13:57:05 +0800315
316 // 检查错误响应,获取更详细的错误信息
317 if (err.response) {
318 console.error('错误响应数据:', err.response.data);
319 console.error('错误响应状态:', err.response.status);
320 console.error('错误响应头:', err.response.headers);
321 }
322
Krishya8f2fec82025-06-04 21:54:46 +0800323 alert('取消点赞失败,请稍后重试');
Krishyaf1d0ea82025-05-03 17:01:58 +0800324 }
325 };
326
Krishyab5ef96d2025-06-05 13:57:05 +0800327 // 评论好友动态
328 // 评论好友动态
329const handleComment = async (dynamicId) => {
330 if (!isLoggedIn) {
331 alert('请先登录');
332 return;
333 }
334
335 if (!commentInput.trim()) {
336 alert('评论内容不能为空');
337 return;
338 }
339
340 try {
341 const res = await axios.post(`/echo/dynamic/${userId}/feeds/${dynamicId}/comments`, {
342 content: commentInput.trim()
343 });
344
345 if (res.status === 200 || res.status === 201) {
346 // 成功获取评论数据
347 const newComment = {
348 user_id: userId,
349 username: username,
350 content: commentInput.trim(),
351 time: new Date().toISOString() // 使用当前时间作为评论时间
352 };
353
354 // 更新本地状态,添加新评论
355 setFeeds(prevFeeds => {
356 return prevFeeds.map(feed => {
357 if (feed.postNo === dynamicId) {
358 // 确保comments是数组,并且正确合并新评论
359 const currentComments = Array.isArray(feed.comments) ? feed.comments : [];
360 return {
361 ...feed,
362 comments: [...currentComments, newComment]
363 };
364 }
365 return feed;
366 });
367 });
368
369 // 更新过滤后的动态列表
370 setFilteredFeeds(prevFeeds => {
371 return prevFeeds.map(feed => {
372 if (feed.postNo === dynamicId) {
373 // 确保comments是数组,并且正确合并新评论
374 const currentComments = Array.isArray(feed.comments) ? feed.comments : [];
375 return {
376 ...feed,
377 comments: [...currentComments, newComment]
378 };
379 }
380 return feed;
381 });
382 });
383
384 // alert('评论成功');
385 setCommentInput('');
386 setCommentBoxVisibleId(null); // 关闭评论框
387 } else {
388 alert(res.data.error || '评论失败');
389 }
390 } catch (err) {
391 console.error('评论失败', err);
392 alert('评论失败,请稍后重试');
393 }
394};
395
Krishyaf1d0ea82025-05-03 17:01:58 +0800396 return (
397 <div className="friend-moments-container">
398 <Header />
399 <div className="fm-header">
400 <button className="create-btn" onClick={() => setShowModal(true)}>
401 <Edit theme="outline" size="18" style={{ marginRight: '6px' }} />
402 创建动态
403 </button>
Krishyaf1d0ea82025-05-03 17:01:58 +0800404 </div>
405
406 <div className="feed-list">
Krishyab5ef96d2025-06-05 13:57:05 +0800407 {loading ? (
408 <div className="loading-message">加载中...</div>
409 ) : error ? (
410 <div className="error-message">{error}</div>
411 ) : !isLoggedIn ? (
412 <div className="login-prompt">
413 <p>请先登录查看好友动态</p>
Krishyaf1d0ea82025-05-03 17:01:58 +0800414 </div>
Krishyab5ef96d2025-06-05 13:57:05 +0800415 ) : filteredFeeds.length === 0 ? (
416 <div className="empty-message">暂无动态</div>
417 ) : (
418 filteredFeeds.map(feed => (
419 <div className="feed-item" key={feed.postNo || `feed-${Math.random()}`}>
420 {/* 显示发布者信息 */}
421 <div className="feed-author">
22301009e68c0dd2025-06-05 15:28:07 +0800422 <img
423 className="user-avatar"
424 src={feed.avatar_url || 'https://example.com/default-avatar.jpg'}
425 alt={feed.username || '用户头像'}
426 />
Krishyab5ef96d2025-06-05 13:57:05 +0800427 <div>
428 <h4>{feed.username || '未知用户'}</h4>
429 <span className="feed-date">{new Date(feed.postTime || Date.now()).toLocaleString()}</span>
430 </div>
431 </div>
432
433 {feed.title && <h4 className="feed-title">{feed.title}</h4>}
434 <p className="feed-content">{feed.postContent || '无内容'}</p>
435
436 {feed.imageUrl && (
437 <div className="feed-images">
Krishyab5ef96d2025-06-05 13:57:05 +0800438 {typeof feed.imageUrl === 'string' ? (
22301009e68c0dd2025-06-05 15:28:07 +0800439 <img src={formatImageUrl(feed.imageUrl)} alt="动态图片" />
Krishyab5ef96d2025-06-05 13:57:05 +0800440 ) : (
441 feed.imageUrl.map((url, i) => (
22301009e68c0dd2025-06-05 15:28:07 +0800442 <img key={i} src={formatImageUrl(url)} alt={`动态图${i}`} />
Krishyab5ef96d2025-06-05 13:57:05 +0800443 ))
444 )}
445 </div>
446 )}
447
22301009e68c0dd2025-06-05 15:28:07 +0800448
Krishyab5ef96d2025-06-05 13:57:05 +0800449 <div className="feed-footer">
450 <div className="like-container">
451 <button className="icon-btn" onClick={() => handleLike(feed.postNo, feed.liked, feed.user_id)}>
452 <GoodTwo theme="outline" size="24" fill={feed.liked ? '#f00' : '#fff'} />
453 <span>{feed.postLikeNum || 0}</span>
454
455 </button>
456
457 <button
458 className="icon-btn"
459 onClick={() => {
460 setCommentBoxVisibleId(feed.postNo);
461 setCommentInput('');
462 }}
463 >
464 <Comment theme="outline" size="24" fill="#333" />
465 <span>评论</span>
466 </button>
467
468 {commentBoxVisibleId === feed.postNo && (
469 <div className="comment-box">
470 <textarea
471 className="comment-input"
472 placeholder="请输入评论内容..."
473 value={commentInput}
474 onChange={(e) => setCommentInput(e.target.value)}
475 />
476 <button
477 className="submit-comment-btn"
478 onClick={() => handleComment(feed.postNo)}
479 >
480 发布评论
481 </button>
482 </div>
483 )}
484 </div>
485 {feed.user_id === userId && (
486 <button className="delete-btn" onClick={() => handleDelete(feed.postNo)}>
487 删除
488 </button>
489 )}
490</div>
491
492 {/* 评论列表 */}
493 {Array.isArray(feed.comments) && feed.comments.length > 0 && (
494 <div className="comments-container">
495 <h5>评论 ({feed.comments.length})</h5>
496 <div className="comments-list">
497 {feed.comments.map((comment, index) => (
498 <div className="comment-item" key={index}>
499 <div className="comment-header">
500 <span className="comment-user">{comment.username || '用户'}</span>
501 {/* <span className="comment-user-id">ID: {comment.user_id}</span> */}
502 <span className="comment-time">
503 {new Date(comment.time || Date.now()).toLocaleString()}
504 </span>
505 </div>
506 <p className="comment-content">{comment.content}</p>
507 </div>
508 ))}
509 </div>
510 </div>
511 )}
512
513 </div>
514 ))
515 )}
Krishyaf1d0ea82025-05-03 17:01:58 +0800516 </div>
517
518 {/* Modal 对话框 */}
519 {showModal && (
520 <div className="modal-overlay" onClick={() => setShowModal(false)}>
521 <div className="modal-dialog" onClick={e => e.stopPropagation()}>
522 <h3>发布新动态</h3>
523 <input
524 type="text"
525 placeholder="标题"
526 value={title}
527 onChange={e => setTitle(e.target.value)}
528 />
529 <textarea
530 placeholder="写下你的内容..."
531 value={content}
532 onChange={e => setContent(e.target.value)}
533 />
Krishyaf1d0ea82025-05-03 17:01:58 +0800534 <label className="file-label">
535 选择图片
536 <input
537 type="file"
538 accept="image/*"
539 multiple
540 onChange={handleImageChange}
541 style={{ display: 'none' }}
542 />
543 </label>
544 <div className="cf-preview">
Krishyab5ef96d2025-06-05 13:57:05 +0800545 {previewUrls.map((url, i) => (
Krishyaf1d0ea82025-05-03 17:01:58 +0800546 <img key={i} src={url} alt={`预览${i}`} />
547 ))}
548 </div>
549 <div className="modal-actions">
550 <button className="btn cancel" onClick={() => setShowModal(false)}>
551 取消
552 </button>
553 <button className="btn submit" onClick={handleSubmit}>
554 发布
555 </button>
556 </div>
557 </div>
558 </div>
559 )}
560 </div>
561 );
562};
563
Krishyab5ef96d2025-06-05 13:57:05 +0800564export default FriendMoments;