鸿蒙原生应用实战:用 ArkUI 搭投票首页 —— Header 内嵌三 Tab 设计
鸿蒙原生应用实战:用 ArkUI 搭投票首页 —— Header 内嵌三 Tab 设计
App 22「校园投票管理」首页(HomeTab),主题色
#7C3AED紫色,4 个 Tab 分别为首页(🗳️)、投票(✅)、结果(📊)、我的(👤)。首页采用**"Header 内嵌 3 Tab"独特设计**——固定 Header(标题"校园投票" + 内嵌进行中/即将结束/热门 3 个下划线 Tab)+ 4 张投票卡(56×56 emoji + 标题 + 总票数 + 状态色 + 剩余时间 + 紫色"参与投票"按钮)。本篇基于22-campus-vote/entry/src/main/ets/pages/HomeTab.ets(约 113 行)逐段拆解,附 4 张实机截图。
一、整体结构:Header 双层 + 投票列表
App 22 首页是"Header + Scroll 投票列表"——但 Header 是双层结构(标题层 + Tab 层),投票列表只有 1 个 @Builder:
build() {
Column() {
this.Header()
Scroll() {
Column({ space: 14 }) {
this.VoteList()
}
.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)
}
2 块结构:
- Header — 标题"校园投票" + 3 Tab(进行中/即将结束/热门)
- VoteList — 4 张投票卡
与系列其他首页对比:
- App 21 问卷首页 3 块(Header + Chip + List)——Header 单独 1 行
- App 22 投票首页 2 块(Header 2 行 + List)——Header 内嵌 Tab
"Header 内嵌 Tab"是 App 22 的特色——首页需要"按时间状态分类"(进行中/即将结束/热门)时,把 Tab 放在 Header 内比放在 List 上方更紧凑。

二、Header:双层结构(标题 + 3 Tab)
Header 是标题层 + Tab 层双层结构(系列唯一):
@Builder Header() {
Column() {
Row() {
Text('校园投票')
.fontSize(22).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 层(标题):
- "校园投票" 22sp 加粗深色
height(this.safeTop + 52)让出状态栏 + 52vp 标题alignItems(VerticalAlign.Bottom)内容靠底- 没用
bottom边框(与 Tab 层自然衔接)
第 2 层(Tab):
- 3 个 Tab(进行中/即将结束/热门)等宽分布
layoutWeight(1)3 个均分textAlign(TextAlign.Center)文字居中padding({ bottom: 12 })给 Tab 文字下留 12vp(视觉缓冲)- 没有下划线(与 App 14/16/18 的"24×2 下划线"不同)——只靠"文字色 + 字重"区分选中
@State tab: number = 0 默认选中"进行中"——onClick(() => { this.tab = idx; }) 切换——注意 demo 未联动过滤(点击 Tab 只切换文字样式,列表永远是全部 4 张)——"假筛选"——真实项目应加 filteredVotes() 按 status 过滤。
2.1 "无下划线 Tab" vs "有下划线 Tab"
App 14/16/18 的 Tab 是"24×2 下划线指示器"(系列标准),App 22 简化成"无下划线"——只靠文字色(紫/灰)+ 字重(加粗/常规)区分选中:
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 })
"无下划线"的优势:更简洁、更轻量——Header 不显笨重。 "无下划线"的劣势:用户不知道"哪个是当前选中的"——靠"字重加粗"不够明显(轻度色弱用户难识别)。
"轻量级 Tab 用无下划线、强调 Tab 坐下划线"——本系列两种风格并存。
三、VoteList:4 张投票卡(本页核心)
VoteList 是 4 张投票卡,每张含 56×56 emoji + 标题 + 总票数 + 状态色 + 剩余时间 + 紫色"参与投票"按钮:
@Builder VoteList() {
Column({ space: 12 }) {
ForEach(this.votes, (v: Vote) => {
Column({ space: 10 }) {
Row({ space: 12 }) {
Row() {
Text(v.emoji).fontSize(30)
}
.width(56).height(56)
.backgroundColor(C.primarySoft).borderRadius(D.rMd)
.justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Text(v.title).fontSize(15).fontWeight(FontWeight.Medium).fontColor(C.text)
Text(v.total).fontSize(12).fontColor(C.textDim)
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column({ space: 4 }) {
Text(v.status).fontSize(11).fontColor(v.statusColor)
Text(v.left).fontSize(11).fontColor(C.warn)
}
}
.width('100%')
Button('参与投票')
.width('100%').height(38)
.fontSize(13).fontColor('#FFFFFF').backgroundColor(C.primary)
.borderRadius(D.rSm)
.onClick(() => { promptAction.showToast({ message: v.title }); })
}
.width('100%')
.padding(14)
.backgroundColor(C.card).borderRadius(D.rLg)
.border({ width: 1, color: C.stroke })
}, (v: Vote) => v.id.toString())
}
.width('100%')
}
3.1 4 张投票的数据
private votes: Vote[] = [
{ id: 1, emoji: '🏆', title: '最佳校园歌手评选', total: '1,856 票', status: '进行中', statusColor: C.ok, left: '剩 3 天' },
{ id: 2, emoji: '🎨', title: '校园logo设计方案投票', total: '920 票', status: '即将结束', statusColor: C.warn, left: '剩 6 小时' },
{ id: 3, emoji: '⚽', title: '最受欢迎运动评选', total: '2,100 票', status: '进行中', statusColor: C.ok, left: '剩 5 天' },
{ id: 4, emoji: '🍜', title: '食堂最佳菜品投票', total: '3,560 票', status: '热门', statusColor: C.danger, left: '剩 2 天' }
];
4 张投票的"3 状态 + 校园主题":
| Emoji | 标题 | 状态 | 总票数 | 剩余 |
|---|---|---|---|---|
| 🏆 | 最佳校园歌手评选 | 进行中(绿) | 1,856 票 | 剩 3 天 |
| 🎨 | 校园 logo 方案 | 即将结束(橙) | 920 票 | 剩 6 小时 |
| ⚽ | 最受欢迎运动 | 进行中(绿) | 2,100 票 | 剩 5 天 |
| 🍜 | 食堂最佳菜品 | 热门(红) | 3,560 票(最高) | 剩 2 天 |
3 状态三色映射:
- 进行中 →
C.ok绿(活跃/好状态) - 即将结束 →
C.warn橙(警示/快截止) - 热门 →
C.danger红(高热度/紧急)
"剩 6 小时"是即将结束——C.warn 橙色(警告用户"快去投")——**"截止倒计时 = 警示色"**的合理映射。
"3,560 票"是热门(食堂投票参与度最高)——"高票数 = 热门"——食堂是校园最关心的话题(民以食为天),1 万学生投票参与形成热门。
3.2 投票卡的"三色"信息
每张投票卡含 3 种信息:
- 左侧 56×56 emoji 块(
C.primarySoft浅紫底 + 🏆/🎨/⚽/🍜 emoji) - 中间 标题 + 总票数(
layoutWeight(1)占主要空间) - 右侧 状态色 + 剩余时间(两行紧凑)
"状态色" + "剩余时间"——两个"紧迫感"信号:
- 状态色告诉用户"这个投票处于什么状态"(进行中/即将结束/热门)
- 剩余时间告诉用户"还剩多久"(3 天/6 小时/2 天)
"双信号"让用户做出投票决策——"还剩 6 小时" + 橙色 = 立刻行动。
3.3 紫色"参与投票"按钮
Button('参与投票') 100% 宽 38vp 紫底白字按钮——整张卡唯一的"行动"入口(点击跳到 Func1Tab 投票页)。
100% 宽 让按钮"贯穿"整张卡——视觉上"参与投票"是这张卡唯一的目的(不是"查看详情"那种次要操作)。
borderRadius(D.rSm) 圆角 10(按钮)——比卡片圆角 20 略小——**"按钮圆角 ≤ 卡片圆角"**是设计原则(按钮嵌在卡片内,圆角不能比卡片大)。
四、"假筛选"与 3 Tab 的关系
@State tab: number = 0 控制 3 个 Tab 的视觉(哪个选中),但 VoteList 的 ForEach(this.votes, ...) 没有联动过滤——4 张卡永远全部展示。
真实项目应实现"按 status 过滤":
private filtered(): Vote[] {
if (this.tab === 0) return this.votes; // 全部
if (this.tab === 1) return this.votes.filter(v => v.status === '进行中');
if (this.tab === 2) return this.votes.filter(v => v.status === '即将结束');
return this.votes.filter(v => v.status === '热门');
}
tab 一变 → filtered() 重算 → ForEach 重渲染。这是本系列"假筛选"共有的简化(App 13/17/20/22 都是)。
"假筛选"在 3 Tab 的设计里影响:用户点"即将结束"看不到其他 Tab 的内容,会以为"现在只有 1 个即将结束的投票"——误导用户——真实项目必须联动过滤。
五、@State 与主题色
HomeTab 只有 1 个 @State:tab(当前 Tab 0-2)。其余数据 private 不可变。
主题色 #7C3AED 紫色——投票/决策/调查类 App 的常见色(SurveyMonkey 用紫色、Tally Form 用绿色、Typeform 用深色——紫色 = 智能/选择/民主决策的隐喻)。紫色调性契合"投票"的庄严感。
C.primarySoft: '#F1EAFE' 浅紫——背景块/胶囊底色。C.accent: '#A78BFA' 浅紫——渐变副色。

六、Header 内嵌 Tab 的设计模式
App 22 的"Header 内嵌 Tab"是"双层 Header"——标题层 + Tab 层:
┌──────────────────────────┐
│ 校园投票 (标题) │ <- 标题层
├──────────────────────────┤
│ 进行中 即将结束 热门 │ <- Tab 层
├──────────────────────────┤
│ [投票卡 1] │
│ [投票卡 2] │
│ [投票卡 3] │ <- List
│ [投票卡 4] │
└──────────────────────────┘
"双层 Header"适用场景:
- 主页有"按状态/类型分类"的需求(进行中/即将结束/热门)
- Tab 数量少(2-3 个)
- Tab 切换不联动复杂过滤
"双层 Header"不适用场景:
- Tab 数量多(4+)——应单独 Header + 独立 Tab 区
- Tab 切换联动复杂过滤——应让 List 区域更大
"Header 内嵌 Tab"是 App 22 的设计选择——"哪些场景用 Header 内嵌 Tab" 取决于"Tab 与 List 的耦合度"。

七、"剩余时间"的双通道设计
App 22 投票卡显示"剩 3 天/剩 6 小时/剩 5 天/剩 2 天"——"剩余时间"是投票场景的特有维度(对比问卷的"截止日期"、商城的"剩余库存")。
"剩余时间"的 3 种精度:
- 天(剩 3 天)—— 长期投票,时间粒度"天"
- 小时(剩 6 小时)—— 紧急投票,时间粒度"小时"
- 分钟(剩 30 分钟)—— 临截止投票,时间粒度"分钟"
App 22 演示了"天/小时"两种精度——真实项目应根据截止时间动态选择精度。
"剩余时间"是倒计时的"倒序显示"——投票刚发布是"剩 7 天"、过半是"剩 3 天"、临截止是"剩 6 小时"——"剩余 = 截止 - 当前"——真实项目应后端实时计算(避免静态数据不准)。
八、3 Tab 切屏的 4 张实机图
本文 4 张图展示 3 个 Tab 的不同状态:
- top(默认 Tab="进行中"):4 张投票全部展示
- mid(点"即将结束"Tab):高亮移到"即将结束",4 张卡同样展示(因为是假筛选)
- lower(点"热门"Tab):高亮移到"热门"
- bottom(切回"进行中"):高亮回到"进行中"
虽然 4 张图内容相同(投票卡不变),但 Tab 高亮位置不同——"假筛选"的可视化证据——读者可对比看 4 张图的"进行中/即将结束/热门"字色变化。

九、本页与系列其他首页的对比
| App | 首页定位 | Tab 位置 | 主列表 |
|---|---|---|---|
| 13/17 音乐/志愿 | 内容流 | 无 Tab | 3 项目 |
| 14 运动 | 行动型 | 无 Tab | 5 场地 |
| 15 环保 | 知识型 | 无 Tab | 4 指南 |
| 16 商城 | 交易型 | 无 Tab | 5 商品 |
| 18 社交 | 人脉型 | 无 Tab | 4 同好 |
| 19 摄影 | 创作型 | 无 Tab | 4 作品 |
| 20 阅读 | 学习型 | 无 Tab | 4 书 |
| 21 问卷 | 任务型 | Chip(横滑) | 5 问卷 |
| 22 投票 | 决策型 | Header 内嵌 Tab | 4 投票 |
"投票" = 决策型——用户来"做决定"(选最喜欢的歌手/菜品/运动)——Header 内嵌 Tab 让"按时间状态分类"更醒目。
"任务型 = Chip(横滑)"、"决策型 = Header 内嵌 Tab"——两种"分类"的不同 UI 形态。
十、3 状态投票卡的"优先级"设计
App 22 投票卡的 3 种状态(进行中/即将结束/热门)按"用户行动优先级"排序:
优先级从高到低:
- 即将结束(剩 6 小时,橙色警示)——"立刻投"——最高优先级
- 热门(高票数,红色突出)——"凑热闹"——次高优先级
- 进行中(剩 3-5 天,绿色)——"慢慢投"——低优先级
"优先级 = 视觉强度":
- 即将结束(剩 6 小时)→
C.warn橙——"警示色 = 优先" - 热门(3,560 票)→
C.danger红——"突出色 = 流量" - 进行中(剩 3-5 天)→
C.ok绿——"平静色 = 常规"
颜色编码对应"行动优先级"——用户看到橙色会想"快去投"、看到绿色会想"不急"、看到红色会想"很多人在投"——"颜色 = 行动指南"。
"剩 6 小时"特别短——这是合理的紧迫感设计(让用户感觉"错过就没了")——"虚假紧迫感"是投票平台提高参与度的常用心理学(淘宝"剩 1 小时"、滴滴"3 分钟内有司机")。
真实项目的"真实紧迫感":从服务端实时计算 截止 - 当前 倒计时——"剩 X 小时 Y 分"动态显示——比静态"剩 6 小时"更可信。
十一、Header 内嵌 Tab 与"分类 Tab"的对比
App 22 的"Header 内嵌 Tab"与系列其他 Tab 类型对比:
| 类型 | 示例 | 位置 | 视觉 | 适用 |
|---|---|---|---|---|
| Header 内嵌 Tab | App 22 首页 | Header 内部 | 文字色+字重 | 分类少 + 强调分类 |
| 列表上方下划线 Tab | App 13/14/16/18/20 记录 | 列表上方 | 24×2 紫条 | 分类 2-3 个 |
| 横滑 Chip | App 14/15/16/19/21 主页 | 列表上方 | 胶囊横滑 | 分类 4-6 个 |
| 列表上方胶囊 | App 18 消息 | 列表上方 | 圆角胶囊 | 分类 4 个 |
| 底部 Tab Bar | App 11-22 主导航 | 屏幕底部 | 4 个 Tab | 4 个一级页面 |
5 种 Tab 类型覆盖所有分类需求——"Tab 类型跟随分类数量和强调程度"——读者做自己产品时,先数分类数量(2-3/4-6/7+)再选 Tab 类型(下划线/横滑/树形)。
"Header 内嵌 Tab"的优势:
- 节省屏幕空间(不占独立行)
- 视觉强调(Header 高度 = 分类区)
- 与标题形成"主标题 + 子分类"层次
"Header 内嵌 Tab"的劣势:
- Tab 与 List 之间无视觉缓冲
- Tab 切换不联动过滤时,List 不会变(用户困惑)
App 22 的"假筛选"恰好命中劣势——读者可引以为戒:内嵌 Tab 必须配过滤逻辑,否则用户不知道"切 Tab 没用"。
十二、"学校投票"的业务化设计
App 22 的"校园歌手/校园 logo/校园运动/校园菜品"4 个投票——全部围绕"校园"主题——这是"校园场景"的业务化:
校园主题的具体体现:
- 🏆 校园歌手——文艺/活动(校园活动经典项目)
- 🎨 校园 logo——学校形象(设计/品牌)
- ⚽ 校园运动——体育(学校体育节)
- 🍜 校园菜品——生活(食堂主题)
4 个投票覆盖"校园 4 大类":
- 文艺(歌手)
- 形象(logo)
- 体育(运动)
- 生活(菜品)
"校园投票"是学生群体的特色应用——真实校园场景下,学生有"集体决策"需求(选课/选活动/选菜品/选学生干部)——App 22 是这类应用的最简骨架。
真实项目扩展:
- 加"投票广场"(全校学生都能发起)
- 加"实名/匿名投票"切换(敏感话题匿名)
- 加"投票截止提醒"(push 通知)
- 加"实时票数更新"(WebSocket 推送)
十三、VoteList 的 Scroll 容器与安全区适配
VoteList 外层包裹了 Scroll 容器,配合安全区适配:
build() {
Column() {
this.Header()
Scroll() {
Column({ space: 14 }) {
this.VoteList()
}
.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)
}
Scroll 容器的 3 个关键属性:
layoutWeight(1)—— 让 Scroll 占满 Header 以下的所有剩余空间——**"Header 固定 + 列表滚动"**的经典布局。scrollBar(BarState.Off)—— 隐藏滚动条——系列标准做法(所有列表页都隐藏滚动条)——"隐藏滚动条 = 视觉更干净"。align(Alignment.Top)—— 内容顶部对齐——滚动初始位置在顶部——"打开就在第一项"。
安全区适配:padding({ bottom: D.pad + this.safeBottom + 20 })——底部 padding = 基础 padding + 安全区高度 + 20vp 额外留白——"底部 Tab 栏不遮挡最后一张卡片"——这是全面屏(有底部手势条)的必备适配。
Column({ space: 14 }) —— Scroll 内层 Column 的 14vp 间距——"卡片间距 = 呼吸感"——适合"卡片型列表"(信息密度低的列表)。VoteList 内部的 Column({ space: 12 }) 则是 4 张投票卡之间的 12vp 间距。
十四、总结
App 22 首页解析完毕。Header 双层结构(标题 + 3 Tab)+ 4 张投票卡(emoji + 状态色 + 剩余时间 + 参与投票按钮)是核心组件。"Header 内嵌 Tab"是系列独有设计——适合"按状态/类型分类"且 Tab 数量少的场景。**"3 状态优先级 = 颜色强度"**是投票场景的特色设计(橙色警示/红色突出/绿色平静)。Scroll 容器 + 安全区适配确保全面屏下底部不被遮挡——这是所有列表页的通用适配方案。

更多推荐





所有评论(0)