refactor(profile): extract CSS classes, fix undefined vars, remove inline styles

- variables.css: add --bg-secondary and --bg-tertiary (used but undefined)
- ContentManagement.css: add 30+ Profile CSS classes following design system
- Profile.tsx: rewrite with className, remove all inline styles and emoji
  - pricing modal tags use semantic colors via CSS classes
  - logout hover uses var(--error) instead of hardcoded #e74c3c
  - menu items use CSS hover instead of onMouseEnter/Leave handlers
This commit is contained in:
小鱼开发
2026-05-22 11:04:01 +08:00
parent aebc9f6bcc
commit 0a52195490
3 changed files with 470 additions and 315 deletions
@@ -403,6 +403,354 @@
border-color: var(--text-secondary);
}
/* ── Profile 页面重构样式(基于设计系统)── */
.profile-user-section {
display: flex;
align-items: center;
gap: var(--spacing-lg);
padding: var(--spacing-xl) 28px;
}
.profile-user-info {
flex: 1;
min-width: 0;
}
.profile-nickname-wrap {
display: flex;
align-items: center;
gap: 6px;
}
.profile-nickname {
font-size: var(--font-lg);
font-weight: 600;
color: var(--text-primary);
}
.profile-nickname-input {
padding: 4px 10px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
font-size: var(--font-lg);
font-weight: 600;
color: var(--text-primary);
outline: none;
width: 160px;
font-family: inherit;
}
.profile-nickname-input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.profile-nickname-input.error {
border-color: var(--error);
}
.profile-nickname-error {
color: var(--error);
font-size: 12px;
}
.profile-edit-icon {
cursor: pointer;
flex-shrink: 0;
color: var(--primary);
}
.profile-mobile {
font-size: var(--font-sm);
color: var(--text-secondary);
margin-top: 2px;
}
.profile-divider {
height: 1px;
background: var(--border-light);
margin: 0 28px;
}
.profile-points-section {
display: flex;
align-items: flex-end;
gap: var(--spacing-lg);
padding: var(--spacing-xl) 28px;
}
.profile-points-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-lg);
flex: 1;
}
.profile-points-card {
background: var(--bg-card);
border: 1px solid var(--border-light);
border-radius: var(--radius-xl);
padding: 20px 24px;
}
.profile-points-label {
font-size: var(--font-sm);
color: var(--text-secondary);
margin-bottom: 12px;
}
.profile-points-value-row {
display: flex;
align-items: baseline;
gap: 6px;
}
.profile-points-value {
font-size: 36px;
font-weight: 700;
line-height: 1;
}
.profile-points-value.primary {
color: var(--primary);
}
.profile-points-value.danger {
color: #ff6b6b;
}
.profile-points-unit {
font-size: var(--font-sm);
color: var(--text-tertiary);
}
.profile-points-actions {
display: flex;
flex-direction: column;
gap: 10px;
}
.profile-points-actions .btn {
padding: 10px 24px;
font-size: var(--font-sm);
white-space: nowrap;
}
.profile-section-title {
font-size: var(--font-md);
font-weight: 600;
margin-bottom: var(--spacing-md);
}
.profile-menu-list {
padding: 0;
overflow: hidden;
}
.profile-menu-item {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 16px 24px;
border: none;
border-bottom: 1px solid var(--border-light);
background: transparent;
cursor: pointer;
font-size: var(--font-base);
font-family: inherit;
color: var(--text-primary);
text-align: left;
transition: background var(--transition-fast);
}
.profile-menu-item:last-child {
border-bottom: none;
}
.profile-menu-item:hover {
background: var(--bg-hover);
}
.profile-menu-icon {
font-size: 18px;
line-height: 1;
color: var(--text-secondary);
}
.profile-menu-label {
flex: 1;
}
.profile-menu-arrow {
color: var(--text-tertiary);
}
.profile-logout-btn {
width: 100%;
padding: 14px;
border-radius: var(--radius-lg);
border: 1px solid var(--border-light);
background: var(--bg-card);
font-size: var(--font-sm);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.profile-logout-btn:hover {
color: var(--error);
border-color: var(--error);
background: rgb(var(--error-rgb) / 6%);
}
/* 定价 Modal */
.profile-pricing-body {
padding: 8px 4px;
}
.profile-pricing-table {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
border: 1px solid var(--border-light);
overflow: hidden;
}
.profile-pricing-header {
display: grid;
grid-template-columns: 1fr 90px 140px;
gap: 12px;
padding: 14px 20px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-light);
font-size: var(--font-sm);
font-weight: 600;
color: var(--text-secondary);
}
.profile-pricing-header span:nth-child(2) {
text-align: center;
}
.profile-pricing-header span:nth-child(3) {
text-align: right;
}
.profile-pricing-row {
display: grid;
grid-template-columns: 1fr 90px 140px;
gap: 12px;
padding: 10px 20px;
border-bottom: 1px solid var(--border-light);
align-items: center;
}
.profile-pricing-row:last-child {
border-bottom: none;
}
.profile-pricing-name {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
}
.profile-pricing-mode {
text-align: center;
}
.profile-pricing-tag {
display: inline-block;
font-size: 12px;
padding: 3px 10px;
border-radius: 20px;
font-weight: 500;
}
.profile-pricing-tag.fixed {
background: #e8f4fd;
color: #1976d2;
}
.profile-pricing-tag.duration {
background: #fff3e0;
color: #e65100;
}
.profile-pricing-points {
font-size: 14px;
font-weight: 600;
color: var(--primary);
text-align: right;
}
.profile-pricing-empty {
text-align: center;
padding: 32px;
color: var(--text-secondary);
font-size: 14px;
}
.profile-pricing-detail-section {
margin-top: var(--spacing-lg);
padding: 16px 20px;
background: #fff8f0;
border-radius: 10px;
border: 1px solid #ffe8d0;
}
.profile-pricing-detail-title {
font-size: var(--font-sm);
font-weight: 600;
color: #d46b08;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
}
.profile-pricing-detail-row {
display: flex;
align-items: baseline;
gap: 8px;
font-size: var(--font-sm);
color: var(--text-secondary);
line-height: 1.6;
}
.profile-pricing-detail-row strong {
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
}
.profile-pricing-info-section {
margin-top: var(--spacing-lg);
padding: 16px 20px;
background: #f8faf8;
border-radius: 10px;
border: 1px solid #e8f0e8;
}
.profile-pricing-info-title {
font-size: var(--font-sm);
font-weight: 600;
color: #2e7d32;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 6px;
}
.profile-pricing-info-list {
margin: 0;
padding-left: 18px;
font-size: var(--font-sm);
color: var(--text-secondary);
line-height: 1.8;
}
/* Avatar Clone Card - 这个就是我们要复用的样式 */
.avatar-card {
position: relative;
+118 -315
View File
@@ -28,10 +28,52 @@ const SOURCE_TYPE_LABELS: Record<string, string> = {
};
function maskMobile(mobile: string): string {
if (!mobile || mobile.length !== 11) {return mobile;}
if (!mobile || mobile.length !== 11) { return mobile; }
return `${mobile.slice(0, 3)}****${mobile.slice(7)}`;
}
const FileTextIcon = () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><polyline points="10 9 9 9 8 9" />
</svg>
);
const DollarSignIcon = () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="1" x2="12" y2="23" /><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
</svg>
);
const SettingsIcon = () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
);
const InfoIcon = () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" /><line x1="12" y1="16" x2="12" y2="12" /><line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
);
const ChevronRightIcon = ({ className }: { className?: string }) => (
<svg className={className} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6" />
</svg>
);
const ClockIcon = () => (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
</svg>
);
const EditIcon = () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" />
</svg>
);
export default function Profile() {
const { navigate } = useNavigation();
const authUser = useAuthStore((s) => s.user);
@@ -61,10 +103,10 @@ export default function Profile() {
setUser(profileData);
setNickname(profileData.nickname || '');
}
if (balanceData) {setBalance(balanceData);}
if (balanceData) { setBalance(balanceData); }
const todayData = await pointsApi.getTodayConsumed().catch(() => null);
if (todayData) {setTodayConsumed(todayData.total);}
if (todayData) { setTodayConsumed(todayData.total); }
} catch (e) {
console.error('[Profile] 加载数据失败:', e);
}
@@ -115,7 +157,7 @@ export default function Profile() {
};
const handleLogout = async () => {
if (!window.confirm('确定要退出登录吗?')) {return;}
if (!window.confirm('确定要退出登录吗?')) { return; }
await logout();
window.location.reload();
};
@@ -124,18 +166,25 @@ export default function Profile() {
const displayAvatar = user?.avatar || authUser?.avatar || '';
const displayMobile = user?.mobile ? maskMobile(user.mobile) : '';
const menuItems = [
{ label: '使用明细', icon: <FileTextIcon />, onClick: () => navigate('usage-detail') },
{ label: '产品定价', icon: <DollarSignIcon />, onClick: handleOpenPricing },
{ label: '系统设置', icon: <SettingsIcon />, onClick: () => navigate('system-update') },
{ label: '关于我们', icon: <InfoIcon />, onClick: () => navigate('about-us') },
];
return (
<div className="settings-page">
{/* 个人资料卡片 */}
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
{/* 用户区 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', padding: '24px 28px' }}>
<div className="profile-user-section">
<img
src={displayAvatar || '/default-avatar.svg'}
alt="avatar"
className="profile-topbar-avatar"
/>
<div style={{ flex: 1, minWidth: 0 }}>
<div className="profile-user-info">
{editing ? (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<input
@@ -145,19 +194,9 @@ export default function Profile() {
maxLength={20}
autoFocus
disabled={saving}
style={{
padding: '4px 10px',
borderRadius: '6px',
border: nickError ? '1px solid #e74c3c' : '1px solid var(--border-color)',
fontSize: '18px',
fontWeight: 600,
color: 'var(--text-primary)',
outline: 'none',
width: '160px',
fontFamily: 'inherit',
}}
className={`profile-nickname-input ${nickError ? 'error' : ''}`}
onKeyDown={(e) => {
if (e.key === 'Enter') {handleSaveNickname();}
if (e.key === 'Enter') { handleSaveNickname(); }
if (e.key === 'Escape') {
setEditing(false);
setNickname(displayName);
@@ -176,99 +215,50 @@ export default function Profile() {
}}
/>
{nickError && (
<span style={{ color: '#e74c3c', fontSize: '12px' }}>
{nickError}
</span>
<span className="profile-nickname-error">{nickError}</span>
)}
</div>
) : (
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ fontSize: '18px', fontWeight: 600, color: 'var(--text-primary)' }}>
{displayName}
<div className="profile-nickname-wrap">
<span className="profile-nickname">{displayName}</span>
<span className="profile-edit-icon" onClick={() => setEditing(true)}>
<EditIcon />
</span>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="#36b26a"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
style={{ cursor: 'pointer', flexShrink: 0 }}
onClick={() => setEditing(true)}
>
<title></title>
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" />
</svg>
</div>
)}
{displayMobile && (
<div style={{ fontSize: '13px', color: 'var(--text-secondary)', marginTop: '2px' }}>
{displayMobile}
</div>
<div className="profile-mobile">{displayMobile}</div>
)}
</div>
</div>
<div style={{ height: 1, background: 'var(--border-light)', margin: '0 28px' }} />
<div className="profile-divider" />
{/* 积分统计卡片 */}
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '16px', padding: '24px 28px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', flex: 1 }}>
{/* 剩余积分 */}
<div
style={{
background: 'var(--bg-card)',
border: '1px solid var(--border-light)',
borderRadius: 'var(--radius-xl)',
padding: '20px 24px',
}}
>
<div style={{ fontSize: 'var(--font-sm)', color: 'var(--text-secondary)', marginBottom: '12px' }}>
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '6px' }}>
<span style={{ fontSize: '36px', fontWeight: 700, color: '#36b26a', lineHeight: 1 }}>
{balance?.balance ?? 0}
</span>
<span style={{ fontSize: 'var(--font-sm)', color: 'var(--text-tertiary)' }}></span>
{/* 积分统计 */}
<div className="profile-points-section">
<div className="profile-points-grid">
<div className="profile-points-card">
<div className="profile-points-label"></div>
<div className="profile-points-value-row">
<span className="profile-points-value primary">{balance?.balance ?? 0}</span>
<span className="profile-points-unit"></span>
</div>
</div>
{/* 今日消耗 */}
<div
style={{
background: 'var(--bg-card)',
border: '1px solid var(--border-light)',
borderRadius: 'var(--radius-xl)',
padding: '20px 24px',
}}
>
<div style={{ fontSize: 'var(--font-sm)', color: 'var(--text-secondary)', marginBottom: '12px' }}>
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '6px' }}>
<span style={{ fontSize: '36px', fontWeight: 700, color: '#ff6b6b', lineHeight: 1 }}>
{todayConsumed}
</span>
<span style={{ fontSize: 'var(--font-sm)', color: 'var(--text-tertiary)' }}></span>
<div className="profile-points-card">
<div className="profile-points-label"></div>
<div className="profile-points-value-row">
<span className="profile-points-value danger">{todayConsumed}</span>
<span className="profile-points-unit"></span>
</div>
</div>
</div>
{/* 操作按钮 */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<button
className="btn btn-primary btn-sm"
style={{ padding: '10px 24px', fontSize: 'var(--font-sm)', whiteSpace: 'nowrap' }}
onClick={() => setShowRechargeModal(true)}
>
<div className="profile-points-actions">
<button className="btn btn-primary btn-sm" onClick={() => setShowRechargeModal(true)}>
</button>
<button
className="btn btn-ghost btn-sm"
style={{ padding: '10px 24px', fontSize: 'var(--font-sm)', whiteSpace: 'nowrap' }}
onClick={() => {
localStorage.setItem('usage-detail-initial-tab', 'recharge');
navigate('usage-detail');
@@ -276,85 +266,30 @@ export default function Profile() {
>
</button>
<button
className="btn btn-ghost btn-sm"
style={{ padding: '10px 24px', fontSize: 'var(--font-sm)', whiteSpace: 'nowrap' }}
onClick={handleOpenPricing}
>
<button className="btn btn-ghost btn-sm" onClick={handleOpenPricing}>
</button>
</div>
</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' }}>
{[
{ label: '使用明细', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>, onClick: () => navigate('usage-detail') },
{ label: '产品定价', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>, onClick: handleOpenPricing },
{ label: '系统设置', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>, onClick: () => navigate('system-update') },
{ label: '关于我们', icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>, onClick: () => navigate('about-us') },
].map((item, index, arr) => (
<button
key={item.label}
onClick={item.onClick}
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
width: '100%',
padding: '16px 24px',
border: 'none',
borderBottom: index < arr.length - 1 ? '1px solid var(--border-light)' : 'none',
background: 'transparent',
cursor: 'pointer',
fontSize: 'var(--font-base)',
fontFamily: 'inherit',
color: 'var(--text-primary)',
textAlign: 'left',
transition: 'background 0.15s',
}}
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.background = 'var(--bg-hover)'; }}
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = 'transparent'; }}
>
<span style={{ fontSize: '18px', lineHeight: 1 }}>{item.icon}</span>
<span style={{ flex: 1 }}>{item.label}</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6" />
</svg>
<h3 className="profile-section-title"></h3>
<div className="card profile-menu-list">
{menuItems.map((item) => (
<button key={item.label} className="profile-menu-item" onClick={item.onClick}>
<span className="profile-menu-icon">{item.icon}</span>
<span className="profile-menu-label">{item.label}</span>
<ChevronRightIcon className="profile-menu-arrow" />
</button>
))}
</div>
</div>
{/* 退出登录 — 页面底部 */}
{/* 退出登录 */}
<div style={{ marginTop: 'var(--spacing-xl)' }}>
<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 className="profile-logout-btn" onClick={handleLogout}>
退
</button>
</div>
@@ -365,85 +300,33 @@ export default function Profile() {
onRechargeSuccess={handleRechargeSuccess}
/>
<Modal
open={showPricingModal}
onClose={() => setShowPricingModal(false)}
width="600px"
>
<Modal open={showPricingModal} onClose={() => setShowPricingModal(false)} width="600px">
{pricingLoading ? (
<div style={{ textAlign: 'center', padding: '40px', color: 'var(--text-secondary)' }}>
...
</div>
) : (
<div style={{ padding: '8px 4px' }}>
{/* 表格区域 */}
<div
style={{
background: 'var(--bg-secondary)',
borderRadius: '12px',
border: '1px solid var(--border-light)',
overflow: 'hidden',
}}
>
{/* 表头 */}
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 90px 140px',
gap: '12px',
padding: '14px 20px',
background: 'var(--bg-tertiary)',
borderBottom: '1px solid var(--border-light)',
fontSize: '13px',
fontWeight: 600,
color: 'var(--text-secondary)',
}}
>
<div className="profile-pricing-body">
{/* 表格 */}
<div className="profile-pricing-table">
<div className="profile-pricing-header">
<span></span>
<span style={{ textAlign: 'center' }}></span>
<span style={{ textAlign: 'right' }}></span>
<span></span>
<span></span>
</div>
{/* 表格行 */}
{pricingRules
.filter((rule) => rule.mode !== 'free')
.map((rule, index, arr) => (
<div
key={rule.sourceType}
style={{
display: 'grid',
gridTemplateColumns: '1fr 90px 140px',
gap: '12px',
padding: '10px 20px',
borderBottom: index < arr.length - 1 ? '1px solid var(--border-light)' : 'none',
alignItems: 'center',
}}
>
<span style={{ fontSize: '14px', fontWeight: 500, color: 'var(--text-primary)' }}>
.map((rule) => (
<div key={rule.sourceType} className="profile-pricing-row">
<span className="profile-pricing-name">
{SOURCE_TYPE_LABELS[rule.sourceType] || rule.sourceType}
</span>
<span style={{ textAlign: 'center' }}>
<span
style={{
display: 'inline-block',
fontSize: '12px',
padding: '3px 10px',
borderRadius: '20px',
fontWeight: 500,
background: rule.mode === 'fixed' ? '#e8f4fd' : '#fff3e0',
color: rule.mode === 'fixed' ? '#1976d2' : '#e65100',
}}
>
<span className="profile-pricing-mode">
<span className={`profile-pricing-tag ${rule.mode}`}>
{rule.mode === 'fixed' ? '固定' : '按时长'}
</span>
</span>
<span
style={{
fontSize: '14px',
fontWeight: 600,
color: '#36b26a',
textAlign: 'right',
}}
>
<span className="profile-pricing-points">
{rule.mode === 'fixed'
? `${rule.points} 积分/次`
: `${rule.unit} ${rule.pointsPerUnit} 积分`}
@@ -451,45 +334,15 @@ export default function Profile() {
</div>
))}
{pricingRules.filter((rule) => rule.mode !== 'free').length === 0 && (
<div
style={{
textAlign: 'center',
padding: '32px',
color: 'var(--text-secondary)',
fontSize: '14px',
}}
>
</div>
<div className="profile-pricing-empty"></div>
)}
</div>
{/* 按时长计费细则 */}
{/* 按时长细则 */}
{pricingRules.some((r) => r.mode === 'duration') && (
<div
style={{
marginTop: '16px',
padding: '16px 20px',
background: '#fff8f0',
borderRadius: '10px',
border: '1px solid #ffe8d0',
}}
>
<div
style={{
fontSize: '13px',
fontWeight: 600,
color: '#d46b08',
marginBottom: '10px',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#d46b08" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
<div className="profile-pricing-detail-section">
<div className="profile-pricing-detail-title">
<ClockIcon />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
@@ -497,34 +350,14 @@ export default function Profile() {
.filter((r) => r.mode === 'duration')
.map((rule) => {
const contentType =
rule.sourceType === 'tts'
? '频'
: rule.sourceType === 'video'
? '视频'
rule.sourceType === 'tts' ? '音频'
: rule.sourceType === 'video' ? '频'
: '内容';
const unitNum = (rule.unit?.match(/\d+/) || ['1'])[0];
return (
<div
key={rule.sourceType}
style={{
display: 'flex',
alignItems: 'baseline',
gap: '8px',
fontSize: '13px',
color: 'var(--text-secondary)',
lineHeight: '1.6',
}}
>
<span
style={{
fontWeight: 600,
color: 'var(--text-primary)',
whiteSpace: 'nowrap',
}}
>
{SOURCE_TYPE_LABELS[rule.sourceType] || rule.sourceType}
</span>
<span style={{ whiteSpace: 'pre-line' }}>
<div key={rule.sourceType} className="profile-pricing-detail-row">
<strong>{SOURCE_TYPE_LABELS[rule.sourceType] || rule.sourceType}</strong>
<span>
{contentType}{rule.unit} {rule.pointsPerUnit} {unitNum}{unitNum}
{rule.sourceType === 'video' ? '使用系统素材每个空镜额外消耗 2 积分。' : ''}
</span>
@@ -535,43 +368,13 @@ export default function Profile() {
</div>
)}
{/* 说明区域 */}
<div
style={{
marginTop: '16px',
padding: '16px 20px',
background: '#f8faf8',
borderRadius: '10px',
border: '1px solid #e8f0e8',
}}
>
<div
style={{
fontSize: '13px',
fontWeight: 600,
color: '#2e7d32',
marginBottom: '10px',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#2e7d32" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
{/* 说明 */}
<div className="profile-pricing-info-section">
<div className="profile-pricing-info-title">
<InfoIcon />
</div>
<ul
style={{
margin: 0,
paddingLeft: '18px',
fontSize: '13px',
color: 'var(--text-secondary)',
lineHeight: '1.8',
}}
>
<ul className="profile-pricing-info-list">
<li>使</li>
<li></li>
</ul>
+4
View File
@@ -15,6 +15,8 @@
--bg-card: #fff;
--bg-sidebar: rgb(255 255 255 / 72%);
--bg-input: #f3f4f6;
--bg-secondary: #f3f4f6;
--bg-tertiary: #eceef1;
--bg-hover: rgb(54 178 106 / 6%);
--bg-overlay: rgb(0 0 0 / 45%);
@@ -99,6 +101,8 @@
--bg-card: #25292e;
--bg-sidebar: rgb(37 41 46 / 82%);
--bg-input: #2f3338;
--bg-secondary: #2f3338;
--bg-tertiary: #374151;
--bg-hover: rgb(54 178 106 / 10%);
--bg-overlay: rgb(0 0 0 / 60%);
--text-primary: #f3f4f6;