38320-vm/dashboard.php
2026-03-22 12:43:17 +00:00

750 lines
32 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
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 Style */
.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;
}
@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>
<!-- Custom Modal -->
<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>
<!-- 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>
<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">
<thead>
<tr class="text-muted small">
<th class="ps-4">项目/地区</th>
<th>号码</th>
<th>短信内容</th>
<th>剩余时间</th>
<th class="text-end pe-4">操作</th>
</tr>
</thead>
<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">第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>
<span class="badge bg-light text-muted fw-normal" id="lastUpdated">已就绪</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();
});
});
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();
} 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;">热门</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;">价格</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 = '更新时间: ' + 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) {
showConfirm('购买确认', `确认扣费 $${price} 购买 ${sname} 号码?`, async () => {
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) {
showToast('号码获取成功!');
loadActiveOrders(); updateBalance(); window.scrollTo({top: 0, behavior: 'smooth'});
} else if (data.code === 401) {
window.location.href = 'index.php';
} else {
showToast(data.msg || '库存不足或接口超时', 'error');
}
} catch (e) {
showToast('获取号码失败,请重试', 'error');
} 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) {
showConfirm('释放确认', '确定取消并释放此号码吗?', async () => {
try {
const res = await fetch(`${apiHandler}?action=release_number&request_id=${id}`);
const data = await res.json();
if (data.code === 0) { showToast('号码已取消!'); loadActiveOrders(); updateBalance(); } else { showToast(data.msg, 'error'); }
} catch (e) { showToast('连接服务器失败', 'error'); }
});
}
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>