各位开发者朋友,欢迎来到今天的技术演讲。想象这样一个场景:你身处内陆城市,却想感受南海涠洲岛的水下世界——不是看视频,而是跟着教练实时连线跟潜,认养一株属于你的珊瑚,看它在你的关注下从白化预警走向恢复。今天,我要带各位潜入一款基于HarmonyOS ArkTS构建的云深潜应用的代码深处。

这款应用用深海蓝#01579B和珊瑚橙#FF7043构建了一个完整的垂直社区:从跟潜房直播到珊瑚认养墙,从潜点热力到潜伴匹配,六个Tab串联起一个潜水员从入门到进阶的完整旅程。而在技术层面,它用底部氧气胶囊导航、Flex弹性标签布局和不可变数据流,讲述了一个关于"如何把线下体验搬到云端"的工程故事。

今天这场演讲不是API文档的复述,而是一次代码深潜——我们会从氧气胶囊的气泡特效讲到珊瑚认养的map回写,从下潜六步流程的交互设计讲到水温柱图的纯布局渲染,看看HarmonyOS的声明式UI如何让一个潜水社区在手机屏幕上"活过来"。


开场故事:一次失败的产品会议和它的技术救赎

在这里插入图片描述

三个月前,我参加了一场产品评审会。产品经理拿着原型图说:"我们要做一个潜水跟练应用,教练在线上带练,学员在客厅里跟着做耳压平衡的动作。要有直播画面、要有弹幕、要有认养系统、要有训练课程、还要有潜伴匹配。"会议室里安静了五秒钟,然后有人问:“这和市面上那些健身App有什么区别?”

区别在于,潜水是一个有门槛、有风险、有情感连接的运动。你不是在客厅里跳操,你是在准备一次真正下潜到海底的旅程。所以这个应用不能只是"跟着视频做动作"——它需要让你感受到海底的氛围,让你对某株具体的珊瑚产生牵挂,让你认识一群和你一样热爱海洋的人。这些情感连接,全都要靠技术来承载。

当我们把这个产品需求翻译成技术语言时,它变成了这样一张蓝图:一个底部导航条要长成氧气胶囊的形状,上面飘着气泡;一个珊瑚认养列表要支持增删改查和置顶;一个下潜流程要有可勾选的步骤清单;一个直播页要有四宫格画面和弹幕。而承载这一切的,是HarmonyOS ArkTS的声明式UI框架。今天,我要带各位走进这段从产品构想到代码实现的全旅程。


第一幕:氧气胶囊导航的故事——让TabBar长出气泡

在这里插入图片描述

各位请看这段代码。这是我们应用里最引人注目的视觉元素——底部氧气胶囊导航。在传统App里,底部TabBar通常是一个朴素的图标加文字排列,选中态换一个颜色就完了。但我们的设计团队说:“这是潜水应用,TabBar要让人联想到氧气瓶上的气泡。”

// ---------- 底部「氧气胶囊」tab ----------
@Builder
tabBar202() {
  Column({ space: 6 }) {
    Row({ space: 16 }) {
      Text('').width(8).height(8).borderRadius(4).backgroundColor('#4FC3F7').opacity(0.7)
      Text('').width(5).height(5).borderRadius(3).backgroundColor('#81D4FA').opacity(0.5)
      Text('').width(10).height(10).borderRadius(5).backgroundColor('#4FC3F7').opacity(0.4)
      Text('').width(6).height(6).borderRadius(3).backgroundColor('#81D4FA').opacity(0.6)
      Text('').width(8).height(8).borderRadius(4).backgroundColor('#4FC3F7').opacity(0.5)
      Text('').width(5).height(5).borderRadius(3).backgroundColor('#81D4FA').opacity(0.7)
    }
    Row({ space: 6 }) {
      ForEach(this.tabs202, (t: string, i: number) => {
        Column({ space: 3 }) {
          Text(this.tabEmojis202[i]).fontSize(17)
          Text(t)
            .fontSize(10)
            .fontColor(this.tabIndex1 === i ? '#FFFFFF' : '#546E7A')
            .fontWeight(this.tabIndex1 === i ? FontWeight.Bold : FontWeight.Normal)
        }
        .padding({ left: 11, right: 11, top: 8, bottom: 8 })
        .borderRadius(21)
        .backgroundColor(this.tabIndex1 === i ? '#01579B' : '#F1F8FB')
        .shadow({
          radius: this.tabIndex1 === i ? 10 : 0,
          color: '#660288D1',
          offsetY: 3
        })
        .scale({ x: this.tabIndex1 === i ? 1.06 : 1, y: this.tabIndex1 === i ? 1.06 : 1 })
        .animation({ duration: 180 })
        .onClick(() => {
          this.tabIndex1 = i
        })
      }, (t: string) => t)
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceEvenly)
  }
  .backgroundColor('#FFFFFF')
  .shadow({ radius: 12, color: '#140288D1', offsetY: -4 })
}

各位,请先看这段代码最上方的那一行——六个Text组件,大小不一(从5到10像素),颜色在#4FC3F7#81D4FA之间交替,透明度从0.4到0.7随机分布。这六个小圆点不是导航按钮,它们是纯装饰性的气泡。当用户第一次打开应用,看到TabBar上方飘着大小不一的蓝色小圆点,大脑会自动联想到水中上升的气泡——这就是设计语言的胜利。

我特别想强调的是justifyContent(FlexAlign.SpaceEvenly)这个属性。它让六个Tab均匀分布在整个宽度上,而不是挤在左侧或右侧。在ArkTS中,FlexAlign有Start、Center、End、SpaceBetween、SpaceAround和SpaceEvenly六种对齐模式。SpaceEvenly是最均匀的——每个Tab之间的间距完全相同,包括首尾到边缘的间距。这种排版让导航条在视觉上保持了完美的平衡感。

然后看每个Tab项的样式。选中态用深海蓝#01579B填充背景,加10像素半径的蓝色阴影#660288D1,再scale放大1.06倍,配180毫秒动画。未选中态用浅蓝#F1F8FB背景。每个Tab项的borderRadius是21——这是个全圆角胶囊形状。这些属性叠加在一起,就创造出了"氧气胶囊"的视觉效果:选中时胶囊变蓝发光微微膨胀,未选中时胶囊呈浅色安静待命。


第二幕:珊瑚认养的故事——从列表到map回写

在这里插入图片描述

现在让我们潜入应用的核心业务——珊瑚认养系统。这是整个应用最有情感连接的功能:用户认养一株珊瑚,给它起名字,追踪它的状态,在它白化预警时收到通知。

interface Coral202 {
  id: number
  name: string
  sea: string
  type: string
  amount: number
  state: string
  days: number
  top: boolean
}

@State corals: Coral202[] = [
  { id: 1, name: '小鹿角', sea: '南海涠洲', type: '鹿角珊瑚', amount: 120, state: '认养中', days: 86, top: true },
  { id: 2, name: '老脑珊瑚', sea: '蜈支洲岛', type: '脑珊瑚', amount: 200, state: '白化预警', days: 240, top: false },
  { id: 3, name: '桌面皇后', sea: '诗巴丹', type: '桌珊瑚', amount: 300, state: '认养中', days: 45, top: false },
  { id: 4, name: '紫软柳', sea: '大堡礁', type: '软珊瑚', amount: 150, state: '已恢复', days: 130, top: false },
  // ...
]

各位请注意这个数据结构。每株珊瑚有一个top布尔字段——这决定了它是否置顶在珊瑚墙首位。还有一个state字段,它的值可能是"认养中"、“白化预警"或"已恢复”。这三个状态分别对应珊瑚橙#FF7043、红色#E53935和绿色#26A69A

现在,当我告诉用户"你可以编辑你认养的珊瑚"时,技术层面发生了什么?请看编辑弹窗的保存逻辑:

Button() {
  Text('保存修改').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
}
.backgroundColor('#FF7043')
.onClick(() => {
  this.corals = this.corals.map((c: Coral202, i: number) => i === this.editIndex ? {
    id: c.id,
    name: this.editName === '' ? c.name : this.editName,
    sea: seaTags202[this.editSea],
    type: c.type,
    amount: this.editAmount,
    state: c.state,
    days: c.days,
    top: this.editTop
  } : c)
  this.showEditSheet = false
})

各位,这段代码的核心是this.corals.map(...)map方法遍历整个珊瑚数组,当索引等于editIndex时,返回一个全新的珊瑚对象——用表单中的新值替换旧值,但保留idtypestatedays不变(因为编辑弹窗不允许修改这些字段)。当索引不等于editIndex时,原样返回原对象。

为什么用map而不是直接修改数组元素?因为HarmonyOS ArkTS的@State是通过引用变化来检测数据更新的。如果你写this.corals[editIndex].name = '新名字',数组引用没变,ArkTS可能不会触发重新渲染。但map返回的是一个全新的数组对象,引用变了,@State立即感知到变化并驱动UI更新。这就是不可变数据更新的精髓——通过创建新引用来触发响应式更新。

这里还有一个细节值得各位注意:name: this.editName === '' ? c.name : this.editName。这个三元表达式实现了"空值回退"——如果用户在编辑弹窗里把名称清空了再保存,不会存入空字符串,而是回退到原始名称。这是一种轻量级的表单校验,虽然没有在UI上给用户提示,但在数据层面防止了空值污染。


第三幕:下潜六步流程的故事——交互式步骤清单

在这里插入图片描述

在跟潜房直播页,我们设计了一个"标准下潜六步流程"的交互式清单。这个设计的灵感来自真实的潜水教学——教练在岸上Briefing时会逐步讲解,学员逐步确认。我们把这种线下教学节奏搬到了线上。

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

@State steps: Step202[] = [
  { id: 1, title: '装备检查', tip: '气瓶 200bar · 配重 4kg · BCD 充排气正常', done: true },
  { id: 2, title: '岸上 Briefing', tip: '手势复核 · 上升速率 · 伙伴制确认', done: true },
  { id: 3, title: '入水下潜', tip: 'BCD 排气 · 沿导索缓降 · 面镜平衡', done: true },
  { id: 4, title: '耳压平衡', tip: '每下潜 2 米做一次 · 捏鼻轻鼓气', done: false },
  { id: 5, title: '中性悬浮', tip: '呼吸控浮 · 脚蹼慢频率 · 不碰珊瑚', done: false },
  { id: 6, title: '安全停留', tip: '5 米停留 3 分钟 · 象拔信号 · 缓慢出水', done: false }
]

请看这六个步骤的tip字段。每一条都是真实的潜水操作要点——"气瓶200bar"是标准满压、“配重4kg"是常见配置、“BCD充排气正常"是下水前必检项。这些细节让应用不只是"好看”,而是"有用”——一个准备考OW证书的潜水员,可以把这个清单当作下水前的自检工具。

现在看交互逻辑。当用户点击某个步骤时,它的done状态会被翻转:

LiveTab202({
  barrages: this.barrages,
  steps: this.steps,
  onStep: (i: number) => {
    this.steps = this.steps.map((s: Step202, idx: number) => idx === i ? {
      id: s.id,
      title: s.title,
      tip: s.tip,
      done: !s.done
    } : s)
  }
})

各位请看这个onStep回调。当用户点击第i个步骤时,父组件收到回调,用map遍历steps数组,将第i项的done取反(!s.done),其他项保持不变。这和珊瑚编辑的map模式完全一致——通过不可变更新触发响应式渲染。

在步骤的UI渲染中,完成态和未完成态有明确的视觉区分。完成态的序号背景色是绿色#26A69A、文字颜色也是绿色,显示✓符号;未完成态的序号背景色是灰色#B0BEC5,显示数字序号。步骤之间还有一条2像素宽的灰色连接线backgroundColor('#CFD8DC'),通过layoutWeight(1)自适应高度填充。这种垂直时间线的视觉设计在移动端步骤引导中非常有效。


第四幕:气泡弹幕的故事——错位排列营造氛围

在这里插入图片描述

弹幕是直播页的氛围担当。在传统直播平台,弹幕是从右向左滚动的。但在我们的潜水直播页,弹幕是静态的、错位排列的——就像水下气泡缓慢上升时的大小不一、位置随机。

interface Barrage202 {
  id: number
  text: string
}

@State barrages: Barrage202[] = [
  { id: 1, text: '教练我的耳压平衡还是痛!' },
  { id: 2, text: '刚才那只海龟也太大了8' },
  { id: 3, text: '求沉船机位多停留两分钟' },
  { id: 4, text: '中性浮力练了三个月终于不转圈了' },
  { id: 5, text: '小鹿角又长高一截!' },
  { id: 6, text: '今晚夜潜记得带头灯' },
  { id: 7, text: '气量控制真的有用,亲测' },
  { id: 8, text: '围观破万了家人们' }
]

// 弹幕渲染
Column({ space: 8 }) {
  Text('弹幕 · 围潜氛围组').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#37474F')
  ForEach(this.barrages, (b: Barrage202, i: number) => {
    Text(b.text)
      .fontSize(11)
      .fontColor('#37474F')
      .padding({ left: 10, right: 10, top: 5, bottom: 5 })
      .borderRadius(12)
      .backgroundColor(i % 3 === 0 ? '#E0F7FA' : '#E0F2F1')
      .margin({ left: (i % 3) * 36 })
      .alignSelf(ItemAlign.Start)
  }, (b: Barrage202) => b.id.toString())
}

各位,请特别注意.margin({ left: (i % 3) * 36 })这一行。这是弹幕错位排列的核心。当i % 3等于0时,左边距为0;等于1时,左边距为36像素;等于2时,左边距为72像素。三条弹幕为一组,形成阶梯状错位。配合i % 3 === 0 ? '#E0F7FA' : '#E0F2F1'的交替背景色,弹幕区域呈现出一种轻松的、不规则的视觉节奏——就像水下气泡不会整齐排列一样。

alignSelf(ItemAlign.Start)也是一个关键属性。它让每条弹幕左对齐,而不是在Column中居中。如果不加这个属性,弹幕会自动居中排列,错位效果就会大打折扣。这行代码虽短,但体现了开发者对布局细节的把控。


第五幕:珊瑚详情的故事——水温柱图与渐变头图

在这里插入图片描述

当用户点击某株珊瑚查看详情时,会弹出一个居中弹框。这个弹框的顶部是一个渐变头图,下方是统计行和月度水温柱图。让我们看看这个详情弹窗的技术实现。

// ---------- 弹框五:珊瑚详情(居中弹框) ----------
@Builder
detailDialog202() {
  Column() {
    Scroll() {
      Column({ space: 0 }) {
        // 渐变头图
        Column({ space: 6 }) {
          Text('🪸').fontSize(40)
          Text(this.corals[this.detailIndex].name)
            .fontSize(19).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
          Text(this.corals[this.detailIndex].type + ' · ' + this.corals[this.detailIndex].sea)
            .fontSize(11).fontColor('#B3E5FC')
        }
        .width('100%')
        .padding({ top: 28, bottom: 22 })
        .linearGradient({ angle: 140, colors: [['#0288D1', 0], ['#01579B', 1]] })

        // 统计行
        Row() {
          Column({ space: 3 }) {
            Text(this.corals[this.detailIndex].days + ' 天')
              .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#01579B')
            Text('认养时长').fontSize(9).fontColor('#90A4AE')
          }
          Column({ space: 3 }) {
            Text('¥ ' + this.corals[this.detailIndex].amount)
              .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FF7043')
            Text('累计认养').fontSize(9).fontColor('#90A4AE')
          }
          Column({ space: 3 }) {
            Text(this.corals[this.detailIndex].state)
              .fontSize(15).fontWeight(FontWeight.Bold)
              .fontColor(coralStateColor202(this.corals[this.detailIndex].state))
            Text('当前状态').fontSize(9).fontColor('#90A4AE')
          }
        }

        // 月度水温柱图
        Column({ space: 8 }) {
          Text('所在海域月度水温(℃)').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#37474F')
          Row({ space: 8 }) {
            ForEach(this.tempLogs, (t: TempLog202) => {
              Column({ space: 4 }) {
                Text(t.temp + '').fontSize(9).fontColor('#0288D1')
                Text('')
                  .width(14)
                  .height(t.temp * 3)
                  .borderRadius(4)
                  .linearGradient({ angle: 180, colors: [['#4FC3F7', 0], ['#01579B', 1]] })
                Text(t.month).fontSize(8).fontColor('#78909C')
              }
            }, (t: TempLog202) => t.month)
          }
          .alignItems(VerticalAlign.Bottom)
        }
      }
      .constraintSize({ maxHeight: '80%' })
    }
    .scrollBar(BarState.Off)
  }
  .width('88%')
  .borderRadius(18)
  .backgroundColor('#FFFFFF')
}

各位请看渐变头图的实现。一个Column通过linearGradient({ angle: 140, colors: [['#0288D1', 0], ['#01579B', 1]] })设置了从浅蓝到深海蓝的140度渐变。珊瑚emoji和名称文字以白色字体叠在渐变背景上,营造了"从水面望向深海"的视觉隐喻。140度角意味着渐变方向从左上到右下,这比标准的90度垂直渐变更有动感。

水温柱图是整个应用中最精巧的纯布局图表之一。每根柱子的高度是t.temp * 3——将水温值乘以3得到像素高度。比如28度对应84像素,22度对应66像素。柱子使用linearGradient从浅蓝#4FC3F7渐变到深海蓝#01579B,配合alignItems(VerticalAlign.Bottom)底部对齐。这种"数据值直接乘以缩放因子"的方式简单粗暴,但在水温这种数值范围已知(20-30度)的场景下完全够用。柱子上方显示温度数值,下方显示月份,信息完整且无冗余。


应用核心数据流

在这里插入图片描述

Tab 0 跟潜房

点击步骤

Tab 1 珊瑚墙

点击认养

点击编辑

点击删除

点击详情

map回写

filter删除

concat新增

Tab 2 潜点

Tab 3 训练

Tab 4 潜伴团

Tab 5 我的

应用启动

渲染深海蓝头部

氧气胶囊导航条

tabIndex1判断

四宫格直播画面

下潜六步流程

map翻转done状态

弹幕错位渲染

珊瑚列表+统计

打开新增认养抽屉

打开编辑认养抽屉

打开删除确认弹框

打开珊瑚详情弹框

更新corals数组

潜点列表+本周柱图

训练课程+人气条

潜伴列表+在线状态

个人卡+下潜日志


第六幕:珊瑚种类统计的故事——动态堆叠条

在珊瑚墙页面,有一个动态计算的珊瑚种类占比堆叠条。它的特别之处在于:不是从静态数据读取,而是实时从珊瑚数组中按类型分组统计。

function coralTypeCounts202(corals: Coral202[]): TypeCount202[] {
  const counts: TypeCount202[] = []
  for (let i = 0; i < coralTypeTags202.length; i++) {
    let n: number = 0
    for (let j = 0; j < corals.length; j++) {
      if (corals[j].type === coralTypeTags202[i]) {
        n++
      }
    }
    counts.push({ label: coralTypeTags202[i], count: n, color: ['#0288D1', '#FF7043', '#26A69A', '#7E57C2', '#FBC02D'][i] })
  }
  return counts
}

各位,请看这个函数的巧妙之处。它接收一个珊瑚数组作为参数,遍历五种珊瑚类型标签(鹿角珊瑚、脑珊瑚、桌珊瑚、软珊瑚、蘑菇珊瑚),对每种类型再遍历珊瑚数组计数。最终返回一个带颜色的统计结果数组。

这个函数之所以重要,是因为它是参数化的——它不依赖全局变量this.corals,而是通过参数接收数据。这意味着当珊瑚列表发生变化(增删改)时,只需重新调用这个函数就能得到最新的统计结果。在build()方法中,coralTypeCounts202(this.corals)会在每次UI重建时被调用,确保堆叠条始终反映最新数据。

堆叠条的渲染同样利用了layoutWeight权重分配:

Row() {
  ForEach(coralTypeCounts202(this.corals), (t: TypeCount202) => {
    Column() {}
    .layoutWeight(t.count > 0 ? t.count : 1)
    .height(14)
    .backgroundColor(t.color)
  }, (t: TypeCount202) => t.label)
}
.width('100%')
.borderRadius(7)
.clip(true)

当某类珊瑚数量为0时,layoutWeight用1作为fallback权重(t.count > 0 ? t.count : 1),确保该段在视觉上仍占一小段宽度而不是完全消失。这是一个很好的设计细节——它让用户知道"这类珊瑚存在但当前没有",而不是"这类珊瑚不存在"。


第七幕:预约跟潜课的故事——步进器与表单联动

预约跟潜课弹窗是应用中表单最复杂的弹窗之一。它有主题选择、等级选择、气瓶规格、深度步进器、水下对讲开关和一对一纠姿开关——六个表单控件,每个都需要独立的@State管理。

// 预约跟潜课表单
@State bookTheme: number = 0
@State bookLevel: number = 1
@State bookDepth: number = 12
@State bookBottle: number = 1
@State bookTalk: boolean = true
@State bookOne: boolean = false

// 深度步进器
Row({ space: 14 }) {
  Column() {
    Text('-').fontSize(16).fontColor('#01579B')
  }
  .width(34)
  .height(34)
  .borderRadius(17)
  .backgroundColor('#E0F7FA')
  .justifyContent(FlexAlign.Center)
  .onClick(() => {
    if (this.bookDepth > 5) {
      this.bookDepth -= 5
    }
  })
  Column({ space: 2 }) {
    Text('目标深度 ' + this.bookDepth + ' 米').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#01579B')
    Text('教练将按深度制定下潜计划').fontSize(9).fontColor('#90A4AE')
  }
  Column() {
    Text('+').fontSize(16).fontColor('#01579B')
  }
  .width(34)
  .height(34)
  .borderRadius(17)
  .backgroundColor('#E0F7FA')
  .justifyContent(FlexAlign.Center)
  .onClick(() => {
    if (this.bookDepth < 40) {
      this.bookDepth += 5
    }
  })
}

// 预估费用联动
Text('¥ ' + (this.bookDepth * 8 + (this.bookOne ? 60 : 0)))
  .fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FF7043')

各位请看深度步进器的设计。减号按钮有下限保护if (this.bookDepth > 5),最小深度5米;加号按钮有上限保护if (this.bookDepth < 40),最大深度40米。每次步进5米。这些边界值不是随意设置的——5米是体验潜水的最大深度,40米是休闲潜水的极限深度。技术参数和潜水安全规范完美对应。

预估费用的计算公式this.bookDepth * 8 + (this.bookOne ? 60 : 0)也很巧妙。深度越深费用越高(每米8元),因为深潜需要更高级的装备和更严格的安全措施。一对一纠姿加60元是附加服务费。这个公式在UI上实时更新——用户每调整一次深度或开关,费用立即重新计算并显示,无需点击任何"计算"按钮。这就是声明式UI的威力——数据驱动渲染,费用是深度的函数,深度变则费用自动变。


第七幕尾声:关闭弹窗的细节——表单重置

当用户提交认养后,弹窗关闭,但代码还做了一件容易被忽略的事——重置表单:

.onClick(() => {
  this.showAdoptSheet = false
  this.adoptName = ''
  this.adoptSea = 0
  this.adoptType = 0
  this.adoptAmount = 100
  this.adoptPublic = true
})

各位,这六行代码看似不起眼,但它们解决了一个常见的用户体验问题——“弹窗残留状态”。如果用户填了一半表单关掉弹窗,再打开时看到的是上次填写的内容,这会让用户困惑。通过在关闭时重置所有表单字段为初始值,确保每次打开弹窗都是干净的初始状态。这是一个小细节,但体现了对用户体验的深度思考。


技术方案对比分析表

技术维度 实现方案 演讲点评 适用场景
底部导航 氧气胶囊+气泡装饰 视觉语言与业务主题高度统一 垂直社区类应用
标签布局 Flex+FlexWrap.Wrap 自动换行适配多标签 标签数量不固定的表单
数据更新 map不可变回写 引用变化触发响应式渲染 需要精确状态检测的场景
步进器 手动加减+边界保护 安全规范与交互边界融合 有物理约束的数值输入
费用联动 公式实时计算 声明式UI的数据驱动优势 价格随参数变化的表单
图表渲染 纯布局组件+缩放因子 无依赖轻量但需控制数据范围 数值范围已知的简单图表
弹窗管理 bindSheet+bindContentCover $$双向绑定简化弹窗生命周期 多弹窗复杂应用
弹幕排列 模运算错位margin 营造水下气泡随机感 氛围导向的静态弹幕
表单重置 关闭时归零 消除弹窗残留状态 所有表单弹窗

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 场景:线上潜水跟练房(腾讯会议类)
// 配色:深海蓝 #01579B × 珊瑚橙 #FF7043 × 浪青 #E0F7FA
// Tab:底部「氧气胶囊」导航(全圆角胶囊 + 选中深海蓝渐变填充 + 上方气泡特效)

// ================= 数据接口 =================

interface WeekLog202 {
  day: string
  count: number
}

interface Coral202 {
  id: number
  name: string
  sea: string
  type: string
  amount: number
  state: string
  days: number
  top: boolean
}

interface Spot202 {
  id: number
  name: string
  level: string
  depth: number
  temp: number
  score: number
  state: string
  heat: number
}

interface Train202 {
  id: number
  name: string
  level: string
  mins: number
  kcal: number
  coach: string
  state: string
  likes: number
}

interface Mate202 {
  id: number
  name: string
  city: string
  cert: string
  dives: number
  online: boolean
  heat: number
}

interface Barrage202 {
  id: number
  text: string
}

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

interface DiveLog202 {
  id: number
  day: string
  mins: number
  depth: number
}

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

interface TempLog202 {
  month: string
  temp: number
}

// ================= 全局标签 =================

const diveThemeTags202: string[] = ['珊瑚花园', '沉船探秘', '夜潜礁盘', '断层漂流', '微距摄影']
const diveLevelTags202: string[] = ['体验潜水', 'OW 开放水域', 'AOW 进阶', '救援潜水员']
const bottleTags202: string[] = ['5L 小瓶', '10.2L 标准', '12L 大瓶', '15L 高容量']
const seaTags202: string[] = ['南海涠洲', '蜈支洲岛', '诗巴丹', '大堡礁', '马尔代夫']
const coralTypeTags202: string[] = ['鹿角珊瑚', '脑珊瑚', '桌珊瑚', '软珊瑚', '蘑菇珊瑚']

// ================= 全局函数 =================

function coralStateColor202(state: string): string {
  if (state === '认养中') {
    return '#FF7043'
  }
  if (state === '白化预警') {
    return '#E53935'
  }
  if (state === '已恢复') {
    return '#26A69A'
  }
  return '#90A4AE'
}

function spotStateColor202(state: string): string {
  if (state === '今日可潜') {
    return '#01579B'
  }
  if (state === '预约满') {
    return '#E53935'
  }
  if (state === '风浪休潜') {
    return '#FB8C00'
  }
  return '#90A4AE'
}

function trainStateColor202(state: string): string {
  if (state === '热约中') {
    return '#FF7043'
  }
  if (state === '可回放') {
    return '#00838F'
  }
  return '#90A4AE'
}

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

function adoptedCount202(corals: Coral202[]): number {
  let n: number = 0
  for (let i = 0; i < corals.length; i++) {
    if (corals[i].state === '认养中') {
      n++
    }
  }
  return n
}

function warnCount202(corals: Coral202[]): number {
  let n: number = 0
  for (let i = 0; i < corals.length; i++) {
    if (corals[i].state === '白化预警') {
      n++
    }
  }
  return n
}

function onlineMateCount202(mates: Mate202[]): number {
  let n: number = 0
  for (let i = 0; i < mates.length; i++) {
    if (mates[i].online) {
      n++
    }
  }
  return n
}

function coralTypeCounts202(corals: Coral202[]): TypeCount202[] {
  const counts: TypeCount202[] = []
  for (let i = 0; i < coralTypeTags202.length; i++) {
    let n: number = 0
    for (let j = 0; j < corals.length; j++) {
      if (corals[j].type === coralTypeTags202[i]) {
        n++
      }
    }
    counts.push({ label: coralTypeTags202[i], count: n, color: ['#0288D1', '#FF7043', '#26A69A', '#7E57C2', '#FBC02D'][i] })
  }
  return counts
}

function topCoral202(corals: Coral202[]): string {
  for (let i = 0; i < corals.length; i++) {
    if (corals[i].top) {
      return corals[i].name
    }
  }
  return '暂无置顶'
}

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

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

  // 弹框开关
  @State showBookSheet: boolean = false
  @State showAdoptSheet: boolean = false
  @State showEditSheet: boolean = false
  @State showDelDialog: boolean = false
  @State showDetailDialog: boolean = false

  // 预约跟潜课表单
  @State bookTheme: number = 0
  @State bookLevel: number = 1
  @State bookDepth: number = 12
  @State bookBottle: number = 1
  @State bookTalk: boolean = true
  @State bookOne: boolean = false

  // 新增认养表单
  @State adoptName: string = ''
  @State adoptSea: number = 0
  @State adoptType: number = 0
  @State adoptAmount: number = 100
  @State adoptPublic: boolean = true

  // 编辑表单
  @State editIndex: number = -1
  @State editName: string = ''
  @State editSea: number = 0
  @State editAmount: number = 100
  @State editTop: boolean = false

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

  // 详情
  @State detailIndex: number = 0

  // 列表数据
  @State corals: Coral202[] = [
    { id: 1, name: '小鹿角', sea: '南海涠洲', type: '鹿角珊瑚', amount: 120, state: '认养中', days: 86, top: true },
    { id: 2, name: '老脑珊瑚', sea: '蜈支洲岛', type: '脑珊瑚', amount: 200, state: '白化预警', days: 240, top: false },
    { id: 3, name: '桌面皇后', sea: '诗巴丹', type: '桌珊瑚', amount: 300, state: '认养中', days: 45, top: false },
    { id: 4, name: '紫软柳', sea: '大堡礁', type: '软珊瑚', amount: 150, state: '已恢复', days: 130, top: false },
    { id: 5, name: '蘑菇云朵', sea: '马尔代夫', type: '蘑菇珊瑚', amount: 80, state: '认养中', days: 21, top: false },
    { id: 6, name: '蓝鹿角', sea: '南海涠洲', type: '鹿角珊瑚', amount: 260, state: '认养中', days: 67, top: false },
    { id: 7, name: '红脑爷爷', sea: '蜈支洲岛', type: '脑珊瑚', amount: 320, state: '认养中', days: 310, top: false },
    { id: 8, name: '雏菊花田', sea: '诗巴丹', type: '软珊瑚', amount: 90, state: '白化预警', days: 15, top: false },
    { id: 9, name: '翡翠圆桌', sea: '大堡礁', type: '桌珊瑚', amount: 180, state: '已恢复', days: 95, top: false },
    { id: 10, name: '夜光蘑菇', sea: '马尔代夫', type: '蘑菇珊瑚', amount: 140, state: '认养中', days: 52, top: false }
  ]

  @State spots: Spot202[] = [
    { id: 1, name: '珊瑚花园', level: 'OW', depth: 12, temp: 27, score: 4.9, state: '今日可潜', heat: 98 },
    { id: 2, name: '沉船加勒比号', level: 'AOW', depth: 24, temp: 25, score: 4.8, state: '预约满', heat: 92 },
    { id: 3, name: '夜潜礁盘', level: 'AOW', depth: 18, temp: 24, score: 4.7, state: '今日可潜', heat: 88 },
    { id: 4, name: '断层漂流', level: '救援', depth: 30, temp: 23, score: 4.9, state: '风浪休潜', heat: 85 },
    { id: 5, name: '微距沙地', level: '体验', depth: 8, temp: 28, score: 4.6, state: '今日可潜', heat: 76 },
    { id: 6, name: '海龟清洁站', level: 'OW', depth: 14, temp: 27, score: 4.8, state: '今日可潜', heat: 81 }
  ]

  @State trains: Train202[] = [
    { id: 1, name: '耳压平衡专项', level: '入门', mins: 20, kcal: 90, coach: '阿鲸教练', state: '热约中', likes: 326 },
    { id: 2, name: '中性浮力精修', level: '进阶', mins: 35, kcal: 160, coach: '蓝鳍教练', state: '热约中', likes: 512 },
    { id: 3, name: '脚蹼打水节奏', level: '入门', mins: 25, kcal: 120, coach: '阿鲸教练', state: '可回放', likes: 288 },
    { id: 4, name: '气量控制呼吸法', level: '进阶', mins: 30, kcal: 110, coach: '珊瑚教练', state: '热约中', likes: 445 },
    { id: 5, name: '水下自然导航', level: '高阶', mins: 40, kcal: 180, coach: '蓝鳍教练', state: '可回放', likes: 379 },
    { id: 6, name: '救援拖带演练', level: '高阶', mins: 45, kcal: 220, coach: '珊瑚教练', state: '热约中', likes: 298 }
  ]

  @State mates: Mate202[] = [
    { id: 1, name: '气泡少女', city: '三亚', cert: 'AOW', dives: 96, online: true, heat: 88 },
    { id: 2, name: '沉船猎人', city: '深圳', cert: '救援', dives: 210, online: true, heat: 95 },
    { id: 3, name: '微距阿茂', city: '广州', cert: 'OW', dives: 54, online: false, heat: 61 },
    { id: 4, name: '海豚音', city: '厦门', cert: 'AOW', dives: 132, online: true, heat: 79 },
    { id: 5, name: '深蓝老周', city: '青岛', cert: '救援', dives: 268, online: false, heat: 90 },
    { id: 6, name: '珊瑚糖', city: '北海', cert: 'OW', dives: 38, online: true, heat: 55 }
  ]

  @State weekLogs: WeekLog202[] = [
    { day: '周一', count: 12 },
    { day: '周二', count: 18 },
    { day: '周三', count: 15 },
    { day: '周四', count: 22 },
    { day: '周五', count: 26 },
    { day: '周六', count: 30 },
    { day: '周日', count: 28 }
  ]

  @State dives: DiveLog202[] = [
    { id: 1, day: '一', mins: 38, depth: 10 },
    { id: 2, day: '二', mins: 42, depth: 12 },
    { id: 3, day: '三', mins: 35, depth: 9 },
    { id: 4, day: '四', mins: 46, depth: 14 },
    { id: 5, day: '五', mins: 40, depth: 12 },
    { id: 6, day: '六', mins: 52, depth: 16 },
    { id: 7, day: '日', mins: 48, depth: 15 }
  ]

  @State barrages: Barrage202[] = [
    { id: 1, text: '教练我的耳压平衡还是痛!' },
    { id: 2, text: '刚才那只海龟也太大了8' },
    { id: 3, text: '求沉船机位多停留两分钟' },
    { id: 4, text: '中性浮力练了三个月终于不转圈了' },
    { id: 5, text: '小鹿角又长高一截!' },
    { id: 6, text: '今晚夜潜记得带头灯' },
    { id: 7, text: '气量控制真的有用,亲测' },
    { id: 8, text: '围观破万了家人们' }
  ]

  @State steps: Step202[] = [
    { id: 1, title: '装备检查', tip: '气瓶 200bar · 配重 4kg · BCD 充排气正常', done: true },
    { id: 2, title: '岸上 Briefing', tip: '手势复核 · 上升速率 · 伙伴制确认', done: true },
    { id: 3, title: '入水下潜', tip: 'BCD 排气 · 沿导索缓降 · 面镜平衡', done: true },
    { id: 4, title: '耳压平衡', tip: '每下潜 2 米做一次 · 捏鼻轻鼓气', done: false },
    { id: 5, title: '中性悬浮', tip: '呼吸控浮 · 脚蹼慢频率 · 不碰珊瑚', done: false },
    { id: 6, title: '安全停留', tip: '5 米停留 3 分钟 · 象拔信号 · 缓慢出水', done: false }
  ]

  @State tempLogs: TempLog202[] = [
    { month: '2月', temp: 22 },
    { month: '3月', temp: 24 },
    { month: '4月', temp: 26 },
    { month: '5月', temp: 28 },
    { month: '6月', temp: 29 },
    { month: '7月', temp: 28 },
    { month: '8月', temp: 27 }
  ]

  tabs202: string[] = ['跟潜房', '珊瑚墙', '潜点', '训练', '潜伴团', '我的']
  tabEmojis202: string[] = ['🤿', '🪸', '📍', '🏊', '🤝', '👤']

  // ---------- 头部(电商大促风,无动画) ----------
  @Builder
  header202() {
    Column({ space: 12 }) {
      Row({ space: 10 }) {
        Column({ space: 4 }) {
          Text('鲸语 · 云深潜').fontSize(19).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('教练连线跟潜 · 珊瑚礁围潜直播').fontSize(11).fontColor('#B3E5FC')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column({ space: 2 }) {
          Text('🐳').fontSize(20)
          Text('1,286').fontSize(12).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('在线围观').fontSize(9).fontColor('#B3E5FC')
        }
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')

      Row({ space: 10 }) {
        Column().width(4).height(34).borderRadius(2).backgroundColor('#FFCA28')
        Column({ space: 3 }) {
          Text('深潜季 · 围潜狂欢周').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          Text('预约跟潜课立减 ¥30 · 一对一纠姿免费体验').fontSize(10).fontColor('#FFE0B2')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Column() {
          Text('去凑单 →').fontSize(11).fontColor('#01579B').fontWeight(FontWeight.Bold)
        }
        .padding({ left: 12, right: 12, top: 7, bottom: 7 })
        .borderRadius(14)
        .backgroundColor('#FFCA28')
        .onClick(() => {
          this.showBookSheet = true
        })
      }
      .width('100%')
      .padding(12)
      .borderRadius(12)
      .backgroundColor('#00365F')
    }
    .alignItems(HorizontalAlign.Start)
    .padding(14)
    .linearGradient({ angle: 140, colors: [['#0288D1', 0], ['#01579B', 1]] })
  }

  // ---------- 底部「氧气胶囊」tab ----------
  @Builder
  tabBar202() {
    Column({ space: 6 }) {
      Row({ space: 16 }) {
        Text('').width(8).height(8).borderRadius(4).backgroundColor('#4FC3F7').opacity(0.7)
        Text('').width(5).height(5).borderRadius(3).backgroundColor('#81D4FA').opacity(0.5)
        Text('').width(10).height(10).borderRadius(5).backgroundColor('#4FC3F7').opacity(0.4)
        Text('').width(6).height(6).borderRadius(3).backgroundColor('#81D4FA').opacity(0.6)
        Text('').width(8).height(8).borderRadius(4).backgroundColor('#4FC3F7').opacity(0.5)
        Text('').width(5).height(5).borderRadius(3).backgroundColor('#81D4FA').opacity(0.7)
      }
      Row({ space: 6 }) {
        ForEach(this.tabs202, (t: string, i: number) => {
          Column({ space: 3 }) {
            Text(this.tabEmojis202[i]).fontSize(17)
            Text(t)
              .fontSize(10)
              .fontColor(this.tabIndex1 === i ? '#FFFFFF' : '#546E7A')
              .fontWeight(this.tabIndex1 === i ? FontWeight.Bold : FontWeight.Normal)
          }
          .justifyContent(FlexAlign.Center)
          .padding({ left: 11, right: 11, top: 8, bottom: 8 })
          .borderRadius(21)
          .backgroundColor(this.tabIndex1 === i ? '#01579B' : '#F1F8FB')
          .shadow({
            radius: this.tabIndex1 === i ? 10 : 0,
            color: '#660288D1',
            offsetY: 3
          })
          .scale({ x: this.tabIndex1 === i ? 1.06 : 1, y: this.tabIndex1 === i ? 1.06 : 1 })
          .animation({ duration: 180 })
          .onClick(() => {
            this.tabIndex1 = i
          })
        }, (t: string) => t)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
    }
    .width('100%')
    .padding({ left: 10, right: 10, top: 6, bottom: 10 })
    .backgroundColor('#FFFFFF')
    .shadow({ radius: 12, color: '#140288D1', offsetY: -4 })
  }

  // ---------- 弹框一:预约跟潜课(底部抽屉) ----------
  @Builder
  bookSheet202() {
    Column() {
      Column() {
      }
      .width(40)
      .height(4)
      .borderRadius(2)
      .backgroundColor('#CFD8DC')
      .margin({ top: 10 })

      Row({ space: 8 }) {
        Text('预约连线跟潜课').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#01579B')
        Text('').layoutWeight(1)
        Column() {
          Text('×').fontSize(16).fontColor('#78909C')
        }
        .width(28)
        .height(28)
        .borderRadius(14)
        .backgroundColor('#ECEFF1')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showBookSheet = false
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12 })

      Scroll() {
        Column({ space: 16 }) {
          Column({ space: 8 }) {
            Text('跟潜主题').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
            Row({ space: 8 }) {
              ForEach(diveThemeTags202, (tag: string, i: number) => {
                Text(tag)
                  .fontSize(11)
                  .fontColor(this.bookTheme === i ? '#FFFFFF' : '#546E7A')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(14)
                  .backgroundColor(this.bookTheme === i ? '#01579B' : '#ECEFF1')
                  .onClick(() => {
                    this.bookTheme = i
                  })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Column({ space: 8 }) {
            Text('潜水等级').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
            Row({ space: 8 }) {
              ForEach(diveLevelTags202, (tag: string, i: number) => {
                Text(tag)
                  .fontSize(11)
                  .fontColor(this.bookLevel === i ? '#FFFFFF' : '#546E7A')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(14)
                  .backgroundColor(this.bookLevel === i ? '#FF7043' : '#ECEFF1')
                  .onClick(() => {
                    this.bookLevel = i
                  })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Column({ space: 8 }) {
            Text('气瓶规格').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
            Row({ space: 8 }) {
              ForEach(bottleTags202, (tag: string, i: number) => {
                Text(tag)
                  .fontSize(11)
                  .fontColor(this.bookBottle === i ? '#FFFFFF' : '#546E7A')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(14)
                  .backgroundColor(this.bookBottle === i ? '#00838F' : '#ECEFF1')
                  .onClick(() => {
                    this.bookBottle = i
                  })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#01579B')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#E0F7FA')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.bookDepth > 5) {
                this.bookDepth -= 5
              }
            })
            Column({ space: 2 }) {
              Text('目标深度 ' + this.bookDepth + ' 米').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#01579B')
              Text('教练将按深度制定下潜计划').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Center)
            Column() {
              Text('+').fontSize(16).fontColor('#01579B')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#E0F7FA')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.bookDepth < 40) {
                this.bookDepth += 5
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 10 }) {
            Text('水下对讲').fontSize(13).fontColor('#37474F')
            Text('').layoutWeight(1)
            Toggle({ type: ToggleType.Switch, isOn: this.bookTalk })
              .onChange((v: boolean) => {
                this.bookTalk = v
              })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#F1F8FB')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('一对一纠姿').fontSize(13).fontColor('#37474F')
              Text('教练单独连线逐帧纠正姿态').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Toggle({ type: ToggleType.Switch, isOn: this.bookOne })
              .onChange((v: boolean) => {
                this.bookOne = v
              })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#F1F8FB')
        }
        .padding({ left: 16, right: 16, bottom: 8 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)

      Row({ space: 12 }) {
        Column({ space: 2 }) {
          Text('预估费用').fontSize(10).fontColor('#90A4AE')
          Text('¥ ' + (this.bookDepth * 8 + (this.bookOne ? 60 : 0))).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FF7043')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Button() {
          Text('确认预约').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
        }
        .padding({ left: 28, right: 28, top: 11, bottom: 11 })
        .borderRadius(22)
        .backgroundColor('#01579B')
        .onClick(() => {
          this.showBookSheet = false
        })
      }
      .width('100%')
      .padding(14)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
  }

  // ---------- 弹框二:新增珊瑚认养(底部抽屉) ----------
  @Builder
  adoptSheet202() {
    Column() {
      Column() {
      }
      .width(40)
      .height(4)
      .borderRadius(2)
      .backgroundColor('#CFD8DC')
      .margin({ top: 10 })

      Row({ space: 8 }) {
        Text('认养一株珊瑚').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#00838F')
        Text('').layoutWeight(1)
        Column() {
          Text('×').fontSize(16).fontColor('#78909C')
        }
        .width(28)
        .height(28)
        .borderRadius(14)
        .backgroundColor('#ECEFF1')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showAdoptSheet = false
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12 })

      Scroll() {
        Column({ space: 16 }) {
          Column({ space: 8 }) {
            Text('给珊瑚起个名字').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
            TextInput({ placeholder: '例如:小鹿角、夜光蘑菇', text: this.adoptName })
              .fontSize(13)
              .padding(12)
              .borderRadius(12)
              .backgroundColor('#F1F8FB')
              .onChange((v: string) => {
                this.adoptName = v
              })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Column({ space: 8 }) {
            Text('所在海域').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(seaTags202, (tag: string, i: number) => {
                Text(tag)
                  .fontSize(11)
                  .fontColor(this.adoptSea === i ? '#FFFFFF' : '#546E7A')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(14)
                  .backgroundColor(this.adoptSea === i ? '#0288D1' : '#ECEFF1')
                  .margin(4)
                  .onClick(() => {
                    this.adoptSea = i
                  })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Column({ space: 8 }) {
            Text('珊瑚种类').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(coralTypeTags202, (tag: string, i: number) => {
                Text(tag)
                  .fontSize(11)
                  .fontColor(this.adoptType === i ? '#FFFFFF' : '#546E7A')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(14)
                  .backgroundColor(this.adoptType === i ? '#26A69A' : '#ECEFF1')
                  .margin(4)
                  .onClick(() => {
                    this.adoptType = i
                  })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#00838F')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#E0F2F1')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.adoptAmount > 50) {
                this.adoptAmount -= 50
              }
            })
            Column({ space: 2 }) {
              Text('认养金额 ¥ ' + this.adoptAmount + ' / 年').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#00838F')
              Text('用于珊瑚苗圃修复与监测浮标').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#00838F')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#E0F2F1')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.adoptAmount < 500) {
                this.adoptAmount += 50
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('公开认养证书').fontSize(13).fontColor('#37474F')
              Text('在珊瑚墙展示你的认养牌').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Toggle({ type: ToggleType.Switch, isOn: this.adoptPublic })
              .onChange((v: boolean) => {
                this.adoptPublic = v
              })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#E0F2F1')
        }
        .padding({ left: 16, right: 16, bottom: 8 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)

      Button() {
        Text('提交认养 · ¥ ' + this.adoptAmount).fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
      }
      .width('90%')
      .padding({ top: 12, bottom: 12 })
      .borderRadius(22)
      .backgroundColor('#00838F')
      .margin({ bottom: 14 })
      .onClick(() => {
        this.showAdoptSheet = false
        this.adoptName = ''
        this.adoptSea = 0
        this.adoptType = 0
        this.adoptAmount = 100
        this.adoptPublic = true
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
  }

  // ---------- 弹框三:编辑认养(底部抽屉) ----------
  @Builder
  editSheet202() {
    Column() {
      Column() {
      }
      .width(40)
      .height(4)
      .borderRadius(2)
      .backgroundColor('#CFD8DC')
      .margin({ top: 10 })

      Row({ space: 8 }) {
        Text('编辑认养信息').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FF7043')
        Text('').layoutWeight(1)
        Column() {
          Text('×').fontSize(16).fontColor('#78909C')
        }
        .width(28)
        .height(28)
        .borderRadius(14)
        .backgroundColor('#ECEFF1')
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.showEditSheet = false
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12 })

      Scroll() {
        Column({ space: 16 }) {
          Column({ space: 8 }) {
            Text('认养名称').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
            TextInput({ placeholder: '输入新名称', text: this.editName })
              .fontSize(13)
              .padding(12)
              .borderRadius(12)
              .backgroundColor('#FFF3E0')
              .onChange((v: string) => {
                this.editName = v
              })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Column({ space: 8 }) {
            Text('所在海域').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(seaTags202, (tag: string, i: number) => {
                Text(tag)
                  .fontSize(11)
                  .fontColor(this.editSea === i ? '#FFFFFF' : '#546E7A')
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(14)
                  .backgroundColor(this.editSea === i ? '#FF7043' : '#ECEFF1')
                  .margin(4)
                  .onClick(() => {
                    this.editSea = i
                  })
              }, (tag: string) => tag)
            }
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Row({ space: 14 }) {
            Column() {
              Text('-').fontSize(16).fontColor('#FF7043')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FFF3E0')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.editAmount > 50) {
                this.editAmount -= 50
              }
            })
            Column({ space: 2 }) {
              Text('续费金额 ¥ ' + this.editAmount + ' / 年').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FF7043')
              Text('调整后将按新档位发放证书').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('+').fontSize(16).fontColor('#FF7043')
            }
            .width(34)
            .height(34)
            .borderRadius(17)
            .backgroundColor('#FFF3E0')
            .justifyContent(FlexAlign.Center)
            .onClick(() => {
              if (this.editAmount < 500) {
                this.editAmount += 50
              }
            })
            Text('').layoutWeight(1)
          }
          .width('100%')

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text('置顶到珊瑚墙首位').fontSize(13).fontColor('#37474F')
              Text('好友进页面第一眼就能看到TA').fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            Text('').layoutWeight(1)
            Toggle({ type: ToggleType.Switch, isOn: this.editTop })
              .onChange((v: boolean) => {
                this.editTop = v
              })
          }
          .width('100%')
          .padding(12)
          .borderRadius(12)
          .backgroundColor('#FFF3E0')
        }
        .padding({ left: 16, right: 16, bottom: 8 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Off)

      Button() {
        Text('保存修改').fontSize(14).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
      }
      .width('90%')
      .padding({ top: 12, bottom: 12 })
      .borderRadius(22)
      .backgroundColor('#FF7043')
      .margin({ bottom: 14 })
      .onClick(() => {
        this.corals = this.corals.map((c: Coral202, i: number) => i === this.editIndex ? {
          id: c.id,
          name: this.editName === '' ? c.name : this.editName,
          sea: seaTags202[this.editSea],
          type: c.type,
          amount: this.editAmount,
          state: c.state,
          days: c.days,
          top: this.editTop
        } : c)
        this.showEditSheet = false
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
  }

  // ---------- 弹框四:删除认养(居中弹框) ----------
  @Builder
  delDialog202() {
    Column({ space: 16 }) {
      Text('🥺').fontSize(34)
      Text('删除这株珊瑚的认养?').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#37474F')
      Text('「' + (this.delIndex >= 0 && this.delIndex < this.corals.length ? this.corals[this.delIndex].name : '') + '」将不再出现在你的珊瑚墙')
        .fontSize(12)
        .fontColor('#90A4AE')
        .textAlign(TextAlign.Center)

      Row({ space: 10 }) {
        Column({ space: 2 }) {
          Text('保留潜水日志').fontSize(12).fontColor('#546E7A')
        }
        .alignItems(HorizontalAlign.Start)
        Text('').layoutWeight(1)
        Toggle({ type: ToggleType.Switch, isOn: this.delKeepLog })
          .onChange((v: boolean) => {
            this.delKeepLog = v
          })
      }
      .width('100%')
      .padding(12)
      .borderRadius(12)
      .backgroundColor('#F1F8FB')

      Row({ space: 12 }) {
        Button() {
          Text('再想想').fontSize(13).fontColor('#546E7A')
        }
        .layoutWeight(1)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(20)
        .backgroundColor('#ECEFF1')
        .onClick(() => {
          this.showDelDialog = false
        })
        Button() {
          Text('忍痛删除').fontSize(13).fontColor('#FFFFFF')
        }
        .layoutWeight(1)
        .padding({ top: 10, bottom: 10 })
        .borderRadius(20)
        .backgroundColor('#E53935')
        .onClick(() => {
          this.corals = this.corals.filter((c: Coral202, i: number) => i !== this.delIndex)
          this.showDelDialog = false
        })
      }
      .width('100%')
    }
    .width('84%')
    .padding(22)
    .borderRadius(18)
    .backgroundColor('#FFFFFF')
  }

  // ---------- 弹框五:珊瑚详情(居中弹框) ----------
  @Builder
  detailDialog202() {
    Column() {
      Scroll() {
        Column({ space: 0 }) {
          Column({ space: 6 }) {
            Text('🪸').fontSize(40)
            Text(this.detailIndex >= 0 && this.detailIndex < this.corals.length ? this.corals[this.detailIndex].name : '').fontSize(19).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text(this.detailIndex >= 0 && this.detailIndex < this.corals.length ? this.corals[this.detailIndex].type + ' · ' + this.corals[this.detailIndex].sea : '').fontSize(11).fontColor('#B3E5FC')
          }
          .width('100%')
          .padding({ top: 28, bottom: 22 })
          .linearGradient({ angle: 140, colors: [['#0288D1', 0], ['#01579B', 1]] })

          Column({ space: 14 }) {
            Row() {
              Column({ space: 3 }) {
                Text(this.detailIndex >= 0 && this.detailIndex < this.corals.length ? this.corals[this.detailIndex].days + ' 天' : '').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#01579B')
                Text('认养时长').fontSize(9).fontColor('#90A4AE')
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
              Column({ space: 3 }) {
                Text(this.detailIndex >= 0 && this.detailIndex < this.corals.length ? '¥ ' + this.corals[this.detailIndex].amount : '').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FF7043')
                Text('累计认养').fontSize(9).fontColor('#90A4AE')
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
              Column({ space: 3 }) {
                Text(this.detailIndex >= 0 && this.detailIndex < this.corals.length ? this.corals[this.detailIndex].state : '').fontSize(15).fontWeight(FontWeight.Bold).fontColor(coralStateColor202(this.detailIndex >= 0 && this.detailIndex < this.corals.length ? this.corals[this.detailIndex].state : ''))
                Text('当前状态').fontSize(9).fontColor('#90A4AE')
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }
            .width('100%')

            Column({ space: 8 }) {
              Text('所在海域月度水温(℃)').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#37474F')
              Row({ space: 8 }) {
                ForEach(this.tempLogs, (t: TempLog202) => {
                  Column({ space: 4 }) {
                    Text(t.temp + '').fontSize(9).fontColor('#0288D1')
                    Text('')
                      .width(14)
                      .height(t.temp * 3)
                      .borderRadius(4)
                      .linearGradient({ angle: 180, colors: [['#4FC3F7', 0], ['#01579B', 1]] })
                    Text(t.month).fontSize(8).fontColor('#78909C')
                  }
                }, (t: TempLog202) => t.month)
              }
              .alignItems(VerticalAlign.Bottom)
              .width('100%')
            }
            .width('100%')
            .padding(12)
            .borderRadius(12)
            .backgroundColor('#F1F8FB')

            Row({ space: 8 }) {
              ForEach(coralTypeTags202.slice(0, 3), (tag: string) => {
                Text(tag)
                  .fontSize(10)
                  .fontColor('#00838F')
                  .padding({ left: 10, right: 10, top: 5, bottom: 5 })
                  .borderRadius(12)
                  .backgroundColor('#E0F2F1')
              }, (tag: string) => tag)
            }
            .width('100%')

            Button() {
              Text('预约跟潜去看TA →').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
            }
            .width('100%')
            .padding({ top: 11, bottom: 11 })
            .borderRadius(20)
            .backgroundColor('#FF7043')
            .onClick(() => {
              this.showDetailDialog = false
              this.showBookSheet = true
            })
          }
          .padding(16)
        }
        .constraintSize({ maxHeight: '80%' })
      }
      .scrollBar(BarState.Off)

      Column() {
        Text('×').fontSize(16).fontColor('#FFFFFF')
      }
      .width(30)
      .height(30)
      .borderRadius(15)
      .backgroundColor('#33000000')
      .justifyContent(FlexAlign.Center)
      .margin({ top: -44, right: 14 })
      .onClick(() => {
        this.showDetailDialog = false
      })
    }
    .width('88%')
    .borderRadius(18)
    .backgroundColor('#FFFFFF')
  }

  build() {
    Column() {
      this.header202()

      Column() {
        if (this.tabIndex1 === 0) {
          LiveTab202({
            barrages: this.barrages,
            steps: this.steps,
            onBook: () => {
              this.showBookSheet = true
            },
            onStep: (i: number) => {
              this.steps = this.steps.map((s: Step202, idx: number) => idx === i ? {
                id: s.id,
                title: s.title,
                tip: s.tip,
                done: !s.done
              } : s)
            }
          })
        } else if (this.tabIndex1 === 1) {
          WallTab202({
            corals: this.corals,
            onNew: () => {
              this.showAdoptSheet = true
            },
            onEdit: (i: number) => {
              this.editIndex = i
              this.editName = this.corals[i].name
              this.editSea = seaTags202.indexOf(this.corals[i].sea) < 0 ? 0 : seaTags202.indexOf(this.corals[i].sea)
              this.editAmount = this.corals[i].amount
              this.editTop = this.corals[i].top
              this.showEditSheet = true
            },
            onDelete: (i: number) => {
              this.delIndex = i
              this.delKeepLog = true
              this.showDelDialog = true
            },
            onDetail: (i: number) => {
              this.detailIndex = i
              this.showDetailDialog = true
            }
          })
        } else if (this.tabIndex1 === 2) {
          SpotTab202({
            spots: this.spots,
            weekLogs: this.weekLogs,
            onBook: () => {
              this.showBookSheet = true
            }
          })
        } else if (this.tabIndex1 === 3) {
          TrainTab202({
            trains: this.trains
          })
        } else if (this.tabIndex1 === 4) {
          MateTab202({
            mates: this.mates
          })
        } else {
          MineTab202({
            corals: this.corals,
            dives: this.dives,
            onNew: () => {
              this.showAdoptSheet = true
            },
            onEdit: (i: number) => {
              this.editIndex = i
              this.editName = this.corals[i].name
              this.editSea = seaTags202.indexOf(this.corals[i].sea) < 0 ? 0 : seaTags202.indexOf(this.corals[i].sea)
              this.editAmount = this.corals[i].amount
              this.editTop = this.corals[i].top
              this.showEditSheet = true
            },
            onDelete: (i: number) => {
              this.delIndex = i
              this.delKeepLog = true
              this.showDelDialog = true
            },
            onDetail: (i: number) => {
              this.detailIndex = i
              this.showDetailDialog = true
            }
          })
        }
      }
      .layoutWeight(1)
      .width('100%')

      this.tabBar202()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F8FB')
    .bindSheet($$this.showBookSheet, this.bookSheet202(), {
      height: 620,
      dragBar: true,
      showClose: false,
      backgroundColor: '#FFFFFF'
    })
    .bindSheet($$this.showAdoptSheet, this.adoptSheet202(), {
      height: 640,
      dragBar: true,
      showClose: false,
      backgroundColor: '#FFFFFF'
    })
    .bindSheet($$this.showEditSheet, this.editSheet202(), {
      height: 560,
      dragBar: true,
      showClose: false,
      backgroundColor: '#FFFFFF'
    })
    .bindContentCover($$this.showDelDialog, this.delDialog202(), {
    })
    .bindContentCover($$this.showDetailDialog, this.detailDialog202(), {
    })
  }
}

// ================= Tab 1:跟潜房(直播) =================

@Component
struct LiveTab202 {
  @Prop barrages: Barrage202[] = []
  @Prop steps: Step202[] = []
  @State micOn: boolean = true
  @State camOn: boolean = true
  @State talkOn: boolean = true
  @State likeCount: number = 328
  onBook: () => void = () => {}
  onStep: (i: number) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Row({ space: 10 }) {
          Column() {
            Text('LIVE').fontSize(9).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .borderRadius(8)
          .backgroundColor('#E53935')
          Text('珊瑚花园 · 教练连线跟潜中').fontSize(13).fontColor('#01579B').fontWeight(FontWeight.Bold)
          Text('').layoutWeight(1)
          Text('🐋 已下潜 26 分钟').fontSize(10).fontColor('#90A4AE')
        }
        .width('100%')
        .padding(12)
        .borderRadius(12)
        .backgroundColor('#FFFFFF')

        Grid() {
          GridItem() {
            Column({ space: 4 }) {
              Text('').layoutWeight(1)
              Row({ space: 6 }) {
                Text('🪸 主镜 · 珊瑚花园').fontSize(11).fontColor('#FFFFFF')
                Text('1.2万').fontSize(9).fontColor('#B3E5FC')
              }
            }
            .padding(8)
            .linearGradient({ angle: 140, colors: [['#0288D1', 0], ['#01579B', 1]] })
          }
          GridItem() {
            Column({ space: 4 }) {
              Text('').layoutWeight(1)
              Row({ space: 6 }) {
                Text('🚢 沉船机位').fontSize(11).fontColor('#FFFFFF')
                Text('3,806').fontSize(9).fontColor('#FFE0B2')
              }
            }
            .padding(8)
            .linearGradient({ angle: 140, colors: [['#00838F', 0], ['#006064', 1]] })
          }
          GridItem() {
            Column({ space: 4 }) {
              Text('').layoutWeight(1)
              Row({ space: 6 }) {
                Text('🧑‍🏫 教练示范位').fontSize(11).fontColor('#FFFFFF')
                Text('在线').fontSize(9).fontColor('#A5D6A7')
              }
            }
            .padding(8)
            .linearGradient({ angle: 140, colors: [['#FF7043', 0], ['#D84315', 1]] })
          }
          GridItem() {
            Column({ space: 4 }) {
              Text('').layoutWeight(1)
              Row({ space: 6 }) {
                Text('🎥 我的水下镜头').fontSize(11).fontColor('#FFFFFF')
                Text(this.camOn ? '已开启' : '已关闭').fontSize(9).fontColor(this.camOn ? '#A5D6A7' : '#FFCDD2')
              }
            }
            .padding(8)
            .linearGradient({ angle: 140, colors: [['#546E7A', 0], ['#37474F', 1]] })
          }
        }
        .columnsTemplate('1fr 1fr')
        .rowsTemplate('1fr 1fr')
        .columnsGap(8)
        .rowsGap(8)
        .height(210)
        .borderRadius(14)
        .width('100%')

        Row({ space: 10 }) {
          Row({ space: 6 }) {
            Text(this.micOn ? '🎤' : '🔇').fontSize(14)
            Text('水下对讲').fontSize(11).fontColor(this.micOn ? '#01579B' : '#90A4AE')
          }
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .borderRadius(16)
          .backgroundColor(this.micOn ? '#E0F7FA' : '#ECEFF1')
          .onClick(() => {
            this.micOn = !this.micOn
          })

          Row({ space: 6 }) {
            Text(this.camOn ? '📹' : '📷').fontSize(14)
            Text('我的镜头').fontSize(11).fontColor(this.camOn ? '#01579B' : '#90A4AE')
          }
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .borderRadius(16)
          .backgroundColor(this.camOn ? '#E0F7FA' : '#ECEFF1')
          .onClick(() => {
            this.camOn = !this.camOn
          })

          Row({ space: 6 }) {
            Text('🔊').fontSize(14)
            Text('教练语音 ' + (this.talkOn ? '开' : '关')).fontSize(11).fontColor(this.talkOn ? '#00838F' : '#90A4AE')
          }
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .borderRadius(16)
          .backgroundColor(this.talkOn ? '#E0F2F1' : '#ECEFF1')
          .onClick(() => {
            this.talkOn = !this.talkOn
          })

          Text('').layoutWeight(1)

          Row({ space: 6 }) {
            Text('👍').fontSize(14)
            Text(this.likeCount + '').fontSize(11).fontColor('#FF7043')
          }
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .borderRadius(16)
          .backgroundColor('#FFF3E0')
          .onClick(() => {
            this.likeCount++
          })
        }
        .width('100%')

        Column({ space: 10 }) {
          Text('标准下潜六步流程').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
          ForEach(this.steps, (s: Step202, i: number) => {
            Row({ space: 10 }) {
              Column() {
                Text(s.done ? '✓' : (i + 1) + '')
                  .fontSize(12)
                  .fontColor('#FFFFFF')
                  .textAlign(TextAlign.Center)
                  .width(26)
                  .height(26)
                  .borderRadius(13)
                  .backgroundColor(s.done ? '#26A69A' : '#B0BEC5')
                if (i < this.steps.length - 1) {
                  Text('')
                    .width(2)
                    .layoutWeight(1)
                    .backgroundColor('#CFD8DC')
                    .margin({ top: 2, bottom: 2 })
                }
              }
              Column({ space: 3 }) {
                Row({ space: 8 }) {
                  Text(s.title).fontSize(12).fontWeight(FontWeight.Bold).fontColor(s.done ? '#26A69A' : '#37474F')
                  Text(s.done ? '已完成' : '待执行').fontSize(9).fontColor(s.done ? '#26A69A' : '#FB8C00')
                }
                Text(s.tip).fontSize(10).fontColor('#90A4AE')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              .padding({ top: 2, bottom: 8 })
              .onClick(() => {
                this.onStep(i)
              })
            }
            .alignItems(VerticalAlign.Top)
            .width('100%')
          }, (s: Step202) => s.id.toString())
        }
        .width('100%')
        .padding(12)
        .borderRadius(12)
        .backgroundColor('#FFFFFF')

        Column({ space: 8 }) {
          Text('弹幕 · 围潜氛围组').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#37474F')
          ForEach(this.barrages, (b: Barrage202, i: number) => {
            Text(b.text)
              .fontSize(11)
              .fontColor('#37474F')
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .borderRadius(12)
              .backgroundColor(i % 3 === 0 ? '#E0F7FA' : '#E0F2F1')
              .margin({ left: (i % 3) * 36 })
              .alignSelf(ItemAlign.Start)
          }, (b: Barrage202) => b.id.toString())
        }
        .width('100%')
        .padding(12)
        .borderRadius(12)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Start)

        Button() {
          Text('预约下一场跟潜课').fontSize(13).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding({ top: 11, bottom: 11 })
        .borderRadius(18)
        .backgroundColor('#01579B')
        .onClick(() => {
          this.onBook()
        })
      }
      .padding(12)
      .alignItems(HorizontalAlign.Start)
    }
    .scrollBar(BarState.Off)
    .width('100%')
    .height('100%')
  }
}

// ================= Tab 2:珊瑚墙 =================

@Component
struct WallTab202 {
  @Prop corals: Coral202[] = []
  onNew: () => void = () => {}
  onEdit: (i: number) => void = () => {}
  onDelete: (i: number) => void = () => {}
  onDetail: (i: number) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Row({ space: 8 }) {
          Column({ space: 2 }) {
            Text(this.corals.length + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#01579B')
            Text('认养总数').fontSize(9).fontColor('#90A4AE')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          .padding({ top: 8, bottom: 8 })
          .borderRadius(12)
          .backgroundColor('#FFFFFF')
          Column({ space: 2 }) {
            Text(adoptedCount202(this.corals) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF7043')
            Text('认养中').fontSize(9).fontColor('#90A4AE')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          .padding({ top: 8, bottom: 8 })
          .borderRadius(12)
          .backgroundColor('#FFFFFF')
          Column({ space: 2 }) {
            Text(warnCount202(this.corals) + '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E53935')
            Text('白化预警').fontSize(9).fontColor('#90A4AE')
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          .padding({ top: 8, bottom: 8 })
          .borderRadius(12)
          .backgroundColor('#FFFFFF')
        }
        .width('100%')

        Column({ space: 8 }) {
          Text('珊瑚种类构成').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#37474F')
          Row() {
            ForEach(coralTypeCounts202(this.corals), (t: TypeCount202) => {
              Column() {
              }
              .layoutWeight(t.count > 0 ? t.count : 1)
              .height(14)
              .backgroundColor(t.color)
            }, (t: TypeCount202) => t.label)
          }
          .width('100%')
          .borderRadius(7)
          .clip(true)
          Row({ space: 8 }) {
            ForEach(coralTypeCounts202(this.corals), (t: TypeCount202) => {
              Row({ space: 4 }) {
                Text('').width(8).height(8).borderRadius(2).backgroundColor(t.color)
                Text(t.label + ' ' + t.count).fontSize(9).fontColor('#78909C')
              }
            }, (t: TypeCount202) => t.label)
          }
          .width('100%')
        }
        .width('100%')
        .padding(12)
        .borderRadius(12)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Start)

        Row({ space: 8 }) {
          Text('我的珊瑚墙').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
          Text('').layoutWeight(1)
          Text('置顶:' + topCoral202(this.corals)).fontSize(10).fontColor('#90A4AE')
          Column() {
            Text('+ 认养').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .borderRadius(14)
          .backgroundColor('#00838F')
          .onClick(() => {
            this.onNew()
          })
        }
        .width('100%')

        ForEach(this.corals, (c: Coral202, i: number) => {
          Row({ space: 10 }) {
            Stack() {
              Column()
                .width(48)
                .height(48)
                .borderRadius(12)
                .linearGradient({ angle: 140, colors: [['#4FC3F7', 0], ['#01579B', 1]] })
              Text('🪸').fontSize(22)
            }
            .width(48)
            .height(48)

            Column({ space: 4 }) {
              Row({ space: 6 }) {
                if (c.top) {
                  Text('置顶').fontSize(8).fontColor('#FF7043').padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6).backgroundColor('#FFF3E0')
                }
                Text(c.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
                Text(c.state)
                  .fontSize(8)
                  .fontColor('#FFFFFF')
                  .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                  .borderRadius(6)
                  .backgroundColor(coralStateColor202(c.state))
              }
              Text(c.type + ' · ' + c.sea + ' · 认养 ' + c.days + ' 天').fontSize(10).fontColor('#90A4AE')
              Row({ space: 8 }) {
                Text('编辑').fontSize(10).fontColor('#01579B').padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8).backgroundColor('#E0F7FA')
                  .onClick(() => {
                    this.onEdit(i)
                  })
                Text('删除').fontSize(10).fontColor('#E53935').padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8).backgroundColor('#FFEBEE')
                  .onClick(() => {
                    this.onDelete(i)
                  })
              }
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Column() {
              Text('¥ ' + c.amount).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#FF7043')
            }
          }
          .width('100%')
          .padding(12)
          .borderRadius(14)
          .backgroundColor('#FFFFFF')
          .onClick(() => {
            this.onDetail(i)
          })
        }, (c: Coral202) => c.id.toString())
      }
      .padding(12)
      .alignItems(HorizontalAlign.Start)
    }
    .scrollBar(BarState.Off)
    .width('100%')
    .height('100%')
  }
}

// ================= Tab 3:潜点 =================

@Component
struct SpotTab202 {
  @Prop spots: Spot202[] = []
  @Prop weekLogs: WeekLog202[] = []
  onBook: () => void = () => {}

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Column({ space: 8 }) {
          Text('本周下潜人次').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#37474F')
          Row({ space: 10 }) {
            ForEach(this.weekLogs, (w: WeekLog202) => {
              Column({ space: 4 }) {
                Text(w.count + '').fontSize(9).fontColor('#01579B')
                Text('')
                  .width(16)
                  .height(w.count * 3)
                  .borderRadius(4)
                  .linearGradient({ angle: 180, colors: [['#4FC3F7', 0], ['#01579B', 1]] })
                Text(w.day).fontSize(9).fontColor('#78909C')
              }
            }, (w: WeekLog202) => w.day)
          }
          .alignItems(VerticalAlign.Bottom)
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
        }
        .width('100%')
        .padding(12)
        .borderRadius(12)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Start)

        Text('热门潜点').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')

        ForEach(this.spots, (s: Spot202, i: number) => {
          Row({ space: 10 }) {
            Stack() {
              Column()
                .width(52)
                .height(52)
                .borderRadius(26)
                .backgroundColor(i % 2 === 0 ? '#E0F7FA' : '#E0F2F1')
              Text('📍').fontSize(22)
              if (i < 3) {
                Text('TOP' + (i + 1))
                  .fontSize(8)
                  .fontColor('#FFFFFF')
                  .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                  .borderRadius(8)
                  .backgroundColor('#FF7043')
                  .position({ x: 26, y: 40 })
              }
            }
            .width(52)
            .height(52)

            Column({ space: 4 }) {
              Row({ space: 6 }) {
                Text(s.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
                Text(s.state)
                  .fontSize(8)
                  .fontColor('#FFFFFF')
                  .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                  .borderRadius(6)
                  .backgroundColor(spotStateColor202(s.state))
              }
              Text(s.level + ' · 最深 ' + s.depth + ' 米 · 水温 ' + s.temp + '℃').fontSize(10).fontColor('#90A4AE')
              Row({ space: 6 }) {
                Text('⭐ ' + s.score).fontSize(10).fontColor('#FB8C00')
                Text('🔥 热度 ' + s.heat).fontSize(10).fontColor('#E53935')
              }
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Column() {
              Text('去预约').fontSize(11).fontColor('#FFFFFF')
            }
            .padding({ left: 12, right: 12, top: 7, bottom: 7 })
            .borderRadius(14)
            .backgroundColor('#01579B')
            .onClick(() => {
              this.onBook()
            })
          }
          .width('100%')
          .padding(12)
          .borderRadius(14)
          .backgroundColor('#FFFFFF')
        }, (s: Spot202) => s.id.toString())
      }
      .padding(12)
      .alignItems(HorizontalAlign.Start)
    }
    .scrollBar(BarState.Off)
    .width('100%')
    .height('100%')
  }
}

// ================= Tab 4:训练 =================

@Component
struct TrainTab202 {
  @Prop trains: Train202[] = []

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Column({ space: 8 }) {
          Text('陆地训练 · 热门课程').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
          Row({ space: 10 }) {
            ForEach(this.trains, (t: Train202) => {
              Column({ space: 4 }) {
                Text(t.mins + '').fontSize(9).fontColor('#00838F')
                Text('')
                  .width(16)
                  .height(t.mins)
                  .borderRadius(4)
                  .linearGradient({ angle: 180, colors: [['#4DD0E1', 0], ['#00838F', 1]] })
                Text(t.name.slice(0, 2)).fontSize(8).fontColor('#78909C')
              }
            }, (t: Train202) => t.id.toString())
          }
          .alignItems(VerticalAlign.Bottom)
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
        }
        .width('100%')
        .padding(12)
        .borderRadius(12)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Start)

        ForEach(this.trains, (t: Train202, i: number) => {
          Column({ space: 10 }) {
            Row({ space: 10 }) {
              Column({ space: 2 }) {
                Text('0' + (i + 1)).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#0288D1')
              }
              Column({ space: 4 }) {
                Row({ space: 6 }) {
                  Text(t.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
                  Text(t.state)
                    .fontSize(8)
                    .fontColor('#FFFFFF')
                    .padding({ left: 5, right: 5, top: 2, bottom: 2 })
                    .borderRadius(6)
                    .backgroundColor(trainStateColor202(t.state))
                }
                Text(t.coach + ' · ' + t.level + ' · ' + t.mins + ' 分钟 · ' + t.kcal + ' kcal').fontSize(10).fontColor('#90A4AE')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Text('❤ ' + t.likes).fontSize(11).fontColor('#FF7043')
            }
            Row() {
              Text('')
                .height(5)
                .borderRadius(3)
                .layoutWeight(t.likes)
                .backgroundColor('#FF7043')
              Text('')
                .height(5)
                .borderRadius(3)
                .layoutWeight(600 - t.likes)
                .backgroundColor('#ECEFF1')
            }
            .width('100%')
            .clip(true)
          }
          .width('100%')
          .padding(12)
          .borderRadius(14)
          .backgroundColor('#FFFFFF')
        }, (t: Train202) => t.id.toString())
      }
      .padding(12)
      .alignItems(HorizontalAlign.Start)
    }
    .scrollBar(BarState.Off)
    .width('100%')
    .height('100%')
  }
}

// ================= Tab 5:潜伴团 =================

@Component
struct MateTab202 {
  @Prop mates: Mate202[] = []

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Row({ space: 8 }) {
          Text('本周潜伴人气榜').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
          Text('').layoutWeight(1)
          Text(onlineMateCount202(this.mates) + ' 位在线').fontSize(10).fontColor('#26A69A')
        }
        .width('100%')

        Column({ space: 10 }) {
          ForEach(this.mates, (m: Mate202, i: number) => {
            Row({ space: 8 }) {
              Text(m.name).fontSize(11).fontColor('#37474F').width(64)
              Row() {
                Text('')
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor(i === 0 ? '#FF7043' : (i === 1 ? '#FB8C00' : '#0288D1'))
                  .width((m.heat / maxMateHeat202(this.mates) * 100) + '%')
                Text('')
                  .layoutWeight(1)
                  .height(10)
              }
              .layoutWeight(1)
              .borderRadius(5)
              .clip(true)
              Text(m.heat + '').fontSize(10).fontColor('#90A4AE').width(24)
            }
            .width('100%')
          }, (m: Mate202) => m.id.toString())
        }
        .width('100%')
        .padding(12)
        .borderRadius(12)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Start)

        ForEach(this.mates, (m: Mate202) => {
          Row({ space: 10 }) {
            Stack() {
              Column()
                .width(46)
                .height(46)
                .borderRadius(23)
                .backgroundColor('#E0F7FA')
              Text('🤿').fontSize(20)
              if (m.online) {
                Text('')
                  .width(10)
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor('#26A69A')
                  .position({ x: 34, y: 34 })
              }
            }
            .width(46)
            .height(46)

            Column({ space: 4 }) {
              Row({ space: 6 }) {
                Text(m.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
                Text(m.online ? '在线' : '离线').fontSize(8).fontColor('#FFFFFF').padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6).backgroundColor(m.online ? '#26A69A' : '#B0BEC5')
              }
              Text(m.city + ' · ' + m.cert + ' · 累计 ' + m.dives + ' 潜').fontSize(10).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Column() {
              Text('约潜').fontSize(11).fontColor('#FFFFFF')
            }
            .padding({ left: 14, right: 14, top: 7, bottom: 7 })
            .borderRadius(14)
            .backgroundColor('#0288D1')
          }
          .width('100%')
          .padding(12)
          .borderRadius(14)
          .backgroundColor('#FFFFFF')
        }, (m: Mate202) => m.id.toString())
      }
      .padding(12)
      .alignItems(HorizontalAlign.Start)
    }
    .scrollBar(BarState.Off)
    .width('100%')
    .height('100%')
  }
}

// ================= Tab 6:我的 =================

@Component
struct MineTab202 {
  @Prop corals: Coral202[] = []
  @Prop dives: DiveLog202[] = []
  onNew: () => void = () => {}
  onEdit: (i: number) => void = () => {}
  onDelete: (i: number) => void = () => {}
  onDetail: (i: number) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Row({ space: 12 }) {
          Column()
            .width(56)
            .height(56)
            .borderRadius(28)
            .linearGradient({ angle: 140, colors: [['#4FC3F7', 0], ['#01579B', 1]] })
            .justifyContent(FlexAlign.Center)
          Column({ space: 4 }) {
            Text('气泡潜水员').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#37474F')
            Text('OW 开放水域 · 累计 46 潜 · 32 小时水下').fontSize(10).fontColor('#90A4AE')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('100%')
        .padding(14)
        .borderRadius(14)
        .linearGradient({ angle: 140, colors: [['#E0F7FA', 0], ['#B3E5FC', 1]] })

        Column({ space: 8 }) {
          Text('我的本周下潜(分钟)').fontSize(12).fontWeight(FontWeight.Bold).fontColor('#37474F')
          Row({ space: 10 }) {
            ForEach(this.dives, (d: DiveLog202) => {
              Column({ space: 4 }) {
                Text(d.mins + '').fontSize(9).fontColor('#01579B')
                Text('')
                  .width(16)
                  .height(d.mins)
                  .borderRadius(4)
                  .linearGradient({ angle: 180, colors: [['#4FC3F7', 0], ['#01579B', 1]] })
                Text(d.day).fontSize(9).fontColor('#78909C')
              }
            }, (d: DiveLog202) => d.id.toString())
          }
          .alignItems(VerticalAlign.Bottom)
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
        }
        .width('100%')
        .padding(12)
        .borderRadius(12)
        .backgroundColor('#FFFFFF')
        .alignItems(HorizontalAlign.Start)

        Row({ space: 8 }) {
          Text('我的认养管理').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#37474F')
          Text('').layoutWeight(1)
          Column() {
            Text('+ 新增认养').fontSize(11).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
          }
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .borderRadius(14)
          .backgroundColor('#00838F')
          .onClick(() => {
            this.onNew()
          })
        }
        .width('100%')

        ForEach(this.corals, (c: Coral202, i: number) => {
          Row({ space: 10 }) {
            Column() {
              Text('🪸').fontSize(20)
            }
            .width(40)
            .height(40)
            .borderRadius(10)
            .backgroundColor('#E0F7FA')
            .justifyContent(FlexAlign.Center)

            Column({ space: 3 }) {
              Text(c.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor('#37474F')
              Text(c.sea + ' · ' + c.type).fontSize(9).fontColor('#90A4AE')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Row({ space: 6 }) {
              Text('详情').fontSize(10).fontColor('#01579B').padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8).backgroundColor('#E0F7FA')
                .onClick(() => {
                  this.onDetail(i)
                })
              Text('编辑').fontSize(10).fontColor('#FB8C00').padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8).backgroundColor('#FFF3E0')
                .onClick(() => {
                  this.onEdit(i)
                })
              Text('删除').fontSize(10).fontColor('#E53935').padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(8).backgroundColor('#FFEBEE')
                .onClick(() => {
                  this.onDelete(i)
                })
            }
          }
          .width('100%')
          .padding(10)
          .borderRadius(12)
          .backgroundColor('#FFFFFF')
        }, (c: Coral202) => ('m' + c.id))

        Text('鲸语 · 云深潜 v3.1.0 · 与海共呼吸').fontSize(10).fontColor('#B0BEC5').margin({ top: 8, bottom: 20 })
      }
      .padding(12)
      .alignItems(HorizontalAlign.Start)
    }
    .scrollBar(BarState.Off)
    .width('100%')
    .height('100%')
  }
}


总结

在这里插入图片描述

各位开发者朋友,让我们从这次代码深潜中浮出水面,回顾一下今天看到的风景。这款云深潜应用用HarmonyOS ArkTS的声明式UI框架,构建了一个从直播跟潜到珊瑚认养、从潜点导航到训练课程的完整垂直社区。它的氧气胶囊导航让TabBar有了海洋的灵魂,它的map/filter不可变更新让数据流清晰可追踪,它的下潜六步流程把真实的潜水安全规范编码进了交互设计。每一个技术选择背后,都有对用户体验的深入思考和对业务领域的真实理解。

从工程角度,这款应用展示了几個值得学习的模式。第一是"数据向下、事件向上"的单向数据流——子组件通过@Prop接收数据,通过回调函数上报事件,父组件集中处理状态变更。第二是"不可变更新"的响应式策略——所有数据修改都通过map/filter创建新引用,确保ArkTS的状态检测可靠工作。第三是"语义化配色"——深海蓝对应潜水主题、珊瑚橙对应认养业务、红色对应白化预警,颜色不只是装饰,更是信息编码。第四是"防御性边界检查"——步进器有上下限保护、数组访问有索引校验、颜色映射有fallback值,这些防线让应用在面对异常输入时不会崩溃。

但这场演讲最想传递的,是技术与人之间的连接。当用户在应用里认养了一株叫"小鹿角"的珊瑚,看到它从"白化预警"变成"已恢复",那不只是一个state字段从'白化预警'变成了'已恢复'——那是一株真实的珊瑚在一群素未谋面的人的关注下获得了修复的机会。代码是冷的,但它承载的故事是温暖的。当我们写下一行this.corals = this.corals.map(...)时,我们不仅是在操作数据,我们是在连接一片海洋和一颗关注它的心。这就是技术的意义——让远方的事物变得可以触及,让冰冷的数据变得有温度。谢谢各位。

Logo

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

更多推荐