Compare commits

..

No commits in common. "ai-dev" and "master" have entirely different histories.

20 changed files with 324 additions and 3369 deletions

719
admin.php
View File

@ -1,719 +0,0 @@
<?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>

View File

@ -1,217 +0,0 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/api/LocalLubanApi.php';
// Price multiplier to earn profit (User requested 1.5 - 2x)
const PRICE_MULTIPLIER = 1.8;
$pdo = db();
// Ensure apikey is loaded
$db_apikey = null;
try {
$stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = 'lubansms_apikey'");
$stmt->execute();
$db_apikey = $stmt->fetchColumn();
// Fallback if direct match fails
if (!$db_apikey) {
$settings = $pdo->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
foreach ($settings as $k => $v) {
if (strpos($k, 'lubansms_apikey') !== false) {
$db_apikey = trim($v);
break;
}
}
}
} catch (Exception $e) {}
$api = new LubanSMS($db_apikey);
$action = $_GET['action'] ?? '';
header('Content-Type: application/json; charset=utf-8');
// Basic Auth check
if (!isset($_SESSION['user_id']) && $action !== 'login') {
echo json_encode(['code' => 401, 'msg' => '未登录或登录已过期']);
exit;
}
try {
switch ($action) {
case 'get_balance':
$stmt = $pdo->prepare("SELECT balance FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$balance = $stmt->fetchColumn();
echo json_encode(['code' => 0, 'balance' => number_format((float)$balance, 2)]);
break;
case 'get_countries':
$res = $api->getCountries();
echo json_encode(['code' => 0, 'data' => $res['msg'] ?? $res['data'] ?? []], JSON_UNESCAPED_UNICODE);
break;
case 'get_services':
$country = $_GET['country'] ?? '';
$service = $_GET['service'] ?? '';
$res = $api->getServices($country, $service);
if ($res && (int)($res['code'] ?? -1) === 0) {
$data = $res['msg'] ?? $res['data'] ?? [];
if (!is_array($data)) $data = [];
foreach ($data as &$item) {
if (isset($item['cost'])) {
$item['cost'] = round((float)$item['cost'] * PRICE_MULTIPLIER, 2);
}
}
echo json_encode(['code' => 0, 'data' => $data], JSON_UNESCAPED_UNICODE);
} else {
echo json_encode($res ?: ['code' => 500, 'msg' => '获取项目列表失败'], JSON_UNESCAPED_UNICODE);
}
break;
case 'get_number':
$service_id = $_GET['service_id'] ?? '';
$country_name = $_GET['country_name'] ?? '未知国家';
$service_name = $_GET['service_name'] ?? '未知项目';
$price = (float)($_GET['price'] ?? 0);
$stmt = $pdo->prepare("SELECT balance FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$balance = (float)$stmt->fetchColumn();
if ($balance < $price) {
echo json_encode(['code' => 400, 'msg' => '余额不足,请先充值']);
break;
}
$res = $api->getNumber($service_id);
if ($res && (int)($res['code'] ?? -1) === 0) {
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("UPDATE users SET balance = balance - ? WHERE id = ?");
$stmt->execute([$price, $_SESSION['user_id']]);
$stmt = $pdo->prepare("INSERT INTO sms_orders (user_id, request_id, number, service_name, country_name, cost, status, expire_at) VALUES (?, ?, ?, ?, ?, ?, 'pending', DATE_ADD(NOW(), INTERVAL 10 MINUTE))");
$stmt->execute([$_SESSION['user_id'], $res['request_id'], $res['number'], $service_name, $country_name, $price]);
$pdo->commit();
echo json_encode($res, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
$pdo->rollBack();
echo json_encode(['code' => 500, 'msg' => '数据库事务错误'], JSON_UNESCAPED_UNICODE);
}
} else {
echo json_encode($res ?: ['code' => 500, 'msg' => 'API获取号码失败'], JSON_UNESCAPED_UNICODE);
}
break;
case 'check_sms':
$request_id = $_GET['request_id'] ?? '';
$res = $api->getSms($request_id);
if ($res && (int)($res['code'] ?? -1) === 0 && (string)($res['msg'] ?? '') === 'success') {
$stmt = $pdo->prepare("UPDATE sms_orders SET sms_content = ?, status = 'received' WHERE request_id = ?");
$stmt->execute([$res['sms_code'], $request_id]);
}
echo json_encode($res ?: ['code' => 500, 'msg' => 'API Error'], JSON_UNESCAPED_UNICODE);
break;
case 'create_recharge':
$amount = (float)($_POST['amount'] ?? 0);
if ($amount < 10) { echo json_encode(['code' => 400, 'msg' => '最低充值金额为 10 USDT']); break; }
$final_amount = floor($amount) + (rand(1, 99) / 100);
$stmt = $pdo->prepare("INSERT INTO recharges (user_id, amount, txid, status) VALUES (?, ?, 'Manual/Auto', 'pending')");
$stmt->execute([$_SESSION['user_id'], $final_amount]);
echo json_encode(['code' => 0, 'recharge_id' => $pdo->lastInsertId(), 'amount' => $final_amount]);
break;
case 'check_recharge_status':
$recharge_id = $_GET['recharge_id'] ?? '';
$stmt = $pdo->prepare("SELECT * FROM recharges WHERE id = ? AND user_id = ?");
$stmt->execute([$recharge_id, $_SESSION['user_id']]);
$recharge = $stmt->fetch();
if (!$recharge) { echo json_encode(['code' => 404, 'msg' => '未找到充值订单']); break; }
if ($recharge['status'] === 'completed') { echo json_encode(['code' => 0, 'status' => 'completed']); break; }
echo json_encode(['code' => 0, 'status' => 'pending']);
break;
case 'send_message':
$message = trim($_POST['message'] ?? '');
$target_user_id = $_POST['user_id'] ?? $_SESSION['user_id'];
if (!$message) { echo json_encode(['code' => 400, 'msg' => '消息内容不能为空']); break; }
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$role = $stmt->fetchColumn();
$sender = ($role === 'admin') ? 'admin' : 'user';
$stmt = $pdo->prepare("INSERT INTO support_messages (user_id, sender, message, `is_read`) VALUES (?, ?, ?, 0)");
$stmt->execute([$target_user_id, $sender, $message]);
echo json_encode(['code' => 0, 'msg' => '已发送']);
break;
case 'get_messages':
$target_user_id = $_GET['user_id'] ?? $_SESSION['user_id'];
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$isAdmin = ($stmt->fetchColumn() === 'admin');
if (!$isAdmin && (int)$target_user_id !== (int)$_SESSION['user_id']) {
echo json_encode(['code' => 403, 'msg' => 'Forbidden']); break;
}
if ($isAdmin && (int)$target_user_id !== (int)$_SESSION['user_id']) {
$pdo->prepare("UPDATE support_messages SET `is_read` = 1 WHERE user_id = ? AND sender = 'user'")->execute([$target_user_id]);
} else if (!$isAdmin) {
$pdo->prepare("UPDATE support_messages SET `is_read` = 1 WHERE user_id = ? AND sender = 'admin'")->execute([$_SESSION['user_id']]);
}
$stmt = $pdo->prepare("SELECT * FROM support_messages WHERE user_id = ? ORDER BY id ASC");
$stmt->execute([$target_user_id]);
echo json_encode(['code' => 0, 'data' => $stmt->fetchAll()]);
break;
case 'get_chat_users':
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
if ($stmt->fetchColumn() !== 'admin') { echo json_encode(['code' => 403, 'msg' => 'Forbidden']); break; }
// Optimized query to get last message reliably
$stmt = $pdo->query("
SELECT u.id, u.username, m.message as last_message, m.created_at as last_time,
(SELECT COUNT(*) FROM support_messages WHERE user_id = u.id AND sender = 'user' AND `is_read` = 0) as unread_count
FROM users u
JOIN (SELECT user_id, MAX(id) as max_id FROM support_messages GROUP BY user_id) last_msg_idx ON u.id = last_msg_idx.user_id
JOIN support_messages m ON m.id = last_msg_idx.max_id
ORDER BY m.id DESC
");
echo json_encode(['code' => 0, 'data' => $stmt->fetchAll()]);
break;
case 'check_new_messages':
$stmt = $pdo->prepare("SELECT role FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$role = $stmt->fetchColumn();
if ($role === 'admin') {
$stmt = $pdo->query("
SELECT m.*, u.username
FROM support_messages m
JOIN users u ON m.user_id = u.id
WHERE m.sender = 'user' AND m.`is_read` = 0
ORDER BY m.id DESC LIMIT 1
");
$last_unread = $stmt->fetch(PDO::FETCH_ASSOC);
$total_unread = $pdo->query("SELECT COUNT(*) FROM support_messages WHERE sender = 'user' AND `is_read` = 0")->fetchColumn();
echo json_encode(['code' => 0, 'unread_total' => $total_unread, 'last_user' => $last_unread['username'] ?? '']);
} else {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM support_messages WHERE user_id = ? AND sender = 'admin' AND `is_read` = 0");
$stmt->execute([$_SESSION['user_id']]);
echo json_encode(['code' => 0, 'unread_total' => $stmt->fetchColumn()]);
}
break;
default:
echo json_encode(['code' => 404, 'msg' => '未知请求']);
break;
}
} catch (Exception $e) {
echo json_encode(['code' => 500, 'msg' => $e->getMessage()]);
}

View File

@ -1,101 +0,0 @@
<?php
require_once __DIR__ . '/../db/config.php';
class LubanSMS {
private $apikey;
private $baseUrl = 'https://lubansms.com/v2/api/';
public function __construct($apikey = null) {
if ($apikey) {
$this->apikey = trim($apikey);
} else {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = 'lubansms_apikey'");
$stmt->execute();
$this->apikey = $stmt->fetchColumn();
if (!$this->apikey) {
$stmt = $pdo->query("SELECT setting_key, setting_value FROM settings");
$settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
foreach ($settings as $k => $v) {
if (strpos($k, 'lubansms_apikey') !== false) {
$this->apikey = trim($v);
break;
}
}
}
} catch (Exception $e) {
// Log error or handle
}
}
}
private function request($endpoint, $params = []) {
if (!$this->apikey || empty($this->apikey)) {
return ['code' => 500, 'msg' => 'API Key not configured (DB check failed)'];
}
$params['apikey'] = $this->apikey;
$url = $this->baseUrl . $endpoint . '?' . http_build_query($params);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
return ['code' => 500, 'msg' => 'CURL Error: ' . $error];
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
return ['code' => 500, 'msg' => 'API Server returned HTTP ' . $httpCode];
}
$decoded = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return ['code' => 500, 'msg' => 'Invalid JSON from API'];
}
return $decoded;
}
public function getBalance() {
return $this->request('getBalance');
}
public function getCountries() {
return $this->request('countries');
}
public function getServices($countryName = '', $serviceName = '', $page = 1) {
$params = [
'page' => $page,
'language' => 'zh'
];
if ($countryName) $params['country'] = $countryName;
if ($serviceName) $params['service'] = $serviceName;
return $this->request('List', $params);
}
public function getNumber($service_id) {
return $this->request('getNumber', ['service_id' => $service_id]);
}
public function getSms($request_id) {
return $this->request('getSms', ['request_id' => $request_id]);
}
public function setStatus($request_id, $status = 'reject') {
return $this->request('setStatus', [
'request_id' => $request_id,
'status' => $status
]);
}
}

View File

@ -1,15 +1,15 @@
:root { :root {
--color-bg: #fff5f7; /* Very Light Pink Background */ --color-bg: #ffffff;
--color-text: #2d1a1e; /* Dark Brownish Pink for Text */ --color-text: #1a1a1a;
--color-primary: #ff4d94; /* Vibrant Pink */ --color-primary: #2563EB; /* Vibrant Blue */
--color-secondary: #ff1a75; /* Deep Pink */ --color-secondary: #000000;
--color-accent: #ffd1dc; /* Soft Pastel Pink */ --color-accent: #A3E635; /* Lime Green */
--color-surface: #ffffff; --color-surface: #f8f9fa;
--font-heading: 'Space Grotesk', sans-serif; --font-heading: 'Space Grotesk', sans-serif;
--font-body: 'Inter', sans-serif; --font-body: 'Inter', sans-serif;
--border-width: 2px; --border-width: 2px;
--shadow-hard: 5px 5px 0px #ff4d94; --shadow-hard: 5px 5px 0px #000;
--shadow-hover: 8px 8px 0px #ff1a75; --shadow-hover: 8px 8px 0px #000;
--radius-pill: 50rem; --radius-pill: 50rem;
--radius-card: 1rem; --radius-card: 1rem;
} }
@ -28,15 +28,15 @@ h1, h2, h3, h4, h5, h6, .navbar-brand {
/* Utilities */ /* Utilities */
.text-primary { color: var(--color-primary) !important; } .text-primary { color: var(--color-primary) !important; }
.bg-pink { background-color: var(--color-primary) !important; } .bg-black { background-color: #000 !important; }
.text-white { color: #fff !important; } .text-white { color: #fff !important; }
.shadow-hard { box-shadow: var(--shadow-hard); } .shadow-hard { box-shadow: var(--shadow-hard); }
.border-2-pink { border: var(--border-width) solid var(--color-primary); } .border-2-black { border: var(--border-width) solid #000; }
.py-section { padding-top: 5rem; padding-bottom: 5rem; } .py-section { padding-top: 5rem; padding-bottom: 5rem; }
/* Navbar */ /* Navbar */
.navbar { .navbar {
background: rgba(255, 245, 247, 0.9); background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-bottom: var(--border-width) solid transparent; border-bottom: var(--border-width) solid transparent;
transition: all 0.3s; transition: all 0.3s;
@ -45,7 +45,7 @@ h1, h2, h3, h4, h5, h6, .navbar-brand {
} }
.navbar.scrolled { .navbar.scrolled {
border-bottom-color: var(--color-primary); border-bottom-color: #000;
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
@ -72,7 +72,7 @@ h1, h2, h3, h4, h5, h6, .navbar-brand {
font-family: var(--font-heading); font-family: var(--font-heading);
padding: 0.8rem 2rem; padding: 0.8rem 2rem;
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
border: var(--border-width) solid var(--color-primary); border: var(--border-width) solid #000;
transition: all 0.2s cubic-bezier(0.25, 1, 0.5, 1); transition: all 0.2s cubic-bezier(0.25, 1, 0.5, 1);
box-shadow: var(--shadow-hard); box-shadow: var(--shadow-hard);
} }
@ -84,29 +84,34 @@ h1, h2, h3, h4, h5, h6, .navbar-brand {
.btn:active { .btn:active {
transform: translate(2px, 2px); transform: translate(2px, 2px);
box-shadow: 0 0 0 var(--color-primary); box-shadow: 0 0 0 #000;
} }
.btn-primary { .btn-primary {
background-color: var(--color-primary); background-color: var(--color-primary);
border-color: var(--color-secondary); border-color: #000;
color: #fff; color: #fff;
} }
.btn-primary:hover { .btn-primary:hover {
background-color: var(--color-secondary); background-color: #1d4ed8;
border-color: var(--color-secondary); border-color: #000;
color: #fff; color: #fff;
} }
.btn-outline-pink { .btn-outline-dark {
background-color: #fff; background-color: #fff;
color: var(--color-primary); color: #000;
} }
.btn-cta { .btn-cta {
background-color: var(--color-accent); background-color: var(--color-accent);
color: var(--color-secondary); color: #000;
}
.btn-cta:hover {
background-color: #8cc629;
color: #000;
} }
/* Hero Section */ /* Hero Section */
@ -147,9 +152,53 @@ h1, h2, h3, h4, h5, h6, .navbar-brand {
padding: 0 5px; padding: 0 5px;
} }
.dot { color: var(--color-primary); }
.badge-pill {
display: inline-block;
padding: 0.5rem 1rem;
border: 2px solid #000;
border-radius: 50px;
font-weight: 700;
background: #fff;
box-shadow: 4px 4px 0 #000;
font-family: var(--font-heading);
font-size: 0.9rem;
}
/* Marquee */
.marquee-container {
overflow: hidden;
white-space: nowrap;
border-top: 2px solid #000;
border-bottom: 2px solid #000;
}
.rotate-divider {
transform: rotate(-2deg) scale(1.05);
z-index: 10;
position: relative;
margin-top: -50px;
margin-bottom: 30px;
}
.marquee-content {
display: inline-block;
animation: marquee 20s linear infinite;
font-family: var(--font-heading);
font-weight: 700;
font-size: 1.5rem;
letter-spacing: 2px;
}
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
/* Portfolio Cards */ /* Portfolio Cards */
.project-card { .project-card {
border: 2px solid var(--color-primary); border: 2px solid #000;
border-radius: var(--radius-card); border-radius: var(--radius-card);
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
@ -162,14 +211,37 @@ h1, h2, h3, h4, h5, h6, .navbar-brand {
.project-card:hover { .project-card:hover {
transform: translateY(-10px); transform: translateY(-10px);
box-shadow: var(--shadow-hover); box-shadow: 8px 8px 0 #000;
} }
.card-img-holder {
height: 250px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 2px solid #000;
position: relative;
font-size: 4rem;
}
.placeholder-art {
transition: transform 0.3s ease;
}
.project-card:hover .placeholder-art {
transform: scale(1.2) rotate(10deg);
}
.bg-soft-blue { background-color: #e0f2fe; }
.bg-soft-green { background-color: #dcfce7; }
.bg-soft-purple { background-color: #f3e8ff; }
.bg-soft-yellow { background-color: #fef9c3; }
.category-tag { .category-tag {
position: absolute; position: absolute;
top: 15px; top: 15px;
right: 15px; right: 15px;
background: var(--color-primary); background: #000;
color: #fff; color: #fff;
padding: 5px 12px; padding: 5px 12px;
border-radius: 20px; border-radius: 20px;
@ -177,16 +249,98 @@ h1, h2, h3, h4, h5, h6, .navbar-brand {
font-weight: 700; font-weight: 700;
} }
.card-body { padding: 1.5rem; }
.link-arrow {
text-decoration: none;
color: #000;
font-weight: 700;
display: inline-flex;
align-items: center;
margin-top: auto;
}
.link-arrow i { transition: transform 0.2s; margin-left: 5px; }
.link-arrow:hover i { transform: translateX(5px); }
/* About */
.about-image-stack {
position: relative;
height: 400px;
width: 100%;
}
.stack-card {
position: absolute;
width: 80%;
height: 100%;
border-radius: var(--radius-card);
border: 2px solid #000;
box-shadow: var(--shadow-hard);
left: 10%;
transform: rotate(-3deg);
background-size: cover;
}
/* Forms */
.form-control { .form-control {
border: 2px solid var(--color-accent); border: 2px solid #000;
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 1rem; padding: 1rem;
font-weight: 500; font-weight: 500;
background: #fff; background: #f8f9fa;
} }
.form-control:focus { .form-control:focus {
box-shadow: 4px 4px 0 var(--color-primary); box-shadow: 4px 4px 0 var(--color-primary);
border-color: var(--color-primary); border-color: #000;
background: #fff; background: #fff;
} }
/* Animations */
.animate-up {
opacity: 0;
transform: translateY(30px);
animation: fadeUp 0.8s ease forwards;
}
.delay-100 { animation-delay: 0.1s; }
.delay-200 { animation-delay: 0.2s; }
@keyframes fadeUp {
to {
opacity: 1;
transform: translateY(0);
}
}
/* Social */
.social-links a {
transition: transform 0.2s;
display: inline-block;
}
.social-links a:hover {
transform: scale(1.2) rotate(10deg);
color: var(--color-accent) !important;
}
/* Responsive */
@media (max-width: 991px) {
.rotate-divider {
transform: rotate(0);
margin-top: 0;
margin-bottom: 2rem;
}
.hero-section {
padding-top: 120px;
text-align: center;
min-height: auto;
padding-bottom: 100px;
}
.display-1 { font-size: 3.5rem; }
.blob-1 { width: 300px; height: 300px; right: -20%; }
.blob-2 { width: 300px; height: 300px; left: -20%; }
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

View File

@ -1,89 +0,0 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
header('Content-Type: application/json');
$action = $_GET['action'] ?? '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$pdo = db();
if ($action === 'register') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
if (empty($username) || empty($password)) {
echo json_encode(['code' => 1, 'msg' => '用户名和密码不能为空']);
exit;
}
if ($password !== $confirm_password) {
echo json_encode(['code' => 1, 'msg' => '两次输入的密码不一致']);
exit;
}
// Check if user exists
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
$stmt->execute([$username]);
if ($stmt->fetch()) {
echo json_encode(['code' => 1, 'msg' => '用户名已存在']);
exit;
}
$hash = password_hash($password, PASSWORD_DEFAULT);
// Check if this is the first user
$stmt = $pdo->query("SELECT COUNT(*) FROM users");
$count = $stmt->fetchColumn();
$role = ($count == 0) ? 'admin' : 'user';
$stmt = $pdo->prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)");
$stmt->execute([$username, $hash, $role]);
// Auto login after registration
$userId = $pdo->lastInsertId();
$_SESSION['user_id'] = $userId;
$_SESSION['username'] = $username;
$_SESSION['role'] = $role;
echo json_encode(['code' => 0, 'msg' => '注册成功']);
exit;
} elseif ($action === 'login') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
echo json_encode(['code' => 1, 'msg' => '用户名和密码不能为空']);
exit;
}
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
echo json_encode(['code' => 0, 'msg' => '登录成功']);
exit;
} else {
echo json_encode(['code' => 1, 'msg' => '用户名或密码错误']);
exit;
}
}
} catch (Exception $e) {
echo json_encode(['code' => 1, 'msg' => '服务器错误: ' . $e->getMessage()]);
exit;
}
}
if ($action === 'logout') {
session_destroy();
header('Location: index.php');
exit;
}
echo json_encode(['code' => 1, 'msg' => '无效的请求']);

View File

@ -1,689 +0,0 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once __DIR__ . '/db/config.php';
$pdo = db();
$stmt = $pdo->prepare("SELECT username, balance FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
$settings = $pdo->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
$notice_text = $settings['notice_text'] ?? '欢迎使用全球接码平台!';
?>
<!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">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-gradient: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
--secondary-gradient: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
--bg-body: #f1f5f9;
--surface: #ffffff;
--text-main: #1e293b;
--text-muted: #64748b;
--border-color: #e2e8f0;
--sidebar-width: 280px;
--radius-xl: 24px;
--radius-lg: 16px;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: var(--bg-body);
color: var(--text-main);
font-size: 14px;
letter-spacing: -0.01em;
}
.main-content {
margin-left: var(--sidebar-width);
padding: 2.5rem;
min-height: 100vh;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
}
.page-title {
font-size: 1.5rem;
font-weight: 800;
color: var(--text-main);
margin-bottom: 0;
}
.balance-card {
background: #fff;
padding: 10px 10px 10px 20px;
border-radius: 100px;
border: 1px solid var(--border-color);
display: flex;
align-items: center;
gap: 15px;
box-shadow: var(--shadow-sm);
}
.notice-banner {
background: #fff;
border-left: 4px solid #f59e0b;
border-radius: var(--radius-lg);
padding: 1.25rem 1.5rem;
margin-bottom: 2rem;
box-shadow: var(--shadow-sm);
display: flex;
align-items: center;
gap: 1rem;
}
.action-card {
background: var(--surface);
border-radius: var(--radius-xl);
padding: 2.5rem;
margin-bottom: 2rem;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
position: relative;
}
.search-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2.5rem;
}
.custom-dropdown {
position: relative;
}
.custom-select-trigger {
background: #f8fafc;
border: 1.5px solid #e2e8f0;
border-radius: var(--radius-lg);
padding: 0 1.25rem;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
}
.custom-select-trigger:hover {
border-color: var(--primary);
background: #fff;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
}
.custom-select-trigger .val { font-weight: 600; color: var(--text-main); font-size: 15px; }
.custom-select-trigger .placeholder { color: var(--text-muted); font-weight: 500; }
.dropdown-menu-custom {
position: absolute;
top: 100%; left: 0; right: 0;
margin-top: 10px;
background: white;
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
z-index: 1100;
display: none;
max-height: 400px;
overflow-y: auto;
animation: dropdownFade 0.2s ease-out;
}
.dropdown-menu-custom.show { display: block !important; }
@keyframes dropdownFade { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
.list-item {
padding: 12px 20px;
border-bottom: 1px solid #f1f5f9;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: space-between;
}
.list-item:hover { background: #f1f5f9; color: var(--primary); }
.list-item:last-child { border-bottom: none; }
.quotation-item {
background: #fff;
padding: 1.5rem 2rem;
border-bottom: 1px solid #f1f5f9;
display: flex;
align-items: center;
transition: all 0.2s;
}
.quotation-item:hover { background: #f8fafc; }
.quotation-item:last-child { border-bottom: none; border-bottom-left-radius: var(--radius-lg); border-bottom-right-radius: var(--radius-lg); }
.quotation-item:first-child { border-top-left-radius: var(--radius-lg); border-top-right-radius: var(--radius-lg); }
.btn-get {
background: var(--primary-gradient);
color: white;
border: none;
padding: 12px 28px;
border-radius: 14px;
font-weight: 700;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
}
.btn-get:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(37, 99, 235, 0.3);
filter: brightness(1.1);
}
.active-tasks-area {
background: #fff;
border-radius: var(--radius-xl);
border: 1px solid var(--border-color);
margin-bottom: 2rem;
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.active-tasks-header {
padding: 1.25rem 1.5rem;
background: #f8fafc;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.sms-badge {
background: #f0fdf4;
color: #166534;
padding: 12px 24px;
border-radius: 12px;
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-weight: 800;
font-size: 1.5rem;
border: 2px dashed #bbf7d0;
display: inline-block;
}
.search-input-wrap {
padding: 1rem;
background: #fff;
position: sticky;
top: 0;
z-index: 10;
border-bottom: 1px solid #f1f5f9;
}
.search-input-wrap input {
border-radius: 12px;
border: 1.5px solid #e2e8f0;
padding: 10px 15px;
}
.search-input-wrap input:focus {
box-shadow: none;
border-color: var(--primary);
}
@media (max-width: 992px) {
.main-content { margin-left: 0; padding: 1.5rem; }
.search-grid { grid-template-columns: 1fr; gap: 1rem; }
.sidebar { display: none; }
}
</style>
</head>
<body>
<?php include 'includes/sidebar.php'; ?>
<div class="main-content">
<div class="page-header">
<h1 class="page-title">购买号码 <span class="text-muted ms-2 fw-medium fs-6">GET NUMBER</span></h1>
<div class="balance-card">
<div class="text-end">
<div class="small text-muted fw-bold" style="font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;">ACCOUNT BALANCE</div>
<div class="fw-bold fs-5 text-primary" id="userBalance">$<?= number_format((float)($user['balance'] ?? 0), 2) ?></div>
</div>
<a href="recharge.php" class="btn btn-primary rounded-circle d-flex align-items-center justify-content-center" style="width: 44px; height: 44px; box-shadow: 0 4px 10px rgba(59, 130, 246, 0.3);">
<i class="fas fa-plus"></i>
</a>
</div>
</div>
<div class="notice-banner">
<div class="bg-warning bg-opacity-10 text-warning rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
<i class="fas fa-volume-up"></i>
</div>
<div class="fw-semibold text-dark-emphasis small">
<?= htmlspecialchars($notice_text) ?>
</div>
</div>
<!-- Active Tasks -->
<div class="active-tasks-area" id="activeTasksSection" style="display: none;">
<div class="active-tasks-header">
<span class="fw-bold text-primary"><i class="fas fa-satellite-dish me-2"></i> 活跃任务 / ACTIVE TASKS</span>
<button class="btn btn-link btn-sm text-decoration-none fw-bold" onclick="loadActiveOrders()" style="color: #64748b;">
<i class="fas fa-sync-alt me-1"></i> 刷新
</button>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<tbody id="activeTasksBody"></tbody>
</table>
</div>
</div>
<div class="action-card">
<div class="search-grid">
<div class="custom-dropdown" id="countryContainer">
<label class="form-label small fw-bold text-muted mb-2 px-1">STEP 1. 选择国家/地区</label>
<div class="custom-select-trigger" onclick="toggleDropdown('countriesDropdown', event)">
<span id="countryLabel" class="placeholder">搜索或选择国家...</span>
<i class="fas fa-search text-muted opacity-50"></i>
</div>
<div id="countriesDropdown" class="dropdown-menu-custom">
<div class="search-input-wrap">
<input type="text" id="countrySearch" class="form-control" placeholder="输入国家名称..." oninput="filterCountries()">
</div>
<div id="countriesList">
<div class="p-4 text-center text-muted small"><i class="fas fa-circle-notch fa-spin me-2"></i>正在加载国家列表...</div>
</div>
</div>
</div>
<div class="custom-dropdown" id="serviceContainer">
<label class="form-label small fw-bold text-muted mb-2 px-1">STEP 2. 选择服务项目</label>
<div class="custom-select-trigger" onclick="toggleDropdown('servicesDropdown', event)">
<span id="serviceLabel" class="placeholder">搜索社交平台项目...</span>
<i class="fas fa-search text-muted opacity-50"></i>
</div>
<div id="servicesDropdown" class="dropdown-menu-custom">
<div class="search-input-wrap">
<input type="text" id="serviceSearch" class="form-control" placeholder="如: Telegram, WhatsApp..." oninput="handleServiceInput()">
</div>
<div id="servicesList"></div>
</div>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-3 px-1">
<h6 class="fw-bold mb-0" style="color: #475569;">实时报价列表 / QUOTATIONS</h6>
<span class="badge bg-light text-muted fw-normal" id="lastUpdated">READY</span>
</div>
<div class="quotation-wrapper border rounded-4 overflow-hidden" style="border-color: #e2e8f0 !important;">
<div id="quotationBody">
<div class="text-center py-5">
<div class="mb-3 opacity-10">
<i class="fas fa-hand-pointer fa-4x"></i>
</div>
<p class="text-muted fw-bold">请先选择上方的国家和项目</p>
</div>
</div>
</div>
</div>
</div>
<!-- Success Modal -->
<div class="modal fade" id="smsModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-5 overflow-hidden">
<div class="modal-body text-center p-5">
<div class="mb-4">
<div class="bg-success bg-opacity-10 text-success rounded-circle d-inline-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">
<i class="fas fa-check-circle fa-3x"></i>
</div>
</div>
<h3 class="fw-bold mb-2">验证码已送达!</h3>
<p class="text-muted mb-4">内容已自动复制到您的剪贴板</p>
<div class="sms-badge mb-4" id="modalSmsCode">------</div>
<button class="btn btn-primary btn-lg w-100 py-3 rounded-4 fw-bold shadow-lg" data-bs-dismiss="modal">确认接收</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const apiHandler = 'ajax_handler.php';
let allCountries = [];
let popularServices = [{name:'Telegram'}, {name:'WhatsApp'}, {name:'TikTok'}, {name:'Google'}, {name:'OpenAI'}, {name:'Facebook'}, {name:'Twitter'}];
let currentCountry = null;
let currentService = null;
let activePolls = {};
let activeTimers = {};
let searchTimeout = null;
document.addEventListener('DOMContentLoaded', () => {
loadCountries();
renderServices(popularServices);
loadActiveOrders();
setInterval(loadActiveOrders, 30000);
document.addEventListener('click', (e) => {
if (!e.target.closest('.custom-dropdown')) hideAllDropdowns();
});
});
async function loadCountries() {
const listContainer = document.getElementById('countriesList');
try {
const res = await fetch(`${apiHandler}?action=get_countries`);
const data = await res.json();
if (data.code === 0) {
allCountries = Array.isArray(data.data) ? data.data : [];
renderCountries();
} else if (data.code === 401) {
window.location.href = 'index.php';
} else {
listContainer.innerHTML = `<div class="p-4 text-center text-danger small">加载失败: ${data.msg || '未知API错误'}</div>`;
}
} catch (e) {
listContainer.innerHTML = '<div class="p-4 text-center text-danger small">网络连接超时,请刷新页面</div>';
}
}
function toggleDropdown(id, event) {
if (event) event.stopPropagation();
const d = document.getElementById(id);
const isShow = d.classList.contains('show');
hideAllDropdowns();
if (!isShow) {
d.classList.add('show');
const input = d.querySelector('input');
if (input) setTimeout(() => input.focus(), 50);
}
}
function hideAllDropdowns() {
document.querySelectorAll('.dropdown-menu-custom').forEach(d => d.classList.remove('show'));
}
function renderCountries(filter = '') {
const container = document.getElementById('countriesList');
if (!container) return;
container.innerHTML = '';
if (!Array.isArray(allCountries) || allCountries.length === 0) {
container.innerHTML = '<div class="p-3 text-center text-muted small">暂无可用国家数据</div>';
return;
}
const filtered = filter ? allCountries.filter(c =>
(c.name_zh && c.name_zh.includes(filter)) ||
(c.name_en && c.name_en.toLowerCase().includes(filter.toLowerCase()))
) : allCountries;
if (filtered.length === 0) {
container.innerHTML = '<div class="p-3 text-center text-muted small">未找到匹配的国家</div>';
return;
}
filtered.slice(0, 100).forEach(c => {
const div = document.createElement('div');
div.className = 'list-item';
div.innerHTML = `<div><span class="fw-bold">${c.name_zh || '未知'}</span><span class="text-muted ms-2 small">${c.name_en || ''}</span></div><i class="fas fa-chevron-right small opacity-25"></i>`;
div.onclick = (e) => { e.stopPropagation(); selectCountry(c); };
container.appendChild(div);
});
}
function renderServices(services) {
const container = document.getElementById('servicesList');
if (!container) return;
container.innerHTML = '';
if (!Array.isArray(services) || services.length === 0) {
container.innerHTML = '<div class="p-3 text-center text-muted small">暂无搜索结果</div>';
return;
}
services.forEach(s => {
const div = document.createElement('div');
div.className = 'list-item';
div.innerHTML = `<span class="fw-bold">${s.name}</span><i class="fas fa-star small text-warning opacity-75"></i>`;
div.onclick = (e) => { e.stopPropagation(); selectService(s); };
container.appendChild(div);
});
}
function filterCountries() { renderCountries(document.getElementById('countrySearch').value); }
function handleServiceInput() {
const q = document.getElementById('serviceSearch').value;
const listContainer = document.getElementById('servicesList');
if (searchTimeout) clearTimeout(searchTimeout);
if (!q) { renderServices(popularServices); return; }
searchTimeout = setTimeout(async () => {
try {
const res = await fetch(`${apiHandler}?action=get_services&service=${encodeURIComponent(q)}`);
const data = await res.json();
if (data.code === 0) {
const unique = [];
const map = new Map();
const services = Array.isArray(data.data) ? data.data : [];
services.forEach(i => {
if(i.service_name && !map.has(i.service_name)){
map.set(i.service_name, true);
unique.push({name: i.service_name});
}
});
renderServices(unique);
} else if (data.code === 401) {
window.location.href = 'index.php';
}
} catch (e) {
console.error("Search error", e);
}
}, 400);
}
function selectCountry(c) {
currentCountry = c;
const l = document.getElementById('countryLabel');
l.textContent = c.name_zh;
l.classList.remove('placeholder');
l.classList.add('val');
hideAllDropdowns();
loadQuotation();
}
function selectService(s) {
currentService = s;
const l = document.getElementById('serviceLabel');
l.textContent = s.name;
l.classList.remove('placeholder');
l.classList.add('val');
hideAllDropdowns();
loadQuotation();
}
async function loadQuotation() {
const body = document.getElementById('quotationBody');
body.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary" style="width: 2rem; height: 2rem;"></div><div class="mt-2 text-muted small">正在调取实时行情...</div></div>';
const cP = currentCountry ? encodeURIComponent(currentCountry.name_zh) : '';
const sP = currentService ? encodeURIComponent(currentService.name) : '';
try {
const res = await fetch(`${apiHandler}?action=get_services&country=${cP}&service=${sP}`);
const data = await res.json();
if (data.code === 0) {
body.innerHTML = '';
const services = Array.isArray(data.data) ? data.data : [];
if (!services.length) {
body.innerHTML = '<div class="p-5 text-center text-muted"><i class="fas fa-exclamation-circle fa-2x mb-3 opacity-25"></i><div>该地区暂无此服务,请尝试其他国家或项目</div></div>';
return;
}
services.forEach(s => {
const item = document.createElement('div');
item.className = 'quotation-item';
const isPop = popularServices.some(ps => ps.name === s.service_name);
item.innerHTML = `
<div class="flex-grow-1">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="fw-bold fs-5">${s.service_name}</span>
${isPop ? '<span class="badge bg-primary bg-opacity-10 text-primary small" style="font-size: 10px; padding: 4px 8px;">POPULAR</span>' : ''}
</div>
<div class="small text-muted"><i class="fas fa-globe-asia me-1 opacity-50"></i> ${s.country_name_zh || (currentCountry ? currentCountry.name_zh : '全球')}</div>
</div>
<div class="text-end me-5">
<div class="small text-muted fw-bold" style="font-size: 10px; letter-spacing: 0.5px;">PRICE</div>
<div class="fw-bold text-dark fs-5">$${s.cost}</div>
</div>
<div>
<button class="btn-get" onclick="getNumber('${s.service_id}', '${s.service_name}', ${s.cost}, this)">获取号码</button>
</div>
`;
body.appendChild(item);
});
document.getElementById('lastUpdated').textContent = 'UPDATED: ' + new Date().toLocaleTimeString();
} else if (data.code === 401) {
window.location.href = 'index.php';
} else {
body.innerHTML = `<div class="p-5 text-center text-danger">加载行情失败: ${data.msg || '未知接口错误'}</div>`;
}
} catch (e) { body.innerHTML = '<div class="p-5 text-center text-danger">行情数据连接失败,请检查网络</div>'; }
}
async function getNumber(sid, sname, price, btn) {
if (!confirm(`确认扣费 $${price} 购买 ${sname} 号码?`)) return;
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i>';
try {
const cname = currentCountry ? currentCountry.name_zh : '全球';
const res = await fetch(`${apiHandler}?action=get_number&service_id=${sid}&service_name=${encodeURIComponent(sname)}&country_name=${encodeURIComponent(cname)}&price=${price}`);
const data = await res.json();
if (data.code === 0) {
loadActiveOrders(); updateBalance(); window.scrollTo({top: 0, behavior: 'smooth'});
} else if (data.code === 401) {
window.location.href = 'index.php';
} else {
alert(data.msg || '库存不足或接口超时');
}
} catch (e) {
alert('获取号码失败,请重试');
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
async function updateBalance() {
try {
const res = await fetch(`${apiHandler}?action=get_balance`);
const data = await res.json();
if (data.code === 0) document.getElementById('userBalance').textContent = '$' + data.balance;
} catch (e) {}
}
async function loadActiveOrders() {
try {
const res = await fetch(`${apiHandler}?action=get_active_orders`);
const data = await res.json();
const body = document.getElementById('activeTasksBody');
const section = document.getElementById('activeTasksSection');
Object.values(activeTimers).forEach(t => clearInterval(t));
activeTimers = {};
if (data.code === 0 && Array.isArray(data.data) && data.data.length > 0) {
section.style.display = 'block';
body.innerHTML = '';
data.data.forEach(o => {
const exp = new Date(o.expire_at.replace(/-/g, "/")).getTime();
let tl = Math.floor((exp - new Date().getTime())/1000);
const row = document.createElement('tr');
row.innerHTML = `
<td class="ps-4 py-4">
<div class="fw-bold text-dark">${o.service_name}</div>
<div class="small text-muted">${o.country_name}</div>
</td>
<td class="fw-bold text-primary fs-5" style="letter-spacing: 1px;">${o.number}</td>
<td id="sms-${o.request_id}">
${o.status === 'received' ? `<span class="sms-badge">${o.sms_content}</span>` : `
<div class="d-flex align-items-center gap-3 text-primary">
<div class="spinner-grow spinner-grow-sm" style="animation-duration: 1.5s;"></div>
<span class="fw-bold small" style="letter-spacing: 0.5px;">等待验证码...</span>
</div>`}
</td>
<td><span class="badge bg-light text-dark border p-2 px-3 fw-bold" id="timer-${o.request_id}">${formatTime(tl)}</span></td>
<td class="text-end pe-4">
<button class="btn btn-sm btn-outline-danger fw-bold px-3 py-2 rounded-3" onclick="releaseNumber('${o.request_id}')">释放号码</button>
</td>
`;
body.appendChild(row);
if (o.status !== 'received') { startPolling(o.request_id); startTimer(o.request_id, tl); }
});
} else { section.style.display = 'none'; }
} catch (e) {}
}
function startPolling(rid) {
if (activePolls[rid]) return;
activePolls[rid] = setInterval(async () => {
try {
const res = await fetch(`${apiHandler}?action=check_sms&request_id=${rid}`);
const data = await res.json();
if (data.code === 0 && (data.msg === 'success' || data.sms_code)) {
const el = document.getElementById(`sms-${rid}`);
if (el) el.innerHTML = `<span class="sms-badge">${data.sms_code}</span>`;
clearInterval(activePolls[rid]); delete activePolls[rid]; showSmsModal(data.sms_code);
} else if (data.code === 400 || (data.code !== 0 && data.code !== 500)) {
clearInterval(activePolls[rid]); delete activePolls[rid]; loadActiveOrders();
}
} catch (e) {}
}, 5000);
}
function startTimer(id, s) {
activeTimers[id] = setInterval(() => {
s--;
const el = document.getElementById('timer-' + id);
if (s <= 0) { clearInterval(activeTimers[id]); loadActiveOrders(); }
else if (el) el.textContent = formatTime(s);
}, 1000);
}
async function releaseNumber(id) {
if (!confirm('确定释放此号码?如果是已产生费用的任务,释放可能不会退费。')) return;
try {
const res = await fetch(`${apiHandler}?action=release_number&request_id=${id}`);
const data = await res.json();
if (data.code === 0) { loadActiveOrders(); updateBalance(); } else { alert(data.msg); }
} catch (e) { alert('连接服务器失败'); }
}
function formatTime(s) {
if (s <= 0) return "00:00";
const m = Math.floor(s/60), sec = s%60;
return `${m.toString().padStart(2,'0')}:${sec.toString().padStart(2,'0')}`;
}
function showSmsModal(code) {
document.getElementById('modalSmsCode').textContent = code;
new bootstrap.Modal(document.getElementById('smsModal')).show();
if(navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code).catch(e => {});
}
}
</script>
</body>
</html>

View File

@ -1,64 +0,0 @@
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
balance DECIMAL(10, 2) DEFAULT 0.00,
role ENUM('user', 'admin') DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS recharges (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
txid VARCHAR(255),
status ENUM('pending', 'completed', 'rejected') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS sms_orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
request_id VARCHAR(50) NOT NULL,
number VARCHAR(20) NOT NULL,
service_name VARCHAR(50),
country_name VARCHAR(50),
cost DECIMAL(10, 2),
sms_content TEXT,
status ENUM('pending', 'received', 'canceled', 'expired') DEFAULT 'pending',
expire_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS settings (
setting_key VARCHAR(50) PRIMARY KEY,
setting_value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS support_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
sender ENUM('user', 'admin') NOT NULL,
message TEXT NOT NULL,
is_read TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Default Settings
INSERT INTO settings (setting_key, setting_value) VALUES
('site_name', '全球接码'),
('site_logo', 'assets/pasted-20260210-082628-83f66727.png'),
('notice_text', '欢迎使用全球专业接码平台!本平台支持全球数百个国家和地区的短信验证码接收服务。'),
('usdt_trc20_address', 'TEm1B...TRC20_ADDRESS_HERE'),
('usdt_erc20_address', '0x71C...ERC20_ADDRESS_HERE'),
('lubansms_apikey', '')
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value);
-- Default Admin (admin / admin123)
INSERT INTO users (username, password_hash, role) VALUES
('admin', '$2y$10$QbKYSCqJI0WQLyf6NNSML.ukYrOZ0MdY61ZpK7Ekn5QQ/A9oDr.hu', 'admin')
ON DUPLICATE KEY UPDATE role = 'admin';

View File

@ -1,191 +0,0 @@
<?php
$current_page = basename($_SERVER['PHP_SELF']);
require_once __DIR__ . '/../db/config.php';
$pdo_sidebar = db();
$settings_sidebar = $pdo_sidebar->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
$site_name = $settings_sidebar['site_name'] ?? '全球接码';
$site_logo = $settings_sidebar['site_logo'] ?? 'assets/pasted-20260210-082628-83f66727.png';
?>
<style>
.sidebar {
width: 280px;
height: 100vh;
position: fixed;
left: 0;
top: 0;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-right: 1px solid rgba(0, 0, 0, 0.05);
z-index: 1000;
padding: 2rem 1.2rem;
display: flex;
flex-direction: column;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar-brand {
padding: 0.5rem 1rem;
margin-bottom: 2.5rem;
display: flex;
align-items: center;
gap: 15px;
text-decoration: none;
}
.sidebar-brand img {
width: 42px;
height: 42px;
border-radius: 12px;
object-fit: cover;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.sidebar-brand span {
font-size: 1.25rem;
font-weight: 800;
background: linear-gradient(135deg, #1a1a1a 0%, #4a4a4a 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.5px;
}
.sidebar .nav-link {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 18px;
color: #64748b;
text-decoration: none;
font-weight: 600;
border-radius: 14px;
margin-bottom: 6px;
transition: all 0.2s ease;
position: relative;
}
.sidebar .nav-link i {
font-size: 1.2rem;
width: 24px;
text-align: center;
transition: transform 0.2s ease;
}
.sidebar .nav-link:hover {
background-color: #f8fafc;
color: #0f172a;
transform: translateX(4px);
}
.sidebar .nav-link:hover i {
transform: scale(1.1);
}
.sidebar .nav-link.active {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: #ffffff;
box-shadow: 0 4px 15px rgba(37, 99, 235, 0.25);
}
.sidebar .nav-link.active i {
color: #ffffff;
}
.user-profile-mini {
background: #f8fafc;
border-radius: 16px;
padding: 16px;
margin-top: auto;
margin-bottom: 1.5rem;
border: 1px solid #f1f5f9;
}
.logout-link {
color: #ef4444 !important;
}
.logout-link:hover {
background-color: #fef2f2 !important;
}
.badge-notif {
position: absolute;
top: 10px;
right: 15px;
background: #ef4444;
color: white;
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 2px white;
}
@media (max-width: 992px) {
.sidebar { width: 85px; padding: 2rem 0.8rem; }
.sidebar-brand span, .sidebar .nav-link span, .user-profile-mini { display: none; }
.sidebar-brand { margin-bottom: 2rem; padding: 0; justify-content: center; }
.sidebar .nav-link { justify-content: center; padding: 15px; margin-bottom: 10px; }
.badge-notif { top: 5px; right: 5px; }
}
</style>
<div class="sidebar">
<a href="dashboard.php" class="sidebar-brand">
<img src="<?= htmlspecialchars($site_logo) ?>?v=<?= time() ?>" alt="Logo">
<span><?= htmlspecialchars($site_name) ?></span>
</a>
<nav class="flex-grow-1">
<a href="dashboard.php" class="nav-link <?= $current_page === 'dashboard.php' ? 'active' : '' ?>">
<i class="fas fa-th-large"></i>
<span>工作台</span>
</a>
<a href="orders.php" class="nav-link <?= $current_page === 'orders.php' ? 'active' : '' ?>">
<i class="fas fa-receipt"></i>
<span>接码记录</span>
</a>
<a href="recharge.php" class="nav-link <?= $current_page === 'recharge.php' ? 'active' : '' ?>">
<i class="fas fa-wallet"></i>
<span>充值中心</span>
</a>
<a href="support.php" class="nav-link <?= $current_page === 'support.php' ? 'active' : '' ?>">
<i class="fas fa-headset"></i>
<span>联系客服</span>
<span id="userSupportBadge" class="badge-notif" style="display:none;">0</span>
</a>
</nav>
<div class="mt-auto">
<?php if (isset($user['username'])): ?>
<div class="user-profile-mini">
<div class="d-flex align-items-center gap-3">
<div class="rounded-circle bg-primary d-flex align-items-center justify-content-center text-white fw-bold" style="width: 32px; height: 32px; font-size: 0.8rem;">
<?= strtoupper(substr($user['username'], 0, 1)) ?>
</div>
<div class="overflow-hidden">
<div class="small text-muted fw-bold">Hi, <?= htmlspecialchars($user['username']) ?></div>
<div class="small fw-bold text-primary">$<?= number_format($user['balance'] ?? 0, 2) ?></div>
</div>
</div>
</div>
<?php endif; ?>
<a href="auth.php?action=logout" class="nav-link logout-link">
<i class="fas fa-sign-out-alt"></i>
<span>退出登录</span>
</a>
</div>
</div>
<script>
async function checkUserNotifications() {
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('userSupportBadge');
if (badge) {
if (count > 0) {
badge.textContent = count;
badge.style.display = 'flex';
} else {
badge.style.display = 'none';
}
}
}
} catch(e) {}
}
setInterval(checkUserNotifications, 5000);
checkUserNotifications();
</script>

422
index.php
View File

@ -1,286 +1,150 @@
<?php <?php
session_start(); declare(strict_types=1);
if (isset($_SESSION['user_id'])) { @ini_set('display_errors', '1');
header('Location: dashboard.php'); @error_reporting(E_ALL);
exit; @date_default_timezone_set('UTC');
}
require_once __DIR__ . '/db/config.php'; $phpVersion = PHP_VERSION;
$pdo = db(); $now = date('Y-m-d H:i:s');
$settings = $pdo->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
$site_name = $settings['site_name'] ?? '全球接码';
$site_logo = $settings['site_logo'] ?? 'assets/pasted-20260210-082628-83f66727.png';
?> ?>
<!DOCTYPE html> <!doctype html>
<html lang="zh-CN"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= htmlspecialchars($site_name) ?> - 全球专业接码平台</title> <title>New Style</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <?php
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> // Read project preview data from environment
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet"> $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
<style> $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
:root { ?>
--primary: #3b82f6; <?php if ($projectDescription): ?>
--primary-gradient: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); <!-- Meta description -->
--bg-body: #f8fafc; <meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
--text-main: #1e293b; <!-- Open Graph meta tags -->
} <meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
body { <!-- Twitter meta tags -->
font-family: 'Plus Jakarta Sans', sans-serif; <meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
background: #f8fafc; <?php endif; ?>
color: var(--text-main); <?php if ($projectImageUrl): ?>
overflow-x: hidden; <!-- Open Graph image -->
letter-spacing: -0.01em; <meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
} <!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
.hero-section { <?php endif; ?>
min-height: 100vh; <link rel="preconnect" href="https://fonts.googleapis.com">
display: flex; <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
align-items: center; <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
background: radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.05) 0%, rgba(59, 130, 246, 0) 50%); <style>
position: relative; :root {
} --bg-color-start: #6a11cb;
.hero-section::after { --bg-color-end: #2575fc;
content: ''; --text-color: #ffffff;
position: absolute; --card-bg-color: rgba(255, 255, 255, 0.01);
top: 0; right: 0; --card-border-color: rgba(255, 255, 255, 0.1);
width: 40%; height: 100%; }
background: linear-gradient(135deg, rgba(59, 130, 246, 0.03) 0%, rgba(59, 130, 246, 0) 100%); body {
clip-path: polygon(25% 0%, 100% 0%, 100% 100%, 0% 100%); margin: 0;
z-index: -1; font-family: 'Inter', sans-serif;
} background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
.navbar-brand img { height: 40px; } display: flex;
justify-content: center;
.hero-title { align-items: center;
font-size: 4rem; min-height: 100vh;
font-weight: 800; text-align: center;
line-height: 1.1; overflow: hidden;
margin-bottom: 1.5rem; position: relative;
letter-spacing: -2px; }
} body::before {
.hero-subtitle { content: '';
font-size: 1.25rem; position: absolute;
color: #64748b; top: 0;
margin-bottom: 2.5rem; left: 0;
max-width: 600px; width: 100%;
} height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
.btn-main { animation: bg-pan 20s linear infinite;
padding: 18px 40px; z-index: -1;
font-weight: 800; }
border-radius: 18px; @keyframes bg-pan {
transition: all 0.3s; 0% { background-position: 0% 0%; }
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.2); 100% { background-position: 100% 100%; }
} }
.btn-main:hover { transform: translateY(-3px); box-shadow: 0 12px 30px rgba(59, 130, 246, 0.3); } main {
padding: 2rem;
.form-card { }
background: rgba(255, 255, 255, 0.8); .card {
backdrop-filter: blur(20px); background: var(--card-bg-color);
border: 1px solid rgba(255, 255, 255, 0.4); border: 1px solid var(--card-border-color);
border-radius: 32px; border-radius: 16px;
padding: 3rem; padding: 2rem;
box-shadow: 0 40px 80px rgba(15, 23, 42, 0.05); backdrop-filter: blur(20px);
} -webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
.input-group-custom { }
margin-bottom: 1.5rem; .loader {
} margin: 1.25rem auto 1.25rem;
.input-group-custom label { width: 48px;
font-weight: 700; height: 48px;
font-size: 13px; border: 3px solid rgba(255, 255, 255, 0.25);
color: #475569; border-top-color: #fff;
margin-bottom: 8px; border-radius: 50%;
display: block; animation: spin 1s linear infinite;
padding-left: 4px; }
} @keyframes spin {
.input-group-custom .form-control { from { transform: rotate(0deg); }
border: 1.5px solid #e2e8f0; to { transform: rotate(360deg); }
border-radius: 16px; }
padding: 14px 20px; .hint {
background: #fff; opacity: 0.9;
font-weight: 600; }
transition: all 0.2s; .sr-only {
} position: absolute;
.input-group-custom .form-control:focus { width: 1px; height: 1px;
border-color: var(--primary); padding: 0; margin: -1px;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.08); overflow: hidden;
} clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
.stat-badge { }
display: inline-flex; h1 {
align-items: center; font-size: 3rem;
gap: 8px; font-weight: 700;
background: #fff; margin: 0 0 1rem;
padding: 10px 20px; letter-spacing: -1px;
border-radius: 100px; }
font-weight: 800; p {
font-size: 12px; margin: 0.5rem 0;
box-shadow: 0 4px 12px rgba(0,0,0,0.03); font-size: 1.1rem;
margin-bottom: 2rem; }
color: var(--primary); code {
} background: rgba(0,0,0,0.2);
padding: 2px 6px;
.floating-shape { border-radius: 4px;
position: absolute; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
z-index: -1; }
opacity: 0.1; footer {
filter: blur(2px); position: absolute;
animation: float 6s ease-in-out infinite; bottom: 1rem;
} font-size: 0.8rem;
@keyframes float { opacity: 0.7;
0% { transform: translateY(0px); } }
50% { transform: translateY(-20px); } </style>
100% { transform: translateY(0px); }
}
@media (max-width: 992px) {
.hero-title { font-size: 3rem; }
.hero-section { text-align: center; padding-top: 100px; padding-bottom: 100px; }
.hero-subtitle { margin-left: auto; margin-right: auto; }
.form-card { margin-top: 3rem; }
}
</style>
</head> </head>
<body> <body>
<main>
<nav class="navbar navbar-expand-lg fixed-top py-4"> <div class="card">
<div class="container"> <h1>Analyzing your requirements and generating your website…</h1>
<a class="navbar-brand fw-bold d-flex align-items-center gap-2" href="#"> <div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<img src="<?= htmlspecialchars($site_logo) ?>" alt="Logo"> <span class="sr-only">Loading…</span>
<span class="fs-4 fw-800" style="letter-spacing: -1px;"><?= htmlspecialchars($site_name) ?></span> </div>
</a> <p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<div class="ms-auto"> <p class="hint">This page will update automatically as the plan is implemented.</p>
<a href="support.php" class="text-muted text-decoration-none fw-bold small me-4">联系客服</a> <p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
<button class="btn btn-outline-primary fw-bold rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#loginModal">登入系统</button>
</div>
</div> </div>
</nav> </main>
<footer>
<section class="hero-section"> Page updated: <?= htmlspecialchars($now) ?> (UTC)
<div class="container"> </footer>
<div class="row align-items-center">
<div class="col-lg-6">
<div class="stat-badge">
<i class="fas fa-signal"></i> 节点状态: 全球高可用实时连接
</div>
<h1 class="hero-title">
专业接码<br><span class="text-primary">从未如此简单</span>
</h1>
<p class="hero-subtitle">
支持全球 200+ 国家和地区,对接数千个热门社交平台及服务项目。全自动 API 监听USDT 秒级入账,为您业务保驾护航。
</p>
<div class="d-flex gap-3">
<button class="btn btn-primary btn-main" data-bs-toggle="modal" data-bs-target="#registerModal">
立即开启接码之旅 <i class="fas fa-rocket ms-2"></i>
</button>
</div>
<div class="mt-5 pt-4 d-flex gap-5 opacity-50">
<div class="text-center">
<div class="h3 fw-800 mb-0">200+</div>
<div class="small fw-bold">国家地区</div>
</div>
<div class="text-center">
<div class="h3 fw-800 mb-0">5000+</div>
<div class="small fw-bold">日均项目</div>
</div>
<div class="text-center">
<div class="h3 fw-800 mb-0">0.01s</div>
<div class="small fw-bold">API 响应</div>
</div>
</div>
</div>
<div class="col-lg-5 offset-lg-1">
<div class="form-card" id="authForm">
<h3 class="fw-800 mb-2">快速加入</h3>
<p class="text-muted small mb-4">创建账号即刻开始您的自动化业务流程</p>
<form id="registerForm" onsubmit="handleAuth(event, 'register')">
<div class="input-group-custom">
<label>设置用户名</label>
<input type="text" name="username" class="form-control" placeholder="输入 4-12 位英文字符" required>
</div>
<div class="input-group-custom">
<label>登录密码</label>
<input type="password" name="password" class="form-control" placeholder="设置您的复杂密码" required>
</div>
<div class="input-group-custom">
<label>确认密码</label>
<input type="password" name="confirm_password" class="form-control" placeholder="再次确认您的密码" required>
</div>
<button type="submit" class="btn btn-primary btn-main w-100 py-3 mt-2">
注册并登入 / SIGN UP
</button>
<p class="text-center mt-4 mb-0 small text-muted fw-bold">
已有账号?<a href="javascript:void(0)" class="text-primary text-decoration-none" data-bs-toggle="modal" data-bs-target="#loginModal">立即登入</a>
</p>
</form>
</div>
</div>
</div>
</div>
</section>
<!-- Login Modal -->
<div class="modal fade" id="loginModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-5 p-4">
<div class="modal-body p-4">
<div class="text-center mb-4">
<h3 class="fw-800">欢迎回来</h3>
<p class="text-muted small">请输入您的凭据以访问控制面板</p>
</div>
<form onsubmit="handleAuth(event, 'login')">
<div class="input-group-custom">
<label>用户名</label>
<input type="text" name="username" class="form-control" placeholder="输入您的用户名" required>
</div>
<div class="input-group-custom">
<label>密码</label>
<input type="password" name="password" class="form-control" placeholder="输入登录密码" required>
</div>
<button type="submit" class="btn btn-primary btn-main w-100 py-3 mt-3">立即登录 / SIGN IN</button>
<div class="text-center mt-4">
<a href="javascript:void(0)" class="small text-muted fw-bold text-decoration-none" data-bs-toggle="modal" data-bs-target="#registerModal">没有账号?点击注册</a>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
async function handleAuth(e, type) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const btn = form.querySelector('button[type="submit"]');
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> 处理中...';
try {
const res = await fetch(`auth.php?action=${type}`, { method: 'POST', body: formData });
const data = await res.json();
if (data.code === 0) {
window.location.href = 'dashboard.php';
} else {
alert(data.msg || '操作失败');
btn.disabled = false;
btn.innerHTML = originalText;
}
} catch (error) {
alert('网络异常');
btn.disabled = false;
btn.innerHTML = originalText;
}
}
</script>
</body> </body>
</html> </html>

View File

@ -1,184 +0,0 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once __DIR__ . '/db/config.php';
$pdo = db();
$stmt = $pdo->prepare("SELECT username, balance FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
$stmt = $pdo->prepare("SELECT * FROM sms_orders WHERE user_id = ? ORDER BY created_at DESC");
$stmt->execute([$_SESSION['user_id']]);
$orders = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>接码记录 - 全球接码</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">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--primary: #3b82f6;
--bg-body: #f1f5f9;
--surface: #ffffff;
--text-main: #1e293b;
--text-muted: #64748b;
--border-color: #e2e8f0;
--sidebar-width: 280px;
--radius-xl: 24px;
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: var(--bg-body);
color: var(--text-main);
letter-spacing: -0.01em;
}
.main-content {
margin-left: var(--sidebar-width);
padding: 2.5rem;
min-height: 100vh;
}
.table-card {
background: white;
border: 1px solid var(--border-color);
border-radius: var(--radius-xl);
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.table { vertical-align: middle; border-collapse: separate; border-spacing: 0 8px; }
.table thead th {
background: #f8fafc;
border: none;
color: var(--text-muted);
font-weight: 700;
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.5px;
padding: 1rem 1.5rem;
}
.table tbody tr {
background: #fff;
transition: transform 0.2s;
}
.table tbody tr:hover { background: #f8fafc; }
.table tbody td {
padding: 1.5rem;
border-top: 1px solid #f1f5f9;
border-bottom: 1px solid #f1f5f9;
font-weight: 500;
}
.table tbody td:first-child { border-left: 1px solid #f1f5f9; border-top-left-radius: 12px; border-bottom-left-radius: 12px; }
.table tbody td:last-child { border-right: 1px solid #f1f5f9; border-top-right-radius: 12px; border-bottom-right-radius: 12px; }
.status-pill {
padding: 6px 14px;
border-radius: 100px;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-received { background: #dcfce7; color: #166534; }
.status-canceled { background: #fee2e2; color: #991b1b; }
.status-pending { background: #eff6ff; color: #1e40af; }
.sms-box {
background: #f1f5f9;
color: #334155;
padding: 8px 16px;
border-radius: 10px;
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-weight: 700;
border: 1px solid #e2e8f0;
display: inline-block;
}
@media (max-width: 992px) {
.main-content { margin-left: 0; padding: 1.5rem; }
.sidebar { display: none; }
}
</style>
</head>
<body>
<?php include 'includes/sidebar.php'; ?>
<div class="main-content">
<div class="mb-5">
<h1 class="fw-bold mb-1" style="font-size: 1.5rem;">接码记录 <span class="text-muted fw-medium ms-2 fs-6">ORDER HISTORY</span></h1>
<p class="text-muted small fw-medium mb-0">记录您账户下所有的号码获取详情与收码状态清单</p>
</div>
<div class="table-card">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>任务时间 / TIME</th>
<th>项目/国家 / DETAILS</th>
<th>号码 / NUMBER</th>
<th>短信内容 / SMS</th>
<th class="text-center">状态 / STATUS</th>
</tr>
</thead>
<tbody>
<?php foreach ($orders as $order): ?>
<tr>
<td class="small text-muted fw-bold">
<div><?= date('Y-m-d', strtotime($order['created_at'])) ?></div>
<div class="opacity-50"><?= date('H:i:s', strtotime($order['created_at'])) ?></div>
</td>
<td>
<div class="fw-bold"><?= htmlspecialchars($order['service_name']) ?></div>
<div class="small text-muted fw-medium"><?= htmlspecialchars($order['country_name']) ?></div>
</td>
<td>
<div class="fw-bold text-primary fs-5" style="letter-spacing: 0.5px;"><?= $order['number'] ?></div>
<div class="small text-muted opacity-50">REQ_ID: <?= $order['request_id'] ?></div>
</td>
<td>
<?php if ($order['sms_content']): ?>
<span class="sms-box"><?= htmlspecialchars($order['sms_content']) ?></span>
<?php else: ?>
<span class="text-muted opacity-25">PENDING..</span>
<?php endif; ?>
</td>
<td class="text-center">
<?php if ($order['status'] === 'received'): ?>
<span class="status-pill status-received">SUCCESS</span>
<?php elseif ($order['status'] === 'canceled' || $order['status'] === 'expired'): ?>
<span class="status-pill status-canceled">CLOSED</span>
<?php else: ?>
<span class="status-pill status-pending">WAITING</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($orders)): ?>
<tr>
<td colspan="5" class="text-center py-5">
<div class="opacity-10 mb-3"><i class="fas fa-history fa-4x"></i></div>
<div class="fw-bold text-muted">暂无任何历史接码记录 / NO ORDERS</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,448 +0,0 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once __DIR__ . '/db/config.php';
$pdo = db();
$stmt = $pdo->prepare("SELECT username, balance FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
$settings = $pdo->query("SELECT setting_key, setting_value FROM settings")->fetchAll(PDO::FETCH_KEY_PAIR);
$trc20_address = $settings['usdt_trc20_address'] ?? 'TEm1B...TRC20_ADDRESS_HERE';
$erc20_address = $settings['usdt_erc20_address'] ?? '0x71C...ERC20_ADDRESS_HERE';
$site_name = $settings['site_name'] ?? '全球接码';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>充值中心 - <?= htmlspecialchars($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">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js"></script>
<style>
:root {
--primary: #3b82f6;
--primary-gradient: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
--bg-body: #f1f5f9;
--surface: #ffffff;
--text-main: #1e293b;
--text-muted: #64748b;
--border-color: #e2e8f0;
--sidebar-width: 280px;
--radius-xl: 24px;
--radius-lg: 16px;
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: var(--bg-body);
color: var(--text-main);
letter-spacing: -0.01em;
}
.main-content {
margin-left: var(--sidebar-width);
padding: 2.5rem;
min-height: 100vh;
}
.card-custom {
background: white;
border: 1px solid var(--border-color);
border-radius: var(--radius-xl);
padding: 2.5rem;
margin-bottom: 2rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.network-btn {
background-color: #f8fafc;
border: 1.5px solid #e2e8f0;
color: var(--text-muted);
padding: 18px;
border-radius: var(--radius-lg);
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
font-weight: 700;
flex: 1;
}
.network-btn.active {
border-color: var(--primary);
color: var(--primary);
background-color: #eff6ff;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
}
.qr-card {
background: #fff;
padding: 24px;
border-radius: var(--radius-xl);
display: inline-block;
border: 1px solid #f1f5f9;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
}
.address-box-wrapper {
background-color: #f8fafc;
padding: 20px 24px;
border-radius: var(--radius-lg);
border: 2px dashed #cbd5e1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 15px;
}
.address-text {
word-break: break-all;
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 1.1rem;
color: #1e293b;
font-weight: 700;
}
.btn-copy {
background: var(--primary);
color: white;
border: none;
width: 48px; height: 48px;
border-radius: 14px;
display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.2s;
}
.btn-copy:hover { transform: scale(1.05); background: #2563eb; }
.btn-primary-action {
background: var(--primary-gradient);
border: none;
padding: 20px;
border-radius: var(--radius-lg);
font-weight: 800;
color: white;
width: 100%;
transition: all 0.3s;
box-shadow: 0 8px 25px rgba(37, 99, 235, 0.25);
}
.btn-primary-action:hover { transform: translateY(-2px); box-shadow: 0 12px 30px rgba(37, 99, 235, 0.35); }
.step-panel { display: none; }
.step-panel.active { display: block; animation: fadeInUp 0.4s ease-out; }
@keyframes fadeInUp { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
.scanner-circle {
width: 100px; height: 100px;
border: 3px solid var(--primary);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
margin-bottom: 24px;
position: relative;
}
.scanner-circle::after {
content: '';
position: absolute;
top: -10px; left: -10px; right: -10px; bottom: -10px;
border: 2px solid rgba(59, 130, 246, 0.2);
border-radius: 50%;
animation: ping 1.5s infinite;
}
@keyframes ping { 75%, 100% { transform: scale(1.4); opacity: 0; } }
.status-badge {
padding: 8px 16px;
border-radius: 100px;
font-weight: 700;
font-size: 12px;
display: inline-flex;
align-items: center;
gap: 8px;
}
.status-sync { background: #dcfce7; color: #166534; }
.amt-btn {
background: #fff;
border: 1.5px solid #e2e8f0;
padding: 15px;
border-radius: var(--radius-lg);
font-weight: 800;
transition: all 0.2s;
}
.amt-btn:hover { border-color: var(--primary); color: var(--primary); background: #f8fafc; }
@media (max-width: 992px) {
.main-content { margin-left: 0; padding: 1.5rem; }
.sidebar { display: none; }
}
</style>
</head>
<body>
<?php include 'includes/sidebar.php'; ?>
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-5">
<div>
<h1 class="fw-bold mb-1" style="font-size: 1.5rem;">资产充值 <span class="text-muted fw-medium ms-2 fs-6">RECHARGE CENTER</span></h1>
<p class="text-muted small mb-0 fw-medium">USDT 全自动区块监听,即时入账安全有保障</p>
</div>
<div class="text-end bg-white border p-3 px-4 rounded-4 shadow-sm d-flex align-items-center gap-3">
<div class="text-end">
<div class="small text-muted fw-bold" style="font-size: 10px; letter-spacing: 0.5px;">BALANCE</div>
<div class="h4 fw-bold text-primary mb-0">$<?= number_format($user['balance'] ?? 0, 2) ?></div>
</div>
<div class="bg-primary bg-opacity-10 text-primary rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
<i class="fas fa-wallet"></i>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-xl-8">
<div class="card-custom">
<!-- Step 1 -->
<div id="step1" class="step-panel active">
<h5 class="fw-bold mb-4" style="color: #334155;">1. 输入充值数额 / AMOUNT</h5>
<div class="p-4 bg-primary bg-opacity-10 rounded-4 mb-4" style="border-left: 4px solid var(--primary);">
<p class="small text-primary fw-bold mb-0">
<i class="fas fa-info-circle me-1"></i> 为了极速识别您的支付,系统会生成一个唯一的<strong>随机小数</strong>。请务必支付<strong>包含小数点的准确金额</strong>
</p>
</div>
<div class="mb-5 text-center py-4">
<label class="form-label fw-bold text-muted small mb-3">充值金额 (USDT)</label>
<div class="d-flex align-items-center justify-content-center gap-2">
<span class="fs-1 fw-bold text-muted opacity-25">$</span>
<input type="number" id="inputAmount" class="form-control border-0 text-center fw-bold" style="font-size: 4rem; width: 300px; color: var(--primary);" placeholder="0" min="10" step="1">
</div>
</div>
<div class="row g-3 mb-5">
<?php foreach([10, 50, 100, 200, 500, 1000] as $amt): ?>
<div class="col-4 col-md-2">
<button class="btn amt-btn w-100" onclick="setAmount(<?= $amt ?>)"><?= $amt ?></button>
</div>
<?php endforeach; ?>
</div>
<button class="btn-primary-action py-4 mt-4" onclick="confirmOrder()">
生成支付订单 / CONFIRM ORDER <i class="fas fa-chevron-right ms-2"></i>
</button>
</div>
<!-- Step 2 -->
<div id="step2" class="step-panel">
<div class="d-flex justify-content-between align-items-center mb-5 pb-4 border-bottom">
<div>
<h5 class="fw-bold mb-1">2. 完成区块链支付 / PAYMENT</h5>
<div class="status-badge status-sync" id="orderIdDisplay">正在连接节点...</div>
</div>
<div class="text-end">
<div class="h2 fw-bold text-primary mb-0" id="displayAmount">0.00</div>
<div class="small text-muted fw-bold" style="font-size: 10px;">PRECISE AMOUNT (USDT)</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-6">
<label class="form-label fw-bold text-muted mb-3 small">A. 选择支付网络 / NETWORK</label>
<div class="d-flex gap-3 mb-4">
<div class="network-btn active" id="btnTRC" onclick="selectNetwork('TRC20')">
<div class="fs-5">TRC20</div>
<div class="small opacity-50">波场 TRON</div>
</div>
<div class="network-btn" id="btnERC" onclick="selectNetwork('ERC20')">
<div class="fs-5">ERC20</div>
<div class="small opacity-50">以太坊 ETH</div>
</div>
</div>
<div class="alert alert-warning border-0 rounded-4 p-4" style="background: #fffbeb;">
<h6 class="fw-bold mb-2 small" style="color: #92400e;"><i class="fas fa-exclamation-triangle me-2"></i> 核心支付准则</h6>
<ol class="small mb-0 ps-3" style="color: #92400e;">
<li class="mb-1">转账金额必须<strong>精确到小数点后两位</strong></li>
<li class="mb-1">转账网络必须选择与您选择的选项一致。</li>
<li>支付后请不要关闭此页面,等待系统自动跳转。</li>
</ol>
</div>
</div>
<div class="col-md-6 text-center">
<div class="qr-card" id="qrcode"></div>
<div class="mt-4">
<div class="badge bg-light text-primary border p-2 px-4 rounded-pill fw-bold">
<i class="fas fa-clock-rotate-left me-2"></i> 有效期: <span id="countdown">60:00</span>
</div>
</div>
</div>
</div>
<div class="mb-5">
<label class="text-muted small mb-2 d-block fw-bold px-1">B. 复制收款地址 / ADDRESS</label>
<div class="address-box-wrapper">
<div class="address-text" id="addressBox">正在加载...</div>
<button class="btn-copy" onclick="copyAddress()" title="点击复制"><i class="fas fa-copy"></i></button>
</div>
</div>
<div class="d-flex flex-column align-items-center py-4">
<div class="scanner-circle">
<i class="fas fa-satellite-dish text-primary fs-3"></i>
</div>
<h6 class="fw-bold mb-2">正在监听区块确认...</h6>
<p class="text-muted small text-center mb-5 px-5">我们正在 24/7 监听您的专属付款。一旦链上确认数达标,系统将瞬间为您入账。</p>
<div class="d-flex gap-3 w-100">
<button class="btn btn-light border flex-grow-1 py-3 rounded-4 fw-bold" style="color: #64748b;" onclick="goBackToStep1()">修改金额</button>
<button class="btn btn-primary flex-grow-1 py-3 rounded-4 fw-bold shadow-sm" onclick="checkStatusManual()">手动刷新状态</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="card-custom" style="padding: 2rem;">
<h6 class="fw-bold mb-4" style="color: #334155;"><i class="fas fa-lightbulb text-warning me-2"></i> 充值助手 / HELP</h6>
<div class="mb-4">
<div class="fw-bold mb-1 small">为什么要支付精确小数?</div>
<p class="small text-muted mb-0">金额是唯一的。精准支付后,系统可实现秒级自动识别入账。否则需要联系人工核实。</p>
</div>
<div class="mb-4">
<div class="fw-bold mb-1 small">多久到账?</div>
<p class="small text-muted mb-0">TRC20 网络通常在 1 分钟内。ERC20 视网络拥堵情况,通常在 5-10 分钟。</p>
</div>
<div class="mt-5 p-4 bg-primary rounded-4 text-white shadow-lg" style="background: var(--primary-gradient) !important;">
<h6 class="fw-bold mb-2">需要人工支持?</h6>
<p class="small opacity-75 mb-3">如果金额支付错误或长时间未到账,请立即联系在线客服。</p>
<a href="support.php" class="btn btn-white btn-sm w-100 fw-bold py-2 rounded-3" style="background: white; color: var(--primary);">发起咨询 / SUPPORT</a>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="successModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-5 overflow-hidden text-center p-5">
<div class="mb-4">
<i class="fas fa-check-circle text-success" style="font-size: 5rem;"></i>
</div>
<h3 class="fw-bold">充值确认成功!</h3>
<p class="text-muted mb-4">您的余额已成功更新。欢迎回到工作台继续接码。</p>
<button class="btn btn-primary btn-lg w-100 py-3 rounded-4 fw-bold" onclick="window.location.href='dashboard.php'">立即前往工作台</button>
</div>
</div>
</div>
<script>
const addresses = { 'TRC20': '<?= $trc20_address ?>', 'ERC20': '<?= $erc20_address ?>' };
let currentNetwork = 'TRC20';
let timeLeft = 3600;
let timerInterval, pollInterval, currentRechargeId = null;
function setAmount(val) { document.getElementById('inputAmount').value = val; }
async function confirmOrder() {
const amt = document.getElementById('inputAmount').value;
if (!amt || amt < 10) { alert('最低充值金额为 10 USDT'); return; }
const btn = event.currentTarget;
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin me-2"></i>正在连接区块链网关...';
try {
const formData = new FormData();
formData.append('amount', amt);
const res = await fetch('ajax_handler.php?action=create_recharge', { method: 'POST', body: formData });
const data = await res.json();
if (data.code === 0) {
currentRechargeId = data.recharge_id;
document.getElementById('orderIdDisplay').textContent = 'ORDER ID: #RE' + currentRechargeId;
document.getElementById('displayAmount').textContent = parseFloat(data.amount).toFixed(2);
document.getElementById('step1').classList.remove('active');
document.getElementById('step2').classList.add('active');
updateDisplay();
startTimer();
startPollingStatus();
} else { alert(data.msg); btn.disabled = false; btn.innerHTML = originalText; }
} catch (e) { btn.disabled = false; btn.innerHTML = originalText; }
}
function startPollingStatus() {
pollInterval = setInterval(async () => {
try {
const res = await fetch(`ajax_handler.php?action=check_recharge_status&recharge_id=${currentRechargeId}`);
const data = await res.json();
if (data.code === 0 && data.status === 'completed') {
clearInterval(pollInterval);
new bootstrap.Modal(document.getElementById('successModal')).show();
setTimeout(() => window.location.href = 'dashboard.php', 3000);
}
} catch (e) {}
}, 3000);
}
async function checkStatusManual() {
const btn = event.currentTarget;
const old = btn.innerHTML;
btn.disabled = true; btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i>';
try {
const res = await fetch(`ajax_handler.php?action=check_recharge_status&recharge_id=${currentRechargeId}`);
const data = await res.json();
if (data.code === 0 && data.status === 'completed') {
new bootstrap.Modal(document.getElementById('successModal')).show();
} else { setTimeout(() => { btn.disabled = false; btn.innerHTML = old; }, 1000); }
} catch (e) { btn.disabled = false; btn.innerHTML = old; }
}
function selectNetwork(net) {
currentNetwork = net;
document.getElementById('btnTRC').classList.toggle('active', net === 'TRC20');
document.getElementById('btnERC').classList.toggle('active', net === 'ERC20');
updateDisplay();
}
function updateDisplay() {
const addr = addresses[currentNetwork];
document.getElementById('addressBox').textContent = addr;
const qr = qrcode(0, 'M');
qr.addData(addr); qr.make();
document.getElementById('qrcode').innerHTML = qr.createImgTag(6, 0);
}
function copyAddress() {
const addr = document.getElementById('addressBox').textContent;
navigator.clipboard.writeText(addr).then(() => {
const btn = document.querySelector('.btn-copy');
const old = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i>';
setTimeout(() => btn.innerHTML = old, 2000);
});
}
function startTimer() {
timerInterval = setInterval(() => {
timeLeft--;
const m = Math.floor(timeLeft / 60), s = timeLeft % 60;
document.getElementById('countdown').textContent = `${m}:${s.toString().padStart(2, '0')}`;
if (timeLeft <= 0) window.location.reload();
}, 1000);
}
function goBackToStep1() {
if (!confirm('确定返回修改金额?当前订单将作废。')) return;
document.getElementById('step2').classList.remove('active');
document.getElementById('step1').classList.add('active');
clearInterval(timerInterval); clearInterval(pollInterval);
document.querySelector('#step1 button.btn-primary-action').disabled = false;
document.querySelector('#step1 button.btn-primary-action').innerHTML = '生成支付订单 / CONFIRM ORDER <i class="fas fa-chevron-right ms-2"></i>';
}
</script>
</body>
</html>

View File

@ -1,44 +0,0 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
$pdo = db();
if (isset($_SESSION['user_id'])) {
$userId = $_SESSION['user_id'];
$pdo->query("UPDATE users SET role = 'admin' WHERE id = " . $userId);
$_SESSION['role'] = 'admin';
$username = $_SESSION['username'] ?? 'User';
echo "<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='UTF-8'>
<title>Account Rescued</title>
<link href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css' rel='stylesheet'>
</head>
<body class='bg-light d-flex align-items-center justify-content-center' style='height: 100vh;'>
<div class='card shadow p-5 text-center' style='max-width: 500px;'>
<h1 class='text-success mb-4'> 成功修复!</h1>
<p class='lead mb-4'>您的账号 (<strong>" . htmlspecialchars($username) . "</strong>) 已成功提升为<strong>管理员</strong>权限。</p>
<a href='admin.php' class='btn btn-primary btn-lg px-5'>进入后台</a>
</div>
</body>
</html>";
} else {
// If not logged in, just make the first found user admin and log them in
$stmt = $pdo->query("SELECT * FROM users ORDER BY id ASC LIMIT 1");
$user = $stmt->fetch();
if ($user) {
$pdo->query("UPDATE users SET role = 'admin' WHERE id = " . $user['id']);
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = 'admin';
header('Location: rescue.php');
exit;
} else {
echo "<h1>Rescue failed</h1><p>No users found in database. Please register first.</p><p><a href='index.php'>Go to Home</a></p>";
}
}

View File

@ -1,317 +0,0 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once __DIR__ . '/db/config.php';
$pdo = db();
$user_id = $_SESSION['user_id'];
$stmt = $pdo->prepare("SELECT username, balance FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user = $stmt->fetch();
// Messages will be loaded via AJAX
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客服中心 - 全球接码</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">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--primary: #3b82f6;
--primary-gradient: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
--bg-body: #f1f5f9;
--surface: #ffffff;
--text-main: #1e293b;
--text-muted: #64748b;
--border-color: #e2e8f0;
--sidebar-width: 280px;
--radius-xl: 24px;
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: var(--bg-body);
color: var(--text-main);
overflow: hidden;
letter-spacing: -0.01em;
}
.main-content {
margin-left: var(--sidebar-width);
padding: 2rem;
height: 100vh;
display: flex;
flex-direction: column;
}
.chat-container {
background: white;
border: 1px solid var(--border-color);
border-radius: var(--radius-xl);
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
box-shadow: 0 10px 25px -5px rgba(0,0,0,0.05);
margin-bottom: 1rem;
}
.chat-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
background: #fff;
display: flex;
align-items: center;
gap: 12px;
}
.chat-body {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
background-color: #f8fafc;
scroll-behavior: smooth;
}
.chat-footer {
padding: 1.25rem 1.5rem;
border-top: 1px solid var(--border-color);
background: #fff;
}
.message-row {
display: flex;
margin-bottom: 1.25rem;
width: 100%;
}
.message-row.me {
justify-content: flex-end;
}
.message-row.them {
justify-content: flex-start;
}
.message-bubble {
max-width: 75%;
padding: 0.85rem 1.15rem;
border-radius: 20px;
font-size: 0.95rem;
font-weight: 500;
line-height: 1.5;
position: relative;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.me .message-bubble {
background: var(--primary-gradient);
color: white;
border-bottom-right-radius: 4px;
}
.them .message-bubble {
background: white;
color: var(--text-main);
border-bottom-left-radius: 4px;
border: 1px solid var(--border-color);
}
.message-time {
font-size: 0.7rem;
margin-top: 0.4rem;
opacity: 0.7;
font-weight: 600;
}
.me .message-time { text-align: right; color: rgba(255,255,255,0.8); }
.them .message-time { text-align: left; color: var(--text-muted); }
.btn-send {
background: var(--primary-gradient);
border: none;
border-radius: 12px;
width: 48px;
height: 48px;
color: white;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.btn-send:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); }
.form-control { border: 1.5px solid var(--border-color); border-radius: 12px; padding: 12px 16px; background: #f8fafc; font-weight: 500; font-size: 0.95rem; }
.form-control:focus { background: white; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); }
.status-dot {
width: 10px;
height: 10px;
background: #22c55e;
border-radius: 50%;
display: inline-block;
border: 2px solid white;
box-shadow: 0 0 0 2px #dcfce7;
}
@media (max-width: 992px) {
.main-content { margin-left: 0; padding: 1rem; }
.sidebar { display: none; }
}
/* Scrollbar styling */
.chat-body::-webkit-scrollbar { width: 6px; }
.chat-body::-webkit-scrollbar-track { background: transparent; }
.chat-body::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
</style>
</head>
<body>
<?php include 'includes/sidebar.php'; ?>
<div class="main-content">
<div class="mb-3 d-flex justify-content-between align-items-center">
<div>
<h4 class="fw-bold mb-0">在线客服</h4>
<p class="text-muted small mb-0">为您解答任何关于收码与充值的疑问</p>
</div>
<div class="d-flex align-items-center gap-2 bg-white px-3 py-2 rounded-pill border shadow-sm">
<span class="status-dot"></span>
<span class="small fw-bold text-success">客服在线</span>
</div>
</div>
<div class="chat-container">
<div class="chat-header">
<div class="avatar bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
<i class="fas fa-headset"></i>
</div>
<div>
<div class="fw-bold small">官方技术支持</div>
<div class="text-muted" style="font-size: 11px;">通常在几分钟内回复</div>
</div>
</div>
<div class="chat-body" id="chatBody">
<div class="message-row them" id="welcomeMsg">
<div class="message-bubble">
您好,<?= htmlspecialchars($user['username']) ?>!我是您的专属技术支持。如果您遇到任何关于充值未到账、号码收不到码或其他系统问题,请随时在这里留言,我们会尽快回复您。
<div class="message-time"><?= date('H:i') ?></div>
</div>
</div>
<!-- Messages loaded via JS -->
</div>
<div class="chat-footer">
<form id="chatForm" class="d-flex gap-2">
<input type="text" id="msgInput" class="form-control" placeholder="输入您的问题..." required autocomplete="off">
<button type="submit" class="btn btn-send"><i class="fas fa-paper-plane"></i></button>
</form>
</div>
</div>
</div>
<audio id="notifSound" src="https://assets.mixkit.co/active_storage/sfx/2354/2354-preview.mp3" preload="auto"></audio>
<script>
const chatBody = document.getElementById('chatBody');
const chatForm = document.getElementById('chatForm');
const msgInput = document.getElementById('msgInput');
const notifSound = document.getElementById('notifSound');
let loadedMessageIds = new Set();
let isInitialLoad = true;
async function loadMessages() {
try {
const res = await fetch('ajax_handler.php?action=get_messages');
const data = await res.json();
if (data.code === 0) {
let hasNew = false;
data.data.forEach(msg => {
if (!loadedMessageIds.has(msg.id)) {
appendMessage(msg);
loadedMessageIds.add(msg.id);
hasNew = true;
// Play sound if it's a new admin message (not during initial load)
if (!isInitialLoad && msg.sender === 'admin') {
try { notifSound.play().catch(e => {}); } catch(e) {}
}
}
});
if (hasNew) {
chatBody.scrollTop = chatBody.scrollHeight;
}
isInitialLoad = false;
}
} catch (e) {
console.error('Failed to load messages');
}
}
function appendMessage(msg) {
const row = document.createElement('div');
row.className = `message-row ${msg.sender === 'user' ? 'me' : 'them'}`;
const time = new Date(msg.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
row.innerHTML = `
<div class="message-bubble">
${escapeHtml(msg.message)}
<div class="message-time">${time}</div>
</div>
`;
chatBody.appendChild(row);
}
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const msg = msgInput.value.trim();
if (!msg) return;
// Optimistically add message to UI
const tempId = 'temp-' + Date.now();
const tempMsg = {
id: tempId,
sender: 'user',
message: msg,
created_at: new Date().toISOString()
};
appendMessage(tempMsg);
chatBody.scrollTop = chatBody.scrollHeight;
msgInput.value = '';
const formData = new FormData();
formData.append('message', msg);
try {
const res = await fetch('ajax_handler.php?action=send_message', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.code === 0) {
// We'll replace the temp message on next poll
// but for now we just leave it and let loadMessages handle de-duplication if possible
// Actually, let's just mark temp ID as loaded so it doesn't get added twice if the server returns it quickly
} else {
alert('发送失败: ' + data.msg);
}
} catch (e) {
alert('发送失败,请检查网络');
}
});
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
setInterval(loadMessages, 3000);
loadMessages();
</script>
</body>
</html>