From 40fd6c501c32e1ec2d62af28e321eac15332b4b9 Mon Sep 17 00:00:00 2001
From: laoyang
Date: Thu, 22 Oct 2026 12:40:14 +0800
Subject: [PATCH] =?UTF-8?q?v78=20=E6=9C=8D=E5=8A=A1=E7=AB=AF=EF=BC=9A?=
=?UTF-8?q?=E9=A6=96=E9=A1=B5=E4=BB=BB=E5=8A=A1=E6=94=B9=E6=88=90=E3=80=8C?=
=?UTF-8?q?=E5=B0=B1=E5=9C=B0=E5=A4=84=E7=90=86=E3=80=8D=E9=9D=A2=E6=9D=BF?=
=?UTF-8?q?=20+=20=E6=96=B0=E5=BB=BA=E4=BB=BB=E5=8A=A1=E6=90=AC=E5=88=B0?=
=?UTF-8?q?=E9=A6=96=E9=A1=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
用户原话:
「网页端,首页点击一个任务后,除了展示信息,应该有相应的功能,可以修改状态:
待办、进行中,可以挪动分组,可以拖动上传附件,可以添加评论,添加子任务等。
其目地,是为了让用户不离开首页,就能对任务进行简单的处理。」
「另外,在全部任务旁边,放一个新建任务的功能,其实就是把任务中的新建和编辑功能,
搬过来,用弹出框来处理。」
一、首页任务浮层由「只读信息」改成「可操作面板」
- 新增 templates/tasks/_panel.php:状态三段 / 看板 chips(带看板色)/
子任务(复用 _task_check.php)/ 附件拖放框(复用 _attachments.php)/ 讨论
- 新增路由 tasks/panel:只吐一块 HTML(找不到 404)
- 既有七个动作路由各加一条 ajax_panel 回包分支(状态 / 挪分组 / 新建子任务 /
评论增删 / 附件增删),没有新开写接口
- helpers.php 新增 task_panel_wanted() / task_panel_json();
fb_entry_json() 的 payload 增加 panel 字段(只有任务会给)
- 每次动作回**整块新面板**:一次动作会牵动子任务计数、附件张数、看板选中态,
逐处同步 DOM 必然漏一处
- 拖进来的附件**立刻上传**(任务已存在,不该再让用户点一次保存),
不复用 _stage_drop.php 那套「暂存 + 认领」
二、新建任务搬到首页
- 从 templates/tasks/index.php 抽出公共片段 templates/tasks/_add_modal.php
(字段与原来一字未改),首页与任务页共用同一份
- 新增 assets/js/task_add.js 管开关(两页共用,幂等绑定);
打开那一刻才 fbUpgradeRange() + fbBootAntd()(hidden 时宽高为 0,antd 会定位错乱)
- App\HomeCards 登记表里加 add => 新建任务,首页卡头与「全部任务」并排
- tasks/create 支持 back=home(白名单),新建完回首页
三、其它
- assets/js/task_panel.js:事件委托;动作后整块换面板;
顺手同步首页那张卡的状态徽标与复选框(关掉浮层不能还是旧状态);
传附件时保住用户写了一半的留言与子任务标题;取不到面板时退回只读信息
- app.css 追加面板样式(含 color-mix 的 @supports 兜底)与卡片窄到宽 2 时允许折行
验证:本地 e2e 1558/0、几何 428/0 + 660/0、v78 反向验证 11/0、
片段作用域体检 35/0、php -l 与 node --check 全过
---
app/HomeCards.php | 7 +
app/helpers.php | 81 +++++++++
assets/css/app.css | 73 ++++++++
assets/js/entry_popup.js | 45 ++++-
assets/js/task_add.js | 62 +++++++
assets/js/task_panel.js | 319 +++++++++++++++++++++++++++++++++
index.php | 116 ++++++++++++
templates/home.php | 32 ++++
templates/home/_card_tasks.php | 10 ++
templates/layout.php | 7 +
templates/tasks/_add_modal.php | 169 +++++++++++++++++
templates/tasks/_panel.php | 281 +++++++++++++++++++++++++++++
templates/tasks/index.php | 192 ++------------------
13 files changed, 1216 insertions(+), 178 deletions(-)
create mode 100644 assets/js/task_add.js
create mode 100644 assets/js/task_panel.js
create mode 100644 templates/tasks/_add_modal.php
create mode 100644 templates/tasks/_panel.php
diff --git a/app/HomeCards.php b/app/HomeCards.php
index ef4d7d0..64e8af9 100644
--- a/app/HomeCards.php
+++ b/app/HomeCards.php
@@ -78,6 +78,13 @@ final class HomeCards
'name' => '近期任务', 'desc' => '进行中与待办(含看板与状态)',
'w' => 4, 'h' => 8, 'route' => 'tasks', 'more' => '全部任务', 'n' => 0,
'mod' => 'tasks',
+ /* 🔴 v78(用户 2026-10-22):「在全部任务旁边,放一个新建任务的功能,
+ 其实就是把任务中的新建和编辑功能,搬过来,用弹出框来处理。」
+ ⇒ 卡片右上角在这一张上多一颗按钮;它打开的是**与任务页同一张**弹框
+ (`templates/tasks/_add_modal.php`)。
+ ⚠️ 登记在这里而不是在模板里写死:以后哪张卡要"新建"就加一行,
+ 模板只负责按有没有这个字段决定画不画。 */
+ 'add' => '新建任务',
],
/* 🔴 v71c(用户 2026-10-21):「通知消息这个,也是首页的卡片,可以调整顺序和大小。」
⇒ 它从"网格外的写死区块"变成**正规卡片**:可拖顺序、可调宽高、可关掉。
diff --git a/app/helpers.php b/app/helpers.php
index 4a19671..96617b9 100644
--- a/app/helpers.php
+++ b/app/helpers.php
@@ -443,6 +443,87 @@ function fb_entry_json(array $ev, string $detail = '', string $edit = '', array
'info' => $evOwn ? url('calendar/event', ['id' => $evRef, 'embed' => 'detail']) : '',
'repeat' => (string)($ev['repeat'] ?? '') !== '',
'occ' => (string)($ev['occ'] ?? ''),
+ /* 🔴 v78(用户 2026-10-22):**就地面板**的地址。
+ 首页「近期任务」点开时不再只给一行行只读信息,而是直接给出这张
+ 面板(状态 / 看板 / 子任务 / 附件 / 评论 都能就地改),用户不必离开首页。
+ ⚠️ 只有**任务**会给这个字段(`_card_tasks.php` 里传进来);
+ 日程/观影/旅程仍然走原来的"详情 + 编辑/删除"三颗按钮 ——
+ 它们没有"在首页上简单处理一下"的诉求,硬塞一个面板只会让浮层变复杂。 */
+ 'panel' => (string)($ev['panel'] ?? ''),
];
return json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT);
}
+
+/**
+ * 这次请求是不是「首页任务面板」发来的(v78)。
+ *
+ * 面板里的每一个动作都打**既有路由**,只多带一个 `ajax_panel=1`;
+ * 服务端据此把"flash + 302 跳转"换成"一段新面板 + 一句人话"。
+ *
+ * ⚠️ 判据只认 POST 里那个字段,**不要**顺手用 `X-Requested-With` 代替:
+ * `tasks/status` 早就用那个头区分"看板上的就地改(只要状态)"和"表单提交",
+ * 两件事混在一个判据里,改一处必然破坏另一处。
+ */
+function task_panel_wanted(): bool
+{
+ return (string)($_POST['ajax_panel'] ?? '') === '1';
+}
+
+/**
+ * 把「这一条任务的最新面板」当 JSON 回给前端(v78)。
+ *
+ * 为什么**每次都重画整块面板**,而不是让前端自己把那一行改掉:
+ * · 一个动作的影响面比看起来大 —— 勾一条子任务会改"2/3"这个计数;
+ * 传一个附件会改附件张数与缩略图墙;挪看板会改上面那排 chip 的选中态。
+ * 逐处同步(本项目在 v55 的状态下拉上已经手写过一次)是"漏一处就自相矛盾"。
+ * · 面板很小(一次 Task::find + 一个片段),重画一次比维护 N 处 DOM 同步便宜。
+ *
+ * ⚠️ `html` 在失败(ok=false)时**也照样给**:失败的典型原因是"权限不够/内容为空",
+ * 这正是最需要让用户看到"现在的真实状态"的时候,回一个空壳反而把旧状态留在屏幕上。
+ *
+ * @param int $taskId 要重画哪一条(子任务新增时传**父任务**,否则会把用户弹到子任务上)
+ * @param string $msg 给用户看的一句话(落在浮层底栏左侧)
+ * @param bool $ok 这次动作成不成功(只影响提示语气与 HTTP 码,不影响 html)
+ */
+function task_panel_json(int $taskId, string $msg = '', bool $ok = true): void
+{
+ $familyId = (int)\App\Auth::familyId();
+ $me = (int)\App\Auth::personId();
+ $t = \App\Task::find($familyId, $taskId, $me);
+ if (!$t) {
+ header('Content-Type: application/json; charset=utf-8', true, 404);
+ echo json_encode(['ok' => false, 'msg' => '找不到这项任务。'], JSON_UNESCAPED_UNICODE);
+ exit;
+ }
+ $people = \App\Person::roster($familyId, $me, 'member', '');
+ /* @人 高亮的候选名:与 tasks/show.php 同一份口径(真名 + 我的昵称都算),
+ 否则同一句话在详情页高亮、在首页面板不高亮。 */
+ $names = [];
+ foreach ($people as $p) {
+ $names[] = person_name($p);
+ $nm = trim((string)($p['name'] ?? ''));
+ if ($nm !== '') {
+ $names[] = $nm;
+ }
+ }
+ $names = array_values(array_unique(array_filter($names)));
+ $html = \App\View::renderPartial('tasks/_panel', [
+ 't' => $t,
+ 'people' => $people,
+ 'names' => $names,
+ 'boards' => \App\Task::boards($familyId),
+ 'categories' => \App\Task::categories($familyId),
+ 'canWrite' => \App\Auth::canWrite() && \App\Task::canWrite($t, $me),
+ 'meId' => $me,
+ ]);
+ header('Content-Type: application/json; charset=utf-8', true, $ok ? 200 : 422);
+ echo json_encode([
+ 'ok' => $ok,
+ 'msg' => $msg,
+ 'id' => $taskId,
+ 'status' => (string)($t['status'] ?? ''),
+ 'word' => \App\Task::statusWord((string)($t['status'] ?? 'todo')),
+ 'html' => $html,
+ ], JSON_UNESCAPED_UNICODE);
+ exit;
+}
diff --git a/assets/css/app.css b/assets/css/app.css
index 909312b..9b7e728 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -6615,3 +6615,76 @@ h5.md-h, h6.md-h { font-size: 14px; }
.hc-set-dim .select { width: 66px; }
.hc-set-n { width: 64px; }
.hc-set-null { width: 0; }
+
+/* ============================================================
+ v78(用户 2026-10-22):首页「近期任务」的就地面板
+ ------------------------------------------------------------
+ 用户原话:「网页端,首页点击一个任务后,除了展示信息,应该有相应的功能,
+ 可以修改状态:待办、进行中,可以挪动分组,可以拖动上传附件,可以添加评论,
+ 添加子任务等。其目地,是为了让用户不离开首页,就能对任务进行简单的处理。」
+
+ 这一段只画**面板本体**(壳、遮罩、底栏都沿用 `.cal-modal*` / `.fb-entry-panel`)。
+ 三块强调色都不新造:状态用既有的 `.kb-st`(三档颜色是它定义的唯一来源),
+ 看板 chip 用看板自己的 `--bc`(与看板列头、卡片上的点同一个色)。
+ ============================================================ */
+.tk-panel { display: flex; flex-direction: column; gap: 14px; }
+.tk-sec { display: flex; flex-direction: column; gap: 8px; }
+/* 段落之间的那条细线:面板里段落多(状态/看板/信息/子任务/附件/讨论),
+ 不划线会糊成一整块;用 line-2(比正文分隔线更淡)以免像个表单边框。 */
+.tk-sec + .tk-sec { border-top: 1px solid var(--line-2); padding-top: 14px; }
+.tk-sec-h {
+ display: flex; align-items: center; gap: 6px;
+ font-size: 13px; font-weight: 600; color: var(--ink);
+}
+.tk-sec-h .group-n { margin-left: 0; }
+/* 「状态」「看板」这两段是"标签 + 一排可点的东西",横排。
+ ⚠️ 用 flex-wrap:看板可能是七八个,窄屏要能折行而不是撑出面板。 */
+.tk-sec-inline { flex-direction: row; align-items: center; gap: 10px; flex-wrap: wrap; }
+.tk-sec-lb { flex: 0 0 34px; font-size: 12.5px; color: var(--faint); }
+.tk-seg { display: flex; gap: 6px; flex-wrap: wrap; }
+/* 面板里的状态徽标比卡片上的大一档(这里是要点的按钮,不是标签) */
+.tk-seg .kb-st { padding: 3px 12px; font-size: 12.5px; }
+.tk-chips { display: flex; gap: 6px; flex-wrap: wrap; }
+.tk-chip {
+ display: inline-flex; align-items: center; gap: 5px;
+ padding: 2px 10px; border-radius: 999px;
+ border: 1px solid var(--line); background: var(--surface);
+ font-size: 12px; color: var(--muted); cursor: pointer;
+ max-width: 100%;
+}
+.tk-chip:hover { border-color: var(--brand-2); color: var(--brand-ink); }
+/* 选中的看板:用**它自己的颜色**描边 + 淡底 —— 与看板列头那颗点是同一个色,
+ 用户不用读字就知道"这条现在在哪一摊事里"。 */
+.tk-chip.is-on {
+ border-color: var(--bc, var(--brand));
+ background: color-mix(in srgb, var(--bc, var(--brand)) 12%, transparent);
+ color: var(--ink); font-weight: 600;
+}
+/* ⚠️ color-mix 在老浏览器上整条声明无效 → 底色会是透明的。
+ 补一条纯 rgba 兜底(不指定色,统一淡灰),保证"选中"至少看得出来。 */
+@supports not (background: color-mix(in srgb, red 10%, transparent)) {
+ .tk-chip.is-on { background: var(--line-2); }
+}
+.tk-chip.is-static { cursor: default; }
+.tk-chip-dot {
+ width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto;
+ background: var(--bc, var(--faint));
+}
+/* 只读那几行(截止/负责人/分类/备注):面板里空间紧,行高比详情页收一点 */
+.tk-kv .kv-row { padding: 6px 0; }
+/* 加子任务那一行:输入框吃掉剩余宽度,按钮不缩 */
+.tk-inline-form { display: flex; align-items: center; gap: 8px; }
+.tk-inline-form .input { flex: 1 1 auto; min-width: 0; }
+.tk-inline-form .btn { flex: 0 0 auto; }
+/* 讨论里那条"删除":长得像链接(`.btn-inline` 负责去掉按钮的灰底,
+ `.link-quiet` 负责颜色),这里只把它推到本行最右。 */
+.cmt-who .cmt-x { margin-left: auto; }
+.tk-drop { margin-top: 4px; }
+.tk-drop .upbox { min-height: 64px; }
+@media (max-width: 640px) {
+ .tk-sec-lb { flex: 0 0 auto; width: 100%; }
+}
+
+/* v78:卡片窄到"宽 2"时,标题 +「全部任务」+「新建任务」三样挤不下 ——
+ 允许折行,别把标题压成 0 宽(那会只剩两颗按钮,看不出这是哪张卡片)。 */
+.hc-card.hc-w2 .hc-head { flex-wrap: wrap; }
diff --git a/assets/js/entry_popup.js b/assets/js/entry_popup.js
index bd2fe14..8201666 100644
--- a/assets/js/entry_popup.js
+++ b/assets/js/entry_popup.js
@@ -42,6 +42,11 @@
就地摘一条会漏;且这条系列在视图里可能横跨多天),以及
拿不到 JSON(老服务端 / 出错)。
⑦ **Esc / 点遮罩只关最上面那层**:antd 面板开着时先让它收(v38/v39 的坑)。
+ ⑧ **v78 新增"面板态"**(任务专属):`data-fb-entry` 里带 `panel` 时,正文换成
+ `tasks/panel` 吐的那块**可操作面板**(状态 / 看板 / 子任务 / 附件 / 评论
+ 都能就地改),底栏仍是「详情」那颗(跳任务详情页)。
+ 面板里的动作由 `task_panel.js` 委托处理 —— **别在这里加**:
+ 两个文件各管一段,谁也不会把谁的监听覆盖掉。
⚠️ 本脚本**全站加载**(layout.php):三处入口分散在不同页面,按需加载的判据
会在这些页面之间来回漏;它只有几 KB,不值得为省这点流量赌漏加载。
============================================================================ */
@@ -120,7 +125,9 @@
var isEvent = (data.source === 'event');
if (titleEl()) { titleEl().textContent = data.title || '详情'; }
if (bodyEl()) { bodyEl().innerHTML = '' + rowsHtml(data.rows) + '
'; }
- if (panelEl()) { panelEl().classList.remove('is-wide'); }
+ /* 🔴 v78:任务带 `panel`(可就地操作的那块)—— 面板比只读信息宽一号,
+ 所以这里按**有没有 panel** 决定宽度,而不是按 source。 */
+ if (panelEl()) { panelEl().classList.toggle('is-wide', !!data.panel); }
setTip('');
/* ⚠️ 顺序要紧:**先** footShow('detail')(它会把所有按钮收掉、只点亮「详情」),
@@ -146,6 +153,42 @@
if (ed) { ed.hidden = !(isEvent && data.edit); }
var de = byId('fbEntryDelete');
if (de) { de.hidden = !(isEvent && data.del); }
+
+ /* 🔴 v78:任务面板 —— 把正文换成"能直接动手"的那一块。
+ 放在最后(上面的只读 rows 已经画好),取不到就保留那份只读信息。 */
+ if (data.panel) { loadPanel(data.panel, data); }
+ }
+
+ /**
+ * 取回任务面板并塞进正文(v78)。
+ *
+ * 用户原话:「网页端,首页点击一个任务后,除了展示信息,应该有相应的功能…
+ * 其目地,是为了让用户不离开首页,就能对任务进行简单的处理。」
+ *
+ * ⚠️ 失败时**不要清空正文**:退回 `rows` 那份只读信息(用户至少还能看清这条是什么),
+ * 并且把宽度收回去 —— 一块空的宽面板比"只有信息"更像坏了。
+ * ⚠️ 面板里的事件全部由 `task_panel.js` 委托处理(动态换 HTML 不重绑),
+ * 这里只负责"取回来放进去"。
+ */
+ function loadPanel(url, data) {
+ if (!url || !window.fetch) { return; }
+ if (bodyEl()) { bodyEl().innerHTML = '
正在载入…
'; } + fetch(url, { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) + .then(function (r) { + if (!r.ok) { throw new Error('panel'); } + return r.text(); + }) + .then(function (html) { + if (!bodyEl() || html.indexOf('data-tk-panel') < 0) { throw new Error('panel'); } + bodyEl().innerHTML = html; + runInlineScripts(bodyEl(), html); + }) + .catch(function () { + if (!bodyEl()) { return; } + bodyEl().innerHTML = '