HarmonyOS 3D 拆解分析:AI 分析接入

发布时间:2026/7/9 9:33:18
HarmonyOS 3D 拆解分析:AI 分析接入 前言之前的拆解流程是拍照 → 网格切分 → 爆炸展示。部件名称是程序根据网格位置自动生成的部件 1“部件 2”没有真正的语义信息。这次迭代做了两件事接入 AI 分析服务——拍照后自动识别物品名称和部件名称加入粒子场动效——工作区背景不再是纯色有了氛围感新增服务AiAnalysisServiceimport 变化import{ParticleField}from../components/ParticleFieldimport{analyzeImage,AiAnalysisResult}from../services/AiAnalysisService两个新组件ParticleField粒子场背景组件analyzeImageAiAnalysisResultAI 图像分析服务AI 分析的状态管理StateaiLoading:booleanfalseStateaiResult:string两个状态变量aiLoadingAI 是否在分析中aiResult分析结果的文字描述AI 分析方法privateasyncrunAiAnalysis():Promisevoid{this.aiLoadingtrue;this.aiResultconstmockBase64:stringmock_image_dataconstresult:AiAnalysisResultawaitanalyzeImage(mockBase64)this.aiLoadingfalseif(result.successresult.parts.length0){this.aiResultresult.objectName · result.parts.length 部件constmaxParts:numberresult.parts.lengththis.pc.length?result.parts.length:this.pc.lengthfor(leti:number0;imaxParts;i){this.pc[i].nresult.parts[i].name}// Force state refreshconstupdated:PCard[][]for(letj:number0;jthis.pc.length;j){updated.push(this.pc[j])}this.pcupdated}else{this.aiResultAI 分析失败: result.errorMessage}}核心逻辑用 mock 的 base64 图片调用analyzeImage返回AiAnalysisResult包含success、objectName、parts、errorMessage成功时用 AI 返回的部件名称替换程序生成的名称失败时显示错误信息为什么需要force state refreshconstupdated:PCard[][]for(letj:number0;jthis.pc.length;j){updated.push(this.pc[j])}this.pcupdated直接修改this.pc[i].n不会触发 ArkUI 的状态更新——因为State变量的引用没有变化。需要创建一个新数组赋值给this.pc才能让 UI 刷新。这是 ArkTS 状态管理的一个常见陷阱修改数组元素不会触发重新渲染必须替换整个数组。AiAnalysisResult 的数据结构interfaceAiAnalysisResult{success:booleanobjectName:string// iPhone 15parts:PartName[]// [{ name: 屏幕 }, { name: 电池 }, ...]errorMessage:string// 失败时的错误信息}interfacePartName{name:string}AI 返回的信息success分析是否成功objectName识别出的物品名称parts部件名称列表errorMessage失败原因当前的 mock 实现analyzeImage传入的是mock_image_data——一个占位字符串。真正的实现需要将图片编码为 base64调用 AI 视觉识别 API解析返回结果粒子场ParticleField// Workspace 背景Stack({alignContent:Alignment.Center}){Column(){}.width(100%).height(100%).borderRadius(18).linearGradient({angle:180,colors:[[#FCFBFE,0],[#F8F6FC,0.5],[#FCFBFE,1]]}).border({width:1.5,color:#E0DAF0}).shadow({radius:16,color:rgba(108,60,224,0.06),offsetY:4})// Native particle fieldParticleField({color:A})// ... ExplodedView or placeholder ...}ParticleField放在背景层渐变色卡片和内容层ExplodedView 或网格预览之间。为什么用粒子场氛围感纯色背景太死粒子场给工作区一种科技感深度暗示粒子的运动暗示了 3D 空间的存在视觉焦点运动的粒子会吸引用户的注意力到工作区粒子场的color参数传入主题色A#8A5DFA粒子颜色和整体紫色主题一致。工作区的视觉重设计旧版纯白背景Column(){}.width(100%).height(100%).borderRadius(18).backgroundColor(#FFFFFF).border({width:1,color:#D8D2E8}).shadow({radius:12,color:rgba(0,0,0,0.06),offsetY:3})新版渐变背景 粒子场Column(){}.width(100%).height(100%).borderRadius(18).linearGradient({angle:180,colors:[[#FCFBFE,0],[#F8F6FC,0.5],[#FCFBFE,1]]}).border({width:1.5,color:#E0DAF0}).shadow({radius:16,color:rgba(108,60,224,0.06),offsetY:4})ParticleField({color:A})变化纯白 → 微妙渐变#FCFBFE → #F8F6FC → #FCFBFE上下略浅中间略深边框从 1px 灰色 → 1.5px 淡紫色和主题色呼应阴影从黑色 → 紫色更柔和新增粒子场空状态的数字展示// 旧版Text().fontSize(36).opacity(0.2).margin({bottom:12})Text(this.name).fontSize(16).fontWeight(FontWeight.Bold).fontColor(T1)Text(this.count 部件 · 点击 [▶ 爆炸] 查看).fontSize(12).fontColor(T3)// 新版Column(){Text(this.count.toString()).fontSize(44).fontWeight(FontWeight.Bold).linearGradient({angle:135,colors:[[A,0],[#C4B5FD,1]]})Text(部件就绪).fontSize(14).fontWeight(FontWeight.Medium).fontColor(T2).margin({top:4})Text(点击 ▶ 爆炸 开始拆解).fontSize(11).fontColor(T3).margin({top:6})}变化 emoji → 大号渐变数字44px紫色渐变文字更简洁“部件就绪” “点击 ▶ 爆炸 开始拆解”为什么用数字替代 emoji数字比如9比 emoji 更直接地传达了有多少部件的信息。用户看到9就知道这个物品会被切成 9 块比看到一个 更有用。Header 的 Tag 组件旧版DotBuilderDot(color:string,text:string){Row(){Row(){}.width(5).height(5).borderRadius(3).backgroundColor(color)Text( text).fontSize(10).fontColor(T2)}}新版TagBuilderTag(color:string,bg:string,text:string){Row(){Row(){}.width(5).height(5).borderRadius(3).backgroundColor(color)Text( text).fontSize(10).fontWeight(FontWeight.Medium).fontColor(color)}.padding({left:7,right:7,top:3,bottom:3}).backgroundColor(bg).borderRadius(6)}变化新增bg参数——背景色文字颜色从 T2灰色→color主题色和圆点颜色一致加了 padding 和背景色——从裸文字变成了胶囊标签调用方式// 旧版this.Dot(A,this.count 部件)this.Dot(A3,this.cols×this.rows 网格)// 新版this.Tag(A,#F4F1FC,this.count部件)this.Tag(#00B894,#E8F8F5,this.cols×this.rows)Tag 更醒目——有了背景色和主题色文字用户一眼就能看到9 部件“3×3”。QuickBtn快捷力度按钮BuilderQuickBtn(label:string,action:()void,active:boolean,color:string){Text(label).fontSize(11).fontWeight(FontWeight.Medium).fontColor(active?#FFFFFF:T2).padding({left:14,right:14,top:5,bottom:5}).borderRadius(12).backgroundColor(active?color:#F5F5F7).onClick(action)}三个预设力度0%、50%、100%。Row({space:6}){this.QuickBtn(0%,(){this.setV(0)},this.val0,A)this.QuickBtn(50%,(){this.setV(0.5)},this.val0.5,#FECA57)this.QuickBtn(100%,(){this.setV(1)},this.val1,#FF6B6B)Row(){}.width(1).height(18).backgroundColor(#E8E4F0).margin({left:4,right:4})ForEach(this.gps,...)}为什么加 QuickBtnSlider 适合精细调节但有时候用户只是想看一下完全展开的效果。QuickBtn 提供了一键设置点100%直接爆炸到最大点0%直接收起。QuickBtn 和网格选择器放在同一行用一个竖线分隔[0%] [50%] [100%] | [2×2] [3×2] [3×3] [4×3] [4×4]左边是力度右边是网格——一行搞定所有控制。底部操作栏Row(){// AI 分析按钮Row(){Text().fontSize(13).margin({right:2})Text(this.aiLoading?分析中...:AI 分析).fontSize(11).fontWeight(FontWeight.Medium).fontColor(this.aiLoading?T3:A)}.padding({left:12,right:12,top:6,bottom:6}).backgroundColor(this.aiLoading?#F5F5F7:AL).borderRadius(12).onClick((){if(!this.aiLoading){this.runAiAnalysis()}})// 部件清单按钮Row(){Text(☰).fontSize(12).fontColor(this.list?A:T3)Text( 部件).fontSize(11).fontWeight(this.list?FontWeight.Medium:FontWeight.Regular).fontColor(this.list?A:T3)}.padding({left:12,right:12,top:6,bottom:6}).backgroundColor(this.list?AL:#F5F5F7).borderRadius(12).margin({left:6}).onClick((){this.list!this.list})Blank()// 重置按钮Row(){Text( 重置).fontSize(11).fontColor(T2)}.padding({left:12,right:12,top:6,bottom:6}).backgroundColor(#F5F5F7).borderRadius(12).onClick((){this.setV(0);this.cg(3,3)})}三个按钮AI 分析调用 AI 识别部件名称部件清单展开/收起部件列表重置力度归零 网格恢复 3×3为什么重置按钮单独放重置是一个破坏性操作——会丢失当前的力度和网格设置。放在右边、用灰色文字降低误触风险。AI 结果指示器if(this.aiResult.length0){Row(){Row(){}.width(6).height(6).borderRadius(3).backgroundColor(this.aiLoading?#FECA57:#00B894)Text( this.aiResult).fontSize(11).fontColor(T2)}.width(100%).padding({left:18,right:18,top:2,bottom:2})}AI 分析完成后在操作栏下方显示结果黄色圆点 “分析中…”加载中绿色圆点 “iPhone 15 · 12 部件”成功绿色圆点 “AI 分析失败: …”失败踩坑记录1. ArkTS 的状态刷新陷阱// 直接修改数组元素——不会触发 UI 更新this.pc[i].nresult.parts[i].name// 必须创建新数组——才能触发 UI 更新constupdated:PCard[][]for(letj:number0;jthis.pc.length;j){updated.push(this.pc[j])}this.pcupdatedArkTS 的State是引用类型。修改数组内部属性不会改变引用所以不会触发重新渲染。必须创建新数组替换整个引用。2. QuickBtn 的 active 判断this.QuickBtn(50%,(){this.setV(0.5)},this.val0.5,#FECA57)this.val 0.5是精确比较。但val是从 Slider 的v / 100计算的可能存在浮点精度问题。比如用户拖到 50%val可能是 0.4999999999 而不是 0.5。建议用范围判断active:Math.abs(this.val-0.5)0.013. ParticleField 的性能粒子场是持续动画——即使用户不在拆解页粒子也在跑。如果粒子数量多或动画复杂可能影响电池续航。建议只在拆解页tab1时启用粒子动画其他 Tab 时暂停动画4. AI 分析的 mock 数据constmockBase64:stringmock_image_dataconstresult:AiAnalysisResultawaitanalyzeImage(mockBase64)当前传入的是假数据。真正的实现需要从this.uri读取图片编码为 base64调用 AI API处理超时和错误在真正接入 AI 之前建议在AiAnalysisService里做好 mock 返回方便前端调试。写在最后AI 分析的接入让拆解从程序自动命名升级到智能识别命名。用户拍一张 iPhone 的照片AI 能告诉它这是 iPhone 15屏幕、电池、摄像头、主板……——这比部件 1、部件 2有用得多。粒子场的加入让工作区有了呼吸感。虽然只是视觉效果但它让整个 App 从工具变成了产品——用户能感受到开发者在细节上花了心思。当前的 AI 分析还是 mock 状态。后续接入真实的 AI 服务后这个功能会成为 App 的核心卖点拍照 → AI 识别 → 爆炸展示 → 分享给朋友。