云养蜂场将传统蜂业与移动互联网直播深度融合,蜂农在线摇蜜、用户实时围观、云认养蜂巢全年配送,构建了一条从蜂箱到餐桌的透明信任链。声明式UI范式让每一罐蜂蜜的溯源路径都可视化、可交互。

在HarmonyOS ArkTS的声明式开发模型中,状态管理是驱动数据流的核心引擎。@State装饰器管理组件内部状态,@Prop实现父子单向数据传递,@Builder抽取可复用UI片段——三者的协同配合构成了一个完整的响应式渲染管线。

本篇将以蜂颂云养蜂场为案例,剖析底部蜂巢格Tab导航、bindSheet抽屉弹窗、bindContentCover居中对话框、柱状图数据可视化、堆叠条占比图等关键技术的工程实现,展示HarmonyOS在垂直领域应用开发中的完整能力栈。

一、引言

在这里插入图片描述

随着乡村振兴战略的深入推进,传统养蜂业正在经历一场数字化变革。蜂农分散在全国各地的山区田野,蜂蜜从蜂箱到消费者手中需要经过摇蜜、过滤、装罐、冷链运输等多重环节,信息不对称导致消费者对蜂蜜品质的信任度不足。蜂颂云养蜂场正是基于这一痛点,将直播连线取蜜、云认养蜂巢、蜂群档案管理、蜜源图鉴科普等功能整合到一个HarmonyOS原生应用中,打造了一个连接蜂农与消费者的信任平台。

从技术架构角度审视,该应用采用了典型的单页面多Tab架构,通过@Entry/@Component装饰器构建入口组件,内部以条件渲染if-else切换六个业务Tab页面。状态管理层面,主组件Index209持有全部业务数据(蜂巢列表、蜜源数据、采收记录、蜂农信息等)作为@State状态变量,通过@Prop向下传递给各子Tab组件,实现了数据的单向数据流。弹窗交互层面,应用使用了三种不同的弹出方式:bindSheet用于底部抽屉式表单(预约取蜜、新增观测、编辑档案),bindContentCover用于居中全屏遮罩对话框(删除确认、蜂巢详情),@Builder方法封装了所有弹窗的UI内容。

从业务设计层面来看,应用围绕"取蜜房、蜂巢箱、蜜源志、工具架、蜂农团、我的"六大功能模块展开。取蜜房是核心直播间,集成了四机位多画面布局、取蜜六步法进度跟踪、围蜜弹幕实时互动;蜂巢箱管理蜂箱档案,支持新增观测、编辑、删除、详情查看和云认养切换;蜜源志展示五种蜜源花期的图鉴与热度横条;工具架管理蜂具库存与箱温监测;蜂农团展示蜂农人气排行榜;我的页面汇总个人收蜜数据与设置开关。整个应用的配色方案采用蜂蜡黄(#FFB300)、蜂巢褐(#6D4C41)和蜜光白(#FFF8E1)三色体系,视觉风格温暖质朴,契合养蜂行业的自然属性。

二、数据接口与工具函数

在这里插入图片描述

2.1 数据模型定义

应用首先定义了一系列interface来约束数据结构,这是ArkTS强类型特性的体现。每个接口对应一个业务实体,字段类型明确、语义清晰。

interface HoneyDay209 {
  day: string
  jars: number
}

interface Hive209 {
  id: number
  code: string
  nectar: string
  bees: number
  yield: number
  state: string
  watchers: number
  adopted: boolean
}

interface Nectar209 {
  id: number
  name: string
  color: string
  bloom: string
  sweetness: number
  heat: number
}

interface Harvest209 {
  id: number
  name: string
  nectar: string
  dayNum: number
  frames: number
  jars: number
  state: string
  top: boolean
}

interface Keeper209 {
  id: number
  name: string
  city: string
  years: number
  online: boolean
  heat: number
}

上述接口定义覆盖了蜂蜜产量(HoneyDay209)、蜂箱信息(Hive209)、蜜源品种(Nectar209)、采收记录(Harvest209)和蜂农资料(Keeper209)等核心业务实体。Hive209接口中的state字段使用字符串枚举(“摇蜜中”、“割蜜盖”、“蜜已封盖”、“产卵扩群”)来描述蜂箱当前状态,adopted布尔值标记是否已被云认养。Nectar209接口同时携带color和heat字段,前者用于UI配色,后者用于热度横条图的宽度计算。这种将展示属性与业务属性合并到同一接口中的做法,在小型应用中可以有效减少数据转换的中间环节。

2.2 状态颜色映射工具函数

在这里插入图片描述

应用定义了一组工具函数,根据业务状态返回对应的颜色值,实现UI配色的动态映射。

function hiveStateColor209(state: string): string {
  if (state === '摇蜜中') {
    return '#FF8F00'
  }
  if (state === '割蜜盖') {
    return '#F4511E'
  }
  if (state === '蜜已封盖') {
    return '#7CB342'
  }
  return '#90A4AE'
}

function harvestStateColor209(state: string): string {
  if (state === '今日摇蜜') {
    return '#F4511E'
  }
  if (state === '等待封盖') {
    return '#FBC02D'
  }
  if (state === '已装罐') {
    return '#7CB342'
  }
  return '#90A4AE'
}

function nectarColor209(nectar: string): string {
  if (nectar === '洋槐') {
    return '#FFE082'
  }
  if (nectar === '油菜') {
    return '#FDD835'
  }
  if (nectar === '荆条') {
    return '#A1887F'
  }
  if (nectar === '荔枝') {
    return '#FB8C00'
  }
  return '#C0A16B'
}

这三个工具函数采用相同的模式:接收一个业务状态字符串,通过if-else条件判断返回对应的十六进制颜色值。hiveStateColor209将蜂箱状态映射为四种颜色——摇蜜中对应橙黄(#FF8F00)表示活跃、割蜜盖对应深橙(#F4511E)表示高风险操作、蜜已封盖对应绿色(#7CB342)表示完成、默认返回蓝灰(#90A4AE)表示闲置。harvestStateColor209和nectarColor209遵循同样的设计理念。这种将颜色逻辑从UI组件中抽离到独立函数的做法,提高了代码的可维护性——当需要调整配色方案时,只需修改函数实现而无需逐个搜索UI代码中的硬编码颜色值。

2.3 聚合统计工具函数

在这里插入图片描述

除了颜色映射,应用还定义了一组数据聚合函数,用于统计蜂箱总数、认养数、摇蜜数、在线蜂农数等关键指标。

function adoptedHiveCount209(hives: Hive209[]): number {
  let n: number = 0
  for (let i = 0; i < hives.length; i++) {
    if (hives[i].adopted) {
      n++
    }
  }
  return n
}

function spinningHiveCount209(hives: Hive209[]): number {
  let n: number = 0
  for (let i = 0; i < hives.length; i++) {
    if (hives[i].state === '摇蜜中' || hives[i].state === '割蜜盖') {
      n++
    }
  }
  return n
}

function onlineKeeperCount209(keepers: Keeper209[]): number {
  let n: number = 0
  for (let i = 0; i < keepers.length; i++) {
    if (keepers[i].online) {
      n++
    }
  }
  return n
}

function maxKeeperHeat209(keepers: Keeper210[]): number {
  let m: number = 1
  for (let i = 0; i < keepers.length; i++) {
    if (keepers[i].heat > m) {
      m = keepers[i].heat
    }
  }
  return m
}

这些聚合函数遍历数组并根据条件计数或求最大值。adoptedHiveCount209统计已认养蜂箱数量,spinningHiveCount209统计正在摇蜜或割蜜盖的活跃蜂箱数,onlineKeeperCount209统计在线蜂农人数,maxKeeperHeat209获取蜂农中的最高人气值(用于后续计算人气横条比例)。这些函数在UI渲染时被直接调用,返回值嵌入到Text组件的内容中,实现了数据的实时聚合与展示。

在ArkTS中,工具函数定义在组件外部作为全局函数存在,可以在任意组件的build方法中直接调用。这种设计使得数据计算逻辑与UI渲染逻辑保持分离,符合单一职责原则。

三、主页面架构与状态管理

3.1 入口组件状态定义

在这里插入图片描述

主页面Index209是整个应用的入口组件,承载了全部状态管理和路由切换逻辑。

@Entry
@Component
struct Index209 {
  @State tabIndex1: number = 0

  // 弹框开关
  @State showJoinSheet: boolean = false
  @State showObserveSheet: boolean = false
  @State showEditSheet: boolean = false
  @State showDelDialog: boolean = false
  @State showDetailDialog: boolean = false

  // 预约连线取蜜表单
  @State joinNectar: number = 0
  @State joinJars: number = 4
  @State joinSmoke: number = 0
  @State joinLive: boolean = true
  @State joinAdopt: boolean = false

  // 新增观测表单
  @State obsCode: string = ''
  @State obsNectar: number = 0
  @State obsBees: number = 4
  @State obsYield: number = 30
  @State obsPublic: boolean = true

  // 编辑表单
  @State editIndex: number = -1
  @State editCode: string = ''
  @State editBees: number = 4
  @State editYield: number = 30
  @State editTop: boolean = false

  // 删除
  @State delIndex: number = -1
  @State delKeepLog: boolean = true

  // 详情
  @State detailIndex: number = 0

状态变量分为四组:Tab索引(tabIndex1)控制页面切换;五个布尔值(showJoinSheet等)控制弹窗显隐;预约取蜜表单(joinNectar/joinJars/joinSmoke/joinLive/joinAdopt)记录用户在预约抽屉中的选择;新增观测表单(obsCode/obsNectar/obsBees/obsYield/obsPublic)记录新增蜂箱观测的输入;编辑表单(editIndex/editCode/editBees/editYield/editTop)记录编辑蜂巢档案的临时数据;删除相关(delIndex/delKeepLog)管理删除确认对话框的状态;详情索引(detailIndex)指定当前查看详情的蜂箱下标。

这种将所有状态集中在入口组件的设计模式,在HarmonyOS中被称为"状态提升"。子组件通过@Prop接收只读数据,通过回调函数向父组件传递变更意图,父组件统一处理状态更新后再将新数据向下分发。这种模式确保了数据流的单向性和可预测性。

3.2 核心业务数据初始化

入口组件持有全部业务数据,在@State初始化时赋予默认值。

  @State hives: Hive209[] = [
    { id: 1, code: 'A-07', nectar: '洋槐', bees: 5, yield: 42, state: '摇蜜中', watchers: 3050, adopted: true },
    { id: 2, code: 'A-12', nectar: '油菜', bees: 4, yield: 35, state: '蜜已封盖', watchers: 1420, adopted: true },
    { id: 3, code: 'B-03', nectar: '荆条', bees: 6, yield: 48, state: '割蜜盖', watchers: 2680, adopted: false },
    { id: 4, code: 'B-08', nectar: '荔枝', bees: 5, yield: 38, state: '摇蜜中', watchers: 3340, adopted: true },
    { id: 5, code: 'C-01', nectar: '椴树', bees: 6, yield: 52, state: '蜜已封盖', watchers: 2210, adopted: false },
    { id: 6, code: 'C-05', nectar: '洋槐', bees: 3, yield: 28, state: '产卵扩群', watchers: 880, adopted: false },
    { id: 7, code: 'D-02', nectar: '荆条', bees: 5, yield: 40, state: '摇蜜中', watchers: 1980, adopted: true },
    { id: 8, code: 'D-09', nectar: '油菜', bees: 4, yield: 33, state: '产卵扩群', watchers: 760, adopted: false },
    { id: 9, code: 'E-04', nectar: '椴树', bees: 6, yield: 55, state: '蜜已封盖', watchers: 4160, adopted: true },
    { id: 10, code: 'E-11', nectar: '荔枝', bees: 4, yield: 30, state: '产卵扩群', watchers: 1020, adopted: false }
  ]

  @State nectars: Nectar209[] = [
    { id: 1, name: '洋槐', color: '#FFE082', bloom: '4-5 月', sweetness: 92, heat: 95 },
    { id: 2, name: '油菜', color: '#FDD835', bloom: '3-4 月', sweetness: 84, heat: 82 },
    { id: 3, name: '荆条', color: '#A1887F', bloom: '6-7 月', sweetness: 88, heat: 86 },
    { id: 4, name: '荔枝', color: '#FB8C00', bloom: '2-3 月', sweetness: 94, heat: 91 },
    { id: 5, name: '椴树', color: '#C0A16B', bloom: '6-7 月', sweetness: 90, heat: 89 }
  ]

hives数组是整个应用最核心的数据源,包含10个蜂箱的完整档案:编号(code)、蜜源品种(nectar)、蜂群脾数(bees)、预估产蜜量(yield)、当前状态(state)、围览权重(watchers)和认养标记(adopted)。nectars数组定义了5种蜜源的花期、甜度和热度值,其中color字段直接被用于UI配色——蜜源图鉴卡片的背景色和堆叠条的段色都取自此字段。数据结构设计上,每个蜂箱的code字段采用"A-07"这样的"区域-序号"编码方式,便于蜂农在实际管理中快速定位。

3.3 头部Builder与蜂巢格Tab导航

头部区域使用@Builder装饰器封装为可复用的UI片段,蜂巢格Tab导航则通过ForEach循环渲染六个蜂巢形按钮。

  @Builder
  header209() {
    Column({ space: 12 }) {
      Row({ space: 10 }) {
        Column({ space: 4 }) {
          Text('蜂颂 · 云养蜂场').fontSize(19).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('蜂农连线取蜜 · 云认养蜂巢直播围观').fontSize(11).fontColor('#FFE082')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column({ space: 2 }) {
          Text('🐝').fontSize(20)
          Text('2,340').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('围摇蜜位').fontSize(9).fontColor('#FFE082')
        }
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')

      Row({ space: 10 }) {
        Column().width(4).height(34).borderRadius(2).backgroundColor('#FF8F00')
        Column({ space: 3 }) {
          Text('洋槐头茬蜜今日开摇').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('连线摇蜜抽头罐鲜蜜 · 认养享全年配送').fontSize(10).fontColor('#FFF8E1')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column() {
          Text('去摇蜜 →').fontSize(11).fontColor('#6D4C41').fontWeight(FontWeight.Bold)
        }
        .padding({ left: 12, right: 12, top: 7, bottom: 7 })
        .borderRadius(14)
        .backgroundColor('#FFF8E1')
        .onClick(() => {
          this.showJoinSheet = true
        })
      }
      .width('100%')
      .padding(12)
      .borderRadius(12)
      .backgroundColor('#5D4037')
    }
    .alignItems(HorizontalAlign.Start)
    .padding(14)
    .linearGradient({ angle: 140, colors: [['#FFB300', 0], ['#FF8F00', 1]] })
  }

头部由两部分组成:上方是应用名称和围摇蜜位数的展示行,右侧用蜜蜂emoji和数字呈现当前直播围观人数;下方是促销卡片,左侧用4px宽的竖条作为视觉引导,中间展示"洋槐头茬蜜今日开摇"的促销文案,右侧是蜜光白背景的"去摇蜜"按钮。整个头部使用linearGradient实现从蜂蜡黄(#FFB300)到深橙(#FF8F00)的140度线性渐变,营造温暖的蜜季氛围。点击"去摇蜜"按钮会触发this.showJoinSheet = true,通过状态绑定弹出预约取蜜的底部抽屉。

@Builder装饰器是ArkTS中封装UI片段的重要手段。与@Component不同,@Builder方法不创建独立的组件实例,而是在调用处内联展开。这意味着@Builder内的this指向当前组件实例,可以直接访问组件的状态变量和调用组件的方法。

3.4 蜂巢格Tab导航实现

在这里插入图片描述

底部Tab导航采用蜂巢六边形的拟物化设计,上窄下宽的两段式布局模拟蜂巢格的视觉效果。

  @Builder
  tabBar209() {
    Column({ space: 0 }) {
      Row({ space: 4 }) {
        ForEach(this.tabs209, (t: string, i: number) => {
          Column({ space: 0 }) {
            // 蜂巢上格(窄)
            Row({ space: 4 }) {
              Text(this.tabIcons209[i]).fontSize(10)
              Text(this.tabIndex1 === i ? '🐝' : '').fontSize(8)
            }
            .width('62%')
            .height(16)
            .justifyContent(FlexAlign.Center)
            .borderRadius({ topLeft: 8, topRight: 8 })
            .backgroundColor(this.tabIndex1 === i ? '#FFD54F' : '#D7CCC8')

            // 蜂巢下格(宽)
            Column({ space: 1 }) {
              Text(t).fontSize(9).fontColor(this.tabIndex1 === i ? '#4E342E' : '#FFFFFF').maxLines(1)
              Text(this.tabIndex1 === i ? '❋' : '·').fontSize(7).fontColor(this.tabIndex1 === i ? '#FF8F00' : '#A1887F')
            }
            .width('100%')
            .height(30)
            .alignItems(HorizontalAlign.Center)
            .justifyContent(FlexAlign.Center)
            .borderRadius({ bottomLeft: 10, bottomRight: 10 })
            .backgroundColor(this.tabIndex1 === i ? '#FFB300' : '#6D4C41')
          }
          .layoutWeight(1)
          .padding({ top: 2, bottom: 2 })
          .scale({ x: this.tabIndex1 === i ? 1.07 : 1, y: this.tabIndex1 === i ? 1.07 : 1 })
          .animation({ duration: 180 })
          .onClick(() => {
            this.tabIndex1 = i
          })
        }, (t: string) => t)
      }
      .width('100%')
      .padding({ left: 8, right: 8, top: 4 })
      .alignItems(VerticalAlign.Top)
    }
    .width('100%')
    .padding({ top: 2, bottom: 6 })
    .backgroundColor('#4E342E')
    .shadow({ radius: 10, color: 'rgba(0,0,0,0.25)', offsetY: -3 })
  }

每个Tab按钮由上下两部分组成:上格为窄行(width 62%, height 16),展示图标和选中状态下的蜜蜂emoji,背景色在选中时为浅蜜黄(#FFD54F)、未选中为浅褐(#D7CCC8);下格为宽行(width 100%, height 30),展示Tab文字和状态标记符号。选中状态下,整个按钮通过scale属性放大1.07倍并添加180ms的animation过渡动画,下格背景变为蜂蜡黄(#FFB300),文字颜色变深。底部导航容器整体使用蜂巢褐(#4E342E)作为背景,配合向上偏移3px的阴影模拟悬浮效果。六个Tab分别是:取蜜房、蜂巢箱、蜜源志、工具架、蜂农团、我的,对应emoji图标依次为🍯🐝🌸🧰🧑‍🌾👤。

3.5 build方法与条件路由

在这里插入图片描述

主组件的build方法通过if-else条件判断,根据tabIndex1的值渲染对应的Tab组件,同时绑定所有弹窗。

  build() {
    Column() {
      this.header209()
      Scroll() {
        Column({ space: 12 }) {
          if (this.tabIndex1 === 0) {
            LiveTab209({
              hives: this.hives,
              keepers: this.keepers,
              harvestSteps: this.harvestSteps,
              barrages: this.barrages,
              onStep: (i: number) => {
                this.harvestSteps = this.harvestSteps.map((s: HarvestStep209, si: number) => {
                  if (si === i) {
                    return { id: s.id, title: s.title, tip: s.tip, done: !s.done }
                  }
                  return s
                })
              },
              onJoin: () => {
                this.showJoinSheet = true
              }
            })
          }
          if (this.tabIndex1 === 1) {
            HiveTab209({
              hives: this.hives,
              onAdd: () => { this.showObserveSheet = true },
              onDetail: (i: number) => { this.detailIndex = i; this.showDetailDialog = true },
              onEdit: (i: number) => {
                this.editIndex = i
                this.editCode = this.hives[i].code
                this.editBees = this.hives[i].bees
                this.editYield = this.hives[i].yield
                this.editTop = this.hives[i].adopted
                this.showEditSheet = true
              },
              onDel: (i: number) => { this.delIndex = i; this.showDelDialog = true },
              onAdopt: (i: number) => {
                this.hives = this.hives.map((h: Hive209, hi: number) => {
                  if (hi === i) {
                    return { ...h, adopted: !h.adopted }
                  }
                  return h
                })
              }
            })
          }
          // ... 其他Tab的条件渲染
        }
        .width('100%')
        .padding(14)
      }
      .layoutWeight(1)
      .align(Alignment.Top)
      this.tabBar209()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFF8E1')
    .bindSheet($$this.showJoinSheet, this.joinSheet209(), {
      height: 620, dragBar: true, showClose: false, backgroundColor: '#FFFFFF'
    })
    .bindSheet($$this.showObserveSheet, this.observeSheet209(), {
      height: 600, dragBar: true, showClose: false, backgroundColor: '#FFFFFF'
    })
    .bindSheet($$this.showEditSheet, this.editSheet209(), {
      height: 560, dragBar: true, showClose: false, backgroundColor: '#FFFFFF'
    })
    .bindContentCover($$this.showDelDialog, this.delDialog209(), {})
    .bindContentCover($$this.showDetailDialog, this.detailDialog209(), {})
  }

build方法的结构清晰分为三层:顶部是header209头部区域;中间是Scroll包裹的可滚动内容区,内含六个if条件渲染块,根据tabIndex1切换显示对应的Tab组件;底部是tabBar209导航栏。每个子Tab组件通过参数传递接收数据(如hives、keepers),通过回调函数(如onStep、onJoin、onDetail、onEdit、onDel、onAdopt)将用户操作回传给父组件处理。注意onStep回调中使用了数组的map方法来更新步骤的done状态——这是ArkTS中不可变数据更新的标准模式,通过返回新数组触发@State的重新渲染。最外层Column通过bindSheet绑定了三个底部抽屉($$this.showJoinSheet等双向绑定实现弹窗显隐),通过bindContentCover绑定了两个居中遮罩对话框。

四、弹窗系统实现

4.1 预约连线取蜜抽屉

预约取蜜抽屉是一个功能完整的表单页面,包含蜜源选择、镇蜂方式、数量选择器、直播开关、认养选项和价格计算。

  @Builder
  joinSheet209() {
    Column({ space: 16 }) {
      Row({ space: 10 }) {
        Column().width(4).height(30).borderRadius(2).backgroundColor('#FF8F00')
        Column({ space: 2 }) {
          Text('预约连线取蜜').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#4E342E')
          Text('和追花老丈同场摇蜜 · 头罐鲜蜜包邮').fontSize(10).fontColor('#8D6E63')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column() {
          Text('✕').fontSize(14).fontColor('#8D6E63')
        }
        .width(30).height(30).borderRadius(15).backgroundColor('#FFF8E1')
        .justifyContent(FlexAlign.Center)
        .onClick(() => { this.showJoinSheet = false })
      }
      .width('100%')

      Scroll() {
        Column({ space: 16 }) {
          Column({ space: 8 }) {
            Text('目标蜜源').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(nectarTags209, (tag: string, i: number) => {
                Text(tag + '蜜')
                  .fontSize(11)
                  .fontColor(this.joinNectar === i ? '#FFFFFF' : '#8D6E63')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(16)
                  .backgroundColor(this.joinNectar === i ? '#FF8F00' : '#FFF8E1')
                  .margin(4)
                  .onClick(() => { this.joinNectar = i })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('预计花费').fontSize(13).fontColor('#6D4C41')
              Text('含冷链邮费').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Text('¥ ' + (this.joinJars * 45 + (this.joinAdopt ? 128 : 0)))
              .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#F4511E')
          }
          .width('100%').padding(12).borderRadius(12).backgroundColor('#FFF8E1')

          Button() {
            Text('确认预约摇蜜').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .width('100%').height(48).borderRadius(24).backgroundColor('#FF8F00')
          .onClick(() => { this.showJoinSheet = false })
        }
        .width('100%')
        .padding({ left: 18, right: 18, top: 4, bottom: 24 })
      }
      .constraintSize({ maxHeight: 460 })
    }
    .width('100%').height('100%')
    .padding({ top: 14 })
    .backgroundColor('#FFFFFF')
  }

抽屉顶部使用4px宽的竖条作为视觉引导,标题区右侧放置关闭按钮。目标蜜源选择区使用Flex({ wrap: FlexWrap.Wrap })实现标签流式布局,五个蜜源标签(洋槐蜜、油菜蜜、荆条蜜、荔枝蜜、椴树蜜)通过ForEach渲染,选中时背景变为橙黄、文字变白。数量选择器采用减号按钮+文字+加号按钮的经典设计,加购数量限制在1-12罐之间。预计花费通过表达式this.joinJars * 45 + (this.joinAdopt ? 128 : 0)实时计算——每罐45元,如果勾选了云认养则额外加128元,计算结果直接绑定到Text组件的内容。底部确认按钮使用Button组件包裹Text,采用圆角24的胶囊形设计,点击后关闭抽屉。

bindSheet是HarmonyOS提供的底部抽屉弹出能力,通过$$语法实现双向数据绑定。当绑定的布尔状态变为true时自动弹出抽屉,用户下滑关闭或调用setState设为false时自动收起。dragBar: true参数在抽屉顶部显示拖拽条,constraintSize的maxHeight限制可滚动区域的最大高度。

4.2 蜂巢详情对话框

详情对话框是一个居中全屏遮罩弹窗,展示蜂箱的温度柱图、蜂群档案和操作入口。

  @Builder
  detailDialog209() {
    Column({ space: 0 }) {
      Scroll() {
        Column({ space: 0 }) {
          Column({ space: 8 }) {
            Row({ space: 10 }) {
              Column() {
                Text('🍯').fontSize(30)
              }
              .width(56).height(56).borderRadius(28)
              .backgroundColor('#FFFFFF')
              .justifyContent(FlexAlign.Center)
              Column({ space: 3 }) {
                Text(this.detailIndex >= 0 && this.detailIndex < this.hives.length
                  ? this.hives[this.detailIndex].code + ' 号箱' : '')
                  .fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
                Text(this.detailIndex >= 0 && this.detailIndex < this.hives.length
                  ? this.hives[this.detailIndex].nectar : '' + '蜜源 · '
                  + (this.detailIndex >= 0 && this.detailIndex < this.hives.length
                  ? this.hives[this.detailIndex].bees : 0) + ' 脾群势')
                  .fontSize(11).fontColor('#FFF8E1')
              }
              .alignItems(HorizontalAlign.Start)
              Text('').layoutWeight(1)
              Column() {
                Text('✕').fontSize(13).fontColor('#FFFFFF')
              }
              .width(28).height(28).borderRadius(14)
              .backgroundColor('rgba(255,255,255,0.2)')
              .justifyContent(FlexAlign.Center)
              .onClick(() => { this.showDetailDialog = false })
            }
            .width('100%')
          }
          .width('100%').padding(18)
          .linearGradient({ angle: 135, colors: [['#FFB300', 0], ['#5D4037', 1]] })

          Column({ space: 14 }) {
            Column({ space: 8 }) {
              Text('近 7 日箱内温度').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4E342E')
              Row({ space: 6 }) {
                ForEach(this.tempLogs, (l: TempLog209) => {
                  Column({ space: 4 }) {
                    Text(l.temp + '°').fontSize(8).fontColor('#FF8F00')
                    Column()
                      .width(16)
                      .height(l.temp)
                      .borderRadius({ topLeft: 4, topRight: 4 })
                      .linearGradient({ angle: 180, colors: [['#FFD54F', 0], ['#FF8F00', 1]] })
                    Text(l.day).fontSize(8).fontColor('#8D6E63')
                  }
                  .alignItems(HorizontalAlign.Center)
                  .layoutWeight(1)
                }, (l: TempLog209) => ('d' + l.day))
              }
              .width('100%')
              .alignItems(VerticalAlign.Bottom)
              .height(80)
            }
            .width('100%').padding(12).borderRadius(12).backgroundColor('#FFFDE7')

            Button() {
              Text('认养TA并连线取蜜').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            }
            .width('100%').height(44).borderRadius(22).backgroundColor('#FF8F00')
            .onClick(() => {
              this.showDetailDialog = false
              this.showJoinSheet = true
            })
          }
          .width('100%').padding(16)
        }
        .width('100%')
      }
      .constraintSize({ maxHeight: 480 })
    }
    .width('86%').borderRadius(20).backgroundColor('#FFFFFF').clip(true)
  }

详情对话框的头部使用linearGradient渐变背景展示蜂箱编号和蜜源信息,关闭按钮使用半透明白色圆形包裹"✕"符号。数据展示区分为三个部分:上方三列数据卡(预估产蜜量、累计围观人次、当前状态),中间是近7日箱内温度的柱状图,下方是蜂群档案表(蜂王状态、健康等级)。温度柱图通过ForEach渲染7天的温度数据,每根柱子使用Column组件,高度直接绑定为温度值(如34px对应34度),柱子使用180度线性渐变从浅黄到深橙,底部对齐排列。底部的"认养TA并连线取蜜"按钮实现了弹窗联动——点击后先关闭详情对话框,再打开预约取蜜抽屉,形成从浏览到下单的操作闭环。

五、核心业务Tab组件

5.1 取蜜房直播Tab

取蜜房是应用的核心功能页,集成了四机位直播画面、取蜜步骤进度和围蜜弹幕。

@Component
struct LiveTab209 {
  @State localMic: boolean = true
  @State localCam: boolean = true
  @State localSmoke: boolean = true
  @Prop hives: Hive209[] = []
  @Prop keepers: Keeper209[] = []
  @Prop harvestSteps: HarvestStep209[] = []
  @Prop barrages: Barrage209[] = []
  onStep: (i: number) => void = () => {}
  onJoin: () => void = () => {}

  build() {
    Column({ space: 12 }) {
      // 四机位网格
      Grid() {
        GridItem() {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Column() {
                Text('🐝').fontSize(18)
              }
              .width(34).height(34).borderRadius(17)
              .backgroundColor('rgba(255,255,255,0.25)')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text('主镜 · 开箱提脾').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text('追花老丈 · 洋槐场').fontSize(9).fontColor('#FFE082')
              }
              .alignItems(HorizontalAlign.Start)
            }
            Text('').layoutWeight(1)
            Row({ space: 6 }) {
              Text('● LIVE').fontSize(9).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
              Text(spinningHiveCount209(this.hives) + ' 箱在摇').fontSize(9).fontColor('#FFE082')
            }
          }
          .linearGradient({ angle: 150, colors: [['#FF8F00', 0], ['#E65100', 1]] })
          .onClick(() => { this.onJoin() })
        }
        // ... 其他三个机位
      }
      .columnsTemplate('1fr 1fr')
      .rowsTemplate('1fr 1fr')
      .height(240)
      .width('100%')

      // 工具条
      Row({ space: 10 }) {
        Column({ space: 3 }) {
          Text(this.localMic ? '🎙️' : '🔇').fontSize(18)
          Text('对讲').fontSize(9).fontColor('#6D4C41')
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Center)
        .borderRadius(12)
        .backgroundColor(this.localMic ? '#FFE082' : '#FFF8E1')
        .onClick(() => { this.localMic = !this.localMic })
        // ... 其他工具按钮
      }
      .width('100%')
    }
    .width('100%')
  }
}

四机位直播画面采用2x2的Grid布局,每个GridItem代表一个直播机位:主镜(开箱提脾,橙红渐变)、摇蜜机位(离心出蜜特写,深橙渐变)、巢脾特写位(封盖率检测,绿色渐变)、我的围观点(镜头开关,褐灰背景)。每个机位的底部展示实时状态信息,如"● LIVE"、箱在摇数量、出蜜罐数、封盖百分比等。工具条包含四个按钮:对讲(麦克风开关)、镜头(摄像头开关)、喷烟(轻烟镇蜂开关)、连蜜(预约入口),每个按钮通过@State管理自身开关状态,点击时切换emoji图标和背景色。

四机位的Grid布局是HarmonyOS声明式UI中处理多画面直播场景的典型方案。通过columnsTemplate和rowsTemplate属性定义行列模板,每个GridItem自动填充到对应的网格位置。机位之间的颜色差异(橙红、深橙、绿色、褐灰)帮助用户快速区分不同视角。

5.2 蜂巢箱Tab

蜂巢箱Tab管理蜂箱列表,支持新增、详情、编辑、删除和认养切换五种操作。

@Component
struct HiveTab209 {
  @Prop hives: Hive209[] = []
  onAdd: () => void = () => {}
  onDetail: (i: number) => void = () => {}
  onEdit: (i: number) => void = () => {}
  onDel: (i: number) => void = () => {}
  onAdopt: (i: number) => void = () => {}

  build() {
    Column({ space: 12 }) {
      // 统计行
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text(this.hives.length + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF8F00')
          Text('在册蜂箱').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Center)
        .borderRadius(10).backgroundColor('#FFF8E1')
        Column({ space: 2 }) {
          Text(adoptedHiveCount209(this.hives) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
          Text('云认养').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Center)
        .borderRadius(10).backgroundColor('#EFEBE9')
        Column({ space: 2 }) {
          Text(spinningHiveCount209(this.hives) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#F4511E')
          Text('摇蜜/割盖').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Center)
        .borderRadius(10).backgroundColor('#FBE9E7')
      }
      .width('100%')

      // 蜜源占比堆叠条
      Column({ space: 8 }) {
        Text('蜂群蜜源构成').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4E342E')
        Row() {
          ForEach(nectarCounts209(this.hives), (c: NectarCount209) => {
            Row() {
              Text(c.label + ' ' + c.count).fontSize(8).fontColor('#FFFFFF')
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
            .backgroundColor(c.color)
          }, (c: NectarCount209) => (c.label + c.count))
        }
        .width('100%').height(22).borderRadius(11).clip(true)
      }

      // 蜂箱卡片列表
      ForEach(this.hives, (h: Hive209, i: number) => {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            Column() {
              Text('🐝').fontSize(20)
            }
            .width(44).height(44).borderRadius(12)
            .backgroundColor(h.adopted ? '#FFECB3' : '#FFF8E1')
            .justifyContent(FlexAlign.Center)
            Column({ space: 3 }) {
              Text(h.code + ' 号箱').fontSize(13).fontColor('#4E342E').fontWeight(FontWeight.Bold)
              Row({ space: 6 }) {
                Text(h.nectar).fontSize(8).fontColor('#FFFFFF')
                  .borderRadius(6).backgroundColor(nectarColor209(h.nectar))
                Text(h.bees + ' 脾').fontSize(9).fontColor('#8D6E63')
                Text('围观 ' + h.watchers).fontSize(9).fontColor('#BCAAA4')
              }
            }
            .alignItems(HorizontalAlign.Start).layoutWeight(1)
            Column() {
              Text(h.adopted ? '💛' : '🤍').fontSize(18)
            }
            .width(32).height(32).justifyContent(FlexAlign.Center)
            .onClick(() => { this.onAdopt(i) })
          }

          Row() {
            Text('预估产蜜').fontSize(9).fontColor('#8D6E63')
            Text(h.yield + ' 斤').fontSize(10).fontColor('#FF8F00').fontWeight(FontWeight.Bold)
            Text('状态').fontSize(9).fontColor('#8D6E63').margin({ left: 14 })
            Text(h.state).fontSize(10).fontColor(hiveStateColor209(h.state)).fontWeight(FontWeight.Bold)
            Text('').layoutWeight(1)
            Column() { Text('详情').fontSize(10).fontColor('#FFFFFF') }
              .borderRadius(10).backgroundColor('#FF8F00')
              .onClick(() => { this.onDetail(i) })
            Column() { Text('编辑').fontSize(10).fontColor('#6D4C41') }
              .borderRadius(10).backgroundColor('#EFEBE9').margin({ left: 6 })
              .onClick(() => { this.onEdit(i) })
            Column() { Text('移除').fontSize(10).fontColor('#F4511E') }
              .borderRadius(10).backgroundColor('#FBE9E7').margin({ left: 6 })
              .onClick(() => { this.onDel(i) })
          }
        }
        .width('100%').padding(12).borderRadius(14).backgroundColor('#FFFFFF')
      }, (h: Hive209) => ('h' + h.id + h.state))
    }
    .width('100%')
  }
}

蜂巢箱Tab的布局自上而下分为四个部分。统计行展示三个关键指标——在册蜂箱数、云认养数、摇蜜/割盖数——使用三等分布局和不同背景色区分。蜜源占比堆叠条通过ForEach渲染nectarCounts209函数返回的五种蜜源统计,每个Row的背景色取自蜜源数据,拼接成一条完整的水平占比条,高度22px,配合clip(true)裁剪圆角。蜂箱卡片列表使用ForEach遍历hives数组,每张卡片包含蜂箱编号、蜜源标签、脾数、围观数、预估产蜜量、当前状态和操作按钮行。认养切换通过💛/🤍 emoji的点击触发onAdopt回调,详情/编辑/移除三个按钮排列在卡片底部。

蜜源占比堆叠条是本应用中数据可视化的亮点之一。nectarCounts209函数遍历蜂箱数组统计各蜜源数量,返回带颜色的统计对象数组,ForEach直接渲染为水平排列的色块。由于每个Row都设置了width(‘100%’),它们在Flex容器中自动按比例分配宽度,形成了真正意义上的"堆叠比例条"效果。

5.3 蜜源志Tab与数据可视化

蜜源志Tab展示五种蜜源的图鉴卡片、热度横条和本周产蜜柱状图。

@Component
struct NectarTab209 {
  @Prop nectars: Nectar209[] = []
  @Prop hives: Hive209[] = []
  @Prop honeyDays: HoneyDay209[] = []

  build() {
    Column({ space: 12 }) {
      // 蜜源图鉴卡
      Column({ space: 8 }) {
        Text('追花蜜源图鉴').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4E342E')
        ForEach(this.nectars, (n: Nectar209) => {
          Row({ space: 10 }) {
            Column() {
              Text('🌸').fontSize(18)
            }
            .width(40).height(40).borderRadius(10)
            .backgroundColor(n.color)
            .justifyContent(FlexAlign.Center)
            Column({ space: 2 }) {
              Row({ space: 6 }) {
                Text(n.name + '蜜').fontSize(12).fontColor('#4E342E').fontWeight(FontWeight.Bold)
                Column().width(10).height(10).borderRadius(5).backgroundColor(n.color)
              }
              Text('花期 ' + n.bloom + ' · 甜度 ' + n.sweetness).fontSize(9).fontColor('#8D6E63')
            }
            .alignItems(HorizontalAlign.Start).layoutWeight(1)
            Text(n.heat + '°').fontSize(13).fontColor('#FF8F00').fontWeight(FontWeight.Bold)
          }
          .width('100%').padding(10).borderRadius(10).backgroundColor('#FFFDE7')
        }, (n: Nectar209) => ('n' + n.id))
      }

      // 蜜源热度横条
      Column({ space: 10 }) {
        Text('蜜源人气热度').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4E342E')
        ForEach(this.nectars, (n: Nectar209) => {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Text(n.name).fontSize(10).fontColor('#6D4C41').width(40)
              Row() {
                Row()
                  .width(n.heat + '%')
                  .height(10)
                  .borderRadius(5)
                  .linearGradient({ angle: 0, colors: [['#FFD54F', 0], ['#FF8F00', 1]] })
              }
              .layoutWeight(1).height(10).borderRadius(5).backgroundColor('#FFF8E1')
              Text(n.heat + '').fontSize(9).fontColor('#FF8F00').fontWeight(FontWeight.Bold)
            }
          }
        }, (n: Nectar209) => ('heat' + n.id))
      }

      // 本周产蜜柱图
      Column({ space: 8 }) {
        Text('本周每日产蜜罐数').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4E342E')
        Row({ space: 6 }) {
          ForEach(this.honeyDays, (d: HoneyDay209) => {
            Column({ space: 4 }) {
              Text(d.jars + '').fontSize(8).fontColor('#FF8F00')
              Column()
                .width(18)
                .height(d.jars * 3)
                .borderRadius({ topLeft: 4, topRight: 4 })
                .linearGradient({ angle: 180, colors: [['#FFD54F', 0], ['#E65100', 1]] })
              Text(d.day).fontSize(8).fontColor('#8D6E63')
            }
            .alignItems(HorizontalAlign.Center).layoutWeight(1)
          }, (d: HoneyDay209) => ('dd' + d.day))
        }
        .alignItems(VerticalAlign.Bottom).height(120)
      }
    }
    .width('100%')
  }
}

蜜源图鉴卡片每行展示一种蜜源:左侧是🌸emoji图标配合蜜源颜色背景的圆形容器,中间是蜜源名称和花期甜度信息,右侧是热度值。热度横条通过Row嵌套实现:外层Row是浅色背景的轨道容器,内层Row的width绑定为n.heat + ‘%’(如洋槐95%),使用0度线性渐变从浅黄到深橙填充。本周产蜜柱图使用ForEach渲染7天数据,每根柱子的height绑定为d.jars * 3(如38罐对应114px),采用180度从上到下的渐变填充,容器设置alignItems(VerticalAlign.Bottom)使所有柱子底部对齐。

六、核心流程图

以下是应用的主要交互流程,从用户进入应用到完成预约取蜜的完整路径:

取蜜房

蜂巢箱

蜜源志

工具架

蜂农团

我的

详情

编辑

认养

移除

用户启动应用

加载主页面 Index209

渲染头部 + 底部Tab导航

用户选择Tab

查看四机位直播

浏览蜂箱列表

查看蜜源图鉴

管理蜂具库存

查看蜂农排行

查看个人数据

点击主镜或连蜜按钮

弹出预约取蜜抽屉

点击蜂箱卡片

选择操作

弹出蜂巢详情对话框

弹出编辑抽屉

切换认养状态

弹出删除确认对话框

查看温度图+档案

点击认养并连线取蜜

选择蜜源/镇蜂/数量

计算价格

确认预约摇蜜

关闭抽屉返回主页

确认移除蜂箱

filter数组更新列表

从流程图可以看出,应用的核心交互路径是从直播画面或蜂箱详情进入预约取蜜流程。用户无论从取蜜房的"连蜜"按钮、蜂巢箱的详情对话框底部按钮,还是蜂农团的"去摇蜜"入口,最终都会汇聚到预约取蜜抽屉这个统一入口,完成蜜源选择、数量确认和价格计算后提交预约。删除操作则通过filter方法从数组中移除指定蜂箱,实现数据的不可变更新。

七、其他Tab组件实现

7.1 工具架Tab

工具架Tab管理蜂具库存和箱温监测数据,使用列表和柱状图展示信息。

@Component
struct FrameTab209 {
  @Prop frames: Frame209[] = []
  @Prop tempLogs: TempLog209[] = []
  @State pickIndex: number = -1

  build() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text(this.frames.length + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF8F00')
          Text('在库蜂具').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Center).borderRadius(10).backgroundColor('#FFF8E1')
        Column({ space: 2 }) {
          Text('86').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
          Text('今日取用').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Center).borderRadius(10).backgroundColor('#EFEBE9')
        Column({ space: 2 }) {
          Text('99%').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#7CB342')
          Text('完好率').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Center).borderRadius(10).backgroundColor('#F1F8E9')
      }

      Column({ space: 8 }) {
        Text('蜂具货架').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4E342E')
        ForEach(this.frames, (f: Frame209, i: number) => {
          Row({ space: 10 }) {
            Column() {
              Text(f.icon).fontSize(18)
            }
            .width(40).height(40).borderRadius(10).backgroundColor('#FFF8E1')
            .justifyContent(FlexAlign.Center)
            Column({ space: 2 }) {
              Text(f.name).fontSize(12).fontColor('#4E342E').fontWeight(FontWeight.Bold)
              Text('库存 ' + f.stock + ' 件 · 累计取用 ' + f.times + ' 次').fontSize(9).fontColor('#8D6E63')
            }
            .alignItems(HorizontalAlign.Start).layoutWeight(1)
            Column() {
              Text(this.pickIndex === i ? '已取用' : (f.capped ? '常用' : '备用')).fontSize(10)
                .fontColor(this.pickIndex === i ? '#FFFFFF' : (f.capped ? '#6D4C41' : '#BCAAA4'))
            }
            .borderRadius(10)
            .backgroundColor(this.pickIndex === i ? '#FF8F00' : (f.capped ? '#EFEBE9' : '#FFFDE7'))
            .onClick(() => { this.pickIndex = i })
          }
          .backgroundColor(this.pickIndex === i ? '#FFF8E1' : '#FAFAFA')
        }, (f: Frame209) => ('t' + f.id + this.pickIndex))
      }
    }
  }
}

工具架Tab展示8种蜂具的库存和使用记录,包括标准巢框、浅继箱框、巢础片、隔王板、饲喂器、脱粉器、起刮刀、蜂扫。每种蜂具都有emoji图标、库存数量和累计取用次数。点击列表项可标记为"已取用",背景变为蜜光黄。底部展示近7日箱内均温柱状图,与详情对话框中的温度图实现方式相同,通过ForEach渲染7根柱子,高度直接绑定为温度值。pickIndex状态的引入使得列表支持单选交互——同时只有一个蜂具可以被标记为"已取用"。

7.2 蜂农团Tab

蜂农团Tab展示蜂农人气排行榜,每个蜂农的头像背景色取自蜜源颜色,形成视觉关联。

@Component
struct KeeperTab209 {
  @Prop keepers: Keeper209[] = []
  @State followIndex: number = -1
  onJoin: () => void = () => {}

  build() {
    Column({ space: 12 }) {
      Column({ space: 8 }) {
        Row({ space: 10 }) {
          Column() { Text('🧑‍🌾').fontSize(24) }
            .width(48).height(48).borderRadius(24).backgroundColor('#FFF8E1')
          Column({ space: 3 }) {
            Text('在线蜂农 ' + onlineKeeperCount209(this.keepers) + ' / ' + this.keepers.length)
              .fontSize(14).fontColor('#4E342E').fontWeight(FontWeight.Bold)
            Text('连线取蜜 · 同场摇蜜发货').fontSize(10).fontColor('#8D6E63')
          }
          .alignItems(HorizontalAlign.Start).layoutWeight(1)
          Column() { Text('去摇蜜').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
            .borderRadius(14).backgroundColor('#FF8F00')
            .onClick(() => { this.onJoin() })
        }
      }

      Column({ space: 10 }) {
        Text('蜂农人气榜').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4E342E')
        ForEach(this.keepers, (k: Keeper209, i: number) => {
          Row({ space: 10 }) {
            Text((i + 1) + '').fontSize(13)
              .fontColor(i < 3 ? '#F4511E' : '#BCAAA4').fontWeight(FontWeight.Bold)
            Column() { Text(k.name.slice(0, 1)).fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold) }
              .width(40).height(40).borderRadius(20)
              .backgroundColor(nectarColor209(nectarTags209[i % nectarTags209.length]))
            Column({ space: 2 }) {
              Row({ space: 6 }) {
                Text(k.name).fontSize(12).fontColor('#4E342E').fontWeight(FontWeight.Bold)
                if (k.online) {
                  Column() { Text('在线').fontSize(8).fontColor('#7CB342') }
                    .borderRadius(6).backgroundColor('#F1F8E9')
                }
              }
              Text(k.city + ' · 养蜂 ' + k.years + ' 年').fontSize(9).fontColor('#8D6E63')
              Row({ space: 6 }) {
                Row() {
                  Row().width((k.heat / maxKeeperHeat209(this.keepers) * 100) + '%')
                    .height(8).borderRadius(4)
                    .linearGradient({ angle: 0, colors: [['#FFD54F', 0], ['#F4511E', 1]] })
                }
                .width(90).height(8).borderRadius(4).backgroundColor('#FFF8E1').clip(true)
                Text('人气 ' + k.heat).fontSize(8).fontColor('#F4511E')
              }
            }
            .alignItems(HorizontalAlign.Start).layoutWeight(1)
            Column() {
              Text(this.followIndex === i ? '已关注' : '关注').fontSize(10)
                .fontColor(this.followIndex === i ? '#FFFFFF' : '#FF8F00')
            }
            .borderRadius(12)
            .backgroundColor(this.followIndex === i ? '#FF8F00' : '#FFF8E1')
            .onClick(() => { this.followIndex = i })
          }
        }, (k: Keeper209) => ('k' + k.id + this.followIndex))
      }
    }
  }
}

蜂农排行榜的排名数字在前三名使用深橙色(#F4511E),之后使用浅灰色(#BCAAA4)区分。每个蜂农的头像背景色通过nectarColor209(nectarTags209[i % nectarTags209.length])计算——即根据排名索引取模选择蜜源颜色,让六位蜂农的头像呈现不同的蜜源色调。人气横条的宽度通过表达式k.heat / maxKeeperHeat209(this.keepers) * 100计算百分比,以最高人气值为基准归一化,确保横条不会溢出容器。关注按钮使用followIndex状态管理单选行为,点击切换"已关注"/"关注"文字和背景色。

蜂农头像背景色与蜜源颜色的关联设计是一个巧妙的细节。nectarTags209数组的循环引用让六位蜂农分别对应洋槐、油菜、荆条、荔枝、椴树、洋槐的颜色,既保证了视觉多样性又暗示了蜂农与蜜源的关联——每位蜂农主打的蜜种不同,头像颜色就是直观的标识。

八、技术点对比分析

技术维度 具体实现 设计优势 适用场景
Tab导航 蜂巢格拟物化设计,上下两段式 视觉独特性强,契合养蜂主题 主题化应用,品牌识别要求高的场景
弹窗管理 bindSheet + bindContentCover双体系 抽屉适合表单输入,遮罩适合详情展示 需要多种弹窗形态的复杂表单流程
状态管理 全局状态集中在入口组件 数据流单向可预测,调试方便 中小型应用,Tab间数据共享度高
数据更新 map/filter不可变数组操作 触发自动重新渲染,避免直接突变 ArkTS声明式范式的标准模式
数据可视化 Column高度绑定实现柱状图 无需第三方图表库,纯原生渲染 简单柱图、横条、堆叠条场景
颜色映射 工具函数if-else返回色值 配色逻辑集中管理,易于维护 状态多且颜色频繁复用的场景
条件渲染 if-else切换Tab内容 编译时优化,无额外组件实例开销 Tab数量固定且页面独立的场景
@Builder复用 弹窗UI内容封装为Builder方法 内联展开,this指向当前组件 弹窗内容与主组件状态强关联
@Prop传递 子组件只读接收父组件数据 单向数据流,防止子组件意外修改 列表渲染、数据展示组件
回调通信 箭头函数传递操作意图 父组件统一处理状态更新 需要子组件触发父组件状态变更
堆叠条图 Row嵌套+width百分比 纯CSS式比例分配,无需计算像素 占比/分布类数据可视化
柱状图 Column高度绑定数据值×系数 简单直观,支持渐变色填充 7日趋势等少量数据点场景

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============================================================
// 场景:蜂农连线取蜜 / 云认养蜂巢直播围观
// 配色:蜂蜡黄 #FFB300 × 蜂巢褐 #6D4C41 × 蜜光白 #FFF8E1
// Tab 布局:底部「蜂巢格」导航(上窄下宽两段式蜂巢六边形象形)
// ============================================================

interface HoneyDay209 {
  day: string
  jars: number
}

interface Hive209 {
  id: number
  code: string
  nectar: string
  bees: number
  yield: number
  state: string
  watchers: number
  adopted: boolean
}

interface Nectar209 {
  id: number
  name: string
  color: string
  bloom: string
  sweetness: number
  heat: number
}

interface Harvest209 {
  id: number
  name: string
  nectar: string
  dayNum: number
  frames: number
  jars: number
  state: string
  top: boolean
}

interface Keeper209 {
  id: number
  name: string
  city: string
  years: number
  online: boolean
  heat: number
}

interface Barrage209 {
  id: number
  text: string
}

interface HarvestStep209 {
  id: number
  title: string
  tip: string
  done: boolean
}

interface NectarCount209 {
  label: string
  count: number
  color: string
}

interface TempLog209 {
  day: string
  temp: number
}

interface Frame209 {
  id: number
  name: string
  icon: string
  stock: number
  capped: boolean
  times: number
}

const nectarTags209: string[] = ['洋槐', '油菜', '荆条', '荔枝', '椴树']
const smokeTags209: string[] = ['轻烟镇蜂', '喷水降躁']

function hiveStateColor209(state: string): string {
  if (state === '摇蜜中') {
    return '#FF8F00'
  }
  if (state === '割蜜盖') {
    return '#F4511E'
  }
  if (state === '蜜已封盖') {
    return '#7CB342'
  }
  return '#90A4AE'
}

function harvestStateColor209(state: string): string {
  if (state === '今日摇蜜') {
    return '#F4511E'
  }
  if (state === '等待封盖') {
    return '#FBC02D'
  }
  if (state === '已装罐') {
    return '#7CB342'
  }
  return '#90A4AE'
}

function nectarColor209(nectar: string): string {
  if (nectar === '洋槐') {
    return '#FFE082'
  }
  if (nectar === '油菜') {
    return '#FDD835'
  }
  if (nectar === '荆条') {
    return '#A1887F'
  }
  if (nectar === '荔枝') {
    return '#FB8C00'
  }
  return '#C0A16B'
}

function adoptedHiveCount209(hives: Hive209[]): number {
  let n: number = 0
  for (let i = 0; i < hives.length; i++) {
    if (hives[i].adopted) {
      n++
    }
  }
  return n
}

function spinningHiveCount209(hives: Hive209[]): number {
  let n: number = 0
  for (let i = 0; i < hives.length; i++) {
    if (hives[i].state === '摇蜜中' || hives[i].state === '割蜜盖') {
      n++
    }
  }
  return n
}

function onlineKeeperCount209(keepers: Keeper209[]): number {
  let n: number = 0
  for (let i = 0; i < keepers.length; i++) {
    if (keepers[i].online) {
      n++
    }
  }
  return n
}

function maxKeeperHeat209(keepers: Keeper209[]): number {
  let m: number = 1
  for (let i = 0; i < keepers.length; i++) {
    if (keepers[i].heat > m) {
      m = keepers[i].heat
    }
  }
  return m
}

function nectarCounts209(hives: Hive209[]): NectarCount209[] {
  const counts: NectarCount209[] = []
  for (let i = 0; i < nectarTags209.length; i++) {
    let n: number = 0
    for (let j = 0; j < hives.length; j++) {
      if (hives[j].nectar === nectarTags209[i]) {
        n++
      }
    }
    counts.push({ label: nectarTags209[i], count: n, color: ['#FDD835', '#FFB300', '#A1887F', '#FB8C00', '#C0A16B'][i] })
  }
  return counts
}

// ================= 主页面 =================

@Entry
@Component
struct Index209 {
  @State tabIndex1: number = 0

  // 弹框开关
  @State showJoinSheet: boolean = false
  @State showObserveSheet: boolean = false
  @State showEditSheet: boolean = false
  @State showDelDialog: boolean = false
  @State showDetailDialog: boolean = false

  // 预约连线取蜜表单
  @State joinNectar: number = 0
  @State joinJars: number = 4
  @State joinSmoke: number = 0
  @State joinLive: boolean = true
  @State joinAdopt: boolean = false

  // 新增观测表单
  @State obsCode: string = ''
  @State obsNectar: number = 0
  @State obsBees: number = 4
  @State obsYield: number = 30
  @State obsPublic: boolean = true

  // 编辑表单
  @State editIndex: number = -1
  @State editCode: string = ''
  @State editBees: number = 4
  @State editYield: number = 30
  @State editTop: boolean = false

  // 删除
  @State delIndex: number = -1
  @State delKeepLog: boolean = true

  // 详情
  @State detailIndex: number = 0

  // 数据
  @State hives: Hive209[] = [
    { id: 1, code: 'A-07', nectar: '洋槐', bees: 5, yield: 42, state: '摇蜜中', watchers: 3050, adopted: true },
    { id: 2, code: 'A-12', nectar: '油菜', bees: 4, yield: 35, state: '蜜已封盖', watchers: 1420, adopted: true },
    { id: 3, code: 'B-03', nectar: '荆条', bees: 6, yield: 48, state: '割蜜盖', watchers: 2680, adopted: false },
    { id: 4, code: 'B-08', nectar: '荔枝', bees: 5, yield: 38, state: '摇蜜中', watchers: 3340, adopted: true },
    { id: 5, code: 'C-01', nectar: '椴树', bees: 6, yield: 52, state: '蜜已封盖', watchers: 2210, adopted: false },
    { id: 6, code: 'C-05', nectar: '洋槐', bees: 3, yield: 28, state: '产卵扩群', watchers: 880, adopted: false },
    { id: 7, code: 'D-02', nectar: '荆条', bees: 5, yield: 40, state: '摇蜜中', watchers: 1980, adopted: true },
    { id: 8, code: 'D-09', nectar: '油菜', bees: 4, yield: 33, state: '产卵扩群', watchers: 760, adopted: false },
    { id: 9, code: 'E-04', nectar: '椴树', bees: 6, yield: 55, state: '蜜已封盖', watchers: 4160, adopted: true },
    { id: 10, code: 'E-11', nectar: '荔枝', bees: 4, yield: 30, state: '产卵扩群', watchers: 1020, adopted: false }
  ]

  @State nectars: Nectar209[] = [
    { id: 1, name: '洋槐', color: '#FFE082', bloom: '4-5 月', sweetness: 92, heat: 95 },
    { id: 2, name: '油菜', color: '#FDD835', bloom: '3-4 月', sweetness: 84, heat: 82 },
    { id: 3, name: '荆条', color: '#A1887F', bloom: '6-7 月', sweetness: 88, heat: 86 },
    { id: 4, name: '荔枝', color: '#FB8C00', bloom: '2-3 月', sweetness: 94, heat: 91 },
    { id: 5, name: '椴树', color: '#C0A16B', bloom: '6-7 月', sweetness: 90, heat: 89 }
  ]

  @State harvests: Harvest209[] = [
    { id: 1, name: 'A-07 洋槐春蜜头茬', nectar: '洋槐', dayNum: 2, frames: 8, jars: 24, state: '今日摇蜜', top: true },
    { id: 2, name: 'A-12 油菜蜜二次取', nectar: '油菜', dayNum: 1, frames: 6, jars: 15, state: '已装罐', top: false },
    { id: 3, name: 'B-03 荆条夏蜜开摇', nectar: '荆条', dayNum: 3, frames: 9, jars: 27, state: '今日摇蜜', top: false },
    { id: 4, name: 'B-08 荔枝蜜封盖检测', nectar: '荔枝', dayNum: 1, frames: 7, jars: 0, state: '等待封盖', top: false },
    { id: 5, name: 'C-01 椴树雪蜜预排', nectar: '椴树', dayNum: 4, frames: 10, jars: 30, state: '等待封盖', top: false },
    { id: 6, name: 'D-02 荆条蜜三框试摇', nectar: '荆条', dayNum: 2, frames: 3, jars: 9, state: '已装罐', top: false },
    { id: 7, name: 'E-04 椴树蜜王浆同步', nectar: '椴树', dayNum: 5, frames: 8, jars: 26, state: '已装罐', top: false },
    { id: 8, name: 'E-11 荔枝小群繁殖期', nectar: '荔枝', dayNum: 1, frames: 0, jars: 0, state: '等待封盖', top: false }
  ]

  @State keepers: Keeper209[] = [
    { id: 1, name: '追花老丈', city: '秦岭', years: 26, online: true, heat: 97 },
    { id: 2, name: '摇蜜阿姐', city: '从化', years: 12, online: true, heat: 90 },
    { id: 3, name: '椴树屯主', city: '饶河', years: 20, online: false, heat: 92 },
    { id: 4, name: '蜂箱小哥', city: '桐庐', years: 7, online: true, heat: 75 },
    { id: 5, name: '荆条婆婆', city: '邢台', years: 23, online: false, heat: 85 },
    { id: 6, name: '蜂场千金', city: '蒙阴', years: 9, online: true, heat: 80 }
  ]

  @State honeyDays: HoneyDay209[] = [
    { day: '周一', jars: 18 },
    { day: '周二', jars: 26 },
    { day: '周三', jars: 22 },
    { day: '周四', jars: 31 },
    { day: '周五', jars: 38 },
    { day: '周六', jars: 46 },
    { day: '周日', jars: 40 }
  ]

  @State tempLogs: TempLog209[] = [
    { day: 'D1', temp: 34 },
    { day: 'D2', temp: 35 },
    { day: 'D3', temp: 34 },
    { day: 'D4', temp: 36 },
    { day: 'D5', temp: 35 },
    { day: 'D6', temp: 34 },
    { day: 'D7', temp: 35 }
  ]

  @State frames: Frame209[] = [
    { id: 1, name: '标准巢框', icon: '🖼️', stock: 42, capped: true, times: 160 },
    { id: 2, name: '浅继箱框', icon: '📦', stock: 24, capped: false, times: 88 },
    { id: 3, name: '巢础片', icon: '🍯', stock: 60, capped: false, times: 210 },
    { id: 4, name: '隔王板', icon: '🚧', stock: 18, capped: true, times: 72 },
    { id: 5, name: '饲喂器', icon: '🥣', stock: 15, capped: false, times: 55 },
    { id: 6, name: '脱粉器', icon: '🌼', stock: 9, capped: false, times: 34 },
    { id: 7, name: '起刮刀', icon: '🔪', stock: 12, capped: true, times: 96 },
    { id: 8, name: '蜂扫', icon: '🪶', stock: 20, capped: false, times: 118 }
  ]

  @State barrages: Barrage209[] = [
    { id: 1, text: '割蜜盖那一下好治愈' },
    { id: 2, text: '蜜蜂环绕的镜头太治愈了' },
    { id: 3, text: '第一次云摇蜜,涨知识' },
    { id: 4, text: '洋槐蜜的颜色像琥珀' },
    { id: 5, text: '追花老丈的手法真稳' },
    { id: 6, text: '这箱蜂数估计六脾了' },
    { id: 7, text: '摇蜜 +1,出 24 罐' },
    { id: 8, text: '认养的 E-04 今天出雪蜜' }
  ]

  @State harvestSteps: HarvestStep209[] = [
    { id: 1, title: '开箱检查', tip: '轻开箱盖 · 观察蜂群情绪', done: true },
    { id: 2, title: '喷烟镇蜂', tip: '轻烟两下 · 蜜蜂低头吃蜜', done: true },
    { id: 3, title: '提脾抖蜂', tip: '提起巢脾 · 轻抖三下落蜂', done: true },
    { id: 4, title: '割除蜜盖', tip: '热刀轻割 · 蜡盖留作蜂蜡', done: false },
    { id: 5, title: '摇蜜机甩蜜', tip: '对称放入 · 匀速摇柄', done: false },
    { id: 6, title: '过滤装罐', tip: '双层滤网 · 静置去泡装罐', done: false }
  ]

  tabs209: string[] = ['取蜜房', '蜂巢箱', '蜜源志', '工具架', '蜂农团', '我的']
  tabIcons209: string[] = ['🍯', '🐝', '🌸', '🧰', '🧑‍🌾', '👤']

  // ---------- 头部(电商蜜季风,无动画) ----------
  @Builder
  header209() {
    Column({ space: 12 }) {
      Row({ space: 10 }) {
        Column({ space: 4 }) {
          Text('蜂颂 · 云养蜂场').fontSize(19).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('蜂农连线取蜜 · 云认养蜂巢直播围观').fontSize(11).fontColor('#FFE082')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column({ space: 2 }) {
          Text('🐝').fontSize(20)
          Text('2,340').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('围摇蜜位').fontSize(9).fontColor('#FFE082')
        }
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')

      Row({ space: 10 }) {
        Column().width(4).height(34).borderRadius(2).backgroundColor('#FF8F00')
        Column({ space: 3 }) {
          Text('洋槐头茬蜜今日开摇').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('连线摇蜜抽头罐鲜蜜 · 认养享全年配送').fontSize(10).fontColor('#FFF8E1')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column() {
          Text('去摇蜜 →').fontSize(11).fontColor('#6D4C41').fontWeight(FontWeight.Bold)
        }
        .padding({ left: 12, right: 12, top: 7, bottom: 7 })
        .borderRadius(14)
        .backgroundColor('#FFF8E1')
        .onClick(() => {
          this.showJoinSheet = true
        })
      }
      .width('100%')
      .padding(12)
      .borderRadius(12)
      .backgroundColor('#5D4037')
    }
    .alignItems(HorizontalAlign.Start)
    .padding(14)
    .linearGradient({ angle: 140, colors: [['#FFB300', 0], ['#FF8F00', 1]] })
  }

  // ---------- 底部「蜂巢格」tab ----------
  @Builder
  tabBar209() {
    Column({ space: 0 }) {
      Row({ space: 4 }) {
        ForEach(this.tabs209, (t: string, i: number) => {
          Column({ space: 0 }) {
            // 蜂巢上格(窄)
            Row({ space: 4 }) {
              Text(this.tabIcons209[i]).fontSize(10)
              Text(this.tabIndex1 === i ? '🐝' : '').fontSize(8)
            }
            .width('62%')
            .height(16)
            .justifyContent(FlexAlign.Center)
            .borderRadius({ topLeft: 8, topRight: 8 })
            .backgroundColor(this.tabIndex1 === i ? '#FFD54F' : '#D7CCC8')

            // 蜂巢下格(宽)
            Column({ space: 1 }) {
              Text(t).fontSize(9).fontColor(this.tabIndex1 === i ? '#4E342E' : '#FFFFFF').maxLines(1)
              Text(this.tabIndex1 === i ? '❋' : '·').fontSize(7).fontColor(this.tabIndex1 === i ? '#FF8F00' : '#A1887F')
            }
            .width('100%')
            .height(30)
            .alignItems(HorizontalAlign.Center)
            .justifyContent(FlexAlign.Center)
            .borderRadius({ bottomLeft: 10, bottomRight: 10 })
            .backgroundColor(this.tabIndex1 === i ? '#FFB300' : '#6D4C41')
          }
          .layoutWeight(1)
          .padding({ top: 2, bottom: 2 })
          .scale({ x: this.tabIndex1 === i ? 1.07 : 1, y: this.tabIndex1 === i ? 1.07 : 1 })
          .animation({ duration: 180 })
          .onClick(() => {
            this.tabIndex1 = i
          })
        }, (t: string) => t)
      }
      .width('100%')
      .padding({ left: 8, right: 8, top: 4 })
      .alignItems(VerticalAlign.Top)
    }
    .width('100%')
    .padding({ top: 2, bottom: 6 })
    .backgroundColor('#4E342E')
    .shadow({ radius: 10, color: 'rgba(0,0,0,0.25)', offsetY: -3 })
  }

  build() {
    Column() {
      this.header209()
      Scroll() {
        Column({ space: 12 }) {
          if (this.tabIndex1 === 0) {
            LiveTab209({
              hives: this.hives,
              keepers: this.keepers,
              harvestSteps: this.harvestSteps,
              barrages: this.barrages,
              onStep: (i: number) => {
                this.harvestSteps = this.harvestSteps.map((s: HarvestStep209, si: number) => {
                  if (si === i) {
                    return { id: s.id, title: s.title, tip: s.tip, done: !s.done }
                  }
                  return s
                })
              },
              onJoin: () => {
                this.showJoinSheet = true
              }
            })
          }
          if (this.tabIndex1 === 1) {
            HiveTab209({
              hives: this.hives,
              onAdd: () => {
                this.showObserveSheet = true
              },
              onDetail: (i: number) => {
                this.detailIndex = i
                this.showDetailDialog = true
              },
              onEdit: (i: number) => {
                this.editIndex = i
                this.editCode = this.hives[i].code
                this.editBees = this.hives[i].bees
                this.editYield = this.hives[i].yield
                this.editTop = this.hives[i].adopted
                this.showEditSheet = true
              },
              onDel: (i: number) => {
                this.delIndex = i
                this.showDelDialog = true
              },
              onAdopt: (i: number) => {
                this.hives = this.hives.map((h: Hive209, hi: number) => {
                  if (hi === i) {
                    return { id: h.id, code: h.code, nectar: h.nectar, bees: h.bees, yield: h.yield, state: h.state, watchers: h.watchers, adopted: !h.adopted }
                  }
                  return h
                })
              }
            })
          }
          if (this.tabIndex1 === 2) {
            NectarTab209({ nectars: this.nectars, hives: this.hives, honeyDays: this.honeyDays })
          }
          if (this.tabIndex1 === 3) {
            FrameTab209({ frames: this.frames, tempLogs: this.tempLogs })
          }
          if (this.tabIndex1 === 4) {
            KeeperTab209({
              keepers: this.keepers,
              onJoin: () => {
                this.showJoinSheet = true
              }
            })
          }
          if (this.tabIndex1 === 5) {
            MineTab209({ hives: this.hives, harvests: this.harvests, honeyDays: this.honeyDays })
          }
        }
        .width('100%')
        .padding(14)
      }
      .layoutWeight(1)
      .align(Alignment.Top)
      this.tabBar209()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFF8E1')
    .bindSheet($$this.showJoinSheet, this.joinSheet209(), {
      height: 620,
      dragBar: true,
      showClose: false,
      backgroundColor: '#FFFFFF'
    })
    .bindSheet($$this.showObserveSheet, this.observeSheet209(), {
      height: 600,
      dragBar: true,
      showClose: false,
      backgroundColor: '#FFFFFF'
    })
    .bindSheet($$this.showEditSheet, this.editSheet209(), {
      height: 560,
      dragBar: true,
      showClose: false,
      backgroundColor: '#FFFFFF'
    })
    .bindContentCover($$this.showDelDialog, this.delDialog209(), {
    })
    .bindContentCover($$this.showDetailDialog, this.detailDialog209(), {
    })
  }

  // ---------- 弹框1:预约连线取蜜(抽屉) ----------
  @Builder
  joinSheet209() {
    Column({ space: 16 }) {
      Row({ space: 10 }) {
        Column().width(4).height(30).borderRadius(2).backgroundColor('#FF8F00')
        Column({ space: 2 }) {
          Text('预约连线取蜜').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#4E342E')
          Text('和追花老丈同场摇蜜 · 头罐鲜蜜包邮').fontSize(10).fontColor('#8D6E63')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column() {
          Text('✕').fontSize(14).fontColor('#8D6E63')
        }
        .width(30)
        .height(30)
        .borderRadius(15)
        .backgroundColor('#FFF8E1')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showJoinSheet = false
        })
      }
      .width('100%')

      Scroll() {
        Column({ space: 16 }) {
          Column({ space: 8 }) {
            Text('目标蜜源').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(nectarTags209, (tag: string, i: number) => {
                Text(tag + '蜜')
                  .fontSize(11)
                  .fontColor(this.joinNectar === i ? '#FFFFFF' : '#8D6E63')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(16)
                  .backgroundColor(this.joinNectar === i ? '#FF8F00' : '#FFF8E1')
                  .margin(4)
                  .onClick(() => {
                    this.joinNectar = i
                  })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Column({ space: 8 }) {
            Text('镇蜂方式').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(smokeTags209, (tag: string, i: number) => {
                Text(tag)
                  .fontSize(11)
                  .fontColor(this.joinSmoke === i ? '#FFFFFF' : '#8D6E63')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(16)
                  .backgroundColor(this.joinSmoke === i ? '#6D4C41' : '#FFF8E1')
                  .margin(4)
                  .onClick(() => {
                    this.joinSmoke = i
                  })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#FF8F00')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FFF8E1')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.joinJars > 1) {
                this.joinJars -= 1
              }
            })
            Column({ space: 2 }) {
              Text('订购 ' + this.joinJars + ' 罐').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FF8F00')
              Text('每罐 500g 现摇现发').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#FF8F00')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FFF8E1')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.joinJars < 12) {
                this.joinJars += 1
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('直播取蜜').fontSize(13).fontColor('#6D4C41')
              Text('出蜜瞬间同步给你看').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Row() {
              Circle().width(18).height(18).fill('#FFFFFF')
            }
            .width(48)
            .height(26)
            .borderRadius(13)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(this.joinLive ? '#FF8F00' : '#BDBDBD')
            .onClick(() => {
              this.joinLive = !this.joinLive
            })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#FFF8E1')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('云认养蜂巢').fontSize(13).fontColor('#6D4C41')
              Text('认养箱位享全年蜜配送').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Row() {
              Circle().width(18).height(18).fill('#FFFFFF')
            }
            .width(48)
            .height(26)
            .borderRadius(13)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(this.joinAdopt ? '#6D4C41' : '#BDBDBD')
            .onClick(() => {
              this.joinAdopt = !this.joinAdopt
            })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#EFEBE9')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('预计花费').fontSize(13).fontColor('#6D4C41')
              Text('含冷链邮费').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Text('¥ ' + (this.joinJars * 45 + (this.joinAdopt ? 128 : 0))).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#F4511E')
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#FFF8E1')

          Button() {
            Text('确认预约摇蜜').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .height(48)
          .borderRadius(24)
          .backgroundColor('#FF8F00')
          .onClick(() => {
            this.showJoinSheet = false
          })

          Text('摇蜜前 2 小时可免费改期 · 头罐归连线者').fontSize(9).fontColor('#BDBDBD')
        }
        .width('100%')
        .padding({ left: 18, right: 18, top: 4, bottom: 24 })
      }
      .constraintSize({ maxHeight: 460 })
    }
    .width('100%')
    .height('100%')
    .padding({ top: 14 })
    .backgroundColor('#FFFFFF')
  }

  // ---------- 弹框2:新增蜂巢观测(抽屉,concat 前插) ----------
  @Builder
  observeSheet209() {
    Column({ space: 16 }) {
      Row({ space: 10 }) {
        Column().width(4).height(30).borderRadius(2).backgroundColor('#6D4C41')
        Column({ space: 2 }) {
          Text('新增蜂巢观测').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#4E342E')
          Text('记录你的蜂箱日志 · 同步蜂群档案').fontSize(10).fontColor('#8D6E63')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column() {
          Text('✕').fontSize(14).fontColor('#8D6E63')
        }
        .width(30)
        .height(30)
        .borderRadius(15)
        .backgroundColor('#FFF8E1')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showObserveSheet = false
        })
      }
      .width('100%')

      Scroll() {
        Column({ space: 16 }) {
          Column({ space: 8 }) {
            Text('蜂箱编号').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
            TextInput({ placeholder: '例如:F-06', text: this.obsCode })
              .fontSize(13)
              .padding(12)
              .borderRadius(12)
              .backgroundColor('#FFF8E1')
              .onChange((v: string) => {
                this.obsCode = v
              })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Column({ space: 8 }) {
            Text('主要蜜源').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(nectarTags209, (tag: string, i: number) => {
                Text(tag)
                  .fontSize(11)
                  .fontColor(this.obsNectar === i ? '#FFFFFF' : '#8D6E63')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(16)
                  .backgroundColor(this.obsNectar === i ? nectarColor209(tag) : '#FFF8E1')
                  .margin(4)
                  .onClick(() => {
                    this.obsNectar = i
                  })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#FF8F00')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FFF8E1')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.obsBees > 1) {
                this.obsBees -= 1
              }
            })
            Column({ space: 2 }) {
              Text('蜂群 ' + this.obsBees + ' 脾').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FF8F00')
              Text('目测巢脾数量').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#FF8F00')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FFF8E1')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.obsBees < 10) {
                this.obsBees += 1
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#6D4C41')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#EFEBE9')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.obsYield > 5) {
                this.obsYield -= 5
              }
            })
            Column({ space: 2 }) {
              Text('预估产蜜 ' + this.obsYield + ' 斤').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
              Text('按封盖面积估算').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#6D4C41')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#EFEBE9')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.obsYield < 80) {
                this.obsYield += 5
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('公开到蜂巢箱').fontSize(13).fontColor('#6D4C41')
              Text('其他蜂友可见你的观测').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Row() {
              Circle().width(18).height(18).fill('#FFFFFF')
            }
            .width(48)
            .height(26)
            .borderRadius(13)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(this.obsPublic ? '#FF8F00' : '#BDBDBD')
            .onClick(() => {
              this.obsPublic = !this.obsPublic
            })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#FFF8E1')

          Button() {
            Text('提交观测').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .height(48)
          .borderRadius(24)
          .backgroundColor('#6D4C41')
          .onClick(() => {
            this.obsCode = ''
          })
        }
        .width('100%')
        .padding({ left: 18, right: 18, top: 4, bottom: 24 })
      }
      .constraintSize({ maxHeight: 440 })
    }
    .width('100%')
    .height('100%')
    .padding({ top: 14 })
    .backgroundColor('#FFFFFF')
  }

  // ---------- 弹框3:编辑观测(抽屉,map 回写) ----------
  @Builder
  editSheet209() {
    Column({ space: 16 }) {
      Row({ space: 10 }) {
        Column().width(4).height(30).borderRadius(2).backgroundColor('#F4511E')
        Column({ space: 2 }) {
          Text('编辑蜂巢档案').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#4E342E')
          Text('修正脾数与预估产蜜').fontSize(10).fontColor('#8D6E63')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column() {
          Text('✕').fontSize(14).fontColor('#8D6E63')
        }
        .width(30)
        .height(30)
        .borderRadius(15)
        .backgroundColor('#FFF8E1')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showEditSheet = false
        })
      }
      .width('100%')

      Scroll() {
        Column({ space: 16 }) {
          Column({ space: 8 }) {
            Text('蜂箱编号').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
            TextInput({ placeholder: '输入新编号', text: this.editCode })
              .fontSize(13)
              .padding(12)
              .borderRadius(12)
              .backgroundColor('#FFF8E1')
              .onChange((v: string) => {
                this.editCode = v
              })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#FF8F00')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FFF8E1')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.editBees > 1) {
                this.editBees -= 1
              }
            })
            Column({ space: 2 }) {
              Text('蜂群 ' + this.editBees + ' 脾').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FF8F00')
              Text('复查后修正').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#FF8F00')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FFF8E1')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.editBees < 10) {
                this.editBees += 1
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#6D4C41')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#EFEBE9')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.editYield > 5) {
                this.editYield -= 5
              }
            })
            Column({ space: 2 }) {
              Text('预估产蜜 ' + this.editYield + ' 斤').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
              Text('按封盖复测修正').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#6D4C41')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#EFEBE9')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.editYield < 80) {
                this.editYield += 5
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('标记云认养').fontSize(13).fontColor('#6D4C41')
              Text('认养蜂箱显示金框').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Row() {
              Circle().width(18).height(18).fill('#FFFFFF')
            }
            .width(48)
            .height(26)
            .borderRadius(13)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(this.editTop ? '#FF8F00' : '#BDBDBD')
            .onClick(() => {
              this.editTop = !this.editTop
            })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#FFF8E1')

          Button() {
            Text('保存修改').fontSize(15).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .height(48)
          .borderRadius(24)
          .backgroundColor('#F4511E')
          .onClick(() => {
            this.hives = this.hives.map((h: Hive209, hi: number) => {
              if (hi === this.editIndex) {
                return {
                  id: h.id,
                  code: this.editCode === '' ? h.code : this.editCode,
                  nectar: h.nectar,
                  bees: this.editBees,
                  yield: this.editYield,
                  state: h.state,
                  watchers: h.watchers,
                  adopted: this.editTop
                }
              }
              return h
            })
            this.showEditSheet = false
          })
        }
        .width('100%')
        .padding({ left: 18, right: 18, top: 4, bottom: 24 })
      }
      .constraintSize({ maxHeight: 420 })
    }
    .width('100%')
    .height('100%')
    .padding({ top: 14 })
    .backgroundColor('#FFFFFF')
  }

  // ---------- 弹框4:删除观测(居中,filter) ----------
  @Builder
  delDialog209() {
    Column({ space: 14 }) {
      Column() {
        Text('🐝').fontSize(34)
      }
      .width(64)
      .height(64)
      .borderRadius(32)
      .backgroundColor('#FFF8E1')
      .justifyContent(FlexAlign.Center)

      Text('移除这条观测?').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#4E342E')
      Text('「' + (this.delIndex >= 0 && this.delIndex < this.hives.length ? this.hives[this.delIndex].code : '') + '」将从蜂巢箱列表移除,蜂群档案不再展示').fontSize(11).fontColor('#8D6E63').textAlign(TextAlign.Center)

      Row({ space: 10 }) {
        Column({ space: 2 }) {
          Text('保留摇蜜记录').fontSize(12).fontColor('#6D4C41')
          Text('仅移除观测卡').fontSize(9).fontColor('#90A4AE')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Row() {
          Circle().width(16).height(16).fill('#FFFFFF')
        }
        .width(44)
        .height(24)
        .borderRadius(12)
        .justifyContent(FlexAlign.Center)
        .backgroundColor(this.delKeepLog ? '#6D4C41' : '#BDBDBD')
        .onClick(() => {
          this.delKeepLog = !this.delKeepLog
        })
      }
      .width('100%')
      .padding(10)
      .borderRadius(10)
      .backgroundColor('#FFF8E1')

      Row({ space: 10 }) {
        Button() {
          Text('再想想').fontSize(14).fontColor('#6D4C41').fontWeight(FontWeight.Bold)
        }
        .layoutWeight(1)
        .height(44)
        .borderRadius(22)
        .backgroundColor('#FFF8E1')
        .onClick(() => {
          this.showDelDialog = false
        })

        Button() {
          Text('确认移除').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
        }
        .layoutWeight(1)
        .height(44)
        .borderRadius(22)
        .backgroundColor('#F4511E')
        .onClick(() => {
          this.hives = this.hives.filter((h: Hive209, hi: number) => hi !== this.delIndex)
          this.showDelDialog = false
        })
      }
      .width('100%')
    }
    .width('82%')
    .padding(20)
    .borderRadius(20)
    .backgroundColor('#FFFFFF')
    .alignItems(HorizontalAlign.Center)
  }

  // ---------- 弹框5:蜂巢详情(居中,图表+跳转联动) ----------
  @Builder
  detailDialog209() {
    Column({ space: 0 }) {
      Scroll() {
        Column({ space: 0 }) {
          Column({ space: 8 }) {
            Row({ space: 10 }) {
              Column() {
                Text('🍯').fontSize(30)
              }
              .width(56)
              .height(56)
              .borderRadius(28)
              .backgroundColor('#FFFFFF')
              .justifyContent(FlexAlign.Center)
              Column({ space: 3 }) {
                Text(this.detailIndex >= 0 && this.detailIndex < this.hives.length ? this.hives[this.detailIndex].code + ' 号箱' : '').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
                Text((this.detailIndex >= 0 && this.detailIndex < this.hives.length ? this.hives[this.detailIndex].nectar : '') + '蜜源 · ' + (this.detailIndex >= 0 && this.detailIndex < this.hives.length ? this.hives[this.detailIndex].bees : 0) + ' 脾群势').fontSize(11).fontColor('#FFF8E1')
              }
              .alignItems(HorizontalAlign.Start)
              Text('').layoutWeight(1)
              Column() {
                Text('✕').fontSize(13).fontColor('#FFFFFF')
              }
              .width(28)
              .height(28)
              .borderRadius(14)
              .backgroundColor('rgba(255,255,255,0.2)')
              .justifyContent(FlexAlign.Center)
              .onClick(() => {
                this.showDetailDialog = false
              })
            }
            .width('100%')
          }
          .width('100%')
          .padding(18)
          .linearGradient({ angle: 135, colors: [['#FFB300', 0], ['#5D4037', 1]] })

          Column({ space: 14 }) {
            Row({ space: 8 }) {
              Column({ space: 2 }) {
                Text((this.detailIndex >= 0 && this.detailIndex < this.hives.length ? this.hives[this.detailIndex].yield : 0) + '斤').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF8F00')
                Text('预估产蜜量').fontSize(9).fontColor('#8D6E63')
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
              .padding({ top: 10, bottom: 10 })
              .borderRadius(10)
              .backgroundColor('#FFF8E1')
              Column({ space: 2 }) {
                Text((this.detailIndex >= 0 && this.detailIndex < this.hives.length ? this.hives[this.detailIndex].watchers : 0) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
                Text('累计围观人次').fontSize(9).fontColor('#8D6E63')
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
              .padding({ top: 10, bottom: 10 })
              .borderRadius(10)
              .backgroundColor('#EFEBE9')
              Column({ space: 2 }) {
                Text(this.detailIndex >= 0 && this.detailIndex < this.hives.length ? this.hives[this.detailIndex].state : '').fontSize(14).fontWeight(FontWeight.Bold).fontColor(hiveStateColor209(this.detailIndex >= 0 && this.detailIndex < this.hives.length ? this.hives[this.detailIndex].state : ''))
                Text('当前状态').fontSize(9).fontColor('#8D6E63')
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
              .padding({ top: 10, bottom: 10 })
              .borderRadius(10)
              .backgroundColor('#EFEBE9')
            }
            .width('100%')

            Column({ space: 8 }) {
              Text('近 7 日箱内温度').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4E342E')
              Row({ space: 6 }) {
                ForEach(this.tempLogs, (l: TempLog209) => {
                  Column({ space: 4 }) {
                    Text(l.temp + '°').fontSize(8).fontColor('#FF8F00')
                    Column()
                      .width(16)
                      .height(l.temp)
                      .borderRadius({ topLeft: 4, topRight: 4 })
                      .linearGradient({ angle: 180, colors: [['#FFD54F', 0], ['#FF8F00', 1]] })
                    Text(l.day).fontSize(8).fontColor('#8D6E63')
                  }
                  .alignItems(HorizontalAlign.Center)
                  .layoutWeight(1)
                }, (l: TempLog209) => ('d' + l.day))
              }
              .width('100%')
              .alignItems(VerticalAlign.Bottom)
              .height(80)
            }
            .width('100%')
            .padding(12)
            .borderRadius(12)
            .backgroundColor('#FFFDE7')

            Column({ space: 8 }) {
              Text('蜂群档案').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4E342E')
              Row({ space: 8 }) {
                Text('蜂王').fontSize(11).fontColor('#8D6E63')
                Text('新王 · 产卵积极').fontSize(11).fontColor('#6D4C41').fontWeight(FontWeight.Bold)
                Text('').layoutWeight(1)
                Text('已标记红点').fontSize(10).fontColor('#F4511E')
              }
              .width('100%')
              Row({ space: 8 }) {
                Text('健康').fontSize(11).fontColor('#8D6E63')
                Text('无螨 · 子脾整齐').fontSize(11).fontColor('#6D4C41').fontWeight(FontWeight.Bold)
                Text('').layoutWeight(1)
                Text('A级').fontSize(10).fontColor('#7CB342')
              }
              .width('100%')
            }
            .width('100%')
            .padding(12)
            .borderRadius(12)
            .backgroundColor('#FFFDE7')
            .alignItems(HorizontalAlign.Start)

            Button() {
              Text('认养TA并连线取蜜').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            }
            .width('100%')
            .height(44)
            .borderRadius(22)
            .backgroundColor('#FF8F00')
            .onClick(() => {
              this.showDetailDialog = false
              this.showJoinSheet = true
            })
          }
          .width('100%')
          .padding(16)
        }
        .width('100%')
      }
      .constraintSize({ maxHeight: 480 })
    }
    .width('86%')
    .borderRadius(20)
    .backgroundColor('#FFFFFF')
    .clip(true)
  }
}

// ================= Tab1:取蜜房(直播) =================

@Component
struct LiveTab209 {
  @State localMic: boolean = true
  @State localCam: boolean = true
  @State localSmoke: boolean = true
  @Prop hives: Hive209[] = []
  @Prop keepers: Keeper209[] = []
  @Prop harvestSteps: HarvestStep209[] = []
  @Prop barrages: Barrage209[] = []
  onStep: (i: number) => void = () => {
  }
  onJoin: () => void = () => {
  }

  build() {
    Column({ space: 12 }) {
      // 四机位
      Grid() {
        GridItem() {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Column() {
                Text('🐝').fontSize(18)
              }
              .width(34)
              .height(34)
              .borderRadius(17)
              .backgroundColor('rgba(255,255,255,0.25)')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text('主镜 · 开箱提脾').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text('追花老丈 · 洋槐场').fontSize(9).fontColor('#FFE082')
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding(10)
            Text('').layoutWeight(1)
            Row({ space: 6 }) {
              Text('● LIVE').fontSize(9).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
              Text(spinningHiveCount209(this.hives) + ' 箱在摇').fontSize(9).fontColor('#FFE082')
            }
            .width('100%')
            .padding(8)
          }
          .width('100%')
          .height('100%')
          .borderRadius(12)
          .padding(6)
          .linearGradient({ angle: 150, colors: [['#FF8F00', 0], ['#E65100', 1]] })
          .onClick(() => {
            this.onJoin()
          })
        }
        GridItem() {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Column() {
                Text('🌀').fontSize(18)
              }
              .width(34)
              .height(34)
              .borderRadius(17)
              .backgroundColor('rgba(255,255,255,0.25)')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text('摇蜜机位').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text('离心出蜜特写').fontSize(9).fontColor('#FFE082')
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding(10)
            Text('').layoutWeight(1)
            Row({ space: 6 }) {
              Text('●').fontSize(9).fontColor('#FF1744')
              Text('已出 12 罐').fontSize(9).fontColor('#FFE082')
            }
            .width('100%')
            .padding(8)
          }
          .width('100%')
          .height('100%')
          .borderRadius(12)
          .padding(6)
          .linearGradient({ angle: 150, colors: [['#F4511E', 0], ['#BF360C', 1]] })
        }
        GridItem() {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Column() {
                Text('🖼️').fontSize(18)
              }
              .width(34)
              .height(34)
              .borderRadius(17)
              .backgroundColor('rgba(255,255,255,0.25)')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text('巢脾特写位').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text('封盖率检测中').fontSize(9).fontColor('#DCEDC8')
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding(10)
            Text('').layoutWeight(1)
            Row({ space: 6 }) {
              Text('●').fontSize(9).fontColor('#76FF03')
              Text('封盖 92%').fontSize(9).fontColor('#DCEDC8')
            }
            .width('100%')
            .padding(8)
          }
          .width('100%')
          .height('100%')
          .borderRadius(12)
          .padding(6)
          .linearGradient({ angle: 150, colors: [['#558B2F', 0], ['#33691E', 1]] })
        }
        GridItem() {
          Column({ space: 4 }) {
            Row({ space: 6 }) {
              Column() {
                Text('🧑‍🌾').fontSize(18)
              }
              .width(34)
              .height(34)
              .borderRadius(17)
              .backgroundColor('rgba(255,255,255,0.25)')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text('我的围观点').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text(this.localCam ? '镜头开启中' : '镜头已关闭').fontSize(9).fontColor('#FFE082')
              }
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            .padding(10)
            Text('').layoutWeight(1)
            Row({ space: 6 }) {
              Text(this.localMic ? '🎙 开麦' : '🔇 静音').fontSize(9).fontColor(this.localMic ? '#FFFFFF' : '#BDBDBD')
              Text(this.localSmoke ? '💨 轻烟中' : '💨 无烟').fontSize(9).fontColor(this.localSmoke ? '#FFF59D' : '#BDBDBD')
            }
            .width('100%')
            .padding(8)
          }
          .width('100%')
          .height('100%')
          .borderRadius(12)
          .padding(6)
          .backgroundColor(this.localCam ? '#8D6E63' : '#37474F')
          .onClick(() => {
            this.localCam = !this.localCam
          })
        }
      }
      .columnsTemplate('1fr 1fr')
      .rowsTemplate('1fr 1fr')
      .height(240)
      .width('100%')

      // 工具条
      Row({ space: 10 }) {
        Column({ space: 3 }) {
          Text(this.localMic ? '🎙️' : '🔇').fontSize(18)
          Text('对讲').fontSize(9).fontColor('#6D4C41')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 8, bottom: 8 })
        .borderRadius(12)
        .backgroundColor(this.localMic ? '#FFE082' : '#FFF8E1')
        .onClick(() => {
          this.localMic = !this.localMic
        })
        Column({ space: 3 }) {
          Text(this.localCam ? '📹' : '📷').fontSize(18)
          Text('镜头').fontSize(9).fontColor('#6D4C41')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 8, bottom: 8 })
        .borderRadius(12)
        .backgroundColor(this.localCam ? '#FFE082' : '#FFF8E1')
        .onClick(() => {
          this.localCam = !this.localCam
        })
        Column({ space: 3 }) {
          Text(this.localSmoke ? '💨' : '🚫').fontSize(18)
          Text('喷烟').fontSize(9).fontColor('#6D4C41')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 8, bottom: 8 })
        .borderRadius(12)
        .backgroundColor(this.localSmoke ? '#FFCC80' : '#FFF8E1')
        .onClick(() => {
          this.localSmoke = !this.localSmoke
        })
        Column({ space: 3 }) {
          Text('🍯').fontSize(18)
          Text('连蜜').fontSize(9).fontColor('#FFFFFF')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 8, bottom: 8 })
        .borderRadius(12)
        .backgroundColor('#FF8F00')
        .onClick(() => {
          this.onJoin()
        })
      }
      .width('100%')

      // 取蜜六步
      Column({ space: 10 }) {
        Text('取蜜六步法').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#4E342E')
        ForEach(this.harvestSteps, (s: HarvestStep209, i: number) => {
          Row({ space: 10 }) {
            Column() {
              Text(s.done ? '✓' : (i + 1) + '').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            }
            .width(24)
            .height(24)
            .borderRadius(12)
            .backgroundColor(s.done ? '#7CB342' : '#FFD54F')
            .justifyContent(FlexAlign.Center)
            Column({ space: 2 }) {
              Text(s.title).fontSize(12).fontColor(s.done ? '#4E342E' : '#8D6E63').fontWeight(FontWeight.Bold)
              Text(s.tip).fontSize(9).fontColor('#BCAAA4')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text(s.done ? '已完成' : '进行中').fontSize(9).fontColor(s.done ? '#7CB342' : '#FF8F00')
          }
          .width('100%')
          .padding(10)
          .borderRadius(10)
          .backgroundColor(s.done ? '#F1F8E9' : '#FFFFFF')
          .onClick(() => {
            this.onStep(i)
          })
        }, (s: HarvestStep209) => ('s' + s.id))
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')

      // 今晚摇蜜看点
      Column({ space: 10 }) {
        Text('今晚摇蜜看点').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#4E342E')
        ForEach(this.hives, (h: Hive209) => {
          if (h.state === '摇蜜中' || h.state === '割蜜盖') {
            Row({ space: 10 }) {
              Column() {
                Text('🍯').fontSize(14)
              }
              .width(30)
              .height(30)
              .borderRadius(8)
              .backgroundColor('#FFF8E1')
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text(h.code + ' 号箱 · ' + h.nectar + '蜜').fontSize(12).fontColor('#4E342E').fontWeight(FontWeight.Bold)
                Text(h.bees + ' 脾群势 · 预估 ' + h.yield + ' 斤').fontSize(9).fontColor('#8D6E63')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Column() {
                Text(h.state).fontSize(9).fontColor('#FFFFFF')
              }
              .padding({ left: 8, right: 8, top: 4, bottom: 4 })
              .borderRadius(8)
              .backgroundColor(hiveStateColor209(h.state))
            }
            .width('100%')
            .padding(8)
            .borderRadius(10)
            .backgroundColor('#FFFDE7')
          }
        }, (h: Hive209) => ('h' + h.id))
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')

      // 弹幕
      Column({ space: 6 }) {
        Text('围蜜弹幕').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
        ForEach(this.barrages, (b: Barrage209, i: number) => {
          Row({ space: 6 }) {
            Text('#' + (i + 1)).fontSize(8).fontColor('#FF8F00')
            Text(b.text).fontSize(10).fontColor('#5D4037')
          }
          .width('100%')
          .padding(6)
          .borderRadius(8)
          .backgroundColor(i % 2 === 0 ? '#FFF8E1' : '#FFECB3')
        }, (b: Barrage209) => ('b' + b.id))
      }
      .width('100%')
      .padding(10)
      .borderRadius(12)
      .backgroundColor('#FFECB3')
    }
    .width('100%')
  }
}

// ================= Tab2:蜂巢箱 =================

@Component
struct HiveTab209 {
  @Prop hives: Hive209[] = []
  onAdd: () => void = () => {
  }
  onDetail: (i: number) => void = () => {
  }
  onEdit: (i: number) => void = () => {
  }
  onDel: (i: number) => void = () => {
  }
  onAdopt: (i: number) => void = () => {
  }

  build() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text(this.hives.length + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF8F00')
          Text('在册蜂箱').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#FFF8E1')
        Column({ space: 2 }) {
          Text(adoptedHiveCount209(this.hives) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#6D4C41')
          Text('云认养').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#EFEBE9')
        Column({ space: 2 }) {
          Text(spinningHiveCount209(this.hives) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#F4511E')
          Text('摇蜜/割盖').fontSize(9).fontColor('#8D6E63')
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(10)
        .backgroundColor('#FBE9E7')
      }
      .width('100%')

      // 新增观测入口
      Row({ space: 10 }) {
        Column({ space: 2 }) {
          Text('🐝').fontSize(14)
        }
        .width(36)
        .height(36)
        .borderRadius(18)
        .backgroundColor('#6D4C41')
        .justifyContent(FlexAlign.Center)
        Column({ space: 2 }) {
          Text('新增蜂巢观测').fontSize(12).fontColor('#4E342E').fontWeight(FontWeight.Bold)
          Text('记箱日志 · 同步蜂群档案').fontSize(9).fontColor('#8D6E63')
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Column() {
          Text('+ 记录').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
        }
        .padding({ left: 14, right: 14, top: 8, bottom: 8 })
        .borderRadius(16)
        .backgroundColor('#6D4C41')
        .onClick(() => {
          this.onAdd()
        })
      }
      .width('100%')
      .padding(12)
      .borderRadius(14)
      .backgroundColor('#FFFFFF')

      // 蜜源占比堆叠条
      Column({ space: 8 }) {
        Text('蜂群蜜源构成').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#4E342E')
        Row() {
          ForEach(nectarCounts209(this.hives), (c: NectarCount209) => {
            Row() {
              Text(c.label + ' ' + c.count).fontSize(8).fontColor('#FFFFFF')
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
            .backgroundColor(c.color)
          }, (c: NectarCount209) => (c.label + c.count))
        }
        .width('100%')
        .height(22)
        .borderRadius(11)
        .clip(true)
        Row({ space: 10 }) {
          ForEach(nectarCounts209(this.hives), (c: NectarCount209) => {
            Row({ space: 4 }) {
              Column().width(8).height(8).borderRadius(4).backgroundColor(c.color)
              Text(c.label).fontSize(9).fontColor('#8D6E63')
            }
          }, (c: NectarCount209) => ('lg' + c.label))
        }
        .width('100%')
      }
      .width('100%')
      .padding(12)
      .borderRadius(12)
      .backgroundColor('#FFFFFF')
      .alignItems(HorizontalAlign.Start)

      ForEach(this.hives, (h: Hive209, i: number) => {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            Column() {
              Text('🐝').fontSize(20)
            }
            .width(44)
            .height(44)
            .borderRadius(12)
            .backgroundColor(h.adopted ? '#FFECB3' : '#FFF8E1')
            .justifyContent(FlexAlign.Center)
            Column({ space: 3 }) {
              Text(h.code + ' 号箱').fontSize(13).fontColor('#4E342E').fontWeight(FontWeight.Bold)
              Row({ space: 6 }) {
                Column() {
                  Text(h.nectar).fontSize(8).fontColor('#FFFFFF')
                }
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .borderRadius(6)
                .backgroundColor(nectarColor209(h.nectar))
                Text(h.bees + ' 脾').fontSize(9).fontColor('#8D6E63')
                Text('围观 ' + h.watchers).fontSize(9).fontColor('#BCAAA4')
              }
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Column() {
              Text(h.adopted ? '💛' : '🤍').fontSize(18)
            }
            .width(32)

  }
}


九、总结

在这里插入图片描述

蜂颂云养蜂场应用展示了HarmonyOS ArkTS在垂直行业应用开发中的完整能力。从数据模型定义、工具函数封装、状态管理架构到UI渲染管线,每一个环节都体现了声明式编程范式的核心思想——数据驱动视图、状态管理统一、组件职责分明。应用最突出的设计亮点在于蜂巢格Tab导航的拟物化设计,上窄下宽的两段式蜂巢形按钮既保留了底部导航的易用性,又赋予了应用独特的视觉识别度,这种将行业特征融入交互元素的做法值得在各类主题应用中借鉴。

从技术工程角度评估,应用的弹窗系统设计成熟稳健。bindSheet和bindContentCover分别承担了底部抽屉和居中遮罩两种弹窗形态,通过$$双向绑定实现显隐控制,通过@Builder封装弹窗UI内容。预约取蜜、新增观测、编辑档案三个抽屉各自承载完整的表单逻辑——标签选择、数量加减器、开关切换、价格计算等交互模式一应俱全。删除确认和蜂巢详情两个居中对话框则承担了信息展示和操作确认的职责。特别是详情对话框中"认养TA并连线取蜜"按钮实现了弹窗联动——从详情查看无缝跳转到预约下单,形成完整的业务闭环。

温度柱状图通过Column高度绑定温度值实现,蜜源热度横条通过Row的width百分比实现,蜜源占比堆叠条通过多个Row在Flex容器中的自动比例分配实现,本周产蜜柱图通过Column高度绑定罐数乘以系数实现。这些可视化方案不依赖任何第三方图表库,完全基于Row、Column、ForEach等基础组件组合构建,既保证了渲染性能又减少了包体积。整体而言,该应用的架构设计、状态管理和交互实现为HarmonyOS垂直行业应用开发提供了一个完整的参考范本,其设计理念和技术选型对于构建类似的直播围观型电商应用具有重要的借鉴价值。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐