feat(report): 为统计报告引入图形化费用分析视图

为增强 HTML 报告的可读性和洞察力,此变更集成了 Chart.js 来将关键的成本数据进行可视化。

现在,报告的每个标签页都会包含一个“数据总览”部分,其中有两个核心图表:
1.  **供应商成本构成图 (饼图)**: 直观展示不同供应商的费用占比,帮助快速定位主要开销来源。
2.  **模型成本排行榜 (条形图)**: 按费用降序排列各个模型,清晰地识别出成本最高的模型。

这些图表是动态生成的,如果统计周期内数据不足,将显示提示信息,避免用户面对空白图表。
This commit is contained in:
minecraft1024a
2025-11-13 10:56:33 +08:00
parent 87cea527a3
commit a8e8a5d170

View File

@@ -154,8 +154,7 @@ class HTMLReportGenerator:
""" """
# 增加饼图和条形图 # 增加饼图和条形图
# static_charts = self._generate_static_charts_div(stat_data, div_id) # 该功能尚未实现 static_charts = self._generate_static_charts_div(stat_data, div_id) # 该功能尚未实现
static_charts = ""
return f""" return f"""
<div id="{div_id}" class="tab-content"> <div id="{div_id}" class="tab-content">
<p class="info-item"> <p class="info-item">
@@ -365,3 +364,83 @@ class HTMLReportGenerator:
""" """
async with aiofiles.open(output_path, "w", encoding="utf-8") as f: async with aiofiles.open(output_path, "w", encoding="utf-8") as f:
await f.write(html_template) await f.write(html_template)
def _generate_static_charts_div(self, stat_data: dict[str, Any], period_id: str) -> str:
"""生成静态图表饼图、条形图的HTML和JS。"""
provider_cost_data = stat_data.get(PIE_CHART_COST_BY_PROVIDER, {})
model_cost_data = stat_data.get(BAR_CHART_COST_BY_MODEL, {})
if not provider_cost_data and not model_cost_data:
return "<h2>数据总览</h2><p>当前时段暂无足够数据生成图表。</p>"
provider_labels = provider_cost_data.get('labels', [])
provider_data = provider_cost_data.get('data', [])
model_labels = model_cost_data.get('labels', [])
model_data = model_cost_data.get('data', [])
return f"""
<h2>数据总览</h2>
<div class="chart-grid">
<div class="chart-container">
<canvas id="providerCostPieChart_{period_id}"></canvas>
</div>
<div class="chart-container">
<canvas id="modelCostBarChart_{period_id}"></canvas>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {{
const colors = ['#3498db', '#2ecc71', '#f1c40f', '#e74c3c', '#9b59b6', '#1abc9c', '#34495e', '#e67e22'];
// Provider Cost Pie Chart
const providerCtx = document.getElementById('providerCostPieChart_{period_id}');
if (providerCtx && {provider_data}) {{
new Chart(providerCtx, {{
type: 'pie',
data: {{
labels: {provider_labels},
datasets: [{{
label: '按供应商花费',
data: {provider_data},
backgroundColor: colors,
}}]
}},
options: {{
responsive: true,
maintainAspectRatio: false,
plugins: {{
title: {{ display: true, text: '按供应商花费分布', font: {{ size: 16 }} }},
legend: {{ position: 'top' }}
}}
}}
}});
}}
// Model Cost Bar Chart
const modelCtx = document.getElementById('modelCostBarChart_{period_id}');
if (modelCtx && {model_data}) {{
new Chart(modelCtx, {{
type: 'bar',
data: {{
labels: {model_labels},
datasets: [{{
label: '按模型花费',
data: {model_data},
backgroundColor: colors,
}}]
}},
options: {{
responsive: true,
maintainAspectRatio: false,
plugins: {{
title: {{ display: true, text: '按模型花费排行', font: {{ size: 16 }} }},
legend: {{ display: false }}
}},
scales: {{
y: {{ beginAtZero: true, title: {{ display: true, text: '花费 (¥)' }} }}
}}
}}
}});
}}
}});
</script>
"""