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 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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
69
recharge.php
69
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-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") ?>`;
|
||||||
|
|||||||
@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user