引言

在这里插入图片描述

赛道驾驶文化在国内汽车运动领域正经历从专业赛车圈向性能车主群体的快速渗透。随着上海国际赛车场、浙江国际赛车场、珠海国际赛车场和北京金港等场地的赛道日开放频率提升,以及半热熔胎、绞牙避震、阶段程序等性能改装件的市场需求增长,赛道爱好者亟需一个集赛道日报名、圈速排名、性能件选购、驾驶培训预约和车队社区互动于一体的综合服务平台。本文深入剖析一个基于HarmonyOS ArkTS声明式UI框架构建的赛道日体验馆全场景应用,该应用采用汽车之家App风格设计语言,以赛道红、沥青黑、计时黄、浅灰底、警示红五色配色体系,打造了一个涵盖赛道日、圈速榜、性能件、驾驶培训、车队社区、消息和个人中心的完整赛道文化生态闭环。

从技术架构视角分析,该应用基于HarmonyOS 6.1.1平台与ArkTS API 24开发,充分运用了声明式UI范式的核心能力。应用入口通过@Entry@Component装饰器声明主组件Index,内部使用十余个@State装饰器管理页面级响应式状态,覆盖当前Tab索引、五个弹窗的显隐控制、用户选择标签索引(赛道、时段、规格、教练、课程等)、步进器数值、话题筛选选择等。selectedItem采用可空类型GoodsItem | null实现选中条目的安全传递。应用通过@Builder装饰器将复杂UI拆分为可复用的构建函数——headerBuilderbottomBuilder封装通用头部和底部导航,tab0tab6分别构建七个功能页面,addModalbuyModaltrainModaldeleteModaldetailModal分别封装五种弹窗交互。这种高内聚低耦合的代码组织方式使得每个功能模块可以独立开发、测试和维护。

在业务设计层面,应用围绕"赛道日体验"这一核心场景构建了多元化的数据模型。通过统一的GoodsItem接口,将赛道日场次、圈速记录、性能改装件、培训课程、社区帖子和系统消息等不同业务实体抽象为同一套数据结构。应用共包含24条赛道日与性能件混排主数据、12条车手圈速榜数据、10条性能件数据、10条驾驶培训课程数据、10条车队社区帖子和12条消息数据。赛道日场次覆盖G级组、A级组、新手场、耐力组、街道组、夜间场、雨地训练、高温测试、漂移场、拉力体验等多种类型。圈速榜展示12位车手在上赛、浙赛、珠海三大赛道的最快圈速和尾速数据。性能件涵盖阶段程序、机械增压、进气排气、中冷、飞轮、点火、燃油泵等全链路改装件。这种数据丰富度构建了一个沉浸式的赛道文化体验场景。

一、数据模型与接口定义

在这里插入图片描述

应用数据层以统一接口为核心,所有业务实体共用同一套字段结构,在ArkTS类型系统下保证类型安全并实现数据高度复用。

interface GoodsItem {
  name: string
  price: number
  desc: string
  score: number
  num: number
  tag: string
  hot: string
}

GoodsItem接口定义了七个字段:name为实体名称(如赛道日场次名称、车手名称+车型、性能件名称、课程名称等),price为价格(赛道日报名费、性能件售价、课程费用等),desc为详细描述(含保险、计时、培训、马力提升等说明),score为评分或进度(报名进度百分比、库存数、好评率等),num为辅助数值(热度值、尾速km/h、提升马力匹数、报名人数、点赞数、未读消息数等),tag为分类标签,hot为热度标记(爆/热/荐/新/王/银/铜等)或时间戳。这种统一接口设计使得全应用只需维护一套卡片渲染逻辑,在HarmonyOS ArkTS声明式框架下可直接用于ForEach列表渲染,配合@State状态管理实现数据驱动UI更新。

二、组件入口与状态管理

在这里插入图片描述

应用入口通过@Entry@Component装饰器声明主组件,内部通过多个@State变量实现响应式状态管理。

@Entry
@Component
struct Index {
  @State currentTab: number = 0
  @State showAddModal: boolean = false
  @State showBuyModal: boolean = false
  @State showTrainModal: boolean = false
  @State showDeleteModal: boolean = false
  @State showDetailModal: boolean = false
  @State selectedItem: GoodsItem | null = null
  @State chipA: number = 0
  @State chipB: number = 0
  @State stepNum: number = 1
  @State topicSel: number = 0

  private tabs: string[] = ['赛道日', '圈速榜', '性能件', '驾驶培训', '车队社区', '消息', '我的']
  private trackChips: string[] = ['上海国际赛车场', '浙江国际赛车场', '珠海国际赛车场', '北京金港']
  private timeChips: string[] = ['周六上午', '周六下午', '周日上午', '周日下午']
  private specChips: string[] = ['赛道版', '街道版', '入门版']
  private coachChips: string[] = ['陈教练', '周教练', '吴教练']
  private lessonChips: string[] = ['新手赛道课', '漂移体验课', '高级驾驶课']
  private topicChips: string[] = ['#上赛刷圈', '#GT4体验', '#刹车改装', '#走线心得', '#赛道日拼车']
  private serviceChips: string[] = ['含保险', '教练随车', '计时服务', '免费拖车', '装备租赁']

状态管理涵盖核心交互控制变量。currentTab控制Tab页面切换(0-6),五个布尔变量分别控制五种弹窗的显隐。selectedItem为可空类型,用于在详情弹窗中传递选中条目。chipAchipB为通用选择器索引——在报名弹窗中管理赛道和时段选择,在购买弹窗中管理规格选择,在培训弹窗中管理课程类型和教练选择。stepNum为步进器数值(1-10圈或1-5件或1-6课时)。topicSel是本应用特有的话题筛选状态变量,用于车队社区Tab的话题标签筛选交互。配置数据涵盖Tab标签、赛道列表(四大场地)、时段选项(周六周日各上下午)、规格选项(赛道/街道/入门版)、教练列表、课程类型、话题标签和服务保障标签。

三、辅助计算方法

在这里插入图片描述

应用封装了多个辅助方法,从状态中安全派生数据或执行业务计算。

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

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

  maxNumOf(list: GoodsItem[]): number {
    let m: number = 1
    for (let i = 0; i < list.length; i++) {
      if (list[i].num > m) {
        m = list[i].num
      }
    }
    return m
  }

  selDesc(): string {
    if (this.selectedItem !== null) {
      return this.selectedItem.desc
    }
    return ''
  }

  selScore(): number {
    if (this.selectedItem !== null) {
      return this.selectedItem.score
    }
    return 0
  }

  selNum(): number {
    if (this.selectedItem !== null) {
      return this.selectedItem.num
    }
    return 0
  }

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

  addFee(): number {
    let unit: number = this.selPrice()
    if (unit <= 0) {
      unit = 1280
    }
    return Math.floor(unit * this.stepNum)
  }

  buyFee(): number {
    let unit: number = this.selPrice()
    if (unit <= 0) {
      unit = 2180
    }
    return Math.floor(unit * this.stepNum)
  }

  trainFee(): number {
    let unit: number = this.selPrice()
    if (unit <= 0) {
      unit = 680
    }
    return Math.floor(unit * this.stepNum)
  }

selNameselTag六个方法采用空安全模式从selectedItem提取数据。maxNumOf方法遍历列表求最大值用于柱状图归一化。addFeebuyFeetrainFee三个方法分别计算赛道日报名费、性能件购买费和培训报名费,计算逻辑统一为单价 * 步进器数值,但当selPrice()返回0时(即未选择条目),使用默认单价(赛道日1280元、性能件2180元、培训680元)作为兜底值。这种防御性编程确保了即使用户在未选择具体条目的情况下打开弹窗,费用计算也不会异常。Math.floor确保费用为整数。

四、静态头部构建器

在这里插入图片描述

头部区域是应用的品牌入口,包含双行标题、赛程入口、消息徽章、搜索栏和预约入口。

  @Builder headerBuilder() {
    Column({ space: 10 }) {
      Row({ space: 14 }) {
        Column({ space: 2 }) {
          Text('赛道日体验馆')
            .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Text('TRACK DAY EXPERIENCE')
            .fontSize(9).fontColor('#BDBDBD').letterSpacing(1)
        }
        .alignItems(HorizontalAlign.Start)

        Column().layoutWeight(1)

        Column({ space: 2 }) {
          Text('📅').fontSize(18)
          Text('赛程').fontSize(8).fontColor('#BDBDBD')
        }
        .alignItems(HorizontalAlign.Center)
        .onClick(() => { this.currentTab = 0 })

        Stack({ alignContent: Alignment.TopEnd }) {
          Column({ space: 2 }) {
            Text('✉').fontSize(18)
            Text('消息').fontSize(8).fontColor('#BDBDBD')
          }
          .alignItems(HorizontalAlign.Center)

          Text('3')
            .fontSize(8).fontColor('#FFFFFF)
            .width(14).height(14).borderRadius(7)
            .backgroundColor('#E53935').textAlign(TextAlign.Center)
        }
        .width(34).height(40)
        .onClick(() => { this.currentTab = 5 })
      }
      .width('100%').alignItems(VerticalAlign.Top)

      Row({ space: 8 }) {
        Text('🏁').fontSize(14)
        Text('搜赛道 / 性能件 / 培训课').fontSize(13).fontColor('#9E9E9E')
        Column().layoutWeight(1)
        Text('搜索').fontSize(12).fontColor('#FFD600')
      }
      .width('100%').height(36).backgroundColor('#424242').borderRadius(18)
      .padding({ left: 14, right: 14, top: 0, bottom: 0 }).alignItems(VerticalAlign.Center)

      Row({ space: 6 }) {
        Text('📍').fontSize(12)
        Text('本周末开放:上海国际赛车场 · 剩余席位 18')
          .fontSize(12).fontColor('#FFD600').maxLines(1)
        Column().layoutWeight(1)
        Text('立即预约')
          .fontSize(11).fontColor('#FFFFFF')
          .padding({ left: 10, right: 10, top: 5, bottom: 5 })
          .backgroundColor('#C62828').borderRadius(10)
          .onClick(() => {
            this.selectedItem = this.mainList[0]
            this.chipA = 0
            this.chipB = 0
            this.stepNum = 1
            this.showAddModal = true
          })
      }
      .width('100%').padding({ left: 10, right: 10, top: 8, bottom: 8 })
      .backgroundColor('#111111').borderRadius(8).alignItems(VerticalAlign.Center)
    }
    .width('100%').backgroundColor('#212121')
    .padding({ left: 16, right: 16, top: 16, bottom: 12 })
  }

头部采用沥青黑#212121作为背景色,营造赛道沥青路的视觉氛围。品牌行采用双行标题设计——主标题"赛道日体验馆"白色粗体,副标题"TRACK DAY EXPERIENCE"灰色字间距1px,形成中英双语品牌标识。右侧设赛程入口和消息入口,消息入口使用Stack({ alignContent: Alignment.TopEnd })将消息图标与未读数徽章(红色圆角"3")叠加在右上角,这是该应用头部设计的特色交互元素。搜索栏使用#424242深灰背景配合圆角18px胶囊形态,右侧"搜索"文字使用计时黄#FFD600引导操作。底部预约横幅使用#111111更深背景配合定位图标,展示本周末开放赛道和剩余席位数,右侧"立即预约"按钮(赛道红背景)点击时将mainList[0]设为选中条目并打开报名弹窗——这种从头部直接触发弹窗的交互设计缩短了用户操作路径。

五、底部导航栏构建器

在这里插入图片描述

底部导航栏通过ForEach渲染七个Tab项,使用文字标签和指示条提供选中状态反馈。

  @Builder bottomBuilder() {
    Row() {
      ForEach(this.tabs, (tab: string, index: number) => {
        Column({ space: 2 }) {
          Text(tab)
            .fontSize(10)
            .fontColor(this.currentTab === index ? '#C62828' : '#999999')
          Column()
            .width(18).height(3).borderRadius(2)
            .backgroundColor(this.currentTab === index ? '#C62828' : '#FFFFFF00')
        }
        .onClick(() => { this.currentTab = index })
      }, (tab: string) => tab)
    }
    .width('100%').height(56).backgroundColor('#FFFFFF')
  }

底部导航栏的选中状态通过文字颜色变为赛道红#C62828和底部出现红色指示条实现。与驾考应用不同,该导航栏的Column未设layoutWeight(1),但ForEach渲染的七项Column通过默认权重自动等分宽度。点击事件通过修改currentTab状态变量触发页面切换。底部栏使用纯文字标签无图标,与头部的双行标题形成统一的国际化设计语言。

六、数据源:赛道日与性能件混排主列表

在这里插入图片描述

主列表是赛道日Tab的核心数据源,包含24条赛道日场次与性能件混排数据。

  private mainList: GoodsItem[] = [
    { name: '上赛 赛道日 · G级组', price: 1280, desc: '含保险 · 教练领跑 2 圈', score: 96, num: 452, tag: '热门', hot: '爆' },
    { name: '上赛 赛道日 · A级组', price: 1680, desc: '不限圈数 · 含计时服务', score: 78, num: 168, tag: '进阶', hot: '热' },
    { name: '浙赛 赛道日 · 新手场', price: 880, desc: '含 30 分钟培训 · 附赠录像', score: 62, num: 320, tag: '新手', hot: '荐' },
    { name: '珠海 赛道日 · 耐力组', price: 1480, desc: '4 小时耐力 · 双车可换手', score: 45, num: 210, tag: '耐力', hot: '热' },
    { name: '金港 赛道日 · 街道组', price: 680, desc: '街车可入 · 含头盔租赁', score: 88, num: 505, tag: '周末', hot: '爆' },
    { name: '上赛 夜间赛道日', price: 1080, desc: '18 点开跑 · 灯光全开', score: 34, num: 96, tag: '夜场', hot: '新' },
    { name: '制动套装 · 6 活塞卡钳', price: 4680, desc: '含钢喉与刹车皮 · 免费安装', score: 71, num: 389, tag: '性能件', hot: '热' },
    { name: '半热熔胎 R888R', price: 2180, desc: '单条 · 干地抓地之王', score: 90, num: 468, tag: '性能件', hot: '爆' },
    { name: '全段排气 · 阀门版', price: 5600, desc: '钛合金 · 可变阀门遥控', score: 57, num: 198, tag: '性能件', hot: '热' },
    { name: '车载计时盒 LTE 版', price: 1290, desc: '官方计时 · 圈速云端同步', score: 81, num: 365, tag: '装备', hot: '爆' }
  ]

主列表的特色在于赛道日场次与性能件的混排设计——前6条为赛道日场次(覆盖G级组到夜间场),中间穿插性能件(制动套装、半热熔胎、全段排气等),最后以计时装备收尾。每条数据包含名称(场地+组别或产品名)、价格(680-8800元)、描述(含保险/计时/培训/安装等服务说明)、进度分(报名进度或库存)、热度值、分类标签和热度标记。圈速榜列表包含12位车手在上赛、浙赛、珠海的最快圈速记录,num字段记录尾速km/h(296-218),hot字段记录排名(王/银/铜/4-12)。性能件列表包含10条全链路改装件,num字段记录提升后马力(520-285匹),覆盖程序、硬件、组合、排气、传动、点火、供油等类别。

七、Tab1 赛道日:统计卡与混排列表

赛道日Tab是应用首页,包含三格统计卡和24条赛道日与性能件混排列表。

  @Builder tab0() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Column({ space: 4 }) {
          Text('6').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#C62828')
          Text('本月场次').fontSize(11).fontColor('#757575')
        }
        .layoutWeight(1).height(78).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        .backgroundColor('#FFFFFF').borderRadius(10)

        Column({ space: 4 }) {
          Text('128').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#212121')
          Text('累计圈数').fontSize(11).fontColor('#757575')
        }
        .layoutWeight(1).height(78).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        .backgroundColor('#FFFFFF').borderRadius(10)

        Column({ space: 4 }) {
          Text('2:08.4').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFD600')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('最快圈速').fontSize(11).fontColor('#757575')
        }
        .layoutWeight(1).height(78).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        .backgroundColor('#FFFFFF').borderRadius(10)
      }
      .width('100%')

      Row({ space: 6 }) {
        Text('🔥 精选场次与性能件').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
        Column().layoutWeight(1)
        Text('全部 ›').fontSize(12).fontColor('#9E9E9E')
      }
      .width('100%')

      ForEach(this.mainList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Text(item.name.substring(0, 1)).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            .width(44).height(44).borderRadius(22).backgroundColor('#C62828').textAlign(TextAlign.Center)

          Column({ space: 5 }) {
            Row({ space: 6 }) {
              Text(item.name).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#212121').maxLines(1)
              Text(item.tag).fontSize(9).fontColor('#FFFFFF')
                .backgroundColor('#FF6D00').borderRadius(4)
                .padding({ left: 5, right: 5, top: 2, bottom: 2 })
            }
            .width('100%')

            Text(item.desc).fontSize(11).fontColor('#9E9E9E').maxLines(1)

            Row() {
              Column().width(item.score + '%').height(5).borderRadius(3).backgroundColor('#C62828')
            }
            .width('100%').height(5).borderRadius(3).backgroundColor('#EEEEEE')
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)

          Column({ space: 6 }) {
            Text('¥' + item.price).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#C62828')
            Row({ space: 4 }) {
              Text(item.hot).fontSize(9).fontColor('#FFFFFF')
                .backgroundColor('#E53935').borderRadius(4)
                .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              Text(item.num + '热度').fontSize(9).fontColor('#BDBDBD')
            }
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(10)
        .onClick(() => {
          this.selectedItem = item
          this.showDetailModal = true
        })
      }, (item: GoodsItem) => item.name)
    }
    .width('100%').padding(12).backgroundColor('#F5F5F5')
  }

三格统计卡展示本月场次(赛道红)、累计圈数(沥青黑)和最快圈速(计时黄,配有脉冲动画模拟计时器跳动效果)。混排列表通过ForEach渲染24条数据,每条卡片采用左侧圆形头像(赛道红背景白色首字)、中间信息区(名称+标签+描述+进度条)和右侧价格区的三栏布局。进度条使用赛道红填充在灰色背景上,展示报名进度或库存进度。热度标签(爆/热/荐/新)配有红色背景和脉冲动画,是该Tab的视觉焦点之一。点击卡片打开详情面板。整个Tab使用浅灰底#F5F5F5作为背景,白色卡片在浅灰底上形成层次感。

八、Tab2 圈速榜:尾速柱状图与排名网格

圈速榜Tab是该应用最具特色的数据可视化页面,包含尾速柱状图和带奖牌排名的圈速网格。

  @Builder tab1() {
    Column({ space: 14 }) {
      Text('单圈尾速对比 · km/h')
        .fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')

      Row({ space: 10 }) {
        ForEach(this.lapList, (item: GoodsItem) => {
          Column({ space: 4 }) {
            Text(item.num + '').fontSize(8).fontColor('#757575')
            Column()
              .width(16)
              .height(item.num / this.maxNumOf(this.lapList) * 120)
              .backgroundColor('#C62828')
              .borderRadius({ topLeft: 4, topRight: 4 })
            Text(item.name.substring(0, 1)).fontSize(9).fontColor('#616161')
          }
          .alignItems(HorizontalAlign.Center)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%').alignItems(VerticalAlign.End)
      .padding({ left: 4, right: 4, top: 12, bottom: 12 })
      .backgroundColor('#FFFFFF').borderRadius(10)

      Row({ space: 6 }) {
        Text('🏆 车手圈速总榜').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
        Column().layoutWeight(1)
        Text('本月更新').fontSize(11).fontColor('#9E9E9E')
      }
      .width('100%')

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.lapList, (item: GoodsItem, idx: number) => {
          Column({ space: 8 }) {
            Row({ space: 8 }) {
              if (idx === 0) {
                Column() {
                  Text((idx + 1) + '').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#212121')
                }
                .width(26).height(26).borderRadius(13)
                .backgroundColor('#FFD600').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              } else {
                Column() {
                  Text((idx + 1) + '').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#212121')
                }
                .width(26).height(26).borderRadius(13)
                .backgroundColor(idx === 1 ? '#CFD8DC' : (idx === 2 ? '#D7A26C' : '#EEEEEE'))
                .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
              }

              Column({ space: 2 }) {
                Text(item.name).fontSize(13).fontWeight(FontWeight.Medium).fontColor('#212121').maxLines(1)
                Text(item.desc).fontSize(10).fontColor('#9E9E9E').maxLines(1)
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')

            Row({ space: 4 }) {
              Text('尾速').fontSize(10).fontColor('#BDBDBD')
              Text(item.num + ' km/h').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#FFD600')
              Column().layoutWeight(1)
              Text(item.tag).fontSize(9).fontColor('#757575')
            }
            .width('100%')
          }
          .width('48%').padding(10).backgroundColor('#FFFFFF').borderRadius(10)
          .onClick(() => {
            this.selectedItem = item
            this.showDetailModal = true
          })
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
    }
    .width('100%').padding(12).backgroundColor('#F5F5F5')
  }

尾速柱状图展示12位车手的单圈尾速对比,每个柱子宽度16px,高度通过item.num / this.maxNumOf(this.lapList) * 120归一化到最大120px。柱子统一使用赛道红#C62828配合顶部圆角,柱子顶部标注尾速数值,底部标注车手姓氏。alignItems(VerticalAlign.End)确保所有柱子底部对齐。

圈速总榜使用Flex双列网格展示12位车手排名,排名徽章是该Tab的视觉亮点。第1名使用计时黄#FFD600圆形徽章(模拟金牌)配有1.05倍脉冲动画,第2名使用浅灰#CFD8DC(模拟银牌),第3名使用棕色#D7A26C(模拟铜牌),第4名及之后使用浅灰#EEEEEE。这种基于索引的条件渲染实现了金银铜奖牌的视觉效果,是ArkTS条件分支在UI设计中的典型应用。每张卡片包含排名徽章+车手信息行和尾速数据行,点击打开详情面板。

九、Tab3 性能件:马力柱状图与横滑卡片

性能件Tab展示马力提升数据可视化、三格统计卡和横向滚动的性能件卡片。

  @Builder tab2() {
    Column({ space: 14 }) {
      Row({ space: 10 }) {
        Column({ space: 4 }) {
          Text('7').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Text('已装性能件').fontSize(10).fontColor('#BDBDBD')
        }
        .layoutWeight(1).height(80).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

        Column({ space: 4 }) {
          Text('186').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFD600')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('累计提升马力').fontSize(10).fontColor('#BDBDBD')
        }
        .layoutWeight(1).height(80).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)

        Column({ space: 4 }) {
          Text('¥9860').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF8A80')
          Text('本月改装花费').fontSize(10).fontColor('#BDBDBD')
        }
        .layoutWeight(1).height(80).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
      }
      .width('100%').padding(12).backgroundColor('#212121').borderRadius(12)

      Text('马力提升柱状图 · 匹').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')

      Row({ space: 10 }) {
        ForEach(this.partList, (item: GoodsItem, idx: number) => {
          Column({ space: 4 }) {
            Text(item.num + '').fontSize(8).fontColor('#757575')
            Column()
              .width(18)
              .height(item.num / this.maxNumOf(this.partList) * 120)
              .backgroundColor(idx % 2 === 0 ? '#C62828' : '#FFD600')
              .borderRadius({ topLeft: 4, topRight: 4 })
            Text(item.tag).fontSize(8).fontColor('#616161')
          }
          .alignItems(HorizontalAlign.Center)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%').alignItems(VerticalAlign.End)
      .padding({ left: 4, right: 4, top: 12, bottom: 12 })
      .backgroundColor('#FFFFFF').borderRadius(10)

      Row({ space: 6 }) {
        Text('🛠 热销性能件').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
        Column().layoutWeight(1)
        Text('左右滑动 ›').fontSize(11).fontColor('#9E9E9E')
      }
      .width('100%')

      Scroll() {
        Row({ space: 12 }) {
          ForEach(this.partList, (item: GoodsItem) => {
            Column({ space: 8 }) {
              Column({ space: 4 }) {
                Text('🔧').fontSize(22)
                Text(item.tag).fontSize(9).fontColor('#B0BEC5')
              }
              .width('100%').height(56).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
              .backgroundColor('#37474F').borderRadius({ topLeft: 10, topRight: 10 })

              Text(item.name).fontSize(12).fontWeight(FontWeight.Medium).fontColor('#212121').maxLines(1)

              Row({ space: 3 }) {
                Text(item.num + '').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#C62828')
                  .scale({ x: 1.05, y: 1.05 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
                Text('匹').fontSize(11).fontColor('#9E9E9E')
              }
              .alignItems(VerticalAlign.Bottom)

              Text('¥' + item.price).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
              Text('库存 ' + item.score + ' 件').fontSize(9).fontColor('#BDBDBD')
            }
            .width(132).padding({ left: 0, right: 0, top: 0, bottom: 10 })
            .backgroundColor('#FFFFFF').borderRadius(10).alignItems(HorizontalAlign.Center)
            .onClick(() => {
              this.selectedItem = item
              this.chipA = 0
              this.stepNum = 1
              this.showBuyModal = true
            })
          }, (item: GoodsItem) => item.name)
        }
        .padding({ left: 2, right: 2, top: 2, bottom: 2 })
      }
      .scrollable(ScrollDirection.Horizontal).width('100%')
    }
    .width('100%').padding(12).backgroundColor('#F5F5F5')
  }

三格统计卡使用沥青黑#212121背景,展示已装性能件数(白色)、累计提升马力(计时黄配脉冲)和本月改装花费(浅红#FF8A80)。马力提升柱状图的柱子颜色交替使用赛道红和计时黄(idx % 2),柱子宽度18px,高度归一化到最大120px,标注提升后马力匹数和分类标签。

横向滚动的性能件卡片使用Scroll配合ScrollDirection.Horizontal实现水平滑动。每张卡片宽度固定132px,顶部使用深灰#37474F背景区承载工具图标和分类标签,下方依次为产品名称、马力数值(大号赛道红配脉冲动画)、价格和库存数。马力数值使用22px大号字体配合1.05倍脉冲动画,是该卡片的核心视觉焦点。点击卡片打开购买弹窗。

十、Tab4 驾驶培训:报名柱状图与课程列表

驾驶培训Tab展示课程报名人数柱状图和10条培训课程列表。

  @Builder tab3() {
    Column({ space: 14 }) {
      Text('近十期课程 · 报名人数')
        .fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')

      Row({ space: 10 }) {
        ForEach(this.courseList, (item: GoodsItem, idx: number) => {
          Column({ space: 4 }) {
            Text(item.num + '').fontSize(8).fontColor('#757575')
            Column()
              .width(18)
              .height(item.num / this.maxNumOf(this.courseList) * 110)
              .backgroundColor(idx % 2 === 0 ? '#C62828' : '#FF8F00')
              .borderRadius({ topLeft: 4, topRight: 4 })
            Text(item.tag).fontSize(8).fontColor('#616161')
          }
          .alignItems(HorizontalAlign.Center)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%').alignItems(VerticalAlign.End)
      .padding({ left: 4, right: 4, top: 12, bottom: 12 })
      .backgroundColor('#FFFFFF').borderRadius(10)

      Row({ space: 6 }) {
        Text('🎓 驾驶培训课程').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
        Column().layoutWeight(1)
        Text('教练认证').fontSize(11).fontColor('#9E9E9E')
      }
      .width('100%')

      ForEach(this.courseList, (item: GoodsItem, idx: number) => {
        Row({ space: 10 }) {
          Column()
            .width(5).height(48)
            .backgroundColor(idx % 3 === 0 ? '#C62828' : (idx % 3 === 1 ? '#FF8F00' : '#FFD600'))
            .borderRadius(3)

          Column({ space: 5 }) {
            Row({ space: 6 }) {
              Text(item.name).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#212121').maxLines(1)
              Text(item.tag).fontSize(9).fontColor('#FFFFFF')
                .backgroundColor('#C62828').borderRadius(4)
                .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            }
            .width('100%')

            Text(item.desc).fontSize(11).fontColor('#9E9E9E').maxLines(1)

            Row() {
              Column().width(item.score + '%').height(5).borderRadius(3).backgroundColor('#FF8F00')
            }
            .width('100%').height(5).borderRadius(3).backgroundColor('#EEEEEE')
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)

          Column({ space: 6 }) {
            Text('¥' + item.price).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#C62828')
            Text(item.num + '人在学').fontSize(9).fontColor('#BDBDBD')
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(10)
        .onClick(() => {
          this.selectedItem = item
          this.chipA = 0
          this.chipB = 0
          this.stepNum = 1
          this.showTrainModal = true
        })
      }, (item: GoodsItem) => item.name)
    }
    .width('100%').padding(12).backgroundColor('#F5F5F5')
  }

报名人数柱状图的柱子颜色交替使用赛道红和深橙#FF8F00,柱子标注报名人数和分类标签。课程列表卡片左侧使用5px宽的彩色装饰条(三色循环:赛道红、深橙、计时黄),中间信息区包含课程名称+标签(标签配脉冲动画)、描述和报名进度条(深橙填充),右侧展示价格和在学人数。点击卡片打开培训报名弹窗。

十一、Tab5 车队社区:话题筛选与帖子列表

车队社区Tab是该应用的社交互动模块,包含话题标签筛选和帖子列表。

  @Builder tab4() {
    Column({ space: 12 }) {
      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
        ForEach(this.topicChips, (chip: string, idx: number) => {
          Text(chip)
            .fontSize(11)
            .fontColor(this.topicSel === idx ? '#FFFFFF' : '#C62828')
            .backgroundColor(this.topicSel === idx ? '#C62828' : '#FDECEA')
            .borderRadius(14)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .margin({ left: 0, right: 8, top: 0, bottom: 4 })
            .onClick(() => { this.topicSel = idx })
        }, (chip: string) => chip)
      }
      .width('100%')

      Row({ space: 6 }) {
        Text('👥 车队新鲜事').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
        Column().layoutWeight(1)
        Text('发帖 +').fontSize(12).fontColor('#C62828')
      }
      .width('100%')

      ForEach(this.postList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Text(item.name.substring(0, 1))
            .fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            .width(40).height(40).borderRadius(20)
            .backgroundColor('#37474F').textAlign(TextAlign.Center)

          Column({ space: 6 }) {
            Row({ space: 6 }) {
              Text(item.name).fontSize(13).fontWeight(FontWeight.Medium).fontColor('#212121')
              Text(item.tag).fontSize(9).fontColor('#C62828')
                .backgroundColor('#FDECEA').borderRadius(4)
                .padding({ left: 5, right: 5, top: 1, bottom: 1 })
            }
            .width('100%')

            Text(item.desc).fontSize(12).fontColor('#616161').maxLines(2)
              .textOverflow({ overflow: TextOverflow.Ellipsis })

            Row({ space: 4 }) {
              Text('🔥').fontSize(11)
              Text(item.num + ' 赞').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#E53935')
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              Column().layoutWeight(1)
              Text(item.hot).fontSize(9).fontColor('#FF8F00')
            }
            .width('100%')
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)
        }
        .width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(10)
      }, (item: GoodsItem) => item.name)
    }
    .width('100%').padding(12).backgroundColor('#F5F5F5')
  }

话题标签筛选是该Tab的特色交互——通过topicSel状态变量管理当前选中话题,选中时标签变为白字红底(赛道红#C62828),未选中时为红字浅红底(#FDECEA)。点击标签更新topicSel,触发标签样式的条件渲染刷新。虽然帖子列表未实际过滤(ForEach仍渲染全部postList),但这种筛选交互模式展示了ArkTS状态驱动UI切换的能力。

帖子卡片使用深灰#37474F圆形头像承载用户姓氏首字,信息区包含用户名+车队标签、正文内容(最多2行,溢出省略)、互动栏(火焰emoji+点赞数配脉冲动画、热度标记)。帖子正文使用textOverflow({ overflow: TextOverflow.Ellipsis })处理多行文本溢出,确保卡片高度一致。

十二、消息中心与个人中心

消息Tab通过消息类型差异化着色和未读红点展示消息列表,个人中心Tab展示沥青黑个人卡和功能菜单。

  @Builder tab5() {
    Column({ space: 10 }) {
      Row({ space: 6 }) {
        Text('✉ 消息中心').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
        Column().layoutWeight(1)
        Text('全部已读').fontSize(11).fontColor('#9E9E9E')
          .onClick(() => { this.currentTab = 5 })
      }
      .width('100%')

      ForEach(this.msgList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Text(item.name.substring(0, 1))
            .fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            .width(42).height(42).borderRadius(21)
            .backgroundColor(item.hot === '群聊' ? '#37474F' : (item.hot === '私聊' ? '#FF8F00' : '#C62828'))
            .textAlign(TextAlign.Center)

          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(item.name).fontSize(13).fontWeight(FontWeight.Medium)
                .fontColor(item.score > 60 ? '#212121' : '#757575').maxLines(1)
              if (item.score > 60) {
                Column()
                  .width(8).height(8).borderRadius(4)
                  .backgroundColor('#E53935')
                  .scale({ x: 1.05, y: 1.05 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              }
              Column().layoutWeight(1)
              Text(item.tag).fontSize(9).fontColor('#BDBDBD')
            }
            .width('100%')

            Text(item.desc).fontSize(11).fontColor('#9E9E9E').maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)

          Column({ space: 4 }) {
            Text(item.num + '条').fontSize(10).fontColor('#BDBDBD')
            Text(item.hot).fontSize(9).fontColor('#C62828')
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(10)
        .onClick(() => {
          this.selectedItem = item
          this.showDetailModal = true
        })
      }, (item: GoodsItem) => item.name)
    }
    .width('100%').padding(12).backgroundColor('#F5F5F5')
  }

消息列表的头像颜色根据消息类型动态着色——群聊使用深灰#37474F、私聊使用深橙#FF8F00、通知使用赛道红#C62828,通过item.hot的值进行三路条件判断。未读状态通过item.score > 60判断——分数大于60视为未读,此时用户名显示为深色#212121(已读为浅灰#757575),且在用户名右侧渲染一个红色脉冲圆点。消息计数使用item.num字段显示条数。这种通过业务数据字段值控制UI状态的差异化渲染方式,是ArkTS条件渲染在消息列表场景的典型应用。

  @Builder tab6() {
    Column({ space: 14 }) {
      Column({ space: 12 }) {
        Row({ space: 12 }) {
          Text('骁').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            .width(56).height(56).borderRadius(28)
            .backgroundColor('#C62828').textAlign(TextAlign.Center)

          Column({ space: 4 }) {
            Text('圈内代号 · 阿骁').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text('场地赛车照 B 级 · 飞驰车队')
              .fontSize(11).fontColor('#FFD600')
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start)

          Text('›').fontSize(20).fontColor('#BDBDBD')
        }
        .width('100%').alignItems(VerticalAlign.Center)

        Row({ space: 8 }) {
          Column({ space: 3 }) {
            Text('23').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text('参加场次').fontSize(10).fontColor('#BDBDBD')
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)

          Column().width(1).height(28).backgroundColor('#424242')

          Column({ space: 3 }) {
            Text('486').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text('总圈数').fontSize(10).fontColor('#BDBDBD')
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)

          Column().width(1).height(28).backgroundColor('#424242')

          Column({ space: 3 }) {
            Text('2:08.4').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFD600')
            Text('最快圈速').fontSize(10).fontColor('#BDBDBD')
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
        }
        .width('100%').padding({ left: 8, right: 8, top: 10, bottom: 4 })
      }
      .width('100%').padding(16).backgroundColor('#212121').borderRadius(14)

个人中心Tab的沥青黑个人卡采用#212121背景,包含赛道红圆形头像(“骁"字)、圈内代号、赛车照等级(计时黄配脉冲动画)、三格统计(参加场次、总圈数白色,最快圈速计时黄)。下方功能菜单包含"我的报名”“圈速证书”“我的车库”“教练预约”“装备租赁记录”"设置"等项,每项点击分别跳转到不同Tab或打开不同弹窗——这种菜单路由分发通过onClick中的条件赋值实现。例如"我的报名"点击打开删除确认弹窗,"我的车库"点击打开购买弹窗,"教练预约"点击打开培训弹窗。

十三、弹窗一:赛道日报名

赛道日报名弹窗是应用的核心业务入口,包含赛道选择、时段选择和圈数步进器。

  @Builder addModal() {
    Column() {
      Column().layoutWeight(1)

      Column({ space: 14 }) {
        Row({ space: 8 }) {
          Text('报名赛道日').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#212121')
          Column().layoutWeight(1)
          Text('×').fontSize(20).fontColor('#9E9E9E').padding(4)
            .onClick(() => { this.showAddModal = false })
        }
        .width('100%')

        Text('选择赛道').fontSize(13).fontColor('#757575').width('100%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.trackChips, (chip: string, idx: number) => {
            Text(chip).fontSize(12)
              .fontColor(this.chipA === idx ? '#FFFFFF' : '#616161')
              .backgroundColor(this.chipA === idx ? '#C62828' : '#F5F5F5')
              .borderRadius(15)
              .padding({ left: 14, right: 14, top: 7, bottom: 7 })
              .margin({ left: 0, right: 8, top: 0, bottom: 8 })
              .onClick(() => { this.chipA = idx })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('选择时段').fontSize(13).fontColor('#757575').width('100%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.timeChips, (chip: string, idx: number) => {
            Text(chip).fontSize(12)
              .fontColor(this.chipB === idx ? '#FFFFFF' : '#616161')
              .backgroundColor(this.chipB === idx ? '#212121' : '#F5F5F5')
              .borderRadius(15)
              .padding({ left: 14, right: 14, top: 7, bottom: 7 })
              .margin({ left: 0, right: 8, top: 0, bottom: 8 })
              .onClick(() => { this.chipB = idx })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('跑圈数量(1 至 10 圈)').fontSize(13).fontColor('#757575').width('100%')

        Row({ space: 16 }) {
          Text('-').fontSize(16).fontColor('#FFFFFF')
            .width(30).height(30).borderRadius(15)
            .backgroundColor('#37474F').textAlign(TextAlign.Center)
            .onClick(() => { if (this.stepNum > 1) { this.stepNum -= 1 } })

          Text(this.stepNum + ' 圈').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')

          Text('+').fontSize(16).fontColor('#FFFFFF')
            .width(30).height(30).borderRadius(15)
            .backgroundColor('#C62828').textAlign(TextAlign.Center)
            .onClick(() => { if (this.stepNum < 10) { this.stepNum += 1 } })

          Column().layoutWeight(1)

          Text('¥' + this.addFee())
            .fontSize(24).fontWeight(FontWeight.Bold).fontColor('#C62828')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%').alignItems(VerticalAlign.Center)

        Text('确认报名 · 支付定金')
          .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          .width('100%').height(46).textAlign(TextAlign.Center)
          .backgroundColor('#C62828').borderRadius(23)
          .onClick(() => { this.showAddModal = false })
      }
      .width('100%').padding(16).backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .constraintSize({ maxHeight: '80%' })
      .onClick((event: ClickEvent) => { event.stopPropagation() })
    }
    .width('100%').height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => { this.showAddModal = false })
  }

赛道日报名弹窗的赛道选择使用chipA管理(选中时赛道红底白字),时段选择使用chipB管理(选中时沥青黑底白字——这种"赛道红=主选择、沥青黑=时段选择"的色彩语义呼应了应用整体配色体系)。步进器控制跑圈数量(1-10圈),减号按钮使用深灰背景、加号按钮使用赛道红背景,形成视觉对比。费用通过addFee()方法计算(selPrice() * stepNum),当未选择条目时使用默认1280元作为单价。费用数值配有24px大号字体和脉冲动画,是弹窗的核心视觉焦点。确认按钮使用赛道红背景、46px固定高度和23px圆角,形成醒目的行动按钮。

十四、弹窗二与三:性能件购买与培训报名

性能件购买弹窗和培训报名弹窗在赛道日弹窗的交互模式基础上进行业务适配。

  @Builder buyModal() {
    Column() {
      Column().layoutWeight(1)

      Column({ space: 14 }) {
        Row({ space: 8 }) {
          Column({ space: 2 }) {
            Text('购买性能件').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#212121')
            Text(this.selName()).fontSize(11).fontColor('#9E9E9E').maxLines(1)
          }
          .alignItems(HorizontalAlign.Start)

          Column().layoutWeight(1)

          Text('×').fontSize(20).fontColor('#9E9E9E').padding(4)
            .onClick(() => { this.showBuyModal = false })
        }
        .width('100%').alignItems(VerticalAlign.Top)

        Text('选择规格').fontSize(13).fontColor('#757575').width('100%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.specChips, (chip: string, idx: number) => {
            Text(chip).fontSize(12)
              .fontColor(this.chipA === idx ? '#FFFFFF' : '#616161')
              .backgroundColor(this.chipA === idx ? '#C62828' : '#F5F5F5')
              .borderRadius(15)
              .padding({ left: 14, right: 14, top: 7, bottom: 7 })
              .margin({ left: 0, right: 8, top: 0, bottom: 8 })
              .onClick(() => { this.chipA = idx })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('购买数量(1 至 5 件)').fontSize(13).fontColor('#757575').width('100%')

        Row({ space: 16 }) {
          Text('-').fontSize(16).fontColor('#FFFFFF')
            .width(30).height(30).borderRadius(15)
            .backgroundColor('#37474F').textAlign(TextAlign.Center)
            .onClick(() => { if (this.stepNum > 1) { this.stepNum -= 1 } })

          Text(this.stepNum + ' 件').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')

          Text('+').fontSize(16).fontColor('#FFFFFF')
            .width(30).height(30).borderRadius(15)
            .backgroundColor('#C62828').textAlign(TextAlign.Center)
            .onClick(() => { if (this.stepNum < 5) { this.stepNum += 1 } })

          Column().layoutWeight(1)

          Column({ space: 2 }) {
            Text('合计').fontSize(10).fontColor('#9E9E9E')
            Text('¥' + this.buyFee())
              .fontSize(24).fontWeight(FontWeight.Bold).fontColor('#C62828')
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%').alignItems(VerticalAlign.Center)

        Text('加入购物车 · 分期免息')
          .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          .width('100%').height(46).textAlign(TextAlign.Center)
          .backgroundColor('#212121').borderRadius(23)
          .onClick(() => { this.showBuyModal = false })
      }
      .width('100%').padding(16).backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .constraintSize({ maxHeight: '80%' })
      .onClick((event: ClickEvent) => { event.stopPropagation() })
    }
    .width('100%').height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => { this.showBuyModal = false })
  }

性能件购买弹窗的标题区采用双行布局——主标题"购买性能件"加副标题显示选中产品名称(通过selName()获取)。规格选择使用chipA管理(赛道版/街道版/入门版三选一),步进器控制购买数量(1-5件),合计费用通过buyFee()计算。与赛道日弹窗不同的是,合计区增加了"合计"标签行,且行动按钮使用沥青黑#212121背景(而非赛道红),配合"加入购物车 · 分期免息"文案,区分了购买场景与报名场景的视觉语义。

培训报名弹窗结构与购买弹窗类似,但选择项变为课程类型(chipA管理,选中时使用深橙#FF8F00背景)和教练选择(chipB管理,选中时使用沥青黑背景),步进器控制课时数量(1-6课时),费用通过trainFee()计算,行动按钮使用深橙背景配合"提交报名 · 教练确认后付款"文案。三种弹窗的行动按钮颜色差异(赛道红/沥青黑/深橙)为用户提供了场景区分的视觉提示。

十五、删除确认弹窗与详情面板

删除确认弹窗采用居中红色警示卡片,详情面板从右侧滑出展示完整信息。

  @Builder deleteModal() {
    Column() {
      Column({ space: 16 }) {
        Column({ space: 8 }) {
          Text('⚠').fontSize(34).fontColor('#FFFFFF')
          Text('确认删除该报名记录?').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Text('删除后不可恢复').fontSize(11).fontColor('#FFCDD2')
        }
        .width('100%')
        .padding({ left: 20, right: 20, top: 22, bottom: 22 })
        .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        .backgroundColor('#E53935').borderRadius({ topLeft: 12, topRight: 12 })

        Row({ space: 12 }) {
          Text('取消').fontSize(14).fontWeight(FontWeight.Medium).fontColor('#616161')
            .layoutWeight(1).height(40).textAlign(TextAlign.Center)
            .backgroundColor('#F5F5F5').borderRadius(20)
            .onClick(() => { this.showDeleteModal = false })

          Text('删除').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            .layoutWeight(1).height(40).textAlign(TextAlign.Center)
            .backgroundColor('#E53935').borderRadius(20)
            .onClick(() => { this.showDeleteModal = false })
        }
        .width('100%').padding({ left: 16, right: 16, top: 0, bottom: 16 })
      }
      .width('72%').borderRadius(12).backgroundColor('#FFFFFF')
      .onClick((event: ClickEvent) => { event.stopPropagation() })
    }
    .width('100%').height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
    .onClick(() => { this.showDeleteModal = false })
  }

删除确认弹窗的顶部警示区使用警示红#E53935背景配合圆角顶部,内含警示图标、标题和说明文字。取消/删除双按钮使用layoutWeight(1)等分宽度,取消按钮灰色弱化、删除按钮红色强化。按钮使用固定40px高度配合20px圆角,形成胶囊形行动按钮。

  @Builder detailModal() {
    Column() {
      Column({ space: 12 }) {
        Column({ space: 8 }) {
          Text('🏁').fontSize(44)
          Text(this.selTag()).fontSize(12).fontColor('#FFCDD2')
        }
        .width('100%').height(150)
        .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        .backgroundColor('#C62828')

        Text(this.selName()).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
        Text(this.selDesc()).fontSize(12).fontColor('#9E9E9E').width('100%')

        Row({ space: 6 }) {
          Text('★★★★★').fontSize(14).fontColor('#FFD600')
          Text('4.8 分').fontSize(11).fontColor('#FF8F00')
          Column().layoutWeight(1)
          Text(this.selNum() + ' 人关注').fontSize(11).fontColor('#9E9E9E')
        }
        .width('100%')

        Row({ space: 4 }) {
          Text('¥').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#C62828')
          Text(this.selPrice() + '').fontSize(32).fontWeight(FontWeight.Bold).fontColor('#C62828')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Column().layoutWeight(1)
          Text('报名进度 ' + this.selScore() + '%').fontSize(11).fontColor('#757575')
        }
        .width('100%').alignItems(VerticalAlign.Bottom)

        Row() {
          Column().width(this.selScore() + '%').height(6).borderRadius(3).backgroundColor('#C62828')
        }
        .width('100%').height(6).borderRadius(3).backgroundColor('#EEEEEE')

        Text('服务保障').fontSize(12).fontColor('#757575').width('100%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.serviceChips, (chip: string) => {
            Text(chip).fontSize(11).fontColor('#C62828')
              .backgroundColor('#FDECEA').borderRadius(12)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .margin({ left: 0, right: 8, top: 0, bottom: 6 })
          }, (chip: string) => chip)
        }
        .width('100%')

        Column().layoutWeight(1)

        Text('立即报名')
          .fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          .width('100%').height(48).textAlign(TextAlign.Center)
          .backgroundColor('#C62828').borderRadius(24)
          .onClick(() => {
            this.showDetailModal = false
            this.chipA = 0
            this.chipB = 0
            this.stepNum = 1
            this.showAddModal = true
          })

        Text('× 关闭')
          .fontSize(12).fontColor('#BDBDBD').width('100%').textAlign(TextAlign.Center)
          .onClick(() => { this.showDetailModal = false })
      }
      .width('80%').height('100%').padding(16).backgroundColor('#FFFFFF')
      .onClick((event: ClickEvent) => { event.stopPropagation() })
    }
    .width('100%').height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.End).alignItems(HorizontalAlign.End)
    .onClick(() => { this.showDetailModal = false })
  }

详情面板从右侧滑出,内容区宽度80%、高度100%。顶部150px高的展示区使用赛道红#C62828背景承载方格旗emoji和分类标签。下方依次展示名称、描述、五星评分+评分+关注人数、价格(32px大号赛道红配脉冲动画)+报名进度、报名进度条(赛道红填充)、服务保障标签云(浅红底#FDECEA赛道红字)、"立即报名"按钮和"× 关闭"文字按钮。"立即报名"按钮点击后会先关闭详情面板,重置选择器状态,再打开报名弹窗——这种跨弹窗跳转通过连续修改多个@State变量实现。详情面板内的Column().layoutWeight(1)空白占位将"立即报名"按钮推至底部,形成了固定底部行动按钮的布局效果。

十六、应用主体构建

最终build方法将所有组件组装为完整应用。

  build() {
    Stack() {
      Column() {
        this.headerBuilder()

        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tab0()
            } else if (this.currentTab === 1) {
              this.tab1()
            } else if (this.currentTab === 2) {
              this.tab2()
            } else if (this.currentTab === 3) {
              this.tab3()
            } else if (this.currentTab === 4) {
              this.tab4()
            } else if (this.currentTab === 5) {
              this.tab5()
            } else {
              this.tab6()
            }
          }
          .width('100%')
        }
        .layoutWeight(1)
        .width('100%')

        this.bottomBuilder()
      }
      .width('100%')
      .height('100%')

      if (this.showAddModal) {
        this.addModal()
      }
      if (this.showBuyModal) {
        this.buyModal()
      }
      if (this.showTrainModal) {
        this.trainModal()
      }
      if (this.showDeleteModal) {
        this.deleteModal()
      }
      if (this.showDetailModal) {
        this.detailModal()
      }
    }
    .width('100%')
    .height('100%')
  }
}

build方法使用Stack作为根容器,将主页面和五个弹窗层叠。主页面采用Column垂直布局,包含头部、可滚动内容区和底部导航栏。内容区使用if-else条件分支根据currentTab动态渲染对应Tab页面构建器。五个弹窗通过独立if条件判断叠加在主页面之上。Stack的层叠特性使弹窗覆盖主页面,配合半透明遮罩实现弹窗效果。当@State布尔变量从true变为false时,对应弹窗自动从渲染树中移除。

赛道日业务流程图

以下流程图展示了车友从浏览赛道日到完成报名的完整业务流程:

赛道日

圈速榜

性能件

驾驶培训

车队社区

消息

我的

查看详情

切换筛选

应用启动

渲染头部与底部导航

用户选择Tab

浏览统计卡与混排列表

查看尾速柱状图与排名网格

查看马力柱状图与横滑卡片

查看报名柱状图与课程列表

话题筛选与浏览帖子

查看消息列表与未读红点

查看个人卡与功能菜单

点击赛道日卡片

右侧滑出详情面板

点击立即报名

打开赛道日报名弹窗

选择赛道与时段

调整跑圈数量步进器

动态计算报名费

确认报名支付定金

关闭弹窗完成报名

关闭弹窗返回列表

点击性能件卡片

打开性能件购买弹窗

选择规格与购买数量

动态计算合计费用

加入购物车

点击培训课程

打开培训报名弹窗

选择课程类型与教练

调整课时步进器

动态计算培训费

提交报名

点击话题标签

技术点对比表格

技术维度 赛道日Tab 圈速榜Tab 性能件Tab 驾驶培训Tab 车队社区Tab
数据列表 mainList 24条 lapList 12条 partList 10条 courseList 10条 postList 10条
布局方式 纵向列表 Flex双列网格 横向Scroll+纵向 纵向列表 纵向卡片
统计卡 三格白色卡 尾速柱状图 沥青黑三格卡 报名柱状图 无统计卡
柱状图着色 统一赛道红 红黄交替 红橙交替 无柱状图
排名徽章 金银铜条件渲染
动画效果 最快圈速脉冲 金牌脉冲 马力数值脉冲 标签脉冲 点赞数脉冲
弹窗触发 detailModal detailModal buyModal trainModal
价格展示 报名费 无价格 产品价格 课程费 无价格
特色交互 混排展示 奖牌排名 横滑购物 装饰条三色 话题筛选

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============================================================
// 赛道日体验馆 主页面
// 风格参考汽车之家 App 的赛道驾驶场景
// 配色:赛道红 #C62828 / 沥青黑 #212121 / 计时黄 #FFD600 / 浅灰底 #F5F5F5 / 警示红 #E53935
// ============================================================

interface GoodsItem {
  name: string
  price: number
  desc: string
  score: number
  num: number
  tag: string
  hot: string
}

@Entry
@Component
struct Index {
  @State currentTab: number = 0
  @State showAddModal: boolean = false
  @State showBuyModal: boolean = false
  @State showTrainModal: boolean = false
  @State showDeleteModal: boolean = false
  @State showDetailModal: boolean = false
  @State selectedItem: GoodsItem | null = null
  @State chipA: number = 0
  @State chipB: number = 0
  @State stepNum: number = 1
  @State topicSel: number = 0

  private tabs: string[] = ['赛道日', '圈速榜', '性能件', '驾驶培训', '车队社区', '消息', '我的']
  private trackChips: string[] = ['上海国际赛车场', '浙江国际赛车场', '珠海国际赛车场', '北京金港']
  private timeChips: string[] = ['周六上午', '周六下午', '周日上午', '周日下午']
  private specChips: string[] = ['赛道版', '街道版', '入门版']
  private coachChips: string[] = ['陈教练', '周教练', '吴教练']
  private lessonChips: string[] = ['新手赛道课', '漂移体验课', '高级驾驶课']
  private topicChips: string[] = ['#上赛刷圈', '#GT4体验', '#刹车改装', '#走线心得', '#赛道日拼车']
  private serviceChips: string[] = ['含保险', '教练随车', '计时服务', '免费拖车', '装备租赁']

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

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

  maxNumOf(list: GoodsItem[]): number {
    let m: number = 1
    for (let i = 0; i < list.length; i++) {
      if (list[i].num > m) {
        m = list[i].num
      }
    }
    return m
  }

  // ---------- 静态头部:无任何动画 ----------
  @Builder headerBuilder() {
    Column({ space: 10 }) {
      Row({ space: 14 }) {
        Column({ space: 2 }) {
          Text('赛道日体验馆')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('TRACK DAY EXPERIENCE')
            .fontSize(9)
            .fontColor('#BDBDBD')
            .letterSpacing(1)
        }
        .alignItems(HorizontalAlign.Start)

        Column().layoutWeight(1)

        Column({ space: 2 }) {
          Text('📅')
            .fontSize(18)
          Text('赛程')
            .fontSize(8)
            .fontColor('#BDBDBD')
        }
        .alignItems(HorizontalAlign.Center)
        .onClick(() => {
          this.currentTab = 0
        })

        Stack({ alignContent: Alignment.TopEnd }) {
          Column({ space: 2 }) {
            Text('✉')
              .fontSize(18)
            Text('消息')
              .fontSize(8)
              .fontColor('#BDBDBD')
          }
          .alignItems(HorizontalAlign.Center)

          Text('3')
            .fontSize(8)
            .fontColor('#FFFFFF')
            .width(14)
            .height(14)
            .borderRadius(7)
            .backgroundColor('#E53935')
            .textAlign(TextAlign.Center)
        }
        .width(34)
        .height(40)
        .onClick(() => {
          this.currentTab = 5
        })
      }
      .width('100%')
      .alignItems(VerticalAlign.Top)

      Row({ space: 8 }) {
        Text('🏁')
          .fontSize(14)
        Text('搜赛道 / 性能件 / 培训课')
          .fontSize(13)
          .fontColor('#9E9E9E')
        Column().layoutWeight(1)
        Text('搜索')
          .fontSize(12)
          .fontColor('#FFD600')
      }
      .width('100%')
      .height(36)
      .backgroundColor('#424242')
      .borderRadius(18)
      .padding({ left: 14, right: 14, top: 0, bottom: 0 })
      .alignItems(VerticalAlign.Center)

      Row({ space: 6 }) {
        Text('📍')
          .fontSize(12)
        Text('本周末开放:上海国际赛车场 · 剩余席位 18')
          .fontSize(12)
          .fontColor('#FFD600')
          .maxLines(1)
        Column().layoutWeight(1)
        Text('立即预约')
          .fontSize(11)
          .fontColor('#FFFFFF')
          .padding({ left: 10, right: 10, top: 5, bottom: 5 })
          .backgroundColor('#C62828')
          .borderRadius(10)
          .onClick(() => {
            this.selectedItem = this.mainList[0]
            this.chipA = 0
            this.chipB = 0
            this.stepNum = 1
            this.showAddModal = true
          })
      }
      .width('100%')
      .padding({ left: 10, right: 10, top: 8, bottom: 8 })
      .backgroundColor('#111111')
      .borderRadius(8)
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .backgroundColor('#212121')
    .padding({ left: 16, right: 16, top: 16, bottom: 12 })
  }

  @Builder bottomBuilder() {
    Row() {
      ForEach(this.tabs, (tab: string, index: number) => {
        Column({ space: 2 }) {
          Text(tab)
            .fontSize(10)
            .fontColor(this.currentTab === index ? '#C62828' : '#999999')
          Column()
            .width(18)
            .height(3)
            .borderRadius(2)
            .backgroundColor(this.currentTab === index ? '#C62828' : '#FFFFFF00')
        }
        .onClick(() => {
          this.currentTab = index
        })
      }, (tab: string) => tab)
    }
    .width('100%')
    .height(56)
    .backgroundColor('#FFFFFF')
  }

  // ---------- 数据:赛道日场次与性能件混排 24 条 ----------
  private mainList: GoodsItem[] = [
    { name: '上赛 赛道日 · G级组', price: 1280, desc: '含保险 · 教练领跑 2 圈', score: 96, num: 452, tag: '热门', hot: '爆' },
    { name: '上赛 赛道日 · A级组', price: 1680, desc: '不限圈数 · 含计时服务', score: 78, num: 168, tag: '进阶', hot: '热' },
    { name: '浙赛 赛道日 · 新手场', price: 880, desc: '含 30 分钟培训 · 附赠录像', score: 62, num: 320, tag: '新手', hot: '荐' },
    { name: '珠海 赛道日 · 耐力组', price: 1480, desc: '4 小时耐力 · 双车可换手', score: 45, num: 210, tag: '耐力', hot: '热' },
    { name: '金港 赛道日 · 街道组', price: 680, desc: '街车可入 · 含头盔租赁', score: 88, num: 505, tag: '周末', hot: '爆' },
    { name: '上赛 夜间赛道日', price: 1080, desc: '18 点开跑 · 灯光全开', score: 34, num: 96, tag: '夜场', hot: '新' },
    { name: '浙赛 雨地训练日', price: 780, desc: '雨地走线专项 · 教练 1 对 1', score: 52, num: 143, tag: '专项', hot: '荐' },
    { name: '珠海 高温测试日', price: 920, desc: '正午时段 · 含冷却站', score: 28, num: 87, tag: '测试', hot: '新' },
    { name: '制动套装 · 6 活塞卡钳', price: 4680, desc: '含钢喉与刹车皮 · 免费安装', score: 71, num: 389, tag: '性能件', hot: '热' },
    { name: '绞牙避震 · 街道可调', price: 3200, desc: '30 段阻尼 · 含四轮定位', score: 64, num: 276, tag: '性能件', hot: '荐' },
    { name: '半热熔胎 R888R', price: 2180, desc: '单条 · 干地抓地之王', score: 90, num: 468, tag: '性能件', hot: '爆' },
    { name: '碳纤维进气冬菇头', price: 860, desc: '碳纤维风箱 · 提升约 12 匹', score: 41, num: 132, tag: '性能件', hot: '新' },
    { name: '全段排气 · 阀门版', price: 5600, desc: '钛合金 · 可变阀门遥控', score: 57, num: 198, tag: '性能件', hot: '热' },
    { name: '顶吧与底盘强化件', price: 1280, desc: '前后顶吧 · 减少侧倾', score: 38, num: 105, tag: '操控', hot: '新' },
    { name: '上赛 赛道日 · 摩托组', price: 760, desc: '两轮专属 · 分组计时', score: 47, num: 178, tag: '两轮', hot: '热' },
    { name: '浙赛 赛道日 · 漂移场', price: 1180, desc: '洒水漂移 · 附赠教学', score: 69, num: 254, tag: '漂移', hot: '爆' },
    { name: '珠海 赛道日 · 拉力体验', price: 980, desc: '砂石段 · 附领航员', score: 33, num: 91, tag: '拉力', hot: '新' },
    { name: '教练随车圈速提升课', price: 1580, desc: '60 分钟 · 数据分析回放', score: 83, num: 342, tag: '增益', hot: '荐' },
    { name: '刹车皮 · 赛道级配方', price: 1180, desc: '前后四轮 · 耐高温 800 度', score: 74, num: 301, tag: '性能件', hot: '热' },
    { name: '轻量化轮毂 18 寸', price: 4800, desc: '旋压工艺 · 单只减重 2.1 公斤', score: 59, num: 187, tag: '性能件', hot: '荐' },
    { name: '机油与冷却液套餐', price: 620, desc: '赛道日保养 · 当日可取', score: 43, num: 120, tag: '保养', hot: '新' },
    { name: '赛道日跟拍服务', price: 480, desc: '机位跟车 · 精修 20 张', score: 66, num: 233, tag: '服务', hot: '荐' },
    { name: '防滚架加装预约', price: 8800, desc: '符合场地认证 · 含年检', score: 22, num: 64, tag: '安全', hot: '新' },
    { name: '车载计时盒 LTE 版', price: 1290, desc: '官方计时 · 圈速云端同步', score: 81, num: 365, tag: '装备', hot: '爆' }
  ]

  // ---------- 数据:车手圈速榜 12 条,num 为尾速 km/h ----------
  private lapList: GoodsItem[] = [
    { name: '陈骁 · GT-R35', price: 1280, desc: '最快圈 2:04.3 · 上赛', score: 96, num: 296, tag: '飞驰队', hot: '王' },
    { name: '林岚 · M4 GTS', price: 1180, desc: '最快圈 2:05.1 · 上赛', score: 92, num: 288, tag: '飞驰队', hot: '银' },
    { name: '赵擎 · 911 GT3', price: 1580, desc: '最快圈 2:05.6 · 上赛', score: 89, num: 284, tag: '银端组', hot: '铜' },
    { name: '何潇 · RC F', price: 1080, desc: '最快圈 2:06.2 · 上赛', score: 85, num: 276, tag: '银端组', hot: '4' },
    { name: '郭野 · AMG GT', price: 1380, desc: '最快圈 2:06.8 · 上赛', score: 81, num: 271, tag: '黑翼会', hot: '5' },
    { name: '苏玥 · 86 改', price: 780, desc: '最快圈 2:08.4 · 浙赛', score: 77, num: 262, tag: '黑翼会', hot: '6' },
    { name: '唐聿 · C63 S', price: 1120, desc: '最快圈 2:09.0 · 浙赛', score: 72, num: 255, tag: '自燃组', hot: '7' },
    { name: '季白 · 苏七百万', price: 990, desc: '最快圈 2:09.7 · 浙赛', score: 68, num: 248, tag: '自燃组', hot: '8' },
    { name: '樊星 · 高R', price: 720, desc: '最快圈 2:10.5 · 浙赛', score: 63, num: 240, tag: '街机组', hot: '9' },
    { name: '路遥 · M2 CS', price: 1040, desc: '最快圈 2:11.2 · 浙赛', score: 58, num: 233, tag: '街机组', hot: '10' },
    { name: '关鹤 · EV6 改', price: 660, desc: '最快圈 2:12.0 · 珠海', score: 52, num: 226, tag: '电流社', hot: '11' },
    { name: '柴进 · 型格改', price: 520, desc: '最快圈 2:13.1 · 珠海', score: 46, num: 218, tag: '电流社', hot: '12' }
  ]

  // ---------- 数据:性能件 10 条,num 为提升后马力 ----------
  private partList: GoodsItem[] = [
    { name: '阶段二程序 · 高增压', price: 8800, desc: '原厂 280 匹 · 刷后 520 匹', score: 91, num: 520, tag: '程序', hot: '爆' },
    { name: '阶段一程序 · 低增压', price: 5600, desc: '原厂 280 匹 · 刷后 430 匹', score: 84, num: 430, tag: '程序', hot: '热' },
    { name: '机械增压套件', price: 32000, desc: '自吸机型专用 · 490 匹', score: 66, num: 490, tag: '硬件', hot: '荐' },
    { name: '进气加排气组合', price: 6460, desc: '配合原程序 · 392 匹', score: 73, num: 392, tag: '组合', hot: '热' },
    { name: '中冷加大套件', price: 4200, desc: '降温 15 度 · 365 匹', score: 61, num: 365, tag: '硬件', hot: '荐' },
    { name: '头段直通 + 阀门', price: 3980, desc: '低损耗 · 355 匹', score: 57, num: 355, tag: '排气', hot: '新' },
    { name: '轻量化飞轮组', price: 5200, desc: '响应提升 · 342 匹', score: 49, num: 342, tag: '传动', hot: '新' },
    { name: '高流量风格', price: 860, desc: '入门首选 · 312 匹', score: 44, num: 312, tag: '入门', hot: '新' },
    { name: '点火强化包', price: 1280, desc: '火花塞加线 · 296 匹', score: 40, num: 296, tag: '点火', hot: '新' },
    { name: '燃油泵升级', price: 2380, desc: '供油稳定 · 285 匹', score: 37, num: 285, tag: '供油', hot: '新' }
  ]

  // ---------- 数据:驾驶培训 10 条,num 为报名人数 ----------
  private courseList: GoodsItem[] = [
    { name: '新手赛道启蒙课', price: 680, desc: '90 分钟 · 含教练随车 2 圈', score: 92, num: 305, tag: '入门', hot: '热' },
    { name: '刹车点与走线课', price: 880, desc: '120 分钟 · 数据回放分析', score: 85, num: 262, tag: '初级', hot: '荐' },
    { name: '漂移体验课 · 洒水场', price: 1280, desc: '含车辆与轮胎 · 3 组练习', score: 78, num: 238, tag: '漂移', hot: '爆' },
    { name: '高级驾驶课 · 全赛道', price: 2580, desc: '全天 · 前车手教练带教', score: 71, num: 196, tag: '高级', hot: '荐' },
    { name: '雨地控车专项课', price: 980, desc: '雨天限定 · 侧滑修正', score: 64, num: 173, tag: '专项', hot: '新' },
    { name: '跟车与防守课', price: 1080, desc: '比赛攻防 · 录像复盘', score: 58, num: 152, tag: '初级', hot: '新' },
    { name: '心理与体能训练课', price: 560, desc: '心率管理 · 反应训练', score: 52, num: 131, tag: '入门', hot: '新' },
    { name: '圈速突破训练营', price: 3680, desc: '两日营 · 目标提升 3 秒', score: 69, num: 118, tag: '高级', hot: '热' },
    { name: '女司机赛道体验课', price: 660, desc: '专属场次 · 女教练带队', score: 47, num: 96, tag: '体验', hot: '新' },
    { name: '青少年卡丁启蒙班', price: 480, desc: '8 至 14 岁 · 含装备', score: 43, num: 84, tag: '青训', hot: '新' }
  ]

  // ---------- 数据:车队社区帖子 10 条,num 为点赞数 ----------
  private postList: GoodsItem[] = [
    { name: '弯道亡魂', price: 0, desc: '上赛 13 号弯终于刹进 120 了,分享一个刹车点的找法,从 100 米牌往后推三档车身就稳了。', score: 97, num: 486, tag: '飞驰队', hot: '精' },
    { name: '胎温监测仪', price: 0, desc: '实测半热熔前三圈根本没有抓地,胎温上来之后圈速直接快了一秒半,别一上来就 push。', score: 90, num: 412, tag: '装备党', hot: '热' },
    { name: '金港常客', price: 0, desc: '金港周末场次太火爆了,下个月席位已经排满,想拼车的兄弟抓紧在评论区集合。', score: 83, num: 355, tag: '拼车组', hot: '置顶' },
    { name: '老周的修车铺', price: 0, desc: '赛道日回来一定要检查刹车油,这次 boils 掉了差点出事,换了 DOT5.1 之后再没软过。', score: 79, num: 318, tag: '技师团', hot: '荐' },
    { name: '雨战狂人', price: 0, desc: '浙赛下雨才是灵魂,雨地走线完全不一样,晚弯心早给油,附三条 wet line 图解。', score: 72, num: 276, tag: '银端组', hot: '热' },
    { name: '电车秒一切', price: 0, desc: '直道尾速 295 被油车嘲笑了三圈,结果弯道被他教做人,弯道才是王道。', score: 65, num: 231, tag: '电流社', hot: '新' },
    { name: '拖车小队长', price: 0, desc: '免费拖车服务本周继续,位置在维修区 P2,冲出缓冲区的朋友喊我一声就行。', score: 58, num: 187, tag: '志愿者', hot: '荐' },
    { name: '新手小透明', price: 0, desc: '第一次下赛道紧张到手抖,教练领跑两圈之后就好多了,感谢 7 号车道的白车大哥。', score: 51, num: 143, tag: '萌新营', hot: '新' },
    { name: '头盔收藏家', price: 0, desc: '晒一晒我的五顶头盔,赛道日最值得投资的真的是装备,别在头盔上省钱。', score: 44, num: 108, tag: '装备党', hot: '新' },
    { name: '夜场发光体', price: 0, desc: '上赛夜场灯光全开的瞬间真的会起鸡皮疙瘩,附十张长曝光大片,随便取用。', score: 38, num: 76, tag: '摄影组', hot: '新' }
  ]

  // ---------- 数据:消息 12 条,score 大于 60 视为未读 ----------
  private msgList: GoodsItem[] = [
    { name: '赛道日小助手', price: 0, desc: '您报名的上赛 G 级组已出票,请查收入场凭证。', score: 95, num: 3, tag: '刚刚', hot: '通知' },
    { name: '飞驰车队群', price: 0, desc: '陈骁:周末上赛拼车还差一位,费用均摊。', score: 88, num: 46, tag: '10:24', hot: '群聊' },
    { name: '改装顾问 阿凯', price: 0, desc: '您咨询的绞牙避震到货了,可预约今日安装。', score: 82, num: 2, tag: '09:41', hot: '客服' },
    { name: '圈速榜播报', price: 0, desc: '恭喜您的上赛最快圈被收录进本月总榜第 9 位。', score: 76, num: 1, tag: '昨天', hot: '通知' },
    { name: '陈教练', price: 0, desc: '昨天的走线录像我标好了三个问题点,方便的话今晚复盘。', score: 71, num: 5, tag: '昨天', hot: '私聊' },
    { name: '订单通知', price: 0, desc: '半热熔胎四条已发货,预计后天送达安装点。', score: 63, num: 1, tag: '周二', hot: '通知' },
    { name: '装备党交流群', price: 0, desc: '胎温监测仪:哪家的胎压枪准一点,求推荐。', score: 52, num: 118, tag: '周二', hot: '群聊' },
    { name: '维修区广播', price: 0, desc: '本周冷却站新增两个工位,高温测试日可用。', score: 47, num: 12, tag: '周一', hot: '通知' },
    { name: '周教练', price: 0, desc: '漂移课周六下午场还剩两个名额,要留一个吗。', score: 41, num: 2, tag: '周一', hot: '私聊' },
    { name: '社区互动', price: 0, desc: '您的帖子雨战心得收到 486 个赞和 37 条评论。', score: 36, num: 1, tag: '上周', hot: '通知' },
    { name: '年检提醒', price: 0, desc: '防滚架认证还有 30 天到期,可预约复检。', score: 28, num: 1, tag: '上周', hot: '通知' },
    { name: '吴教练', price: 0, desc: '高级驾驶课的录像已上传云端,注意查收链接。', score: 22, num: 2, tag: '上周', hot: '私聊' }
  ]

  // ---------- 其余取值辅助 ----------
  selDesc(): string {
    if (this.selectedItem !== null) {
      return this.selectedItem.desc
    }
    return ''
  }

  selScore(): number {
    if (this.selectedItem !== null) {
      return this.selectedItem.score
    }
    return 0
  }

  selNum(): number {
    if (this.selectedItem !== null) {
      return this.selectedItem.num
    }
    return 0
  }

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

  addFee(): number {
    let unit: number = this.selPrice()
    if (unit <= 0) {
      unit = 1280
    }
    return Math.floor(unit * this.stepNum)
  }

  buyFee(): number {
    let unit: number = this.selPrice()
    if (unit <= 0) {
      unit = 2180
    }
    return Math.floor(unit * this.stepNum)
  }

  trainFee(): number {
    let unit: number = this.selPrice()
    if (unit <= 0) {
      unit = 680
    }
    return Math.floor(unit * this.stepNum)
  }

  // ---------- Tab1 赛道日 ----------
  @Builder tab0() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Column({ space: 4 }) {
          Text('6') .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor('#C62828')
          Text('本月场次') .fontSize(11) .fontColor('#757575')
        } .layoutWeight(1) .height(78) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .backgroundColor('#FFFFFF') .borderRadius(10)

        Column({ space: 4 }) {
          Text('128') .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor('#212121')
          Text('累计圈数') .fontSize(11) .fontColor('#757575')
        } .layoutWeight(1) .height(78) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .backgroundColor('#FFFFFF') .borderRadius(10)

        Column({ space: 4 }) {
          Text('2:08.4') .fontSize(22) .fontWeight(FontWeight.Bold) .fontColor('#FFD600') .scale({ x: 1.05, y: 1.05 }) .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('最快圈速') .fontSize(11) .fontColor('#757575')
        } .layoutWeight(1) .height(78) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .backgroundColor('#FFFFFF') .borderRadius(10)
      } .width('100%')

      Row({ space: 6 }) {
        Text('🔥 精选场次与性能件') .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor('#212121')
        Column().layoutWeight(1)
        Text('全部 ›') .fontSize(12) .fontColor('#9E9E9E')
      } .width('100%')

      ForEach(this.mainList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Text(item.name.substring(0, 1)) .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor('#FFFFFF')
            .width(44)
            .height(44)
            .borderRadius(22)
            .backgroundColor('#C62828')
            .textAlign(TextAlign.Center)

          Column({ space: 5 }) {
            Row({ space: 6 }) {
              Text(item.name)
                .fontSize(14)
                .fontWeight(FontWeight.Medium)
                .fontColor('#212121')
                .maxLines(1)
              Text(item.tag)
                .fontSize(9)
                .fontColor('#FFFFFF')
                .backgroundColor('#FF6D00')
                .borderRadius(4)
                .padding({ left: 5, right: 5, top: 2, bottom: 2 })
            }
            .width('100%')

            Text(item.desc)
              .fontSize(11)
              .fontColor('#9E9E9E')
              .maxLines(1)

            Row() {
              Column()
                .width(item.score + '%')
                .height(5)
                .borderRadius(3)
                .backgroundColor('#C62828')
            }
            .width('100%')
            .height(5)
            .borderRadius(3)
            .backgroundColor('#EEEEEE')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column({ space: 6 }) {
            Text('¥' + item.price)
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#C62828')
            Row({ space: 4 }) {
              Text(item.hot)
                .fontSize(9)
                .fontColor('#FFFFFF')
                .backgroundColor('#E53935')
                .borderRadius(4)
                .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              Text(item.num + '热度')
                .fontSize(9)
                .fontColor('#BDBDBD')
            }
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .onClick(() => {
          this.selectedItem = item
          this.showDetailModal = true
        })
      }, (item: GoodsItem) => item.name)
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#F5F5F5')
  }

  // ---------- Tab2 圈速榜 ----------
  @Builder tab1() {
    Column({ space: 14 }) {
      Text('单圈尾速对比 · km/h')
        .fontSize(16)
        .fontWeight(FontWeight.Bold) .fontColor('#212121') .width('100%')

      Row({ space: 10 }) {
        ForEach(this.lapList, (item: GoodsItem) => {
          Column({ space: 4 }) {
            Text(item.num + '') .fontSize(8) .fontColor('#757575')
            Column() .width(16) .height(item.num / this.maxNumOf(this.lapList) * 120) .backgroundColor('#C62828') .borderRadius({ topLeft: 4, topRight: 4 })
            Text(item.name.substring(0, 1)) .fontSize(9) .fontColor('#616161')
          } .alignItems(HorizontalAlign.Center)
        }, (item: GoodsItem) => item.name)
      } .width('100%').padding({ left: 4, right: 4, top: 12, bottom: 12 }) .backgroundColor('#FFFFFF') .borderRadius(10)

      Row({ space: 6 }) {
        Text('🏆 车手圈速总榜') .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor('#212121')
        Column().layoutWeight(1)
        Text('本月更新') .fontSize(11) .fontColor('#9E9E9E')
      }
      .width('100%')

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.lapList, (item: GoodsItem, idx: number) => {
          Column({ space: 8 }) {
            Row({ space: 8 }) {
              if (idx === 0) {
                Column() {
                  Text((idx + 1) + '')
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#212121')
                }
                .width(26)
                .height(26)
                .borderRadius(13)
                .backgroundColor('#FFD600')
                .justifyContent(FlexAlign.Center)
                .alignItems(HorizontalAlign.Center)
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              } else {
                Column() {
                  Text((idx + 1) + '')
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#212121')
                }
                .width(26)
                .height(26)
                .borderRadius(13)
                .backgroundColor(idx === 1 ? '#CFD8DC' : (idx === 2 ? '#D7A26C' : '#EEEEEE'))
                .justifyContent(FlexAlign.Center)
                .alignItems(HorizontalAlign.Center)
              }

              Column({ space: 2 }) {
                Text(item.name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Medium)
                  .fontColor('#212121')
                  .maxLines(1)
                Text(item.desc)
                  .fontSize(10)
                  .fontColor('#9E9E9E')
                  .maxLines(1)
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')

            Row({ space: 4 }) {
              Text('尾速')
                .fontSize(10)
                .fontColor('#BDBDBD')
              Text(item.num + ' km/h')
                .fontSize(11)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFD600')
              Column().layoutWeight(1)
              Text(item.tag)
                .fontSize(9)
                .fontColor('#757575')
            }
            .width('100%')
          }
          .width('48%')
          .padding(10)
          .backgroundColor('#FFFFFF')
          .borderRadius(10)
          .onClick(() => {
            this.selectedItem = item
            this.showDetailModal = true
          })
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#F5F5F5')
  }

  // ---------- Tab3 性能件 ----------
  @Builder tab2() {
    Column({ space: 14 }) {
      Row({ space: 10 }) {
        Column({ space: 4 }) {
          Text('7')
            .fontSize(22)
            .fontWeight(FontWeight.Bold) .fontColor('#FFFFFF')
          Text('已装性能件') .fontSize(10) .fontColor('#BDBDBD')
        } .layoutWeight(1) .height(80) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center)

        Column({ space: 4 }) {
          Text('186') .fontSize(22) .fontWeight(FontWeight.Bold) .fontColor('#FFD600') .scale({ x: 1.05, y: 1.05 }) .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('累计提升马力') .fontSize(10) .fontColor('#BDBDBD')
        } .layoutWeight(1) .height(80) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center)

        Column({ space: 4 }) {
          Text('¥9860') .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor('#FF8A80')
          Text('本月改装花费') .fontSize(10) .fontColor('#BDBDBD')
        } .layoutWeight(1) .height(80) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center)
      } .width('100%') .padding(12) .backgroundColor('#212121') .borderRadius(12)

      Text('马力提升柱状图 · 匹') .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor('#212121') .width('100%')

      Row({ space: 10 }) {
        ForEach(this.partList, (item: GoodsItem, idx: number) => {
          Column({ space: 4 }) {
            Text(item.num + '') .fontSize(8) .fontColor('#757575')
            Column() .width(18) .height(item.num / this.maxNumOf(this.partList) * 120) .backgroundColor(idx % 2 === 0 ? '#C62828' : '#FFD600') .borderRadius({ topLeft: 4, topRight: 4 })
            Text(item.tag) .fontSize(8) .fontColor('#616161')
          } .alignItems(HorizontalAlign.Center)
        }, (item: GoodsItem) => item.name)
      } .width('100%').padding({ left: 4, right: 4, top: 12, bottom: 12 }) .backgroundColor('#FFFFFF') .borderRadius(10)

      Row({ space: 6 }) {
        Text('🛠 热销性能件')
          .fontSize(16)
          .fontWeight(FontWeight.Bold) .fontColor('#212121')
        Column().layoutWeight(1)
        Text('左右滑动 ›') .fontSize(11) .fontColor('#9E9E9E')
      } .width('100%')

      Scroll() {
        Row({ space: 12 }) {
          ForEach(this.partList, (item: GoodsItem) => {
            Column({ space: 8 }) {
              Column({ space: 4 }) {
                Text('🔧') .fontSize(22)
                Text(item.tag) .fontSize(9) .fontColor('#B0BEC5')
              } .width('100%') .height(56) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .backgroundColor('#37474F') .borderRadius({ topLeft: 10, topRight: 10 })

              Text(item.name) .fontSize(12) .fontWeight(FontWeight.Medium) .fontColor('#212121') .maxLines(1)

              Row({ space: 3 }) {
                Text(item.num + '') .fontSize(22) .fontWeight(FontWeight.Bold) .fontColor('#C62828') .scale({ x: 1.05, y: 1.05 }) .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
                Text('匹') .fontSize(11) .fontColor('#9E9E9E')
              } .alignItems(VerticalAlign.Bottom)

              Text('¥' + item.price) .fontSize(13) .fontWeight(FontWeight.Bold) .fontColor('#212121')

              Text('库存 ' + item.score + ' 件') .fontSize(9) .fontColor('#BDBDBD')
            } .width(132) .padding({ left: 0, right: 0, top: 0, bottom: 10 }) .backgroundColor('#FFFFFF') .borderRadius(10) .alignItems(HorizontalAlign.Center) .onClick(() => {
              this.selectedItem = item
              this.chipA = 0
              this.stepNum = 1
              this.showBuyModal = true
            })
          }, (item: GoodsItem) => item.name)
        } .padding({ left: 2, right: 2, top: 2, bottom: 2 })
      } .scrollable(ScrollDirection.Horizontal) .width('100%')
    } .width('100%') .padding(12) .backgroundColor('#F5F5F5')
  }

  // ---------- Tab4 驾驶培训 ----------
  @Builder tab3() {
    Column({ space: 14 }) {
      Text('近十期课程 · 报名人数')
        .fontSize(16)
        .fontWeight(FontWeight.Bold) .fontColor('#212121') .width('100%')

      Row({ space: 10 }) {
        ForEach(this.courseList, (item: GoodsItem, idx: number) => {
          Column({ space: 4 }) {
            Text(item.num + '') .fontSize(8) .fontColor('#757575')
            Column() .width(18) .height(item.num / this.maxNumOf(this.courseList) * 110) .backgroundColor(idx % 2 === 0 ? '#C62828' : '#FF8F00') .borderRadius({ topLeft: 4, topRight: 4 })
            Text(item.tag) .fontSize(8) .fontColor('#616161')
          } .alignItems(HorizontalAlign.Center)
        }, (item: GoodsItem) => item.name)
      } .width('100%').padding({ left: 4, right: 4, top: 12, bottom: 12 }) .backgroundColor('#FFFFFF') .borderRadius(10)

      Row({ space: 6 }) {
        Text('🎓 驾驶培训课程') .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor('#212121')
        Column().layoutWeight(1)
        Text('教练认证') .fontSize(11) .fontColor('#9E9E9E')
      }
      .width('100%')

      ForEach(this.courseList, (item: GoodsItem, idx: number) => {
        Row({ space: 10 }) {
          Column()
            .width(5)
            .height(48)
            .backgroundColor(idx % 3 === 0 ? '#C62828' : (idx % 3 === 1 ? '#FF8F00' : '#FFD600'))
            .borderRadius(3)

          Column({ space: 5 }) {
            Row({ space: 6 }) {
              Text(item.name)
                .fontSize(14)
                .fontWeight(FontWeight.Medium)
                .fontColor('#212121')
                .maxLines(1)
              Text(item.tag)
                .fontSize(9)
                .fontColor('#FFFFFF')
                .backgroundColor('#C62828')
                .borderRadius(4)
                .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            }
            .width('100%')

            Text(item.desc)
              .fontSize(11)
              .fontColor('#9E9E9E')
              .maxLines(1)

            Row() {
              Column()
                .width(item.score + '%')
                .height(5)
                .borderRadius(3)
                .backgroundColor('#FF8F00')
            }
            .width('100%')
            .height(5)
            .borderRadius(3)
            .backgroundColor('#EEEEEE')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column({ space: 6 }) {
            Text('¥' + item.price)
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#C62828')
            Text(item.num + '人在学')
              .fontSize(9)
              .fontColor('#BDBDBD')
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .onClick(() => {
          this.selectedItem = item
          this.chipA = 0
          this.chipB = 0
          this.stepNum = 1
          this.showTrainModal = true
        })
      }, (item: GoodsItem) => item.name)
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#F5F5F5')
  }

  // ---------- Tab5 车队社区 ----------
  @Builder tab4() {
    Column({ space: 12 }) {
      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
        ForEach(this.topicChips, (chip: string, idx: number) => {
          Text(chip)
            .fontSize(11)
            .fontColor(this.topicSel === idx ? '#FFFFFF' : '#C62828')
            .backgroundColor(this.topicSel === idx ? '#C62828' : '#FDECEA')
            .borderRadius(14)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .margin({ left: 0, right: 8, top: 0, bottom: 4 })
            .onClick(() => {
              this.topicSel = idx
            })
        }, (chip: string) => chip)
      }
      .width('100%')

      Row({ space: 6 }) {
        Text('👥 车队新鲜事')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#212121')
        Column().layoutWeight(1)
        Text('发帖 +')
          .fontSize(12)
          .fontColor('#C62828')
      }
      .width('100%')

      ForEach(this.postList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Text(item.name.substring(0, 1))
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .width(40)
            .height(40)
            .borderRadius(20)
            .backgroundColor('#37474F')
            .textAlign(TextAlign.Center)

          Column({ space: 6 }) {
            Row({ space: 6 }) {
              Text(item.name)
                .fontSize(13)
                .fontWeight(FontWeight.Medium)
                .fontColor('#212121')
              Text(item.tag)
                .fontSize(9)
                .fontColor('#C62828')
                .backgroundColor('#FDECEA')
                .borderRadius(4)
                .padding({ left: 5, right: 5, top: 1, bottom: 1 })
            }
            .width('100%')

            Text(item.desc)
              .fontSize(12)
              .fontColor('#616161')
              .maxLines(2)
              .textOverflow({ overflow: TextOverflow.Ellipsis })

            Row({ space: 4 }) {
              Text('🔥')
                .fontSize(11)
              Text(item.num + ' 赞')
                .fontSize(11)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E53935')
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              Column().layoutWeight(1)
              Text(item.hot)
                .fontSize(9)
                .fontColor('#FF8F00')
            }
            .width('100%')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
      }, (item: GoodsItem) => item.name)
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#F5F5F5')
  }

  // ---------- Tab6 消息 ----------
  @Builder tab5() {
    Column({ space: 10 }) {
      Row({ space: 6 }) {
        Text('✉ 消息中心')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#212121')
        Column().layoutWeight(1)
        Text('全部已读')
          .fontSize(11)
          .fontColor('#9E9E9E')
          .onClick(() => {
            this.currentTab = 5
          })
      }
      .width('100%')

      ForEach(this.msgList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Text(item.name.substring(0, 1))
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .width(42)
            .height(42)
            .borderRadius(21)
            .backgroundColor(item.hot === '群聊' ? '#37474F' : (item.hot === '私聊' ? '#FF8F00' : '#C62828'))
            .textAlign(TextAlign.Center)

          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(item.name)
                .fontSize(13)
                .fontWeight(FontWeight.Medium)
                .fontColor(item.score > 60 ? '#212121' : '#757575')
                .maxLines(1)
              if (item.score > 60) {
                Column()
                  .width(8)
                  .height(8)
                  .borderRadius(4)
                  .backgroundColor('#E53935')
                  .scale({ x: 1.05, y: 1.05 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              }
              Column().layoutWeight(1)
              Text(item.tag)
                .fontSize(9)
                .fontColor('#BDBDBD')
            }
            .width('100%')

            Text(item.desc)
              .fontSize(11)
              .fontColor('#9E9E9E')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column({ space: 4 }) {
            Text(item.num + '条')
              .fontSize(10)
              .fontColor('#BDBDBD')
            Text(item.hot)
              .fontSize(9)
              .fontColor('#C62828')
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .onClick(() => {
          this.selectedItem = item
          this.showDetailModal = true
        })
      }, (item: GoodsItem) => item.name)
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#F5F5F5')
  }

  // ---------- Tab7 我的 ----------
  @Builder tab6() {
    Column({ space: 14 }) {
      Column({ space: 12 }) {
        Row({ space: 12 }) {
          Text('骁')
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .width(56)
            .height(56)
            .borderRadius(28)
            .backgroundColor('#C62828')
            .textAlign(TextAlign.Center)

          Column({ space: 4 }) {
            Text('圈内代号 · 阿骁')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('场地赛车照 B 级 · 飞驰车队')
              .fontSize(11)
              .fontColor('#FFD600')
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Text('›')
            .fontSize(20)
            .fontColor('#BDBDBD')
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Row({ space: 8 }) {
          Column({ space: 3 }) {
            Text('23')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('参加场次')
              .fontSize(10)
              .fontColor('#BDBDBD')
          } .layoutWeight(1) .alignItems(HorizontalAlign.Center)

          Column() .width(1) .height(28) .backgroundColor('#424242')

          Column({ space: 3 }) {
            Text('486') .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor('#FFFFFF')
            Text('总圈数') .fontSize(10) .fontColor('#BDBDBD')
          } .layoutWeight(1) .alignItems(HorizontalAlign.Center)

          Column() .width(1) .height(28) .backgroundColor('#424242')

          Column({ space: 3 }) {
            Text('2:08.4') .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor('#FFD600')
            Text('最快圈速') .fontSize(10) .fontColor('#BDBDBD')
          } .layoutWeight(1) .alignItems(HorizontalAlign.Center)
        } .width('100%') .padding({ left: 8, right: 8, top: 10, bottom: 4 })
      } .width('100%') .padding(16) .backgroundColor('#212121') .borderRadius(14)

      Column({ space: 0 }) {
        Row({ space: 10 }) {
          Text('🎫') .fontSize(16)
          Text('我的报名') .fontSize(14) .fontColor('#212121')
          Column().layoutWeight(1)
          Text('3 场待开始') .fontSize(11) .fontColor('#9E9E9E')
          Text('›') .fontSize(16) .fontColor('#BDBDBD')
        } .width('100%') .padding({ left: 16, right: 16, top: 14, bottom: 14 }) .onClick(() => {
          this.selectedItem = this.mainList[4]
          this.showDeleteModal = true
        })

        Row({ space: 10 }) {
          Text('🏅') .fontSize(16)
          Text('圈速证书') .fontSize(14) .fontColor('#212121')
          Column().layoutWeight(1)
          Text('9 张') .fontSize(11) .fontColor('#9E9E9E')
          Text('›') .fontSize(16) .fontColor('#BDBDBD')
        } .width('100%') .padding({ left: 16, right: 16, top: 14, bottom: 14 }) .onClick(() => {
          this.currentTab = 1
        })

        Row({ space: 10 }) {
          Text('🚗') .fontSize(16)
          Text('我的车库') .fontSize(14) .fontColor('#212121')
          Column().layoutWeight(1)
          Text('2 台') .fontSize(11) .fontColor('#9E9E9E')
          Text('›') .fontSize(16) .fontColor('#BDBDBD')
        } .width('100%') .padding({ left: 16, right: 16, top: 14, bottom: 14 }) .onClick(() => {
          this.selectedItem = this.partList[0]
          this.chipA = 0
          this.stepNum = 1
          this.showBuyModal = true
        })

        Row({ space: 10 }) {
          Text('🎓') .fontSize(16)
          Text('教练预约') .fontSize(14) .fontColor('#212121')
          Column().layoutWeight(1)
          Text('1 个待确认') .fontSize(11) .fontColor('#9E9E9E')
          Text('›') .fontSize(16) .fontColor('#BDBDBD')
        } .width('100%') .padding({ left: 16, right: 16, top: 14, bottom: 14 }) .onClick(() => {
          this.selectedItem = this.courseList[0]
          this.chipA = 0
          this.chipB = 0
          this.stepNum = 1
          this.showTrainModal = true
        })

        Row({ space: 10 }) {
          Text('🧰') .fontSize(16)
          Text('装备租赁记录') .fontSize(14) .fontColor('#212121')
          Column().layoutWeight(1)
          Text('头盔 · 赛服') .fontSize(11) .fontColor('#9E9E9E')
          Text('›') .fontSize(16) .fontColor('#BDBDBD')
        } .width('100%') .padding({ left: 16, right: 16, top: 14, bottom: 14 }) .onClick(() => {
          this.currentTab = 2
        })

        Row({ space: 10 }) {
          Text('⚙') .fontSize(16)
          Text('设置') .fontSize(14) .fontColor('#212121')
          Column().layoutWeight(1)
          Text('消息提醒已开启') .fontSize(11) .fontColor('#9E9E9E')
          Text('›') .fontSize(16) .fontColor('#BDBDBD')
        } .width('100%') .padding({ left: 16, right: 16, top: 14, bottom: 14 }) .onClick(() => {
          this.currentTab = 6
        })
      } .width('100%') .backgroundColor('#FFFFFF') .borderRadius(12)
    } .width('100%') .padding(12) .backgroundColor('#F5F5F5')
  }

  // ---------- 弹框 1:报名赛道日(底部弹出) ----------
  @Builder addModal() {
    Column() {
      Column().layoutWeight(1)

      Column({ space: 14 }) {
        Row({ space: 8 }) {
          Text('报名赛道日')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Column().layoutWeight(1)
          Text('×')
            .fontSize(20)
            .fontColor('#9E9E9E')
            .padding(4)
            .onClick(() => {
              this.showAddModal = false
            })
        }
        .width('100%')

        Text('选择赛道')
          .fontSize(13)
          .fontColor('#757575')
          .width('100%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.trackChips, (chip: string, idx: number) => {
            Text(chip) .fontSize(12) .fontColor(this.chipA === idx ? '#FFFFFF' : '#616161') .backgroundColor(this.chipA === idx ? '#C62828' : '#F5F5F5') .borderRadius(15) .padding({ left: 14, right: 14, top: 7, bottom: 7 }) .margin({ left: 0, right: 8, top: 0, bottom: 8 }) .onClick(() => {
              this.chipA = idx
            })
          }, (chip: string) => chip)
        } .width('100%')

        Text('选择时段') .fontSize(13) .fontColor('#757575') .width('100%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.timeChips, (chip: string, idx: number) => {
            Text(chip) .fontSize(12) .fontColor(this.chipB === idx ? '#FFFFFF' : '#616161') .backgroundColor(this.chipB === idx ? '#212121' : '#F5F5F5') .borderRadius(15) .padding({ left: 14, right: 14, top: 7, bottom: 7 }) .margin({ left: 0, right: 8, top: 0, bottom: 8 }) .onClick(() => {
              this.chipB = idx
            })
          }, (chip: string) => chip)
        } .width('100%')

        Text('跑圈数量(1 至 10 圈)') .fontSize(13) .fontColor('#757575') .width('100%')

        Row({ space: 16 }) {
          Text('-') .fontSize(16)
            .fontColor('#FFFFFF')
            .width(30)
            .height(30)
            .borderRadius(15)
            .backgroundColor('#37474F')
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepNum > 1) {
                this.stepNum -= 1
              }
            })

          Text(this.stepNum + ' 圈')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')

          Text('+')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .width(30)
            .height(30)
            .borderRadius(15)
            .backgroundColor('#C62828')
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepNum < 10) {
                this.stepNum += 1
              }
            })

          Column().layoutWeight(1)

          Text('¥' + this.addFee())
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#C62828')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Text('确认报名 · 支付定金')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .width('100%')
          .height(46)
          .textAlign(TextAlign.Center)
          .backgroundColor('#C62828')
          .borderRadius(23)
          .onClick(() => {
            this.showAddModal = false
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showAddModal = false
    })
  }

  // ---------- 弹框 2:性能件购买(底部弹出) ----------
  @Builder buyModal() {
    Column() {
      Column().layoutWeight(1)

      Column({ space: 14 }) {
        Row({ space: 8 }) {
          Column({ space: 2 }) {
            Text('购买性能件')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor('#212121')
            Text(this.selName())
              .fontSize(11)
              .fontColor('#9E9E9E')
              .maxLines(1)
          }
          .alignItems(HorizontalAlign.Start)

          Column().layoutWeight(1)

          Text('×')
            .fontSize(20)
            .fontColor('#9E9E9E')
            .padding(4)
            .onClick(() => {
              this.showBuyModal = false
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Top)

        Text('选择规格')
          .fontSize(13)
          .fontColor('#757575')
          .width('100%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.specChips, (chip: string, idx: number) => {
            Text(chip) .fontSize(12) .fontColor(this.chipA === idx ? '#FFFFFF' : '#616161') .backgroundColor(this.chipA === idx ? '#C62828' : '#F5F5F5') .borderRadius(15) .padding({ left: 14, right: 14, top: 7, bottom: 7 }) .margin({ left: 0, right: 8, top: 0, bottom: 8 }) .onClick(() => {
              this.chipA = idx
            })
          }, (chip: string) => chip)
        } .width('100%')

        Text('购买数量(1 至 5 件)') .fontSize(13) .fontColor('#757575') .width('100%')

        Row({ space: 16 }) {
          Text('-') .fontSize(16) .fontColor('#FFFFFF')
            .width(30)
            .height(30)
            .borderRadius(15)
            .backgroundColor('#37474F')
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepNum > 1) {
                this.stepNum -= 1
              }
            })

          Text(this.stepNum + ' 件')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')

          Text('+')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .width(30)
            .height(30)
            .borderRadius(15)
            .backgroundColor('#C62828')
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepNum < 5) {
                this.stepNum += 1
              }
            })

          Column().layoutWeight(1)

          Column({ space: 2 }) {
            Text('合计')
              .fontSize(10)
              .fontColor('#9E9E9E')
            Text('¥' + this.buyFee())
              .fontSize(24)
              .fontWeight(FontWeight.Bold)
              .fontColor('#C62828')
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Text('加入购物车 · 分期免息')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .width('100%')
          .height(46)
          .textAlign(TextAlign.Center)
          .backgroundColor('#212121')
          .borderRadius(23)
          .onClick(() => {
            this.showBuyModal = false
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showBuyModal = false
    })
  }

  // ---------- 弹框 3:驾驶培训报名(底部弹出) ----------
  @Builder trainModal() {
    Column() {
      Column().layoutWeight(1)

      Column({ space: 14 }) {
        Row({ space: 8 }) {
          Text('驾驶培训报名')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Column().layoutWeight(1)
          Text('×')
            .fontSize(20)
            .fontColor('#9E9E9E')
            .padding(4)
            .onClick(() => {
              this.showTrainModal = false
            })
        }
        .width('100%')

        Text('课程类型')
          .fontSize(13)
          .fontColor('#757575')
          .width('100%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.lessonChips, (chip: string, idx: number) => {
            Text(chip) .fontSize(12) .fontColor(this.chipA === idx ? '#FFFFFF' : '#616161') .backgroundColor(this.chipA === idx ? '#FF8F00' : '#F5F5F5') .borderRadius(15) .padding({ left: 14, right: 14, top: 7, bottom: 7 }) .margin({ left: 0, right: 8, top: 0, bottom: 8 }) .onClick(() => {
              this.chipA = idx
            })
          }, (chip: string) => chip)
        } .width('100%')

        Text('选择教练') .fontSize(13) .fontColor('#757575') .width('100%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.coachChips, (chip: string, idx: number) => {
            Text(chip) .fontSize(12) .fontColor(this.chipB === idx ? '#FFFFFF' : '#616161') .backgroundColor(this.chipB === idx ? '#212121' : '#F5F5F5') .borderRadius(15) .padding({ left: 14, right: 14, top: 7, bottom: 7 }) .margin({ left: 0, right: 8, top: 0, bottom: 8 }) .onClick(() => {
              this.chipB = idx
            })
          }, (chip: string) => chip)
        } .width('100%')

        Text('课时数量(1 至 6 课时)') .fontSize(13) .fontColor('#757575') .width('100%')

        Row({ space: 16 }) {
          Text('-') .fontSize(16)
            .fontColor('#FFFFFF')
            .width(30)
            .height(30)
            .borderRadius(15)
            .backgroundColor('#37474F')
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepNum > 1) {
                this.stepNum -= 1
              }
            })

          Text(this.stepNum + ' 课时')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')

          Text('+')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .width(30)
            .height(30)
            .borderRadius(15)
            .backgroundColor('#FF8F00')
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepNum < 6) {
                this.stepNum += 1
              }
            })

          Column().layoutWeight(1)

          Text('¥' + this.trainFee())
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF8F00')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Text('提交报名 · 教练确认后付款')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .width('100%')
          .height(46)
          .textAlign(TextAlign.Center)
          .backgroundColor('#FF8F00')
          .borderRadius(23)
          .onClick(() => {
            this.showTrainModal = false
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showTrainModal = false
    })
  }

  // ---------- 弹框 4:删除确认(居中小卡警示样式) ----------
  @Builder deleteModal() {
    Column() {
      Column({ space: 16 }) {
        Column({ space: 8 }) {
          Text('⚠')
            .fontSize(34)
            .fontColor('#FFFFFF')
          Text('确认删除该报名记录?')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('删除后不可恢复')
            .fontSize(11)
            .fontColor('#FFCDD2')
        }
        .width('100%')
        .padding({ left: 20, right: 20, top: 22, bottom: 22 })
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .backgroundColor('#E53935')
        .borderRadius({ topLeft: 12, topRight: 12 })

        Row({ space: 12 }) {
          Text('取消')
            .fontSize(14)
            .fontWeight(FontWeight.Medium)
            .fontColor('#616161')
            .layoutWeight(1)
            .height(40)
            .textAlign(TextAlign.Center)
            .backgroundColor('#F5F5F5')
            .borderRadius(20)
            .onClick(() => {
              this.showDeleteModal = false
            })

          Text('删除')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .layoutWeight(1)
            .height(40)
            .textAlign(TextAlign.Center)
            .backgroundColor('#E53935')
            .borderRadius(20)
            .onClick(() => {
              this.showDeleteModal = false
            })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 0, bottom: 16 })
      }
      .width('72%')
      .borderRadius(12)
      .backgroundColor('#FFFFFF')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .onClick(() => {
      this.showDeleteModal = false
    })
  }

  // ---------- 弹框 5:详情(右侧滑出全高面板) ----------
  @Builder detailModal() {
    Column() {
      Column({ space: 12 }) {
        Column({ space: 8 }) {
          Text('🏁')
            .fontSize(44)
          Text(this.selTag())
            .fontSize(12)
            .fontColor('#FFCDD2')
        }
        .width('100%')
        .height(150)
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .backgroundColor('#C62828')

        Text(this.selName())
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#212121')
          .width('100%')

        Text(this.selDesc())
          .fontSize(12)
          .fontColor('#9E9E9E')
          .width('100%')

        Row({ space: 6 }) {
          Text('★★★★★')
            .fontSize(14)
            .fontColor('#FFD600')
          Text('4.8 分')
            .fontSize(11)
            .fontColor('#FF8F00')
          Column().layoutWeight(1)
          Text(this.selNum() + ' 人关注')
            .fontSize(11)
            .fontColor('#9E9E9E')
        }
        .width('100%')

        Row({ space: 4 }) {
          Text('¥')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#C62828')
          Text(this.selPrice() + '')
            .fontSize(32)
            .fontWeight(FontWeight.Bold)
            .fontColor('#C62828')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Column().layoutWeight(1)
          Text('报名进度 ' + this.selScore() + '%')
            .fontSize(11)
            .fontColor('#757575')
        }
        .width('100%')
        .alignItems(VerticalAlign.Bottom)

        Row() {
          Column()
            .width(this.selScore() + '%')
            .height(6)
            .borderRadius(3)
            .backgroundColor('#C62828')
        }
        .width('100%')
        .height(6)
        .borderRadius(3)
        .backgroundColor('#EEEEEE')

        Text('服务保障')
          .fontSize(12)
          .fontColor('#757575')
          .width('100%')

        Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
          ForEach(this.serviceChips, (chip: string) => {
            Text(chip)
              .fontSize(11)
              .fontColor('#C62828')
              .backgroundColor('#FDECEA')
              .borderRadius(12)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .margin({ left: 0, right: 8, top: 0, bottom: 6 })
          }, (chip: string) => chip)
        }
        .width('100%')

        Column().layoutWeight(1)

        Text('立即报名')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .width('100%')
          .height(48)
          .textAlign(TextAlign.Center)
          .backgroundColor('#C62828')
          .borderRadius(24)
          .onClick(() => {
            this.showDetailModal = false
            this.chipA = 0
            this.chipB = 0
            this.stepNum = 1
            this.showAddModal = true
          })

        Text('× 关闭')
          .fontSize(12)
          .fontColor('#BDBDBD')
          .width('100%')
          .textAlign(TextAlign.Center)
          .onClick(() => {
            this.showDetailModal = false
          })
      }
      .width('80%')
      .height('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.End)
    .alignItems(HorizontalAlign.End)
    .onClick(() => {
      this.showDetailModal = false
    })
  }

  build() {
    Stack() {
      Column() {
        this.headerBuilder()

        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tab0()
            } else if (this.currentTab === 1) {
              this.tab1()
            } else if (this.currentTab === 2) {
              this.tab2()
            } else if (this.currentTab === 3) {
              this.tab3()
            } else if (this.currentTab === 4) {
              this.tab4()
            } else if (this.currentTab === 5) {
              this.tab5()
            } else {
              this.tab6()
            }
          }
          .width('100%')
        }
        .layoutWeight(1)
        .width('100%')

        this.bottomBuilder()
      }
      .width('100%')
      .height('100%')

      if (this.showAddModal) {
        this.addModal()
      }
      if (this.showBuyModal) {
        this.buyModal()
      }
      if (this.showTrainModal) {
        this.trainModal()
      }
      if (this.showDeleteModal) {
        this.deleteModal()
      }
      if (this.showDetailModal) {
        this.detailModal()
      }
    }
    .width('100%')
    .height('100%')
  }
}


总结

在这里插入图片描述

本文详细剖析了一个基于HarmonyOS API 24的ArkTS赛道日体验馆全场景应用的技术实现。从统一数据模型设计到响应式状态管理,从多Tab页面构建器到五种弹窗交互系统,该应用完整展示了声明式UI范式在赛道驾驶这一汽车运动垂直领域下的实践方法。GoodsItem统一接口使得赛道日场次、圈速记录、性能改装件、培训课程、社区帖子和系统消息六大业务实体共享同一套数据结构和渲染逻辑。@State响应式状态管理配合@Builder构建器函数实现了数据驱动UI更新的核心机制——步进器数值变化、标签选择切换、话题筛选、弹窗显隐等所有交互都通过状态变量变更自动触发UI刷新。

在数据可视化层面,该应用的圈速榜Tab是最具技术亮点的模块。尾速柱状图通过归一化高度计算将不同车手的尾速数据转化为直观的柱形对比,金银铜奖牌徽章通过idx索引的条件渲染实现了排名视觉差异化——第1名计时黄配脉冲、第2名浅灰、第3名棕色、其余浅灰,这种条件分支渲染展示了ArkTS在竞赛排名场景下的表现力。性能件Tab的马力柱状图采用红黄交替着色增强视觉辨识度,横滑卡片中的22px大号马力数值配脉冲动画是该Tab的视觉焦点。消息Tab通过item.hot的值进行三路条件判断实现头像颜色差异化(群聊深灰、私聊深橙、通知赛道红),通过item.score > 60判断未读状态并渲染脉冲红点——这种通过业务数据字段值控制UI状态的差异化渲染是ArkTS条件渲染的典型应用。

从工程架构角度审视,该应用的代码组织体现了良好的设计原则。三种费用计算方法(addFeebuyFeetrainFee)采用统一的单价 * 步进器计算逻辑配合不同的默认兜底值,保证了弹窗在未选择条目时也不会异常。五种弹窗的行动按钮颜色差异化(赛道红/沥青黑/深橙)为用户提供了场景区分的视觉提示。详情面板的跨弹窗跳转(关闭详情面板后打开报名弹窗)通过连续修改多个@State变量实现,展示了ArkTS多状态联动的能力。build方法通过Stack层叠主页面和弹窗,if条件判断控制弹窗的独立渲染和卸载。在HarmonyOS 6.1.1的ArkTS框架下,该应用充分展现了声明式UI在赛道驾驶这一高互动、多场景领域的开发效率和可维护性优势,为汽车运动类应用的开发提供了完整的技术参考范本。

Logo

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

更多推荐