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
+2744 -2701

No files matched your search

File diff suppressed because it is too large. Load diff
+207 -207
View File
@@ -1,207 +1,207 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { adminAPI } from '@/api'; import { adminAPI } from '@/api';
interface Overview { interface Overview {
total_users: number; active_users: number; new_users: number; total_users: number; active_users: number; new_users: number;
api_calls: number; avg_response_time: number; error_rate: number; api_calls: number; avg_response_time: number; error_rate: number;
tokens_used: number; total_cost: number; total_chats: number; total_messages: number; revenue: number; tokens_used: number; total_cost: number; total_chats: number; total_messages: number; revenue: number;
} }
interface TrendItem { date: string; calls?: number; errors?: number; tokens?: number; new_users?: number; cumulative?: number; revenue?: number; consumption?: number; } interface TrendItem { date: string; calls?: number; errors?: number; tokens?: number; new_users?: number; cumulative?: number; revenue?: number; consumption?: number; }
interface ModelUsage { model_name: string; call_count: number; total_tokens: number; total_cost: number; avg_tokens: number; } interface ModelUsage { model_name: string; call_count: number; total_tokens: number; total_cost: number; avg_tokens: number; }
const Analytics: React.FC = () => { const Analytics: React.FC = () => {
const [days, setDays] = useState(7); const [days, setDays] = useState(7);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [overview, setOverview] = useState<Overview | null>(null); const [overview, setOverview] = useState<Overview | null>(null);
const [apiTrend, setApiTrend] = useState<TrendItem[]>([]); const [apiTrend, setApiTrend] = useState<TrendItem[]>([]);
const [tokenTrend, setTokenTrend] = useState<{ daily: any[]; model_distribution: Record<string, number> }>({ daily: [], model_distribution: {} }); const [tokenTrend, setTokenTrend] = useState<{ daily: any[]; model_distribution: Record<string, number> }>({ daily: [], model_distribution: {} });
const [userTrend, setUserTrend] = useState<TrendItem[]>([]); const [userTrend, setUserTrend] = useState<TrendItem[]>([]);
const [revenueTrend, setRevenueTrend] = useState<TrendItem[]>([]); const [revenueTrend, setRevenueTrend] = useState<TrendItem[]>([]);
const [modelUsage, setModelUsage] = useState<ModelUsage[]>([]); const [modelUsage, setModelUsage] = useState<ModelUsage[]>([]);
const fetchAll = useCallback(async () => { const fetchAll = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const [ov, api, tok, usr, rev, mu] = await Promise.all([ const [ov, api, tok, usr, rev, mu] = await Promise.all([
adminAPI.getAnalyticsOverview(days), adminAPI.getAnalyticsOverview(days),
adminAPI.getAnalyticsApiTrends(days), adminAPI.getAnalyticsApiTrends(days),
adminAPI.getAnalyticsTokenTrends(days), adminAPI.getAnalyticsTokenTrends(days),
adminAPI.getAnalyticsUserTrends(days), adminAPI.getAnalyticsUserTrends(days),
adminAPI.getAnalyticsRevenueTrends(days), adminAPI.getAnalyticsRevenueTrends(days),
adminAPI.getAnalyticsModelUsage(days), adminAPI.getAnalyticsModelUsage(days),
]); ]);
setOverview((ov as any)?.data || ov); setOverview((ov as any)?.data || ov);
setApiTrend((api as any)?.data || []); setApiTrend((api as any)?.data || []);
const td = (tok as any)?.data || { daily: [], model_distribution: {} }; const td = (tok as any)?.data || { daily: [], model_distribution: {} };
setTokenTrend(td); setTokenTrend(td);
setUserTrend((usr as any)?.data || []); setUserTrend((usr as any)?.data || []);
setRevenueTrend((rev as any)?.data || []); setRevenueTrend((rev as any)?.data || []);
setModelUsage((mu as any)?.data || []); setModelUsage((mu as any)?.data || []);
} catch (e) { console.error('fetchAnalytics:', e); } } catch (e) { console.error('fetchAnalytics:', e); }
setLoading(false); setLoading(false);
}, [days]); }, [days]);
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 (
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '8px', height: '180px', padding: '0 16px' }}> <div style={{ display: 'flex', alignItems: 'flex-end', gap: '8px', height: '180px', padding: '0 16px' }}>
{data.map((d: any, i) => { {data.map((d: any, i) => {
const v = d[key] || 0; const v = d[key] || 0;
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>
); );
})} })}
</div> </div>
); );
}; };
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);
const points = data.map((d: any, i) => `${i * w},${100 - ((d[key] || 0) / max) * 90}`).join(' '); const points = data.map((d: any, i) => `${i * w},${100 - ((d[key] || 0) / max) * 90}`).join(' ');
return ( return (
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ width: '100%', height: '180px' }}> <svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ width: '100%', height: '180px' }}>
<polyline points={points} fill="none" stroke={color} strokeWidth="1.5" /> <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" />
{data.map((d: any, i) => ( {data.map((d: any, i) => (
<circle key={i} cx={i * w} cy={100 - ((d[key] || 0) / max) * 90} r="1" fill={color} /> <circle key={i} cx={i * w} cy={100 - ((d[key] || 0) / max) * 90} r="1" fill={color} />
))} ))}
</svg> </svg>
); );
}; };
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' },
{ label: 'Token用量', value: overview.tokens_used, color: '#ec4899', icon: 'T' }, { label: 'Token用量', value: overview.tokens_used, color: '#ec4899', icon: 'T' },
{ label: '总费用(元)', value: overview.total_cost?.toFixed(2), color: '#ef4444', icon: '¥' }, { label: '总费用(元)', value: overview.total_cost?.toFixed(2), color: '#ef4444', icon: '¥' },
{ label: '对话总数', value: overview.total_chats, color: '#8b5cf6', icon: 'C' }, { label: '对话总数', value: overview.total_chats, color: '#8b5cf6', icon: 'C' },
{ label: '消息总数', value: overview.total_messages, color: '#06b6d4', icon: 'M' }, { label: '消息总数', value: overview.total_messages, color: '#06b6d4', icon: 'M' },
] : []; ] : [];
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)}%`} />;
}); });
})()} })()}
</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>
); );
})} })}
</div> </div>
</div> </div>
</div> </div>
)} )}
</> </>
)} )}
</div> </div>
); );
}; };
export default Analytics; export default Analytics;
+385 -385
View File
@@ -1,385 +1,385 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { adminAPI } from '@/api'; import { adminAPI } from '@/api';
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 20 }) => ( const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 20 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"> <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
{children} {children}
</svg> </svg>
); );
interface DashboardStats { interface DashboardStats {
total_users: number; total_users: number;
active_users: number; active_users: number;
total_models: number; total_models: number;
enabled_models: number; enabled_models: number;
total_conversations: number; total_conversations: number;
active_conversations: number; active_conversations: number;
total_knowledge_bases: number; total_knowledge_bases: number;
total_documents: number; total_documents: number;
today_orders: number; today_orders: number;
today_tokens: number; today_tokens: number;
week_tokens: number; week_tokens: number;
total_tokens: number; total_tokens: number;
today_conversations: number; today_conversations: number;
server_time: string; server_time: string;
} }
interface DailyStat { interface DailyStat {
date: string; date: string;
tokens: number; tokens: number;
} }
const quickEntries = [ const quickEntries = [
{ path: '/admin/models', label: '大模型库', desc: '管理AI模型与供应商', icon: <SVGIcon><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></SVGIcon> }, { path: '/admin/models', label: '大模型库', desc: '管理AI模型与供应商', icon: <SVGIcon><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></SVGIcon> },
{ path: '/admin/settings', label: '网站设置', desc: '品牌、邮件、短信配置', icon: <SVGIcon><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-2.83 2.83l-.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-4 0v-.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-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.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 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.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 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></SVGIcon> }, { path: '/admin/settings', label: '网站设置', desc: '品牌、邮件、短信配置', icon: <SVGIcon><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-2.83 2.83l-.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-4 0v-.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-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.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 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.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 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></SVGIcon> },
{ path: '/admin/payment', label: '支付配置', desc: '微信、支付宝渠道配置', icon: <SVGIcon><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></SVGIcon> }, { path: '/admin/payment', label: '支付配置', desc: '微信、支付宝渠道配置', icon: <SVGIcon><rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/></SVGIcon> },
{ path: '/admin/services', label: '增值服务', desc: '服务管理与定制订单', icon: <SVGIcon><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></SVGIcon> }, { path: '/admin/services', label: '增值服务', desc: '服务管理与定制订单', icon: <SVGIcon><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></SVGIcon> },
{ path: '/admin/membership', label: '会员体系', desc: '订阅计划与自动化规则', icon: <SVGIcon><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></SVGIcon> }, { path: '/admin/membership', label: '会员体系', desc: '订阅计划与自动化规则', icon: <SVGIcon><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></SVGIcon> },
{ path: '/admin/market', label: '应用市场', desc: '应用审核与优惠券管理', icon: <SVGIcon><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></SVGIcon> }, { path: '/admin/market', label: '应用市场', desc: '应用审核与优惠券管理', icon: <SVGIcon><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></SVGIcon> },
{ path: '/admin/logs', label: '日志风控', desc: '操作/调用/付费日志与风控', icon: <SVGIcon><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"/></SVGIcon> }, { path: '/admin/logs', label: '日志风控', desc: '操作/调用/付费日志与风控', icon: <SVGIcon><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"/></SVGIcon> },
]; ];
// SVG Bar Chart Component // SVG Bar Chart Component
const BarChart: React.FC<{ data: DailyStat[] }> = ({ data }) => { const BarChart: React.FC<{ data: DailyStat[] }> = ({ data }) => {
if (!data || data.length === 0) return <div style={{ color: '#6B7280', textAlign: 'center', padding: 40 }}>暂无数据</div>; if (!data || data.length === 0) return <div style={{ color: '#6B7280', textAlign: 'center', padding: 40 }}>暂无数据</div>;
const maxVal = Math.max(...data.map(d => d.tokens), 1); const maxVal = Math.max(...data.map(d => d.tokens), 1);
const w = 600, h = 200, padLeft = 55, padRight = 10, padTop = 10, padBottom = 25; const w = 600, h = 200, padLeft = 55, padRight = 10, padTop = 10, padBottom = 25;
const barAreaW = w - padLeft - padRight; const barAreaW = w - padLeft - padRight;
const barAreaH = h - padTop - padBottom; const barAreaH = h - padTop - padBottom;
const barGap = 2; const barGap = 2;
const barW = Math.max(2, (barAreaW / data.length) - barGap); const barW = Math.max(2, (barAreaW / data.length) - barGap);
// Y axis ticks // Y axis ticks
const yTicks = [0, Math.round(maxVal * 0.25), Math.round(maxVal * 0.5), Math.round(maxVal * 0.75), maxVal]; const yTicks = [0, Math.round(maxVal * 0.25), Math.round(maxVal * 0.5), Math.round(maxVal * 0.75), maxVal];
const formatK = (n: number) => n >= 10000 ? (n / 1000).toFixed(0) + 'k' : String(n); const formatK = (n: number) => n >= 10000 ? (n / 1000).toFixed(0) + 'k' : String(n);
const formatDate = (d: string) => { const formatDate = (d: string) => {
const parts = d.split('-'); const parts = d.split('-');
return parts.length >= 2 ? `${parts[1]}/${parts[2]}` : d; return parts.length >= 2 ? `${parts[1]}/${parts[2]}` : d;
}; };
return ( return (
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} style={{ width: '100%', height: 'auto', maxWidth: 600 }}> <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} style={{ width: '100%', height: 'auto', maxWidth: 600 }}>
{/* Grid lines */} {/* Grid lines */}
{yTicks.map((tick, i) => { {yTicks.map((tick, i) => {
const y = padTop + barAreaH - (tick / maxVal) * barAreaH; const y = padTop + barAreaH - (tick / maxVal) * barAreaH;
return ( return (
<g key={i}> <g key={i}>
<line x1={padLeft} y1={y} x2={w - padRight} y2={y} stroke="rgba(0,0,0,0.06)" strokeWidth="0.5" /> <line x1={padLeft} y1={y} x2={w - padRight} y2={y} stroke="rgba(0,0,0,0.06)" strokeWidth="0.5" />
<text x={padLeft - 8} y={y + 4} textAnchor="end" fontSize="10" fill="#6B7280">{formatK(tick)}</text> <text x={padLeft - 8} y={y + 4} textAnchor="end" fontSize="10" fill="#6B7280">{formatK(tick)}</text>
</g> </g>
); );
})} })}
{/* Bars */} {/* Bars */}
{data.map((d, i) => { {data.map((d, i) => {
const barH = (d.tokens / maxVal) * barAreaH; const barH = (d.tokens / maxVal) * barAreaH;
const x = padLeft + i * (barAreaW / data.length); const x = padLeft + i * (barAreaW / data.length);
const y = padTop + barAreaH - barH; const y = padTop + barAreaH - barH;
return ( return (
<g key={i}> <g key={i}>
<rect x={x + barGap / 2} y={y} width={barW} height={Math.max(barH, 1)} rx="2" fill="#6C63FF" opacity="0.85"> <rect x={x + barGap / 2} y={y} width={barW} height={Math.max(barH, 1)} rx="2" fill="#6C63FF" opacity="0.85">
<title>{`${d.date}: ${d.tokens.toLocaleString()} tokens`}</title> <title>{`${d.date}: ${d.tokens.toLocaleString()} tokens`}</title>
</rect> </rect>
{/* X label - every 7th bar */} {/* X label - every 7th bar */}
{i % Math.max(1, Math.floor(data.length / 8)) === 0 && ( {i % Math.max(1, Math.floor(data.length / 8)) === 0 && (
<text x={x + barW / 2} y={h - 6} textAnchor="middle" fontSize="9" fill="#6B7280">{formatDate(d.date)}</text> <text x={x + barW / 2} y={h - 6} textAnchor="middle" fontSize="9" fill="#6B7280">{formatDate(d.date)}</text>
)} )}
</g> </g>
); );
})} })}
</svg> </svg>
); );
}; };
// Mini Donut for stat cards // Mini Donut for stat cards
const MiniDonut: React.FC<{ value: number; max: number; color: string }> = ({ value, max, color }) => { const MiniDonut: React.FC<{ value: number; max: number; color: string }> = ({ value, max, color }) => {
const pct = max > 0 ? Math.min(value / max, 1) : 0; const pct = max > 0 ? Math.min(value / max, 1) : 0;
const r = 16, c = 20, circumference = 2 * Math.PI * r; const r = 16, c = 20, circumference = 2 * Math.PI * r;
const dash = circumference * pct; const dash = circumference * pct;
return ( return (
<svg width={c * 2} height={c * 2} viewBox={`0 0 ${c * 2} ${c * 2}`}> <svg width={c * 2} height={c * 2} viewBox={`0 0 ${c * 2} ${c * 2}`}>
<circle cx={c} cy={c} r={r} fill="none" stroke="rgba(0,0,0,0.06)" strokeWidth="3" /> <circle cx={c} cy={c} r={r} fill="none" stroke="rgba(0,0,0,0.06)" strokeWidth="3" />
<circle cx={c} cy={c} r={r} fill="none" stroke={color} strokeWidth="3" <circle cx={c} cy={c} r={r} fill="none" stroke={color} strokeWidth="3"
strokeDasharray={`${dash} ${circumference - dash}`} strokeLinecap="round" strokeDasharray={`${dash} ${circumference - dash}`} strokeLinecap="round"
transform={`rotate(-90 ${c} ${c})`} style={{ transition: 'stroke-dasharray 0.6s ease' }} /> transform={`rotate(-90 ${c} ${c})`} style={{ transition: 'stroke-dasharray 0.6s ease' }} />
<text x={c} y={c + 4} textAnchor="middle" fontSize="9" fontWeight="600" fill={color}> <text x={c} y={c + 4} textAnchor="middle" fontSize="9" fontWeight="600" fill={color}>
{Math.round(pct * 100)}% {Math.round(pct * 100)}%
</text> </text>
</svg> </svg>
); );
}; };
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [stats, setStats] = useState<DashboardStats>({ const [stats, setStats] = useState<DashboardStats>({
total_users: 0, active_users: 0, total_models: 0, enabled_models: 0, total_users: 0, active_users: 0, total_models: 0, enabled_models: 0,
total_conversations: 0, active_conversations: 0, total_knowledge_bases: 0, total_documents: 0, total_conversations: 0, active_conversations: 0, total_knowledge_bases: 0, total_documents: 0,
today_orders: 0, today_tokens: 0, week_tokens: 0, total_tokens: 0, today_orders: 0, today_tokens: 0, week_tokens: 0, total_tokens: 0,
today_conversations: 0, server_time: '', today_conversations: 0, server_time: '',
}); });
const [dailyStats, setDailyStats] = useState<DailyStat[]>([]); const [dailyStats, setDailyStats] = useState<DailyStat[]>([]);
const [operationLogs, setOperationLogs] = useState<any[]>([]); const [operationLogs, setOperationLogs] = useState<any[]>([]);
const [auditLogs, setAuditLogs] = useState<any[]>([]); const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
setLoading(true); setLoading(true);
try { try {
const [statsRes, dailyRes, logsRes, auditRes] = await Promise.all([ const [statsRes, dailyRes, logsRes, auditRes] = await Promise.all([
adminAPI.getStats() as any, adminAPI.getStats() as any,
(adminAPI as any).getDailyStats?.(30) || Promise.resolve({ data: [] }), (adminAPI as any).getDailyStats?.(30) || Promise.resolve({ data: [] }),
adminAPI.getRecentLogs() as any, adminAPI.getRecentLogs() as any,
adminAPI.getRecentAudit() as any, adminAPI.getRecentAudit() as any,
]); ]);
// Stats: backend returns raw dict (no {code,data} wrapper) — use .data or the response itself // Stats: backend returns raw dict (no {code,data} wrapper) — use .data or the response itself
const statsData = statsRes?.data || statsRes; const statsData = statsRes?.data || statsRes;
if (statsData && typeof statsData === 'object') setStats(statsData); if (statsData && typeof statsData === 'object') setStats(statsData);
// Daily stats: backend returns {code, data: {days, data: [...]}} — access nested .data.data // Daily stats: backend returns {code, data: {days, data: [...]}} — access nested .data.data
const dailyData = dailyRes?.data?.data || (Array.isArray(dailyRes?.data) ? dailyRes.data : []) || []; const dailyData = dailyRes?.data?.data || (Array.isArray(dailyRes?.data) ? dailyRes.data : []) || [];
setDailyStats(Array.isArray(dailyData) ? dailyData : []); setDailyStats(Array.isArray(dailyData) ? dailyData : []);
// Recent logs: backend returns raw list (no wrapper) — use the response directly if it's an array // Recent logs: backend returns raw list (no wrapper) — use the response directly if it's an array
const logsData = Array.isArray(logsRes) ? logsRes : (Array.isArray(logsRes?.data) ? logsRes.data : []); const logsData = Array.isArray(logsRes) ? logsRes : (Array.isArray(logsRes?.data) ? logsRes.data : []);
setOperationLogs(logsData); setOperationLogs(logsData);
// Recent audit: same as logs // Recent audit: same as logs
const auditData = Array.isArray(auditRes) ? auditRes : (Array.isArray(auditRes?.data) ? auditRes.data : []); const auditData = Array.isArray(auditRes) ? auditRes : (Array.isArray(auditRes?.data) ? auditRes.data : []);
setAuditLogs(auditData); setAuditLogs(auditData);
} catch (err) { } catch (err) {
console.error('Dashboard数据加载失败:', err); console.error('Dashboard数据加载失败:', err);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
fetchData(); fetchData();
}, []); }, []);
const formatDate = (d: string) => { const formatDate = (d: string) => {
if (!d) return '-'; if (!d) return '-';
const dt = new Date(d); const dt = new Date(d);
if (isNaN(dt.getTime())) return d.slice(0, 19) || '-'; if (isNaN(dt.getTime())) return d.slice(0, 19) || '-';
return dt.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); return dt.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}; };
const formatTokens = (n: number): string => { const formatTokens = (n: number): string => {
if (n >= 100000000) return (n / 100000000).toFixed(2) + '亿'; if (n >= 100000000) return (n / 100000000).toFixed(2) + '亿';
if (n >= 10000) return (n / 10000).toFixed(1) + '万'; if (n >= 10000) return (n / 10000).toFixed(1) + '万';
return n.toLocaleString('en-US'); return n.toLocaleString('en-US');
}; };
const statCardDefs = [ const statCardDefs = [
{ label: '总用户', value: stats.total_users, sub: `活跃 ${stats.active_users}`, color: '#6C63FF', bg: 'rgba(108,99,255,0.12)', subBg: 'rgba(108,99,255,0.08)', { label: '总用户', value: stats.total_users, sub: `活跃 ${stats.active_users}`, color: '#6C63FF', bg: 'rgba(108,99,255,0.12)', subBg: 'rgba(108,99,255,0.08)',
icon: <SVGIcon size={22}><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></SVGIcon> }, icon: <SVGIcon size={22}><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></SVGIcon> },
{ label: 'AI模型', value: stats.total_models, sub: `启用 ${stats.enabled_models}`, color: '#219B5E', bg: 'rgba(33,155,94,0.12)', subBg: 'rgba(33,155,94,0.08)', { label: 'AI模型', value: stats.total_models, sub: `启用 ${stats.enabled_models}`, color: '#219B5E', bg: 'rgba(33,155,94,0.12)', subBg: 'rgba(33,155,94,0.08)',
icon: <SVGIcon size={22}><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></SVGIcon> }, icon: <SVGIcon size={22}><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></SVGIcon> },
{ label: '对话数', value: stats.total_conversations, sub: `活跃 ${stats.active_conversations}`, color: '#E8922C', bg: 'rgba(232,146,44,0.12)', subBg: 'rgba(232,146,44,0.08)', { label: '对话数', value: stats.total_conversations, sub: `活跃 ${stats.active_conversations}`, color: '#E8922C', bg: 'rgba(232,146,44,0.12)', subBg: 'rgba(232,146,44,0.08)',
icon: <SVGIcon size={22}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></SVGIcon> }, icon: <SVGIcon size={22}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></SVGIcon> },
{ label: '知识库', value: stats.total_knowledge_bases, sub: `文档 ${stats.total_documents}`, color: '#3B82F6', bg: 'rgba(59,130,246,0.12)', subBg: 'rgba(59,130,246,0.08)', { label: '知识库', value: stats.total_knowledge_bases, sub: `文档 ${stats.total_documents}`, color: '#3B82F6', bg: 'rgba(59,130,246,0.12)', subBg: 'rgba(59,130,246,0.08)',
icon: <SVGIcon size={22}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></SVGIcon> }, icon: <SVGIcon size={22}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></SVGIcon> },
{ label: '今日对话', value: stats.today_conversations, sub: `订单 ${stats.today_orders}`, color: '#8B5CF6', bg: 'rgba(139,92,246,0.12)', subBg: 'rgba(139,92,246,0.08)', { label: '今日对话', value: stats.today_conversations, sub: `订单 ${stats.today_orders}`, color: '#8B5CF6', bg: 'rgba(139,92,246,0.12)', subBg: 'rgba(139,92,246,0.08)',
icon: <SVGIcon size={22}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon> }, icon: <SVGIcon size={22}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon> },
{ label: '今日Token', value: stats.today_tokens, sub: `本周 ${formatTokens(stats.week_tokens)}`, color: '#EC4899', bg: 'rgba(236,72,153,0.12)', subBg: 'rgba(236,72,153,0.08)', format: true, { label: '今日Token', value: stats.today_tokens, sub: `本周 ${formatTokens(stats.week_tokens)}`, color: '#EC4899', bg: 'rgba(236,72,153,0.12)', subBg: 'rgba(236,72,153,0.08)', format: true,
icon: <SVGIcon size={22}><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></SVGIcon> }, icon: <SVGIcon size={22}><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></SVGIcon> },
]; ];
if (loading) { if (loading) {
return ( return (
<div style={{ animation: 'fadeIn var(--transition-normal)', padding: 'var(--space-2xl)', textAlign: 'center' }}> <div style={{ animation: 'fadeIn var(--transition-normal)', padding: 'var(--space-2xl)', textAlign: 'center' }}>
<div className="audit-spinner" /> <div className="audit-spinner" />
<span style={{ marginLeft: 'var(--space-md)', color: 'var(--color-text-tertiary)' }}>加载仪表盘数据...</span> <span style={{ marginLeft: 'var(--space-md)', color: 'var(--color-text-tertiary)' }}>加载仪表盘数据...</span>
</div> </div>
); );
} }
return ( return (
<div style={{ animation: 'fadeIn var(--transition-normal)' }}> <div style={{ animation: 'fadeIn var(--transition-normal)' }}>
<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)',
padding: 'var(--space-lg) var(--space-xl)', padding: 'var(--space-lg) var(--space-xl)',
}}> }}>
<div style={{ <div style={{
width: 48, height: 48, borderRadius: 14, width: 48, height: 48, borderRadius: 14,
background: s.bg, color: s.color, background: s.bg, color: s.color,
display: 'flex', alignItems: 'center', justifyContent: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center',
flexShrink: 0, flexShrink: 0,
}}> }}>
{s.icon} {s.icon}
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div className="admin-stat-label">{s.label}</div> <div className="admin-stat-label">{s.label}</div>
<div className="admin-stat-value" style={{ fontSize: 'var(--text-xl)' }}> <div className="admin-stat-value" style={{ fontSize: 'var(--text-xl)' }}>
{s.format ? formatTokens(s.value as number) : (s.value as number).toLocaleString()} {s.format ? formatTokens(s.value as number) : (s.value as number).toLocaleString()}
</div> </div>
<div style={{ <div style={{
fontSize: 11, color: s.color, marginTop: 2, fontSize: 11, color: s.color, marginTop: 2,
background: s.subBg || s.bg, padding: '1px 6px', borderRadius: 4, background: s.subBg || s.bg, padding: '1px 6px', borderRadius: 4,
display: 'inline-block', display: 'inline-block',
}}>{s.sub}</div> }}>{s.sub}</div>
</div> </div>
</div> </div>
))} ))}
</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">
<h3 className="admin-card-title"> <h3 className="admin-card-title">
<SVGIcon size={18}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon> <SVGIcon size={18}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon>
{' '}Token消耗趋势(近30天) {' '}Token消耗趋势(近30天)
</h3> </h3>
</div> </div>
<div style={{ padding: 'var(--space-lg)', display: 'flex', justifyContent: 'center' }}> <div style={{ padding: 'var(--space-lg)', display: 'flex', justifyContent: 'center' }}>
<BarChart data={dailyStats} /> <BarChart data={dailyStats} />
</div> </div>
</div> </div>
{/* System Health */} {/* System Health */}
<div className="admin-card"> <div className="admin-card">
<div className="admin-card-header"> <div className="admin-card-header">
<h3 className="admin-card-title"> <h3 className="admin-card-title">
<SVGIcon size={18}><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></SVGIcon> <SVGIcon size={18}><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></SVGIcon>
{' '}平台概览 {' '}平台概览
</h3> </h3>
</div> </div>
<div style={{ padding: 'var(--space-lg)' }}> <div style={{ padding: 'var(--space-lg)' }}>
{[ {[
{ label: '累计Token消耗', value: formatTokens(stats.total_tokens), color: '#6C63FF', pct: 100 }, { label: '累计Token消耗', value: formatTokens(stats.total_tokens), color: '#6C63FF', pct: 100 },
{ label: '模型利用率', value: `${stats.enabled_models}/${stats.total_models}`, color: '#219B5E', pct: stats.total_models > 0 ? stats.enabled_models / stats.total_models : 0 }, { label: '模型利用率', value: `${stats.enabled_models}/${stats.total_models}`, color: '#219B5E', pct: stats.total_models > 0 ? stats.enabled_models / stats.total_models : 0 },
{ label: '用户活跃率', value: `${stats.active_users}/${stats.total_users}`, color: '#E8922C', pct: stats.total_users > 0 ? stats.active_users / stats.total_users : 0 }, { label: '用户活跃率', value: `${stats.active_users}/${stats.total_users}`, color: '#E8922C', pct: stats.total_users > 0 ? stats.active_users / stats.total_users : 0 },
{ label: '对话活跃率', value: `${stats.active_conversations}/${stats.total_conversations}`, color: '#3B82F6', pct: stats.total_conversations > 0 ? stats.active_conversations / stats.total_conversations : 0 }, { label: '对话活跃率', value: `${stats.active_conversations}/${stats.total_conversations}`, color: '#3B82F6', pct: stats.total_conversations > 0 ? stats.active_conversations / stats.total_conversations : 0 },
].map((item, i) => ( ].map((item, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 0', borderBottom: i < 3 ? '1px solid rgba(0,0,0,0.06)' : 'none' }}> <div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 0', borderBottom: i < 3 ? '1px solid rgba(0,0,0,0.06)' : 'none' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<MiniDonut value={item.pct * 100} max={100} color={item.color} /> <MiniDonut value={item.pct * 100} max={100} color={item.color} />
<span style={{ fontSize: 13, color: '#1A1F36' }}>{item.label}</span> <span style={{ fontSize: 13, color: '#1A1F36' }}>{item.label}</span>
</div> </div>
<span style={{ fontSize: 15, fontWeight: 700, color: item.color, fontFamily: 'var(--font-mono)' }}>{item.value}</span> <span style={{ fontSize: 15, fontWeight: 700, color: item.color, fontFamily: 'var(--font-mono)' }}>{item.value}</span>
</div> </div>
))} ))}
<div style={{ marginTop: 12, fontSize: 11, color: '#6B7280', textAlign: 'center' }}> <div style={{ marginTop: 12, fontSize: 11, color: '#6B7280', textAlign: 'center' }}>
服务器时间: {stats.server_time ? new Date(stats.server_time).toLocaleString('zh-CN') : '-'} 服务器时间: {stats.server_time ? new Date(stats.server_time).toLocaleString('zh-CN') : '-'}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* 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}
onClick={() => navigate(entry.path)} onClick={() => navigate(entry.path)}
style={{ style={{
background: '#FFFFFF', borderRadius: 'var(--radius-lg)', border: '1px solid rgba(0,0,0,0.08)', background: '#FFFFFF', borderRadius: 'var(--radius-lg)', border: '1px solid rgba(0,0,0,0.08)',
padding: 'var(--space-md) var(--space-lg)', cursor: 'pointer', transition: 'all 0.2s', padding: 'var(--space-md) var(--space-lg)', cursor: 'pointer', transition: 'all 0.2s',
display: 'flex', alignItems: 'center', gap: 'var(--space-md)', display: 'flex', alignItems: 'center', gap: 'var(--space-md)',
boxShadow: '0 1px 3px rgba(0,0,0,0.04)', boxShadow: '0 1px 3px rgba(0,0,0,0.04)',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#6C63FF'; e.currentTarget.style.borderColor = '#6C63FF';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(108,99,255,0.15)'; e.currentTarget.style.boxShadow = '0 2px 8px rgba(108,99,255,0.15)';
e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.transform = 'translateY(-1px)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'var(--color-border)'; e.currentTarget.style.borderColor = 'var(--color-border)';
e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.boxShadow = 'none';
e.currentTarget.style.transform = 'none'; e.currentTarget.style.transform = 'none';
}} }}
> >
<div style={{ color: '#6C63FF', flexShrink: 0 }}>{entry.icon}</div> <div style={{ color: '#6C63FF', flexShrink: 0 }}>{entry.icon}</div>
<div> <div>
<div style={{ fontSize: 13, fontWeight: 600, color: '#1A1F36' }}>{entry.label}</div> <div style={{ fontSize: 13, fontWeight: 600, color: '#1A1F36' }}>{entry.label}</div>
<div style={{ fontSize: 11, color: '#6B7280' }}>{entry.desc}</div> <div style={{ fontSize: 11, color: '#6B7280' }}>{entry.desc}</div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
{/* Recent Logs */} {/* Recent Logs */}
<div className="admin-card" style={{ marginBottom: 'var(--space-xl)' }}> <div className="admin-card" style={{ marginBottom: 'var(--space-xl)' }}>
<div className="admin-card-header"> <div className="admin-card-header">
<h3 className="admin-card-title">最近操作日志</h3> <h3 className="admin-card-title">最近操作日志</h3>
<span style={{ fontSize: 12, color: '#6B7280' }}>最近10条</span> <span style={{ fontSize: 12, color: '#6B7280' }}>最近10条</span>
</div> </div>
<div style={{ overflowX: 'auto' }}> <div style={{ overflowX: 'auto' }}>
<table className="admin-table"> <table className="admin-table">
<thead> <thead>
<tr> <tr>
<th>用户</th> <th>用户</th>
<th>操作</th> <th>操作</th>
<th>目标</th> <th>目标</th>
<th>详情</th> <th>详情</th>
<th>IP</th> <th>IP</th>
<th>时间</th> <th>时间</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{operationLogs.length > 0 ? operationLogs.map(log => ( {operationLogs.length > 0 ? operationLogs.map(log => (
<tr key={log.id}> <tr key={log.id}>
<td style={{ fontWeight: 500 }}>{log.username}</td> <td style={{ fontWeight: 500 }}>{log.username}</td>
<td><span className="admin-badge badge-info">{log.action}</span></td> <td><span className="admin-badge badge-info">{log.action}</span></td>
<td>{log.target}</td> <td>{log.target}</td>
<td style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{log.detail}</td> <td style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{log.detail}</td>
<td style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{log.ip}</td> <td style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{log.ip}</td>
<td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{formatDate(log.created_at)}</td> <td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{formatDate(log.created_at)}</td>
</tr> </tr>
)) : ( )) : (
<tr><td colSpan={6} style={{ textAlign: 'center', color: 'var(--color-text-tertiary)', padding: 'var(--space-lg)' }}>暂无操作日志</td></tr> <tr><td colSpan={6} style={{ textAlign: 'center', color: 'var(--color-text-tertiary)', padding: 'var(--space-lg)' }}>暂无操作日志</td></tr>
)} )}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
{/* Recent Audit Logs */} {/* Recent Audit Logs */}
<div className="admin-card"> <div className="admin-card">
<div className="admin-card-header"> <div className="admin-card-header">
<h3 className="admin-card-title">最近审计日志</h3> <h3 className="admin-card-title">最近审计日志</h3>
<span style={{ fontSize: 12, color: '#6B7280' }}>最近10条</span> <span style={{ fontSize: 12, color: '#6B7280' }}>最近10条</span>
</div> </div>
<div style={{ overflowX: 'auto' }}> <div style={{ overflowX: 'auto' }}>
<table className="admin-table"> <table className="admin-table">
<thead> <thead>
<tr> <tr>
<th>操作人</th> <th>操作人</th>
<th>操作类型</th> <th>操作类型</th>
<th>目标类型</th> <th>目标类型</th>
<th>详情</th> <th>详情</th>
<th>时间</th> <th>时间</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{auditLogs.length > 0 ? auditLogs.map(log => ( {auditLogs.length > 0 ? auditLogs.map(log => (
<tr key={log.id}> <tr key={log.id}>
<td style={{ fontWeight: 500 }}>{log.admin_name}</td> <td style={{ fontWeight: 500 }}>{log.admin_name}</td>
<td><span className="admin-badge badge-warning">{log.action}</span></td> <td><span className="admin-badge badge-warning">{log.action}</span></td>
<td><span className="admin-badge badge neutral">{log.target_type}</span></td> <td><span className="admin-badge badge neutral">{log.target_type}</span></td>
<td>{log.detail}</td> <td>{log.detail}</td>
<td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{formatDate(log.created_at)}</td> <td style={{ fontSize: 'var(--text-xs)', color: 'var(--color-text-tertiary)' }}>{formatDate(log.created_at)}</td>
</tr> </tr>
)) : ( )) : (
<tr><td colSpan={5} style={{ textAlign: 'center', color: 'var(--color-text-tertiary)', padding: 'var(--space-lg)' }}>暂无审计日志</td></tr> <tr><td colSpan={5} style={{ textAlign: 'center', color: 'var(--color-text-tertiary)', padding: 'var(--space-lg)' }}>暂无审计日志</td></tr>
)} )}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
); );
}; };
export default Dashboard; export default Dashboard;
+486 -451
View File
@@ -1,451 +1,486 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { mockAdBanners, mockApps } from '@/mock/data'; import { mockAdBanners, mockApps } from '@/mock/data';
import type { AdBanner, MarketApp } from '@/types'; import type { AdBanner, MarketApp } from '@/types';
import './LandingPage.css'; import './LandingPage.css';
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => ( const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{children} {children}
</svg> </svg>
); );
const typeLabels: Record<string, string> = { skill: 'Skill', agent: 'Agent', mcp: 'MCP' }; const typeLabels: Record<string, string> = { skill: 'Skill', agent: 'Agent', mcp: 'MCP' };
const typeColors: Record<string, string> = { skill: '#10B981', agent: '#5B5FED', mcp: '#F59E0B' }; const typeColors: Record<string, string> = { skill: '#10B981', agent: '#5B5FED', mcp: '#F59E0B' };
const LandingPage: React.FC = () => { const LandingPage: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [currentSlide, setCurrentSlide] = useState(0); const [currentSlide, setCurrentSlide] = useState(0);
const [isPaused, setIsPaused] = useState(false); const [isPaused, setIsPaused] = useState(false);
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 [toastMsg, setToastMsg] = useState(''); const [skillData, setSkillData] = useState<{skills: any[], agents: any[], mcps: any[]}>({skills: [], agents: [], mcps: []});
const [scrolled, setScrolled] = useState(false); useEffect(() => {
(async () => {
const filteredApps = appTypeFilter try {
? publishedApps.filter(a => a.type === appTypeFilter) const apiModule = await import('@/api');
: publishedApps; 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 || [];
useEffect(() => { const sortFn = (a: any, b: any) => (b.usage_count || b.install_count || 0) - (a.usage_count || a.install_count || 0);
const handleScroll = () => setScrolled(window.scrollY > 60); const skills = allApps.filter(a => a.type === 'skill').sort(sortFn).slice(0, 5);
window.addEventListener('scroll', handleScroll); const agents = allApps.filter(a => a.type === 'agent').sort(sortFn).slice(0, 5);
return () => window.removeEventListener('scroll', handleScroll); 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)});
const showToast = (msg: string) => { } else { setSkillData({skills, agents, mcps}); }
setToastMsg(msg); } catch (e) {
setTimeout(() => setToastMsg(''), 2000); 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 handleAppClick = (appName: string) => { }, []);
showToast(`"${appName}" 需登录后使用,即将跳转登录页`); const [toastMsg, setToastMsg] = useState('');
setTimeout(() => navigate('/login'), 800); const [scrolled, setScrolled] = useState(false);
};
const filteredApps = appTypeFilter
const nextSlide = useCallback(() => { ? publishedApps.filter(a => a.type === appTypeFilter)
if (banners.length === 0) return; : publishedApps;
setCurrentSlide(prev => (prev + 1) % banners.length);
}, [banners.length]); useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 60);
useEffect(() => { window.addEventListener('scroll', handleScroll);
if (banners.length <= 1 || isPaused) return; return () => window.removeEventListener('scroll', handleScroll);
const timer = setInterval(nextSlide, 4000); }, []);
return () => clearInterval(timer);
}, [banners.length, isPaused, nextSlide]); const showToast = (msg: string) => {
setToastMsg(msg);
const goToSlide = (idx: number) => setCurrentSlide(idx); setTimeout(() => setToastMsg(''), 2000);
};
const getGradient = (app: MarketApp): string => {
const gradients: Record<string, string> = { const handleAppClick = (appName: string) => {
a1: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', showToast(`"${appName}" 需登录后使用,即将跳转登录页`);
a2: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', setTimeout(() => navigate('/login'), 800);
a3: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', };
a4: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
a5: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', const nextSlide = useCallback(() => {
a6: 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)', if (banners.length === 0) return;
a8: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)', setCurrentSlide(prev => (prev + 1) % banners.length);
}; }, [banners.length]);
return gradients[app.id] || 'linear-gradient(135deg, #5B5FED 0%, #A78BFA 100%)';
}; useEffect(() => {
if (banners.length <= 1 || isPaused) return;
// Platform Architecture - layers from bottom to top const timer = setInterval(nextSlide, 4000);
const archLayers = [ return () => clearInterval(timer);
{ }, [banners.length, isPaused, nextSlide]);
label: '应用层',
color: '#6C63FF', const goToSlide = (idx: number) => setCurrentSlide(idx);
items: ['AI 对话工作台', '知识库问答', '自动化流程', 'IM Bot 接入', 'API 集成'],
}, const getGradient = (app: MarketApp): string => {
{ const gradients: Record<string, string> = {
label: '能力层', a1: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: '#10B981', a2: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
items: ['技能市场 (Skill)', '智能体 (Agent)', 'MCP 协议', 'RAG 检索增强', '工具链编排'], a3: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
}, a4: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
{ a5: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
label: '模型层', a6: 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)',
color: '#F59E0B', a8: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
items: ['DeepSeek', 'GPT-4o', 'Claude', 'Qwen', '多模态模型'], };
}, return gradients[app.id] || 'linear-gradient(135deg, #5B5FED 0%, #A78BFA 100%)';
{ };
label: '基础设施',
color: '#3B82F6', // Platform Architecture - layers from bottom to top
items: ['向量数据库', '对象存储', '任务调度', 'API 网关', '监控告警'], const archLayers = [
}, {
]; label: '应用层',
color: '#6C63FF',
// Capability groups items: ['AI 对话工作台', '知识库问答', '自动化流程', 'IM Bot 接入', 'API 集成'],
const capabilities = [ },
{ {
icon: <SVGIcon size={28}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></SVGIcon>, label: '能力层',
title: 'AI 对话', color: '#10B981',
desc: '接入 50+ 主流大模型,支持流式输出、多轮对话、Function Calling,你的专属 AI 助手', items: ['技能市场 (Skill)', '智能体 (Agent)', 'MCP 协议', 'RAG 检索增强', '工具链编排'],
}, },
{ {
icon: <SVGIcon size={28}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></SVGIcon>, label: '模型层',
title: '知识库', color: '#F59E0B',
desc: '上传文档、网页、代码构建个人知识库,RAG 增强检索让 AI 回答更精准可靠', items: ['DeepSeek', 'GPT-4o', 'Claude', 'Qwen', '多模态模型'],
}, },
{ {
icon: <SVGIcon size={28}><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></SVGIcon>, label: '基础设施',
title: '技能市场', color: '#3B82F6',
desc: '100+ 预制 AI 技能,一键安装即用。支持 Skill、Agent、MCP 三种形态,持续扩展', items: ['向量数据库', '对象存储', '任务调度', 'API 网关', '监控告警'],
}, },
{ ];
icon: <SVGIcon size={28}><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></SVGIcon>,
title: 'IM Bot', // Capability groups
desc: '将 AI 能力接入飞书、微信、钉钉,在聊天中调用 AI,无缝融入日常工作流', const capabilities = [
}, {
{ icon: <SVGIcon size={28}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></SVGIcon>,
icon: <SVGIcon size={28}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon>, title: 'AI 对话',
title: 'AI 自进化', desc: '接入 50+ 主流大模型,支持流式输出、多轮对话、Function Calling,你的专属 AI 助手',
desc: '从每次对话中持续学习,自动沉淀知识图谱,你的 AI 越用越懂你', },
}, {
{ icon: <SVGIcon size={28}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></SVGIcon>,
icon: <SVGIcon size={28}><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></SVGIcon>, title: '知识库',
title: 'Token 驾驶舱', desc: '上传文档、网页、代码构建个人知识库,RAG 增强检索让 AI 回答更精准可靠',
desc: '实时监控调用量、Token 消耗与成本,精细化管理你的 AI 使用预算', },
}, {
]; icon: <SVGIcon size={28}><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></SVGIcon>,
title: '技能市场',
// Scenarios - individual practitioner focused desc: '100+ 预制 AI 技能,一键安装即用。支持 Skill、Agent、MCP 三种形态,持续扩展',
const scenarios = [ },
{ icon: '📚', title: '学习助手', desc: '构建课程知识库,AI 辅助理解复杂概念、整理笔记、解答疑问' }, {
{ icon: '💼', title: '工作效率', desc: '自动化重复任务、智能撰写文档邮件、快速检索信息,效率翻倍' }, icon: <SVGIcon size={28}><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></SVGIcon>,
{ icon: '🤖', title: 'AI Agent 开发', desc: '用 Skill + Agent + MCP 搭建专属 AI 助手,从原型到生产一站完成' }, title: 'IM Bot',
{ icon: '📊', title: '数据分析', desc: '自然语言查询数据库,自动生成可视化报告,让数据说话更简单' }, desc: '将 AI 能力接入飞书、微信、钉钉,在聊天中调用 AI,无缝融入日常工作流',
{ icon: '✍️', title: '内容创作', desc: 'AI 辅助写文章、做翻译、润色文案,让创作更有灵感' }, },
{ icon: '🔬', title: '技术探索', desc: '接入最新模型做实验,对比不同 LLM 效果,探索 AI 的前沿能力' }, {
]; icon: <SVGIcon size={28}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon>,
title: 'AI 自进化',
return ( desc: '从每次对话中持续学习,自动沉淀知识图谱,你的 AI 越用越懂你',
<div className="landing-page-v2"> },
{/* Toast */} {
{toastMsg && <div className="landing-toast-v2">{toastMsg}</div>} icon: <SVGIcon size={28}><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></SVGIcon>,
title: 'Token 驾驶舱',
{/* Header */} desc: '实时监控调用量、Token 消耗与成本,精细化管理你的 AI 使用预算',
<header className={`landing-header-v2 ${scrolled ? 'scrolled' : ''}`}> },
<div className="landing-header-inner-v2"> ];
<div className="landing-brand-v2">
<svg width="36" height="36" viewBox="0 0 32 32" fill="none"> // Scenarios - individual practitioner focused
<defs> const scenarios = [
<linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%"> { icon: '📚', title: '学习助手', desc: '构建课程知识库,AI 辅助理解复杂概念、整理笔记、解答疑问' },
<stop offset="0%" stopColor="#5B5FED"/> { icon: '💼', title: '工作效率', desc: '自动化重复任务、智能撰写文档邮件、快速检索信息,效率翻倍' },
<stop offset="100%" stopColor="#A78BFA"/> { icon: '🤖', title: 'AI Agent 开发', desc: '用 Skill + Agent + MCP 搭建专属 AI 助手,从原型到生产一站完成' },
</linearGradient> { icon: '📊', title: '数据分析', desc: '自然语言查询数据库,自动生成可视化报告,让数据说话更简单' },
</defs> { icon: '✍️', title: '内容创作', desc: 'AI 辅助写文章、做翻译、润色文案,让创作更有灵感' },
<rect width="32" height="32" rx="8" fill="url(#logoGrad)"/> { icon: '🔬', title: '技术探索', desc: '接入最新模型做实验,对比不同 LLM 效果,探索 AI 的前沿能力' },
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/> ];
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
</svg> return (
<span className="landing-brand-text">七虎AI</span> <div className="landing-page-v2">
</div> {/* Toast */}
<nav className="landing-nav-v2" style={{ cursor: 'pointer' }}> {toastMsg && <div className="landing-toast-v2">{toastMsg}</div>}
<a onClick={() => document.getElementById('what-is')?.scrollIntoView({ behavior: 'smooth' })}>平台介绍</a>
<a onClick={() => document.getElementById('architecture')?.scrollIntoView({ behavior: 'smooth' })}>平台架构</a> {/* Header */}
<a onClick={() => document.getElementById('capabilities')?.scrollIntoView({ behavior: 'smooth' })}>能力体系</a> <header className={`landing-header-v2 ${scrolled ? 'scrolled' : ''}`}>
<a onClick={() => document.getElementById('scenarios')?.scrollIntoView({ behavior: 'smooth' })}>应用场景</a> <div className="landing-header-inner-v2">
<a onClick={() => document.getElementById('market')?.scrollIntoView({ behavior: 'smooth' })}>技能生态</a> <div className="landing-brand-v2">
</nav> <svg width="36" height="36" viewBox="0 0 32 32" fill="none">
<div className="landing-actions-v2"> <defs>
<button className="btn-ghost-v2" onClick={() => navigate('/login')}>登录</button> <linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<button className="btn-primary-v2" onClick={() => navigate('/login')}>免费注册</button> <stop offset="0%" stopColor="#5B5FED"/>
</div> <stop offset="100%" stopColor="#A78BFA"/>
</div> </linearGradient>
</header> </defs>
<rect width="32" height="32" rx="8" fill="url(#logoGrad)"/>
{/* Hero Section — What is 七虎AI */} <path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
<section className="hero-section-v2"> <path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
<div className="hero-bg-orb orb-1" /> </svg>
<div className="hero-bg-orb orb-2" /> <span className="landing-brand-text">七虎AI</span>
<div className="hero-bg-orb orb-3" /> </div>
<div className="hero-content-v2"> <nav className="landing-nav-v2" style={{ cursor: 'pointer' }}>
<div className="hero-badge-v2"> <a onClick={() => document.getElementById('what-is')?.scrollIntoView({ behavior: 'smooth' })}>平台介绍</a>
<span className="hero-badge-dot" /> <a onClick={() => document.getElementById('architecture')?.scrollIntoView({ behavior: 'smooth' })}>平台架构</a>
AI 个人工作台 · 为创造者而生 <a onClick={() => document.getElementById('capabilities')?.scrollIntoView({ behavior: 'smooth' })}>能力体系</a>
</div> <a onClick={() => document.getElementById('scenarios')?.scrollIntoView({ behavior: 'smooth' })}>应用场景</a>
<h1 className="hero-title-v2"> <a onClick={() => document.getElementById('market')?.scrollIntoView({ behavior: 'smooth' })}>技能生态</a>
每一个 AI 创造者 </nav>
<span className="hero-gradient-text"> 的超级工作台</span> <div className="landing-actions-v2">
</h1> <button className="btn-ghost-v2" onClick={() => navigate('/login')}>登录</button>
<p className="hero-subtitle-v2"> <button className="btn-primary-v2" onClick={() => navigate('/login')}>免费注册</button>
七虎AI 是一站式 AI 能力集成平台,让你在一个地方完成对话、知识库、技能编排、模型切换、自动化搭建<br/> </div>
无需拼接多个工具,从想法到落地,一个平台就够了 </div>
</p> </header>
<div className="hero-cta-v2">
<button className="btn-hero-primary" onClick={() => navigate('/login')}> {/* Hero Section — What is 七虎AI */}
免费开始使用 <section className="hero-section-v2">
<SVGIcon size={18}><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></SVGIcon> <div className="hero-bg-orb orb-1" />
</button> <div className="hero-bg-orb orb-2" />
<button className="btn-hero-secondary" onClick={() => document.getElementById('what-is')?.scrollIntoView({ behavior: 'smooth' })}> <div className="hero-bg-orb orb-3" />
了解平台 <div className="hero-content-v2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M6 9l6 6 6-6"/></svg> <div className="hero-badge-v2">
</button> <span className="hero-badge-dot" />
</div> AI 个人工作台 · 为创造者而生
{/* Stats */} </div>
<div className="hero-stats-v2"> <h1 className="hero-title-v2">
<div className="hero-stat-item"> 每一个 AI 创造者
<span className="hero-stat-num">100+</span> <span className="hero-gradient-text"> 的超级工作台</span>
<span className="hero-stat-label">AI 技能</span> </h1>
</div> <p className="hero-subtitle-v2">
<div className="hero-stat-divider" /> 七虎AI 是一站式 AI 能力集成平台,让你在一个地方完成对话、知识库、技能编排、模型切换、自动化搭建<br/>
<div className="hero-stat-item"> 无需拼接多个工具,从想法到落地,一个平台就够了
<span className="hero-stat-num">50+</span> </p>
<span className="hero-stat-label">大模型接入</span> <div className="hero-cta-v2">
</div> <button className="btn-hero-primary" onClick={() => navigate('/login')}>
<div className="hero-stat-divider" /> 免费开始使用
<div className="hero-stat-item"> <SVGIcon size={18}><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></SVGIcon>
<span className="hero-stat-num">6</span> </button>
<span className="hero-stat-label">IM 平台</span> <button className="btn-hero-secondary" onClick={() => document.getElementById('what-is')?.scrollIntoView({ behavior: 'smooth' })}>
</div> 了解平台
<div className="hero-stat-divider" /> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M6 9l6 6 6-6"/></svg>
<div className="hero-stat-item"> </button>
<span className="hero-stat-num">99.9%</span> </div>
<span className="hero-stat-label">可用性</span> {/* Stats */}
</div> <div className="hero-stats-v2">
</div> <div className="hero-stat-item">
</div> <span className="hero-stat-num">100+</span>
</section> <span className="hero-stat-label">AI 技能</span>
</div>
{/* What is 七虎AI — Platform Introduction */} <div className="hero-stat-divider" />
<section id="what-is" className="whatis-section-v2"> <div className="hero-stat-item">
<div className="section-header-v2"> <span className="hero-stat-num">50+</span>
<span className="section-badge-v2">平台介绍</span> <span className="hero-stat-label">大模型接入</span>
<h2>七虎AI 是什么?</h2> </div>
<p>一个面向 AI 爱好者和从业者的个人 AI 能力集成平台</p> <div className="hero-stat-divider" />
</div> <div className="hero-stat-item">
<div className="whatis-cards-v2"> <span className="hero-stat-num">6</span>
<div className="whatis-card"> <span className="hero-stat-label">IM 平台</span>
<div className="whatis-card-icon">🎯</div> </div>
<h3>能力聚合</h3> <div className="hero-stat-divider" />
<p>将大模型对话、知识库、技能编排、自动化、IM Bot 等能力整合在一个平台,告别工具碎片化</p> <div className="hero-stat-item">
</div> <span className="hero-stat-num">99.9%</span>
<div className="whatis-card"> <span className="hero-stat-label">可用性</span>
<div className="whatis-card-icon">⚡</div> </div>
<h3>开箱即用</h3> </div>
<p>预设 100+ AI 技能模板,50+ 大模型即配即用,无需从零搭建,注册即可上手</p> </div>
</div> </section>
<div className="whatis-card">
<div className="whatis-card-icon">🔌</div> {/* What is 七虎AI — Platform Introduction */}
<h3>开放扩展</h3> <section id="what-is" className="whatis-section-v2">
<p>支持 MCP 协议、Skill 插件、Agent 编排、API 开放,你可以自由扩展和定制</p> <div className="section-header-v2">
</div> <span className="section-badge-v2">平台介绍</span>
<div className="whatis-card"> <h2>七虎AI 是什么?</h2>
<div className="whatis-card-icon">🚀</div> <p>一个面向 AI 爱好者和从业者的个人 AI 能力集成平台</p>
<h3>持续进化</h3> </div>
<p>AI 自进化引擎从使用中持续学习,知识库自动更新,模型热切换,你的工作台越来越强</p> <div className="whatis-cards-v2">
</div> <div className="whatis-card">
</div> <div className="whatis-card-icon">🎯</div>
</section> <h3>能力聚合</h3>
<p>将大模型对话、知识库、技能编排、自动化、IM Bot 等能力整合在一个平台,告别工具碎片化</p>
{/* Platform Architecture */} </div>
<section id="architecture" className="architecture-section-v2"> <div className="whatis-card">
<div className="section-header-v2"> <div className="whatis-card-icon">⚡</div>
<span className="section-badge-v2">平台架构</span> <h3>开箱即用</h3>
<h2>四层架构 · 层层赋能</h2> <p>预设 100+ AI 技能模板,50+ 大模型即配即用,无需从零搭建,注册即可上手</p>
<p>从基础设施到上层应用,理解 七虎AI 的技术全貌</p> </div>
</div> <div className="whatis-card">
<div className="arch-layers-v2"> <div className="whatis-card-icon">🔌</div>
{archLayers.map((layer, idx) => ( <h3>开放扩展</h3>
<div key={idx} className="arch-layer" style={{ '--layer-color': layer.color } as React.CSSProperties}> <p>支持 MCP 协议、Skill 插件、Agent 编排、API 开放,你可以自由扩展和定制</p>
<div className="arch-layer-header"> </div>
<div className="arch-layer-dot" style={{ background: layer.color }} /> <div className="whatis-card">
<span className="arch-layer-label">{layer.label}</span> <div className="whatis-card-icon">🚀</div>
<div className="arch-layer-line" /> <h3>持续进化</h3>
</div> <p>AI 自进化引擎从使用中持续学习,知识库自动更新,模型热切换,你的工作台越来越强</p>
<div className="arch-layer-items"> </div>
{layer.items.map((item, i) => ( </div>
<span key={i} className="arch-layer-chip">{item}</span> </section>
))}
</div> {/* Platform Architecture */}
</div> <section id="architecture" className="architecture-section-v2">
))} <div className="section-header-v2">
{/* Downward arrows between layers */} <span className="section-badge-v2">平台架构</span>
</div> <h2>四层架构 · 层层赋能</h2>
</section> <p>从基础设施到上层应用,理解 七虎AI 的技术全貌</p>
</div>
{/* Banner Carousel */} <div className="arch-layers-v2">
{banners.length > 0 && ( {archLayers.map((layer, idx) => (
<section className="banner-section-v2"> <div key={idx} className="arch-layer" style={{ '--layer-color': layer.color } as React.CSSProperties}>
<div <div className="arch-layer-header">
className="banner-carousel-v2" <div className="arch-layer-dot" style={{ background: layer.color }} />
onMouseEnter={() => setIsPaused(true)} <span className="arch-layer-label">{layer.label}</span>
onMouseLeave={() => setIsPaused(false)} <div className="arch-layer-line" />
> </div>
<div className="banner-track-v2" style={{ transform: `translateX(-${currentSlide * 100}%)` }}> <div className="arch-layer-items">
{banners.map((banner, idx) => ( {layer.items.map((item, i) => (
<div key={idx} className="banner-slide-v2"> <span key={i} className="arch-layer-chip">{item}</span>
<div className="banner-content-v2"> ))}
<span className="banner-tag">AI 精选</span> </div>
<h2>{banner.title}</h2> </div>
<p>{banner.title}</p> ))}
</div> {/* Downward arrows between layers */}
<div className="banner-visual-v2"> </div>
<div className="banner-shape" /> </section>
</div>
</div> {/* Banner Carousel */}
))} {banners.length > 0 && (
</div> <section className="banner-section-v2">
<div className="banner-dots-v2"> <div
{banners.map((_, idx) => ( className="banner-carousel-v2"
<button onMouseEnter={() => setIsPaused(true)}
key={idx} onMouseLeave={() => setIsPaused(false)}
className={`banner-dot ${idx === currentSlide ? 'active' : ''}`} >
onClick={() => goToSlide(idx)} <div className="banner-track-v2" style={{ transform: `translateX(-${currentSlide * 100}%)` }}>
/> {banners.map((banner, idx) => (
))} <div key={idx} className="banner-slide-v2">
</div> <div className="banner-content-v2">
</div> <span className="banner-tag">AI 精选</span>
</section> <h2>{banner.title}</h2>
)} <p>{banner.title}</p>
</div>
{/* Capabilities Section */} <div className="banner-visual-v2">
<section id="capabilities" className="capabilities-section-v2"> <div className="banner-shape" />
<div className="section-header-v2"> </div>
<span className="section-badge-v2">能力体系</span> </div>
<h2>六大核心能力</h2> ))}
<p>从对话到自动化,覆盖 AI 从业者的全部工作流</p> </div>
</div> <div className="banner-dots-v2">
<div className="capabilities-grid-v2"> {banners.map((_, idx) => (
{capabilities.map((c, idx) => ( <button
<div key={idx} className="capability-card-v2"> key={idx}
<div className="capability-icon-v2">{c.icon}</div> className={`banner-dot ${idx === currentSlide ? 'active' : ''}`}
<h3>{c.title}</h3> onClick={() => goToSlide(idx)}
<p>{c.desc}</p> />
</div> ))}
))} </div>
</div> </div>
</section> </section>
)}
{/* Scenarios Section */}
<section id="scenarios" className="scenarios-section-v2"> {/* Capabilities Section */}
<div className="section-header-v2"> <section id="capabilities" className="capabilities-section-v2">
<span className="section-badge-v2">应用场景</span> <div className="section-header-v2">
<h2>每个 AI 创作者都用得上</h2> <span className="section-badge-v2">能力体系</span>
<p>无论你是学习者、开发者、还是内容创作者,都能找到属于你的打开方式</p> <h2>六大核心能力</h2>
</div> <p>从对话到自动化,覆盖 AI 从业者的全部工作流</p>
<div className="scenarios-grid-v2"> </div>
{scenarios.map((s, idx) => ( <div className="capabilities-grid-v2">
<div key={idx} className="scenario-card-v2"> {capabilities.map((c, idx) => (
<div className="scenario-emoji">{s.icon}</div> <div key={idx} className="capability-card-v2">
<h3>{s.title}</h3> <div className="capability-icon-v2">{c.icon}</div>
<p>{s.desc}</p> <h3>{c.title}</h3>
</div> <p>{c.desc}</p>
))} </div>
</div> ))}
</section> </div>
</section>
{/* Market Preview */}
<section id="market" className="market-section-v2"> {/* Scenarios Section */}
<div className="section-header-v2"> <section id="scenarios" className="scenarios-section-v2">
<span className="section-badge-v2">技能生态</span> <div className="section-header-v2">
<h2>丰富的 AI 技能生态</h2> <span className="section-badge-v2">应用场景</span>
<p>Skills、Agent、MCP 工具一键安装,持续扩展你的 AI 能力边界</p> <h2>每个 AI 创作者都用得上</h2>
<div className="market-filter-v2"> <p>无论你是学习者、开发者、还是内容创作者,都能找到属于你的打开方式</p>
{(['', 'skill', 'agent', 'mcp'] as const).map(t => ( </div>
<button <div className="scenarios-grid-v2">
key={t || 'all'} {scenarios.map((s, idx) => (
className={`filter-chip ${appTypeFilter === t ? 'active' : ''}`} <div key={idx} className="scenario-card-v2">
onClick={() => setAppTypeFilter(t)} <div className="scenario-emoji">{s.icon}</div>
> <h3>{s.title}</h3>
{t === '' ? '全部' : typeLabels[t]} <p>{s.desc}</p>
</button> </div>
))} ))}
</div> </div>
</div> </section>
<div className="market-grid-v2">
{filteredApps.slice(0, 6).map(app => ( {/* Market Preview */}
<div key={app.id} className="market-card-v2" onClick={() => handleAppClick(app.name)}> <section id="market" className="market-section-v2">
<div className="market-card-icon-v2" style={{ background: getGradient(app) }}> <div className="section-header-v2">
{app.name.charAt(0)} <span className="section-badge-v2">技能生态</span>
</div> <h2>丰富的 AI 技能生态</h2>
<div className="market-card-body-v2"> <p>Skills、Agent、MCP 工具一键安装,持续扩展你的 AI 能力边界</p>
<div className="market-card-header-v2"> <div className="market-filter-v2">
<h4>{app.name}</h4> {(['', 'skill', 'agent', 'mcp'] as const).map(t => (
<span className="type-badge" style={{ background: typeColors[app.type], color: '#fff' }}> <button
{typeLabels[app.type]} key={t || 'all'}
</span> className={`filter-chip ${appTypeFilter === t ? 'active' : ''}`}
</div> onClick={() => setAppTypeFilter(t)}
<p className="market-card-desc-v2">{app.description}</p> >
<div className="market-card-meta-v2"> {t === '' ? '全部' : typeLabels[t]}
<span>⭐ 4.8</span> </button>
<span>📥 1.2k</span> ))}
<span>{app.price === 0 ? '免费' : `¥${app.price}`}</span> </div>
</div> </div>
</div> <div className="market-grid-v2">
</div> {(() => {
))} const hasRealData = skillData.skills.length + skillData.agents.length + skillData.mcps.length > 0;
</div> let displayApps: any[];
</section> if (appTypeFilter) {
const key = appTypeFilter === 'skill' ? 'skills' : appTypeFilter === 'agent' ? 'agents' : 'mcps';
{/* CTA */} displayApps = hasRealData ? skillData[key] : filteredApps.slice(0, 5);
<section className="cta-section-v2"> } else {
<div className="cta-bg-glow" /> displayApps = hasRealData ? [...skillData.skills, ...skillData.agents, ...skillData.mcps] : filteredApps.slice(0, 6);
<h2>准备好开始你的 AI 之旅了吗?</h2> }
<p>免费注册,即刻拥有属于你自己的 AI 超级工作台</p> return displayApps.map((app: any) => {
<button className="btn-hero-primary" onClick={() => navigate('/login')}> const appType = app.type || 'skill';
免费注册 const appColor = typeColors[appType] || '#6C63FF';
<SVGIcon size={18}><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></SVGIcon> const appLabel = typeLabels[appType] || 'Skill';
</button> const appName = app.name || app.title || '未命名';
</section> const appDesc = app.description || app.desc || '';
const appUsage = app.usage_count || app.install_count || 0;
{/* Footer */} const appPrice = app.price || 0;
<footer className="landing-footer-v2"> return (
<div className="footer-inner-v2"> <div key={app.id || appName} className="market-card-v2" onClick={() => handleAppClick(appName)}>
<div className="footer-brand-v2"> <div className="market-card-icon-v2" style={{ background: hasRealData ? `linear-gradient(135deg, ${appColor}44, ${appColor}11)` : getGradient(app) }}>
<svg width="28" height="28" viewBox="0 0 32 32" fill="none"> <span style={{ color: appColor, fontWeight: 700, fontSize: 16 }}>{appName.charAt(0)}</span>
<rect width="32" height="32" rx="8" fill="#5B5FED"/> </div>
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/> <div className="market-card-body-v2">
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/> <div className="market-card-header-v2">
</svg> <h4>{appName}</h4>
<span>七虎AI</span> <span className="type-badge" style={{ background: appColor, color: '#fff' }}>{appLabel}</span>
</div> </div>
<div className="footer-links-v2"> <p className="market-card-desc-v2">{appDesc}</p>
<div className="footer-col-v2"> <div className="market-card-meta-v2">
<h4>平台</h4> {appUsage > 0 && <span>📥 {appUsage >= 1000 ? `${(appUsage/1000).toFixed(1)}k` : appUsage}次</span>}
<a onClick={() => document.getElementById('what-is')?.scrollIntoView({ behavior: 'smooth' })}>平台介绍</a> <span>{appPrice === 0 ? '免费' : `¥${appPrice}`}</span>
<a onClick={() => document.getElementById('architecture')?.scrollIntoView({ behavior: 'smooth' })}>平台架构</a> </div>
<a onClick={() => document.getElementById('capabilities')?.scrollIntoView({ behavior: 'smooth' })}>能力体系</a> </div>
</div> </div>
<div className="footer-col-v2"> );
<h4>资源</h4> });
<a href="#">API 文档</a> })()}
<a href="#">开发者指南</a> </div>
<a href="#">更新日志</a> </section>
</div>
<div className="footer-col-v2"> {/* CTA */}
<h4>支持</h4> <section className="cta-section-v2">
<a href="#">帮助中心</a> <div className="cta-bg-glow" />
<a href="#">联系我们</a> <h2>准备好开始你的 AI 之旅了吗?</h2>
<a href="#">社区论坛</a> <p>免费注册,即刻拥有属于你自己的 AI 超级工作台</p>
</div> <button className="btn-hero-primary" onClick={() => navigate('/login')}>
</div> 免费注册
</div> <SVGIcon size={18}><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></SVGIcon>
<div className="footer-bottom-v2"> </button>
<p>&copy; 2026 七虎AI. All rights reserved.</p> </section>
</div>
</footer> {/* Footer */}
</div> <footer className="landing-footer-v2">
); <div className="footer-inner-v2">
}; <div className="footer-brand-v2">
<svg width="28" height="28" viewBox="0 0 32 32" fill="none">
export default LandingPage; <rect width="32" height="32" rx="8" fill="#5B5FED"/>
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
</svg>
<span>七虎AI</span>
</div>
<div className="footer-links-v2">
<div className="footer-col-v2">
<h4>平台</h4>
<a onClick={() => document.getElementById('what-is')?.scrollIntoView({ behavior: 'smooth' })}>平台介绍</a>
<a onClick={() => document.getElementById('architecture')?.scrollIntoView({ behavior: 'smooth' })}>平台架构</a>
<a onClick={() => document.getElementById('capabilities')?.scrollIntoView({ behavior: 'smooth' })}>能力体系</a>
</div>
<div className="footer-col-v2">
<h4>资源</h4>
<a href="#">API 文档</a>
<a href="#">开发者指南</a>
<a href="#">更新日志</a>
</div>
<div className="footer-col-v2">
<h4>支持</h4>
<a href="#">帮助中心</a>
<a href="#">联系我们</a>
<a href="#">社区论坛</a>
</div>
</div>
</div>
<div className="footer-bottom-v2">
<p>&copy; 2026 七虎AI. All rights reserved.</p>
</div>
</footer>
</div>
);
};
export default LandingPage;
File diff suppressed because it is too large. Load diff