Files
FamilyBook/templates/tasks/show.php
T
laoyang 8c651b43eb v43 第 43 批:六条反馈(界面 5 + 条目详情浮层)
用户 2026-10-11 反馈六条,逐条定位真根因后修复:

① 纪念日「日期」多出一个控件 —— 只保留最新的那个
   两层根因:(a)原生 `input[type=date]` 当初没包在 `.dt-native` 里,
   v42 那条"AntD 挂上后收起原生框"的选择器根本不命中;(b)更要命的是
   `.dt-native` 基类写着 `display: contents` —— 该元素**没有盒子**,
   给它 position/width 完全无效 ⇒ 必须把 display 显式改回 block 才收得起来。

② 编辑纪念日把底层列表页弄没 —— 「修改」原来是整页跳 `days/edit`
   (套 layout = 一张只有弹框的空白页)。改为 `?embed=1` 只吐弹框那一块,
   列表页 JS 取回来替换 `#anNewCard` 并打开 —— 与「新建」同一观感。
   内嵌形态标 `$anModalOnly` 以免打 standalone 标记(关闭时不再跳页)。

③ 拖图裁剪的层被弹框盖住 —— `.crop-overlay` 是 z-index:100 而 `.cal-modal`
   是 200 ⇒ 裁剪层被整块盖住。提到 260(与 v41 的 .cn-modal 同一处理)。

④ 行程弹框里"参与的人"弹不出选人框 —— person_picker.js 只绑定一次,
   而弹框表单是 AJAX 片段 ⇒ 里面的 `.pp` 从未被 init。三件事一起:
   暴露 `window.fbInitPickers()`(幂等)+ layout 暴露 `window.fbLoadPicker()`
   (脚本是按需加载的)+ 塞完片段后补调初始化。

⑤ 日程新建排版/控件/文案 —— 改走观影式 `.mv-grid`(标签对齐 + 冒号);
   起止补上 `$dtPlugin='antd'`(试点正式推广到日程);标签「时间范围」→「开始 / 结束」;
   删三句提示(结束留空 / 多选提醒 / 全天)。

⑥ 条目详情浮层(首页日程 / 首页近期任务 / 日程页条目)——
   点条目不再直接跳页,弹浮层显示详情;「详细信息」跳该来源详情页;
   「编辑」就地换成该来源的编辑表单(四处 `?embed=1` 片段)。
   新增 `templates/_entry_popup.php` + `assets/js/entry_popup.js` +
   `fb_entry_json()`(三个入口共用一个编码函数)。

验证:php -l 157 文件全过;div 深度 33 组合全配平;片段体检 20 片段无越界;
本地 e2e 1510/0;v43 真机 33/0;v43 反向验证 8/0;v38 36/0;v42 33/0;
b31/b32 反向验证 8/0 与 5/0;部署断言本地空跑 RC=0;完整流水线 PUSH_RC=0。
2026-10-12 00:24:20 +08:00

333 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 任务详情(v12):正文 + 子任务 + 评论(@人)+ 附件 + 共享。
*
* @人:评论里写「@老杨」会高亮(Task::commentHtml),**不会**真的发通知 ——
* 家庭场景下"点名"主要是让人一眼看到在说谁,做通知要先解决推送渠道,先不做。
*
* @param array $t 任务(带 children / comments / attachs / shares)
* @param array $people 名册(负责人 / 共享 / @人 提示)
* @param array $parents 可选的父任务候选(把当前任务挂到别的父任务下)
*/
$canWrite = \App\Auth::canWrite() && \App\Task::canWrite($t, (int)\App\Auth::personId());
$meId = (int)\App\Auth::personId();
$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)));
$isChild = !empty($t['parent_id']);
?>
<div class="page-head">
<h1>
<?php /* 🔴 v24 第 26 批 用户第 5 条:**详情页页头不再放复选框**。
用户原话:「任务详情页面,就不需要复选框了,因为有标记完成这个按钮。」
⇒ 右侧 actions 里那颗「标记完成 / 重新打开」已经把这个动作表达得很清楚,
标题前再挂一颗是重复的(同一页两个入口做同一件事,还容易看不出区别)。
⚠️ **子任务列表里的复选框保留**(见文件下面)—— 那条需求说的是"详情页",
而子任务**没有**自己的「标记完成」按钮,去掉它子任务就没法在列表里勾了。 */ ?>
<?= e((string)$t['title']) ?>
</h1>
<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">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
<?php if ($t['status'] !== 'done'): ?>
<input type="hidden" name="status" value="done">
<button class="btn" type="submit">标记完成</button>
<?php else: ?>
<input type="hidden" name="status" value="doing">
<button class="btn btn-ghost" type="submit">重新打开</button>
<?php endif; ?>
</form>
<?php endif; ?>
<a class="btn btn-ghost" href="<?= e(url('tasks')) ?>">返回任务</a>
</div>
</div>
<div class="card card-pad">
<dl class="kv">
<?php /* v24 第 14 批 用户第 2 条:状态要**能选**(不只是"标记完成/重新打开"两档)。
这里给完整的三档下拉,与看板卡片上那个徽标走同一个路由。 */ ?>
<div class="kv-row">
<dt>状态</dt>
<dd>
<?php if ($canWrite): ?>
<form method="post" action="<?= e(url('tasks/status')) ?>" class="res-inline st-form">
<?= 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="任务状态">
<?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>
</form>
<?php else: ?>
<span class="kb-st is-<?= e((string)$t['status']) ?> is-static">
<?= e(\App\Task::statusWord((string)$t['status'])) ?>
</span>
<?php endif; ?>
</dd>
</div>
<?php if (($t['due_at'] ?? '') !== ''): ?>
<div class="kv-row"><dt>截止</dt>
<dd class="<?= \App\Task::isOverdue($t['due_at'] ?? null) && $t['status'] !== 'done' ? 'kb-late' : '' ?>">
<?= e((string)$t['due_at']) ?><?= \App\Task::dueWord($t['due_at'] ?? null) !== '' ? '(' . e(\App\Task::dueWord($t['due_at'] ?? null)) . ')' : '' ?>
</dd>
</div>
<?php endif; ?>
<?php if (!empty($t['repeat_rule'])): ?>
<div class="kv-row"><dt>循环</dt><dd><?= e(\App\Task::repeatWord($t['repeat_rule'])) ?></dd></div>
<?php endif; ?>
<?php if (!empty($t['assignee_name'])): ?>
<div class="kv-row"><dt>负责人</dt>
<dd><?= e(person_name(['name' => $t['assignee_name'], 'my_nickname' => $t['assignee_nick'] ?? ''])) ?></dd>
</div>
<?php endif; ?>
<?php if (!empty($t['creator_name'])): ?>
<div class="kv-row"><dt>谁建的</dt>
<dd><?= e(person_name(['name' => $t['creator_name'], 'my_nickname' => $t['creator_nick'] ?? ''])) ?></dd>
</div>
<?php endif; ?>
<?php if ($isChild && !empty($t['parent_title'])): ?>
<div class="kv-row"><dt>属于</dt>
<dd><a href="<?= e(url('tasks/show', ['id' => (int)$t['parent_id']])) ?>"><?= e((string)$t['parent_title']) ?></a></dd>
</div>
<?php endif; ?>
<?php if (!empty($t['note'])): ?>
<div class="kv-row"><dt>备注</dt><dd><?= nl2br(e((string)$t['note'])) ?></dd></div>
<?php endif; ?>
</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 endif; ?>
</div>
<?php /* ---- 子任务 ---- */ ?>
<?php if (!$isChild): ?>
<div class="card card-pad">
<h2 class="cinema-h2">子任务 <span class="group-n"><?= count($t['children']) ?> 项</span></h2>
<?php if (!$t['children']): ?>
<p class="empty-hint">还没有子任务。把一件大事拆成几件小事,进度才看得见。</p>
<?php else: ?>
<ul class="sub-list">
<?php foreach ($t['children'] as $c): ?>
<li class="sub-row<?= $c['status'] === 'done' ? ' is-done' : '' ?>">
<?php /* v24 第 25 批 用户第 6 条:子任务的复选框**改用全站那一颗**
—— 原来是"每行一个 form + ☐/☑ 字符":
· 每行一个 form = 子任务一多就是几十个 form(名册页那个坑);
· ☐/☑ 是普通字符,跨字体基线会歪,且与看板上的复选框长得不一样。
现在卡片上只放 data-tk-done,表单全页一份(见页尾)。 */ ?>
<?php
$tkId = (int)$c['id']; $tkStatus = (string)$c['status'];
$tkBack = 'show'; $tkStatic = !$canWrite;
$tkLabel = '子任务' . (string)$c['title'];
require __DIR__ . '/../_task_check.php';
?>
<a class="sub-title" href="<?= e(url('tasks/show', ['id' => (int)$c['id']])) ?>">
<?= e((string)$c['title']) ?>
</a>
<?php if (($c['due_at'] ?? '') !== ''): ?>
<span class="node-time<?= \App\Task::isOverdue($c['due_at'] ?? null) && $c['status'] !== 'done' ? ' kb-late' : '' ?>">
<?= e(\App\Task::dueWord($c['due_at'] ?? null)) ?>
</span>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if ($canWrite): ?>
<details class="node-add">
<summary class="link-quiet">+ 加一项子任务</summary>
<form method="post" action="<?= e(url('tasks/create')) ?>" class="inline-form">
<?= csrf_field() ?>
<input type="hidden" name="parent_id" value="<?= (int)$t['id'] ?>">
<input class="input" name="title" required placeholder="如:擦玻璃">
<?php $dtName = 'due_at'; $dtValue = null; $dtRequired = false;
require __DIR__ . '/../_datetime.php'; ?>
<button class="btn btn-sm" type="submit">加上</button>
</form>
</details>
<?php endif; ?>
</div>
<?php endif; ?>
<?php /* ---- 附件 ---- */ ?>
<div class="card card-pad">
<h2 class="cinema-h2">附件</h2>
<?php if (!$t['attachs']): ?>
<p class="empty-hint">还没有附件。</p>
<?php else: ?>
<?php
$attRows = $t['attachs'];
$attCanEdit = $canWrite;
$attDelUrl = 'tasks/attach_del';
$attDelField = 'id';
$attExtra = [];
require __DIR__ . '/../_attachments.php';
?>
<?php endif; ?>
<?php if ($canWrite): ?>
<form class="inline-form" method="post" enctype="multipart/form-data" action="<?= e(url('tasks/attach_add')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
<?php /* v24 第 21 批 第 7 条:不再放原生文件框 + 按钮,改成一个「点击或拖动」的框。
⚠️ 这个表单**唯一**的用途就是传附件,所以提交键仍然要有 ——
"点击/拖动"只是选文件的方式,最后还得点一下才能提交。 */
$sdName = 'attach[]'; $sdSlot = 'photo';
require __DIR__ . '/../_stage_drop.php'; ?>
<div class="btn-row" style="margin-top:12px">
<button class="btn btn-sm" type="submit">上传附件</button>
</div>
</form>
<?php endif; ?>
</div>
<?php /* ---- 共享 ---- */ ?>
<div class="card card-pad">
<h2 class="cinema-h2">共享</h2>
<?php if (!$t['shares']): ?>
<p class="empty-hint">还没共享给别人。共享之后,任何一个人完成都算完成。</p>
<?php else: ?>
<div class="res-shares">
<?php foreach ($t['shares'] as $s): ?>
<?= person_chip($s) ?>
<span class="node-badge"><?= ($s['perm'] ?? '') === 'write' ? '可写' : '只读' ?></span>
<?php if ($canWrite): ?>
<form method="post" action="<?= e(url('tasks/unshare')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
<input type="hidden" name="person_id" value="<?= (int)$s['person_id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit">取消</button>
</form>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($canWrite): ?>
<form class="inline-form" method="post" action="<?= e(url('tasks/share')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
<?php
$ppName = 'person_id';
$ppPeople = array_values(array_filter($people, fn($p) => (int)$p['id'] !== $meId));
$ppSelected = [];
$ppMode = 'single';
$ppOpenBtn = '选择要共享给谁';
$ppTitle = '共享给谁';
$ppEmptyText = '未选择';
require __DIR__ . '/../_person_picker.php';
?>
<select class="select" name="perm">
<option value="write">可写</option>
<option value="read">只读</option>
</select>
<button class="btn btn-sm" type="submit">共享</button>
</form>
<?php endif; ?>
</div>
<?php /* ---- 评论 ---- */ ?>
<div class="card card-pad">
<h2 class="cinema-h2">讨论 <span class="group-n"><?= count($t['comments']) ?> 条</span></h2>
<?php if (!$t['comments']): ?>
<p class="empty-hint">还没有留言。写 @名字 可以点名。</p>
<?php else: ?>
<ul class="cmt-list">
<?php foreach ($t['comments'] as $c): ?>
<li class="cmt-row">
<div class="cmt-who">
<?= person_chip($c) ?>
<span class="cmt-time"><?= e((string)$c['created_at']) ?></span>
</div>
<div class="cmt-body"><?= \App\Task::commentHtml((string)$c['content'], $names) ?></div>
<?php if ((int)$c['person_id'] === $meId): ?>
<form method="post" action="<?= e(url('tasks/comment_del')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
<input type="hidden" name="task_id" value="<?= (int)$t['id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit" onclick="return confirm('删掉这条留言?')">删除</button>
</form>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if ($canWrite): ?>
<form method="post" action="<?= e(url('tasks/comment_add')) ?>" class="cmt-form">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
<textarea class="input" name="content" rows="2" required
placeholder="说点什么… 写 @名字 可以点名"><?= e(old('content', '')) ?></textarea>
<div class="btn-row"><button class="btn btn-sm" type="submit">发表</button></div>
</form>
<?php endif; ?>
</div>
<?php if ($canWrite): ?>
<div class="task-danger">
<form method="post" action="<?= e(url('tasks/destroy')) ?>"
onsubmit="return confirm('删除「<?= e((string)$t['title']) ?>」?子任务、附件与留言会一起删掉。')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
<button class="btn btn-danger btn-sm" type="submit">删除这项任务</button>
</form>
</div>
<?php endif; ?>
<?php /* 完成复选框的**全页一份**表单(用户第 6 条)—— 本页可能出现多颗
(任务自己 + 每条子任务),所以更要"卡片上只放数据、表单只此一份"。 */ ?>
<?php if ($canWrite): ?>
<form method="post" action="<?= e(url('tasks/status')) ?>" id="taskDoneForm" class="hidden-form">
<?= csrf_field() ?>
<input type="hidden" name="id" id="doneId">
<input type="hidden" name="status" id="doneStatus">
<input type="hidden" name="back" id="doneBack" value="show">
</form>
<?php /* ⚠️ 本页的复选框走**原生提交**(不 AJAX):
详情页是一次性动作,改完最常见的是"看一眼就返回";
就地改反而要在页面上同步三处(标题旁的状态、kv 里的状态、子任务进度)。
原生提交 + back=show → 服务端 302 回本页,状态永远与服务端一致。
代价只是刷一次页 —— 详情页本来就"打开看某一条",没有滚动位置要保住
(那是看板/首页才有的诉求)。
⚠️ 但**不能只渲染 form 不绑事件**:那样点了按钮毫无反应
("按钮坏了"比"刷一下"糟得多)。这里只做一件最小的事:
把"点的是哪一条 / 改成什么"灌进表单再提交。 */ ?>
<script>
(function () {
var form = document.getElementById('taskDoneForm');
if (!form) { return; }
Array.prototype.forEach.call(document.querySelectorAll('.tk-check[data-tk-done]'), function (b) {
b.addEventListener('click', function (ev) {
ev.preventDefault();
var now = b.getAttribute('data-tk-status') || 'todo';
document.getElementById('doneId').value = b.getAttribute('data-tk-done');
document.getElementById('doneStatus').value = (now === 'done') ? 'todo' : 'done';
document.getElementById('doneBack').value = b.getAttribute('data-tk-back') || 'show';
form.submit();
});
});
})();
</script>
<?php endif; ?>