引言

在这里插入图片描述

汽车音响改装是一个兼具技术深度与用户个性化需求的垂直领域,涵盖喇叭、主机、功放、低音炮、隔音材料等多品类器材的选购与搭配,同时涉及门店施工预约、试音间体验和改装方案定制等复杂业务流程。将这样一个多维度、高专业度的后市场服务场景转化为移动应用页面,需要开发者具备扎实的数据建模能力、丰富的可视化呈现手段以及精细的交互流程设计能力。本文将深入解析一款基于HarmonyOS ArkTS声明式UI框架构建的汽车音响改装舱应用页面,该页面以深夜蓝为主色调、声浪橙为强调色、电音青为辅助色,营造出演播室般的专业声学氛围。

在技术架构方面,该应用采用HarmonyOS ArkTS的声明式组件模型,通过@Entry@Component装饰器定义入口组件,以@State装饰器管理十余个响应式状态变量。应用以单一GoodsItem接口统一描述所有商品数据结构,通过七组私有数据数组分别承载音响集市、喇叭主机、低音炮、隔音工程、试音间、消息通知及菜单功能的数据。整个页面以Tab切换为核心导航模式,配合五类弹窗实现完整的选品-比价-预约-试听-改装业务闭环,所有交互均通过状态驱动的方式自动触发UI更新,体现了声明式编程范式的核心优势。

在视觉设计与交互体验方面,应用采用了深色系配色策略——以深夜蓝(#0D1B2A)作为主背景色,声浪橙(#FF6D00)作为价格强调色和操作按钮色,电音青(#00B8D4)作为标签和辅助信息色。这种配色方案不仅契合了汽车音响改装"夜间驾驶听歌"的使用场景想象,也通过高对比度色彩组合确保了信息层级的清晰可读性。应用在多个关键数据点(如价格、热度标签、评分等)添加了700毫秒无限循环的脉冲缩放动画,通过scaleanimation属性的组合运用,为静态页面注入了动态的视觉节奏感,仿佛音乐节拍在UI层面的映射。

数据接口与状态定义

在这里插入图片描述

应用以一个简洁而通用的数据接口作为所有商品列表的数据契约,同时定义了丰富的状态变量来管理Tab切换和弹窗显隐。

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 showPackModal: boolean = false
  @State showShopModal: 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 stepDays: number = 1
  @State inputDesc: string = ''

  private tabs: string[] = ['音响集市', '喇叭主机', '低音炮', '隔音工程', '试音间', '消息', '我的']
  private kindChips: string[] = ['喇叭', '主机', '功放', '低音炮', '隔音材料']
  private brandChips: string[] = ['丹拿', '摩雷', '劲浪', '阿尔派', '先锋']
  private planChips: string[] = ['入门音改', '进阶音改', '发烧音改', '全家桶']
  private shopChips: string[] = ['国贸店', '望京店', '五棵松店', '亦庄店']
  private timeChips: string[] = ['上午', '下午', '晚间']
  private fitChips: string[] = ['轿车', 'SUV', 'MPV', '掀背车', '硬派越野']
  private styleChips: string[] = ['人声', '摇滚', '电子', '古典', '嘻哈', '民谣']
  private menuList: string[] = ['我的器材库', '改装订单', '收藏搭配', '预约记录', '收货地址', '设置中心']

GoodsItem接口定义了七个字段,其中num字段在不同列表中具有不同的语义:在mainListspeakList中表示额定功率瓦特数,在subList中表示低音炮英寸尺寸,在muteList中表示降噪分贝值,在roomList中表示试听次数,在msgList中表示未读消息条数。这种字段多义性设计虽然增加了开发者的认知负担,但有效减少了接口定义数量,在中小型应用中是一种可接受的设计权衡。状态变量中,stepNumstepDays分别用于套餐定制弹窗的喇叭数量步进和门店预约弹窗的施工天数步进,体现了不同弹窗场景对步进器语义的差异化需求。

核心商品数据源

在这里插入图片描述

音响集市主列表包含24款音响器材,覆盖了从入门级同轴喇叭到旗舰级三分频套装的完整产品线。

  private mainList: GoodsItem[] = [
    { name: '丹拿 Esotec 236 两分频套装', price: 3299, desc: '6.5寸中低音 · 4欧阻抗', score: 96, num: 150, tag: '热销', hot: '爆' },
    { name: '摩雷 UFO 顶级三分频套装', price: 8999, desc: '7寸中低音 · 3欧阻抗', score: 97, num: 180, tag: '旗舰', hot: '荐' },
    { name: '劲浪 PS165F 两分频喇叭', price: 2899, desc: '6.5寸玻璃纤维盆 · 4欧', score: 94, num: 120, tag: '口碑', hot: '赞' },
    { name: '阿尔派 PXW-654 四路功放', price: 2599, desc: '4通道 · 480W 额定输出', score: 92, num: 320, tag: '热销', hot: '爆' },
    { name: '先锋 TS-F1640R 同轴喇叭', price: 699, desc: '6.5寸同轴 · 250W 峰值', score: 89, num: 220, tag: '性价比', hot: '火' },
    { name: 'JBL Stadium GTO 620M', price: 1380, desc: '6.5寸两分频 · 92dB 灵敏度', score: 91, num: 260, tag: '口碑', hot: '赞' },
    { name: '丹拿 Esotec 232 前声场', price: 2199, desc: '6.5寸两分频 · 4欧阻抗', score: 95, num: 175, tag: '热销', hot: '爆' },
    { name: '赫兹 Mille Pro MPX 165', price: 3480, desc: '6.5寸中低音 · 3欧阻抗', score: 93, num: 130, tag: '进口', hot: '荐' },
    { name: '捷力 C1-650 两分频套装', price: 1580, desc: '6.5寸 · 90dB 灵敏度', score: 90, num: 240, tag: '性价比', hot: '火' },
    { name: '阿尔派 iLX-W650 主机', price: 3280, desc: '9寸悬浮屏 · 无线CarPlay', score: 94, num: 200, tag: '热销', hot: '爆' },
    { name: '先锋 DMH-Z5350BT 主机', price: 2380, desc: '6.8寸屏 · 蓝牙5.0', score: 91, num: 210, tag: '口碑', hot: '赞' },
    { name: '歌乐 A-50 四路功放', price: 1899, desc: 'D类 · 500W 额定', score: 90, num: 350, tag: '性价比', hot: '火' },
    { name: '来福 P300-12 有源低音炮', price: 3680, desc: '12寸内置功放 · 300W', score: 92, num: 300, tag: '热销', hot: '爆' },
    { name: 'Kicker 44CWCS124 低音炮', price: 2299, desc: '12寸 · 双4欧音圈', score: 93, num: 280, tag: '进口', hot: '荐' },
    { name: 'JL Audio 10TW3 薄型低音', price: 4599, desc: '10寸超薄 · 深度仅68mm', score: 96, num: 160, tag: '旗舰', hot: '荐' },
    { name: '大白鲨隔音四门套餐', price: 899, desc: '止震板+吸音棉 · 四门', score: 88, num: 420, tag: '性价比', hot: '火' },
    { name: '平静隔音底盘套餐', price: 1280, desc: '底盘+后备箱 · 8平米', score: 90, num: 380, tag: '口碑', hot: '赞' },
    { name: '摩雷 Virtus 602 两分频', price: 3980, desc: '6寸中低音 · 3欧阻抗', score: 95, num: 140, tag: '进口', hot: '荐' },
    { name: '劲浪 RSE-165 两分频', price: 1690, desc: '6.5寸 · 91dB 灵敏度', score: 89, num: 230, tag: '性价比', hot: '火' },
    { name: '丹拿 Esotec 130 中音', price: 2890, desc: '5.2寸中音 · 4欧阻抗', score: 94, num: 110, tag: '发烧', hot: '荐' },
    { name: '史泰格 ST400 四路功放', price: 2190, desc: 'AB类 · 400W 额定', score: 91, num: 330, tag: '口碑', hot: '赞' },
    { name: '德高 RS-165 舞台同轴', price: 980, desc: '6.5寸同轴 · 4欧', score: 87, num: 250, tag: '性价比', hot: '火' },
    { name: '卡顿 DSP-8 处理器', price: 1480, desc: '8通道 · 延时校正', score: 92, num: 310, tag: '热销', hot: '爆' },
    { name: '骇客 V12 超薄低音炮', price: 2680, desc: '8寸有源 · 备胎位安装', score: 90, num: 290, tag: '口碑', hot: '赞' }
  ]

24款器材覆盖了喇叭(丹拿、摩雷、劲浪、JBL、捷力、赫兹)、主机(阿尔派、先锋)、功放(阿尔派、歌乐、史泰格)、低音炮(来福、Kicker、JL Audio、骇客)、隔音材料(大白鲨、平静)和DSP处理器(卡顿)六大品类,价格从699元的入门同轴喇叭到8999元的顶级三分频套装,形成了完整的价格梯度。每条数据的desc字段包含了专业参数信息(如尺寸、阻抗、灵敏度、功率等),这些信息在列表中直接展示给用户,帮助具有专业知识的用户快速筛选。hot字段使用"爆"“荐”“赞”"火"四个字来标记热度等级,在UI中以橙色背景胶囊的形式呈现。

专业数据源:喇叭、低音炮、隔音与试音间

在这里插入图片描述

除主列表外,应用还定义了四个专业维度的数据源,分别服务于喇叭主机Tab、低音炮Tab、隔音工程Tab和试音间Tab。

  private speakList: GoodsItem[] = [
    { name: '丹拿 236 中低音单元', price: 3299, desc: '6.5寸 · 150W 额定', score: 96, num: 150, tag: '4Ω', hot: '爆' },
    { name: '摩雷 UFO 中低音单元', price: 8999, desc: '7寸 · 180W 额定', score: 97, num: 180, tag: '3Ω', hot: '荐' },
    { name: '劲浪 PS165F 中低音', price: 2899, desc: '6.5寸 · 120W 额定', score: 94, num: 120, tag: '4Ω', hot: '赞' },
    { name: '阿尔派 PXW-654 功放', price: 2599, desc: '4通道 · 480W 额定', score: 92, num: 480, tag: '4Ω', hot: '爆' },
    { name: '先锋 TS-F1640 同轴', price: 699, desc: '6.5寸 · 220W 峰值', score: 89, num: 220, tag: '4Ω', hot: '火' },
    { name: 'JBL GTO 620M 中低音', price: 1380, desc: '6.5寸 · 260W 峰值', score: 91, num: 260, tag: '3Ω', hot: '赞' },
    { name: '赫兹 MPX 165 中低音', price: 3480, desc: '6.5寸 · 130W 额定', score: 93, num: 130, tag: '3Ω', hot: '荐' },
    { name: '歌乐 A-50 功放', price: 1899, desc: 'D类 · 500W 额定', score: 90, num: 500, tag: '2Ω', hot: '火' },
    { name: '史泰格 ST400 功放', price: 2190, desc: 'AB类 · 400W 额定', score: 91, num: 400, tag: '4Ω', hot: '赞' },
    { name: '卡顿 DSP-8 处理器', price: 1480, desc: '8通道 · 310W 输出', score: 92, num: 310, tag: '4Ω', hot: '爆' },
    { name: '捷力 C1-650 中低音', price: 1580, desc: '6.5寸 · 240W 峰值', score: 90, num: 240, tag: '4Ω', hot: '火' },
    { name: '骇客 V12 低音单元', price: 2680, desc: '8寸有源 · 290W 峰值', score: 90, num: 290, tag: '2Ω', hot: '赞' }
  ]

  private subList: GoodsItem[] = [
    { name: 'K牌 8寸薄型低音炮', price: 1699, desc: '深度90mm · 备胎位', score: 90, num: 8, tag: '8寸', hot: '火' },
    { name: '来福 10寸有源低音炮', price: 2699, desc: '内置功放 · 300W', score: 92, num: 10, tag: '10寸', hot: '爆' },
    { name: 'JBL 12寸双音圈低音炮', price: 2299, desc: '双4欧 · 500W峰值', score: 93, num: 12, tag: '12寸', hot: '荐' },
    { name: '捷力 10W3v3-4 低音炮', price: 3180, desc: '10寸 · 500W 峰值', score: 95, num: 10, tag: '10寸', hot: '荐' },
    { name: 'Kicker 12寸 CWCS124', price: 2299, desc: '12寸 · 双4欧音圈', score: 93, num: 12, tag: '12寸', hot: '赞' },
    { name: '摩雷 15寸神炮', price: 5680, desc: '15寸 · 1000W峰值', score: 96, num: 15, tag: '15寸', hot: '荐' },
    { name: '骇客 8寸备胎低音炮', price: 2680, desc: '备胎位 · 290W峰值', score: 90, num: 8, tag: '8寸', hot: '火' },
    { name: '丹拿 18寸至尊低音', price: 8999, desc: '18寸 · 1500W峰值', score: 97, num: 18, tag: '18寸', hot: '爆' },
    { name: '劲浪 12寸 SUB 620', price: 3580, desc: '12寸 · 600W 峰值', score: 94, num: 12, tag: '12寸', hot: '赞' },
    { name: '阿尔派 10寸 SWT-10', price: 1980, desc: '10寸 · 350W 峰值', score: 91, num: 10, tag: '10寸', hot: '火' }
  ]

  private muteList: GoodsItem[] = [
    { name: '大白鲨四门双层隔音', price: 899, desc: '止震板+吸音棉 · 车门', score: 88, num: 28, tag: '入门', hot: '火' },
    { name: '平静底盘装甲隔音', price: 1280, desc: '底盘+后舱 · 8平米', score: 90, num: 32, tag: '进阶', hot: '赞' },
    { name: 'STP航空级隔音套餐', price: 2680, desc: '俄罗斯进口 · 全车', score: 95, num: 38, tag: '发烧', hot: '荐' },
    { name: '大能蓝钻四门隔音', price: 1180, desc: '2.0mm止震 · 车门', score: 91, num: 30, tag: '进阶', hot: '赞' },
    { name: '止震板引擎盖隔音', price: 380, desc: '铝箔止震 · 发动机舱', score: 85, num: 26, tag: '入门', hot: '火' },
    { name: '后备箱空腔隔音', price: 960, desc: '空腔填充 · 后备箱', score: 92, num: 34, tag: '进阶', hot: '赞' },
    { name: '轮弧翼子板隔音', price: 720, desc: '轮弧衬套 · 四轮', score: 89, num: 29, tag: '入门', hot: '火' },
    { name: '车门密封条加固', price: 260, desc: '三层密封 · 四门', score: 84, num: 25, tag: '入门', hot: '火' },
    { name: '顶棚吸音棉施工', price: 880, desc: '白棉填充 · 车顶', score: 88, num: 27, tag: '进阶', hot: '赞' },
    { name: '防火墙隔音升级', price: 1580, desc: '隔音垫 · 防火墙', score: 93, num: 36, tag: '发烧', hot: '荐' }
  ]

  private roomList: GoodsItem[] = [
    { name: '声压老张', price: 0, desc: '丹拿236配阿尔派功放,人声甜到冒泡,蔡琴的口水声都在', score: 96, num: 356, tag: '丹拿+阿尔派', hot: '爆' },
    { name: '低音狂魔', price: 0, desc: 'JL 10TW3塞进备胎位,下潜深不见底,鼓点砸胸口', score: 94, num: 289, tag: 'JL+骇客', hot: '火' },
    { name: '改装匠老李', price: 0, desc: '四门大白鲨隔音做完整车安静一档,胎噪基本听不见了', score: 90, num: 198, tag: '大白鲨', hot: '赞' },
    { name: '夜路听歌的人', price: 0, desc: '先锋主机直推劲浪,无损音源差距真的大,别再听MP3了', score: 92, num: 245, tag: '先锋+劲浪', hot: '赞' },
    { name: '电音控KK', price: 0, desc: 'DSP调完延时,声场仪表台正中,低频弹性拉满', score: 95, num: 412, tag: '卡顿DSP', hot: '爆' },
    { name: '摩雷发烧友', price: 0, desc: 'UFO三分频倒模A柱,结像精准到能数出歌手站位', score: 97, num: 520, tag: '摩雷UFO', hot: '荐' },
    { name: '途观车主小周', price: 0, desc: '原车喇叭对比改装喇叭,同一首歌像换了张唱片', score: 89, num: 176, tag: '捷力套装', hot: '火' },
    { name: '古典乐派赵教授', price: 0, desc: '弦乐群奏层次分明,小提琴泛音一丝不乱,值回票价', score: 93, num: 203, tag: '赫兹套装', hot: '赞' },
    { name: '跑山青年阿凯', price: 0, desc: '功放推力余量要留足,动态一上来才知道什么叫从容', score: 91, num: 234, tag: '史泰格', hot: '赞' },
    { name: '奶爸车主大熊', price: 0, desc: '有源低音炮免布线,后备箱还能塞婴儿车,真香', score: 88, num: 158, tag: '来福有源', hot: '火' }
  ]

speakList中的tag字段存储阻抗值(2Ω、3Ω、4Ω),num表示额定功率瓦特数,这些数据将在喇叭主机Tab中用于绘制功率对比柱状图。subList中的tag字段存储英寸尺寸(8寸-18寸),num也存储英寸数值,在低音炮Tab中用于箱体尺寸分布柱状图。muteList中的tag按"入门"“进阶”"发烧"三级分类,num表示降噪分贝值,在隔音工程Tab中用于降噪量对比柱状图。roomList是试音间Tab的数据源,desc字段存储了车友的试听感受描述,num表示试听次数,tag标注了搭配方案的品牌组合信息。

辅助方法设计

在这里插入图片描述

应用定义了一组辅助方法用于安全访问选中商品属性和计算列表最大值,这些方法在多个Tab页面和弹窗中被反复调用。

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

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

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

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

  selHot(): string {
    if (this.selectedItem !== null) {
      return this.selectedItem.hot
    }
    return ''
  }

  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
  }

五个sel*方法遵循统一的防御性编程模式:先检查selectedItem是否为null,非空时返回对应属性,空时返回默认值。这种设计确保了详情弹窗在用户未选中商品时不会因空指针访问而崩溃。maxNumOf方法通过简单的线性遍历找到列表中num字段的最大值,该值在柱状图渲染中用于计算各柱条的相对高度,实现数据的归一化缩放。值得注意的是,初始值设为1而非0,这确保了当列表中所有num值都为0时不会出现除以零的异常。

静态头部与底部导航

在这里插入图片描述

头部组件集成了品牌标识、搜索入口和消息通知功能,底部导航栏实现七个Tab的快速切换。

  // ============ 头部(静态,无任何动画) ============
  @Builder
  headerBuilder() {
    Column({ space: 10 }) {
      Row() {
        Text('汽车音响改装舱')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Column()
          .layoutWeight(1)
        Stack({ alignContent: Alignment.TopEnd }) {
          Text('♡')
            .fontSize(20)
            .fontColor('#FFFFFF')
        }
        .width(32)
        .height(32)
        .margin({ right: 6 })
        Stack({ alignContent: Alignment.TopEnd }) {
          Text('✉')
            .fontSize(20)
            .fontColor('#FFFFFF')
          Text('3')
            .fontSize(9)
            .fontColor('#FFFFFF')
            .backgroundColor('#E53935')
            .borderRadius(8)
            .padding({ left: 4, right: 4, top: 1, bottom: 1 })
        }
        .width(32)
        .height(32)
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12 })

      Row({ space: 8 }) {
        Text('🎵')
          .fontSize(16)
        Text('搜喇叭/功放/低音炮')
          .fontSize(13)
          .fontColor('#8FA3B8')
      }
      .width('100%')
      .height(38)
      .borderRadius(19)
      .backgroundColor('#1B2F45')
      .padding({ left: 14, right: 14 })
      .alignItems(VerticalAlign.Center)
      .margin({ left: 16, right: 16 })

      Row({ space: 6 }) {
        Text('🔥')
          .fontSize(12)
        Text('本季爆款:丹拿改装套餐 · 已交付 682 台')
          .fontSize(12)
          .fontColor('#FFB877')
      }
      .width('100%')
      .alignItems(VerticalAlign.Center)
      .padding({ left: 16, right: 16, bottom: 12 })
    }
    .width('100%')
    .backgroundColor('#0D1B2A')
  }

头部以深夜蓝#0D1B2A为背景,采用三层垂直布局。顶部为品牌标题行,“汽车音响改装舱"使用22号粗体白字,右侧依次放置收藏按钮和消息按钮,消息按钮通过StackalignContent: Alignment.TopEnd在右上角叠加了红色未读数角标"3”。中间层是搜索胶囊条,使用深蓝色#1B2F45背景和19像素圆角,内部包含音符Emoji和搜索提示文字。底部为爆款信息条,以浅橙色#FFB877文字展示当季热销数据。整个头部注释标明"静态,无任何动画",与各Tab页面内的动画元素形成动静对比。

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

底部导航栏通过ForEach渲染七个Tab项,每个Tab使用layoutWeight(1)实现等宽分布。当前选中的Tab文字颜色为深夜蓝#0D1B2A,底部指示条为声浪橙#FF6D00;未选中的Tab文字为灰色#999999,指示条为透明色。这种基于状态的条件样式切换完全由currentTab状态变量驱动,体现了声明式UI的核心设计理念。

音响集市Tab:统计看板与商品列表

在这里插入图片描述

音响集市是首页Tab,展示运营数据概览和24款音响器材的列表,是用户浏览商品的主入口。

  // ============ Tab1 音响集市 ============
  @Builder
  tab0() {
    Column({ space: 10 }) {
      Row({ space: 8 }) {
        Column({ space: 4 }) {
          Text('386')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0D1B2A')
          Text('在售器材')
            .fontSize(11)
            .fontColor('#999999')
        }
        .layoutWeight(1)
        .height(64)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)

        Column({ space: 4 }) {
          Text('124')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF6D00')
          Text('本月改装')
            .fontSize(11)
            .fontColor('#999999')
        }
        .layoutWeight(1)
        .height(64)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)

        Column({ space: 4 }) {
          Text('98%')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00B8D4')
          Text('好评率')
            .fontSize(11)
            .fontColor('#999999')
        }
        .layoutWeight(1)
        .height(64)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })

三栏统计卡片采用等宽布局,数字分别使用深夜蓝、声浪橙和电音青三种主题色,形成了与品牌配色一致的数据层级。每张卡片固定64像素高度,使用白色背景和10像素圆角,内部通过alignItems(HorizontalAlign.Center)justifyContent(FlexAlign.Center)实现内容居中对齐。

      ForEach(this.mainList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Column() {
            Text(item.name.substring(0, 1))
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
          }
          .width(46)
          .height(46)
          .borderRadius(23)
          .backgroundColor('#0D1B2A')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)

          Column({ space: 4 }) {
            Text(item.name)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#222222')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Text(item.desc)
              .fontSize(11)
              .fontColor('#999999')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Row({ space: 4 }) {
              Column()
                .width(item.score + '%')
                .height(4)
                .borderRadius(2)
                .backgroundColor('#FF6D00')
              Column()
                .layoutWeight(1)
                .height(4)
                .borderRadius(2)
                .backgroundColor('#EEEEEE')
            }
            .width('100%')

            Text('口碑 ' + item.score + '%')
              .fontSize(10)
              .fontColor('#FF6D00')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column({ space: 6 }) {
            Text('¥' + item.price)
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E53935')
            Text(item.tag)
              .fontSize(10)
              .fontColor('#00B8D4')
              .border({ width: 1, color: '#00B8D4' })
              .borderRadius(4)
              .padding({ left: 4, right: 4, top: 1, bottom: 1 })
            Row({ space: 3 }) {
              Text('🔥' + item.num)
                .fontSize(10)
                .fontColor('#FF6D00')
              Text(item.hot)
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#FF6D00')
                .borderRadius(6)
                .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            }
            .alignItems(VerticalAlign.Center)
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding(12)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(VerticalAlign.Center)
        .margin({ left: 12, right: 12, bottom: 8 })
        .onClick(() => {
          this.selectedItem = item
          this.showDetailModal = true
        })
      }, (item: GoodsItem) => item.name)

      Row({ space: 8 }) {
        Text('想出闲置器材?')
          .fontSize(13)
          .fontColor('#666666')
        Text('去发布')
          .fontSize(13)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF6D00')
          .borderRadius(14)
          .padding({ left: 14, right: 14, top: 6, bottom: 6 })
          .onClick(() => {
            this.chipA = 0
            this.chipB = 0
            this.inputDesc = ''
            this.showAddModal = true
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .padding({ top: 4, bottom: 20 })
    }
    .width('100%')
    .padding({ top: 10 })
    .backgroundColor('#F5F5F5')
  }

商品列表的每行卡片采用三段式布局。左侧为46x46圆形头像,使用商品名称首字符item.name.substring(0, 1)作为显示文字,深夜蓝背景。中间信息区使用textOverflow({ overflow: TextOverflow.Ellipsis })实现单行溢出省略,好评率进度条采用双层Column结构——内层橙色进度条宽度设为item.score + '%',外层灰色背景条填充剩余宽度。右侧价格区使用警示红#E53935显示价格,电音青描边标签显示tag,热度标签hot添加了脉冲缩放动画。底部"去发布"按钮绑定onClick事件,重置选择状态后打开发布弹窗。

页面交互流程图

以下是用户从浏览音响器材到完成改装预约的完整交互流程:

音响集市

喇叭主机

低音炮

隔音工程

试音间

消息

我的

立即购买

关闭返回

应用启动

音响集市首页

用户选择Tab

浏览24款器材

功率对比柱状图

箱体尺寸分布图

降噪量对比图

试音案例列表

消息通知列表

个人中心

点击商品卡片

点击商品卡片

详情弹窗右滑展开

用户决策

关闭详情弹窗

点击隔音套餐

改装套餐弹窗

选择方案与喇叭数量

提交套餐方案

点击进店试听

门店预约弹窗

选择门店与时段

设置施工天数

确认预约

点击发布闲置

发布器材弹窗

选择品类品牌

填写描述上架

点击去发布

喇叭主机Tab:功率柱状图与网格卡片

喇叭主机Tab通过横向滚动的功率对比柱状图和双列网格卡片展示喇叭与功放产品。

  // ============ Tab2 喇叭主机 ============
  @Builder
  tab1() {
    Column({ space: 10 }) {
      Text('额定功率对比 (W)')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#0D1B2A')
        .margin({ left: 12, top: 10 })

      Scroll() {
        Row({ space: 10 }) {
          ForEach(this.speakList, (item: GoodsItem) => {
            Column({ space: 4 }) {
              Text(item.num + 'W')
                .fontSize(9)
                .fontColor('#FF6D00')
              Column()
                .width(16)
                .height(item.num / this.maxNumOf(this.speakList) * 120)
                .borderRadius(4)
                .backgroundColor(item.num > 300 ? '#FF6D00' : '#00B8D4')
              Text(item.name.substring(0, 2))
                .fontSize(9)
                .fontColor('#666666')
            }
            .alignItems(HorizontalAlign.Center)
          }, (item: GoodsItem) => item.name)
        }
        .alignItems(VerticalAlign.End)
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .height(170)
      .backgroundColor('#FFFFFF')
      .margin({ left: 12, right: 12 })
      .borderRadius(10)

功率柱状图是该Tab的核心可视化组件。通过Scroll容器配合scrollable(ScrollDirection.Horizontal)实现水平滚动,scrollBar(BarState.Off)隐藏滚动条以保持界面简洁。每个柱条由数值标签、柱条本体和名称缩写三部分组成,柱条高度公式为item.num / this.maxNumOf(this.speakList) * 120。柱条颜色根据功率阈值动态切换——当item.num > 300时使用声浪橙,否则使用电音青,这种条件着色帮助用户快速识别高功率器材。

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.speakList, (item: GoodsItem) => {
          Column({ space: 6 }) {
            Row({ space: 6 }) {
              Text(item.tag)
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#00B8D4')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              Text(item.hot)
                .fontSize(10)
                .fontColor('#FF6D00')
            }
            .alignItems(VerticalAlign.Center)

            Text(item.name)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#222222')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })

            Text(item.desc)
              .fontSize(11)
              .fontColor('#999999')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })

            Row({ space: 4 }) {
              Text('口碑 ' + item.score + '%')
                .fontSize(10)
                .fontColor('#FF6D00')
            }

            Text('¥' + item.price)
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E53935')
          }
          .width('48%')
          .padding(12)
          .borderRadius(10)
          .backgroundColor('#FFFFFF')
          .margin({ bottom: 10 })
          .alignItems(HorizontalAlign.Start)
          .onClick(() => {
            this.selectedItem = item
            this.showDetailModal = true
          })
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12, bottom: 20 })
    }
    .width('100%')
    .backgroundColor('#F5F5F5')
  }

双列网格卡片使用Flex容器配合wrap: FlexWrap.Wrap实现自动换行布局。每个卡片宽度为48%,通过justifyContent: FlexAlign.SpaceBetween在两列间分配间距。卡片顶部标签行展示了阻抗值标签(带脉冲动画)和热度标记,下方依次为商品名称、描述、口碑百分比和价格。卡片整体绑定onClick事件,点击后打开详情弹窗。

低音炮与隔音工程Tab

低音炮Tab设计了深色统计看板、箱体尺寸柱状图和横向滑动卡片列表;隔音工程Tab通过降噪量柱状图和施工列表展示隔音方案。

  // ============ Tab3 低音炮 ============
  @Builder
  tab2() {
    Column({ space: 10 }) {
      Row({ space: 8 }) {
        Column({ space: 4 }) {
          Text('10')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF6D00')
          Text('在售低音炮')
            .fontSize(11)
            .fontColor('#8FA3B8')
        }
        .layoutWeight(1)
        .height(70)
        .borderRadius(10)
        .backgroundColor('#1B2F45')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)

        Column({ space: 4 }) {
          Text('328')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00B8D4')
          Text('本月成交')
            .fontSize(11)
            .fontColor('#8FA3B8')
        }
        .layoutWeight(1)
        .height(70)
        .borderRadius(10)
        .backgroundColor('#1B2F45')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)

        Column({ space: 4 }) {
          Text('27L')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFB877')
          Text('平均箱体容积')
            .fontSize(11)
            .fontColor('#8FA3B8')
        }
        .layoutWeight(1)
        .height(70)
        .borderRadius(10)
        .backgroundColor('#1B2F45')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 10 })
      .backgroundColor('#0D1B2A')

低音炮Tab的统计看板使用深蓝色#1B2F45卡片背景,与页面顶部的深夜蓝背景#0D1B2A形成微妙的色差层次。三栏数据分别使用声浪橙、电音青和浅橙色,展示在售数量、月成交量(台)和平均箱体容积(升)。70像素的卡片高度配合justifyContent(FlexAlign.Center)确保内容垂直居中。

      Scroll() {
        Row({ space: 12 }) {
          ForEach(this.subList, (item: GoodsItem) => {
            Column({ space: 8 }) {
              Column({ space: 4 }) {
                Text(item.tag)
                  .fontSize(34)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FFFFFF')
                  .scale({ x: 1.05, y: 1.05 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
                Text('英寸箱体')
                  .fontSize(10)
                  .fontColor('#8FA3B8')
              }
              .width(130)
              .height(80)
              .borderRadius(10)
              .backgroundColor('#1B2F45')
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)

              Text(item.name)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#222222')
                .maxLines(1)

              Text(item.desc)
                .fontSize(10)
                .fontColor('#999999')
                .maxLines(1)

              Text('¥' + item.price)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E53935')
            }
            .width(130)
            .padding(10)
            .borderRadius(10)
            .backgroundColor('#FFFFFF')
            .alignItems(HorizontalAlign.Start)
            .onClick(() => {
              this.selectedItem = item
              this.showDetailModal = true
            })
          }, (item: GoodsItem) => item.name)
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .padding({ bottom: 20 })
    }
    .width('100%')
    .backgroundColor('#F5F5F5')
  }

横向滑动卡片列表中每张卡片固定130像素宽度,顶部为深蓝色箱体尺寸展示区,英寸数值以34号大字配合脉冲动画展示。卡片整体绑定onClick打开详情弹窗。

隔音工程Tab的降噪量柱状图使用了与功率柱状图类似的归一化算法,但增加了基于阈值的双色逻辑——当item.num >= 34时柱条为声浪橙,否则为电音青。隔音施工列表中每行卡片左侧使用4像素宽的色条标识(同样基于34dB阈值变色),右侧展示降噪分贝值(带脉冲动画)和价格。

试音间与消息Tab

试音间Tab以车友试听评价列表的形式展示改装效果,消息Tab以通知列表展示各类消息。

  // ============ Tab5 试音间 ============
  @Builder
  tab4() {
    Column({ space: 10 }) {
      Scroll() {
        Row({ space: 8 }) {
          ForEach(this.styleChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .fontColor(index === 0 ? '#FFFFFF' : '#0D1B2A')
              .backgroundColor(index === 0 ? '#FF6D00' : '#FFFFFF')
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
          }, (chip: string) => chip)
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .margin({ top: 10 })

      ForEach(this.roomList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Column() {
            Text(item.name.substring(0, 1))
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
          }
          .width(42)
          .height(42)
          .borderRadius(21)
          .backgroundColor('#00B8D4')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)

          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(item.name)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#222222')
              Text(item.tag)
                .fontSize(9)
                .fontColor('#FF6D00')
                .backgroundColor('#FFF3E6')
                .borderRadius(4)
                .padding({ left: 4, right: 4, top: 1, bottom: 1 })
            }
            .alignItems(VerticalAlign.Center)

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

            Text('口碑指数 ' + item.score + '%')
              .fontSize(10)
              .fontColor('#00B8D4')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column({ space: 4 }) {
            Text('♪')
              .fontSize(14)
              .fontColor('#FF6D00')
            Text('' + item.num)
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF6D00')
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            Text('试听')
              .fontSize(10)
              .fontColor('#999999')
          }
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .padding(12)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(VerticalAlign.Top)
        .margin({ left: 12, right: 12, bottom: 8 })
      }, (item: GoodsItem) => item.name)

      Text('进店试听 · 免费预约')
        .fontSize(13)
        .fontColor('#FFFFFF')
        .backgroundColor('#0D1B2A')
        .borderRadius(16)
        .padding({ left: 20, right: 20, top: 8, bottom: 8 })
        .margin({ bottom: 20 })
        .onClick(() => {
          this.chipA = 0
          this.chipB = 0
          this.stepDays = 1
          this.showShopModal = true
        })
    }
    .width('100%')
    .backgroundColor('#F5F5F5')
  }

试音间Tab顶部是音乐风格标签栏(人声、摇滚、电子、古典、嘻哈、民谣),通过横向滚动展示,首个标签以声浪橙填充表示选中。每个试音案例卡片包含用户头像(电音青圆形背景)、用户名与搭配方案标签、试听感受描述(两行省略)、口碑指数和试听次数(带音符图标和脉冲动画)。底部"进店试听"按钮绑定门店预约弹窗。

弹窗系统:发布、套餐与预约

应用设计了五类弹窗,以下展示发布器材弹窗和改装套餐弹窗的核心实现。

  // ============ 弹框1 发布器材 ============
  @Builder
  addModal() {
    Column() {
      Column()
        .layoutWeight(1)
      Column({ space: 14 }) {
        Row() {
          Text('发布音响器材')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0D1B2A')
          Column()
            .layoutWeight(1)
          Text('✕')
            .fontSize(16)
            .fontColor('#999999')
            .onClick(() => {
              this.showAddModal = false
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Text('选择品类')
          .fontSize(13)
          .fontColor('#666666')

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.kindChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .fontColor(this.chipA === index ? '#FFFFFF' : '#0D1B2A')
              .backgroundColor(this.chipA === index ? '#FF6D00' : '#F5F5F5')
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .margin({ right: 8, bottom: 8 })
              .onClick(() => {
                this.chipA = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('选择品牌')
          .fontSize(13)
          .fontColor('#666666')

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.brandChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .fontColor(this.chipB === index ? '#FFFFFF' : '#00B8D4')
              .backgroundColor(this.chipB === index ? '#00B8D4' : '#FFFFFF')
              .border({ width: 1, color: '#00B8D4' })
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .margin({ right: 8, bottom: 8 })
              .onClick(() => {
                this.chipB = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('器材描述')
          .fontSize(13)
          .fontColor('#666666')

        TextInput({ placeholder: '填写器材参数与成色', text: this.inputDesc })
          .fontSize(13)
          .height(44)
          .borderRadius(8)
          .backgroundColor('#F5F5F5')
          .onChange((value: string) => {
            this.inputDesc = value
          })

        Text('立即上架')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF6D00')
          .borderRadius(22)
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .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
    })
  }

发布器材弹窗采用底部弹出模式。品类选择芯片使用声浪橙选中色,品牌选择芯片使用电音青描边样式(选中时填充电音青背景),两种芯片的视觉风格区分帮助用户识别不同维度的选择。TextInput通过text: this.inputDesc实现初始值绑定,onChange回调更新状态变量,形成双向数据流。弹窗内容区使用constraintSize({ maxHeight: '80%' })限制最大高度,内层onClick调用event.stopPropagation()阻止冒泡。

  // ============ 弹框2 改装套餐 ============
  @Builder
  packModal() {
    Column() {
      Column()
        .layoutWeight(1)
      Column({ space: 14 }) {
        Row() {
          Text('改装套餐定制')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0D1B2A')
          Column()
            .layoutWeight(1)
          Text('✕')
            .fontSize(16)
            .fontColor('#999999')
            .onClick(() => {
              this.showPackModal = false
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Column({ space: 4 }) {
          Text(this.selName())
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#222222')
          Text(this.selDesc())
            .fontSize(12)
            .fontColor('#999999')
        }
        .width('100%')
        .padding(12)
        .borderRadius(10)
        .backgroundColor('#F5F5F5')
        .alignItems(HorizontalAlign.Start)

        Text('选择方案')
          .fontSize(13)
          .fontColor('#666666')

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.planChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .fontColor(this.chipA === index ? '#FFFFFF' : '#0D1B2A')
              .backgroundColor(this.chipA === index ? '#0D1B2A' : '#F5F5F5')
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .margin({ right: 8, bottom: 8 })
              .onClick(() => {
                this.chipA = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Row() {
          Text('喇叭数量')
            .fontSize(13)
            .fontColor('#666666')
          Column()
            .layoutWeight(1)
          Text('-')
            .fontSize(16)
            .fontColor('#666666')
            .backgroundColor('#F0F0F0')
            .borderRadius(15)
            .width(30)
            .height(30)
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepNum > 1) {
                this.stepNum = this.stepNum - 1
              }
            })
          Text(' ' + this.stepNum + ' 只 ')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0D1B2A')
            .margin({ left: 10, right: 10 })
          Text('+')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .backgroundColor('#FF6D00')
            .borderRadius(15)
            .width(30)
            .height(30)
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepNum < 8) {
                this.stepNum = this.stepNum + 1
              }
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Row({ space: 6 }) {
          Text('预估报价')
            .fontSize(13)
            .fontColor('#666666')
          Text('¥' + Math.floor(this.packPrice * this.stepNum))
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#E53935')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .alignItems(VerticalAlign.End)

        Text('提交套餐方案')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .backgroundColor('#0D1B2A')
          .borderRadius(22)
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .onClick(() => {
            this.showPackModal = 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.showPackModal = false
    })
  }

改装套餐弹窗是应用中最复杂的弹窗之一。顶部展示选中器材的名称和描述信息,中间是方案选择芯片组(深夜蓝选中色),下方是喇叭数量步进器(减号灰色背景、加号声浪橙背景,上下限为1-8只)。预估报价通过Math.floor(this.packPrice * this.stepNum)实时计算(packPrice默认值为1299),报价数字添加了脉冲缩放动画以强调价格变化。底部提交按钮使用深夜蓝背景,与方案选择芯片形成颜色呼应。

详情弹窗与主页面构建

详情弹窗采用右侧滑出全高面板设计,主页面build方法将所有组件组装成完整的页面结构。

  // ============ 弹框5 详情(右侧滑出全高面板) ============
  @Builder
  detailModal() {
    Column() {
      Column({ space: 12 }) {
        Column({ space: 8 }) {
          Text('🎧')
            .fontSize(48)
          Text(this.selHot() + ' 器材')
            .fontSize(12)
            .fontColor('#8FA3B8')
        }
        .width('100%')
        .height(180)
        .backgroundColor('#0D1B2A')
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)

        Text(this.selName())
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#222222')
          .padding({ left: 16, right: 16 })

        Text(this.selDesc())
          .fontSize(12)
          .fontColor('#999999')
          .padding({ left: 16, right: 16 })

        Row({ space: 4 }) {
          Text('★★★★★')
            .fontSize(14)
            .fontColor('#FF6D00')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('口碑 ' + this.selScore() + '%')
            .fontSize(12)
            .fontColor('#FF6D00')
        }
        .padding({ left: 16, right: 16 })
        .alignItems(VerticalAlign.Center)

        Row({ space: 4 }) {
          Column()
            .width(this.selScore() + '%')
            .height(6)
            .borderRadius(3)
            .backgroundColor('#FF6D00')
          Column()
            .layoutWeight(1)
            .height(6)
            .borderRadius(3)
            .backgroundColor('#EEEEEE')
        }
        .padding({ left: 16, right: 16 })

        Text('¥' + this.selPrice())
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E53935')
          .padding({ left: 16, right: 16 })

        Text('适配车型')
          .fontSize(13)
          .fontColor('#666666')
          .padding({ left: 16, right: 16 })

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.fitChips, (chip: string) => {
            Text(chip)
              .fontSize(11)
              .fontColor('#00B8D4')
              .border({ width: 1, color: '#00B8D4' })
              .borderRadius(4)
              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .margin({ right: 8, bottom: 8 })
          }, (chip: string) => chip)
        }
        .padding({ left: 16, right: 16 })

        Column()
          .layoutWeight(1)

        Text('立即购买')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF6D00')
          .borderRadius(24)
          .width('90%')
          .textAlign(TextAlign.Center)
          .padding({ top: 14, bottom: 14 })
          .margin({ bottom: 24 })
          .onClick(() => {
            this.showDetailModal = false
          })
      }
      .width('80%')
      .height('100%')
      .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%,通过外层容器的justifyContent(FlexAlign.End)alignItems(HorizontalAlign.End)定位到屏幕右侧。面板顶部为180像素高的深夜蓝商品图占位区,包含耳机Emoji和热度标记。中间依次展示商品名称、描述、五星评分(带脉冲动画)、口碑进度条、价格和适配车型标签组(电音青描边样式)。底部"立即购买"按钮使用声浪橙背景和24像素圆角,宽度为90%居中显示。

  // ============ 页面主结构 ============
  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%')
        .scrollBar(BarState.Off)
        this.bottomBuilder()
      }
      .width('100%')
      .height('100%')

      if (this.showAddModal) {
        this.addModal()
      }
      if (this.showPackModal) {
        this.packModal()
      }
      if (this.showShopModal) {
        this.shopModal()
      }
      if (this.showDeleteModal) {
        this.deleteModal()
      }
      if (this.showDetailModal) {
        this.detailModal()
      }
    }
    .width('100%')
    .height('100%')
  }
}

build方法以Stack为根容器,实现弹窗的层叠渲染。主内容区由头部、可滚动内容区和底部导航栏三部分组成,Scroll组件通过scrollBar(BarState.Off)隐藏滚动条。Tab切换通过if-else条件链实现,五个弹窗通过独立的if条件块叠加在Stack上层。这种结构确保了弹窗与主内容之间的独立渲染和事件隔离。

技术点对比

技术维度 实现方式 设计特点 适用场景
配色系统 深夜蓝+声浪橙+电音青三色体系 高对比度深色主题,专业声学氛围 夜间使用场景与专业领域
数据可视化 横向Scroll+ForEach柱状图 归一化算法+阈值双色条件着色 多维度参数对比展示
柱状图着色 阈值条件分支backgroundColor 高于阈值橙色,低于阈值青色 功率/降噪量/隔热率分档标识
弹窗层级 Stack条件渲染叠加 五种弹窗独立if判断,支持并发显隐 多类型弹窗并存场景
表单交互 chipA/chipB双选择器+stepNum/stepDays双步进 通用状态变量复用,不同弹窗语义不同 多弹窗表单数据管理
文本溢出 maxLines+textOverflow(Ellipsis) 单行/双行省略,保持卡片高度一致 长文本列表项布局
动画策略 scale(1.05)+animation(700ms,-1,EaseInOut) 关键数据点脉冲呼吸效果 价格、热度、评分等强调元素
网格布局 Flex(Wrap)+48%宽度+SpaceBetween 自动换行双列卡片 喇叭主机商品网格
滚动体验 scrollBar(BarState.Off)隐藏滚动条 横向滚动列表保持视觉简洁 柱状图与卡片横向滑动
事件控制 stopPropagation阻止冒泡 弹窗内容区点击不关闭,遮罩点击关闭 模态弹窗交互精确控制

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 汽车音响改装舱 - 汽车之家风格 ArkTS 单文件页面
// 配色:深夜蓝 #0D1B2A / 声浪橙 #FF6D00 / 电音青 #00B8D4 / 浅灰底 #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 showPackModal: boolean = false
  @State showShopModal: 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 stepDays: number = 1
  @State inputDesc: string = ''

  private tabs: string[] = ['音响集市', '喇叭主机', '低音炮', '隔音工程', '试音间', '消息', '我的']
  private kindChips: string[] = ['喇叭', '主机', '功放', '低音炮', '隔音材料']
  private brandChips: string[] = ['丹拿', '摩雷', '劲浪', '阿尔派', '先锋']
  private planChips: string[] = ['入门音改', '进阶音改', '发烧音改', '全家桶']
  private shopChips: string[] = ['国贸店', '望京店', '五棵松店', '亦庄店']
  private timeChips: string[] = ['上午', '下午', '晚间']
  private fitChips: string[] = ['轿车', 'SUV', 'MPV', '掀背车', '硬派越野']
  private styleChips: string[] = ['人声', '摇滚', '电子', '古典', '嘻哈', '民谣']
  private menuList: string[] = ['我的器材库', '改装订单', '收藏搭配', '预约记录', '收货地址', '设置中心']

  private mainList: GoodsItem[] = [
    { name: '丹拿 Esotec 236 两分频套装', price: 3299, desc: '6.5寸中低音 · 4欧阻抗', score: 96, num: 150, tag: '热销', hot: '爆' },
    { name: '摩雷 UFO 顶级三分频套装', price: 8999, desc: '7寸中低音 · 3欧阻抗', score: 97, num: 180, tag: '旗舰', hot: '荐' },
    { name: '劲浪 PS165F 两分频喇叭', price: 2899, desc: '6.5寸玻璃纤维盆 · 4欧', score: 94, num: 120, tag: '口碑', hot: '赞' },
    { name: '阿尔派 PXW-654 四路功放', price: 2599, desc: '4通道 · 480W 额定输出', score: 92, num: 320, tag: '热销', hot: '爆' },
    { name: '先锋 TS-F1640R 同轴喇叭', price: 699, desc: '6.5寸同轴 · 250W 峰值', score: 89, num: 220, tag: '性价比', hot: '火' },
    { name: 'JBL Stadium GTO 620M', price: 1380, desc: '6.5寸两分频 · 92dB 灵敏度', score: 91, num: 260, tag: '口碑', hot: '赞' },
    { name: '丹拿 Esotec 232 前声场', price: 2199, desc: '6.5寸两分频 · 4欧阻抗', score: 95, num: 175, tag: '热销', hot: '爆' },
    { name: '赫兹 Mille Pro MPX 165', price: 3480, desc: '6.5寸中低音 · 3欧阻抗', score: 93, num: 130, tag: '进口', hot: '荐' },
    { name: '捷力 C1-650 两分频套装', price: 1580, desc: '6.5寸 · 90dB 灵敏度', score: 90, num: 240, tag: '性价比', hot: '火' },
    { name: '阿尔派 iLX-W650 主机', price: 3280, desc: '9寸悬浮屏 · 无线CarPlay', score: 94, num: 200, tag: '热销', hot: '爆' },
    { name: '先锋 DMH-Z5350BT 主机', price: 2380, desc: '6.8寸屏 · 蓝牙5.0', score: 91, num: 210, tag: '口碑', hot: '赞' },
    { name: '歌乐 A-50 四路功放', price: 1899, desc: 'D类 · 500W 额定', score: 90, num: 350, tag: '性价比', hot: '火' },
    { name: '来福 P300-12 有源低音炮', price: 3680, desc: '12寸内置功放 · 300W', score: 92, num: 300, tag: '热销', hot: '爆' },
    { name: 'Kicker 44CWCS124 低音炮', price: 2299, desc: '12寸 · 双4欧音圈', score: 93, num: 280, tag: '进口', hot: '荐' },
    { name: 'JL Audio 10TW3 薄型低音', price: 4599, desc: '10寸超薄 · 深度仅68mm', score: 96, num: 160, tag: '旗舰', hot: '荐' },
    { name: '大白鲨隔音四门套餐', price: 899, desc: '止震板+吸音棉 · 四门', score: 88, num: 420, tag: '性价比', hot: '火' },
    { name: '平静隔音底盘套餐', price: 1280, desc: '底盘+后备箱 · 8平米', score: 90, num: 380, tag: '口碑', hot: '赞' },
    { name: '摩雷 Virtus 602 两分频', price: 3980, desc: '6寸中低音 · 3欧阻抗', score: 95, num: 140, tag: '进口', hot: '荐' },
    { name: '劲浪 RSE-165 两分频', price: 1690, desc: '6.5寸 · 91dB 灵敏度', score: 89, num: 230, tag: '性价比', hot: '火' },
    { name: '丹拿 Esotec 130 中音', price: 2890, desc: '5.2寸中音 · 4欧阻抗', score: 94, num: 110, tag: '发烧', hot: '荐' },
    { name: '史泰格 ST400 四路功放', price: 2190, desc: 'AB类 · 400W 额定', score: 91, num: 330, tag: '口碑', hot: '赞' },
    { name: '德高 RS-165 舞台同轴', price: 980, desc: '6.5寸同轴 · 4欧', score: 87, num: 250, tag: '性价比', hot: '火' },
    { name: '卡顿 DSP-8 处理器', price: 1480, desc: '8通道 · 延时校正', score: 92, num: 310, tag: '热销', hot: '爆' },
    { name: '骇客 V12 超薄低音炮', price: 2680, desc: '8寸有源 · 备胎位安装', score: 90, num: 290, tag: '口碑', hot: '赞' }
  ]

  private speakList: GoodsItem[] = [
    { name: '丹拿 236 中低音单元', price: 3299, desc: '6.5寸 · 150W 额定', score: 96, num: 150, tag: '4Ω', hot: '爆' },
    { name: '摩雷 UFO 中低音单元', price: 8999, desc: '7寸 · 180W 额定', score: 97, num: 180, tag: '3Ω', hot: '荐' },
    { name: '劲浪 PS165F 中低音', price: 2899, desc: '6.5寸 · 120W 额定', score: 94, num: 120, tag: '4Ω', hot: '赞' },
    { name: '阿尔派 PXW-654 功放', price: 2599, desc: '4通道 · 480W 额定', score: 92, num: 480, tag: '4Ω', hot: '爆' },
    { name: '先锋 TS-F1640 同轴', price: 699, desc: '6.5寸 · 220W 峰值', score: 89, num: 220, tag: '4Ω', hot: '火' },
    { name: 'JBL GTO 620M 中低音', price: 1380, desc: '6.5寸 · 260W 峰值', score: 91, num: 260, tag: '3Ω', hot: '赞' },
    { name: '赫兹 MPX 165 中低音', price: 3480, desc: '6.5寸 · 130W 额定', score: 93, num: 130, tag: '3Ω', hot: '荐' },
    { name: '歌乐 A-50 功放', price: 1899, desc: 'D类 · 500W 额定', score: 90, num: 500, tag: '2Ω', hot: '火' },
    { name: '史泰格 ST400 功放', price: 2190, desc: 'AB类 · 400W 额定', score: 91, num: 400, tag: '4Ω', hot: '赞' },
    { name: '卡顿 DSP-8 处理器', price: 1480, desc: '8通道 · 310W 输出', score: 92, num: 310, tag: '4Ω', hot: '爆' },
    { name: '捷力 C1-650 中低音', price: 1580, desc: '6.5寸 · 240W 峰值', score: 90, num: 240, tag: '4Ω', hot: '火' },
    { name: '骇客 V12 低音单元', price: 2680, desc: '8寸有源 · 290W 峰值', score: 90, num: 290, tag: '2Ω', hot: '赞' }
  ]

  private subList: GoodsItem[] = [
    { name: 'K牌 8寸薄型低音炮', price: 1699, desc: '深度90mm · 备胎位', score: 90, num: 8, tag: '8寸', hot: '火' },
    { name: '来福 10寸有源低音炮', price: 2699, desc: '内置功放 · 300W', score: 92, num: 10, tag: '10寸', hot: '爆' },
    { name: 'JBL 12寸双音圈低音炮', price: 2299, desc: '双4欧 · 500W峰值', score: 93, num: 12, tag: '12寸', hot: '荐' },
    { name: '捷力 10W3v3-4 低音炮', price: 3180, desc: '10寸 · 500W 峰值', score: 95, num: 10, tag: '10寸', hot: '荐' },
    { name: 'Kicker 12寸 CWCS124', price: 2299, desc: '12寸 · 双4欧音圈', score: 93, num: 12, tag: '12寸', hot: '赞' },
    { name: '摩雷 15寸神炮', price: 5680, desc: '15寸 · 1000W峰值', score: 96, num: 15, tag: '15寸', hot: '荐' },
    { name: '骇客 8寸备胎低音炮', price: 2680, desc: '备胎位 · 290W峰值', score: 90, num: 8, tag: '8寸', hot: '火' },
    { name: '丹拿 18寸至尊低音', price: 8999, desc: '18寸 · 1500W峰值', score: 97, num: 18, tag: '18寸', hot: '爆' },
    { name: '劲浪 12寸 SUB 620', price: 3580, desc: '12寸 · 600W 峰值', score: 94, num: 12, tag: '12寸', hot: '赞' },
    { name: '阿尔派 10寸 SWT-10', price: 1980, desc: '10寸 · 350W 峰值', score: 91, num: 10, tag: '10寸', hot: '火' }
  ]

  private muteList: GoodsItem[] = [
    { name: '大白鲨四门双层隔音', price: 899, desc: '止震板+吸音棉 · 车门', score: 88, num: 28, tag: '入门', hot: '火' },
    { name: '平静底盘装甲隔音', price: 1280, desc: '底盘+后舱 · 8平米', score: 90, num: 32, tag: '进阶', hot: '赞' },
    { name: 'STP航空级隔音套餐', price: 2680, desc: '俄罗斯进口 · 全车', score: 95, num: 38, tag: '发烧', hot: '荐' },
    { name: '大能蓝钻四门隔音', price: 1180, desc: '2.0mm止震 · 车门', score: 91, num: 30, tag: '进阶', hot: '赞' },
    { name: '止震板引擎盖隔音', price: 380, desc: '铝箔止震 · 发动机舱', score: 85, num: 26, tag: '入门', hot: '火' },
    { name: '后备箱空腔隔音', price: 960, desc: '空腔填充 · 后备箱', score: 92, num: 34, tag: '进阶', hot: '赞' },
    { name: '轮弧翼子板隔音', price: 720, desc: '轮弧衬套 · 四轮', score: 89, num: 29, tag: '入门', hot: '火' },
    { name: '车门密封条加固', price: 260, desc: '三层密封 · 四门', score: 84, num: 25, tag: '入门', hot: '火' },
    { name: '顶棚吸音棉施工', price: 880, desc: '白棉填充 · 车顶', score: 88, num: 27, tag: '进阶', hot: '赞' },
    { name: '防火墙隔音升级', price: 1580, desc: '隔音垫 · 防火墙', score: 93, num: 36, tag: '发烧', hot: '荐' }
  ]

  private roomList: GoodsItem[] = [
    { name: '声压老张', price: 0, desc: '丹拿236配阿尔派功放,人声甜到冒泡,蔡琴的口水声都在', score: 96, num: 356, tag: '丹拿+阿尔派', hot: '爆' },
    { name: '低音狂魔', price: 0, desc: 'JL 10TW3塞进备胎位,下潜深不见底,鼓点砸胸口', score: 94, num: 289, tag: 'JL+骇客', hot: '火' },
    { name: '改装匠老李', price: 0, desc: '四门大白鲨隔音做完整车安静一档,胎噪基本听不见了', score: 90, num: 198, tag: '大白鲨', hot: '赞' },
    { name: '夜路听歌的人', price: 0, desc: '先锋主机直推劲浪,无损音源差距真的大,别再听MP3了', score: 92, num: 245, tag: '先锋+劲浪', hot: '赞' },
    { name: '电音控KK', price: 0, desc: 'DSP调完延时,声场仪表台正中,低频弹性拉满', score: 95, num: 412, tag: '卡顿DSP', hot: '爆' },
    { name: '摩雷发烧友', price: 0, desc: 'UFO三分频倒模A柱,结像精准到能数出歌手站位', score: 97, num: 520, tag: '摩雷UFO', hot: '荐' },
    { name: '途观车主小周', price: 0, desc: '原车喇叭对比改装喇叭,同一首歌像换了张唱片', score: 89, num: 176, tag: '捷力套装', hot: '火' },
    { name: '古典乐派赵教授', price: 0, desc: '弦乐群奏层次分明,小提琴泛音一丝不乱,值回票价', score: 93, num: 203, tag: '赫兹套装', hot: '赞' },
    { name: '跑山青年阿凯', price: 0, desc: '功放推力余量要留足,动态一上来才知道什么叫从容', score: 91, num: 234, tag: '史泰格', hot: '赞' },
    { name: '奶爸车主大熊', price: 0, desc: '有源低音炮免布线,后备箱还能塞婴儿车,真香', score: 88, num: 158, tag: '来福有源', hot: '火' }
  ]

  private msgList: GoodsItem[] = [
    { name: '国贸改装店', price: 0, desc: '您预约的隔音施工已排期,请按时到店', score: 0, num: 3, tag: '10:24', hot: '约' },
    { name: '丹拿官方旗舰店', price: 0, desc: '您收藏的 Esotec 236 限时直降300元', score: 0, num: 1, tag: '09:41', hot: '促' },
    { name: '声压老张', price: 0, desc: '老哥,我那套调音参数发你了,收一下', score: 0, num: 5, tag: '昨天', hot: '私' },
    { name: '改装匠老李', price: 0, desc: '四门隔音周三可以开工,材料已备齐', score: 0, num: 2, tag: '昨天', hot: '私' },
    { name: '汽车音响交流群', price: 0, desc: '群内热议:DSP调音到底有没有必要', score: 0, num: 12, tag: '周二', hot: '群' },
    { name: '摩雷发烧友', price: 0, desc: 'UFO倒模的施工图我发群里了,参考下', score: 0, num: 4, tag: '周二', hot: '私' },
    { name: '低音狂魔', price: 0, desc: '备胎位低音炮装好了,周末来听效果', score: 0, num: 1, tag: '周一', hot: '私' },
    { name: '系统通知', price: 0, desc: '您发布的器材已通过审核,成功上架', score: 0, num: 1, tag: '周一', hot: '系' },
    { name: '望京改装店', price: 0, desc: '试音间周末场次还有余位,速来锁定', score: 0, num: 2, tag: '上周', hot: '约' },
    { name: '电音控KK', price: 0, desc: '这个版本的延时参数更准,重新发你', score: 0, num: 6, tag: '上周', hot: '私' },
    { name: '来福官方商城', price: 0, desc: '有源低音炮到货提醒,库存仅剩5台', score: 0, num: 1, tag: '上周', hot: '促' },
    { name: '古典乐派赵教授', price: 0, desc: '那套赫兹的听感笔记写完了,回邮件给你', score: 0, num: 2, tag: '两周前', hot: '私' }
  ]

  private packPrice: number = 1299

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

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

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

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

  selHot(): string {
    if (this.selectedItem !== null) {
      return this.selectedItem.hot
    }
    return ''
  }

  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() {
        Text('汽车音响改装舱')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Column()
          .layoutWeight(1)
        Stack({ alignContent: Alignment.TopEnd }) {
          Text('♡')
            .fontSize(20)
            .fontColor('#FFFFFF')
        }
        .width(32)
        .height(32)
        .margin({ right: 6 })
        Stack({ alignContent: Alignment.TopEnd }) {
          Text('✉')
            .fontSize(20)
            .fontColor('#FFFFFF')
          Text('3')
            .fontSize(9)
            .fontColor('#FFFFFF')
            .backgroundColor('#E53935')
            .borderRadius(8)
            .padding({ left: 4, right: 4, top: 1, bottom: 1 })
        }
        .width(32)
        .height(32)
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12 })

      Row({ space: 8 }) {
        Text('🎵')
          .fontSize(16)
        Text('搜喇叭/功放/低音炮')
          .fontSize(13)
          .fontColor('#8FA3B8')
      }
      .width('100%')
      .height(38)
      .borderRadius(19)
      .backgroundColor('#1B2F45')
      .padding({ left: 14, right: 14 })
      .alignItems(VerticalAlign.Center)
      .margin({ left: 16, right: 16 })

      Row({ space: 6 }) {
        Text('🔥')
          .fontSize(12)
        Text('本季爆款:丹拿改装套餐 · 已交付 682 台')
          .fontSize(12)
          .fontColor('#FFB877')
      }
      .width('100%')
      .alignItems(VerticalAlign.Center)
      .padding({ left: 16, right: 16, bottom: 12 })
    }
    .width('100%')
    .backgroundColor('#0D1B2A')
  }

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

  // ============ Tab1 音响集市 ============
  @Builder
  tab0() {
    Column({ space: 10 }) {
      Row({ space: 8 }) {
        Column({ space: 4 }) {
          Text('386')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0D1B2A')
          Text('在售器材')
            .fontSize(11)
            .fontColor('#999999')
        }
        .layoutWeight(1)
        .height(64)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)

        Column({ space: 4 }) {
          Text('124')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF6D00')
          Text('本月改装')
            .fontSize(11)
            .fontColor('#999999')
        }
        .layoutWeight(1)
        .height(64)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)

        Column({ space: 4 }) {
          Text('98%')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00B8D4')
          Text('好评率')
            .fontSize(11)
            .fontColor('#999999')
        }
        .layoutWeight(1)
        .height(64)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })

      ForEach(this.mainList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Column() {
            Text(item.name.substring(0, 1))
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
          }
          .width(46)
          .height(46)
          .borderRadius(23)
          .backgroundColor('#0D1B2A')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)

          Column({ space: 4 }) {
            Text(item.name)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#222222')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Text(item.desc)
              .fontSize(11)
              .fontColor('#999999')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Row({ space: 4 }) {
              Column()
                .width(item.score + '%')
                .height(4)
                .borderRadius(2)
                .backgroundColor('#FF6D00')
              Column()
                .layoutWeight(1)
                .height(4)
                .borderRadius(2)
                .backgroundColor('#EEEEEE')
            }
            .width('100%')

            Text('口碑 ' + item.score + '%')
              .fontSize(10)
              .fontColor('#FF6D00')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column({ space: 6 }) {
            Text('¥' + item.price)
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E53935')
            Text(item.tag)
              .fontSize(10)
              .fontColor('#00B8D4')
              .border({ width: 1, color: '#00B8D4' })
              .borderRadius(4)
              .padding({ left: 4, right: 4, top: 1, bottom: 1 })
            Row({ space: 3 }) {
              Text('🔥' + item.num)
                .fontSize(10)
                .fontColor('#FF6D00')
              Text(item.hot)
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#FF6D00')
                .borderRadius(6)
                .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            }
            .alignItems(VerticalAlign.Center)
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding(12)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(VerticalAlign.Center)
        .margin({ left: 12, right: 12, bottom: 8 })
        .onClick(() => {
          this.selectedItem = item
          this.showDetailModal = true
        })
      }, (item: GoodsItem) => item.name)

      Row({ space: 8 }) {
        Text('想出闲置器材?')
          .fontSize(13)
          .fontColor('#666666')
        Text('去发布')
          .fontSize(13)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF6D00')
          .borderRadius(14)
          .padding({ left: 14, right: 14, top: 6, bottom: 6 })
          .onClick(() => {
            this.chipA = 0
            this.chipB = 0
            this.inputDesc = ''
            this.showAddModal = true
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .padding({ top: 4, bottom: 20 })
    }
    .width('100%')
    .padding({ top: 10 })
    .backgroundColor('#F5F5F5')
  }

  // ============ Tab2 喇叭主机 ============
  @Builder
  tab1() {
    Column({ space: 10 }) {
      Text('额定功率对比 (W)')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#0D1B2A')
        .margin({ left: 12, top: 10 })

      Scroll() {
        Row({ space: 10 }) {
          ForEach(this.speakList, (item: GoodsItem) => {
            Column({ space: 4 }) {
              Text(item.num + 'W')
                .fontSize(9)
                .fontColor('#FF6D00')
              Column()
                .width(16)
                .height(item.num / this.maxNumOf(this.speakList) * 120)
                .borderRadius(4)
                .backgroundColor(item.num > 300 ? '#FF6D00' : '#00B8D4')
              Text(item.name.substring(0, 2))
                .fontSize(9)
                .fontColor('#666666')
            }
            .alignItems(HorizontalAlign.Center)
          }, (item: GoodsItem) => item.name)
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .height(170)
      .backgroundColor('#FFFFFF')
      .margin({ left: 12, right: 12 })
      .borderRadius(10)

      Text('喇叭 / 主机精选')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#0D1B2A')
        .margin({ left: 12, top: 6 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.speakList, (item: GoodsItem) => {
          Column({ space: 6 }) {
            Row({ space: 6 }) {
              Text(item.tag)
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#00B8D4')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              Text(item.hot)
                .fontSize(10)
                .fontColor('#FF6D00')
            }
            .alignItems(VerticalAlign.Center)

            Text(item.name)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#222222')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })

            Text(item.desc)
              .fontSize(11)
              .fontColor('#999999')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })

            Row({ space: 4 }) {
              Text('口碑 ' + item.score + '%')
                .fontSize(10)
                .fontColor('#FF6D00')
            }

            Text('¥' + item.price)
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E53935')
          }
          .width('48%')
          .padding(12)
          .borderRadius(10)
          .backgroundColor('#FFFFFF')
          .margin({ bottom: 10 })
          .alignItems(HorizontalAlign.Start)
          .onClick(() => {
            this.selectedItem = item
            this.showDetailModal = true
          })
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12, bottom: 20 })
    }
    .width('100%')
    .backgroundColor('#F5F5F5')
  }

  // ============ Tab3 低音炮 ============
  @Builder
  tab2() {
    Column({ space: 10 }) {
      Row({ space: 8 }) {
        Column({ space: 4 }) {
          Text('10')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF6D00')
          Text('在售低音炮')
            .fontSize(11)
            .fontColor('#8FA3B8')
        }
        .layoutWeight(1)
        .height(70)
        .borderRadius(10)
        .backgroundColor('#1B2F45')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)

        Column({ space: 4 }) {
          Text('328')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00B8D4')
          Text('本月成交')
            .fontSize(11)
            .fontColor('#8FA3B8')
        }
        .layoutWeight(1)
        .height(70)
        .borderRadius(10)
        .backgroundColor('#1B2F45')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)

        Column({ space: 4 }) {
          Text('27L')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFB877')
          Text('平均箱体容积')
            .fontSize(11)
            .fontColor('#8FA3B8')
        }
        .layoutWeight(1)
        .height(70)
        .borderRadius(10)
        .backgroundColor('#1B2F45')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 10 })
      .backgroundColor('#0D1B2A')

      Text('箱体尺寸分布 (英寸)')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#0D1B2A')
        .margin({ left: 12, top: 10 })

      Row({ space: 10 }) {
        ForEach(this.subList, (item: GoodsItem) => {
          Column({ space: 4 }) {
            Text(item.num + '"')
              .fontSize(10)
              .fontColor('#FF6D00')
            Column()
              .width(18)
              .height(item.num / this.maxNumOf(this.subList) * 110)
              .borderRadius(4)
              .backgroundColor('#0D1B2A')
            Text(item.name.substring(0, 2))
              .fontSize(9)
              .fontColor('#666666')
          }
          .alignItems(HorizontalAlign.Center)
        }, (item: GoodsItem) => item.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })
      .height(160)
      .backgroundColor('#FFFFFF')
      .margin({ left: 12, right: 12 })
      .borderRadius(10)

      Text('热门低音炮 · 横滑选箱')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#0D1B2A')
        .margin({ left: 12, top: 6 })

      Scroll() {
        Row({ space: 12 }) {
          ForEach(this.subList, (item: GoodsItem) => {
            Column({ space: 8 }) {
              Column({ space: 4 }) {
                Text(item.tag)
                  .fontSize(34)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FFFFFF')
                  .scale({ x: 1.05, y: 1.05 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
                Text('英寸箱体')
                  .fontSize(10)
                  .fontColor('#8FA3B8')
              }
              .width(130)
              .height(80)
              .borderRadius(10)
              .backgroundColor('#1B2F45')
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)

              Text(item.name)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#222222')
                .maxLines(1)

              Text(item.desc)
                .fontSize(10)
                .fontColor('#999999')
                .maxLines(1)

              Text('¥' + item.price)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E53935')
            }
            .width(130)
            .padding(10)
            .borderRadius(10)
            .backgroundColor('#FFFFFF')
            .alignItems(HorizontalAlign.Start)
            .onClick(() => {
              this.selectedItem = item
              this.showDetailModal = true
            })
          }, (item: GoodsItem) => item.name)
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .padding({ bottom: 20 })
    }
    .width('100%')
    .backgroundColor('#F5F5F5')
  }

  // ============ Tab4 隔音工程 ============
  @Builder
  tab3() {
    Column({ space: 10 }) {
      Text('施工降噪量对比 (dB)')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#0D1B2A')
        .margin({ left: 12, top: 10 })

      Scroll() {
        Row({ space: 10 }) {
          ForEach(this.muteList, (item: GoodsItem) => {
            Column({ space: 4 }) {
              Text(item.num + 'dB')
                .fontSize(9)
                .fontColor('#FF6D00')
              Column()
                .width(16)
                .height(item.num / this.maxNumOf(this.muteList) * 110)
                .borderRadius(4)
                .backgroundColor(item.num >= 34 ? '#FF6D00' : '#00B8D4')
              Text(item.name.substring(0, 2))
                .fontSize(9)
                .fontColor('#666666')
            }
            .alignItems(HorizontalAlign.Center)
          }, (item: GoodsItem) => item.name)
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .height(160)
      .backgroundColor('#FFFFFF')
      .margin({ left: 12, right: 12 })
      .borderRadius(10)

      ForEach(this.muteList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Column()
            .width(4)
            .height(52)
            .borderRadius(2)
            .backgroundColor(item.num >= 34 ? '#FF6D00' : '#00B8D4')

          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(item.name)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#222222')
              Text(item.tag)
                .fontSize(9)
                .fontColor('#00B8D4')
                .border({ width: 1, color: '#00B8D4' })
                .borderRadius(4)
                .padding({ left: 4, right: 4, top: 1, bottom: 1 })
            }
            .alignItems(VerticalAlign.Center)

            Text(item.desc)
              .fontSize(11)
              .fontColor('#999999')

            Row({ space: 4 }) {
              Column()
                .width(item.score + '%')
                .height(4)
                .borderRadius(2)
                .backgroundColor('#00B8D4')
              Column()
                .layoutWeight(1)
                .height(4)
                .borderRadius(2)
                .backgroundColor('#EEEEEE')
            }
            .width('100%')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column({ space: 6 }) {
            Text('-' + item.num + 'dB')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF6D00')
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            Text('¥' + item.price)
              .fontSize(12)
              .fontColor('#E53935')
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding(12)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(VerticalAlign.Center)
        .margin({ left: 12, right: 12, bottom: 8 })
        .onClick(() => {
          this.selectedItem = item
          this.showPackModal = true
        })
      }, (item: GoodsItem) => item.name)

      Text('想预约门店施工?')
        .fontSize(13)
        .fontColor('#FFFFFF')
        .backgroundColor('#FF6D00')
        .borderRadius(16)
        .padding({ left: 20, right: 20, top: 8, bottom: 8 })
        .margin({ bottom: 20 })
        .onClick(() => {
          this.chipA = 0
          this.chipB = 0
          this.stepDays = 1
          this.showShopModal = true
        })
    }
    .width('100%')
    .backgroundColor('#F5F5F5')
  }

  // ============ Tab5 试音间 ============
  @Builder
  tab4() {
    Column({ space: 10 }) {
      Scroll() {
        Row({ space: 8 }) {
          ForEach(this.styleChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .fontColor(index === 0 ? '#FFFFFF' : '#0D1B2A')
              .backgroundColor(index === 0 ? '#FF6D00' : '#FFFFFF')
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
          }, (chip: string) => chip)
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .margin({ top: 10 })

      ForEach(this.roomList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Column() {
            Text(item.name.substring(0, 1))
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
          }
          .width(42)
          .height(42)
          .borderRadius(21)
          .backgroundColor('#00B8D4')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)

          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(item.name)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#222222')
              Text(item.tag)
                .fontSize(9)
                .fontColor('#FF6D00')
                .backgroundColor('#FFF3E6')
                .borderRadius(4)
                .padding({ left: 4, right: 4, top: 1, bottom: 1 })
            }
            .alignItems(VerticalAlign.Center)

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

            Text('口碑指数 ' + item.score + '%')
              .fontSize(10)
              .fontColor('#00B8D4')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column({ space: 4 }) {
            Text('♪')
              .fontSize(14)
              .fontColor('#FF6D00')
            Text('' + item.num)
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF6D00')
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            Text('试听')
              .fontSize(10)
              .fontColor('#999999')
          }
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .padding(12)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(VerticalAlign.Top)
        .margin({ left: 12, right: 12, bottom: 8 })
      }, (item: GoodsItem) => item.name)

      Text('进店试听 · 免费预约')
        .fontSize(13)
        .fontColor('#FFFFFF')
        .backgroundColor('#0D1B2A')
        .borderRadius(16)
        .padding({ left: 20, right: 20, top: 8, bottom: 8 })
        .margin({ bottom: 20 })
        .onClick(() => {
          this.chipA = 0
          this.chipB = 0
          this.stepDays = 1
          this.showShopModal = true
        })
    }
    .width('100%')
    .backgroundColor('#F5F5F5')
  }

  // ============ Tab6 消息 ============
  @Builder
  tab5() {
    Column({ space: 0 }) {
      ForEach(this.msgList, (item: GoodsItem) => {
        Row({ space: 10 }) {
          Column() {
            Text(item.hot)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
          }
          .width(44)
          .height(44)
          .borderRadius(22)
          .backgroundColor('#0D1B2A')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)

          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(item.name)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#222222')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Column().layoutWeight(1)
              Text(item.tag)
                .fontSize(10)
                .fontColor('#BBBBBB')
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

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

          Column() {
            Text('' + item.num)
              .fontSize(10)
              .fontColor('#FFFFFF')
              .backgroundColor('#E53935')
              .borderRadius(9)
              .padding({ left: 5, right: 5, top: 1, bottom: 1 })
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          }
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
        }
        .width('100%')
        .padding({ left: 12, right: 12, top: 10, bottom: 10 })
        .backgroundColor('#FFFFFF')
        .alignItems(VerticalAlign.Center)
        .margin({ bottom: 1 })
        .onClick(() => {
          this.currentTab = 4
        })
      }, (item: GoodsItem) => item.name)

      Text('已加载全部消息')
        .fontSize(11)
        .fontColor('#BBBBBB')
        .padding({ top: 20, bottom: 20 })
    }
    .width('100%')
    .backgroundColor('#F5F5F5')
  }

  // ============ Tab7 我的 ============
  @Builder
  tab6() {
    Column({ space: 10 }) {
      Row({ space: 12 }) {
        Column() {
          Text('音')
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
        }
        .width(64)
        .height(64)
        .borderRadius(32)
        .backgroundColor('#FF6D00')
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)

        Column({ space: 6 }) {
          Text('低频猎人')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Row({ space: 6 }) {
            Text('改装段位 · 白银调音师')
              .fontSize(11)
              .fontColor('#FFB877')
              .backgroundColor('#1B2F45')
              .borderRadius(8)
              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            Text('Lv.5')
              .fontSize(11)
              .fontColor('#00B8D4')
          }
          .alignItems(VerticalAlign.Center)
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#0D1B2A')
      .alignItems(VerticalAlign.Center)

      Row({ space: 8 }) {
        Column({ space: 4 }) {
          Text('12')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0D1B2A')
          Text('我的器材')
            .fontSize(11)
            .fontColor('#999999')
        }
        .layoutWeight(1)
        .height(64)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.currentTab = 0
        })

        Column({ space: 4 }) {
          Text('3')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF6D00')
          Text('改装订单')
            .fontSize(11)
            .fontColor('#999999')
        }
        .layoutWeight(1)
        .height(64)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showShopModal = true
        })

        Column({ space: 4 }) {
          Text('26')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#00B8D4')
          Text('收藏搭配')
            .fontSize(11)
            .fontColor('#999999')
        }
        .layoutWeight(1)
        .height(64)
        .borderRadius(10)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showDeleteModal = true
        })
      }
      .width('100%')
      .padding({ left: 12, right: 12 })

      ForEach(this.menuList, (menu: string, index: number) => {
        Row({ space: 10 }) {
          Column() {
            Text('' + (index + 1))
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
          }
          .width(30)
          .height(30)
          .borderRadius(15)
          .backgroundColor(index % 2 === 0 ? '#FF6D00' : '#00B8D4')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)

          Text(menu)
            .fontSize(14)
            .fontColor('#222222')
            .layoutWeight(1)

          Text('›')
            .fontSize(20)
            .fontColor('#CCCCCC')
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 12, bottom: 12 })
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .alignItems(VerticalAlign.Center)
        .margin({ left: 12, right: 12, bottom: 6 })
        .onClick(() => {
          this.currentTab = 0
        })
      }, (menu: string) => menu)

      Column()
        .height(20)
    }
    .width('100%')
    .backgroundColor('#F5F5F5')
  }

  // ============ 弹框1 发布器材 ============
  @Builder
  addModal() {
    Column() {
      Column()
        .layoutWeight(1)
      Column({ space: 14 }) {
        Row() {
          Text('发布音响器材')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0D1B2A')
          Column()
            .layoutWeight(1)
          Text('✕')
            .fontSize(16)
            .fontColor('#999999')
            .onClick(() => {
              this.showAddModal = false
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Text('选择品类')
          .fontSize(13)
          .fontColor('#666666')

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.kindChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .fontColor(this.chipA === index ? '#FFFFFF' : '#0D1B2A')
              .backgroundColor(this.chipA === index ? '#FF6D00' : '#F5F5F5')
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .margin({ right: 8, bottom: 8 })
              .onClick(() => {
                this.chipA = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('选择品牌')
          .fontSize(13)
          .fontColor('#666666')

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.brandChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .fontColor(this.chipB === index ? '#FFFFFF' : '#00B8D4')
              .backgroundColor(this.chipB === index ? '#00B8D4' : '#FFFFFF')
              .border({ width: 1, color: '#00B8D4' })
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .margin({ right: 8, bottom: 8 })
              .onClick(() => {
                this.chipB = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('器材描述')
          .fontSize(13)
          .fontColor('#666666')

        TextInput({ placeholder: '填写器材参数与成色', text: this.inputDesc })
          .fontSize(13)
          .height(44)
          .borderRadius(8)
          .backgroundColor('#F5F5F5')
          .onChange((value: string) => {
            this.inputDesc = value
          })

        Text('立即上架')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF6D00')
          .borderRadius(22)
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .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
  packModal() {
    Column() {
      Column()
        .layoutWeight(1)
      Column({ space: 14 }) {
        Row() {
          Text('改装套餐定制')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0D1B2A')
          Column()
            .layoutWeight(1)
          Text('✕')
            .fontSize(16)
            .fontColor('#999999')
            .onClick(() => {
              this.showPackModal = false
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Column({ space: 4 }) {
          Text(this.selName())
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#222222')
          Text(this.selDesc())
            .fontSize(12)
            .fontColor('#999999')
        }
        .width('100%')
        .padding(12)
        .borderRadius(10)
        .backgroundColor('#F5F5F5')
        .alignItems(HorizontalAlign.Start)

        Text('选择方案')
          .fontSize(13)
          .fontColor('#666666')

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.planChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .fontColor(this.chipA === index ? '#FFFFFF' : '#0D1B2A')
              .backgroundColor(this.chipA === index ? '#0D1B2A' : '#F5F5F5')
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .margin({ right: 8, bottom: 8 })
              .onClick(() => {
                this.chipA = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Row() {
          Text('喇叭数量')
            .fontSize(13)
            .fontColor('#666666')
          Column()
            .layoutWeight(1)
          Text('-')
            .fontSize(16)
            .fontColor('#666666')
            .backgroundColor('#F0F0F0')
            .borderRadius(15)
            .width(30)
            .height(30)
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepNum > 1) {
                this.stepNum = this.stepNum - 1
              }
            })
          Text(' ' + this.stepNum + ' 只 ')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0D1B2A')
            .margin({ left: 10, right: 10 })
          Text('+')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .backgroundColor('#FF6D00')
            .borderRadius(15)
            .width(30)
            .height(30)
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepNum < 8) {
                this.stepNum = this.stepNum + 1
              }
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Row({ space: 6 }) {
          Text('预估报价')
            .fontSize(13)
            .fontColor('#666666')
          Text('¥' + Math.floor(this.packPrice * this.stepNum))
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#E53935')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')

        Text('提交套餐方案')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .backgroundColor('#0D1B2A')
          .borderRadius(22)
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .onClick(() => {
            this.showPackModal = 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.showPackModal = false
    })
  }

  // ============ 弹框3 门店预约 ============
  @Builder
  shopModal() {
    Column() {
      Column()
        .layoutWeight(1)
      Column({ space: 14 }) {
        Row() {
          Text('门店施工预约')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0D1B2A')
          Column()
            .layoutWeight(1)
          Text('✕')
            .fontSize(16)
            .fontColor('#999999')
            .onClick(() => {
              this.showShopModal = false
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Text('选择门店')
          .fontSize(13)
          .fontColor('#666666')

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.shopChips, (chip: string, index: number) => {
            Column({ space: 4 }) {
              Text(chip)
                .fontSize(12)
                .fontColor(this.chipA === index ? '#FFFFFF' : '#0D1B2A')
              Text('距您 ' + (index + 2) + '.5 km')
                .fontSize(9)
                .fontColor(this.chipA === index ? '#FFD9B8' : '#999999')
            }
            .backgroundColor(this.chipA === index ? '#FF6D00' : '#F5F5F5')
            .borderRadius(10)
            .padding({ left: 12, right: 12, top: 8, bottom: 8 })
            .margin({ right: 8, bottom: 8 })
            .alignItems(HorizontalAlign.Center)
            .onClick(() => {
              this.chipA = index
            })
          }, (chip: string) => chip)
        }
        .width('100%')

        Text('选择时段')
          .fontSize(13)
          .fontColor('#666666')

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.timeChips, (chip: string, index: number) => {
            Text(chip)
              .fontSize(12)
              .fontColor(this.chipB === index ? '#FFFFFF' : '#00B8D4')
              .backgroundColor(this.chipB === index ? '#00B8D4' : '#FFFFFF')
              .border({ width: 1, color: '#00B8D4' })
              .borderRadius(14)
              .padding({ left: 18, right: 18, top: 6, bottom: 6 })
              .margin({ right: 8, bottom: 8 })
              .onClick(() => {
                this.chipB = index
              })
          }, (chip: string) => chip)
        }
        .width('100%')

        Row() {
          Text('施工天数')
            .fontSize(13)
            .fontColor('#666666')
          Column()
            .layoutWeight(1)
          Text('-')
            .fontSize(16)
            .fontColor('#666666')
            .backgroundColor('#F0F0F0')
            .borderRadius(15)
            .width(30)
            .height(30)
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepDays > 1) {
                this.stepDays = this.stepDays - 1
              }
            })
          Text(' ' + this.stepDays + ' 天 ')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#0D1B2A')
            .margin({ left: 10, right: 10 })
          Text('+')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .backgroundColor('#00B8D4')
            .borderRadius(15)
            .width(30)
            .height(30)
            .textAlign(TextAlign.Center)
            .onClick(() => {
              if (this.stepDays < 3) {
                this.stepDays = this.stepDays + 1
              }
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Text('确认预约')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF6D00')
          .borderRadius(22)
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .onClick(() => {
            this.showShopModal = 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.showShopModal = false
    })
  }

  // ============ 弹框4 删除确认(居中小卡) ============
  @Builder
  deleteModal() {
    Column() {
      Column({ space: 14 }) {
        Column({ space: 6 }) {
          Text('⚠')
            .fontSize(28)
            .fontColor('#FFFFFF')
          Text('警告操作')
            .fontSize(12)
            .fontColor('#FFD9D9')
        }
        .width('100%')
        .padding({ top: 16, bottom: 16 })
        .backgroundColor('#E53935')
        .borderRadius({ topLeft: 12, topRight: 12 })
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)

        Text('确认删除该收藏器材?')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#222222')

        Text(this.selName())
          .fontSize(12)
          .fontColor('#999999')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .padding({ left: 12, right: 12 })

        Row({ space: 12 }) {
          Text('取消')
            .fontSize(14)
            .fontColor('#666666')
            .backgroundColor('#F5F5F5')
            .borderRadius(18)
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .padding({ top: 10, bottom: 10 })
            .onClick(() => {
              this.showDeleteModal = false
            })

          Text('删除')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .backgroundColor('#E53935')
            .borderRadius(18)
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .padding({ top: 10, bottom: 10 })
            .onClick(() => {
              this.showDeleteModal = false
            })
        }
        .width('100%')
        .padding({ left: 16, right: 16, 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(48)
          Text(this.selHot() + ' 器材')
            .fontSize(12)
            .fontColor('#8FA3B8')
        }
        .width('100%')
        .height(180)
        .backgroundColor('#0D1B2A')
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)

        Text(this.selName())
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#222222')
          .padding({ left: 16, right: 16 })

        Text(this.selDesc())
          .fontSize(12)
          .fontColor('#999999')
          .padding({ left: 16, right: 16 })

        Row({ space: 4 }) {
          Text('★★★★★')
            .fontSize(14)
            .fontColor('#FF6D00')
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('口碑 ' + this.selScore() + '%')
            .fontSize(12)
            .fontColor('#FF6D00')
        }
        .padding({ left: 16, right: 16 })
        .alignItems(VerticalAlign.Center)

        Row({ space: 4 }) {
          Column()
            .width(this.selScore() + '%')
            .height(6)
            .borderRadius(3)
            .backgroundColor('#FF6D00')
          Column()
            .layoutWeight(1)
            .height(6)
            .borderRadius(3)
            .backgroundColor('#EEEEEE')
        }
        .padding({ left: 16, right: 16 })

        Text('¥' + this.selPrice())
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E53935')
          .padding({ left: 16, right: 16 })

        Text('适配车型')
          .fontSize(13)
          .fontColor('#666666')
          .padding({ left: 16, right: 16 })

        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(this.fitChips, (chip: string) => {
            Text(chip)
              .fontSize(11)
              .fontColor('#00B8D4')
              .border({ width: 1, color: '#00B8D4' })
              .borderRadius(4)
              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .margin({ right: 8, bottom: 8 })
          }, (chip: string) => chip)
        }
        .padding({ left: 16, right: 16 })

        Column()
          .layoutWeight(1)

        Text('立即购买')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .backgroundColor('#FF6D00')
          .borderRadius(24)
          .width('90%')
          .textAlign(TextAlign.Center)
          .padding({ top: 14, bottom: 14 })
          .margin({ bottom: 24 })
          .onClick(() => {
            this.showDetailModal = false
          })
      }
      .width('80%')
      .height('100%')
      .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%')
        .scrollBar(BarState.Off)
        this.bottomBuilder()
      }
      .width('100%')
      .height('100%')

      if (this.showAddModal) {
        this.addModal()
      }
      if (this.showPackModal) {
        this.packModal()
      }
      if (this.showShopModal) {
        this.shopModal()
      }
      if (this.showDeleteModal) {
        this.deleteModal()
      }
      if (this.showDetailModal) {
        this.detailModal()
      }
    }
    .width('100%')
    .height('100%')
  }
}


总结

在这里插入图片描述

本文深入解析了一款基于HarmonyOS ArkTS声明式UI框架构建的汽车音响改装舱应用页面的完整实现。该应用以深夜蓝、声浪橙和电音青三色体系构建了专业声学氛围的视觉基调,通过七大Tab页面(音响集市、喇叭主机、低音炮、隔音工程、试音间、消息、我的)和五类弹窗(发布器材、改装套餐、门店预约、删除确认、商品详情)实现了从器材浏览到改装预约的完整业务链路。

在数据可视化方面,应用通过横向滚动的Scroll+ForEach组合实现了三种柱状图——功率对比、箱体尺寸分布和降噪量对比,利用归一化算法item.num / this.maxNumOf(list) * 120确保柱条按比例缩放,并通过阈值条件分支实现双色着色(如功率大于300W为橙色、否则为青色),帮助用户快速识别高参数器材。在交互设计方面,改装套餐弹窗的步进器实现了1-8只喇叭的数量选择和实时报价计算,门店预约弹窗的施工天数步进器实现了1-3天的工期选择,两个步进器通过stepNumstepDays两个独立状态变量分别管理,体现了不同业务场景对步进器语义的差异化需求。

从工程架构角度来看,该应用的设计体现了HarmonyOS ArkTS开发的多项核心实践:单一GoodsItem接口复用降低数据结构复杂度、防御性sel*方法确保空值安全、textOverflow文本溢出省略保持卡片布局一致性、Stack条件渲染实现弹窗层叠管理、stopPropagation精确控制事件冒泡。这些技术实践为汽车后市场服务类应用的声明式UI开发提供了可借鉴的实现范式,尤其适合需要多维度数据展示、多类型弹窗交互和多Tab页面导航的复杂业务场景。

Logo

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

更多推荐