|
@@ -1,6 +1,5 @@
|
|
|
import debug from 'debug';
|
|
import debug from 'debug';
|
|
|
import React, { useEffect, useState } from 'react';
|
|
import React, { useEffect, useState } from 'react';
|
|
|
-import { Layout, Card, Avatar, Button, Typography, List, Spin, Tabs, Divider, Badge } from 'antd';
|
|
|
|
|
import { UserOutlined, EditOutlined, HeartOutlined, UserAddOutlined, UserDeleteOutlined } from '@ant-design/icons';
|
|
import { UserOutlined, EditOutlined, HeartOutlined, UserAddOutlined, UserDeleteOutlined } from '@ant-design/icons';
|
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
|
import { userClient } from '@/client/api';
|
|
import { userClient } from '@/client/api';
|
|
@@ -8,10 +7,6 @@ import { useAuth, User } from '@/client/home/hooks/AuthProvider';
|
|
|
|
|
|
|
|
const rpcLogger = debug('frontend:api:rpc');
|
|
const rpcLogger = debug('frontend:api:rpc');
|
|
|
|
|
|
|
|
-const { Content } = Layout;
|
|
|
|
|
-const { Title, Text, Paragraph } = Typography;
|
|
|
|
|
-const { TabPane } = Tabs;
|
|
|
|
|
-
|
|
|
|
|
const UserProfilePage: React.FC = () => {
|
|
const UserProfilePage: React.FC = () => {
|
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [user, setUser] = useState<User | null>(null);
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [loading, setLoading] = useState(true);
|
|
@@ -22,6 +17,7 @@ const UserProfilePage: React.FC = () => {
|
|
|
const id = Number(userId);
|
|
const id = Number(userId);
|
|
|
const navigate = useNavigate();
|
|
const navigate = useNavigate();
|
|
|
const { user: currentUser } = useAuth();
|
|
const { user: currentUser } = useAuth();
|
|
|
|
|
+ const [activeTab, setActiveTab] = useState('posts');
|
|
|
|
|
|
|
|
// 获取用户资料
|
|
// 获取用户资料
|
|
|
const fetchUserProfile = async () => {
|
|
const fetchUserProfile = async () => {
|
|
@@ -98,182 +94,182 @@ const UserProfilePage: React.FC = () => {
|
|
|
|
|
|
|
|
if (loading) {
|
|
if (loading) {
|
|
|
return (
|
|
return (
|
|
|
- <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
|
|
|
|
|
- <Spin size="large" />
|
|
|
|
|
|
|
+ <div className="flex justify-center items-center min-h-screen">
|
|
|
|
|
+ <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-600"></div>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (!user) {
|
|
if (!user) {
|
|
|
return (
|
|
return (
|
|
|
- <div style={{ textAlign: 'center', padding: '50px' }}>
|
|
|
|
|
- <Title level={3}>用户不存在</Title>
|
|
|
|
|
- <Button onClick={() => navigate('/')}>返回首页</Button>
|
|
|
|
|
|
|
+ <div className="text-center py-16">
|
|
|
|
|
+ <h2 className="text-2xl font-bold text-gray-900 mb-4">用户不存在</h2>
|
|
|
|
|
+ <button
|
|
|
|
|
+ className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
|
|
|
|
+ onClick={() => navigate('/')}
|
|
|
|
|
+ >
|
|
|
|
|
+ 返回首页
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- <Layout>
|
|
|
|
|
- <Content style={{ padding: '24px', maxWidth: 1200, margin: '0 auto', width: '100%' }}>
|
|
|
|
|
- <Card style={{ marginBottom: 24 }}>
|
|
|
|
|
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', padding: '24px 0' }}>
|
|
|
|
|
- <Avatar
|
|
|
|
|
- src={user.avatar || <UserOutlined style={{ fontSize: 64 }} />}
|
|
|
|
|
- size={128}
|
|
|
|
|
- style={{ marginBottom: 16 }}
|
|
|
|
|
- />
|
|
|
|
|
- <Title level={2} style={{ margin: 0 }}>{user.nickname || user.username}</Title>
|
|
|
|
|
|
|
+ <div className="min-h-screen bg-gray-50">
|
|
|
|
|
+ <main className="container mx-auto px-4 py-8 max-w-5xl">
|
|
|
|
|
+ {/* 用户资料卡片 */}
|
|
|
|
|
+ <div className="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
|
|
|
+ <div className="flex flex-col items-center py-6">
|
|
|
|
|
+ <div className="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center mb-4 overflow-hidden">
|
|
|
|
|
+ {user.avatar ? (
|
|
|
|
|
+ <img src={user.avatar} alt={user.username} className="w-full h-full object-cover" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <UserOutlined className="text-6xl text-gray-400" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <h1 className="text-2xl font-bold text-gray-900 mb-2">{user.nickname || user.username}</h1>
|
|
|
|
|
|
|
|
- <div style={{ display: 'flex', margin: '16px 0' }}>
|
|
|
|
|
- <div style={{ textAlign: 'center', margin: '0 24px' }}>
|
|
|
|
|
- <Text strong style={{ fontSize: 24 }}>0</Text>
|
|
|
|
|
- <br />
|
|
|
|
|
- <Text>帖子</Text>
|
|
|
|
|
|
|
+ <div className="flex justify-center space-x-8 my-6">
|
|
|
|
|
+ <div className="text-center">
|
|
|
|
|
+ <p className="text-2xl font-bold text-gray-900">0</p>
|
|
|
|
|
+ <p className="text-gray-500">帖子</p>
|
|
|
</div>
|
|
</div>
|
|
|
- <div style={{ textAlign: 'center', margin: '0 24px' }}>
|
|
|
|
|
- <Text strong style={{ fontSize: 24 }}>{followerCount}</Text>
|
|
|
|
|
- <br />
|
|
|
|
|
- <Text>粉丝</Text>
|
|
|
|
|
|
|
+ <div className="text-center">
|
|
|
|
|
+ <p className="text-2xl font-bold text-gray-900">{followerCount}</p>
|
|
|
|
|
+ <p className="text-gray-500">粉丝</p>
|
|
|
</div>
|
|
</div>
|
|
|
- <div style={{ textAlign: 'center', margin: '0 24px' }}>
|
|
|
|
|
- <Text strong style={{ fontSize: 24 }}>{followingCount}</Text>
|
|
|
|
|
- <br />
|
|
|
|
|
- <Text>关注</Text>
|
|
|
|
|
|
|
+ <div className="text-center">
|
|
|
|
|
+ <p className="text-2xl font-bold text-gray-900">{followingCount}</p>
|
|
|
|
|
+ <p className="text-gray-500">关注</p>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{currentUser && currentUser.id !== user.id ? (
|
|
{currentUser && currentUser.id !== user.id ? (
|
|
|
- <Button
|
|
|
|
|
- type={isFollowing ? "default" : "primary"}
|
|
|
|
|
- icon={isFollowing ? <UserDeleteOutlined /> : <UserAddOutlined />}
|
|
|
|
|
|
|
+ <button
|
|
|
|
|
+ className={`px-4 py-2 rounded-md transition-colors flex items-center gap-2 ${
|
|
|
|
|
+ isFollowing
|
|
|
|
|
+ ? 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
|
|
|
+ : 'bg-blue-600 text-white hover:bg-blue-700'
|
|
|
|
|
+ }`}
|
|
|
onClick={handleFollowToggle}
|
|
onClick={handleFollowToggle}
|
|
|
>
|
|
>
|
|
|
- {isFollowing ? '取消关注' : '关注'}
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+ {isFollowing ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <UserDeleteOutlined className="text-sm" />
|
|
|
|
|
+ 取消关注
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <UserAddOutlined className="text-sm" />
|
|
|
|
|
+ 关注
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </button>
|
|
|
) : currentUser && currentUser.id === user.id ? (
|
|
) : currentUser && currentUser.id === user.id ? (
|
|
|
- <Button icon={<EditOutlined />} onClick={() => navigate('/profile/edit')}>
|
|
|
|
|
|
|
+ <button
|
|
|
|
|
+ className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors flex items-center gap-2"
|
|
|
|
|
+ onClick={() => navigate('/profile/edit')}
|
|
|
|
|
+ >
|
|
|
|
|
+ <EditOutlined className="text-sm" />
|
|
|
编辑资料
|
|
编辑资料
|
|
|
- </Button>
|
|
|
|
|
|
|
+ </button>
|
|
|
) : null}
|
|
) : null}
|
|
|
|
|
|
|
|
{user.bio && (
|
|
{user.bio && (
|
|
|
- <Paragraph style={{ marginTop: 16, maxWidth: 600, textAlign: 'center' }}>
|
|
|
|
|
|
|
+ <p className="mt-6 text-gray-600 max-w-2xl text-center">
|
|
|
{user.bio}
|
|
{user.bio}
|
|
|
- </Paragraph>
|
|
|
|
|
|
|
+ </p>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
- <div style={{ display: 'flex', alignItems: 'center', marginTop: 8 }}>
|
|
|
|
|
|
|
+ <div className="flex items-center mt-4 text-gray-500">
|
|
|
{user.location && (
|
|
{user.location && (
|
|
|
- <Text style={{ marginRight: 16 }}>{user.location}</Text>
|
|
|
|
|
|
|
+ <span className="mr-4">{user.location}</span>
|
|
|
)}
|
|
)}
|
|
|
{user.website && (
|
|
{user.website && (
|
|
|
- <Text
|
|
|
|
|
- style={{ color: '#1890ff', cursor: 'pointer' }}
|
|
|
|
|
- onClick={() => window.open(user.website, '_blank')}
|
|
|
|
|
|
|
+ <a
|
|
|
|
|
+ href={user.website}
|
|
|
|
|
+ target="_blank"
|
|
|
|
|
+ rel="noopener noreferrer"
|
|
|
|
|
+ className="text-blue-600 hover:underline"
|
|
|
>
|
|
>
|
|
|
{user.website}
|
|
{user.website}
|
|
|
- </Text>
|
|
|
|
|
|
|
+ </a>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- </Card>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <Tabs defaultActiveKey="posts" style={{ marginBottom: 24 }}>
|
|
|
|
|
- <TabPane tab="帖子" key="posts">
|
|
|
|
|
- <List
|
|
|
|
|
- dataSource={[]} // 这里应该是用户的帖子数据
|
|
|
|
|
- renderItem={item => (
|
|
|
|
|
- <List.Item
|
|
|
|
|
- actions={[
|
|
|
|
|
- <Button icon={<HeartOutlined />}>
|
|
|
|
|
- {item.likesCount > 0 && item.likesCount}
|
|
|
|
|
- </Button>
|
|
|
|
|
- ]}
|
|
|
|
|
- >
|
|
|
|
|
- <List.Item.Meta
|
|
|
|
|
- description={
|
|
|
|
|
- <>
|
|
|
|
|
- <Paragraph>{item.content}</Paragraph>
|
|
|
|
|
- {item.images?.map((img, idx) => (
|
|
|
|
|
- <img key={idx} src={img} alt={`Post image ${idx}`} style={{ maxWidth: '100%', margin: '8px 0' }} />
|
|
|
|
|
- ))}
|
|
|
|
|
- <Text type="secondary">{new Date(item.createdAt).toLocaleString()}</Text>
|
|
|
|
|
- </>
|
|
|
|
|
- }
|
|
|
|
|
- />
|
|
|
|
|
- </List.Item>
|
|
|
|
|
- )}
|
|
|
|
|
- locale={{ emptyText: '该用户暂无帖子' }}
|
|
|
|
|
- />
|
|
|
|
|
- </TabPane>
|
|
|
|
|
- <TabPane tab="关注" key="following">
|
|
|
|
|
- <List
|
|
|
|
|
- dataSource={[]} // 这里应该是用户关注的人数据
|
|
|
|
|
- renderItem={item => (
|
|
|
|
|
- <List.Item
|
|
|
|
|
- actions={[
|
|
|
|
|
- <Button
|
|
|
|
|
- size="small"
|
|
|
|
|
- type={item.isFollowing ? "default" : "primary"}
|
|
|
|
|
- onClick={() => {/* 关注/取消关注逻辑 */}}
|
|
|
|
|
- >
|
|
|
|
|
- {item.isFollowing ? '已关注' : '关注'}
|
|
|
|
|
- </Button>
|
|
|
|
|
- ]}
|
|
|
|
|
- >
|
|
|
|
|
- <List.Item.Meta
|
|
|
|
|
- avatar={<Avatar src={item.avatar || <UserOutlined />} />}
|
|
|
|
|
- title={
|
|
|
|
|
- <Text
|
|
|
|
|
- style={{ cursor: 'pointer' }}
|
|
|
|
|
- onClick={() => navigate(`/users/${item.id}`)}
|
|
|
|
|
- >
|
|
|
|
|
- {item.nickname || item.username}
|
|
|
|
|
- </Text>
|
|
|
|
|
- }
|
|
|
|
|
- description={item.bio || '暂无简介'}
|
|
|
|
|
- />
|
|
|
|
|
- </List.Item>
|
|
|
|
|
- )}
|
|
|
|
|
- locale={{ emptyText: '该用户暂无关注' }}
|
|
|
|
|
- />
|
|
|
|
|
- </TabPane>
|
|
|
|
|
- <TabPane tab="粉丝" key="followers">
|
|
|
|
|
- <List
|
|
|
|
|
- dataSource={[]} // 这里应该是用户的粉丝数据
|
|
|
|
|
- renderItem={item => (
|
|
|
|
|
- <List.Item
|
|
|
|
|
- actions={[
|
|
|
|
|
- <Button
|
|
|
|
|
- size="small"
|
|
|
|
|
- type={item.isFollowing ? "default" : "primary"}
|
|
|
|
|
- onClick={() => {/* 关注/取消关注逻辑 */}}
|
|
|
|
|
- >
|
|
|
|
|
- {item.isFollowing ? '已关注' : '关注'}
|
|
|
|
|
- </Button>
|
|
|
|
|
- ]}
|
|
|
|
|
- >
|
|
|
|
|
- <List.Item.Meta
|
|
|
|
|
- avatar={<Avatar src={item.avatar || <UserOutlined />} />}
|
|
|
|
|
- title={
|
|
|
|
|
- <Text
|
|
|
|
|
- style={{ cursor: 'pointer' }}
|
|
|
|
|
- onClick={() => navigate(`/users/${item.id}`)}
|
|
|
|
|
- >
|
|
|
|
|
- {item.nickname || item.username}
|
|
|
|
|
- </Text>
|
|
|
|
|
- }
|
|
|
|
|
- description={item.bio || '暂无简介'}
|
|
|
|
|
- />
|
|
|
|
|
- </List.Item>
|
|
|
|
|
- )}
|
|
|
|
|
- locale={{ emptyText: '该用户暂无粉丝' }}
|
|
|
|
|
- />
|
|
|
|
|
- </TabPane>
|
|
|
|
|
- </Tabs>
|
|
|
|
|
- </Content>
|
|
|
|
|
- </Layout>
|
|
|
|
|
|
|
+ {/* 标签页导航 */}
|
|
|
|
|
+ <div className="bg-white rounded-lg shadow-sm mb-6">
|
|
|
|
|
+ <div className="flex border-b">
|
|
|
|
|
+ <button
|
|
|
|
|
+ className={`py-4 px-6 font-medium transition-colors ${
|
|
|
|
|
+ activeTab === 'posts'
|
|
|
|
|
+ ? 'text-blue-600 border-b-2 border-blue-600'
|
|
|
|
|
+ : 'text-gray-500 hover:text-gray-700 border-b-2 border-transparent'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ onClick={() => setActiveTab('posts')}
|
|
|
|
|
+ >
|
|
|
|
|
+ 帖子
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ className={`py-4 px-6 font-medium transition-colors ${
|
|
|
|
|
+ activeTab === 'following'
|
|
|
|
|
+ ? 'text-blue-600 border-b-2 border-blue-600'
|
|
|
|
|
+ : 'text-gray-500 hover:text-gray-700 border-b-2 border-transparent'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ onClick={() => setActiveTab('following')}
|
|
|
|
|
+ >
|
|
|
|
|
+ 关注
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ className={`py-4 px-6 font-medium transition-colors ${
|
|
|
|
|
+ activeTab === 'followers'
|
|
|
|
|
+ ? 'text-blue-600 border-b-2 border-blue-600'
|
|
|
|
|
+ : 'text-gray-500 hover:text-gray-700 border-b-2 border-transparent'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ onClick={() => setActiveTab('followers')}
|
|
|
|
|
+ >
|
|
|
|
|
+ 粉丝
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 帖子内容区 */}
|
|
|
|
|
+ {activeTab === 'posts' && (
|
|
|
|
|
+ <div className="p-6">
|
|
|
|
|
+ <div className="bg-gray-50 rounded-lg p-16 text-center">
|
|
|
|
|
+ <p className="text-gray-500">该用户暂无帖子</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 关注列表 */}
|
|
|
|
|
+ {activeTab === 'following' && (
|
|
|
|
|
+ <div className="p-6">
|
|
|
|
|
+ <div className="bg-gray-50 rounded-lg p-16 text-center">
|
|
|
|
|
+ <p className="text-gray-500">该用户暂无关注</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 粉丝列表 */}
|
|
|
|
|
+ {activeTab === 'followers' && (
|
|
|
|
|
+ <div className="p-6">
|
|
|
|
|
+ <div className="bg-gray-50 rounded-lg p-16 text-center">
|
|
|
|
|
+ <p className="text-gray-500">该用户暂无粉丝</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </main>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 页脚 */}
|
|
|
|
|
+ <footer className="bg-gray-100 py-6 mt-auto">
|
|
|
|
|
+ <div className="container mx-auto px-4 text-center text-gray-500 text-sm">
|
|
|
|
|
+ 社交媒体平台 ©{new Date().getFullYear()} Created with React & Tailwind CSS
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </footer>
|
|
|
|
|
+ </div>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
|
|
|
|