一、搜索页面的空结果与回滚的业务入口与状态边界

“搜索页面的空结果与回滚”要处理的问题是:搜索词变化时不能保留旧结果位置。状态边界、资源生命周期和失败路径需要在页面组合之前确定;窗口变化、断网、返回页面和重复操作才能沿同一套规则恢复。

处理“搜索页面的空结果与回滚”时,业务输入、状态变化和异常反馈必须落在明确的边界内。下文用状态模型、关键接口和失败路径展开这条链路。参考 arkui

运行结果

二、搜索页面的空结果与回滚的职责拆分

“搜索页面的空结果与回滚”采用四层分工:输入层接收点击、文本和选择;领域服务层完成校验、转换和持久化;页面状态层订阅明确字段;异常层处理空数据、权限、网络、设备能力和资源缺失。页面扩展时只接入对应层,不复制已有判断。

层级 责任 这篇的落点
输入层 收集点击、筛选、文本或文件选择 搜索词变化时不能保留旧结果位置
服务层 校验参数、读写数据、处理异步结果 搜索框、过滤列表和空状态
状态层 用可观测字段驱动 UI 刷新 选中态、加载态、错误态分离
验收范围 正常输入、空状态、失败与恢复 页面状态、结构图与异常结果

三、搜索页面的空结果与回滚的关键状态流转

关键实现聚焦状态从页面进入服务层后的约束:输入先归一化,状态只经由一个入口更新,失败信息保留给页面反馈。

@Component
class SearchService {
  static search(keyword: string, moduleId?: string): SearchHit[] {
    const kw = keyword.trim().toLowerCase()
    return COURSES
      .filter(c => moduleId === undefined || moduleId === 'all' || c.moduleId === moduleId)
      .filter(c => SearchService.matches(c, kw))
      .map(c => ({ course: c, moduleTitle: findModule(c.moduleId)?.title ?? '' }))
  }
}

页面以 keyword 是否为空分支:空值显示热门搜索,非空值调用 SearchService;命中数为 0 时显示“未找到相关内容”。清空关键词后切回热门搜索与课程入口,这就是本篇所说的回滚。

“搜索页面的空结果与回滚”的入口验收检查三项:不可用条件是否禁用入口、离开后状态是否复位、失败后是否保留继续操作的路径。失败分支与主流程使用同一套状态回收规则。

四、搜索页面的空结果与回滚的异常处理与回退

边界流程决定“搜索页面的空结果与回滚”是否稳定:空数据不能继续渲染详情,异步失败要给出下一步操作,多窗口或跨设备状态不能只依赖页面局部字段。

“搜索页面的空结果与回滚”按生命周期划分状态:即时交互留在页面,需要跨页面、重启或共享的数据经由 Service、Repository 或持久化层管理。截图用于观察页面结果,结构图用于说明状态和依赖的归属。

五、搜索页面的空结果与回滚的验收路径

  1. 进入“搜索页面的空结果与回滚”对应页面,确认入口、标题、主要内容和操作区可见。
  2. 完成一次目标交互,核对加载、成功或下一步状态是否同步。
  3. 触发空输入、重复操作或不可用条件,确认提示和恢复入口。
  4. 返回后重新进入,核对选中态、列表、统计或播放状态。
  5. 分别检查窄窗口与宽窗口,确认文本、卡片和操作控件没有重叠。

六、搜索页面的空结果与回滚的边界结论

“搜索页面的空结果与回滚”的稳定性取决于状态、服务、UI 和异常路径是否各归其位。边界清楚后,功能扩展不会把数据恢复、资源释放和页面渲染缠在同一个入口。

补充:空结果不是异常,它是查询状态的一种输出

搜索页常见的错误是把“没有匹配课程”当成数据加载失败,于是用户既看不到自己输入了什么,也不知道该清除关键词还是切换模块。本篇把关键词和模块条件保留在查询状态中,命中列表与空结果只是在同一查询下的两种投影。

static search(keyword: string, moduleId?: string): SearchHit[] {
  const kw = keyword.trim().toLowerCase()
  return COURSES
    .filter((course) => moduleId === undefined || moduleId === 'all' || course.moduleId === moduleId)
    .filter((course) => SearchService.matches(course, kw))
    .map((course) => ({ course, moduleTitle: findModule(course.moduleId)?.title ?? '' }))
}

空关键词和无命中词不能使用同一段提示。前者适合展示热门问题或引导探索,后者应显示当前词和清除动作;否则用户无法判断系统是没有开始搜索,还是确实没有数据。

private hits(): SearchHit[] {
  return SearchService.search(this.keyword, this.moduleFilter)
}

private clearQuery(): void {
  this.keyword = ''
  this.moduleFilter = 'all'
}
查询状态 列表区域 下一步
空关键词 热门问题 选择示例或输入词
有词且命中 SearchHit 列表 进入课程详情
有词无命中 保留关键词的空态 清除或调整模块

验收时先在单一模块输入一个不存在的关键词,确认空态仍展示当前条件;随后清除关键词并切回全部模块,确认结果列表恢复。这证明回滚的是查询状态,而不是通过重新打开页面碰巧得到默认列表。

补充:从页面现象回到服务边界

这个能力的真正约束是“无结果搜索不应把筛选条件和可返回路径一起丢失”。因此不能只把一次点击的视觉变化当作完成,而要让后续页面和下一次启动都从同一份领域状态推导结果。

当前实现的核心决策是先规范化关键词并以课程字段匹配返回 SearchHit。下方保留的是同一条调用链中的连续代码,而不是为了填充篇幅截取的孤立片段;它可以直接看到输入如何被规范化、何处写回状态、以及后续读取依赖什么。

export class SearchService {
  static search(keyword: string, moduleId?: string): SearchHit[] {
    const kw: string = keyword.trim().toLowerCase();
    return COURSES
      .filter(c => moduleId === undefined || moduleId === 'all' || c.moduleId === moduleId)
      .filter(c => SearchService.matches(c, kw))
      .map<SearchHit>((c: Course): SearchHit => {
        const m: LearnModule | undefined = findModule(c.moduleId);
        return { course: c, moduleTitle: m?.title ?? '' } as SearchHit;
      });
  }

  private static matches(c: Course, kw: string): boolean {
    if (kw.length === 0) return true;
    if (c.title.toLowerCase().indexOf(kw) >= 0) return true;
    if (c.desc.toLowerCase().indexOf(kw) >= 0) return true;
    if (c.subCategoryLabel.indexOf(kw) >= 0) return true;
    for (const k of c.keywords) {
      if (k.toLowerCase().indexOf(kw) >= 0) return true;
    }
    return false;
  }
}


  private hits(): SearchHit[] {
    return SearchService.search(this.keyword, this.moduleFilter);
  }

  @Builder
  resultSection() {
    Column() {
      if (this.hits().length === 0) {
        EmptyState({
          emoji: '🔍',
          title: '未找到相关内容',
          subtitle: '换个关键词试试,或在分类里浏览'
        });
      } else {
        this.resultTitle();
        Column({ space: AppSizes.s2 }) {
          ForEach(this.hits(), (h: SearchHit) => {
            this.hitRow(h);
          }, (h: SearchHit) => h.course.id);
        }
        .padding({ left: AppSizes.s4, right: AppSizes.s4 });
      }
    }
    .width('100%');
  }

  @Builder
  resultTitle() {
    Row() {
      Text('搜索结果(' + this.hits().length + ')')
        .fontSize(AppText.fsTitle)
        .fontWeight(AppText.fwSemi)
        .fontColor(this.palette.textPrimary)
        .layoutWeight(1);
    }
    .width('100%')
    .padding({ left: AppSizes.s4, right: AppSizes.s4, top: AppSizes.s2, bottom: AppSizes.s2 });
  }

  @Builder
  hitRow(h: SearchHit) {
    Row({ space: AppSizes.s3 }) {
      Column() {
        PlaceholderImage({
          imgKey: h.course.thumbKey,
          label: h.course.title,
          imgRes: courseCover(h.course.thumbKey)
        })
          .width(72).height(56);
      }
      .width(72).height(56);
      Column({ space: AppSizes.s1 }) {
        Text(h.course.title)
          .fontSize(AppText.fsSubtitle)
          .fontWeight(AppText.fwSemi)
          .fontColor(this.palette.textPrimary)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis });
        Text(h.moduleTitle + ' · ' + h.course.subCategoryLabel)
          .fontSize(AppText.fsSmall)

边界不应由页面临时 if 掩盖:空关键词、模块筛选和无命中必须区分。服务层把空值、重复操作或恢复后的数据先变成稳定结果,页面再选择展示列表、空态或提示,因此没有必要以假成功掩盖失败。

验收动作应当沿着数据流走完,而不是只看一个控件。输入无命中词、保留条件,再清除词并回读课程结果。当动作、存储或规则计算、以及第二个入口的读回都一致时,文章中的工程结论才有可检查的含义。

这类拆分还带来一个维护收益:新增入口时只要复用既有服务和状态键,就不会复制旧判断。若未来业务规则扩展,应把新条件放在同一决策边界,并用同样的回读路径验证,而不是为新页面补一套不可追踪的局部状态。读者排查问题时也应先确认事实来源和更新时机,再检查页面是否正确投影;反过来从颜色、空态或单次点击结果推断服务层成功,往往会把偶然显示误写成稳定能力。

Logo

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

更多推荐