Files
FamilyBook/templates/trips/form.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

182 lines
9.0 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
/**
* 记一次活动 / 编辑活动(v24 第 15 批重排)。
*
* ⚠️ 用户原话(2026-10-05 第 15 批):
* 「旅程模块,按钮是记一次活动,而不是记一趟旅程。旅程是活动里面的概念。」
* 「活动不需要开始和结束时间,是根据活动中的旅程…计算出来的。目的地也不需要…
* 去的城市,也是根据旅程,得到的。」
* 「添加活动的页面,顺序是这样的:添加标题,上传并裁剪题图,然后一段简介,就可以了。」
*
* 所以表单**主区就三样**(顺序就是用户说的顺序):
* ① 标题 ② 题图(上传 + 16:9 裁剪)
* ③ 简介
* 时间 / 城市**不在表单里** —— 它们是"旅程算出来的",不是"这里填进去的"。
* 页面上只做一句说明,让人知道这两个东西从哪来(不然会去找"怎么没地方填")。
*
* 「备注 / 公开 / 同行的人」功能没丢,收进「更多设置」折叠区:
* 用户说的是"就可以了"(主流程够短),不是"把这些删掉"。
* ⚠️ 但它们在同一个 <form> 里、只是视觉上折叠 —— 少了哪个字段都会静默丢数据。
*
* ⚠️ 同行的人:活动上登记的这份名单**仍然有用**(参与人 = 活动登记 ∪ 各段旅程成员)。
* 但既然活动现在"没有时间也没有地点",多数人会直接在每段旅程里填成员;
* 所以它退到折叠区,默认不占地方。
*/
$isEdit = !empty($t);
$canWrite = \App\Auth::canWrite();
$selIds = [];
if ($isEdit) {
foreach ((array)($t['attendees'] ?? []) as $a) {
$selIds[] = (int)$a['id'];
}
} else {
$selIds = array_map('intval', (array)($presetAttendees ?? []));
}
$cvSlot = \App\ImageSlot::get('cover') ?: ['ratio' => '16:9', 'w' => 1600, 'h' => 900,
'label' => '题图', 'hint' => ''];
$hasCover = $isEdit && !empty($t['cover_key']);
// 折叠区里"有没有填过东西" —— 有就默认展开,别让改过的人还要再点一次
$moreOpen = $isEdit && (trim((string)($t['note'] ?? '')) !== ''
|| (string)($t['is_public'] ?? '1') === '0'
|| $selIds);
?>
<div class="page-head">
<h1><?= $isEdit ? '编辑活动' : '记一次活动' ?></h1>
<span class="sub">活动是容器;里面一段一段的旅程才是日程</span>
<div class="actions">
<a class="btn btn-ghost"
href="<?= e($isEdit ? url('trips/show', ['id' => (int)$t['id']]) : url('trips')) ?>">返回</a>
</div>
</div>
<div class="card card-pad">
<form method="post" enctype="multipart/form-data"
action="<?= e(url($isEdit ? 'trips/update' : 'trips/create')) ?>">
<?= csrf_field() ?>
<?php if ($isEdit): ?>
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
<?php endif; ?>
<?php /* ==================== ① 标题 ==================== */ ?>
<div class="field full">
<label for="t-title">这次活动叫什么 *</label>
<input class="input" id="t-title" name="title" required autofocus
value="<?= e(old('title', $t['title'] ?? '')) ?>" placeholder="如:青岛之行 · 2026 暑假">
<?php if (!empty($errors['title'])): ?><span class="err"><?= e($errors['title']) ?></span><?php endif; ?>
</div>
<?php /* ==================== ② 题图(上传 + 裁剪)====================
⚠️ 用声明式绑定:`data-pick-input="#t-cover"` 让 app.js 的通用逻辑接管
"点图 = 打开文件选择框"。上一批这里**没有**这段绑定(JS 只写在
people/form.php 里),于是点图毫无反应 —— 用户当轮就报了这个问题。 */ ?>
<div class="field full">
<label>题图</label>
<div class="cover-picker">
<img id="cover-preview"
src="<?= e($hasCover ? media_url((string)$t['cover_key']) : '') ?>"
alt="" title="点击选择题图"
data-pick-input="#t-cover"
data-has-cover="<?= $hasCover ? '1' : '0' ?>">
<input class="sr-only" id="t-cover" name="cover" type="file" accept="image/*"
data-crop-ratio="<?= e((string)$cvSlot['ratio']) ?>"
data-crop-w="<?= (int)$cvSlot['w'] ?>"
data-crop-h="<?= (int)$cvSlot['h'] ?>"
data-crop-label="<?= e((string)$cvSlot['label']) ?>"
data-crop-hint="<?= e((string)$cvSlot['hint']) ?>"
data-crop-preview="#cover-preview">
<?php /* 用户挑的裁剪框(归一化 0..1);没裁就留空 = 服务端按 16:9 居中裁 */ ?>
<input type="hidden" name="cover_crop" value="">
<span class="avatar-hint small muted">
点左边那块选题图,选完弹裁剪框。<br>
题图是 16:9 的宽幅横图,多大都能传,保存时自动缩成
<?= (int)$cvSlot['w'] ?>×<?= (int)$cvSlot['h'] ?>。
<?php if ($hasCover): ?>不换图就不用管这里。<?php endif; ?>
</span>
</div>
</div>
<?php /* ==================== ③ 简介 ==================== */ ?>
<div class="field full">
<label for="t-sum">一段简介</label>
<textarea class="input" id="t-sum" name="summary" rows="3"
placeholder="这趟玩得怎么样、为什么想记下来"><?= e(old('summary', $t['summary'] ?? '')) ?></textarea>
</div>
<?php if (!$isEdit): ?>
<?php /* 时间与城市**不是输入项** —— 说清楚它们从哪来,
否则用户会满页找"怎么没地方填时间"。 */ ?>
<p class="form-note">
时间与去过的城市不用填:加进一段段旅程之后,会自动取<b>最早的一段</b>、
<b>最晚的一段</b>和<b>各段去的地方</b>算出来。
</p>
<?php else: ?>
<?php
/* 编辑时把"已经算出来什么"直接摆出来:
不显示的话,改完标题保存会以为"我把时间弄丢了"。 */
$tSpan = \App\Trip::activitySpan($t['starts_at'] ?? null, $t['ends_at'] ?? null);
$tCity = (string)($t['city'] ?? '');
?>
<p class="form-note">
当前时间:<b><?= e($tSpan) ?></b>
<?php if ($tCity !== ''): ?>· 去过:<b><?= e($tCity) ?></b><?php endif; ?>
<br>这两样都是从下面各段旅程算出来的,改这里没用;要改就去改对应的那段旅程。
</p>
<?php endif; ?>
<?php /* ==================== 更多设置(折叠)====================
功能一个都没删,只是主流程只留用户点名的那三项。 */ ?>
<details class="more-set"<?= $moreOpen ? ' open' : '' ?>>
<summary>更多设置(备注 / 公开 / 同行的人)</summary>
<div class="form-grid">
<div class="field full">
<label for="t-note">备注</label>
<textarea class="input" id="t-note" name="note" rows="3"
placeholder="选填。不显示在列表里,只在这趟活动的页面上"><?= e(old('note', $t['note'] ?? '')) ?></textarea>
</div>
<?php /* 用户 2026-10-03 第 4 条:不公开的活动只留着自己看。
⚠️ 先放 hidden=0:不勾选时提交的是"明确的不公开",
而不是"这个字段没出现"(后者会被当成"没传"而默认公开)。 */ ?>
<?php $isPub = old('is_public', (string)($t['is_public'] ?? 1)) !== '0'; ?>
<div class="field full">
<label>公开</label>
<label class="chk-inline">
<input type="hidden" name="is_public" value="0">
<input type="checkbox" name="is_public" value="1" <?= $isPub ? 'checked' : '' ?>>
<span>在活动列表与日历里显示这次活动</span>
</label>
<span class="tip">不勾 = 只留着自己看:不进列表、不上日历,但有链接仍能打开。</span>
</div>
<div class="field full">
<label>同行的人</label>
<?php if (!$people): ?>
<p class="empty-hint">名册里还没有人。</p>
<?php else: ?>
<span class="tip">
这里登记的是"整趟活动都在的人"。只在某一段旅程里的人,
到那段旅程里勾就行 —— 活动页上的名单是两边合起来算的。
</span>
<?php
$ppName = 'attendees[]';
$ppPeople = $people;
$ppSelected = $selIds;
$ppMode = 'multi';
$ppOpenBtn = '+ 添加同行人';
$ppTitle = '选择同行的人';
$ppTip = '';
require __DIR__ . '/../_person_picker.php';
?>
<?php endif; ?>
</div>
</div>
</details>
<div class="btn-row">
<button class="btn" type="submit"><?= $isEdit ? '保存' : '记下来' ?></button>
<a class="btn btn-ghost"
href="<?= e($isEdit ? url('trips/show', ['id' => (int)$t['id']]) : url('trips')) ?>">取消</a>
</div>
</form>
</div>