本文基于 HarmonyOS ArkTS API 24(API Level 12)完整实现一款「多多健身社交」应用,覆盖健身课程、运动器材、精选私教、运动打卡、健身社区、个人中心六大核心模块,采用活力绿 #00C853 × 能量橙 #FF6D00 双主色 + 底色 #F5FFF0 运动活力风视觉体系,通过六个独立 @Component 子组件 + 一个 @Entry 主入口组件构建六组件多Tab健身社交电商架构,内置分类chips横滑、精选大卡、双列课程网格、品类网格、限时特价横条、教练列表、运动数据环形图、打卡日历、打卡记录列表、健身动态、照片墙、热门话题、月度运动时长绿色系渐变柱状图、成就徽章等差异化布局,并实现报名课程底部抽屉(含时段选择+教练选择)、取消报名居中可移除、编辑健身目标底部抽屉TextInput、删除打卡记录居中可移除四种模态弹窗,完整还原健身社交从课程→器材→私教→打卡→社区→个人的全链路业务闭环。

多多健身社交整体运行效果

一、项目背景与设计理念

健身社交已从单纯的运动打卡演变为涵盖在线课程、运动器材电商、私教预约、数据追踪、社区分享的完整生态。一位健身爱好者从选择适合的课程、购买专业器材、预约私教指导、每日运动打卡记录数据、到社区分享健身成果,形成了一条强数据驱动、强社交属性、强电商转化的业务链路。

「多多健身社交」正是针对这一全链路场景设计的一站式健身社交服务平台,核心设计理念如下:

  • 课程即选择:健身课程的核心是「分类选择+难度匹配」,用户最关心的是「什么类型、什么难度、消耗多少大卡、哪个教练、多少钱」,因此课程Tab以「分类chips横滑(瑜伽/力量/有氧/HIIT/舞蹈/拉伸/搏击/游泳8分类)+精选大卡+双列课程网格」为核心,每门课程显示难度标签(初级绿/中级橙/高级红)、消耗大卡、教练、时长、评分、学习人数,帮助用户快速匹配适合的课程。
  • 器材即装备:健身离不开专业器材,器材Tab以「限时特价横条(倒计时+5折起)+双列器材网格」为核心,每件器材显示品牌、销量、价格、评分,还原运动器材电商体验。
  • 私教即专业:专业指导是健身效果的关键,私教Tab以「教练详情大卡列表」为核心,每位教练显示头像、认证标签(ACE-CPT/RYT-500/NSCA-CSCS等)、专长、简介、评分、学员数、课时费,配合「预约体验+立即预约」双按钮,还原私教预约体验。
  • 打卡即数据:健身打卡的核心是「数据可视化+坚持激励」,打卡Tab以「运动数据环形图(85%目标完成)+本周数据三列(打卡天数/消耗大卡/运动分钟)+连续打卡条+打卡记录列表」为核心,每条记录显示运动类型、日期、时长、消耗大卡、类型标签,完整还原运动数据追踪体验。
  • 社区即分享:健身爱好者有强烈的「成果分享、经验交流」需求,社区Tab以「热门话题chips横滑(减脂打卡/增肌心得/瑜伽日常/跑步日记/私教推荐五色话题)+健身动态列表(含头像+昵称+标签+内容+照片墙三图+点赞评论分享)」为核心,构建健身社区氛围。
  • 个人即成就:健身爱好者有强烈的「坚持成就、运动成就」荣誉感,个人Tab以活力绿渐变头+四列数据悬浮卡(连续打卡/运动小时/消耗大卡/报名课程)+月度运动时长绿色系渐变柱状图(3-8月从浅绿#A5D6A7到深活力绿#00C853,8月30小时最高)+成就徽章列表(连续打卡30天/力量大师/跑步达人/瑜伽修行者已解锁✅,健身王者未解锁🔒opacity0.5半透明)构建个人成就体系。

视觉上采用活力绿 #00C853 × 能量橙 #FF6D00 双主色体系:活力绿代表健康、活力、运动的生命力,能量橙代表能量、热情、燃脂的激情,二者搭配形成强烈的「运动活力风」视觉冲击;底色采用 #F5FFF0 极浅绿白,既保证内容区的可读性,又与绿色主色形成柔和过渡;辅助色涵盖难度三色(初级绿#00C853/中级橙#FF6D00/高级红#E53935)、运动类型八色(瑜伽紫/力量红/有氧蓝/HIIT橙/舞蹈粉/拉伸青/搏击棕/游泳浅蓝)、话题五色(绿/红/紫/蓝/橙),构建完整的健身业务色彩语义体系。

二、整体架构概览:六组件多Tab健身社交电商架构

多多健身社交采用六个独立 @Component 子组件 + 一个 @Entry 主入口组件的六组件多Tab健身社交电商架构,每个Tab对应一个独立的子组件,主入口组件 DuoDuoFitnessApp 通过 @State activeTab 状态变量控制当前显示的Tab,并通过 @Builder bottomTabItem 复用底部Tab栏项。

多多健身社交架构与课程Tab运行效果

2.1 组件清单

组件名 装饰器 对应Tab 核心职责
CourseTab69 @Component 课程📚 搜索头+分类chips横滑+精选大卡+双列课程网格+报名课程弹窗
EquipmentTab @Component 器材🏋️ 限时特价横条+双列器材网格
CoachTab @Component 私教👨‍🏫 教练详情大卡列表+预约体验/立即预约双按钮
CheckInTab @Component 打卡📊 运动数据环形图+连续打卡条+打卡记录列表+编辑目标弹窗+删除记录弹窗
CommunityTab69 @Component 社区💬 热门话题chips横滑+健身动态列表含照片墙
ProfileTab69 @Component 我的👤 渐变头+四列数据悬浮卡+月度运动柱状图+成就徽章
DuoDuoFitnessApp @Entry @Component 主入口 六Tab切换调度+底部Tab栏+全局底色

2.2 主入口组件核心代码

@Entry
@Component
struct DuoDuoFitnessApp {
  @State activeTab: number = 0

  @Builder
  bottomTabItem(icon: string, label: string, tabIdx: number) {
    Column({ space: 3 }) {
      Text(icon)
        .fontSize(20)
        .opacity(this.activeTab === tabIdx ? 1.0 : 0.45)
      Text(label)
        .fontSize(9)
        .fontColor(this.activeTab === tabIdx ? '#00C853' : '#999999')
        .fontWeight(this.activeTab === tabIdx ? FontWeight.Bold : FontWeight.Normal)
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 5, bottom: 5 })
    .onClick(() => {
      this.activeTab = tabIdx
    })
  }

  build() {
    Column() {
      Stack() {
        if (this.activeTab === 0) {
          CourseTab69()
        } else if (this.activeTab === 1) {
          EquipmentTab()
        } else if (this.activeTab === 2) {
          CoachTab()
        } else if (this.activeTab === 3) {
          CheckInTab()
        } else if (this.activeTab === 4) {
          CommunityTab69()
        } else {
          ProfileTab69()
        }
      }
      .layoutWeight(1)

      Row() {
        this.bottomTabItem('📚', '课程', 0)
        this.bottomTabItem('🏋️', '器材', 1)
        this.bottomTabItem('👨‍🏫', '私教', 2)
        this.bottomTabItem('📊', '打卡', 3)
        this.bottomTabItem('💬', '社区', 4)
        this.bottomTabItem('👤', '我的', 5)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .padding({ top: 4, bottom: 6 })
      .shadow({ radius: 8, color: 'rgba(0,200,83,0.12)', offsetY: -2 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5FFF0')
  }
}

主入口组件的核心设计要点:

  • @State activeTab:声明式状态变量,控制当前显示的Tab索引,点击底部Tab时更新该值触发UI重新渲染。
  • @Builder bottomTabItem:复用构建器,接收图标、标签、Tab索引三个参数,统一构建底部Tab栏项,选中态图标opacity1.0+文字活力绿#00C853+粗体,未选中态图标opacity0.45+文字灰色#999999,实现「绿橙渐变选中+未选灰色」的底栏样式。
  • Stack + 条件渲染:使用Stack容器叠加六个子组件,通过 if/else if 条件渲染根据 activeTab 显示对应Tab,保证Tab切换时的性能和状态隔离。
  • 底部Tab栏:六个Tab项(课程📚/器材🏋️/私教👨‍🏫/打卡📊/社区💬/我的👤)使用Row横向排列,每项 layoutWeight(1) 均分宽度,白色背景+顶部绿色阴影 shadow({ radius: 8, color: 'rgba(0,200,83,0.12)', offsetY: -2 }) 增强层次感。
  • 全局底色:最外层Column设置 backgroundColor('#F5FFF0') 极浅绿白,统一应用整体运动活力风底色。

三、色彩系统:活力绿能量橙双主色与难度三色运动类型八色业务语义体系

多多健身社交的色彩系统以活力绿 #00C853 × 能量橙 #FF6D00 为双主色,底色 #F5FFF0,并围绕健身业务场景构建了完整的难度三色+运动类型八色业务语义色彩体系。

多多健身社交色彩系统与器材Tab运行效果

3.1 主色与底色

色彩角色 色值 用途
活力绿(主色) #00C853 六个Tab统一头部背景、选中Tab文字、按钮主色、难度初级标签、打卡目标完成、已解锁徽章、进度条
能量橙(辅主色) #FF6D00 价格文字、消耗大卡、难度中级标签、限时特价、连续打卡、评分、柱状图最深色
底色 #F5FFF0 全局背景、报名弹窗未选中时段背景、编辑目标TextInput背景
文字主色 #333333 标题、课程名、正文
文字次色 #666666 副标题、说明文字、教练专长
文字辅助色 #999999 时间、原价、辅助信息
分割线 #F0F0F0 / #E0E0E0 卡片分割、边框

3.2 难度三色业务语义体系

健身课程的核心业务语义是「难度等级」,应用构建了难度三色体系,初级/中级/高级各对应独立的颜色+浅底色组合,贯穿课程Tab全场景:

难度 色值 浅底色 代表课程
初级 #00C853 活力绿 #E8F5E9 浅绿 零基础瑜伽入门、尊巴舞蹈瘦身、全身拉伸放松、晨跑5公里计划
中级 #FF6D00 能量橙 #FFF3E0 浅橙 燃脂HIIT挑战20天、动感单车燃脂营、自由泳速成班、马甲线养成计划
高级 #E53935 #FFEBEE 浅红 腹肌撕裂者进阶、拳击搏击燃脂

难度三色通过 FitnessCourse.level + levelColor + levelBg 三字段组合驱动,在双列课程网格中统一使用,用户可通过颜色快速识别课程难度,匹配自己的运动水平。

3.3 运动类型八色与话题五色

除难度三色外,应用还构建了运动类型八色和话题五色等业务语义色彩:

运动类型八色CheckInRecord.typeColor + typeBg 字段驱动,每种运动类型各对应独立颜色:

  • HIIT燃脂:橙#FF6D00+浅橙底
  • 瑜伽放松:紫#7B1FA2+浅紫底
  • 力量训练:红#E53935+浅红底
  • 晨跑5km:蓝#1565C0+浅蓝底
  • 游泳:浅蓝#0288D1+浅蓝底
  • 尊巴舞蹈:粉#EC407A+浅粉底
  • 拉伸放松:青#00897B+浅青底

分类chips八色CATS_69 8个分类各自独立color/bg(瑜伽紫/力量红/有氧蓝/HIIT橙/舞蹈粉/拉伸青/搏击棕/游泳浅蓝),选中态统一活力绿底白字,未选中态对应分类浅底色+分类色文字。

话题五色:社区Tab热门话题chips:

  • #减脂打卡:绿#00C853+浅绿底
  • #增肌心得:红#E53935+浅红底
  • #瑜伽日常:紫#7B1FA2+浅紫底
  • #跑步日记:蓝#1565C0+浅蓝底
  • #私教推荐:橙#FF6D00+浅橙底

3.4 月度运动时长绿色系渐变柱状图

个人Tab月度运动时长柱状图采用绿色系渐变色,从3月到8月颜色由浅到深,直观呈现运动时长的增长趋势:

月份 运动时长 色值 色彩层级
3月 15小时 #A5D6A7 极浅绿
4月 22小时 #A5D6A7 极浅绿
5月 18小时 #A5D6A7 极浅绿
6月 25小时 #81C784 浅绿
7月 28小时 #4CAF50 中绿
8月 30小时 #00C853 深活力绿

柱状图通过纯声明式 Column + backgroundColor(bar.color) + height(bar.value * 3) 实现,无需Canvas,alignItems(VerticalAlign.Bottom) 让柱子从底部向上生长,8月30小时最高柱子颜色最深(活力绿主色#00C853),形成强烈的视觉对比,直观呈现用户从3月15小时到8月30小时的运动时长增长(翻倍),体现坚持运动的进步。

四、数据模型:8个interface强类型定义与业务字段设计

多多健身社交使用 TypeScript interface 定义了8个强类型数据模型,覆盖健身课程、运动器材、私教、打卡记录、社区动态、分类、柱状图、成就徽章八大业务实体,每个interface都针对健身业务场景设计了专属字段。

4.1 数据模型清单

Interface 用途 核心字段 特色字段
FitnessCourse 健身课程 id/title/coach/cover/price/originalPrice/duration/calories/level/levelColor/levelBg/students/rating/tag/category level/levelColor/levelBg难度独立色、calories消耗大卡、duration时长
Equipment 运动器材 id/name/brand/cover/price/originalPrice/sold/tag/category/rating brand品牌、sold销量、category分类
Coach 私教 id/name/avatar/title/specialty/rating/students/price/cert/bio cert认证标签(ACE-CPT/RYT-500等)、specialty专长、bio简介、price课时费
CheckInRecord 打卡记录 id/date/type/typeIcon/duration/calories/typeColor/typeBg type/typeColor/typeBg运动类型独立色、typeIcon类型图标、duration时长、calories消耗
PostItem69 社区动态 id/avatar/name/content/image/likes/comments/date/tag image照片、tag动态标签、likes/comments互动数据
CatChip69 分类标签 label/icon/color/bg color/bg分类独立色(8分类各自独立配色)
BarData69 柱状图数据 label/value/color color柱子颜色(绿色系渐变)
Achievement69 成就徽章 icon/label/desc/unlocked unlocked解锁状态布尔值(驱动opacity0.5半透明)

4.2 核心数据模型详解

FitnessCourse(健身课程) 是最核心的数据模型,10条课程数据涵盖瑜伽/力量/有氧/HIIT/舞蹈/搏击/拉伸/游泳八大分类,包含燃脂HIIT挑战20天(中级¥99 8.6万人 9.5分)、零基础瑜伽入门(初级¥79 12.3万人 9.6分最高人气)、腹肌撕裂者进阶(高级¥129 硬核)、晨跑5公里计划(初级¥59免费 15.6万人 9.8分最高分)等经典课程,每条数据都包含 level+levelColor+levelBg 难度三字段组合,以及 calories(消耗大卡)、duration(时长)、students(学习人数)、rating(评分)等健身课程核心指标。

Coach(私教) 的特色是 cert(认证标签,如ACE-CPT美国运动委员会认证、RYT-500印度瑜伽联盟500小时认证、NSCA-CSCS美国体能协会认证、ACSM-CPT美国运动医学会认证、ZIN尊巴官方认证)字段,以及 specialty(专长)、bio(简介)、price(课时费)、students(学员数),6位教练涵盖HIIT/瑜伽/力量/有氧/舞蹈/塑形六大专长,每位都有专业认证背书,构建专业可信的私教形象。

CheckInRecord(打卡记录) 的特色是 type(运动类型)+typeIcon(类型图标)+typeColor+typeBg 运动类型四字段组合,以及 duration(时长)、calories(消耗大卡),7条打卡记录涵盖HIIT/瑜伽/力量/晨跑/游泳/尊巴/拉伸七种运动类型,每条记录都有独立的类型颜色,用户可通过颜色快速识别运动类型。

Achievement69(成就徽章) 的特色是 unlocked 布尔值字段,通过 a.unlocked ? 1.0 : 0.5 动态设置卡片opacity,未解锁徽章半透明显示,并配合 if (a.unlocked) 条件渲染显示「✅ 已解锁」或「🔒 未解锁」标签,5个徽章中4个已解锁(连续打卡30天/力量大师/跑步达人/瑜伽修行者),1个未解锁(健身王者,累计运动1000小时解锁)。

五、课程Tab:搜索头+分类chips横滑+精选大卡+双列课程网格

课程Tab(CourseTab69)是多多健身社交的首页核心,以「分类chips横滑+精选大卡+双列课程网格」为视觉核心,配合搜索头,完整呈现健身课程信息。

5.1 布局结构

课程Tab采用 Column 纵向布局,从上到下依次为:

  1. 活力绿搜索头height(44) + backgroundColor('#00C853'),左侧🔍搜索图标+「搜索健身课程、教练…」占位文字(白色70%透明度),右侧💪图标。
  2. 分类chips横滑Scroll 横向滚动+Row,8个分类(瑜伽🧘/力量💪/有氧🏃/HIIT🔥/舞蹈💃/拉伸🤸/搏击🥊/游泳🏊),每个分类独立color/bg,选中态活力绿底#00C853白字粗体,未选中态对应分类浅底色+分类色文字。
  3. 精选大卡:双大卡横向排列,左侧燃脂HIIT 20天大卡(活力绿底#00C853+🔥44号+张力教练+8.6万人+¥99起白底橙字标签,width55%),右侧零基础瑜伽大卡(紫底#7B1FA2+🧘44号+李静教练+12.3万人+¥79起白底紫字标签,layoutWeight1)。
  4. 热门课程标题栏:左侧「🔥 热门课程」粗体标题,右侧「全部 >」活力绿链接。
  5. 双列课程网格ForEach(courses) 遍历10门课程,每门用 Row 横向排列左右两卡,左卡显示tag橙底+难度标签(levelColor/levelBg独立色)+课程名+教练+时长+消耗大卡(橙)+价格+评分人数,右卡显示tag绿底+分类+课程名+教练+时长+消耗大卡+价格+评分人数,左右卡信息差异化呈现,点击任意卡打开报名课程弹窗。

5.2 精选大卡核心代码

Column({ space: 12 }) {
  Row({ space: 12 }) {
    Column({ space: 6 }) {
      Text('🔥')
        .fontSize(44)
      Text('燃脂HIIT 20天')
        .fontSize(15)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)
      Row({ space: 6 }) {
        Text('张力教练')
          .fontSize(10)
          .fontColor('rgba(255,255,255,0.8)')
        Text('8.6万人')
          .fontSize(10)
          .fontColor('rgba(255,255,255,0.6)')
      }
      Text('¥99 起')
        .fontSize(14)
        .fontColor('#FF6D00')
        .fontWeight(FontWeight.Bold)
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .padding({ left: 8, right: 8, top: 2, bottom: 2 })
    }
    .width('55%')
    .padding(16)
    .borderRadius(12)
    .backgroundColor('#00C853')
    .alignItems(HorizontalAlign.Start)

    Column({ space: 6 }) {
      Text('🧘')
        .fontSize(44)
      Text('零基础瑜伽')
        .fontSize(15)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)
      Row({ space: 6 }) {
        Text('李静教练')
          .fontSize(10)
          .fontColor('rgba(255,255,255,0.8)')
        Text('12.3万人')
          .fontSize(10)
          .fontColor('rgba(255,255,255,0.6)')
      }
      Text('¥79 起')
        .fontSize(14)
        .fontColor('#7B1FA2')
        .fontWeight(FontWeight.Bold)
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .padding({ left: 8, right: 8, top: 2, bottom: 2 })
    }
    .layoutWeight(1)
    .padding(16)
    .borderRadius(12)
    .backgroundColor('#7B1FA2')
    .alignItems(HorizontalAlign.Start)
  }
  .width('100%')
  .padding({ left: 12, right: 12 })
}

精选大卡的设计要点:

  • 双大卡不对称布局:左侧燃脂HIIT大卡 width('55%') 占据主要视觉空间,右侧零基础瑜伽大卡 layoutWeight(1) 占据剩余空间,形成主次分明的视觉层次。
  • 绿紫双色系搭配:左侧大卡活力绿#00C853(主色),右侧大卡紫#7B1FA2(瑜伽分类色),二者形成鲜明的色彩对比,既保持视觉活力又有课程类型区分度。
  • 白底价格标签:每张卡底部的价格标签(¥99起/¥79起)使用白色背景+圆角8,价格文字使用对应主题色(HIIT橙#FF6D00/瑜伽紫#7B1FA2),在深色背景上形成强烈的视觉焦点,突出价格信息。
  • 44号大图标:课程图标使用44号大字号,在深色背景上形成强烈的视觉焦点,增强课程的识别度。

5.3 双列课程网格难度标签

双列课程网格的核心是难度标签,通过 FitnessCourse.level + levelColor + levelBg 三字段组合驱动:

Row() {
  Text(c.tag)
    .fontSize(9)
    .fontColor('#FFFFFF')
    .backgroundColor('#FF6D00')
    .borderRadius(3)
    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
  Text(' ' + c.level)
    .fontSize(9)
    .fontColor(c.levelColor)
    .backgroundColor(c.levelBg)
    .borderRadius(3)
    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
}

难度标签的设计要点:

  • tag+level双标签:每门课程顶部显示tag标签(爆款/热门/硬核/推荐等,左卡橙底右卡绿底)+难度标签(初级/中级/高级,levelColor/levelBg独立色),双标签横向排列,用户可一眼识别课程的热度和难度。
  • 难度三色独立色:初级绿#00C853+浅绿底#E8F5E9、中级橙#FF6D00+浅橙底#FFF3E0、高级红#E53935+浅红底#FFEBEE,三色对应三个难度等级,用户可通过颜色快速匹配自己的运动水平。
  • 左右卡差异化:左卡tag使用橙底#FF6D00,显示难度标签level;右卡tag使用绿底#00C853,显示分类category,左右卡信息侧重点不同,避免完全重复的单调感。

每门课程卡还显示消耗大卡(能量橙#FF6D00,如350大卡/180大卡/500大卡)、价格(能量橙粗体+原价删除线)、评分+学习人数,完整呈现健身课程的核心信息,点击任意卡打开报名课程底部抽屉弹窗,用户可选择训练时段、选择教练、确认报名。

六、器材Tab:限时特价横条+双列器材网格

器材Tab(EquipmentTab)聚焦运动器材电商场景,以限时特价和双列器材网格为核心,还原运动器材电商体验。

6.1 布局结构

器材Tab采用 Column 纵向布局,从上到下依次为:

  1. 活力绿头部栏:「🏋️ 运动器材」标题+右侧🛒购物车图标。
  2. 限时特价横条:浅橙底#FFF3E0+⚡28号图标+「限时特价 5折起」粗体标题+「筋膜枪、哑铃、瑜伽垫热销中」说明+右侧倒计时「02:15:30」(白底橙字圆角6),营造限时抢购的紧迫感。
  3. 热销器材标题栏:左侧「🔥 热销器材」粗体标题,右侧「全部 >」活力绿链接。
  4. 双列器材网格ForEach(equipments) 遍历8件器材,每件用 Row 横向排列左右两卡,左卡显示tag橙底+品牌+器材名+销量+价格+评分(绿),右卡显示tag绿底+分类+器材名+品牌+销量+价格+评分,左右卡信息差异化呈现。

6.2 限时特价横条核心代码

Row({ space: 12 }) {
  Text('⚡')
    .fontSize(28)
    .fontColor('#FF6D00')
  Column({ space: 2 }) {
    Text('限时特价 5折起')
      .fontSize(14)
      .fontColor('#333333')
      .fontWeight(FontWeight.Bold)
    Text('筋膜枪、哑铃、瑜伽垫热销中')
      .fontSize(10)
      .fontColor('#999999')
  }
  .alignItems(HorizontalAlign.Start)
  .layoutWeight(1)
  Text('02:15:30')
    .fontSize(12)
    .fontColor('#FFFFFF')
    .backgroundColor('#FF6D00')
    .borderRadius(6)
    .padding({ left: 8, right: 8, top: 4, bottom: 4 })
}
.width('92%')
.padding(12)
.backgroundColor('#FFF3E0')
.borderRadius(10)
.margin({ left: 12, right: 12, top: 12, bottom: 8 })

限时特价横条的设计要点:

  • 浅橙底+能量橙主题:整体使用浅橙底#FFF3E0,配合⚡能量橙图标和能量橙倒计时背景,营造「限时特价、能量抢购」的氛围,能量橙#FF6D00是健身应用的辅主色,代表能量和燃脂激情。
  • 倒计时标签:右侧「02:15:30」倒计时使用能量橙底#FF6D00+白字+圆角6,突出限时抢购的紧迫感,是电商类应用常用的促销手法。
  • 热销品类说明:中间说明文字「筋膜枪、哑铃、瑜伽垫热销中」明确指出当前特价的热销器材品类,引导用户关注。

8件器材涵盖可调哑铃20kg(海德¥299 已售2.3万)、瑜伽垫加厚防滑(Keep¥89 已售8.6万爆款)、跳绳智能计数(Keep¥49 已售12.3万最高销量)、弹力带5件套(迪卡侬¥39)、筋膜枪mini版(SKG¥199)、智能手环运动版(华为¥249)、壶铃16kg(海德¥159)、平衡球65cm(迪卡侬¥79),覆盖力量/瑜伽/有氧/拉伸/恢复/穿戴/核心七大器材品类,数据贴合真实健身器材电商行情。

七、私教Tab:教练详情大卡列表+预约双按钮

私教Tab(CoachTab)聚焦私教预约场景,以教练详情大卡列表为核心,配合预约体验/立即预约双按钮,还原私教预约体验。

多多健身社交私教Tab运行效果

7.1 布局结构

私教Tab采用 Column 纵向布局,从上到下依次为:

  1. 活力绿头部栏:「👨‍🏫 精选私教」标题+右侧🔍搜索图标。
  2. 教练详情大卡列表ForEach(coaches) 遍历6位教练,每位教练一个大卡,包含:
    • 顶部:44号头像+教练名+认证标签(cert,绿底浅绿,如ACE-CPT/RYT-500/NSCA-CSCS)+职称+专长+简介(bio)
    • 数据行:评分⭐(橙)+学员数👥+课时费💰(橙粗体)
    • 底部双按钮:预约体验(绿底浅绿边框绿字,layoutWeight1)+立即预约(活力绿底白字,layoutWeight1)

7.2 教练详情大卡核心代码

Column({ space: 8 }) {
  Row({ space: 12 }) {
    Text(c.avatar)
      .fontSize(44)
    Column({ space: 4 }) {
      Row({ space: 6 }) {
        Text(c.name)
          .fontSize(15)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
        Text(c.cert)
          .fontSize(9)
          .fontColor('#00C853')
          .backgroundColor('#E8F5E9')
          .borderRadius(3)
          .padding({ left: 5, right: 5, top: 1, bottom: 1 })
      }
      Text(c.title + ' · ' + c.specialty)
        .fontSize(11)
        .fontColor('#666666')
      Text(c.bio)
        .fontSize(10)
        .fontColor('#999999')
        .lineHeight(15)
      Row({ space: 12 }) {
        Row({ space: 4 }) {
          Text('⭐')
            .fontSize(10)
          Text(c.rating.toString())
            .fontSize(11)
            .fontColor('#FF6D00')
            .fontWeight(FontWeight.Bold)
        }
        Row({ space: 4 }) {
          Text('👥')
            .fontSize(10)
          Text(c.students + '学员')
            .fontSize(10)
            .fontColor('#999999')
        }
        Row({ space: 4 }) {
          Text('💰')
            .fontSize(10)
          Text('¥' + c.price.toString() + '/课时')
            .fontSize(10)
            .fontColor('#FF6D00')
            .fontWeight(FontWeight.Bold)
        }
      }
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)
  }
  .width('100%')

  Row({ space: 12 }) {
    Button() {
      Text('预约体验')
        .fontSize(12)
        .fontColor('#00C853')
        .fontWeight(FontWeight.Bold)
    }
    .type(ButtonType.Capsule)
    .height(36)
    .layoutWeight(1)
    .backgroundColor('#E8F5E9')
    .border({ width: 1, color: '#00C853' })

    Button() {
      Text('立即预约')
        .fontSize(12)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)
    }
    .type(ButtonType.Capsule)
    .height(36)
    .layoutWeight(1)
    .backgroundColor('#00C853')
  }
  .width('100%')
}

教练详情大卡的设计要点:

  • 认证标签专业背书:每位教练名字旁显示 cert 认证标签(活力绿#00C853字+浅绿底#E8F5E9),如ACE-CPT(美国运动委员会认证私教)、RYT-500(印度瑜伽联盟500小时认证)、NSCA-CSCS(美国体能协会认证体能专家)、ACSM-CPT(美国运动医学会认证私教)、ZIN(尊巴官方认证),专业认证标签是私教可信度的核心背书,活力绿统一颜色体现专业健康的形象。
  • 三维数据展示:每位教练显示评分⭐(能量橙#FF6D00粗体)、学员数👥(灰色)、课时费💰(能量橙#FF6D00粗体/课时),三维数据帮助用户快速评估教练的专业度、人气和价格。
  • 预约双按钮设计:底部「预约体验」(浅绿底#E8F5E9+绿边框+绿字,低门槛体验课)+「立即预约」(活力绿底#00C853+白字,正式预约)双按钮横向排列layoutWeight1均分,双按钮设计满足不同用户的需求(想先体验vs直接报名),是私教预约场景的标准交互模式。
  • 简介lineHeight:教练简介bio使用 lineHeight(15) 行高,10号灰色小字,保证多行简介的可读性。

6位教练涵盖张力(HIIT/燃脂,ACE-CPT,¥199/课时,8.6万学员)、李静(瑜伽/冥想,RYT-500,¥179,12.3万学员最高人气)、王猛(增肌/力量,NSCA-CSCS,¥229最高课时费,全国健美冠军)、陈飞(跑步/单车,ACSM-CPT,¥159,马拉松PB2:38)、赵舞(尊巴/舞蹈,ZIN,¥149最低课时费)、林美(塑形/马甲线,ACE-CPT,¥189,IFBB比基尼选手),数据贴合真实健身私教行业行情。

八、打卡Tab:运动数据环形图+连续打卡条+打卡记录列表

打卡Tab(CheckInTab)聚焦运动数据追踪场景,以运动数据环形图和打卡记录列表为核心,配合编辑健身目标和删除打卡记录弹窗,完整还原运动打卡体验。

多多健身社交打卡Tab运行效果

8.1 布局结构

打卡Tab采用 Column 纵向布局,从上到下依次为:

  1. 活力绿头部栏:「📊 运动打卡」标题+右侧🎯目标图标(点击打开编辑健身目标弹窗,预填「每天运动30分钟,消耗300大卡」)。
  2. 运动数据总览卡:白色卡片,左侧环形数据(85%目标完成,圆形width/height80圆角40浅绿底#E8F5E9+4px活力绿边框模拟环形进度条,居中显示85%),右侧「本周运动数据」+三列统计(打卡天数5/7绿/消耗大卡1,910橙/运动分钟273紫)。
  3. 连续打卡条:浅绿底#E8F5E9+「✅ 今日已打卡」绿粗体+「连续30天 🔥」橙。
  4. 打卡记录标题
  5. 打卡记录列表:7条记录,每条含类型图标36号+运动类型+日期📅+时长⏱️+消耗大卡🔥(橙)+类型标签(typeColor/typeBg独立色)+右侧⋮更多按钮(点击打开删除打卡记录弹窗)。

8.2 运动数据环形图核心代码

Row({ space: 16 }) {
  Column({ space: 4 }) {
    Text('85%')
      .fontSize(28)
      .fontColor('#00C853')
      .fontWeight(FontWeight.Bold)
    Text('目标完成')
      .fontSize(10)
      .fontColor('#999999')
  }
  .width(80)
  .height(80)
  .borderRadius(40)
  .backgroundColor('#E8F5E9')
  .border({ width: 4, color: '#00C853' })
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)

  Column({ space: 6 }) {
    Text('本周运动数据')
      .fontSize(14)
      .fontColor('#333333')
      .fontWeight(FontWeight.Bold)
    Row({ space: 16 }) {
      Column({ space: 2 }) {
        Text('5/7')
          .fontSize(16)
          .fontColor('#00C853')
          .fontWeight(FontWeight.Bold)
        Text('打卡天数')
          .fontSize(9)
          .fontColor('#999999')
      }
      Column({ space: 2 }) {
        Text('1,910')
          .fontSize(16)
          .fontColor('#FF6D00')
          .fontWeight(FontWeight.Bold)
        Text('消耗大卡')
          .fontSize(9)
          .fontColor('#999999')
      }
      Column({ space: 2 }) {
        Text('273')
          .fontSize(16)
          .fontColor('#7B1FA2')
          .fontWeight(FontWeight.Bold)
        Text('运动分钟')
          .fontSize(9)
          .fontColor('#999999')
      }
    }
  }
  .layoutWeight(1)
}

运动数据环形图的设计要点:

  • 圆形边框模拟环形进度条:使用 width(80).height(80).borderRadius(40) 圆形+backgroundColor('#E8F5E9') 浅绿底+border({ width: 4, color: '#00C853' }) 4px活力绿边框,模拟环形进度条的视觉效果,居中显示「85%」大字号(活力绿粗体)+「目标完成」标签,无需Canvas即可实现环形进度图的视觉效果,简洁高效。
  • 本周三列数据:右侧显示「本周运动数据」标题+三列统计(打卡天数5/7活力绿/消耗大卡1,910能量橙/运动分钟273紫),三色对应三个数据维度,用户可一眼掌握本周的运动数据概览。
  • 85%目标完成:环形图显示85%的目标完成率,说明用户本周运动目标完成度很高,配合连续30天打卡条,体现用户的坚持和自律。

8.3 打卡记录类型标签

打卡记录列表的核心是运动类型标签,通过 CheckInRecord.type + typeIcon + typeColor + typeBg 四字段组合驱动,7条打卡记录涵盖7种运动类型,每种类型都有独立的图标和颜色:

  • HIIT燃脂:🔥图标,橙#FF6D00+浅橙底,30分钟350大卡
  • 瑜伽放松:🧘图标,紫#7B1FA2+浅紫底,45分钟180大卡
  • 力量训练:💪图标,红#E53935+浅红底,40分钟280大卡
  • 晨跑5km:🏃图标,蓝#1565C0+浅蓝底,28分钟300大卡
  • 游泳:🏊图标,浅蓝#0288D1+浅蓝底,60分钟500大卡(最高消耗)
  • 尊巴舞蹈:💃图标,粉#EC407A+浅粉底,50分钟320大卡
  • 拉伸放松:🤸图标,青#00897B+浅青底,20分钟80大卡(最低消耗)

每条记录右侧的⋮更多按钮点击后打开删除打卡记录居中弹窗(🗑️图标+「删除打卡记录?」+「删除后该记录将无法恢复」+取消/删除双按钮),用户可删除误记录的打卡数据。

打卡Tab头部右侧的🎯目标图标点击后打开编辑健身目标底部抽屉弹窗(TextInput height44,预填「每天运动30分钟,消耗300大卡」,浅绿底#F5FFF0+浅绿边框#A5D6A7,取消/保存目标双按钮),用户可设定和修改自己的每日运动目标。

九、社区Tab:热门话题chips横滑+健身动态列表含照片墙

社区Tab(CommunityTab69)聚焦健身爱好者的「分享交流」场景,以热门话题和健身动态为核心,构建健身社区氛围。

多多健身社交社区Tab运行效果

9.1 布局结构

社区Tab采用 Column 纵向布局,从上到下依次为:

  1. 活力绿头部栏:「💬 健身社区」标题+右侧➕发布图标。
  2. 热门话题chips横滑Scroll 横向滚动+Row,5个话题标签(#减脂打卡绿/#增肌心得红/#瑜伽日常紫/#跑步日记蓝/#私教推荐橙),每个话题独立颜色+对应浅底色+圆角胶囊样式。
  3. 健身动态列表:5条动态,每条含头像+昵称+日期+动态标签(绿底浅绿)+动态内容(lineHeight18)+照片墙(三个📸32号图标横向排列)+底部互动栏(点赞👍含数量/评论💬含数量/分享📤,justifyContent SpaceBetween两端对齐)。

9.2 照片墙设计

健身动态列表的特色是「照片墙」设计,每条动态下方显示三个📸32号图标横向排列(space8),模拟健身爱好者分享的运动照片、身材对比照、健身餐照片等,三图照片墙是社交类应用动态的标准展示形式,增强动态的视觉丰富度和真实感。

5条动态涵盖:

  • 燃脂达人(减脂打卡标签):坚持HIIT 20天,体脂从28%降到22%,点赞1560最高
  • 瑜伽小姐姐(瑜伽心得标签):每天瑜伽45分钟,体态变好了,点赞980
  • 肌肉狂魔(增肌打卡标签):深蹲硬拉卧推三大项全PB,增肌5kg,点赞756
  • 晨跑健将(跑步打卡标签):连续晨跑30天,5km从30分钟缩短到23分钟,点赞620
  • 舞蹈女孩(快乐燃脂标签):尊巴太快乐了,跳着跳着就瘦了,点赞530

动态内容围绕健身成果分享、经验交流,高互动数据(点赞1560/评论128)体现健身社区的活跃氛围,减脂打卡类动态获得最高互动,符合健身社区的主流需求。

十、我的Tab:渐变头+四列数据悬浮卡+月度运动柱状图+成就徽章

我的Tab(ProfileTab69)聚焦健身爱好者的「个人成就」场景,以运动数据统计和成就徽章为核心,构建个人成就体系。

多多健身社交我的Tab运行效果

10.1 布局结构

我的Tab采用 Column 纵向布局,从上到下依次为:

  1. 活力绿渐变头backgroundColor('#00C853') + padding top32 bottom24,居中🏃56号大图标+「健身达人」昵称(白色18号粗体)+「连续打卡30天·累计运动273小时」说明(白色80%透明度)。
  2. 四列数据悬浮卡:白色卡片+margin top:-12(与渐变头重叠形成悬浮效果)+四列统计横向排列(连续打卡30绿/运动小时273橙/消耗大卡12,560紫/报名课程4红),每列layoutWeight1均分。
  3. 月度运动时长标题
  4. 月度运动时长绿色系渐变柱状图:白色卡片+6个月柱状图(3-8月,绿色系从浅#A5D6A7到深#00C853,height=value*3动态计算,alignItems VerticalAlign Bottom从底部向上生长,8月30小时最高柱子最深活力绿)。
  5. 成就徽章标题
  6. 成就徽章列表:5个徽章(连续打卡30天🔥/力量大师💪/跑步达人🏃/瑜伽修行者🧘已解锁✅,健身王者🏆未解锁🔒opacity0.5半透明),每个徽章含图标+徽章名+描述+解锁状态(if unlocked显示✅已解锁绿,否则🔒未解锁灰),未解锁徽章半透明显示。

10.2 四列数据悬浮卡

四列数据悬浮卡是我的Tab的核心数据展示,四列分别对应健身爱好者最关心的四个个人数据指标:

指标 数值 颜色 业务含义
连续打卡 30天 活力绿#00C853 坚持运动的天数,体现自律
运动小时 273小时 能量橙#FF6D00 累计运动时长,体现运动量
消耗大卡 12,560大卡 紫#7B1FA2 累计消耗热量,体现燃脂成果
报名课程 4门 红#E53935 报名的课程数量,体现学习投入

四列数据通过绿/橙/紫/红四色区分,margin({ top: -12 }) 让白色卡片向上偏移12px与上方活力绿渐变头重叠,形成「卡片悬浮在渐变头之上」的层次感,是电商/社交类应用个人中心常用的设计手法。

10.3 月度运动时长绿色系渐变柱状图

月度运动时长柱状图采用6个月绿色系渐变色,从3月到8月颜色由浅到深,直观呈现运动时长的增长趋势:

月份 运动时长 色值 色彩层级
3月 15小时 #A5D6A7 极浅绿
4月 22小时 #A5D6A7 极浅绿
5月 18小时 #A5D6A7 极浅绿
6月 25小时 #81C784 浅绿
7月 28小时 #4CAF50 中绿
8月 30小时 #00C853 深活力绿

柱状图通过纯声明式 Column + backgroundColor(bar.color) + height(bar.value * 3) 实现,无需Canvas,alignItems(VerticalAlign.Bottom) 让柱子从底部向上生长,8月30小时最高柱子颜色最深(活力绿主色#00C853),形成强烈的视觉对比,直观呈现用户从3月15小时到8月30小时的运动时长翻倍增长,体现坚持运动的进步和成就感。

10.4 成就徽章解锁状态

成就徽章列表的特色是已解锁/未解锁状态区分,通过 Achievement69.unlocked 布尔值字段驱动:

  • 已解锁徽章(连续打卡30天🔥/力量大师💪/跑步达人🏃/瑜伽修行者🧘):opacity1.0完全不透明,显示「✅ 已解锁」活力绿#00C853标签。
  • 未解锁徽章(健身王者🏆,累计运动1000小时,当前273小时):opacity0.5半透明显示,显示「🔒 未解锁」灰色#CCCCCC标签,传达「累计运动1000小时即可解锁」的激励信号。

通过 a.unlocked ? 1.0 : 0.5 动态设置卡片opacity,配合 if (a.unlocked) 条件渲染显示不同的解锁状态标签,构建成就徽章的解锁状态视觉,激励用户继续运动以解锁健身王者徽章。5个徽章中4个已解锁1个未解锁,体现用户的高成就水平,同时保留一个未解锁徽章作为持续激励。

十一、四种模态弹窗:报名课程底部抽屉+取消报名居中+编辑健身目标底部抽屉+删除打卡记录居中

多多健身社交共实现四种模态弹窗,覆盖报名课程、取消报名、编辑健身目标、删除打卡记录四大交互场景,采用「底部抽屉」和「居中确认框」两种弹窗样式。

多多健身社交弹窗运行效果

11.1 弹窗清单

弹窗名称 样式 触发位置 核心内容
报名课程 底部抽屉 课程Tab课程卡点击 课程信息+4时段选择(06:00/12:00/19:00/21:00)+6教练选择(头像+名字+价格)+实付金额+确认报名按钮
取消报名 居中确认框 (设计预留) 取消确认+再想想/确认取消双按钮
编辑健身目标 底部抽屉 打卡Tab🎯目标图标点击 TextInput预填目标+取消/保存目标双按钮
删除打卡记录 居中确认框 打卡Tab记录⋮按钮点击 🗑️图标+删除确认+取消/删除双按钮

11.2 报名课程底部抽屉详解

报名课程弹窗是四种弹窗中最复杂的,采用底部抽屉样式(Stack alignContent Bottom),内容丰富:

  1. 顶部拖拽条:width40 height4 灰色圆角,居中显示。
  2. 课程信息:🔥课程图48号+课程名「燃脂HIIT挑战20天」+教练·时长·消耗大卡+价格¥99(橙粗体)+原价¥299删除线。
  3. 时段选择:标题「选择训练时段」+4个时段选项(06:00-07:00/12:00-13:00/19:00-20:00/21:00-22:00),选中态活力绿底#00C853白字粗体+绿边框,未选中态浅绿底#F5FFF0灰字+灰边框,点击切换选中时段selectedTime。
  4. 教练选择:标题「选择教练」+6个教练选项(头像24号+名字+价格),选中态活力绿底白字+绿边框,未选中态浅绿底灰字+灰边框,点击切换选中教练selectedCoach。
  5. 底部操作栏:左侧实付金额(「实付金额」灰+「¥99」橙20号粗体),右侧「确认报名」活力绿底白字Capsule按钮height44,点击关闭弹窗。

报名课程弹窗的核心是时段+教练双选择,用户可根据自己的时间安排和教练偏好选择适合的训练时段和教练,完整还原在线课程报名流程,4个时段覆盖早中晚三个时间段,6位教练覆盖不同专长,满足不同用户的需求。

11.3 编辑健身目标底部抽屉

编辑健身目标弹窗采用底部抽屉样式:

  • 标题:「🎯 编辑健身目标」16号粗体+副标题「设定你的每日运动目标」。
  • 输入框:TextInput height44,placeholder「例如:每天运动30分钟,消耗300大卡」,背景色#F5FFF0,边框#A5D6A7浅绿,点击🎯目标图标时预填 this.goalContent = '每天运动30分钟,消耗300大卡'
  • 底部按钮:取消(灰底Capsule)+保存目标(活力绿底白字Capsule),双按钮横向排列layoutWeight1均分。

这个弹窗功能简单但实用,用户可设定和修改自己的每日运动目标,目标数据会驱动打卡Tab的环形进度图(85%目标完成),形成「设定目标→运动打卡→查看完成率」的完整闭环。

11.4 居中确认框统一设计

取消报名和删除打卡记录两种弹窗采用统一的居中确认框样式(Stack alignContent Center):

  • 白色卡片width80%+padding24+圆角16,居中显示。
  • 顶部图标(删除打卡记录🗑️40号)。
  • 标题(「删除打卡记录?」15号粗体)。
  • 说明(「删除后该记录将无法恢复」12号灰色)。
  • 底部双按钮(取消灰底Capsule+删除红底#E53935白字Capsule,layoutWeight1均分,space12间距)。

居中确认框样式简洁统一,适用于「危险操作确认」类场景(取消、删除),通过红色主按钮传达「此操作不可逆」的警示信号,与报名课程、编辑目标等底部抽屉表单类弹窗形成样式区分。

十二、核心技术特性总结

12.1 六组件多Tab健身社交电商架构

应用采用六个独立 @Component 子组件(CourseTab69/EquipmentTab/CoachTab/CheckInTab/CommunityTab69/ProfileTab69)+ 一个 @Entry 主入口组件(DuoDuoFitnessApp)的六组件多Tab架构,每个Tab对应一个独立子组件,职责清晰、状态隔离,主入口通过 @State activeTab + if/else if 条件渲染调度Tab切换,@Builder bottomTabItem 复用底部Tab栏项,是ArkTS声明式UI多Tab应用的标准架构模式。

12.2 活力绿能量橙双主色与难度三色运动类型八色业务语义体系

应用以活力绿 #00C853 × 能量橙 #FF6D00 为双主色,底色 #F5FFF0,构建了完整的业务语义色彩体系:难度三色(初级绿/中级橙/高级红)、运动类型八色(HIIT橙/瑜伽紫/力量红/晨跑蓝/游泳浅蓝/尊巴粉/拉伸青)、分类chips八色、话题五色(减脂打卡绿/增肌心得红/瑜伽日常紫/跑步日记蓝/私教推荐橙),每种业务状态都有独立的颜色+浅底色组合,信息传达高效直观。

12.3 六大Tab差异化布局

课程Tab(搜索头+分类chips横滑8分类+精选大卡双色系+双列课程网格含难度三色标签)、器材Tab(限时特价横条含倒计时+双列器材网格)、私教Tab(教练详情大卡含认证标签+预约体验/立即预约双按钮)、打卡Tab(运动数据环形图85%+本周三列数据+连续打卡条+打卡记录列表含运动类型八色标签)、社区Tab(五色话题chips横滑+健身动态含三图照片墙)、我的Tab(活力绿渐变头+四列数据悬浮卡+月度运动绿色系渐变柱状图+成就徽章解锁状态),每个Tab都有独特的视觉核心和布局特色,避免千篇一律。

12.4 四模态弹窗与底部抽屉+居中确认框双样式

应用实现四种模态弹窗:报名课程底部抽屉(4时段选择+6教练选择+实付金额)、取消报名居中确认框、编辑健身目标底部抽屉(TextInput预填目标)、删除打卡记录居中确认框,采用底部抽屉(复杂表单类)+居中确认框(危险操作确认类)双样式,底部抽屉统一顶部拖拽条设计,居中确认框统一图标+标题+说明+双按钮设计,样式规范统一。

12.5 丰富的数据可视化

打卡Tab运动数据环形图(圆形width/height80+4px活力绿边框模拟环形进度条,居中85%)、打卡记录消耗大卡、我的Tab月度运动时长绿色系渐变柱状图(3-8月从浅绿#A5D6A7到深活力绿#00C853,height=value*3,8月30小时最高最深),通过纯声明式Column+背景色+圆形边框+动态height实现,无需Canvas,简洁高效。

12.6 8个interface强类型定义与8组静态配置数据

FitnessCourse(含level三字段难度独立色+calories消耗大卡)、Equipment、Coach(含cert认证标签+specialty专长+bio简介)、CheckInRecord(含type四字段运动类型独立色+typeIcon图标)、PostItem69、CatChip69(8分类独立color/bg)、BarData69、Achievement69(含unlocked驱动opacity),10课程+8器材+6教练+7打卡记录+5社区动态+8分类+6月柱状图+5成就徽章,数据丰富且贴合真实健身社交业务。

十三、与同类健身社交App的特性对比

对比维度 普通健身App 多多健身社交 优势说明
首页核心 推荐课程流 分类chips+精选大卡+双列课程网格含难度标签 以「分类+难度」为核心筛选,8分类chips横滑+难度三色标签,用户快速匹配适合的课程
器材电商 无或简单链接 限时特价横条含倒计时+双列器材网格 完整的运动器材电商体验,限时特价倒计时营造紧迫感,8件器材覆盖七大品类
私教预约 简单教练列表 教练详情大卡含认证标签+预约体验/立即预约双按钮 专业认证标签(ACE/RYT/NSCA等)+三维数据(评分/学员/课时费)+双按钮预约,还原专业私教预约体验
打卡功能 简单打卡记录 运动数据环形图+本周三列数据+连续打卡条+类型八色标签 丰富的数据可视化(环形图85%+三列统计),运动类型八色标签,完整还原运动数据追踪体验
社区功能 评论/点赞 五色话题chips+健身动态含三图照片墙 健身专属社区,五色话题分类+三图照片墙+高互动数据,贴合健身爱好者分享交流需求
个人中心 订单/设置 四列数据悬浮卡+月度运动绿色系柱状图+成就徽章解锁状态 以「运动成就」为核心,月度运动增长可视化+徽章解锁状态激励,贴合健身爱好者的坚持成就感
色彩体系 单一品牌色 活力绿×能量橙双主色+难度三色+运动类型八色+话题五色 丰富的业务语义色彩,难度/运动类型/话题各有独立配色,信息传达高效
弹窗体系 简单确认框 底部抽屉+居中确认框双样式,报名含时段+教练双选择 弹窗样式规范统一,报名课程弹窗内容完整(4时段+6教练+实付金额),还原真实课程报名流程

十四、完整代码实现

完整的 69.ets 源码包含8个interface定义、8组静态配置数据(CATS_69分类8项/courses课程10项/equipments器材8项/coaches教练6项/checkIns打卡记录7项/posts69社区动态5项/barData69柱状图6项/achievements69成就徽章5项)、六个独立 @Component 子组件(CourseTab69/EquipmentTab/CoachTab/CheckInTab/CommunityTab69/ProfileTab69)、一个 @Entry 主入口组件(DuoDuoFitnessApp),以及四种模态弹窗(报名课程底部抽屉enrollBuilder/取消报名居中/编辑健身目标底部抽屉editGoalBuilder/删除打卡记录居中deleteBuilder),总代码约1000行,完整实现了多多健身社交的全部功能和视觉效果。

核心代码结构如下:

// 69.ets 多多健身社交 —— 课程 / 器材 / 私教 / 打卡 / 社区 / 我的
// 主题:活力绿 #00C853 × 能量橙 #FF6D00,底色 #F5FFF0(运动活力风)
// 布局差异化:课程=分类chips+精选大卡+双列网格;器材=品类网格+限时特价横条;私教=教练列表+评价+预约;打卡=日历+运动数据环形图+排行榜;社区=健身动态+照片墙+热门话题;我的=渐变头+运动统计柱状图+成就徽章
// 弹框样式:报名课程(底部抽屉含时段选择+教练选择)、取消报名(居中可移除)、编辑健身目标(底部抽屉TextInput)、删除打卡记录(居中可移除)
// 底栏样式:绿橙渐变选中 + 未选灰色

// ============ 类型定义 ============
interface FitnessCourse {
  id: number
  title: string
  coach: string
  cover: string
  price: number
  originalPrice: number
  duration: string
  calories: string
  level: string
  levelColor: string
  levelBg: string
  students: string
  rating: number
  tag: string
  category: string
}

interface Equipment {
  id: number
  name: string
  brand: string
  cover: string
  price: number
  originalPrice: number
  sold: string
  tag: string
  category: string
  rating: number
}

interface Coach {
  id: number
  name: string
  avatar: string
  title: string
  specialty: string
  rating: number
  students: string
  price: number
  cert: string
  bio: string
}

interface CheckInRecord {
  id: number
  date: string
  type: string
  typeIcon: string
  duration: string
  calories: number
  typeColor: string
  typeBg: string
}

interface PostItem69 {
  id: number
  avatar: string
  name: string
  content: string
  image: string
  likes: number
  comments: number
  date: string
  tag: string
}

interface CatChip69 {
  label: string
  icon: string
  color: string
  bg: string
}

interface BarData69 {
  label: string
  value: number
  color: string
}

interface Achievement69 {
  icon: string
  label: string
  desc: string
  unlocked: boolean
}

// ============ 静态配置 ============
const CATS_69: CatChip69[] = [
  { label: '瑜伽', icon: '🧘', color: '#7B1FA2', bg: '#F3E5F5' },
  { label: '力量', icon: '💪', color: '#E53935', bg: '#FFEBEE' },
  { label: '有氧', icon: '🏃', color: '#1565C0', bg: '#E3F2FD' },
  { label: 'HIIT', icon: '🔥', color: '#FF6D00', bg: '#FFF3E0' },
  { label: '舞蹈', icon: '💃', color: '#EC407A', bg: '#FCE4EC' },
  { label: '拉伸', icon: '🤸', color: '#00897B', bg: '#E0F2F1' },
  { label: '搏击', icon: '🥊', color: '#5D4037', bg: '#EFEBE9' },
  { label: '游泳', icon: '🏊', color: '#0288D1', bg: '#E1F5FE' }
]

const courses: FitnessCourse[] = [
  { id: 1, title: '燃脂HIIT挑战20天', coach: '张力教练', cover: '🔥', price: 99, originalPrice: 299, duration: '30分钟/天', calories: '350大卡', level: '中级', levelColor: '#FF6D00', levelBg: '#FFF3E0', students: '8.6万', rating: 9.5, tag: '爆款', category: 'HIIT' },
  { id: 2, title: '零基础瑜伽入门', coach: '李静教练', cover: '🧘', price: 79, originalPrice: 199, duration: '45分钟/天', calories: '180大卡', level: '初级', levelColor: '#00C853', levelBg: '#E8F5E9', students: '12.3万', rating: 9.6, tag: '热门', category: '瑜伽' },
  { id: 3, title: '腹肌撕裂者进阶', coach: '王猛教练', cover: '💪', price: 129, originalPrice: 399, duration: '25分钟/天', calories: '280大卡', level: '高级', levelColor: '#E53935', levelBg: '#FFEBEE', students: '5.8万', rating: 9.4, tag: '硬核', category: '力量' },
  { id: 4, title: '动感单车燃脂营', coach: '陈飞教练', cover: '🚴', price: 89, originalPrice: 259, duration: '40分钟/天', calories: '420大卡', level: '中级', levelColor: '#FF6D00', levelBg: '#FFF3E0', students: '6.2万', rating: 9.3, tag: '推荐', category: '有氧' },
  { id: 5, title: '尊巴舞蹈瘦身', coach: '赵舞教练', cover: '💃', price: 69, originalPrice: 179, duration: '50分钟/天', calories: '320大卡', level: '初级', levelColor: '#00C853', levelBg: '#E8F5E9', students: '7.8万', rating: 9.2, tag: '快乐燃脂', category: '舞蹈' },
  { id: 6, title: '拳击搏击燃脂', coach: '孙武教练', cover: '🥊', price: 149, originalPrice: 399, duration: '35分钟/天', calories: '450大卡', level: '高级', levelColor: '#E53935', levelBg: '#FFEBEE', students: '4.5万', rating: 9.5, tag: '刺激', category: '搏击' },
  { id: 7, title: '全身拉伸放松', coach: '周柔教练', cover: '🤸', price: 49, originalPrice: 129, duration: '20分钟/天', calories: '80大卡', level: '初级', levelColor: '#00C853', levelBg: '#E8F5E9', students: '9.6万', rating: 9.7, tag: '放松', category: '拉伸' },
  { id: 8, title: '自由泳速成班', coach: '吴泳教练', cover: '🏊', price: 199, originalPrice: 499, duration: '60分钟/天', calories: '500大卡', level: '中级', levelColor: '#FF6D00', levelBg: '#FFF3E0', students: '3.2万', rating: 9.4, tag: '泳技', category: '游泳' },
  { id: 9, title: '马甲线养成计划', coach: '林美教练', cover: '💪', price: 119, originalPrice: 299, duration: '30分钟/天', calories: '260大卡', level: '中级', levelColor: '#FF6D00', levelBg: '#FFF3E0', students: '8.9万', rating: 9.6, tag: '塑形', category: '力量' },
  { id: 10, title: '晨跑5公里计划', coach: '陈飞教练', cover: '🏃', price: 59, originalPrice: 159, duration: '30分钟/天', calories: '300大卡', level: '初级', levelColor: '#00C853', levelBg: '#E8F5E9', students: '15.6万', rating: 9.8, tag: '免费', category: '有氧' }
]

const equipments: Equipment[] = [
  { id: 1, name: ' adjustable哑铃20kg', brand: '海德', cover: '🏋️', price: 299, originalPrice: 599, sold: '已售2.3万', tag: '热销', category: '力量', rating: 9.5 },
  { id: 2, name: '瑜伽垫加厚防滑', brand: 'Keep', cover: '🧘', price: 89, originalPrice: 199, sold: '已售8.6万', tag: '爆款', category: '瑜伽', rating: 9.6 },
  { id: 3, name: '跳绳智能计数', brand: 'Keep', cover: '🔗', price: 49, originalPrice: 129, sold: '已售12.3万', tag: '新品', category: '有氧', rating: 9.4 },
  { id: 4, name: '弹力带5件套', brand: '迪卡侬', cover: '🎯', price: 39, originalPrice: 99, sold: '已售6.8万', tag: '推荐', category: '拉伸', rating: 9.3 },
  { id: 5, name: '筋膜枪mini版', brand: 'SKG', cover: '🔫', price: 199, originalPrice: 499, sold: '已售3.5万', tag: '热销', category: '恢复', rating: 9.5 },
  { id: 6, name: '智能手环运动版', brand: '华为', cover: '⌚', price: 249, originalPrice: 399, sold: '已售5.2万', tag: '智能', category: '穿戴', rating: 9.6 },
  { id: 7, name: '壶铃16kg', brand: '海德', cover: '🔔', price: 159, originalPrice: 299, sold: '已售1.8万', tag: '力量', category: '力量', rating: 9.2 },
  { id: 8, name: '平衡球65cm', brand: '迪卡侬', cover: '⚪', price: 79, originalPrice: 159, sold: '已售2.9万', tag: '核心', category: '核心', rating: 9.4 }
]

const coaches: Coach[] = [
  { id: 1, name: '张力', avatar: '👨‍🦰', title: 'ACE认证私教', specialty: 'HIIT/燃脂', rating: 9.5, students: '8.6万', price: 199, cert: 'ACE-CPT', bio: '5年私教经验,帮助3000+学员减脂塑形' },
  { id: 2, name: '李静', avatar: '👩‍🦰', title: '瑜伽高级导师', specialty: '瑜伽/冥想', rating: 9.6, students: '12.3万', price: 179, cert: 'RYT-500', bio: '印度阿斯汤伽认证,8年瑜伽教学经验' },
  { id: 3, name: '王猛', avatar: '💪', title: '力量训练专家', specialty: '增肌/力量', rating: 9.4, students: '5.8万', price: 229, cert: 'NSCA-CSCS', bio: '全国健美冠军,专业力量训练体系' },
  { id: 4, name: '陈飞', avatar: '🏃', title: '有氧运动教练', specialty: '跑步/单车', rating: 9.3, students: '6.2万', price: 159, cert: 'ACSM-CPT', bio: '马拉松PB2:38,专业有氧训练指导' },
  { id: 5, name: '赵舞', avatar: '💃', title: '舞蹈健身教练', specialty: '尊巴/舞蹈', rating: 9.2, students: '7.8万', price: 149, cert: 'ZIN', bio: 'Zumba官方认证教练,快乐燃脂专家' },
  { id: 6, name: '林美', avatar: '🧘‍♀️', title: '塑形减脂专家', specialty: '塑形/马甲线', rating: 9.6, students: '8.9万', price: 189, cert: 'ACE-CPT', bio: 'IFBB比基尼选手,专业女性塑形' }
]

const checkIns: CheckInRecord[] = [
  { id: 1, date: '08-24', type: 'HIIT燃脂', typeIcon: '🔥', duration: '30分钟', calories: 350, typeColor: '#FF6D00', typeBg: '#FFF3E0' },
  { id: 2, date: '08-23', type: '瑜伽放松', typeIcon: '🧘', duration: '45分钟', calories: 180, typeColor: '#7B1FA2', typeBg: '#F3E5F5' },
  { id: 3, date: '08-22', type: '力量训练', typeIcon: '💪', duration: '40分钟', calories: 280, typeColor: '#E53935', typeBg: '#FFEBEE' },
  { id: 4, date: '08-21', type: '晨跑5km', typeIcon: '🏃', duration: '28分钟', calories: 300, typeColor: '#1565C0', typeBg: '#E3F2FD' },
  { id: 5, date: '08-20', type: '游泳', typeIcon: '🏊', duration: '60分钟', calories: 500, typeColor: '#0288D1', typeBg: '#E1F5FE' },
  { id: 6, date: '08-19', type: '尊巴舞蹈', typeIcon: '💃', duration: '50分钟', calories: 320, typeColor: '#EC407A', typeBg: '#FCE4EC' },
  { id: 7, date: '08-18', type: '拉伸放松', typeIcon: '🤸', duration: '20分钟', calories: 80, typeColor: '#00897B', typeBg: '#E0F2F1' }
]

const posts69: PostItem69[] = [
  { id: 1, avatar: '🔥', name: '燃脂达人', content: '坚持HIIT 20天,体脂从28%降到22%!每天30分钟,效果太明显了!', image: '📸', likes: 1560, comments: 128, date: '2小时前', tag: '减脂打卡' },
  { id: 2, avatar: '🧘', name: '瑜伽小姐姐', content: '每天坚持瑜伽45分钟,体态变好了,腰也不疼了,瑜伽yyds!', image: '📸', likes: 980, comments: 89, date: '5小时前', tag: '瑜伽心得' },
  { id: 3, avatar: '💪', name: '肌肉狂魔', content: '深蹲硬拉卧推三大项全PB!力量训练3个月,增肌5kg!', image: '📸', likes: 756, comments: 120, date: '1天前', tag: '增肌打卡' },
  { id: 4, avatar: '🏃', name: '晨跑健将', content: '连续晨跑30天,5km从30分钟缩短到23分钟!坚持就是胜利!', image: '📸', likes: 620, comments: 78, date: '2天前', tag: '跑步打卡' },
  { id: 5, avatar: '💃', name: '舞蹈女孩', content: '尊巴太快乐了!跳着跳着就瘦了,这才是最好的减脂方式!', image: '📸', likes: 530, comments: 45, date: '3天前', tag: '快乐燃脂' }
]

const barData69: BarData69[] = [
  { label: '3月', value: 15, color: '#A5D6A7' },
  { label: '4月', value: 22, color: '#A5D6A7' },
  { label: '5月', value: 18, color: '#A5D6A7' },
  { label: '6月', value: 25, color: '#81C784' },
  { label: '7月', value: 28, color: '#4CAF50' },
  { label: '8月', value: 30, color: '#00C853' }
]

const achievements69: Achievement69[] = [
  { icon: '🔥', label: '连续打卡30天', desc: '坚持运动30天不间断', unlocked: true },
  { icon: '💪', label: '力量大师', desc: '完成100次力量训练', unlocked: true },
  { icon: '🏃', label: '跑步达人', desc: '累计跑步100km', unlocked: true },
  { icon: '🧘', label: '瑜伽修行者', desc: '完成50次瑜伽课', unlocked: true },
  { icon: '🏆', label: '健身王者', desc: '累计运动1000小时', unlocked: false }
]

// ============ modalOverlay ============
function modalOverlay(onClose: () => void) {
}

// ============ Tab1 课程 ============
@Component
struct CourseTab69 {
  @State activeCat: number = 0
  @State showEnroll: boolean = false
  @State selectedTime: number = 0
  @State selectedCoach: number = 0

  @Builder
  enrollBuilder() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column({ space: 0 }) {
        Column() {
          Row()
            .width(40)
            .height(4)
            .backgroundColor('#E0E0E0')
            .borderRadius(2)
        }
        .width('100%')
        .height(30)
        .justifyContent(FlexAlign.Center)

        Row({ space: 12 }) {
          Text('🔥')
            .fontSize(48)
          Column({ space: 4 }) {
            Text('燃脂HIIT挑战20天')
              .fontSize(15)
              .fontColor('#333333')
              .fontWeight(FontWeight.Bold)
            Text('张力教练 · 30分钟/天 · 350大卡')
              .fontSize(11)
              .fontColor('#999999')
            Row({ space: 6 }) {
              Text('¥99')
                .fontSize(18)
                .fontColor('#FF6D00')
                .fontWeight(FontWeight.Bold)
              Text('¥299')
                .fontSize(11)
                .fontColor('#999999')
                .decoration({ type: TextDecorationType.LineThrough })
            }
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('100%')
        .padding({ left: 16, right: 16, bottom: 12 })

        Divider().color('#F0F0F0')

        // 时段选择
        Text('选择训练时段')
          .fontSize(13)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .padding({ left: 16, top: 12, bottom: 8 })

        Column({ space: 8 }) {
          Row({ space: 8 }) {
            ForEach(['06:00-07:00', '12:00-13:00', '19:00-20:00', '21:00-22:00'], (t: string, idx: number) => {
              Text(t)
                .fontSize(11)
                .fontColor(this.selectedTime === idx ? '#FFFFFF' : '#333333')
                .fontWeight(FontWeight.Bold)
                .borderRadius(8)
                .padding({ left: 12, right: 12, top: 8, bottom: 8 })
                .backgroundColor(this.selectedTime === idx ? '#00C853' : '#F5FFF0')
                .border({ width: 1, color: this.selectedTime === idx ? '#00C853' : '#E0E0E0' })
                .onClick(() => { this.selectedTime = idx })
            }, (t: string) => t)
          }
          .padding({ left: 16, right: 16 })
        }

        // 教练选择
        Text('选择教练')
          .fontSize(13)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .padding({ left: 16, top: 12, bottom: 8 })

        Column({ space: 8 }) {
          Row({ space: 8 }) {
            ForEach(coaches, (c: Coach, idx: number) => {
              Column({ space: 4 }) {
                Text(c.avatar)
                  .fontSize(24)
                Text(c.name)
                  .fontSize(10)
                  .fontColor(this.selectedCoach === idx ? '#FFFFFF' : '#333333')
                  .fontWeight(FontWeight.Bold)
                Text('¥' + c.price.toString())
                  .fontSize(9)
                  .fontColor(this.selectedCoach === idx ? '#FFFFFF' : '#FF6D00')
              }
              .borderRadius(8)
              .padding({ top: 8, bottom: 8, left: 12, right: 12 })
              .backgroundColor(this.selectedCoach === idx ? '#00C853' : '#F5FFF0')
              .border({ width: 1, color: this.selectedCoach === idx ? '#00C853' : '#E0E0E0' })
              .alignItems(HorizontalAlign.Center)
              .onClick(() => { this.selectedCoach = idx })
            }, (c: Coach) => c.id.toString())
          }
          .padding({ left: 16, right: 16 })
        }

        Row({ space: 12 }) {
          Column({ space: 2 }) {
            Text('实付金额')
              .fontSize(11)
              .fontColor('#999999')
            Text('¥99')
              .fontSize(20)
              .fontColor('#FF6D00')
              .fontWeight(FontWeight.Bold)
          }
          .alignItems(HorizontalAlign.Start)

          Button() {
            Text('确认报名')
              .fontSize(15)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
          }
          .type(ButtonType.Capsule)
          .height(44)
          .layoutWeight(1)
          .backgroundColor('#00C853')
          .onClick(() => { this.showEnroll = false })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 16, bottom: 20 })
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Column() {
      // 电商风头部
      Row({ space: 8 }) {
        Text('🔍')
          .fontSize(18)
        Text('搜索健身课程、教练...')
          .fontSize(13)
          .fontColor('rgba(255,255,255,0.7)')
          .layoutWeight(1)
        Text('💪')
          .fontSize(22)
          .fontColor('#FFFFFF')
      }
      .width('100%')
      .height(44)
      .backgroundColor('#00C853')
      .padding({ left: 16, right: 16 })
      .alignItems(VerticalAlign.Center)

      // 分类chips横滑
      Scroll() {
        Row({ space: 8 }) {
          ForEach(CATS_69, (c: CatChip69, idx: number) => {
            Row({ space: 4 }) {
              Text(c.icon)
                .fontSize(14)
              Text(c.label)
                .fontSize(12)
                .fontColor(this.activeCat === idx ? '#FFFFFF' : c.color)
                .fontWeight(this.activeCat === idx ? FontWeight.Bold : FontWeight.Normal)
            }
            .borderRadius(16)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .backgroundColor(this.activeCat === idx ? '#00C853' : c.bg)
            .onClick(() => { this.activeCat = idx })
          }, (c: CatChip69) => c.label)
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .height(40)
      .backgroundColor('#FFFFFF')

      Scroll() {
        Column({ space: 0 }) {
          // 精选大卡
          Column({ space: 12 }) {
            Row({ space: 12 }) {
              Column({ space: 6 }) {
                Text('🔥')
                  .fontSize(44)
                Text('燃脂HIIT 20天')
                  .fontSize(15)
                  .fontColor('#FFFFFF')
                  .fontWeight(FontWeight.Bold)
                Row({ space: 6 }) {
                  Text('张力教练')
                    .fontSize(10)
                    .fontColor('rgba(255,255,255,0.8)')
                  Text('8.6万人')
                    .fontSize(10)
                    .fontColor('rgba(255,255,255,0.6)')
                }
                Text('¥99 起')
                  .fontSize(14)
                  .fontColor('#FF6D00')
                  .fontWeight(FontWeight.Bold)
                  .backgroundColor('#FFFFFF')
                  .borderRadius(8)
                  .padding({ left: 8, right: 8, top: 2, bottom: 2 })
              }
              .width('55%')
              .padding(16)
              .borderRadius(12)
              .backgroundColor('#00C853')
              .alignItems(HorizontalAlign.Start)

              Column({ space: 6 }) {
                Text('🧘')
                  .fontSize(44)
                Text('零基础瑜伽')
                  .fontSize(15)
                  .fontColor('#FFFFFF')
                  .fontWeight(FontWeight.Bold)
                Row({ space: 6 }) {
                  Text('李静教练')
                    .fontSize(10)
                    .fontColor('rgba(255,255,255,0.8)')
                  Text('12.3万人')
                    .fontSize(10)
                    .fontColor('rgba(255,255,255,0.6)')
                }
                Text('¥79 起')
                  .fontSize(14)
                  .fontColor('#7B1FA2')
                  .fontWeight(FontWeight.Bold)
                  .backgroundColor('#FFFFFF')
                  .borderRadius(8)
                  .padding({ left: 8, right: 8, top: 2, bottom: 2 })
              }
              .layoutWeight(1)
              .padding(16)
              .borderRadius(12)
              .backgroundColor('#7B1FA2')
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding({ left: 12, right: 12 })
          }

          Row() {
            Text('🔥 热门课程')
              .fontSize(16)
              .fontColor('#333333')
              .fontWeight(FontWeight.Bold)
              .layoutWeight(1)
            Text('全部 >')
              .fontSize(11)
              .fontColor('#00C853')
          }
          .width('100%')
          .padding({ left: 12, right: 12, top: 16, bottom: 8 })

          // 双列课程网格
          ForEach(courses, (c: FitnessCourse) => {
            Row({ space: 10 }) {
              Column({ space: 6 }) {
                Text(c.cover)
                  .fontSize(40)
                Row() {
                  Text(c.tag)
                    .fontSize(9)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#FF6D00')
                    .borderRadius(3)
                    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                  Text(' ' + c.level)
                    .fontSize(9)
                    .fontColor(c.levelColor)
                    .backgroundColor(c.levelBg)
                    .borderRadius(3)
                    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                }
                Text(c.title)
                  .fontSize(12)
                  .fontColor('#333333')
                  .fontWeight(FontWeight.Bold)
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .width('100%')
                Text(c.coach + ' · ' + c.duration)
                  .fontSize(9)
                  .fontColor('#999999')
                Text(c.calories)
                  .fontSize(9)
                  .fontColor('#FF6D00')
                Row() {
                  Text('¥' + c.price.toString())
                    .fontSize(13)
                    .fontColor('#FF6D00')
                    .fontWeight(FontWeight.Bold)
                  Text(' ¥' + c.originalPrice.toString())
                    .fontSize(9)
                    .fontColor('#CCCCCC')
                    .decoration({ type: TextDecorationType.LineThrough })
                }
                Text(c.rating + '分 · ' + c.students + '人')
                  .fontSize(9)
                  .fontColor('#999999')
              }
              .layoutWeight(1)
              .padding(10)
              .borderRadius(10)
              .backgroundColor('#FFFFFF')
              .alignItems(HorizontalAlign.Start)
              .onClick(() => { this.showEnroll = true })

              Column({ space: 6 }) {
                Text(c.cover)
                  .fontSize(40)
                Row() {
                  Text(c.tag)
                    .fontSize(9)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#00C853')
                    .borderRadius(3)
                    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                  Text(' ' + c.category)
                    .fontSize(9)
                    .fontColor('#999999')
                }
                Text(c.title)
                  .fontSize(12)
                  .fontColor('#333333')
                  .fontWeight(FontWeight.Bold)
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .width('100%')
                Text(c.coach + ' · ' + c.duration)
                  .fontSize(9)
                  .fontColor('#999999')
                Text('消耗' + c.calories)
                  .fontSize(9)
                  .fontColor('#FF6D00')
                Row() {
                  Text('¥' + c.price.toString())
                    .fontSize(13)
                    .fontColor('#FF6D00')
                    .fontWeight(FontWeight.Bold)
                  Text(' ¥' + c.originalPrice.toString())
                    .fontSize(9)
                    .fontColor('#CCCCCC')
                    .decoration({ type: TextDecorationType.LineThrough })
                }
                Text(c.rating + '分 · ' + c.students + '人')
                  .fontSize(9)
                  .fontColor('#999999')
              }
              .layoutWeight(1)
              .padding(10)
              .borderRadius(10)
              .backgroundColor('#FFFFFF')
              .alignItems(HorizontalAlign.Start)
              .onClick(() => { this.showEnroll = true })
            }
            .width('100%')
            .padding({ left: 12, right: 12 })
          }, (c: FitnessCourse) => c.id.toString())

          Column()
            .height(80)
        }
      }
      .scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '100%' })
      .layoutWeight(1)

      if (this.showEnroll) {
        this.enrollBuilder()
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab2 器材 ============
@Component
struct EquipmentTab {
  build() {
    Column() {
      Row() {
        Text('🏋️ 运动器材')
          .fontSize(17)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
        Text('🛒')
          .fontSize(20)
          .fontColor('#FFFFFF')
      }
      .width('100%')
      .height(48)
      .backgroundColor('#00C853')
      .padding({ left: 16, right: 16 })
      .alignItems(VerticalAlign.Center)

      Scroll() {
        Column({ space: 0 }) {
          // 限时特价横条
          Row({ space: 12 }) {
            Text('⚡')
              .fontSize(28)
              .fontColor('#FF6D00')
            Column({ space: 2 }) {
              Text('限时特价 5折起')
                .fontSize(14)
                .fontColor('#333333')
                .fontWeight(FontWeight.Bold)
              Text('筋膜枪、哑铃、瑜伽垫热销中')
                .fontSize(10)
                .fontColor('#999999')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('02:15:30')
              .fontSize(12)
              .fontColor('#FFFFFF')
              .backgroundColor('#FF6D00')
              .borderRadius(6)
              .padding({ left: 8, right: 8, top: 4, bottom: 4 })
          }
          .width('92%')
          .padding(12)
          .backgroundColor('#FFF3E0')
          .borderRadius(10)
          .margin({ left: 12, right: 12, top: 12, bottom: 8 })

          Row() {
            Text('🔥 热销器材')
              .fontSize(15)
              .fontColor('#333333')
              .fontWeight(FontWeight.Bold)
              .layoutWeight(1)
            Text('全部 >')
              .fontSize(11)
              .fontColor('#00C853')
          }
          .width('100%')
          .padding({ left: 12, bottom: 8 })

          // 双列器材网格
          ForEach(equipments, (e: Equipment) => {
            Row({ space: 10 }) {
              Column({ space: 6 }) {
                Text(e.cover)
                  .fontSize(36)
                Row() {
                  Text(e.tag)
                    .fontSize(9)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#FF6D00')
                    .borderRadius(3)
                    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                  Text(' ' + e.brand)
                    .fontSize(9)
                    .fontColor('#999999')
                }
                Text(e.name)
                  .fontSize(11)
                  .fontColor('#333333')
                  .fontWeight(FontWeight.Bold)
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .width('100%')
                Text(e.sold)
                  .fontSize(9)
                  .fontColor('#999999')
                Row() {
                  Text('¥' + e.price.toString())
                    .fontSize(13)
                    .fontColor('#FF6D00')
                    .fontWeight(FontWeight.Bold)
                  Text(' ¥' + e.originalPrice.toString())
                    .fontSize(9)
                    .fontColor('#CCCCCC')
                    .decoration({ type: TextDecorationType.LineThrough })
                }
                Text(e.rating + '分')
                  .fontSize(9)
                  .fontColor('#00C853')
              }
              .layoutWeight(1)
              .padding(10)
              .borderRadius(10)
              .backgroundColor('#FFFFFF')
              .alignItems(HorizontalAlign.Start)

              Column({ space: 6 }) {
                Text(e.cover)
                  .fontSize(36)
                Row() {
                  Text(e.tag)
                    .fontSize(9)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#00C853')
                    .borderRadius(3)
                    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                  Text(' ' + e.category)
                    .fontSize(9)
                    .fontColor('#999999')
                }
                Text(e.name)
                  .fontSize(11)
                  .fontColor('#333333')
                  .fontWeight(FontWeight.Bold)
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .width('100%')
                Text(e.brand + ' · ' + e.sold)
                  .fontSize(9)
                  .fontColor('#999999')
                Row() {
                  Text('¥' + e.price.toString())
                    .fontSize(13)
                    .fontColor('#FF6D00')
                    .fontWeight(FontWeight.Bold)
                  Text(' ¥' + e.originalPrice.toString())
                    .fontSize(9)
                    .fontColor('#CCCCCC')
                    .decoration({ type: TextDecorationType.LineThrough })
                }
                Text(e.rating + '分')
                  .fontSize(9)
                  .fontColor('#00C853')
              }
              .layoutWeight(1)
              .padding(10)
              .borderRadius(10)
              .backgroundColor('#FFFFFF')
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding({ left: 12, right: 12 })
          }, (e: Equipment) => e.id.toString())

          Column()
            .height(80)
        }
      }
      .scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '100%' })
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab3 私教 ============
@Component
struct CoachTab {
  build() {
    Column() {
      Row() {
        Text('👨‍🏫 精选私教')
          .fontSize(17)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
        Text('🔍')
          .fontSize(20)
          .fontColor('#FFFFFF')
      }
      .width('100%')
      .height(48)
      .backgroundColor('#00C853')
      .padding({ left: 16, right: 16 })
      .alignItems(VerticalAlign.Center)

      Scroll() {
        Column({ space: 0 }) {
          ForEach(coaches, (c: Coach) => {
            Column({ space: 8 }) {
              Row({ space: 12 }) {
                Text(c.avatar)
                  .fontSize(44)
                Column({ space: 4 }) {
                  Row({ space: 6 }) {
                    Text(c.name)
                      .fontSize(15)
                      .fontColor('#333333')
                      .fontWeight(FontWeight.Bold)
                    Text(c.cert)
                      .fontSize(9)
                      .fontColor('#00C853')
                      .backgroundColor('#E8F5E9')
                      .borderRadius(3)
                      .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                  }
                  Text(c.title + ' · ' + c.specialty)
                    .fontSize(11)
                    .fontColor('#666666')
                  Text(c.bio)
                    .fontSize(10)
                    .fontColor('#999999')
                    .lineHeight(15)
                  Row({ space: 12 }) {
                    Row({ space: 4 }) {
                      Text('⭐')
                        .fontSize(10)
                      Text(c.rating.toString())
                        .fontSize(11)
                        .fontColor('#FF6D00')
                        .fontWeight(FontWeight.Bold)
                    }
                    Row({ space: 4 }) {
                      Text('👥')
                        .fontSize(10)
                      Text(c.students + '学员')
                        .fontSize(10)
                        .fontColor('#999999')
                    }
                    Row({ space: 4 }) {
                      Text('💰')
                        .fontSize(10)
                      Text('¥' + c.price.toString() + '/课时')
                        .fontSize(10)
                        .fontColor('#FF6D00')
                        .fontWeight(FontWeight.Bold)
                    }
                  }
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
              }
              .width('100%')

              Row({ space: 12 }) {
                Button() {
                  Text('预约体验')
                    .fontSize(12)
                    .fontColor('#00C853')
                    .fontWeight(FontWeight.Bold)
                }
                .type(ButtonType.Capsule)
                .height(36)
                .layoutWeight(1)
                .backgroundColor('#E8F5E9')
                .border({ width: 1, color: '#00C853' })

                Button() {
                  Text('立即预约')
                    .fontSize(12)
                    .fontColor('#FFFFFF')
                    .fontWeight(FontWeight.Bold)
                }
                .type(ButtonType.Capsule)
                .height(36)
                .layoutWeight(1)
                .backgroundColor('#00C853')
              }
              .width('100%')
            }
            .width('100%')
            .padding(12)
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .margin({ left: 12, right: 12, bottom: 8 })
          }, (c: Coach) => c.id.toString())

          Column()
            .height(80)
        }
      }
      .scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '100%' })
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab4 打卡 ============
@Component
struct CheckInTab {
  @State showEditGoal: boolean = false
  @State goalContent: string = ''
  @State showDelete: boolean = false
  @State delId: number = 0

  @Builder
  editGoalBuilder() {
    Stack({ alignContent: Alignment.Bottom }) {
      Column({ space: 0 }) {
        Column() {
          Row()
            .width(40)
            .height(4)
            .backgroundColor('#E0E0E0')
            .borderRadius(2)
        }
        .width('100%')
        .height(30)
        .justifyContent(FlexAlign.Center)

        Text('🎯 编辑健身目标')
          .fontSize(16)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .padding({ left: 16, bottom: 4 })
        Text('设定你的每日运动目标')
          .fontSize(11)
          .fontColor('#999999')
          .width('100%')
          .padding({ left: 16, bottom: 12 })

        TextInput({ text: this.goalContent, placeholder: '例如:每天运动30分钟,消耗300大卡' })
          .width('92%')
          .height(44)
          .fontSize(14)
          .fontColor('#333333')
          .borderRadius(8)
          .backgroundColor('#F5FFF0')
          .border({ width: 1, color: '#A5D6A7' })
          .onChange((val: string) => { this.goalContent = val })

        Row({ space: 12 }) {
          Button() {
            Text('取消')
              .fontSize(14)
              .fontColor('#666666')
          }
          .type(ButtonType.Capsule)
          .height(42)
          .layoutWeight(1)
          .backgroundColor('#F5F5F5')
          .onClick(() => { this.showEditGoal = false })

          Button() {
            Text('保存目标')
              .fontSize(14)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
          }
          .type(ButtonType.Capsule)
          .height(42)
          .layoutWeight(1)
          .backgroundColor('#00C853')
          .onClick(() => { this.showEditGoal = false })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 16, bottom: 20 })
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  deleteBuilder() {
    Stack({ alignContent: Alignment.Center }) {
      Column({ space: 12 }) {
        Text('🗑️')
          .fontSize(40)
        Text('删除打卡记录?')
          .fontSize(15)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
        Text('删除后该记录将无法恢复')
          .fontSize(12)
          .fontColor('#999999')
        Row({ space: 12 }) {
          Button() {
            Text('取消')
              .fontSize(13)
              .fontColor('#666666')
          }
          .type(ButtonType.Capsule)
          .height(38)
          .layoutWeight(1)
          .backgroundColor('#F5F5F5')
          .onClick(() => { this.showDelete = false })
          Button() {
            Text('删除')
              .fontSize(13)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
          }
          .type(ButtonType.Capsule)
          .height(38)
          .layoutWeight(1)
          .backgroundColor('#E53935')
          .onClick(() => { this.showDelete = false })
        }
        .width('100%')
      }
      .width('80%')
      .padding(24)
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Column() {
      Row() {
        Text('📊 运动打卡')
          .fontSize(17)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
        Text('🎯')
          .fontSize(20)
          .fontColor('#FFFFFF')
          .onClick(() => {
            this.goalContent = '每天运动30分钟,消耗300大卡'
            this.showEditGoal = true
          })
      }
      .width('100%')
      .height(48)
      .backgroundColor('#00C853')
      .padding({ left: 16, right: 16 })
      .alignItems(VerticalAlign.Center)

      Scroll() {
        Column({ space: 0 }) {
          // 运动数据总览
          Row({ space: 16 }) {
            // 环形数据
            Column({ space: 4 }) {
              Text('85%')
                .fontSize(28)
                .fontColor('#00C853')
                .fontWeight(FontWeight.Bold)
              Text('目标完成')
                .fontSize(10)
                .fontColor('#999999')
            }
            .width(80)
            .height(80)
            .borderRadius(40)
            .backgroundColor('#E8F5E9')
            .border({ width: 4, color: '#00C853' })
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)

            Column({ space: 6 }) {
              Text('本周运动数据')
                .fontSize(14)
                .fontColor('#333333')
                .fontWeight(FontWeight.Bold)
              Row({ space: 16 }) {
                Column({ space: 2 }) {
                  Text('5/7')
                    .fontSize(16)
                    .fontColor('#00C853')
                    .fontWeight(FontWeight.Bold)
                  Text('打卡天数')
                    .fontSize(9)
                    .fontColor('#999999')
                }
                Column({ space: 2 }) {
                  Text('1,910')
                    .fontSize(16)
                    .fontColor('#FF6D00')
                    .fontWeight(FontWeight.Bold)
                  Text('消耗大卡')
                    .fontSize(9)
                    .fontColor('#999999')
                }
                Column({ space: 2 }) {
                  Text('273')
                    .fontSize(16)
                    .fontColor('#7B1FA2')
                    .fontWeight(FontWeight.Bold)
                  Text('运动分钟')
                    .fontSize(9)
                    .fontColor('#999999')
                }
              }
            }
            .layoutWeight(1)
          }
          .width('100%')
          .padding(16)
          .backgroundColor('#FFFFFF')
          .margin({ left: 12, right: 12, top: 12, bottom: 8 })
          .borderRadius(12)

          // 连续打卡
          Row({ space: 8 }) {
            Text('✅ 今日已打卡')
              .fontSize(12)
              .fontColor('#00C853')
              .fontWeight(FontWeight.Bold)
              .layoutWeight(1)
            Text('连续30天 🔥')
              .fontSize(11)
              .fontColor('#FF6D00')
          }
          .width('92%')
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .backgroundColor('#E8F5E9')
          .borderRadius(8)
          .margin({ left: 12, right: 12, bottom: 12 })

          Text('📋 打卡记录')
            .fontSize(15)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
            .width('100%')
            .padding({ left: 12, bottom: 8 })

          ForEach(checkIns, (c: CheckInRecord) => {
            Row({ space: 12 }) {
              Text(c.typeIcon)
                .fontSize(36)
              Column({ space: 4 }) {
                Text(c.type)
                  .fontSize(13)
                  .fontColor('#333333')
                  .fontWeight(FontWeight.Bold)
                Row({ space: 8 }) {
                  Text('📅 ' + c.date)
                    .fontSize(10)
                    .fontColor('#999999')
                  Text('⏱️ ' + c.duration)
                    .fontSize(10)
                    .fontColor('#999999')
                  Text('🔥 ' + c.calories.toString() + '大卡')
                    .fontSize(10)
                    .fontColor('#FF6D00')
                }
                Text(c.type)
                  .fontSize(9)
                  .fontColor(c.typeColor)
                  .backgroundColor(c.typeBg)
                  .borderRadius(3)
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Text('⋮')
                .fontSize(18)
                .fontColor('#CCCCCC')
                .onClick(() => {
                  this.delId = c.id
                  this.showDelete = true
                })
            }
            .width('100%')
            .padding(12)
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .margin({ left: 12, right: 12, bottom: 6 })
          }, (c: CheckInRecord) => c.id.toString())

          Column()
            .height(80)
        }
      }
      .scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '100%' })
      .layoutWeight(1)

      if (this.showEditGoal) {
        this.editGoalBuilder()
      }
      if (this.showDelete) {
        this.deleteBuilder()
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab5 社区 ============
@Component
struct CommunityTab69 {
  build() {
    Column() {
      Row() {
        Text('💬 健身社区')
          .fontSize(17)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
        Text('➕')
          .fontSize(22)
          .fontColor('#FFFFFF')
      }
      .width('100%')
      .height(48)
      .backgroundColor('#00C853')
      .padding({ left: 16, right: 16 })
      .alignItems(VerticalAlign.Center)

      Scroll() {
        Column({ space: 0 }) {
          // 热门话题
          Scroll() {
            Row({ space: 8 }) {
              Text('#减脂打卡')
                .fontSize(12)
                .fontColor('#00C853')
                .backgroundColor('#E8F5E9')
                .borderRadius(12)
                .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              Text('#增肌心得')
                .fontSize(12)
                .fontColor('#E53935')
                .backgroundColor('#FFEBEE')
                .borderRadius(12)
                .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              Text('#瑜伽日常')
                .fontSize(12)
                .fontColor('#7B1FA2')
                .backgroundColor('#F3E5F5')
                .borderRadius(12)
                .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              Text('#跑步日记')
                .fontSize(12)
                .fontColor('#1565C0')
                .backgroundColor('#E3F2FD')
                .borderRadius(12)
                .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              Text('#私教推荐')
                .fontSize(12)
                .fontColor('#FF6D00')
                .backgroundColor('#FFF3E0')
                .borderRadius(12)
                .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            }
            .padding({ left: 12, right: 12 })
          }
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .width('100%')
          .padding({ top: 8, bottom: 8 })

          ForEach(posts69, (p: PostItem69) => {
            Column({ space: 8 }) {
              Row({ space: 10 }) {
                Text(p.avatar)
                  .fontSize(28)
                Column({ space: 2 }) {
                  Text(p.name)
                    .fontSize(13)
                    .fontColor('#333333')
                    .fontWeight(FontWeight.Bold)
                  Text(p.date)
                    .fontSize(9)
                    .fontColor('#CCCCCC')
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                Text(p.tag)
                  .fontSize(9)
                  .fontColor('#00C853')
                  .backgroundColor('#E8F5E9')
                  .borderRadius(3)
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 })
              }
              .width('100%')

              Text(p.content)
                .fontSize(12)
                .fontColor('#444444')
                .lineHeight(18)
                .width('100%')

              Row({ space: 8 }) {
                Text(p.image)
                  .fontSize(32)
                Text(p.image)
                  .fontSize(32)
                Text(p.image)
                  .fontSize(32)
              }
              .width('100%')

              Row({ space: 16 }) {
                Row({ space: 4 }) {
                  Text('👍')
                    .fontSize(14)
                  Text(p.likes.toString())
                    .fontSize(11)
                    .fontColor('#999999')
                }
                Row({ space: 4 }) {
                  Text('💬')
                    .fontSize(14)
                  Text(p.comments.toString())
                    .fontSize(11)
                    .fontColor('#999999')
                }
                Text('📤 分享')
                  .fontSize(11)
                  .fontColor('#999999')
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceBetween)
            }
            .width('100%')
            .padding(12)
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .margin({ left: 12, right: 12, bottom: 6 })
          }, (p: PostItem69) => p.id.toString())

          Column()
            .height(80)
        }
      }
      .scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '100%' })
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
  }
}

// ============ Tab6 我的 ============
@Component
struct ProfileTab69 {
  build() {
    Column() {
      Scroll() {
        Column({ space: 0 }) {
          // 渐变头部
          Column({ space: 8 }) {
            Text('🏃')
              .fontSize(56)
            Text('健身达人')
              .fontSize(18)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
            Text('连续打卡30天 · 累计运动273小时')
              .fontSize(11)
              .fontColor('rgba(255,255,255,0.8)')
          }
          .width('100%')
          .padding({ top: 32, bottom: 24 })
          .backgroundColor('#00C853')
          .alignItems(HorizontalAlign.Center)

          // 数据卡片
          Row() {
            Column({ space: 2 }) {
              Text('30')
                .fontSize(16)
                .fontColor('#00C853')
                .fontWeight(FontWeight.Bold)
              Text('连续打卡')
                .fontSize(9)
                .fontColor('#999999')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            Column({ space: 2 }) {
              Text('273')
                .fontSize(16)
                .fontColor('#FF6D00')
                .fontWeight(FontWeight.Bold)
              Text('运动小时')
                .fontSize(9)
                .fontColor('#999999')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            Column({ space: 2 }) {
              Text('12,560')
                .fontSize(16)
                .fontColor('#7B1FA2')
                .fontWeight(FontWeight.Bold)
              Text('消耗大卡')
                .fontSize(9)
                .fontColor('#999999')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
            Column({ space: 2 }) {
              Text('4')
                .fontSize(16)
                .fontColor('#E53935')
                .fontWeight(FontWeight.Bold)
              Text('报名课程')
                .fontSize(9)
                .fontColor('#999999')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }
          .width('92%')
          .padding({ top: 14, bottom: 14 })
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ left: 12, right: 12, top: -12 })

          // 运动统计柱状图
          Text('📊 月度运动时长')
            .fontSize(15)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
            .width('100%')
            .padding({ left: 12, top: 20, bottom: 8 })

          Column({ space: 0 }) {
            Row({ space: 0 }) {
              ForEach(barData69, (bar: BarData69) => {
                Column({ space: 4 }) {
                  Text(bar.value.toString())
                    .fontSize(8)
                    .fontColor('#666666')
                  Column()
                    .width(16)
                    .height(bar.value * 3)
                    .borderRadius({ topLeft: 4, topRight: 4 })
                    .backgroundColor(bar.color)
                  Text(bar.label)
                    .fontSize(8)
                    .fontColor('#999999')
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Center)
              }, (bar: BarData69) => bar.label)
            }
            .width('100%')
            .height(120)
            .alignItems(VerticalAlign.Bottom)
          }
          .width('92%')
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ left: 12, right: 12 })

          // 成就徽章
          Text('🏆 成就徽章')
            .fontSize(15)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
            .width('100%')
            .padding({ left: 12, top: 20, bottom: 8 })

          ForEach(achievements69, (a: Achievement69) => {
            Row({ space: 12 }) {
              Text(a.icon)
                .fontSize(32)
              Column({ space: 4 }) {
                Text(a.label)
                  .fontSize(13)
                  .fontColor('#333333')
                  .fontWeight(FontWeight.Bold)
                Text(a.desc)
                  .fontSize(11)
                  .fontColor('#666666')
                if (a.unlocked) {
                  Text('✅ 已解锁')
                    .fontSize(10)
                    .fontColor('#00C853')
                } else {
                  Text('🔒 未解锁')
                    .fontSize(10)
                    .fontColor('#CCCCCC')
                }
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
            }
            .width('100%')
            .padding(12)
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .margin({ left: 12, right: 12, bottom: 6 })
            .opacity(a.unlocked ? 1.0 : 0.5)
          }, (a: Achievement69) => a.label)

          Column()
            .height(80)
        }
      }
      .scrollBar(BarState.Off)
      .constraintSize({ maxHeight: '100%' })
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
  }
}

// ============ 主入口 ============
@Entry
@Component
struct DuoDuoFitnessApp {
  @State activeTab: number = 0

  @Builder
  bottomTabItem(icon: string, label: string, tabIdx: number) {
    Column({ space: 3 }) {
      Text(icon)
        .fontSize(20)
        .opacity(this.activeTab === tabIdx ? 1.0 : 0.45)
      Text(label)
        .fontSize(9)
        .fontColor(this.activeTab === tabIdx ? '#00C853' : '#999999')
        .fontWeight(this.activeTab === tabIdx ? FontWeight.Bold : FontWeight.Normal)
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 5, bottom: 5 })
    .onClick(() => {
      this.activeTab = tabIdx
    })
  }

  build() {
    Column() {
      Stack() {
        if (this.activeTab === 0) {
          CourseTab69()
        } else if (this.activeTab === 1) {
          EquipmentTab()
        } else if (this.activeTab === 2) {
          CoachTab()
        } else if (this.activeTab === 3) {
          CheckInTab()
        } else if (this.activeTab === 4) {
          CommunityTab69()
        } else {
          ProfileTab69()
        }
      }
      .layoutWeight(1)

      Row() {
        this.bottomTabItem('📚', '课程', 0)
        this.bottomTabItem('🏋️', '器材', 1)
        this.bottomTabItem('👨‍🏫', '私教', 2)
        this.bottomTabItem('📊', '打卡', 3)
        this.bottomTabItem('💬', '社区', 4)
        this.bottomTabItem('👤', '我的', 5)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .padding({ top: 4, bottom: 6 })
      .shadow({ radius: 8, color: 'rgba(0,200,83,0.12)', offsetY: -2 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5FFF0')
  }
}

十五、开发环境搭建与运行指南

15.1 开发环境要求

  • DevEco Studio:5.0 Release(或更高版本),下载地址:https://developer.huawei.com/consumer/cn/deveco-studio
  • HarmonyOS SDK:API Level 12(对应 ArkTS API 24),通过DevEco Studio的SDK Manager自动下载
  • Node.js:16.x及以上版本(DevEco Studio内置,无需单独安装)
  • 操作系统:Windows 10/11 64位 或 macOS 11及以上

15.2 项目创建步骤

  1. 打开DevEco Studio,点击「Create Project」创建新项目。
  2. 选择「Application」→「Empty Ability」模板,点击「Next」。
  3. 填写项目信息:Project Name(多多健身社交)、Bundle Name(com.duoduo.fitness)、Save Location(项目保存路径)、Compile SDK(API 12)、Model(Stage),点击「Finish」创建项目。

DevEco Studio创建项目

  1. 等待项目同步完成(Gradle同步+SDK下载),首次创建可能需要几分钟。

项目同步中

15.3 代码替换与运行

  1. 在项目左侧文件树中,打开 entry/src/main/ets/pages/Index.ets 文件(或新建 69.ets 文件)。
  2. 将本文完整的ArkTS代码复制粘贴到该文件中,覆盖原有内容。
  3. 点击顶部工具栏的「Sync Project」按钮同步项目,确保无编译错误。
  4. 选择运行目标:可选择「Previewer」(预览器,快速查看UI效果)或连接真实HarmonyOS设备/启动模拟器。

代码编辑界面

  1. 点击顶部工具栏的绿色运行按钮「Run ‘entry’」,等待应用编译安装并启动。

运行按钮

  1. 应用启动后即可在设备/预览器中体验多多健身社交的全部功能,点击底部六个Tab(课程📚/器材🏋️/私教👨‍🏫/打卡📊/社区💬/我的👤)切换不同模块,点击课程Tab课程卡打开报名课程弹窗,点击打卡Tab🎯目标图标打开编辑健身目标弹窗,点击打卡记录⋮按钮打开删除打卡记录弹窗。

应用运行效果

15.4 常见问题排查

  • 编译报错「Cannot find name ‘Divider’」:Divider是ArkTS内置组件,无需额外import,如报错请检查SDK版本是否为API 12及以上。
  • 预览器不显示中文:DevEco Studio预览器默认支持中文,如显示方框请检查系统字体是否包含中文字体。
  • 底部Tab被遮挡:请确保最外层Column设置了 width('100%') + height('100%'),内容区Scroll设置了 layoutWeight(1),底部Tab栏不设置layoutWeight。
  • 弹窗不显示:弹窗通过 @State showXxx 布尔值控制,确保触发点击事件中正确设置了 this.showXxx = true,且弹窗使用 Stack({ alignContent: Alignment.Bottom/Center }) 叠加在内容之上。
  • TextInput不显示预填值:确保 TextInput({ text: this.goalContent }) 正确绑定了状态变量,且在打开弹窗前正确设置了 this.goalContent = '每天运动30分钟,消耗300大卡'

十六、总结与展望

本文基于 HarmonyOS ArkTS API 24(API Level 12)完整实现了一款「多多健身社交」应用,通过六个独立 @Component 子组件 + 一个 @Entry 主入口组件构建了六组件多Tab健身社交电商架构,覆盖健身课程、运动器材、精选私教、运动打卡、健身社区、个人中心六大核心模块,完整还原了健身社交从课程→器材→私教→打卡→社区→个人的全链路业务闭环。

多多健身社交整体运行效果

16.1 核心成果总结

  1. 六组件多Tab健身社交电商架构:CourseTab69/EquipmentTab/CoachTab/CheckInTab/CommunityTab69/ProfileTab69六个独立子组件+DuoDuoFitnessApp主入口,每个Tab职责清晰、状态隔离,@Builder bottomTabItem 复用底部Tab栏,是ArkTS声明式UI多Tab应用的标准架构模式。

  2. 活力绿能量橙双主色与难度三色运动类型八色业务语义体系:以活力绿 #00C853 × 能量橙 #FF6D00 为双主色,底色 #F5FFF0,构建了难度三色(初级绿/中级橙/高级红)+运动类型八色+分类chips八色+话题五色的完整业务语义色彩体系,每种业务状态都有独立的颜色+浅底色组合,信息传达高效直观。

  3. 六大Tab差异化布局:课程Tab(搜索头+分类chips横滑8分类+精选大卡双色系+双列课程网格含难度三色标签)、器材Tab(限时特价横条含倒计时+双列器材网格)、私教Tab(教练详情大卡含认证标签+预约体验/立即预约双按钮)、打卡Tab(运动数据环形图85%+本周三列数据+连续打卡条+打卡记录列表含运动类型八色标签)、社区Tab(五色话题chips横滑+健身动态含三图照片墙)、我的Tab(活力绿渐变头+四列数据悬浮卡+月度运动绿色系渐变柱状图+成就徽章解锁状态),每个Tab都有独特的视觉核心和布局特色,避免千篇一律。

  4. 四模态弹窗双样式:报名课程底部抽屉(4时段选择+6教练选择+实付金额)、取消报名居中确认框、编辑健身目标底部抽屉(TextInput预填目标)、删除打卡记录居中确认框,采用底部抽屉(复杂表单类)+居中确认框(危险操作确认类)双样式,顶部拖拽条+双按钮设计规范统一。

  5. 丰富的数据可视化:打卡Tab运动数据环形图(圆形+4px活力绿边框模拟环形进度条85%)、我的Tab月度运动时长绿色系渐变柱状图(3-8月从浅绿#A5D6A7到深活力绿#00C853,8月30小时最高最深),通过纯声明式Column+背景色+圆形边框+动态height实现,无需Canvas,简洁高效。

  6. 8个interface强类型定义与8组静态配置数据:FitnessCourse(含level三字段难度独立色+calories消耗大卡)、Coach(含cert认证标签+specialty专长+bio简介)、CheckInRecord(含type四字段运动类型独立色+typeIcon图标)、Achievement69(含unlocked驱动opacity),10课程+8器材+6教练+7打卡记录+5社区动态+8分类+6月柱状图+5成就徽章,数据丰富且贴合真实健身社交业务。

16.2 未来扩展方向

  1. 可穿戴设备数据同步:当前打卡数据为静态展示,未来可对接华为运动健康Health Kit,同步华为手表/手环的实时心率、步数、卡路里、睡眠数据,实现运动数据自动记录和同步,无需手动打卡。

  2. AI私教指导:当前私教为静态教练列表,未来可引入AI大模型能力,根据用户的身体数据(身高/体重/体脂/运动基础)和健身目标(减脂/增肌/塑形/康复),AI自动生成个性化训练计划和饮食建议,并在运动过程中提供实时语音指导。

  3. AR运动姿势矫正:利用HarmonyOS的AR能力和摄像头,实时识别用户的运动姿势(如深蹲、硬拉、俯卧撑),AI分析关节角度和动作标准度,实时反馈姿势矫正建议,降低运动损伤风险。

  4. 社区直播跟练:当前社区为图文动态,未来可增加健身直播功能,教练实时直播带练,用户在线跟练并实时互动(弹幕、点赞、送礼物),直播结束后生成回放和运动数据报告。

  5. 健身挑战赛事:引入线上健身挑战赛事(如30天减脂挑战、百日跑挑战、平板支撑大赛),用户报名参与,系统自动记录运动数据并排名,完赛用户获得成就徽章和奖励(优惠券、免费课程、实体奖牌),增强用户粘性和运动动力。

  6. 器材智能联动:对接智能健身器材(智能跑步机、智能动感单车、智能哑铃),通过蓝牙或WiFi实时获取器材数据(速度、阻力、重量、次数),自动记录运动数据并同步到打卡Tab,实现「器材-数据-社区」的智能联动闭环。

多多健身社交作为一款基于HarmonyOS ArkTS API 24的健身社交全链路服务平台,通过六组件多Tab架构、活力绿能量橙双主色色彩体系、六大Tab差异化布局、四模态弹窗、丰富的数据可视化和完整的业务数据模型,完整还原了健身社交从课程到个人的全链路体验,为HarmonyOS生态下的健身社交应用开发提供了可参考的完整实现方案。

Logo

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

更多推荐