一、技术背景:HarmonyOS 6.1.1与ArkTS声明式UI范式

HarmonyOS 6.1.1 作为华为鸿蒙操作系统的重要迭代版本,在分布式软总线、原子化服务、ArkUI声明式开发框架等核心能力上进行了深度增强。HarmonyOS 6.1.1 延续了"一次开发,多端部署"的设计理念,同时进一步优化了 ArkTS 语言的编译效率和运行时性能。ArkTS 是鸿蒙生态的专属应用开发语言,它在 TypeScript 的基础上扩展了面向 UI 开发的语法能力,引入了装饰器系统、状态管理注解、声明式 UI 构建范式等特性,使开发者能够以更简洁、更具表达力的方式构建复杂界面。

HarmonyOS ArkTS API 24 是与 HarmonyOS 6.1.1 配套的应用程序接口集合,它涵盖了从基础 UI 组件(如 Text、Image、Button)到高级容器组件(如 Stack、Flex、Scroll),再到动画系统、状态管理、路由导航等全方位能力。基于HarmonyOS API 24 进行开发,开发者可以充分利用声明式 UI 范式的优势:UI 视图与状态数据自动绑定,状态变化驱动 UI 重新渲染,无需手动操作 DOM 或视图树,从架构层面保证了数据与视图的一致性。

声明式 UI 范式的核心思想是"描述 UI 应该是什么样子,而非如何构建它"。在 ArkTS 中,开发者通过链式调用的方式描述组件的属性、样式、事件和行为。框架负责在状态变化时高效地计算差异并更新视图。这种范式与传统的命令式 UI(如 Android View 体系、iOS UIKit 命令式操作)形成鲜明对比,它降低了状态管理的复杂度,减少了因手动视图操作带来的 bug 隐患。ArkTS 的 @Component 装饰器将一个结构体标记为自定义组件,@Entry 装饰器标记入口组件,@State 装饰器声明可观察的响应式状态变量,@Builder 装饰器定义可复用的 UI 构建函数——这套装饰器体系构成了声明式 UI 的骨架。

HarmonyOS 6.1.1 在 ArkUI 框架层面带来了多项改进:增强了 linearGradient 线性渐变的渲染性能与精度;优化了 animation 属性动画的帧率稳定性;改进了 Flex 弹性布局在换行场景下的布局算法;提升了 Scroll 组件的滚动流畅度与手势识别灵敏度。这些底层能力的增强为上层应用开发提供了坚实基础。本篇文章将以一个企业内部社交社区应用"同事圈"为案例,完整剖析从类型定义、数据建模、组件设计、状态管理到交互逻辑的全链路实现,深入讲解每一个 ArkUI 组件的用法、属性与设计考量。


二、应用业务背景:企业内部社交社区"同事圈"

"同事圈"是一个面向企业内部员工的社交社区平台,旨在为企业员工提供一个分享工作日常、参与话题讨论、报名企业活动、浏览团队相册的轻量级社交入口。应用采用珊瑚粉(Coral Pink)作为主色调,营造出温暖、活泼且不失专业的视觉氛围。底部导航栏设计了五个核心模块:动态、话题、活动、相册和我的。

动态模块是应用的核心信息流,员工可以浏览同事发布的动态内容,支持图文九宫格展示、点赞、评论、分享、举报等社交互动操作。话题模块提供热门话题榜单和数据概览,包含一个本周互动量柱状图,帮助员工了解社区活跃度趋势。活动模块展示企业组织的各类团建、培训、福利活动,支持在线报名并展示名额进度。相册模块以瀑布流网格的形式展示企业珍贵瞬间的合集,支持点赞收藏。我的模块提供个人资料卡片、互动周报数据、荣耀勋章墙以及已发布动态列表。

整个应用包含六种弹窗交互:发布动态(底部抽屉式)、评论列表、删除动态警示、举报内容、活动详情报名、编辑个人资料。此外,还实现了三种动效:点赞缩放心跳动画、发布按钮旋转动画、活动进度条增长动画。这些交互细节共同构成了一个体验完整、视觉精致的企业社交应用。


三、架构总览与组件层次设计

从架构角度看,整个应用遵循"数据层 → 类型层 → 逻辑层 → 视图层"的四层分离设计。数据层由一组全局常量数组构成,提供模拟的业务数据。类型层通过 interface 定义了严格的数据结构契约。逻辑层包含若干全局纯函数,负责数据处理与格式转换。视图层由六个自定义组件构成,分别承载不同的功能模块。

视图层

逻辑层

类型层

数据层

FEED_LIST 动态数据

TOPIC_LIST 话题数据

ACTIVITY_LIST 活动数据

ALBUM_LIST 相册数据

INTERACT_LOG 互动数据

CIRCLE_TABS Tab元数据

FeedItem 接口

TopicItem 接口

ActivityItem 接口

AlbumItem 接口

InteractLog 接口

CircleTabMeta 接口

CircleTab 枚举

getFeedImages 纯函数

getTopicCateColor 纯函数

getActStatusColor 纯函数

CircleApp 入口组件

FeedListContent 动态模块

TopicSquareContent 话题模块

ActivityCenterContent 活动模块

AlbumWallContent 相册模块

MyCircleContent 我的模块

在这里插入图片描述

这种分层设计的优势在于关注点分离:数据结构独立于具体数据,数据独立于视图逻辑,视图逻辑独立于组件渲染。当需要接入真实后端 API 时,只需替换数据层的全局常量为异步请求,类型层和逻辑层的代码几乎无需改动。当需要调整视图展示时,也不影响数据层和类型层的稳定性。

下面这张组件依赖图展示了六个自定义组件之间的引用关系与数据依赖。

条件渲染

条件渲染

条件渲染

条件渲染

条件渲染

读取

调用

读取

读取

调用

读取

调用

读取

独立状态

CircleApp
入口+Tab导航

FeedListContent

TopicSquareContent

ActivityCenterContent

AlbumWallContent

MyCircleContent

FEED_LIST

getFeedImages

TOPIC_LIST

INTERACT_LOG

getTopicCateColor

ACTIVITY_LIST

getActStatusColor

ALBUM_LIST

内部模拟数据

在这里插入图片描述

入口组件 CircleApp 通过 @State currentTab 状态变量控制五个子组件的条件渲染,实现了 Tab 切换的核心导航逻辑。每个子组件都是自包含的 @Component,拥有独立的状态空间和数据源,组件之间没有直接通信,完全解耦。


四、类型定义层:interface类型系统解析

ArkTS 中的 interface 用于定义对象的数据结构契约,它不能包含实现逻辑,只能声明属性名称和类型。类型定义是整个应用的数据契约基石,确保所有模块在操作数据时拥有类型安全保障。

4.1 FeedItem:动态数据接口

interface FeedItem {
  id: number
  name: string
  avatar: string
  dept: string
  time: string
  content: string
  images: number
  topic: string
  likes: number
  comments: number
  official: boolean
}

这段代码定义了动态信息的数据结构。id 是数字类型的唯一标识符,用于 ForEach 的键值生成和点赞列表的去重判断。name 存储发布者姓名,avatar 使用 emoji 字符串作为头像占位(实际项目中应为图片资源 URI)。dept 记录发布者所属部门,time 存储相对时间描述字符串(如"12分钟前")。

content 是动态正文字符串,images 是一个数字类型,表示动态附带的图片数量(而非图片数组本身),这是一个巧妙的设计:将图片数量与图片占位符解耦,运行时再通过 getFeedImages 函数根据数量生成对应的 emoji 占位数组。topic 存储话题标签字符串(如"#项目里程碑")。likescomments 分别是点赞数和评论数。official 是布尔类型标记,标识该动态是否来自官方账号,用于在头像上叠加"官"角标。

ArkTS 的类型系统在编译期进行严格类型检查。例如,如果开发者尝试将 official 赋值为字符串 "true",编译器会直接报错。这种静态类型保障在大型应用开发中尤为重要,它能将大量潜在的运行时类型错误前置到编译阶段消除。

4.2 TopicItem:话题数据接口

interface TopicItem {
  id: number
  title: string
  heat: string
  joined: string
  icon: string
  cate: string
}

在这里插入图片描述

TopicItem 定义了话题榜条目结构。heat 使用字符串而非数字,是因为话题热度值通常包含"万"等单位后缀(如"1286.4万"),用字符串直接存储避免了运行时的格式转换。joined 同理,存储"2.3万人参与"这样的格式化字符串。cate 是话题分类标签(官方、职场、健康等),后续通过 getTopicCateColor 函数映射为对应的颜色值。

4.3 ActivityItem:活动数据接口

interface ActivityItem {
  id: number
  name: string
  icon: string
  date: string
  addr: string
  quota: number
  joined: number
  host: string
  status: string
}

活动数据接口中,quotajoined 都是数字类型,因为需要参与数学计算——进度条宽度通过 joined / quota * 100 计算得出。status 是活动状态字符串(报名中、已满员、常驻、已结束),通过 getActStatusColor 函数映射为状态标签颜色。host 存储活动主办方信息。

4.4 AlbumItem 与 InteractLog:相册与互动数据接口

interface AlbumItem {
  id: number
  icon: string
  label: string
  likes: string
}

interface InteractLog {
  id: number
  day: string
  count: number
}

在这里插入图片描述

AlbumItemlikes 使用字符串存储点赞数(如"328"),因为相册场景中点赞数仅用于展示,无需数学运算。InteractLogcount 是数字类型,因为需要根据其值动态计算柱状图的高度(height(l.count))和颜色(l.count >= 72 的条件判断)。

ArkUI技术点:interface与type的区别。 在ArkTS中,interfacetype都可以定义对象类型,但interface支持声明合并(同名接口会自动合并属性),而type不支持。在HarmonyOS API 24的开发规范中,推荐使用interface定义数据契约,因为它更接近面向对象的设计哲学,且在IDE中的类型提示更加友好。


五、数据层:模拟数据集设计

数据层由六个全局常量数组构成,使用 const 声明为不可变引用。这些数组在模块加载时即完成初始化,作为整个应用的静态数据源。

5.1 动态列表 FEED_LIST

动态列表包含十条数据,覆盖了产品部、人事部、运维部、设计部、技术中心、培训部、市场部、行政部、算法组、实习组等不同部门的发布者。每条数据都精心设计了真实的业务场景文本:项目上线里程碑、人事体检通知、机房割接日记、美食探店、技术沙龙预告、管理者培训结业、出差归来工位惊喜、下午茶菜单曝光、算法A/B实验成果、实习转正感言。

其中第2、5、8条数据的 official 字段为 true,标记为官方账号发布。第10条数据(转正感言)的点赞数最高(452),体现了社区对新人成长故事的共鸣与鼓励。这些数据的多样性设计确保了 ForEach 渲染时能充分展示各种条件分支逻辑——官方角标的显示与否、图片数量的差异化(1-4张)、不同话题标签的颜色风格。

ArkUI技术点:const与let。 ArkTS中const声明的变量引用不可变但内容可变(对于数组/对象),let声明的变量引用也可变。在声明全局静态数据时使用const是最佳实践,它向编译器和阅读者传达了"此数据不应被重新赋值"的意图。在HarmonyOS API 24中,全局常量数组会被编译器优化,在多组件共享同一份数据时不会产生额外的内存拷贝。

5.2 话题榜单 TOPIC_LIST

话题榜单包含八条数据,每条包含标题、热度值、参与人数、图标和分类。热度值从1286.4万递减到214.8万,形成自然的热度排序。分类涵盖官方、职场、生活、环保、趣味、健康、社交、福利,确保 getTopicCateColor 函数的所有颜色分支都能被触发展示。

5.3 活动列表 ACTIVITY_LIST

活动列表包含六条数据,状态涵盖"报名中"(3条)、“已满员”(1条)、“常驻”(1条)、“已结束”(1条)。这种状态分布确保了 getActStatusColor 的三个分支(粉色、橙色、灰色)均有数据覆盖。其中第2条数据"亲子开放日"的 joined 等于 quota(80/80),触发进度条满额和"满"角标显示逻辑。

5.4 相册墙 ALBUM_LIST 与互动量 INTERACT_LOG

相册墙包含十二条数据,覆盖团建日出、生日会、运动会、年会、美食节、颁奖礼、宠物日、植树节、游泳赛、K歌王、攀岩墙、乔迁等场景。互动量数据记录周一至周日的互动次数,周五最高(95),周日最低(38),这种分布使柱状图呈现出自然的工作日高峰、周末回落的规律。


在这里插入图片描述

六、导航层:Tab定义与全局工具函数

6.1 枚举与Tab元数据

enum CircleTab { FEED, TOPIC, ACTIVITY, ALBUM, MINE }

interface CircleTabMeta {
  icon: string
  label: string
}

const CIRCLE_TABS: CircleTabMeta[] = [
  { icon: '🔥', label: '动态' },
  { icon: '#️⃣', label: '话题' },
  { icon: '🎪', label: '活动' },
  { icon: '🖼️', label: '相册' },
  { icon: '👤', label: '我的' }
]

enum CircleTab 定义了五个枚举值,从0到4分别对应五个Tab。枚举值天然是数字类型,可以直接与 @State currentTab 进行比较,也可以与 ForEach 的索引 idx 进行匹配。CircleTabMeta 接口定义了Tab的显示元数据——图标和文字标签。

CIRCLE_TABS 数组将枚举值与显示元数据解耦:枚举负责逻辑判断,数组负责渲染展示。这种分离设计使得增删Tab时只需修改枚举和数组两处,无需改动渲染逻辑代码。

用户点击Tab图标

currentTab == idx?

高亮当前Tab
字体加粗变粉色

切换currentTab值

if条件渲染链

currentTab == FEED?

渲染 FeedListContent

currentTab == TOPIC?

渲染 TopicSquareContent

currentTab == ACTIVITY?

渲染 ActivityCenterContent

currentTab == ALBUM?

渲染 AlbumWallContent

渲染 MyCircleContent

在这里插入图片描述

ArkUI技术点:enum枚举类型。 ArkTS的enum在编译后会被转换为JavaScript对象,枚举成员的值默认从0开始递增。在HarmonyOS API 24中,枚举常用于定义有限的选项集合(如Tab类型、状态类型)。使用枚举而非魔法数字(magic number)能显著提升代码可读性和可维护性。注意,ArkTS枚举成员名建议使用大写字母加下划线的命名风格。

6.2 全局纯函数 getFeedImages

function getFeedImages(f: FeedItem): string[] {
  const IMGS: string[] = ['📷', '🌅', '🍜', '🎬', '🎉', '🏆']
  const result: string[] = []
  for (let i = 0; i < f.images && i < 6; i++) {
    result.push(IMGS[i])
  }
  return result
}

在这里插入图片描述

这个函数接收一个 FeedItem 对象,返回一个 emoji 字符串数组。函数内部定义了一个静态的六元素图片占位符数组 IMGS。循环条件 i < f.images && i < 6 同时满足两个约束:取出的数量不超过动态本身声明的图片数,也不超过占位符数组的最大容量(6个)。

这个纯函数的设计体现了数据驱动的思想:动态的 images 字段只存储数量,实际渲染时通过函数动态生成对应数量的占位符。如果未来需要替换为真实图片URL,只需修改此函数的内部实现,调用方代码无需任何变更。纯函数的特性——相同输入总是产生相同输出、无副作用——保证了它在 ForEach 中的调用是安全的、可预测的。

6.3 全局纯函数 getTopicCateColor 与 getActStatusColor

function getTopicCateColor(c: string): string {
  if (c === '官方') {
    return '#E91E63'
  } else if (c === '职场') {
    return '#7B1FA2'
  } else if (c === '健康') {
    return '#00897B'
  } else {
    return '#5C6BC0'
  }
}

function getActStatusColor(s: string): string {
  if (s === '报名中') {
    return '#E91E63'
  } else if (s === '已满员') {
    return '#EF6C00'
  } else {
    return '#9E9E9E'
  }
}

这两个函数将业务语义字符串映射为颜色十六进制值。getTopicCateColor 处理四类话题分类:官方对应珊瑚粉(#E91E63)、职场对应深紫色(#7B1FA2)、健康对应青绿色(#00897B)、其他分类对应靛蓝色(#5C6BC0)。getActStatusColor 处理三类活动状态:报名中对应珊瑚粉、已满员对应深橙色(#EF6C00)、其他(常驻/已结束)对应灰色(#9E9E9E)。

将颜色映射逻辑提取为独立纯函数的好处是集中管理。如果设计团队要求调整"官方"分类的颜色,只需修改一处代码,所有引用此函数的组件会自动获得新颜色。这比在多处硬编码颜色值要易于维护得多。

ArkUI技术点:颜色值规范。 HarmonyOS ArkUI支持多种颜色表示方式:十六进制字符串(如'#E91E63')、rgba字符串(如'rgba(0,0,0,0.5)')、颜色枚举(如Color.Red)。在HarmonyOS API 24中,推荐使用十六进制字符串以获得最佳兼容性和渲染性能。注意颜色值中的alpha通道:rgba格式支持透明度,而纯十六进制#RRGGBB格式不支持。当需要半透明背景时,可使用'rgba(R,G,B,A)'格式。


七、主入口组件 CircleApp:页面骨架与Tab导航

7.1 组件声明与状态定义

@Entry
@Component
struct CircleApp {
  @State currentTab: CircleTab = CircleTab.FEED

  build() {
    // ...
  }
}

@Entry 装饰器将 CircleApp 标记为页面入口组件——每个页面有且仅有一个入口组件,它是渲染树的根节点。@Component 装饰器将 struct 标记为自定义组件,使其可以在其他组件的 build() 方法中被引用。@State 装饰器声明了 currentTab 响应式状态变量,初始值为 CircleTab.FEED(动态Tab)。

ArkUI技术点:@State状态管理装饰器。 @State是ArkUI中最核心的状态管理装饰器。被@State修饰的变量具有以下特性:(1) 响应式——当变量值变化时,引用此变量的UI组件会自动重新渲染;(2) 组件内部私有——@State变量仅在当前组件内可见,子组件无法直接访问;(3) 支持基本类型和引用类型——对于数组类型,@State能感知数组元素的增删(通过concat等不可变操作触发)。在HarmonyOS API 24中,@State的响应式机制基于ArkUI的依赖追踪系统:框架在首次渲染时建立变量与UI节点之间的依赖关系图,当变量被赋新值时,框架通过diff算法精准更新受影响的UI节点,而非全量重渲染。

build() 方法是每个组件必须实现的方法,它返回该组件的UI描述。在 ArkTS 的声明式范式中,build() 方法内只能包含组件声明和链式属性调用,不能包含复杂控制流(复杂逻辑应在方法外部或 @Builder 中处理)。

7.2 头部区域:渐变背景与搜索栏

Column() {
  Row() {
    Column() {
      Text('同事圈').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
      Text('全司 4,286 位伙伴在这里').fontSize(9).fontColor('#F8BBD0').margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Start).layoutWeight(1)

    Text('📅 已连续签到 46 天').fontSize(9).fontColor('#E91E63')
      .backgroundColor('#FFFFFF').borderRadius(11)
      .padding({ left: 8, right: 8, top: 5, bottom: 5 })

    Text('🔍').fontSize(18).margin({ left: 10 })
  }
  .width('100%').padding({ left: 16, right: 16, top: 10 })

  Row() {
    Text('  🔍 搜同事、话题、活动...').fontSize(11).fontColor('#B0BEC5')
      .layoutWeight(1)
  }
  .width('92%').height(32).backgroundColor('#FFFFFF').borderRadius(16)
  .margin({ top: 12, bottom: 12 })
}
.width('100%')
.linearGradient({ angle: 120, colors: [['#E91E63', 0], ['#F06292', 1]] })

在这里插入图片描述

头部区域由一个外层 Column 容器承载,内部包含两行内容。第一行是一个 Row 水平布局容器,内部分为三部分:左侧标题区、中间签到标签、右侧搜索图标。

左侧标题区是一个 Column 垂直布局容器,包含应用标题"同事圈"和副标题"全司4,286位伙伴在这里"。.alignItems(HorizontalAlign.Start) 将内部子元素左对齐,.layoutWeight(1) 使该Column占据剩余水平空间,将签到标签和搜索图标推向右侧。Text('同事圈') 使用 .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF') 设置为大号粗体白色文字,.fontColor('#F8BBD0') 将副标题设为浅粉色,形成主次分明的层次感。

中间签到标签 Text('📅 已连续签到 46 天') 使用白底粉字的胶囊样式:.backgroundColor('#FFFFFF').borderRadius(11).padding({...}),营造一个可点击的徽章效果。右侧搜索图标 Text('🔍') 使用 .margin({ left: 10 }) 与签到标签保持间距。

第二行是搜索栏占位区域,一个 Row 容器内放置了占位提示文字。.width('92%').height(32).backgroundColor('#FFFFFF').borderRadius(16) 创建了一个圆角搜索框外观,文字颜色设为浅灰色(#B0BEC5),模拟未聚焦的placeholder状态。

最关键的是外层 Column.linearGradient({ angle: 120, colors: [['#E91E63', 0], ['#F06292', 1]] }) 属性。这行代码为整个头部区域应用了一个从左下到右上的线性渐变背景。angle: 120 指定渐变角度为120度,colors 数组定义了两个色标:起始色 #E91E63(珊瑚粉,位置0%)和结束色 #F06292(浅珊瑚粉,位置100%),形成柔和的粉色渐变效果。

ArkUI技术点:Column容器组件。 Column是ArkUI三大基础布局容器之一,它将子元素按垂直方向从上到下排列。常用属性包括:.alignItems(HorizontalAlign.Start/Center/End)设置子元素水平对齐方式,.justifyContent(FlexAlign.Start/Center/End/SpaceBetween/SpaceAround)设置主轴(垂直方向)的对齐与间距分配,.layoutWeight(n)设置弹性权重,padding({...})设置内边距,margin({...})设置外边距。在HarmonyOS API 24中,Column是构建列表、卡片等纵向布局结构的首选容器。

ArkUI技术点:Row容器组件。 RowColumn对应,将子元素按水平方向从左到右排列。常用属性与Column类似,但.alignItems控制的是垂直方向对齐(VerticalAlign.Top/Center/Bottom),.justifyContent控制的是水平方向的间距分配。Row常用于导航栏、工具栏、标签行等横向布局场景。

ArkUI技术点:linearGradient线性渐变。 .linearGradient()是ArkUI的背景渐变属性,语法为linearGradient({angle, colors})angle为渐变角度(0-360度),colors[[颜色, 位置], ...]的二维数组,位置值范围为0.0到1.0。HarmonyOS 6.1.1优化了渐变渲染性能,使渐变背景在滚动场景下不再产生明显的帧率波动。除了linearGradient,ArkUI还支持sweepGradient(扇形渐变)和radialGradient(径向渐变)。

7.3 内容区:条件渲染与Tab切换

Column() {
  if (this.currentTab === CircleTab.FEED) {
    FeedListContent()
  } else if (this.currentTab === CircleTab.TOPIC) {
    TopicSquareContent()
  } else if (this.currentTab === CircleTab.ACTIVITY) {
    ActivityCenterContent()
  } else if (this.currentTab === CircleTab.ALBUM) {
    AlbumWallContent()
  } else {
    MyCircleContent()
  }
}
.layoutWeight(1).width('100%')

内容区是页面主体区域,通过 if-else if 条件链根据 currentTab 的值动态渲染不同的子组件。当 currentTabFEED 时渲染 FeedListContent(),为 TOPIC 时渲染 TopicSquareContent(),以此类推。.layoutWeight(1) 使内容区占据头部和底部Tab之间的所有剩余空间。

ArkUI 的条件渲染(if/else if/else)不仅控制首次渲染显示哪个分支,还会在条件变化时自动销毁旧分支的组件实例、创建新分支的组件实例。这意味着切换Tab时,旧Tab组件的 @State 状态会被重置。这种设计在本应用中是合理的——用户切换Tab时通常希望看到最新数据,而非保留之前的中间操作状态。

ArkUI技术点:条件渲染if/else。 ArkUI的if条件渲染与JavaScript的if语法一致,但在UI描述上下文中有特殊语义:条件为true时创建并挂载对应分支的UI节点,条件为false时卸载对应节点。每次条件变化时,ArkUI框架会执行diff操作,精确增删受影响的UI节点。与if相对的还有@Conditional装饰器和Visibility属性控制(.visibility(Visibility.Hidden/None)),后者仅控制可见性而不销毁节点,适合需要保留状态的场景。

7.4 底部Tab栏:ForEach渲染与高亮逻辑

Divider().color('#FCE4EC').strokeWidth(1)

Row() {
  ForEach(CIRCLE_TABS, (t: CircleTabMeta, idx: number) => {
    Column() {
      Text(t.icon).fontSize(19)
        .animation({ duration: 200, curve: Curve.EaseOut })
      Text(t.label).fontSize(9)
        .fontColor(this.currentTab === idx ? '#E91E63' : '#9E9E9E')
        .margin({ top: 2 })
        .fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Center)
    .padding({ top: 6, bottom: 6 })
    .onClick(() => {
      this.currentTab = idx as CircleTab
    })
  }, (t: CircleTabMeta) => t.label)
}
.width('100%').backgroundColor('#FFFFFF')

底部Tab栏首先用 Divider().color('#FCE4EC').strokeWidth(1) 绘制一条浅粉色分隔线,与上方内容区和下方Tab栏形成视觉分隔。

Tab栏主体是一个 Row 容器,内部使用 ForEach 遍历 CIRCLE_TABS 数组渲染五个Tab项。ForEach的第一个参数是数据源数组,第二个参数是项渲染函数(接收项数据和索引),第三个参数是键值生成函数(用于diff优化)。

每个Tab项是一个 Column 容器,包含图标 Text 和标签 Text。图标的 .animation({ duration: 200, curve: Curve.EaseOut }) 为图标添加了200毫秒的缓出动画效果,当Tab切换时图标会有一个微妙的缩放/位移过渡感。标签的颜色和字重通过三元表达式动态控制:当前激活Tab的标签为珊瑚粉色(#E91E63)且粗体,非激活Tab为灰色(#9E9E9E)且正常字重。

.layoutWeight(1) 使每个Tab项等分水平空间,.alignItems(HorizontalAlign.Center) 使图标和标签水平居中对齐。.onClick 回调将点击项的索引赋值给 this.currentTab,触发条件渲染链切换页面内容。idx as CircleTab 将数字索引强制转换为枚举类型。

ArkUI技术点:ForEach循环渲染组件。 ForEach是ArkUI中最重要的列表渲染组件,语法为ForEach(数据源, 项渲染函数, 键值生成函数)。键值生成函数是性能优化的关键——它为每个数据项生成唯一标识,当数据源变化时框架通过比对键值来决定哪些项需要新增、删除或更新,而非全量重建。在HarmonyOS API 24中,ForEach的键值生成函数返回值应具有唯一性和稳定性:同一数据项在不同渲染周期应返回相同键值,不同数据项的键值不能重复。如果键值生成函数省略或返回不唯一,可能导致渲染异常或性能下降。

ArkUI技术点:Divider分隔线组件。 Divider是ArkUI的专用分隔线组件,通过.color()设置线条颜色、.strokeWidth()设置线条粗细、.vertical()设置是否为垂直方向。它默认占据父容器全宽(垂直方向全高),常用于列表项之间的视觉分隔。在HarmonyOS API 24中,Divider的渲染性能优于使用Text('-')border属性模拟的分隔线。

ArkUI技术点:animation属性动画。 .animation()属性为组件的样式变化添加过渡动画。语法为animation({duration, curve, delay, iterations, playMode}),其中duration为动画时长(毫秒),curve为动画曲线(Curve.EaseOut表示先快后慢的缓出效果)。注意:.animation()只对前一个属性变化生效,且需要属性确实发生变化才会触发动画。在HarmonyOS 6.1.1中,animation动画基于属性动画系统实现,不占用主线程,保证了动画过程中的UI响应性。

currentTab==FEED

页面初始化

currentTab = FEED

build方法执行

渲染头部渐变区

if条件链判断

渲染底部Tab栏

渲染FeedListContent

页面首次渲染完成

ForEach遍历5个Tab

idx==currentTab?

图标粉色加粗

图标灰色正常

用户点击其他Tab

currentTab = 新值

ArkUI框架diff

条件链重新求值

销毁旧Tab组件

创建新Tab组件

Tab切换完成

7.5 根容器样式

.width('100%').height('100%').backgroundColor('#FDF2F6')

Column 容器设置全屏宽高,背景色为极浅的粉色(#FDF2F6),为整个应用奠定珊瑚粉的基调氛围。这个背景色在所有Tab页面中保持一致,即使各Tab组件有自己的背景色,根背景色也作为兜底底色。


八、动态Tab模块 FeedListContent

FeedListContent 是应用中最复杂、功能最丰富的组件。它承载了动态信息流展示、四种弹窗交互(发布、评论、删除、举报)、点赞缩放心跳动画、悬浮发布按钮旋转动画等核心功能。

8.1 状态变量定义

@State showPublish: boolean = false
@State showComment: boolean = false
@State showDelete: boolean = false
@State showReport: boolean = false
@State likedIds: number[] = [2, 5, 8]
@State likeScale: number = 1.0
@State pubSpin: number = 0
@State feedName: string = ''
@State postContent: string = ''
@State postScope: string = '全司可见'
@State selectedTopic: string = ''

组件定义了十一个 @State 状态变量,可按功能分为四组:

弹窗控制组showPublishshowCommentshowDeleteshowReport 四个布尔变量分别控制四种弹窗的显示/隐藏。这种"一个布尔对应一个弹窗"的设计简单直接,且天然互斥——虽然代码没有强制互斥逻辑,但由于每个弹窗的触发入口不同,实际上不会出现两个弹窗同时弹出。

点赞状态组likedIds 是一个数字数组,存储用户已点赞的动态ID。初始值为 [2, 5, 8],表示用户初始已对ID为2、5、8的动态点了赞。likeScale 控制点赞图标的缩放比例,用于实现心跳动画。

发布动态状态组postContent 存储用户输入的动态正文,postScope 存储选择的可见范围(默认"全司可见"),selectedTopic 存储选择的话题标签。这三个状态变量与发布动态弹窗的表单数据绑定。

交互上下文组pubSpin 控制悬浮发布按钮的旋转角度,feedName 存储当前操作的动态作者名(用于举报和评论弹窗的上下文传递)。

ArkUI技术点:@State数组类型。@State修饰数组类型时,ArkUI框架能感知数组本身被重新赋值的变化,但对于数组元素的直接修改(如this.likedIds.push(3))不一定会触发响应式更新。在HarmonyOS API 24中,推荐使用不可变操作来更新数组:this.likedIds = this.likedIds.concat([3])this.likedIds = [...this.likedIds, 3],这种方式创建新数组引用,确保框架能检测到变化。本应用中所有数组更新都采用了concat方法,正是遵循了这一最佳实践。

初始态

点赞动画态

评论态

举报态

点击悬浮按钮

点击发布/遮罩

点击评论图标

点击关闭/遮罩

点击删除(id=10)

确认/取消

点击···按钮

提交举报

点击❤️

动画结束

showPublish=false

showComment=false

showDelete=false

showReport=false

likedIds=[2,5,8]

likeScale=1.0

pubSpin=0

发布态

showPublish=true

pubSpin+=360

showComment=true

feedName=作者名

删除态

showDelete=true

showReport=true

likeScale=1.3

likedIds增加/不变

160ms后

8.2 遮罩层构建器 modalOverlay

@Builder modalOverlay(onClose: () => void) {
  Column() {
    Column().width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)')
      .onClick(() => {
        onClose()
      })
  }
  .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(998)
}

@Builder 装饰器定义了一个可复用的UI构建方法。modalOverlay 接收一个 onClose 回调函数作为参数,构建一个半透明黑色遮罩层。

内部实现是一个 Column 容器包含一个空的 Column,空Column设置了全屏尺寸和 rgba(0,0,0,0.5) 的半透明黑色背景。点击遮罩层会触发 onClose() 回调,关闭对应的弹窗。外层Column使用 .position({ x: 0, y: 0 }) 定位到屏幕左上角,.zIndex(998) 设置层级为998——比弹窗内容(zIndex 999)低一层,确保弹窗内容浮于遮罩之上。

ArkUI技术点:@Builder构建器装饰器。 @Builder是ArkUI中用于封装和复用UI片段的装饰器。被@Builder修饰的方法可以在build()中像组件一样被调用。与自定义@Component组件的区别在于:@Builder方法没有独立的状态空间,它直接访问所在组件的@State变量;@Builder方法可以接收参数(如本例的onClose回调),实现参数化的UI复用。在HarmonyOS API 24中,@Builder支持按引用传递参数($$语法)和按值传递参数两种方式。当UI片段需要在不同位置重复使用时,提取为@Builder是最佳实践,它比复制粘贴代码更易维护,比独立@Component更轻量(无需独立状态管理开销)。

ArkUI技术点:zIndex层级控制。 .zIndex(n)属性设置组件在Stack容器或绝对定位场景中的Z轴层级。数值越大,组件越在上层。在HarmonyOS API 24中,zIndex仅在同级元素之间生效——如果两个组件分别属于不同的Stack容器,它们的zIndex不会跨容器比较。本应用中遮罩层zIndex=998、弹窗内容zIndex=999,确保弹窗内容始终显示在遮罩之上。

ArkUI技术点:position绝对定位。 .position({x, y})属性将组件从文档流中脱离,使用绝对定位放置在指定坐标位置。坐标系原点为父容器的左上角。与.offset({x, y})的区别在于:position是绝对定位(脱离文档流),offset是相对偏移(仍占据文档流位置)。在弹窗场景中,position常用于将遮罩层和弹窗内容覆盖在正常内容之上。

8.3 弹窗1:发布动态(底部抽屉)

@Builder publishModal() {
  Column() {
    Column() {
      Row() {
        Text('✏️ 发布动态').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
        Text('发布').fontSize(12).fontColor('#FFFFFF').backgroundColor('#E91E63')
          .borderRadius(13).padding({ left: 14, right: 14, top: 6, bottom: 6 })
          .margin({ left: 12 })
          .onClick(() => {
            this.showPublish = false
          })
      }
      .width('100%').padding({ left: 16, right: 16, top: 16 })

      TextArea({ placeholder: '分享这一刻的想法...', text: this.postContent })
        .fontSize(13).height(90)
        .onChange((v: string) => {
          this.postContent = v
        })
        .width('92%').backgroundColor('#FDF2F6').borderRadius(10)
        .margin({ top: 14 })

      Row() {
        Text('📷').fontSize(24)
          .width(52).height(52).backgroundColor('#FDF2F6').borderRadius(8)
          .textAlign(TextAlign.Center).margin({ right: 10 })
        Text('🎥').fontSize(24)
          .width(52).height(52).backgroundColor('#FDF2F6').borderRadius(8)
          .textAlign(TextAlign.Center).margin({ right: 10 })
        Text('📎').fontSize(24)
          .width(52).height(52).backgroundColor('#FDF2F6').borderRadius(8)
          .textAlign(TextAlign.Center)
      }
      .width('92%').margin({ top: 12 })

发布动态弹窗是一个底部抽屉式设计。外层 Column 设置 .position({ x: 0, y: 0 }).zIndex(999) 覆盖全屏,内层 Column 设置白底和顶部圆角(.borderRadius({ topLeft: 20, topRight: 20 })),形成从底部弹出的卡片效果。

弹窗顶部是一个 Row,左侧标题 Text('✏️ 发布动态') 为深色粗体,右侧"发布"按钮为白字粉底圆角胶囊。点击发布按钮将 this.showPublish 设为 false 关闭弹窗。

正文输入区使用 TextArea 组件,通过 text: this.postContent 实现双向绑定,.onChange 回调将输入值同步到状态变量。TextArea 是ArkUI的多行文本输入组件,区别于 TextInput 的单行输入。

工具栏区是一个 Row,包含三个媒体附件图标(相机📷、摄像机🎥、附件📎),每个都是52x52的圆角方块,模拟可点击的附件按钮。

      Text('选择话题').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#212121')
        .width('92%').margin({ top: 14 })

      Scroll() {
        Row() {
          ForEach(['#项目里程碑', '#职场温暖', '#转正感言', '#下午茶', '#运维日记', '#算法实战'], (t: string) => {
            Text(t).fontSize(11)
              .fontColor(this.selectedTopic === t ? '#FFFFFF' : '#E91E63')
              .backgroundColor(this.selectedTopic === t ? '#E91E63' : '#FCE4EC')
              .borderRadius(12)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .margin({ right: 8 })
              .onClick(() => {
                this.selectedTopic = t
              })
          }, (t: string) => t)
        }
      }
      .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
      .width('92%').margin({ top: 8 })

话题选择区使用 Scroll 组件包裹一个 Row,实现横向滚动的话题标签列表。ForEach 遍历六个话题字符串,每个话题标签通过三元表达式判断是否被选中:选中状态为白字粉底,未选中为粉字浅粉底。点击标签将话题字符串赋值给 this.selectedTopic

.scrollable(ScrollDirection.Horizontal) 设置滚动方向为水平,.scrollBar(BarState.Off) 隐藏滚动条——这是移动端常见的做法,用户通过手势滑动操作但不需要看到滚动条视觉元素。

ArkUI技术点:Scroll滚动容器组件。 Scroll是ArkUI的滚动容器组件,它使内部超出容器尺寸的内容可以通过手势滚动查看。常用属性包括:.scrollable(ScrollDirection.Vertical/Horizontal)设置滚动方向,.scrollBar(BarState.On/Off/Auto)控制滚动条显示,.scrollEdge()设置边缘效果,.onScroll()监听滚动事件。在HarmonyOS API 24中,Scroll组件内置了惯性滚动、边界回弹等手势交互,开发者无需自行实现。Scroll与Column/Row配合使用:内部通常放一个Column(垂直滚动)或Row(水平滚动)来承载多个子组件。

ArkUI技术点:ScrollDirection滚动方向枚举。 ScrollDirection是ArkUI中控制滚动方向的枚举:Vertical垂直滚动(默认)、Horizontal水平滚动。当Scroll内部是Row且需要横向滑动时,必须设置scrollable(ScrollDirection.Horizontal),否则默认垂直滚动会导致内容无法水平滑动。

继续分析发布弹窗的可见范围选择区:

      Text('可见范围').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#212121')
        .width('92%').margin({ top: 14 })

      Row() {
        ForEach(['全司可见', '仅部门', '仅自己'], (s: string) => {
          Row() {
            Radio({ value: s, group: 'scope' })
              .checked(this.postScope === s)
              .onChange((c: boolean) => {
                if (c) {
                  this.postScope = s
                }
              })
              .width(16).height(16)
            Text(s).fontSize(11).fontColor('#616161').margin({ left: 4 })
          }
          .margin({ right: 20 })
        }, (s: string) => s)
      }
      .width('92%').margin({ top: 8 })

      Text('已选话题:' + (this.selectedTopic === '' ? '未选择' : this.selectedTopic))
        .fontSize(10).fontColor('#9E9E9E').width('92%').margin({ top: 12, bottom: 24 })

可见范围使用 Radio 单选按钮组件,三个选项共享 group: 'scope' 组名,确保互斥选择。.checked(this.postScope === s) 通过三元判断当前选项是否被选中。.onChange 回调参数 c 表示选中状态,当某个Radio被选中时(c === true),将对应值赋给 this.postScope

ArkUI技术点:Radio单选按钮组件。 Radio是ArkUI的单选按钮组件,语法为Radio({value, group})value是该单选项的值,group是组名——同一组的Radio互斥,只能选中一个。.checked(boolean)设置选中状态,.onChange((isChecked) => {...})监听选中变化。与Toggle/Checkbox的区别在于:Radio强调互斥单选,Checkbox强调多选。在HarmonyOS API 24中,Radio组件的视觉样式可通过.radioStyle()进一步自定义。

底部信息行 Text('已选话题:...') 动态展示当前选择的话题,通过三元表达式在"未选择"和实际话题字符串之间切换,提供即时的状态反馈。

8.4 弹窗2:评论列表

@Builder commentModal() {
  Column() {
    Column() {
      Row() {
        Text('💬 全部评论').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#212121')
        Text('✕').fontSize(16).fontColor('#9E9E9E').margin({ left: 8 })
          .onClick(() => {
            this.showComment = false
          })
      }
      .width('100%').padding({ left: 16, right: 16, top: 16 })

      Scroll() {
        Column() {
          ForEach([['👩‍🎨', 'Lisa', '同款云南菜+1!下周末一起呀'], ...], (c: string[]) => {
            Row() {
              Text(c[0]).fontSize(24)
                .width(36).height(36).backgroundColor('#FCE4EC').borderRadius(18)
                .textAlign(TextAlign.Center)
              Column() {
                Text(c[1]).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#E91E63')
                Text(c[2]).fontSize(11).fontColor('#424242').margin({ top: 3 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 10 })
              Text('❤️').fontSize(13)
            }
            .width('100%').padding({ left: 16, right: 16, top: 10, bottom: 10 })

            Divider().color('#F8BBD0')
          }, (c: string[]) => c[1])
        }
        .constraintSize({ maxHeight: 260 })
      }
      .scrollBar(BarState.Off).margin({ top: 6 })

评论弹窗采用居中卡片式设计,通过 .position({ y: '16%' }) 定位在屏幕偏上方。顶部标题行包含"全部评论"标题和关闭按钮"✕",点击关闭按钮将 showComment 设为 false

评论列表使用 Scroll 组件包裹,内部 Column 设置 .constraintSize({ maxHeight: 260 }) 限制最大高度,超出部分通过滚动查看。ForEach 遍历一个二维数组(每个元素是一个评论的 [头像, 昵称, 内容] 三元组),键值生成函数返回 c[1](昵称)作为唯一标识。

每条评论是一个 Row:左侧36x36的圆形头像、中间的昵称+内容Column、右侧的点赞图标。.layoutWeight(1) 使中间内容区占据剩余空间,将点赞图标推到右侧。评论之间用浅粉色 Divider 分隔。

ArkUI技术点:constraintSize约束尺寸。 .constraintSize({maxWidth, maxHeight, minWidth, minHeight})为组件设置尺寸约束。与直接设置.width()/.height()的区别在于:constraintSize是约束条件而非固定值,组件实际尺寸会在约束范围内根据内容自适应。在HarmonyOS API 24中,maxHeight常用于限制Scroll内部Column的高度,使列表在内容不足时不会撑满整个滚动容器,在内容过多时启用滚动。

评论弹窗的底部输入栏:

      Row() {
        Text('😊').fontSize(20).margin({ right: 10 })
        TextInput({ placeholder: '友善评论,温暖同行...' })
          .fontSize(12).layoutWeight(1).height(36)
          .backgroundColor('#FDF2F6').borderRadius(18)
        Text('发送').fontSize(12).fontColor('#FFFFFF').backgroundColor('#E91E63')
          .borderRadius(13).padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .margin({ left: 10 })
      }
      .width('100%').padding({ left: 16, right: 16, top: 10, bottom: 20 })

底部输入栏是一个 Row,包含表情按钮、文本输入框和发送按钮。TextInput 使用 .layoutWeight(1) 占据剩余空间,创建一个紧凑的评论输入区。发送按钮为白字粉底圆角胶囊样式。

ArkUI技术点:TextInput单行文本输入组件。 TextInput是ArkUI的单行文本输入组件,语法为TextInput({placeholder, text})。常用属性:.type(InputType.Normal/Password/Number/Email...)设置输入类型,.maxLength(n)限制最大字符数,.onChange((value) => {...})监听输入变化。在HarmonyOS API 24中,TextInput支持输入法类型适配(如密码输入自动切换到安全键盘)、输入校验等能力。与TextArea的区别:TextInput单行不换行,TextArea多行可换行。

8.5 弹窗3:删除动态警示

@Builder deletePostModal() {
  Column() {
    Column() {
      Text('🗑️').fontSize(36).margin({ top: 18 })
      Text('删除这条动态?').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#212121').margin({ top: 10 })
      Text('删除后该动态的点赞、评论将一并消失,且无法恢复。').fontSize(11).fontColor('#9E9E9E')
        .textAlign(TextAlign.Center).margin({ top: 8 }).padding({ left: 20, right: 20 })

      Row() {
        Button() {
          Text('再想想').fontSize(13).fontColor('#616161')
        }
        .layoutWeight(1).height(38).backgroundColor('#F5F5F5').borderRadius(19)
        .onClick(() => {
          this.showDelete = false
        })

        Button() {
          Text('确认删除').fontSize(13).fontColor('#FFFFFF')
        }
        .layoutWeight(1).height(38).backgroundColor('#D32F2F').borderRadius(19)
        .margin({ left: 12 })
        .onClick(() => {
          this.showDelete = false
        })
      }
      .width('100%').padding({ left: 20, right: 20, top: 18, bottom: 20 })
    }
    .width('78%').backgroundColor('#FFFFFF').borderRadius(16)
  }
  .width('100%').alignItems(HorizontalAlign.Center)
  .position({ y: '34%' }).zIndex(999)
}

删除弹窗是一个居中警示卡片,通过 .position({ y: '34%' }) 垂直定位在屏幕中偏上位置。卡片宽度78%,白底圆角。

内容从上到下依次是:垃圾桶图标(fontSize 36)、警示标题"删除这条动态?"、说明文字、双按钮操作区。说明文字使用 .textAlign(TextAlign.Center) 居中对齐,并设置左右padding确保文字不会太宽影响阅读。

双按钮采用 Button 组件包裹 Text 的方式构建。左侧"再想想"按钮为浅灰底深灰字,右侧"确认删除"按钮为红色底(#D32F2F)白字。两个按钮都用 .layoutWeight(1) 等分宽度,.borderRadius(19) 设置为半圆胶囊形状。.margin({ left: 12 }) 在两个按钮之间添加间距。

ArkUI技术点:Button按钮组件。 Button是ArkUI的按钮容器组件,可以包裹子组件作为按钮内容。语法有两种形式:Button('文本')直接设置文本按钮,或Button() { Text(...) }包裹自定义内容。常用属性:.type(ButtonType.Capsule/Circle/Normal)设置按钮形状,.backgroundColor()设置背景色,.onClick()设置点击事件。在HarmonyOS API 24中,Button内置了按下态(pressed)的视觉反馈(透明度变化),开发者无需自行实现。使用Button包裹Text而非直接使用Text+onClick的好处是:Button提供了语义化的可访问性标记和默认的触摸反馈效果。

ArkUI技术点:textAlign文本对齐。 .textAlign(TextAlign.Start/Center/End)设置文本在组件内的水平对齐方式。注意它对齐的是文本在Text组件自身宽度内的位置,而非Text组件在父容器中的位置。当Text组件宽度由.width('100%')撑满父容器时,textAlign才能实现视觉上的居中/右对齐效果。在HarmonyOS API 24中,TextAlign.Start等效于左对齐(LTR排版下),TextAlign.End等效于右对齐。

8.6 弹窗4:举报内容

@Builder reportModal() {
  Column() {
    Column() {
      Text('🚨 举报不当内容').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#212121')
        .width('100%').padding({ top: 16, left: 16 })

      Text('请选择举报类型(可多选)').fontSize(10).fontColor('#9E9E9E')
        .width('100%').padding({ left: 16, top: 6 })

      Column() {
        ForEach(['垃圾广告 / 恶意营销', '人身攻击 / 不友善', '泄露公司机密信息', '色情低俗内容', '虚假不实信息'], (r: string) => {
          Row() {
            Radio({ value: r, group: 'report' })
              .width(15).height(15)
            Text(r).fontSize(12).fontColor('#424242').margin({ left: 8 })
          }
          .width('100%').padding({ left: 16, top: 9, bottom: 9 })

          Divider().color('#F5F5F5')
        }, (r: string) => r)
      }
      .width('100%').margin({ top: 4 })

      TextArea({ placeholder: '补充说明(选填,200字内)...' })
        .fontSize(12).height(64)
        .width('88%').backgroundColor('#FDF2F6').borderRadius(8)
        .margin({ top: 10 })

      Text('提交举报').fontSize(13).fontColor('#FFFFFF').backgroundColor('#E91E63')
        .borderRadius(18).padding({ left: 32, right: 32, top: 8, bottom: 8 })
        .margin({ top: 14, bottom: 18 })
        .onClick(() => {
          this.showReport = false
        })
    }
    .width('88%').backgroundColor('#FFFFFF').borderRadius(16)
  }
  .width('100%').alignItems(HorizontalAlign.Center)
  .position({ y: '10%' }).zIndex(999)
}

举报弹窗通过 .position({ y: '10%' }) 定位在屏幕偏上方。内容从上到下依次是标题、提示文字、举报类型单选列表、补充说明文本域、提交按钮。

举报类型列表使用 ForEach 渲染五条选项,每条是一个 Row 包含 Radio 单选按钮和 Text 说明文字。注意这里虽然提示文字说"可多选",但实际使用的是 Radio 组件(单选),这是模拟代码中的一个小小的不一致——在真实项目中应使用 Checkbox 组件实现多选功能。

每条选项下方有一个浅灰色 Divider 分隔线。补充说明使用 TextArea 组件,高度64,placeholder提示用户输入选填信息。提交按钮为白字粉底圆角胶囊,点击后关闭弹窗。

用户点击动态卡片···按钮

feedName = f.name

showReport = true

渲染遮罩层 zIndex=998

渲染举报弹窗 zIndex=999

用户选择举报类型

Radio单选切换

用户填写补充说明

TextArea输入

用户点击提交举报

showReport = false

弹窗卸载

遮罩层卸载

用户点击遮罩层

onClose回调

showReport = false

8.7 动态信息流主内容

build() {
  Stack() {
    Scroll() {
      Column() {
        Row() {
          Text('📢').fontSize(13)
          Text('人事通知:年度体检预约通道已开启,截止 9/30').fontSize(11).fontColor('#AD1457')
            .layoutWeight(1)
          Text('›').fontSize(14).fontColor('#AD1457')
        }
        .width('96%').backgroundColor('#FCE4EC').borderRadius(10)
        .padding({ left: 12, right: 12, top: 8, bottom: 8 })
        .margin({ top: 10 })

build() 方法返回一个 Stack 容器。Stack 是ArkUI的层叠布局容器,子元素按先后顺序堆叠——后声明的子元素在上层。这里使用Stack是为了让悬浮发布按钮和弹窗层能够覆盖在滚动内容之上。

Stack 内部第一个元素是 Scroll 滚动容器,包裹整个动态信息流。信息流顶部是一个公告条 Row,浅粉色背景圆角,包含公告图标、通知文字(.layoutWeight(1) 占据中间空间)和右箭头。这是一个典型的可点击通知条设计。

ArkUI技术点:Stack层叠布局容器。 Stack是ArkUI三大基础布局容器之一(与Column、Row并列),它将子元素按Z轴方向层叠堆放。常用属性:.alignContent(Alignment.TopStart/Center/BottomEnd...)设置子元素在Stack内的对齐方式。子元素默认居中堆叠,后声明的子元素在上层。在HarmonyOS API 24中,Stack常用于:弹窗层叠效果、悬浮按钮、角标叠加、绝对定位组合等场景。Stack与position配合使用时,position使子元素脱离Stack的默认居中对齐,实现精确定位。

8.8 动态卡片渲染

        ForEach(FEED_LIST, (f: FeedItem) => {
          Column() {
            Row() {
              Stack() {
                Text(f.avatar).fontSize(24)
                  .width(42).height(42).backgroundColor('#FCE4EC').borderRadius(21)
                  .textAlign(TextAlign.Center)
                if (f.official) {
                  Text('官').fontSize(8).fontColor('#FFFFFF').backgroundColor('#E91E63')
                    .borderRadius(7).padding({ left: 4, right: 4, top: 1, bottom: 1 })
                    .position({ x: 30, y: 32 })
                }
              }

              Column() {
                Row() {
                  Text(f.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#AD1457')
                  if (f.official) {
                    Text('官方号').fontSize(7).fontColor('#FFFFFF').backgroundColor('#E91E63')
                      .borderRadius(4).padding({ left: 3, right: 3, top: 1, bottom: 1 }).margin({ left: 6 })
                  }
                }
                Text(f.dept + ' · ' + f.time).fontSize(9).fontColor('#9E9E9E').margin({ top: 2 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 10 })

              Column() {
                Text('···').fontSize(18).fontColor('#B0BEC5')
              }
              .onClick(() => {
                this.feedName = f.name
                this.showReport = true
              })
            }
            .width('100%')

动态卡片是整个应用最核心的UI单元。每张卡片是一个 Column 容器,白底圆角,内边距14,外边距在四周留出间距。

卡片顶部是一个 Row,包含三个部分:头像区、作者信息区、操作按钮区。头像区使用 Stack 层叠容器,底层是42x42的圆形头像背景+emoji头像文字,上层是条件渲染的"官"角标——仅当 f.officialtrue 时渲染。角标使用 .position({ x: 30, y: 32 }) 绝对定位到头像右下角,形成角标叠加效果。

作者信息区是一个 Column,包含姓名行和部门时间行。姓名行又是一个 Row,当动态为官方时在姓名右侧追加"官方号"小标签。部门时间行将部门和发布时间用"·"连接拼接为一条文字。.layoutWeight(1) 使作者信息区占据中间空间,将操作按钮推到右侧。

操作按钮区是一个 Column 包含"···"省略号文字,点击触发举报弹窗——先将作者名存入 this.feedName,再设置 this.showReport = true。这里将 feedName 作为上下文传递,使举报弹窗知道是针对哪位作者的动态。

            Text(f.content).fontSize(12).fontColor('#424242').lineHeight(18)
              .width('100%').margin({ top: 10 })

            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(getFeedImages(f), (img: string) => {
                Text(img).fontSize(26)
                  .width(84).height(84).backgroundColor('#FDF2F6').borderRadius(8)
                  .textAlign(TextAlign.Center).margin({ right: 6, top: 6 })
              }, (img: string) => f.id.toString() + '_' + img)
            }
            .width('100%').margin({ top: 6 })

            Text(f.topic).fontSize(10).fontColor('#E91E63').backgroundColor('#FCE4EC')
              .borderRadius(9).padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .margin({ top: 10 })

正文区域是 Text(f.content),设置字号12、深灰色、行高18——.lineHeight(18) 使多行文本拥有舒适的行间距。图片九宫格使用 Flex({ wrap: FlexWrap.Wrap }) 弹性换行布局容器,内部 ForEach 调用 getFeedImages(f) 函数获取图片占位数组。每个图片占位是84x84的圆角方块,间距通过 .margin({ right: 6, top: 6 }) 实现。

键值生成函数 f.id.toString() + '_' + img 将动态ID与图片占位符拼接为唯一键,避免不同动态的相同图片占位符产生键值冲突。

话题标签是 Text(f.topic),设置为粉字浅粉底圆角胶囊样式,与正文之间通过 .margin({ top: 10 }) 保持间距。

ArkUI技术点:Flex弹性布局容器。 Flex是ArkUI的弹性布局容器,语法为Flex({direction, wrap, justifyContent, alignItems})wrap参数控制换行行为:FlexWrap.NoWrap不换行(默认)、FlexWrap.Wrap换行。在HarmonyOS API 24中,FlexRow/Column的区别在于:Flex支持子元素换行、支持更丰富的对齐策略(如FlexAlign.SpaceBetweenFlexAlign.SpaceAroundFlexAlign.SpaceEvenly)、支持子元素flexGrow/flexShrink弹性权重。当需要实现网格/九宫格布局时,Flex({wrap: FlexWrap.Wrap})是最简洁的方案——配合固定宽度的子元素,自动换行排列。

ArkUI技术点:lineHeight行高属性。 .lineHeight(n)设置文本行高。当文本内容超过一行时,行高决定了行与行之间的间距。适当增大行高可以提升多行文本的可读性。在HarmonyOS API 24中,lineHeight的单位是vp(虚拟像素),通常设置为字号大小的1.4-1.6倍可获得舒适的阅读体验。本应用中12号字体配18行高(1.5倍),是常见的正文排版比例。

8.9 点赞交互与心跳动画

            Divider().color('#F8BBD0').margin({ top: 10 })

            Row() {
              Row() {
                Text('❤️').fontSize(15)
                  .animation({ duration: 150, curve: Curve.EaseOut })
                Text(this.likedIds.indexOf(f.id) >= 0 ? (f.likes + 1).toString() : f.likes.toString())
                  .fontSize(11).fontColor(this.likedIds.indexOf(f.id) >= 0 ? '#E91E63' : '#757575')
                  .margin({ left: 4 })
              }
              .onClick(() => {
                this.likeScale = 1.3
                if (this.likedIds.indexOf(f.id) < 0) {
                  this.likedIds = this.likedIds.concat([f.id])
                } else {
                  this.likeScale = 1.0
                }
                setTimeout(() => {
                  this.likeScale = 1.0
                }, 160)
              })
              .margin({ right: 26 })

点赞交互区是动态卡片底部的操作栏第一个元素。通过 this.likedIds.indexOf(f.id) >= 0 判断当前用户是否已对此动态点赞:如果已赞,点赞数显示 f.likes + 1(原赞数加用户自己的一赞),文字颜色为粉色;如果未赞,显示原始点赞数,文字颜色为灰色。

点击点赞按钮的逻辑分为三步:首先设置 this.likeScale = 1.3 触发放大效果;然后检查 likedIds 中是否已包含当前动态ID——如果未包含,通过 this.likedIds = this.likedIds.concat([f.id]) 添加(使用concat创建新数组以触发响应式更新);如果已包含(即取消点赞场景),将 likeScale 设回1.0。最后通过 setTimeout 在160毫秒后将 likeScale 重置为1.0,实现"放大→回弹"的心跳动画效果。

ArkUI技术点:animation与setTimeout配合实现动画。 ArkUI的.animation()属性监听前一个属性的变化并添加过渡动画。本例中likeScale从1.0变为1.3时,.animation({duration: 150})使变化以150毫秒的缓出动画过渡;随后setTimeout在160毫秒后将其重置为1.0,触发第二次动画(从1.3回到1.0)。两次动画的组合形成了"放大→回弹"的心跳效果。这种"立即放大+定时回弹"的模式是ArkUI中实现点击反馈动画的经典范式。在HarmonyOS 6.1.1中,这种属性动画完全由GPU加速,不会阻塞主线程的UI响应。

未包含

已包含

用户点击❤️

likeScale = 1.3

likedIds中
已包含f.id?

likedIds = concat
添加f.id

likeScale = 1.0

点赞数+1
文字变粉色

取消点赞状态
但显示不变

animation触发
150ms放大动画

setTimeout 160ms

likeScale = 1.0

animation触发
回弹动画

心跳效果完成

8.10 评论、分享与删除操作

              Row() {
                Text('💬').fontSize(15)
                Text(f.comments.toString()).fontSize(11).fontColor('#757575').margin({ left: 4 })
              }
              .onClick(() => {
                this.feedName = f.name
                this.showComment = true
              })
              .margin({ right: 26 })

              Row() {
                Text('↗️').fontSize(15)
                Text('分享').fontSize(11).fontColor('#757575').margin({ left: 4 })
              }

              if (f.id === 10) {
                Text('删除').fontSize(11).fontColor('#EF6C00').margin({ left: 16 })
                  .onClick(() => {
                    this.showDelete = true
                  })
              }
            }
            .width('100%').padding({ top: 10, bottom: 6 })

评论按钮点击后设置 feedName 并显示评论弹窗。分享按钮仅为视觉展示,未绑定具体逻辑。"删除"按钮使用 if (f.id === 10) 条件渲染——仅第10条动态(转正感言)显示删除入口,模拟"仅能删除自己发布的动态"的业务逻辑。点击删除按钮触发删除警示弹窗。

操作栏整体是一个 Row,三个操作元素通过 .margin({ right: 26 }) 保持等间距。这种用固定margin而非 justifyContent(FlexAlign.SpaceBetween) 的设计使操作按钮紧密排列在左侧,而非分散到两端。

8.11 悬浮发布按钮与旋转动画

      Text('✏️').fontSize(24)
        .width(54).height(54).backgroundColor('#E91E63').borderRadius(27)
        .textAlign(TextAlign.Center)
        .shadow({ radius: 12, color: 'rgba(233,30,99,0.4)', offsetX: 0, offsetY: 4 })
        .rotate({ angle: this.pubSpin })
        .animation({ duration: 300, curve: Curve.EaseOut })
        .position({ x: '78%', y: '80%' })
        .onClick(() => {
          this.pubSpin = this.pubSpin + 360
          this.showPublish = true
        })

悬浮发布按钮是Stack容器的第二个子元素,通过 .position({ x: '78%', y: '80%' }) 绝对定位到屏幕右下角。按钮为54x54的珊瑚粉圆形(.borderRadius(27) 即半径),内含发布图标"✏️"。

.shadow({ radius: 12, color: 'rgba(233,30,99,0.4)', offsetX: 0, offsetY: 4 }) 为按钮添加投影效果:半径12的模糊、珊瑚粉带0.4透明度的颜色、垂直偏移4px,形成浮于内容之上的视觉层次感。

.rotate({ angle: this.pubSpin }) 是旋转动画的核心属性。pubSpin 初始值为0,每次点击时 this.pubSpin = this.pubSpin + 360 增加360度。配合 .animation({ duration: 300, curve: Curve.EaseOut }),按钮会在300毫秒内完成一周完整旋转,使用缓出曲线使旋转速度先快后慢,模拟"拧开"的物理感受。

ArkUI技术点:rotate旋转变换属性。 .rotate({angle, centerX, centerY, z})属性对组件进行旋转变换。angle为旋转角度(正数顺时针,负数逆时针),centerX/centerY为旋转中心点的相对位置(默认为组件中心),z为Z轴旋转角度(3D旋转)。在HarmonyOS API 24中,rotate变换是GPU加速的,不会触发组件重新布局,仅影响绘制层。这使得旋转动画极其流畅,即使在大列表中也不会影响滚动性能。

ArkUI技术点:shadow阴影属性。 .shadow({radius, color, offsetX, offsetY})为组件添加投影效果。radius为模糊半径(值越大越模糊),color为阴影颜色,offsetX/offsetY为阴影偏移量。在HarmonyOS 6.1.1中,shadow渲染性能显著优化,多层嵌套阴影不再产生明显的帧率下降。设计上,悬浮元素通常使用带颜色的半透明阴影(如rgba(主色, 0.3-0.5)),而非纯黑色阴影,这样能与主题色更协调。

8.12 弹窗条件渲染

      if (this.showPublish) {
        this.modalOverlay(() => {
          this.showPublish = false
        })
        this.publishModal()
      }

      if (this.showComment) {
        this.modalOverlay(() => {
          this.showComment = false
        })
        this.commentModal()
      }

      if (this.showDelete) {
        this.modalOverlay(() => {
          this.showDelete = false
        })
        this.deletePostModal()
      }

      if (this.showReport) {
        this.modalOverlay(() => {
          this.showReport = false
        })
        this.reportModal()
      }

四个弹窗的渲染逻辑结构完全一致:当对应的 show* 布尔值为 true 时,先渲染遮罩层 this.modalOverlay(onClose回调),再渲染弹窗内容 this.*Modal()。遮罩层的 onClose 回调将对应布尔值设为 false,实现点击遮罩关闭弹窗的功能。

这种"遮罩+弹窗内容"的成对渲染模式是ArkUI弹窗交互的标准范式。遮罩层zIndex=998在底层拦截背景点击,弹窗内容zIndex=999在上层展示具体交互表单。由于四个 if 条件是独立的(而非if-else if链),理论上可以同时弹出多个弹窗——但实际使用场景中不会出现这种情况,因为各弹窗的触发入口互斥。


九、话题Tab模块 TopicSquareContent

话题模块展示社区热门话题榜单和互动数据概览,包含一个手工绘制的柱状图组件。

9.1 话题数据概览

@Component
struct TopicSquareContent {
  @State hotIdx: number = 0

  build() {
    Stack() {
      Scroll() {
        Column() {
          Row() {
            Column() {
              Text('842').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('热门话题').fontSize(8).fontColor('#F8BBD0').margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)

            Divider().color('rgba(255,255,255,0.3)').vertical(true).height(30)

            Column() {
              Text('12.6万').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('今日讨论').fontSize(8).fontColor('#F8BBD0').margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)

            Divider().color('rgba(255,255,255,0.3)').vertical(true).height(30)

            Column() {
              Text('2,845').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('活跃作者').fontSize(8).fontColor('#F8BBD0').margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
          }
          .width('96%').padding({ top: 14, bottom: 14 })
          .linearGradient({ angle: 135, colors: [['#E91E63', 0], ['#F48FB1', 1]] })
          .borderRadius(14).margin({ top: 10 })

话题模块仅有一个 @State 变量 hotIdx,初始值为0,用于记录用户关注的话题ID。值为0表示未关注任何话题,大于0表示关注了对应ID的话题。

数据概览卡片是一个 Row,包含三组数据(热门话题842、今日讨论12.6万、活跃作者2,845),每组之间用垂直 Divider 分隔。.vertical(true) 使Divider变为竖直方向,.height(30) 控制其高度。分隔线颜色使用 rgba(255,255,255,0.3) 即30%透明度的白色,在粉色渐变背景上形成柔和的分隔效果。

整个卡片使用135度渐变背景(从深珊瑚粉 #E91E63 到浅粉 #F48FB1),.borderRadius(14) 圆角处理。

ArkUI技术点:Divider.vertical垂直分隔线。 Divider组件通过.vertical(true)可以变为垂直方向的分隔线。这在水平排列的多个数据项之间创建视觉分隔时非常实用。注意垂直Divider需要显式设置.height(),否则可能不显示。在HarmonyOS API 24中,垂直Divider常用于Row内的数据统计区分隔。

9.2 本周互动量柱状图

          Column() {
            Text('📊 本周互动量').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
              .width('100%').padding({ left: 14, top: 12 })

            Row() {
              ForEach(INTERACT_LOG, (l: InteractLog) => {
                Column() {
                  Column()
                    .width(16).height(l.count)
                    .backgroundColor(l.count >= 72 ? '#E91E63' : '#F8BBD0')
                    .borderRadius({ topLeft: 3, topRight: 3 })
                    .animation({ duration: 800, curve: Curve.EaseOut })
                  Text(l.day).fontSize(8).fontColor('#9E9E9E').margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Center).margin({ left: 9, right: 9 })
              }, (l: InteractLog) => l.id.toString())
            }
            .width('100%').padding({ top: 14, bottom: 14 })
            .alignItems(VerticalAlign.Bottom).height(120)
          }
          .width('96%').backgroundColor('#FFFFFF').borderRadius(14)
          .margin({ top: 12, left: 8, right: 8 })

这是一个手工实现的柱状图,没有使用任何图表库,完全通过ArkUI基础组件构建。外层 Column 提供标题和图表区域,内层 Row 作为图表容器。

图表容器 Row 设置 .alignItems(VerticalAlign.Bottom).height(120),使柱子在容器内底部对齐,最大高度120——这是柱状图的关键布局属性,确保所有柱子从同一基线向上生长。

每根柱子是一个 Column,包含柱体和日期标签。柱体是一个空的 Column 组件,.width(16) 固定宽度16,.height(l.count) 将数据值直接映射为柱体高度——这就是纯数据驱动的柱状图实现方式。柱体颜色通过三元表达式控制:当 l.count >= 72 时使用主色珊瑚粉(#E91E63),否则使用浅粉色(#F8BBD0),实现"高互动量高亮"的视觉效果。.borderRadius({ topLeft: 3, topRight: 3 }) 仅设置左上和右上的圆角,模拟柱状图常见的圆角顶部。

.animation({ duration: 800, curve: Curve.EaseOut }) 为柱体添加800毫秒的缓出动画。当组件首次渲染时,柱体从0高度增长到 l.count 高度,形成"柱子长出来"的入场动画效果。

ArkUI技术点:alignItems与垂直对齐。 在Row容器中,.alignItems(VerticalAlign.Top/Center/Bottom)控制子元素在垂直方向的对齐方式。.alignItems(VerticalAlign.Bottom)使所有子元素底部对齐,这是柱状图的基线对齐需求。在HarmonyOS API 24中,VerticalAlign枚举提供Top、Center、Bottom三个选项。当Row的height大于子元素高度时,alignItems的效果最明显——子元素会根据对齐方式在容器内定位。

ArkUI技术点:borderRadius部分圆角。 .borderRadius()可以接受数字(四角统一圆角)或对象(分别设置四角)。.borderRadius({topLeft: 3, topRight: 3, bottomLeft: 0, bottomRight: 0})仅设置上方两角圆角,下方保持直角。这在柱状图、标签、卡片顶部圆角等场景中非常实用。在HarmonyOS API 24中,部分圆角的渲染性能与统一圆角一致。

9.3 话题热搜榜

          Column() {
            Row() {
              Text('🔥 话题热搜榜').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
              Text('每周三更新').fontSize(8).fontColor('#B0BEC5').margin({ left: 8 })
            }
            .width('100%').padding({ left: 14, right: 14, top: 12 })

            ForEach(TOPIC_LIST, (t: TopicItem) => {
              Row() {
                Text(t.id.toString())
                  .fontSize(15).fontWeight(FontWeight.Bold)
                  .fontColor(t.id <= 3 ? '#E91E63' : '#B0BEC5')
                  .width(26).textAlign(TextAlign.Center)

                Text(t.icon).fontSize(20).margin({ left: 4 })

                Column() {
                  Row() {
                    Text(t.title).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#212121')
                    Text(t.cate).fontSize(7).fontColor('#FFFFFF')
                      .backgroundColor(getTopicCateColor(t.cate))
                      .borderRadius(4).padding({ left: 3, right: 3, top: 1, bottom: 1 }).margin({ left: 6 })
                  }
                  Text(t.joined).fontSize(9).fontColor('#9E9E9E').margin({ top: 3 })
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 10 })

                Column() {
                  Text(t.heat).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#E91E63')
                  Text('热度').fontSize(7).fontColor('#B0BEC5').margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.End)

                Text(this.hotIdx === t.id ? '✓' : '+').fontSize(13)
                  .fontColor(this.hotIdx === t.id ? '#FFFFFF' : '#E91E63')
                  .backgroundColor(this.hotIdx === t.id ? '#E91E63' : '#FCE4EC')
                  .borderRadius(10).padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .margin({ left: 10 })
                  .onClick(() => {
                    if (this.hotIdx !== t.id) {
                      this.hotIdx = t.id
                    } else {
                      this.hotIdx = 0
                    }
                  })
              }
              .width('100%').padding({ left: 14, right: 14, top: 11, bottom: 11 })

              Divider().color('#F8BBD0')
            }, (t: TopicItem) => t.id.toString())
          }
          .width('96%').backgroundColor('#FFFFFF').borderRadius(14)
          .margin({ top: 12, left: 8, right: 8 })

话题热搜榜是话题模块的核心内容。外层 Column 白底圆角,内部通过 ForEach 遍历 TOPIC_LIST 渲染八条话题。

每条话题是一个 Row,从左到右依次是:排名序号、话题图标、话题信息区(标题+分类标签+参与人数)、热度数据区、关注按钮。

排名序号通过 .fontColor(t.id <= 3 ? '#E91E63' : '#B0BEC5') 区分前三名(珊瑚粉色加粗)和后续名次(灰色),这是榜单常见的视觉层次设计。话题分类标签的颜色通过 getTopicCateColor(t.cate) 函数动态获取,不同分类呈现不同颜色。

关注按钮的交互逻辑是一个"toggle"切换:点击未关注的话题将其ID设为 hotIdx(关注),再次点击已关注的话题将 hotIdx 重置为0(取消关注)。按钮的文字和颜色随关注状态变化:已关注为"✓"白字粉底,未关注为"+"粉字浅粉底。

否 未关注

是 已关注

话题榜首次渲染

hotIdx=0 未关注

ForEach渲染8条话题

所有关注按钮显示+
浅粉底粉字

用户点击话题3关注按钮

hotIdx == t.id?
3==3?

hotIdx = 3

按钮变✓
白字粉底

其他按钮保持+

用户再次点击话题3

hotIdx == t.id?
3==3?

hotIdx = 0

按钮变回+

取消关注状态

用户点击话题5

hotIdx == 5?
3==5?

hotIdx = 5

话题3按钮变回+

话题5按钮变✓


十、活动Tab模块 ActivityCenterContent

活动模块展示企业活动列表和详情报名弹窗,核心特性是进度条增长动画。

10.1 状态变量定义

@Component
struct ActivityCenterContent {
  @State showDetail: boolean = false
  @State actName: string = '城市定向越野赛'
  @State actAddr: string = '滨江森林公园'
  @State actDate: string = '09-06 09:00'
  @State actQuota: number = 120
  @State actJoined: number = 96
  @State joinedIds: number[] = [4]
  @State progressGrow: boolean = false

活动模块定义了八个 @State 变量。showDetail 控制详情弹窗的显示。actName/actAddr/actDate/actQuota/actJoined 五个变量存储当前选中活动的详情数据,初始值为第一条活动的数据。joinedIds 存储用户已报名的活动ID数组,初始值为 [4](即10KM晨跑团)。progressGrow 是进度条动画的控制开关。

这种"一组状态变量表示当前选中项详情"的设计模式,使得点击不同活动时只需更新这组变量即可刷新弹窗内容,无需为每个活动创建独立的弹窗实例。

10.2 活动横幅卡

  build() {
    Stack() {
      Scroll() {
        Column() {
          Column() {
            Text('🎪').fontSize(44).margin({ top: 20 })
            Text('城市定向越野赛 · 员工专属').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 10 })
            Text('96/120 已报名 · 09-06 滨江森林公园开跑').fontSize(10).fontColor('#F8BBD0').margin({ top: 6 })
            Text('查看详情').fontSize(11).fontColor('#E91E63').backgroundColor('#FFFFFF')
              .borderRadius(14).padding({ left: 16, right: 16, top: 6, bottom: 6 })
              .margin({ top: 14, bottom: 20 })
              .onClick(() => {
                this.progressGrow = false
                this.showDetail = true
                setTimeout(() => {
                  this.progressGrow = true
                }, 100)
              })
          }
          .width('96%')
          .linearGradient({ angle: 135, colors: [['#AD1457', 0], ['#F06292', 1]] })
          .borderRadius(16).alignItems(HorizontalAlign.Center)
          .margin({ top: 10 })

顶部横幅是一个居中对齐的 Column,使用深珊瑚粉渐变背景(从 #AD1457#F06292)。内容从上到下依次是活动图标(44号大字体)、活动名称、报名概况、查看详情按钮。

"查看详情"按钮的点击逻辑是一个精心设计的动画触发序列:首先将 this.progressGrow = false 重置进度条为0,然后设置 this.showDetail = true 显示弹窗,最后通过 setTimeout 在100毫秒后将 this.progressGrow = true 触发进度条增长动画。这个100毫秒的延迟是必要的——它确保弹窗先完成渲染,再触发进度条从0增长到目标比例的动画,如果同步设置则动画可能不会正确触发(因为ArkUI需要在渲染完成后才能检测到属性变化)。

10.3 活动列表与名额进度条

          ForEach(ACTIVITY_LIST, (a: ActivityItem) => {
            Column() {
              Row() {
                Stack() {
                  Text(a.icon).fontSize(26)
                    .width(52).height(52).backgroundColor('#FCE4EC').borderRadius(26)
                    .textAlign(TextAlign.Center)
                  if (a.id === 2) {
                    Text('满').fontSize(9).fontColor('#FFFFFF').backgroundColor('#EF6C00')
                      .borderRadius(8).padding({ left: 4, right: 4, top: 1, bottom: 1 })
                      .position({ x: 36, y: 40 })
                  }
                }

                Column() {
                  Row() {
                    Text(a.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
                    Text(a.status).fontSize(8).fontColor('#FFFFFF')
                      .backgroundColor(getActStatusColor(a.status))
                      .borderRadius(4).padding({ left: 4, right: 4, top: 1, bottom: 1 }).margin({ left: 6 })
                  }
                  Text('🕘 ' + a.date + ' · 📍 ' + a.addr).fontSize(9).fontColor('#9E9E9E').margin({ top: 4 })
                  Text('主办:' + a.host).fontSize(9).fontColor('#9E9E9E').margin({ top: 3 })
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Start).margin({ left: 12 })

                Column() {
                  Text(this.joinedIds.indexOf(a.id) >= 0 ? '已报名' : '报名')
                    .fontSize(10)
                    .fontColor(this.joinedIds.indexOf(a.id) >= 0 ? '#9E9E9E' : '#FFFFFF')
                    .backgroundColor(this.joinedIds.indexOf(a.id) >= 0 ? '#F5F5F5' : '#E91E63')
                    .borderRadius(11).padding({ left: 12, right: 12, top: 5, bottom: 5 })
                    .onClick(() => {
                      if (this.joinedIds.indexOf(a.id) < 0) {
                        this.joinedIds = this.joinedIds.concat([a.id])
                      }
                    })
                }
                .alignItems(HorizontalAlign.End)
                .onClick(() => {
                  this.actName = a.name
                  this.actAddr = a.addr
                  this.actDate = a.date
                  this.actQuota = a.quota
                  this.actJoined = a.joined
                  this.progressGrow = false
                  this.showDetail = true
                  setTimeout(() => {
                    this.progressGrow = true
                  }, 100)
                })
              }
              .width('100%')

活动卡片的头部结构与动态卡片类似:头像区使用 Stack 叠加,当活动已满员(a.id === 2)时在头像右下角显示"满"角标。活动信息区包含名称+状态标签行、时间地点行、主办方行。状态标签颜色通过 getActStatusColor(a.status) 动态获取。

报名按钮的文字和样式通过三元表达式根据 joinedIds 中是否包含当前活动ID来切换。点击报名按钮如果未报名则添加ID到数组(concat 不可变操作)。注意报名按钮的 onClick 和外层卡片的 onClick 是独立的——点击报名按钮不会触发卡片点击(因为事件不会冒泡到外层),但点击卡片其他区域会打开详情弹窗。

卡片整体点击时,将当前活动的数据赋值到五个详情状态变量,然后执行与横幅"查看详情"相同的动画触发序列:progressGrow = falseshowDetail = truesetTimeoutprogressGrow = true

              Column() {
                Column()
                  .width((a.joined / a.quota * 100).toFixed(0) + '%')
                  .height(4).backgroundColor(a.joined >= a.quota ? '#EF6C00' : '#F06292').borderRadius(2)
              }
              .width('100%').height(4).backgroundColor('#FCE4EC').borderRadius(2)
              .margin({ top: 10 })

              Row() {
                Text('已报 ' + a.joined + '/' + a.quota + ' 人').fontSize(8).fontColor('#9E9E9E')
                Text('查看详情 ›').fontSize(8).fontColor('#E91E63').margin({ left: 10 })
              }
              .width('100%').justifyContent(FlexAlign.SpaceBetween).margin({ top: 6 })
            }
            .width('
          this.showEdit = false
        })
        this.editProfileModal()
      }
    }
    .width('100%').height('100%')
  }
}

在这里插入图片描述

19.10 总结:

本应用全面运用了基于HarmonyOS API 24的ArkTS声明式UI开发能力。从基础组件(Text、Image占位、Button、Radio、TextInput、TextArea、Divider)到布局容器(Column、Row、Stack、Flex、Scroll),从渲染控制(ForEach、if/else条件渲染)到状态管理(@State、@Entry、@Component、@Builder),从样式系统(linearGradient、borderRadius、padding、margin、shadow)到变换动画(rotate、animation、Curve曲线),从交互系统(onClick、onChange)到定位系统(position、zIndex),涵盖了ArkUI开发的核心技术栈。

HarmonyOS 6.1.1 在ArkUI框架层面的增强(渐变渲染性能优化、动画帧率稳定性提升、Flex换行布局算法改进、Scroll滚动流畅度优化)为这些能力的运用提供了坚实的底层支撑。声明式UI范式将开发者从繁琐的命令式视图操作中解放出来,使精力可以集中在业务逻辑和用户体验设计上。@State 的响应式机制保证了数据与视图的自动同步,ForEach 的键值diff优化保证了列表渲染的高效性,@Builder 的UI复用能力保证了代码的简洁性和可维护性。

Logo

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

更多推荐