鸿蒙原生应用实战(三):表单交互与搜索筛选——添加包裹、搜索过滤与公司管理

鸿蒙原生应用实战(三):表单交互与搜索筛选------添加包裹、搜索过滤与公司管理

本文是系列第三篇,深入讲解快递追踪 App 中的三个交互型页面:添加包裹表单、搜索与筛选、快递公司管理。涵盖表单验证、选择器实现、多条件搜索、Toggle 开关等实战内容。


一、概述

本项目的交互型页面有三个,各有特点:

页面 核心交互 技术要点
AddPackagePage 文本输入 + 下拉选择 + 表单验证 TextInput、自定义Picker、实时校验
SearchPage 文本搜索 + 标签筛选 + 结果列表 多条件组合查询、Tab 筛选
CompanyManagePage 搜索过滤 + Toggle 开关 + 收藏管理 计算属性、ToggleType.Switch

二、添加包裹页面(AddPackagePage)

2.1 页面布局

复制代码
┌─────────────────────────────┐
│ ← 添加包裹                   │
├─────────────────────────────┤
│ 输入快递单号                  │
│ ┌───────────────────────┐   │
│ │ 请输入快递单号          │   │
│ └───────────────────────┘   │
│                              │
│ 快递公司                     │
│ ┌───────────────────────┐   │
│ │ 顺丰速运            ▼  │   │
│ ├───────────────────────┤   │  ← 点击展开
│ │ 圆通速递               │   │
│ │ 中通快递               │   │
│ │ ...                    │   │
│ └───────────────────────┘   │
│                              │
│ 备注                        │
│ ┌───────────────────────┐   │
│ │ 选填                  │   │
│ └───────────────────────┘   │
│                              │
│ ┌───────────────────────┐   │
│ │        保存           │   │
│ └───────────────────────┘   │
│     单号格式正确 ✓          │  ← 实时校验提示
└─────────────────────────────┘

2.2 状态管理

typescript 复制代码
@Entry
@Component
struct AddPackagePage {
  @State trackingNo: string = '';       // 快递单号
  @State selectedCompany: string = '顺丰速运';  // 选中的快递公司
  @State note: string = '';             // 备注
  @State showPicker: boolean = false;   // 选择器展开/收起
}

2.3 自定义下拉选择器

鸿蒙没有原生的下拉选择器(Picker/Select),我们用 Text + 条件渲染实现一个:

typescript 复制代码
// 快递公司列表
let courierCompanies: string[] = [
  '顺丰速运', '圆通速递', '中通快递', 
  '韵达快递', '京东快递', '邮政EMS', '极兔速递'
];

// 选择器触发器
Row() {
  Text(this.selectedCompany)
  Blank()
  Text('▼')  // 下拉箭头
}
.onClick(() => {
  this.showPicker = !this.showPicker;  // 切换展开/收起
})

// 下拉列表(条件渲染)
if (this.showPicker) {
  Column() {
    ForEach(courierCompanies, (company: string) => {
      Text(company)
        .onClick(() => {
          this.selectedCompany = company;  // 选中
          this.showPicker = false;         // 收起
        })
    }, (company: string) => company)
  }
}

设计要点

  • 选中项高亮显示:fontColor(this.selectedCompany === company ? primary : text_primary)
  • 点击列表项后自动收起选择器
  • 列表外部没有点击遮罩层------可以通过再次点击触发器收起

2.4 表单实时验证

typescript 复制代码
// 保存按钮
Button($r('app.string.btn_save'))
  .onClick(() => {
    if (this.trackingNo.length === 0) {
      return;  // 空单号不处理
    }
    if (this.trackingNo.length < 6) {
      return;  // 单号太短
    }
    router.back();  // 验证通过,返回首页
  })

// 实时提示文字
Row() {
  if (this.trackingNo.length > 0 && this.trackingNo.length < 6) {
    Text('单号长度不足,请检查')
      .fontColor($r('app.color.status_exception'))  // 红色警告
  } else if (this.trackingNo.length === 0) {
    Text('请输入快递单号')
      .fontColor($r('app.color.text_hint'))  // 灰色提示
  } else {
    Text('单号格式正确 ✓')
      .fontColor($r('app.color.status_delivered'))  // 绿色成功
  }
}

三种状态提示

状态 条件 颜色 文案
未输入 length === 0 灰色 请输入快递单号
输入中但不足 length > 0 && length < 6 红色 单号长度不足
输入完成 length >= 6 绿色 单号格式正确 ✓

这是典型的正向反馈设计------用户每输入一个字符都能看到状态变化。

2.5 为什么不使用 TextArea 做备注?

当前备注使用了单行 TextInput,对于简短备注(如"手机""书籍")足够。如果需要长文本,应该替换为 TextArea

typescript 复制代码
// 如果需要多行备注
TextArea({ placeholder: '选填,支持多行', text: this.note })
  .height(100)

三、搜索与筛选页面(SearchPage)

3.1 功能设计

搜索页支持两种筛选维度:

  1. 文本搜索:按快递单号、公司名、备注搜索
  2. 状态筛选:全部 / 运输中 / 已签收 / 异常

两种维度组合生效,即搜索结果同时匹配文本和状态。

3.2 数据结构

typescript 复制代码
interface PackageResult {
  id: number;
  trackingNo: string;
  company: string;
  status: string;      // transit | delivered | exception
  statusText: string;
  note: string;
  updateTime: string;
}

3.3 多条件组合搜索

typescript 复制代码
doSearch(): void {
  this.hasSearched = true;
  let query = this.searchQuery.toLowerCase().trim();
  this.searchResults = [];

  for (let pkg of this.allPackages) {
    // 条件1:文本匹配(单号/公司/备注)
    let matchQuery = query.length === 0 ||
      pkg.trackingNo.toLowerCase().indexOf(query) >= 0 ||
      pkg.company.indexOf(query) >= 0 ||
      pkg.note.indexOf(query) >= 0;

    // 条件2:状态筛选
    let matchFilter = this.selectedFilter === 0 ||  // 全部
      (this.selectedFilter === 1 && pkg.status === 'transit') ||
      (this.selectedFilter === 2 && pkg.status === 'delivered') ||
      (this.selectedFilter === 3 && pkg.status === 'exception');

    // AND 组合
    if (matchQuery && matchFilter) {
      this.searchResults.push(pkg);
    }
  }
}

设计模式

  • 将"文本匹配"和"状态匹配"分开为两个布尔变量
  • && 组合,语义清晰
  • query.length === 0 时不限制文本,即显示该状态筛选下的全部

3.4 标签筛选栏

typescript 复制代码
private filterTabs: string[] = ['全部', '运输中', '已签收', '异常'];

Row() {
  ForEach(this.filterTabs, (tab: string, index: number) => {
    Text(tab)
      .fontColor(this.selectedFilter === index ? Color.White : $r('app.color.text_primary'))
      .backgroundColor(this.selectedFilter === index ? $r('app.color.primary') : $r('app.color.background'))
      .borderRadius(16)
      .onClick(() => { this.onFilterClick(index); })
  }, (tab: string) => tab)
}

选中态与未选中态的视觉区分

  • 选中:白色文字 + 主题色背景
  • 未选中:主题色文字 + 浅灰背景

3.5 搜索框与键盘交互

typescript 复制代码
TextInput({ placeholder: '输入单号/快递公司/备注', text: this.searchQuery })
  .onChange((value: string) => { this.searchQuery = value; })
  .onSubmit(() => { this.doSearch(); })  // 键盘回车触发搜索

onSubmit 捕获键盘回车事件,提升移动端输入体验------用户输入完毕直接点回车即可搜索。

3.6 搜索状态的三种 UI

typescript 复制代码
// 状态1:初始态(未搜索过)
if (!this.hasSearched) {
  // 显示引导文案
}

// 状态2:搜索有结果
if (this.hasSearched && this.searchResults.length > 0) {
  // 显示结果列表
}

// 状态3:搜索无结果
if (this.hasSearched && this.searchResults.length === 0) {
  // 显示"没有找到"空状态
}

三种状态的切换必须处理好"闪烁"问题 ------hasSearched 初始为 false,用户首次点击搜索才置为 true


四、快递公司管理页面(CompanyManagePage)

4.1 功能需求

  • 展示所有支持的快递公司列表
  • 可搜索过滤公司
  • 通过 Toggle 开关标记常用公司
  • 顶部显示已选数量统计

4.2 数据结构

typescript 复制代码
interface CompanyItem {
  name: string;
  phone: string;       // 客服电话
  website: string;     // 官网
  isFavorite: boolean; // 是否收藏
}

4.3 计算属性(getter)

typescript 复制代码
// 筛选后的公司列表
get filteredCompanies(): CompanyItem[] {
  if (!this.companies) return [];
  if (this.searchText.length === 0) return this.companies;
  
  let q = this.searchText.toLowerCase();
  let result: CompanyItem[] = [];
  for (let c of this.companies) {
    if (c.name.toLowerCase().indexOf(q) >= 0) {
      result.push(c);
    }
  }
  return result;
}

// 收藏数量
get favoriteCount(): number {
  if (!this.companies) return 0;
  let count = 0;
  for (let c of this.companies) {
    if (c.isFavorite) count++;
  }
  return count;
}

ArkTS 的计算属性(getter)会在每次渲染时重新求值,因此不需要额外声明 @State 来存储筛选结果,减少了状态同步的复杂度。

4.4 Toggle 开关组件

typescript 复制代码
Toggle({ type: ToggleType.Switch, isOn: company.isFavorite })
  .onChange(() => { 
    this.toggleFavorite(company.name); 
  })

Toggle 是鸿蒙原生开关组件,支持三种类型:

  • ToggleType.Switch:滑动开关(最常用)
  • ToggleType.Checkbox:复选框
  • ToggleType.Button:按钮式

⚠️ 注意:isOn 是初始值,但 Toggle 内部会维护自己的选中状态。当用户操作时,我们需要通过 toggleFavorite 同步修改 companies 数据。

typescript 复制代码
toggleFavorite(name: string): void {
  for (let i = 0; i < this.companies.length; i++) {
    if (this.companies[i].name === name) {
      this.companies[i].isFavorite = !this.companies[i].isFavorite;
      break;
    }
  }
}

由于 companies 是用 @State 装饰的数组,通过索引修改元素属性不会 触发 UI 更新------但这里我们只是修改 isFavorite 布尔值,Toggle 组件自身维护了视觉状态,所以 UI 不会出现不同步。

如果需要确保 UI 同步,应该创建新数组:

typescript 复制代码
toggleFavorite(name: string): void {
  this.companies = this.companies.map(c => 
    c.name === name ? { ...c, isFavorite: !c.isFavorite } : c
  );
}

4.5 头像圆圈设计

typescript 复制代码
Stack() {
  Circle()
    .width(44).height(44)
    .fill($r('app.color.primary'))
  Text(company.name.substring(0, 1))  // 取公司名的第一个字
    .fontSize(20)
    .fontColor(Color.White)
    .fontWeight(FontWeight.Bold)
}

使用 Stack 叠放圆形背景和首字,形成类似微信头像的风格。取公司名第一个字(顺→S、圆→Y、中→Z),简单有效。


五、表单验证的最佳实践

5.1 验证时机

验证时机 实现方式 适用场景
实时验证 onChange + 状态提示 单号长度、格式
提交时验证 onClick 统一校验 必填项检查
失焦验证 onBlur 输入完成后检查

5.2 用户反馈层次

好的表单反馈应该有层次感:

复制代码
🔴 单号长度不足,请检查    → 错误(阻止提交)
⚪ 请输入快递单号           → 提示(允许提交,但提交会失败)
🟢 单号格式正确 ✓          → 成功(可提交)

六、交互设计要点总结

6.1 添加包裹页

  • showPicker 控制下拉展开/收起,点外侧不会自动关闭(简化实现)
  • 实时校验三个状态:未输入/错误/正确
  • 保存按钮校验后 router.back() 返回首页(实际项目应传递数据给首页)

6.2 搜索页

  • 文本 + 状态双维度筛选,组合使用
  • 三种 UI 状态:未搜索 / 有结果 / 无结果
  • 搜索框支持回车提交

6.3 公司管理页

  • 搜索即时过滤(onChange 触发)
  • Toggle 开关管理收藏状态
  • 顶部显示统计 "已选 N/M"
  • 公司名首字 + 圆形背景构成头像

七、小结

本篇完成了三个交互密集型页面的开发:

  1. AddPackagePage:表单输入 + 自定义选择器 + 实时验证
  2. SearchPage:多条件搜索 + 标签筛选 + 三种状态 UI
  3. CompanyManagePage:搜索过滤 + Toggle 开关 + 计算属性

核心知识点:

  • 条件渲染实现下拉选择器
  • 搜索的逻辑组合与性能
  • @State 数组修改的限制与正确方式
  • 表单验证的层次反馈设计

下一篇将进入 物流时间线与历史记录,详解时间线 UI 组件的绘制、router 参数传递与接收、列表统计等高级内容。


系列索引

  • 第一篇:项目初始化与工程架构
  • 第二篇:首页与列表开发实战
  • 第三篇:表单交互与搜索筛选(本文)
  • 第四篇:物流时间线与历史记录
  • 第五篇:数据统计与个人中心
相关推荐
xcLeigh2 小时前
鸿蒙平台 gThumb 图片查看器适配实战:从 Linux GTK 到 Electron 鸿蒙壳工程
linux·electron·harmonyos·gnome·桌面环境·gthumb
金启攻2 小时前
鸿蒙原生应用开发实战(四):复杂页面与交互体验——鱼种百科、天气详情与钓点详情
harmonyos
lqj_本人2 小时前
鸿蒙pc:Hoppscotch-hoppscotch-ohos适配全记录
华为·harmonyos
xcLeigh2 小时前
鸿蒙PC平台 imv 图片查看器适配实战:极简主义设计的 Electron 迁移
华为·electron·harmonyos·鸿蒙·imv·图片操作·web_engine
不羁的木木2 小时前
《HarmonyOS 6.1 新能力实战之智感握姿》第四篇:进阶应用——横屏游戏手柄模式
游戏·华为·harmonyos
IT大白鼠3 小时前
IPv6过渡技术:原理、分类与应用
网络·网络协议·华为
风华圆舞3 小时前
在 Flutter 鸿蒙项目里接入语音识别的完整思路
flutter·语音识别·harmonyos
Swift社区3 小时前
鸿蒙游戏Runtime解析:Store如何驱动整个游戏世界?
游戏·华为·harmonyos