feat: 简航 SimpleNav 个人网站导航系统(PHP + SQLite)

- 前台:搜索引擎切换+联想下拉、网格板块(常用/工具)、合集标签页
- 后台:初始化/登录、网站/板块/引擎增删改、上下移排序
- 添加网站自动抓取名称与图标(存储于服务器),支持自定义简称
- 一键全部更新(批量刷新图标+名称,保留自定义简称)
- 搜索联想服务端代理(必应/百度/谷歌/搜狗/360/DuckDuckGo)
- 零依赖:原生 CSS/JS,PDO SQLite
This commit is contained in:
2026-09-28 15:48:35 +08:00
parent 714874f11f
commit 6e83cc3ecc
44 changed files with 2169 additions and 2 deletions
+116
View File
@@ -0,0 +1,116 @@
<?php
/**
* 简航 SimpleNav —— 搜索联想代理
* 浏览器直接调用各搜索引擎的联想接口会有跨域/JSONP 限制,这里由服务端代理并统一输出 JSON。
* 用法:suggest.php?slug=bing&q=关键词
*/
declare(strict_types=1);
require_once __DIR__ . '/functions.php';
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
$slug = isset($_GET['slug']) ? preg_replace('/[^a-z0-9\-_]/i', '', (string)$_GET['slug']) : '';
$q = isset($_GET['q']) ? trim((string)$_GET['q']) : '';
if ($q === '') {
echo json_encode(['q' => '', 'items' => []]);
exit;
}
$items = [];
try {
$st = db()->prepare('SELECT * FROM engines WHERE slug=? AND is_active=1 LIMIT 1');
$st->execute([$slug]);
$engine = $st->fetch();
if ($engine && $engine['suggest_url'] !== '') {
$u = str_replace('{q}', rawurlencode($q), $engine['suggest_url']);
$resp = http_get($u, 100000, 5);
if ($resp !== null) {
$items = parse_suggest($engine['suggest_type'], ensure_utf8($resp['body']), $q);
}
}
} catch (Throwable $e) {
$items = [];
}
// 去重、限长、裁剪
$items = array_values(array_unique(array_filter(array_map('trim', $items))));
$items = array_slice($items, 0, 10);
echo json_encode(['q' => $q, 'items' => $items], JSON_UNESCAPED_UNICODE);
exit;
/** 按引擎类型解析联想结果 */
function parse_suggest(string $type, string $body, string $q): array
{
$body = trim($body);
$out = [];
switch ($type) {
case 'bing_osjson':
case 'google_firefox':
// ["query", ["s1","s2",...]]
$j = json_decode($body, true);
if (is_array($j) && isset($j[1]) && is_array($j[1])) {
$out = $j[1];
}
break;
case 'baidu_sugrec':
// {"q":"x","g":[{"type":"..","q":".."}], ...}
$j = json_decode($body, true);
if (is_array($j)) {
if (!empty($j['g']) && is_array($j['g'])) {
foreach ($j['g'] as $it) { if (isset($it['q'])) $out[] = $it['q']; }
} elseif (!empty($j['s']) && is_array($j['s'])) {
$out = $j['s'];
}
}
break;
case 'so360':
// {"s":[".."]} 或 {"result":[{"word":".."}]}
$j = json_decode($body, true);
if (is_array($j)) {
if (!empty($j['s']) && is_array($j['s'])) { $out = $j['s']; }
elseif (!empty($j['result']) && is_array($j['result'])) {
foreach ($j['result'] as $it) { if (isset($it['word'])) $out[] = $it['word']; }
}
}
break;
case 'sogou_ajaj':
// {"query":"x","items":["a","b"]}(可能是 GBK,已在上面转码)
$j = json_decode($body, true);
if (is_array($j) && !empty($j['items']) && is_array($j['items'])) {
$out = $j['items'];
} else {
// 兜底:正则抓引号字符串
if (preg_match_all('#"([^"]{1,40})"#u', $body, $m)) {
$out = array_slice($m[1], 1); // 跳过 query 自身
}
}
break;
case 'ddg':
// [{"phrase":".."},...]
$j = json_decode($body, true);
if (is_array($j)) {
foreach ($j as $it) {
if (is_array($it) && isset($it['phrase'])) { $out[] = $it['phrase']; }
elseif (is_string($it)) { $out[] = $it; }
}
}
break;
default:
// 通用兜底:尝试 JSON,再退化正则
$j = json_decode($body, true);
if (is_array($j)) {
if (isset($j[1]) && is_array($j[1])) { $out = $j[1]; }
elseif (!empty($j['s']) && is_array($j['s'])) { $out = $j['s']; }
}
}
return $out;
}