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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user