HarmonyOS 3D 拆解分析:发现 Tab 与网络层接入
前言
之前的 3D 拆解分析是纯本地应用——拍照、切分、爆炸,所有操作都在设备上完成。但一个工具类 App 如果只有"自己用"的功能,用户粘性很低。
这次迭代加了"发现"Tab——一个内容 feed 页面,展示其他用户的拆解分析。虽然目前用的是 mock 数据,但网络层已经搭好了,后续接入真实 API 只需要改 BASE_URL。
架构变化:从 3 Tab 到 4 Tab
导航栏扩展
Row() {
this.TabBtn(0, '首页')
this.TabBtn(1, '拆解')
this.TabBtn(2, '发现')
this.TabBtn(3, '历史')
}

四个 Tab 的图标:
private tabIcon(idx: number, active: boolean): Resource {
if (active) {
if (idx === 0) return $r('sys.symbol.house_fill')
if (idx === 1) return $r('sys.symbol.scope')
if (idx === 2) return $r('sys.symbol.star_fill')
return $r('sys.symbol.clock')
}
if (idx === 0) return $r('sys.symbol.house')
if (idx === 1) return $r('sys.symbol.scope')
if (idx === 2) return $r('sys.symbol.star')
return $r('sys.symbol.clock')
}

发现 Tab 用星星图标(sys.symbol.star / sys.symbol.star_fill)——“发现"本质上是一个推荐/精选内容流,星星图标暗示"优质内容”。
四个 Tab 的职责
| Tab | 图标 | 职责 |
|---|---|---|
| 首页 | house | 场景选择、功能入口 |
| 拆解 | scope | 分析工作台 |
| 发现 | star | 内容浏览、社区 feed |
| 历史 | clock | 个人分析记录 |
四个 Tab 的信息架构:
- 首页和发现是"浏览型"——用户被动接收信息
- 拆解是"操作型"——用户主动交互
- 历史是"回顾型"——用户查看过去的行为
网络层接入
import 变化
// 旧版
import { generateId } from '../services/AnalysisModel'
import { recommendGrid, segmentImage } from '../services/SegmentationService'
// 新版
import { generateId } from '../services/AnalysisModel'
import { recommendGrid, segmentImage } from '../services/SegmentationService'
import { getDiscoverFeed, uploadAnalysis, ApiResponse } from '../services/NetworkService'

新增了 NetworkService 的三个导出:
getDiscoverFeed:获取发现页 feed 数据uploadAnalysis:上传分析结果(后续用)ApiResponse:API 响应的类型定义
异步数据加载
@State discoverLoading: boolean = false
@State discoverItems: DiscoverItem[] = []
aboutToAppear(): void {
this.loadDiscover()
}
private async loadDiscover(): Promise<void> {
this.discoverLoading = true
const resp: ApiResponse = await getDiscoverFeed(1)
this.discoverLoading = false
if (resp.code === 200 && resp.data.length > 0) {
this.discoverItems = this.getMockDiscover()
} else {
this.discoverItems = this.getMockDiscover()
}
}

当前的实现逻辑:
aboutToAppear时调用loadDiscover- 发起 API 请求,设置
discoverLoading = true - 请求完成后,无论成功还是失败,都用 mock 数据
这是典型的"渐进式接入"——先把网络层搭好,UI 做好 loading 状态,但数据源暂时用 mock。后续只需要改 loadDiscover 里的数据解析逻辑,就能无缝切换到真实 API。
Mock 数据
private getMockDiscover(): DiscoverItem[] {
return [
{ title: 'iPhone 15 拆解', desc: '专业维修指南 · 12 部件 · 4×3 网格', likes: '👍 128', color: '#FF6B6B' },
{ title: 'MacBook 主板分析', desc: '芯片级拆解 · 16 部件 · 4×4 网格', likes: '👍 96', color: '#5DADE2' },
{ title: 'AirPods 内部结构', desc: '微型设备拆解 · 6 部件 · 2×2 网格', likes: '👍 203', color: '#00B894' },
{ title: 'Switch 手柄拆解', desc: '游戏设备维修 · 9 部件 · 3×3 网格', likes: '👍 74', color: '#FECA57' },
{ title: '机械键盘轴体', desc: '外设深度拆解 · 9 部件 · 3×3 网格', likes: '👍 51', color: A },
{ title: '无人机电路分析', desc: '飞行器拆解 · 12 部件 · 4×3 网格', likes: '👍 37', color: '#6C5CE7' }
]
}

6 个 mock 项目,覆盖不同设备类型(手机、笔记本、耳机、游戏机、外设、无人机),每个项目有标题、描述、点赞数、主题色。
为什么 mock 数据要这么丰富?
因为发现页是用户看到的第一个"内容型"页面——它决定了用户对 App 内容质量的第一印象。如果 mock 数据只有"测试 1"“测试 2”,用户会觉得这个 App 是半成品。
DiscoverItem 数据模型
interface DiscoverItem {
title: string // 标题
desc: string // 描述
likes: string // 点赞数(带 emoji)
color: string // 主题色
}
四个字段,尽量精简。likes 用 string 类型而不是 number,是因为它包含了 emoji 前缀(👍 128)。如果后续需要排序或筛选,可以加一个 likeCount: number 字段。
发现页 UI
页面结构
@Builder DiscoverTab() {
Column() {
// 返回按钮
Row() { ... }
// 标题
Text('发现拆解').fontSize(20).fontWeight(FontWeight.Bold)
// 网络状态指示器
Row() {
Row() {}.width(6).height(6).borderRadius(3)
.backgroundColor(this.discoverLoading ? '#FECA57' : '#00B894')
Text(this.discoverLoading ? ' 加载中...' : ' 在线').fontSize(11).fontColor(T3)
}
// 内容列表
Scroll() {
Column({ space: 12 }) {
ForEach(this.discoverItems, (item: DiscoverItem) => {
this.DiscoverCard(item.title, item.desc, item.likes, item.color)
})
Text('—— 上拉加载更多 ——').fontSize(12).fontColor(T3)
}
}
}
}
网络状态指示器
Row() {
Row() {}.width(6).height(6).borderRadius(3)
.backgroundColor(this.discoverLoading ? '#FECA57' : '#00B894')
Text(this.discoverLoading ? ' 加载中...' : ' 在线').fontSize(11).fontColor(T3)
}
两个状态:
- 加载中:黄色圆点 + “加载中…”
- 在线:绿色圆点 + “在线”
这个指示器很轻量——只占一行,但给了用户明确的网络状态反馈。用户看到黄色圆点就知道"数据正在加载",看到绿色圆点就知道"数据已就绪"。
发现卡片
@Builder DiscoverCard(title: string, desc: string, likes: string, color: string) {
Row() {
// 左侧:彩色图标徽章
Column() {}.width(44).height(44).borderRadius(12)
.backgroundColor(color + '18')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Stack() {
Text('🔧').fontSize(18)
}
// 中间:标题和描述
Column() {
Text(title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(T1)
Text(desc).fontSize(11).fontColor(T3).margin({ top: 2 })
}.alignItems(HorizontalAlign.Start).margin({ left: 12 }).layoutWeight(1)
// 右侧:点赞数和箭头
Column() {
Text(likes).fontSize(11).fontWeight(FontWeight.Medium).fontColor(color)
SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(12).fontColor(['#DDD'])
.margin({ top: 4 })
}.alignItems(HorizontalAlign.Center)
}
.width('100%').padding(12).backgroundColor(CW).borderRadius(14)
.border({ width: 1, color: '#F0EEF6' })
}
卡片结构:
- 左侧徽章:44×44 的圆角矩形,背景色是主题色的 10% 透明度版本(
color + '18'),里面放 🔧 emoji - 中间内容:标题(15px 粗体)+ 描述(11px 灰色)
- 右侧操作:点赞数(彩色)+ 右箭头
卡片之间用 Column({ space: 12 }) 保持 12px 间距。
"上拉加载更多"占位
Text('—— 上拉加载更多 ——').fontSize(12).fontColor(T3).width('100%')
.textAlign(TextAlign.Center).margin({ top: 8, bottom: 30 })
目前是静态文字,没有实现真正的分页加载。但位置和样式已经预留好了——后续实现分页时,只需要把这段文字替换成 loading 指示器或"没有更多了"提示。
API 占位符的处理
从截图可以看到:
当前 API 地址 https://api.ershou.example.com 是占位符。要使联网真正生效,需要:
1. 部署后端服务器 — 替换 NetworkService.ets 中的 BASE_URL
2. 或接入第三方 API — 如 AI 视觉识别服务
构建,联网功能已就绪。
这是一个务实的做法:
- 先把网络层代码写好,UI 做好 loading/error 状态
- 用占位 URL 标记需要替换的地方
- 在编译输出中提醒开发者需要做什么
比"等后端好了再写前端"高效得多——前端可以独立开发和测试,后端就绪后只需要改一个常量。
踩坑记录
1. async/await 在 ArkTS 中的使用
private async loadDiscover(): Promise<void> {
this.discoverLoading = true
const resp: ApiResponse = await getDiscoverFeed(1)
this.discoverLoading = false
// ...
}
ArkTS 支持 async/await,但需要注意:
aboutToAppear中调用async方法不需要await——它是"发射后不管"的模式@State变量在async回调中修改后会触发 UI 更新——和同步代码一样- 错误处理建议用
try/catch,当前代码没有处理网络异常
2. ForEach 的数据源变化
ForEach(this.discoverItems.length > 0 ? this.discoverItems : this.getMockDiscover(),
(item: DiscoverItem) => {
this.DiscoverCard(item.title, item.desc, item.likes, item.color)
})
这里用三元运算符决定数据源——有真实数据用真实数据,没有就用 mock。但 getMockDiscover() 每次调用都创建新数组,会导致 ForEach 重新渲染。
更好的做法:
private get displayItems(): DiscoverItem[] {
return this.discoverItems.length > 0 ? this.discoverItems : this.mockItems
}
在 aboutToAppear 中预计算 mock 数据,避免每次渲染都创建新数组。
3. 颜色透明度的计算
.backgroundColor(color + '18')
'18' 是十六进制的 24,对应约 9% 的透明度(24/255 ≈ 0.09)。这个值是手动调的——太淡看不见,太深抢焦点。
不同颜色的 + '18' 效果不同:
- 红色
#FF6B6B+'18'→ 浅红 - 紫色
#8A5DFA+'18'→ 浅紫 - 绿色
#00B894+'18'→ 浅绿
如果需要更精确的透明度控制,可以用 rgba() 格式。
4. Tab 切换时的网络请求
loadDiscover 在 aboutToAppear 中调用——App 启动时就发起请求,不管用户是否切换到发现 Tab。
两种策略:
- 预加载(当前方案):启动时就加载,切换到发现 Tab 时数据已就绪
- 懒加载:切换到发现 Tab 时才加载,节省网络资源
预加载适合数据量小、请求快的场景。如果 API 响应慢或数据量大,建议改成懒加载。
写在最后
发现 Tab 的加入让 App 从"工具"变成了"工具 + 内容平台"。用户不只是用来拆解自己的东西,还能看到别人拆解了什么——这增加了用户粘性和回访率。
网络层的接入采用了"渐进式"策略——先搭好骨架,用 mock 数据填充,后续无缝切换到真实 API。这种做法在前端开发中很常见:不被后端进度阻塞,同时保证最终接入时改动最小。
当前的 mock 数据已经覆盖了多种设备类型,给用户一个"这个 App 能做什么"的直观印象。后续接入真实 API 时,只需要替换数据源,UI 层不需要任何改动。
更多推荐



所有评论(0)