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:
+291
-19
@@ -85,7 +85,159 @@ final class Trip
|
||||
return in_array($key, self::STAGE_ON_CALENDAR, true);
|
||||
}
|
||||
|
||||
/** 全部旅程(按出发时间倒序) */
|
||||
// ==================== 活动的时间与目的地:**从旅程派生** ====================
|
||||
//
|
||||
// 用户原话(2026-10-05 第 15 批 第 2 条):
|
||||
// 「活动不需要开始和结束时间,是根据活动中的旅程,最早的旅程开始时间,
|
||||
// 和最晚的旅程结束时间,计算出来的。目的地也不需要,因为一次活动,
|
||||
// 可能去好几个城市,所以去的城市,也是根据旅程,得到的。」
|
||||
//
|
||||
// ⚠️ 这三样**不再是活动的字段**(表单里删了),一律读时算出来:
|
||||
// · 开始 = 最早一段旅程的 starts_at
|
||||
// · 结束 = 最晚一段旅程的 ends_at(缺 ends_at 就退回它的 starts_at)
|
||||
// · 城市 = 各段旅程 place 的去重集合
|
||||
// 理由与"参与人是合集"完全同一条:同一份数据存两处,迟早对不上。
|
||||
// 自己填的时间和行程里的时间一旦打架,看的人不知道该信哪个。
|
||||
//
|
||||
// ⚠️ 老数据里 events.starts_at / location 可能是有值的 —— **保留不动**
|
||||
// (万一将来要回退),但页面一律不读它们,只读派生值。
|
||||
// 迁移成本为 0,而"删列"是不可逆的。
|
||||
|
||||
/**
|
||||
* 一条活动的派生时间与城市(单条详情用)。
|
||||
*
|
||||
* @return array{starts_at:?string, ends_at:?string, cities:string[], city:string}
|
||||
* city 是给"一行文字"用的拼法(如「青岛 · 威海 → 济南」)。
|
||||
*/
|
||||
public static function derived(int $eventId): array
|
||||
{
|
||||
$st = Database::get()->prepare(
|
||||
"SELECT MIN(n.starts_at) AS start_at,
|
||||
MAX(COALESCE(n.ends_at, n.starts_at)) AS end_at
|
||||
FROM trip_nodes n
|
||||
WHERE n.event_id = ? AND n.stage IN ('plan','actual')"
|
||||
);
|
||||
$st->execute([$eventId]);
|
||||
$r = $st->fetch() ?: [];
|
||||
$cities = self::cities($eventId);
|
||||
return [
|
||||
'starts_at' => self::dtOrNull((string)($r['start_at'] ?? '')),
|
||||
'ends_at' => self::dtOrNull((string)($r['end_at'] ?? '')),
|
||||
'cities' => $cities,
|
||||
'city' => self::citiesWord($cities),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 一条活动去过的城市(各段旅程 place 去重,按**最早出现的时间**排)。
|
||||
*
|
||||
* ⚠️ 顺序按时间排而不是按字母/插入顺序:
|
||||
* "青岛 · 威海" 让人一眼看出先后,排成字母序就完全没有这层信息了。
|
||||
* ⚠️ skipped(未成行)的**不算去过** —— 计划去但没去成的地方不该出现在
|
||||
* "去过哪几个城市"里(这与"不上日程、不进往年今日"是同一条口径)。
|
||||
*/
|
||||
public static function cities(int $eventId): array
|
||||
{
|
||||
// ⚠️ ORDER BY 里**不能**写聚合别名(first_at):MySQL/MariaDB 会报
|
||||
// `Reference 'first_at' not supported (reference to group function)` ——
|
||||
// 而且这是**运行时**才炸(php -l 看不出),整个旅程模块直接 Fatal。
|
||||
// 写成 `ORDER BY MIN(n.starts_at), n.place` 就没问题(聚合函数本身可用)。
|
||||
$st = Database::get()->prepare(
|
||||
"SELECT n.place, MIN(n.starts_at) AS first_at
|
||||
FROM trip_nodes n
|
||||
WHERE n.event_id = ? AND n.stage IN ('plan','actual')
|
||||
AND n.place IS NOT NULL AND n.place <> ''
|
||||
GROUP BY n.place
|
||||
ORDER BY MIN(n.starts_at), n.place"
|
||||
);
|
||||
$st->execute([$eventId]);
|
||||
$out = [];
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
$p = trim((string)$r['place']);
|
||||
if ($p !== '' && !in_array($p, $out, true)) {
|
||||
$out[] = mb_substr($p, 0, 60, 'UTF-8');
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 城市列表 → 一行字。
|
||||
*
|
||||
* 用「 · 」分隔而不是顿号:城市名里本来就可能含顿号或逗号
|
||||
* ("海拉尔、满洲里"这种写法真有人用),混在一起会分不清有几个城市。
|
||||
*/
|
||||
public static function citiesWord(array $cities): string
|
||||
{
|
||||
return implode(' · ', $cities);
|
||||
}
|
||||
|
||||
/**
|
||||
* 给**一批**活动补上派生时间与城市(列表页用;N+1 会让几十趟活动明显变慢)。
|
||||
*
|
||||
* ⚠️ 与 attachAttendees 同一思路:一次查询按 event_id 分桶,再挂回每行。
|
||||
*/
|
||||
public static function attachDerived(array &$rows): void
|
||||
{
|
||||
$ids = [];
|
||||
foreach ($rows as $r) {
|
||||
$id = (int)($r['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
$ids[] = $id;
|
||||
}
|
||||
}
|
||||
$span = [];
|
||||
$city = [];
|
||||
foreach ($ids as $id) {
|
||||
$span[$id] = ['starts_at' => null, 'ends_at' => null];
|
||||
$city[$id] = [];
|
||||
}
|
||||
if ($ids) {
|
||||
$in = implode(',', array_fill(0, count($ids), '?'));
|
||||
$st = Database::get()->prepare(
|
||||
"SELECT event_id, MIN(starts_at) AS start_at,
|
||||
MAX(COALESCE(ends_at, starts_at)) AS end_at
|
||||
FROM trip_nodes
|
||||
WHERE event_id IN ($in) AND stage IN ('plan','actual')
|
||||
GROUP BY event_id"
|
||||
);
|
||||
$st->execute($ids);
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
$id = (int)$r['event_id'];
|
||||
$span[$id] = [
|
||||
'starts_at' => self::dtOrNull((string)($r['start_at'] ?? '')),
|
||||
'ends_at' => self::dtOrNull((string)($r['end_at'] ?? '')),
|
||||
];
|
||||
}
|
||||
// ⚠️ 同 cities():ORDER BY 只写聚合函数,不写别名
|
||||
$st2 = Database::get()->prepare(
|
||||
"SELECT event_id, place, MIN(starts_at) AS first_at
|
||||
FROM trip_nodes
|
||||
WHERE event_id IN ($in) AND stage IN ('plan','actual')
|
||||
AND place IS NOT NULL AND place <> ''
|
||||
GROUP BY event_id, place
|
||||
ORDER BY event_id, MIN(starts_at), place"
|
||||
);
|
||||
$st2->execute($ids);
|
||||
foreach ($st2->fetchAll() as $r) {
|
||||
$id = (int)$r['event_id'];
|
||||
$p = mb_substr(trim((string)$r['place']), 0, 60, 'UTF-8');
|
||||
if ($id > 0 && $p !== '' && !in_array($p, $city[$id], true)) {
|
||||
$city[$id][] = $p;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($rows as &$r) {
|
||||
$id = (int)$r['id'];
|
||||
$r['starts_at'] = $span[$id]['starts_at'] ?? null;
|
||||
$r['ends_at'] = $span[$id]['ends_at'] ?? null;
|
||||
$r['cities'] = $city[$id] ?? [];
|
||||
$r['city'] = self::citiesWord($r['cities']);
|
||||
}
|
||||
unset($r);
|
||||
}
|
||||
|
||||
/** 全部旅程(按**派生**的出发时间倒序 —— 活动自己没有时间字段了) */
|
||||
public static function all(int $familyId, string $keyword = ''): array
|
||||
{
|
||||
// ⚠️ 用户 2026-10-03 第 4 条:「旅程的活动中,只显示 is_public=1 的活动」。
|
||||
@@ -93,9 +245,24 @@ final class Trip
|
||||
// 老数据/异常情况下 trip_details 可能缺行,INNER JOIN 会让整趟旅程
|
||||
// 从列表里消失("行程丢了"比"多显示一条"严重得多),
|
||||
// COALESCE(...,1) 表示"没有这行就算公开"。
|
||||
// ⚠️ 参与人 = **所有行程参与人的合集**(v24 第 14 批 用户第 4 条):
|
||||
// 只在活动上登记、每段行程都没填成员的人也要算进来(否则老数据会"人没了")。
|
||||
// ⚠️ e.starts_at / e.ends_at / e.location 这三列**仍然 SELECT 出来**,
|
||||
// 但马上会被 attachDerived() 的派生值覆盖(用户第 15 批 第 2 条)。
|
||||
// 留着 select 是为了:① 不 SELECT 的话 PHP 侧少键,若哪天有代码先读再做
|
||||
// 兜底,会出现 Undefined index;② 出问题时肉眼能对着看"老值是什么"。
|
||||
$sql = 'SELECT e.id, e.title, e.starts_at, e.ends_at, e.location, e.note,
|
||||
d.cover_key, d.summary, COALESCE(d.is_public, 1) AS is_public,
|
||||
(SELECT COUNT(*) FROM event_attendees a WHERE a.event_id = e.id) AS attendee_count,
|
||||
/* ⚠️ 不能写成 SELECT COUNT(*) FROM (… WHERE a.event_id = e.id
|
||||
UNION …) u:派生表里的 e.id 是**外层**列,MySQL 8 之前
|
||||
不支持(要 LATERAL),MariaDB 也一样 —— 直接报列不存在。
|
||||
所以拆成两段相加:活动上登记的 + 行程里**没重复**的那些。 */
|
||||
((SELECT COUNT(*) FROM event_attendees a WHERE a.event_id = e.id)
|
||||
+ (SELECT COUNT(DISTINCT m.person_id) FROM trip_node_members m
|
||||
JOIN trip_nodes n2 ON n2.id = m.node_id
|
||||
WHERE n2.event_id = e.id
|
||||
AND m.person_id NOT IN (SELECT a2.person_id FROM event_attendees a2
|
||||
WHERE a2.event_id = e.id))) AS attendee_count,
|
||||
(SELECT COUNT(*) FROM trip_nodes n WHERE n.event_id = e.id) AS node_count
|
||||
FROM events e
|
||||
LEFT JOIN trip_details d ON d.event_id = e.id
|
||||
@@ -103,21 +270,32 @@ final class Trip
|
||||
$bind = [$familyId, 'trip'];
|
||||
$kw = trim($keyword);
|
||||
if ($kw !== '') {
|
||||
$sql .= ' AND (e.title LIKE ? OR e.location LIKE ? OR e.note LIKE ?)';
|
||||
// ⚠️ 搜索里的时间/城市也是**派生**的:
|
||||
// 活动自己的 location 已经不用了(用户第 2 条),
|
||||
// 搜城市必须搜到"在行程里填过的地方",否则搜"青岛"搜不到那趟青岛之行。
|
||||
$sql .= ' AND (e.title LIKE ? OR e.note LIKE ?
|
||||
OR EXISTS (SELECT 1 FROM trip_nodes n3
|
||||
WHERE n3.event_id = e.id AND n3.place LIKE ?))';
|
||||
$like = '%' . $kw . '%';
|
||||
array_push($bind, $like, $like, $like);
|
||||
}
|
||||
$sql .= ' ORDER BY e.starts_at DESC, e.id DESC';
|
||||
// 排序也走派生时间:子查询里没有行程时得排到最后(NULL)
|
||||
$sql .= ' ORDER BY (SELECT MIN(n4.starts_at) FROM trip_nodes n4
|
||||
WHERE n4.event_id = e.id AND n4.stage IN ("plan","actual")) DESC,
|
||||
e.id DESC';
|
||||
$st = Database::get()->prepare($sql);
|
||||
$st->execute($bind);
|
||||
return $st->fetchAll();
|
||||
$rows = $st->fetchAll();
|
||||
// 派生时间与城市:一次查询批量补(见 attachDerived 的说明)
|
||||
self::attachDerived($rows);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给一批旅程补上「同行的人」。
|
||||
* 给一批旅程补上「同行的人」= **所有行程参与人的合集**(用户第 4 条)。
|
||||
*
|
||||
* ⚠️ 必须一次性补(N+1 会让列表在几十趟旅程时明显变慢,观影就是这么踩的)。
|
||||
* 一次查询拿全部 event_attendees,再按 event_id 分桶挂回去。
|
||||
* 一次查询拿全部(活动登记 ∪ 各段行程成员),再按 event_id 分桶挂回去。
|
||||
*/
|
||||
public static function attachAttendees(array &$rows, int $viewerId = 0): void
|
||||
{
|
||||
@@ -134,14 +312,23 @@ final class Trip
|
||||
}
|
||||
if ($ids) {
|
||||
$in = implode(',', array_fill(0, count($ids), '?'));
|
||||
// 合集:**活动上登记的人** ∪ **每段行程的参与人**(用户第 4 条)。
|
||||
// UNION 自带去重,所以同一个人既在活动上、又在三段行程里也只出现一次。
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT a.event_id, p.id, p.name, p.avatar, p.avatar_color, n.nickname AS my_nickname
|
||||
FROM event_attendees a JOIN persons p ON p.id = a.person_id
|
||||
'SELECT u.event_id, p.id, p.name, p.avatar, p.avatar_color, n.nickname AS my_nickname
|
||||
FROM (
|
||||
SELECT a.event_id, a.person_id FROM event_attendees a
|
||||
WHERE a.event_id IN (' . $in . ')
|
||||
UNION
|
||||
SELECT nd.event_id, m.person_id FROM trip_node_members m
|
||||
JOIN trip_nodes nd ON nd.id = m.node_id
|
||||
WHERE nd.event_id IN (' . $in . ')
|
||||
) u
|
||||
JOIN persons p ON p.id = u.person_id
|
||||
LEFT JOIN nicknames n ON n.target_id = p.id AND n.viewer_id = ?
|
||||
WHERE a.event_id IN (' . $in . ')
|
||||
ORDER BY a.position, p.id'
|
||||
ORDER BY u.event_id, p.id'
|
||||
);
|
||||
$st->execute(array_merge([$viewerId], $ids));
|
||||
$st->execute(array_merge($ids, $ids, [$viewerId]));
|
||||
foreach ($st->fetchAll() as $a) {
|
||||
$byEvent[(int)$a['event_id']][] = $a;
|
||||
}
|
||||
@@ -167,11 +354,72 @@ final class Trip
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
$row['attendees'] = self::attendees($id, $viewerId);
|
||||
// 参与人 = 所有行程参与人的**合集**(用户第 4 条),不是只在活动上登记的那几个
|
||||
$row['attendees'] = self::tripPeople($id, $viewerId);
|
||||
$row['photos'] = \App\Attachment::list('trip', $id);
|
||||
// 时间与城市 = 从各段旅程派生(用户第 15 批 第 2 条)。
|
||||
// ⚠️ **覆盖**掉 events 上的老值:老数据可能自己填过起止与目的地,
|
||||
// 但页面必须以行程为准 —— 否则会出现"活动写着 8/1–8/5,行程只有 8/3 一段"
|
||||
// 这种自相矛盾的显示,而看的人无从判断该信哪个。
|
||||
$d = self::derived($id);
|
||||
$row['starts_at'] = $d['starts_at'];
|
||||
$row['ends_at'] = $d['ends_at'];
|
||||
$row['cities'] = $d['cities'];
|
||||
$row['city'] = $d['city'];
|
||||
$row['location'] = $d['city']; // 模板里还有读 location 的地方,一并给派生值
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一趟旅程的参与人 = **所有行程(节点)参与人的合集**(v24 第 14 批 用户第 4 条)。
|
||||
*
|
||||
* 用户原话:「一个活动的参与人,应该是这个活动下的所有旅行(程)的参与人的合集才对。」
|
||||
*
|
||||
* 为什么不在保存行程时"顺手同步"到活动上:
|
||||
* 那就是同一份名单存两处,改行程要记得改活动、删行程要记得减人 ——
|
||||
* 任何一处漏了就对不上。合求是**读的时候算出来的**,永远不会漂移。
|
||||
*
|
||||
* ⚠️ 取的是**并集**(活动上单独登记的人也保留):
|
||||
* 老数据里很多人只在活动上登记过、每段行程没填成员,
|
||||
* 只按行程算会让这些人"凭空消失"。
|
||||
*
|
||||
* @return array 去重后的人(带「我叫他什么」与主色)
|
||||
*/
|
||||
public static function tripPeople(int $eventId, int $viewerId = 0): array
|
||||
{
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT DISTINCT p.id, p.name, p.avatar, p.avatar_color, n.nickname AS my_nickname
|
||||
FROM (
|
||||
SELECT a.person_id FROM event_attendees a WHERE a.event_id = ?
|
||||
UNION
|
||||
SELECT m.person_id FROM trip_node_members m
|
||||
JOIN trip_nodes nd ON nd.id = m.node_id
|
||||
WHERE nd.event_id = ?
|
||||
) u
|
||||
JOIN persons p ON p.id = u.person_id
|
||||
LEFT JOIN nicknames n ON n.target_id = p.id AND n.viewer_id = ?
|
||||
ORDER BY p.id'
|
||||
);
|
||||
$st->execute([$eventId, $eventId, $viewerId]);
|
||||
return $st->fetchAll();
|
||||
}
|
||||
|
||||
/** 同上,但只给一个**人数**(列表页用,避免为每行再拉一堆人) */
|
||||
public static function tripPeopleCount(int $eventId): int
|
||||
{
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT COUNT(*) FROM (
|
||||
SELECT a.person_id FROM event_attendees a WHERE a.event_id = ?
|
||||
UNION
|
||||
SELECT m.person_id FROM trip_node_members m
|
||||
JOIN trip_nodes nd ON nd.id = m.node_id
|
||||
WHERE nd.event_id = ?
|
||||
) u'
|
||||
);
|
||||
$st->execute([$eventId, $eventId]);
|
||||
return (int)$st->fetchColumn();
|
||||
}
|
||||
|
||||
/** 同行的人(带「我叫他什么」与主色,页面按人画标签) */
|
||||
public static function attendees(int $eventId, int $viewerId = 0): array
|
||||
{
|
||||
@@ -190,13 +438,16 @@ final class Trip
|
||||
public static function create(int $familyId, ?int $creatorId, array $d): int
|
||||
{
|
||||
$db = Database::get();
|
||||
// ⚠️ 用户第 15 批 第 2 条:活动**不存**起止时间与目的地 ——
|
||||
// 它们从各段旅程派生(见 derived())。这里显式写 NULL,
|
||||
// 不接表单传来的值(表单里也没有这些字段了)。
|
||||
// events.starts_at / ends_at / location 保持可空,只是为了不动表结构。
|
||||
$db->prepare(
|
||||
"INSERT INTO events (family_id, creator_id, kind, title, starts_at, ends_at, location, note, source_ref)
|
||||
VALUES (?, ?, 'trip', ?, ?, ?, ?, ?, ?)"
|
||||
VALUES (?, ?, 'trip', ?, NULL, NULL, NULL, ?, ?)"
|
||||
)->execute([
|
||||
$familyId, $creatorId, (string)$d['title'],
|
||||
self::dtOrNull($d['starts_at'] ?? ''), self::dtOrNull($d['ends_at'] ?? ''),
|
||||
self::textOrNull($d['location'] ?? ''), self::textOrNull($d['note'] ?? ''),
|
||||
self::textOrNull($d['note'] ?? ''),
|
||||
$d['source_ref'] ?? null,
|
||||
]);
|
||||
$id = (int)$db->lastInsertId();
|
||||
@@ -208,13 +459,15 @@ final class Trip
|
||||
public static function update(int $familyId, int $id, array $d): void
|
||||
{
|
||||
$db = Database::get();
|
||||
// ⚠️ 与 create 同理:**不碰** starts_at / ends_at / location。
|
||||
// 连"置 NULL"都不做 —— 老数据里可能存过值,编辑一次活动不该把它抹掉
|
||||
// (页面反正不读它们;真出问题时还能拿老值对着看)。
|
||||
$db->prepare(
|
||||
"UPDATE events SET title=?, starts_at=?, ends_at=?, location=?, note=?
|
||||
"UPDATE events SET title=?, note=?
|
||||
WHERE id=? AND family_id=? AND kind='trip'"
|
||||
)->execute([
|
||||
(string)$d['title'],
|
||||
self::dtOrNull($d['starts_at'] ?? ''), self::dtOrNull($d['ends_at'] ?? ''),
|
||||
self::textOrNull($d['location'] ?? ''), self::textOrNull($d['note'] ?? ''),
|
||||
self::textOrNull($d['note'] ?? ''),
|
||||
$id, $familyId,
|
||||
]);
|
||||
self::saveDetails($id, $d);
|
||||
@@ -552,6 +805,25 @@ final class Trip
|
||||
// ---------------- 展示 ----------------
|
||||
|
||||
/** 起止日期的人话展示(与 Movie::formatWatch 同款口径) */
|
||||
/**
|
||||
* 活动的时间文案(用户第 15 批 第 2 条:时间是从旅程算出来的)。
|
||||
*
|
||||
* ⚠️ 空的时候**不能**沿用 formatSpan 的「日期待补」:那是给"一段没填时间的行程"
|
||||
* 用的短语,放在活动上会让人以为"这里本来该由我填、我忘了填"。
|
||||
* 活动的时间本来就该由旅程决定 —— 没有旅程时要说清楚是**去加一段旅程**。
|
||||
*/
|
||||
public static function activitySpan(?string $start, ?string $end): string
|
||||
{
|
||||
if ($start === null || $start === '') {
|
||||
return '还没有旅程';
|
||||
}
|
||||
$s = date('Y年n月j日', (int)strtotime($start));
|
||||
if ($end === null || $end === '') {
|
||||
return $s . ' 起';
|
||||
}
|
||||
return $s . ' – ' . date('Y年n月j日', (int)strtotime($end));
|
||||
}
|
||||
|
||||
public static function formatSpan(?string $start, ?string $end): string
|
||||
{
|
||||
if ($start === null || $start === '') {
|
||||
|
||||
Reference in New Issue
Block a user