// 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 = '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 = 'No data
'; return; } // Build simple bar chart var chartHtml = '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 = ''; 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 = '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 = '