From 90bd40ffdee0c21f60a94771065f31da8239a970 Mon Sep 17 00:00:00 2001 From: MasonLiu <2857911564@qq.com> Date: Sun, 6 Sep 2026 14:19:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E4=BB=93=E5=BA=93?= =?UTF-8?q?=EF=BC=8C=E5=88=9B=E5=BB=BA=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .htaccess | 0 admin/_guard.php | 34 +++ admin/content.php | 662 ++++++++++++++++++++++++++++++++++++++++++ admin/login.php | 56 ++++ admin/logout.php | 9 + admin/nav.php | 322 ++++++++++++++++++++ admin/tools.php | 265 +++++++++++++++++ api/nav.php | 46 +++ assets/css/common.css | 501 ++++++++++++++++++++++++++++++++ assets/img/logo.svg | 10 + assets/js/codec.js | 240 +++++++++++++++ assets/js/common.js | 308 ++++++++++++++++++++ assets/js/gmcrypto.js | 206 +++++++++++++ assets/js/ip.js | 289 ++++++++++++++++++ data/avlist.js | 55 ++++ data/homepage.db | Bin 0 -> 49152 bytes data/quota.txt | 13 + func/_bar.php | 15 + func/av.php | 142 +++++++++ func/codec.php | 148 ++++++++++ func/gmcodec.php | 237 +++++++++++++++ func/index.php | 49 ++++ func/ip.php | 59 ++++ func/password.php | 143 +++++++++ func/qrcode.php | 148 ++++++++++ includes/auth.php | 101 +++++++ includes/db.php | 206 +++++++++++++ includes/layout.php | 208 +++++++++++++ index.php | 143 +++++++++ nav.php | 145 +++++++++ nginx.htaccess | 0 plan.txt | 50 ++++ plan_v2.md | 546 ++++++++++++++++++++++++++++++++++ 33 files changed, 5356 insertions(+) create mode 100644 .htaccess create mode 100644 admin/_guard.php create mode 100644 admin/content.php create mode 100644 admin/login.php create mode 100644 admin/logout.php create mode 100644 admin/nav.php create mode 100644 admin/tools.php create mode 100644 api/nav.php create mode 100644 assets/css/common.css create mode 100644 assets/img/logo.svg create mode 100644 assets/js/codec.js create mode 100644 assets/js/common.js create mode 100644 assets/js/gmcrypto.js create mode 100644 assets/js/ip.js create mode 100644 data/avlist.js create mode 100644 data/homepage.db create mode 100644 data/quota.txt create mode 100644 func/_bar.php create mode 100644 func/av.php create mode 100644 func/codec.php create mode 100644 func/gmcodec.php create mode 100644 func/index.php create mode 100644 func/ip.php create mode 100644 func/password.php create mode 100644 func/qrcode.php create mode 100644 includes/auth.php create mode 100644 includes/db.php create mode 100644 includes/layout.php create mode 100644 index.php create mode 100644 nav.php create mode 100644 nginx.htaccess create mode 100644 plan.txt create mode 100644 plan_v2.md diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..e69de29 diff --git a/admin/_guard.php b/admin/_guard.php new file mode 100644 index 0000000..e0f8c17 --- /dev/null +++ b/admin/_guard.php @@ -0,0 +1,34 @@ +
'; + echo ''; + layout_logo_img('logo', $site); + echo '' . he($site) . ' · 后台'; + $links = [ + 'nav' => ['nav.php', '导航内容管理'], + 'tools' => ['tools.php', '功能区管理'], + 'content' => ['content.php', '内容与顺序'], + ]; + foreach ($links as $k => $lk) { + $cls = ($active === $k) ? 'active' : ''; + echo '' . he($lk[1]) . ''; + } + echo ''; + echo '管理员:' . he($admin) . ''; + echo '退出登录'; + echo ''; + echo '
' . "\n"; +} diff --git a/admin/content.php b/admin/content.php new file mode 100644 index 0000000..fcaec43 --- /dev/null +++ b/admin/content.php @@ -0,0 +1,662 @@ +prepare('INSERT INTO settings (key, value) VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value'); + $st->execute([$key, $value]); +} + +/** 删除当前已上传的自定义 logo 文件(仅限 data/logo 目录内) */ +function remove_old_logo(): void +{ + $old = setting_get('site_logo', ''); + if ($old !== '' && strpos($old, 'data/logo/') === 0) { + $file = DATA_DIR . '/logo/' . basename($old); + if (is_file($file)) { + @unlink($file); + } + } +} + +function alert_html(string $kind, string $msg): void +{ + echo '
' . he($msg) . '
'; +} + +$msgKind = ''; +$msgText = ''; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + if (!csrf_verify()) { + $msgKind = 'err'; + $msgText = '安全校验失败,请刷新页面重试。'; + } else { + $act = (string)($_POST['act'] ?? ''); + switch ($act) { + case 'site': + settings_set('site_name', trim((string)($_POST['site_name'] ?? '知识导航站'))); + settings_set('site_slogan', trim((string)($_POST['site_slogan'] ?? ''))); + touch_last_updated(); + $msgKind = 'ok'; + $msgText = '站点信息已保存。'; + break; + case 'boards': + $names = (array)($_POST['b_name'] ?? []); + $descs = (array)($_POST['b_desc'] ?? []); + $icons = (array)($_POST['b_icon'] ?? []); + $sorts = (array)($_POST['b_sort'] ?? []); + $stUpd = $pdo->prepare('UPDATE categories SET name = ?, description = ?, icon = ?, sort = ? WHERE id = ?'); + foreach (array_keys($names) as $i) { + $name = trim((string)($names[$i] ?? '')); + $desc = trim((string)($descs[$i] ?? '')); + $icon = trim((string)($icons[$i] ?? '')); + $sort = max(0, (int)($sorts[$i] ?? 0)); + $id = (int)$i; + $key = $pdo->query('SELECT key FROM categories WHERE id = ' . $id)->fetchColumn(); + if ($name === '' || !$key || !in_array((string)$key, $catKeys, true)) continue; + $stUpd->execute([$name, $desc, $icon, $sort, $id]); + } + touch_last_updated(); + $msgKind = 'ok'; + $msgText = '板块名称 / 说明 / 图标 / 顺序已保存。'; + break; + case 'footer': + settings_set('icp_no', trim((string)($_POST['icp_no'] ?? ''))); + settings_set('gongan_no', trim((string)($_POST['gongan_no'] ?? ''))); + settings_set('gongan_link', trim((string)($_POST['gongan_link'] ?? ''))); + settings_set('copyright', trim((string)($_POST['copyright'] ?? ''))); + settings_set('footer_text', trim((string)($_POST['footer_text'] ?? ''))); + touch_last_updated(); + $msgKind = 'ok'; + $msgText = '底部信息已保存。'; + break; + case 'quotes': + $content = (string)($_POST['quotes'] ?? ''); + $file = DATA_DIR . '/quota.txt'; + if (!is_dir(DATA_DIR)) @mkdir(DATA_DIR, 0777, true); + $savedBytes = @file_put_contents($file, $content); + if ($savedBytes === false) { + $msgKind = 'err'; + $msgText = '名言文件写入失败,请检查 data 目录权限。'; + } else { + touch_last_updated(); + $msgKind = 'ok'; + $msgText = '名言库已保存。'; + } + break; + case 'logo': + $up = $_FILES['logo_file'] ?? null; + if (!$up || ($up['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { + $msgKind = 'err'; + $msgText = '未收到有效图片,请重新选择。'; + break; + } + if (($up['size'] ?? 0) > 2 * 1024 * 1024) { + $msgKind = 'err'; + $msgText = '图片不能超过 2MB。'; + break; + } + $extMap = [ + 'png' => 'png', 'jpg' => 'jpg', 'jpeg' => 'jpg', + 'gif' => 'gif', 'webp' => 'webp', 'svg' => 'svg', 'ico' => 'ico', + ]; + $ext = strtolower(pathinfo((string)$up['name'], PATHINFO_EXTENSION)); + if (!isset($extMap[$ext])) { + $msgKind = 'err'; + $msgText = '仅支持 png / jpg / gif / webp / svg / ico 图片。'; + break; + } + $logoDir = DATA_DIR . '/logo'; + if (!is_dir($logoDir)) { + @mkdir($logoDir, 0777, true); + } + $filename = 'site_logo_' . date('YmdHis') . '_' . bin2hex(random_bytes(4)) . '.' . $extMap[$ext]; + if (!@move_uploaded_file($up['tmp_name'], $logoDir . '/' . $filename)) { + $msgKind = 'err'; + $msgText = '文件保存失败,请检查 data 目录写入权限。'; + break; + } + remove_old_logo(); + settings_set('site_logo', 'data/logo/' . $filename); + touch_last_updated(); + $msgKind = 'ok'; + $msgText = 'LOGO 已更新并应用到全站。'; + break; + case 'logo_clear': + remove_old_logo(); + settings_set('site_logo', ''); + touch_last_updated(); + $msgKind = 'ok'; + $msgText = '已恢复默认 LOGO。'; + break; + case 'hero': + settings_set('hero_bg', trim((string)($_POST['hero_bg'] ?? ''))); + touch_last_updated(); + $msgKind = 'ok'; + $msgText = '首页顶部标识栏背景已保存。'; + break; + case 'cdn': + settings_set('cdn_ranges', trim((string)($_POST['cdn_ranges'] ?? ''))); + touch_last_updated(); + $msgKind = 'ok'; + $msgText = 'CDN IP 段库已保存。'; + break; + case 'engines': + $raw = (string)($_POST['data'] ?? ''); + $rows = json_decode($raw, true); + if (!is_array($rows)) { + $msgKind = 'err'; + $msgText = '引擎数据格式错误。'; + break; + } + $list = []; + foreach ($rows as $e) { + if (!is_array($e)) continue; + $name = trim((string)($e['name'] ?? '')); + $url = trim((string)($e['url'] ?? '')); + $icon = trim((string)($e['icon'] ?? '')); + if ($name === '' || $url === '' || strpos($url, '{kw}') === false) continue; + $list[] = [ + 'key' => 'e' . (count($list) + 1), + 'name' => $name, + 'icon' => ($icon !== '' ? $icon : 'S'), + 'url' => $url, + ]; + } + if (!$list) { + $msgKind = 'err'; + $msgText = '至少需要保留一个有效搜索引擎(名称与含 {kw} 的地址必填)。'; + break; + } + settings_set('search_engines', json_encode($list, JSON_UNESCAPED_UNICODE)); + touch_last_updated(); + $msgKind = 'ok'; + $msgText = '已保存 ' . count($list) . ' 个搜索引擎。'; + break; + case 'engines_reset': + settings_set('search_engines', ''); + touch_last_updated(); + $msgKind = 'ok'; + $msgText = '已恢复默认搜索引擎(Bing / 百度 / GitHub / Google)。'; + break; + case 'quicklinks': + $raw = (string)($_POST['data'] ?? ''); + $rows = json_decode($raw, true); + if (!is_array($rows)) { + $msgKind = 'err'; + $msgText = '快捷站点数据格式错误。'; + break; + } + $clean = []; + foreach ($rows as $i => $r) { + if (!is_array($r)) continue; + $name = trim((string)($r['name'] ?? '')); + $url = trim((string)($r['url'] ?? '')); + $icon = trim((string)($r['icon'] ?? '')); + $note = trim((string)($r['note'] ?? '')); + if ($name === '' && $url === '') continue; + if ($name === '' || $url === '') { + $msgKind = 'err'; + $msgText = '快捷站点第 ' . ((int)$i + 1) . ' 行:名称与地址均不能为空。'; + break; + } + if (!preg_match('#^https?://#i', $url)) { + $msgKind = 'err'; + $msgText = '快捷站点第 ' . ((int)$i + 1) . ' 行:地址需为完整 http(s) 链接。'; + break; + } + $clean[] = [ + 'id' => (int)($r['id'] ?? 0), + 'icon' => ($icon !== '' ? $icon : '🔗'), + 'name' => $name, + 'url' => $url, + 'note' => $note, + 'enabled' => (((string)($r['enabled'] ?? '1') === '1')) ? 1 : 0, + ]; + } + if ($msgKind === '' && !$clean) { + $msgKind = 'err'; + $msgText = '至少需要保留一个快捷站点。'; + } + if ($msgKind === '') { + try { + $pdo->beginTransaction(); + $oldIds = array_map('intval', array_column($pdo->query('SELECT id FROM quick_links')->fetchAll(), 'id')); + $keep = []; + foreach ($clean as $c) { + if ($c['id'] > 0) $keep[] = $c['id']; + } + $del = array_values(array_diff($oldIds, $keep)); + foreach ($del as $did) { + $pdo->prepare('DELETE FROM quick_links WHERE id = ?')->execute([$did]); + } + $stUpd = $pdo->prepare('UPDATE quick_links SET name=?, url=?, icon=?, note=?, enabled=?, sort=? WHERE id=?'); + $stIns = $pdo->prepare('INSERT INTO quick_links (name, url, icon, note, enabled, sort) VALUES (?,?,?,?,?,?)'); + $sort = 0; + foreach ($clean as $c) { + $sort++; + if ($c['id'] > 0 && in_array($c['id'], $oldIds, true)) { + $stUpd->execute([$c['name'], $c['url'], $c['icon'], $c['note'], $c['enabled'], $sort, $c['id']]); + } else { + $stIns->execute([$c['name'], $c['url'], $c['icon'], $c['note'], $c['enabled'], $sort]); + } + } + $pdo->commit(); + touch_last_updated(); + $msgKind = 'ok'; + $msgText = '快捷站点已保存。'; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + $msgKind = 'err'; + $msgText = '保存失败:' . $e->getMessage(); + } + } + break; + case 'pwd': + $np = (string)($_POST['new_pwd'] ?? ''); + $np2 = (string)($_POST['new_pwd2'] ?? ''); + if ($np !== $np2) { + $msgKind = 'err'; + $msgText = '两次输入的新密码不一致。'; + } else { + [$ok, $text] = hp_change_password((string)($_POST['old_pwd'] ?? ''), $np); + $msgKind = $ok ? 'ok' : 'err'; + $msgText = $text; + } + break; + default: + $msgKind = 'err'; + $msgText = '未知操作。'; + } + } +} + +// 板块数据(顺序展示,sort 同值按 id 兜底) +$cats = $pdo->query('SELECT * FROM categories ORDER BY sort ASC, id ASC')->fetchAll(); +// 板块说明 seed(未编辑过时为空则给占位) +$boardDefaults = [ + 'popular' => '面向大众与初学者的计算机科学、网络安全与前沿技术科普。', + 'red' => '面向渗透测试与攻防演练的红队资源集。', + 'blue' => '面向防御侧:应急响应、威胁狩猎、取证溯源。', + 'tool' => '日常效率与安全实用小工具,直达功能区。', +]; + +$logo = setting_get('site_logo', ''); +$quotesFile = DATA_DIR . '/quota.txt'; +$quotesContent = is_file($quotesFile) ? (string)@file_get_contents($quotesFile) : ''; +$all = []; +foreach ($pdo->query('SELECT key, value FROM settings') as $r) { + $all[$r['key']] = (string)$r['value']; +} +$site = $all['site_name'] ?? '知识导航站'; + +layout_head('内容与顺序'); +admin_topbar('content'); +?> +
+
+

内容与顺序

+

对全站展示内容与 logo 进行维护,保存后自动更新首页"上次更新时间"。

+ + + +
+ +
+
站点信息
+
+ + + + + +
+
+
+ + +
+
站点 LOGO(浏览器标签页图标与页面内容同步生效)
+
+ 当前logo + 当前生效的 logo;更换后浏览器标签页图标(favicon)与页面头部 logo 都会更新。 +
+
+ + + +
+ +
+
+ +
+ +
+
+ +
+ + +
+
首页顶部标识栏背景(Hero 区,支持纯色或 CSS 渐变)
+
+ + + +
+ + + + + + + +
+

纯色直接填色值(如 #2563eb);渐变填完整 CSS,如 linear-gradient(135deg, #1f3a5f 0%, #2563eb 100%)。夜间模式沿用同一背景。

+
+
+
+ + +
+
四大板块(名称 / 悬停说明文案 / 顺序)
+
+ + +
+
+ + +
+
+
+ + + + +
+ + +
+
+ + +
+
+ + +
+
首页快捷站点(四大板块上方的胶囊按钮,新标签页打开)
+
+ + +

用于放置本域名下的其它网站入口;点击后新开标签页。地址需为完整 http(s) 链接。列表顺序即展示顺序。

+
+ query('SELECT * FROM quick_links ORDER BY sort ASC, id ASC')->fetchAll(); ?> + +
+ + + + + + + + + + + + + + + +
+ +
+
+ + +
+
+
+ + +
+
底部信息
+
+ + + + + + + + + + + +
+
+
+ + +
+
搜索引擎(导航区搜索栏可切换的引擎)
+
+ + +

图标用于搜索栏切换按钮常驻展示;搜索引擎全称在展开下拉列表时才显示。搜索地址需包含 {kw} 占位符,例如 https://www.bing.com/search?q={kw}。列表按从上到下顺序生效,可先删行再重新添加来调整顺序。

+
+ +
+ + + + +
+ +
+
+ + +
+
+
+ + +
+
+ + +
+
名言库(data/quota.txt,首页随机展示)
+
+ + + +
+
+
+ + +
+
CDN IP 段库(供“IP 地址处理”工具识别 CDN 来源)
+
+ + + +
+

识别时会按“先命中即归为 CDN”处理;未命中且非内网的 IP 一律归为公网。默认内置 Cloudflare 官方 IPv4 段,可按需补充阿里云 / 腾讯云等厂商段。

+
+
+ + +
+
修改登录密码
+
+ + + + + + + +
+
+
+
+
+
+ + + + + + + + diff --git a/admin/login.php b/admin/login.php new file mode 100644 index 0000000..f7d726c --- /dev/null +++ b/admin/login.php @@ -0,0 +1,56 @@ + +
+
+ +

后台管理登录

+

+ +
+ + +
已安全退出登录。
+ +
+ + + + +
+ +
+
+

初始账号:admin / admin123(首次登录后请在“内容与顺序”中修改)

+
+
+ + + + diff --git a/admin/logout.php b/admin/logout.php new file mode 100644 index 0000000..52e5464 --- /dev/null +++ b/admin/logout.php @@ -0,0 +1,9 @@ +prepare('SELECT * FROM categories WHERE key = ? LIMIT 1'); +$stCat->execute([$cat]); +$catRow = $stCat->fetch(); +if (!$catRow) { + http_response_code(404); + die('分类不存在'); +} + +$saved = isset($_GET['ok']); +$errorMsg = ''; + +// ---------- 保存处理 ---------- +if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save') { + if (!csrf_verify()) { + $errorMsg = '安全校验失败,请刷新页面重试。'; + } else { + $raw = (string)($_POST['data'] ?? ''); + $data = json_decode($raw, true); + if (!is_array($data)) { + $errorMsg = '提交数据格式错误。'; + } else { + try { + $pdo->beginTransaction(); + // 现有块 + $st = $pdo->prepare('SELECT id FROM nav_blocks WHERE cat_id = ?'); + $st->execute([(int)$catRow['id']]); + $oldBlocks = array_map('intval', array_column($st->fetchAll(), 'id')); + // 现有链接(按块分组) + $oldItemsByBlock = []; + foreach ($oldBlocks as $bid) { + $st = $pdo->prepare('SELECT id FROM nav_items WHERE block_id = ?'); + $st->execute([$bid]); + $oldItemsByBlock[$bid] = array_map('intval', array_column($st->fetchAll(), 'id')); + } + // 本轮保留的块 id + $keepBlocks = []; + $keepItems = []; + foreach ($data as $b) { + if (is_array($b) && isset($b['title'])) { + $id = (int)($b['id'] ?? 0); + if ($id > 0) $keepBlocks[] = $id; + foreach (($b['items'] ?? []) as $it) { + if (is_array($it) && (int)($it['id'] ?? 0) > 0) { + $keepItems[] = (int)$it['id']; + } + } + } + } + // 删除被移除的块及其链接 + $delBlocks = array_values(array_diff($oldBlocks, $keepBlocks)); + foreach ($delBlocks as $dbid) { + $pdo->prepare('DELETE FROM nav_items WHERE block_id = ?')->execute([$dbid]); + $pdo->prepare('DELETE FROM nav_blocks WHERE id = ?')->execute([$dbid]); + unset($oldItemsByBlock[$dbid]); + } + // 删除被移除的链接 + foreach ($oldItemsByBlock as $bid => $ids) { + $del = array_values(array_diff($ids, $keepItems)); + foreach ($del as $did) { + $pdo->prepare('DELETE FROM nav_items WHERE id = ?')->execute([$did]); + } + } + // 写回块与链接 + $stInsBlock = $pdo->prepare('INSERT INTO nav_blocks (cat_id, title, sort) VALUES (?,?,?)'); + $stUpdBlock = $pdo->prepare('UPDATE nav_blocks SET title = ?, sort = ? WHERE id = ?'); + $stInsItem = $pdo->prepare('INSERT INTO nav_items (block_id, label, url, note, sort) VALUES (?,?,?,?,?)'); + $stUpdItem = $pdo->prepare('UPDATE nav_items SET label = ?, url = ?, note = ?, sort = ? WHERE id = ? AND block_id = ?'); + $blockSort = 0; + foreach ($data as $b) { + if (!is_array($b) || !isset($b['title'])) continue; + $title = trim((string)$b['title']); + if ($title === '') continue; + $bid = (int)($b['id'] ?? 0); + $blockBelongs = in_array($bid, $oldBlocks, true); + if ($bid > 0 && $blockBelongs) { + $stUpdBlock->execute([$title, $blockSort, $bid]); + } else { + $stInsBlock->execute([(int)$catRow['id'], $title, $blockSort]); + $bid = (int)$pdo->lastInsertId(); + } + $itemSort = 0; + foreach (($b['items'] ?? []) as $it) { + if (!is_array($it)) continue; + $label = trim((string)($it['label'] ?? '')); + $url = trim((string)($it['url'] ?? '')); + $note = trim((string)($it['note'] ?? '')); + if ($label === '' && $url === '') continue; + $iid = (int)($it['id'] ?? 0); + if ($iid > 0 && in_array($iid, $oldItemsByBlock[$bid] ?? [], true)) { + $stUpdItem->execute([$label, $url, $note, $itemSort, $iid, $bid]); + } else { + $stInsItem->execute([$bid, $label, $url, $note, $itemSort]); + } + $itemSort++; + } + $blockSort++; + } + $pdo->commit(); + touch_last_updated(); + header('Location: nav.php?c=' . urlencode($cat) . '&ok=1'); + exit; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + $errorMsg = '保存失败:' . $e->getMessage(); + } + } + } +} + +// ---------- 读取现有数据用于展示 ---------- +$st = $pdo->prepare('SELECT * FROM nav_blocks WHERE cat_id = ? ORDER BY sort ASC, id ASC'); +$st->execute([(int)$catRow['id']]); +$blocks = $st->fetchAll(); +$itemsMap = []; +if ($blocks) { + $ids = array_map('intval', array_column($blocks, 'id')); + $in = implode(',', array_fill(0, count($ids), '?')); + $st = $pdo->prepare("SELECT * FROM nav_items WHERE block_id IN ($in) ORDER BY sort ASC, id ASC"); + $st->execute($ids); + foreach ($st->fetchAll() as $it) { + $itemsMap[(int)$it['block_id']][] = $it; + } +} + +$catNames = []; +foreach ($catKeys as $ck) { + $row = $pdo->query("SELECT key, name FROM categories WHERE key = '" . $ck . "'")->fetch(); + $catNames[$ck] = $row['name'] ?? $ck; +} + +$csrfVal = csrf_token(); + +layout_head('导航内容管理'); +admin_topbar('nav'); +?> +
+
+

导航内容管理

+

管理 的导航块与链接;块内最多 15 个链接(3 小列 × 5 行),超出部分请拆分到新块。

+ + +
+ + + +
+ + +
保存成功,前台页面已同步更新。
+ + +
+ + + +
+
+ + + + + + diff --git a/admin/tools.php b/admin/tools.php new file mode 100644 index 0000000..66ea426 --- /dev/null +++ b/admin/tools.php @@ -0,0 +1,265 @@ + $r) { + if (!is_array($r)) continue; + $name = trim((string)($r['name'] ?? '')); + $url = trim((string)($r['url'] ?? '')); + $icon = trim((string)($r['icon'] ?? '')); + $desc = trim((string)($r['description'] ?? '')); + $ext = ((string)($r['ext'] ?? '0') === '1') ? 1 : 0; + if ($name === '' && $url === '') continue; // 空行忽略 + if ($name === '' || $url === '') { + $errorMsg = '第 ' . ((int)$i + 1) . ' 行:名称与地址均不能为空。'; + break; + } + if ($ext === 1) { + if (!preg_match('#^https?://#i', $url)) { + $errorMsg = '第 ' . ((int)$i + 1) . ' 行:外部链接需以 http(s):// 开头。'; + break; + } + } else { + if (preg_match('#^https?://#i', $url)) { + $errorMsg = '第 ' . ((int)$i + 1) . ' 行:站内页面地址请勿填写完整网址(填如 codec.php),如需外链请选择“外部链接”。'; + break; + } + if (preg_match('/^[a-z][a-z0-9+.-]*:/i', $url) || strpos($url, '//') === 0) { + $errorMsg = '第 ' . ((int)$i + 1) . ' 行:站内页面仅支持相对地址(如 codec.php),不允许填写协议或跨协议地址。'; + break; + } + } + $clean[] = [ + 'id' => (int)($r['id'] ?? 0), + 'icon' => ($icon !== '' ? $icon : '🧩'), + 'name' => $name, + 'description' => $desc, + 'url' => $url, + 'ext' => $ext, + 'enabled' => (((string)($r['enabled'] ?? '1') === '1')) ? 1 : 0, + ]; + } + if ($errorMsg === '') { + if (!$clean) { + $errorMsg = '功能区至少需要保留一个工具。'; + } else { + try { + $pdo->beginTransaction(); + $oldIds = array_map('intval', array_column($pdo->query('SELECT id FROM func_tools')->fetchAll(), 'id')); + $keep = []; + foreach ($clean as $c) { + if ($c['id'] > 0) $keep[] = $c['id']; + } + $del = array_values(array_diff($oldIds, $keep)); + foreach ($del as $did) { + $pdo->prepare('DELETE FROM func_tools WHERE id = ?')->execute([$did]); + } + $stUpd = $pdo->prepare('UPDATE func_tools SET name=?, icon=?, description=?, url=?, is_external=?, enabled=?, sort=? WHERE id=?'); + $stIns = $pdo->prepare('INSERT INTO func_tools (name, icon, description, url, is_external, enabled, sort) VALUES (?,?,?,?,?,?,?)'); + $sort = 0; + foreach ($clean as $c) { + $sort++; + if ($c['id'] > 0 && in_array($c['id'], $oldIds, true)) { + $stUpd->execute([$c['name'], $c['icon'], $c['description'], $c['url'], $c['ext'], $c['enabled'], $sort, $c['id']]); + } else { + $stIns->execute([$c['name'], $c['icon'], $c['description'], $c['url'], $c['ext'], $c['enabled'], $sort]); + } + } + $pdo->commit(); + touch_last_updated(); + header('Location: tools.php?ok=1'); + exit; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + $errorMsg = '保存失败:' . $e->getMessage(); + } + } + } + } + } +} + +// ---------- 展示现有工具 ---------- +$tools = $pdo->query('SELECT * FROM func_tools ORDER BY sort ASC, id ASC')->fetchAll(); + +layout_head('功能区管理'); +admin_topbar('tools'); +?> +
+
+

功能区管理

+

+ 管理功能区首页展示的工具卡片。站内工具页需先在 func/ 下开发对应页面,再到此处登记入口; + 也可登记外部网址作为外链卡片(新窗口打开)。 +

+ + +
保存成功,功能区首页已同步更新。
+ + +
+ + +
+
+ + + + +
+ +
+ + + + + + + + + + + + + + + + +
+ +
+ +
+ + + 工具数量与展示顺序实时反映到前台功能区首页 +
+
+
+
+
+ + + + + + diff --git a/api/nav.php b/api/nav.php new file mode 100644 index 0000000..30c5fd0 --- /dev/null +++ b/api/nav.php @@ -0,0 +1,46 @@ + 'cat 参数须为 popular/red/blue'], JSON_UNESCAPED_UNICODE); + exit; +} + +$pdo = db(); +$st = $pdo->prepare('SELECT * FROM categories WHERE key = ? LIMIT 1'); +$st->execute([$cat]); +$catRow = $st->fetch(); +if (!$catRow) { + http_response_code(404); + echo json_encode(['error' => '分类不存在'], JSON_UNESCAPED_UNICODE); + exit; +} + +$out = []; +$st = $pdo->prepare('SELECT * FROM nav_blocks WHERE cat_id = ? ORDER BY sort ASC, id ASC'); +$st->execute([(int)$catRow['id']]); +$blocks = $st->fetchAll(); +$itemSt = $pdo->prepare('SELECT * FROM nav_items WHERE block_id = ? ORDER BY sort ASC, id ASC LIMIT 15'); +foreach ($blocks as $b) { + $itemSt->execute([(int)$b['id']]); + $items = []; + foreach ($itemSt->fetchAll() as $it) { + $items[] = [ + 'label' => (string)$it['label'], + 'url' => (string)$it['url'], + 'note' => (string)$it['note'], + ]; + } + $out[] = ['title' => (string)$b['title'], 'items' => $items]; +} + +echo json_encode($out, JSON_UNESCAPED_UNICODE); diff --git a/assets/css/common.css b/assets/css/common.css new file mode 100644 index 0000000..5a1a82d --- /dev/null +++ b/assets/css/common.css @@ -0,0 +1,501 @@ +/* common.css —— 全站统一样式与组件(含昼/夜主题、响应式) */ +/* ============ 主题变量 ============ */ +:root, [data-theme="light"] { + --bg: #f2f5fa; + --bg-2: #e8edf5; + --card: #ffffff; + --line: #dde3ec; + --line-2: #c9d2e0; + --text: #1f2d3d; + --text-2: #3c4b61; + --text-3: #71809a; + --brand: #2563eb; + --brand-2: #1f3a5f; + --brand-ink: #ffffff; + --accent-soft: rgba(37, 99, 235, 0.10); + --hover-bg: rgba(37, 99, 235, 0.08); + --danger: #dc2626; + --ok: #16a34a; + --warn: #d97706; + --shadow: 0 4px 16px rgba(15, 30, 55, 0.08); + --shadow-sm: 0 1px 4px rgba(15, 30, 55, 0.06); + --code-bg: #f4f6fa; + --code-text: #1f2d3d; + --tooltip-bg: #1f2d3d; + --tooltip-text: #f2f5fa; + --input-bg: #ffffff; + --brand-grad: linear-gradient(135deg, #1f3a5f 0%, #2563eb 100%); + --board-tint: linear-gradient(160deg, rgba(37, 99, 235, 0.10), rgba(31, 58, 95, 0.04)); + color-scheme: light; +} +[data-theme="dark"] { + --bg: #0f1520; + --bg-2: #161e2c; + --card: #1a2332; + --line: #2a3648; + --line-2: #3a4a60; + --text: #e7edf5; + --text-2: #c2ccda; + --text-3: #8fa0b6; + --brand: #60a5fa; + --brand-2: #93c5fd; + --brand-ink: #0b1220; + --accent-soft: rgba(96, 165, 250, 0.14); + --hover-bg: rgba(96, 165, 250, 0.12); + --danger: #f87171; + --ok: #4ade80; + --warn: #fbbf24; + --shadow: 0 4px 16px rgba(0, 0, 0, 0.35); + --shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.3); + --code-bg: #111927; + --code-text: #dbe4f0; + --tooltip-bg: #e7edf5; + --tooltip-text: #1a2332; + --input-bg: #131c2a; + --brand-grad: linear-gradient(135deg, #12233d 0%, #1d4ed8 100%); + --board-tint: linear-gradient(160deg, rgba(96, 165, 250, 0.13), rgba(18, 35, 61, 0.10)); + color-scheme: dark; +} + +/* ============ 基础 ============ */ +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: -apple-system, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + background: var(--bg); + color: var(--text); + font-size: 15px; + line-height: 1.6; + min-height: 100vh; + display: flex; + flex-direction: column; + transition: background .25s ease, color .25s ease; +} +a { color: var(--brand); text-decoration: none; } +a:hover { text-decoration: none; } +img { max-width: 100%; } +.wrap { width: 100%; max-width: 1200px; margin: 0 auto; padding: 0 20px; } +main.page-main { flex: 1 0 auto; padding-bottom: 24px; } + +h1, h2, h3, h4 { line-height: 1.3; margin: 0 0 .5em; } +button { font-family: inherit; } + +/* 提示小字 */ +.tip { font-size: 13px; color: var(--text-3); } +.tip code { background: var(--code-bg); color: var(--code-text); border-radius: 4px; padding: 1px 6px; } +.err-msg, .ok-msg { border-radius: 8px; padding: 10px 14px; font-size: 14px; margin: 12px 0; } +.err-msg { background: rgba(220, 38, 38, .10); color: var(--danger); } +.ok-msg { background: rgba(22, 163, 74, .12); color: var(--ok); } +.seg-check { display: inline-flex; align-items: center; gap: 5px; font-size: 14px; color: var(--text-2); padding: 6px 10px; border: 1px solid var(--line-2); border-radius: 8px; background: var(--card); cursor: pointer; user-select: none; } +.seg-check:hover { border-color: var(--brand); } +.seg-check input { margin: 0; } + +/* ============ 按钮 ============ */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 6px; + border: 1px solid var(--line-2); background: var(--card); color: var(--text); + padding: 8px 16px; border-radius: 8px; font-size: 14px; cursor: pointer; + transition: all .15s ease; line-height: 1.4; vertical-align: middle; +} +.btn:hover { border-color: var(--brand); color: var(--brand); } +.btn-primary { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); } +.btn-primary:hover { filter: brightness(1.08); color: var(--brand-ink); } +.btn-ghost { background: transparent; } +.btn-danger { color: var(--danger); border-color: rgba(220, 38, 38, .4); } +.btn-danger:hover { background: rgba(220, 38, 38, .1); border-color: var(--danger); color: var(--danger); } +.btn-sm { padding: 4px 10px; font-size: 13px; border-radius: 6px; } +.btn[disabled] { opacity: .5; cursor: not-allowed; } + +/* ============ 表单 ============ */ +label.fl { display: block; font-size: 13px; color: var(--text-3); margin: 10px 0 4px; } +input[type=text], input[type=password], input[type=number], input[type=url], input[type=file], +select, textarea { + width: 100%; padding: 8px 12px; border: 1px solid var(--line-2); border-radius: 8px; + background: var(--input-bg); color: var(--text); font-size: 14px; font-family: inherit; + transition: border .15s ease, box-shadow .15s ease; +} +input:focus, select:focus, textarea:focus { + outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--accent-soft); +} +textarea { resize: vertical; min-height: 90px; line-height: 1.6; } +input[type=checkbox] { accent-color: var(--brand); } +select { width: auto; min-width: 150px; } +.field-row { display: flex; gap: 10px; align-items: flex-start; flex-wrap: wrap; } + +/* ============ 主题浮动按钮 ============ */ +.theme-fab { + position: fixed; right: 18px; bottom: 22px; width: 46px; height: 46px; z-index: 1000; + border-radius: 50%; border: 1px solid var(--line-2); background: var(--card); + color: var(--text); font-size: 20px; cursor: pointer; + box-shadow: var(--shadow); display: flex; align-items: center; justify-content: center; + transition: transform .2s ease, background .2s ease; +} +.theme-fab:hover { transform: scale(1.08); } + +/* ============ 通用头部(品牌条 / 顶部条) ============ */ +.brand { display: inline-flex; align-items: center; gap: 12px; } +.brand .logo { + width: 52px; height: 52px; border-radius: 12px; object-fit: contain; + background: var(--card); padding: 4px; box-shadow: var(--shadow-sm); +} +.brand .site-name { font-size: 21px; font-weight: 700; } +.brand .site-slogan { font-size: 13px; color: var(--text-3); } +.brand-sm .logo { width: 38px; height: 38px; border-radius: 9px; } +.brand-sm .site-name { font-size: 17px; } + +/* ---------- 首页 Hero 展示栏 ---------- */ +.hero { + background: var(--brand-grad); color: #fff; + padding: 26px 0 18px; min-height: 20vh; box-shadow: var(--shadow); +} +.hero .brand .site-name, .hero .brand .site-slogan { color: #fff; } +.hero .brand .logo { background: rgba(255, 255, 255, .95); } +.hero-inner { display: flex; flex-direction: column; gap: 14px; } +.hero-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; } +.hero-meta { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: rgba(255, 255, 255, .9); text-align: right; } +.hero-meta .val { font-variant-numeric: tabular-nums; font-weight: 600; color: #fff; } +.weather-box { display: flex; justify-content: center; } +.weather-box iframe { display: block; border-radius: 8px; background: transparent; max-width: 100%; } +/* 第三方天气 iframe 内容为白底,无法跨域改色:深色主题下用反色滤镜融入当前背景 */ +[data-theme="dark"] .weather-box iframe { filter: invert(0.9) hue-rotate(180deg) contrast(0.92) saturate(0.9); } + +/* ---------- 名言条 ---------- */ +.quote-bar { background: var(--card); border-bottom: 1px solid var(--line); } +.quote-inner { display: flex; align-items: center; justify-content: center; gap: 12px; padding: 12px 16px; text-align: center; } +.quote-mark { font-size: 22px; color: var(--brand); } +.quote-text { + font-size: 17px; color: var(--text-2); cursor: pointer; user-select: none; + padding: 2px 8px; border-radius: 8px; + transition: color .15s ease, background .15s ease; +} +.quote-text:hover { color: var(--brand); background: var(--hover-bg); } + +/* ============ 首页四大板块 ============ */ +/* 首页板块区:内容放得下时填满视口剩余高度(不出现竖向滚动条),内容超高时自然滚动 */ +.board-section { flex: 1 0 auto; display: flex; flex-direction: column; padding: 26px 0 24px; } +.board-section > .wrap { flex: 1; display: flex; flex-direction: column; justify-content: center; } +/* 首页快捷站点条(后台“内容与顺序”配置,点击新标签页打开) */ +.quick-strip { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-bottom: 24px; } +.quick-chip { + position: relative; display: inline-flex; align-items: center; gap: 6px; padding: 8px 18px; border-radius: 999px; + background: var(--card); border: 1px solid var(--line-2); color: var(--text-2); font-size: 14px; + box-shadow: var(--shadow-sm); text-decoration: none; + transition: border-color .15s ease, color .15s ease, transform .12s ease, box-shadow .15s ease; +} +.quick-chip:hover { border-color: var(--brand); color: var(--brand); transform: translateY(-1px); box-shadow: var(--shadow); } +.quick-chip .q-icon { font-size: 15px; line-height: 1; display: inline-flex; align-items: center; } +.quick-chip .icon-img { width: 16px; height: 16px; object-fit: contain; } +/* 快捷站点按钮 hover 备注(data-note) */ +a.quick-chip[data-note]::after { + content: attr(data-note); position: absolute; z-index: 60; bottom: calc(100% + 8px); left: 50%; + transform: translateX(-50%) translateY(4px); width: max-content; max-width: 320px; + background: var(--tooltip-bg); color: var(--tooltip-text); font-size: 12px; line-height: 1.5; + padding: 6px 10px; border-radius: 7px; white-space: normal; word-break: break-word; + text-align: center; opacity: 0; pointer-events: none; box-shadow: var(--shadow); + transition: opacity .14s ease, transform .14s ease; +} +a.quick-chip[data-note]:hover::after { opacity: 1; transform: translateX(-50%) translateY(0); } +/* 通用图标 (site_icon 输出)在各容器中的尺寸 */ +.icon-img { object-fit: contain; vertical-align: middle; } +.board-icon img.icon-img { width: 30px; height: 30px; } +.board-section .section-title { text-align: center; font-size: 22px; margin-bottom: 6px; } +.board-section .section-sub { text-align: center; color: var(--text-3); font-size: 14px; margin-bottom: 18px; } +.boards { position: relative; display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; } +.board-card { + position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; + gap: 12px; background-color: var(--card); background-image: var(--board-tint); + border: 1px solid var(--line); border-radius: 16px; padding: 26px 14px; text-align: center; + box-shadow: var(--shadow-sm); color: var(--text); + transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease; + min-height: 230px; +} +.board-card:hover { transform: translateY(-4px); box-shadow: var(--shadow); border-color: var(--brand); } +.board-icon { + width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; + font-size: 30px; color: #fff; background: var(--brand-grad); box-shadow: var(--shadow); +} +.board-name { font-size: 19px; font-weight: 700; } +.board-hint { font-size: 13px; color: var(--text-3); } +.func-card .fc-icon img.icon-img { width: 24px; height: 24px; } + +/* 板块展开说明浮层 */ +.board-panel { + position: absolute; top: 6px; bottom: 6px; width: 52%; z-index: 5; + background: var(--card); border: 1px solid var(--brand); border-radius: 16px; + padding: 24px 26px; box-shadow: var(--shadow); pointer-events: none; + opacity: 0; transform: translateY(8px) scale(.98); transition: opacity .18s ease, transform .18s ease; + display: flex; flex-direction: column; justify-content: center; +} +.board-panel.visible { opacity: 1; transform: none; } +.board-panel.panel-left { left: 0; } +.board-panel.panel-right { right: 0; } +.board-panel-title { font-size: 20px; font-weight: 700; color: var(--brand); margin-bottom: 10px; } +.board-panel-desc { font-size: 15px; color: var(--text-2); } + +/* 名言隐藏池 */ +.quote-pool { display: none; } + +/* ============ 页脚 ============ */ +.footer { background: var(--bg-2); border-top: 1px solid var(--line); color: var(--text-3); font-size: 13px; } +.footer-inner { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 22px 16px; text-align: center; } +.footer .admin-link { opacity: .65; } +.footer .admin-link:hover { opacity: 1; } + +/* ============ 顶部条(导航区/其他内页) ============ */ +.top-strip { background: var(--card); border-bottom: 1px solid var(--line); } +.strip-inner { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 20px; } +.nav-top { padding-top: 14px; } + +/* ============ 搜索栏 ============ */ +.search-zone { padding: 10vh 0 0; } +.search-zone .search-title { text-align: center; color: var(--text-3); font-size: 14px; margin: 8px 0 6px; } +.search-box { + max-width: 760px; margin: 0 auto; display: flex; align-items: center; + border: 1px solid var(--line-2); border-radius: 26px; background: var(--card); + box-shadow: var(--shadow-sm); overflow: visible; padding: 4px; + transition: border-color .15s ease, box-shadow .15s ease; +} +.search-box:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--accent-soft); } +.engine-btn { + position: relative; display: inline-flex; align-items: center; gap: 5px; + border: none; background: transparent; color: var(--text-2); padding: 8px 14px 8px 16px; + border-radius: 22px 0 0 22px; cursor: pointer; font-size: 14px; white-space: nowrap; +} +.engine-btn:hover { background: var(--hover-bg); } +.engine-btn .arrow { font-size: 10px; opacity: .7; } +.engine-menu { + position: absolute; top: calc(100% + 6px); left: 0; margin: 0; padding: 6px; list-style: none; + background: var(--card); border: 1px solid var(--line-2); border-radius: 10px; box-shadow: var(--shadow); + min-width: 150px; display: none; z-index: 20; +} +.engine-menu.open { display: block; } +.engine-menu li { padding: 7px 10px; border-radius: 6px; cursor: pointer; font-size: 14px; } +.engine-menu li:hover { background: var(--hover-bg); } +.engine-menu li.active { color: var(--brand); font-weight: 600; } +.search-field { flex: 1; position: relative; display: flex; align-items: center; } +.search-field input { border: none; box-shadow: none !important; background: transparent; width: 100%; padding: 9px 34px 9px 6px; font-size: 15px; } +.search-field input:focus { border: none; } +.search-clear { + position: absolute; right: 8px; top: 50%; transform: translateY(-50%); + width: 20px; height: 20px; border-radius: 50%; border: none; background: var(--bg-2); + color: var(--text-3); font-size: 12px; line-height: 1; cursor: pointer; + display: flex; align-items: center; justify-content: center; visibility: hidden; +} +.search-clear:hover { background: var(--line-2); color: var(--text); } +.search-go { + border: none; background: var(--brand); color: var(--brand-ink); border-radius: 20px; + padding: 9px 22px; font-size: 15px; cursor: pointer; font-weight: 600; +} +.search-go:hover { filter: brightness(1.08); } + +/* ============ 导航区 ============ */ +.nav-area { margin: 26px 15% 40px; } +.nav-area-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 16px; gap: 10px; flex-wrap: wrap; } +.nav-area-title { font-size: 20px; font-weight: 700; } +.nav-area-count { color: var(--text-3); font-size: 13px; } +.nav-cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; align-items: start; } +.nav-col { display: flex; flex-direction: column; gap: 18px; } +.nav-block { + background: var(--card); border: 1px solid var(--line); border-radius: 14px; + padding: 14px 14px 12px; box-shadow: var(--shadow-sm); break-inside: avoid; +} +.nav-block-title { font-weight: 700; font-size: 15px; margin: 0 0 10px; color: var(--brand-2); padding-left: 2px; } +.nav-block-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 36px; gap: 8px; } +.nav-btn { + position: relative; display: flex; align-items: center; justify-content: center; padding: 0 6px; + border: 1px solid var(--line); border-radius: 8px; background: var(--bg); + color: var(--text-2); font-size: 13px; text-align: center; overflow: visible; + transition: background .15s ease, color .15s ease, border-color .15s ease, transform .12s ease; + min-width: 0; white-space: nowrap; +} +a.nav-btn:hover { background: var(--brand); border-color: var(--brand); color: #fff; transform: translateY(-1px); } +span.nav-btn.is-empty { visibility: hidden; } +span.nav-btn.is-off { opacity: .45; cursor: not-allowed; } +/* tooltip:有备注按钮 hover 浮出气泡 */ +a.nav-btn[data-note]::after { + content: attr(data-note); position: absolute; z-index: 60; bottom: calc(100% + 7px); left: 50%; + transform: translateX(-50%) translateY(4px); background: var(--tooltip-bg); color: var(--tooltip-text); + font-size: 12px; line-height: 1.5; padding: 5px 10px; border-radius: 7px; white-space: nowrap; + max-width: 280px; overflow: hidden; text-overflow: ellipsis; + opacity: 0; pointer-events: none; transition: opacity .14s ease, transform .14s ease; + box-shadow: var(--shadow); +} +a.nav-btn[data-note]:hover::after { opacity: 1; transform: translateX(-50%) translateY(0); } + +/* ============ 功能区 ============ */ +.func-bar { background: var(--card); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 50; } +.func-bar-inner { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 14px; } +.func-bar-title { font-size: 16px; font-weight: 700; text-align: center; flex: 0 1 auto; } +.func-bar .spacer { width: 120px; } +.sub-hero { position: relative; background: var(--brand-grad); color: #fff; text-align: center; padding: 46px 20px 40px; } +.sub-hero h1 { font-size: 26px; margin: 0 0 8px; } +.sub-hero p { color: rgba(255, 255, 255, .88); margin: 0; font-size: 14px; } +.sub-hero .back-home { position: absolute; top: 14px; left: 18px; color: rgba(255, 255, 255, .92); font-size: 14px; } +.sub-hero .back-home:hover { color: #fff; text-decoration: underline; } +.page-body { padding: 26px 0 10px; } + +/* 工具入口卡片 */ +.func-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 18px; margin: 6px 0 20px; } +.func-card { + display: flex; align-items: center; gap: 14px; background: var(--card); border: 1px solid var(--line); + border-radius: 14px; padding: 18px; box-shadow: var(--shadow-sm); color: var(--text); + transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; +} +.func-card:hover { transform: translateY(-3px); box-shadow: var(--shadow); border-color: var(--brand); } +.func-card .fc-icon { + width: 50px; height: 50px; flex: 0 0 50px; border-radius: 12px; display: flex; align-items: center; justify-content: center; + font-size: 24px; color: var(--brand); background: var(--accent-soft); +} +.func-card .fc-name { font-weight: 700; font-size: 15px; } +.func-card .fc-desc { font-size: 13px; color: var(--text-3); margin-top: 2px; } + +/* 工具卡片容器 */ +.panel-card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 20px; margin-bottom: 20px; box-shadow: var(--shadow-sm); } +.panel-card > h2 { font-size: 17px; margin-top: 0; } + +/* IO 双文本区布局 */ +.io-area { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 14px; } +.io-box { display: flex; flex-direction: column; } +.io-box .io-label { font-size: 13px; color: var(--text-3); margin-bottom: 6px; display: flex; justify-content: space-between; align-items: center; } +.io-box textarea { min-height: 230px; font-family: Consolas, Menlo, monospace; font-size: 13px; } +.copy-btn .btn { display: inline-flex; } + +/* 工具操作行 */ +.tool-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin: 10px 0; } +.seg { display: inline-flex; border: 1px solid var(--line-2); border-radius: 8px; overflow: hidden; } +.seg button { border: none; background: var(--card); color: var(--text-2); padding: 7px 16px; cursor: pointer; font-size: 14px; } +.seg button.active { background: var(--brand); color: #fff; } + +/* 密码结果 */ +.pwd-results { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; } +.pwd-line { display: flex; align-items: center; gap: 10px; } +.pwd-line code { flex: 1; background: var(--code-bg); color: var(--code-text); padding: 8px 12px; border-radius: 8px; font-size: 14px; overflow-x: auto; white-space: pre; word-break: break-all; } + +/* 二维码 */ +.qr-stage { display: flex; flex-direction: column; align-items: center; gap: 14px; margin-top: 10px; } +.qr-stage canvas, .qr-stage img { border-radius: 10px; background: #fff; padding: 8px; box-shadow: var(--shadow-sm); } + +/* 数据表格 */ +.tbl { width: 100%; border-collapse: collapse; font-size: 14px; margin: 10px 0; } +.tbl th, .tbl td { border: 1px solid var(--line); padding: 8px 12px; text-align: left; } +.tbl th { background: var(--bg-2); font-weight: 600; } +.tbl tr:hover td { background: var(--hover-bg); } +.badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; } +.badge-ok { background: rgba(22, 163, 74, .14); color: var(--ok); } +.badge-no { background: rgba(148, 163, 184, .16); color: var(--text-3); } +/* IP 地址处理:来源分类徽章 */ +.badge-cdn { background: rgba(37, 99, 235, .14); color: var(--brand); } +.badge-priv { background: rgba(217, 119, 6, .15); color: var(--warn); } +.badge-pub { background: rgba(148, 163, 184, .16); color: var(--text-3); } +/* IP 处理结果预览(红色标记重点 IP) */ +.ip-pre { + font-family: Consolas, Menlo, monospace; font-size: 13px; line-height: 1.8; + background: var(--code-bg); color: var(--code-text); + border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; + white-space: pre-wrap; word-break: break-all; overflow: auto; max-height: 440px; +} +.ip-pre .ip-hit, .ip-pre .ip-seg-hit { color: var(--danger); font-weight: 700; } +.ip-pre .ip-seg-hit { background: rgba(220, 38, 38, .10); border-radius: 4px; } + +/* ============ 后台管理 ============ */ +.admin-nav { background: var(--brand-2); color: #fff; } +.admin-nav a { color: rgba(255, 255, 255, .9); } +.admin-nav-inner { display: flex; align-items: center; gap: 18px; padding: 10px 20px; flex-wrap: wrap; } +.admin-nav-inner .sp { margin-left: auto; display: flex; gap: 14px; align-items: center; } +.admin-nav-inner .logo { height: 30px; width: 30px; border-radius: 7px; object-fit: contain; background: #fff; padding: 2px; } +.admin-nav .brand .site-name { color: #fff; } +.admin-nav a.active { text-decoration: underline; text-underline-offset: 4px; font-weight: 600; } + +.fieldset-card { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 18px; margin-bottom: 20px; box-shadow: var(--shadow-sm); } +.fieldset-card > .fs-title { font-weight: 700; font-size: 16px; margin: 0 0 12px; padding-bottom: 8px; border-bottom: 1px solid var(--line); } +.form-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0 18px; } + +/* 后台导航内容管理 */ +.m-block { border: 1px solid var(--line); border-radius: 12px; margin-bottom: 16px; overflow: hidden; } +.m-block-head { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: var(--bg-2); flex-wrap: wrap; } +.m-block-head input[type=text] { max-width: 320px; } +.m-block-head input.b-title { flex: 1 1 240px; width: auto; max-width: 520px; } +.m-block-head .ops { margin-left: auto; display: flex; gap: 6px; align-items: center; } +.m-items { padding: 4px 12px 12px; } +.m-item { display: grid; grid-template-columns: 1fr 1.4fr 1.2fr 36px; gap: 8px; align-items: center; margin-top: 8px; } +.m-item .m-del { text-align: center; } +/* 后台搜索引擎管理行 */ +.e-row { display: grid; grid-template-columns: 70px 1.1fr 1.8fr 40px; gap: 8px; align-items: center; margin-top: 8px; } +/* 后台图标控件:预览框 + 文本输入 + 上传按钮 + 隐藏文件选择 */ +.icon-ctl { display: flex; align-items: center; gap: 6px; flex: 0 1 250px; min-width: 170px; } +.icon-ctl .icon-preview { + width: 30px; height: 30px; flex: 0 0 30px; border: 1px solid var(--line-2); border-radius: 8px; + background: var(--bg); color: var(--text-2); overflow: hidden; + display: flex; align-items: center; justify-content: center; + font-size: 13px; line-height: 1; white-space: nowrap; text-overflow: ellipsis; +} +.icon-ctl .icon-preview img { width: 100%; height: 100%; object-fit: contain; } +.icon-ctl .icon-input { flex: 1 1 90px; min-width: 70px; padding: 7px 9px; font-size: 13px; } +.icon-ctl .icon-upload { flex: 0 0 auto; padding: 7px 12px; font-size: 13px; } +/* 后台功能区工具管理行 / 首页快捷站点管理行:flex 自适应,窄屏自动换行,避免重叠 */ +.t-row, .l-row { + display: flex; flex-wrap: wrap; align-items: center; gap: 10px; + padding: 12px 0; margin-top: 12px; border-top: 1px dashed var(--line); +} +.t-row:first-child, .l-row:first-child { margin-top: 0; padding-top: 0; border-top: none; } +.t-row .icon-ctl, .l-row .icon-ctl { flex: 0 1 240px; } +.t-row .t-name, .l-row .l-name { flex: 1 1 120px; min-width: 90px; } +.t-row .t-desc { flex: 1 1 170px; min-width: 110px; } +.t-row .t-url, .l-row .l-url { flex: 2 1 230px; min-width: 150px; } +.t-row .t-ext { flex: 0 1 132px; min-width: 0; width: 100%; } +.l-row .l-note { flex: 1 1 170px; min-width: 120px; } +.t-row .ops, .l-row .ops { display: flex; gap: 6px; flex: 0 0 auto; margin-left: auto; } +.t-on { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: var(--text-2); white-space: nowrap; flex: 0 0 auto; } +.add-btn { margin: 6px 0 18px; } + +/* 登录页 */ +.login-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg-2); padding: 20px; } +.login-card { width: 100%; max-width: 380px; background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 30px; box-shadow: var(--shadow); } +.login-card h1 { text-align: center; font-size: 20px; margin-bottom: 4px; } +.login-card .lc-sub { text-align: center; color: var(--text-3); font-size: 13px; margin-bottom: 16px; } +.login-logo { text-align: center; margin-bottom: 10px; } +.login-logo img { width: 64px; height: 64px; border-radius: 14px; } + +/* 顶部 logo 占位展示 */ +.logo-preview { display: flex; align-items: center; gap: 14px; margin: 8px 0; } +.logo-preview img { width: 64px; height: 64px; border-radius: 12px; border: 1px solid var(--line); object-fit: contain; background: #fff; } + +/* ============ 响应式 ============ */ +@media (max-width: 1199px) { + .nav-area { margin: 24px 5% 36px; } +} +@media (max-width: 991px) { + .form-grid { grid-template-columns: 1fr; } +} +@media (max-width: 767px) { + .hero { min-height: 0; } + .hero-top { flex-direction: column; text-align: center; } + .hero-meta { text-align: center; } + .weather-box iframe { width: 100%; } + .quote-inner { flex-wrap: wrap; } + .boards { grid-template-columns: repeat(2, 1fr); gap: 12px; min-height: 0; } + .board-panel { width: 100%; } + .nav-cols { grid-template-columns: 1fr; gap: 14px; } + .nav-area { margin: 18px 12px 30px; } + .search-zone { padding-top: 6vh; } + .search-box { flex-wrap: wrap; border-radius: 16px; padding: 6px; } + .engine-btn { border-radius: 12px; } + .search-go { border-radius: 12px; } + .search-field { order: 3; flex-basis: 100%; } + .io-area { grid-template-columns: 1fr; } + .io-box textarea { min-height: 160px; } + .m-item { grid-template-columns: 1fr; gap: 4px; } + .e-row { grid-template-columns: 1fr; gap: 4px; } + .t-row, .l-row { gap: 6px; padding: 10px 0; } + .t-row > *, .l-row > * { flex: 1 1 100%; min-width: 0; } + .icon-ctl { min-width: 0; } + .t-row .t-on, .l-row .t-on { flex: 0 0 auto; } + .t-row .ops, .l-row .ops { flex: 0 0 auto; margin-left: auto; justify-content: flex-end; } + .m-block-head input[type=text] { max-width: 100%; } + .func-bar-title { font-size: 14px; } + .func-bar .spacer { width: 60px; } +} +@media (max-width: 600px) { + .nav-block-grid { grid-template-columns: 1fr; grid-auto-rows: auto; } + .nav-btn { justify-content: flex-start; padding: 8px 12px; } +} diff --git a/assets/img/logo.svg b/assets/img/logo.svg new file mode 100644 index 0000000..525095e --- /dev/null +++ b/assets/img/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/js/codec.js b/assets/js/codec.js new file mode 100644 index 0000000..2ce13db --- /dev/null +++ b/assets/js/codec.js @@ -0,0 +1,240 @@ +/* codec.js —— 编码/加解密纯前端算法库 + * 提供:UTF-8、Base64、Base32、URL、Unicode、SHA 系列(Web Crypto 原生) + * MD5 / SHA-224 依赖 CryptoJS(由页面懒加载,双 CDN fallback) + */ +(function (global) { + 'use strict'; + + /* ---------- UTF-8 工具 ---------- */ + function utf8Encode(str) { + if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(str); + // 降级实现 + var out = [], ch, i, code; + for (i = 0; i < str.length; i++) { + code = str.charCodeAt(i); + if (code < 0x80) out.push(code); + else if (code < 0x800) { + out.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f)); + } else if (code < 0xd800 || code >= 0xe000) { + out.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f)); + } else { + // surrogate pair + ch = (code - 0xd800) * 0x400 + (str.charCodeAt(++i) - 0xdc00) + 0x10000; + out.push(0xf0 | (ch >> 18), 0x80 | ((ch >> 12) & 0x3f), 0x80 | ((ch >> 6) & 0x3f), 0x80 | (ch & 0x3f)); + } + } + return new Uint8Array(out); + } + + function utf8Decode(bytes) { + if (typeof TextDecoder !== 'undefined') return new TextDecoder('utf-8').decode(bytes); + var out = '', i = 0, b1, b2, b3, b4, code; + while (i < bytes.length) { + b1 = bytes[i++]; + if (b1 < 0x80) { out += String.fromCharCode(b1); } + else if (b1 < 0xe0) { b2 = bytes[i++]; out += String.fromCharCode(((b1 & 0x1f) << 6) | (b2 & 0x3f)); } + else if (b1 < 0xf0) { + b2 = bytes[i++]; b3 = bytes[i++]; + out += String.fromCharCode(((b1 & 0x0f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f)); + } else { + b2 = bytes[i++]; b3 = bytes[i++]; b4 = bytes[i++]; + code = ((b1 & 0x07) << 18) | ((b2 & 0x3f) << 12) | ((b3 & 0x3f) << 6) | (b4 & 0x3f); + code -= 0x10000; + out += String.fromCharCode(0xd800 + (code >> 10), 0xdc00 + (code & 0x3ff)); + } + } + return out; + } + + function bytesToBin(bytes) { + var bin = '', i; + for (i = 0; i < bytes.length; i++) { + bin += String.fromCharCode(bytes[i]); + } + return bin; + } + + function binToBytes(bin) { + var out = new Uint8Array(bin.length), i; + for (i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; + } + + /* ---------- Base64(UTF-8 安全) ---------- */ + function b64Encode(str) { + var bytes = utf8Encode(str); + // 浏览器环境用 btoa + if (typeof btoa !== 'undefined') { + var bin = ''; + for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); + return btoa(bin); + } + return global.Base64 && global.Base64.encode(bytesToBin(bytes)) ? global.Base64.encode(bytesToBin(bytes)) : ''; + } + + function b64Decode(b64) { + b64 = String(b64 || '').replace(/\s+/g, ''); + var bin; + if (typeof atob !== 'undefined') { + bin = atob(b64); + } else if (global.Base64) { + bin = global.Base64.decode(b64); + } else { + throw new Error('当前环境不支持 Base64 解码'); + } + return utf8Decode(binToBytes(bin)); + } + + /* ---------- Base32(RFC 4648,UTF-8 安全) ---------- */ + var B32_ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + + function base32Encode(str) { + var bytes = utf8Encode(str), out = '', bits = 0, value = 0, i; + for (i = 0; i < bytes.length; i++) { + value = (value << 8) | bytes[i]; + bits += 8; + while (bits >= 5) { + out += B32_ALPHA[(value >>> (bits - 5)) & 31]; + bits -= 5; + } + } + if (bits > 0) out += B32_ALPHA[(value << (5 - bits)) & 31]; + while (out.length % 8 !== 0) out += '='; + return out; + } + + function base32Decode(b32) { + b32 = String(b32 || '').toUpperCase().replace(/[=\s]/g, ''); + if (!b32) return ''; + var bits = 0, value = 0, out = [], i, idx, b; + for (i = 0; i < b32.length; i++) { + idx = B32_ALPHA.indexOf(b32[i]); + if (idx === -1) throw new Error('Base32 字符串包含非法字符: ' + b32[i]); + value = (value << 5) | idx; + bits += 5; + if (bits >= 8) { + b = (value >>> (bits - 8)) & 0xff; + out.push(b); + bits -= 8; + } + } + return utf8Decode(new Uint8Array(out)); + } + + /* ---------- URL 编解码 ---------- */ + function urlEncode(str) { return encodeURIComponent(String(str)); } + function urlDecode(str) { return decodeURIComponent(String(str).replace(/\+/g, ' ')); } + + /* ---------- Unicode 编解码(支持中文、emoji) ---------- */ + function unicodeEncode(str) { + str = String(str); + var out = [], i, code; + for (i = 0; i < str.length; i++) { + code = str.charCodeAt(i); + if (code >= 0xd800 && code <= 0xdbff && i + 1 < str.length) { + // 代理对:按两个 \uXXXX 输出,JS 自动组合 + out.push('\\u' + code.toString(16).toUpperCase().padStart(4, '0')); + out.push('\\u' + str.charCodeAt(++i).toString(16).toUpperCase().padStart(4, '0')); + } else { + out.push('\\u' + code.toString(16).toUpperCase().padStart(4, '0')); + } + } + return out.join(''); + } + + function unicodeDecode(str) { + str = String(str); + // 兼容 \u{1F600} 与 \uXXXX 两种写法 + str = str.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, function (m, h) { + var cp = parseInt(h, 16); + if (cp > 0xffff) { + cp -= 0x10000; + return String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff)); + } + return String.fromCharCode(cp); + }); + str = str.replace(/\\u([0-9a-fA-F]{4})/g, function (m, h) { + return String.fromCharCode(parseInt(h, 16)); + }); + return str; + } + + /* ---------- SHA 系列(Web Crypto 原生) ---------- */ + var HASHES = { 'SHA-1': 'SHA-1', 'SHA-256': 'SHA-256', 'SHA-384': 'SHA-384', 'SHA-512': 'SHA-512' }; + + function hexFromBuffer(buf) { + var bytes = new Uint8Array(buf), out = '', i; + for (i = 0; i < bytes.length; i++) out += bytes[i].toString(16).padStart(2, '0'); + return out; + } + + function shaDigest(algorithm, text) { + var bytes = utf8Encode(text); + if (typeof crypto !== 'undefined' && crypto.subtle) { + return crypto.subtle.digest(algorithm, bytes).then(hexFromBuffer); + } + // 非安全上下文(如普通 http)无 Web Crypto:回退 crypto-js + return cryptoJsDigest(algorithm, text); + } + + /* ---------- CryptoJS 懒加载(MD5 / SHA-224 等需要) ---------- */ + var CDN_CRYPTO_JS = [ + 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js', + 'https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js' + ]; + + var cryptoJSPromise = null; + function loadCryptoJS() { + if (global.CryptoJS) return Promise.resolve(global.CryptoJS); + if (cryptoJSPromise) return cryptoJSPromise; + cryptoJSPromise = new Promise(function (resolve) { + var load = function (idx) { + if (global.CryptoJS) { resolve(global.CryptoJS); return; } + if (idx >= CDN_CRYPTO_JS.length) { resolve(null); return; } + var s = document.createElement('script'); + s.src = CDN_CRYPTO_JS[idx]; + s.onload = function () { resolve(global.CryptoJS || null); }; + s.onerror = function () { load(idx + 1); }; + document.head.appendChild(s); + }; + load(0); + }); + return cryptoJSPromise; + } + + var CRYPTO_JS_METHODS = { + 'MD5': 'MD5', 'SHA-1': 'SHA1', 'SHA-224': 'SHA224', + 'SHA-256': 'SHA256', 'SHA-384': 'SHA384', 'SHA-512': 'SHA512' + }; + + function cryptoJsDigest(algo, text) { + return loadCryptoJS().then(function (C) { + if (!C) throw new Error('计算 ' + algo + ' 需要联网加载加密组件(crypto-js)失败,请检查网络'); + var method = CRYPTO_JS_METHODS[algo]; + if (!method || typeof C[method] !== 'function') throw new Error('不支持的算法: ' + algo); + return C[method](text).toString(C.enc.Hex); + }); + } + + function hashText(algo, text) { + if (HASHES[algo]) return shaDigest(HASHES[algo], text); + if (algo === 'MD5' || algo === 'SHA-224') return cryptoJsDigest(algo, text); + return Promise.reject(new Error('未知算法: ' + algo)); + } + + /* ---------- 导出 ---------- */ + global.CodecLib = { + utf8Encode: utf8Encode, + utf8Decode: utf8Decode, + b64Encode: b64Encode, + b64Decode: b64Decode, + base32Encode: base32Encode, + base32Decode: base32Decode, + urlEncode: urlEncode, + urlDecode: urlDecode, + unicodeEncode: unicodeEncode, + unicodeDecode: unicodeDecode, + hashText: hashText, + hashes: ['MD5', 'SHA-1', 'SHA-224', 'SHA-256', 'SHA-384', 'SHA-512'] + }; +})(window); diff --git a/assets/js/common.js b/assets/js/common.js new file mode 100644 index 0000000..1342641 --- /dev/null +++ b/assets/js/common.js @@ -0,0 +1,308 @@ +/* common.js —— 全站通用交互 + * 功能:昼/夜主题切换、实时时钟、随机名言刷新、四大板块 hover 浮层、 + * 搜索栏(引擎下拉/清除/回车搜索)、复制到剪贴板、DOM 工具函数 + */ +(function (global) { + 'use strict'; + + /* ---------- 工具 ---------- */ + function $(sel, root) { return (root || document).querySelector(sel); } + function $$(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); } + + /* ---------- 主题(昼/夜) ---------- */ + var THEME_KEY = 'hp-theme'; + + function currentTheme() { + var saved = null; + try { saved = localStorage.getItem(THEME_KEY); } catch (e) { /* ignore */ } + if (saved === 'light' || saved === 'dark') return saved; + // 未设置:跟随系统 + return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'; + } + + function applyTheme(t, persist) { + document.documentElement.setAttribute('data-theme', t); + if (persist) { + try { localStorage.setItem(THEME_KEY, t); } catch (e) { /* ignore */ } + } + var icons = $$('.js-theme-icon'); + icons.forEach(function (el) { el.textContent = (t === 'dark') ? '☀' : '☾'; }); + var btn = $('.js-theme-btn'); + if (btn) btn.title = (t === 'dark') ? '切换到白天模式' : '切换到黑夜模式'; + } + + function initTheme() { + applyTheme(currentTheme(), false); + $$('.js-theme-btn').forEach(function (btn) { + btn.addEventListener('click', function () { + var next = (document.documentElement.getAttribute('data-theme') === 'dark') ? 'light' : 'dark'; + applyTheme(next, true); + }); + }); + if (window.matchMedia) { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { + // 用户手动选过则不跟随系统变化 + try { + if (!localStorage.getItem(THEME_KEY)) applyTheme(currentTheme(), false); + } catch (e) { applyTheme(currentTheme(), false); } + }); + } + } + + /* ---------- 实时时钟 ---------- */ + function pad(n) { return (n < 10 ? '0' : '') + n; } + function formatTime(d) { + return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); + } + function initClocks() { + function tick() { $$('.js-clock').forEach(function (el) { el.textContent = formatTime(new Date()); }); } + tick(); + setInterval(tick, 1000); + } + + /* ---------- 名言条:点击格言本身切换 ---------- */ + function initQuoteRefresh() { + $$('.js-quote').forEach(function (quote) { + quote.addEventListener('click', function () { + var list = $$('.js-quote-pool span', document); // 服务端把所有名言放入隐藏池 + if (!list.length) return; + // 定位当前展示文本在池中的位置,避免换到下一条仍是同一条 + var currentText = quote.textContent; + var cur = -1; + for (var i = 0; i < list.length; i++) { + if (list[i].textContent === currentText) { cur = i; break; } + } + var n = list.length; + var idx = Math.floor(Math.random() * n); + if (n > 1 && cur !== -1) { + while (idx === cur) idx = (idx + 1) % n; + } + quote.textContent = list[idx].textContent; + }); + }); + } + + /* ---------- 首页四大板块 hover 展开说明 ---------- */ + function initBoardHover() { + var section = $('.js-boards'); + if (!section) return; + var cards = $$('.js-board-card', section); + var overlay = $('.js-board-panel', section); + if (!cards.length || !overlay) return; + var titleEl = $('.js-board-panel-title', overlay); + var descEl = $('.js-board-panel-desc', overlay); + var bodyEl = section; + + function showPanel(card) { + var name = card.getAttribute('data-name') || ''; + var desc = card.getAttribute('data-desc') || ''; + var idx = cards.indexOf(card); + if (titleEl) titleEl.textContent = name; + if (descEl) descEl.textContent = desc; + overlay.classList.remove('panel-left', 'panel-right'); + // 第 0 / 1 块吸附左侧边缘,第 2 / 3 块吸附右侧边缘(实际板块位置不变) + if (idx === 0 || idx === 1) overlay.classList.add('panel-left'); + else overlay.classList.add('panel-right'); + overlay.classList.add('visible'); + } + + cards.forEach(function (card) { + card.addEventListener('mouseenter', function () { showPanel(card); }); + card.addEventListener('click', function () { /* 点击由 包裹,这里无额外处理 */ }); + }); + section.addEventListener('mouseleave', function () { overlay.classList.remove('visible'); }); + } + + /* ---------- 搜索栏 ---------- */ + var DEFAULT_ENGINES = [ + { key: 'bing', name: 'Bing', icon: 'B', url: 'https://www.bing.com/search?q={kw}' }, + { key: 'baidu', name: '百度', icon: '度', url: 'https://www.baidu.com/s?wd={kw}' }, + { key: 'github', name: 'GitHub', icon: 'G', url: 'https://github.com/search?q={kw}' }, + { key: 'google', name: 'Google', icon: 'G', url: 'https://www.google.com/search?q={kw}' } + ]; + + function initSearch() { + var box = $('.js-search'); + if (!box) return; + var cfg = null; + try { cfg = JSON.parse(box.getAttribute('data-engines') || 'null'); } catch (e) { cfg = null; } + var engines = (cfg && cfg.length) ? cfg : DEFAULT_ENGINES; + var curKey = box.getAttribute('data-engine') || engines[0].key; + var cur = engines.filter(function (e) { return e.key === curKey; })[0] || engines[0]; + + var input = $('.js-search-input', box); + var clearBtn = $('.js-search-clear', box); + var engineBtn = $('.js-engine-btn', box); + var engineName = $('.js-engine-name', box); + var menu = $('.js-engine-menu', box); + + function setEngine(e) { + cur = e; + // 按钮区域只显示图标(全称在展开下拉列表时展示) + if (engineName) engineName.textContent = e.icon || ''; + if (engineBtn) engineBtn.title = '搜索引擎:' + e.name; + box.setAttribute('data-engine', e.key); + hideMenu(); + } + function showMenu() { + if (!menu) return; + if (!menu.children.length) { + engines.forEach(function (e) { + var li = document.createElement('li'); + li.textContent = e.icon + ' ' + e.name; + li.className = (e.key === cur.key) ? 'active' : ''; + li.addEventListener('click', function () { setEngine(e); }); + menu.appendChild(li); + }); + } + menu.classList.add('open'); + } + function hideMenu() { if (menu) menu.classList.remove('open'); } + + function updateClear() { + if (clearBtn) clearBtn.style.visibility = (input && input.value) ? 'visible' : 'hidden'; + } + function doSearch() { + var kw = (input ? input.value : '').trim(); + if (!kw) { if (input) input.focus(); return; } + window.open(cur.url.replace('{kw}', encodeURIComponent(kw)), '_blank', 'noopener'); + } + + if (engineBtn) engineBtn.addEventListener('click', function (ev) { + ev.stopPropagation(); + if (menu && menu.classList.contains('open')) hideMenu(); else showMenu(); + }); + if (clearBtn) clearBtn.addEventListener('click', function () { + if (input) { input.value = ''; input.focus(); } + updateClear(); + }); + if (input) { + input.addEventListener('input', updateClear); + input.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') doSearch(); }); + } + document.addEventListener('click', function () { hideMenu(); }); + var goBtn = $('.js-search-go', box); + if (goBtn) goBtn.addEventListener('click', doSearch); + setEngine(cur); + updateClear(); + } + + /* ---------- 后台图标控件:文本 / 图片上传(≤200KB → dataURL) ---------- */ + function isIconImage(v) { + v = String(v || '').trim(); + if (!v) return false; + if (/^data:image\//i.test(v) || /^https?:\/\//i.test(v)) return true; + if (/\.(png|jpe?g|gif|svg|webp)(\?|#|$)/i.test(v)) return true; + return v.charAt(0) === '/' || v.indexOf('./') === 0 || v.indexOf('../') === 0; + } + function updateIconPreview(ctl) { + var input = ctl.querySelector('.icon-input'); + var box = ctl.querySelector('.icon-preview'); + if (!input || !box) return; + var v = input.value.trim(); + box.innerHTML = ''; + if (!v) return; // 空:显示空心占位框 + if (isIconImage(v)) { + var img = document.createElement('img'); + img.src = v; + img.alt = ''; + img.loading = 'lazy'; + box.appendChild(img); + } else { + box.textContent = v; + } + } + function initIconControls() { + // 点击“上传”→ 触发隐藏 file + document.addEventListener('click', function (ev) { + var btn = ev.target && ev.target.closest ? ev.target.closest('.icon-upload') : null; + if (!btn) return; + var ctl = btn.closest('.icon-ctl'); + var file = ctl ? ctl.querySelector('.icon-file') : null; + if (file) file.click(); + }); + // 选择图片 → 校验并转 dataURL 写入同控件文本框 + document.addEventListener('change', function (ev) { + var file = ev.target; + if (!file || !file.classList || !file.classList.contains('icon-file')) return; + var ctl = file.closest('.icon-ctl'); + var input = ctl ? ctl.querySelector('.icon-input') : null; + var f = file.files && file.files[0]; + function reset() { file.value = ''; } + if (!f) return; + if (f.type.indexOf('image/') !== 0) { alert('请选择图片文件。'); reset(); return; } + if (f.size > 200 * 1024) { alert('图片不能超过 200KB。'); reset(); return; } + var rd = new FileReader(); + rd.onload = function () { + if (input) { + input.value = rd.result; + updateIconPreview(ctl); + } + reset(); + }; + rd.onerror = function () { alert('图片读取失败,请重试。'); reset(); }; + rd.readAsDataURL(f); + }); + // 手动输入文本 → 实时预览 + document.addEventListener('input', function (ev) { + var t = ev.target; + if (t && t.classList && t.classList.contains('icon-input')) { + updateIconPreview(t.closest('.icon-ctl')); + } + }); + // 初始化页面既有控件预览 + $$('.icon-ctl').forEach(updateIconPreview); + } + + /* ---------- 复制 ---------- */ + function copyText(text, btnEl) { + function done(ok) { + if (!btnEl) return; + var old = btnEl.textContent; + btnEl.textContent = ok ? '✓ 已复制' : '复制失败'; + setTimeout(function () { btnEl.textContent = old; }, 1500); + } + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(function () { done(true); }, function () { fallbackCopy(text, done); }); + } else { + fallbackCopy(text, done); + } + } + function fallbackCopy(text, done) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.cssText = 'position:fixed;opacity:0;'; + document.body.appendChild(ta); + ta.select(); + try { done(document.execCommand('copy')); } catch (e) { done(false); } + document.body.removeChild(ta); + } + function initCopyButtons() { + document.addEventListener('click', function (ev) { + var btn = ev.target.closest ? ev.target.closest('.js-copy') : null; + if (!btn) return; + var text = btn.getAttribute('data-copy'); + if (text === null || text === undefined) { + var target = $(btn.getAttribute('data-target') || ''); + text = target ? target.value : ''; + } + copyText(text, btn); + }); + } + + /* ---------- 自动初始化 ---------- */ + document.addEventListener('DOMContentLoaded', function () { + initTheme(); + initClocks(); + initQuoteRefresh(); + initBoardHover(); + initSearch(); + initIconControls(); + initCopyButtons(); + }); + + global.HomePage = { + $: $, $$: $$, copyText: copyText, applyTheme: applyTheme, currentTheme: currentTheme + }; +})(window); diff --git a/assets/js/gmcrypto.js b/assets/js/gmcrypto.js new file mode 100644 index 0000000..53bb9c7 --- /dev/null +++ b/assets/js/gmcrypto.js @@ -0,0 +1,206 @@ +/* gmcrypto.js —— 国密 SM2 / SM3 / SM4 前端封装 + * SM1 为国密硬件算法(不公开、依赖专用加密芯片),无法在浏览器纯软件实现, + * 故本页提供可在网页中直接使用的 SM2(非对称)/ SM3(摘要)/ SM4(对称分组)。 + * 底层使用 sm-crypto@0.3.13,运行时按需双 CDN 懒加载(jsdelivr → unpkg)。 + */ +(function (global) { + 'use strict'; + + var VERSION = '0.3.13'; + var LIBS = { + sm2: { + file: 'sm2.js', key: 'sm2', + urls: [ + 'https://cdn.jsdelivr.net/npm/sm-crypto@' + VERSION + '/dist/sm2.js', + 'https://unpkg.com/sm-crypto@' + VERSION + '/dist/sm2.js' + ] + }, + sm3: { + file: 'sm3.js', key: 'sm3', + urls: [ + 'https://cdn.jsdelivr.net/npm/sm-crypto@' + VERSION + '/dist/sm3.js', + 'https://unpkg.com/sm-crypto@' + VERSION + '/dist/sm3.js' + ] + }, + sm4: { + file: 'sm4.js', key: 'sm4', + urls: [ + 'https://cdn.jsdelivr.net/npm/sm-crypto@' + VERSION + '/dist/sm4.js', + 'https://unpkg.com/sm-crypto@' + VERSION + '/dist/sm4.js' + ] + } + }; + var loadCache = {}; + + /** 加载指定算法库(返回 Promise<库对象|null>) */ + function loadLib(name) { + var lib = LIBS[name]; + if (!lib) return Promise.resolve(null); + if (global[lib.key]) return Promise.resolve(global[lib.key]); + if (loadCache[name]) return loadCache[name]; + loadCache[name] = new Promise(function (resolve) { + var load = function (idx) { + if (global[lib.key]) { resolve(global[lib.key]); return; } + if (idx >= lib.urls.length) { resolve(null); return; } + var s = document.createElement('script'); + s.src = lib.urls[idx]; + s.onload = function () { resolve(global[lib.key] || null); }; + s.onerror = function () { load(idx + 1); }; + document.head.appendChild(s); + }; + load(0); + }); + return loadCache[name]; + } + + /* ---------- 基础编码工具 ---------- */ + function utf8Bytes(str) { + if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(String(str)); + var out = [], code, i; + for (i = 0; i < str.length; i++) { + code = str.charCodeAt(i); + if (code < 0x80) out.push(code); + else if (code < 0x800) out.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f)); + else if (code < 0xd800 || code >= 0xe000) { + out.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f)); + } else { + var ch = (code - 0xd800) * 0x400 + (str.charCodeAt(++i) - 0xdc00) + 0x10000; + out.push(0xf0 | (ch >> 18), 0x80 | ((ch >> 12) & 0x3f), 0x80 | ((ch >> 6) & 0x3f), 0x80 | (ch & 0x3f)); + } + } + return new Uint8Array(out); + } + + function hexToBytes(hexStr) { + var h = String(hexStr).replace(/[^0-9a-fA-F]/g, ''); + if (h === '' || h.length % 2 !== 0) throw new Error('十六进制串长度需为偶数'); + var out = new Uint8Array(h.length / 2), i; + for (i = 0; i < out.length; i++) { + out[i] = parseInt(h.substr(i * 2, 2), 16); + } + return out; + } + + function bytesToHex(bytes) { + var s = '', i, t; + for (i = 0; i < bytes.length; i++) { + t = bytes[i].toString(16); + s += t.length < 2 ? '0' + t : t; + } + return s; + } + + function bytesToB64(bytes) { + var s = '', i; + for (i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]); + return global.btoa(s); + } + + function b64ToBytes(b64) { + var s = global.atob(String(b64).trim()); + var out = new Uint8Array(s.length), i; + for (i = 0; i < s.length; i++) out[i] = s.charCodeAt(i); + return out; + } + + function randomHex(bytesLen) { + var c = global.crypto || global.msCrypto; + var out = new Uint8Array(bytesLen), i; + if (c && c.getRandomValues) { + c.getRandomValues(out); + } else { + for (i = 0; i < bytesLen; i++) out[i] = Math.floor(Math.random() * 256); + } + return bytesToHex(out); + } + + /* ---------- SM4 密钥 / IV 规范化 ---------- */ + function sm4KeyBytes(keyStr) { + var s = String(keyStr || '').trim(); + if (!s) throw new Error('请输入 SM4 密钥'); + if (/^[0-9a-fA-F]{32}$/.test(s)) return hexToBytes(s); + var b = utf8Bytes(s); + if (b.length === 16) return b; + throw new Error('SM4 密钥需为 128 位(16 字节):可输入 32 位十六进制串,或恰好 16 个 ASCII 字符'); + } + + function sm4IvHex(ivStr) { + var s = String(ivStr || '').trim(); + if (/^[0-9a-fA-F]{32}$/.test(s)) return s; + var b = utf8Bytes(s); + if (b.length === 16) return bytesToHex(b); + throw new Error('SM4 CBC 模式需要 16 字节 IV(32 位十六进制或恰好 16 个字符)'); + } + + /* ---------- 业务 Promise ---------- */ + function asArray(bytes) { return Array.prototype.slice.call(bytes); } + + function need(name) { + return loadLib(name).then(function (lib) { + if (!lib) throw new Error('国密 ' + name.toUpperCase() + ' 组件加载失败,请检查网络后重试'); + return lib; + }); + } + + function sm3Digest(text, hmacKeyHex) { + return need('sm3').then(function (sm3) { + if (hmacKeyHex) return sm3(String(text), { key: hmacKeyHex }); + return sm3(String(text)); + }); + } + + function sm4Run(text, keyStr, mode, ivStr, isEncrypt) { + return need('sm4').then(function (sm4) { + var kb = asArray(sm4KeyBytes(keyStr)); + var opt = {}; + if (mode === 'cbc') opt = { mode: 'cbc', iv: sm4IvHex(ivStr) }; + if (isEncrypt) { + var hex = sm4.encrypt(String(text), kb, opt); + return { ok: true, hex: hex }; + } + var raw = String(text).trim().replace(/\s+/g, ''); + var inArr; + if (/^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0) inArr = asArray(hexToBytes(raw)); + else inArr = asArray(b64ToBytes(raw)); + var out = sm4.decrypt(inArr, kb, opt); + return { ok: true, hex: '', text: out }; + }); + } + + function sm2GenKey() { + return need('sm2').then(function (sm2) { + var kp = sm2.generateKeyPairHex(); + return { publicKey: kp.publicKey, privateKey: kp.privateKey }; + }); + } + + function sm2Encrypt(text, publicKey) { + return need('sm2').then(function (sm2) { + if (!String(publicKey || '').trim()) throw new Error('SM2 加密请填写对方公钥'); + return sm2.doEncrypt(String(text), String(publicKey).trim(), 1); + }); + } + + function sm2Decrypt(cipherHex, privateKey) { + return need('sm2').then(function (sm2) { + if (!String(privateKey || '').trim()) throw new Error('SM2 解密请填写自己的私钥'); + return sm2.doDecrypt(String(cipherHex).trim().replace(/\s+/g, ''), String(privateKey).trim(), 1); + }); + } + + /* ---------- 导出 ---------- */ + global.GmCrypto = { + load: loadLib, + utf8Bytes: utf8Bytes, + hexToBytes: hexToBytes, + bytesToHex: bytesToHex, + bytesToB64: bytesToB64, + b64ToBytes: b64ToBytes, + randomHex: randomHex, + sm3: sm3Digest, + sm4: sm4Run, + sm2Key: sm2GenKey, + sm2Encrypt: sm2Encrypt, + sm2Decrypt: sm2Decrypt + }; +})(window); diff --git a/assets/js/ip.js b/assets/js/ip.js new file mode 100644 index 0000000..4687194 --- /dev/null +++ b/assets/js/ip.js @@ -0,0 +1,289 @@ +/* ip.js —— IP 地址处理 + * 提取输入文本中全部 IPv4 → 按 /24 段聚合输出(文本 / 高亮预览): + * IP段 发现数量(段内去重IP数) IP段性质 + * 1.2.3.0/24 8 公网 + * 1.2.3.4 + * + * 「重点匹配 IP」:填写的 IP 命中后,其所在 /24 段优先置顶、对应 IP 红色标出。 + * CDN 段库由服务端通过 #ipCdnRaw 隐藏文本框注入,可在后台维护。 + */ +(function () { + 'use strict'; + + function $(id) { return document.getElementById(id); } + + // 内网 / 保留地址段(IPv4) + var PRIV_RANGES = [ + ['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8], + ['169.254.0.0', 16], ['172.16.0.0', 12], ['192.168.0.0', 16], + ['192.0.2.0', 24], ['198.51.100.0', 24], ['203.0.113.0', 24], + ['224.0.0.0', 4], ['240.0.0.0', 4] + ].map(function (r) { + return { net: ipToInt(r[0]), prefix: r[1] }; + }); + + function ipToInt(ip) { + var p = String(ip).split('.'); + return (((+p[0]) << 24) + ((+p[1]) << 16) + ((+p[2]) << 8) + (+p[3])) >>> 0; + } + function inRange(ipInt, net, prefix) { + var mask = (prefix === 0) ? 0 : ((0xFFFFFFFF << (32 - prefix)) >>> 0); + return ((ipInt & mask) >>> 0) === ((net & mask) >>> 0); + } + function isPrivate(ipInt) { + for (var i = 0; i < PRIV_RANGES.length; i++) { + if (inRange(ipInt, PRIV_RANGES[i].net, PRIV_RANGES[i].prefix)) return true; + } + return false; + } + + /* ---------- CDN 段库解析 ---------- */ + function parseCdnText(text) { + var out = [], lines = String(text || '').split(/\r?\n/), i, m, p; + for (i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (!line || line.charAt(0) === '#') continue; + p = line.split(/\s+/); + m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(p[0]); + if (!m) continue; + var a = +m[1], b = +m[2], c = +m[3], d = +m[4], pre = +m[5]; + if (a > 255 || b > 255 || c > 255 || d > 255 || pre > 32) continue; + out.push({ + net: (((a << 24) + (b << 16) + (c << 8) + d) >>> 0), + prefix: pre, + label: p.slice(1).join(' ') || 'CDN' + }); + } + return out; + } + function cdnOf(ipInt, ranges) { + for (var i = 0; i < ranges.length; i++) { + if (inRange(ipInt, ranges[i].net, ranges[i].prefix)) return ranges[i].label; + } + return null; + } + + /* ---------- IPv4 提取 ---------- */ + function extractIps(text) { + var re = /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/g, m, found = []; + while ((m = re.exec(text)) !== null) { + var a = +m[1], b = +m[2], c = +m[3], d = +m[4]; + if (a > 255 || b > 255 || c > 255 || d > 255) continue; + found.push(a + '.' + b + '.' + c + '.' + d); + } + return found; + } + + /* ---------- 重点 IP 解析 ---------- */ + function parseFocus(text) { + var set = {}; + var parts = String(text || '').split(/[\s,,;;]+/); + for (var i = 0; i < parts.length; i++) { + var p = parts[i].trim(); + if (!p) continue; + var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(p); + if (!m) continue; + if (+m[1] > 255 || +m[2] > 255 || +m[3] > 255 || +m[4] > 255) continue; + set[m[1] + '.' + m[2] + '.' + m[3] + '.' + m[4]] = true; + } + return set; + } + + function ipLess(a, b) { + var aa = a.split('.').map(Number), bb = b.split('.').map(Number), k; + for (k = 0; k < 4; k++) { + if (aa[k] !== bb[k]) return aa[k] - bb[k]; + } + return 0; + } + function keyLess(a, b) { + var aa = a.split('.').map(Number), bb = b.split('.').map(Number), k; + for (k = 0; k < 3; k++) { + if (aa[k] !== bb[k]) return aa[k] - bb[k]; + } + return 0; + } + + var TYPE_ZH = { priv: '内网', cdn: 'CDN', pub: '公网' }; + + /* 返回 { segs:[{line, hit, ips:[{ip,hit}]}], segTotals, totalIps, totalSeg, hitSeg, hitIps, rangeCount } */ + function analyze(text, cdnRaw, focusSet) { + var all = extractIps(text); + if (!all.length) return null; + var ranges = parseCdnText(cdnRaw); + + var segMap = {}, segOrder = []; + for (var i = 0; i < all.length; i++) { + var ip = all[i]; + var key = ip.substring(0, ip.lastIndexOf('.')); + if (!segMap[key]) { + segMap[key] = { key: key, ips: [], classes: {} }; + segOrder.push(key); + } + var seg = segMap[key]; + if (seg.ips.indexOf(ip) === -1) { + seg.ips.push(ip); + var int = ipToInt(ip); + var vendor = cdnOf(int, ranges); + var cls = vendor ? 'cdn' : (isPrivate(int) ? 'priv' : 'pub'); + seg.classes[cls] = (seg.classes[cls] || 0) + 1; + } + } + segOrder.sort(keyLess); + + function segLabel(s) { + var keys = Object.keys(s.classes); + if (keys.length === 1) return TYPE_ZH[keys[0]]; + var best = keys.slice().sort(function (x, y) { + return (s.classes[y] - s.classes[x]) || (TYPE_ZH[x] < TYPE_ZH[y] ? -1 : 1); + })[0]; + return TYPE_ZH[best] + '为主(混合)'; + } + + var segs = segOrder.map(function (k) { return segMap[k]; }); + segs.forEach(function (s) { s.label = segLabel(s); }); + // 命中重点 IP 的段排前,组内段号升序 + var focusSegs = [], otherSegs = []; + segs.forEach(function (s) { + s.hit = s.ips.some(function (ip) { return focusSet[ip]; }); + (s.hit ? focusSegs : otherSegs).push(s); + }); + segs = focusSegs.concat(otherSegs); + + var totalIps = 0, hitIps = 0, hitSeg = 0; + segs.forEach(function (s) { + totalIps += s.ips.length; + if (s.hit) hitSeg++; + s.ips.sort(ipLess); + var h = [], o = []; + s.ips.forEach(function (ip) { + if (focusSet[ip]) { h.push(ip); hitIps++; } + else o.push(ip); + }); + s.details = h.concat(o); + s.line = s.key + '.0/24\t' + s.ips.length + '\t' + s.label; + }); + + var cntSeg = { priv: 0, cdn: 0, pub: 0 }; + segs.forEach(function (s) { + var lab = s.label; + if (lab.indexOf('内网') === 0) cntSeg.priv++; + else if (lab.indexOf('CDN') === 0) cntSeg.cdn++; + else cntSeg.pub++; + }); + + return { + segs: segs, + totalIps: totalIps, + totalSeg: segs.length, + hitSeg: hitSeg, + hitIps: hitIps, + cntSeg: cntSeg, + rangeCount: ranges.length + }; + } + + /* ---------- 文本 / 高亮输出 ---------- */ + function esc(s) { + return String(s).replace(/&/g, '&').replace(//g, '>'); + } + function plainText(data) { + var lines = ['IP段\t发现数量\tIP段性质']; + data.segs.forEach(function (s) { + lines.push(s.line); + s.details.forEach(function (ip) { lines.push('\t' + ip); }); + lines.push(''); + }); + return lines.join('\n'); + } + function previewHtml(data) { + var parts = []; + parts.push(esc('IP段\t发现数量\tIP段性质').replace(/\t/g, ' ')); + data.segs.forEach(function (s) { + var lineTxt = s.line.replace(/\t/g, ' '); + parts.push(s.hit ? '★ ' + esc(lineTxt) + '' : esc(lineTxt)); + s.details.forEach(function (ip) { + parts.push(focusCur[ip] ? '' + esc(ip) + '' : esc(ip)); + }); + parts.push(''); + }); + return parts.join('\n'); + } + var focusCur = {}; + + /* ---------- 事件 ---------- */ + var lastData = null; + function run() { + var input = $('ipInput'); + var msg = $('ipMsg'); + msg.innerHTML = ''; + var text = input.value; + if (!text.trim()) { + msg.innerHTML = '
请先粘贴要分析的日志 / 文本内容。
'; + return; + } + var cdnRaw = ($('ipCdnRaw') && $('ipCdnRaw').value) || ''; + var fs = parseFocus($('ipFocus').value); + focusCur = fs; + lastData = analyze(text, cdnRaw, fs); + if (!lastData) { + $('ipResult').hidden = true; + msg.innerHTML = '
未在输入内容中解析到有效的 IPv4 地址。
'; + return; + } + var d = lastData; + $('ipPreview').innerHTML = previewHtml(d); + var st = d.cntSeg; + var note = '共提取去重 IP ' + d.totalIps + ' 个,涉及 ' + d.totalSeg + ' 个 /24 段(内网 ' + + st.priv + ' 段 / CDN ' + st.cdn + ' 段 / 公网 ' + st.pub + ' 段)。'; + if (Object.keys(parseFocus($('ipFocus').value)).length === 0) { + note += ' 未填写重点 IP,按原顺序输出。'; + } else if (d.hitIps === 0) { + note += ' 填写的重点 IP 均未在输入内容中命中。'; + } else { + note += ' 命中重点 IP ' + d.hitIps + ' 个,' + d.hitSeg + ' 个 /24 段已优先置顶。'; + } + if (d.rangeCount === 0) note += ' CDN 段库为空,可在后台补充。'; + $('ipStat').textContent = note; + $('ipResult').hidden = false; + msg.innerHTML = ''; + } + function copyText(text) { + function done(ok) { + var btn = $('ipCopy'); + var old = btn.textContent; + btn.textContent = ok ? '✓ 已复制' : '复制失败'; + setTimeout(function () { btn.textContent = old; }, 1200); + } + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(function () { done(true); }, function () { fallback(text, done); }); + } else { + fallback(text, done); + } + } + function fallback(text, done) { + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.cssText = 'position:fixed;opacity:0;'; + document.body.appendChild(ta); + ta.select(); + try { done(document.execCommand('copy')); } catch (e) { done(false); } + document.body.removeChild(ta); + } + function clearAll() { + $('ipInput').value = ''; + $('ipFocus').value = ''; + $('ipMsg').innerHTML = ''; + $('ipResult').hidden = true; + $('ipInput').focus(); + } + + $('ipBtn').addEventListener('click', run); + $('ipCopy').addEventListener('click', function () { + if (lastData) copyText(plainText(lastData)); + }); + $('ipClear').addEventListener('click', clearAll); + $('ipInput').addEventListener('keydown', function (ev) { + if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) run(); + }); +})(); diff --git a/data/avlist.js b/data/avlist.js new file mode 100644 index 0000000..806dbf9 --- /dev/null +++ b/data/avlist.js @@ -0,0 +1,55 @@ +// avlist.js —— 杀软/安全软件进程识别映射表 +// 格式:进程名 -> "产品 - 组件说明"。匹配时忽略大小写。 +// 可在此文件追加条目;如需通过后台管理,可在"内容设计"中扩展。 +var avList = { + "360tray.exe": "360安全卫士 - 实时保护", + "360safe.exe": "360安全卫士 - 主程序", + "360rp.exe": "360杀毒 - 实时监控", + "360sd.exe": "360杀毒 - 主程序", + "QQPCRTP.exe": "QQ电脑管家 - 实时防护", + "QQPCMgr.exe": "QQ电脑管家 - 主程序", + "kxescore.exe": "金山毒霸 - 核心防护", + "kwsprotect64.exe": "金山毒霸 - 服务进程", + "kavsvc.exe": "金山毒霸 - 杀毒引擎", + "risingcore.exe": "瑞星 - 云安全引擎", + "ravmond.exe": "瑞星杀毒软件 - 监控进程", + "rsmain.exe": "瑞星杀毒软件 - 主程序", + "ekrn.exe": "ESET NOD32 - 核心服务", + "egui.exe": "ESET NOD32 - 图形界面", + "MsMpEng.exe": "Windows Defender - 恶意软件防护", + "NisSrv.exe": "Windows Defender - 网络检查服务", + "Sense.exe": "Windows Defender ATP - 终端感知", + "avp.exe": "卡巴斯基 - 核心进程", + "avpui.exe": "卡巴斯基 - 用户界面", + "avgnt.exe": "Avira/小红伞 - 实时防护", + "avguard.exe": "Avira/小红伞 - 扫描守护", + "avcenter.exe": "Avast - 主控制台", + "ashWebSv.exe": "Avast - 网页防护", + "ccSvcHst.exe": "赛门铁克/诺顿 - 核心服务", + "rtvscan.exe": "诺顿杀毒 - 扫描进程", + "McAfeeSecurity.exe": "McAfee - 安全中心", + "UdaterUI.exe": "McAfee - 更新程序", + "mfemms.exe": "McAfee - 管理服务", + "hipsvc.exe": "火绒安全 - 主服务", + "HipsTray.exe": "火绒安全 - 托盘程序", + "usysdiag.exe": "火绒安全 - 诊断工具", + "wsctrl.exe": "火绒安全 - 网络控制", + "TMBMSRV.exe": "趋势科技 - 管理服务", + "PccNTMon.exe": "趋势科技 - 监控", + "WRSA.exe": "Webroot SecureAnywhere", + "dwengine.exe": "Deep Instinct - 引擎", + "CylanceSvc.exe": "Cylance - 服务", + "MBAMService.exe": "Malwarebytes - 服务", + "mbam.exe": "Malwarebytes - 主程序", + "SophosUI.exe": "Sophos - 界面", + "SEDService.exe": "Sophos - 终端检测服务", + "vsserv.exe": "比特梵德 - 服务进程", + "bdagent.exe": "比特梵德 - 代理进程", + "cis.exe": "Comodo Internet Security", + "cavsvc.exe": "Comodo - 反病毒服务", + "LavasoftTcpService.exe": "Ad-Aware - 服务", + "sp_svc.exe": "超级巡警", + "HipsDaemon.exe": "火绒/其他HIPS - 守护进程", + "360Tray.exe": "360安全卫士 - 托盘(同360tray)", + "avgui.exe": "Avast - 图形界面" +}; diff --git a/data/homepage.db b/data/homepage.db new file mode 100644 index 0000000000000000000000000000000000000000..994ee891b3bf523d9318586cef58fc41050527fc GIT binary patch literal 49152 zcmeI*ZBP^G9RTp%Y)HaOvbNUAIF7?w>YR*vCV)6z=Q`z(T3X>a#c7>uO+&I`N=z^t zymLA?LwH9~up+(y0w)5V2%N8w0M7JFXKvbWy?*I8&F&^2Iy%$q)K8uMpIvrI6g)a} zAI_Wq4hhfhKKs1TAyTpku`4l8*Ih}1yBG5PyhwK z`vOhB%QoBd^5|A`*j?l2Pd4~GbuPazP$#4Wb4m}El~k8;)g|v8DC1IgaNDd|+|uIn zs)6##>azW12f3<)lxstE}F!d`!T7f+t5+m;Jh$t9&0G zhYuV`+20WID~YD<=k}HTtmN>4YVNJKQlk1i!GNCBiZKE~cxRzywqguEKn%Q`Rt2p@ zdXCPN4257QOp;qQvO}$Besfip*}i29-Bza%A8QDBT;X8QFD!?$(}0(^ujHGW=l^3& zj!Sb)sf&iE$61j_1AKifFE4mPzWT5a+C#~2#k91R)zf;#I=yuT7yogX4+Y%**MO(g zNNY-x{;DmsQggoc0If-zxJHj^;*WTiEzzIny=)QY#3#;oxN z;rfJ2O*=?qOR2iwUBfGv&1$W!+piWuZ|K$APdoedf@rO*5k-DwQO^3N(>jq-c*RH! zWxj%>ZeKx3tL|EiJnrz<2!(xNKfDyIR{f>AX?ea}v!)-Ll$y1aC^duZ_}XB|2dBLr zGN;vSdHV`ABj=lrxR&bG@u!tz$dgio!aU)x2kpA?Yc zYI=oclYRddn(_s_{KvvcKU_C1cSAU+th<(}T+ZdNrQT?^Z{AEt3KemI4~Kn$T0#4r zk*2L}O|@3d6s;8Elxyac+uy(|oJ%j-yE07n(#@+CQ_I8U)Iz2z#$?~Voj#(LP_35$ ze^2>01BW03ZTH7B#=)VDSL(6d%_pk<1O~M9emNA z;yuM4kE3R9VUgEST;wQl?=I#GJdP}=7tQ{Mf*)K^00mG01yBG5Pyhu`00mG01yBG5 zzFPto#-6G450LTy40euUpR*GCclOT^#03RV00mG01yBG5Pyhu`00mG01yJDIB9N28 zi?XIp=&>=(7J7MBo5^B;&-=<3|7|qvS)S-&B%lAYZNH${r|dBMNA@IJ z$`;!GVw<*E9AZO+2oUcPomp5xe_;$oh2Y=vNPO|iF#b2=Pb^M9DdY%TP=g|Mrk-V671 z@D?}raP#wa3GyT7di0Esmtf$wN02({1APe4^ts`Sw*#?(C2To2AZ1X`u6IW0N#6 z30uXsR&i=B9&3#+_KSDM;&a3DaIbvpVSM4S+&cl$Y$nLIHW;XN2b3H4?0`?=6-(=Z zU_3f0wob|m7sRPfakSkD67o@9GPa7jrZ!CG`ZhZEaLJJJ^KgtOZH#v)0dhU zS5W{3Pyhu`00mG01yBG5Pyhu`00mYMfG!bwsY@h#9kZ2A?c>P$K9f(UcW7j?Odg%m zp<%HyxwPJ&VJ3T1`W;MUuio#Fk;QDJ)qVvd*`fC!Fyye59t1-Ud~rbU4j|+IM(Yg4 z`fLwun<0P;3ZMWApa2S>01BW03ZMWApuo36pw4KaHso%I?4rqkG*QQU#puQ7L*p~j z<(uN{Z0bNTj0=-d=++5!I9QzNRu4!vFNgyZiN{xy?Oo62yX2-hu{|dD-%l(qif4ur zu(vA;1Kq~R28QIeDe?(j1Gno>TpUbZ8B%iVA9*(0ogDmB?70Br&*I`pd~qCxof8jQ zq-amFzYS7_k!lEt(O#+Xrg-rj1Yo>coQNcvBl6i>a@Tn>eElGjZ0{C3+NA3}q_Ew6 ziRp+GorXl@{#mj09vRA3#s}o{ZA1P#Wt90h3 z)cX*sLyDnhHx!je8WU5CQs07J)a30eiTe|zMp~JrzIJKk29((l`2lUAHbKjMl35?D zZ}7WA>L9+td9hEm6i3J7^OuQ4#~^(7Ako-L3Oh9-PhFMmlc&&C%n%J*VW8^Woa5Q(W#;DmCK@qZ)x4~mVm z|7QOQ0bEc31yBG5Pyhu`00mG01yBG5PyhwqBmoPvLFptik^t$yVa&{W(qUrAVm4`= z5bFQ`dqJ@;-lVEwSy2E5Pyhu`00mG01yBG5Pyhu`00q_}V4^dqOxEI5KmUJ4v0ts_ zD2#&wD1ZVefC4Ch0w{n2D1ZVefC4D+rU+!wX87#S!l(a&Lh||FWIau>H`sl)xUJ3l zUs%Bf1yBG5Pyhu`00mG01yBG5P~e*rh!{-S)b^69;C*!A5bkMeb>P zKGfACb@#x0crvN~G0X_RL#A%GP9&Qrxbmt~MVvVIP>v13)NrvqDxaT`CP$=8Gi2I# z+c~ML6YgOTbHuq_T{ShbjuVB5!7SVUQY0;NZJHs5im!pX&F zqw(2ukiR(JD346WV-Lwx_IsC}&3D$G@C3cQhm<4ZH^yvgD_GFPLK^HU zKGZ)V--TKA_qniJsPp@TFqc@o2$d#v?V3xro=ZILmnY6b_Hc)R<@xOk(~y_@6iMB9 zl*yu;>#Yr2An|y#OOD-_ha+01BW03ZMWApuo3J;D4`&YE%FK literal 0 HcmV?d00001 diff --git a/data/quota.txt b/data/quota.txt new file mode 100644 index 0000000..c53af12 --- /dev/null +++ b/data/quota.txt @@ -0,0 +1,13 @@ +# 名人名言库(quota.txt) +# 每行一条,空行或 # 开头的行会被忽略;后台"内容设计"页可在线维护本文件。 + +道阻且长,行则将至。 +知识就是力量。 +知彼知己,百战不殆。 +学而不思则罔,思而不学则殆。 +合抱之木,生于毫末;九层之台,起于累土。 +工欲善其事,必先利其器。 +人类的一切智慧,都包含在"等待"和"希望"这四个字里面。 +所有的胜利,首先是思想的胜利。 +保持热爱,奔赴山海。 +真正的自由,来源于自律与清醒。 diff --git a/func/_bar.php b/func/_bar.php new file mode 100644 index 0000000..2b518dd --- /dev/null +++ b/func/_bar.php @@ -0,0 +1,15 @@ +'; + echo '
'; + echo '← 返回功能区首页'; + echo '' . he($title) . ''; + echo '首页'; + echo '
' . "\n"; +} diff --git a/func/av.php b/func/av.php new file mode 100644 index 0000000..e0b515e --- /dev/null +++ b/func/av.php @@ -0,0 +1,142 @@ + +
+
+
+

使用说明

+

在目标机器执行 tasklist 复制全部输出,粘贴到下方输入框后点击“识别”。 + 系统会逐行解析进程名并与预置的安全软件进程表(data/avlist.js)进行大小写不敏感匹配,表格仅展示识别出的安全软件进程,常规进程不展示。

+ + +
+ + +
+
+
+ + +
+
+ + + + + + + + diff --git a/func/codec.php b/func/codec.php new file mode 100644 index 0000000..09f8423 --- /dev/null +++ b/func/codec.php @@ -0,0 +1,148 @@ + +
+
+
+
+ + +
+ + +
+ + + +
+
+
+ +
+
+
+ 输入区(可编辑) + +
+ +
+
+
+ 输出区(可编辑) + +
+ +
+
+
+
+ + + + + + + + diff --git a/func/gmcodec.php b/func/gmcodec.php new file mode 100644 index 0000000..815a002 --- /dev/null +++ b/func/gmcodec.php @@ -0,0 +1,237 @@ + +
+
+
+

支持 SM2 / SM3 / SM4 三种国密算法;SM1 为不公开的硬件算法(需国密芯片),无法纯软件实现。 + SM2/SM4 运算依赖在线加载 sm-crypto 组件(双 CDN),加载失败会明确提示。

+ +
+ + +
+ + +
+ + + +
+ + +
+
+ + + +
+
+ + + + + + +
+
+ + + + + + + +
+
+ +
+
+
+ 输入区(明文 / 待解密内容,可编辑) + +
+ +
+
+
+ 输出区(可编辑) + +
+ +
+
+
+
+ + + + + + + + diff --git a/func/index.php b/func/index.php new file mode 100644 index 0000000..75f088e --- /dev/null +++ b/func/index.php @@ -0,0 +1,49 @@ +query('SELECT * FROM func_tools WHERE enabled = 1 ORDER BY sort ASC, id ASC')->fetchAll(); + +layout_head('功能区'); +?> +
+ +
+ ← 返回首页 +

功能区

+

工具统一收纳于此;新工具的入口可在后台“功能区管理”中登记

+
+ +
+ + + +
+
功能区还没有可用的工具,请到后台“功能区管理”中添加。
+
+ +
+
+ + + + + + diff --git a/func/ip.php b/func/ip.php new file mode 100644 index 0000000..f96ce43 --- /dev/null +++ b/func/ip.php @@ -0,0 +1,59 @@ + +
+
+
+

使用说明

+

粘贴任意日志 / 文本,系统提取其中全部 IPv4(IPv6 暂不参与), + 按 /24 段聚合为文本结果:每段一行 IP段 / 发现数量 / IP段性质, + 其中「发现数量」为该段内去重后的 IP 个数;段行下方缩进列出该段具体 IP,段与段之间空一行分隔。 + 段性质按 内网(含保留)/ CDN / 公网 区分。 + 若在下方「重点匹配 IP」中填写了需要关注的 IP,命中其所在的 /24 段会优先置顶,并在预览结果中用红色标出该 IP(复制得到的纯文本不含颜色)。

+ + + + +
+ + + 支持 Ctrl / Cmd + Enter 快速分析 +
+
+
+ + +
+
+ + + + + + + + diff --git a/func/password.php b/func/password.php new file mode 100644 index 0000000..f20f2c7 --- /dev/null +++ b/func/password.php @@ -0,0 +1,143 @@ + +
+
+
+
+ + + + +
+
+ + +
+
+ + + + +
+
+ + +
+
+
+
+
+
+ + + + + + + diff --git a/func/qrcode.php b/func/qrcode.php new file mode 100644 index 0000000..73c6c7c --- /dev/null +++ b/func/qrcode.php @@ -0,0 +1,148 @@ + +
+
+
+
+ + + +
+
+ + +
+
+ + +
+
+
+ + + + + + + diff --git a/includes/auth.php b/includes/auth.php new file mode 100644 index 0000000..37a061a --- /dev/null +++ b/includes/auth.php @@ -0,0 +1,101 @@ +prepare('SELECT password_hash FROM users WHERE username = ? LIMIT 1'); + $st->execute([$username]); + $row = $st->fetch(); + if (!$row) { + return false; + } + return hash_equals($row['password_hash'], hp_password_hash($plain)); +} + +/** 执行登录(成功返回 true) */ +function hp_login(string $username, string $plain): bool +{ + if (hp_verify_user($username, $plain)) { + session_regenerate_id(true); + $_SESSION['admin'] = $username; + return true; + } + return false; +} + +/** 修改当前用户密码 */ +function hp_change_password(string $oldPlain, string $newPlain): array +{ + if (!is_logged_in()) { + return [false, '未登录']; + } + if (!hp_verify_user((string)$_SESSION['admin'], $oldPlain)) { + return [false, '原密码不正确']; + } + if (strlen($newPlain) < 6) { + return [false, '新密码至少 6 位']; + } + $st = db()->prepare('UPDATE users SET password_hash = ? WHERE username = ?'); + $st->execute([hp_password_hash($newPlain), (string)$_SESSION['admin']]); + return [true, '密码修改成功']; +} + +/** 退出登录 */ +function hp_logout(): void +{ + $_SESSION = []; + if (ini_get('session.use_cookies')) { + $p = session_get_cookie_params(); + setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']); + } + session_destroy(); +} + +/** CSRF token */ +function csrf_token(): string +{ + if (empty($_SESSION['csrf'])) { + $_SESSION['csrf'] = bin2hex(random_bytes(16)); + } + return (string)$_SESSION['csrf']; +} + +function csrf_field(): string +{ + return ''; +} + +function csrf_verify(): bool +{ + return isset($_POST['csrf']) && is_string($_POST['csrf']) && hash_equals(csrf_token(), $_POST['csrf']); +} diff --git a/includes/db.php b/includes/db.php new file mode 100644 index 0000000..86cc4fe --- /dev/null +++ b/includes/db.php @@ -0,0 +1,206 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + $pdo->exec('PRAGMA journal_mode=WAL;'); + db_init($pdo); + } catch (Throwable $e) { + http_response_code(500); + die('数据库初始化失败:' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8')); + } + return $pdo; +} + +/** 若旧表缺少某列则补充(SQLite 兼容迁移) */ +function db_ensure_column(PDO $pdo, string $table, string $column, string $definition): void +{ + $cols = $pdo->query('PRAGMA table_info(' . $table . ')')->fetchAll(); + foreach ($cols as $col) { + if (strtolower((string)$col['name']) === strtolower($column)) { + return; + } + } + $pdo->exec('ALTER TABLE ' . $table . ' ADD COLUMN ' . $column . ' ' . $definition); +} + +function db_init(PDO $pdo): void +{ + $pdo->exec('CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL + )'); + $pdo->exec('CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT + )'); + $pdo->exec('CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + icon TEXT DEFAULT \'\', + description TEXT DEFAULT \'\', + sort INTEGER DEFAULT 0 + )'); + $pdo->exec('CREATE TABLE IF NOT EXISTS nav_blocks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + cat_id INTEGER NOT NULL, + title TEXT NOT NULL, + sort INTEGER DEFAULT 0 + )'); + $pdo->exec('CREATE TABLE IF NOT EXISTS nav_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + block_id INTEGER NOT NULL, + label TEXT NOT NULL, + url TEXT NOT NULL, + note TEXT DEFAULT \'\', + sort INTEGER DEFAULT 0 + )'); + $pdo->exec('CREATE TABLE IF NOT EXISTS func_tools ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + icon TEXT DEFAULT \'\', + note TEXT DEFAULT \'\', + description TEXT DEFAULT \'\', + url TEXT NOT NULL DEFAULT \'\', + is_external INTEGER NOT NULL DEFAULT 0, + enabled INTEGER NOT NULL DEFAULT 1, + sort INTEGER NOT NULL DEFAULT 0 + )'); + $pdo->exec('CREATE TABLE IF NOT EXISTS quick_links ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + url TEXT NOT NULL DEFAULT \'\', + icon TEXT DEFAULT \'\', + note TEXT DEFAULT \'\', + enabled INTEGER NOT NULL DEFAULT 1, + sort INTEGER NOT NULL DEFAULT 0 + )'); + + // 兼容旧库:为已存在的 categories 表补充 icon 列 + db_ensure_column($pdo, 'categories', 'icon', "TEXT DEFAULT ''"); + // 兼容旧库:quick_links / func_tools 若缺列一并补齐 + db_ensure_column($pdo, 'quick_links', 'note', "TEXT DEFAULT ''"); + db_ensure_column($pdo, 'func_tools', 'note', "TEXT DEFAULT ''"); + + // ---------- 默认分类种子 ---------- + $count = (int)$pdo->query('SELECT COUNT(*) FROM categories')->fetchColumn(); + if ($count === 0) { + $seedCats = [ + ['popular', '科普库', '📖', 1, '面向大众与初学者的计算机科学、网络安全与前沿技术科普,帮你建立体系化的安全认知。'], + ['red', '红队库', '⚔️', 2, '面向渗透测试与攻防演练的资源集,汇聚漏洞利用、武器化、情报收集与红队基础设施相关的高质量工具与文档。'], + ['blue', '蓝队库', '🛡️', 3, '面向防御侧的资源集,汇集应急响应、威胁狩猎、流量分析与取证溯源相关的工具与知识。'], + ['tool', '工具库', '🧰', 4, '日常效率工具与安全实用小工具都在这里,点击直达功能区。'], + ]; + $st = $pdo->prepare('INSERT INTO categories (key, name, icon, sort, description) VALUES (?,?,?,?,?)'); + foreach ($seedCats as $c) { + $st->execute($c); + } + } + + // ---------- 默认设置种子 ---------- + $hasSettings = (int)$pdo->query('SELECT COUNT(*) FROM settings')->fetchColumn(); + if ($hasSettings === 0) { + $defaults = [ + 'site_name' => '知识导航站', + 'site_slogan' => '汇聚科普、攻防与效率工具的实用首页', + 'site_logo' => '', // 空 = 使用默认 assets/img/logo.svg + 'icp_no' => '京ICP备00000000号-1', + 'gongan_no' => '京公网安备11000000000000号', + 'gongan_link' => '', // 公安备案号链接(可空则不显示为链接) + 'copyright' => 'Copyright © 2026 知识导航站 版权所有。本站内容仅供学习研究使用。', + 'footer_text' => '', + 'search_engines'=> '', // 搜索引擎列表 JSON;空 = 使用默认 4 个 + 'last_updated' => '', + 'hero_bg' => '', // 首页顶部标识栏背景(纯色 / CSS 渐变),空 = 主题默认 + 'cdn_ranges' => '', // CDN 段库文本;空 = 使用默认内置列表 + ]; + $st = $pdo->prepare('INSERT INTO settings (key, value) VALUES (?,?)'); + foreach ($defaults as $k => $v) { + $st->execute([$k, $v]); + } + } + + // ---------- 默认管理员 ---------- + $userCount = (int)$pdo->query('SELECT COUNT(*) FROM users')->fetchColumn(); + if ($userCount === 0) { + // 密码算法:先 base64 再 md5(按原始需求实现) + $st = $pdo->prepare('INSERT INTO users (username, password_hash) VALUES (?,?)'); + $st->execute(['admin', md5(base64_encode('admin123'))]); + } + + // ---------- 功能区工具默认种子 ---------- + $toolCount = (int)$pdo->query('SELECT COUNT(*) FROM func_tools')->fetchColumn(); + if ($toolCount === 0) { + $seedTools = [ + ['编码 / 加解密', '🔐', 'Base64 / Base32 / URL / Unicode / MD5 / SHA 系列', 'codec.php', 0], + ['随机密码生成', '🔑', '自定义字符集与长度,一次生成多组', 'password.php', 0], + ['二维码生成器', '▦', '任意内容生成二维码,可放大并下载 PNG', 'qrcode.php', 0], + ['杀软识别', '🛡', '粘贴 tasklist 输出,匹配进程对应的安全软件', 'av.php', 0], + ]; + $st = $pdo->prepare('INSERT INTO func_tools (name, icon, description, url, is_external, enabled, sort) VALUES (?,?,?,?,?,1,?)'); + $sort = 0; + foreach ($seedTools as $t) { + $st->execute([$t[0], $t[1], $t[2], $t[3], $t[4], ++$sort]); + } + } + + // ---------- 后期新增功能页默认登记(按 url 去重,允许后台再停用/删除) ---------- + $extraTools = [ + ['国密加解密', '🔏', 'SM2 非对称加解密 / SM3 摘要 / SM4 对称加解密(SM1 为不公开硬件算法)', 'gmcodec.php'], + ['IP 地址处理', '🌐', '提取日志中的全部 IPv4 并统计出现次数,再按内网 / CDN / 公网区分展示', 'ip.php'], + ]; + $chkUrl = $pdo->prepare('SELECT COUNT(*) FROM func_tools WHERE url = ?'); + $insTool = $pdo->prepare('INSERT INTO func_tools (name, icon, description, url, is_external, enabled, sort) VALUES (?,?,?,?,0,1,?)'); + foreach ($extraTools as $t) { + $chkUrl->execute([$t[3]]); + if ((int)$chkUrl->fetchColumn() === 0) { + $mx = (int)$pdo->query('SELECT COALESCE(MAX(sort), 0) FROM func_tools')->fetchColumn(); + $insTool->execute([$t[0], $t[1], $t[2], $t[3], $mx + 1]); + } + } +} + +/** 标记内容已更新(首页展示"上次更新时间") */ +function touch_last_updated(): void +{ + try { + $st = db()->prepare('INSERT INTO settings (key, value) VALUES (\'last_updated\', ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value'); + $st->execute([date('Y-m-d H:i:s')]); + } catch (Throwable $e) { + // 不阻断主流程 + } +} + +/** 读取一条设置 */ +function setting_get(string $key, string $default = ''): string +{ + static $cache = null; + if ($cache === null) { + $cache = []; + foreach (db()->query('SELECT key, value FROM settings') as $row) { + $cache[$row['key']] = (string)$row['value']; + } + } + return array_key_exists($key, $cache) ? $cache[$key] : $default; +} diff --git a/includes/layout.php b/includes/layout.php new file mode 100644 index 0000000..6b596ba --- /dev/null +++ b/includes/layout.php @@ -0,0 +1,208 @@ +:标题/favicon/主题防闪烁/公共样式 */ +function layout_head(string $title = ''): void +{ + global $P; + $site = setting_get('site_name', '知识导航站'); + $logo = site_logo_rel(); + $fullTitle = ($title !== '' ? $title . ' - ' : '') . $site; + $prefix = $P; + echo ''; + echo ''; + echo ''; + // 主题防闪烁:在任何样式加载前应用 + echo ''; + echo '' . he($fullTitle) . ''; + echo ''; + echo ''; + echo '' . "\n"; +} + +/** 昼夜切换浮动按钮 */ +function layout_theme_fab(): void +{ + echo '' . "\n"; +} + +/** 站内 logo 图片(内容区使用,同 favicon) */ +function layout_logo_img(string $cls = 'logo', string $alt = 'logo'): void +{ + global $P; + echo '' . he($alt) . ''; +} + +/** 全站页脚 */ +function layout_footer(): void +{ + global $P; + $site = setting_get('site_name', '知识导航站'); + $icp = setting_get('icp_no', ''); + $gongan = setting_get('gongan_no', ''); + $gonganLink = setting_get('gongan_link', ''); + $copyright = setting_get('copyright', ''); + $extra = setting_get('footer_text', ''); + echo '' . "\n"; +} + +/** 读取首页展示的名言(quota.txt 随机一行) */ +function home_quote(): string +{ + $file = DATA_DIR . '/quota.txt'; + $lines = []; + if (is_file($file)) { + $raw = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (is_array($raw)) { + foreach ($raw as $ln) { + $ln = trim($ln); + if ($ln !== '' && strpos($ln, '#') !== 0) { + $lines[] = $ln; + } + } + } + } + if (!$lines) { + return '知识就是力量。'; + } + return $lines[array_rand($lines)]; +} + +/** 图标值是否属于图片(dataURL / http / 常见图片扩展 / 相对路径) */ +function icon_is_image(string $raw): bool +{ + $v = trim($raw); + if ($v === '') { + return false; + } + if (preg_match('/^data:image\//i', $v) || preg_match('#^https?://#i', $v)) { + return true; + } + if (preg_match('/\.(png|jpe?g|gif|svg|webp)(\?|#|$)/i', $v)) { + return true; + } + return $v[0] === '/' || strpos($v, './') === 0 || strpos($v, '../') === 0; +} + +/** 渲染图标:图片返回 ,否则返回转义文本(供前台/后台预览复用) */ +function site_icon(?string $raw, string $alt = 'icon'): string +{ + $v = trim((string)$raw); + if ($v === '') { + return ''; + } + if (icon_is_image($v)) { + return '' . he($alt) . ''; + } + return he($v); +} + +/** 默认 CDN IPv4 段库(每行:CIDR + 空格 + 厂商名;# 开头为注释,由后台可覆盖) */ +function default_cdn_ranges(): string +{ + return implode("\n", [ + '# 内置默认:Cloudflare 官方 IPv4 段(可在后台“CDN IP 段库”补充其它厂商)', + '103.21.244.0/22 Cloudflare', + '103.22.200.0/22 Cloudflare', + '103.31.4.0/22 Cloudflare', + '104.16.0.0/13 Cloudflare', + '104.24.0.0/14 Cloudflare', + '108.162.192.0/18 Cloudflare', + '131.0.72.0/22 Cloudflare', + '141.101.64.0/18 Cloudflare', + '162.158.0.0/15 Cloudflare', + '172.64.0.0/13 Cloudflare', + '173.245.48.0/20 Cloudflare', + '188.114.96.0/20 Cloudflare', + '190.93.240.0/20 Cloudflare', + '197.234.240.0/22 Cloudflare', + '198.41.128.0/17 Cloudflare', + ]); +} + +/** 当前生效的 CDN 段库文本(后台配置优先,为空回退默认内置列表) */ +function cdn_ranges_text(): string +{ + $v = setting_get('cdn_ranges', ''); + return $v !== '' ? $v : default_cdn_ranges(); +} + +/** 默认搜索引擎列表(后台未配置时的兜底) */ +function default_search_engines(): array +{ + return [ + ['key' => 'bing', 'name' => 'Bing', 'icon' => 'B', 'url' => 'https://www.bing.com/search?q={kw}'], + ['key' => 'baidu', 'name' => '百度', 'icon' => '度', 'url' => 'https://www.baidu.com/s?wd={kw}'], + ['key' => 'github', 'name' => 'GitHub', 'icon' => 'G', 'url' => 'https://github.com/search?q={kw}'], + ['key' => 'google', 'name' => 'Google', 'icon' => 'G', 'url' => 'https://www.google.com/search?q={kw}'], + ]; +} + +/** 读取当前生效的搜索引擎列表(优先后台配置,无效或为空回退默认) */ +function search_engines(): array +{ + $raw = setting_get('search_engines', ''); + if ($raw === '') { + return default_search_engines(); + } + $arr = json_decode($raw, true); + if (!is_array($arr) || count($arr) === 0) { + return default_search_engines(); + } + $out = []; + foreach ($arr as $i => $e) { + if (!is_array($e)) continue; + $name = trim((string)($e['name'] ?? '')); + $url = trim((string)($e['url'] ?? '')); + $icon = trim((string)($e['icon'] ?? '')); + if ($name === '' || $url === '' || strpos($url, '{kw}') === false) continue; + $key = trim((string)($e['key'] ?? '')); + if ($key === '') { + $key = 'e' . ((int)$i + 1); + } + $out[] = ['key' => $key, 'name' => $name, 'icon' => ($icon !== '' ? $icon : 'S'), 'url' => $url]; + } + return $out !== [] ? $out : default_search_engines(); +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..22361bb --- /dev/null +++ b/index.php @@ -0,0 +1,143 @@ +query('SELECT * FROM categories ORDER BY sort ASC, id ASC')->fetchAll(); +// 首页快捷站点(后台配置,新标签页跳转本域名其它网站) +$quickLinks = db()->query('SELECT * FROM quick_links WHERE enabled = 1 ORDER BY sort ASC, id ASC')->fetchAll(); + +$catLink = [ + 'popular' => 'nav.php?cat=popular', + 'red' => 'nav.php?cat=red', + 'blue' => 'nav.php?cat=blue', + 'tool' => 'func/index.php', +]; +$catIcon = [ + 'popular' => '📖', + 'red' => '⚔️', + 'blue' => '🛡️', + 'tool' => '🧰', +]; +$catHint = [ + 'popular' => '入门·科普·前沿', + 'red' => '攻防演练资源', + 'blue' => '防御·应急·溯源', + 'tool' => '进入功能区', +]; + +layout_head(''); +?> + +
style="background:"> +
+
+
+ +
+
+ +
+ +
+
+
+
当前时间 --
+
上次更新 
+
+
+
+ +
+
+
+ + +
+
+ + + +
+
+ + + +
+
+ + +
+
+ + + +
+ + + + + + + + + +
+
+
+ + + + + + + diff --git a/nav.php b/nav.php new file mode 100644 index 0000000..90c307d --- /dev/null +++ b/nav.php @@ -0,0 +1,145 @@ +prepare('SELECT * FROM categories WHERE key = ? LIMIT 1'); +$st->execute([$catKey]); +$cat = $st->fetch(); +if (!$cat) { + http_response_code(404); + layout_head('导航页不存在'); + echo '
导航分类不存在,请返回首页。
'; + layout_theme_fab(); + layout_footer(); + echo ''; + exit; +} + +// 块与按钮数据 +$st = $pdo->prepare('SELECT * FROM nav_blocks WHERE cat_id = ? ORDER BY sort ASC, id ASC'); +$st->execute([(int)$cat['id']]); +$blocks = $st->fetchAll(); + +$itemsByBlock = []; +if ($blocks) { + $ids = array_map(function ($b) { return (int)$b['id']; }, $blocks); + $in = implode(',', array_fill(0, count($ids), '?')); + $st = $pdo->prepare("SELECT * FROM nav_items WHERE block_id IN ($in) ORDER BY sort ASC, id ASC"); + $st->execute($ids); + foreach ($st->fetchAll() as $item) { + $itemsByBlock[(int)$item['block_id']][] = $item; + } +} + +// 三列轮转分配(保证列内块数均衡) +$cols = [[], [], []]; +$idx = 0; +foreach ($blocks as $b) { + $cols[$idx % 3][] = $b; + $idx++; +} +$totalItems = 0; +foreach ($itemsByBlock as $list) { + $totalItems += count($list); +} + +$siteName = setting_get('site_name', '知识导航站'); +$engines = search_engines(); +$defaultEngine = $engines[0]['key'] ?? 'bing'; +$enginesJson = json_encode($engines, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + +layout_head((string)$cat['name']); +?> + + + + +
+
+ +

可切换搜索引擎进行搜索( 个可用,后台可维护)

+
+
+ + + + + + + + + + diff --git a/nginx.htaccess b/nginx.htaccess new file mode 100644 index 0000000..e69de29 diff --git a/plan.txt b/plan.txt new file mode 100644 index 0000000..ac037a0 --- /dev/null +++ b/plan.txt @@ -0,0 +1,50 @@ +我现在需要你去写一个网站首页,要求如下: + +页面分为四种: +1.首页区 +2.导航区 +3.功能区 +4.后台区 + +技术栈: +常见网页技术都行:包括但不限于js、html、php、css。 +layui等样式均可使用 + +基础页面展示要求: +1.首页区要求 + 主内容占据页面60%内容 + 主内容为:占据页面20%的展示栏位于上方,下方40%为导航区块,用户可自行选择四大板块 + 分别为:科普库,蓝队库,红队库,工具库 + 剩余40%空间,上部10%放置名人名言(从quota.txt中获取),(中部为主内容),下部放置ICP备案号,公安备案号,版权声明和其他内容 +2.导航区要求 + 最大限度保证信息密度和美观度,同时页面分为两部分,上半部分占据20%,为搜索栏 + (注意,最上方留白10%) + 下半部分为导航区域 + 导航区域要求: + PC页面两侧各留白15%,中间划分为三大列,导航块内容分别置于各列中(大列无标题) + 导航块为固定一标题加三小列五行的格式,标题加粗居左,行列内容为导航按钮块,鼠标移动上方时变色(若存在备注信息则会在按钮上分蹦出来一个语言栏,没有则仅变色),点击后新建一个页面打开该链接 + 每个导航块大小固定,在移动设备页面中,单小列显示 +3.功能区要求 +设计一个功能区首页(要求与其他页面的风格一致) + 顶部居中大展示栏,最右侧为首页按钮,点击返回首页区,最左侧为返回功能区首页按钮,中间为本功能页标题 + 你需要设计以下功能: + 编码/加解密页:Base64编解码,Base32编解码,md5加密,url编解码,unicode编解码(需支持中文以及其他复杂内容的编解码),各类sha加密 + 注意:页面需同时具备可自由编辑输入的输入和输出区块 + 随机密码生成页:用户自由选择大写、小写字母,数字和特殊符号或者自定义内容并自己选择密码长度进行随机密码生成 + 二维码生成器:用户可放入内容用以生成二维码(可自由放大图片并下载) + 杀软识别:识别输入框内的tasklist内容,随后在avlist.js中匹配内容,以表格形式输出匹配结果 + 格式示例:avList = {"360tray.exe": "360 安全卫士 - 实时保护",....} +4.后台区要求 + 具备对其他页面进行调整的功能 + 1.顺序排布 + 2.内容设计 + 3.导航区导航内容的增删改查 + + 使用sqlite存储登录密码以及其他内容(密码加密格式:先base64再进行md5) + +高级要求: + 展示栏:一个居中小区块,左侧为logo,右侧为当前时间和上次更新时间,在其下还有天气区块() + 搜索栏:最左侧为切换按钮,可自由切换导航引擎为bing,baidu,github等,中间为输入框,最右侧为搜索按钮,存在输入内容时在输入框最右侧显示清除按钮,用户可点击搜索按钮或者直接回车打开新页面搜索内容 + 首页四大板块在用户鼠标移动至上方时用延申文本板块,覆盖其他几个板块并展示说明,该延申文本不具备点击效果以及其他特性,用户鼠标移出该四大板块区域时消失,或者移动至其他板块时以同样的效果展开显示 + 注意,若鼠标所在的板块不在最左侧或最右侧,则在展开说明时移动至旁侧(即最左侧或者最右侧),这时,实际板块位置不变,若用户将鼠标移至其他板块上方,则对该板块进行展开 + 各类输入输出框:支持拉动右下侧边角进行区域扩大(竖向扩大,横向不变),搜索框除外 diff --git a/plan_v2.md b/plan_v2.md new file mode 100644 index 0000000..60ded9a --- /dev/null +++ b/plan_v2.md @@ -0,0 +1,546 @@ +# 网站首页修订版 Plan(v2) + +> 本文件在 `plan.txt` 基础上修订:补全缺失说明、澄清歧义、统一各处术语。 +> 凡原 plan 未说明而本文给出默认值的,均已标注 **【决策】**,可自行调整。 +> 本版以"可直接指导编码实现"为标准编写,页面清单、数据模型、布局规格均已明确。 + +--- + +## 1. 修订要点总览(原 plan 问题 → 本版处理) + +| # | 原 plan 问题 | 本版处理 | +| --- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| 1 | 页面体系与跳转关系未定义 | §2 站点地图,明确首页为最前置页,三大知识库各有独立导航区,工具库跳转功能区 | +| 2 | 首页百分比布局自相矛盾(60% 主内容 / 20% 展示栏 / 40% 导航 / 剩余 40% 里"中部主内容") | §4 重构为自洽的分区模型,保留 20% 展示栏 + 40% 板块区意图 | +| 3 | 技术栈表述笼统 | §3 确定 PHP + SQLite(PDO) + 原生 HTML/CSS/JS,可加 layui | +| 4 | 导航区"留白 10% / 上半部 20%"表述含糊 | §5 给出明确高度计算规则 | +| 5 | "按钮上分蹦出来一个语言栏"(疑似笔误) | §5 定义为悬浮气泡 tooltip 显示备注 | +| 6 | 导航块数据来源、后台归属、超量排布未定义 | §5.3 + §7 数据模型 + §6.4 分栏规则 | +| 7 | 功能区首页布局缺失、顶部组件关系不清 | §6 定义功能区首页网格布局 + 三类顶部组件分工 | +| 8 | 后台"顺序排布 / 内容设计"对象空泛 | §7.3 拆解为可操作的具体配置项 | +| 9 | 登录会话 / 初始管理员 / 越权访问缺失 | §7.1、§7.2 补齐 | +| 10 | quota.txt、avlist.js 格式未约定 | §10 明确格式 | +| 11 | 备案号、版权文本来源未说明 | §7.3.2 后台可配置 + 默认占位 | +| 12 | 移动端适配规则不全 | §9 响应式断点表 | +| 13 | 目录结构缺失 | §8 目录规划 | +| 14 | "上次更新时间"无定义 | §4.5 定义数据来源与刷新逻辑 | +| 15 | 密码"base64+md5"安全局限 | §7.1 注明局限(按需求实现) | +| 16 | 板块 hover 展开交互描述绕口 | §4.6 重写为实现级规格 | + +--- + +## 2. 站点地图与页面跳转关系 + +站点逻辑结构(已确认): + +``` +首页区(index) + ├─ 科普库 → 科普导航页 + ├─ 红队库 → 红队导航页 + ├─ 蓝队库 → 蓝队导航页 + └─ 工具库 → 功能区 + 功能区首页 + ├─ 编码/加解密页 + ├─ 随机密码生成页 + ├─ 二维码生成器 + └─ 杀软识别页 +后台区(/admin,需登录) +``` + +页面清单: + +| 区域 | 页面 | 文件(建议) | 说明 | +| -------- | ------------------------ | --------------------- | ----------------------------------------------------------------------------- | +| 首页区 | 首页 | `index.php` | 最前置页面 | +| 导航区 | 科普库导航页 | `nav.php?cat=popular` | 科普 / 红队 / 蓝队 三个导航页共用同一模板,按分类参数取数,等价于三个独立页面 | +| 导航区 | 红队库导航页 | `nav.php?cat=red` | 同上 | +| 导航区 | 蓝队库导航页 | `nav.php?cat=blue` | 同上 | +| 功能区 | 功能区首页 | `func/index.php` | 工具入口列表 | +| 功能区 | 编码/加解密 | `func/codec.php` | | +| 功能区 | 随机密码生成 | `func/password.php` | | +| 功能区 | 二维码生成器 | `func/qrcode.php` | | +| 功能区 | 杀软识别 | `func/av.php` | | +| 后台区 | 登录页 | `admin/login.php` | | +| 后台区 | 导航内容管理(增删改查) | `admin/nav.php` | 含排序 | +| 后台区 | 功能区管理(工具入口) | `admin/tools.php` | 工具增删改/排序/启停,见 §6.2 | +| 后台区 | 顺序排布 / 内容设计 | `admin/content.php` | 见 §7.3 | +| 数据接口 | JSON 输出 | `api/nav.php` 等 | 供页面 JS 取导航数据 | + +> **【决策】文件全部采用 `.php`**:便于统一 include 顶部栏/页脚、做导航数据渲染、后台鉴权; +> 纯前端静态页若要读 sqlite 也必须走后端接口,统一为 PHP 最省事。 + +--- + +## 3. 技术栈与运行环境 + +- 后端:**PHP ≥ 7.4 + PDO_SQLITE 扩展**,数据库 `data/homepage.db`(SQLite3 文件)。 +- 前端:原生 HTML5 + CSS3 + 原生 JS;允许引入 layui 或轻量库(见 §6 工具页选型)。 +- 编码类算法:优先原生 Web Crypto(SHA);UTF-8 编解码用 `TextEncoder`/`TextDecoder`;如需统一兼容 IE 类场景才引入 `crypto-js`。**【决策:现代浏览器优先,不兼容老 IE】** +- 二维码:引入 `qrcodejs`(单文件 JS 库)。 +- 部署形态:最终放置于服务器(Apache/Nginx + PHP),文档根目录即本工程目录。 + +> 实现约束:编码/加解密、密码生成等均为纯前端 JS 计算,不产生请求;仅导航数据与后台写库走 PHP。 + +--- + +## 4. 首页区规格(index.php) + +### 4.1 整体分区(自洽版布局) + +页面允许纵向滚动(**不用 JS 死锁 100vh**,避免小屏挤压)。各区块高度按"视口高度比例(vh)+ 自然内容高度"混用,规格如下: + +| 区块 | 顺序 | 高度规格 | 内容 | +| ------------- | ---- | ----------------------------------- | ----------------- | +| A. 顶部展示栏 | 1 | 约 20vh(自适应,最小不低于 120px) | 见 §4.2 | +| B. 名言条 | 2 | 内容自适应(约占 10% 视口) | 随机名言,见 §4.3 | +| C. 四大板块区 | 3 | 约 40vh 起,内容超高可自然延伸 | 见 §4.4 / §4.6 | +| D. 页脚 | 4 | 内容自适应 | 见 §4.7 | + +> 原 plan"主内容占 60%(20% 展示栏 + 40% 板块)+ 剩余 40% 内 10% 名言 + 下部备案"存在加总冲突, +> 本版将"20% 展示栏 + 40% 板块 + 10% 名言"作为主要视口内规划(合计约 70%,留有余量),备案等落入页脚区块,不再要求凑满 100vh。 + +### 4.2 顶部展示栏(首页版) + +- 整体居中布局,内部水平分布: + - **左侧**:站点 Logo + 站点名称/副标题。 + - **右侧**:当前时间(实时刷新,格式 `YYYY-MM-DD HH:mm:ss`)+ 上次更新时间(见 §4.5)。 + - **下方居中**:天气区块:``(在移动端可改为宽度 100% 居中,iframe 原样保留尺寸缩放)。 +- 展示栏背景与整体风格统一(§4.8)。 + +### 4.3 名言条(B 区块) + +- 内容源:`data/quota.txt`,每次**刷新页面随机取一条**(也可用 JS 刷新按钮换一条)。 +- 文件格式见 §10.1。 +- 名言为空/文件缺失时显示默认文案,页面不报错。 + +### 4.4 四大板块区(C 区块) + +- 四个板块横排均分,按后台 categories 顺序渲染: + **科普库 → 科普导航页**、**红队库 → 红队导航页**、**蓝队库 → 蓝队导航页**、**工具库 → 功能区首页**。 +- 板块区上方不再显示“四大板块”大标题,改为**快捷站点胶囊条**(数据表 `quick_links`,后台“内容与顺序”维护): + - 用于放置本域名下其它网站,点击 `target="_blank"` 新标签页打开且保留本页; + - 无配置时不显示该条,板块区保持居中填满布局。 +- 每个板块卡片内容:图标 + 名称 + 一句话简介(文案后台可配,见 §7.3.2)。 +- 点击行为:科普/红/蓝打开对应导航页;工具库打开功能区首页。 +- 基础悬停:变色/放大反馈;高级展开说明见 §4.6。 + +### 4.5 "上次更新时间"定义(【决策】) + +含义 = **导航库内容最近一次被后台修改的时间**。 + +- 后台任何写库操作成功后,同步写入 `settings` 表键 `last_updated`(格式时间戳)。 +- 页面通过 PHP 端直接输出该值;无记录时显示"暂无更新记录"。 +- 不跟随"站点上线时间",只代表内容数据新鲜度。 + +### 4.6 板块 hover 展开说明(高级要求,实现级规格) + +目的:鼠标悬停某板块时,弹出**只读说明层**,覆盖在板块区上方,用于解释该库是什么;该层**不可点击、不参与布局**(position: absolute 叠层)。 + +规则: + +1. 悬停任一板块 → 生成该板块说明浮层,覆盖其上方并向外**延展**,延展优先方向: + - 处于最左侧板块:向右延展;处于最右侧板块:向左延展; + - 处于中间板块时:浮层**移位显示到距其最近的边缘空白侧**(第 2 块→吸附左侧边缘展开,第 3 块→吸附右侧边缘展开)。 +2. 所有板块的**实际位置在展开过程中不移动**(视觉上"旁侧浮现"由浮层绝对定位实现,不做网格挤压)。 +3. 鼠标移出四大板块整体区域 → 浮层消失。 +4. 鼠标从一块移到另一块 → 浮层内容与吸附位置即时切换为该板块。 +5. 浮层 z-index 高于板块卡片;浮层 pointer-events 不拦截,防止遮挡后无法移动鼠标。 + +> 说明文案来源:categories.description,可在后台"内容设计"中编辑。**【决策:文案每块 1~3 句】** + +### 4.7 页脚(D 区块) + +内容:ICP 备案号(`备案号`占位,后台可配)、公安备案号(后台可配,含图标)、版权声明(默认 `© 2026 <站点名> 版权所有`,后台可配)、站点名/其他自定义文本(后台可配,见 §7.3.2)。底部同时放一个低调的"后台管理"入口链接(指向 `admin/login.php`)。 + +### 4.8 统一视觉规范(全局) + +| 项 | 约定 | 备注 | +| ------------ | --------------------------------------------- | ---------------------- | +| 主色 | 深空蓝 `#1f3a5f` + 强调蓝 `#2563eb` | 各页统一 | +| 背景 | 浅灰 `#f5f7fa`,卡片白底 | | +| 字体 | 系统字体栈 + 中文回退(`Microsoft YaHei` 等) | 由 CSS 变量统一 | +| 圆角/阴影 | 卡片统一 8px 圆角、轻投影 | | +| 全局公共样式 | 独立 `assets/css/common.css` | 各页引入,保证风格一致 | +| favicon/标题 | 站点名作为 `` 统一后缀 | 后台可改站点名 | + +--- + +## 5. 导航区规格(nav.php,科普/红队/蓝队共用) + +### 5.1 页面纵向结构 + +自顶向下依次为: + +| 区块 | 规则 | 备注 | +| ---------- | ---------------------------------------------------- | ------------------------------------------------ | +| 顶部留白 | 约 **10vh**(不小于 60px) | 纯背景留白,不做内容 | +| 搜索栏 | 位于留白之下,高度 **20vh**(下限 72px、上限 160px) | 即原 plan"上半部 20% + 最上方留白 10%"的明确算法 | +| 导航内容区 | 占据剩余空间,内容超高时**整页纵向滚动** | 见 5.3 | + +### 5.2 搜索栏规格(高级要求) + +- 结构(水平一行,居中,定宽容器内):**引擎切换按钮(左)|输入框(中)|搜索按钮(右)**。 +- 引擎切换:点击循环切换或下拉选择,引擎列表默认:`bing / baidu / github / google`(**【决策】可通过后端接口/常量维护,便于增删**)。 +- 搜索 URL 模板(选中引擎后拼 `关键词` 后 `target="_blank"` 新开页): + - bing: `https://www.bing.com/search?q={kw}` + - baidu: `https://www.baidu.com/s?wd={kw}` + - github: `https://github.com/search?q={kw}` + - google: `https://www.google.com/search?q={kw}` +- 输入框**有内容**时,最右侧出现"清除(×)"按钮;点击清空并聚焦。 +- 触发方式:点击搜索按钮或**回车**。 +- 关键词需 `encodeURIComponent` 处理。 + +### 5.3 导航内容区(主体) + +- 桌面(≥1200px): + - 页面左右两侧各留白 **15%**; + - 中间 70% 区域划分为**三大列**(列宽相等、列间有间距); + - 大列**无标题**,仅是布局容器。 +- 导航块(最小管理单元): + - 每个块 = **1 个块标题(加粗居左)+ 内部 3 小列 × 5 行**(即每块最多 15 个按钮位); + - 块标题、按钮是否填满不限制(3 列 5 行是最大学位,不足则留空)。 + - 块尺寸固定(宽度=所在列宽,高度固定,各按钮行高统一)。 +- 分列规则(【决策】):取当前分类下所有导航块(按 sort 排序),**按序轮转填充三列**(第 1 块→列1,第 2 块→列2,第 3 块→列3,第 4 块→列1……),保证三列块数均衡;列内纵向堆叠,超高则整页滚动。 +- 按钮交互: + - 鼠标移上 → 按钮变色(统一高亮样式); + - 该按钮存在备注信息时 → **浮出悬浮气泡 tooltip** 显示备注,移出消失;无备注仅变色; + - 点击 → `target="_blank"` 新标签页打开链接(加 `rel="noopener noreferrer"`)。 + - 链接规范:允许完整 URL(http/https);后台录入时为空则按钮置灰不可点。 +- 内容来源:按当前 `cat`(popular/red/blue)读取 `nav_blocks`、`nav_items`(§7.4)。 +- 空状态:无任何块时显示占位提示,不报错。 + +### 5.4 移动端 + +见 §9 响应式;核心:中间区域改单列,导航块铺满屏宽,块内 3 小列在 ≥600px 保持 3 列、<600px 折叠为 1 列或 2 列(【决策:<600px 时单小列显示,即一行一个按钮】)。导航块标题列布局保留。 + +--- + +## 6. 功能区规格 + +### 6.1 页面清单与公共头部(功能区统一) + +功能区包含 1 个入口首页 + 4 个工具页。所有功能区页面使用统一的**功能页头部条**(区别于首页展示栏): + +``` +[ ← 返回功能区首页 ] 本功能页标题(居中) [ 首页 ] +``` + +- 最左侧按钮 → 返回 `func/index.php`(功能子页上);在功能区首页本身时该按钮隐藏或置灰。 +- 最右侧按钮 → 返回站点首页 `index.php`。 +- 中间标题 → 当前功能页名称。 +- 功能区首页的头部条中间显示"功能区"。 + +### 6.2 功能区首页(func/index.php) + +- 顶部为风格统一的大展示横幅(复用首页视觉,可含功能区简介)。 +- 主体为工具**卡片网格**:每张卡片 = 图标 + 工具名 + 一句话说明,点击进入对应子页。 +- 工具卡片列表**由 `func_tools` 表驱动,后台“功能区管理”(admin/tools.php)可增删改/排序/启停**: + - 站内工具(如 codec.php):需先在 `func/` 下开发对应页面,再到后台登记入口; + - 外链工具:填写完整 http(s) 地址,作为新窗口卡片直接使用,无需写代码; + - 默认预置:编码/加解密、随机密码生成、二维码生成器、杀软识别四张卡片。 + +### 6.3 编码/加解密页(func/codec.php) + +- **布局**:左侧算法/方向选择区 + 上下(或左右)两个**自由编辑**文本区(输入区、输出区)。 + - 两个文本区都允许用户自由编辑;输出区不设只读。 + - 两个文本区均支持 **右下角拖拽竖向放大(resize: vertical),横向宽度不变**。 + - 提供"交换 / 回填"便捷操作:将输出内容一键放回输入、或将输入/输出一键互换(【决策】)。 +- **支持算法与方向**: + +| 算法 | 编码方向 | 解码方向 | 实现要求 | +| --------------------- | -------------------- | -------------------- | --------------------------------------------------------------------------- | +| Base64 | 编码 | 解码 | 支持中文等 Unicode:先 UTF-8 再 Base64 | +| Base32 | 编码 | 解码 | 同上 | +| MD5 | — | 不可逆 | 输出 32 位十六进制小写 | +| SHA-1/224/256/384/512 | — | 不可逆 | 输出十六进制小写 | +| URL | `encodeURIComponent` | `decodeURIComponent` | 整段处理 | +| Unicode | 中→`\uXXXX` 序列 | `\uXXXX`→文本 | 需支持中文;emoji/增补字符按 UTF-16 代理对编码成两个 `\uXXXX`,解码正确还原 | + +- 交互模型:选算法 + 选方向(编码/解码/加密哈希等按需)→ 点击"执行" → 结果写入输出区;错误(非法输入等)以页面内提示展示,不弹窗。 +- 执行按钮文案随方向变化(如"编码""解码""加密")。 + +### 6.4 随机密码生成页(func/password.php) + +- 字符集选项(多选/开关): + - 大写字母 `A-Z`、小写字母 `a-z`、数字 `0-9`、特殊符号(建议默认 `!@#$%^&*()-_=+[]{};:,.<>?`); + - **自定义字符集输入框**:勾选后启用,用户自定义任意字符(去重);勾选"自定义"时以自定义为准,忽略其余开关。 +- 长度:滑块或数字输入,范围 **4~64**,默认 16。 +- 生成:一次生成 **多组(默认 5 条)**【决策】,逐条显示在可编辑文本区/列表,每条后带"复制"按钮。 +- 结果文本区同样支持竖向拖拽放大。 +- 至少选一个字符集且长度合法才可生成,否则提示。 + +### 6.5 二维码生成器(func/qrcode.php) + +- 输入框 + "生成"按钮;使用 `qrcodejs` 渲染。 +- **放大预览**:提供尺寸滑块(如 128~512px)或点击图片弹出大图预览(大图可在新层中查看);**下载**:提供下载按钮,导出为 PNG(canvas 转 dataURL 或库的 toDataURL)。 +- 边界处理:内容为空禁止生成;内容过长超出二维码容量时提示(库报错或预检测)。 +- 空白页签、无输入时不展示二维码区。 + +### 6.6 杀软识别页(func/av.php) + +- 输入:粘贴 `tasklist` 命令输出文本的 textarea(支持竖向放大,横向不可变)。 +- 匹配库:`data/avlist.js`(§10.2)。 +- 解析规则: + - 按行读取,忽略表头行(含"映像名称/Image Name/PID/===")、空行; + - 每行取**首个 token** 为进程名(空格/制表分隔); + - 与 `avList` 键匹配,**大小写不敏感**;key 若不带扩展名,比较时双方都去掉扩展名后再比一次(【决策】兼容两类写法)。 +- 输出:匹配结果**表格**,列:`进程名 | 匹配结果 | 产品/说明`;未匹配的行结果列显示"未识别",说明列留空。 +- 附加统计:总进程数、匹配数、未识别数。 +- 前端直接 `<script src="../data/avlist.js">` 引入全局 `avList` 对象即可,无需后端。 + +--- + +## 7. 后台区规格(/admin) + +### 7.1 登录与会话 + +- 登录页:用户名 + 密码;错误统一提示"用户名或密码错误"。 +- 密码存储算法:**按需求:`md5(base64(明文密码))`**。 + - ⚠️ 注明局限:该方案是弱单向哈希(无盐),仅用于低安全要求的个人站;不用于高价值系统。若后期可接受,建议换 `password_hash()`(bcrypt)升级,本版实现严格按需求算法。 +- 会话管理:PHP `session`,登录成功后写入 `$_SESSION['admin']`;后台所有页面(除 login 外)在顶部做鉴权: + - 未登录访问后台页 → 302 跳转 `login.php`; + - 退出登录 → `logout.php`(销毁 session 并跳登录页); + - 会话空闲超时(**【决策】30 分钟**)后失效,需重新登录。 +- 初始管理员:首次启动数据库不存在用户时,由 `install` 逻辑自动创建默认账号(**【决策】默认账号 `admin` / 密码 `admin123`,首次登录后引导修改**)。 +- CSRF 防护:后台写操作表单带一次性 token(session 存),提交校验(低成本基础防护)。 +- 登录尝试不做复杂限流,仅简单延时提示(个人站可接受)。 + +### 7.2 后台整体布局 + +- 后台顶部导航条:后台名称 + 快捷入口(导航内容管理 / 顺序排布与内容设计 / 返回前台首页)+ 当前管理员 + 退出。 +- 后台页面自身不引入首页天气/名言等前台组件,保持简洁(风格色板仍与前台统一)。 +- 所有表单保存成功后给反馈(保存成功条/提示),失败显示原因(如 SQL 错误信息可折叠展示,便于排查)。 + +### 7.3 后台功能拆解(原 plan 空泛项的落地) + +原 plan 只写了"1.顺序排布 2.内容设计 3.导航内容增删改查",本版将其拆成可操作模块: + +#### 7.3.1 导航内容管理(admin/nav.php) + +- 顶部先选择操作分类:**科普库 / 红队库 / 蓝队库**(工具库对应功能区,不在导航内容范围内)。 +- 块级操作(针对 nav_blocks):新增块、删除块、编辑块标题、块上移/下移(调整在页面中的先后); +- 按钮位操作(针对 nav_items):在每个块内新增/编辑/删除按钮,字段: + - 文字(必填)、URL(必填)、备注(可空,空则不显示 tooltip)、序号; + - 按钮在块内"3 小列 × 5 行"中的位置可由排序决定(**【决策】按 sort 顺序自动排列,不足位留空,不做拖拽定位到格**); +- 前端交互:用可展开的块列表展示,每个块内按钮表格行式管理,保存按钮批量提交(一次提交当前分类全部变更)。 +- 删除块/按钮需二次确认。 + +#### 7.3.2 内容设计(admin/content.php) + +统一为一个"站点内容与设置"页,按分组编辑并保存到 `settings` 表: + +| 分组 | 可编辑项 | 存储 | 用途 | +| -------- | ------------------------------------------------------------ | ---------------------------------- | ------------------------------- | +| 站点信息 | 站点名、副标题 | settings | 首页展示栏、`<title>` 后缀 | +| 板块文案 | 四大板块各自的图标选择/文案/一句话简介 | settings 或 categories.description | 首页四大板块卡片与 hover 说明层 | +| 底部信息 | ICP 备案号、公安备案号、版权行、自定义底部文本 | settings | 首页/全站页脚 | +| 名言库 | 名言**新增 / 删除 / 清空**(文本框逐行维护,写回 quota.txt) | data/quota.txt | 首页名言条 | +| 其他 | 上次更新时间说明(只读展示) | — | — | + +- 底部自定义文本支持纯文本与单条链接(安全起见只允许 http(s) 开头的 `<a>` 形态,**不做富文本/HTML 注入**)。 + +#### 7.3.3 顺序排布(admin/content.php 或独立区) + +明确排序对象与层级(自外向内): + +1. **分类(四大板块)顺序** → 决定首页四大板块横排顺序; +2. **块顺序**(每个分类内)→ 决定导航页块的前后; +3. **块内按钮顺序** → 决定按钮在块内 3×5 格中的先后。 + +排序交互统一提供"上移/下移 + 序号输入"两种方式(拖拽列为可选项)。 + +### 7.4 数据模型(SQLite DDL) + +```sql +-- 管理员表 +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL -- md5(base64(明文)) +); + +-- 键值设置表(站点名、备案号、版权、板块文案、last_updated 等) +CREATE TABLE settings ( + key TEXT PRIMARY KEY, + value TEXT +); + +-- 四大板块分类(popular/red/blue/tool) +CREATE TABLE categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT UNIQUE NOT NULL, -- popular/red/blue/tool + name TEXT NOT NULL, -- 科普库/红队库/蓝队库/工具库 + description TEXT DEFAULT '', -- 首页卡片一句话简介 + hover 说明文案 + sort INTEGER DEFAULT 0 +); + +-- 导航块(仅 popular/red/blue 三类使用) +CREATE TABLE nav_blocks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + cat_id INTEGER NOT NULL REFERENCES categories(id), + title TEXT NOT NULL, + sort INTEGER DEFAULT 0 +); + +-- 块内按钮位(每块最多 3×5=15,由 sort 顺序占位) +CREATE TABLE nav_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + block_id INTEGER NOT NULL REFERENCES nav_blocks(id), + label TEXT NOT NULL, + url TEXT NOT NULL, + note TEXT DEFAULT '', -- 备注;空则按钮 hover 不弹气泡 + sort INTEGER DEFAULT 0 +); + +-- 功能区工具入口(后台“功能区管理”维护) +CREATE TABLE func_tools ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + icon TEXT DEFAULT '', -- 字符/emoji 图标 + description TEXT DEFAULT '', -- 一句话说明 + url TEXT NOT NULL DEFAULT '', -- 站内相对地址 或 外链 http(s) + is_external INTEGER NOT NULL DEFAULT 0,-- 1=外链(新窗口) + enabled INTEGER NOT NULL DEFAULT 1,-- 1=前台展示 + sort INTEGER NOT NULL DEFAULT 0 +); + +-- 首页快捷站点(本域名其它网站,target=_blank;后台“内容与顺序”维护) +CREATE TABLE quick_links ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + url TEXT NOT NULL DEFAULT '', -- 完整 http(s) 地址 + icon TEXT DEFAULT '', -- 字符/emoji 图标 + enabled INTEGER NOT NULL DEFAULT 1, + sort INTEGER NOT NULL DEFAULT 0 +); +``` + +> 名言不建表(保持 quota.txt 为数据源);`categories` 四行 + `settings` 基础键在首次运行时自动初始化(install 脚本)。 + +--- + +## 8. 目录结构规划 + +``` +HomePage/ # 网站根(部署时即站点根) +├── index.php # 首页区 +├── nav.php # 导航区模板(?cat=popular|red|blue) +├── func/ +│ ├── index.php # 功能区首页 +│ ├── codec.php # 编码/加解密 +│ ├── password.php # 随机密码 +│ ├── qrcode.php # 二维码 +│ └── av.php # 杀软识别 +├── admin/ +│ ├── login.php # 后台登录 +│ ├── logout.php +│ ├── nav.php # 导航内容管理 + 排序 +│ ├── tools.php # 功能区工具入口管理 +│ ├── content.php # 内容设计 / 站点设置 +│ └── _guard.php # 鉴权 include(require 到各后台页顶部) +├── api/ +│ └── nav.php # 输出导航 JSON(前台导航页/首页读取用) +├── includes/ +│ ├── db.php # PDO + SQLite 连接与 install 初始化 +│ ├── auth.php # 登录/校验/session 工具 +│ └── settings.php # settings 读写封装 +├── assets/ +│ ├── css/common.css # 全局样式(统一风格、响应式断点) +│ ├── js/common.js # 通用组件(tooltip、竖向 resize 类等) +│ ├── js/codec.js # 编码算法(base64/base32/url/unicode) +│ ├── js/sha.js # SHA 系列 +│ ├── js/qrcode.min.js # qrcodejs +│ └── img/ # logo/图标 +├── data/ +│ ├── homepage.db # SQLite(运行时自动创建) +│ ├── quota.txt # 名人名言(行式,后台可编辑) +│ └── avlist.js # 杀软进程映射表 +└── plan.txt # 原始 plan(保留存档) +``` + +--- + +## 9. 响应式断点规划 + +| 断点 | 首页 | 导航区 | 功能区 | +| ------------------ | ------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------- | +| ≥1200px(桌面) | 四板块横排;天气 iframe 500×40 原尺寸 | 左右各留白 15%,中间三列 | 工具卡片多列;编码页输入/输出可并排 | +| 768–1199px(平板) | 四板块两行两列或自适应横排 | 左右留白收窄至 5%,仍三列(列宽收紧) | 卡片两列 | +| <768px(手机) | 板块单列或 2×2 网格;名言条自然换行 | **单列**:中间区全宽,导航块一列、块内小列 1 列显示(原要求);搜索栏自适应 | 卡片单列 | + +补充规则: + +- 顶部展示栏/搜索栏/功能页头部条在移动端自动收缩高度与内边距,天气 iframe 宽度等比缩放居中。 +- 编码/二维码等页面在 <768px 时输入输出区改为上下堆叠。 +- 底部备案等信息移动端居中换行。 + +--- + +## 10. 数据文件格式约定 + +### 10.1 data/quota.txt(名人名言) + +- 编码:UTF-8(无 BOM)。 +- 每行一条名言,**空行或 `#` 开头为注释**(后台编辑时自动忽略)。 +- 首次实现时预置 5~10 条示例。 +- 读取:后端 PHP 读取并随机输出一条(首页渲染),避免名言文件被直接下载的路径泄露问题不在此列(公开文件无碍)。 + +示例: + +``` +# 示例名言 +知彼知己,百战不殆。 +知识就是力量。 +``` + +### 10.2 data/avlist.js(杀软进程映射表) + +- 全局对象写法(沿用原示例结构),首次实现预置若干常见条目: + +```js +var avList = { + "360tray.exe": "360 安全卫士 - 实时保护", + "360safe.exe": "360 安全卫士", + "QQPCRTP.exe": "QQ电脑管家", + "kxescore.exe": "金山毒霸", + "ekrn.exe": "ESET NOD32", + "MsMpEng.exe": "Windows Defender", +}; +``` + +- 约定:value 统一为 `产品名 - 组件描述` 风格;key 含扩展名;匹配时忽略大小写。 +- 该文件由实现时创建,**不进后台管理**(如需可扩展)。 + +--- + +## 11. 部署与初始化说明 + +1. 环境要求:PHP ≥ 7.4(启用 PDO_SQLITE)、Apache/Nginx;无需额外安装数据库服务。 +2. 将工程目录整体上传至服务器站点根目录;`data/` 目录需 PHP 可写(用于创建/写入 `homepage.db` 与 `quota.txt`)。 +3. 首次访问任意页面触发自动初始化:建库建表、写入 `categories` 四行、默认 settings、默认管理员 `admin / admin123`、生成 quota.txt/avlist.js 示例(若不存在)。 +4. 首登后台后请修改默认密码(后台导航条提供修改密码入口)。 +5. 天气 iframe 依赖外网(`i.tianqi.com`),离线环境下该区块显示占位不阻塞页面。 + +--- + +## 12. 实施顺序与验收要点(供编码阶段使用) + +| 阶段 | 交付物 | 验收标准 | +| ----------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| 1. 框架 | 目录结构、common.css/common.js、includes/db.php、install 初始化、数据示例 | 首次运行自动建库建表成功;首页可展示默认数据 | +| 2. 首页 | index.php:展示栏/名言条/四板块 hover 展开/页脚 | 四板块正确跳转四大目标;hover 展开规则与 §4.6 一致;时间刷新、名言随机;布局符合 §4 | +| 3. 导航区 | nav.php 三分类 + 搜索栏 + tooltip + 分列 | bing/baidu/github 搜索新开页;回车/清除按钮可用;导航块 hover 变色/tooltip 正确;三列轮转正确;空态正常 | +| 4. 功能区 | func 五个页面 | 中文字符 Base64/Base32/Unicode 编解码正确;SHA/MD5 输出正确;密码可自定义生成;二维码生成/放大/下载;杀软识别匹配与统计正确;textarea 竖向可拉伸 | +| 5. 后台 | 登录/导航管理/内容设计/排序 | 默认账号可登录;未登录访问被拦截;分类/块/按钮 CRUD 与排序即时反映到前台;备案/名言等设置生效;last_updated 更新并展示到首页 | +| 6. 联调打磨 | 响应式与细节 | 三档断点符合 §9;外链新开页;风格一致;无 500/JS 报错 | + +--- + +## 13. 仍建议你确认的开放点(不影响启动开发) + +1. 功能区头部条是否也带天气 iframe —— 本版默认**仅首页展示栏带天气**,功能页不带(避免重复加载)。 +2. 首页名言是否允许点击换一条 —— 默认刷新页面换一条。 +3. 导航页大列块超多时是否允许整页纵向滚动 —— 默认允许。 +4. 二维码是否提供"滚轮缩放" —— 默认提供尺寸滑块 + 点击放大预览两种。 + +> 若以上默认与你设想不同,修改对应【决策】标注即可,其余规格不受影响。