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:
@@ -16,6 +16,7 @@ import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { FbColors, FbMetric } from '../common/Theme';
|
||||
import { FbAppState, FB_WIDE_BREAKPOINT, fbAppState } from '../common/Store';
|
||||
import { fbModuleColor } from '../model/Modules';
|
||||
import { FbDrawerShell } from '../components/Drawer';
|
||||
import { FbTopBar } from '../components/TopBar';
|
||||
import { FB_GRIP, fbStartGripWatch } from '../common/FbGrip';
|
||||
@@ -23,6 +24,8 @@ import { FbFloatDock, FB_DOCK_HIDE_Y } from '../components/FloatDock';
|
||||
import { FbHomePage } from './home/FbHomePage';
|
||||
import { FbMoviesPage } from './movies/FbMoviesPage';
|
||||
import { FbTripsPage } from './trips/FbTripsPage';
|
||||
/* 🔴 v75:旅程详情是**整页**(用户:「点击一个旅程,应该进入到一个新的界面」) */
|
||||
import { FbTripDetailPage } from './trips/FbTripDetailPage';
|
||||
import { FbCalendarPage } from './calendar/FbCalendarPage';
|
||||
import { FbTasksPage } from './tasks/FbTasksPage';
|
||||
import { FbPeoplePage } from './people/FbPeoplePage';
|
||||
@@ -669,6 +672,11 @@ struct Index {
|
||||
FbNotesPage({ app: this.app, topBar: this.topBarSlot })
|
||||
} else if (this.app.activeModule === '__notifications') {
|
||||
FbNotificationsPage({ app: this.app, topBar: this.topBarSlot })
|
||||
/* 🔴 v75(用户 2026-10-21):旅程详情整页。
|
||||
⚠️ 必须传 `accent`(旅程模块绿),否则详情页的按钮/圆点会回落砖红,
|
||||
与列表页颜色不一致。 */
|
||||
} else if (this.app.activeModule === '__trip_detail') {
|
||||
FbTripDetailPage({ app: this.app, topBar: this.topBarSlot, accent: fbModuleColor('trips') })
|
||||
} else if (this.app.activeModule === '__settings') {
|
||||
FbSettingsPage({ app: this.app, topBar: this.topBarSlot })
|
||||
} else if (this.app.activeModule === '__account') {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,28 @@
|
||||
/**
|
||||
* 家书 FamilyBook · 旅程模块(鸿蒙端)
|
||||
*
|
||||
* 对应 Web 端 `templates/trips/index.php` + `trips/show.php`。
|
||||
* 对应 Web 端 `templates/trips/index.php`(列表)+ `trips/show.php`(详情)。
|
||||
*
|
||||
* 术语照 Web 端**固定下来**的那套(用户明确要求):
|
||||
* · **旅程** = 列表里的一条(容器,如「青岛·崂山 两日」);
|
||||
* · **行程** = 旅程里面的一段一段(详情页时间轴上的节点)。
|
||||
* · **行程** = 旅程里面的一段一段(详情页里的一组组节点)。
|
||||
*
|
||||
* 端上的形态:**上列表 / 下详情同屏**不现实(手机高度不够),
|
||||
* 所以这页做「列表 + 展开选中项的行程摘要」;点某条旅程展开它的行程时间轴。
|
||||
* 时间轴节点分「计划 / 实际」两阶段 —— 与 Web 端一致(v41 加的四种动作按钮)。
|
||||
* 🔴 v75(用户 2026-10-21)改了端上的形态:
|
||||
* 「每一个旅程里面的行程才是重头戏……点击一个旅程,应该进入到**一个新的界面**,
|
||||
* 在里面列出所有的行程,就跟网页版的一样。」
|
||||
* ⇒ 本页只负责**列表**;点卡片进 `FbTripDetailPage`(整页,走
|
||||
* `app.activeModule = '__trip_detail'`)。
|
||||
* ⚠️ 原来那套「就地展开行程时间轴」(`expandedId` / `stageBlock` / `nodeRow`)
|
||||
* 已整块删除 —— 卡片宽度放不下详情页要显示的内容(说明/路线/花销/参与人/照片)。
|
||||
* 别把它加回来当"快捷查看":两套并存的代价是两份节点渲染要同时维护。
|
||||
*/
|
||||
|
||||
import { FbColors, FbMetric, fbTint, FbTint, FB_MASK, fbAlpha } from '../../common/Theme';
|
||||
import { FbColors, FbMetric, fbTint, FbTint, FB_MASK } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbTripItem, FbTripNode } from '../../model/Types';
|
||||
import { fbMoney } from '../../model/DemoData';
|
||||
import { FbTripItem } from '../../model/Types';
|
||||
import { parseTrips } from '../../data/FbParse';
|
||||
import { loadModule } from '../../data/FbModuleLoad';
|
||||
import { FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FbEmptyBox, FbIconButton, FbPrimaryButton } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
import { fbOnScroll, fbOnTouch } from '../../common/FbScroll';
|
||||
import { fbMaskTransition, fbSheetTransition } from '../../common/FbAnim';
|
||||
@@ -28,9 +32,7 @@ import { FB_SET_TRIPS } from '../../model/FbSettingsMenus';
|
||||
/* v65:题图要走媒体代理(`Image()` 加不了自定义请求头,令牌得用 URL query 带过去) */
|
||||
import { fbImageSrc } from '../../data/FbAttachQueue';
|
||||
import { FB_TOKEN_STORE } from '../../data/FbTokenStore';
|
||||
import { fetchTripNodes } from '../../data/FbTripRepo';
|
||||
import { FbTripNewSheet } from '../../components/FbTripNewSheet';
|
||||
import { FbTripNodeSheet } from '../../components/FbTripNodeSheet';
|
||||
/* v66:旅程三项设置(车辆 / 出行方式 / 节点类型) */
|
||||
import { FbNodeTypeSheet, FbTravelModesSheet, FbVehicleSheet } from '../../components/FbTripsSettingsSheets';
|
||||
|
||||
@@ -60,20 +62,10 @@ export struct FbTripsPage {
|
||||
@Local settingsOpen: boolean = false;
|
||||
|
||||
@Local trips: FbTripItem[] = [];
|
||||
/**
|
||||
* 行程节点。
|
||||
*
|
||||
* ⚠️ 接口 `/trips` 只给旅程**本身**(含 `node_count`),不给每一段。
|
||||
* 所以这里保持空 —— 展开时才去拉 `/trips/<id>`(待实现)。
|
||||
* 不假装有数据:宁可展开后显示"暂无行程段"。
|
||||
*/
|
||||
@Local nodes: FbTripNode[] = [];
|
||||
/** 顶部提示(同步状态) */
|
||||
@Local syncNote: string = '';
|
||||
/** 模块主题色(旅程 = 绿) */
|
||||
private accent: string = '#3F7D5A';
|
||||
/** 当前展开的旅程 id(0 = 都没展开) */
|
||||
@Local expandedId: number = 0;
|
||||
/**
|
||||
* 「新建旅程」浮层是否打开(v65)。
|
||||
*
|
||||
@@ -85,16 +77,8 @@ export struct FbTripsPage {
|
||||
@Local editingId: number = 0;
|
||||
@Local editingTitle: string = '';
|
||||
@Local editingSummary: string = '';
|
||||
/** 行程节点正在加载(展开时拉 `/trips/<id>/nodes`) */
|
||||
@Local nodesLoading: boolean = false;
|
||||
/** 节点加载失败的说明(空 = 没失败) */
|
||||
@Local nodesNote: string = '';
|
||||
/** 哪一趟旅程的节点是「当前这份 `nodes`」(避免 A 的节点画到 B 上) */
|
||||
@Local nodesTripId: number = 0;
|
||||
/** 题图加载失败集合(一张图坏了不该反复重试,会让列表一直闪) */
|
||||
@Local coverFailedIds: number[] = [];
|
||||
/** 「加一段行程」浮层是否打开,以及它属于哪一趟旅程(0 = 关着) */
|
||||
@Local nodeSheetTripId: number = 0;
|
||||
|
||||
/**
|
||||
* 🔴 v66(用户 2026-10-20):「旅程的……车辆管理、出行方式、节点设置,
|
||||
@@ -124,10 +108,6 @@ export struct FbTripsPage {
|
||||
private backOnce(): boolean {
|
||||
/* v65:返回手势要**先关新的浮层**,再关设置菜单 ——
|
||||
顺序按"层级由浅到深"(后打开的在前),否则按一次返回会跳关两层。 */
|
||||
if (this.nodeSheetTripId > 0) {
|
||||
this.nodeSheetTripId = 0;
|
||||
return true;
|
||||
}
|
||||
/* v66:三项设置浮层。⚠️ 内部还有"表单展开 / 待确认删除"两层,
|
||||
按返回先由浮层自己处理(组件的遮罩 onClick 也会关)—— 这里只负责关整层。 */
|
||||
if (this.vehiclesOpen) {
|
||||
@@ -150,11 +130,8 @@ export struct FbTripsPage {
|
||||
this.settingsOpen = false;
|
||||
return true;
|
||||
}
|
||||
// 行程展开着时,返回先收起它(否则用户只能靠再点一次卡片)
|
||||
if (this.expandedId !== 0) {
|
||||
this.expandedId = 0;
|
||||
return true;
|
||||
}
|
||||
/* 🔴 v75:原来这里还有「收起展开的行程」一层 —— 就地展开那套已删除
|
||||
(点卡片改成进详情页了)。别留一个永远为假的判断:`expandedId` 已不存在。 */
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -197,12 +174,6 @@ export struct FbTripsPage {
|
||||
fbAppState().sheetCount = this.newOpen ? 1 : 0;
|
||||
}
|
||||
|
||||
/** 「加一段行程」浮层同样要让悬浮栏让位 */
|
||||
@Monitor('nodeSheetTripId')
|
||||
onNodeSheetChanged(): void {
|
||||
fbAppState().sheetCount = this.nodeSheetTripId > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 三个设置浮层同样要让悬浮栏让位。
|
||||
*
|
||||
@@ -359,32 +330,12 @@ export struct FbTripsPage {
|
||||
onSaved: (): void => {
|
||||
this.newOpen = false;
|
||||
// 保存后重新拉一遍列表(服务端的城市/时间是派生的,本地拼不出来)
|
||||
this.expandedId = 0;
|
||||
this.load();
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 加一段行程浮层(v65)----
|
||||
if (this.nodeSheetTripId > 0) {
|
||||
FbTripNodeSheet({
|
||||
tripId: this.nodeSheetTripId,
|
||||
accent: this.accent,
|
||||
onClose: (): void => {
|
||||
this.nodeSheetTripId = 0;
|
||||
},
|
||||
onSaved: (): void => {
|
||||
const id: number = this.nodeSheetTripId;
|
||||
this.nodeSheetTripId = 0;
|
||||
// 加完重拉节点(服务端会重算派生时间,列表也要跟着变)
|
||||
this.load();
|
||||
this.loadNodes(id);
|
||||
}
|
||||
})
|
||||
.transition(fbSheetTransition())
|
||||
}
|
||||
|
||||
// ---- 旅程三项设置浮层(v66)----
|
||||
if (this.vehiclesOpen) {
|
||||
FbVehicleSheet({
|
||||
@@ -421,227 +372,184 @@ export struct FbTripsPage {
|
||||
}
|
||||
|
||||
/**
|
||||
* 一张旅程卡:**题图 + 标题 + 时间/城市/段数 + 简介 + 同行人**。
|
||||
* 一张旅程卡:**题图在上(占满整宽)+ 标题与信息在下**。
|
||||
*
|
||||
* 🔴 v65(用户 2026-10-20):「旅程的列表也不太对呀,应该有,题图啊、标题啊、
|
||||
* 然后城市啊,应该跟网页版的题图是类似的。」
|
||||
* ⇒ 照 `templates/trips/_list_body.php` 的 `trip-card` 重做:
|
||||
* 网页版是**横排**(左题图 132×96 + 右 `trip-main`)。
|
||||
* 手机上按同样比例缩到 96×72(4:3),右边留足文字宽度。
|
||||
* 🔴🔴 v75(用户 2026-10-21 原话):
|
||||
* 「这个旅程的图片是比较重要的,所以我建议就是这个显示的时候啊,
|
||||
* 图片在正上方,就是充满整个的这个列表的宽度。然后呢把这个标题
|
||||
* 还有其他的一些信息放在下面。如果是图片放左边,信息放右边的话,
|
||||
* 它总是被挤占,显示不全。这样的话就可以显示更多的信息。
|
||||
* 想看其他的这个旅程的话,直接往下滑就可以了。」
|
||||
*
|
||||
* ⚠️ 题图比例:网页版列表卡是 132×96 ≈ 4:3,而**详情页**的 hero 是 16:9。
|
||||
* 这里按列表卡的 4:3 走(与用户要的"跟网页版类似"一致)。
|
||||
* ⇒ 两条都是**用户点名的**,别改回去:
|
||||
* ① 题图**横跨整张卡**(`width('100%')` + 定比例),不再是左上角 96×72 小图;
|
||||
* ② 信息**全部移到图下面**(原来在右边,被 96 宽的小图挤得只剩一半宽度,
|
||||
* 标题折两行、简介被截、城市被省略号吃掉)。
|
||||
*
|
||||
* ⚠️ 题图比例用 **16:9**,不是网页版列表卡的 4:3:
|
||||
* `app/ImageSlot.php` 里 `cover` 槽位是 **1920×1080(16:9)**,
|
||||
* 上传时按 16:9 裁的。用 4:3 显示会把两边裁掉 —— 正是用户当年在网页版
|
||||
* 报过的「题图比例不对,把两边裁掉了」。整宽 + 16:9 = 图**原样**呈现。
|
||||
*
|
||||
* ⚠️ 点卡片 = **进详情页**(不再就地展开)。行程才是重头戏,一屏展开放不下。
|
||||
*/
|
||||
@Builder
|
||||
tripCard(t: FbTripItem) {
|
||||
Column({ space: 0 }) {
|
||||
// ---- 卡头:左题图 + 右内容(横排,同网页版)----
|
||||
Row({ space: 12 }) {
|
||||
// 题图:有图放真图,没图放占位(网页版是 🧳)
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
if (t.coverKey !== '') {
|
||||
Image(fbImageSrc(t.coverKey, FB_TOKEN_STORE.get()))
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.objectFit(ImageFit.Cover)
|
||||
// ⚠️ 不给 alt 图标:旅程封面是实景照,失败时给纯色底比给个
|
||||
// "行李箱图标"更不刺眼(网页版失败时也是底色调)
|
||||
.onError((): void => {
|
||||
this.markCoverFailed(t.id);
|
||||
})
|
||||
}
|
||||
// 有图但加载失败时,也别留一块透明 —— 统一给底色 + 图标
|
||||
if (t.coverKey === '' || this.coverFailed(t.id)) {
|
||||
Column({ space: 4 }) {
|
||||
Image($r('app.media.fb_ic_trips'))
|
||||
.width(22)
|
||||
.height(22)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.colorFilter(fbTint(FbTint.faint))
|
||||
}
|
||||
// ---- 题图:占满整宽,16:9(与上传槽位比例一致)----
|
||||
Stack({ alignContent: Alignment.Center }) {
|
||||
if (t.coverKey !== '' && !this.coverFailed(t.id)) {
|
||||
Image(fbImageSrc(t.coverKey, FB_TOKEN_STORE.get()))
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.backgroundColor('#F1E7D6')
|
||||
.objectFit(ImageFit.Cover)
|
||||
// ⚠️ 不给 alt 图标:旅程封面是实景照,失败时给纯色底比给个
|
||||
// "行李箱图标"更不刺眼(网页版失败时也是底色调)
|
||||
.onError((): void => {
|
||||
this.markCoverFailed(t.id);
|
||||
})
|
||||
}
|
||||
// 没图 / 图坏了:统一给底色 + 图标(不能留一块透明)
|
||||
if (t.coverKey === '' || this.coverFailed(t.id)) {
|
||||
Column({ space: 6 }) {
|
||||
Image($r('app.media.fb_ic_trips'))
|
||||
.width(30)
|
||||
.height(30)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.colorFilter(fbTint('#C9B8A4'))
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.backgroundColor('#F1E7D6')
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.aspectRatio(16 / 9)
|
||||
.clip(true)
|
||||
|
||||
// ---- 图下面的信息区(挤占问题消失,能放的都放进来)----
|
||||
Column({ space: 6 }) {
|
||||
// 标题
|
||||
Text(t.title)
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
|
||||
// 时间 · 城市(一行放得下了 —— 整宽之后不再被小图挤)
|
||||
Row({ space: 10 }) {
|
||||
if (t.dateRange !== '') {
|
||||
Text(t.dateRange)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
}
|
||||
if (t.city !== '') {
|
||||
Text('📍 ' + t.city)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
} else {
|
||||
Column().layoutWeight(1)
|
||||
}
|
||||
}
|
||||
.width(96)
|
||||
.height(72)
|
||||
.borderRadius(FbMetric.radiusS)
|
||||
.clip(true)
|
||||
.flexShrink(0)
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 右侧:标题 / meta / 简介
|
||||
Column({ space: 4 }) {
|
||||
Text(t.title)
|
||||
.fontSize(FbMetric.fontTitle)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.maxLines(2)
|
||||
// 段数 / 已完成 / 同行人数
|
||||
Row({ space: 8 }) {
|
||||
Text(t.nodeCount.toString() + ' 段行程')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(t.nodeCount > 0 ? this.accent : FbColors.faint)
|
||||
if (t.doneCount > 0) {
|
||||
Text('已完成 ' + t.doneCount.toString())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
if (t.attendeeCount > 0) {
|
||||
Text('· ' + t.attendeeCount.toString() + ' 人同行')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
// 简介:整宽之后可以给到 3 行(原来挤在右边只有 2 行)
|
||||
if (t.summary !== '') {
|
||||
Text(t.summary)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(3)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
|
||||
// meta 一行:时间 · 📍城市 · N 段行程
|
||||
Row({ space: 10 }) {
|
||||
if (t.dateRange !== '') {
|
||||
Text(t.dateRange)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
}
|
||||
if (t.city !== '') {
|
||||
Text('📍 ' + t.city)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
.layoutWeight(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
} else {
|
||||
Column().layoutWeight(1)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
// 段数 + 同行人数(网页版是一排 meta,这里小屏分两行更清楚)
|
||||
Row({ space: 8 }) {
|
||||
Text(t.nodeCount.toString() + ' 段行程')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(t.nodeCount > 0 ? this.accent : FbColors.faint)
|
||||
if (t.doneCount > 0) {
|
||||
Text('已完成 ' + t.doneCount.toString())
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
if (t.attendeeCount > 0) {
|
||||
Text('· ' + t.attendeeCount.toString() + ' 人同行')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
// 简介:网页版截 80 字,这里两行截断(手机一行更窄)
|
||||
if (t.summary !== '') {
|
||||
Text(t.summary)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(2)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
.lineHeight(17)
|
||||
}
|
||||
.lineHeight(18)
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.layoutWeight(1)
|
||||
.flexShrink(1)
|
||||
|
||||
// 卡尾:进详情 + 编辑
|
||||
Row({ space: 12 }) {
|
||||
/* 🔴 这颗「看行程」是**主要入口**,必须自己带 onClick(v66 真机踩过:
|
||||
屏幕上写着"查看行程"却是纯文字,点它什么都不发生)。 */
|
||||
Text('看行程')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(this.accent)
|
||||
.padding({ left: 6, right: 12, top: 8, bottom: 6 })
|
||||
.onClick((): void => {
|
||||
this.openDetail(t);
|
||||
})
|
||||
Column().layoutWeight(1)
|
||||
Text('编辑')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick((): void => {
|
||||
this.openEdit(t);
|
||||
})
|
||||
Image($r('app.media.fb_ic_forward'))
|
||||
.width(12)
|
||||
.height(12)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.colorFilter(fbTint(FbTint.faint))
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Center)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.alignItems(VerticalAlign.Top)
|
||||
.onClick((): void => {
|
||||
// 点卡片主体 = 展开/收起行程(网页版是"进详情",手机上一屏放不下,
|
||||
// 就地展开更顺手;编辑走下面那颗小按钮)
|
||||
this.toggleExpand(t);
|
||||
})
|
||||
|
||||
// ---- 卡尾操作条:编辑(网页版进详情页才有编辑,这里给个显式入口)----
|
||||
Row({ space: 14 }) {
|
||||
/* 🔴 这颗原来只是「字」,没有 `onClick` —— 屏幕上写着"查看行程"却点不动。
|
||||
现在接上 `toggleExpand`(与卡片主体同一份逻辑),并加内边距把热区做大
|
||||
(字号只有 fontTiny,纯文字热区太小,真机上不好点)。 */
|
||||
Text(this.isExpanded(t.id) ? '收起行程' : '查看行程')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
.padding({ left: 6, right: 10, top: 8, bottom: 8 })
|
||||
.onClick((): void => {
|
||||
this.toggleExpand(t);
|
||||
})
|
||||
Column().layoutWeight(1)
|
||||
Text('编辑')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
|
||||
.backgroundColor(FbColors.inputBg)
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.onClick((): void => {
|
||||
this.openEdit(t);
|
||||
})
|
||||
Image($r('app.media.fb_ic_forward'))
|
||||
.width(12)
|
||||
.height(12)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.colorFilter(fbTint(FbTint.faint))
|
||||
.rotate({ angle: this.isExpanded(t.id) ? 90 : 0 })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: 12 })
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// ---- 展开区:行程时间轴 ----
|
||||
if (this.isExpanded(t.id)) {
|
||||
Divider()
|
||||
.color(FbColors.line2)
|
||||
.margin({ left: FbMetric.cardPad, right: FbMetric.cardPad })
|
||||
|
||||
if (this.nodesLoading) {
|
||||
Row({ space: 8 }) {
|
||||
LoadingProgress()
|
||||
.width(16)
|
||||
.height(16)
|
||||
.color(this.accent)
|
||||
Text('正在取行程…')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(FbMetric.cardPad)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
} else if (this.nodesNote !== '') {
|
||||
/* 拉失败时**说实话**,不假装"还没排行程" ——
|
||||
后者会让用户以为自己的行程丢了。 */
|
||||
Column({ space: 6 }) {
|
||||
Text(this.nodesNote)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.muted)
|
||||
Text('点这里重试')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
.onClick((): void => {
|
||||
this.loadNodes(t.id);
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.padding(FbMetric.cardPad)
|
||||
} else if (this.nodes.length === 0) {
|
||||
Column({ space: 8 }) {
|
||||
Text('这个旅程还没有排行程')
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontColor(FbColors.faint)
|
||||
.width('100%')
|
||||
this.addNodeBtn(t.id)
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.padding(FbMetric.cardPad)
|
||||
} else {
|
||||
Column({ space: 0 }) {
|
||||
// 分两段列:「实际」(已发生的)在前,「计划」在后 —— 与 Web 端顺序一致
|
||||
this.stageBlock('实际', 'actual')
|
||||
this.stageBlock('计划', 'plan')
|
||||
// 已有节点时,加号排在最后(像网页版时间轴的末尾)
|
||||
this.addNodeBtn(t.id)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: FbMetric.cardPad, right: FbMetric.cardPad, bottom: FbMetric.cardPad })
|
||||
}
|
||||
}
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
.width('100%')
|
||||
.backgroundColor(FbColors.surface)
|
||||
.borderRadius(FbMetric.radius)
|
||||
.clip(true)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.onClick((): void => {
|
||||
/* 整卡可点(比只点那行小字好按得多)。⚠️ 卡里的「编辑」自己带 onClick,
|
||||
点它会冒泡到这里吗?ArkUI 的 onClick 不冒泡到父容器(子组件消费掉),
|
||||
所以「编辑」不会误进详情 —— 但两者的热区都在卡内,别把编辑挪出去。 */
|
||||
this.openDetail(t);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入旅程详情页(**整页**,不是就地展开)。
|
||||
*
|
||||
* 🔴 必须**同时**置两个状态:
|
||||
* `tripDetailId` —— 看哪一趟;
|
||||
* `activeModule = '__trip_detail'` —— 走哪条分发。
|
||||
* 只置前者 ⇒ 画面不动(还停在列表);只置后者 ⇒ 打开上一次看过的那趟。
|
||||
* ⚠️ 用 `switchModule()` 而不是直接赋 `activeModule`:它会把"当前页"压进
|
||||
* `moduleHistory`,返回手势才回得来(`Index.onBackPress` 靠它)。
|
||||
*/
|
||||
private openDetail(t: FbTripItem): void {
|
||||
this.app.tripDetailId = t.id;
|
||||
this.app.switchModule('__trip_detail');
|
||||
}
|
||||
|
||||
/** 打开编辑浮层(预填当前值) */
|
||||
@@ -652,76 +560,6 @@ export struct FbTripsPage {
|
||||
this.newOpen = true;
|
||||
}
|
||||
|
||||
/** 展开区底部那颗「+ 加一段行程」 */
|
||||
@Builder
|
||||
addNodeBtn(tripId: number) {
|
||||
Row({ space: 6 }) {
|
||||
Image($r('app.media.fb_ic_plus'))
|
||||
.width(13)
|
||||
.height(13)
|
||||
.renderMode(ImageRenderMode.Template)
|
||||
.colorFilter(fbTint(this.accent))
|
||||
Text('加一段行程')
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(this.accent)
|
||||
}
|
||||
.padding({ left: 10, right: 12, top: 7, bottom: 7 })
|
||||
.backgroundColor(fbAlpha(this.accent, 0.08))
|
||||
.borderRadius(FbMetric.radiusPill)
|
||||
.alignItems(VerticalAlign.Center)
|
||||
.margin({ top: 6 })
|
||||
.onClick((): void => {
|
||||
this.nodeSheetTripId = tripId;
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉某趟旅程的行程节点。
|
||||
*
|
||||
* 🔴 v65:这是端上原先**最大**的缺口 —— `nodes` 一直是空数组,
|
||||
* 所以每趟旅程展开后永远显示"还没有排行程"。
|
||||
* 用户原话:「现在这个旅程下面也没有这个行程啊」。
|
||||
*
|
||||
* ⚠️ 三个状态要分清(否则用户会以为数据丢了):
|
||||
* ① 正在拉 → 转圈;② 拉失败 → 明说"没取到" + 可重试;
|
||||
* ③ 拉成功但确实没有 → 才说"还没排行程"。
|
||||
*/
|
||||
private async loadNodes(tripId: number): Promise<void> {
|
||||
if (this.nodesLoading) {
|
||||
return;
|
||||
}
|
||||
this.nodesLoading = true;
|
||||
this.nodesNote = '';
|
||||
this.nodesTripId = tripId;
|
||||
const rows: FbTripNode[] = await fetchTripNodes(FB_TOKEN_STORE.get(), tripId);
|
||||
this.nodesLoading = false;
|
||||
/* ⚠️ 用户可能在等待期间又点了别的旅程 ⇒ 只认"当前还展开着的那一趟",
|
||||
否则 A 的节点会画到 B 的展开区里。 */
|
||||
if (this.expandedId !== tripId) {
|
||||
return;
|
||||
}
|
||||
this.nodes = rows;
|
||||
/* 拉回来是空数组有两种可能:真的没有 / 请求失败。`fetchTripNodes` 失败也回 [],
|
||||
这里无法区分 ⇒ 由下面 `node_count` 兜一道:
|
||||
列表里说有 N 段、这里却是 0 ⇒ 那就是没取到,要明说。 */
|
||||
if (rows.length === 0) {
|
||||
const cur: FbTripItem | null = this.tripById(tripId);
|
||||
if (cur !== null && cur.nodeCount > 0) {
|
||||
this.nodesNote = '行程没取到(网络或权限问题),列表里显示有 '
|
||||
+ cur.nodeCount.toString() + ' 段。';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private tripById(id: number): FbTripItem | null {
|
||||
for (const t of this.trips) {
|
||||
if (t.id === id) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private markCoverFailed(id: number): void {
|
||||
if (this.coverFailedIds.indexOf(id) < 0) {
|
||||
this.coverFailedIds = this.coverFailedIds.concat([id]);
|
||||
@@ -732,116 +570,4 @@ export struct FbTripsPage {
|
||||
return this.coverFailedIds.indexOf(id) >= 0;
|
||||
}
|
||||
|
||||
/** 一段阶段块(实际 / 计划)+ 它下面的节点时间轴 */
|
||||
@Builder
|
||||
stageBlock(title: string, stage: string) {
|
||||
if (this.nodesOf(stage).length > 0) {
|
||||
Column({ space: 0 }) {
|
||||
Row({ space: 6 }) {
|
||||
FbTag({
|
||||
text: title,
|
||||
tone: stage === 'actual' ? 'green' : 'blue'
|
||||
})
|
||||
Text(this.nodesOf(stage).length.toString() + ' 段')
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ top: 10, bottom: 6 })
|
||||
|
||||
ForEach(this.nodesOf(stage), (n: FbTripNode) => {
|
||||
this.nodeRow(n)
|
||||
}, (n: FbTripNode): string => 'tn_' + stage + '_' + n.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
}
|
||||
}
|
||||
|
||||
/** 一个行程节点:左侧圆点 + 内容块,内容块的左边框连成时间轴竖线 */
|
||||
@Builder
|
||||
nodeRow(n: FbTripNode) {
|
||||
Row({ space: 8 }) {
|
||||
// 圆点:顶对齐,与标题首行齐平
|
||||
FbDot({
|
||||
dia: 9,
|
||||
fillC: n.status === 'done' ? this.accent : '#A8988A'
|
||||
})
|
||||
.margin({ top: 5 })
|
||||
|
||||
// 内容块:左边框 = 时间轴的竖线(节点之间无间距,边框首尾相接成为连续线)
|
||||
Column({ space: 4 }) {
|
||||
Row({ space: 8 }) {
|
||||
Text(n.title)
|
||||
.fontSize(FbMetric.fontSmall)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor(FbColors.ink)
|
||||
.layoutWeight(1)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(n.when)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.faint)
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Text(n.place)
|
||||
.fontSize(FbMetric.fontTiny)
|
||||
.fontColor(FbColors.muted)
|
||||
.maxLines(1)
|
||||
.textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
.width('100%')
|
||||
|
||||
Row({ space: 6 }) {
|
||||
if (n.mode.length > 0) {
|
||||
FbTag({ text: n.mode, tone: 'blue' })
|
||||
}
|
||||
if (n.status === 'done') {
|
||||
FbTag({ text: '已完成', tone: 'green' })
|
||||
} else if (n.status === 'skip') {
|
||||
FbTag({ text: '跳过', tone: 'muted' })
|
||||
} else {
|
||||
FbTag({ text: '待办', tone: 'muted' })
|
||||
}
|
||||
if (n.expenseCent > 0) {
|
||||
Text(fbMoney(n.expenseCent))
|
||||
.fontSize(10)
|
||||
.fontColor(FbColors.muted)
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
}
|
||||
.layoutWeight(1)
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.padding({ left: 12, bottom: 16 })
|
||||
.border({ width: { left: 1 }, color: FbColors.line })
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(VerticalAlign.Top)
|
||||
}
|
||||
|
||||
private isExpanded(id: number): boolean {
|
||||
return this.expandedId === id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开 / 收起某个旅程的行程时间轴。
|
||||
*
|
||||
* 🔴 v66 真机验收补漏(2026-10-20):原来这段逻辑**只写在卡片主体的 onClick 里**,
|
||||
* 而卡尾那颗「查看行程 / 收起行程」是一颗**没有任何 onClick 的纯 Text**
|
||||
* ⇒ 屏幕上明明写着「查看行程」,点它却什么都不发生(真机验收 C2/C3/C4 就是这么红的)。
|
||||
* 这类"看得见、点不动"的入口是本项目的红线,必须两处都能点。
|
||||
* ⇒ 抽成一个方法,卡片主体与卡尾文字共用,行为不会漂。
|
||||
*/
|
||||
private toggleExpand(trip: FbTripItem): void {
|
||||
const was: boolean = this.isExpanded(trip.id);
|
||||
this.expandedId = was ? 0 : trip.id;
|
||||
if (!was) {
|
||||
this.loadNodes(trip.id);
|
||||
}
|
||||
}
|
||||
|
||||
private nodesOf(stage: string): FbTripNode[] {
|
||||
return this.nodes.filter((n: FbTripNode): boolean => n.stage === stage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user