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 不改变状态)——这是本系列个人中心的又一次进化。

运动场地预约我的页首屏 · 绿色渐变用户卡+Lv.5+经验条+签到按钮+4栏统计

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

配图

二、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))+ #FFFFFF 30% 透明 + 🏃 图标
  • 姓名:"张运动" 18sp 加粗白字 + "Lv.5" 白色胶囊标签(#FFFFFF 25% 透明)
  • 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)
}

两种角标形态

  • NEWborderRadius(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 无真实版权,但演示了"底部品牌区"的规范。

运动场地预约我的页底部 · 菜单完整+意见反馈+AboutInfo版本信息+底部Tab栏

八、@State 的克制与签到状态

ProfileTab 只有 1 个 @Statechecked: boolean(签到状态)。这是 4 个 Tab 中最少的 @State 页面。

为什么这么少?因为本页几乎纯展示(用户数据/成就/图表都是 private 不可变数据),唯一"会变"的是签到状态。**"展示页 @State 少,交互页 @State 多"**是健康状态管理的铁律:

页面@State 数量类型
首页 HomeTab1(keyword)展示+搜索
场地 Func1Tab5(venue/selected/name/phone/note)交互表单
预约 Func2Tab1(activeTab)筛选
我的 ProfileTab1(checked)展示+签到

4 个 Tab 的 @State 数量分布正好反映"页面复杂度"——表单页最多(5 个),展示页最少(1 个)。

运动场地预约我的页中段 · 4栏统计+3列成就墙+本周运动时长条形图

九、签到 → 经验 → 成就的游戏化闭环

App 14 我的页把"游戏化"做完整了:

  1. 签到 +10 积分(每日激励)→ 改变 checked 状态
  2. 经验条 1280/2000(长期目标)→ 进度 64%,还差 720 经验升级
  3. 成就墙 4/6(身份认同)→ 4 个已解锁,2 个待挑战
  4. 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(版本 + 技术栈 + 版权)
)

核心可复用点

  1. 可交互签到@State checked + if (!this.checked) 防重复)
  2. 3 列 Grid 成就墙(比 Row 一行更清晰)
  3. 数字徽标constraintSize({ minWidth: 18 }) 正圆)
  4. 负边距悬浮卡margin({ top: -28 })

个人中心页模板已在本系列出现 2 次(App 13/14)。

十一、签到状态的持久化问题

@State checked: boolean = false 的签到状态目前只存在内存里——应用重启后 checked 会重置为 false,用户又能"再签一次"。

真实项目的签到必须持久化,有三个层级:

  1. 本地持久化@ohos.data.preferences(首选项)存 checked + lastCheckInDate。启动时读:如果 lastCheckInDate 是今天,checked = true(显示"今日已签到");如果昨天,重置为 false 并允许签到(新的一天)
  2. 服务端持久化:签到调 POST /api/checkin,服务端记录"今天这个用户签过了",返回积分。即使卸载重装,服务端也知道"今天已签"
  3. 本地 + 服务端双写:最稳的方案——本地首选项保证离线可用(弱网时先本地标记,联网后同步服务端)

@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 到产品"的差距在哪**——状态管理只是第一步,持久化才是真实业务。

运动场地预约我的页下部 · 成就墙完整+6项菜单(预约日历2/NEW角标)

十二、用户数据的"跨页一致性"治理

我的页的统计(预约 14/时长 28h/卡路里 2.5k)与首页 HeroCard(本月预约 14/运动时长 28h/消耗卡路里 2.5k)完全一致——同一份用户数据在两个页面重复出现。

demo 的手写一致有两个隐患

  1. 改一处漏一处:如果产品经理说"把预约数改成 15",首页改了我这页忘了改,两个页面数字打架
  2. 数据源不唯一: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「运动场地预约」我的页解析完毕。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐