refactor(report): 优化报告样式和数据加载逻辑
- 调整了报告页面的CSS样式,包括颜色、阴影和布局,以提供更专业、现代的视觉效果。 - 改进了从后端向前端JavaScript传递图表数据的方式。现在通过一个独立的`<script>`标签注入JSON字符串,而不是直接嵌入到JS代码中,这增强了鲁棒性并避免了特殊字符导致的解析错误。 - 在JavaScript中增加了对JSON解析和图表数据有效性的检查,以防止因数据格式错误或缺失导致页面渲染失败。 - 将统计模块中的耗时相关键名统一为大写格式,以提高代码一致性。
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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, [])
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user