鸿蒙原生应用实战(三):表单交互与搜索筛选——添加包裹、搜索过滤与公司管理
本文是系列第三篇,深入讲解快递追踪 App 中的三个交互型页面:添加包裹表单、搜索与筛选、快递公司管理。涵盖表单验证、选择器实现、多条件搜索、Toggle 开关等实战内容。
一、概述
本项目的交互型页面有三个,各有特点:
| 页面 | 核心交互 | 技术要点 |
|---|---|---|
| AddPackagePage | 文本输入 + 下拉选择 + 表单验证 | TextInput、自定义Picker、实时校验 |
| SearchPage | 文本搜索 + 标签筛选 + 结果列表 | 多条件组合查询、Tab 筛选 |
| CompanyManagePage | 搜索过滤 + Toggle 开关 + 收藏管理 | 计算属性、ToggleType.Switch |
二、添加包裹页面(AddPackagePage)
2.1 页面布局
┌─────────────────────────────┐ │ ← 添加包裹 │ ├─────────────────────────────┤ │ 输入快递单号 │ │ ┌───────────────────────┐ │ │ │ 请输入快递单号 │ │ │ └───────────────────────┘ │ │ │ │ 快递公司 │ │ ┌───────────────────────┐ │ │ │ 顺丰速运 ▼ │ │ │ ├───────────────────────┤ │ ← 点击展开 │ │ 圆通速递 │ │ │ │ 中通快递 │ │ │ │ ... │ │ │ └───────────────────────┘ │ │ │ │ 备注 │ │ ┌───────────────────────┐ │ │ │ 选填 │ │ │ └───────────────────────┘ │ │ │ │ ┌───────────────────────┐ │ │ │ 保存 │ │ │ └───────────────────────┘ │ │ 单号格式正确 ✓ │ ← 实时校验提示 └─────────────────────────────┘2.2 状态管理
@Entry@Componentstruct AddPackagePage{@StatetrackingNo:string='';// 快递单号@StateselectedCompany:string='顺丰速运';// 选中的快递公司@Statenote:string='';// 备注@StateshowPicker:boolean=false;// 选择器展开/收起}2.3 自定义下拉选择器
鸿蒙没有原生的下拉选择器(Picker/Select),我们用 Text + 条件渲染实现一个:
// 快递公司列表letcourierCompanies: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 表单实时验证
// 保存按钮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'))// 红色警告}elseif(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:
// 如果需要多行备注TextArea({placeholder:'选填,支持多行',text:this.note}).height(100)三、搜索与筛选页面(SearchPage)
3.1 功能设计
搜索页支持两种筛选维度:
- 文本搜索:按快递单号、公司名、备注搜索
- 状态筛选:全部 / 运输中 / 已签收 / 异常
两种维度组合生效,即搜索结果同时匹配文本和状态。
3.2 数据结构
interfacePackageResult{id:number;trackingNo:string;company:string;status:string;// transit | delivered | exceptionstatusText:string;note:string;updateTime:string;}3.3 多条件组合搜索
doSearch():void{this.hasSearched=true;letquery=this.searchQuery.toLowerCase().trim();this.searchResults=[];for(letpkgofthis.allPackages){// 条件1:文本匹配(单号/公司/备注)letmatchQuery=query.length===0||pkg.trackingNo.toLowerCase().indexOf(query)>=0||pkg.company.indexOf(query)>=0||pkg.note.indexOf(query)>=0;// 条件2:状态筛选letmatchFilter=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 标签筛选栏
privatefilterTabs: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 搜索框与键盘交互
TextInput({placeholder:'输入单号/快递公司/备注',text:this.searchQuery}).onChange((value:string)=>{this.searchQuery=value;}).onSubmit(()=>{this.doSearch();})// 键盘回车触发搜索onSubmit捕获键盘回车事件,提升移动端输入体验——用户输入完毕直接点回车即可搜索。
3.6 搜索状态的三种 UI
// 状态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 数据结构
interfaceCompanyItem{name:string;phone:string;// 客服电话website:string;// 官网isFavorite:boolean;// 是否收藏}4.3 计算属性(getter)
// 筛选后的公司列表getfilteredCompanies():CompanyItem[]{if(!this.companies)return[];if(this.searchText.length===0)returnthis.companies;letq=this.searchText.toLowerCase();letresult:CompanyItem[]=[];for(letcofthis.companies){if(c.name.toLowerCase().indexOf(q)>=0){result.push(c);}}returnresult;}// 收藏数量getfavoriteCount():number{if(!this.companies)return0;letcount=0;for(letcofthis.companies){if(c.isFavorite)count++;}returncount;}ArkTS 的计算属性(getter)会在每次渲染时重新求值,因此不需要额外声明@State来存储筛选结果,减少了状态同步的复杂度。
4.4 Toggle 开关组件
Toggle({type:ToggleType.Switch,isOn:company.isFavorite}).onChange(()=>{this.toggleFavorite(company.name);})Toggle是鸿蒙原生开关组件,支持三种类型:
ToggleType.Switch:滑动开关(最常用)ToggleType.Checkbox:复选框ToggleType.Button:按钮式
⚠️ 注意:isOn是初始值,但Toggle内部会维护自己的选中状态。当用户操作时,我们需要通过toggleFavorite同步修改companies数据。
toggleFavorite(name:string):void{for(leti=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 同步,应该创建新数组:
toggleFavorite(name:string):void{this.companies=this.companies.map(c=>c.name===name?{...c,isFavorite:!c.isFavorite}:c);}4.5 头像圆圈设计
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”
- 公司名首字 + 圆形背景构成头像
七、小结
本篇完成了三个交互密集型页面的开发:
- ✅AddPackagePage:表单输入 + 自定义选择器 + 实时验证
- ✅SearchPage:多条件搜索 + 标签筛选 + 三种状态 UI
- ✅CompanyManagePage:搜索过滤 + Toggle 开关 + 计算属性
核心知识点:
- 条件渲染实现下拉选择器
- 搜索的逻辑组合与性能
- @State 数组修改的限制与正确方式
- 表单验证的层次反馈设计
下一篇将进入物流时间线与历史记录,详解时间线 UI 组件的绘制、router 参数传递与接收、列表统计等高级内容。
系列索引:
- 第一篇:项目初始化与工程架构
- 第二篇:首页与列表开发实战
- 第三篇:表单交互与搜索筛选(本文)
- 第四篇:物流时间线与历史记录
- 第五篇:数据统计与个人中心