鸿蒙原生应用实战:活动相册活动页的进行中/已结束卡片列表

App 40「校园活动照片」活动页(Func1Tab),主题色 #6C5CE7 紫色(violet),4 个 Tab 分别为首页(📸)、活动(🎉)、上传(📤)、我的(👤)。活动页采用"Header + 分段切换 + 活动列表"三区布局——白色双行 Header("活动"20 号加粗 + "进行中 / 已结束"两段切换,@State tab 驱动选中态) + 4 张活动卡片(🎊 2026 迎新晚会 / 🏃 秋季运动会 / 🎸 校园音乐节 / 🎭 话剧社公演,每张含 120 高 emoji 大图区 + 活动名 + 📅 日期 + 👥 参与人数 + 📷 照片数 + 紫色"查看相册 >")。本篇基于 40-event-photo/entry/src/main/ets/pages/Func1Tab.ets(共 89 行)逐段拆解,附 4 张实机截图。

一、整体结构:三区"Header + 分段 + 活动列表"布局

活动页是"品牌头部 + 状态分段 + 活动明细"的三区布局:

build() {
  Column() {
    this.Header()
    Scroll() {
      Column({ space: 14 }) {
        this.EventList()
      }
      .width('100%')
      .padding({ left: D.pad, right: D.pad, top: 14, bottom: D.pad + this.safeBottom + 20 })
    }
    .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
  }
  .width('100%').height('100%').backgroundColor(C.bg)
}

3 块结构:

  1. Header — 双行结构:标题行 + 分段切换行,都固定在顶部非滚动区
  2. Scroll — 活动卡片列表滚动区
  3. EventList — 4 张活动卡(当前 Demo 两段共用同一列表,切换只改变高亮态)

与首页同构的"Header + Scroll + Column(space:14)"骨架再次出现,系列统一性延续。

40 活动页首屏

项目源码开源:https://gitee.com/codenestFlow/HarmonyOSHub

二、Header:双行结构 + 分段切换

Header() {
  Column() {
    Row() { Text('活动').fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text) }
    .width('100%').height(this.safeTop + 52).padding({ top: this.safeTop, left: D.pad, right: D.pad })
    .alignItems(VerticalAlign.Bottom)
    Row() {
      ForEach(this.tabs, (t: string, idx: number) => {
        Text(t).fontSize(14)
          .fontColor(this.tab === idx ? C.primary : C.textSub)
          .fontWeight(this.tab === idx ? FontWeight.Bold : FontWeight.Normal)
          .layoutWeight(1).textAlign(TextAlign.Center).padding({ bottom: 12 })
          .onClick(() => { this.tab = idx; })
      }, (t: string) => t)
    }.width('100%')
  }.width('100%').backgroundColor(C.card)
}

技术拆解:

  1. 标题行:"活动"20 号加粗,safeTop + 52 高度、文字底部对齐——与系列其他二级页一致。
  2. 分段切换行:ForEach(this.tabs, ...) 遍历 ['进行中', '已结束'] 两个字符串,每个 Text 用 layoutWeight(1) 平分整行宽度、textAlign(TextAlign.Center) 居中。
  3. 选中态驱动:@State tab: number 记录当前选中索引,this.tab === idx 决定颜色(选中紫色加粗 / 未选灰色常规)——这是"文本模拟分段控件"的经典实现,比 Tabs/TabContent 轻量得多,适合只有两三个分段的场景。
  4. 点击切换:onClick(() => { this.tab = idx; }) 更新索引,UI 自动重绘。
  5. 底部内边距:padding({ bottom: 12 }) 让分段文字与下方滚动内容保持间距。

分段交互的意义:产品语义上"进行中"展示正在举办的活动、"已结束"展示历史活动(可回顾照片)。Demo 为演示简洁共用同一列表,真实实现中 tab === 0 时渲染"进行中"数据源、tab === 1 时渲染"已结束"数据源即可,切换逻辑已就绪。

三、EventList:活动卡片列表

EventList() {
  Column({ space: 14 }) {
    ForEach(this.events, (e: Event) => {
      Column() {
        Row() { Text(e.emoji).fontSize(50) }
        .width('100%').height(120).backgroundColor(C.primarySoft)
        .borderRadius({ topLeft: D.rLg, topRight: D.rLg }).justifyContent(FlexAlign.Center)
        Column({ space: 8 }) {
          Text(e.name).fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
          Row() {
            Text('📅 ' + e.date).fontSize(12).fontColor(C.textDim).layoutWeight(1)
            Text('👥 ' + e.joined + ' 人参与').fontSize(12).fontColor(C.textDim)
          }.width('100%')
          Row() {
            Text('📷 ' + e.photos + ' 张照片').fontSize(12).fontColor(C.primary).layoutWeight(1)
            Text('查看相册 >').fontSize(12).fontColor(C.primary)
          }.width('100%')
        }.padding(14)
      }
      .width('100%').backgroundColor(C.card).borderRadius(D.rLg).border({ width: 1, color: C.stroke })
      .onClick(() => { promptAction.showToast({ message: e.name }); })
    }, (e: Event) => e.id.toString())
  }.width('100%')
}

活动数据(23-28 行)由 private events: Event[] 静态数组提供:

活动日期参与人数照片数
🎊 2026 迎新晚会9月10日320128
🏃 秋季运动会10月15日580256
🎸 校园音乐节11月2日450176
🎭 话剧社公演11月20日12045

卡片细节:

  1. 大图区:120 高、50 号 emoji 居中、primarySoft 浅紫底、只圆上两角(borderRadius({ topLeft: D.rLg, topRight: D.rLg }))——比首页瀑布流卡(46 号 emoji、140~200 高)更大更醒目,活动卡是"主推对象"。
  2. 活动名:16 号加粗全宽,信息层级最高。
  3. 元信息第一行:📅 日期 与 👥 N 人参与 用 Row + layoutWeight(1) 分列左右,均为 12 号浅灰。
  4. 元信息第二行:📷 N 张照片 与 查看相册 > 用紫色 12 号,与第一行浅灰形成"数据 vs 动作"的语义区分——紫色暗示可点击。
  5. 整卡交互:点击任意位置 Toast 活动名,ForEach key 用 e.id.toString()。

两行元信息的排版价值:每行都用"左标签 + 右补充"的 layoutWeight(1) 结构,四行信息(名称/日期+人数/照片+入口)信息密度高但绝不拥挤,是活动类列表卡的成熟模板。

四、跨页数据自洽:活动页是首页相册的"来源"

活动页与首页、我的页的数据关系形成完整闭环:

  • 同名呼应:活动页 4 个活动(迎新晚会/秋季运动会/校园音乐节/话剧社公演)与首页 6 个相册中的 4 个完全同名——首页相册即活动照片集,活动页负责"活动视角"、首页负责"相册视角",同一个数据源两种切片。
  • 照片数一致:迎新晚会 128、运动会 256、音乐节 176、话剧 45,与首页相册的 count 字段完全一致——同一份数据,两处展示。
  • 参与人数:320/580/450/120 是活动维度独有字段,首页未展示——两页信息互补,拼起来才是完整活动画像。
  • 我的页"12 参与活动" ↔ Banner"12 场活动"↔ 活动页 4 个 + 已结束 8 个:12 是平台/个人活动总数,与分段"进行中/已结束"的状态划分呼应。

五、实机截图与交互演示

本节结合 4 张实机截图,逐张还原活动页的视觉效果与交互过程。

1. 活动页首屏:标题 + 分段 + 活动卡

第一张截图是活动页默认首屏:顶部"活动"标题,下方"进行中 / 已结束"分段——默认"进行中"为紫色加粗选中态,"已结束"为灰色;下方是第一张活动卡"🎊 2026 迎新晚会"(120 高 emoji 大图 + 名称 + 📅 9月10日 + 👥 320 人参与 + 📷 128 张照片 + 查看相册 >),底部可见第二张"🏃 秋季运动会"卡的上半部分。

2. 点击"已结束":分段切换状态

第二张截图是点击"已结束"分段后的状态:高亮从"进行中"切换到"已结束"(紫色加粗),@State tab 由 0 变 1、UI 即时重绘。虽然 Demo 中列表内容不变(两段共用数据),但分段切换的交互反馈完整可验证,为真实产品"按状态过滤活动"预留了结构。

3. 点击活动卡:弹出活动名 Toast

第三张截图是点击第一张活动卡任意位置后的反馈:弹出"2026 迎新晚会"Toast。整卡挂载 onClick、闭包捕获 e.name,点击卡面任何区域(大图区/名称/元信息)都触发同一反馈,点击热区大、误触率低。

4. 滚动到底:全部活动卡可见

第四张截图是滚动到底部的视图:第三张"🎸 校园音乐节"与第四张"🎭 话剧社公演"完整露出,四张活动卡全部可见。每张卡的 120 高 emoji 大图区 + 三行信息的结构整齐划一,列表滚动流畅,底部安全区内边距保证 Tab 栏可点。

40 活动页已结束切换

六、扩展思考:分段切换的三种实现方案

活动页用"文本 + @State"实现了分段切换,工程上还有更丰富的方案:

  1. 文本模拟(本页方案):ForEach + 条件样式 + @State 索引。最轻量、零依赖,适合 2-4 个分段;缺点是缺少滑动动画与无障碍语义。
  2. Tabs + TabContent:ArkUI 原生分段容器,自带滑动切换动画与手势,适合"分段内容差异大、需要独立滚动"的场景;缺点是结构较重,每段需要独立内容区。
  3. 分段指示条:在选中 Text 下方渲染一个 2-3 高的圆角条(@State 驱动的位移或条件渲染),配合 animation 实现滑动指示动画——比纯文本高亮更精致,是电商/资讯类 App 的常见做法。
  4. 胶囊分段:把选中项包成紫色圆角胶囊(类似兴趣页选中态),未选中为浅底——视觉更"按钮化",适合操作型分段。

本页的文本模拟方案在"只有两个分段、内容未分源"的 Demo 阶段完全够用,四种方案的演进路径清晰。

七、扩展思考:活动数据模型的完整设计

从活动页的 Event 接口反推真实产品的活动模型:

  1. 基础字段:{ id, emoji, name, date, photos, joined } 已覆盖展示所需;真实产品需扩展 coverUrl(封面图)、status(进行中/已结束/未开始)、location(地点)、organizer(主办方)、description(简介)。
  2. status 驱动分段:tab === 0 ? events.filter(e => e.status === 'ongoing') : events.filter(e => e.status === 'ended')——分段切换从"改高亮"升级为"过滤数据",一行动态过滤即可。
  3. 照片与活动的关系:一个活动对应一个相册(一对多:activity → photos),活动卡的"📷 N 张照片"即相册的 count——首页相册与活动页共享这张映射表。
  4. 参与人数:joined 可实时增长(用户点"参加"按钮 +1),配合上传页的"关联活动"胶囊(迎新晚会/秋季运动会/校园音乐节/话剧公演),上传照片即写入对应活动的照片集——三页联动的数据总线就此打通。

八、常见问题与开发避坑

  1. 分段切换"没变化":Demo 两段共用同一列表,用户切换后只见高亮变化、不见内容变化,可能误以为 Bug。真实实现必须绑定不同数据源,否则建议在 Demo 文案中说明"切换仅改变状态高亮"。
  2. textAlign 需要固定宽度:layoutWeight(1) 已使 Text 占满列宽,此时 textAlign(TextAlign.Center) 才能生效;若 Text 宽度自适应内容,居中无效。
  3. emoji 大图区的溢出:50 号 emoji 在 120 高容器内垂直居中(justifyContent(FlexAlign.Center)),若 emoji 换成真实图片,记得加 objectFit(ImageFit.Cover) 裁切,否则图片会拉伸变形。
  4. 圆角与图片裁切:与首页同理,若大图区放网络图片,需外层容器 clip(true) 否则图片溢出圆角。
  5. 列表空态:若某分段无活动(如"已结束"暂无数据),页面只剩标题一片空白——应补充空态视图("暂无已结束活动" + 图标),工程化必备。

40 活动页滚动底部

九、扩展思考:活动详情页与参与流程设计

活动页的卡片点击后(当前 Toast)应进入活动详情页,围绕"了解活动 → 参与活动 → 回顾照片"设计完整流程:

  1. 详情页信息架构:封面大图 + 活动名 + 时间/地点/主办方(可复用活动卡的四行信息结构放大)+ 活动简介 + "参与/已参与"状态按钮 + 相册入口 + 评论区。活动卡已有的 date/joined/photos 字段全部成为详情页的头图信息。
  2. 参与状态机:未参与 → 已参与 → 活动结束 三态——对应分段"进行中/已结束":进行中的活动可点"参加"(joined +1),已结束的活动展示"回顾照片"。@State joined: boolean 驱动按钮文案与色态切换。
  3. 倒计时组件:进行中的活动可在详情页头部展示"距开始还有 N 天"(setInterval 定时刷新),增强紧迫感与参与动力——@ohos.timer 或 ArkUI 定时器可实现。
  4. 活动与照片的联动:详情页相册入口跳转首页对应相册;活动结束时相册自动"封存"并计入"已结束"分段——活动状态驱动照片可见性,是活动照片共享类产品的核心业务规则。
  5. 分享传播:详情页配"分享"按钮,生成活动海报(封面 + 时间 + 参与人数)分享到社交平台,为平台引流——照片社区的拉新入口。

十、扩展思考:时间线与活动的生命周期管理

从活动页的日期字段(9月10日/10月15日/11月2日/11月20日)可以展开活动的完整生命周期:

  1. 生命周期四阶段:筹备中 → 进行中 → 已结束 → 归档——当前 Demo 只有"进行中/已结束"两态,真实产品还有"未开始"(预热报名)与"已归档"(照片长期可查)两个阶段。
  2. 状态自动流转:系统按当前日期与 date 对比自动更新状态——new Date() >= date ? 'ended' : 'ongoing',无需人工干预;配合 onPageShow 刷新,App 打开即是最新状态。
  3. 照片的时段属性:活动照片可按拍摄时间细分(开幕式/比赛/闭幕式),相册内提供时间线滑动筛选——@ohos.data 的按时间查询能力支撑。
  4. 历史活动沉淀:已结束活动不删数据,照片持续开放浏览(甚至支持按届数归档:"2026 迎新晚会" vs "2025 迎新晚会"),平台内容随时间沉淀,Banner"已收录 12 场活动 · 858 张照片"即这种沉淀的量级体现。
  5. 统计与运营:参与人数、照片数、获赞数按活动聚合,可支撑运营看板(哪类活动最受欢迎)——数据在活动卡上已有雏形,聚合查询是进阶能力。

十一、系列横向对比:活动清单类页面的四种范式

活动页属于"清单 + 状态筛选"类页面,本系列提供了多种可对照的实现范式:

范式代表页筛选方式列表形态
分段切换App 40 活动页进行中/已结束文本分段图文大卡
分类胶囊App 38 技能交换6 类横滑胶囊双行技能卡
搜索过滤App 37 考试资料搜索框 + 分类资料列表
Tab 页签App 34 打印资料底部 Tab 内分组资料卡

App 40 用"双段文本切换"覆盖了活动清单最核心的筛选诉求(当前/历史),交互成本最低、用户零学习成本;而 App 38 的横滑胶囊适合"分类多且需要快速切换"的场景,App 37 的搜索适合"资料量大需要精确检索"的场景。筛选控件的选型取决于备选数量:2-3 个状态用分段、4-8 个分类用横滑胶囊、大量条目用搜索——这是清单类产品设计的第一决策点。

活动卡本身的信息结构(emoji 大图 + 名称 + 两行元信息)与 App 38 技能卡(头像 + 名称 + 标签 + 按钮)同属"图文卡"家族,但活动卡弱化操作按钮("查看相册 >"是文字链而非按钮)、强化数据信息(日期/人数/照片数)——因为活动的"转化动作"是低频的(报名/查看),而技能的"交换"是高频操作。信息组织的优先级永远服务于核心动作,这一原则在两张卡片的设计差异中体现得淋漓尽致。

40 活动页活动卡 Toast

十二、开发者视角:分段切换与列表联动的调试技巧

  1. @State 切换无效的排查:若点击分段后高亮不变化,先确认 onClick 是否真正触发(可临时加 console 日志),再确认 @State tab 是否被正确赋值——常见坑是 this.tab = idx 写成了局部变量赋值。
  2. 分段与列表的数据过滤:产品化后 tab === 0 ? 进行中 : 已结束 的过滤逻辑建议写成计算属性(get currentEvents()),避免在构建函数里写复杂条件分支——既清晰又便于单测。
  3. 大图区的图片替换:把 emoji 换成真实活动封面时,务必加 objectFit(ImageFit.Cover) 与固定宽高,否则不同尺寸的封面会拉伸变形破坏卡片统一性——回归测试时用竖图、横图、方形图各试一遍。
  4. 日期显示与时区:📅 9月10日 若来自服务端 ISO 日期,注意时区转换(UTC → 本地),否则可能显示成 9 月 9 日——用 @ohos.i18n 的日期格式化接口做本地化。
  5. 列表滚动性能:活动卡含 120 高图片区 + 三行文字,若数量上百,应把 ForEach 升级为 LazyForEach 并按 id 缓存图片——滚动卡顿时优先检查是否有整列表重建(如 key 不稳定)。

十三、FAQ 与一句话总结

Q1:活动页和首页相册有什么区别? 活动页以"活动"为维度(时间/参与人数/状态),首页以"相册"为维度(照片数量/瀑布流展示)。二者共享同一批照片数据,是同一内容源的两种组织视角——一个问"办了哪些活动",一个问"有哪些相册可看"。

Q2:分段切换为什么不做成可滑动的? 两段内容共用列表,滑动切换无实际意义。当分段内容真正分离(进行中列表 vs 已结束列表)时,可升级为 Tabs 获得滑动动画。

Q3:"查看相册 >"为什么点整卡都是弹活动名? 整卡统一挂载 onClick 弹活动名,查看相册 > 没有单独挂事件——原型阶段一个点击入口足够。产品化时给"查看相册 >"单独挂跳转(进入相册详情页),整卡点击与入口跳转可并存。

Q4:日期格式为什么是"9月10日"而非"2026-09-10"? 面向 C 端展示用友好格式(9月10日)比标准格式更亲切,是产品文案的常见选择;数据层仍应存标准 ISO 日期,展示层再做格式化。

Q5:为什么只有 4 个活动? 4 张卡恰好一屏展示主体 + 滚动留白,信息量适中。真实产品分页加载,首屏 10 条左右为佳。

一句话总结:活动页用"双行 Header + 文本分段 + 四行信息活动卡"完成了"活动清单 + 状态筛选"的产品职能。分段切换的 @State 驱动、两行元信息的 layoutWeight 分列、emoji 大图区与三行信息区"图文卡"结构,都是 ArkUI 列表页的高频手法——尤其"数据行浅灰、动作行紫色"的颜色语义化,是本页最值得抄走的细节。

再补充一点观察:活动页的 Header 是二级页中少见的"标题 + 分段"双行结构,这一差异源于活动清单的"状态筛选"需求——分段行成为页面交互的副导航,与内容区形成"筛选驱动列表"的联动关系。若未来活动数量增长到几十个,可在分段下方追加"按月份筛选"的横向滚动月份条,让"活动清单"演化为完整的"校园活动时间线"。同时,活动卡的 joined(参与人数)与 photos(照片数)两个数据字段已具备"活动热度"的雏形——产品化后可用 joined * 0.7 + photos * 0.3 计算热度分,支持"热门活动"排序,让列表从"固定顺序"升级为"热度驱动",进一步贴近真实运营场景。活动页虽仅 89 行,却是"清单 + 筛选 + 数据"三要素俱全的完整页面。

最后,从验收视角看:四张实机截图覆盖了首屏(分段 + 第一张活动卡)、分段切换(已结束高亮)、卡片点击(活动名 Toast)、滚动底部(全部四卡可见)四个关键状态,证明 @State tab 切换、整卡点击、列表滚动三项核心交互均工作正常。"迎新晚会 128 张"等数据与页面内其他位置一致,再次验证了数据自洽的验收标准。 整体而言,本页以最小的代码量实现了清单、筛选、数据三类能力,配合四张实机截图的完整验证,为校园活动场景交出了一份高质量的参考实现。

配图

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐