轨交换乘台已经能够根据站网拓扑、候车时间、无障碍约束和封站通知给出候选路线,也能识别收藏路线中的失效通行边。临近出行时,另一个问题变得突出:列表中刚刚选中的方案在几秒后可能遇到站网更新,用户打开此前保存的清单,却发现显示的是新查询的站点和旧查询的到达时间。两份数据各自正确,组合起来却形成了错误的出行指引。

这个问题需要在“选择方案”和“再次打开清单”之间建立稳定的数据契约。清单应记录被确认时的站网版本、途经站点、通行边、时间和参数;之后的线路变化触发复核,而不是悄悄改写已经确认的内容。这样,首页可以继续刷新路线,已存清单也能交代它当初为何成立。

01

一、把实时方案和行程清单分开

实时搜索结果是一个持续变化的视图。同一起终点在08:36出发和08:42出发时,候车时间可能不同;即使出发时间相同,站网版本18与版本19也可能包含不同的可通行边。若清单只保存一个搜索结果数组下标,新的排序就可能让下标指向另一条路线。保存路线对象的内存引用同样危险:页面中修改数组,历史清单会同步改变。

工程里采用两层对象:查询阶段产生可编辑的候选方案,确认阶段构造仅包含业务必需字段的行程快照。快照有独立标识,记住当时的拓扑版本,同时保存完整的站点和边序列。显示层拿到快照后只负责渲染,不在其内部重新排序站点。站网版本变化时,创建新的复核结果,与旧清单并列展示。

例如,一份从东门到湖滨码头的清单选择08:36出发、08:49到达,途经北桥和市政广场,换乘1次、步行6分钟。它所依据的站网版本为18。后来下载到版本19,界面应继续展示原清单细节,并标明“需复核”,直到使用者确认新的方案。

二、快照模型必须容纳完整路线证据

先固定类型再处理按钮事件,可以避免保存时只写入展示文字。站点序列负责阅读,通行边序列负责合法性检查;二者顺序不同,不能相互替代。时刻使用统一的分钟数表示,展示时再转成时分,防止跨小时加减造成计算错误。

interface RouteEdge {
  id: string;
  from: string;
  to: string;
  enabled: boolean;
}
interface CandidateTrip {
  stationIds: string[];
  edgeIds: string[];
  departMinute: number;
  arriveMinute: number;
  transferCount: number;
  walkMinutes: number;
}
interface NetworkSnapshot {
  version: number;
  edges: RouteEdge[];
}
interface SavedItinerary {
  id: string;
  networkVersion: number;
  stationIds: string[];
  edgeIds: string[];
  departMinute: number;
  arriveMinute: number;
  transferCount: number;
  walkMinutes: number;
  routeFingerprint: string;
}

在这里,networkVersion保存的是清单确认时的版本,不是读取清单时的最新版本。routeFingerprint是对关键业务字段按稳定顺序拼接形成的比较标识,主要用于发现字段发生改变,并非用于密码学签名。时间采用从当天零点开始计数的整数。若业务将来覆盖跨日行程,需要扩展为日期与时区明确的时间模型,而不是继续依赖当日分钟数。

三、冻结前再次核对每段通行边

候选方案从列表传到详情页后,可能经过一次异步站网更新。因此冻结时不能只判断起终点相等,需要核对边数量、相邻站点衔接、时长区间和版本一致性。错误情况保持在编辑态,给出具体原因;正常结果则复制数组,避免后续修改原数组影响快照。

function fingerprint(trip: CandidateTrip, version: number): string {
  return `${version}|${trip.stationIds.join('>')}|` +
    `${trip.edgeIds.join(',')}|${trip.departMinute}|${trip.arriveMinute}|` +
    `${trip.transferCount}|${trip.walkMinutes}`;
}
function freezeItinerary(trip: CandidateTrip, sourceVersion: number,
  network: NetworkSnapshot, id: string): SavedItinerary | undefined {
  if (sourceVersion !== network.version ||
      trip.edgeIds.length !== trip.stationIds.length - 1 ||
      trip.arriveMinute <= trip.departMinute || trip.walkMinutes < 0) {
    return undefined;
  }
  for (let i = 0; i < trip.edgeIds.length; i++) {
    const edge = network.edges.find((item: RouteEdge) => item.id === trip.edgeIds[i]);
    if (!edge || !edge.enabled || edge.from !== trip.stationIds[i] ||
        edge.to !== trip.stationIds[i + 1]) {
      return undefined;
    }
  }
  return {
    id: id, networkVersion: network.version,
    stationIds: [...trip.stationIds], edgeIds: [...trip.edgeIds],
    departMinute: trip.departMinute, arriveMinute: trip.arriveMinute,
    transferCount: trip.transferCount, walkMinutes: trip.walkMinutes,
    routeFingerprint: fingerprint(trip, network.version)
  };
}

循环中按索引核对通行边的起点和终点,比仅检查边标识存在更严格。若路线经过同站换乘,模型还要将站内步行边作为可识别的边类型,并在分钟数中体现换乘成本。候选方案来源版本由查询服务携带,不能在点击保存时临时改成最新版本,否则旧路径会披上新版本号。首次冻结失败后,应保持用户选择不变,提供“重新计算”入口,不直接覆盖已有清单。

02

四、从“版本变化”判断到“线路变化”判断

站网版本不同并不总意味着当前行程失效。例如别的片区新增站点,东门到湖滨码头的几段边可能仍然可通行。因此复核分成两个步骤。第一步判断版本是否改变;如果没有改变,且清单数据自身结构有效,状态可显示为“版本一致”。第二步在版本改变时检查旧路径经过的每条边,区分仍可通行与存在已关闭通行边。

若旧路径完整可通行,界面可以提示“可沿原路线继续使用,时刻建议刷新”;若有边失效,则标记“需要重新规划”,保留原版本和旧路径以便对照。这里仍须注意,站网版本一致仅说明拓扑信息来源一致,具体班次与实时拥挤情况还需要各自的数据更新时间。把所有动态信息都塞进一个版本数字,会给维护带来误判。

状态还应考虑离线场景。无法取得新站网时,不能把本地版本18自动标成“最新”,而是显示当前持有版本及最近校验结果;用户可以查看原清单,但新的有效性状态应为“待更新”。重新联网后先检查站网,再更新状态,避免凭借一次网络恢复事件直接认定路线可用。

五、ArkUI页面只订阅确认结果

冻结和复核属于业务决策,页面只存储当前展示的摘要和状态。这样列表更新、窗口宽度变化与返回页面不会重新执行冻结逻辑。下面的界面片段专注于已存清单的状态呈现;实际工程由页面进入时的加载流程提供状态文本,点击复核时调用前述业务函数并替换状态值。

@Entry
@Component
struct ItineraryReviewPage {
  @State reviewState: string = '已冻结快照 · 站网版本18';
  @State currentVersion: number = 18;
  @State stations: string[] = ['东门', '北桥', '市政广场', '湖滨码头'];

  build() {
    Column({ space: 14 }) {
      Text('行程快照确认').fontSize(25).fontWeight(FontWeight.Bold)
      Text('东门 → 湖滨码头').fontSize(21)
      Text('08:36 出发 · 08:49 到达 · 换乘1次 · 步行6分钟')
      List() {
        ForEach(this.stations, (station: string) => {
          ListItem() { Text(station).fontSize(17).padding(12) }
        }, (station: string) => station)
      }.height(250)
      Text(this.reviewState).fontColor('#236F62')
      Button('复核当前线路').onClick(() => {
        this.currentVersion = 19;
        this.reviewState = '检测到站网版本19 · 需重新核对通行边';
      })
    }.padding(20).width('100%')
  }
}

这里采用@State保存可观察的页面状态,ForEach提供稳定的站点键。站点名称在同一路线可能重复,正式工程建议为每个站点步骤配置独立的步骤标识,而不是把名称当作唯一键。按钮中的版本变化表示页面收到一个新版本事件后的显示分支;在完整实现中,新版本应由站网仓库提供,并且须等通行边逐段复核后才能显示“可通行”。当页面消失时结束与此页面绑定的异步订阅;在页面再次出现时读取服务层最新结果,避免使用已经过期的页面对象继续写入状态。

六、图片中的清单为什么要保留旧结论

以下页面包含四个站点、站网版本18、路线段检查和版本19提示。截图中的08:36至08:49、换乘1次与步行6分钟来自同一份示例快照;蓝色路径展示已确认的行程,黄色提醒展示后续版本事件。这两张卡片表达的是不同时间点的事实,因此不宜把提醒写到已冻结数据结构内部。

03

用户选择“查看已存清单”时,默认读取原快照并保留冻结时的完整数据;选择“复核当前线路”时,按版本19重新计算候选路径,得到新的草稿。只有再次确认,才创建另一个快照版本并明确记录新旧关联。这样一来,界面滚动、搜索列表重排甚至应用窗口切换,都不会改变已经保存的路线详情。

七、异常、权限与适配处理

行程数据如果只保存在进程内,页面销毁后无法恢复。可以将快照序列化后交给已有的本地数据仓库持久化,落盘时记录版本、写入时间和结构校验字段;写入失败时不能把按钮状态永久改为“已保存”。同一个清单标识重复提交时,应根据内容指纹与确认状态进行去重。对于异步站网复核,给每次请求附加递增代际标记,迟到结果必须经过代际核对才能影响当前页面。

本章代码涉及本地业务对象及ArkUI页面状态,不依赖敏感设备权限。若扩展到位置获取、联网实时班次或跨设备传输,应根据实际接入的系统能力分别处理授权、拒绝和网络恢复。手机采用纵向站点列表,平板或折叠屏可采用左侧清单右侧详情;无论布局如何变化,业务服务都返回同一个快照对象。布局变化不能成为更新已确认业务数据的理由。

调试时优先构造六类输入:正常冻结、站点与边不匹配、来源版本过期、无关线路升级、关键边关闭和网络短暂中断。对每种输入检查返回状态、清单对象是否改变以及是否意外触发重复写入。还需比较连续两次点击确认后的快照标识,确保不会出现一份路线对应多个内容冲突的已存记录。

八、系列收束:变化的数据与确定的决定分离

前七篇构建了可查询、可诊断、可迁移的轨交换乘路径,最后一篇补齐了出行前最关键的一道确认关:用不可变快照记录用户当时选择的方案,再用独立复核结果解释后续站网变化。只有来源版本、连续通行边、时间数据和确认动作同时满足约束,才能把候选方案转换为可信的已存清单;条件发生变化时,应保留历史并生成新结论,而不是改写旧事实。

进一步完善工程可围绕跨日时区模型、站内换乘边精细化和动态班次新鲜度展开,仍然沿用“查询—确认—冻结—复核”的数据流。对开发者而言,这比增加几个路线按钮更重要,因为它让复杂多终端界面里的每一次状态变化都有明确来源。

相关资料:ArkTS语言简介、创建列表(List)、ArkUI组件动态创建说明。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐