合并代码
Change-Id: Id5e0dc4f7b448b4ba615243bf1dfa964907f5d68
diff --git a/Merge/front/src/api/api_ljc.js b/Merge/front/src/api/api_ljc.js
new file mode 100644
index 0000000..b8130ca
--- /dev/null
+++ b/Merge/front/src/api/api_ljc.js
@@ -0,0 +1,31 @@
+import axios from 'axios';
+
+const api = axios.create({
+ baseURL: 'http://10.126.59.25:5715/api/',
+ withCredentials: true
+});
+
+// 用户相关API
+export const getCurrentUser = () => api.get('/current-user');
+export const getUser = (userId) => api.get(`/user/${userId}`);
+export const updateUser = (userId, data) => api.put(`/user/${userId}`, data);
+
+// 收藏相关API
+export const getFavorites = (userId) => api.get(`/user/${userId}/favorites`);
+
+// 关注相关API
+export const followUser = (followeeId) => api.post(`/follow/${followeeId}`);
+export const unfollowUser = (followeeId) => api.delete(`/follow/${followeeId}`);
+
+// 帖子相关API
+export const getUserPosts = (userId) => api.get(`/user/${userId}/posts`);
+
+// 关注列表API
+export const getUserFollowing = (userId) => api.get(`/user/${userId}/following`);
+
+// 用户互动数据API
+export const getUserInteractions = (userId) => api.get(`/user/${userId}/interactions`);
+// 获取粉丝
+export const getUserFollowers = (userId) => api.get(`/user/${userId}/followers`);
+
+export default api;
\ No newline at end of file
diff --git a/Merge/front/src/components/EditProfileForm.jsx b/Merge/front/src/components/EditProfileForm.jsx
new file mode 100644
index 0000000..d33cbcb
--- /dev/null
+++ b/Merge/front/src/components/EditProfileForm.jsx
@@ -0,0 +1,116 @@
+import React, { useState } from 'react';
+import { FaCamera, FaTimes } from 'react-icons/fa';
+
+const EditProfileForm = ({ user, onSave, onCancel }) => {
+ const [avatar, setAvatar] = useState(user.avatar || '');
+ const [bio, setBio] = useState(user.bio || '');
+ const [gender, setGender] = useState('secret');
+ const [birthday, setBirthday] = useState('');
+ const [location, setLocation] = useState('');
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ onSave({ avatar, bio });
+ };
+
+ return (
+ <form onSubmit={handleSubmit}>
+ <div className="mb-6">
+ <label className="block text-sm font-medium text-gray-700 mb-2">头像</label>
+ <div className="flex items-center">
+ <div className="relative">
+ <div className="w-20 h-20 rounded-full bg-gradient-to-r from-pink-300 to-orange-300 flex items-center justify-center">
+ {avatar ? (
+ <img src={avatar} alt="Avatar" className="w-full h-full rounded-full" />
+ ) : (
+ <div className="text-white text-2xl">{user.username.charAt(0)}</div>
+ )}
+ </div>
+ <button
+ type="button"
+ className="absolute bottom-0 right-0 bg-white rounded-full p-1 shadow-md"
+ >
+ <FaCamera className="text-gray-700 text-sm" />
+ </button>
+ </div>
+ <div className="ml-4">
+ <input
+ type="text"
+ value={avatar}
+ onChange={(e) => setAvatar(e.target.value)}
+ placeholder="输入头像URL"
+ className="w-full rounded-md border-gray-300 shadow-sm text-sm"
+ />
+ </div>
+ </div>
+ </div>
+
+ <div className="mb-4">
+ <label className="block text-sm font-medium text-gray-700 mb-2">个人简介</label>
+ <textarea
+ value={bio}
+ onChange={(e) => setBio(e.target.value)}
+ rows="3"
+ maxLength="100"
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm"
+ placeholder="介绍一下自己吧~"
+ />
+ <div className="text-right text-xs text-gray-500 mt-1">{bio.length}/100</div>
+ </div>
+
+ <div className="grid grid-cols-2 gap-4 mb-4">
+ <div>
+ <label className="block text-sm font-medium text-gray-700 mb-2">性别</label>
+ <select
+ value={gender}
+ onChange={(e) => setGender(e.target.value)}
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm"
+ >
+ <option value="secret">保密</option>
+ <option value="male">男</option>
+ <option value="female">女</option>
+ </select>
+ </div>
+
+ <div>
+ <label className="block text-sm font-medium text-gray-700 mb-2">生日</label>
+ <input
+ type="date"
+ value={birthday}
+ onChange={(e) => setBirthday(e.target.value)}
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm"
+ />
+ </div>
+ </div>
+
+ <div className="mb-6">
+ <label className="block text-sm font-medium text-gray-700 mb-2">地区</label>
+ <input
+ type="text"
+ value={location}
+ onChange={(e) => setLocation(e.target.value)}
+ placeholder="填写你所在的城市"
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm"
+ />
+ </div>
+
+ <div className="flex justify-end space-x-3">
+ <button
+ type="button"
+ onClick={onCancel}
+ className="px-5 py-2 bg-gray-100 text-gray-700 rounded-full text-sm hover:bg-gray-200"
+ >
+ 取消
+ </button>
+ <button
+ type="submit"
+ className="px-5 py-2 bg-red-500 text-white rounded-full text-sm hover:bg-red-600"
+ >
+ 保存
+ </button>
+ </div>
+ </form>
+ );
+};
+
+export default EditProfileForm;
\ No newline at end of file
diff --git a/Merge/front/src/components/FavoritePosts.jsx b/Merge/front/src/components/FavoritePosts.jsx
new file mode 100644
index 0000000..4afe125
--- /dev/null
+++ b/Merge/front/src/components/FavoritePosts.jsx
@@ -0,0 +1,95 @@
+import React, { useState, useEffect } from 'react';
+import { getFavorites } from '../api/api_ljc';
+import { FaHeart } from 'react-icons/fa';
+
+const FavoritePosts = ({ userId }) => {
+ const [favorites, setFavorites] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchFavorites = async () => {
+ try {
+ setLoading(true);
+ const response = await getFavorites(userId);
+ setFavorites(response.data);
+ } catch (error) {
+ console.error('Failed to fetch favorites:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (userId) {
+ fetchFavorites();
+ }
+ }, [userId]);
+
+ if (loading) {
+ return (
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
+ {[1, 2, 3, 4, 5, 6].map(item => (
+ <div key={item} className="bg-gray-100 rounded-xl aspect-square animate-pulse"></div>
+ ))}
+ </div>
+ );
+ }
+
+ if (favorites.length === 0) {
+ return (
+ <div className="text-center py-16">
+ <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 text-red-500 mb-4">
+ <FaHeart className="text-2xl" />
+ </div>
+ <h3 className="text-lg font-medium text-gray-900">暂无收藏内容</h3>
+ <p className="mt-1 text-gray-500">你还没有收藏任何笔记</p>
+ </div>
+ );
+ }
+
+ // 模拟瀑布流布局数据
+ const waterfallData = favorites.map(post => ({
+ ...post,
+ height: Math.floor(Math.random() * 100) + 200 // 随机高度
+ }));
+
+ return (
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
+ {waterfallData.map(post => (
+ <div
+ key={post.id}
+ className="bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow"
+ >
+ <div
+ className="relative bg-gray-200"
+ style={{ height: `${post.height}px` }}
+ >
+ {/* 占位图片 */}
+ <div className="absolute inset-0 bg-gradient-to-br from-pink-100 to-orange-100"></div>
+
+ {/* 类型标签 */}
+ <div className="absolute top-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded-full">
+ {post.type === 'image' ? '图文' :
+ post.type === 'video' ? '视频' : '文档'}
+ </div>
+
+ {/* 收藏标记 */}
+ <div className="absolute bottom-2 right-2 bg-red-500 rounded-full p-1">
+ <FaHeart className="text-white text-xs" />
+ </div>
+ </div>
+
+ <div className="p-3">
+ <h3 className="font-medium line-clamp-2">{post.title}</h3>
+ <div className="flex items-center mt-2 text-xs text-gray-500">
+ <span>❤️ 2.5k</span>
+ <span className="mx-2">•</span>
+ <span>⭐ 156</span>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ );
+};
+
+export default FavoritePosts;
\ No newline at end of file
diff --git a/Merge/front/src/components/FollowButton.jsx b/Merge/front/src/components/FollowButton.jsx
new file mode 100644
index 0000000..0776191
--- /dev/null
+++ b/Merge/front/src/components/FollowButton.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { followUser, unfollowUser } from '../api/api_ljc';
+
+const FollowButton = ({ userId, isFollowing, onFollowChange }) => {
+ const handleFollow = async () => {
+ try {
+ if (isFollowing) {
+ await unfollowUser(userId);
+ onFollowChange(false);
+ } else {
+ await followUser(userId);
+ onFollowChange(true);
+ }
+ } catch (error) {
+ console.error('关注操作失败:', error);
+ }
+ };
+
+ return (
+ <button
+ onClick={handleFollow}
+ className={`px-6 py-2 rounded-full text-sm font-medium transition-all ${
+ isFollowing
+ ? 'bg-gray-100 text-gray-800 hover:bg-gray-200'
+ : 'bg-red-500 text-white hover:bg-red-600'
+ }`}
+ >
+ {isFollowing ? '已关注' : '关注'}
+ </button>
+ );
+};
+
+export default FollowButton;
\ No newline at end of file
diff --git a/Merge/front/src/components/Header.jsx b/Merge/front/src/components/Header.jsx
index 60a50b7..3b21c98 100644
--- a/Merge/front/src/components/Header.jsx
+++ b/Merge/front/src/components/Header.jsx
@@ -1,18 +1,29 @@
import React from 'react'
+import { useNavigate } from 'react-router-dom'
import { User } from 'lucide-react'
import '../App.css' // 或者单独的 Header.css
export default function Header() {
+ const navigate = useNavigate()
+
+ const handleUserClick = () => {
+ navigate('/user/1') // 或者使用实际的用户ID
+ }
+
return (
<header className="header">
<div className="header-left">
<div className="logo">小红书</div>
<h1 className="header-title">创作服务平台</h1>
</div>
- <div className="header-right">
+ <div
+ className="header-right"
+ onClick={handleUserClick}
+ style={{ cursor: 'pointer' }}
+ >
<div className="user-info">
<User size={16} />
- <span>小红薯63081EA1</span>
+ <span>小红薯1</span>
</div>
</div>
</header>
diff --git a/Merge/front/src/components/HomeFeed.jsx b/Merge/front/src/components/HomeFeed.jsx
index 39e0ca8..c681858 100644
--- a/Merge/front/src/components/HomeFeed.jsx
+++ b/Merge/front/src/components/HomeFeed.jsx
@@ -1,6 +1,7 @@
// src/components/HomeFeed.jsx
import React, { useState, useEffect } from 'react'
+import { useNavigate } from 'react-router-dom'
import { ThumbsUp } from 'lucide-react'
import { fetchPosts, fetchPost } from '../api/posts_wzy'
import '../style/HomeFeed.css'
@@ -11,6 +12,7 @@
]
export default function HomeFeed() {
+ const navigate = useNavigate()
const [activeCat, setActiveCat] = useState('推荐')
const [items, setItems] = useState([])
const [loading, setLoading] = useState(true)
diff --git a/Merge/front/src/components/UserProfile.jsx b/Merge/front/src/components/UserProfile.jsx
new file mode 100644
index 0000000..8421997
--- /dev/null
+++ b/Merge/front/src/components/UserProfile.jsx
@@ -0,0 +1,1152 @@
+import React, { useState, useEffect, Profiler } from 'react';
+import {
+ Box,
+ Grid,
+ Typography,
+ Avatar,
+ Button,
+ Tabs,
+ Tab,
+ Card,
+ CardMedia,
+ CardContent,
+ CardActions,
+ IconButton,
+ Divider,
+ List,
+ ListItem,
+ ListItemAvatar,
+ ListItemText,
+ TextField,
+ InputAdornment,
+ Chip,
+ Badge,
+ Fab,
+ Paper,
+ MenuItem,
+ Container,
+ useMediaQuery,
+ useTheme,
+ CircularProgress,
+ Snackbar,
+ Alert
+} from '@mui/material';
+import { useParams } from 'react-router-dom';
+import {
+ CameraAlt,
+ Edit,
+ Favorite,
+ Bookmark,
+ Share,
+ MoreVert,
+ LocationOn,
+ Cake,
+ Female,
+ Male,
+ Public,
+ Add,
+ Search,
+ Notifications,
+ Person,
+ Collections,
+ Group,
+ ChevronLeft,
+ ChevronRight,
+ Close,
+ People
+} from '@mui/icons-material';
+import { createTheme, ThemeProvider } from '@mui/material/styles';
+import { Link, useNavigate } from 'react-router-dom';
+
+// 导入API服务
+import {
+ getCurrentUser,
+ getUser,
+ updateUser as updateUserApi,
+ getFavorites,
+ followUser as followUserApi,
+ unfollowUser as unfollowUserApi,
+ getUserPosts,
+ getUserFollowing,
+ getUserInteractions,
+ getUserFollowers
+} from '../api/api_ljc';
+
+// 创建小红书主题
+const theme = createTheme({
+ palette: {
+ primary: {
+ main: '#ff4081',
+ },
+ secondary: {
+ main: '#f50057',
+ },
+ background: {
+ default: '#f5f5f5',
+ },
+ },
+ typography: {
+ fontFamily: '"PingFang SC", "Helvetica Neue", Arial, sans-serif',
+ h5: {
+ fontWeight: 600,
+ },
+ subtitle1: {
+ color: 'rgba(0, 0, 0, 0.6)',
+ },
+ },
+ components: {
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ borderRadius: 20,
+ textTransform: 'none',
+ fontWeight: 500,
+ },
+ },
+ },
+ MuiCard: {
+ styleOverrides: {
+ root: {
+ borderRadius: 16,
+ },
+ },
+ },
+ },
+});
+
+const UserProfile = () => {
+ const { userId } = useParams();
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
+ const navigate = useNavigate();
+ const [activeTab, setActiveTab] = useState(0);
+ const [isEditing, setIsEditing] = useState(false);
+ const [followers, setFollowers] = useState([]);
+ const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' });
+
+ // 用户数据状态
+ const [currentUser, setCurrentUser] = useState(null);
+ const [profileUser, setProfileUser] = useState(null);
+ const [favorites, setFavorites] = useState([]);
+ const [following, setFollowing] = useState([]);
+ const [posts, setPosts] = useState([]);
+ const [interactions, setInteractions] = useState({
+ likes_count: 0,
+ favorites_count: 0
+ });
+
+ // 加载状态
+ const [loading, setLoading] = useState(true);
+ const [updating, setUpdating] = useState(false);
+ const [tabLoading, setTabLoading] = useState(false);
+
+ // 表单状态
+ const [formData, setFormData] = useState({
+ avatar: '',
+ bio: '',
+ gender: '',
+ birthday: '',
+ location: ''
+ });
+
+ // 显示提示信息
+ const showSnackbar = (message, severity = 'success') => {
+ setSnackbar({ open: true, message, severity });
+ };
+
+ // 加载用户数据
+ useEffect(() => {
+ const fetchInteractions = async () => {
+ try {
+ const response = await getUserInteractions(userId);
+ if (response.data.success) {
+ setInteractions(response.data.data);
+ } else {
+ console.error(response.data.error);
+ }
+ } catch (error) {
+ console.error('获取互动数据失败:', error);
+ }
+ };
+
+ fetchInteractions();
+
+ const handleFollowUser = async (followeeId) => {
+ try {
+ await followUserApi(followeeId);
+ showSnackbar('关注成功');
+
+ // 更新粉丝列表状态(将刚关注的用户标记为已关注)
+ setFollowers(prev => prev.map(user =>
+ user.id === followeeId ? { ...user, is_following: true } : user
+ ));
+
+ // 更新当前用户关注数
+ if (currentUser) {
+ setCurrentUser(prev => ({
+ ...prev,
+ following_count: prev.following_count + 1
+ }));
+ }
+
+ } catch (error) {
+ console.error('关注操作失败:', error);
+ showSnackbar('关注失败,请重试', 'error');
+ }
+ };
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+
+ // 获取当前登录用户
+ const currentUserRes = await getCurrentUser();
+ setCurrentUser(currentUserRes.data);
+
+ // 获取目标用户信息
+ console.log('userId', userId)
+ const profileUserRes = await getUser(userId);
+ setProfileUser(profileUserRes.data);
+ setFormData({
+ avatar: profileUserRes.data.avatar || '',
+ bio: profileUserRes.data.bio || '',
+ gender: profileUserRes.data.gender || '',
+ birthday: profileUserRes.data.birthday || '',
+ location: profileUserRes.data.location || ''
+ });
+
+ // 获取用户帖子
+ const postsRes = await getUserPosts(userId);
+ setPosts(postsRes.data);
+
+ // 获取用户互动数据(获赞和收藏数量)
+ const interactionsRes = await getUserInteractions(userId);
+ setInteractions(interactionsRes.data);
+
+ } catch (error) {
+ console.error('获取用户数据失败:', error);
+ showSnackbar('获取用户数据失败,请重试', 'error');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [userId]);
+
+ // 根据标签页加载数据
+ useEffect(() => {
+ const fetchTabData = async () => {
+ if (!profileUser) return;
+
+ try {
+ setTabLoading(true);
+
+ if (activeTab === 1) {
+ // 加载收藏数据
+ const favoritesRes = await getFavorites(userId);
+ setFavorites(favoritesRes.data);
+ } else if (activeTab === 2) {
+ // 加载关注列表
+ const followingRes = await getUserFollowing(userId);
+ setFollowing(followingRes.data);
+ console.log(followingRes.data)
+ } else if (activeTab === 3) {
+ // 加载粉丝列表
+ const followersRes = await getUserFollowers(userId);
+ //
+ setFollowers(followersRes.data.data);
+ console.log(followersRes.data.data)
+ }
+
+ } catch (error) {
+ console.error('加载数据失败:', error);
+ showSnackbar('加载数据失败,请重试', 'error');
+ } finally {
+ setTabLoading(false);
+ }
+ };
+
+ fetchTabData();
+ }, [activeTab, userId, profileUser]);
+
+ const handleTabChange = (event, newValue) => {
+ setActiveTab(newValue);
+ };
+
+ const handleFollowToggle = async () => {
+ if (!currentUser || !profileUser) return;
+
+ try {
+ if (profileUser.is_following) {
+ await unfollowUserApi(profileUser.id);
+ showSnackbar('已取消关注');
+ } else {
+ await followUserApi(profileUser.id);
+ showSnackbar('关注成功');
+ }
+
+ // 更新用户信息
+ const updatedUser = await getUser(userId);
+ setProfileUser(updatedUser.data);
+
+ } catch (error) {
+ console.error('关注操作失败:', error);
+ showSnackbar('操作失败,请重试', 'error');
+ }
+ };
+
+ const handleFollowUser = async (followeeId) => {
+ try {
+ await followUserApi(followeeId);
+ showSnackbar('关注成功');
+
+ // 更新粉丝列表状态
+ setFollowers(prev => prev.map(user =>
+ user.id === followeeId ? {...user, is_following: true} : user
+ ));
+
+ // 更新当前用户关注数
+ if (currentUser) {
+ setCurrentUser(prev => ({
+ ...prev,
+ following_count: prev.following_count + 1
+ }));
+ }
+
+ } catch (error) {
+ console.error('关注操作失败:', error);
+ showSnackbar('关注失败,请重试', 'error');
+ }
+ };
+
+ const handleUnfollow = async (followeeId, e) => {
+ e.stopPropagation(); // 阻止事件冒泡
+
+ try {
+ await unfollowUserApi(followeeId);
+ showSnackbar('已取消关注');
+
+ // 更新关注列表
+ setFollowing(prev => prev.filter(user => user.id !== followeeId));
+
+ // 更新当前用户关注数
+ if (currentUser) {
+ setCurrentUser(prev => ({
+ ...prev,
+ following_count: prev.following_count - 1
+ }));
+ }
+
+ // 更新目标用户粉丝数
+ setProfileUser(prev => ({
+ ...prev,
+ followers_count: prev.followers_count - 1
+ }));
+
+ } catch (error) {
+ console.error('取消关注失败:', error);
+ showSnackbar('操作失败,请重试', 'error');
+ }
+ };
+
+ const handleFormChange = (e) => {
+ const { name, value } = e.target;
+ setFormData({ ...formData, [name]: value });
+ };
+
+ const handleUpdateProfile = async () => {
+ if (!profileUser) return;
+
+ try {
+ setUpdating(true);
+ const data = {
+ avatar: formData.avatar,
+ bio: formData.bio,
+ gender: formData.gender,
+ birthday: formData.birthday,
+ location: formData.location
+ };
+
+ // 调用更新API
+ const updatedUser = await updateUserApi(profileUser.id, data);
+
+ // 更新本地状态
+ setProfileUser({ ...profileUser, ...updatedUser.data });
+ setFormData({ ...formData, ...data });
+
+
+ showSnackbar('个人资料更新成功');
+ setIsEditing(false);
+
+ } catch (error) {
+ console.error('更新个人资料失败:', error);
+ showSnackbar('更新失败,请重试', 'error');
+ } finally {
+ setUpdating(false);
+ }
+ };
+
+ const navigateToUserProfile = (userId) => {
+ navigate(`/user/${userId}`);
+ };
+
+ if (loading) {
+ return (
+ <Box sx={{
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '100vh'
+ }}>
+ <CircularProgress size={60} />
+ </Box>
+ );
+ }
+
+ if (!profileUser) {
+ return (
+ <Box sx={{
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '100vh',
+ flexDirection: 'column'
+ }}>
+ <Typography variant="h6" sx={{ mb: 2 }}>用户不存在</Typography>
+ <Button variant="outlined" onClick={() => window.location.reload()}>
+ 重新加载
+ </Button>
+ </Box>
+ );
+ }
+
+ const isOwnProfile = currentUser && currentUser.id === parseInt(userId);
+
+ return (
+ <ThemeProvider theme={theme}>
+ <Box sx={{
+ bgcolor: 'background.default',
+ minHeight: '100vh',
+ pb: isMobile ? 8 : 4
+ }}>
+ {/* 顶部横幅 */}
+ <Box sx={{
+ height: isMobile ? 200 : 250,
+ background: 'linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%)',
+ position: 'relative',
+ borderBottomLeftRadius: 24,
+ borderBottomRightRadius: 24,
+ boxShadow: 1
+ }}>
+ <Fab
+ color="primary"
+ size="small"
+ sx={{
+ position: 'absolute',
+ bottom: -20,
+ right: 16
+ }}
+ >
+ <CameraAlt />
+ </Fab>
+ </Box>
+
+ <Container maxWidth="lg">
+ {/* 用户信息区域 */}
+ <Box sx={{ px: isMobile ? 3 : 0, mt: -8, position: 'relative' }}>
+ <Grid container spacing={3}>
+ <Grid item xs={12} sm="auto">
+ <Badge
+ overlap="circular"
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
+ badgeContent={
+ <IconButton
+ size="small"
+ sx={{
+ bgcolor: 'grey.200',
+ '&:hover': { bgcolor: 'grey.300' }
+ }}
+ onClick={() => setIsEditing(true)}
+ disabled={!isOwnProfile}
+ >
+ <Edit fontSize="small" />
+ </IconButton>
+ }
+ >
+ <Avatar
+ sx={{
+ width: 120,
+ height: 120,
+ border: '4px solid white',
+ boxShadow: 3
+ }}
+ src={profileUser.avatar || 'https://www.8848seo.cn/zb_users/upload/2023/02/20230210092856_68763.jpeg'}
+ />
+ </Badge>
+ </Grid>
+
+ <Grid item xs={12} sm>
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', flexDirection: isMobile ? 'column' : 'row' }}>
+ <Box>
+ <Typography variant="h5" fontWeight="bold">
+ {profileUser.username}
+ </Typography>
+ <Typography variant="subtitle1" sx={{ mt: 0.5, maxWidth: 600 }}>
+ {profileUser.bio || '这个人很懒,还没有写简介~'}
+ </Typography>
+ <Box sx={{ display: 'flex', mt: 1, gap: 1, flexWrap: 'wrap' }}>
+ <Chip
+ icon={<LocationOn fontSize="small" />}
+ label={formData.location}
+ size="small"
+ variant="outlined"
+ />
+ <Chip
+ icon={<Cake fontSize="small" />}
+ label={formData.birthday}
+ size="small"
+ variant="outlined"
+ />
+ <Chip
+ icon={<Female fontSize="small" />}
+ label={formData.gender}
+ size="small"
+ variant="outlined"
+ />
+ </Box>
+ </Box>
+
+ <Box sx={{ mt: isMobile ? 2 : 0, alignSelf: 'flex-start' }}>
+ {!isOwnProfile && currentUser && (
+ <>
+ <Button
+ variant={profileUser.is_following ? "outlined" : "contained"}
+ color="primary"
+ onClick={handleFollowToggle}
+ sx={{
+ borderRadius: 20,
+ px: 3,
+ fontWeight: 'bold'
+ }}
+ >
+ {profileUser.is_following ? '已关注' : '关注'}
+ </Button>
+ <IconButton sx={{ ml: 1 }}>
+ <MoreVert />
+ </IconButton>
+ </>
+ )}
+ </Box>
+ </Box>
+
+ <Grid container spacing={2} sx={{ mt: 2 }}>
+ <Grid item>
+ <Box textAlign="center">
+ <Typography variant="h6">{posts.length}</Typography>
+ <Typography variant="body2" color="textSecondary">笔记</Typography>
+ </Box>
+ </Grid>
+ <Grid item>
+ <Box textAlign="center">
+ <Typography variant="h6">{profileUser.followers_count || 0}</Typography>
+ <Typography variant="body2" color="textSecondary">粉丝</Typography>
+ </Box>
+ </Grid>
+ <Grid item>
+ <Box textAlign="center">
+ <Typography variant="h6">{profileUser.following_count || 0}</Typography>
+ <Typography variant="body2" color="textSecondary">关注</Typography>
+ </Box>
+ </Grid>
+ <Grid item>
+ <Box textAlign="center">
+ {/* 使用真实数据:获赞与收藏总数 */}
+ <Typography variant="h6">
+ {(interactions.likes_count + interactions.favorites_count).toLocaleString()}
+ </Typography>
+ <Typography variant="body2" color="textSecondary">获赞与收藏</Typography>
+ </Box>
+ </Grid>
+ </Grid>
+ </Grid>
+ </Grid>
+ </Box>
+
+ {/* 标签栏 */}
+ <Box sx={{ mt: 4 }}>
+ <Tabs
+ value={activeTab}
+ onChange={handleTabChange}
+ variant={isMobile ? "fullWidth" : "standard"}
+ indicatorColor="primary"
+ textColor="primary"
+ sx={{
+ borderBottom: 1,
+ borderColor: 'divider'
+ }}
+ >
+ <Tab icon={isMobile ? <Collections /> : null} label="笔记" />
+ <Tab icon={isMobile ? <Bookmark /> : null} label="收藏" />
+ <Tab icon={isMobile ? <Group /> : null} label="关注" />
+ <Tab icon={isMobile ? <People /> : null} label="粉丝" />
+ </Tabs>
+ </Box>
+
+ {/* 内容区域 */}
+ <Box sx={{ mt: 3 }}>
+ {activeTab === 0 && (
+ <Grid container spacing={3}>
+ {tabLoading ? (
+ <Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
+ <CircularProgress />
+ </Grid>
+ ) : posts.length > 0 ? (
+ posts.map((post, index) => (
+ <Grid item xs={12} sm={6} lg={3} key={post.id}>
+ <Card elevation={0} sx={{
+ bgcolor: 'white',
+ borderRadius: 3,
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column'
+ }}>
+ <CardMedia
+ component="img"
+ height="180"
+ image={`https://source.unsplash.com/random/400x300?${index + 1}`}
+ alt={post.title}
+ />
+ <CardContent sx={{ flexGrow: 1 }}>
+ <Typography gutterBottom variant="h6" component="div">
+ {post.title}
+ </Typography>
+ <Typography variant="body2" color="text.secondary">
+ {post.content.substring(0, 60)}...
+ </Typography>
+ </CardContent>
+ <CardActions sx={{ justifyContent: 'space-between', px: 2, pb: 2 }}>
+ <Box>
+ <IconButton aria-label="add to favorites">
+ <Favorite />
+ <Typography variant="body2" sx={{ ml: 1 }}>
+ {post.heat || Math.floor(Math.random() * 1000) + 1000}
+ </Typography>
+ </IconButton>
+ <IconButton aria-label="share">
+ <Share />
+ </IconButton>
+ </Box>
+ <Chip
+ label={post.type === 'image' ? '图文' : post.type === 'video' ? '视频' : '文档'}
+ size="small"
+ color="primary"
+ variant="outlined"
+ />
+ </CardActions>
+ </Card>
+ </Grid>
+ ))
+ ) : (
+ <Grid item xs={12}>
+ <Box sx={{
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ py: 8,
+ textAlign: 'center'
+ }}>
+ <Collections sx={{ fontSize: 60, color: 'grey.300', mb: 2 }} />
+ <Typography variant="h6" sx={{ mb: 1 }}>
+ 还没有发布笔记
+ </Typography>
+ <Typography variant="body1" color="textSecondary" sx={{ mb: 3 }}>
+ {isOwnProfile ? '分享你的生活点滴吧~' : '该用户还没有发布任何笔记'}
+ </Typography>
+ {isOwnProfile && (
+ <Button variant="contained" color="primary">
+ 发布第一篇笔记
+ </Button>
+ )}
+ </Box>
+ </Grid>
+ )}
+
+ {posts.length > 0 && (
+ <Grid item xs={12}>
+ <Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
+ <Button
+ variant="outlined"
+ sx={{
+ borderRadius: 20,
+ px: 4,
+ display: 'flex',
+ alignItems: 'center'
+ }}
+ >
+ <ChevronLeft sx={{ mr: 1 }} />
+ 上一页
+ <ChevronRight sx={{ ml: 2 }} />
+ </Button>
+ </Box>
+ </Grid>
+ )}
+ </Grid>
+ )}
+
+ {activeTab === 1 && (
+ <Grid container spacing={3}>
+ {tabLoading ? (
+ <Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
+ <CircularProgress />
+ </Grid>
+ ) : favorites.length > 0 ? (
+ favorites.map((favorite) => (
+ <Grid item xs={12} sm={6} md={4} lg={3} key={favorite.id}>
+ <Card elevation={0} sx={{
+ bgcolor: 'white',
+ borderRadius: 3,
+ transition: 'transform 0.3s, box-shadow 0.3s',
+ '&:hover': {
+ transform: 'translateY(-5px)',
+ boxShadow: 3
+ }
+ }}>
+ <Box sx={{
+ height: 160,
+ position: 'relative',
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ overflow: 'hidden'
+ }}>
+ <CardMedia
+ component="img"
+ height="160"
+ image={`https://source.unsplash.com/random/400x300?${favorite.id}`}
+ alt={favorite.title}
+ />
+ <Box sx={{
+ position: 'absolute',
+ top: 8,
+ right: 8,
+ bgcolor: 'rgba(0,0,0,0.6)',
+ color: 'white',
+ px: 1,
+ py: 0.5,
+ borderRadius: 4,
+ fontSize: 12
+ }}>
+ {favorite.type === 'image' ? '图文' : favorite.type === 'video' ? '视频' : '文档'}
+ </Box>
+ </Box>
+ <CardContent>
+ <Typography gutterBottom variant="subtitle1" fontWeight="medium">
+ {favorite.title}
+ </Typography>
+ <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
+ <Favorite fontSize="small" color="error" />
+ <Typography variant="body2" sx={{ ml: 0.5 }}>
+ {favorite.heat || Math.floor(Math.random() * 1000) + 1000}
+ </Typography>
+ </Box>
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
+ <Bookmark fontSize="small" color="primary" />
+ <Typography variant="body2" sx={{ ml: 0.5 }}>
+ {Math.floor(Math.random() * 500) + 100}
+ </Typography>
+ </Box>
+ </Box>
+ </CardContent>
+ </Card>
+ </Grid>
+ ))
+ ) : (
+ <Grid item xs={12}>
+ <Box sx={{
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ py: 8,
+ textAlign: 'center'
+ }}>
+ <Bookmark sx={{ fontSize: 60, color: 'grey.300', mb: 2 }} />
+ <Typography variant="h6" sx={{ mb: 1 }}>
+ {isOwnProfile ? '你还没有收藏内容' : '该用户没有收藏内容'}
+ </Typography>
+ <Typography variant="body1" color="textSecondary">
+ {isOwnProfile ? '看到喜欢的笔记可以收藏起来哦~' : ''}
+ </Typography>
+ </Box>
+ </Grid>
+ )}
+ </Grid>
+ )}
+
+ {activeTab === 2 && (
+ <Grid container spacing={3}>
+ {tabLoading ? (
+ <Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
+ <CircularProgress />
+ </Grid>
+ ) : following.length > 0 ? (
+ following.map((follow) => (
+ <Grid item xs={12} sm={6} md={4} key={follow.id}>
+ <Paper
+ elevation={0}
+ sx={{
+ bgcolor: 'white',
+ borderRadius: 3,
+ p: 2,
+ display: 'flex',
+ alignItems: 'center',
+ cursor: 'pointer',
+ '&:hover': {
+ boxShadow: 1
+ }
+ }}
+ onClick={() => navigateToUserProfile(follow.id)}
+ >
+ <Avatar
+ src={follow.avatar || 'https://randomuser.me/api/portraits/men/22.jpg'}
+ sx={{ width: 60, height: 60 }}
+ />
+ <Box sx={{ ml: 2, flexGrow: 1 }}>
+ <Typography fontWeight="medium">{follow.username}</Typography>
+ <Typography variant="body2" color="textSecondary">
+ {follow.followers_count || Math.floor(Math.random() * 100) + 10} 粉丝
+ </Typography>
+ </Box>
+ {isOwnProfile && (
+ <Button
+ variant="outlined"
+ size="small"
+ sx={{ borderRadius: 20 }}
+ onClick={(e) => handleUnfollow(follow.id, e)}
+ >
+ 已关注
+ </Button>
+ )}
+ </Paper>
+ </Grid>
+ ))
+ ) : (
+ <Grid item xs={12}>
+ <Box sx={{
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ py: 8,
+ textAlign: 'center'
+ }}>
+ <Group sx={{ fontSize: 60, color: 'grey.300', mb: 2 }} />
+ <Typography variant="h6" sx={{ mb: 1 }}>
+ {isOwnProfile ? '你还没有关注任何人' : '该用户还没有关注任何人'}
+ </Typography>
+ <Typography variant="body1" color="textSecondary">
+ {isOwnProfile ? '发现有趣的人并关注他们吧~' : ''}
+ </Typography>
+ </Box>
+ </Grid>
+ )}
+ </Grid>
+ )}
+ {activeTab === 3 && (
+ <Grid container spacing={3}>
+ {tabLoading ? (
+ <Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
+ <CircularProgress />
+ </Grid>
+ ) : followers.length > 0 ? (
+ followers.map((follower) => (
+ <Grid item xs={12} sm={6} md={4} key={follower.id}>
+ <Paper
+ elevation={0}
+ sx={{
+ bgcolor: 'white',
+ borderRadius: 3,
+ p: 2,
+ display: 'flex',
+ alignItems: 'center',
+ cursor: 'pointer',
+ '&:hover': {
+ boxShadow: 1
+ }
+ }}
+ onClick={() => navigateToUserProfile(follower.id)}
+ >
+ <Avatar
+ src={follower.avatar || 'https://randomuser.me/api/portraits/men/22.jpg'}
+ sx={{ width: 60, height: 60 }}
+ />
+ <Box sx={{ ml: 2, flexGrow: 1 }}>
+ <Typography fontWeight="medium">{follower.username}</Typography>
+ <Typography variant="body2" color="textSecondary">
+ {follower.bio || '暂无简介'}
+ </Typography>
+ </Box>
+ {currentUser && currentUser.id !== follower.id && (
+ <Button
+ variant={follower.is_following ? "outlined" : "contained"}
+ color="primary"
+ size="small"
+ sx={{ borderRadius: 20 }}
+ onClick={(e) => {
+ e.stopPropagation();
+ if (follower.is_following) {
+ handleUnfollow(follower.id, e);
+ } else {
+ handleFollowUser(follower.id);
+ }
+ }}
+ >
+ {follower.is_following ? '已关注' : '关注'}
+ </Button>
+ )}
+ </Paper>
+ </Grid>
+ ))
+ ) : (
+ <Grid item xs={12}>
+ <Box sx={{
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ py: 8,
+ textAlign: 'center'
+ }}>
+ <People sx={{ fontSize: 60, color: 'grey.300', mb: 2 }} />
+ <Typography variant="h6" sx={{ mb: 1 }}>
+ {isOwnProfile ? '你还没有粉丝' : '该用户还没有粉丝'}
+ </Typography>
+ <Typography variant="body1" color="textSecondary">
+ {isOwnProfile ? '分享更多内容来吸引粉丝吧~' : ''}
+ </Typography>
+ </Box>
+ </Grid>
+ )}
+ </Grid>
+ )}
+ </Box>
+ </Container>
+
+ {/* 底部导航栏 - 仅移动端显示 */}
+ {isMobile && (
+ <Box sx={{
+ position: 'fixed',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ bgcolor: 'white',
+ boxShadow: 3,
+ py: 1,
+ display: 'flex',
+ justifyContent: 'space-around'
+ }}>
+ <IconButton color="primary">
+ <Search fontSize="large" />
+ </IconButton>
+ <IconButton>
+ <Collections fontSize="large" />
+ </IconButton>
+ <Fab color="primary" size="medium" sx={{ mt: -2 }}>
+ <Add />
+ </Fab>
+ <IconButton>
+ <Notifications fontSize="large" />
+ </IconButton>
+ <IconButton>
+ <Person fontSize="large" />
+ </IconButton>
+ </Box>
+ )}
+
+ {/* 编辑资料模态框 */}
+ {isEditing && (
+ <Box sx={{
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ bgcolor: 'rgba(0,0,0,0.5)',
+ zIndex: 1300,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ px: 2
+ }}>
+ <Paper sx={{
+ width: '100%',
+ maxWidth: 600,
+ borderRadius: 4,
+ overflow: 'hidden'
+ }}>
+ <Box sx={{
+ bgcolor: 'primary.main',
+ color: 'white',
+ p: 2,
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}>
+ <Typography variant="h6">编辑资料</Typography>
+ <IconButton color="inherit" onClick={() => setIsEditing(false)}>
+ <Close />
+ </IconButton>
+ </Box>
+
+ <Box sx={{ p: 3 }}>
+ <Box sx={{ display: 'flex', justifyContent: 'center', mb: 3 }}>
+ <Badge
+ overlap="circular"
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
+ badgeContent={
+ <IconButton
+ size="small"
+ sx={{
+ bgcolor: 'grey.200',
+ '&:hover': { bgcolor: 'grey.300' }
+ }}
+ >
+ <CameraAlt fontSize="small" />
+ </IconButton>
+ }
+ >
+ <Avatar
+ sx={{ width: 100, height: 100 }}
+ src={formData.avatar || 'https://www.8848seo.cn/zb_users/upload/2023/02/20230210092856_68763.jpeg'}
+ />
+ </Badge>
+ </Box>
+
+ <TextField
+ fullWidth
+ label="用户名"
+ value={profileUser.username}
+ margin="normal"
+ disabled
+ />
+
+ <TextField
+ fullWidth
+ name="avatar"
+ label="头像URL"
+ value={formData.avatar}
+ onChange={handleFormChange}
+ margin="normal"
+ />
+
+ <TextField
+ fullWidth
+ name="bio"
+ label="个人简介"
+ value={formData.bio}
+ onChange={handleFormChange}
+ margin="normal"
+ multiline
+ rows={3}
+ />
+
+ <Grid container spacing={2} sx={{ mt: 1 }}>
+ <Grid item xs={6}>
+ <TextField
+ select
+ fullWidth
+ name="gender"
+ label="性别"
+ value={formData.gender}
+ onChange={handleFormChange}
+ margin="normal"
+ >
+ <MenuItem value="female">
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
+ <Female sx={{ mr: 1 }} /> 女
+ </Box>
+ </MenuItem>
+ <MenuItem value="male">
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
+ <Male sx={{ mr: 1 }} /> 男
+ </Box>
+ </MenuItem>
+ <MenuItem value="other">
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
+ <Public sx={{ mr: 1 }} /> 其他
+ </Box>
+ </MenuItem>
+ </TextField>
+ </Grid>
+ <Grid item xs={6}>
+ <TextField
+ fullWidth
+ name="birthday"
+ label="生日"
+ type="date"
+ value={formData.birthday}
+ onChange={handleFormChange}
+ margin="normal"
+ InputLabelProps={{ shrink: true }}
+ />
+ </Grid>
+ </Grid>
+
+ <TextField
+ fullWidth
+ name="location"
+ label="地区"
+ value={formData.location}
+ onChange={handleFormChange}
+ margin="normal"
+ InputProps={{
+ startAdornment: (
+ <InputAdornment position="start">
+ <LocationOn />
+ </InputAdornment>
+ ),
+ }}
+ />
+
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 3 }}>
+ <Button
+ variant="outlined"
+ sx={{ width: '48%' }}
+ onClick={() => setIsEditing(false)}
+ disabled={updating}
+ >
+ 取消
+ </Button>
+ <Button
+ variant="contained"
+ color="primary"
+ sx={{ width: '48%' }}
+ onClick={handleUpdateProfile}
+ disabled={updating}
+ >
+ {updating ? <CircularProgress size={24} /> : '保存'}
+ </Button>
+ </Box>
+ </Box>
+ </Paper>
+ </Box>
+ )}
+
+ {/* 提示信息 */}
+ <Snackbar
+ open={snackbar.open}
+ autoHideDuration={3000}
+ onClose={() => setSnackbar({ ...snackbar, open: false })}
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
+ >
+ <Alert
+ severity={snackbar.severity}
+ sx={{ width: '100%' }}
+ onClose={() => setSnackbar({ ...snackbar, open: false })}
+ >
+ {snackbar.message}
+ </Alert>
+ </Snackbar>
+ </Box>
+ </ThemeProvider>
+ );
+};
+
+export default UserProfile;
\ No newline at end of file
diff --git a/Merge/front/src/router/App.js b/Merge/front/src/router/App.js
index 4d606f9..3137fd8 100644
--- a/Merge/front/src/router/App.js
+++ b/Merge/front/src/router/App.js
@@ -14,6 +14,7 @@
import PlaceholderPage from '../components/PlaceholderPage'// src/components/PlaceholderPage.jsx
import UploadPage from '../components/UploadPage' // src/components/UploadPage.jsx
+import UserProfile from '../components/UserProfile'; // src/components/UserProfileRoute.jsx
import LoginPage from '../pages/LoginPage/LoginPage';
import RegisterPage from '../pages/RegisterPage/RegisterPage';
@@ -35,7 +36,7 @@
<Route path="/notes" element={<PlaceholderPage pageId="notes" />} />
<Route path="/creator" element={<PlaceholderPage pageId="creator" />} />
<Route path="/journal" element={<PlaceholderPage pageId="journal" />} />
-
+ <Route path="/user/:userId" element={<UserProfile />} />
<Route path="/dashboard/*" element={<UploadPage />} />
{/* 根路径重定向到 dashboard */}