引子:数据有了,怎么展示才好看

重构后的星空运势应用,星座数据从原来的 5 个字段扩展到了 8 个——新增了优势(strengths)、弱点(weaknesses)、最佳配对(matches)。数据丰富了,但展示是个问题:三种不同类型的信息,怎么排列才清晰?

如果只是把文字堆上去,页面会变成"星座百科",密密麻麻全是字,用户看两眼就划走了。得用卡片、标签、图标把信息分层,让用户一眼就能抓住重点。

完整效果
在这里插入图片描述
在这里插入图片描述

数据结构:三种信息的建模

星座的详情数据分三类:

class SignData {
  name: string = '';        // 基础信息
  emoji: string = '';
  date: string = '';
  elem: string = '';
  trait: string = '';
  stars: StarPt[] = [];     // 星点坐标
  strengths: string = '';   // 优势(文本)
  weaknesses: string = '';  // 弱点(文本)
  matches: string[] = [];   // 最佳配对(数组)
}

在这里插入图片描述

为什么不用嵌套对象?

比如有人会这样设计:

class SignData {
  detail: {
    strengths: string;
    weaknesses: string;
    matches: string[];
  }
}

嵌套对象的好处是"分组明确",但在这个场景下不太合适:

  1. 扁平结构更简单:访问 sign.strengthssign.detail.strengths 简洁
  2. 类型安全:ArkTS 对扁平对象的类型推断更准确
  3. 序列化方便:如果要存到数据库或网络传输,扁平结构更容易处理

数组 vs 字符串

matchesstring[] 而不是 string,因为:

  • 一个星座可能有多个最佳配对(比如天秤座、水瓶座)
  • 数组方便遍历渲染,不需要手动拆分字符串
  • 类型更明确,编译器能检查越界

UI 结构:信息的层次化呈现

星图详情的 UI 分四个层次:

┌─────────────────────────────────┐
│      星点可视化(圆形区域)       │  ← 视觉焦点
│   · · · · · · ·                │
│     · ♊ ·                      │
│   · · · · · · ·                │
├─────────────────────────────────┤
│  风象星座  5.21-6.21            │  ← 基础标签
│  聪明灵活·好奇心强               │  ← 一句话特质
├─────────────────────────────────┤
│  ✨ 优势                        │  ← 分区标题
│  思维敏捷,沟通能力出众...        │  ← 文本内容
├─────────────────────────────────┤
│  ⚠️ 弱点                        │  ← 分区标题
│  注意力分散,容易半途而废...      │  ← 文本内容
├─────────────────────────────────┤
│  ❤️ 最佳配对                     │  ← 分区标题
│  天秤座  水瓶座                  │  ← 标签列表
└─────────────────────────────────┘

视觉层次的设计原则

信息的视觉层次遵循"从上到下,从重要到次要":

  1. 第一层(星点可视化):吸引注意力,让用户知道"这是什么星座"
  2. 第二层(基础标签):提供快速定位信息(元素、日期)
  3. 第三层(优势/弱点):详细描述,需要阅读理解
  4. 第四层(最佳配对):可选信息,感兴趣的用户会看

这种层次化设计让用户可以根据兴趣选择阅读深度——赶时间只看标签,有空可以看详情。

条件渲染:根据数据状态切换 UI

星图详情有三种状态:

@State selectedSign: number = -1;   // 选中的星座索引
@State showDetail: boolean = false; // 是否展开详情

三种状态的 UI 切换

状态1:未选中(selectedSign === -1)
┌─────────────────────────────┐
│     "星座星图" 标题          │
│     点击星座·探索你的星空     │
│     (空)                     │
│  ┌──────┐ ┌──────┐ ┌──────┐│
│  │ 白羊 │ │ 金牛 │ │ 双子 ││
│  └──────┘ └──────┘ └──────┘│
└─────────────────────────────┘

状态2:选中未展开(selectedSign >= 0, showDetail === false)
┌─────────────────────────────┐
│     "星座星图" 标题          │
│     双子座详情               │
│     风象星座 · 5.21-6.21    │
│     聪明灵活·好奇心强        │
│     [收起星图]               │
│  ┌──────┐ ┌──────┐ ┌──────┐│
│  │ 白羊 │ │ 金牛 │ │ 双子 ││  ← 双子选中
│  └──────┘ └──────┘ └──────┘│
└─────────────────────────────┘

状态3:选中并展开(selectedSign >= 0, showDetail === true)
┌─────────────────────────────┐
│     星点可视化               │
│     风象星座 · 5.21-6.21    │
│     聪明灵活·好奇心强        │
│     ✨ 优势                 │
│     ⚠️ 弱点                 │
│     ❤️ 最佳配对             │
│     [收起详情] [关闭星图]    │
│  ┌──────┐ ┌──────┐ ┌──────┐│
│  │ 白羊 │ │ 金牛 │ │ 双子 ││
│  └──────┘ └──────┘ └──────┘│
└─────────────────────────────┘

状态切换的代码

// 点击星座
onClick(() => {
  if (this.selectedSign === i) {
    // 再次点击同一个:收起详情
    this.selectedSign = -1;
    this.showDetail = false;
  } else {
    // 点击新的:展开详情
    this.selectedSign = i;
    this.showDetail = true;
  }
})

// 收起详情按钮
.onClick(() => { this.showDetail = false; })

// 关闭星图按钮
.onClick(() => {
  this.selectedSign = -1;
  this.showDetail = false;
})

这种"点击切换"的交互模式很常见——用户可以点击查看详情,再点击收起,不需要单独的"关闭"按钮。但这个应用加了两个按钮:

  • “收起详情”:只收起详情,不取消选中(用户可能还想看其他星座)
  • “关闭星图”:取消选中,回到初始状态

两个按钮的区分让用户操作更精确。

分区展示:优势、弱点、配对

三个分区用类似的结构展示,但内容类型不同:

优势/弱点:纯文本

Column() {
  Row() {
    Text('✨').fontSize(14)
    Text('优势').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E8DFF8')
  }
  Text(SIGNS[this.selectedSign].strengths)
    .fontSize(12).fontColor('#BBAAE0').lineHeight(20).margin({ top: 6 })
}
.width('100%').padding(12)
.backgroundColor('#0E0B25').borderRadius(10)
.border({ width: 0.5, color: '#1C1545', style: BorderStyle.Solid })
.margin({ bottom: 8 })

设计要点

  1. 标题区:emoji + 文字,用 Row 水平排列
  2. 内容区:单独的 Text 组件,设置 lineHeight 保证行距舒适
  3. 背景色:深色卡片 #0E0B25,和页面背景 #06060F 有区分
  4. 边框:极细的 0.5px 边框,增加层次感但不抢眼

最佳配对:标签列表

Column() {
  Row() {
    Text('❤️').fontSize(14)
    Text('最佳配对').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E8DFF8')
  }
  Row() {
    ForEach(SIGNS[this.selectedSign].matches, (m: string) => {
      Text(m).fontSize(12).fontColor('#FFD700')
        .padding({ left: 10, right: 10, top: 4, bottom: 4 })
        .borderRadius(12)
        .backgroundColor('#1C1545')
        .border({ width: 0.5, color: '#3B2F78', style: BorderStyle.Solid })
        .margin({ right: 8 })
    })
  }
  .margin({ top: 6 })
}
.width('100%').padding(12)
.backgroundColor('#0E0B25').borderRadius(10)
.border({ width: 0.5, color: '#1C1545', style: BorderStyle.Solid })

标签的设计

  • 圆角borderRadius(12) 让标签看起来像药丸
  • 背景色#1C1545 比卡片背景略亮,区分内容层次
  • 文字色:金色 #FFD700,和标签背景形成对比
  • 间距margin({ right: 8 }) 让标签之间有呼吸感

为什么配对用标签而不是文本?

如果把"天秤座 水瓶座"写成纯文本,和优势/弱点混在一起,用户很难区分。用标签有三个好处:

  1. 视觉区分:标签的形状和颜色与文本不同,用户一眼能看出"这是配对信息"
  2. 可交互性:标签可以点击跳转到对应星座(虽然当前版本没做,但预留了可能性)
  3. 可扩展性:如果以后要加更多配对,标签可以自动换行排列

数据驱动的 UI 更新

整个详情区域的 UI 完全由数据驱动:

if (this.selectedSign >= 0) {
  // 数据变化 → UI 自动更新
  Column() {
    // 星点可视化
    // 基础标签
    // 优势卡片
    // 弱点卡片
    // 配对卡片
  }
}

selectedSign-1 变为有效值时,详情区域自动渲染。当 selectedSign 变回 -1 时,详情区域自动销毁。

这种"数据驱动"的方式比手动操作 UI 简洁得多——不需要写 document.getElementById('xxx').style.display = 'block' 这种命令式代码。

动画的配合

详情区域的展开/收起配合了动画效果:

Column() {
  // 详情内容
}
.animation({ duration: 300 })

animation({ duration: 300 }) 让详情区域在 300ms 内渐入渐出。但这里有个细节——animation() 加在容器上,而不是每个子组件上。

为什么加在容器上?

如果给每个子组件都加 animation(),展开时四个卡片会同时渐入,视觉上很混乱。加在容器上,整个详情区域作为一个整体渐入,更干净。

如果要更精细的动画(比如卡片依次渐入),可以用 animation()delay 参数:

Column() {
  // 卡片1
  .animation({ duration: 300, delay: 0 })
  // 卡片2
  .animation({ duration: 300, delay: 100 })
  // 卡片3
  .animation({ duration: 300, delay: 200 })
}

但当前版本用整体渐入已经够用了。

踩坑记录

坑 1:ForEach 的 key 管理

ForEach 遍历配对数组时,需要确保每个元素有唯一的 key。如果数组中有重复项(比如两个星座都配对"天秤座"),可能会导致渲染错误。

解决方案是用索引作为 key:

ForEach(SIGNS[this.selectedSign].matches, (m: string, i: number) => {
  Text(m).key(`match_${i}`)
})

坑 2:详情区域的高度计算

详情区域的高度是动态的——优势文本长,高度就高;配对标签多,高度也高。如果容器高度固定,内容可能会被裁切。

解决方案是不设固定高度,让内容自动撑开:

Column() {
  // 详情内容
}
.width('100%')  // 不设 height,让内容自动撑开

坑 3:选中状态的视觉反馈

选中的星座用金色边框(#FFD700)和深色背景(#1C1545)区分。但如果用户色弱,可能看不清颜色差异。

解决方案是加一个额外的视觉线索——比如选中的星座 emoji 放大:

Text(s.emoji)
  .fontSize(i === this.selectedSign ? 36 : 28)
  .animation({ duration: 300 })

颜色 + 大小双重反馈,容错性更高。

坑 4:详情区域的滚动

如果详情内容很长(比如优势文本特别长),可能会超出屏幕。当前版本没有加滚动功能,内容会被裁切。

解决方案是给详情区域加 Scroll 组件:

Scroll() {
  Column() {
    // 详情内容
  }
}
.height(300)  // 限制最大高度
.scrollable(ScrollDirection.Vertical)

但当前版本的内容量不大,不加滚动也能接受。

代码改进建议

1. 详情区域组件化

当前详情区域的代码直接写在 StarView 里,如果以后要做"星座详情弹窗",需要复制粘贴。建议提取成独立组件:

@Component
struct SignDetail {
  @Prop sign: SignData;
  
  build() {
    Column() {
      // 详情内容
    }
  }
}

2. 配对的点击跳转

当前配对标签是静态的。如果用户点击"天秤座"标签,可以跳转到天秤座的详情:

.onClick(() => {
  const idx = SIGNS.findIndex(s => s.name === m);
  if (idx >= 0) { this.selectedSign = idx; }
})

这种"关联跳转"让用户可以快速浏览相关星座。

3. 数据的持久化

当前星座数据是硬编码的。如果以后要支持"自定义星座"或"编辑详情",需要把数据存到本地。可以用 Preferences API 或关系型数据库。

4. 响应式布局

当前详情区域固定宽度。在大屏设备上(比如平板),可以改成双栏布局:

Row() {
  Column() { /* 优势 */ }.layoutWeight(1)
  Column() { /* 弱点 */ }.layoutWeight(1)
}

总结

星图详情页的核心是"数据驱动 UI"——把结构化数据用层次化的可视化方式呈现。三种不同类型的信息(文本、标签、列表)用不同的组件和样式展示,让用户一眼就能抓住重点。

适用边界:这个部分适合用作 ArkUI 数据驱动展示的学习案例,涵盖了条件渲染、分区布局、标签组件、动画配合等核心知识点。但如果要上架应用商店,还需要补充滚动支持、配对跳转、数据持久化、响应式布局等内容。建议在此基础上逐步扩展,而不是一次性做完所有功能。

说白了,详情页就是"把数据摆好看"。数据结构设计好了,UI 就是套模板;数据结构没设计好,UI 怎么改都别扭。

Logo

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

更多推荐