v75 旅程重构:列表页 + 详情整页,卡片改「图在上、信息在下」

用户 2026-10-21:
「每一个旅程里面的行程才是重头戏……点击一个旅程,应该进入到一个新的界面,
  在里面列出所有的行程,就跟网页版的一样。
  这个旅程的图片是比较重要的……图片在正上方,就是充满整个的这个列表的宽度。
  然后呢把这个标题还有其他的一些信息放在下面。如果是图片放左边,信息放右边的话,
  它总是被挤占,显示不全。……想看其他的这个旅程的话,直接往下滑就可以了。」

- 新建 `pages/trips/FbTripDetailPage.ets`(照 `trips/show.php`):题图 16:9 占满整宽、
  五项统计、行程按 计划/实际/未成行 三组(空组不显示)、页头两行动作区
  (+计划=蓝 / +实际=绿 / 编辑=黄 / 删除=红 / 设置)
- `FbTripsPage` 846→573 行:删掉就地展开全套,`tripCard` 改「题图在上、信息全在下面」
  卡尾「看行程」自己带 onClick,整卡也可点
- `Types.ets` `FbTripNode` 8→18 字段 + 四个新类型;`FbTripRepo` 新增
  `fetchTripDetail` / `fetchTripSummary` 与四个解析辅助
- 修真 bug:`parseNodes` 读 `expense_cent`,服务端写的是 `spent_cents`
  ⇒ 每段「💰 这一段花了」从来没显示过
- `Store.ets` 新增 `@Trace tripDetailId` 与 `leaveTo(parentKey)`:
  删完整趟回列表要走它(内部 `applyModule` 会 `resetScroll`),
  直接赋 `activeModule` 会让回列表后轻轻一滑就收起悬浮栏
- 接线:`Index` 分发 / `TopBar` 标题「旅程」/ `Modules.fbModuleColor` 归到 trips 色
- `FbTripNodeSheet` 新增 `@Param startStage` 预选阶段(选择器保留,点错还能改)

守卫 931/0,反向验证 164/0,编译 BUILD SUCCESSFUL,真机待设备连上后跑 _fb_v75_verify.py
This commit is contained in:
2026-10-22 00:07:16 +08:00
parent 6e7cdaf5fc
commit 3e2904cc3d
10 changed files with 1862 additions and 462 deletions
+57 -7
View File
@@ -134,14 +134,64 @@ export const FB_DEMO_TRIPS: FbTripItem[] = [
}
];
/** 一段旅程的行程(trips/show 时间轴;stage 分「计划 / 实际」) */
/**
* 一段旅程的行程(trips/show 时间轴;stage 分「计划 / 实际 / 未成行」)。
*
* ⚠️ v75:`FbTripNode` 从 8 个字段扩到 18 个(详情页要显示 说明/路线/车次/
* 车牌/参与人/照片),这份演示数据必须跟着补齐 —— 少一个字段就编译不过
* (ArkTS 的对象字面量要**结构完全匹配**)。
* ⚠️ 它目前只是"形状参考",没有任何页面在读(真数据一律来自接口)。
* 但别删:新写详情页/浮层时对着它就能知道一个节点长什么样。
*/
export const FB_DEMO_TRIP_NODES: FbTripNode[] = [
{ id: 1, title: '出发', when: '10-02 07:30', place: '家 → 青岛北站', stage: 'actual', status: 'done', mode: '自驾', expenseCent: 0 },
{ id: 2, title: '抵达民宿', when: '10-02 10:20', place: '崂山区·沙子口民宿', stage: 'actual', status: 'done', mode: '自驾', expenseCent: 68000 },
{ id: 3, title: '海边散步 + 午餐', when: '10-02 12:00', place: '仰口海水浴场', stage: 'actual', status: 'done', mode: '步行', expenseCent: 21800 },
{ id: 4, title: '太清宫', when: '10-02 15:00', place: '崂山太清景区', stage: 'actual', status: 'done', mode: '自驾', expenseCent: 27000 },
{ id: 5, title: '登山(巨峰)', when: '10-03 08:00', place: '崂山巨峰景区', stage: 'plan', status: 'done', mode: '自驾', expenseCent: 34000 },
{ id: 6, title: '返程', when: '10-03 17:00', place: '青岛 → 家', stage: 'plan', status: 'done', mode: '自驾', expenseCent: 0 }
{
id: 1, title: '出发', when: '10-02 07:30', place: '家 → 青岛北站',
stage: 'actual', status: 'done', mode: 'drive', expenseCent: 0,
nodeType: 'move', detail: '六点起床,路上买了包子当早饭。',
fromPlace: '家', toPlace: '青岛北站', lineName: '', carrier: '',
tripNo: '', vehiclePlate: '鲁B·12345', revisionN: 0,
members: [], attachs: []
},
{
id: 2, title: '抵达民宿', when: '10-02 10:20', place: '崂山区·沙子口民宿',
stage: 'actual', status: 'done', mode: 'drive', expenseCent: 68000,
nodeType: 'stay', detail: '老板留了钥匙在门口花盆下,房间朝海。',
fromPlace: '青岛北站', toPlace: '沙子口民宿', lineName: '', carrier: '',
tripNo: '', vehiclePlate: '鲁B·12345', revisionN: 0,
members: [], attachs: []
},
{
id: 3, title: '海边散步 + 午餐', when: '10-02 12:00', place: '仰口海水浴场',
stage: 'actual', status: 'done', mode: 'walk', expenseCent: 21800,
nodeType: 'play', detail: '在礁石上捡了几块小石头。',
fromPlace: '沙子口民宿', toPlace: '仰口海水浴场', lineName: '', carrier: '',
tripNo: '', vehiclePlate: '', revisionN: 0,
members: [], attachs: []
},
{
id: 4, title: '太清宫', when: '10-02 15:00', place: '崂山太清景区',
stage: 'actual', status: 'done', mode: 'drive', expenseCent: 27000,
nodeType: 'play', detail: '',
fromPlace: '仰口', toPlace: '太清宫', lineName: '', carrier: '',
tripNo: '', vehiclePlate: '鲁B·12345', revisionN: 0,
members: [], attachs: []
},
{
id: 5, title: '登山(巨峰)', when: '10-03 08:00', place: '崂山巨峰景区',
stage: 'plan', status: 'planned', mode: 'drive', expenseCent: 34000,
nodeType: 'play', detail: '带够水,山上买贵。',
fromPlace: '沙子口民宿', toPlace: '巨峰景区', lineName: '', carrier: '',
tripNo: '', vehiclePlate: '鲁B·12345', revisionN: 0,
members: [], attachs: []
},
{
id: 6, title: '返程', when: '10-03 17:00', place: '青岛 → 家',
stage: 'plan', status: 'planned', mode: 'train', expenseCent: 0,
nodeType: 'move', detail: '',
fromPlace: '青岛北站', toPlace: '家', lineName: '京沪高铁', carrier: '中国铁路',
tripNo: 'G1234', vehiclePlate: '', revisionN: 0,
members: [], attachs: []
}
];
// ============================================================
+7 -1
View File
@@ -95,7 +95,13 @@ export function fbModuleName(key: string): string {
/** 模块主题色(认不出回落到首页砖红) */
export function fbModuleColor(key: string): string {
const m = fbModuleOf(key);
/* 🔴 v75:旅程详情页是**旅程模块的下一层**,不是独立模块
⇒ 不给它单独定色,直接沿用旅程的绿。否则详情页的按钮/圆点会回落砖红,
同一模块里出现两种主题色(用户一眼就看出"这一页跟刚才不一样")。
⚠️ 这类"下一层页面"以后再出现(观影详情、任务详情…)都在这里加一行,
别在页面里各自写死颜色。 */
const k: string = key === '__trip_detail' ? 'trips' : key;
const m = fbModuleOf(k);
return m !== undefined ? m.color : '#A84A28';
}
+100 -7
View File
@@ -409,23 +409,116 @@ export interface FbTripItem {
attendeeCount: number;
}
/** 旅程里的一段行程(trips/show 的时间轴节点) */
/**
* 旅程里的一段行程(trips/show 的时间轴节点)。
*
* 🔴 v75(用户 2026-10-21):「点击一个旅程,应该进入到一个新的界面,
* 在里面列出所有的行程,就跟网页版的一样。」
* ⇒ 端上第一次有**真正的详情页**,节点要显示的字段从 8 个涨到十几个 ——
* 网页版 `templates/trips/show.php` 的每个 `.node-*` 块都要有对应来源。
*
* ⚠️ 字段口径以 `app/Trip.php` 的 `nodes()` 真实返回为准(不是猜的):
* `spent_cents`(不是 `expense_cent`,那个名字不存在)、`members`、
* `attachs`、`vehicle_plate`(nodes() 单独挂上去的)、`revision_n`。
*/
export interface FbTripNode {
id: number;
/** 段标题,如「出发」「抵达民宿」 */
title: string;
/** 时间文案 */
/** 时间文案(已按服务端 `nodeSpan` 的口径折成「10-20 09:30」这种短串) */
when: string;
/** 地点 */
/** 地点(`place`,没有则「出发地 → 到达地」) */
place: string;
/** 阶段:plan(计划)/ actual(实际) */
/** 阶段:plan(计划)/ actual(实际)/ skipped(未成行) */
stage: string;
/** 状态:todo / done / skip */
/** 状态:planned / ongoing / done / cancelled */
status: string;
/** 交通方式(trips/modes) */
/** 交通方式 key(walk / drive / train…),空串 = 没填 */
mode: string;
/** 花费(分) */
/** 这一段花了多少(**分**;来自 `spent_cents`,是花销表按 node_id 汇总的) */
expenseCent: number;
/**
* 节点类型(`node_type`)。
* ⚠️ 两种取值:老数据的英文 key(play/meal/move/stay/other),
* 或用户在「节点类型」里自定义的**中文名字**。取图标时两种都要认。
*/
nodeType: string;
/** 详细说明(网页版 `.node-detail`) */
detail: string;
/** 出发地 / 到达地(网页版 `.node-route-line`,各带导航链接) */
fromPlace: string;
toPlace: string;
/** 线路名 / 承运方(网页版 `.node-traffic` 的两个 `.ntag`) */
lineName: string;
carrier: string;
/** 车次 / 航班号(火车、飞机才有;跟在模式词后面) */
tripNo: string;
/** 自驾时的车牌号(服务端 `nodes()` 用一条 IN 查询挂上去的) */
vehiclePlate: string;
/** 已修正次数(>0 才显示「已修正 N 次」徽章) */
revisionN: number;
/** 这一段的参与人 */
members: FbTripNodePerson[];
/** 照片 / 附件(图片直接铺缩略图墙) */
attachs: FbTripNodePhoto[];
}
/** 行程节点里的一个人(服务端 `Trip::nodeMembersFor()` 的返回) */
export interface FbTripNodePerson {
id: number;
/** 本名 */
name: string;
/** 「我叫他什么」(服务端 `my_nickname`;空则用 name) */
nickname: string;
avatarColor: string;
/** 头像 key(裸 key,渲染时走 `fbImageSrc`) */
avatar: string;
}
/** 行程节点上的一个附件(图片/视频/文件) */
export interface FbTripNodePhoto {
id: number;
/** 存储相对 key(裸 key,渲染时走 `fbImageSrc`) */
key: string;
/** 原始文件名(非图片才有意义) */
name: string;
/** 1 = 图片,可直接铺缩略图 */
isImage: boolean;
}
/** 旅程详情页右上角的五项统计(服务端 `Trip::summary()`) */
export interface FbTripSummary {
/** 参与人(带「我叫他什么」) */
people: FbTripNodePerson[];
/** 去过的城市(去重) */
cities: string[];
/** 天数(含首尾;0 = 还没有行程) */
days: number;
/** 段数(只数计划 + 实际,未成行单独算) */
nodeCount: number;
/** 未成行的段数 */
skipped: number;
/** 总花销(分) */
cents: number;
}
/**
* 旅程详情(`GET /api/v1/trips/<id>` + `/summary` 合成)。
*
* ⚠️ 列表项的 `FbTripItem` 不够用:详情页要 `cities` 数组(不是拼好的串)、
* 要一天的文案(`activitySpan`),还要参与人名单。
*/
export interface FbTripDetail {
id: number;
title: string;
/** 一段时间文案(服务端 `activitySpan`:「2026年10月2日 – 2026年10月3日」) */
dateRange: string;
coverKey: string;
summary: string;
/** 是否公开(编辑表单要回填) */
isPublic: boolean;
cities: string[];
attendees: FbTripNodePerson[];
}
/**