v32 第 32 批(4 条):资源共享弹框真落库 / 消息通知覆盖全部共享入口 / 浏览器通知(站内消息+日程到期提醒) / 首页任务随看板色

用户原话:
①「资源共享功能有问题,弹框中,点击共享给某个人后,弹框消失,但是并没有共享成功。」
②「消息通知,在所有引用自己的地方,都通知,包括别人分享了日历本给自己,
   分享了笔记给自己,分享了资源给自己等等。」
③「当有消息通知时,还有日历中日程到期提醒时,比如一个日程,设置了10点开始,
   提前十分钟提醒,则需要在 9:50 时浏览器中发提醒通知。」
④「首页,近期任务,任务的文字和前面的圆点,颜色并没有跟看板颜色一致,这个需要修正。」

四条真根因:
① v28 把资源编辑改成三页签后,每个页签各有自己的 form 与 <input name=id>,
   而设 id 的代码是 panel.querySelector() → 只命中文档序第一个(info 的 update),
   ⇒ share/att 表单的 id 恒空 ⇒ 提交 id=0 ⇒ Resource::find(familyId,0,me) 返回 null
   ⇒ 静默不共享。修:表单打 data-res-idform 标记 + querySelectorAll 逐个填。
② v30 只在 saveAttendees 后调 syncForTarget,6 个共享入口一个都没接。
   新增 Notification::syncShares(家庭,类型,宿主id,动作人) —— 照共享表现状重建差额
   (写表后调用;漏调只会没通知,不会状态不一致)。SHARE_KINDS 抽常量。
③ 完全没有浏览器通知。新增 Calendar::remindersBetween()(窗口留 -2 分钟防后台节流)
   + ?r=notifications/poll JSON 路由 + assets/js/notify.js(轮询 + Notification API
   + id@at 去重)+ 顶栏铃铛(权限只能用户点一下才要,自动申请会被 Chrome 静默拒绝)。
④ v30 把用户那句和主题颜色一样理解成了**任务模块主题色**,用户要的是**看板色**。
   新增 Task::boardColorById(家庭,看板id)(没设色按看板名派生稳定色),home.php 两处改 $tkColor。

验证:诊断探针 18/0、真机 25/0(真点共享→库里真的多一行;三类共享真发通知;
9:50 窗口真算出 10:00 那条;首页每条任务色 == 其看板色)、反向 5/0、
e2e 1502/0、升级 153/0、几何 428+680+92、城市 32/0。
部署脚本静态断言段本地空跑通过;线上站点自检 19 条全过,notifications/poll 未登录 302。
This commit is contained in:
2026-10-09 07:25:08 +08:00
parent be582569dd
commit df6fe894d0
8 changed files with 572 additions and 23 deletions
+21 -13
View File
@@ -369,17 +369,24 @@ unset($evRef);
<?php foreach ($tasks as $t): ?>
<?php $late = \App\Task::isOverdue($t['due_at'] ?? null); ?>
<?php
/* 🔴 v30 第 3 条:「首页,近期任务,任务的名称和前面的点,都应该和主题颜色
一样,都保持一致。」
用户说的"主题颜色"= **任务模块的主题色**(个人设置里可换的那个)。
⚠️ 原来圆点用的是 `var(--brand)` —— 那是**当前页面**(首页)的主题色,
不是任务模块的;两者在用户改过模块配色时会不一样
(首页 = 砖红,任务可能是别的色),正是用户看到的不一致。
⇒ 两条都用同一个 `--tk-c`(= Module::color('tasks')),
与右上角「全部任务」按钮的 `--hd-c` 同源,全块一个色。
⚠️ 逾期仍用红色:那是状态语义(要显眼),不能被主题色盖掉;
但改名后的那个点也统一走 `--tk-c`(不逾期时)。 */
$tkColor = \App\Module::color('tasks', (int)\App\Auth::familyId());
/* 🔴 v32 第 4 条(用户 2026-10-08):「首页,近期任务,任务的文字和前面的圆点,
颜色并没有跟**看板颜色**一致,这个需要修正。」
⚠️ v30 那轮我把它改成了 `--tk-c`(= **任务模块**的主题色),
当时理解成"用户说的主题色"。**理解错了** —— 用户要的是
「这条任务属于哪个**看板**」的颜色(任务模块自己早就支持
"给看板选颜色",用户原话在第 26 批:「任务在设置看板的时候,
可以选择颜色,这样在首页列表中显示的时候,就可以更加直观的
看到他属于哪个看板。」)。
⇒ 用 `Task::boardColorById(家庭, 看板id)`:
看板没设色时它按**看板名派生**一个稳定色(同名永远同色),
所以这里不需要任何回退分支。
⚠️ 逾期仍用红色(状态语义,不能被看板色盖掉)——
但那是"整行变红",与"没逾期时跟看板一致"并不冲突。
⚠️ 没归看板的(board_id 为空)走"默认看板"的派生色,
与任务模块列表页看到的完全一致(同一函数)。 */
$tkColor = \App\Task::boardColorById((int)\App\Auth::familyId(),
(int)($t['board_id'] ?? 0) ?: null);
?>
<li class="dash-row tk-row" data-task-row="<?= (int)$t['id'] ?>"
style="--tk-c: <?= e($tkColor) ?>;">
@@ -453,11 +460,12 @@ unset($evRef);
);
if (trim((string)$actorName) === '') { $actorName = '有人'; }
?>
<li class="ntf-row<?= empty($n['is_read']) ? ' is-unread' : '' ?>">
<li class="ntf-row ntf-row-<?= e((string)($n['kind'] ?? '')) ?><?= empty($n['is_read']) ? ' is-unread' : '' ?>">
<span class="ntf-kind" aria-hidden="true"><?= \App\Notification::icon((string)($n['kind'] ?? '')) ?></span>
<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'])) ?>
<b><?= e((string)$actorName) ?></b><?= e(\App\Notification::bodyWithoutActor((string)$n['title'])) ?>
</span>
<span class="ntf-time"><?= e(fb_time_ago((string)$n['created_at'])) ?></span>
</a>
+30
View File
@@ -90,6 +90,21 @@ $isLoggedIn = \App\Auth::check();
<?php endforeach; ?>
</nav>
<div class="topbar-right">
<?php /* 🔴 v32 第 3 条:浏览器提醒的**开关**(小铃铛)。
用户原话:「…则需要在 9:50 时浏览器中发提醒通知。」
⚠️ 浏览器**不会**允许"页面加载时自动申请通知权限"(Chrome 会直接静默拒绝,
并把域名记进"滥用"名单),所以权限必须由用户**点一下**才要。
⇒ 这里放一个按钮,由 assets/js/notify.js 绑定。
⚠️ 已经授权时(或浏览器不支持 Notification 时)按钮自动隐藏 ——
见 notify.js 的 paint()。所以未授权的人才会看到它。 */ ?>
<button type="button" class="btn btn-ghost btn-icon fb-notify-enable" hidden
title="开启浏览器提醒" aria-label="开启浏览器提醒">
<svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true" fill="none"
stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 7a4 4 0 0 1 8 0v3l1 2H3l1-2z"/>
<path d="M6.5 12.5a1.6 1.6 0 0 0 3 0"/>
</svg>
</button>
<?php /* 姓名 + 头像 = 一个下拉菜单,而不是「点名字改资料」+旁边两个裸链接。
裸链接在窄屏只能靠 display:none 藏起来(一藏入口就没了),
收进菜单之后同一个入口在手机上也点得到。
@@ -161,6 +176,21 @@ $isLoggedIn = \App\Auth::check();
</div>
<script src="<?= e(asset('/assets/js/app.js')) ?>"></script>
<?php /* 🔴 v32 第 3 条:浏览器通知(站内消息 + 日程到期提醒)。
⚠️ **只在已登录时加载** —— 登录页轮询会一直 302 去登录页,白跑。
同理也不在"免登录的导航展示页"上跑(那页没有会话)。
⚠️ 里面**不含**任何权限申请:权限必须由用户点小铃铛才要(见 notify.js 头部第 ①条)。 */ ?>
<?php if (\App\Auth::check()): ?>
<script>
window.FB_NOTIFY = {
poll: <?= json_encode(url('notifications/poll')) ?>,
list: <?= json_encode(url('notifications')) ?>,
icon: <?= json_encode(asset('/assets/icons/icon-96.png')) ?>,
canPush: <?= json_encode(\App\Auth::canWrite()) ?>
};
</script>
<script src="<?= e(asset('/assets/js/notify.js')) ?>" defer></script>
<?php endif; ?>
<?php /* v24 第 20 批:选人组件脚本改成**按需加载**(页面上真有 `.pp` 才拉)。
⚠️ 理由与 Cropper.js 同一套:`person_picker.js` 只在放选人组件的页面才有用,
而它并不小;每页都无条件带一份,等于让"没有选人组件"的页面白付流量。
+3 -3
View File
@@ -50,17 +50,17 @@ $items = $items ?? [];
// 图标:按 kind 分(观影 = 胶片,旅程 = 脚印,其余 = 铃铛)
$kind = (string)($n['kind'] ?? '');
?>
<li class="ntf-row<?= $isUnread ? ' is-unread' : '' ?>">
<li class="ntf-row ntf-row-<?= e($kind) ?><?= $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'])) ?>
<?= e(\App\Notification::bodyWithoutActor((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 ? '🧭' : '🔔') ?>
<?= \App\Notification::icon($kind) ?>
</span>
<form method="post" action="<?= e(url('notifications/del')) ?>" class="ntf-del">
<?= csrf_field() ?>
+32 -5
View File
@@ -325,7 +325,7 @@ $myPerson = (int)\App\Auth::personId();
<?php /* ---- 页签 ① 信息 ---- */ ?>
<div class="res-tabpage" data-res-page="info">
<form method="post" action="<?= e(url('resources/update')) ?>">
<form method="post" data-res-idform action="<?= e(url('resources/update')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<div class="res-pb">
@@ -367,7 +367,7 @@ $myPerson = (int)\App\Auth::personId();
<div class="rd-att-list" id="resAttNow"></div>
</div>
<form method="post" enctype="multipart/form-data"
action="<?= e(url('resources/attach_add')) ?>" id="resAttForm">
action="<?= e(url('resources/attach_add')) ?>" id="resAttForm" data-res-idform>
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<h3 class="res-sub">添加附件</h3>
@@ -387,7 +387,7 @@ $myPerson = (int)\App\Auth::personId();
<?php /* ---- 页签 ③ 共享(原「共享」按钮那整套搬进来)---- */ ?>
<div class="res-tabpage" data-res-page="share" hidden>
<div class="res-pb">
<form method="post" action="<?= e(url('resources/share')) ?>">
<form method="post" action="<?= e(url('resources/share')) ?>" data-res-idform>
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<div class="field">
@@ -422,7 +422,7 @@ $myPerson = (int)\App\Auth::personId();
</div>
</div>
<?php /* 取消共享:唯一的一份表单,脚本填两个隐藏字段后提交。 */ ?>
<form method="post" action="<?= e(url('resources/unshare')) ?>" id="resUnshareForm" hidden>
<form method="post" action="<?= e(url('resources/unshare')) ?>" id="resUnshareForm" data-res-idform hidden>
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
<input type="hidden" name="person_id" value="">
@@ -430,7 +430,7 @@ $myPerson = (int)\App\Auth::personId();
</div>
<?php /* 删附件:唯一的一份表单,脚本填 id 后提交。 */ ?>
<form method="post" action="<?= e(url('resources/attach_del')) ?>" id="resAttDelForm" hidden>
<form method="post" action="<?= e(url('resources/attach_del')) ?>" id="resAttDelForm" data-res-idform hidden>
<?= csrf_field() ?>
<input type="hidden" name="id" value="">
</form>
@@ -586,6 +586,33 @@ $myPerson = (int)\App\Auth::personId();
var panel = panels[which];
if (!panel) { return; }
/* 🔴 v32 第 1 条真根因(用户:「弹框中,点击共享给某个人后,弹框消失,
但是并没有共享成功。」):
编辑面板现在是**三个页签**,每个页签有**自己的** `<form>` 和
**自己的** `<input name="id">`(info=resources/update、att=attach_add、
share=resources/share),面板末尾还有两个"唯一表单"(#resUnshareForm /
#resAttDelForm,也都带 name="id")。
而 v28 把三者并成一个面板时,这里写的是:
var idInp = panel.querySelector('input[name="id"]');
idInp.value = id; // ← 只填了**第一个**
`querySelector` 返回**文档序里第一个**(info 那个 / resources/update),
于是 att 与 **share** 两个页签的表单 `id` **一直是空** ⇒ 提交 `id=0` ⇒
后端 `Resource::find($familyId, 0, $me)` 返回 null ⇒ **静默不共享**
(只在列表页闪一句"这条资源不能共享。",用户多半没注意)。
⇒ 改成把 id 填进**这一屏里每一个会提交的表单**。
⚠️ 别写成"只填当前页签那份":用户会在弹框里切页签,
数据一次灌好、切页签就是纯显示切换(与上面注释同一取舍)。
⚠️⚠️ 判据用**显式标记**(`[data-res-idform]`),**不要**按 action 匹配:
`url()` 走 http_build_query,斜杠是 URL 编码的
(实测 `action="index.php?r=resources%2Fupdate"`),
写 `action*="resources/update"` 一条都匹配不上 —— 看着"改了",其实没改。 */
Array.prototype.forEach.call(
panel.querySelectorAll('form[data-res-idform]'),
function (f) {
var i = f.querySelector('input[name="id"]');
if (i) { i.value = id; }
});
// 每个面板都有的两样:标题回显 + 宿主 id
var tt = panel.querySelector('.rd-title');
if (tt) { tt.textContent = title; }