Files
SimpleNav/config.php
T
laoyang 717a8877bb 后台排序改造:按板块标签页 + 拖拽排序 + 顺序完全固定
- 后台网站列表按板块分标签页,每个板块单独调顺序;用 URL hash
  (#sgroup-<gid>) 定位,上移/下移提交后回跳到原来的板块
- 支持鼠标拖拽排序(原生 HTML5 拖拽,零库依赖):拖动时实时重排预览,
  松手后整组落库(Ajax 动作 site_reorder,带 CSRF 校验与板块归属校验,
  防止跨板块搬条目)
- 稀疏排序:新增链接的顺序号 = 同板块 MAX + SORT_STEP(1000),天然排最后;
  拖拽落库、历史数据迁移时整组归一化为 index * SORT_STEP
- 修复真根因:旧机制靠「交换两条 sort_order」上移/下移,一旦历史数据里
  出现重复值,交换即无效、位置退化为按 id 排序 —— 这正是「排了顺序又被
  打乱」的来源。新增一次性归一化迁移,用 settings.mig_sort_sparse 作哨兵,
  避免每次请求重复覆盖用户设置
- 新增/修改链接不再用 MAX+1(MAX+1 会落进已有间隙、插到用户排好的中间)
- 改变所属板块时,该条目自动排到新板块的最后
- 后台页面 + 拖拽手柄做了配套样式,触屏设备降级为上下移按钮
2026-09-29 09:21:51 +08:00

139 lines
5.6 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 —— 基础配置
* 面向个人的轻量级网站导航系统
*/
declare(strict_types=1);
date_default_timezone_set('Asia/Shanghai');
if (function_exists('mb_internal_encoding')) {
mb_internal_encoding('UTF-8');
}
define('APP_NAME', '简航');
define('APP_PATH', __DIR__);
define('DATA_PATH', APP_PATH . '/data'); // 数据库等敏感数据(禁止 Web 访问)
define('DB_PATH', DATA_PATH . '/nav.db');
define('ICON_PATH', APP_PATH . '/uploads/icons'); // 抓取的网站图标(可公开访问)
define('ICON_URL', 'uploads/icons'); // 相对站点根
define('ICON_STORE', 128); // 图标统一存成 128×128(高清屏下 45~64px 显示仍锐利)
/**
* 排序步长(稀疏排序)。
*
* 为什么不用「连续整数 + 每次全表重排」:
* · 拖拽排序只需要改**被拖动那一条**(在两邻居之间取中间值),不必重写整组;
* · 新增条目一律 `同组 MAX + SORT_STEP`,天然排在最后,**永远不会插进用户已排好的
* 顺序中间**(这是用户明确要求的行为);
* · 只有「间隙用尽」或「用户拖完一次」时才做一次归一化(见 apply_sort_order())。
*
* 取值 1000:一个小组最多能容纳约 1000 次「往同两邻居之间插」而不归一化,
* 个人导航站的规模下基本永远不会退化。
*/
define('SORT_STEP', 1000);
/**
* 「站点整体图标」(浏览器标签页 favicon + 首页品牌图标)—— 用户上传后自动生成的一整套尺寸。
*
* ⚠️ 与上面的 ICON_PATH 是**两件事**,目录也刻意分开:
* ICON_PATH = 导航格子里**某个网站**自己的小图标,一个网站一份(键 = icon_key(网址));
* SITE_ICON_PATH = **本站自己**的图标,全局唯一(键 = 随机 16 位 hex)。
* 混在一起的话,清理图标目录时两边会互相误伤。
*/
define('SITE_ICON_PATH', APP_PATH . '/uploads/site');
define('SITE_ICON_URL', 'uploads/site');
/**
* 是否「由 PHP 再发一颗」绕开服务器侧页面缓存的标记 cookie。
*
* 背景(2026-09-28 线上实测):服务器装了宝塔面板的「网站加速」,它是一层基于
* memcached 的整页缓存,本站实测其过期时间是 30 秒。实测它
* **读不到** FastCGI 后端返回的 `Cache-Control` 响应头,所以 `no-store` 之类对它
* 无效;它唯一认的信号是**请求** cookie `X_CACHE=1`。
*
* 主要解法在根目录 `.htaccess` 里(用 `RequestHeader` 在请求侧注入该标记),
* 对所有访客一次生效、无需 PHP 配合;下面这个开关是**第二道保险**,
* 用于 Nginx 等不读 .htaccess 的环境。
*
* true = 动态页顺带把该 cookie 种到访客浏览器(首次访问各发一次)
* false = 只发响应头,完全依赖 .htaccess / 服务器配置
*/
define('BYPASS_PAGE_CACHE', true);
/**
* 自动探测应用根 URL,兼容「放在子目录 /simplenav」与「放在根目录」两种部署。
* 返回形如 ''(根目录)或 '/simplenav'。
*/
function app_base(): string
{
$dir = isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : '/';
$dir = str_replace('\\', '/', dirname($dir));
$dir = preg_replace('#/admin$#', '', $dir); // admin/ 下的脚本回退到应用根
$dir = rtrim($dir, '/');
return $dir === '/' ? '' : $dir;
}
/** 生成带应用根的 URL */
function url(string $path = ''): string
{
return app_base() . '/' . ltrim($path, '/');
}
/** 站点名(可在后台设置里覆盖)
*
* ⚠️ 这是**唯一**允许「空则回退默认」的设置:站点名留空会让 <title> / <h1> / alt
* 变成空白,页面连名字都没有。
* 这里显式写出来,而不是指望 setting() 全局回退 —— 那正是「副标题删不掉」的病根。
*/
function site_name(): string
{
$n = setting('site_name');
return $n !== '' ? $n : APP_NAME;
}
/**
* 设置表缓存(可失效)。
*
* ⚠️ 为什么必须能失效:这个缓存只在**首次读取**时查库,之后整进程复用。
* 后台「保存设置 → 重定向」是两次请求,所以以前没暴露问题;但只要有**同一次请求里
* 先写设置、再读回来**的路径(上传站点图标就是:写完马上要用新键拼 URL),
* 就会读到写之前的旧值 —— 表现为「上传成功但图标没变」。
* 任何写入设置的地方,写完都要调 settings_reload()。
*/
function settings_cache(bool $reload = false): array
{
static $cache = null;
if ($cache === null || $reload) {
$cache = [];
try {
foreach (db()->query('SELECT k, v FROM settings') as $row) {
$cache[$row['k']] = $row['v'];
}
} catch (Throwable $e) {
$cache = [];
}
}
return $cache;
}
/** 写入设置后调用,让本次请求后续的 setting() 读到新值 */
function settings_reload(): void
{
settings_cache(true);
}
/**
* 读取设置项。
*
* ⚠️ 空串是一等公民:**只有「这一行不存在」才回退 $default**。
* 曾经这里写的是 `$cache[$key] !== ''`,把「用户主动清空」和「从未配置过」当成一回事,
* 于是后台删掉副标题后前台照样显示默认的「个人网站导航」——删不掉。
* 需要「空则回退」的设置请在自己的读取点显式写明(如 site_name()),
* 不要把这个语义塞回函数里。
*/
function setting(string $key, string $default = ''): string
{
$cache = settings_cache();
return array_key_exists($key, $cache) ? $cache[$key] : $default;
}