feat(report): 增强统计报告,增加模块花费图表并优化UI
本次更新对统计报告进行了多项功能增强和界面优化,旨在提供更丰富的分析维度和更佳的用户体验。 主要变更包括: - **新功能**: - 新增“按模块花费”饼图,以提供新的成本分析维度。 - 在供应商统计表格中加入“平均耗时”指标,用于性能评估。 - 在报告顶部添加“名词解释”卡片,帮助用户理解关键指标。 - **UI/UX 优化**: - 重构页面布局为主内容区与图表侧边栏,提升信息密度和可读性,并实现响应式设计。 - 全面优化图表视觉效果,包括更新调色板、增加加载动画、改进提示框,使其更具表现力和交互性。
This commit is contained in:
@@ -92,6 +92,7 @@ class HTMLReportGenerator:
|
||||
f"<td>{stat_data[TPS_BY_PROVIDER].get(provider_name, 0):.2f}</td>"
|
||||
f"<td>{stat_data[COST_PER_KTOK_BY_PROVIDER].get(provider_name, 0):.4f} ¥</td>"
|
||||
f"<td>{stat_data[COST_BY_PROVIDER].get(provider_name, 0):.4f} ¥</td>"
|
||||
f"<td>{stat_data.get(AVG_TIME_COST_BY_PROVIDER, {}).get(provider_name, 0):.3f} 秒</td>"
|
||||
f"</tr>"
|
||||
for provider_name, count in sorted(stat_data[REQ_CNT_BY_PROVIDER].items())
|
||||
]
|
||||
@@ -316,10 +317,12 @@ class HTMLReportGenerator:
|
||||
period_id = period[0]
|
||||
static_chart_data[period_id] = {
|
||||
"provider_cost_data": stat[period_id].get(PIE_CHART_COST_BY_PROVIDER, {}),
|
||||
"module_cost_data": stat[period_id].get(PIE_CHART_COST_BY_MODULE, {}),
|
||||
"model_cost_data": stat[period_id].get(BAR_CHART_COST_BY_MODEL, {}),
|
||||
}
|
||||
static_chart_data["all_time"] = {
|
||||
"provider_cost_data": stat["all_time"].get(PIE_CHART_COST_BY_PROVIDER, {}),
|
||||
"module_cost_data": stat["all_time"].get(PIE_CHART_COST_BY_MODULE, {}),
|
||||
"model_cost_data": stat["all_time"].get(BAR_CHART_COST_BY_MODEL, {}),
|
||||
}
|
||||
|
||||
|
||||
@@ -299,6 +299,7 @@ class StatisticOutputTask(AsyncTask):
|
||||
# Chart data
|
||||
PIE_CHART_COST_BY_PROVIDER: {},
|
||||
PIE_CHART_REQ_BY_PROVIDER: {},
|
||||
PIE_CHART_COST_BY_MODULE: {},
|
||||
BAR_CHART_COST_BY_MODEL: {},
|
||||
BAR_CHART_REQ_BY_MODEL: {},
|
||||
}
|
||||
@@ -457,6 +458,15 @@ class StatisticOutputTask(AsyncTask):
|
||||
"data": [round(item[1], 4) for item in sorted_providers],
|
||||
}
|
||||
|
||||
# 按模块花费饼图
|
||||
module_costs = period_stats[COST_BY_MODULE]
|
||||
if module_costs:
|
||||
sorted_modules = sorted(module_costs.items(), key=lambda item: item[1], reverse=True)
|
||||
period_stats[PIE_CHART_COST_BY_MODULE] = {
|
||||
"labels": [item[0] for item in sorted_modules],
|
||||
"data": [round(item[1], 4) for item in sorted_modules],
|
||||
}
|
||||
|
||||
# 按模型花费条形图
|
||||
model_costs = period_stats[COST_BY_MODEL]
|
||||
if model_costs:
|
||||
|
||||
@@ -59,6 +59,7 @@ STD_TIME_COST_BY_PROVIDER = "std_time_costs_by_provider"
|
||||
# 新增饼图和条形图数据
|
||||
PIE_CHART_COST_BY_PROVIDER = "pie_chart_cost_by_provider"
|
||||
PIE_CHART_REQ_BY_PROVIDER = "pie_chart_req_by_provider"
|
||||
PIE_CHART_COST_BY_MODULE = "pie_chart_cost_by_module"
|
||||
BAR_CHART_COST_BY_MODEL = "bar_chart_cost_by_model"
|
||||
BAR_CHART_REQ_BY_MODEL = "bar_chart_req_by_model"
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ body {
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 4px 16px var(--md-sys-color-shadow);
|
||||
animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
@@ -48,18 +49,43 @@ body {
|
||||
/* Dashboard Layout */
|
||||
.dashboard-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
align-items: start;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
width: 380px;
|
||||
min-width: 380px;
|
||||
flex-shrink: 0;
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg, #FAFAFA 0%, #FFFFFF 100%);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
}
|
||||
|
||||
/* 自定义侧边栏滚动条 */
|
||||
.sidebar-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.sidebar-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sidebar-content::-webkit-scrollbar-thumb {
|
||||
background: var(--md-sys-color-outline);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sidebar-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
/* Typography - Material Design 3 */
|
||||
@@ -378,6 +404,24 @@ tbody tr:hover {
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
position: static;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-height: none;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 20px;
|
||||
|
||||
@@ -178,8 +178,21 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
Object.keys(staticChartData).forEach(period_id => {
|
||||
const providerCostData = staticChartData[period_id].provider_cost_data;
|
||||
const moduleCostData = staticChartData[period_id].module_cost_data;
|
||||
const modelCostData = staticChartData[period_id].model_cost_data;
|
||||
const colors = ['#3498db', '#2ecc71', '#f1c40f', '#e74c3c', '#9b59b6', '#1abc9c', '#34495e', '#e67e22'];
|
||||
// 扩展的Material Design调色板 - 包含多种蓝色系和其他配色
|
||||
const colors = [
|
||||
'#1976D2', '#42A5F5', '#2196F3', '#64B5F6', '#90CAF9', '#BBDEFB', // 蓝色系
|
||||
'#1565C0', '#0D47A1', '#82B1FF', '#448AFF', // 深蓝色系
|
||||
'#00BCD4', '#26C6DA', '#4DD0E1', '#80DEEA', // 青色系
|
||||
'#009688', '#26A69A', '#4DB6AC', '#80CBC4', // 青绿色系
|
||||
'#4CAF50', '#66BB6A', '#81C784', '#A5D6A7', // 绿色系
|
||||
'#FF9800', '#FFA726', '#FFB74D', '#FFCC80', // 橙色系
|
||||
'#FF5722', '#FF7043', '#FF8A65', '#FFAB91', // 深橙色系
|
||||
'#9C27B0', '#AB47BC', '#BA68C8', '#CE93D8', // 紫色系
|
||||
'#E91E63', '#EC407A', '#F06292', '#F48FB1', // 粉色系
|
||||
'#607D8B', '#78909C', '#90A4AE', '#B0BEC5' // 蓝灰色系
|
||||
];
|
||||
|
||||
// Provider Cost Pie Chart
|
||||
const providerCtx = document.getElementById(`providerCostPieChart_${period_id}`);
|
||||
@@ -200,28 +213,95 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
aspectRatio: 1.5,
|
||||
aspectRatio: 1.3,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '按供应商花费分布',
|
||||
font: { size: 14, weight: '500' },
|
||||
text: '🏢 按供应商花费分布',
|
||||
font: { size: 13, weight: '500' },
|
||||
color: '#1C1B1F',
|
||||
padding: { top: 8, bottom: 16 }
|
||||
padding: { top: 4, bottom: 12 }
|
||||
},
|
||||
legend: {
|
||||
position: 'right',
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 15,
|
||||
font: { size: 12 }
|
||||
padding: 10,
|
||||
font: { size: 11 }
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
padding: 12,
|
||||
titleFont: { size: 14 },
|
||||
bodyFont: { size: 13 },
|
||||
titleFont: { size: 13 },
|
||||
bodyFont: { size: 12 },
|
||||
cornerRadius: 8,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += context.parsed.toFixed(4) + ' ¥';
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((context.parsed / total) * 100).toFixed(2);
|
||||
label += ` (${percentage}%)`;
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
animateRotate: true,
|
||||
animateScale: true,
|
||||
duration: 1000,
|
||||
easing: 'easeInOutQuart'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Module Cost Pie Chart
|
||||
const moduleCtx = document.getElementById(`moduleCostPieChart_${period_id}`);
|
||||
if (moduleCtx && moduleCostData && moduleCostData.data && moduleCostData.data.length > 0) {
|
||||
new Chart(moduleCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: moduleCostData.labels,
|
||||
datasets: [{
|
||||
label: '按模块花费',
|
||||
data: moduleCostData.data,
|
||||
backgroundColor: colors,
|
||||
borderColor: '#FFFFFF',
|
||||
borderWidth: 2,
|
||||
hoverOffset: 8
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
aspectRatio: 1.3,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '🔧 按模块使用分布',
|
||||
font: { size: 13, weight: '500' },
|
||||
color: '#1C1B1F',
|
||||
padding: { top: 4, bottom: 12 }
|
||||
},
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 10,
|
||||
font: { size: 11 }
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
padding: 12,
|
||||
titleFont: { size: 13 },
|
||||
bodyFont: { size: 12 },
|
||||
cornerRadius: 8,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
@@ -261,28 +341,28 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
backgroundColor: colors,
|
||||
borderColor: colors,
|
||||
borderWidth: 2,
|
||||
borderRadius: 8,
|
||||
borderRadius: 6,
|
||||
hoverBackgroundColor: colors.map(c => c + 'dd')
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
aspectRatio: 1.5,
|
||||
aspectRatio: 1.2,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: '按模型花费排行',
|
||||
font: { size: 14, weight: '500' },
|
||||
text: '🤖 按模型花费排行',
|
||||
font: { size: 13, weight: '500' },
|
||||
color: '#1C1B1F',
|
||||
padding: { top: 8, bottom: 16 }
|
||||
padding: { top: 4, bottom: 12 }
|
||||
},
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
padding: 12,
|
||||
titleFont: { size: 14 },
|
||||
bodyFont: { size: 13 },
|
||||
titleFont: { size: 13 },
|
||||
bodyFont: { size: 12 },
|
||||
cornerRadius: 8,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
@@ -293,16 +373,22 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: false }
|
||||
grid: { display: false },
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: '💰 花费 (¥)',
|
||||
font: { size: 13, weight: 'bold' }
|
||||
font: { size: 11, weight: 'bold' }
|
||||
},
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: {
|
||||
font: { size: 10 }
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<h2>📊 数据总览</h2>
|
||||
<div style="display: flex; flex-direction: column; gap: 24px;">
|
||||
<div class="chart-container" style="height: 300px;">
|
||||
<div style="margin: -24px -24px 0 -24px; padding: 20px 24px; background: linear-gradient(135deg, #1976D2 0%, #2196F3 100%); border-radius: 20px 20px 0 0; margin-bottom: 20px;">
|
||||
<h2 style="margin: 0; color: white; font-size: 1.3em; font-weight: 500; display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 1.2em;">📊</span> 数据可视化
|
||||
</h2>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 20px;">
|
||||
<div class="chart-container" style="height: 280px; margin: 0; padding: 16px; background: white;">
|
||||
<canvas id="providerCostPieChart_{{ period_id }}"></canvas>
|
||||
</div>
|
||||
<div class="chart-container" style="height: 300px;">
|
||||
<div class="chart-container" style="height: 280px; margin: 0; padding: 16px; background: white;">
|
||||
<canvas id="moduleCostPieChart_{{ period_id }}"></canvas>
|
||||
</div>
|
||||
<div class="chart-container" style="height: 300px; margin: 0; padding: 16px; background: white;">
|
||||
<canvas id="modelCostBarChart_{{ period_id }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,25 @@
|
||||
<div id="{{ div_id }}" class="tab-content">
|
||||
<div class="info-item" style="background: linear-gradient(135deg, #E3F2FD 0%, #BBDEFB 100%); border-left-color: #1976D2;">
|
||||
<strong>📖 名词解释</strong>
|
||||
<div style="margin-top: 12px; line-height: 1.8; font-size: 0.9em;">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<strong style="color: #1976D2;">🎯 Token:</strong> AI处理文本的基本单位,约等于0.75个英文单词或0.5个中文字
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<strong style="color: #1976D2;">💸 TPS:</strong> Token Per Second,每秒处理的Token数量,衡量AI响应速度
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<strong style="color: #1976D2;">📊 每K Token成本:</strong> 每1000个Token的成本,用于比较不同模型的性价比
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<strong style="color: #1976D2;">⚡ Token效率:</strong> 输出Token与输入Token的比率,反映模型输出丰富度
|
||||
</div>
|
||||
<div>
|
||||
<strong style="color: #1976D2;">🔄 消息/请求比:</strong> 平均每次AI请求处理的用户消息数量
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-layout">
|
||||
<div class="main-content">
|
||||
<p class="info-item">
|
||||
@@ -15,7 +36,9 @@
|
||||
|
||||
<h2>🏢 按供应商分类统计</h2>
|
||||
<table>
|
||||
<tr><th>供应商名称</th><th>调用次数</th><th>Token总量</th><th>TPS</th><th>每K Token成本</th><th>累计花费</th></tr>
|
||||
<thead>
|
||||
<tr><th>供应商名称</th><th>调用次数</th><th>Token总量</th><th>TPS</th><th>每K Token成本</th><th>累计花费</th><th>平均耗时(秒)</th></tr>
|
||||
</thead>
|
||||
<tbody>{{ provider_rows }}</tbody>
|
||||
</table>
|
||||
|
||||
@@ -58,9 +81,9 @@
|
||||
<tbody>{{ efficiency_rows }}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
{{ static_charts }}
|
||||
<div class="sidebar-content">
|
||||
{{ static_charts }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user