refactor(profile): 重新设计个人中心页面排版
- 合并用户信息卡片和积分资产区,移除独立的账户信息区块 - 退出登录移至页面底部,避免与侧边栏重复 - 接入真实用户数据到侧边栏头像和昵称 - 新增系统默认头像 SVG,替代首字母占位 - 清理不再使用的 CSS 样式
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user