Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07ad4b74fb | ||
|
|
e1da173526 | ||
|
|
12ab6c8a13 | ||
|
|
fd1a7fb782 | ||
|
|
fb7b115095 | ||
|
|
48b78f8a22 | ||
|
|
f96783f9cf | ||
|
|
f09a03c89c | ||
|
|
d3d1a8600b | ||
|
|
d0b702e8b2 | ||
|
|
1af2cf25db | ||
|
|
d7eac0da6a |
719
admin.php
Normal file
@ -0,0 +1,719 @@
|
|||||||
|
<?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>
|
||||||
217
ajax_handler.php
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<?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()]);
|
||||||
|
}
|
||||||
101
api/LocalLubanApi.php
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,15 @@
|
|||||||
:root {
|
:root {
|
||||||
--color-bg: #ffffff;
|
--color-bg: #fff5f7; /* Very Light Pink Background */
|
||||||
--color-text: #1a1a1a;
|
--color-text: #2d1a1e; /* Dark Brownish Pink for Text */
|
||||||
--color-primary: #2563EB; /* Vibrant Blue */
|
--color-primary: #ff4d94; /* Vibrant Pink */
|
||||||
--color-secondary: #000000;
|
--color-secondary: #ff1a75; /* Deep Pink */
|
||||||
--color-accent: #A3E635; /* Lime Green */
|
--color-accent: #ffd1dc; /* Soft Pastel Pink */
|
||||||
--color-surface: #f8f9fa;
|
--color-surface: #ffffff;
|
||||||
--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 #000;
|
--shadow-hard: 5px 5px 0px #ff4d94;
|
||||||
--shadow-hover: 8px 8px 0px #000;
|
--shadow-hover: 8px 8px 0px #ff1a75;
|
||||||
--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-black { background-color: #000 !important; }
|
.bg-pink { background-color: var(--color-primary) !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-black { border: var(--border-width) solid #000; }
|
.border-2-pink { border: var(--border-width) solid var(--color-primary); }
|
||||||
.py-section { padding-top: 5rem; padding-bottom: 5rem; }
|
.py-section { padding-top: 5rem; padding-bottom: 5rem; }
|
||||||
|
|
||||||
/* Navbar */
|
/* Navbar */
|
||||||
.navbar {
|
.navbar {
|
||||||
background: rgba(255, 255, 255, 0.9);
|
background: rgba(255, 245, 247, 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: #000;
|
border-bottom-color: var(--color-primary);
|
||||||
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 #000;
|
border: var(--border-width) solid var(--color-primary);
|
||||||
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,34 +84,29 @@ 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 #000;
|
box-shadow: 0 0 0 var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
border-color: #000;
|
border-color: var(--color-secondary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background-color: #1d4ed8;
|
background-color: var(--color-secondary);
|
||||||
border-color: #000;
|
border-color: var(--color-secondary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline-dark {
|
.btn-outline-pink {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: #000;
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-cta {
|
.btn-cta {
|
||||||
background-color: var(--color-accent);
|
background-color: var(--color-accent);
|
||||||
color: #000;
|
color: var(--color-secondary);
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cta:hover {
|
|
||||||
background-color: #8cc629;
|
|
||||||
color: #000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hero Section */
|
/* Hero Section */
|
||||||
@ -152,53 +147,9 @@ 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 #000;
|
border: 2px solid var(--color-primary);
|
||||||
border-radius: var(--radius-card);
|
border-radius: var(--radius-card);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@ -211,37 +162,14 @@ h1, h2, h3, h4, h5, h6, .navbar-brand {
|
|||||||
|
|
||||||
.project-card:hover {
|
.project-card:hover {
|
||||||
transform: translateY(-10px);
|
transform: translateY(-10px);
|
||||||
box-shadow: 8px 8px 0 #000;
|
box-shadow: var(--shadow-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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: #000;
|
background: var(--color-primary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 5px 12px;
|
padding: 5px 12px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@ -249,98 +177,16 @@ 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 #000;
|
border: 2px solid var(--color-accent);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background: #f8f9fa;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
box-shadow: 4px 4px 0 var(--color-primary);
|
box-shadow: 4px 4px 0 var(--color-primary);
|
||||||
border-color: #000;
|
border-color: var(--color-primary);
|
||||||
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%; }
|
|
||||||
}
|
|
||||||
BIN
assets/pasted-20260210-051518-6ed1892c.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
assets/pasted-20260210-054709-c7166cf7.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/pasted-20260210-074218-c408deaa.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
assets/pasted-20260210-074819-9c0ead1f.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/pasted-20260210-075800-30a55110.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
assets/pasted-20260210-080314-97487350.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
assets/pasted-20260210-082628-83f66727.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
89
auth.php
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<?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' => '无效的请求']);
|
||||||
689
dashboard.php
Normal file
@ -0,0 +1,689 @@
|
|||||||
|
<?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>
|
||||||
64
db/schema.sql
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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';
|
||||||
191
includes/sidebar.php
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
<?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
@ -1,150 +1,286 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
session_start();
|
||||||
@ini_set('display_errors', '1');
|
if (isset($_SESSION['user_id'])) {
|
||||||
@error_reporting(E_ALL);
|
header('Location: dashboard.php');
|
||||||
@date_default_timezone_set('UTC');
|
exit;
|
||||||
|
}
|
||||||
$phpVersion = PHP_VERSION;
|
require_once __DIR__ . '/db/config.php';
|
||||||
$now = date('Y-m-d H:i:s');
|
$pdo = db();
|
||||||
|
$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="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>New Style</title>
|
<title><?= htmlspecialchars($site_name) ?> - 全球专业接码平台</title>
|
||||||
<?php
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
// Read project preview data from environment
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
<style>
|
||||||
?>
|
:root {
|
||||||
<?php if ($projectDescription): ?>
|
--primary: #3b82f6;
|
||||||
<!-- Meta description -->
|
--primary-gradient: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
--bg-body: #f8fafc;
|
||||||
<!-- Open Graph meta tags -->
|
--text-main: #1e293b;
|
||||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
}
|
||||||
<!-- Twitter meta tags -->
|
body {
|
||||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||||
<?php endif; ?>
|
background: #f8fafc;
|
||||||
<?php if ($projectImageUrl): ?>
|
color: var(--text-main);
|
||||||
<!-- Open Graph image -->
|
overflow-x: hidden;
|
||||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
letter-spacing: -0.01em;
|
||||||
<!-- Twitter image -->
|
}
|
||||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
|
||||||
<?php endif; ?>
|
.hero-section {
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
min-height: 100vh;
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
display: flex;
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
align-items: center;
|
||||||
<style>
|
background: radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.05) 0%, rgba(59, 130, 246, 0) 50%);
|
||||||
:root {
|
position: relative;
|
||||||
--bg-color-start: #6a11cb;
|
}
|
||||||
--bg-color-end: #2575fc;
|
.hero-section::after {
|
||||||
--text-color: #ffffff;
|
content: '';
|
||||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
position: absolute;
|
||||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
top: 0; right: 0;
|
||||||
}
|
width: 40%; height: 100%;
|
||||||
body {
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.03) 0%, rgba(59, 130, 246, 0) 100%);
|
||||||
margin: 0;
|
clip-path: polygon(25% 0%, 100% 0%, 100% 100%, 0% 100%);
|
||||||
font-family: 'Inter', sans-serif;
|
z-index: -1;
|
||||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
}
|
||||||
color: var(--text-color);
|
|
||||||
display: flex;
|
.navbar-brand img { height: 40px; }
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
.hero-title {
|
||||||
min-height: 100vh;
|
font-size: 4rem;
|
||||||
text-align: center;
|
font-weight: 800;
|
||||||
overflow: hidden;
|
line-height: 1.1;
|
||||||
position: relative;
|
margin-bottom: 1.5rem;
|
||||||
}
|
letter-spacing: -2px;
|
||||||
body::before {
|
}
|
||||||
content: '';
|
.hero-subtitle {
|
||||||
position: absolute;
|
font-size: 1.25rem;
|
||||||
top: 0;
|
color: #64748b;
|
||||||
left: 0;
|
margin-bottom: 2.5rem;
|
||||||
width: 100%;
|
max-width: 600px;
|
||||||
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>');
|
|
||||||
animation: bg-pan 20s linear infinite;
|
.btn-main {
|
||||||
z-index: -1;
|
padding: 18px 40px;
|
||||||
}
|
font-weight: 800;
|
||||||
@keyframes bg-pan {
|
border-radius: 18px;
|
||||||
0% { background-position: 0% 0%; }
|
transition: all 0.3s;
|
||||||
100% { background-position: 100% 100%; }
|
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.2);
|
||||||
}
|
}
|
||||||
main {
|
.btn-main:hover { transform: translateY(-3px); box-shadow: 0 12px 30px rgba(59, 130, 246, 0.3); }
|
||||||
padding: 2rem;
|
|
||||||
}
|
.form-card {
|
||||||
.card {
|
background: rgba(255, 255, 255, 0.8);
|
||||||
background: var(--card-bg-color);
|
backdrop-filter: blur(20px);
|
||||||
border: 1px solid var(--card-border-color);
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
border-radius: 16px;
|
border-radius: 32px;
|
||||||
padding: 2rem;
|
padding: 3rem;
|
||||||
backdrop-filter: blur(20px);
|
box-shadow: 0 40px 80px rgba(15, 23, 42, 0.05);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
}
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
.input-group-custom {
|
||||||
.loader {
|
margin-bottom: 1.5rem;
|
||||||
margin: 1.25rem auto 1.25rem;
|
}
|
||||||
width: 48px;
|
.input-group-custom label {
|
||||||
height: 48px;
|
font-weight: 700;
|
||||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
font-size: 13px;
|
||||||
border-top-color: #fff;
|
color: #475569;
|
||||||
border-radius: 50%;
|
margin-bottom: 8px;
|
||||||
animation: spin 1s linear infinite;
|
display: block;
|
||||||
}
|
padding-left: 4px;
|
||||||
@keyframes spin {
|
}
|
||||||
from { transform: rotate(0deg); }
|
.input-group-custom .form-control {
|
||||||
to { transform: rotate(360deg); }
|
border: 1.5px solid #e2e8f0;
|
||||||
}
|
border-radius: 16px;
|
||||||
.hint {
|
padding: 14px 20px;
|
||||||
opacity: 0.9;
|
background: #fff;
|
||||||
}
|
font-weight: 600;
|
||||||
.sr-only {
|
transition: all 0.2s;
|
||||||
position: absolute;
|
}
|
||||||
width: 1px; height: 1px;
|
.input-group-custom .form-control:focus {
|
||||||
padding: 0; margin: -1px;
|
border-color: var(--primary);
|
||||||
overflow: hidden;
|
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.08);
|
||||||
clip: rect(0, 0, 0, 0);
|
}
|
||||||
white-space: nowrap; border: 0;
|
|
||||||
}
|
.stat-badge {
|
||||||
h1 {
|
display: inline-flex;
|
||||||
font-size: 3rem;
|
align-items: center;
|
||||||
font-weight: 700;
|
gap: 8px;
|
||||||
margin: 0 0 1rem;
|
background: #fff;
|
||||||
letter-spacing: -1px;
|
padding: 10px 20px;
|
||||||
}
|
border-radius: 100px;
|
||||||
p {
|
font-weight: 800;
|
||||||
margin: 0.5rem 0;
|
font-size: 12px;
|
||||||
font-size: 1.1rem;
|
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
|
||||||
}
|
margin-bottom: 2rem;
|
||||||
code {
|
color: var(--primary);
|
||||||
background: rgba(0,0,0,0.2);
|
}
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
.floating-shape {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
position: absolute;
|
||||||
}
|
z-index: -1;
|
||||||
footer {
|
opacity: 0.1;
|
||||||
position: absolute;
|
filter: blur(2px);
|
||||||
bottom: 1rem;
|
animation: float 6s ease-in-out infinite;
|
||||||
font-size: 0.8rem;
|
}
|
||||||
opacity: 0.7;
|
@keyframes float {
|
||||||
}
|
0% { transform: translateY(0px); }
|
||||||
</style>
|
50% { transform: translateY(-20px); }
|
||||||
|
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>
|
|
||||||
<div class="card">
|
<nav class="navbar navbar-expand-lg fixed-top py-4">
|
||||||
<h1>Analyzing your requirements and generating your website…</h1>
|
<div class="container">
|
||||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
<a class="navbar-brand fw-bold d-flex align-items-center gap-2" href="#">
|
||||||
<span class="sr-only">Loading…</span>
|
<img src="<?= htmlspecialchars($site_logo) ?>" alt="Logo">
|
||||||
</div>
|
<span class="fs-4 fw-800" style="letter-spacing: -1px;"><?= htmlspecialchars($site_name) ?></span>
|
||||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
</a>
|
||||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
<div class="ms-auto">
|
||||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
<a href="support.php" class="text-muted text-decoration-none fw-bold small me-4">联系客服</a>
|
||||||
|
<button class="btn btn-outline-primary fw-bold rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#loginModal">登入系统</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</nav>
|
||||||
<footer>
|
|
||||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
<section class="hero-section">
|
||||||
</footer>
|
<div class="container">
|
||||||
|
<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>
|
||||||
184
orders.php
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<?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>
|
||||||
448
recharge.php
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
<?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>
|
||||||
44
rescue.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?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>";
|
||||||
|
}
|
||||||
|
}
|
||||||
317
support.php
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
<?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>
|
||||||