鸿蒙 ArkUI 实战:运动预约首页的绿色 HeroCard 与场地卡片

App 14「运动场地预约」首页(HomeTab),主题色 #16A34A 绿色,4 个 Tab 分别为首页(🏟️)、场地(🏀)、预约(📅)、我的(👤)。首页采用固定 Header + 绿色渐变 HeroCard(校运动会报名 + 3 栏统计 + 搜索框)+ 快捷运动入口 + 本周运动趋势条形图 + 5 个场地卡片(星级/开放时间/价格/预约进度/状态/预约按钮)+ 热门排行。本篇基于 14-sports-booking/entry/src/main/ets/pages/HomeTab.ets(约 320 行)逐段拆解,附 4 张实机截图。

一、整体结构:固定 Header + 内容流

HomeTab 与 App 13 首页的骨架一致——固定 Header 在外面,Scroll 容器装内容流,但内容更丰富(7 个 @Builder 块):

build() {
  Column() {
    this.Header()
    Scroll() {
      Column({ space: 14 }) {
        this.HeroCard()
        this.QuickActionRow()
        this.WeeklyChart()
        this.SectionTitle('运动场地', '查看全部')
        this.VenueList()
        this.SectionTitle('热门排行', '更多')
        this.RankList()
      }
      .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)
}

7 个块按"活动 → 入口 → 数据 → 列表 → 排行"五段叙事

  1. HeroCard — 校运动会报名 + 3 栏统计 + 搜索框(品牌区)
  2. QuickActionRow — 4 个运动入口(导航区)
  3. WeeklyChart — 本周运动趋势(数据区)
  4. SectionTitle('运动场地') + VenueList — 5 个场地卡片(主内容区)
  5. SectionTitle('热门排行') + RankList — 热门排行榜(次级内容区)

关键差异SectionTitle 是参数化 @Builder(SectionTitle(title: string, action: string)),把"区块标题 + 右侧操作链接"抽成可复用组件。两个区块共用,传入不同标题(运动场地/热门排行)和不同操作文字(查看全部/更多)。这是 @Builder 参数化的典型价值——同一块 UI,不同数据,两处复用

padding 的顶部 top: 14 和底部 bottom: D.pad + this.safeBottom + 20 给内容流留出边界呼吸感。

运动场地预约首页首屏 · 绿色渐变HeroCard+运动会报名+搜索框+快捷入口+趋势图

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

配图

二、Header:两行文字的品牌区

Header 与 App 13 不同——不是 Row 而是 Column,两行文字上下排列:

@Builder
Header() {
  Column() {
    Text('运动预约')
      .fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.text)
      .width('100%')
      .padding({ top: this.safeTop + 10, left: D.pad, right: D.pad, bottom: 4 })
    Text('动起来,释放青春活力')
      .fontSize(12).fontColor(C.textDim)
      .width('100%')
      .padding({ left: D.pad, right: D.pad, bottom: 12 })
  }
  .width('100%').backgroundColor(C.card)
}
  • 主标题 "运动预约" 22sp 加粗,深色
  • 副标题 "动起来,释放青春活力" 12sp 灰色

副标题是品牌 Slogan(运动 App 的"口号"),让 Header 从"功能标题"升级为"品牌区"。Slogan 放在 Header 是一种轻量品牌化手法——不占过多空间,但传递 App 的调性(青春、活力)。

padding({ top: this.safeTop + 10 }) 让主标题让出状态栏(safeTop)+ 10vp 缓冲,副标题紧贴其下。Header 用 Column 两行布局比 App 13 的"单行标题+右侧图标"更简洁——首页不需要右侧图标(搜索在 HeroCard 里)。

三、HeroCard:绿色渐变 + 报名活动 + 3 栏统计 + 搜索框

HeroCard 是首页的视觉焦点,比 App 13 的 HeroCard 多了一层"搜索框":

@Builder
HeroCard() {
  Column({ space: 14 }) {
    Row({ space: 12 }) {
      Text('🏆').fontSize(36)
      Column({ space: 4 }) {
        Text('校运动会报名中').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
        Text('9 月 15 日 · 田径场').fontSize(12).fontColor('#FFFFFF').opacity(0.85)
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)
      Text('报名 >').fontSize(12).fontColor('#FFFFFF')
    }
    .width('100%')
    .onClick(() => { promptAction.showToast({ message: '运动会报名' }); })

    Row() {
      ForEach(this.stats, (s: StatItem) => {
        Column({ space: 4 }) {
          Text(s.value).fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Text(s.label).fontSize(11).fontColor('#FFFFFF').opacity(0.8)
        }
        .layoutWeight(1)
      }, (s: StatItem) => s.label)
    }
    .width('100%')

    Row({ space: 8 }) {
      TextInput({ placeholder: '搜索场地名称...', text: this.keyword })
        .onChange((val: string) => { this.keyword = val; })
        .backgroundColor('#33FFFFFF').borderRadius(D.rSm).height(40)
        .layoutWeight(1).placeholderColor('#FFFFFF').placeholderFont({ size: 13 })
        .fontColor('#FFFFFF')
      Text('🔍')
        .fontSize(16).width(40).height(40).textAlign(TextAlign.Center)
        .backgroundColor('#FFFFFF').borderRadius(D.rSm)
        .onClick(() => {
          promptAction.showToast({ message: this.keyword.length > 0 ? '搜索: ' + this.keyword : '请输入关键词' });
        })
    }
    .width('100%')
  }
  .width('100%')
  .padding(16)
  .borderRadius(D.rLg)
  .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
}

3.1 报名活动横幅

顶部一行:🏆 大图标 + "校运动会报名中"主标题 + "9 月 15 日 · 田径场"副标题 + 右侧"报名 >"入口。整行可点击(onClick 弹 Toast "运动会报名")——这是"活动 Banner"的经典结构:标题 + 时间地点 + CTA。

opacity(0.85) 让副标题比主标题淡一些,形成"主次层次"。Text('报名 >') 白色 + > 箭头,暗示"可点击"。

3.2 3 栏统计

stats[{ value: '14', label: '本月预约' }, { value: '28h', label: '运动时长' }, { value: '2.5k', label: '消耗卡路里' }]

  • 本月预约 14 — 量(次数)
  • 运动时长 28h — 时(小时)
  • 消耗卡路里 2.5k — 果(热量)

"量 → 时 → 果"三个维度正好覆盖用户最关心的运动数据。与 App 13 相比,App 13 是"总数/今日/完成率"(效率型),App 14 是"预约/时长/卡路里"(健康型)——数据选择反映产品定位

3.3 半透明白搜索框

搜索框是 App 14 HeroCard 的特色:

Row({ space: 8 }) {
  TextInput({ placeholder: '搜索场地名称...', text: this.keyword })
    .onChange((val: string) => { this.keyword = val; })
    .backgroundColor('#33FFFFFF').borderRadius(D.rSm).height(40)
    .layoutWeight(1).placeholderColor('#FFFFFF').placeholderFont({ size: 13 })
    .fontColor('#FFFFFF')
  Text('🔍')
    .fontSize(16).width(40).height(40).textAlign(TextAlign.Center)
    .backgroundColor('#FFFFFF').borderRadius(D.rSm)
    .onClick(() => {
      promptAction.showToast({ message: this.keyword.length > 0 ? '搜索: ' + this.keyword : '请输入关键词' });
    })
}

半透明白背景 #33FFFFFF(20% 不透明度白色)让输入框在绿色渐变上若隐若现,视觉轻盈。白色占位文字 placeholderColor('#FFFFFF') 与输入文字 fontColor('#FFFFFF') 都是白色,区分度靠"占位 13sp 常规 + 输入加粗"。

右侧 🔍 是 40×40 纯白圆角方块backgroundColor('#FFFFFF')),点击时 this.keyword.length > 0 ? '搜索: ' + this.keyword : '请输入关键词' 做"输入了才搜"的判断——这是搜索交互的入门级实现,真实项目应跳搜索页/过滤列表。

@State keyword: string = '' 是首页唯一的交互状态,用于双向绑定 TextInput。

四、QuickActionRow:4 个圆形运动入口

QuickActionRow 是 4 个运动项目(篮球/羽毛球/足球/网球)的圆形图标入口:

@Builder
QuickActionRow() {
  Row() {
    ForEach(this.quickActions, (q: QuickAction) => {
      Column({ space: 6 }) {
        Row() {
          Text(q.emoji).fontSize(26)
        }
        .width(48).height(48)
        .backgroundColor(C.primarySoft).borderRadius(24)
        .justifyContent(FlexAlign.Center)
        Text(q.label).fontSize(11).fontColor(C.textSub)
      }
      .layoutWeight(1)
      .onClick(() => { promptAction.showToast({ message: q.label + '场地' }); })
    }, (q: QuickAction) => q.id.toString())
  }
  .width('100%')
  .padding({ top: 14, bottom: 14 })
  .backgroundColor(C.card).borderRadius(D.rMd)
  .border({ width: 1, color: C.stroke })
}

圆形图标块width(48).height(48).borderRadius(24) — 48×48 方块 + 24 圆角(一半)即正圆。C.primarySoft 浅绿背景 + emoji 图标。

与 App 13 的差异

  • App 13 是 borderRadius(D.rSm) 方形圆角(10vp),App 14 是 borderRadius(24) 正圆
  • App 13 图标块 44×44,App 14 是 48×48(更大更圆润)
  • App 13 有边框,App 14 无边框(圆角够柔和不需边框)

"圆形 vs 圆角方形"是入口设计的视觉语言选择:圆形更活泼(适合运动、游戏),方形更正式(适合工具、办公)。App 14 用圆形呼应"运动活力"定位。

点击弹 q.label + '场地'("篮球场地""羽毛球场")——"轻反馈"策略的延续

五、WeeklyChart:带平均值的运动趋势图

WeeklyChart 是本周 7 天的运动趋势条形图,右上角显示"平均 61%":

@Builder
WeeklyChart() {
  Column({ space: 12 }) {
    Row() {
      Text('本周运动趋势').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
      Blank()
      Text('平均 61%').fontSize(12).fontColor(C.primary)
    }
    .width('100%')

    Row({ space: 4 }) {
      ForEach(this.barData, (b: BarItem) => {
        Column({ space: 6 }) {
          Stack({ alignContent: Alignment.Bottom }) {
            Column()
              .width(18).height(b.value)
              .linearGradient({ angle: 180, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
              .borderRadius(4)
          }
          .width(24).height(90)
          Text(b.day).fontSize(10).fontColor(C.textDim)
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
      }, (b: BarItem) => b.day)
    }
    .width('100%')
  }
  .width('100%')
  .padding(14)
  .backgroundColor(C.card).borderRadius(D.rMd)
  .border({ width: 1, color: C.stroke })
}

柱子高度 = b.value(value 是 40/65/50/80/90/70/35 的百分比数),最高 90(周五)→ 最低 35(周日)。与 App 13 不同,App 13 用 b.value / 7 * 80 归一化到 80vp 上限,App 14 直接用 value 当高度(40-90vp)——因为数据本身是"百分比",数值即高度更直观。

Stack({ alignContent: Alignment.Bottom }) 底部对齐:柱子从容器底部"长"起来(而不是从顶部),.width(24).height(90) 是容器(90vp 高),柱子 .width(18).height(b.value) 从底部对齐。这是条形图"从下往上长"的标准实现——如果不加 Stack Bottom,柱子会从容器顶部开始显示,视觉上"倒挂"。

"平均 61%" 是 7 天数值的平均(40+65+50+80+90+70+35=430,430/7≈61.4),用"平均值"比"总计"更能反映"日常强度"。(40+65+50+80+90+70+35)/7 ≈ 61.4,与显示 61% 吻合——数据不是随便写的,是有数学根据的

barData 的"周五 90 最高"与 App 13 一致(周五是工作日尾巴,运动意愿强),但 App 14 的周日 35 是最低(休息日,运动少)——符合"周五冲、周日躺"的校园运动规律。

六、SectionTitle:参数化区块标题

SectionTitle 是首页最精巧的"复用单元":

@Builder
SectionTitle(title: string, action: string) {
  Row() {
    Text(title).fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text)
    Blank()
    Text(action + ' >').fontSize(12).fontColor(C.textDim)
  }
  .width('100%')
}

接收 2 个参数title(区块标题,如"运动场地")和 action(右侧操作文字,如"查看全部")。两处调用:

this.SectionTitle('运动场地', '查看全部')
this.SectionTitle('热门排行', '更多')

同一 @Builder,不同参数,两处复用。这种"参数化区块标题"几乎适用于所有列表页——任何"标题 + 操作"的区块都能用。

Blank() 把标题推到左侧、操作推到右侧。action + ' >' 拼接箭头暗示可点击。

运动场地预约首页底部 · 热门排行完整+底部Tab栏

七、VenueList:5 个场地卡片(最复杂的 @Builder)

VenueList 是整页的"主内容"——5 个场地卡片,每个都含 emoji 图标、名称、星级、开放时间、价格、预约按钮、预约进度条、状态、详情入口:

@Builder
VenueList() {
  Column({ space: 12 }) {
    ForEach(this.venues, (v: Venue) => {
      Column({ space: 10 }) {
        Row({ space: 12 }) {
          Row() {
            Text(v.emoji).fontSize(28)
          }
          .width(56).height(56)
          .backgroundColor(C.primarySoft).borderRadius(D.rMd)
          .justifyContent(FlexAlign.Center)

          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(v.name).fontSize(15).fontWeight(FontWeight.Medium).fontColor(C.text)
              Row({ space: 1 }) {
                ForEach([0, 1, 2, 3, 4], (i: number) => {
                  Text(i < v.rating ? '★' : '☆')
                    .fontSize(11).fontColor(i < v.rating ? C.warn : C.textDim)
                }, (i: number) => i.toString())
              }
            }
            .width('100%')
            Text('开放 ' + v.open + ' · ' + v.price).fontSize(12).fontColor(C.textDim)
          }
          .alignItems(HorizontalAlign.Start).layoutWeight(1)

          Button('预约')
            .fontSize(12).fontColor('#FFFFFF').backgroundColor(C.primary)
            .borderRadius(D.rSm).height(32)
            .onClick(() => { promptAction.showToast({ message: '预约 ' + v.name }); })
        }
        .width('100%')

        Row({ space: 8 }) {
          Text('预约进度').fontSize(11).fontColor(C.textDim)
          Progress({ value: v.booked * 100 / v.total, total: 100 })
            .color(C.primary).backgroundColor(C.cardSoft)
            .layoutWeight(1).height(6)
          Text(v.booked + '/' + v.total).fontSize(11).fontColor(C.textSub)
        }
        .width('100%')

        Row() {
          Text(v.status).fontSize(11).fontColor(v.statusColor).fontWeight(FontWeight.Medium)
          Blank()
          Text('查看详情 >').fontSize(11).fontColor(C.primary)
        }
        .width('100%')
      }
      .width('100%')
      .padding(14)
      .backgroundColor(C.card).borderRadius(D.rMd)
      .border({ width: 1, color: C.stroke })
      .onClick(() => { promptAction.showToast({ message: v.name }); })
    }, (v: Venue) => v.id.toString())
  }
  .width('100%')
}

7.1 卡片头部三区布局

第一行分三区:

  1. 左侧 56×56 浅绿 emoji 图标(🏀/🏸/⚽/🎾/🏓)
  2. 中间"名称 + 星级 + 开放时间 + 价格"两行
  3. 右侧"预约"按钮

星级渲染ForEach([0, 1, 2, 3, 4]) 造 5 颗星循环:Text(i < v.rating ? '★' : '☆') 实心/空心,fontColor(i < v.rating ? C.warn : C.textDim) 金色/灰色。"固定数组造循环"是星级渲染的标准手法——不硬编码 5 颗星,而是用 rating 值决定哪几颗实心。5 个场地:篮球场 5 星、羽毛球场 4 星、足球场 5 星、网球场 4 星、乒乓球场 4 星——评分数据有区分度,不是全 5 星。

开放时间 + 价格 '开放 ' + v.open + ' · ' + v.price 拼接:08:00-22:00 · 免费08:00-21:00 · ¥5/时…… 免费/¥5/¥10/¥8 的价格梯度让用户一眼对比。

7.2 预约进度条(官方 Progress 组件)

Row({ space: 8 }) {
  Text('预约进度').fontSize(11).fontColor(C.textDim)
  Progress({ value: v.booked * 100 / v.total, total: 100 })
    .color(C.primary).backgroundColor(C.cardSoft)
    .layoutWeight(1).height(6)
  Text(v.booked + '/' + v.total).fontSize(11).fontColor(C.textSub)
}

这是本系列首次用官方 Progress 组件(App 11/12/13 都用 Stack 自定义进度条)!

Progress({ value: v.booked * 100 / v.total, total: 100 })
  .color(C.primary).backgroundColor(C.cardSoft)
  • value = booked * 100 / total — 已预约数占比(篮球场 8/10=80%)
  • total = 100 — 百分比总量
  • .color(C.primary) 绿色前景 + .backgroundColor(C.cardSoft) 灰色背景

Progress vs Stack 的取舍:简单线性进度用 Progress(代码短、系统级渲染优化),复杂视觉(渐变/圆角/多段)用 Stack。这里 Progress 的 height(6) 圆角由系统处理,layoutWeight(1) 撑满剩余宽度。

"8/10" 与 80% 双通道:左侧"预约进度"标签 + 进度条 80% + 右侧"8/10"数字,三者表达同一信息(已预约比例),色弱用户也能读。

7.3 状态 + 详情入口

Row() {
  Text(v.status).fontSize(11).fontColor(v.statusColor).fontWeight(FontWeight.Medium)
  Blank()
  Text('查看详情 >').fontSize(11).fontColor(C.primary)
}

状态用 v.statusColor 字段(数据携带颜色):

  • "2 场空闲" → C.ok 绿(有余量)
  • "1 场空闲" → C.ok 绿
  • "已约满" → C.danger 红(爆满)
  • "空闲" → C.ok 绿

"空闲/已约满"是场地可预约性的关键信号,红色"已约满"会立刻让用户转移目标。Blank() 把"查看详情 >"推到右侧,绿色主色。

booked/total 数据一致性:篮球场 8/10(80% 已约)、足球场 12/12(100% 已约满)、网球场 3/8(37.5% 空闲多)——预约进度条与状态文字互相印证(足球场 100% 满 + "已约满"),数据不是孤立的。

运动场地预约首页中段 · 运动场地列表前2个场地卡片+星级+进度条

八、RankList:3 条热门排行

RankList 是热门场地排行(篮球场/羽毛球场/足球场):

@Builder
RankList() {
  Column({ space: 10 }) {
    ForEach(this.rankings, (r: RankItem) => {
      Row({ space: 12 }) {
        Text(r.emoji).fontSize(22)
        Text(r.name).fontSize(14).fontColor(C.text).fontWeight(FontWeight.Medium).layoutWeight(1)
        Column({ space: 2 }) {
          Text(r.count).fontSize(12).fontColor(C.textSub)
          Text(r.trend).fontSize(11).fontColor(C.ok)
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')
      .padding({ left: 14, right: 14, top: 12, bottom: 12 })
      .backgroundColor(C.card).borderRadius(D.rSm)
      .border({ width: 1, color: C.stroke })
      .onClick(() => { promptAction.showToast({ message: r.name }); })
    }, (r: RankItem) => r.id.toString())
  }
  .width('100%')
}

3 条排行的数据

  • 🥇 篮球场 — 128 次预约,+12%
  • 🥈 羽毛球场 — 96 次预约,+8%
  • 🥉 足球场 — 72 次预约,+5%

奖牌 emoji 表排名:🥇🥈🥉 让排行"一眼看懂"(金色/银色/青铜)。预约次数 + 增长率双维度:次数显示"热度绝对值",增长率(+12%/+8%/+5% 绿色)显示"增长趋势"——排行不只是"数次数",还体现"谁在涨"

排行与上方场地列表互相呼应:篮球场(第 1 名 128 次)正好是场地列表第一条 5 星场地,数据一致性强,让首页"自洽"。

alignItems(HorizontalAlign.End) 让右侧"次数 + 增长率"右对齐(数字靠右,视觉整齐)。

运动场地预约首页下部 · 场地列表完整+已约满状态+热门排行

九、数据编排的"故事性"

App 14 首页的 5 个场地 + 3 条排行 + 7 天趋势,数据编排非常讲究"互相印证":

  1. 足球场已约满(12/12,100%,红色)→ 排行第 3(72 次预约)——"约满的场地预约次数反而少",因为"约满=不再能约",次数是历史累计
  2. 篮球场最热(5 星 + 8/10 + 排行第 1)——"评分高 + 预约多 + 排名高"三合一,用户一看就懂"篮球场是首选"
  3. 周五趋势最高 90% ——"一周之最"在周五,对应学生"周末前最后一次运动"的规律

"数据即叙事":真实项目的数据是"测出来的",demo 的数据是"编出来但讲究因果的"。好的 demo 数据能让用户第一眼信服,而"信服感"来自数据之间的自洽(评分↔预约↔排行↔趋势)。这是 demo 设计容易被忽略但极其重要的环节。

十、首页模板总结

把 App 14 首页抽象成模板:

固定 Header(品牌区:标题 + Slogan)
Scroll(
  HeroCard(活动 Banner + 3 栏统计 + 搜索框)+
  QuickActionRow(4 个圆形入口)+
  WeeklyChart(趋势条形图 + 平均值)+
  SectionTitle('运动场地') + VenueList(5 个场地卡片)+
  SectionTitle('热门排行') + RankList(3 条排行)
)

这套骨架适合"运动/健身/场所预约"类应用。与 App 13(音乐歌单)的差异

维度App 13 首页App 14 首页
主题色紫 #8B5CF6绿 #16A34A
Header无(HeroCard 兼作)有(标题+Slogan)
HeroCard欢迎语+统计活动+统计+搜索框
主列表3 条推荐5 个场地
数据图可点击周图静态周图+平均
特色分类筛选星级+进度+排行

同一个"内容流首页"模板,换主题色、换数据、换模块,就变成完全不同的应用——这正是系列化 demo 的核心价值。

十一、首页的搜索联动与数据流

@State keyword: string = '' 是首页唯一的交互状态,它目前的用途只有两个:一是双向绑定搜索框(用户输入实时更新 keyword),二是搜索按钮点击时判断"有没有关键词"(this.keyword.length > 0 ? '搜索: ' + this.keyword : '请输入关键词')。

真实项目里,搜索应该联动场地列表。当 keyword 非空时,VenueList 应该只显示名称包含 keyword 的场地:

// 伪代码:搜索联动
private filteredVenues(): Venue[] {
  if (this.keyword.trim() === '') return this.venues;
  return this.venues.filter(v => v.name.includes(this.keyword.trim()));
}

然后 ForEach(this.filteredVenues(), ...) 替代 ForEach(this.venues, ...)——@State keyword 一变,filteredVenues 重算,列表实时过滤。这是 ArkUI 声明式搜索的标准实现:状态驱动 + 计算属性过滤。

如果进一步,搜索框在 HeroCard 里、列表在下方很远的位置,用户搜索时可能看不到反馈。真实项目应在搜索框下方弹出"搜索结果浮层"(或者把 HeroCard 折叠起来),让输入和结果在同一视野。这是搜索交互的进阶优化,demo 受篇幅限制未做。

"数据流"完整回顾@State keyword(输入)→ 搜索按钮 onClick 判断 → Toast 反馈。数据流单向(输入 → 状态 → UI),符合 ArkUI 的声明式范式。未来接后端时,只需在 onClick 里调用 searchVenues(keyword) API,把返回结果赋值给 @State searchResults 即可。

十二、首页排版的"信息密度"控制

App 14 首页是 4 个 Tab 中信息密度最高的——HeroCard(活动+统计+搜索)、快捷入口、趋势图、5 个场地、3 条排行,一屏根本放不下,需要滚动 2-3 屏。信息密度的控制靠 3 个手段

  1. space: 14 统一间距:所有区块之间 14vp,区块内卡片 10-12vp——"区块间距 > 卡片间距"形成视觉层次,用户能分清"这里是新板块"
  2. 卡片化:每个区块都包在白卡片里(C.card + C.stroke 边框),5 个场地各自一张卡、排行 3 条各一张卡——"一个卡片一个语义单元"
  3. SectionTitle 分隔:大区块(场地列表/热门排行)前用标题分隔,用户滚动时知道"接下来是什么"

这种"内容流"的信息密度设计与新闻类 App(今日头条、微博)类似——靠间距 + 卡片 + 标题三级分隔,让大量内容不显拥挤。App 13 首页信息密度低(3 条推荐),App 14 高(5 场地+3 排行),密度应与产品定位匹配(音乐歌单"精选少量",运动预约"场地很多")。

Logo

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

更多推荐