Files
FamilyBook/templates/tasks/show.php
T
laoyang 71a7ec35a0 v14: 观影/家谱/日程/旅程/资源/任务六模块统一,首页改 dashboard
用户 14 条需求一次交付(1-13 功能 + 14 工作方式)。

观影(1-4)
- 返回文案全模块统一为「返回观影」(不再有「返回列表」「返回观影记录」三种叫法)
- 观影人一律用彩色人物标签 person_chip:底色由头像主色算出来(上传时存库),
  瀑布流卡片也按人显示;五处观影模板全部改用它
- 人员显示统一走 person_name()(shown → 我给他起的称呼 → 姓名)
- 影院管理页与年度报告页的上下两块用 .card + .card 统一给间距(原来紧贴成一整块)

家谱(5)
- 树根从「我」换成**辈分最高的祖先**:库画的是"树根的祖先链+后代+树根的兄弟姐妹",
  根=我 时伯父/堂兄/嫂子/侄子一个都不画(12 人夹具只画出 8 人)
- focusMap 按**代数**圈定三代(不再按步数),normalizeLinks 把子女对称挂到父母双方
  —— 修掉"母亲登录看不到孩子"
- 新增「看全部 / 回到我」两个按钮,放在画布外(画布整块被库接管了手势)

统一事件原语(6-9)
- 旅程:TripLog 全量迁入(3 趟旅程 / 21 段行程 / 72 张照片 / 5 个人)。
  复用 events(kind='trip') + trip_details + trip_nodes(计划/执行双轨保留)
- 日程:每人可建日历本(自定义色)+ 共享只读/可写;观影与旅行是每人默认两本,
  内容是**从各模块实时派生**的(不复制,所以改了观影日期日历立刻跟着变);
  没有具体时分 = 全天;月视图参照群晖 Calendar
- 资源:挂在某人名下的附属信息(标题+正文+多附件),可共享给多人并设只读/读写,
  默认折叠、可搜标题与正文
- 任务:看板三列 + 两级子任务 + 评论@人 + 共享 + 循环(完成时按原截止日推下一期)
- 统一附件表:四个模块的附件共用一张,下载地址不落库、一律由 media.php 现算

模块化(10-11)
- App\Module 是模块的唯一来源(加一行 = 导航条/主题色/个人设置三处自动出现)
- 个人设置里可勾选要显示哪些模块、拖动排序(关掉的模块**不会**被兜底补回来)
- 每个模块一套主题色(家庭级),只覆盖 --brand 三兄弟,不碰纸墨底色

其他(12-13)
- 新增成员表单可选所属分组(编辑表单没有:分组是"我和他的关系",只归我自己)
- 首页改 dashboard:接下来的日子 / 未来七天 / 手头在做 / 往年今日。
  家人卡片从首页撤走(看人去名册)

验证(全部通过)
- e2e 542 条(新增 4 模块端到端 60+ 条)
- 桌面几何 368 / 移动端几何 520 / 家谱 12 / 交互 97 / 老库升级 123
- SQL 列名静态校验 0 处可疑(对着真实表结构)
- 反向验证 18 条新注入全部抓住

顺带修掉的既有问题
- 老库升级回填 SQL 按实际存在的列动态拼(缺列不再 1054 炸断升级)
- 全新安装时 Schema::ensure 跑在"人还没建"之前 → 补建日历本三处齐备
- 窄屏导航横向溢出(模块从 4 个加到 8 个后顶栏撑破视口)
2026-10-02 09:28:07 +08:00

308 lines
14 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><?= 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">
<div class="kv-row"><dt>状态</dt><dd><?= e(\App\Task::statusWord((string)$t['status'])) ?></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>
<form method="post" action="<?= e(url('tasks/update')) ?>" class="node-form">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
<div class="nf-grid">
<div class="field full">
<label>标题</label>
<input class="input" name="title" required value="<?= e((string)$t['title']) ?>">
</div>
<div class="field">
<label>截止日期(留空=不上日历)</label>
<input class="input" type="date" name="due_date" value="<?= e(substr((string)($t['due_at'] ?? ''), 0, 10)) ?>">
</div>
<div class="field">
<label>负责人</label>
<select class="select" name="assignee_id">
<option value="0">不指定</option>
<?php foreach ($people as $p): ?>
<option value="<?= (int)$p['id'] ?>"<?= (int)($t['assignee_id'] ?? 0) === (int)$p['id'] ? ' selected' : '' ?>>
<?= e(person_name($p)) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="field">
<label>循环</label>
<select class="select" name="repeat_rule">
<?php foreach (\App\Task::REPEAT as $rk => $rv): ?>
<option value="<?= e($rk) ?>"<?= (string)($t['repeat_rule'] ?? '') === (string)$rk ? ' selected' : '' ?>>
<?= e($rv) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="field">
<label>挂到哪个父任务下</label>
<select class="select" name="parent_id">
<option value="0">(这是一项独立的任务)</option>
<?php foreach ($parents as $p): ?>
<option value="<?= (int)$p['id'] ?>"<?= (int)($t['parent_id'] ?? 0) === (int)$p['id'] ? ' selected' : '' ?>>
<?= e((string)$p['title']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="field full">
<label>备注</label>
<textarea class="input" name="note" rows="2"><?= e((string)($t['note'] ?? '')) ?></textarea>
</div>
</div>
<div class="btn-row"><button class="btn btn-sm" type="submit">保存</button></div>
</form>
</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 if ($canWrite): ?>
<form method="post" action="<?= e(url('tasks/move')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
<input type="hidden" name="status" value="<?= $c['status'] === 'done' ? 'todo' : 'done' ?>">
<button class="sub-check" type="submit" title="<?= $c['status'] === 'done' ? '取消完成' : '标记完成' ?>">
<?= $c['status'] === 'done' ? '☑' : '☐' ?>
</button>
</form>
<?php else: ?>
<span class="sub-check is-static"><?= $c['status'] === 'done' ? '☑' : '☐' ?></span>
<?php endif; ?>
<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="如:擦玻璃">
<input class="input" type="date" name="due_date">
<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: ?>
<div class="attach-row">
<?php foreach ($t['attachs'] as $a): ?>
<span class="attach-item">
<a href="<?= e(media_url((string)$a['key'])) ?>" target="_blank" rel="noopener">
<?= (int)$a['is_image'] === 1 ? '🖼' : '📄' ?> <?= e((string)($a['name'] ?: '附件')) ?>
</a>
<span class="res-size"><?= e(\App\Attachment::sizeWord($a['size'] ?? 0)) ?></span>
<a class="link-quiet" href="<?= e(media_url((string)$a['key'], ['dl' => 1])) ?>">下载</a>
<?php if ($canWrite): ?>
<form method="post" action="<?= e(url('tasks/attach_del')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$a['id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit" onclick="return confirm('删掉这个附件?')">删除</button>
</form>
<?php endif; ?>
</span>
<?php endforeach; ?>
</div>
<?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'] ?>">
<input type="file" name="attach[]" multiple required>
<button class="btn btn-sm" type="submit">上传附件</button>
</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'] ?>">
<select class="select" name="person_id">
<?php foreach ($people as $p): ?>
<?php if ((int)$p['id'] === $meId) { continue; } ?>
<option value="<?= (int)$p['id'] ?>"><?= e(person_name($p)) ?></option>
<?php endforeach; ?>
</select>
<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; ?>