feat: 统计图表功能 - 人数时间线曲线图、事件类型分布、历史趋势

This commit is contained in:
2026-04-17 11:34:10 +08:00
parent 7dea6fa38f
commit a7e005bc65
4 changed files with 415 additions and 0 deletions
+130
View File
@@ -436,6 +436,136 @@ function closeSettingsModal() {
document.getElementById('settings-modal').classList.remove('active');
}
// Stats Management
function openStatsModal() {
loadDailyStats();
loadHistoryStats();
document.getElementById('stats-modal').classList.add('active');
}
function closeStatsModal() {
document.getElementById('stats-modal').classList.remove('active');
}
function loadDailyStats() {
var dateSelect = document.getElementById('stats-date-select');
var dateValue = dateSelect.value;
var targetDate;
if (dateValue === 'yesterday') {
var yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
targetDate = yesterday.toISOString().split('T')[0];
} else {
targetDate = new Date().toISOString().split('T')[0];
}
fetch(API_BASE + '/api/stats/daily?date=' + targetDate)
.then(function(res) { return res.json(); })
.then(function(data) {
renderDailyStats(data);
})
.catch(function(e) { console.error('Load stats failed:', e); });
}
function renderDailyStats(data) {
// Summary
var summaryDiv = document.getElementById('stats-summary');
summaryDiv.innerHTML = '<div class="stats-summary">' +
'<span>Date: ' + data.date + '</span>' +
'<span>Images: ' + data.total_images + '</span>' +
'<span>Events: ' + data.total_events + '</span>' +
'</div>';
// Person count timeline chart (using simple CSS bars)
var timeline = data.timeline;
var chartCanvas = document.getElementById('person-chart');
if (timeline.length === 0) {
chartCanvas.innerHTML = '<p style="color:#888;text-align:center;">No data</p>';
return;
}
// Build simple bar chart
var chartHtml = '<div class="timeline-chart">';
var maxCount = Math.max.apply(Math, timeline.map(function(t) { return t.person_count || 0 }));
maxCount = Math.max(maxCount, 1);
timeline.slice(-20).forEach(function(t) {
var timeLabel = new Date(t.time).toLocaleTimeString('en-US', {hour: '2-digit', minute: '2-digit'});
var count = t.person_count || 0;
var height = (count / maxCount) * 100;
chartHtml += '<div class="timeline-bar">' +
'<div class="bar-fill" style="height:' + height + '%"></div>' +
'<div class="bar-label">' + timeLabel + '</div>' +
'<div class="bar-value">' + count + '</div>' +
'</div>';
});
chartHtml += '</div>';
chartCanvas.innerHTML = chartHtml;
// Event types distribution
var eventTypesDiv = document.getElementById('event-types-chart');
eventTypesDiv.innerHTML = '';
if (data.event_types.length === 0) {
eventTypesDiv.innerHTML = '<p style="color:#888;">No events</p>';
return;
}
var maxEventCount = Math.max.apply(Math, data.event_types.map(function(e) { return e.count }));
data.event_types.forEach(function(et) {
var width = (et.count / maxEventCount) * 100;
var barHtml = '<div class="event-type-bar">' +
'<span class="event-type-label">' + et.type + '</span>' +
'<div class="event-type-fill" style="width:' + width + '%">' +
'<span class="event-type-count">' + et.count + '</span>' +
'</div></div>';
eventTypesDiv.innerHTML += barHtml;
});
}
function loadHistoryStats() {
fetch(API_BASE + '/api/stats/history?days=7')
.then(function(res) { return res.json(); })
.then(function(data) {
renderHistoryStats(data);
})
.catch(function(e) { console.error('Load history failed:', e); });
}
function renderHistoryStats(data) {
var historyDiv = document.getElementById('history-chart');
if (data.history.length === 0) {
historyDiv.innerHTML = '<p style="color:#888;">No history data</p>';
return;
}
var maxImages = Math.max.apply(Math, data.history.map(function(h) { return h.total_images }));
maxImages = Math.max(maxImages, 1);
var html = '<div class="history-chart">';
data.history.reverse().forEach(function(h) {
var dateLabel = h.date.slice(5); // MM-DD
var height = (h.total_images / maxImages) * 100;
html += '<div class="history-bar">' +
'<div class="bar-fill images" style="height:' + height + '%"></div>' +
'<div class="bar-label">' + dateLabel + '</div>' +
'<div class="bar-value">' + h.total_images + '</div>' +
'</div>';
});
html += '</div>';
historyDiv.innerHTML = html;
}
// Persons Management
function openPersonsModal() {
loadPersonsList();
+44
View File
@@ -34,6 +34,11 @@
<button onclick="openPersonsModal()" class="btn-settings">查看人员库</button>
</div>
<div class="panel-section">
<h3>📊 统计图表</h3>
<button onclick="openStatsModal()" class="btn-settings">查看统计</button>
</div>
<div class="panel-section">
<h3>⚙️ 系统设置</h3>
<button onclick="openSettingsModal()" class="btn-settings">🔧 设置参数</button>
@@ -235,6 +240,45 @@
</div>
</div>
<!-- 统计图表模态框 -->
<div class="modal" id="stats-modal" onclick="closeModalOnBackground(event, 'stats-modal')">
<div class="modal-content stats-modal-content" onclick="event.stopPropagation()">
<span class="modal-close" onclick="closeStatsModal()">×</span>
<h2>📊 统计图表</h2>
<div class="stats-controls">
<select id="stats-date-select">
<option value="yesterday">Yesterday</option>
<option value="today">Today</option>
</select>
<button onclick="loadDailyStats()" class="btn-small">Refresh</button>
</div>
<div class="stats-summary-box" id="stats-summary">
<!-- 统计摘要 -->
</div>
<div class="chart-container">
<h4>👥 Person Count Timeline</h4>
<canvas id="person-chart"></canvas>
</div>
<div class="chart-container">
<h4>📋 Event Types Distribution</h4>
<div id="event-types-chart">
<!-- 事件类型图表 -->
</div>
</div>
<div class="chart-container">
<h4>📈 History (Last 7 Days)</h4>
<div id="history-chart">
<!-- 历史趋势 -->
</div>
</div>
</div>
</div>
<!-- 人员库模态框 -->
<div class="modal" id="persons-modal" onclick="closeModalOnBackground(event, 'persons-modal')">
<div class="modal-content settings-modal-content" onclick="event.stopPropagation()">
+120
View File
@@ -666,6 +666,126 @@ button {
gap: 8px;
}
/* 统计图表样式 */
.stats-modal-content {
max-width: 600px;
max-height: 85vh;
overflow-y: auto;
}
.stats-controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
align-items: center;
}
.stats-summary-box {
margin-bottom: 15px;
}
.chart-container {
margin-bottom: 20px;
}
.chart-container h4 {
margin-bottom: 10px;
color: #667eea;
font-size: 13px;
}
.timeline-chart {
display: flex;
gap: 2px;
height: 150px;
align-items: flex-end;
background: #f5f5f5;
padding: 10px;
border-radius: 5px;
}
.timeline-bar {
flex: 1;
min-width: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
height: 100%;
}
.bar-fill {
background: #667eea;
width: 80%;
min-height: 5px;
border-radius: 3px 3px 0 0;
}
.bar-label {
font-size: 9px;
color: #888;
margin-top: 3px;
text-align: center;
}
.bar-value {
font-size: 10px;
color: #667eea;
font-weight: bold;
}
.history-chart {
display: flex;
gap: 5px;
height: 120px;
align-items: flex-end;
background: #f5f5f5;
padding: 10px;
border-radius: 5px;
}
.history-bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
height: 100%;
}
.bar-fill.images {
background: #4CAF50;
}
.event-type-bar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.event-type-label {
min-width: 100px;
font-size: 12px;
color: #555;
}
.event-type-fill {
background: #667eea;
height: 20px;
border-radius: 3px;
display: flex;
align-items: center;
padding-left: 8px;
min-width: 30px;
}
.event-type-count {
font-size: 12px;
color: white;
font-weight: bold;
}
/* 响应式 */
@media (max-width: 768px) {
.control-panel {