/* ============================================================================ 首页任务面板 —— 就地改状态 / 挪看板 / 加子任务 / 传附件 / 发评论(v78) ---------------------------------------------------------------------------- 用户原话(2026-10-22): 「网页端,首页点击一个任务后,除了展示信息,应该有相应的功能,可以修改状态: 待办、进行中,可以挪动分组,可以拖动上传附件,可以添加评论,添加子任务等。 其目地,是为了让用户不离开首页,就能对任务进行简单的处理。」 这块面板由 `tasks/panel` 吐 HTML、`entry_popup.js` 塞进 `#fbEntryBody`。 本脚本只做一件事:**把面板里的每个动作发出去,再把服务端回来的新面板换上去**。 三条纪律(每条都对应一次踩过的坑): ① **一律事件委托挂 document**:面板 HTML 每次动作后整块换掉 (换完就是一批全新的节点),逐个 `addEventListener` 必然漏第二块。 ② **动作之后重画整块面板**,不自己改 DOM: 勾一条子任务会改「2/3」这个计数、传一个附件会改附件张数与缩略图墙、 挪看板会改上面那排 chip 的选中态 —— 逐处同步必然漏一处(v55 手写过一次, 结局是"下拉改了、页头还写着旧状态")。面板很小,重画比同步便宜且不会自相矛盾。 ③ **拿不到 JSON 就整页重载**:宁可刷一次,也不要在屏幕上留一个"看着成功了、 其实服务端没记上"的假状态。这与全站"没有 JS 也能用"的口径是同一条。 ============================================================================ */ (function () { 'use strict'; function bodyEl() { return document.getElementById('fbEntryBody'); } function panelEl() { var b = bodyEl(); return b ? b.querySelector('[data-tk-panel]') : null; } function tipEl() { return document.getElementById('fbEntryTip'); } function setTip(msg, err) { var t = tipEl(); if (!t) { return; } t.textContent = msg || ''; t.className = 'cal-modal-tip' + (err ? ' err' : ''); } /** CSRF:面板自己带一份(`data-tk-csrf`);没有就退回浮层遮罩里那份 */ function csrf() { var p = panelEl(); var el = p ? p.querySelector('[data-tk-csrf]') : null; if (!el) { el = document.querySelector('#fbEntryModal .fb-entry-csrf input[name="_csrf"]'); } return el ? el.value : ''; } function taskId() { var p = panelEl(); return p ? (p.getAttribute('data-tk-panel') || '0') : '0'; } /** 面板在 HTML 里声明自己的动作地址 —— 脚本里**不写死路由名**(改名不会漏改这一处) */ function urlOf(key) { var p = panelEl(); return p ? (p.getAttribute('data-tk-url-' + key) || '') : ''; } /** 用服务端回来的新面板替换旧的(成功失败都换:失败时服务端给的是**真实状态**) */ function paint(d, keepDraft) { var b = bodyEl(); if (!b || !d || typeof d.html !== 'string' || d.html === '') { return false; } /* ⚠️ 传附件时**保住**用户还没发出去的那段留言: 拖一张图就把写了一半的评论清空,是那种"不报错、但让人想骂人"的丢失。 其他动作(发评论/加子任务)不保 —— 那是他自己刚提交的内容,回填反而是错的。 */ var draft = '', draftTitle = ''; if (keepDraft) { var ta = b.querySelector('textarea[name="content"]'); if (ta) { draft = ta.value; } var ti = b.querySelector('.tk-inline-form input[name="title"]'); if (ti) { draftTitle = ti.value; } } b.innerHTML = d.html; if (draft !== '') { var ta2 = b.querySelector('textarea[name="content"]'); if (ta2) { ta2.value = draft; } } if (draftTitle !== '') { 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) { /* 挂不上就留在原生下拉,不阻断面板 */ } } /* #78:重画后选人组件是新节点 ⇒ 要重新接管(自带 data-pp-init 幂等标记)。 不补这一步的话,做过任何动作之后「更换负责人 / + 添加人」就点不动了。 */ if (typeof window.fbInitPickers === 'function') { try { window.fbInitPickers(b); } catch (err) { /* 忽略 */ } } return true; } /** * 把面板里的最新状态**顺手**同步到首页那张卡上(v78)。 * * 为什么要有这一步:面板是浮层,背后那张任务是"带着旧状态"站在那儿的。 * 用户改完关掉浮层,如果卡片上还写着「待办」,他会怀疑"到底改上了没有"。 * 只改状态徽标与复选框 —— 看板徽标/逾期色这些要重算的东西留给下次刷新, * 不在这里猜(猜错比不猜更糟)。 * * 🔴🔴 工单 #20(用户 2026-10-28,第二次提同一条需求): * 「首页,近期任务,如果勾选了完成,应该刷新一下这个列表,把完成的删掉。」 * 第一版只改了行首那颗复选框(home.php 里的脚本),**漏了这里**—— * 从面板点「已完成」时,行留在原地只把徽标改成「已完成」。 * 同一个用户动作(都是"标成完成")两条路径两种结果,用户复现的就是这条。 * * ⇒ 变更后是 done 时,也把这一行从首页卡片里摘掉。 * 真正干活的在 app.js 的 `window.fbHomeTaskDrop()`:它自己判断 * "行在不在 `[data-tk-home-host]` 里",**不在就完全不碰** * (任务看板页的行也在 `[data-task-row]` 上,那里必须保留该行)。 * * ⚠️ `d.status` 是**父任务**的状态(`task_panel_json()` 用 `panel_of` 找的父任务, * 见 app/helpers.php:506)。面板里勾的是**子任务**时,请求带 `panel_of`, * 回来的 status 仍是父任务的 ⇒ 这里不会因为"子任务做完了"就删掉父任务那一行。 * * ⚠️ 删行前先把 `wasStatus` 取出来:下面几行会改写 `data-tk-status`, * 改完再读就只能读到 done,概述卡不知道该减「在做」还是「待办」。 */ function syncRow(d) { if (!d || !d.status) { return; } var row = document.querySelector('[data-task-row="' + taskId() + '"]'); if (!row) { return; } /* 🔴 必须在改写 `data-tk-status` **之前**读 —— 见上面最后一条 ⚠️。 */ var chk0 = row.querySelector('.tk-check[data-tk-done]'); var was = chk0 ? (chk0.getAttribute('data-tk-status') || 'todo') : ''; var badge = row.querySelector('.kb-st'); if (badge) { badge.className = 'kb-st is-' + d.status + ' is-static'; if (d.word) { badge.textContent = d.word; } } if (chk0) { chk0.setAttribute('data-tk-status', d.status); chk0.classList.toggle('is-done', d.status === 'done'); chk0.setAttribute('aria-pressed', d.status === 'done' ? 'true' : 'false'); } /* 标成完成 ⇒ 从首页「近期任务」里摘掉(工单 #20)。 ⚠️ 用服务端回的 `d.status`,不是"我点之前猜的 next"。 */ if (d.status === 'done' && typeof window.fbHomeTaskDrop === 'function') { window.fbHomeTaskDrop(row, was); } } /** * 发一个动作。 * @param {string} url 动作地址(空 = 这条路由没登记,直接放弃) * @param {FormData} fd 已经带好 `_csrf` / `ajax_panel` 的请求体 * @param {boolean} keepDraft 换面板时要不要保住用户正在打的字(传附件时为 true) */ /** * 发请求并刷新面板。 * * @param keepDraft #78:重画后保住用户没发出去的评论草稿(传附件时用) * @param closeOnOk #78:成功后**关掉浮层**(「保存」用;其它动作不关) */ function send(url, fd, keepDraft, closeOnOk) { if (!url) { setTip('这一项暂时不能在首页改。', true); return; } if (!window.fetch || !window.FormData) { window.location.reload(); return; } fetch(url, { 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) { if (!d) { window.location.reload(); return; } // 会话过期 / 500:整页重载最诚实 /* 🔴 #78(用户 2026-11-05):「点击保存之后,按道理说应该直接把这个窗口关闭才对呀。」 ⇒ 保存成功就关窗,不再重画面板(用户已经不在看了)。 ⚠️ 关窗前**先更新背后那张首页卡片**:否则用户关掉浮层看到的还是旧标题/旧状态, "改了没生效"的错觉比不关窗更糟。 ⚠️ 失败(d.ok === false)时**不关** —— 让用户看到错误提示、就地改。 */ if (closeOnOk && d.ok !== false) { syncRow(d); if (typeof window.fbEntryClose === 'function') { window.fbEntryClose(); } else if (typeof window.fbCloseEntry === 'function') { window.fbCloseEntry(); } return; } paint(d, keepDraft); syncRow(d); setTip(d.msg || '', !d.ok); }).catch(function () { window.location.reload(); }); } /** 把「id + 几个附加字段」包成一个动作请求 */ /** * 发一个面板动作。 * * @param url 目标地址 * @param extra 额外字段 * @param closeOnOk #78:成功后**关掉浮层**(只有「保存」用)。 * 用户 2026-11-05:「点击保存之后,按道理说应该直接把这个窗口关闭才对呀。」 * ⚠️ 只给「保存」用:改状态 / 挪看板 / 加子任务 / 发评论 / 传附件 * 都还要继续在这个面板里操作,关窗会把用户赶走。 */ function act(url, extra, closeOnOk) { var fd = new FormData(); fd.append('_csrf', csrf()); fd.append('id', taskId()); fd.append('ajax_panel', '1'); Object.keys(extra || {}).forEach(function (k) { fd.append(k, extra[k]); }); send(url, fd, false, closeOnOk); } /* ---- ① 表单提交(加子任务 / 发评论 / 删附件)------------------------------ 面板里**只有需要打字或选文件的地方**才用真正的