Autosave: 20260222-060540

This commit is contained in:
Flatlogic Bot 2026-02-22 06:05:40 +00:00
parent 9cc9c493bd
commit 9aa5517bf8
8 changed files with 131 additions and 171 deletions

View File

@ -18,7 +18,7 @@ ob_start();
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<div class="text-muted small">总用户数</div> <div class="text-muted small">总用户数</div>
<div class="fs-3 fw-bold"><?= number_format($total_users) ?></div> <div class="fs-3 fw-bold" id="stat-total-users"><?= number_format($total_users) ?></div>
</div> </div>
<div class="text-primary"><i class="bi bi-people fs-1 opacity-25"></i></div> <div class="text-primary"><i class="bi bi-people fs-1 opacity-25"></i></div>
</div> </div>
@ -29,7 +29,7 @@ ob_start();
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<div class="text-muted small">总充值 (USDT)</div> <div class="text-muted small">总充值 (USDT)</div>
<div class="fs-3 fw-bold"><?= number_format($total_recharge, 2) ?></div> <div class="fs-3 fw-bold" id="stat-total-recharge"><?= number_format($total_recharge, 2) ?></div>
</div> </div>
<div class="text-success"><i class="bi bi-cash-stack fs-1 opacity-25"></i></div> <div class="text-success"><i class="bi bi-cash-stack fs-1 opacity-25"></i></div>
</div> </div>
@ -40,7 +40,7 @@ ob_start();
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<div class="text-muted small">总提现 (USDT)</div> <div class="text-muted small">总提现 (USDT)</div>
<div class="fs-3 fw-bold"><?= number_format($total_withdrawal, 2) ?></div> <div class="fs-3 fw-bold" id="stat-total-withdrawal"><?= number_format($total_withdrawal, 2) ?></div>
</div> </div>
<div class="text-danger"><i class="bi bi-bank fs-1 opacity-25"></i></div> <div class="text-danger"><i class="bi bi-bank fs-1 opacity-25"></i></div>
</div> </div>
@ -51,7 +51,7 @@ ob_start();
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<div> <div>
<div class="text-muted small">待办事项</div> <div class="text-muted small">待办事项</div>
<div class="fs-3 fw-bold text-warning"><?= $pending_finance + $pending_kyc ?></div> <div class="fs-3 fw-bold text-warning" id="stat-pending-tasks"><?= $pending_finance + $pending_kyc ?></div>
</div> </div>
<div class="text-warning"><i class="bi bi-list-check fs-1 opacity-25"></i></div> <div class="text-warning"><i class="bi bi-list-check fs-1 opacity-25"></i></div>
</div> </div>

View File

@ -337,16 +337,37 @@ function renderAdminPage($content, $title = '后台管理') {
utterance.lang = 'zh-CN'; utterance.lang = 'zh-CN';
window.speechSynthesis.speak(utterance); window.speechSynthesis.speak(utterance);
} }
// Also try native notification
if (Notification.permission === "granted") {
new Notification("新消息提醒", { body: text, icon: '/assets/images/logo.png' });
}
}
// Request notification permission
if (Notification.permission !== "granted" && Notification.permission !== "denied") {
Notification.requestPermission();
} }
function checkNotifications() { function checkNotifications() {
const currentPage = window.location.pathname; const currentPage = window.location.pathname;
fetch('../api/admin_notifications.php') const isDashboard = currentPage.includes('index.php') || currentPage.endsWith('/admin/');
const url = isDashboard ? '../api/admin_notifications.php?stats=1' : '../api/admin_notifications.php';
fetch(url)
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
const counts = data.counts; const counts = data.counts;
// Update dashboard stats if available
if (data.stats) {
const s = data.stats;
if (document.getElementById('stat-total-users')) document.getElementById('stat-total-users').innerText = parseInt(s.total_users).toLocaleString();
if (document.getElementById('stat-total-recharge')) document.getElementById('stat-total-recharge').innerText = parseFloat(s.total_recharge).toLocaleString(undefined, {minimumFractionDigits: 2});
if (document.getElementById('stat-total-withdrawal')) document.getElementById('stat-total-withdrawal').innerText = parseFloat(s.total_withdrawal).toLocaleString(undefined, {minimumFractionDigits: 2});
if (document.getElementById('stat-pending-tasks')) document.getElementById('stat-pending-tasks').innerText = s.pending_tasks;
}
// Auto-clear current page types // Auto-clear current page types
if (currentPage.includes('finance.php')) { if (currentPage.includes('finance.php')) {
fetch('../api/admin_notifications.php?action=clear&type=finance'); fetch('../api/admin_notifications.php?action=clear&type=finance');
@ -455,6 +476,27 @@ function renderAdminPage($content, $title = '后台管理') {
if (lastSoundTotal !== -1 && soundTotal > lastSoundTotal) { if (lastSoundTotal !== -1 && soundTotal > lastSoundTotal) {
speak("你有新的消息,请注意查收"); speak("你有新的消息,请注意查收");
// Show a Toast notification
if (window.Swal) {
Swal.fire({
title: '新提醒',
text: '您有新的充提申请或客服消息',
icon: 'info',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 5000,
timerProgressBar: true,
didOpen: (toast) => {
toast.addEventListener('mouseenter', Swal.stopTimer)
toast.addEventListener('mouseleave', Swal.resumeTimer)
toast.onclick = () => {
if (counts.recharge > 0 || counts.withdrawal > 0) location.href = 'finance.php';
else if (counts.messages > 0) location.href = 'customer_service.php';
};
}
});
}
} }
lastTotal = total; lastTotal = total;
lastSoundTotal = soundTotal; lastSoundTotal = soundTotal;

View File

@ -66,6 +66,15 @@ if ($admin['is_agent']) {
$total = $pending_recharge + $pending_withdrawal + $pending_kyc + $active_binary + $active_spot + $active_contract + $new_messages; $total = $pending_recharge + $pending_withdrawal + $pending_kyc + $active_binary + $active_spot + $active_contract + $new_messages;
$sound_trigger_count = $total; // Trigger sound for any pending action $sound_trigger_count = $total; // Trigger sound for any pending action
// Add dashboard stats if requested
$stats = [];
if (isset($_GET['stats'])) {
$stats['total_users'] = getCount($db, "SELECT COUNT(*) FROM users", []);
$stats['total_recharge'] = (float)getCount($db, "SELECT SUM(amount) FROM finance_requests WHERE type='recharge' AND status='3'", []) ?: 0;
$stats['total_withdrawal'] = (float)getCount($db, "SELECT SUM(amount) FROM finance_requests WHERE type='withdrawal' AND status='3'", []) ?: 0;
$stats['pending_tasks'] = $pending_recharge + $pending_withdrawal + $pending_kyc;
}
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'counts' => [ 'counts' => [
@ -79,5 +88,6 @@ echo json_encode([
'users' => $new_registrations, 'users' => $new_registrations,
'total' => $total, 'total' => $total,
'sound_total' => $sound_trigger_count 'sound_total' => $sound_trigger_count
] ],
'stats' => $stats
]); ]);

View File

@ -1,6 +1,7 @@
<?php <?php
// Generated by setup_mariadb_project.sh — edit as needed. // Generated by setup_mariadb_project.sh — edit as needed.
// Baota / Local Deployment Settings - Change these to match your database // Baota / Local Deployment Settings - Change these to match your database
date_default_timezone_set('Asia/Shanghai');
define('DB_HOST', '127.0.0.1'); define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_38451'); define('DB_NAME', 'app_38451');
define('DB_USER', 'app_38451'); define('DB_USER', 'app_38451');

View File

@ -212,6 +212,7 @@ $service_link = getSetting('service_link');
</div> </div>
<script> <script>
(function() {
const csToggle = document.getElementById('cs-toggle'); const csToggle = document.getElementById('cs-toggle');
const csBox = document.getElementById('cs-box'); const csBox = document.getElementById('cs-box');
const csClose = document.getElementById('cs-close'); const csClose = document.getElementById('cs-close');
@ -220,24 +221,18 @@ const csInput = document.getElementById('cs-input');
const csMessages = document.getElementById('cs-messages'); const csMessages = document.getElementById('cs-messages');
const csUploadBtn = document.getElementById('cs-upload-btn'); const csUploadBtn = document.getElementById('cs-upload-btn');
const csFileInput = document.getElementById('cs-file-input'); const csFileInput = document.getElementById('cs-file-input');
const apiPath = (window.REL_PATH || '') + 'api/chat.php';
csUploadBtn.addEventListener('click', () => csFileInput.click()); if (csUploadBtn) csUploadBtn.addEventListener('click', () => csFileInput.click());
csFileInput.addEventListener('change', async () => { if (csFileInput) csFileInput.addEventListener('change', async () => {
if (!csFileInput.files[0]) return; if (!csFileInput.files[0]) return;
const file = csFileInput.files[0]; const file = csFileInput.files[0];
// Create local preview for "0 latency"
const localUrl = URL.createObjectURL(file); const localUrl = URL.createObjectURL(file);
const tempId = 'temp_img_' + Date.now(); const tempId = 'temp_img_' + Date.now();
const localMsgHtml = `<img src="${localUrl}" class="img-fluid rounded chat-img-preview" style="max-width: 100%; max-height: 250px; object-fit: contain; margin: 5px 0; opacity: 0.6;">`; const localMsgHtml = `<img src="${localUrl}" class="img-fluid rounded chat-img-preview" style="max-width: 100%; max-height: 250px; object-fit: contain; margin: 5px 0; opacity: 0.6;">`;
appendMessageHTML({ appendMessageHTML({ id: tempId, sender: 'user', message: localMsgHtml, created_at: new Date().toISOString() });
id: tempId,
sender: 'user',
message: localMsgHtml,
created_at: new Date().toISOString()
});
scrollToBottom(); scrollToBottom();
const formData = new FormData(); const formData = new FormData();
@ -245,16 +240,9 @@ csFileInput.addEventListener('change', async () => {
formData.append('action', 'upload_image'); formData.append('action', 'upload_image');
try { try {
const resp = await fetch('api/chat.php', { const resp = await fetch(apiPath, { method: 'POST', body: formData });
method: 'POST',
body: formData
});
const data = await resp.json(); const data = await resp.json();
document.querySelector(`[data-id="${tempId}"]`)?.remove();
// Remove local preview
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
if (tempMsg) tempMsg.remove();
if (data.success && data.message) { if (data.success && data.message) {
appendMessageHTML(data.message); appendMessageHTML(data.message);
scrollToBottom(); scrollToBottom();
@ -264,101 +252,69 @@ csFileInput.addEventListener('change', async () => {
} }
} catch (err) { } catch (err) {
console.error('Upload error:', err); console.error('Upload error:', err);
const tempMsg = document.querySelector(`[data-id="${tempId}"]`); document.querySelector(`[data-id="${tempId}"]`)?.remove();
if (tempMsg) tempMsg.remove();
} }
csFileInput.value = ''; csFileInput.value = '';
// Clean up object URL
setTimeout(() => URL.revokeObjectURL(localUrl), 5000); setTimeout(() => URL.revokeObjectURL(localUrl), 5000);
}); });
csToggle.addEventListener('click', () => { if (csToggle) csToggle.addEventListener('click', () => {
csBox.classList.toggle('d-none'); csBox.classList.toggle('d-none');
if (!csBox.classList.contains('d-none')) { if (!csBox.classList.contains('d-none')) {
const now = new Date(); const now = new Date();
const timeStr = now.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'}); const timeStr = now.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
fetch('api/chat.php?action=ping&user_time=' + encodeURIComponent(timeStr)); fetch(apiPath + '?action=ping&user_time=' + encodeURIComponent(timeStr));
scrollToBottom(); scrollToBottom();
pollMessages(); pollMessages();
} }
}); });
csClose.addEventListener('click', () => csBox.classList.add('d-none')); if (csClose) csClose.addEventListener('click', () => csBox.classList.add('d-none'));
function scrollToBottom() { function scrollToBottom() {
setTimeout(() => { setTimeout(() => { if (csMessages) csMessages.scrollTop = csMessages.scrollHeight; }, 50);
csMessages.scrollTop = csMessages.scrollHeight;
}, 50);
} }
csForm.addEventListener('submit', async (e) => { if (csForm) csForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const msg = csInput.value.trim(); const msg = csInput.value.trim();
if (!msg) return; if (!msg) return;
csInput.value = ''; csInput.value = '';
// Optimistic UI update
const tempId = 'temp_msg_' + Date.now(); const tempId = 'temp_msg_' + Date.now();
appendMessageHTML({ appendMessageHTML({ id: tempId, sender: 'user', message: msg, created_at: new Date().toISOString() });
id: tempId,
sender: 'user',
message: msg,
created_at: new Date().toISOString()
});
scrollToBottom(); scrollToBottom();
try { try {
const resp = await fetch('api/chat.php?action=send_message', { const resp = await fetch(apiPath + '?action=send_message', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `message=${encodeURIComponent(msg)}` body: `message=${encodeURIComponent(msg)}`
}); });
const data = await resp.json(); const data = await resp.json();
document.querySelector(`[data-id="${tempId}"]`)?.remove();
// Remove temp message and wait for poll to bring the real one
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
if (tempMsg) tempMsg.remove();
if (data.success) { if (data.success) {
appendMessageHTML(data.message); appendMessageHTML(data.message);
scrollToBottom(); scrollToBottom();
pollMessages(); pollMessages();
} }
} catch (err) { } catch (err) { console.error('Failed to send message:', err); }
console.error('Failed to send message:', err);
}
}); });
function appendMessageHTML(m) { function appendMessageHTML(m) {
if (!m || !m.id || document.querySelector(`[data-id="${m.id}"]`)) return; if (!m || !m.id || document.querySelector(`[data-id="${m.id}"]`)) return;
const sender = m.sender; const sender = m.sender;
const text = (m.message || '').toString(); const text = (m.message || '').toString();
const time = m.created_at || new Date().toISOString(); const time = m.created_at || new Date().toISOString();
const isImage = text.includes('<img') || text.includes('/assets/images/chat/') || text.includes('data:image'); const isImage = text.includes('<img') || text.includes('/assets/images/chat/') || text.includes('data:image');
let displayMsg = text; let displayMsg = text;
if (isImage) { if (isImage) {
if (!displayMsg.includes('<img')) { if (!displayMsg.includes('<img')) {
displayMsg = `<img src="${displayMsg}" class="img-fluid rounded chat-img-preview" style="max-width: 100%; max-height: 250px; object-fit: contain; margin: 5px 0; cursor: zoom-in;" onclick="window.showLightbox ? window.showLightbox(this.src) : window.open(this.src)">`; displayMsg = `<img src="${displayMsg.includes('assets/') ? (window.REL_PATH || '') + displayMsg : displayMsg}" class="img-fluid rounded chat-img-preview" style="max-width: 100%; max-height: 250px; object-fit: contain; margin: 5px 0; cursor: zoom-in;" onclick="window.showLightbox ? window.showLightbox(this.src) : window.open(this.src)">`;
}
if (!displayMsg.includes('chat-img-preview')) {
displayMsg = displayMsg.replace('<img ', '<img class="chat-img-preview" ');
}
if (displayMsg.includes('src="assets/')) {
displayMsg = displayMsg.replace('src="assets/', 'src="/assets/');
} }
if (!displayMsg.includes('chat-img-preview')) displayMsg = displayMsg.replace('<img ', '<img class="chat-img-preview" ');
if (displayMsg.includes('src="assets/')) displayMsg = displayMsg.replace('src="assets/', 'src="' + (window.REL_PATH || '') + 'assets/');
} }
let dateObj = time.includes('-') ? new Date(time.replace(/-/g, "/")) : new Date(time);
let dateObj;
if (typeof time === 'string' && time.includes('-')) {
dateObj = new Date(time.replace(/-/g, "/"));
} else {
dateObj = new Date(time);
}
const timeStr = isNaN(dateObj.getTime()) ? '---' : dateObj.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'}); const timeStr = isNaN(dateObj.getTime()) ? '---' : dateObj.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
const msgHtml = ` const msgHtml = `
<div class="mb-3 d-flex ${sender === 'user' ? 'justify-content-end' : 'justify-content-start'} message-item w-100 animate__animated animate__fadeInUp animate__faster" data-id="${m.id}" style="--animate-duration: 0.3s;"> <div class="mb-3 d-flex ${sender === 'user' ? 'justify-content-end' : 'justify-content-start'} message-item w-100 animate__animated animate__fadeInUp animate__faster" data-id="${m.id}" style="--animate-duration: 0.3s;">
<div class="d-flex flex-column ${sender === 'user' ? 'align-items-end' : 'align-items-start'}" style="max-width: 85%;"> <div class="d-flex flex-column ${sender === 'user' ? 'align-items-end' : 'align-items-start'}" style="max-width: 85%;">
@ -367,71 +323,41 @@ function appendMessageHTML(m) {
<div style="font-size: 9px; opacity: 0.7; position: absolute; bottom: 5px; ${sender === 'user' ? 'right: 12px;' : 'left: 12px;'} ${isImage ? 'background: rgba(0,0,0,0.5); padding: 1px 6px; border-radius: 6px; bottom: 10px; right: 10px; backdrop-filter: blur(4px);' : ''}">${timeStr}</div> <div style="font-size: 9px; opacity: 0.7; position: absolute; bottom: 5px; ${sender === 'user' ? 'right: 12px;' : 'left: 12px;'} ${isImage ? 'background: rgba(0,0,0,0.5); padding: 1px 6px; border-radius: 6px; bottom: 10px; right: 10px; backdrop-filter: blur(4px);' : ''}">${timeStr}</div>
</div> </div>
</div> </div>
</div> </div>`;
`;
csMessages.insertAdjacentHTML('beforeend', msgHtml); csMessages.insertAdjacentHTML('beforeend', msgHtml);
} }
// Polling for new messages
let lastChatIds = new Set(); let lastChatIds = new Set();
let lastPingTime = 0;
async function pollMessages() { async function pollMessages() {
if (csBox.classList.contains('d-none')) return; if (csBox.classList.contains('d-none')) return;
// Ping every 10 seconds to update user time
const now = Date.now(); const now = Date.now();
if (typeof lastPingTime === 'undefined') window.lastPingTime = 0;
if (now - lastPingTime > 10000) { if (now - lastPingTime > 10000) {
const timeStr = new Date().toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'}); const timeStr = new Date().toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
fetch('api/chat.php?action=ping&user_time=' + encodeURIComponent(timeStr)); fetch(apiPath + '?action=ping&user_time=' + encodeURIComponent(timeStr));
lastPingTime = now; lastPingTime = now;
} }
try { try {
const resp = await fetch('api/chat.php?action=get_messages'); const resp = await fetch(apiPath + '?action=get_messages');
const data = await resp.json(); const data = await resp.json();
if (data && Array.isArray(data)) { if (Array.isArray(data)) {
let hasNew = false; let hasNew = false;
data.forEach(m => { data.forEach(m => { if (!lastChatIds.has(m.id)) { appendMessageHTML(m); lastChatIds.add(m.id); hasNew = true; } });
if (!lastChatIds.has(m.id)) { if (hasNew) scrollToBottom();
appendMessageHTML(m);
lastChatIds.add(m.id);
hasNew = true;
}
});
if (hasNew) {
scrollToBottom();
}
} }
} catch (err) {} } catch (err) {}
} }
setInterval(pollMessages, 300); // 300ms polling for "zero delay" feel setInterval(pollMessages, 2000);
})();
</script> </script>
<style> <style>
.bg-darker { .bg-darker { background-color: #080808; }
background-color: #080808; footer a:hover { color: var(--primary) !important; }
}
footer a:hover {
color: var(--primary) !important;
}
#cs-messages::-webkit-scrollbar { display: none; } #cs-messages::-webkit-scrollbar { display: none; }
.bubble-user { .bubble-user { border-radius: 18px 18px 2px 18px !important; background: linear-gradient(135deg, #00c6ff, #0072ff) !important; }
border-radius: 18px 18px 2px 18px !important; .bubble-admin { border-radius: 18px 18px 18px 2px !important; background: #1e2329 !important; }
background: linear-gradient(135deg, #00c6ff, #0072ff) !important; .chat-img-preview { transition: transform 0.2s; }
} .chat-img-preview:hover { transform: scale(1.02); }
.bubble-admin {
border-radius: 18px 18px 18px 2px !important;
background: #1e2329 !important;
}
.chat-img-preview {
transition: transform 0.2s;
}
.chat-img-preview:hover {
transform: scale(1.02);
}
</style> </style>
</body> </body>
</html> </html>

View File

@ -37,6 +37,18 @@ if (isset($_SESSION['user_id'])) {
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
window.APP_ROOT = '<?= (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]" ?>';
// In case of subdirectory deployment, we can try to guess or let the user define it.
// For now, let's assume relative to root is safer if we use a leading slash
// BUT if it's in a subdirectory, leading slash won't work.
// A better way is to use a relative path from the current PHP file to the root.
<?php
$depth = substr_count(trim($_SERVER['PHP_SELF'], '/'), '/');
$relRoot = str_repeat('../', $depth);
?>
window.REL_PATH = '<?= $relRoot ?>';
</script>
<style> <style>
:root { :root {
--primary: #0062ff; --primary: #0062ff;

View File

@ -198,54 +198,13 @@ $bep20_addr = $settings['usdt_bep20_address'] ?? '0x742d35Cc6634C0532925a3b844Bc
<div class="modal-content border-0 shadow-lg overflow-hidden" style="border-radius: 24px; background: #ffffff;"> <div class="modal-content border-0 shadow-lg overflow-hidden" style="border-radius: 24px; background: #ffffff;">
<div class="modal-body p-0"> <div class="modal-body p-0">
<div class="row g-0"> <div class="row g-0">
<!-- Left Side: Online Service --> <!-- Left Side: Online Service (REMOVED) -->
<div class="col-lg-6 d-flex flex-column border-end border-light order-2 order-lg-1 chat-column" style="background: #fff0f5;">
<div class="p-4 border-bottom border-light bg-white bg-opacity-50">
<div class="d-flex align-items-center gap-3">
<div class="position-relative">
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center shadow-sm" style="width: 48px; height: 48px; background: #ff4d94 !important;">
<i class="bi bi-headset text-white fs-4"></i>
</div>
<div class="position-absolute bottom-0 end-0 bg-success border border-2 border-white rounded-circle" style="width: 14px; height: 14px;"></div>
</div>
<div class="flex-grow-1">
<h6 class="mb-0 fw-bold text-dark fs-5"><?= __('online_support') ?></h6>
<div class="d-flex align-items-center gap-2 mt-1">
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-10 small px-2 py-1" style="color: #ff4d94 !important; border-color: #ff4d94 !important;"><?= __('online') ?></span>
<span class="text-muted small"><?= __('ip') ?></span>
<span class="text-dark small fw-bold"><?= getRealIP() ?></span>
</div>
</div>
<button type="button" class="btn-close ms-auto shadow-none" data-bs-dismiss="modal"></button>
</div>
</div>
<div id="modal-chat-messages" class="flex-grow-1 p-4 overflow-y-auto" style="scrollbar-width: thin; background: #fff0f5; min-height: 300px;">
<div class="text-center text-muted small mb-4 py-3 bg-white rounded-3 border border-light">
<i class="bi bi-shield-lock-fill text-success me-2"></i><?= __('welcome_support') ?>
</div>
</div>
<div class="p-4 bg-white border-top border-light">
<form id="modal-chat-form" class="d-flex gap-2 align-items-center">
<input type="file" id="modal-chat-file" class="d-none" accept="image/*">
<button type="button" id="modal-chat-upload" class="btn btn-light border-0 rounded-circle d-flex align-items-center justify-content-center" style="width: 42px; height: 42px; background: #ffe4e1;">
<i class="bi bi-plus-lg text-primary fs-5" style="color: #ff4d94 !important;"></i>
</button>
<div class="flex-grow-1 position-relative">
<input type="text" id="modal-chat-input" class="form-control bg-light border-0 py-2 ps-3 rounded-pill shadow-none" placeholder="<?= __('type_message') ?>" style="height: 42px;">
</div>
<button type="submit" class="btn btn-primary rounded-circle d-flex align-items-center justify-content-center shadow-sm" style="width: 42px; height: 42px; background: #ff4d94 !important; border: none;">
<i class="bi bi-send-fill text-white"></i>
</button>
</form>
</div>
</div>
<!-- Right Side: Account Matching --> <!-- Right Side: Account Matching -->
<div class="col-lg-6 p-4 p-lg-5 d-flex flex-column justify-content-center info-side position-relative overflow-hidden order-1" style="background: #fff;"> <div class="col-lg-12 p-4 p-lg-5 d-flex flex-column justify-content-center info-side position-relative overflow-hidden" style="background: #fff; border-radius: 24px;">
<div class="text-center text-lg-start position-relative" style="z-index: 2;"> <div class="text-center text-lg-start position-relative" style="z-index: 2;">
<div class="mb-4 text-center"> <div class="mb-4 text-center">
<button type="button" class="btn-close position-absolute top-0 end-0 m-3 shadow-none" data-bs-dismiss="modal"></button>
<div class="d-inline-flex align-items-center gap-2 px-3 py-2 rounded-pill bg-primary bg-opacity-10 text-primary small fw-bold mb-3 border border-primary border-opacity-10" style="color: #ff4d94 !important; border-color: #ff4d94 !important;"> <div class="d-inline-flex align-items-center gap-2 px-3 py-2 rounded-pill bg-primary bg-opacity-10 text-primary small fw-bold mb-3 border border-primary border-opacity-10" style="color: #ff4d94 !important; border-color: #ff4d94 !important;">
<span class="pulse-dot-pink"></span> <span style="letter-spacing: 1px;"><?= __('waiting_allocation') ?></span> <span class="pulse-dot-pink"></span> <span style="letter-spacing: 1px;"><?= __('waiting_allocation') ?></span>
</div> </div>
@ -568,18 +527,19 @@ function openRechargeModal(initialMessage, isRestore = false, orderId = null) {
initModalChat(); initModalChat();
} }
function startStatusPolling(orderId) { function startStatusPolling(order_id) {
if (window.statusPollingInterval) clearInterval(window.statusPollingInterval); if (window.statusPollingInterval) clearInterval(window.statusPollingInterval);
const checkStatus = async () => { const checkStatus = async () => {
const modalEl = document.getElementById('rechargeModal'); const modalEl = document.getElementById('rechargeModal');
if (!modalEl || !modalEl.classList.contains('show')) return; if (!modalEl || !modalEl.classList.contains('show')) return;
try { try {
const r = await fetch(`api/recharge_status.php?id=${orderId}&_t=${Date.now()}`); const path = (window.REL_PATH || '') + `api/recharge_status.php?id=${order_id}&_t=${Date.now()}`;
const r = await fetch(path);
const data = await r.json(); const data = await r.json();
if (data.success) { if (data.success) {
console.log('Order status update:', data.status, data); console.log('Order status update:', data.status, data);
renderRechargeUI(data); renderRechargeUI(data);
if (data.status === 'finished') clearInterval(window.statusPollingInterval); if (data.status === 'finished' || data.status === '3') clearInterval(window.statusPollingInterval);
} }
} catch (e) { console.error('Status polling error:', e); } } catch (e) { console.error('Status polling error:', e); }
}; };
@ -705,6 +665,7 @@ function renderRechargeUI(data) {
} }
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
updateRate();
const savedState = localStorage.getItem('recharge_state'); const savedState = localStorage.getItem('recharge_state');
if (savedState) { if (savedState) {
const state = JSON.parse(savedState); const state = JSON.parse(savedState);
@ -805,22 +766,26 @@ function confirmFiatOrder(btn, event) {
if (isNaN(amount) || amount <= 0) { notify('warning', '<?= __("enter_amount") ?>'); return; } if (isNaN(amount) || amount <= 0) { notify('warning', '<?= __("enter_amount") ?>'); return; }
const originalText = btn.innerHTML; btn.disabled = true; btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${originalText}`; const originalText = btn.innerHTML; btn.disabled = true; btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${originalText}`;
const formData = new FormData(); formData.append('action', 'recharge'); formData.append('amount', amount / rate); formData.append('symbol', 'USDT'); formData.append('fiat_amount', amount); formData.append('fiat_currency', currency); formData.append('method', '<?= __("fiat_recharge") ?> (' + currency + ')'); const formData = new FormData(); formData.append('action', 'recharge'); formData.append('amount', amount / rate); formData.append('symbol', 'USDT'); formData.append('fiat_amount', amount); formData.append('fiat_currency', currency); formData.append('method', '<?= __("fiat_recharge") ?> (' + currency + ')');
fetch('api/finance.php', { method: 'POST', body: formData }).then(r => r.json()).then(data => { fetch((window.REL_PATH || '') + 'api/finance.php', { method: 'POST', body: formData }).then(r => r.json()).then(data => {
btn.disabled = false; btn.innerHTML = originalText; btn.disabled = false; btn.innerHTML = originalText;
if (data.success) { if (data.success) {
let msg = `<?= __("recharge_msg_fiat") ?>`; msg = msg.replace('%uid%', userId).replace('%amount%', amount).replace('%currency%', currency).replace('%rate%', rate).replace('%res%', (amount / rate).toFixed(4)); let msg = `<?= __("recharge_msg_fiat") ?>`;
msg = msg.replace('%uid%', userId).replace('%amount%', amount).replace('%currency%', currency);
openRechargeModal(msg, false, data.id); openRechargeModal(msg, false, data.id);
} else notify('error', data.error || '<?= __("request_failed") ?>'); document.getElementById('fiatAmount').value = '';
}
else notify('error', data.error || '<?= __("request_failed") ?>');
}).catch(err => { btn.disabled = false; btn.innerHTML = originalText; notify('error', err.message); }); }).catch(err => { btn.disabled = false; btn.innerHTML = originalText; notify('error', err.message); });
} }
function confirmCryptoOrder(btn, event) { function confirmCryptoOrder(btn, event) {
if (event) event.preventDefault(); if (event) event.preventDefault();
const amountInput = document.getElementById('cryptoAmount'), amount = parseFloat(amountInput.value); const amountInput = document.getElementById('cryptoAmount');
const amount = parseFloat(amountInput.value);
if (isNaN(amount) || amount <= 0) { notify('warning', '<?= __("enter_amount") ?>'); return; } if (isNaN(amount) || amount <= 0) { notify('warning', '<?= __("enter_amount") ?>'); return; }
const originalText = btn.innerHTML; btn.disabled = true; btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${originalText}`; const originalText = btn.innerHTML; btn.disabled = true; btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${originalText}`;
const formData = new FormData(); formData.append('action', 'recharge'); formData.append('amount', amount); formData.append('symbol', 'USDT'); formData.append('method', currentNetwork); const formData = new FormData(); formData.append('action', 'recharge'); formData.append('amount', amount); formData.append('symbol', 'USDT'); formData.append('method', currentNetwork);
fetch('api/finance.php', { method: 'POST', body: formData }).then(r => r.json()).then(data => { fetch((window.REL_PATH || '') + 'api/finance.php', { method: 'POST', body: formData }).then(r => r.json()).then(data => {
btn.disabled = false; btn.innerHTML = originalText; btn.disabled = false; btn.innerHTML = originalText;
if (data.success) { if (data.success) {
let msg = `<?= __("recharge_msg_crypto") ?>`; let msg = `<?= __("recharge_msg_crypto") ?>`;

View File

@ -195,6 +195,10 @@ $available = $bal['available'] ?? 0;
</style> </style>
<script> <script>
document.addEventListener('DOMContentLoaded', () => {
updateFiatWithdrawRate();
});
let currentWithdrawNetwork = 'TRC20'; let currentWithdrawNetwork = 'TRC20';
function notify(icon, title, text = '') { function notify(icon, title, text = '') {
@ -276,7 +280,7 @@ function confirmCryptoWithdraw(btn, event) {
formData.append('address', addr); formData.append('address', addr);
formData.append('password', password); formData.append('password', password);
fetch('api/finance.php', { fetch((window.REL_PATH || '') + 'api/finance.php', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
@ -333,7 +337,7 @@ function confirmFiatWithdraw(btn, event) {
formData.append('address', '<?= __('fiat_withdraw') ?> (' + currency + ')'); formData.append('address', '<?= __('fiat_withdraw') ?> (' + currency + ')');
formData.append('password', password); formData.append('password', password);
fetch('api/finance.php', { fetch((window.REL_PATH || '') + 'api/finance.php', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })