前言

上一版的发现页有一个问题:所有卡片都用同一个🔧图标。不管是 iPhone 拆解还是无人机电路分析,左边的徽章都是一个扳手。用户扫一眼列表,很难快速区分"这是什么东西的拆解"。

这次改造的核心是"让每个项目有自己的图标"——iPhone 用📱,MacBook 用💻,AirPods 用🎧。用户不需要读标题,看图标就知道是什么设备。

DiscoverItem 的字段升级

旧版

interface DiscoverItem {
  title: string
  desc: string
  likes: string   // '👍 128'
  color: string
}

新版

interface DiscoverItem {
  title: string
  desc: string
  icon: string    // 📱💻🎧🎮⌨️🚁
  likes: string   // '128'(不再带 emoji)
  color: string
}

在这里插入图片描述

两个变化:

  1. 新增 icon 字段:设备专属的 emoji
  2. likes 简化:去掉了 👍 前缀,只保留数字字符串

为什么去掉 likes 的 emoji 前缀?

因为卡片上已经加了一个心形图标(sys.symbol.heart),不需要再用 emoji 表示"点赞"。如果心形图标 + 👍 128 同时出现,信息重复。

Mock 数据:每个设备一个图标

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

在这里插入图片描述

六个设备,六个不同的 emoji:

设备 Emoji 为什么选这个
iPhone 📱 手机的通用符号
MacBook 💻 笔记本电脑
AirPods 🎧 耳机
Switch 🎮 游戏手柄
键盘 ⌨️ 键盘本身
无人机 🚁 直升机/飞行器

这些 emoji 都是 Unicode 标准的一部分,在所有设备上都能正常显示。

DiscoverCard 的重构

旧版:传四个参数

@Builder DiscoverCard(title: string, desc: string, likes: string, color: string) {
  Row() {
    Column() {}.width(44).height(44).borderRadius(12)
      .backgroundColor(color + '18')
    Stack() {
      Text('🔧').fontSize(18)  // 所有卡片都是🔧
    }
    // ...
  }
}

新版:传整个对象

@Builder DiscoverCard(item: DiscoverItem) {
  Row() {
    // 设备图标徽章
    Column() {
      Text(item.icon).fontSize(24)
    }.width(48).height(48).borderRadius(14)
      .backgroundColor(item.color + '15')
      .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

    // 标题和描述
    Column() {
      Text(item.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(T1)
      Text(item.desc).fontSize(11).fontColor(T2).margin({ top: 2 })
    }

    // 点赞和箭头
    Column() {
      Row({ space: 3 }) {
        SymbolGlyph($r('sys.symbol.heart')).fontSize(11).fontColor([item.color])
        Text(item.likes).fontSize(12).fontWeight(FontWeight.Medium).fontColor(item.color)
      }
      SymbolGlyph($r('sys.symbol.chevron_right')).fontSize(12).fontColor(['#DDD'])
        .margin({ top: 6 })
    }
  }
  .width('100%').padding(14).backgroundColor(CW).borderRadius(16)
  .border({ width: 1, color: '#F0EEF6' })
  .shadow({ radius: 4, color: 'rgba(0,0,0,0.02)', offsetY: 1 })
}

在这里插入图片描述

为什么改成传对象?

  1. 可扩展性:后续加字段(比如 viewCountauthor)不需要改 Builder 的参数列表
  2. 类型安全:传对象时 TypeScript 会检查字段名是否正确,传参数时不会
  3. 代码可读性item.iconicon 参数更明确地表达了"这是什么"

图标徽章的变化

// 旧版
.width(44).height(44).borderRadius(12)
Text('🔧').fontSize(18)

// 新版
.width(48).height(48).borderRadius(14)
Text(item.icon).fontSize(24)

三个变化:

  1. 尺寸从 44×44 增大到 48×48——图标是卡片的视觉焦点,大一点更醒目
  2. 圆角从 12 增大到 14——和卡片的圆角(16)更协调
  3. 字号从 18 增大到 24——emoji 在更大的容器里需要更大的字号才能居中

背景色透明度微调

// 旧版
.backgroundColor(color + '18')  // 约 9% 透明度

// 新版
.backgroundColor(item.color + '15')  // 约 8% 透明度

'18'(24/255 ≈ 9.4%)降到 '15'(21/255 ≈ 8.2%)。因为图标从🔧变成了彩色 emoji——emoji 本身已经有颜色,背景色再深就会和图标"抢色"。

点赞区域:心形图标

Row({ space: 3 }) {
  SymbolGlyph($r('sys.symbol.heart')).fontSize(11).fontColor([item.color])
  Text(item.likes).fontSize(12).fontWeight(FontWeight.Medium).fontColor(item.color)
}

用系统心形图标(sys.symbol.heart)+ 数字的组合,替代了旧版的 👍 128。好处:

  1. 心形图标和卡片主题色一致,视觉更协调
  2. 数字更紧凑,不会因为 emoji 宽度导致对齐问题
  3. sys.symbol.heart 是矢量图标,在不同分辨率下都清晰

卡片阴影

// 旧版:无阴影

// 新版
.shadow({ radius: 4, color: 'rgba(0,0,0,0.02)', offsetY: 1 })

极淡的阴影(2% 透明度),给卡片一个"微微浮起"的感觉。在紫色背景上,白色卡片 + 极淡阴影 = 层次感。

调用方式的变化

旧版

ForEach(this.discoverItems,
  (item: DiscoverItem) => {
    this.DiscoverCard(item.title, item.desc, item.likes, item.color)
  })

新版

ForEach(this.discoverItems,
  (item: DiscoverItem) => {
    this.DiscoverCard(item)
  })

从传四个参数变成传一个对象。调用代码更简洁,Builder 的参数列表也更短。

视觉效果对比

旧版卡片

┌─────────────────────────────┐
│ ┌────┐                     │
│ │ 🔧 │  iPhone 15 拆解      │
│ └────┘  专业维修指南...      │
│                      👍 128 │
│                             │
└─────────────────────────────┘

所有卡片左边都是🔧,需要读标题才能区分。

新版卡片

┌─────────────────────────────┐
│ ┌────┐                     │
│ │ 📱 │  iPhone 15 拆解      │
│ └────┘  专业维修指南...      │
│                     ♡ 128   │
│                             │
└─────────────────────────────┘

┌─────────────────────────────┐
│ ┌────┐                     │
│ │ 💻 │  MacBook 主板分析     │
│ └────┘  芯片级拆解...        │
│                     ♡ 96    │
│                             │
└─────────────────────────────┘

每个卡片左边是不同的设备图标,扫一眼就能区分。

踩坑记录

1. emoji 的宽度不一致

不同 emoji 的显示宽度不同——📱 比 🔧 宽,⌨️ 比 🎮 窗。在 48×48 的容器里,有些 emoji 会显得"挤",有些会显得"空"。

解决方案:用 fontSize(24) 统一字号,让 emoji 自然居中。如果某个 emoji 显示效果不好,可以微调 fontSize

2. sys.symbol.heart 的颜色

SymbolGlyph($r('sys.symbol.heart')).fontSize(11).fontColor([item.color])

心形图标的颜色跟随卡片主题色。但如果主题色是黄色(#FECA57),心形图标在白色背景上对比度不够。这是多色系统的一个已知限制——亮色系的主题色在小图标上可读性较差。

3. ForEach 的 keyGenerator

当前的 ForEach 没有指定 keyGenerator。ArkUI 会用默认的索引作为 key。如果列表顺序变化(比如排序、插入),可能导致动画异常。

建议后续加上:

ForEach(this.discoverItems,
  (item: DiscoverItem) => {
    this.DiscoverCard(item)
  },
  (item: DiscoverItem) => item.title)  // 用标题作为 key

4. 异步数据和 mock 数据的切换

ForEach(this.discoverItems.length > 0 ? this.discoverItems : this.getMockDiscover(),
  (item: DiscoverItem) => {
    this.DiscoverCard(item)
  })

getMockDiscover()ForEach 的三元表达式中被调用——每次渲染都会创建新数组。如果 discoverItems 为空(比如 API 还没返回),每次渲染都会重新创建 6 个 mock 对象。

这个问题在上一篇文章中已经提过,这次仍然存在。建议在 aboutToAppear 中预计算 mock 数据。

写在最后

发现卡片的个性化改造解决了一个核心问题:信息辨识度。旧版的🔧图标让用户需要读文字才能区分项目,新版的设备 emoji 让用户扫一眼就知道"这是什么"。

图标的选择遵循一个原则:用最常见的符号。📱代表手机、💻代表电脑、🎧代表耳机——这些符号已经被用户广泛认知,不需要学习成本。

后续如果接入真实 API,后端需要为每个拆解项目提供 icon 字段。可以是预定义的 emoji 列表,也可以是自定义图标(需要处理跨平台兼容性)。

Logo

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

更多推荐