NewHome/admin/visits.php
2026-09-14 22:06:21 +08:00

486 lines
25 KiB
PHP
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
/**
* admin/visits.php —— 风控管理
* 访问日志查询(精简 / 完整两种模式 + IP / 关键词筛选 + 分页)与 IP 黑名单管理。
* 黑名单命中规则后前台与后台(含登录页)请求均被直接拒绝(与前台同一风控提示页)。
*/
$P = '../';
require_once __DIR__ . '/_guard.php';
require_once __DIR__ . '/_visits_lib.php';
$pdo = db();
function settings_set(string $key, string $value): void
{
$st = db()->prepare('INSERT INTO settings (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value');
$st->execute([$key, $value]);
}
/* 批量删除黑名单(按 id 列表),同步清除单 IP 规则的风控计数;返回删除条数 */
function bl_delete_ids(PDO $pdo, array $ids): int
{
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), function ($v) {
return $v > 0;
})));
if (!$ids)
return 0;
$ph = implode(',', array_fill(0, count($ids), '?'));
$st = $pdo->prepare('SELECT rule FROM ip_blacklist WHERE id IN (' . $ph . ')');
$st->execute($ids);
$rules = $st->fetchAll();
$pdo->prepare('DELETE FROM ip_blacklist WHERE id IN (' . $ph . ')')->execute($ids);
foreach ($rules as $r) {
if (filter_var((string) $r['rule'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
risk_clear_for_ip((string) $r['rule']);
}
}
return count($rules);
}
/* ---------- POST黑名单增删 / 清空日志 / 风控规则 ---------- */
$msgKind = '';
$msgText = '';
$banChanged = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!csrf_verify()) {
$msgKind = 'err';
$msgText = '安全校验失败,请刷新页面重试。';
} else {
$act = (string) ($_POST['act'] ?? '');
switch ($act) {
case 'ban_add':
$rule = trim((string) ($_POST['rule'] ?? ''));
$note = trim((string) ($_POST['note'] ?? ''));
if ($rule === '') {
$msgKind = 'err';
$msgText = '请填写要拦截的 IP / 网段。';
break;
}
$parsed = blacklist_parse_rule($rule);
if (!$parsed[0]) {
$msgKind = 'err';
$msgText = $parsed[3];
break;
}
$st = $pdo->prepare('INSERT INTO ip_blacklist (rule, note, created_at, source) VALUES (?,?,?,?)');
$st->execute([$rule, $note, date('Y-m-d H:i:s'), 'manual']);
$banChanged = true;
$msgKind = 'ok';
$msgText = '黑名单规则已添加,匹配的访问将立即被拒绝。';
break;
case 'ban_del':
$id = max(0, (int) ($_POST['id'] ?? 0));
if ($id > 0) {
$bs = $pdo->prepare('SELECT rule FROM ip_blacklist WHERE id = ? LIMIT 1');
$bs->execute([$id]);
$brow = $bs->fetch();
$pdo->prepare('DELETE FROM ip_blacklist WHERE id = ?')->execute([$id]);
// 解封单 IP 时同步清除其风控计数,避免“刚解封又被历史计数封回”
if ($brow && filter_var((string) $brow['rule'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
risk_clear_for_ip((string) $brow['rule']);
}
$banChanged = true;
$msgKind = 'ok';
$msgText = '黑名单规则已移除。';
} else {
$msgKind = 'err';
$msgText = '无效的黑名单记录。';
}
break;
case 'log_clear':
@file_put_contents(ACCESS_LOG_FILE, '', LOCK_EX);
$msgKind = 'ok';
$msgText = '访问日志已清空data/vistors.log。';
break;
case 'risk_save':
$clip = function ($v, $d) {
$raw = trim((string) ($v ?? ''));
$x = ($raw === '') ? (int) $d : (int) $raw;
return max(1, min(1000000, $x));
};
settings_set('risk_login_on', isset($_POST['r_login_on']) ? '1' : '0');
settings_set('risk_login_n', (string) $clip($_POST['r_login_n'] ?? null, 10));
settings_set('risk_404_on', isset($_POST['r_404_on']) ? '1' : '0');
settings_set('risk_404_n', (string) $clip($_POST['r_404_n'] ?? null, 100));
settings_set('risk_rate_on', isset($_POST['r_rate_on']) ? '1' : '0');
settings_set('risk_rate_n', (string) $clip($_POST['r_rate_n'] ?? null, 120));
settings_set('risk_tip_main', trim((string) ($_POST['r_tip_main'] ?? '')));
settings_set('risk_tip_sub', trim((string) ($_POST['r_tip_sub'] ?? '')));
$msgKind = 'ok';
$msgText = '风控规则已保存并即时生效。';
break;
case 'risk_clear':
risk_clear_all();
$msgKind = 'ok';
$msgText = '全部风险计数已清空(黑名单本身不受影响)。';
break;
case 'loginlog_clear':
login_log_clear();
$msgKind = 'ok';
$msgText = '登录日志已清空(含成功与失败记录)。';
break;
case 'ban_batch':
$n = bl_delete_ids($pdo, (array) ($_POST['bid'] ?? []));
$banChanged = true;
$msgKind = $n > 0 ? 'ok' : 'err';
$msgText = $n > 0 ? '已批量移除 ' . $n . ' 条黑名单规则(对应单 IP 计数已清除)。' : '未勾选任何需要移除的规则。';
break;
case 'ban_clear_auto':
$rows = $pdo->query("SELECT rule FROM ip_blacklist WHERE source = 'auto'")->fetchAll();
$cnt = count($rows);
$pdo->exec("DELETE FROM ip_blacklist WHERE source = 'auto'");
foreach ($rows as $r) {
if (filter_var((string) $r['rule'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
risk_clear_for_ip((string) $r['rule']);
}
}
blacklist_rules(true);
$banChanged = true;
$msgKind = 'ok';
$msgText = '已一键移除全部风控自动封禁(共 ' . $cnt . ' 条),相关 IP 风控计数已同步清除。';
break;
case 'ban_clear_all':
$pdo->exec('DELETE FROM ip_blacklist');
risk_clear_all();
blacklist_rules(true);
$banChanged = true;
$msgKind = 'ok';
$msgText = '已清空全部黑名单,风险计数一并清零。';
break;
default:
$msgKind = 'err';
$msgText = '未知操作。';
}
}
}
/* ---------- 概要统计数据来源data/vistors.log ---------- */
$visStat = vis_stat_all();
$statTotal = $visStat['total'];
$statIps = $visStat['ips'];
$statToday = $visStat['today'];
$statBan = $visStat['ban'];
/* ---------- 筛选参数(供表单回显)与访问记录查询 ---------- */
$mode = (string) ($_GET['mode'] ?? 'full');
if (!in_array($mode, ['full', 'compact'], true)) {
$mode = 'full';
}
$ipF = substr(trim((string) ($_GET['ip'] ?? '')), 0, 64);
$q = substr(trim((string) ($_GET['q'] ?? '')), 0, 100);
$page = max(1, (int) ($_GET['page'] ?? 1));
$perPage = 30;
$visAccess = vis_query_access($mode, $ipF, $q, $page, $perPage);
/* ---------- 当前访问者信息(用于黑名单自查提示) ---------- */
$curIp = client_ip();
if ($curIp === '') {
$curIp = '未知';
}
$curHit = null;
if ($curIp !== '未知') {
if ($banChanged) {
blacklist_rules(true);
}
$curHit = blacklist_ip_hit($curIp);
}
/* ---------- 自动风控规则配置与当前 IP 计数 ---------- */
$rcfg = risk_rule_cfg();
$curRisk = ($curIp !== '未知') ? risk_row($curIp) : null;
/* ---------- 登录信息:当前会话 / 历史成功登录 / 登录失败按「IP × 日期」聚合 ---------- */
$lgStat = login_log_stat();
$lgRecent = login_log_recent_ok(15);
$lgFailF = substr(trim((string) ($_GET['lf'] ?? '')), 0, 64);
$lgFailData = vis_query_loginfail($lgFailF, 200);
$curAdmin = (string) ($_SESSION['admin'] ?? '');
$curLoginAt = (string) ($_SESSION['login_at'] ?? '');
$curLoginIp = (string) ($_SESSION['login_ip'] ?? ($curIp !== '未知' ? $curIp : ''));
$curLoginUa = (string) ($_SESSION['login_ua'] ?? '');
$curLoginDevice = (string) ($_SESSION['login_device'] ?? '');
if ($curLoginDevice === '') {
$curLoginDevice = login_ua_brief((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''));
}
/* ---------- 黑名单规则(直接读表展示,便于核对备注与来源) ---------- */
$banRows = $pdo->query('SELECT id, rule, note, created_at, source FROM ip_blacklist ORDER BY id DESC')->fetchAll();
$banAutoCount = 0;
foreach ($banRows as $b) {
if (((string) ($b['source'] ?? 'manual')) === 'auto')
$banAutoCount++;
}
layout_head('风控管理');
admin_topbar('visits');
?>
<main class="page-main">
<div class="wrap page-body">
<div class="admin-head">
<div class="ah-main">
<h2 class="ah-title">风控管理</h2>
<p class="ah-sub">记录访客对本站的动态请求(含后台),完整模式逐条展示访问明细,精简模式按 IP 汇总次数;黑名单 / 自动风控对所有访问统一生效,命中即返回 403 风控提示页,后台与登录页不例外。
</p>
</div>
</div>
<?php if ($msgText !== ''): ?>
<script>window.hpToastMsg = <?= json_encode(['kind' => $msgKind === 'ok' ? 'ok' : 'err', 'text' => $msgText], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
<?php endif; ?>
<!-- 概要统计 -->
<div class="fieldset-card">
<div class="fs-title">访问概况</div>
<div class="vis-stats">
<div class="vis-stat"><b><?= $statTotal ?></b><span>累计访问</span></div>
<div class="vis-stat"><b><?= $statIps ?></b><span>独立 IP</span></div>
<div class="vis-stat"><b><?= $statToday ?></b><span>今日访问</span></div>
<div class="vis-stat"><b><?= $statBan ?></b><span>黑名单规则</span></div>
</div>
<form method="post" data-confirm="确认清空全部访问日志?此操作不可恢复。" style="margin-top:14px">
<?= csrf_field() ?><input type="hidden" name="act" value="log_clear">
<button type="submit" class="btn btn-sm btn-danger">清空访问日志</button>
<span class="tip" style="margin-left:8px">日志按行追加写入 <code>data/vistors.log</code>(制表符分隔:时间 / IP / 请求方式 / 响应码 /
URL完整保留不设上限可自行归档或删除该文件。</span>
</form>
</div>
<!-- 登录信息(当前会话 + 历史成功登录) -->
<div class="fieldset-card">
<div class="fs-title">登录信息</div>
<div class="vis-stats">
<div class="vis-stat"><b><?= he($curAdmin) ?></b><span>当前用户</span></div>
<div class="vis-stat"><b
style="font-size:15px"><?= he($curLoginAt !== '' ? $curLoginAt : '本次会话未记录') ?></b><span>本次登录时间</span></div>
<div class="vis-stat"><b style="font-size:15px"><?= he($curLoginIp !== '' ? $curLoginIp : '未知') ?></b><span>本次登录
IP</span></div>
<div class="vis-stat"><b style="font-size:15px"><?= he($curLoginDevice) ?></b><span>本次登录设备</span></div>
</div>
<p class="tip" style="margin:12px 0 4px">历史登录记录(最近 <?= count($lgRecent) ?> 条成功登录;今日成功
<?= (int) $lgStat['ok_today'] ?> 次)
</p>
<div class="v-scroll">
<table class="v-tbl">
<thead>
<tr>
<th>登录时间</th>
<th>登录 IP</th>
<th>登录设备(浏览器)</th>
<th>用户名</th>
</tr>
</thead>
<tbody>
<?php if ($lgRecent): ?>
<?php foreach ($lgRecent as $r): ?>
<tr>
<td class="v-nowrap"><?= he((string) $r['created_at']) ?></td>
<td class="v-nowrap"><?= he((string) $r['ip']) ?></td>
<td><?= he(vis_device_text($r)) ?></td>
<td class="v-nowrap"><?= he((string) $r['username']) ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="4"><span class="tip">暂无成功登录记录(本次登录为首次记录)。</span></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- 登录失败日志(按 IP × 日期聚合) -->
<div class="fieldset-card">
<div class="fs-title">登录失败日志</div>
<div class="vis-stats">
<div class="vis-stat"><b><?= (int) $lgStat['fail_today'] ?></b><span>今日失败次数</span></div>
<div class="vis-stat"><b><?= (int) $lgStat['fail_ips'] ?></b><span>失败涉及 IP</span></div>
<div class="vis-stat"><b><?= (int) $lgStat['ok_today'] ?></b><span>今日成功登录</span></div>
<div class="vis-stat"><b><?= (int) $lgStat['total'] ?></b><span>登录日志总数</span></div>
</div>
<form method="get" class="v-filter" style="margin-top:12px" data-vis-form="loginfail">
<input type="text" name="lf" value="<?= he($lgFailF) ?>" placeholder="按 IP 筛选,支持模糊(如 203.0">
<button type="submit" class="btn btn-primary">筛选</button>
<a class="btn" href="visits.php" data-vis-section="loginfail" data-vis-page="">重置</a>
<span class="tip" style="align-self:center">按「IP × 日期」聚合:同一 IP 当天的失败次数与最近一次设备信息</span>
</form>
<div id="lfResults"><?= vis_render_loginfail($lgFailData) ?></div>
<form method="post" data-confirm="确认清空全部登录日志(成功与失败记录)?此操作不可恢复。" style="margin-top:12px">
<?= csrf_field() ?><input type="hidden" name="act" value="loginlog_clear">
<button type="submit" class="btn btn-sm btn-danger">清空登录日志</button>
<span class="tip" style="margin-left:8px">记录存于数据库 <code>login_log</code> 表(含成功与失败、时间 / 用户名 / IP / 设备)。</span>
</form>
</div>
<!-- 筛选 + 日志列表 -->
<div class="fieldset-card">
<div class="fs-title">访问记录</div>
<form method="get" class="v-filter" data-vis-form="access">
<select name="mode" title="展示模式"
onchange="if(this.form.requestSubmit){this.form.requestSubmit();}else{this.form.submit();}">
<option value="full" <?= $mode === 'full' ? ' selected' : '' ?>>完整模式(逐条明细)</option>
<option value="compact" <?= $mode === 'compact' ? ' selected' : '' ?>>精简模式(按访问次数排序)</option>
</select>
<input type="text" name="ip" value="<?= he($ipF) ?>" placeholder="按 IP 筛选,支持模糊(如 203.0">
<input type="text" name="q" class="w-s" value="<?= he($q) ?>" placeholder="关键词:地址 / 请求方式">
<button type="submit" class="btn btn-primary">筛选</button>
<a class="btn" href="visits.php" data-vis-section="access" data-vis-page="">重置</a>
</form>
<div id="visResults"><?= vis_render_access($visAccess) ?></div>
</div>
<!-- 自动风控规则 -->
<div class="fieldset-card">
<div class="fs-title">风控规则</div>
<form method="post">
<?= csrf_field() ?><input type="hidden" name="act" value="risk_save">
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:9px 0">
<label class="seg-check" style="min-width:148px"><input type="checkbox" name="r_login_on" value="1"
<?= $rcfg['login_fail']['on'] ? ' checked' : '' ?>> 后台登录失败封禁</label>
<span class="tip">失败次数 ≥ <input type="number" name="r_login_n" min="1" max="1000000"
value="<?= (int) $rcfg['login_fail']['n'] ?>"
style="width:96px;padding:4px 8px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
次 → 永久封禁(建议 5-20</span>
</div>
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:9px 0">
<label class="seg-check" style="min-width:148px"><input type="checkbox" name="r_404_on" value="1"
<?= $rcfg['not_found']['on'] ? ' checked' : '' ?>> 前台 404 封禁</label>
<span class="tip">累计次数 ≥ <input type="number" name="r_404_n" min="1" max="1000000"
value="<?= (int) $rcfg['not_found']['n'] ?>"
style="width:96px;padding:4px 8px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
次 → 永久封禁(建议 50-300</span>
</div>
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:9px 0">
<label class="seg-check" style="min-width:148px"><input type="checkbox" name="r_rate_on" value="1"
<?= $rcfg['rate']['on'] ? ' checked' : '' ?>> 请求频率封禁</label>
<span class="tip">60 秒内 ≥ <input type="number" name="r_rate_n" min="1" max="1000000"
value="<?= (int) $rcfg['rate']['n'] ?>"
style="width:96px;padding:4px 8px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
次 → 永久封禁(多人共用出口 IP 时建议关闭)</span>
</div>
<p class="tip" style="margin:8px 0 6px">计数方式404
与登录失败只要发生即累计(规则关闭也照常计数,便于后台观察);规则启用且达到阈值时自动封禁,保存后即时生效。自动封禁在黑名单中标注“风控自动”,可随时移除解封(解封时同步清除该 IP 计数)。</p>
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0 0 8px">
<span class="tip" style="min-width:148px">主提示(留空=默认)</span>
<input type="text" name="r_tip_main" maxlength="100" value="<?= he((string) $rcfg['_tip']['raw_main']) ?>"
placeholder="例:您触发了本站风控,请稍后再访问" title="黑名单命中时提示页的大号主文案"
style="flex:1 1 300px;min-width:260px;padding:6px 10px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
</div>
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:0 0 12px">
<span class="tip" style="min-width:148px">次行说明(留空=默认)</span>
<input type="text" name="r_tip_sub" maxlength="200" value="<?= he((string) $rcfg['_tip']['raw_sub']) ?>"
placeholder="例:如为正常访问,请稍后重试;若频繁误判,请联系站点管理员处理。" title="主提示下方的次要说明文案"
style="flex:1 1 300px;min-width:260px;padding:6px 10px;border:1px solid var(--line-2);border-radius:8px;background:var(--card);color:inherit">
</div>
<button type="submit" class="btn btn-primary">保存风控规则</button>
</form>
<form method="post" data-confirm="确认清空全部风险计数?各 IP 将从 0 重新累计,不影响已封禁的黑名单。" style="margin-top:14px">
<?= csrf_field() ?><input type="hidden" name="act" value="risk_clear">
<button type="submit" class="btn btn-sm">清空全部风险计数</button>
<?php if ($curRisk): ?>
<span class="tip" style="margin-left:8px">当前访问 IP<?= he($curIp) ?>)计数:登录失败 <?= (int) $curRisk['login_fail'] ?>
· 404 <?= (int) $curRisk['not_found'] ?> 次<?php if ($rcfg['rate']['on']): ?> · 本分钟请求
<?= (int) $curRisk['rate_count'] ?> 次<?php endif; ?></span>
<?php endif; ?>
</form>
</div>
<!-- IP 黑名单 -->
<div class="fieldset-card">
<div class="fs-title">黑名单管理</div>
<form method="post" class="v-filter">
<?= csrf_field() ?><input type="hidden" name="act" value="ban_add">
<input type="text" name="rule" required placeholder="如203.0.113.7 / 203.0.113.0/24 / 203.0.113.*"
style="flex:1 1 260px;max-width:420px">
<input type="text" name="note" placeholder="备注(可选,如:恶意扫描器)" style="flex:1 1 220px;max-width:340px">
<button type="submit" class="btn btn-primary">加入黑名单</button>
</form>
<p class="tip" style="margin:10px 0 2px">
支持三种写法:单个 IP、CIDR 网段(如 <code>203.0.113.0/24</code>)、星号通配段(如 <code>203.0.113.*</code> 或
<code>192.168.*.*</code>
目前仅按 IPv4 匹配。黑名单即时生效且不受「站长提示」开关影响,命中 IP 的<strong>前台与后台(含登录页)访问一律被拒绝</strong>。如需解除,请从未被封禁的网络进入后台移除该规则,或直接在
data/homepage.db 的 ip_blacklist 表中删除记录。
</p>
<div style="margin:6px 0 10px">
<?php if ($curHit): ?>
<span class="vis-warn-tip">当前访问 IP<?= he($curIp) ?> ——
命中规则「<?= he($curHit['rule']) ?>」(<?= ($curHit['note'] !== '') ? he((string) $curHit['note']) : '无备注' ?>),全站(前台与后台)访问将被拦截</span>
<?php else: ?>
<span class="vis-ok-tip">当前访问 IP<?= he($curIp) ?> —— 未命中任何黑名单规则</span>
<?php endif; ?>
</div>
<?php if ($banRows): ?>
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin:8px 0 4px">
<form method="post" id="blBatchForm" data-confirm="确认移除选中的黑名单规则?对应单 IP 风控计数将同步清除。">
<?= csrf_field() ?><input type="hidden" name="act" value="ban_batch">
<label class="tip"
style="display:inline-flex;align-items:center;gap:4px;cursor:pointer;margin:0 4px 0 0"><input
type="checkbox" id="blCheckAll"> 全选</label>
<button type="submit" class="btn btn-sm btn-danger" id="blBatchBtn" disabled>移除选中0</button>
</form>
<?php if ($banAutoCount > 0): ?>
<form method="post" data-confirm="确认一键移除全部风控自动封禁(共 <?= $banAutoCount ?> 条)?相关 IP 风控计数将同步清除。">
<?= csrf_field() ?><input type="hidden" name="act" value="ban_clear_auto">
<button type="submit" class="btn btn-sm">一键移除风控自动(<?= $banAutoCount ?></button>
</form>
<?php endif; ?>
<form method="post" data-confirm="确认清空全部黑名单?全部风险计数将一并清零。">
<?= csrf_field() ?><input type="hidden" name="act" value="ban_clear_all">
<button type="submit" class="btn btn-sm btn-danger">清空全部</button>
</form>
</div>
<?php foreach ($banRows as $b): ?>
<div class="bl-row">
<label class="tip" style="display:inline-flex;align-items:center;margin:0" title="勾选后可在上方批量移除"><input
type="checkbox" form="blBatchForm" class="js-bl-check" name="bid[]" value="<?= (int) $b['id'] ?>"></label>
<code class="bl-rule"><?= he((string) $b['rule']) ?></code>
<?php if (((string) ($b['source'] ?? 'manual')) === 'auto'): ?>
<span class="v-tag warn" style="min-width:0">风控自动</span>
<?php else: ?>
<span class="v-tag info" style="min-width:0">手动</span>
<?php endif; ?>
<span
class="bl-note"><?= $b['note'] !== '' ? he((string) $b['note']) : '<span class="tip">(无备注)</span>' ?></span>
<span class="bl-time"><?= he((string) $b['created_at']) ?></span>
<form method="post" data-confirm="确认移除黑名单规则「<?= he((string) $b['rule']) ?>」?">
<?= csrf_field() ?><input type="hidden" name="act" value="ban_del"><input type="hidden" name="id"
value="<?= (int) $b['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">移除</button>
</form>
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="tip" style="margin:6px 0 0">尚未配置黑名单规则。启用上方「自动风控」后,异常 IP 会自动记录到这里;也可在下方手动添加规则。</p>
<?php endif; ?>
</div>
</div>
</main>
<?php layout_theme_fab(); ?>
<script src="../assets/js/common.js"></script>
<script>
(function () {
var all = document.getElementById('blCheckAll');
var btn = document.getElementById('blBatchBtn');
var boxes = Array.prototype.slice.call(document.querySelectorAll('.js-bl-check'));
if (!all || !btn || !boxes.length) return;
function refresh() {
var n = boxes.filter(function (c) { return c.checked; }).length;
btn.disabled = n === 0;
btn.textContent = '移除选中(' + n + '';
all.checked = n > 0 && n === boxes.length;
}
all.addEventListener('change', function () {
boxes.forEach(function (c) { c.checked = all.checked; });
refresh();
});
boxes.forEach(function (c) { c.addEventListener('change', refresh); });
})();
</script>
</body>
</html>