HarmonyOS 校园应用系列之HarmonyOS ArkTS实战:水电缴费我的页 —— 负边距上浮统计与成就墙锁定态组合技

应用背景:10 水电缴费个人中心页(ProfileTab.ets,289 行)展示用户画像、缴费统计、成就体系与活跃度。本文聚焦 UserInfo 渐变资料卡负 Margin 统计卡成就墙锁定态菜单列表 四大模块。

应用效果图

一、页面整体结构

// ProfileTab.ets — 页面根结构
Column() {
  Scroll() {
    Column({ space: 0 }) {
      this.UserInfo()
      this.StatGrid()
      this.AchievementWall()
      this.WeekChart()
      this.MenuList()
      this.AboutInfo()
    }
    .width('100%')
    .padding({ bottom: this.safeBottom + 20 })
  }
  .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
}
.width('100%').height('100%').backgroundColor(C.bg)

本页没有独立 Header——渐变 UserInfo 卡直接顶到屏幕最上方充当头部(内部自行处理 safeTop),滚动区 Column({ space: 0 }) 纵向排布 6 个模块,各模块自带左右 D.pad margin。

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

配图

二、UserInfo 渐变资料卡

2.1 布局与签到交互

// UserInfo — 个人资料卡
@Builder
UserInfo() {
  Column({ space: 14 }) {
    Row({ space: 14 }) {
      Column() {
        Text('👤').fontSize(34)
      }
      .width(64).height(64)
      .backgroundColor('#33FFFFFF').borderRadius(32)
      .justifyContent(FlexAlign.Center)

      Column({ space: 4 }) {
        Row({ space: 8 }) {
          Text('张同学').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Text('Lv.' + this.level).fontSize(11).fontColor('#FFFFFF')
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .backgroundColor('#33FFFFFF').borderRadius(10)
        }
        Text('梅园 6 栋 302 室').fontSize(12).fontColor('#FFFFFF').opacity(0.85)
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)

      Column({ space: 4 }) {
        Text(this.checked ? '✓ 已签到' : '签到').fontSize(12).fontColor('#FFFFFF')
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .backgroundColor(this.checked ? '#33FFFFFF' : '#FFFFFF')
          .fontColor(this.checked ? '#FFFFFF' : C.primary)
          .borderRadius(16)
          .onClick(() => {
            this.checked = !this.checked;
            if (this.checked) { this.exp = Math.min(this.exp + 10, 100); }
            promptAction.showToast({ message: this.checked ? '签到成功 +10 经验' : '已取消' });
          })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width('100%')

    Column({ space: 6 }) {
      Row() {
        Text('经验值').fontSize(11).fontColor('#FFFFFF').opacity(0.8)
        Blank()
        Text(this.exp + ' / 100').fontSize(11).fontColor('#FFFFFF').opacity(0.8)
      }
      .width('100%')
      Progress({ value: this.exp, total: 100 }).color('#FFFFFF').backgroundColor('#33FFFFFF').width('100%')
    }
    .width('100%')
  }
  .width('100%')
  .padding({ left: D.pad, right: D.pad, top: this.safeTop + 30, bottom: 24 })
  .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
}

渐变卡无 Stack 分层,纯 Column + linearGradient(135° 主色→亮青)实现。三个细节值得注意:

  • 头像:64vp 圆形(borderRadius(32)),底色是 20% 半透明白 #33FFFFFF 而非纯白,与渐变底融合更自然
  • 等级徽标'Lv.' + this.level(Lv.5),白字 + 半透明白底胶囊
  • 签到是 Text 而非 Button:三元表达式同时切换文案(签到 ↔ ✓ 已签到)、底色(白 ↔ 半透明白)、字色(主色 ↔ 白);点击翻转 checked,签到时 Math.min(this.exp + 10, 100) 经验 +10 封顶 100,toast 反馈"签到成功 +10 经验"

2.2 经验进度条

// 经验条(UserInfo 内部)
Column({ space: 6 }) {
  Row() {
    Text('经验值').fontSize(11).fontColor('#FFFFFF').opacity(0.8)
    Blank()
    Text(this.exp + ' / 100').fontSize(11).fontColor('#FFFFFF').opacity(0.8)
  }
  .width('100%')
  Progress({ value: this.exp, total: 100 }).color('#FFFFFF').backgroundColor('#33FFFFFF').width('100%')
}
.width('100%')

系统 Progress 组件:白色填充 + 20% 透明白轨道,exp 初始 68,签到 +10 且封顶 100。

我的页顶部

三、StatGrid 四维统计

// StatGrid — 统计卡
@Builder
StatGrid() {
  Row() {
    ForEach(this.stats, (s: StatItem) => {
      Column({ space: 4 }) {
        Text(s.value).fontSize(18).fontWeight(FontWeight.Bold).fontColor(C.text)
        Text(s.label).fontSize(11).fontColor(C.textDim)
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Center)
    }, (s: StatItem) => s.label)
  }
  .width('90%')
  .padding({ top: 16, bottom: 16 })
  .backgroundColor(C.card)
  .borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke })
  .margin({ top: -28, left: D.pad, right: D.pad })
  .shadow({ radius: 12, color: '#10000000', offsetX: 0, offsetY: 4 })
}

负 Margin 上浮.margin({ top: -28 }) 让白卡向上叠进渐变卡底部,配合 shadow 投影强化悬浮层次。四维统计:缴费次数(24) / 累计金额(¥478) / 优惠券(12) / 绑定房间(3),layoutWeight(1) 四等分、无分隔线。

// stats 数据定义
private stats: StatItem[] = [
  { value: '24', label: '缴费次数' },
  { value: '¥478', label: '累计金额' },
  { value: '12', label: '优惠券' },
  { value: '3', label: '绑定房间' }
]

四、AchievementWall 成就墙

4.1 数据模型

// 成就数据
private badges: BadgeItem[] = [
  { emoji: '🏆', name: '缴费达人', locked: false },
  { emoji: '🌱', name: '节能先锋', locked: false },
  { emoji: '⚡', name: '闪电充值', locked: false },
  { emoji: '💧', name: '节水卫士', locked: false },
  { emoji: '🔥', name: '连续签到', locked: true },
  { emoji: '👑', name: '年度冠军', locked: true }
]

BadgeItem 仅三字段(emoji/name/locked),6 个成就按 30% 宽度三列两行排布:前 4 个已解锁,后 2 个锁定。

4.2 锁定态差异化渲染

// AchievementWall — 成就墙
@Builder
AchievementWall() {
  Column({ space: 12 }) {
    Text('成就墙').fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text).width('100%')
    Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
      ForEach(this.badges, (b: BadgeItem) => {
        Column({ space: 6 }) {
          Row() {
            Text(b.emoji).fontSize(28).opacity(b.locked ? 0.4 : 1)
          }
          .width(52).height(52)
          .backgroundColor(b.locked ? C.cardSoft : C.primarySoft)
          .borderRadius(26)
          .justifyContent(FlexAlign.Center)

          Text(b.name).fontSize(10).fontColor(b.locked ? C.textDim : C.textSub)
          Text(b.locked ? '未解锁' : '已获得').fontSize(9).fontColor(b.locked ? C.textDim : C.primary)
        }
        .width('30%')
        .padding({ top: 10, bottom: 10 })
        .alignItems(HorizontalAlign.Center)
        .onClick(() => {
          promptAction.showToast({ message: b.locked ? b.name + '(未解锁)' : b.name + '(已获得)' });
        })
      }, (b: BadgeItem) => b.name)
    }
    .width('100%')
  }
  .width('100%')
  .padding(14)
  .backgroundColor(C.card)
  .borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke })
  .margin({ top: 14, left: D.pad, right: D.pad })
}

锁定态双重区分(无虚线边框):图标 opacity(0.4) 半透明 + 圆形底色由浅青 C.primarySoft 换浅灰 C.cardSoft;第三行状态文字"未解锁"灰 / "已获得"主色。点击 toast 带解锁状态后缀。

我的页中部

五、WeekChart 缴费活跃度

// WeekChart — 活跃度图表
@Builder
WeekChart() {
  Column({ space: 12 }) {
    Row() {
      Text('本周缴费活跃度').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
      Blank()
      Text('连续 5 天').fontSize(12).fontColor(C.primary)
    }
    .width('100%')

    Row({ space: 6 }) {
      ForEach(this.weekBars, (w: WeekBar) => {
        Column({ space: 6 }) {
          Column() {
            Column()
              .width('100%')
              .height(Math.floor(w.value / this.maxBar * 70))
              .backgroundColor(C.primary)
              .borderRadius(4)
          }
          .width('100%').height(70)

          Text(w.day).fontSize(11).fontColor(C.textDim)
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Center)
      }, (w: WeekBar) => w.day)
    }
    .width('100%')
  }
  .width('100%')
  .padding(14)
  .backgroundColor(C.card)
  .borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke })
  .margin({ top: 14, left: D.pad, right: D.pad })
}

与首页 WeekChart 同构但轨道高 70(首页 80):Math.floor(w.value / this.maxBar * 70) 归一化柱高,maxBar 同为 90,数据同为 45/72/38/90/55/68/42。柱体 width('100%')layoutWeight(1) 七等分自适应,标题行右侧"连续 5 天"主色激励文案。

我的页下部

六、MenuList 菜单列表

// MenuList — 菜单列表
@Builder
MenuList() {
  Column({ space: 0 }) {
    ForEach(this.menus, (m: MenuItem) => {
      Row({ space: 12 }) {
        Row() {
          Text(m.icon).fontSize(20)
        }
        .width(36).height(36)
        .backgroundColor(C.cardSoft).borderRadius(D.rSm)
        .justifyContent(FlexAlign.Center)

        Text(m.label).fontSize(14).fontColor(C.text).layoutWeight(1)

        if (m.badge.length > 0) {
          Text(m.badge).fontSize(11).fontColor(C.danger)
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .backgroundColor(C.primarySoft).borderRadius(8)
        }
        Text('>').fontSize(14).fontColor(C.textDim)
      }
      .width('100%')
      .padding({ left: 14, right: 14, top: 12, bottom: 12 })
      .onClick(() => { promptAction.showToast({ message: m.action }); })

      if (m.label !== '帮助反馈') {
        Divider().color(C.stroke).strokeWidth(0.5).margin({ left: 62 })
      }
    }, (m: MenuItem) => m.label)
  }
  .width('100%')
  .backgroundColor(C.card)
  .borderRadius(D.rLg)
  .border({ width: 1, color: C.stroke })
  .margin({ top: 14, left: D.pad, right: D.pad })
}

菜单数据与交互:

// 菜单数据
private menus: MenuItem[] = [
  { icon: '💳', label: '我的钱包', badge: '¥86', action: '钱包余额' },
  { icon: '🎟️', label: '优惠券', badge: '3张', action: '查看优惠券' },
  { icon: '🏠', label: '房屋管理', badge: '', action: '管理房屋' },
  { icon: '🔔', label: '消息通知', badge: '2', action: '消息通知' },
  { icon: '🛡️', label: '安全中心', badge: '', action: '安全设置' },
  { icon: '❓', label: '帮助反馈', badge: '', action: '帮助与反馈' }
]
  • actionstring(toast 文案)而非回调函数,点击统一 promptAction.showToast({ message: m.action })
  • 徽标条件渲染 if (m.badge.length > 0)红字浅青底C.danger 字 + C.primarySoft 底)
  • 分割线 Divider 左缩进 62vp 对齐文字,末项"帮助反馈"后不渲染

七、AboutInfo 版本信息

// AboutInfo — 版本信息
@Builder
AboutInfo() {
  Column({ space: 6 }) {
    Text('水电缴费 v1.0.0').fontSize(12).fontColor(C.textDim)
    Text('Powered by HarmonyOS 6.0+ · ArkTS').fontSize(11).fontColor(C.textDim)
    Text('© 2025 校园智慧生活').fontSize(10).fontColor(C.textDim)
  }
  .width('100%')
  .padding({ top: 20, bottom: 10 })
  .alignItems(HorizontalAlign.Center)
}

页脚三行居中灰字:版本号 v1.0.0 / 技术栈标识 Powered by HarmonyOS 6.0+ / 版权信息,字号 12→11→10 递减。

我的页底部

八、本章小结

组件核心技术学习价值
UserInfolinearGradient + Text 签到切换个人中心标准头部
StatGridmargin(-28) 上浮 + shadow卡片层次感设计
AchievementWallopacity + 底色锁定态游戏化成就系统
WeekChartMath.floor 归一化柱高数据可视化一致性
MenuListstring action + 条件徽标功能导航通用方案
AboutInfo三行递减页脚版本信息规范

UserInfo 渐变资料卡 最具学习价值——签到 Text 三元切换文案/底色/字色,Math.min(exp + 10, 100) 一行完成经验值封顶,配合负 Margin 统计卡上浮构建层次感。

Logo

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

更多推荐