鸿蒙原生应用实战:用 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 块结构

  1. Header — 标题"校园投票" + 3 Tab(进行中/即将结束/热门)
  2. 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双层(校园投票+进行中/即将结束/热门3Tab)+4张投票卡(状态色+剩余时间+参与投票按钮)

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

二、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 种信息

  1. 左侧 56×56 emoji 块C.primarySoft 浅紫底 + 🏆/🎨/⚽/🍜 emoji)
  2. 中间 标题 + 总票数layoutWeight(1) 占主要空间)
  3. 右侧 状态色 + 剩余时间(两行紧凑)

"状态色" + "剩余时间"——两个"紧迫感"信号

  • 状态色告诉用户"这个投票处于什么状态"(进行中/即将结束/热门)
  • 剩余时间告诉用户"还剩多久"(3 天/6 小时/2 天)

"双信号"让用户做出投票决策——"还剩 6 小时" + 橙色 = 立刻行动。

3.3 紫色"参与投票"按钮

Button('参与投票') 100% 宽 38vp 紫底白字按钮——整张卡唯一的"行动"入口(点击跳到 Func1Tab 投票页)。

100% 宽 让按钮"贯穿"整张卡——视觉上"参与投票"是这张卡唯一的目的(不是"查看详情"那种次要操作)。

borderRadius(D.rSm) 圆角 10(按钮)——比卡片圆角 20 略小——**"按钮圆角 ≤ 卡片圆角"**是设计原则(按钮嵌在卡片内,圆角不能比卡片大)。

四、"假筛选"与 3 Tab 的关系

@State tab: number = 0 控制 3 个 Tab 的视觉(哪个选中),但 VoteListForEach(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 个 @Statetab(当前 Tab 0-2)。其余数据 private 不可变。

主题色 #7C3AED 紫色——投票/决策/调查类 App 的常见色(SurveyMonkey 用紫色、Tally Form 用绿色、Typeform 用深色——紫色 = 智能/选择/民主决策的隐喻)。紫色调性契合"投票"的庄严感

C.primarySoft: '#F1EAFE' 浅紫——背景块/胶囊底色。C.accent: '#A78BFA' 浅紫——渐变副色。

校园投票首页中段 · 切换到"即将结束"Tab · 文字色+字重变化

六、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 的耦合度"。

校园投票首页下部 · 切换到"热门"Tab · 状态色高亮

七、"剩余时间"的双通道设计

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 张图的"进行中/即将结束/热门"字色变化。

校园投票首页底部 · 切回"进行中"Tab · 默认状态

九、本页与系列其他首页的对比

App首页定位Tab 位置主列表
13/17 音乐/志愿内容流无 Tab3 项目
14 运动行动型无 Tab5 场地
15 环保知识型无 Tab4 指南
16 商城交易型无 Tab5 商品
18 社交人脉型无 Tab4 同好
19 摄影创作型无 Tab4 作品
20 阅读学习型无 Tab4 书
21 问卷任务型Chip(横滑)5 问卷
22 投票决策型Header 内嵌 Tab4 投票

"投票" = 决策型——用户来"做决定"(选最喜欢的歌手/菜品/运动)——Header 内嵌 Tab 让"按时间状态分类"更醒目

"任务型 = Chip(横滑)"、"决策型 = Header 内嵌 Tab"——两种"分类"的不同 UI 形态。

十、3 状态投票卡的"优先级"设计

App 22 投票卡的 3 种状态(进行中/即将结束/热门)按"用户行动优先级"排序:

优先级从高到低

  1. 即将结束(剩 6 小时,橙色警示)——"立刻投"——最高优先级
  2. 热门(高票数,红色突出)——"凑热闹"——次高优先级
  3. 进行中(剩 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 内嵌 TabApp 22 首页Header 内部文字色+字重分类少 + 强调分类
列表上方下划线 TabApp 13/14/16/18/20 记录列表上方24×2 紫条分类 2-3 个
横滑 ChipApp 14/15/16/19/21 主页列表上方胶囊横滑分类 4-6 个
列表上方胶囊App 18 消息列表上方圆角胶囊分类 4 个
底部 Tab BarApp 11-22 主导航屏幕底部4 个 Tab4 个一级页面

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 个关键属性

  1. layoutWeight(1) —— 让 Scroll 占满 Header 以下的所有剩余空间——**"Header 固定 + 列表滚动"**的经典布局。

  2. scrollBar(BarState.Off) —— 隐藏滚动条——系列标准做法(所有列表页都隐藏滚动条)——"隐藏滚动条 = 视觉更干净"

  3. 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 容器 + 安全区适配确保全面屏下底部不被遮挡——这是所有列表页的通用适配方案。

配图

Logo

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

更多推荐