719 lines
34 KiB
PHP
719 lines
34 KiB
PHP
<?php
|
|
session_start();
|
|
require_once __DIR__ . '/db/config.php';
|
|
|
|
$pdo = db();
|
|
|
|
if (!isset($_SESSION['user_id'])) {
|
|
header('Location: index.php');
|
|
exit;
|
|
}
|
|
|
|
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
|
$stmt->execute([$_SESSION['user_id']]);
|
|
$user = $stmt->fetch();
|
|
|
|
if (!$user) {
|
|
session_destroy();
|
|
header('Location: index.php?error=user_not_found');
|
|
exit;
|
|
}
|
|
|
|
// Ensure role is admin
|
|
if ($user['role'] !== 'admin') {
|
|
// Check if there are ANY admins in the system
|
|
$adminCount = $pdo->query("SELECT COUNT(*) FROM users WHERE role = 'admin'")->fetchColumn();
|
|
if ($adminCount == 0) {
|
|
// No admin exists? Make this user the admin automatically to prevent lock-out
|
|
$pdo->query("UPDATE users SET role = 'admin' WHERE id = " . $user['id']);
|
|
$user['role'] = 'admin';
|
|
$_SESSION['role'] = 'admin';
|
|
} else {
|
|
// Nicer access denied page
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Access Denied</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
|
<style>
|
|
body { height: 100vh; display: flex; align-items: center; justify-content: center; background: #f1f5f9; font-family: system-ui, -apple-system, sans-serif; }
|
|
.error-card { max-width: 450px; padding: 40px; text-align: center; border-radius: 16px; background: white; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); }
|
|
.icon-circle { width: 80px; height: 80px; background: #fee2e2; color: #ef4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 40px; margin: 0 auto 24px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="error-card">
|
|
<div class="icon-circle">
|
|
<i class="fas fa-shield-alt"></i>
|
|
</div>
|
|
<h4 class="fw-bold mb-3">权限不足 (Access Denied)</h4>
|
|
<p class="text-muted mb-4">您当前以 <strong><?= htmlspecialchars($user['username']) ?></strong> (角色: <?= htmlspecialchars($user['role']) ?>) 身份登录。后台管理面板仅限管理员访问。</p>
|
|
<div class="d-grid gap-2">
|
|
<a href="dashboard.php" class="btn btn-primary py-2">返回个人中心</a>
|
|
<a href="auth.php?action=logout" class="btn btn-outline-danger py-2">退出并登录管理员账号</a>
|
|
</div>
|
|
<div class="mt-4 pt-3 border-top">
|
|
<small class="text-muted">提示: 如果您是系统所有者但无法进入,请尝试访问 <code>/rescue.php</code></small>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
}
|
|
|
|
$action = $_GET['action'] ?? 'dashboard';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if ($action === 'confirm_recharge') {
|
|
$id = $_POST['id'];
|
|
$pdo->beginTransaction();
|
|
try {
|
|
$stmt = $pdo->prepare("SELECT * FROM recharges WHERE id = ? AND status = 'pending'");
|
|
$stmt->execute([$id]);
|
|
$recharge = $stmt->fetch();
|
|
if ($recharge) {
|
|
$stmt = $pdo->prepare("UPDATE recharges SET status = 'completed' WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$stmt = $pdo->prepare("UPDATE users SET balance = balance + ? WHERE id = ?");
|
|
$stmt->execute([$recharge['amount'], $recharge['user_id']]);
|
|
$pdo->commit();
|
|
} else {
|
|
$pdo->rollBack();
|
|
}
|
|
} catch (Exception $e) {
|
|
$pdo->rollBack();
|
|
}
|
|
header('Location: admin.php?action=recharges');
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'update_settings') {
|
|
foreach ($_POST['settings'] as $key => $value) {
|
|
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?");
|
|
$stmt->execute([$key, $value, $value]);
|
|
}
|
|
header('Location: admin.php?action=settings&success=1');
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'update_user') {
|
|
$id = $_POST['id'];
|
|
$balance = $_POST['balance'];
|
|
$role = $_POST['role'];
|
|
$stmt = $pdo->prepare("UPDATE users SET balance = ?, role = ? WHERE id = ?");
|
|
$stmt->execute([$balance, $role, $id]);
|
|
header('Location: admin.php?action=users');
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'add_user') {
|
|
$username = trim($_POST['username'] ?? '');
|
|
$password = $_POST['password'] ?? '';
|
|
$role = $_POST['role'] ?? 'user';
|
|
$balance = (float)($_POST['balance'] ?? 0);
|
|
|
|
if ($username && $password) {
|
|
$hash = password_hash($password, PASSWORD_DEFAULT);
|
|
$stmt = $pdo->prepare("INSERT INTO users (username, password_hash, role, balance) VALUES (?, ?, ?, ?)");
|
|
try {
|
|
$stmt->execute([$username, $hash, $role, $balance]);
|
|
} catch (Exception $e) {
|
|
header('Location: admin.php?action=users&error=user_exists');
|
|
exit;
|
|
}
|
|
}
|
|
header('Location: admin.php?action=users');
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'delete_user') {
|
|
$id = $_POST['id'];
|
|
if ($id != $_SESSION['user_id']) {
|
|
// Foreign keys are ON DELETE CASCADE, so this is safe
|
|
$stmt = $pdo->prepare("DELETE FROM users WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
}
|
|
header('Location: admin.php?action=users');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
$settings = $pdo->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
|
|
// Fetch stats for dashboard
|
|
$stats = [
|
|
'total_users' => $pdo->query("SELECT COUNT(*) FROM users")->fetchColumn(),
|
|
'total_recharge' => $pdo->query("SELECT SUM(amount) FROM recharges WHERE status = 'completed'")->fetchColumn() ?: 0,
|
|
'total_orders' => $pdo->query("SELECT COUNT(*) FROM sms_orders")->fetchColumn(),
|
|
'pending_recharges' => $pdo->query("SELECT COUNT(*) FROM recharges WHERE status = 'pending'")->fetchColumn()
|
|
];
|
|
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>后台管理 - <?= htmlspecialchars($settings['site_name'] ?? '全球接码') ?></title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--primary: #3b82f6;
|
|
--bg-body: #f8fafc;
|
|
--sidebar-width: 260px;
|
|
}
|
|
body { background: var(--bg-body); font-family: system-ui, -apple-system, sans-serif; }
|
|
.sidebar {
|
|
width: var(--sidebar-width);
|
|
height: 100vh;
|
|
position: fixed;
|
|
left: 0; top: 0;
|
|
background: #1e293b;
|
|
color: white;
|
|
padding: 20px;
|
|
z-index: 1000;
|
|
}
|
|
.main-content { margin-left: var(--sidebar-width); padding: 40px; }
|
|
.nav-link { color: #cbd5e1; padding: 12px 15px; border-radius: 8px; margin-bottom: 5px; }
|
|
.nav-link:hover, .nav-link.active { background: #334155; color: white; }
|
|
.nav-link i { width: 20px; margin-right: 10px; }
|
|
.card { border: none; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
.stat-card { padding: 24px; }
|
|
.stat-value { font-size: 24px; font-weight: 700; margin: 8px 0; }
|
|
.stat-label { color: #64748b; font-size: 14px; text-transform: uppercase; }
|
|
|
|
/* Support page styles */
|
|
.chat-user-item { transition: all 0.2s; border-radius: 10px; margin-bottom: 5px; }
|
|
.chat-user-item:hover { background: #f1f5f9; }
|
|
.chat-user-item.active { background: #e2e8f0; border-left: 4px solid var(--primary) !important; }
|
|
|
|
.message-row { display: flex; margin-bottom: 15px; width: 100%; }
|
|
.message-row.me { justify-content: flex-end; }
|
|
.message-row.them { justify-content: flex-start; }
|
|
|
|
.message-bubble {
|
|
max-width: 75%;
|
|
padding: 10px 15px;
|
|
border-radius: 18px;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
position: relative;
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
|
}
|
|
.me .message-bubble { background: var(--primary); color: white; border-bottom-right-radius: 4px; }
|
|
.them .message-bubble { background: white; color: #334155; border-bottom-left-radius: 4px; border: 1px solid #e2e8f0; }
|
|
|
|
.message-time { font-size: 10px; margin-top: 5px; opacity: 0.6; }
|
|
.me .message-time { text-align: right; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="sidebar">
|
|
<div class="mb-4 px-3 d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h5 class="fw-bold mb-0">管理后台</h5>
|
|
<small class="text-muted">ADMIN PANEL</small>
|
|
</div>
|
|
</div>
|
|
<nav class="nav flex-column">
|
|
<a class="nav-link <?= $action === 'dashboard' ? 'active' : '' ?>" href="admin.php?action=dashboard"><i class="fas fa-home"></i> 控制台</a>
|
|
<a class="nav-link <?= $action === 'users' ? 'active' : '' ?>" href="admin.php?action=users"><i class="fas fa-users"></i> 用户管理</a>
|
|
<a class="nav-link <?= $action === 'recharges' ? 'active' : '' ?>" href="admin.php?action=recharges"><i class="fas fa-wallet"></i> 充值管理</a>
|
|
<a class="nav-link <?= $action === 'orders' ? 'active' : '' ?>" href="admin.php?action=orders"><i class="fas fa-shopping-cart"></i> 订单记录</a>
|
|
<a class="nav-link <?= $action === 'support' ? 'active' : '' ?>" href="admin.php?action=support" style="position: relative;">
|
|
<i class="fas fa-headset"></i> 客服消息
|
|
<span id="supportBadgeGlobal" class="badge bg-danger rounded-pill" style="display:none; font-size: 10px; margin-left: 5px;">0</span>
|
|
</a>
|
|
<a class="nav-link <?= $action === 'settings' ? 'active' : '' ?>" href="admin.php?action=settings"><i class="fas fa-cog"></i> 系统设置</a>
|
|
<hr class="my-3 border-secondary">
|
|
<a class="nav-link" href="dashboard.php"><i class="fas fa-arrow-left"></i> 返回前台</a>
|
|
<a class="nav-link text-danger" href="auth.php?action=logout"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Notification Toast -->
|
|
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 9999;">
|
|
<div id="msgToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
|
<div class="toast-header bg-primary text-white">
|
|
<i class="fas fa-comment-alt me-2"></i>
|
|
<strong class="me-auto">新消息提醒</strong>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
<div class="toast-body" id="toastBody">
|
|
您收到了来自用户的新客服消息!
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<?php if ($action === 'dashboard'): ?>
|
|
<h4 class="fw-bold mb-4">数据概览</h4>
|
|
<div class="row g-4">
|
|
<div class="col-md-3">
|
|
<div class="card stat-card">
|
|
<div class="stat-label">总用户数</div>
|
|
<div class="stat-value"><?= $stats['total_users'] ?></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card stat-card">
|
|
<div class="stat-label">总充值金额</div>
|
|
<div class="stat-value">$<?= number_format($stats['total_recharge'], 2) ?></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card stat-card">
|
|
<div class="stat-label">总订单数</div>
|
|
<div class="stat-value"><?= $stats['total_orders'] ?></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card stat-card">
|
|
<div class="stat-label">待审核充值</div>
|
|
<div class="stat-value text-warning"><?= $stats['pending_recharges'] ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($action === 'users'): ?>
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h4 class="fw-bold mb-0">用户管理</h4>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addUserModal"><i class="fas fa-user-plus me-2"></i>添加用户</button>
|
|
</div>
|
|
|
|
<?php if (isset($_GET['error']) && $_GET['error'] === 'user_exists'): ?>
|
|
<div class="alert alert-danger">用户名已存在,请换一个。</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="card p-3">
|
|
<table class="table align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>用户名</th>
|
|
<th>余额</th>
|
|
<th>角色</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$users = $pdo->query("SELECT * FROM users ORDER BY id DESC")->fetchAll();
|
|
foreach ($users as $u): ?>
|
|
<tr>
|
|
<td><?= $u['id'] ?></td>
|
|
<td><?= htmlspecialchars($u['username']) ?></td>
|
|
<td>$<?= number_format($u['balance'], 2) ?></td>
|
|
<td><span class="badge bg-<?= $u['role'] === 'admin' ? 'danger' : 'info' ?>"><?= $u['role'] ?></span></td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-primary" onclick='editUser(<?= json_encode($u) ?>)'>编辑</button>
|
|
<?php if ($u['id'] != $_SESSION['user_id']): ?>
|
|
<form method="POST" action="admin.php?action=delete_user" style="display:inline;" onsubmit="return confirm('确定要删除此用户吗?其关联的订单、充值和聊天记录都将被永久删除!')">
|
|
<input type="hidden" name="id" value="<?= $u['id'] ?>">
|
|
<button type="submit" class="btn btn-sm btn-outline-danger">删除</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Add User Modal -->
|
|
<div class="modal fade" id="addUserModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<form class="modal-content" method="POST" action="admin.php?action=add_user">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">添加新用户</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">用户名</label>
|
|
<input type="text" class="form-control" name="username" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">密码</label>
|
|
<input type="password" class="form-control" name="password" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">初始余额</label>
|
|
<input type="number" step="0.01" class="form-control" name="balance" value="0.00">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">角色</label>
|
|
<select class="form-select" name="role">
|
|
<option value="user">User</option>
|
|
<option value="admin">Admin</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="submit" class="btn btn-primary">立即创建</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit User Modal -->
|
|
<div class="modal fade" id="userModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<form class="modal-content" method="POST" action="admin.php?action=update_user">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">编辑用户</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" name="id" id="userId">
|
|
<div class="mb-3">
|
|
<label class="form-label">余额</label>
|
|
<input type="number" step="0.01" class="form-control" name="balance" id="userBalanceInput">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">角色</label>
|
|
<select class="form-select" name="role" id="userRoleSelect">
|
|
<option value="user">User</option>
|
|
<option value="admin">Admin</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="submit" class="btn btn-primary">保存修改</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
function editUser(user) {
|
|
document.getElementById('userId').value = user.id;
|
|
document.getElementById('userBalanceInput').value = user.balance;
|
|
document.getElementById('userRoleSelect').value = user.role;
|
|
new bootstrap.Modal(document.getElementById('userModal')).show();
|
|
}
|
|
</script>
|
|
|
|
<?php elseif ($action === 'recharges'): ?>
|
|
<h4 class="fw-bold mb-4">充值管理</h4>
|
|
<div class="card p-3">
|
|
<table class="table align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>用户</th>
|
|
<th>金额</th>
|
|
<th>TXID / 备注</th>
|
|
<th>时间</th>
|
|
<th>状态</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$recharges = $pdo->query("SELECT r.*, u.username FROM recharges r JOIN users u ON r.user_id = u.id ORDER BY r.created_at DESC")->fetchAll();
|
|
foreach ($recharges as $r): ?>
|
|
<tr>
|
|
<td><?= $r['id'] ?></td>
|
|
<td><?= htmlspecialchars($r['username']) ?></td>
|
|
<td class="fw-bold">$<?= number_format($r['amount'], 2) ?></td>
|
|
<td><small class="text-muted"><?= htmlspecialchars($r['txid']) ?></small></td>
|
|
<td><?= $r['created_at'] ?></td>
|
|
<td>
|
|
<span class="badge bg-<?= $r['status'] === 'completed' ? 'success' : ($r['status'] === 'pending' ? 'warning' : 'secondary') ?>">
|
|
<?= $r['status'] ?>
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<?php if ($r['status'] === 'pending'): ?>
|
|
<form method="POST" action="admin.php?action=confirm_recharge" style="display:inline;">
|
|
<input type="hidden" name="id" value="<?= $r['id'] ?>">
|
|
<button type="submit" class="btn btn-sm btn-success" onclick="return confirm('确定手动确认此笔充值并增加用户余额?')">确认入账</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<?php elseif ($action === 'orders'): ?>
|
|
<h4 class="fw-bold mb-4">订单记录</h4>
|
|
<div class="card p-3">
|
|
<table class="table align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>用户</th>
|
|
<th>项目</th>
|
|
<th>国家</th>
|
|
<th>号码</th>
|
|
<th>费用</th>
|
|
<th>状态</th>
|
|
<th>时间</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$orders = $pdo->query("SELECT o.*, u.username FROM sms_orders o JOIN users u ON o.user_id = u.id ORDER BY o.id DESC LIMIT 100")->fetchAll();
|
|
foreach ($orders as $o): ?>
|
|
<tr>
|
|
<td><?= $o['id'] ?></td>
|
|
<td><?= htmlspecialchars($o['username']) ?></td>
|
|
<td><?= htmlspecialchars($o['service_name']) ?></td>
|
|
<td><?= htmlspecialchars($o['country_name']) ?></td>
|
|
<td><?= $o['number'] ?></td>
|
|
<td>$<?= number_format($o['cost'], 2) ?></td>
|
|
<td><span class="badge bg-<?= $o['status'] === 'received' ? 'success' : ($o['status'] === 'pending' ? 'warning' : 'secondary') ?>"><?= $o['status'] ?></span></td>
|
|
<td><small><?= $o['created_at'] ?></small></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<?php elseif ($action === 'support'): ?>
|
|
<h4 class="fw-bold mb-4">客服消息</h4>
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="card p-2" style="height: 600px; overflow-y: auto; background: white;">
|
|
<div id="chatUserList">
|
|
<div class="text-center py-5 text-muted">正在加载对话...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<div class="card d-flex flex-column shadow-sm" style="height: 600px; border: 1px solid #e2e8f0;">
|
|
<div class="card-header bg-white py-3 border-bottom d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<span id="chatTitle" class="fw-bold">请选择一个对话</span>
|
|
<div id="chatSubTitle" class="text-muted small" style="display:none;">在线</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-4 flex-grow-1" id="chatContent" style="overflow-y: auto; background: #f8fafc;">
|
|
<div class="h-100 d-flex align-items-center justify-content-center text-muted">
|
|
<div class="text-center">
|
|
<i class="fas fa-comments fs-1 mb-3 opacity-25"></i>
|
|
<p>点击左侧用户开始聊天</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer bg-white p-3 border-top">
|
|
<form id="adminChatForm" onsubmit="event.preventDefault(); sendMessage();" class="input-group">
|
|
<input type="text" id="adminMsgInput" class="form-control border-end-0" placeholder="输入回复内容..." autocomplete="off">
|
|
<button type="submit" class="btn btn-primary px-4 border-start-0"><i class="fas fa-paper-plane"></i></button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
let currentChatUser = null;
|
|
let loadedMessageIds = new Set();
|
|
let isFirstLoad = true;
|
|
|
|
async function loadChatUsers() {
|
|
try {
|
|
const res = await fetch('ajax_handler.php?action=get_chat_users');
|
|
const data = await res.json();
|
|
if (data.code === 0) {
|
|
const list = document.getElementById('chatUserList');
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
data.data.forEach(u => {
|
|
const div = document.createElement('div');
|
|
const isActive = currentChatUser === parseInt(u.id);
|
|
div.className = `p-3 chat-user-item border-bottom ${isActive ? 'active' : ''}`;
|
|
div.style.cursor = 'pointer';
|
|
div.innerHTML = `
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<strong class="${u.unread_count > 0 ? 'text-primary' : ''}">${u.username}</strong>
|
|
<small class="text-muted" style="font-size: 10px;">${u.last_time.split(' ')[1]}</small>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div class="text-truncate small ${u.unread_count > 0 ? 'fw-bold' : 'text-muted'}" style="max-width: 150px;">${u.last_message}</div>
|
|
${u.unread_count > 0 ? `<span class="badge bg-danger rounded-pill" style="font-size: 10px;">${u.unread_count}</span>` : ''}
|
|
</div>
|
|
`;
|
|
div.onclick = () => selectUser(parseInt(u.id), u.username);
|
|
fragment.appendChild(div);
|
|
});
|
|
list.innerHTML = '';
|
|
list.appendChild(fragment);
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
function selectUser(id, name) {
|
|
if (currentChatUser !== id) {
|
|
currentChatUser = id;
|
|
loadedMessageIds.clear();
|
|
document.getElementById('chatContent').innerHTML = '';
|
|
isFirstLoad = true;
|
|
}
|
|
document.getElementById('chatTitle').textContent = name;
|
|
document.getElementById('chatSubTitle').style.display = 'block';
|
|
loadMessages();
|
|
loadChatUsers();
|
|
document.getElementById('adminMsgInput').focus();
|
|
}
|
|
|
|
async function loadMessages() {
|
|
if (!currentChatUser) return;
|
|
try {
|
|
const res = await fetch('ajax_handler.php?action=get_messages&user_id=' + currentChatUser);
|
|
const data = await res.json();
|
|
if (data.code === 0) {
|
|
const content = document.getElementById('chatContent');
|
|
let hasNew = false;
|
|
|
|
data.data.forEach(m => {
|
|
if (!loadedMessageIds.has(m.id)) {
|
|
appendMessageToUI(m);
|
|
loadedMessageIds.add(m.id);
|
|
hasNew = true;
|
|
}
|
|
});
|
|
|
|
if (hasNew) {
|
|
content.scrollTop = content.scrollHeight;
|
|
}
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
function appendMessageToUI(m) {
|
|
const content = document.getElementById('chatContent');
|
|
const isMe = m.sender === 'admin';
|
|
const div = document.createElement('div');
|
|
div.className = `message-row ${isMe ? 'me' : 'them'}`;
|
|
|
|
const time = m.created_at.split(' ')[1].substring(0, 5);
|
|
|
|
div.innerHTML = `
|
|
<div class="message-bubble shadow-sm">
|
|
<div style="word-break: break-all;">${escapeHtml(m.message)}</div>
|
|
<div class="message-time">${time}</div>
|
|
</div>
|
|
`;
|
|
content.appendChild(div);
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function sendMessage() {
|
|
const input = document.getElementById('adminMsgInput');
|
|
const msg = input.value.trim();
|
|
if (!msg || !currentChatUser) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('message', msg);
|
|
formData.append('user_id', currentChatUser);
|
|
|
|
input.value = '';
|
|
try {
|
|
const res = await fetch('ajax_handler.php?action=send_message', { method: 'POST', body: formData });
|
|
const data = await res.json();
|
|
if (data.code === 0) {
|
|
loadMessages();
|
|
loadChatUsers();
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
loadChatUsers();
|
|
setInterval(() => { loadChatUsers(); loadMessages(); }, 3000);
|
|
</script>
|
|
|
|
<?php elseif ($action === 'settings'): ?>
|
|
<h4 class="fw-bold mb-4">系统设置</h4>
|
|
<div class="card p-4">
|
|
<?php if (isset($_GET['success'])): ?>
|
|
<div class="alert alert-success">设置已更新</div>
|
|
<?php endif; ?>
|
|
<form method="POST" action="admin.php?action=update_settings">
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">网站名称</label>
|
|
<input type="text" class="form-control" name="settings[site_name]" value="<?= htmlspecialchars($settings['site_name'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">网站Logo URL</label>
|
|
<input type="text" class="form-control" name="settings[site_logo]" value="<?= htmlspecialchars($settings['site_logo'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-12 mb-3">
|
|
<label class="form-label">系统公告</label>
|
|
<textarea class="form-control" name="settings[notice_text]" rows="3"><?= htmlspecialchars($settings['notice_text'] ?? '') ?></textarea>
|
|
</div>
|
|
<div class="col-md-12 mb-3">
|
|
<label class="form-label">Luban SMS API Key</label>
|
|
<input type="text" class="form-control" name="settings[lubansms_apikey]" value="<?= htmlspecialchars($settings['lubansms_apikey'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">USDT (TRC20) 收款地址</label>
|
|
<input type="text" class="form-control" name="settings[usdt_trc20_address]" value="<?= htmlspecialchars($settings['usdt_trc20_address'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">USDT (ERC20) 收款地址</label>
|
|
<input type="text" class="form-control" name="settings[usdt_erc20_address]" value="<?= htmlspecialchars($settings['usdt_erc20_address'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">汇率 (1 USDT = ? 余额)</label>
|
|
<input type="number" step="0.01" class="form-control" name="settings[exchange_rate]" value="<?= htmlspecialchars($settings['exchange_rate'] ?? '1.0') ?>">
|
|
</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary px-4">保存设置</button>
|
|
</form>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<audio id="notifSound" src="https://assets.mixkit.co/active_storage/sfx/2354/2354-preview.mp3" preload="auto"></audio>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
let lastUnreadCount = 0;
|
|
const msgToast = new bootstrap.Toast(document.getElementById('msgToast'), { delay: 5000 });
|
|
const notifSound = document.getElementById('notifSound');
|
|
|
|
async function checkGlobalNotifications() {
|
|
try {
|
|
const res = await fetch('ajax_handler.php?action=check_new_messages');
|
|
const data = await res.json();
|
|
if (data.code === 0) {
|
|
const count = parseInt(data.unread_total);
|
|
const badge = document.getElementById('supportBadgeGlobal');
|
|
if (count > 0) {
|
|
badge.textContent = count;
|
|
badge.style.display = 'inline-block';
|
|
|
|
if (count > lastUnreadCount) {
|
|
const toastBody = document.getElementById('toastBody');
|
|
toastBody.textContent = `您收到了来自 ${data.last_user || '用户'} 的新客服消息!`;
|
|
msgToast.show();
|
|
try { notifSound.play().catch(e => console.log('Audio play failed')); } catch(e) {}
|
|
}
|
|
} else {
|
|
badge.style.display = 'none';
|
|
}
|
|
lastUnreadCount = count;
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
setInterval(checkGlobalNotifications, 5000);
|
|
checkGlobalNotifications();
|
|
</script>
|
|
</body>
|
|
</html>
|