539 lines
25 KiB
PHP
Executable File
539 lines
25 KiB
PHP
Executable File
<?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; ?>&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 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 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; ?>&page=<?php echo $page; echo $searchQuerySuffix; ?>&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; ?>&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; ?>&page=<?php echo $page + 1; echo $searchQuerySuffix; ?>">下一页</a>
|
||
<?php } ?>
|
||
</div>
|
||
<?php } ?>
|
||
<?php } ?>
|
||
</section>
|
||
</body>
|
||
</html>
|