日程行程条目端上同步:字段、列表卡「出发地→目的地」可导航 (#67)

同步服务端 #67 的新字段与三处改动:

数据层
- RawCalEvent / FbCalEvent 加 fromPlace/toPlace/fromLng/fromLat/toLng/toLat
- FbParse 解析赋值;新增 numStr() 把 DECIMAL 可能给的 number 转成字符串
  (`s()` 签名只收 string|null|undefined,坐标字段会报类型错)
- FbCalStore.toModels、DemoData 的 8 条演示数据同步补字段

渲染
- FbDayEventPanel 列表卡片:在时间行**上面**加一行「出发地 → 目的地」,
  只对两端都有值的条目画(普通日程 fromPlace/toPlace 都是空串,不画)。
  目的地用日历本色 + 下划线,点击 fbOpenNavigation:
  有坐标走 `lat,lng?地名`(同名地点不会搜错城市),没有就让 FbNav 地理编码。
  不塞进时间行是因为那里已有「时刻 + 重复徽标 + 地点」,窄屏会挤爆。
- FbDayTimeline 全天块:一行文字「出发地 → 目的地」,替代原来的 place。
  不给点击 —— 26vp 高的块切两个热区点不准,导航入口在详情卡。

图标与 ✓ 的改动在服务端(Trip::leadIcon / 删  ✓ ),端上只消费 title,
所以不需要额外处理。
This commit is contained in:
2026-11-05 16:12:14 +08:00
parent c498037ec8
commit f30ea5b1e4
6 changed files with 115 additions and 11 deletions
@@ -19,7 +19,9 @@
* (传 `events` 还有个好处:每次都是新数组引用 ⇒ 一定触发重建。) * (传 `events` 还有个好处:每次都是新数组引用 ⇒ 一定触发重建。)
*/ */
import common from '@ohos.app.ability.common';
import { FbColors, FbMetric } from '../common/Theme'; import { FbColors, FbMetric } from '../common/Theme';
import { fbOpenNavigation } from '../common/FbNav';
import { FbCalEvent } from '../model/Types'; import { FbCalEvent } from '../model/Types';
import { FbCalStyle } from '../data/FbCalSettings'; import { FbCalStyle } from '../data/FbCalSettings';
import { FbTimelineUtil } from '../common/FbTimeline'; import { FbTimelineUtil } from '../common/FbTimeline';
@@ -163,6 +165,45 @@ export struct FbDayEventPanel {
.textOverflow({ overflow: TextOverflow.Ellipsis }) .textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%') .width('100%')
/* 🔴 #67(用户 2026-11-05):「如果行程里面有地址,也就是有出发地和目的地,
应该像行程里显示的那样,把它显示在下面,这样一点就可以导航。」
⇒ 单独一行,放在时间行**上面**(先看到"从哪到哪",再看几点)。
只对行程条目画(两端都有值才画,缺一端就没意义)。
⚠️ 不塞进时间行:那里已经有「时刻 + 重复徽标 + 地点」,窄屏会挤爆。
颜色用 `ev.color`(该日历本的主题色)而不是写死 —— 用户可以在
「日历本设置」里改色,写死就不跟着变了。 */
if (ev.fromPlace !== '' && ev.toPlace !== '') {
Row({ space: 4 }) {
Text(ev.fromPlace)
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.muted)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.constraintSize({ maxWidth: '42%' })
Text('→')
.fontSize(FbMetric.fontTiny)
.fontColor(FbColors.faint)
Text(ev.toPlace)
.fontSize(FbMetric.fontTiny)
.fontColor(ev.color)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.decoration({ type: TextDecorationType.Underline, color: ev.color })
}
.width('100%')
.onClick((): void => {
/* 有坐标就带坐标(同名地点不会搜错城市);没有就让 FbNav 自己地理编码 */
const hasPos: boolean = ev.toLng !== '' && ev.toLat !== '';
const target: string = hasPos
? (ev.toLat + ',' + ev.toLng + '?' + ev.toPlace)
: ev.toPlace;
fbOpenNavigation(getContext(this) as common.UIAbilityContext,
target, ev.toPlace);
})
}
Row({ space: 6 }) { Row({ space: 6 }) {
Text(ev.allDay ? '全天' Text(ev.allDay ? '全天'
: (ev.endTime !== '' ? (ev.time + ' - ' + ev.endTime) : ev.time)) : (ev.endTime !== '' ? (ev.time + ' - ' + ev.endTime) : ev.time))
@@ -422,7 +422,22 @@ export struct FbDayTimeline {
.maxLines(1) .maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis }) .textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1) .layoutWeight(1)
if (e.place !== '') { /* 🔴 #67(用户 2026-11-05):「如果行程里面有地址,也就是有出发地和目的地,
应该像行程里显示的那样,把它显示在下面。」
⇒ 行程条目优先画「出发地 → 目的地」,普通日程仍画自己的 place。
⚠️ 这里**只画文字不给点击**:全天块整块高 26vp,再切成两个热区
会小到点不准(而且整块已经绑了 `onPick` 去看详情)。
导航入口放在详情卡里 —— 那里空间够,也符合"先看再操作"。
⚠️ 两个地名都截断到单行:这一行宽度本来就紧张(左侧还有「全天」徽标)。 */
if (e.fromPlace !== '' && e.toPlace !== '') {
Text(e.fromPlace + ' → ' + e.toPlace)
.fontSize(10)
.fontColor('#D9FFFFFF')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.constraintSize({ maxWidth: 150 })
} else if (e.place !== '') {
Text(e.place) Text(e.place)
.fontSize(10) .fontSize(10)
.fontColor('#D9FFFFFF') .fontColor('#D9FFFFFF')
+8 -1
View File
@@ -277,7 +277,14 @@ export class FbCalStore {
allDay: r.num('all_day', 0) === 1, allDay: r.num('all_day', 0) === 1,
place: r.strOr('place', ''), place: r.strOr('place', ''),
note: r.strOr('note', ''), note: r.strOr('note', ''),
kind: r.strOr('kind', '') kind: r.strOr('kind', ''),
/* #67:行程条目的「从哪到哪」(普通日程一律空串,界面按空串不画) */
fromPlace: r.strOr('from_place', ''),
toPlace: r.strOr('to_place', ''),
fromLng: r.strOr('from_lng', ''),
fromLat: r.strOr('from_lat', ''),
toLng: r.strOr('to_lng', ''),
toLat: r.strOr('to_lat', '')
}; };
out.push(e); out.push(e);
} }
+25 -1
View File
@@ -32,6 +32,14 @@ export function s(v: string | null | undefined): string {
return v !== null && v !== undefined ? v : ''; return v !== null && v !== undefined ? v : '';
} }
/** 把 `string | number | null | undefined` 统一成字符串(#67:坐标字段可能是 number) */
function numStr(v: string | number | null | undefined): string {
if (v === null || v === undefined) {
return '';
}
return String(v);
}
/** 安全转数字 */ /** 安全转数字 */
export function n(v: number | null | undefined): number { export function n(v: number | null | undefined): number {
return v !== null && v !== undefined ? v : 0; return v !== null && v !== undefined ? v : 0;
@@ -111,6 +119,13 @@ interface RawCalEvent {
date?: string; time?: string; end_time?: string; all_day?: boolean | number; date?: string; time?: string; end_time?: string; all_day?: boolean | number;
kind?: string; ref_id?: number; location?: string; note?: string; kind?: string; ref_id?: number; location?: string; note?: string;
repeat?: string; occ?: string; repeat?: string; occ?: string;
/* 🔴 #67(用户 2026-11-05):「如果行程里面有地址,也就是有出发地和目的地,
应该像行程里显示的那样,把它显示在下面,这样一点就可以导航。」
⇒ 服务端 `Calendar::window()` 的 trip 派生段现在会带出这六个字段。
坐标单独带是因为导航要用:只给地名的话,同名地点会搜错城市。 */
from_place?: string; to_place?: string;
from_lng?: string | number; from_lat?: string | number;
to_lng?: string | number; to_lat?: string | number;
} }
interface RawTrip { interface RawTrip {
@@ -311,7 +326,16 @@ function parseCalRows(rows: RawCalEvent[]): FbCalEvent[] {
allDay: b(r.all_day), allDay: b(r.all_day),
place: s(r.location), place: s(r.location),
note: s(r.note), note: s(r.note),
kind: s(r.kind) kind: s(r.kind),
/* #67:行程条目才有值,普通日程是空串 */
/* ⚠️ 坐标字段服务端可能给 number(DECIMAL),`s()` 只收 string
⇒ 统一走 numStr() 转成字符串,避免类型报错 */
fromPlace: s(r.from_place),
toPlace: s(r.to_place),
fromLng: numStr(r.from_lng),
fromLat: numStr(r.from_lat),
toLng: numStr(r.to_lng),
toLat: numStr(r.to_lat)
}; };
out.push(e); out.push(e);
} }
+8 -8
View File
@@ -268,17 +268,17 @@ export const FB_DEMO_CAL_BOOKS: FbCalBook[] = [
]; ];
export const FB_DEMO_EVENTS_TODAY: FbCalEvent[] = [ export const FB_DEMO_EVENTS_TODAY: FbCalEvent[] = [
{ id: 1, title: '儿子数学兴趣班', date: '2026-10-11', time: '09:00', calName: '孩子的课表', color: '#4A7C82', allDay: false, place: '小区活动中心', calId: 0, endTime: '', note: '', kind: 'custom' }, { id: 1, title: '儿子数学兴趣班', date: '2026-10-11', time: '09:00', calName: '孩子的课表', color: '#4A7C82', allDay: false, place: '小区活动中心', calId: 0, endTime: '', note: '', kind: 'custom', fromPlace: '', toPlace: '', fromLng: '', fromLat: '', toLng: '', toLat: '' },
{ id: 2, title: '家庭大扫除', date: '2026-10-11', time: '10:30', calName: '家庭日程', color: '#B0763A', allDay: false, place: '家', calId: 0, endTime: '', note: '', kind: 'custom' }, { id: 2, title: '家庭大扫除', date: '2026-10-11', time: '10:30', calName: '家庭日程', color: '#B0763A', allDay: false, place: '家', calId: 0, endTime: '', note: '', kind: 'custom', fromPlace: '', toPlace: '', fromLng: '', fromLat: '', toLng: '', toLat: '' },
{ id: 3, title: '晚上看电影', date: '2026-10-11', time: '19:30', calName: '观影', color: '#7A4E8C', allDay: false, place: '家里', calId: 0, endTime: '', note: '', kind: 'custom' }, { id: 3, title: '晚上看电影', date: '2026-10-11', time: '19:30', calName: '观影', color: '#7A4E8C', allDay: false, place: '家里', calId: 0, endTime: '', note: '', kind: 'custom', fromPlace: '', toPlace: '', fromLng: '', fromLat: '', toLng: '', toLat: '' },
{ id: 4, title: '妈妈体检预约', date: '2026-10-11', time: '', calName: '家庭日程', color: '#B0763A', allDay: true, place: '市立医院', calId: 0, endTime: '', note: '', kind: 'custom' }, { id: 4, title: '妈妈体检预约', date: '2026-10-11', time: '', calName: '家庭日程', color: '#B0763A', allDay: true, place: '市立医院', calId: 0, endTime: '', note: '', kind: 'custom', fromPlace: '', toPlace: '', fromLng: '', fromLat: '', toLng: '', toLat: '' },
]; ];
export const FB_DEMO_EVENTS_SOON: FbCalEvent[] = [ export const FB_DEMO_EVENTS_SOON: FbCalEvent[] = [
{ id: 11, title: '女儿钢琴课', date: '10-12', time: '16:00', calName: '孩子的课表', color: '#4A7C82', allDay: false, place: '琴行', calId: 0, endTime: '', note: '', kind: 'custom' }, { id: 11, title: '女儿钢琴课', date: '10-12', time: '16:00', calName: '孩子的课表', color: '#4A7C82', allDay: false, place: '琴行', calId: 0, endTime: '', note: '', kind: 'custom', fromPlace: '', toPlace: '', fromLng: '', fromLat: '', toLng: '', toLat: '' },
{ id: 12, title: '结婚周年', date: '10-17', time: '', calName: '家庭日程', color: '#B0763A', allDay: true, place: '', calId: 0, endTime: '', note: '', kind: 'custom' }, { id: 12, title: '结婚周年', date: '10-17', time: '', calName: '家庭日程', color: '#B0763A', allDay: true, place: '', calId: 0, endTime: '', note: '', kind: 'custom', fromPlace: '', toPlace: '', fromLng: '', fromLat: '', toLng: '', toLat: '' },
{ id: 13, title: '周末露营', date: '10-18', time: '08:00', calName: '家庭日程', color: '#B0763A', allDay: false, place: '仰口', calId: 0, endTime: '', note: '', kind: 'custom' }, { id: 13, title: '周末露营', date: '10-18', time: '08:00', calName: '家庭日程', color: '#B0763A', allDay: false, place: '仰口', calId: 0, endTime: '', note: '', kind: 'custom', fromPlace: '', toPlace: '', fromLng: '', fromLat: '', toLng: '', toLat: '' },
{ id: 14, title: '家长会', date: '10-20', time: '18:30', calName: '孩子的课表', color: '#4A7C82', allDay: false, place: '学校', calId: 0, endTime: '', note: '', kind: 'custom' }, { id: 14, title: '家长会', date: '10-20', time: '18:30', calName: '孩子的课表', color: '#4A7C82', allDay: false, place: '学校', calId: 0, endTime: '', note: '', kind: 'custom', fromPlace: '', toPlace: '', fromLng: '', fromLat: '', toLng: '', toLat: '' },
]; ];
// ============================================================ // ============================================================
+17
View File
@@ -677,6 +677,23 @@ export interface FbCalEvent {
note: string; note: string;
/** 条目类型:`custom` = 用户自己的日程;其余是衍生条目(农历/节气/节假日/纪念日…) */ /** 条目类型:`custom` = 用户自己的日程;其余是衍生条目(农历/节气/节假日/纪念日…) */
kind: string; kind: string;
/**
* 出发地 / 目的地(#67)。
*
* 🔴 用户 2026-11-05:「如果行程里面有地址,也就是有出发地和目的地,
* 应该像行程里显示的那样,把它显示在下面,这样一点就可以导航。」
*
* 只有 `kind === 'trip'` 的条目才有值,普通日程一律空串。
* ⚠️ 与 `place` 不是一回事:`place` 是日程自己的「地点」字段(用户手填的),
* 这两个是行程节点上的「从哪到哪」,语义不同、来源也不同。
*/
fromPlace: string;
toPlace: string;
/** 出发地 / 目的地坐标(导航用;可能为空 —— 老数据没存过) */
fromLng: string;
fromLat: string;
toLng: string;
toLat: string;
} }
/** 这个条目能不能当"用户日程"来编辑(只有 custom 才算) */ /** 这个条目能不能当"用户日程"来编辑(只有 custom 才算) */