任务面板五项细化:弹窗选人、协作人、保存关窗、去编辑、评论附件 (#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 的既有设计(观影那边也一样),
需要改成"弹窗打开时才渲染列表"或按需加载。
This commit is contained in:
2026-11-05 18:28:15 +08:00
parent aac7dd5974
commit 11aa7c6ca5
6 changed files with 320 additions and 30 deletions
+34
View File
@@ -6953,6 +6953,40 @@ h6.md-h { font-size: 17px; color: var(--muted); }
padding-left: 56px;
}
.tk-attr-tip { font-size: 12px; color: var(--green, #2E7D4F); }
/* #78:面板里的选人组件(负责人单选 / 协作人多选)。
组件本身按"整页宽"设计,塞进 .tk-attr-row 的弹性项里要收一下:
- 不加 min-width:0 的话,chips 一多会把标签挤到上一行;
- max-width 与其它控件对齐(300px),别让它撑满整行。 */
.tk-attr-row > .pp-root { flex: 1 1 auto; min-width: 0; max-width: 340px; }
.tk-attr-row > .pp-root .pp-chips { margin-bottom: 6px; }
/* 标签在多行控件(备注、协作人)上方对齐,而不是垂直居中 */
.tk-attr-row-top { align-items: flex-start; }
.tk-attr-row-top > .tk-attr-lb { padding-top: 8px; }
/* 只读态的协作人 chips */
.tk-share-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.tk-share-chip {
font-size: 12.5px; padding: 3px 10px; border-radius: 999px;
background: var(--surface-2, #F2F0EC); color: var(--ink);
}
/* #78:评论里的「添加附件」。
用原生 file input(视觉隐藏),点 label 触发 —— 与站内其它上传入口同一手法。 */
.cmt-attach-row {
display: flex; align-items: center; gap: 8px;
margin-top: 6px;
}
.cmt-attach-pick {
display: inline-flex; align-items: center; gap: 5px;
font-size: 12.5px; color: var(--muted); cursor: pointer;
padding: 4px 10px; border-radius: 999px;
border: 1px dashed var(--line); background: transparent;
}
.cmt-attach-pick:hover { color: var(--brand); border-color: var(--brand); }
.cmt-attach-name {
font-size: 12px; color: var(--faint);
max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tk-sec-lb { flex: 0 0 34px; font-size: 12.5px; color: var(--faint); }
.tk-seg { display: flex; gap: 6px; flex-wrap: wrap; }
/* 面板里的状态徽标比卡片上的大一档(这里是要点的按钮,不是标签) */
+23 -1
View File
@@ -176,7 +176,16 @@
/* 🔴 用户:「来自其他模块时,下面就没有编辑、也没有删除,就只有一个详情」——
所以这两颗**只对 source='event'** 显(且各要有地址)。 */
var ed = byId('fbEntryEdit');
if (ed) { ed.hidden = !(isEvent && data.edit); }
/* 🔴 #78(用户 2026-11-05):「现在在首页上点一个任务,直接就是编辑页面啊。
所以不需要再加一个编辑按钮了。」
⇒ 任务(带 `panel`)的面板里已经能改**全部**字段
(标题/截止/负责人/协作人/分类/备注 + 状态/看板/子任务/评论/附件),
再给一颗「编辑」只会让用户以为"是不是还有别的东西要进另一个页面改"。
⚠️ **不能无条件藏**:观影 / 旅程 / 日程 / 纪念日 那些来源没有 panel,
它们仍然只有「详情 + 编辑」,那颗按钮必须留着。
⚠️ `data.del`(删除)不动 —— 用户没要求改删除,保持原样。 */
if (ed) { ed.hidden = !(isEvent && data.edit) || !!data.panel; }
var de = byId('fbEntryDelete');
if (de) { de.hidden = !(isEvent && data.del); }
@@ -226,6 +235,19 @@
if (typeof window.fbBootAntd === 'function') {
try { window.fbBootAntd(); } catch (e) { /* 挂不上就留在原生下拉,不阻断面板 */ }
}
/* 🔴 #78(用户 2026-11-05):「选人的时候不能按列表,因为人太多了。
你参考一下观影模式里面选人的方式。」
⇒ 面板里的负责人/协作人改用 `_person_picker.php`(弹窗 + 拼音联想),
那个组件靠 `fbInitPickers(容器的)` 接管点击。
`loadEdit()` 里一直有这一句(第 315 行),**`loadPanel()` 漏了** ——
所以面板里的选人按钮点了没反应。
⚠️ 组件自带 `data-pp-init` 幂等标记,重复调用不会重复绑。 */
if (typeof window.fbLoadPicker === 'function') {
try { window.fbLoadPicker(); } catch (e) { /* 忽略 */ }
}
if (typeof window.fbInitPickers === 'function') {
try { window.fbInitPickers(bodyEl()); } catch (e) { /* 忽略 */ }
}
})
.catch(function () {
if (!bodyEl()) { return; }
+62 -6
View File
@@ -92,6 +92,11 @@
if (typeof window.fbBootAntd === 'function') {
try { window.fbBootAntd(); } catch (err) { /* 挂不上就留在原生下拉,不阻断面板 */ }
}
/* #78:重画后选人组件是新节点 ⇒ 要重新接管(自带 data-pp-init 幂等标记)。
不补这一步的话,做过任何动作之后「更换负责人 / + 添加人」就点不动了。 */
if (typeof window.fbInitPickers === 'function') {
try { window.fbInitPickers(b); } catch (err) { /* 忽略 */ }
}
return true;
}
@@ -154,7 +159,13 @@
* @param {FormData} fd 已经带好 `_csrf` / `ajax_panel` 的请求体
* @param {boolean} keepDraft 换面板时要不要保住用户正在打的字(传附件时为 true)
*/
function send(url, fd, keepDraft) {
/**
* 发请求并刷新面板。
*
* @param keepDraft #78:重画后保住用户没发出去的评论草稿(传附件时用)
* @param closeOnOk #78:成功后**关掉浮层**(「保存」用;其它动作不关)
*/
function send(url, fd, keepDraft, closeOnOk) {
if (!url) { setTip('这一项暂时不能在首页改。', true); return; }
if (!window.fetch || !window.FormData) { window.location.reload(); return; }
fetch(url, {
@@ -166,6 +177,20 @@
return r.json().catch(function () { return null; });
}).then(function (d) {
if (!d) { window.location.reload(); return; } // 会话过期 / 500:整页重载最诚实
/* 🔴 #78(用户 2026-11-05):「点击保存之后,按道理说应该直接把这个窗口关闭才对呀。」
⇒ 保存成功就关窗,不再重画面板(用户已经不在看了)。
⚠️ 关窗前**先更新背后那张首页卡片**:否则用户关掉浮层看到的还是旧标题/旧状态,
"改了没生效"的错觉比不关窗更糟。
⚠️ 失败(d.ok === false)时**不关** —— 让用户看到错误提示、就地改。 */
if (closeOnOk && d.ok !== false) {
syncRow(d);
if (typeof window.fbEntryClose === 'function') {
window.fbEntryClose();
} else if (typeof window.fbCloseEntry === 'function') {
window.fbCloseEntry();
}
return;
}
paint(d, keepDraft);
syncRow(d);
setTip(d.msg || '', !d.ok);
@@ -173,13 +198,23 @@
}
/** 把「id + 几个附加字段」包成一个动作请求 */
function act(url, extra) {
/**
* 发一个面板动作。
*
* @param url 目标地址
* @param extra 额外字段
* @param closeOnOk #78:成功后**关掉浮层**(只有「保存」用)。
* 用户 2026-11-05:「点击保存之后,按道理说应该直接把这个窗口关闭才对呀。」
* ⚠️ 只给「保存」用:改状态 / 挪看板 / 加子任务 / 发评论 / 传附件
* 都还要继续在这个面板里操作,关窗会把用户赶走。
*/
function act(url, extra, closeOnOk) {
var fd = new FormData();
fd.append('_csrf', csrf());
fd.append('id', taskId());
fd.append('ajax_panel', '1');
Object.keys(extra || {}).forEach(function (k) { fd.append(k, extra[k]); });
send(url, fd);
send(url, fd, false, closeOnOk);
}
/* ---- ① 表单提交(加子任务 / 发评论 / 删附件)------------------------------
@@ -377,11 +412,32 @@
setTimeout(function () {
var extra = {};
extra['due_at'] = readDue(dbox);
/* ⚠️ 只有控件真的在 DOM 里才提交该项 —— 没有成员时负责人下拉不渲染,
那时不该把它当成 0 去清掉原值。 */
/* ⚠️ 只有控件真的在 DOM 里才提交该项 —— 控件不渲染时不该把原值清掉。 */
if (asg) { extra['assignee_id'] = asg.value; }
if (cat) { extra['category_id'] = cat.value; }
act(urlOf('save'), extra);
/* #78:标题 / 备注 —— 删掉「编辑」按钮后,它们只能在这里改 */
var ti = bd.querySelector('#tkp-title');
if (ti) { extra['title'] = ti.value; }
var nt = bd.querySelector('#tkp-note');
if (nt) { extra['note'] = nt.value; }
/* #78:协作人(`_person_picker.php` 的 multi 选人把结果存在
隐藏 checkbox 里,name="shares[]")。逐个收集。
⚠️ 必须带 `shares_submitted=1` 这个哨兵:
服务端靠它区分"表单里根本没有协作人控件"(不动)与
"用户把它们全删了"(清空)—— 只靠 `shares[]` 是否存在,
空数组时 PHP 收不到任何键,无法区分。 */
var shareBox = bd.querySelector('input[name="shares[]"]');
if (shareBox) {
extra['shares_submitted'] = '1';
var picked = bd.querySelectorAll('input[name="shares[]"]:checked');
for (var i = 0; i < picked.length; i++) {
extra['shares[' + i + ']'] = picked[i].value;
}
}
act(urlOf('save'), extra, true);
}, 60);
/* 面板会被整块换掉(act → send → paint),按钮随之重建,不用手动恢复 */
setTimeout(function () { btn.disabled = false; }, 4000);