From 9ab7d40a3b38d3360ae2e092d2a36338fc8532fa Mon Sep 17 00:00:00 2001 From: laoyang Date: Thu, 5 Nov 2026 18:07:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E9=9D=A2=E6=9D=BF=EF=BC=9A?= =?UTF-8?q?=E5=8A=A0=E4=BF=9D=E5=AD=98=E6=8C=89=E9=92=AE=EF=BC=8C=E8=B4=9F?= =?UTF-8?q?=E8=B4=A3=E4=BA=BA=E5=92=8C=E5=88=86=E7=B1=BB=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=8F=AF=E7=BC=96=E8=BE=91=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户 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 控件 / 原有状态/附件/评论 全部在位 ✅ --- app/Task.php | 50 ++++++++++++++ assets/css/app.css | 23 +++++++ assets/js/entry_popup.js | 4 -- assets/js/task_panel.js | 101 ++++++++++++---------------- index.php | 54 +++++++++++++++ templates/tasks/_panel.php | 130 +++++++++++++++++++++++++------------ 6 files changed, 260 insertions(+), 102 deletions(-) diff --git a/app/Task.php b/app/Task.php index 9bb9c58..c5c9204 100644 --- a/app/Task.php +++ b/app/Task.php @@ -749,6 +749,56 @@ final class Task return true; } + /** + * 只更新「截止时间 / 负责人 / 分类」三项(#76)。 + * + * 🔴 用户 2026-11-05:「负责人和分类都没法修改,按道理讲都应该可以修改才对。」 + * 上一版面板里这三项都是只读的 `dl.kv` 行。 + * + * ⚠️ **不要复用 `update()`**:那是全字段覆盖。面板只交这三项的话, + * title / note / status / repeat / parent / board 会被默认值抹空 —— + * 与 `setDue()` 记的是同一个坑。 + * + * ⚠️ 三个参数都「显式传」才算改:`due_at` 传 `null` = 不动; + * 传 `''` = 清掉。这样调用方可以只改其中一项。 + * (用 `array_key_exists` 判"传没传",而不是 `isset` —— + * `isset(null)` 为 false,会把"显式传 null"误判成"没传"。) + * + * @param array $d 可含 due_at / assignee_id / category_id + */ + public static function setAttrs(int $familyId, int $id, array $d): bool + { + $old = self::row($familyId, $id); + if (!$old) { + return false; + } + $sets = []; + $args = []; + if (array_key_exists('due_at', $d)) { + $sets[] = 'due_at = ?'; + $args[] = self::dtOrNull((string)($d['due_at'] ?? '')); + } + if (array_key_exists('assignee_id', $d)) { + /* 0 / 空 = 不指定(存 NULL,与 update() 里 `?: null` 同一口径) */ + $sets[] = 'assignee_id = ?'; + $aid = (int)($d['assignee_id'] ?? 0); + $args[] = $aid > 0 ? $aid : null; + } + if (array_key_exists('category_id', $d)) { + $sets[] = 'category_id = ?'; + $args[] = self::categoryIdOf($familyId, (int)($d['category_id'] ?? 0)); + } + if (!$sets) { + return true; // 三个都没传:什么都不做,但也算"成功"(幂等) + } + $args[] = $id; + $args[] = $familyId; + Database::get()->prepare( + 'UPDATE tasks SET ' . implode(', ', $sets) . ' WHERE id = ? AND family_id = ?' + )->execute($args); + return true; + } + public static function setStatus(int $familyId, int $id, string $status, int $position = 0): int { $old = self::row($familyId, $id); diff --git a/assets/css/app.css b/assets/css/app.css index d31faab..76e13a3 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -6930,6 +6930,29 @@ h6.md-h { font-size: 17px; color: var(--muted); } /* 「还剩 3 天」那类小字:贴在控件右边,不与控件抢宽度 */ .tk-due-word { flex: 0 0 auto; font-size: 12.5px; color: var(--faint); white-space: nowrap; } .tk-due-word.kb-late { color: var(--red); } + +/* 🔴 #76:「更多信息」小节 —— 截止 / 负责人 / 分类 三行 + 保存。 + 用户 2026-11-05:「没有保存按钮…负责人和分类都没法修改。」 + 每行结构 = 固定宽标签 + 占满剩余宽度的控件,与 .mv-row 同一套观感 + (但标签用 tk-attr-lb,因为面板里字号比整页表单小一号)。 */ +.tk-attrs { display: flex; flex-direction: column; gap: 10px; } +.tk-attr-row { + display: flex; align-items: center; gap: 10px; flex-wrap: wrap; +} +.tk-attr-lb { + flex: 0 0 46px; font-size: 12.5px; color: var(--faint); text-align: right; +} +/* 控件区占满标签右侧(min-width:0 让它能被压缩,不会把标签挤出去) */ +.tk-attr-row > *:not(.tk-attr-lb) { flex: 1 1 auto; min-width: 0; } +/* AntD 的 picker 本身有固定宽度,别让它撑满整行 */ +.tk-attr-row > .dt-input { max-width: 300px; } +.tk-attr-row > select.select { max-width: 300px; } +/* 保存按钮那一行:与上面的控件左缘对齐(= 46px 标签宽 + 10px gap) */ +.tk-attr-actions { + display: flex; align-items: center; gap: 10px; + padding-left: 56px; +} +.tk-attr-tip { font-size: 12px; color: var(--green, #2E7D4F); } .tk-sec-lb { flex: 0 0 34px; font-size: 12.5px; color: var(--faint); } .tk-seg { display: flex; gap: 6px; flex-wrap: wrap; } /* 面板里的状态徽标比卡片上的大一档(这里是要点的按钮,不是标签) */ diff --git a/assets/js/entry_popup.js b/assets/js/entry_popup.js index 2ab115a..fae36a6 100644 --- a/assets/js/entry_popup.js +++ b/assets/js/entry_popup.js @@ -226,10 +226,6 @@ if (typeof window.fbBootAntd === 'function') { try { window.fbBootAntd(); } catch (e) { /* 挂不上就留在原生下拉,不阻断面板 */ } } - /* 截止控件的 change 监听(#74):首次打开也要绑,否则选完时间不会保存 */ - if (typeof window.fbTaskPanelBindDue === 'function') { - try { window.fbTaskPanelBindDue(); } catch (e) { /* 忽略 */ } - } }) .catch(function () { if (!bodyEl()) { return; } diff --git a/assets/js/task_panel.js b/assets/js/task_panel.js index cddd0dd..a57828b 100644 --- a/assets/js/task_panel.js +++ b/assets/js/task_panel.js @@ -92,56 +92,9 @@ if (typeof window.fbBootAntd === 'function') { try { window.fbBootAntd(); } catch (err) { /* 挂不上就留在原生下拉,不阻断面板 */ } } - /* 重画之后节点是新的 ⇒ 监听要重新绑一遍(旧的已经随旧节点一起没了) */ - bindDue(); return true; } - /** - * 监听「截止时间」控件的变化,改完**立即保存**(#74)。 - * - * 为什么不用 `data-tk-act="due"` 那种按钮: - * 面板里其它动作都是"点一下就生效",截止时间是**选完时间**就该生效, - * 再让用户点一次「保存」会让这一处与其它处不一致。 - * - * 为什么监听**隐藏字段的 change** 而不是 AntD 的回调: - * · AntD 挂载时会把选中的值写回 `.dt-date` / `.dt-hour` / `.dt-minute` - * 这些原生字段,并 copy 一个 `change` 事件(见 app.js 的 writeDT); - * · 原生下拉那一档(AntD 没挂上时的兜底)也是同一批发 `change`; - * · ⇒ 监听一次就能覆盖**两种形态**,不用分别处理。 - * - * ⚠️ `data-fb-due-bound` 幂等标记:`bindDue()` 在每次 paint 后都会跑, - * 没有它会给同一个节点挂上多个监听 ⇒ 改一次时间发好几条请求。 - */ - function bindDue() { - var b = bodyEl(); - if (!b) { return; } - var box = b.querySelector('.dt-input[data-dt]'); - if (!box) { return; } - var cfg = {}; - try { cfg = JSON.parse(box.getAttribute('data-dt') || '{}'); } catch (e) { return; } - if (cfg.name !== 'due_at') { return; } - if (box.getAttribute('data-fb-due-bound') === '1') { return; } - box.setAttribute('data-fb-due-bound', '1'); - - /* 用 `change`(不是 `input`):AntD 选完日期/时间才写回, - 中途打字不该触发几十次请求。 */ - box.addEventListener('change', function () { - var d = box.querySelector('.dt-date'); - var date = d ? String(d.value || '').trim() : ''; - if (date === '') { - /* 清空 = 去掉截止时间(allowClear 会走到这里) */ - act(urlOf('due'), { due_at: '' }); - return; - } - var h = box.querySelector('.dt-hour'); - var m = box.querySelector('.dt-minute'); - var hh = h && String(h.value || '').trim() !== '' ? String(h.value) : '00'; - var mm = m && String(m.value || '').trim() !== '' ? String(m.value) : '00'; - act(urlOf('due'), { due_at: date + ' ' + hh + ':' + mm + ':00' }); - }); - } - /** * 把面板里的最新状态**顺手**同步到首页那张卡上(v78)。 * @@ -282,6 +235,49 @@ act(urlOf('move'), { board: btn.getAttribute('data-board') || '0' }); return; } + /* 🔴 #76(用户 2026-11-05):「时间控件是对的,但是现在没有保存按钮… + 另外,负责人和分类都没法修改,按道理讲都应该可以修改才对。」 + + ⇒ 「截止 / 负责人 / 分类」三项一次性提交。 + ⚠️ 上一版是"选完截止时间立即保存"(监听 change),用户不认可 —— + 他要的是"改完点保存"这种确定感。 + ⚠️ 只提交这三项,服务端 `Task::setAttrs()` 也只写这三列, + 不碰 title/note/status/repeat(见那个方法的注释)。 */ + if (what === 'save-attrs') { + e.preventDefault(); + if (btn.disabled) { return; } + var bd = bodyEl(); + if (!bd) { return; } + var dbox = bd.querySelector('.dt-input[data-dt]'); + var dueVal = ''; + if (dbox) { + var dt = dbox.querySelector('.dt-date'); + var dstr = dt ? String(dt.value || '').trim() : ''; + if (dstr !== '') { + /* 截止时间是"日期 + 时刻":时分没选就补 00:00(与 readDT 同一口径) */ + var hh = dbox.querySelector('.dt-hour'); + var mm = dbox.querySelector('.dt-minute'); + var h2 = hh && String(hh.value || '').trim() !== '' ? String(hh.value) : '00'; + var m2 = mm && String(mm.value || '').trim() !== '' ? String(mm.value) : '00'; + dueVal = dstr + ' ' + h2 + ':' + m2 + ':00'; + } + } + var asg = bd.querySelector('select[name="assignee_id"]'); + var cat = bd.querySelector('select[name="category_id"]'); + var extra = {}; + /* ⚠️ 只有控件真的在 DOM 里才提交该项 —— 没有成员时负责人下拉不渲染, + 那时不该把它当成 0 去清掉原值。 */ + extra['due_at'] = dueVal; + if (asg) { extra['assignee_id'] = asg.value; } + if (cat) { extra['category_id'] = cat.value; } + + btn.disabled = true; + act(urlOf('save'), extra); + /* 面板会被整块换掉(act → send → paint),按钮随之重建,不用手动恢复 */ + setTimeout(function () { btn.disabled = false; }, 4000); + return; + } + if (what === 'cmt-del') { e.preventDefault(); if (!window.confirm('删掉这条留言?')) { return; } @@ -406,15 +402,4 @@ step(); } - /** - * 对外暴露「给截止控件绑 change 监听」(#74)。 - * - * 为什么需要:首次打开面板走的是 `entry_popup.js` 的 `loadPanel()`, - * 那条路只 innerHTML、不经过本文件的 `paint()` ⇒ `bindDue()` 不会自动跑 - * ⇒ 用户选完截止时间不会保存(只有做过一次别的动作、触发过一次重画才会生效)。 - * `loadPanel()` 在插入 HTML 后显式调这个。 - * - * ⚠️ 内部有 `data-fb-due-bound` 幂等标记,重复调用不会重复绑。 - */ - window.fbTaskPanelBindDue = bindDue; })(); diff --git a/index.php b/index.php index 5d47c60..be80d15 100644 --- a/index.php +++ b/index.php @@ -7326,6 +7326,60 @@ $want = array_keys($want); } redirect('tasks'); + /* 🔴 #76(用户 2026-11-05):「时间控件是对的,但是现在没有保存按钮, + 只有下面一个评论的发表按钮。另外,负责人和分类都没法修改, + 按道理讲都应该可以修改才对。」 + + ⇒ 面板里「截止 / 负责人 / 分类」三项做成可编辑控件 + 一颗「保存」。 + 这是用户明确要的交互:先改完,再一次性提交,而不是每动一下发一次请求 + (上一版 `tasks/due` 是"选完立即保存",用户不认可)。 + + ⚠️ 用 `Task::setAttrs()`,只写这三列 —— 不是 `Task::update()`。 + 后者全字段覆盖,面板没交的 title/note/status/repeat 会被抹空。 + ⚠️ 三个字段都**可选**:表单里 `category_id` 没提交就不动它 + (`setAttrs` 用 `array_key_exists` 判,与 `?? null` 的"没传"区分开)。 */ + case 'tasks/panel_save': + $familyId = (int)Auth::familyId(); + $me = (int)Auth::personId(); + Auth::requireWrite(); + csrf_check(); + $id = (int)($_POST['id'] ?? 0); + $t = Task::find($familyId, $id, $me); + if (!$t || !Task::canWrite($t, $me)) { + if (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest') { + header('Content-Type: application/json; charset=utf-8', true, 403); + echo json_encode(['ok' => false, 'msg' => '这项任务不能改。'], JSON_UNESCAPED_UNICODE); + exit; + } + flash('err', '这项任务不能改。'); + redirect('tasks'); + } + /* 只把**表单真的提交了**的字段放进 $sd: + 面板里负责人下拉在"没有成员"等极端情况下可能不渲染, + 那时就不该把 assignee_id 当成 0 去清掉原值。 */ + $sd = []; + if (array_key_exists('due_at', $_POST)) { + $sd['due_at'] = trim((string)$_POST['due_at']); + } + if (array_key_exists('assignee_id', $_POST)) { + $sd['assignee_id'] = (int)$_POST['assignee_id']; + } + if (array_key_exists('category_id', $_POST)) { + $sd['category_id'] = (int)$_POST['category_id']; + } + Task::setAttrs($familyId, $id, $sd); + $msg = '已保存。'; + if (task_panel_wanted()) { + task_panel_json($id, $msg); + } + if (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'XMLHttpRequest') { + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['ok' => true, 'msg' => $msg], JSON_UNESCAPED_UNICODE); + exit; + } + flash('ok', $msg); + redirect('tasks/show', ['id' => $id]); + case 'tasks/comment_add': $familyId = (int)Auth::familyId(); $me = (int)Auth::personId(); diff --git a/templates/tasks/_panel.php b/templates/tasks/_panel.php index 6db5785..024b719 100644 --- a/templates/tasks/_panel.php +++ b/templates/tasks/_panel.php @@ -77,7 +77,9 @@ $tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpD data-tk-url-cmt-del="" data-tk-url-attach="" - data-tk-url-due=""> + data-tk-url-due="" + + data-tk-url-save=""> @@ -125,51 +127,99 @@ $tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpD -
截止
…
- 而且**没有截止时间时整行连显示都不显示** —— 用户根本不知道该去哪填。 - ⇒ 换成与 `_edit_form.php` 同一套的 AntD 单点控件(`dtPlugin=antd-single`), - 并且**不管有没有值都画出来**(空着就是"还没设",比"没有这个字段"清楚)。 - `$dtValue = $tpDue` ⇒ 已有截止时间会带进控件(用户明确要求)。 + 🔴 用户 2026-11-05:「时间控件是对的,但是现在没有保存按钮,只有下面一个 + 评论的发表按钮。另外,负责人和分类都没法修改,按道理讲都应该可以修改才对。」 - ⚠️ 控件里改完**立即保存**(`data-tk-act="due"`,见 task_panel.js), - 与状态/看板/子任务/评论/附件同一套交互 —— 面板里不该有"保存按钮"。 - ⚠️ 这个面板是 AJAX 塞进来的,AntD 要靠 `fbBootAntd()` 补挂, - 见 task_panel.js 的 paint()。 */ ?> -
- 截止 - - - - + 改之前的状态: + · 截止:上一版做成"选完立即保存",用户不认可 —— 他要显式保存按钮; + · 负责人 / 分类:**只读的 `dl.kv` 行**,而且值为空时整行都不渲染 + (`if (!empty($tpT['assignee_name']))` / `if ($tpCatName !== '')`) + ⇒ 用户不仅改不了,连"这一项存在"都看不出来。 + + ⇒ 三项合成一个带「保存」的小节:先改完,点一次提交。 + `Task::setAttrs()` 只写这三列,不碰 title/note/status(见那里的说明)。 + + ⚠️ 负责人用原生 ` + + + + + + + + $tpT['assignee_name'], + 'my_nickname' => $tpT['assignee_nick'] ?? ''])) + : '不指定' ?> + +
+ +
+ 分类 + + + + + +
+ + +
+ + +
- - - + - +
- -
负责人
-
$tpT['assignee_name'], 'my_nickname' => $tpT['assignee_nick'] ?? ''])) ?>
-
- - -
分类
-
循环