38320-vm/dashboard.php
2026-03-23 05:08:28 +00:00

657 lines
27 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
date_default_timezone_set("Asia/Shanghai");
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-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);
}
.toast-container { position: fixed; top: 20px; right: 20px; z-index: 2000; }
.custom-toast { background: white; border-radius: 12px; box-shadow: var(--shadow-lg); padding: 16px 24px; border: 1px solid var(--border-color); margin-bottom: 10px; display: flex; align-items: center; gap: 12px; }
.custom-toast.error { border-left: 4px solid #ef4444; }
.custom-toast.success { border-left: 4px solid #22c55e; }
.modal-custom {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 3000;
display: none; align-items: center; justify-content: center;
background: rgba(0,0,0,0.4); backdrop-filter: blur(4px);
}
.modal-content-custom {
background: white; width: 400px; border-radius: 20px; padding: 2rem;
box-shadow: var(--shadow-lg); text-align: center;
}
.highlight-area {
background: #f0f7ff;
border: 2px dashed #3b82f6;
border-radius: var(--radius-lg);
padding: 1.5rem;
margin-bottom: 2rem;
}
@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="toast-container" id="toastContainer"></div>
<div class="modal-custom" id="confirmModal">
<div class="modal-content-custom">
<h5 class="fw-bold mb-3" id="confirmTitle">确认操作</h5>
<p class="text-muted mb-4" id="confirmBody"></p>
<div class="d-flex gap-2">
<button class="btn btn-light flex-grow-1 py-2 fw-bold" onclick="closeConfirm()">取消</button>
<button class="btn btn-primary flex-grow-1 py-2 fw-bold" id="confirmBtn">确定</button>
</div>
</div>
</div>
<div class="main-content">
<div class="page-header">
<h1 class="page-title">购买号码</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;">账户余额</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>
<div class="action-card">
<!-- 最新获取号码 -->
<div class="highlight-area" id="latestOrderArea" style="display: none;">
<div class="d-flex align-items-center mb-3">
<div class="bg-success text-white rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-check fa-xs"></i>
</div>
<h6 class="fw-bold mb-0 text-success">最新获取号码</h6>
</div>
<div id="latestOrderContent" class="row align-items-center text-center">
<!-- Data populated here -->
</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> 活跃任务</span>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="text-muted small">
<th class="ps-4">项目/地区</th>
<th>号码/剩余时间</th>
<th class="text-end pe-4">操作</th>
</tr>
</thead>
<tbody id="activeTasksBody"></tbody>
</table>
</div>
</div>
<div class="search-grid">
<div class="custom-dropdown" id="countryContainer">
<label class="form-label small fw-bold text-muted mb-2 px-1">第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">第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;">实时行情列表</h6>
</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>
<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 searchTimeout = null;
document.addEventListener('DOMContentLoaded', () => {
loadCountries();
renderServices(popularServices);
loadActiveOrders();
setInterval(loadActiveOrders, 10000);
});
function showToast(msg, type = 'success') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `custom-toast ${type}`;
toast.innerHTML = `<i class="fas ${type === 'success' ? 'fa-check-circle text-success' : 'fa-exclamation-circle text-danger'}"></i> <span>${msg}</span>`;
container.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
function showConfirm(title, msg, onConfirm) {
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmBody').textContent = msg;
const btn = document.getElementById('confirmBtn');
btn.onclick = () => { onConfirm(); closeConfirm(); };
document.getElementById('confirmModal').style.display = 'flex';
}
function closeConfirm() { document.getElementById('confirmModal').style.display = 'none'; }
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();
}
} catch (e) {}
}
function toggleDropdown(id, event) {
if (event) event.stopPropagation();
const d = document.getElementById(id);
const isShow = d.classList.contains('show');
document.querySelectorAll('.dropdown-menu-custom').forEach(dd => dd.classList.remove('show'));
if (!isShow) {
d.classList.add('show');
const input = d.querySelector('input');
if (input) setTimeout(() => input.focus(), 50);
}
}
function renderCountries(filter = '') {
const container = document.getElementById('countriesList');
if (!container) return;
container.innerHTML = '';
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;
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></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 = '';
services.forEach(s => {
const div = document.createElement('div');
div.className = 'list-item';
div.innerHTML = `<span class="fw-bold">${s.name}</span>`;
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;
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();
(data.data || []).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);
}
} catch (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');
document.querySelectorAll('.dropdown-menu-custom').forEach(dd => dd.classList.remove('show'));
loadQuotation();
}
function selectService(s) {
currentService = s;
const l = document.getElementById('serviceLabel');
l.textContent = s.name;
l.classList.remove('placeholder'); l.classList.add('val');
document.querySelectorAll('.dropdown-menu-custom').forEach(dd => dd.classList.remove('show'));
loadQuotation();
}
async function loadQuotation() {
const body = document.getElementById('quotationBody');
body.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary"></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 = '';
(data.data || []).forEach(s => {
const item = document.createElement('div');
item.className = 'quotation-item';
item.innerHTML = `
<div class="flex-grow-1">
<div class="fw-bold fs-5">${s.service_name}</div>
<div class="small text-muted">${s.country_name_zh}</div>
</div>
<div class="text-end me-5"><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);
});
}
} catch (e) {}
}
async function getNumber(sid, sname, price, btn) {
showConfirm('购买确认', `确认扣费 $${price} 购买 ${sname} 号码?`, async () => {
try {
const res = await fetch(`${apiHandler}?action=get_number&service_id=${sid}&service_name=${encodeURIComponent(sname)}&country_name=${encodeURIComponent(currentCountry.name_zh)}&price=${price}`);
const data = await res.json();
if (data.code === 0) { showToast('获取成功!'); loadActiveOrders(); updateBalance(); }
else { showToast(data.msg || '获取失败', 'error'); }
} catch (e) { showToast('接口异常', 'error'); }
});
}
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');
if (data.code === 0 && Array.isArray(data.data) && data.data.length > 0) {
section.style.display = 'block';
body.innerHTML = '';
data.data.forEach(o => {
const row = document.createElement('tr');
const expireAt = new Date(o.expire_at.replace(/-/g, "/").replace(" ", "T"));
const now = new Date();
const diffMs = expireAt - now;
const diffMinutes = Math.floor(diffMs / 60000);
const diffSeconds = Math.floor((diffMs % 60000) / 1000);
if (diffMs < 0) {
// Do not auto-release here, let the status update or user handle it
continue;
}
const timeRemaining = `${diffMinutes}分${diffSeconds}秒`;
row.innerHTML = `
<td class="ps-4">${o.service_name} / ${o.country_name}</td>
<td>${o.number || ''} <span class="text-muted ms-2">${timeRemaining}</span></td>
<td class="text-end pe-4"><button class="btn btn-sm btn-outline-danger fw-bold" onclick="releaseNumber('${o.request_id}')">取消</button></td>
`;
body.appendChild(row);
if (o.status !== 'received' && !activePolls[o.request_id]) startPolling(o.request_id);
});
} else { section.style.display = 'none'; }
} catch (e) {}
}
function startPolling(rid) {
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.sms_code) {
clearInterval(activePolls[rid]); delete activePolls[rid]; showSmsModal(data.sms_code); loadActiveOrders();
}
} catch (e) {}
}, 5000);
}
async function releaseNumber(id) {
try {
const res = await fetch(`${apiHandler}?action=release_number&request_id=${id}`);
const data = await res.json();
if (data.code === 0) { showToast("已取消"); } else { showToast(data.msg || "取消失败", "error"); }
loadActiveOrders(); updateBalance();
} catch (e) {}
}
function showSmsModal(code) {
document.getElementById('modalSmsCode').textContent = code;
new bootstrap.Modal(document.getElementById('smsModal')).show();
}
</script>
</body>
</html>