ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

鸿蒙原生应用 ArkUI 实战:从零实现垃圾分类识别页的真实搜索与结果联动

鸿蒙原生应用 ArkUI 实战:从零实现垃圾分类识别页的真实搜索与结果联动 鸿蒙原生应用 ArkUI 实战从零实现垃圾分类识别页的真实搜索与结果联动App 15「智能垃圾分类」识别 TabFunc1Tab是本系列第一个有真实搜索逻辑的页面——搜索框输入垃圾名称 →doSearch()遍历模板数据 → 命中则联动更新分类选择、结果预览未命中则提示暂无匹配。整页还包含四色分类选择网格、渐变识别结果预览卡、6 个常见垃圾快捷查询3 列 Grid、补充信息表单、提交记录校验。本篇基于15-trash-classify/entry/src/main/ets/pages/Func1Tab.ets约 300 行逐段拆解附 4 张实机截图。一、整体结构搜索 → 选择 → 预览 → 提交Func1Tab 是 7 个 Builder 3 个辅助方法doSearch()/getCategoryId()/getColorByCat()组成的识别漏斗build() { Column() { this.Header() Scroll() { Column({ space: 16 }) { this.SearchCard() this.CategorySelector() this.PreviewCard() this.QuickTemplateRow() this.FormCard() this.SubmitBtn() } .width(100%) .padding({ left: D.pad, right: D.pad, top: 16, bottom: D.pad this.safeBottom 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width(100%).height(100%).backgroundColor(C.bg) }7 个块 3 个方法Header— 标题栏SearchCard— 搜索 拍照/语音入口CategorySelector— 四色分类选择网格手动选PreviewCard— 识别结果预览自动联动QuickTemplateRow— 常见垃圾快捷查询一键识别FormCard— 补充信息备注 来源SubmitBtn— 提交记录校验分类3 个辅助方法是本页的灵魂doSearch()搜索逻辑、getCategoryId()分类名 → id 映射、getColorByCat()分类名 → 颜色映射。辅助方法 状态联动让页面从静态展示进化为真实交互——这是 App 15 区别于 App 13/14 的核心进步。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、Header SearchCard渐变搜索卡 拍照/语音入口SearchCard 是渐变搜索区含搜索框 拍照识别 语音输入Builder SearchCard() { Column({ space: 12 }) { Row({ space: 8 }) { TextInput({ placeholder: 输入垃圾名称查询..., text: this.keyword }) .onChange((val: string) { this.keyword val; }) .backgroundColor(#33FFFFFF).borderRadius(D.rSm).height(44) .layoutWeight(1).placeholderColor(#FFFFFF).placeholderFont({ size: 13 }) .fontColor(#FFFFFF) Text() .fontSize(18).width(44).height(44).textAlign(TextAlign.Center) .backgroundColor(#FFFFFF).borderRadius(D.rSm) .onClick(() { this.doSearch(); }) } .width(100%) Row({ space: 8 }) { Text().fontSize(20) Text(拍照识别).fontSize(13).fontColor(#FFFFFF) Blank() Text(语音输入 ).fontSize(13).fontColor(#FFFFFF).opacity(0.85) } .width(100%) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) .backgroundColor(#33FFFFFF).borderRadius(D.rSm) .onClick(() { promptAction.showToast({ message: 拍照识别 }); }) } .width(100%) .padding(16) .borderRadius(D.rLg) .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] }) }关键差异搜索按钮点击调用this.doSearch()App 13/14 只弹 Toast——从伪交互到真逻辑的跨越。拍照识别 语音输入入口一行内左 拍照识别 右语音输入 85% 透明度。这是真实识别类 App如拍照识物的标配入口。demo 只弹 Toast真实项目应调用相机 APIohos.multimedia.camera或语音识别 APIohos.ai.speechRecognizer。三、CategorySelector四色分类选择网格CategorySelector 是 4 大分类的 2 列 Grid每个分类卡带自己的颜色Builder CategorySelector() { Column({ space: 12 }) { Text(选择垃圾类型).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Grid() { ForEach(this.categories, (cat: Category) { GridItem() { Column({ space: 6 }) { Row() { Text(cat.emoji).fontSize(28) } .width(52).height(52) .backgroundColor(this.selectedCat cat.id ? cat.color 33 : cat.color 1A) .borderRadius(26) .justifyContent(FlexAlign.Center) .border({ width: this.selectedCat cat.id ? 2 : 0, color: cat.color }) Text(cat.name).fontSize(12).fontColor(this.selectedCat cat.id ? cat.color : C.text) .fontWeight(this.selectedCat cat.id ? FontWeight.Bold : FontWeight.Normal) Text(cat.desc).fontSize(10).fontColor(C.textDim) } .alignItems(HorizontalAlign.Center) .onClick(() { this.selectedCat cat.id; this.resultCat cat.name; this.resultColor cat.color; }) } }, (cat: Category) cat.id.toString()) } .columnsTemplate(1fr 1fr) .columnsGap(10) .rowsGap(10) .width(100%) } .width(100%) .padding(14) .backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }) }3.1 4 分类数据private categories: Category[] [ { id: 1, emoji: ♻️, name: 可回收物, color: #3B82F6, desc: 适宜回收利用的废弃物 }, { id: 2, emoji: ⚠️, name: 有害垃圾, color: #EF4444, desc: 含有害物质需特殊处理 }, { id: 3, emoji: , name: 厨余垃圾, color: #22C55E, desc: 易腐烂的生物质废弃物 }, { id: 4, emoji: ️, name: 其他垃圾, color: #9CA3AF, desc: 除前三类以外的生活垃圾 } ];与首页 QuickActionRow 的同一套四色编码蓝/红/绿/灰——跨页颜色一致用户在首页记住蓝可回收识别页看到蓝色卡自动关联。每个分类卡 emoji 图标52×52 正圆 名称 描述三行。3.2 选中态三件套颜色进数据.backgroundColor(this.selectedCat cat.id ? cat.color 33 : cat.color 1A) .borderRadius(26) .justifyContent(FlexAlign.Center) .border({ width: this.selectedCat cat.id ? 2 : 0, color: cat.color }) Text(cat.name).fontSize(12).fontColor(this.selectedCat cat.id ? cat.color : C.text)背景选中cat.color 3320% 同色透明vs 未选cat.color 1A10%——选中比未选深一档比换颜色更细腻边框选中 2vp 同色描边 vs 未选无边框文字选中同色加粗 vs 未选深色常规同色系深浅变化表达选中态是比换主题色更高级的手法——用户感知这个卡变深了/有框了但颜色语义蓝可回收保持不变。**选中不变色、只加深**是四色系统的正确打开方式如果选中改成主题绿用户就分不清哪个分类被选了。3.3 点击联动 3 个状态.onClick(() { this.selectedCat cat.id; this.resultCat cat.name; this.resultColor cat.color; })一次点击更新 3 个 StateselectedCat分类网格高亮resultCat预览卡分类名resultColor预览卡颜色——手动选择与搜索结果共用同一套状态预览卡自动联动。四、PreviewCard识别结果预览PreviewCard 是渐变结果卡实时显示选择了/搜索了什么分类Builder PreviewCard() { Row({ space: 12 }) { Column({ space: 6 }) { Text(识别结果).fontSize(11).fontColor(#FFFFFF).opacity(0.85) Text(this.resultCat.length 0 ? this.resultCat : 请选择或搜索).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#FFFFFF) Text(this.result.length 0 ? this.result : 等待识别...).fontSize(12).fontColor(#FFFFFF).opacity(0.9) } .alignItems(HorizontalAlign.Start).layoutWeight(1) Column({ space: 4 }) { Row() { Text(this.resultCat.length 0 ? this.categories[this.selectedCat - 1].emoji : ).fontSize(28) } .width(52).height(52) .backgroundColor(#FFFFFF).opacity(0.25).borderRadius(26) .justifyContent(FlexAlign.Center) } .alignItems(HorizontalAlign.Center) } .width(100%) .padding(16) .borderRadius(D.rLg) .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] }) }三态文字分类名resultCat.length 0 ? resultCat : 请选择或搜索有结果显分类无则引导详情result.length 0 ? result : 等待识别...有描述显描述无则等待图标resultCat.length 0 ? categories[selectedCat - 1].emoji : 有分类显分类 emoji无则默认草注意categories[this.selectedCat - 1].emojiselectedCat是 1-4 的 idcategories数组下标 0-3所以id - 1取下标——id 从 1 开始下标从 0 开始的经典偏移。如果selectedCat 0未选择时是 0categories[-1]会越界——但外层三元已用resultCat.length 0拦截resultCat为空时不会进入——防御性写法。与 App 14 场地页 PreviewCard 的对比App 14 显示场地 场次App 15 显示分类 描述 emoji——预览卡是当前选择的通用反馈组件换数据即可复用。五、QuickTemplateRow6 个常见垃圾快捷查询QuickTemplateRow 是 6 个常见垃圾废电池/旧报纸/香蕉皮/灯泡/塑料瓶/骨头的 3 列 GridBuilder QuickTemplateRow() { Column({ space: 10 }) { Text(常见垃圾快捷查询).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Grid() { ForEach(this.templates, (t: Template) { GridItem() { Column({ space: 4 }) { Text(t.emoji).fontSize(24) Text(t.name).fontSize(11).fontColor(C.text) } .width(100%) .padding({ top: 10, bottom: 10 }) .backgroundColor(C.cardSoft).borderRadius(D.rSm) .border({ width: 1, color: C.stroke }) .alignItems(HorizontalAlign.Center) .onClick(() { this.keyword t.name; this.result t.desc; this.resultCat t.category; this.selectedCat this.getCategoryId(t.category); this.resultColor this.getColorByCat(t.category); promptAction.showToast({ message: t.name → t.category }); }) } }, (t: Template) t.id.toString()) } .columnsTemplate(1fr 1fr 1fr) .columnsGap(10) .rowsGap(10) .width(100%) } .width(100%) }5.1 6 个垃圾的数据设计private templates: Template[] [ { id: 1, emoji: , name: 废电池, category: 有害垃圾, desc: 含汞、镉等重金属 }, { id: 2, emoji: , name: 旧报纸, category: 可回收物, desc: 纸质可回收 }, { id: 3, emoji: , name: 香蕉皮, category: 厨余垃圾, desc: 果皮易腐 }, { id: 4, emoji: , name: 灯泡, category: 有害垃圾, desc: 含荧光粉 }, { id: 5, emoji: , name: 塑料瓶, category: 可回收物, desc: PET材质 }, { id: 6, emoji: , name: 骨头, category: 其他垃圾, desc: 难腐蚀 } ];6 个物品覆盖 4 大分类有害废电池/灯泡、可回收旧报纸/塑料瓶、厨余香蕉皮、其他骨头——每个分类至少 1 个示例用户能通过快捷查询体验完整分类。5.2 一键识别联动 5 个状态.onClick(() { this.keyword t.name; this.result t.desc; this.resultCat t.category; this.selectedCat this.getCategoryId(t.category); this.resultColor this.getColorByCat(t.category); promptAction.showToast({ message: t.name → t.category }); })一次点击联动 5 个 State搜索框文字keyword、结果描述result、结果分类resultCat、分类网格高亮selectedCat、结果颜色resultColor——全页面 5 处 UI 同时更新Toast 显示废电池 → 有害垃圾反馈。getCategoryId()和getColorByCat()是名称 → id/颜色的映射函数private getCategoryId(name: string): number { for (let i 0; i this.categories.length; i) { if (this.categories[i].name name) return this.categories[i].id; } return 0; } private getColorByCat(name: string): string { for (let i 0; i this.categories.length; i) { if (this.categories[i].name name) return this.categories[i].color; } return C.primary; }两个方法都是线性遍历 匹配 兜底返回找不到返回 0getCategoryId/ C.primary 绿色getColorByCat——**防御性默认值**避免undefined报错。真实项目建议用字典映射替代线性遍历数据量大时更快// 推荐Record 字典 private catMap: Recordstring, number { 可回收物: 1, 有害垃圾: 2, 厨余垃圾: 3, 其他垃圾: 4 }; private getCategoryId(name: string): number { return this.catMap[name] ?? 0; }Recordstring, number哈希查找 O(1)且代码更短。但 demo 用线性遍历也不差4 个分类遍历成本可忽略——**数据量小用遍历数据量大用字典**是工程权衡。六、FormCard补充信息 来源选择FormCard 是备注输入 来源选择家庭/办公/户外Builder FormCard() { Column({ space: 12 }) { Text(补充信息).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).width(100%) Row({ space: 8 }) { Text(备注).fontSize(13).fontColor(C.textDim).width(50) TextInput({ placeholder: 选填如垃圾数量、来源等, text: this.note }) .onChange((val: string) { this.note val; }) .backgroundColor(C.cardSoft).borderRadius(D.rSm).height(40) .layoutWeight(1).placeholderColor(C.textDim).placeholderFont({ size: 13 }) } .width(100%) Row({ space: 8 }) { Text(来源).fontSize(13).fontColor(C.textDim).width(50) Row({ space: 8 }) { Text(家庭).fontSize(12).fontColor(C.primary) .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor(C.primarySoft).borderRadius(8) Text(办公).fontSize(12).fontColor(C.textSub) .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor(C.cardSoft).borderRadius(8) Text(户外).fontSize(12).fontColor(C.textSub) .padding({ left: 10, right: 10, top: 6, bottom: 6 }) .backgroundColor(C.cardSoft).borderRadius(8) } } .width(100%) } .width(100%) .padding(16) .backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }) }来源选择是假单选——家庭看起来被选中绿字浅绿底办公/户外未选中灰字浅灰底但没有 State 控制点击无反应。真实项目应加State source: number 0点击this.source idx三处用idx this.source判断高亮——与 App 13 优先级选择PriorityCard同款逻辑。备注占位选填如垃圾数量、来源等——选填标识在占位符里用户一看知道可以不填。七、SubmitBtn校验分类的提交SubmitBtn 是提交记录按钮校验是否已选分类Builder SubmitBtn() { Button(提交记录) .width(100%).height(48) .fontSize(16).fontColor(#FFFFFF).backgroundColor(C.primary) .borderRadius(D.rMd) .onClick(() { if (this.resultCat.length 0) { promptAction.showToast({ message: 请选择垃圾类型 }); return; } promptAction.showToast({ message: 分类记录已提交: this.resultCat }); }) }单校验 早返回.onClick(() { if (this.resultCat.length 0) { promptAction.showToast({ message: 请选择垃圾类型 }); return; } promptAction.showToast({ message: 分类记录已提交: this.resultCat }); })resultCat.length 0→ 提示请选择垃圾类型否则提交并回显分类记录已提交: this.resultCat。成功提示带分类名回显用户确认我提交的是厨余垃圾。与 App 14 场地页 SubmitBtn 的差异App 14 是场次 姓名双校验App 15 是分类单校验——校验项数量跟随业务预约必须选场次填名字分类记录只要有分类就能提交。八、doSearch()真实搜索逻辑本页灵魂doSearch()是本系列第一个真实搜索方法——不是弹 Toast而是真正遍历数据、联动状态private doSearch(): void { if (this.keyword.length 0) { promptAction.showToast({ message: 请输入垃圾名称 }); return; } let found: boolean false; for (let i 0; i this.templates.length; i) { if (this.templates[i].name this.keyword) { this.result this.templates[i].desc; this.resultCat this.templates[i].category; this.selectedCat this.getCategoryId(this.templates[i].category); this.resultColor this.getColorByCat(this.templates[i].category); found true; break; } } if (!found) { this.result 暂无匹配请手动选择类型; this.resultCat ; } }逻辑拆解空输入拦截keyword.length 0→ Toast 请输入垃圾名称 return早返回遍历模板for循环匹配templates[i].name keyword精确匹配不是包含命中联动同快捷查询一样更新 5 个状态result/resultCat/selectedCat/resultColorfound true break未命中降级result 暂无匹配请手动选择类型resultCat 清空分类引导手动选命中联动 未命中降级是搜索的标准双分支。demo 用精确匹配真实项目应// 推荐模糊匹配 过滤列表 const results this.templates.filter(t t.name.includes(this.keyword.trim()));break提前退出——找到第一个匹配就停止遍历避免无谓循环。found布尔标志区分命中/未命中两个分支。doSearch 的执行入口有两个搜索按钮 点击onClick(() { this.doSearch(); })和快捷查询点击内联 5 状态联动——两处入口共用同一套状态更新逻辑快捷查询是内联版 doSearch。九、State 的 6 状态设计Func1Tab 有 6 个 State是 4 个 Tab 中最多的State类型用途更新来源keywordstring搜索框文字输入 / 快捷查询selectedCatnumber分类网格高亮1-4选择 / 搜索 / 快捷查询resultstring结果描述搜索 / 快捷查询resultCatstring结果分类名选择 / 搜索 / 快捷查询resultColorstring结果颜色选择 / 搜索 / 快捷查询notestring备注输入输入6 个状态 6 个交互点输入/选择/搜索/备注。核心是resultCat——它同时驱动预览卡分类名、提交校验、图标 emojiresultCat.length 0判断——一个状态多处渲染的响应式精髓。注意resultColor当前没有被 UI 使用预览卡渐变是固定的 C.primary→C.accent它是为未来让预览卡渐变跟随分类颜色预留的——demo 的前瞻性字段真实项目应让PreviewCard的linearGradient使用resultColor。十、与 App 14 场地预约页的对比维度App 14 场地预约App 15 识别查询核心交互场地切换 场次网格搜索 分类选择搜索无仅 ToastdoSearch() 真实逻辑数据联动切换清空场次命中联动 5 状态预览卡场地场次分类描述emoji快捷方式3 个模板横滑6 个常见垃圾 3 列 Grid校验场次姓名双校验分类单校验App 15 的进步搜索从弹 Toast升级为真逻辑快捷方式从3 个横滑升级为6 个 Grid——每个系列应用都比前一个多走一步。十一、总结识别页模板Header Scroll( SearchCard渐变搜索 拍照/语音入口 CategorySelector四色分类 2 列 Grid PreviewCard结果预览联动 QuickTemplateRow常见垃圾 3 列 Grid FormCard备注 来源 SubmitBtn分类校验提交 )核心可复用点doSearch()搜索模式空输入拦截 → 遍历匹配 → 命中联动 → 未命中降级名称→id/颜色映射函数getCategoryId/getColorByCat四色选择器同色系深浅 边框表达选中态一键识别联动 5 状态快捷查询这套查询 选择 预览 提交模板可复用到垃圾分类、药品查询、词典查词、车牌识别、植物识别——任何输入/选择 → 得到结果 → 提交的场景。十二、搜索体验的进阶设计doSearch()是基础搜索真实产品还要考虑 4 个进阶点1. 防抖Debounce。用户每敲一个字都触发搜索会浪费资源。真实项目应加 300ms 防抖onChange里clearTimeout(timer); timer setTimeout(() doSearch(), 300)——停止输入 300ms 后才搜索避免废废电废电池三次请求。2. 联想/自动补全。用户输入废时应弹出废电池/废纸/废玻璃联想列表下拉浮层点击即完成搜索。实现filter(t t.name.includes(keyword))过滤出匹配项用List展示。边输边联想是搜索的体验天花板。3. 结果态管理。搜索有 4 种结果态未输入提示请输入、匹配成功显示分类、匹配失败显示暂无匹配、网络错误显示请重试。状态枚举化type SearchState idle | success | empty | errorUI 按状态渲染不同文案/图标。4. 容错匹配。用户输入香蕉皮但数据库存香蕉皮精确匹配 OK输入香蕉包含匹配或香焦错别字就不行。真实项目应做**包含匹配 同义词表 拼音匹配三层容错——demo 用精确匹配是简化但搜索要容错**这条认知值得记住。本页的 doSearch 是教学级实现——逻辑清晰拦截→遍历→命中联动→未命中降级读者能一眼看懂搜索该怎么做真实项目在此之上加防抖/联想/容错即可。十三、快捷查询与搜索的双入口设计本页有两个识别入口入口交互适用场景搜索框输入文字 → doSearch()知道垃圾名称快捷查询 Grid点击物品卡 → 一键联动不确定名称/想探索双入口覆盖两类用户目的明确型知道旧报纸叫什么 → 直接搜和浏览探索型不知道什么垃圾常见 → 点卡片看。两个入口共用同一套状态更新result/resultCat/selectedCat/resultColor——多入口单逻辑是交互设计的标准不同入口触达同一功能不重复实现逻辑。如果两个入口各写一套状态更新改一处漏一处bug 丛生。快捷查询 Grid 的 6 个物品选得讲究废电池有害、旧报纸可回收、香蕉皮厨余、灯泡有害、塑料瓶可回收、骨头其他——4 类全覆盖 每类 1-2 个代表。废电池/旧报纸/香蕉皮是每个家庭最常见的垃圾用户点开就能看到我家的垃圾属于哪类。真实项目应把快捷查询做成动态热词根据用户历史搜索推荐高频物品废电池/塑料瓶排前面而不是固定 6 个——数据驱动排序比静态模板更能命中用户需求。十四、识别页的可复用价值识别页的查询交互模型输入/选择 → 联动结果 → 校验提交可复用到垃圾分类垃圾名 → 分类结果药品查询药名 → 用法用量词典查词单词 → 释义植物识别拍照 → 植物信息车牌查询车牌 → 车辆信息核心模式State keyword输入辅助方法查询逻辑State 结果联动渲染提交校验。搜索 结果联动是查询类应用的第一性模式本页把它实现得清晰完整。十五、小结App 15 识别页解析完毕。doSearch() 真实搜索逻辑是本系列的里程碑——从演示交互到真实逻辑的跨越。四色选择器 结果预览联动 双入口 三方法映射让识别页成为查询类页面的完整范本。
返回列表