日程详情卡:补齐行程的出发地/目的地 (#69)

用户 2026-11-05:「点击行程里旅行日历本里的信息时,弹出来的那个框是在首页上
弹出的,还是没有出发地和目的地。」

根因链(三段,缺一段都不显示):

1. FbCalEventFull 没有那四个字段
   #67 只给了 FbCalEvent(列表用),详情卡消费的是 FbCalEventFull。

2. shellOf() 只塞了 location: b.place
   行程节点的 place 是空串 —— 它的位置概念是"从哪到哪",没有单一地点。

3. 最关键:行程条目**永远走 shell,压根不请求详情接口**
   openEvent() 里 `if (!isEditableEvent(e)) { shellOf(e); return; }`,
   行程的 kind 是 trip 不是 custom ⇒ 直接短路。
   而且即使去请求也没意义:Calendar::event($id) 查的是 calendar_events,
   行程节点在 trip_nodes 表,那个 id 查不到东西。
   ⇒ 位置信息**只能**靠 shell 带过去。

改动:

- FbCalEventFull 加 fromPlace/toPlace/fromLng/fromLat/toLng/toLat
- FbCalRepo.toFull() 从接口读(接口给就填,普通日程取到空串)
- 首页 FbHomePage.shellOf()、日程页 FbCalendarPage.shellOf() 两处传递
  (两处是独立实现,只改一处会漏)
- FbEventSheet:
  · navRow 改 navRowRaw(label, place, navTarget) —— 标签不再硬编码「地点」
  · onNavigate 从无参改成 onNavigate(navTarget)
    原来宿主只能读 event.location 猜导航目标,无法区分出发地/目的地
  · 渲染三分支:有 fromPlace 画「出发地」、有 toPlace 画「目的地」、
    有 location 画「地点」(三者互斥:行程条目只有前两个,普通日程只有第三个)
  · 新增 navTargetOf(place, lng, lat):有坐标返回 `lat,lng?地名`
    (同名地点不会搜错城市),没有就只给地名让 FbNav 地理编码
- 首页 navigateEvent / 日程页 navigateDetail 改成接受 navTarget,
  非空直接用,为空退回原来那套推导(底部那颗「导航」按钮传空串)
This commit is contained in:
2026-11-05 16:21:16 +08:00
parent f30ea5b1e4
commit ad9b934760
4 changed files with 137 additions and 17 deletions
+55 -6
View File
@@ -56,7 +56,16 @@ export struct FbEventSheet {
* undefined。宿主页本来就有可靠的上下文来源,且"要不要拉地图"属于导航决策 * undefined。宿主页本来就有可靠的上下文来源,且"要不要拉地图"属于导航决策
* (跟"当前在哪个模块"有关),不是这张卡的职责。 * (跟"当前在哪个模块"有关),不是这张卡的职责。
*/ */
@Event onNavigate: () => void = (): void => { /**
* 点某一行位置信息去导航。
*
* 🔴 #69(用户 2026-11-05):原来是**无参**的 —— 宿主只能自己猜"该导航到哪",
* 实际上它读的是 `event.location`。而行程条目根本没有 location
* (服务端 trip 段不输出那个字段),于是详情卡里既没有位置信息,
* 就算硬加一行也没法指定导航到出发地还是目的地。
* ⇒ 改成带 `navTarget`:点哪一行就把那一行的地名/坐标交出去。
*/
@Event onNavigate: (navTarget: string) => void = (navTarget: string): void => {
}; };
@Event onClose: () => void = (): void => { @Event onClose: () => void = (): void => {
}; };
@@ -234,10 +243,27 @@ export struct FbEventSheet {
* 重建),不存在"数据变了行不刷新"的场景,所以用 Builder 是安全的; * 重建),不存在"数据变了行不刷新"的场景,所以用 Builder 是安全的;
* 但仍保持**单参数**,把风险面压到最小。 * 但仍保持**单参数**,把风险面压到最小。
*/ */
/**
* 算出交给 `onNavigate` 的导航目标(#69)。
*
* 有坐标就带坐标:`lat,lng?地名` —— 同名地点不会搜错城市
* (旅程模块踩过:全国搜「珠江路1018号」搜到了江苏南通,差 500 公里)。
* 没有坐标就只给地名,让 `FbNav` 自己走系统地理编码。
*
* ⚠️ 这是个**纯函数**(不读 this),所以放在 Builder 外面,
* 不会引入"@Builder 里调方法导致不刷新"的问题。
*/
private navTargetOf(place: string, lng: string, lat: string): string {
if (lng !== '' && lat !== '') {
return lat + ',' + lng + '?' + place;
}
return place;
}
@Builder @Builder
navRow(place: string) { navRowRaw(label: string, place: string, navTarget: string) {
Row({ space: 10 }) { Row({ space: 10 }) {
Text('地点') Text(label)
.fontSize(FbMetric.fontTiny) .fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint) .fontColor(FbColors.faint)
.width(56) .width(56)
@@ -271,7 +297,7 @@ export struct FbEventSheet {
.padding({ top: 8, bottom: 8 }) .padding({ top: 8, bottom: 8 })
.alignItems(VerticalAlign.Center) .alignItems(VerticalAlign.Center)
.onClick((): void => { .onClick((): void => {
this.onNavigate(); this.onNavigate(navTarget);
}) })
} }
@@ -359,6 +385,26 @@ export struct FbEventSheet {
if (this.remindText() !== '') { if (this.remindText() !== '') {
this.row('提醒', this.remindText()) this.row('提醒', this.remindText())
} }
/* 🔴 #69(用户 2026-11-05):「点击行程里旅行日历本里的信息时,
弹出来的那个框…还是没有出发地和目的地。」
⇒ 行程派生条目(kind='trip')服务端**不给 `location`** ——
行程没有单一"地点"概念,有的是"从哪到哪"。
所以这里必须按 fromPlace / toPlace 分开画两行。
三者互斥地覆盖所有情况:
· 行程条目 → 出发地 + 目的地
· 普通日程 → 地点
不会同时出现(行程的 location 恒为空串,
普通日程的 fromPlace/toPlace 恒为空串)。 */
if (this.event.fromPlace !== '') {
this.navRowRaw('出发地', this.event.fromPlace,
this.navTargetOf(this.event.fromPlace, this.event.fromLng, this.event.fromLat))
}
if (this.event.toPlace !== '') {
this.navRowRaw('目的地', this.event.toPlace,
this.navTargetOf(this.event.toPlace, this.event.toLng, this.event.toLat))
}
if (this.event.location !== '') { if (this.event.location !== '') {
/* 🔴 v73(用户 2026-10-21「有地址的还可以直接一键导航」): /* 🔴 v73(用户 2026-10-21「有地址的还可以直接一键导航」):
地点不再是纯文字 —— **整行可点**,右侧带一颗「导航」小标签。 地点不再是纯文字 —— **整行可点**,右侧带一颗「导航」小标签。
@@ -368,7 +414,8 @@ export struct FbEventSheet {
⚠️ 但**视觉上**仍要有那颗「导航」标签 —— 只有"整行可点" ⚠️ 但**视觉上**仍要有那颗「导航」标签 —— 只有"整行可点"
而没有任何标记的话,用户根本不知道地点能点 而没有任何标记的话,用户根本不知道地点能点
(本项目踩过:卡尾"查看行程"是纯文本,用户以为按钮坏了)。 */ (本项目踩过:卡尾"查看行程"是纯文本,用户以为按钮坏了)。 */
this.navRow(this.event.location) this.navRowRaw('地点', this.event.location,
this.navTargetOf(this.event.location, '', ''))
} }
if (this.event.note !== '') { if (this.event.note !== '') {
this.row('备注', this.event.note) this.row('备注', this.event.note)
@@ -421,7 +468,9 @@ export struct FbEventSheet {
.borderRadius(FbMetric.radiusS) .borderRadius(FbMetric.radiusS)
.border({ width: 1, color: this.accentAlpha('33') }) .border({ width: 1, color: this.accentAlpha('33') })
.onClick((): void => { .onClick((): void => {
this.onNavigate(); /* #69:底部这颗按钮不带具体行 —— 交给宿主自己挑一个
(传空串 = "按老规则推导":location 优先) */
this.onNavigate('');
}) })
} }
if (this.canEdit) { if (this.canEdit) {
+28 -1
View File
@@ -42,6 +42,25 @@ export interface FbCalEventFull {
transp: boolean; transp: boolean;
/** 创建者 person_id */ /** 创建者 person_id */
created_by: number; created_by: number;
/**
* 出发地 / 目的地(#69)。
*
* 🔴 用户 2026-11-05:「点击行程里旅行日历本里的信息时,弹出来的那个框…
* 还是没有出发地和目的地。」
*
* 行程派生条目(kind='trip')服务端**不输出 `location`** ——
* 行程没有单一"地点"概念,它有的是"从哪到哪"。
* ⇒ 详情卡必须靠这几个字段才能显示,不能只读 `location`。
*
* 普通日程一律空串。
*/
fromPlace: string;
toPlace: string;
/** 坐标(导航用;老数据可能为空) */
fromLng: string;
fromLat: string;
toLng: string;
toLat: string;
} }
/** 写日程时要发的字段(全部可选 —— 只传变化的) */ /** 写日程时要发的字段(全部可选 —— 只传变化的) */
@@ -226,7 +245,15 @@ export class FbCalRepo {
reminder_minutes: this.remStr(o, 'reminder_minutes'), reminder_minutes: this.remStr(o, 'reminder_minutes'),
status: this.str(o, 'status') !== '' ? this.str(o, 'status') : 'confirmed', status: this.str(o, 'status') !== '' ? this.str(o, 'status') : 'confirmed',
transp: o['transp'] === true || o['transp'] === 1, transp: o['transp'] === true || o['transp'] === 1,
created_by: this.num(o, 'created_by') created_by: this.num(o, 'created_by'),
/* #69:行程条目的「从哪到哪」。接口里只有行程派生条目才给这几个字段,
普通日程取到的是默认空串。 */
fromPlace: this.str(o, 'from_place'),
toPlace: this.str(o, 'to_place'),
fromLng: this.str(o, 'from_lng'),
fromLat: this.str(o, 'from_lat'),
toLng: this.str(o, 'to_lng'),
toLat: this.str(o, 'to_lat')
}; };
return e; return e;
} }
@@ -1022,13 +1022,17 @@ export struct FbCalendarPage {
* 浮层那边对空串就是"不显示这一行",不会画出假信息。 * 浮层那边对空串就是"不显示这一行",不会画出假信息。
* (宁可少一行,也不要说"不重复" —— 那是错的。) * (宁可少一行,也不要说"不重复" —— 那是错的。)
*/ */
/** #69:与首页 `FbHomePage.shellOf()` 同一套(两处独立实现,改一处容易漏)。 */
private shellOf(b: FbCalEvent): FbCalEventFull { private shellOf(b: FbCalEvent): FbCalEventFull {
const shell: FbCalEventFull = { const shell: FbCalEventFull = {
id: b.id, calendar_id: b.calId, title: b.title, location: b.place, id: b.id, calendar_id: b.calId, title: b.title, location: b.place,
starts_at: b.date + ' ' + (b.allDay ? '00:00:00' : (b.time + ':00')), starts_at: b.date + ' ' + (b.allDay ? '00:00:00' : (b.time + ':00')),
ends_at: b.endTime !== '' ? (b.date + ' ' + b.endTime + ':00') : '', ends_at: b.endTime !== '' ? (b.date + ' ' + b.endTime + ':00') : '',
all_day: b.allDay, note: b.note, rrule: '', reminder_minutes: '', all_day: b.allDay, note: b.note, rrule: '', reminder_minutes: '',
status: 'confirmed', transp: false, created_by: 0 status: 'confirmed', transp: false, created_by: 0,
/* #69:与首页 shellOf 同一套(两处独立实现,容易只改一处) */
fromPlace: b.fromPlace, toPlace: b.toPlace,
fromLng: b.fromLng, fromLat: b.fromLat, toLng: b.toLng, toLat: b.toLat
}; };
return shell; return shell;
} }
@@ -1073,10 +1077,19 @@ export struct FbCalendarPage {
* ⚠️ 拿 `UIAbilityContext`(`startAbility` 只在这个上下文上有, * ⚠️ 拿 `UIAbilityContext`(`startAbility` 只在这个上下文上有,
* 用 `common.Context` 编译就报错 —— 名册拨号那次实测过)。 * 用 `common.Context` 编译就报错 —— 名册拨号那次实测过)。
*/ */
private navigateDetail(): void { /**
* 去导航(#69:改成接受 `navTarget`)。
*
* 原来无参、只读 `detailFull.location`。行程条目没有 location,
* 且详情卡现在有「出发地」「目的地」两行,要能区分点的是哪一行。
*/
private navigateDetail(navTarget: string): void {
const placeOfFull: string = this.detailFull !== null ? this.detailFull.location : ''; const placeOfFull: string = this.detailFull !== null ? this.detailFull.location : '';
const placeOfBase: string = this.detailBase !== null ? this.detailBase.place : ''; const placeOfBase: string = this.detailBase !== null ? this.detailBase.place : '';
const place: string = placeOfFull !== '' ? placeOfFull : placeOfBase; /* 优先用详情卡传来的目标;为空才退回原来那套推导 */
const place: string = navTarget !== ''
? navTarget
: (placeOfFull !== '' ? placeOfFull : placeOfBase);
const title: string = this.detailFull !== null ? this.detailFull.title const title: string = this.detailFull !== null ? this.detailFull.title
: (this.detailBase !== null ? this.detailBase.title : ''); : (this.detailBase !== null ? this.detailBase.title : '');
const ctx: common.UIAbilityContext | undefined = const ctx: common.UIAbilityContext | undefined =
@@ -1226,8 +1239,8 @@ export struct FbCalendarPage {
用户改完点保存才报「这本日历你没有写权限」。 用户改完点保存才报「这本日历你没有写权限」。
与首页 `FbHomePage` 的 canEdit 保持同一口径。 */ 与首页 `FbHomePage` 的 canEdit 保持同一口径。 */
canEdit: this.detailBase !== null && isEventWritable(this.detailBase, this.books), canEdit: this.detailBase !== null && isEventWritable(this.detailBase, this.books),
onNavigate: (): void => { onNavigate: (navTarget: string): void => {
this.navigateDetail(); this.navigateDetail(navTarget);
}, },
onClose: (): void => { onClose: (): void => {
this.closeDetail(); this.closeDetail();
+36 -5
View File
@@ -513,13 +513,34 @@ export struct FbHomePage {
} }
/** 列表那条 → 完整字段的壳(只垫列表**已知**的字段,别编) */ /** 列表那条 → 完整字段的壳(只垫列表**已知**的字段,别编) */
/**
* 把列表条目垫成一份"详情壳"。
*
* 🔴 #69(用户 2026-11-05):「点击行程里旅行日历本里的信息时,弹出来的那个框…
* 还是没有出发地和目的地。」
*
* 为什么这个壳很重要:`openEvent()` 里
* `if (!isEditableEvent(e)) { this.evtFull = this.shellOf(e); return; }`
* —— 行程条目的 `kind` 是 `'trip'` 不是 `'custom'`,
* **永远走这条捷径、压根不请求详情接口**(而且也没有意义:
* 行程节点的数据在 `trip_nodes` 表里,`Calendar::event($id)` 查的是
* `calendar_events`,对节点 id 必然查不到)。
*
* ⇒ 位置信息**只能**靠这个壳带过去。原来只塞了 `location: b.place`,
* 而行程节点的 `place` 是空串(它的位置概念是"从哪到哪"),
* 于是详情卡里一行位置信息都没有。
*/
private shellOf(b: FbCalEvent): FbCalEventFull { private shellOf(b: FbCalEvent): FbCalEventFull {
const shell: FbCalEventFull = { const shell: FbCalEventFull = {
id: b.id, calendar_id: b.calId, title: b.title, location: b.place, id: b.id, calendar_id: b.calId, title: b.title, location: b.place,
starts_at: b.date + ' ' + (b.allDay ? '00:00:00' : (b.time + ':00')), starts_at: b.date + ' ' + (b.allDay ? '00:00:00' : (b.time + ':00')),
ends_at: b.endTime !== '' ? (b.date + ' ' + b.endTime + ':00') : '', ends_at: b.endTime !== '' ? (b.date + ' ' + b.endTime + ':00') : '',
all_day: b.allDay, note: b.note, rrule: '', reminder_minutes: '', all_day: b.allDay, note: b.note, rrule: '', reminder_minutes: '',
status: 'confirmed', transp: false, created_by: 0 status: 'confirmed', transp: false, created_by: 0,
/* #69:行程条目的「从哪到哪」—— 服务端不给 location,
不给这几个字段的话详情卡里就没有任何位置信息 */
fromPlace: b.fromPlace, toPlace: b.toPlace,
fromLng: b.fromLng, fromLat: b.fromLat, toLng: b.toLng, toLat: b.toLat
}; };
return shell; return shell;
} }
@@ -533,10 +554,20 @@ export struct FbHomePage {
} }
/** 详情卡上点「导航」/ 点地点行 → 拉起地图 */ /** 详情卡上点「导航」/ 点地点行 → 拉起地图 */
private navigateEvent(): void { /**
* 去导航(#69:改成接受 `navTarget`)。
*
* 原来是无参的 —— 只读 `evtFull.location`。行程条目没有 location,
* 且详情卡里现在有「出发地」「目的地」两行,得知道点的是哪一行。
* ⇒ 详情卡把那一行的地名(带坐标时是 `lat,lng?地名`)直接交进来。
*/
private navigateEvent(navTarget: string): void {
const placeOfFull: string = this.evtFull !== null ? this.evtFull.location : ''; const placeOfFull: string = this.evtFull !== null ? this.evtFull.location : '';
const placeOfBase: string = this.evtBase !== null ? this.evtBase.place : ''; const placeOfBase: string = this.evtBase !== null ? this.evtBase.place : '';
const place: string = placeOfFull !== '' ? placeOfFull : placeOfBase; /* 优先用详情卡传来的目标;它为空(老路径 / 极少数情况)才退回原来那套推导 */
const place: string = navTarget !== ''
? navTarget
: (placeOfFull !== '' ? placeOfFull : placeOfBase);
const title: string = this.evtFull !== null ? this.evtFull.title const title: string = this.evtFull !== null ? this.evtFull.title
: (this.evtBase !== null ? this.evtBase.title : ''); : (this.evtBase !== null ? this.evtBase.title : '');
/* ⚠️ 必须是 `UIAbilityContext`(`startAbility` 只在这个上下文上有; /* ⚠️ 必须是 `UIAbilityContext`(`startAbility` 只在这个上下文上有;
@@ -1846,8 +1877,8 @@ export struct FbHomePage {
v86 开编辑入口的决定本身没错(编辑就地改、不跳页), v86 开编辑入口的决定本身没错(编辑就地改、不跳页),
错在没把"没有写权限的本"排除掉 ⇒ 改为按 calWrite 判定。 */ 错在没把"没有写权限的本"排除掉 ⇒ 改为按 calWrite 判定。 */
canEdit: this.evtBase !== null && isEventWritable(this.evtBase, this.calBooks), canEdit: this.evtBase !== null && isEventWritable(this.evtBase, this.calBooks),
onNavigate: (): void => { onNavigate: (navTarget: string): void => {
this.navigateEvent(); this.navigateEvent(navTarget);
}, },
onClose: (): void => { onClose: (): void => {
this.closeEvent(); this.closeEvent();