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
+130 -87

No files matched your search

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