PageSourceSearch

https://getabf.net/qrtable/js/qr-scanner.js

js getabf.net collected 2026-10-04 20:21:57 UTC 7,124 bytes, 223 lines download raw bytes

1// ============================================
2// MODULE: Quét QR Code bằng camera
3// ============================================
4
5let html5QrCode = null;
6let isScanning = false;
7let canScan = true;
8let scanCooldown = 1500;
9
10function playSuccessBeep() {
11    try {
12        const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
13        const oscillator = audioCtx.createOscillator();
14        const gainNode = audioCtx.createGain();
15        
16        oscillator.connect(gainNode);
17        gainNode.connect(audioCtx.destination);
18        
19        oscillator.frequency.value = 880;
20        gainNode.gain.value = 0.3;
21        
22        oscillator.start();
23        gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.3);
24        oscillator.stop(audioCtx.currentTime + 0.3);
25        
26        if (audioCtx.state === 'suspended') {
27            audioCtx.resume();
28        }
29    } catch(e) {
30        console.log('Không thể phát âm thanh:', e);
31    }
32}
33
34function playDuplicateBeep() {
35    try {
36        const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
37        const oscillator = audioCtx.createOscillator();
38        const gainNode = audioCtx.createGain();
39        
40        oscillator.connect(gainNode);
41        gainNode.connect(audioCtx.destination);
42        
43        oscillator.frequency.value = 440;
44        gainNode.gain.value = 0.3;
45        
46        oscillator.start();
47        gainNode.gain.exponentialRampToValueAtTime(0.00001, audioCtx.currentTime + 0.2);
48        oscillator.stop(audioCtx.currentTime + 0.2);
49        
50        if (audioCtx.state === 'suspended') {
51            audioCtx.resume();
52        }
53    } catch(e) {
54        console.log('Không thể phát âm thanh:', e);
55    }
56}
57
58function showToast(message, type = 'success') {
59    let toast = document.getElementById('qr-toast');
60    if (!toast) {
61        toast = document.createElement('div');
62        toast.id = 'qr-toast';
63        toast.style.cssText = `
64            position: fixed;
65            bottom: 80px;
66            left: 50%;
67            transform: translateX(-50%);
68            background: rgba(0,0,0,0.85);
69            color: white;
70            padding: 10px 20px;
71            border-radius: 8px;
72            z-index: 10000;
73            font-size: 14px;
74            white-space: nowrap;
75            transition: opacity 0.3s;
76            pointer-events: none;
77            font-family: 'Segoe UI', Arial, sans-serif;
78        `;
79        document.body.appendChild(toast);
80    }
81    
82    if (type === 'success') {
83        toast.style.background = 'rgba(40, 167, 69, 0.95)';
84    } else if (type === 'error') {
85        toast.style.background = 'rgba(220, 53, 69, 0.95)';
86    } else {
87        toast.style.background = 'rgba(255, 152, 0, 0.95)';
88    }
89    
90    toast.textContent = message;
91    toast.style.opacity = '1';
92    toast.style.whiteSpace = 'normal';
93    toast.style.maxWidth = '90%';
94    toast.style.textAlign = 'center';
95    
96    setTimeout(() => {
97        toast.style.opacity = '0';
98    }, 1500);
99}
100
101async function startQRScan() {
102    const qrReaderDiv = document.getElementById('qr-reader');
103    const startBtn = document.getElementById('startScanBtn');
104    const stopBtn = document.getElementById('stopScanBtn');
105    
106    if (!qrReaderDiv) return;
107    
108    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
109        showToast('⚠️ Trình duyệt không hỗ trợ camera', 'error');
110        return;
111    }
112    
113    try {
114        canScan = true;
115        qrReaderDiv.style.display = 'block';
116        
117        if (typeof Html5Qrcode === 'undefined') {
118            await loadHtml5QrcodeLibrary();
119        }
120        
121        html5QrCode = new Html5Qrcode("qr-reader");
122        
123        await html5QrCode.start(
124            { facingMode: "environment" },
125            { fps: 15, qrbox: { width: 250, height: 250 }, aspectRatio: 1.0 },
126            onQRCodeSuccess,
127            onQRCodeError
128        );
129        
130        isScanning = true;
131        startBtn.style.display = 'none';
132        stopBtn.style.display = 'inline-block';
133        showToast('📷 Camera đã sẵn sàng', 'success');
134        
135    } catch (error) {
136        console.error(error);
137        showToast('❌ Không thể khởi tạo camera', 'error');
138        qrReaderDiv.style.display = 'none';
139    }
140}
141
142async function stopQRScan() {
143    const qrReaderDiv = document.getElementById('qr-reader');
144    const startBtn = document.getElementById('startScanBtn');
145    const stopBtn = document.getElementById('stopScanBtn');
146    
147    if (html5QrCode && isScanning) {
148        try {
149            await html5QrCode.stop();
150            html5QrCode.clear();
151        } catch (error) {
152            console.error(error);
153        }
154    }
155    
156    isScanning = false;
157    canScan = true;
158    html5QrCode = null;
159    qrReaderDiv.style.display = 'none';
160    startBtn.style.display = 'inline-block';
161    stopBtn.style.display = 'none';
162}
163
164function onQRCodeSuccess(decodedText) {
165    if (!canScan) return;
166    canScan = false;
167    
168    try {
169        const parsedData = parseQRCode(decodedText);
170        const newIndex = parsedData.index;
171        const currentData = getCurrentData();
172        const isDuplicate = currentData.some(row => row.index === newIndex);
173        
174        if (isDuplicate) {
175            playDuplicateBeep();
176            showToast(`⚠️ Index ${newIndex} đã tồn tại`, 'warning');
177            setTimeout(() => { canScan = true; }, scanCooldown);
178            return;
179        }
180        
181        // Nếu có Excel active, cập nhật thời gian + lotNo (không lấy J,K,L,M từ Excel)
182        const lotNo = document.getElementById('lotNo')?.value || '';
183        if (typeof excelState !== 'undefined' && excelState && excelState.isActive) {
184            const indexExists = excelState.indexMap.has(newIndex);
185            if (indexExists) {
186                // Chỉ cập nhật thời gian và lotNo, không ghi đè J,K,L,M
187                updateExcelRow(newIndex, parsedData, lotNo, false);
188            } else {
189                // Thêm dòng mới vào Excel
190                addExcelRow(newIndex, parsedData, lotNo);
191            }
192        }
193        
194        addRow(parsedData);
195        playSuccessBeep();
196        showToast(`✅ Đã thêm: ${newIndex}`, 'success');
197        
198        setTimeout(() => { canScan = true; }, scanCooldown);
199        
200    } catch (error) {
201        playDuplicateBeep();
202        showToast('❌ ' + error.message, 'error');
203        setTimeout(() => { canScan = true; }, scanCooldown);
204    }
205}
206
207function onQRCodeError(error) {
208    // Bỏ qua lỗi nhỏ
209}
210
211function loadHtml5QrcodeLibrary() {
212    return new Promise((resolve, reject) => {
213        if (typeof Html5Qrcode !== 'undefined') {
214            resolve();
215            return;
216        }
217        const script = document.createElement('script');
218        script.src = 'https://unpkg.com/[email protected]/html5-qrcode.min.js';
219        script.onload = () => resolve();
220        script.onerror = () => reject(new Error('Không thể tải thư viện QR'));
221        document.head.appendChild(script);
222    });
223}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.