鸿蒙 ArkUI 实战:运动预约首页的绿色 HeroCard 与场地卡片
鸿蒙 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 个块按"活动 → 入口 → 数据 → 列表 → 排行"五段叙事:
- HeroCard — 校运动会报名 + 3 栏统计 + 搜索框(品牌区)
- QuickActionRow — 4 个运动入口(导航区)
- WeeklyChart — 本周运动趋势(数据区)
- SectionTitle('运动场地') + VenueList — 5 个场地卡片(主内容区)
- SectionTitle('热门排行') + RankList — 热门排行榜(次级内容区)
关键差异:SectionTitle 是参数化 @Builder(SectionTitle(title: string, action: string)),把"区块标题 + 右侧操作链接"抽成可复用组件。两个区块共用,传入不同标题(运动场地/热门排行)和不同操作文字(查看全部/更多)。这是 @Builder 参数化的典型价值——同一块 UI,不同数据,两处复用。
padding 的顶部 top: 14 和底部 bottom: D.pad + this.safeBottom + 20 给内容流留出边界呼吸感。


二、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 + ' >' 拼接箭头暗示可点击。

七、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 卡片头部三区布局
第一行分三区:
- 左侧 56×56 浅绿 emoji 图标(🏀/🏸/⚽/🎾/🏓)
- 中间"名称 + 星级 + 开放时间 + 价格"两行
- 右侧"预约"按钮
星级渲染用 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% 满 + "已约满"),数据不是孤立的。

八、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 天趋势,数据编排非常讲究"互相印证":
- 足球场已约满(12/12,100%,红色)→ 排行第 3(72 次预约)——"约满的场地预约次数反而少",因为"约满=不再能约",次数是历史累计
- 篮球场最热(5 星 + 8/10 + 排行第 1)——"评分高 + 预约多 + 排名高"三合一,用户一看就懂"篮球场是首选"
- 周五趋势最高 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 个手段:
space: 14统一间距:所有区块之间 14vp,区块内卡片 10-12vp——"区块间距 > 卡片间距"形成视觉层次,用户能分清"这里是新板块"- 卡片化:每个区块都包在白卡片里(
C.card+C.stroke边框),5 个场地各自一张卡、排行 3 条各一张卡——"一个卡片一个语义单元" - SectionTitle 分隔:大区块(场地列表/热门排行)前用标题分隔,用户滚动时知道"接下来是什么"
这种"内容流"的信息密度设计与新闻类 App(今日头条、微博)类似——靠间距 + 卡片 + 标题三级分隔,让大量内容不显拥挤。App 13 首页信息密度低(3 条推荐),App 14 高(5 场地+3 排行),密度应与产品定位匹配(音乐歌单"精选少量",运动预约"场地很多")。
更多推荐




所有评论(0)