refactor(report): 优化报告样式和数据加载逻辑

- 调整了报告页面的CSS样式,包括颜色、阴影和布局,以提供更专业、现代的视觉效果。
- 改进了从后端向前端JavaScript传递图表数据的方式。现在通过一个独立的`<script>`标签注入JSON字符串,而不是直接嵌入到JS代码中,这增强了鲁棒性并避免了特殊字符导致的解析错误。
- 在JavaScript中增加了对JSON解析和图表数据有效性的检查,以防止因数据格式错误或缺失导致页面渲染失败。
- 将统计模块中的耗时相关键名统一为大写格式,以提高代码一致性。
This commit is contained in:
minecraft1024a
2025-11-13 14:31:54 +08:00
parent a9f4c422b1
commit 992a1129ce
5 changed files with 50 additions and 35 deletions

View File

@@ -2,12 +2,13 @@
该模块用于生成HTML格式的统计报告。 该模块用于生成HTML格式的统计报告。
""" """
from datetime import datetime, timedelta
from typing import Any
import json import json
import os import os
from jinja2 import Environment, FileSystemLoader from datetime import datetime, timedelta
from typing import Any
import aiofiles import aiofiles
from jinja2 import Environment, FileSystemLoader
from .statistic_keys import * # noqa: F403 from .statistic_keys import * # noqa: F403

View File

@@ -412,9 +412,9 @@ class StatisticOutputTask(AsyncTask):
(REQ_CNT_BY_MODEL, "model"), (REQ_CNT_BY_MODEL, "model"),
(REQ_CNT_BY_MODULE, "module"), (REQ_CNT_BY_MODULE, "module"),
]: ]:
time_cost_key = f"time_costs_by_{items}" time_cost_key = f"TIME_COST_BY_{items.upper()}"
avg_key = f"avg_time_costs_by_{items}" avg_key = f"AVG_TIME_COST_BY_{items.upper()}"
std_key = f"std_time_costs_by_{items}" std_key = f"STD_TIME_COST_BY_{items.upper()}"
for item_name in period_stats[category_key]: for item_name in period_stats[category_key]:
time_costs = period_stats[time_cost_key].get(item_name, []) time_costs = period_stats[time_cost_key].get(item_name, [])

View File

@@ -3,22 +3,20 @@ body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
background-color: #F8F9FA; /* Light grey background */ background-color: #f0f4f8; /* Light blue-gray background */
color: #495057; /* Softer text color */ color: #333; /* Darker text for better contrast */
line-height: 1.6; line-height: 1.6;
} }
/* Main Container */ /* Main Container */
.container { .container {
max-width: 1200px; max-width: 95%; /* Make container almost full-width */
margin: 20px auto; margin: 20px auto;
background-color: #FFFFFF; /* Pure white background */ background-color: #FFFFFF; /* Pure white background */
padding: 30px; padding: 30px;
border-radius: 8px; border-radius: 12px; /* Slightly more rounded corners */
box-shadow: 0 4px 12px rgba(0,0,0,0.05); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.07); /* Softer, deeper shadow */
border: 1px solid #EAEAEA;
} }
/* Dashboard Layout */ /* Dashboard Layout */
.dashboard-layout { .dashboard-layout {
display: flex; display: flex;
@@ -56,28 +54,28 @@ h1 {
text-align: center; text-align: center;
font-size: 2.2em; font-size: 2.2em;
margin-bottom: 20px; margin-bottom: 20px;
color: #4A90E2; /* Main blue for title */ color: #2A6CB5; /* A deeper, more professional blue */
} }
h2 { h2 {
font-size: 1.5em; font-size: 1.5em;
margin-top: 40px; margin-top: 40px;
margin-bottom: 15px; margin-bottom: 15px;
border-bottom: 2px solid #EAEAEA; border-bottom: 2px solid #DDE6ED; /* Lighter border color */
} }
/* Info Banners */ /* Info Banners */
.info-item { .info-item {
background-color: #E9ECEF; background-color: #E9F2FA; /* Light blue background */
padding: 10px 15px; padding: 12px 18px;
border-radius: 6px; border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
font-size: 0.95em; font-size: 0.95em;
border: 1px solid #DEE2E6; border: 1px solid #D1E3F4; /* Light blue border */
} }
.info-item strong { .info-item strong {
color: #4A90E2; color: #2A6CB5; /* Deeper blue for emphasis */
} }
/* Tabs */ /* Tabs */
@@ -101,12 +99,13 @@ h2 {
} }
.tabs button:hover { .tabs button:hover {
color: #212529; color: #2A6CB5;
background-color: #f0f4f8; /* Subtle hover background */
} }
.tabs button.active { .tabs button.active {
color: #4A90E2; color: #2A6CB5; /* Active tab color */
border-bottom-color: #4A90E2; border-bottom-color: #2A6CB5; /* Active tab border color */
} }
.tab-content { .tab-content {
@@ -131,7 +130,7 @@ h2 {
padding: 20px; padding: 20px;
border-radius: 8px; border-radius: 8px;
text-align: center; text-align: center;
border: 1px solid #EAEAEA; border: 1px solid #DDE6ED; /* Lighter border */
transition: all 0.3s ease; transition: all 0.3s ease;
} }
@@ -166,9 +165,8 @@ th, td {
text-align: left; text-align: left;
border-bottom: 1px solid #EAEAEA; border-bottom: 1px solid #EAEAEA;
} }
th { th {
background-color: #4A90E2; background-color: #4A90E2; /* Main theme blue */
color: white; color: white;
font-weight: bold; font-weight: bold;
font-size: 0.95em; font-size: 0.95em;
@@ -177,11 +175,11 @@ th {
} }
tr:nth-child(even) { tr:nth-child(even) {
background-color: #F8F9FA; background-color: #F7FAFC; /* Very light blue for alternate rows */
} }
tr:hover { tr:hover {
background-color: #E9ECEF; background-color: #E9F2FA; /* Light blue for hover */
} }
/* Chart Container in Sidebar */ /* Chart Container in Sidebar */

View File

@@ -14,6 +14,10 @@
<div class="tabs">{{ tab_list }}</div> <div class="tabs">{{ tab_list }}</div>
{{ tab_content }} {{ tab_content }}
</div> </div>
<script>
const all_chart_data_json_string = `{{ all_chart_data|safe }}`;
const static_chart_data_json_string = `{{ static_chart_data|safe }}`;
</script>
<script>{{ report_js }}</script> <script>{{ report_js }}</script>
</body> </body>
</html> </html>

View File

@@ -11,9 +11,15 @@ function showTab(evt, tabName) {
} }
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// This is a placeholder for chart data which will be injected by python. // Chart data is injected by python via the HTML template.
const allChartData = JSON.parse('{{ all_chart_data }}') let allChartData = {};
; try {
allChartData = JSON.parse(all_chart_data_json_string);
} catch (e) {
console.error("Failed to parse all_chart_data:", e);
console.error("Problematic all_chart_data string:", all_chart_data_json_string);
}
let currentCharts = {}; let currentCharts = {};
const chartConfigs = { const chartConfigs = {
totalCost: { id: 'totalCostChart', title: '总花费', yAxisLabel: '花费 (¥)', dataKey: 'total_cost_data', fill: true }, totalCost: { id: 'totalCostChart', title: '总花费', yAxisLabel: '花费 (¥)', dataKey: 'total_cost_data', fill: true },
@@ -73,8 +79,14 @@ document.addEventListener('DOMContentLoaded', function () {
} }
// Static charts // Static charts
const staticChartData = JSON.parse('{{ static_chart_data }}') let staticChartData = {};
; try {
staticChartData = JSON.parse(static_chart_data_json_string);
} catch (e) {
console.error("Failed to parse static_chart_data:", e);
console.error("Problematic static_chart_data string:", static_chart_data_json_string);
}
Object.keys(staticChartData).forEach(period_id => { Object.keys(staticChartData).forEach(period_id => {
const providerCostData = staticChartData[period_id].provider_cost_data; const providerCostData = staticChartData[period_id].provider_cost_data;
const modelCostData = staticChartData[period_id].model_cost_data; const modelCostData = staticChartData[period_id].model_cost_data;
@@ -82,7 +94,7 @@ document.addEventListener('DOMContentLoaded', function () {
// Provider Cost Pie Chart // Provider Cost Pie Chart
const providerCtx = document.getElementById(`providerCostPieChart_${period_id}`); const providerCtx = document.getElementById(`providerCostPieChart_${period_id}`);
if (providerCtx && providerCostData && providerCostData.data.length > 0) { if (providerCtx && providerCostData && providerCostData.data && providerCostData.data.length > 0) {
new Chart(providerCtx, { new Chart(providerCtx, {
type: 'pie', type: 'pie',
data: { data: {
@@ -106,7 +118,7 @@ document.addEventListener('DOMContentLoaded', function () {
// Model Cost Bar Chart // Model Cost Bar Chart
const modelCtx = document.getElementById(`modelCostBarChart_${period_id}`); const modelCtx = document.getElementById(`modelCostBarChart_${period_id}`);
if (modelCtx && modelCostData && modelCostData.data.length > 0) { if (modelCtx && modelCostData && modelCostData.data && modelCostData.data.length > 0) {
new Chart(modelCtx, { new Chart(modelCtx, {
type: 'bar', type: 'bar',
data: { data: {