HarmonyOS ArkTS实战:运动预约我的页 —— 签到按钮与成就墙的组合技
HarmonyOS ArkTS实战:运动预约我的页 —— 签到按钮与成就墙的组合技
App 14「运动场地预约」我的 Tab(ProfileTab),是个人中心页。整页用渐变用户卡片(张运动 + Lv.5 + 1280/2000 经验条 + 可交互签到按钮)+ 4 栏统计 + 3 列成就墙(6 徽章 4 解锁)+ 本周运动时长条形图 + 6 项菜单(带数字/NEW 角标)+ AboutInfo 组成。本篇基于
14-sports-booking/entry/src/main/ets/pages/ProfileTab.ets(约 313 行)逐段拆解,附 4 张实机截图。
一、整体结构:6 个 @Builder 的绿色个人中心
ProfileTab 与 App 13 我的页结构同源,但有几处关键差异(签到交互、3 列成就墙、菜单角标数字):
build() {
Column() {
Scroll() {
Column({ space: 14 }) {
this.UserCard()
this.StatRow()
this.AchievementWall()
this.WeeklyChart()
this.MenuList()
this.AboutInfo()
}
.width('100%')
.padding({ left: D.pad, right: D.pad, bottom: D.pad + this.safeBottom + 20 })
}
.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
}
.width('100%').height('100%').backgroundColor(C.bg)
}
6 个块顺序:UserCard(身份)→ StatRow(数据)→ AchievementWall(成就)→ WeeklyChart(行为图)→ MenuList(资产)→ AboutInfo(关于)。
与 App 13 我的页对比:
| 维度 | App 13 我的 | App 14 我的 |
|---|---|---|
| 主题 | 紫 #8B5CF6 | 绿 #16A34A |
| 用户卡 | 小明同学 Lv.4 | 张运动 Lv.5 |
| 经验条 | 74%(手动硬编码) | 1280/2000 + Progress 64% |
| 签到 | 静态(只弹 Toast) | 可交互(checked 状态翻转) |
| 成就墙 | 横向 Row 一行 6 个 | Grid 3 列 2 行 |
| 菜单 | 5 项 | 6 项(带数字角标) |
App 14 的签到是"真交互"(App 13 的签到只弹 Toast 不改变状态)——这是本系列个人中心的又一次进化。


二、UserCard:渐变用户卡 + 经验条 + 可交互签到
UserCard 是个人中心的"门面",比 App 13 多了签到按钮的状态翻转:
@Builder
UserCard() {
Column({ space: 12 }) {
Row({ space: 14 }) {
Row() {
Text('🏃').fontSize(32)
}
.width(60).height(60)
.backgroundColor('#FFFFFF').opacity(0.3).borderRadius(30)
.justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Row({ space: 8 }) {
Text('张运动').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Row() {
Text('Lv.5').fontSize(10).fontColor('#FFFFFF')
}
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#FFFFFF').opacity(0.25).borderRadius(8)
}
Text('运动达人 · 已坚持 28 天').fontSize(12).fontColor('#FFFFFF').opacity(0.85)
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('编辑').fontSize(12).fontColor('#FFFFFF').opacity(0.85)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.border({ width: 1, color: '#FFFFFF' }).borderRadius(D.rSm)
.onClick(() => { promptAction.showToast({ message: '编辑资料' }); })
}
.width('100%')
Column({ space: 4 }) {
Row() {
Text('经验值').fontSize(11).fontColor('#FFFFFF').opacity(0.85)
Blank()
Text('1280 / 2000').fontSize(11).fontColor('#FFFFFF').opacity(0.85)
}
.width('100%')
Progress({ value: 64, total: 100 })
.color('#FFFFFF').backgroundColor('#33FFFFFF')
.height(6).borderRadius(3).width('100%')
}
.width('100%')
Row() {
Text(this.checked ? '✓ 今日已签到' : '每日签到 +10 积分')
.fontSize(13).fontColor('#FFFFFF')
.layoutWeight(1)
Text(this.checked ? '已签到' : '签到')
.fontSize(13).fontColor(C.primary)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.backgroundColor('#FFFFFF').borderRadius(D.rSm)
.onClick(() => {
if (!this.checked) {
this.checked = true;
promptAction.showToast({ message: '签到成功 +10 积分' });
}
})
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
.backgroundColor('#33FFFFFF').borderRadius(D.rSm)
}
.width('100%')
.padding(16)
.borderRadius(D.rLg)
.linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
}
2.1 头像 + 姓名 + Lv.5 + 编辑
- 头像:60×60 正圆(
borderRadius(30))+#FFFFFF30% 透明 + 🏃 图标 - 姓名:"张运动" 18sp 加粗白字 + "Lv.5" 白色胶囊标签(
#FFFFFF25% 透明) - Slogan:"运动达人 · 已坚持 28 天"(12sp 白字 85% 透明)
- 编辑:右侧"编辑"文字按钮,白描边(
border({ width: 1, color: '#FFFFFF' }))——暗示"个人资料可编辑"
Lv.5 与 App 13 的 Lv.4 呼应:App 14 用户等级更高(5 vs 4),经验更多(1280/2000 vs 74%)——不同 App 的用户数据不同,体现"每个 demo 是独立用户"。
2.2 经验条:数字 + Progress 双通道
Column({ space: 4 }) {
Row() {
Text('经验值').fontSize(11).fontColor('#FFFFFF').opacity(0.85)
Blank()
Text('1280 / 2000').fontSize(11).fontColor('#FFFFFF').opacity(0.85)
}
.width('100%')
Progress({ value: 64, total: 100 })
.color('#FFFFFF').backgroundColor('#33FFFFFF')
.height(6).borderRadius(3).width('100%')
}
Progress({ value: 64, total: 100 }):1280/2000 = 64%,进度条显示 64%。数字(1280/2000)+ 进度条(64%)双通道——数字告诉精确值,进度条告诉大致比例。
白色进度条(.color('#FFFFFF') 前景白 + .backgroundColor('#33FFFFFF') 背景 20% 白)——在绿色渐变上,白色进度条比绿色更醒目(与背景区分)。
2.3 签到按钮:真交互(本页核心亮点)
Row() {
Text(this.checked ? '✓ 今日已签到' : '每日签到 +10 积分')
.fontSize(13).fontColor('#FFFFFF')
.layoutWeight(1)
Text(this.checked ? '已签到' : '签到')
.fontSize(13).fontColor(C.primary)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.backgroundColor('#FFFFFF').borderRadius(D.rSm)
.onClick(() => {
if (!this.checked) {
this.checked = true;
promptAction.showToast({ message: '签到成功 +10 积分' });
}
})
}
签到按钮是真状态翻转:
.onClick(() => {
if (!this.checked) {
this.checked = true;
promptAction.showToast({ message: '签到成功 +10 积分' });
}
})
- 未签到(
checked = false)→ 左侧文字"每日签到 +10 积分" + 右侧白色"签到"按钮 - 点击签到 →
this.checked = true→ 左侧变"✓ 今日已签到" + 右侧变"已签到" + Toast "签到成功 +10 积分" - 再点不触发(
if (!this.checked)拦截)——防止重复签到
这是本系列第一个"点击后状态永久改变"的交互(App 13 的签到只弹 Toast 不改变状态)。@State checked: boolean = false 驱动两处文字同步变化(左侧提示 + 右侧按钮文案),一处状态、两处渲染。
签到是"每日留存"机制:+10 积分激励每天回来签到,配合成就墙(长期目标)形成"每日 + 长期"的双层激励。
三、StatRow:4 栏统计 + 悬浮效果
StatRow 是 4 个统计数字(预约 14 / 时长 28h / 场地 5 / 卡路里 2.5k):
@Builder
StatRow() {
Row() {
ForEach(this.stats, (s: StatItem) => {
Column({ space: 4 }) {
Text(s.value).fontSize(20).fontWeight(FontWeight.Bold).fontColor(C.text)
Text(s.label).fontSize(11).fontColor(C.textDim)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (s: StatItem) => s.label)
}
.width('100%')
.margin({ top: -28 })
.padding({ top: 14, bottom: 14 })
.backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
}
**margin({ top: -28 }) 负边距让统计卡"上浮"**到 UserCard 底部,形成卡片叠加效果(与 App 13 一致)。4 栏同色(C.text 深色)——与预约页统计一样,"颜色留给状态"。
统计口径与首页 HeroCard 一致:预约 14 = 首页"本月预约 14"、时长 28h = 首页"运动时长 28h"、卡路里 2.5k = 首页"消耗卡路里 2.5k"——跨页数据一致是"单一数据源"的体现(虽然 demo 是手写一致的)。
四、AchievementWall:3 列 Grid 成就墙
AchievementWall 是 6 个徽章的 3 列 Grid(4 解锁 2 未解锁):
@Builder
AchievementWall() {
Column({ space: 12 }) {
Row() {
Text('成就墙').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
Blank()
Text('4/6').fontSize(12).fontColor(C.textDim)
}
.width('100%')
Grid() {
ForEach(this.badges, (b: Badge) => {
GridItem() {
Column({ space: 6 }) {
Row() {
Text(b.emoji).fontSize(24)
}
.width(48).height(48)
.backgroundColor(b.unlocked ? C.primarySoft : C.cardSoft)
.borderRadius(24)
.justifyContent(FlexAlign.Center)
.opacity(b.unlocked ? 1.0 : 0.4)
Text(b.name).fontSize(10).fontColor(b.unlocked ? C.text : C.textDim)
}
.alignItems(HorizontalAlign.Center)
}
}, (b: Badge) => b.id.toString())
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.width('100%')
}
.width('100%')
.padding(14)
.backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
}
与 App 13 的差异:Row 一行 6 个 → Grid 3 列 2 行:
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
App 13 用 Row 一行排 6 个(每个 1/6 宽,很挤),App 14 用 Grid 3 列(每个 1/3 宽,徽章更大更清晰)——Grid 更适合"徽章多、要看清图标"的场景。
6 个徽章(运动主题):
- 🏀 篮球达人(解锁)⚽ 足球之星(解锁)🏸 羽毛球手(解锁)🎯 百发百中(解锁)
- 🏆 运动冠军(未解锁)🥇 金牌会员(未解锁)
解锁/未解锁三件套:背景(C.primarySoft 浅绿 vs C.cardSoft 浅灰)、透明度(opacity 1.0 vs 0.4)、文字(C.text vs C.textDim)。
4/6 是硬编码(App 13 用 .filter(a => a.unlocked).length 动态算),App 14 直接写死"4/6"——如果 badge 数据变了(如解锁 5 个),这里的 4/6 不会自动更新。真实项目应像 App 13 那样动态计算。这是 App 14 的一个"回退",值得对比学习。
五、WeeklyChart:本周运动时长
WeeklyChart 与首页 WeeklyChart 同构(7 天条形图 + 总计 7.5h):
@Builder
WeeklyChart() {
Column({ space: 12 }) {
Row() {
Text('本周运动时长').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
Blank()
Text('总计 7.5h').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 })
}
与首页 WeeklyChart 完全同构(barData 相同:40/65/50/80/90/70/35),只是标题"本周运动趋势"→"本周运动时长"、汇总"平均 61%"→"总计 7.5h"。
"总计 7.5h"的计算:40+65+50+80+90+70+35 = 430(百分比),如果按"每周 7.5h 运动"反推,430% ≈ 7.5h/周 → 每天约 61 分钟——数据设计有内在一致性(平均 61% ≈ 每天 61 分钟 ≈ 总计 7.5h)。
为什么我的页和首页用同一个图?——因为"本周运动数据"是全局用户数据,首页(趋势视角)和我的页(个人视角)显示同一份数据的不同标题。跨页数据一致是"单一数据源"原则的体现。
六、MenuList:6 项菜单 + 数字/NEW 角标
MenuList 是 6 个菜单项(预约日历/我的好友/我的收藏/积分商城/消息通知/意见反馈),角标有"数字"和"NEW"两种:
@Builder
MenuList() {
Column({ space: 0 }) {
ForEach(this.menus, (m: MenuItem) => {
Row({ space: 12 }) {
Row() {
Text(m.emoji).fontSize(20)
}
.width(36).height(36)
.backgroundColor(C.primarySoft).borderRadius(10)
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text(m.label).fontSize(14).fontColor(C.text)
Text(m.desc).fontSize(11).fontColor(C.textDim)
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (m.badge.length > 0) {
if (m.badge === 'NEW') {
Text(m.badge).fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(C.danger).borderRadius(4)
} else {
Text(m.badge).fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(C.danger).borderRadius(9)
.constraintSize({ minWidth: 18 }).textAlign(TextAlign.Center)
}
}
Text('>').fontSize(12).fontColor(C.textDim)
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 12 })
.backgroundColor(C.card)
.onClick(() => { promptAction.showToast({ message: m.label }); })
if (m.id < this.menus.length) {
Divider().color(C.stroke).margin({ left: 50 })
}
}, (m: MenuItem) => m.id.toString())
}
.width('100%')
.borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
.clip(true)
}
6.1 两种角标:NEW 胶囊 + 数字圆形
if (m.badge === 'NEW') {
Text(m.badge).fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(C.danger).borderRadius(4)
} else {
Text(m.badge).fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor(C.danger).borderRadius(9)
.constraintSize({ minWidth: 18 }).textAlign(TextAlign.Center)
}
两种角标形态:
- NEW →
borderRadius(4)小圆角胶囊(C.danger红底白字) - 数字("2")→
borderRadius(9)大圆角接近正圆 +constraintSize({ minWidth: 18 })最小宽 18vp(让"2"也撑成圆形)+textAlign(TextAlign.Center)居中
"数字徽标"(badge)是移动端标配——iOS 的红色数字角标、微信的未读消息数都是这个设计。constraintSize({ minWidth: 18 }) 让 1 位数也是正圆、2 位数(如 "23")自动变椭圆——"最小宽 + 居中"是数字徽标的正确做法。
6 项菜单的角标编排:
- 📅 预约日历 — badge "2"(数字:2 个待确认预约,呼应预约页统计)
- 👥 我的好友 — 无角标
- ⭐ 我的收藏 — 无角标
- 🎁 积分商城 — badge "NEW"(新功能引导)
- ⚙️ 消息通知 — 无角标
- 💬 意见反馈 — 无角标
"2"与预约页的"待确认 2"呼应(跨页数据一致),"NEW"标记新功能——数字给"有内容要处理"的信号,NEW 给"有新功能"的信号,两种角标语义不同。
6.2 分隔线
if (m.id < this.menus.length) {
Divider().color(C.stroke).margin({ left: 50 })
}
m.id < this.menus.length 判断最后一条不加分隔线——id 从 1 开始,menus.length=6,所以 id=1~5 加线、id=6 不加。注意这个判断比 idx < length - 1 更脆弱——如果菜单数据 id 不是从 1 开始连续(如删除一条 id=2),判断会失效。用 ForEach 的 idx 判断更可靠(idx < this.menus.length - 1)。
margin({ left: 50 }) 让分隔线不贯穿全宽(从图标右侧开始),iOS 风格。
七、AboutInfo:版本号
AboutInfo 是底部版本信息(比 App 13 多一行版权):
@Builder
AboutInfo() {
Column({ space: 6 }) {
Text('运动场地预约 v1.0.0').fontSize(12).fontColor(C.textDim)
Text('基于 HarmonyOS 6.0+ ArkTS 开发').fontSize(11).fontColor(C.textDim)
Text('© 2024 智慧校园').fontSize(10).fontColor(C.textDim).opacity(0.6)
}
.width('100%')
.padding({ top: 16, bottom: 16 })
.alignItems(HorizontalAlign.Center)
}
3 行:版本号 → 技术栈 → 版权。opacity(0.6) 让版权行最淡(层级最低)。"© 2024 智慧校园"是品牌露出——虽然 demo 无真实版权,但演示了"底部品牌区"的规范。

八、@State 的克制与签到状态
ProfileTab 只有 1 个 @State:checked: boolean(签到状态)。这是 4 个 Tab 中最少的 @State 页面。
为什么这么少?因为本页几乎纯展示(用户数据/成就/图表都是 private 不可变数据),唯一"会变"的是签到状态。**"展示页 @State 少,交互页 @State 多"**是健康状态管理的铁律:
| 页面 | @State 数量 | 类型 |
|---|---|---|
| 首页 HomeTab | 1(keyword) | 展示+搜索 |
| 场地 Func1Tab | 5(venue/selected/name/phone/note) | 交互表单 |
| 预约 Func2Tab | 1(activeTab) | 筛选 |
| 我的 ProfileTab | 1(checked) | 展示+签到 |
4 个 Tab 的 @State 数量分布正好反映"页面复杂度"——表单页最多(5 个),展示页最少(1 个)。

九、签到 → 经验 → 成就的游戏化闭环
App 14 我的页把"游戏化"做完整了:
- 签到 +10 积分(每日激励)→ 改变
checked状态 - 经验条 1280/2000(长期目标)→ 进度 64%,还差 720 经验升级
- 成就墙 4/6(身份认同)→ 4 个已解锁,2 个待挑战
- Lv.5 徽章(等级象征)→ 最高等级身份
"每日签到(行为)→ 经验增长(进度)→ 成就解锁(身份)→ 等级提升(地位)" 是完整游戏化链路。与 App 13 相比:App 13 的签到"只弹 Toast 不改状态",App 14 的签到"真改变状态"——但 App 14 的经验条没有跟着签到 +10 积分变化(1280/2000 是硬编码),真实项目应 exp += 10 并联动进度条。"状态联动"是游戏化的下一步。
十、总结:本页模板
Scroll(
UserCard(渐变 + 头像/姓名/Lv + 经验条 + 签到交互)+
StatRow(4 栏统计 + 负边距悬浮)+
AchievementWall(3 列 Grid 成就墙)+
WeeklyChart(7 天条形图)+
MenuList(6 项菜单 + 数字/NEW 角标)+
AboutInfo(版本 + 技术栈 + 版权)
)
核心可复用点:
- 可交互签到(
@State checked+if (!this.checked)防重复) - 3 列 Grid 成就墙(比 Row 一行更清晰)
- 数字徽标(
constraintSize({ minWidth: 18 })正圆) - 负边距悬浮卡(
margin({ top: -28 }))
个人中心页模板已在本系列出现 2 次(App 13/14)。
十一、签到状态的持久化问题
@State checked: boolean = false 的签到状态目前只存在内存里——应用重启后 checked 会重置为 false,用户又能"再签一次"。
真实项目的签到必须持久化,有三个层级:
- 本地持久化:
@ohos.data.preferences(首选项)存checked+lastCheckInDate。启动时读:如果lastCheckInDate是今天,checked = true(显示"今日已签到");如果昨天,重置为 false 并允许签到(新的一天) - 服务端持久化:签到调
POST /api/checkin,服务端记录"今天这个用户签过了",返回积分。即使卸载重装,服务端也知道"今天已签" - 本地 + 服务端双写:最稳的方案——本地首选项保证离线可用(弱网时先本地标记,联网后同步服务端)
@ohos.data.preferences 是 ArkTS 的本地存储 API(类似 Web 的 localStorage),示例:
import { preferences } from '@kit.ArkData';
// 读
const prefs = await preferences.getPreferences(context, 'user_pref');
const checked = await prefs.get('checked', false);
// 写
await prefs.put('checked', true);
await prefs.flush();
"每日签到"的核心是"日"的判定:lastCheckInDate === today 则已签、否则可签。用日期字符串(如 '2026-08-18')比时间戳更可靠(时区问题少)。"状态 + 日期"双字段才能支撑"每天一次"的业务逻辑。
当前 demo 的"内存状态"是原型阶段的合理简化,但把"持久化"写进这篇解析,是为了让读者知道**"从 demo 到产品"的差距在哪**——状态管理只是第一步,持久化才是真实业务。

十二、用户数据的"跨页一致性"治理
我的页的统计(预约 14/时长 28h/卡路里 2.5k)与首页 HeroCard(本月预约 14/运动时长 28h/消耗卡路里 2.5k)完全一致——同一份用户数据在两个页面重复出现。
demo 的手写一致有两个隐患:
- 改一处漏一处:如果产品经理说"把预约数改成 15",首页改了我这页忘了改,两个页面数字打架
- 数据源不唯一:HomeTab 的
stats和 ProfileTab 的stats是两份独立硬编码,没有"共享一份数据"的机制
真实项目的解决方案:
// 全局用户数据源(AppStorage 或单例)
AppStorage.setOrCreate('userStats', {
appointments: 14, duration: '28h', calories: '2.5k'
});
// 任何页面读取
@StorageProp('userStats') userStats: UserStats;
把用户数据放进全局存储(AppStorage)或抽成 service 单例,各页只读不写——"单一数据源"原则的落地。这样改数据只需改一处,所有页面自动同步。
更进一步,如果用户数据来自后端,应该在 EntryAbility 启动时拉取一次存进 AppStorage,各页 @StorageProp 订阅——**"拉取一次,全局共享,各页响应式更新"**是鸿蒙应用数据架构的推荐模式。
"跨页一致性"是数据架构的试金石:demo 可以靠"手写一致"糊弄过去,真实项目必须靠"数据源唯一"保证。读者从这篇解析里应该学到:把"会跨页共享的数据"提升到全局存储,而不是各页各存一份。
十三、签到 +10 积分与经验值的联动
当前签到点击只改 checked,经验值 1280/2000 没有跟着 +10。真实项目应联动:
// 签到:积分 + 经验 双增
onClick(() => {
if (!this.checked) {
this.checked = true;
this.exp = Math.min(this.exp + 10, 2000); // 经验 +10,封顶 2000
promptAction.showToast({ message: '签到成功 +10 积分 +10 经验' });
}
})
Math.min(exp + 10, 2000) 防止经验溢出(签到 72 次后满级)。经验条 Progress value 也要改为 this.exp / 2000 * 100 动态计算——一处签到、多处联动(checked + exp + 进度条 + 文案)。
更进一步,经验升级应联动成就墙——exp >= 1600 时解锁"运动冠军"徽章(badges[4].unlocked = true)。"签到 → 经验 → 成就"的完整联动才是游戏化的终点。
"状态联动"是响应式 UI 的进阶:@State 一变,所有依赖它的 UI 同步更新。App 14 的签到只联动 1 处(checked → 文案),真实项目应联动 N 处(checked + exp + progress + badge)。从"1 处联动"到"N 处联动"是 demo 与产品的差距,也是本文想传递的"游戏化进阶"思路。
到此,App 14「运动场地预约」我的页解析完毕。
更多推荐


所有评论(0)