优化网站

This commit is contained in:
MasonLiu 2026-09-14 22:06:21 +08:00
parent 9bb6e6a41e
commit 0e7265481f
22 changed files with 1071 additions and 238 deletions

View File

@ -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
### 新增

View File

@ -8,8 +8,8 @@
- **多库分区**:科普库 / 红队库 / 蓝队库三大板块 + 工具库 + 文章库
- 每个库内分四区:**导航区 · 内部功能区 · 文章区 · 外部工具区**
- 首页顶部组件三种模式(天气 / 访客信息 / 空白精简)可后台切换
- 快捷站点磁贴,悬停显示备注
- 首页顶部组件可在后台切换(天气 / 访客信息 / 空白精简 / 渗透测试 / 安全巡检 / 安全态势 / 访问趋势 / 大字时钟 / 公告滚动 / 打字机标语,共 10 种)
- 快捷站点磁贴:圆形图标 + 名称 + 备注一览(悬停亦可查看完整备注)
### 功能区(纯前端,隐私优先)

View File

@ -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) {
?>
<div class="tip" style="margin:10px 0 6px">
共命中 <?= (int) $d['total'] ?> <?= $mode === 'compact' ? '个 IP' : '条记录' ?>
共命中 <?= (int) $d['total'] ?> <?= $mode === 'compact' ? '个 IP(按访问次数从多到少)' : '条记录' ?>
当前第 <?= $page ?> / <?= $totalPages ?> 页,每页 <?= (int) $d['perPage'] ?> 条。
</div>
<div class="v-scroll">

View File

@ -244,8 +244,12 @@ admin_topbar('articles');
?>
<main class="page-main">
<div class="wrap page-body">
<h2 style="margin-bottom:6px">文章管理</h2>
<p class="tip">文章库(第五大板块)的文章维护:正文为标准 Markdown图片统一上传到 data/articles/img 目录。</p>
<div class="admin-head">
<div class="ah-main">
<h2 class="ah-title">文章管理</h2>
<p class="ah-sub">文章库(第五大板块)的文章维护:正文为标准 Markdown图片统一上传到 data/articles/img 目录。</p>
</div>
</div>
<?php if ($savedFlag): ?>
<script>window.hpToastMsg = <?= json_encode(['kind' => 'ok', 'text' => '保存成功,文章库前台已同步更新。'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>

View File

@ -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');
?>
<main class="page-main">
<div class="wrap page-body">
<h2 style="margin-bottom:6px">网站管理</h2>
<p class="tip">对全站展示内容与 logo 进行维护,保存后自动更新首页"上次更新时间"</p>
<div class="admin-head">
<div class="ah-main">
<h2 class="ah-title">网站管理</h2>
<p class="ah-sub">对全站展示内容与 LOGO 进行维护,保存后自动更新首页“上次更新时间”。</p>
</div>
</div>
<?php if ($msgText !== ''):
alert_html($msgKind, $msgText); endif; ?>
@ -366,11 +370,9 @@ admin_topbar('content');
<label class="sw" title="点击立即启用/关闭站长提示(即时生效,无需保存)"><input type="checkbox" class="sw-in tips-toggle"
<?= ((string) ($all['show_st_tips'] ?? '1') === '1') ? ' checked' : '' ?>><span class="sw-b"><span
class="sw-on">ON</span><span class="sw-off">OFF</span></span></label>
<span class="tip" style="margin:0">开启时,前台显示“可切换搜索引擎进行搜索…”等辅助旁白</span>
<span class="tip" style="margin:0">开启时,前台显示搜索栏说明、各分区说明条等辅助旁白</span>
</div>
<p class="tip" style="margin-top:6px">前台类似“可切换搜索引擎进行搜索4 个可用,后台可维护)”、各分区说明条等辅助旁白,以及文案中带后台入口字样的引导句,均带
<code>st-tip</code>
标记;关闭后这些文案在全部前台页面统一隐藏,站点导航、搜索、内容等核心功能不受影响。各分区/页面在没有数据时展示的空态占位框(如“该库还没有导航内容”)不受本开关影响,会始终显示以避免空页突兀。</p>
<p class="tip" style="margin-top:6px">上述辅助旁白均带 <code>st-tip</code> 标记,关闭后在全站前台统一隐藏;站点导航、搜索与内容等核心功能不受影响。各分区/页面无数据时的空态占位框(如“该库还没有导航内容”)不受本开关影响,始终显示以避免空页突兀。</p>
</div>
<!-- 站点信息 -->
@ -444,15 +446,31 @@ admin_topbar('content');
<form method="post">
<?= csrf_field() ?><input type="hidden" name="act" value="hero_mode">
<div class="field-row" style="align-items:center">
<label class="seg-check"><input type="radio" name="hero_mode" value="weather" <?= $hm === 'weather' ? ' checked' : '' ?>> 🌤️ 天气模式(默认)</label>
<label class="seg-check"><input type="radio" name="hero_mode" value="info" <?= $hm === 'info' ? ' checked' : '' ?>> 🖥️ 访客信息模式</label>
<label class="seg-check"><input type="radio" name="hero_mode" value="weather" <?= $hm === 'weather' ? ' checked' : '' ?>> 🌤️ 天气模式</label>
<label class="seg-check"><input type="radio" name="hero_mode" value="info" <?= $hm === 'info' ? ' checked' : '' ?>> 🖥️ 信息模式</label>
<label class="seg-check"><input type="radio" name="hero_mode" value="blank" <?= $hm === 'blank' ? ' checked' : '' ?>> ⬜ 空白模式</label>
<label class="seg-check"><input type="radio" name="hero_mode" value="red" <?= $hm === 'red' ? ' checked' : '' ?>> ⚔️ 渗透测试</label>
<label class="seg-check"><input type="radio" name="hero_mode" value="blue" <?= $hm === 'blue' ? ' checked' : '' ?>> 🛡️ 安全巡检</label>
<label class="seg-check"><input type="radio" name="hero_mode" value="posture" <?= $hm === 'posture' ? ' checked' : '' ?>> 📊 安全态势</label>
<label class="seg-check"><input type="radio" name="hero_mode" value="trend" <?= $hm === 'trend' ? ' checked' : '' ?>> 📈 访问趋势</label>
<label class="seg-check"><input type="radio" name="hero_mode" value="clock" <?= $hm === 'clock' ? ' checked' : '' ?>> 🕐 大字时钟</label>
<label class="seg-check"><input type="radio" name="hero_mode" value="notice" <?= $hm === 'notice' ? ' checked' : '' ?>> 📢 公告滚动</label>
<label class="seg-check"><input type="radio" name="hero_mode" value="slogan" <?= $hm === 'slogan' ? ' checked' : '' ?>> ⌨️ 标语打印</label>
</div>
<div style="margin-top:12px"><button type="submit" class="btn btn-primary">保存顶部组件模式</button></div>
</form>
<p class="tip" style="margin:10px 0 0">
天气模式(默认):横幅展示第三方天气条;访客信息模式:横幅显示当前访问者 IP、浏览器、操作系统、内核、屏幕分辨率与系统语言空白模式隐藏横幅仅保留站点信息与时间信息顶部高度自动缩减。
</p>
<ul class="tip" style="margin:10px 0 0;padding-left:20px;line-height:1.9">
<li><b>天气(默认)</b>:横幅展示第三方天气条</li>
<li><b>访客信息</b>:显示访问者 IP、浏览器、操作系统、内核、屏幕分辨率与系统语言</li>
<li><b>空白精简</b>:隐藏横幅,仅保留站点信息与时间,顶部高度自动缩减</li>
<li><b>渗透测试</b>:动态步进展示 侦察→武器化→投递→利用→提权→横移→维持→达成</li>
<li><b>安全巡检</b>:动态步进展示 测绘→基线→收敛→布防→研判→处置→溯源→复盘</li>
<li><b>安全态势</b>:动态展示今日/累计访问、独立 IP、拦截次数、黑名单与最近登录</li>
<li><b>访问趋势</b>:近 7 天访问量柱状图</li>
<li><b>大字时钟</b>:大号时间 + 日期 + 时段问候</li>
<li><b>公告滚动</b>:横向滚动展示启用中的公告(置顶优先)</li>
<li><b>标语打印</b>:逐字打印轮播名言,复用「滚动信息栏」内容;此时首页底部信息栏自动隐藏</li>
</ul>
</div>
<!-- 底部信息 -->

View File

@ -133,12 +133,16 @@ admin_topbar('exttools');
?>
<main class="page-main">
<div class="wrap page-body">
<h2 style="margin-bottom:6px">外部工具区管理</h2>
<p class="tip">
链接较多时可先筛选再编辑:按 分区/栏目、启停状态、展示范围、关键词 快速过滤(仅影响当前浏览,不影响保存)。
行首“分区 / 栏目”即前台分组标题(如“合集 / 导航”),<strong>留空 = 不分组平铺</strong>
展示范围勾选库,<strong>全部不勾选 = 所有库通用</strong>
</p>
<div class="admin-head">
<div class="ah-main">
<h2 class="ah-title">外部工具区管理</h2>
<p class="ah-sub">
链接较多时可先筛选再编辑:按 分区/栏目、启停状态、展示范围、关键词 快速过滤(仅影响当前浏览,不影响保存)。
行首“分区 / 栏目”即前台分组标题(如“合集 / 导航”),<strong>留空 = 不分组平铺</strong>
展示范围勾选库,<strong>全部不勾选 = 所有库通用</strong>
</p>
</div>
</div>
<?php if ($saved): ?>
<script>window.hpToastMsg = <?= json_encode(['kind' => 'ok', 'text' => '保存成功,库首页外部工具区已同步更新。'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>

View File

@ -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 = '用户名或密码错误。';
}
}

View File

@ -156,11 +156,15 @@ admin_topbar('nav');
?>
<main class="page-main">
<div class="wrap page-body">
<h2 style="margin-bottom:6px">导航管理</h2>
<p class="tip">管理 <?= he($catNames[$cat]) ?> 的导航块与链接;块内最多 15 个链接3 小列 × 5 行),超出部分请拆分到新块。</p>
<div class="admin-head">
<div class="ah-main">
<h2 class="ah-title">导航管理</h2>
<p class="ah-sub">管理 <?= he($catNames[$cat]) ?> 的导航块与链接;块内最多 15 个链接3 小列 × 5 行),超出部分请拆分到新块。</p>
</div>
</div>
<!-- 分类切换 -->
<div class="field-row" style="margin:12px 0">
<div class="field-row" style="margin:0 0 12px">
<?php foreach ($catKeys as $ck): ?>
<a class="btn <?= $cat === $ck ? 'btn-primary' : '' ?>"
href="nav.php?c=<?= he($ck) ?>"><?= he($catNames[$ck]) ?></a>

View File

@ -106,12 +106,16 @@ admin_topbar('notice');
?>
<main class="page-main">
<div class="wrap page-body">
<h2 style="margin-bottom:6px">更新公告管理</h2>
<p class="tip">
前台页脚「更新公告」页展示内容:可发布多条公告并<strong>保留历史</strong>,任意一条均可再次编辑。
内容支持超链接,写法 <code>[文字](链接)</code>;站内用相对路径(如 <code>/index.php</code><code>/func/index.php</code>
外链用 <code>https://</code>,也可直接粘贴网址自动识别。换行会被保留。
</p>
<div class="admin-head">
<div class="ah-main">
<h2 class="ah-title">更新公告管理</h2>
<p class="ah-sub">
前台页脚「更新公告」页展示内容:可发布多条公告并<strong>保留历史</strong>,任意一条均可再次编辑。
内容支持超链接,写法 <code>[文字](链接)</code>;站内用相对路径(如 <code>/index.php</code><code>/func/index.php</code>
外链用 <code>https://</code>,也可直接粘贴网址自动识别。换行会被保留。
</p>
</div>
</div>
<?php if ($savedFlag): ?>
<script>window.hpToastMsg = <?= json_encode(['kind' => 'ok', 'text' => '公告已保存,前台已同步更新。'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>

View File

@ -144,11 +144,15 @@ admin_topbar('tools');
?>
<main class="page-main">
<div class="wrap page-body">
<h2 style="margin-bottom:6px">内部功能</h2>
<p class="tip">
管理功能区首页展示的工具卡片。站内工具页需先在 <code>func/</code> 下开发对应页面,再到此处登记入口;
也可登记外部网址作为外链卡片(新窗口打开)。
</p>
<div class="admin-head">
<div class="ah-main">
<h2 class="ah-title">内部功能</h2>
<p class="ah-sub">
管理功能区首页展示的工具卡片。站内工具页需先在 <code>func/</code> 下开发对应页面,再到此处登记入口;
也可登记外部网址作为外链卡片(新窗口打开)。
</p>
</div>
</div>
<?php if ($saved): ?>
<script>window.hpToastMsg = <?= json_encode(['kind' => 'ok', 'text' => '保存成功,功能区首页已同步更新。'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>

View File

@ -217,8 +217,13 @@ admin_topbar('visits');
?>
<main class="page-main">
<div class="wrap page-body">
<h2 style="margin-bottom:6px">风控管理</h2>
<p class="tip">记录访客对本站的动态请求(含后台),完整模式逐条展示访问明细,精简模式按 IP 汇总次数;黑名单 / 自动风控对所有访问统一生效,命中即返回 403 风控提示页,后台与登录页不例外。</p>
<div class="admin-head">
<div class="ah-main">
<h2 class="ah-title">风控管理</h2>
<p class="ah-sub">记录访客对本站的动态请求(含后台),完整模式逐条展示访问明细,精简模式按 IP 汇总次数;黑名单 / 自动风控对所有访问统一生效,命中即返回 403 风控提示页,后台与登录页不例外。
</p>
</div>
</div>
<?php if ($msgText !== ''): ?>
<script>window.hpToastMsg = <?= json_encode(['kind' => $msgKind === 'ok' ? 'ok' : 'err', 'text' => $msgText], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>;</script>
@ -315,7 +320,7 @@ admin_topbar('visits');
<select name="mode" title="展示模式"
onchange="if(this.form.requestSubmit){this.form.requestSubmit();}else{this.form.submit();}">
<option value="full" <?= $mode === 'full' ? ' selected' : '' ?>>完整模式(逐条明细)</option>
<option value="compact" <?= $mode === 'compact' ? ' selected' : '' ?>>精简模式(仅时间 / IP / 次数</option>
<option value="compact" <?= $mode === 'compact' ? ' selected' : '' ?>>精简模式(按访问次数排序</option>
</select>
<input type="text" name="ip" value="<?= he($ipF) ?>" placeholder="按 IP 筛选,支持模糊(如 203.0">
<input type="text" name="q" class="w-s" value="<?= he($q) ?>" placeholder="关键词:地址 / 请求方式">

View File

@ -1,70 +0,0 @@
<?php
/**
* article/data.php —— 文章库 JSON 数据接口
* data.php?mode=list 文章元数据列表(按分区分组,含分区名)
* data.php?mode=content&id=N 单篇文章完整数据(含 markdown 正文)
* 说明仅返回前台可见enabled=1)文章;隐文不对外。
*/
require_once dirname(__DIR__) . '/includes/db.php';
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$pdo = db();
function json_out(array $data): void
{
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
$mode = (string)($_GET['mode'] ?? 'list');
if ($mode === 'content') {
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
json_out(['ok' => 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]);

View File

@ -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; }
/* 通用图标 <img>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; }

BIN
assets/img/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -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 () { /* 点击由 <a> 包裹,这里无额外处理 */ });
});
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();

View File

@ -1,21 +0,0 @@
<?php
/**
* func/_bar.php —— 功能区子页公共头部条(需先引入 includes/layout.php
* 仅包含函数,不直接输出。
*/
function func_bar(string $title): void
{
// 从库首页“内部功能区”卡片进入时(?from=nav返回按钮改为“返回上一页”
$fromNav = (($_GET['from'] ?? '') === 'nav');
echo '<div class="func-bar">';
echo '<div class="wrap func-bar-inner">';
if ($fromNav) {
echo '<a class="btn btn-sm" href="javascript:history.back()" title="返回来源页面">← 返回上一页</a>';
} else {
echo '<a class="btn btn-sm" href="index.php">← 返回功能区首页</a>';
}
echo '<span class="func-bar-title">' . he($title) . '</span>';
echo '<a class="btn btn-sm" href="../index.php">首页</a>';
echo '</div></div>' . "\n";
}

View File

@ -1,7 +1,7 @@
<?php
/**
* auth.php —— 后台登录会话工具
* 密码存储算法(按需求):md5(base64(明文))
* 密码存储算法PHP password_hash 强散列bcrypt/argon2兼容旧库 md5(base64(明文)) 并自动升级。
*/
require_once __DIR__ . '/db.php';

View File

@ -215,7 +215,7 @@ function db_init(PDO $pdo): void
'search_engines' => '', // 搜索引擎列表 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
{

View File

@ -146,28 +146,6 @@ function layout_footer(): void
echo '</div></footer>' . "\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
{

312
index.html Normal file
View File

@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<script>
/* ======================= 加载页配置(在此调整) =======================
* logo : 标志图片(白底会被自动抠成透明并做霓虹发光)
* target : 加载动画结束后跳转的目标页
* speed : 全局速度倍率,>1 更快、<1 更慢
* collapseIn : 结尾“向正中心坍缩”的时长(秒)
* holdBeforeCollapse : 主体动画结束到开始坍缩之间的停顿(秒)
* once : true = 同一浏览器会话内仅播放一次(跨标签页共享,关闭浏览器后再进入会重新播放)
* title / icon : 浏览器标签标题 / 图标icon 留空则使用 logo
* ==================================================================== */
window.LOGO_CONFIG = {
logo: 'assets/img/logo.jpg',
target: 'index.php',
speed: 1.4,
collapseIn: 0.55,
holdBeforeCollapse: 0.25,
once: true,
title: '',
icon: ''
};
</script>
<script>
/* 本会话已播放过:立即跳转目标页(脚本置于最前,尽量缩短空白/闪烁)。
* 使用“会话级 Cookie”同一会话内所有标签页共享关闭浏览器后自动失效。 */
(function () {
try {
var C = window.LOGO_CONFIG || {};
if (C.once !== false && /(^|;\s*)hp_intro=1(;|$)/.test(document.cookie)) {
window.__hpIntroSkip = true; /* 已播放过:跳过动画脚本,直接跳转 */
location.replace(C.target || 'index.php');
}
} catch (e) { }
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>加载中…</title>
<noscript><meta http-equiv="refresh" content="0;url=index.php"></noscript>
<style>
:root{--neon:#00ff41;--neon2:#a6ffb8;--bg:#000;}
*{margin:0;padding:0;box-sizing:border-box;}
html,body{width:100%;height:100%;overflow:hidden;background:var(--bg);}
body{font-family:"Consolas","Menlo","Courier New",monospace;color:#7dffa0;}
/* 背景层 */
#rain{position:fixed;inset:0;z-index:0;opacity:0;pointer-events:none;}
.grid{position:fixed;inset:0;pointer-events:none;opacity:.5;background-image:
linear-gradient(rgba(0,255,65,.06) 1px,transparent 1px),
linear-gradient(90deg,rgba(0,255,65,.06) 1px,transparent 1px);background-size:44px 44px;
-webkit-mask-image:radial-gradient(ellipse at center,#000 18%,transparent 78%);
mask-image:radial-gradient(ellipse at center,#000 18%,transparent 78%);}
.scan{position:fixed;inset:0;pointer-events:none;opacity:.35;
background:repeating-linear-gradient(0deg,rgba(0,0,0,.4) 0 1px,transparent 1px 3px);}
.vig{position:fixed;inset:0;pointer-events:none;
background:radial-gradient(ellipse at center,transparent 45%,rgba(0,0,0,.85) 100%);}
.corner{position:fixed;width:40px;height:40px;pointer-events:none;
border:2px solid rgba(0,255,65,.5);}
.corner.tl{top:18px;left:18px;border-right:0;border-bottom:0;}
.corner.tr{top:18px;right:18px;border-left:0;border-bottom:0;}
.corner.bl{bottom:18px;left:18px;border-right:0;border-top:0;}
.corner.br{bottom:18px;right:18px;border-left:0;border-top:0;}
.hud{position:fixed;pointer-events:none;font-size:12px;letter-spacing:2px;
color:rgba(0,255,65,.75);white-space:nowrap;}
.hud.tl{top:22px;left:66px;} .hud.tr{top:22px;right:66px;}
/* 动画层(结尾坍缩) */
#animLayer{position:fixed;inset:0;z-index:10;overflow:hidden;background:var(--bg);
transform-origin:50% 50%;transition-property:transform,opacity;
transition-timing-function:cubic-bezier(.7,0,.84,0),ease-in;
transition-duration:.9s;will-change:transform,opacity;}
#animLayer.collapse{transform:scale(.02);opacity:0;}
/* 舞台 */
.center{position:fixed;inset:0;display:flex;flex-direction:column;align-items:center;
justify-content:center;gap:22px;pointer-events:none;}
svg#logoStage{width:min(56vw,300px);height:auto;overflow:visible;}
.glitch{animation:flick .55s linear 1;}
@keyframes flick{0%,100%{opacity:1}12%{opacity:.28}22%{opacity:1}38%{opacity:.5}
50%{opacity:1}64%{opacity:.18}74%{opacity:1}88%{opacity:.55}}
/* 终端 + 进度 */
.term{min-height:76px;font-size:12.5px;line-height:1.7;letter-spacing:1px;
color:#7dffa0;text-align:left;white-space:pre;text-shadow:0 0 8px rgba(0,255,65,.5);}
.term b{color:var(--neon);font-weight:700;}
.bar{width:min(62vw,320px);height:4px;background:rgba(0,255,65,.14);border-radius:3px;overflow:hidden;}
.bar i{display:block;height:100%;width:0%;border-radius:3px;
background:linear-gradient(90deg,#00ff41,#c9ffd6);box-shadow:0 0 12px rgba(0,255,65,.85);}
</style>
</head>
<body>
<div id="animLayer">
<canvas id="rain"></canvas>
<div class="grid"></div>
<div class="vig"></div>
<div class="scan"></div>
<div class="corner tl"></div><div class="corner tr"></div>
<div class="corner bl"></div><div class="corner br"></div>
<div class="hud tl">/// SECHOME-BOOT &nbsp;//&nbsp; SECURE CHANNEL</div>
<div class="hud tr">AUTH: PENDING &nbsp;//&nbsp; NODE 0x1F</div>
<div class="center">
<svg id="logoStage" viewBox="0 0 520 560" preserveAspectRatio="xMidYMid meet">
<defs>
<!-- 白底抠除 + 霓虹发光(一次成型,动画只改 clip不重复跑滤镜 -->
<filter id="logoFx" x="-30%" y="-30%" width="160%" height="160%" color-interpolation-filters="sRGB">
<feColorMatrix type="matrix"
values="0 0 0 0 .06 0 0 0 0 1 0 0 0 0 .28 -.299 -.587 -.114 0 1"/>
<feComponentTransfer>
<feFuncA type="linear" slope="1.9" intercept="-.28"/>
</feComponentTransfer>
<feGaussianBlur stdDeviation="5" result="g"/>
<feMerge>
<feMergeNode in="g"/><feMergeNode in="g"/><feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<clipPath id="revealClip"><rect id="revealRect" x="0" y="0" width="520" height="0"/></clipPath>
</defs>
<g clip-path="url(#revealClip)">
<g id="logoWrap">
<g id="logoG" filter="url(#logoFx)">
<image id="logoImg" x="0" y="0" width="520" height="560" href="assets/img/logo.jpg"
preserveAspectRatio="xMidYMid meet"/>
</g>
</g>
</g>
<rect id="scanEdge" x="0" y="0" width="520" height="3" fill="#7dffa0" opacity="0"/>
</svg>
<div class="term" id="term"></div>
<div class="bar"><i id="barFill"></i></div>
</div>
</div><!-- /#animLayer -->
<script>
(function(){
if(window.__hpIntroSkip) return; /* 本会话已播放过,正在跳转,无需再跑动画 */
var CFG=Object.assign(
{logo:'assets/img/logo.jpg',target:'index.php',speed:1,collapseIn:0.9,
holdBeforeCollapse:0.4,once:true,title:'',icon:''}, window.LOGO_CONFIG||{});
var K=1/(CFG.speed>0?CFG.speed:1);
var clamp01=function(v){return v<0?0:v>1?1:v;};
var $=function(id){return document.getElementById(id);};
var animLayer=$('animLayer');
var logoStage=$('logoStage'), logoImg=$('logoImg'), logoG=$('logoG');
var revealRect=$('revealRect'), scanEdge=$('scanEdge'), logoWrap=$('logoWrap');
var barFill=$('barFill'), termEl=$('term'), rainCv=$('rain');
/* 会话级标记:开始播放即写入,本次会话(含其它标签页)不再重复播放 */
if(CFG.once!==false){
try{ document.cookie='hp_intro=1; path=/; SameSite=Lax'; }catch(e){}
}
/* 标签标题 / 图标(静态页:默认取配置,留空则用 logo 作为图标) */
if(CFG.title) document.title=CFG.title;
function setFavicon(href){
var links=document.querySelectorAll("link[rel*='icon']");
if(links.length){ for(var i=0;i<links.length;i++) links[i].href=href; }
else{ var l=document.createElement('link'); l.rel='icon'; l.href=href; document.head.appendChild(l); }
}
setFavicon(CFG.icon || CFG.logo);
/* 读取图片真实尺寸,按比例布置揭示/扫光几何 */
var W=520,H=560;
function applyGeom(){
logoStage.setAttribute('viewBox','0 0 '+W+' '+H);
logoStage.style.aspectRatio=W+' / '+H;
logoImg.setAttribute('width',W); logoImg.setAttribute('height',H);
revealRect.setAttribute('width',W);
scanEdge.setAttribute('width',W);
}
var probe=new Image();
probe.onload=function(){ if(probe.naturalWidth){ W=probe.naturalWidth; H=probe.naturalHeight; } applyGeom(); };
probe.onerror=function(){ applyGeom(); };
probe.src=CFG.logo;
logoImg.setAttribute('href',CFG.logo);
applyGeom();
/* 与站点 logo 保持同步api/site.php失败或未设置则使用默认 logo.jpg + 霓虹)。 */
(function syncSiteLogo(){
try{
var xhr=new XMLHttpRequest();
xhr.open('GET','api/site.php',true);
xhr.timeout=2500;
xhr.onreadystatechange=function(){
if(xhr.readyState!==4) return;
try{
var d=JSON.parse(xhr.responseText||'{}');
if(d && typeof d.logo==='string' && d.logo!==''){
/* 自定义 logo按原样展示关闭白底抠除/霓虹,避免破坏配色) */
logoG.removeAttribute('filter');
logoImg.setAttribute('href',d.logo);
probe.onload=function(){ if(probe.naturalWidth){ W=probe.naturalWidth; H=probe.naturalHeight; } applyGeom(); };
probe.src=d.logo;
}
}catch(e){}
};
xhr.onerror=function(){};
xhr.send();
}catch(e){}
})();
/* ---------- 矩阵雨(轻量:限制列数 + 降帧) ---------- */
var cx=rainCv.getContext('2d');
var cols=[], cw=14, fs=14, rainOn=true;
function rainResize(){
rainCv.width=innerWidth; rainCv.height=innerHeight;
var n=Math.ceil(rainCv.width/cw);
cols=[]; for(var i=0;i<n;i++) cols.push(Math.random()*rainCv.height);
}
function rainStep(){
if(!rainOn) return;
cx.fillStyle='rgba(0,6,2,0.14)'; cx.fillRect(0,0,rainCv.width,rainCv.height);
cx.font=fs+'px monospace';
for(var i=0;i<cols.length;i++){
var ch=String.fromCharCode(0x30A0+((Math.random()*90)|0));
cx.fillStyle = Math.random()<0.05 ? '#c9ffd6' : 'rgba(0,255,65,0.55)';
cx.fillText(ch, i*cw, cols[i]);
cols[i] = cols[i]>rainCv.height+Math.random()*260 ? 0 : cols[i]+fs;
}
}
rainResize(); addEventListener('resize',rainResize);
var rainTimer=setInterval(rainStep,46); /* ≈22fps开销很小 */
/* ---------- 状态 & 时间线 ---------- */
var S={rev:0,rain:0,edgeY:0,edgeO:0,flashB:1,prog:0};
var ez={
linear:function(t){return t;},
out3:function(t){return 1-Math.pow(1-t,3);},
inOut3:function(t){return t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;},
inOut5:function(t){return t<.5?16*t*t*t*t*t:1-Math.pow(-2*t+2,5)/2;}
};
var tweens=[];
var tw=function(s,d,apply,ease){s*=K;d*=K;tweens.push({start:s,end:s+d,dur:d,apply:apply,e:ez[ease]||ez.inOut3});};
tw(0.00,0.60,function(e){S.rain=e*0.55;},'out3'); /* 矩阵雨淡入 */
tw(0.10,1.05,function(e){ /* 自上而下扫描揭示 */
S.rev=e; S.edgeY=H*e;
S.edgeO=0.95*Math.min(1,e*6)*Math.min(1,(1-e)*8); /* 揭示边缘亮线 */
},'inOut5');
tw(1.78,0.45,function(e){S.flashB=1+Math.sin(e*Math.PI)*0.9;},'linear'); /* 通电闪光 */
tw(1.95,0.75,function(e){S.edgeY=-20+e*H; S.edgeO=Math.sin(clamp01(e)*Math.PI)*0.9;},'inOut3'); /* 扫光过顶 */
tw(2.40,1.10,function(e){S.prog=e;},'inOut3'); /* 进度条 */
var maxEnd=Math.max.apply(null,tweens.map(function(x){return x.end;}));
/* 终端打字 */
var LINES=[
{t:0.10, txt:'> INITIALIZING SECHOME CORE...', cps:34},
{t:1.30, txt:'> DECRYPTING MODULE 0x9F3A ...... [ OK ]', cps:34},
{t:2.20, txt:'> VERIFYING SIGNATURE .......... [ OK ]', cps:34},
{t:3.20, txt:'> ACCESS GRANTED', cps:26}
];
function updateTerm(t){
var html='';
for(var i=0;i<LINES.length;i++){
var L=LINES[i];
if(t<L.t) break;
var n=Math.min(L.txt.length, Math.max(0,(t-L.t)*L.cps)|0);
html+= (i?'\n':'')+L.txt.slice(0,n)+(n<L.txt.length?'_':'');
}
termEl.textContent=html;
}
function render(){
revealRect.setAttribute('height',(H*S.rev).toFixed(2));
scanEdge.setAttribute('y',(S.edgeY-1.5).toFixed(2));
scanEdge.style.opacity=S.edgeO.toFixed(3);
logoWrap.style.filter = S.flashB>1.001 ? 'brightness('+S.flashB.toFixed(3)+')' : 'none';
rainCv.style.opacity=S.rain.toFixed(3);
barFill.style.width=(S.prog*100).toFixed(1)+'%';
}
/* 稳定抖动 */
setTimeout(function(){ if(!collapsed) logoG.classList.add('glitch'); }, 1240/K);
/* ---------- 结尾坍缩 → 跳转目标页 ---------- */
var collapsed=false;
function startCollapse(){
rainOn=false; clearInterval(rainTimer);
var dur=Math.max(0.05,CFG.collapseIn*K);
animLayer.style.transitionDuration=dur+'s';
void animLayer.offsetWidth;
animLayer.classList.add('collapse');
setTimeout(function(){ location.replace(CFG.target||'index.php'); }, dur*1000+60);
}
var t0=null;
function frame(now){
if(t0===null) t0=now;
var t=(now-t0)/1000;
for(var i=0;i<tweens.length;i++){
var x=tweens[i];
if(t<x.start||t>x.end) continue;
x.apply(x.e((t-x.start)/x.dur));
}
render(); updateTerm(t);
if(t>maxEnd+CFG.holdBeforeCollapse*K){
if(!collapsed){ collapsed=true; startCollapse(); }
return;
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
</script>
</body>
</html>

196
index.php
View File

@ -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('');
<div class="vb-cell"><span>系统语言</span><b class="js-vlang">--</b></div>
</div>
</div>
<?php elseif ($heroMode === 'red'): ?>
<!-- 红队 · 渗透测试流程(动态步进) -->
<div class="ptflow ptflow-red js-ptflow">
<div class="ptflow-head">
<span class="ptflow-title">红队 · 渗透测试<span class="ptflow-tag">RED TEAM</span></span>
<span class="ptflow-phase js-ptflow-phase">当前阶段侦察1/8</span>
</div>
<ol class="ptflow-track">
<?php
$redSteps = [
['侦察', '🔍', '信息收集', '域名 / 子域 / 指纹 / 社工面'],
['武器化', '🧰', '载荷准备', '利用链与免杀载荷'],
['投递', '✉️', '边界投递', '钓鱼 / 暴露服务入口'],
['利用', '💥', '漏洞利用', '获取初始立足点'],
['提权', '⬆️', '权限提升', '本地提权至最高权限'],
['横移', '🌐', '内网横向', '隧道与凭据横向移动'],
['维持', '📌', '权限维持', '后门与持久化驻留'],
['达成', '🎯', '目标达成', '取证留痕并输出报告'],
];
foreach ($redSteps as $s): ?>
<li class="ptflow-step" data-phase="<?= he($s[0]) ?>">
<span class="ptflow-ic"><?= he($s[1]) ?></span>
<b><?= he($s[2]) ?></b>
<small><?= he($s[3]) ?></small>
</li>
<?php endforeach; ?>
</ol>
</div>
<?php elseif ($heroMode === 'blue'): ?>
<!-- 蓝队 · 渗透测试(动态步进) -->
<div class="ptflow ptflow-blue js-ptflow">
<div class="ptflow-head">
<span class="ptflow-title">蓝队 · 安全巡检<span class="ptflow-tag">BLUE TEAM</span></span>
<span class="ptflow-phase js-ptflow-phase">当前阶段测绘1/8</span>
</div>
<ol class="ptflow-track">
<?php
$blueSteps = [
['测绘', '🗺', '资产测绘', '暴露面与资产台账'],
['基线', '🧭', '基线核查', '配置与合规核查'],
['收敛', '🧱', '攻击面收敛', '暴露服务治理'],
['布防', '📡', '监测布防', '流量 / 主机 / 日志'],
['研判', '🚨', '告警研判', '规则命中与降噪'],
['处置', '🧯', '应急处置', '隔离封禁与止损'],
['溯源', '🎯', '溯源反制', '攻击者画像定位'],
['复盘', '🔒', '加固复盘', '策略加固与总结'],
];
foreach ($blueSteps as $s): ?>
<li class="ptflow-step" data-phase="<?= he($s[0]) ?>">
<span class="ptflow-ic"><?= he($s[1]) ?></span>
<b><?= he($s[2]) ?></b>
<small><?= he($s[3]) ?></small>
</li>
<?php endforeach; ?>
</ol>
</div>
<?php elseif ($heroMode === 'posture'): ?>
<!-- 安全态势简报:今日/累计访问、独立 IP、拦截、黑名单、最近登录数字动态增长 -->
<div class="visitor-box js-posture">
<div class="vb-grid">
<div class="vb-cell"><span>今日访问</span><b class="js-count" data-to="<?= (int) ($secStats['today'] ?? 0) ?>">0</b>
</div>
<div class="vb-cell"><span>今日独立 IP</span><b class="js-count"
data-to="<?= (int) ($secStats['ips_today'] ?? 0) ?>">0</b></div>
<div class="vb-cell"><span>累计访问</span><b class="js-count" data-to="<?= (int) ($secStats['total'] ?? 0) ?>">0</b>
</div>
<div class="vb-cell"><span>拦截次数</span><b class="js-count"
data-to="<?= (int) ($secStats['blocked_total'] ?? 0) ?>">0</b></div>
<div class="vb-cell"><span>黑名单</span><b class="js-count" data-to="<?= (int) ($secStats['ban'] ?? 0) ?>">0</b>
</div>
<div class="vb-cell"><span>最近登录</span><b><?php
$ll = $secStats['last_login'] ?? null;
if (is_array($ll) && (string) ($ll['time'] ?? '') !== '') {
echo he(substr((string) $ll['time'], 0, 16));
} else {
echo '暂无记录';
}
?></b></div>
</div>
</div>
<?php elseif ($heroMode === 'trend'): ?>
<!-- 访问趋势:近 7 天访问量柱状图(逐柱动态生长) -->
<?php
$daily = $secStats['daily'] ?? [];
$maxCnt = 0;
foreach ($daily as $d) {
if ((int) $d['cnt'] > $maxCnt) {
$maxCnt = (int) $d['cnt'];
}
}
$weekMap = ['日', '一', '二', '三', '四', '五', '六'];
?>
<div class="trend-box js-trend">
<div class="trend-head">
<span class="trend-title"> 7 天访问趋势</span>
<span class="trend-sub">累计 <b><?= (int) ($secStats['total'] ?? 0) ?></b> 次 · 今日
<b><?= (int) ($secStats['today'] ?? 0) ?></b> 次</span>
</div>
<div class="trend-chart">
<?php foreach ($daily as $d):
$cnt = (int) $d['cnt'];
$h = $maxCnt > 0 ? (int) round($cnt / $maxCnt * 100) : 0;
$ts = (int) strtotime((string) $d['day']);
?>
<div class="trend-col" title="<?= he((string) $d['day']) ?><?= $cnt ?> 次">
<span class="trend-val"><?= $cnt ?></span>
<span class="trend-bar-wrap"><i class="trend-bar" style="--h:<?= $h ?>%"></i></span>
<span class="trend-lbl"><?= he(date('m-d', $ts)) ?></span>
<span class="trend-wk"><?= he($weekMap[(int) date('w', $ts)]) ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
<?php elseif ($heroMode === 'clock'): ?>
<!-- 大字时钟:大号时间 + 日期 + 时段问候(前端实时刷新) -->
<div class="clock-box js-hero-clock">
<div class="ch-greet js-ch-greet">--</div>
<div class="ch-time js-ch-time">--:--:--</div>
<div class="ch-date"><span class="js-ch-date">--------</span><span class="js-ch-week"></span></div>
<div class="ch-extra">
<span class="ch-lunar"><span class="js-ch-lunar">农历 --</span><span class="ch-fest js-ch-fest"></span></span>
<span class="ch-count js-ch-count"><span class="cc-label js-cc-label">距下一个节日</span><span
class="cc-time js-cc-time">--</span></span>
</div>
</div>
<?php elseif ($heroMode === 'notice'): ?>
<!-- 公告滚动:横向无缝滚动当前启用的公告(置顶优先) -->
<div class="ticker-box">
<span class="tk-label">公告</span>
<?php if ($heroNotices): ?>
<div class="tk-viewport js-tk-viewport">
<div class="tk-track js-tk-track">
<?php foreach ($heroNotices as $n):
$nt = trim((string) $n['title']);
$nt = $nt !== '' ? $nt : '公告';
?>
<a class="tk-item" href="notice.php"><?php if ((int) $n['pinned'] === 1): ?><span
class="tk-pin">置顶</span><?php endif; ?><?= he($nt) ?></a>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<span class="tk-empty">暂无更新公告<span class="st-tip">,可在后台「更新公告管理」中添加</span></span>
<?php endif; ?>
</div>
<?php elseif ($heroMode === 'slogan'): ?>
<!-- 打字机标语:逐字打印轮播(复用「滚动信息栏」名言库内容) -->
<div class="slogan-box">
<span class="sg-mark">&gt;</span>
<span class="sg-text js-sg-text"></span><span class="sg-caret"></span>
<span class="js-sg-pool" hidden>
<?php foreach ($quotes as $q): ?><span><?= he($q) ?></span><?php endforeach; ?>
</span>
</div>
<?php endif; ?>
</div>
</header>
<!-- 名言条 -->
<div class="quote-bar">
<div class="wrap quote-inner">
<span class="quote-mark"></span>
<span class="quote-text js-quote" data-current="0" title="点击换一条"><?= he($quoteText) ?></span>
<span class="quote-mark"></span>
<?php if ($heroMode !== 'slogan'): // 打字机标语模式已复用同一信息栏内容,此处不再重复展示 ?>
<!-- 名言条 -->
<div class="quote-bar">
<div class="wrap quote-inner">
<span class="quote-mark"></span>
<span class="quote-text js-quote" data-current="0" title="点击换一条"><?= he($quoteText) ?></span>
<span class="quote-mark"></span>
</div>
<div class="quote-pool js-quote-pool">
<?php foreach ($quotes as $q): ?>
<span><?= he($q) ?></span>
<?php endforeach; ?>
</div>
</div>
<div class="quote-pool js-quote-pool">
<?php foreach ($quotes as $q): ?>
<span><?= he($q) ?></span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- 四大板块区 -->
<section class="board-section">

View File

@ -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 秒自动消失),不会占用页面顶部。
---