433 lines
12 KiB
HTML
433 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Face Check-In / Register</title>
|
|
<style>
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background-color: #f5f7fa;
|
|
color: #333;
|
|
}
|
|
|
|
h2 {
|
|
color: #2c3e50;
|
|
text-align: center;
|
|
margin-bottom: 20px;
|
|
border-bottom: 2px solid #3498db;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
video {
|
|
width: 100%;
|
|
max-width: 1200px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
display: block;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
input[type="text"], input[type="email"] {
|
|
width: 100%;
|
|
padding: 12px;
|
|
margin: 8px 0;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
box-sizing: border-box;
|
|
font-size: 16px;
|
|
transition: border-color 0.3s;
|
|
}
|
|
|
|
input[type="text"]:focus, input[type="email"]:focus {
|
|
border-color: #3498db;
|
|
outline: none;
|
|
box-shadow: 0 0 5px rgba(52, 152, 219, 0.5);
|
|
}
|
|
|
|
button {
|
|
background-color: #3498db;
|
|
color: white;
|
|
border: none;
|
|
padding: 12px 20px;
|
|
margin: 10px 5px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
transition: background-color 0.3s;
|
|
}
|
|
|
|
button:hover {
|
|
background-color: #2980b9;
|
|
}
|
|
|
|
#register {
|
|
background-color: #2ecc71;
|
|
}
|
|
|
|
#register:hover {
|
|
background-color: #27ae60;
|
|
}
|
|
|
|
.container {
|
|
background-color: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.button-group {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-top: 20px;
|
|
gap: 10px;
|
|
}
|
|
|
|
#auto-checkin {
|
|
background-color: #f39c12;
|
|
}
|
|
|
|
#auto-checkin:hover {
|
|
background-color: #d35400;
|
|
}
|
|
|
|
#auto-checkin.active {
|
|
background-color: #e74c3c;
|
|
}
|
|
|
|
.logs-container {
|
|
background-color: white;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
th, td {
|
|
padding: 12px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #ddd;
|
|
}
|
|
|
|
th {
|
|
background-color: #f2f2f2;
|
|
color: #333;
|
|
font-weight: bold;
|
|
}
|
|
|
|
tr:hover {
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.refresh-btn {
|
|
background-color: #9b59b6;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.refresh-btn:hover {
|
|
background-color: #8e44ad;
|
|
}
|
|
|
|
.empty-logs {
|
|
text-align: center;
|
|
color: #7f8c8d;
|
|
padding: 20px;
|
|
font-style: italic;
|
|
}
|
|
|
|
.main-layout {
|
|
display: flex;
|
|
gap: 20px;
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.left-panel {
|
|
flex: 6;
|
|
}
|
|
|
|
.right-panel {
|
|
flex: 5;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.main-layout {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
|
|
.newest-log {
|
|
background-color: #e8f8f5;
|
|
font-weight: bold;
|
|
animation: highlight 2s ease-in-out;
|
|
}
|
|
|
|
@keyframes highlight {
|
|
0% { background-color: #d4efdf; }
|
|
50% { background-color: #a9dfbf; }
|
|
100% { background-color: #e8f8f5; }
|
|
}
|
|
|
|
.shortcut-hint {
|
|
text-align: center;
|
|
margin-top: 10px;
|
|
color: #7f8c8d;
|
|
font-style: italic;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.custom-alert {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
color: white;
|
|
padding: 12px 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
font-family: sans-serif;
|
|
font-size: 16px;
|
|
z-index: 9999;
|
|
animation: slideIn 0.3s ease-out;
|
|
}
|
|
|
|
.alert-success {
|
|
background-color: #34d399; /* green-400 */
|
|
}
|
|
|
|
.alert-error {
|
|
background-color: #f87171; /* red-400 */
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="main-layout">
|
|
<div class="left-panel">
|
|
<div class="container">
|
|
<h2>📸 Face Camera</h2>
|
|
<div class="video-container" style="position: relative; display: flex; justify-content: center; align-items: center;">
|
|
<video id="video" autoplay style="width: 100%;"></video>
|
|
<img src="http://127.0.0.1:8000/static/face-removebg-preview.png" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 60%; height: auto; pointer-events: none; opacity: 0.5;" alt="Hướng dẫn nhận diện khuôn mặt">
|
|
</div>
|
|
<br>
|
|
|
|
<div class="form-group">
|
|
<input type="text" id="name" placeholder="Tên học sinh (khi đăng ký)">
|
|
</div>
|
|
<div class="form-group">
|
|
<input type="email" id="email" placeholder="Email học sinh (bắt buộc)">
|
|
</div>
|
|
|
|
<div class="button-group">
|
|
<button id="register">📥 Đăng ký khuôn mặt</button>
|
|
<button id="checkin">✅ Điểm danh</button>
|
|
<button id="auto-checkin">🔄 Tự động điểm danh</button>
|
|
</div>
|
|
<div class="shortcut-hint">
|
|
Nhấn phím Space để điểm danh nhanh
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="right-panel">
|
|
<div class="logs-container">
|
|
<h2>📋 Lịch sử điểm danh</h2>
|
|
<button id="refresh-logs" class="refresh-btn">🔄 Làm mới dữ liệu</button>
|
|
<div id="logs-table-container">
|
|
<table id="logs-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Tên học sinh</th>
|
|
<th>Thời gian</th>
|
|
<th>Camera ID</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="logs-body">
|
|
<!-- Dữ liệu logs sẽ được thêm vào đây -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<canvas id="canvas" width="1200" height="900" style="display:none;"></canvas>
|
|
|
|
<script>
|
|
const video = document.getElementById('video');
|
|
const canvas = document.getElementById('canvas');
|
|
const context = canvas.getContext('2d');
|
|
|
|
// Mở camera
|
|
navigator.mediaDevices.getUserMedia({ video: true })
|
|
.then((stream) => {
|
|
video.srcObject = stream;
|
|
})
|
|
.catch((err) => {
|
|
showAlert("Không mở được camera: " + err);
|
|
});
|
|
|
|
function showAlert(text, status = 'error') {
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = `custom-alert alert-${status}`;
|
|
alertDiv.innerText = text;
|
|
document.body.appendChild(alertDiv);
|
|
|
|
setTimeout(() => {
|
|
alertDiv.style.opacity = '0';
|
|
alertDiv.style.transition = 'opacity 0.5s ease-out';
|
|
setTimeout(() => {
|
|
alertDiv.remove();
|
|
}, 500);
|
|
}, 2000);
|
|
}
|
|
|
|
// Hàm chụp ảnh từ video và gửi đến API
|
|
function sendImage(url, extraData = {}) {
|
|
context.drawImage(video, 0, 0, 1200, 900);
|
|
canvas.toBlob((blob) => {
|
|
const formData = new FormData();
|
|
formData.append("file", blob, "frame.jpg");
|
|
|
|
for (const [key, value] of Object.entries(extraData)) {
|
|
formData.append(key, value);
|
|
}
|
|
|
|
fetch(url, {
|
|
method: "POST",
|
|
body: formData
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const status = data.message && data.message.includes("successful") ? 'success' : 'error';
|
|
showAlert(data.message || JSON.stringify(data), status);
|
|
if (url === "/checkin" && data.message && data.message.includes("successful")) {
|
|
loadLogs(); // Tải lại logs sau khi check-in thành công
|
|
}
|
|
})
|
|
.catch(err => showAlert("Lỗi gửi ảnh: " + err, 'error'));
|
|
}, "image/jpeg");
|
|
}
|
|
|
|
// Sự kiện: Đăng ký
|
|
document.getElementById('register').addEventListener('click', () => {
|
|
const name = document.getElementById('name').value.trim();
|
|
const email = document.getElementById('email').value.trim();
|
|
if (!name || !email) {
|
|
showAlert("Vui lòng nhập cả tên và email.", 'error');
|
|
return;
|
|
}
|
|
sendImage("/register", { name, email });
|
|
});
|
|
|
|
let autoCheckinInterval = null;
|
|
const autoCheckinButton = document.getElementById('auto-checkin');
|
|
|
|
// Hàm bật/tắt tự động điểm danh
|
|
function toggleAutoCheckin() {
|
|
if (autoCheckinInterval) {
|
|
// Tắt tự động
|
|
clearInterval(autoCheckinInterval);
|
|
autoCheckinInterval = null;
|
|
autoCheckinButton.classList.remove('active');
|
|
autoCheckinButton.textContent = '🔄 Tự động điểm danh';
|
|
} else {
|
|
// Bật tự động
|
|
autoCheckinInterval = setInterval(() => {
|
|
sendImage("/checkin", { camera_id: "webcam" });
|
|
}, 1000); // Gửi mỗi giây
|
|
autoCheckinButton.classList.add('active');
|
|
autoCheckinButton.textContent = '⏹️ Dừng tự động';
|
|
}
|
|
}
|
|
|
|
// Sự kiện: Bật/tắt tự động điểm danh
|
|
autoCheckinButton.addEventListener('click', toggleAutoCheckin);
|
|
|
|
// Sự kiện: Check-in
|
|
document.getElementById('checkin').addEventListener('click', () => {
|
|
if (autoCheckinInterval) {
|
|
toggleAutoCheckin(); // Tắt tự động nếu đang bật
|
|
}
|
|
sendImage("/checkin", { camera_id: "webcam" });
|
|
});
|
|
|
|
// Sự kiện: Nhấn phím Space để điểm danh
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.code === 'Space' || event.keyCode === 32) {
|
|
event.preventDefault();
|
|
if (autoCheckinInterval) {
|
|
toggleAutoCheckin(); // Tắt tự động nếu đang bật
|
|
}
|
|
sendImage("/checkin", { camera_id: "webcam" });
|
|
}
|
|
});
|
|
|
|
// Hàm tải logs từ server
|
|
function loadLogs() {
|
|
fetch("/logs")
|
|
.then(res => res.json())
|
|
.then(logs => {
|
|
const logsBody = document.getElementById('logs-body');
|
|
logsBody.innerHTML = '';
|
|
|
|
if (logs.length === 0) {
|
|
logsBody.innerHTML = '<tr><td colspan="3" class="empty-logs">Chưa có dữ liệu điểm danh</td></tr>';
|
|
return;
|
|
}
|
|
|
|
// Sắp xếp logs từ mới đến cũ
|
|
logs.sort((a, b) => new Date(b.time) - new Date(a.time));
|
|
|
|
logs.forEach((log, index) => {
|
|
// Highlight log mới nhất
|
|
const isNewest = index === 0;
|
|
const row = document.createElement('tr');
|
|
if (isNewest) {
|
|
row.classList.add('newest-log');
|
|
}
|
|
row.innerHTML = `
|
|
<td>${log.name}</td>
|
|
<td>${log.time}</td>
|
|
<td>${log.camera_id}</td>
|
|
`;
|
|
logsBody.appendChild(row);
|
|
});
|
|
})
|
|
.catch(err => {
|
|
console.error("Lỗi tải logs:", err);
|
|
document.getElementById('logs-body').innerHTML =
|
|
'<tr><td colspan="3" class="empty-logs">Lỗi khi tải dữ liệu</td></tr>';
|
|
});
|
|
}
|
|
|
|
// Sự kiện: Làm mới logs
|
|
document.getElementById('refresh-logs').addEventListener('click', loadLogs);
|
|
|
|
// Tải logs khi trang được tải
|
|
document.addEventListener('DOMContentLoaded', loadLogs);
|
|
</script>
|
|
</body>
|
|
</html>
|