首页任务面板:可直接改截止时间 (#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] ✅
状态/附件/子任务/评论 均仍在
This commit is contained in:
@@ -721,6 +721,34 @@ final class Task
|
||||
*
|
||||
* @return int 因循环而新生成的任务 id(没有则 0)
|
||||
*/
|
||||
/**
|
||||
* 只改「截止时间」(#74)。
|
||||
*
|
||||
* 🔴 用户 2026-11-05:「点开之后,就可以修改任务的所有信息,包括…
|
||||
* 修改截止时间。当然,如果现在已经有截止时间,就把它带进来。」
|
||||
*
|
||||
* ⚠️ **不要复用 `update()`**:那个方法是全字段覆盖(title/note/status/
|
||||
* assignee/repeat/parent/board/category 一起写),表单只交了 `due_at`
|
||||
* 的话,其余字段会被 `title('')`、`textOrNull(null)` 之类的默认值抹成空 ——
|
||||
* "只想改个日期,结果标题和备注没了",而且这种破坏不可恢复。
|
||||
* ⇒ 单独一条只写 `due_at` 的 UPDATE。
|
||||
*
|
||||
* 传空串 = 清掉截止时间(`dtOrNull` 会转成 NULL)。
|
||||
*
|
||||
* @return bool 是否真的更新了(任务不存在 / 不属于这个家庭时为 false)
|
||||
*/
|
||||
public static function setDue(int $familyId, int $id, string $dueAt): bool
|
||||
{
|
||||
$old = self::row($familyId, $id);
|
||||
if (!$old) {
|
||||
return false;
|
||||
}
|
||||
Database::get()->prepare(
|
||||
'UPDATE tasks SET due_at = ? WHERE id = ? AND family_id = ?'
|
||||
)->execute([self::dtOrNull($dueAt), $id, $familyId]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function setStatus(int $familyId, int $id, string $status, int $position = 0): int
|
||||
{
|
||||
$old = self::row($familyId, $id);
|
||||
|
||||
@@ -6920,6 +6920,16 @@ h6.md-h { font-size: 17px; color: var(--muted); }
|
||||
/* 「状态」「看板」这两段是"标签 + 一排可点的东西",横排。
|
||||
⚠️ 用 flex-wrap:看板可能是七八个,窄屏要能折行而不是撑出面板。 */
|
||||
.tk-sec-inline { flex-direction: row; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
/* 🔴 #74:面板里的「截止时间」AntD 控件。
|
||||
`.dt-input` 是 `display:flex; flex-wrap:wrap`,放进 `.tk-sec-inline`
|
||||
(也是 flex-wrap)里会被当成一个弹性项 —— 不给宽度约束时它会按内容撑,
|
||||
在窄屏上可能把「截止」标签挤到上一行、或者控件自己溢出。
|
||||
`flex: 1 1 auto` + `min-width: 0` 让它占满标签右侧的剩余宽度,
|
||||
`max-width` 兜住极宽屏(AntD 的 picker 本身有固定宽度)。 */
|
||||
.tk-sec-inline > .dt-input { flex: 1 1 auto; min-width: 0; max-width: 320px; }
|
||||
/* 「还剩 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); }
|
||||
.tk-sec-lb { flex: 0 0 34px; font-size: 12.5px; color: var(--faint); }
|
||||
.tk-seg { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
/* 面板里的状态徽标比卡片上的大一档(这里是要点的按钮,不是标签) */
|
||||
|
||||
@@ -78,9 +78,70 @@
|
||||
var ti2 = b.querySelector('.tk-inline-form input[name="title"]');
|
||||
if (ti2) { ti2.value = draftTitle; }
|
||||
}
|
||||
/* 🔴 #74(用户 2026-11-05):「点开之后,就可以修改任务的所有信息,
|
||||
包括…修改截止时间。」
|
||||
|
||||
⇒ 面板里现在有 AntD 的单点日期控件(`[data-dt-antd-single]`)。
|
||||
这段 HTML 是 AJAX 换进来的,**不会自己跑初始化** ——
|
||||
`bootAntd()` 只在页面加载时扫过一次 `[data-dt-antd-single]`,
|
||||
那时面板还不存在 ⇒ 控件会是一片原生下拉(或空白)。
|
||||
⇒ 每次重画面板都要补挂一次。
|
||||
|
||||
⚠️ `mountAntd*` 有 `data-antd-done` 幂等标记,重复调用不会挂两遍。
|
||||
⚠️ 必须放在 `innerHTML` 之后 —— 要挂的节点得先真的在 DOM 里。 */
|
||||
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)。
|
||||
*
|
||||
|
||||
@@ -7277,6 +7277,55 @@ $want = array_keys($want);
|
||||
redirect('tasks');
|
||||
break;
|
||||
|
||||
/* 🔴 #74(用户 2026-11-05):「点开之后,就可以修改任务的所有信息,
|
||||
包括…修改截止时间。当然,如果现在已经有截止时间,就把它带进来。」
|
||||
|
||||
⇒ 首页任务面板里**直接改截止时间**,不再需要点「编辑」进那张整页表单。
|
||||
与 status / move 同一套约定:`ajax_panel=1` 就回**整块新面板**,
|
||||
否则按老规矩 flash + 302(无 JS 的兜底)。
|
||||
|
||||
⚠️ 用 `Task::setDue()` 而**不是** `Task::update()`:后者是全字段覆盖,
|
||||
只交 due_at 会把 title/note/assignee 等全部抹空(详见那个方法的注释)。
|
||||
⚠️ 字段名沿用 `due_at`(与 `_edit_form.php` 一致),
|
||||
空串 = 清掉截止时间。 */
|
||||
case 'tasks/due':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$dueAt = trim((string)($_POST['due_at'] ?? ''));
|
||||
$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');
|
||||
}
|
||||
Task::setDue($familyId, $id, $dueAt);
|
||||
$msg = $dueAt !== ''
|
||||
? '截止时间改成 ' . substr($dueAt, 0, 16) . '。'
|
||||
: '已清掉截止时间。';
|
||||
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);
|
||||
$back = (string)($_POST['back'] ?? '');
|
||||
if ($back === 'show') {
|
||||
redirect('tasks/show', ['id' => $id]);
|
||||
} elseif ($back === 'home') {
|
||||
redirect('home');
|
||||
}
|
||||
redirect('tasks');
|
||||
|
||||
case 'tasks/comment_add':
|
||||
$familyId = (int)Auth::familyId();
|
||||
$me = (int)Auth::personId();
|
||||
|
||||
@@ -75,7 +75,9 @@ $tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpD
|
||||
data-tk-url-status="<?= e(url('tasks/status')) ?>"
|
||||
data-tk-url-move="<?= e(url('tasks/move')) ?>"
|
||||
data-tk-url-cmt-del="<?= e(url('tasks/comment_del')) ?>"
|
||||
data-tk-url-attach="<?= e(url('tasks/attach_add')) ?>">
|
||||
data-tk-url-attach="<?= e(url('tasks/attach_add')) ?>"
|
||||
<?php /* #74:面板里直接改截止时间(不必点「编辑」进整页表单) */ ?>
|
||||
data-tk-url-due="<?= e(url('tasks/due')) ?>">
|
||||
<?php /* CSRF 放在面板里(见头部第 ① 条):表单各自带一份,JS 里的按钮动作
|
||||
从这里取一份 —— 两处同一个值,`csrf_token()` 天然幂等。 */ ?>
|
||||
<input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>" data-tk-csrf>
|
||||
@@ -123,13 +125,43 @@ $tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpD
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php /* ---- 截止时间:**可直接改**(#74) ----
|
||||
用户 2026-11-05:「点开之后,就可以修改任务的所有信息,包括…
|
||||
修改截止时间。当然,如果现在已经有截止时间,就把它带进来。」
|
||||
|
||||
🔴 原来是**只读的一行**:
|
||||
<?php if ($tpDue !== ''): ?><div class="kv-row"><dt>截止</dt><dd>…</dd></div><?php endif; ?>
|
||||
而且**没有截止时间时整行连显示都不显示** —— 用户根本不知道该去哪填。
|
||||
⇒ 换成与 `_edit_form.php` 同一套的 AntD 单点控件(`dtPlugin=antd-single`),
|
||||
并且**不管有没有值都画出来**(空着就是"还没设",比"没有这个字段"清楚)。
|
||||
`$dtValue = $tpDue` ⇒ 已有截止时间会带进控件(用户明确要求)。
|
||||
|
||||
⚠️ 控件里改完**立即保存**(`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>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<span class="<?= $tpLate ? 'kb-late' : '' ?>"><?= $tpDue !== '' ? e($tpDue) : '未设' ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php /* ---- 只读的事实(有才显示) ---- */ ?>
|
||||
<dl class="kv tk-kv">
|
||||
<?php if ($tpDue !== ''): ?>
|
||||
<div class="kv-row"><dt>截止</dt>
|
||||
<dd class="<?= $tpLate ? 'kb-late' : '' ?>"><?= e($tpDue) ?><?= $tpDueWord !== '' ? '(' . e($tpDueWord) . ')' : '' ?></dd>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?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>
|
||||
|
||||
Reference in New Issue
Block a user