feat: 统计图表功能 - 人数时间线曲线图、事件类型分布、历史趋势
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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()">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user