38451-vm/admin/customer_service.php
2026-02-19 11:32:11 +00:00

325 lines
11 KiB
PHP

<?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;
}
.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: 12px;
}
.msg {
max-width: 80%;
padding: 8px 14px;
border-radius: 12px;
font-size: 14px;
line-height: 1.5;
position: relative;
}
.msg-admin {
align-self: flex-end;
background: #007bff;
color: #fff;
border-bottom-right-radius: 2px;
}
.msg-user {
align-self: flex-start;
background: #f0f0f0;
color: #333;
border-bottom-left-radius: 2px;
}
.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: 8px;
height: 8px;
background: #28a745;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
}
#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;">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="m-0 fw-bold" id="header-name">用户名称</h6>
<div class="small" id="header-meta"><span class="text-muted">IP:</span> <span class="text-primary fw-bold" id="info-ip-header">---</span></div>
</div>
<div>
<span class="status-online"></span>
<span class="small text-success">在线</span>
</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>
<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>
<!-- 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>最近活跃:</strong> <span id="info-time">---</span></div>
</div>
</div>
</div>
<script>
let selectedUser = null;
let selectedIp = null;
let lastMsgId = 0;
async function refreshUsers() {
const r = await fetch('/api/chat.php?action=admin_get_all');
const users = await r.json();
const list = document.getElementById('user-list');
const search = document.getElementById('user-search').value.toLowerCase();
let html = '';
users.forEach(u => {
const username = u.username || '匿名用户';
const uid = u.uid || '---';
const ip = u.ip_address;
const remark = u.remark || '';
if (search && !username.toLowerCase().includes(search) && !ip.includes(search) && !uid.toString().includes(search)) {
return;
}
const isActive = (selectedIp === ip && selectedUser == u.user_id);
html += `
<div class="user-card ${isActive ? 'active' : ''}" onclick="openChat('${u.user_id}', '${ip}', '${username}', '${uid}', '${remark.replace(/'/g, "\\'")}')">
<div class="d-flex justify-content-between mb-1">
<span class="fw-bold small text-truncate" style="max-width: 150px;">${username}</span>
<span class="text-muted" style="font-size: 10px;">${new Date(u.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
</div>
${remark ? `<div class="small text-danger text-truncate mb-1" style="font-size: 11px;">[备注: ${remark}]</div>` : ''}
<div class="small text-truncate text-muted mb-1" style="font-size: 12px;">${u.message}</div>
<div class="d-flex justify-content-between align-items-center" style="font-size: 10px;">
<span class="text-secondary">UID: ${uid}</span>
<span class="text-primary fw-bold">${ip}</span>
</div>
</div>
`;
});
list.innerHTML = html;
}
function openChat(userId, ip, name, uid, remark) {
selectedUser = userId;
selectedIp = ip;
document.getElementById('header-name').innerText = name;
document.getElementById('info-ip-header').innerText = ip;
document.getElementById('chat-header').style.display = 'block';
document.getElementById('input-area').style.display = 'block';
document.getElementById('remark-area').style.display = 'block';
document.getElementById('remark-text').value = remark;
document.getElementById('info-uid').innerText = uid;
document.getElementById('info-ip').innerText = ip;
lastMsgId = 0;
fetchMessages();
refreshUsers(); // Refresh list to update active state
}
async function fetchMessages() {
if (!selectedIp) return;
const r = await fetch(`/api/chat.php?action=get_messages&user_id=${selectedUser}&ip=${selectedIp}`);
const msgs = await r.json();
const area = document.getElementById('messages-area');
const filtered = msgs.filter(m => m.ip_address === selectedIp && (m.user_id == selectedUser || m.user_id == 0));
if (filtered.length > lastMsgId) {
area.innerHTML = '';
filtered.forEach(m => {
const div = document.createElement('div');
div.className = `msg ${m.sender === 'admin' ? 'msg-admin' : 'msg-user'}`;
div.innerHTML = m.message;
area.appendChild(div);
});
area.scrollTop = area.scrollHeight;
lastMsgId = filtered.length;
if (filtered.length > 0) {
document.getElementById('info-time').innerText = new Date(filtered[filtered.length-1].created_at).toLocaleString();
}
}
}
document.getElementById('plus-btn').addEventListener('click', () => {
document.getElementById('image-input').click();
});
document.getElementById('image-input').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('user_id', selectedUser);
formData.append('ip_address', selectedIp);
const r = await fetch('/api/chat.php?action=upload_image', {
method: 'POST',
body: formData
});
const res = await r.json();
if (res.success) {
fetchMessages();
} else {
alert('上传失败: ' + res.error);
}
e.target.value = ''; // Reset
});
document.getElementById('chat-form').addEventListener('submit', async (e) => {
e.preventDefault();
const input = document.getElementById('msg-input');
const msg = input.value.trim();
if (!msg) return;
const fd = new URLSearchParams();
fd.append('message', msg);
fd.append('user_id', selectedUser);
fd.append('ip_address', selectedIp);
await fetch('/api/chat.php?action=admin_send', { method: 'POST', body: fd });
input.value = '';
fetchMessages();
});
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('remark', remark);
const r = await fetch('/api/chat.php?action=save_remark', { method: 'POST', body: fd });
const res = await r.json();
if (res.success) {
alert('备注已保存');
refreshUsers();
}
});
document.getElementById('user-search').addEventListener('input', refreshUsers);
setInterval(refreshUsers, 500);
setInterval(fetchMessages, 500);
refreshUsers();
</script>
<?php
$content = ob_get_clean();
renderAdminPage($content, '在线客服');
?>