This commit is contained in:
Flatlogic Bot 2026-02-19 03:14:52 +00:00
parent 32be9227d1
commit 301a100f16
23 changed files with 611 additions and 250 deletions

View File

@ -135,24 +135,37 @@ ob_start();
<input type="password" name="password" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">权限设置</label>
<div class="d-flex flex-wrap gap-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">权限设置</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="manage_users" id="p_users" checked>
<label class="form-check-label" for="p_users">管理用户</label>
<input class="form-check-input" type="checkbox" id="selectAllAdd">
<label class="form-check-label small" for="selectAllAdd">全选</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="view_orders" id="p_orders" checked>
<label class="form-check-label" for="p_orders">查看订单</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="audit_finance" id="p_finance" checked>
<label class="form-check-label" for="p_finance">财务审核</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permissions[]" value="manage_kyc" id="p_kyc">
<label class="form-check-label" for="p_kyc">实名审核</label>
</div>
<div class="d-flex flex-wrap gap-2 p-3 bg-light rounded border" id="add_permissions_container">
<?php
$available_perms = [
'manage_users' => '用户管理',
'audit_finance' => '财务管理',
'manage_kyc' => '实名认证',
'view_orders' => '订单查看',
'manage_spot' => '币币交易',
'manage_contract' => '合约交易',
'manage_mining' => '矿机管理',
'manage_exchange' => '兑换管理',
'manage_binary' => '秒合约',
'manage_ai' => 'AI控制',
'manage_chat' => '客服系统',
'manage_settings' => '系统设置',
'view_stats' => '数据统计'
];
foreach ($available_perms as $val => $label):
?>
<div class="form-check" style="min-width: 100px;">
<input class="form-check-input add-p-checkbox" type="checkbox" name="permissions[]" value="<?= $val ?>" id="p_<?= $val ?>">
<label class="form-check-label" for="p_<?= $val ?>"><?= $label ?></label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
@ -184,24 +197,20 @@ ob_start();
<input type="text" name="password" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">权限设置</label>
<div class="d-flex flex-wrap gap-2" id="edit_permissions_container">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">权限设置</label>
<div class="form-check">
<input class="form-check-input p-checkbox" type="checkbox" name="permissions[]" value="manage_users" id="ep_users">
<label class="form-check-label" for="ep_users">管理用户</label>
<input class="form-check-input" type="checkbox" id="selectAllEdit">
<label class="form-check-label small" for="selectAllEdit">全选</label>
</div>
<div class="form-check">
<input class="form-check-input p-checkbox" type="checkbox" name="permissions[]" value="view_orders" id="ep_orders">
<label class="form-check-label" for="ep_orders">查看订单</label>
</div>
<div class="form-check">
<input class="form-check-input p-checkbox" type="checkbox" name="permissions[]" value="audit_finance" id="ep_finance">
<label class="form-check-label" for="ep_finance">财务审核</label>
</div>
<div class="form-check">
<input class="form-check-input p-checkbox" type="checkbox" name="permissions[]" value="manage_kyc" id="ep_kyc">
<label class="form-check-label" for="ep_kyc">实名审核</label>
</div>
<div class="d-flex flex-wrap gap-2 p-3 bg-light rounded border" id="edit_permissions_container">
<?php foreach ($available_perms as $val => $label): ?>
<div class="form-check" style="min-width: 100px;">
<input class="form-check-input edit-p-checkbox" type="checkbox" name="permissions[]" value="<?= $val ?>" id="ep_<?= $val ?>">
<label class="form-check-label" for="ep_<?= $val ?>"><?= $label ?></label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
@ -220,20 +229,34 @@ function editAgent(agent) {
document.getElementById('edit_username').value = agent.username;
// Clear checkboxes
document.querySelectorAll('.p-checkbox').forEach(cb => cb.checked = false);
document.querySelectorAll('.edit-p-checkbox').forEach(cb => cb.checked = false);
document.getElementById('selectAllEdit').checked = false;
// Set checkboxes
if (agent.permissions) {
const perms = JSON.parse(agent.permissions);
perms.forEach(p => {
const cb = document.querySelector(`.p-checkbox[value="${p}"]`);
const cb = document.querySelector(`.edit-p-checkbox[value="${p}"]`);
if (cb) cb.checked = true;
});
// Check if all are checked
const allChecked = Array.from(document.querySelectorAll('.edit-p-checkbox')).every(cb => cb.checked);
document.getElementById('selectAllEdit').checked = allChecked;
}
var modal = new bootstrap.Modal(document.getElementById('editAgentModal'));
modal.show();
}
// Select All logic
document.getElementById('selectAllAdd').addEventListener('change', function() {
document.querySelectorAll('.add-p-checkbox').forEach(cb => cb.checked = this.checked);
});
document.getElementById('selectAllEdit').addEventListener('change', function() {
document.querySelectorAll('.edit-p-checkbox').forEach(cb => cb.checked = this.checked);
});
</script>
<?php

View File

@ -15,7 +15,6 @@ function getLocalSetting($key, $default = null) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$settings = [
'email_verification_enabled' => $_POST['email_verification_enabled'] ?? '0',
'site_logo' => $_POST['site_logo'] ?? '',
'usdt_trc20_address' => $_POST['usdt_trc20_address'] ?? '',
'usdt_erc20_address' => $_POST['usdt_erc20_address'] ?? '',
'usdt_bep20_address' => $_POST['usdt_bep20_address'] ?? '',
@ -42,12 +41,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt->execute([$logo_path, $logo_path]);
}
}
// Handle favicon upload if provided
if (isset($_FILES['favicon_file']) && $_FILES['favicon_file']['error'] === 0) {
$ext = pathinfo($_FILES['favicon_file']['name'], PATHINFO_EXTENSION);
$filename = 'favicon_' . time() . '.' . $ext;
$target = __DIR__ . '/../assets/images/' . $filename;
if (!is_dir(__DIR__ . '/../assets/images/')) {
mkdir(__DIR__ . '/../assets/images/', 0777, true);
}
if (move_uploaded_file($_FILES['favicon_file']['tmp_name'], $target)) {
$favicon_path = '/assets/images/' . $filename;
$stmt = db()->prepare("INSERT INTO system_settings (setting_key, setting_value) VALUES ('site_favicon', ?) ON DUPLICATE KEY UPDATE setting_value = ?");
$stmt->execute([$favicon_path, $favicon_path]);
}
}
$success = true;
}
$email_verify = getLocalSetting('email_verification_enabled', '0');
$site_logo = getLocalSetting('site_logo', '');
$site_favicon = getLocalSetting('site_favicon', '');
$trc20_addr = getLocalSetting('usdt_trc20_address', '');
$erc20_addr = getLocalSetting('usdt_erc20_address', '');
$bep20_addr = getLocalSetting('usdt_bep20_address', '');
@ -82,7 +97,7 @@ ob_start();
</div>
<div class="mb-4">
<label class="form-label fw-bold">前端 LOGO / 网站图标</label>
<label class="form-label fw-bold">网站 LOGO</label>
<div class="d-flex align-items-center gap-3 mb-2">
<?php if ($site_logo): ?>
<div class="bg-dark p-2 rounded">
@ -91,7 +106,20 @@ ob_start();
<?php endif; ?>
<input type="file" name="logo_file" class="form-control">
</div>
<div class="form-text">上传后将同步更新网站 LOGO Favicon 浏览器图标。建议使用透明 PNG。</div>
<div class="form-text">上传后将同步更新网站所有页面的 LOGO。建议使用透明 PNG。</div>
</div>
<div class="mb-4">
<label class="form-label fw-bold">网站图标 (Favicon)</label>
<div class="d-flex align-items-center gap-3 mb-2">
<?php if ($site_favicon): ?>
<div class="bg-white p-2 rounded border">
<img src="<?= $site_favicon ?>" height="32" width="32" class="d-block">
</div>
<?php endif; ?>
<input type="file" name="favicon_file" class="form-control">
</div>
<div class="form-text">上传后将同步更新浏览器标签页图标。建议使用 32x32 64x64 的正方形图片。</div>
</div>
<hr class="my-4">

View File

@ -1,6 +1,7 @@
<?php
session_start();
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/layout.php';
// Check if admin
if (!isset($_SESSION['admin_id'])) {
@ -8,78 +9,164 @@ if (!isset($_SESSION['admin_id'])) {
exit;
}
// In a real app, check for admin role here
ob_start();
?>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Byro Admin | 客服系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body { background-color: #f0f2f5; font-family: 'Noto Sans SC', sans-serif; height: 100vh; margin: 0; }
.sidebar { width: 350px; background: #fff; border-right: 1px solid #ddd; display: flex; flex-direction: column; }
.main-chat { flex: 1; display: flex; flex-direction: column; background: #fff; }
.user-list { flex: 1; overflow-y: auto; }
.user-card { padding: 15px; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: all 0.2s; }
.user-card:hover { background: #f8f9fa; }
.user-card.active { background: #e7f3ff; border-left: 5px solid #007bff; }
.chat-header { padding: 15px 25px; border-bottom: 1px solid #ddd; background: #fff; z-index: 10; }
.messages-area { flex: 1; padding: 25px; background: #f9f9f9; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
.msg { max-width: 75%; padding: 10px 15px; border-radius: 18px; font-size: 14px; line-height: 1.5; }
.msg-admin { align-self: flex-end; background: #007bff; color: #fff; border-bottom-right-radius: 2px; }
.msg-user { align-self: flex-start; background: #fff; color: #333; border: 1px solid #eee; border-bottom-left-radius: 2px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
.chat-input-area { padding: 20px; border-top: 1px solid #ddd; background: #fff; }
.status-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 5px; }
.status-online { background: #28a745; }
</style>
</head>
<body class="d-flex">
<style>
.chat-container {
display: flex;
height: calc(100vh - 120px);
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0,0,0,0.05);
}
.user-sidebar {
width: 300px;
border-right: 1px solid #eee;
display: flex;
flex-direction: column;
}
.main-chat {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
}
.user-list {
flex: 1;
overflow-y: auto;
}
.user-card {
padding: 12px 15px;
border-bottom: 1px solid #f8f9fa;
cursor: pointer;
transition: all 0.2s;
}
.user-card:hover {
background: #f8f9fa;
}
.user-card.active {
background: #e7f3ff;
border-left: 4px solid #007bff;
}
.chat-header {
padding: 12px 20px;
border-bottom: 1px solid #eee;
background: #fff;
}
.messages-area {
flex: 1;
padding: 20px;
background: #fdfdfd;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.msg {
max-width: 80%;
padding: 8px 14px;
border-radius: 12px;
font-size: 14px;
line-height: 1.5;
position: relative;
}
.msg-admin {
align-self: flex-end;
background: #007bff;
color: #fff;
border-bottom-right-radius: 2px;
}
.msg-user {
align-self: flex-start;
background: #f0f0f0;
color: #333;
border-bottom-left-radius: 2px;
}
.chat-input-area {
padding: 15px;
border-top: 1px solid #eee;
}
.remark-area {
width: 250px;
border-left: 1px solid #eee;
padding: 15px;
background: #fcfcfc;
}
.status-online {
width: 8px;
height: 8px;
background: #28a745;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
}
#remark-text {
height: 150px;
font-size: 13px;
}
</style>
<div class="sidebar">
<div class="p-3 bg-primary text-white d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<a href="/admin/index.php" class="text-white"><i class="bi bi-arrow-left"></i></a>
<h5 class="m-0 fw-bold">客服中心</h5>
<div class="chat-container">
<!-- User List Sidebar -->
<div class="user-sidebar">
<div class="p-3 border-bottom bg-light">
<input type="text" id="user-search" class="form-control form-control-sm" placeholder="搜索用户/IP...">
</div>
<div class="user-list" id="user-list">
<!-- User cards filled by JS -->
</div>
<span class="badge bg-light text-primary">Live</span>
</div>
<div class="p-2">
<input type="text" class="form-control form-control-sm" placeholder="搜索用户 UID / IP">
</div>
<div class="user-list" id="user-list">
<!-- User cards -->
</div>
</div>
<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>
<h5 class="m-0 fw-bold" id="header-name">用户名称</h5>
<div class="small text-muted" id="header-meta">UID: --- | IP: ---</div>
</div>
<div class="text-end">
<span class="status-dot status-online"></span>
<span class="small text-success fw-bold">正在对话</span>
<!-- Main Chat Window -->
<div class="main-chat">
<div class="chat-header" id="chat-header" style="display: none;">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="m-0 fw-bold" id="header-name">用户名称</h6>
<div class="small" id="header-meta"><span class="text-muted">IP:</span> <span class="text-primary fw-bold" id="info-ip-header">---</span></div>
</div>
<div>
<span class="status-online"></span>
<span class="small text-success">在线</span>
</div>
</div>
</div>
</div>
<div class="messages-area" id="messages-area">
<div class="m-auto text-center text-muted">
<i class="bi bi-chat-left-dots fs-1 d-block mb-3"></i>
<p>从左侧选择一个会话开始实时对话</p>
<div class="messages-area" id="messages-area">
<div class="m-auto text-center text-muted">
<i class="bi bi-chat-left-dots fs-1 d-block mb-2"></i>
<p>请从左侧选择一个会话</p>
</div>
</div>
<div class="chat-input-area" id="input-area" style="display: none;">
<form id="chat-form" class="d-flex gap-2 align-items-center">
<input type="file" id="image-input" style="display: none;" accept="image/*">
<button type="button" class="btn btn-outline-secondary btn-sm rounded-circle" id="plus-btn" style="width: 32px; height: 32px; flex-shrink: 0;">
<i class="bi bi-plus-lg"></i>
</button>
<input type="text" id="msg-input" class="form-control" placeholder="输入回复内容..." autocomplete="off">
<button type="submit" class="btn btn-primary btn-sm px-3">发送</button>
</form>
</div>
</div>
<div class="chat-input-area" id="input-area" style="display: none;">
<form id="chat-form" class="d-flex gap-2">
<input type="text" id="msg-input" class="form-control" placeholder="输入消息回复用户..." autocomplete="off">
<button type="submit" class="btn btn-primary px-4 fw-bold">发送回复</button>
</form>
<!-- Remark Area -->
<div class="remark-area" id="remark-area" style="display: none;">
<h6 class="fw-bold mb-3 mt-1"><i class="bi bi-pencil-square me-1"></i> 用户备注</h6>
<div class="mb-3">
<label class="small text-muted mb-1">当前用户备注信息</label>
<textarea id="remark-text" class="form-control" placeholder="在此输入对该用户的备注..."></textarea>
</div>
<button id="save-remark-btn" class="btn btn-dark btn-sm w-100 fw-bold">保存备注</button>
<hr>
<div class="user-info-box small">
<div class="mb-2"><strong>UID:</strong> <span id="info-uid">---</span></div>
<div class="mb-2"><strong>当前IP:</strong> <span id="info-ip">---</span></div>
<div class="mb-2"><strong>最近活跃:</strong> <span id="info-time">---</span></div>
</div>
</div>
</div>
@ -92,20 +179,31 @@ async function refreshUsers() {
const r = await fetch('/api/chat.php?action=admin_get_all');
const users = await r.json();
const list = document.getElementById('user-list');
const search = document.getElementById('user-search').value.toLowerCase();
let html = '';
users.forEach(u => {
const isActive = (selectedIp === u.ip_address && selectedUser == u.user_id);
const username = u.username || '匿名用户';
const uid = u.uid || '---';
const ip = u.ip_address;
const remark = u.remark || '';
if (search && !username.toLowerCase().includes(search) && !ip.includes(search) && !uid.toString().includes(search)) {
return;
}
const isActive = (selectedIp === ip && selectedUser == u.user_id);
html += `
<div class="user-card ${isActive ? 'active' : ''}" onclick="openChat('${u.user_id}', '${u.ip_address}', '${u.username || '匿名用户'}', '${u.uid || '---'}')">
<div class="user-card ${isActive ? 'active' : ''}" onclick="openChat('${u.user_id}', '${ip}', '${username}', '${uid}', '${remark.replace(/'/g, "\\'")}')">
<div class="d-flex justify-content-between mb-1">
<span class="fw-bold">${u.username || '匿名用户'}</span>
<span class="small text-muted">${new Date(u.created_at).toLocaleTimeString()}</span>
<span class="fw-bold small text-truncate" style="max-width: 150px;">${username}</span>
<span class="text-muted" style="font-size: 10px;">${new Date(u.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
</div>
<div class="small text-truncate text-muted mb-1">${u.message}</div>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-light text-dark border" style="font-size: 10px;">UID: ${u.uid || '---'}</span>
<span class="text-primary" style="font-size: 10px;">${u.ip_address}</span>
${remark ? `<div class="small text-danger text-truncate mb-1" style="font-size: 11px;">[备注: ${remark}]</div>` : ''}
<div class="small text-truncate text-muted mb-1" style="font-size: 12px;">${u.message}</div>
<div class="d-flex justify-content-between align-items-center" style="font-size: 10px;">
<span class="text-secondary">UID: ${uid}</span>
<span class="text-primary fw-bold">${ip}</span>
</div>
</div>
`;
@ -113,15 +211,22 @@ async function refreshUsers() {
list.innerHTML = html;
}
function openChat(userId, ip, name, uid) {
function openChat(userId, ip, name, uid, remark) {
selectedUser = userId;
selectedIp = ip;
document.getElementById('header-name').innerText = name;
document.getElementById('header-meta').innerText = `UID: ${uid} | IP: ${ip}`;
document.getElementById('info-ip-header').innerText = ip;
document.getElementById('chat-header').style.display = 'block';
document.getElementById('input-area').style.display = 'block';
document.getElementById('remark-area').style.display = 'block';
document.getElementById('remark-text').value = remark;
document.getElementById('info-uid').innerText = uid;
document.getElementById('info-ip').innerText = ip;
lastMsgId = 0;
fetchMessages();
refreshUsers(); // Refresh list to update active state
}
async function fetchMessages() {
@ -130,7 +235,6 @@ async function fetchMessages() {
const msgs = await r.json();
const area = document.getElementById('messages-area');
// Simple filter for the specific conversation
const filtered = msgs.filter(m => m.ip_address === selectedIp && (m.user_id == selectedUser || m.user_id == 0));
if (filtered.length > lastMsgId) {
@ -143,9 +247,39 @@ async function fetchMessages() {
});
area.scrollTop = area.scrollHeight;
lastMsgId = filtered.length;
if (filtered.length > 0) {
document.getElementById('info-time').innerText = new Date(filtered[filtered.length-1].created_at).toLocaleString();
}
}
}
document.getElementById('plus-btn').addEventListener('click', () => {
document.getElementById('image-input').click();
});
document.getElementById('image-input').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('user_id', selectedUser);
formData.append('ip_address', selectedIp);
const r = await fetch('/api/chat.php?action=upload_image', {
method: 'POST',
body: formData
});
const res = await r.json();
if (res.success) {
fetchMessages();
} else {
alert('上传失败: ' + res.error);
}
e.target.value = ''; // Reset
});
document.getElementById('chat-form').addEventListener('submit', async (e) => {
e.preventDefault();
const input = document.getElementById('msg-input');
@ -162,16 +296,29 @@ document.getElementById('chat-form').addEventListener('submit', async (e) => {
fetchMessages();
});
// Clear notifications when on this page
function clearNotifications() {
fetch('/api/admin_notifications.php?action=clear&type=messages');
}
setInterval(clearNotifications, 5000);
clearNotifications();
document.getElementById('save-remark-btn').addEventListener('click', async () => {
const remark = document.getElementById('remark-text').value;
const fd = new URLSearchParams();
fd.append('user_id', selectedUser);
fd.append('ip_address', selectedIp);
fd.append('remark', remark);
setInterval(refreshUsers, 3000);
setInterval(fetchMessages, 1000);
const r = await fetch('/api/chat.php?action=save_remark', { method: 'POST', body: fd });
const res = await r.json();
if (res.success) {
alert('备注已保存');
refreshUsers();
}
});
document.getElementById('user-search').addEventListener('input', refreshUsers);
setInterval(refreshUsers, 5000);
setInterval(fetchMessages, 2000);
refreshUsers();
</script>
</body>
</html>
<?php
$content = ob_get_clean();
renderAdminPage($content, '在线客服');
?>

View File

@ -33,12 +33,19 @@ function renderAdminPage($content, $title = '后台管理') {
global $admin, $lang;
$current_page = basename($_SERVER['PHP_SELF']);
$site_logo = '';
$site_logo = '/assets/images/logo.png';
$site_favicon = '';
$site_name = 'Byro';
try {
$stmt = db()->prepare("SELECT setting_value FROM system_settings WHERE setting_key = 'site_logo'");
$stmt->execute();
$site_logo = $stmt->fetchColumn() ?: '';
$val = $stmt->fetchColumn();
if ($val) $site_logo = $val;
$stmt = db()->prepare("SELECT setting_value FROM system_settings WHERE setting_key = 'site_favicon'");
$stmt->execute();
$val = $stmt->fetchColumn();
$site_favicon = $val ?: $site_logo;
$stmt = db()->prepare("SELECT setting_value FROM system_settings WHERE setting_key = 'site_name'");
$stmt->execute();
@ -53,8 +60,8 @@ function renderAdminPage($content, $title = '后台管理') {
<title><?= $title ?> - <?= $site_name ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<?php if ($site_logo): ?>
<link rel="icon" href="<?= $site_logo ?>">
<?php if ($site_favicon): ?>
<link rel="icon" href="<?= $site_favicon ?>">
<?php endif; ?>
<style>
:root {

View File

@ -10,6 +10,26 @@ if (isset($_SESSION['admin_id'])) {
exit;
}
$site_logo = '/assets/images/logo.png';
$site_favicon = '';
$site_name = 'Byro Admin';
try {
$stmt = db()->prepare("SELECT setting_value FROM system_settings WHERE setting_key = 'site_logo'");
$stmt->execute();
$val = $stmt->fetchColumn();
if ($val) $site_logo = $val;
$stmt = db()->prepare("SELECT setting_value FROM system_settings WHERE setting_key = 'site_favicon'");
$stmt->execute();
$val = $stmt->fetchColumn();
$site_favicon = $val ?: $site_logo;
$stmt = db()->prepare("SELECT setting_value FROM system_settings WHERE setting_key = 'site_name'");
$stmt->execute();
$name = $stmt->fetchColumn();
if ($name) $site_name = $name . ' Admin';
} catch (Exception $e) {}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@ -40,9 +60,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理员登录 - Byro Admin</title>
<title>管理员登录 - <?= $site_name ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<?php if ($site_favicon): ?>
<link rel="icon" href="<?= $site_favicon ?>">
<?php endif; ?>
<style>
body {
background: #f4f7f6;
@ -78,7 +101,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<body>
<div class="login-card">
<div class="logo">
<i class="bi bi-shield-lock-fill me-2"></i> 后台管理系统
<?php if ($site_logo): ?>
<img src="<?= $site_logo ?>" height="40" class="mb-2 d-block mx-auto">
<?php else: ?>
<i class="bi bi-shield-lock-fill me-2"></i>
<?php endif; ?>
<div>后台管理系统</div>
</div>
<?php if ($error): ?>

View File

@ -194,7 +194,8 @@ ob_start();
<?php
$sql = "SELECT u.*,
(SELECT available FROM user_balances WHERE user_id = u.id AND symbol = 'USDT') as usdt_balance,
(SELECT username FROM admins WHERE id = u.agent_id) as agent_name
(SELECT username FROM admins WHERE id = u.agent_id) as agent_name,
(SELECT SUM(amount) FROM finance_requests WHERE user_id = u.id AND type='recharge' AND status='approved' AND symbol='USDT') as calculated_recharge
FROM users u";
$params = [];
if ($admin['is_agent']) {
@ -210,7 +211,8 @@ ob_start();
<tr>
<td><code><?= $u['uid'] ?></code></td>
<td>
<div class="fw-bold"><?= htmlspecialchars($u['username']) ?> <span class="badge bg-warning text-dark" style="font-size: 10px;">VIP <?= $u['vip_level'] ?></span></div>
<?php $dynamicVip = getAutoVipLevel($u['calculated_recharge'] ?? 0); ?>
<div class="fw-bold"><?= htmlspecialchars($u['username']) ?> <span class="badge bg-warning text-dark" style="font-size: 10px;">VIP <?= $dynamicVip ?></span></div>
<div class="text-muted" style="font-size: 11px;"><?= $u['created_at'] ?></div>
</td>
<?php if (!$admin['is_agent']): ?>
@ -382,18 +384,16 @@ ob_start();
</select>
</div>
<div class="col-md-6">
<label class="form-label">VIP等级</label>
<select name="vip_level" id="edit_vip_level" class="form-control">
<label class="form-label">VIP等级 (系统根据充值自动计算)</label>
<select name="vip_level" id="edit_vip_level" class="form-control" disabled>
<option value="0">VIP 0</option>
<option value="1">VIP 1</option>
<option value="2">VIP 2</option>
<option value="3">VIP 3</option>
<option value="4">VIP 4</option>
<option value="5">VIP 5</option>
<option value="6">VIP 6</option>
<option value="7">VIP 7</option>
<option value="8">VIP 8</option>
</select>
<div class="form-text">当前累计充值: <span id="edit_calculated_recharge" class="fw-bold text-primary">0.00</span> USDT</div>
</div>
<?php if (!$admin['is_agent']): ?>
<div class="col-md-6">
@ -442,7 +442,19 @@ function editUser(user) {
document.getElementById('edit_credit_score').value = user.credit_score;
document.getElementById('edit_status').value = user.status;
document.getElementById('edit_win_loss').value = user.win_loss_control;
document.getElementById('edit_vip_level').value = user.vip_level || 0;
// Use calculated VIP level for display
const recharge = parseFloat(user.calculated_recharge || 0);
document.getElementById('edit_calculated_recharge').innerText = recharge.toFixed(2);
let vip = 0;
if (recharge >= 250000) vip = 5;
else if (recharge >= 180000) vip = 4;
else if (recharge >= 100000) vip = 3;
else if (recharge >= 50000) vip = 2;
else if (recharge >= 10000) vip = 1;
document.getElementById('edit_vip_level').value = vip;
document.getElementById('edit_remark').value = user.remark || '';
if (document.getElementById('edit_agent_id_select')) {
document.getElementById('edit_agent_id_select').value = user.agent_id || '';

View File

@ -28,13 +28,22 @@ 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;
$user_id = $_SESSION['user_id'] ?? 0;
$ip = $_SERVER['REMOTE_ADDR'];
$message = '<img src="' . $imageUrl . '" class="img-fluid rounded cursor-pointer" onclick="window.open(\'' . $imageUrl . '\')">';
$message = '<img src="' . $imageUrl . '" class="img-fluid rounded cursor-pointer" style="max-width: 200px; max-height: 200px;" onclick="window.open(\'' . $imageUrl . '\')">';
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
$stmt->execute([$user_id, 'user', $message, $ip]);
if (isset($_SESSION['admin_id'])) {
$user_id = $_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;
$ip = getRealIP();
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
$stmt->execute([$user_id, 'user', $message, $ip]);
}
echo json_encode(['success' => true, 'url' => $imageUrl]);
} else {
@ -46,7 +55,7 @@ if ($action === 'upload_image' || (isset($_POST['action']) && $_POST['action'] =
if ($action === 'get_messages') {
$user_id = $_SESSION['user_id'] ?? 0;
$target_user_id = $_GET['user_id'] ?? $user_id;
$target_ip = $_GET['ip'] ?? $_SERVER['REMOTE_ADDR'];
$target_ip = $_GET['ip'] ?? getRealIP();
// If admin is requesting, we use the provided user_id and ip
if (isset($_SESSION['admin_id'])) {
@ -55,7 +64,7 @@ if ($action === 'get_messages') {
} 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, $_SERVER['REMOTE_ADDR']]);
$stmt->execute([$user_id, getRealIP()]);
}
$messages = $stmt->fetchAll();
@ -68,7 +77,7 @@ if ($action === 'send_message') {
if (!$message) exit(json_encode(['success' => false]));
$user_id = $_SESSION['user_id'] ?? 0;
$ip = $_SERVER['REMOTE_ADDR'];
$ip = getRealIP();
$stmt = db()->prepare("INSERT INTO messages (user_id, sender, message, ip_address) VALUES (?, ?, ?, ?)");
$stmt->execute([$user_id, 'user', $message, $ip]);
@ -94,9 +103,68 @@ if ($action === 'admin_send') {
exit;
}
if ($action === 'ping') {
$user_id = $_SESSION['user_id'] ?? 0;
$ip = getRealIP();
$stmt = db()->prepare("INSERT INTO chat_visitors (user_id, ip_address) VALUES (?, ?) ON DUPLICATE KEY UPDATE last_ping = CURRENT_TIMESTAMP");
$stmt->execute([$user_id, $ip]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'admin_get_all') {
// Get distinct users/IPs who have messaged
$stmt = db()->query("SELECT m.*, u.username, u.uid FROM messages m LEFT JOIN users u ON m.user_id = u.id WHERE m.id IN (SELECT MAX(id) FROM messages GROUP BY user_id, ip_address) ORDER BY created_at DESC");
// Get distinct users/IPs who have messaged or pinged
// Combine messages and visitors to show even those who haven't messaged yet
$stmt = db()->query("
SELECT
combined.user_id,
combined.ip_address,
COALESCE(m.message, '用户已进入聊天室') as message,
COALESCE(m.created_at, combined.last_activity) as created_at,
u.username,
u.uid,
r.remark
FROM (
SELECT user_id, ip_address, MAX(created_at) as last_activity FROM messages GROUP BY user_id, ip_address
UNION
SELECT user_id, ip_address, last_ping as last_activity FROM chat_visitors
) combined
LEFT JOIN (
SELECT user_id, ip_address, message, created_at FROM messages WHERE id IN (SELECT MAX(id) FROM messages GROUP BY user_id, ip_address)
) m ON (combined.user_id = m.user_id AND combined.ip_address = m.ip_address)
LEFT JOIN users u ON combined.user_id = u.id
LEFT JOIN chat_remarks r ON (combined.user_id = r.user_id AND combined.ip_address = r.ip_address)
WHERE combined.last_activity > DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY created_at DESC
");
echo json_encode($stmt->fetchAll());
exit;
}
if ($action === 'save_remark') {
if (!isset($_SESSION['admin_id'])) exit(json_encode(['success' => false, 'error' => 'Unauthorized']));
$user_id = $_POST['user_id'] ?? 0;
$ip = $_POST['ip_address'] ?? '';
$remark = $_POST['remark'] ?? '';
$stmt = db()->prepare("INSERT INTO chat_remarks (user_id, ip_address, remark) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE remark = ?");
$stmt->execute([$user_id, $ip, $remark, $remark]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'get_remark') {
if (!isset($_SESSION['admin_id'])) exit(json_encode(['success' => false, 'error' => 'Unauthorized']));
$user_id = $_GET['user_id'] ?? 0;
$ip = $_GET['ip'] ?? '';
$stmt = db()->prepare("SELECT remark FROM chat_remarks WHERE user_id = ? AND ip_address = ?");
$stmt->execute([$user_id, $ip]);
$remark = $stmt->fetchColumn() ?: '';
echo json_encode(['success' => true, 'remark' => $remark]);
exit;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -8,7 +8,7 @@ if (!function_exists('getSetting')) {
$stmt = db()->prepare("SELECT setting_value FROM system_settings WHERE setting_key = ?");
$stmt->execute([$key]);
$row = $stmt->fetch();
return $row ? $row['setting_value'] : $default;
return ($row && !empty($row['setting_value'])) ? $row['setting_value'] : $default;
}
}

View File

@ -15,3 +15,28 @@ function db() {
}
return $pdo;
}
function getRealIP() {
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
}
return $_SERVER['REMOTE_ADDR'];
}
function getUserTotalRecharge($userId) {
$stmt = db()->prepare("SELECT SUM(amount) FROM finance_requests WHERE user_id = ? AND type='recharge' AND status='approved' AND symbol='USDT'");
$stmt->execute([$userId]);
return (float)$stmt->fetchColumn() ?: 0;
}
function getAutoVipLevel($totalRecharge) {
if ($totalRecharge >= 250000) return 5;
if ($totalRecharge >= 180000) return 4;
if ($totalRecharge >= 100000) return 3;
if ($totalRecharge >= 50000) return 2;
if ($totalRecharge >= 10000) return 1;
return 0;
}

View File

@ -1 +1 @@
{"USD":1,"AED":3.67,"AFN":63.59,"ALL":81.5,"AMD":377.17,"ANG":1.79,"AOA":919.5,"ARS":1452.25,"AUD":1.41,"AWG":1.79,"AZN":1.7,"BAM":1.65,"BBD":2,"BDT":122.33,"BGN":1.6,"BHD":0.376,"BIF":2969.83,"BMD":1,"BND":1.26,"BOB":6.94,"BRL":5.22,"BSD":1,"BTN":90.69,"BWP":13.48,"BYN":2.85,"BZD":2,"CAD":1.36,"CDF":2291.41,"CHF":0.77,"CLF":0.0218,"CLP":863.28,"CNH":6.9,"CNY":6.91,"COP":3672.12,"CRC":482.41,"CUP":24,"CVE":93.14,"CZK":20.5,"DJF":177.72,"DKK":6.31,"DOP":62.2,"DZD":129.91,"EGP":47.01,"ERN":15,"ETB":155.2,"EUR":0.845,"FJD":2.19,"FKP":0.738,"FOK":6.31,"GBP":0.738,"GEL":2.68,"GGP":0.738,"GHS":11.03,"GIP":0.738,"GMD":74.13,"GNF":8753.13,"GTQ":7.68,"GYD":209.29,"HKD":7.82,"HNL":26.5,"HRK":6.36,"HTG":130.99,"HUF":319.14,"IDR":16842.78,"ILS":3.1,"IMP":0.738,"INR":90.69,"IQD":1309.66,"IRR":1280515.41,"ISK":122.56,"JEP":0.738,"JMD":156.27,"JOD":0.709,"JPY":153.28,"KES":129.03,"KGS":87.44,"KHR":4015.28,"KID":1.41,"KMF":415.55,"KRW":1443.65,"KWD":0.306,"KYD":0.833,"KZT":490.62,"LAK":21622.5,"LBP":89500,"LKR":309.18,"LRD":186.33,"LSL":16.04,"LYD":6.3,"MAD":9.14,"MDL":16.98,"MGA":4361.67,"MKD":52.04,"MMK":2103.54,"MNT":3531.98,"MOP":8.05,"MRU":39.84,"MUR":45.91,"MVR":15.46,"MWK":1743.52,"MXN":17.17,"MYR":3.9,"MZN":63.79,"NAD":16.04,"NGN":1354.91,"NIO":36.86,"NOK":9.53,"NPR":145.11,"NZD":1.66,"OMR":0.384,"PAB":1,"PEN":3.35,"PGK":4.3,"PHP":57.89,"PKR":279.89,"PLN":3.56,"PYG":6529.25,"QAR":3.64,"RON":4.31,"RSD":99.11,"RUB":76.99,"RWF":1461.96,"SAR":3.75,"SBD":7.93,"SCR":14.48,"SDG":449.19,"SEK":8.98,"SGD":1.26,"SHP":0.738,"SLE":24.62,"SLL":24619.11,"SOS":570.39,"SRD":37.7,"SSP":4590.66,"STN":20.69,"SYP":112.45,"SZL":16.04,"THB":31.24,"TJS":9.39,"TMT":3.5,"TND":2.86,"TOP":2.36,"TRY":43.72,"TTD":6.8,"TVD":1.41,"TWD":31.38,"TZS":2581.08,"UAH":43.3,"UGX":3516.59,"UYU":38.86,"UZS":12158.3,"VES":396.37,"VND":25898.69,"VUV":118.27,"WST":2.67,"XAF":554.06,"XCD":2.7,"XCG":1.79,"XDR":0.726,"XOF":554.06,"XPF":100.8,"YER":238.68,"ZAR":16.04,"ZMW":18.49,"ZWG":25.59,"ZWL":25.59}
{"USD":1,"AED":3.67,"AFN":62.82,"ALL":81.38,"AMD":377.18,"ANG":1.79,"AOA":921.13,"ARS":1452.25,"AUD":1.42,"AWG":1.79,"AZN":1.7,"BAM":1.66,"BBD":2,"BDT":122.19,"BGN":1.6,"BHD":0.376,"BIF":2967.31,"BMD":1,"BND":1.27,"BOB":6.92,"BRL":5.22,"BSD":1,"BTN":90.77,"BWP":13.29,"BYN":2.84,"BZD":2,"CAD":1.37,"CDF":2281.91,"CHF":0.772,"CLF":0.0219,"CLP":866.65,"CNH":6.9,"CNY":6.91,"COP":3648.29,"CRC":481.3,"CUP":24,"CVE":93.37,"CZK":20.52,"DJF":177.72,"DKK":6.31,"DOP":61.55,"DZD":129.72,"EGP":46.95,"ERN":15,"ETB":154.44,"EUR":0.847,"FJD":2.2,"FKP":0.739,"FOK":6.31,"GBP":0.739,"GEL":2.68,"GGP":0.739,"GHS":11.02,"GIP":0.739,"GMD":74.1,"GNF":8762.47,"GTQ":7.67,"GYD":209.26,"HKD":7.82,"HNL":26.44,"HRK":6.38,"HTG":131.36,"HUF":320.31,"IDR":16895.02,"ILS":3.11,"IMP":0.739,"INR":90.77,"IQD":1310.4,"IRR":1281616.32,"ISK":122.37,"JEP":0.739,"JMD":155.71,"JOD":0.709,"JPY":154.42,"KES":129,"KGS":87.48,"KHR":4017.09,"KID":1.42,"KMF":416.61,"KRW":1447.31,"KWD":0.306,"KYD":0.833,"KZT":489.38,"LAK":21513.29,"LBP":89500,"LKR":309.13,"LRD":185.55,"LSL":16.06,"LYD":6.3,"MAD":9.14,"MDL":17.01,"MGA":4331.69,"MKD":52.14,"MMK":2100.51,"MNT":3550.21,"MOP":8.05,"MRU":39.97,"MUR":45.96,"MVR":15.45,"MWK":1739.99,"MXN":17.18,"MYR":3.9,"MZN":63.72,"NAD":16.06,"NGN":1338.38,"NIO":36.78,"NOK":9.52,"NPR":145.24,"NZD":1.67,"OMR":0.384,"PAB":1,"PEN":3.35,"PGK":4.33,"PHP":57.94,"PKR":279.71,"PLN":3.57,"PYG":6522.64,"QAR":3.64,"RON":4.3,"RSD":99.15,"RUB":76.67,"RWF":1460.55,"SAR":3.75,"SBD":8.02,"SCR":14.01,"SDG":543.58,"SEK":9.02,"SGD":1.27,"SHP":0.739,"SLE":24.46,"SLL":24455.37,"SOS":570.65,"SRD":37.71,"SSP":4568.21,"STN":20.75,"SYP":113.49,"SZL":16.06,"THB":31.26,"TJS":9.4,"TMT":3.5,"TND":2.86,"TOP":2.36,"TRY":43.77,"TTD":6.75,"TVD":1.42,"TWD":31.47,"TZS":2574.76,"UAH":43.3,"UGX":3532.39,"UYU":38.82,"UZS":12156.76,"VES":398.75,"VND":25919.9,"VUV":118.59,"WST":2.68,"XAF":555.48,"XCD":2.7,"XCG":1.79,"XDR":0.726,"XOF":555.48,"XPF":101.05,"YER":238.28,"ZAR":16.06,"ZMW":18.61,"ZWG":25.6,"ZWL":25.6}

View File

@ -244,6 +244,9 @@ csFileInput.addEventListener('change', async () => {
csToggle.addEventListener('click', () => {
csBox.classList.toggle('d-none');
scrollToBottom();
if (!csBox.classList.contains('d-none')) {
fetch('/api/chat.php?action=ping');
}
});
csClose.addEventListener('click', () => csBox.classList.add('d-none'));

View File

@ -14,7 +14,7 @@ if (!function_exists('getSetting')) {
$stmt = db()->prepare("SELECT setting_value FROM system_settings WHERE setting_key = ?");
$stmt->execute([$key]);
$row = $stmt->fetch();
return $row ? $row['setting_value'] : $default;
return ($row && !empty($row['setting_value'])) ? $row['setting_value'] : $default;
}
}
?>
@ -25,16 +25,18 @@ if (!function_exists('getSetting')) {
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<?php
$site_logo = getSetting('site_logo', '/assets/images/logo.png');
$site_favicon = getSetting('site_favicon', $site_logo);
$site_name = getSetting('site_name', 'BYRO');
?>
<title><?= $site_name ?> | <?= __('site_title') ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="/assets/css/style.css?v=<?= time() ?>">
<link rel="icon" href="<?= $site_logo ?>">
<link rel="icon" href="<?= $site_favicon ?>">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
:root {
--primary: #0062ff;

View File

@ -66,10 +66,10 @@ $translations = [
'approx' => '约等于',
'cookie_policy_title' => '隐私政策',
'cookie_policy_content' => '<h3>1. 什么是隐私数据?</h3><p>隐私数据是您访问网站时存储在您的计算机或移动设备上的信息。它们广泛用于使网站运行或更高效地运行。</p>',
'recharge_msg_fiat' => "用户ID%uid% 申请充值金额:%amount% %currency%=%res%泰达币",
'recharge_msg_crypto' => "用户ID%uid% 申请充值金额:%amount% 泰达币 (%network%)",
'withdraw_msg_fiat' => "用户ID%uid% 申请提现金额:%amount% 泰达币=%res% %currency%",
'withdraw_msg_crypto' => "用户ID%uid% 申请提现金额:%amount% 泰达币 (%network%)",
'recharge_msg_fiat' => "用户ID%uid% 申请充值金额:%amount% %currency%=%res% USDT",
'recharge_msg_crypto' => "用户ID%uid% 申请充值金额:%amount% USDT (%network%)",
'withdraw_msg_fiat' => "用户ID%uid% 申请提现金额:%amount% USDT=%res% %currency%",
'withdraw_msg_crypto' => "用户ID%uid% 申请提现金额:%amount% USDT (%network%)",
'select_network' => '选择网络',
'address_copied' => '地址已复制',
'withdrawal_password_error' => '提现密码错误',
@ -78,7 +78,7 @@ $translations = [
'withdraw_amount_label' => '提现金额',
'i_have_paid' => '我已付款',
'enter_amount' => '请输入金额',
'crypto_recharge_warning' => '请仅向此地址发送泰达币。发送其他资产可能会导致永久丢失。',
'crypto_recharge_warning' => '请仅向此地址发送 USDT。发送其他资产可能会导致永久丢失。',
'recharge_steps' => '充值步骤',
'security_tips' => '安全提示',
'est_receive_fiat' => '预计收到法币',
@ -91,7 +91,7 @@ $translations = [
'recharge_amount' => '充值金额',
'withdraw_amount' => '提现金额',
'withdraw_address' => '提现地址',
'min_withdraw_hint' => '最小提现金额 10 泰达币',
'min_withdraw_hint' => '最小提现金额 10 USDT',
'secure' => '安全',
'fast' => '极速',
'support_247' => '全天候支持',
@ -107,7 +107,7 @@ $translations = [
'select_currency' => '选择币种',
'fiat_recharge' => '法币充值',
'crypto_recharge' => '加密货币充值',
'est_usdt' => '预计泰达币',
'est_usdt' => '预计 USDT',
'enter_address' => '请输入地址',
'enter_password' => '请输入密码',
'request_sent' => '请求已发送,请检查聊天。',
@ -185,23 +185,23 @@ $translations = [
'chinese' => '中文',
'english' => '英文',
'qr_code' => '二维码',
'bitcoin' => '比特币',
'ethereum' => '以太坊',
'tether' => '泰达币',
'binance_coin' => '币安币',
'solana' => '索拉纳',
'ripple' => '瑞波币',
'cardano' => '卡尔达诺',
'dogecoin' => '狗狗币',
'polkadot' => '波卡',
'polygon' => '多边形',
'avalanche' => '雪崩',
'chainlink' => '链连',
'shiba_inu' => '柴犬币',
'tron' => '波场',
'bitcoin_cash' => '比特币现金',
'litecoin' => '莱特币',
'uniswap' => '独角兽',
'bitcoin' => 'BTC',
'ethereum' => 'ETH',
'tether' => 'USDT',
'binance_coin' => 'BNB',
'solana' => 'SOL',
'ripple' => 'XRP',
'cardano' => 'ADA',
'dogecoin' => 'DOGE',
'polkadot' => 'DOT',
'polygon' => 'MATIC',
'avalanche' => 'AVAX',
'chainlink' => 'LINK',
'shiba_inu' => 'SHIB',
'tron' => 'TRX',
'bitcoin_cash' => 'BCH',
'litecoin' => 'LTC',
'uniswap' => 'UNI',
'site_title' => '全球领先的数字资产交易平台',
'unverified' => '未认证',
'pending' => '审核中',
@ -447,22 +447,22 @@ $translations = [
'level' => '等级',
'recharge_to_upgrade' => '还需充值 %amount% USDT 升级到 VIP %level%',
'highest_level' => '已达到最高等级',
'USDT' => '泰达币',
'BTC' => '比特币',
'ETH' => '以太坊',
'BNB' => '币安币',
'SOL' => '索拉纳',
'XRP' => '瑞波币',
'ADA' => '卡尔达诺',
'DOGE' => '狗狗币',
'DOT' => '波卡',
'MATIC' => '多边形',
'LINK' => '链连',
'SHIB' => '柴犬币',
'TRX' => '波场',
'BCH' => '比特币现金',
'LTC' => '莱特币',
'UNI' => '独角兽',
'USDT' => 'USDT',
'BTC' => 'BTC',
'ETH' => 'ETH',
'BNB' => 'BNB',
'SOL' => 'SOL',
'XRP' => 'XRP',
'ADA' => 'ADA',
'DOGE' => 'DOGE',
'DOT' => 'DOT',
'MATIC' => 'MATIC',
'LINK' => 'LINK',
'SHIB' => 'SHIB',
'TRX' => 'TRX',
'BCH' => 'BCH',
'LTC' => 'LTC',
'UNI' => 'UNI',
'security_level' => '安全等级',
'login_password' => '登录密码',
'old_password' => '原密码',
@ -697,23 +697,23 @@ $translations = [
'email' => 'Email',
'email_placeholder' => 'Enter email address',
'registration_verify' => 'Registration Verify',
'bitcoin' => 'Bitcoin',
'ethereum' => 'Ethereum',
'tether' => 'Tether',
'bitcoin' => 'BTC',
'ethereum' => 'ETH',
'tether' => 'USDT',
'binance_coin' => 'BNB',
'solana' => 'Solana',
'ripple' => 'Ripple',
'cardano' => 'Cardano',
'dogecoin' => 'Dogecoin',
'polkadot' => 'Polkadot',
'polygon' => 'Polygon',
'avalanche' => 'Avalanche',
'chainlink' => 'Chainlink',
'shiba_inu' => 'Shiba Inu',
'tron' => 'TRON',
'bitcoin_cash' => 'Bitcoin Cash',
'litecoin' => 'Litecoin',
'uniswap' => 'Uniswap',
'solana' => 'SOL',
'ripple' => 'XRP',
'cardano' => 'ADA',
'dogecoin' => 'DOGE',
'polkadot' => 'DOT',
'polygon' => 'MATIC',
'avalanche' => 'AVAX',
'chainlink' => 'LINK',
'shiba_inu' => 'SHIB',
'tron' => 'TRX',
'bitcoin_cash' => 'BCH',
'litecoin' => 'LTC',
'uniswap' => 'UNI',
'site_title' => 'Leading Digital Asset Platform',
'unverified' => 'Unverified',
'pending' => 'Pending',

View File

@ -15,7 +15,7 @@ require_once __DIR__ . '/includes/header.php';
</div>
<div class="carousel-inner">
<div class="carousel-item active">
<div class="carousel-content p-5 text-white d-flex align-items-center" style="height: 550px; background: url('assets/images/pexels/slide1.jpg') center/cover;">
<div class="carousel-content p-5 text-white d-flex align-items-center" style="height: 550px; background: url('assets/images/carousel/slide1.jpg') center/cover;">
<div class="carousel-overlay" style="background: linear-gradient(90deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.3) 100%); position: absolute; top:0; left:0; right:0; bottom:0;"></div>
<div class="container" style="position: relative; z-index: 10;">
<div class="row">
@ -32,7 +32,7 @@ require_once __DIR__ . '/includes/header.php';
</div>
</div>
<div class="carousel-item">
<div class="carousel-content p-5 text-white d-flex align-items-center" style="height: 550px; background: url('assets/images/pexels/slide2.jpg') center/cover;">
<div class="carousel-content p-5 text-white d-flex align-items-center" style="height: 550px; background: url('assets/images/carousel/slide2.jpg') center/cover;">
<div class="carousel-overlay" style="background: linear-gradient(90deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.3) 100%); position: absolute; top:0; left:0; right:0; bottom:0;"></div>
<div class="container" style="position: relative; z-index: 10;">
<div class="row">
@ -49,7 +49,7 @@ require_once __DIR__ . '/includes/header.php';
</div>
</div>
<div class="carousel-item">
<div class="carousel-content p-5 text-white d-flex align-items-center" style="height: 550px; background: url('assets/images/pexels/slide3.jpg') center/cover;">
<div class="carousel-content p-5 text-white d-flex align-items-center" style="height: 550px; background: url('assets/images/carousel/slide3.jpg') center/cover;">
<div class="carousel-overlay" style="background: linear-gradient(90deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.3) 100%); position: absolute; top:0; left:0; right:0; bottom:0;"></div>
<div class="container" style="position: relative; z-index: 10;">
<div class="row">

View File

@ -30,9 +30,9 @@ require_once __DIR__ . '/includes/header.php';
<hr class="border-secondary">
<h5 class="fw-bold mb-3"><?= __('popular_topics') ?></h5>
<div class="d-flex flex-wrap gap-2">
<span class="badge border border-secondary p-2">#Bitcoin</span>
<span class="badge border border-secondary p-2">#BTC</span>
<span class="badge border border-secondary p-2">#Web3</span>
<span class="badge border border-secondary p-2">#Ethereum</span>
<span class="badge border border-secondary p-2">#ETH</span>
<span class="badge border border-secondary p-2">#DeFi</span>
<span class="badge border border-secondary p-2">#NFTs</span>
</div>

View File

@ -42,6 +42,7 @@ foreach ($userBalances as $ub) {
}
}
// Get coins prices (simple static list)
function getCoinPrice($symbol) {
$prices = [
'BTC' => 65432.10,
@ -60,26 +61,9 @@ function getCoinPrice($symbol) {
return $prices[strtoupper($symbol)] ?? 1.00;
}
// Use the VIP level from the database column as the source of truth
$vipLevel = $userData['vip_level'] ?? 0;
function getAutoVipLevel($totalRecharge) {
if ($totalRecharge >= 10000000) return 7;
if ($totalRecharge >= 5000000) return 6;
if ($totalRecharge >= 1000000) return 5;
if ($totalRecharge >= 500000) return 4;
if ($totalRecharge >= 100000) return 3;
if ($totalRecharge >= 50000) return 2;
if ($totalRecharge >= 10001) return 1;
return 0;
}
// If the database VIP level is 0, we can optionally fall back to calculation,
// but since the admin can now set it to any level, we should respect the database.
// However, to keep it "automatic" for new users, we can do:
if ($vipLevel == 0) {
$vipLevel = getAutoVipLevel($userData['total_recharge'] ?? 0);
}
// Calculate total recharge and dynamic VIP level
$totalRecharge = getUserTotalRecharge($user['id']);
$vipLevel = getAutoVipLevel($totalRecharge);
// Fetch transactions
$stmt = db()->prepare("SELECT * FROM transactions WHERE user_id = ? ORDER BY created_at DESC LIMIT 50");
@ -133,10 +117,10 @@ $kycStatusColor = [
<span class="text-white-50 small"><i class="bi bi-gem me-2 text-warning"></i><?= __('vip_level') ?></span>
<span class="badge bg-warning text-dark fw-bold"><?= __('level') ?> <?= $vipLevel ?></span>
</div>
<?php if ($vipLevel < 7):
<?php if ($vipLevel < 5):
$nextLevel = $vipLevel + 1;
$thresholds = [1 => 10001, 2 => 50000, 3 => 100000, 4 => 500000, 5 => 1000000, 6 => 5000000, 7 => 10000000];
$needed = $thresholds[$nextLevel] - ($userData['total_recharge'] ?? 0);
$thresholds = [1 => 10000, 2 => 50000, 3 => 100000, 4 => 180000, 5 => 250000];
$needed = $thresholds[$nextLevel] - $totalRecharge;
if ($needed > 0):
?>
<div class="list-group-item py-2 bg-black bg-opacity-20 border-0">
@ -144,7 +128,7 @@ $kycStatusColor = [
<?= str_replace(['%amount%', '%level%'], [number_format($needed, 2), $nextLevel], __('recharge_to_upgrade')) ?>
</div>
<div class="progress rounded-pill shadow-sm" style="height: 4px; background: rgba(255,255,255,0.1);">
<div class="progress-bar bg-warning progress-bar-striped progress-bar-animated" style="width: <?= min(100, (($userData['total_recharge'] ?? 0) / $thresholds[$nextLevel]) * 100) ?>%"></div>
<div class="progress-bar bg-warning progress-bar-striped progress-bar-animated" style="width: <?= min(100, ($totalRecharge / $thresholds[$nextLevel]) * 100) ?>%"></div>
</div>
</div>
<?php endif; ?>

View File

@ -122,7 +122,7 @@ $bep20_addr = $settings['usdt_bep20_address'] ?? '0x742d35Cc6634C0532925a3b844Bc
<img src="<?= getCoinIcon('USDT') ?>" width="32" height="32" alt="USDT">
<div>
<div class="fw-bold text-white"><?= $lang === 'zh' ? __('USDT') : 'USDT' ?></div>
<div class="text-white-50 small"><?= $lang === 'zh' ? __('tether') : 'Tether' ?></div>
<div class="text-white-50 small"><?= __('tether') ?></div>
</div>
</div>
</div>
@ -194,6 +194,18 @@ $bep20_addr = $settings['usdt_bep20_address'] ?? '0x742d35Cc6634C0532925a3b844Bc
let currentNetwork = 'TRC20';
let currentAddress = '<?= $trc20_addr ?>';
function notify(icon, title, text = '') {
return Swal.fire({
icon: icon,
title: title,
text: text,
background: '#1e2329',
color: '#fff',
confirmButtonColor: '#0062ff',
confirmButtonText: '<?= __("confirm") ?>'
});
}
function updateRate() {
const select = document.getElementById('fiatCurrency');
const symbol = select.value;
@ -241,7 +253,7 @@ function confirmFiatOrder() {
const rate = parseFloat(select.options[select.selectedIndex].getAttribute('data-rate'));
if (isNaN(amount) || amount <= 0) {
alert('<?= __("enter_amount") ?>');
notify('warning', '<?= __("enter_amount") ?>');
return;
}
@ -271,7 +283,7 @@ function confirmFiatOrder() {
.replace('%res%', preciseRes);
sendToCS(message);
} else {
alert(data.error || '<?= __('request_failed') ?>');
notify('error', data.error || '<?= __('request_failed') ?>');
}
});
}
@ -281,7 +293,7 @@ function confirmCryptoOrder() {
const amount = parseFloat(amountInput.value);
if (isNaN(amount) || amount <= 0) {
alert('<?= __("enter_amount") ?>');
notify('warning', '<?= __("enter_amount") ?>');
return;
}
@ -304,7 +316,7 @@ function confirmCryptoOrder() {
.replace('%amount%', amount);
sendToCS(message);
} else {
alert(data.error || '<?= __('request_failed') ?>');
notify('error', data.error || '<?= __('request_failed') ?>');
}
});
}
@ -320,9 +332,9 @@ function sendToCS(message) {
if (csInput) {
csInput.value = message;
document.getElementById('cs-form').dispatchEvent(new Event('submit'));
alert('<?= __("request_sent") ?>');
notify('success', '<?= __("request_sent") ?>');
} else {
alert('<?= __("cs_connect_fail") ?>');
notify('error', '<?= __("cs_connect_fail") ?>');
}
}
@ -330,7 +342,17 @@ function copyAddress() {
const addr = document.getElementById('cryptoAddress');
addr.select();
document.execCommand('copy');
alert('<?= __("copy_success") ?>');
Swal.fire({
icon: 'success',
title: '<?= __("copy_success") ?>',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
background: '#1e2329',
color: '#fff'
});
}
</script>

View File

@ -55,7 +55,7 @@ $available = $bal['available'] ?? 0;
<img src="<?= getCoinIcon('USDT') ?>" width="32" height="32" alt="USDT">
<div>
<div class="fw-bold text-white"><?= $lang === 'zh' ? __('USDT') : 'USDT' ?></div>
<div class="text-white-50 small"><?= $lang === 'zh' ? __('tether') : 'Tether' ?></div>
<div class="text-white-50 small"><?= __('tether') ?></div>
</div>
</div>
</div>
@ -186,6 +186,18 @@ $available = $bal['available'] ?? 0;
<script>
let currentWithdrawNetwork = 'TRC20';
function notify(icon, title, text = '') {
return Swal.fire({
icon: icon,
title: title,
text: text,
background: '#1e2329',
color: '#fff',
confirmButtonColor: '#0062ff',
confirmButtonText: '<?= __("confirm") ?>'
});
}
function setMax(inputId) {
document.getElementById(inputId).value = <?= $available ?>;
if (inputId === 'withdrawAmount') calculateCryptoWithdraw();
@ -236,10 +248,10 @@ function confirmCryptoWithdraw() {
const amount = parseFloat(document.getElementById('withdrawAmount').value);
const password = document.getElementById('withdrawPassword').value;
if (!addr) { alert('<?= __("enter_address") ?>'); return; }
if (!amount || amount < 10) { alert('<?= __("min_withdraw_hint") ?>'); return; }
if (amount > <?= $available ?>) { alert('<?= __("insufficient_balance") ?>'); return; }
if (!password) { alert('<?= __("enter_password") ?>'); return; }
if (!addr) { notify('warning', '<?= __("enter_address") ?>'); return; }
if (!amount || amount < 10) { notify('warning', '<?= __("min_withdraw_hint") ?>'); return; }
if (amount > <?= $available ?>) { notify('error', '<?= __("insufficient_balance") ?>'); return; }
if (!password) { notify('warning', '<?= __("enter_password") ?>'); return; }
const formData = new FormData();
formData.append('action', 'withdraw');
@ -261,7 +273,7 @@ function confirmCryptoWithdraw() {
.replace('%amount%', amount.toFixed(2));
sendWithdrawToCS(message);
} else {
alert(data.error || '<?= __('request_failed') ?>');
notify('error', data.error || '<?= __('request_failed') ?>');
}
});
}
@ -274,9 +286,9 @@ function confirmFiatWithdraw() {
const rate = parseFloat(select.options[select.selectedIndex].getAttribute('data-rate'));
const password = document.getElementById('fiatWithdrawPassword').value;
if (isNaN(amount) || amount < 10) { alert('<?= __("min_withdraw_hint") ?>'); return; }
if (amount > <?= $available ?>) { alert('<?= __("insufficient_balance") ?>'); return; }
if (!password) { alert('<?= __("enter_password") ?>'); return; }
if (isNaN(amount) || amount < 10) { notify('warning', '<?= __("min_withdraw_hint") ?>'); return; }
if (amount > <?= $available ?>) { notify('error', '<?= __("insufficient_balance") ?>'); return; }
if (!password) { notify('warning', '<?= __("enter_password") ?>'); return; }
const estFiat = amount * rate;
@ -305,7 +317,7 @@ function confirmFiatWithdraw() {
.replace('%res%', preciseRes);
sendWithdrawToCS(message);
} else {
alert(data.error || '<?= __('request_failed') ?>');
notify('error', data.error || '<?= __('request_failed') ?>');
}
});
}
@ -321,9 +333,9 @@ function sendWithdrawToCS(message) {
if (csInput) {
csInput.value = message;
document.getElementById('cs-form').dispatchEvent(new Event('submit'));
alert('<?= __("request_sent") ?>');
notify('success', '<?= __("request_sent") ?>');
} else {
alert('<?= __("cs_connect_fail") ?>');
notify('error', '<?= __("cs_connect_fail") ?>');
}
}
</script>