Autosave: 20260221-080254
@ -49,6 +49,28 @@ ob_start();
|
||||
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;
|
||||
@ -61,38 +83,56 @@ ob_start();
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
.msg {
|
||||
max-width: 80%;
|
||||
padding: 8px 14px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
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: 4px;
|
||||
margin-top: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.msg-admin {
|
||||
align-self: flex-end;
|
||||
background: #007bff;
|
||||
margin-left: auto;
|
||||
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-radius: 18px 18px 2px 18px;
|
||||
}
|
||||
.msg-admin .msg-time {
|
||||
text-align: right;
|
||||
color: #e0e0e0;
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
.msg-user {
|
||||
align-self: flex-start;
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
border-bottom-left-radius: 2px;
|
||||
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;
|
||||
@ -118,13 +158,22 @@ ob_start();
|
||||
background: #fcfcfc;
|
||||
}
|
||||
.status-online {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
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;
|
||||
@ -144,20 +193,26 @@ ob_start();
|
||||
|
||||
<!-- 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"><span id="header-name">用户名称</span> <small class="text-muted fw-normal">(UID: <span id="header-uid">---</span>)</small></h6>
|
||||
<div class="small" id="header-meta">
|
||||
<span class="text-muted">实时 IP:</span> <span class="text-primary fw-bold" id="info-ip-header">---</span>
|
||||
<span class="mx-2 text-muted">|</span>
|
||||
<span class="text-muted">用户时间:</span> <span class="text-dark fw-bold" id="info-user-time">---</span>
|
||||
<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>
|
||||
<span class="status-online"></span>
|
||||
<span class="small text-success">在线</span>
|
||||
<button class="btn btn-link btn-sm text-danger text-decoration-none ms-2" onclick="deleteUser()"><i class="bi bi-trash"></i> 删除用户</button>
|
||||
<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>
|
||||
@ -245,6 +300,7 @@ ob_start();
|
||||
<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>
|
||||
@ -260,113 +316,124 @@ let currentUserContext = '';
|
||||
let lastMsgCount = 0;
|
||||
let notifySound = new Audio('https://assets.mixkit.co/active_storage/sfx/2358/2358-preview.mp3');
|
||||
|
||||
async function refreshUsers() {
|
||||
try {
|
||||
const list = document.getElementById('user-list');
|
||||
if (!list) return;
|
||||
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('/api/chat.php?action=admin_get_all');
|
||||
if (!r.ok) return;
|
||||
|
||||
const users = await r.json();
|
||||
if (users.error || !Array.isArray(users)) return;
|
||||
|
||||
// Sound notification for new users or new messages
|
||||
let currentTotalMsgs = users.reduce((acc, u) => acc + (u.message ? 1 : 0), 0);
|
||||
if (lastMsgCount > 0 && currentTotalMsgs > lastMsgCount) {
|
||||
notifySound.play().catch(e => {});
|
||||
// Visual feedback
|
||||
if (document.hidden) {
|
||||
document.title = "【新消息】客服系统";
|
||||
}
|
||||
}
|
||||
lastMsgCount = currentTotalMsgs;
|
||||
|
||||
// Reset title when active
|
||||
window.onfocus = () => { document.title = "客服系统"; };
|
||||
|
||||
if (users.length === 0) {
|
||||
list.innerHTML = '<div class="p-4 text-center text-muted small">暂无活跃会话 (720h内)</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
users.forEach(u => {
|
||||
const userId = u.user_id || 0;
|
||||
const username = (u.username || '匿名访客').toString();
|
||||
const uid = (u.uid || '---').toString();
|
||||
const ip = (u.ip_address || '---').toString();
|
||||
const rawRemark = (u.remark || '').toString();
|
||||
const userTime = (u.user_time || '---').toString();
|
||||
const searchInput = document.getElementById('user-search');
|
||||
const search = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
|
||||
// Search filter
|
||||
if (search && !username.toLowerCase().includes(search) && !ip.includes(search) && !uid.includes(search)) {
|
||||
const r = await fetch('/api/chat.php?action=admin_get_all');
|
||||
if (!r.ok) return;
|
||||
|
||||
const users = await r.json();
|
||||
if (users.error || !Array.isArray(users)) return;
|
||||
|
||||
// Sound notification for new users or new messages
|
||||
let currentTotalMsgs = users.reduce((acc, u) => acc + (u.message ? 1 : 0), 0);
|
||||
if (lastMsgCount > 0 && currentTotalMsgs > lastMsgCount) {
|
||||
notifySound.play().catch(e => {});
|
||||
// Visual feedback
|
||||
if (document.hidden) {
|
||||
document.title = "【新消息】客服系统";
|
||||
}
|
||||
}
|
||||
lastMsgCount = currentTotalMsgs;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
const createdAt = u.created_at || '';
|
||||
const lastTimeStr = createdAt ? createdAt.replace(/-/g, "/") : new Date().toISOString();
|
||||
const lastTime = new Date(lastTimeStr);
|
||||
|
||||
let lastMsgText = (u.message || '').toString();
|
||||
if (lastMsgText.startsWith('[PAYMENT_INFO]')) {
|
||||
lastMsgText = '[收款账号信息]';
|
||||
let html = '';
|
||||
users.forEach(u => {
|
||||
const userId = u.user_id || 0;
|
||||
const username = (u.username || '匿名访客').toString();
|
||||
const uid = (u.uid || '---').toString();
|
||||
const ip = (u.ip_address || '---').toString();
|
||||
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) && !ip.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();
|
||||
if (lastMsgText.startsWith('[PAYMENT_INFO]')) {
|
||||
lastMsgText = '[收款账号信息]';
|
||||
}
|
||||
|
||||
const isActive = (selectedIp === ip && selectedUser == userId);
|
||||
|
||||
// Safe strings for onclick
|
||||
const jsName = username.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||
const jsRemark = rawRemark.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||
|
||||
html += `
|
||||
<div class="user-card ${isActive ? 'active' : ''}"
|
||||
onclick="openChat('${userId}', '${ip}', '${jsName}', '${uid}', '${jsRemark}', '${userTime}', '${registrationIp}')">
|
||||
<i class="bi bi-trash text-muted delete-chat-btn" title="删除会话" onclick="deleteChat('${userId}', '${ip}', event)"></i>
|
||||
<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;">${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>${ip}</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>`;
|
||||
}
|
||||
|
||||
const isActive = (selectedIp === ip && selectedUser == userId);
|
||||
|
||||
// Safe strings for onclick
|
||||
const jsName = username.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||
const jsRemark = rawRemark.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
||||
|
||||
html += `
|
||||
<div class="user-card ${isActive ? 'active' : ''}"
|
||||
onclick="openChat('${userId}', '${ip}', '${jsName}', '${uid}', '${jsRemark}', '${userTime}')">
|
||||
<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;">${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;">
|
||||
<span class="text-secondary">UID: ${uid}</span>
|
||||
<span class="text-primary fw-bold">${ip}</span>
|
||||
</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, name, uid, remark, userTime) {
|
||||
selectedUser = userId;
|
||||
selectedIp = ip;
|
||||
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';
|
||||
|
||||
document.getElementById('remark-text').value = remark;
|
||||
document.getElementById('info-uid').innerText = uid;
|
||||
document.getElementById('info-ip').innerText = ip;
|
||||
|
||||
lastMsgId = 0;
|
||||
fetchMessages();
|
||||
refreshUsers();
|
||||
}
|
||||
function openChat(userId, ip, name, uid, remark, userTime, regIp) {
|
||||
selectedUser = userId;
|
||||
selectedIp = ip;
|
||||
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
|
||||
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();
|
||||
refreshUsers();
|
||||
}
|
||||
|
||||
async function recallMessage(msgId) {
|
||||
if (!confirm('确定撤回该消息吗?')) return;
|
||||
@ -379,6 +446,32 @@ async function recallMessage(msgId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteChat(userId, ip, event) {
|
||||
if (event) event.stopPropagation();
|
||||
if (!confirm('确定删除该会话及其所有记录吗?')) return;
|
||||
const fd = new URLSearchParams();
|
||||
fd.append('user_id', userId);
|
||||
fd.append('ip_address', ip);
|
||||
const r = await fetch('/api/chat.php?action=admin_delete_user', { method: 'POST', body: fd });
|
||||
const res = await r.json();
|
||||
if (res.success) {
|
||||
if (selectedUser == userId && selectedIp == ip) {
|
||||
selectedUser = null;
|
||||
selectedIp = 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();
|
||||
@ -442,26 +535,41 @@ async function fetchMessages() {
|
||||
}
|
||||
}
|
||||
|
||||
function appendMessageHTML(m) {
|
||||
const area = document.getElementById('messages-area');
|
||||
if (!area) return;
|
||||
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>` : '';
|
||||
const isImage = typeof m.message === 'string' && m.message.indexOf('<img') !== -1;
|
||||
const isPaymentInfo = typeof m.message === 'string' && m.message.startsWith('[PAYMENT_INFO]');
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = `msg ${m.sender === 'admin' ? 'msg-admin' : 'msg-user'}`;
|
||||
div.setAttribute('data-id', m.id);
|
||||
|
||||
let displayMsg = (m.message || '').toString();
|
||||
if (isImage && !displayMsg.includes('chat-img-preview')) {
|
||||
displayMsg = displayMsg.replace('<img ', '<img class="chat-img-preview" ');
|
||||
}
|
||||
if (isPaymentInfo) {
|
||||
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');
|
||||
const isPaymentInfo = displayMsg.startsWith('[PAYMENT_INFO]');
|
||||
|
||||
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 (isPaymentInfo) {
|
||||
try {
|
||||
const info = JSON.parse(displayMsg.replace('[PAYMENT_INFO]', ''));
|
||||
displayMsg = `
|
||||
@ -496,14 +604,14 @@ function appendMessageHTML(m) {
|
||||
}
|
||||
|
||||
if (isImage) {
|
||||
div.style.padding = '5px';
|
||||
div.style.background = m.sender === 'admin' ? '#007bff' : '#f0f0f0';
|
||||
div.style.lineHeight = '0';
|
||||
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.4); color: #fff; padding: 0 6px; border-radius: 4px; font-size: 9px; line-height: 1.5;' : ''}">${timeStr} ${recallHtml}</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);
|
||||
@ -687,7 +795,7 @@ document.getElementById('user-search').addEventListener('input', refreshUsers);
|
||||
async function startPolling() {
|
||||
await refreshUsers();
|
||||
await fetchMessages();
|
||||
setTimeout(startPolling, 3000);
|
||||
setTimeout(startPolling, 1000);
|
||||
}
|
||||
|
||||
startPolling();
|
||||
|
||||
@ -507,14 +507,17 @@ function renderAdminPage($content, $title = '后台管理') {
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
window.showLightbox = function(src) {
|
||||
const lightbox = document.getElementById('chat-lightbox');
|
||||
const img = document.getElementById('lightbox-img');
|
||||
if (lightbox && img) {
|
||||
img.src = src;
|
||||
lightbox.classList.add('active');
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('chat-img-preview')) {
|
||||
const lightbox = document.getElementById('chat-lightbox');
|
||||
const img = document.getElementById('lightbox-img');
|
||||
if (lightbox && img) {
|
||||
img.src = e.target.src;
|
||||
lightbox.classList.add('active');
|
||||
}
|
||||
window.showLightbox(e.target.src);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
108
api/chat.php
@ -28,11 +28,21 @@ if ($action === 'upload_image' || (isset($_POST['action']) && $_POST['action'] =
|
||||
$targetPath = $targetDir . $filename;
|
||||
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
|
||||
$imageUrl = '/assets/images/chat/' . $filename;
|
||||
$message = '<img src="' . $imageUrl . '" class="img-fluid rounded cursor-pointer chat-img-preview" style="max-width: 100%; max-height: 250px; object-fit: contain; margin: 5px 0;">';
|
||||
$message = '<img src="' . $imageUrl . '" class="img-fluid rounded chat-img-preview" style="max-width: 100%; max-height: 250px; object-fit: contain; margin: 5px 0; display: block; cursor: pointer;" onclick="if(window.showLightbox) window.showLightbox(this.src); else window.open(this.src);">';
|
||||
|
||||
if (isset($_SESSION['admin_id'])) {
|
||||
$user_id = (int)($_POST['user_id'] ?? 0);
|
||||
$ip = $_POST['ip_address'] ?? '';
|
||||
// If IP is missing, try to get it from messages or fallback to current IP
|
||||
if (empty($ip)) {
|
||||
if ($user_id != 0) {
|
||||
$stmt = db()->prepare("SELECT ip_address FROM messages WHERE user_id = ? AND ip_address != '' ORDER BY id DESC LIMIT 1");
|
||||
$stmt->execute([$user_id]);
|
||||
$ip = $stmt->fetchColumn() ?: getRealIP();
|
||||
} else {
|
||||
$ip = getRealIP();
|
||||
}
|
||||
}
|
||||
$sender = 'admin';
|
||||
$admin_id = $_SESSION['admin_id'];
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_id, admin_id, sender, message, ip_address) VALUES (?, ?, ?, ?, ?)");
|
||||
@ -40,6 +50,14 @@ if ($action === 'upload_image' || (isset($_POST['action']) && $_POST['action'] =
|
||||
} else {
|
||||
$user_id = (int)($_SESSION['user_id'] ?? 0);
|
||||
$ip = getRealIP();
|
||||
|
||||
// Fallback: If user_id is 0 but we find a user with this registration IP, associate it
|
||||
if ($user_id === 0) {
|
||||
$stmt = db()->prepare("SELECT id FROM users WHERE registration_ip = ? OR last_login_ip = ? ORDER BY id DESC LIMIT 1");
|
||||
$stmt->execute([$ip, $ip]);
|
||||
$user_id = (int)($stmt->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
$sender = 'user';
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$user_id, $sender, $message, $ip]);
|
||||
@ -47,6 +65,14 @@ if ($action === 'upload_image' || (isset($_POST['action']) && $_POST['action'] =
|
||||
|
||||
$newId = db()->lastInsertId();
|
||||
$createdAt = date('Y-m-d H:i:s');
|
||||
|
||||
// Update visitors table to ensure immediate visibility for both users and admins
|
||||
if ($sender === 'user') {
|
||||
$user_time = date('H:i:s');
|
||||
$stmt = db()->prepare("INSERT INTO chat_visitors (user_id, ip_address, user_time) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE last_ping = CURRENT_TIMESTAMP");
|
||||
$stmt->execute([$user_id, $ip, $user_time]);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'url' => $imageUrl,
|
||||
@ -70,12 +96,23 @@ if ($action === 'get_messages') {
|
||||
|
||||
// If admin is requesting, we use the provided user_id and ip
|
||||
if (isset($_SESSION['admin_id'])) {
|
||||
$stmt = db()->prepare("SELECT * FROM messages WHERE (user_id = ? AND user_id != 0) OR (user_id = 0 AND ip_address = ?) ORDER BY created_at ASC");
|
||||
$stmt = db()->prepare("SELECT * FROM messages WHERE (user_id = ? AND user_id != 0) OR (ip_address = ? AND ip_address != '') ORDER BY created_at ASC");
|
||||
$stmt->execute([$target_user_id, $target_ip]);
|
||||
} else {
|
||||
// User requesting their own messages
|
||||
$stmt = db()->prepare("SELECT * FROM messages WHERE (user_id = ? AND user_id != 0) OR (user_id = 0 AND ip_address = ?) ORDER BY created_at ASC");
|
||||
$stmt->execute([$user_id, getRealIP()]);
|
||||
// If logged in, get by user_id. Also get by IP to catch visitor history.
|
||||
$user_id = (int)($_SESSION['user_id'] ?? 0);
|
||||
$ip = getRealIP();
|
||||
|
||||
// Fallback: If user_id is 0 but we find a user with this IP, associate it
|
||||
if ($user_id === 0) {
|
||||
$stmt = db()->prepare("SELECT id FROM users WHERE registration_ip = ? OR last_login_ip = ? ORDER BY id DESC LIMIT 1");
|
||||
$stmt->execute([$ip, $ip]);
|
||||
$user_id = (int)($stmt->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("SELECT * FROM messages WHERE (user_id = ? AND user_id != 0) OR (ip_address = ? AND ip_address != '') ORDER BY created_at ASC");
|
||||
$stmt->execute([$user_id, $ip]);
|
||||
}
|
||||
|
||||
$messages = $stmt->fetchAll();
|
||||
@ -89,6 +126,13 @@ if ($action === 'send_message') {
|
||||
|
||||
$user_id = (int)($_SESSION['user_id'] ?? 0);
|
||||
$ip = getRealIP();
|
||||
|
||||
// Fallback: If user_id is 0 but we find a user with this registration IP, associate it
|
||||
if ($user_id === 0) {
|
||||
$stmt = db()->prepare("SELECT id FROM users WHERE registration_ip = ? OR last_login_ip = ? ORDER BY id DESC LIMIT 1");
|
||||
$stmt->execute([$ip, $ip]);
|
||||
$user_id = (int)($stmt->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$user_id, 'user', $message, $ip]);
|
||||
@ -112,6 +156,17 @@ if ($action === 'admin_send') {
|
||||
|
||||
if (!$message) exit(json_encode(['success' => false, 'error' => 'Empty message']));
|
||||
|
||||
// Robust IP matching: if target_ip is empty but user_id is set, try to find their last IP
|
||||
if (empty($target_ip)) {
|
||||
if ($user_id != 0) {
|
||||
$stmt = db()->prepare("SELECT ip_address FROM messages WHERE user_id = ? AND ip_address != '' ORDER BY id DESC LIMIT 1");
|
||||
$stmt->execute([$user_id]);
|
||||
$target_ip = $stmt->fetchColumn() ?: getRealIP();
|
||||
} else {
|
||||
$target_ip = getRealIP();
|
||||
}
|
||||
}
|
||||
|
||||
$admin_id = $_SESSION['admin_id'] ?? 1;
|
||||
$sender = 'admin';
|
||||
|
||||
@ -141,10 +196,10 @@ if ($action === 'admin_get_all') {
|
||||
exit;
|
||||
}
|
||||
try {
|
||||
// Robust query to get all active chat sessions
|
||||
// Robust query to get all active chat sessions, merging guest sessions into user sessions if IP matches
|
||||
$stmt = db()->query("
|
||||
SELECT
|
||||
v.user_id,
|
||||
v.final_user_id as user_id,
|
||||
v.ip_address,
|
||||
CASE
|
||||
WHEN m.message LIKE '<img%' THEN '[图片消息]'
|
||||
@ -154,32 +209,53 @@ if ($action === 'admin_get_all') {
|
||||
COALESCE(m.created_at, v.last_activity) as created_at,
|
||||
COALESCE(u.username, u.email, CONCAT('访客 ', COALESCE(v.ip_address, '0.0.0.0'))) as username,
|
||||
IFNULL(u.uid, '---') as uid,
|
||||
IFNULL(u.registration_ip, '---') as registration_ip,
|
||||
IFNULL(r.remark, '') as remark,
|
||||
IFNULL(v.user_time, '---') as user_time
|
||||
FROM (
|
||||
SELECT
|
||||
user_id,
|
||||
ip_address,
|
||||
final_user_id,
|
||||
MAX(ip_address) as ip_address,
|
||||
MAX(last_activity) as last_activity,
|
||||
MAX(user_time) as user_time,
|
||||
MAX(has_recharge) as has_recharge
|
||||
FROM (
|
||||
SELECT COALESCE(user_id, 0) as user_id, IFNULL(ip_address, '') as ip_address, created_at as last_activity, NULL as user_time, 0 as has_recharge FROM messages
|
||||
SELECT
|
||||
COALESCE(NULLIF(user_id, 0), (SELECT id FROM users WHERE registration_ip = messages.ip_address LIMIT 1), 0) as final_user_id,
|
||||
IFNULL(ip_address, '') as ip_address,
|
||||
created_at as last_activity,
|
||||
NULL as user_time,
|
||||
0 as has_recharge
|
||||
FROM messages
|
||||
UNION ALL
|
||||
SELECT COALESCE(user_id, 0) as user_id, IFNULL(ip_address, '') as ip_address, last_ping as last_activity, user_time, 0 as has_recharge FROM chat_visitors
|
||||
SELECT
|
||||
COALESCE(NULLIF(user_id, 0), (SELECT id FROM users WHERE registration_ip = chat_visitors.ip_address LIMIT 1), 0) as final_user_id,
|
||||
IFNULL(ip_address, '') as ip_address,
|
||||
last_ping as last_activity,
|
||||
user_time,
|
||||
0 as has_recharge
|
||||
FROM chat_visitors
|
||||
UNION ALL
|
||||
SELECT COALESCE(user_id, 0) as user_id, IFNULL(ip_address, '') as ip_address, created_at as last_activity, NULL as user_time, 1 as has_recharge FROM finance_requests
|
||||
SELECT
|
||||
COALESCE(NULLIF(user_id, 0), (SELECT id FROM users WHERE registration_ip = finance_requests.ip_address LIMIT 1), 0) as final_user_id,
|
||||
IFNULL(ip_address, '') as ip_address,
|
||||
created_at as last_activity,
|
||||
NULL as user_time,
|
||||
1 as has_recharge
|
||||
FROM finance_requests
|
||||
) t1
|
||||
GROUP BY (CASE WHEN user_id = 0 THEN 0 ELSE user_id END), (CASE WHEN user_id = 0 THEN ip_address ELSE '0' END)
|
||||
GROUP BY (CASE WHEN final_user_id = 0 THEN 0 ELSE final_user_id END), (CASE WHEN final_user_id = 0 THEN ip_address ELSE '0' END)
|
||||
) v
|
||||
LEFT JOIN (
|
||||
SELECT m1.* FROM messages m1
|
||||
INNER JOIN (
|
||||
SELECT MAX(id) as max_id FROM messages GROUP BY COALESCE(user_id, 0), (CASE WHEN COALESCE(user_id, 0) = 0 THEN ip_address ELSE '0' END)
|
||||
SELECT MAX(id) as max_id FROM (
|
||||
SELECT id, COALESCE(NULLIF(user_id, 0), (SELECT id FROM users WHERE registration_ip = messages.ip_address LIMIT 1), 0) as final_user_id, ip_address FROM messages
|
||||
) t2 GROUP BY (CASE WHEN final_user_id = 0 THEN 0 ELSE final_user_id END), (CASE WHEN final_user_id = 0 THEN ip_address ELSE '0' END)
|
||||
) m2 ON m1.id = m2.max_id
|
||||
) m ON (v.user_id = COALESCE(m.user_id, 0) AND (v.user_id != 0 OR IFNULL(v.ip_address, '') = IFNULL(m.ip_address, '')))
|
||||
LEFT JOIN users u ON (v.user_id = u.id AND v.user_id != 0)
|
||||
LEFT JOIN chat_remarks r ON (v.user_id = COALESCE(r.user_id, 0) AND (v.user_id != 0 OR IFNULL(v.ip_address, '') = IFNULL(m.ip_address, '')))
|
||||
) m ON (v.final_user_id = COALESCE(NULLIF(m.user_id, 0), (SELECT id FROM users WHERE registration_ip = m.ip_address LIMIT 1), 0))
|
||||
LEFT JOIN users u ON (v.final_user_id = u.id AND v.final_user_id != 0)
|
||||
LEFT JOIN chat_remarks r ON (v.final_user_id = COALESCE(r.user_id, 0) AND (v.final_user_id != 0 OR IFNULL(v.ip_address, '') = IFNULL(m.ip_address, '')))
|
||||
ORDER BY created_at DESC
|
||||
");
|
||||
echo json_encode($stmt->fetchAll());
|
||||
|
||||
BIN
assets/images/chat/1771656431_699954ef30d7d.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/images/chat/1771656734_6999561e26dde.jpeg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/images/chat/1771656835_699956831fd39.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/images/chat/1771656847_6999568fb1844.jpeg
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
assets/images/chat/1771656995_699957235bc5e.jpeg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/images/chat/1771657601_6999598181dec.gif
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
assets/images/chat/1771658354_69995c7264127.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/chat/1771658478_69995ceea47f5.jpeg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/images/chat/1771658929_69995eb18f7db.jpeg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/images/chat/1771658953_69995ec9907ab.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/images/chat/1771659438_699960ae5919d.jpeg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/images/chat/1771659466_699960ca7fdd7.jpeg
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
assets/images/chat/1771659724_699961cc83bf1.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/images/chat/1771660505_699964d9a77f0.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/images/chat/1771660518_699964e630f30.jpeg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
assets/images/chat/1771660790_699965f662dcf.jpeg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
assets/images/chat/1771660806_69996606a0afb.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
@ -45,7 +45,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$email = $username . '@user.byro'; // Fallback
|
||||
}
|
||||
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
$ip = getRealIP();
|
||||
$stmt = db()->prepare("INSERT INTO users (username, email, password_hash, uid, credit_score, total_recharge, role, registration_ip) VALUES (?, ?, ?, ?, ?, 0, 'user', ?)");
|
||||
$stmt->execute([$username, $email, $hash, $uid, 80, $ip]);
|
||||
$userId = db()->lastInsertId();
|
||||
|
||||
@ -60,6 +60,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Mobile Bottom Navigation -->
|
||||
@ -335,12 +336,20 @@ function appendMessageHTML(m) {
|
||||
const sender = m.sender;
|
||||
const text = (m.message || '').toString();
|
||||
const time = m.created_at || new Date().toISOString();
|
||||
const isImage = text.indexOf('<img') !== -1;
|
||||
const isImage = text.includes('<img') || text.includes('/assets/images/chat/') || text.includes('data:image');
|
||||
const isPaymentInfo = text.startsWith('[PAYMENT_INFO]');
|
||||
|
||||
let displayMsg = text;
|
||||
if (isImage && !displayMsg.includes('chat-img-preview')) {
|
||||
displayMsg = displayMsg.replace('<img ', '<img class="chat-img-preview" ');
|
||||
if (isImage) {
|
||||
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)">`;
|
||||
}
|
||||
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 (isPaymentInfo) {
|
||||
try {
|
||||
@ -385,10 +394,12 @@ function appendMessageHTML(m) {
|
||||
const timeStr = isNaN(dateObj.getTime()) ? '---' : dateObj.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
||||
|
||||
const msgHtml = `
|
||||
<div class="mb-3 d-flex flex-column ${sender === 'user' ? 'align-items-end' : 'align-items-start'} message-item" data-id="${m.id}">
|
||||
<div class="p-2 px-3 rounded-4 small ${sender === 'user' ? 'bg-primary text-white' : 'bg-dark text-white border border-secondary'}" style="max-width: 80%; color: #ffffff !important; word-break: break-all; position: relative; padding-bottom: 20px !important; ${isImage ? 'padding: 5px !important; padding-bottom: 5px !important; line-height: 0;' : ''}">
|
||||
${displayMsg}
|
||||
<div style="font-size: 9px; opacity: 0.6; position: absolute; bottom: 4px; ${sender === 'user' ? 'right: 10px;' : 'left: 10px;'} ${isImage ? 'background: rgba(0,0,0,0.4); padding: 0 4px; border-radius: 4px; bottom: 8px;' : ''}">${timeStr}</div>
|
||||
<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="p-2 px-3 shadow-sm ${sender === 'user' ? 'bg-primary text-white bubble-user' : 'bg-dark text-white border border-secondary bubble-admin'}" style="word-break: break-all; position: relative; padding-bottom: 22px !important; min-width: 60px; ${isImage ? 'padding: 6px !important; padding-bottom: 6px !important; border-radius: 12px !important;' : ''}">
|
||||
<div class="message-text" style="font-size: 14px; line-height: 1.5;">${displayMsg}</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>
|
||||
`;
|
||||
@ -441,6 +452,20 @@ footer a:hover {
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
#cs-messages::-webkit-scrollbar { display: none; }
|
||||
.bubble-user {
|
||||
border-radius: 18px 18px 2px 18px !important;
|
||||
background: linear-gradient(135deg, #00c6ff, #0072ff) !important;
|
||||
}
|
||||
.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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -544,14 +544,17 @@ if (isset($_SESSION['user_id'])) {
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
window.showLightbox = function(src) {
|
||||
const lightbox = document.getElementById('chat-lightbox');
|
||||
const img = document.getElementById('lightbox-img');
|
||||
if (lightbox && img) {
|
||||
img.src = src;
|
||||
lightbox.classList.add('active');
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('chat-img-preview')) {
|
||||
const lightbox = document.getElementById('chat-lightbox');
|
||||
const img = document.getElementById('lightbox-img');
|
||||
if (lightbox && img) {
|
||||
img.src = e.target.src;
|
||||
lightbox.classList.add('active');
|
||||
}
|
||||
window.showLightbox(e.target.src);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
18
recharge.php
@ -752,11 +752,19 @@ function appendModalMessage(m) {
|
||||
if (!container || document.querySelector(`[data-modal-id="${m.id}"]`)) return;
|
||||
|
||||
const sender = m.sender;
|
||||
const text = m.message;
|
||||
const text = (m.message || '').toString();
|
||||
let displayMsg = text;
|
||||
const isImage = text.indexOf('<img') !== -1;
|
||||
if (isImage && !displayMsg.includes('chat-img-preview')) {
|
||||
displayMsg = displayMsg.replace('<img ', '<img class="chat-img-preview" ');
|
||||
const isImage = text.includes('<img') || text.includes('/assets/images/chat/') || text.includes('data:image');
|
||||
if (isImage) {
|
||||
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)">`;
|
||||
}
|
||||
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/');
|
||||
}
|
||||
}
|
||||
|
||||
let timeStr = '';
|
||||
@ -773,7 +781,7 @@ function appendModalMessage(m) {
|
||||
|
||||
const html = `
|
||||
<div class="mb-3 d-flex flex-column ${sender === 'user' ? 'align-items-end' : 'align-items-start'} modal-msg" data-modal-id="${m.id}">
|
||||
<div class="msg-bubble p-2 px-3 rounded-4 small ${sender === 'user' ? 'bg-primary text-white' : 'bg-dark text-white border border-secondary border-opacity-30'}" style="max-width: 85%; position: relative; ${isImage ? 'padding: 5px !important; line-height: 0;' : 'padding-bottom: 22px !important;'}">
|
||||
<div class="msg-bubble p-2 px-3 rounded-4 small ${sender === 'user' ? 'bg-primary text-white' : 'bg-dark text-white border border-secondary border-opacity-30'}" style="max-width: 85%; position: relative; ${isImage ? 'padding: 5px !important;' : 'padding-bottom: 22px !important;'}">
|
||||
<div class="message-content" style="text-shadow: 0 1px 2px rgba(0,0,0,0.2); font-size: 14px; line-height: 1.5;">
|
||||
${displayMsg}
|
||||
</div>
|
||||
|
||||