fix: 恢复 rsync 误删的近期开发(agents/skills/mcp/knowledge/workflow/tenant/billing/storage/privacy/promotion/achievement/automation/code-exec/developer/distribution/im-bot/portal/casdoor 等后端模块 + 对应前端页面路由, 补回 main.py 挂载与 App.tsx 路由

This commit is contained in:
qyyfw committed 2026-07-11 11:40:39 +08:00
1 parent d95b847fee
commit e8e5ffa1a9
65 files changed
+35624 -339

No files matched your search

+207
View File
@@ -0,0 +1,207 @@
import { useState, useEffect, useCallback } from 'react';
import { adminAPI } from '@/api';
interface Overview {
total_users: number; active_users: number; new_users: 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;
}
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; }
const Analytics: React.FC = () => {
const [days, setDays] = useState(7);
const [loading, setLoading] = useState(false);
const [overview, setOverview] = useState<Overview | null>(null);
const [apiTrend, setApiTrend] = useState<TrendItem[]>([]);
const [tokenTrend, setTokenTrend] = useState<{ daily: any[]; model_distribution: Record<string, number> }>({ daily: [], model_distribution: {} });
const [userTrend, setUserTrend] = useState<TrendItem[]>([]);
const [revenueTrend, setRevenueTrend] = useState<TrendItem[]>([]);
const [modelUsage, setModelUsage] = useState<ModelUsage[]>([]);
const fetchAll = useCallback(async () => {
setLoading(true);
try {
const [ov, api, tok, usr, rev, mu] = await Promise.all([
adminAPI.getAnalyticsOverview(days),
adminAPI.getAnalyticsApiTrends(days),
adminAPI.getAnalyticsTokenTrends(days),
adminAPI.getAnalyticsUserTrends(days),
adminAPI.getAnalyticsRevenueTrends(days),
adminAPI.getAnalyticsModelUsage(days),
]);
setOverview((ov as any)?.data || ov);
setApiTrend((api as any)?.data || []);
const td = (tok as any)?.data || { daily: [], model_distribution: {} };
setTokenTrend(td);
setUserTrend((usr as any)?.data || []);
setRevenueTrend((rev as any)?.data || []);
setModelUsage((mu as any)?.data || []);
} catch (e) { console.error('fetchAnalytics:', e); }
setLoading(false);
}, [days]);
useEffect(() => { fetchAll(); }, [fetchAll]);
const renderBar = (data: TrendItem[], key: string, color: string, label: string) => {
if (!data.length) return <div style={{ textAlign: 'center', padding: '40px', color: '#6B7280' }}>暂无数据</div>;
const values = data.map((d: any) => d[key] || 0);
const max = Math.max(...values, 1);
return (
<div style={{ display: 'flex', alignItems: 'flex-end', gap: '8px', height: '180px', padding: '0 16px' }}>
{data.map((d: any, i) => {
const v = d[key] || 0;
const h = (v / max) * 140;
return (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: '40px' }}>
<div style={{ fontSize: '10px', color: '#8892B0', marginBottom: '4px' }}>{v > 9999 ? `${(v/1000).toFixed(0)}k` : v}</div>
<div style={{ width: '100%', height: `${h}px`, background: `linear-gradient(180deg, ${color}, ${color}aa)`, borderRadius: '4px 4px 0 0', transition: 'height 0.3s' }} />
<div style={{ fontSize: '10px', color: '#6B7280', marginTop: '6px' }}>{d.date?.substring(5)}</div>
</div>
);
})}
</div>
);
};
const renderLine = (data: TrendItem[], key: string, color: string) => {
if (!data.length) return <div style={{ textAlign: 'center', padding: '40px', color: '#6B7280' }}>暂无数据</div>;
const values = data.map((d: any) => d[key] || 0);
const max = Math.max(...values, 1);
const w = 100 / (data.length - 1 || 1);
const points = data.map((d: any, i) => `${i * w},${100 - ((d[key] || 0) / max) * 90}`).join(' ');
return (
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ width: '100%', height: '180px' }}>
<polyline points={points} fill="none" stroke={color} strokeWidth="1.5" />
{data.map((d: any, i) => (
<circle key={i} cx={i * w} cy={100 - ((d[key] || 0) / max) * 90} r="1" fill={color} />
))}
</svg>
);
};
const statCards = overview ? [
{ label: '总用户数', value: overview.total_users, color: '#6C63FF', icon: 'U' },
{ label: '活跃用户', value: overview.active_users, color: '#10b981', icon: 'A' },
{ label: '新增用户', value: overview.new_users, color: '#3b82f6', icon: '+' },
{ label: 'API调用数', value: overview.api_calls, color: '#f59e0b', icon: 'API' },
{ label: 'Token用量', value: overview.tokens_used, color: '#ec4899', icon: 'T' },
{ label: '总费用(元)', value: overview.total_cost?.toFixed(2), color: '#ef4444', icon: '¥' },
{ label: '对话总数', value: overview.total_chats, color: '#8b5cf6', icon: 'C' },
{ label: '消息总数', value: overview.total_messages, color: '#06b6d4', icon: 'M' },
] : [];
return (
<div style={{ padding: '24px', color: '#1A1F36' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<h1 style={{ fontSize: '24px', fontWeight: 700 }}>数据分析面板</h1>
<div style={{ display: 'flex', gap: '8px' }}>
{[7, 14, 30, 90].map(d => (
<button key={d} onClick={() => setDays(d)} style={{ padding: '6px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
background: days === d ? '#6C63FF' : '#F0F2F8', color: days === d ? '#fff' : '#5A6480', fontWeight: 600, fontSize: '13px' }}>{d}天</button>
))}
</div>
</div>
{loading ? <div style={{ textAlign: 'center', padding: '80px', color: '#6B7280' }}>加载中...</div> : (
<>
{/* Stats grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '16px', marginBottom: '24px' }}>
{statCards.map((s, i) => (
<div key={i} style={{ background: '#FFFFFF', borderRadius: '12px', padding: '20px', border: `1px solid ${s.color}33`, display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '48px', height: '48px', borderRadius: '12px', background: `${s.color}22`, color: s.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '18px', fontWeight: 800 }}>{s.icon}</div>
<div>
<div style={{ fontSize: '24px', fontWeight: 800, color: s.color }}>{typeof s.value === 'number' ? s.value.toLocaleString() : s.value}</div>
<div style={{ fontSize: '12px', color: '#8892B0', marginTop: '2px' }}>{s.label}</div>
</div>
</div>
))}
</div>
{/* Charts grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))', gap: '16px', marginBottom: '16px' }}>
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px' }}>API调用趋势 ({days}天)</h3>
{renderBar(apiTrend, 'calls', '#f59e0b', '调用次数')}
</div>
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px' }}>Token使用趋势 ({days}天)</h3>
{renderLine(tokenTrend.daily, 'total_tokens', '#ec4899')}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))', gap: '16px', marginBottom: '16px' }}>
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px' }}>用户增长 ({days}天)</h3>
{renderBar(userTrend, 'new_users', '#3b82f6', '新增用户')}
</div>
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px' }}>收入趋势 ({days}天)</h3>
{renderBar(revenueTrend, 'revenue', '#10b981', '收入')}
</div>
</div>
{/* Model usage */}
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '20px' }}>模型使用分析 ({days}天)</h3>
{modelUsage.length > 0 ? (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead><tr style={{ borderBottom: '2px solid rgba(0,0,0,0.08)' }}>
{['模型', '调用次数', '总Tokens', '平均Tokens', '费用(元)'].map(h => (
<th key={h} style={{ padding: '10px 16px', textAlign: 'left', fontSize: '13px', color: '#8892B0', fontWeight: 600 }}>{h}</th>
))}
</tr></thead>
<tbody>
{modelUsage.map((m, i) => (
<tr key={i} style={{ borderBottom: '1px solid rgba(0,0,0,0.08)' }}>
<td style={{ padding: '10px 16px', fontSize: '14px', fontWeight: 600 }}>{m.model_name}</td>
<td style={{ padding: '10px 16px', fontSize: '14px' }}>{m.call_count.toLocaleString()}</td>
<td style={{ padding: '10px 16px', fontSize: '14px', color: '#ec4899', fontWeight: 600 }}>{m.total_tokens.toLocaleString()}</td>
<td style={{ padding: '10px 16px', fontSize: '13px', color: '#8892B0' }}>{m.avg_tokens.toLocaleString()}</td>
<td style={{ padding: '10px 16px', fontSize: '14px', color: '#ef4444' }}>¥{m.total_cost.toFixed(4)}</td>
</tr>
))}
</tbody>
</table>
) : <div style={{ textAlign: 'center', padding: '40px', color: '#6B7280' }}>暂无模型使用数据</div>}
</div>
{/* Model distribution pie (CSS) */}
{Object.keys(tokenTrend.model_distribution || {}).length > 0 && (
<div style={{ background: '#FFFFFF', borderRadius: '12px', padding: '24px', marginTop: '16px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '20px' }}>模型使用分布</h3>
<div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>
<div style={{ display: 'flex', borderRadius: '50%', overflow: 'hidden', width: '160px', height: '160px' }}>
{(() => {
const entries = Object.entries(tokenTrend.model_distribution);
const total = entries.reduce((s, [, v]) => s + v, 1);
const colors = ['#6C63FF', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444'];
return entries.map(([model, val], i) => {
const pct = (val / total) * 100;
return <div key={model} style={{ width: `${pct}%`, background: colors[i % colors.length] }} title={`${model}: ${pct.toFixed(1)}%`} />;
});
})()}
</div>
<div style={{ flex: 1 }}>
{Object.entries(tokenTrend.model_distribution).map(([model, val], i) => {
const colors = ['#6C63FF', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444'];
const total = Object.values(tokenTrend.model_distribution).reduce((s, v) => s + v, 1);
return (
<div key={model} style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<div style={{ width: '12px', height: '12px', borderRadius: '3px', background: colors[i % colors.length] }} />
<span style={{ fontSize: '13px', color: '#1A1F36' }}>{model}</span>
<span style={{ fontSize: '13px', color: '#8892B0', marginLeft: 'auto' }}>{((val / total) * 100).toFixed(1)}%</span>
</div>
);
})}
</div>
</div>
</div>
)}
</>
)}
</div>
);
};
export default Analytics;