GAP / app /web /templates /report.html
misonL's picture
Upload 52 files
e82bac2 verified
Raw
History Blame Contribute Delete
32.7 kB
{% if not is_spa_request %}
{% extends "_base.html" %}
{% endif %}
{% block content %}
{# 将所有内容包裹在 page-content-wrapper 中,以便SPA提取 #}
<div id="page-content-wrapper">
<div class="container-lg mt-4 px-lg-5 px-md-4 px-3">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>API 使用情况报告</h1>
<small class="text-muted">最后更新时间: <span id="last-updated-time">加载中...</span></small>
</div>
<div class="row">
<!-- 左列 -->
<div class="col-lg-6">
<!-- 容量与使用情况 -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">容量与使用情况</h5>
</div>
<div class="card-body">
<div class="row align-items-center">
<!-- RPD 图表 -->
<div class="col-md-6 text-center mb-3 mb-md-0"> <!-- 调整为 col-md-6 -->
<h6>RPD (每日请求)</h6>
<div class="chart-container mx-auto" style="position: relative;">
<canvas id="rpd-chart"></canvas>
<div id="rpd-percentage" class="chart-percentage">--%</div>
</div>
<p id="rpd-stats" class="mt-2 small text-muted">加载中...</p>
</div>
<!-- TPD 输入图表 -->
<div class="col-md-6 text-center mb-3 mb-md-0"> <!-- 调整为 col-md-6 -->
<h6>TPD 输入 (每日 Token)</h6>
<div class="chart-container mx-auto" style="position: relative;">
<canvas id="tpd-input-chart"></canvas>
<div id="tpd-input-percentage" class="chart-percentage">--%</div>
</div>
<p id="tpd-input-stats" class="mt-2 small text-muted">加载中...</p>
</div>
<!-- 关键统计数据 -->
<div class="col-12 mt-4"> <!-- 调整为 col-12 并添加上边距 -->
<h6>关键指标</h6>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
活跃 Key 数量:
<span id="active-keys-count" class="badge bg-primary rounded-pill">...</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
启动时无效 Key:
<span id="invalid-keys-startup" class="badge bg-warning text-dark rounded-pill">...</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
过去 7 天平均日 RPD:
<span id="avg-daily-rpd" class="badge bg-info rounded-pill">...</span>
</li>
<!-- RPD 容量列表容器 -->
<li id="rpd-capacity-list-container" class="list-group-item">
RPD 容量估算:
{% if has_valid_keys %}
<div class="small text-muted mt-1">加载中...</div>
{% else %}
<div class="small mt-1">无有效 API Key,无法估算容量</div> {# 无有效 Key 时的提示 #}
{% endif %}
</li>
<!-- TPD 输入容量列表容器 -->
<li id="tpd-input-capacity-list-container" class="list-group-item">
TPD 输入容量估算:
{% if has_valid_keys %}
<div class="small text-muted mt-1">加载中...</div>
{% else %}
<div class="small mt-1">无有效 API Key,无法估算容量</div> {# 无有效 Key 时的提示 #}
{% endif %}
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Key 使用概览 -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">Key 使用概览</h5>
</div>
<div class="card-body">
<div id="key-status-charts" class="row justify-content-center">
<!-- 图表将动态添加到这里 -->
<p id="key-status-loading" class="text-center text-muted">正在加载 Key 状态图表...</p>
</div>
<hr>
<h6>状态详情</h6>
<div id="key-status-details" class="mt-3 small">
<!-- 详细状态文本将添加到这里 -->
<p class="text-center text-muted">暂无详情数据。</p>
</div>
</div>
</div>
</div>
<!-- 右列 -->
<div class="col-lg-6">
<!-- Key 数量建议 -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">Key 数量建议</h5>
</div>
<div class="card-body">
<p id="key-suggestion" class="lead">正在加载建议...</p>
</div>
</div>
<!-- Top IP 统计 -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">Top 5 IP 地址统计 (PT 时间)</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-12 mb-4">
<h6>请求次数</h6>
<div class="table-responsive">
<table class="table table-sm table-striped table-hover">
<thead class="table-light">
<tr><th>时间范围</th><th>IP 地址</th><th>次数</th></tr>
</thead>
<tbody id="top-ips-requests">
<tr><td colspan="3" class="text-center text-muted">正在加载...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="col-12">
<h6>输入 Token</h6>
<div class="table-responsive">
<table class="table table-sm table-striped table-hover">
<thead class="table-light">
<tr><th>时间范围</th><th>IP 地址</th><th>Token 数</th></tr>
</thead>
<tbody id="top-ips-tokens">
<tr><td colspan="3" class="text-center text-muted">正在加载...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Key 筛选原因统计 -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">Key 筛选原因统计</h5>
</div>
<div class="card-body">
<h6>按原因汇总</h6>
<ul id="key-selection-reasons-summary" class="list-group list-group-flush mb-4">
<li class="list-group-item text-center text-muted">正在加载...</li>
</ul>
<h6>按 Key 详细统计</h6>
<div class="table-responsive">
<table class="table table-sm table-striped table-hover">
<thead class="table-light">
<tr>
<th>Key ID (部分)</th>
<th>筛选次数</th>
<th>主要原因</th>
</tr>
</thead>
<tbody id="key-selection-details">
<tr><td colspan="3" class="text-center text-muted">正在加载...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* 图表容器样式 */
/* Chart container styles */
.chart-container {
position: relative;
width: 100%; /* 确保容器宽度为 100% */
max-width: 200px; /* 添加最大宽度以控制大小 */
margin: auto;
/* 为图表添加更明显的阴影效果 */
/* Add a more prominent shadow effect to charts */
filter: drop-shadow(0px 8px 15px rgba(0, 0, 0, 0.2)) drop-shadow(0px 3px 6px rgba(0, 0, 0, 0.15)); /* 加深阴影 */
}
/* 图表中心显示百分比的样式 */
/* Style to display percentage in the center of doughnut chart */
.chart-percentage {
position: absolute;
top: 55%; /* 调整垂直位置,使其更居中于半圆 */
left: 50%; /* 恢复水平居中 */
/* 微调垂直位置,使其向下偏移 */
/* Finely tune vertical position to shift it downwards */
transform: translate(-50%, -50%); /* 调整为标准水平垂直居中 */
font-size: 1.2rem; /* 恢复默认字体大小或更小 */
font-weight: bold;
color: #333; /* 恢复默认颜色 */
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); /* 添加文本阴影 */
pointer-events: none; /* 确保不影响图表交互 */
}
/* 确保 Key 状态图表在列内响应式 */
#key-status-charts .col-lg-4,
#key-status-charts .col-md-6,
#key-status-charts .col-sm-6 {
/* 可以根据需要调整图表大小 */
max-width: 200px; /* 限制图表容器的最大宽度 */
margin-left: auto;
margin-right: auto;
}
#key-status-charts canvas {
max-height: 150px; /* 限制 Canvas 高度 */
}
/* 修正 Bootstrap 栅格列在宽屏下的宽度优先级问题 */
@media (min-width: 992px) { /* Bootstrap's 'lg' breakpoint */
.row .col-lg-6 { /* Increased specificity to override .row > * */
flex: 0 0 auto; /* Ensure flex properties are set */
width: 50% !important; /* Force width */
}
}
#active-keys-count,
#invalid-keys-startup,
#avg-daily-rpd {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* 优化关键指标徽章的阴影效果 */
}
</style>
<script>
// 将所有页面特定的JS逻辑封装到这个初始化函数中
async function initializeManageReportPage() {
console.log('Report Page: initializeManageReportPage() called.');
const token = localStorage.getItem('access_token');
if (!token) {
console.log('Report Page: No token found, redirecting to login.');
window.location.href = '/'; // 重定向到登录页
return;
} else {
console.log('Report Page: Token found.');
}
let rpdChartInstance = null;
let tpdInputChartInstance = null;
let keyStatusChartInstances = {};
function formatNumber(num) {
if (num === null || num === undefined || isNaN(num)) {
return 'N/A';
}
const numberValue = parseFloat(num);
if (isNaN(numberValue)) {
return 'N/A';
}
return numberValue.toLocaleString();
}
function renderKeyStatusCharts(keyUsageSummaryData) {
const chartsContainer = document.getElementById('key-status-charts');
const detailsContainer = document.getElementById('key-status-details');
if (chartsContainer) chartsContainer.innerHTML = '';
if (detailsContainer) detailsContainer.innerHTML = '';
keyStatusChartInstances = {};
const modelsData = keyUsageSummaryData?.models;
if (!modelsData || !Array.isArray(modelsData) || modelsData.length === 0) {
if (chartsContainer) chartsContainer.innerHTML = '<p class="text-center text-muted col-12">暂无 Key 使用数据可供可视化。</p>';
if (detailsContainer) detailsContainer.innerHTML = '<p class="text-center text-muted">暂无详情数据。</p>';
return;
}
modelsData.forEach(modelSummary => {
const modelName = modelSummary.model_name;
const totalKeys = modelSummary.key_count;
const statusDistribution = modelSummary.status_distribution;
if (!modelName || !statusDistribution || !Array.isArray(statusDistribution) || statusDistribution.length === 0) {
console.warn(`Skipping rendering chart for model with incomplete data: ${modelName}`, modelSummary);
return;
}
const chartCol = document.createElement('div');
chartCol.className = 'col-lg-4 col-md-6 col-sm-6 mb-4 text-center';
const chartTitle = document.createElement('h6');
chartTitle.textContent = `${modelName} (${formatNumber(totalKeys)} Keys)`;
const canvas = document.createElement('canvas');
canvas.id = `key-status-chart-${modelName}`;
canvas.style.margin = '0 auto';
chartCol.appendChild(chartTitle);
chartCol.appendChild(canvas);
if (chartsContainer) chartsContainer.appendChild(chartCol);
const labels = statusDistribution.map(item => item.status);
const dataCounts = statusDistribution.map(item => item.count);
const backgroundColors = [
'rgba(40, 167, 69, 0.7)',
'rgba(255, 193, 7, 0.7)',
'rgba(220, 53, 69, 0.7)',
'rgba(0, 123, 255, 0.7)',
'rgba(108, 117, 125, 0.7)',
'rgba(111, 66, 193, 0.7)'
];
const borderColors = backgroundColors.map(color => color.replace('0.7', '1'));
const ctx = canvas.getContext('2d');
keyStatusChartInstances[modelName] = new Chart(ctx, {
type: 'pie',
data: {
labels: labels.map(label => label.length > 20 ? `${label.substring(0, 18)}...` : label),
datasets: [{
label: 'Key 数量',
data: dataCounts,
backgroundColor: backgroundColors.slice(0, dataCounts.length),
borderColor: borderColors.slice(0, dataCounts.length),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 10,
padding: 15,
font: {
size: 10
}
}
},
tooltip: {
callbacks: {
label: function(context) {
let originalLabel = labels[context.dataIndex] || '';
let value = context.parsed || 0;
return `${originalLabel}: ${formatNumber(value)}`;
}
}
}
}
}
});
const detailDiv = document.createElement('div');
detailDiv.className = 'mb-3 border-start ps-3';
let detailHtml = `<h6>${modelName}</h6><ul>`;
statusDistribution.forEach(item => {
detailHtml += `<li>${item.status}: ${formatNumber(item.count)}</li>`;
});
detailHtml += `</ul>`;
detailDiv.innerHTML = detailHtml;
if (detailsContainer) detailsContainer.appendChild(detailDiv);
});
if (Object.keys(keyStatusChartInstances).length === 0) {
if (chartsContainer) chartsContainer.innerHTML = '<p class="text-center text-muted col-12">暂无 Key 使用数据可供可视化。</p>';
if (detailsContainer) detailsContainer.innerHTML = '<p class="text-center text-muted">暂无详情数据。</p>';
}
}
function renderCapacityCharts(rpdData, tpdInputData) {
const rpdCanvas = document.getElementById('rpd-chart');
const tpdInputCanvas = document.getElementById('tpd-input-chart');
const rpdPercentageDiv = document.getElementById('rpd-percentage');
const tpdInputPercentageDiv = document.getElementById('tpd-input-percentage');
const rpdStatsElem = document.getElementById('rpd-stats');
const tpdInputStatsElem = document.getElementById('tpd-input-stats');
if (rpdChartInstance) rpdChartInstance.destroy();
if (tpdInputChartInstance) tpdInputChartInstance.destroy();
const rpdCapacity = rpdData?.capacity || 0;
const rpdUsedToday = rpdData?.used_today || 0;
const rpdEstimatedToday = rpdData?.estimated_today;
if (rpdCanvas) {
const rpdCtx = rpdCanvas.getContext('2d');
const rpdUsedValue = (typeof rpdEstimatedToday === 'number' && rpdEstimatedToday > 0) ? rpdEstimatedToday : rpdUsedToday;
const rpdUsageRatio = (rpdCapacity > 0) ? (rpdUsedValue / rpdCapacity) * 100 : 0;
const rpdRemainingRatio = Math.max(0, 100 - rpdUsageRatio);
rpdChartInstance = new Chart(rpdCtx, {
type: 'doughnut',
data: {
labels: ['已用/预估', '剩余'],
datasets: [{
data: [rpdUsageRatio, rpdRemainingRatio],
backgroundColor: ['rgba(220, 53, 69, 0.7)', 'rgba(76, 175, 80, 0.8)'],
borderColor: ['rgba(220, 53, 69, 1)', 'rgba(76, 175, 80, 1)'],
borderWidth: 1,
circumference: 180,
rotation: -90,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `${context.label}: ${context.parsed.toFixed(1)}%`;
}
}
},
legend: { display: false }
},
cutout: '70%'
}
});
if (rpdPercentageDiv) rpdPercentageDiv.textContent = `${rpdUsageRatio.toFixed(1)}%`;
if (rpdStatsElem) rpdStatsElem.textContent = `已用: ${formatNumber(rpdUsedToday)}, 预估: ${formatNumber(rpdEstimatedToday) || 'N/A'} / 容量: ${formatNumber(rpdCapacity)}`;
} else {
if (rpdStatsElem) rpdStatsElem.textContent = `已用: ${formatNumber(rpdUsedToday)} / 容量: ${formatNumber(rpdCapacity) || 'N/A'}`;
if (rpdPercentageDiv) rpdPercentageDiv.textContent = '--%';
}
const tpdInputCapacity = tpdInputData?.tpd_input_capacity || 0;
const tpdInputUsedToday = tpdInputData?.tpd_input_used_today || 0;
const tpdInputEstimatedToday = tpdInputData?.tpd_input_estimated_today;
if (tpdInputCanvas) {
const tpdInputCtx = tpdInputCanvas.getContext('2d');
const tpdInputUsedValue = (typeof tpdInputEstimatedToday === 'number' && tpdInputEstimatedToday > 0) ? tpdInputEstimatedToday : tpdInputUsedToday;
const tpdInputUsageRatio = (tpdInputCapacity > 0) ? (tpdInputUsedValue / tpdInputCapacity) * 100 : 0;
const tpdInputRemainingRatio = Math.max(0, 100 - tpdInputUsageRatio);
tpdInputChartInstance = new Chart(tpdInputCtx, {
type: 'doughnut',
data: {
labels: ['已用/预估', '剩余'],
datasets: [{
data: [tpdInputUsageRatio, tpdInputRemainingRatio],
backgroundColor: ['rgba(220, 53, 69, 0.7)', 'rgba(76, 175, 80, 0.8)'],
borderColor: ['rgba(220, 53, 69, 1)', 'rgba(76, 175, 80, 1)'],
borderWidth: 1,
circumference: 180,
rotation: -90,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
tooltip: {
callbacks: {
label: function(context) {
return `${context.label}: ${context.parsed.toFixed(1)}%`;
}
}
},
legend: { display: false }
},
cutout: '70%'
}
});
if (tpdInputPercentageDiv) tpdInputPercentageDiv.textContent = `${tpdInputUsageRatio.toFixed(1)}%`;
if (tpdInputStatsElem) tpdInputStatsElem.textContent = `已用: ${formatNumber(tpdInputUsedToday)}, 预估: ${formatNumber(tpdInputEstimatedToday) || 'N/A'} / 容量: ${formatNumber(tpdInputCapacity)}`;
} else {
if (tpdInputStatsElem) tpdInputStatsElem.textContent = `已用: ${formatNumber(tpdInputUsedToday)} / 容量: ${formatNumber(tpdInputCapacity) || 'N/A'}`;
if (tpdInputPercentageDiv) tpdInputPercentageDiv.textContent = '--%';
}
}
function renderKeySuggestion(suggestionData) {
const suggestionElem = document.getElementById('key-suggestion');
if (suggestionElem) {
if (suggestionData && suggestionData.suggestion) {
suggestionElem.textContent = suggestionData.suggestion;
} else {
suggestionElem.textContent = '暂无 Key 数量建议。';
}
};
}
function renderTopIps(topIPsData) {
const requestsTbody = document.getElementById('top-ips-requests');
const tokensTbody = document.getElementById('top-ips-tokens');
if (requestsTbody) requestsTbody.innerHTML = '';
if (tokensTbody) tokensTbody.innerHTML = '';
if (topIPsData && topIPsData.requests && topIPsData.requests.length > 0) {
topIPsData.requests.forEach(item => {
const row = requestsTbody.insertRow();
row.insertCell().textContent = item.time_range;
row.insertCell().textContent = item.ip_address;
row.insertCell().textContent = formatNumber(item.request_count);
});
} else {
const row = requestsTbody.insertRow();
const cell = row.insertCell();
cell.colSpan = 3;
cell.className = 'text-center text-muted';
cell.textContent = '暂无请求统计数据。';
}
if (topIPsData && topIPsData.tokens && topIPsData.tokens.length > 0) {
topIPsData.tokens.forEach(item => {
const row = tokensTbody.insertRow();
row.insertCell().textContent = item.time_range;
row.insertCell().textContent = item.ip_address;
row.insertCell().textContent = formatNumber(item.token_count);
});
} else {
const row = tokensTbody.insertRow();
const cell = row.insertCell();
cell.colSpan = 3;
cell.className = 'text-center text-muted';
cell.textContent = '暂无 Token 统计数据。';
}
}
function renderKeySelectionStats(keySelectionStatsData) {
const reasonsSummaryList = document.getElementById('key-selection-reasons-summary');
const detailsTableBody = document.getElementById('key-selection-details');
if (reasonsSummaryList) reasonsSummaryList.innerHTML = '';
if (detailsTableBody) detailsTableBody.innerHTML = '';
if (!keySelectionStatsData ||
(!keySelectionStatsData.total_by_reason || Object.keys(keySelectionStatsData.total_by_reason).length === 0) &&
(!keySelectionStatsData.details_by_key || Object.keys(keySelectionStatsData.details_by_key).length === 0))
{
if (reasonsSummaryList) reasonsSummaryList.innerHTML = '<li class="list-group-item text-center text-muted">暂无 Key 筛选原因统计数据。</li>';
if (detailsTableBody) detailsTableBody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">暂无 Key 筛选详细统计数据。</td></tr>';
return;
}
const reasonsSummary = keySelectionStatsData.total_by_reason || {};
if (Object.keys(reasonsSummary).length > 0) {
const sortedReasons = Object.entries(reasonsSummary).sort((a, b) => a[0].localeCompare(b[0]));
sortedReasons.forEach(([reason, count]) => {
const listItem = document.createElement('li');
listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
listItem.textContent = `${reason}:`;
const badge = document.createElement('span');
badge.className = 'badge bg-secondary rounded-pill';
badge.textContent = formatNumber(count);
listItem.appendChild(badge);
if (reasonsSummaryList) reasonsSummaryList.appendChild(listItem);
});
} else {
if (reasonsSummaryList) reasonsSummaryList.innerHTML = '<li class="list-group-item text-center text-muted">暂无按原因汇总数据。</li>';
}
const keyDetails = keySelectionStatsData.details_by_key || {};
if (Object.keys(keyDetails).length > 0) {
const sortedKeyDetails = Object.entries(keyDetails).sort((a, b) => a[0].localeCompare(b[0]));
sortedKeyDetails.forEach(([keyId, reasonsCounter]) => {
let mainReason = '未知原因';
let maxCount = 0;
let totalCountForKey = 0;
for (const reason in reasonsCounter) {
const count = reasonsCounter[reason];
totalCountForKey += count;
if (count > maxCount) {
maxCount = count;
mainReason = reason;
}
}
const row = detailsTableBody.insertRow();
row.insertCell().textContent = keyId;
row.insertCell().textContent = formatNumber(totalCountForKey);
row.insertCell().textContent = mainReason;
});
} else {
if (detailsTableBody) detailsTableBody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">暂无按 Key 详细统计数据。</td></tr>';
}
}
async function fetchReportData() {
document.getElementById('last-updated-time').textContent = '加载中...';
try {
const response = await fetch('/api/manage/report/data', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
console.error('认证失败或无权限,重定向到登录页。');
window.location.href = '/';
return;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('报告数据获取成功:', data);
const now = new Date();
document.getElementById('last-updated-time').textContent = now.toLocaleString();
renderCapacityCharts(data.overall_stats, data.overall_stats);
renderKeyStatusCharts(data.key_usage_summary);
renderKeySuggestion(data.key_suggestion);
renderTopIps(data.top_ips);
renderKeySelectionStats(data.key_selection_tracking);
} catch (error) {
console.error('获取报告数据失败:', error);
document.getElementById('last-updated-time').textContent = '加载失败';
document.getElementById('rpd-stats').textContent = '数据加载失败';
document.getElementById('tpd-input-stats').textContent = '数据加载失败';
document.getElementById('key-status-loading').textContent = '数据加载失败';
document.getElementById('key-status-details').innerHTML = '<p class="text-center text-danger">数据加载失败。</p>';
document.getElementById('key-suggestion').textContent = '数据加载失败。';
document.getElementById('top-ips-requests').innerHTML = '<tr><td colspan="3" class="text-center text-danger">数据加载失败。</td></tr>';
document.getElementById('top-ips-tokens').innerHTML = '<tr><td colspan="3" class="text-center text-danger">数据加载失败。</td></tr>';
document.getElementById('key-selection-reasons-summary').innerHTML = '<li class="list-group-item text-center text-danger">数据加载失败。</li>';
document.getElementById('key-selection-details').innerHTML = '<tr><td colspan="3" class="text-center text-danger">数据加载失败。</td></tr>';
}
}
// document.addEventListener('DOMContentLoaded', fetchReportData); // 移除,由 initializeManageReportPage 控制
// Chart.js 插件注册移到 initializeManageReportPage 内部,确保 Chart 对象已加载
if (typeof Chart !== 'undefined') {
Chart.register({
id: 'centerText',
beforeDraw: function(chart) {
if (chart.config.options.cutout !== '70%' || chart.config.options.circumference !== 180) {
return;
}
const ctx = chart.ctx;
const width = chart.width;
const height = chart.height;
const centerX = width / 2;
const centerY = height * 0.7;
ctx.restore();
const fontSize = (height / 114).toFixed(2);
ctx.font = `${fontSize}em sans-serif`;
ctx.textBaseline = "middle";
const text = chart.config.options.plugins.centerText?.text || '';
const textX = centerX;
const textY = centerY;
ctx.fillText(text, textX, textY);
ctx.save();
}
});
} else {
console.warn("Chart.js 未定义,无法注册 centerText 插件。");
}
fetchReportData(); // 在函数定义后立即调用以加载初始数据
} // initializeManageReportPage 函数结束
if (typeof initializeManageReportPage === 'function') {
console.log('Manage Report (content script): Calling initializeManageReportPage.');
initializeManageReportPage();
}
</script>
</div>
{% endblock %}