This commit is contained in:
2026-07-22 16:56:42 +08:00
parent ed2e1715d1
commit 4fdbe0c946
6 changed files with 80 additions and 57 deletions
+45 -44
View File
@@ -361,55 +361,56 @@ const HorizontalBarChart: React.FC<{ data: { teamName?: string; modelName?: stri
};
// ── 视频参数分布(紧凑饼图+列表)──
const PieBarChart: React.FC<{ data: { label: string; count: number }[] }> = ({ data }) => {
const PieBarChart: React.FC<{ data: { model: string; label: string; count: number }[] }> = ({ data }) => {
if (!data.length) return <EmptyChart />;
// 按模型分组
const modelMap = new Map<string, { label: string; count: number }[]>();
data.forEach(d => {
if (!modelMap.has(d.model)) modelMap.set(d.model, []);
modelMap.get(d.model)!.push({ label: d.label, count: d.count });
});
const models = Array.from(modelMap.entries());
const total = data.reduce((s, d) => s + d.count, 0) || 1;
const maxVal = Math.max(...data.map(d => d.count), 1);
return (
<div style={{ height: 220, display: 'flex', alignItems: 'center', gap: 12 }}>
{/* 迷你饼图 */}
<svg width={90} height={90} viewBox="0 0 90 90" style={{ flexShrink: 0, filter: 'drop-shadow(0 2px 6px rgba(0,0,0,0.08))' }}>
{(() => {
const cx = 45, cy = 45, r = 38;
let cum = -90;
return data.map((d, i) => {
const angle = (d.count / total) * 360;
const start = cum; cum += angle;
const end = cum;
const sr = (start * Math.PI) / 180, er = (end * Math.PI) / 180;
const x1 = cx + r * Math.cos(sr), y1 = cy + r * Math.sin(sr);
const x2 = cx + r * Math.cos(er), y2 = cy + r * Math.sin(er);
const la = angle > 180 ? 1 : 0;
return <path key={i} d={`M${cx},${cy} L${x1},${y1} A${r},${r} 0 ${la} 1 ${x2},${y2} Z`} fill={COLORS[i % COLORS.length]} stroke="#fff" strokeWidth={1.5} style={{ transition: 'transform 0.25s', transformOrigin: `${cx}px ${cy}px`, cursor: 'pointer' }}
onMouseEnter={e => { (e.target as SVGPathElement).style.transform = 'scale(1.08)'; }}
onMouseLeave={e => { (e.target as SVGPathElement).style.transform = 'scale(1)'; }}
/>;
});
})()}
<circle cx={45} cy={45} r={20} fill="#fff" />
<text x={45} y={48} textAnchor="middle" fontSize={12} fontWeight={700} fill="#1e293b">{total}</text>
</svg>
{/* 列表 */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, overflowY: 'auto', maxHeight: 200 }}>
{data.map((d, i) => {
const pct = (d.count / maxVal) * 100;
return (
<div key={i} style={{ padding: '3px 6px', borderRadius: 6, transition: 'background-color 0.2s' }}
onMouseEnter={e => { e.currentTarget.style.backgroundColor = '#fafbff'; }}
onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'transparent'; }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 2 }}>
<span style={{ fontSize: 11, color: '#475569', fontWeight: 500 }}>{d.label}</span>
<span style={{ fontSize: 11, fontWeight: 700, color: '#1e293b' }}>{d.count} <span style={{ fontSize: 9, color: '#94a3b8' }}>({((d.count / total) * 100).toFixed(1)}%)</span></span>
</div>
<div style={{ height: 8, background: '#f1f5f9', borderRadius: 4, overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${pct}%`, background: COLORS[i % COLORS.length], borderRadius: 4, transition: 'width 0.3s' }} />
</div>
<div style={{ height: 220, overflowY: 'auto' }}>
{models.map(([model, items], mi) => {
const modelTotal = items.reduce((s, it) => s + it.count, 0);
return (
<div key={model} style={{ marginBottom: 12 }}>
{/* 模型名称 + 总数 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: COLORS[mi % COLORS.length] }}>{model}</span>
<span style={{ fontSize: 10, color: '#94a2b3' }}> {modelTotal} ({((modelTotal / total) * 100).toFixed(1)}%)</span>
</div>
);
})}
</div>
{/* 各参数 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
{items.map((item, i) => (
<div key={i} style={{
flex: '0 0 calc(50% - 2px)',
padding: '4px 8px',
background: '#f8fafc',
borderRadius: 6,
border: '1px solid #f1f5f9',
transition: 'all 0.2s',
cursor: 'default',
}}
onMouseEnter={e => { Object.assign(e.currentTarget.style, { background: '#fafbff', boxShadow: '0 2px 8px rgba(99,102,241,0.1)', transform: 'translateY(-1px)' }); }}
onMouseLeave={e => { Object.assign(e.currentTarget.style, { background: '#f8fafc', boxShadow: 'none', transform: 'translateY(0)' }); }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 11, color: '#475569', fontWeight: 500 }}>{item.label}</span>
<span style={{ fontSize: 11, fontWeight: 700, color: '#1e293b' }}>{item.count}</span>
</div>
<div style={{ height: 4, background: '#e2e8f0', borderRadius: 2, marginTop: 3, overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${(item.count / modelTotal) * 100}%`, background: COLORS[mi % COLORS.length], borderRadius: 2, transition: 'width 0.3s' }} />
</div>
</div>
))}
</div>
</div>
);
})}
</div>
);
};
+1
View File
@@ -218,6 +218,7 @@ export interface ModelUsageOut {
}
export interface VideoParamOut {
model: string;
label: string;
count: number;
}