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:
@@ -1080,6 +1080,124 @@ final class Calendar
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🌟 时间冲突分列(v30 第 4 条)—— 「两个日程撞在一起不再堆叠,要错开画」。
|
||||
*
|
||||
* 用户原话:「日程中,如果两个事件时间上有冲突了,现在是堆叠在一起,应该错开,
|
||||
* 显示两个色块,如果三个冲突了,就是三个色块,错开显示。」
|
||||
* 并指定「这个显示效果,在 SyncCalendar 这个代码中有算法,你去读一下,借鉴一下」。
|
||||
*
|
||||
* ⇒ 算法逐行对照 SyncCalendar/entry/src/main/ets/common/TimelineUtil.ets 的
|
||||
* build() + groupsOf() 搬过来(那边跑在 ArkTS 上,这里是 PHP,但**判据一字未改**):
|
||||
* ① 先划分「冲突组」:按开始时间升序,一条与组内**最晚结束**还存在重叠
|
||||
* (`start < endMax`)就并进这一组 —— 这是"传递性连通",注意它会把
|
||||
* 「A 7:30-9:30 / B 8:20-9:00 / C 9:15-9:55」并成一组;
|
||||
* ② 组内**贪心分列**(区间图着色):每条放进"结束时间 <= 它开始时间"的
|
||||
* **编号最小**的那一列;开不出新列时才加一列。
|
||||
* ⇒ 列数 = 该组的**最大同时重叠数**,不是组内条数 ——
|
||||
* 上面那个例子里 B 与 C 并不重叠,可共用一列,所以只需 2 列而不是 3 列。
|
||||
* 这正是 SyncCalendar 注释里特意点出的"旧算法按传递性重叠分 3 列"的错处。
|
||||
* ③ 宽度 = 1/列数,左偏移 = 列号/列数 —— 于是 2 条冲突各占一半、3 条各占三分之一。
|
||||
*
|
||||
* ⚠️ 每条日程的"结束分钟"若缺失或 <= 开始(跨天日程的第二天、或用户填反了),
|
||||
* 按 30 分钟的最短可见块参与分列 —— 与 build_time_grid 的口径保持一致,
|
||||
* 否则"没有结束时间"的那些会被算成零长、永远不参与冲突。
|
||||
* ⚠️ **只在有时间的事件之间判冲突**:全天日程不进时间网格(它们挂顶上那一行)。
|
||||
*
|
||||
* @param array $events 某一天的时间型事件(元素含 time / end_time,单位 'HH:MM')
|
||||
* @return array 与入参同序的 [left, width](都是 0~1 的比例;无冲突时是 [0,1])
|
||||
*/
|
||||
public static function conflictLanes(array $events): array
|
||||
{
|
||||
$n = count($events);
|
||||
if ($n <= 1) {
|
||||
return array_fill(0, $n, [0.0, 1.0]);
|
||||
}
|
||||
|
||||
// ---- 前缀扫描动作:把每条事件换成一个"动作"(start 记 +1、end 记 -1)----
|
||||
// 这样"任意一点上有几条重叠"就是同一个坐标上的动作个数 —— 这正是
|
||||
// SyncCalendar 的贪心分列(laneEnd <= start ⇒ 复用该列)的等价表述,
|
||||
// 但**完全不需要保留 laneEnd 数组**,也就绕开了浮点比较。
|
||||
// ⚠️ 不能简单地按"起点同时刻"分组:结束点也要参与,且同时刻的 end 必须先于 start
|
||||
// 处理(否则"9:00 结束的"与"9:00 开始的"会被算成重叠)。
|
||||
// ---- ① 归一成 [下标, 开始, 结束](结束缺失/倒挂 → 30 分钟最短块,同上口径)----
|
||||
$rows = [];
|
||||
foreach ($events as $i => $e) {
|
||||
$s = self::minutesOf((string)($e['time'] ?? ''));
|
||||
if ($s === null) {
|
||||
continue;
|
||||
}
|
||||
$en = self::minutesOf((string)($e['end_time'] ?? ''));
|
||||
if ($en === null || $en <= $s) {
|
||||
$en = $s + 30;
|
||||
}
|
||||
$rows[] = ['i' => $i, 's' => $s, 'e' => min($en, 1440)];
|
||||
}
|
||||
if (!$rows) {
|
||||
return array_fill(0, $n, [0.0, 1.0]);
|
||||
}
|
||||
// 按开始时间升序(同开始则长的在前 —— 与 SyncCalendar 的稳定排序一致)
|
||||
usort($rows, static function (array $a, array $b): int {
|
||||
return $a['s'] <=> $b['s'] ?: $b['e'] <=> $a['e'];
|
||||
});
|
||||
|
||||
// ---- ② 划分冲突组:与组内**最晚结束**仍重叠(start < endMax)就并进本组 ----
|
||||
$groups = [];
|
||||
$cur = [];
|
||||
$endMax = -1;
|
||||
foreach ($rows as $r) {
|
||||
if ($cur && $r['s'] >= $endMax) {
|
||||
$groups[] = $cur;
|
||||
$cur = [];
|
||||
$endMax = -1;
|
||||
}
|
||||
$cur[] = $r;
|
||||
$endMax = max($endMax, $r['e']);
|
||||
}
|
||||
if ($cur) {
|
||||
$groups[] = $cur;
|
||||
}
|
||||
|
||||
// ---- ③ 组内贪心分列:每列记"最后结束分钟",放进编号最小的可用列 ----
|
||||
// ⚠️ 列数是**每组各自**的(组与组之间时间不重叠,可互相复用 0 号列)——
|
||||
// 所以一个"孤立日程"仍然是整宽,不会被别处的三方冲突连累成 1/3 宽。
|
||||
$out = [];
|
||||
for ($i = 0; $i < $n; $i++) {
|
||||
$out[$i] = [0.0, 1.0];
|
||||
}
|
||||
foreach ($groups as $g) {
|
||||
$laneEnd = []; // 每列最后结束分钟
|
||||
$laneOf = []; // 本组第 k 条 → 列号
|
||||
foreach ($g as $k => $r) {
|
||||
$lane = -1;
|
||||
foreach ($laneEnd as $li => $le) {
|
||||
if ($le <= $r['s']) { $lane = $li; break; }
|
||||
}
|
||||
if ($lane < 0) {
|
||||
$lane = count($laneEnd);
|
||||
$laneEnd[] = $r['e'];
|
||||
} else {
|
||||
$laneEnd[$lane] = $r['e'];
|
||||
}
|
||||
$laneOf[$k] = $lane;
|
||||
}
|
||||
$cols = max(1, count($laneEnd));
|
||||
foreach ($g as $k => $r) {
|
||||
$out[$r['i']] = [$laneOf[$k] / $cols, 1 / $cols];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** 'HH:MM' → 分钟数;空/不合法返回 null(第 4 条的分列用) */
|
||||
private static function minutesOf(string $t): ?int
|
||||
{
|
||||
if (preg_match('/^(\d{1,2}):(\d{2})/', trim($t), $m)) {
|
||||
return ((int)$m[1]) * 60 + (int)$m[2];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 把 [start, end] 夹到 [from, to] 里,逐天列出(含端点) */
|
||||
private static function eachDay(string $start, string $end, string $from, string $to): array
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user