鸿蒙原生应用 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 个方法

  1. Header — 标题栏
  2. SearchCard — 搜索 + 拍照/语音入口
  3. CategorySelector — 四色分类选择网格(手动选)
  4. PreviewCard — 识别结果预览(自动联动)
  5. QuickTemplateRow — 常见垃圾快捷查询(一键识别)
  6. FormCard — 补充信息(备注 + 来源)
  7. 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,真实项目应调用相机 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 个 @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 的 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 = '';
  }
}

逻辑拆解

  1. 空输入拦截keyword.length === 0 → Toast "请输入垃圾名称" + return(早返回)
  2. 遍历模板for 循环匹配 templates[i].name === keyword精确匹配,不是包含)
  3. 命中联动:同快捷查询一样更新 5 个状态(result/resultCat/selectedCat/resultColor)+ found = true + break
  4. 未命中降级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——它同时驱动预览卡分类名、提交校验、图标 emoji(resultCat.length > 0 判断)——"一个状态多处渲染"的响应式精髓

注意 resultColor 当前没有被 UI 使用(预览卡渐变是固定的 C.primary→C.accent),它是为"未来让预览卡渐变跟随分类颜色"预留的——demo 的"前瞻性字段",真实项目应让 PreviewCardlinearGradient 使用 resultColor

智能垃圾分类识别页底部 · 提交记录按钮+安全区留白+底部Tab栏

十、与 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(分类校验提交)
)

核心可复用点

  1. doSearch() 搜索模式(空输入拦截 → 遍历匹配 → 命中联动 → 未命中降级)
  2. 名称→id/颜色映射函数getCategoryId/getColorByCat
  3. 四色选择器(同色系深浅 + 边框表达选中态)
  4. 一键识别联动 5 状态(快捷查询)

这套"查询 + 选择 + 预览 + 提交"模板可复用到:垃圾分类、药品查询、词典查词、车牌识别、植物识别——任何"输入/选择 → 得到结果 → 提交"的场景

十二、搜索体验的进阶设计

doSearch() 是"基础搜索",真实产品还要考虑 4 个进阶点:

1. 防抖(Debounce)。用户每敲一个字都触发搜索会浪费资源。真实项目应加 300ms 防抖:onChangeclearTimeout(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 是"教学级"实现——逻辑清晰(拦截→遍历→命中联动→未命中降级),读者能一眼看懂"搜索该怎么做";真实项目在此之上加防抖/联想/容错即可。

智能垃圾分类识别页中段 · 识别结果预览卡+6个常见垃圾快捷查询网格

十三、快捷查询与搜索的"双入口"设计

本页有两个识别入口

入口交互适用场景
搜索框输入文字 → doSearch()知道垃圾名称
快捷查询 Grid点击物品卡 → 一键联动不确定名称/想探索

"双入口"覆盖两类用户目的明确型(知道"旧报纸"叫什么 → 直接搜)和浏览探索型(不知道"什么垃圾常见" → 点卡片看)。

两个入口共用同一套状态更新(result/resultCat/selectedCat/resultColor)——"多入口单逻辑"是交互设计的标准:不同入口触达同一功能,不重复实现逻辑。如果两个入口各写一套状态更新,改一处漏一处,bug 丛生。

快捷查询 Grid 的 6 个物品选得讲究:🔋废电池(有害)、📰旧报纸(可回收)、🍌香蕉皮(厨余)、💡灯泡(有害)、🥤塑料瓶(可回收)、🦴骨头(其他)——4 类全覆盖 + 每类 1-2 个代表。"废电池/旧报纸/香蕉皮"是每个家庭最常见的垃圾,用户点开就能看到"我家的垃圾属于哪类"。

真实项目应把快捷查询做成"动态热词":根据用户历史搜索推荐高频物品(废电池/塑料瓶排前面),而不是固定 6 个——"数据驱动排序"比"静态模板"更能命中用户需求

十四、识别页的可复用价值

识别页的"查询交互"模型(输入/选择 → 联动结果 → 校验提交)可复用到:

  • 垃圾分类:垃圾名 → 分类结果
  • 药品查询:药名 → 用法用量
  • 词典查词:单词 → 释义
  • 植物识别:拍照 → 植物信息
  • 车牌查询:车牌 → 车辆信息

核心模式@State keyword(输入)+ 辅助方法(查询逻辑)+ @State 结果(联动渲染)+ 提交校验"搜索 + 结果联动"是查询类应用的第一性模式,本页把它实现得清晰完整。

十五、小结

App 15 识别页解析完毕。doSearch() 真实搜索逻辑是本系列的里程碑——从"演示交互"到"真实逻辑"的跨越。四色选择器 + 结果预览联动 + 双入口 + 三方法映射,让识别页成为"查询类页面"的完整范本。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐