fix: scrolling, responsive grid, analytics light theme, dashboard 2x3, sidebar spacing, skills top5

This commit is contained in:
qyyfw committed 2026-07-08 09:10:43 +08:00
1 parent c729f61831
commit 4d9691187a
5 files changed
+123 -80

No files matched your search

+18 -32
View File
@@ -1,7 +1,7 @@
/* ===== Admin Layout - Light Theme V5 ===== */
:root {
--admin-sidebar-width: 240px;
--admin-sidebar-width: 220px;
--sidebar-collapsed: 64px;
}
@@ -31,7 +31,7 @@
.admin-sidebar-header {
display: flex;
flex-direction: column;
padding: 14px 16px 10px;
padding: 12px 16px 8px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
flex-shrink: 0;
}
@@ -80,7 +80,7 @@
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 4px 8px;
padding: 2px 6px;
}
.sidebar-nav::-webkit-scrollbar {
@@ -98,14 +98,14 @@
/* Nav Group */
.nav-group {
margin-bottom: 0;
margin-bottom: 2px;
}
.nav-group-header {
display: flex;
align-items: center;
gap: 4px;
padding: 7px 10px;
padding: 6px 10px;
cursor: pointer;
border-radius: 6px;
transition: background 0.15s ease;
@@ -163,7 +163,7 @@
display: flex;
flex-direction: column;
gap: 1px;
padding: 2px 0 4px 0;
padding: 1px 0 3px 0;
overflow: hidden;
}
@@ -172,7 +172,7 @@
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px 7px 30px;
padding: 6px 10px 6px 28px;
border-radius: 6px;
color: #5A6480;
text-decoration: none;
@@ -882,30 +882,16 @@
}
/* ========== Responsive ========== */
@media (max-width: 1024px) {
.admin-content { padding: 16px 20px; }
.admin-stats { grid-template-columns: repeat(2, 1fr); }
.admin-stats-responsive { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
.admin-sidebar {
position: fixed;
left: -100%;
top: 0;
bottom: 0;
transition: left 0.3s ease;
}
.admin-sidebar.mobile-open {
left: 0;
}
.admin-content {
padding: 16px;
}
.admin-stats {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
.admin-modal {
min-width: auto;
width: 90vw;
max-width: 90vw;
}
.admin-sidebar { position: fixed; left: -100%; top: 0; bottom: 0; transition: left 0.3s ease; z-index: 1000; }
.admin-sidebar.mobile-open { left: 0; }
.admin-content { padding: 12px; }
.admin-stats { grid-template-columns: 1fr; }
.admin-stats-responsive { grid-template-columns: 1fr; }
.admin-modal { min-width: auto; width: 90vw; max-width: 90vw; }
}
+28 -28
View File
@@ -44,7 +44,7 @@ const Analytics: React.FC = () => {
useEffect(() => { fetchAll(); }, [fetchAll]);
const renderBar = (data: TrendItem[], key: string, color: string, label: string) => {
if (!data.length) return <div style={{ textAlign: 'center', padding: '40px', color: '#64748b' }}>暂无数据</div>;
if (!data.length) return <div style={{ textAlign: 'center', padding: '40px', color: '#6B7280' }}>暂无数据</div>;
const values = data.map((d: any) => d[key] || 0);
const max = Math.max(...values, 1);
return (
@@ -54,9 +54,9 @@ const Analytics: React.FC = () => {
const h = (v / max) * 140;
return (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: '40px' }}>
<div style={{ fontSize: '10px', color: '#94a3b8', marginBottom: '4px' }}>{v > 9999 ? `${(v/1000).toFixed(0)}k` : v}</div>
<div style={{ fontSize: '10px', color: '#8892B0', marginBottom: '4px' }}>{v > 9999 ? `${(v/1000).toFixed(0)}k` : v}</div>
<div style={{ width: '100%', height: `${h}px`, background: `linear-gradient(180deg, ${color}, ${color}aa)`, borderRadius: '4px 4px 0 0', transition: 'height 0.3s' }} />
<div style={{ fontSize: '10px', color: '#64748b', marginTop: '6px' }}>{d.date?.substring(5)}</div>
<div style={{ fontSize: '10px', color: '#6B7280', marginTop: '6px' }}>{d.date?.substring(5)}</div>
</div>
);
})}
@@ -65,7 +65,7 @@ const Analytics: React.FC = () => {
};
const renderLine = (data: TrendItem[], key: string, color: string) => {
if (!data.length) return <div style={{ textAlign: 'center', padding: '40px', color: '#64748b' }}>暂无数据</div>;
if (!data.length) return <div style={{ textAlign: 'center', padding: '40px', color: '#6B7280' }}>暂无数据</div>;
const values = data.map((d: any) => d[key] || 0);
const max = Math.max(...values, 1);
const w = 100 / (data.length - 1 || 1);
@@ -81,7 +81,7 @@ const Analytics: React.FC = () => {
};
const statCards = overview ? [
{ label: '总用户数', value: overview.total_users, color: '#6366f1', icon: 'U' },
{ label: '总用户数', value: overview.total_users, color: '#6C63FF', icon: 'U' },
{ label: '活跃用户', value: overview.active_users, color: '#10b981', icon: 'A' },
{ label: '新增用户', value: overview.new_users, color: '#3b82f6', icon: '+' },
{ label: 'API调用数', value: overview.api_calls, color: '#f59e0b', icon: 'API' },
@@ -92,90 +92,90 @@ const Analytics: React.FC = () => {
] : [];
return (
<div style={{ padding: '24px', color: '#e2e8f0' }}>
<div style={{ padding: '24px', color: '#1A1F36' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<h1 style={{ fontSize: '24px', fontWeight: 700 }}>数据分析面板</h1>
<div style={{ display: 'flex', gap: '8px' }}>
{[7, 14, 30, 90].map(d => (
<button key={d} onClick={() => setDays(d)} style={{ padding: '6px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
background: days === d ? '#6366f1' : '#1e293b', color: days === d ? '#fff' : '#94a3b8', fontWeight: 600, fontSize: '13px' }}>{d}天</button>
background: days === d ? '#6C63FF' : '#F0F2F8', color: days === d ? '#fff' : '#5A6480', fontWeight: 600, fontSize: '13px' }}>{d}天</button>
))}
</div>
</div>
{loading ? <div style={{ textAlign: 'center', padding: '80px', color: '#64748b' }}>加载中...</div> : (
{loading ? <div style={{ textAlign: 'center', padding: '80px', color: '#6B7280' }}>加载中...</div> : (
<>
{/* Stats grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '16px', marginBottom: '24px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '16px', marginBottom: '24px' }}>
{statCards.map((s, i) => (
<div key={i} style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', border: `1px solid ${s.color}33`, display: 'flex', alignItems: 'center', gap: '16px' }}>
<div key={i} style={{ background: '#FFFFFF', borderRadius: '12px', padding: '20px', border: `1px solid ${s.color}33`, display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '48px', height: '48px', borderRadius: '12px', background: `${s.color}22`, color: s.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '18px', fontWeight: 800 }}>{s.icon}</div>
<div>
<div style={{ fontSize: '24px', fontWeight: 800, color: s.color }}>{typeof s.value === 'number' ? s.value.toLocaleString() : s.value}</div>
<div style={{ fontSize: '12px', color: '#94a3b8', marginTop: '2px' }}>{s.label}</div>
<div style={{ fontSize: '12px', color: '#8892B0', marginTop: '2px' }}>{s.label}</div>
</div>
</div>
))}
</div>
{/* Charts grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))', gap: '16px', marginBottom: '16px' }}>
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px' }}>API调用趋势 ({days}天)</h3>
{renderBar(apiTrend, 'calls', '#f59e0b', '调用次数')}
</div>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px' }}>
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px' }}>Token使用趋势 ({days}天)</h3>
{renderLine(tokenTrend.daily, 'total_tokens', '#ec4899')}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))', gap: '16px', marginBottom: '16px' }}>
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px' }}>用户增长 ({days}天)</h3>
{renderBar(userTrend, 'new_users', '#3b82f6', '新增用户')}
</div>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px' }}>
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px' }}>收入趋势 ({days}天)</h3>
{renderBar(revenueTrend, 'revenue', '#10b981', '收入')}
</div>
</div>
{/* Model usage */}
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px' }}>
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '20px' }}>模型使用分析 ({days}天)</h3>
{modelUsage.length > 0 ? (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead><tr style={{ borderBottom: '2px solid #334155' }}>
<thead><tr style={{ borderBottom: '2px solid rgba(0,0,0,0.08)' }}>
{['模型', '调用次数', '总Tokens', '平均Tokens', '费用(元)'].map(h => (
<th key={h} style={{ padding: '10px 16px', textAlign: 'left', fontSize: '13px', color: '#94a3b8', fontWeight: 600 }}>{h}</th>
<th key={h} style={{ padding: '10px 16px', textAlign: 'left', fontSize: '13px', color: '#8892B0', fontWeight: 600 }}>{h}</th>
))}
</tr></thead>
<tbody>
{modelUsage.map((m, i) => (
<tr key={i} style={{ borderBottom: '1px solid #334155' }}>
<tr key={i} style={{ borderBottom: '1px solid rgba(0,0,0,0.08)' }}>
<td style={{ padding: '10px 16px', fontSize: '14px', fontWeight: 600 }}>{m.model_name}</td>
<td style={{ padding: '10px 16px', fontSize: '14px' }}>{m.call_count.toLocaleString()}</td>
<td style={{ padding: '10px 16px', fontSize: '14px', color: '#ec4899', fontWeight: 600 }}>{m.total_tokens.toLocaleString()}</td>
<td style={{ padding: '10px 16px', fontSize: '13px', color: '#94a3b8' }}>{m.avg_tokens.toLocaleString()}</td>
<td style={{ padding: '10px 16px', fontSize: '13px', color: '#8892B0' }}>{m.avg_tokens.toLocaleString()}</td>
<td style={{ padding: '10px 16px', fontSize: '14px', color: '#ef4444' }}>¥{m.total_cost.toFixed(4)}</td>
</tr>
))}
</tbody>
</table>
) : <div style={{ textAlign: 'center', padding: '40px', color: '#64748b' }}>暂无模型使用数据</div>}
) : <div style={{ textAlign: 'center', padding: '40px', color: '#6B7280' }}>暂无模型使用数据</div>}
</div>
{/* Model distribution pie (CSS) */}
{Object.keys(tokenTrend.model_distribution || {}).length > 0 && (
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px', marginTop: '16px' }}>
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px', marginTop: '16px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '20px' }}>模型使用分布</h3>
<div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>
<div style={{ display: 'flex', borderRadius: '50%', overflow: 'hidden', width: '160px', height: '160px' }}>
{(() => {
const entries = Object.entries(tokenTrend.model_distribution);
const total = entries.reduce((s, [, v]) => s + v, 1);
const colors = ['#6366f1', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444'];
const colors = ['#6C63FF', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444'];
return entries.map(([model, val], i) => {
const pct = (val / total) * 100;
return <div key={model} style={{ width: `${pct}%`, background: colors[i % colors.length] }} title={`${model}: ${pct.toFixed(1)}%`} />;
@@ -184,13 +184,13 @@ const Analytics: React.FC = () => {
</div>
<div style={{ flex: 1 }}>
{Object.entries(tokenTrend.model_distribution).map(([model, val], i) => {
const colors = ['#6366f1', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444'];
const colors = ['#6C63FF', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444'];
const total = Object.values(tokenTrend.model_distribution).reduce((s, v) => s + v, 1);
return (
<div key={model} style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<div style={{ width: '12px', height: '12px', borderRadius: '3px', background: colors[i % colors.length] }} />
<span style={{ fontSize: '13px', color: '#e2e8f0' }}>{model}</span>
<span style={{ fontSize: '13px', color: '#94a3b8', marginLeft: 'auto' }}>{((val / total) * 100).toFixed(1)}%</span>
<span style={{ fontSize: '13px', color: '#1A1F36' }}>{model}</span>
<span style={{ fontSize: '13px', color: '#8892B0', marginLeft: 'auto' }}>{((val / total) * 100).toFixed(1)}%</span>
</div>
);
})}
+3 -3
View File
@@ -200,7 +200,7 @@ const Dashboard: React.FC = () => {
<h1 className="admin-page-title">平台驾驶舱</h1>
{/* Stats Cards Row */}
<div className="admin-stats" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px', marginBottom: '24px' }}>
<div className="admin-stats" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px', marginBottom: '24px' }}>
{statCardDefs.map((s, i) => (
<div key={i} className="admin-stat-card" style={{
display: 'flex', alignItems: 'flex-start', gap: 'var(--space-lg)',
@@ -230,7 +230,7 @@ const Dashboard: React.FC = () => {
</div>
{/* Charts Row */}
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 'var(--space-xl)', marginBottom: 'var(--space-xl)' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))', gap: 'var(--space-xl)', marginBottom: 'var(--space-xl)' }}>
{/* Token Trend Chart */}
<div className="admin-card">
<div className="admin-card-header">
@@ -277,7 +277,7 @@ const Dashboard: React.FC = () => {
{/* Quick Actions */}
<div style={{ marginBottom: 'var(--space-xl)' }}>
<h3 style={{ fontSize: 15, fontWeight: 600, marginBottom: 12, color: '#1A1F36' }}>快捷入口</h3>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 'var(--space-md)' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: 'var(--space-md)' }}>
{quickEntries.map((entry) => (
<div
key={entry.path}
+48 -13
View File
@@ -20,6 +20,25 @@ const LandingPage: React.FC = () => {
const banners = mockAdBanners.filter(b => b.status === 'active');
const publishedApps = mockApps.filter(a => a.status === 'published');
const [appTypeFilter, setAppTypeFilter] = useState<'' | 'skill' | 'agent' | 'mcp'>('');
const [skillData, setSkillData] = useState<{skills: any[], agents: any[], mcps: any[]}>({skills: [], agents: [], mcps: []});
useEffect(() => {
(async () => {
try {
const apiModule = await import('@/api');
const res = await apiModule.adminAPI.getAdminSkills({page: 1, page_size: 50, status: 'published'});
const allApps: any[] = (res as any)?.data?.items || (res as any)?.data || [];
const sortFn = (a: any, b: any) => (b.usage_count || b.install_count || 0) - (a.usage_count || a.install_count || 0);
const skills = allApps.filter(a => a.type === 'skill').sort(sortFn).slice(0, 5);
const agents = allApps.filter(a => a.type === 'agent').sort(sortFn).slice(0, 5);
const mcps = allApps.filter(a => a.type === 'mcp').sort(sortFn).slice(0, 5);
if (skills.length + agents.length + mcps.length === 0) {
setSkillData({skills: publishedApps.filter(a => a.type === 'skill').slice(0, 5), agents: publishedApps.filter(a => a.type === 'agent').slice(0, 5), mcps: publishedApps.filter(a => a.type === 'mcp').slice(0, 5)});
} else { setSkillData({skills, agents, mcps}); }
} catch (e) {
setSkillData({skills: publishedApps.filter(a => a.type === 'skill').slice(0, 5), agents: publishedApps.filter(a => a.type === 'agent').slice(0, 5), mcps: publishedApps.filter(a => a.type === 'mcp').slice(0, 5)});
}
})();
}, []);
const [toastMsg, setToastMsg] = useState('');
const [scrolled, setScrolled] = useState(false);
@@ -373,27 +392,43 @@ const LandingPage: React.FC = () => {
</div>
</div>
<div className="market-grid-v2">
{filteredApps.slice(0, 6).map(app => (
<div key={app.id} className="market-card-v2" onClick={() => handleAppClick(app.name)}>
<div className="market-card-icon-v2" style={{ background: getGradient(app) }}>
{app.name.charAt(0)}
{(() => {
const hasRealData = skillData.skills.length + skillData.agents.length + skillData.mcps.length > 0;
let displayApps: any[];
if (appTypeFilter) {
const key = appTypeFilter === 'skill' ? 'skills' : appTypeFilter === 'agent' ? 'agents' : 'mcps';
displayApps = hasRealData ? skillData[key] : filteredApps.slice(0, 5);
} else {
displayApps = hasRealData ? [...skillData.skills, ...skillData.agents, ...skillData.mcps] : filteredApps.slice(0, 6);
}
return displayApps.map((app: any) => {
const appType = app.type || 'skill';
const appColor = typeColors[appType] || '#6C63FF';
const appLabel = typeLabels[appType] || 'Skill';
const appName = app.name || app.title || '未命名';
const appDesc = app.description || app.desc || '';
const appUsage = app.usage_count || app.install_count || 0;
const appPrice = app.price || 0;
return (
<div key={app.id || appName} className="market-card-v2" onClick={() => handleAppClick(appName)}>
<div className="market-card-icon-v2" style={{ background: hasRealData ? `linear-gradient(135deg, ${appColor}44, ${appColor}11)` : getGradient(app) }}>
<span style={{ color: appColor, fontWeight: 700, fontSize: 16 }}>{appName.charAt(0)}</span>
</div>
<div className="market-card-body-v2">
<div className="market-card-header-v2">
<h4>{app.name}</h4>
<span className="type-badge" style={{ background: typeColors[app.type], color: '#fff' }}>
{typeLabels[app.type]}
</span>
<h4>{appName}</h4>
<span className="type-badge" style={{ background: appColor, color: '#fff' }}>{appLabel}</span>
</div>
<p className="market-card-desc-v2">{app.description}</p>
<p className="market-card-desc-v2">{appDesc}</p>
<div className="market-card-meta-v2">
<span>⭐ 4.8</span>
<span>📥 1.2k</span>
<span>{app.price === 0 ? '免费' : `¥${app.price}`}</span>
{appUsage > 0 && <span>📥 {appUsage >= 1000 ? `${(appUsage/1000).toFixed(1)}k` : appUsage}次</span>}
<span>{appPrice === 0 ? '免费' : `¥${appPrice}`}</span>
</div>
</div>
</div>
))}
);
});
})()}
</div>
</section>
+26 -4
View File
@@ -123,13 +123,11 @@ body {
color: var(--color-text-primary);
background: var(--color-bg-primary);
line-height: 1.6;
overflow: hidden;
height: 100vh;
min-height: 100vh;
}
#root {
height: 100vh;
overflow: hidden;
min-height: 100vh;
}
a {
@@ -745,3 +743,27 @@ a:hover {
border-color: rgba(108, 99, 255, 0.2) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08) !important;
}
/* ===== Responsive Grid Patterns ===== */
.responsive-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
@media (max-width: 768px) { .responsive-grid-2 { grid-template-columns: 1fr; } }
.responsive-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
@media (max-width: 1024px) { .responsive-grid-3 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 768px) { .responsive-grid-3 { grid-template-columns: 1fr; } }
.responsive-grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
@media (max-width: 1200px) { .responsive-grid-4 { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 900px) { .responsive-grid-4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .responsive-grid-4 { grid-template-columns: 1fr; } }
.responsive-grid-auto { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
@media (max-width: 600px) { .responsive-grid-auto { grid-template-columns: 1fr; } }
.responsive-flex { display: flex; flex-wrap: wrap; gap: 16px; }
.responsive-flex > * { flex: 1 1 280px; min-width: 0; }
.pagination-bar { display: flex; align-items: center; justify-content: center; gap: 4px; margin-top: 20px; padding: 8px 0; }
.pagination-bar button { min-width: 32px; height: 32px; border-radius: 6px; border: 1px solid rgba(0,0,0,0.10); background: #FFFFFF; color: #5A6480; font-size: 13px; cursor: pointer; transition: all 0.15s ease; padding: 0 8px; }
.pagination-bar button:hover { background: #F0F2F8; color: #1A1F36; }
.pagination-bar button.active { background: #6C63FF; border-color: #6C63FF; color: #FFFFFF; }
.pagination-bar button:disabled { opacity: 0.4; cursor: not-allowed; }
.pagination-bar .page-info { font-size: 12px; color: #8892B0; margin: 0 8px; }
.admin-stats-responsive { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
@media (max-width: 900px) { .admin-stats-responsive { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .admin-stats-responsive { grid-template-columns: 1fr; } }