// 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 = '状态: 运行中'; } else { document.getElementById('scheduler-status').innerHTML = '状态: 已停止'; } // 更新切换按钮状态 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 = ''; 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 = '

No images

'; 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 ? '' + personIndices.slice(0, 3).join(' ') + '' : ''; item.innerHTML = '#' + img.id + '' + '' + time + '' + '' + status + '' + indicesDisplay + '' + events + ''; 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 ? ' [' + personIndices.join(', ') + ']' : ''; localSection.innerHTML = '

Local Analysis (' + localEvents.length + ')' + indicesDisplay + '

'; localEvents.forEach(function(event) { var div = document.createElement('div'); div.className = 'modal-event source-local'; var eventType = event.event_type.replace('(本地)', '').trim(); div.innerHTML = '' + eventType + ' ' + '' + event.confidence + '' + '
' + event.description + '
'; localSection.appendChild(div); }); eventsDiv.appendChild(localSection); } // AI分析结果 if (aiEvents.length > 0) { var aiSection = document.createElement('div'); aiSection.className = 'modal-events-section'; aiSection.innerHTML = '

AI Analysis (' + aiEvents.length + ')

'; 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 = '' + eventType + ' ' + '' + event.confidence + '' + '
' + event.description + '
'; aiSection.appendChild(div); }); eventsDiv.appendChild(aiSection); } } else { eventsDiv.innerHTML = '

No events recorded

'; } 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 = '

No events

'; 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 = 'Local Analysis' + localEvents.length + ' events'; 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 = 'AI Analysis' + aiEvents.length + ' events'; 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 = '
' + '' + eventType + '' + '' + (source === 'local' ? 'Local' : 'AI') + '' + '' + time + '
' + '
' + event.description + '
' + '
' + event.confidence + '
'; 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 = '
' + 'Date: ' + data.date + '' + 'Images: ' + data.total_images + '' + 'Events: ' + data.total_events + '' + '
'; // Person count timeline chart (using simple CSS bars) var timeline = data.timeline; var chartCanvas = document.getElementById('person-chart'); if (timeline.length === 0) { chartCanvas.innerHTML = '

No data

'; return; } // Build simple bar chart var chartHtml = '
'; 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 += '
' + '
' + '
' + timeLabel + '
' + '
' + count + '
' + '
'; }); chartHtml += '
'; chartCanvas.innerHTML = chartHtml; // Event types distribution var eventTypesDiv = document.getElementById('event-types-chart'); eventTypesDiv.innerHTML = ''; if (data.event_types.length === 0) { eventTypesDiv.innerHTML = '

No events

'; 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 = '
' + '' + et.type + '' + '
' + '' + et.count + '' + '
'; 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 = '

No history data

'; return; } var maxImages = Math.max.apply(Math, data.history.map(function(h) { return h.total_images })); maxImages = Math.max(maxImages, 1); var html = '
'; data.history.reverse().forEach(function(h) { var dateLabel = h.date.slice(5); // MM-DD var height = (h.total_images / maxImages) * 100; html += '
' + '
' + '
' + dateLabel + '
' + '
' + h.total_images + '
' + '
'; }); html += '
'; 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 = '
' + 'Total: ' + data.stats.total_persons + '' + 'Detected: ' + data.stats.total_detections + '' + 'Known: ' + data.stats.known_persons_detected + '' + 'New: ' + data.stats.new_persons_added + '' + '
'; // 人员列表 if (data.persons.length === 0) { listDiv.innerHTML = '

No persons recorded yet

'; 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 = '
No Image
'; if (person.face_path) { faceImgHtml = '' + person.name + ''; } item.innerHTML = '
' + faceImgHtml + '
' + '
' + '
' + '' + person.name + '' + '' + person.person_id + '' + '
' + '
' + 'Visits: ' + person.visit_count + '' + 'First: ' + firstSeen + '' + 'Last: ' + lastSeen + '' + '
' + '
' + '' + '' + '
' + '
'; 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.'); }