38451-vm/admin/customer_service.php
Flatlogic Bot bb14c2ece7 123456
2026-02-22 08:19:34 +00:00

818 lines
33 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
session_start();
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/layout.php';
// Check if admin
if (!isset($_SESSION['admin_id'])) {
header("Location: /admin/login.php");
exit;
}
ob_start();
?>
<style>
.chat-container {
display: flex;
height: calc(100vh - 120px);
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0,0,0,0.05);
}
.user-sidebar {
width: 300px;
border-right: 1px solid #eee;
display: flex;
flex-direction: column;
}
.main-chat {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
}
.user-list {
flex: 1;
overflow-y: auto;
}
.user-card {
padding: 12px 15px;
border-bottom: 1px solid #f8f9fa;
cursor: pointer;
transition: all 0.2s;
}
.user-card:hover {
background: #f8f9fa;
}
.user-card.active {
background: #e7f3ff;
border-left: 4px solid #007bff;
}
.user-card {
position: relative;
}
.delete-chat-btn {
position: absolute;
top: 35px;
right: 10px;
opacity: 0;
transition: opacity 0.2s;
z-index: 10;
padding: 5px;
border-radius: 4px;
background: rgba(255,255,255,0.9);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.user-card:hover .delete-chat-btn {
opacity: 1;
}
.delete-chat-btn:hover {
background: #fff0f0;
color: #dc3545 !important;
}
.chat-header {
padding: 12px 20px;
border-bottom: 1px solid #eee;
background: #fff;
}
.messages-area {
flex: 1;
padding: 20px;
background: #fdfdfd;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.msg {
max-width: 75%;
padding: 10px 16px;
font-size: 14.5px;
line-height: 1.6;
position: relative;
display: flex;
flex-direction: column;
box-shadow: 0 2px 5px rgba(0,0,0,0.03);
transition: transform 0.2s;
}
.msg:hover {
transform: translateY(-1px);
}
.msg-time {
font-size: 10px;
opacity: 0.7;
margin-top: 6px;
font-weight: 500;
}
.msg-admin {
align-self: flex-end;
margin-left: auto;
background: linear-gradient(135deg, #007bff, #0056b3);
color: #fff;
border-radius: 18px 18px 2px 18px;
}
.msg-admin .msg-time {
text-align: right;
color: rgba(255,255,255,0.8);
}
.msg-user {
align-self: flex-start;
margin-right: auto;
background: #f1f3f5;
color: #212529;
border-radius: 18px 18px 18px 2px;
border: 1px solid #e9ecef;
}
.msg-user::before {
content: attr(data-ip);
position: absolute;
top: -20px;
left: 4px;
font-size: 10px;
font-weight: bold;
color: #adb5bd;
white-space: nowrap;
}
.msg-user .msg-time {
color: #888;
}
.recall-btn {
font-size: 10px;
text-decoration: underline;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s;
}
.msg-admin:hover .recall-btn {
opacity: 1;
}
.chat-input-area {
padding: 15px;
border-top: 1px solid #eee;
}
.remark-area {
width: 250px;
border-left: 1px solid #eee;
padding: 15px;
background: #fcfcfc;
}
.status-online {
width: 10px;
height: 10px;
background: #28a745;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
}
.pulse-animation {
animation: pulse-green 2s infinite;
box-shadow: 0 0 0 rgba(40, 167, 69, 0.4);
}
@keyframes pulse-green {
0% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(40, 167, 69, 0); }
100% { box-shadow: 0 0 0 0 rgba(40, 167, 69, 0); }
}
#remark-text {
height: 150px;
font-size: 13px;
}
</style>
<div class="chat-container">
<!-- User List Sidebar -->
<div class="user-sidebar">
<div class="p-3 border-bottom bg-light">
<input type="text" id="user-search" class="form-control form-control-sm" placeholder="搜索用户/IP...">
</div>
<div class="user-list" id="user-list">
<!-- User cards filled by JS -->
</div>
</div>
<!-- Main Chat Window -->
<div class="main-chat">
<div class="chat-header" id="chat-header" style="display: none; background: #f8f9fa; border-bottom: 2px solid #e9ecef;">
<div class="d-flex justify-content-between align-items-center p-3">
<div class="d-flex align-items-center gap-3">
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center shadow-sm" style="width: 45px; height: 45px;">
<i class="bi bi-person-fill fs-4"></i>
</div>
<div>
<h5 class="m-0 fw-bold text-dark"><span id="header-name">用户名称</span> <small class="text-muted fw-normal" style="font-size: 12px;">(UID: <span id="header-uid">---</span>)</small></h5>
<div class="d-flex align-items-center gap-2 mt-1">
<span class="badge bg-danger px-2 py-1 shadow-sm" style="font-size: 11px;"><i class="bi bi-geo-alt-fill me-1"></i>实时定位: <span id="info-ip-header">---</span></span>
<span class="badge bg-secondary px-2 py-1 shadow-sm" style="font-size: 11px;"><i class="bi bi-clock-fill me-1"></i>本地时间: <span id="info-user-time">---</span></span>
</div>
</div>
</div>
<div class="text-end">
<div class="d-flex align-items-center gap-2 mb-1 justify-content-end">
<span class="status-online pulse-animation"></span>
<span class="small text-success fw-bold">在线通话中</span>
</div>
<button class="btn btn-outline-danger btn-sm py-0 px-2 border-0" onclick="deleteUser()" style="font-size: 11px;"><i class="bi bi-trash"></i> 清空记录</button>
</div>
</div>
</div>
<div class="messages-area" id="messages-area">
<div class="m-auto text-center text-muted">
<i class="bi bi-chat-left-dots fs-1 d-block mb-2"></i>
<p>请从左侧选择一个会话</p>
</div>
</div>
<div class="chat-input-area" id="input-area" style="display: none;">
<form id="chat-form" class="d-flex gap-2 align-items-center">
<input type="file" id="image-input" style="display: none;" accept="image/*">
<button type="button" class="btn btn-outline-secondary btn-sm rounded-circle" id="plus-btn" style="width: 32px; height: 32px; flex-shrink: 0;">
<i class="bi bi-plus-lg"></i>
</button>
<button type="button" class="btn btn-outline-primary btn-sm rounded-circle" id="payment-btn" style="width: 32px; height: 32px; flex-shrink: 0;" title="发送收款账号">
<i class="bi bi-bank"></i>
</button>
<input type="text" id="msg-input" class="form-control" placeholder="输入回复内容..." autocomplete="off">
<button type="submit" class="btn btn-primary btn-sm px-3">发送</button>
</form>
</div>
</div>
<!-- Payment Info Modal -->
<div class="modal fade" id="paymentModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title fw-bold"><i class="bi bi-bank me-2"></i>匹配收款账户 (法币/USDT)</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div class="alert alert-info small border-0 bg-light text-primary">
<i class="bi bi-info-circle-fill me-2"></i>填写后点击发送,前端充值弹窗将自动切换并显示此账户。
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">银行名称 / 支付方式 (Bank Name)</label>
<input type="text" id="pay-bank" class="form-control form-control-lg fs-6" placeholder="例如: 建设银行, Alipay, TRC20, etc.">
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">收款人姓名 (Payee Name)</label>
<input type="text" id="pay-name" class="form-control form-control-lg fs-6" placeholder="收款人姓名或账户别名">
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">收款账号 / 地址 (Account Number)</label>
<input type="text" id="pay-account" class="form-control form-control-lg fs-6 fw-bold text-primary" placeholder="银行卡号或钱包地址">
</div>
<div class="mb-0">
<label class="form-label small fw-bold text-muted">转账说明 / 备注 (Instructions)</label>
<textarea id="pay-note" class="form-control" rows="3" placeholder="告知用户转账时需要注意的事项例如务必备注UID"></textarea>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0 justify-content-between">
<button type="button" class="btn btn-light px-3 fw-bold" data-bs-dismiss="modal">取消</button>
<div class="d-flex gap-2">
<button type="button" class="btn btn-info text-white px-3 fw-bold shadow" onclick="notifyMatchSuccess()">
<i class="bi bi-megaphone-fill me-1"></i>匹配成功
</button>
<button type="button" class="btn btn-primary px-4 fw-bold shadow" onclick="sendPaymentInfo()">
<i class="bi bi-check-circle-fill me-1"></i>发送账户
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Remark Area -->
<div class="remark-area" id="remark-area" style="display: none;">
<h6 class="fw-bold mb-3 mt-1"><i class="bi bi-pencil-square me-1"></i> 用户备注</h6>
<div class="mb-3">
<label class="small text-muted mb-1">当前用户备注信息</label>
<textarea id="remark-text" class="form-control" placeholder="在此输入对该用户的备注..."></textarea>
</div>
<button id="save-remark-btn" class="btn btn-dark btn-sm w-100 fw-bold">保存备注</button>
<hr>
<div class="user-info-box small">
<div class="mb-2"><strong>UID:</strong> <span id="info-uid">---</span></div>
<div class="mb-2"><strong>当前IP:</strong> <span id="info-ip">---</span></div>
<div class="mb-2"><strong>注册IP:</strong> <span id="info-reg-ip">---</span></div>
<div class="mb-2"><strong>最近活跃:</strong> <span id="info-time">---</span></div>
</div>
</div>
</div>
<script>
let selectedUser = null;
let selectedIp = null;
let selectedSid = null;
let lastMsgId = 0;
let lastChatIds = new Set();
let currentUserContext = '';
let lastMsgCount = 0;
let notifySound = new Audio('https://assets.mixkit.co/active_storage/sfx/2358/2358-preview.mp3');
// Get API path dynamically
const apiPath = (window.location.origin + window.location.pathname).split('/admin/')[0] + '/api/';
async function refreshUsers() {
try {
const list = document.getElementById('user-list');
if (!list) return;
const searchInput = document.getElementById('user-search');
const search = searchInput ? searchInput.value.toLowerCase() : '';
const r = await fetch(apiPath + 'chat.php?action=admin_get_all&v=' + Date.now());
if (!r.ok) {
list.innerHTML = `<div class="p-4 text-center text-danger small">API 错误: ${r.status} ${r.statusText}</div>`;
return;
}
const users = await r.json();
if (users.success === false) {
list.innerHTML = `<div class="p-4 text-center text-danger small">服务器错误: ${users.error}</div>`;
return;
}
if (users.error || !Array.isArray(users)) return;
// Sound notification for new messages based on total unread count
let currentUnread = users.reduce((acc, u) => acc + (parseInt(u.unread_count) || 0), 0);
if (lastMsgCount !== null && currentUnread > lastMsgCount) {
notifySound.play().catch(e => {});
if (document.hidden) document.title = "【新消息】客服系统";
}
lastMsgCount = currentUnread;
// Reset title when active
window.onfocus = () => { document.title = "客服系统"; };
if (users.length === 0) {
list.innerHTML = '<div class="p-4 text-center text-muted small">暂无活跃会话</div>';
return;
}
let html = '';
users.forEach(u => {
const userId = u.user_id || 0;
const username = (u.username || '匿名访客').toString();
const uid = (u.uid || '---').toString();
const rawIp = (u.ip_address || '').toString();
const rawSid = (u.session_id || '').toString();
const displayIp = rawIp || '---';
const rawRemark = (u.remark || '').toString();
const userTime = (u.user_time || '---').toString();
const registrationIp = (u.registration_ip || '---').toString();
// Search filter
if (search && !username.toLowerCase().includes(search) && !rawIp.includes(search) && !uid.includes(search)) {
return;
}
const createdAt = u.created_at || '';
const lastTimeStr = createdAt ? createdAt.replace(/-/g, "/") : new Date().toISOString();
const lastTime = new Date(lastTimeStr);
let lastMsgText = (u.message || '').toString();
const isActive = (selectedSid && rawSid ? selectedSid === rawSid : selectedIp === rawIp) && selectedUser == userId;
if (isActive && displayIp !== '---') {
const headerIpEl = document.getElementById('info-ip-header');
if (headerIpEl && !headerIpEl.innerText.includes(displayIp)) {
headerIpEl.innerText = displayIp;
document.getElementById('info-ip').innerText = displayIp;
// Re-fetch location for the new IP
fetch(`https://ipapi.co/${displayIp}/json/`).then(r => r.json()).then(data => {
if (data.city) {
headerIpEl.innerText = `${displayIp} (${data.city}, ${data.country_name})`;
}
}).catch(() => {});
}
}
// Safe strings for onclick
const jsName = username.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const jsRemark = rawRemark.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const unreadCount = parseInt(u.unread_count || 0);
html += `
<div class="user-card ${isActive ? 'active' : ''}"
onclick="openChat('${userId}', '${rawIp}', '${rawSid}', '${jsName}', '${uid}', '${jsRemark}', '${userTime}', '${registrationIp}')">
<i class="bi bi-trash text-muted delete-chat-btn" title="删除会话" onclick="deleteChat('${userId}', '${rawIp}', '${rawSid}', event)"></i>
<div class="d-flex justify-content-between mb-1">
<span class="fw-bold small text-truncate" style="max-width: 150px;">
${username}
${unreadCount > 0 ? `<span class="badge bg-danger rounded-pill ms-1" style="font-size: 9px; padding: 2px 5px;">${unreadCount}</span>` : ''}
</span>
<span class="text-muted" style="font-size: 10px;">${isNaN(lastTime.getTime()) ? '---' : lastTime.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit'})}</span>
</div>
${rawRemark ? `<div class="small text-danger text-truncate mb-1" style="font-size: 11px;">[备注: ${rawRemark}]</div>` : ''}
<div class="small text-truncate text-muted mb-1" style="font-size: 12px;">${lastMsgText}</div>
<div class="d-flex justify-content-between align-items-center" style="font-size: 10px; margin-top: 4px; padding: 4px; background: rgba(0,123,255,0.05); border-radius: 4px;">
<span class="text-secondary">UID: ${uid}</span>
<span class="text-primary fw-bold"><i class="bi bi-geo-alt-fill me-1"></i>${displayIp}</span>
</div>
<div class="mt-1 text-muted" style="font-size: 9px;"><i class="bi bi-person-plus me-1"></i>注册IP: ${registrationIp}</div>
</div>
`;
});
list.innerHTML = html || '<div class="p-4 text-center text-muted small">未找到匹配的会话</div>';
} catch (err) {
console.error('Refresh users failed:', err);
const list = document.getElementById('user-list');
if (list) {
list.innerHTML = `<div class="p-4 text-center text-danger small">脚本运行错误: ${err.message}</div>`;
}
}
}
function openChat(userId, ip, sid, name, uid, remark, userTime, regIp) {
selectedUser = userId;
selectedIp = ip;
selectedSid = sid;
document.getElementById('header-name').innerText = name;
document.getElementById('header-uid').innerText = uid;
document.getElementById('info-ip-header').innerText = ip;
document.getElementById('info-user-time').innerText = userTime;
document.getElementById('chat-header').style.display = 'block';
document.getElementById('input-area').style.display = 'block';
document.getElementById('remark-area').style.display = 'block';
// IP Location fetch
if (ip && ip !== '---') {
fetch(`https://ipapi.co/${ip}/json/`).then(r => r.json()).then(data => {
if (data.city) {
document.getElementById('info-ip-header').innerText = `${ip} (${data.city}, ${data.country_name})`;
}
}).catch(() => {});
}
document.getElementById('remark-text').value = remark;
document.getElementById('info-uid').innerText = uid;
document.getElementById('info-ip').innerText = ip;
document.getElementById('info-reg-ip').innerText = regIp || '---';
lastMsgId = 0;
fetchMessages();
// Mark as read immediately when opening chat
const fd = new URLSearchParams();
fd.append('user_id', userId);
fd.append('ip_address', ip);
fd.append('session_id', sid);
fetch(apiPath + 'chat.php?action=mark_read&v=' + Date.now(), { method: 'POST', body: fd }).then(() => refreshUsers());
}
async function recallMessage(msgId) {
if (!confirm('确定撤回该消息吗?')) return;
const fd = new URLSearchParams();
fd.append('message_id', msgId);
const r = await fetch(apiPath + 'chat.php?action=admin_recall_message&v=' + Date.now(), { method: 'POST', body: fd });
const res = await r.json();
if (res.success) {
fetchMessages();
}
}
async function deleteChat(userId, ip, sid, event) {
if (event) event.stopPropagation();
if (!confirm('确定删除该会话及其所有记录吗?')) return;
const fd = new URLSearchParams();
fd.append('user_id', userId);
fd.append('ip_address', ip);
fd.append('session_id', sid);
const r = await fetch(apiPath + 'chat.php?action=admin_delete_user&v=' + Date.now(), { method: 'POST', body: fd });
const res = await r.json();
if (res.success) {
if (selectedSid == sid && selectedIp == ip && selectedUser == userId) {
selectedUser = null;
selectedIp = null;
selectedSid = null;
document.getElementById('chat-header').style.display = 'none';
document.getElementById('input-area').style.display = 'none';
document.getElementById('remark-area').style.display = 'none';
document.getElementById('messages-area').innerHTML = `
<div class="m-auto text-center text-muted">
<i class="bi bi-chat-left-dots fs-1 d-block mb-2"></i>
<p>请从左侧选择一个会话</p>
</div>
`;
}
refreshUsers();
}
}
async function deleteUser() {
if (!confirm('确定删除该用户的所有聊天记录吗?此操作不可恢复!')) return;
const fd = new URLSearchParams();
fd.append('user_id', selectedUser);
fd.append('ip_address', selectedIp);
fd.append('session_id', selectedSid);
const r = await fetch(apiPath + 'chat.php?action=admin_delete_user&v=' + Date.now(), { method: 'POST', body: fd });
const res = await r.json();
if (res.success) {
selectedUser = null;
selectedIp = null;
selectedSid = null;
document.getElementById('chat-header').style.display = 'none';
document.getElementById('input-area').style.display = 'none';
document.getElementById('remark-area').style.display = 'none';
document.getElementById('messages-area').innerHTML = `
<div class="m-auto text-center text-muted">
<i class="bi bi-chat-left-dots fs-1 d-block mb-2"></i>
<p>请从左侧选择一个会话</p>
</div>
`;
refreshUsers();
}
}
async function fetchMessages() {
if (!selectedIp && !selectedUser && !selectedSid) return;
try {
const r = await fetch(apiPath + `chat.php?action=get_messages&user_id=${selectedUser}&ip=${selectedIp}&session_id=${selectedSid}&v=${Date.now()}`);
const msgs = await r.json();
if (!msgs || !Array.isArray(msgs)) return;
const context = selectedUser + '_' + selectedIp + '_' + selectedSid;
if (currentUserContext !== context) {
document.getElementById('messages-area').innerHTML = '';
lastChatIds.clear();
currentUserContext = context;
}
const area = document.getElementById('messages-area');
let hasNew = false;
msgs.forEach(m => {
if (!lastChatIds.has(m.id)) {
appendMessageHTML(m);
lastChatIds.add(m.id);
hasNew = true;
}
});
if (hasNew) {
area.scrollTop = area.scrollHeight;
}
if (msgs.length > 0) {
const lastMsg = msgs[msgs.length - 1];
if (lastMsg.created_at) {
document.getElementById('info-time').innerText = new Date(lastMsg.created_at.replace(/-/g, "/")).toLocaleString('zh-CN');
}
}
} catch (err) {
console.error('Fetch messages error:', err);
}
}
function appendMessageHTML(m) {
const area = document.getElementById('messages-area');
if (!area) return;
const time = m.created_at || new Date().toISOString();
const msgDate = time.includes('-') ? new Date(time.replace(/-/g, "/")) : new Date(time);
const timeStr = msgDate.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
const recallHtml = m.sender === 'admin' ? `<span class="recall-btn text-white-50 ms-2" style="cursor:pointer; text-decoration:underline;" onclick="recallMessage(${m.id})">撤回</span>` : '';
let displayMsg = (m.message || '').toString();
const isImage = displayMsg.includes('<img') || displayMsg.includes('/assets/images/chat/') || displayMsg.includes('data:image');
if (isImage) {
if (!displayMsg.includes('<img')) {
// It's just a path or data URL, wrap it
displayMsg = `<img src="${displayMsg}" class="img-fluid rounded chat-img-preview" style="max-width: 100%; max-height: 250px; object-fit: contain; margin: 5px 0; display: block; 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/');
}
}
const div = document.createElement('div');
div.className = `msg ${m.sender === 'admin' ? 'msg-admin' : 'msg-user'}`;
div.setAttribute('data-id', m.id);
if (m.sender === 'user' && m.ip_address) {
div.setAttribute('data-ip', 'IP: ' + m.ip_address);
div.style.marginTop = '18px'; // Make space for IP label
}
if (isImage) {
div.style.padding = '6px';
div.style.background = m.sender === 'admin' ? '#007bff' : '#f8f9fa';
div.style.border = '1px solid rgba(0,0,0,0.05)';
}
div.innerHTML = `
<div class="msg-content">${displayMsg}</div>
<div class="msg-time" style="${isImage ? 'position: absolute; bottom: 8px; right: 10px; background: rgba(0,0,0,0.5); color: #fff; padding: 2px 6px; border-radius: 6px; font-size: 9px; line-height: 1; backdrop-filter: blur(4px);' : ''}">${timeStr} ${recallHtml}</div>
`;
area.appendChild(div);
}
document.getElementById('plus-btn').addEventListener('click', () => {
document.getElementById('image-input').click();
});
const paymentModalEl = document.getElementById('paymentModal');
const paymentModal = paymentModalEl ? new bootstrap.Modal(paymentModalEl) : null;
document.getElementById('payment-btn').addEventListener('click', () => {
if (paymentModal) paymentModal.show();
});
async function notifyMatchSuccess() {
if (!selectedUser) return;
const bank = document.getElementById('pay-bank').value.trim();
const name = document.getElementById('pay-name').value.trim();
const account = document.getElementById('pay-account').value.trim();
if (!bank || !name || !account) {
alert('请完整填写收款信息(银行、姓名、账号)');
return;
}
const fd = new URLSearchParams();
fd.append('user_id', selectedUser);
fd.append('bank', bank);
fd.append('name', name);
fd.append('account', account);
try {
const r = await fetch(apiPath + 'admin_recharge.php?action=match_success&v=' + Date.now(), { method: 'POST', body: fd });
const res = await r.json();
if (res.success) {
alert('匹配成功!状态已更新。若要向用户显示收款账户,请继续点击“发送账户”按钮。');
} else {
alert('错误: ' + res.error);
}
} catch(err) {}
}
async function sendPaymentInfo() {
if (!selectedUser) return;
const bank = document.getElementById('pay-bank').value.trim();
const name = document.getElementById('pay-name').value.trim();
const account = document.getElementById('pay-account').value.trim();
if (!bank || !name || !account) {
alert('请完整填写收款信息');
return;
}
const fd = new URLSearchParams();
fd.append('user_id', selectedUser);
fd.append('bank', bank);
fd.append('name', name);
fd.append('account', account);
try {
console.log('Sending account info...', { bank, name, account });
const r = await fetch(apiPath + 'admin_recharge.php?action=send_account&v=' + Date.now(), { method: 'POST', body: fd });
const res = await r.json();
if (res.success) {
console.log('Account sent successfully');
if (paymentModal) paymentModal.hide();
// Clear inputs
document.getElementById('pay-bank').value = '';
document.getElementById('pay-name').value = '';
document.getElementById('pay-account').value = '';
document.getElementById('pay-note').value = '';
alert('账户信息已发送,用户页面将立即显示收款账户');
} else {
console.error('Send failed:', res.error);
alert('发送失败: ' + res.error);
}
} catch(err) {
console.error('Network error:', err);
alert('网络请求失败');
}
}
document.getElementById('image-input').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
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: 'admin',
message: localMsgHtml,
created_at: new Date().toISOString()
});
const area = document.getElementById('messages-area');
if (area) area.scrollTop = area.scrollHeight;
const formData = new FormData();
formData.append('file', file);
formData.append('user_id', selectedUser || 0);
formData.append('ip_address', selectedIp || '');
formData.append('session_id', selectedSid || '');
try {
const r = await fetch(apiPath + 'chat.php?action=upload_image&v=' + Date.now(), {
method: 'POST',
body: formData
});
const res = await r.json();
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
if (tempMsg) tempMsg.remove();
if (res.success && res.message) {
appendMessageHTML(res.message);
if (area) area.scrollTop = area.scrollHeight;
fetchMessages();
} else {
alert('上传失败: ' + res.error);
}
} catch(err) {
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
if (tempMsg) tempMsg.remove();
}
e.target.value = '';
setTimeout(() => URL.revokeObjectURL(localUrl), 5000);
});
document.getElementById('chat-form').addEventListener('submit', async (e) => {
e.preventDefault();
const input = document.getElementById('msg-input');
const msg = input.value.trim();
if (!msg) return;
input.value = '';
const tempId = 'temp_msg_' + Date.now();
appendMessageHTML({
id: tempId,
sender: 'admin',
message: msg,
created_at: new Date().toISOString()
});
const area = document.getElementById('messages-area');
if (area) area.scrollTop = area.scrollHeight;
const fd = new URLSearchParams();
fd.append('message', msg);
fd.append('user_id', selectedUser);
fd.append('ip_address', selectedIp);
fd.append('session_id', selectedSid);
try {
const r = await fetch(apiPath + 'chat.php?action=admin_send&v=' + Date.now(), { method: 'POST', body: fd });
const res = await r.json();
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
if (tempMsg) tempMsg.remove();
if (res.success && res.message) {
appendMessageHTML(res.message);
if (area) area.scrollTop = area.scrollHeight;
fetchMessages();
}
} catch(err) {}
});
document.getElementById('save-remark-btn').addEventListener('click', async () => {
const remark = document.getElementById('remark-text').value;
const fd = new URLSearchParams();
fd.append('user_id', selectedUser);
fd.append('ip_address', selectedIp);
fd.append('session_id', selectedSid);
fd.append('remark', remark);
const r = await fetch(apiPath + 'chat.php?action=save_remark&v=' + Date.now(), { method: 'POST', body: fd });
const res = await r.json();
if (res.success) {
alert('备注已保存');
refreshUsers();
}
});
document.getElementById('user-search').addEventListener('input', refreshUsers);
// Polling logic
async function startPolling() {
await refreshUsers();
await fetchMessages();
setTimeout(startPolling, 1000);
}
startPolling();
</script>
<?php
$content = ob_get_clean();
renderAdminPage($content, '在线客服');
?>