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 与线上逐一核对一致。
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
<?php
|
||||
/**
|
||||
* 行程节点表单(新增 / 编辑共用;被 show.php 循环 require)。
|
||||
* 一段旅程的表单(新增 / 编辑共用;被 show.php 循环 require)。
|
||||
*
|
||||
* v24 第 13 批:旅程重构后,**行程不再分两条轨**,而是**一条记录 + 状态机**。
|
||||
* ⚠️ 术语(用户第 15 批 第 1 条):这是**一段旅程**;「计划 / 实际」是它的阶段。
|
||||
*
|
||||
* v24 第 13 批:旅程重构后,**不再分两条轨**,而是**一条记录 + 状态机**。
|
||||
* 用户原话:「这个实际的行程和计划行程,是一条记录,不是两条」。
|
||||
* 所以这里加一个「这是哪一类」的下拉:计划行程 / 实际行程
|
||||
* 所以这里有「这是哪一类」的下拉:计划 / 实际
|
||||
* (新建时由用户选;编辑时是当前状态)。
|
||||
* 「修正」「标记为实际」「完成」「标记未成行」是**另外三个按钮**(在 show.php 上),
|
||||
* 不走这张表单 —— 它们的语义是"对既有行程的操作",不是"填一遍字段"。
|
||||
* 「修正」「标记为实际」「完成」「标记未成行」是**另外几个按钮**(在 show.php 上),
|
||||
* 不走这张表单 —— 它们的语义是"对既有旅程的操作",不是"填一遍字段"。
|
||||
*
|
||||
* 变量(require 前设好):
|
||||
* $nfNode 被编辑的节点数组,或 null(新增)
|
||||
* $nfTripId 旅程 id(events.id)
|
||||
* $nfTripId 活动 id(events.id)
|
||||
* $nfCategory 【兼容保留】'plan' | 'reality'(新版只用来定默认值)
|
||||
* $nfPeople 可选人名册(选参与人用),为空就不显示成员多选
|
||||
* $nfMembers 已选成员 id 列表
|
||||
@@ -35,7 +37,7 @@ $nfStage = $nfIsEdit
|
||||
|
||||
<div class="nf-grid">
|
||||
<div class="field">
|
||||
<label for="<?= e($nfPrefix) ?>-title">这一段 *</label>
|
||||
<label for="<?= e($nfPrefix) ?>-title">这段旅程叫什么 *</label>
|
||||
<input class="input" id="<?= e($nfPrefix) ?>-title" name="title" required
|
||||
value="<?= e($nfNode['title'] ?? '') ?>" placeholder="如:栈桥看海">
|
||||
</div>
|
||||
@@ -51,12 +53,12 @@ $nfStage = $nfIsEdit
|
||||
</div>
|
||||
<?php if (!$nfIsEdit): ?>
|
||||
<div class="field">
|
||||
<label for="<?= e($nfPrefix) ?>-stage">这是哪一类 *</label>
|
||||
<label for="<?= e($nfPrefix) ?>-stage">阶段 *</label>
|
||||
<select class="select" id="<?= e($nfPrefix) ?>-stage" name="stage_pick">
|
||||
<option value="plan"<?= $nfStage === 'plan' ? ' selected' : '' ?>>计划行程(还没走)</option>
|
||||
<option value="actual"<?= $nfStage === 'actual' ? ' selected' : '' ?>>实际行程(已经走过)</option>
|
||||
<option value="plan"<?= $nfStage === 'plan' ? ' selected' : '' ?>>计划(还没走)</option>
|
||||
<option value="actual"<?= $nfStage === 'actual' ? ' selected' : '' ?>>实际(已经走过)</option>
|
||||
</select>
|
||||
<span class="tip">计划行程走完后可用「修正 / 完成」转成实际行程。</span>
|
||||
<span class="tip">计划走完后可用「修正 / 完成」把它转成实际。</span>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="field">
|
||||
@@ -93,11 +95,11 @@ $nfStage = $nfIsEdit
|
||||
<div class="field full">
|
||||
<label for="<?= e($nfPrefix) ?>-detail">记录</label>
|
||||
<textarea class="input" id="<?= e($nfPrefix) ?>-detail" name="detail" rows="2"
|
||||
placeholder="这一段发生了什么"><?= e($nfNode['detail'] ?? '') ?></textarea>
|
||||
placeholder="这段旅程发生了什么"><?= e($nfNode['detail'] ?? '') ?></textarea>
|
||||
</div>
|
||||
<?php if (!empty($nfPeople)): ?>
|
||||
<div class="field full">
|
||||
<label>这一段里有谁(不勾 = 全趟同行的人)</label>
|
||||
<label>这段旅程里有谁(不勾 = 整个活动同行的人)</label>
|
||||
<div class="nf-members">
|
||||
<?php foreach ($nfPeople as $p): ?>
|
||||
<label class="check-item">
|
||||
@@ -111,6 +113,6 @@ $nfStage = $nfIsEdit
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-sm" type="submit"><?= $nfIsEdit ? '保存' : '加一段' ?></button>
|
||||
<button class="btn btn-sm" type="submit"><?= $nfIsEdit ? '保存' : '加这段旅程' ?></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* 「修正」表单(v24 第 13 批):记录一条计划行程的**实际**执行情况。
|
||||
* 「修正」表单(v24 第 13 批):记录一段计划旅程的**实际**执行情况。
|
||||
*
|
||||
* 用户原话:「另外一个按钮,用来记录实际的情况,按钮名字就叫修正,但是记住,
|
||||
* 这个实际的行程和计划行程,是一条记录,不是两条,点击这个按钮,
|
||||
@@ -14,8 +14,8 @@
|
||||
* 两者都作用在同一条 trip_nodes 记录上(用户明确"是一条记录,不是两条")。
|
||||
*
|
||||
* 变量(require 前设好):
|
||||
* $rvNode 被修正的行程
|
||||
* $rvTripId 旅程 id
|
||||
* $rvNode 被修正的这段旅程
|
||||
* $rvTripId 活动 id
|
||||
* $rvPeople 人名册(选实际参与人)
|
||||
* $rvMembers 当前参与人 id
|
||||
*/
|
||||
|
||||
+142
-96
@@ -1,13 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* 记一趟旅程 / 编辑旅程(v12)。
|
||||
* 记一次活动 / 编辑活动(v24 第 15 批重排)。
|
||||
*
|
||||
* 字段分两组:
|
||||
* · 骨架(落在 events 上):标题 / 出发 / 回来 / 目的地 / 备注 —— 与观影同一套原语;
|
||||
* · 旅程特有(落在 trip_details 上):封面图 + 一段话小结。
|
||||
* ⚠️ 用户原话(2026-10-05 第 15 批):
|
||||
* 「旅程模块,按钮是记一次活动,而不是记一趟旅程。旅程是活动里面的概念。」
|
||||
* 「活动不需要开始和结束时间,是根据活动中的旅程…计算出来的。目的地也不需要…
|
||||
* 去的城市,也是根据旅程,得到的。」
|
||||
* 「添加活动的页面,顺序是这样的:添加标题,上传并裁剪题图,然后一段简介,就可以了。」
|
||||
*
|
||||
* ⚠️ 同行的人用多选框(人不多,一次性列全比"搜索+联想"快);
|
||||
* 显示名一律是「我叫他什么」(person_name),提交的是 person_id。
|
||||
* 所以表单**主区就三样**(顺序就是用户说的顺序):
|
||||
* ① 标题 ② 题图(上传 + 16:9 裁剪)
|
||||
* ③ 简介
|
||||
* 时间 / 城市**不在表单里** —— 它们是"旅程算出来的",不是"这里填进去的"。
|
||||
* 页面上只做一句说明,让人知道这两个东西从哪来(不然会去找"怎么没地方填")。
|
||||
*
|
||||
* 「备注 / 公开 / 同行的人」功能没丢,收进「更多设置」折叠区:
|
||||
* 用户说的是"就可以了"(主流程够短),不是"把这些删掉"。
|
||||
* ⚠️ 但它们在同一个 <form> 里、只是视觉上折叠 —— 少了哪个字段都会静默丢数据。
|
||||
*
|
||||
* ⚠️ 同行的人:活动上登记的这份名单**仍然有用**(参与人 = 活动登记 ∪ 各段旅程成员)。
|
||||
* 但既然活动现在"没有时间也没有地点",多数人会直接在每段旅程里填成员;
|
||||
* 所以它退到折叠区,默认不占地方。
|
||||
*/
|
||||
$isEdit = !empty($t);
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
@@ -19,12 +32,20 @@ if ($isEdit) {
|
||||
} 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>
|
||||
<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>
|
||||
href="<?= e($isEdit ? url('trips/show', ['id' => (int)$t['id']]) : url('trips')) ?>">返回</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,101 +57,126 @@ if ($isEdit) {
|
||||
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="field full">
|
||||
<label for="t-title">这趟旅程叫什么 *</label>
|
||||
<input class="input" id="t-title" name="title" required
|
||||
value="<?= e(old('title', $t['title'] ?? '')) ?>" placeholder="如:青岛三日 · 暑假">
|
||||
<?php if (!empty($errors['title'])): ?><span class="err"><?= e($errors['title']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
<?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 /* 用户 2026-10-04 第 7 条:出发/回来是一个**时间范围**,
|
||||
用一个范围选择器一次选完(不再是并列两个控件)。 */ ?>
|
||||
<div class="field dt-range">
|
||||
<label for="t-start">出发 / 回来</label>
|
||||
<?php $dtName = 'starts_at'; $dtValue = $t['starts_at'] ?? '';
|
||||
$dtId = 't-start'; $dtSeconds = true; $dtRange = 'start';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
<?php if (!empty($errors['starts_at'])): ?><span class="err"><?= e($errors['starts_at']) ?></span><?php endif; ?>
|
||||
<?php $dtName = 'ends_at'; $dtValue = $t['ends_at'] ?? ''; $dtId = 't-end'; $dtSeconds = true;
|
||||
$dtRange = 'end';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
<?php if (!empty($errors['ends_at'])): ?><span class="err"><?= e($errors['ends_at']) ?></span><?php endif; ?>
|
||||
<span class="tip">回来的时间可不填。</span>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="t-loc">目的地</label>
|
||||
<input class="input" id="t-loc" name="location"
|
||||
value="<?= e(old('location', $t['location'] ?? '')) ?>" placeholder="如:青岛">
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="field full">
|
||||
<label for="t-note">备注</label>
|
||||
<textarea class="input" id="t-note" name="note" rows="3"><?= e(old('note', $t['note'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php /* 用户 2026-10-03 第 4 条:旅程列表只显示公开的(is_public=1)。
|
||||
默认**勾上**(公开)—— 新建一趟行程却默认藏起来是反直觉的;
|
||||
编辑时按库里的值回填,老数据 COALESCE 成 1 也是勾上。
|
||||
⚠️ 提交的是 `is_public=1`,不勾则完全不提交这个键,
|
||||
saveDetails 里 `!empty($d['is_public'])` 自然得到 0。 */ ?>
|
||||
<?php $isPub = old('is_public', (string)($t['is_public'] ?? 1)) !== '0'; ?>
|
||||
<div class="field full">
|
||||
<label>公开</label>
|
||||
<label class="chk-inline">
|
||||
<?php /* ⚠️ 先放一个 hidden=0:不勾选时提交的就是"明确的不公开",
|
||||
而不是"这个字段没出现" —— 后者会被当成"没传"而默认公开,
|
||||
于是"取消勾选"看起来毫无作用(经典 checkbox 陷阱)。 */ ?>
|
||||
<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 /* v24 第 11 条:与全局选人组件统一(原来是平铺的 checkbox 网格,
|
||||
名册一多就变成"在一长串里瞪眼找",也搜不了拼音) */ ?>
|
||||
<?php if (!$people): ?>
|
||||
<p class="empty-hint">名册里还没有人。</p>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
$ppName = 'attendees[]';
|
||||
$ppPeople = $people;
|
||||
$ppSelected = $selIds;
|
||||
$ppMode = 'multi';
|
||||
$ppOpenBtn = '+ 添加同行人';
|
||||
$ppTitle = '选择同行的人';
|
||||
$ppTip = '';
|
||||
require __DIR__ . '/../_person_picker.php';
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="t-cover">封面图</label>
|
||||
<?php if ($isEdit && !empty($t['cover_key'])): ?>
|
||||
<div class="cover-now">
|
||||
<img src="<?= e(media_url((string)$t['cover_key'])) ?>" alt="">
|
||||
<span class="tip">不换图就不用管这里;选一张新图会替换它。</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<input class="input" type="file" id="t-cover" name="cover" accept="image/*">
|
||||
<?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>
|
||||
</div>
|
||||
+23
-13
@@ -1,18 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* 旅程列表(v12,TripLog activities/index 迁来,但按家书的统一事件原语重写)。
|
||||
* 活动列表(v12 建立,v24 第 15 批把术语理清)。
|
||||
*
|
||||
* 与观影列表同一个骨架:页头 + 搜索 + 卡片列表。
|
||||
* ⚠️ 卡片上的人是「我叫他什么」(person_chip),不是姓名(用户第 3 条)。
|
||||
* ⚠️ 术语(用户 2026-10-05 第 15 批 第 1 条原话):
|
||||
* 「旅程模块,按钮是记一次活动,而不是记一趟旅程。旅程是活动里面的概念。」
|
||||
* 所以:这一页是**活动**的列表,一条活动里头才是若干段**旅程**。
|
||||
* 页面上的"趟"一律改成"次"(一次活动);"段行程"改成"段旅程"。
|
||||
*
|
||||
* 时间与城市都是从各段旅程**派生**的(用户第 2 条)——
|
||||
* 列表里读的是 Trip::attachDerived() 挂上来的值,不是活动自己的字段。
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1>旅程</h1>
|
||||
<span class="sub">共 <?= (int)$total ?> 趟</span>
|
||||
<h1>活动</h1>
|
||||
<?php /* 导航条上这个模块仍叫「旅程」(用户就是这么称呼它的),
|
||||
但这一页列的顶层单位是**活动** —— 一句副标题把两级关系说清楚,
|
||||
否则"导航写旅程、页面写活动"会让人以为点错了地方。 */ ?>
|
||||
<span class="sub">共 <?= (int)$total ?> 次 · 每次活动里可以有若干段旅程</span>
|
||||
<div class="actions">
|
||||
<?php if ($canWrite): ?>
|
||||
<a class="btn" href="<?= e(url('trips/new')) ?>">记一趟旅程</a>
|
||||
<a class="btn" href="<?= e(url('trips/new')) ?>">记一次活动</a>
|
||||
<?php else: ?>
|
||||
<span class="link-quiet">只读账号</span>
|
||||
<?php endif; ?>
|
||||
@@ -21,7 +29,7 @@ $canWrite = \App\Auth::canWrite();
|
||||
|
||||
<form class="search" method="get" action="index.php">
|
||||
<input type="hidden" name="r" value="trips">
|
||||
<input class="input" type="text" name="keyword" value="<?= e($keyword) ?>" placeholder="搜标题 / 目的地 / 备注">
|
||||
<input class="input" type="text" name="keyword" value="<?= e($keyword) ?>" placeholder="搜活动名 / 去过的城市 / 备注">
|
||||
<button class="btn" type="submit">搜索</button>
|
||||
<?php if ($keyword !== ''): ?>
|
||||
<a class="search-clear" href="<?= e(url('trips')) ?>">清除</a>
|
||||
@@ -30,9 +38,10 @@ $canWrite = \App\Auth::canWrite();
|
||||
|
||||
<?php if (!$rows): ?>
|
||||
<div class="card card-pad empty">
|
||||
<b><?= $keyword !== '' ? '没有匹配的旅程' : '还没有旅程记录' ?></b>
|
||||
<b><?= $keyword !== '' ? '没有匹配的活动' : '还没有活动记录' ?></b>
|
||||
<?= $keyword !== '' ? '换个关键词试试,或清除搜索看全部。'
|
||||
: ($canWrite ? '点右上角「记一趟旅程」,把一次出行记下来。' : '家人还没有记录旅程。') ?>
|
||||
: ($canWrite ? '点右上角「记一次活动」,把一次出行记下来;进去之后再加一段段旅程。'
|
||||
: '家人还没有记录活动。') ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="trip-list">
|
||||
@@ -48,11 +57,12 @@ $canWrite = \App\Auth::canWrite();
|
||||
<div class="trip-main">
|
||||
<h2 class="trip-title"><?= e((string)$t['title']) ?></h2>
|
||||
<div class="trip-meta">
|
||||
<span><?= e(\App\Trip::formatSpan($t['starts_at'] ?? null, $t['ends_at'] ?? null)) ?></span>
|
||||
<?php if (!empty($t['location'])): ?>
|
||||
<span>📍 <?= e((string)$t['location']) ?></span>
|
||||
<?php /* 时间是从各段旅程算的;一段都没有时如实说"还没有旅程" */ ?>
|
||||
<span><?= e(\App\Trip::activitySpan($t['starts_at'] ?? null, $t['ends_at'] ?? null)) ?></span>
|
||||
<?php if (!empty($t['city'])): ?>
|
||||
<span>📍 <?= e((string)$t['city']) ?></span>
|
||||
<?php endif; ?>
|
||||
<span><?= (int)($t['node_count'] ?? 0) ?> 段行程</span>
|
||||
<span><?= (int)($t['node_count'] ?? 0) ?> 段旅程</span>
|
||||
</div>
|
||||
<?php if (!empty($t['summary'])): ?>
|
||||
<p class="trip-sum"><?= e(mb_substr((string)$t['summary'], 0, 80, 'UTF-8')) ?></p>
|
||||
|
||||
+52
-28
@@ -1,9 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* 旅程详情(v12):封面 + 小结 + 同行的人 + 照片 + 行程节点(计划 / 执行双轨)。
|
||||
* 活动详情(v12 建立,v24 第 15 批整理术语与派生值)。
|
||||
*
|
||||
* ⚠️ 双轨的意义:出发前排的是「打算怎么走」,走完记的是「实际怎么走的」。
|
||||
* 两轨并存才能回看"本来想去哪、后来去了哪"—— 这是 TripLog 最有价值的一点,搬过来时保留了。
|
||||
* 页面结构:封面 + 简介 + 同行的人 + 照片 + 下面的若干段**旅程**。
|
||||
*
|
||||
* ⚠️ 术语(用户 2026-10-05 第 15 批 第 1 条):这里是**一次活动**,
|
||||
* 里头一段一段的才是**旅程**。所以「+ 加一段计划行程」这类按钮
|
||||
* 统一改成「+ 加一段旅程」(阶段用「计划 / 实际」修饰)。
|
||||
*
|
||||
* ⚠️ 时间与城市都来自 $t 上挂的**派生值**(Trip::derived(),用户第 2 条)——
|
||||
* 页面不再读 events.starts_at / location。
|
||||
*/
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
// v24 第 13 批:不再按 category 双轨分,按**状态机 stage** 分三组展示。
|
||||
@@ -26,18 +32,19 @@ $memberIds = array_map('intval', array_column((array)($t['attendees'] ?? []), 'i
|
||||
?>
|
||||
<div class="page-head">
|
||||
<h1><?= e((string)$t['title']) ?></h1>
|
||||
<span class="sub"><?= e(\App\Trip::formatSpan($t['starts_at'] ?? null, $t['ends_at'] ?? null)) ?></span>
|
||||
<?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']) ?>」吗?照片与行程节点会一起删掉,不可恢复。')">
|
||||
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>
|
||||
<a class="btn btn-ghost" href="<?= e(url('trips')) ?>">返回活动</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,17 +57,29 @@ $memberIds = array_map('intval', array_column((array)($t['attendees'] ?? []), 'i
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="trip-hero-body">
|
||||
<?php if (!empty($t['location'])): ?>
|
||||
<p class="trip-loc">📍 <?= e((string)$t['location']) ?></p>
|
||||
<?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'])): ?>
|
||||
<div class="viewers-list">
|
||||
<?php foreach ($t['attendees'] as $a): ?>
|
||||
<?= person_chip($a, url('people/show', ['id' => (int)$a['id']])) ?>
|
||||
<?php endforeach; ?>
|
||||
<?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'])): ?>
|
||||
@@ -110,19 +129,22 @@ $memberIds = array_map('intval', array_column((array)($t['attendees'] ?? []), 'i
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php /* ==================== 行程(v24 第 13 批重构)====================
|
||||
用户 2026-10-05 原话要点:
|
||||
· 刚开始添加时选「计划行程 / 实际行程」
|
||||
· 计划行程有三个按钮:删除(**标记未成行,不真删**)、修正(记实际执行,可无限次)、
|
||||
<?php /* ==================== 这个活动里的旅程(v24 第 13 批重构)====================
|
||||
⚠️ 术语(用户第 15 批 第 1 条):这一块里每一条都是**一段旅程**,
|
||||
不是"行程";分三组是按它的**阶段**(计划 / 实际 / 未成行)。
|
||||
|
||||
用户 2026-10-05(第 13 批)原话要点:
|
||||
· 刚开始添加时选「计划 / 实际」
|
||||
· 计划的三段有三个按钮:删除(**标记未成行,不真删**)、修正(记实际执行,可无限次)、
|
||||
完成(实际=计划,直接变实际)
|
||||
· 未成行的仍显示、但带标记;不上日程、不进往年今日
|
||||
· 实际行程上日程 + 往年今日
|
||||
分三组渲染:计划 / 实际 / 未成行(未成行放最后,用灰调)。 */ ?>
|
||||
· 实际的会同时上日程 + 往年今日
|
||||
分三组渲染(未成行放最后,用灰调)。 */ ?>
|
||||
<?php
|
||||
$stageMeta = [
|
||||
'plan' => ['计划行程', '还没走,下面的「修正 / 完成 / 标记未成行」都能用'],
|
||||
'actual' => ['实际行程', '已走过 —— 会出现在日历与首页「往年今日」'],
|
||||
'skipped' => ['未成行', '这次计划没成行;不再出现在日程与「往年今日」,但在这里保留可查'],
|
||||
'plan' => ['计划的旅程', '还没走。每段下面的「修正 / 完成 / 标记未成行」都能用'],
|
||||
'actual' => ['走过的旅程', '已走过 —— 会出现在日历与首页「往年今日」'],
|
||||
'skipped' => ['没成行的', '这次没去成;不再出现在日程与「往年今日」,但在这里保留可查'],
|
||||
];
|
||||
foreach (['plan', 'actual', 'skipped'] as $sg):
|
||||
$list = $bysStage[$sg];
|
||||
@@ -136,7 +158,8 @@ foreach (['plan', 'actual', 'skipped'] as $sg):
|
||||
|
||||
<?php if (!$list): ?>
|
||||
<p class="empty-hint">
|
||||
<?= $sg === 'plan' ? '还没有排计划。' : ($sg === 'actual' ? '还没有走过的行程。' : '没有未成行的计划。') ?>
|
||||
<?= $sg === 'plan' ? '还没有排计划。点下面的「+ 加一段旅程」,把打算走的地方排进来。'
|
||||
: ($sg === 'actual' ? '还没有走过的旅程。' : '没有未成行的。') ?>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<ol class="node-list">
|
||||
@@ -212,9 +235,9 @@ foreach (['plan', 'actual', 'skipped'] as $sg):
|
||||
<summary class="btn btn-ghost btn-sm">修正</summary>
|
||||
<div class="rev-panel">
|
||||
<p class="rev-tip">
|
||||
记录**实际**执行情况。这仍与上面那条计划是<b>同一条记录</b>,
|
||||
记录实际执行情况。这仍与上面那条计划是<b>同一条记录</b>,
|
||||
原来的计划数据会保留在「修正记录」里,方便日后对比。
|
||||
点完这里<b>行程仍是计划</b>;确认无误后再点「标记为实际」。
|
||||
点完这里<b>这段旅程仍是计划</b>;确认无误后再点「标记为实际」。
|
||||
</p>
|
||||
<?php
|
||||
$rvNode = $n; $rvTripId = (int)$t['id'];
|
||||
@@ -225,14 +248,14 @@ foreach (['plan', 'actual', 'skipped'] as $sg):
|
||||
</div>
|
||||
</details>
|
||||
<form method="post" action="<?= e(url('trips/node_done')) ?>"
|
||||
onsubmit="return confirm('实际执行与计划完全相同,直接转为实际行程?')">
|
||||
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('标记为「未成行」?它不会再出现在日程与往年今日,但在这趟旅程里仍然看得到。')">
|
||||
onsubmit="return confirm('标记为「未成行」?它不会再出现在日程与往年今日,但在这个活动里仍然看得到。')">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$n['id'] ?>">
|
||||
<input type="hidden" name="trip_id" value="<?= (int)$t['id'] ?>">
|
||||
@@ -265,7 +288,7 @@ foreach (['plan', 'actual', 'skipped'] as $sg):
|
||||
?>
|
||||
</details>
|
||||
<form method="post" action="<?= e(url('trips/node_del')) ?>"
|
||||
onsubmit="return confirm('**彻底删除**这一段?连同它的修正记录一起消失,不可恢复。\n\n如果你只是想表达"这次没去成",请用「标记未成行」。')">
|
||||
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'] ?>">
|
||||
@@ -280,7 +303,8 @@ foreach (['plan', 'actual', 'skipped'] as $sg):
|
||||
|
||||
<?php if ($canWrite && $sg !== 'skipped'): ?>
|
||||
<details class="node-add">
|
||||
<summary class="link-quiet">+ 加一段<?= e($stageMeta[$sg][0]) ?></summary>
|
||||
<?php /* 术语:加的是"一段旅程",阶段只是它的属性 */ ?>
|
||||
<summary class="link-quiet">+ 加一段旅程(<?= $sg === 'actual' ? '实际' : '计划' ?>)</summary>
|
||||
<?php
|
||||
$nfNode = null; $nfTripId = (int)$t['id']; $nfCategory = $sg;
|
||||
$nfPeople = $people ?? []; $nfMembers = $memberIds;
|
||||
|
||||
Reference in New Issue
Block a user