feat: 优化用户中心和认证页面
- 重构用户中心页面UI和交互 - 新增CenterNew.tsx提供更好的用户体验 - 优化登录注册页面的表单验证 - 集成头像上传和个人资料修改功能 - 改进页面样式和响应式布局
This commit is contained in:
parent
0a63a30a23
commit
bca9345e02
@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="rgba(255,255,255,0.1)" d="M0,96L48,112C96,128,192,160,288,160C384,160,480,128,576,112C672,96,768,96,864,112C960,128,1056,160,1152,160C1248,160,1344,128,1392,112L1440,96L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path></svg>') 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;
|
||||
}
|
||||
|
||||
@ -68,40 +68,109 @@ const UserCenter: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="user-center-page">
|
||||
<div className="page-header">
|
||||
{/* 封面背景 */}
|
||||
<div className="profile-cover">
|
||||
<div className="cover-overlay"></div>
|
||||
</div>
|
||||
|
||||
{/* 个人信息卡片 */}
|
||||
<div className="profile-section">
|
||||
<div className="container">
|
||||
<div className="user-profile-card">
|
||||
<div className="user-profile-header">
|
||||
<div className="user-avatar-wrapper">
|
||||
<Avatar size={100} src={user.avatar} icon={<UserOutlined />} />
|
||||
<div className="profile-main-card">
|
||||
{/* 左侧:头像和基本信息 */}
|
||||
<div className="profile-left">
|
||||
<div className="avatar-section">
|
||||
<Avatar
|
||||
size={120}
|
||||
src={user.avatar}
|
||||
icon={<UserOutlined />}
|
||||
className="user-avatar-large"
|
||||
/>
|
||||
<div className="avatar-badge">
|
||||
<Tag color="gold">Lv.{user.level}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div className="user-info-content">
|
||||
<h2>{user.username}</h2>
|
||||
<p className="user-email">
|
||||
<MailOutlined />
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
<div className="user-actions">
|
||||
<Button icon={<EditOutlined />}>编辑资料</Button>
|
||||
|
||||
<div className="user-basic-info">
|
||||
<div className="user-name-row">
|
||||
<h1 className="user-nickname">{user.nickname}</h1>
|
||||
<Tag color="blue" className="user-tag">@{user.username}</Tag>
|
||||
</div>
|
||||
|
||||
<div className="user-meta-info">
|
||||
<div className="meta-item">
|
||||
<MailOutlined />
|
||||
<span>{user.email}</span>
|
||||
</div>
|
||||
{user.phone && (
|
||||
<div className="meta-item">
|
||||
<span>📱</span>
|
||||
<span>{user.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
{user.createdAt && (
|
||||
<div className="meta-item">
|
||||
<ClockCircleOutlined />
|
||||
<span>加入于 {user.createdAt}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{user.bio && (
|
||||
<p className="user-bio">{user.bio}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-stats">
|
||||
<div className="stat-item">
|
||||
<HeartOutlined className="stat-icon" />
|
||||
<div className="stat-value">{user.favorites.length}</div>
|
||||
<div className="stat-label">收藏数</div>
|
||||
{/* 右侧:操作按钮 */}
|
||||
<div className="profile-right">
|
||||
<div className="profile-actions">
|
||||
<Button type="primary" icon={<EditOutlined />} size="large">
|
||||
编辑资料
|
||||
</Button>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<CommentOutlined className="stat-icon" />
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-label">评论数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据统计卡片 */}
|
||||
<div className="stats-cards-grid">
|
||||
<div className="stat-card-simple">
|
||||
<div className="stat-icon-circle">
|
||||
<HeartOutlined />
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<ClockCircleOutlined className="stat-icon" />
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-label">学习时长(小时)</div>
|
||||
<div className="stat-info">
|
||||
<div className="stat-number">{user.favorites.length}</div>
|
||||
<div className="stat-text">收藏</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card-simple">
|
||||
<div className="stat-icon-circle">
|
||||
<CommentOutlined />
|
||||
</div>
|
||||
<div className="stat-info">
|
||||
<div className="stat-number">0</div>
|
||||
<div className="stat-text">评论</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card-simple">
|
||||
<div className="stat-icon-circle">
|
||||
<ClockCircleOutlined />
|
||||
</div>
|
||||
<div className="stat-info">
|
||||
<div className="stat-number">0</div>
|
||||
<div className="stat-text">学习时长</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card-simple">
|
||||
<div className="stat-icon-circle">
|
||||
<TrophyOutlined />
|
||||
</div>
|
||||
<div className="stat-info">
|
||||
<div className="stat-number">{user.points || 0}</div>
|
||||
<div className="stat-text">积分</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
729
src/pages/User/CenterNew.tsx
Normal file
729
src/pages/User/CenterNew.tsx
Normal file
@ -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: <QuestionCircleOutlined /> },
|
||||
{ label: '男', value: 1, icon: <ManOutlined /> },
|
||||
{ label: '女', value: 2, icon: <WomanOutlined /> },
|
||||
]
|
||||
|
||||
// 浏览历史类型映射
|
||||
const historyTypeLabels: Record<ViewHistoryTargetType, string> = {
|
||||
heritage: '非遗项目',
|
||||
inheritor: '传承人',
|
||||
news: '资讯',
|
||||
event: '活动',
|
||||
}
|
||||
|
||||
const historyTypeColors: Record<ViewHistoryTargetType, string> = {
|
||||
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<UserProfile | null>(null)
|
||||
const [stats, setStats] = useState<UserStats | null>(null)
|
||||
const [viewHistory, setViewHistory] = useState<ViewHistoryItem[]>([])
|
||||
const [historyTotal, setHistoryTotal] = useState(0)
|
||||
const [historyPage, setHistoryPage] = useState(1)
|
||||
const [historyPageSize] = useState(10)
|
||||
const [historyType, setHistoryType] = useState<ViewHistoryTargetType | ''>('')
|
||||
|
||||
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<UploadFile>) => {
|
||||
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 (
|
||||
<List.Item
|
||||
extra={
|
||||
item.targetCover && (
|
||||
<img
|
||||
width={120}
|
||||
height={80}
|
||||
alt={item.targetTitle}
|
||||
src={item.targetCover}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<Space>
|
||||
<Tag color={historyTypeColors[item.targetType]}>
|
||||
{historyTypeLabels[item.targetType]}
|
||||
</Tag>
|
||||
<Link to={getLink()} style={{ fontSize: 16, fontWeight: 500 }}>
|
||||
{item.targetTitle}
|
||||
</Link>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<Space direction="vertical" size={4}>
|
||||
<div style={{ color: '#666', fontSize: 14 }}>
|
||||
{item.targetDescription}
|
||||
</div>
|
||||
<div style={{ color: '#999', fontSize: 13 }}>
|
||||
<HistoryOutlined /> 浏览时间:{dayjs(item.viewTime).format('YYYY-MM-DD HH:mm')}
|
||||
</div>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<Empty description="加载中..." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="user-center-page">
|
||||
{/* 封面背景 */}
|
||||
<div className="profile-cover">
|
||||
<div className="cover-overlay"></div>
|
||||
</div>
|
||||
|
||||
{/* 个人信息概览 */}
|
||||
<div className="profile-section">
|
||||
<div className="container">
|
||||
<Card className="profile-overview-card">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 24 }}>
|
||||
{/* 头像上传组件 */}
|
||||
<Upload
|
||||
name="file"
|
||||
listType="picture-circle"
|
||||
className="avatar-uploader"
|
||||
showUploadList={false}
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={handleAvatarChange}
|
||||
customRequest={customUploadRequest}
|
||||
>
|
||||
{avatarUploading ? (
|
||||
<div style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<LoadingOutlined style={{ fontSize: 32, color: '#1890ff' }} />
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Avatar
|
||||
size={100}
|
||||
src={profile.avatar}
|
||||
icon={<UserOutlined />}
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
width: 32,
|
||||
height: 32,
|
||||
background: '#1890ff',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
>
|
||||
<CameraOutlined style={{ color: '#fff', fontSize: 16 }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Upload>
|
||||
<div style={{ flex: 1 }}>
|
||||
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 8 }}>
|
||||
{profile.nickname}
|
||||
</h1>
|
||||
<Space size="middle">
|
||||
<span style={{ color: '#666' }}>
|
||||
<UserOutlined /> @{profile.username}
|
||||
</span>
|
||||
{profile.email && (
|
||||
<span style={{ color: '#666' }}>
|
||||
<MailOutlined /> {profile.email}
|
||||
</span>
|
||||
)}
|
||||
{profile.phone && (
|
||||
<span style={{ color: '#666' }}>
|
||||
<PhoneOutlined /> {profile.phone}
|
||||
</span>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<LockOutlined />}
|
||||
onClick={() => setPasswordModalVisible(true)}
|
||||
>
|
||||
修改密码
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
{stats && (
|
||||
<>
|
||||
<Divider />
|
||||
<Row gutter={16}>
|
||||
<Col span={4}>
|
||||
<Statistic
|
||||
title="浏览历史"
|
||||
value={stats.viewHistoryCount}
|
||||
prefix={<EyeOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Statistic
|
||||
title="我的评论"
|
||||
value={stats.commentCount}
|
||||
prefix={<CommentOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Statistic
|
||||
title="我的点赞"
|
||||
value={stats.likeCount}
|
||||
prefix={<LikeOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Statistic
|
||||
title="我的收藏"
|
||||
value={stats.favoriteCount}
|
||||
prefix={<StarOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Statistic
|
||||
title="活动报名"
|
||||
value={stats.eventRegistrationCount}
|
||||
prefix={<CalendarOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 详细信息区 */}
|
||||
<div className="user-content-section section-spacing">
|
||||
<div className="container">
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab} size="large">
|
||||
{/* 个人资料 */}
|
||||
<TabPane tab={<span><UserOutlined />个人资料</span>} key="profile">
|
||||
<Card
|
||||
title="基本信息"
|
||||
extra={
|
||||
<Button
|
||||
type={profileEditing ? 'default' : 'primary'}
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
if (profileEditing) {
|
||||
profileForm.resetFields()
|
||||
}
|
||||
setProfileEditing(!profileEditing)
|
||||
}}
|
||||
>
|
||||
{profileEditing ? '取消编辑' : '编辑资料'}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={profileForm}
|
||||
layout="vertical"
|
||||
disabled={!profileEditing}
|
||||
>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="昵称" name="nickname">
|
||||
<Input placeholder="请输入昵称" size="large" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="邮箱"
|
||||
name="email"
|
||||
rules={[
|
||||
{ type: 'email', message: '请输入正确的邮箱格式' }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入邮箱" size="large" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="手机号"
|
||||
name="phone"
|
||||
rules={[
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入手机号" size="large" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item label="性别" name="gender">
|
||||
<Select placeholder="请选择性别" size="large">
|
||||
{genderOptions.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.icon} {opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item label="生日" name="birthday">
|
||||
<DatePicker
|
||||
placeholder="请选择生日"
|
||||
size="large"
|
||||
style={{ width: '100%' }}
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item label="省份" name="province">
|
||||
<Input placeholder="请输入省份" size="large" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item label="城市" name="city">
|
||||
<Input placeholder="请输入城市" size="large" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{profileEditing && (
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={loading}
|
||||
onClick={handleUpdateProfile}
|
||||
>
|
||||
保存修改
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div style={{ color: '#999', fontSize: 13 }}>
|
||||
注册时间:{dayjs(profile.createTime).format('YYYY-MM-DD HH:mm')}
|
||||
</div>
|
||||
</Card>
|
||||
</TabPane>
|
||||
|
||||
{/* 浏览历史 */}
|
||||
<TabPane tab={<span><HistoryOutlined />浏览历史</span>} key="history">
|
||||
<Card>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<span>筛选类型:</span>
|
||||
<Select
|
||||
value={historyType}
|
||||
onChange={(value) => {
|
||||
setHistoryType(value)
|
||||
setHistoryPage(1)
|
||||
}}
|
||||
style={{ width: 150 }}
|
||||
>
|
||||
<Option value="">全部</Option>
|
||||
<Option value="heritage">非遗项目</Option>
|
||||
<Option value="inheritor">传承人</Option>
|
||||
<Option value="news">资讯</Option>
|
||||
<Option value="event">活动</Option>
|
||||
</Select>
|
||||
</Space>
|
||||
|
||||
<List
|
||||
loading={loading}
|
||||
dataSource={viewHistory}
|
||||
renderItem={renderHistoryItem}
|
||||
locale={{ emptyText: <Empty description="暂无浏览历史" /> }}
|
||||
/>
|
||||
|
||||
{/* 自定义分页 */}
|
||||
{viewHistory.length > 0 && (
|
||||
<CustomPagination
|
||||
current={historyPage}
|
||||
pageSize={historyPageSize}
|
||||
total={historyTotal}
|
||||
unit="条记录"
|
||||
onChange={(page) => {
|
||||
setHistoryPage(page)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 修改密码弹窗 */}
|
||||
<Modal
|
||||
title="修改密码"
|
||||
open={passwordModalVisible}
|
||||
onOk={handleUpdatePassword}
|
||||
onCancel={() => {
|
||||
setPasswordModalVisible(false)
|
||||
passwordForm.resetFields()
|
||||
}}
|
||||
confirmLoading={passwordSubmitting}
|
||||
okText="确认修改"
|
||||
cancelText="取消"
|
||||
width={480}
|
||||
>
|
||||
<Form
|
||||
form={passwordForm}
|
||||
layout="vertical"
|
||||
style={{ marginTop: 24 }}
|
||||
>
|
||||
<Form.Item
|
||||
label="旧密码"
|
||||
name="oldPassword"
|
||||
rules={[{ required: true, message: '请输入旧密码' }]}
|
||||
>
|
||||
<Input.Password placeholder="请输入旧密码" size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="新密码"
|
||||
name="newPassword"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 6, max: 20, message: '密码长度必须在6-20个字符之间' }
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请输入新密码" size="large" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="确认新密码"
|
||||
name="confirmPassword"
|
||||
rules={[
|
||||
{ required: true, message: '请确认新密码' },
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请再次输入新密码" size="large" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserCenterNew
|
||||
@ -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"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
name="account"
|
||||
rules={[{ required: true, message: '请输入账号' }]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="用户名" />
|
||||
<Input prefix={<UserOutlined />} placeholder="用户名/邮箱/手机号" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
|
||||
@ -16,8 +16,11 @@ const Register: React.FC = () => {
|
||||
|
||||
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: '用户名格式为数字、字母及下划线' },
|
||||
]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="用户名" />
|
||||
<Input prefix={<UserOutlined />} placeholder="用户名(3-16位,字母/数字/下划线)" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="nickname"
|
||||
rules={[
|
||||
{ required: true, message: '请输入昵称' },
|
||||
]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="昵称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' },
|
||||
]}
|
||||
>
|
||||
<Input prefix={<MailOutlined />} placeholder="邮箱" />
|
||||
<Input prefix={<MailOutlined />} placeholder="邮箱(可选)" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="phone"
|
||||
rules={[
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' },
|
||||
]}
|
||||
>
|
||||
<Input prefix={<UserOutlined />} placeholder="手机号(可选)" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' },
|
||||
{ min: 6, max: 20, message: '密码长度为 6-20 位' },
|
||||
]}
|
||||
>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="密码(6-20位)" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
dependencies={['password']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认密码' },
|
||||
{ required: true, message: '请输入确认密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('password') === value) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user