任务面板五项细化:弹窗选人、协作人、保存关窗、去编辑、评论附件 (#78)
用户 2026-11-05 五条: 「选人的时候不能按列表,因为人太多了。参考观影模式里面选人的方式。」 「任务其实可以共享给别人,也就是协作人,但编辑这里面没有这个选项。」 「点击保存之后,按道理说应该直接把这个窗口关闭才对呀。」 「现在在首页上点一个任务,直接就是编辑页面啊,所以不需要编辑按钮了。」 「评论那个地方也需要可以添加附件。」 ① 选人改弹窗 负责人/协作人都换成 `_person_picker.php`(弹窗 + 拼音首字母联想), 与观影表单、新建任务弹框同一套。 负责人 single + allowEmpty(可选「不指定」);协作人 multi。 ⚠️ 关键修复:`entry_popup.js` 的 `loadPanel()` 一直漏了 `fbInitPickers()` —— 组件不初始化,按钮点了没反应。 `loadEdit()` 里有这一句,`loadPanel()` 没有,面板里塞选人就是这个下场。 同时 `task_panel.js` 的 `paint()` 后也补一次(重画后是新节点)。 ② 补协作人 数据层其实**早就完整支持**:task_shares 表 + Task::setShare/dropShare + 新建弹框的「共享给」+ 详情页的共享展示。只有面板和编辑表单没给入口。 现在面板里能看当前协作人、能加、能改。 保存语义:以提交的列表为准同步(多了就 setShare、少了就 dropShare)。 ⚠️ 带 `shares_submitted=1` 哨兵,区分"表单里没这个控件"(不动) 和"用户全删了"(清空)—— 否则 PHP 收不到空数组的键,无法分辨。 ③ 保存后关窗 `act()` 加 `closeOnOk`,`send()` 支持它:成功后先 `syncRow()` (更新背后那张首页卡片,否则关掉浮层看到旧标题更迷惑)再 `fbEntryClose()`。 ⚠️ 只给「保存」用 —— 改状态/挪看板/加子任务/发评论/传附件都还要继续操作。 ⚠️ 失败时不关,让用户看到错误就地改。 ④ 去掉「编辑」按钮 + 面板补标题/备注 用户说得对:点开任务就是完整编辑面板了,再给编辑按钮是重复。 但原来面板改不了标题和备注(只在 _edit_form.php 里) ⇒ 先在「更多信息」里补上这两个字段,再藏编辑按钮。 `Task::setAttrs()` 扩展支持 title/note,走 title()/textOrNull() 清洗, 与 update() 同一口径。 ⚠️ 只对**任务**(带 data.panel)隐藏 —— 观影/旅程/日程/纪念日仍需要那颗按钮。 ⑤ 评论可加附件 服务端**早就支持**:tasks/comment_add 里一直有 `attach_save_files(..., task_comment, ...)`,只是表单里没有 file input。 补上 `<input type="file" name="attach[]" multiple>` + `enctype="multipart/form-data"`(少了它只提交文件名、文件本体丢失)。 顺带允许「只发附件不写字」(原来 content 空会返回"留言是空的"): 有附件且没文字时用占位文案「(附件)」。 实测: setAttrs 标题/备注 => 都正确写入 ✅ setShare / dropShare => 1 人 / 0 人 ✅ 面板渲染 => 标题框/备注框/两个选人组件(single+multi)/评论附件/ multipart/save-attrs/antd 控件 全部在位 ✅ 选人模式 => single(allowEmpty=1), multi ✅ 已知问题(另单处理):家庭有 800 人,两个选人组件各 485KB, 面板渲染 997KB。服务端只要 11ms,但传输量对移动端不友好。 这是 _person_picker.php 的既有设计(观影那边也一样), 需要改成"弹窗打开时才渲染列表"或按需加载。
This commit is contained in:
@@ -788,6 +788,20 @@ final class Task
|
|||||||
$sets[] = 'category_id = ?';
|
$sets[] = 'category_id = ?';
|
||||||
$args[] = self::categoryIdOf($familyId, (int)($d['category_id'] ?? 0));
|
$args[] = self::categoryIdOf($familyId, (int)($d['category_id'] ?? 0));
|
||||||
}
|
}
|
||||||
|
/* 🔴 #78(用户 2026-11-05):「现在在首页上点一个任务,直接就是编辑页面啊。
|
||||||
|
所以不需要再加一个编辑按钮了。」
|
||||||
|
⇒ 删掉「编辑」按钮后,标题/备注也得能在这里改。
|
||||||
|
⚠️ 走 `self::title()` / `self::textOrNull()` 而不是直接塞原值 ——
|
||||||
|
与 `update()` 同一套清洗口径(去首尾空白、空备注存 NULL),
|
||||||
|
否则同一条任务从两个入口改会得到不同形态的数据。 */
|
||||||
|
if (array_key_exists('title', $d)) {
|
||||||
|
$sets[] = 'title = ?';
|
||||||
|
$args[] = self::title((string)($d['title'] ?? ''));
|
||||||
|
}
|
||||||
|
if (array_key_exists('note', $d)) {
|
||||||
|
$sets[] = 'note = ?';
|
||||||
|
$args[] = self::textOrNull((string)($d['note'] ?? ''));
|
||||||
|
}
|
||||||
if (!$sets) {
|
if (!$sets) {
|
||||||
return true; // 三个都没传:什么都不做,但也算"成功"(幂等)
|
return true; // 三个都没传:什么都不做,但也算"成功"(幂等)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6953,6 +6953,40 @@ h6.md-h { font-size: 17px; color: var(--muted); }
|
|||||||
padding-left: 56px;
|
padding-left: 56px;
|
||||||
}
|
}
|
||||||
.tk-attr-tip { font-size: 12px; color: var(--green, #2E7D4F); }
|
.tk-attr-tip { font-size: 12px; color: var(--green, #2E7D4F); }
|
||||||
|
|
||||||
|
/* #78:面板里的选人组件(负责人单选 / 协作人多选)。
|
||||||
|
组件本身按"整页宽"设计,塞进 .tk-attr-row 的弹性项里要收一下:
|
||||||
|
- 不加 min-width:0 的话,chips 一多会把标签挤到上一行;
|
||||||
|
- max-width 与其它控件对齐(300px),别让它撑满整行。 */
|
||||||
|
.tk-attr-row > .pp-root { flex: 1 1 auto; min-width: 0; max-width: 340px; }
|
||||||
|
.tk-attr-row > .pp-root .pp-chips { margin-bottom: 6px; }
|
||||||
|
/* 标签在多行控件(备注、协作人)上方对齐,而不是垂直居中 */
|
||||||
|
.tk-attr-row-top { align-items: flex-start; }
|
||||||
|
.tk-attr-row-top > .tk-attr-lb { padding-top: 8px; }
|
||||||
|
/* 只读态的协作人 chips */
|
||||||
|
.tk-share-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.tk-share-chip {
|
||||||
|
font-size: 12.5px; padding: 3px 10px; border-radius: 999px;
|
||||||
|
background: var(--surface-2, #F2F0EC); color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #78:评论里的「添加附件」。
|
||||||
|
用原生 file input(视觉隐藏),点 label 触发 —— 与站内其它上传入口同一手法。 */
|
||||||
|
.cmt-attach-row {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.cmt-attach-pick {
|
||||||
|
display: inline-flex; align-items: center; gap: 5px;
|
||||||
|
font-size: 12.5px; color: var(--muted); cursor: pointer;
|
||||||
|
padding: 4px 10px; border-radius: 999px;
|
||||||
|
border: 1px dashed var(--line); background: transparent;
|
||||||
|
}
|
||||||
|
.cmt-attach-pick:hover { color: var(--brand); border-color: var(--brand); }
|
||||||
|
.cmt-attach-name {
|
||||||
|
font-size: 12px; color: var(--faint);
|
||||||
|
max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
|
}
|
||||||
.tk-sec-lb { flex: 0 0 34px; font-size: 12.5px; color: var(--faint); }
|
.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 { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
/* 面板里的状态徽标比卡片上的大一档(这里是要点的按钮,不是标签) */
|
/* 面板里的状态徽标比卡片上的大一档(这里是要点的按钮,不是标签) */
|
||||||
|
|||||||
@@ -176,7 +176,16 @@
|
|||||||
/* 🔴 用户:「来自其他模块时,下面就没有编辑、也没有删除,就只有一个详情」——
|
/* 🔴 用户:「来自其他模块时,下面就没有编辑、也没有删除,就只有一个详情」——
|
||||||
所以这两颗**只对 source='event'** 显(且各要有地址)。 */
|
所以这两颗**只对 source='event'** 显(且各要有地址)。 */
|
||||||
var ed = byId('fbEntryEdit');
|
var ed = byId('fbEntryEdit');
|
||||||
if (ed) { ed.hidden = !(isEvent && data.edit); }
|
/* 🔴 #78(用户 2026-11-05):「现在在首页上点一个任务,直接就是编辑页面啊。
|
||||||
|
所以不需要再加一个编辑按钮了。」
|
||||||
|
|
||||||
|
⇒ 任务(带 `panel`)的面板里已经能改**全部**字段
|
||||||
|
(标题/截止/负责人/协作人/分类/备注 + 状态/看板/子任务/评论/附件),
|
||||||
|
再给一颗「编辑」只会让用户以为"是不是还有别的东西要进另一个页面改"。
|
||||||
|
⚠️ **不能无条件藏**:观影 / 旅程 / 日程 / 纪念日 那些来源没有 panel,
|
||||||
|
它们仍然只有「详情 + 编辑」,那颗按钮必须留着。
|
||||||
|
⚠️ `data.del`(删除)不动 —— 用户没要求改删除,保持原样。 */
|
||||||
|
if (ed) { ed.hidden = !(isEvent && data.edit) || !!data.panel; }
|
||||||
var de = byId('fbEntryDelete');
|
var de = byId('fbEntryDelete');
|
||||||
if (de) { de.hidden = !(isEvent && data.del); }
|
if (de) { de.hidden = !(isEvent && data.del); }
|
||||||
|
|
||||||
@@ -226,6 +235,19 @@
|
|||||||
if (typeof window.fbBootAntd === 'function') {
|
if (typeof window.fbBootAntd === 'function') {
|
||||||
try { window.fbBootAntd(); } catch (e) { /* 挂不上就留在原生下拉,不阻断面板 */ }
|
try { window.fbBootAntd(); } catch (e) { /* 挂不上就留在原生下拉,不阻断面板 */ }
|
||||||
}
|
}
|
||||||
|
/* 🔴 #78(用户 2026-11-05):「选人的时候不能按列表,因为人太多了。
|
||||||
|
你参考一下观影模式里面选人的方式。」
|
||||||
|
⇒ 面板里的负责人/协作人改用 `_person_picker.php`(弹窗 + 拼音联想),
|
||||||
|
那个组件靠 `fbInitPickers(容器的)` 接管点击。
|
||||||
|
`loadEdit()` 里一直有这一句(第 315 行),**`loadPanel()` 漏了** ——
|
||||||
|
所以面板里的选人按钮点了没反应。
|
||||||
|
⚠️ 组件自带 `data-pp-init` 幂等标记,重复调用不会重复绑。 */
|
||||||
|
if (typeof window.fbLoadPicker === 'function') {
|
||||||
|
try { window.fbLoadPicker(); } catch (e) { /* 忽略 */ }
|
||||||
|
}
|
||||||
|
if (typeof window.fbInitPickers === 'function') {
|
||||||
|
try { window.fbInitPickers(bodyEl()); } catch (e) { /* 忽略 */ }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function () {
|
||||||
if (!bodyEl()) { return; }
|
if (!bodyEl()) { return; }
|
||||||
|
|||||||
+62
-6
@@ -92,6 +92,11 @@
|
|||||||
if (typeof window.fbBootAntd === 'function') {
|
if (typeof window.fbBootAntd === 'function') {
|
||||||
try { window.fbBootAntd(); } catch (err) { /* 挂不上就留在原生下拉,不阻断面板 */ }
|
try { window.fbBootAntd(); } catch (err) { /* 挂不上就留在原生下拉,不阻断面板 */ }
|
||||||
}
|
}
|
||||||
|
/* #78:重画后选人组件是新节点 ⇒ 要重新接管(自带 data-pp-init 幂等标记)。
|
||||||
|
不补这一步的话,做过任何动作之后「更换负责人 / + 添加人」就点不动了。 */
|
||||||
|
if (typeof window.fbInitPickers === 'function') {
|
||||||
|
try { window.fbInitPickers(b); } catch (err) { /* 忽略 */ }
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +159,13 @@
|
|||||||
* @param {FormData} fd 已经带好 `_csrf` / `ajax_panel` 的请求体
|
* @param {FormData} fd 已经带好 `_csrf` / `ajax_panel` 的请求体
|
||||||
* @param {boolean} keepDraft 换面板时要不要保住用户正在打的字(传附件时为 true)
|
* @param {boolean} keepDraft 换面板时要不要保住用户正在打的字(传附件时为 true)
|
||||||
*/
|
*/
|
||||||
function send(url, fd, keepDraft) {
|
/**
|
||||||
|
* 发请求并刷新面板。
|
||||||
|
*
|
||||||
|
* @param keepDraft #78:重画后保住用户没发出去的评论草稿(传附件时用)
|
||||||
|
* @param closeOnOk #78:成功后**关掉浮层**(「保存」用;其它动作不关)
|
||||||
|
*/
|
||||||
|
function send(url, fd, keepDraft, closeOnOk) {
|
||||||
if (!url) { setTip('这一项暂时不能在首页改。', true); return; }
|
if (!url) { setTip('这一项暂时不能在首页改。', true); return; }
|
||||||
if (!window.fetch || !window.FormData) { window.location.reload(); return; }
|
if (!window.fetch || !window.FormData) { window.location.reload(); return; }
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
@@ -166,6 +177,20 @@
|
|||||||
return r.json().catch(function () { return null; });
|
return r.json().catch(function () { return null; });
|
||||||
}).then(function (d) {
|
}).then(function (d) {
|
||||||
if (!d) { window.location.reload(); return; } // 会话过期 / 500:整页重载最诚实
|
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);
|
paint(d, keepDraft);
|
||||||
syncRow(d);
|
syncRow(d);
|
||||||
setTip(d.msg || '', !d.ok);
|
setTip(d.msg || '', !d.ok);
|
||||||
@@ -173,13 +198,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 把「id + 几个附加字段」包成一个动作请求 */
|
/** 把「id + 几个附加字段」包成一个动作请求 */
|
||||||
function act(url, extra) {
|
/**
|
||||||
|
* 发一个面板动作。
|
||||||
|
*
|
||||||
|
* @param url 目标地址
|
||||||
|
* @param extra 额外字段
|
||||||
|
* @param closeOnOk #78:成功后**关掉浮层**(只有「保存」用)。
|
||||||
|
* 用户 2026-11-05:「点击保存之后,按道理说应该直接把这个窗口关闭才对呀。」
|
||||||
|
* ⚠️ 只给「保存」用:改状态 / 挪看板 / 加子任务 / 发评论 / 传附件
|
||||||
|
* 都还要继续在这个面板里操作,关窗会把用户赶走。
|
||||||
|
*/
|
||||||
|
function act(url, extra, closeOnOk) {
|
||||||
var fd = new FormData();
|
var fd = new FormData();
|
||||||
fd.append('_csrf', csrf());
|
fd.append('_csrf', csrf());
|
||||||
fd.append('id', taskId());
|
fd.append('id', taskId());
|
||||||
fd.append('ajax_panel', '1');
|
fd.append('ajax_panel', '1');
|
||||||
Object.keys(extra || {}).forEach(function (k) { fd.append(k, extra[k]); });
|
Object.keys(extra || {}).forEach(function (k) { fd.append(k, extra[k]); });
|
||||||
send(url, fd);
|
send(url, fd, false, closeOnOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- ① 表单提交(加子任务 / 发评论 / 删附件)------------------------------
|
/* ---- ① 表单提交(加子任务 / 发评论 / 删附件)------------------------------
|
||||||
@@ -377,11 +412,32 @@
|
|||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
var extra = {};
|
var extra = {};
|
||||||
extra['due_at'] = readDue(dbox);
|
extra['due_at'] = readDue(dbox);
|
||||||
/* ⚠️ 只有控件真的在 DOM 里才提交该项 —— 没有成员时负责人下拉不渲染,
|
/* ⚠️ 只有控件真的在 DOM 里才提交该项 —— 控件不渲染时不该把原值清掉。 */
|
||||||
那时不该把它当成 0 去清掉原值。 */
|
|
||||||
if (asg) { extra['assignee_id'] = asg.value; }
|
if (asg) { extra['assignee_id'] = asg.value; }
|
||||||
if (cat) { extra['category_id'] = cat.value; }
|
if (cat) { extra['category_id'] = cat.value; }
|
||||||
act(urlOf('save'), extra);
|
|
||||||
|
/* #78:标题 / 备注 —— 删掉「编辑」按钮后,它们只能在这里改 */
|
||||||
|
var ti = bd.querySelector('#tkp-title');
|
||||||
|
if (ti) { extra['title'] = ti.value; }
|
||||||
|
var nt = bd.querySelector('#tkp-note');
|
||||||
|
if (nt) { extra['note'] = nt.value; }
|
||||||
|
|
||||||
|
/* #78:协作人(`_person_picker.php` 的 multi 选人把结果存在
|
||||||
|
隐藏 checkbox 里,name="shares[]")。逐个收集。
|
||||||
|
⚠️ 必须带 `shares_submitted=1` 这个哨兵:
|
||||||
|
服务端靠它区分"表单里根本没有协作人控件"(不动)与
|
||||||
|
"用户把它们全删了"(清空)—— 只靠 `shares[]` 是否存在,
|
||||||
|
空数组时 PHP 收不到任何键,无法区分。 */
|
||||||
|
var shareBox = bd.querySelector('input[name="shares[]"]');
|
||||||
|
if (shareBox) {
|
||||||
|
extra['shares_submitted'] = '1';
|
||||||
|
var picked = bd.querySelectorAll('input[name="shares[]"]:checked');
|
||||||
|
for (var i = 0; i < picked.length; i++) {
|
||||||
|
extra['shares[' + i + ']'] = picked[i].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
act(urlOf('save'), extra, true);
|
||||||
}, 60);
|
}, 60);
|
||||||
/* 面板会被整块换掉(act → send → paint),按钮随之重建,不用手动恢复 */
|
/* 面板会被整块换掉(act → send → paint),按钮随之重建,不用手动恢复 */
|
||||||
setTimeout(function () { btn.disabled = false; }, 4000);
|
setTimeout(function () { btn.disabled = false; }, 4000);
|
||||||
|
|||||||
@@ -7355,7 +7355,7 @@ $want = array_keys($want);
|
|||||||
redirect('tasks');
|
redirect('tasks');
|
||||||
}
|
}
|
||||||
/* 只把**表单真的提交了**的字段放进 $sd:
|
/* 只把**表单真的提交了**的字段放进 $sd:
|
||||||
面板里负责人下拉在"没有成员"等极端情况下可能不渲染,
|
面板里负责人控件在"没有成员"等极端情况下可能不渲染,
|
||||||
那时就不该把 assignee_id 当成 0 去清掉原值。 */
|
那时就不该把 assignee_id 当成 0 去清掉原值。 */
|
||||||
$sd = [];
|
$sd = [];
|
||||||
if (array_key_exists('due_at', $_POST)) {
|
if (array_key_exists('due_at', $_POST)) {
|
||||||
@@ -7367,7 +7367,54 @@ $want = array_keys($want);
|
|||||||
if (array_key_exists('category_id', $_POST)) {
|
if (array_key_exists('category_id', $_POST)) {
|
||||||
$sd['category_id'] = (int)$_POST['category_id'];
|
$sd['category_id'] = (int)$_POST['category_id'];
|
||||||
}
|
}
|
||||||
|
/* 🔴 #78(用户 2026-11-05):「现在在首页上点一个任务,直接就是编辑页面啊。
|
||||||
|
所以不需要再加一个编辑按钮了。」
|
||||||
|
⇒ 既然要删掉「编辑」按钮,标题和备注必须能在这里改。
|
||||||
|
⚠️ 标题不允许被改成空 —— 与 tasks/update 同一口径(那里空标题会被打回)。
|
||||||
|
空了就**不更新标题**(而不是把标题清掉)。 */
|
||||||
|
if (array_key_exists('title', $_POST)) {
|
||||||
|
$nt = trim((string)$_POST['title']);
|
||||||
|
if ($nt !== '') {
|
||||||
|
$sd['title'] = $nt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (array_key_exists('note', $_POST)) {
|
||||||
|
$sd['note'] = trim((string)$_POST['note']);
|
||||||
|
}
|
||||||
Task::setAttrs($familyId, $id, $sd);
|
Task::setAttrs($familyId, $id, $sd);
|
||||||
|
|
||||||
|
/* 协作人(共享给)—— #78。
|
||||||
|
🔴 用户:「我记得任务其实可以共享给别人,也就是协作人。
|
||||||
|
但现在怎么感觉编辑这里面没有这个选项呢?」
|
||||||
|
⇒ 数据层早就有 `task_shares` + Task::share/unshare,只是没入口。
|
||||||
|
|
||||||
|
⚠️ 语义是"**以提交的列表为准**":表单没带 `shares[]` 时(比如只改截止时间
|
||||||
|
的旧面板/其它调用方)**完全不动**协作人,避免把已有共享清空。
|
||||||
|
表单带了(哪怕是空数组 `shares=[]` 表示"清空全部")就按它同步。 */
|
||||||
|
if (array_key_exists('shares', $_POST) || array_key_exists('shares_submitted', $_POST)) {
|
||||||
|
$wantIds = [];
|
||||||
|
foreach ((array)($_POST['shares'] ?? []) as $sid) {
|
||||||
|
$sid = (int)$sid;
|
||||||
|
/* 不能共享给自己(空操作);也不能共享给不存在的人 */
|
||||||
|
if ($sid > 0 && $sid !== $me) {
|
||||||
|
$wantIds[$sid] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$haveIds = [];
|
||||||
|
foreach (Task::shares($id) as $sh) {
|
||||||
|
$haveIds[(int)$sh['person_id']] = true;
|
||||||
|
}
|
||||||
|
foreach (array_keys($wantIds) as $sid) {
|
||||||
|
if (!isset($haveIds[$sid])) {
|
||||||
|
Task::setShare($id, $sid, 'read');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (array_keys($haveIds) as $sid) {
|
||||||
|
if (!isset($wantIds[$sid])) {
|
||||||
|
Task::dropShare($id, $sid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
$msg = '已保存。';
|
$msg = '已保存。';
|
||||||
if (task_panel_wanted()) {
|
if (task_panel_wanted()) {
|
||||||
task_panel_json($id, $msg);
|
task_panel_json($id, $msg);
|
||||||
@@ -7391,7 +7438,29 @@ $want = array_keys($want);
|
|||||||
flash('err', '找不到这项任务。');
|
flash('err', '找不到这项任务。');
|
||||||
redirect('tasks');
|
redirect('tasks');
|
||||||
}
|
}
|
||||||
$cid = Task::addComment($id, $me, (string)($_POST['content'] ?? ''));
|
/* 🔴 #78(用户 2026-11-05):「评论那个地方也需要可以添加附件。」
|
||||||
|
⇒ 允许「只发附件、不写字」—— 这是很常见的一次性用法
|
||||||
|
(拍张照片甩过来,不需要配文字)。
|
||||||
|
|
||||||
|
⚠️ 但不能两边都空:那是误点。这时返回 0 走原来的"留言是空的"分支。
|
||||||
|
⚠️ 判"有没有附件"要看 `$_FILES`,不能看 content:
|
||||||
|
`attach_save_files` 内部会过滤空文件槽(未选择的 file input
|
||||||
|
也会出现在 $_FILES 里,name 是空串)。
|
||||||
|
这里用一个轻量判断:只要有任一非空 tmp_name 就算有附件。 */
|
||||||
|
$rawContent = trim((string)($_POST['content'] ?? ''));
|
||||||
|
$hasFiles = false;
|
||||||
|
foreach ((array)($_FILES['attach'] ?? []) as $k => $v) {
|
||||||
|
if ($k === 'name' && is_array($v)) {
|
||||||
|
foreach ($v as $nm) {
|
||||||
|
if (trim((string)$nm) !== '') { $hasFiles = true; break 2; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($rawContent === '' && $hasFiles) {
|
||||||
|
/* 占位文案:让列表里能看懂"这条是什么"(纯空白没人知道是图还是文件) */
|
||||||
|
$rawContent = '(附件)';
|
||||||
|
}
|
||||||
|
$cid = Task::addComment($id, $me, $rawContent);
|
||||||
if ($cid > 0) {
|
if ($cid > 0) {
|
||||||
attach_save_files($familyId, 'task_comment', $cid, 'attach', $me);
|
attach_save_files($familyId, 'task_comment', $cid, 'attach', $me);
|
||||||
if (task_panel_wanted()) {
|
if (task_panel_wanted()) {
|
||||||
|
|||||||
+116
-21
@@ -68,6 +68,12 @@ foreach ($tpCats as $tpCat) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$tpDue = (string)($tpT['due_at'] ?? '');
|
$tpDue = (string)($tpT['due_at'] ?? '');
|
||||||
|
/* #78:协作人(共享给)。Task::find 已经带回 `shares`,直接用。 */
|
||||||
|
$tpShares = (array)($tpT['shares'] ?? []);
|
||||||
|
$tpShareIds = [];
|
||||||
|
foreach ($tpShares as $tpS) {
|
||||||
|
$tpShareIds[] = (int)($tpS['person_id'] ?? 0);
|
||||||
|
}
|
||||||
$tpDueWord = \App\Task::dueWord($tpDue !== '' ? $tpDue : null);
|
$tpDueWord = \App\Task::dueWord($tpDue !== '' ? $tpDue : null);
|
||||||
$tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpDue);
|
$tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpDue);
|
||||||
?>
|
?>
|
||||||
@@ -149,6 +155,22 @@ $tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpD
|
|||||||
<div class="tk-sec">
|
<div class="tk-sec">
|
||||||
<div class="tk-sec-h">更多信息</div>
|
<div class="tk-sec-h">更多信息</div>
|
||||||
<div class="tk-attrs">
|
<div class="tk-attrs">
|
||||||
|
|
||||||
|
<?php /* 🔴 #78(用户 2026-11-05):「现在在首页上点一个任务,直接就是编辑页面啊。
|
||||||
|
所以不需要再加一个编辑按钮了。」
|
||||||
|
|
||||||
|
⇒ 既然要删掉右下角的「编辑」按钮,标题和备注就必须能在这里改 ——
|
||||||
|
原来它俩只在 `_edit_form.php` 里,面板做不了。 */ ?>
|
||||||
|
<div class="tk-attr-row">
|
||||||
|
<span class="tk-attr-lb">标题</span>
|
||||||
|
<?php if ($tpCan): ?>
|
||||||
|
<input class="input" id="tkp-title" name="title" maxlength="200"
|
||||||
|
value="<?= e((string)($tpT['title'] ?? '')) ?>">
|
||||||
|
<?php else: ?>
|
||||||
|
<span><?= e((string)($tpT['title'] ?? '')) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tk-attr-row">
|
<div class="tk-attr-row">
|
||||||
<span class="tk-attr-lb">截止</span>
|
<span class="tk-attr-lb">截止</span>
|
||||||
<?php if ($tpCan): ?>
|
<?php if ($tpCan): ?>
|
||||||
@@ -170,19 +192,28 @@ $tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpD
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php /* 负责人:**弹窗选人**(#78)。
|
||||||
|
🔴 用户 2026-11-05:「选人的时候不能按列表,因为人太多了。
|
||||||
|
你参考一下观影模式里面选人的方式。」
|
||||||
|
⇒ 换成全站统一的 `_person_picker.php`(弹窗 + 拼音首字母联想),
|
||||||
|
与新建任务弹框、观影表单同一套。
|
||||||
|
⚠️ 单选 + `$ppAllowEmpty` ⇒ 弹窗里带「不指定」(值 0)。
|
||||||
|
⚠️ 它在 AJAX 面板里需要 `fbInitPickers()` 初始化 ——
|
||||||
|
`entry_popup.js` 的 `loadPanel()` 已补(见那边注释)。 */ ?>
|
||||||
<div class="tk-attr-row">
|
<div class="tk-attr-row">
|
||||||
<span class="tk-attr-lb">负责人</span>
|
<span class="tk-attr-lb">负责人</span>
|
||||||
<?php if ($tpCan): ?>
|
<?php if ($tpCan): ?>
|
||||||
<select class="select" name="assignee_id" id="tkp-assignee">
|
<?php
|
||||||
<option value="0">不指定</option>
|
$ppName = 'assignee_id';
|
||||||
<?php foreach ($tpPeople as $tpP): ?>
|
$ppPeople = $tpPeople;
|
||||||
<?php $tpPid = (int)($tpP['id'] ?? 0); ?>
|
$ppSelected = [(int)($tpT['assignee_id'] ?? 0)];
|
||||||
<option value="<?= $tpPid ?>"
|
$ppMode = 'single';
|
||||||
<?= $tpPid === (int)($tpT['assignee_id'] ?? 0) ? ' selected' : '' ?>>
|
$ppOpenBtn = '更换';
|
||||||
<?= e(person_name($tpP)) ?>
|
$ppTitle = '谁负责这件事';
|
||||||
</option>
|
$ppEmptyText = '不指定';
|
||||||
<?php endforeach; ?>
|
$ppAllowEmpty = true;
|
||||||
</select>
|
require __DIR__ . '/../_person_picker.php';
|
||||||
|
?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span><?= !empty($tpT['assignee_name'])
|
<span><?= !empty($tpT['assignee_name'])
|
||||||
? e(person_name(['name' => $tpT['assignee_name'],
|
? e(person_name(['name' => $tpT['assignee_name'],
|
||||||
@@ -191,6 +222,41 @@ $tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpD
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php /* 协作人 / 共享给(#78)。
|
||||||
|
🔴 用户:「我记得任务其实可以共享给别人,也就是协作人。
|
||||||
|
但现在怎么感觉编辑这里面没有这个选项呢?」
|
||||||
|
|
||||||
|
⇒ 数据层早就支持(`task_shares` 表 + Task::share/unshare +
|
||||||
|
`_add_modal.php` 的新建弹框都有),只有**编辑表单和面板漏了入口**。
|
||||||
|
这里补上:多选弹窗 + 当前协作人 chips。
|
||||||
|
⚠️ 候选里去掉自己 —— 「共享给自己」是空操作(与新建弹框同一口径)。 */ ?>
|
||||||
|
<div class="tk-attr-row tk-attr-row-top">
|
||||||
|
<span class="tk-attr-lb">协作人</span>
|
||||||
|
<?php if ($tpCan): ?>
|
||||||
|
<?php
|
||||||
|
$ppName = 'shares[]';
|
||||||
|
$ppPeople = array_values(array_filter($tpPeople,
|
||||||
|
fn($pp) => (int)($pp['id'] ?? 0) !== $tpMe));
|
||||||
|
$ppSelected = array_values(array_filter($tpShareIds,
|
||||||
|
fn($sid) => $sid !== $tpMe));
|
||||||
|
$ppMode = 'multi';
|
||||||
|
$ppOpenBtn = '+ 添加人';
|
||||||
|
$ppTitle = '共享给谁';
|
||||||
|
require __DIR__ . '/../_person_picker.php';
|
||||||
|
?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php if (!$tpShares): ?>
|
||||||
|
<span>无</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="tk-share-chips">
|
||||||
|
<?php foreach ($tpShares as $tpS): ?>
|
||||||
|
<span class="tk-share-chip"><?= e(person_name($tpS)) ?></span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tk-attr-row">
|
<div class="tk-attr-row">
|
||||||
<span class="tk-attr-lb">分类</span>
|
<span class="tk-attr-lb">分类</span>
|
||||||
<?php if ($tpCan): ?>
|
<?php if ($tpCan): ?>
|
||||||
@@ -209,24 +275,31 @@ $tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpD
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tk-attr-row tk-attr-row-top">
|
||||||
|
<span class="tk-attr-lb">备注</span>
|
||||||
|
<?php if ($tpCan): ?>
|
||||||
|
<textarea class="input" id="tkp-note" name="note" rows="2"
|
||||||
|
placeholder="补充说明…"><?= e((string)($tpT['note'] ?? '')) ?></textarea>
|
||||||
|
<?php else: ?>
|
||||||
|
<span><?= trim((string)($tpT['note'] ?? '')) !== ''
|
||||||
|
? nl2br(e((string)$tpT['note'])) : '无' ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php if ($tpCan): ?>
|
<?php if ($tpCan): ?>
|
||||||
<div class="tk-attr-actions">
|
<div class="tk-attr-actions">
|
||||||
<button type="button" class="btn btn-sm" data-tk-act="save-attrs">保存</button>
|
<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>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php /* ---- 其余只读事实(循环 / 备注) ---- */ ?>
|
<?php /* ---- 其余只读事实(循环)——备注已挪进「更多信息」(#78) ---- */ ?>
|
||||||
<dl class="kv tk-kv">
|
<?php if (!empty($tpT['repeat_rule'])): ?>
|
||||||
<?php if (!empty($tpT['repeat_rule'])): ?>
|
<dl class="kv tk-kv">
|
||||||
<div class="kv-row"><dt>循环</dt><dd><?= e(\App\Task::repeatWord((string)$tpT['repeat_rule'])) ?></dd></div>
|
<div class="kv-row"><dt>循环</dt><dd><?= e(\App\Task::repeatWord((string)$tpT['repeat_rule'])) ?></dd></div>
|
||||||
<?php endif; ?>
|
</dl>
|
||||||
<?php if (trim((string)($tpT['note'] ?? '')) !== ''): ?>
|
<?php endif; ?>
|
||||||
<div class="kv-row"><dt>备注</dt><dd><?= nl2br(e((string)$tpT['note'])) ?></dd></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<?php /* ---- 子任务(子任务自己不再挂子任务,与详情页同一口径) ---- */ ?>
|
<?php /* ---- 子任务(子任务自己不再挂子任务,与详情页同一口径) ---- */ ?>
|
||||||
<?php if (!$tpIsChild): ?>
|
<?php if (!$tpIsChild): ?>
|
||||||
@@ -341,12 +414,34 @@ $tpLate = $tpDue !== '' && $tpStatus !== 'done' && \App\Task::isOverdue($tpD
|
|||||||
<p class="empty-hint">还没有留言。写 @名字 可以点名。</p>
|
<p class="empty-hint">还没有留言。写 @名字 可以点名。</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($tpCan): ?>
|
<?php if ($tpCan): ?>
|
||||||
<form method="post" action="<?= e(url('tasks/comment_add')) ?>" class="cmt-form">
|
<form method="post" action="<?= e(url('tasks/comment_add')) ?>" class="cmt-form"
|
||||||
|
enctype="multipart/form-data">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="id" value="<?= $tpId ?>">
|
<input type="hidden" name="id" value="<?= $tpId ?>">
|
||||||
<input type="hidden" name="ajax_panel" value="1">
|
<input type="hidden" name="ajax_panel" value="1">
|
||||||
<textarea class="input" name="content" rows="2" required
|
<textarea class="input" name="content" rows="2"
|
||||||
placeholder="说点什么… 写 @名字 可以点名"></textarea>
|
placeholder="说点什么… 写 @名字 可以点名"></textarea>
|
||||||
|
<?php /* 🔴 #78(用户 2026-11-05):「评论那个地方也需要可以添加附件。」
|
||||||
|
|
||||||
|
服务端**早就支持**了 —— `tasks/comment_add` 里一直有
|
||||||
|
`attach_save_files($familyId, 'task_comment', $cid, 'attach', $me);`
|
||||||
|
(index.php 那一行),只是这个表单里**没有放 file input**。
|
||||||
|
⇒ 补上。字段名 `attach[]` 与详情页、任务附件同一套。
|
||||||
|
|
||||||
|
⚠️ `<form>` 必须带 `enctype="multipart/form-data"`,
|
||||||
|
否则浏览器只提交文件名、文件本体丢失(那种"上传成功但打开是空的"最迷惑)。
|
||||||
|
⚠️ 纯附件不写字也允许(把 `required` 从 textarea 上去掉了)——
|
||||||
|
"只发一张图"是常见用法;服务端在 content 为空时会用一句
|
||||||
|
占位文案,见 comment_add 的处理。
|
||||||
|
⚠️ 用原生 `<input type=file>` 而不是拖拽框:评论是一次性提交,
|
||||||
|
拖拽框那套(`data-tk-drop-box`)是"点了立刻上传",两者语义不同。 */ ?>
|
||||||
|
<div class="cmt-attach-row">
|
||||||
|
<label class="cmt-attach-pick">
|
||||||
|
<input type="file" name="attach[]" multiple class="sr-only">
|
||||||
|
<span aria-hidden="true">📎</span> 添加附件
|
||||||
|
</label>
|
||||||
|
<span class="cmt-attach-name" data-cmt-file-name></span>
|
||||||
|
</div>
|
||||||
<div class="btn-row"><button class="btn btn-sm" type="submit">发表</button></div>
|
<div class="btn-row"><button class="btn btn-sm" type="submit">发表</button></div>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user