1
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -218,6 +218,7 @@ export interface ModelUsageOut {
|
||||
}
|
||||
|
||||
export interface VideoParamOut {
|
||||
model: string;
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user