引言

在这里插入图片描述

航模运动作为一项融合了空气动力学、电子技术和精密机械的综合性 hobby,在国内拥有庞大的爱好者群体。从固定翼飞机、穿越机到遥控车、遥控器设备,航模装备品类丰富、技术迭代快速,许多玩家手中积压了大量闲置装备。二手航模交易平台的需求由此应运而生——它不仅能帮助玩家以更合理的价格流转闲置装备,也能让新手以较低门槛进入这一爱好领域。然而,航模装备的专业属性极为复杂:翼展尺寸、通道数量、电机型号、电池循环次数、遥控协议类型等参数对于买家决策至关重要,传统通用二手平台难以精准呈现这些技术参数。本项目"翼上循环"正是针对这一垂直领域打造的专属交易平台,通过声明式UI将航模装备的核心参数以数据可视化的方式直观展示。

在技术架构层面,本应用采用HarmonyOS ArkTS声明式开发范式,以单一@Entry @Component组件RcPlaneLoopApp为根节点构建完整应用。架构采用三层设计模式:数据模型层定义了六个接口类型覆盖航模装备全品类数据需求;视图层通过七个@Builder方法构建七个功能Tab页面和五个弹窗组件;逻辑层封装了翼展最大值计算、车速最大值计算和选中项安全提取等私有方法。整体配色采用飞行橙白风格,以#F4511E飞行橙为主色调、#42A5F5天空蓝为辅助色、#F4FAFF云白为背景色、#FFD600柠檬黄为强调色,营造出与航模飞行主题高度契合的视觉氛围。这种"色彩即主题"的设计理念贯穿于头部、Tab导航、列表卡片和弹窗的每一个细节。

在业务设计层面,应用围绕"浏览装备-查看详情-租飞体验-参加活动"构建完整用户旅程。浏览环节提供航模集市、固定翼专区、遥控车专区、遥控设备专区、飞场活动、消息中心和个人中心共七个功能模块。交易环节涵盖发布航模、航模租飞和飞场活动报名三种弹窗交互。增值服务包括免费调机试飞、电池健康查询和飞场直供等特色功能。特别值得关注的是,应用在遥控车Tab中采用了双列网格布局(flexWrap(FlexWrap.Wrap)),这与其它Tab的单列列表布局形成差异化对比;在飞场活动Tab中通过报名进度条和"即将满员"脉冲动画创造了紧迫感。这些针对不同品类特点定制的交互设计,体现了垂直电商应用对用户体验的深度思考。

接口定义与数据模型构建

在这里插入图片描述

本应用首先定义了六个接口类型,分别对应航模装备的六大品类,构建起完整且类型安全的数据模型层。每个接口精准刻画了对应品类的关键参数,为后续的列表渲染和柱状图计算提供数据基础。

interface PlaneItem {
  name: string
  brand: string
  price: number
  originPrice: number
  flights: number
  city: string
  tag: string
}

interface FixedWingItem {
  name: string
  wingspan: number
  channel: number
  price: number
  motor: string
}

interface CarItem {
  name: string
  scale: string
  speed: number
  price: number
  runs: number
}

interface RadioItem {
  name: string
  protocol: string
  channels: number
  price: number
  battery: number
}

interface EventItem {
  name: string
  field: string
  joined: number
  total: number
  date: string
}

interface NoteItem {
  name: string
  content: string
  time: string
  unread: number
  type: string
}

PlaneItem接口是航模集市的核心数据结构,包含名称、品牌、售价、原价、飞行次数、城市和标签七个字段。其中flights飞行次数是航模装备独有磨损指标,类似于潜水装备中的"下水次数",直接反映装备的使用频率和机况完好度。FixedWingItem接口针对固定翼飞机增加了wingspan翼展(毫米)、channel通道数和motor电机型号三个专业参数,翼展是固定翼飞机最重要的尺寸指标。CarItem接口关注遥控车的scale比例(如1/10、1/8)、speed极速和runs下场次数。RadioItem接口记录遥控器的protocol协议(如FASST、ELRS、多协议)、channels通道数和battery电池电量。EventItemNoteItem分别用于飞场活动报名和消息通知。这套接口体系完整覆盖了航模交易全链路的数据需求。

组件状态声明与静态数据集

在这里插入图片描述

RcPlaneLoopApp主组件通过@State装饰器声明了十个响应式状态变量,构成应用全部交互逻辑的状态中枢。同时定义了七个静态数据数组为各Tab页面提供数据源。

@Entry
@Component
struct RcPlaneLoopApp {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showRent: boolean = false
  @State showEvent: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: PlaneItem | null = null
  @State rentDays: number = 2
  @State eventField: number = 0
  @State itemKind: number = 0
  @State priceField: string = ''

  private tabs: string[] = ['航模集市', '固定翼', '遥控车', '遥控设备', '飞场活动', '消息', '我的']

  private planeList: PlaneItem[] = [
    { name: 'DJI Mini 4 Pro 全套', brand: 'DJI', price: 4280, originPrice: 6788, flights: 18, city: '深圳', tag: '热门' },
    { name: 'FMS 1400mm P-51 野马', brand: 'FMS', price: 980, originPrice: 1980, flights: 25, city: '北京', tag: '包邮' },
    { name: 'Freewing 90mm 米格15', brand: 'Freewing', price: 1680, originPrice: 3280, flights: 12, city: '上海', tag: '精品' },
    { name: 'E-flite Timber 1.5m', brand: 'E-flite', price: 1280, originPrice: 2580, flights: 30, city: '成都', tag: '95新' },
    { name: '天地飞 9通道遥控器', brand: '天地飞', price: 320, originPrice: 680, flights: 0, city: '杭州', tag: '全新' },
    { name: '乐迪 AT9S Pro 遥控', brand: 'Radiolink', price: 450, originPrice: 899, flights: 0, city: '广州', tag: '包邮' },
    { name: 'AXi 2820 无刷电机', brand: 'AXi', price: 220, originPrice: 480, flights: 40, city: '南京', tag: '急出' },
    { name: '好盈 60A 电调', brand: 'Hobbywing', price: 160, originPrice: 350, flights: 35, city: '武汉', tag: '自提' },
    { name: '华科尔 D10 遥控器', brand: 'Deviation', price: 580, originPrice: 1180, flights: 0, city: '苏州', tag: '95新' },
    { name: '京商 1/10 电越电动', brand: 'Kyosho', price: 890, originPrice: 1780, flights: 50, city: '天津', tag: '急出' },
    { name: 'HPI RS4 漂移车', brand: 'HPI', price: 720, originPrice: 1480, flights: 60, city: '重庆', tag: '包邮' },
    { name: 'Tamiya TT-02 底盘', brand: 'Tamiya', price: 380, originPrice: 780, flights: 20, city: '西安', tag: '热门' },
    { name: 'Rotor Logic 3D 特技机', brand: 'RL', price: 560, originPrice: 1180, flights: 15, city: '长沙', tag: '精品' },
    { name: '凤凰 480 穿越机套装', brand: 'iFlight', price: 680, originPrice: 1380, flights: 22, city: '青岛', tag: '95新' },
    { name: '格氏 4S 1500mAh 电池×4', brand: '格氏', price: 240, originPrice: 520, flights: 45, city: '大连', tag: '急出' },
    { name: 'Futaba T16IZ 遥控器', brand: 'Futaba', price: 2680, originPrice: 4980, flights: 0, city: '厦门', tag: '收藏' },
    { name: 'Jumper T18 遥控器', brand: 'Jumper', price: 780, originPrice: 1580, flights: 0, city: '宁波', tag: '包邮' },
    { name: 'Skywing 50CC Extra', brand: 'Skywing', price: 3880, originPrice: 7600, flights: 8, city: '无锡', tag: '精品' },
    { name: '双天 2205 电机×4', brand: 'T-Motor', price: 280, originPrice: 560, flights: 30, city: '济南', tag: '自提' },
    { name: '银燕 9g 舵机 ×10', brand: '银燕', price: 120, originPrice: 260, flights: 0, city: '合肥', tag: '全新' },
    { name: '链博士充电器 300W', brand: 'ToolkitRC', price: 320, originPrice: 660, flights: 0, city: '郑州', tag: '包邮' },
    { name: 'DJI FPV 眼镜 Goggles2', brand: 'DJI', price: 1580, originPrice: 2980, flights: 0, city: '佛山', tag: '95新' },
    { name: 'BetaFPV 口袋遥控', brand: 'BetaFPV', price: 260, originPrice: 540, flights: 0, city: '东莞', tag: '热门' },
    { name: '收云 3D 纸飞机 KT板', brand: '收云', price: 65, originPrice: 148, flights: 12, city: '福州', tag: '急出' }
  ]

currentTab控制当前激活的Tab页面索引,五个show布尔变量分别控制五个弹窗的显示与隐藏。selectedItem使用PlaneItem | null联合类型,在用户点击列表项时被赋值。rentDays默认值为2(航模租飞通常按天计费),eventField用于飞场活动报名中的场地选择,itemKindpriceField服务于发布弹窗。planeList数组包含24条装备数据,覆盖了DJI航拍机、FMS固定翼、穿越机、遥控车、遥控器、电机电调、电池配件等全品类,每条数据均包含品牌、价格、飞行次数和城市信息。这些静态数据虽以硬编码方式定义,但在实际应用中可轻松替换为网络请求获取的动态数据。

私有方法:最大值计算与安全提取

在这里插入图片描述

组件封装了两个最大值计算方法和六个安全提取方法,体现了ArkTS中防御性编程与数据归一化的重要实践。

  private maxWing(): number {
    let max: number = 0
    for (let i = 0; i < this.wingList.length; i++) {
      if (this.wingList[i].wingspan > max) {
        max = this.wingList[i].wingspan
      }
    }
    return max
  }

  private maxSpeed(): number {
    let max: number = 0
    for (let i = 0; i < this.carList.length; i++) {
      if (this.carList[i].speed > max) {
        max = this.carList[i].speed
      }
    }
    return max
  }

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

  private selPrice(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.price + ''
  }

  private selBrand(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.brand
  }

  private selFlights(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.flights
  }

  private selCity(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.city
  }

  private selOrigin(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.originPrice + ''
  }

maxWing()遍历固定翼列表计算最大翼展值,maxSpeed()遍历遥控车列表计算最大车速值。这两个方法的返回值分别用于翼展柱状图和车速柱状图的高度归一化计算。动态计算最大值而非硬编码上限的好处在于:当数据集变化时(如新增或删除装备),柱状图会自动重新归一化,始终保持最优的视觉比例。六个sel前缀方法实现了空值安全提取模式,每个方法首先检查selectedItem是否为null,若为空则返回空字符串或零值。这种防御性策略确保了详情弹窗在selectedItem未赋值时不会因空指针而崩溃。selPrice()selOrigin()方法中使用+ ''将数字转为字符串,这是ArkTS中简洁的类型转换写法。

主构建函数:三段式布局骨架

在这里插入图片描述

build()方法构建了应用的三段式页面骨架:顶部头部、中间可滚动内容区和底部Tab导航,并在末尾条件渲染五个弹窗。

  build() {
    Column() {
      // 头部:静态航模电商风
      Column() {
        Row() {
          Column() {
            Text('翼上循环')
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('二手航模装备 起飞流转')
              .fontSize(11)
              .fontColor('#FFE0CC')
              .margin({ top: 2 })
          }
          .alignItems(HorizontalAlign.Start)

          Row() {
            Text('🛩️')
              .fontSize(15)
            Text('搜航模 / 品牌')
              .fontSize(12)
              .fontColor('#B5A08F')
              .margin({ left: 6 })
          }
          .width(145)
          .height(32)
          .justifyContent(FlexAlign.Center)
          .backgroundColor('#F4FAFF')
          .borderRadius(16)
          .margin({ left: 12 })

          Text('发布')
            .fontSize(13)
            .fontWeight(FontWeight.Medium)
            .fontColor('#3E2723')
            .width(52)
            .height(30)
            .textAlign(TextAlign.Center)
            .backgroundColor('#FFD600')
            .borderRadius(15)
            .margin({ left: 10 })
            .onClick(() => {
              this.showPublish = true
            })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 12, bottom: 10 })

        Row() {
          Text('🛠️ 免费调机')
            .fontSize(11)
            .fontColor('#FFE0CC')
            .margin({ right: 12 })
          Text('🔋 电池健康可查')
            .fontSize(11)
            .fontColor('#FFE0CC')
            .margin({ right: 12 })
          Text('🪂 飞场直供')
            .fontSize(11)
            .fontColor('#FFE0CC')
        }
        .width('100%')
        .padding({ left: 16, bottom: 12 })
      }
      .width('100%')
      .backgroundColor('#F4511E')
      .borderRadius({ bottomLeft: 18, bottomRight: 18 })

      Scroll() {
        Column() {
          if (this.currentTab === 0) {
            this.MarketTab()
          } else if (this.currentTab === 1) {
            this.WingTab()
          } else if (this.currentTab === 2) {
            this.CarTab()
          } else if (this.currentTab === 3) {
            this.RadioTab()
          } else if (this.currentTab === 4) {
            this.EventTab()
          } else if (this.currentTab === 5) {
            this.NoteTab()
          } else {
            this.MineTab()
          }
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)
      .width('100%')

      Row() {
        ForEach(this.tabs, (tab: string, index: number) => {
          Column() {
            Text(this.tabIcon(index))
              .fontSize(18)
              .fontColor(this.currentTab === index ? '#F4511E' : '#9E9E9E')
            Text(tab)
              .fontSize(10)
              .fontColor(this.currentTab === index ? '#F4511E' : '#9E9E9E')
              .margin({ top: 2 })
            if (this.currentTab === index) {
              Column()
                .width(20)
                .height(3)
                .backgroundColor('#42A5F5')
                .borderRadius(2)
                .margin({ top: 3 })
            } else {
              Column()
                .width(20)
                .height(3)
                .backgroundColor('#00000000')
                .margin({ top: 3 })
            }
          }
          .justifyContent(FlexAlign.Center)
          .layoutWeight(1)
          .onClick(() => {
            this.currentTab = index
          })
        }, (tab: string) => tab)
      }
      .width('100%')
      .height(58)
      .backgroundColor('#FFFFFF')
      .border({ width: 1, color: '#FFE0CC', radius: 0 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F4FAFF')

    if (this.showPublish) {
      this.PublishDialog()
    }
    if (this.showRent) {
      this.RentDialog()
    }
    if (this.showEvent) {
      this.EventDialog()
    }
    if (this.showDelete) {
      this.DeleteDialog()
    }
    if (this.showDetail) {
      this.DetailDialog()
    }
  }

头部区域以飞行橙#F4511E为背景色,底部圆角borderRadius({ bottomLeft: 18, bottomRight: 18 })营造出从屏幕顶部"起飞"的视觉隐喻。头部第一行包含应用名称"翼上循环"、副标题、搜索框和发布按钮。发布按钮使用柠檬黄#FFD600背景和深棕#3E2723文字色,与飞行橙形成强烈的色彩对比,引导用户执行发布操作。头部第二行展示三个服务标签:免费调机、电池健康可查、飞场直供。中间内容区使用Scroll容器配合layoutWeight(1)占据剩余空间。底部Tab导航通过ForEach遍历tabs数组渲染七个导航项,选中Tab以飞行橙高亮并附带天空蓝指示条。tabIcon()方法返回对应emoji图标:购物袋、飞机、赛车、手柄、降落伞、对话和人物。五个弹窗在build()末尾通过if条件渲染,确保覆盖在主内容之上。

Tab1 航模集市:横幅广告与行式列表

在这里插入图片描述

航模集市作为首页Tab,包含渐变横幅广告和装备列表。列表通过ForEach遍历planeList数组渲染卡片,每张卡片展示装备图标、名称、品牌、机况进度条和价格信息。

  // Tab1 航模集市:行式列表
  @Builder
  MarketTab() {
    Column() {
      Row() {
        Column() {
          Text('🛩️ 飞手集结季')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('旧机折抵新机 立减 500')
            .fontSize(11)
            .fontColor('#FFE0CC')
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text('🚀')
          .fontSize(36)
          .scale({ x: 1.12, y: 1.12 })
          .animation({ duration: 1000, iterations: -1, curve: Curve.EaseInOut })
      }
      .width('100%')
      .padding(16)
      .linearGradient({
        direction: GradientDirection.Right,
        colors: [['#F4511E', 0], ['#42A5F5', 1]]
      })
      .borderRadius(14)
      .margin({ top: 12, left: 12, right: 12 })

      Row() {
        Text('装备精选')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')
        Column()
          .layoutWeight(1)
        Text('飞行次数 ▾')
          .fontSize(12)
          .fontColor('#F4511E')
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14 })

      ForEach(this.planeList, (item: PlaneItem) => {
        Row() {
          Column() {
            Text('✈️')
              .fontSize(26)
          }
          .width(66)
          .height(66)
          .justifyContent(FlexAlign.Center)
          .backgroundColor('#FFEBE0')
          .borderRadius(12)

          Column() {
            Text(item.name)
              .fontSize(13)
              .fontWeight(FontWeight.Medium)
              .fontColor('#BF360C')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Row() {
              Text(item.brand)
                .fontSize(9)
                .fontColor('#F4511E')
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .backgroundColor('#FFCCBC')
                .borderRadius(4)
              Text(item.flights + ' 飞')
                .fontSize(9)
                .fontColor('#B5A08F')
                .margin({ left: 6 })
              Text('· ' + item.city)
                .fontSize(9)
                .fontColor('#B5A08F')
            }
            .margin({ top: 4 })
            Row() {
              Text('机况')
                .fontSize(9)
                .fontColor('#B5A08F')
              Column()
                .width((100 - item.flights) / 100 * 55)
                .height(3)
                .backgroundColor(item.flights < 20 ? '#66BB6A' : '#FFB74D')
                .borderRadius(2)
                .margin({ left: 4 })
              Text((100 - item.flights) + '%')
                .fontSize(9)
                .fontColor('#B5A08F')
                .margin({ left: 4 })
            }
            .margin({ top: 5 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })

          Column() {
            Text('¥' + item.price)
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#F4511E')
            Text('¥' + item.originPrice)
              .fontSize(9)
              .fontColor('#CDB2A8')
              .decoration({ type: TextDecorationType.LineThrough })
              .margin({ top: 2 })
            Text(item.tag)
              .fontSize(9)
              .fontColor('#3E2723')
              .padding({ left: 5, right: 5, top: 1, bottom: 1 })
              .backgroundColor('#FFD600')
              .borderRadius(3)
              .margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding(11)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .margin({ left: 12, right: 12, top: 5 })
        .onClick(() => {
          this.selectedItem = item
          this.showDetail = true
        })
      }, (item: PlaneItem) => item.name)

      Column()
        .height(20)
    }
    .width('100%')
  }

横幅区域使用linearGradient创建从飞行橙#F4511E到天空蓝#42A5F5的横向渐变,火箭emoji通过scaleanimation实现持续缩放呼吸动画。列表卡片采用三栏布局:左侧66x66图标区使用浅橙#FFEBE0背景;中间区域通过layoutWeight(1)自适应,展示名称、品牌标签、飞行次数和机况进度条;右侧显示价格、原价和标签。机况进度条宽度计算为(100 - item.flights) / 100 * 55,即以100次飞行为满磨损基准,剩余完好度映射为像素宽度。进度条颜色以20次飞行为分界:低于20次显示绿色#66BB6A表示机况良好,高于20次显示橙色#FFB74D提示需关注。标签使用柠檬黄背景和深棕文字色,与飞行橙主题协调统一。每张卡片的onClick将当前item赋值给selectedItem并打开详情弹窗。

Tab2 固定翼专区:翼展柱状图

在这里插入图片描述

固定翼Tab的核心特色是翼展柱状图,通过将每架飞机的翼展值映射为柱子高度,实现翼展尺寸的直观对比。

  // Tab2 固定翼:翼展柱状图 + 列表
  @Builder
  WingTab() {
    Column() {
      Row() {
        Text('✈️ 固定翼专区')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')
        Column()
          .layoutWeight(1)
        Text('共 ' + this.wingList.length + ' 架')
          .fontSize(11)
          .fontColor('#B5A08F')
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14 })

      Text('翼展分布(mm)')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#BF360C')
        .margin({ top: 12, left: 16 })

      Row() {
        ForEach(this.wingList, (item: FixedWingItem) => {
          Column() {
            Column()
              .width(22)
              .height(item.wingspan / this.maxWing() * 115)
              .backgroundColor(item.wingspan > 1500 ? '#F4511E' : (item.wingspan > 1000 ? '#42A5F5' : '#FFB74D'))
              .borderRadius(5)
            Text(item.wingspan + '')
              .fontSize(8)
              .fontColor('#B5A08F')
              .margin({ top: 3 })
          }
          .justifyContent(FlexAlign.End)
          .alignItems(HorizontalAlign.Center)
          .layoutWeight(1)
        }, (item: FixedWingItem) => item.name)
      }
      .width('100%')
      .height(155)
      .padding({ left: 10, right: 10 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 8, left: 12, right: 12 })
      .justifyContent(FlexAlign.End)

      ForEach(this.wingList, (item: FixedWingItem) => {
        Row() {
          Column() {
            Text('✈️')
              .fontSize(26)
          }
          .width(54)
          .height(54)
          .justifyContent(FlexAlign.Center)
          .backgroundColor('#E3F2FD')
          .borderRadius(10)

          Column() {
            Text(item.name)
              .fontSize(13)
              .fontWeight(FontWeight.Medium)
              .fontColor('#BF360C')
              .maxLines(1)
            Row() {
              Text(item.wingspan + 'mm')
                .fontSize(9)
                .fontColor('#FFFFFF')
                .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                .backgroundColor('#42A5F5')
                .borderRadius(3)
              Text(item.channel + ' 通道')
                .fontSize(10)
                .fontColor('#B5A08F')
                .margin({ left: 6 })
            }
            .margin({ top: 4 })
            Text(item.motor)
              .fontSize(10)
              .fontColor('#B5A08F')
              .margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })

          Text('¥' + item.price)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#F4511E')
        }
        .width('100%')
        .padding(11)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .margin({ left: 12, right: 12, top: 6 })
      }, (item: FixedWingItem) => item.name)

      Column()
        .height(20)
    }
    .width('100%')
  }

翼展柱状图整体高度155,每根柱子宽度22,高度通过item.wingspan / this.maxWing() * 115计算——以数据集中最大翼展值为满刻度,115为最大像素高度。这种使用maxWing()方法动态获取最大值的方式确保了柱状图始终能填满预设高度区域。柱子颜色采用三段分级:翼展超过1500mm的大比例机使用飞行橙#F4511E,超过1000mm的中比例机使用天空蓝#42A5F5,低于1000mm的小比例机使用浅橙#FFB74D。柱状图容器设置justifyContent(FlexAlign.End)使柱子从底部生长。列表卡片展示飞机名称、翼展标签、通道数和电机型号,翼展标签使用天空蓝背景突出显示。

Tab3 遥控车专区:双列网格布局

遥控车Tab是应用中唯一采用双列网格布局的页面,通过flexWrap(FlexWrap.Wrap)实现自动换行,每张卡片宽度为47%。

  // Tab3 遥控车:车速柱状图 + 双列网格
  @Builder
  CarTab() {
    Column() {
      Text('极速分布(km/h)')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#BF360C')
        .margin({ top: 14, left: 16 })

      Row() {
        ForEach(this.carList, (item: CarItem) => {
          Column() {
            Column()
              .width(22)
              .height(item.speed / this.maxSpeed() * 115)
              .backgroundColor(item.speed > 55 ? '#F4511E' : (item.speed > 35 ? '#42A5F5' : '#FFB74D'))
              .borderRadius(5)
            Text(item.speed + '')
              .fontSize(8)
              .fontColor('#B5A08F')
              .margin({ top: 3 })
          }
          .justifyContent(FlexAlign.End)
          .alignItems(HorizontalAlign.Center)
          .layoutWeight(1)
        }, (item: CarItem) => item.name)
      }
      .width('100%')
      .height(155)
      .padding({ left: 10, right: 10 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 8, left: 12, right: 12 })
      .justifyContent(FlexAlign.End)

      Row() {
        ForEach(this.carList, (item: CarItem) => {
          Column() {
            Column() {
              Text('🏎️')
                .fontSize(34)
                .scale({ x: 1.08, y: 1.08 })
                .animation({ duration: 1100, iterations: -1, curve: Curve.EaseInOut })
            }
            .width('100%')
            .height(72)
            .justifyContent(FlexAlign.Center)
            .backgroundColor('#FFEBE0')
            .borderRadius(10)

            Text(item.name)
              .fontSize(12)
              .fontWeight(FontWeight.Medium)
              .fontColor('#BF360C')
              .margin({ top: 6 })
              .maxLines(1)
            Row() {
              Text(item.scale)
                .fontSize(9)
                .fontColor('#FFFFFF')
                .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                .backgroundColor('#42A5F5')
                .borderRadius(3)
              Text(item.speed + ' km/h')
                .fontSize(9)
                .fontColor('#B5A08F')
                .margin({ left: 5 })
            }
            .margin({ top: 4 })
            Text(item.runs + ' 次下场')
              .fontSize(9)
              .fontColor('#B5A08F')
              .margin({ top: 3 })
            Text('¥' + item.price)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#F4511E')
              .margin({ top: 4 })
          }
          .width('47%')
          .padding(10)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ top: 8 })
          .alignItems(HorizontalAlign.Start)
        }, (item: CarItem) => item.name)
      }
      .width('94%')
      .margin({ left: 12 })
      .flexWrap(FlexWrap.Wrap)

      Column()
        .height(20)
    }
    .width('100%')
  }

遥控车Tab首先展示极速柱状图,高度通过item.speed / this.maxSpeed() * 115计算,以数据集中最大车速为满刻度。柱子颜色三段分级:超过55km/h的极速车使用飞行橙,超过35km/h的中速车使用天空蓝,低于35km/h的低速车(如攀爬车)使用浅橙。柱状图下方是双列网格卡片区域,外层Row设置flexWrap(FlexWrap.Wrap)和宽度94%,每张卡片宽度47%,间距通过margin自然形成。卡片顶部是72高度的图标区域,赛车emoji附带scaleanimation实现持续缩放动画。卡片内容包含车名、比例标签、极速、下场次数和价格。双列网格布局相比单列列表能够在一屏内展示更多装备,适合遥控车这类信息量适中的品类。

Tab4 遥控设备:通道数柱状图与电量进度

遥控设备Tab展示通道数柱状图和遥控器列表,列表中特别增加了电池电量进度条。

  // Tab4 遥控器设备:横向滚动卡 + 电量进度
  @Builder
  RadioTab() {
    Column() {
      Row() {
        Text('🎮 遥控器设备专区')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')
        Column()
          .layoutWeight(1)
        Text('共 ' + this.radioList.length + ' 台')
          .fontSize(11)
          .fontColor('#B5A08F')
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14 })

      Text('通道数量分布(ch)')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#BF360C')
        .margin({ top: 12, left: 16 })

      Row() {
        ForEach(this.radioList, (item: RadioItem) => {
          Column() {
            Column()
              .width(24)
              .height(item.channels / 18 * 115)
              .backgroundColor(item.channels >= 16 ? '#F4511E' : (item.channels >= 9 ? '#42A5F5' : '#FFD600'))
              .borderRadius(5)
            Text(item.channels + 'ch')
              .fontSize(8)
              .fontColor('#B5A08F')
              .margin({ top: 3 })
          }
          .justifyContent(FlexAlign.End)
          .alignItems(HorizontalAlign.Center)
          .layoutWeight(1)
        }, (item: RadioItem) => item.name)
      }
      .width('100%')
      .height(155)
      .padding({ left: 10, right: 10 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 8, left: 12, right: 12 })
      .justifyContent(FlexAlign.End)

      ForEach(this.radioList, (item: RadioItem) => {
        Row() {
          Column() {
            Text('🎮')
              .fontSize(26)
          }
          .width(54)
          .height(54)
          .justifyContent(FlexAlign.Center)
          .backgroundColor('#E3F2FD')
          .borderRadius(10)

          Column() {
            Text(item.name)
              .fontSize(13)
              .fontWeight(FontWeight.Medium)
              .fontColor('#BF360C')
              .maxLines(1)
            Row() {
              Text(item.protocol)
                .fontSize(9)
                .fontColor('#FFFFFF')
                .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                .backgroundColor('#F4511E')
                .borderRadius(3)
              Text(item.channels + ' 通道')
                .fontSize(10)
                .fontColor('#B5A08F')
                .margin({ left: 6 })
            }
            .margin({ top: 4 })
            Row() {
              Text('电池')
                .fontSize(9)
                .fontColor('#B5A08F')
              Column()
                .width(item.battery / 100 * 55)
                .height(3)
                .backgroundColor(item.battery > 80 ? '#66BB6A' : '#FFB74D')
                .borderRadius(2)
                .margin({ left: 4 })
              Text(item.battery + '%')
                .fontSize(9)
                .fontColor('#B5A08F')
                .margin({ left: 4 })
            }
            .margin({ top: 5 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })

          Text('¥' + item.price)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#F4511E')
        }
        .width('100%')
        .padding(11)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .margin({ left: 12, right: 12, top: 6 })
      }, (item: RadioItem) => item.name)

      Column()
        .height(20)
    }
    .width('100%')
  }

通道数柱状图以18通道为满刻度(item.channels / 18 * 115),柱子宽度24略宽于其它Tab的22,因为通道数标签"16ch"等较长。柱子颜色三段分级:16通道及以上的高端遥控器使用飞行橙,9通道及以上的中端使用天空蓝,低于9通道的入门款使用柠檬黄#FFD600。列表卡片中的协议标签使用飞行橙背景,电池电量进度条宽度为item.battery / 100 * 55,电量高于80%显示绿色,低于80%显示橙色。协议类型标签直接展示了FASST、ELRS、多协议、CC2500、AFHDS、DSM2等专业协议名称,对于了解遥控器的玩家来说是关键的选购参数。

Tab5 飞场活动:报名进度与脉冲动画

飞场活动Tab展示活动列表,每条活动卡片包含报名进度条和动态状态标签。

  // Tab5 飞场活动:报名进度
  @Builder
  EventTab() {
    Column() {
      Row() {
        Text('🪂 飞场活动')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')
        Column()
          .layoutWeight(1)
        Text('发布活动')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .padding({ left: 12, right: 12, top: 4, bottom: 4 })
          .backgroundColor('#F4511E')
          .borderRadius(12)
          .onClick(() => {
            this.showEvent = true
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14 })

      ForEach(this.eventList, (item: EventItem) => {
        Column() {
          Row() {
            Column() {
              Text('🪂')
                .fontSize(28)
            }
            .width(58)
            .height(58)
            .justifyContent(FlexAlign.Center)
            .backgroundColor('#FFEBE0')
            .borderRadius(10)

            Column() {
              Text(item.name)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#BF360C')
                .maxLines(1)
              Row() {
                Text(item.date)
                  .fontSize(9)
                  .fontColor('#FFFFFF')
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                  .backgroundColor('#42A5F5')
                  .borderRadius(3)
                Text('📍 ' + item.field)
                  .fontSize(10)
                  .fontColor('#B5A08F')
                  .margin({ left: 6 })
              }
              .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            .margin({ left: 10 })

            Text('报名')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .backgroundColor('#FFD600')
              .borderRadius(14)
              .onClick(() => {
                this.showEvent = true
              })
          }
          .width('100%')

          Row() {
            Text('已报 ' + item.joined + '/' + item.total)
              .fontSize(10)
              .fontColor('#B5A08F')
            Stack({ alignContent: Alignment.Start }) {
              Column()
                .width(130)
                .height(5)
                .backgroundColor('#FFEBE0')
                .borderRadius(3)
              Column()
                .width(item.joined / item.total * 130)
                .height(5)
                .backgroundColor('#F4511E')
                .borderRadius(3)
            }
            .width(130)
            .height(5)
            .margin({ left: 8 })
            Text(item.joined / item.total > 0.8 ? '即将满员' : '报名中')
              .fontSize(11)
              .fontWeight(FontWeight.Bold)
              .fontColor('#F4511E')
              .margin({ left: 8 })
              .scale({ x: 1.04, y: 1.04 })
              .animation({ duration: 900, iterations: -1, curve: Curve.EaseInOut })
            Column()
              .layoutWeight(1)
          }
          .width('100%')
          .margin({ top: 10 })
        }
        .width('100%')
        .padding(11)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .margin({ left: 12, right: 12, top: 6 })
      }, (item: EventItem) => item.name)

      Column()
        .height(20)
    }
    .width('100%')
  }

活动卡片采用上下两段布局:上段展示活动图标、名称、日期标签、飞场名称和报名按钮,下段展示报名进度条和动态状态文字。报名进度条使用Stack容器叠放背景轨道(浅橙#FFEBE0)和填充条(飞行橙#F4511E),填充宽度为item.joined / item.total * 130。状态文字通过三元运算符动态判断:当报名率超过80%时显示"即将满员",否则显示"报名中"。状态文字附带scaleanimation属性实现持续脉冲缩放动画,营造出报名即将截止的紧迫感。报名按钮使用柠檬黄背景和白色文字,位于卡片右上角,点击后打开飞场活动报名弹窗。

应用交互流程图

航模集市

固定翼

遥控车

遥控设备

飞场活动

消息

我的

点击列表项

点击发布按钮

点击租飞入口

点击活动报名

点击消息项

点击租3天

点击聊卖家

点击遮罩/关闭

选择类别+输入价格

点击遮罩/关闭

选择天数

点击预约租飞

点击取消/遮罩

选择场地

点击立即报名

点击再想想

应用启动

渲染头部+Tab导航+内容区

用户选择Tab

MarketTab: 渐变横幅+装备列表+机况进度

WingTab: 翼展柱状图+飞机列表

CarTab: 车速柱状图+双列网格卡片

RadioTab: 通道柱状图+电量进度条

EventTab: 报名进度条+脉冲动画

NoteTab: 消息列表+未读徽标

MineTab: 用户卡片+统计+服务入口

用户操作

打开详情弹窗 DetailDialog

打开发布弹窗 PublishDialog

打开租飞弹窗 RentDialog

打开飞场报名弹窗 EventDialog

打开删除确认弹窗 DeleteDialog

详情弹窗操作

关闭详情弹窗

发布弹窗操作

点击立即发布

租飞弹窗操作

费用动态计算+脉冲动画

关闭弹窗

飞场报名弹窗操作

显示场地+调机券信息

弹窗系统:发布航模与航模租飞

发布弹窗从底部滑出,提供装备类别选择和价格输入;租飞弹窗居中显示,提供天数选择和费用脉冲动画。

  // 弹窗1:发布航模(底部)
  @Builder
  PublishDialog() {
    Column() {
      Column() {
        Row() {
          Text('发布闲置航模')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#BF360C')
          Column()
            .layoutWeight(1)
          Text('✕')
            .fontSize(16)
            .fontColor('#9E9E9E')
            .onClick(() => {
              this.showPublish = false
            })
        }
        .width('100%')

        Text('装备类别')
          .fontSize(12)
          .fontColor('#B5A08F')
          .margin({ top: 14 })
        Row() {
          ForEach(this.itemKinds, (k: string, index: number) => {
            Text(k)
              .fontSize(11)
              .fontColor(this.itemKind === index ? '#FFFFFF' : '#F4511E')
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .backgroundColor(this.itemKind === index ? '#F4511E' : '#FFEBE0')
              .borderRadius(13)
              .margin({ right: 8 })
              .onClick(() => {
                this.itemKind = index
              })
          }, (k: string) => k)
        }
        .margin({ top: 8 })

        Text('期望售价(¥)')
          .fontSize(12)
          .fontColor('#B5A08F')
          .margin({ top: 14 })
        Text(this.priceField === '' ? '输入价格,如 980' : '¥ ' + this.priceField)
          .fontSize(14)
          .fontColor(this.priceField === '' ? '#CDB2A8' : '#BF360C')
          .padding(12)
          .backgroundColor('#FFEBE0')
          .borderRadius(10)
          .width('100%')
          .margin({ top: 8 })
          .onClick(() => {
            this.priceField = '980'
          })

        Text('整机免费调机试飞 · 锂电池需合规寄运')
          .fontSize(10)
          .fontColor('#B5A08F')
          .margin({ top: 10 })

        Text('立即发布')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#3E2723')
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#FFD600')
          .borderRadius(24)
          .margin({ top: 16 })
          .onClick(() => {
            this.showPublish = false
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 20, topRight: 20 })
      .constraintSize({ maxHeight: '80%' })
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.End)
    .backgroundColor('#66000000')
    .onClick(() => {
      this.showPublish = false
    })
  }

发布弹窗采用底部滑出式设计,外层Column设置justifyContent(FlexAlign.End)使卡片贴底显示。装备类别选择区通过ForEach遍历itemKinds数组(固定翼、穿越机、遥控车、遥控器、电机电调、电池配件)渲染标签按钮,选中状态使用飞行橙背景。价格输入区使用Text模拟输入框,点击后填充预设值980。发布按钮使用柠檬黄背景和深棕文字色,与头部发布按钮风格一致。内容卡片设置onClick((event: ClickEvent) => { event.stopPropagation() })阻止事件冒泡,实现"点击遮罩关闭、点击内容保留"的交互模式。

  // 弹窗2:航模租飞(天数 + 价格脉冲)
  @Builder
  RentDialog() {
    Column() {
      Column() {
        Text('🚀 航模租飞')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')

        Text('选择租飞天数')
          .fontSize(12)
          .fontColor('#B5A08F')
          .margin({ top: 14 })
          .width('100%')

        Row() {
          ForEach([1, 2, 3, 7, 15], (d: number) => {
            Text(d + ' 天')
              .fontSize(11)
              .fontColor(this.rentDays === d ? '#FFFFFF' : '#1565C0')
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .backgroundColor(this.rentDays === d ? '#42A5F5' : '#E3F2FD')
              .borderRadius(14)
              .margin({ right: 8 })
              .onClick(() => {
                this.rentDays = d
              })
          }, (d: number) => d + '')
        }
        .margin({ top: 8 })

        Row() {
          Column() {
            Row() {
              Text('预估费用:')
                .fontSize(12)
                .fontColor('#8D6E63')
              Text('¥ ' + (this.rentDays * 88))
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#F4511E')
                .scale({ x: 1.06, y: 1.06 })
                .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
            }
          }
          .alignItems(HorizontalAlign.Start)
        }
        .margin({ top: 12 })

        Column() {
          Text('含炸机险 · 教练带飞一次 · 归还免费检修')
            .fontSize(10)
            .fontColor('#B5A08F')
            .margin({ top: 4 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#E3F2FD')
        .borderRadius(10)
        .margin({ top: 12 })

        Row() {
          Text('预约租飞')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#3E2723')
            .padding({ left: 22, right: 22, top: 10, bottom: 10 })
            .backgroundColor('#FFD600')
            .borderRadius(20)
            .onClick(() => {
              this.showRent = false
            })
          Text('取消')
            .fontSize(14)
            .fontColor('#B5A08F')
            .padding({ left: 22, right: 22, top: 10, bottom: 10 })
            .backgroundColor('#FFEBE0')
            .borderRadius(20)
            .margin({ left: 12 })
            .onClick(() => {
              this.showRent = false
            })
        }
        .margin({ top: 18 })
      }
      .width('86%')
      .padding(20)
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
    .onClick(() => {
      this.showRent = false
    })
  }

租飞弹窗居中显示,宽度86%。天数选择区遍历内联数组[1, 2, 3, 7, 15]渲染五个选项,选中状态使用天空蓝#42A5F5背景,未选中使用浅蓝#E3F2FD。预估费用通过this.rentDays * 88动态计算(每日88元),费用数字附带scaleanimation实现持续脉冲缩放动画。服务提示区使用浅蓝背景展示"含炸机险·教练带飞一次·归还免费检修",这些增值服务是航模租飞场景的特色保障。底部操作区包含"预约租飞"(柠檬黄背景)和"取消"(浅橙背景)两个按钮,主次分明。

弹窗系统:飞场报名与详情侧滑

  // 弹窗3:飞场报名(场地选择)
  @Builder
  EventDialog() {
    Column() {
      Column() {
        Text('🪂 飞场活动报名')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')

        Text('选择常飞场地')
          .fontSize(12)
          .fontColor('#B5A08F')
          .margin({ top: 14 })
          .width('100%')

        Row() {
          ForEach(this.eventFields, (f: string, index: number) => {
            Text(f)
              .fontSize(10)
              .fontColor(this.eventField === index ? '#FFFFFF' : '#1565C0')
              .padding({ left: 8, right: 8, top: 5, bottom: 5 })
              .backgroundColor(this.eventField === index ? '#42A5F5' : '#E3F2FD')
              .borderRadius(12)
              .margin({ right: 6, top: 6 })
              .onClick(() => {
                this.eventField = index
              })
          }, (f: string) => f)
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)

        Column() {
          Row() {
            Text('常飞场地:' + this.eventFields[this.eventField])
              .fontSize(12)
              .fontColor('#BF360C')
            Column()
              .layoutWeight(1)
            Text('送调机券')
              .fontSize(11)
              .fontWeight(FontWeight.Bold)
              .fontColor('#F4511E')
          }
          .width('100%')
          Text('到场签到即送 50 元调机抵扣券,可叠加使用')
            .fontSize(10)
            .fontColor('#B5A08F')
            .margin({ top: 6 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#F4FAFF')
        .borderRadius(10)
        .margin({ top: 14 })

        Row() {
          Text('立即报名')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .padding({ left: 22, right: 22, top: 10, bottom: 10 })
            .backgroundColor('#42A5F5')
            .borderRadius(20)
            .onClick(() => {
              this.showEvent = false
            })
          Text('再想想')
            .fontSize(14)
            .fontColor('#B5A08F')
            .padding({ left: 22, right: 22, top: 10, bottom: 10 })
            .backgroundColor('#E3F2FD')
            .borderRadius(20)
            .margin({ left: 12 })
            .onClick(() => {
              this.showEvent = false
            })
        }
        .margin({ top: 18 })
      }
      .width('86%')
      .padding(20)
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
    .onClick(() => {
      this.showEvent = false
    })
  }

飞场报名弹窗使用flexWrap(FlexWrap.Wrap)让场地标签自动换行,因为五个飞场名称(九龙湖飞场、顺义航模基地、室内漂移馆、雁栖湖飞场、固安飞行营)长度不一。选中场地使用天空蓝背景,未选中使用浅蓝。信息卡片动态显示当前选中的场地名称this.eventFields[this.eventField],并附带"送调机券"优惠提示。

  // 弹窗5:航模详情(右侧滑出)
  @Builder
  DetailDialog() {
    Column() {
      Column() {
        Row() {
          Text('航模详情')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#BF360C')
          Column()
            .layoutWeight(1)
          Text('✕')
            .fontSize(16)
            .fontColor('#9E9E9E')
            .onClick(() => {
              this.showDetail = false
            })
        }
        .width('100%')

        Column() {
          Text('✈️')
            .fontSize(54)
        }
        .width('100%')
        .height(120)
        .justifyContent(FlexAlign.Center)
        .backgroundColor('#E3F2FD')
        .borderRadius(12)
        .margin({ top: 14 })

        Text(this.selName())
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')
          .margin({ top: 12 })
          .width('100%')

        Row() {
          Text(this.selBrand())
            .fontSize(11)
            .fontColor('#F4511E')
            .padding({ left: 8, right: 8, top: 3, bottom: 3 })
            .backgroundColor('#FFCCBC')
            .borderRadius(4)
          Text('· ' + this.selCity())
            .fontSize(11)
            .fontColor('#B5A08F')
        }
        .margin({ top: 8 })

        Row() {
          Text('¥' + this.selPrice())
            .fontSize(26)
            .fontWeight(FontWeight.Bold)
            .fontColor('#F4511E')
          Text('原价 ¥' + this.selOrigin())
            .fontSize(12)
            .fontColor('#CDB2A8')
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 10 })
        }
        .margin({ top: 12 })

        Column() {
          Row() {
            Text('机况完好度')
              .fontSize(12)
              .fontColor('#B5A08F')
            Column()
              .layoutWeight(1)
            Text((100 - this.selFlights()) + '%')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor(this.selFlights() < 20 ? '#2E7D32' : '#EF6C00')
          }
          .width('100%')
          Stack({ alignContent: Alignment.Start }) {
            Column()
              .width('100%')
              .height(6)
              .backgroundColor('#FFEBE0')
              .borderRadius(3)
            Column()
              .width((100 - this.selFlights()) + '%')
              .height(6)
              .backgroundColor(this.selFlights() < 20 ? '#66BB6A' : '#FFB74D')
              .borderRadius(3)
          }
          .width('100%')
          .height(6)
          .margin({ top: 6 })
          Text('共 ' + this.selFlights() + ' 次飞行 · 平台调机测试后发货')
            .fontSize(9)
            .fontColor('#B5A08F')
            .margin({ top: 4 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#F4FAFF')
        .borderRadius(10)
        .margin({ top: 14 })

        Row() {
          Text('租 3 天')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1565C0')
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .backgroundColor('#FFFFFF')
            .border({ width: 1, color: '#42A5F5', radius: 20 })
            .onClick(() => {
              this.showDetail = false
              this.showRent = true
            })
          Text('聊卖家')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .backgroundColor('#F4511E')
            .borderRadius(20)
            .margin({ left: 12 })
            .onClick(() => {
              this.showDetail = false
            })
        }
        .margin({ top: 16 })
      }
      .width('88%')
      .height('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 20, bottomLeft: 20 })
      .constraintSize({ maxHeight: '80%' })
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.End)
    .backgroundColor('#66000000')
    .onClick(() => {
      this.showDetail = false
    })
  }

详情弹窗采用右侧滑出设计,通过justifyContent(FlexAlign.End)贴右显示,borderRadius({ topLeft: 20, bottomLeft: 20 })设置左上左下圆角。弹窗内所有数据均通过selName()selPrice()等安全方法提取。机况完好度进度条使用Stack叠放背景轨道和填充条,填充宽度为(100 - this.selFlights()) + '%',颜色根据飞行次数动态变化。底部"租3天"按钮使用边框样式(天空蓝边框+白色背景)与"聊卖家"实心按钮(飞行橙背景)形成视觉区分。"租3天"的onClick先关闭详情再打开租飞弹窗,实现弹窗间跳转联动。

技术点对比表

技术点实现方式适用场景优势分析注意事项
@State状态管理十个状态变量驱动全部交互弹窗开关、Tab切换、选中项、选择项响应式自动更新,逻辑集中管理状态较多时需注意变量命名规范
@Builder视图构建七Tab+五弹窗均用@Builder多页面应用的视图拆分结构清晰,可读性强Builder内无法直接修改外部状态
ForEach列表渲染遍历数据数组生成列表项装备列表、消息列表、Tab导航支持键值优化diff键值需唯一,否则渲染异常
柱状图可视化Column高度按比例计算翼展、车速、通道数三种数据维度纯原生实现,无第三方依赖需动态计算最大值归一化
双列网格布局flexWrap(FlexWrap.Wrap)+47%宽度遥控车专区卡片展示一屏展示更多内容,空间利用率高需注意卡片高度一致性
进度条可视化Stack叠放两层Column机况完好度、电池电量、报名进度轻量高效,样式灵活宽度百分比计算需注意精度
脉冲动画scale+animation无限循环费用数字、报名状态文字吸引用户注意力,增强紧迫感无限循环动画需关注性能
linearGradient渐变colors数组定义色值头部、横幅、用户卡片视觉层次丰富,主题氛围强颜色值格式需统一
条件渲染弹窗if语句块在build末尾五个弹窗独立显隐控制覆盖层级清晰,互不干扰需处理弹窗叠加场景
stopPropagationonClick中调用event.stopPropagation弹窗内容区阻止冒泡标准弹窗交互模式仅ClickEvent回调可用
flexWrap自动换行FlexWrap.Wrap+宽度百分比飞场标签、课程等级标签自适应内容长度,避免溢出需设置合理间距
constraintSize约束maxHeight限制弹窗高度发布弹窗、详情弹窗防止内容超出屏幕需配合内部Scroll使用

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 飞行橙白风格:飞行橙 #F4511E / 天空蓝 #42A5F5 / 云白 #F4FAFF / 柠檬黄 #FFD600
// 7 Tab:航模集市 / 固定翼 / 遥控车 / 遥控器设备 / 飞场活动 / 消息 / 我的
// 5 弹框:发布航模(底部) / 航模租飞(天数+价格脉冲) / 飞场报名(场地选择) / 删除确认(警示) / 航模详情(右侧滑出)

interface PlaneItem {
  name: string
  brand: string
  price: number
  originPrice: number
  flights: number
  city: string
  tag: string
}

interface FixedWingItem {
  name: string
  wingspan: number
  channel: number
  price: number
  motor: string
}

interface CarItem {
  name: string
  scale: string
  speed: number
  price: number
  runs: number
}

interface RadioItem {
  name: string
  protocol: string
  channels: number
  price: number
  battery: number
}

interface EventItem {
  name: string
  field: string
  joined: number
  total: number
  date: string
}

interface NoteItem {
  name: string
  content: string
  time: string
  unread: number
  type: string
}

@Entry
@Component
struct RcPlaneLoopApp {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showRent: boolean = false
  @State showEvent: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: PlaneItem | null = null
  @State rentDays: number = 2
  @State eventField: number = 0
  @State itemKind: number = 0
  @State priceField: string = ''

  private tabs: string[] = ['航模集市', '固定翼', '遥控车', '遥控设备', '飞场活动', '消息', '我的']

  private planeList: PlaneItem[] = [
    { name: 'DJI Mini 4 Pro 全套', brand: 'DJI', price: 4280, originPrice: 6788, flights: 18, city: '深圳', tag: '热门' },
    { name: 'FMS 1400mm P-51 野马', brand: 'FMS', price: 980, originPrice: 1980, flights: 25, city: '北京', tag: '包邮' },
    { name: 'Freewing 90mm 米格15', brand: 'Freewing', price: 1680, originPrice: 3280, flights: 12, city: '上海', tag: '精品' },
    { name: 'E-flite Timber 1.5m', brand: 'E-flite', price: 1280, originPrice: 2580, flights: 30, city: '成都', tag: '95新' },
    { name: '天地飞 9通道遥控器', brand: '天地飞', price: 320, originPrice: 680, flights: 0, city: '杭州', tag: '全新' },
    { name: '乐迪 AT9S Pro 遥控', brand: 'Radiolink', price: 450, originPrice: 899, flights: 0, city: '广州', tag: '包邮' },
    { name: 'AXi 2820 无刷电机', brand: 'AXi', price: 220, originPrice: 480, flights: 40, city: '南京', tag: '急出' },
    { name: '好盈 60A 电调', brand: 'Hobbywing', price: 160, originPrice: 350, flights: 35, city: '武汉', tag: '自提' },
    { name: '华科尔 D10 遥控器', brand: 'Deviation', price: 580, originPrice: 1180, flights: 0, city: '苏州', tag: '95新' },
    { name: '京商 1/10 电越电动', brand: 'Kyosho', price: 890, originPrice: 1780, flights: 50, city: '天津', tag: '急出' },
    { name: 'HPI RS4 漂移车', brand: 'HPI', price: 720, originPrice: 1480, flights: 60, city: '重庆', tag: '包邮' },
    { name: 'Tamiya TT-02 底盘', brand: 'Tamiya', price: 380, originPrice: 780, flights: 20, city: '西安', tag: '热门' },
    { name: 'Rotor Logic 3D 特技机', brand: 'RL', price: 560, originPrice: 1180, flights: 15, city: '长沙', tag: '精品' },
    { name: '凤凰 480 穿越机套装', brand: 'iFlight', price: 680, originPrice: 1380, flights: 22, city: '青岛', tag: '95新' },
    { name: '格氏 4S 1500mAh 电池×4', brand: '格氏', price: 240, originPrice: 520, flights: 45, city: '大连', tag: '急出' },
    { name: 'Futaba T16IZ 遥控器', brand: 'Futaba', price: 2680, originPrice: 4980, flights: 0, city: '厦门', tag: '收藏' },
    { name: 'Jumper T18 遥控器', brand: 'Jumper', price: 780, originPrice: 1580, flights: 0, city: '宁波', tag: '包邮' },
    { name: 'Skywing 50CC Extra', brand: 'Skywing', price: 3880, originPrice: 7600, flights: 8, city: '无锡', tag: '精品' },
    { name: '双天 2205 电机×4', brand: 'T-Motor', price: 280, originPrice: 560, flights: 30, city: '济南', tag: '自提' },
    { name: '银燕 9g 舵机 ×10', brand: '银燕', price: 120, originPrice: 260, flights: 0, city: '合肥', tag: '全新' },
    { name: '链博士充电器 300W', brand: 'ToolkitRC', price: 320, originPrice: 660, flights: 0, city: '郑州', tag: '包邮' },
    { name: 'DJI FPV 眼镜 Goggles2', brand: 'DJI', price: 1580, originPrice: 2980, flights: 0, city: '佛山', tag: '95新' },
    { name: 'BetaFPV 口袋遥控', brand: 'BetaFPV', price: 260, originPrice: 540, flights: 0, city: '东莞', tag: '热门' },
    { name: '收云 3D 纸飞机 KT板', brand: '收云', price: 65, originPrice: 148, flights: 12, city: '福州', tag: '急出' }
  ]

  private wingList: FixedWingItem[] = [
    { name: 'FMS 1400 P-51 野马', wingspan: 1400, channel: 6, price: 980, motor: '无刷 3536' },
    { name: 'Freewing 90mm 米格15', wingspan: 1220, channel: 7, price: 1680, motor: 'EDF 90mm' },
    { name: 'E-flite Timber 1.5m', wingspan: 1500, channel: 6, price: 1280, motor: '无刷 3236' },
    { name: 'Skywing 50CC Extra', wingspan: 2000, channel: 9, price: 3880, motor: '汽油 50CC' },
    { name: '收云 3D 纸飞机', wingspan: 800, channel: 4, price: 65, motor: '无刷 2205' },
    { name: 'Rotor Logic 特技机', wingspan: 1150, channel: 7, price: 560, motor: '无刷 2820' },
    { name: 'Volantex 米格3 战斗机', wingspan: 1100, channel: 5, price: 620, motor: '无刷 2815' },
    { name: 'E-flite Valiant 1.3m', wingspan: 1300, channel: 6, price: 1150, motor: '无刷 3230' },
    { name: 'FMS 800 天鹅教练机', wingspan: 800, channel: 4, price: 420, motor: '有刷 850' },
    { name: '双马 2.4G 滑翔机', wingspan: 1050, channel: 4, price: 280, motor: '有刷 380' }
  ]

  private carList: CarItem[] = [
    { name: '京商 1/10 电越', scale: '1/10', speed: 60, price: 890, runs: 50 },
    { name: 'HPI RS4 漂移车', scale: '1/10', speed: 45, price: 720, runs: 60 },
    { name: 'Tamiya TT-02 底盘', scale: '1/10', speed: 40, price: 380, runs: 20 },
    { name: 'Traxxas Slash 短卡', scale: '1/10', speed: 70, price: 1580, runs: 15 },
    { name: 'Axial SCX10 攀爬', scale: '1/10', speed: 15, price: 1180, runs: 25 },
    { name: 'MST RMX 2.0 漂移', scale: '1/10', speed: 42, price: 980, runs: 35 },
    { name: '田宫 大脚虫 1/14', scale: '1/14', speed: 30, price: 320, runs: 40 },
    { name: 'Losi 迷你 8ight', scale: '1/14', speed: 55, price: 1080, runs: 18 },
    { name: 'HSP 1/16 越野', scale: '1/16', speed: 35, price: 260, runs: 55 },
    { name: 'KPI 1/8 大脚车', scale: '1/8', speed: 65, price: 1280, runs: 22 }
  ]

  private radioList: RadioItem[] = [
    { name: 'Futaba T16IZ', protocol: 'FASST', channels: 16, price: 2680, battery: 92 },
    { name: 'Jumper T18', protocol: '多协议', channels: 18, price: 780, battery: 85 },
    { name: '乐迪 AT9S Pro', protocol: 'CC2500', channels: 9, price: 450, battery: 78 },
    { name: '天地飞 ET07', protocol: 'AFHDS', channels: 7, price: 320, battery: 70 },
    { name: '华科尔 D10', protocol: 'DSM2', channels: 10, price: 580, battery: 82 },
    { name: 'RadioMaster TX16S', protocol: '多协议', channels: 16, price: 690, battery: 88 },
    { name: 'BetaFPV LiteRadio3', protocol: 'ELRS', channels: 4, price: 260, battery: 65 },
    { name: 'Jumper T-Lite', protocol: 'ELRS', channels: 6, price: 380, battery: 72 }
  ]

  private eventList: EventItem[] = [
    { name: '周末固定翼编队飞', field: '九龙湖飞场', joined: 18, total: 30, date: '08-29' },
    { name: 'FPV 竞速新手赛', field: '顺义航模基地', joined: 24, total: 40, date: '09-05' },
    { name: '漂移车碰碰夜', field: '室内漂移馆', joined: 12, total: 20, date: '08-27' },
    { name: '攀爬车越野挑战', field: '狼坡岩场', joined: 15, total: 25, date: '09-12' },
    { name: '航拍作品分享会', field: '科技园展厅', joined: 30, total: 50, date: '09-18' },
    { name: '穿越机花飞教学', field: '雁栖湖飞场', joined: 8, total: 15, date: '08-31' },
    { name: '油机声浪聚会', field: '固安飞行营', joined: 10, total: 20, date: '09-20' },
    { name: '新手炸机互助局', field: '奥森北园', joined: 22, total: 30, date: '08-25' }
  ]

  private noteList: NoteItem[] = [
    { name: '云端飞手', content: '遥控器能试频吗', time: '12:08', unread: 2, type: '买家' },
    { name: '系统通知', content: '飞场活动报名成功', time: '11:30', unread: 1, type: '官方' },
    { name: '老张航模', content: '电池循环多少次了', time: '10:15', unread: 0, type: '买家' },
    { name: '飞场管理员', content: '本周末风大注意安全', time: '昨天', unread: 1, type: '官方' },
    { name: '航模严选铺', content: '回收闲置航模设备', time: '昨天', unread: 0, type: '商家' },
    { name: '穿越机小队', content: '桨叶还有几对', time: '昨天', unread: 0, type: '买家' },
    { name: '交易助手', content: '请确认收货并评价', time: '前天', unread: 0, type: '官方' },
    { name: '漂移车队', content: '底盘调校好了', time: '前天', unread: 0, type: '买家' },
    { name: '安全提醒', content: '锂电池请合规寄运', time: '3天前', unread: 0, type: '官方' },
    { name: '租飞管家', content: '租用航模已归还入库', time: '3天前', unread: 0, type: '官方' },
    { name: '飞友会', content: '10月全国航模展集合', time: '4天前', unread: 0, type: '官方' },
    { name: '阿凯模型', content: 'EDF 推力测过了', time: '4天前', unread: 0, type: '买家' }
  ]

  private eventFields: string[] = ['九龙湖飞场', '顺义航模基地', '室内漂移馆', '雁栖湖飞场', '固安飞行营']

  private itemKinds: string[] = ['固定翼', '穿越机', '遥控车', '遥控器', '电机电调', '电池配件']

  private maxWing(): number {
    let max: number = 0
    for (let i = 0; i < this.wingList.length; i++) {
      if (this.wingList[i].wingspan > max) {
        max = this.wingList[i].wingspan
      }
    }
    return max
  }

  private maxSpeed(): number {
    let max: number = 0
    for (let i = 0; i < this.carList.length; i++) {
      if (this.carList[i].speed > max) {
        max = this.carList[i].speed
      }
    }
    return max
  }

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

  private selPrice(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.price + ''
  }

  private selBrand(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.brand
  }

  private selFlights(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.flights
  }

  private selCity(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.city
  }

  private selOrigin(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.originPrice + ''
  }

  build() {
    Column() {
      Column() {
        // 头部:静态航模电商风
        Column() {
          Row() {
            Column() {
              Text('翼上循环')
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('二手航模装备 起飞流转')
                .fontSize(11)
                .fontColor('#FFE0CC')
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start)

            Row() {
              Text('🛩️')
                .fontSize(15)
              Text('搜航模 / 品牌')
                .fontSize(12)
                .fontColor('#B5A08F')
                .margin({ left: 6 })
            }
            .width(145)
            .height(32)
            .justifyContent(FlexAlign.Center)
            .backgroundColor('#F4FAFF')
            .borderRadius(16)
            .margin({ left: 12 })

            Text('发布')
              .fontSize(13)
              .fontWeight(FontWeight.Medium)
              .fontColor('#3E2723')
              .width(52)
              .height(30)
              .textAlign(TextAlign.Center)
              .backgroundColor('#FFD600')
              .borderRadius(15)
              .margin({ left: 10 })
              .onClick(() => {
                this.showPublish = true
              })
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 12, bottom: 10 })

          Row() {
            Text('🛠️ 免费调机')
              .fontSize(11)
              .fontColor('#FFE0CC')
              .margin({ right: 12 })
            Text('🔋 电池健康可查')
              .fontSize(11)
              .fontColor('#FFE0CC')
              .margin({ right: 12 })
            Text('🪂 飞场直供')
              .fontSize(11)
              .fontColor('#FFE0CC')
          }
          .width('100%')
          .padding({ left: 16, bottom: 12 })
        }
        .width('100%')
        .backgroundColor('#F4511E')
        .borderRadius({ bottomLeft: 18, bottomRight: 18 })

        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.MarketTab()
            } else if (this.currentTab === 1) {
              this.WingTab()
            } else if (this.currentTab === 2) {
              this.CarTab()
            } else if (this.currentTab === 3) {
              this.RadioTab()
            } else if (this.currentTab === 4) {
              this.EventTab()
            } else if (this.currentTab === 5) {
              this.NoteTab()
            } else {
              this.MineTab()
            }
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)
        .width('100%')

        Row() {
          ForEach(this.tabs, (tab: string, index: number) => {
            Column() {
              Text(this.tabIcon(index))
                .fontSize(18)
                .fontColor(this.currentTab === index ? '#F4511E' : '#9E9E9E')
              Text(tab)
                .fontSize(10)
                .fontColor(this.currentTab === index ? '#F4511E' : '#9E9E9E')
                .margin({ top: 2 })
              if (this.currentTab === index) {
                Column()
                  .width(20)
                  .height(3)
                  .backgroundColor('#42A5F5')
                  .borderRadius(2)
                  .margin({ top: 3 })
              } else {
                Column()
                  .width(20)
                  .height(3)
                  .backgroundColor('#00000000')
                  .margin({ top: 3 })
              }
            }
            .justifyContent(FlexAlign.Center)
            .layoutWeight(1)
            .onClick(() => {
              this.currentTab = index
            })
          }, (tab: string) => tab)
        }
        .width('100%')
        .height(58)
        .backgroundColor('#FFFFFF')
        .border({ width: 1, color: '#FFE0CC', radius: 0 })
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#F4FAFF')

      if (this.showPublish) {
        this.PublishDialog()
      }
      if (this.showRent) {
        this.RentDialog()
      }
      if (this.showEvent) {
        this.EventDialog()
      }
      if (this.showDelete) {
        this.DeleteDialog()
      }
      if (this.showDetail) {
        this.DetailDialog()
      }
    }
  }

  private tabIcon(index: number): string {
    let icons: string[] = ['🛍️', '✈️', '🏎️', '🎮', '🪂', '💬', '👤']
    return icons[index]
  }

  // Tab1 航模集市:行式列表
  @Builder
  MarketTab() {
    Column() {
      Row() {
        Column() {
          Text('🛩️ 飞手集结季')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('旧机折抵新机 立减 500')
            .fontSize(11)
            .fontColor('#FFE0CC')
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text('🚀')
          .fontSize(36)
          .scale({ x: 1.12, y: 1.12 })
          .animation({ duration: 1000, iterations: -1, curve: Curve.EaseInOut })
      }
      .width('100%')
      .padding(16)
      .linearGradient({
        direction: GradientDirection.Right,
        colors: [['#F4511E', 0], ['#42A5F5', 1]]
      })
      .borderRadius(14)
      .margin({ top: 12, left: 12, right: 12 })

      Row() {
        Text('装备精选')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')
        Column()
          .layoutWeight(1)
        Text('飞行次数 ▾')
          .fontSize(12)
          .fontColor('#F4511E')
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14 })

      ForEach(this.planeList, (item: PlaneItem) => {
        Row() {
          Column() {
            Text('✈️')
              .fontSize(26)
          }
          .width(66)
          .height(66)
          .justifyContent(FlexAlign.Center)
          .backgroundColor('#FFEBE0')
          .borderRadius(12)

          Column() {
            Text(item.name)
              .fontSize(13)
              .fontWeight(FontWeight.Medium)
              .fontColor('#BF360C')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Row() {
              Text(item.brand)
                .fontSize(9)
                .fontColor('#F4511E')
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .backgroundColor('#FFCCBC')
                .borderRadius(4)
              Text(item.flights + ' 飞')
                .fontSize(9)
                .fontColor('#B5A08F')
                .margin({ left: 6 })
              Text('· ' + item.city)
                .fontSize(9)
                .fontColor('#B5A08F')
            }
            .margin({ top: 4 })
            Row() {
              Text('机况')
                .fontSize(9)
                .fontColor('#B5A08F')
              Column()
                .width((100 - item.flights) / 100 * 55)
                .height(3)
                .backgroundColor(item.flights < 20 ? '#66BB6A' : '#FFB74D')
                .borderRadius(2)
                .margin({ left: 4 })
              Text((100 - item.flights) + '%')
                .fontSize(9)
                .fontColor('#B5A08F')
                .margin({ left: 4 })
            }
            .margin({ top: 5 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })

          Column() {
            Text('¥' + item.price)
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#F4511E')
            Text('¥' + item.originPrice)
              .fontSize(9)
              .fontColor('#CDB2A8')
              .decoration({ type: TextDecorationType.LineThrough })
              .margin({ top: 2 })
            Text(item.tag)
              .fontSize(9)
              .fontColor('#3E2723')
              .padding({ left: 5, right: 5, top: 1, bottom: 1 })
              .backgroundColor('#FFD600')
              .borderRadius(3)
              .margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .padding(11)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .margin({ left: 12, right: 12, top: 5 })
        .onClick(() => {
          this.selectedItem = item
          this.showDetail = true
        })
      }, (item: PlaneItem) => item.name)

      Column()
        .height(20)
    }
    .width('100%')
  }

  // Tab2 固定翼:翼展柱状图 + 列表
  @Builder
  WingTab() {
    Column() {
      Row() {
        Text('✈️ 固定翼专区')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')
        Column()
          .layoutWeight(1)
        Text('共 ' + this.wingList.length + ' 架')
          .fontSize(11)
          .fontColor('#B5A08F')
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14 })

      Text('翼展分布(mm)')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#BF360C')
        .margin({ top: 12, left: 16 })

      Row() {
        ForEach(this.wingList, (item: FixedWingItem) => {
          Column() {
            Column()
              .width(22)
              .height(item.wingspan / this.maxWing() * 115)
              .backgroundColor(item.wingspan > 1500 ? '#F4511E' : (item.wingspan > 1000 ? '#42A5F5' : '#FFB74D'))
              .borderRadius(5)
            Text(item.wingspan + '')
              .fontSize(8)
              .fontColor('#B5A08F')
              .margin({ top: 3 })
          }
          .justifyContent(FlexAlign.End)
          .alignItems(HorizontalAlign.Center)
          .layoutWeight(1)
        }, (item: FixedWingItem) => item.name)
      }
      .width('100%')
      .height(155)
      .padding({ left: 10, right: 10 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 8, left: 12, right: 12 })
      .justifyContent(FlexAlign.End)

      ForEach(this.wingList, (item: FixedWingItem) => {
        Row() {
          Column() {
            Text('✈️')
              .fontSize(26)
          }
          .width(54)
          .height(54)
          .justifyContent(FlexAlign.Center)
          .backgroundColor('#E3F2FD')
          .borderRadius(10)

          Column() {
            Text(item.name)
              .fontSize(13)
              .fontWeight(FontWeight.Medium)
              .fontColor('#BF360C')
              .maxLines(1)
            Row() {
              Text(item.wingspan + 'mm')
                .fontSize(9)
                .fontColor('#FFFFFF')
                .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                .backgroundColor('#42A5F5')
                .borderRadius(3)
              Text(item.channel + ' 通道')
                .fontSize(10)
                .fontColor('#B5A08F')
                .margin({ left: 6 })
            }
            .margin({ top: 4 })
            Text(item.motor)
              .fontSize(10)
              .fontColor('#B5A08F')
              .margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })

          Text('¥' + item.price)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#F4511E')
        }
        .width('100%')
        .padding(11)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .margin({ left: 12, right: 12, top: 6 })
      }, (item: FixedWingItem) => item.name)

      Column()
        .height(20)
    }
    .width('100%')
  }

  // Tab3 遥控车:车速柱状图 + 双列网格
  @Builder
  CarTab() {
    Column() {
      Text('极速分布(km/h)')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#BF360C')
        .margin({ top: 14, left: 16 })

      Row() {
        ForEach(this.carList, (item: CarItem) => {
          Column() {
            Column()
              .width(22)
              .height(item.speed / this.maxSpeed() * 115)
              .backgroundColor(item.speed > 55 ? '#F4511E' : (item.speed > 35 ? '#42A5F5' : '#FFB74D'))
              .borderRadius(5)
            Text(item.speed + '')
              .fontSize(8)
              .fontColor('#B5A08F')
              .margin({ top: 3 })
          }
          .justifyContent(FlexAlign.End)
          .alignItems(HorizontalAlign.Center)
          .layoutWeight(1)
        }, (item: CarItem) => item.name)
      }
      .width('100%')
      .height(155)
      .padding({ left: 10, right: 10 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 8, left: 12, right: 12 })
      .justifyContent(FlexAlign.End)

      Row() {
        ForEach(this.carList, (item: CarItem) => {
          Column() {
            Column() {
              Text('🏎️')
                .fontSize(34)
                .scale({ x: 1.08, y: 1.08 })
                .animation({ duration: 1100, iterations: -1, curve: Curve.EaseInOut })
            }
            .width('100%')
            .height(72)
            .justifyContent(FlexAlign.Center)
            .backgroundColor('#FFEBE0')
            .borderRadius(10)

            Text(item.name)
              .fontSize(12)
              .fontWeight(FontWeight.Medium)
              .fontColor('#BF360C')
              .margin({ top: 6 })
              .maxLines(1)
            Row() {
              Text(item.scale)
                .fontSize(9)
                .fontColor('#FFFFFF')
                .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                .backgroundColor('#42A5F5')
                .borderRadius(3)
              Text(item.speed + ' km/h')
                .fontSize(9)
                .fontColor('#B5A08F')
                .margin({ left: 5 })
            }
            .margin({ top: 4 })
            Text(item.runs + ' 次下场')
              .fontSize(9)
              .fontColor('#B5A08F')
              .margin({ top: 3 })
            Text('¥' + item.price)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#F4511E')
              .margin({ top: 4 })
          }
          .width('47%')
          .padding(10)
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .margin({ top: 8 })
          .alignItems(HorizontalAlign.Start)
        }, (item: CarItem) => item.name)
      }
      .width('94%')
      .margin({ left: 12 })

      Column()
        .height(20)
    }
    .width('100%')
  }

  // Tab4 遥控器设备:横向滚动卡 + 电量进度
  @Builder
  RadioTab() {
    Column() {
      Row() {
        Text('🎮 遥控器设备专区')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')
        Column()
          .layoutWeight(1)
        Text('共 ' + this.radioList.length + ' 台')
          .fontSize(11)
          .fontColor('#B5A08F')
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14 })

      Text('通道数量分布(ch)')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#BF360C')
        .margin({ top: 12, left: 16 })

      Row() {
        ForEach(this.radioList, (item: RadioItem) => {
          Column() {
            Column()
              .width(24)
              .height(item.channels / 18 * 115)
              .backgroundColor(item.channels >= 16 ? '#F4511E' : (item.channels >= 9 ? '#42A5F5' : '#FFD600'))
              .borderRadius(5)
            Text(item.channels + 'ch')
              .fontSize(8)
              .fontColor('#B5A08F')
              .margin({ top: 3 })
          }
          .justifyContent(FlexAlign.End)
          .alignItems(HorizontalAlign.Center)
          .layoutWeight(1)
        }, (item: RadioItem) => item.name)
      }
      .width('100%')
      .height(155)
      .padding({ left: 10, right: 10 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 8, left: 12, right: 12 })
      .justifyContent(FlexAlign.End)

      ForEach(this.radioList, (item: RadioItem) => {
        Row() {
          Column() {
            Text('🎮')
              .fontSize(26)
          }
          .width(54)
          .height(54)
          .justifyContent(FlexAlign.Center)
          .backgroundColor('#E3F2FD')
          .borderRadius(10)

          Column() {
            Text(item.name)
              .fontSize(13)
              .fontWeight(FontWeight.Medium)
              .fontColor('#BF360C')
              .maxLines(1)
            Row() {
              Text(item.protocol)
                .fontSize(9)
                .fontColor('#FFFFFF')
                .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                .backgroundColor('#F4511E')
                .borderRadius(3)
              Text(item.channels + ' 通道')
                .fontSize(10)
                .fontColor('#B5A08F')
                .margin({ left: 6 })
            }
            .margin({ top: 4 })
            Row() {
              Text('电池')
                .fontSize(9)
                .fontColor('#B5A08F')
              Column()
                .width(item.battery / 100 * 55)
                .height(3)
                .backgroundColor(item.battery > 80 ? '#66BB6A' : '#FFB74D')
                .borderRadius(2)
                .margin({ left: 4 })
              Text(item.battery + '%')
                .fontSize(9)
                .fontColor('#B5A08F')
                .margin({ left: 4 })
            }
            .margin({ top: 5 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })

          Text('¥' + item.price)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#F4511E')
        }
        .width('100%')
        .padding(11)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .margin({ left: 12, right: 12, top: 6 })
      }, (item: RadioItem) => item.name)

      Column()
        .height(20)
    }
    .width('100%')
  }

  // Tab5 飞场活动:报名进度
  @Builder
  EventTab() {
    Column() {
      Row() {
        Text('🪂 飞场活动')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')
        Column()
          .layoutWeight(1)
        Text('发布活动')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .padding({ left: 12, right: 12, top: 4, bottom: 4 })
          .backgroundColor('#F4511E')
          .borderRadius(12)
          .onClick(() => {
            this.showEvent = true
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14 })

      ForEach(this.eventList, (item: EventItem) => {
        Column() {
          Row() {
            Column() {
              Text('🪂')
                .fontSize(28)
            }
            .width(58)
            .height(58)
            .justifyContent(FlexAlign.Center)
            .backgroundColor('#FFEBE0')
            .borderRadius(10)

            Column() {
              Text(item.name)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor('#BF360C')
                .maxLines(1)
              Row() {
                Text(item.date)
                  .fontSize(9)
                  .fontColor('#FFFFFF')
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                  .backgroundColor('#42A5F5')
                  .borderRadius(3)
                Text('📍 ' + item.field)
                  .fontSize(10)
                  .fontColor('#B5A08F')
                  .margin({ left: 6 })
              }
              .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            .margin({ left: 10 })

            Text('报名')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .backgroundColor('#FFD600')
              .borderRadius(14)
              .onClick(() => {
                this.showEvent = true
              })
          }
          .width('100%')

          Row() {
            Text('已报 ' + item.joined + '/' + item.total)
              .fontSize(10)
              .fontColor('#B5A08F')
            Stack({ alignContent: Alignment.Start }) {
              Column()
                .width(130)
                .height(5)
                .backgroundColor('#FFEBE0')
                .borderRadius(3)
              Column()
                .width(item.joined / item.total * 130)
                .height(5)
                .backgroundColor('#F4511E')
                .borderRadius(3)
            }
            .width(130)
            .height(5)
            .margin({ left: 8 })
            Text(item.joined / item.total > 0.8 ? '即将满员' : '报名中')
              .fontSize(11)
              .fontWeight(FontWeight.Bold)
              .fontColor('#F4511E')
              .margin({ left: 8 })
              .scale({ x: 1.04, y: 1.04 })
              .animation({ duration: 900, iterations: -1, curve: Curve.EaseInOut })
            Column()
              .layoutWeight(1)
          }
          .width('100%')
          .margin({ top: 10 })
        }
        .width('100%')
        .padding(11)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .margin({ left: 12, right: 12, top: 6 })
      }, (item: EventItem) => item.name)

      Column()
        .height(20)
    }
    .width('100%')
  }

  // Tab6 消息
  @Builder
  NoteTab() {
    Column() {
      Text('消息')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#BF360C')
        .margin({ top: 14, left: 16 })

      ForEach(this.noteList, (item: NoteItem) => {
        Row() {
          Column() {
            Text(this.noteIcon(item.type))
              .fontSize(22)
          }
          .width(46)
          .height(46)
          .justifyContent(FlexAlign.Center)
          .backgroundColor(item.type === '官方' ? '#E3F2FD' : '#FFEBE0')
          .borderRadius(23)

          Column() {
            Row() {
              Text(item.name)
                .fontSize(13)
                .fontWeight(FontWeight.Medium)
                .fontColor('#BF360C')
                .maxLines(1)
              Column()
                .layoutWeight(1)
              Text(item.time)
                .fontSize(10)
                .fontColor('#CDB2A8')
            }
            .width('100%')
            Row() {
              Text(item.content)
                .fontSize(11)
                .fontColor('#B5A08F')
                .maxLines(1)
                .layoutWeight(1)
              if (item.unread > 0) {
                Text(item.unread + '')
                  .fontSize(9)
                  .fontColor('#FFFFFF')
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                  .backgroundColor('#F4511E')
                  .borderRadius(8)
                  .margin({ left: 6 })
              }
            }
            .width('100%')
            .margin({ top: 3 })
          }
          .layoutWeight(1)
          .margin({ left: 10 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .margin({ left: 12, right: 12, top: 5 })
        .onClick(() => {
          this.showDelete = true
        })
      }, (item: NoteItem) => item.name)

      Column()
        .height(20)
    }
    .width('100%')
  }

  private noteIcon(type: string): string {
    if (type === '官方') {
      return '📢'
    }
    if (type === '商家') {
      return '🏪'
    }
    return '💬'
  }

  // Tab7 我的
  @Builder
  MineTab() {
    Column() {
      Row() {
        Column() {
          Text('🛩️')
            .fontSize(36)
        }
        .width(64)
        .height(64)
        .justifyContent(FlexAlign.Center)
        .backgroundColor('#FFCCBC')
        .borderRadius(32)

        Column() {
          Text('云端飞手')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('飞行 5 年 · AOPA 持证 · 信用极好')
            .fontSize(11)
            .fontColor('#FFE0CC')
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .margin({ left: 12 })
      }
      .width('100%')
      .padding(16)
      .linearGradient({
        direction: GradientDirection.Right,
        colors: [['#F4511E', 0], ['#42A5F5', 1]]
      })
      .borderRadius(14)
      .margin({ top: 12, left: 12, right: 12 })

      Row() {
        Column() {
          Text('14')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#BF360C')
          Text('在售')
            .fontSize(10)
            .fontColor('#B5A08F')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('62')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#BF360C')
          Text('卖出')
            .fontSize(10)
            .fontColor('#B5A08F')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('5.0')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#BF360C')
          Text('评分')
            .fontSize(10)
            .fontColor('#B5A08F')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('86 飞')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#F4511E')
          Text('本年累计')
            .fontSize(10)
            .fontColor('#B5A08F')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 10, left: 12, right: 12 })

      Text('我的服务')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#BF360C')
        .margin({ top: 16, left: 16 })

      Column() {
        Row() {
          Text('🪂')
            .fontSize(18)
          Text('飞场活动报名')
            .fontSize(13)
            .fontColor('#BF360C')
            .margin({ left: 10 })
          Column()
            .layoutWeight(1)
          Text('本周 2 场')
            .fontSize(11)
            .fontColor('#2E7D32')
            .margin({ right: 6 })
          Text('>')
            .fontSize(14)
            .fontColor('#CDB2A8')
        }
        .padding(14)
        .onClick(() => {
          this.showEvent = true
        })
        Row() {
          Text('🚀')
            .fontSize(18)
          Text('航模租飞记录')
            .fontSize(13)
            .fontColor('#BF360C')
            .margin({ left: 10 })
          Column()
            .layoutWeight(1)
          Text('2 次租飞')
            .fontSize(11)
            .fontColor('#1565C0')
            .margin({ right: 6 })
          Text('>')
            .fontSize(14)
            .fontColor('#CDB2A8')
        }
        .padding(14)
        .onClick(() => {
          this.showRent = true
        })
        Row() {
          Text('💰')
            .fontSize(18)
          Text('我的钱包')
            .fontSize(13)
            .fontColor('#BF360C')
            .margin({ left: 10 })
          Column()
            .layoutWeight(1)
          Text('¥760.00')
            .fontSize(12)
            .fontColor('#F4511E')
            .margin({ right: 6 })
          Text('>')
            .fontSize(14)
            .fontColor('#CDB2A8')
        }
        .padding(14)
        Row() {
          Text('📦')
            .fontSize(18)
          Text('我的订单')
            .fontSize(13)
            .fontColor('#BF360C')
            .margin({ left: 10 })
          Column()
            .layoutWeight(1)
          Text('2 待发货')
            .fontSize(11)
            .fontColor('#EF6C00')
            .margin({ right: 6 })
          Text('>')
            .fontSize(14)
            .fontColor('#CDB2A8')
        }
        .padding(14)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 8, left: 12, right: 12 })

      Column()
        .height(20)
    }
    .width('100%')
  }

  // 弹框1:发布航模(底部)
  @Builder
  PublishDialog() {
    Column() {
      Column() {
        Row() {
          Text('发布闲置航模')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#BF360C')
          Column()
            .layoutWeight(1)
          Text('✕')
            .fontSize(16)
            .fontColor('#9E9E9E')
            .onClick(() => {
              this.showPublish = false
            })
        }
        .width('100%')

        Text('装备类别')
          .fontSize(12)
          .fontColor('#B5A08F')
          .margin({ top: 14 })
        Row() {
          ForEach(this.itemKinds, (k: string, index: number) => {
            Text(k)
              .fontSize(11)
              .fontColor(this.itemKind === index ? '#FFFFFF' : '#F4511E')
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .backgroundColor(this.itemKind === index ? '#F4511E' : '#FFEBE0')
              .borderRadius(13)
              .margin({ right: 8 })
              .onClick(() => {
                this.itemKind = index
              })
          }, (k: string) => k)
        }
        .margin({ top: 8 })

        Text('期望售价(¥)')
          .fontSize(12)
          .fontColor('#B5A08F')
          .margin({ top: 14 })
        Text(this.priceField === '' ? '输入价格,如 980' : '¥ ' + this.priceField)
          .fontSize(14)
          .fontColor(this.priceField === '' ? '#CDB2A8' : '#BF360C')
          .padding(12)
          .backgroundColor('#FFEBE0')
          .borderRadius(10)
          .width('100%')
          .margin({ top: 8 })
          .onClick(() => {
            this.priceField = '980'
          })

        Text('整机免费调机试飞 · 锂电池需合规寄运')
          .fontSize(10)
          .fontColor('#B5A08F')
          .margin({ top: 10 })

        Text('立即发布')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#3E2723')
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#FFD600')
          .borderRadius(24)
          .margin({ top: 16 })
          .onClick(() => {
            this.showPublish = false
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 20, topRight: 20 })
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.End)
    .backgroundColor('#66000000')
    .onClick(() => {
      this.showPublish = false
    })
  }

  // 弹框2:航模租飞(天数 + 价格脉冲)
  @Builder
  RentDialog() {
    Column() {
      Column() {
        Text('🚀 航模租飞')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')

        Text('选择租飞天数')
          .fontSize(12)
          .fontColor('#B5A08F')
          .margin({ top: 14 })
          .width('100%')

        Row() {
          ForEach([1, 2, 3, 7, 15], (d: number) => {
            Text(d + ' 天')
              .fontSize(11)
              .fontColor(this.rentDays === d ? '#FFFFFF' : '#1565C0')
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .backgroundColor(this.rentDays === d ? '#42A5F5' : '#E3F2FD')
              .borderRadius(14)
              .margin({ right: 8 })
              .onClick(() => {
                this.rentDays = d
              })
          }, (d: number) => d + '')
        }
        .margin({ top: 8 })

        Row() {
          Column() {
            Row() {
              Text('预估费用:')
                .fontSize(12)
                .fontColor('#8D6E63')
              Text('¥ ' + (this.rentDays * 88))
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#F4511E')
                .scale({ x: 1.06, y: 1.06 })
                .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
            }
          }
          .alignItems(HorizontalAlign.Start)
        }
        .margin({ top: 12 })

        Column() {
          Text('含炸机险 · 教练带飞一次 · 归还免费检修')
            .fontSize(10)
            .fontColor('#B5A08F')
            .margin({ top: 4 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#E3F2FD')
        .borderRadius(10)
        .margin({ top: 12 })

        Row() {
          Text('预约租飞')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#3E2723')
            .padding({ left: 22, right: 22, top: 10, bottom: 10 })
            .backgroundColor('#FFD600')
            .borderRadius(20)
            .onClick(() => {
              this.showRent = false
            })
          Text('取消')
            .fontSize(14)
            .fontColor('#B5A08F')
            .padding({ left: 22, right: 22, top: 10, bottom: 10 })
            .backgroundColor('#FFEBE0')
            .borderRadius(20)
            .margin({ left: 12 })
            .onClick(() => {
              this.showRent = false
            })
        }
        .margin({ top: 18 })
      }
      .width('86%')
      .padding(20)
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
    .onClick(() => {
      this.showRent = false
    })
  }

  // 弹框3:飞场报名(场地选择)
  @Builder
  EventDialog() {
    Column() {
      Column() {
        Text('🪂 飞场活动报名')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')

        Text('选择常飞场地')
          .fontSize(12)
          .fontColor('#B5A08F')
          .margin({ top: 14 })
          .width('100%')

        Row() {
          ForEach(this.eventFields, (f: string, index: number) => {
            Text(f)
              .fontSize(10)
              .fontColor(this.eventField === index ? '#FFFFFF' : '#1565C0')
              .padding({ left: 8, right: 8, top: 5, bottom: 5 })
              .backgroundColor(this.eventField === index ? '#42A5F5' : '#E3F2FD')
              .borderRadius(12)
              .margin({ right: 6, top: 6 })
              .onClick(() => {
                this.eventField = index
              })
          }, (f: string) => f)
        }
        .width('100%')

        Column() {
          Row() {
            Text('常飞场地:' + this.eventFields[this.eventField])
              .fontSize(12)
              .fontColor('#BF360C')
            Column()
              .layoutWeight(1)
            Text('送调机券')
              .fontSize(11)
              .fontWeight(FontWeight.Bold)
              .fontColor('#F4511E')
          }
          .width('100%')
          Text('到场签到即送 50 元调机抵扣券,可叠加使用')
            .fontSize(10)
            .fontColor('#B5A08F')
            .margin({ top: 6 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#F4FAFF')
        .borderRadius(10)
        .margin({ top: 14 })

        Row() {
          Text('立即报名')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .padding({ left: 22, right: 22, top: 10, bottom: 10 })
            .backgroundColor('#42A5F5')
            .borderRadius(20)
            .onClick(() => {
              this.showEvent = false
            })
          Text('再想想')
            .fontSize(14)
            .fontColor('#B5A08F')
            .padding({ left: 22, right: 22, top: 10, bottom: 10 })
            .backgroundColor('#E3F2FD')
            .borderRadius(20)
            .margin({ left: 12 })
            .onClick(() => {
              this.showEvent = false
            })
        }
        .margin({ top: 18 })
      }
      .width('86%')
      .padding(20)
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
    .onClick(() => {
      this.showEvent = false
    })
  }

  // 弹框4:删除确认(警示小卡)
  @Builder
  DeleteDialog() {
    Column() {
      Column() {
        Text('🛸')
          .fontSize(36)
          .margin({ top: 8 })
        Text('删除该会话?')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')
          .margin({ top: 10 })
        Text('删除后聊天记录不可恢复哦')
          .fontSize(12)
          .fontColor('#B5A08F')
          .margin({ top: 6 })

        Row() {
          Text('取消')
            .fontSize(14)
            .fontColor('#B5A08F')
            .padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .backgroundColor('#FFEBE0')
            .borderRadius(18)
            .onClick(() => {
              this.showDelete = false
            })
          Text('删除')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .backgroundColor('#F4511E')
            .borderRadius(18)
            .margin({ left: 12 })
            .onClick(() => {
              this.showDelete = false
            })
        }
        .margin({ top: 16 })
      }
      .width('70%')
      .padding(20)
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#66000000')
    .onClick(() => {
      this.showDelete = false
    })
  }

  // 弹框5:航模详情(右侧滑出)
  @Builder
  DetailDialog() {
    Column() {
      Column() {
        Row() {
          Text('航模详情')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#BF360C')
          Column()
            .layoutWeight(1)
          Text('✕')
            .fontSize(16)
            .fontColor('#9E9E9E')
            .onClick(() => {
              this.showDetail = false
            })
        }
        .width('100%')

        Column() {
          Text('✈️')
            .fontSize(54)
        }
        .width('100%')
        .height(120)
        .justifyContent(FlexAlign.Center)
        .backgroundColor('#E3F2FD')
        .borderRadius(12)
        .margin({ top: 14 })

        Text(this.selName())
          .fontSize(17)
          .fontWeight(FontWeight.Bold)
          .fontColor('#BF360C')
          .margin({ top: 12 })
          .width('100%')

        Row() {
          Text(this.selBrand())
            .fontSize(11)
            .fontColor('#F4511E')
            .padding({ left: 8, right: 8, top: 3, bottom: 3 })
            .backgroundColor('#FFCCBC')
            .borderRadius(4)
          Text('· ' + this.selCity())
            .fontSize(11)
            .fontColor('#B5A08F')
        }
        .margin({ top: 8 })

        Row() {
          Text('¥' + this.selPrice())
            .fontSize(26)
            .fontWeight(FontWeight.Bold)
            .fontColor('#F4511E')
          Text('原价 ¥' + this.selOrigin())
            .fontSize(12)
            .fontColor('#CDB2A8')
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 10 })
        }
        .margin({ top: 12 })

        Column() {
          Row() {
            Text('机况完好度')
              .fontSize(12)
              .fontColor('#B5A08F')
            Column()
              .layoutWeight(1)
            Text((100 - this.selFlights()) + '%')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor(this.selFlights() < 20 ? '#2E7D32' : '#EF6C00')
          }
          .width('100%')
          Stack({ alignContent: Alignment.Start }) {
            Column()
              .width('100%')
              .height(6)
              .backgroundColor('#FFEBE0')
              .borderRadius(3)
            Column()
              .width((100 - this.selFlights()) + '%')
              .height(6)
              .backgroundColor(this.selFlights() < 20 ? '#66BB6A' : '#FFB74D')
              .borderRadius(3)
          }
          .width('100%')
          .height(6)
          .margin({ top: 6 })
          Text('共 ' + this.selFlights() + ' 次飞行 · 平台调机测试后发货')
            .fontSize(9)
            .fontColor('#B5A08F')
            .margin({ top: 4 })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#F4FAFF')
        .borderRadius(10)
        .margin({ top: 14 })

        Row() {
          Text('租 3 天')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1565C0')
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .backgroundColor('#FFFFFF')
            .border({ width: 1, color: '#42A5F5', radius: 20 })
            .onClick(() => {
              this.showDetail = false
              this.showRent = true
            })
          Text('聊卖家')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .backgroundColor('#F4511E')
            .borderRadius(20)
            .margin({ left: 12 })
            .onClick(() => {
              this.showDetail = false
            })
        }
        .margin({ top: 16 })
      }
      .width('88%')
      .height('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 20, bottomLeft: 20 })
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.End)
    .backgroundColor('#66000000')
    .onClick(() => {
      this.showDetail = false
    })
  }
}


总结

在这里插入图片描述

本文深入解析了一个基于HarmonyOS ArkTS声明式UI范式开发的二手航模装备交易平台"翼上循环"。该应用以单一@Entry @Component组件为核心,通过十个@State状态变量驱动全部交互逻辑,七个@Builder方法构建七个功能Tab页面,五个@Builder方法实现五种弹窗交互。整体架构遵循"数据模型-视图渲染-逻辑计算"三层分离设计原则。六个装备接口类型完整覆盖了固定翼、遥控车、遥控器、飞场活动等航模全品类数据需求。飞行橙白配色方案贯穿头部、Tab导航、列表卡片和弹窗的每一个细节,营造出与航模飞行主题高度契合的视觉氛围。

在数据可视化方面,本应用展现了ArkTS原生组件的强大布局能力。通过Column高度按比例计算实现了翼展、车速、通道数三种柱状图;通过Stack叠放实现了机况完好度、电池电量、报名进度三种进度条。这些可视化组件完全基于原生ArkTS构建,无需任何第三方图表库依赖。特别值得一提的是,遥控车Tab采用了独特的双列网格布局(flexWrap(FlexWrap.Wrap)+47%宽度),与其他Tab的单列列表形成差异化对比,体现了针对不同品类特点定制交互布局的设计理念。飞场活动Tab中的报名进度条配合"即将满员"脉冲动画,巧妙地创造了报名紧迫感。

在弹窗交互设计方面,五个弹窗各具特色:发布弹窗底部滑出提供类别选择和价格输入、租飞弹窗居中显示附带费用脉冲动画、飞场报名弹窗使用标签自动换行选择场地、删除确认弹窗为警示小卡、详情弹窗右侧滑出展示机况进度条。弹窗间通过状态变量联动实现跳转(如详情弹窗"租3天"按钮跳转到租飞弹窗),通过stopPropagation事件控制实现"点击遮罩关闭、点击内容保留"的标准交互模式。这些设计模式为HarmonyOS应用开发中的多弹窗管理提供了完整的参考范式,充分展示了ArkTS声明式UI在垂直电商应用开发中的技术潜力和工程价值。

Logo

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

更多推荐