Files
SimpleNav/db.php
T
laoyang 13b12a3d67 后台新增「联想词按键」设置:可用 , 和 . 代替 ↑/↓ 选联想词
输入法手感 —— 习惯中文输入法的话,手不用离开主键区就能在联想下拉里选词。

- 设置项 suggest_keys:arrows(默认,等同改造前)/ punct(, 和 .)/ both。
  后台为三选一下拉,保存走白名单回退(非白名单一律 arrows),默认值 = 旧行为。
- app.js 认**物理键**(e.code 的 Comma / Period)而不是字符 —— 中文输入法下按逗号
  打出来的是全角「,」,e.key 会变成「,」,但物理键不变;「跟输入法一样」就该这么认。
- ⚠️ 逗号和句号同时是搜索框的合法输入字符(如「python, pandas」)—— 所以只在
  **联想下拉打开时**才把它们当选词键;没有下拉时放行,照常输入。这条边界写进了
  后台说明(想输入逗号本身先按 Esc 关掉下拉)。
- 「,」= 上一条(同 ↑)、「.」= 下一条(同 ↓),首尾循环、与方向键行为完全一致。
- 前台输出两处同源标记:#search-form 的 data-keys(供断言/排查)与
  window.SIMPLENAV.suggestKeys(JS 实际读它决定用哪组键)。

验证:
- jsdom 交互测试 54/54 —— arrows / punct / both 三种模式各跑一遍;覆盖全角「,」「。」
  (中文输入法场景)、「没有下拉时 ,/. 不被拦截」(关键回归)、
  以及上一轮的核心回归「直接回车用原文」在三种模式下都成立。
- 反向验证两个变体:把回车改回旧行为 → 「直接回车不改动输入框」精确失败(27 项);
  把「没下拉放行」改坏 → 恰好只失败「没有下拉时按 ./, 不被拦截」那两条(2 项),
  其余全过 —— 两条核心断言各自有效、定位精准。
- e2e 128/128(新增联想按键场景:保存 → data-keys / suggestKeys 同步、
  非法值回退 arrows、保存不动其它设置)。
- 线上部署 0 FAIL(77 条静态断言、38 文件哈希逐字节一致);线上几何 25/25;
  直连线上核验 data-keys / suggestKeys / app.js 内容均已就位。
2026-09-28 20:25:33 +08:00

251 lines
11 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
/**
* 简航 SimpleNav —— 数据库(PDO SQLite)
* 首次访问自动建表并写入一份通用的默认数据(示例链接,可在后台随意增删)。
*/
declare(strict_types=1);
require_once __DIR__ . '/config.php';
function db(): PDO
{
static $pdo = null;
if ($pdo === null) {
if (!is_dir(DATA_PATH)) {
@mkdir(DATA_PATH, 0755, true);
}
$pdo = new PDO('sqlite:' . DB_PATH);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$pdo->exec('PRAGMA foreign_keys = ON');
db_init($pdo);
}
return $pdo;
}
function db_init(PDO $pdo): void
{
$pdo->exec(<<<'SQL'
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT DEFAULT '',
display TEXT NOT NULL DEFAULT 'grid', -- grid=普通网格板块 tabs=合集标签页
sort_order INTEGER NOT NULL DEFAULT 0,
is_visible INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS sites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL,
url TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '', -- 自动抓取的网站名称
short_name TEXT NOT NULL DEFAULT '', -- 用户自定义简称(页面展示用)
icon TEXT NOT NULL DEFAULT '', -- 本地图标文件名
icon_custom INTEGER NOT NULL DEFAULT 0, -- 1=用户自定义图标("全部更新"不再覆盖)
sort_order INTEGER NOT NULL DEFAULT 0,
is_visible INTEGER NOT NULL DEFAULT 1,
created_at TEXT DEFAULT '',
updated_at TEXT DEFAULT '',
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS engines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
slug TEXT NOT NULL,
search_url TEXT NOT NULL,
param TEXT NOT NULL DEFAULT 'q',
suggest_url TEXT NOT NULL DEFAULT '',
suggest_type TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT '#2F6BF7',
is_active INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS settings (
k TEXT PRIMARY KEY,
v TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT ''
);
SQL);
// 老库升级:为已存在的 sites 表补 icon_custom 列
$cols = [];
foreach ($pdo->query('PRAGMA table_info(sites)') as $c) {
$cols[(string)$c['name']] = true;
}
if (!isset($cols['icon_custom'])) {
$pdo->exec('ALTER TABLE sites ADD COLUMN icon_custom INTEGER NOT NULL DEFAULT 0');
}
// 老库升级:清掉「搜狗联想」这个已经彻底失效的默认值
// (2026-09-28 实测:suggnew/ajajjson 返回 200 但 0 字节,联想域名 w.sugg.sogou.com 已不解析)
// 只匹配默认种子里的那个 URL —— 用户自己改过的地址绝不动。
$pdo->prepare('UPDATE engines SET suggest_url=?, suggest_type=? WHERE slug=? AND suggest_url=?')
->execute(['', '', 'sogou', 'https://www.sogou.com/suggnew/ajajjson?type=web&key={q}']);
// 是否为空库(用于判断是否写入默认数据)
$isEmpty = (int)$pdo->query('SELECT COUNT(*) FROM groups')->fetchColumn() === 0;
seed_settings($pdo);
seed_engines($pdo);
if ($isEmpty) {
seed_groups_and_sites($pdo);
}
}
function seed_settings(PDO $pdo): void
{
$defaults = [
'site_name' => '简航',
'subtitle' => '个人网站导航',
'default_engine' => 'bing',
'primary_color' => '#2F6BF7',
// '1' = 顶部搜索框与所有站点/合集链接都在**新标签页**打开;'0' = 都在**当前页面**打开。
// 默认 '1' 与引入该设置前的旧行为一致;老库若已有这一行不会被覆盖(INSERT OR IGNORE)。
'open_blank' => '1',
// 'arrows' = ↓/↑ 选词(默认,与引入该设置前的旧行为一致);
// 'punct' = 用 , 和 . 选词(输入法手感);'both' = 两组都认。
'suggest_keys' => 'arrows',
];
$stmt = $pdo->prepare('INSERT OR IGNORE INTO settings (k, v) VALUES (?, ?)');
foreach ($defaults as $k => $v) {
$stmt->execute([$k, $v]);
}
}
function seed_engines(PDO $pdo): void
{
if ((int)$pdo->query('SELECT COUNT(*) FROM engines')->fetchColumn() > 0) {
return;
}
// name, slug, search_url, param, suggest_url, suggest_type, color, active, sort
// ⚠️ suggest_url 留空的含义:该引擎自己没有可用的联想接口。
// 反正默认「联想词来源 = 百度」,留空只会让 auto 模式下少一次无谓的超时等待。
// 搜狗(suggnew/ajajjson 已失效)、谷歌 / DuckDuckGo(国内不可达)都不填。
$rows = [
['必应', 'bing', 'https://cn.bing.com/search', 'q', 'https://api.bing.com/osjson.aspx?query={q}', 'bing_osjson', '#60A504', 1, 1],
['百度', 'baidu', 'https://www.baidu.com/s', 'wd', 'https://www.baidu.com/sugrec?prod=pc&wd={q}', 'baidu_sugrec', '#2100E0', 1, 2],
['谷歌', 'google', 'https://www.google.com/search', 'q', '', '', '#3B83FA', 0, 3],
['搜狗', 'sogou', 'https://www.sogou.com/web', 'query', '', '', '#FE6B14', 0, 4],
['360', 'so360', 'https://www.so.com/s', 'q', 'https://sug.so.360.cn/suggest?encodein=utf-8&encodeout=utf-8&format=json&word={q}', 'so360', '#1BB04A', 0, 5],
['DuckGo','ddg', 'https://duckduckgo.com/', 'q', '', '', '#DE5833', 0, 6],
];
$stmt = $pdo->prepare('INSERT INTO engines (name,slug,search_url,param,suggest_url,suggest_type,color,is_active,sort_order) VALUES (?,?,?,?,?,?,?,?,?)');
foreach ($rows as $r) {
$stmt->execute($r);
}
}
function seed_groups_and_sites(PDO $pdo): void
{
// 板块定义: [名称, 类型, 排序, [ [名称, 网址], ... ] ]
$data = [
['常用', 'grid', 1, [
['GitHub', 'https://github.com/'],
['阿里云', 'https://www.aliyun.com/'],
['知乎', 'https://www.zhihu.com/'],
['天翼云盘', 'https://cloud.189.cn/'],
['快科技', 'http://www.mydrivers.com/'],
['豆瓣', 'https://www.douban.com/'],
['少数派', 'https://sspai.com/'],
['红围脖', 'https://hongbowei.com/'],
]],
['工具', 'grid', 2, [
['宝塔面板', 'https://www.bt.cn/'],
['阿里云盘', 'https://www.alipan.com/'],
['网易云课堂', 'https://study.163.com/'],
['学堂在线', 'https://www.xuetangx.com/'],
['CSDN', 'https://www.csdn.net/'],
['稀土掘金', 'https://juejin.cn/'],
['淘宝', 'https://www.taobao.com/'],
['京东', 'https://www.jd.com/'],
['腾讯文档', 'https://docs.qq.com/'],
['石墨文档', 'https://shimo.im/'],
['飞书', 'https://www.feishu.cn/'],
['Gitee', 'https://gitee.com/'],
['百度网盘', 'https://pan.baidu.com/'],
['豆瓣电影', 'https://movie.douban.com/'],
['花瓣网', 'https://huaban.com/'],
['蓝奏云', 'https://www.lanzou.com/'],
['bilibili', 'https://www.bilibili.com/'],
['优酷', 'https://www.youku.com/'],
['爱奇艺', 'https://www.iqiyi.com/'],
['腾讯视频', 'https://v.qq.com/'],
['芒果TV', 'https://www.mgtv.com/'],
['draw.io', 'https://app.diagrams.net/'],
['稿定PS', 'https://ps.gaoding.com/'],
['Bitwarden', 'https://bitwarden.com/'],
['PDF工具', 'https://pdf.wps.cn/'],
['微信读书', 'https://weread.qq.com/'],
['高德地图', 'https://gaode.com/'],
['百度地图', 'https://map.baidu.com/'],
['建设银行', 'http://ccb.com/cn/home/index.html'],
['支付宝', 'https://www.alipay.com/'],
['MOOC', 'https://www.icourse163.org/'],
]],
['常用软件', 'tabs', 10, [
['WPS', 'https://www.wps.cn/'],
['Chrome', 'https://www.google.cn/chrome/index.html'],
['VS Code', 'https://code.visualstudio.com/'],
['Draw.IO', 'https://github.com/jgraph/drawio-desktop/releases'],
['火绒', 'https://www.huorong.cn/'],
['7-Zip', 'https://sparanoid.com/lab/7z/'],
['Tabby', 'https://github.com/Eugeny/tabby/'],
['GitAhead', 'https://gitahead.github.io/gitahead.com/'],
['Everything', 'https://www.voidtools.com/zh-cn/'],
['Wox', 'https://github.com/Wox-launcher/Wox'],
['BCUninstaller', 'https://github.com/Klocman/Bulk-Crap-Uninstaller'],
['DBeaver', 'https://github.com/dbeaver/dbeaver'],
['Thunderbird', 'https://www.thunderbird.net/zh-CN/'],
['FDM', 'https://www.freedownloadmanager.org/zh/'],
['GIMP', 'https://www.gimp.org/'],
['VLC', 'https://www.videolan.org/vlc/'],
['Zotero', 'https://www.zotero.org/'],
['QQ音乐', 'https://y.qq.com/'],
['微信', 'https://weixin.qq.com/'],
['TIM', 'https://tim.qq.com/'],
['Shotcut', 'https://github.com/mltframework/shotcut'],
]],
['图片资源', 'tabs', 11, [
['图标库', 'https://www.iconfont.cn/'],
['插画库', 'https://undraw.co/illustrations'],
['Pixabay', 'https://pixabay.com/zh/'],
['Pexels', 'https://www.pexels.com/'],
]],
['科研论文', 'tabs', 12, [
['DeepL翻译', 'https://www.deepl.com/'],
['Linggle', 'https://linggle.com/'],
['Netspeak', 'https://netspeak.org/'],
['Semantic Scholar', 'https://www.semanticscholar.org/'],
['LaTex Table', 'https://www.tablesgenerator.com/'],
]],
];
$g = $pdo->prepare('INSERT INTO groups (name, slug, display, sort_order, is_visible) VALUES (?,?,?,?,1)');
$s = $pdo->prepare('INSERT INTO sites (group_id, url, name, short_name, icon, sort_order, is_visible, created_at, updated_at) VALUES (?,?,?,?,\'\',?,1,?,?)');
$now = date('Y-m-d H:i:s');
foreach ($data as $row) {
[$name, $display, $order, $sites] = $row;
$g->execute([$name, slugify($name), $display, $order]);
$gid = (int)$pdo->lastInsertId();
$i = 0;
foreach ($sites as $site) {
$s->execute([$gid, $site[1], $site[0], $site[0], $i++, $now, $now]);
}
}
}
/** 生成 slug(中文退化为拼音不可用时用 md5 短串) */
function slugify(string $s): string
{
$s = trim($s);
if (preg_match('/^[a-z0-9\-]+$/i', $s)) {
return strtolower($s);
}
return 'g' . substr(md5($s), 0, 6);
}