前言

详情页最怕失败后空白。用户不知道是没数据、没权限、网络断了,还是页面坏了。HarmonyOS7 页面里我会把这些状态显式拆开,用页面状态做兜底。

这篇单独聊 详情页 这个场景。重点不是堆 API,而是用页面状态把加载、成功、空数据和失败兜底说清楚。

一张适合中文技术文章的手绘笔记风信息图,主题是“HarmonyOS7 详情页错误边界用页面状态兜底”

为什么这个问题经常被写乱

错误边界用页面状态兜底 这类内容很容易被写成“代码能跑就算讲完了”,但对初学者来说,这恰恰是最不够的地方。真正让人卡住的,往往不是某个组件名记不住,而是不知道这段代码为什么要这样拆、状态为什么要这样放、以后需求变化时应该从哪里改。

所以这篇文章不只想给你一个能跑的例子,更想把背后的判断过程讲清楚。你只要把这个判断过程吃透,后面自己改页面、补需求、查问题时,心里会稳很多。

场景:订单详情加载失败

详情页失败后空白,是移动端体验里很糟糕的一类问题。用户点进订单详情,页面什么都没有,他不知道是订单不存在、网络断了、没有权限,还是应用出错。测试同学截图也只能给一句“白屏”,开发排查成本很高。

我更喜欢把详情页当成一个小状态机:加载中、正常内容、空数据、无权限、错误。每一种状态都有明确的页面表现和下一步动作。这样即使接口失败,用户也知道可以重试;如果数据为空,也不会误以为页面坏了。

业务页面的错误边界,不只是防崩溃,更是给用户一条能继续往下走的路。

状态拆分

一张中文手绘流程图,讲解 HarmonyOS7 订单详情页的状态流转和错误边界处理。流程从“进入详情

状态 触发条件 页面表现 用户动作
Loading 首次进入或重试 加载文案或骨架 等待
Content 有有效数据 展示详情内容 正常操作
Empty 接口成功但无数据 空状态说明 返回或刷新
Forbidden 无权限或已下架 权限说明 返回列表
Error 网络或服务异常 错误原因 + 重试 重新加载

实操步骤

  1. 定义页面状态枚举,让页面始终只处在一种状态。
  2. loadDetail() 统一处理状态流转,进入请求前先切到 Loading
  3. 接口成功后继续区分 ContentEmptyForbidden,不要全都当成功页面渲染。
  4. 错误页保留重试按钮,重试时复用同一个加载方法。
  5. 正文只在 Content 状态下渲染,避免访问空字段。
  6. 面向用户展示可理解的错误说明,技术细节放日志里。

先把页面目标想清楚

在真正写代码之前,先别急着盯着 API。更有用的做法是先想清楚:这个页面到底想解决什么问题,用户最在意的反馈是什么,哪些状态必须一直保持一致。

当你先把这条主线想明白,再回头看组件和状态设计,很多选择都会顺理成章。对小白来说,这一步尤其重要,因为它能帮你从“照着抄”慢慢过渡到“看得懂、改得动”。

完整示例:订单详情的页面状态兜底

enum DetailState {
  Loading,
  Content,
  Empty,
  Forbidden,
  Error
}

@Entry
@Component
struct DetailFallbackPage {
  @State pageState: DetailState = DetailState.Loading
  @State title: string = ''
  @State amountText: string = ''
  @State errorText: string = ''
  @State retryCount: number = 0

  private orderId: string = 'NO20260711001'

  aboutToAppear() {
    this.loadDetail('error')
  }

  private loadDetail(mockResult: string) {
    this.pageState = DetailState.Loading
    this.errorText = ''

    if (mockResult === 'content') {
      this.title = 'HarmonyOS7 实战课程订单'
      this.amountText = '¥129.00'
      this.pageState = DetailState.Content
      return
    }

    if (mockResult === 'empty') {
      this.pageState = DetailState.Empty
      return
    }

    if (mockResult === 'forbidden') {
      this.errorText = '该订单已下架或当前账号无查看权限'
      this.pageState = DetailState.Forbidden
      return
    }

    this.errorText = '订单详情加载失败,请检查网络后重试'
    this.pageState = DetailState.Error
  }

  private retry() {
    this.retryCount += 1
    this.loadDetail(this.retryCount > 1 ? 'content' : 'error')
  }

  @Builder
  StateHint(title: string, desc: string, actionText: string) {
    Column({ space: 10 }) {
      Text(title)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor(title.indexOf('失败') >= 0 ? '#D92D20' : '#222222')
      Text(desc)
        .fontSize(14)
        .fontColor('#666666')
      Button(actionText)
        .onClick(() => this.retry())
    }
    .alignItems(HorizontalAlign.Start)
    .padding(16)
    .backgroundColor('#F5F7FA')
    .borderRadius(8)
  }

  build() {
    Column({ space: 16 }) {
      Text(`订单 ${this.orderId}`)
        .fontSize(22)
        .fontWeight(FontWeight.Bold)

      if (this.pageState === DetailState.Loading) {
        Text('正在加载订单详情...')
          .fontSize(16)
          .fontColor('#666666')
      } else if (this.pageState === DetailState.Content) {
        Column({ space: 8 }) {
          Text(this.title).fontSize(18).fontWeight(FontWeight.Medium)
          Text(this.amountText).fontSize(24).fontColor('#0A59F7').fontWeight(FontWeight.Bold)
          Text('已支付,课程权益已发放到当前账号。').fontSize(14).fontColor('#666666')
        }.alignItems(HorizontalAlign.Start)
      } else if (this.pageState === DetailState.Empty) {
        this.StateHint('暂无订单内容', '可能是订单已删除,或列表数据还未同步。', '刷新')
      } else if (this.pageState === DetailState.Forbidden) {
        this.StateHint('无法查看订单', this.errorText, '返回后重试')
      } else {
        this.StateHint('加载失败', this.errorText, '重试')
      }
    }
    .padding(20)
    .alignItems(HorizontalAlign.Start)
  }
}

一张中文手绘框架图,表达“先把页面目标想清楚,再写代码”的详情页状态设计思路。画面上方是一个标题框:

把关键代码一段段拆开

DetailState 让页面状态互斥。相比 isLoadingisErrorisEmpty 三个布尔值,枚举更不容易出现“既加载又失败”的矛盾状态。

loadDetail() 是唯一的状态流转入口。页面首次加载、失败重试、下拉刷新都应该走同一套逻辑,这样不会出现某个入口忘记清理 errorText 的问题。

StateHint() 把空态、无权限和错误态的结构收起来,但文案仍然由调用处传入。这样既减少重复 UI,又不会把所有状态揉成同一段泛泛提示。

容易踩坑的点

  • 接口返回空数组时直接渲染正文,导致字段为空或页面白屏。
  • 失败和空数据使用同一套“暂无数据”,用户不知道能不能重试。
  • 多个布尔值互相冲突,刷新时出现错误页和加载页同时闪动。
  • 错误文案直接展示接口异常,比如 500 Internal Server Error
  • 重试按钮写了另一套逻辑,和首次加载结果不一致。

优化建议

如果页面首屏结构固定,可以把 Loading 状态做成骨架屏,减少等待焦虑。对于详情页里的关键操作,比如支付、取消订单、联系商家,只有在 Content 状态才展示,避免用户在错误状态下误触。

日志方面,错误状态进入时应记录模块名、订单 id、错误码和重试次数,但不要把 token、手机号、地址等敏感信息输出到日志。用户看到的是友好文案,开发看到的是可定位线索。

错误兜底要让用户有下一步

错误边界不是把异常挡住就结束。详情页失败后,用户需要知道发生了什么、能不能重试、是否应该返回列表。加载、空数据、无权限和网络失败必须分开表达,因为它们对应的下一步完全不同。

示例用 DetailState 让页面状态互斥,正文只在 Content 状态访问业务字段。这样即使接口没有返回数据,也不会因为页面继续读空字段而变成白屏。

写在最后

真实项目里建议把用户文案和开发日志分层处理。用户看到“加载失败,请检查网络后重试”,开发日志里记录模块名、订单 id、错误码和重试次数。这样既不吓用户,也能给排查留下线索。

Logo

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

更多推荐