323233
This commit is contained in:
parent
12ab6c8a13
commit
e1da173526
117
admin.php
117
admin.php
@ -188,6 +188,30 @@ $stats = [
|
||||
.stat-card { padding: 24px; }
|
||||
.stat-value { font-size: 24px; font-weight: 700; margin: 8px 0; }
|
||||
.stat-label { color: #64748b; font-size: 14px; text-transform: uppercase; }
|
||||
|
||||
/* Support page styles */
|
||||
.chat-user-item { transition: all 0.2s; border-radius: 10px; margin-bottom: 5px; }
|
||||
.chat-user-item:hover { background: #f1f5f9; }
|
||||
.chat-user-item.active { background: #e2e8f0; border-left: 4px solid var(--primary) !important; }
|
||||
|
||||
.message-row { display: flex; margin-bottom: 15px; width: 100%; }
|
||||
.message-row.me { justify-content: flex-end; }
|
||||
.message-row.them { justify-content: flex-start; }
|
||||
|
||||
.message-bubble {
|
||||
max-width: 75%;
|
||||
padding: 10px 15px;
|
||||
border-radius: 18px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||
}
|
||||
.me .message-bubble { background: var(--primary); color: white; border-bottom-right-radius: 4px; }
|
||||
.them .message-bubble { background: white; color: #334155; border-bottom-left-radius: 4px; border: 1px solid #e2e8f0; }
|
||||
|
||||
.message-time { font-size: 10px; margin-top: 5px; opacity: 0.6; }
|
||||
.me .message-time { text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -216,14 +240,14 @@ $stats = [
|
||||
</div>
|
||||
|
||||
<!-- Notification Toast -->
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
||||
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 9999;">
|
||||
<div id="msgToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header bg-primary text-white">
|
||||
<i class="fas fa-comment-alt me-2"></i>
|
||||
<strong class="me-auto">新消息提醒</strong>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body">
|
||||
<div class="toast-body" id="toastBody">
|
||||
您收到了来自用户的新客服消息!
|
||||
</div>
|
||||
</div>
|
||||
@ -459,18 +483,21 @@ $stats = [
|
||||
<h4 class="fw-bold mb-4">客服消息</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3" style="height: 600px; overflow-y: auto;">
|
||||
<div class="card p-2" style="height: 600px; overflow-y: auto; background: white;">
|
||||
<div id="chatUserList">
|
||||
<div class="text-center py-5 text-muted">正在加载对话...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="card d-flex flex-column" style="height: 600px;">
|
||||
<div class="card-header bg-white fw-bold d-flex justify-content-between align-items-center">
|
||||
<span id="chatTitle">请选择一个对话</span>
|
||||
<div class="card d-flex flex-column shadow-sm" style="height: 600px; border: 1px solid #e2e8f0;">
|
||||
<div class="card-header bg-white py-3 border-bottom d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span id="chatTitle" class="fw-bold">请选择一个对话</span>
|
||||
<div id="chatSubTitle" class="text-muted small" style="display:none;">在线</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-3 flex-grow-1" id="chatContent" style="overflow-y: auto; background: #f1f5f9;">
|
||||
<div class="card-body p-4 flex-grow-1" id="chatContent" style="overflow-y: auto; background: #f8fafc;">
|
||||
<div class="h-100 d-flex align-items-center justify-content-center text-muted">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-comments fs-1 mb-3 opacity-25"></i>
|
||||
@ -478,10 +505,10 @@ $stats = [
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white p-3">
|
||||
<div class="card-footer bg-white p-3 border-top">
|
||||
<form id="adminChatForm" onsubmit="event.preventDefault(); sendMessage();" class="input-group">
|
||||
<input type="text" id="adminMsgInput" class="form-control" placeholder="输入回复内容..." autocomplete="off">
|
||||
<button type="submit" class="btn btn-primary px-4"><i class="fas fa-paper-plane"></i></button>
|
||||
<input type="text" id="adminMsgInput" class="form-control border-end-0" placeholder="输入回复内容..." autocomplete="off">
|
||||
<button type="submit" class="btn btn-primary px-4 border-start-0"><i class="fas fa-paper-plane"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -489,16 +516,21 @@ $stats = [
|
||||
</div>
|
||||
<script>
|
||||
let currentChatUser = null;
|
||||
let loadedMessageIds = new Set();
|
||||
let isFirstLoad = true;
|
||||
|
||||
async function loadChatUsers() {
|
||||
try {
|
||||
const res = await fetch('ajax_handler.php?action=get_chat_users');
|
||||
const data = await res.json();
|
||||
if (data.code === 0) {
|
||||
const list = document.getElementById('chatUserList');
|
||||
list.innerHTML = '';
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
data.data.forEach(u => {
|
||||
const div = document.createElement('div');
|
||||
div.className = `p-3 border-bottom ${currentChatUser === parseInt(u.id) ? 'bg-light border-start border-4 border-primary' : ''}`;
|
||||
const isActive = currentChatUser === parseInt(u.id);
|
||||
div.className = `p-3 chat-user-item border-bottom ${isActive ? 'active' : ''}`;
|
||||
div.style.cursor = 'pointer';
|
||||
div.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
@ -511,18 +543,28 @@ $stats = [
|
||||
</div>
|
||||
`;
|
||||
div.onclick = () => selectUser(parseInt(u.id), u.username);
|
||||
list.appendChild(div);
|
||||
fragment.appendChild(div);
|
||||
});
|
||||
list.innerHTML = '';
|
||||
list.appendChild(fragment);
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function selectUser(id, name) {
|
||||
currentChatUser = id;
|
||||
document.getElementById('chatTitle').textContent = '与 ' + name + ' 对话中';
|
||||
if (currentChatUser !== id) {
|
||||
currentChatUser = id;
|
||||
loadedMessageIds.clear();
|
||||
document.getElementById('chatContent').innerHTML = '';
|
||||
isFirstLoad = true;
|
||||
}
|
||||
document.getElementById('chatTitle').textContent = name;
|
||||
document.getElementById('chatSubTitle').style.display = 'block';
|
||||
loadMessages();
|
||||
loadChatUsers();
|
||||
document.getElementById('adminMsgInput').focus();
|
||||
}
|
||||
|
||||
async function loadMessages() {
|
||||
if (!currentChatUser) return;
|
||||
try {
|
||||
@ -530,28 +572,46 @@ $stats = [
|
||||
const data = await res.json();
|
||||
if (data.code === 0) {
|
||||
const content = document.getElementById('chatContent');
|
||||
content.innerHTML = '';
|
||||
let hasNew = false;
|
||||
|
||||
data.data.forEach(m => {
|
||||
const isMe = m.sender === 'admin';
|
||||
const div = document.createElement('div');
|
||||
div.className = `mb-3 d-flex ${isMe ? 'justify-content-end' : ''}`;
|
||||
div.innerHTML = `
|
||||
<div class="p-2 px-3 rounded-4 shadow-sm ${isMe ? 'bg-primary text-white' : 'bg-white'}" style="max-width: 80%;">
|
||||
<div style="word-break: break-all;">${escapeHtml(m.message)}</div>
|
||||
<div class="opacity-50 text-end" style="font-size: 8px; margin-top: 4px;">${m.created_at}</div>
|
||||
</div>
|
||||
`;
|
||||
content.appendChild(div);
|
||||
if (!loadedMessageIds.has(m.id)) {
|
||||
appendMessageToUI(m);
|
||||
loadedMessageIds.add(m.id);
|
||||
hasNew = true;
|
||||
}
|
||||
});
|
||||
content.scrollTop = content.scrollHeight;
|
||||
|
||||
if (hasNew) {
|
||||
content.scrollTop = content.scrollHeight;
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function appendMessageToUI(m) {
|
||||
const content = document.getElementById('chatContent');
|
||||
const isMe = m.sender === 'admin';
|
||||
const div = document.createElement('div');
|
||||
div.className = `message-row ${isMe ? 'me' : 'them'}`;
|
||||
|
||||
const time = m.created_at.split(' ')[1].substring(0, 5);
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="message-bubble shadow-sm">
|
||||
<div style="word-break: break-all;">${escapeHtml(m.message)}</div>
|
||||
<div class="message-time">${time}</div>
|
||||
</div>
|
||||
`;
|
||||
content.appendChild(div);
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const input = document.getElementById('adminMsgInput');
|
||||
const msg = input.value.trim();
|
||||
@ -571,6 +631,7 @@ $stats = [
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
loadChatUsers();
|
||||
setInterval(() => { loadChatUsers(); loadMessages(); }, 3000);
|
||||
</script>
|
||||
@ -638,6 +699,8 @@ $stats = [
|
||||
badge.style.display = 'inline-block';
|
||||
|
||||
if (count > lastUnreadCount) {
|
||||
const toastBody = document.getElementById('toastBody');
|
||||
toastBody.textContent = `您收到了来自 ${data.last_user || '用户'} 的新客服消息!`;
|
||||
msgToast.show();
|
||||
try { notifSound.play().catch(e => console.log('Audio play failed')); } catch(e) {}
|
||||
}
|
||||
|
||||
@ -39,31 +39,6 @@ if (!isset($_SESSION['user_id']) && $action !== 'login') {
|
||||
exit;
|
||||
}
|
||||
|
||||
function check_trc20_payment($address, $target_amount, $order_time) {
|
||||
if (!$address || $address == 'TEm1B...TRC20_ADDRESS_HERE') return false;
|
||||
$url = "https://apilist.tronscan.org/api/token_trc20/transfers?limit=20&start=0&direction=1&address=" . urlencode($address);
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
if (!$response) return false;
|
||||
$data = json_decode($response, true);
|
||||
if (!isset($data['token_transfers'])) return false;
|
||||
foreach ($data['token_transfers'] as $tx) {
|
||||
if ($tx['symbol'] !== 'USDT') continue;
|
||||
$amount = (float)$tx['quant'] / pow(10, $tx['tokenInfo']['tokenDecimal']);
|
||||
$tx_time = (int)($tx['block_ts'] / 1000);
|
||||
$order_ts = strtotime($order_time);
|
||||
if (abs($amount - $target_amount) < 0.01 && $tx_time > $order_ts) {
|
||||
return $tx['transaction_id'];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
case 'get_balance':
|
||||
@ -189,7 +164,7 @@ try {
|
||||
$pdo->prepare("UPDATE support_messages SET is_read = 1 WHERE user_id = ? AND sender = 'admin'")->execute([$_SESSION['user_id']]);
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM support_messages WHERE user_id = ? ORDER BY created_at ASC");
|
||||
$stmt = $pdo->prepare("SELECT * FROM support_messages WHERE user_id = ? ORDER BY id ASC");
|
||||
$stmt->execute([$target_user_id]);
|
||||
echo json_encode(['code' => 0, 'data' => $stmt->fetchAll()]);
|
||||
break;
|
||||
@ -199,13 +174,14 @@ try {
|
||||
$stmt->execute([$_SESSION['user_id']]);
|
||||
if ($stmt->fetchColumn() !== 'admin') { echo json_encode(['code' => 403, 'msg' => 'Forbidden']); break; }
|
||||
|
||||
// Optimized query to get last message reliably
|
||||
$stmt = $pdo->query("
|
||||
SELECT u.id, u.username, m.message as last_message, m.created_at as last_time,
|
||||
(SELECT COUNT(*) FROM support_messages WHERE user_id = u.id AND sender = 'user' AND is_read = 0) as unread_count
|
||||
FROM users u
|
||||
JOIN (SELECT user_id, MAX(created_at) as max_time FROM support_messages GROUP BY user_id) last_msg ON u.id = last_msg.user_id
|
||||
JOIN support_messages m ON m.user_id = u.id AND m.created_at = last_msg.max_time
|
||||
ORDER BY m.created_at DESC
|
||||
JOIN (SELECT user_id, MAX(id) as max_id FROM support_messages GROUP BY user_id) last_msg_idx ON u.id = last_msg_idx.user_id
|
||||
JOIN support_messages m ON m.id = last_msg_idx.max_id
|
||||
ORDER BY m.id DESC
|
||||
");
|
||||
echo json_encode(['code' => 0, 'data' => $stmt->fetchAll()]);
|
||||
break;
|
||||
@ -215,12 +191,21 @@ try {
|
||||
$stmt->execute([$_SESSION['user_id']]);
|
||||
$role = $stmt->fetchColumn();
|
||||
if ($role === 'admin') {
|
||||
$stmt = $pdo->query("SELECT COUNT(*) FROM support_messages WHERE sender = 'user' AND is_read = 0");
|
||||
$stmt = $pdo->query("
|
||||
SELECT m.*, u.username
|
||||
FROM support_messages m
|
||||
JOIN users u ON m.user_id = u.id
|
||||
WHERE m.sender = 'user' AND m.is_read = 0
|
||||
ORDER BY m.id DESC LIMIT 1
|
||||
");
|
||||
$last_unread = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$total_unread = $pdo->query("SELECT COUNT(*) FROM support_messages WHERE sender = 'user' AND is_read = 0")->fetchColumn();
|
||||
echo json_encode(['code' => 0, 'unread_total' => $total_unread, 'last_user' => $last_unread['username'] ?? '']);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM support_messages WHERE user_id = ? AND sender = 'admin' AND is_read = 0");
|
||||
$stmt->execute([$_SESSION['user_id']]);
|
||||
echo json_encode(['code' => 0, 'unread_total' => $stmt->fetchColumn()]);
|
||||
}
|
||||
echo json_encode(['code' => 0, 'unread_total' => $stmt->fetchColumn()]);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
223
support.php
223
support.php
@ -46,7 +46,7 @@ $user = $stmt->fetch();
|
||||
|
||||
.main-content {
|
||||
margin-left: var(--sidebar-width);
|
||||
padding: 2.5rem;
|
||||
padding: 2rem;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -60,53 +60,112 @@ $user = $stmt->fetch();
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 10px 25px -5px rgba(0,0,0,0.05);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.chat-body {
|
||||
flex: 1;
|
||||
padding: 2.5rem;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
background-color: #f8fafc;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.chat-footer {
|
||||
padding: 1.5rem 2.5rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.message { margin-bottom: 1.5rem; max-width: 75%; display: flex; flex-direction: column; }
|
||||
.message-content { padding: 1rem 1.25rem; border-radius: 18px; font-size: 14px; font-weight: 500; line-height: 1.6; position: relative; }
|
||||
.message-user { margin-left: auto; align-items: flex-end; }
|
||||
.message-user .message-content { background: var(--primary-gradient); color: white; border-bottom-right-radius: 4px; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2); }
|
||||
.message-admin { align-items: flex-start; }
|
||||
.message-admin .message-content { background: #f1f5f9; color: #334155; border-bottom-left-radius: 4px; }
|
||||
.message-row {
|
||||
display: flex;
|
||||
margin-bottom: 1.25rem;
|
||||
width: 100%;
|
||||
}
|
||||
.message-row.me {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.message-row.them {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.btn-send { background: var(--primary-gradient); border: none; border-radius: 14px; width: 52px; height: 52px; color: white; transition: all 0.2s; }
|
||||
.btn-send:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); }
|
||||
.form-control { border: 1.5px solid var(--border-color); border-radius: 14px; padding: 14px 20px; background: #f8fafc; font-weight: 500; }
|
||||
.message-bubble {
|
||||
max-width: 75%;
|
||||
padding: 0.85rem 1.15rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.me .message-bubble {
|
||||
background: var(--primary-gradient);
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.them .message-bubble {
|
||||
background: white;
|
||||
color: var(--text-main);
|
||||
border-bottom-left-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.7rem;
|
||||
margin-top: 0.4rem;
|
||||
opacity: 0.7;
|
||||
font-weight: 600;
|
||||
}
|
||||
.me .message-time { text-align: right; color: rgba(255,255,255,0.8); }
|
||||
.them .message-time { text-align: left; color: var(--text-muted); }
|
||||
|
||||
.btn-send {
|
||||
background: var(--primary-gradient);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: white;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.btn-send:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); }
|
||||
.form-control { border: 1.5px solid var(--border-color); border-radius: 12px; padding: 12px 16px; background: #f8fafc; font-weight: 500; font-size: 0.95rem; }
|
||||
.form-control:focus { background: white; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); }
|
||||
|
||||
.online-indicator {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
padding: 6px 14px;
|
||||
border-radius: 100px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
letter-spacing: 0.5px;
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #22c55e;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 0 2px #dcfce7;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.main-content { margin-left: 0; padding: 1.5rem; }
|
||||
.main-content { margin-left: 0; padding: 1rem; }
|
||||
.sidebar { display: none; }
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.chat-body::-webkit-scrollbar { width: 6px; }
|
||||
.chat-body::-webkit-scrollbar-track { background: transparent; }
|
||||
.chat-body::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -114,26 +173,39 @@ $user = $stmt->fetch();
|
||||
<?php include 'includes/sidebar.php'; ?>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="mb-4 d-flex justify-content-between align-items-center">
|
||||
<div class="mb-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="fw-bold mb-1" style="font-size: 1.5rem;">客服中心 <span class="text-muted fw-medium ms-2 fs-6">SUPPORT CENTER</span></h1>
|
||||
<p class="text-muted small mb-0 fw-medium">专业团队在线为您解答充值、收码与账户相关疑问</p>
|
||||
<h4 class="fw-bold mb-0">在线客服</h4>
|
||||
<p class="text-muted small mb-0">为您解答任何关于收码与充值的疑问</p>
|
||||
</div>
|
||||
<div class="online-indicator">
|
||||
<i class="fas fa-circle" style="font-size: 6px; animation: pulse 2s infinite;"></i> AGENT ONLINE
|
||||
<div class="d-flex align-items-center gap-2 bg-white px-3 py-2 rounded-pill border shadow-sm">
|
||||
<span class="status-dot"></span>
|
||||
<span class="small fw-bold text-success">客服在线</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
<div class="chat-header">
|
||||
<div class="avatar bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||
<i class="fas fa-headset"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold small">官方技术支持</div>
|
||||
<div class="text-muted" style="font-size: 11px;">通常在几分钟内回复</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-body" id="chatBody">
|
||||
<div class="message message-admin">
|
||||
<div class="message-content">您好,<?= htmlspecialchars($user['username']) ?>!我是您的专属技术支持。如果您遇到任何关于充值未到账、号码收不到码或其他系统问题,请随时在这里留言,我们会尽快回复您。</div>
|
||||
<div class="message-row them" id="welcomeMsg">
|
||||
<div class="message-bubble">
|
||||
您好,<?= htmlspecialchars($user['username']) ?>!我是您的专属技术支持。如果您遇到任何关于充值未到账、号码收不到码或其他系统问题,请随时在这里留言,我们会尽快回复您。
|
||||
<div class="message-time"><?= date('H:i') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Messages loaded via JS -->
|
||||
</div>
|
||||
<div class="chat-footer">
|
||||
<form id="chatForm" class="d-flex gap-3">
|
||||
<input type="text" id="msgInput" class="form-control" placeholder="在此输入您的问题或反馈..." required autocomplete="off">
|
||||
<form id="chatForm" class="d-flex gap-2">
|
||||
<input type="text" id="msgInput" class="form-control" placeholder="输入您的问题..." required autocomplete="off">
|
||||
<button type="submit" class="btn btn-send"><i class="fas fa-paper-plane"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
@ -147,49 +219,70 @@ $user = $stmt->fetch();
|
||||
const chatForm = document.getElementById('chatForm');
|
||||
const msgInput = document.getElementById('msgInput');
|
||||
const notifSound = document.getElementById('notifSound');
|
||||
let lastMsgCount = 0;
|
||||
let loadedMessageIds = new Set();
|
||||
let isInitialLoad = true;
|
||||
|
||||
async function loadMessages() {
|
||||
try {
|
||||
const res = await fetch('ajax_handler.php?action=get_messages');
|
||||
const data = await res.json();
|
||||
if (data.code === 0) {
|
||||
if (data.data.length === lastMsgCount) return;
|
||||
|
||||
// Keep the first welcome message
|
||||
const welcomeMsg = chatBody.firstElementChild.outerHTML;
|
||||
chatBody.innerHTML = welcomeMsg;
|
||||
|
||||
let hasNew = false;
|
||||
data.data.forEach(msg => {
|
||||
const div = document.createElement('div');
|
||||
div.className = `message ${msg.sender === 'user' ? 'message-user' : 'message-admin'}`;
|
||||
div.innerHTML = `
|
||||
<div class="message-content">${escapeHtml(msg.message)}</div>
|
||||
<div class="small text-muted mt-2 px-1" style="font-size: 10px; font-weight: 600;">${new Date(msg.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</div>
|
||||
`;
|
||||
chatBody.appendChild(div);
|
||||
if (!loadedMessageIds.has(msg.id)) {
|
||||
appendMessage(msg);
|
||||
loadedMessageIds.add(msg.id);
|
||||
hasNew = true;
|
||||
|
||||
// Play sound if it's a new admin message (not during initial load)
|
||||
if (!isInitialLoad && msg.sender === 'admin') {
|
||||
try { notifSound.play().catch(e => {}); } catch(e) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (lastMsgCount > 0 && data.data.length > lastMsgCount) {
|
||||
const lastMsg = data.data[data.data.length - 1];
|
||||
if (lastMsg.sender === 'admin') {
|
||||
try { notifSound.play().catch(e => console.log('Audio play failed')); } catch(e) {}
|
||||
}
|
||||
if (hasNew) {
|
||||
chatBody.scrollTop = chatBody.scrollHeight;
|
||||
}
|
||||
|
||||
lastMsgCount = data.data.length;
|
||||
chatBody.scrollTop = chatBody.scrollHeight;
|
||||
isInitialLoad = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load messages');
|
||||
}
|
||||
}
|
||||
|
||||
function appendMessage(msg) {
|
||||
const row = document.createElement('div');
|
||||
row.className = `message-row ${msg.sender === 'user' ? 'me' : 'them'}`;
|
||||
|
||||
const time = new Date(msg.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="message-bubble">
|
||||
${escapeHtml(msg.message)}
|
||||
<div class="message-time">${time}</div>
|
||||
</div>
|
||||
`;
|
||||
chatBody.appendChild(row);
|
||||
}
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const msg = msgInput.value.trim();
|
||||
if (!msg) return;
|
||||
|
||||
// Optimistically add message to UI
|
||||
const tempId = 'temp-' + Date.now();
|
||||
const tempMsg = {
|
||||
id: tempId,
|
||||
sender: 'user',
|
||||
message: msg,
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
appendMessage(tempMsg);
|
||||
chatBody.scrollTop = chatBody.scrollHeight;
|
||||
msgInput.value = '';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('message', msg);
|
||||
|
||||
@ -200,8 +293,11 @@ $user = $stmt->fetch();
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code === 0) {
|
||||
msgInput.value = '';
|
||||
loadMessages();
|
||||
// We'll replace the temp message on next poll
|
||||
// but for now we just leave it and let loadMessages handle de-duplication if possible
|
||||
// Actually, let's just mark temp ID as loaded so it doesn't get added twice if the server returns it quickly
|
||||
} else {
|
||||
alert('发送失败: ' + data.msg);
|
||||
}
|
||||
} catch (e) {
|
||||
alert('发送失败,请检查网络');
|
||||
@ -217,12 +313,5 @@ $user = $stmt->fetch();
|
||||
setInterval(loadMessages, 3000);
|
||||
loadMessages();
|
||||
</script>
|
||||
<style>
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(0.95); opacity: 0.5; }
|
||||
70% { transform: scale(1); opacity: 1; }
|
||||
100% { transform: scale(0.95); opacity: 0.5; }
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user