feat: initial commit

This commit is contained in:
root
2026-06-14 14:00:24 +08:00
commit 72d388f642
118 changed files with 39015 additions and 0 deletions
+538
View File
@@ -0,0 +1,538 @@
<?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>