再次修复
@ -203,6 +203,8 @@ ob_start();
|
|||||||
let selectedUser = null;
|
let selectedUser = null;
|
||||||
let selectedIp = null;
|
let selectedIp = null;
|
||||||
let lastMsgId = 0;
|
let lastMsgId = 0;
|
||||||
|
let lastChatIds = new Set();
|
||||||
|
let currentUserContext = '';
|
||||||
|
|
||||||
async function refreshUsers() {
|
async function refreshUsers() {
|
||||||
const r = await fetch('/api/chat.php?action=admin_get_all');
|
const r = await fetch('/api/chat.php?action=admin_get_all');
|
||||||
@ -301,50 +303,73 @@ async function deleteUser() {
|
|||||||
|
|
||||||
async function fetchMessages() {
|
async function fetchMessages() {
|
||||||
if (!selectedIp && !selectedUser) return;
|
if (!selectedIp && !selectedUser) return;
|
||||||
const r = await fetch(`/api/chat.php?action=get_messages&user_id=${selectedUser}&ip=${selectedIp}`);
|
try {
|
||||||
const msgs = await r.json();
|
const r = await fetch(`/api/chat.php?action=get_messages&user_id=${selectedUser}&ip=${selectedIp}`);
|
||||||
const area = document.getElementById('messages-area');
|
const msgs = await r.json();
|
||||||
|
if (!msgs || !Array.isArray(msgs)) return;
|
||||||
|
|
||||||
// For registered users, we show all their messages. For guests, we filter by IP.
|
// If user changed, clear everything
|
||||||
const filtered = msgs.filter(m => {
|
const context = selectedUser + '_' + selectedIp;
|
||||||
if (selectedUser != 0) {
|
if (currentUserContext !== context) {
|
||||||
return m.user_id == selectedUser;
|
document.getElementById('messages-area').innerHTML = '';
|
||||||
} else {
|
lastChatIds.clear();
|
||||||
return m.ip_address === selectedIp && m.user_id == 0;
|
currentUserContext = context;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (filtered.length > 0) {
|
const area = document.getElementById('messages-area');
|
||||||
const isAtBottom = area.scrollTop + area.clientHeight >= area.scrollHeight - 50;
|
let hasNew = false;
|
||||||
|
|
||||||
let html = '';
|
msgs.forEach(m => {
|
||||||
filtered.forEach(m => {
|
if (!lastChatIds.has(m.id)) {
|
||||||
const msgDate = new Date(m.created_at.replace(/-/g, "/"));
|
appendMessageHTML(m);
|
||||||
const timeStr = msgDate.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
lastChatIds.add(m.id);
|
||||||
const recallHtml = m.sender === 'admin' ? `<span class="recall-btn text-white-50" onclick="recallMessage(${m.id})">撤回</span>` : '';
|
hasNew = true;
|
||||||
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>
|
|
||||||
`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (area.innerHTML !== html) {
|
if (hasNew) {
|
||||||
area.innerHTML = html;
|
area.scrollTop = area.scrollHeight;
|
||||||
if (isAtBottom || lastMsgId === 0) {
|
}
|
||||||
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;
|
} catch (err) {
|
||||||
|
console.error('Fetch messages error:', err);
|
||||||
const lastMsg = filtered[filtered.length - 1];
|
|
||||||
document.getElementById('info-time').innerText = new Date(lastMsg.created_at.replace(/-/g, "/")).toLocaleString('zh-CN');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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('plus-btn').addEventListener('click', () => {
|
||||||
document.getElementById('image-input').click();
|
document.getElementById('image-input').click();
|
||||||
});
|
});
|
||||||
@ -353,22 +378,50 @@ document.getElementById('image-input').addEventListener('change', async (e) => {
|
|||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) return;
|
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();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('user_id', selectedUser);
|
formData.append('user_id', selectedUser || 0);
|
||||||
formData.append('ip_address', selectedIp);
|
formData.append('ip_address', selectedIp || '');
|
||||||
|
|
||||||
const r = await fetch('/api/chat.php?action=upload_image', {
|
try {
|
||||||
method: 'POST',
|
const r = await fetch('/api/chat.php?action=upload_image', {
|
||||||
body: formData
|
method: 'POST',
|
||||||
});
|
body: formData
|
||||||
const res = await r.json();
|
});
|
||||||
if (res.success) {
|
const res = await r.json();
|
||||||
fetchMessages();
|
|
||||||
} else {
|
// Remove temp
|
||||||
alert('上传失败: ' + res.error);
|
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
|
e.target.value = ''; // Reset
|
||||||
|
setTimeout(() => URL.revokeObjectURL(localUrl), 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('chat-form').addEventListener('submit', async (e) => {
|
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();
|
const msg = input.value.trim();
|
||||||
if (!msg) return;
|
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();
|
const fd = new URLSearchParams();
|
||||||
fd.append('message', msg);
|
fd.append('message', msg);
|
||||||
fd.append('user_id', selectedUser);
|
fd.append('user_id', selectedUser);
|
||||||
fd.append('ip_address', selectedIp);
|
fd.append('ip_address', selectedIp);
|
||||||
|
|
||||||
await fetch('/api/chat.php?action=admin_send', { method: 'POST', body: fd });
|
try {
|
||||||
input.value = '';
|
const r = await fetch('/api/chat.php?action=admin_send', { method: 'POST', body: fd });
|
||||||
fetchMessages();
|
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 () => {
|
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
|
// Force simplified Chinese for admin
|
||||||
$lang = 'zh';
|
$lang = 'zh';
|
||||||
$_SESSION['lang'] = 'zh';
|
// $_SESSION['lang'] = 'zh'; // Do not persist to session to avoid affecting front-end default language
|
||||||
|
|
||||||
// Admin check
|
// Admin check
|
||||||
$admin = null;
|
$admin = null;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/../db/config.php';
|
require_once __DIR__ . '/../db/config.php';
|
||||||
require_once __DIR__ . '/../includes/lang.php';
|
require_once __DIR__ . '/../includes/lang.php';
|
||||||
|
$lang = 'zh'; // Force Simplified Chinese for admin login
|
||||||
|
|
||||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
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;
|
$targetPath = $targetDir . $filename;
|
||||||
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
|
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
|
||||||
$imageUrl = '/assets/images/chat/' . $filename;
|
$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 . '\')">';
|
$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'])) {
|
if (isset($_SESSION['admin_id'])) {
|
||||||
$user_id = $_POST['user_id'] ?? 0;
|
$user_id = (int)($_POST['user_id'] ?? 0);
|
||||||
$ip = $_POST['ip_address'] ?? '';
|
$ip = $_POST['ip_address'] ?? '';
|
||||||
$sender = 'admin';
|
$sender = 'admin';
|
||||||
$admin_id = $_SESSION['admin_id'];
|
$admin_id = $_SESSION['admin_id'];
|
||||||
$stmt = db()->prepare("INSERT INTO messages (user_id, admin_id, sender, message, ip_address) VALUES (?, ?, ?, ?, ?)");
|
$stmt = db()->prepare("INSERT INTO messages (user_id, admin_id, sender, message, ip_address) VALUES (?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([$user_id, $admin_id, $sender, $message, $ip]);
|
$stmt->execute([$user_id, $admin_id, $sender, $message, $ip]);
|
||||||
} else {
|
} else {
|
||||||
$user_id = $_SESSION['user_id'] ?? 0;
|
$user_id = (int)($_SESSION['user_id'] ?? 0);
|
||||||
$ip = getRealIP();
|
$ip = getRealIP();
|
||||||
|
$sender = 'user';
|
||||||
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
|
$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 {
|
} else {
|
||||||
echo json_encode(['success' => false, 'error' => 'Failed to move uploaded file']);
|
echo json_encode(['success' => false, 'error' => 'Failed to move uploaded file']);
|
||||||
}
|
}
|
||||||
@ -76,19 +87,19 @@ if ($action === 'send_message') {
|
|||||||
$message = $_POST['message'] ?? '';
|
$message = $_POST['message'] ?? '';
|
||||||
if (!$message) exit(json_encode(['success' => false]));
|
if (!$message) exit(json_encode(['success' => false]));
|
||||||
|
|
||||||
$user_id = $_SESSION['user_id'] ?? 0;
|
$user_id = (int)($_SESSION['user_id'] ?? 0);
|
||||||
$ip = getRealIP();
|
$ip = getRealIP();
|
||||||
|
|
||||||
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
|
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
|
||||||
$stmt->execute([$user_id, 'user', $message, $ip]);
|
$stmt->execute([$user_id, 'user', $message, $ip]);
|
||||||
$newId = db()->lastInsertId();
|
$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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($action === 'admin_send') {
|
if ($action === 'admin_send') {
|
||||||
$message = $_POST['message'] ?? '';
|
$message = $_POST['message'] ?? '';
|
||||||
$user_id = $_POST['user_id'] ?? 0;
|
$user_id = (int)($_POST['user_id'] ?? 0);
|
||||||
$target_ip = $_POST['ip_address'] ?? '';
|
$target_ip = $_POST['ip_address'] ?? '';
|
||||||
|
|
||||||
if (!$message) exit(json_encode(['success' => false]));
|
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 = db()->prepare("INSERT INTO messages (user_id, admin_id, sender, message, ip_address) VALUES (?, ?, ?, ?, ?)");
|
||||||
$stmt->execute([$user_id, $admin_id, $sender, $message, $target_ip]);
|
$stmt->execute([$user_id, $admin_id, $sender, $message, $target_ip]);
|
||||||
$newId = db()->lastInsertId();
|
$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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if ($action === 'ping') {
|
if ($action === 'ping') {
|
||||||
$user_id = $_SESSION['user_id'] ?? 0;
|
$user_id = $_SESSION['user_id'] ?? 0;
|
||||||
$ip = getRealIP();
|
$ip = getRealIP();
|
||||||
@ -118,7 +130,7 @@ if ($action === 'admin_get_all') {
|
|||||||
SELECT
|
SELECT
|
||||||
v.user_id,
|
v.user_id,
|
||||||
v.ip_address,
|
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,
|
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,
|
CASE WHEN u.email LIKE '%@user.byro' THEN u.username ELSE COALESCE(u.email, u.username) END as username,
|
||||||
u.uid,
|
u.uid,
|
||||||
@ -131,20 +143,20 @@ if ($action === 'admin_get_all') {
|
|||||||
MAX(last_activity) as last_activity,
|
MAX(last_activity) as last_activity,
|
||||||
MAX(user_time) as user_time
|
MAX(user_time) as user_time
|
||||||
FROM (
|
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
|
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
|
) t1
|
||||||
GROUP BY user_id, (CASE WHEN user_id = 0 THEN ip_address ELSE '0' END)
|
GROUP BY user_id, (CASE WHEN user_id = 0 THEN ip_address ELSE '0' END)
|
||||||
) v
|
) v
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT m1.* FROM messages m1
|
SELECT m1.* FROM messages m1
|
||||||
INNER JOIN (
|
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
|
) 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 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)
|
WHERE v.last_activity > DATE_SUB(NOW(), INTERVAL 48 HOUR)
|
||||||
ORDER BY created_at DESC
|
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>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<button class="btn btn-primary btn-sm rounded-pill px-3" onclick="window.open('<?= htmlspecialchars($cert) ?>', '_blank')">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
// 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_HOST', '127.0.0.1');
|
||||||
define('DB_NAME', 'app_38451');
|
define('DB_NAME', 'app_38451');
|
||||||
define('DB_USER', '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 */
|
/*M!999999\- enable the sandbox mode */
|
||||||
-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
|
-- 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 () => {
|
csFileInput.addEventListener('change', async () => {
|
||||||
if (!csFileInput.files[0]) return;
|
if (!csFileInput.files[0]) return;
|
||||||
const file = csFileInput.files[0];
|
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();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('action', 'upload_image');
|
formData.append('action', 'upload_image');
|
||||||
|
|
||||||
appendMessage('user', '<i class="bi bi-image"></i> <?= __("uploading") ?>');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/chat.php', {
|
const resp = await fetch('/api/chat.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
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();
|
pollMessages();
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || '<?= __("send_failed") ?>');
|
alert(data.error || '<?= __("send_failed") ?>');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Upload error:', err);
|
console.error('Upload error:', err);
|
||||||
|
const tempMsg = document.querySelector(`[data-id="${tempId}"]`);
|
||||||
|
if (tempMsg) tempMsg.remove();
|
||||||
}
|
}
|
||||||
csFileInput.value = '';
|
csFileInput.value = '';
|
||||||
|
// Clean up object URL
|
||||||
|
setTimeout(() => URL.revokeObjectURL(localUrl), 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
csToggle.addEventListener('click', () => {
|
csToggle.addEventListener('click', () => {
|
||||||
csBox.classList.toggle('d-none');
|
csBox.classList.toggle('d-none');
|
||||||
scrollToBottom();
|
|
||||||
if (!csBox.classList.contains('d-none')) {
|
if (!csBox.classList.contains('d-none')) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const timeStr = now.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
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));
|
fetch('/api/chat.php?action=ping&user_time=' + encodeURIComponent(timeStr));
|
||||||
|
scrollToBottom();
|
||||||
|
pollMessages();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
csClose.addEventListener('click', () => csBox.classList.add('d-none'));
|
csClose.addEventListener('click', () => csBox.classList.add('d-none'));
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
csMessages.scrollTop = csMessages.scrollHeight;
|
setTimeout(() => {
|
||||||
|
csMessages.scrollTop = csMessages.scrollHeight;
|
||||||
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
csForm.addEventListener('submit', async (e) => {
|
csForm.addEventListener('submit', async (e) => {
|
||||||
@ -271,6 +297,16 @@ csForm.addEventListener('submit', async (e) => {
|
|||||||
|
|
||||||
csInput.value = '';
|
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 {
|
try {
|
||||||
const resp = await fetch('/api/chat.php?action=send_message', {
|
const resp = await fetch('/api/chat.php?action=send_message', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -278,7 +314,14 @@ csForm.addEventListener('submit', async (e) => {
|
|||||||
body: `message=${encodeURIComponent(msg)}`
|
body: `message=${encodeURIComponent(msg)}`
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
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) {
|
if (data.success) {
|
||||||
|
appendMessageHTML(data.message);
|
||||||
|
scrollToBottom();
|
||||||
pollMessages();
|
pollMessages();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -286,38 +329,43 @@ csForm.addEventListener('submit', async (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function appendMessage(sender, text, time = null) {
|
function appendMessageHTML(m) {
|
||||||
const div = document.createElement('div');
|
if (document.querySelector(`[data-id="${m.id}"]`)) return;
|
||||||
div.className = `mb-3 d-flex flex-column ${sender === 'user' ? 'align-items-end' : 'align-items-start'}`;
|
|
||||||
|
const sender = m.sender;
|
||||||
|
const text = m.message;
|
||||||
|
const time = m.created_at;
|
||||||
|
const isImage = text.indexOf('<img') !== -1;
|
||||||
|
|
||||||
let dateObj;
|
let dateObj;
|
||||||
if (time) {
|
if (typeof time === 'string' && time.includes('-')) {
|
||||||
dateObj = new Date(time.replace(/-/g, "/"));
|
dateObj = new Date(time.replace(/-/g, "/"));
|
||||||
} else {
|
} else {
|
||||||
dateObj = new Date();
|
dateObj = new Date(time);
|
||||||
}
|
}
|
||||||
const timeStr = dateObj.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
const timeStr = dateObj.toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
||||||
|
|
||||||
div.innerHTML = `
|
const msgHtml = `
|
||||||
<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;">
|
<div class="mb-3 d-flex flex-column ${sender === 'user' ? 'align-items-end' : 'align-items-start'} message-item" data-id="${m.id}">
|
||||||
${text}
|
<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;' : ''}">
|
||||||
<div style="font-size: 9px; opacity: 0.6; position: absolute; bottom: 4px; ${sender === 'user' ? 'right: 10px;' : 'left: 10px;'}">${timeStr}</div>
|
${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>
|
</div>
|
||||||
`;
|
`;
|
||||||
csMessages.appendChild(div);
|
|
||||||
scrollToBottom();
|
csMessages.insertAdjacentHTML('beforeend', msgHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Polling for new messages
|
// Polling for new messages
|
||||||
let lastMsgId = 0;
|
let lastChatIds = new Set();
|
||||||
let lastPingTime = 0;
|
|
||||||
let lastChatHtml = '';
|
|
||||||
|
|
||||||
async function pollMessages() {
|
async function pollMessages() {
|
||||||
if (csBox.classList.contains('d-none')) return;
|
if (csBox.classList.contains('d-none')) return;
|
||||||
|
|
||||||
// Ping every 10 seconds to update user time
|
// Ping every 10 seconds to update user time
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
if (typeof lastPingTime === 'undefined') window.lastPingTime = 0;
|
||||||
if (now - lastPingTime > 10000) {
|
if (now - lastPingTime > 10000) {
|
||||||
const timeStr = new Date().toLocaleTimeString('zh-CN', {hour: '2-digit', minute:'2-digit', second: '2-digit'});
|
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));
|
fetch('/api/chat.php?action=ping&user_time=' + encodeURIComponent(timeStr));
|
||||||
@ -327,37 +375,23 @@ async function pollMessages() {
|
|||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/chat.php?action=get_messages');
|
const resp = await fetch('/api/chat.php?action=get_messages');
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data) {
|
if (data && Array.isArray(data)) {
|
||||||
let html = '<div class="text-center text-muted small mb-3"><?= __("welcome_support") ?></div>';
|
let hasNew = false;
|
||||||
data.forEach(m => {
|
data.forEach(m => {
|
||||||
const sender = m.sender;
|
if (!lastChatIds.has(m.id)) {
|
||||||
const text = m.message;
|
appendMessageHTML(m);
|
||||||
const time = m.created_at;
|
lastChatIds.add(m.id);
|
||||||
const isImage = text.indexOf('<img') !== -1;
|
hasNew = true;
|
||||||
|
}
|
||||||
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 (csMessages.innerHTML !== html) {
|
if (hasNew) {
|
||||||
const isAtBottom = csMessages.scrollTop + csMessages.clientHeight >= csMessages.scrollHeight - 50;
|
scrollToBottom();
|
||||||
csMessages.innerHTML = html;
|
|
||||||
if (isAtBottom) scrollToBottom();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
setInterval(pollMessages, 300);
|
setInterval(pollMessages, 300); // 300ms polling for "zero delay" feel
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
$lang = $_SESSION['lang'] ?? 'en';
|
$lang = $_SESSION['lang'] ?? 'en'; // Default to English
|
||||||
if (isset($_GET['lang'])) {
|
if (isset($_GET['lang'])) {
|
||||||
$lang = $_GET['lang'] === 'en' ? 'en' : 'zh';
|
$lang = $_GET['lang'] === 'zh' ? 'zh' : 'en';
|
||||||
$_SESSION['lang'] = $lang;
|
$_SESSION['lang'] = $lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,6 +371,7 @@ $translations = [
|
|||||||
'platform_certificate' => '平台注册证书',
|
'platform_certificate' => '平台注册证书',
|
||||||
'no_certificate_yet' => '暂无证书',
|
'no_certificate_yet' => '暂无证书',
|
||||||
'trade' => '交易',
|
'trade' => '交易',
|
||||||
|
'view_full_image' => '查看高清原图',
|
||||||
'failed' => '失败',
|
'failed' => '失败',
|
||||||
'success' => '成功',
|
'success' => '成功',
|
||||||
'direction' => '方向',
|
'direction' => '方向',
|
||||||
@ -891,6 +892,7 @@ $translations = [
|
|||||||
'platform_certificate' => 'Platform Registration Certificate',
|
'platform_certificate' => 'Platform Registration Certificate',
|
||||||
'no_certificate_yet' => 'No certificate available yet',
|
'no_certificate_yet' => 'No certificate available yet',
|
||||||
'trade' => 'Trade',
|
'trade' => 'Trade',
|
||||||
|
'view_full_image' => 'View Full Image',
|
||||||
'failed' => 'Failed',
|
'failed' => 'Failed',
|
||||||
'success' => 'Success',
|
'success' => 'Success',
|
||||||
'direction' => 'Direction',
|
'direction' => 'Direction',
|
||||||
|
|||||||