refactor(report): 使用Jinja2模板重构报告生成器

将报告生成逻辑与表示层分离,以提高代码的可维护性和可读性。

- HTML、CSS 和 JavaScript 从 Python f-string 中提取到独立的模板文件中。
- 引入 Jinja2 模板引擎动态渲染报告内容,使未来修改报告样式和结构更加方便,实现了逻辑和视图的分离。
This commit is contained in:
minecraft1024a
2025-11-13 12:09:37 +08:00
parent 81b83c88dc
commit b1a022cc3c
7 changed files with 468 additions and 267 deletions

View File

@@ -0,0 +1,134 @@
let i, tab_content, tab_links;
tab_content = document.getElementsByClassName("tab-content");
tab_links = document.getElementsByClassName("tab-link");
if (tab_content.length > 0) tab_content[0].classList.add("active");
if (tab_links.length > 0) tab_links[0].classList.add("active");
function showTab(evt, tabName) {
for (i = 0; i < tab_content.length; i++) tab_content[i].classList.remove("active");
for (i = 0; i < tab_links.length; i++) tab_links[i].classList.remove("active");
document.getElementById(tabName).classList.add("active");
evt.currentTarget.classList.add("active");
}
document.addEventListener('DOMContentLoaded', function () {
// This is a placeholder for chart data which will be injected by python.
const allChartData = JSON.parse('{{ all_chart_data }}')
;
let currentCharts = {};
const chartConfigs = {
totalCost: { id: 'totalCostChart', title: '总花费', yAxisLabel: '花费 (¥)', dataKey: 'total_cost_data', fill: true },
costByModule: { id: 'costByModuleChart', title: '各模块花费', yAxisLabel: '花费 (¥)', dataKey: 'cost_by_module', fill: false },
costByModel: { id: 'costByModelChart', title: '各模型花费', yAxisLabel: '花费 (¥)', dataKey: 'cost_by_model', fill: false },
messageByChat: { id: 'messageByChatChart', title: '各聊天流消息数', yAxisLabel: '消息数', dataKey: 'message_by_chat', fill: false }
};
window.switchTimeRange = function(timeRange) {
document.querySelectorAll('.time-range-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
updateAllCharts(allChartData[timeRange], timeRange);
}
function updateAllCharts(data, timeRange) {
Object.values(currentCharts).forEach(chart => chart && chart.destroy());
currentCharts = {};
Object.keys(chartConfigs).forEach(type => createChart(type, data, timeRange));
}
function createChart(chartType, data, timeRange) {
const config = chartConfigs[chartType];
if (!data || !data[config.dataKey]) return;
const colors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#34495e', '#e67e22', '#95a5a6', '#f1c40f'];
let datasets = [];
if (chartType === 'totalCost') {
datasets = [{ label: config.title, data: data[config.dataKey], borderColor: colors[0], backgroundColor: 'rgba(52, 152, 219, 0.1)', tension: 0.4, fill: config.fill }];
} else {
let i = 0;
Object.entries(data[config.dataKey]).forEach(([name, chartData]) => {
datasets.push({ label: name, data: chartData, borderColor: colors[i % colors.length], backgroundColor: colors[i % colors.length] + '20', tension: 0.4, fill: config.fill });
i++;
});
}
currentCharts[chartType] = new Chart(document.getElementById(config.id), {
type: 'line',
data: { labels: data.time_labels, datasets: datasets },
options: {
responsive: true,
plugins: { title: { display: true, text: `${timeRange}${config.title}趋势`, font: { size: 16 } }, legend: { display: chartType !== 'totalCost', position: 'top' } },
scales: { x: { title: { display: true, text: '时间' }, ticks: { maxTicksLimit: 12 } }, y: { title: { display: true, text: config.yAxisLabel }, beginAtZero: true } },
interaction: { intersect: false, mode: 'index' }
}
});
}
if (allChartData['24h']) {
updateAllCharts(allChartData['24h'], '24h');
// Activate the 24h button by default
document.querySelectorAll('.time-range-btn').forEach(btn => {
if (btn.textContent.includes('24小时')) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
}
// Static charts
const staticChartData = JSON.parse('{{ static_chart_data }}')
;
Object.keys(staticChartData).forEach(period_id => {
const providerCostData = staticChartData[period_id].provider_cost_data;
const modelCostData = staticChartData[period_id].model_cost_data;
const colors = ['#3498db', '#2ecc71', '#f1c40f', '#e74c3c', '#9b59b6', '#1abc9c', '#34495e', '#e67e22'];
// Provider Cost Pie Chart
const providerCtx = document.getElementById(`providerCostPieChart_${period_id}`);
if (providerCtx && providerCostData && providerCostData.data.length > 0) {
new Chart(providerCtx, {
type: 'pie',
data: {
labels: providerCostData.labels,
datasets: [{
label: '按供应商花费',
data: providerCostData.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 && modelCostData && modelCostData.data.length > 0) {
new Chart(modelCtx, {
type: 'bar',
data: {
labels: modelCostData.labels,
datasets: [{
label: '按模型花费',
data: modelCostData.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: '花费 (¥)' } }
}
}
});
}
});
});