前言

上一版的发现页有一个体验问题:点击一个拆解卡片后跳到拆解页,拆解完想回到发现页,得先回首页再切到发现。因为拆解页的返回按钮是硬编码 this.tab = 0(回首页),不管你从哪个 Tab 进来的。

这次改动解决两个问题:

  1. 返回按钮记住"你从哪来的"——从发现页进来就回发现页,从首页进来就回首页
  2. 发现卡片可点击——点一个 iPhone 拆解,直接带着 4×3 网格参数进入拆解页

prevTab:导航记忆的核心

新增状态变量

@State tab: number = 0
@State prevTab: number = 0   // 新增:记录上一个 Tab

prevTab 只做一件事:记录用户切换 Tab 之前在哪个页面。

TabBtn 的变化

@Builder TabBtn(idx: number, label: string) {
  Column({ space: 3 }) {
    SymbolGlyph(this.tabIcon(idx, this.tab === idx))
      .fontSize(22).fontColor([this.tab === idx ? A : '#C0BFC6'])
    Text(label).fontSize(10).fontColor(this.tab === idx ? A : '#C0BFC6')
  }.onClick(() => { this.prevTab = this.tab; this.tab = idx })
  //    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}

在这里插入图片描述

关键变化: onClick 里先 this.prevTab = this.tab,再 this.tab = idx

这行代码的执行顺序很重要:

  1. this.prevTab = this.tab —— 把当前 Tab 存到 prevTab
  2. this.tab = idx —— 切换到新 Tab

比如用户在发现页(tab=2)点击历史 Tab(idx=3):

  • prevTab = 2(发现页)
  • tab = 3(历史页)

之后在历史页点击返回按钮:

  • this.tab = this.prevTab → tab = 2(回到发现页)

startAnalysis 也记录 prevTab

private startAnalysis(uri: string, c: number, r: number, n: string): void {
  this.uri = uri; this.cols = c; this.rows = r; this.name = n
  this.val = 0; this.on = false; this.list = false
  this.rebuildAnalysis()
  this.prevTab = this.tab   // 记录当前 Tab
  this.tab = 1              // 切到拆解页
}

在这里插入图片描述

从首页点"开始使用" → prevTab=0,tab=1
从发现页点卡片 → prevTab=2,tab=1

拆解页的返回按钮根据 prevTab 决定去哪。

返回按钮的统一改造

拆解页

// 旧版
.onClick(() => { this.tab = 0 })  // 永远回首页

// 新版
.onClick(() => { this.tab = this.prevTab })  // 回到上一个 Tab

发现页

// 旧版
.onClick(() => { this.tab = 0 })

// 新版
.onClick(() => { this.tab = this.prevTab })

历史页

// 旧版
.onClick(() => { this.tab = 0 })

// 新版
.onClick(() => { this.tab = this.prevTab })

在这里插入图片描述

三个子页面的返回按钮逻辑完全一致:回到 prevTab。

为什么不用 router.back()?

因为这是单页应用——所有 Tab 都在同一个 Index 组件里,没有路由栈。Tab 切换只是 this.tab 的值变化,不涉及页面压栈/出栈。所以不能用 router.back(),只能用状态变量控制。

发现卡片的数据升级

DiscoverItem 接口

// 旧版
interface DiscoverItem {
  title: string; desc: string; icon: string; likes: string; color: string
}

// 新版
interface DiscoverItem {
  title: string; desc: string; icon: string; likes: string; color: string;
  cols: number; rows: number   // 新增:网格参数
}

新增 colsrows 字段,让每个发现项目携带自己的网格配置。

Mock 数据

private getMockDiscover(): DiscoverItem[] {
  return [
    { title: 'iPhone 15 拆解', desc: '...', icon: '📱', likes: '128', color: '#FF6B6B', cols: 4, rows: 3 },
    { title: 'MacBook 主板分析', desc: '...', icon: '💻', likes: '96', color: '#5DADE2', cols: 4, rows: 4 },
    { title: 'AirPods 内部结构', desc: '...', icon: '🎧', likes: '203', color: '#00B894', cols: 2, rows: 3 },
    { title: 'Switch 手柄拆解', desc: '...', icon: '🎮', likes: '74', color: '#FECA57', cols: 3, rows: 3 },
    { title: '机械键盘轴体', desc: '...', icon: '⌨️', likes: '51', color: A, cols: 3, rows: 3 },
    { title: '无人机电路分析', desc: '...', icon: '🚁', likes: '37', color: '#6C5CE7', cols: 4, rows: 3 }
  ]
}

在这里插入图片描述

每个项目的网格参数:

设备 网格 部件数 为什么这么配
iPhone 4×3 12 手机零件多,需要较细的切分
MacBook 4×4 16 主板芯片密集,最细网格
AirPods 2×3 6 微型设备,粗网格就够
Switch 3×3 9 中等复杂度
键盘 3×3 9 轴体排列规整
无人机 4×3 12 电路板+机械结构

发现卡片的交互闭环

旧版:只读

.onClick(() => {
  // Navigate to analysis detail (future)
})

点击卡片什么也不做——只有占位注释。

新版:可操作

.onClick(() => {
  this.startAnalysis('', item.cols, item.rows, item.title)
})

点击卡片直接进入拆解页,携带该项目的网格参数和标题。

交互流程:

发现页 → 点击"iPhone 15 拆解" → 拆解页(4×3 网格,标题"iPhone 15 拆解")
                                    ↓
                              点击返回按钮
                                    ↓
                               回到发现页(prevTab=2)

为什么 uri 传空字符串?

this.startAnalysis('', item.cols, item.rows, item.title)

uri 是图片路径。发现页的卡片是别人分享的拆解,不需要自己的照片。传空字符串时,拆解页会显示网格预览而不是真实图片——用户看到的是"这个拆解用了什么网格配置",而不是"这张照片长什么样"。

发现页头部重设计

旧版:标题和状态指示器分开

Text('发现拆解').fontSize(20).fontWeight(FontWeight.Bold)
// ... 中间有空白 ...
Row() {
  Row() {}.width(6).height(6).borderRadius(3)
    .backgroundColor(this.discoverLoading ? '#FECA57' : '#00B894')
  Text(this.discoverLoading ? ' 加载中...' : ' 在线').fontSize(11)
}

标题和状态指示器不在同一行,视觉上比较散。

新版:标题和状态指示器同行

Row() {
  // 返回按钮
  Row() { SymbolGlyph($r('sys.symbol.chevron_left'))... }
    .width(34).height(34).borderRadius(17)
    .backgroundColor('rgba(0,0,0,0.03)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => { this.tab = this.prevTab })

  Text('发现拆解').fontSize(20).fontWeight(FontWeight.Bold)
    .fontColor(T1).margin({ left: 10 })
  Blank()

  // 在线状态指示器(紧凑版)
  Row() {
    Row() {}.width(6).height(6).borderRadius(3)
      .backgroundColor(this.discoverLoading ? '#FECA57' : '#00B894')
    Text(' 在线').fontSize(11).fontColor(T3)
  }.padding({ left: 8, right: 8, top: 4, bottom: 4 })
    .backgroundColor('#F5F5F7').borderRadius(10)
}

变化:

  1. 标题和返回按钮、状态指示器在同一行
  2. 状态指示器加了灰色背景和圆角——从"文字"变成了"徽章"
  3. 去掉了"加载中…"文字——只有"在线"一个状态(加载中的时候显示黄色圆点)

热门拆解横幅

// Featured banner
Stack({ alignContent: Alignment.Center }) {
  Column() {}.width('100%').height(100).borderRadius(18)
    .linearGradient({ angle: 135, colors: [[A, 0], ['#A78BFA', 1]] })

  Circle({ width: 80, height: 80 }).fill('rgba(255,255,255,0.06)')
    .position({ x: '70%', y: -20 })
  Circle({ width: 40, height: 40 }).fill('rgba(255,255,255,0.08)')
    .position({ x: '5%', y: 50 })

  Row() {
    Column() {
      Text('🔥 热门拆解').fontSize(16).fontWeight(FontWeight.Bold).fontColor(Color.White)
      Text('看看大家都在拆什么').fontSize(11)
        .fontColor('rgba(255,255,255,0.6)').margin({ top: 2 })
    }.alignItems(HorizontalAlign.Start).layoutWeight(1).margin({ left: 18 })

    Text('📱').fontSize(36).margin({ right: 18 })
  }.width('100%')
}.width('100%').height(100).borderRadius(18)
  .margin({ left: 18, right: 18, bottom: 18 })

横幅的结构:

  1. 渐变背景:紫色渐变(和 Hero Card 一致)
  2. 装饰圆:两个半透明白色圆,营造层次感
  3. 文字:标题"热门拆解" + 副标题"看看大家都在拆什么"
  4. 右侧 emoji:📱 作为装饰

为什么加横幅?

发现页之前直接就是卡片列表,没有视觉焦点。加一个横幅:

  1. 给页面一个"起点"——用户进入发现页,第一眼看到的是横幅,然后自然向下滚动看卡片
  2. 横幅可以后续做成轮播,推荐不同的内容
  3. 和首页的 Hero Card 风格一致——整个 App 的视觉语言统一

底部提示文案

// 旧版
Text('—— 上拉加载更多 ——').fontSize(12).fontColor(T3)

// 新版
Row() {
  Text('———— 已经到底了 ————').fontSize(11).fontColor(T3)
}.width('100%').justifyContent(FlexAlign.Center)
  .margin({ top: 12, bottom: 30 })

从"上拉加载更多"改成"已经到底了"——因为当前没有分页功能, mock 数据只有 6 条。告诉用户"到底了"比"上拉加载"更诚实。

后续实现分页时,可以动态切换:

Text(this.hasMore ? '上拉加载更多' : '已经到底了')

踩坑记录

1. prevTab 的初始值

@State prevTab: number = 0

初始值是 0(首页)。这意味着 App 启动时,如果用户直接切到拆解页再返回,会回到首页——因为 prevTab 默认是 0。

这个默认值是合理的:App 启动时在首页,第一次进入拆解页一定是从首页来的。

2. startAnalysis 和 TabBtn 都写 prevTab

// TabBtn
.onClick(() => { this.prevTab = this.tab; this.tab = idx })

// startAnalysis
this.prevTab = this.tab
this.tab = 1

两处都写 prevTab,但语义不同:

  • TabBtn:用户主动切换 Tab,prevTab 记录"刚才在哪个 Tab"
  • startAnalysis:程序主动跳转(点击卡片/按钮),prevTab 记录"触发跳转时在哪个 Tab"

如果只在一处写 prevTab,另一处就会丢失记忆。

3. 发现卡片的 onClick 传空 uri

this.startAnalysis('', item.cols, item.rows, item.title)

传空 uri 时,拆解页显示网格预览而不是真实图片。但用户可能期望"点击一个拆解项目能看到完整的爆炸图"。

后续可以考虑:

  • 在 DiscoverItem 里加 imageUri 字段
  • 或者跳到一个独立的"详情页"而不是拆解页

4. 横幅没有点击事件

Stack({ alignContent: Alignment.Center }) {
  // ...
}
// 没有 .onClick()

当前横幅是纯展示,不能点击。如果后续要做"热门推荐详情",需要加 onClick。

写在最后

prevTab 是一个很小的改动——只加了一个状态变量,改了几行 onClick。但它解决了一个真实的体验问题:用户不再需要"绕路"回到之前的页面。

发现卡片的交互闭环让发现页从"只读列表"变成了"操作入口"。用户看到一个感兴趣的拆解,点一下就能用自己的照片尝试同样的网格配置——这是从"看别人做什么"到"自己动手做"的关键转化。

后续如果要做"分享我的拆解"功能,DiscoverItem 还需要加 imageUriauthortimestamp 等字段。但核心的导航记忆和交互闭环已经搭好了。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐