Files
CLOAK/dashboard.php
2026-07-06 10:34:34 +08:00

1609 lines
74 KiB
PHP
Executable File
Raw Permalink 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
require_once __DIR__ . '/cong.php';
require_once __DIR__ . '/admin/bootstrap.php';
require_once __DIR__ . '/lib/Cloak/CountryAllowlist.php';
require_once __DIR__ . '/admin/ConfigWriter.php';
require_once __DIR__ . '/lib/Cloak/DomainRepository.php';
require_once __DIR__ . '/lib/CloakAdSourceGuard.php';
require_once __DIR__ . '/lib/Cloak/CloudflareConfig.php';
require_once __DIR__ . '/lib/Cloak/DomainProvisioningService.php';
session_start();
$filename = basename(__FILE__);
$current_name = pathinfo($filename, PATHINFO_FILENAME);
// 登出
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "logout") {
unset($_SESSION['password']);
unset($_SESSION['check_result']);
echo("<script>window.location='{$current_name}';</script>");
exit();
}
// 重置临时链接参数
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "delUrlArgs") {
$check_name = strip_tags($_REQUEST['check_name']);
$dir = dirname(__FILE__) . '/args';
$files = glob($dir . '/' . $check_name . "_*.txt");
foreach ($files as $file) {
if (preg_match('/_(\d{10})\.txt$/', $file, $m)) {
unlink($file);
}
}
header('Location: ' . $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'] . '?action=edit_config&name=' . urlencode($check_name));
exit;
}
// 确定当前配置上下文(供表单回显用)
$isNewConfigDraft = false;
$configCopySource = '';
$copyFrom = dash_sanitize_config_name($_REQUEST['copy_from'] ?? '');
if (!empty($_REQUEST["name"])) {
$check_name = trim($_REQUEST["name"]);
$config_name = str_replace(".php", "", $check_name);
$config_file = "check_config/" . $config_name . "_config.php";
if (!file_exists($config_file)) {
$isNewConfigDraft = true;
$sourceFile = ($copyFrom !== '') ? "check_config/{$copyFrom}_config.php" : '';
if ($copyFrom !== '' && is_file($sourceFile)) {
$config_file = $sourceFile;
$configCopySource = $copyFrom;
if ($check_name === 'new_config' || $check_name === '') {
$check_name = dash_suggest_copy_config_name($copyFrom);
}
} else {
$config_file = "check_config/index_config.php";
}
}
} else {
$check_name = "index";
$config_file = "check_config/index_config.php";
}
include($config_file);
// 密码处理
if (!empty($_REQUEST['password'])) {
$_SESSION['password'] = $_REQUEST['password'];
$_SESSION["myaction"] = $_REQUEST['myaction'];
}
// 设置测试访问状态(写入完整 Session 判定缓存,便于测试广告落地链接直达真实页)
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "setuvi") {
require_once __DIR__ . '/lib/Cloak/VisitorRepository.php';
if (!empty($_REQUEST["res"]) && $_REQUEST["res"] == "true") {
cloak_session_mark_real_visitor();
}
if (!empty($_REQUEST["res"]) && $_REQUEST["res"] == "false") {
cloak_session_clear_real_visitor();
}
echo("<script>window.location='{$current_name}?action=edit_config&name={$check_name}';</script>");
exit();
}
// 保存配置
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "submit") {
$check_name = trim($_REQUEST["check_name"]);
$methods = trim($_REQUEST["methods"]);
$country_on = in_array(strtoupper(trim($_REQUEST['country_on'] ?? '')), ['ON']) ? 'ON' : 'OFF';
$country_raw = $country_on === 'ON' ? (string) ($_REQUEST['country'] ?? '') : '';
$country = $country_on === 'ON' ? CountryAllowlist::formatForConfig($country_raw) : '';
$blacklist_group_id = (int)($_REQUEST["blacklist_group_id"] ?? 0);
$whitelist_group_id = (int)($_REQUEST["whitelist_group_id"] ?? 0);
try {
$pdoDash = new PDO(
'mysql:host=127.0.0.1;dbname=' . DB_NAME . ';charset=utf8mb4',
DB_USERNAME, DB_PASSWORD,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$chk = $pdoDash->prepare('SELECT 1 FROM ip_groups WHERE id = ? AND type = ? LIMIT 1');
if ($blacklist_group_id > 0) {
$chk->execute([$blacklist_group_id, 'black']);
if (!$chk->fetchColumn()) $blacklist_group_id = 0;
}
if ($whitelist_group_id > 0) {
$chk->execute([$whitelist_group_id, 'white']);
if (!$chk->fetchColumn()) $whitelist_group_id = 0;
}
} catch (Throwable $e) {
$blacklist_group_id = $whitelist_group_id = 0;
}
$DB_fp = $_REQUEST["DB_fp"];
$DB_zp = trim($_REQUEST["DB_zp"]);
$redirect_method = trim($_REQUEST["redirect_method"]);
$show_content = trim($_REQUEST["show_content"]);
$costm_ip_score = trim($_REQUEST["costm_ip_score"]);
$write_log = trim($_REQUEST["write_log"]);
$zh_on = trim($_REQUEST["zh_on"]);
$os_on = trim($_REQUEST["os_on"]);
$mobile_on = trim($_REQUEST["mobile_on"]);
$v_referer = trim($_REQUEST["v_referer"]);
$is_virtual = trim($_REQUEST["is_virtual"]);
$auto_black = trim($_REQUEST["auto_black"]);
$auto_black_times = trim($_REQUEST["auto_black_times"]);
$white_params = trim($_REQUEST["white_params"]);
$keep_params = trim($_REQUEST["keep_params"]);
$iphone_model = $_REQUEST["iphone_model"];
$risk_number = $_REQUEST["risk_number"];
$risk_enhanced = in_array(strtoupper(trim($_REQUEST["risk_enhanced"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$url_args_timeout = $_REQUEST["url_args_timeout"];
$debug_mode = in_array(strtoupper(trim($_REQUEST["debug_mode"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$ad_fb = in_array(strtoupper(trim($_REQUEST["ad_fb"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$ad_google = in_array(strtoupper(trim($_REQUEST["ad_google"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$ad_tiktok = in_array(strtoupper(trim($_REQUEST["ad_tiktok"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$ad_strict = in_array(strtoupper(trim($_REQUEST["ad_strict"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$config_name_submit = str_replace('.php', '', $check_name);
$existing_spm_id = '';
$existing_cfg = 'check_config/' . $config_name_submit . '_config.php';
if (file_exists($existing_cfg)) {
$existing_raw = file_get_contents($existing_cfg);
if (preg_match("/define\('CLOAK_AD_SPM_ID',\s*'([^']*)'\)/", $existing_raw, $spmM)) {
$existing_spm_id = $spmM[1];
}
}
$ad_spm_id = CloakAdSourceGuard::isValidSpmId($existing_spm_id) ? $existing_spm_id : '';
if (($ad_fb === 'ON' || $ad_google === 'ON' || $ad_tiktok === 'ON') && $ad_spm_id === '') {
$ad_spm_id = CloakAdSourceGuard::generateSpmId();
}
if (strtoupper($auto_black) === 'ON' && $blacklist_group_id <= 0) {
echo "<script>alert('开启「自动黑名单」时,必须先选择「黑名单 IP 组」。');history.back();</script>";
exit();
}
if ($country_on === 'ON' && $country === '') {
echo "<script>alert('已开启国家条件,请填写允许的国家代码(如 US,GB,CA)。');history.back();</script>";
exit();
}
if (!empty($methods)) {
ConfigWriter::save($check_name, [
'methods' => $methods,
'blacklist_group_id' => $blacklist_group_id,
'whitelist_group_id' => $whitelist_group_id,
'country' => $country,
'show_content' => $show_content,
'DB_fp' => $DB_fp,
'costm_ip_score' => $costm_ip_score,
'auto_black' => $auto_black,
'is_virtual' => $is_virtual,
'auto_black_times' => $auto_black_times,
'white_params' => $white_params,
'iphone_model' => $iphone_model,
'write_log' => $write_log,
'zh_on' => $zh_on,
'os_on' => $os_on,
'keep_params' => $keep_params,
'redirect_method' => $redirect_method,
'DB_zp' => $DB_zp,
'mobile_on' => $mobile_on,
'v_referer' => $v_referer,
'risk_number' => $risk_number,
'risk_enhanced' => $risk_enhanced,
'url_args_timeout' => $url_args_timeout,
'debug_mode' => $debug_mode,
'ad_fb' => $ad_fb,
'ad_google' => $ad_google,
'ad_tiktok' => $ad_tiktok,
'ad_strict' => $ad_strict,
'ad_spm_id' => $ad_spm_id,
]);
try {
$pdoBind = DomainRepository::pdo();
DomainRepository::ensureTable($pdoBind);
$siteDomainRaw = $_REQUEST['site_domain_id'] ?? '0';
if ($siteDomainRaw === 'random') {
$existingId = DomainRepository::domainIdForConfig($pdoBind, $check_name);
if ($existingId > 0) {
$siteDomainId = $existingId;
} else {
$siteDomainId = DomainRepository::pickRandomDomainId($pdoBind);
}
} else {
$siteDomainId = (int) $siteDomainRaw;
}
if ($siteDomainId > 0) {
DomainRepository::bindConfigToDomain($pdoBind, $check_name, $siteDomainId);
} else {
DomainRepository::clearConfigBinding($pdoBind, $check_name);
}
} catch (Throwable $e) {
}
echo("<script>alert('保存成功!');window.location='{$current_name}?action=edit_config&name={$check_name}';</script>");
exit();
}
}
function write_file($file_name, $text_str) {
$f = fopen($file_name, "w");
rewind($f);
fwrite($f, $text_str);
fclose($f);
}
function onoff_radio($name, $val, $label_on='开启', $label_off='关闭') {
$on = ($val == 'ON') ? 'active' : '';
$off = ($val != 'ON') ? 'active' : '';
return '<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm btn-outline-primary ' . $on . '">
<input type="radio" name="' . $name . '" value="ON" ' . ($val=='ON'?'checked':'') . '> ' . $label_on . '
</label>
<label class="btn btn-sm btn-outline-primary ' . $off . '">
<input type="radio" name="' . $name . '" value="OFF" ' . ($val!='ON'?'checked':'') . '> ' . $label_off . '
</label>
</div>';
}
function dash_truncate_attr($text, $maxLen = 52) {
$text = (string) $text;
$esc = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
if (mb_strlen($text, 'UTF-8') <= $maxLen) {
return $esc;
}
return '<span class="cell-truncate" title="' . $esc . '">' . htmlspecialchars(mb_substr($text, 0, $maxLen, 'UTF-8'), ENT_QUOTES, 'UTF-8') . '…</span>';
}
if (strlen($text) <= $maxLen) {
return $esc;
}
return '<span class="cell-truncate" title="' . $esc . '">' . htmlspecialchars(substr($text, 0, $maxLen), ENT_QUOTES, 'UTF-8') . '…</span>';
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>IPCLOAKAPI 多功能管理后台面板 v8.2.1</title>
<link rel="stylesheet" href="./dist/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="./dist/jquery-2.2.4.min.js"></script>
<script src="./dist/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script>
<style>
body { background:#f0f2f5; font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif; }
.topbar { background:linear-gradient(135deg,#16baaa,#0e8d81); color:#fff; padding:14px 24px; display:flex; align-items:center; justify-content:space-between; box-shadow:0 2px 8px rgba(0,0,0,.15); }
.topbar a { color:rgba(255,255,255,.85); text-decoration:none; font-size:.9rem; }
.topbar a:hover { color:#fff; }
.topbar h1 { margin:0; font-size:1.25rem; font-weight:600; }
.page-body { max-width:1100px; margin:32px auto; padding:0 16px; }
.page-body.page-body--list { width:90%; max-width:none; margin-left:auto; margin-right:auto; }
.status-bar { display:flex; gap:12px; margin-bottom:20px; flex-wrap:wrap; }
.status-badge { display:inline-flex; align-items:center; gap:6px; padding:6px 14px; border-radius:20px; font-size:.82rem; font-weight:500; }
.status-ok { background:#d4edda; color:#155724; }
.status-err { background:#f8d7da; color:#721c24; }
.card-header-custom { background:#fff; border-bottom:1px solid #e9ecef; padding:14px 20px; display:flex; align-items:center; justify-content:space-between; border-radius:8px 8px 0 0; }
.card-header-custom h5 { margin:0; font-size:1rem; font-weight:600; }
.config-table th { background:#f8f9fa; font-weight:600; }
.config-table td, .config-table th { vertical-align:middle !important; padding:12px 16px !important; }
.config-table tr:hover { background:#f0f7ff; }
.url-text { font-size:.78rem; color:#6c757d; }
.ad-link-row { display:flex; align-items:flex-start; gap:6px; margin-bottom:6px; }
.ad-link-row:last-child { margin-bottom:0; }
.ad-link-label { font-size:.72rem; color:#495057; white-space:nowrap; min-width:58px; padding-top:2px; }
.btn-copy-ad { font-size:.72rem; padding:2px 8px; flex-shrink:0; }
.ad-link-hint { font-size:.68rem; color:#adb5bd; margin-top:4px; }
.section-heading { background:#e8f4fd; color:#1565c0; font-weight:600; padding:8px 14px; border-radius:4px; margin:6px 0 2px; font-size:.85rem; letter-spacing:.3px; }
.form-label-cell { width:36%; font-size:.88rem; color:#444; padding:10px 14px !important; }
.form-val-cell { padding:8px 14px !important; }
.form-section { border:1px solid #dee2e6; border-radius:6px; margin-bottom:8px; overflow:hidden; }
.form-section .section-toggle { background:#f8f9fa; padding:10px 16px; cursor:pointer; display:flex; align-items:center; justify-content:space-between; font-weight:600; font-size:.9rem; border:none; width:100%; text-align:left; }
.form-section .section-toggle:hover { background:#e9ecef; }
.form-section .section-toggle .arrow { transition:transform .2s; }
.form-section .section-toggle.collapsed .arrow { transform:rotate(-90deg); }
.back-link { display:inline-flex; align-items:center; gap:6px; color:#1565c0; font-size:.9rem; margin-bottom:16px; text-decoration:none; }
.back-link:hover { text-decoration:underline; }
.sticky-actions { position:sticky; bottom:0; background:#fff; border-top:1px solid #dee2e6; padding:12px 20px; display:flex; gap:10px; align-items:center; z-index:100; border-radius:0 0 8px 8px; }
.login-wrap { min-height:100vh; display:flex; align-items:center; justify-content:center; }
.login-card { background:#fff; border-radius:10px; box-shadow:0 4px 20px rgba(0,0,0,.1); padding:40px; width:340px; }
.login-card h2 { text-align:center; color:#16baaa; margin-bottom:24px; font-size:1.4rem; }
.list-page-title { margin-bottom: 4px; }
.list-page-title h2 { margin: 0 0 6px; font-size: 1.2rem; font-weight: 600; color: #2c3e50; }
.list-page-title p { margin: 0; font-size: .85rem; color: #8c98a4; }
.card-shell { border-radius: 10px; overflow: hidden; border: 1px solid #e8ecf1; box-shadow: 0 2px 12px rgba(15,23,42,.04); }
.card-header-custom .header-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.card-header-custom .header-actions .btn-teal { background: #16baaa; color: #fff; border: none; }
.card-header-custom .header-actions .btn-teal:hover { background: #0e8d81; color: #fff; }
.list-table-wrap { overflow-x: auto; }
.config-table thead th {
position: sticky;
top: 0;
z-index: 5;
background: linear-gradient(180deg, #f4f7fb 0%, #eef2f7 100%) !important;
border-bottom: 2px solid #dde4ec !important;
font-size: .82rem;
letter-spacing: .02em;
text-transform: none;
white-space: nowrap;
}
.config-table tbody tr:nth-child(even) { background: #fafbfc; }
.config-table tbody tr:hover { background: #eef6ff !important; }
.config-table tbody tr { transition: background-color .15s ease; }
.config-table tbody td { border-color: #eef1f5 !important; font-size: .86rem; }
.list-table thead th {
background: #f4f7fb;
font-weight: 600;
font-size: .82rem;
border-bottom: 2px solid #dde4ec !important;
padding: 10px 14px !important;
white-space: nowrap;
}
.list-table tbody tr:nth-child(even) { background: #fafbfc; }
.list-table tbody tr:hover { background: #eef6ff; }
.list-table tbody td { vertical-align: middle !important; padding: 11px 14px !important; border-color: #eef1f5 !important; font-size: .84rem; }
.cell-truncate { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; cursor: default; }
.url-text-truncate { font-size: .78rem; color: #6c757d; word-break: break-all; }
.action-btn-group { display: flex; flex-wrap: wrap; gap: 6px; }
.action-btn-group .btn { border-radius: 6px; }
.domain-host-badge { display: inline-block; padding: 2px 8px; background: #e8f4fd; color: #1565c0; border-radius: 4px; font-size: .78rem; font-family: 'SF Mono', Consolas, monospace; }
.config-name-cell strong { color: #2c3e50; font-size: .92rem; }
.empty-hint { padding: 28px 20px; text-align: center; color: #8c98a4; font-size: .88rem; background: #fafbfc; border-radius: 0 0 10px 10px; }
.list-toolbar { display:flex; flex-wrap:wrap; align-items:center; gap:10px; padding:12px 16px; border-bottom:1px solid #e9ecef; background:#fafbfc; }
.list-toolbar .search-form { display:flex; align-items:center; gap:8px; flex:1; min-width:220px; }
.list-toolbar .search-form input[type="search"] { max-width:280px; }
.list-pagination { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; padding:12px 16px; border-top:1px solid #e9ecef; background:#fff; font-size:.84rem; color:#6c757d; }
.list-pagination .pagination { margin:0; }
.config-table .col-check { width:36px; text-align:center; }
/* 表单开关:选中实心蓝,未选中蓝框蓝字 */
.btn-group-toggle .btn.btn-outline-primary,
.btn-group-toggle .btn.btn-outline-secondary {
color: #1565c0;
border-color: #64b5f6;
background: #fff;
font-weight: 500;
min-width: 52px;
}
.btn-group-toggle .btn.btn-outline-primary:hover,
.btn-group-toggle .btn.btn-outline-secondary:hover {
color: #0d47a1;
background: #e3f2fd;
border-color: #42a5f5;
}
.btn-group-toggle .btn.btn-outline-primary.active,
.btn-group-toggle .btn.btn-outline-secondary.active,
.btn-group-toggle .btn.btn-outline-primary.active:hover,
.btn-group-toggle .btn.btn-outline-secondary.active:hover {
color: #fff !important;
background: linear-gradient(180deg, #2196f3 0%, #1565c0 100%) !important;
border-color: #1565c0 !important;
box-shadow: 0 1px 4px rgba(21, 101, 192, .35);
}
.btn-group-toggle .btn.btn-outline-primary:not(.active):focus,
.btn-group-toggle .btn.btn-outline-secondary:not(.active):focus {
box-shadow: 0 0 0 .15rem rgba(33, 150, 243, .25);
}
</style>
</head>
<body>
<?php if (empty($_SESSION['password'])): ?>
<!-- ===================== 登录页 ===================== -->
<div class="login-wrap">
<div class="login-card">
<h2>IPCLOAKAPI 多功能管理后台面板 v8.2.1</h2>
<form method="post" action="<?php echo $current_name; ?>">
<div class="form-group">
<label>登录密码</label>
<input type="password" name="password" class="form-control" placeholder="请输入密码" autofocus>
</div>
<input type="hidden" name="myaction" value="dolist">
<button type="submit" class="btn btn-block" style="background:#16baaa;color:#fff;margin-top:8px;">进入后台</button>
</form>
</div>
</div>
<?php exit(); endif; ?>
<?php
// 密码验证
if ($_SESSION['password'] != LOGIN_PASSWORD) {
unset($_SESSION['password']);
echo("<script>alert('密码错误!');window.location='{$current_name}';</script>");
exit();
}
// 站点域名管理
$cloakDomainMsg = '';
$cloakDomainErr = '';
$cloakCfEnabled = CloudflareConfig::isEnabled();
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'add_domain') {
$hostText = trim((string) ($_POST['new_hostnames'] ?? $_POST['new_hostname'] ?? ''));
if ($hostText !== '') {
try {
$pdoDom = DomainRepository::pdo();
$dnsMode = $_POST['dns_mode'] ?? 'auto';
if (!in_array($dnsMode, ['auto', 'manual'], true)) {
$dnsMode = 'auto';
}
if ($dnsMode === 'auto' && !$cloakCfEnabled) {
$cloakDomainErr = '自动模式需要先在 cong.php 配置 Cloudflare API Token 与 Account ID,或改选手动模式。';
} else {
$res = DomainProvisioningService::provisionBatch($pdoDom, $hostText, $dnsMode);
if ($res['ok']) {
$addedNames = array_map(static function ($row) {
return $row['hostname'] ?? '';
}, $res['added'] ?? []);
$addedNames = array_filter($addedNames);
$cloakDomainMsg = '已成功添加 ' . count($addedNames) . ' 个域名:'
. htmlspecialchars(implode('、', $addedNames));
if (($res['dns_mode'] ?? '') === 'auto' && $cloakCfEnabled) {
$cloakDomainMsg .= '。请到注册商修改 Nameserver 后,在列表中点击「检测/刷新」。';
} elseif (($res['dns_mode'] ?? '') === 'manual') {
$cloakDomainMsg .= '(手动解析模式,无需 Cloudflare 检测)。';
} else {
$cloakDomainMsg .= ',请在编辑配置时关联此域名。';
}
if (!empty($res['failed'])) {
$failParts = [];
foreach ($res['failed'] as $f) {
$failParts[] = ($f['hostname'] ?? '?') . '' . ($f['error'] ?? '失败') . '';
}
$cloakDomainErr = count($res['failed']) . ' 个域名添加失败:'
. htmlspecialchars(implode('', $failParts));
}
} else {
$cloakDomainErr = $res['error'] ?? '添加失败';
if (!empty($res['failed'])) {
$failParts = [];
foreach ($res['failed'] as $f) {
$failParts[] = ($f['hostname'] ?? '?') . '' . ($f['error'] ?? '失败') . '';
}
$cloakDomainErr .= '' . htmlspecialchars(implode('', $failParts));
}
}
}
} catch (Throwable $e) {
$cloakDomainErr = $e->getMessage();
}
} else {
$cloakDomainErr = '请至少输入一个域名';
}
}
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'check_domain') {
$domainId = (int) ($_POST['domain_id'] ?? $_REQUEST['domain_id'] ?? 0);
$jsonOut = isset($_REQUEST['format']) && $_REQUEST['format'] === 'json';
try {
$res = DomainProvisioningService::checkAndProvision(DomainRepository::pdo(), $domainId);
if ($jsonOut) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode($res, JSON_UNESCAPED_UNICODE);
exit;
}
if ($res['ok']) {
$cloakDomainMsg = $res['message'] ?? '检测完成';
} else {
$cloakDomainErr = $res['message'] ?? '检测失败';
}
} catch (Throwable $e) {
if ($jsonOut) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
exit;
}
$cloakDomainErr = $e->getMessage();
}
}
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'delete_domain' && !empty($_REQUEST['domain_id'])) {
try {
$res = DomainProvisioningService::deleteWithCloudflare(DomainRepository::pdo(), (int) $_REQUEST['domain_id']);
if ($res['ok']) {
$cloakDomainMsg = $res['message'] ?? '域名已删除。';
if (!empty($res['warning'])) {
$cloakDomainErr = $res['warning'];
}
} else {
$cloakDomainErr = $res['message'] ?? '删除失败';
}
} catch (Throwable $e) {
$cloakDomainErr = $e->getMessage();
}
}
// 系统状态检测(列表页用)
function dash_check_mysql() {
try {
$pdo = new PDO('mysql:host=127.0.0.1;dbname=' . DB_NAME . ';charset=utf8mb4', DB_USERNAME, DB_PASSWORD, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
return true;
} catch (Throwable $e) { return false; }
}
function dash_check_redis() {
if (!extension_loaded('redis') || strtoupper(REDIS_ENABLED) !== 'ON') return null;
try {
$r = new Redis();
if (!@$r->connect(REDIS_HOST, (int)REDIS_PORT, 0.5)) return false;
$r->ping();
return true;
} catch (Throwable $e) { return false; }
}
function dash_sanitize_config_name($name)
{
$name = trim(str_replace('.php', '', (string) $name));
if ($name === '' || !preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
return '';
}
return $name;
}
function dash_suggest_copy_config_name($source)
{
$source = dash_sanitize_config_name($source);
if ($source === '') {
return 'new_config';
}
$base = $source . '_copy';
$candidate = $base;
$suffix = 2;
while (file_exists(__DIR__ . "/check_config/{$candidate}_config.php")) {
$candidate = $base . $suffix;
$suffix++;
}
return $candidate;
}
function dash_cfg_get($raw, $name)
{
if (preg_match("/define\('" . preg_quote($name, '/') . "',\s*'([^']*)'\)/", $raw, $m)) {
return $m[1];
}
return '';
}
function dash_build_ad_links($pf, $protocol, $host)
{
$path = __DIR__ . "/check_config/{$pf}_config.php";
try {
$pdoDashHost = DomainRepository::pdo();
DomainRepository::ensureTable($pdoDashHost);
$bound = DomainRepository::primaryHostnameForConfig($pdoDashHost, $pf);
if ($bound !== '') {
$host = $bound;
}
} catch (Throwable $e) {
}
$host = DomainRepository::normalizeHostname($host);
if ($host === '') {
$host = DomainRepository::normalizeHostname($_SERVER['HTTP_HOST'] ?? '');
}
$base = rtrim($protocol . $host, '/') . '/' . (($pf != 'index') ? $pf : '');
if (!file_exists($path)) {
return [['label' => '落地链接', 'url' => $base]];
}
$raw = file_get_contents($path);
$enabled = [];
if (dash_cfg_get($raw, 'CLOAK_AD_FB') === 'ON') {
$enabled[] = CloakAdSourceGuard::PLATFORM_FB;
}
if (dash_cfg_get($raw, 'CLOAK_AD_GOOGLE') === 'ON') {
$enabled[] = CloakAdSourceGuard::PLATFORM_GOOGLE;
}
if (dash_cfg_get($raw, 'CLOAK_AD_TIKTOK') === 'ON') {
$enabled[] = CloakAdSourceGuard::PLATFORM_TIKTOK;
}
if (empty($enabled)) {
return [['label' => '落地链接', 'url' => $base]];
}
$spmId = dash_cfg_get($raw, 'CLOAK_AD_SPM_ID');
$fpUrls = [];
if (preg_match("/define\('DB_FP',\s*(array\s*\(.*?\))\s*\)/s", $raw, $fpMatch)) {
@eval('$fpUrls = ' . $fpMatch[1] . ';');
}
$query = CloakAdSourceGuard::buildAdLinkQuery($enabled, $spmId, $fpUrls);
$url = $base . ($query !== '' ? '?' . $query : '');
return [['label' => '落地链接', 'url' => $url]];
}
function dash_build_config_index()
{
$items = [];
$dir = __DIR__ . '/check_config';
$dh = @opendir($dir);
if (!$dh) {
return $items;
}
while (false !== ($f = readdir($dh))) {
if ($f === '.' || $f === '..') {
continue;
}
$path = $dir . '/' . $f;
if (is_dir($path) || $f === 'index.php' || $f === 'a.txt') {
continue;
}
if (substr($f, -11) !== '_config.php') {
continue;
}
$name = str_replace('_config.php', '', $f);
if ($name === '' || !preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
continue;
}
$entryPhp = __DIR__ . '/' . $name . '.php';
if (!file_exists($entryPhp)) {
copy(__DIR__ . '/tem_link.php', $entryPhp);
}
$items[] = [
'name' => $name,
'mtime' => (int) @filemtime($path),
'path' => $path,
];
}
closedir($dh);
usort($items, function ($a, $b) {
if ($a['mtime'] === $b['mtime']) {
return strcmp($a['name'], $b['name']);
}
return $b['mtime'] <=> $a['mtime'];
});
return $items;
}
function dash_paginate_configs(array $all, $q, $page, $perPage)
{
$q = trim((string) $q);
$page = max(1, (int) $page);
$perPage = max(1, (int) $perPage);
if ($q !== '') {
$qLower = function_exists('mb_strtolower') ? mb_strtolower($q, 'UTF-8') : strtolower($q);
$all = array_values(array_filter($all, function ($row) use ($qLower) {
$name = (string) ($row['name'] ?? '');
$hay = function_exists('mb_strtolower') ? mb_strtolower($name, 'UTF-8') : strtolower($name);
return strpos($hay, $qLower) !== false;
}));
}
$total = count($all);
$totalPages = $total > 0 ? (int) ceil($total / $perPage) : 1;
if ($page > $totalPages) {
$page = $totalPages;
}
$offset = ($page - 1) * $perPage;
return [
'items' => array_slice($all, $offset, $perPage),
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'total_pages' => $totalPages,
];
}
function dash_delete_config($name)
{
$name = dash_sanitize_config_name($name);
if ($name === '' || $name === 'index') {
return false;
}
try {
DomainRepository::clearConfigBinding(DomainRepository::pdo(), $name);
} catch (Throwable $e) {
}
@unlink(__DIR__ . "/check_config/{$name}_config.php");
@unlink(__DIR__ . "/{$name}.php");
return true;
}
function dash_list_query_params($q, $page)
{
$params = [];
if (trim((string) $q) !== '') {
$params['q'] = trim((string) $q);
}
if ((int) $page > 1) {
$params['page'] = (int) $page;
}
return $params;
}
function dash_list_url($currentName, $q, $page)
{
$params = dash_list_query_params($q, $page);
$qs = $params !== [] ? '?' . http_build_query($params) : '';
return $currentName . $qs;
}
// 配置列表索引(按文件修改时间倒序)
$configListAll = dash_build_config_index();
$listSearchQ = trim((string) ($_GET['q'] ?? ''));
$listPage = max(1, (int) ($_GET['page'] ?? 1));
$listPerPage = 10;
$configPage = dash_paginate_configs($configListAll, $listSearchQ, $listPage, $listPerPage);
$products_file_ary = array_map(function ($row) {
return $row['name'];
}, $configListAll);
// 删除 / 批量删除配置
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "delete_config" && !empty($_REQUEST["name"])) {
$name = trim($_REQUEST["name"]);
if ($name == "index") {
echo("<script>alert('请勿删除默认配置!');window.location='{$current_name}';</script>");
exit();
}
dash_delete_config($name);
echo("<script>alert('删除成功!');window.location='{$current_name}';</script>");
exit();
}
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'batch_delete_config') {
$names = $_REQUEST['names'] ?? [];
if (!is_array($names)) {
$names = [$names];
}
$deleted = 0;
$seen = [];
foreach ($names as $rawName) {
$name = dash_sanitize_config_name($rawName);
if ($name === '' || $name === 'index' || isset($seen[$name])) {
continue;
}
$seen[$name] = true;
if (dash_delete_config($name)) {
$deleted++;
}
}
$redirect = dash_list_url($current_name, $_POST['q'] ?? '', 1);
$msg = $deleted > 0 ? "已删除 {$deleted} 个配置" : '未删除任何配置';
echo("<script>alert('" . addslashes($msg) . "');window.location='" . addslashes($redirect) . "';</script>");
exit();
}
// 加载 IP 组(编辑页用)
$blackGroups = $whiteGroups = [];
try {
$pdoDash = new PDO('mysql:host=127.0.0.1;dbname=' . DB_NAME . ';charset=utf8mb4', DB_USERNAME, DB_PASSWORD, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
$stb = $pdoDash->prepare("SELECT id, name FROM ip_groups WHERE type = 'black' ORDER BY id ASC");
$stb->execute(); $blackGroups = $stb->fetchAll();
$stw = $pdoDash->prepare("SELECT id, name FROM ip_groups WHERE type = 'white' ORDER BY id ASC");
$stw->execute(); $whiteGroups = $stw->fetchAll();
} catch (Throwable $e) {}
$currentBlacklistGroupId = defined('BLACKLIST_GROUP_ID') ? (int)BLACKLIST_GROUP_ID : 0;
$currentWhitelistGroupId = defined('WHITELIST_GROUP_ID') ? (int)WHITELIST_GROUP_ID : 0;
$action = $_REQUEST['action'] ?? '';
$isEdit = in_array($action, ['edit_config', 'submit'], true);
$cloakSiteDomains = [];
$domainConfigMap = [];
$selectedDomainId = 0;
$dashboardDefaultHost = DomainRepository::normalizeHostname($_SERVER['HTTP_HOST'] ?? '');
try {
$pdoDash = $pdoDash ?? DomainRepository::pdo();
DomainRepository::ensureTable($pdoDash);
$cloakSiteDomains = DomainRepository::listAll($pdoDash);
$domainConfigMap = DomainRepository::groupConfigsByDomainId($pdoDash);
if ($isEdit) {
$selectedDomainId = DomainRepository::domainIdForConfig($pdoDash, $check_name);
}
} catch (Throwable $e) {
$cloakSiteDomains = [];
$domainConfigMap = [];
}
// 视图判断
$_d = function($name, $default) { return defined($name) ? constant($name) : $default; };
?>
<!-- ===================== 顶栏 ===================== -->
<div class="topbar">
<h1>IPCLOAKAPI 多功能管理后台面板 v8.2.1</h1>
<div style="display:flex;gap:16px;align-items:center;">
<?php if ($isEdit): ?>
<a href="<?php echo $current_name; ?>">← 返回列表</a>
<?php endif; ?>
<a href="<?php echo $current_name; ?>?action=logout">退出登录</a>
</div>
</div>
<div class="page-body<?php echo $isEdit ? '' : ' page-body--list'; ?>">
<?php if (!$isEdit): ?>
<!-- ===================== 列表视图 ===================== -->
<div class="list-page-title">
<h2>配置管理</h2>
<p>管理 Cloak 配置、广告落地链接与站点域名</p>
</div>
<!-- 系统状态 -->
<div class="status-bar">
<?php
$mysqlOk = dash_check_mysql();
$redisOk = dash_check_redis();
echo '<span class="status-badge ' . ($mysqlOk ? 'status-ok' : 'status-err') . '">● MySQL ' . ($mysqlOk ? '已连接' : '连接失败') . '</span>';
if ($redisOk === null) {
echo '<span class="status-badge" style="background:#fff3cd;color:#856404;">● Redis 未启用</span>';
} else {
echo '<span class="status-badge ' . ($redisOk ? 'status-ok' : 'status-err') . '">● Redis ' . ($redisOk ? '已连接' : '连接失败') . '</span>';
}
?>
<span class="status-badge" style="background:#e2e3e5;color:#383d41;">● 配置数:<?php echo (int) $configPage['total']; ?><?php if ($listSearchQ !== ''): ?>(搜索「<?php echo htmlspecialchars($listSearchQ); ?>」)<?php endif; ?></span>
<span class="status-badge" style="background:#e2e3e5;color:#383d41;">● 已登记域名:<?php echo count($cloakSiteDomains); ?></span>
</div>
<?php if ($cloakDomainMsg): ?>
<div class="alert alert-success"><?php echo $cloakDomainMsg; ?></div>
<?php endif; ?>
<?php if ($cloakDomainErr): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($cloakDomainErr); ?></div>
<?php endif; ?>
<!-- 配置列表卡片 -->
<div class="card shadow-sm card-shell">
<div class="card-header-custom">
<h5>配置列表</h5>
<div class="header-actions">
<a href="/table" target="_blank" class="btn btn-sm btn-outline-secondary">查看日志</a>
<a href="/log_analytics.php" target="_blank" class="btn btn-sm btn-outline-secondary">数据分析</a>
<button type="submit" form="batchDeleteForm" class="btn btn-sm btn-outline-danger" id="batchDeleteBtn" onclick="return dashBatchDeleteConfirm();">批量删除</button>
<a href="<?php echo $current_name; ?>?action=edit_config&name=new_config" class="btn btn-sm btn-teal">+ 新增配置</a>
</div>
</div>
<div class="list-toolbar">
<form method="get" action="<?php echo htmlspecialchars($current_name); ?>" class="search-form mb-0">
<input type="search" name="q" class="form-control form-control-sm" placeholder="搜索配置名称…" value="<?php echo htmlspecialchars($listSearchQ, ENT_QUOTES); ?>">
<button type="submit" class="btn btn-sm btn-outline-primary">搜索</button>
<?php if ($listSearchQ !== ''): ?>
<a href="<?php echo htmlspecialchars($current_name); ?>" class="btn btn-sm btn-outline-secondary">清除</a>
<?php endif; ?>
</form>
</div>
<form method="post" action="<?php echo htmlspecialchars($current_name); ?>" id="batchDeleteForm">
<input type="hidden" name="action" value="batch_delete_config">
<input type="hidden" name="q" value="<?php echo htmlspecialchars($listSearchQ, ENT_QUOTES); ?>">
<div class="list-table-wrap">
<table class="table config-table mb-0">
<thead>
<tr>
<th class="col-check"><input type="checkbox" id="checkAllConfigs" title="全选"></th>
<th style="width:20%">配置名称</th>
<th style="width:12%">绑定域名</th>
<th>广告链接(广告后台填入的链接)</th>
<th>真实页链接(客户到达的链接)</th>
<th style="width:20%">操作</th>
</tr>
</thead>
<tbody>
<?php if (empty($configPage['items'])): ?>
<tr><td colspan="6" class="empty-hint">暂无匹配的配置<?php if ($listSearchQ !== ''): ?>,请调整搜索关键词<?php endif; ?></td></tr>
<?php endif; ?>
<?php foreach ($configPage['items'] as $cfgRow):
$pf = $cfgRow['name'];
$pcfg = "check_config/{$pf}_config.php";
$pfp = [];
if (file_exists($pcfg)) {
// 简单读取 DB_FP 和 DB_ZP 值用于展示
$raw = file_get_contents($pcfg);
if (preg_match("/define\('DB_FP',\s*(array\s*\(.*?\))\s*\)/s", $raw, $m)) {
@eval('$pfp = ' . $m[1] . ';');
}
preg_match("/define\('DB_ZP',\s*'([^']*)'\)/", $raw, $zpM);
$pzp = $zpM[1] ?? '';
}
?>
<tr>
<td class="col-check">
<?php if ($pf !== 'index'): ?>
<input type="checkbox" class="config-row-check" name="names[]" value="<?php echo htmlspecialchars($pf, ENT_QUOTES); ?>">
<?php endif; ?>
</td>
<td class="config-name-cell">
<strong><?php echo htmlspecialchars($pf); ?></strong>
<?php if ($pf === 'index'): ?><span class="badge badge-secondary ml-1" style="font-size:.7rem;">默认</span><?php endif; ?>
</td>
<td style="font-size:.82rem;">
<?php
$pfHost = '';
try {
$pfHost = DomainRepository::primaryHostnameForConfig($pdoDash ?? DomainRepository::pdo(), $pf);
} catch (Throwable $e) {
}
if ($pfHost !== '') {
echo '<span class="domain-host-badge" title="' . htmlspecialchars($pfHost, ENT_QUOTES) . '">' . htmlspecialchars($pfHost) . '</span>';
} else {
echo '<span class="text-muted cell-truncate" title="' . htmlspecialchars($dashboardDefaultHost, ENT_QUOTES) . '">' . htmlspecialchars($dashboardDefaultHost) . '<br><small>(默认后台域)</small></span>';
}
?>
</td>
<td>
<?php
$adLinks = dash_build_ad_links($pf, $protocol, $_SERVER['HTTP_HOST']);
foreach ($adLinks as $link):
?>
<div class="ad-link-row">
<span class="ad-link-label"><?php echo htmlspecialchars($link['label']); ?></span>
<div class="url-text flex-grow-1 url-text-truncate"><?php echo dash_truncate_attr($link['url'], 56); ?></div>
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy-ad" data-copy="<?php echo htmlspecialchars($link['url'], ENT_QUOTES); ?>">复制</button>
</div>
<?php endforeach; ?>
</td>
<td>
<?php if (!empty($pfp)) {
$first = is_array($pfp) ? $pfp[0] : $pfp;
echo '<div class="url-text url-text-truncate">' . dash_truncate_attr($first, 48) . '</div>';
if (is_array($pfp) && count($pfp) > 1) {
echo '<div class="url-text text-muted">+' . (count($pfp)-1) . ' 个链接</div>';
}
} else { ?>
<span class="text-muted" style="font-size:.82rem;">—</span>
<?php } ?>
</td>
<td>
<div class="action-btn-group">
<a href="<?php echo $current_name . '?action=edit_config&name=' . urlencode($pf); ?>" class="btn btn-sm btn-outline-primary">编辑</a>
<a href="<?php echo $current_name . '?action=edit_config&name=new_config&amp;copy_from=' . urlencode($pf); ?>" class="btn btn-sm btn-outline-info">复制</a>
<a href="/<?php echo $pf; ?>.php" target="_blank" class="btn btn-sm btn-outline-secondary">查看</a>
<?php if ($pf !== 'index'): ?>
<button class="btn btn-sm btn-outline-danger" onclick="if(confirm('确定删除配置「<?php echo htmlspecialchars($pf); ?>」?')) location='<?php echo $current_name . '?action=delete_config&name=' . $pf; ?>'">删除</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($configPage['total_pages'] > 1 || $configPage['total'] > 0): ?>
<div class="list-pagination">
<span>第 <?php echo (int) $configPage['page']; ?> / <?php echo (int) $configPage['total_pages']; ?> 页,共 <?php echo (int) $configPage['total']; ?> 条</span>
<?php if ($configPage['total_pages'] > 1): ?>
<nav aria-label="配置分页">
<ul class="pagination pagination-sm">
<?php
$curPage = (int) $configPage['page'];
$maxPage = (int) $configPage['total_pages'];
$prevPage = max(1, $curPage - 1);
$nextPage = min($maxPage, $curPage + 1);
?>
<li class="page-item<?php echo $curPage <= 1 ? ' disabled' : ''; ?>">
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $prevPage)); ?>">上一页</a>
</li>
<?php for ($p = 1; $p <= $maxPage; $p++): ?>
<li class="page-item<?php echo $p === $curPage ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $p)); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
<li class="page-item<?php echo $curPage >= $maxPage ? ' disabled' : ''; ?>">
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $nextPage)); ?>">下一页</a>
</li>
</ul>
</nav>
<?php endif; ?>
</div>
<?php endif; ?>
</form>
</div>
<!-- 站点域名 -->
<div class="card shadow-sm card-shell mb-4" style="margin-top: 16px;">
<div class="card-header-custom">
<h5>站点域名</h5>
</div>
<div style="padding:16px 20px;">
<?php if ($cloakCfEnabled): ?>
<div class="alert alert-info mb-3" style="font-size:.88rem;">
<strong>Cloudflare 已启用:</strong>选择「自动」时将创建 Zone 并配置 DNS/SSL;选择「手动」时仅本地登记(您已自行配置 DNS)。
源站仍需在宝塔添加站点并监听 <code><?php echo htmlspecialchars(CloudflareConfig::serverIp() ?: 'SERVER_IP'); ?></code>。
</div>
<?php else: ?>
<div class="alert alert-warning mb-3" style="font-size:.88rem;">
<strong>提示:</strong>自动模式需先在 <code>cong.php</code> 配置 Cloudflare API Token 与 Account ID。若已自行完成 DNS 解析,请选择「手动」模式。
使用前请先在宝塔添加站点并开启 SSL。
</div>
<?php endif; ?>
<form method="post" action="<?php echo $current_name; ?>" class="mb-3">
<input type="hidden" name="action" value="add_domain">
<div class="form-group mb-2">
<label class="mb-1"><strong>域名列表</strong> <small class="text-muted">(每行一个,也支持逗号分隔)</small></label>
<textarea name="new_hostnames" class="form-control form-control-sm" rows="5" style="max-width:520px;font-family:monospace;" placeholder="shop.example.com&#10;api.example.com&#10;landing.example.com" required></textarea>
</div>
<div class="form-group mb-2">
<label class="d-block mb-1"><strong>DNS 配置方式</strong></label>
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm btn-outline-primary active">
<input type="radio" name="dns_mode" value="auto" checked autocomplete="off"> 自动
</label>
<label class="btn btn-sm btn-outline-primary">
<input type="radio" name="dns_mode" value="manual" autocomplete="off"> 手动
</label>
</div>
<small class="text-muted d-block mt-1">
自动:通过 Cloudflare 创建 Zone 并配置 DNS/SSL(默认)。
手动:仅登记域名,不调用 Cloudflare,无需检测/刷新。
</small>
</div>
<button type="submit" class="btn btn-sm" style="background:#16baaa;color:#fff;">批量添加</button>
</form>
<?php if (empty($cloakSiteDomains)): ?>
<div class="empty-hint">尚未添加域名。未添加时,广告链接将使用当前后台访问域名:<code><?php echo htmlspecialchars($dashboardDefaultHost); ?></code></div>
<?php else: ?>
<div class="list-table-wrap">
<table class="table table-sm mb-0 list-table">
<thead><tr>
<th>域名</th>
<th>关联配置</th>
<?php if ($cloakCfEnabled): ?><th>Cloudflare</th><th>Nameserver</th><?php endif; ?>
<th style="width:140px;">操作</th>
</tr></thead>
<tbody>
<?php foreach ($cloakSiteDomains as $dom):
$cfStatus = $dom['cf_status'] ?? 'local';
$cfLabel = CloudflareConfig::statusLabel($cfStatus);
$badgeCls = 'secondary';
if ($cfStatus === 'pending_ns') { $badgeCls = 'warning'; }
elseif ($cfStatus === 'manual') { $badgeCls = 'secondary'; }
elseif ($cfStatus === 'provisioning') { $badgeCls = 'info'; }
elseif ($cfStatus === 'ready') { $badgeCls = 'success'; }
elseif ($cfStatus === 'error') { $badgeCls = 'danger'; }
$nsList = CloudflareConfig::parseNameservers($dom['cf_nameservers'] ?? '');
$delConfirm = '确定删除该域名?';
if ($cloakCfEnabled && !empty($dom['cf_zone_id'])) {
$delConfirm .= '将同时删除 Cloudflare Zone(不可恢复)。';
}
?>
<tr>
<td><span class="domain-host-badge" title="<?php echo htmlspecialchars($dom['hostname'], ENT_QUOTES); ?>"><?php echo htmlspecialchars($dom['hostname']); ?></span></td>
<td><?php
$boundConfigs = $domainConfigMap[(int) $dom['id']] ?? [];
if ($boundConfigs === []) {
echo '<span class="text-muted">未关联</span>';
} else {
foreach ($boundConfigs as $cfgName) {
echo '<span class="badge badge-light border mr-1">' . htmlspecialchars($cfgName) . '</span>';
}
}
?></td>
<?php if ($cloakCfEnabled): ?>
<td>
<span class="badge badge-<?php echo $badgeCls; ?>"><?php echo htmlspecialchars($cfLabel); ?></span>
<?php if ($cfStatus === 'error' && !empty($dom['cf_error'])): ?>
<br><small class="text-danger"><?php echo htmlspecialchars($dom['cf_error']); ?></small>
<?php endif; ?>
</td>
<td style="font-size:.8rem;max-width:220px;">
<?php if ($nsList): ?>
<?php foreach ($nsList as $ns): ?><div><span class="cell-truncate domain-host-badge" style="display:block;max-width:200px;" title="<?php echo htmlspecialchars($ns, ENT_QUOTES); ?>"><?php echo htmlspecialchars($ns); ?></span></div><?php endforeach; ?>
<?php else: ?>
<span class="text-muted">—</span>
<?php endif; ?>
</td>
<?php endif; ?>
<td class="text-nowrap">
<div class="action-btn-group">
<?php if ($cloakCfEnabled && in_array($cfStatus, ['pending_ns', 'provisioning', 'ready', 'error'], true)): ?>
<form method="post" action="<?php echo $current_name; ?>" class="d-inline">
<input type="hidden" name="action" value="check_domain">
<input type="hidden" name="domain_id" value="<?php echo (int)$dom['id']; ?>">
<button type="submit" class="btn btn-sm btn-outline-primary">检测/刷新</button>
</form>
<?php endif; ?>
<a href="<?php echo $current_name; ?>?action=delete_domain&amp;domain_id=<?php echo (int)$dom['id']; ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('<?php echo htmlspecialchars($delConfirm, ENT_QUOTES); ?>');">删除</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<?php else: ?>
<!-- ===================== 编辑视图 ===================== -->
<a href="<?php echo $current_name; ?>" class="back-link">← 返回配置列表</a>
<div class="card shadow-sm" style="border-radius:8px;overflow:hidden;">
<div class="card-header-custom">
<h5><?php echo $isNewConfigDraft ? '新增配置' : '编辑配置'; ?><?php echo htmlspecialchars($check_name); ?><?php if ($isNewConfigDraft): ?> <span class="badge badge-warning" style="font-size:.75rem;vertical-align:middle;">未保存</span><?php endif; ?><?php if ($isNewConfigDraft && $configCopySource !== ''): ?> <span class="badge badge-info" style="font-size:.75rem;vertical-align:middle;">从 <?php echo htmlspecialchars($configCopySource); ?> 复制</span><?php endif; ?></h5>
<div>
<?php if (empty($_SESSION["check_result"]) || $_SESSION["check_result"] != "true"): ?>
<a href="<?php echo $current_name; ?>?action=setuvi&res=true&name=<?php echo $check_name; ?>" class="btn btn-sm btn-outline-success mr-1">设置当前浏览器为真实访客</a>
<?php else: ?>
<a href="<?php echo $current_name; ?>?action=setuvi&res=false&name=<?php echo $check_name; ?>" class="btn btn-sm btn-outline-warning mr-1">清除访客状态</a>
<?php endif; ?>
</div>
</div>
<form action="<?php echo $current_name; ?>?action=submit" method="post" id="configForm">
<div style="padding:16px;">
<!-- ===== 基本设置 ===== -->
<div class="form-section">
<button type="button" class="section-toggle" data-target="sec-basic">
基本设置 <span class="arrow">▼</span>
</button>
<div id="sec-basic">
<table class="table table-sm mb-0">
<tr>
<td class="form-label-cell"><strong>配置名称</strong></td>
<td class="form-val-cell">
<input type="text" name="check_name" class="form-control form-control-sm" value="<?php echo htmlspecialchars($check_name); ?>" style="max-width:300px;">
<small class="text-muted"><?php echo ($isNewConfigDraft && $configCopySource !== '') ? '已按「' . htmlspecialchars($configCopySource) . '」预填,请确认配置名称后保存' : '示例:f123、d258'; ?></small>
</td>
</tr>
<tr>
<td class="form-label-cell">
<strong>关联域名</strong><br>
<small class="text-muted">请先在上方「站点域名」中添加并在宝塔创建该站点与 SSL</small>
</td>
<td class="form-val-cell">
<select name="site_domain_id" class="form-control form-control-sm" style="max-width:360px;">
<option value="0"<?php echo $selectedDomainId === 0 ? ' selected' : ''; ?>>
不指定(广告链接使用当前后台域名:<?php echo htmlspecialchars($dashboardDefaultHost); ?>
</option>
<?php if (count($cloakSiteDomains) >= 2 && ($isNewConfigDraft || $selectedDomainId === 0)): ?>
<option value="random">随机域名(从全部站点域名中随机分配)</option>
<?php endif; ?>
<?php foreach ($cloakSiteDomains as $dom):
$boundOnDom = $domainConfigMap[(int) $dom['id']] ?? [];
$boundCount = count($boundOnDom);
?>
<option value="<?php echo (int) $dom['id']; ?>"<?php echo $selectedDomainId === (int) $dom['id'] ? ' selected' : ''; ?>>
<?php echo htmlspecialchars($dom['hostname']); ?>
<?php if ($boundCount > 0): ?>
<?php echo $boundCount; ?> 个配置已关联)
<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
<?php if (empty($cloakSiteDomains)): ?>
<small class="text-warning d-block mt-1">尚未添加任何域名,保存后广告链接将使用 <?php echo htmlspecialchars($dashboardDefaultHost); ?></small>
<?php endif; ?>
</td>
</tr>
<tr>
<td class="form-label-cell">
是否开启 DEBUG 模式<br>
<small class="text-muted">开启后访问落地链接将显示格式化调试页(判定结果、理由、代码行、访客信息),不会跳转真实页/安全页;生产环境请关闭</small>
</td>
<td class="form-val-cell"><?php echo onoff_radio('debug_mode', $_d('CLOAK_DEBUG_MODE','OFF')); ?></td>
</tr>
<tr>
<td class="form-label-cell">屏蔽模式</td>
<td class="form-val-cell">
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm btn-outline-primary <?php if(SHOW_SITE_MODE_SWITCH == 'ip_check') echo 'active'; ?>">
<input type="radio" name="methods" value="ip_check" <?php if(SHOW_SITE_MODE_SWITCH == 'ip_check') echo 'checked'; ?>> 正常屏蔽模式
</label>
<label class="btn btn-sm btn-outline-primary <?php if(SHOW_SITE_MODE_SWITCH == 'fp') echo 'active'; ?>">
<input type="radio" name="methods" value="fp" <?php if(SHOW_SITE_MODE_SWITCH == 'fp') echo 'checked'; ?>> 真实页模式
</label>
<label class="btn btn-sm btn-outline-primary <?php if(SHOW_SITE_MODE_SWITCH == 'zp') echo 'active'; ?>">
<input type="radio" name="methods" value="zp" <?php if(SHOW_SITE_MODE_SWITCH == 'zp') echo 'checked'; ?>> 安全页模式
</label>
</div>
</td>
</tr>
<tr>
<td class="form-label-cell">
国家条件<br>
<small class="text-muted">开启后仅允许填写的国家访问真实页;关闭则不做国家拦截。</small>
</td>
<td class="form-val-cell">
<?php
$countryCodesVal = defined('SHOW_SITE_COUNTRY')
? CountryAllowlist::formatForConfig((string) SHOW_SITE_COUNTRY)
: '';
$countryOnVal = $countryCodesVal !== '' ? 'ON' : 'OFF';
echo onoff_radio('country_on', $countryOnVal, '开启', '关闭');
?>
</td>
</tr>
<tr id="countryCodesRow"<?php echo $countryOnVal === 'ON' ? '' : ' style="display:none;"'; ?>>
<td class="form-label-cell">允许的国家代码(多个使用英文逗号隔开)</td>
<td class="form-val-cell">
<input type="text" name="country" class="form-control form-control-sm" value="<?php echo htmlspecialchars($countryCodesVal); ?>" placeholder="如:US,GB,CA(小写会自动转大写;支持中英文逗号、分号)" style="max-width:360px;">
</td>
</tr>
<tr>
<td class="form-label-cell">广告策略 ID</td>
<td class="form-val-cell"><input type="text" name="costm_ip_score" class="form-control form-control-sm" value="<?php echo COSTM_IP_SCORE; ?>" style="max-width:360px;"></td>
</tr>
<tr>
<td class="form-label-cell">安全页跳转方式</td>
<td class="form-val-cell">
<select name="redirect_method" class="form-control form-control-sm" style="max-width:320px;">
<option value="curl" <?php if(CLOAK_REDIRECT_METHOD=='curl') echo 'selected'; ?>>抓取显示(链接不变)</option>
<option value="302" <?php if(CLOAK_REDIRECT_METHOD=='302') echo 'selected'; ?>>302 跳转(域名改变)</option>
<option value="js" <?php if(CLOAK_REDIRECT_METHOD=='js') echo 'selected'; ?>>JS 跳转</option>
</select>
</td>
</tr>
<tr>
<td class="form-label-cell">安全页链接(留空显示空白页)</td>
<td class="form-val-cell"><input type="text" name="DB_zp" class="form-control form-control-sm" value="<?php echo DB_ZP; ?>" placeholder="https://..." style="max-width:420px;"></td>
</tr>
<tr>
<td class="form-label-cell">真实页跳转方式</td>
<td class="form-val-cell">
<select name="show_content" class="form-control form-control-sm" style="max-width:320px;">
<option value="curl" <?php if(CLOAK_SHOW_CONTENT=='curl') echo 'selected'; ?>>抓取显示(链接不变)</option>
<option value="302" <?php if(CLOAK_SHOW_CONTENT=='302') echo 'selected'; ?>>302 跳转(域名改变)</option>
<option value="js" <?php if(CLOAK_SHOW_CONTENT=='js') echo 'selected'; ?>>JS 跳转</option>
</select>
</td>
</tr>
<tr>
<td class="form-label-cell">真实页链接地址</td>
<td class="form-val-cell" id="db_fp">
<?php
if (DB_FP) {
foreach (DB_FP as $idx => $link) {
if ($idx == 0) {
echo '<input type="text" name="DB_fp[]" class="form-control form-control-sm mb-1" value="' . htmlspecialchars($link) . '" style="max-width:420px;" data-idx="1"> <a href="javascript:void(0);" id="addNewLinkBtn" class="btn btn-sm btn-outline-secondary">+ 新增</a>';
} else {
echo '<br><input type="text" name="DB_fp[]" class="form-control form-control-sm mt-1" value="' . htmlspecialchars($link) . '" style="max-width:420px;" data-idx="' . ($idx+1) . '"> <a href="javascript:void(0);" class="removeLinkBtn btn btn-sm btn-outline-danger">删除</a>';
}
}
} else {
echo '<input type="text" name="DB_fp[]" class="form-control form-control-sm" value="" style="max-width:420px;" data-idx="1"> <a href="javascript:void(0);" id="addNewLinkBtn" class="btn btn-sm btn-outline-secondary">+ 新增</a>';
}
?>
</td>
</tr>
</table>
</div>
</div>
<!-- ===== IP 与名单 ===== -->
<div class="form-section">
<button type="button" class="section-toggle collapsed" data-target="sec-ip">
IP黑白单管理 <span class="arrow">▼</span>
</button>
<div id="sec-ip" style="display:none;">
<table class="table table-sm mb-0">
<tr>
<td class="form-label-cell">白名单 IP 组(允许访问真实页)</td>
<td class="form-val-cell">
<select name="whitelist_group_id" class="form-control form-control-sm" style="max-width:320px;">
<option value="0" <?php if($currentWhitelistGroupId===0) echo 'selected'; ?>>无</option>
<?php foreach($whiteGroups as $wg): ?>
<option value="<?php echo (int)$wg['id']; ?>" <?php if($currentWhitelistGroupId===(int)$wg['id']) echo 'selected'; ?>><?php echo htmlspecialchars($wg['name']); ?> (ID <?php echo $wg['id']; ?>)</option>
<?php endforeach; ?>
</select>
<small><a href="/admin_ip_manager.php?type=white" target="_blank">管理白名单</a></small>
</td>
</tr>
<tr>
<td class="form-label-cell">黑名单 IP 组(命中则拦截)</td>
<td class="form-val-cell">
<select name="blacklist_group_id" class="form-control form-control-sm" style="max-width:320px;">
<option value="0" <?php if($currentBlacklistGroupId===0) echo 'selected'; ?>>无</option>
<?php foreach($blackGroups as $bg): ?>
<option value="<?php echo (int)$bg['id']; ?>" <?php if($currentBlacklistGroupId===(int)$bg['id']) echo 'selected'; ?>><?php echo htmlspecialchars($bg['name']); ?> (ID <?php echo $bg['id']; ?>)</option>
<?php endforeach; ?>
</select>
<small><a href="/admin_ip_manager.php?type=black" target="_blank">管理黑名单</a></small>
</td>
</tr>
<tr>
<td class="form-label-cell">自动黑名单(达到次数后自动加入黑名单组)</td>
<td class="form-val-cell">
<div class="d-flex align-items-center gap-2" style="gap:10px;">
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm btn-outline-primary <?php if(AUTO_BLACK=='ON') echo 'active'; ?>">
<input type="radio" name="auto_black" id="auto_black_on" value="ON" <?php if(AUTO_BLACK=='ON') echo 'checked'; ?>> 开启
</label>
<label class="btn btn-sm btn-outline-primary <?php if(AUTO_BLACK!='ON') echo 'active'; ?>">
<input type="radio" name="auto_black" id="auto_black_off" value="OFF" <?php if(AUTO_BLACK!='ON') echo 'checked'; ?>> 关闭
</label>
</div>
<span class="ml-2">触发次数:</span>
<input type="number" name="auto_black_times" value="<?php echo AUTO_BLACK_TIMES; ?>" class="form-control form-control-sm" style="width:80px;" min="1">
</div>
</td>
</tr>
<tr>
<td class="form-label-cell">白名单链接参数(携带此参数直接放行,格式:param=value</td>
<td class="form-val-cell"><input type="text" name="white_params" class="form-control form-control-sm" value="<?php echo htmlspecialchars(WHITE_PARAMS); ?>" style="max-width:280px;" placeholder="如:token=abc123"></td>
</tr>
</table>
</div>
</div>
<!-- ===== 设备与语言 ===== -->
<div class="form-section">
<button type="button" class="section-toggle collapsed" data-target="sec-device">
设备与语言过滤 <span class="arrow">▼</span>
</button>
<div id="sec-device" style="display:none;">
<table class="table table-sm mb-0">
<tr>
<td class="form-label-cell">屏蔽无 Referer 来源(防爬虫)</td>
<td class="form-val-cell"><?php echo onoff_radio('v_referer', CLOAK_V_REFERER); ?></td>
</tr>
<tr>
<td class="form-label-cell">仅允许移动端访问</td>
<td class="form-val-cell"><?php echo onoff_radio('mobile_on', CLOAK_MOBILE_ON); ?></td>
</tr>
<tr>
<td class="form-label-cell">屏蔽中文浏览器</td>
<td class="form-val-cell"><?php echo onoff_radio('zh_on', CLOAK_ZH_ON); ?></td>
</tr>
<tr>
<td class="form-label-cell">仅允许 iOS / Android 系统</td>
<td class="form-val-cell"><?php echo onoff_radio('os_on', CLOAK_OS_ON); ?></td>
</tr>
<tr>
<td class="form-label-cell">保留跳转链接参数</td>
<td class="form-val-cell"><?php echo onoff_radio('keep_params', KEEP_PARAMS); ?></td>
</tr>
<tr>
<td class="form-label-cell">屏蔽 iPhone 型号</td>
<td class="form-val-cell">
<select name="iphone_model" class="form-control form-control-sm" style="max-width:260px;">
<option value="0" <?php if(IPHONE_MODEL=='0') echo 'selected'; ?>>不屏蔽</option>
<option value="8" <?php if(IPHONE_MODEL=='8') echo 'selected'; ?>>屏蔽 iPhone 8 及以下</option>
<option value="10" <?php if(IPHONE_MODEL=='10') echo 'selected'; ?>>屏蔽 iPhone X 及以下</option>
<option value="11" <?php if(IPHONE_MODEL=='11') echo 'selected'; ?>>屏蔽 iPhone 11 及以下</option>
<option value="12" <?php if(IPHONE_MODEL=='12') echo 'selected'; ?>>屏蔽 iPhone 12 及以下</option>
</select>
</td>
</tr>
<tr>
<td class="form-label-cell">
虚拟设备检测<br>
<small class="text-muted">简单:全平台自动化/爬虫;标准:+环境一致性(移动无触摸等);严格:+移动GPU校验与传感器(约180ms</small>
</td>
<td class="form-val-cell">
<select name="is_virtual" class="form-control form-control-sm" style="max-width:260px;">
<option value="0" <?php if(IS_VIRTUAL=='0') echo 'selected'; ?>>不检测</option>
<option value="1" <?php if(IS_VIRTUAL=='1') echo 'selected'; ?>>简单</option>
<option value="2" <?php if(IS_VIRTUAL=='2') echo 'selected'; ?>>标准</option>
<option value="3" <?php if(IS_VIRTUAL=='3') echo 'selected'; ?>>严格</option>
</select>
</td>
</tr>
</table>
</div>
</div>
<!-- ===== 广告来源限制 ===== -->
<div class="form-section">
<button type="button" class="section-toggle collapsed" data-target="sec-ad-source">
广告来源限制 <span class="arrow">▼</span>
</button>
<div id="sec-ad-source" style="display:none;">
<table class="table table-sm mb-0">
<tr>
<td class="form-label-cell">Facebook</td>
<td class="form-val-cell"><?php echo onoff_radio('ad_fb', $_d('CLOAK_AD_FB', 'OFF')); ?></td>
</tr>
<tr>
<td class="form-label-cell">Google</td>
<td class="form-val-cell"><?php echo onoff_radio('ad_google', $_d('CLOAK_AD_GOOGLE', 'OFF')); ?></td>
</tr>
<tr>
<td class="form-label-cell">TikTok</td>
<td class="form-val-cell"><?php echo onoff_radio('ad_tiktok', $_d('CLOAK_AD_TIKTOK', 'OFF')); ?></td>
</tr>
<tr>
<td class="form-label-cell">严格模式</td>
<td class="form-val-cell">
<?php echo onoff_radio('ad_strict', $_d('CLOAK_AD_STRICT', 'OFF')); ?>
<div class="text-muted mt-1" style="font-size:.78rem;">开启后,未通过广告来源验证的访问将直接跳转安全页;关闭时仅追加「未确定流量来源」并继续后续判定,日志行显示为黄色。</div>
</td>
</tr>
<tr>
<td class="form-label-cell">强验证参数 ad_spm_id</td>
<td class="form-val-cell">
<?php $spmShow = $_d('CLOAK_AD_SPM_ID', ''); ?>
<?php if ($spmShow !== ''): ?>
<code><?php echo htmlspecialchars($spmShow); ?></code>
<div class="text-muted mt-1" style="font-size:.78rem;">保存配置且开启任一广告平台时自动生成,写入广告链接用于强验证。</div>
<?php else: ?>
<span class="text-muted">保存并开启任一广告平台后自动生成</span>
<?php endif; ?>
</td>
</tr>
</table>
</div>
</div>
<!-- ===== 高级设置 ===== -->
<div class="form-section">
<button type="button" class="section-toggle collapsed" data-target="sec-advanced">
高级设置 <span class="arrow">▼</span>
</button>
<div id="sec-advanced" style="display:none;">
<table class="table table-sm mb-0">
<tr>
<td class="form-label-cell">指纹风险系数(0=关闭,最大90</td>
<td class="form-val-cell">
<input type="number" name="risk_number" class="form-control form-control-sm" value="<?php echo CLOAK_RISK_NUMBER; ?>" style="width:100px;" min="0" max="90">
<div class="text-muted mt-2" style="font-size:.78rem;line-height:1.55;">
数值越高,拦截越严格。访客风险分达到或超过此值时跳转安全页,低于则进入真实页。
设为 <strong>0</strong> 表示关闭二次风控。
</div>
<details class="mt-2" style="font-size:.78rem;line-height:1.55;">
<summary style="cursor:pointer;color:#0c5460;font-weight:600;">推荐设置(与下方「加强判断」配合)</summary>
<div class="text-muted mt-2">
<p class="mb-2">请根据「加强判断」开关选择对应数值,两种模式<strong>不宜使用同一分数</strong>。</p>
<table class="table table-sm table-bordered mb-2" style="font-size:.76rem;background:#fff;">
<thead class="thead-light">
<tr><th>加强判断</th><th>建议分数</th><th>说明</th></tr>
</thead>
<tbody>
<tr>
<td><strong>OFF</strong></td>
<td><strong>68</strong></td>
<td>默认推荐,兼顾流量与防护,误拦较少</td>
</tr>
<tr>
<td><strong>ON</strong></td>
<td><strong>52</strong></td>
<td>识别更严,适合需要更高防护的场景</td>
</tr>
</tbody>
</table>
<p class="mb-1"><strong>微调:</strong></p>
<ul class="mb-0 pl-3">
<li>真实用户被误拦 → 分数 <strong>+3 ~ +5</strong></li>
<li>可疑访问漏过 → 分数 <strong>3 ~ 5</strong></li>
<li>可在「访问日志 → 显示更多 → 判定详情」查看每条记录的风险分与拦截原因</li>
</ul>
</div>
</details>
</td>
</tr>
<tr>
<td class="form-label-cell">加强判断</td>
<td class="form-val-cell">
<?php echo onoff_radio('risk_enhanced', $_d('CLOAK_RISK_ENHANCED', 'OFF')); ?>
<div class="text-muted mt-1" style="font-size:.78rem;line-height:1.55;">
开启后识别更精细,可拦截更多自动化访问;老旧设备或弱网环境下可能受影响。<br>
建议配合上方风险系数:OFF 用 <strong>68</strong>ON 用 <strong>52</strong>。
</div>
</td>
</tr>
<tr>
<td class="form-label-cell">
临时链接参数有效时长(分钟,0=关闭)<br>
<small class="text-muted">通过广告来源限制规则(ad_spm_id / clid / Referer / UA 等)后开始计时,有效时长内放行;到期重新判断。未开启广告平台时仍按 clid/ad_spm_id 触发。</small>
</td>
<td class="form-val-cell">
<div class="d-flex align-items-center" style="gap:8px;">
<input type="number" name="url_args_timeout" class="form-control form-control-sm" value="<?php echo CLOAK_URL_ARGS_TIMEOUT; ?>" style="width:90px;" min="0"> 分钟
<button type="button" class="btn btn-sm btn-outline-warning" onclick="delUrlArgs()">重置计时</button>
</div>
</td>
</tr>
<tr>
<td class="form-label-cell">记录访客日志</td>
<td class="form-val-cell">
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm btn-outline-primary <?php if(WRITE_LOG=='1') echo 'active'; ?>">
<input type="radio" name="write_log" value="1" <?php if(WRITE_LOG=='1') echo 'checked'; ?>> 开启
</label>
<label class="btn btn-sm btn-outline-primary <?php if(WRITE_LOG!='1') echo 'active'; ?>">
<input type="radio" name="write_log" value="0" <?php if(WRITE_LOG!='1') echo 'checked'; ?>> 关闭
</label>
</div>
</td>
</tr>
</table>
</div>
</div>
</div><!-- /padding -->
<!-- 底部操作栏 -->
<div class="sticky-actions">
<button type="submit" class="btn btn-primary">保存配置</button>
<a href="<?php echo $current_name . '?action=edit_config&name=' . $check_name; ?>" class="btn btn-outline-secondary">重置</a>
<a href="/table" target="_blank" class="btn btn-outline-info">查看日志</a>
<a href="/log_analytics.php" target="_blank" class="btn btn-outline-info">数据分析</a>
<a href="/share_log.php" target="_blank" class="btn btn-outline-info">分享日志</a>
</div>
</form>
</div>
<?php endif; ?>
</div><!-- /page-body -->
<script>
$(function() {
// 手风琴展开/折叠
$('.section-toggle').on('click', function() {
var target = '#' + $(this).data('target');
$(this).toggleClass('collapsed');
$(target).slideToggle(180);
});
function syncCountryCodesRow() {
var on = $('input[name="country_on"]:checked').val() === 'ON';
$('#countryCodesRow').toggle(on);
if (!on) {
$('input[name="country"]').val('');
}
}
$(document).on('change', 'input[name="country_on"]', syncCountryCodesRow);
syncCountryCodesRow();
// 动态添加链接
var linkIdx = 1;
$(document).on('click', '#addNewLinkBtn', function() {
linkIdx++;
var html = '<br><input type="text" name="DB_fp[]" class="form-control form-control-sm mt-1" value="" style="max-width:420px;" data-idx="' + linkIdx + '"> <a href="javascript:void(0);" class="removeLinkBtn btn btn-sm btn-outline-danger">删除</a>';
$(this).after(html);
});
$(document).on('click', '.removeLinkBtn', function() {
$(this).prev('br').remove();
$(this).prev('input').remove();
$(this).remove();
});
// 自动黑名单校验
$('#configForm').on('submit', function() {
var on = $('#auto_black_on').is(':checked');
var gid = parseInt($('select[name=blacklist_group_id]').val(), 10) || 0;
if (on && gid <= 0) {
alert('开启「自动黑名单」时,必须先选择「黑名单 IP 组」。');
return false;
}
return true;
});
// 广告链接复制
$(document).on('click', '.btn-copy-ad', function() {
var text = $(this).attr('data-copy') || '';
if (!text) return;
var btn = this;
var done = function() {
var orig = btn.textContent;
btn.textContent = '已复制';
setTimeout(function() { btn.textContent = orig; }, 1200);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done).catch(function() {
window.prompt('请手动复制链接:', text);
});
} else {
window.prompt('请手动复制链接:', text);
}
});
$('#checkAllConfigs').on('change', function() {
$('.config-row-check').prop('checked', this.checked);
});
$(document).on('change', '.config-row-check', function() {
var all = $('.config-row-check');
var checked = all.filter(':checked').length;
$('#checkAllConfigs').prop('checked', all.length > 0 && checked === all.length);
});
});
function dashBatchDeleteConfirm() {
var checked = document.querySelectorAll('.config-row-check:checked');
if (checked.length === 0) {
alert('请至少选择一个配置');
return false;
}
return confirm('确定删除选中的 ' + checked.length + ' 个配置?此操作不可恢复。');
}
function delUrlArgs() {
if (!confirm('确认重置临时链接参数计时?')) return;
window.location.href = '<?php echo $current_name; ?>?action=delUrlArgs&check_name=<?php echo urlencode($check_name); ?>';
}
</script>
</body>
</html>