Files
FamilyBook/templates/trips/show.php
T
laoyang 7138314dc7 v24 第 15 批:活动 / 旅程两级语义(术语 · 派生时间城市 · 表单三项 · 题图可点)
用户原话四条,逐条落地:

1. 「旅程模块,按钮是记一次活动,而不是记一趟旅程。旅程是活动里面的概念。」
   → 顶层一律改称**活动**:按钮「记一次活动」、页标题「活动」、`N 次` / `N 段旅程`;
     活动详情三组改叫「计划的旅程 / 走过的旅程 / 没成行的」;
     节点表单「这段旅程叫什么」「阶段」;flash 文案 17 处同步。
     (导航条仍叫「旅程」——用户就是这么称呼这个模块,列表页加副标题说清两级关系。)

2. 「活动不需要开始和结束时间,是根据活动中的旅程,最早的旅程开始时间,和最晚的
     旅程结束时间,计算出来的。目的地也不需要…去的城市,也是根据旅程,得到的。」
   → 活动**不再存**这三样,全部**读时算出来**:
     · Trip::derived()      最早开始 / 最晚结束 / 城市去重
     · Trip::cities()       按最早出现时间排(先后顺序本身是信息,不是字母序)
     · Trip::attachDerived() 列表页批量补(避免 N+1)
     · Trip::activitySpan()  没有旅程时说「还没有旅程」,不用「日期待补」
     create/update 不再写 events 的起止与地点(老值保留不动,页面不读)。
     列表排序/搜索、首页「最近去了」改走子查询。

3. 「题图那里,没有办法上传图片,点击哪里都没反应。」——**真 bug**
   「点图 = 开文件框」那句 JS 只写在 people/form.php 的内页脚本里(头像专用);
   第 14 批加的旅程题图与图书封面照抄了同一套 DOM 却没有对应的 JS,
   于是图块有 cursor:pointer 与 hover 高亮(看着能点),点下去毫无反应。
   → 做成声明式通用绑定:图上写 data-pick-input="#fileInputId",
     app.js 统一接管(一个页面多个也各绑各的),并删掉旧的那份专用实现。
     三处(头像 / 题图 / 图书封面)全挂上。

4. 「添加活动的页面,顺序是这样的:添加标题,上传并裁剪题图,然后一段简介,就可以了。」
   → 主区就这三项,顺序照原话;时间/城市不做输入项(只留一句说明它们从哪来,
     不说清用户会满页找"怎么没地方填时间")。
     备注 / 公开 / 同行的人收进「更多设置」折叠区——功能一个没丢。

顺带修掉两处会炸的实现问题:
· ORDER BY 引用聚合别名(first_at)在 MariaDB/MySQL 报 1247,且只在运行时炸、
  php -l 看不出 → 改成 ORDER BY MIN(starts_at),否则整个旅程模块 Fatal。
· e2e 里"详情页显示了派生时间"原本写成整页查找,下面每段旅程自己也渲染日期,
  于是把派生值塞成 null 也照样绿(借光假绿)→ 判据收进页头那一段。

验证:e2e 845/0(+15 条本批断言)· 升级 134/0 · 桌面几何 428/0 · 移动几何 688/0 ·
交互 92/0 · 题图真机点击 4/0 · 读书窄屏 15/0 · 家谱 34/0 + 10/0 + 反向 3/3 ·
观影按钮 60/0 + 反向 3/3 · 本批反向验证 4/4(题图绑定抽掉→红、派生值不挂→红、
城市退回读 location→红、时间字段加回表单→红)· 部署静态断言新增两组(含"活动不许
再写起止时间/目的地"与"ORDER BY 不许引用聚合别名"的反退化防线)。
部署后服务器自检 19/19,五个关键文件 md5 与线上逐一核对一致。
2026-10-05 10:58:17 +08:00

318 lines
16 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 建立,v24 第 15 批整理术语与派生值)。
*
* 页面结构:封面 + 简介 + 同行的人 + 照片 + 下面的若干段**旅程**。
*
* ⚠️ 术语(用户 2026-10-05 第 15 批 第 1 条):这里是**一次活动**,
* 里头一段一段的才是**旅程**。所以「+ 加一段计划行程」这类按钮
* 统一改成「+ 加一段旅程」(阶段用「计划 / 实际」修饰)。
*
* ⚠️ 时间与城市都来自 $t 上挂的**派生值**(Trip::derived(),用户第 2 条)——
* 页面不再读 events.starts_at / location。
*/
$canWrite = \App\Auth::canWrite();
// v24 第 13 批:不再按 category 双轨分,按**状态机 stage** 分三组展示。
// 计划行程 / 实际行程 / 未成行(未成行的也要显示,用户明确要求"只不过要标记上")。
$bysStage = ['plan' => [], 'actual' => [], 'skipped' => []];
foreach ((array)($nodes ?? []) as $n) {
$sg = (string)($n['stage'] ?? (($n['category'] ?? 'plan') === 'reality' ? 'actual' : 'plan'));
if (!isset($bysStage[$sg])) { $sg = 'plan'; }
$bysStage[$sg][] = $n;
}
// 每条行程带上它的修正历史(模板里画"原计划 → 实际"的对比)
foreach ($bysStage as $sg => &$lst) {
foreach ($lst as &$nd) {
$nd['revisions'] = \App\Trip::revisions((int)$nd['id']);
}
unset($nd);
}
unset($lst);
$memberIds = array_map('intval', array_column((array)($t['attendees'] ?? []), 'id'));
?>
<div class="page-head">
<h1><?= e((string)$t['title']) ?></h1>
<?php /* 时间/城市都是派生的:一段旅程都没加时显示「还没有旅程」,不是「日期待补」 */ ?>
<span class="sub"><?= e(\App\Trip::activitySpan($t['starts_at'] ?? null, $t['ends_at'] ?? null)) ?></span>
<div class="actions">
<?php if ($canWrite): ?>
<a class="btn btn-ghost" href="<?= e(url('trips/edit', ['id' => (int)$t['id']])) ?>">编辑</a>
<form method="post" action="<?= e(url('trips/destroy')) ?>"
onsubmit="return confirm('确定删除「<?= e((string)$t['title']) ?>」吗?里面的旅程、照片会一起删掉,不可恢复。')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
<button class="btn btn-danger" type="submit">删除</button>
</form>
<?php endif; ?>
<a class="btn btn-ghost" href="<?= e(url('trips')) ?>">返回活动</a>
</div>
</div>
<div class="card card-pad trip-hero">
<div class="trip-hero-cover">
<?php if (!empty($t['cover_key'])): ?>
<img src="<?= e(media_url((string)$t['cover_key'])) ?>" alt="">
<?php else: ?>
<span class="trip-cover-empty">🧳</span>
<?php endif; ?>
</div>
<div class="trip-hero-body">
<?php /* 地名可能是好几个城市 → 用派生出来的列表逐颗画(用户第 2 条) */ ?>
<?php if (!empty($t['cities'])): ?>
<p class="trip-loc">
<?php foreach ((array)$t['cities'] as $i => $city): ?>
<?= $i > 0 ? '<span class="city-sep">·</span>' : '📍' ?>
<span class="city-chip"><?= e((string)$city) ?></span>
<?php endforeach; ?>
</p>
<?php endif; ?>
<?php if (!empty($t['summary'])): ?>
<p class="info-text"><?= nl2br(e((string)$t['summary'])) ?></p>
<?php endif; ?>
<?php if (!empty($t['attendees'])): ?>
<?php /* v24 第 14 批 用户第 4 条:「一个活动的参与人,应该是这个活动下的
所有旅程的参与人的合集」—— 这上面的名单是**每段行程参与人的并集**,
在行程里加了人,这里自动出现,不用再到活动上登记一遍。 */ ?>
<div class="trip-people-block">
<span class="small muted">参与的人(各段行程的合集)</span>
<div class="viewers-list">
<?php foreach ($t['attendees'] as $a): ?>
<?= person_chip($a, url('people/show', ['id' => (int)$a['id']])) ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if (!empty($t['note'])): ?>
<p class="remark-text"><?= nl2br(e((string)$t['note'])) ?></p>
<?php endif; ?>
</div>
</div>
<?php /* ---- 照片 ---- */ ?>
<div class="card card-pad">
<h2 class="cinema-h2">照片</h2>
<?php if (!$photos): ?>
<p class="empty-hint">还没有照片。</p>
<?php else: ?>
<div class="photo-grid">
<?php foreach ($photos as $ph): ?>
<figure class="photo-cell">
<?php /* v24 第 3 条:照片也在**本页**预览(原来 target=_blank 是另开一个标签页
只显示一张图,既没有下载按钮也不好回来看) */ ?>
<a href="<?= e(media_url((string)$ph['key'])) ?>"
data-prev="image"
data-src="<?= e(media_url((string)$ph['key'])) ?>"
data-dl="<?= e(media_url((string)$ph['key'], ['dl' => 1])) ?>"
data-name="<?= e((string)($ph['name'] ?? '照片')) ?>">
<img src="<?= e(media_url((string)$ph['key'])) ?>" alt="<?= e((string)($ph['name'] ?? '')) ?>">
</a>
<?php if ($canWrite): ?>
<form method="post" action="<?= e(url('trips/photo_del')) ?>" class="photo-del">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
<input type="hidden" name="photo_id" value="<?= (int)$ph['id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit"
onclick="return confirm('删掉这张照片?')">删除</button>
</form>
<?php endif; ?>
</figure>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($canWrite): ?>
<form class="inline-form" method="post" enctype="multipart/form-data" action="<?= e(url('trips/photo_add')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
<input type="file" name="photo" accept="image/*" multiple required>
<button class="btn btn-sm" type="submit">上传照片</button>
</form>
<?php endif; ?>
</div>
<?php /* ==================== 这个活动里的旅程(v24 第 13 批重构)====================
⚠️ 术语(用户第 15 批 第 1 条):这一块里每一条都是**一段旅程**,
不是"行程";分三组是按它的**阶段**(计划 / 实际 / 未成行)。
用户 2026-10-05(第 13 批)原话要点:
· 刚开始添加时选「计划 / 实际」
· 计划的三段有三个按钮:删除(**标记未成行,不真删**)、修正(记实际执行,可无限次)、
完成(实际=计划,直接变实际)
· 未成行的仍显示、但带标记;不上日程、不进往年今日
· 实际的会同时上日程 + 往年今日
分三组渲染(未成行放最后,用灰调)。 */ ?>
<?php
$stageMeta = [
'plan' => ['计划的旅程', '还没走。每段下面的「修正 / 完成 / 标记未成行」都能用'],
'actual' => ['走过的旅程', '已走过 —— 会出现在日历与首页「往年今日」'],
'skipped' => ['没成行的', '这次没去成;不再出现在日程与「往年今日」,但在这里保留可查'],
];
foreach (['plan', 'actual', 'skipped'] as $sg):
$list = $bysStage[$sg];
?>
<div class="card card-pad trip-stage-card stage-<?= e($sg) ?>">
<h2 class="cinema-h2">
<?= e($stageMeta[$sg][0]) ?>
<span class="group-n"><?= count($list) ?> 段</span>
</h2>
<p class="stage-hint"><?= e($stageMeta[$sg][1]) ?></p>
<?php if (!$list): ?>
<p class="empty-hint">
<?= $sg === 'plan' ? '还没有排计划。点下面的「+ 加一段旅程」,把打算走的地方排进来。'
: ($sg === 'actual' ? '还没有走过的旅程。' : '没有未成行的。') ?>
</p>
<?php else: ?>
<ol class="node-list">
<?php foreach ($list as $n): ?>
<li class="node-row status-<?= e((string)$n['status']) ?> stage-<?= e($sg) ?>"
id="node-<?= (int)$n['id'] ?>">
<span class="node-ico" title="<?= e(\App\Trip::typeWord((string)$n['node_type'])) ?>">
<?= e(\App\Trip::typeIcon((string)$n['node_type'])) ?>
</span>
<div class="node-body">
<div class="node-head">
<b><?= e((string)$n['title']) ?></b>
<span class="node-badge nb-<?= e($sg) ?>"><?= e(\App\Trip::stageWord($sg)) ?></span>
<?php if ((int)($n['revision_n'] ?? 0) > 0): ?>
<span class="rev-badge" title="已修正过多次,点下方「看修正记录」查看">已修正 <?= (int)$n['revision_n'] ?> 次</span>
<?php endif; ?>
<?php if (!empty($n['place'])): ?>
<span class="node-place">📍 <?= e((string)$n['place']) ?></span>
<?php endif; ?>
</div>
<?php $span = \App\Trip::formatSpan($n['starts_at'] ?? null, $n['ends_at'] ?? null); ?>
<?php if ($span !== '日期待补'): ?>
<div class="node-time">
<?= e($span) ?>
<?php if ($sg === 'actual'): ?><span class="actual-tag">实际</span><?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($n['detail'])): ?>
<p class="node-detail"><?= nl2br(e((string)$n['detail'])) ?></p>
<?php endif; ?>
<?php if (!empty($n['members'])): ?>
<div class="trip-people">
<?php foreach ($n['members'] as $mm): ?><?= person_chip($mm) ?><?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($n['attachs'])): ?>
<?php
$attRows = $n['attachs'];
$attCanEdit = false;
$attDelUrl = '';
require __DIR__ . '/../_attachments.php';
?>
<?php endif; ?>
<?php /* ---- 修正记录:原计划 → 每次修正的对比 ---- */ ?>
<?php if (!empty($n['revisions'])): ?>
<details class="rev-hist">
<summary class="link-quiet">看修正记录(<?= count($n['revisions']) ?> 条)</summary>
<table class="rev-table">
<thead>
<tr><th>#</th><th>时间</th><th>地点</th><th>说明</th></tr>
</thead>
<tbody>
<?php foreach ($n['revisions'] as $rv): ?>
<tr>
<td><?= (int)$rv['seq'] === 1 ? '原计划' : '第 ' . ((int)$rv['seq'] - 1) . ' 次修正' ?></td>
<td class="small"><?= e(\App\Trip::formatSpan($rv['starts_at'] ?? null, $rv['ends_at'] ?? null)) ?></td>
<td><?= e((string)($rv['place'] ?? '')) ?></td>
<td class="small"><?= e((string)($rv['detail'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</details>
<?php endif; ?>
</div>
<?php if ($canWrite): ?>
<div class="node-ops">
<?php /* ---- 计划行程:三个动作(用户点名的三个按钮)---- */ ?>
<?php if ($sg === 'plan'): ?>
<details class="rev-open">
<summary class="btn btn-ghost btn-sm">修正</summary>
<div class="rev-panel">
<p class="rev-tip">
记录实际执行情况。这仍与上面那条计划是<b>同一条记录</b>,
原来的计划数据会保留在「修正记录」里,方便日后对比。
点完这里<b>这段旅程仍是计划</b>;确认无误后再点「标记为实际」。
</p>
<?php
$rvNode = $n; $rvTripId = (int)$t['id'];
$rvPeople = $people ?? [];
$rvMembers = array_map('intval', array_column((array)($n['members'] ?? []), 'id'));
require __DIR__ . '/_revise_form.php';
?>
</div>
</details>
<form method="post" action="<?= e(url('trips/node_done')) ?>"
onsubmit="return confirm('实际执行与计划完全相同,直接转为实际?')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
<button class="btn btn-sm" type="submit">完成</button>
</form>
<form method="post" action="<?= e(url('trips/node_skip')) ?>"
onsubmit="return confirm('标记为「未成行」?它不会再出现在日程与往年今日,但在这个活动里仍然看得到。')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit">标记未成行</button>
</form>
<?php if ((int)($n['revision_n'] ?? 0) > 0): ?>
<form method="post" action="<?= e(url('trips/node_actual')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
<button class="btn btn-sm btn-primary" type="submit">标记为实际</button>
</form>
<?php endif; ?>
<?php elseif ($sg === 'skipped'): ?>
<form method="post" action="<?= e(url('trips/node_restore')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit">恢复为计划</button>
</form>
<?php endif; ?>
<details class="node-edit">
<summary class="link-quiet">编辑</summary>
<?php
$nfNode = $n; $nfTripId = (int)$t['id']; $nfCategory = $sg;
$nfPeople = $people ?? [];
$nfMembers = array_map('intval', array_column((array)($n['members'] ?? []), 'id'));
require __DIR__ . '/_node_form.php';
?>
</details>
<form method="post" action="<?= e(url('trips/node_del')) ?>"
onsubmit="return confirm('彻底删除这一段旅程?连同它的修正记录一起消失,不可恢复。\n\n如果你只是想表达「这次没去成」,请用「标记未成行」。')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit">彻底删除</button>
</form>
</div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
<?php endif; ?>
<?php if ($canWrite && $sg !== 'skipped'): ?>
<details class="node-add">
<?php /* 术语:加的是"一段旅程",阶段只是它的属性 */ ?>
<summary class="link-quiet">+ 加一段旅程(<?= $sg === 'actual' ? '实际' : '计划' ?>)</summary>
<?php
$nfNode = null; $nfTripId = (int)$t['id']; $nfCategory = $sg;
$nfPeople = $people ?? []; $nfMembers = $memberIds;
require __DIR__ . '/_node_form.php';
?>
</details>
<?php endif; ?>
</div>
<?php endforeach; ?>