鸿蒙原生应用实战:活动相册活动页的进行中/已结束卡片列表
鸿蒙原生应用实战:活动相册活动页的进行中/已结束卡片列表
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 块结构:
- Header — 双行结构:标题行 + 分段切换行,都固定在顶部非滚动区
- Scroll — 活动卡片列表滚动区
- EventList — 4 张活动卡(当前 Demo 两段共用同一列表,切换只改变高亮态)
与首页同构的"Header + Scroll + Column(space:14)"骨架再次出现,系列统一性延续。

二、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)
}
技术拆解:
- 标题行:"活动"20 号加粗,
safeTop + 52高度、文字底部对齐——与系列其他二级页一致。 - 分段切换行:
ForEach(this.tabs, ...)遍历['进行中', '已结束']两个字符串,每个 Text 用layoutWeight(1)平分整行宽度、textAlign(TextAlign.Center)居中。 - 选中态驱动:
@State tab: number记录当前选中索引,this.tab === idx决定颜色(选中紫色加粗 / 未选灰色常规)——这是"文本模拟分段控件"的经典实现,比Tabs/TabContent轻量得多,适合只有两三个分段的场景。 - 点击切换:
onClick(() => { this.tab = idx; })更新索引,UI 自动重绘。 - 底部内边距:
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日 | 320 | 128 |
| 🏃 秋季运动会 | 10月15日 | 580 | 256 |
| 🎸 校园音乐节 | 11月2日 | 450 | 176 |
| 🎭 话剧社公演 | 11月20日 | 120 | 45 |
卡片细节:
- 大图区:120 高、50 号 emoji 居中、
primarySoft浅紫底、只圆上两角(borderRadius({ topLeft: D.rLg, topRight: D.rLg }))——比首页瀑布流卡(46 号 emoji、140~200 高)更大更醒目,活动卡是"主推对象"。 - 活动名:16 号加粗全宽,信息层级最高。
- 元信息第一行:
📅 日期与👥 N 人参与用Row+layoutWeight(1)分列左右,均为 12 号浅灰。 - 元信息第二行:
📷 N 张照片与查看相册 >用紫色 12 号,与第一行浅灰形成"数据 vs 动作"的语义区分——紫色暗示可点击。 - 整卡交互:点击任意位置 Toast 活动名,
ForEachkey 用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 栏可点。

六、扩展思考:分段切换的三种实现方案
活动页用"文本 + @State"实现了分段切换,工程上还有更丰富的方案:
- 文本模拟(本页方案):
ForEach+ 条件样式 +@State索引。最轻量、零依赖,适合 2-4 个分段;缺点是缺少滑动动画与无障碍语义。 - Tabs + TabContent:ArkUI 原生分段容器,自带滑动切换动画与手势,适合"分段内容差异大、需要独立滚动"的场景;缺点是结构较重,每段需要独立内容区。
- 分段指示条:在选中 Text 下方渲染一个 2-3 高的圆角条(
@State驱动的位移或条件渲染),配合animation实现滑动指示动画——比纯文本高亮更精致,是电商/资讯类 App 的常见做法。 - 胶囊分段:把选中项包成紫色圆角胶囊(类似兴趣页选中态),未选中为浅底——视觉更"按钮化",适合操作型分段。
本页的文本模拟方案在"只有两个分段、内容未分源"的 Demo 阶段完全够用,四种方案的演进路径清晰。
七、扩展思考:活动数据模型的完整设计
从活动页的 Event 接口反推真实产品的活动模型:
- 基础字段:
{ id, emoji, name, date, photos, joined }已覆盖展示所需;真实产品需扩展coverUrl(封面图)、status(进行中/已结束/未开始)、location(地点)、organizer(主办方)、description(简介)。 - status 驱动分段:
tab === 0 ? events.filter(e => e.status === 'ongoing') : events.filter(e => e.status === 'ended')——分段切换从"改高亮"升级为"过滤数据",一行动态过滤即可。 - 照片与活动的关系:一个活动对应一个相册(一对多:activity → photos),活动卡的"📷 N 张照片"即相册的 count——首页相册与活动页共享这张映射表。
- 参与人数:
joined可实时增长(用户点"参加"按钮 +1),配合上传页的"关联活动"胶囊(迎新晚会/秋季运动会/校园音乐节/话剧公演),上传照片即写入对应活动的照片集——三页联动的数据总线就此打通。
八、常见问题与开发避坑
- 分段切换"没变化":Demo 两段共用同一列表,用户切换后只见高亮变化、不见内容变化,可能误以为 Bug。真实实现必须绑定不同数据源,否则建议在 Demo 文案中说明"切换仅改变状态高亮"。
- textAlign 需要固定宽度:
layoutWeight(1)已使 Text 占满列宽,此时textAlign(TextAlign.Center)才能生效;若 Text 宽度自适应内容,居中无效。 - emoji 大图区的溢出:50 号 emoji 在 120 高容器内垂直居中(
justifyContent(FlexAlign.Center)),若 emoji 换成真实图片,记得加objectFit(ImageFit.Cover)裁切,否则图片会拉伸变形。 - 圆角与图片裁切:与首页同理,若大图区放网络图片,需外层容器
clip(true)否则图片溢出圆角。 - 列表空态:若某分段无活动(如"已结束"暂无数据),页面只剩标题一片空白——应补充空态视图("暂无已结束活动" + 图标),工程化必备。

九、扩展思考:活动详情页与参与流程设计
活动页的卡片点击后(当前 Toast)应进入活动详情页,围绕"了解活动 → 参与活动 → 回顾照片"设计完整流程:
- 详情页信息架构:封面大图 + 活动名 + 时间/地点/主办方(可复用活动卡的四行信息结构放大)+ 活动简介 + "参与/已参与"状态按钮 + 相册入口 + 评论区。活动卡已有的
date/joined/photos字段全部成为详情页的头图信息。 - 参与状态机:
未参与 → 已参与 → 活动结束三态——对应分段"进行中/已结束":进行中的活动可点"参加"(joined +1),已结束的活动展示"回顾照片"。@State joined: boolean驱动按钮文案与色态切换。 - 倒计时组件:进行中的活动可在详情页头部展示"距开始还有 N 天"(
setInterval定时刷新),增强紧迫感与参与动力——@ohos.timer或 ArkUI 定时器可实现。 - 活动与照片的联动:详情页相册入口跳转首页对应相册;活动结束时相册自动"封存"并计入"已结束"分段——活动状态驱动照片可见性,是活动照片共享类产品的核心业务规则。
- 分享传播:详情页配"分享"按钮,生成活动海报(封面 + 时间 + 参与人数)分享到社交平台,为平台引流——照片社区的拉新入口。
十、扩展思考:时间线与活动的生命周期管理
从活动页的日期字段(9月10日/10月15日/11月2日/11月20日)可以展开活动的完整生命周期:
- 生命周期四阶段:
筹备中 → 进行中 → 已结束 → 归档——当前 Demo 只有"进行中/已结束"两态,真实产品还有"未开始"(预热报名)与"已归档"(照片长期可查)两个阶段。 - 状态自动流转:系统按当前日期与
date对比自动更新状态——new Date() >= date ? 'ended' : 'ongoing',无需人工干预;配合onPageShow刷新,App 打开即是最新状态。 - 照片的时段属性:活动照片可按拍摄时间细分(开幕式/比赛/闭幕式),相册内提供时间线滑动筛选——
@ohos.data的按时间查询能力支撑。 - 历史活动沉淀:已结束活动不删数据,照片持续开放浏览(甚至支持按届数归档:"2026 迎新晚会" vs "2025 迎新晚会"),平台内容随时间沉淀,Banner"已收录 12 场活动 · 858 张照片"即这种沉淀的量级体现。
- 统计与运营:参与人数、照片数、获赞数按活动聚合,可支撑运营看板(哪类活动最受欢迎)——数据在活动卡上已有雏形,聚合查询是进阶能力。
十一、系列横向对比:活动清单类页面的四种范式
活动页属于"清单 + 状态筛选"类页面,本系列提供了多种可对照的实现范式:
| 范式 | 代表页 | 筛选方式 | 列表形态 |
|---|---|---|---|
| 分段切换 | App 40 活动页 | 进行中/已结束文本分段 | 图文大卡 |
| 分类胶囊 | App 38 技能交换 | 6 类横滑胶囊 | 双行技能卡 |
| 搜索过滤 | App 37 考试资料 | 搜索框 + 分类 | 资料列表 |
| Tab 页签 | App 34 打印资料 | 底部 Tab 内分组 | 资料卡 |
App 40 用"双段文本切换"覆盖了活动清单最核心的筛选诉求(当前/历史),交互成本最低、用户零学习成本;而 App 38 的横滑胶囊适合"分类多且需要快速切换"的场景,App 37 的搜索适合"资料量大需要精确检索"的场景。筛选控件的选型取决于备选数量:2-3 个状态用分段、4-8 个分类用横滑胶囊、大量条目用搜索——这是清单类产品设计的第一决策点。
活动卡本身的信息结构(emoji 大图 + 名称 + 两行元信息)与 App 38 技能卡(头像 + 名称 + 标签 + 按钮)同属"图文卡"家族,但活动卡弱化操作按钮("查看相册 >"是文字链而非按钮)、强化数据信息(日期/人数/照片数)——因为活动的"转化动作"是低频的(报名/查看),而技能的"交换"是高频操作。信息组织的优先级永远服务于核心动作,这一原则在两张卡片的设计差异中体现得淋漓尽致。

十二、开发者视角:分段切换与列表联动的调试技巧
- @State 切换无效的排查:若点击分段后高亮不变化,先确认
onClick是否真正触发(可临时加 console 日志),再确认@State tab是否被正确赋值——常见坑是this.tab = idx写成了局部变量赋值。 - 分段与列表的数据过滤:产品化后
tab === 0 ? 进行中 : 已结束的过滤逻辑建议写成计算属性(get currentEvents()),避免在构建函数里写复杂条件分支——既清晰又便于单测。 - 大图区的图片替换:把 emoji 换成真实活动封面时,务必加
objectFit(ImageFit.Cover)与固定宽高,否则不同尺寸的封面会拉伸变形破坏卡片统一性——回归测试时用竖图、横图、方形图各试一遍。 - 日期显示与时区:
📅 9月10日若来自服务端 ISO 日期,注意时区转换(UTC → 本地),否则可能显示成 9 月 9 日——用@ohos.i18n的日期格式化接口做本地化。 - 列表滚动性能:活动卡含 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 张"等数据与页面内其他位置一致,再次验证了数据自洽的验收标准。 整体而言,本页以最小的代码量实现了清单、筛选、数据三类能力,配合四张实机截图的完整验证,为校园活动场景交出了一份高质量的参考实现。

更多推荐



所有评论(0)