blob: 526c06d930c775909187dc7c70608d9be820ecca [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
Krishyaf1d0ea82025-05-03 17:01:58 +08008const FriendMoments = () => {
9 const [feeds, setFeeds] = useState([]);
10 const [filteredFeeds, setFilteredFeeds] = useState([]);
11 const [query, setQuery] = useState('');
Krishyab5ef96d2025-06-05 13:57:05 +080012 const [loading, setLoading] = useState(true);
13 const [error, setError] = useState(null);
14 const [commentBoxVisibleId, setCommentBoxVisibleId] = useState(null); // 当前显示评论框的动态ID
15 const [commentInput, setCommentInput] = useState(''); // 当前输入的评论内容
16
17 // 从上下文中获取用户信息
18 const { user } = useContext(UserContext);
19 const userId = user?.userId || null; // 从用户上下文中获取userId
20 const username = user?.username || '未知用户'; // 获取用户名
Krishyaf1d0ea82025-05-03 17:01:58 +080021
22 // Modal state & form fields
23 const [showModal, setShowModal] = useState(false);
24 const [title, setTitle] = useState('');
25 const [content, setContent] = useState('');
Krishya8f2fec82025-06-04 21:54:46 +080026 const [selectedImages, setSelectedImages] = useState([]);
Krishyab5ef96d2025-06-05 13:57:05 +080027 const [previewUrls, setPreviewUrls] = useState([]);
28
29 // 检查用户是否已登录
30 const isLoggedIn = !!userId;
Krishyaf1d0ea82025-05-03 17:01:58 +080031
32 // 拉取好友动态列表
33 const fetchFeeds = async () => {
Krishyab5ef96d2025-06-05 13:57:05 +080034 if (!isLoggedIn) {
35 setLoading(false);
36 setError('请先登录');
37 return;
38 }
39
40 setLoading(true);
41 setError(null);
Krishyaf1d0ea82025-05-03 17:01:58 +080042 try {
Krishyab5ef96d2025-06-05 13:57:05 +080043 // 注意这里修改了API路径,使用getAllDynamics接口
Krishya8f2fec82025-06-04 21:54:46 +080044 const res = await axios.get(`/echo/dynamic/${userId}/getAllDynamics`);
Krishyab5ef96d2025-06-05 13:57:05 +080045
46 // 检查API返回的数据结构
47 console.log('API响应数据:', res.data);
48
49 // 从响应中提取dynamic数组
50 const dynamicList = res.data.dynamic || [];
51
52 // 将API返回的数据结构转换为前端期望的格式
53 const formattedFeeds = dynamicList.map(item => ({
54 postNo: item.dynamic_id, // 使用API返回的dynamic_id作为帖子ID
55 title: item.title,
56 postContent: item.content,
57 imageUrl: item.images, // 使用API返回的images字段
58 postTime: item.time, // 使用API返回的time字段
59 postLikeNum: item.likes?.length || 0, // 点赞数
60 liked: item.likes?.some(like => like.user_id === userId), // 当前用户是否已点赞
61 user_id: item.user_id, // 发布者ID
62 username: item.username, // 发布者昵称
63 avatar_url: item.avatar_url, // 发布者头像
64 comments: item.comments || [] // 评论列表
65 }));
66
67 setFeeds(formattedFeeds);
68 setFilteredFeeds(formattedFeeds);
Krishyaf1d0ea82025-05-03 17:01:58 +080069 } catch (err) {
70 console.error('获取动态列表失败:', err);
Krishyab5ef96d2025-06-05 13:57:05 +080071 setError('获取动态列表失败,请稍后重试');
72 } finally {
73 setLoading(false);
Krishyaf1d0ea82025-05-03 17:01:58 +080074 }
75 };
76
77 useEffect(() => {
78 fetchFeeds();
Krishya8f2fec82025-06-04 21:54:46 +080079 }, [userId]);
Krishyaf1d0ea82025-05-03 17:01:58 +080080
81 // 搜索处理
82 const handleSearch = () => {
83 const q = query.trim().toLowerCase();
Krishya8f2fec82025-06-04 21:54:46 +080084 if (!q) {
85 setFilteredFeeds(feeds);
86 return;
87 }
Krishyaf1d0ea82025-05-03 17:01:58 +080088 setFilteredFeeds(
Krishya8f2fec82025-06-04 21:54:46 +080089 feeds.filter(f =>
90 (f.title || '').toLowerCase().includes(q) ||
91 (f.postContent || '').toLowerCase().includes(q)
92 )
Krishyaf1d0ea82025-05-03 17:01:58 +080093 );
94 };
Krishya8f2fec82025-06-04 21:54:46 +080095
Krishyaf1d0ea82025-05-03 17:01:58 +080096 const handleReset = () => {
97 setQuery('');
98 setFilteredFeeds(feeds);
99 };
100
Krishya8f2fec82025-06-04 21:54:46 +0800101 // 对话框内:处理图片选择
102 const handleImageChange = (e) => {
Krishyaf1d0ea82025-05-03 17:01:58 +0800103 const files = Array.from(e.target.files);
104 if (!files.length) return;
Krishya8f2fec82025-06-04 21:54:46 +0800105
Krishya8f2fec82025-06-04 21:54:46 +0800106 const previewUrls = files.map(file => URL.createObjectURL(file));
107
108 setSelectedImages(files);
Krishyab5ef96d2025-06-05 13:57:05 +0800109 setPreviewUrls(previewUrls);
Krishyaf1d0ea82025-05-03 17:01:58 +0800110 };
111
112 // 对话框内:提交新动态
113 const handleSubmit = async () => {
Krishyab5ef96d2025-06-05 13:57:05 +0800114 if (!isLoggedIn) {
115 alert('请先登录');
116 return;
117 }
118
Krishyaf1d0ea82025-05-03 17:01:58 +0800119 if (!content.trim()) {
120 alert('内容不能为空');
121 return;
122 }
Krishya8f2fec82025-06-04 21:54:46 +0800123
Krishyaf1d0ea82025-05-03 17:01:58 +0800124 try {
Krishyab5ef96d2025-06-05 13:57:05 +0800125 // 使用formData格式提交
Krishya8f2fec82025-06-04 21:54:46 +0800126 const formData = new FormData();
Krishya8f2fec82025-06-04 21:54:46 +0800127 formData.append('title', title.trim() || '');
128 formData.append('content', content.trim());
129
130 // 添加图片文件
131 selectedImages.forEach((file, index) => {
132 formData.append('image_url', file);
133 });
134
Krishyab5ef96d2025-06-05 13:57:05 +0800135 // 调用创建动态API
Krishya8f2fec82025-06-04 21:54:46 +0800136 await axios.post(`/echo/dynamic/${userId}/createDynamic`, formData, {
137 headers: {
138 'Content-Type': 'multipart/form-data'
139 }
140 });
141
Krishyaf1d0ea82025-05-03 17:01:58 +0800142 // 重置表单
143 setTitle('');
144 setContent('');
Krishya8f2fec82025-06-04 21:54:46 +0800145 setSelectedImages([]);
Krishyab5ef96d2025-06-05 13:57:05 +0800146 setPreviewUrls([]);
Krishyaf1d0ea82025-05-03 17:01:58 +0800147 setShowModal(false);
148 fetchFeeds();
Krishya8f2fec82025-06-04 21:54:46 +0800149 alert('发布成功');
Krishyaf1d0ea82025-05-03 17:01:58 +0800150 } catch (err) {
151 console.error('发布失败', err);
152 alert('发布失败,请稍后重试');
153 }
154 };
155
Krishyab5ef96d2025-06-05 13:57:05 +0800156 // 删除动态 - 注意:API文档中未提供删除接口,这里保留原代码
Krishya8f2fec82025-06-04 21:54:46 +0800157 const handleDelete = async (dynamicId) => {
Krishyab5ef96d2025-06-05 13:57:05 +0800158
159 if (!isLoggedIn) {
160 alert('请先登录');
161 return;
162 }
163
Krishyaf1d0ea82025-05-03 17:01:58 +0800164 if (!window.confirm('确定要删除这条动态吗?')) return;
165 try {
Krishyab5ef96d2025-06-05 13:57:05 +0800166 // 注意:API文档中未提供删除接口,这里使用原代码中的路径
Krishya8f2fec82025-06-04 21:54:46 +0800167 await axios.delete(`/echo/dynamic/me/deleteDynamic/${dynamicId}`);
Krishyaf1d0ea82025-05-03 17:01:58 +0800168 fetchFeeds();
Krishya8f2fec82025-06-04 21:54:46 +0800169 alert('删除成功');
Krishyaf1d0ea82025-05-03 17:01:58 +0800170 } catch (err) {
171 console.error('删除失败', err);
Krishya8f2fec82025-06-04 21:54:46 +0800172 alert('删除失败,请稍后重试');
173 }
174 };
175
176 // 点赞动态
Krishyab5ef96d2025-06-05 13:57:05 +0800177 const handleLike = async (dynamicId,islike) => {
178 if (islike) {
179 handleUnlike(dynamicId);
180 return
181 }
182 if (!isLoggedIn) {
183 alert('请先登录');
184 return;
185 }
186
187 // 验证dynamicId是否有效
188 if (!dynamicId) {
189 console.error('无效的dynamicId:', dynamicId);
190 alert('点赞失败:动态ID无效');
191 return;
192 }
193
194 // 检查是否已经点赞,防止重复请求
195 // const currentFeed = feeds.find(feed => feed.postNo === dynamicId);
196 // if (currentFeed && currentFeed.liked) {
197 // console.warn('尝试重复点赞,已忽略');
198 // return;
199 // }
200
201 console.log('当前用户ID:', userId);
202 console.log('即将点赞的动态ID:', dynamicId);
203
Krishya8f2fec82025-06-04 21:54:46 +0800204 try {
Krishyab5ef96d2025-06-05 13:57:05 +0800205 // 确保参数是整数类型
206 const requestData = {
207 userId: parseInt(userId),
208 dynamicId: parseInt(dynamicId)
209 };
210
211 // 验证参数是否为有效数字
212 if (isNaN(requestData.userId) || isNaN(requestData.dynamicId)) {
213 console.error('无效的参数:', requestData);
214 alert('点赞失败:参数格式错误');
215 return;
216 }
217
218 console.log('点赞请求数据:', requestData);
219
220 const res = await axios.post(`/echo/dynamic/like`, requestData, {
221 headers: {
222 'Content-Type': 'application/json' // 明确指定JSON格式
223 }
Krishya8f2fec82025-06-04 21:54:46 +0800224 });
225
Krishyab5ef96d2025-06-05 13:57:05 +0800226 console.log('点赞API响应:', res.data);
227
Krishya8f2fec82025-06-04 21:54:46 +0800228 if (res.status === 200) {
229 // 更新本地状态
Krishyab5ef96d2025-06-05 13:57:05 +0800230 feeds.forEach(feed => {
Krishya8f2fec82025-06-04 21:54:46 +0800231 if (feed.postNo === dynamicId) {
Krishyab5ef96d2025-06-05 13:57:05 +0800232 feed.postLikeNum = (feed.postLikeNum || 0) + 1;
233 feed.liked = true;
Krishya8f2fec82025-06-04 21:54:46 +0800234 }
Krishyab5ef96d2025-06-05 13:57:05 +0800235 });
236 setFeeds([...feeds]); // 更新状态以触发重新渲染
Krishya8f2fec82025-06-04 21:54:46 +0800237 } else {
238 alert(res.data.message || '点赞失败');
239 }
240 } catch (err) {
241 console.error('点赞失败', err);
Krishyab5ef96d2025-06-05 13:57:05 +0800242
243 // 检查错误响应,获取更详细的错误信息
244 if (err.response) {
245 console.error('错误响应数据:', err.response.data);
246 console.error('错误响应状态:', err.response.status);
247 console.error('错误响应头:', err.response.headers);
248 }
249
Krishya8f2fec82025-06-04 21:54:46 +0800250 alert('点赞失败,请稍后重试');
251 }
252 };
253
254 // 取消点赞
255 const handleUnlike = async (dynamicId) => {
Krishyab5ef96d2025-06-05 13:57:05 +0800256 if (!isLoggedIn) {
257 alert('请先登录');
258 return;
259 }
260
261 // 验证dynamicId是否有效
262 if (!dynamicId) {
263 console.error('无效的dynamicId:', dynamicId);
264 alert('取消点赞失败:动态ID无效');
265 return;
266 }
267
268 // 检查是否已经取消点赞,防止重复请求
269 const currentFeed = feeds.find(feed => feed.postNo === dynamicId);
270 if (currentFeed && !currentFeed.liked) {
271 console.warn('尝试重复取消点赞,已忽略');
272 return;
273 }
274
Krishya8f2fec82025-06-04 21:54:46 +0800275 try {
Krishyab5ef96d2025-06-05 13:57:05 +0800276 // 确保参数是整数类型
277 const requestData = {
278 userId: parseInt(userId),
279 dynamicId: parseInt(dynamicId)
280 };
281
282 // 验证参数是否为有效数字
283 if (isNaN(requestData.userId) || isNaN(requestData.dynamicId)) {
284 console.error('无效的参数:', requestData);
285 alert('取消点赞失败:参数格式错误');
286 return;
287 }
288
289 console.log('取消点赞请求数据:', requestData);
290
Krishya8f2fec82025-06-04 21:54:46 +0800291 const res = await axios.delete(`/echo/dynamic/unlike`, {
Krishyab5ef96d2025-06-05 13:57:05 +0800292 headers: {
293 'Content-Type': 'application/json' // 明确指定JSON格式
294 },
295 data: requestData // 将参数放在data属性中
Krishya8f2fec82025-06-04 21:54:46 +0800296 });
297
Krishyab5ef96d2025-06-05 13:57:05 +0800298 console.log('取消点赞API响应:', res.data);
299
Krishya8f2fec82025-06-04 21:54:46 +0800300 if (res.status === 200) {
301 // 更新本地状态
Krishyab5ef96d2025-06-05 13:57:05 +0800302 feeds.forEach(feed => {
Krishya8f2fec82025-06-04 21:54:46 +0800303 if (feed.postNo === dynamicId) {
Krishyab5ef96d2025-06-05 13:57:05 +0800304 feed.postLikeNum = Math.max(0, (feed.postLikeNum || 0) - 1);
305 feed.liked = false;
Krishya8f2fec82025-06-04 21:54:46 +0800306 }
Krishyab5ef96d2025-06-05 13:57:05 +0800307 });
308 setFeeds([...feeds]); // 更新状态以触发重新渲染
Krishya8f2fec82025-06-04 21:54:46 +0800309 } else {
310 alert(res.data.message || '取消点赞失败');
311 }
312 } catch (err) {
313 console.error('取消点赞失败', err);
Krishyab5ef96d2025-06-05 13:57:05 +0800314
315 // 检查错误响应,获取更详细的错误信息
316 if (err.response) {
317 console.error('错误响应数据:', err.response.data);
318 console.error('错误响应状态:', err.response.status);
319 console.error('错误响应头:', err.response.headers);
320 }
321
Krishya8f2fec82025-06-04 21:54:46 +0800322 alert('取消点赞失败,请稍后重试');
Krishyaf1d0ea82025-05-03 17:01:58 +0800323 }
324 };
325
Krishyab5ef96d2025-06-05 13:57:05 +0800326 // 评论好友动态
327 // 评论好友动态
328const handleComment = async (dynamicId) => {
329 if (!isLoggedIn) {
330 alert('请先登录');
331 return;
332 }
333
334 if (!commentInput.trim()) {
335 alert('评论内容不能为空');
336 return;
337 }
338
339 try {
340 const res = await axios.post(`/echo/dynamic/${userId}/feeds/${dynamicId}/comments`, {
341 content: commentInput.trim()
342 });
343
344 if (res.status === 200 || res.status === 201) {
345 // 成功获取评论数据
346 const newComment = {
347 user_id: userId,
348 username: username,
349 content: commentInput.trim(),
350 time: new Date().toISOString() // 使用当前时间作为评论时间
351 };
352
353 // 更新本地状态,添加新评论
354 setFeeds(prevFeeds => {
355 return prevFeeds.map(feed => {
356 if (feed.postNo === dynamicId) {
357 // 确保comments是数组,并且正确合并新评论
358 const currentComments = Array.isArray(feed.comments) ? feed.comments : [];
359 return {
360 ...feed,
361 comments: [...currentComments, newComment]
362 };
363 }
364 return feed;
365 });
366 });
367
368 // 更新过滤后的动态列表
369 setFilteredFeeds(prevFeeds => {
370 return prevFeeds.map(feed => {
371 if (feed.postNo === dynamicId) {
372 // 确保comments是数组,并且正确合并新评论
373 const currentComments = Array.isArray(feed.comments) ? feed.comments : [];
374 return {
375 ...feed,
376 comments: [...currentComments, newComment]
377 };
378 }
379 return feed;
380 });
381 });
382
383 // alert('评论成功');
384 setCommentInput('');
385 setCommentBoxVisibleId(null); // 关闭评论框
386 } else {
387 alert(res.data.error || '评论失败');
388 }
389 } catch (err) {
390 console.error('评论失败', err);
391 alert('评论失败,请稍后重试');
392 }
393};
394
Krishyaf1d0ea82025-05-03 17:01:58 +0800395 return (
396 <div className="friend-moments-container">
397 <Header />
398 <div className="fm-header">
399 <button className="create-btn" onClick={() => setShowModal(true)}>
400 <Edit theme="outline" size="18" style={{ marginRight: '6px' }} />
401 创建动态
402 </button>
Krishyab5ef96d2025-06-05 13:57:05 +0800403 {/* <div className="f-search-bar">
Krishyaf1d0ea82025-05-03 17:01:58 +0800404 <input
405 className="search-input"
406 type="text"
407 value={query}
408 onChange={e => setQuery(e.target.value)}
409 placeholder="输入要搜索的动态"
410 />
411 <button className="search-btn" onClick={handleSearch}>搜索</button>
412 <button className="search-btn" onClick={handleReset}>重置</button>
Krishyab5ef96d2025-06-05 13:57:05 +0800413 </div> */}
Krishyaf1d0ea82025-05-03 17:01:58 +0800414 </div>
415
416 <div className="feed-list">
Krishyab5ef96d2025-06-05 13:57:05 +0800417 {loading ? (
418 <div className="loading-message">加载中...</div>
419 ) : error ? (
420 <div className="error-message">{error}</div>
421 ) : !isLoggedIn ? (
422 <div className="login-prompt">
423 <p>请先登录查看好友动态</p>
Krishyaf1d0ea82025-05-03 17:01:58 +0800424 </div>
Krishyab5ef96d2025-06-05 13:57:05 +0800425 ) : filteredFeeds.length === 0 ? (
426 <div className="empty-message">暂无动态</div>
427 ) : (
428 filteredFeeds.map(feed => (
429 <div className="feed-item" key={feed.postNo || `feed-${Math.random()}`}>
430 {/* 显示发布者信息 */}
431 <div className="feed-author">
432 <img src={feed.avatar_url || 'https://example.com/default-avatar.jpg'} alt={feed.username || '用户头像'} />
433 <div>
434 <h4>{feed.username || '未知用户'}</h4>
435 <span className="feed-date">{new Date(feed.postTime || Date.now()).toLocaleString()}</span>
436 </div>
437 </div>
438
439 {feed.title && <h4 className="feed-title">{feed.title}</h4>}
440 <p className="feed-content">{feed.postContent || '无内容'}</p>
441
442 {feed.imageUrl && (
443 <div className="feed-images">
444 {/* 处理可能是单张图片或多张图片的情况 */}
445 {typeof feed.imageUrl === 'string' ? (
446 <img src={feed.imageUrl} alt="动态图片" />
447 ) : (
448 feed.imageUrl.map((url, i) => (
449 <img key={i} src={url} alt={`动态图${i}`} />
450 ))
451 )}
452 </div>
453 )}
454
455 <div className="feed-footer">
456 <div className="like-container">
457 <button className="icon-btn" onClick={() => handleLike(feed.postNo, feed.liked, feed.user_id)}>
458 <GoodTwo theme="outline" size="24" fill={feed.liked ? '#f00' : '#fff'} />
459 <span>{feed.postLikeNum || 0}</span>
460
461 </button>
462
463 <button
464 className="icon-btn"
465 onClick={() => {
466 setCommentBoxVisibleId(feed.postNo);
467 setCommentInput('');
468 }}
469 >
470 <Comment theme="outline" size="24" fill="#333" />
471 <span>评论</span>
472 </button>
473
474 {commentBoxVisibleId === feed.postNo && (
475 <div className="comment-box">
476 <textarea
477 className="comment-input"
478 placeholder="请输入评论内容..."
479 value={commentInput}
480 onChange={(e) => setCommentInput(e.target.value)}
481 />
482 <button
483 className="submit-comment-btn"
484 onClick={() => handleComment(feed.postNo)}
485 >
486 发布评论
487 </button>
488 </div>
489 )}
490 </div>
491 {feed.user_id === userId && (
492 <button className="delete-btn" onClick={() => handleDelete(feed.postNo)}>
493 删除
494 </button>
495 )}
496</div>
497
498 {/* 评论列表 */}
499 {Array.isArray(feed.comments) && feed.comments.length > 0 && (
500 <div className="comments-container">
501 <h5>评论 ({feed.comments.length})</h5>
502 <div className="comments-list">
503 {feed.comments.map((comment, index) => (
504 <div className="comment-item" key={index}>
505 <div className="comment-header">
506 <span className="comment-user">{comment.username || '用户'}</span>
507 {/* <span className="comment-user-id">ID: {comment.user_id}</span> */}
508 <span className="comment-time">
509 {new Date(comment.time || Date.now()).toLocaleString()}
510 </span>
511 </div>
512 <p className="comment-content">{comment.content}</p>
513 </div>
514 ))}
515 </div>
516 </div>
517 )}
518
519 </div>
520 ))
521 )}
Krishyaf1d0ea82025-05-03 17:01:58 +0800522 </div>
523
524 {/* Modal 对话框 */}
525 {showModal && (
526 <div className="modal-overlay" onClick={() => setShowModal(false)}>
527 <div className="modal-dialog" onClick={e => e.stopPropagation()}>
528 <h3>发布新动态</h3>
529 <input
530 type="text"
531 placeholder="标题"
532 value={title}
533 onChange={e => setTitle(e.target.value)}
534 />
535 <textarea
536 placeholder="写下你的内容..."
537 value={content}
538 onChange={e => setContent(e.target.value)}
539 />
Krishyaf1d0ea82025-05-03 17:01:58 +0800540 <label className="file-label">
541 选择图片
542 <input
543 type="file"
544 accept="image/*"
545 multiple
546 onChange={handleImageChange}
547 style={{ display: 'none' }}
548 />
549 </label>
550 <div className="cf-preview">
Krishyab5ef96d2025-06-05 13:57:05 +0800551 {previewUrls.map((url, i) => (
Krishyaf1d0ea82025-05-03 17:01:58 +0800552 <img key={i} src={url} alt={`预览${i}`} />
553 ))}
554 </div>
555 <div className="modal-actions">
556 <button className="btn cancel" onClick={() => setShowModal(false)}>
557 取消
558 </button>
559 <button className="btn submit" onClick={handleSubmit}>
560 发布
561 </button>
562 </div>
563 </div>
564 </div>
565 )}
566 </div>
567 );
568};
569
Krishyab5ef96d2025-06-05 13:57:05 +0800570export default FriendMoments;