refactor(profile): 重新设计个人中心页面排版

- 合并用户信息卡片和积分资产区,移除独立的账户信息区块
- 退出登录移至页面底部,避免与侧边栏重复
- 接入真实用户数据到侧边栏头像和昵称
- 新增系统默认头像 SVG,替代首字母占位
- 清理不再使用的 CSS 样式
This commit is contained in:
小鱼开发
2026-05-10 20:49:38 +08:00
parent 6e52935768
commit c1eaae64aa
5 changed files with 135 additions and 225 deletions
+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#36b26a"/>
<stop offset="100%" stop-color="#2d9e5a"/>
</linearGradient>
</defs>
<circle cx="50" cy="50" r="50" fill="url(#g)"/>
<circle cx="50" cy="38" r="13" fill="none" stroke="white" stroke-width="4.5" stroke-linecap="round"/>
<path d="M26 80 Q26 58 50 58 Q74 58 74 80" fill="none" stroke="white" stroke-width="4.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 533 B

@@ -181,12 +181,6 @@
background: var(--bg-hover);
}
.sidebar-divider {
height: 1px;
background: var(--border-light);
margin: var(--spacing-xs) 0;
}
.user-avatar {
width: 36px;
height: 36px;
@@ -216,39 +210,4 @@
text-overflow: ellipsis;
}
.user-role {
font-size: var(--font-xs);
color: var(--text-tertiary);
}
.sidebar-logout {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-md);
color: var(--text-tertiary);
background: transparent;
border: none;
cursor: pointer;
font-size: var(--font-xs);
font-family: var(--font-family);
width: 100%;
text-align: center;
transition: all var(--transition-fast);
}
.sidebar-logout:hover {
background: var(--bg-hover);
color: var(--text-secondary);
}
.sidebar-logout svg {
flex-shrink: 0;
opacity: 0.7;
}
.sidebar-logout:hover svg {
opacity: 1;
}
+10 -18
View File
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { createNewProject } from '../../store';
import { createNewProject, useAuthStore } from '../../store';
import { useNavigation } from '../../contexts/NavigationContext';
import './Sidebar.css';
@@ -45,6 +45,7 @@ interface SidebarProps {
export default function Sidebar({ currentPath, onNavigate }: SidebarProps) {
const { appEnvironment } = useNavigation();
const authUser = useAuthStore((s) => s.user);
const [expandedItems, setExpandedItems] = useState<Set<string>>(
new Set(['content-management', 'settings'])
);
@@ -198,26 +199,17 @@ export default function Sidebar({ currentPath, onNavigate }: SidebarProps) {
</div>
)}
<div className="sidebar-footer">
<div className="sidebar-user" onClick={() => onNavigate('profile')}>
<div className="user-avatar">U</div>
<div className="sidebar-user" onClick={() => onNavigate('profile')} title="个人中心">
<img
src={authUser?.avatar || '/default-avatar.svg'}
alt="avatar"
className="user-avatar"
style={{ objectFit: 'cover' }}
/>
<div className="user-info">
<span className="user-name"></span>
<span className="user-role"></span>
<span className="user-name">{authUser?.nickname || '用户'}</span>
</div>
</div>
<div className="sidebar-divider" />
<button
className="sidebar-logout"
onClick={() => onNavigate('logout')}
title="退出登录"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
<span>退</span>
</button>
</div>
</aside>
);
@@ -371,17 +371,6 @@
============================================================ */
/* 顶部用户信息栏 */
.profile-topbar {
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-xl) var(--spacing-2xl);
background: var(--bg-card);
border-radius: var(--radius-xl);
border: 1px solid var(--border-light);
margin-bottom: var(--spacing-xl);
}
.profile-topbar-avatar {
width: 64px;
height: 64px;
@@ -397,24 +386,6 @@
object-fit: cover;
}
.profile-topbar-info {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.profile-topbar-name {
font-size: var(--font-xl);
font-weight: 600;
color: var(--text-primary);
}
.profile-topbar-meta {
font-size: var(--font-sm);
color: var(--text-secondary);
}
.profile-topbar-logout {
padding: 8px 20px;
border-radius: var(--radius-lg);
@@ -432,43 +403,6 @@
border-color: var(--text-secondary);
}
/* 积分统计行 */
.profile-stats-row {
display: flex;
gap: var(--spacing-lg);
align-items: stretch;
}
.profile-stat-box {
flex: 1;
background: var(--bg-card);
border: 1px solid var(--border-light);
border-radius: var(--radius-xl);
padding: var(--spacing-xl);
display: flex;
flex-direction: column;
justify-content: center;
}
.profile-stat-value {
font-size: 32px;
font-weight: 700;
line-height: 1.2;
color: var(--text-primary);
}
.profile-stat-label {
font-size: var(--font-sm);
color: var(--text-secondary);
margin-top: 4px;
}
.profile-stat-action {
display: flex;
align-items: center;
padding: 0 var(--spacing-xl);
}
/* Avatar Clone Card - 这个就是我们要复用的样式 */
.avatar-card {
position: relative;
+114 -100
View File
@@ -13,11 +13,6 @@ interface UserProfile {
avatar: string;
}
function getInitials(nickname: string | null): string {
if (!nickname) return 'U';
return nickname.charAt(0).toUpperCase();
}
function formatTxTime(iso: string): string {
const d = new Date(iso);
return d.toLocaleString('zh-CN', {
@@ -123,36 +118,95 @@ export default function Profile() {
return (
<div className="settings-page">
{/* 顶部用户信息栏 */}
<div className="profile-topbar">
{displayAvatar ? (
<img src={displayAvatar} alt="avatar" className="profile-topbar-avatar" />
) : (
<div className="profile-topbar-avatar">{getInitials(displayName)}</div>
)}
<div className="profile-topbar-info">
<div className="profile-topbar-name">{displayName}</div>
</div>
</div>
{/* 积分统计 */}
<div className="profile-stats-row">
<div className="profile-stat-box">
<div className="profile-stat-value" style={{ color: '#36b26a' }}>
{balance?.balance ?? 0}
{/* 个人资料卡片 */}
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
{/* 用户区 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', padding: '24px 28px' }}>
<img
src={displayAvatar || '/default-avatar.svg'}
alt="avatar"
className="profile-topbar-avatar"
/>
<div style={{ flex: 1, minWidth: 0 }}>
{editing ? (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<input
type="text"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
maxLength={20}
autoFocus
style={{
padding: '6px 12px',
borderRadius: '8px',
border: '1px solid var(--border-color)',
fontSize: '15px',
outline: 'none',
width: '160px',
}}
onKeyDown={(e) => { if (e.key === 'Enter') handleSaveNickname(); }}
/>
<button className="btn btn-primary btn-sm" onClick={handleSaveNickname} disabled={saving}>
{saving ? '保存中' : '保存'}
</button>
<button
className="btn btn-ghost btn-sm"
onClick={() => { setEditing(false); setNickname(displayName); setNickError(''); }}
disabled={saving}
>
</button>
</div>
) : (
<div style={{ display: 'flex', alignItems: 'baseline', gap: '8px' }}>
<span style={{ fontSize: '18px', fontWeight: 600, color: 'var(--text-primary)' }}>
{displayName}
</span>
<button
onClick={() => setEditing(true)}
style={{
fontSize: '12px',
color: '#36b26a',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 0,
}}
>
</button>
</div>
)}
{nickError && (
<div style={{ color: '#e74c3c', fontSize: '12px', marginTop: '4px' }}>
{nickError}
</div>
)}
{displayMobile && (
<div style={{ fontSize: '13px', color: 'var(--text-secondary)', marginTop: '2px' }}>
{displayMobile}
</div>
)}
</div>
<div className="profile-stat-label"></div>
</div>
<div className="profile-stat-box">
<div className="profile-stat-value">{balance?.totalRecharged ?? 0}</div>
<div className="profile-stat-label"></div>
</div>
<div className="profile-stat-box">
<div className="profile-stat-value">{balance?.totalConsumed ?? 0}</div>
<div className="profile-stat-label"></div>
</div>
<div className="profile-stat-action">
<button className="btn btn-primary" onClick={() => setShowRechargeModal(true)}>
<div style={{ height: 1, background: 'var(--border-light)', margin: '0 28px' }} />
{/* 积分区 */}
<div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', padding: '24px 28px' }}>
<div>
<div style={{ fontSize: '13px', color: 'var(--text-secondary)', marginBottom: '8px' }}>
</div>
<div style={{ fontSize: '40px', fontWeight: 700, color: '#36b26a', lineHeight: 1 }}>
{balance?.balance ?? 0}
</div>
</div>
<button
className="btn btn-primary"
style={{ padding: '10px 28px', fontSize: '14px' }}
onClick={() => setShowRechargeModal(true)}
>
线
</button>
</div>
@@ -223,74 +277,34 @@ export default function Profile() {
</div>
</div>
{/* 账户信息 */}
{/* 退出登录 — 页面底部 */}
<div style={{ marginTop: 'var(--spacing-xl)' }}>
<h3 style={{ fontSize: 'var(--font-md)', fontWeight: 600, marginBottom: 'var(--spacing-md)' }}>
</h3>
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
<div className="settings-row">
<span className="settings-row-label"></span>
<span className="settings-row-value" style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-sm)' }}>
{editing ? (
<>
<input
type="text"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
maxLength={20}
autoFocus
style={{
padding: '4px 10px',
borderRadius: '6px',
border: '1px solid var(--border-color)',
fontSize: 'var(--font-sm)',
outline: 'none',
width: '160px',
}}
onKeyDown={(e) => { if (e.key === 'Enter') handleSaveNickname(); }}
/>
<button className="btn btn-primary btn-sm" onClick={handleSaveNickname} disabled={saving}>
{saving ? '保存中' : '保存'}
</button>
<button
className="btn btn-ghost btn-sm"
onClick={() => { setEditing(false); setNickname(displayName); setNickError(''); }}
disabled={saving}
>
</button>
</>
) : (
<>
<span>{displayName}</span>
<button className="btn btn-ghost btn-sm" onClick={() => setEditing(true)}>
</button>
</>
)}
</span>
</div>
{nickError && (
<div style={{ padding: '0 var(--spacing-lg) var(--spacing-md)', color: '#e74c3c', fontSize: 'var(--font-sm)' }}>
{nickError}
</div>
)}
{displayMobile && (
<div className="settings-row" style={{ borderTop: '1px solid var(--border-light)' }}>
<span className="settings-row-label"></span>
<span className="settings-row-value">{displayMobile}</span>
</div>
)}
<div className="settings-row" style={{ borderTop: '1px solid var(--border-light)' }}>
<span className="settings-row-label" />
<button className="profile-logout-btn" style={{ width: 'auto', padding: '8px 24px', margin: 0 }} onClick={handleLogout}>
退
</button>
</div>
</div>
<button
onClick={handleLogout}
style={{
width: '100%',
padding: '14px',
borderRadius: 'var(--radius-lg)',
border: '1px solid var(--border-light)',
background: 'var(--bg-card)',
fontSize: 'var(--font-sm)',
color: 'var(--text-secondary)',
cursor: 'pointer',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
(e.target as HTMLButtonElement).style.color = '#e74c3c';
(e.target as HTMLButtonElement).style.borderColor = '#e74c3c';
(e.target as HTMLButtonElement).style.background = '#fdf2f2';
}}
onMouseLeave={(e) => {
(e.target as HTMLButtonElement).style.color = 'var(--text-secondary)';
(e.target as HTMLButtonElement).style.borderColor = 'var(--border-light)';
(e.target as HTMLButtonElement).style.background = 'var(--bg-card)';
}}
>
退
</button>
</div>
<RechargeModal