school-checkin/static/index.html

373 lines
9.4 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: 400px;
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;
}
.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;
}
.left-panel {
flex: 1;
}
.right-panel {
flex: 1;
}
@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;
background-color: #f87171; /* red-400 */
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;
}
@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>
<video id="video" autoplay></video>
<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>
</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="400" height="300" 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) {
const alertDiv = document.createElement('div');
alertDiv.className = 'custom-alert';
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, 400, 300);
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 => {
showAlert(data.message || JSON.stringify(data));
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));
}, "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) return showAlert("Vui lòng nhập cả tên và email.");
sendImage("/register", { name, email });
});
// Sự kiện: Check-in
document.getElementById('checkin').addEventListener('click', () => {
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(); // Ngăn chặn hành vi mặc định của phím Space
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>