v30 后半(5 条):导入框选中反馈+单选 / 首页日程改色块 / 近期任务主题色 / 时间冲突错开 / 通知消息模块

用户 2026-10-08 第二批 5 条。

1. 导入日程选中文件后没有任何反应 + 不允许多选
   `_stage_drop.php` 现在有两种模式:暂存型(附件,拖进来即刻上传)与导入型
   (`$sdStage=false`,写回 input.files、随表单提交、PHP 读完即弃)。
   暂存型框靠 queueDrop 画缩略图,而**导入型框选中文件后什么都不显示** ——
   框里那句「点击或拖动进行上传」从头到尾不变,用户看不出"到底选上没有"。
   改:app.js 新增 bindFileEcho(),只给**非暂存框**补 change 监听,
   选中后框里的话换成文件名 + 图标变 ✓ + `.upbox.is-picked` 稳定态;
   取消选择则还原原始文案(不是空字符串)。单选由 `$sdMultiple=false` 保证。

2. 首页「今天的日程」用色块表示,和日程保持一致
   原来那套是"白底卡片 + 左边一条 3px 色线",色只占一条线,看不出是哪个日历本。
   改:与日历页 `.tg-block` 同款实心色块(底色=日历本色,白字,圆角,无阴影),
   left/width/top/height 全部由内联给出(冲突分列 + 真实时长)。
   ⚠️ `.ag-ev` 的 CSS 里去掉 `right` —— 绝对定位下 left+right+width 三者同时存在是
      过约束,内联 width 会失效(冲突就不会错开了)。

3. 首页「近期任务」名称与圆点走任务模块主题色
   原来用 `var(--brand)` —— 那是**当前页面**(首页)的主题色,不是任务模块的;
   用户改过模块配色时两者不一致(首页砖红、任务别的色),正是他看到的"不一致"。
   改:两处都走 `--tk-c`(= Module::color('tasks')),与右上角「全部任务」按钮同源。
   逾期仍用红色(状态语义,不能被主题色盖掉)。

4. 日程时间冲突错开(算法借鉴 SyncCalendar)
   用户指定去读 SyncCalendar 的算法。落地自
   `entry/src/main/ets/common/TimelineUtil.ets` 的 build()/groupsOf():
   ① 划分冲突组(与组内最晚结束仍重叠就并入);② 组内贪心分列(区间图着色),
   列数 = **每组各自的最大同时重叠数**(sync 原注释点名的反例:传递性重叠但
   B/C 不重叠 → 只该 2 列)。落成 `App\Calendar::conflictLanes()`(PHP,判据一字未改)。
   ⚠️ 两处比原算法更细,都有单测钉住:
     · 列数按**组**算 —— 否则一个孤立日程会被别处的三方冲突连累成 1/3 宽;
     · "缺结束时间"不能一律按 30 分钟参与冲突(会与后面所有日程假冲突),
       改为优先拿**同列下一条的起点**当它的视觉结束 → 因此必须**先分列再算高度**。
   首页与日历页共用同一套(用户第 2 条要求"和日程保持一致")。

5. 通知消息模块(Schema v30 → v31)
   不在导航显示,入口在头像下拉里、**排在个人设置之前**,名字「通知消息」;
   有人在观影/旅程加我时发通知;通知出现在首页、**往年今天之前**。
   新增:app/Notification.php + notifications 表(唯一键
   (person_id,target_type,target_id) —— 参与人是整组替换,没有它改三次标题就收三条)+
   ?r=notifications{,/del,/clear} + templates/notifications/index.php + 头像下拉未读角标 +
   app/helpers.php 的 fb_time_ago()。
   ⚠️ 三条设计判断:不进 Module::LIST(进去就会被顶栏渲染、被"模块显示与排序"当开关);
      只在 person_id !== actor_id 时发(不给自己发,"通知"≠流水账);
      移出参与人**撤销未读**、**保留已读**(未读是没送达的提醒,已读是看过的历史)。

验证(全部 0 失败):e2e 1478/0(+14 条新断言)、升级 153/0、vCard 40 项、城市 32/0、
几何 428/0 + 移动 680/0 + 交互 92/0、b24/b27/b28/b29 真机全绿;
冲突分列算法单测 12/0(含 SyncCalendar 点名的反例)、
新探针 _b30_ui2 16/0 + _b30_notify 16/0 + _b30_import 26/0、
反向验证 18/0(五条各自改回旧行为 → 断言全红)、
部署脚本新增 9 条守卫 + 守卫证伪 11/0。线上 11 文件 SHA256 一致,Schema v31 已升级。
This commit is contained in:
2026-10-08 17:41:30 +08:00
parent 91c55038c9
commit 2a10df2557
11 changed files with 918 additions and 31 deletions
+45 -1
View File
@@ -124,6 +124,14 @@ final class Schema
* ⚠️ 用一次性哨兵(`note_shares_migrated_v29`),理由同 v28 那次:
* 迁移是"一次性数据转换",不是"持续同步",
* 否则用户取消共享后下次启动又被灌回来(本项目已栽过 4 次)。
* 31 → **通知消息(用户 2026-10-08 第 5 条)**:新增 `notifications`。动机是用户的话:
* 「引入通知模块,这个模块,不在导航显示,在个人头像点击那里显示,
* 放在个人设置前面,名字:通知消息。就是当有人在观影、旅程等地方加我的时候,
* 发一个通知,通知出现在首页中,往年今天的前面。」
* ⇒ 通知**不进 Module::LIST**(进就会被顶栏渲染、被"模块显示与排序"当成开关),
* 它是一个独立页面(`?r=notifications`),入口只在头像下拉里。
* ⚠️ 唯一键 (person_id, target_type, target_id) 是整套设计的基石:
* 参与人是"整组替换",没有它,用户改三次标题就收到三条"加了你"。
*/
/**
* 结构版本(**人读**用:日志、设置页上显示"库结构是哪一版")。
@@ -131,7 +139,7 @@ final class Schema
* ⚠️ 它**不再**作为"要不要跑升级"的判据 —— 见 fingerprint() 的说明。
* 它仍然要手工维护,因为它给人看(排查时一眼知道代码到哪一版了)。
*/
public const VERSION = '30';
public const VERSION = '31';
/**
* 「源码指纹」:拿**本文件自身的内容**算一个哈希,作为"要不要跑升级"的判据。
@@ -1080,6 +1088,42 @@ CREATE TABLE IF NOT EXISTS `note_shares` (
CONSTRAINT `fk_ns_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='笔记共享给别人:可只读、可读写(v29:取代"共享给全家"那个布尔)'
SQL,
// ---- 通知消息(v30 第 5 条)----
//
// 用户原话:「引入通知模块,这个模块,不在导航显示,在个人头像点击那里显示,
// 放在个人设置前面,名字:通知消息。就是当有人在观影、旅程等地方
// 加我的时候,发一个通知,通知出现在首页中,往年今天的前面。」
//
// ⚠️ `target_type` + `target_id` **不做外键**(它指向 events 等多张表,
// 外键表达不了这个形状)—— 与 `attachments` 完全同一套做法与理由。
// 宿主删除时由 Notification::removeForTarget() 手动清,
// 否则通知点进去 404。
// ⚠️ 唯一键 `uk_ntf`(person + target_type + target_id)是**整套设计的基石**:
// 观影/旅程保存参与人是"整组替换"(先删再插),
// 没有它,用户改三次标题就会收到三条"有人把你加进了《…》"。
// Notification::syncForTarget() 靠 INSERT IGNORE 落在这条键上天然去重。
<<<'SQL'
CREATE TABLE IF NOT EXISTS `notifications` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`family_id` INT UNSIGNED NOT NULL,
`person_id` INT UNSIGNED NOT NULL COMMENT '收件人(**不含**动作发出者 —— 不给自己发)',
`actor_id` INT UNSIGNED DEFAULT NULL COMMENT '谁触发的(把别人加进去的人)',
`kind` VARCHAR(24) NOT NULL DEFAULT '' COMMENT 'movie / trip …(决定图标)',
`title` VARCHAR(255) NOT NULL COMMENT '一句话正文(已含宿主标题)',
`href` VARCHAR(64) DEFAULT NULL COMMENT '跳转路由(url() 的 r 值)',
`params` VARCHAR(255) DEFAULT NULL COMMENT '跳转参数(json,如 {"id":12})',
`target_type` VARCHAR(24) NOT NULL DEFAULT '' COMMENT '宿主类型(去重维度之一)',
`target_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '宿主 id(去重维度之一)',
`is_read` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ntf` (`person_id`, `target_type`, `target_id`),
KEY `idx_ntf_unread` (`person_id`, `is_read`, `id`),
CONSTRAINT `fk_ntf_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_ntf_actor` FOREIGN KEY (`actor_id`) REFERENCES `persons` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='通知消息(v30:别人在观影/旅程等地方把我加进去)'
SQL,
];
}