再次修复
@ -203,6 +203,8 @@ ob_start();
|
||||
let selectedUser = null;
|
||||
let selectedIp = null;
|
||||
let lastMsgId = 0;
|
||||
let lastChatIds = new Set();
|
||||
let currentUserContext = '';
|
||||
|
||||
async function refreshUsers() {
|
||||
const r = await fetch('/api/chat.php?action=admin_get_all');
|
||||
@ -301,50 +303,73 @@ async function deleteUser() {
|
||||
|
||||
async function fetchMessages() {
|
||||
if (!selectedIp && !selectedUser) 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');
|
||||
|
||||
// For registered users, we show all their messages. For guests, we filter by IP.
|
||||
const filtered = msgs.filter(m => {
|
||||
if (selectedUser != 0) {
|
||||
return m.user_id == selectedUser;
|
||||
} else {
|
||||
return m.ip_address === selectedIp && m.user_id == 0;
|
||||
try {
|
||||
const r = await fetch(`/api/chat.php?action=get_messages&user_id=${selectedUser}&ip=${selectedIp}`);
|
||||
const msgs = await r.json();
|
||||
if (!msgs || !Array.isArray(msgs)) return;
|
||||
|
||||
// If user changed, clear everything
|
||||
const context = selectedUser + '_' + selectedIp;
|
||||
if (currentUserContext !== context) {
|
||||
document.getElementById('messages-area').innerHTML = '';
|
||||
lastChatIds.clear();
|
||||
currentUserContext = context;
|
||||
}
|
||||
});
|
||||
|
||||
if (filtered.length > 0) {
|
||||
const isAtBottom = area.scrollTop + area.clientHeight >= area.scrollHeight - 50;
|
||||
|
||||
const area = document.getElementById('messages-area');
|
||||
let hasNew = false;
|
||||
|
||||
let html = '';
|
||||
filtered.forEach(m => {
|
||||
const msgDate = new Date(m.created_at.replace(/-/g, "/"));
|
||||
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" onclick="recallMessage(${m.id})">撤回</span>` : '';
|
||||
const isImage = m.message.indexOf('<img') !== -1;
|
||||
|
||||
html += `
|
||||
<div class="msg ${m.sender === 'admin' ? 'msg-admin' : 'msg-user'}" style="${isImage ? 'padding: 5px; background: ' + (m.sender === 'admin' ? '#007bff' : '#f0f0f0') + ';' : ''}">
|
||||
<div class="msg-content">${m.message}</div>
|
||||
<div class="msg-time" style="${isImage ? 'position: absolute; bottom: 4px; right: 10px; background: rgba(0,0,0,0.3); color: #fff; padding: 0 4px; border-radius: 4px;' : ''}">${timeStr} ${recallHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
msgs.forEach(m => {
|
||||
if (!lastChatIds.has(m.id)) {
|
||||
appendMessageHTML(m);
|
||||
lastChatIds.add(m.id);
|
||||
hasNew = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (area.innerHTML !== html) {
|
||||
area.innerHTML = html;
|
||||
if (isAtBottom || lastMsgId === 0) {
|
||||
area.scrollTop = area.scrollHeight;
|
||||
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');
|
||||
}
|
||||
}
|
||||
lastMsgId = filtered.length;
|
||||
|
||||
const lastMsg = filtered[filtered.length - 1];
|
||||
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 || area.querySelector(`[data-id="${m.id}"]`)) 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 div = document.createElement('div');
|
||||
div.className = `msg ${m.sender === 'admin' ? 'msg-admin' : 'msg-user'}`;
|
||||
div.setAttribute('data-id', m.id);
|
||||
|
||||
if (isImage) {
|
||||
div.style.padding = '5px';
|
||||
div.style.background = m.sender === 'admin' ? '#007bff' : '#f0f0f0';
|
||||
div.style.lineHeight = '0';
|
||||
}
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="msg-content">${m.message}</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>
|
||||
`;
|
||||
|
||||
area.appendChild(div);
|
||||
}
|
||||
|
||||
document.getElementById('plus-btn').addEventListener('click', () => {
|
||||
document.getElementById('image-input').click();
|
||||
});
|
||||
@ -353,22 +378,50 @@ document.getElementById('image-input').addEventListener('change', async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Local preview for "0 latency"
|
||||
const localUrl = URL.createObjectURL(file);
|
||||
const tempId = 'temp_img_' + Date.now();
|
||||
const localMsgHtml = `<img src="${localUrl}" class="img-fluid rounded" 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');
|
||||
area.scrollTop = area.scrollHeight;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('user_id', selectedUser);
|
||||
formData.append('ip_address', selectedIp);
|
||||
formData.append('user_id', selectedUser || 0);
|
||||
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);
|
||||
try {
|
||||
const r = await fetch('/api/chat.php?action=upload_image', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const res = await r.json();
|
||||
|
||||
// Remove temp
|
||||
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
|
||||
if (tempMsg) tempMsg.remove();
|
||||
|
||||
if (res.success && res.message) {
|
||||
appendMessageHTML(res.message);
|
||||
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 = ''; // Reset
|
||||
setTimeout(() => URL.revokeObjectURL(localUrl), 5000);
|
||||
});
|
||||
|
||||
document.getElementById('chat-form').addEventListener('submit', async (e) => {
|
||||
@ -377,14 +430,37 @@ document.getElementById('chat-form').addEventListener('submit', async (e) => {
|
||||
const msg = input.value.trim();
|
||||
if (!msg) return;
|
||||
|
||||
input.value = '';
|
||||
|
||||
// Optimistic UI
|
||||
const tempId = 'temp_msg_' + Date.now();
|
||||
appendMessageHTML({
|
||||
id: tempId,
|
||||
sender: 'admin',
|
||||
message: msg,
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
const area = document.getElementById('messages-area');
|
||||
area.scrollTop = area.scrollHeight;
|
||||
|
||||
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();
|
||||
try {
|
||||
const r = await fetch('/api/chat.php?action=admin_send', { 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);
|
||||
area.scrollTop = area.scrollHeight;
|
||||
fetchMessages();
|
||||
}
|
||||
} catch(err) {}
|
||||
});
|
||||
|
||||
document.getElementById('save-remark-btn').addEventListener('click', async () => {
|
||||
|
||||
@ -5,7 +5,7 @@ if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
|
||||
// Force simplified Chinese for admin
|
||||
$lang = 'zh';
|
||||
$_SESSION['lang'] = 'zh';
|
||||
// $_SESSION['lang'] = 'zh'; // Do not persist to session to avoid affecting front-end default language
|
||||
|
||||
// Admin check
|
||||
$admin = null;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../includes/lang.php';
|
||||
$lang = 'zh'; // Force Simplified Chinese for admin login
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
|
||||
|
||||
42
api/chat.php
@ -28,24 +28,35 @@ 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" style="max-width: 100%; max-height: 250px; object-fit: contain; margin: 5px 0;" onclick="window.open(\'' . $imageUrl . '\')">';
|
||||
|
||||
if (isset($_SESSION['admin_id'])) {
|
||||
$user_id = $_POST['user_id'] ?? 0;
|
||||
$user_id = (int)($_POST['user_id'] ?? 0);
|
||||
$ip = $_POST['ip_address'] ?? '';
|
||||
$sender = 'admin';
|
||||
$admin_id = $_SESSION['admin_id'];
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_id, admin_id, sender, message, ip_address) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$user_id, $admin_id, $sender, $message, $ip]);
|
||||
} else {
|
||||
$user_id = $_SESSION['user_id'] ?? 0;
|
||||
$user_id = (int)($_SESSION['user_id'] ?? 0);
|
||||
$ip = getRealIP();
|
||||
$sender = 'user';
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$user_id, 'user', $message, $ip]);
|
||||
$stmt->execute([$user_id, $sender, $message, $ip]);
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'url' => $imageUrl]);
|
||||
$newId = db()->lastInsertId();
|
||||
$createdAt = date('Y-m-d H:i:s');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'url' => $imageUrl,
|
||||
'message' => [
|
||||
'id' => $newId,
|
||||
'sender' => $sender,
|
||||
'message' => $message,
|
||||
'created_at' => $createdAt
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'error' => 'Failed to move uploaded file']);
|
||||
}
|
||||
@ -76,19 +87,19 @@ if ($action === 'send_message') {
|
||||
$message = $_POST['message'] ?? '';
|
||||
if (!$message) exit(json_encode(['success' => false]));
|
||||
|
||||
$user_id = $_SESSION['user_id'] ?? 0;
|
||||
$user_id = (int)($_SESSION['user_id'] ?? 0);
|
||||
$ip = getRealIP();
|
||||
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$user_id, 'user', $message, $ip]);
|
||||
$newId = db()->lastInsertId();
|
||||
echo json_encode(['success' => true, 'id' => $newId]);
|
||||
echo json_encode(['success' => true, 'id' => $newId, 'message' => ['id' => $newId, 'sender' => 'user', 'message' => $message, 'created_at' => date('Y-m-d H:i:s')]]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'admin_send') {
|
||||
$message = $_POST['message'] ?? '';
|
||||
$user_id = $_POST['user_id'] ?? 0;
|
||||
$user_id = (int)($_POST['user_id'] ?? 0);
|
||||
$target_ip = $_POST['ip_address'] ?? '';
|
||||
|
||||
if (!$message) exit(json_encode(['success' => false]));
|
||||
@ -99,10 +110,11 @@ if ($action === 'admin_send') {
|
||||
$stmt = db()->prepare("INSERT INTO messages (user_id, admin_id, sender, message, ip_address) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$user_id, $admin_id, $sender, $message, $target_ip]);
|
||||
$newId = db()->lastInsertId();
|
||||
echo json_encode(['success' => true, 'id' => $newId]);
|
||||
echo json_encode(['success' => true, 'id' => $newId, 'message' => ['id' => $newId, 'sender' => $sender, 'message' => $message, 'created_at' => date('Y-m-d H:i:s')]]);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
if ($action === 'ping') {
|
||||
$user_id = $_SESSION['user_id'] ?? 0;
|
||||
$ip = getRealIP();
|
||||
@ -118,7 +130,7 @@ if ($action === 'admin_get_all') {
|
||||
SELECT
|
||||
v.user_id,
|
||||
v.ip_address,
|
||||
CASE WHEN m.message LIKE '<img%' THEN '[图片]' ELSE COALESCE(m.message, '用户已进入聊天室') END as message,
|
||||
CASE WHEN m.message LIKE '<img%' THEN '[Image]' ELSE COALESCE(m.message, 'User joined') END as message,
|
||||
COALESCE(m.created_at, v.last_activity) as created_at,
|
||||
CASE WHEN u.email LIKE '%@user.byro' THEN u.username ELSE COALESCE(u.email, u.username) END as username,
|
||||
u.uid,
|
||||
@ -131,20 +143,20 @@ if ($action === 'admin_get_all') {
|
||||
MAX(last_activity) as last_activity,
|
||||
MAX(user_time) as user_time
|
||||
FROM (
|
||||
SELECT user_id, ip_address, MAX(created_at) as last_activity, NULL as user_time FROM messages GROUP BY user_id, ip_address
|
||||
SELECT COALESCE(user_id, 0) as user_id, ip_address, MAX(created_at) as last_activity, NULL as user_time FROM messages GROUP BY COALESCE(user_id, 0), ip_address
|
||||
UNION
|
||||
SELECT user_id, ip_address, MAX(last_ping) as last_activity, MAX(user_time) as user_time FROM chat_visitors GROUP BY user_id, ip_address
|
||||
SELECT COALESCE(user_id, 0) as user_id, ip_address, MAX(last_ping) as last_activity, MAX(user_time) as user_time FROM chat_visitors GROUP BY COALESCE(user_id, 0), ip_address
|
||||
) t1
|
||||
GROUP BY user_id, (CASE WHEN 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 user_id, (CASE WHEN user_id = 0 THEN ip_address ELSE '0' END)
|
||||
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)
|
||||
) m2 ON m1.id = m2.max_id
|
||||
) m ON (v.user_id = m.user_id AND (v.user_id != 0 OR v.ip_address = m.ip_address))
|
||||
) m ON (v.user_id = COALESCE(m.user_id, 0) AND (v.user_id != 0 OR v.ip_address = 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 = r.user_id AND (v.user_id != 0 OR v.ip_address = r.ip_address))
|
||||
LEFT JOIN chat_remarks r ON (v.user_id = COALESCE(r.user_id, 0) AND (v.user_id != 0 OR v.ip_address = r.ip_address))
|
||||
WHERE v.last_activity > DATE_SUB(NOW(), INTERVAL 48 HOUR)
|
||||
ORDER BY created_at DESC
|
||||
");
|
||||
|
||||
BIN
assets/images/chat/1771573402_6998109a5f249.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/images/chat/1771573456_699810d0d3b69.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/images/chat/1771573715_699811d307ae4.jpeg
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
assets/images/chat/1771573770_6998120a23a0f.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/images/chat/1771574091_6998134bbc58b.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/images/chat/1771574125_6998136d06446.gif
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
assets/images/chat/1771574423_6998149715bff.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
@ -36,7 +36,7 @@ $site_certificate = getSetting('site_certificate', '');
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-primary btn-sm rounded-pill px-3" onclick="window.open('<?= htmlspecialchars($cert) ?>', '_blank')">
|
||||
<i class="bi bi-zoom-in me-2"></i>查看高清原图
|
||||
<i class="bi bi-zoom-in me-2"></i><?= __('view_full_image') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
||||
// Baota / Local Deployment Settings - Change these to match your database
|
||||
define('DB_HOST', '127.0.0.1');
|
||||
define('DB_NAME', 'app_38451');
|
||||
define('DB_USER', 'app_38451');
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
/*
|
||||
BYRO Digital Asset Platform - Full Database Dump
|
||||
Date: 2026-02-20
|
||||
Instructions: Import this file into your MySQL/MariaDB database.
|
||||
*/
|
||||
|
||||
/*M!999999\- enable the sandbox mode */
|
||||
-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
|
||||
--
|
||||
|
||||
@ -225,43 +225,69 @@ csUploadBtn.addEventListener('click', () => csFileInput.click());
|
||||
csFileInput.addEventListener('change', async () => {
|
||||
if (!csFileInput.files[0]) return;
|
||||
const file = csFileInput.files[0];
|
||||
|
||||
// Create local preview for "0 latency"
|
||||
const localUrl = URL.createObjectURL(file);
|
||||
const tempId = 'temp_img_' + Date.now();
|
||||
const localMsgHtml = `<img src="${localUrl}" class="img-fluid rounded" style="max-width: 100%; max-height: 250px; object-fit: contain; margin: 5px 0; opacity: 0.6;">`;
|
||||
|
||||
appendMessageHTML({
|
||||
id: tempId,
|
||||
sender: 'user',
|
||||
message: localMsgHtml,
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
scrollToBottom();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('action', 'upload_image');
|
||||
|
||||
appendMessage('user', '<i class="bi bi-image"></i> <?= __("uploading") ?>');
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/chat.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
|
||||
// Remove local preview
|
||||
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
|
||||
if (tempMsg) tempMsg.remove();
|
||||
|
||||
if (data.success && data.message) {
|
||||
appendMessageHTML(data.message);
|
||||
scrollToBottom();
|
||||
pollMessages();
|
||||
} else {
|
||||
alert(data.error || '<?= __("send_failed") ?>');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload error:', err);
|
||||
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
|
||||
if (tempMsg) tempMsg.remove();
|
||||
}
|
||||
csFileInput.value = '';
|
||||
// Clean up object URL
|
||||
setTimeout(() => URL.revokeObjectURL(localUrl), 5000);
|
||||
});
|
||||
|
||||
csToggle.addEventListener('click', () => {
|
||||
csBox.classList.toggle('d-none');
|
||||
scrollToBottom();
|
||||
if (!csBox.classList.contains('d-none')) {
|
||||
const now = new Date();
|
||||
const timeStr = now.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
||||
fetch('/api/chat.php?action=ping&user_time=' + encodeURIComponent(timeStr));
|
||||
scrollToBottom();
|
||||
pollMessages();
|
||||
}
|
||||
});
|
||||
|
||||
csClose.addEventListener('click', () => csBox.classList.add('d-none'));
|
||||
|
||||
function scrollToBottom() {
|
||||
csMessages.scrollTop = csMessages.scrollHeight;
|
||||
setTimeout(() => {
|
||||
csMessages.scrollTop = csMessages.scrollHeight;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
csForm.addEventListener('submit', async (e) => {
|
||||
@ -270,6 +296,16 @@ csForm.addEventListener('submit', async (e) => {
|
||||
if (!msg) return;
|
||||
|
||||
csInput.value = '';
|
||||
|
||||
// Optimistic UI update
|
||||
const tempId = 'temp_msg_' + Date.now();
|
||||
appendMessageHTML({
|
||||
id: tempId,
|
||||
sender: 'user',
|
||||
message: msg,
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
scrollToBottom();
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/chat.php?action=send_message', {
|
||||
@ -278,7 +314,14 @@ csForm.addEventListener('submit', async (e) => {
|
||||
body: `message=${encodeURIComponent(msg)}`
|
||||
});
|
||||
const data = await resp.json();
|
||||
|
||||
// Remove temp message and wait for poll to bring the real one
|
||||
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
|
||||
if (tempMsg) tempMsg.remove();
|
||||
|
||||
if (data.success) {
|
||||
appendMessageHTML(data.message);
|
||||
scrollToBottom();
|
||||
pollMessages();
|
||||
}
|
||||
} catch (err) {
|
||||
@ -286,38 +329,43 @@ csForm.addEventListener('submit', async (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
function appendMessage(sender, text, time = null) {
|
||||
const div = document.createElement('div');
|
||||
div.className = `mb-3 d-flex flex-column ${sender === 'user' ? 'align-items-end' : 'align-items-start'}`;
|
||||
function appendMessageHTML(m) {
|
||||
if (document.querySelector(`[data-id="${m.id}"]`)) return;
|
||||
|
||||
const sender = m.sender;
|
||||
const text = m.message;
|
||||
const time = m.created_at;
|
||||
const isImage = text.indexOf('<img') !== -1;
|
||||
|
||||
let dateObj;
|
||||
if (time) {
|
||||
if (typeof time === 'string' && time.includes('-')) {
|
||||
dateObj = new Date(time.replace(/-/g, "/"));
|
||||
} else {
|
||||
dateObj = new Date();
|
||||
dateObj = new Date(time);
|
||||
}
|
||||
const timeStr = dateObj.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
||||
|
||||
div.innerHTML = `
|
||||
<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;">
|
||||
${text}
|
||||
<div style="font-size: 9px; opacity: 0.6; position: absolute; bottom: 4px; ${sender === 'user' ? 'right: 10px;' : 'left: 10px;'}">${timeStr}</div>
|
||||
|
||||
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;' : ''}">
|
||||
${text}
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
csMessages.appendChild(div);
|
||||
scrollToBottom();
|
||||
|
||||
csMessages.insertAdjacentHTML('beforeend', msgHtml);
|
||||
}
|
||||
|
||||
// Polling for new messages
|
||||
let lastMsgId = 0;
|
||||
let lastPingTime = 0;
|
||||
let lastChatHtml = '';
|
||||
let lastChatIds = new Set();
|
||||
|
||||
async function pollMessages() {
|
||||
if (csBox.classList.contains('d-none')) return;
|
||||
|
||||
// Ping every 10 seconds to update user time
|
||||
const now = Date.now();
|
||||
if (typeof lastPingTime === 'undefined') window.lastPingTime = 0;
|
||||
if (now - lastPingTime > 10000) {
|
||||
const timeStr = new Date().toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
||||
fetch('/api/chat.php?action=ping&user_time=' + encodeURIComponent(timeStr));
|
||||
@ -327,37 +375,23 @@ async function pollMessages() {
|
||||
try {
|
||||
const resp = await fetch('/api/chat.php?action=get_messages');
|
||||
const data = await resp.json();
|
||||
if (data) {
|
||||
let html = '<div class="text-center text-muted small mb-3"><?= __("welcome_support") ?></div>';
|
||||
if (data && Array.isArray(data)) {
|
||||
let hasNew = false;
|
||||
data.forEach(m => {
|
||||
const sender = m.sender;
|
||||
const text = m.message;
|
||||
const time = m.created_at;
|
||||
const isImage = text.indexOf('<img') !== -1;
|
||||
|
||||
let dateObj = new Date(time.replace(/-/g, "/"));
|
||||
const timeStr = dateObj.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
||||
|
||||
html += `
|
||||
<div class="mb-3 d-flex flex-column ${sender === 'user' ? 'align-items-end' : 'align-items-start'}">
|
||||
<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;' : ''}">
|
||||
${text}
|
||||
<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;' : ''}">${timeStr}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
if (parseInt(m.id) > lastMsgId) lastMsgId = parseInt(m.id);
|
||||
if (!lastChatIds.has(m.id)) {
|
||||
appendMessageHTML(m);
|
||||
lastChatIds.add(m.id);
|
||||
hasNew = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (csMessages.innerHTML !== html) {
|
||||
const isAtBottom = csMessages.scrollTop + csMessages.clientHeight >= csMessages.scrollHeight - 50;
|
||||
csMessages.innerHTML = html;
|
||||
if (isAtBottom) scrollToBottom();
|
||||
if (hasNew) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
setInterval(pollMessages, 300);
|
||||
setInterval(pollMessages, 300); // 300ms polling for "zero delay" feel
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
$lang = $_SESSION['lang'] ?? 'en';
|
||||
$lang = $_SESSION['lang'] ?? 'en'; // Default to English
|
||||
if (isset($_GET['lang'])) {
|
||||
$lang = $_GET['lang'] === 'en' ? 'en' : 'zh';
|
||||
$lang = $_GET['lang'] === 'zh' ? 'zh' : 'en';
|
||||
$_SESSION['lang'] = $lang;
|
||||
}
|
||||
|
||||
@ -371,6 +371,7 @@ $translations = [
|
||||
'platform_certificate' => '平台注册证书',
|
||||
'no_certificate_yet' => '暂无证书',
|
||||
'trade' => '交易',
|
||||
'view_full_image' => '查看高清原图',
|
||||
'failed' => '失败',
|
||||
'success' => '成功',
|
||||
'direction' => '方向',
|
||||
@ -891,6 +892,7 @@ $translations = [
|
||||
'platform_certificate' => 'Platform Registration Certificate',
|
||||
'no_certificate_yet' => 'No certificate available yet',
|
||||
'trade' => 'Trade',
|
||||
'view_full_image' => 'View Full Image',
|
||||
'failed' => 'Failed',
|
||||
'success' => 'Success',
|
||||
'direction' => 'Direction',
|
||||
|
||||