Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <?php
- // index.php
- session_start();
- if (empty($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
- header('Location: login.php');
- exit;
- }
- if (!isset($_SESSION['csrf_token'])) {
- $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
- }
- $csrfToken = $_SESSION['csrf_token'];
- function loadEnv(string $path): array {
- $vars = [];
- if (!file_exists($path)) return $vars;
- foreach (file($path, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $line) {
- $line = trim($line);
- if ($line === '' || $line[0] === '#' || strpos($line,'=')===false) continue;
- list($n,$v) = explode('=',$line,2);
- $vars[trim($n)] = trim($v," \t\n\r\0\x0B'\"");
- }
- return $vars;
- }
- $env = loadEnv(__DIR__.'/.env');
- $QR_VAL = $env['QR'] ?? '';
- ?>
- <!DOCTYPE html>
- <html lang="vi">
- <head>
- <meta charset="UTF-8">
- <title>Giao diện tạo QR</title>
- <meta name="viewport" content="width=device-width,initial-scale=1.0">
- <meta name="csrf-token" content="<?=htmlspecialchars($csrfToken,ENT_QUOTES)?>">
- <style>
- body{margin:0;padding:0;font-family:Arial,sans-serif;background:#f5f5f5;color:#222}
- .dark-mode{background:#222;color:#ddd}
- .dark-mode input,.dark-mode select,.dark-mode button,.dark-mode .col {
- background:#333;color:#ddd;border-color:#555
- }
- #btnThemeIcon{position:fixed;top:12px;right:12px;font-size:24px;background:none;border:none;cursor:pointer}
- #infoPanel{position:fixed;top:12px;left:12px;background:rgba(255,255,255,0.8);padding:8px 12px;border-radius:6px;
- font-size:14px;line-height:1.4}
- #popup{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
- background:#fff;padding:16px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.2);
- width:90vw;max-width:600px}
- .input-group{display:flex;margin:8px 0}
- .input-group input#calc{flex:1;padding:8px;font-size:16px;border:2px solid #ccc;border-radius:4px;transition:0.3s}
- .input-group input#calc:focus{border-color:#007bff;box-shadow:0 0 6px rgba(0,123,255,0.5);outline:none}
- .input-group button#btnSend{margin-left:4px;padding:8px 12px;font-size:16px;border:none;border-radius:4px;
- background:#28a745;color:#fff;cursor:pointer;transition:0.2s}
- .input-group button#btnSend:hover{opacity:.9}.input-group button#btnSend:active{transform:scale(0.98)}
- #sendStatus{font-size:13px;color:#333;min-height:1em;margin-bottom:8px}
- .bank-select{display:flex;gap:8px;margin:8px 0}
- .bank-select input,.bank-select select{flex:1;padding:6px;font-size:14px;border:2px solid #ccc;border-radius:4px}
- #qrContainer{text-align:center;margin:12px 0}
- #qrContainer img{max-width:200px;border:1px solid #ccc;padding:4px;background:#fff;display:none}
- .qr-actions{display:flex;justify-content:space-between;margin:8px 0}
- .qr-actions button{flex:1;margin:0 4px;padding:6px;font-size:14px;border:none;border-radius:4px;
- color:#fff;cursor:pointer;transition:0.2s}
- #btnPersonal{background:#007bff}#btnCustom{background:#17a2b8}#btnCloseQR{background:#dc3545}
- .qr-actions button:hover{opacity:.9}
- .columns{display:flex;gap:12px;margin-top:12px}
- .col{flex:1;background:#fafafa;padding:8px;border-radius:4px;max-height:250px;overflow:auto}
- .col header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}
- .col ul{list-style:none;padding:0;margin:0}
- .col li{font-size:13px;margin:4px 0;display:flex;justify-content:space-between;gap:8px}
- .col .time{color:#555;margin-right:6px}.col .status-emoji{margin-right:4px}.col .amount{font-weight:bold}
- #toggleAmt{background:none;border:none;cursor:pointer;font-size:18px}
- #popupMessage{position:absolute;top:10px;right:10px;padding:5px 10px;border-radius:5px;
- background:rgba(0,0,0,0.6);color:lightgreen;font-size:12px;display:none}
- </style>
- </head>
- <body id="body">
- <button id="btnThemeIcon">🎨</button>
- <div id="infoPanel">
- <div id="clock-day">–</div>
- <div id="clock-time">Giờ: –</div>
- <div id="start-time">Bắt đầu: –</div>
- <div id="elapsed-time">Chạy: –</div>
- </div>
- <div id="popup">
- <div id="popupMessage"></div>
- <div class="input-group">
- <input id="calc" type="text" placeholder="Nhập số, phép tính, hoặc /lệnh">
- <button id="btnSend">Gửi</button>
- </div>
- <div id="sendStatus"><em>Chưa gửi gì.</em></div>
- <div class="bank-select">
- <input id="account" type="text" placeholder="Số tài khoản">
- <select id="bank"><option>Đang tải…</option></select>
- </div>
- <div id="qrContainer">
- <img id="qrImage" alt="QR Code">
- </div>
- <div class="qr-actions">
- <button id="btnPersonal">QR Cá Nhân</button>
- <button id="btnCustom">QR Tùy Chỉnh</button>
- <button id="btnCloseQR">Tắt QR</button>
- </div>
- <div class="columns">
- <div class="col" id="historyCol">
- <header>
- <strong>Lịch sử GD</strong>
- <button id="toggleAmt">👁️</button>
- </header>
- <ul id="historyList">
- <li><em>Chưa có giao dịch.</em></li>
- </ul>
- </div>
- <div class="col" id="queueCol">
- <header><strong>Danh sách đợi</strong></header>
- <ul id="queueList">
- <li><em>Đang tải…</em></li>
- </ul>
- </div>
- </div>
- </div>
- <script>
- const csrfToken = document.head.querySelector('[name="csrf-token"]').content;
- const QR_VAL = '<?=htmlspecialchars($QR_VAL,ENT_QUOTES)?>';
- let historyList = [], isProcessing = false, showAmt = false;
- const pageStart = new Date();
- const days = ['Chủ Nhật','Thứ Hai','Thứ Ba','Thứ Tư','Thứ Năm','Thứ Sáu','Thứ Bảy'];
- const calc = document.getElementById('calc');
- function showMessage(msg, isErr=false) {
- const d = document.getElementById('popupMessage');
- d.style.color = isErr ? 'red' : 'lightgreen';
- d.textContent = msg;
- d.style.display = 'block';
- setTimeout(() => d.style.display = 'none', 3000);
- }
- const formatThousands = x => x.toString().replace(/\B(?=(\d{3})+(?!\d))/g,'.');
- const unformatThousands = x => x.toString().replace(/\./g,'');
- function updateInfo() {
- const now = new Date();
- document.getElementById('clock-day').textContent =
- days[now.getDay()] + ', ' + now.toLocaleDateString('vi-VN');
- document.getElementById('clock-time').textContent =
- 'Giờ: ' + now.toLocaleTimeString('vi-VN');
- document.getElementById('start-time').textContent =
- 'Bắt đầu: ' + pageStart.toLocaleTimeString('vi-VN');
- let diff = Math.floor((now - pageStart)/1000),
- h = String(Math.floor(diff/3600)).padStart(2,'0'),
- m = String(Math.floor((diff%3600)/60)).padStart(2,'0'),
- s = String(diff%60).padStart(2,'0');
- document.getElementById('elapsed-time').textContent =
- `Chạy: ${h}:${m}:${s}`;
- }
- setInterval(updateInfo, 1000);
- updateInfo();
- function renderHistory() {
- const ul = document.getElementById('historyList');
- ul.innerHTML = '';
- if (!historyList.length) {
- ul.innerHTML = '<li><em>Chưa có giao dịch.</em></li>';
- return;
- }
- historyList.forEach(h => {
- const li = document.createElement('li');
- const amt = showAmt ? formatThousands(h.amount) : '••••';
- li.innerHTML = `
- <span>
- <span class="time">${h.time}</span>
- <span class="status-emoji">${h.icon}</span>
- </span>
- <span class="amount">${amt}</span>`;
- ul.append(li);
- });
- }
- async function loadQueue() {
- const r = await fetch('queue.php?action=list');
- return r.ok ? await r.json() : [];
- }
- async function renderQueue() {
- const data = await loadQueue();
- const ul = document.getElementById('queueList');
- ul.innerHTML = '';
- if (!data.length) {
- ul.innerHTML = '<li><em>Không có tin lỗi</em></li>';
- return;
- }
- data.forEach((it, idx) => {
- const li = document.createElement('li');
- li.innerHTML = `
- <span class="time">${new Date(it.time).toLocaleString()}</span>
- <span style="color:green;cursor:pointer" data-edit-idx="${idx}">Sửa</span>
- <span style="color:blue;cursor:pointer" data-del-idx="${idx}">Xóa</span>`;
- ul.append(li);
- });
- // Xóa
- ul.querySelectorAll('[data-del-idx]').forEach(el => {
- el.onclick = async () => {
- if (!confirm('Xác nhận xóa?')) return;
- await fetch(`queue.php?action=delete&index=${el.dataset.delIdx}`, {
- method: 'POST',
- headers: { 'X-CSRF-Token': csrfToken }
- });
- renderQueue();
- };
- });
- // Sửa
- ul.querySelectorAll('[data-edit-idx]').forEach(el => {
- el.onclick = async () => {
- const idx = el.dataset.editIdx;
- const old = data[idx].payload;
- const neu = prompt('Nhập payload mới:', old);
- if (neu === null || neu.trim() === '' || neu === old) return;
- await fetch(`queue.php?action=update&index=${idx}`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'X-CSRF-Token': csrfToken
- },
- body: `payload=${encodeURIComponent(neu)}&time=${encodeURIComponent(data[idx].time)}`
- });
- renderQueue();
- };
- });
- }
- // Khởi tạo lịch sử từ localStorage
- (function initHistory() {
- const saved = JSON.parse(localStorage.getItem('send_history') || '[]');
- historyList = saved;
- renderHistory();
- })();
- // Refresh queue định kỳ
- renderQueue();
- setInterval(renderQueue, 15000);
- // --- Server-Sent Events: nhận giao dịch real-time từ events.php ---
- let lastId = parseInt(localStorage.getItem('lastId') || '-1', 10);
- const evt = new EventSource('events.php?lastId=' + lastId);
- evt.onmessage = e => {
- const tx = JSON.parse(e.data);
- lastId = parseInt(e.lastEventId, 10);
- localStorage.setItem('lastId', lastId);
- // Thêm vào lịch sử và hiển thị
- historyList.unshift({
- time: tx.time,
- amount: tx.amount,
- icon: '✅'
- });
- localStorage.setItem('send_history', JSON.stringify(historyList));
- renderHistory();
- showMessage(
- `Giao dịch mới: ${tx.date} ${tx.time} – ${Number(tx.amount).toLocaleString('vi-VN')} VND`
- );
- };
- evt.onerror = err => {
- console.error('SSE error:', err);
- evt.close();
- };
- // Các hàm gửi, xử lý queue, format, xử lý input...
- async function sendDirect(v) {
- isProcessing = true;
- document.getElementById('sendStatus').textContent = `Đang gửi: ${formatThousands(v)}`;
- try {
- const res = await fetch('send.php', {
- method:'POST',
- headers:{
- 'Content-Type':'application/x-www-form-urlencoded',
- 'X-CSRF-Token':csrfToken
- },
- body:`result=${encodeURIComponent(v)}&time=${encodeURIComponent(new Date().toISOString())}`
- });
- const txt = await res.text();
- if (txt.toLowerCase().includes('thành công')) {
- historyList.unshift({ time:new Date().toLocaleTimeString('vi-VN'), icon:'✅', amount:v });
- renderHistory();
- } else throw 'Lỗi server';
- } catch {
- await fetch('queue.php?action=add',{
- method:'POST',
- headers:{ 'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':csrfToken },
- body:`payload=${encodeURIComponent(v)}&time=${encodeURIComponent(new Date().toISOString())}`
- });
- renderQueue();
- } finally {
- isProcessing = false;
- document.getElementById('sendStatus').textContent = '';
- processQueue();
- }
- }
- async function processQueue() {
- if (isProcessing) return;
- isProcessing = true;
- while (true) {
- const q = await loadQueue();
- if (!q.length) break;
- const it = q[0];
- document.getElementById('sendStatus').textContent = `Resend: ${formatThousands(it.payload)}`;
- try {
- const res = await fetch('send.php', {
- method:'POST',
- headers:{ 'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':csrfToken },
- body:`result=${encodeURIComponent(it.payload)}&time=${encodeURIComponent(it.time)}`
- });
- const txt = await res.text();
- if (txt.toLowerCase().includes('thành công')) {
- await fetch(`queue.php?action=delete&index=0`,{ method:'POST','X-CSRF-Token':csrfToken });
- historyList.unshift({ time:new Date().toLocaleTimeString('vi-VN'), icon:'🔄', amount:it.payload });
- renderHistory();
- renderQueue();
- } else throw 'Server lỗi';
- } catch {
- break;
- }
- }
- isProcessing = false;
- document.getElementById('sendStatus').textContent = '';
- }
- function sendToServer(cmd) {
- fetch('send.php', {
- method:'POST',
- headers:{ 'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':csrfToken },
- body:`result=${encodeURIComponent(cmd)}`
- })
- .then(r=>r.text())
- .then(txt=>{
- const now = new Date().toLocaleTimeString('vi-VN');
- const ok = txt.toLowerCase().includes('thành công');
- historyList.unshift({
- time: now,
- icon: ok?'✅':'❌',
- amount: ok?cmd:`${cmd}: ${txt.trim()}`
- });
- renderHistory();
- showMessage(ok?'✅ Lệnh thành công':`❌ ${txt.trim()}`, !ok);
- })
- .catch(e=>{
- const now = new Date().toLocaleTimeString('vi-VN');
- historyList.unshift({ time:now, icon:'⚠️', amount:`Lỗi: ${e}` });
- renderHistory();
- showMessage('⚠️ Lỗi khi gửi', true);
- });
- }
- async function handleInput(raw) {
- const v0 = raw.trim();
- if (!v0) return;
- calc.value = ''; calc.focus();
- if (v0.startsWith('/')) {
- sendToServer(v0);
- return;
- }
- const calcPattern = /^\d+(\s*[\+\-\*\/]\s*\d+)+$/;
- if (calcPattern.test(v0)) {
- try { const r = eval(v0); if (!isNaN(r)) calc.value = r; }
- catch{}
- calc.focus();
- return;
- }
- const num = unformatThousands(v0);
- const q = await loadQueue();
- if (!isProcessing && !q.length) {
- await sendDirect(num);
- } else {
- await fetch('queue.php?action=add',{
- method:'POST',
- headers:{ 'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':csrfToken },
- body:`payload=${encodeURIComponent(num)}&time=${encodeURIComponent(new Date().toISOString())}`
- });
- renderQueue();
- }
- }
- calc.addEventListener('input', e=>{
- const el = e.target, v = el.value, pos = el.selectionStart;
- if (v.startsWith('/') || /[+\-*/()]/.test(v)) return;
- const raw = v.replace(/\D/g,''), fmt = raw.replace(/\B(?=(\d{3})+(?!\d))/g,'.');
- const diff = fmt.length - v.length;
- el.value = fmt;
- el.setSelectionRange(pos+diff,pos+diff);
- });
- document.getElementById('toggleAmt').onclick = ()=>{
- showAmt = !showAmt;
- document.getElementById('toggleAmt').textContent = showAmt?'👁️':'🙈';
- renderHistory();
- };
- document.getElementById('btnSend').onclick = ()=> handleInput(calc.value);
- document.body.addEventListener('keydown', e=>{
- if (e.key==='Enter'){ e.preventDefault(); handleInput(calc.value); }
- if (e.key==='Escape'){ e.preventDefault(); calc.value=''; calc.focus(); }
- });
- const qrImg = document.getElementById('qrImage');
- function openQR(type,withAmount){
- const acc = encodeURIComponent(document.getElementById('account').value.trim());
- const bin = encodeURIComponent(document.getElementById('bank').value);
- const amt0 = withAmount? unformatThousands(calc.value.trim()) : 0;
- const data = `${QR_VAL}?bank=${bin}&account=${acc}` + (withAmount?`&amount=${amt0}`:'');
- qrImg.src = `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(data)}&size=200x200`;
- qrImg.style.display = 'block';
- }
- document.getElementById('btnPersonal').onclick = ()=> openQR('personal',false);
- document.getElementById('btnCustom').onclick = ()=> openQR('custom',true);
- document.getElementById('btnCloseQR').onclick = ()=> qrImg.style.display='none';
- window.addEventListener('load',()=>{
- fetch('https://api.vietqr.io/v2/banks')
- .then(r=>r.json()).then(o=>{
- const sel = document.getElementById('bank');
- sel.innerHTML = '';
- o.data.forEach(b=> sel.append(new Option(b.shortName,b.bin)));
- })
- .catch(()=> showMessage('⚠️ Không tải được danh sách ngân hàng',true));
- document.getElementById('toggleAmt').textContent = showAmt ? '👁️' : '🙈';
- renderHistory();
- renderQueue();
- processQueue();
- });
- const bodyEl = document.getElementById('body');
- if (localStorage.getItem('darkMode')==='true') bodyEl.classList.add('dark-mode');
- document.getElementById('btnThemeIcon').onclick = ()=>{
- bodyEl.classList.toggle('dark-mode');
- localStorage.setItem('darkMode',bodyEl.classList.contains('dark-mode'));
- };
- window.addEventListener('beforeunload', e=>{
- e.preventDefault();
- e.returnValue = 'Bạn có chắc chắn muốn tải lại trang?';
- });
- ;(function(){
- // SSE: nhận real-time
- let sseLastId = parseInt(localStorage.getItem('lastId')||'-1',10);
- const evt = new EventSource('QR/events.php?lastId='+sseLastId);
- evt.onmessage = e => {
- try {
- const tx = JSON.parse(e.data);
- sseLastId = parseInt(e.lastEventId,10);
- localStorage.setItem('lastId', sseLastId);
- historyList.unshift({
- time: new Date(tx.time).toLocaleTimeString('vi-VN'),
- amount: tx.amount,
- icon: '✅'
- });
- localStorage.setItem('send_history', JSON.stringify(historyList));
- renderHistory();
- showMessage(`Giao dịch mới (SSE): ${tx.amount} VND`);
- } catch(e) { console.error(e); }
- };
- evt.onerror = err => { console.error('SSE error', err); evt.close(); };
- // Polling fallback
- let pollLastId = parseInt(localStorage.getItem('lastPollId')||'-1',10);
- async function pollNew(){
- try {
- const res = await fetch('QR/get.php?lastId='+pollLastId, {
- headers: {'X-CSRF-Token': csrfToken}
- });
- if (!res.ok) return;
- const list = await res.json();
- list.forEach(tx=>{
- historyList.unshift({
- time: new Date(tx.time).toLocaleTimeString('vi-VN'),
- amount: tx.amount,
- icon: '✅'
- });
- renderHistory();
- showMessage(`Giao dịch mới (poll): ${tx.amount} VND`);
- pollLastId = tx.id;
- });
- if (list.length) localStorage.setItem('lastPollId', pollLastId);
- } catch(e) { console.error(e); }
- }
- pollNew();
- setInterval(pollNew, 5000);
- })();
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment