优化网站
This commit is contained in:
parent
9bb6e6a41e
commit
0e7265481f
11
CHANGELOG.md
11
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
|
||||
|
||||
### 新增
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
|
||||
- **多库分区**:科普库 / 红队库 / 蓝队库三大板块 + 工具库 + 文章库
|
||||
- 每个库内分四区:**导航区 · 内部功能区 · 文章区 · 外部工具区**
|
||||
- 首页顶部组件三种模式(天气 / 访客信息 / 空白精简)可后台切换
|
||||
- 快捷站点磁贴,悬停显示备注
|
||||
- 首页顶部组件可在后台切换(天气 / 访客信息 / 空白精简 / 渗透测试 / 安全巡检 / 安全态势 / 访问趋势 / 大字时钟 / 公告滚动 / 打字机标语,共 10 种)
|
||||
- 快捷站点磁贴:圆形图标 + 名称 + 备注一览(悬停亦可查看完整备注)
|
||||
|
||||
### 功能区(纯前端,隐私优先)
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
|
||||
@ -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">
|
||||
<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>
|
||||
|
||||
@ -28,13 +28,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
} else {
|
||||
// 轻量防爆破延时(命中黑名单的 IP 在访问本页前即被 access_boot 拦截)
|
||||
usleep(500000);
|
||||
$risk = risk_login_fail($curIp);
|
||||
if ($risk[1]) {
|
||||
// 累计登录失败次数(达阈值会自动封禁);此处统一返回同一提示,避免暴露账号状态
|
||||
risk_login_fail($curIp);
|
||||
$err = '用户名或密码错误。';
|
||||
// $err = '登录失败次数过多(本次累计 ' . $risk[0] . ' 次,达到阈值 ' . $risk[2] . ' 次),当前 IP 已被风控自动封禁,全站(含后台登录)访问均被拒绝。请稍后从未被封禁的网络进入后台「风控管理 → IP 黑名单」移除该规则解除,或联系站长处理。';
|
||||
} else {
|
||||
$err = '用户名或密码错误。';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
<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>
|
||||
|
||||
@ -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">
|
||||
<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>
|
||||
|
||||
@ -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="关键词:地址 / 请求方式">
|
||||
|
||||
@ -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]);
|
||||
@ -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
BIN
assets/img/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@ -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;
|
||||
}
|
||||
// 先淡出、换字后再淡入,避免文字瞬间跳变
|
||||
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();
|
||||
|
||||
@ -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";
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* auth.php —— 后台登录会话工具
|
||||
* 密码存储算法(按需求):md5(base64(明文))
|
||||
* 密码存储算法:PHP password_hash 强散列(bcrypt/argon2);兼容旧库 md5(base64(明文)) 并自动升级。
|
||||
*/
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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
312
index.html
Normal 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 // SECURE CHANNEL</div>
|
||||
<div class="hud tr">AUTH: PENDING // 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>
|
||||
172
index.php
172
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,10 +117,165 @@ 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">></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>
|
||||
|
||||
<?php if ($heroMode !== 'slogan'): // 打字机标语模式已复用同一信息栏内容,此处不再重复展示 ?>
|
||||
<!-- 名言条 -->
|
||||
<div class="quote-bar">
|
||||
<div class="wrap quote-inner">
|
||||
@ -122,6 +289,7 @@ layout_head('');
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- 四大板块区 -->
|
||||
<section class="board-section">
|
||||
|
||||
6
操作文档.md
6
操作文档.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 秒自动消失),不会占用页面顶部。
|
||||
|
||||
---
|
||||
|
||||
Loading…
Reference in New Issue
Block a user