Files
vision-record/web/static/app.js

782 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Vision Record System - Frontend Logic
var API_BASE = '';
var currentImageId = null;
var refreshTimer = null;
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadConfig();
refreshAll();
});
// Toast 提示(自动消失)
function showToast(message, duration) {
duration = duration || 2000;
var toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(function() {
toast.classList.add('fade-out');
setTimeout(function() {
toast.remove();
}, 300);
}, duration);
}
// 点击模态框背景关闭
function closeModalOnBackground(event, modalId) {
if (event.target.id === modalId) {
document.getElementById(modalId).classList.remove('active');
}
}
function loadConfig() {
fetch(API_BASE + '/api/config')
.then(function(res) { return res.json(); })
.then(function(config) {
var interval = config.refresh_interval || 5;
startAutoRefresh(interval);
document.getElementById('refresh-info').textContent = 'Refresh: ' + interval + 's';
})
.catch(function(e) {
console.error('Load config failed:', e);
startAutoRefresh(5);
});
}
function startAutoRefresh(intervalSeconds) {
if (refreshTimer) {
clearInterval(refreshTimer);
}
refreshTimer = setInterval(refreshAll, intervalSeconds * 1000);
}
function refreshAll() {
loadStatus();
loadImages();
loadEvents();
}
// Status
function loadStatus() {
fetch(API_BASE + '/api/status')
.then(function(res) { return res.json(); })
.then(function(data) {
updateUI(data);
})
.catch(function(e) {
console.error('Load status failed:', e);
});
}
function updateUI(data) {
// 更新状态文本
if (data.scheduler.running) {
document.getElementById('scheduler-status').innerHTML = '状态: <span style="color:#4CAF50;">运行中</span>';
} else {
document.getElementById('scheduler-status').innerHTML = '状态: <span style="color:#f44336;">已停止</span>';
}
// 更新切换按钮状态
updateToggleButton(data.scheduler.running);
document.getElementById('capture-count').textContent = '拍照次数: ' + data.scheduler.capture_count;
document.getElementById('total-images').textContent = data.stats.total_images;
document.getElementById('analyzed-images').textContent = data.stats.analyzed_images;
document.getElementById('total-events').textContent = data.stats.total_events;
// 事件类型筛选
var eventFilter = document.getElementById('event-filter');
eventFilter.innerHTML = '<option value="all">全部类型</option>';
data.stats.event_types.forEach(function(item) {
var opt = document.createElement('option');
opt.value = item.type;
opt.textContent = item.type + ' (' + item.count + ')';
eventFilter.appendChild(opt);
});
}
// Control
function toggleScheduler() {
var btn = document.getElementById('toggle-btn');
if (btn.classList.contains('stopped')) {
// 当前已停止,启动
fetch('/api/scheduler/start', {method: 'POST'})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
btn.classList.remove('stopped');
btn.classList.add('running');
btn.innerHTML = '▶ 运行中';
btn.style.background = '#4CAF50';
showToast('已启动!', 1500);
refreshAll();
} else {
showToast('启动失败: ' + data.error, 3000);
}
})
.catch(function(e) { showToast('错误: ' + e.message, 3000); });
} else {
// 当前运行中,停止
fetch('/api/scheduler/stop', {method: 'POST'})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
btn.classList.remove('running');
btn.classList.add('stopped');
btn.innerHTML = '⏹ 已停止';
btn.style.background = '#f44336';
showToast('已停止!', 1500);
refreshAll();
}
})
.catch(function(e) { showToast('错误: ' + e.message, 3000); });
}
}
function updateToggleButton(running) {
var btn = document.getElementById('toggle-btn');
if (running) {
btn.classList.remove('stopped');
btn.classList.add('running');
btn.innerHTML = '▶ 运行中';
btn.style.background = '#4CAF50';
} else {
btn.classList.remove('running');
btn.classList.add('stopped');
btn.innerHTML = '⏹ 已停止';
btn.style.background = '#f44336';
}
}
function captureNow() {
fetch('/api/capture', {method: 'POST'})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
showToast('拍照成功ID: ' + data.image_id, 1500);
refreshAll();
} else {
showToast('拍照失败: ' + data.error, 3000);
}
})
.catch(function(e) { showToast('错误: ' + e.message, 3000); });
}
function analyzeUnanalyzed() {
fetch('/api/analyze/unanalyzed', {method: 'POST'})
.then(function(res) { return res.json(); })
.then(function(data) {
showToast('分析了 ' + data.results.length + ' 张图片', 1500);
refreshAll();
})
.catch(function(e) { showToast('错误: ' + e.message, 3000); });
}
// Images
function loadImages() {
var filter = document.getElementById('image-filter').value;
var analyzedOnly = filter === 'analyzed';
fetch(API_BASE + '/api/images?limit=50&analyzed_only=' + analyzedOnly)
.then(function(res) { return res.json(); })
.then(function(data) {
renderImages(data.images);
})
.catch(function(e) { console.error('Load images failed:', e); });
}
function renderImages(images) {
var list = document.getElementById('images-list');
list.innerHTML = '';
if (images.length === 0) {
list.innerHTML = '<p style="color:#888;text-align:center;">No images</p>';
return;
}
images.forEach(function(img) {
var item = document.createElement('div');
item.className = 'image-item ' + (img.analyzed ? 'analyzed' : 'unanalyzed');
item.onclick = function() { openImageModal(img.id); };
var time = new Date(img.timestamp).toLocaleString();
var status = img.analyzed ? 'Analyzed' : 'Unanalyzed';
var events = img.events_summary || 'No events';
// Check for person indices in events
var personIndices = [];
if (img.events && img.events.length > 0) {
img.events.forEach(function(event) {
var match = event.description.match(/#(\d+)/g);
if (match) {
match.forEach(function(m) {
if (personIndices.indexOf(m) === -1) {
personIndices.push(m);
}
});
}
});
}
var indicesDisplay = personIndices.length > 0 ?
'<span class="image-person-indices">' + personIndices.slice(0, 3).join(' ') + '</span>' : '';
item.innerHTML = '<span class="image-number">#' + img.id + '</span>' +
'<span class="image-time">' + time + '</span>' +
'<span class="image-status">' + status + '</span>' +
indicesDisplay +
'<span class="image-events-summary">' + events + '</span>';
list.appendChild(item);
});
}
function openImageModal(imageId) {
currentImageId = imageId;
fetch(API_BASE + '/api/images/' + imageId)
.then(function(res) { return res.json(); })
.then(function(data) {
var filename = data.path.split('/').pop().split('\\').pop();
document.getElementById('modal-image').src = '/images/' + filename;
document.getElementById('modal-title').textContent = 'Image #' + data.id;
document.getElementById('modal-time').textContent = 'Time: ' + new Date(data.timestamp).toLocaleString();
var eventsDiv = document.getElementById('modal-events');
eventsDiv.innerHTML = '';
if (data.events && data.events.length > 0) {
// 分组显示
var localEvents = [];
var aiEvents = [];
data.events.forEach(function(event) {
if (event.event_type.indexOf('(本地)') >= 0) {
localEvents.push(event);
} else if (event.event_type.indexOf('(AI)') >= 0) {
aiEvents.push(event);
} else {
localEvents.push(event);
}
});
// 本地分析结果
if (localEvents.length > 0) {
var localSection = document.createElement('div');
localSection.className = 'modal-events-section';
// 显示人员序号
var personIndices = [];
localEvents.forEach(function(event) {
if (event.person_index) {
personIndices.push('#' + event.person_index);
}
});
var indicesDisplay = personIndices.length > 0 ?
' <span class="person-indices">[' + personIndices.join(', ') + ']</span>' : '';
localSection.innerHTML = '<h4 class="section-title local">Local Analysis (' + localEvents.length + ')' + indicesDisplay + '</h4>';
localEvents.forEach(function(event) {
var div = document.createElement('div');
div.className = 'modal-event source-local';
var eventType = event.event_type.replace('(本地)', '').trim();
div.innerHTML = '<span class="event-type">' + eventType + '</span> ' +
'<span class="event-confidence">' + event.confidence + '</span>' +
'<div>' + event.description + '</div>';
localSection.appendChild(div);
});
eventsDiv.appendChild(localSection);
}
// AI分析结果
if (aiEvents.length > 0) {
var aiSection = document.createElement('div');
aiSection.className = 'modal-events-section';
aiSection.innerHTML = '<h4 class="section-title ai">AI Analysis (' + aiEvents.length + ') </h4>';
aiEvents.forEach(function(event) {
var div = document.createElement('div');
div.className = 'modal-event source-ai';
var eventType = event.event_type.replace('(AI)', '').trim();
div.innerHTML = '<span class="event-type">' + eventType + '</span> ' +
'<span class="event-confidence">' + event.confidence + '</span>' +
'<div>' + event.description + '</div>';
aiSection.appendChild(div);
});
eventsDiv.appendChild(aiSection);
}
} else {
eventsDiv.innerHTML = '<p style="color:#888;">No events recorded</p>';
}
document.getElementById('image-modal').classList.add('active');
})
.catch(function(e) { console.error('Load image failed:', e); });
}
function closeImageModal() {
document.getElementById('image-modal').classList.remove('active');
currentImageId = null;
}
function analyzeCurrentImage() {
if (!currentImageId) return;
fetch(API_BASE + '/api/analyze/' + currentImageId, {method: 'POST'})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
alert('Analyzed!');
openImageModal(currentImageId);
refreshAll();
} else {
alert('Failed: ' + data.error);
}
})
.catch(function(e) { alert('Error: ' + e.message); });
}
function deleteCurrentImage() {
if (!currentImageId) return;
if (!confirm('Delete this image?')) return;
fetch(API_BASE + '/api/images/' + currentImageId, {method: 'DELETE'})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
closeImageModal();
refreshAll();
}
})
.catch(function(e) { alert('Error: ' + e.message); });
}
// Events
function loadEvents() {
var filter = document.getElementById('event-filter').value;
var eventType = filter === 'all' ? '' : filter;
var url = API_BASE + '/api/events?limit=50';
if (eventType) {
url += '&event_type=' + encodeURIComponent(eventType);
}
fetch(url)
.then(function(res) { return res.json(); })
.then(function(data) {
renderEvents(data.events);
})
.catch(function(e) { console.error('Load events failed:', e); });
}
function renderEvents(events) {
var list = document.getElementById('events-list');
list.innerHTML = '';
if (events.length === 0) {
list.innerHTML = '<p style="color:#888;text-align:center;">No events</p>';
return;
}
// 分组显示本地分析和AI分析
var localEvents = [];
var aiEvents = [];
events.forEach(function(event) {
if (event.event_type.indexOf('(本地)') >= 0 || event.event_type.indexOf('(local)') >= 0) {
localEvents.push(event);
} else if (event.event_type.indexOf('(AI)') >= 0) {
aiEvents.push(event);
} else {
localEvents.push(event);
}
});
// 显示本地分析结果
if (localEvents.length > 0) {
var localHeader = document.createElement('div');
localHeader.className = 'events-header';
localHeader.innerHTML = '<span class="header-label local">Local Analysis</span><span class="header-count">' + localEvents.length + ' events</span>';
list.appendChild(localHeader);
localEvents.forEach(function(event) {
var card = createEventCard(event, 'local');
list.appendChild(card);
});
}
// 显示AI分析结果
if (aiEvents.length > 0) {
var aiHeader = document.createElement('div');
aiHeader.className = 'events-header';
aiHeader.innerHTML = '<span class="header-label ai">AI Analysis</span><span class="header-count">' + aiEvents.length + ' events</span>';
list.appendChild(aiHeader);
aiEvents.forEach(function(event) {
var card = createEventCard(event, 'ai');
list.appendChild(card);
});
}
}
function createEventCard(event, source) {
var card = document.createElement('div');
card.className = 'event-card type-' + event.event_type.replace('(本地)', '').replace('(AI)', '').trim() + ' source-' + source;
card.onclick = function() { openImageModal(event.image_id); };
var time = new Date(event.timestamp).toLocaleString();
var eventType = event.event_type.replace('(本地)', '').replace('(AI)', '').trim();
card.innerHTML = '<div class="event-header">' +
'<span class="event-type">' + eventType + '</span>' +
'<span class="event-source">' + (source === 'local' ? 'Local' : 'AI') + '</span>' +
'<span class="event-time">' + time + '</span></div>' +
'<div class="event-desc">' + event.description + '</div>' +
'<div class="event-confidence">' + event.confidence + '</div>';
return card;
}
// Filter listeners
document.getElementById('image-filter').addEventListener('change', loadImages);
document.getElementById('event-filter').addEventListener('change', loadEvents);
// Settings
function openSettingsModal() {
loadSettingsForm();
document.getElementById('settings-modal').classList.add('active');
}
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();
document.getElementById('persons-modal').classList.add('active');
}
function closePersonsModal() {
document.getElementById('persons-modal').classList.remove('active');
}
function loadPersonsList() {
fetch(API_BASE + '/api/persons')
.then(function(res) { return res.json(); })
.then(function(data) {
var statsDiv = document.getElementById('persons-stats');
var listDiv = document.getElementById('persons-list');
// 统计信息
statsDiv.innerHTML = '<div class="stats-summary">' +
'<span>Total: ' + data.stats.total_persons + '</span>' +
'<span>Detected: ' + data.stats.total_detections + '</span>' +
'<span>Known: ' + data.stats.known_persons_detected + '</span>' +
'<span>New: ' + data.stats.new_persons_added + '</span>' +
'</div>';
// 人员列表
if (data.persons.length === 0) {
listDiv.innerHTML = '<p style="color:#888;text-align:center;">No persons recorded yet</p>';
return;
}
listDiv.innerHTML = '';
data.persons.forEach(function(person) {
var item = document.createElement('div');
item.className = 'person-item';
var firstSeen = new Date(person.first_seen).toLocaleString();
var lastSeen = new Date(person.last_seen).toLocaleString();
// 构建人员图片URL
var faceImgHtml = '<div class="person-face-placeholder">No Image</div>';
if (person.face_path) {
faceImgHtml = '<img class="person-face-img" src="/api/persons/' + person.person_id + '/face" alt="' + person.name + '" onerror="this.parentElement.innerHTML=\'<div class=&quot;person-face-placeholder&quot;>No Image</div>\'">';
}
item.innerHTML = '<div class="person-face">' + faceImgHtml + '</div>' +
'<div class="person-content">' +
'<div class="person-info">' +
'<span class="person-name">' + person.name + '</span>' +
'<span class="person-id">' + person.person_id + '</span>' +
'</div>' +
'<div class="person-stats">' +
'<span>Visits: ' + person.visit_count + '</span>' +
'<span>First: ' + firstSeen + '</span>' +
'<span>Last: ' + lastSeen + '</span>' +
'</div>' +
'<div class="person-actions">' +
'<button onclick="renamePerson(\'' + person.person_id + '\')" class="btn-small">Rename</button>' +
'<button onclick="deletePerson(\'' + person.person_id + '\')" class="btn-small btn-danger">Delete</button>' +
'</div>' +
'</div>';
listDiv.appendChild(item);
});
})
.catch(function(e) { console.error('Load persons failed:', e); });
}
function renamePerson(personId) {
var newName = prompt('Enter new name:');
if (!newName) return;
fetch(API_BASE + '/api/persons/' + personId + '/rename?name=' + encodeURIComponent(newName), {
method: 'POST'
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
showToast('Renamed to ' + newName, 1500);
loadPersonsList();
}
})
.catch(function(e) { showToast('Error: ' + e.message, 3000); });
}
function deletePerson(personId) {
if (!confirm('Delete this person?')) return;
fetch(API_BASE + '/api/persons/' + personId, {method: 'DELETE'})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
showToast('Person deleted', 1500);
loadPersonsList();
}
})
.catch(function(e) { showToast('Error: ' + e.message, 3000); });
}
function loadSettingsForm() {
fetch(API_BASE + '/api/config')
.then(function(res) { return res.json(); })
.then(function(config) {
document.getElementById('setting-images-dir').value = config.images_dir || '';
document.getElementById('setting-interval').value = config.capture_interval || 60;
document.getElementById('setting-camera-index').value = config.camera_index || 0;
document.getElementById('setting-auto-analyze').checked = config.auto_analyze !== false;
document.getElementById('setting-display-limit').value = config.display_limit || 20;
document.getElementById('setting-refresh-interval').value = config.refresh_interval || 5;
// Person Detection (YOLO)
document.getElementById('setting-use-yolo').checked = config.use_yolo !== false;
document.getElementById('setting-yolo-confidence').value = config.yolo_min_confidence || 0.3;
// Person Identification
document.getElementById('setting-use-face-rec').checked = config.use_face_recognition !== false;
document.getElementById('setting-use-mediapipe').checked = config.use_mediapipe_face !== false;
document.getElementById('setting-use-color-hist').checked = config.use_color_histogram !== false;
document.getElementById('setting-match-threshold').value = config.face_match_threshold || 0.6;
// Confirmation Settings
document.getElementById('setting-confirm-frames').value = config.confirm_frames || 3;
document.getElementById('setting-leave-frames').value = config.leave_frames || 2;
// Vision API settings
document.getElementById('setting-use-vision-api').checked = config.use_vision_api === true;
document.getElementById('setting-vision-trigger').value = config.vision_api_trigger || 'person_change';
// API config
document.getElementById('setting-api-url').value = config.vision_api_url || '';
document.getElementById('setting-api-key').value = config.vision_api_key || '';
document.getElementById('setting-model').value = config.vision_model || '';
})
.catch(function(e) { console.error('Load settings failed:', e); });
}
function saveSettings() {
var settings = {
images_dir: document.getElementById('setting-images-dir').value,
capture_interval: parseInt(document.getElementById('setting-interval').value),
camera_index: parseInt(document.getElementById('setting-camera-index').value),
auto_analyze: document.getElementById('setting-auto-analyze').checked,
display_limit: parseInt(document.getElementById('setting-display-limit').value),
refresh_interval: parseInt(document.getElementById('setting-refresh-interval').value),
// Person Detection (YOLO)
use_yolo: document.getElementById('setting-use-yolo').checked,
yolo_min_confidence: parseFloat(document.getElementById('setting-yolo-confidence').value),
// Person Identification
use_face_recognition: document.getElementById('setting-use-face-rec').checked,
use_mediapipe_face: document.getElementById('setting-use-mediapipe').checked,
use_color_histogram: document.getElementById('setting-use-color-hist').checked,
face_match_threshold: parseFloat(document.getElementById('setting-match-threshold').value),
// Confirmation Settings
confirm_frames: parseInt(document.getElementById('setting-confirm-frames').value),
leave_frames: parseInt(document.getElementById('setting-leave-frames').value),
// Vision API
use_vision_api: document.getElementById('setting-use-vision-api').checked,
vision_api_trigger: document.getElementById('setting-vision-trigger').value,
// API config
vision_api_url: document.getElementById('setting-api-url').value,
vision_api_key: document.getElementById('setting-api-key').value,
vision_model: document.getElementById('setting-model').value
};
fetch(API_BASE + '/api/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(settings)
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
showToast('Settings saved!', 1500);
closeSettingsModal();
loadConfig();
refreshAll();
}
})
.catch(function(e) { showToast('Error: ' + e.message, 3000); });
}
function browseImagesDir() {
alert('Please enter the path manually.\nExample: D:\\vision-images\nImages will be saved to date subfolders automatically.');
}