Autosave: 20260222-060540
This commit is contained in:
parent
9cc9c493bd
commit
9aa5517bf8
@ -18,7 +18,7 @@ ob_start();
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<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 class="text-primary"><i class="bi bi-people fs-1 opacity-25"></i></div>
|
||||
</div>
|
||||
@ -29,7 +29,7 @@ ob_start();
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<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 class="text-success"><i class="bi bi-cash-stack fs-1 opacity-25"></i></div>
|
||||
</div>
|
||||
@ -40,7 +40,7 @@ ob_start();
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<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 class="text-danger"><i class="bi bi-bank fs-1 opacity-25"></i></div>
|
||||
</div>
|
||||
@ -51,7 +51,7 @@ ob_start();
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<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 class="text-warning"><i class="bi bi-list-check fs-1 opacity-25"></i></div>
|
||||
</div>
|
||||
|
||||
@ -337,16 +337,37 @@ function renderAdminPage($content, $title = '后台管理') {
|
||||
utterance.lang = 'zh-CN';
|
||||
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() {
|
||||
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(data => {
|
||||
if (data.success) {
|
||||
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
|
||||
if (currentPage.includes('finance.php')) {
|
||||
fetch('../api/admin_notifications.php?action=clear&type=finance');
|
||||
@ -455,6 +476,27 @@ function renderAdminPage($content, $title = '后台管理') {
|
||||
|
||||
if (lastSoundTotal !== -1 && soundTotal > lastSoundTotal) {
|
||||
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;
|
||||
lastSoundTotal = soundTotal;
|
||||
|
||||
@ -66,6 +66,15 @@ if ($admin['is_agent']) {
|
||||
$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
|
||||
|
||||
// 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([
|
||||
'success' => true,
|
||||
'counts' => [
|
||||
@ -79,5 +88,6 @@ echo json_encode([
|
||||
'users' => $new_registrations,
|
||||
'total' => $total,
|
||||
'sound_total' => $sound_trigger_count
|
||||
]
|
||||
],
|
||||
'stats' => $stats
|
||||
]);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
||||
// 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_NAME', 'app_38451');
|
||||
define('DB_USER', 'app_38451');
|
||||
|
||||
@ -212,6 +212,7 @@ $service_link = getSetting('service_link');
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const csToggle = document.getElementById('cs-toggle');
|
||||
const csBox = document.getElementById('cs-box');
|
||||
const csClose = document.getElementById('cs-close');
|
||||
@ -220,24 +221,18 @@ const csInput = document.getElementById('cs-input');
|
||||
const csMessages = document.getElementById('cs-messages');
|
||||
const csUploadBtn = document.getElementById('cs-upload-btn');
|
||||
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;
|
||||
const file = csFileInput.files[0];
|
||||
|
||||
// Create local preview for "0 latency"
|
||||
const localUrl = URL.createObjectURL(file);
|
||||
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;">`;
|
||||
|
||||
appendMessageHTML({
|
||||
id: tempId,
|
||||
sender: 'user',
|
||||
message: localMsgHtml,
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
appendMessageHTML({ id: tempId, sender: 'user', message: localMsgHtml, created_at: new Date().toISOString() });
|
||||
scrollToBottom();
|
||||
|
||||
const formData = new FormData();
|
||||
@ -245,16 +240,9 @@ csFileInput.addEventListener('change', async () => {
|
||||
formData.append('action', 'upload_image');
|
||||
|
||||
try {
|
||||
const resp = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const resp = await fetch(apiPath, { method: 'POST', body: formData });
|
||||
const data = await resp.json();
|
||||
|
||||
// Remove local preview
|
||||
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
|
||||
if (tempMsg) tempMsg.remove();
|
||||
|
||||
document.querySelector(`[data-id="${tempId}"]`)?.remove();
|
||||
if (data.success && data.message) {
|
||||
appendMessageHTML(data.message);
|
||||
scrollToBottom();
|
||||
@ -264,101 +252,69 @@ csFileInput.addEventListener('change', async () => {
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload error:', err);
|
||||
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
|
||||
if (tempMsg) tempMsg.remove();
|
||||
document.querySelector(`[data-id="${tempId}"]`)?.remove();
|
||||
}
|
||||
csFileInput.value = '';
|
||||
// Clean up object URL
|
||||
setTimeout(() => URL.revokeObjectURL(localUrl), 5000);
|
||||
});
|
||||
|
||||
csToggle.addEventListener('click', () => {
|
||||
if (csToggle) csToggle.addEventListener('click', () => {
|
||||
csBox.classList.toggle('d-none');
|
||||
if (!csBox.classList.contains('d-none')) {
|
||||
const now = new Date();
|
||||
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();
|
||||
pollMessages();
|
||||
}
|
||||
});
|
||||
|
||||
csClose.addEventListener('click', () => csBox.classList.add('d-none'));
|
||||
if (csClose) csClose.addEventListener('click', () => csBox.classList.add('d-none'));
|
||||
|
||||
function scrollToBottom() {
|
||||
setTimeout(() => {
|
||||
csMessages.scrollTop = csMessages.scrollHeight;
|
||||
}, 50);
|
||||
setTimeout(() => { if (csMessages) csMessages.scrollTop = csMessages.scrollHeight; }, 50);
|
||||
}
|
||||
|
||||
csForm.addEventListener('submit', async (e) => {
|
||||
if (csForm) csForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const msg = csInput.value.trim();
|
||||
if (!msg) return;
|
||||
|
||||
csInput.value = '';
|
||||
|
||||
// Optimistic UI update
|
||||
const tempId = 'temp_msg_' + Date.now();
|
||||
appendMessageHTML({
|
||||
id: tempId,
|
||||
sender: 'user',
|
||||
message: msg,
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
appendMessageHTML({ id: tempId, sender: 'user', message: msg, created_at: new Date().toISOString() });
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
const resp = await fetch('api/chat.php?action=send_message', {
|
||||
const resp = await fetch(apiPath + '?action=send_message', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `message=${encodeURIComponent(msg)}`
|
||||
});
|
||||
const data = await resp.json();
|
||||
|
||||
// Remove temp message and wait for poll to bring the real one
|
||||
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
|
||||
if (tempMsg) tempMsg.remove();
|
||||
|
||||
document.querySelector(`[data-id="${tempId}"]`)?.remove();
|
||||
if (data.success) {
|
||||
appendMessageHTML(data.message);
|
||||
scrollToBottom();
|
||||
pollMessages();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to send message:', err);
|
||||
}
|
||||
} catch (err) { console.error('Failed to send message:', err); }
|
||||
});
|
||||
|
||||
function appendMessageHTML(m) {
|
||||
if (!m || !m.id || document.querySelector(`[data-id="${m.id}"]`)) return;
|
||||
|
||||
const sender = m.sender;
|
||||
const text = (m.message || '').toString();
|
||||
const time = m.created_at || new Date().toISOString();
|
||||
const isImage = text.includes('<img') || text.includes('/assets/images/chat/') || text.includes('data:image');
|
||||
|
||||
let displayMsg = text;
|
||||
if (isImage) {
|
||||
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)">`;
|
||||
}
|
||||
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/');
|
||||
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="' + (window.REL_PATH || '') + 'assets/');
|
||||
}
|
||||
|
||||
let dateObj;
|
||||
if (typeof time === 'string' && time.includes('-')) {
|
||||
dateObj = new Date(time.replace(/-/g, "/"));
|
||||
} else {
|
||||
dateObj = new Date(time);
|
||||
}
|
||||
let dateObj = time.includes('-') ? new Date(time.replace(/-/g, "/")) : new Date(time);
|
||||
const timeStr = isNaN(dateObj.getTime()) ? '---' : dateObj.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
||||
|
||||
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="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>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
</div>`;
|
||||
csMessages.insertAdjacentHTML('beforeend', msgHtml);
|
||||
}
|
||||
|
||||
// Polling for new messages
|
||||
let lastChatIds = new Set();
|
||||
|
||||
let lastPingTime = 0;
|
||||
async function pollMessages() {
|
||||
if (csBox.classList.contains('d-none')) return;
|
||||
|
||||
// Ping every 10 seconds to update user time
|
||||
const now = Date.now();
|
||||
if (typeof lastPingTime === 'undefined') window.lastPingTime = 0;
|
||||
if (now - lastPingTime > 10000) {
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('api/chat.php?action=get_messages');
|
||||
const resp = await fetch(apiPath + '?action=get_messages');
|
||||
const data = await resp.json();
|
||||
if (data && Array.isArray(data)) {
|
||||
if (Array.isArray(data)) {
|
||||
let hasNew = false;
|
||||
data.forEach(m => {
|
||||
if (!lastChatIds.has(m.id)) {
|
||||
appendMessageHTML(m);
|
||||
lastChatIds.add(m.id);
|
||||
hasNew = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasNew) {
|
||||
scrollToBottom();
|
||||
}
|
||||
data.forEach(m => { if (!lastChatIds.has(m.id)) { appendMessageHTML(m); lastChatIds.add(m.id); hasNew = true; } });
|
||||
if (hasNew) scrollToBottom();
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
setInterval(pollMessages, 300); // 300ms polling for "zero delay" feel
|
||||
setInterval(pollMessages, 2000);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bg-darker {
|
||||
background-color: #080808;
|
||||
}
|
||||
footer a:hover {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
.bg-darker { background-color: #080808; }
|
||||
footer a:hover { color: var(--primary) !important; }
|
||||
#cs-messages::-webkit-scrollbar { display: none; }
|
||||
.bubble-user {
|
||||
border-radius: 18px 18px 2px 18px !important;
|
||||
background: linear-gradient(135deg, #00c6ff, #0072ff) !important;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.bubble-user { border-radius: 18px 18px 2px 18px !important; background: linear-gradient(135deg, #00c6ff, #0072ff) !important; }
|
||||
.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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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 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>
|
||||
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>
|
||||
:root {
|
||||
--primary: #0062ff;
|
||||
|
||||
71
recharge.php
71
recharge.php
@ -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-body p-0">
|
||||
<div class="row g-0">
|
||||
<!-- Left Side: Online Service -->
|
||||
<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>
|
||||
|
||||
<!-- Left Side: Online Service (REMOVED) -->
|
||||
|
||||
<!-- 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="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;">
|
||||
<span class="pulse-dot-pink"></span> <span style="letter-spacing: 1px;"><?= __('waiting_allocation') ?></span>
|
||||
</div>
|
||||
@ -568,18 +527,19 @@ function openRechargeModal(initialMessage, isRestore = false, orderId = null) {
|
||||
initModalChat();
|
||||
}
|
||||
|
||||
function startStatusPolling(orderId) {
|
||||
function startStatusPolling(order_id) {
|
||||
if (window.statusPollingInterval) clearInterval(window.statusPollingInterval);
|
||||
const checkStatus = async () => {
|
||||
const modalEl = document.getElementById('rechargeModal');
|
||||
if (!modalEl || !modalEl.classList.contains('show')) return;
|
||||
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();
|
||||
if (data.success) {
|
||||
console.log('Order status update:', data.status, 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); }
|
||||
};
|
||||
@ -705,6 +665,7 @@ function renderRechargeUI(data) {
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
updateRate();
|
||||
const savedState = localStorage.getItem('recharge_state');
|
||||
if (savedState) {
|
||||
const state = JSON.parse(savedState);
|
||||
@ -805,22 +766,26 @@ function confirmFiatOrder(btn, event) {
|
||||
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 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;
|
||||
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);
|
||||
} 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); });
|
||||
}
|
||||
|
||||
function confirmCryptoOrder(btn, event) {
|
||||
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; }
|
||||
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);
|
||||
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;
|
||||
if (data.success) {
|
||||
let msg = `<?= __("recharge_msg_crypto") ?>`;
|
||||
|
||||
@ -195,6 +195,10 @@ $available = $bal['available'] ?? 0;
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateFiatWithdrawRate();
|
||||
});
|
||||
|
||||
let currentWithdrawNetwork = 'TRC20';
|
||||
|
||||
function notify(icon, title, text = '') {
|
||||
@ -276,7 +280,7 @@ function confirmCryptoWithdraw(btn, event) {
|
||||
formData.append('address', addr);
|
||||
formData.append('password', password);
|
||||
|
||||
fetch('api/finance.php', {
|
||||
fetch((window.REL_PATH || '') + 'api/finance.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
@ -333,7 +337,7 @@ function confirmFiatWithdraw(btn, event) {
|
||||
formData.append('address', '<?= __('fiat_withdraw') ?> (' + currency + ')');
|
||||
formData.append('password', password);
|
||||
|
||||
fetch('api/finance.php', {
|
||||
fetch((window.REL_PATH || '') + 'api/finance.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user