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

579 lines
22 KiB
JavaScript
Raw 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) {
var startBtn = document.getElementById('start-btn');
var stopBtn = document.getElementById('stop-btn');
if (data.scheduler.running) {
startBtn.disabled = true;
stopBtn.disabled = false;
document.getElementById('scheduler-status').innerHTML = 'Status: Running';
} else {
startBtn.disabled = false;
stopBtn.disabled = true;
document.getElementById('scheduler-status').innerHTML = 'Status: Stopped';
}
document.getElementById('capture-count').textContent = 'Capture count: ' + 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;
// Event type filter
var eventFilter = document.getElementById('event-filter');
eventFilter.innerHTML = '<option value="all">All types</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 startScheduler() {
fetch(API_BASE + '/api/scheduler/start', {method: 'POST'})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
showToast('Started!', 1500);
refreshAll();
} else {
showToast('Failed: ' + data.error, 3000);
}
})
.catch(function(e) { showToast('Error: ' + e.message, 3000); });
}
function stopScheduler() {
fetch(API_BASE + '/api/scheduler/stop', {method: 'POST'})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
showToast('Stopped!', 1500);
refreshAll();
}
})
.catch(function(e) { showToast('Error: ' + e.message, 3000); });
}
function captureNow() {
fetch(API_BASE + '/api/capture', {method: 'POST'})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
showToast('Captured! ID: ' + data.image_id, 1500);
refreshAll();
} else {
showToast('Failed: ' + data.error, 3000);
}
})
.catch(function(e) { showToast('Error: ' + e.message, 3000); });
}
function analyzeUnanalyzed() {
fetch(API_BASE + '/api/analyze/unanalyzed', {method: 'POST'})
.then(function(res) { return res.json(); })
.then(function(data) {
showToast('Analyzed ' + data.results.length + ' images', 1500);
refreshAll();
})
.catch(function(e) { showToast('Error: ' + 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';
item.innerHTML = '<span class="image-number">#' + img.id + '</span>' +
'<span class="image-time">' + time + '</span>' +
'<span class="image-status">' + status + '</span>' +
'<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';
localSection.innerHTML = '<h4 class="section-title local">Local Analysis (' + localEvents.length + ') </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');
}
// 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).toLocaleDateString();
var lastSeen = new Date(person.last_seen).toLocaleDateString();
item.innerHTML = '<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>';
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;
// Detection algorithm settings
document.getElementById('setting-use-yolo').checked = config.use_yolo !== false;
document.getElementById('setting-use-mediapipe').checked = config.use_mediapipe_face !== false;
document.getElementById('setting-use-haar').checked = config.use_haar_cascade === true;
document.getElementById('setting-use-face-rec').checked = config.use_face_recognition !== false;
// Confirmation settings
document.getElementById('setting-confirm-frames').value = config.confirm_frames || 3;
document.getElementById('setting-min-confidence').value = config.min_detection_confidence || 0.3;
// 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),
// Detection algorithms
use_yolo: document.getElementById('setting-use-yolo').checked,
use_haar_cascade: document.getElementById('setting-use-haar').checked,
use_mediapipe_face: document.getElementById('setting-use-mediapipe').checked,
use_face_recognition: document.getElementById('setting-use-face-rec').checked,
// Confirmation settings
confirm_frames: parseInt(document.getElementById('setting-confirm-frames').value),
min_detection_confidence: parseFloat(document.getElementById('setting-min-confidence').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.');
}