实现了个人主页和主页的连接,以及个人主页界面的初步实现,为后续作品、通知显示预留出位置
Change-Id: Iad88dde17107c9d229de1d88a0ea696962560e67
diff --git a/src/AppLayout.tsx b/src/AppLayout.tsx
index ba15c74..93cef48 100644
--- a/src/AppLayout.tsx
+++ b/src/AppLayout.tsx
@@ -73,9 +73,9 @@
const dropdownMenuItems = [
{
key: 'profile',
- label: '个人中心',
+ label: '个人主页',
icon: <UserOutlined />,
- onClick: () => navigate('/profile'),
+ onClick: () => navigate('/user'),
},
{
key: 'settings',
diff --git a/src/feature/user/UserHome.tsx b/src/feature/user/UserHome.tsx
new file mode 100644
index 0000000..5f925dc
--- /dev/null
+++ b/src/feature/user/UserHome.tsx
@@ -0,0 +1,370 @@
+import { Avatar, Button, Card, Col, Row, Space, Typography, Divider, Badge, Empty, Spin } from 'antd';
+import { UserOutlined, MailOutlined, PlusOutlined, BellOutlined, EyeOutlined, CalendarOutlined } from '@ant-design/icons';
+import axios from 'axios';
+import { useEffect, useState } from 'react';
+import { useAppSelector, useAppDispatch } from '../../store/hooks';
+import { getUserInfo } from './userSlice';
+
+const { Title, Text, Paragraph } = Typography;
+
+// 定义 WorkResponse 接口,与后端 WorkResponse 类对应
+interface WorkResponse {
+ id: number;
+ title: string;
+ author: string;
+ views: number;
+ categoryId: number;
+ description: string;
+ createTime: string;
+}
+
+// 模拟通知数据
+const mockNotifications = [
+ {
+ id: 1,
+ title: '系统通知',
+ content: '您的作品《创意设计》获得了新的点赞!',
+ time: '2小时前',
+ type: 'like',
+ unread: true
+ },
+ {
+ id: 2,
+ title: '评论通知',
+ content: '用户"设计师小王"评论了您的作品',
+ time: '1天前',
+ type: 'comment',
+ unread: true
+ },
+ {
+ id: 3,
+ title: '系统消息',
+ content: '平台将于本周末进行系统维护',
+ time: '3天前',
+ type: 'system',
+ unread: false
+ }
+];
+
+function UserHome() {
+ const userState = useAppSelector(state => state.user);
+ const dispatch = useAppDispatch();
+ const [userWorks, setUserWorks] = useState<WorkResponse[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [pageLoading, setPageLoading] = useState(true);
+
+ // 检查token并获取用户信息
+ useEffect(() => {
+ const initializeUser = async () => {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ // 如果没有token,重定向到登录页
+ window.location.href = '/login';
+ return;
+ }
+
+ // 如果用户信息为空或状态为idle,重新获取
+ if (!userState.username || userState.status === 'idle') {
+ try {
+ await dispatch(getUserInfo()).unwrap();
+ } catch (error) {
+ console.error('获取用户信息失败:', error);
+ // 如果获取用户信息失败,可能token过期,重定向到登录页
+ localStorage.removeItem('token');
+ window.location.href = '/login';
+ return;
+ }
+ }
+ setPageLoading(false);
+ };
+
+ initializeUser();
+ }, [dispatch, userState.username, userState.status]);
+
+ // 获取用户作品
+ useEffect(() => {
+ const fetchUserWorks = async () => {
+ if (!userState.username) return;
+
+ try {
+ setLoading(true);
+ const token = localStorage.getItem('token');
+ if (token) {
+ const response = await axios.get('/api/works/works/byAuthor', {
+ headers: {
+ token: token
+ },
+ params: {
+ author: userState.username
+ }
+ });
+ if (response.data.code === 200) {
+ setUserWorks(response.data.data || []);
+ } else {
+ console.error('获取作品失败:', response.data.message);
+ setUserWorks([]);
+ }
+ }
+ } catch (error) {
+ console.error('获取用户作品信息失败:', error);
+ setUserWorks([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 只有当用户信息加载完成且用户名存在时才获取作品
+ if (userState.username && userState.status === 'succeeded') {
+ fetchUserWorks();
+ }
+ }, [userState.username, userState.status]);
+
+ // 格式化时间显示
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString('zh-CN');
+ };
+
+ // 获取分类名称
+ const getCategoryName = (categoryId: number) => {
+ const categories: { [key: number]: string } = {
+ 1: '文学创作',
+ 2: '视觉设计',
+ 3: '音乐创作',
+ 4: '影视制作',
+ 5: '其他'
+ };
+ return categories[categoryId] || '未分类';
+ };
+
+ // 如果页面正在初始化,显示加载状态
+ if (pageLoading || userState.status === 'loading') {
+ return (
+ <div style={{
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ minHeight: '400px'
+ }}>
+ <Spin size="large" tip="加载用户信息中..." />
+ </div>
+ );
+ }
+
+ // 如果获取用户信息失败
+ if (userState.status === 'failed' || !userState.username) {
+ return (
+ <div style={{
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ minHeight: '400px',
+ flexDirection: 'column'
+ }}>
+ <Empty
+ description="无法获取用户信息"
+ style={{ marginBottom: 16 }}
+ />
+ <Button
+ type="primary"
+ onClick={() => dispatch(getUserInfo())}
+ >
+ 重新加载
+ </Button>
+ </div>
+ );
+ }
+
+ return (
+ <div className="user-home-container" style={{ padding: '0 24px' }}>
+ {/* 用户信息横栏 */}
+ <Card
+ style={{
+ marginBottom: 24,
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
+ border: 'none'
+ }}
+ >
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+ <Space size="large" align="center">
+ <Avatar
+ size={80}
+ icon={<UserOutlined />}
+ style={{ backgroundColor: '#fff', color: '#667eea' }}
+ />
+ <div>
+ <Title level={2} style={{ color: 'white', margin: 0 }}>
+ {userState.username}
+ </Title>
+ <Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '16px' }}>
+ <MailOutlined /> {userState.email || '暂无邮箱'}
+ </Text>
+ <div style={{ marginTop: 8 }}>
+ <Text style={{ color: 'rgba(255,255,255,0.6)' }}>
+ 用户ID: {userState.userid}
+ </Text>
+ </div>
+ </div>
+ </Space>
+ <div style={{ textAlign: 'center', color: 'white' }}>
+ <div style={{ fontSize: '24px', fontWeight: 'bold' }}>{userWorks.length}</div>
+ <div style={{ opacity: 0.8 }}>发布作品</div>
+ </div>
+ </div>
+ </Card>
+
+ {/* 两栏布局 */}
+ <Row gutter={24}>
+ {/* 左侧:作品展示栏 */}
+ <Col span={16}>
+ <Card
+ title={
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+ <span>我的作品</span>
+ <Button
+ type="primary"
+ icon={<PlusOutlined />}
+ onClick={() => {
+ // 这里后续添加跳转到发布作品页面的逻辑
+ console.log('跳转到发布作品页面');
+ }}
+ >
+ 发布作品
+ </Button>
+ </div>
+ }
+ style={{ minHeight: '600px' }}
+ >
+ {loading ? (
+ <div style={{ textAlign: 'center', padding: '50px 0' }}>
+ <Spin size="large" tip="加载作品中..." />
+ </div>
+ ) : userWorks.length === 0 ? (
+ <Empty
+ description="暂无作品,快去发布第一个作品吧!"
+ style={{ padding: '50px 0' }}
+ />
+ ) : (
+ <Row gutter={[16, 16]}>
+ {userWorks.map((work) => (
+ <Col span={12} key={work.id}>
+ <Card
+ hoverable
+ style={{ height: '100%' }}
+ actions={[
+ <div key="views" style={{ color: '#666' }}>
+ <EyeOutlined /> {work.views}
+ </div>,
+ <div key="category" style={{ color: '#666' }}>
+ {getCategoryName(work.categoryId)}
+ </div>
+ ]}
+ >
+ <Card.Meta
+ title={
+ <div style={{
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap'
+ }}>
+ {work.title}
+ </div>
+ }
+ description={
+ <div>
+ <Paragraph
+ ellipsis={{ rows: 2 }}
+ style={{ marginBottom: 8, minHeight: '40px' }}
+ >
+ {work.description || '暂无描述'}
+ </Paragraph>
+ <div style={{
+ display: 'flex',
+ justifyContent: 'space-between',
+ fontSize: '12px',
+ color: '#999'
+ }}>
+ <span>
+ <CalendarOutlined /> {formatDate(work.createTime)}
+ </span>
+ <span>ID: {work.id}</span>
+ </div>
+ </div>
+ }
+ />
+ </Card>
+ </Col>
+ ))}
+ </Row>
+ )}
+ </Card>
+ </Col>
+
+ {/* 右侧:通知栏 */}
+ <Col span={8}>
+ <Card
+ title={
+ <div style={{ display: 'flex', alignItems: 'center' }}>
+ <BellOutlined style={{ marginRight: 8 }} />
+ 通知中心
+ <Badge count={2} style={{ marginLeft: 8 }} />
+ </div>
+ }
+ style={{ minHeight: '600px' }}
+ >
+ <div>
+ {mockNotifications.map((notification, index) => (
+ <div key={notification.id}>
+ <div
+ style={{
+ padding: '12px',
+ backgroundColor: notification.unread ? '#f6ffed' : 'transparent',
+ borderRadius: '4px',
+ }}
+ >
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
+ <div style={{ flex: 1 }}>
+ <div style={{
+ fontWeight: notification.unread ? 'bold' : 'normal',
+ marginBottom: '4px',
+ display: 'flex',
+ alignItems: 'center'
+ }}>
+ {notification.title}
+ {notification.unread && (
+ <Badge
+ color="red"
+ style={{ marginLeft: '8px' }}
+ />
+ )}
+ </div>
+ <div style={{
+ color: '#666',
+ fontSize: '14px',
+ lineHeight: '1.4',
+ marginBottom: '8px'
+ }}>
+ {notification.content}
+ </div>
+ <div style={{ color: '#999', fontSize: '12px' }}>
+ {notification.time}
+ </div>
+ </div>
+ </div>
+ </div>
+ {index < mockNotifications.length - 1 && <Divider style={{ margin: '0' }} />}
+ </div>
+ ))}
+
+ {/* 查看更多按钮 */}
+ <div style={{ textAlign: 'center', marginTop: '16px' }}>
+ <Button type="link">查看全部通知</Button>
+ </div>
+ </div>
+ </Card>
+ </Col>
+ </Row>
+ </div>
+ );
+}
+
+export default UserHome;
\ No newline at end of file
diff --git a/src/feature/user/UserLayout.tsx b/src/feature/user/UserLayout.tsx
new file mode 100644
index 0000000..cb88dde
--- /dev/null
+++ b/src/feature/user/UserLayout.tsx
@@ -0,0 +1,18 @@
+import { Layout } from 'antd';
+import { Outlet } from 'react-router';
+
+const { Content } = Layout;
+
+function UserLayout() {
+ return (
+
+ <Content style={{ margin: '24px 16px 0' }}>
+ <div style={{ padding: 24, minHeight: 360 }}>
+ <Outlet /> {/* 这里会渲染子路由对应的组件 */}
+ </div>
+ </Content>
+
+ );
+}
+
+export default UserLayout;
\ No newline at end of file
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index 401b6bb..d380bde 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -11,6 +11,8 @@
import Work from "../feature/work/Work";
import CreateWork from "../feature/work/CreateWork";
+import UserHome from "../feature/user/UserHome";
+
// 创建受保护的组件
const ProtectedHome = withProtect(Home);
const ProtectedWork = withProtect(Work);
@@ -36,6 +38,10 @@
{path: 'categories/other', Component: OtherCategory,},
{path: '/works/:id', Component: WorkPage, },
{
+ path: "user",
+ Component: UserHome,
+ },
+ {
Component: AuthLayout,
children: [
{ path: "/login", Component: Login },