650 lines
21 KiB
PHP
650 lines
21 KiB
PHP
<?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 #hash(replaceState 不产生额外历史记录),
|
||
// 这样从本页跳去文章/工具再“返回上一页”或刷新时,仍能回到原来的分区。
|
||
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>
|