579 lines
22 KiB
JavaScript
579 lines
22 KiB
JavaScript
// 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.');
|
||
} |