diff --git a/CHANGELOG.md b/CHANGELOG.md index 1100663..1260965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ --- +## 2026-09-14 + +### 优化 + +- 全站清理:移除首页板块悬停浮层的残留样式与脚本、未使用的函数、冗余分支及过时注释(`common.css` / `common.js` / `includes/layout.php` / `admin/login.php` / `includes/auth.php`) +- 删除迁移后遗留的未引用文件:`article/data.php`(文章 JSON 接口,已改为服务端渲染)、`func/_bar.php`(旧 PHP 工具页头部条,工具页已静态化) +- 首页磁贴与板块卡新增依次上浮入场动画;「滚动信息栏」名言点击切换改为淡出淡入;均遵循系统「减少动态效果」设置 +- 优化后台文案:整理「首页顶部组件」模式说明为清单、精简「提示开关」说明 +- 后台体验统一:各内容页(导航 / 内部功能 / 文章 / 外部工具 / 网站 / 公告 / 风控)改用统一的 `.admin-head` 标题区(标题 + 说明 + 操作区 + 分隔线),去除各页零散内联样式 +- 更新说明文档:首页顶部组件模式(现共 10 种)与密码散列方案说明 + ## 2026-09-12 ### 新增 diff --git a/README.md b/README.md index 0a8a056..095bd2b 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ - **多库分区**:科普库 / 红队库 / 蓝队库三大板块 + 工具库 + 文章库 - 每个库内分四区:**导航区 · 内部功能区 · 文章区 · 外部工具区** -- 首页顶部组件三种模式(天气 / 访客信息 / 空白精简)可后台切换 -- 快捷站点磁贴,悬停显示备注 +- 首页顶部组件可在后台切换(天气 / 访客信息 / 空白精简 / 渗透测试 / 安全巡检 / 安全态势 / 访问趋势 / 大字时钟 / 公告滚动 / 打字机标语,共 10 种) +- 快捷站点磁贴:圆形图标 + 名称 + 备注一览(悬停亦可查看完整备注) ### 功能区(纯前端,隐私优先) diff --git a/admin/_visits_lib.php b/admin/_visits_lib.php index e6ecc80..cf25c6a 100644 --- a/admin/_visits_lib.php +++ b/admin/_visits_lib.php @@ -36,9 +36,8 @@ function vis_query_access(string $mode, string $ipF, string $q, int $page, int $ $entries = visitors_entries(); $all = []; if ($mode === 'compact') { - // 精简模式:按 IP 聚合(最近访问时间 / IP / 访问次数),首次出现即该 IP 最新一条 + // 精简模式:按 IP 聚合(最近访问时间 / IP / 访问次数),并默认按访问次数从多到少排序 $grp = []; - $order = []; foreach ($entries as $e) { if ($ipF !== '' && stripos($e['ip'], $ipF) === false) { continue; @@ -48,13 +47,18 @@ function vis_query_access(string $mode, string $ipF, string $q, int $page, int $ } if (!isset($grp[$e['ip']])) { $grp[$e['ip']] = ['ip' => $e['ip'], 'last_at' => $e['created_at'], 'cnt' => 0]; - $order[] = $e['ip']; } $grp[$e['ip']]['cnt']++; } - foreach ($order as $ip) { - $all[] = $grp[$ip]; - } + $all = array_values($grp); + // 访问次数多的在前;次数相同按最近访问时间倒序;再相同按 IP 升序 + usort($all, static function (array $a, array $b): int { + if ($a['cnt'] !== $b['cnt']) { + return $b['cnt'] <=> $a['cnt']; + } + $d = strcmp((string) $b['last_at'], (string) $a['last_at']); + return $d !== 0 ? $d : strcmp((string) $a['ip'], (string) $b['ip']); + }); } else { // 完整模式:逐条明细 foreach ($entries as $e) { @@ -117,7 +121,7 @@ function vis_render_access(array $d): string if ($logs) { ?>
- 共命中 ; + 共命中 ; 当前第 / 页,每页 条。
diff --git a/admin/articles.php b/admin/articles.php index 69b5b0a..eee2625 100644 --- a/admin/articles.php +++ b/admin/articles.php @@ -244,8 +244,12 @@ admin_topbar('articles'); ?>
-

文章管理

-

文章库(第五大板块)的文章维护:正文为标准 Markdown,图片统一上传到 data/articles/img 目录。

+
+
+

文章管理

+

文章库(第五大板块)的文章维护:正文为标准 Markdown,图片统一上传到 data/articles/img 目录。

+
+
diff --git a/admin/content.php b/admin/content.php index 4571a03..1ab5253 100644 --- a/admin/content.php +++ b/admin/content.php @@ -162,12 +162,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { break; case 'hero_mode': $mode = (string) ($_POST['hero_mode'] ?? 'weather'); - if (!in_array($mode, ['weather', 'info', 'blank'], true)) { + if (!in_array($mode, ['weather', 'info', 'blank', 'red', 'blue', 'posture', 'trend', 'clock', 'notice', 'slogan'], true)) { $mode = 'weather'; } settings_set('hero_mode', $mode); touch_last_updated(); - $modeText = ['weather' => '天气模式', 'info' => '访客信息模式', 'blank' => '空白精简模式'][$mode]; + $modeText = ['weather' => '天气模式', 'info' => '信息模式', 'blank' => '精简模式', 'red' => '渗透测试', 'blue' => '安全巡检', 'posture' => '安全态势', 'trend' => '访问趋势', 'clock' => '大字时钟', 'notice' => '公告滚动', 'slogan' => '打字机标语'][$mode]; $msgKind = 'ok'; $msgText = '首页顶部组件模式已保存:' . $modeText . '。'; break; @@ -351,8 +351,12 @@ admin_topbar('content'); ?>
-

网站管理

-

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

+
+
+

网站管理

+

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

+
+
@@ -366,11 +370,9 @@ admin_topbar('content'); - 开启时,前台显示“可切换搜索引擎进行搜索…”等辅助旁白 + 开启时,前台显示搜索栏说明、各分区说明条等辅助旁白
-

前台类似“可切换搜索引擎进行搜索(4 个可用,后台可维护)”、各分区说明条等辅助旁白,以及文案中带后台入口字样的引导句,均带 - st-tip - 标记;关闭后这些文案在全部前台页面统一隐藏,站点导航、搜索、内容等核心功能不受影响。各分区/页面在没有数据时展示的空态占位框(如“该库还没有导航内容”)不受本开关影响,会始终显示以避免空页突兀。

+

上述辅助旁白均带 st-tip 标记,关闭后在全站前台统一隐藏;站点导航、搜索与内容等核心功能不受影响。各分区/页面无数据时的空态占位框(如“该库还没有导航内容”)不受本开关影响,始终显示以避免空页突兀。

@@ -444,15 +446,31 @@ admin_topbar('content');
- - + + + + + + + + +
-

- 天气模式(默认):横幅展示第三方天气条;访客信息模式:横幅显示当前访问者 IP、浏览器、操作系统、内核、屏幕分辨率与系统语言;空白模式:隐藏横幅,仅保留站点信息与时间信息,顶部高度自动缩减。 -

+
diff --git a/admin/exttools.php b/admin/exttools.php index b98e2de..d2aab69 100644 --- a/admin/exttools.php +++ b/admin/exttools.php @@ -133,12 +133,16 @@ admin_topbar('exttools'); ?>
-

外部工具区管理

-

- 链接较多时可先筛选再编辑:按 分区/栏目、启停状态、展示范围、关键词 快速过滤(仅影响当前浏览,不影响保存)。 - 行首“分区 / 栏目”即前台分组标题(如“合集 / 导航”),留空 = 不分组平铺; - 展示范围勾选库,全部不勾选 = 所有库通用。 -

+
+
+

外部工具区管理

+

+ 链接较多时可先筛选再编辑:按 分区/栏目、启停状态、展示范围、关键词 快速过滤(仅影响当前浏览,不影响保存)。 + 行首“分区 / 栏目”即前台分组标题(如“合集 / 导航”),留空 = 不分组平铺; + 展示范围勾选库,全部不勾选 = 所有库通用。 +

+
+
diff --git a/admin/login.php b/admin/login.php index 1b5e4db..43c7e7d 100644 --- a/admin/login.php +++ b/admin/login.php @@ -28,13 +28,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } else { // 轻量防爆破延时(命中黑名单的 IP 在访问本页前即被 access_boot 拦截) usleep(500000); - $risk = risk_login_fail($curIp); - if ($risk[1]) { - $err = '用户名或密码错误。'; - // $err = '登录失败次数过多(本次累计 ' . $risk[0] . ' 次,达到阈值 ' . $risk[2] . ' 次),当前 IP 已被风控自动封禁,全站(含后台登录)访问均被拒绝。请稍后从未被封禁的网络进入后台「风控管理 → IP 黑名单」移除该规则解除,或联系站长处理。'; - } else { - $err = '用户名或密码错误。'; - } + // 累计登录失败次数(达阈值会自动封禁);此处统一返回同一提示,避免暴露账号状态 + risk_login_fail($curIp); + $err = '用户名或密码错误。'; } } diff --git a/admin/nav.php b/admin/nav.php index ace0120..f302a74 100644 --- a/admin/nav.php +++ b/admin/nav.php @@ -156,11 +156,15 @@ admin_topbar('nav'); ?>
-

导航管理

-

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

+
+
+

导航管理

+

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

+
+
-
+
diff --git a/admin/notice.php b/admin/notice.php index b443094..a66a252 100644 --- a/admin/notice.php +++ b/admin/notice.php @@ -106,12 +106,16 @@ admin_topbar('notice'); ?>
-

更新公告管理

-

- 前台页脚「更新公告」页展示内容:可发布多条公告并保留历史,任意一条均可再次编辑。 - 内容支持超链接,写法 [文字](链接);站内用相对路径(如 /index.php/func/index.php), - 外链用 https://…,也可直接粘贴网址自动识别。换行会被保留。 -

+
+
+

更新公告管理

+

+ 前台页脚「更新公告」页展示内容:可发布多条公告并保留历史,任意一条均可再次编辑。 + 内容支持超链接,写法 [文字](链接);站内用相对路径(如 /index.php/func/index.php), + 外链用 https://…,也可直接粘贴网址自动识别。换行会被保留。 +

+
+
diff --git a/admin/tools.php b/admin/tools.php index a379fda..2438cf6 100644 --- a/admin/tools.php +++ b/admin/tools.php @@ -144,11 +144,15 @@ admin_topbar('tools'); ?>
-

内部功能

-

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

+
+
+

内部功能

+

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

+
+
diff --git a/admin/visits.php b/admin/visits.php index 4588ad5..58e91da 100644 --- a/admin/visits.php +++ b/admin/visits.php @@ -217,8 +217,13 @@ admin_topbar('visits'); ?>
-

风控管理

-

记录访客对本站的动态请求(含后台),完整模式逐条展示访问明细,精简模式按 IP 汇总次数;黑名单 / 自动风控对所有访问统一生效,命中即返回 403 风控提示页,后台与登录页不例外。

+
+
+

风控管理

+

记录访客对本站的动态请求(含后台),完整模式逐条展示访问明细,精简模式按 IP 汇总次数;黑名单 / 自动风控对所有访问统一生效,命中即返回 403 风控提示页,后台与登录页不例外。 +

+
+
@@ -315,7 +320,7 @@ admin_topbar('visits'); diff --git a/article/data.php b/article/data.php deleted file mode 100644 index 92c52dd..0000000 --- a/article/data.php +++ /dev/null @@ -1,70 +0,0 @@ - false, 'msg' => '缺少文章 id']); - } - $st = $pdo->prepare('SELECT id, part, title, summary, markdown, pinned, created_at, updated_at - FROM articles WHERE id = ? AND enabled = 1 LIMIT 1'); - $st->execute([$id]); - $row = $st->fetch(); - if (!$row) { - json_out(['ok' => false, 'msg' => '文章不存在或已隐藏']); - } - json_out(['ok' => true, 'article' => $row]); -} - -// ---- 默认:list 分组元数据(分区顺序取各分区首篇 id 序;分区内置顶 → sort → id) ---- -$rows = $pdo->query('SELECT id, part, title, summary, pinned, sort, created_at, updated_at - FROM articles WHERE enabled = 1 ORDER BY id ASC')->fetchAll(); -$parts = []; -if ($rows) { - $partOrder = []; - $byPart = []; - foreach ($rows as $r) { - $part = trim((string)$r['part']); - if ($part === '') { - $part = '未分类'; - } - if (!isset($byPart[$part])) { - $byPart[$part] = []; - $partOrder[] = $part; - } - $byPart[$part][] = $r; - } - foreach ($partOrder as $part) { - $items = $byPart[$part]; - usort($items, static function (array $a, array $b): int { - if ((int)$a['pinned'] !== (int)$b['pinned']) { - return (int)$b['pinned'] - (int)$a['pinned']; - } - if ((int)$a['sort'] !== (int)$b['sort']) { - return (int)$a['sort'] <=> (int)$b['sort']; - } - return (int)$a['id'] <=> (int)$b['id']; - }); - $parts[] = ['part' => $part, 'articles' => $items]; - } -} -json_out(['ok' => true, 'parts' => $parts]); diff --git a/assets/css/common.css b/assets/css/common.css index ddbd9e3..c6e0bf8 100644 --- a/assets/css/common.css +++ b/assets/css/common.css @@ -184,6 +184,89 @@ select { width: auto; min-width: 150px; } /* ---------- 首页顶部组件:空白精简模式(hero_mode=blank) ---------- */ .hero-slim { min-height: 0; padding: 16px 0 12px; } +/* ---------- 首页顶部组件:红队/蓝队渗透测试流程模式(hero_mode=red|blue,动态步进) ---------- */ +.ptflow { display: flex; flex-direction: column; gap: 10px; } +.ptflow-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; } +.ptflow-title { font-size: 15px; font-weight: 700; color: #fff; } +.ptflow-title .ptflow-tag { display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; margin-left: 8px; vertical-align: middle; letter-spacing: .5px; } +.ptflow-red .ptflow-title .ptflow-tag { background: rgba(239, 68, 68, .22); color: #fecaca; border: 1px solid rgba(239, 68, 68, .5); } +.ptflow-blue .ptflow-title .ptflow-tag { background: rgba(37, 99, 235, .22); color: #bfdbfe; border: 1px solid rgba(59, 130, 246, .5); } +.ptflow-phase { margin-left: auto; font-size: 12.5px; color: rgba(255, 255, 255, .85); font-variant-numeric: tabular-nums; } +.ptflow-track { display: flex; gap: 8px; margin: 0; padding: 0; list-style: none; position: relative; overflow: hidden; } +.ptflow-track::after { content: ''; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, .08) 50%, transparent 100%); animation: ptflowSweep 3.2s linear infinite; } +@keyframes ptflowSweep { from { transform: translateX(-100%); } to { transform: translateX(100%); } } +.ptflow-step { flex: 1 1 0; min-width: 0; position: relative; padding: 9px 10px; border-radius: 10px; background: rgba(10, 22, 44, .32); border: 1px solid rgba(255, 255, 255, .18); transition: transform .35s ease, border-color .35s ease, background .35s ease, box-shadow .35s ease; } +.ptflow-step .ptflow-ic { font-size: 16px; display: block; line-height: 1; } +.ptflow-step b { display: block; color: #fff; font-size: 12.5px; margin-top: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.ptflow-step small { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; color: rgba(255, 255, 255, .68); font-size: 11px; line-height: 1.35; margin-top: 2px; } +.ptflow-step.active { transform: translateY(-3px); background: rgba(10, 22, 44, .5); } +.ptflow-red .ptflow-step.active { border-color: #ef4444; box-shadow: 0 8px 22px rgba(239, 68, 68, .35); } +.ptflow-blue .ptflow-step.active { border-color: #3b82f6; box-shadow: 0 8px 22px rgba(59, 130, 246, .35); } +.ptflow-step.done { opacity: .55; } +.ptflow-step.done::after { content: '✓'; position: absolute; top: 6px; right: 8px; font-size: 10px; color: #34d399; } +@media (max-width: 767px) { + .ptflow-track { flex-wrap: wrap; } + .ptflow-step { flex: 1 1 30%; } + .ptflow-phase { margin-left: 0; } +} + +/* ---------- 首页顶部组件:安全态势简报(hero_mode=posture,数字增长) ---------- */ +.js-posture .vb-cell b.js-count { color: #fff; font-variant-numeric: tabular-nums; } + +/* ---------- 首页顶部组件:访问趋势(hero_mode=trend) ---------- */ +.trend-box { display: flex; flex-direction: column; gap: 10px; } +.trend-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; } +.trend-title { font-size: 15px; font-weight: 700; color: #fff; } +.trend-sub { margin-left: auto; font-size: 12.5px; color: rgba(255, 255, 255, .82); } +.trend-sub b { color: #fff; font-variant-numeric: tabular-nums; } +.trend-chart { display: flex; align-items: flex-end; gap: 8px; height: 132px; } +.trend-col { flex: 1 1 0; min-width: 0; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; } +.trend-val { font-size: 12px; color: rgba(255, 255, 255, .85); font-variant-numeric: tabular-nums; margin-bottom: 4px; } +.trend-bar-wrap { flex: 1; width: 100%; display: flex; align-items: flex-end; justify-content: center; } +.trend-bar { display: block; width: 62%; max-width: 34px; height: 0; border-radius: 6px 6px 3px 3px; background: linear-gradient(180deg, #7dd3fc, #38bdf8); box-shadow: 0 0 12px rgba(56, 189, 248, .45); animation: trendGrow .9s cubic-bezier(.2, .8, .2, 1) forwards; } +@keyframes trendGrow { from { height: 0; } to { height: var(--h); } } +.trend-lbl { font-size: 11px; color: rgba(255, 255, 255, .8); margin-top: 5px; font-variant-numeric: tabular-nums; } +.trend-wk { font-size: 10.5px; color: rgba(255, 255, 255, .55); } + +/* ---------- 首页顶部组件:大字时钟(hero_mode=clock) ---------- */ +.clock-box { display: flex; flex-direction: column; align-items: center; gap: 2px; } +.ch-greet { font-size: 13px; letter-spacing: 2px; color: rgba(255, 255, 255, .8); } +.ch-time { font-size: clamp(34px, 8vw, 58px); font-weight: 700; line-height: 1.05; color: #fff; font-variant-numeric: tabular-nums; letter-spacing: 2px; text-shadow: 0 2px 18px rgba(0, 0, 0, .35); } +.ch-date { font-size: 13.5px; color: rgba(255, 255, 255, .85); display: flex; gap: 10px; } +.ch-date .js-ch-week { color: rgba(255, 255, 255, .7); } +.ch-extra { display: flex; align-items: center; justify-content: center; gap: 16px; margin-top: 3px; flex-wrap: wrap; } +.ch-lunar { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; color: rgba(255, 255, 255, .85); flex-wrap: wrap; } +.ch-lunar .js-ch-lunar { letter-spacing: .5px; } +.ch-fest { display: none; font-size: 12px; padding: 1px 8px; border-radius: 999px; background: rgba(239, 68, 68, .22); border: 1px solid rgba(239, 68, 68, .5); color: #fecaca; white-space: nowrap; } +.ch-fest.on { display: inline-block; } +.ch-count { display: inline-flex; align-items: baseline; gap: 6px; font-size: 13px; color: rgba(255, 255, 255, .85); flex-wrap: wrap; } +.ch-count .cc-label { color: rgba(255, 255, 255, .72); } +.ch-count .cc-time { font-weight: 700; color: #7dd3fc; font-variant-numeric: tabular-nums; letter-spacing: 1px; } + +/* ---------- 首页顶部组件:公告滚动条(hero_mode=notice) ---------- */ +.ticker-box { display: flex; align-items: center; gap: 10px; background: rgba(10, 22, 44, .32); border: 1px solid rgba(255, 255, 255, .18); border-radius: 10px; padding: 9px 12px; overflow: hidden; } +.tk-label { flex: none; font-size: 12px; font-weight: 700; color: #fff; background: var(--brand); border-radius: 6px; padding: 2px 8px; letter-spacing: 1px; } +.tk-viewport { position: relative; flex: 1; overflow: hidden; white-space: nowrap; } +.tk-track { display: inline-flex; align-items: center; will-change: transform; } +.tk-item { display: inline-flex; align-items: center; gap: 6px; color: #fff; text-decoration: none; font-size: 13.5px; padding: 0 26px; position: relative; } +.tk-item:hover { color: #bae6fd; } +.tk-item::after { content: '·'; position: absolute; right: 0; color: rgba(255, 255, 255, .4); } +.tk-pin { font-size: 10.5px; padding: 1px 6px; border-radius: 999px; background: rgba(239, 68, 68, .25); border: 1px solid rgba(239, 68, 68, .5); color: #fecaca; } +.tk-empty { font-size: 13.5px; color: rgba(255, 255, 255, .75); } +@keyframes tkScroll { from { transform: translateX(0); } to { transform: translateX(-50%); } } + +/* ---------- 首页顶部组件:打字机标语(hero_mode=slogan) ---------- */ +.slogan-box { display: flex; align-items: center; gap: 10px; min-height: 40px; font-size: clamp(16px, 2.6vw, 22px); color: #fff; } +.sg-mark { color: #7dd3fc; font-weight: 700; } +.sg-text { font-weight: 600; letter-spacing: .5px; } +.sg-caret { display: inline-block; width: 9px; height: 1.05em; background: #7dd3fc; margin-left: 2px; transform: translateY(2px); animation: sgBlink 1s steps(1) infinite; } +@keyframes sgBlink { 50% { opacity: 0; } } +@media (max-width: 767px) { + .trend-chart { height: 108px; gap: 5px; } + .trend-wk { display: none; } + .slogan-box { font-size: 16px; } +} + /* ============ 后台风控管理页 ============ */ .vis-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; } .vis-stat { background: var(--bg-2); border: 1px solid var(--line); border-radius: 12px; padding: 12px 16px; } @@ -221,9 +304,12 @@ select { width: auto; min-width: 150px; } .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; + transition: color .15s ease, background .15s ease, opacity .14s ease, transform .14s ease; } .quote-text:hover { color: var(--brand); background: var(--hover-bg); } +/* 点击切换名言时先淡出再淡入,避免文字瞬变 */ +.quote-text.quote-fade { opacity: 0; transform: translateY(-3px); } +@media (prefers-reduced-motion: reduce) { .quote-text { transition: color .15s ease, background .15s ease; } } /* ============ 首页四大板块 ============ */ /* 首页板块区:内容放得下时填满视口剩余高度(不出现竖向滚动条),内容超高时自然滚动 */ @@ -253,8 +339,6 @@ select { width: auto; min-width: 150px; } /* 通用图标 (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(auto-fit, minmax(190px, 1fr)); gap: 18px; } .board-card { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; @@ -275,21 +359,22 @@ select { width: auto; min-width: 150px; } font-size: 13px; color: var(--text-3); line-height: 1.5; max-width: 100%; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } -.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; +/* 首页磁贴 / 板块卡入场动画:依次上浮浮现(fill-mode 用 backwards,动画结束后归还 :hover 位移) */ +@keyframes hpRise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } } +.quick-strip .quick-tile, .boards .board-card { animation: hpRise .46s cubic-bezier(.2, .8, .25, 1) backwards; } +.quick-strip > :nth-child(1), .boards > :nth-child(1) { animation-delay: .02s; } +.quick-strip > :nth-child(2), .boards > :nth-child(2) { animation-delay: .07s; } +.quick-strip > :nth-child(3), .boards > :nth-child(3) { animation-delay: .12s; } +.quick-strip > :nth-child(4), .boards > :nth-child(4) { animation-delay: .17s; } +.quick-strip > :nth-child(5), .boards > :nth-child(5) { animation-delay: .22s; } +.quick-strip > :nth-child(6), .boards > :nth-child(6) { animation-delay: .27s; } +.quick-strip > :nth-child(7), .boards > :nth-child(7) { animation-delay: .32s; } +.quick-strip > :nth-child(8), .boards > :nth-child(8) { animation-delay: .37s; } +.quick-strip > :nth-child(n+9), .boards > :nth-child(n+9) { animation-delay: .42s; } +@media (prefers-reduced-motion: reduce) { + .quick-strip .quick-tile, .boards .board-card { animation: none; } } -.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); } +.func-card .fc-icon img.icon-img { width: 24px; height: 24px; } /* 名言隐藏池 */ .quote-pool { display: none; } @@ -474,6 +559,20 @@ a.nav-btn[data-note]:hover::after { opacity: 1; transform: translateX(-50%) tran .admin-nav .brand .site-name { color: #fff; } .admin-nav a.active { text-decoration: underline; text-underline-offset: 4px; font-weight: 600; } +/* 后台页面标题区(统一各页标题 / 说明 / 操作区) */ +.admin-head { + display: flex; align-items: flex-end; justify-content: space-between; gap: 14px; flex-wrap: wrap; + margin-bottom: 18px; padding-bottom: 12px; border-bottom: 1px solid var(--line); +} +.admin-head .ah-main { min-width: 0; } +.admin-head .ah-title { font-size: 20px; font-weight: 700; margin: 0; } +.admin-head .ah-sub { margin: 6px 0 0; font-size: 13px; color: var(--text-3); line-height: 1.7; } +.admin-head .ah-acts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } +@media (max-width: 600px) { + .admin-head { align-items: flex-start; } + .admin-head .ah-title { font-size: 18px; } +} + .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; } @@ -638,7 +737,6 @@ a.nav-btn[data-note]:hover::after { opacity: 1; transform: translateX(-50%) tran .v-filter input[type=text] { 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; } diff --git a/assets/img/logo.jpg b/assets/img/logo.jpg new file mode 100644 index 0000000..1abc0c3 Binary files /dev/null and b/assets/img/logo.jpg differ diff --git a/assets/js/common.js b/assets/js/common.js index 4f915d0..852bcb4 100644 --- a/assets/js/common.js +++ b/assets/js/common.js @@ -1,5 +1,5 @@ /* common.js —— 全站通用交互 - * 功能:昼/夜主题切换、实时时钟、随机名言刷新、四大板块 hover 浮层、 + * 功能:昼/夜主题切换、实时时钟、随机名言刷新、返回顶部、 * 搜索栏(引擎下拉/清除/回车搜索)、复制到剪贴板、DOM 工具函数 */ (function (global) { @@ -64,7 +64,9 @@ /* ---------- 名言条:点击格言本身切换 ---------- */ function initQuoteRefresh() { $$('.js-quote').forEach(function (quote) { + var swapping = false; quote.addEventListener('click', function () { + if (swapping) return; var list = $$('.js-quote-pool span', document); // 服务端把所有名言放入隐藏池 if (!list.length) return; // 定位当前展示文本在池中的位置,避免换到下一条仍是同一条 @@ -78,42 +80,18 @@ if (n > 1 && cur !== -1) { while (idx === cur) idx = (idx + 1) % n; } - quote.textContent = list[idx].textContent; + // 先淡出、换字后再淡入,避免文字瞬间跳变 + swapping = true; + quote.classList.add('quote-fade'); + setTimeout(function () { + quote.textContent = list[idx].textContent; + quote.classList.remove('quote-fade'); + swapping = false; + }, 140); }); }); } - /* ---------- 首页四大板块 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}' }, @@ -756,18 +734,288 @@ } catch (e) { /* 同源限制等:保持默认图标 */ } } + /* ---------- 首页顶部组件:红队 / 蓝队渗透测试流程(hero_mode=red|blue)动态步进 ---------- */ + function initHeroFlows() { + var boxes = $$('.js-ptflow'); + if (!boxes.length) return; + boxes.forEach(function (box) { + var steps = $$('.ptflow-step', box); + if (!steps.length) return; + var phaseEl = $('.js-ptflow-phase', box); + var idx = 0; + function paint() { + steps.forEach(function (s, i) { + s.classList.toggle('active', i === idx); + s.classList.toggle('done', i < idx); + }); + if (phaseEl) { + var cur = steps[idx]; + var name = cur ? (cur.getAttribute('data-phase') || '') : ''; + phaseEl.textContent = '当前阶段:' + name + '(' + (idx + 1) + '/' + steps.length + ')'; + } + idx = (idx + 1) % steps.length; + } + paint(); + setInterval(paint, 1800); + }); + } + + /* ---------- 首页顶部组件:安全态势数字增长(hero_mode=posture) ---------- */ + function initHeroCountUp() { + var els = $$('.js-count'); + if (!els.length) return; + els.forEach(function (el) { + var to = parseInt(el.getAttribute('data-to') || '0', 10); + if (isNaN(to) || to <= 0) { el.textContent = '0'; return; } + var dur = 900, t0 = null; + function step(now) { + if (t0 === null) t0 = now; + var p = Math.min(1, (now - t0) / dur); + var eased = 1 - Math.pow(1 - p, 3); + if (p < 1) { el.textContent = String(Math.round(to * eased)); requestAnimationFrame(step); } + else { el.textContent = String(to); } + } + requestAnimationFrame(step); + }); + } + + /* ---------- 农历 / 节日库(1900-2100,纯前端本地计算,不依赖任何第三方接口) ---------- */ + var LUNAR_INFO = [ + 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, + 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, + 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, + 0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, + 0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, + 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, + 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, + 0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, + 0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, + 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0, + 0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, + 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, + 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, + 0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, + 0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, + 0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0, + 0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4, + 0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, + 0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, + 0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, + 0x0d520 + ]; + var LUNAR_GAN = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸']; + var LUNAR_ZHI = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥']; + var LUNAR_MON = ['正', '二', '三', '四', '五', '六', '七', '八', '九', '十', '冬', '腊']; + var LUNAR_D1 = ['日', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十']; + var LUNAR_D2 = ['初', '十', '廿', '三']; + var LUNAR_FEST = [ + [1, 1, '春节'], [1, 15, '元宵节'], [2, 2, '龙抬头'], [5, 5, '端午节'], [7, 7, '七夕'], + [7, 15, '中元节'], [8, 15, '中秋节'], [9, 9, '重阳节'], [12, 8, '腊八节'], [12, 23, '小年'] + ]; + var SOLAR_FEST = { + '1-1': '元旦', '2-14': '情人节', '3-8': '妇女节', '3-12': '植树节', '4-1': '愚人节', + '5-1': '劳动节', '5-4': '青年节', '6-1': '儿童节', '7-1': '建党节', '8-1': '建军节', + '9-10': '教师节', '10-1': '国庆节', '12-25': '圣诞节' + }; + function lunLeapMonth(y) { return LUNAR_INFO[y - 1900] & 0xf; } + function lunLeapDays(y) { return lunLeapMonth(y) ? ((LUNAR_INFO[y - 1900] & 0x10000) ? 30 : 29) : 0; } + function lunMonthDays(y, m) { return (LUNAR_INFO[y - 1900] & (0x10000 >> m)) ? 30 : 29; } + function lunYearDays(y) { + var i, sum = 348; + for (i = 0x8000; i > 0x8; i >>= 1) { sum += (LUNAR_INFO[y - 1900] & i) ? 1 : 0; } + return sum + lunLeapDays(y); + } + function solar2lunar(date) { + var offset = (Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) - Date.UTC(1900, 0, 31)) / 86400000; + var i, temp = 0; + for (i = 1900; i < 2101 && offset > 0; i++) { temp = lunYearDays(i); offset -= temp; } + if (offset < 0) { offset += temp; i--; } + var year = i, leap = lunLeapMonth(i), isLeap = false; + for (i = 1; i < 13 && offset > 0; i++) { + if (leap > 0 && i === (leap + 1) && isLeap === false) { --i; isLeap = true; temp = lunLeapDays(year); } + else { temp = lunMonthDays(year, i); } + if (isLeap === true && i === (leap + 1)) { isLeap = false; } + offset -= temp; + } + if (offset === 0 && leap > 0 && i === leap + 1) { + if (isLeap) { isLeap = false; } else { isLeap = true; --i; } + } + if (offset < 0) { offset += temp; --i; } + return { year: year, month: i, day: offset + 1, isLeap: isLeap }; + } + function lunDayName(d) { + if (d === 10) { return '初十'; } + if (d === 20) { return '二十'; } + if (d === 30) { return '三十'; } + return LUNAR_D2[Math.floor(d / 10)] + LUNAR_D1[d % 10]; + } + function ganzhiYear(y) { return LUNAR_GAN[(y - 4) % 10] + LUNAR_ZHI[(y - 4) % 12]; } + function lunMonthName(m, isLeap) { return (isLeap ? '闰' : '') + (LUNAR_MON[m - 1] || m) + '月'; } + function festivalsOf(date) { + var out = []; + var lu = solar2lunar(date); + for (var i = 0; i < LUNAR_FEST.length; i++) { + if (!lu.isLeap && lu.month === LUNAR_FEST[i][0] && lu.day === LUNAR_FEST[i][1]) { out.push(LUNAR_FEST[i][2]); } + } + if (!lu.isLeap && lu.month === 12 && lu.day === lunMonthDays(lu.year, 12)) { out.push('除夕'); } + var key = (date.getMonth() + 1) + '-' + date.getDate(); + if (SOLAR_FEST[key]) { out.push(SOLAR_FEST[key]); } + var w = date.getDay(), mo = date.getMonth() + 1, dd = date.getDate(); + if (mo === 5 && w === 0 && dd >= 8 && dd <= 14) { out.push('母亲节'); } + if (mo === 6 && w === 0 && dd >= 15 && dd <= 21) { out.push('父亲节'); } + return out; + } + function lunarText(date) { + var lu = solar2lunar(date); + return '农历 ' + ganzhiYear(lu.year) + '年 ' + lunMonthName(lu.month, lu.isLeap) + lunDayName(lu.day); + } + + /* ---------- 首页顶部组件:大字时钟(hero_mode=clock,含农历/节日) ---------- */ + function initHeroClock() { + var boxes = $$('.js-hero-clock'); + if (!boxes.length) return; + var WK = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; + function pad(n) { return n < 10 ? '0' + n : '' + n; } + function greet(h) { + if (h < 6) return '凌晨好'; + if (h < 9) return '早上好'; + if (h < 12) return '上午好'; + if (h < 14) return '中午好'; + if (h < 18) return '下午好'; + if (h < 23) return '晚上好'; + return '夜深了'; + } + var cacheDay = '', lunarStr = '', festStr = ''; + var nextFestName = '', nextFestTs = 0; + /* 下一个节日:从明天起向后找(含当天已过的),最多一年 */ + function findNextFest(now) { + var y = now.getFullYear(), m = now.getMonth(), d = now.getDate(); + for (var i = 1; i <= 370; i++) { + var dt = new Date(y, m, d + i); + var f = festivalsOf(dt); + if (f.length) { + return { name: f[0], ts: new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(), 0, 0, 0).getTime() }; + } + } + return null; + } + function pad2(n) { return n < 10 ? '0' + n : '' + n; } + function fmtCountdown(ms) { + if (ms < 0) ms = 0; + var total = Math.floor(ms / 1000); + var days = Math.floor(total / 86400); + var hh = Math.floor((total % 86400) / 3600); + var mm = Math.floor((total % 3600) / 60); + var ss = total % 60; + var t = pad2(hh) + ':' + pad2(mm) + ':' + pad2(ss); + return days > 0 ? (days + '天 ' + t) : t; + } + /* 每日只需重算一次(农历 / 今日节日 / 下一个节日) */ + function refreshDaily(now) { + var key = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate(); + if (key === cacheDay) return; + cacheDay = key; + lunarStr = lunarText(now); + var todayFest = festivalsOf(now); + festStr = todayFest.length ? todayFest.join(' · ') : ''; + var nf = findNextFest(now); + nextFestName = nf ? nf.name : ''; + nextFestTs = nf ? nf.ts : 0; + } + function tick() { + var d = new Date(); + refreshDaily(d); + var timeStr = pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); + var dateStr = d.getFullYear() + '年' + pad(d.getMonth() + 1) + '月' + pad(d.getDate()) + '日'; + var wk = WK[d.getDay()]; + var gr = greet(d.getHours()); + var left = nextFestTs ? fmtCountdown(nextFestTs - d.getTime()) : '--'; + boxes.forEach(function (box) { + var te = $('.js-ch-time', box), de = $('.js-ch-date', box), + we = $('.js-ch-week', box), ge = $('.js-ch-greet', box), + le = $('.js-ch-lunar', box), fe = $('.js-ch-fest', box), + cl = $('.js-cc-label', box), ct = $('.js-cc-time', box); + if (te) te.textContent = timeStr; + if (de) de.textContent = dateStr; + if (we) we.textContent = wk; + if (ge) ge.textContent = gr; + if (le) le.textContent = lunarStr; + if (fe) { + fe.textContent = festStr; + fe.className = 'ch-fest js-ch-fest' + (festStr ? ' on' : ''); + } + if (cl) cl.textContent = nextFestName ? ('距 ' + nextFestName) : '下一个节日'; + if (ct) ct.textContent = left; + }); + } + tick(); + setInterval(tick, 1000); + } + + /* ---------- 首页顶部组件:公告滚动条(hero_mode=notice) ---------- */ + function initNoticeTicker() { + var track = $('.js-tk-track'); + if (!track) return; + var viewport = $('.js-tk-viewport'); + if (!$$('.tk-item', track).length) return; + var oneSet = track.innerHTML; + var targetW = viewport ? viewport.clientWidth : 0; + var guard = 0; + while (track.scrollWidth < targetW && guard < 20) { + track.innerHTML += oneSet; + guard++; + } + track.innerHTML += track.innerHTML; /* 再补一组,配合 -50% 位移无缝循环 */ + var half = track.scrollWidth / 2; + var dur = Math.max(12, half / 60); /* 约 60px/s */ + track.style.animation = 'tkScroll ' + dur.toFixed(1) + 's linear infinite'; + } + + /* ---------- 首页顶部组件:打字机标语(hero_mode=slogan) ---------- */ + function initHeroSlogan() { + var out = $('.js-sg-text'); + if (!out) return; + var pool = $$('.js-sg-pool span'); + var list = []; + for (var i = 0; i < pool.length; i++) { + var t = (pool[i].textContent || '').trim(); + if (t) list.push(t); + } + if (!list.length) list = ['知识就是力量。']; + var idx = 0, pos = 0, deleting = false; + function run() { + var full = list[idx]; + if (!deleting) { + pos++; + out.textContent = full.slice(0, pos); + if (pos >= full.length) { deleting = true; setTimeout(run, 1800); return; } + setTimeout(run, 90); + } else { + pos--; + out.textContent = full.slice(0, pos); + if (pos <= 0) { deleting = false; idx = (idx + 1) % list.length; setTimeout(run, 360); return; } + setTimeout(run, 38); + } + } + run(); + } + /* ---------- 自动初始化 ---------- */ document.addEventListener('DOMContentLoaded', function () { initSiteIcon(); initTheme(); initClocks(); initQuoteRefresh(); - initBoardHover(); initSearch(); initIconControls(); initCopyButtons(); bindConfirmForms(); initVisitorInfo(); + initHeroFlows(); + initHeroCountUp(); + initHeroClock(); + initNoticeTicker(); + initHeroSlogan(); initLoginDevice(); initVisitsAjax(); initAdminSide(); diff --git a/func/_bar.php b/func/_bar.php deleted file mode 100644 index a7556f5..0000000 --- a/func/_bar.php +++ /dev/null @@ -1,21 +0,0 @@ -'; - echo '
'; - if ($fromNav) { - echo '← 返回上一页'; - } else { - echo '← 返回功能区首页'; - } - echo '' . he($title) . ''; - echo '首页'; - echo '
' . "\n"; -} diff --git a/includes/auth.php b/includes/auth.php index 7345510..90d740a 100644 --- a/includes/auth.php +++ b/includes/auth.php @@ -1,7 +1,7 @@ '', // 搜索引擎列表 JSON;空 = 使用默认 4 个 'last_updated' => '', 'hero_bg' => '', // 首页顶部标识栏背景(纯色 / CSS 渐变),空 = 主题默认 - 'hero_mode' => 'weather', // 首页顶部组件模式:weather=天气(默认) / info=访客信息 / blank=空白精简 + 'hero_mode' => 'weather', // 首页顶部组件模式:weather=天气(默认) / info=信息 / blank=精简 / red=渗透测试 / blue=安全巡检 / posture=安全态势 / trend=访问趋势 / clock=大字时钟 / notice=公告滚动 / slogan=打字机标语 'cdn_ranges' => '', // CDN 段库文本;空 = 使用默认内置列表 'article_note' => '', // 文章区首页顶部注要(Markdown 风格,纯文本) ]; @@ -568,6 +568,72 @@ function visitors_entries(): array return array_reverse($out); // 新→旧 } +/** + * 首页顶部组件「安全态势 / 访问趋势」所需的汇总数据(一次遍历访问日志 + 两条轻量查询)。 + * 返回:今日/累计访问、今日/累计独立 IP、拦截次数、黑名单条数、近 N 天每日访问、最近一次成功登录。 + */ +function home_security_stats(int $days = 7): array +{ + $entries = visitors_entries(); + $total = count($entries); + $today = 0; + $allIps = []; + $todayIps = []; + $blockedTotal = 0; + $blockedToday = 0; + $todayStart = date('Y-m-d 00:00:00'); + // 近 N 天日期桶(含今天,由旧到新) + $buckets = []; + $order = []; + for ($i = $days - 1; $i >= 0; $i--) { + $d = date('Y-m-d', strtotime('-' . $i . ' day')); + $buckets[$d] = 0; + $order[] = $d; + } + foreach ($entries as $e) { + $allIps[$e['ip']] = 1; + if ((int) $e['status'] === 403) { + $blockedTotal++; + } + $day = substr((string) $e['created_at'], 0, 10); + if (isset($buckets[$day])) { + $buckets[$day]++; + } + if ((string) $e['created_at'] >= $todayStart) { + $today++; + $todayIps[$e['ip']] = 1; + if ((int) $e['status'] === 403) { + $blockedToday++; + } + } + } + $daily = []; + foreach ($order as $d) { + $daily[] = ['day' => $d, 'cnt' => $buckets[$d]]; + } + $ban = (int) db()->query('SELECT COUNT(*) FROM ip_blacklist')->fetchColumn(); + $lastLogin = null; + $row = db()->query("SELECT username, ip, created_at FROM login_log WHERE status = 'ok' ORDER BY created_at DESC, id DESC LIMIT 1")->fetch(); + if ($row) { + $lastLogin = [ + 'username' => (string) ($row['username'] ?? ''), + 'ip' => (string) ($row['ip'] ?? ''), + 'time' => (string) ($row['created_at'] ?? ''), + ]; + } + return [ + 'total' => $total, + 'today' => $today, + 'ips_total' => count($allIps), + 'ips_today' => count($todayIps), + 'blocked_total' => $blockedTotal, + 'blocked_today' => $blockedToday, + 'ban' => $ban, + 'daily' => $daily, + 'last_login' => $lastLogin, + ]; +} + /** 自动启动:注册访问日志;黑名单对所有请求(前台与后台登录页)统一拦截;自动风控自检仅针对前台 */ function access_boot(): void { diff --git a/includes/layout.php b/includes/layout.php index 0accea3..f368a72 100644 --- a/includes/layout.php +++ b/includes/layout.php @@ -146,28 +146,6 @@ function layout_footer(): void 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)]; -} - /** 首页访客信息(hero 信息模式):IP 与浏览器/系统/内核由服务端解析 UA 提供,屏幕/语言由前端补齐 */ function visitor_brief(): array { diff --git a/index.html b/index.html new file mode 100644 index 0000000..e28e144 --- /dev/null +++ b/index.html @@ -0,0 +1,312 @@ + + + + + + + +加载中… + + + + + +
+ +
+
+
+
+
+
/// SECHOME-BOOT  //  SECURE CHANNEL
+
AUTH: PENDING  //  NODE 0x1F
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + diff --git a/index.php b/index.php index 2016a8a..4c2ea45 100644 --- a/index.php +++ b/index.php @@ -9,9 +9,10 @@ require_once __DIR__ . '/includes/layout.php'; $siteName = setting_get('site_name', 'SecHome'); $siteSlogan = setting_get('site_slogan', ''); $heroBg = setting_get('hero_bg', ''); // 后台自定义顶部背景(纯色 / CSS 渐变) -// 首页顶部组件模式:weather=天气(默认) / info=访客信息 / blank=空白精简 +// 首页顶部组件模式:weather=天气(默认) / info=信息 / blank=精简 / red=渗透测试 / blue=安全巡检 +// / posture=安全态势 / trend=访问趋势 / clock=大字时钟 / notice=公告滚动 / slogan=打字机标语 $heroMode = setting_get('hero_mode', 'weather'); -if (!in_array($heroMode, ['weather', 'info', 'blank'], true)) { +if (!in_array($heroMode, ['weather', 'info', 'blank', 'red', 'blue', 'posture', 'trend', 'clock', 'notice', 'slogan'], true)) { $heroMode = 'weather'; } $visitor = []; @@ -39,6 +40,17 @@ if (is_file($quoteFile)) { } $quoteText = $quotes ? $quotes[array_rand($quotes)] : '知识就是力量。'; +// 安全态势 / 访问趋势:汇总统计数据(仅对应模式时查询,避免无谓开销) +$secStats = null; +if ($heroMode === 'posture' || $heroMode === 'trend') { + $secStats = home_security_stats(7); +} +// 公告滚动:置顶优先、发布时间倒序 +$heroNotices = []; +if ($heroMode === 'notice') { + $heroNotices = db()->query('SELECT title, pinned, created_at FROM notices WHERE enabled = 1 ORDER BY pinned DESC, created_at DESC, id DESC')->fetchAll(); +} + // 四大板块(按后台排序输出) $cats = db()->query('SELECT * FROM categories ORDER BY sort ASC, id ASC')->fetchAll(); // 首页快捷站点(后台配置,新标签页跳转本域名其它网站) @@ -105,23 +117,179 @@ layout_head('');
系统语言--
+ + +
+
+ 红队 · 渗透测试RED TEAM + 当前阶段:侦察(1/8) +
+
    + +
  1. + + + +
  2. + +
+
+ + +
+
+ 蓝队 · 安全巡检BLUE TEAM + 当前阶段:测绘(1/8) +
+
    + +
  1. + + + +
  2. + +
+
+ + +
+
+
今日访问0 +
+
今日独立 IP0
+
累计访问0 +
+
拦截次数0
+
黑名单0 +
+
最近登录
+
+
+ + + $maxCnt) { + $maxCnt = (int) $d['cnt']; + } + } + $weekMap = ['日', '一', '二', '三', '四', '五', '六']; + ?> +
+
+ 近 7 天访问趋势 + 累计 次 · 今日 + +
+
+ 0 ? (int) round($cnt / $maxCnt * 100) : 0; + $ts = (int) strtotime((string) $d['day']); + ?> +
+ + + + +
+ +
+
+ + +
+
--
+
--:--:--
+
----年--月--日
+
+ 农历 -- + 距下一个节日-- +
+
+ + +
+ 公告 + +
+
+ + 置顶 + +
+
+ + 暂无更新公告,可在后台「更新公告管理」中添加 + +
+ + +
+ > + + +
- -
-
- - - + + +
+
+ + + +
+
+ + + +
-
- - - -
-
+
diff --git a/操作文档.md b/操作文档.md index 1ef138a..12328e6 100644 --- a/操作文档.md +++ b/操作文档.md @@ -7,7 +7,7 @@ ### 1. 首页与板块 - 首页是板块选择页:**科普库 / 红队库 / 蓝队库 / 工具库**,另含文章库入口 -- 顶部组件由站长设置,可能是三种之一:天气、访客信息、空白精简 +- 顶部组件由站长设置,共 10 种可选:天气(默认)、访客信息、空白精简、渗透测试、安全巡检、安全态势、访问趋势、大字时钟、公告滚动、打字机标语 - 右下角「☾ / ☀」按钮切换昼夜主题;「↑」回到顶部 ### 2. 库页面(科普 / 红队 / 蓝队) @@ -89,7 +89,7 @@ - **站点 logo**:上传后全局生效(浏览器标签图标与各页面 logo 同步,**含静态工具页**) - **名言库**:编辑 `data/quota.txt` - **底部信息**:版权、ICP、公安备案等 -- **首页顶部组件**:天气(默认)/ 访客信息 / 空白精简 +- **首页顶部组件**:10 种模式可选(天气 / 访客信息 / 空白精简 / 渗透测试 / 安全巡检 / 安全态势 / 访问趋势 / 大字时钟 / 公告滚动 / 打字机标语) - **修改密码**:建议首次登录即修改默认 `admin/admin123` ### 6. 更新公告(notice.php) @@ -158,7 +158,7 @@ assets/js/blowfish.js egoroof-blowfish 2.2.2 (Navicat 密码解密用) assets/js/sm-crypto/sm2.js|sm3.js|sm4.js sm-crypto 0.3.13 ``` -5. **忘记后台密码**:数据库 `users` 表密码为 `md5(base64(明文))`;可将某行改为已知口令的散列后登录,随后在后台重设。 +5. **忘记后台密码**:数据库 `users` 表存储的是密码散列(PHP `password_hash` 强散列)。可将某行临时改为已知口令的旧版散列 `md5(base64(明文))`(系统兼容旧版并会在登录后自动升级)后登录,随后在后台重设。 6. **保存设置后提示去哪里了?** 后台操作提示统一显示为**右下角 Toast**(成功约 3.6 秒、失败约 6 秒自动消失),不会占用页面顶部。 ---