v26 第 26 批(5 条):财务多选按集合取图 / 首页全天置顶与红线定位 / 任务复选框去提示与看板颜色 / 导航搜索引擎切换与站点复用 / 详情页去复选框
- 财务:勾选多个标签时上面那张「总金额」图按**选中集合**统计(新增 ledger/total_chart 路由 + 单一容器 data-ck-total-host,按集合现取并缓存; Ledger::timeline 第二参数放宽为 string|array) - 首页日程:全天日程移出滚动容器固定置顶;进场把当前时刻红线居中 (scrollTop 计算,不用 scrollIntoView),触发条件不绑 $showNow - 任务:复选框去除 title 提示;看板可设颜色(Task::boardColor/updateBoard), 首页任务行用该颜色显示所属看板 - 导航:展示页补回 id=engine-tabs(切换搜索引擎无效的根因);补内置引擎; 新增站点输 URL 可复用他人已加过的站点(sharedByUrl/copyIconFile/adoptSite) - 任务详情页页头去掉复选框(已有「标记完成」按钮)
This commit is contained in:
+194
-3
@@ -280,6 +280,144 @@ final class Nav
|
||||
return $n !== '' ? $n : self::domainOf((string)($s['url'] ?? ''));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 跨用户复用同一个站点(v24 第 26 批 用户第 4 条后半)
|
||||
//
|
||||
// 用户原话:「导航,每个人添加新的导航,输入 url 之后,如果其他用户添加了,
|
||||
// 他可以直接复用,复用的时候,自定义图标和简称,他可以自己再设置,
|
||||
// 设置好以后就固定了。」
|
||||
//
|
||||
// 三条语义,逐条落成代码:
|
||||
// ① **输入 URL 之后**才知道能不能复用 → 需要一个"按 URL 查"的接口
|
||||
// (`sharedByUrl()`),供前端在输入框失焦时问一次。
|
||||
// ② "他可以直接复用" → 复用的是一个**模板**:把别人那条的
|
||||
// 名字 / 图标 / 颜色搬过来当预填,**不是**共享同一条记录
|
||||
// (共享一条的话,A 改简称会改到 B 的导航上 —— 那谁也说不清是谁的)。
|
||||
// ③ "设置好以后就固定了" → 复用来的图标必须标 `icon_custom = 1`
|
||||
// (见下面 `adoptSite()` 的说明:否则下次「全部更新」会把它冲掉)。
|
||||
//
|
||||
// ⚠️ 隐私边界:这里**只暴露"这个 URL 有人加过"以及可复用的展示信息**
|
||||
// (名字 / 简称 / 图标文件名),**不暴露是谁加的**、也不暴露他还有哪些站。
|
||||
// 导航本身就是"要给别人看的"(展示页公开),所以这几个字段不算隐私;
|
||||
// 但"谁加过什么"是另一回事,不查也不给。
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* 按 URL 找**别人**已经加过的同款站点(用来做"可复用"提示与预填)。
|
||||
*
|
||||
* @return array<int,array> 最多返回几条(同一 URL 可能被多个人加过,去重后取前几个)
|
||||
*/
|
||||
public static function sharedByUrl(int $personId, string $url, int $limit = 6): array
|
||||
{
|
||||
$norm = self::normUrl($url);
|
||||
if ($norm === '') {
|
||||
return [];
|
||||
}
|
||||
// ⚠️ 用**归一化后的 URL** 比:用户手输时 https / 末尾斜杠 / 大小写都可能不同,
|
||||
// 直接比原串的话"同一个站点"会认不出来(这正是复用的价值所在)。
|
||||
// 库里存的也是归一化后的(saveSite 里就走 normUrl),所以等价比较成立。
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT url, name, short_name, icon, icon_custom
|
||||
FROM nav_sites
|
||||
WHERE person_id <> ? AND url = ?
|
||||
ORDER BY (short_name <> "") DESC, id ASC
|
||||
LIMIT ' . max(1, min(20, $limit))
|
||||
);
|
||||
$st->execute([$personId, $norm]);
|
||||
$out = [];
|
||||
$seen = [];
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
// 同一 URL 多人加过 → 按"显示名 + 图标"去重(对用户来说它们就是同一条)
|
||||
$key = self::siteLabel($r) . '|' . (string)$r['icon'];
|
||||
if (isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$key] = true;
|
||||
$out[] = $r;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把别人那条站点的展示信息**搬到自己这条**上(复用)。
|
||||
*
|
||||
* 与 `saveSite()` 分开写而不是给它加参数:`saveSite` 是"用户手填"那条路,
|
||||
* 这里是"照抄别人"那条路 —— 两者的**校验重点不同**(前者校验输入合法性,
|
||||
* 后者校验"那个图标文件还在不在")。混在一个函数里会让两条路的判断互相干扰。
|
||||
*
|
||||
* @param array $from sharedByUrl() 返回的一条
|
||||
* @param int $siteId 自己站点表里那条新建记录的 id
|
||||
*/
|
||||
public static function adoptSite(int $personId, int $siteId, array $from, string $name, string $short): bool
|
||||
{
|
||||
$db = Database::get();
|
||||
$row = self::site($personId, $siteId);
|
||||
if (!$row) {
|
||||
return false;
|
||||
}
|
||||
$name = trim($name);
|
||||
$short = trim($short);
|
||||
if ($name === '' && $short === '') {
|
||||
// 用户什么都没填 → 用别人的显示名当兜底(否则这条站没有名字可显示)
|
||||
$name = self::siteLabel($from);
|
||||
}
|
||||
|
||||
$icon = '';
|
||||
$isCus = 0;
|
||||
$srcIcon = trim((string)($from['icon'] ?? ''));
|
||||
if ($srcIcon !== '') {
|
||||
/* 🔴 "设置好以后就固定了" 这句话的落点就在这里。
|
||||
*
|
||||
* 复用的图标是**别人抓的**(或别人自定义的)。搬过来时必须标
|
||||
* `icon_custom = 1`,否则「全部更新」会按本站的抓取规则重新抓一遍
|
||||
* —— 那时抓到的可能是另一个图标,用户会看到"我明明选好了,怎么变了"。
|
||||
* 标成自定义 = 从此不再自动改它,与用户自己上传图标同等待遇。
|
||||
* ⚠️ 文件是**复制**一份到本站点名下(不是引用别人那个):
|
||||
* 否则别人删站 / 更新图标会连带影响你的展示页。
|
||||
*/
|
||||
$copied = self::copyIconFile($srcIcon);
|
||||
if ($copied !== '') {
|
||||
$icon = $copied;
|
||||
$isCus = 1;
|
||||
}
|
||||
}
|
||||
$db->prepare(
|
||||
'UPDATE nav_sites SET name = ?, short_name = ?, icon = ?, icon_custom = ?,
|
||||
updated_at = ? WHERE id = ? AND person_id = ?'
|
||||
)->execute([$name, $short, $icon, $isCus, date('Y-m-d H:i:s'), $siteId, $personId]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把图标文件复制一份、返回新文件名(失败返回 '')。
|
||||
*
|
||||
* ⚠️ 只认 `ICON_DIR` 下的**纯文件名**(与 iconUrl/deleteIconFile 同一套判据):
|
||||
* 带斜杠或 `..` 一律拒绝 —— 否则"复用"会变成一个任意文件读取的口子。
|
||||
*/
|
||||
public static function copyIconFile(string $file): string
|
||||
{
|
||||
$file = trim($file);
|
||||
if ($file === '' || strpos($file, '/') !== false || strpos($file, '\\') !== false
|
||||
|| strpos($file, '..') !== false) {
|
||||
return '';
|
||||
}
|
||||
$dir = FB_ROOT . '/' . self::ICON_DIR;
|
||||
$abs = $dir . '/' . $file;
|
||||
if (!is_file($abs)) {
|
||||
return ''; // 源文件不在了(别人删过)→ 不复制,退化成"没有图标"
|
||||
}
|
||||
// 新文件名:保持同样的扩展名(图标出口按扩展名给 Content-Type)
|
||||
$ext = strtolower((string)pathinfo($file, PATHINFO_EXTENSION));
|
||||
if (!preg_match('/^[a-z0-9]{1,5}$/', $ext)) {
|
||||
$ext = 'png';
|
||||
}
|
||||
$new = bin2hex(random_bytes(8)) . '.' . $ext;
|
||||
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
|
||||
return '';
|
||||
}
|
||||
return @copy($abs, $dir . '/' . $new) ? $new : '';
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 搜索引擎
|
||||
// ================================================================
|
||||
@@ -823,15 +961,68 @@ final class Nav
|
||||
/** 默认搜索引擎(照搬 SimpleNav 的 seed_engines) */
|
||||
public const ENGINE_SEED = [
|
||||
// 名称, slug, 搜索地址, 参数, 联想地址, 联想类型, 颜色, 是否启用
|
||||
//
|
||||
// 🔴 v24 第 26 批 用户第 4 条(后半):「**可以内置几个,让用户选择**」。
|
||||
// 原来只启用必应/百度两档,用户根本看不出"可选"这件事(列表里另外 4 个
|
||||
// 是以"未启用"状态躺着的,展示页的标签条只渲染启用的)。
|
||||
// ⇒ 把**实测可用的三档**都默认启用:必应 / 百度 / 360。
|
||||
// 谷歌 / DuckGo 保持未启用 —— 它们在**国内网络下连不上**
|
||||
// (见 nav_suggest.php 头部那段线上实测:连接超时),
|
||||
// 默认启用只会让用户点了发现"搜不动",比不提供更差。
|
||||
// 搜狗保持未启用:它的联想域名已不解析(实测 0 字节)。
|
||||
// ⚠️ 这三条实测结论来自 nav_suggest.php 的注释,不要凭印象改动。
|
||||
['必应', 'bing', 'https://cn.bing.com/search', 'q',
|
||||
'https://api.bing.com/osjson.aspx?query={q}', 'bing_osjson', '#60A504', 1],
|
||||
['百度', 'baidu', 'https://www.baidu.com/s', 'wd',
|
||||
'https://www.baidu.com/sugrec?prod=pc&wd={q}', 'baidu_sugrec', '#2100E0', 1],
|
||||
['谷歌', 'google', 'https://www.google.com/search', 'q', '', '', '#3B83FA', 0],
|
||||
['搜狗', 'sogou', 'https://www.sogou.com/web', 'query', '', '', '#FE6B14', 0],
|
||||
['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],
|
||||
'so360', '#1BB04A', 1],
|
||||
['谷歌', 'google', 'https://www.google.com/search', 'q', '', '', '#3B83FA', 0],
|
||||
['搜狗', 'sogou', 'https://www.sogou.com/web', 'query', '', '', '#FE6B14', 0],
|
||||
['DuckGo', 'ddg', 'https://duckduckgo.com/', 'q', '', '', '#DE5833', 0],
|
||||
];
|
||||
|
||||
/**
|
||||
* 把**缺失的内置引擎**补给这个人(v24 第 26 批 用户第 4 条)。
|
||||
*
|
||||
* 为什么需要它:`ensureSeed()` 只在"一条引擎都没有"时才灌种子
|
||||
* (它的判据是 `nav_groups` 是否为空)—— 于是**老用户**(在更新前就建过导航的人)
|
||||
* 永远拿不到后加的内置引擎:他们的库里只有当年那几条。
|
||||
* 用户说"内置几个让用户选择",对老用户就等于"一个都没有"。
|
||||
*
|
||||
* ⚠️ 按 **slug** 判重(不是 name):用户可能把"百度"改名成"度娘",
|
||||
* 若按名字判重会再灌一条重复的 baidu。
|
||||
* ⚠️ 只补**缺的**、绝不改动已有的(用户可能改过 URL / 颜色 / 启用状态,
|
||||
* 覆盖回去等于"我帮你改好了" —— 那种"好意"最难查)。
|
||||
*
|
||||
* @return int 新补了几条
|
||||
*/
|
||||
public static function addMissingEngines(int $personId): int
|
||||
{
|
||||
$db = Database::get();
|
||||
$st = $db->prepare('SELECT slug FROM nav_engines WHERE person_id = ?');
|
||||
$st->execute([$personId]);
|
||||
$have = [];
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
$have[(string)$r['slug']] = true;
|
||||
}
|
||||
$sort = (int)$db->query('SELECT COALESCE(MAX(sort_order),0) FROM nav_engines WHERE person_id = '
|
||||
. (int)$personId)->fetchColumn();
|
||||
$ins = $db->prepare(
|
||||
'INSERT INTO nav_engines (person_id, name, slug, search_url, param, suggest_url,
|
||||
suggest_type, color, is_active, sort_order) VALUES (?,?,?,?,?,?,?,?,?,?)');
|
||||
$n = 0;
|
||||
foreach (self::ENGINE_SEED as $e) {
|
||||
if (isset($have[(string)$e[1]])) {
|
||||
continue;
|
||||
}
|
||||
$sort += self::SORT_STEP;
|
||||
// ⚠️ 补进来的按种子的 `is_active` 走(必应/百度/360 = 1)。
|
||||
// "老用户库里已经有的那几条"保持原状 —— 不在这里动它们。
|
||||
$ins->execute([$personId, $e[0], $e[1], $e[2], $e[3], $e[4], $e[5], $e[6], $e[7], $sort]);
|
||||
$n++;
|
||||
}
|
||||
return $n;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user