引言

在这里插入图片描述

随着全民健身理念的深入普及,运动健身类电商应用的需求日益增长。本文将以一个运动健身仓应用为案例,深入解析如何基于HarmonyOS ArkTS API 24构建一个涵盖装备集市、力量器械、有氧器械、运动服饰、私教课程、消息中心和个人中心七大功能模块的综合健身电商平台。该应用不仅实现了运动装备的展示与销售,还创新性地引入了器械按天租赁和私教课程预约两大特色业务,充分展示了ArkTS在垂直行业场景下的业务建模能力和交互设计灵活性。

在技术架构层面,该应用采用Stack作为根容器,通过层叠布局实现弹窗覆盖效果。主页面由深色头部导航栏(headerBar)、可滚动内容区和深色底部导航栏(bottomBar)三部分组成。应用使用了深灰色(#212121)作为主背景色调,搭配红色(#D32F2F)和青绿色(#00BFA5)作为强调色,形成了硬朗的运动风格视觉体系。头部导航栏采用深色背景配青绿色促销文字,底部导航栏使用青绿色作为选中态指示色,整体色调与运动健身的硬核气质高度契合。内容区根据currentTab状态变量动态切换七个@Builder方法构建的页面,每个页面都集成了独特的数据可视化组件。

在数据模型设计层面,应用定义了六种接口类型来管理不同品类的运动商品和服务数据,包括运动装备、力量器械、有氧器械、运动服饰、私教教练和消息通知。与前两个案例不同的是,本应用的数据模型更加注重性能参数的量化展示:力量器械的weight字段记录重量,有氧器械的powermaxSpeed字段分别记录电机马力和极速,运动服饰的rating字段记录好评率,私教教练的sessions字段记录累计授课节数。这些数值型字段通过自定义柱状图、水平进度条和五段式评分条等多种可视化组件呈现,为用户提供了直观的产品参数对比视图。应用还实现了五套弹窗交互逻辑,包括二手器械发布、器械租赁、私教课程预约、删除确认和商品详情侧滑面板,其中租赁和课程预约弹窗包含了多层级的选项选择和动态费用计算逻辑。

数据模型与状态管理

在这里插入图片描述

接口类型体系设计

本应用定义了六种核心接口类型,每种接口针对运动健身场景的特定品类进行了字段设计,既包含了展示所需的基础信息,也涵盖了用于数据可视化和费用计算的数值型字段。

interface GearItem {
  name: string
  price: number
  sold: number
  quality: number
  category: string
  tag: string
}

interface StrengthItem {
  name: string
  weight: number
  material: string
  price: number
  stock: number
}

interface CardioItem {
  name: string
  power: number
  maxSpeed: number
  price: number
  programs: number
}

interface ApparelItem {
  name: string
  size: number
  fabric: string
  price: number
  rating: number
}

interface CoachItem {
  name: string
  specialty: string
  years: number
  sessions: number
  price: number
  score: number
}

interface MessageItem {
  title: string
  content: string
  time: string
  unread: number
}

GearItem是装备集市的核心数据结构,quality字段表示品质评分(0-100),用于在商品列表中渲染竖向品质进度条;sold字段记录付款人数,用于销量展示。StrengthItem接口中weight字段表示器械重量,是力量器械页面的柱状图数据源;material字段记录材质信息,stock字段记录库存。CardioItem接口是本应用中最复杂的接口类型,包含power(电机马力HP)、maxSpeed(极速km/h)和programs(程序数量)三个性能参数字段,分别用于五段式评分条、水平进度条和程序数量标签的渲染。ApparelItem接口的size字段记录推荐身高码数,rating字段用于好评率进度条和柱状图。CoachItem接口包含sessions(授课节数)、years(从业年限)、score(好评率)和price(课时单价)四个数值字段,其中sessions用于柱状图展示。MessageItem接口的unread字段控制未读徽章的显示与动画效果。

组件状态与选项配置

在这里插入图片描述

组件通过多个@State变量管理Tab切换、弹窗显示和选中项等交互状态,同时预置了四组选项数组服务于弹窗中的选择交互。

@Entry
@Component
struct FitnessWarehousePage {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showRent: boolean = false
  @State showCourse: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: GearItem | null = null
  @State selectedType: number = 0
  @State selectedWeight: number = 0
  @State selectedDays: number = 0
  @State selectedLevel: number = 0
  @State qty: number = 1
  @State inputName: string = ''

  private tabs: string[] = ['装备集市', '力量器械', '有氧器械', '运动服饰', '私教课程', '消息', '我的']
  private types: string[] = ['哑铃', '杠铃', '壶铃', '弹力带', '护具', '跳绳']
  private weights: string[] = ['5kg 以下', '5-15kg', '15-30kg', '30kg 以上']
  private dayOptions: string[] = ['租 7 天', '租 15 天', '租 30 天', '租 90 天']
  private levels: string[] = ['入门燃脂', '增肌进阶', 'HIIT 冲刺', '康复拉伸']

状态变量分为三类:弹窗控制类(showPublishshowRentshowCourseshowDeleteshowDetail)管理五种弹窗;选中项管理类(selectedItemselectedTypeselectedWeightselectedDaysselectedLevel)记录用户在列表和弹窗中的选择状态;数值输入类(qtyinputName)管理租赁/课程数量和商品名称输入。selectedTypeselectedWeight服务于发布弹窗的器械类型和重量档位选择,selectedDays服务于租赁弹窗的租期选择,selectedLevel服务于课程弹窗的训练目标选择。四组选项数组typesweightsdayOptionslevels分别定义了发布弹窗的器械类型(哑铃/杠铃/壶铃/弹力带/护具/跳绳)、重量档位(5kg以下到30kg以上)、租期选项(7天到90天)和训练目标(入门燃脂到康复拉伸),这些数组在ForEach中渲染为可选择的标签按钮。

数据集合与计算方法

在这里插入图片描述

模拟数据初始化

应用预置了涵盖六个品类的模拟数据,每条记录都包含了该品类特有的业务字段,为数据可视化提供丰富的数据源。

  private gears: GearItem[] = [
    { name: '可调节哑铃 24kg 单只', price: 399, sold: 2865, quality: 95, category: '哑铃', tag: '爆款' },
    { name: '包胶哑铃 20kg 对装', price: 259, sold: 3412, quality: 92, category: '哑铃', tag: '实惠' },
    { name: '奥杆杠铃 1.8m 套装', price: 529, sold: 962, quality: 93, category: '杠铃', tag: '推荐' },
    { name: '深蹲架多功能 power rack', price: 1899, sold: 436, quality: 97, category: '杠铃', tag: '旗舰' },
    { name: '壶铃铸铁 16kg', price: 149, sold: 2051, quality: 91, category: '壶铃', tag: '爆款' },
    { name: '弹力带套装 150lb 五条', price: 89, sold: 5620, quality: 90, category: '弹力带', tag: '爆款' },
    { name: '腰带健身真皮 10mm', price: 199, sold: 1205, quality: 96, category: '护具', tag: '推荐' },
    { name: '钢丝竞速跳绳 轴承', price: 59, sold: 4385, quality: 94, category: '跳绳', tag: '爆款' },
    { name: '瑜伽垫加宽 183cm', price: 89, sold: 5104, quality: 92, category: '护具', tag: '爆款' }
  ]

  private strengths: StrengthItem[] = [
    { name: '可调哑铃 24kg', weight: 24, material: '铸铁包胶', price: 399, stock: 168 },
    { name: '六角哑铃 12kg 对', weight: 12, material: '电镀钢', price: 189, stock: 245 },
    { name: '奥杆杠铃 20kg', weight: 20, material: '合金钢', price: 359, stock: 92 },
    { name: '壶铃 8kg', weight: 8, material: '铸铁', price: 99, stock: 210 },
    { name: '壶铃 20kg', weight: 20, material: '竞技钢', price: 239, stock: 76 },
    { name: '杠铃片 25kg 对', weight: 25, material: '包胶铸铁', price: 299, stock: 58 },
    { name: '深蹲架 60kg 套装', weight: 60, material: '碳钢', price: 1299, stock: 32 },
    { name: '可调哑铃 40kg', weight: 40, material: '快锁结构', price: 799, stock: 41 }
  ]

  private cardios: CardioItem[] = [
    { name: '折叠跑步机 X3 家用', power: 2, maxSpeed: 16, price: 2299, programs: 12 },
    { name: '商用跑步机 T900', power: 4, maxSpeed: 20, price: 6999, programs: 24 },
    { name: '磁控椭圆机 E20', power: 1, maxSpeed: 12, price: 1799, programs: 8 },
    { name: '前置飞轮椭圆机 E50', power: 2, maxSpeed: 15, price: 3499, programs: 16 },
    { name: '风阻划船机 R1', power: 2, maxSpeed: 18, price: 2899, programs: 10 },
    { name: '动感单车 S5 自发电', power: 2, maxSpeed: 20, price: 2499, programs: 18 },
    { name: '爬楼机 Stair3 商用', power: 3, maxSpeed: 16, price: 5499, programs: 15 },
    { name: '滑雪机 SkiFit 双轨', power: 2, maxSpeed: 17, price: 3999, programs: 12 }
  ]

gears数组是装备集市的数据源,每条记录包含装备名称、价格、销量、品质评分、品类和标签。strengths数组用于力量器械页面,weight字段将作为柱状图的高度数据,直观展示各器械的重量分布;material字段记录材质,stock字段记录库存。cardios数组是有氧器械页面的数据源,其中power字段取值1-4,用于五段式评分条的渲染;maxSpeed字段取值12-20,用于水平进度条展示;programs字段记录程序数量,以标签形式展示。这三组数据集合的设计充分考虑了后续可视化的需求,数值型字段可以直接参与布局计算和比例换算。

计算方法与安全访问器

在这里插入图片描述

组件定义了四个最大值计算方法和六个选中项访问器方法,为柱状图比例计算和详情页数据展示提供支持。

  private maxWeight(): number {
    let m: number = 0
    this.strengths.forEach((s: StrengthItem) => {
      if (s.weight > m) {
        m = s.weight
      }
    })
    return m
  }

  private maxPower(): number {
    let m: number = 0
    this.cardios.forEach((c: CardioItem) => {
      if (c.power > m) {
        m = c.power
      }
    })
    return m
  }

  private maxSessions(): number {
    let m: number = 0
    this.coaches.forEach((c: CoachItem) => {
      if (c.sessions > m) {
        m = c.sessions
      }
    })
    return m
  }

  private maxSpeed(): number {
    let m: number = 0
    this.cardios.forEach((c: CardioItem) => {
      if (c.maxSpeed > m) {
        m = c.maxSpeed
      }
    })
    return m
  }

  private selName(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.name
  }

  private selPrice(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.price
  }

  private selCategory(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.category
  }

  private selQuality(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.quality
  }

  private selSold(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.sold
  }

四个max系列方法分别遍历strengthscardios(两次,分别查找最大马力和最大极速)和coaches数组,使用forEach迭代找出对应字段的最大值。maxWeight()返回最大重量值(如60kg),作为力量器械柱状图的高度基准。maxPower()返回最大马力值(如4HP),用于五段式评分条的比例计算。maxSessions()返回最大授课节数,用于私教柱状图。maxSpeed()返回最大极速值(如20km/h),用于极速进度条的比例计算。sel系列访问器方法采用空值检查模式,在selectedItemnull时返回默认值,确保详情弹窗渲染时的安全性。这些方法在build函数中被调用,由于ArkTS的声明式渲染特性,访问器在每次状态变化时都会被重新调用。

页面主框架构建

在这里插入图片描述

Stack根容器与页面骨架

应用使用Stack作为根容器,通过层叠方式实现弹窗覆盖。主页面采用深色主题,头部和底部导航栏均使用#212121深灰色背景。

  build() {
    Stack() {
      Column() {
        this.headerBar()
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tabMarket()
            } else if (this.currentTab === 1) {
              this.tabStrength()
            } else if (this.currentTab === 2) {
              this.tabCardio()
            } else if (this.currentTab === 3) {
              this.tabApparel()
            } else if (this.currentTab === 4) {
              this.tabCoach()
            } else if (this.currentTab === 5) {
              this.tabMessage()
            } else {
              this.tabMine()
            }
          }
          .width('100%')
          .padding({ left: 12, right: 12, top: 10, bottom: 10 })
        }
        .layoutWeight(1)
        .width('100%')
        .scrollBar(BarState.Off)
        this.bottomBar()
      }
      .width('100%')
      .height('100%')

      if (this.showPublish) {
        this.modalPublish()
      }
      if (this.showRent) {
        this.modalRent()
      }
      if (this.showCourse) {
        this.modalCourse()
      }
      if (this.showDelete) {
        this.modalDelete()
      }
      if (this.showDetail) {
        this.modalDetail()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

Stack容器的层叠特性使弹窗组件天然覆盖在主内容之上。主页面Column包含头部导航、可滚动内容区和底部导航栏三个垂直区域。内容区通过if-else if-else链式条件判断,根据currentTab值调用对应的@Builder方法。Scroll容器设置scrollBar(BarState.Off)隐藏滚动条,内层Column设置12像素的左右内边距。五种弹窗作为Stack的子元素,通过独立的if条件控制渲染。整体背景色为浅灰色(#F5F5F5),与深色导航栏形成鲜明对比,内容区域的白底卡片在浅灰背景上更加突出。

深色头部与底部导航

在这里插入图片描述

头部导航栏使用深灰色背景,配青绿色促销文字和红色Banner。底部导航栏使用青绿色作为选中态指示色。

  @Builder
  headerBar() {
    Column() {
      Row() {
        Column() {
          Text('运动健身仓')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
        }
        Column() {
          Row() {
            Text('搜索器械 / 课程 / 教练')
              .fontSize(12)
              .fontColor('#9E9E9E')
          }
          .width('100%')
          .height(30)
          .backgroundColor('#FFFFFF')
          .borderRadius(15)
          .justifyContent(FlexAlign.Start)
          .padding({ left: 14 })
        }
        .layoutWeight(1)
        .margin({ left: 12, right: 12 })
        Column() {
          Text('消息')
            .fontSize(13)
            .fontColor('#FFFFFF')
        }
        .onClick(() => {
          this.currentTab = 5
        })
      }
      .width('100%')
      .height(50)
      .padding({ left: 14, right: 14 })
      .alignItems(VerticalAlign.Center)

      Row() {
        Text('哑铃日 · 力量专区 5 折起')
          .fontSize(12)
          .fontColor('#00BFA5')
          .fontWeight(FontWeight.Medium)
        Text('器械租赁上线')
          .fontSize(12)
          .fontColor('#00BFA5')
          .margin({ left: 14 })
        Text('私教体验 0 元')
          .fontSize(12)
          .fontColor('#00BFA5')
          .margin({ left: 14 })
      }
      .width('100%')
      .padding({ left: 14, bottom: 8 })
      .justifyContent(FlexAlign.Start)
    }
    .width('100%')
    .backgroundColor('#212121')
  }

  @Builder
  bottomBar() {
    Row() {
      ForEach(this.tabs, (tab: string, index: number) => {
        Column() {
          Column()
            .width(index === this.currentTab ? 18 : 6)
            .height(3)
            .borderRadius(2)
            .backgroundColor(index === this.currentTab ? '#00BFA5' : 'rgba(255,255,255,0.3)')
          Text(tab)
            .fontSize(11)
            .fontColor(index === this.currentTab ? '#00BFA5' : 'rgba(255,255,255,0.6)')
            .margin({ top: 5 })
        }
        .layoutWeight(1)
        .height(54)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.currentTab = index
        })
      })
    }
    .width('100%')
    .backgroundColor('#212121')
  }

头部导航栏的深灰色背景(#212121)与青绿色促销文字(#00BFA5)形成强烈的色彩对比,传递出运动健身的专业感和科技感。第一行是50像素高的导航行,左侧是"运动健身仓"品牌标题,中间是搜索框,右侧是消息入口。第二行是促销信息栏,使用青绿色文字展示"哑铃日力量专区5折起"、"器械租赁上线"和"私教体验0元"三项核心服务。底部导航栏的选中指示条使用青绿色——选中时宽度18像素、青绿色背景,未选中时宽度6像素、30%透明度白色背景,宽度差异比宠物应用更大(18 vs 16),视觉反馈更明显。Tab文字选中时为青绿色,未选中时为60%透明度的白色。整个底部栏使用深灰色背景,与头部形成统一的暗色主题。

核心业务页面实现

装备集市:红色Banner与品质进度条

装备集市页面(tabMarket)是应用首页,包含红色促销Banner、横向品类筛选、品质进度条商品列表和租赁入口四个区域。

  @Builder
  tabMarket() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('夏季塑形冲刺季')
              .fontSize(19)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('器械直降 · 满1000减150 · 12期免息')
              .fontSize(12)
              .fontColor('#B2DFDB')
              .margin({ top: 6 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          Text('租')
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00BFA5')
            .width(64)
            .height(64)
            .textAlign(TextAlign.Center)
            .backgroundColor('#FFFFFF')
            .borderRadius(32)
            .scale({ x: 1.06, y: 1.06 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            .onClick(() => {
              this.showRent = true
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .padding(18)
      .backgroundColor('#D32F2F')
      .borderRadius(14)

      Scroll() {
        Row() {
          ForEach(this.types, (t: string, idx: number) => {
            Text(t)
              .fontSize(12)
              .fontColor(this.selectedType === idx ? '#FFFFFF' : '#616161')
              .backgroundColor(this.selectedType === idx ? '#D32F2F' : '#FFFFFF')
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .margin({ right: 8 })
              .onClick(() => {
                this.selectedType = idx
              })
          })
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .margin({ top: 12 })

促销Banner使用红色背景(#D32F2F),左侧展示"夏季塑形冲刺季"标题和促销规则,右侧是一个64x64的白色圆形"租"字按钮,使用青绿色文字和106%缩放动画,点击后打开器械租赁弹窗。Banner的设计将促销信息与核心业务入口(租赁)有机结合,用户可以在了解促销活动的同时直接发起租赁操作。品类筛选区使用横向Scroll容器,渲染六个器械类型标签,选中态使用红色背景白色文字,未选中态为白色背景灰色文字。

商品列表部分通过ForEach遍历gears数组渲染,每行包含左侧品质指示条、商品信息和价格操作区:

      Column() {
        ForEach(this.gears, (g: GearItem) => {
          Row() {
            Column()
              .width(10)
              .height(96)
              .backgroundColor(g.quality > 92 ? '#00BFA5' : '#FFB74D')
              .borderRadius(5)
            Column() {
              Text(g.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#212121')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text(g.category)
                  .fontSize(10)
                  .fontColor('#D32F2F')
                  .backgroundColor('#FDE7E7')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text(g.tag)
                  .fontSize(10)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#212121')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .margin({ left: 6 })
              }
              .margin({ top: 6 })

              Row() {
                Stack({ alignContent: Alignment.Start }) {
                  Column()
                    .width('100%')
                    .height(6)
                    .backgroundColor('#EEEEEE')
                    .borderRadius(3)
                  Column()
                    .width(g.quality + '%')
                    .height(6)
                    .backgroundColor('#00BFA5')
                    .borderRadius(3)
                }
                .width(120)
                Text('品质 ' + g.quality)
                  .fontSize(10)
                  .fontColor('#00BFA5')
                  .margin({ left: 6 })
              }
              .margin({ top: 8 })

              Row() {
                Text('¥' + g.price)
                  .fontSize(17)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#D32F2F')
                Text(g.sold + '人付款')
                  .fontSize(11)
                  .fontColor('#9E9E9E')
                  .margin({ left: 8 })
                Column().layoutWeight(1)
                Text('租')
                  .fontSize(11)
                  .fontColor('#00BFA5')
                  .backgroundColor('#E0F2F1')
                  .borderRadius(10)
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .onClick(() => {
                    this.showRent = true
                  })
              }
              .width('100%')
              .alignItems(VerticalAlign.Center)
              .margin({ top: 8 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            this.selectedItem = g
            this.showDetail = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

装备集市列表的设计有一个独特之处:每行左侧有一个10像素宽、96像素高的竖向品质指示条,背景色根据品质评分是否大于92来选择青绿色或橙色,这种设计在商品列表中形成了视觉引导,用户可以通过颜色快速识别高品质商品。品质进度条使用Stack叠加两层Column,宽度为120像素,上层进度条宽度为g.quality + '%'。品类标签使用浅红色背景红色文字,促销标签使用深灰色背景白色文字,形成品类与标签的双重分类展示。价格区域使用Column().layoutWeight(1)占据中间空间,将"租"按钮推至右侧。每行的"租"按钮使用浅青绿色背景(#E0F2F1)和青绿色文字,点击后打开租赁弹窗,为用户提供了从商品列表直接发起租赁的快捷入口。

力量器械:重量柱状图与双列卡片

力量器械页面(tabStrength)使用深色背景卡片展示重量分布柱状图,配合Flex双列网格展示器械卡片。

  @Builder
  tabStrength() {
    Column() {
      Column() {
        Row() {
          Text('力量器械 · 重量分布')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('单位 kg')
            .fontSize(11)
            .fontColor('#B2DFDB')
            .margin({ left: 10 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Row() {
          ForEach(this.strengths, (s: StrengthItem) => {
            Column() {
              Column()
                .width(15)
                .height(s.weight / this.maxWeight() * 88)
                .backgroundColor('#00BFA5')
                .borderRadius({ topLeft: 3, topRight: 3 })
            }
            .height(92)
            .justifyContent(FlexAlign.End)
            .margin({ right: 5 })
          })
        }
        .width('100%')
        .alignItems(VerticalAlign.End)
        .margin({ top: 12 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#37474F')
      .borderRadius(12)
      .margin({ top: 4 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.strengths, (s: StrengthItem) => {
          Column() {
            Row() {
              Text(s.weight + 'kg')
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D32F2F')
              Text(s.material)
                .fontSize(10)
                .fontColor('#9E9E9E')
                .margin({ left: 8 })
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

            Text(s.name)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#212121')
              .margin({ top: 6 })
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })

            Row() {
              Text('¥' + s.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D32F2F')
              Text('库存' + s.stock)
                .fontSize(10)
                .fontColor('#9E9E9E')
                .margin({ left: 8 })
            }
            .width('100%')
            .margin({ top: 6 })

            Row() {
              Text('按天租')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#00BFA5')
                .borderRadius(10)
                .padding({ left: 12, right: 12, top: 4, bottom: 4 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
                .onClick(() => {
                  this.selectedDays = 0
                  this.showRent = true
                })
              Text('发布同款')
                .fontSize(11)
                .fontColor('#616161')
                .backgroundColor('#FAFAFA')
                .borderRadius(10)
                .padding({ left: 12, right: 12, top: 4, bottom: 4 })
                .margin({ left: 8 })
                .onClick(() => {
                  this.showPublish = true
                })
            }
            .margin({ top: 8 })
          }
          .width('48%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(12)
          .margin({ top: 10 })
          .alignItems(HorizontalAlign.Start)
        })
      }
      .width('100%')
    }
    .width('100%')
  }

重量分布柱状图使用深蓝灰色背景(#37474F),与页面的深色主题呼应。每个柱子宽度15像素,高度通过s.weight / this.maxWeight() * 88计算——以最大重量为基准,乘以88作为最大高度。柱子使用青绿色背景,顶部设置3像素圆角。柱状图容器使用alignItems(VerticalAlign.End)使所有柱子底部对齐。器械卡片使用Flex容器配合FlexWrap.WrapFlexAlign.SpaceBetween实现双列网格布局,每个卡片宽度48%。卡片顶部突出展示重量数值(18号红色粗体),旁边是材质信息。每个卡片底部提供"按天租"和"发布同款"两个操作按钮——"按天租"按钮使用青绿色背景并配合缩放动画,点击后重置selectedDays为0并打开租赁弹窗;"发布同款"按钮使用浅灰色背景,点击后打开发布弹窗。这种双按钮设计为用户提供了租赁和发布两种业务入口。

有氧器械:马力评分条与极速进度条

有氧器械页面(tabCardio)是该应用中数据可视化最丰富的页面,集成了五段式马力评分条、极速水平进度条和程序数量标签三种可视化组件。

  @Builder
  tabCardio() {
    Column() {
      Row() {
        Column() {
          Text('有氧器械馆')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('电机马力与极速参数一览')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)

        Text('免费安装')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#D32F2F')
          .borderRadius(10)
          .padding({ left: 10, right: 10, top: 6, bottom: 6 })
          .scale({ x: 1.05, y: 1.05 })
          .animation({ duration: 900, iterations: -1, curve: Curve.EaseInOut })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 4 })
      .alignItems(VerticalAlign.Center)

      Column() {
        ForEach(this.cardios, (c: CardioItem) => {
          Column() {
            Row() {
              Column() {
                Text(c.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#212121')
                Row() {
                  Text(c.power + 'HP 电机')
                    .fontSize(10)
                    .fontColor('#00BFA5')
                    .backgroundColor('#E0F2F1')
                    .borderRadius(4)
                    .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  Text(c.programs + '种程序')
                    .fontSize(10)
                    .fontColor('#616161')
                    .backgroundColor('#FAFAFA')
                    .borderRadius(4)
                    .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                    .margin({ left: 6 })
                }
                .margin({ top: 5 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)

              Text('¥' + c.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D32F2F')
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

            Row() {
              Text('极速')
                .fontSize(10)
                .fontColor('#9E9E9E')
              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(8)
                  .backgroundColor('#EEEEEE')
                  .borderRadius(4)
                Column()
                  .width(c.maxSpeed / this.maxSpeed() * 100 + '%')
                  .height(8)
                  .backgroundColor('#D32F2F')
                  .borderRadius(4)
              }
              .layoutWeight(1)
              .margin({ left: 8 })
              Text(c.maxSpeed + 'km/h')
                .fontSize(10)
                .fontColor('#D32F2F')
                .fontWeight(FontWeight.Bold)
                .margin({ left: 8 })
            }
            .width('100%')
            .margin({ top: 10 })
            .alignItems(VerticalAlign.Center)

            Row() {
              Text(c.power / this.maxPower() * 100 + '%')
                .fontSize(10)
                .fontColor('#00BFA5')
              Row() {
                ForEach([1, 2, 3, 4, 5], (n: number) => {
                  Column()
                    .width(c.power >= n ? 12 : 12)
                    .height(4)
                    .borderRadius(2)
                    .backgroundColor(c.power >= n ? '#00BFA5' : '#EEEEEE')
                    .margin({ left: 3 })
                })
              }
              .margin({ left: 6 })
              Column().layoutWeight(1)
              Text('租赁体验')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#37474F')
                .borderRadius(10)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .onClick(() => {
                  this.selectedDays = 0
                  this.showRent = true
                })
            }
            .width('100%')
            .margin({ top: 8 })
            .alignItems(VerticalAlign.Center)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ top: 10 })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

有氧器械页面的数据可视化设计是该应用的技术亮点。每个器械卡片包含三层可视化信息:第一层是电机马力和程序数量的标签展示,马力标签使用浅青绿色背景青绿色文字,程序数量标签使用浅灰色背景灰色文字。第二层是极速进度条,使用Stack叠加两层Column——底层为100%宽度的灰色背景条,上层宽度为c.maxSpeed / this.maxSpeed() * 100 + '%'的红色进度条,直观展示各器械的极速参数对比。第三层是五段式马力评分条,这是本应用最独特的可视化组件——通过ForEach([1, 2, 3, 4, 5])渲染五个12x4的小方块,每个方块的背景色根据c.power >= n判断:当器械马力大于等于当前序号时为青绿色,否则为浅灰色。这种五段式评分条类似于电商应用中的五星评分,但更适合展示马力这种离散数值型参数。每个卡片底部的"租赁体验"按钮使用深蓝灰色背景,点击后打开租赁弹窗。

私教课程:授课柱状图与好评进度条

私教课程页面(tabCoach)展示金牌私教团队,使用深色背景卡片展示授课节数柱状图,配合好评率进度条和约课入口。

  @Builder
  tabCoach() {
    Column() {
      Column() {
        Row() {
          Text('金牌私教团')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('首课 0 元')
            .fontSize(11)
            .fontColor('#00E5FF')
            .margin({ left: 10 })
            .scale({ x: 1.08, y: 1.08 })
            .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Row() {
          ForEach(this.coaches, (c: CoachItem) => {
            Column() {
              Column()
                .width(14)
                .height(c.sessions / this.maxSessions() * 76)
                .backgroundColor('#00BFA5')
                .borderRadius({ topLeft: 3, topRight: 3 })
            }
            .height(80)
            .justifyContent(FlexAlign.End)
            .margin({ right: 5 })
          })
        }
        .width('100%')
        .alignItems(VerticalAlign.End)
        .margin({ top: 10 })
        Text('累计授课节数 (节)')
          .fontSize(10)
          .fontColor('#B2DFDB')
          .margin({ top: 6 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#212121')
      .borderRadius(12)
      .margin({ top: 4 })

      Column() {
        ForEach(this.coaches, (c: CoachItem) => {
          Row() {
            Column() {
              Text(c.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#212121')
              Text(c.specialty + ' · 从业' + c.years + '年')
                .fontSize(11)
                .fontColor('#9E9E9E')
                .margin({ top: 3 })
              Row() {
                Text('好评 ' + c.score + '%')
                  .fontSize(10)
                  .fontColor('#00BFA5')
                Stack({ alignContent: Alignment.Start }) {
                  Column()
                    .width('100%')
                    .height(6)
                    .backgroundColor('#EEEEEE')
                    .borderRadius(3)
                  Column()
                    .width(c.score + '%')
                    .height(6)
                    .backgroundColor('#00BFA5')
                    .borderRadius(3)
                }
                .width(80)
                .margin({ left: 6 })
              }
              .margin({ top: 6 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Column() {
              Text('¥' + c.price + '/节')
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D32F2F')
              Text('约课')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#D32F2F')
                .borderRadius(10)
                .padding({ left: 12, right: 12, top: 4, bottom: 4 })
                .margin({ top: 6 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ top: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            this.selectedLevel = 0
            this.showCourse = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

私教页面的头部卡片使用深灰色背景(#212121),"首课0元"标签使用亮青色(#00E5FF)并配合缩放动画,在深色背景上格外醒目。授课节数柱状图使用青绿色柱子,宽度14像素,高度通过c.sessions / this.maxSessions() * 76计算。教练列表每行展示教练名称、专长领域、从业年限、好评率进度条和课时单价。好评率进度条使用Stack叠加两层Column,宽度80像素,上层进度条宽度为c.score + '%'。每个教练卡片右侧展示课时单价和"约课"按钮,点击后重置selectedLevel为0并打开课程预约弹窗。教练信息的设计将量化数据(从业年限、好评率、授课节数)与定性描述(专长领域)有机结合,帮助用户全面评估教练能力。

页面交互流程

点击商品

点击租按钮

点击租字圆按钮

租赁

购买

删除

到店自提

送货上门

切换Tab

1

2

3

4

5

6

应用启动

渲染深色头部+底部导航

默认加载装备集市

用户浏览装备列表

用户操作

selectedItem赋值

showDetail=true 侧滑详情

showRent=true 租赁弹窗

详情页操作

关闭详情

showDelete=true 删除确认

选择租期7/15/30/90天

选择数量

查看押金和租金

配送方式

关闭弹窗

currentTab值

力量器械 重量柱状图

有氧器械 马力评分条

运动服饰 好评率柱状图

私教课程 授课柱状图

点击按天租 打开租赁弹窗

点击租赁体验 打开租赁弹窗

点击约课 打开课程弹窗

选训练目标 入门/增肌/HIIT/康复

选课时数量

查看费用 首课免费

提交预约

消息中心

我的页面

清除浏览记录

弹窗交互体系

器械租赁弹窗(底部弹出)

租赁弹窗(modalRent)是本应用的核心业务弹窗,包含租期选择、数量调节、押金和租金计算三个功能模块,并提供到店自提和送货上门两种配送方式。

  @Builder
  modalRent() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('器械租赁')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('押金 ¥' + (this.selectedDays + 1) * 100)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00BFA5')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text('租期选择')
            .fontSize(13)
            .fontColor('#616161')
          Row() {
            ForEach(this.dayOptions, (d: string, idx: number) => {
              Text(d)
                .fontSize(12)
                .fontColor(this.selectedDays === idx ? '#FFFFFF' : '#616161')
                .backgroundColor(this.selectedDays === idx ? '#37474F' : '#F5F5F5')
                .borderRadius(8)
                .padding({ left: 16, right: 16, top: 9, bottom: 9 })
                .margin({ right: 8, top: 10 })
                .onClick(() => {
                  this.selectedDays = idx
                })
            })
          }
          .width('100%')
          .flexWrap(FlexWrap.Wrap)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Row() {
          Text('数量')
            .fontSize(13)
            .fontColor('#616161')
          Column().layoutWeight(1)
          Row() {
            Text('-')
              .fontSize(16)
              .fontColor('#616161')
              .width(30)
              .height(30)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5F5F5')
              .borderRadius(6)
              .onClick(() => {
                if (this.qty > 1) {
                  this.qty -= 1
                }
              })
            Text(this.qty + '')
              .fontSize(14)
              .fontColor('#212121')
              .width(40)
              .height(30)
              .textAlign(TextAlign.Center)
            Text('+')
              .fontSize(16)
              .fontColor('#616161')
              .width(30)
              .height(30)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5F5F5')
              .borderRadius(6)
              .onClick(() => {
                if (this.qty < 9) {
                  this.qty += 1
                }
              })
          }
        }
        .width('100%')
        .margin({ top: 18 })
        .alignItems(VerticalAlign.Center)

        Text('预计租金:¥' + ((this.selectedDays + 1) * 15 * this.qty) + ' (到期可退押金)')
          .fontSize(14)
          .fontColor('#D32F2F')
          .fontWeight(FontWeight.Bold)
          .margin({ top: 16 })

        Row() {
          Text('到店自提')
            .fontSize(14)
            .fontColor('#616161')
            .layoutWeight(1)
            .height(42)
            .textAlign(TextAlign.Center)
            .backgroundColor('#F5F5F5')
            .borderRadius(21)
            .onClick(() => {
              this.showRent = false
            })
          Text('送货上门')
            .fontSize(14)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Bold)
            .layoutWeight(1)
            .height(42)
            .textAlign(TextAlign.Center)
            .backgroundColor('#D32F2F')
            .borderRadius(21)
            .margin({ left: 10 })
            .onClick(() => {
              this.showRent = false
            })
        }
        .width('100%')
        .margin({ top: 18 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '80%' })
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .padding(16)
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showRent = false
    })
  }

租赁弹窗的费用体系包含两个维度:押金和租金。押金计算公式为(this.selectedDays + 1) * 100,即租期档位(1-4)乘以100元——租7天押金200元,租90天押金400元。租金计算公式为(this.selectedDays + 1) * 15 * this.qty,即租期档位乘以15元再乘以数量。押金金额使用青绿色文字并配合700毫秒缩放动画,租金使用红色粗体文字。租期选择标签使用深蓝灰色(#37474F)作为选中色,与其他弹窗的红色或青绿色选中色形成区分。数量调节器的上限为9件。底部操作区提供"到店自提"(浅灰色背景)和"送货上门"(红色背景)两个按钮,使用layoutWeight(1)等分宽度,两种配送方式点击后均关闭弹窗。整个弹窗使用底部弹出模式,通过Column().layoutWeight(1)占位将内容推至底部,constraintSize({ maxHeight: '80%' })限制最大高度。

私教课程预约弹窗(居中卡片)

课程预约弹窗(modalCourse)采用居中卡片模式,包含训练目标选择、课时数量调节和费用计算三个功能模块。

  @Builder
  modalCourse() {
    Column() {
      Column() {
        Text('预约私教体验课')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#212121')
          .margin({ top: 18 })

        Column() {
          Text('训练目标')
            .fontSize(13)
            .fontColor('#616161')
          Row() {
            ForEach(this.levels, (l: string, idx: number) => {
              Text(l)
                .fontSize(12)
                .fontColor(this.selectedLevel === idx ? '#FFFFFF' : '#616161')
                .backgroundColor(this.selectedLevel === idx ? '#D32F2F' : '#F5F5F5')
                .borderRadius(14)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 10 })
                .onClick(() => {
                  this.selectedLevel = idx
                })
            })
          }
          .width('100%')
          .flexWrap(FlexWrap.Wrap)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Row() {
          Text('课时')
            .fontSize(13)
            .fontColor('#616161')
          Column().layoutWeight(1)
          Row() {
            Text('-')
              .fontSize(15)
              .fontColor('#616161')
              .width(28)
              .height(28)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5F5F5')
              .borderRadius(6)
              .onClick(() => {
                if (this.qty > 1) {
                  this.qty -= 1
                }
              })
            Text(this.qty + '')
              .fontSize(14)
              .fontColor('#212121')
              .width(36)
              .height(28)
              .textAlign(TextAlign.Center)
            Text('+')
              .fontSize(15)
              .fontColor('#616161')
              .width(28)
              .height(28)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5F5F5')
              .borderRadius(6)
              .onClick(() => {
                if (this.qty < 20) {
                  this.qty += 1
                }
              })
          }
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(VerticalAlign.Center)

        Text('首课免费 · 合计:¥' + (this.qty - 1) * 320)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#D32F2F')
          .margin({ top: 18 })
          .scale({ x: 1.04, y: 1.04 })
          .animation({ duration: 650, iterations: -1, curve: Curve.EaseInOut })

        Text('提交预约')
          .fontSize(14)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(42)
          .textAlign(TextAlign.Center)
          .backgroundColor('#D32F2F')
          .borderRadius(21)
          .margin({ top: 20, bottom: 18 })
          .onClick(() => {
            this.showCourse = false
          })
      }
      .width('86%')
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .padding({ left: 18, right: 18 })
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showCourse = false
    })
  }

课程预约弹窗的费用计算有一个独特的"首课免费"逻辑:费用公式为(this.qty - 1) * 320,即总课时减去1(首课免费)再乘以320元/节。当用户选择1节课时费用为0元,选择2节课时费用为320元,以此类推。课时数量的上限为20节,远高于租赁弹窗的9件和宠物预约弹窗的5只,体现了课程预约场景的数量弹性。训练目标标签使用红色作为选中色,四个选项分别为"入门燃脂"、“增肌进阶”、“HIIT冲刺"和"康复拉伸”,覆盖了不同健身阶段的用户需求。费用文字配合650毫秒的缩放动画,"首课免费"的前缀提示与费用金额形成营销引导。整个弹窗使用justifyContent(FlexAlign.Center)居中显示,卡片宽度86%。

发布弹窗与商品详情侧滑

发布弹窗(modalPublish)包含器械类型选择、重量档位选择和动态回收价计算三个功能模块,是应用中表单最丰富的弹窗。

  @Builder
  modalPublish() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('发布二手器械')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('关闭')
            .fontSize(13)
            .fontColor('#9E9E9E')
            .padding(6)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Column() {
          Text('器械名称')
            .fontSize(13)
            .fontColor('#616161')
          TextInput({ placeholder: '例如:可调节哑铃 24kg 九成新' })
            .height(40)
            .fontSize(13)
            .backgroundColor('#F5F5F5')
            .borderRadius(8)
            .margin({ top: 6 })
            .onChange((value: string) => {
              this.inputName = value
            })
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('器械类型')
            .fontSize(13)
            .fontColor('#616161')
          Row() {
            ForEach(this.types, (t: string, idx: number) => {
              Text(t)
                .fontSize(12)
                .fontColor(this.selectedType === idx ? '#FFFFFF' : '#616161')
                .backgroundColor(this.selectedType === idx ? '#D32F2F' : '#F5F5F5')
                .borderRadius(14)
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedType = idx
                })
            })
          }
          .width('100%')
          .flexWrap(FlexWrap.Wrap)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('重量档位')
            .fontSize(13)
            .fontColor('#616161')
          Row() {
            ForEach(this.weights, (w: string, idx: number) => {
              Text(w)
                .fontSize(12)
                .fontColor(this.selectedWeight === idx ? '#FFFFFF' : '#616161')
                .backgroundColor(this.selectedWeight === idx ? '#00BFA5' : '#F5F5F5')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedWeight = idx
                })
            })
          }
          .width('100%')
          .flexWrap(FlexWrap.Wrap)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Text('预估回收价:¥' + ((this.selectedType + 1) * 88 + (this.selectedWeight + 1) * 25))
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#00BFA5')
          .margin({ top: 18 })
          .scale({ x: 1.04, y: 1.04 })
          .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })

        Text('确认发布')
          .fontSize(15)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(44)
          .textAlign(TextAlign.Center)
          .backgroundColor('#D32F2F')
          .borderRadius(22)
          .margin({ top: 18 })
          .onClick(() => {
            this.showPublish = false
          })
      }
      .width('100%')
      .constraintSize({ maxHeight: '80%' })
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .padding(16)
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showPublish = false
    })
  }

发布弹窗包含三个表单模块:TextInput器械名称输入、器械类型选择(红色选中色)和重量档位选择(青绿色选中色)。预估回收价的计算公式为(this.selectedType + 1) * 88 + (this.selectedWeight + 1) * 25,即器械类型档位乘以88元加上重量档位乘以25元——哑铃5kg以下的回收价为113元,杠铃30kg以上的回收价为613元。回收价使用青绿色文字并配合600毫秒缩放动画。两种选择标签使用不同的选中色(红色和青绿色)形成视觉区分,帮助用户快速识别当前选择的维度。TextInputonChange回调将输入值同步到this.inputName状态变量。"确认发布"按钮使用红色背景,44像素高度,全宽圆角设计。

商品详情弹窗(modalDetail)使用Row作为根容器,包含78%宽度的滚动内容区和22%宽度的透明点击关闭区。内容区通过Scroll包裹Column实现可滚动,顶部是180高度的占位图区域(浅青绿色背景),下方展示价格品质分、商品名称、品类标签、品质评分进度条、已售信息和操作按钮。操作按钮区提供"租赁"和"购买"两个选项,"租赁"按钮使用浅青绿色背景,点击后打开租赁弹窗;"购买"按钮使用红色背景。底部还提供"删除该记录"入口,点击后打开删除确认弹窗。

消息中心与个人页面

消息列表与已读处理

消息中心页面(tabMessage)展示各类通知消息,通过未读徽章动画和点击已读处理实现消息管理。

  @Builder
  tabMessage() {
    Column() {
      Row() {
        Text('消息中心')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#212121')
        Text('6条未读')
          .fontSize(11)
          .fontColor('#D32F2F')
          .margin({ left: 8 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 4 })

      Column() {
        ForEach(this.messages, (m: MessageItem) => {
          Row() {
            Column() {
              Text(m.title.substring(0, 1))
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
            }
            .width(44)
            .height(44)
            .backgroundColor('#37474F')
            .borderRadius(10)
            .justifyContent(FlexAlign.Center)

            Column() {
              Row() {
                Text(m.title)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#212121')
                if (m.unread > 0) {
                  Text(m.unread + '')
                    .fontSize(10)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#D32F2F')
                    .borderRadius(8)
                    .padding({ left: 6, right: 6, top: 1, bottom: 1 })
                    .margin({ left: 6 })
                    .scale({ x: 1.1, y: 1.1 })
                    .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
                }
              }
              Text(m.content)
                .fontSize(12)
                .fontColor('#9E9E9E')
                .margin({ top: 3 })
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })

            Text(m.time)
              .fontSize(11)
              .fontColor('#BDBDBD')
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ top: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            m.unread = 0
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

消息列表的每行左侧是44x44的圆角方形头像容器(borderRadius(10)),使用深蓝灰色背景(#37474F),与应用整体的深色主题一致。头像内容使用消息标题首字符(m.title.substring(0, 1)),白色粗体文字。未读徽章通过if (m.unread > 0)条件渲染,使用红色背景白色数字,并配合110%缩放和700毫秒呼吸动画。每行的onClick事件直接设置m.unread = 0,将消息标记为已读,利用ArkTS对象属性修改的响应式特性实现未读徽章的即时消失。消息内容使用maxLines(1)单行展示并省略溢出。消息列表的设计涵盖了预约通知、租期提醒、直播提醒、降价提醒等多种业务场景的消息类型。

个人中心页面

个人中心页面(tabMine)展示用户信息、训练统计数据和功能菜单列表。

  @Builder
  tabMine() {
    Column() {
      Row() {
        Column()
          .width(64)
          .height(64)
          .backgroundColor('#B2DFDB')
          .borderRadius(10)
        Column() {
          Text('铁馆常客_Ken')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('黑铁会员 · 训练 428 小时')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
        .padding({ left: 12 })
        Text('周训练计划')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#00BFA5')
          .borderRadius(12)
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .scale({ x: 1.05, y: 1.05 })
          .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
          .onClick(() => {
            this.showCourse = true
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 4 })
      .alignItems(VerticalAlign.Center)

      Row() {
        Column() {
          Text('12')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('租赁中')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('9')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('优惠券')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('86')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('收藏夹')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('21')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('打卡天')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding({ top: 14, bottom: 14 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 10 })

      Column() {
        ForEach(['我的订单', '租赁记录', '课程表', '体测档案', '客服中心', '清除浏览记录'], (m: string, idx: number) => {
          Row() {
            Text(m)
              .fontSize(14)
              .fontColor('#212121')
            Column().layoutWeight(1)
            Text('>')
              .fontSize(14)
              .fontColor('#BDBDBD')
          }
          .width('100%')
          .padding({ top: 14, bottom: 14 })
          .border({ width: 1, color: '#F5F5F5' })
          .onClick(() => {
            if (idx === 5) {
              this.showDelete = true
            }
          })
        })
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .padding({ left: 14, right: 14 })
      .margin({ top: 10 })
    }
    .width('100%')
  }

个人中心页面顶部是用户信息行,头像使用浅青绿色背景(#B2DFDB)的圆角方形(borderRadius(10)),与运动主题呼应。用户名"铁馆常客_Ken"和"黑铁会员 · 训练428小时"的描述体现了运动健身社区的氛围。“周训练计划"按钮使用青绿色背景并配合缩放动画,点击后打开课程预约弹窗。下方四列统计卡片展示租赁中(12)、优惠券(9)、收藏夹(86)和打卡天(21)四项数据,其中"租赁中"和"打卡天"是运动健身场景特有的统计维度。功能菜单列表通过ForEach渲染六个菜单项,包含"租赁记录”、"课程表"和"体测档案"等健身特色菜单。"清除浏览记录"菜单项(索引5)触发删除确认弹窗。

技术点对比

技术维度装备集市列表力量器械柱状图有氧器械马力评分条私教授课柱状图弹窗交互体系
布局方式Row + 竖向品质指示条Flex双列网格 + Row柱状图Column + Row多层进度条Row + 深色背景柱状图卡片Stack层叠 + 底部/居中
数据驱动gears数组 + ForEachstrengths数组 + maxWeightcardios数组 + maxPower/maxSpeedcoaches数组 + maxSessions@State布尔变量控制
可视化手段竖向品质进度条 + 左侧色条重量柱状图(青绿)五段式评分条 + 极速进度条授课柱状图(青绿) + 好评进度条费用动态计算 + 缩放动画
交互模式点击打开详情 + 租按钮按天租/发布同款双按钮租赁体验按钮约课按钮遮罩关闭 + stopPropagation
状态管理selectedItem赋值selectedDays重置selectedDays重置selectedLevel重置selectedType/Weight/Days/Level等
颜色体系深灰#212121+红#D32F2F+青绿#00BFA5深蓝灰#37474F背景红色极速条+青绿评分条深灰#212121背景红/青绿/深蓝灰三色弹窗区分
动画效果租字圆按钮缩放按天租按钮缩放免费安装标签缩放首课0元标签缩放押金/费用/回收价缩放动画
费用计算租金=(天数+1)15qty, 课程=(qty-1)*320, 回收价=(类型+1)*88+(重量+1)*25

安装DevEco Studio程序

在这里插入图片描述
选择目标安装目录:

在这里插入图片描述
设置环境变量,但是需要重启一下:

在这里插入图片描述
新建一个空白模板:

在这里插入图片描述
设置API为24的模板项目:
在这里插入图片描述
初始化项目,自动下载相关依赖:

在这里插入图片描述


完整代码:

interface GearItem {
  name: string
  price: number
  sold: number
  quality: number
  category: string
  tag: string
}

interface StrengthItem {
  name: string
  weight: number
  material: string
  price: number
  stock: number
}

interface CardioItem {
  name: string
  power: number
  maxSpeed: number
  price: number
  programs: number
}

interface ApparelItem {
  name: string
  size: number
  fabric: string
  price: number
  rating: number
}

interface CoachItem {
  name: string
  specialty: string
  years: number
  sessions: number
  price: number
  score: number
}

interface MessageItem {
  title: string
  content: string
  time: string
  unread: number
}

@Entry
@Component
struct FitnessWarehousePage {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showRent: boolean = false
  @State showCourse: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: GearItem | null = null
  @State selectedType: number = 0
  @State selectedWeight: number = 0
  @State selectedDays: number = 0
  @State selectedLevel: number = 0
  @State qty: number = 1
  @State inputName: string = ''

  private tabs: string[] = ['装备集市', '力量器械', '有氧器械', '运动服饰', '私教课程', '消息', '我的']
  private types: string[] = ['哑铃', '杠铃', '壶铃', '弹力带', '护具', '跳绳']
  private weights: string[] = ['5kg 以下', '5-15kg', '15-30kg', '30kg 以上']
  private dayOptions: string[] = ['租 7 天', '租 15 天', '租 30 天', '租 90 天']
  private levels: string[] = ['入门燃脂', '增肌进阶', 'HIIT 冲刺', '康复拉伸']

  private gears: GearItem[] = [
    { name: '可调节哑铃 24kg 单只', price: 399, sold: 2865, quality: 95, category: '哑铃', tag: '爆款' },
    { name: '包胶哑铃 20kg 对装', price: 259, sold: 3412, quality: 92, category: '哑铃', tag: '实惠' },
    { name: '电镀六角哑铃 30kg 对', price: 349, sold: 1783, quality: 94, category: '哑铃', tag: '热卖' },
    { name: '奥杆杠铃 1.8m 套装', price: 529, sold: 962, quality: 93, category: '杠铃', tag: '推荐' },
    { name: '深蹲架多功能 power rack', price: 1899, sold: 436, quality: 97, category: '杠铃', tag: '旗舰' },
    { name: '壶铃铸铁 16kg', price: 149, sold: 2051, quality: 91, category: '壶铃', tag: '爆款' },
    { name: '竞技壶铃 24kg 双色', price: 279, sold: 687, quality: 95, category: '壶铃', tag: '进口' },
    { name: '弹力带套装 150lb 五条', price: 89, sold: 5620, quality: 90, category: '弹力带', tag: '爆款' },
    { name: '环形阻力带三条装', price: 45, sold: 3914, quality: 88, category: '弹力带', tag: '实惠' },
    { name: '举重护腕绑带加固', price: 65, sold: 1537, quality: 92, category: '护具', tag: '热卖' },
    { name: '助力带硬拉专用', price: 39, sold: 2742, quality: 89, category: '护具', tag: '实惠' },
    { name: '腰带健身真皮 10mm', price: 199, sold: 1205, quality: 96, category: '护具', tag: '推荐' },
    { name: '护膝压缩加压运动款', price: 119, sold: 1861, quality: 93, category: '护具', tag: '热卖' },
    { name: '负重马甲 10kg 可拆', price: 229, sold: 743, quality: 91, category: '护具', tag: '新品' },
    { name: '钢丝竞速跳绳 轴承', price: 59, sold: 4385, quality: 94, category: '跳绳', tag: '爆款' },
    { name: '智能计数跳绳 无绳', price: 79, sold: 3127, quality: 92, category: '跳绳', tag: '热卖' },
    { name: '加重跳绳 1kg 锻炼', price: 69, sold: 2043, quality: 90, category: '跳绳', tag: '实惠' },
    { name: '健腹轮静音回弹', price: 75, sold: 2578, quality: 91, category: '跳绳', tag: '推荐' },
    { name: '俯卧撑支架 S 型', price: 49, sold: 3316, quality: 87, category: '护具', tag: '实惠' },
    { name: '门上单杠引体向上', price: 129, sold: 1742, quality: 90, category: '护具', tag: '热卖' },
    { name: '折叠仰卧板多功能', price: 319, sold: 854, quality: 93, category: '护具', tag: '推荐' },
    { name: '筋膜枪深层放松专业', price: 399, sold: 1687, quality: 96, category: '护具', tag: '新品' },
    { name: '泡沫轴高密度 60cm', price: 59, sold: 2953, quality: 89, category: '护具', tag: '实惠' },
    { name: '瑜伽垫加宽 183cm', price: 89, sold: 5104, quality: 92, category: '护具', tag: '爆款' }
  ]

  private strengths: StrengthItem[] = [
    { name: '可调哑铃 24kg', weight: 24, material: '铸铁包胶', price: 399, stock: 168 },
    { name: '六角哑铃 12kg 对', weight: 12, material: '电镀钢', price: 189, stock: 245 },
    { name: '奥杆杠铃 20kg', weight: 20, material: '合金钢', price: 359, stock: 92 },
    { name: '直杆杠铃杆 10kg', weight: 10, material: '烤漆钢', price: 199, stock: 134 },
    { name: '壶铃 8kg', weight: 8, material: '铸铁', price: 99, stock: 210 },
    { name: '壶铃 20kg', weight: 20, material: '竞技钢', price: 239, stock: 76 },
    { name: '杠铃片 25kg 对', weight: 25, material: '包胶铸铁', price: 299, stock: 58 },
    { name: '深蹲架 60kg 套装', weight: 60, material: '碳钢', price: 1299, stock: 32 },
    { name: '可调哑铃 40kg', weight: 40, material: '快锁结构', price: 799, stock: 41 },
    { name: '六角哑铃 30kg 对', weight: 30, material: '电镀钢', price: 449, stock: 65 },
    { name: '腕沙袋 5kg 对', weight: 5, material: '尼龙+铁砂', price: 69, stock: 380 },
    { name: '负重背心 15kg', weight: 15, material: '牛津布', price: 269, stock: 118 }
  ]

  private cardios: CardioItem[] = [
    { name: '折叠跑步机 X3 家用', power: 2, maxSpeed: 16, price: 2299, programs: 12 },
    { name: '商用跑步机 T900', power: 4, maxSpeed: 20, price: 6999, programs: 24 },
    { name: '磁控椭圆机 E20', power: 1, maxSpeed: 12, price: 1799, programs: 8 },
    { name: '前置飞轮椭圆机 E50', power: 2, maxSpeed: 15, price: 3499, programs: 16 },
    { name: '风阻划船机 R1', power: 2, maxSpeed: 18, price: 2899, programs: 10 },
    { name: '磁阻划船机 R2 折叠', power: 1, maxSpeed: 14, price: 1599, programs: 8 },
    { name: '立式磁控健身车 C10', power: 1, maxSpeed: 12, price: 1299, programs: 6 },
    { name: '动感单车 S5 自发电', power: 2, maxSpeed: 20, price: 2499, programs: 18 },
    { name: '爬楼机 Stair3 商用', power: 3, maxSpeed: 16, price: 5499, programs: 15 },
    { name: '滑雪机 SkiFit 双轨', power: 2, maxSpeed: 17, price: 3999, programs: 12 }
  ]

  private apparels: ApparelItem[] = [
    { name: '速干训练短袖 男款', size: 175, fabric: '聚酯速干', price: 99, rating: 96 },
    { name: '压缩 leggings 女款', size: 165, fabric: '锦氨压缩', price: 159, rating: 97 },
    { name: '五分压缩裤 黑', size: 180, fabric: '锦氨压缩', price: 129, rating: 94 },
    { name: '无袖健身上衣 背心', size: 170, fabric: '网眼透气', price: 79, rating: 92 },
    { name: '训练连帽卫衣 加绒', size: 185, fabric: '棉混抓绒', price: 199, rating: 95 },
    { name: '运动短裤 随行袋', size: 175, fabric: '四向弹力', price: 89, rating: 93 },
    { name: '综训鞋 训练旗舰', size: 260, fabric: '一体织鞋面', price: 399, rating: 98 },
    { name: '硬拉袜 加厚防滑', size: 265, fabric: '毛巾底', price: 49, rating: 90 },
    { name: '举重鞋 木跟稳定', size: 255, fabric: '牛皮+木跟', price: 599, rating: 97 },
    { name: '训练手套 全指防磨', size: 230, fabric: '牛皮掌贴', price: 69, rating: 91 }
  ]

  private coaches: CoachItem[] = [
    { name: '陈教练 · 力量举', specialty: '三大项训练', years: 8, sessions: 1206, price: 320, score: 98 },
    { name: 'Lisa · 体态矫正', specialty: '圆肩驼背改善', years: 6, sessions: 987, price: 320,score: 97 },
    { name: '王教练 · 增肌', specialty: '瘦人增重', years: 10, sessions: 1543, price: 320,score: 99 },
    { name: '阿杰 · HIIT', specialty: '高效燃脂', years: 5, sessions: 876, price: 320,score: 95 },
    { name: '李教练 · 产后修复', specialty: '腹直肌修复', years: 7, sessions: 1054,price: 320, score: 98 },
    { name: 'Michael · 拳击', specialty: '搏击燃脂', years: 9, sessions: 1320, price: 320,score: 96 },
    { name: '赵教练 · 康复', specialty: '膝腰疼痛缓解', years: 12, sessions: 1678, price: 320,score: 99 },
    { name: '小柔 · 普拉提', specialty: '核心塑形', years: 4, sessions: 654, price: 320,score: 94 },
    { name: '孙教练 · 功能性', specialty: '体能综合素质', years: 6, sessions: 921, price: 320,score: 95 },
    { name: '大伟 · 街健', specialty: '自重训练', years: 7, sessions: 1130, price: 320,score: 96 }
  ]

  private messages: MessageItem[] = [
    { title: '预约成功', content: '您的私教体验课已预约周三 19:00', time: '11:02', unread: 1 },
    { title: '租期提醒', content: '哑铃租赁剩余 3 天,可续租或归还', time: '10:15', unread: 1 },
    { title: '开票通知', content: '您申请的电子发票已开具并发送邮箱', time: '昨天', unread: 0 },
    { title: '直播提醒', content: '今晚 8 点世界冠军直播硬拉教学', time: '昨天', unread: 2 },
    { title: '降价提醒', content: '您关注的筋膜枪降价 100 元', time: '08-22', unread: 0 },
    { title: '签到成功', content: '连续打卡 21 天,获得运动毛巾一条', time: '08-21', unread: 1 },
    { title: '物流更新', content: '您的跑步机已预约周五送货安装', time: '08-20', unread: 0 },
    { title: '课程上新', content: 'HIIT 冲刺营第 5 期开放报名', time: '08-19', unread: 0 },
    { title: '售后进度', content: '您的弹力带换新申请已寄出', time: '08-18', unread: 1 },
    { title: '体测报告', content: '7 月体测报告已生成,体脂下降 2%', time: '08-17', unread: 0 },
    { title: '活动预告', content: '健身节大促 9 月 1 日开启,定金翻倍', time: '08-16', unread: 0 },
    { title: '社群动态', content: '硬核力量圈新增 128 位成员', time: '08-15', unread: 0 }
  ]

  private maxWeight(): number {
    let m: number = 0
    this.strengths.forEach((s: StrengthItem) => {
      if (s.weight > m) {
        m = s.weight
      }
    })
    return m
  }

  private maxPower(): number {
    let m: number = 0
    this.cardios.forEach((c: CardioItem) => {
      if (c.power > m) {
        m = c.power
      }
    })
    return m
  }

  private maxSessions(): number {
    let m: number = 0
    this.coaches.forEach((c: CoachItem) => {
      if (c.sessions > m) {
        m = c.sessions
      }
    })
    return m
  }

  private maxSpeed(): number {
    let m: number = 0
    this.cardios.forEach((c: CardioItem) => {
      if (c.maxSpeed > m) {
        m = c.maxSpeed
      }
    })
    return m
  }

  private selName(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.name
  }

  private selPrice(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.price
  }

  private selCategory(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.category
  }

  private selTag(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.tag
  }

  private selQuality(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.quality
  }

  private selSold(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.sold
  }

  build() {
    Stack() {
      Column() {
        this.headerBar()
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tabMarket()
            } else if (this.currentTab === 1) {
              this.tabStrength()
            } else if (this.currentTab === 2) {
              this.tabCardio()
            } else if (this.currentTab === 3) {
              this.tabApparel()
            } else if (this.currentTab === 4) {
              this.tabCoach()
            } else if (this.currentTab === 5) {
              this.tabMessage()
            } else {
              this.tabMine()
            }
          }
          .width('100%')
          .padding({ left: 12, right: 12, top: 10, bottom: 10 })
        }
        .layoutWeight(1)
        .width('100%')
        .scrollBar(BarState.Off)
        this.bottomBar()
      }
      .width('100%')
      .height('100%')

      if (this.showPublish) {
        this.modalPublish()
      }
      if (this.showRent) {
        this.modalRent()
      }
      if (this.showCourse) {
        this.modalCourse()
      }
      if (this.showDelete) {
        this.modalDelete()
      }
      if (this.showDetail) {
        this.modalDetail()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  headerBar() {
    Column() {
      Row() {
        Column() {
          Text('运动健身仓')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
        }
        Column() {
          Row() {
            Text('搜索器械 / 课程 / 教练')
              .fontSize(12)
              .fontColor('#9E9E9E')
          }
          .width('100%')
          .height(30)
          .backgroundColor('#FFFFFF')
          .borderRadius(15)
          .justifyContent(FlexAlign.Start)
          .padding({ left: 14 })
        }
        .layoutWeight(1)
        .margin({ left: 12, right: 12 })
        Column() {
          Text('消息')
            .fontSize(13)
            .fontColor('#FFFFFF')
        }
        .onClick(() => {
          this.currentTab = 5
        })
      }
      .width('100%')
      .height(50)
      .padding({ left: 14, right: 14 })
      .alignItems(VerticalAlign.Center)

      Row() {
        Text('哑铃日 · 力量专区 5 折起')
          .fontSize(12)
          .fontColor('#00BFA5')
          .fontWeight(FontWeight.Medium)
        Text('器械租赁上线')
          .fontSize(12)
          .fontColor('#00BFA5')
          .margin({ left: 14 })
        Text('私教体验 0 元'
        )
          .fontSize(12)
          .fontColor('#00BFA5')
          .margin({ left: 14 })
      }
      .width('100%')
      .padding({ left: 14, bottom: 8 })
      .justifyContent(FlexAlign.Start)
    }
    .width('100%')
    .backgroundColor('#212121')
  }

  @Builder
  bottomBar() {
    Row() {
      ForEach(this.tabs, (tab: string, index: number) => {
        Column() {
          Column()
            .width(index === this.currentTab ? 18 : 6)
            .height(3)
            .borderRadius(2)
            .backgroundColor(index === this.currentTab ? '#00BFA5' : 'rgba(255,255,255,0.3)')
          Text(tab)
            .fontSize(11)
            .fontColor(index === this.currentTab ? '#00BFA5' : 'rgba(255,255,255,0.6)')
            .margin({ top: 5 })
        }
        .layoutWeight(1)
        .height(54)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.currentTab = index
        })
      })
    }
    .width('100%')
    .backgroundColor('#212121')
  }

  @Builder
  tabMarket() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('夏季塑形冲刺季')
              .fontSize(19)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('器械直降 · 满1000减150 · 12期免息')
              .fontSize(12)
              .fontColor('#B2DFDB')
              .margin({ top: 6 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          Text('租')
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00BFA5')
            .width(64)
            .height(64)
            .textAlign(TextAlign.Center)
            .backgroundColor('#FFFFFF')
            .borderRadius(32)
            .scale({ x: 1.06, y: 1.06 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            .onClick(() => {
              this.showRent = true
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .padding(18)
      .backgroundColor('#D32F2F')
      .borderRadius(14)

      Scroll() {
        Row() {
          ForEach(this.types, (t: string, idx: number) => {
            Text(t)
              .fontSize(12)
              .fontColor(this.selectedType === idx ? '#FFFFFF' : '#616161')
              .backgroundColor(this.selectedType === idx ? '#D32F2F' : '#FFFFFF')
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .margin({ right: 8 })
              .onClick(() => {
                this.selectedType = idx
              })
          })
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .margin({ top: 12 })

      Column() {
        ForEach(this.gears, (g: GearItem) => {
          Row() {
            Column()
              .width(10)
              .height(96)
              .backgroundColor(g.quality > 92 ? '#00BFA5' : '#FFB74D')
              .borderRadius(5)
            Column() {
              Text(g.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#212121')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text(g.category)
                  .fontSize(10)
                  .fontColor('#D32F2F')
                  .backgroundColor('#FDE7E7')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text(g.tag)
                  .fontSize(10)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#212121')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .margin({ left: 6 })
              }
              .margin({ top: 6 })

              Row() {
                Stack({ alignContent: Alignment.Start }) {
                  Column()
                    .width('100%')
                    .height(6)
                    .backgroundColor('#EEEEEE')
                    .borderRadius(3)
                  Column()
                    .width(g.quality + '%')
                    .height(6)
                    .backgroundColor('#00BFA5')
                    .borderRadius(3)
                }
                .width(120)
                Text('品质 ' + g.quality)
                  .fontSize(10)
                  .fontColor('#00BFA5')
                  .margin({ left: 6 })
              }
              .margin({ top: 8 })

              Row() {
                Text('¥' + g.price)
                  .fontSize(17)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#D32F2F')
                Text(g.sold + '人付款')
                  .fontSize(11)
                  .fontColor('#9E9E9E')
                  .margin({ left: 8 })
                Column().layoutWeight(1)
                Text('租')
                  .fontSize(11)
                  .fontColor('#00BFA5')
                  .backgroundColor('#E0F2F1')
                  .borderRadius(10)
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .onClick(() => {
                    this.showRent = true
                  })
              }
              .width('100%')
              .alignItems(VerticalAlign.Center)
              .margin({ top: 8 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            this.selectedItem = g
            this.showDetail = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabStrength() {
    Column() {
      Column() {
        Row() {
          Text('力量器械 · 重量分布')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('单位 kg')
            .fontSize(11)
            .fontColor('#B2DFDB')
            .margin({ left: 10 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Row() {
          ForEach(this.strengths, (s: StrengthItem) => {
            Column() {
              Column()
                .width(15)
                .height(s.weight / this.maxWeight() * 88)
                .backgroundColor('#00BFA5')
                .borderRadius({ topLeft: 3, topRight: 3 })
            }
            .height(92)
            .justifyContent(FlexAlign.End)
            .margin({ right: 5 })
          })
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#37474F')
      .borderRadius(12)
      .margin({ top: 4 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.strengths, (s: StrengthItem) => {
          Column() {
            Row() {
              Text(s.weight + 'kg')
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D32F2F')
              Text(s.material)
                .fontSize(10)
                .fontColor('#9E9E9E')
                .margin({ left: 8 })
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

            Text(s.name)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#212121')
              .margin({ top: 6 })
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })

            Row() {
              Text('¥' + s.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D32F2F')
              Text('库存' + s.stock)
                .fontSize(10)
                .fontColor('#9E9E9E')
                .margin({ left: 8 })
            }
            .width('100%')
            .margin({ top: 6 })

            Row() {
              Text('按天租')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#00BFA5')
                .borderRadius(10)
                .padding({ left: 12, right: 12, top: 4, bottom: 4 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
                .onClick(() => {
                  this.selectedDays = 0
                  this.showRent = true
                })
              Text('发布同款')
                .fontSize(11)
                .fontColor('#616161')
                .backgroundColor('#FAFAFA')
                .borderRadius(10)
                .padding({ left: 12, right: 12, top: 4, bottom: 4 })
                .margin({ left: 8 })
                .onClick(() => {
                  this.showPublish = true
                })
            }
            .margin({ top: 8 })
          }
          .width('48%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(12)
          .margin({ top: 10 })
          .alignItems(HorizontalAlign.Start)
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabCardio() {
    Column() {
      Row() {
        Column() {
          Text('有氧器械馆')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('电机马力与极速参数一览')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)

        Text('免费安装')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#D32F2F')
          .borderRadius(10)
          .padding({ left: 10, right: 10, top: 6, bottom: 6 })
          .scale({ x: 1.05, y: 1.05 })
          .animation({ duration: 900, iterations: -1, curve: Curve.EaseInOut })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 4 })
      .alignItems(VerticalAlign.Center)

      Column() {
        ForEach(this.cardios, (c: CardioItem) => {
          Column() {
            Row() {
              Column() {
                Text(c.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#212121')
                Row() {
                  Text(c.power + 'HP 电机')
                    .fontSize(10)
                    .fontColor('#00BFA5')
                    .backgroundColor('#E0F2F1')
                    .borderRadius(4)
                    .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  Text(c.programs + '种程序')
                    .fontSize(10)
                    .fontColor('#616161')
                    .backgroundColor('#FAFAFA')
                    .borderRadius(4)
                    .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                    .margin({ left: 6 })
                }
                .margin({ top: 5 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)

              Text('¥' + c.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D32F2F')
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

            Row() {
              Text('极速')
                .fontSize(10)
                .fontColor('#9E9E9E')
              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(8)
                  .backgroundColor('#EEEEEE')
                  .borderRadius(4)
                Column()
                  .width(c.maxSpeed / this.maxSpeed() * 100 + '%')
                  .height(8)
                  .backgroundColor('#D32F2F')
                  .borderRadius(4)
              }
              .layoutWeight(1)
              .margin({ left: 8 })
              Text(c.maxSpeed + 'km/h')
                .fontSize(10)
                .fontColor('#D32F2F')
                .fontWeight(FontWeight.Bold)
                .margin({ left: 8 })
            }
            .width('100%')
            .margin({ top: 10 })
            .alignItems(VerticalAlign.Center)

            Row() {
              Text(c.power / this.maxPower() * 100 + '%')
                .fontSize(10)
                .fontColor('#00BFA5')
              Row() {
                ForEach([1, 2, 3, 4, 5], (n: number) => {
                  Column()
                    .width(c.power >= n ? 12 : 12)
                    .height(4)
                    .borderRadius(2)
                    .backgroundColor(c.power >= n ? '#00BFA5' : '#EEEEEE')
                    .margin({ left: 3 })
                })
              }
              .margin({ left: 6 })
              Column().layoutWeight(1)
              Text('租赁体验')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#37474F')
                .borderRadius(10)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .onClick(() => {
                  this.selectedDays = 0
                  this.showRent = true
                })
            }
            .width('100%')
            .margin({ top: 8 })
            .alignItems(VerticalAlign.Center)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ top: 10 })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabApparel() {
    Column() {
      Column() {
        Text('尺码覆盖率 (%)')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#212121')
        Row() {
          ForEach(this.apparels, (a: ApparelItem) => {
            Column() {
              Column()
                .width(16)
                .height(a.rating)
                .backgroundColor('#FF8A65')
                .borderRadius({ topLeft: 3, topRight: 3 })
            }
            .height(104)
            .justifyContent(FlexAlign.End)
            .margin({ right: 5 })
          })
        }
        .width('100%')
        .margin({ top: 12 })
        Text('橙色柱高 = 好评率 · 覆盖 160-260 码段')
          .fontSize(10)
          .fontColor('#9E9E9E')
          .margin({ top: 8 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 4 })

      Column() {
        ForEach(this.apparels, (a: ApparelItem) => {
          Row() {
            Column() {
              Text(a.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#212121')
              Text(a.fabric + ' · 码数 ' + a.size)
                .fontSize(11)
                .fontColor('#9E9E9E')
                .margin({ top: 3 })
              Row() {
                Text('好评')
                  .fontSize(10)
                  .fontColor('#9E9E9E')
                Stack({ alignContent: Alignment.Start }) {
                  Column()
                    .width('100%')
                    .height(6)
                    .backgroundColor('#EEEEEE')
                    .borderRadius(3)
                  Column()
                    .width(a.rating + '%')
                    .height(6)
                    .backgroundColor('#FF8A65')
                    .borderRadius(3)
                }
                .width(110)
                .margin({ left: 6 })
              }
              .margin({ top: 6 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Column() {
              Text('¥' + a.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D32F2F')
              Text('选码')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#212121')
                .borderRadius(10)
                .padding({ left: 12, right: 12, top: 4, bottom: 4 })
                .margin({ top: 6 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ top: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            this.qty = 1
            this.showRent = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabCoach() {
    Column() {
      Column() {
        Row() {
          Text('金牌私教团')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('首课 0 元')
            .fontSize(11)
            .fontColor('#00E5FF')
            .margin({ left: 10 })
            .scale({ x: 1.08, y: 1.08 })
            .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Row() {
          ForEach(this.coaches, (c: CoachItem) => {
            Column() {
              Column()
                .width(14)
                .height(c.sessions / this.maxSessions() * 76)
                .backgroundColor('#00BFA5')
                .borderRadius({ topLeft: 3, topRight: 3 })
            }
            .height(80)
            .justifyContent(FlexAlign.End)
            .margin({ right: 5 })
          })
        }
        .width('100%')
        .margin({ top: 10 })
        Text('累计授课节数 (节)')
          .fontSize(10)
          .fontColor('#B2DFDB')
          .margin({ top: 6 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#212121')
      .borderRadius(12)
      .margin({ top: 4 })

      Column() {
        ForEach(this.coaches, (c: CoachItem) => {
          Row() {
            Column() {
              Text(c.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#212121')
              Text(c.specialty + ' · 从业' + c.years + '年')
                .fontSize(11)
                .fontColor('#9E9E9E')
                .margin({ top: 3 })
              Row() {
                Text('好评 ' + c.score + '%')
                  .fontSize(10)
                  .fontColor('#00BFA5')
                Stack({ alignContent: Alignment.Start }) {
                  Column()
                    .width('100%')
                    .height(6)
                    .backgroundColor('#EEEEEE')
                    .borderRadius(3)
                  Column()
                    .width(c.score + '%')
                    .height(6)
                    .backgroundColor('#00BFA5')
                    .borderRadius(3)
                }
                .width(80)
                .margin({ left: 6 })
              }
              .margin({ top: 6 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Column() {
              Text('¥' + c.price + '/节')
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D32F2F')
              Text('约课')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#D32F2F')
                .borderRadius(10)
                .padding({ left: 12, right: 12, top: 4, bottom: 4 })
                .margin({ top: 6 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ top: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            this.selectedLevel = 0
            this.showCourse = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabMessage() {
    Column() {
      Row() {
        Text('消息中心')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#212121')
        Text('6条未读')
          .fontSize(11)
          .fontColor('#D32F2F')
          .margin({ left: 8 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 4 })

      Column() {
        ForEach(this.messages, (m: MessageItem) => {
          Row() {
            Column() {
              Text(m.title.substring(0, 1))
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
            }
            .width(44)
            .height(44)
            .backgroundColor('#37474F')
            .borderRadius(10)
            .justifyContent(FlexAlign.Center)

            Column() {
              Row() {
                Text(m.title)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#212121')
                if (m.unread > 0) {
                  Text(m.unread + '')
                    .fontSize(10)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#D32F2F')
                    .borderRadius(8)
                    .padding({ left: 6, right: 6, top: 1, bottom: 1 })
                    .margin({ left: 6 })
                    .scale({ x: 1.1, y: 1.1 })
                    .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
                }
              }
              Text(m.content)
                .fontSize(12)
                .fontColor('#9E9E9E')
                .margin({ top: 3 })
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })

            Text(m.time)
              .fontSize(11)
              .fontColor('#BDBDBD')
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ top: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            m.unread = 0
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabMine() {
    Column() {
      Row() {
        Column()
          .width(64)
          .height(64)
          .backgroundColor('#B2DFDB')
          .borderRadius(10)
        Column() {
          Text('铁馆常客_Ken')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('黑铁会员 · 训练 428 小时')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
        .padding({ left: 12 })
        Text('周训练计划')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#00BFA5')
          .borderRadius(12)
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .scale({ x: 1.05, y: 1.05 })
          .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
          .onClick(() => {
            this.showCourse = true
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 4 })
      .alignItems(VerticalAlign.Center)

      Row() {
        Column() {
          Text('12')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('租赁中')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('9')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('优惠券')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('86')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('收藏夹')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('21')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('打卡天')
            .fontSize(11)
            .fontColor('#9E9E9E')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding({ top: 14, bottom: 14 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 10 })

      Column() {
        ForEach(['我的订单', '租赁记录', '课程表', '体测档案', '客服中心', '清除浏览记录'], (m: string, idx: number) => {
          Row() {
            Text(m)
              .fontSize(14)
              .fontColor('#212121')
            Column().layoutWeight(1)
            Text('>')
              .fontSize(14)
              .fontColor('#BDBDBD')
          }
          .width('100%')
          .padding({ top: 14, bottom: 14 })
          .border({ width: 1, color: '#F5F5F5' })
          .onClick(() => {
            if (idx === 5) {
              this.showDelete = true
            }
          })
        })
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .padding({ left: 14, right: 14 })
      .margin({ top: 10 })
    }
    .width('100%')
  }

  @Builder
  modalPublish() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('发布二手器械')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('关闭')
            .fontSize(13)
            .fontColor('#9E9E9E')
            .padding(6)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Column() {
          Text('器械名称')
            .fontSize(13)
            .fontColor('#616161')
          TextInput({ placeholder: '例如:可调节哑铃 24kg 九成新' })
            .height(40)
            .fontSize(13)
            .backgroundColor('#F5F5F5')
            .borderRadius(8)
            .margin({ top: 6 })
            .onChange((value: string) => {
              this.inputName = value
            })
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('器械类型')
            .fontSize(13)
            .fontColor('#616161')
          Row() {
            ForEach(this.types, (t: string, idx: number) => {
              Text(t)
                .fontSize(12)
                .fontColor(this.selectedType === idx ? '#FFFFFF' : '#616161')
                .backgroundColor(this.selectedType === idx ? '#D32F2F' : '#F5F5F5')
                .borderRadius(14)
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedType = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('重量档位')
            .fontSize(13)
            .fontColor('#616161')
          Row() {
            ForEach(this.weights, (w: string, idx: number) => {
              Text(w)
                .fontSize(12)
                .fontColor(this.selectedWeight === idx ? '#FFFFFF' : '#616161')
                .backgroundColor(this.selectedWeight === idx ? '#00BFA5' : '#F5F5F5')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedWeight = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Text('预估回收价:¥' + ((this.selectedType + 1) * 88 + (this.selectedWeight + 1) * 25))
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#00BFA5')
          .margin({ top: 18 })
          .scale({ x: 1.04, y: 1.04 })
          .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })

        Text('确认发布')
          .fontSize(15)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(44)
          .textAlign(TextAlign.Center)
          .backgroundColor('#D32F2F')
          .borderRadius(22)
          .margin({ top: 18 })
          .onClick(() => {
            this.showPublish = false
          })
      }
      .width('100%')
      .constraintSize({ maxHeight: '80%' })
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showPublish = false
    })
  }

  @Builder
  modalRent() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('器械租赁')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('押金 ¥' + (this.selectedDays + 1) * 100)
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00BFA5')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text('租期选择')
            .fontSize(13)
            .fontColor('#616161')
          Row() {
            ForEach(this.dayOptions, (d: string, idx: number) => {
              Text(d)
                .fontSize(12)
                .fontColor(this.selectedDays === idx ? '#FFFFFF' : '#616161')
                .backgroundColor(this.selectedDays === idx ? '#37474F' : '#F5F5F5')
                .borderRadius(8)
                .padding({ left: 16, right: 16, top: 9, bottom: 9 })
                .margin({ right: 8, top: 10 })
                .onClick(() => {
                  this.selectedDays = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Row() {
          Text('数量')
            .fontSize(13)
            .fontColor('#616161')
          Column().layoutWeight(1)
          Row() {
            Text('-')
              .fontSize(16)
              .fontColor('#616161')
              .width(30)
              .height(30)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5F5F5')
              .borderRadius(6)
              .onClick(() => {
                if (this.qty > 1) {
                  this.qty -= 1
                }
              })
            Text(this.qty + '')
              .fontSize(14)
              .fontColor('#212121')
              .width(40)
              .height(30)
              .textAlign(TextAlign.Center)
            Text('+')
              .fontSize(16)
              .fontColor('#616161')
              .width(30)
              .height(30)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5F5F5')
              .borderRadius(6)
              .onClick(() => {
                if (this.qty < 9) {
                  this.qty += 1
                }
              })
          }
        }
        .width('100%')
        .margin({ top: 18 })
        .alignItems(VerticalAlign.Center)

        Text('预计租金:¥' + ((this.selectedDays + 1) * 15 * this.qty) + ' (到期可退押金)')
          .fontSize(14)
          .fontColor('#D32F2F')
          .fontWeight(FontWeight.Bold)
          .margin({ top: 16 })

        Row() {
          Text('到店自提')
            .fontSize(14)
            .fontColor('#616161')
            .layoutWeight(1)
            .height(42)
            .textAlign(TextAlign.Center)
            .backgroundColor('#F5F5F5')
            .borderRadius(21)
            .onClick(() => {
              this.showRent = false
            })
          Text('送货上门')
            .fontSize(14)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Bold)
            .layoutWeight(1)
            .height(42)
            .textAlign(TextAlign.Center)
            .backgroundColor('#D32F2F')
            .borderRadius(21)
            .margin({ left: 10 })
            .onClick(() => {
              this.showRent = false
            })
        }
        .width('100%')
        .margin({ top: 18 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '80%' })
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showRent = false
    })
  }

  @Builder
  modalCourse() {
    Column() {
      Column() {
        Text('预约私教体验课')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#212121')
          .margin({ top: 18 })

        Column() {
          Text('训练目标')
            .fontSize(13)
            .fontColor('#616161')
          Row() {
            ForEach(this.levels, (l: string, idx: number) => {
              Text(l)
                .fontSize(12)
                .fontColor(this.selectedLevel === idx ? '#FFFFFF' : '#616161')
                .backgroundColor(this.selectedLevel === idx ? '#D32F2F' : '#F5F5F5')
                .borderRadius(14)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 10 })
                .onClick(() => {
                  this.selectedLevel = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Row() {
          Text('课时')
            .fontSize(13)
            .fontColor('#616161')
          Column().layoutWeight(1)
          Row() {
            Text('-')
              .fontSize(15)
              .fontColor('#616161')
              .width(28)
              .height(28)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5F5F5')
              .borderRadius(6)
              .onClick(() => {
                if (this.qty > 1) {
                  this.qty -= 1
                }
              })
            Text(this.qty + '')
              .fontSize(14)
              .fontColor('#212121')
              .width(36)
              .height(28)
              .textAlign(TextAlign.Center)
            Text('+')
              .fontSize(15)
              .fontColor('#616161')
              .width(28)
              .height(28)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5F5F5')
              .borderRadius(6)
              .onClick(() => {
                if (this.qty < 20) {
                  this.qty += 1
                }
              })
          }
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(VerticalAlign.Center)

        Text('首课免费 · 合计:¥' + (this.qty - 1) * 320)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#D32F2F')
          .margin({ top: 18 })
          .scale({ x: 1.04, y: 1.04 })
          .animation({ duration: 650, iterations: -1, curve: Curve.EaseInOut })

        Text('提交预约')
          .fontSize(14)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(42)
          .textAlign(TextAlign.Center)
          .backgroundColor('#D32F2F')
          .borderRadius(21)
          .margin({ top: 20, bottom: 18 })
          .onClick(() => {
            this.showCourse = false
          })
      }
      .width('86%')
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .padding({ left: 18, right: 18 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showCourse = false
    })
  }

  @Builder
  modalDelete() {
    Column() {
      Column() {
        Text('!')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .width(56)
          .height(56)
          .textAlign(TextAlign.Center)
          .backgroundColor('#D32F2F')
          .borderRadius(28)
        Text('确认清除')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#212121')
          .margin({ top: 14 })
        Text('清除后浏览记录不可恢复,是否继续?')
          .fontSize(13)
          .fontColor('#9E9E9E')
          .margin({ top: 8 })

        Row() {
          Text('取消')
            .fontSize(14)
            .fontColor('#616161')
            .layoutWeight(1)
            .height(40)
            .textAlign(TextAlign.Center)
            .backgroundColor('#F5F5F5')
            .borderRadius(20)
            .onClick(() => {
              this.showDelete = false
            })
          Text('确认清除')
            .fontSize(14)
            .fontColor('#FFFFFF')
            .layoutWeight(1)
            .height(40)
            .textAlign(TextAlign.Center)
            .backgroundColor('#D32F2F')
            .borderRadius(20)
            .margin({ left: 10 })
            .onClick(() => {
              this.showDelete = false
              this.showDetail = false
            })
        }
        .width('100%')
        .margin({ top: 22, bottom: 20 })
      }
      .width('78%')
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .padding(20)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showDelete = false
    })
  }

  @Builder
  modalDetail() {
    Row() {
      Scroll() {
        Column() {
          Column()
            .width('100%')
            .height(180)
            .backgroundColor('#B2DFDB')
            .borderRadius({ topLeft: 16, bottomLeft: 16 })

          Column() {
            Row() {
              Text('¥' + this.selPrice())
                .fontSize(24)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D32F2F')
              Text('品质分 ' + this.selQuality())
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#00BFA5')
                .borderRadius(10)
                .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                .margin({ left: 10 })
                .scale({ x: 1.06, y: 1.06 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            }
            .alignItems(VerticalAlign.Center)

            Text(this.selName())
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#212121')
              .margin({ top: 8 })

            Row() {
              Text(this.selCategory())
                .fontSize(10)
                .fontColor('#D32F2F')
                .backgroundColor('#FDE7E7')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              Text(this.selTag())
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#212121')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .margin({ left: 6 })
            }
            .margin({ top: 8 })

            Column() {
              Text('品质评分 ' + this.selQuality() + '%')
                .fontSize(12)
                .fontColor('#616161')
              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(8)
                  .backgroundColor('#EEEEEE')
                  .borderRadius(4)
                Column()
                  .width(this.selQuality() + '%')
                  .height(8)
                  .backgroundColor('#00BFA5')
                  .borderRadius(4)
              }
              .width('100%')
              .margin({ top: 6 })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 14 })

            Row() {
              Text('已售 ' + this.selSold())
                .fontSize(12)
                .fontColor('#9E9E9E')
              Text('包邮到家')
                .fontSize(12)
                .fontColor('#9E9E9E')
                .margin({ left: 12 })
              Text('以租代购')
                .fontSize(12)
                .fontColor('#00BFA5')
                .margin({ left: 12 })
            }
            .margin({ top: 12 })

            Row() {
              Text('租赁')
                .fontSize(14)
                .fontColor('#00BFA5')
                .fontWeight(FontWeight.Bold)
                .layoutWeight(1)
                .height(40)
                .textAlign(TextAlign.Center)
                .backgroundColor('#E0F2F1')
                .borderRadius(20)
                .onClick(() => {
                  this.showRent = true
                })
              Text('购买')
                .fontSize(14)
                .fontColor('#FFFFFF')
                .fontWeight(FontWeight.Bold)
                .layoutWeight(1)
                .height(40)
                .textAlign(TextAlign.Center)
                .backgroundColor('#D32F2F')
                .borderRadius(20)
                .margin({ left: 10 })
                .onClick(() => {
                  this.showPublish = false
                })
            }
            .width('100%')
            .margin({ top: 18 })

            Row() {
              Text('删除该记录')
                .fontSize(13)
                .fontColor('#D32F2F')
                .padding({ top: 12 })
            }
            .width('100%')
            .onClick(() => {
              this.showDelete = true
            })
          }
          .width('100%')
          .padding(16)
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
      }
      .width('78%')
      .height('100%')
      .backgroundColor('#FFFFFF')

      Column()
        .layoutWeight(1)
        .height('100%')
        .onClick(() => {
          this.showDetail = false
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showDetail = false
    })
  }
}


总结

在这里插入图片描述

本文以一个运动健身仓应用为案例,全面剖析了基于HarmonyOS ArkTS API 24构建垂直行业电商应用的技术方案。从六种接口类型的数据模型设计,到Stack根容器的层叠布局架构,再到七个@Builder页面组件和五套弹窗交互的实现,充分展示了ArkTS在运动健身这一垂直领域的业务建模能力和交互设计灵活性。应用采用深灰色(#212121)作为主色调,搭配红色(#D32F2F)和青绿色(#00BFA5)作为强调色,形成了硬朗的运动风格视觉体系,与运动健身的硬核气质高度契合。

在数据可视化方面,应用实现了多种创新的可视化组件。有氧器械页面的五段式马力评分条通过ForEach([1, 2, 3, 4, 5])渲染五个小方块,根据c.power >= n判断每个方块的颜色,将离散的马力数值转化为直观的视觉评分。装备集市列表的竖向品质指示条使用10像素宽的色条区分高品质和普通商品,为用户提供了快速筛选的视觉引导。各页面的柱状图分别展示重量分布、授课节数和好评率,通过maxWeight()maxPower()maxSessions()maxSpeed()四个计算方法提供比例基准,确保可视化数据的相对比例正确。这些可视化组件的实现完全基于ArkTS原生的基础组件和布局能力,无需引入第三方图表库。

在交互设计方面,应用实现了器械租赁和私教课程预约两大特色业务弹窗。租赁弹窗的费用体系包含押金和租金两个维度,押金公式为(selectedDays + 1) * 100,租金公式为(selectedDays + 1) * 15 * qty,并到店自提和送货上门两种配送方式。课程预约弹窗采用"首课免费"的营销逻辑,费用公式为(qty - 1) * 320,课时上限达20节。发布弹窗的回收价计算公式(selectedType + 1) * 88 + (selectedWeight + 1) * 25综合了器械类型和重量两个维度。这些基于索引的动态费用计算方式简洁高效,配合缩放呼吸动画实时反馈价格变化,提升了用户的操作感知和决策效率。整体而言,该应用为HarmonyOS生态中的垂直行业电商开发提供了有价值的参考范例,展示了如何在保持代码结构清晰的同时实现丰富的业务功能和多元化的数据可视化。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐