鸿蒙原生应用 ArkUI 实战:从零实现垃圾分类识别页的真实搜索与结果联动
鸿蒙原生应用 ArkUI 实战:从零实现垃圾分类识别页的真实搜索与结果联动
App 15「智能垃圾分类」识别 Tab(Func1Tab),是本系列第一个有真实搜索逻辑的页面——搜索框输入垃圾名称 →
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 的核心进步。

二、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,真实项目应调用相机 API(@ohos.multimedia.camera)或语音识别 API(@ohos.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 + '33'(20% 同色透明)vs 未选cat.color + '1A'(10%)——"选中比未选深一档",比"换颜色"更细腻 - 边框:选中 2vp 同色描边 vs 未选无边框
- 文字:选中同色加粗 vs 未选深色常规
"同色系深浅变化"表达选中态是比"换主题色"更高级的手法——用户感知"这个卡变深了/有框了",但颜色语义(蓝=可回收)保持不变。**"选中不变色、只加深"**是四色系统的正确打开方式(如果选中改成主题绿,用户就分不清哪个分类被选了)。
3.3 点击联动 3 个状态
.onClick(() => {
this.selectedCat = cat.id;
this.resultCat = cat.name;
this.resultColor = cat.color;
})
一次点击更新 3 个 @State:selectedCat(分类网格高亮)+ 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].emoji:selectedCat 是 1-4 的 id,categories 数组下标 0-3,所以 id - 1 取下标——"id 从 1 开始,下标从 0 开始"的经典偏移。如果 selectedCat = 0(未选择时是 0),categories[-1] 会越界——但外层三元已用 resultCat.length > 0 拦截(resultCat 为空时不会进入)——防御性写法。
与 App 14 场地页 PreviewCard 的对比:App 14 显示"场地 + 场次",App 15 显示"分类 + 描述 + emoji"——预览卡是"当前选择"的通用反馈组件,换数据即可复用。
五、QuickTemplateRow:6 个常见垃圾快捷查询
QuickTemplateRow 是 6 个常见垃圾(废电池/旧报纸/香蕉皮/灯泡/塑料瓶/骨头)的 3 列 Grid:
@Builder
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;
}
两个方法都是"线性遍历 + 匹配 + 兜底返回":找不到返回 0(getCategoryId)/ C.primary 绿色(getColorByCat)——**"防御性默认值"**避免 undefined 报错。
真实项目建议用"字典映射"替代线性遍历(数据量大时更快):
// 推荐:Record 字典
private catMap: Record<string, number> = {
'可回收物': 1, '有害垃圾': 2, '厨余垃圾': 3, '其他垃圾': 4
};
private getCategoryId(name: string): number {
return this.catMap[name] ?? 0;
}
Record<string, 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/resultColor)+
found = 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 | 类型 | 用途 | 更新来源 |
|---|---|---|---|
keyword | string | 搜索框文字 | 输入 / 快捷查询 |
selectedCat | number | 分类网格高亮(1-4) | 选择 / 搜索 / 快捷查询 |
result | string | 结果描述 | 搜索 / 快捷查询 |
resultCat | string | 结果分类名 | 选择 / 搜索 / 快捷查询 |
resultColor | string | 结果颜色 | 选择 / 搜索 / 快捷查询 |
note | string | 备注输入 | 输入 |
6 个状态 = 6 个交互点(输入/选择/搜索/备注)。核心是 resultCat——它同时驱动预览卡分类名、提交校验、图标 emoji(resultCat.length > 0 判断)——"一个状态多处渲染"的响应式精髓。
注意 resultColor 当前没有被 UI 使用(预览卡渐变是固定的 C.primary→C.accent),它是为"未来让预览卡渐变跟随分类颜色"预留的——demo 的"前瞻性字段",真实项目应让 PreviewCard 的 linearGradient 使用 resultColor。

十、与 App 14 场地预约页的对比
| 维度 | App 14 场地预约 | App 15 识别查询 |
|---|---|---|
| 核心交互 | 场地切换 + 场次网格 | 搜索 + 分类选择 |
| 搜索 | 无(仅 Toast) | doSearch() 真实逻辑 |
| 数据联动 | 切换清空场次 | 命中联动 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' | 'error',UI 按状态渲染不同文案/图标。
4. 容错匹配。用户输入"香蕉皮"但数据库存"香蕉皮"(精确匹配 OK),输入"香蕉"(包含匹配)或"香焦"(错别字)就不行。真实项目应做**"包含匹配 + 同义词表 + 拼音匹配"三层容错——demo 用 === 精确匹配是简化,但"搜索要容错"**这条认知值得记住。
本页的 doSearch 是"教学级"实现——逻辑清晰(拦截→遍历→命中联动→未命中降级),读者能一眼看懂"搜索该怎么做";真实项目在此之上加防抖/联想/容错即可。

十三、快捷查询与搜索的"双入口"设计
本页有两个识别入口:
| 入口 | 交互 | 适用场景 |
|---|---|---|
| 搜索框 | 输入文字 → doSearch() | 知道垃圾名称 |
| 快捷查询 Grid | 点击物品卡 → 一键联动 | 不确定名称/想探索 |
"双入口"覆盖两类用户:目的明确型(知道"旧报纸"叫什么 → 直接搜)和浏览探索型(不知道"什么垃圾常见" → 点卡片看)。
两个入口共用同一套状态更新(result/resultCat/selectedCat/resultColor)——"多入口单逻辑"是交互设计的标准:不同入口触达同一功能,不重复实现逻辑。如果两个入口各写一套状态更新,改一处漏一处,bug 丛生。
快捷查询 Grid 的 6 个物品选得讲究:🔋废电池(有害)、📰旧报纸(可回收)、🍌香蕉皮(厨余)、💡灯泡(有害)、🥤塑料瓶(可回收)、🦴骨头(其他)——4 类全覆盖 + 每类 1-2 个代表。"废电池/旧报纸/香蕉皮"是每个家庭最常见的垃圾,用户点开就能看到"我家的垃圾属于哪类"。
真实项目应把快捷查询做成"动态热词":根据用户历史搜索推荐高频物品(废电池/塑料瓶排前面),而不是固定 6 个——"数据驱动排序"比"静态模板"更能命中用户需求。
十四、识别页的可复用价值
识别页的"查询交互"模型(输入/选择 → 联动结果 → 校验提交)可复用到:
- 垃圾分类:垃圾名 → 分类结果
- 药品查询:药名 → 用法用量
- 词典查词:单词 → 释义
- 植物识别:拍照 → 植物信息
- 车牌查询:车牌 → 车辆信息
核心模式:@State keyword(输入)+ 辅助方法(查询逻辑)+ @State 结果(联动渲染)+ 提交校验。"搜索 + 结果联动"是查询类应用的第一性模式,本页把它实现得清晰完整。
十五、小结
App 15 识别页解析完毕。doSearch() 真实搜索逻辑是本系列的里程碑——从"演示交互"到"真实逻辑"的跨越。四色选择器 + 结果预览联动 + 双入口 + 三方法映射,让识别页成为"查询类页面"的完整范本。
更多推荐




所有评论(0)