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