前言

我在《会议随记 Pro》里整理启动链路以后,很快遇到了另一个更实际的问题:页面越来越多,页面之间的关系也开始变复杂。

早期页面少的时候,会议列表进入会议详情,会议详情再进入会议编辑,直接在页面里写跳转逻辑还能接受。后来项目里陆续增加了主 Tab、新建会议、会议详情、会议编辑、项目详情、联系人详情、设置页。如果继续让每个页面自己决定下一个页面怎么打开,后面会很难维护。

真正让我重新处理导航逻辑的,是返回刷新。

会议详情页进入编辑页以后,编辑页可能会修改标题、标签、参会人,也可能会调整时间轴笔记。编辑页保存并返回以后,详情页要重新读取当前会议;详情页再返回会议列表时,列表也要知道会议数据已经变化。这个过程如果只靠页面之间互相约定,时间一长就很容易忘记哪条路径负责刷新。

我后来把页面跳转集中到 NavPathStack。首页维护一份全局页面栈,页面名称统一注册,页面之间只传业务参数。详情页进入编辑页时,把返回后的刷新动作绑定在这次跳转上。底部 Tab 页面不需要关心业务页面怎么打开,它只负责展示自己的内容。

这里最容易混在一起的其实有三件事。

事情 负责对象 项目里的处理
页面怎么打开 NavPathStack 首页统一注册页面名称
页面打开哪条数据 meetingId 等业务参数 页面之间只传必要 ID
返回以后谁刷新 当前页面或全局刷新信号 详情页用返回回调,列表用刷新 key

NavigationNavPathStack 在这个项目里不只是页面跳转工具,它们更像页面关系的中心。启动链路负责把应用带到首页,首页导航栈再负责业务页面之间的流转。只要这个边界立住,后续增加桌面卡片入口、通知入口、项目详情页、联系人详情页时,页面之间不会互相缠在一起。

img

一、跳转要有一个入口

《会议随记 Pro》的首页 Index.ets 里提供了一份全局导航栈。

@Provide('appStack') appStack: NavPathStack = new NavPathStack();

这个 appStack 不属于某个详情页,也不属于某个列表页。它由首页提供出来,后面的会议详情页、会议编辑页需要返回或者继续打开新页面时,都可以通过 @Consume('appStack') 拿到同一份导航栈。

我更愿意让页面跳转从首页出去。会议列表不需要知道会议详情页的组件文件在哪里,详情页也不需要知道编辑页怎么创建。它们只需要知道目标页面名称和当前业务 ID。

首页同时把页面名称注册到 pageMaps() 里。项目里当前已经有 mainTabswelcomemeetingNewmeetingDetailmeetingEdit、联系人详情、项目详情、关于页、设置页等映射。页面名称和组件之间的对应关系都放在这里,业务页面只通过名称入栈。

这个处理对项目结构的影响很直接。

页面关系 页面之间传什么 组件由谁注册
会议列表进入详情页 meetingId Index.ets
会议详情进入编辑页 meetingId Index.ets
项目列表进入项目详情 projectId Index.ets
联系人列表进入联系人详情 contactId Index.ets

这里我会保留一个习惯:页面名称和业务参数分开处理。

meetingDetail 是页面名称,表示要打开会议详情页。meetingId 是业务参数,表示详情页要处理哪一条会议记录。它们放在一起看很容易混淆,拆开以后会好维护很多。后面要复用同一个详情页,或者要给详情页追加来源标记、刷新策略,也不会影响页面注册方式。

底部 Tab 页面也会因此变轻。会议列表页只负责显示会议列表,点击一条会议时把 meetingId 交给导航栈。至于详情页组件怎么创建、它在页面栈里处在哪一层,都交给统一的导航入口。

二、参数不要传得太满

会议详情页不需要拿到一整条会议对象。它真正需要的是 meetingId

这个判断来自真实项目里的数据结构。会议详情页打开以后,不只是展示标题和摘要,还要读取会议主记录、时间轴笔记、待办、评论和参会人。如果列表页把完整对象传过去,详情页拿到的只是一份快照。编辑页修改标题以后,详情页手里的旧对象马上过期。

所以我在这个项目里更倾向于传业务 ID。详情页拿到 meetingId,再通过 Repository 加载当前会议。编辑页也是一样,拿到同一个 meetingId,自己读取要编辑的数据。

参数方式 适合场景 在这个项目里的处理
传完整对象 临时确认页、一次性展示页 不用于会议详情和会议编辑
传业务 ID 详情页、编辑页、需要重新读取数据的页面 会议详情和会议编辑采用这个方式
传筛选条件 列表页、搜索页、聚合页 会议列表和项目列表继续沿用
传入口动作 启动页、通知跳转、桌面卡片入口 进入首页后再转换成页面动作

参数越少,返回刷新越容易处理。详情页不需要判断上一个页面传来的对象是不是过期,也不需要合并局部字段。编辑页返回以后,详情页直接根据 meetingId 重新读取当前会议,页面状态就能回到最新数据上。

这里还有一个容易忽略的边界。导航栈只负责页面关系,不负责数据仓库。页面要显示哪条记录,可以通过参数决定;这条记录怎么读取、怎么更新、怎么处理关联数据,仍然要回到 Repository。把这两层混在一起,后面会出现页面能打开,但数据刷新很难查的问题。

我之前在页面跳转里踩过一个坑。列表页跳详情页时传了完整对象,详情页显示出来没问题;详情页再进入编辑页,编辑完返回以后,详情页标题没有更新。继续排查才发现,详情页展示的是旧对象,不是重新查询后的会议记录。改成只传 meetingId 后,这类问题会少很多。

三、返回刷新要绑定在这次跳转上

详情页进入编辑页时,我不会只写一个普通的 pushPath。这个跳转本身就带着后续动作:编辑页返回以后,详情页要重新加载。

真实项目里可以这样处理。

private handleEdit(): void {
  if (!this.meeting) {
    return;
  }

  if (this.isPlaying && this.player) {
    this.player.pause();
  }

  const param: MeetingDetailParam = {
    meetingId: this.meeting.id
  };

  this.appStack.pushPath({
    name: 'meetingEdit',
    param: param,
    onPop: () => {
      this.loadData();
    }
  });
}

这段逻辑里有两个动作我会保留。详情页进入编辑页前,先暂停正在播放的录音,避免用户编辑会议时音频还在继续播放。然后把 loadData() 绑定到这次入栈的 onPop 上。编辑页返回时,详情页重新读取当前会议。

这个写法的好处不在 API 调用本身,而在刷新关系清楚。详情页打开编辑页,编辑页返回以后详情页刷新。这是一条当前页面栈里的关系,不需要变成全局监听,也不需要让编辑页知道详情页内部方法。

编辑页保存时,做的事情也应该收住。它更新会议数据,通知全局会议数据已经变化,然后调用 appStack.pop() 返回上一页。详情页通过 onPop 重新加载,列表和工作台通过全局刷新信号感知变化。

刷新方式 适合的页面关系 在项目里的位置
onPop 回调 详情页打开编辑页,编辑完成后详情页重新加载 详情页进入编辑页时绑定
MeetingReloadKey 列表页、工作台、其他不在当前栈顶的页面感知数据变化 保存、删除、编辑完成后通知
页面初始化加载 页面首次打开,根据当前业务 ID 加载数据 详情页、编辑页、项目详情页
Tab 显示检查 Tab 重新出现时检查是否需要刷新 列表页和工作台

把这些刷新方式放在不同位置,后面排查问题会轻松一些。详情页不用等待全局信号来判断自己要不要刷新,列表页也不用知道详情页和编辑页之间发生了什么。每个页面只处理自己所在位置能确定的事情。

四、用一个小页面验证状态链路

为了把这套关系看清楚,我把真实项目里的页面栈压缩成一个小页面。这个页面里保留三种状态:会议列表、会议详情、会议编辑。它不连接真实数据库,会议数据保存在页面状态里。

这个示例不是为了替代真实项目里的 NavPathStack。它的作用是把页面栈、业务 ID、保存动作、列表刷新、详情重载、编辑返回这几件事放在同一个界面里观察。真实项目里再把这个状态链路迁回 NavPathStack + onPop + Repository + RefreshUtil

我在这个小页面里把状态更新放在同一个组件里,原因很实际。文章示例需要稳定展示运行结果,不能让页面栈、标题更新和计数器状态分散在多个 NavDestination 子页面之间。完整项目可以用更细的组件拆分,文章里的演示页先保证状态链路足够清楚。

这个小页面会跑出一条固定路径。

会议列表 → 会议详情 → 会议编辑 → 保存并返回 → 会议详情 → 返回列表

每一步对应的状态变化如下。

操作 页面栈 列表刷新次数 详情重载次数 编辑返回次数
初始进入列表 meetingList 0 0 0
点击会议进入详情 meetingList → meetingDetail 0 1 0
点击编辑标题 meetingList → meetingDetail → meetingEdit 0 1 0
保存并返回详情 meetingList → meetingDetail 1 2 1
返回列表 meetingList 1 2 1

这样跑出来以后,截图里就能观察到四个结果。

第一,当前页面栈会随着列表、详情、编辑切换而变化。第二,编辑保存以后,详情页标题会显示新标题。第三,列表刷新次数、详情重载次数、编辑返回次数都会增加。第四,返回列表后,对应会议卡片也会显示保存后的标题。

五、迁回真实项目时怎么处理

这个小页面为了便于观察,把列表、详情、编辑三种状态压缩到一个 Index.ets 里。真实项目不会这样写。项目里仍然是列表页、详情页、编辑页分开,页面栈交给 NavPathStack,数据读写交给 Repository,跨页面通知交给 RefreshUtil

迁回真实项目时,我会保留下面这几条关系。

小页面里的逻辑 真实项目里的处理
currentPage 模拟页面栈 NavPathStack 管理页面栈
currentMeetingId 页面跳转时传入的业务 ID
syncDetailSnapshot() 详情页里的 loadData()
saveAndBackToDetail() 编辑页保存会议,再调用 appStack.pop()
listRefreshCount RefreshUtil.notifyMeetingUpdate() 后列表感知刷新
detailReloadCount 详情页通过 onPop 重新加载
editReturnCount 编辑页保存并返回这一条路径的观察值

这里最值得保留的是边界,而不是演示代码的组件组织方式。真实项目里我不会把所有页面都塞进一个文件,也不会让编辑页直接知道列表怎么刷新。编辑页只负责保存和返回。详情页负责返回后的重新加载。列表页和工作台通过全局刷新信号感知会议数据变化。

这个边界很适合《会议随记 Pro》现在的结构。会议详情页本身已经有播放器、时间轴、待办、评论、参会人等模块,编辑页保存以后,详情页必须重新加载;会议列表和工作台不在当前页面栈顶部,只要通过全局刷新信号知道数据变化就够了。

总结

NavPathStack 在这个项目里解决的是页面关系维护问题。页面名称统一注册以后,列表页和详情页不需要互相导入组件。页面之间只传 meetingId 这类业务参数,具体数据继续交给 Repository 读取。详情页打开编辑页时,把返回后的重新加载绑定在这次跳转上,编辑页只负责保存并返回。

这套处理我会继续用在《会议随记 Pro》的业务页面里。会议详情、会议编辑、项目详情、联系人详情、设置页都可以放在同一份导航栈里。编辑页保存以后,当前详情页通过 onPop 重新加载;列表和工作台通过全局刷新信号感知数据变化。页面栈管页面关系,数据层管数据读取,这个边界保留下来,后面继续增加页面时会少很多绕路。

这几个点在项目里要分开处理:

  • 当前页面栈由 NavPathStack 维护
  • 当前业务数据由 meetingId 决定
  • 详情页返回刷新由 onPop 触发
  • 列表和工作台刷新由 MeetingReloadKey 通知
  • 编辑页只负责保存数据和返回上一页

我在《会议随记 Pro》里已经使用了这套页面跳转和返回刷新处理,应用目前已经上架华为应用市场。里面包含会议录音、时间轴笔记、联系人、项目、标签管理和多设备适配这些功能。对鸿蒙原生应用的完整实现感兴趣的话,可以下载体验一下:会议随记 Pro

完整代码

interface MeetingItem {
  id: string;
  title: string;
  summary: string;
  updatedAt: number;
}

interface RouteLog {
  id: number;
  action: string;
  detail: string;
}

enum DemoPage {
  List = 0,
  Detail = 1,
  Edit = 2
}

@Entry
@Component
struct Index {
  @State currentPage: DemoPage = DemoPage.List;

  @State meetings: MeetingItem[] = [
    {
      id: 'meeting-001',
      title: '产品评审会',
      summary: '确认 1.3 版本多设备适配范围',
      updatedAt: 1717819200000
    },
    {
      id: 'meeting-002',
      title: '录音链路复盘',
      summary: '整理录音状态机、保存流程和权限降级',
      updatedAt: 1717905600000
    },
    {
      id: 'meeting-003',
      title: '桌面卡片讨论',
      summary: '确认 FormID 管理和卡片刷新时机',
      updatedAt: 1717992000000
    }
  ];

  @State currentMeetingId: string = 'meeting-001';

  @State detailTitle: string = '产品评审会';
  @State detailSummary: string = '确认 1.3 版本多设备适配范围';
  @State detailUpdatedAt: number = 1717819200000;

  @State editTitleDraft: string = '';

  @State listRefreshCount: number = 0;
  @State detailReloadCount: number = 0;
  @State editReturnCount: number = 0;

  @State stackText: string = 'meetingList';
  @State logSeed: number = 0;
  @State logs: RouteLog[] = [];

  private addLog(action: string, detail: string): void {
    const next: RouteLog = {
      id: this.logSeed + 1,
      action: action,
      detail: detail
    };

    this.logSeed = next.id;
    this.logs = [next, ...this.logs].slice(0, 12);
  }

  private setStack(names: string[]): void {
    this.stackText = names.join('  →  ');
  }

  private getMeetingById(meetingId: string): MeetingItem | undefined {
    return this.meetings.find((item: MeetingItem) => item.id === meetingId);
  }

  private syncDetailSnapshot(meetingId: string, reason: string): void {
    const current = this.getMeetingById(meetingId);

    if (!current) {
      this.detailTitle = '会议不存在';
      this.detailSummary = '当前 meetingId 没有对应的会议记录';
      this.detailUpdatedAt = 0;
      this.addLog('detail empty', `没有找到会议,meetingId=${meetingId}`);
      return;
    }

    this.currentMeetingId = meetingId;
    this.detailTitle = current.title;
    this.detailSummary = current.summary;
    this.detailUpdatedAt = current.updatedAt;
    this.detailReloadCount += 1;

    this.addLog('detail reload', `${reason},meetingId=${meetingId}`);
  }

  private prepareEditDraft(): void {
    const current = this.getMeetingById(this.currentMeetingId);

    if (current) {
      this.editTitleDraft = current.title;
      return;
    }

    this.editTitleDraft = '未命名会议';
  }

  private openDetail(meetingId: string): void {
    this.syncDetailSnapshot(meetingId, '打开详情页时同步会议快照');
    this.setStack(['meetingList', 'meetingDetail']);
    this.currentPage = DemoPage.Detail;
    this.addLog('push detail', `会议列表进入会议详情,meetingId=${meetingId}`);
  }

  private openEdit(): void {
    this.prepareEditDraft();
    this.setStack(['meetingList', 'meetingDetail', 'meetingEdit']);
    this.currentPage = DemoPage.Edit;
    this.addLog('push edit', `会议详情进入会议编辑,meetingId=${this.currentMeetingId}`);
  }

  private saveAndBackToDetail(): void {
    const finalTitle = this.editTitleDraft.trim().length > 0 ? this.editTitleDraft.trim() : '未命名会议';
    const savedAt = Date.now();

    let savedSummary = this.detailSummary;

    const nextItems = this.meetings.map((item: MeetingItem): MeetingItem => {
      if (item.id === this.currentMeetingId) {
        savedSummary = item.summary;

        return {
          id: item.id,
          title: finalTitle,
          summary: item.summary,
          updatedAt: savedAt
        };
      }

      return item;
    });

    this.meetings = nextItems;

    this.detailTitle = finalTitle;
    this.detailSummary = savedSummary;
    this.detailUpdatedAt = savedAt;

    this.listRefreshCount += 1;
    this.detailReloadCount += 1;
    this.editReturnCount += 1;

    this.setStack(['meetingList', 'meetingDetail']);
    this.currentPage = DemoPage.Detail;

    this.addLog('save', `会议 ${this.currentMeetingId} 的标题保存为:${finalTitle}`);
    this.addLog('list refresh', `列表刷新次数增加到 ${this.listRefreshCount}`);
    this.addLog('detail reload', `详情重载次数增加到 ${this.detailReloadCount}`);
    this.addLog('edit return', `编辑返回次数增加到 ${this.editReturnCount}`);
  }

  private backToList(): void {
    this.setStack(['meetingList']);
    this.currentPage = DemoPage.List;
    this.addLog('pop detail', '会议详情返回会议列表');
  }

  private manualRefreshList(): void {
    this.listRefreshCount += 1;
    this.addLog('list refresh', `手动刷新会议列表,刷新次数 ${this.listRefreshCount}`);
  }

  private manualReloadDetail(): void {
    this.syncDetailSnapshot(this.currentMeetingId, '详情页手动重新同步会议数据');
  }

  @Builder
  private StatCard(label: string, value: string) {
    Column({ space: 6 }) {
      Text(label)
        .fontSize(12)
        .fontColor('#64748B')

      Text(value)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#0F172A')
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)
    .padding(14)
    .backgroundColor(Color.White)
    .borderRadius(16)
  }

  @Builder
  private StackCard() {
    Column({ space: 12 }) {
      Text('当前页面栈')
        .fontSize(13)
        .fontColor('#64748B')

      Text(this.stackText)
        .fontSize(17)
        .fontWeight(FontWeight.Medium)
        .fontColor('#0F172A')
        .width('100%')
    }
    .width('100%')
    .padding(14)
    .backgroundColor(Color.White)
    .borderRadius(16)
  }

  @Builder
  private MeetingCard(item: MeetingItem) {
    Column({ space: 8 }) {
      Row() {
        Column({ space: 4 }) {
          Text(item.title)
            .fontSize(17)
            .fontWeight(FontWeight.Medium)
            .fontColor('#0F172A')

          Text(item.summary)
            .fontSize(13)
            .fontColor('#64748B')
            .lineHeight(20)
            .maxLines(2)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)

        Text(this.currentMeetingId === item.id ? '当前' : '打开')
          .fontSize(12)
          .fontColor(this.currentMeetingId === item.id ? '#2563EB' : '#64748B')
          .padding({
            left: 10,
            right: 10,
            top: 4,
            bottom: 4
          })
          .backgroundColor(this.currentMeetingId === item.id ? '#DBEAFE' : '#F1F5F9')
          .borderRadius(12)
      }
      .width('100%')

      Text(`updatedAt=${item.updatedAt}`)
        .fontSize(11)
        .fontColor('#94A3B8')
        .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(18)
    .shadow({
      radius: 10,
      color: '#10000000',
      offsetX: 0,
      offsetY: 3
    })
    .onClick(() => {
      this.openDetail(item.id);
    })
  }

  @Builder
  private CounterPanel() {
    Column({ space: 12 }) {
      Row({ space: 12 }) {
        this.StatCard('当前 meetingId', this.currentMeetingId)
        this.StatCard('列表刷新次数', this.listRefreshCount.toString())
      }
      .width('100%')

      Row({ space: 12 }) {
        this.StatCard('详情重载次数', this.detailReloadCount.toString())
        this.StatCard('编辑返回次数', this.editReturnCount.toString())
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  private BuildListPage() {
    Column({ space: 18 }) {
      Column({ space: 8 }) {
        Text('会议列表')
          .fontSize(26)
          .fontWeight(FontWeight.Bold)
          .fontColor('#0F172A')

        Text('列表页只展示会议摘要。点击一条会议后,页面栈会进入会议详情,当前 meetingId 会同步到页面层。')
          .fontSize(14)
          .fontColor('#475569')
          .lineHeight(22)
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)

      this.StackCard()
      this.CounterPanel()

      Button('手动刷新列表')
        .width('100%')
        .height(44)
        .backgroundColor('#2563EB')
        .fontColor(Color.White)
        .borderRadius(22)
        .onClick(() => {
          this.manualRefreshList();
        })

      Column({ space: 12 }) {
        ForEach(this.meetings, (item: MeetingItem) => {
          this.MeetingCard(item)
        }, (item: MeetingItem) => `${item.id}-${item.updatedAt}-${this.listRefreshCount}`)
      }
      .width('100%')

      this.LogPanel()
    }
    .width('100%')
  }

  @Builder
  private BuildDetailPage() {
    Column({ space: 18 }) {
      Column({ space: 8 }) {
        Text('会议详情')
          .fontSize(26)
          .fontWeight(FontWeight.Bold)
          .fontColor('#0F172A')

        Text('详情页展示当前会议快照。编辑页保存返回后,标题、更新时间和刷新计数会一起变化。')
          .fontSize(14)
          .fontColor('#475569')
          .lineHeight(22)
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)

      this.StackCard()

      Column({ space: 12 }) {
        Text(this.detailTitle)
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#0F172A')

        Text(this.detailSummary)
          .fontSize(15)
          .fontColor('#475569')
          .lineHeight(24)

        Text(`meetingId=${this.currentMeetingId}`)
          .fontSize(12)
          .fontColor('#64748B')

        Text(`updatedAt=${this.detailUpdatedAt}`)
          .fontSize(12)
          .fontColor('#64748B')
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)
      .padding(18)
      .backgroundColor(Color.White)
      .borderRadius(20)

      Row({ space: 12 }) {
        Button('编辑标题')
          .layoutWeight(1)
          .height(44)
          .backgroundColor('#2563EB')
          .fontColor(Color.White)
          .borderRadius(22)
          .onClick(() => {
            this.openEdit();
          })

        Button('重新加载')
          .layoutWeight(1)
          .height(44)
          .backgroundColor('#E2E8F0')
          .fontColor('#0F172A')
          .borderRadius(22)
          .onClick(() => {
            this.manualReloadDetail();
          })
      }
      .width('100%')

      Button('返回列表')
        .width('100%')
        .height(44)
        .backgroundColor('#0F766E')
        .fontColor(Color.White)
        .borderRadius(22)
        .onClick(() => {
          this.backToList();
        })

      this.CounterPanel()
      this.LogPanel()
    }
    .width('100%')
  }

  @Builder
  private BuildEditPage() {
    Column({ space: 18 }) {
      Column({ space: 8 }) {
        Text('会议编辑')
          .fontSize(26)
          .fontWeight(FontWeight.Bold)
          .fontColor('#0F172A')

        Text('编辑页只负责修改标题。保存动作会更新会议数组、详情快照和三个统计值。')
          .fontSize(14)
          .fontColor('#475569')
          .lineHeight(22)
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)

      this.StackCard()

      Column({ space: 12 }) {
        Text('会议标题')
          .fontSize(14)
          .fontColor('#64748B')

        TextInput({
          text: this.editTitleDraft,
          placeholder: '请输入会议标题'
        })
          .height(48)
          .fontSize(16)
          .backgroundColor('#F8FAFC')
          .borderRadius(14)
          .padding({
            left: 12,
            right: 12
          })
          .onChange((value: string) => {
            this.editTitleDraft = value;
          })

        Text(`meetingId=${this.currentMeetingId}`)
          .fontSize(12)
          .fontColor('#94A3B8')
      }
      .width('100%')
      .padding(18)
      .backgroundColor(Color.White)
      .borderRadius(20)

      Button('保存并返回')
        .width('100%')
        .height(46)
        .backgroundColor('#2563EB')
        .fontColor(Color.White)
        .borderRadius(23)
        .onClick(() => {
          this.saveAndBackToDetail();
        })

      this.CounterPanel()
      this.LogPanel()
    }
    .width('100%')
  }

  @Builder
  private LogPanel() {
    Column({ space: 12 }) {
      Text('导航日志')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#0F172A')
        .width('100%')

      if (this.logs.length === 0) {
        Text('还没有导航记录')
          .fontSize(13)
          .fontColor('#94A3B8')
          .width('100%')
          .padding(14)
          .backgroundColor('#F8FAFC')
          .borderRadius(14)
      } else {
        ForEach(this.logs, (item: RouteLog) => {
          Row({ space: 10 }) {
            Text(item.action)
              .fontSize(11)
              .fontColor('#1D4ED8')
              .padding({
                left: 8,
                right: 8,
                top: 3,
                bottom: 3
              })
              .backgroundColor('#DBEAFE')
              .borderRadius(10)

            Text(item.detail)
              .fontSize(13)
              .fontColor('#334155')
              .lineHeight(20)
              .layoutWeight(1)
          }
          .width('100%')
          .alignItems(VerticalAlign.Top)
          .padding(12)
          .backgroundColor('#F8FAFC')
          .borderRadius(14)
        }, (item: RouteLog) => item.id.toString())
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(20)
  }

  build() {
    Scroll() {
      Column({ space: 18 }) {
        if (this.currentPage === DemoPage.List) {
          this.BuildListPage()
        } else if (this.currentPage === DemoPage.Detail) {
          this.BuildDetailPage()
        } else {
          this.BuildEditPage()
        }
      }
      .width('100%')
      .padding(20)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#EEF2F7')
  }
}
Logo

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

更多推荐