任务面板:加保存按钮,负责人和分类改为可编辑 (#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 控件 /
                    原有状态/附件/评论 全部在位 ✅
This commit is contained in:
2026-11-05 18:07:17 +08:00
parent 7a108396ab
commit 9ab7d40a3b
6 changed files with 260 additions and 102 deletions
+90 -40
View File
@@ -77,7 +77,9 @@ $tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpD
data-tk-url-cmt-del="<?= e(url('tasks/comment_del')) ?>"
data-tk-url-attach="<?= e(url('tasks/attach_add')) ?>"
<?php /* #74:面板里直接改截止时间(不必点「编辑」进整页表单) */ ?>
data-tk-url-due="<?= e(url('tasks/due')) ?>">
data-tk-url-due="<?= e(url('tasks/due')) ?>"
<?php /* #76:截止/负责人/分类 三项一起保存(用户要显式「保存」按钮) */ ?>
data-tk-url-save="<?= e(url('tasks/panel_save')) ?>">
<?php /* CSRF 放在面板里(见头部第 ① 条):表单各自带一份,JS 里的按钮动作
从这里取一份 —— 两处同一个值,`csrf_token()` 天然幂等。 */ ?>
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>" data-tk-csrf>
@@ -125,51 +127,99 @@ $tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpD
</div>
<?php endif; ?>
<?php /* ---- 截止时间:**可直接改**(#74) ----
用户 2026-11-05:「点开之后,就可以修改任务的所有信息,包括…
修改截止时间。当然,如果现在已经有截止时间,就把它带进来。」
<?php /* ---- 更多信息:截止 / 负责人 / 分类 ---- 可编辑 + 保存(#76) ----
🔴 原来是**只读的一行**:
<?php if ($tpDue !== ''): ?><div class="kv-row"><dt>截止</dt><dd>…</dd></div><?php endif; ?>
而且**没有截止时间时整行连显示都不显示** —— 用户根本不知道该去哪填。
⇒ 换成与 `_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()。 */ ?>
<div class="tk-sec tk-sec-inline">
<span class="tk-sec-lb">截止</span>
<?php if ($tpCan): ?>
<?php
/* ⚠️ 变量名必须与 `_datetime.php` 的约定一致;
它内部一律用 `dt` 前缀的临时变量,不会踩到这里的 $tp* 或宿主页的 $t。 */
$dtName = 'due_at';
$dtValue = $tpDue;
$dtPlugin = 'antd-single';
$dtRequired = false;
require __DIR__ . '/../_datetime.php';
?>
<?php if ($tpDueWord !== ''): ?>
<span class="tk-due-word<?= $tpLate ? ' kb-late' : '' ?>"><?= e($tpDueWord) ?></span>
改之前的状态:
· 截止:上一版做成"选完立即保存",用户不认可 —— 他要显式保存按钮;
· 负责人 / 分类:**只读的 `dl.kv` 行**,而且值为空时整行都不渲染
(`if (!empty($tpT['assignee_name']))` / `if ($tpCatName !== '')`)
⇒ 用户不仅改不了,连"这一项存在"都看不出来。
⇒ 三项合成一个带「保存」的小节:先改完,点一次提交。
`Task::setAttrs()` 只写这三列,不碰 title/note/status(见那里的说明)。
⚠️ 负责人用原生 `<select>` 而不是 `_person_picker.php` 的弹窗选人:
面板本身已在浮层里,再弹一层模态 = 两层遮罩,手机上很容易点错;
而且 picker 需要 `fbInitPickers()` 初始化,`loadPanel()` 目前没调它。
家庭成员通常 3~10 人,select 够用。**人多了再换弹窗**,那时记得
把 `fbInitPickers` 也补进 `loadPanel()`。 */ ?>
<div class="tk-sec">
<div class="tk-sec-h">更多信息</div>
<div class="tk-attrs">
<div class="tk-attr-row">
<span class="tk-attr-lb">截止</span>
<?php if ($tpCan): ?>
<?php
/* ⚠️ 变量名必须与 `_datetime.php` 的约定一致;
它内部一律用 `dt` 前缀的临时变量,不会踩到这里的 $tp* 或宿主页的 $t。 */
$dtName = 'due_at';
$dtValue = $tpDue;
$dtPlugin = 'antd-single';
$dtRequired = false;
$dtId = 'tkp-due';
require __DIR__ . '/../_datetime.php';
?>
<?php if ($tpDueWord !== ''): ?>
<span class="tk-due-word<?= $tpLate ? ' kb-late' : '' ?>"><?= e($tpDueWord) ?></span>
<?php endif; ?>
<?php else: ?>
<span class="<?= $tpLate ? 'kb-late' : '' ?>"><?= $tpDue !== '' ? e($tpDue) : '未设' ?></span>
<?php endif; ?>
</div>
<div class="tk-attr-row">
<span class="tk-attr-lb">负责人</span>
<?php if ($tpCan): ?>
<select class="select" name="assignee_id" id="tkp-assignee">
<option value="0">不指定</option>
<?php foreach ($tpPeople as $tpP): ?>
<?php $tpPid = (int)($tpP['id'] ?? 0); ?>
<option value="<?= $tpPid ?>"
<?= $tpPid === (int)($tpT['assignee_id'] ?? 0) ? ' selected' : '' ?>>
<?= e(person_name($tpP)) ?>
</option>
<?php endforeach; ?>
</select>
<?php else: ?>
<span><?= !empty($tpT['assignee_name'])
? e(person_name(['name' => $tpT['assignee_name'],
'my_nickname' => $tpT['assignee_nick'] ?? '']))
: '不指定' ?></span>
<?php endif; ?>
</div>
<div class="tk-attr-row">
<span class="tk-attr-lb">分类</span>
<?php if ($tpCan): ?>
<select class="select" name="category_id" id="tkp-category">
<option value="0">未分类</option>
<?php foreach ($tpCats as $tpC): ?>
<?php $tpCid = (int)($tpC['id'] ?? 0); ?>
<option value="<?= $tpCid ?>"
<?= $tpCid === (int)($tpT['category_id'] ?? 0) ? ' selected' : '' ?>>
<?= e((string)($tpC['name'] ?? '')) ?>
</option>
<?php endforeach; ?>
</select>
<?php else: ?>
<span><?= $tpCatName !== '' ? e($tpCatName) : '未分类' ?></span>
<?php endif; ?>
</div>
<?php if ($tpCan): ?>
<div class="tk-attr-actions">
<button type="button" class="btn btn-sm" data-tk-act="save-attrs">保存</button>
<span class="tk-attr-tip" data-tk-save-tip hidden>已保存</span>
</div>
<?php endif; ?>
<?php else: ?>
<span class="<?= $tpLate ? 'kb-late' : '' ?>"><?= $tpDue !== '' ? e($tpDue) : '未设' ?></span>
<?php endif; ?>
</div>
</div>
<?php /* ---- 只读的事实(有才显示) ---- */ ?>
<?php /* ---- 其余只读事实(循环 / 备注) ---- */ ?>
<dl class="kv tk-kv">
<?php if (!empty($tpT['assignee_name'])): ?>
<div class="kv-row"><dt>负责人</dt>
<dd><?= e(person_name(['name' => $tpT['assignee_name'], 'my_nickname' => $tpT['assignee_nick'] ?? ''])) ?></dd>
</div>
<?php endif; ?>
<?php if ($tpCatName !== ''): ?>
<div class="kv-row"><dt>分类</dt><dd><?= e($tpCatName) ?></dd></div>
<?php endif; ?>
<?php if (!empty($tpT['repeat_rule'])): ?>
<div class="kv-row"><dt>循环</dt><dd><?= e(\App\Task::repeatWord((string)$tpT['repeat_rule'])) ?></dd></div>
<?php endif; ?>