Initial commit

This commit is contained in:
2026-01-07 14:09:59 +08:00
commit 8a00aa71d5
939 changed files with 40616 additions and 0 deletions

View File

@@ -0,0 +1,273 @@
/**
* QR Code Scanner Module
*/
class QRScanner {
constructor(options = {}) {
this.options = {
scannerId: 'qr-reader',
resultId: 'scan-result',
facingMode: 'environment',
fps: 10,
qrbox: { width: 250, height: 250 },
...options
};
this.scanner = null;
this.isScanning = false;
this.lastScanTime = 0;
this.scanCooldown = 2000; // 2 seconds cooldown
this.init();
}
init() {
this.scanner = new Html5Qrcode(this.options.scannerId);
// Create result container if not exists
if (!document.getElementById(this.options.resultId)) {
const resultDiv = document.createElement('div');
resultDiv.id = this.options.resultId;
document.querySelector(`#${this.options.scannerId}`).parentNode.appendChild(resultDiv);
}
}
async start() {
if (this.isScanning) return;
try {
await this.scanner.start(
{ facingMode: this.options.facingMode },
this.options,
this.onScanSuccess.bind(this),
this.onScanError.bind(this)
);
this.isScanning = true;
this.updateUI('started');
} catch (error) {
console.error('Failed to start scanner:', error);
this.showError('Failed to start camera. Please check permissions.');
}
}
stop() {
if (!this.isScanning) return;
this.scanner.stop().then(() => {
this.isScanning = false;
this.updateUI('stopped');
}).catch(error => {
console.error('Failed to stop scanner:', error);
});
}
onScanSuccess(decodedText, decodedResult) {
const currentTime = Date.now();
// Prevent multiple scans in short time
if (currentTime - this.lastScanTime < this.scanCooldown) {
return;
}
this.lastScanTime = currentTime;
// Show scanning indicator
this.showResult('scanning', 'Scanning QR code...');
// Process the scan
this.processQRCode(decodedText);
}
onScanError(error) {
console.warn('QR Scan Error:', error);
// Don't show errors to user unless critical
}
async processQRCode(qrData) {
try {
const response = await fetch('../api/scan_qr.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ qr_code: qrData })
});
const data = await response.json();
if (data.success) {
this.showResult('success', data);
this.playSound('success');
// Auto-stop after successful scan (optional)
if (this.options.autoStop) {
setTimeout(() => this.stop(), 3000);
}
} else {
this.showResult('error', data.message);
this.playSound('error');
}
} catch (error) {
console.error('Processing error:', error);
this.showResult('error', 'Network error. Please try again.');
this.playSound('error');
}
}
showResult(type, data) {
const resultDiv = document.getElementById(this.options.resultId);
let html = '';
switch(type) {
case 'scanning':
html = `
<div class="alert alert-info">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
${data}
</div>
`;
break;
case 'success':
html = `
<div class="alert alert-success attendance-success">
<div class="d-flex align-items-center">
<i class="bi bi-check-circle-fill me-3" style="font-size: 2rem;"></i>
<div>
<h5 class="mb-1">Attendance Recorded!</h5>
<p class="mb-1"><strong>Student:</strong> ${data.data.student_name}</p>
<p class="mb-1"><strong>Activity:</strong> ${data.data.activity_name}</p>
<p class="mb-0"><strong>Time:</strong> ${data.data.time}</p>
</div>
</div>
</div>
`;
break;
case 'error':
html = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
${data}
</div>
`;
break;
}
resultDiv.innerHTML = html;
// Auto-clear result after some time
if (type !== 'scanning') {
setTimeout(() => {
if (resultDiv.innerHTML.includes(html)) {
resultDiv.innerHTML = '';
}
}, 5000);
}
}
showError(message) {
const resultDiv = document.getElementById(this.options.resultId);
resultDiv.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-camera-video-off me-2"></i>
${message}
</div>
`;
}
updateUI(state) {
const scannerDiv = document.getElementById(this.options.scannerId);
if (state === 'started') {
scannerDiv.classList.add('scanning');
} else {
scannerDiv.classList.remove('scanning');
}
}
playSound(type) {
// Create audio context for sounds
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
if (type === 'success') {
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5
oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); // E5
oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); // G5
} else {
oscillator.frequency.setValueAtTime(200, audioContext.currentTime); // Low tone
}
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.3);
} catch (error) {
console.log('Audio not supported');
}
}
toggleCamera() {
if (this.options.facingMode === 'environment') {
this.options.facingMode = 'user';
} else {
this.options.facingMode = 'environment';
}
this.stop();
setTimeout(() => this.start(), 500);
}
}
// Initialize scanner when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Check for scanner elements on page
const scannerElements = document.querySelectorAll('[data-scanner]');
scannerElements.forEach(element => {
const scannerId = element.getAttribute('data-scanner') || 'qr-reader';
const scanner = new QRScanner({
scannerId: scannerId,
autoStop: element.getAttribute('data-auto-stop') === 'true'
});
// Add start/stop buttons if not auto-start
if (element.getAttribute('data-auto-start') !== 'true') {
const controls = document.createElement('div');
controls.className = 'scanner-controls mt-3';
controls.innerHTML = `
<button class="btn btn-success me-2 start-scan">
<i class="bi bi-play-circle"></i> Start Scanner
</button>
<button class="btn btn-danger me-2 stop-scan">
<i class="bi bi-stop-circle"></i> Stop Scanner
</button>
<button class="btn btn-secondary toggle-camera">
<i class="bi bi-camera-video"></i> Switch Camera
</button>
`;
element.parentNode.appendChild(controls);
// Add event listeners
controls.querySelector('.start-scan').addEventListener('click', () => scanner.start());
controls.querySelector('.stop-scan').addEventListener('click', () => scanner.stop());
controls.querySelector('.toggle-camera').addEventListener('click', () => scanner.toggleCamera());
} else {
// Auto-start scanner
setTimeout(() => scanner.start(), 1000);
}
});
});