v29 第 29 批(9 条):日程定位/复选框提示/名册错乱三处真因修复 + 读书落盘 + 导航四设置 + 笔记阅读态与共享
前 4 条是"上批改了但用户仍报"的,这次先拿真机证据再动手,三处真因如下:
1. 首页没定位到红线 —— 真根因:定位脚本的 PHP 条件是
`if (!$todayEvents || empty($timed))`,**今天只有全天日程时整段脚本不渲染**
(实测 hasScript=False、scrollTop=0),而 .ag-grid(0–24h) 与红线都在。
有分时段日程时定位其实完全正确(偏差 0px)。已改成 `if (!$todayEvents)`。
2. 任务复选框提示还在 —— 真根因:`_task_check.php` 里那句无障碍文字
`<span class="visually-hidden">` 用的类名**在 app.css 里从来没有定义过**
(项目一直只用 .sr-only)→ 没有规则 = 普通 span = 直接可见。
已把两个类名合并到同一条规则(clip 隐藏,仍可读屏)。
3. 名册完全错乱 —— 真根因:v29 重写平铺列表时**漏了一个 `</div>`**(闭合 .person),
浏览器把下一张卡嵌进上一张里,实测 .person-main 只剩 24~151px(本该 ~800px)。
`php -l` 与全部静态断言都抓不到,只有真机量 DOM 才现形。已补上闭合。
另把 v28 引入的多列 grid 改回单列(它与 content-visibility 冲突)+窄屏动作区折行。
4. 读书慢 —— 按用户给的思路「做一个同步,然后落盘」:
新增 weread_snapshots 表 + App\WeReadCache(按人+数据集存 JSON);
渲染路径改走 notebooksCached()/readStatsCached()(永不联网);
同步从"渲染之前"挪到"**响应之后**"(queueBackgroundSync + fastcgi_finish_request)。
5. 导航设置改成右上角**下拉菜单 + 四视图**(addr/general/board/engine),
表单带 back 域 → 保存后回原来那一项。
🔴 顺带修掉一个真 bug:`if (!$inSettings && $canWrite)` 的 endif 漂到了文件末尾,
把 board/engine 两个视图也包进了"非设置态" → 打开时标题对、**内容是空的**。
6. 笔记列表缩略图:cover → contain + 固定高度(按比例完整显示,不裁切)。
7. 笔记段落编辑器:加 Markdown 工具条 + 斜线命令(行首打 /co → 回车补全代码块),
两者共用同一份命令表。
8. 笔记点进来默认**阅读态**(图文混排、附件在最下方、图片四档宽度、点「编辑」才可改);
清单块并入 Markdown —— 但**历史 list 块转写成 `- 项` 文本,老数据不丢**。
9. 笔记共享改成**逐人 + 权限**(note_shares,与 resource_shares 同构);
可见性收敛成全站唯一的 Note::VIS_COND;迁移用一次性哨兵。
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
/**
|
||||
* 微信读书远端数据的**本地快照**(v29 用户 2026-10-08 第 4 条)。
|
||||
*
|
||||
* 用户原话:
|
||||
* 「读书还是慢。是不是可以做一个同步,然后落盘,这样就不用每次都读取
|
||||
* 微信读书的数据了。」
|
||||
*
|
||||
* ────────────────────────────────────────────────────────────────
|
||||
* 为什么单开一个类、而不是塞进 `WeRead`:
|
||||
* `WeRead` 的头部写得很清楚 ——「这个类**只负责取数据**,不碰数据库」。
|
||||
* 那条约定是有价值的(网络与存储分开,测试可以只测取数不连外网)。
|
||||
* 落盘是**另一件事**,所以放另一个类;`WeRead` 保持"干净的网络层"。
|
||||
*
|
||||
* 为什么是"JSON 快照"而不是"拆成表":
|
||||
* 这些数据是**只读的远端结果**,字段完全由官方决定(说改就改),
|
||||
* 本站从不按其中任何字段做查询、排序、JOIN。
|
||||
* 拆表 = 把别人的字段设计背到自己身上,官方一变就要写迁移;
|
||||
* 存 JSON = 官方怎么变都能原样兜住(渲染端按需取,取不到就当空)。
|
||||
*
|
||||
* 为什么渲染路径**永远不等**刷新(关键设计):
|
||||
* `get()` 只读本地,永不联网。若快照过期,调用方可以"顺手"触发一次刷新,
|
||||
* 但**必须先拿旧快照把页面渲染出来**,刷新结果留到下次访问可见。
|
||||
* 反面做法是"过期就同步完再渲染" —— 那就又回到"点进来要等很久",
|
||||
* 也就是用户这次报的那个问题。**"数据新鲜"永远排在"页面能立刻打开"之后。**
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
final class WeReadCache
|
||||
{
|
||||
/** 快照的数据集名 —— 只在这里登记,别处一律用常量,避免拼错字符串 */
|
||||
public const K_NOTEBOOKS = 'notebooks';
|
||||
|
||||
/** 阅读统计(按 mode 各存一份:overall / weekly / monthly / annually) */
|
||||
public static function statsKey(string $mode): string
|
||||
{
|
||||
return 'stats_' . $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读一份快照。
|
||||
*
|
||||
* @param bool $freshOnly true = 过期就当没有(返回 null)——
|
||||
* 给那些"宁可不显示也不要旧数据"的场景用
|
||||
* @return array{ok:bool,data:mixed,updated_at:int,stale:bool}
|
||||
* ok=false 表示**本地没有**(从没同步过)
|
||||
*/
|
||||
public static function get(int $personId, string $key, bool $freshOnly = false): array
|
||||
{
|
||||
$miss = ['ok' => false, 'data' => null, 'updated_at' => 0, 'stale' => true];
|
||||
if ($personId <= 0 || $key === '') {
|
||||
return $miss;
|
||||
}
|
||||
$db = Database::get();
|
||||
try {
|
||||
$st = $db->prepare(
|
||||
'SELECT v, UNIX_TIMESTAMP(updated_at) AS ts
|
||||
FROM weread_snapshots WHERE person_id = ? AND k = ?'
|
||||
);
|
||||
$st->execute([$personId, $key]);
|
||||
$row = $st->fetch();
|
||||
} catch (\Throwable $e) {
|
||||
// 表还没建(升级中)→ 当作"没有快照",绝不让缓存层把页面打挂
|
||||
return $miss;
|
||||
}
|
||||
if (!$row) {
|
||||
return $miss;
|
||||
}
|
||||
$ts = (int)($row['ts'] ?? 0);
|
||||
$stale = (time() - $ts) > WeRead::SNAP_TTL;
|
||||
if ($freshOnly && $stale) {
|
||||
return $miss;
|
||||
}
|
||||
$data = json_decode((string)($row['v'] ?? ''), true);
|
||||
return ['ok' => true, 'data' => $data, 'updated_at' => $ts, 'stale' => $stale];
|
||||
}
|
||||
|
||||
/** 写一份快照(覆盖旧的)。失败静默 —— 缓存写不进去不该影响功能。 */
|
||||
public static function put(int $personId, string $key, $data): void
|
||||
{
|
||||
if ($personId <= 0 || $key === '') {
|
||||
return;
|
||||
}
|
||||
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
if ($json === false) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$db = Database::get();
|
||||
$st = $db->prepare(
|
||||
'INSERT INTO weread_snapshots (person_id, k, v, updated_at)
|
||||
VALUES (?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE v = VALUES(v), updated_at = VALUES(updated_at)'
|
||||
);
|
||||
$st->execute([$personId, $key, $json]);
|
||||
} catch (\Throwable $e) {
|
||||
// 静默(缓存是加速手段,不是功能本体)
|
||||
}
|
||||
}
|
||||
|
||||
/** 最后一次落盘是什么时候(页面上显示"数据更新于 …") */
|
||||
public static function updatedAt(int $personId, string $key): int
|
||||
{
|
||||
return (int)self::get($personId, $key)['updated_at'];
|
||||
}
|
||||
|
||||
/** 这个人有没有**任何**快照(判断"是不是第一次用") */
|
||||
public static function hasAny(int $personId): bool
|
||||
{
|
||||
if ($personId <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT COUNT(*) FROM weread_snapshots WHERE person_id = ?'
|
||||
);
|
||||
$st->execute([$personId]);
|
||||
return (int)$st->fetchColumn() > 0;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 清掉一个人的全部快照(换 Key / 停用自动同步时用) */
|
||||
public static function forget(int $personId): void
|
||||
{
|
||||
if ($personId <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Database::get()->prepare('DELETE FROM weread_snapshots WHERE person_id = ?')
|
||||
->execute([$personId]);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user