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:
+92
-10
@@ -3087,8 +3087,12 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
|
||||
.tg-col.is-today { background: #FDF6F1; }
|
||||
.tg-line { display: block; height: 44px; border-top: 1px solid var(--line-2); box-sizing: border-box; }
|
||||
.tg-line:first-child { border-top: 0; }
|
||||
/* ⚠️ v30 第 4 条:`left` 与 `width` 现在由**内联样式**给出(冲突分列后每条不同),
|
||||
所以这里**不能**再写 `left/right` —— 绝对定位下 left+right+width 三者同时存在是
|
||||
"过约束",浏览器会丢掉 `right`(看着像是碰巧对)。留一个兜底 left:2px,
|
||||
万一某条没带内联 left(老数据/兜底路径)也不至于跑到列外去。 */
|
||||
.tg-block {
|
||||
position: absolute; left: 2px; right: 2px;
|
||||
position: absolute; left: 2px;
|
||||
display: block; overflow: hidden; padding: 2px 5px;
|
||||
border-radius: 4px; color: #fff; font-size: 11px; line-height: 1.35;
|
||||
text-decoration: none; box-sizing: border-box;
|
||||
@@ -3902,6 +3906,18 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b
|
||||
}
|
||||
.upbox-ico { font-size: 22px; line-height: 1; font-weight: 300; }
|
||||
.upbox-msg { font-size: 13.5px; }
|
||||
/* 🔴 v30 第 1 条:「选中文件后,那个文件输入框,得有点反应才对。」
|
||||
非暂存框(导入型)选中文件后由 app.js 的 bindFileEcho() 加 `.is-picked`
|
||||
并把框里的话换成文件名 —— 这里把它从"虚线待放"变成"实线已选",
|
||||
配色也换成品牌色的浅底,一眼能看出"这一步已经做完了"。
|
||||
⚠️ 与 `.is-over`(拖动经过的高亮)区分开:那个是**临时**高亮,
|
||||
这个是**已选**的稳定态,两者同时命中时以 is-picked 为准(写在后面)。 */
|
||||
.upbox.is-picked {
|
||||
border-style: solid; border-color: var(--brand);
|
||||
background: #FDF3EC; color: var(--brand-ink, var(--brand));
|
||||
}
|
||||
.upbox.is-picked .upbox-ico { font-size: 18px; font-weight: 600; }
|
||||
.upbox.is-picked .upbox-msg { font-weight: 600; word-break: break-all; }
|
||||
/* 窄屏:框再矮一点,别把表单撑太长 */
|
||||
@media (max-width: 480px) {
|
||||
.upbox { min-height: 78px; padding: 12px; }
|
||||
@@ -4260,17 +4276,28 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b
|
||||
border-radius: 50%; background: var(--red);
|
||||
}
|
||||
/* 日程块 */
|
||||
/* 🔴 v30 第 2 条:「首页上的今天的日程,用色块表示,和日程保持一致。」
|
||||
⇒ 从原来的"白底卡片 + 左边 3px 色线"改成**与日历页 `.tg-block` 同款的实心色块**:
|
||||
底色 = 日历本色(内联给)、白字、圆角、无边框阴影。
|
||||
⚠️ `left` 与 `width` 由**内联样式**给出(冲突分列的结果,见 templates/home.php)——
|
||||
这里不再写 left/right(绝对定位下 left+right+width 三者同时存在是过约束)。
|
||||
⚠️ `transform: translateY(2px)` 那点微调去掉了:色块要精确落在刻度上,
|
||||
偏移 2px 会让"9:00 的块"看起来不在 9:00 那条线上。
|
||||
⚠️ 白字压浅色本的兜底:加一层极轻的 text-shadow(不加深色罩,保持本色)。 */
|
||||
.ag-ev {
|
||||
position: absolute; left: 46px; right: 4px; display: flex; align-items: center; gap: 8px;
|
||||
padding: 5px 9px; min-height: 30px;
|
||||
background: var(--surface); border: 1px solid var(--line);
|
||||
border-left: 3px solid var(--brand); border-radius: var(--radius-s);
|
||||
box-shadow: var(--shadow); font-size: 13.5px; color: var(--ink);
|
||||
transform: translateY(2px);
|
||||
position: absolute; left: 46px; right: 4px;
|
||||
display: block; overflow: hidden; padding: 3px 7px;
|
||||
background: var(--brand); border-radius: 4px;
|
||||
font-size: 12px; line-height: 1.35; color: #fff;
|
||||
text-decoration: none; box-sizing: border-box;
|
||||
border: 1px solid rgba(255,255,255,.35);
|
||||
text-shadow: 0 1px 1px rgba(0,0,0,.18);
|
||||
}
|
||||
.ag-ev:hover { box-shadow: var(--shadow-l); }
|
||||
.ag-ev .ag-ev-t { flex: 0 0 auto; font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
||||
.ag-ev .ag-ev-w { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ag-ev:hover { text-decoration: none; filter: brightness(1.08); z-index: 2; }
|
||||
.ag-ev .ag-ev-t { display: block; font-weight: 600; font-variant-numeric: tabular-nums; margin-right: 4px; float: left; }
|
||||
.ag-ev .ag-ev-w { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
|
||||
/* 日历本名:色块右下角的极淡小字(色块面积小,不加大徽标) */
|
||||
.ag-ev .ag-ev-cal { display: block; font-size: 10.5px; opacity: .82; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* ---- 任务行:标题 + 状态 + **看板名** + 截止 ---- */
|
||||
.tk-list { display: flex; flex-direction: column; }
|
||||
@@ -4983,3 +5010,58 @@ h5.md-h, h6.md-h { font-size: 14px; }
|
||||
.note-img-size.is-on { border-color: var(--brand); background: var(--brand-soft, #FDF3EC); color: var(--brand-ink); }
|
||||
/* 图上标着当前档位,编辑器里一眼能看出"这张被缩过了" */
|
||||
.note-block[data-kind="image"] .note-block-img { transition: width .12s ease; }
|
||||
|
||||
/* ============================================================
|
||||
通知消息(v30 第 5 条)
|
||||
------------------------------------------------------------
|
||||
用户原话:「引入通知模块,这个模块,不在导航显示,在个人头像点击那里显示,
|
||||
放在个人设置前面,名字:通知消息。」
|
||||
|
||||
⚠️ 三处共用这一套样式:头像下拉里的入口、?r=notifications 页、首页那一块。
|
||||
分开写三遍迟早长歪(本项目在"设置下拉"上刚吃过这个亏)。
|
||||
⚠️ 未读用**左侧一条品牌色竖线 + 浅底**表达,不用加粗字重 ——
|
||||
字重变化会让整行宽度轻微跳动,一列通知看下来眼睛累。
|
||||
============================================================ */
|
||||
/* 头像下拉里的入口:文案 + 未读角标(角标推到最右) */
|
||||
.menu-list a:has(.menu-badge),
|
||||
.menu-list a .menu-badge {
|
||||
display: inline-flex; align-items: center;
|
||||
}
|
||||
.menu-list a .menu-badge {
|
||||
margin-left: auto; min-width: 18px; height: 18px; padding: 0 5px;
|
||||
border-radius: 999px; background: var(--red); color: #fff;
|
||||
font-size: 11px; line-height: 18px; justify-content: center; font-weight: 600;
|
||||
}
|
||||
|
||||
.ntf-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||
.ntf-row {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 8px 10px; border-radius: var(--radius-s);
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.ntf-row:hover { background: var(--line-2); }
|
||||
/* 未读:左侧品牌色竖线 + 极浅底(见上面的说明:不用字重) */
|
||||
.ntf-row.is-unread { border-left-color: var(--brand); background: var(--brand-soft, #FDF6F1); }
|
||||
.ntf-av {
|
||||
flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%;
|
||||
object-fit: cover; background: var(--line-2);
|
||||
}
|
||||
.ntf-main {
|
||||
flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 10px;
|
||||
color: var(--ink); text-decoration: none;
|
||||
}
|
||||
.ntf-main:hover { text-decoration: none; color: var(--brand); }
|
||||
.ntf-title { flex: 1 1 auto; min-width: 0; font-size: 14px; overflow: hidden;
|
||||
text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ntf-title b { font-weight: 600; margin-right: 2px; }
|
||||
.ntf-time { flex: 0 0 auto; font-size: 12px; color: var(--faint); font-variant-numeric: tabular-nums; }
|
||||
.ntf-kind { flex: 0 0 auto; font-size: 15px; line-height: 1; }
|
||||
.ntf-del { flex: 0 0 auto; }
|
||||
/* 首页那块:行距紧一点(它只是"瞥一眼",不是消息中心) */
|
||||
.ntf-list-home .ntf-row { padding: 6px 8px; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
/* 窄屏把时间挪到下一行,别把标题挤成两三个字 */
|
||||
.ntf-main { flex-wrap: wrap; gap: 2px 10px; }
|
||||
.ntf-title { flex: 1 1 100%; }
|
||||
}
|
||||
|
||||
+55
-1
@@ -657,6 +657,60 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 「选中文件后要有反应」—— v30 用户第 1 条。
|
||||
*
|
||||
* 用户原话:「导入日程,选中文件后,那个地方没有任何反应,虽然能导入,
|
||||
* 但是体验不好,选中文件后,那个文件输入框,得有点反应才对。」
|
||||
*
|
||||
* 根因:**非暂存**的上传框(导入型:ics / 通讯录)在选中文件后**什么都不显示** ——
|
||||
* 暂存型框靠 queueDrop() 画缩略图卡片,而导入型只是把 File 写进 `<input>`,
|
||||
* 框里那句"点击或拖动进行上传"从头到尾不变,用户完全看不出"到底选上没有"。
|
||||
* ⇒ 这里给所有"指向某个 file input 的框"补一条 change 监听:
|
||||
* 选到文件就把框里的话换成**文件名**(+ 可点"重选"),选不到(取消)就还原。
|
||||
*
|
||||
* ⚠️ 只认 `data-sd-for` 指向的那些框(片段自己声明的),不碰别的上传区 ——
|
||||
* 暂存型框有自己的清单 UI,这里插一脚会和它打架。
|
||||
* ⚠️ 用**原始**文案做还原基准(`data-sd-msg`):用户点了重选又取消时,
|
||||
* 框里那句话必须回到原文,不能变成空字符串。
|
||||
*/
|
||||
function bindFileEcho(box, input) {
|
||||
if (!box || !input || box.getAttribute('data-sd-echo') === '1') { return; }
|
||||
// ⚠️ **只给非暂存框**挂(暂存型框由 queueDrop 画缩略图卡片,别去抢它的 UI)。
|
||||
// 暂存型框带 data-drop-stage;导入型不带(见 _stage_drop.php 头部的 v30 说明)。
|
||||
if (box.hasAttribute('data-drop-stage')) { return; }
|
||||
box.setAttribute('data-sd-echo', '1');
|
||||
// 记下原始文案(片段渲染的那句「点击或拖动进行上传」或 $sdMsg)
|
||||
if (!box.hasAttribute('data-sd-msg')) {
|
||||
var msgEl = box.querySelector('.upbox-msg');
|
||||
box.setAttribute('data-sd-msg', msgEl ? msgEl.textContent : '');
|
||||
}
|
||||
var msgEl2 = box.querySelector('.upbox-msg');
|
||||
var icoEl = box.querySelector('.upbox-ico');
|
||||
function paint() {
|
||||
var f = input.files && input.files[0];
|
||||
var list = input.files ? Array.prototype.slice.call(input.files) : [];
|
||||
if (!list.length) {
|
||||
// 没选(或用户取消了)→ 还原成原始提示
|
||||
box.classList.remove('is-picked');
|
||||
if (msgEl2) { msgEl2.textContent = box.getAttribute('data-sd-msg'); }
|
||||
if (icoEl) { icoEl.textContent = '+'; }
|
||||
return;
|
||||
}
|
||||
box.classList.add('is-picked');
|
||||
if (icoEl) { icoEl.textContent = '✓'; }
|
||||
if (msgEl2) {
|
||||
// 单选直接给文件名;多选给"第一个 + 等 N 个"
|
||||
msgEl2.textContent = list.length === 1
|
||||
? ('已选择:' + (f.name || '文件') + '(点这里可重选)')
|
||||
: ('已选择 ' + list.length + ' 个文件:' + (f.name || '') + ' 等(点这里可重选)');
|
||||
}
|
||||
}
|
||||
input.addEventListener('change', paint);
|
||||
// 拖进来的文件也会派发 change(见 bindZone),所以这一条同时覆盖"点选"和"拖入"
|
||||
paint();
|
||||
}
|
||||
|
||||
/** 找"属于这个 input 的上传框/预览图",逐个挂上 */
|
||||
function bindInput(input) {
|
||||
bindZone(input, input);
|
||||
@@ -675,7 +729,7 @@
|
||||
if (input.id) {
|
||||
Array.prototype.forEach.call(
|
||||
document.querySelectorAll('[data-sd-for="' + input.id + '"]'),
|
||||
function (bx) { bindZone(bx, input); bindBox(bx, input); });
|
||||
function (bx) { bindZone(bx, input); bindBox(bx, input); bindFileEcho(bx, input); });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user