fix-img
Change-Id: Ida77fc6aed06b28e41e2abcb6ae09d5f63d016f2
diff --git a/.env.development b/.env.development
index a4fa618..e6d3898 100644
--- a/.env.development
+++ b/.env.development
@@ -1,3 +1,4 @@
# REACT_APP_API_BASE=http://127.0.0.1:4523/m1/6139971-5831803-default
# REACT_APP_API_BASE=http://localhost:8080
# REACT_APP_API_BASE=''
+REACT_APP_AVATAR_BASE_URL=http://localhost:8080
diff --git a/.env.test b/.env.test
index b6cee00..efe7cf6 100644
--- a/.env.test
+++ b/.env.test
@@ -1 +1,3 @@
REACT_APP_API_BASE=http://localhost:8080
+REACT_APP_AVATAR_BASE_URL=http://localhost:8080
+
diff --git a/src/App.js b/src/App.js
index 09e401b..1209f65 100644
--- a/src/App.js
+++ b/src/App.js
@@ -20,6 +20,7 @@
// import UserDynamics from './pages/UserCenter/UserDynamics';
import UserFriends from './pages/UserCenter/UserFriends';
import UserCollect from './pages/UserCenter/UserCollect';
+import SimpleUploader from './pages/PublishSeed/SimpleUploader';
function RedirectToAuth() {
if (typeof window !== 'undefined') {
@@ -40,7 +41,8 @@
<Route path="/forum/post/:postId" component={PostDetailPage} />
<Route path="/forum/create-post" component={CreatePostPage} />
<Route path="/seed-list" component={SeedList} />
- <Route path="/publish-seed" component={PublishSeed} />
+ {/* <Route path="/publish-seed" component={PublishSeed} /> */}
+ <Route path="/publish-seed" component={SimpleUploader} />
<Route path="/seed/:seed_id" component={SeedDetail} />
<Route path="/interest-groups" component={InterestGroup} />
<Route path="/user/profile" component={UserProfile} />
diff --git a/src/components/Header.jsx b/src/components/Header.jsx
index 50e6d37..10e8efb 100644
--- a/src/components/Header.jsx
+++ b/src/components/Header.jsx
@@ -1,90 +1,11 @@
-// import React, { useState, useEffect } from 'react';
-// import './Header.css'; // 导入 Header.css 文件
-// import logo from '../assets/logo.png';
-
-// const Header = () => {
-// const [currentPath, setCurrentPath] = useState(window.location.pathname);
-
-// // 提取所有页面路径,简化 active 类逻辑
-// const navLinks = [
-// { to: '/friend-moments', label: '好友动态' },
-// { to: '/forum', label: '论坛' },
-// { to: '/interest-groups', label: '兴趣小组' },
-// { to: '/seed-list', label: '种子列表' },
-// { to: '/publish-seed', label: '发布种子' },
-// ];
-
-// // 更新当前路径状态
-// useEffect(() => {
-// const handleLocationChange = () => {
-// setCurrentPath(window.location.pathname);
-// };
-
-// // 监听历史记录变化
-// window.addEventListener('popstate', handleLocationChange);
-
-// // 清理事件监听器
-// return () => {
-// window.removeEventListener('popstate', handleLocationChange);
-// };
-// }, []);
-
-// // 判断路径是否是当前活动页面
-// const isActive = (path) => currentPath.startsWith(path);
-
-// // 自定义链接点击逻辑
-// const handleLinkClick = (to) => {
-// window.history.pushState({}, '', to);
-// setCurrentPath(to); // 更新当前路径状态
-// };
-
-// return (
-// <div className="main-page">
-// {/* 顶部栏 */}
-// <header className="header">
-// {/* 左侧 logo 和网站名称 */}
-// <div className="logo-and-name">
-// <img src={logo} alt="网站 logo" className="logo" />
-// <span className="site-name">Echo</span>
-// </div>
-// {/* 右侧用户头像和消息中心 */}
-// <div className="user-and-message">
-// {/* 用户头像点击跳转到个人主页 */}
-// <a href="/user/profile">
-// <img src="user-avatar.png" alt="用户头像" className="user-avatar" />
-// </a>
-// <span className="message-center">消息</span>
-// </div>
-// </header>
-
-// {/* 导航栏 */}
-// <nav className="nav">
-// {navLinks.map(({ to, label }) => (
-// <a
-// key={to}
-// href={to}
-// onClick={(e) => {
-// e.preventDefault(); // 防止默认跳转行为
-// handleLinkClick(to); // 手动更新当前路径
-// }}
-// className={`nav-item ${isActive(to) ? 'active' : ''}`}
-// >
-// {label}
-// </a>
-// ))}
-// </nav>
-// </div>
-// );
-// };
-
-// export default Header;
-
import React, { useState, useEffect } from 'react';
import './Header.css';
import logo from '../assets/logo.png';
+import { useUser } from '../context/UserContext';
const Header = () => {
const [currentPath, setCurrentPath] = useState(window.location.pathname);
+ const { user } = useUser();
const navLinks = [
{ to: '/friend-moments', label: '好友动态' },
@@ -120,11 +41,14 @@
</div>
<div className="user-and-message">
<a href="/user/profile">
- <img src="user-avatar.png" alt="用户头像" className="user-avatar" />
+ <img
+ src={user?.avatarUrl}
+ alt="用户头像"
+ className="user-avatar"
+ />
</a>
- {/* 修改这里,点击跳转消息页面 */}
- <span
- className="message-center"
+ <span
+ className="message-center"
onClick={() => handleLinkClick('/messages')}
style={{ cursor: 'pointer' }}
>
@@ -135,8 +59,8 @@
<nav className="nav">
{navLinks.map(({ to, label }) => (
- <a
- key={to}
+ <a
+ key={to}
href={to}
onClick={(e) => {
e.preventDefault();
diff --git a/src/components/utils/avatar.js b/src/components/utils/avatar.js
new file mode 100644
index 0000000..ba13e1a
--- /dev/null
+++ b/src/components/utils/avatar.js
@@ -0,0 +1,5 @@
+const avatarBaseUrl = process.env.REACT_APP_AVATAR_BASE_URL;
+
+export const formatAvatarUrl = (avatarPath) => {
+ return avatarPath ? `${avatarBaseUrl}${avatarPath}` : 'https://example.com/default-avatar.jpg';
+};
diff --git a/src/context/UserContext.js b/src/context/UserContext.js
index a76b638..01f500d 100644
--- a/src/context/UserContext.js
+++ b/src/context/UserContext.js
@@ -1,28 +1,53 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
+const avatarBaseUrl = process.env.REACT_APP_AVATAR_BASE_URL || '';
+
export const UserContext = createContext();
export const UserProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
+ // ✅ 标准化用户数据结构
+ const formatUser = (raw) => {
+ if (!raw) return null;
+ return {
+ userId: raw.userId || raw.id || null,
+ username: raw.username || '匿名用户',
+ avatarUrl: raw.avatarUrl ? `${avatarBaseUrl}${raw.avatarUrl}` : null,
+ ...raw, // 保留其它字段(如 email, level 等)
+ };
+ };
+
useEffect(() => {
const storedUser = localStorage.getItem('user');
if (storedUser) {
- setUser(JSON.parse(storedUser));
+ try {
+ const parsed = JSON.parse(storedUser);
+ setUser(formatUser(parsed));
+ } catch (err) {
+ console.error('本地用户信息解析失败:', err);
+ localStorage.removeItem('user');
+ setUser(null);
+ }
} else {
// 设置默认用户
- const defaultUser = { userId: 1, username: '测试用户' };
+ const defaultUser = {
+ userId: 1,
+ username: '测试用户',
+ avatarUrl: null,
+ };
localStorage.setItem('user', JSON.stringify(defaultUser));
- setUser(defaultUser);
+ setUser(formatUser(defaultUser));
}
setLoading(false);
}, []);
const saveUser = (userData) => {
- if (userData && userData.id) {
- localStorage.setItem('user', JSON.stringify(userData));
- setUser(userData);
+ if (userData && (userData.userId || userData.id)) {
+ const formatted = formatUser(userData);
+ localStorage.setItem('user', JSON.stringify(userData)); // 原始数据存储
+ setUser(formatted); // 格式化后使用
} else {
console.error('无效的用户数据:', userData);
}
@@ -46,4 +71,4 @@
throw new Error('useUser must be used within a UserProvider');
}
return context;
-};
\ No newline at end of file
+};
diff --git a/src/pages/Forum/posts-create/CreatePost.jsx b/src/pages/Forum/posts-create/CreatePost.jsx
index 07f3a0c..b8add81 100644
--- a/src/pages/Forum/posts-create/CreatePost.jsx
+++ b/src/pages/Forum/posts-create/CreatePost.jsx
@@ -1,81 +1,4 @@
-// import React, { useState } from 'react';
-// import axios from 'axios';
-// import './CreatePost.css'; // 如果你打算加样式
-//
-
-// const CreatePost = ({ user_id }) => {
-// const [title, setTitle] = useState('');
-// const [content, setContent] = useState('');
-// const [imageUrl, setImageUrl] = useState('');
-// const [message, setMessage] = useState('');
-// const [error, setError] = useState('');
-
-// const handleSubmit = async (e) => {
-// e.preventDefault();
-// setMessage('');
-// setError('');
-
-// if (!title.trim() || !content.trim()) {
-// setError('标题和内容不能为空');
-// return;
-// }
-
-// try {
-// const res = await axios.post(`/echo/forum/posts/${user_id}/createPost`, {
-// title,
-// post_content: content,
-// image_url: imageUrl
-// });
-
-// setMessage(`发帖成功,帖子ID:${res.data.post_id}`);
-// setTitle('');
-// setContent('');
-// setImageUrl('');
-// } catch (err) {
-// console.error(err);
-// setError(err.response?.data?.error || '发帖失败,请稍后重试');
-// }
-// };
-
-// return (
-// <div className="create-post-container">
-// <h2>发表新帖子</h2>
-// <form onSubmit={handleSubmit} className="create-post-form">
-// <div className="form-group">
-// <label>标题:</label>
-// <input
-// type="text"
-// value={title}
-// onChange={(e) => setTitle(e.target.value)}
-// placeholder="输入帖子标题"
-// />
-// </div>
-// <div className="form-group">
-// <label>内容:</label>
-// <textarea
-// value={content}
-// onChange={(e) => setContent(e.target.value)}
-// placeholder="输入帖子内容"
-// />
-// </div>
-// <div className="form-group">
-// <label>图片链接(可选):</label>
-// <input
-// type="text"
-// value={imageUrl}
-// onChange={(e) => setImageUrl(e.target.value)}
-// placeholder="例如:https://example.com/img.jpg"
-// />
-// </div>
-// <button type="submit">发布</button>
-// </form>
-
-// {message && <p className="success-text">{message}</p>}
-// {error && <p className="error-text">{error}</p>}
-// </div>
-// );
-// };
// export default CreatePost;
diff --git a/src/pages/Forum/posts-detail/PostDetailPage.jsx b/src/pages/Forum/posts-detail/PostDetailPage.jsx
index 159bdfa..f703fd6 100644
--- a/src/pages/Forum/posts-detail/PostDetailPage.jsx
+++ b/src/pages/Forum/posts-detail/PostDetailPage.jsx
@@ -1,200 +1,171 @@
-import React, { useContext, useEffect, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
import { useParams } from 'wouter';
import { GoodTwo, Star } from '@icon-park/react';
-import { getPostDetail, getPostComments, likePost, unlikePost, addCommentToPost, collectPost, uncollectPost } from './api'; // 引入你的 API 函数
+import { getPostDetail, getPostComments, likePost, unlikePost, addCommentToPost, collectPost } from './api'; // 你的 API 函数
import './PostDetailPage.css';
-import { UserContext, useUser } from '../../../context/UserContext'; // 注意路径
+import { UserContext } from '../../../context/UserContext'; // 用户上下文
import Header from '../../../components/Header';
+const formatImageUrl = (url) => {
+ if (!url) return '';
+
+ if (url.startsWith('http')) return url;
+
+ // 如果是 /images/... ,替换成 /uploads/post/...
+ if (url.startsWith('/images/')) {
+ // 这里把 /images/ 替换成 /uploads/post/
+ return `http://localhost:8080/uploads/post/${url.slice('/images/'.length)}`;
+ }
+
+ // 其它情况默认直接拼接,不加斜杠
+ return `http://localhost:8080${url.startsWith('/') ? '' : '/'}${url}`;
+};
+
+
+// 头像地址格式化,处理 avatarUrl 字段
+export function formatAvatarUrlNoDefault(avatarUrl) {
+ if (!avatarUrl) return '';
+ if (avatarUrl.startsWith('http')) return avatarUrl;
+ return `http://localhost:8080${avatarUrl}`;
+}
+
const PostDetailPage = () => {
- const { postId } = useParams(); // 获取帖子ID
+ const { postId } = useParams();
+ const { user } = useContext(UserContext);
+
const [postDetail, setPostDetail] = useState(null);
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(true);
const [errorMsg, setErrorMsg] = useState('');
- const [newComment, setNewComment] = useState(''); // 新评论内容
- // const [isAnonymous, setIsAnonymous] = useState(false); // 是否匿名
- const [isLiked, setIsLiked] = useState(false); // 是否已点赞
- const [isCollected, setIsCollected] = useState(false); // 是否已收藏
- const [replyToCommentId, setReplyToCommentId] = useState(null); // 回复的评论ID
+ const [newComment, setNewComment] = useState('');
+ const [isLiked, setIsLiked] = useState(false);
+ const [isCollected, setIsCollected] = useState(false);
+ const [replyToCommentId, setReplyToCommentId] = useState(null);
const [replyToUsername, setReplyToUsername] = useState(null);
- // 获取当前用户ID(假设从上下文中获取)
- const { user } = useContext(UserContext);
- // const { user } = useUser(); // 你需要从用户上下文获取用户 ID
-
useEffect(() => {
- const fetchPostDetail = async () => {
+ const fetchData = async () => {
setLoading(true);
setErrorMsg('');
try {
- // 获取帖子详情
const postData = await getPostDetail(postId);
setPostDetail(postData);
-
- // 获取帖子评论
const commentsData = await getPostComments(postId);
setComments(commentsData);
- // 设置是否已经点赞
- if (postData.likedByUser) {
- setIsLiked(true);
- } else {
- setIsLiked(false);
- }
-
- // 设置是否已经收藏
- if (postData.collectedByUser) {
- setIsCollected(true);
- } else {
- setIsCollected(false);
- }
+ setIsLiked(!!postData.likedByUser);
+ setIsCollected(!!postData.collectedByUser);
} catch (err) {
- console.error('加载失败:', err);
+ console.error(err);
setErrorMsg('加载失败,请稍后重试');
} finally {
setLoading(false);
}
};
-
- fetchPostDetail();
+ fetchData();
}, [postId]);
- // 点赞功能
+ // 点赞
const toggleLike = async () => {
- if (!user) {
- alert('请先登录');
- return;
- }
-
+ if (!user) return alert('请先登录');
try {
if (isLiked) {
- // 取消点赞
await unlikePost(postId, user.userId);
setIsLiked(false);
- setPostDetail((prev) => ({
+ setPostDetail(prev => ({
...prev,
postLikeNum: prev.postLikeNum - 1,
}));
} else {
- // 点赞
await likePost(postId, user.userId);
setIsLiked(true);
- setPostDetail((prev) => ({
+ setPostDetail(prev => ({
...prev,
postLikeNum: prev.postLikeNum + 1,
}));
}
- } catch (err) {
- console.error('点赞失败:', err);
+ } catch {
alert('点赞失败,请稍后再试');
}
};
-// 收藏功能
-const toggleCollect = async () => {
- if (!user) {
- alert('请先登录');
- return;
+ // 收藏
+ const toggleCollect = async () => {
+ if (!user) return alert('请先登录');
+ try {
+ if (isCollected) {
+ await collectPost(postId, user.userId, 'cancel');
+ setIsCollected(false);
+ setPostDetail(prev => ({
+ ...prev,
+ postCollectNum: prev.postCollectNum - 1,
+ }));
+ } else {
+ await collectPost(postId, user.userId, 'collect');
+ setIsCollected(true);
+ setPostDetail(prev => ({
+ ...prev,
+ postCollectNum: prev.postCollectNum + 1,
+ }));
+ }
+ } catch {
+ alert('收藏失败,请稍后再试');
}
+ };
+
+ // 添加评论
+ const handleAddComment = async () => {
+ if (!user || !user.userId) return alert('请先登录后再评论');
+ if (!newComment.trim()) return alert('评论内容不能为空');
try {
- if (isCollected) {
- // 取消收藏 - 使用原有的collectPost函数,传递action: "cancel"
- await collectPost(postId, user.userId, "cancel");
- setIsCollected(false);
- setPostDetail((prev) => ({
- ...prev,
- postCollectNum: prev.postCollectNum - 1,
- }));
- } else {
- // 收藏
- await collectPost(postId, user.userId, "collect");
- setIsCollected(true);
- setPostDetail((prev) => ({
- ...prev,
- postCollectNum: prev.postCollectNum + 1,
- }));
- }
- } catch (err) {
- console.error('收藏操作失败:', err);
- alert('收藏操作失败,请稍后再试');
+ const commentPayload = {
+ content: newComment,
+ userId: user.userId,
+ isAnonymous: false,
+ com_comment_id: replyToCommentId || null,
+ };
+ const commentData = await addCommentToPost(postId, commentPayload);
+
+ const newCommentItem = {
+ commentId: commentData?.commentId || Date.now(),
+ post_id: postId,
+ userId: user.userId,
+ username: user.username || '匿名',
+ content: newComment,
+ commentTime: new Date().toISOString(),
+ comCommentId: replyToCommentId,
+ userAvatar: user.avatar_url || '',
+ };
+
+ setComments(prev => [newCommentItem, ...prev]);
+ setNewComment('');
+ setReplyToCommentId(null);
+ setReplyToUsername(null);
+ } catch (error) {
+ alert(error.response?.data?.message || '评论失败,请稍后再试');
}
-};
+ };
-// 添加评论
-const handleAddComment = async () => {
- // 直接使用组件顶层获取的 user
- if (!user || !user.userId) {
- alert('请先登录后再评论');
- return;
- }
-
- if (!newComment.trim()) {
- alert('评论内容不能为空');
- return;
- }
-
- try {
- // 构建评论数据
- const commentPayload = {
- content: newComment,
- userId: user.userId, // 使用已获取的用户ID
- isAnonymous: false,
- com_comment_id: replyToCommentId || null,
- };
-
- // 发送评论请求
- const commentData = await addCommentToPost(postId, commentPayload);
-
- // 更新评论列表
- const newCommentItem = {
- commentId: commentData?.commentId || Date.now(),
- post_id: postId,
- userId: user.userId,
- content: newComment,
- commentTime: new Date().toISOString(),
- comCommentId: replyToCommentId,
- };
-
- setComments((prevComments) => [newCommentItem, ...prevComments]);
-
- // 重置表单
- setNewComment('');
- setReplyToCommentId(null);
-
- // alert('评论成功!');
- } catch (error) {
- console.error('评论失败:', error);
-
- const errorMessage =
- error.response?.data?.message ||
- error.message ||
- '评论失败,请稍后再试';
-
- alert(errorMessage);
- }
-};
-
-
-
- // 回复评论
+ // 回复按钮点击
const handleReply = (commentId) => {
- setReplyToCommentId(commentId);
- const comment = comments.find(c => c.commentId === commentId);
- if (comment) {
- // 这里用用户名或者用户ID
- setReplyToUsername(comment.username || comment.userId);
- } else {
- setReplyToUsername(null);
- }
-};
+ setReplyToCommentId(commentId);
+ const comment = comments.find(c => c.commentId === commentId);
+ setReplyToUsername(comment?.username || comment?.userId || '未知用户');
+ };
-const findUsernameByCommentId = (id) => {
- const comment = comments.find(c => c.commentId === id);
- return comment ? (comment.username || comment.userId) : '未知用户';
-};
+ // 查找回复的用户名
+ const findUsernameByCommentId = (id) => {
+ const comment = comments.find(c => c.commentId === id);
+ return comment ? (comment.username || comment.userId || '未知用户') : '未知用户';
+ };
+ // 帖子图片处理,imgUrl 是单字符串,包装成数组
+ const getPostImages = () => {
+ if (!postDetail) return [];
+ if (postDetail.imgUrl) return [formatImageUrl(postDetail.imgUrl)];
+ return [];
+ };
return (
<div className="post-detail-page">
@@ -207,122 +178,116 @@
<div className="post-detail">
<h1>{postDetail.title}</h1>
<div className="post-meta">
- <span className="post-user">用户ID: {postDetail.user_id}</span>
+ <div className="post-user-info">
+ {postDetail.avatarUrl ? (
+ <img
+ className="avatar"
+ src={formatAvatarUrlNoDefault(postDetail.avatarUrl)}
+ alt={postDetail.username || '用户头像'}
+ />
+ ) : null}
+ <span className="post-username">{postDetail.username || '匿名用户'}</span>
+ </div>
<span className="post-time">
- 发布时间:{new Date(postDetail.postTime).toLocaleString()}
+ 发布时间:{new Date(postDetail.postTime).toLocaleString()}
</span>
</div>
+
<div className="post-content">
<p>{postDetail.postContent}</p>
- {Array.isArray(postDetail.imgUrl) ? (
- <div className="post-images">
- {postDetail.imgUrl.map((url, idx) => (
- <img key={idx} src={url} alt={`图片${idx}`} />
- ))}
- </div>
- ) : (
- postDetail.imgUrl && (
- <img className="post-image" src={postDetail.imgUrl} alt="帖子图片" />
- )
- )}
-
+ <div className="post-images">
+ {getPostImages().map((url, idx) => (
+ <img key={idx} src={url} alt={`图片${idx + 1}`} />
+ ))}
+ </div>
</div>
- {/* 点赞和收藏 */}
<div className="post-actions">
- <button
- className="icon-btn"
- onClick={toggleLike} // 点赞操作
- >
- <GoodTwo
- theme="outline"
- size="20"
- fill={isLiked ? '#f00' : '#ccc'} // 如果已点赞,显示红色
- />
+ <button className="icon-btn" onClick={toggleLike} title="点赞">
+ <GoodTwo theme="outline" size="20" fill={isLiked ? '#f00' : '#ccc'} />
<span>{postDetail.postLikeNum}</span>
</button>
- <button
- className="icon-btn"
- onClick={toggleCollect} // 收藏操作
- >
- <Star
- theme="outline"
- size="20"
- fill={isCollected ? '#ffd700' : '#ccc'} // 如果已收藏,显示金色
- />
+ <button className="icon-btn" onClick={toggleCollect} title="收藏">
+ <Star theme="outline" size="20" fill={isCollected ? '#ffd700' : '#ccc'} />
<span>{postDetail.postCollectNum}</span>
</button>
</div>
-
+
<hr className="divider" />
- {/* 评论部分 */}
+
<h3>评论区</h3>
- <div className="comments-section">
- {comments.length ? (
- comments.map((comment) => (
- <div key={comment.commentId} className="comment">
- <div className="comment-header">
- <span className="comment-user">用户 ID: {comment.userId}</span>
- <button className="reply-btn" onClick={() => handleReply(comment.commentId)}>回复</button>
- </div>
-
- <p className="comment-content">
- {comment.comCommentId ? (
- <>
- <span className="reply-to">回复 {findUsernameByCommentId(comment.comCommentId)}:</span>
- {comment.content}
- </>
- ) : (
- comment.content
- )}
- </p>
-
- <div className="comment-time">
- {new Date(comment.commentTime).toLocaleString()}
- </div>
-
- {/* 回复框 */}
- {replyToCommentId === comment.commentId && (
- <div className="reply-form">
- <div className="replying-to">
- 回复 <strong>{replyToUsername}</strong>:
- </div>
- <textarea
- placeholder="输入你的回复..."
- value={newComment}
- onChange={(e) => setNewComment(e.target.value)}
+ <div className="comments-section">
+ {comments.length ? comments.map(comment => (
+ <div key={comment.commentId} className="comment">
+ <div className="comment-header">
+ <div className="comment-user-info">
+ {comment.userAvatar ? (
+ <img
+ className="avatar-small"
+ src={formatAvatarUrlNoDefault(comment.userAvatar)}
+ alt={comment.username || '用户头像'}
/>
- <div className="comment-options">
- <button onClick={handleAddComment}>发布回复</button>
- </div>
- </div>
- )}
+ ) : null}
+ <span className="comment-username">{comment.username || '匿名用户'}</span>
+ </div>
+ <button className="reply-btn" onClick={() => handleReply(comment.commentId)}>回复</button>
</div>
- ))
- ) : (
- <p>暂无评论</p>
- )}
+ <p className="comment-content">
+ {comment.comCommentId ? (
+ <>
+ <span className="reply-to">回复 {findUsernameByCommentId(comment.comCommentId)}:</span>
+ {comment.content}
+ </>
+ ) : (
+ comment.content
+ )}
+ </p>
- {/* 添加评论表单 */}
- <div className="add-comment-form">
- <textarea
- placeholder="输入你的评论..."
- value={newComment}
- onChange={(e) => setNewComment(e.target.value)}
- />
- <div className="comment-options">
- {/* <label>
- <input
- type="checkbox"
- checked={isAnonymous}
- onChange={() => setIsAnonymous(!isAnonymous)}
- />
- 匿名评论
- </label> */}
- <button onClick={handleAddComment}>发布评论</button>
+ <div className="comment-time">
+ {new Date(comment.commentTime).toLocaleString()}
+ </div>
+
+ {replyToCommentId === comment.commentId && (
+ <div className="reply-form">
+ <div className="replying-to">
+ 回复 <strong>{replyToUsername}</strong>:
+ </div>
+ <textarea
+ placeholder="输入你的回复..."
+ value={newComment}
+ onChange={(e) => setNewComment(e.target.value)}
+ />
+ <div className="comment-options">
+ <button onClick={handleAddComment}>发布回复</button>
+ <button
+ onClick={() => {
+ setReplyToCommentId(null);
+ setReplyToUsername(null);
+ setNewComment('');
+ }}
+ style={{ marginLeft: '8px' }}
+ >
+ 取消
+ </button>
+ </div>
+ </div>
+ )}
</div>
- </div>
+ )) : <p>暂无评论</p>}
+
+ {!replyToCommentId && (
+ <div className="add-comment-form">
+ <textarea
+ placeholder="输入你的评论..."
+ value={newComment}
+ onChange={(e) => setNewComment(e.target.value)}
+ />
+ <div className="comment-options">
+ <button onClick={handleAddComment}>发布评论</button>
+ </div>
+ </div>
+ )}
</div>
</div>
) : (
@@ -332,4 +297,4 @@
);
};
-export default PostDetailPage;
\ No newline at end of file
+export default PostDetailPage;
diff --git a/src/pages/Forum/posts-main/components/CreatePostButton.jsx b/src/pages/Forum/posts-main/components/CreatePostButton.jsx
index 0f23e82..e324056 100644
--- a/src/pages/Forum/posts-main/components/CreatePostButton.jsx
+++ b/src/pages/Forum/posts-main/components/CreatePostButton.jsx
@@ -2,20 +2,18 @@
import axios from 'axios';
import { Edit } from '@icon-park/react';
import './CreatePostButton.css';
-
-const user = JSON.parse(localStorage.getItem('user')); // user = { user_id: 123, ... }
-const userId = user?.user_id;
-
+import { useUser } from '../../../../context/UserContext';
const CreatePostButton = () => {
- const [showModal, setShowModal] = useState(false);
+ const { user } = useUser();
+ const userId = user?.userId; // 这里改为 userId,跟 UserContext 统一
+ const [showModal, setShowModal] = useState(false);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [previewUrls, setPreviewUrls] = useState([]);
const [files, setFiles] = useState([]);
- // 选择图片并预览
const handleImageChange = (e) => {
const selectedFiles = Array.from(e.target.files);
if (!selectedFiles.length) return;
@@ -24,6 +22,10 @@
};
const handleSubmit = async () => {
+ if (!userId) {
+ alert('用户未登录或用户ID未获取到,无法发帖');
+ return;
+ }
if (!title.trim() || !content.trim()) {
alert('标题和内容均为必填项');
return;
@@ -34,7 +36,7 @@
formData.append('postContent', content.trim());
files.forEach(file => {
- formData.append('imageUrl', file); // 多文件使用同一个字段名
+ formData.append('imageUrl', file);
});
try {
@@ -46,7 +48,6 @@
}
);
- // 清空表单
setTitle('');
setContent('');
setFiles([]);
@@ -119,4 +120,3 @@
};
export default CreatePostButton;
-
diff --git a/src/pages/Forum/posts-main/components/PostList.jsx b/src/pages/Forum/posts-main/components/PostList.jsx
index 9e088ea..c1d9b7b 100644
--- a/src/pages/Forum/posts-main/components/PostList.jsx
+++ b/src/pages/Forum/posts-main/components/PostList.jsx
@@ -3,8 +3,16 @@
import { Link } from 'wouter';
import { GoodTwo, Star, Delete } from '@icon-park/react';
import { likePost } from '../../posts-detail/api';
+import { formatAvatarUrl } from '../../../../components/utils/avatar';
import './PostList.css';
+// 修改后的封面图 URL 拼接函数
+const formatImageUrl = (url) => {
+ if (!url) return '';
+ const filename = url.split('/').pop(); // 提取文件名部分
+ return `http://localhost:8080/uploads/post/${filename}`;
+};
+
const PostList = ({ search }) => {
const [posts, setPosts] = useState([]);
const [page, setPage] = useState(1);
@@ -21,49 +29,26 @@
setErrorMsg('');
try {
const res = await axios.get(`/echo/forum/posts/getAllPosts`, {
- params: {
- page,
- pageSize: size,
- sortBy: 'createdAt',
- order: 'desc'
- }
+ params: { page, pageSize: size, sortBy: 'createdAt', order: 'desc' }
});
- // 检查响应结构是否符合预期
- if (!res.data || !Array.isArray(res.data.posts)) {
- throw new Error('API返回格式不正确');
- }
+ if (!res.data || !Array.isArray(res.data.posts)) throw new Error('API返回格式不正确');
- const postsData = res.data.posts || [];
+ const postsData = res.data.posts;
- const userIds = [...new Set(postsData.map(post => post.user_id))];
+ const filteredPosts = postsData.filter(post =>
+ post.title?.toLowerCase().includes(search.toLowerCase())
+ );
- const profiles = await Promise.all(userIds.map(async id => {
- try {
- const r = await axios.get(`/echo/user/profile`, {
- params: { user_id: id }
- });
- return { id, profile: r.data };
- } catch (e) {
- return { id, profile: { nickname: '未知用户', avatar_url: 'default-avatar.png' } };
- }
+ const postsWithProfiles = filteredPosts.map(post => ({
+ ...post,
+ username: post.username || '未知用户',
+ avatarUrl: formatAvatarUrl(post.avatarUrl || ''),
+ liked: false,
+ collected: false,
+ commentCount: 0
}));
- const userMap = {};
- profiles.forEach(({ id, profile }) => {
- userMap[id] = profile;
- });
-
- const postsWithProfiles = postsData
- .filter(post => post.title.toLowerCase().includes(search.toLowerCase()))
- .map(post => ({
- ...post,
- userProfile: userMap[post.user_id] || { nickname: '未知用户', avatar_url: 'default-avatar.png' },
- liked: false,
- collected: false,
- commentCount: 0
- }));
-
setPosts(postsWithProfiles);
setTotal(res.data.total || 0);
} catch (err) {
@@ -78,77 +63,65 @@
}, [page, search]);
const toggleLike = async (postNo, liked, userId) => {
- try {
- if (liked) {
- // 修改为 POST 请求,并带上 user_id 参数
- await axios.post(`/echo/forum/posts/${postNo}/unlike`, {
- user_id: userId
- });
- } else {
- await likePost(postNo, userId); // 你已有的点赞逻辑
+ try {
+ if (liked) {
+ await axios.post(`/echo/forum/posts/${postNo}/unlike`, { user_id: userId });
+ } else {
+ await likePost(postNo, userId);
+ }
+
+ setPosts(posts =>
+ posts.map(post =>
+ post.postNo === postNo
+ ? {
+ ...post,
+ liked: !liked,
+ likeCount: liked ? post.likeCount - 1 : post.likeCount + 1
+ }
+ : post
+ )
+ );
+ } catch (err) {
+ console.error('点赞失败:', err);
+ alert('点赞操作失败,请稍后重试');
}
+ };
- setPosts(posts =>
- posts.map(post =>
- post.postNo === postNo
- ? { ...post, liked: !liked, likeCount: liked ? post.likeCount - 1 : post.likeCount + 1 }
- : post
- )
- );
- } catch (err) {
- console.error('点赞失败:', err);
- alert('点赞操作失败,请稍后重试');
- }
-};
-
-
- // 收藏帖子
const toggleCollect = async (postNo, collected, userId) => {
- try {
- if (collected) {
- // 取消收藏:DELETE 请求 + JSON 请求体
- await axios.delete(`/echo/forum/posts/${postNo}/uncollect`, {
- data: { user_id: userId } // 注意:DELETE 请求的请求体需放在 data 字段中
- });
- } else {
- // 收藏:POST 请求 + JSON 请求体
- await axios.post(`/echo/forum/posts/${postNo}/collect`, {
- user_id: userId
- });
+ try {
+ if (collected) {
+ await axios.delete(`/echo/forum/posts/${postNo}/uncollect`, {
+ data: { user_id: userId }
+ });
+ } else {
+ await axios.post(`/echo/forum/posts/${postNo}/collect`, {
+ user_id: userId
+ });
+ }
+
+ setPosts(posts =>
+ posts.map(post =>
+ post.postNo === postNo
+ ? {
+ ...post,
+ collected: !collected,
+ collectCount: collected ? post.collectCount - 1 : post.collectCount + 1
+ }
+ : post
+ )
+ );
+ } catch (err) {
+ console.error('收藏操作失败:', err);
+ alert('收藏操作失败,请稍后重试');
}
+ };
- setPosts(posts =>
- posts.map(post =>
- post.postNo === postNo
- ? {
- ...post,
- collected: !collected,
- collectCount: collected ? post.collectCount - 1 : post.collectCount + 1
- }
- : post
- )
- );
- } catch (err) {
- console.error('收藏操作失败:', err.response?.data || err.message);
- alert('收藏操作失败,请稍后重试');
- }
-};
-
-
-
- // 删除帖子
const handleDeletePost = async (postNo) => {
if (window.confirm('确定要删除这篇帖子吗?')) {
try {
await axios.delete(`/echo/forum/posts/${postNo}/deletePost`);
-
- // 从列表中移除已删除的帖子
setPosts(posts => posts.filter(post => post.postNo !== postNo));
-
- // 如果删除后当前页没有帖子了,尝试加载上一页
- if (posts.length === 1 && page > 1) {
- setPage(page - 1);
- }
+ if (posts.length === 1 && page > 1) setPage(page - 1);
} catch (err) {
console.error('删除帖子失败:', err);
alert('删除帖子失败,请稍后再试');
@@ -161,43 +134,52 @@
{loading ? <p>加载中...</p> :
errorMsg ? <p className="error-text">{errorMsg}</p> :
posts.length === 0 ? <p>暂无帖子。</p> :
- posts.map(post => (
- <div
- key={post.postNo}
- className="post-card"
- style={{ backgroundColor: '#e9ded2' }}
- >
- <div className="user-info">
- <img className="avatar" src={post.userProfile.avatar_url} alt="头像" />
- <span className="nickname" style={{ color: '#755e50' }}>{post.userProfile.nickname}</span>
- </div>
- {post.imgUrl && (
- <img className="cover-image" src={post.imgUrl} alt="封面" />
- )}
- <h3 style={{ color: '#000000' }}>{post.title}</h3>
- <div className="post-meta">
- <span>发布时间:{new Date(post.createdAt).toLocaleString()}</span>
- <div className="post-actions">
- <button className="icon-btn" onClick={() => toggleLike(post.postNo, post.liked, post.user_id)}>
- <GoodTwo theme="outline" size="24" fill={post.liked ? '#f00' : '#fff'} />
- <span>{post.likeCount}</span>
- </button>
+ posts.map(post => {
+ const createdAtDate = new Date(post.createdAt);
+ const timeText = isNaN(createdAtDate.getTime()) ? '时间未设置' : createdAtDate.toLocaleString();
- <button className="icon-btn" onClick={() => toggleCollect(post.postNo, post.collected, post.user_id)}>
- <Star theme="outline" size="24" fill={post.collected ? '#ffd700' : '#fff'} />
- <span>{post.collectCount}</span>
- </button>
-
- <button className="icon-btn" onClick={() => handleDeletePost(post.postNo)}>
- <Delete theme="outline" size="24" fill="#333" />
- </button>
+ let coverImage = null;
+ if (post.imgUrl) {
+ const imgs = post.imgUrl.split(',').map(i => i.trim()).filter(Boolean);
+ coverImage = imgs.length > 0 ? formatImageUrl(imgs[0]) : null;
+ }
+
+ return (
+ <div key={post.postNo} className="post-card" style={{ backgroundColor: '#e9ded2' }}>
+ <div className="user-info">
+ <img
+ className="avatar"
+ src={post.avatarUrl}
+ alt="头像"
+ />
+ <span className="nickname" style={{ color: '#755e50' }}>{post.username}</span>
+ </div>
+
+ {coverImage && <img className="cover-image" src={coverImage} alt="封面" />}
+
+ <h3 style={{ color: '#000000' }}>{post.title || '无标题'}</h3>
+ <div className="post-meta">
+ <span>发布时间:{timeText}</span>
+ <div className="post-actions">
+ <button className="icon-btn" onClick={() => toggleLike(post.postNo, post.liked, post.user_id)}>
+ <GoodTwo theme="outline" size="24" fill={post.liked ? '#f00' : '#fff'} />
+ <span>{post.likeCount}</span>
+ </button>
+ <button className="icon-btn" onClick={() => toggleCollect(post.postNo, post.collected, post.user_id)}>
+ <Star theme="outline" size="24" fill={post.collected ? '#ffd700' : '#fff'} />
+ <span>{post.collectCount}</span>
+ </button>
+ <button className="icon-btn" onClick={() => handleDeletePost(post.postNo)}>
+ <Delete theme="outline" size="24" fill="#333" />
+ </button>
+ </div>
+ </div>
+ <div className="detail-button-wrapper">
+ <Link href={`/forum/post/${post.postNo}`} className="detail-button">查看详情</Link>
</div>
</div>
- <div className="detail-button-wrapper">
- <Link href={`/forum/post/${post.postNo}`} className="detail-button">查看详情</Link>
- </div>
- </div>
- ))
+ );
+ })
}
<div className="pagination">
@@ -209,4 +191,4 @@
);
};
-export default PostList;
\ No newline at end of file
+export default PostList;
diff --git a/src/pages/PublishSeed/PublishSeed.jsx b/src/pages/PublishSeed/PublishSeed.jsx
index e3be08c..f27ebc2 100644
--- a/src/pages/PublishSeed/PublishSeed.jsx
+++ b/src/pages/PublishSeed/PublishSeed.jsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useRef } from 'react';
import axios from 'axios';
import Header from '../../components/Header';
import './PublishSeed.css';
@@ -9,23 +9,36 @@
const [description, setDescription] = useState('');
const [tags, setTags] = useState([]);
const [category, setCategory] = useState('movie');
- const [file, setFile] = useState(null);
const [imageUrl, setImageUrl] = useState('');
const [message, setMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
+ const [fileName, setFileName] = useState('');
- const { user } = useUser(); // 从上下文中获取当前登录用户信息
+ const fileInputRef = useRef(null); // ✅ 获取 input file 引用
+ const { user } = useUser();
const handleTagsChange = (e) => {
setTags(e.target.value.split(',').map(tag => tag.trim()));
};
+ const handleFileButtonClick = () => {
+ fileInputRef.current?.click(); // 点击隐藏的 input
+ };
+
const handleFileChange = (e) => {
- setFile(e.target.files[0]);
+ const selectedFile = e.target.files[0];
+ if (selectedFile) {
+ setFileName(selectedFile.name); // 仅展示文件名
+ }
};
const handleSubmit = async (e) => {
e.preventDefault();
+ console.log('[handleSubmit] 表单提交开始');
+
+ const currentFile = fileInputRef.current?.files[0]; // ✅ 获取文件
+ console.log('[handleSubmit] currentFile:', currentFile);
+
setIsLoading(true);
setMessage('');
@@ -35,20 +48,20 @@
return;
}
- if (!file || !file.name.endsWith('.torrent')) {
+ if (!currentFile || !currentFile.name.toLowerCase().endsWith('.torrent')) {
setMessage('请上传一个 .torrent 文件');
setIsLoading(false);
return;
}
const formData = new FormData();
- formData.append('file', file); // 文件字段
+ formData.append('file', currentFile);
formData.append('title', title);
formData.append('description', description);
formData.append('category', category);
formData.append('imageUrl', imageUrl);
- formData.append('tags', tags.join(',')); // 后端使用字符串或数组自行处理
- formData.append('uploader', user.id); // 添加上传者 ID(必须字段)
+ formData.append('tags', tags.join(','));
+ formData.append('uploader', user.id);
try {
const response = await axios.post('/seeds/upload', formData, {
@@ -63,7 +76,7 @@
setMessage(response.data.msg || '上传失败,请稍后再试');
}
} catch (error) {
- console.error(error);
+ console.error('[handleSubmit] 上传失败:', error);
setMessage('上传失败,发生了错误');
} finally {
setIsLoading(false);
@@ -121,16 +134,17 @@
<div className="seed-file">
<label>种子文件</label>
- <label className="seed-file-label">
+ <div className="seed-file-label" onClick={handleFileButtonClick}>
点击选择文件
- <input
- type="file"
- accept=".torrent"
- onChange={handleFileChange}
- style={{ display: 'none' }}
- />
- </label>
- {file && <div style={{ marginTop: '5px' }}>{file.name}</div>}
+ </div>
+ <input
+ type="file"
+ accept=".torrent"
+ ref={fileInputRef}
+ onChange={handleFileChange}
+ style={{ display: 'none' }}
+ />
+ {fileName && <div style={{ marginTop: '5px' }}>{fileName}</div>}
</div>
<div className="form-group">
diff --git a/src/pages/PublishSeed/SimpleUploader.jsx b/src/pages/PublishSeed/SimpleUploader.jsx
new file mode 100644
index 0000000..0f44b6a
--- /dev/null
+++ b/src/pages/PublishSeed/SimpleUploader.jsx
@@ -0,0 +1,45 @@
+import React, { useRef, useState } from 'react';
+import axios from 'axios';
+
+const SimpleUploader = () => {
+ const fileInputRef = useRef(null);
+ const [message, setMessage] = useState('');
+
+ const handleUpload = async () => {
+ const file = fileInputRef.current?.files[0];
+ console.log('[handleUpload] file:', file);
+
+ if (!file) {
+ setMessage('请先选择文件');
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const response = await axios.post('/seeds/upload', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+
+ console.log('[handleUpload] response:', response);
+ setMessage(response.data?.msg || '上传成功');
+ } catch (err) {
+ console.error('[handleUpload] 上传失败:', err);
+ setMessage('上传失败');
+ }
+ };
+
+ return (
+ <div style={{ padding: '2rem' }}>
+ <h2>种子上传测试</h2>
+ <input type="file" accept=".torrent" ref={fileInputRef} />
+ <button onClick={handleUpload} style={{ marginLeft: '1rem' }}>
+ 上传
+ </button>
+ <div style={{ marginTop: '1rem' }}>{message}</div>
+ </div>
+ );
+};
+
+export default SimpleUploader;
diff --git a/src/pages/UserCenter/UserProfile.css b/src/pages/UserCenter/UserProfile.css
index b63c408..d2f848b 100644
--- a/src/pages/UserCenter/UserProfile.css
+++ b/src/pages/UserCenter/UserProfile.css
@@ -55,4 +55,25 @@
padding: 10% 20%;
margin-left: 5%;
margin-right: 5%;
-}
\ No newline at end of file
+}
+.avatar-wrapper {
+ position: relative;
+ display: inline-block;
+}
+
+.avatar-upload-label {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ background: #3498db;
+ color: white;
+ padding: 4px 8px;
+ font-size: 12px;
+ cursor: pointer;
+ border-radius: 4px;
+ opacity: 0.85;
+}
+
+.avatar-upload-label:hover {
+ opacity: 1;
+}
diff --git a/src/pages/UserCenter/UserProfile.jsx b/src/pages/UserCenter/UserProfile.jsx
index a10657c..fb472c8 100644
--- a/src/pages/UserCenter/UserProfile.jsx
+++ b/src/pages/UserCenter/UserProfile.jsx
@@ -3,17 +3,17 @@
import './UserProfile.css';
import UserNav from './UserNav';
import Header from '../../components/Header';
-import { useUser } from '../../context/UserContext';
+import { useUser } from '../../context/UserContext';
+
+const DEFAULT_AVATAR_URL = `${process.env.PUBLIC_URL}/default-avatar.png`;
const UserProfile = () => {
- const { user, loading } = useUser(); // 从上下文拿用户和加载状态
+ const { user, loading } = useUser();
const [userProfile, setUserProfile] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
- if (loading) return; // 用户信息还没加载完,先不请求
- console.log('用户:', user, '加载:', loading);
-
+ if (loading) return;
if (!user || !user.userId) {
setError('未登录或用户信息缺失');
setUserProfile(null);
@@ -23,11 +23,7 @@
const fetchUserProfile = async () => {
try {
setError(null);
- const response = await axios.get(`/echo/user/${user.userId}/getProfile`);
-
- console.log('响应数据:', response); // 调试用
- const raw = response.data;
- console.log('raw:', raw); // 调试用
+ const { data: raw } = await axios.get(`/echo/user/${user.userId}/getProfile`);
if (!raw) {
setError('用户数据为空');
@@ -36,7 +32,9 @@
}
const profile = {
- avatar_url: raw.avatarUrl || 'https://example.com/default-avatar.jpg',
+ avatarUrl: raw.avatarUrl
+ ? `${process.env.REACT_APP_AVATAR_BASE_URL}${raw.avatarUrl}`
+ : DEFAULT_AVATAR_URL,
nickname: raw.username || '未知用户',
email: raw.email || '未填写',
gender: raw.gender || '保密',
@@ -44,19 +42,15 @@
interests: raw.hobbies ? raw.hobbies.split(',') : [],
level: raw.level || '未知',
experience: raw.experience ?? 0,
- upload_amount: raw.uploadCount ?? 0,
- download_amount: raw.downloadCount ?? 0,
- share_rate: raw.shareRate ?? 0,
- joined_date: raw.registrationTime,
+ uploadAmount: raw.uploadCount ?? 0,
+ downloadAmount: raw.downloadCount ?? 0,
+ shareRate: raw.shareRate ?? 0,
+ joinedDate: raw.registrationTime,
};
setUserProfile(profile);
} catch (err) {
- if (err.response?.status === 404) {
- setError('用户不存在');
- } else {
- setError('请求失败,请稍后再试');
- }
+ setError(err.response?.status === 404 ? '用户不存在' : '请求失败,请稍后再试');
setUserProfile(null);
}
};
@@ -64,10 +58,54 @@
fetchUserProfile();
}, [user, loading]);
+ const handleAvatarUpload = async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const { data } = await axios.post(
+ `/echo/user/${user.userId}/uploadAvatar`,
+ formData,
+ { headers: { 'Content-Type': 'multipart/form-data' } }
+ );
+
+ if (data?.avatarUrl) {
+ setUserProfile((prev) => ({
+ ...prev,
+ avatarUrl: `${process.env.REACT_APP_AVATAR_BASE_URL}${data.avatarUrl}`,
+ }));
+ alert('头像上传成功');
+ } else {
+ alert('头像上传成功,但未返回新头像地址');
+ }
+ } catch (err) {
+ console.error('上传失败:', err);
+ alert('头像上传失败,请重试');
+ }
+ };
+
if (loading) return <p>正在加载用户信息...</p>;
if (error) return <p className="error">{error}</p>;
if (!userProfile) return null;
+ const {
+ avatarUrl,
+ nickname,
+ email,
+ gender,
+ bio,
+ interests,
+ level,
+ experience,
+ uploadAmount,
+ downloadAmount,
+ shareRate,
+ joinedDate,
+ } = userProfile;
+
return (
<div className="user-profile-container">
<Header />
@@ -78,25 +116,33 @@
<div className="common-card">
<div className="right-content">
<div className="profile-header">
- <img
- src={userProfile.avatar_url}
- alt={userProfile.nickname}
- className="avatar"
- />
- <h1>{userProfile.nickname}</h1>
+ <div className="avatar-wrapper">
+ <img src={avatarUrl} alt={nickname} className="avatar" />
+ <label htmlFor="avatar-upload" className="avatar-upload-label">
+ 上传头像
+ </label>
+ <input
+ type="file"
+ id="avatar-upload"
+ accept="image/*"
+ style={{ display: 'none' }}
+ onChange={handleAvatarUpload}
+ />
+ </div>
+ <h1>{nickname}</h1>
</div>
<div className="profile-details">
- <p><strong>邮箱:</strong>{userProfile.email}</p>
- <p><strong>性别:</strong>{userProfile.gender}</p>
- <p><strong>个人简介:</strong>{userProfile.bio}</p>
- <p><strong>兴趣:</strong>{userProfile.interests.length > 0 ? userProfile.interests.join(', ') : '无'}</p>
- <p><strong>等级:</strong>{userProfile.level}</p>
- <p><strong>经验:</strong>{userProfile.experience}</p>
- <p><strong>上传量:</strong>{userProfile.upload_amount}</p>
- <p><strong>下载量:</strong>{userProfile.download_amount}</p>
- <p><strong>分享率:</strong>{(userProfile.share_rate * 100).toFixed(2)}%</p>
- <p><strong>加入时间:</strong>{new Date(userProfile.joined_date).toLocaleDateString()}</p>
+ <p><strong>邮箱:</strong>{email}</p>
+ <p><strong>性别:</strong>{gender}</p>
+ <p><strong>个人简介:</strong>{bio}</p>
+ <p><strong>兴趣:</strong>{interests.length > 0 ? interests.join(', ') : '无'}</p>
+ <p><strong>等级:</strong>{level}</p>
+ <p><strong>经验:</strong>{experience}</p>
+ <p><strong>上传量:</strong>{uploadAmount}</p>
+ <p><strong>下载量:</strong>{downloadAmount}</p>
+ <p><strong>分享率:</strong>{(shareRate * 100).toFixed(2)}%</p>
+ <p><strong>加入时间:</strong>{new Date(joinedDate).toLocaleDateString()}</p>
</div>
</div>
</div>