325 lines
11 KiB
PHP
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, 5000);
|
|
setInterval(fetchMessages, 2000);
|
|
refreshUsers();
|
|
</script>
|
|
|
|
<?php
|
|
$content = ob_get_clean();
|
|
renderAdminPage($content, '在线客服');
|
|
?>
|