From 190dc563016cf2e3de2f9e9e7b6bc199bfa6417b Mon Sep 17 00:00:00 2001 From: laoyang Date: Sat, 17 Oct 2026 23:10:49 +0800 Subject: [PATCH] =?UTF-8?q?v55=EF=BC=9A=E7=AC=94=E8=AE=B0=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E4=B8=8D=E9=87=8D=E5=A4=8D=E6=A0=87=E9=A2=98=20/=20?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=8A=B6=E6=80=81=E9=80=89=E5=AE=8C=E5=8D=B3?= =?UTF-8?q?=E6=94=B9=EF=BC=88=E5=8E=BB=E6=8C=89=E9=92=AE=EF=BC=89/=20?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E4=BB=BB=E5=8A=A1=E5=BC=B9=E6=A1=86=20/=20?= =?UTF-8?q?=E5=AD=90=E4=BB=BB=E5=8A=A1=E6=88=AA=E6=AD=A2=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=8D=A2=E6=9C=80=E6=96=B0=E6=8E=A7=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户 2026-10-17 四条。 ① 笔记详情:正文里不再重复显示标题 用户原话:「笔记详情里面的标题已经变成这个日笔记的题目了,那**下面那个里面 就不用再显示这个题目**了。」 背景:v52 已把**页头**改成笔记自己的标题(并参与 ), 而**正文区里又写了一遍**同一个标题 ⇒ 同一页同一个名字出现两次。 ⇒ 删正文里那个 `<h1 class="note-read-title">`;**页头那份保留**。 连带删掉已无使用者的 `.note-read-title` CSS 规则 (留一条没有使用者的规则,只会让下一个人以为"正文里还该有个标题")。 ② 任务详情:状态**选一下就改**(去掉「改」按钮) 用户原话:「任务详情里边,状态后面**不用加按钮**了。不是说选了之后再点一下才改, 而是**只要一选就让它改**:选一下,状态就发生改变就行了。」 ⇒ `<select>` 的 `change` 直接 fetch 提交到**既有的** `tasks/status` (它本来就认 `X-Requested-With` 并回 JSON —— 不新开路由、不加新契约)。 成功后**就地**同步三处(不刷页、不丢滚动位置): · 下拉本身; · 页头副标题的状态词; · 页头那颗「标记完成 ↔ 重新打开」按钮的文案与 hidden 值 (改了状态而按钮还写"标记完成"就自相矛盾)。 反馈:一条**浮在顶部**的轻提示 `.tk-flash`(`position:fixed` 不占文档流 ⇒ 改状态不会让页面跳动)。循环任务那句"下一次也排上了"(`d.msg`)照旧显示 —— 不能因为改交互把 v24 定下的行为丢掉。 ⚠️ **无 JS 兜底**:`<noscript>` 里那颗「改」按钮 (规范:脚本开启时 noscript **整块不渲染**)。不要用 `<button hidden>`+JS 去"显示"它(会闪、脚本报错时永远不出现)。 🔴🔴 **踩坑(本轮最值钱)**:第一版写成"先 `sel.disabled = true`、再 `new FormData(form)`" ⇒ **HTML 规范:禁用的控件不参与表单数据构造** ⇒ 字段丢了 ⇒ 服务端收到空 status ⇒ 落回 `todo`。 症状是"请求成功、回包 ok、页面还给了提示,唯独值不对" —— 极具误导性。 ⇒ **先构建、后禁用**。 ③ 编辑任务 = 弹框 用户原话:「**编辑任务,弹框处理**。」 原来详情页是个 `<details>` 就地展开表单(展开会把下面的子任务/附件/评论整段往下推); 现在点「编辑这项任务」**浮起弹框**(`.cal-modal`,与「新建任务 / 新建日程 / 编辑观影记录」同一套类名)。 ⚠️ **表单一个字没改** —— 仍 require 同一份 `_edit_form.php` (条目浮层 `tasks/edit?embed=1` 也用它,两处永远同一张)。 这次只加了个 `$tkEditModal` 开关:true 时套 `.cal-modal` 壳 + 「保存」挪到底栏(与新建任务一致:主要动作在右下角)。 ⚠️ 打开那一刻才 `fbBootAntd()` / `fbUpgradeRange()` 升级里面的日期控件 (hidden 时也能挂上,但 antd 弹层定位需要元素有实际尺寸)。 ⚠️ 事件委托挂 `document`(与观影详情页同一理由:校验失败若重画这块,委托仍有效)。 ④ 子任务截止时间:换最新控件 用户原话:「**子任务的截止时间,使用最新的时间控件**。」 **真根因**:`tasks/show.php` 子任务那块 require `_datetime.php` 时 **漏传 `$dtPlugin`** ⇒ 空值时走**旧的三下拉(年/月/日)** ⇒ 与「新建任务」「任务编辑」里的 antd 控件**不一样**。 ⇒ 补 `$dtPlugin = 'antd-single'`(截止时间是**单个时刻**,不是范围)。 ⚠️ 全仓这一族的调用点**逐处核对过**:只有子任务这一处漏了;其余 (日程起止 / 观影起止 / 任务新建 / 任务编辑 / 行程起止 / 修正起止) 都已有 `'antd'` 或 `'antd-single'`。 ⚠️ `people/form.php`(生日)**有意不传** —— 生日是"日期",年份要能下拉到 1900, 与"截止时间"不是一回事,别顺手改。 验证(新增 2 套件 + 更新 deploy 断言): · _fb_v55_ui.py(**30 条**真机断言) · _b55_falsify.py(**8 注入 / 8 全抓**,基线 30/0) · e2e **1518/0**;familybook_deploy.sh 加「第55批形态」段(**13 条**) · 部署 **0 FAIL**;线上 4 文件 md5 逐字节一致; 线上剥注释核实:笔记模板/`app.css` 里已无 `note-read-title`(反向)、 任务模板有 `data-st-select` / `fd=FormData` / `noscript` 兜底 / `antd-single`、 编辑表单有 `tkEditCard`/`tkEditSubmit`、`app.css` 有 `.tk-flash`; 诊断页已删复验 404 ⚠️ 本轮踩坑(已写进 pitfalls.md): · 🔴🔴 **先 `disabled=true` 再 `new FormData(form)` ⇒ 字段会丢**(表单数据不含禁用控件); · 连带印证:**用裸 `strpos`/`grep` 做反向断言会命中注释**(我的线上诊断脚本 就因为这个连报两次"还有",剥注释后才对)—— 反向断言一律走"剥注释"。 ⚠️ 另:工作树里 `app/ApiHandler.php` / `app/PersonQuery.php` / `app/Home.php` / `app/Api.php` / `app/ApiToken.php` / `app/Schema.php` / `index.php` / `templates/settings/me.php` 是**另一个会话**正在做的 App 端 / 设备管理改动, 一字未碰、也未提交。 --- assets/css/app.css | 22 ++- templates/notes/form.php | 13 +- templates/tasks/_edit_form.php | 45 +++++- templates/tasks/show.php | 263 +++++++++++++++++++++++++++++++-- 4 files changed, 320 insertions(+), 23 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index f01babc..38bec9f 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -583,6 +583,24 @@ textarea.input { min-height: 76px; resize: vertical; } } .flash-item.err { background: #F8DDDA; color: #A1341F; } +/* ---- 🔴 v55(用户 2026-10-17 第 2 条):任务详情「状态选完即改」的**轻提示** ---- + 就地改完要给一句反馈(尤其循环任务会顺带排下一次,那句"下一次也排上了"不能丢)。 + 它**不是**页首那条 `.flash`(那是一整块、会撑开版面)—— + 这里是一个**浮在顶部**的小条:`position: fixed` 不占文档流, + 所以"改状态"不会让页面跳动一下(那正是就地改要避免的)。 + ⚠️ 配色沿用上面 `.flash-item` 那两套(绿=成功 / 红=失败),保持全站一致。 */ +.tk-flash { + position: fixed; top: 14px; left: 50%; transform: translateX(-50%); + z-index: 300; /* 高于自建弹框(.cal-modal 是 200) */ + max-width: min(92vw, 460px); + padding: 9px 16px; border-radius: 999px; + font-size: 13.5px; line-height: 1.5; text-align: center; + box-shadow: 0 3px 14px rgba(0,0,0,.14); + background: #E6F0DC; color: #3D6B1F; +} +.tk-flash.is-err { background: #F8DDDA; color: #A1341F; } +.tk-flash[hidden] { display: none; } + .empty { background: var(--surface); border: 1px dashed var(--line); border-radius: var(--radius); padding: 40px 20px; text-align: center; @@ -5197,7 +5215,9 @@ h5.md-h, h6.md-h { font-size: 14px; } 正文卡与其它页面的 .card 保持同宽,视觉上才是一列。 ⇒ 直接放开限宽(.card 本身已有全页统一的内边距与最大宽度约束)。 */ .note-read { max-width: none; } -.note-read-title { margin: 0 0 6px; font-size: 22px; font-weight: 600; color: var(--ink); } +/* ⚠️ v55:`.note-read-title` 已删 —— 标题只在**页头**显示一次(用户 2026-10-17 第 1 条 + 「标题已经变成这个笔记的题目了,那下面那个里面就不用再显示这个题目了」)。 + 留一条没有使用者的规则,只会让下一个人以为"正文里还该有个标题"。 */ .note-read-tags { margin: 0 0 14px; } .note-read-body { display: flex; flex-direction: column; gap: 14px; } /* 🔴 v52(用户第 4 条):详情页的**分段编辑** —— 每一块外面包的这个容器。 diff --git a/templates/notes/form.php b/templates/notes/form.php index 9f36521..f7ef9aa 100644 --- a/templates/notes/form.php +++ b/templates/notes/form.php @@ -116,10 +116,15 @@ $imgParts = static function (string $c): array { ⚠️ 附件(`$editing['files']`)**排在正文之后、页面最下方** —— 用户原话「附件在最下方」。 */ ?> <article class="note-read card card-pad"> - <?php $title = trim((string)($editing['title'] ?? '')); ?> - <?php if ($title !== ''): ?> - <h1 class="note-read-title"><?= e($title) ?></h1> - <?php endif; ?> + <?php /* 🔴 v55(用户 2026-10-17 第 1 条):「标题已经变成这个日笔记的题目了, + 那下面那个里面就**不用再显示这个题目**了。」 + ⇒ 正文区里那个重复的 `<h1 class="note-read-title">` 删掉。 + ⚠️ 背景:v52 已经把**页头**改成笔记自己的标题(见本文件上面那段), + 而正文这里**又写了一遍同一个标题** ⇒ 同一页同一个名字出现两次 + (页头一行、正文开头一行),用户看到的就是这个重复。 + ⚠️ **页头那份保留**(那是"这一页叫什么",也参与 <title> 与浏览器标签); + 删的只是正文里的副本。 + ⚠️ 编辑态不受影响(本段只在阅读态渲染)。 */ ?> <?php if (!empty($editing['tags'])): ?> <div class="note-tags note-read-tags"> diff --git a/templates/tasks/_edit_form.php b/templates/tasks/_edit_form.php index 425388b..973ef28 100644 --- a/templates/tasks/_edit_form.php +++ b/templates/tasks/_edit_form.php @@ -25,17 +25,44 @@ * $parents 可选父任务候选 * $categories / $boards 分类与看板 * $tkEditEmbed 是否"浮层形态"(true = 不输出自己的「保存」,由浮层底栏给) + * $tkEditModal 🔴 v55:是否**套 `.cal-modal` 弹框壳**(用户 2026-10-17 第 3 条 + * 「编辑任务,弹框处理」)。详情页传 true;条目浮层 / AJAX 片段不传 + * (它自己已经在浮层里,再套一层弹框就是"框中框")。 + * $tkEditOpen 弹框初始是否打开(默认 **关闭** —— 详情页要点「编辑」才开)。 + * $tkEditTitle 弹框标题(默认「编辑任务」) */ $tkT = is_array($t ?? null) ? $t : []; $tkPeople = (array)($people ?? []); $tkParents = (array)($parents ?? []); $tkCats = (array)($categories ?? []); $tkBoards = (array)($boards ?? []); +$tkModal = !empty($tkEditModal); +$tkOpen = !empty($tkEditOpen); +$tkMdlTitle = (string)($tkEditTitle ?? '编辑任务'); $tkEmbed = !empty($tkEditEmbed); ?> +<?php +/* 🔴 v55(用户 2026-10-17 第 3 条):「编辑任务,弹框处理。」 + 弹框壳(`.cal-modal`,与「新建任务 / 新建日程 / 编辑观影记录」同一套类名)。 + ⚠️ **表单本体一个字未改** —— 只是外面多包一层;字段名 / action / 契约全不动。 + ⚠️ `$tkEditEmbed` 为 true 时(详情页与条目浮层都是)保存键由**底栏**给, + 所以下面 `$tkEmbed` 那段的「保存」不输出,改在底栏放。 */ +?> +<?php if ($tkModal): ?> +<div class="cal-modal" id="tkEditCard" role="dialog" aria-modal="true" + aria-label="<?= e($tkMdlTitle) ?>"<?= $tkOpen ? '' : ' hidden' ?>> + <div class="cal-modal-panel"> + <div class="cal-modal-head"> + <b><?= e($tkMdlTitle) ?></b> + <button type="button" class="cal-modal-x" id="tkEditX" aria-label="关闭">✕</button> + </div> + <div class="cal-modal-body"> +<?php endif; ?> <form method="post" action="<?= e(url('tasks/update')) ?>" class="node-form" id="tkEditForm"> <?= csrf_field() ?> <input type="hidden" name="id" value="<?= (int)($tkT['id'] ?? 0) ?>"> + <?php /* 🔴 v55:编辑完回详情页(服务端 `tasks/update` 本来就 `redirect('tasks/show')`)—— + 这里**不传** back,与原来一致。 */ ?> <?php /* 🔴 v44(用户 2026-10-12 第 5 条):编辑表单也统一成观影式排版 + 最新时间控件 + 文案精简(与「新建任务」弹框一致)—— 同一件事在两个入口长得不一样,用户会以为"进了别的页面"。 */ ?> @@ -115,12 +142,23 @@ $tkEmbed = !empty($tkEditEmbed); <textarea class="input" id="tk-note" name="note" rows="3"><?= e((string)($tkT['note'] ?? '')) ?></textarea> </div> </div> - <?php /* ⚠️ 「状态」不在这里改(由 tasks/status 路由管,它还要处理循环任务)。 - 浮层形态下由它的底栏给「保存」;整页详情页里带自己的按钮。 */ ?> - <?php if (!$tkEmbed): ?> + <?php /* ⚠️ 「状态」不在这里改(由 tasks/status 路由管,它还要处理循环任务)。 */ ?> + <?php if ($tkModal): ?> + <?php /* 🔴 v55:**弹框形态下不在这里放「保存」** —— 它归底栏(与新建任务一致: + 主要动作在右下角)。`.btn-row` 那份是给"页面上的普通卡片形态"用的。 */ ?> + <?php elseif (!$tkEmbed): ?> <div class="btn-row"><button class="btn btn-sm" type="submit">保存</button></div> <?php endif; ?> </form> +<?php if ($tkModal): ?> + </div><?php /* /.cal-modal-body */ ?> + <div class="cal-modal-foot"> + <button class="btn btn-sm" type="submit" form="tkEditForm" id="tkEditSubmit">保存</button> + <button class="btn btn-ghost btn-sm" type="button" id="tkEditCancel">取消</button> + </div> + </div><?php /* /.cal-modal-panel */ ?> +</div><?php /* /.cal-modal */ ?> +<?php endif; ?> <?php /* 🔴🔴 —— **千万不要 unset 入参变量**! 这几种是宿主模板的"大局变量"(`$t` = 这条任务、`$people` = 名册…), include 之后宿主还要继续用它们(`$t['children']`、评论、子任务…)。 @@ -129,6 +167,7 @@ $tkEmbed = !empty($tkEditEmbed); (e2e 当场抓到 2 条:子任务没了、@高亮没了)。 ⇒ 这里只清**本片段自己造的**临时变量(`$tk*` 与 `$dt*` / `$pp*` 那几组)。 */ unset($tkT, $tkPeople, $tkParents, $tkCats, $tkBoards, $tkEmbed, + $tkModal, $tkOpen, $tkMdlTitle, // 🔴 v55:弹框壳那三个 $dtName, $dtValue, $dtRequired, $ppName, $ppPeople, $ppSelected, $ppMode, $ppOpenBtn, $ppTitle, $ppEmptyText, $ppAllowEmpty); ?> \ No newline at end of file diff --git a/templates/tasks/show.php b/templates/tasks/show.php index 1921d77..6cbed3f 100644 --- a/templates/tasks/show.php +++ b/templates/tasks/show.php @@ -35,7 +35,11 @@ $isChild = !empty($t['parent_id']); <span class="sub"><?= e(\App\Task::statusWord((string)$t['status'])) ?></span> <div class="actions"> <?php if ($canWrite): ?> - <form method="post" action="<?= e(url('tasks/move')) ?>" class="res-inline"> + <?php /* 🔴 v55(第 2 条):给它一个 id —— 状态下拉改完要**就地**同步这颗按钮的 + 文案与语义(选了"已完成"它还写"标记完成"就自相矛盾了)。 + ⚠️ 只加 id(`tkMarkBtn`,与任务模块其余 id 的 `tk-` 前缀一致), + 表单 action / 字段名一字未改。 */ ?> + <form method="post" action="<?= e(url('tasks/move')) ?>" class="res-inline" id="tkMarkBtn"> <?= csrf_field() ?> <input type="hidden" name="id" value="<?= (int)$t['id'] ?>"> <?php if ($t['status'] !== 'done'): ?> @@ -54,23 +58,43 @@ $isChild = !empty($t['parent_id']); <div class="card card-pad"> <dl class="kv"> <?php /* v24 第 14 批 用户第 2 条:状态要**能选**(不只是"标记完成/重新打开"两档)。 - 这里给完整的三档下拉,与看板卡片上那个徽标走同一个路由。 */ ?> + 这里给完整的三档下拉,与看板卡片上那个徽标走同一个路由。 + + 🔴 v55(用户 2026-10-17 第 2 条):「任务详情里边,状态后面**不用加按钮**了。 + 不是说选了之后再点一下才改,而是**只要一选就让它改**: + 选一下,状态就发生改变就行了。」 + ⇒ 去掉那颗「改」按钮;`<select>` 的 `change` 直接提交。 + 实现走**既有的 `tasks/status` + `X-Requested-With`**(它本来就回 JSON, + 见 index.php 那个分支)—— 不新开路由、不加新契约。 + ⚠️ **无 JS 时仍要能用**:`<noscript>` 里给一颗提交按钮。 + 这是本仓一贯的兜底口径(弹框/就地改都保留原生路径)。 + ⚠️ 表单里**不放** `back` 的 submit 按钮时,服务端拿到的 `back` 仍是 'show' + (下面那个 hidden 一直在),所以无 JS 提交也回本页。 */ ?> <div class="kv-row"> <dt>状态</dt> <dd> <?php if ($canWrite): ?> - <form method="post" action="<?= e(url('tasks/status')) ?>" class="res-inline st-form"> + <?php /* v55:`data-st-select` 是 JS 的挂钩;`name=status` 仍是提交契约(一字未改)。 */ ?> + <form method="post" action="<?= e(url('tasks/status')) ?>" class="res-inline st-form" id="tkStForm"> <?= csrf_field() ?> <input type="hidden" name="id" value="<?= (int)$t['id'] ?>"> <input type="hidden" name="back" value="show"> - <select class="select" name="status" aria-label="任务状态"> + <select class="select" name="status" aria-label="任务状态" data-st-select + data-st-id="<?= (int)$t['id'] ?>"> <?php foreach (\App\Task::STATUS as $sk => $sv): ?> <option value="<?= e($sk) ?>" <?= (string)$t['status'] === $sk ? 'selected' : '' ?>> <?= e($sv) ?> </option> <?php endforeach; ?> </select> - <button class="btn btn-sm" type="submit">改</button> + <?php /* ⚠️ **无 JS 时的兜底**:`<noscript>` 在"脚本可用"时**整块不渲染** + (HTML5 规范:body 里的 noscript,脚本开启则其内容不渲染), + 所以有 JS 时选完即改、页面上看不到任何按钮 —— 正是用户要去掉的那颗; + 没有 JS 时它才出现,让下拉仍能提交。 + ⚠️ **不要**用 `<button hidden>` + JS 去显示它:那需要等脚本跑起来才"显示", + 中间会闪一下,且脚本一旦报错按钮就永远不出现(=点了没反应)。 + ⚠️ 这里**不写** `class="btn"` 之外的东西:与全站按钮同一套样式。 */ ?> + <noscript><button class="btn btn-sm" type="submit">改</button></noscript> </form> <?php else: ?> <span class="kb-st is-<?= e((string)$t['status']) ?> is-static"> @@ -110,19 +134,34 @@ $isChild = !empty($t['parent_id']); </dl> <?php if ($canWrite): ?> - <details class="node-add"> - <summary class="link-quiet">编辑这项任务</summary> - <?php /* 🔴 v43(用户第 6 条):表单**抽成独立一份** `_edit_form.php` —— - 条目详情浮层点「编辑」时要单独取它(`tasks/edit?embed=1`), - 不可能去详情页里抠。抽出来后两处用的是同一张表单,不会不同步。 - ⚠️ 片段内部用 `$tk*` 前缀的局部变量,**不碰宿主的 `$t`/`$parents`**。 */ - $tkEditEmbed = false; // 整页形态:带上自己的「保存」按钮 - require __DIR__ . '/_edit_form.php'; - ?> - </details> + <?php /* 🔴 v55(用户 2026-10-17 第 3 条):「**编辑任务,弹框处理**。」 + 原来这里是一个 `<details>`:点一下**就地展开**一张表单。 + ⇒ 改成**弹框**(与「新建任务」「新建日程」「编辑观影记录」同一形态)。 + + ⚠️ 表单本身**一个字没改** —— 仍 require 同一份 `_edit_form.php` + (条目浮层 `tasks/edit?embed=1` 也用它,两处永远同一张)。 + 这次只是给它套一层 `.cal-modal` 壳(由 `$tkEditModal` 开关)。 + ⚠️ 为什么"就地展开"不够:展开会把下面的**子任务 / 附件 / 评论**整段往下推, + 而用户只是想改几个字段;弹框则原地浮起、关掉就回来。 + ⚠️ `$tkEditEmbed` 仍为 false —— 那是"要不要自己的保存键", + 弹框形态下按钮在**弹框底栏**(与新建任务一致),所以这里仍传 false, + 由 `$tkEditModal` 把"保存"挪到底栏。 */ ?> + <button class="btn btn-sm" type="button" id="tkEditBtn" + aria-haspopup="dialog" aria-expanded="false">编辑这项任务</button> <?php endif; ?> </div> +<?php if ($canWrite): ?> + <?php /* ---- 编辑任务的弹框(内嵌同一份 _edit_form.php;初始关闭)---- */ ?> + <?php + $tkEditEmbed = true; // 浮层形态:不输出自己的「保存」,由弹框底栏给 + $tkEditModal = true; // 🔴 v55:套 `.cal-modal` 壳 + $tkEditOpen = false; // 初始关闭(点「编辑这项任务」才开) + require __DIR__ . '/_edit_form.php'; + unset($tkEditEmbed, $tkEditModal, $tkEditOpen); + ?> +<?php endif; ?> + <?php /* ---- 子任务 ---- */ ?> <?php if (!$isChild): ?> <div class="card card-pad"> @@ -163,7 +202,19 @@ $isChild = !empty($t['parent_id']); <?= csrf_field() ?> <input type="hidden" name="parent_id" value="<?= (int)$t['id'] ?>"> <input class="input" name="title" required placeholder="如:擦玻璃"> + <?php /* 🔴 v55(用户 2026-10-17 第 4 条):「**子任务的截止时间,使用最新的时间控件**。」 + 真根因:这一处 require `_datetime.php` 时**漏传了 `$dtPlugin`** —— + 而 `$dtPlugin` 为空时控件走**旧的三下拉(年/月/日)**, + 与「新建任务」弹框、任务编辑表单里的 antd 控件**不一样** + (用户看到的正是"这里怎么还是老样子")。 + ⇒ 补 `$dtPlugin = 'antd-single'`(截止时间是**单个时刻**,不是范围)。 + ⚠️ 全仓这一族的调用点已逐处核对:只有子任务这一处漏了, + 其余(日程起止 / 观影起止 / 任务新建 / 任务编辑 / 行程起止 / 修正起止) + 都已有 `'antd'` 或 `'antd-single'`。 + ⚠️ `people/form.php`(生日)**有意不传** —— 生日是"日期"不含时刻, + 且年份要能下拉到 1900,与"截止时间"不是一回事,别顺手改。 */ ?> <?php $dtName = 'due_at'; $dtValue = null; $dtRequired = false; + $dtPlugin = 'antd-single'; require __DIR__ . '/../_datetime.php'; ?> <button class="btn btn-sm" type="submit">加上</button> </form> @@ -330,3 +381,185 @@ $isChild = !empty($t['parent_id']); })(); </script> <?php endif; ?> + +<?php /* ==================== 🔴 v55(用户 2026-10-17 第 3 条):编辑任务的弹框 ==================== + 用户原话:「编辑任务,弹框处理。」 + 原来详情页是一个 `<details>` 就地展开表单;现在点「编辑这项任务」浮起弹框。 + + ⚠️ **`_edit_form.php` 里的日期控件要在"打开弹框的那一刻"才升级成 antd** —— + 弹框初始是 `hidden` 的,`app.js` 的挂载器按"页面里有没有 `[data-dt-antd]`"来找, + 而 `hidden` 的元素**在 DOM 里是存在的**,所以挂载本身会成功; + 但 antd 的弹出面板依赖元素有**实际尺寸**(hidden 时宽高为 0), + 拿到 0 尺寸会让面板定位错乱。⇒ 打开时显式再 boot 一次(幂等,见 movies/show.php 同一手法)。 + ⚠️ 事件**委托**(挂 document)而不是逐个 addEventListener: + 与观影详情页同一理由 —— 校验失败若重画了这块,委托仍然有效,不用重绑。 + ⚠️ 幂等闸门 `__fbTkEditBound`:本页脚本只跑一次,但加上它以后若有人复用这段 + (比如某天详情页也做"替换正文再重跑脚本")不会被绑第二套监听。 */ ?> +<?php if ($canWrite): ?> + <script> + (function () { + if (window.__fbTkEditBound) { return; } + window.__fbTkEditBound = true; + + function modal() { return document.getElementById('tkEditCard'); } + function setOpen(on) { + var m = modal(); + if (!m) { return; } + m.hidden = !on; + var b = document.getElementById('tkEditBtn'); + if (b) { b.setAttribute('aria-expanded', on ? 'true' : 'false'); } + if (on) { + /* 打开这一刻才升级日期控件(见上面的说明)。两步都要: + 先绑自建下拉(兜底),再挂 antd(正主)。两个函数都会自判幂等。 */ + if (window.fbUpgradeRange) { window.fbUpgradeRange(); } + if (window.fbBootAntd) { window.fbBootAntd(); } + var t = document.getElementById('tk-title'); + if (t) { t.focus(); } + } + } + function antdPanelOpen() { + var d = document.querySelector('.ant-picker-dropdown'); + return !!(d && !d.classList.contains('ant-picker-dropdown-hidden')); + } + + document.addEventListener('click', function (e) { + var t = e.target; + if (!t || !t.closest) { return; } + if (t.closest('#tkEditBtn')) { e.preventDefault(); setOpen(true); return; } + if (t.closest('#tkEditX') || t.closest('#tkEditCancel')) { setOpen(false); return; } + var m = modal(); + if (m && t === m) { setOpen(false); } // 点遮罩关;点面板内部不关 + }); + document.addEventListener('keydown', function (e) { + if (e.key !== 'Escape') { return; } + var m = modal(); + if (!m || m.hidden) { return; } + if (antdPanelOpen()) { return; } // 让 antd 先收它自己的面板 + setOpen(false); + }); + })(); + </script> +<?php endif; ?> + +<?php /* ==================== 🔴 v55(用户 2026-10-17 第 2 条):状态**选完即改** ==================== + 用户原话:「任务详情里边,状态后面**不用加按钮**了。不是说选了之后再点一下才改, + 而是**只要一选就让它改**:选一下,状态就发生改变就行了。」 + + 做法:`<select>` 的 `change` → fetch 提交到**既有的** `tasks/status` + (它本来就认 `X-Requested-With` 并回 `{ok,id,status,word,msg}`,见 index.php)。 + 成功后**就地**更新页面上的三处显示,不刷页: + · 这个下拉自己(保持选中项); + · 页头副标题里的状态词(`page-head .sub`); + · 页头那颗「标记完成 / 重新打开」按钮的文案与语义(它跟着状态走)。 + 失败/拿不到 JSON 时**退回原生提交**(下拉还在那个 form 里,`form.submit()` 即可)—— + 与全站"没有 JS 照样能用"的口径一致。 + + ⚠️ 为什么不用「表单自动提交」(`this.form.submit()` 就完事): + 那会整页刷新、滚动位置丢掉,而用户在详情页往下翻着看子任务/评论—— + 改个状态就跳回顶部是"打一次断",用户要的是"选一下就改了"。 + ⚠️ 循环任务(`repeat_rule` + 有截止日):服务端会顺带排下一次, + 它的提示语在 `d.msg` 里 —— 所以这里要把 msg 显示出来, + 否则用户不知道"下一次也排上了"(那是 v24 就定下的行为,不能因为改交互而丢掉)。 + 提示落在页面顶部一条轻提示里(`#tkStFlash`),2.5 秒后自淡出。 + ⚠️ **幂等**:本段脚本在页面里只跑一次(详情页没有"替换正文再重跑"那套), + 但为了以后若有人复用这段,加了 `window.__fbTkStBound` 闸门。 */ ?> +<?php if ($canWrite): ?> + <div class="tk-flash" id="tkStFlash" hidden role="status" aria-live="polite"></div> + <script> + (function () { + if (window.__fbTkStBound) { return; } + window.__fbTkStBound = true; + + var sel = document.querySelector('select[data-st-select]'); + if (!sel) { return; } + var form = document.getElementById('tkStForm'); + if (!form) { return; } + + function flash(msg, ok) { + var el = document.getElementById('tkStFlash'); + if (!el) { return; } + el.textContent = msg; + el.hidden = false; + el.className = 'tk-flash' + (ok ? ' is-ok' : ' is-err'); + clearTimeout(el.__fbTimer); + el.__fbTimer = setTimeout(function () { el.hidden = true; }, 2500); + } + + /* 就地同步页面上的状态显示(不只是这个下拉)。 + ⚠️ 页头那颗「标记完成 / 重新打开」按钮**也跟状态走** —— + 选了"已完成"之后它还写着"标记完成"就是自相矛盾。 + 它是个 form(hidden status + submit),所以这里同时改它的 + hidden 值与按钮文字,与服务端渲染逻辑保持一致。 */ + function apply(status, word) { + var head = document.querySelector('.page-head .sub'); + if (head) { head.textContent = word; } + + var mark = document.getElementById('tkMarkBtn'); + if (mark) { + var st = mark.querySelector('input[name=status]'); + var btn = mark.querySelector('button[type=submit]'); + if (st && btn) { + if (status === 'done') { st.value = 'doing'; btn.textContent = '重新打开'; } + else { st.value = 'done'; btn.textContent = '标记完成'; } + } + } + /* 复选框(如果这一页有)也跟着走,免得与下拉说的不一致 */ + var chk = document.querySelector('.tk-check[data-tk-done="' + sel.getAttribute('data-st-id') + '"]'); + if (chk) { + chk.setAttribute('data-tk-status', status); + chk.classList.toggle('is-done', status === 'done'); + chk.setAttribute('aria-pressed', status === 'done' ? 'true' : 'false'); + } + } + + var busy = false; + sel.addEventListener('change', function () { + if (busy) { return; } + var want = sel.value; + if (!window.fetch || !window.FormData) { form.submit(); return; } // 老浏览器:原生提交 + busy = true; + /* 🔴🔴 **必须先构造 FormData、再禁用控件!** + 实测踩坑:`sel.disabled = true` 之后 `new FormData(form)` **不会带上这个字段** + (HTML 规范:禁用的表单控件不参与表单数据构造)⇒ 服务端收到空 status + ⇒ `normalizeStatus('')` 落回 'todo' ⇒ 看起来"选了没反应、还提示已标成待办"。 + ⚠️ 症状极具误导性:网络请求成功、JSON 也 ok、页面还给了"已标成待办"的提示, + 唯一的反常是"我明明选了进行中"。**先构建、后禁用** 就好。 */ + var fd = new FormData(form); + sel.disabled = true; + fetch(form.getAttribute('action'), { + method: 'POST', + body: fd, + credentials: 'same-origin', + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }).then(function (r) { + return r.json().catch(function () { return null; }); + }).then(function (d) { + busy = false; + sel.disabled = false; + if (d && d.ok) { + var st = String(d.status || want); + var word = String(d.word || ''); + sel.value = st; + apply(st, word); + flash(String(d.msg || ('已改成「' + word + '」。')), true); + return; + } + if (d && d.msg) { + /* 服务端明确说不让改(权限/找不到)⇒ 回退到原值并提示,不要假装成功 */ + sel.value = sel.getAttribute('data-st-was') || sel.value; + flash(String(d.msg), false); + return; + } + // 拿不到 JSON(会话过期 / 500)→ 退回原生提交,让服务端说话 + form.submit(); + }).catch(function () { + busy = false; + sel.disabled = false; + form.submit(); + }); + }); + /* 记下"改之前是什么",失败时好回退 */ + sel.setAttribute('data-st-was', sel.value); + })(); + </script> +<?php endif; ?>