v24 第 16 批:旅程 / 行程两级(术语固定 · 题图16:9 · 五项统计 · 独立行程页 · 设置三页)

用户十条,逐条落地:

1. 「题图应该按照 16:9 显示,因为添加的时候是按这个比例裁剪的,现在的显示比例不对。」
   → 详情页题图容器从 4:3 改成 16:9,与 ImageSlot 的 cover 槽位同一个数字
     (部署断言比对这两处,避免哪天改一处漏一处)。

2. 「题图的右侧,显示的是参与人都有谁、去了几个城市、一共去了几天、一共几段行程,
     一共花了多少钱,这些信息,都从该活动下的所有行程统计总结,简介那一段话保留。」
   → 新增 Trip::summary():参与人(各段并集)/ 城市(去重)/ 天数 / 段数 / 总花销,
     全部读时算(与"时间城市派生"同一条原则,不存冗余)。
     天数按**日历天**含首尾(8/1→8/3 = 3 天,不是 24 小时整除的 2 天)。
     城市与时间从标题旁挪进信息区,简介保留。

3. 「活动页面,不需要上传图片,图片视频,都是针对一次具体的行程的。」
   → 删掉旅程页的照片上传板块与照片墙;照片一律挂在**行程**上
     (行程表单里有上传,附件 target_type='trip_node')。

4. 「计划行程、未成行的行程,都不要显示……只要是没有就是 0 条,那个类型就不显示了,
     现在显示的是 0 条,这样没意义。」
   → 空分组直接跳过不渲染;全空时给一句引导(否则新建旅程页面一片空白)。
     有内容的分组照常显示(计划 / 实际 / 未成行三种)。

5. 「行程的编辑功能,点击后会把页面撑破。编辑行程,打开新的页面进行编辑。」
   → 行程的新增与编辑改成**独立页面**(trips/node_new / trips/node_edit)。
     原来内联在详情页:N 份表单互相撞元素 id、密码管理器逐个扫描变卡,
     正是"把页面撑破"的成因。

6. 「添加计划行程和实际行程的按钮,都放在右上角。点击后打开新的页面进行添加。」
   → 详情页右上角两个按钮(+ 计划行程 / + 实际行程),指向独立新增页。
     顺带:标题旁边不再放时间(时间移到题图右侧信息区)。

7. 「添加和编辑行程的内容,你去找 triplog 里面的添加节点功能,跟那个一样。」
   → 字段全量照搬 TripLog 的 nodes/form.php:
     类型 / 标题 / 起止时间 / 出行方式 + 车辆 / 线路名 / 承运方 / 车次航班号 /
     座位 / 登机口 / 出发地 / 到达地 / 内容 / 参与人(含角色)/ 照片 / 花销。
     交通字段不做条件列(先填高铁车次、后来改成自驾,填过的东西不该丢)。

8. 「右上角的按钮,需要增加一个设置按钮,用来设置车辆管理、出行方式、节点类型。
     注意,我们在财务中,已经把车辆添加进去了,所以这里不用重复添加那里面的车辆,
     直接读取就可以,但是可以添加其他的车辆,比如租的车,借别人的车。」
   → 新增旅程设置菜单(三页):车辆管理 / 出行方式 / 节点类型。
     车辆**两个来源刻意不合并**:财务里 kind='car' 的账目(那是资产,没有车牌车型)
     由 Vehicle::fromLedger() 只读列出,选车下拉里标「来自财务」;
     自家 / 租的 / 借的车录进 vehicles 表。不复制财务的车(避免同一台车两处维护)。
     节点类型做成**可自定义**的表(TripLog 同款),内置 5 种不可删;
     行程里存类型**名字**,所以改类型名会级联更新已用它的行程。

9. 「活动都改名吧,改成旅程,新建旅程,旅程包含行程,这个名字就固定下来。」
   → 术语固定:**旅程**(容器)包含**行程**(单元)。
     顶层按钮「新建旅程」、详情页三组叫「计划的行程 / 实际行程 / 未成行」、
     列表「N 段行程」、设置菜单「旅程设置」。上一批的"活动"叫法全部换掉。

10. 「新建旅程里面,怎么还有备注啊,同行的人啊,这些都不要了,这些都是从行程里面
      总结出来的。在新建旅程和编辑旅程的时候,都不要了。」
   → 旅程表单只剩:标题 → 题图 → 简介 + 公开开关。
     备注与同行的人全部移除(参与人 = 各段行程参与人的合集)。
     ⚠️ 但**老数据里活动级那份参与人名单不能被清空**(那时还没有行程级参与人),
     所以路由"没传这个键就不动",并加了断言钉住这一点。

数据结构(Schema v21,老库自动升级):
· 新表 vehicles(自家/租的/借的)、node_types(可自定义)、trip_expenses(行程级花销)
· trip_nodes 补 9 列:出行方式 / 车辆 / 线路名 / 承运方 / 车次 / 座位 / 登机口 / 起终点
· trip_node_members 补 role_tag(司机 / 摄影担当这类角色)
· 花销金额存**分**,解析走字符串拆小数点(128.55*100 在浮点下会差一分)

验证:e2e 891/0(+46 条本批断言)· 升级 134/0 · 桌面几何 428/0 · 交互 92/0 ·
窄屏溢出 30/0(扩到行程表单 + 设置三页 + 详情页)· 题图点击 4/0 · 静态体检全绿。
**反向验证 6/6**:题图退回 4:3、标题旁加回时间、空分组照旧渲染、编辑退回非独立页、
车辆不读财务、总花销不汇总 —— 六种坏法全部被断言抓住。
部署静态断言新增第 16 批一组(含"空分组不许渲染""行程页不许有照片上传"
"旅程表单不许有备注与同行的人""题图必须 16:9"等反退化防线)。
部署后服务器自检 19/19,六个关键文件 md5 与线上逐一核对一致。
This commit is contained in:
2026-10-05 13:26:49 +08:00
parent 7138314dc7
commit 2e51d3f5e4
16 changed files with 2265 additions and 456 deletions
+129 -1
View File
@@ -2345,8 +2345,12 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
.trip-people { display: flex; flex-wrap: wrap; gap: 4px 8px; }
.trip-hero { display: flex; gap: 20px; flex-wrap: wrap; }
/* ⚠️ v24 第 16 批 用户第 1 条:「题图应该按照 16:9 显示,因为添加的时候,
是按照这个比例裁剪的,现在的显示比例不对」—— 原来是 4:3 把 16:9 的图裁掉了两边。
这个数字必须与 ImageSlot 的 'cover' 槽位比例**一致**(部署断言专门比对两处)。 */
.trip-hero-cover {
width: 260px; max-width: 100%; aspect-ratio: 4 / 3; flex: 0 0 auto; border-radius: var(--radius-s);
width: 420px; max-width: 100%; aspect-ratio: 16 / 9; flex: 0 0 auto;
border-radius: var(--radius-s);
overflow: hidden; background: #F1E7D6; display: flex; align-items: center; justify-content: center;
}
.trip-hero-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
@@ -3207,3 +3211,127 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
.more-set .form-grid { padding: 14px; margin: 0; }
/* 折叠区里的字段少一点内边距,看起来是"附带的"而不是主流程 */
.more-set .field { margin-bottom: 0; }
/* ============================================================
v24 第 16 批:旅程(容器)/ 行程(单元)两级
术语固定:旅程包含行程(用户第 9 条)。
新增:题图 16:9、五项统计、行程独立页、设置三页、花销。
============================================================ */
/* ---- 题图右侧的信息区:时间 + 城市 ---- */
.trip-when {
display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px;
font-size: 14px; color: var(--muted);
}
.trip-when-date { font-weight: 600; color: var(--ink); }
.trip-when-cities { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 5px; }
.city-chip {
padding: 1px 9px; border-radius: 999px;
background: var(--line-2); color: var(--brand-ink); font-size: 12.5px;
}
.city-sep { color: var(--faint); }
/* ---- 五项统计(用户第 2 条)---- */
.trip-stats {
display: flex; flex-wrap: wrap; gap: 10px 26px;
padding: 10px 0; border-top: 1px solid var(--line-2); border-bottom: 1px solid var(--line-2);
}
.ts-item { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
.ts-n {
font-size: 19px; font-weight: 700; color: var(--brand);
font-variant-numeric: tabular-nums; line-height: 1.2;
}
.ts-l { font-size: 12px; color: var(--muted); }
.trip-sum { margin: 0; }
/* ---- 行程行上的新元素:出行方式 / 交通细节 / 起终点 / 花销 / 角色 ---- */
.node-mode { font-size: 12px; color: var(--muted); white-space: nowrap; }
.node-traffic { display: flex; flex-wrap: wrap; gap: 5px; }
.node-traffic .ntag {
font-size: 11.5px; padding: 1px 8px; border-radius: 5px;
background: var(--line-2); color: var(--muted);
}
.node-route-line { font-size: 12.5px; color: var(--muted); }
.node-route-line .arrow { color: var(--faint); margin: 0 3px; }
.node-spent { font-size: 12.5px; color: var(--brand-ink); }
.node-member { display: inline-flex; align-items: center; gap: 4px; }
.node-role {
font-size: 11px; padding: 0 6px; border-radius: 999px;
background: rgba(168, 74, 40, .12); color: var(--brand-ink);
}
/* ---- 行程表单:参与人角色 ---- */
.role-set { margin-top: 6px; }
.role-grid { display: grid; gap: 6px; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); margin-top: 8px; }
.role-row { display: flex; align-items: center; gap: 8px; }
.role-who { flex: 0 0 72px; font-size: 13px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.role-row .input { flex: 1 1 auto; min-width: 0; padding: 6px 9px; font-size: 13px; }
/* ---- 花销 ---- */
.exp-list { list-style: none; margin: 0 0 12px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.exp-row {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
padding: 7px 11px; border: 1px solid var(--line-2); border-radius: var(--radius-s);
background: #FDFBF7; font-size: 13.5px;
}
.exp-cat {
font-size: 11.5px; padding: 1px 8px; border-radius: 999px;
background: var(--line-2); color: var(--brand-ink);
}
.exp-title { flex: 1 1 140px; min-width: 0; }
.exp-amt { font-weight: 600; font-variant-numeric: tabular-nums; }
.exp-total { margin: 0 0 12px; font-size: 14px; }
.exp-form-row { display: flex; flex-wrap: wrap; gap: 8px; }
.exp-form-row .input { flex: 1 1 130px; min-width: 0; }
.exp-form-row .select { flex: 0 0 auto; width: auto; min-width: 96px; }
/* ---- 设置三页:车辆 / 出行方式 / 节点类型 ---- */
.set-sub { font-size: 14px; font-weight: 600; margin: 16px 0 8px; }
.veh-list { list-style: none; margin: 0 0 10px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.veh-row {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding: 8px 11px; border: 1px solid var(--line-2); border-radius: var(--radius-s);
background: #FDFBF7;
}
.veh-ro { background: var(--line-2); }
.veh-ico { font-size: 17px; }
.veh-name { font-size: 14px; font-weight: 600; }
.veh-tag { font-size: 11.5px; padding: 1px 8px; border-radius: 999px; background: var(--surface); color: var(--muted); border: 1px solid var(--line); }
.veh-form { flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-width: 0; }
.veh-form .input { flex: 1 1 110px; min-width: 0; padding: 6px 9px; font-size: 13px; }
.veh-form .select { flex: 0 0 auto; width: auto; padding: 6px 9px; font-size: 13px; }
.veh-def { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; color: var(--muted); white-space: nowrap; }
.veh-add { margin-top: 4px; }
.mode-grid { display: flex; flex-direction: column; gap: 8px; }
.mode-row {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
padding: 8px 11px; border: 1px solid var(--line-2); border-radius: var(--radius-s);
background: #FDFBF7;
}
.mode-ico { font-size: 17px; }
.mode-name { flex: 0 0 64px; font-size: 14px; }
.mode-row .input { flex: 1 1 220px; min-width: 0; padding: 6px 9px; font-size: 13px; }
.mode-none { flex: 1 1 auto; }
.type-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.type-row {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding: 8px 11px; border: 1px solid var(--line-2); border-radius: var(--radius-s);
background: #FDFBF7;
}
.type-form { flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-width: 0; }
.type-form .input { flex: 1 1 140px; min-width: 0; padding: 6px 9px; font-size: 13px; }
.type-ico { flex: 0 0 52px !important; text-align: center; font-size: 16px !important; }
.type-tag { font-size: 11.5px; padding: 1px 8px; border-radius: 999px; background: var(--surface); color: var(--muted); border: 1px solid var(--line); }
.type-tag-custom { background: rgba(168, 74, 40, .1); color: var(--brand-ink); border-color: rgba(168, 74, 40, .25); }
.type-used { flex: 0 0 auto; }
.type-add { margin-top: 4px; }
/* ---- 窄屏 ---- */
@media (max-width: 860px) {
/* 题图在窄屏铺满一行(信息区折到下面)—— 16:9 的比例保持不变 */
.trip-hero-cover { width: 100%; }
.trip-stats { gap: 8px 18px; }
.ts-n { font-size: 17px; }
}