Files
laoyang 2599eba440 页脚加「开源」「反馈」链接
用户要求:首页下方除了「管理」,再放开源地址与反馈链接。

- config.php 新增 REPO_URL / FEEDBACK_URL 两个常量,集中管理外链,
  以后换仓库或换反馈系统只改一处,不用翻模板
- index.php 页脚顺序:管理 · 开源 · 反馈 · 站点名
- 外链带 target=_blank + rel=noopener(新标签打开,且不给 window.opener)

反馈地址用 https://feedlite.yangyq.net/index.php?sw=simplenav
(sw= 是 FeedLite 区分来源的约定;原来的 ?sw=SimpleNav 写法是错的)

测试:_sn_footer_e2e.py 15 项(地址精确匹配 / target / rel / 顺序 / 常量未定义会露馅)
反向验证 8 变体全精确命中,含「反馈地址写成旧值」——正是本次修掉的那个错。
2026-11-04 17:49:15 +08:00

225 lines
9.7 KiB
PHP
Raw Permalink 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
/**
* 简航 SimpleNav —— 前台首页
* 顶部:品牌 + 搜索引擎切换 + 搜索框(带联想下拉)
* 中部:常用 / 工具等网格板块
* 下部:合集(标签页折叠 或 全部平铺,后台可切换)
*/
declare(strict_types=1);
require_once __DIR__ . '/functions.php';
send_no_cache();
/**
* 未安装就先去装:库里还没有任何一个管理员账号时,前台没有意义
* (打开只会看到一个空导航,既没有提示也不知道去哪)。
*
* ⚠️ 只在 install.php 真的存在时才跳。用户装完删掉 install.php 后,
* 这个分支自动失效,不会跳到 404。
* ⚠️ 必须放在 db() 之前判断的等价位置:app_installed() 内部会调 db(),
* 但那边有 try/catch 兜住建库失败的情况,不会把异常抛到前台。
*/
if (!app_installed() && is_file(__DIR__ . '/install.php')) {
header('Location: ' . url('install.php'));
exit;
}
$pdo = db();
// 前台数据
$gridGroups = $pdo->query("SELECT * FROM groups WHERE is_visible=1 AND display='grid' ORDER BY sort_order, id")->fetchAll();
$tabGroups = $pdo->query("SELECT * FROM groups WHERE is_visible=1 AND display='tabs' ORDER BY sort_order, id")->fetchAll();
$engines = $pdo->query("SELECT * FROM engines WHERE is_active=1 ORDER BY sort_order, id")->fetchAll();
// 合集区的显示方式(后台「设置 → 合集显示方式」):
// tabs = 标签页折叠(默认)—— 一次只平铺当前这一个合集
// flat = 全部平铺 —— 每个合集各占一块,一眼看全、不用点
$collLayout = setting('collections_layout', 'tabs') === 'flat' ? 'flat' : 'tabs';
/** 站点链接与顶部搜索框的打开方式(后台「设置 → 打开方式」)
* 返回 `_blank`(新标签页,默认,等同改造前的旧行为)或 `_self`(当前页面)。
* ⚠️ 搜索框与站点格必须共用这一个来源,不要在任何一处写死 target。 */
function open_target(): string
{
return setting('open_blank', '1') === '1' ? '_blank' : '_self';
}
$openTarget = open_target();
$openMode = $openTarget === '_blank' ? 'blank' : 'self';
// 联想词的选中按键(后台「设置 → 联想词按键」):
// both = ↓/↑ 与 , . 都认(默认) / arrows = 只认 ↓/↑ / punct = 只认 , 和 .
// 白名单回退:任何非法值都回到 both(默认值,与 db.php 一致),别让手改库/被篡改的表单把交互搞坏。
$suggestKeys = setting('suggest_keys', 'both');
if (!in_array($suggestKeys, ['arrows', 'punct', 'both'], true)) { $suggestKeys = 'both'; }
// 默认引擎
$defSlug = setting('default_engine', 'bing');
$default = null;
foreach ($engines as $en) {
if ($en['slug'] === $defSlug) { $default = $en; break; }
}
if ($default === null) { $default = $engines[0] ?? null; }
$primary = setting('primary_color', '#2F6BF7');
// 副标题(后台「设置 → 副标题」)。
// ⚠️ 允许为空:用户清空后就**不显示**副标题,不要回退成默认文案 ——
// 这正是 bug 的现场(原先空串被当成「未设置」,删了照样显示)。
// 空串原样取出(默认值取 ''),渲染处再按需判断是否输出。
$subtitle = setting('subtitle', '');
function sites_of(int $gid): array
{
$st = db()->prepare("SELECT * FROM sites WHERE group_id=? AND is_visible=1 ORDER BY sort_order, id");
$st->execute([$gid]);
return $st->fetchAll();
}
function site_label(array $s): string
{
return $s['short_name'] !== '' ? $s['short_name'] : ($s['name'] !== '' ? $s['name'] : domain_of($s['url']));
}
/** 渲染一个网站格(网格板块与合集共用) */
function site_tile(array $s): void
{
$label = site_label($s);
$src = icon_src($s['icon']);
// 打开方式与顶部搜索框同源(见 open_target()):新标签页 or 当前页面
$tgt = open_target();
?>
<li class="site">
<a href="<?= e($s['url']) ?>" target="<?= e($tgt) ?>"<?= $tgt === '_blank' ? ' rel="noopener"' : '' ?> title="<?= e($s['name'] ?: $label) ?>">
<?php if ($src): ?>
<img class="icon" src="<?= e($src) ?>" alt="" loading="lazy">
<?php else: ?>
<span class="ico-letter" style="background:<?= e(letter_color($label)) ?>"><?= e(mb_substr($label, 0, 1)) ?></span>
<?php endif; ?>
<span class="name"><?= e($label) ?></span>
</a>
</li>
<?php
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= e(site_name()) ?><?= $subtitle !== '' ? ' · ' . e($subtitle) : '' ?></title>
<meta name="description" content="<?= e(site_name()) ?> —— 个人网站导航">
<meta name="theme-color" content="<?= e($primary) ?>">
<!-- 站点图标(favicon / apple-touch / PWA manifest):有自定义就用自定义,
没有就完全退回内置资源。三处页面共用 site_icon_head(),避免半生效。 -->
<?= site_icon_head() ?>
<link rel="stylesheet" href="<?= e(asset('assets/css/style.css')) ?>">
<?php if ($primary !== '#2F6BF7'): ?>
<style>:root { --primary: <?= e($primary) ?>; }</style>
<?php endif; ?>
</head>
<body data-open="<?= e($openMode) ?>">
<div class="container">
<!-- 顶部品牌区 -->
<header class="hero" data-site-icon="<?= e(site_icon_mode()) ?>">
<img class="logo" src="<?= e(site_icon_url('logo', 512) ?: asset('assets/img/logo.svg')) ?>" alt="<?= e(site_name()) ?>">
<h1><?= e(site_name()) ?></h1>
<?php if ($subtitle !== ''): ?>
<p><?= e($subtitle) ?></p>
<?php endif; ?>
</header>
<!-- 顶部搜索 -->
<div id="search" class="s-search">
<div class="search-type" id="engine-tabs">
<?php foreach ($engines as $en): ?>
<label class="s-engine<?= $default && $en['slug'] === $default['slug'] ? ' active' : '' ?>"
data-search="<?= e($en['search_url']) ?>"
data-param="<?= e($en['param']) ?>"
data-slug="<?= e($en['slug']) ?>"
data-placeholder="<?= e($en['name']) ?>搜索"
style="--ec:<?= e($en['color']) ?>">
<span><?= e($en['name']) ?></span>
</label>
<?php endforeach; ?>
<?php if (!$engines): ?>
<label class="s-engine active" data-search="https://cn.bing.com/search" data-param="q" data-slug="" data-placeholder="搜索"><span>搜索</span></label>
<?php endif; ?>
</div>
<form id="search-form" action="<?= e($default['search_url'] ?? 'https://cn.bing.com/search') ?>" method="get" target="<?= e($openTarget) ?>"<?= $openTarget === '_blank' ? ' rel="noopener"' : '' ?>
data-slug="<?= e($default['slug'] ?? '') ?>" data-keys="<?= e($suggestKeys) ?>">
<input type="text" id="search-text" name="<?= e($default['param'] ?? 'q') ?>"
placeholder="<?= e(($default['name'] ?? '搜索') . '搜索') ?>" autocomplete="off">
<button class="submit" type="submit" aria-label="搜索">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true">
<circle cx="11" cy="11" r="7"></circle>
<path d="M20 20l-3.6-3.6"></path>
</svg>
</button>
<ul id="word"></ul>
</form>
</div>
<!-- 网格板块:常用 / 工具 ... -->
<?php foreach ($gridGroups as $g): $items = sites_of((int)$g['id']); if (!$items) continue; ?>
<ul class="mylist">
<li class="title"><?= e($g['name']) ?></li>
<?php foreach ($items as $s) { site_tile($s); } ?>
</ul>
<?php endforeach; ?>
<!-- 合集:标签页折叠 或 全部平铺(后台「设置 → 合集显示方式」切换) -->
<?php
// 先收一遍「有内容的合集」,空合集不占位(标签页模式下也不会多出一个空标签)
$collGroups = [];
foreach ($tabGroups as $g) {
$items = sites_of((int)$g['id']);
if ($items) { $collGroups[] = ['g' => $g, 'items' => $items]; }
}
?>
<?php if ($collGroups): ?>
<div class="collections<?= $collLayout === 'flat' ? ' flat' : '' ?>" data-layout="<?= e($collLayout) ?>">
<div class="collections-title">合集</div>
<?php if ($collLayout === 'flat'): ?>
<?php foreach ($collGroups as $c): ?>
<ul class="mylist">
<li class="title"><?= e($c['g']['name']) ?></li>
<?php foreach ($c['items'] as $s) { site_tile($s); } ?>
</ul>
<?php endforeach; ?>
<?php else: ?>
<div class="tabs" id="coll-tabs" role="tablist">
<?php foreach ($collGroups as $i => $c): ?>
<button type="button" class="tab<?= $i === 0 ? ' active' : '' ?>" role="tab"
aria-selected="<?= $i === 0 ? 'true' : 'false' ?>"
data-target="coll-<?= (int)$c['g']['id'] ?>"><?= e($c['g']['name']) ?></button>
<?php endforeach; ?>
</div>
<div class="tab-panels">
<?php foreach ($collGroups as $i => $c): ?>
<ul class="mylist tab-panel<?= $i === 0 ? ' active' : '' ?>" id="coll-<?= (int)$c['g']['id'] ?>" role="tabpanel">
<?php foreach ($c['items'] as $s) { site_tile($s); } ?>
</ul>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<footer class="foot">
<a href="<?= e(url('admin/')) ?>">管理</a>
<span>·</span>
<a href="<?= e(REPO_URL) ?>" target="_blank" rel="noopener">开源</a>
<span>·</span>
<a href="<?= e(FEEDBACK_URL) ?>" target="_blank" rel="noopener">反馈</a>
<span>·</span>
<span><?= e(site_name()) ?></span>
</footer>
</div>
<script>window.SIMPLENAV = { suggest: "<?= e(url('suggest.php')) ?>", suggestKeys: "<?= e($suggestKeys) ?>" };</script>
<script src="<?= e(asset('assets/js/app.js')) ?>"></script>
</body>
</html>