From bca9345e02bcc0297af95b3254c4be4d944ed997 Mon Sep 17 00:00:00 2001 From: Leo <98382335+gaoziman@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:42:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E5=92=8C=E8=AE=A4=E8=AF=81=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构用户中心页面UI和交互 - 新增CenterNew.tsx提供更好的用户体验 - 优化登录注册页面的表单验证 - 集成头像上传和个人资料修改功能 - 改进页面样式和响应式布局 --- src/pages/User/Center.css | 644 +++++++++++++++++++++++-------- src/pages/User/Center.tsx | 123 ++++-- src/pages/User/CenterNew.tsx | 729 +++++++++++++++++++++++++++++++++++ src/pages/User/Login.tsx | 20 +- src/pages/User/Register.tsx | 45 ++- 5 files changed, 1354 insertions(+), 207 deletions(-) create mode 100644 src/pages/User/CenterNew.tsx diff --git a/src/pages/User/Center.css b/src/pages/User/Center.css index 5b38b35..1d3f923 100644 --- a/src/pages/User/Center.css +++ b/src/pages/User/Center.css @@ -1,165 +1,331 @@ -/* 用户中心页样式 */ +/* 用户中心页样式 - 恢复版本 */ .user-center-page { min-height: 100vh; - background: #fafaf8; + background: #f8f9fa; + padding-bottom: 60px; } -.page-header { - background: linear-gradient(135deg, #fafaf8 0%, #f5f0e8 50%, #fafaf8 100%); - padding: 80px 0 40px; +/* ========== 封面背景 ========== */ +.profile-cover { position: relative; + height: 320px; + background: linear-gradient(135deg, #c8363d 0%, #d4a574 50%, #e8b864 100%); + background-size: cover; + background-position: center; overflow: hidden; } -/* 装饰性背景圆圈 */ -.page-header::before { +.profile-cover::before { content: ''; position: absolute; - top: -30%; - right: 5%; - width: 400px; - height: 400px; - background: radial-gradient(circle, rgba(200, 54, 61, 0.06) 0%, transparent 70%); - border-radius: 50%; - pointer-events: none; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml;utf8,') no-repeat; + background-size: cover; + background-position: bottom; + opacity: 0.6; } -.page-header::after { - content: ''; +.cover-overlay { position: absolute; - bottom: -30%; - left: 5%; - width: 350px; - height: 350px; - background: radial-gradient(circle, rgba(212, 165, 116, 0.06) 0%, transparent 70%); - border-radius: 50%; - pointer-events: none; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.3) 100%); } -.user-profile-card { - background: #ffffff; - border-radius: 16px; - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); - padding: 40px; - border: 1px solid rgba(232, 227, 219, 0.6); +/* ========== 个人信息区域 ========== */ +.profile-section { + margin-top: -160px; position: relative; - z-index: 1; + z-index: 10; } -.user-profile-header { +.profile-main-card { + background: #ffffff; + border-radius: 20px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08); + padding: 40px; + margin-bottom: 24px; + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 32px; + border: 1px solid rgba(0, 0, 0, 0.06); +} + +/* 左侧:头像和信息 */ +.profile-left { + flex: 1; + display: flex; + gap: 24px; + align-items: flex-start; +} + +.avatar-section { + position: relative; + flex-shrink: 0; +} + +.user-avatar-large { + border: 5px solid #ffffff; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + background: linear-gradient(135deg, #c8363d 0%, #d4a574 100%); +} + +.avatar-badge { + position: absolute; + bottom: -8px; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; +} + +.avatar-badge .ant-tag { + border-radius: 12px; + font-weight: 600; + font-size: 13px; + padding: 2px 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +/* 用户基本信息 */ +.user-basic-info { + flex: 1; + padding-top: 8px; +} + +.user-name-row { display: flex; align-items: center; - gap: 24px; - margin-bottom: 32px; - padding-bottom: 32px; - border-bottom: 1px solid #f0f0f0; + gap: 12px; + margin-bottom: 16px; + flex-wrap: wrap; } -.user-avatar-wrapper { - position: relative; -} - -.user-avatar-wrapper .ant-avatar { - border: 4px solid #f5f0e8; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); -} - -.user-info-content { - flex: 1; -} - -.user-info-content h2 { - font-size: 24px; +.user-nickname { + font-size: 32px; font-weight: 700; - color: #2c2c2c; - margin: 0 0 8px 0; + color: #1a1a1a; + margin: 0; font-family: 'Noto Serif SC', 'Songti SC', serif; + letter-spacing: -0.5px; } -.user-email { +.user-tag { + border-radius: 6px; font-size: 14px; - color: #999999; - margin: 0; + padding: 4px 12px; + font-weight: 500; +} + +.user-meta-info { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 16px; +} + +.meta-item { display: flex; align-items: center; gap: 8px; + font-size: 14px; + color: #666; } -.user-actions { +.meta-item .anticon, +.meta-item > span:first-child { + color: #999; + font-size: 16px; +} + +.user-bio { + font-size: 14px; + color: #666; + line-height: 1.8; + margin: 16px 0 0 0; + padding-left: 20px; + border-left: 3px solid #c8363d; +} + +/* 右侧:操作按钮 */ +.profile-right { + flex-shrink: 0; +} + +.profile-actions { display: flex; + flex-direction: column; gap: 12px; - margin-left: auto; } -.user-actions .ant-btn { - border-radius: 8px; +.profile-actions .ant-btn { + border-radius: 10px; font-weight: 500; - height: 38px; - padding: 0 20px; + min-width: 140px; + height: 44px; + font-size: 15px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } -.user-actions .ant-btn-default { - border-color: #d9d9d9; - color: #666666; +.profile-actions .ant-btn-primary { + background: linear-gradient(135deg, #c8363d 0%, #d4a574 100%); + border: none; } -.user-actions .ant-btn-default:hover { - border-color: #c8363d; - color: #c8363d; -} - -.user-stats { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 24px; -} - -.stat-item { - text-align: center; - padding: 24px; - background: linear-gradient(135deg, #fafaf8 0%, #f5f0e8 100%); - border-radius: 12px; - transition: all 0.3s ease; - border: 1px solid transparent; -} - -.stat-item:hover { +.profile-actions .ant-btn-primary:hover { + background: linear-gradient(135deg, #b3303a 0%, #c19666 100%); transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); - border-color: rgba(200, 54, 61, 0.2); + box-shadow: 0 4px 12px rgba(200, 54, 61, 0.4); } -.stat-icon { - font-size: 32px; - color: #c8363d; - margin-bottom: 12px; - display: block; +/* ========== 数据统计卡片(精致版) ========== */ +.stats-cards-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; } -.stat-value { +.stat-card-simple { + position: relative; + background: #ffffff; + border-radius: 16px; + padding: 28px 24px; + display: flex; + align-items: center; + gap: 20px; + overflow: hidden; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); + border: 1px solid rgba(0, 0, 0, 0.05); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} + +/* 左侧装饰色块 - 已移除 */ +/* .stat-card-simple::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + transition: width 0.3s ease; +} + +.stat-card-simple:hover::before { + width: 6px; +} */ + +.stat-card-simple:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + border-color: rgba(0, 0, 0, 0.08); +} + +/* 图标圆形背景 */ +.stat-icon-circle { + flex-shrink: 0; + width: 56px; + height: 56px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + transition: all 0.3s ease; + position: relative; +} + +.stat-card-simple:hover .stat-icon-circle { + transform: scale(1.08); +} + +/* 统计数据区域 */ +.stat-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +/* 数字 */ +.stat-number { font-size: 28px; font-weight: 700; - color: #2c2c2c; - margin: 0 0 4px 0; - font-family: 'Noto Serif SC', 'Songti SC', serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + line-height: 1.2; + background: linear-gradient(135deg, #c8363d 0%, #d4a574 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } -.stat-label { +/* 文字标签 */ +.stat-text { font-size: 13px; - color: #999999; - margin: 0; + color: #888; + font-weight: 500; } -/* 页面内容 */ +/* 收藏卡片 */ +.stat-card-simple:nth-child(1) .stat-icon-circle { + background: linear-gradient(135deg, rgba(200, 54, 61, 0.12) 0%, rgba(200, 54, 61, 0.08) 100%); + color: #c8363d; +} + +/* 评论卡片 */ +.stat-card-simple:nth-child(2) .stat-icon-circle { + background: linear-gradient(135deg, rgba(212, 165, 116, 0.12) 0%, rgba(212, 165, 116, 0.08) 100%); + color: #d4a574; +} + +/* 学习时长卡片 */ +.stat-card-simple:nth-child(3) .stat-icon-circle { + background: linear-gradient(135deg, rgba(232, 184, 100, 0.12) 0%, rgba(232, 184, 100, 0.08) 100%); + color: #e8b864; +} + +/* 积分卡片 */ +.stat-card-simple:nth-child(4) .stat-icon-circle { + background: linear-gradient(135deg, rgba(200, 54, 61, 0.12) 0%, rgba(212, 165, 116, 0.08) 100%); + color: #c8363d; +} + +/* 移除不需要的颜色类 */ +.heart-bg, +.comment-bg, +.clock-bg, +.trophy-bg { + /* 这些类现在通过 nth-child 处理 */ +} + +/* ========== 页面内容 ========== */ .page-content { - background: #fafaf8; + background: transparent; + padding-top: 32px; } .page-content .ant-tabs { background: #ffffff; padding: 24px; - border-radius: 12px; + border-radius: 16px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); + border: 1px solid rgba(0, 0, 0, 0.06); +} + +.page-content .ant-tabs-nav { + margin-bottom: 24px; +} + +.page-content .ant-tabs-tab { + font-size: 15px; + font-weight: 500; + padding: 12px 24px; } /* 卡片封面 */ @@ -168,7 +334,8 @@ width: 100%; padding-top: 66.67%; overflow: hidden; - background: #f5f0e8; + background: #f5f5f5; + border-radius: 8px; } .card-cover img { @@ -178,20 +345,27 @@ width: 100%; height: 100%; object-fit: cover; + transition: transform 0.3s ease; +} + +.ant-card:hover .card-cover img { + transform: scale(1.05); } /* 成就网格 */ .achievements-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 24px; } .achievement-card { text-align: center; - padding: 32px 16px; - border-radius: 12px; + padding: 32px 20px; + border-radius: 16px; transition: all 0.3s ease; + background: #ffffff; + border: 2px solid #f0f0f0; } .achievement-card.locked { @@ -200,101 +374,251 @@ } .achievement-card:not(.locked):hover { - transform: translateY(-4px); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + transform: translateY(-6px); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15); + border-color: #c8363d; } .achievement-icon { - font-size: 48px; - color: #d4a574; + font-size: 56px; + color: #fee140; margin-bottom: 16px; + filter: drop-shadow(0 4px 8px rgba(254, 225, 64, 0.3)); } .achievement-card h3 { - font-size: 16px; - font-weight: 600; - color: #2c2c2c; + font-size: 17px; + font-weight: 700; + color: #1a1a1a; margin-bottom: 8px; } .achievement-card p { font-size: 13px; - color: #999999; + color: #999; margin: 0; + line-height: 1.6; } -/* 响应式 */ -@media (max-width: 992px) { - .user-profile-header { - flex-direction: column; - text-align: center; - } - - .user-info-content { - text-align: center; - } - - .user-email { - justify-content: center; - } - - .user-actions { - margin-left: 0; - justify-content: center; - } - - .user-stats { - grid-template-columns: repeat(3, 1fr); - } -} - -@media (max-width: 768px) { - .user-stats { +/* ========== 响应式设计 ========== */ +@media (max-width: 1200px) { + .stats-cards-grid { grid-template-columns: repeat(2, 1fr); } } -@media (max-width: 576px) { - .page-header { - padding: 60px 0 30px; +@media (max-width: 992px) { + .profile-cover { + height: 240px; } - .user-profile-card { - padding: 24px; + .profile-section { + margin-top: -120px; } - .user-profile-header { - margin-bottom: 24px; - padding-bottom: 24px; + .profile-main-card { + flex-direction: column; + padding: 32px; } - .user-avatar-wrapper .ant-avatar { - width: 80px !important; - height: 80px !important; + .profile-left { + flex-direction: column; + align-items: center; + text-align: center; } - .user-info-content h2 { - font-size: 20px; + .user-name-row { + justify-content: center; } - .user-actions { + .user-meta-info { + justify-content: center; + } + + .profile-right { width: 100%; } - .user-actions .ant-btn { + .profile-actions { + flex-direction: row; + } + + .profile-actions .ant-btn { flex: 1; } - .user-stats { - grid-template-columns: 1fr; + .user-bio { + text-align: left; + border-left: none; + padding-left: 0; + border-top: 3px solid #c8363d; + padding-top: 16px; + } +} + +@media (max-width: 768px) { + .profile-cover { + height: 200px; + } + + .profile-section { + margin-top: -100px; + } + + .profile-main-card { + padding: 24px; + } + + .user-avatar-large { + width: 100px !important; + height: 100px !important; + } + + .user-nickname { + font-size: 26px; + } + + .stats-cards-grid { + grid-template-columns: repeat(2, 1fr); gap: 16px; } - .stat-item { - padding: 20px; + .stat-card-simple { + padding: 20px 16px; + gap: 16px; + } + + .stat-icon-circle { + width: 48px; + height: 48px; + font-size: 20px; + border-radius: 12px; + } + + .stat-number { + font-size: 24px; + } + + .stat-text { + font-size: 12px; } .achievements-grid { grid-template-columns: 1fr; } } + +@media (max-width: 576px) { + .profile-cover { + height: 160px; + } + + .profile-section { + margin-top: -80px; + } + + .profile-main-card { + padding: 20px; + } + + .user-avatar-large { + width: 80px !important; + height: 80px !important; + } + + .user-nickname { + font-size: 22px; + } + + .user-meta-info { + flex-direction: column; + gap: 8px; + } + + .profile-actions { + flex-direction: column; + } + + .stats-cards-grid { + grid-template-columns: 1fr; + gap: 12px; + } + + .stat-card-simple { + padding: 18px 16px; + gap: 14px; + } + + .stat-icon-circle { + width: 44px; + height: 44px; + font-size: 18px; + border-radius: 10px; + } + + .stat-number { + font-size: 22px; + } + + .stat-text { + font-size: 12px; + } +} + +/* ========== 头像上传器 ========== */ +.avatar-uploader { + flex-shrink: 0; +} + +.avatar-uploader .ant-upload { + border: none !important; + background: transparent !important; + width: 100px; + height: 100px; + border-radius: 50%; + cursor: pointer; + position: relative; +} + +.avatar-uploader .ant-upload:hover { + border-color: transparent !important; +} + +.avatar-uploader .ant-upload:hover .ant-avatar { + opacity: 0.8; +} + +/* 上传按钮悬浮效果 */ +.avatar-uploader .ant-upload:hover div[style*="position: absolute"] { + transform: scale(1.1); + background: #1677ff !important; +} + +/* 上传中的加载动画 */ +.avatar-uploader .anticon-loading { + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* 概览卡片统一样式 */ +.profile-overview-card { + border-radius: 16px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(0, 0, 0, 0.06); +} + +.profile-overview-card .ant-card-body { + padding: 32px; +} + +/* ========== 加载状态 ========== */ +.loading-container { + display: flex; + align-items: center; + justify-content: center; + min-height: 400px; +} diff --git a/src/pages/User/Center.tsx b/src/pages/User/Center.tsx index 395abf0..2230e11 100644 --- a/src/pages/User/Center.tsx +++ b/src/pages/User/Center.tsx @@ -68,40 +68,109 @@ const UserCenter: React.FC = () => { return (
-
+ {/* 封面背景 */} +
+
+
+ + {/* 个人信息卡片 */} +
-
-
-
- } /> +
+ {/* 左侧:头像和基本信息 */} +
+
+ } + className="user-avatar-large" + /> +
+ Lv.{user.level} +
-
-

{user.username}

-

- - {user.email} -

-
-
- + +
+
+

{user.nickname}

+ @{user.username} +
+ +
+
+ + {user.email} +
+ {user.phone && ( +
+ 📱 + {user.phone} +
+ )} + {user.createdAt && ( +
+ + 加入于 {user.createdAt} +
+ )} +
+ + {user.bio && ( +

{user.bio}

+ )}
-
-
- -
{user.favorites.length}
-
收藏数
+ {/* 右侧:操作按钮 */} +
+
+
-
- -
0
-
评论数
+
+
+ + {/* 数据统计卡片 */} +
+
+
+
-
- -
0
-
学习时长(小时)
+
+
{user.favorites.length}
+
收藏
+
+
+ +
+
+ +
+
+
0
+
评论
+
+
+ +
+
+ +
+
+
0
+
学习时长
+
+
+ +
+
+ +
+
+
{user.points || 0}
+
积分
diff --git a/src/pages/User/CenterNew.tsx b/src/pages/User/CenterNew.tsx new file mode 100644 index 0000000..90eed3f --- /dev/null +++ b/src/pages/User/CenterNew.tsx @@ -0,0 +1,729 @@ +/** + * 用户中心页(重构版) + * 包含:个人资料、修改密码、用户统计、浏览历史 + */ + +import React, { useEffect, useState } from 'react' +import { + Tabs, Card, Avatar, Button, Form, Input, Select, DatePicker, + message, Modal, Statistic, Row, Col, List, Empty, Tag, Space, Divider, + Upload +} from 'antd' +import { + UserOutlined, + EditOutlined, + LockOutlined, + EyeOutlined, + LikeOutlined, + StarOutlined, + CommentOutlined, + CalendarOutlined, + HistoryOutlined, + MailOutlined, + PhoneOutlined, + ManOutlined, + WomanOutlined, + QuestionCircleOutlined, + CameraOutlined, + LoadingOutlined, +} from '@ant-design/icons' +import type { UploadChangeParam, UploadFile } from 'antd/es/upload/interface' +import { Link, useNavigate } from 'react-router-dom' +import dayjs from 'dayjs' +import { getToken, removeToken } from '@/utils/request' +import CustomPagination from '@components/CustomPagination' +import { useUserStore } from '@/store/useUserStore' +import { + getUserProfile, + updateUserProfile, + updateUserPassword, + getUserStats, + getViewHistory, + uploadAvatar, +} from '@services/userApi' +import type { + UserProfile, + UserProfileUpdateParams, + UserStats, + ViewHistoryItem, + ViewHistoryTargetType, +} from '@/types' +import './Center.css' + +const { TabPane } = Tabs +const { Option } = Select + +// 性别选项 +const genderOptions = [ + { label: '未知', value: 0, icon: }, + { label: '男', value: 1, icon: }, + { label: '女', value: 2, icon: }, +] + +// 浏览历史类型映射 +const historyTypeLabels: Record = { + heritage: '非遗项目', + inheritor: '传承人', + news: '资讯', + event: '活动', +} + +const historyTypeColors: Record = { + heritage: 'red', + inheritor: 'blue', + news: 'green', + event: 'orange', +} + +const UserCenterNew: React.FC = () => { + const navigate = useNavigate() + const [profileForm] = Form.useForm() + const [passwordForm] = Form.useForm() + + // 获取Zustand store的updateUser方法 + const { updateUser } = useUserStore() + + // 状态管理 + const [activeTab, setActiveTab] = useState('profile') + const [profile, setProfile] = useState(null) + const [stats, setStats] = useState(null) + const [viewHistory, setViewHistory] = useState([]) + const [historyTotal, setHistoryTotal] = useState(0) + const [historyPage, setHistoryPage] = useState(1) + const [historyPageSize] = useState(10) + const [historyType, setHistoryType] = useState('') + + const [loading, setLoading] = useState(false) + const [profileEditing, setProfileEditing] = useState(false) + const [passwordModalVisible, setPasswordModalVisible] = useState(false) + const [passwordSubmitting, setPasswordSubmitting] = useState(false) + const [avatarUploading, setAvatarUploading] = useState(false) + + useEffect(() => { + // 检查登录状态 + const token = getToken() + if (!token) { + message.warning('请先登录') + navigate('/login') + return + } + + fetchUserProfile() + fetchUserStats() + }, []) + + useEffect(() => { + if (activeTab === 'history') { + fetchViewHistory() + } + }, [activeTab, historyPage, historyType]) + + // 获取个人资料 + const fetchUserProfile = async () => { + setLoading(true) + try { + const data = await getUserProfile() + setProfile(data) + + // 设置表单初始值 + profileForm.setFieldsValue({ + nickname: data.nickname, + email: data.email, + phone: data.phone, + gender: data.gender, + birthday: data.birthday ? dayjs(data.birthday) : null, + province: data.province, + city: data.city, + }) + } catch (error: any) { + message.error(error.message || '获取个人资料失败') + } finally { + setLoading(false) + } + } + + // 获取用户统计 + const fetchUserStats = async () => { + try { + const data = await getUserStats() + setStats(data) + } catch (error: any) { + console.error('获取统计信息失败:', error) + } + } + + // 获取浏览历史 + const fetchViewHistory = async () => { + setLoading(true) + try { + const result = await getViewHistory({ + pageNum: historyPage, + pageSize: historyPageSize, + targetType: historyType || undefined, + }) + setViewHistory(result.records) + setHistoryTotal(result.total) + } catch (error: any) { + message.error(error.message || '获取浏览历史失败') + } finally { + setLoading(false) + } + } + + // 修改个人资料 + const handleUpdateProfile = async () => { + try { + const values = await profileForm.validateFields() + setLoading(true) + + const params: UserProfileUpdateParams = { + nickname: values.nickname, + email: values.email, + phone: values.phone, + gender: values.gender, + birthday: values.birthday ? values.birthday.format('YYYY-MM-DD') : undefined, + province: values.province, + city: values.city, + } + + await updateUserProfile(params) + message.success('个人资料修改成功') + setProfileEditing(false) + + // 刷新资料 + await fetchUserProfile() + + // 🔥 关键: 同步更新Zustand store,使Header实时更新 + // 特别是昵称的更新 + updateUser({ + nickname: values.nickname, + email: values.email, + phone: values.phone, + }) + } catch (error: any) { + if (error.errorFields) { + // 表单验证错误 + return + } + message.error(error.message || '修改失败') + } finally { + setLoading(false) + } + } + + // 上传头像前的校验 + const beforeUpload = (file: File) => { + // 校验文件类型 + const isImage = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/gif' + if (!isImage) { + message.error('只能上传 JPG/PNG/GIF 格式的图片!') + return false + } + + // 校验文件大小 (2MB) + const isLt2M = file.size / 1024 / 1024 < 2 + if (!isLt2M) { + message.error('图片大小不能超过 2MB!') + return false + } + + return true + } + + // 处理头像上传 + const handleAvatarChange = async (info: UploadChangeParam) => { + if (info.file.status === 'uploading') { + setAvatarUploading(true) + return + } + + if (info.file.status === 'done') { + // 上传成功,获取返回的头像URL + const avatarUrl = info.file.response + + if (avatarUrl && profile) { + // 更新本地状态 + setProfile({ ...profile, avatar: avatarUrl }) + + // 🔥 关键: 同步更新Zustand store,使Header实时更新 + updateUser({ avatar: avatarUrl }) + + message.success('头像上传成功!') + } + + setAvatarUploading(false) + } else if (info.file.status === 'error') { + setAvatarUploading(false) + message.error('头像上传失败,请重试') + } + } + + // 自定义上传请求 + const customUploadRequest = async (options: any) => { + const { file, onSuccess, onError } = options + + try { + const avatarUrl = await uploadAvatar(file) + onSuccess(avatarUrl) + } catch (error: any) { + console.error('上传失败:', error) + onError(error) + } + } + + // 修改密码 + const handleUpdatePassword = async () => { + try { + const values = await passwordForm.validateFields() + + if (values.newPassword !== values.confirmPassword) { + message.error('两次输入的新密码不一致') + return + } + + setPasswordSubmitting(true) + + await updateUserPassword({ + oldPassword: values.oldPassword, + newPassword: values.newPassword, + confirmPassword: values.confirmPassword, + }) + + message.success('密码修改成功,请重新登录') + passwordForm.resetFields() + setPasswordModalVisible(false) + + // 清除token,跳转登录页 + removeToken() + setTimeout(() => { + navigate('/login') + }, 1500) + } catch (error: any) { + if (error.errorFields) { + return + } + message.error(error.message || '密码修改失败') + } finally { + setPasswordSubmitting(false) + } + } + + // 渲染浏览历史项 + const renderHistoryItem = (item: ViewHistoryItem) => { + const getLink = () => { + switch (item.targetType) { + case 'heritage': + return `/heritage/${item.targetId}` + case 'inheritor': + return `/inheritor/${item.targetId}` + case 'news': + return `/news/${item.targetId}` + case 'event': + return `/events/${item.targetId}` + default: + return '#' + } + } + + return ( + + ) + } + > + + + {historyTypeLabels[item.targetType]} + + + {item.targetTitle} + + + } + description={ + +
+ {item.targetDescription} +
+
+ 浏览时间:{dayjs(item.viewTime).format('YYYY-MM-DD HH:mm')} +
+
+ } + /> +
+ ) + } + + if (!profile) { + return ( +
+ +
+ ) + } + + return ( +
+ {/* 封面背景 */} +
+
+
+ + {/* 个人信息概览 */} +
+
+ +
+ {/* 头像上传组件 */} + + {avatarUploading ? ( +
+ +
+ ) : ( +
+ } + style={{ flexShrink: 0 }} + /> +
+ +
+
+ )} +
+
+

+ {profile.nickname} +

+ + + @{profile.username} + + {profile.email && ( + + {profile.email} + + )} + {profile.phone && ( + + {profile.phone} + + )} + +
+ +
+ + {/* 统计信息 */} + {stats && ( + <> + + + + } + /> + + + } + /> + + + } + /> + + + } + /> + + + } + /> + + + + )} +
+
+
+ + {/* 详细信息区 */} +
+
+ + {/* 个人资料 */} + 个人资料} key="profile"> + } + onClick={() => { + if (profileEditing) { + profileForm.resetFields() + } + setProfileEditing(!profileEditing) + }} + > + {profileEditing ? '取消编辑' : '编辑资料'} + + } + > +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {profileEditing && ( + + + + )} +
+ + + +
+ 注册时间:{dayjs(profile.createTime).format('YYYY-MM-DD HH:mm')} +
+
+
+ + {/* 浏览历史 */} + 浏览历史} key="history"> + + + 筛选类型: + + + + }} + /> + + {/* 自定义分页 */} + {viewHistory.length > 0 && ( + { + setHistoryPage(page) + }} + /> + )} + + +
+
+
+ + {/* 修改密码弹窗 */} + { + setPasswordModalVisible(false) + passwordForm.resetFields() + }} + confirmLoading={passwordSubmitting} + okText="确认修改" + cancelText="取消" + width={480} + > +
+ + + + + + + + + + + +
+
+
+ ) +} + +export default UserCenterNew diff --git a/src/pages/User/Login.tsx b/src/pages/User/Login.tsx index 58fec16..532d5c1 100644 --- a/src/pages/User/Login.tsx +++ b/src/pages/User/Login.tsx @@ -14,14 +14,18 @@ const Login: React.FC = () => { const navigate = useNavigate() const login = useUserStore((state) => state.login) - const onFinish = async (values: { username: string; password: string }) => { + const onFinish = async (values: { account: string; password: string }) => { setLoading(true) try { - await login(values.username, values.password) - message.success('登录成功') - navigate('/') + const success = await login(values.account, values.password) + if (success) { + message.success('登录成功') + navigate('/') + } } catch (error) { - message.error('登录失败,请检查用户名或密码') + // 提取后端返回的具体错误信息 + const errorMessage = error instanceof Error ? error.message : '登录失败,请稍后重试' + message.error(errorMessage) } finally { setLoading(false) } @@ -44,10 +48,10 @@ const Login: React.FC = () => { size="large" > - } placeholder="用户名" /> + } placeholder="用户名/邮箱/手机号" /> { const onFinish = async (values: { username: string - email: string + nickname: string + email?: string + phone?: string password: string + confirmPassword: string agree: boolean }) => { if (!values.agree) { @@ -27,12 +30,13 @@ const Register: React.FC = () => { setLoading(true) try { - await register(values.username, values.email, values.password) + await register(values.username, values.password, values.confirmPassword, values.nickname, values.email, values.phone) message.success('注册成功,欢迎加入非遗文化传承平台!') navigate('/') } catch (error) { - const err = error as Error - message.error(err.message || '注册失败,请稍后重试') + // 提取后端返回的具体错误信息 + const errorMessage = error instanceof Error ? error.message : '注册失败,请稍后重试' + message.error(errorMessage) } finally { setLoading(false) } @@ -58,38 +62,55 @@ const Register: React.FC = () => { name="username" rules={[ { required: true, message: '请输入用户名' }, - { min: 3, message: '用户名至少3个字符' }, - { max: 20, message: '用户名最多20个字符' }, + { min: 3, max: 16, message: '用户名长度为 3-16 位' }, + { pattern: /^[A-Za-z0-9_]+$/, message: '用户名格式为数字、字母及下划线' }, ]} > - } placeholder="用户名" /> + } placeholder="用户名(3-16位,字母/数字/下划线)" /> + + + + } placeholder="昵称" /> - } placeholder="邮箱" /> + } placeholder="邮箱(可选)" /> + + + + } placeholder="手机号(可选)" /> - } placeholder="密码" /> + } placeholder="密码(6-20位)" /> ({ validator(_, value) { if (!value || getFieldValue('password') === value) {