laoyang
|
cb846d378f
|
评论附件支持拖拽 + 首页卡片同步标题 (#79)
用户 2026-11-05:
「评论框下面的附件也需要可以拖动添加。」
「首页卡片没有刷新。」
① 评论附件拖拽
给 `.cmt-attach-row` 挂 `data-cmt-drop`,由 task_panel.js 接管
dragover / dragenter / dragleave / drop。
🔴 核心技术点:`<input type=file>.files` 是**只读**的,
不能直接 `input.files = event.dataTransfer.files`(某些内核静默失败)。
必须用 DataTransfer 重建 FileList 再赋回去:
var dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files; ← 这个赋值是允许的
而且**必须真的赋给 input** —— 否则只是"看起来加上了",
表单提交时浏览器一个文件都不带。
⚠️ 累加语义:用户可能先拖一张再拖一张,所以每次把 input.files
现有内容 + 新拖入的合并后写回。
⚠️ 去重:按 name+size+lastModified 判重,否则"只拖一次却传上去两张一样的"。
⚠️ dragover 必须 preventDefault(不拦的话浏览器把文件当"打开",
直接跳走、当前页面全丢)。
⚠️ 与「任务附件」那条 `[data-tk-drop-box]` 是**两条独立的路**:
那个是"点了/拖了立刻上传"(任务已存在),
这个是"挂到评论表单、点发表才一起提交"。
实测(Playwright + 真实 DataTransfer):
拖入 a.txt+b.png => count=2 ✅
再拖 c.jpg => count=3 ✅ 累加
重复拖 a.txt => count=3 ✅ 去重
FormData(form) => ["a.txt","b.png","c.jpg"] ✅ 提交时真会传
② 首页卡片同步标题
根因:`syncRow()` 原来**只管状态徽标和复选框**,代码第一行就是
`if (!d || !d.status) return;`,压根不碰标题。
而 #78 之后面板里能改标题了 ⇒ 改完点保存,卡片上还是旧标题。
改两处:
· `task_panel_json()` 返回值补 `title`(服务端直接给,不依赖 DOM 还在)
· `syncRow()` 补标题同步:`.dash-what` 的文本 + `title` 属性
(`title` 是悬停提示,不同步会出现"悬停是旧标题"的怪事),
并把 `data-fb-entry` 里的 JSON title 一并改掉
(否则"卡片上是新标题、再点开浮层显示旧标题")。
读不到 d.title 时兜底从 `#tkp-title` 输入框读。
|
2026-11-05 18:32:40 +08:00 |
|
laoyang
|
11aa7c6ca5
|
任务面板五项细化:弹窗选人、协作人、保存关窗、去编辑、评论附件 (#78)
用户 2026-11-05 五条:
「选人的时候不能按列表,因为人太多了。参考观影模式里面选人的方式。」
「任务其实可以共享给别人,也就是协作人,但编辑这里面没有这个选项。」
「点击保存之后,按道理说应该直接把这个窗口关闭才对呀。」
「现在在首页上点一个任务,直接就是编辑页面啊,所以不需要编辑按钮了。」
「评论那个地方也需要可以添加附件。」
① 选人改弹窗
负责人/协作人都换成 `_person_picker.php`(弹窗 + 拼音首字母联想),
与观影表单、新建任务弹框同一套。
负责人 single + allowEmpty(可选「不指定」);协作人 multi。
⚠️ 关键修复:`entry_popup.js` 的 `loadPanel()` 一直漏了
`fbInitPickers()` —— 组件不初始化,按钮点了没反应。
`loadEdit()` 里有这一句,`loadPanel()` 没有,面板里塞选人就是这个下场。
同时 `task_panel.js` 的 `paint()` 后也补一次(重画后是新节点)。
② 补协作人
数据层其实**早就完整支持**:task_shares 表 + Task::setShare/dropShare
+ 新建弹框的「共享给」+ 详情页的共享展示。只有面板和编辑表单没给入口。
现在面板里能看当前协作人、能加、能改。
保存语义:以提交的列表为准同步(多了就 setShare、少了就 dropShare)。
⚠️ 带 `shares_submitted=1` 哨兵,区分"表单里没这个控件"(不动)
和"用户全删了"(清空)—— 否则 PHP 收不到空数组的键,无法分辨。
③ 保存后关窗
`act()` 加 `closeOnOk`,`send()` 支持它:成功后先 `syncRow()`
(更新背后那张首页卡片,否则关掉浮层看到旧标题更迷惑)再 `fbEntryClose()`。
⚠️ 只给「保存」用 —— 改状态/挪看板/加子任务/发评论/传附件都还要继续操作。
⚠️ 失败时不关,让用户看到错误就地改。
④ 去掉「编辑」按钮 + 面板补标题/备注
用户说得对:点开任务就是完整编辑面板了,再给编辑按钮是重复。
但原来面板改不了标题和备注(只在 _edit_form.php 里)
⇒ 先在「更多信息」里补上这两个字段,再藏编辑按钮。
`Task::setAttrs()` 扩展支持 title/note,走 title()/textOrNull() 清洗,
与 update() 同一口径。
⚠️ 只对**任务**(带 data.panel)隐藏 —— 观影/旅程/日程/纪念日仍需要那颗按钮。
⑤ 评论可加附件
服务端**早就支持**:tasks/comment_add 里一直有
`attach_save_files(..., task_comment, ...)`,只是表单里没有 file input。
补上 `<input type="file" name="attach[]" multiple>` +
`enctype="multipart/form-data"`(少了它只提交文件名、文件本体丢失)。
顺带允许「只发附件不写字」(原来 content 空会返回"留言是空的"):
有附件且没文字时用占位文案「(附件)」。
实测:
setAttrs 标题/备注 => 都正确写入 ✅
setShare / dropShare => 1 人 / 0 人 ✅
面板渲染 => 标题框/备注框/两个选人组件(single+multi)/评论附件/
multipart/save-attrs/antd 控件 全部在位 ✅
选人模式 => single(allowEmpty=1), multi ✅
已知问题(另单处理):家庭有 800 人,两个选人组件各 485KB,
面板渲染 997KB。服务端只要 11ms,但传输量对移动端不友好。
这是 _person_picker.php 的既有设计(观影那边也一样),
需要改成"弹窗打开时才渲染列表"或按需加载。
|
2026-11-05 18:28:15 +08:00 |
|
laoyang
|
aac7dd5974
|
修截止时间时/分保存不上:保存前替用户点 antd 的「确定」(#77)
用户给的方法很对:「看一下数据库里有没有记时和分。如果没有记,
就说明是修改它的时候出了问题。」查库结果:
tasks #15 [台式机采购]
due_at = 2026-11-07 00:00:00
updated = 2026-11-05 18:04:05 ← 正是用户操作的时间点
⇒ 时分在**写入时**就丢了,不是显示问题。
用 Playwright + 站点真实 antd 5 逐帧实测,找到决定性证据:
选完日期/时间(不点弹层里的「确定」):
vis="2026-11-16 20:34" onChange 次数 = 0
点面板的「保存」(这一下同时把弹层关掉):
50ms 后 readDue = "2026-11-07 00:00:00" ← **弹回原值!**
onChange 次数仍然是 0
根因:antd 的 DatePicker(showTime) **只有点它内部的「确定」才提交**。
点别处(包括点我们的保存按钮)会被当成取消,值弹回上一次确认的值。
「台式机采购」原本就是 2026-11-07 00:00:00,用户改完时间直接点保存,
antd 把修改丢了,于是存进去的还是原来的 00:00:00。
修法(三步,缺一不可):
① app.js 的 writeBack():把 onChange 拿到的字符串写到
box[data-fb-dt-value]。这是"控件刚确认的值",不经镜像、不受弹层影响。
② task_panel.js 新增 commitAntd():保存前主动点掉弹层「确定」。
只在弹层真开着时点(:not(.ant-picker-dropdown-hidden))。
③ 读值改为**异步**:commitAntd() 之后等 60ms 再 readDue()。
antd 的 onChange → setState → 重渲染落在微任务/下一帧,
同步读拿到的是提交前那一帧(实测差几分钟,或干脆是旧值)。
readDue 的优先级也理顺了:
① data-fb-dt-value(onChange 直挂,最可靠)
② antd 可见框(降级兜底)
③ 原生隐藏字段(antd 没挂上时)
④ 都没有 → 空(清掉截止时间)
实测(复刻完整流程:选日期+20时+30分,不点确定,直接点保存):
保存读到 = "2026-11-16 20:30:00" ✅
onChange 次数 = 1
dataset="2026-11-16 20:30" h=20 m=30 vis="2026-11-16 20:30"
服务端侧确认无需改动:Task::dtOrNull() 对
YYYY-MM-DD HH:mm[:ss] / YYYY-MM-DD 都能正确补全,
只有带 T 分隔的会返回 null(那是整条清空,不是时分丢失)。
|
2026-11-05 18:19:46 +08:00 |
|
laoyang
|
20ba36ad62
|
修截止时间时/分保存不上:改为读 AntD 可见框 (#77)
用户 2026-11-05:「截止日期的时间保存有bug,时和分没保存上。」
根因:save-attrs 原来直接读 .dt-hour / .dt-minute 的 value。
那两个 <select> 是 AntD 挂上后**被 CSS 收起**的镜像(.dt-input.is-antd
> .dt-native { display:none }),它们的值依赖 mountAntdSingleDT 的
writeBack() 写回 —— 而 writeBack 走 writeDT(box, str),str 来自 antd
的 onChange 第二参数。
用 Playwright + 站点真实 app.js/antd 实测发现问题:
· 只给 select 放一个 option 时,writeDT 写入不存在的值 → value 变空串;
· 时序上(首屏挂载 / 面板 AJAX 换入 / 受控重渲染)隐藏字段可能滞后于
用户看到的那个值。
⇒ 读隐藏字段本质上是"读镜像",镜像一旦滞后或缺失,时分就丢。
修法:新增 readDue(),按优先级读:
① AntD 的可见输入框 .ant-picker-input > input(用户实际看到/改的那一个,
格式 YYYY-MM-DD HH:mm)—— 这是唯一权威来源;
② 没有 AntD(降级成原生下拉)时才读 .dt-date/.dt-hour/.dt-minute;
③ 都没有 → 空(= 清掉截止时间)。
AntD 的框是只读文本框(用户通过弹层选,不能直接打字),
所以解析它的 text 不会碰到"敲了半截日期"的中间态。
实测(Playwright 复刻完整 mountAntdSingleDT + readDue):
AntD 可见框 = 2026-10-16 20:30
隐藏 hour/min = 20 / 30
readDue 结果 = 2026-10-16 20:30:00 ✅
服务端侧已确认无需改动:Task::dtOrNull 会把
YYYY-MM-DD HH:mm 补秒成 YYYY-MM-DD HH:mm:ss,
YYYY-MM-DD 补成 00:00:00,格式非法才回 null。
|
2026-11-05 18:12:44 +08:00 |
|
laoyang
|
9ab7d40a3b
|
任务面板:加保存按钮,负责人和分类改为可编辑 (#76)
用户 2026-11-05:「时间控件是对的,但是现在没有保存按钮,只有下面一个
评论的发表按钮。另外,负责人和分类都没法修改,按道理讲都应该可以修改才对。」
三处改动:
① 取消"立即保存",改成显式「保存」按钮
上一版(#74)做的是"选完截止时间就发请求"。用户不认可:他要的是
"改完点一下保存"那种确定感。
⇒ 移除 bindDue() 的 change 监听,改成 save-attrs 动作一次性提交三项。
② 负责人 / 分类 从只读改成可编辑
改前是只读 dl.kv 行,而且**值为空时整行不渲染**
(if (!empty($tpT[assignee_name])) / if ($tpCatName !== ))
⇒ 用户不仅改不了,连"这一项存在"都看不出来。
现在三项合成「更多信息」小节:
截止 [ AntD 日期控件 ]
负责人 [ 下拉 ]
分类 [ 下拉 ]
[ 保存 ]
负责人用原生 select 而不是 _person_picker.php 的弹窗选人:
面板已在浮层里,再弹一层模态 = 两层遮罩,手机上容易点错;
且 picker 需要 fbInitPickers() 初始化,loadPanel() 目前没调它。
家里 3~10 人 select 够用;人多了再换,那时记得把 fbInitPickers
也补进 loadPanel()。
③ Task::setAttrs()(新增)
只更新 due_at / assignee_id / category_id 三列。
不复用 update():那是全字段覆盖,面板只交三项的话
title/note/status/repeat/parent/board 会被默认值抹空
—— 与 setDue() 记的是同一个坑。
用 array_key_exists 判"传没传",这样可以只改其中一项
(面板在"没有成员"等极端情况下可能不渲染负责人下拉,
那时不该把它当成 0 去清掉原值)。
路由 tasks/panel_save:只把表单**真的提交了**的字段放进 $sd,
与 status/move 同一套约定(ajax_panel=1 回整块新面板)。
清理:bindDue / window.fbTaskPanelBindDue 全部移除
(entry_popup.js 里的调用、task_panel.js 里的定义与导出)
—— 留着会引用未定义函数,直接抛错。
实测:
三项一起改 => due_at/assignee/category 全对 ✅
标题/状态/备注/看板 均未被破坏 ✅
只改 due_at => 另两项保留、未被清空 ✅
面板渲染 => save-url / 保存按钮 / 两个下拉 / AntD 控件 /
原有状态/附件/评论 全部在位 ✅
|
2026-11-05 18:07:17 +08:00 |
|
laoyang
|
7a108396ab
|
任务面板首次打开时截止控件没升级成 AntD (#75)
用户:「空间有了,但是不是我想要的空间,这个空间需要换。」
经确认「空间」=「控件」,现象是五个原生下拉(年/月/日/时/分),
期望 AntD 弹层日历。
真根因:首次打开面板走 entry_popup.js 的 loadPanel(),
它只 innerHTML + runInlineScripts,**没有补挂 AntD**。
上一条 #74 我在 task_panel.js 的 paint() 里加了 fbBootAntd(),
但 paint() 只在"面板里做了动作之后重画"时才跑
(点状态/挪看板/加子任务/发评论/传附件 → act() → 回来 paint())。
**首次打开不经过 paint()** ⇒ 用户第一眼看到的是从未升级过的原生下拉。
(先点一下「已完成」再点回来就会看到 AntD 挂上了 —— 这种"时好时坏"最迷惑。)
为什么 bootAntd 首屏也没兜住:
if (!hosts.length && !dateRanges.length && !dates.length && !singles.length) return;
首页首屏任务卡里没有 [data-dt-antd-single](那控件要打开面板才有)
⇒ 首屏调用时直接 return。
修法:
① entry_popup.js loadPanel():插入 HTML 后调 fbBootAntd()
② 顺带修第二个 bug:bindDue() 原来也只在 paint() 里调,
首次打开不绑定 ⇒ 选完截止时间 change 没人听 ⇒ **不会保存**。
⇒ 把 bindDue 暴露成 window.fbTaskPanelBindDue,loadPanel 显式调一次。
加载顺序已确认:两者都是 defer,entry_popup.js(253) 在前、
task_panel.js(268) 在后 ⇒ task_panel 先执行完并注册好
window.fbTaskPanelBindDue,之后 loadPanel 才在用户点击时调用。
不存在"调的时候还没定义"。
antd 样式确认:vendor 目录只有 reset.css(antd 5 是 CSS-in-JS 运行时注入),
不存在"缺样式导致看起来像原生下拉"的可能。
|
2026-11-05 17:59:17 +08:00 |
|
laoyang
|
836f6f1d2f
|
首页任务面板:可直接改截止时间 (#74)
用户 2026-11-05:「点开之后,就可以修改任务的所有信息,包括:
1. 上传附件;2. 添加评论;3. 添加子任务;4. 修改截止时间
(已有值要带进来);5. 改状态、挪看板。这些操作全部都在这一个页面完成。」
盘点发现:附件 / 评论 / 子任务 / 状态 / 看板 **本来就有**(_panel.php 里
早就实现了),只有「截止时间」是只读的一行 dd。所以只补这一项。
四处改动:
① Task::setDue()(新增)
不复用 update():那是全字段覆盖(title/note/status/assignee/repeat/
parent/board/category 一起写)。表单只交 due_at 的话,其余字段会被
默认值抹成空 ——「只想改个日期,结果标题和备注没了」,不可恢复。
⇒ 单独一条只写 due_at 的 UPDATE。
② tasks/due 路由(新增,紧跟 tasks/status)
与 status/move 同一套约定:ajax_panel=1 回整块新面板,否则 flash+302。
空串 = 清掉截止时间。
③ _panel.php:只读「截止」行 → AntD 单点控件
原来:
<?php if ($tpDue !== ): ?>…<dd>截止 …</dd>…<?php endif; ?>
**没有截止时间时整行都不显示** —— 用户根本不知道该去哪填。
现在不管有没有值都画控件,$dtValue = $tpDue 自动带出已有值。
有值且在逾期时,右侧补一行「还剩 N 天」小字(.tk-due-word)。
④ task_panel.js
· paint() 后调 fbBootAntd():面板是 AJAX 换进来的,bootAntd 首屏
扫过一次 [data-dt-antd-single],那时面板还不存在 ⇒ 必须补挂。
· bindDue():监听 .dt-input 的 change 立即保存。
不用按钮式 data-tk-act(面板里其它动作都是"点一下/选完就生效",
再让用户点一次保存会与其它处不一致)。
监听隐藏字段的 change 而不是 AntD 回调 ⇒ 覆盖 AntD 与原生下拉两种形态。
data-fb-due-bound 幂等标记防止重复绑监听(否则改一次发好几条请求)。
⑤ CSS:.tk-sec-inline > .dt-input 加 flex/min-width/max-width ——
.dt-input 是 flex-wrap,放进也是 flex-wrap 的 .tk-sec-inline 里
会被当弹性项按内容撑,窄屏上可能把标签挤走或控件溢出。
实测:
setDue 设值 => [2026-12-25 18:30:00] ✅
标题仍为原值 ✅ 没被清空(证明没用 update())
setDue 清空 => NULL ✅
面板渲染:data-tk-url-due / data-dt-antd-single / due_at_date 全在,
带出的日期值 = [2026-10-22] ✅
状态/附件/子任务/评论 均仍在
|
2026-11-05 17:44:11 +08:00 |
|
laoyang
|
4941e9db9c
|
fix(home): 从浮层把任务标成完成时该行不消失(只修了复选框那条路)(#20)
用户第二次提同一条需求。核了线上代码:4 文件 md5 与本地一致、mtime 18:14,
部署没问题;复跑原判据仍 20/0 全绿。判据绿 + 用户能复现 ⇒ 判据有盲区。
盲区:首页把任务标成完成有**两个入口**
① 每行前面那颗复选框 —— home.php 末尾的脚本(上一版已修)
② 点整行弹浮层 → 面板里「状态」三段按钮 → assets/js/task_panel.js 的 syncRow()
上一版只改了 ①。syncRow() 只改徽章与复选框的 aria 状态,**从不删行**,
所以从面板点「已完成」后行留在原地,只把徽标换成「已完成」。
用户点的就是 ②,而原判据 grep 全文没有 panel / 浮层 / data-fb-entry —— 完全没测这条路。
改法:
- 把删行逻辑抽成 window.fbHomeTaskDrop(row, wasStatus) 放 assets/js/app.js,
两个入口共用一份。作用域限定 [data-tk-home-host]:任务**看板**页的行也带
data-task-row,那里勾完成只改状态、**必须保留该行**(看板按状态分栏,删了就没处看)。
- task_panel.js 的 syncRow() 在 d.status === 'done' 时调它;
wasStatus 必须在改写 data-tk-status **之前**读,否则概述卡不知道该减「在做」还是「待办」。
- home.php 原来那份本地实现改成薄转发,避免两份实现改一处忘一处。
- 顺带修掉一条说谎的注释:原注释写"补偿了一个可撤回的提示",但代码里从来没有
撤回按钮,只有一句 3.2 秒自动消失的 say()。
- _card_tasks.php 补记"两个入口"这件事。
子任务安全:task_panel_json() 用 panel_of 找**父任务**,回的 status 是父任务的,
所以面板里勾子任务不会把父任务那一行删掉(判据 ⑱ 专门守这条)。
判据:
- 新增 _b38_panel_done_drop.py(21 条,走真实浮层点击):
修复前 **16 通过 / 5 失败**(⑥⑦⑧⑬⑭)→ 修复后 **21 / 0**
- 注入反证 _b38_falsify.py:把删行那两行用 if(false) 挡掉,锚点断言 count==1,
立刻红回同样的 5 条、关键条目 ⑥⑧⑬⑭ 命中 4/4,副本自动还原
- 原 _b37_home_task_drop.py(复选框路径)仍 **20 / 0**
- _b37_regress.py **11 / 0**:看板仍改状态且**不删行**、详情页子任务复选框正常、
空态 display 真的是 none
- home.php 内联 JS 0 块坏;app.js / task_panel.js node --check 全过
|
2026-10-28 18:29:59 +08:00 |
|
laoyang
|
40fd6c501c
|
v78 服务端:首页任务改成「就地处理」面板 + 新建任务搬到首页
用户原话:
「网页端,首页点击一个任务后,除了展示信息,应该有相应的功能,可以修改状态:
待办、进行中,可以挪动分组,可以拖动上传附件,可以添加评论,添加子任务等。
其目地,是为了让用户不离开首页,就能对任务进行简单的处理。」
「另外,在全部任务旁边,放一个新建任务的功能,其实就是把任务中的新建和编辑功能,
搬过来,用弹出框来处理。」
一、首页任务浮层由「只读信息」改成「可操作面板」
- 新增 templates/tasks/_panel.php:状态三段 / 看板 chips(带看板色)/
子任务(复用 _task_check.php)/ 附件拖放框(复用 _attachments.php)/ 讨论
- 新增路由 tasks/panel:只吐一块 HTML(找不到 404)
- 既有七个动作路由各加一条 ajax_panel 回包分支(状态 / 挪分组 / 新建子任务 /
评论增删 / 附件增删),没有新开写接口
- helpers.php 新增 task_panel_wanted() / task_panel_json();
fb_entry_json() 的 payload 增加 panel 字段(只有任务会给)
- 每次动作回**整块新面板**:一次动作会牵动子任务计数、附件张数、看板选中态,
逐处同步 DOM 必然漏一处
- 拖进来的附件**立刻上传**(任务已存在,不该再让用户点一次保存),
不复用 _stage_drop.php 那套「暂存 + 认领」
二、新建任务搬到首页
- 从 templates/tasks/index.php 抽出公共片段 templates/tasks/_add_modal.php
(字段与原来一字未改),首页与任务页共用同一份
- 新增 assets/js/task_add.js 管开关(两页共用,幂等绑定);
打开那一刻才 fbUpgradeRange() + fbBootAntd()(hidden 时宽高为 0,antd 会定位错乱)
- App\HomeCards 登记表里加 add => 新建任务,首页卡头与「全部任务」并排
- tasks/create 支持 back=home(白名单),新建完回首页
三、其它
- assets/js/task_panel.js:事件委托;动作后整块换面板;
顺手同步首页那张卡的状态徽标与复选框(关掉浮层不能还是旧状态);
传附件时保住用户写了一半的留言与子任务标题;取不到面板时退回只读信息
- app.css 追加面板样式(含 color-mix 的 @supports 兜底)与卡片窄到宽 2 时允许折行
验证:本地 e2e 1558/0、几何 428/0 + 660/0、v78 反向验证 11/0、
片段作用域体检 35/0、php -l 与 node --check 全过
|
2026-10-22 12:40:14 +08:00 |
|