Files
CLOAK/admin_ip_manager.php
2026-06-14 14:00:24 +08:00

539 lines
25 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
/**
* IP 黑白名单管理(单文件)
* 依赖 cong.php 中的数据库常量与 LOGIN_PASSWORD
*
* INSERT IGNORE 去重依赖唯一约束,若尚未建立可执行:
* ALTER TABLE ip_list ADD UNIQUE KEY uk_group_ip (group_id, ip_address);
*/
declare(strict_types=1);
require_once __DIR__ . '/cong.php';
session_start();
header('Content-Type: text/html; charset=utf-8');
if (empty($_SESSION['password']) || $_SESSION['password'] !== LOGIN_PASSWORD) {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['admin_password'])) {
if (trim((string) $_POST['admin_password']) === LOGIN_PASSWORD) {
$_SESSION['password'] = LOGIN_PASSWORD;
header('Location: ' . strtok($_SERVER['REQUEST_URI'] ?? 'admin_ip_manager.php', '?'));
exit;
}
$loginError = '密码错误';
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登录 — IP 名单管理</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 360px; margin: 4rem auto; padding: 0 1rem; }
input[type=password] { width: 100%; padding: 0.5rem; box-sizing: border-box; margin: 0.5rem 0; }
button { padding: 0.5rem 1rem; cursor: pointer; }
.err { color: #c00; margin-bottom: 1rem; }
</style>
</head>
<body>
<h1>IP 名单管理</h1>
<?php if (!empty($loginError)) { ?><p class="err"><?php echo htmlspecialchars($loginError, ENT_QUOTES, 'UTF-8'); ?></p><?php } ?>
<form method="post">
<label>后台密码</label>
<input type="password" name="admin_password" required autocomplete="current-password">
<button type="submit">进入</button>
</form>
<p><small>与 dashboard 使用相同登录密码;若已在后台登录可刷新本页。</small></p>
</body>
</html>
<?php
exit;
}
$type = (isset($_GET['type']) && $_GET['type'] === 'white') ? 'white' : 'black';
$self = htmlspecialchars($_SERVER['PHP_SELF'] ?? 'admin_ip_manager.php', ENT_QUOTES, 'UTF-8');
$flash = '';
$flashOk = true;
try {
$dsn = 'mysql:host=127.0.0.1;dbname=' . DB_NAME . ';charset=utf8mb4';
$pdo = new PDO($dsn, DB_USERNAME, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
http_response_code(500);
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>错误</title></head><body><p>数据库连接失败。</p></body></html>';
exit;
}
/** 校验分组属于当前 type,返回 true 或 false */
function groupBelongsToType(PDO $pdo, int $groupId, string $type): bool
{
$st = $pdo->prepare('SELECT 1 FROM ip_groups WHERE id = ? AND type = ? LIMIT 1');
$st->execute([$groupId, $type]);
return (bool) $st->fetchColumn();
}
// —— POST:新建分组 ——
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_group') {
$name = trim((string) ($_POST['group_name'] ?? ''));
$postType = ($_POST['list_type'] ?? '') === 'white' ? 'white' : 'black';
if ($name === '') {
$flash = '分组名称不能为空';
$flashOk = false;
} else {
$st = $pdo->prepare('INSERT INTO ip_groups (name, type) VALUES (?, ?)');
$st->execute([$name, $postType]);
$flash = '分组已创建';
header('Location: ' . $self . '?type=' . urlencode($postType) . '&msg=' . urlencode($flash) . '&ok=1');
exit;
}
}
// —— POST:批量导入 IP ——
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'bulk_import') {
$groupId = (int) ($_POST['import_group_id'] ?? 0);
$raw = (string) ($_POST['ip_lines'] ?? '');
if ($groupId <= 0 || !groupBelongsToType($pdo, $groupId, $type)) {
$flash = '请选择有效的分组';
$flashOk = false;
} else {
$lines = preg_split('/\R/u', $raw) ?: [];
$uniqueFromText = [];
foreach ($lines as $line) {
$ip = trim($line);
if ($ip === '') {
continue;
}
$uniqueFromText[$ip] = true;
}
$candidateIps = array_keys($uniqueFromText);
$exSt = $pdo->prepare('SELECT ip_address FROM ip_list WHERE group_id = ?');
$exSt->execute([$groupId]);
$already = [];
while ($row = $exSt->fetch(PDO::FETCH_ASSOC)) {
$already[$row['ip_address']] = true;
}
$toInsert = [];
foreach ($candidateIps as $ip) {
if (!isset($already[$ip])) {
$toInsert[] = $ip;
}
}
$inserted = 0;
$stmt = $pdo->prepare('INSERT IGNORE INTO ip_list (group_id, ip_address) VALUES (?, ?)');
$pdo->beginTransaction();
try {
foreach ($toInsert as $ip) {
$stmt->execute([$groupId, $ip]);
$inserted += $stmt->rowCount();
}
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
$flash = '导入失败:' . $e->getMessage();
$flashOk = false;
}
if ($flashOk) {
$skippedDupText = count($candidateIps) - count($toInsert);
$flash = '批量导入完成:新插入 ' . $inserted . ' 条;文本去重后 ' . count($candidateIps) . ' 条;本组已存在跳过 ' . $skippedDupText . ' 条';
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . '&ok=1');
exit;
}
}
}
// —— POST:按行批量删除 IP(当前 type 下;可选限定分组)——
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'bulk_delete_ips') {
$scopeGroup = (int) ($_POST['bulk_delete_group_id'] ?? 0);
$raw = (string) ($_POST['bulk_delete_lines'] ?? '');
if ($scopeGroup > 0 && !groupBelongsToType($pdo, $scopeGroup, $type)) {
$flash = '批量删除:分组无效';
$flashOk = false;
} else {
$lines = preg_split('/\R/u', $raw) ?: [];
$ips = [];
foreach ($lines as $line) {
$ip = trim($line);
if ($ip !== '') {
$ips[$ip] = true;
}
}
$ips = array_keys($ips);
if ($ips === []) {
$flash = '批量删除:未填写任何 IP';
$flashOk = false;
} else {
$sql = 'DELETE il FROM ip_list il
INNER JOIN ip_groups ig ON ig.id = il.group_id
WHERE ig.type = ? AND il.ip_address = ?';
if ($scopeGroup > 0) {
$sql .= ' AND il.group_id = ?';
}
$stmt = $pdo->prepare($sql);
$deleted = 0;
$pdo->beginTransaction();
try {
foreach ($ips as $ip) {
if ($scopeGroup > 0) {
$stmt->execute([$type, $ip, $scopeGroup]);
} else {
$stmt->execute([$type, $ip]);
}
$deleted += $stmt->rowCount();
}
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
$flash = '批量删除失败:' . $e->getMessage();
$flashOk = false;
}
if ($flashOk) {
$flash = '批量删除完成,共删除 ' . $deleted . ' 条记录(不存在的 IP 不计入)';
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . '&ok=1');
exit;
}
}
}
}
// —— POST:清空当前类型下所有 IP(保留分组)——
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'clear_ips_only') {
$phrase = trim((string) ($_POST['clear_confirm_phrase'] ?? ''));
if ($phrase !== '清空IP') {
$flash = '清空失败:请在确认框中完整输入「清空IP」';
$flashOk = false;
} else {
$del = $pdo->prepare(
'DELETE il FROM ip_list il
INNER JOIN ip_groups ig ON ig.id = il.group_id
WHERE ig.type = ?'
);
$del->execute([$type]);
$flash = '已清空当前类型下所有 IP(分组已保留),删除 ' . $del->rowCount() . ' 条';
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . '&ok=1');
exit;
}
}
// —— POST:清空当前类型下全部分组及 IP ——
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'clear_type_all') {
$phrase = trim((string) ($_POST['clear_confirm_phrase_all'] ?? ''));
if ($phrase !== '清空全部') {
$flash = '清空失败:请在确认框中完整输入「清空全部」';
$flashOk = false;
} else {
$pdo->beginTransaction();
try {
$pdo->prepare(
'DELETE il FROM ip_list il
INNER JOIN ip_groups ig ON ig.id = il.group_id
WHERE ig.type = ?'
)->execute([$type]);
$pdo->prepare('DELETE FROM ip_groups WHERE type = ?')->execute([$type]);
$pdo->commit();
$flash = '已清空当前类型下的所有分组及 IP';
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . '&ok=1');
exit;
} catch (Throwable $e) {
$pdo->rollBack();
$flash = '清空失败:' . $e->getMessage();
$flashOk = false;
}
}
}
// —— GET:删除分组 ——
if (isset($_GET['delete_group'])) {
$gid = (int) $_GET['delete_group'];
if ($gid > 0 && groupBelongsToType($pdo, $gid, $type)) {
$pdo->beginTransaction();
try {
$pdo->prepare('DELETE FROM ip_list WHERE group_id = ?')->execute([$gid]);
$pdo->prepare('DELETE FROM ip_groups WHERE id = ? AND type = ?')->execute([$gid, $type]);
$pdo->commit();
$flash = '分组及其 IP 已删除';
} catch (Throwable $e) {
$pdo->rollBack();
$flash = '删除分组失败';
$flashOk = false;
}
} else {
$flash = '无效的分组';
$flashOk = false;
}
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . ($flashOk ? '&ok=1' : ''));
exit;
}
// —— GET:删除单条 IP ——
if (isset($_GET['delete_ip'])) {
$ipId = (int) $_GET['delete_ip'];
if ($ipId > 0) {
$del = $pdo->prepare(
'DELETE il FROM ip_list il
INNER JOIN ip_groups ig ON ig.id = il.group_id
WHERE il.id = ? AND ig.type = ?'
);
$del->execute([$ipId, $type]);
$flash = $del->rowCount() > 0 ? '已删除该 IP 记录' : '未找到记录或无权删除';
$flashOk = $del->rowCount() > 0;
} else {
$flash = '无效的 IP 记录';
$flashOk = false;
}
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . ($flashOk ? '&ok=1' : ''));
exit;
}
if (isset($_GET['msg'])) {
$flash = (string) $_GET['msg'];
$flashOk = isset($_GET['ok']) && $_GET['ok'] === '1';
}
// 当前 type 下的分组
$groupsStmt = $pdo->prepare('SELECT id, name FROM ip_groups WHERE type = ? ORDER BY id ASC');
$groupsStmt->execute([$type]);
$groups = $groupsStmt->fetchAll();
$ipSearch = isset($_GET['ip_search']) ? trim((string) $_GET['ip_search']) : '';
$ipSearchSql = '';
$ipListBind = [$type];
if ($ipSearch !== '') {
$ipSearchSql = ' AND il.ip_address LIKE ?';
$ipListBind[] = '%' . addcslashes($ipSearch, '\\%_') . '%';
}
$searchQuerySuffix = $ipSearch !== '' ? '&ip_search=' . rawurlencode($ipSearch) : '';
// IP 列表分页
$perPage = 50;
$page = max(1, (int) ($_GET['page'] ?? 1));
$offset = ($page - 1) * $perPage;
$countSql = 'SELECT COUNT(*) FROM ip_list il INNER JOIN ip_groups ig ON ig.id = il.group_id WHERE ig.type = ?' . $ipSearchSql;
$totalStmt = $pdo->prepare($countSql);
$totalStmt->execute($ipListBind);
$totalIps = (int) $totalStmt->fetchColumn();
$totalPages = max(1, (int) ceil($totalIps / $perPage));
if ($page > $totalPages) {
$page = $totalPages;
$offset = ($page - 1) * $perPage;
}
$listSql = 'SELECT il.id, il.ip_address, il.group_id, ig.name AS group_name
FROM ip_list il
INNER JOIN ip_groups ig ON ig.id = il.group_id
WHERE ig.type = ?' . $ipSearchSql . '
ORDER BY il.id DESC
LIMIT ' . (int) $perPage . ' OFFSET ' . (int) $offset;
$listStmt = $pdo->prepare($listSql);
$listStmt->execute($ipListBind);
$ipRows = $listStmt->fetchAll();
function h(string $s): string
{
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
$typeParam = 'type=' . urlencode($type);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>IP 名单管理 — <?php echo $type === 'white' ? '白名单' : '黑名单'; ?></title>
<style>
:root { --bd: #ccc; --bg: #f6f7f9; --ok: #0a0; --err: #c00; }
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 1rem 1.25rem 2rem; background: var(--bg); color: #222; }
h1 { font-size: 1.35rem; margin-top: 0; }
h2 { font-size: 1.1rem; margin: 1.5rem 0 0.75rem; border-bottom: 1px solid var(--bd); padding-bottom: 0.35rem; }
.tabs { display: flex; gap: 0.5rem; margin-bottom: 1.25rem; flex-wrap: wrap; }
.tabs a {
display: inline-block; padding: 0.45rem 1rem; text-decoration: none; border-radius: 6px;
border: 1px solid var(--bd); background: #fff; color: #111;
}
.tabs a.active { background: #111; color: #fff; border-color: #111; }
.flash { padding: 0.65rem 1rem; border-radius: 6px; margin-bottom: 1rem; }
.flash.ok { background: #e8f5e9; color: #1b5e20; }
.flash.bad { background: #ffebee; color: #b71c1c; }
section.card { background: #fff; border: 1px solid var(--bd); border-radius: 8px; padding: 1rem 1.1rem; margin-bottom: 1rem; max-width: 960px; }
label { display: block; font-weight: 600; margin: 0.5rem 0 0.25rem; font-size: 0.9rem; }
input[type=text], select, textarea { width: 100%; max-width: 100%; box-sizing: border-box; padding: 0.45rem 0.5rem; border: 1px solid var(--bd); border-radius: 4px; font-size: 0.95rem; }
textarea { min-height: 160px; font-family: ui-monospace, monospace; }
button, .btn-link {
display: inline-block; margin-top: 0.5rem; padding: 0.45rem 0.9rem; cursor: pointer; border-radius: 4px;
border: 1px solid var(--bd); background: #fff; font-size: 0.9rem; text-decoration: none; color: #111;
}
button.primary { background: #1976d2; color: #fff; border-color: #1565c0; }
button.danger, a.danger { color: var(--err); border-color: #e57373; }
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
th, td { border: 1px solid var(--bd); padding: 0.45rem 0.55rem; text-align: left; }
th { background: #eee; }
tr:nth-child(even) { background: #fafafa; }
.pager { margin-top: 1rem; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.muted { color: #666; font-size: 0.85rem; }
.row-inline { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: flex-end; }
.row-inline > div { flex: 1; min-width: 200px; }
section.card.danger-zone { border-color: #e57373; background: #fff8f8; }
section.card.danger-zone h2 { color: #b71c1c; }
</style>
</head>
<body>
<h1>IP 黑白名单管理</h1>
<nav class="tabs">
<a href="<?php echo $self; ?>?type=black<?php echo $searchQuerySuffix; ?>" class="<?php echo $type === 'black' ? 'active' : ''; ?>">黑名单管理</a>
<a href="<?php echo $self; ?>?type=white<?php echo $searchQuerySuffix; ?>" class="<?php echo $type === 'white' ? 'active' : ''; ?>">白名单管理</a>
</nav>
<?php if ($flash !== '') { ?>
<div class="flash <?php echo $flashOk ? 'ok' : 'bad'; ?>"><?php echo h($flash); ?></div>
<?php } ?>
<section class="card">
<h2>新建分组</h2>
<form method="post" action="<?php echo $self; ?>?<?php echo $typeParam; ?>">
<input type="hidden" name="action" value="create_group">
<input type="hidden" name="list_type" value="<?php echo h($type); ?>">
<label for="group_name">组名</label>
<input type="text" id="group_name" name="group_name" required maxlength="191" placeholder="例如:广告渠道 A">
<button type="submit" class="primary">创建分组(<?php echo $type === 'white' ? '白名单' : '黑名单'; ?></button>
</form>
</section>
<section class="card">
<h2>当前视图下的分组</h2>
<?php if ($groups === []) { ?>
<p class="muted">暂无分组,请先创建。</p>
<?php } else { ?>
<table>
<thead>
<tr><th>ID</th><th>组名</th><th>操作</th></tr>
</thead>
<tbody>
<?php foreach ($groups as $g) { ?>
<tr>
<td><?php echo (int) $g['id']; ?></td>
<td><?php echo h((string) $g['name']); ?></td>
<td>
<a class="btn-link danger" href="<?php echo $self; ?>?<?php echo $typeParam; ?>&amp;delete_group=<?php echo (int) $g['id']; ?>"
onclick="return confirm(<?php echo json_encode('确定删除分组「' . (string) $g['name'] . '」及其下所有 IP', JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>);">删除组</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } ?>
</section>
<section class="card">
<h2>批量导入 IP</h2>
<p class="muted">每行一个 IP;首尾空格与空行忽略;同一文本内重复只计一次;本组数据库中已存在的 IP 不会重复插入。</p>
<form method="post" action="<?php echo $self; ?>?<?php echo $typeParam; ?>">
<input type="hidden" name="action" value="bulk_import">
<label for="import_group_id">目标分组</label>
<select id="import_group_id" name="import_group_id" required>
<option value="">— 请选择 —</option>
<?php foreach ($groups as $g) { ?>
<option value="<?php echo (int) $g['id']; ?>"><?php echo h((string) $g['name']); ?> (ID <?php echo (int) $g['id']; ?>)</option>
<?php } ?>
</select>
<label for="ip_lines">IP 列表</label>
<textarea id="ip_lines" name="ip_lines" placeholder="1.2.3.4&#10;2001:db8::1"></textarea>
<button type="submit" class="primary">导入到所选分组</button>
</form>
</section>
<section class="card">
<h2>批量删除 IP</h2>
<p class="muted">每行一个 IP;同一 IP 在文本中多次出现会去重后删除。选择「不限定分组」时,会删除当前<?php echo $type === 'white' ? '白' : '黑'; ?>名单类型下所有分组中匹配的 IP。</p>
<form method="post" action="<?php echo $self; ?>?<?php echo $typeParam; ?>" onsubmit="return confirm('确定按列表批量删除?');">
<input type="hidden" name="action" value="bulk_delete_ips">
<label for="bulk_delete_group_id">作用范围</label>
<select id="bulk_delete_group_id" name="bulk_delete_group_id">
<option value="0">不限定分组(当前类型下全部匹配)</option>
<?php foreach ($groups as $g) { ?>
<option value="<?php echo (int) $g['id']; ?>">仅分组:<?php echo h((string) $g['name']); ?> (ID <?php echo (int) $g['id']; ?>)</option>
<?php } ?>
</select>
<label for="bulk_delete_lines">要删除的 IP 列表</label>
<textarea id="bulk_delete_lines" name="bulk_delete_lines" placeholder="1.2.3.4&#10;5.6.7.8"></textarea>
<button type="submit" class="danger">执行批量删除</button>
</form>
</section>
<section class="card danger-zone">
<h2>清空数据(危险操作)</h2>
<p class="muted">以下操作仅影响当前视图类型(<?php echo $type === 'white' ? '白名单' : '黑名单'; ?>),不影响另一类型。</p>
<form method="post" action="<?php echo $self; ?>?<?php echo $typeParam; ?>" style="margin-bottom:1.25rem;" onsubmit="return confirm('确定清空当前类型下所有 IP?分组将保留。');">
<input type="hidden" name="action" value="clear_ips_only">
<label for="clear_confirm_phrase">确认清空 IP(在框内输入:<strong>清空IP</strong></label>
<input type="text" id="clear_confirm_phrase" name="clear_confirm_phrase" autocomplete="off" placeholder="清空IP">
<button type="submit" class="danger">清空当前类型全部 IP(保留分组)</button>
</form>
<form method="post" action="<?php echo $self; ?>?<?php echo $typeParam; ?>" onsubmit="return confirm('确定删除当前类型下所有分组及 IP?此操作不可恢复。');">
<input type="hidden" name="action" value="clear_type_all">
<label for="clear_confirm_phrase_all">确认清空全部(在框内输入:<strong>清空全部</strong></label>
<input type="text" id="clear_confirm_phrase_all" name="clear_confirm_phrase_all" autocomplete="off" placeholder="清空全部">
<button type="submit" class="danger">清空当前类型全部分组及 IP</button>
</form>
</section>
<section class="card">
<h2>IP 列表(<?php echo $type === 'white' ? '白名单' : '黑名单'; ?></h2>
<form method="get" action="<?php echo $self; ?>" class="row-inline" style="margin-bottom:12px;align-items:center;">
<input type="hidden" name="type" value="<?php echo h($type); ?>">
<div style="flex:2;min-width:220px;">
<label for="ip_search">搜索 IP(模糊匹配)</label>
<input type="text" id="ip_search" name="ip_search" value="<?php echo h($ipSearch); ?>" placeholder="例如 192.168 或完整 IP">
</div>
<div style="padding-top:1.5rem;">
<button type="submit" class="primary">搜索</button>
<?php if ($ipSearch !== '') { ?>
<a class="btn-link" href="<?php echo $self; ?>?<?php echo $typeParam; ?>">清除</a>
<?php } ?>
</div>
</form>
<p class="muted">共 <?php echo $totalIps; ?> 条,每页 <?php echo $perPage; ?> 条<?php echo $ipSearch !== '' ? '(已筛选)' : ''; ?>。</p>
<?php if ($ipRows === []) { ?>
<p class="muted">当前类型下暂无 IP。</p>
<?php } else { ?>
<table>
<thead>
<tr><th>ID</th><th>IP</th><th>分组</th><th>操作</th></tr>
</thead>
<tbody>
<?php foreach ($ipRows as $row) { ?>
<tr>
<td><?php echo (int) $row['id']; ?></td>
<td><code><?php echo h((string) $row['ip_address']); ?></code></td>
<td><?php echo h((string) $row['group_name']); ?></td>
<td>
<a class="btn-link danger" href="<?php echo $self; ?>?<?php echo $typeParam; ?>&amp;page=<?php echo $page; echo $searchQuerySuffix; ?>&amp;delete_ip=<?php echo (int) $row['id']; ?>"
onclick="return confirm(<?php echo json_encode('确定删除 IP ' . (string) $row['ip_address'] . '', JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>);">删除</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<?php if ($totalPages > 1) { ?>
<div class="pager">
<?php if ($page > 1) { ?>
<a class="btn-link" href="<?php echo $self; ?>?<?php echo $typeParam; ?>&amp;page=<?php echo $page - 1; echo $searchQuerySuffix; ?>">上一页</a>
<?php } ?>
<span>第 <?php echo $page; ?> / <?php echo $totalPages; ?> 页</span>
<?php if ($page < $totalPages) { ?>
<a class="btn-link" href="<?php echo $self; ?>?<?php echo $typeParam; ?>&amp;page=<?php echo $page + 1; echo $searchQuerySuffix; ?>">下一页</a>
<?php } ?>
</div>
<?php } ?>
<?php } ?>
</section>
</body>
</html>