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:
@@ -303,9 +303,20 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
||||
<span class="tg-now" data-now-line style="top: <?= (float)$nowTop ?>%;"
|
||||
title="现在 <?= e(date('H:i')) ?>"></span>
|
||||
<?php endif; ?>
|
||||
<?php /* 🔴 v30 第 4 条:时间冲突的日程**错开画**(不再堆叠)。
|
||||
用户原话:「如果两个事件时间上有冲突了,现在是堆叠在一起,应该错开,
|
||||
显示两个色块,如果三个冲突了,就是三个色块,错开显示。」
|
||||
left/width 由 App\Calendar::conflictLanes 算好(算法借鉴 SyncCalendar
|
||||
的 TimelineUtil:贪心分列,列数 = 最大同时重叠数)。
|
||||
⚠️ 用**内联 left/width 百分比**而不是 CSS 类:
|
||||
列数 1/2/3/4 是运行时才知道的,类名会爆炸;
|
||||
而且模板已经在内联 top/height 了,同一套坐标系最不容易错。 */ ?>
|
||||
<?php foreach (($tgrid['timed'][$i] ?? []) as $ev): ?>
|
||||
<a class="tg-block" data-cal="<?= (int)$ev['cal'] ?>"
|
||||
style="background: <?= e((string)$ev['color']) ?>; top: <?= (float)$ev['top'] ?>%; height: <?= (float)$ev['height'] ?>%;"
|
||||
style="background: <?= e((string)$ev['color']) ?>;
|
||||
top: <?= (float)$ev['top'] ?>%; height: <?= (float)$ev['height'] ?>%;
|
||||
left: calc(2px + <?= (float)($ev['left'] ?? 0) ?>%);
|
||||
width: calc(<?= (float)($ev['width'] ?? 100) ?>% - 4px);"
|
||||
href="<?= e((string)$ev['href']) ?>"
|
||||
title="<?= e((string)$ev['title']) ?>">
|
||||
<b><?= e((string)$ev['time']) ?></b> <?= e((string)$ev['title']) ?>
|
||||
|
||||
+113
-9
@@ -227,6 +227,23 @@ if ($timed) {
|
||||
$nowH = (int)date('G');
|
||||
$nowPos = ($nowH - $startH) / max(1, ($endH - $startH)); // 红线位置(0~1)
|
||||
$showNow = $nowPos >= 0 && $nowPos <= 1;
|
||||
|
||||
/* 🔴 v30 第 2/4 条:首页日程也用**和日程模块同一套冲突分列**。
|
||||
⇒ 复用 App\Calendar::conflictLanes(算法借鉴 SyncCalendar 的 TimelineUtil):
|
||||
两条撞车就左右各半、三条就三分 —— 首页与日历页看到的是同一个布局。
|
||||
⚠️ 必须**先按时间排序**再分列(lanes 是按传入顺序返回的):
|
||||
`$todayEvents` 从路由出来已是"全天在前、其余按时刻"升序,这里再稳一次。
|
||||
⚠️ 入参只挑**有时间**的那些(全天日程不进时间网格)。 */
|
||||
usort($timed, static function (array $a, array $b): int {
|
||||
return strcmp((string)($a['time'] ?? ''), (string)($b['time'] ?? ''));
|
||||
});
|
||||
$laneInfo = \App\Calendar::conflictLanes($timed);
|
||||
foreach ($timed as $i => &$evRef) {
|
||||
[$l, $w] = $laneInfo[$i] ?? [0.0, 1.0];
|
||||
$evRef['left'] = round($l * 100, 4);
|
||||
$evRef['width'] = round($w * 100, 4);
|
||||
}
|
||||
unset($evRef);
|
||||
?>
|
||||
<section class="section">
|
||||
<div class="home-duo">
|
||||
@@ -282,18 +299,48 @@ $showNow = $nowPos >= 0 && $nowPos <= 1;
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php /* 日程块:按"相对第一个刻度的小时数"定位(与日视图同一套比例)---- */ ?>
|
||||
<?php foreach ($timed as $ev): ?>
|
||||
<?php
|
||||
<?php
|
||||
/* 🔴 v30 第 2 条:「首页上的今天的日程,用色块表示,和日程保持一致。」
|
||||
用户要的是**和日程模块(周/日视图)一样的实心色块**,而不是原来的
|
||||
"白底 + 左边一条色线" —— 后者色只占 3px,一眼看不出是哪个日历本。
|
||||
|
||||
⇒ 三处对齐日程模块的 `.tg-block`:
|
||||
① 底色 = 该日程所属日历本的颜色(不是主题色);
|
||||
② 白字(`.tg-block` 就是 `color:#fff`);
|
||||
③ 冲突错开(left/width 由 Calendar::conflictLanes 算,见 index.php)。
|
||||
⚠️ 深浅:`--tg-*` 那几个本子的颜色都是中低明度,白字压得住;
|
||||
但用户可能自定义成很浅的色 —— 所以标题那一行加 `text-shadow`
|
||||
兜底(比"再加一层半透明黑罩"更不破坏本色)。
|
||||
⚠️ 高度用 `min-height` 而不是固定值:15 分钟的日程也要看得见
|
||||
(与 .tg-block 的 15 分钟最短块口径一致)。 */
|
||||
foreach ($timed as $ev):
|
||||
$p = explode(':', (string)$ev['time']);
|
||||
$evMin = ((int)($p[0] ?? 0)) * 60 + (int)($p[1] ?? 0);
|
||||
$evMin = ((int)($p[0] ?? 0)) * 60 + ((int)($p[1] ?? 0));
|
||||
$topPct = (($evMin / 60) - $startH) / max(1, ($endH - $startH)) * 100;
|
||||
$topPct = max(0, min(96, $topPct));
|
||||
?>
|
||||
// 结束时刻:缺省时按"下一件事的起点"或 30 分钟(与日历口径一致)
|
||||
$et2 = (string)($ev['end_time'] ?? '');
|
||||
if ($et2 !== '' && preg_match('/^(\d{1,2}):(\d{2})/', $et2, $mm2)) {
|
||||
$endMin = ((int)$mm2[1]) * 60 + ((int)$mm2[2]);
|
||||
} else {
|
||||
$endMin = $evMin + 30;
|
||||
}
|
||||
if ($endMin <= $evMin) { $endMin = $evMin + 30; }
|
||||
$endMin = min($endMin, $endH * 60);
|
||||
$hPct = ($endMin / 60 - $startH) / max(1, ($endH - $startH)) * 100 - $topPct;
|
||||
$hPct = max(2.2, $hPct); // 至少能塞下一行
|
||||
?>
|
||||
<a class="ag-ev" href="<?= e((string)$ev['href']) ?>"
|
||||
style="top: <?= number_format($topPct, 2) ?>%; border-left-color: <?= e((string)$ev['color']) ?>;">
|
||||
style="background: <?= e((string)$ev['color']) ?>;
|
||||
top: <?= number_format($topPct, 2) ?>%;
|
||||
height: <?= number_format($hPct, 2) ?>%;
|
||||
left: calc(46px + <?= (float)($ev['left'] ?? 0) ?>%);
|
||||
width: calc(<?= (float)($ev['width'] ?? 100) ?>% - 50px);"
|
||||
title="<?= e((string)$ev['title']) ?>"
|
||||
data-cal="<?= (int)$ev['cal'] ?>">
|
||||
<span class="ag-ev-t"><?= e((string)$ev['time']) ?></span>
|
||||
<span class="ag-ev-w"><?= e((string)$ev['title']) ?></span>
|
||||
<span class="dash-cal" style="background:<?= e((string)$ev['color']) ?>;"><?= e((string)$ev['calname']) ?></span>
|
||||
<span class="ag-ev-cal"><?= e((string)$ev['calname']) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div><?php /* /.ag-grid */ ?>
|
||||
@@ -321,14 +368,29 @@ $showNow = $nowPos >= 0 && $nowPos <= 1;
|
||||
<ul class="dash-list tk-list">
|
||||
<?php foreach ($tasks as $t): ?>
|
||||
<?php $late = \App\Task::isOverdue($t['due_at'] ?? null); ?>
|
||||
<li class="dash-row tk-row" data-task-row="<?= (int)$t['id'] ?>">
|
||||
<?php
|
||||
/* 🔴 v30 第 3 条:「首页,近期任务,任务的名称和前面的点,都应该和主题颜色
|
||||
一样,都保持一致。」
|
||||
用户说的"主题颜色"= **任务模块的主题色**(个人设置里可换的那个)。
|
||||
⚠️ 原来圆点用的是 `var(--brand)` —— 那是**当前页面**(首页)的主题色,
|
||||
不是任务模块的;两者在用户改过模块配色时会不一样
|
||||
(首页 = 砖红,任务可能是别的色),正是用户看到的不一致。
|
||||
⇒ 两条都用同一个 `--tk-c`(= Module::color('tasks')),
|
||||
与右上角「全部任务」按钮的 `--hd-c` 同源,全块一个色。
|
||||
⚠️ 逾期仍用红色:那是状态语义(要显眼),不能被主题色盖掉;
|
||||
但改名后的那个点也统一走 `--tk-c`(不逾期时)。 */
|
||||
$tkColor = \App\Module::color('tasks', (int)\App\Auth::familyId());
|
||||
?>
|
||||
<li class="dash-row tk-row" data-task-row="<?= (int)$t['id'] ?>"
|
||||
style="--tk-c: <?= e($tkColor) ?>;">
|
||||
<?php
|
||||
$tkId = (int)$t['id']; $tkStatus = (string)($t['status'] ?? 'todo');
|
||||
$tkBack = 'home'; $tkStatic = !\App\Auth::canWrite();
|
||||
require __DIR__ . '/_task_check.php';
|
||||
?>
|
||||
<span class="dash-dot" style="background: <?= e($late ? 'var(--red)' : 'var(--brand)') ?>;"></span>
|
||||
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>">
|
||||
<span class="dash-dot" style="background: <?= e($late ? 'var(--red)' : 'var(--tk-c)') ?>;"></span>
|
||||
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>"
|
||||
style="color: <?= e($late ? 'var(--red)' : $tkColor) ?>;">
|
||||
<?= e((string)$t['title']) ?>
|
||||
</a>
|
||||
<?php /* 用户第 3 条:「除了显示任务的标题之外,还要显示他的状态。
|
||||
@@ -364,6 +426,48 @@ $showNow = $nowPos >= 0 && $nowPos <= 1;
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php /* ==================== 通知消息(v30 第 5 条)====================
|
||||
用户原话:「…发一个通知,通知出现在首页中,**往年今天的前面**。」
|
||||
⇒ 位置是硬性要求:必须在「往年的今天」**之前**。
|
||||
⚠️ 没有通知时**整块不渲染** —— 首页是"每天第一眼看的地方",
|
||||
不要用一个空盒子占掉一屏(与"今天没有安排"那种信息位不同:
|
||||
通知本来就是事件驱动的,没有就真的不该出现)。 */ ?>
|
||||
<?php if (!empty($notifs)): ?>
|
||||
<section class="section">
|
||||
<div class="page-head" style="margin-bottom:12px">
|
||||
<h1 style="font-size:15px">
|
||||
通知消息
|
||||
<?php if ($notifUnread > 0): ?><span class="sub"><?= (int)$notifUnread ?> 条未读</span><?php endif; ?>
|
||||
</h1>
|
||||
<div class="actions">
|
||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('notifications')) ?>">全部通知</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-pad">
|
||||
<ul class="ntf-list ntf-list-home">
|
||||
<?php foreach ($notifs as $n): ?>
|
||||
<?php
|
||||
$actorName = \App\Nickname::display(
|
||||
(int)$meId,
|
||||
['name' => $n['actor_name'] ?? '', 'my_nickname' => $n['actor_nick'] ?? '']
|
||||
);
|
||||
if (trim((string)$actorName) === '') { $actorName = '有人'; }
|
||||
?>
|
||||
<li class="ntf-row<?= empty($n['is_read']) ? ' is-unread' : '' ?>">
|
||||
<img class="ntf-av" src="<?= e(avatar_url($n['actor_avatar'] ?? null)) ?>" alt="">
|
||||
<a class="ntf-main" href="<?= e(\App\Notification::href($n)) ?>">
|
||||
<span class="ntf-title">
|
||||
<b><?= e((string)$actorName) ?></b><?= e(preg_replace('/^有人/', '', (string)$n['title'])) ?>
|
||||
</span>
|
||||
<span class="ntf-time"><?= e(fb_time_ago((string)$n['created_at'])) ?></span>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ==================== ③ 往年的今天 ==================== */ ?>
|
||||
<section class="section">
|
||||
<div class="page-head" style="margin-bottom:12px">
|
||||
|
||||
@@ -103,6 +103,20 @@ $isLoggedIn = \App\Auth::check();
|
||||
<i class="caret" aria-hidden="true"></i>
|
||||
</summary>
|
||||
<div class="menu-list">
|
||||
<?php
|
||||
/* 🔴 v30 第 5 条:「引入通知模块,这个模块,不在导航显示,在个人头像点击那里显示,
|
||||
放在个人设置前面,名字:通知消息。」
|
||||
⇒ 它排在**个人设置之前**,且带一个未读角标(有几条没看)。
|
||||
⚠️ 这是通知唯一的入口 —— 顶栏不渲染它(它不在 Module::LIST,
|
||||
见 app/Notification.php 头部的第 ①条)。 */
|
||||
$fbUnread = \App\Notification::unreadCount((int)\App\Auth::personId());
|
||||
?>
|
||||
<a href="<?= e(url('notifications')) ?>">
|
||||
通知消息
|
||||
<?php if ($fbUnread > 0): ?>
|
||||
<span class="menu-badge"><?= (int)$fbUnread ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<a href="<?= e(url('settings/me')) ?>">个人设置</a>
|
||||
<?php if (\App\Auth::isAdmin()): ?>
|
||||
<a href="<?= e(url('settings')) ?>">系统设置</a>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* 通知消息(v30 第 5 条)。
|
||||
*
|
||||
* 用户原话:「引入通知模块,这个模块,不在导航显示,在个人头像点击那里显示,
|
||||
* 放在个人设置前面,名字:通知消息。就是当有人在观影、旅程等地方
|
||||
* 加我的时候,发一个通知。」
|
||||
*
|
||||
* 所以这一页**只有一条线**:谁、在什么时候、把"我"加进了哪件事 —— 点进去就是那件事。
|
||||
*
|
||||
* ⚠️ 进页面即全部标已读(在路由里做的)—— 消息中心的惯例:
|
||||
* 打开看看就等于"看过了",不需要再让用户手动一条条点"已读"。
|
||||
* 但**未读的才会被"撤销"**(被移出参与人时):已读的是历史,不删(见 Notification 头部)。
|
||||
*
|
||||
* @param array $items 我的通知(最新在前,含 actor_name / actor_nick / actor_avatar)
|
||||
*/
|
||||
$items = $items ?? [];
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>通知消息</h1>
|
||||
<span class="sub">共 <?= count($items) ?> 条</span>
|
||||
<div class="actions">
|
||||
<?php if ($items): ?>
|
||||
<form method="post" action="<?= e(url('notifications/clear')) ?>"
|
||||
onsubmit="return confirm('清空全部通知?清掉就找不回来了。')" style="display:inline">
|
||||
<?= csrf_field() ?>
|
||||
<button class="btn btn-ghost btn-sm" type="submit">清空</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-ghost btn-sm" href="<?= e(url('home')) ?>">返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-pad">
|
||||
<?php if (!$items): ?>
|
||||
<p class="empty-hint">
|
||||
还没有通知。别人在观影、旅程里把你加进去时,这里就会有消息。
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<ul class="ntf-list">
|
||||
<?php foreach ($items as $n): ?>
|
||||
<?php
|
||||
// 谁发起的:优先用"我叫他什么"(与全站称呼口径一致)
|
||||
$actorName = \App\Nickname::display(
|
||||
(int)\App\Auth::personId(),
|
||||
['name' => $n['actor_name'] ?? '', 'my_nickname' => $n['actor_nick'] ?? '']
|
||||
);
|
||||
if (trim((string)$actorName) === '') { $actorName = '有人'; }
|
||||
$isUnread = empty($n['is_read']);
|
||||
// 图标:按 kind 分(观影 = 胶片,旅程 = 脚印,其余 = 铃铛)
|
||||
$kind = (string)($n['kind'] ?? '');
|
||||
?>
|
||||
<li class="ntf-row<?= $isUnread ? ' is-unread' : '' ?>">
|
||||
<img class="ntf-av" src="<?= e(avatar_url($n['actor_avatar'] ?? null)) ?>" alt="">
|
||||
<a class="ntf-main" href="<?= e(\App\Notification::href($n)) ?>">
|
||||
<span class="ntf-title">
|
||||
<b><?= e((string)$actorName) ?></b>
|
||||
<?= e(preg_replace('/^有人/', '', (string)$n['title'])) ?>
|
||||
</span>
|
||||
<span class="ntf-time"><?= e(fb_time_ago((string)$n['created_at'])) ?></span>
|
||||
</a>
|
||||
<span class="ntf-kind ntf-kind-<?= e($kind) ?>" aria-hidden="true">
|
||||
<?= $kind === 'movie' ? '🎬' : (strpos($kind, 'trip') === 0 ? '🧭' : '🔔') ?>
|
||||
</span>
|
||||
<form method="post" action="<?= e(url('notifications/del')) ?>" class="ntf-del">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
|
||||
<button class="btn btn-ghost btn-sm" type="submit" title="删除这条">删除</button>
|
||||
</form>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
Reference in New Issue
Block a user