NewHome/nav.php

424 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* nav.php —— 导航区模板(科普库 / 红队库 / 蓝队库共用【方案A 正式版】
* URL 参数cat=popular | red | blue
* 结构:品牌条 → 搜索条 → 分区导航(文本居中,下划线滑动指示)
* 四区切换:导航区 / 内部功能区 / 文章区 / 外部工具区
* 说明:库间切换仅在首页板块完成,库内不提供切换入口;板块说明文案常显于首页板块卡。
* 数据来源:
* 导航区 = nav_blocks / nav_items按当前库
* 内部功能区 = func_tools后台“功能区管理”中 board_cats 控制按库展示,空=通用);
* 文章区 = articles后台“文章管理”中 board_cats 勾选分发到库,未分发不显示);
* 外部工具区 = ext_links后台“外部工具”维护cat_keys 控制范围,空=通用)。
*/
$P = '';
require_once __DIR__ . '/includes/layout.php';
$catKey = isset($_GET['cat']) ? (string)$_GET['cat'] : '';
$allowed = ['popular', 'red', 'blue'];
if (!in_array($catKey, $allowed, true)) {
$catKey = 'popular';
}
/** 范围列是否包含当前库:空字符串视为“通用(所有库)” */
function board_cats_has(string $csv, string $key): bool
{
$csv = trim($csv);
if ($csv === '') return true;
return in_array($key, array_map('trim', explode(',', $csv)), true);
}
$pdo = db();
$st = $pdo->prepare('SELECT * FROM categories WHERE key = ? LIMIT 1');
$st->execute([$catKey]);
$cat = $st->fetch();
if (!$cat) {
http_response_code(404);
layout_head('导航页不存在');
echo '<main class="page-main"><div class="wrap"><div class="err-msg">导航分类不存在,请返回首页。</div></div></main>';
layout_theme_fab();
layout_footer();
echo '<script src="assets/js/common.js"></script></body></html>';
exit;
}
// —— 导航区数据 ——
$st = $pdo->prepare('SELECT * FROM nav_blocks WHERE cat_id = ? ORDER BY sort ASC, id ASC');
$st->execute([(int)$cat['id']]);
$blocks = $st->fetchAll();
$itemsByBlock = [];
if ($blocks) {
$ids = array_map(function ($b) { return (int)$b['id']; }, $blocks);
$in = implode(',', array_fill(0, count($ids), '?'));
$st = $pdo->prepare("SELECT * FROM nav_items WHERE block_id IN ($in) ORDER BY sort ASC, id ASC");
$st->execute($ids);
foreach ($st->fetchAll() as $item) {
$itemsByBlock[(int)$item['block_id']][] = $item;
}
}
$totalNav = 0;
foreach ($itemsByBlock as $list) { $totalNav += count($list); }
$navCols = [[], [], []];
$idx = 0;
foreach ($blocks as $b) { $navCols[$idx % 3][] = $b; $idx++; }
// —— 内部功能区数据(通用或包含当前库) ——
$libTools = [];
foreach ($pdo->query('SELECT * FROM func_tools WHERE enabled = 1 ORDER BY sort ASC, id ASC')->fetchAll() as $t) {
if (board_cats_has((string)($t['board_cats'] ?? ''), $catKey)) {
$libTools[] = $t;
}
}
// —— 文章区数据(仅分发到当前库;按分类分组,分类内按最后更新时间倒序) ——
$libArts = [];
foreach ($pdo->query('SELECT id, part, title, summary, updated_at, board_cats FROM articles WHERE enabled = 1 ORDER BY updated_at DESC, id DESC')->fetchAll() as $a) {
if (in_array($catKey, array_map('trim', explode(',', trim((string)($a['board_cats'] ?? '')))), true)) {
$libArts[] = $a;
}
}
// 按分类part分组分类顺序后台拖出的分区顺序优先否则按各组最新更新时间降序
$artGroups = [];
foreach ($libArts as $a) {
$p = trim((string)$a['part']);
if ($p === '') $p = '未分类';
if (!isset($artGroups[$p])) {
$artGroups[$p] = ['ts' => (string)$a['updated_at'], 'items' => []];
}
$artGroups[$p]['items'][] = $a;
if (strcmp((string)$a['updated_at'], $artGroups[$p]['ts']) > 0) {
$artGroups[$p]['ts'] = (string)$a['updated_at'];
}
}
$poIdx = [];
$poCfg = trim((string)setting_get('article_part_order', ''));
if ($poCfg !== '') {
foreach (explode(',', $poCfg) as $po) {
$po = trim($po);
if ($po !== '' && isset($artGroups[$po])) $poIdx[$po] = count($poIdx);
}
}
uksort($artGroups, static function ($a, $b) use ($poIdx, $artGroups) {
$ia = $poIdx[$a] ?? PHP_INT_MAX;
$ib = $poIdx[$b] ?? PHP_INT_MAX;
if ($ia !== $ib) return $ia <=> $ib;
$d = strcmp($artGroups[$b]['ts'], $artGroups[$a]['ts']);
return $d !== 0 ? $d : strcmp($a, $b);
});
$artGroupOrder = [];
foreach ($artGroups as $p => $g) {
$artGroupOrder[] = ['p' => $p, 'items' => $g['items']];
}
// —— 外部工具区数据(通用或包含当前库) ——
$extLinks = [];
foreach ($pdo->query('SELECT * FROM ext_links WHERE enabled = 1 ORDER BY sort ASC, id ASC')->fetchAll() as $e) {
if (board_cats_has((string)($e['cat_keys'] ?? ''), $catKey)) {
$extLinks[] = $e;
}
}
$siteName = setting_get('site_name', '知识导航站');
$engines = search_engines();
$defaultEngine = $engines[0]['key'] ?? 'bing';
$enginesJson = json_encode($engines, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$domainOf = static function (string $u): string {
$h = (string)parse_url($u, PHP_URL_HOST);
return $h !== '' ? preg_replace('/^www\./i', '', $h) : $u;
};
$catName = (string)$cat['name'];
$catIcon = trim((string)$cat['icon']);
layout_head($catName);
?>
<style>
/* ===== 库首页分区导航:文本居中 + 下划线滑动指示 ===== */
.lnx-nav {
position: relative; display: flex; justify-content: center; align-items: center;
gap: 0 38px; margin: 6px 0 16px; border-bottom: 1px solid var(--line); padding: 0 4px;
}
.lnx-tab { position: relative; z-index: 1; border: none; background: none; cursor: pointer;
padding: 12px 6px 13px; font-size: 15px; color: var(--text-3);
transition: color .2s ease; white-space: nowrap; }
.lnx-tab:hover { color: var(--text); }
.lnx-tab.active { color: var(--brand); font-weight: 700; }
.lnx-ink {
position: absolute; left: 0; bottom: -1px; width: 0; height: 3px; border-radius: 3px;
background: var(--brand); box-shadow: 0 0 8px var(--accent-soft);
transition: left .28s cubic-bezier(.45, 0, .25, 1), width .28s cubic-bezier(.45, 0, .25, 1);
}
.lnx-panel { display: none; }
.lnx-panel.active { display: block; }
.lnx-phead { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin: 4px 0 14px; }
.lnx-phead h3 { margin: 0; font-size: 18px; }
.lnx-phead .lnx-go { font-size: 13px; color: var(--text-3); text-decoration: none; }
.lnx-note { font-size: 12.5px; color: var(--text-3); background: var(--card); border: 1px dashed var(--line-2);
border-radius: 10px; padding: 8px 12px; margin-bottom: 14px; }
/* 外部工具区分区标题(按 grp 分组的小节) */
.lnx-eg { display: flex; align-items: center; gap: 10px; margin: 18px 0 8px; font-size: 14.5px; font-weight: 600; color: var(--brand); }
.lnx-eg::after { content: ''; flex: 1; height: 1px; background: var(--line); }
.lnx-exts + .lnx-eg { margin-top: 24px; }
.lnx-empty { color: var(--text-3); font-size: 13.5px; background: var(--card); border: 1px solid var(--line);
border-radius: 12px; padding: 22px 16px; text-align: center; }
.lnx-tools { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; }
.lnx-tool { display: flex; gap: 12px; align-items: flex-start; text-decoration: none; color: var(--text);
background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 14px; box-shadow: var(--shadow-sm);
transition: transform .15s ease, border-color .15s ease; }
.lnx-tool:hover { transform: translateY(-2px); border-color: var(--brand); }
.lnx-tool .lnx-ti { font-size: 24px; }
.lnx-tool b { display: block; font-size: 15px; margin-bottom: 4px; }
.lnx-tool span.tip { margin: 0; line-height: 1.5; display: block; }
.lnx-arts { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.lnx-art { display: block; text-decoration: none; color: var(--text); background: var(--card);
border: 1px solid var(--line); border-radius: 14px; padding: 14px; box-shadow: var(--shadow-sm);
transition: transform .15s ease, border-color .15s ease; }
.lnx-art:hover { transform: translateY(-2px); border-color: var(--brand); }
.lnx-art .lnx-ab { display: inline-block; font-size: 12px; color: var(--brand); background: var(--accent-soft);
border-radius: 6px; padding: 2px 8px; margin-bottom: 8px; }
.lnx-art h4 { margin: 0 0 6px; font-size: 15.5px; }
.lnx-art p { margin: 0 0 8px; font-size: 13px; color: var(--text-3); line-height: 1.6; }
.lnx-art time { font-size: 12px; color: var(--text-3); }
.lnx-exts { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 12px; }
.lnx-ext { display: block; text-decoration: none; color: var(--text); background: var(--card);
border: 1px solid var(--line); border-radius: 14px; padding: 12px 14px; box-shadow: var(--shadow-sm);
transition: transform .15s ease, border-color .15s ease; }
.lnx-ext:hover { transform: translateY(-2px); border-color: var(--brand); }
.lnx-ext .lnx-eh { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.lnx-ext .lnx-en { font-weight: 600; font-size: 14.5px; }
.lnx-ext .lnx-ed { font-size: 12px; color: var(--text-3); word-break: break-all; }
.lnx-ext p { margin: 6px 0 0; font-size: 12.5px; color: var(--text-3); line-height: 1.5; }
@media (max-width: 760px) {
.lnx-nav { gap: 0 10px; }
.lnx-tab { flex: 1 1 0; min-width: 0; font-size: 14px; padding: 10px 2px 12px; text-align: center; }
.nav-cols { grid-template-columns: 1fr; }
}
</style>
<main>
<!-- 顶部留白 + 品牌条(上半区不变) -->
<div class="nav-top">
<div class="wrap" style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px;flex-wrap:wrap;">
<a class="brand brand-sm" href="index.php" title="返回首页">
<?php layout_logo_img('logo', $siteName); ?>
<span class="site-name"><?= he($siteName) ?></span>
</a>
<span style="font-size:13px;color:var(--text-3);padding-top:10px;"><?= he($catIcon) ?> <?= he($catName) ?></span>
</div>
</div>
<!-- 搜索条(保持不变) -->
<div class="search-zone">
<div class="wrap">
<div class="search-box js-search" data-engine="<?= he($defaultEngine) ?>" data-engines="<?= he($enginesJson) ?>">
<button type="button" class="engine-btn js-engine-btn" title="切换搜索引擎">
<span class="js-engine-name"><?= he($engines[0]['icon'] ?? 'S') ?></span>
<span class="arrow">▼</span>
<ul class="engine-menu js-engine-menu"></ul>
</button>
<div class="search-field">
<input type="text" class="js-search-input" placeholder="输入关键词,回车或点击搜索…" autocomplete="off">
<button type="button" class="search-clear js-search-clear" title="清除">✕</button>
</div>
<button type="button" class="search-go js-search-go">搜索</button>
</div>
<p class="search-title st-tip">可切换搜索引擎进行搜索(<?= count($engines) ?> 个可用,后台可维护)</p>
</div>
</div>
<div class="wrap">
<!-- 主导航:分区文本居中,点击下滑线滑动指示;库切换请从首页板块入口进入 -->
<nav class="lnx-nav" id="lnxNav">
<button type="button" class="lnx-tab active" data-t="nav">导航区</button>
<button type="button" class="lnx-tab" data-t="tools">功能区</button>
<button type="button" class="lnx-tab" data-t="art">文章区</button>
<button type="button" class="lnx-tab" data-t="ext">工具区</button>
<span class="lnx-ink" id="lnxInk" aria-hidden="true"></span>
</nav>
<!-- ============ 导航区 ============ -->
<section class="lnx-panel active" data-panel="nav">
<div class="lnx-phead">
<h3>导航区 · <?= he($catName) ?></h3>
<span class="lnx-go">共 <?= count($blocks) ?> 个导航块 / <?= $totalNav ?> 个链接 · 悬停按钮可看备注</span>
</div>
<?php if (!$blocks): ?>
<div class="lnx-empty">该库还没有导航内容<span class="st-tip">,可在后台「导航内容管理」中添加</span>。</div>
<?php else: ?>
<div class="nav-cols" style="margin:0">
<?php foreach ($navCols as $colBlocks): ?>
<div class="nav-col">
<?php foreach ($colBlocks as $b):
$items = $itemsByBlock[(int)$b['id']] ?? [];
$show = array_slice($items, 0, 15);
?>
<div class="nav-block">
<div class="nav-block-title"><?= he((string)$b['title']) ?></div>
<div class="nav-block-grid">
<?php for ($i = 0; $i < 15; $i++): ?>
<?php if (isset($show[$i])):
$it = $show[$i];
$label = (string)$it['label'];
$url = trim((string)$it['url']);
$note = trim((string)$it['note']);
if ($url !== ''): ?>
<a class="nav-btn" href="<?= he($url) ?>" target="_blank" rel="noopener noreferrer"<?php if ($note !== ''): ?> data-note="<?= he($note) ?>"<?php endif; ?>><?= he($label ?: $url) ?></a>
<?php else: ?>
<span class="nav-btn is-off" title="链接未配置"><?= he($label) ?></span>
<?php endif; ?>
<?php else: ?>
<span class="nav-btn is-empty"></span>
<?php endif; ?>
<?php endfor; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<!-- ============ 内部功能区 ============ -->
<section class="lnx-panel" data-panel="tools">
<div class="lnx-phead">
<h3>内部功能区 · <?= he($catName) ?>常用工具</h3>
<a class="lnx-go" href="func/index.php">前往完整功能区 </a>
</div>
<div class="lnx-note st-tip">由后台「功能区管理」为每个工具勾选“展示库”,未勾选默认出现在所有库。点击工具进入详情页(返回按钮已适配为返回上一页)。</div>
<?php if (!$libTools): ?>
<div class="lnx-empty">尚未把工具分配到本库<span class="st-tip">,可到后台「功能区管理」开启</span>。</div>
<?php else: ?>
<div class="lnx-tools">
<?php foreach ($libTools as $tk):
$isExt = (int)$tk['is_external'] === 1;
$url = trim((string)$tk['url']);
$href = $isExt ? $url : 'func/' . $url . '?from=nav';
$tIcon = trim((string)($tk['icon'] ?? ''));
$tIcon = $tIcon !== '' ? $tIcon : '🧩';
?>
<a class="lnx-tool" href="<?= he($href) ?>"<?php if ($isExt): ?> target="_blank" rel="noopener noreferrer"<?php endif; ?>>
<span class="lnx-ti"><?= site_icon($tIcon, '') ?></span>
<span>
<b><?= he((string)$tk['name']) ?></b>
<span class="tip"><?= he((string)$tk['description']) ?></span>
</span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<!-- ============ 文章区 ============ -->
<section class="lnx-panel" data-panel="art">
<div class="lnx-phead">
<h3>文章区 · <?= he($catName) ?>相关</h3>
<a class="lnx-go" href="article/index.php">前往完整文章库 </a>
</div>
<div class="lnx-note st-tip">由后台「文章管理」为文章勾选分发到的库;点击进入阅读页(返回按钮已适配为返回上一页)。</div>
<?php if (!$artGroupOrder): ?>
<div class="lnx-empty">还没有文章分发到本库<span class="st-tip">,可到后台「文章管理」编辑文章并勾选“展示到库首页”</span>。</div>
<?php else: ?>
<?php foreach ($artGroupOrder as $ag): ?>
<h4 class="lnx-eg"><?= he($ag['p']) ?></h4>
<div class="lnx-arts">
<?php foreach ($ag['items'] as $ak): ?>
<a class="lnx-art" href="article/index.php?id=<?= (int)$ak['id'] ?>&from=nav&cat=<?= he($catKey) ?>">
<h4><?= he((string)$ak['title']) ?></h4>
<p><?= he(trim((string)$ak['summary']) !== '' ? (string)$ak['summary'] : '(无摘要)') ?></p>
<time>更新于 <?= he((string)$ak['updated_at']) ?></time>
</a>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</section>
<!-- ============ 外部工具区 ============ -->
<section class="lnx-panel" data-panel="ext">
<div class="lnx-phead">
<h3>外部工具区 · 开源工具/好用站点/点开即用</h3>
<span class="lnx-go st-tip">快速链接 · 名称 · 简介 · 新标签页打开 · 后台可增删</span>
</div>
<div class="lnx-note st-tip">由后台「外部工具」维护(可限制展示在特定库,留空则所有库通用)。</div>
<?php if (!$extLinks): ?>
<div class="lnx-empty">暂无外部工具<span class="st-tip">,可到后台「外部工具」添加</span>。</div>
<?php else:
$extGroups = [];
foreach ($extLinks as $ek) {
$eg = trim((string)($ek['grp'] ?? ''));
$extGroups[$eg === '' ? "\x00" : $eg][] = $ek;
}
?>
<?php foreach ($extGroups as $eg => $gItems): ?>
<?php if ($eg !== "\x00"): ?><h4 class="lnx-eg"><?= he($eg) ?></h4><?php endif; ?>
<div class="lnx-exts">
<?php foreach ($gItems as $ek):
$eUrl = trim((string)$ek['url']);
?>
<a class="lnx-ext" href="<?= he($eUrl) ?>" target="_blank" rel="noopener noreferrer">
<span class="lnx-eh">
<span class="lnx-en"><?= he((string)$ek['name']) ?></span>
<span class="lnx-ed"><?= he($domainOf($eUrl)) ?></span>
</span>
<p><?= he((string)($ek['description'] ?? '')) ?></p>
</a>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</section>
</div>
</main>
<?php layout_theme_fab(); ?>
<?php layout_footer(); ?>
<script src="assets/js/common.js"></script>
<script>
(function () {
'use strict';
// 下划线滑动指示导航:切换分区时激活项高亮,指示条平滑滑动到该项下方
var nav = document.getElementById('lnxNav');
var ink = document.getElementById('lnxInk');
var tabs = nav ? Array.prototype.slice.call(nav.querySelectorAll('.lnx-tab')) : [];
var panels = document.querySelectorAll('.lnx-panel');
if (!nav || !ink || !tabs.length) return;
function moveInk() {
var cur = nav.querySelector('.lnx-tab.active');
if (!cur) { ink.style.left = '0px'; ink.style.width = '0px'; return; }
ink.style.left = cur.offsetLeft + 'px';
ink.style.width = cur.offsetWidth + 'px';
}
function show(btn, fromHash) {
var t = btn.getAttribute('data-t');
tabs.forEach(function (x) { x.classList.toggle('active', x === btn); });
panels.forEach(function (p) { p.classList.toggle('active', p.getAttribute('data-panel') === t); });
moveInk();
if (!fromHash) window.scrollTo({ top: 0, behavior: 'smooth' });
// 把当前分区写入 URL #hashreplaceState 不产生额外历史记录),
// 这样从本页跳去文章/工具再“返回上一页”或刷新时,仍能回到原来的分区。
if (location.hash !== '#' + t) {
history.replaceState(null, '', '#' + t);
}
}
tabs.forEach(function (btn) {
btn.addEventListener('click', function () { show(btn); });
});
window.addEventListener('resize', function () { moveInk(); });
// 初始恢复URL 中带 #分区(返回上一页 / 手动分享)时,激活对应分区
var wantTab = (location.hash || '').replace(/^#/, '');
var foundTab = null;
tabs.forEach(function (b) { if (!foundTab && b.getAttribute('data-t') === wantTab) foundTab = b; });
if (foundTab) {
show(foundTab, true);
} else {
moveInk();
// 无分区 hash 时清掉可能遗留的无效 hash如 #footer保持地址干净
if (location.hash !== '') {
history.replaceState(null, '', location.pathname + location.search);
}
}
})();
</script>
</body>
</html>