HarmonyOS ArkTS API 24自由潜水装备馆面镜脚蹼选购与潜水日志系统深度实践——基于HarmonyOS API 24海洋蓝调UI设计全解析

自由潜水作为一项融合挑战与宁静的极限运动,正在全球范围内吸引越来越多的爱好者。一款专业的潜水装备选购与潜水日志管理应用,能够帮助潜水员科学地管理装备、记录每一次下潜、追踪训练进度。本文将深入解析一款以深海蓝为主色调的自由潜水装备馆应用,展示HarmonyOS ArkTS如何构建清新专业的海洋风格UI。

该应用以"DEEP BLUE"为品牌标识,围绕自由潜水装备生态构建了首页、面镜、脚蹼、潜水服、配重、个人中心六大功能模块。采用浅海蓝到深海蓝的渐变配色体系,配合珊瑚橙作为强调色,营造出清爽专业的海洋氛围。技术上采用组件化架构与纯函数数据处理,确保应用的高性能与可扩展性。

引言

在这里插入图片描述

自由潜水(Freediving)是指不携带氧气瓶,仅通过自身肺活量调节呼吸,尽可能深地潜入水下的运动。这项运动不仅考验身体的生理极限,更追求内心的平静与专注。随着自由潜水运动的普及,潜水员对专业装备的需求日益增长,面镜、脚蹼、潜水服、配重系统等装备的选择直接影响着潜水体验和安全。

本应用正是在这样的背景下诞生,它不仅仅是一个装备电商平台,更是一个集装备选购、潜水日志、课程报名、训练记录于一体的综合性服务平台。应用采用浅色系设计风格,以浅海蓝作为背景色,深海蓝作为主色调,珊瑚橙作为强调色,整体视觉清新明快,符合海洋运动的活力特质。技术架构上遵循HarmonyOS声明式UI最佳实践,将业务拆分为独立的Tab组件,每个组件内部维护自己的状态和数据,通过纯函数工具集实现数据过滤和统计计算。

在交互体验方面,应用独具匠心地融入了多种海洋元素动画:气泡上升效果模拟水下的视觉感受,深度仪表盘指针摆动营造专业氛围,物品卡片的浮动动画仿佛随波漂动。这些细节设计让用户在使用过程中仿佛置身海洋世界,大大增强了品牌记忆点和用户粘性。同时,应用还提供了完整的商品管理功能,支持新增、编辑、删除等操作,满足运营人员的日常管理需求。

主题系统与品牌设计

海洋配色体系

在这里插入图片描述

应用的主题系统采用了精心调配的海洋配色方案,从浅到深构建了完整的色彩层次。背景色选用淡蓝色调,营造清爽开阔的视觉感受;主色采用深海蓝色,传递专业与可信赖的品牌形象;强调色选用温暖的珊瑚橙色,在蓝色基调中形成鲜明对比,有效引导用户视线。

interface DvTheme {
  bg: string
  primary: string
  accent: string
  card: string
  text: string
  sub: string
  line: string
  glow: string
  deep: string
}

const DV: DvTheme = {
  bg: '#EAF6FA',
  primary: '#0B5E8A',
  accent: '#FF8A5C',
  card: '#FFFFFF',
  text: '#0E3A52',
  sub: '#7FA8BC',
  line: '#D4E8F0',
  glow: '#57C4E5',
  deep: '#063A5C'
}

浅色主题与深色主题在设计策略上有着本质的区别。深色主题通过减少光线发射来营造沉浸感,适合娱乐类应用;浅色主题则通过明亮的视觉环境提升可读性和信任感,更适合电商、工具等实用类应用。本应用作为潜水装备选购平台,采用浅色主题能够更好地展示商品细节,同时传递出专业可靠的品牌形象。

主题接口中特别增加了glowdeep两个色值,分别用于发光效果和极深区域的展示,这两个扩展色值进一步丰富了视觉层次。在实际开发中,根据业务需求合理扩展主题字段是一种值得推荐的做法,它能够在保持主题统一性的同时,为特殊视觉效果提供支持。

数据模型设计

在这里插入图片描述

应用的数据模型围绕潜水装备的核心品类展开,包括面镜、脚蹼、潜水服、配重四大主要装备类别,以及潜水日志、订单、月度消费等辅助数据。每个数据接口都经过精心设计,完整覆盖了对应品类的关键属性。

interface DvMask {
  name: string
  volume: number
  lens: string
  price: number
  sold: number
  rating: number
}

interface DvFin {
  name: string
  material: string
  length: number
  price: number
  sold: number
  stiffness: number
}

interface DvSuit {
  name: string
  thickness: number
  temp: number
  style: string
  price: number
  sold: number
}

interface DvLog {
  date: string
  site: string
  depth: number
  minutes: number
  temp: number
}

以面镜为例,数据模型包含了容积、镜片类型、价格、销量、评分等核心属性。其中容积是自由潜水面镜最重要的参数之一,低容积面镜能够减少平压所需的空气量,是自由潜水员的首选。脚蹼数据则关注材质、长度、硬度等参数,这些直接影响踢水效率和潜水体验。潜水日志数据记录了日期、潜点、深度、时长、水温等信息,是潜水员追踪训练进度的重要依据。

动画系统:海洋元素的视觉表达

气泡与浮动动画

在这里插入图片描述

应用最具特色的动画效果当属气泡上升动画,它通过数学函数模拟气泡在水中上升的自然轨迹。每个气泡拥有不同的上升速度和水平摆动幅度,形成错落有致的视觉效果,为页面增添了灵动的海洋气息。

function dvBubbleY(w: number, i: number): number {
  return -((w * 2.2 + i * 41) % 160)
}

function dvBubbleX(w: number, i: number): number {
  return ((w * 3 + i * 53) % 26) - 13
}

function dvFloatY(w: number, i: number): number {
  return Math.sin((w + i * 18) * 0.11) * 4
}

气泡动画的实现原理是利用模运算让位移值在固定范围内循环,结合索引偏移量实现多个气泡的错峰运动。Y轴方向的线性递增模拟气泡上升,X轴方向的周期性偏移模拟水中的摆动。这种纯数学计算的动画方式性能极高,即使同时运行数十个气泡动画也不会造成性能问题。

dvBubbleY函数计算气泡的垂直位移,通过模运算让气泡在160像素范围内循环上升;dvBubbleX函数计算水平摆动,在-13到13像素之间周期性变化。两者结合便模拟出了气泡在水中蜿蜒上升的自然效果。dvFloatY函数则实现了物品的上下浮动,基于正弦函数产生平滑的周期性位移,营造出物品随波漂动的视觉感受。

仪表盘与脉冲动画

在这里插入图片描述

首页的深度仪表盘是应用的视觉焦点之一,它通过指针的摆动模拟深度计的实时读数效果。脉冲动画则用于强调重要的操作按钮,吸引用户的注意力。

function dvPulse(w: number): number {
  return 0.55 + 0.45 * Math.abs(Math.sin(w * 0.09))
}

function dvNeedle(w: number): number {
  return Math.sin(w * 0.06) * 40
}

function dvSpin(w: number, i: number): number {
  return (w * 4 + i * 90) % 360
}

dvNeedle函数基于正弦函数计算指针的摆动角度,摆动范围为正负40度,模拟了真实深度计指针的微动效果。dvPulse函数产生呼吸灯般的脉冲效果,通过绝对值正弦函数实现透明度在0.55到1.0之间的平滑变化,常用于重要按钮和促销标签的强调展示。dvSpin函数实现旋转动画,可用于加载指示器或装饰元素。

主应用架构

在这里插入图片描述

根组件与搜索功能

应用根组件采用经典的Column垂直布局,从上到下依次为品牌头部、搜索栏、内容区域和底部Tab栏。与深色主题应用不同的是,本应用的头部和Tab栏采用白色卡片设计,在浅蓝色背景上形成清晰的视觉层次。

@Entry
@Component
struct DvApp {
  @State tabIdx: number = 0
  @State keyword: string = ''

  build() {
    Column() {
      Row() {
        Column() {
          Text('DEEP BLUE')
            .fontSize(19)
            .fontWeight(FontWeight.Bold)
            .fontColor(DV.primary)
            .letterSpacing(2)
          Text('自由潜水装备馆 · 一口呼吸下潜')
            .fontSize(10)
            .fontColor(DV.sub)
            .margin({ top: 3 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text('🔔')
          .fontSize(19)
          .margin({ right: 14 })
        Text('🛒')
          .fontSize(19)
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14, bottom: 12 })
      .backgroundColor(DV.card)
      .border({ width: { bottom: 1 }, color: DV.line })
      
      // 搜索栏
      Row() {
        Text('🔍')
          .fontSize(14)
          .margin({ left: 12 })
        TextInput()
          .fontSize(12)
          .fontColor(DV.text)
          .placeholderFont({ size: 12 })
          .placeholderColor(DV.sub)
          .backgroundColor(Color.Transparent)
          .layoutWeight(1)
          .height(36)
          .onChange((v: string) => {
            this.keyword = v
          })
      }
      .width('92%')
      .height(38)
      .borderRadius(19)
      .backgroundColor(DV.bg)
      .border({ width: 1, color: DV.line })
      .margin({ top: 10 })
      
      // 内容区与Tab栏...
    }
    .width('100%')
    .height('100%')
    .backgroundColor(DV.bg)
  }
}

搜索栏采用了TextInput组件实现真正的输入功能,用户输入的内容通过onChange回调实时同步到状态变量中。相比纯展示型的搜索框,这种实现方式为后续的搜索功能扩展提供了基础。搜索栏的背景色使用了主题的背景色,与白色的头部形成对比,增强了搜索区域的可识别性。

底部导航系统

在这里插入图片描述

底部Tab栏采用白色卡片设计,六个Tab项分别对应首页、面镜、脚蹼、潜水服、配重和我的页面。选中态使用主色进行标记,未选中态使用次级灰色,视觉区分清晰。

const DV_TABS: DvTab[] = [
  { label: '首页', icon: '🌊' },
  { label: '面镜', icon: '🥽' },
  { label: '脚蹼', icon: '🦶' },
  { label: '潜水服', icon: '🩱' },
  { label: '配重', icon: '⚓' },
  { label: '我的', icon: '👤' }
]

Tab图标采用emoji实现,既保持了良好的视觉效果,又避免了图片资源的加载开销。每个Tab的图标与标签垂直排列,点击区域占据整个Tab宽度,确保了足够的触控面积,符合移动端交互设计规范。

应用流程图

个人中心

我的页面

订单管理

消费统计图表

潜水日志

商品模块

面镜Tab

按镜片类型筛选

双列网格展示

销量柱状图

点击商品卡片

商品详情弹窗

查看参数

加入购物车

编辑商品

删除商品

脚蹼Tab

按材质筛选

潜水服Tab

按厚度筛选

配重Tab

按类型筛选

首页模块

首页展示

深度仪表盘动画

气泡上升动效

快捷功能入口

潜水日志

装备保养

优惠券

潜水课程

用户操作

新增商品?

填写商品信息

选择分类属性

确认提交

列表数据更新

面镜Tab:商品展示与数据可视化

页面结构与筛选功能

面镜Tab是应用商品展示模块的典型代表,采用了筛选标签 + 销量柱状图 + 双列商品网格的复合布局。这种布局将数据可视化与商品列表有机结合,用户不仅可以浏览商品,还能直观了解各商品的销售情况。

@Component
struct DvMaskTab {
  @State wave: number = 0
  @State timer: number = -1
  @State chip: string = '全部'
  @State masks: DvMask[] = []
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showEdit: boolean = false
  @State showDel: boolean = false
  @State selIdx: number = 0
  @State delIdx: number = 0
  @State nName: string = ''
  @State nVolume: string = ''
  @State nPrice: string = ''
  @State ePrice: string = ''

  aboutToAppear(): void {
    this.masks = DV_MASKS.slice()
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }
  
  // ... build方法
}

组件状态设计的合理性直接影响代码的可维护性。对于商品类Tab组件,通常需要维护以下几类状态:动画驱动状态(wave、timer)、筛选状态(chip)、列表数据状态(masks)、弹窗显示状态(showDetail、showAdd等)、选中索引(selIdx、delIdx)、表单输入状态(nName、nPrice等)。合理命名和组织这些状态变量,能够让组件逻辑更加清晰易读。

组件维护了丰富的状态变量,包括动画帧计数器、当前筛选标签、面镜列表数据、各种弹窗的显示状态、选中项索引以及表单输入值。这些状态变量协同工作,支撑起整个页面的交互逻辑。组件在aboutToAppear生命周期中初始化数据并启动动画定时器,在aboutToDisappear中清理资源,确保生命周期管理的完整性。

销量柱状图可视化

面镜Tab的一个亮点是销量柱状图的实现。它通过高度不等的柱形直观展示各款面镜的销量对比,柱形的颜色和透明度还会随动画变化,增强了视觉吸引力。

Column() {
  Text('销量柱状图')
    .fontSize(14)
    .fontWeight(FontWeight.Bold)
    .fontColor(DV.text)
    .alignSelf(ItemAlign.Start)
  Row({ space: 6 }) {
    ForEach(DV_MASKS, (m: DvMask, i: number) => {
      Column() {
        Row() {
          Column()
            .width(16)
            .height(dvVBarH(m.sold, dvMaxMaskSold()))
            .backgroundColor(i === 7 ? DV.accent : DV.primary)
            .borderRadius(4)
            .opacity(dvPulse(this.wave + i * 12))
        }
        .height(110)
        .alignItems(VerticalAlign.Bottom)

        Text(m.name.charAt(0))
          .fontSize(9)
          .fontColor(DV.sub)
          .margin({ top: 6 })
      }
    })
  }
  .width('100%')
  .margin({ top: 12 })
}
.width('92%')
.padding(14)
.backgroundColor(DV.card)
.borderRadius(14)
.border({ width: 1, color: DV.line })
.margin({ top: 14 })

数据可视化是提升应用专业感的重要手段。即使是简单的柱状图,也能让用户快速把握数据的整体分布和相对关系。在ArkTS中实现图表无需依赖第三方库,通过基础布局组件结合数据计算函数即可完成。这种原生实现的方式不仅减少了依赖体积,还能根据业务需求灵活定制样式和交互。

柱状图的高度通过dvVBarH函数动态计算,该函数将销量值映射到像素高度,最大值对应110像素的柱高。每个柱形的透明度应用了脉冲动画效果,且通过索引偏移实现错峰脉冲,形成此起彼伏的视觉效果。销量最高的柱形使用强调色突出显示,帮助用户快速识别热销商品。

双列商品网格

商品列表采用双列网格布局,每个商品卡片包含商品图标、名称、类型、评分、价格以及操作按钮。这种布局在有限的屏幕空间内展示了尽可能多的商品信息,同时保持了良好的可读性。

Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
  ForEach(this.masks, (m: DvMask, i: number) => {
    Column() {
      Column() {
        Text('🥽')
          .fontSize(26)
          .translate({ y: dvFloatY(this.wave, i) })
        Text(m.volume + 'ml')
          .fontSize(9)
          .fontColor(Color.White)
          .backgroundColor(DV.primary)
          .borderRadius(8)
          .padding({ left: 8, right: 8, top: 2, bottom: 2 })
          .margin({ top: 6 })
      }
      .width('100%')
      .height(88)
      .justifyContent(FlexAlign.Center)
      .backgroundColor(DV.bg)
      .borderRadius(12)
      .border({ width: 1, color: DV.line })

      Text(m.name)
        .fontSize(12)
        .fontWeight(FontWeight.Bold)
        .fontColor(DV.text)
        .margin({ top: 8 })
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Row({ space: 4 }) {
        Text(m.lens)
          .fontSize(9)
          .fontColor(DV.sub)
        Text('★' + m.rating)
          .fontSize(9)
          .fontColor(DV.accent)
      }
      .margin({ top: 4 })
      Text(dvPrice(m.price))
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor(DV.primary)
        .margin({ top: 4 })

      Row() {
        Text('编辑')
          .fontSize(10)
          .fontColor(DV.primary)
          .padding({ left: 10, right: 10, top: 4, bottom: 4 })
          .borderRadius(10)
          .border({ width: 1, color: DV.primary })
          .onClick(() => {
            this.selIdx = i
            this.ePrice = ''
            this.showEdit = true
          })
        Column()
          .width(8)
        Text('删除')
          .fontSize(10)
          .fontColor(DV.accent)
          .padding({ left: 10, right: 10, top: 4, bottom: 4 })
          .borderRadius(10)
          .border({ width: 1, color: DV.accent })
          .onClick(() => {
            this.delIdx = i
            this.showDel = true
          })
      }
      .margin({ top: 6 })

      Text('查看详情')
        .fontSize(10)
        .fontColor(Color.White)
        .backgroundColor(DV.primary)
        .borderRadius(10)
        .padding({ left: 14, right: 14, top: 5, bottom: 5 })
        .margin({ top: 4 })
        .onClick(() => {
          this.selIdx = i
          this.showDetail = true
        })
    }
    .width('48.5%')
    .padding(10)
    .backgroundColor(DV.card)
    .borderRadius(14)
    .border({ width: 1, color: DV.line })
    .margin({ top: 12 })
  })
}
.width('92%')

商品卡片的设计注重信息层级的划分:商品图标区域占据最大视觉比重,配合浮动动画吸引注意力;名称和价格是核心信息,使用粗体和主色突出;类型和评分是辅助信息,使用较小字号和次级颜色。操作按钮采用边框按钮样式,既提供了操作入口,又不会过度干扰商品信息的展示。

商品详情弹窗

点击"查看详情"按钮会弹出商品详情窗口,展示更全面的商品参数信息。详情弹窗包含商品图片、名称、镜片类型、容积、评分、销量、价格等完整信息,还配有容积对比进度条帮助用户理解参数含义。

if (this.showDetail) {
  Stack() {
    this.modalOverlay()
    Column() {
      Text('面镜详情')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(DV.text)
      Column() {
        Text('🥽')
          .fontSize(34)
          .translate({ y: dvFloatY(this.wave, 3) })
      }
      .width(90)
      .height(90)
      .justifyContent(FlexAlign.Center)
      .backgroundColor(DV.bg)
      .borderRadius(14)
      .border({ width: 1, color: DV.line })
      .margin({ top: 14 })

      Row() {
        Text('名称')
          .fontSize(12)
          .fontColor(DV.sub)
          .layoutWeight(1)
        Text(this.masks.length > this.selIdx ? this.masks[this.selIdx].name : '')
          .fontSize(13)
          .fontColor(DV.text)
      }
      .width('100%')
      .margin({ top: 14 })
      
      // ... 更多参数行
      
      Text('镜片容积对比 (越小越易平压)')
        .fontSize(11)
        .fontColor(DV.sub)
        .margin({ top: 14 })
        .alignSelf(ItemAlign.Start)
      Stack({ alignContent: Alignment.Start }) {
        Row()
          .width('100%')
          .height(8)
          .backgroundColor(DV.bg)
          .borderRadius(4)
        Row()
          .width(dvBarW(this.masks.length > this.selIdx ? this.masks[this.selIdx].volume : 0, 110))
          .height(8)
          .backgroundColor(DV.glow)
          .borderRadius(4)
      }
      .width('100%')
      .margin({ top: 6 })

      Row() {
        Text('关闭')
          .fontSize(13)
          .fontColor(DV.sub)
          .layoutWeight(1)
          .padding({ top: 10, bottom: 10 })
          .borderRadius(12)
          .backgroundColor(DV.bg)
          .onClick(() => {
            this.showDetail = false
          })
        Column()
          .width(12)
        Text('加入购物车')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
          .layoutWeight(1)
          .padding({ top: 10, bottom: 10 })
          .borderRadius(12)
          .backgroundColor(DV.primary)
          .onClick(() => {
            this.showDetail = false
          })
      }
      .width('100%')
      .margin({ top: 18 })
    }
    .width('86%')
    .padding(20)
    .backgroundColor(DV.card)
    .borderRadius(18)
    .constraintSize({ maxHeight: '80%' })
    .zIndex(1000)
  }
}

详情弹窗的设计体现了"渐进式信息披露"的UX原则。列表页只展示核心信息,确保浏览效率;详情页展示完整参数,满足深度了解的需求。同时,在详情页中加入可视化的参数对比(如容积进度条),比单纯的数字展示更直观易懂,能够有效帮助用户做出购买决策。

详情弹窗底部采用双按钮布局,"关闭"按钮使用浅色背景,"加入购物车"按钮使用主色背景,视觉权重分明,引导用户进行核心操作。弹窗还通过constraintSize限制了最大高度,确保在内容较多时不会超出屏幕范围,用户可以通过滚动查看完整内容。

首页:深度仪表盘与课程报名

深度仪表盘设计

首页最引人注目的是圆形深度仪表盘,它采用深蓝色圆形背景配合白色深度数值,模拟了真实潜水电脑表的显示效果。仪表盘中央的指针随动画摆动,周围环绕着上升的气泡,营造出身临其境的潜水氛围。

Stack() {
  Column() {
    Text('DEPTH')
      .fontSize(9)
      .fontColor('rgba(255,255,255,0.6)')
      .letterSpacing(2)
    Text('18')
      .fontSize(34)
      .fontWeight(FontWeight.Bold)
      .fontColor(Color.White)
      .margin({ top: 2 })
    Text('米 · 当前训练深度')
      .fontSize(9)
      .fontColor('rgba(255,255,255,0.7)')
      .margin({ top: 2 })
  }
  .justifyContent(FlexAlign.Center)

  Text('|')
    .fontSize(30)
    .fontWeight(FontWeight.Bold)
    .fontColor(DV.glow)
    .rotate({ angle: dvNeedle(this.wave) })
    .translate({ y: 26 })
}
.width(170)
.height(170)
.borderRadius(85)
.backgroundColor(DV.deep)
.border({ width: 4, color: DV.primary })

仪表盘的指针通过一个简单的竖线字符实现,配合rotate变换和dvNeedle函数计算的旋转角度,模拟出指针左右摆动的效果。这种用简单元素实现复杂视觉效果的方式,充分体现了声明式UI的灵活表达能力。

潜水课程列表

首页还展示了潜水课程报名入口,以列表形式呈现不同级别的课程信息。每门课程包含课程名称、价格和时长,报名按钮采用脉冲动画吸引用户点击。

Row() {
  Text('🎓')
    .fontSize(18)
  Column() {
    Text('AIDA2 自由潜')
      .fontSize(13)
      .fontWeight(FontWeight.Bold)
      .fontColor(DV.text)
    Text('¥2880 · 3 天')
      .fontSize(10)
      .fontColor(DV.sub)
      .margin({ top: 3 })
  }
  .alignItems(HorizontalAlign.Start)
  .layoutWeight(1)
  .margin({ left: 10 })

  Text('报名')
    .fontSize(11)
    .fontWeight(FontWeight.Bold)
    .fontColor(Color.White)
    .padding({ left: 14, right: 14, top: 6, bottom: 6 })
    .borderRadius(12)
    .backgroundColor(DV.primary)
    .opacity(dvPulse(this.wave + 30))
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.border({ width: { bottom: 1 }, color: DV.line })

课程列表采用分隔线式设计,每个课程项底部有一条细分隔线,清晰地划分了不同的课程选项。报名按钮的脉冲动画通过不同的相位偏移实现错峰闪烁,避免了多个按钮同时闪烁造成的视觉混乱。

技术实现对比表格

技术特性 实现方式 性能表现 适用场景
气泡动画 数学函数 + translateY/translateX 优秀,纯GPU合成层操作 装饰性背景动画
深度仪表盘 Stack叠加 + rotate旋转 优秀,变换属性不触发重排 数据展示与品牌展示
商品筛选 纯函数过滤 + 状态驱动列表 良好,数据量小时性能优异 分类商品列表筛选
销量柱状图 ForEach + 动态height计算 良好,数据项固定时稳定 数据可视化展示
双列网格布局 Flex wrap + SpaceBetween 优秀,原生布局引擎优化 商品卡片式展示
详情弹窗 Stack叠加 + zIndex层级 良好,弹窗数量多时注意优化 表单、详情、确认操作
搜索功能 TextInput + 状态绑定 优秀,原生输入组件性能好 关键词搜索输入
数据管理 组件内部@State + 数组操作 良好,中小规模数据适用 单页面内数据维护

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

interface DvTheme {
  bg: string
  primary: string
  accent: string
  card: string
  text: string
  sub: string
  line: string
  glow: string
  deep: string
}

const DV: DvTheme = {
  bg: '#EAF6FA',
  primary: '#0B5E8A',
  accent: '#FF8A5C',
  card: '#FFFFFF',
  text: '#0E3A52',
  sub: '#7FA8BC',
  line: '#D4E8F0',
  glow: '#57C4E5',
  deep: '#063A5C'
}

interface DvMask {
  name: string
  volume: number
  lens: string
  price: number
  sold: number
  rating: number
}

interface DvFin {
  name: string
  material: string
  length: number
  price: number
  sold: number
  stiffness: number
}

interface DvSuit {
  name: string
  thickness: number
  temp: number
  style: string
  price: number
  sold: number
}

interface DvWeight {
  name: string
  kind: string
  kg: number
  price: number
  stock: number
}

interface DvLog {
  date: string
  site: string
  depth: number
  minutes: number
  temp: number
}

interface DvOrder {
  id: string
  name: string
  price: number
  status: string
  date: string
}

interface DvMonth {
  m: string
  pay: number
}

interface DvTab {
  label: string
  icon: string
}

const DV_MASKS: DvMask[] = [
  { name: '深渊低容积面镜', volume: 65, lens: '单镜片', price: 599, sold: 812, rating: 4.9 },
  { name: '自由潜竞赛镜', volume: 58, lens: '单镜片', price: 880, sold: 446, rating: 4.8 },
  { name: '珊瑚礁广角镜', volume: 92, lens: '双镜片', price: 369, sold: 1023, rating: 4.7 },
  { name: '光学度数定制镜', volume: 88, lens: '光学', price: 728, sold: 312, rating: 4.6 },
  { name: '墨蓝硅胶贴合镜', volume: 74, lens: '单镜片', price: 458, sold: 905, rating: 4.8 },
  { name: '夜潜荧光边框镜', volume: 80, lens: '双镜片', price: 526, sold: 388, rating: 4.5 },
  { name: '黑水摄影专用镜', volume: 70, lens: '单镜片', price: 698, sold: 275, rating: 4.9 },
  { name: '玻璃硅胶经典镜', volume: 105, lens: '双镜片', price: 249, sold: 1352, rating: 4.4 },
  { name: '极浅训练软裙镜', volume: 62, lens: '单镜片', price: 645, sold: 198, rating: 4.7 },
  { name: '联名限定渐变镜', volume: 78, lens: '双镜片', price: 999, sold: 156, rating: 5.0 }
]

const DV_FINS: DvFin[] = [
  { name: '碳纤维长脚蹼', material: '碳纤', length: 96, price: 3280, sold: 236, stiffness: 4 },
  { name: '玻璃纤维竞技蹼', material: '玻纤', length: 91, price: 1880, sold: 458, stiffness: 3 },
  { name: '热塑训练蹼', material: '塑胶', length: 84, price: 680, sold: 890, stiffness: 2 },
  { name: '复合材质旅行蹼', material: '复合', length: 78, price: 960, sold: 542, stiffness: 2 },
  { name: '竞技黑金碳蹼', material: '碳纤', length: 99, price: 4280, sold: 98, stiffness: 5 },
  { name: '软弹休闲玻纤蹼', material: '玻纤', length: 86, price: 1480, sold: 623, stiffness: 2 },
  { name: '入门分体蹼', material: '塑胶', length: 72, price: 399, sold: 1120, stiffness: 1 },
  { name: '大师定制碳蹼', material: '碳纤', length: 94, price: 5180, sold: 45, stiffness: 4 }
]

const DV_SUITS: DvSuit[] = [
  { name: '1.5mm 竞技皮', thickness: 1.5, temp: 27, style: '套头', price: 880, sold: 402 },
  { name: '3mm 开放细胞款', thickness: 3, temp: 24, style: '套头', price: 1480, sold: 586 },
  { name: '3mm 两件式套装', thickness: 3, temp: 23, style: '两件式', price: 1980, sold: 438 },
  { name: '5mm 深水保温衣', thickness: 5, temp: 18, style: '两件式', price: 2580, sold: 352 },
  { name: '7mm 极寒深潜衣', thickness: 7, temp: 12, style: '两件式', price: 3280, sold: 148 },
  { name: '2mm 女款彩绘皮', thickness: 2, temp: 26, style: '套头', price: 1180, sold: 296 },
  { name: '无帽子冲浪底衣', thickness: 0.5, temp: 29, style: '套头', price: 460, sold: 712 },
  { name: '4mm 黑水摄影衣', thickness: 4, temp: 20, style: '套头', price: 2180, sold: 205 }
]

const DV_WEIGHTS: DvWeight[] = [
  { name: '快卸配重带 1.5kg', kind: '配重带', kg: 1.5, price: 260, stock: 46 },
  { name: '软质铅袋 2kg', kind: '铅袋', kg: 2, price: 180, stock: 62 },
  { name: '颈配重 1kg', kind: '颈配重', kg: 1, price: 220, stock: 28 },
  { name: '整合式配重背心', kind: '背心', kg: 4, price: 680, stock: 15 },
  { name: '哑光铅块 0.5kg', kind: '铅块', kg: 0.5, price: 60, stock: 120 },
  { name: '涂层环保配重 1kg', kind: '铅块', kg: 1, price: 95, stock: 88 }
]

const DV_LOGS: DvLog[] = [
  { date: '08-22', site: '千岛湖训练池', depth: 18, minutes: 42, temp: 22 },
  { date: '08-15', site: '涠洲岛珊瑚区', depth: 25, minutes: 65, temp: 28 },
  { date: '08-03', site: '汤加大蓝洞', depth: 32, minutes: 78, temp: 26 },
  { date: '07-21', site: '薄荷岛断崖', depth: 28, minutes: 55, temp: 27 },
  { date: '07-06', site: '千岛湖训练池', depth: 15, minutes: 38, temp: 21 },
  { date: '06-18', site: '三亚蜈支洲', depth: 21, minutes: 48, temp: 29 }
]

const DV_ORDERS: DvOrder[] = [
  { id: 'DB52018801', name: '碳纤维长脚蹼', price: 3280, status: '已发货', date: '08-23' },
  { id: 'DB52018802', name: '3mm 两件式套装', price: 1980, status: '待收货', date: '08-16' },
  { id: 'DB52018803', name: '深渊低容积面镜', price: 599, status: '已完成', date: '08-02' },
  { id: 'DB52018804', name: '快卸配重带 1.5kg', price: 260, status: '已完成', date: '07-27' },
  { id: 'DB52018805', name: '竞技黑金碳蹼', price: 4280, status: '已签收', date: '07-11' },
  { id: 'DB52018806', name: '颈配重 1kg', price: 220, status: '售后中', date: '06-30' }
]

const DV_MONTHS: DvMonth[] = [
  { m: '3月', pay: 860 },
  { m: '4月', pay: 1980 },
  { m: '5月', pay: 640 },
  { m: '6月', pay: 2460 },
  { m: '7月', pay: 4280 },
  { m: '8月', pay: 3879 }
]

const DV_TABS: DvTab[] = [
  { label: '首页', icon: '🌊' },
  { label: '面镜', icon: '🥽' },
  { label: '脚蹼', icon: '🦶' },
  { label: '潜水服', icon: '🩱' },
  { label: '配重', icon: '⚓' },
  { label: '我的', icon: '👤' }
]

function dvPrice(p: number): string {
  return '¥' + p
}

function dvBarW(v: number, max: number): string {
  let pct: number = max <= 0 ? 0 : v * 100 / max
  if (pct > 100) {
    pct = 100
  }
  return pct.toFixed(0) + '%'
}

function dvVBarH(v: number, max: number): number {
  let h: number = max <= 0 ? 8 : v * 110 / max
  if (h < 8) {
    h = 8
  }
  return Math.round(h)
}

function dvBubbleY(w: number, i: number): number {
  return -((w * 2.2 + i * 41) % 160)
}

function dvBubbleX(w: number, i: number): number {
  return ((w * 3 + i * 53) % 26) - 13
}

function dvFloatY(w: number, i: number): number {
  return Math.sin((w + i * 18) * 0.11) * 4
}

function dvPulse(w: number): number {
  return 0.55 + 0.45 * Math.abs(Math.sin(w * 0.09))
}

function dvNeedle(w: number): number {
  return Math.sin(w * 0.06) * 40
}

function dvSpin(w: number, i: number): number {
  return (w * 4 + i * 90) % 360
}

function dvMaskFilter(arr: DvMask[], t: string): DvMask[] {
  let r: DvMask[] = []
  for (let i = 0; i < arr.length; i++) {
    if (t === '全部' || arr[i].lens === t) {
      r.push(arr[i])
    }
  }
  return r
}

function dvMaxMaskSold(): number {
  let m: number = 0
  for (let i = 0; i < DV_MASKS.length; i++) {
    if (DV_MASKS[i].sold > m) {
      m = DV_MASKS[i].sold
    }
  }
  return m
}

function dvMaxFinSold(): number {
  let m: number = 0
  for (let i = 0; i < DV_FINS.length; i++) {
    if (DV_FINS[i].sold > m) {
      m = DV_FINS[i].sold
    }
  }
  return m
}

function dvMaxSuitSold(): number {
  let m: number = 0
  for (let i = 0; i < DV_SUITS.length; i++) {
    if (DV_SUITS[i].sold > m) {
      m = DV_SUITS[i].sold
    }
  }
  return m
}

function dvMaxDepth(): number {
  let m: number = 0
  for (let i = 0; i < DV_LOGS.length; i++) {
    if (DV_LOGS[i].depth > m) {
      m = DV_LOGS[i].depth
    }
  }
  return m
}

function dvTotalPay(): number {
  let t: number = 0
  for (let i = 0; i < DV_MONTHS.length; i++) {
    t += DV_MONTHS[i].pay
  }
  return t
}

function dvMaxMonth(): number {
  let m: number = 0
  for (let i = 0; i < DV_MONTHS.length; i++) {
    if (DV_MONTHS[i].pay > m) {
      m = DV_MONTHS[i].pay
    }
  }
  return m
}

function dvStatusColor(s: string): string {
  if (s === '已发货' || s === '已签收' || s === '已完成') {
    return DV.primary
  }
  if (s === '售后中') {
    return DV.accent
  }
  return DV.sub
}

function dvMaterialColor(m: string): string {
  if (m === '碳纤') {
    return DV.deep
  }
  if (m === '玻纤') {
    return DV.primary
  }
  if (m === '塑胶') {
    return DV.glow
  }
  return DV.accent
}

function dvMaskTop(): DvMask[] {
  let r: DvMask[] = DV_MASKS.slice()
  r.sort((a: DvMask, b: DvMask) => b.sold - a.sold)
  return r.slice(0, 5)
}

@Entry
@Component
struct DvApp {
  @State tabIdx: number = 0
  @State keyword: string = ''

  build() {
    Column() {
      Row() {
        Column() {
          Text('DEEP BLUE')
            .fontSize(19)
            .fontWeight(FontWeight.Bold)
            .fontColor(DV.primary)
            .letterSpacing(2)
          Text('自由潜水装备馆 · 一口呼吸下潜')
            .fontSize(10)
            .fontColor(DV.sub)
            .margin({ top: 3 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text('🔔')
          .fontSize(19)
          .margin({ right: 14 })
        Text('🛒')
          .fontSize(19)
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14, bottom: 12 })
      .backgroundColor(DV.card)
      .border({ width: { bottom: 1 }, color: DV.line })

      Row() {
        Text('🔍')
          .fontSize(14)
          .margin({ left: 12 })
        TextInput()
          .fontSize(12)
          .fontColor(DV.text)
          .placeholderFont({ size: 12 })
          .placeholderColor(DV.sub)
          .backgroundColor(Color.Transparent)
          .layoutWeight(1)
          .height(36)
          .onChange((v: string) => {
            this.keyword = v
          })
      }
      .width('92%')
      .height(38)
      .borderRadius(19)
      .backgroundColor(DV.bg)
      .border({ width: 1, color: DV.line })
      .margin({ top: 10 })

      Column() {
        if (this.tabIdx === 0) {
          DvHomeTab()
        } else if (this.tabIdx === 1) {
          DvMaskTab()
        } else if (this.tabIdx === 2) {
          DvFinTab()
        } else if (this.tabIdx === 3) {
          DvSuitTab()
        } else if (this.tabIdx === 4) {
          DvWeightTab()
        } else {
          DvMineTab()
        }
      }
      .layoutWeight(1)
      .width('100%')

      Row() {
        ForEach(DV_TABS, (t: DvTab, i: number) => {
          Column() {
            Text(t.icon)
              .fontSize(19)
              .opacity(this.tabIdx === i ? 1 : 0.45)
            Text(t.label)
              .fontSize(10)
              .fontColor(this.tabIdx === i ? DV.primary : DV.sub)
              .fontWeight(this.tabIdx === i ? FontWeight.Bold : FontWeight.Normal)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.tabIdx = i
          })
        })
      }
      .width('100%')
      .height(56)
      .backgroundColor(DV.card)
      .border({ width: { top: 1 }, color: DV.line })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(DV.bg)
  }
}

@Component
struct DvHomeTab {
  @State wave: number = 0
  @State timer: number = -1
  @State showLog: boolean = false
  @State showCare: boolean = false
  @State showCoupon: boolean = false
  @State showCourse: boolean = false
  @State nSite: string = ''
  @State nDepth: string = ''
  @State nMinutes: string = ''

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }

  @Builder modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(6,30,48,0.62)')
      .position({ x: 0, y: 0 })
      .zIndex(999)
      .onClick(() => {
        this.showLog = false
        this.showCare = false
        this.showCoupon = false
        this.showCourse = false
      })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Column() {
            Stack() {
              Column() {
                Text('DEPTH')
                  .fontSize(9)
                  .fontColor('rgba(255,255,255,0.6)')
                  .letterSpacing(2)
                Text('18')
                  .fontSize(34)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(Color.White)
                  .margin({ top: 2 })
                Text('米 · 当前训练深度')
                  .fontSize(9)
                  .fontColor('rgba(255,255,255,0.7)')
                  .margin({ top: 2 })
              }
              .justifyContent(FlexAlign.Center)

              Text('|')
                .fontSize(30)
                .fontWeight(FontWeight.Bold)
                .fontColor(DV.glow)
                .rotate({ angle: dvNeedle(this.wave) })
                .translate({ y: 26 })
            }
            .width(170)
            .height(170)
            .borderRadius(85)
            .backgroundColor(DV.deep)
            .border({ width: 4, color: DV.primary })

            Row({ space: 10 }) {
              Column() {
                Text('○')
                  .fontSize(10)
                  .fontColor('rgba(255,255,255,0.8)')
                  .translate({ y: dvBubbleY(this.wave, 0), x: dvBubbleX(this.wave, 0) })
              }
              .width(24)
              .height(24)
              .justifyContent(FlexAlign.End)
              .alignItems(HorizontalAlign.Start)

              Column() {
                Text('○')
                  .fontSize(13)
                  .fontColor('rgba(255,255,255,0.6)')
                  .translate({ y: dvBubbleY(this.wave, 1), x: dvBubbleX(this.wave, 1) })
              }
              .width(26)
              .height(26)
              .justifyContent(FlexAlign.End)
              .alignItems(HorizontalAlign.Start)

              Column() {
                Text('○')
                  .fontSize(8)
                  .fontColor('rgba(255,255,255,0.7)')
                  .translate({ y: dvBubbleY(this.wave, 2), x: dvBubbleX(this.wave, 2) })
              }
              .width(20)
              .height(20)
              .justifyContent(FlexAlign.End)
              .alignItems(HorizontalAlign.Start)
            }
            .position({ x: 24, y: 24 })
          }
          .width('100%')
          .height(230)
          .backgroundColor(DV.primary)
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)

          Row() {
            ForEach(['下潜打卡', '装备保养', '领券中心', '教练课程'], (s: string, i: number) => {
              Column() {
                Column() {
                  Text(i === 0 ? '🌊' : (i === 1 ? '🔧' : (i === 2 ? '🎫' : '🎓')))
                    .fontSize(20)
                }
                .width(46)
                .height(46)
                .justifyContent(FlexAlign.Center)
                .backgroundColor(DV.bg)
                .borderRadius(14)

                Text(s)
                  .fontSize(10)
                  .fontColor(DV.text)
                  .margin({ top: 6 })
              }
              .layoutWeight(1)
              .onClick(() => {
                if (i === 0) {
                  this.showLog = true
                } else if (i === 1) {
                  this.showCare = true
                } else if (i === 2) {
                  this.showCoupon = true
                } else {
                  this.showCourse = true
                }
              })
            })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(DV.card)
          .borderRadius(14)
          .margin({ top: -30 })
          .border({ width: 1, color: DV.line })

          Column() {
            Text('最近下潜记录')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
              .alignSelf(ItemAlign.Start)
            ForEach(DV_LOGS, (l: DvLog, i: number) => {
              Row({ space: 10 }) {
                Column() {
                  Text(l.depth + 'm')
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(Color.White)
                }
                .width(52)
                .height(40)
                .justifyContent(FlexAlign.Center)
                .backgroundColor(i === 2 ? DV.accent : DV.primary)
                .borderRadius(10)

                Column() {
                  Text(l.site)
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(DV.text)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                  Text(l.date + ' · ' + l.minutes + ' 分钟 · ' + l.temp + '℃')
                    .fontSize(10)
                    .fontColor(DV.sub)
                    .margin({ top: 3 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)

                Text(dvBarW(l.depth, dvMaxDepth()))
                  .fontSize(11)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(DV.primary)
              }
              .width('100%')
              .padding({ top: 9, bottom: 9 })
              .border({ width: { bottom: 1 }, color: DV.line })
            })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(DV.card)
          .borderRadius(14)
          .margin({ top: 14 })
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('面镜热销榜')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
              .alignSelf(ItemAlign.Start)
              .margin({ bottom: 8 })
            ForEach(dvMaskTop(), (m: DvMask, i: number) => {
              Row({ space: 10 }) {
                Text((i + 1).toString())
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(Color.White)
                  .width(22)
                  .height(22)
                  .textAlign(TextAlign.Center)
                  .backgroundColor(i === 0 ? DV.accent : (i === 1 ? DV.primary : DV.glow))
                  .borderRadius(6)

                Column() {
                  Text(m.name)
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(DV.text)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                  Text('容积 ' + m.volume + 'ml · 已售 ' + m.sold)
                    .fontSize(10)
                    .fontColor(DV.sub)
                    .margin({ top: 3 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)

                Text(dvPrice(m.price))
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(DV.primary)
              }
              .width('100%')
              .padding({ top: 7, bottom: 7 })
            })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(DV.card)
          .borderRadius(14)
          .margin({ top: 14 })
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('本周潜水地推荐')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
              .alignSelf(ItemAlign.Start)
              .margin({ bottom: 10 })
            Scroll() {
              Row({ space: 12 }) {
                ForEach(['汤加 · 追鲸', '薄荷岛 · 断崖', '千岛湖 · 淡水', '三亚 · 珊瑚', '帕劳 · 蓝洞'], (s: string, i: number) => {
                  Column() {
                    Text('🌊')
                      .fontSize(24)
                      .translate({ y: dvFloatY(this.wave, i) })
                    Text(s)
                      .fontSize(11)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(DV.text)
                      .margin({ top: 8 })
                    Text('26-29℃')
                      .fontSize(9)
                      .fontColor(DV.sub)
                      .margin({ top: 3 })
                  }
                  .width(110)
                  .padding(12)
                  .backgroundColor(DV.bg)
                  .borderRadius(12)
                  .border({ width: 1, color: DV.line })
                })
              }
            }
            .scrollable(ScrollDirection.Horizontal)
            .width('100%')
          }
          .width('92%')
          .padding(14)
          .backgroundColor(DV.card)
          .borderRadius(14)
          .margin({ top: 14, bottom: 24 })
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
      }
      .width('100%')
      .height('100%')

      if (this.showLog) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('记录一次下潜')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Column() {
              Text('潜点名称')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nSite = v
                })
            }
            .width('100%')
            .margin({ top: 14 })
            Column() {
              Text('最大深度 (米)')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .type(InputType.Number)
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nDepth = v
                })
            }
            .width('100%')
            .margin({ top: 12 })
            Column() {
              Text('水下时长 (分钟)')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .type(InputType.Number)
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nMinutes = v
                })
            }
            .width('100%')
            .margin({ top: 12 })
            Row() {
              Text('取消')
                .fontSize(13)
                .fontColor(DV.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.bg)
                .onClick(() => {
                  this.showLog = false
                })
              Column()
                .width(12)
              Text('保存记录')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.primary)
                .onClick(() => {
                  this.nSite = ''
                  this.nDepth = ''
                  this.nMinutes = ''
                  this.showLog = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .constraintSize({ maxHeight: '80%' })
          .zIndex(1000)
        }
      }

      if (this.showCare) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('装备保养提醒')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            ForEach(['面镜裙边: 每 30 潜更换', '脚蹼脚套: 每 50 潜检查', '潜水服: 每次淡水冲洗', '配重带: 每季检查卡扣'], (s: string) => {
              Row() {
                Text('🌊')
                  .fontSize(13)
                Text(s)
                  .fontSize(12)
                  .fontColor(DV.text)
                  .margin({ left: 10 })
              }
              .width('100%')
              .padding({ top: 10, bottom: 10 })
              .border({ width: { bottom: 1 }, color: DV.line })
            })
            Text('好的装备状态是安全下潜的第一道防线。')
              .fontSize(10)
              .fontColor(DV.sub)
              .margin({ top: 12 })
            Text('知道了')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(Color.White)
              .width('100%')
              .textAlign(TextAlign.Center)
              .padding({ top: 10, bottom: 10 })
              .borderRadius(12)
              .backgroundColor(DV.primary)
              .margin({ top: 12 })
              .onClick(() => {
                this.showCare = false
              })
          }
          .width('82%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .zIndex(1000)
        }
      }

      if (this.showCoupon) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('领券中心')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Column() {
              Row() {
                Text('新人礼金')
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(Color.White)
                Text('满 500 减 60')
                  .fontSize(11)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(DV.accent)
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .borderRadius(12)
                  .backgroundColor(Color.White)
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceBetween)
              .padding(14)
              .backgroundColor(DV.primary)
              .borderRadius(12)
              .margin({ top: 10 })
              Text('装备专享 · 满 2000 减 260')
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor(DV.text)
                .width('100%')
                .padding(14)
                .backgroundColor(DV.bg)
                .borderRadius(12)
                .margin({ top: 10 })
              Text('潜水服季 · 满 1000 减 120')
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor(DV.text)
                .width('100%')
                .padding(14)
                .backgroundColor(DV.bg)
                .borderRadius(12)
                .margin({ top: 10 })
              Text('老带新 · 双方各得 50')
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor(DV.text)
                .width('100%')
                .padding(14)
                .backgroundColor(DV.bg)
                .borderRadius(12)
                .margin({ top: 10 })
            }
            .width('100%')
            Text('关闭')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.sub)
              .width('100%')
              .textAlign(TextAlign.Center)
              .padding({ top: 10, bottom: 10 })
              .borderRadius(12)
              .backgroundColor(DV.bg)
              .margin({ top: 14 })
              .onClick(() => {
                this.showCoupon = false
              })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .constraintSize({ maxHeight: '80%' })
          .zIndex(1000)
        }
      }

      if (this.showCourse) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('教练课程')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Column() {
              Row() {
                Text('🎓')
                  .fontSize(18)
                Column() {
                  Text('AIDA1 体验课')
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(DV.text)
                  Text('¥880 · 1 天')
                    .fontSize(10)
                    .fontColor(DV.sub)
                    .margin({ top: 3 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 10 })

                Text('报名')
                  .fontSize(11)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(Color.White)
                  .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                  .borderRadius(12)
                  .backgroundColor(DV.primary)
                  .opacity(dvPulse(this.wave))
              }
              .width('100%')
              .padding({ top: 10, bottom: 10 })
              .border({ width: { bottom: 1 }, color: DV.line })

              Row() {
                Text('🎓')
                  .fontSize(18)
                Column() {
                  Text('AIDA2 自由潜')
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(DV.text)
                  Text('¥2880 · 3 天')
                    .fontSize(10)
                    .fontColor(DV.sub)
                    .margin({ top: 3 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 10 })

                Text('报名')
                  .fontSize(11)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(Color.White)
                  .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                  .borderRadius(12)
                  .backgroundColor(DV.primary)
                  .opacity(dvPulse(this.wave + 30))
              }
              .width('100%')
              .padding({ top: 10, bottom: 10 })
              .border({ width: { bottom: 1 }, color: DV.line })

              Row() {
                Text('🎓')
                  .fontSize(18)
                Column() {
                  Text('泳池静态闭气')
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(DV.text)
                  Text('¥680 · 半天')
                    .fontSize(10)
                    .fontColor(DV.sub)
                    .margin({ top: 3 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 10 })

                Text('报名')
                  .fontSize(11)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(Color.White)
                  .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                  .borderRadius(12)
                  .backgroundColor(DV.primary)
                  .opacity(dvPulse(this.wave + 60))
              }
              .width('100%')
              .padding({ top: 10, bottom: 10 })
              .border({ width: { bottom: 1 }, color: DV.line })

              Row() {
                Text('🎓')
                  .fontSize(18)
                Column() {
                  Text('深度进阶私教')
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(DV.text)
                  Text('¥1980 · 1 天')
                    .fontSize(10)
                    .fontColor(DV.sub)
                    .margin({ top: 3 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 10 })

                Text('报名')
                  .fontSize(11)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(Color.White)
                  .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                  .borderRadius(12)
                  .backgroundColor(DV.primary)
                  .opacity(dvPulse(this.wave + 90))
              }
              .width('100%')
              .padding({ top: 10, bottom: 10 })
            }
            .width('100%')
            Text('取消')
              .fontSize(13)
              .fontColor(DV.sub)
              .width('100%')
              .textAlign(TextAlign.Center)
              .padding({ top: 10, bottom: 10 })
              .borderRadius(12)
              .backgroundColor(DV.bg)
              .margin({ top: 12 })
              .onClick(() => {
                this.showCourse = false
              })
          }
          .width('82%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .constraintSize({ maxHeight: '80%' })
          .zIndex(1000)
        }
      }
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct DvMaskTab {
  @State wave: number = 0
  @State timer: number = -1
  @State chip: string = '全部'
  @State masks: DvMask[] = []
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showEdit: boolean = false
  @State showDel: boolean = false
  @State selIdx: number = 0
  @State delIdx: number = 0
  @State nName: string = ''
  @State nVolume: string = ''
  @State nPrice: string = ''
  @State ePrice: string = ''

  aboutToAppear(): void {
    this.masks = DV_MASKS.slice()
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }

  @Builder modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(6,30,48,0.62)')
      .position({ x: 0, y: 0 })
      .zIndex(999)
      .onClick(() => {
        this.showDetail = false
        this.showAdd = false
        this.showEdit = false
        this.showDel = false
      })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Text('面镜 MASK')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(DV.primary)
            .margin({ top: 16, left: 16 })
          Text('低容积 · 高贴合 · 看清每一米深蓝')
            .fontSize(12)
            .fontColor(DV.sub)
            .margin({ top: 4, left: 16 })

          Scroll() {
            Row({ space: 8 }) {
              ForEach(['全部', '单镜片', '双镜片', '光学'], (t: string) => {
                Text(t)
                  .fontSize(12)
                  .fontColor(this.chip === t ? Color.White : DV.text)
                  .backgroundColor(this.chip === t ? DV.primary : DV.card)
                  .borderRadius(16)
                  .padding({ left: 14, right: 14, top: 7, bottom: 7 })
                  .border({ width: 1, color: DV.line })
                  .onClick(() => {
                    this.chip = t
                    this.masks = dvMaskFilter(DV_MASKS, t)
                  })
              })
            }
          }
          .scrollable(ScrollDirection.Horizontal)
          .width('92%')
          .margin({ top: 12 })

          Column() {
            Text('销量柱状图')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
              .alignSelf(ItemAlign.Start)
            Row({ space: 6 }) {
              ForEach(DV_MASKS, (m: DvMask, i: number) => {
                Column() {
                  Row() {
                    Column()
                      .width(16)
                      .height(dvVBarH(m.sold, dvMaxMaskSold()))
                      .backgroundColor(i === 7 ? DV.accent : DV.primary)
                      .borderRadius(4)
                      .opacity(dvPulse(this.wave + i * 12))
                  }
                  .height(110)
                  .alignItems(VerticalAlign.Bottom)

                  Text(m.name.charAt(0))
                    .fontSize(9)
                    .fontColor(DV.sub)
                    .margin({ top: 6 })
                }
              })
            }
            .width('100%')
            .margin({ top: 12 })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(DV.card)
          .borderRadius(14)
          .border({ width: 1, color: DV.line })
          .margin({ top: 14 })

          Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
            ForEach(this.masks, (m: DvMask, i: number) => {
              Column() {
                Column() {
                  Text('🥽')
                    .fontSize(26)
                    .translate({ y: dvFloatY(this.wave, i) })
                  Text(m.volume + 'ml')
                    .fontSize(9)
                    .fontColor(Color.White)
                    .backgroundColor(DV.primary)
                    .borderRadius(8)
                    .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                    .margin({ top: 6 })
                }
                .width('100%')
                .height(88)
                .justifyContent(FlexAlign.Center)
                .backgroundColor(DV.bg)
                .borderRadius(12)
                .border({ width: 1, color: DV.line })

                Text(m.name)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(DV.text)
                  .margin({ top: 8 })
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Row({ space: 4 }) {
                  Text(m.lens)
                    .fontSize(9)
                    .fontColor(DV.sub)
                  Text('★' + m.rating)
                    .fontSize(9)
                    .fontColor(DV.accent)
                }
                .margin({ top: 4 })
                Text(dvPrice(m.price))
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(DV.primary)
                  .margin({ top: 4 })

                Row() {
                  Text('编辑')
                    .fontSize(10)
                    .fontColor(DV.primary)
                    .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                    .borderRadius(10)
                    .border({ width: 1, color: DV.primary })
                    .onClick(() => {
                      this.selIdx = i
                      this.ePrice = ''
                      this.showEdit = true
                    })
                  Column()
                    .width(8)
                  Text('删除')
                    .fontSize(10)
                    .fontColor(DV.accent)
                    .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                    .borderRadius(10)
                    .border({ width: 1, color: DV.accent })
                    .onClick(() => {
                      this.delIdx = i
                      this.showDel = true
                    })
                }
                .margin({ top: 6 })

                Text('查看详情')
                  .fontSize(10)
                  .fontColor(Color.White)
                  .backgroundColor(DV.primary)
                  .borderRadius(10)
                  .padding({ left: 14, right: 14, top: 5, bottom: 5 })
                  .margin({ top: 4 })
                  .onClick(() => {
                    this.selIdx = i
                    this.showDetail = true
                  })
              }
              .width('48.5%')
              .padding(10)
              .backgroundColor(DV.card)
              .borderRadius(14)
              .border({ width: 1, color: DV.line })
              .margin({ top: 12 })
            })
          }
          .width('92%')

          Row() {
            Text('+ 上架新面镜')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(Color.White)
          }
          .width('92%')
          .height(46)
          .justifyContent(FlexAlign.Center)
          .backgroundColor(DV.primary)
          .borderRadius(14)
          .margin({ top: 16, bottom: 24 })
          .opacity(dvPulse(this.wave * 2))
          .onClick(() => {
            this.showAdd = true
          })
        }
        .width('100%')
      }
      .width('100%')
      .height('100%')

      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('面镜详情')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Column() {
              Text('🥽')
                .fontSize(34)
                .translate({ y: dvFloatY(this.wave, 3) })
            }
            .width(90)
            .height(90)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(DV.bg)
            .borderRadius(14)
            .border({ width: 1, color: DV.line })
            .margin({ top: 14 })

            Row() {
              Text('名称')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text(this.masks.length > this.selIdx ? this.masks[this.selIdx].name : '')
                .fontSize(13)
                .fontColor(DV.text)
            }
            .width('100%')
            .margin({ top: 14 })
            Row() {
              Text('镜片类型')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text(this.masks.length > this.selIdx ? this.masks[this.selIdx].lens : '')
                .fontSize(13)
                .fontColor(DV.text)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Text('镜片容积')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text((this.masks.length > this.selIdx ? this.masks[this.selIdx].volume : 0) + ' ml')
                .fontSize(13)
                .fontColor(DV.text)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Text('评分')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text('★ ' + (this.masks.length > this.selIdx ? this.masks[this.selIdx].rating : 0))
                .fontSize(13)
                .fontColor(DV.accent)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Text('已售')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text((this.masks.length > this.selIdx ? this.masks[this.selIdx].sold : 0) + ' 件')
                .fontSize(13)
                .fontColor(DV.text)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Text('售价')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text(dvPrice(this.masks.length > this.selIdx ? this.masks[this.selIdx].price : 0))
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(DV.primary)
            }
            .width('100%')
            .margin({ top: 8 })

            Text('镜片容积对比 (越小越易平压)')
              .fontSize(11)
              .fontColor(DV.sub)
              .margin({ top: 14 })
              .alignSelf(ItemAlign.Start)
            Stack({ alignContent: Alignment.Start }) {
              Row()
                .width('100%')
                .height(8)
                .backgroundColor(DV.bg)
                .borderRadius(4)
              Row()
                .width(dvBarW(this.masks.length > this.selIdx ? this.masks[this.selIdx].volume : 0, 110))
                .height(8)
                .backgroundColor(DV.glow)
                .borderRadius(4)
            }
            .width('100%')
            .margin({ top: 6 })

            Row() {
              Text('关闭')
                .fontSize(13)
                .fontColor(DV.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.bg)
                .onClick(() => {
                  this.showDetail = false
                })
              Column()
                .width(12)
              Text('加入购物车')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.primary)
                .onClick(() => {
                  this.showDetail = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .constraintSize({ maxHeight: '80%' })
          .zIndex(1000)
        }
      }

      if (this.showAdd) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('上架新面镜')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Column() {
              Text('面镜名称')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nName = v
                })
            }
            .width('100%')
            .margin({ top: 14 })
            Column() {
              Text('镜片容积 (ml)')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .type(InputType.Number)
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nVolume = v
                })
            }
            .width('100%')
            .margin({ top: 12 })
            Column() {
              Text('售价 (元)')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .type(InputType.Number)
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nPrice = v
                })
            }
            .width('100%')
            .margin({ top: 12 })
            Row() {
              Text('取消')
                .fontSize(13)
                .fontColor(DV.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.bg)
                .onClick(() => {
                  this.showAdd = false
                })
              Column()
                .width(12)
              Text('确认上架')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.primary)
                .onClick(() => {
                  let it: DvMask = {
                    name: this.nName === '' ? '未命名面镜' : this.nName,
                    volume: isNaN(parseInt(this.nVolume)) ? 80 : parseInt(this.nVolume),
                    lens: '单镜片',
                    price: isNaN(parseInt(this.nPrice)) ? 499 : parseInt(this.nPrice),
                    sold: 0,
                    rating: 4.5
                  }
                  this.masks.unshift(it)
                  this.nName = ''
                  this.nVolume = ''
                  this.nPrice = ''
                  this.showAdd = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .constraintSize({ maxHeight: '80%' })
          .zIndex(1000)
        }
      }

      if (this.showEdit) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('编辑面镜')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Text('正在编辑: ' + (this.masks.length > this.selIdx ? this.masks[this.selIdx].name : ''))
              .fontSize(12)
              .fontColor(DV.sub)
              .margin({ top: 8 })
            Column() {
              Text('调整售价 (元)')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .type(InputType.Number)
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.ePrice = v
                })
            }
            .width('100%')
            .margin({ top: 14 })
            Row() {
              Text('取消')
                .fontSize(13)
                .fontColor(DV.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.bg)
                .onClick(() => {
                  this.showEdit = false
                })
              Column()
                .width(12)
              Text('保存修改')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.primary)
                .onClick(() => {
                  if (!isNaN(parseInt(this.ePrice)) && this.masks.length > this.selIdx) {
                    this.masks[this.selIdx].price = parseInt(this.ePrice)
                  }
                  this.ePrice = ''
                  this.showEdit = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .zIndex(1000)
        }
      }

      if (this.showDel) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('删除面镜')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Text('确认从货架删除「' + (this.masks.length > this.delIdx ? this.masks[this.delIdx].name : '') + '」?删除后不可恢复。')
              .fontSize(12)
              .fontColor(DV.sub)
              .textAlign(TextAlign.Center)
              .margin({ top: 12 })
              .lineHeight(18)
            Row() {
              Text('取消')
                .fontSize(13)
                .fontColor(DV.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.bg)
                .onClick(() => {
                  this.showDel = false
                })
              Column()
                .width(12)
              Text('确认删除')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.accent)
                .onClick(() => {
                  this.masks.splice(this.delIdx, 1)
                  this.showDel = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('76%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .zIndex(1000)
        }
      }
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct DvFinTab {
  @State wave: number = 0
  @State timer: number = -1
  @State fins: DvFin[] = []
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showDel: boolean = false
  @State selIdx: number = 0
  @State delIdx: number = 0
  @State nName: string = ''
  @State nLength: string = ''
  @State nPrice: string = ''

  aboutToAppear(): void {
    this.fins = DV_FINS.slice()
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }

  @Builder modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(6,30,48,0.62)')
      .position({ x: 0, y: 0 })
      .zIndex(999)
      .onClick(() => {
        this.showDetail = false
        this.showAdd = false
        this.showDel = false
      })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Row() {
            Column() {
              Text('脚蹼 FIN')
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor(DV.primary)
              Text('碳纤推进 · 一蹬三米')
                .fontSize(12)
                .fontColor(DV.sub)
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('+ 上架')
              .fontSize(12)
              .fontColor(Color.White)
              .backgroundColor(DV.primary)
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 7, bottom: 7 })
              .onClick(() => {
                this.showAdd = true
              })
          }
          .width('92%')
          .margin({ top: 16 })

          Column() {
            Text('蹼片长度对比 (cm)')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
              .alignSelf(ItemAlign.Start)
              .margin({ bottom: 10 })
            ForEach(this.fins, (f: DvFin) => {
              Column() {
                Row() {
                  Circle()
                    .width(10)
                    .height(10)
                    .fill(dvMaterialColor(f.material))
                  Text(f.name)
                    .fontSize(11)
                    .fontColor(DV.text)
                    .layoutWeight(1)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                    .margin({ left: 8 })
                  Text(f.length + 'cm')
                    .fontSize(11)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(DV.primary)
                }
                Stack({ alignContent: Alignment.Start }) {
                  Row()
                    .width('100%')
                    .height(8)
                    .backgroundColor(DV.bg)
                    .borderRadius(4)
                  Row()
                    .width(dvBarW(f.length, 100))
                    .height(8)
                    .backgroundColor(dvMaterialColor(f.material))
                    .borderRadius(4)
                }
                .width('100%')
                .margin({ top: 4 })
              }
              .margin({ bottom: 10 })
            })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(DV.card)
          .borderRadius(14)
          .border({ width: 1, color: DV.line })
          .margin({ top: 14 })

          ForEach(this.fins, (f: DvFin, i: number) => {
            Column() {
              Row({ space: 12 }) {
                Column() {
                  Text('🦶')
                    .fontSize(24)
                    .rotate({ angle: dvSpin(this.wave, i) > 180 ? 12 : -12 })
                }
                .width(70)
                .height(70)
                .justifyContent(FlexAlign.Center)
                .backgroundColor(dvMaterialColor(f.material))
                .borderRadius(14)

                Column() {
                  Text(f.name)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(DV.text)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                  Row({ space: 6 }) {
                    Text(f.material)
                      .fontSize(10)
                      .fontColor(Color.White)
                      .backgroundColor(dvMaterialColor(f.material))
                      .borderRadius(8)
                      .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                    Text('硬度 ' + f.stiffness)
                      .fontSize(10)
                      .fontColor(DV.sub)
                    Text('长度 ' + f.length + 'cm')
                      .fontSize(10)
                      .fontColor(DV.sub)
                  }
                  .margin({ top: 6 })

                  Row() {
                    Column() {
                      Text(dvPrice(f.price))
                        .fontSize(16)
                        .fontWeight(FontWeight.Bold)
                        .fontColor(DV.primary)
                      Text('已售 ' + f.sold)
                        .fontSize(10)
                        .fontColor(DV.sub)
                        .margin({ top: 2 })
                    }
                    .alignItems(HorizontalAlign.Start)
                    .layoutWeight(1)

                    Text('删除')
                      .fontSize(10)
                      .fontColor(DV.accent)
                      .padding({ left: 10, right: 10, top: 5, bottom: 5 })
                      .borderRadius(10)
                      .border({ width: 1, color: DV.accent })
                      .onClick(() => {
                        this.delIdx = i
                        this.showDel = true
                      })
                    Text('详情')
                      .fontSize(10)
                      .fontColor(Color.White)
                      .backgroundColor(DV.primary)
                      .borderRadius(10)
                      .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                      .onClick(() => {
                        this.selIdx = i
                        this.showDetail = true
                      })
                  }
                  .width('100%')
                  .alignItems(VerticalAlign.Bottom)
                  .margin({ top: 8 })
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
              }
              .width('100%')

              Column() {
                Text('硬度指示')
                  .fontSize(9)
                  .fontColor(DV.sub)
                Row({ space: 4 }) {
                  ForEach([0, 1, 2, 3, 4], (k: number) => {
                    Column()
                      .width(12)
                      .height(6)
                      .borderRadius(3)
                      .backgroundColor(k < f.stiffness ? DV.accent : DV.line)
                  })
                }
                .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start)
              .margin({ top: 10 })
            }
            .width('92%')
            .padding(14)
            .backgroundColor(DV.card)
            .borderRadius(14)
            .border({ width: 1, color: DV.line })
            .margin({ top: 12 })
            .translate({ y: dvFloatY(this.wave, i) })
          })

          Column() {
            Text('销量柱状图')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
              .alignSelf(ItemAlign.Start)
            Row({ space: 8 }) {
              ForEach(this.fins, (f: DvFin, i: number) => {
                Column() {
                  Row() {
                    Column()
                      .width(18)
                      .height(dvVBarH(f.sold, dvMaxFinSold()))
                      .backgroundColor(dvMaterialColor(f.material))
                      .borderRadius(4)
                  }
                  .height(100)
                  .alignItems(VerticalAlign.Bottom)

                  Text(f.material)
                    .fontSize(9)
                    .fontColor(DV.sub)
                    .margin({ top: 6 })
                }
              })
            }
            .width('100%')
            .margin({ top: 12 })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(DV.card)
          .borderRadius(14)
          .border({ width: 1, color: DV.line })
          .margin({ top: 16, bottom: 24 })
        }
        .width('100%')
      }
      .width('100%')
      .height('100%')

      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('脚蹼详情')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Row({ space: 12 }) {
              Column() {
                Text('🦶')
                  .fontSize(26)
                  .rotate({ angle: dvSpin(this.wave, 1) > 180 ? 10 : -10 })
              }
              .width(70)
              .height(70)
              .justifyContent(FlexAlign.Center)
              .backgroundColor(this.fins.length > this.selIdx ? dvMaterialColor(this.fins[this.selIdx].material) : DV.primary)
              .borderRadius(14)

              Column() {
                Text(this.fins.length > this.selIdx ? this.fins[this.selIdx].name : '')
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(DV.text)
                Text((this.fins.length > this.selIdx ? this.fins[this.selIdx].material : '') + '材质 · ' + (this.fins.length > this.selIdx ? this.fins[this.selIdx].length : 0) + 'cm')
                  .fontSize(11)
                  .fontColor(DV.sub)
                  .margin({ top: 6 })
                Text(dvPrice(this.fins.length > this.selIdx ? this.fins[this.selIdx].price : 0))
                  .fontSize(16)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(DV.primary)
                  .margin({ top: 8 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
            }
            .width('100%')
            .margin({ top: 16 })

            Row() {
              Text('硬度等级')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text((this.fins.length > this.selIdx ? this.fins[this.selIdx].stiffness : 0) + ' / 5')
                .fontSize(13)
                .fontColor(DV.text)
            }
            .width('100%')
            .margin({ top: 14 })
            Row() {
              Text('适用深度')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text((this.fins.length > this.selIdx && this.fins[this.selIdx].stiffness > 3 ? '20-40m 竞技' : '10-25m 休闲'))
                .fontSize(13)
                .fontColor(DV.text)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Text('已售')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text((this.fins.length > this.selIdx ? this.fins[this.selIdx].sold : 0) + ' 件')
                .fontSize(13)
                .fontColor(DV.text)
            }
            .width('100%')
            .margin({ top: 8 })

            Row() {
              Text('关闭')
                .fontSize(13)
                .fontColor(DV.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.bg)
                .onClick(() => {
                  this.showDetail = false
                })
              Column()
                .width(12)
              Text('立即购买')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.primary)
                .onClick(() => {
                  this.showDetail = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .constraintSize({ maxHeight: '80%' })
          .zIndex(1000)
        }
      }

      if (this.showAdd) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('上架新脚蹼')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Column() {
              Text('脚蹼名称')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nName = v
                })
            }
            .width('100%')
            .margin({ top: 14 })
            Column() {
              Text('蹼片长度 (cm)')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .type(InputType.Number)
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nLength = v
                })
            }
            .width('100%')
            .margin({ top: 12 })
            Column() {
              Text('售价 (元)')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .type(InputType.Number)
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nPrice = v
                })
            }
            .width('100%')
            .margin({ top: 12 })
            Row() {
              Text('取消')
                .fontSize(13)
                .fontColor(DV.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.bg)
                .onClick(() => {
                  this.showAdd = false
                })
              Column()
                .width(12)
              Text('确认上架')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.primary)
                .onClick(() => {
                  let it: DvFin = {
                    name: this.nName === '' ? '未命名脚蹼' : this.nName,
                    material: '玻纤',
                    length: isNaN(parseInt(this.nLength)) ? 88 : parseInt(this.nLength),
                    price: isNaN(parseInt(this.nPrice)) ? 1580 : parseInt(this.nPrice),
                    sold: 0,
                    stiffness: 3
                  }
                  this.fins.unshift(it)
                  this.nName = ''
                  this.nLength = ''
                  this.nPrice = ''
                  this.showAdd = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .constraintSize({ maxHeight: '80%' })
          .zIndex(1000)
        }
      }

      if (this.showDel) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('下架脚蹼')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Text('「' + (this.fins.length > this.delIdx ? this.fins[this.delIdx].name : '') + '」下架后将不再对外展示,库存记录保留 90 天。')
              .fontSize(12)
              .fontColor(DV.sub)
              .textAlign(TextAlign.Center)
              .margin({ top: 12 })
              .lineHeight(18)
            Row() {
              Text('取消')
                .fontSize(13)
                .fontColor(DV.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.bg)
                .onClick(() => {
                  this.showDel = false
                })
              Column()
                .width(12)
              Text('确认下架')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.accent)
                .onClick(() => {
                  this.fins.splice(this.delIdx, 1)
                  this.showDel = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('78%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .zIndex(1000)
        }
      }
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct DvSuitTab {
  @State wave: number = 0
  @State timer: number = -1
  @State suits: DvSuit[] = []
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showEdit: boolean = false
  @State selIdx: number = 0
  @State editIdx: number = 0
  @State nName: string = ''
  @State nThick: string = ''
  @State nPrice: string = ''
  @State eTemp: string = ''

  aboutToAppear(): void {
    this.suits = DV_SUITS.slice()
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }

  @Builder modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(6,30,48,0.62)')
      .position({ x: 0, y: 0 })
      .zIndex(999)
      .onClick(() => {
        this.showDetail = false
        this.showAdd = false
        this.showEdit = false
      })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Text('潜水服 WETSUIT')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(DV.primary)
            .margin({ top: 16, left: 16 })
          Text('按水温选厚度 · 开放细胞锁温')
            .fontSize(12)
            .fontColor(DV.sub)
            .margin({ top: 4, left: 16 })

          Column() {
            Text('厚度 → 适温水温对照')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
              .alignSelf(ItemAlign.Start)
              .margin({ bottom: 12 })
            ForEach(this.suits, (s: DvSuit) => {
              Column() {
                Row() {
                  Text(s.thickness + 'mm')
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(Color.White)
                    .width(52)
                    .textAlign(TextAlign.Center)
                    .backgroundColor(DV.primary)
                    .borderRadius(8)
                    .padding({ top: 3, bottom: 3 })
                  Text(s.name)
                    .fontSize(11)
                    .fontColor(DV.text)
                    .layoutWeight(1)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                    .margin({ left: 8 })
                  Text('适温 ' + s.temp + '℃')
                    .fontSize(11)
                    .fontColor(DV.accent)
                }
                Stack({ alignContent: Alignment.End }) {
                  Row()
                    .width('100%')
                    .height(6)
                    .backgroundColor(DV.bg)
                    .borderRadius(3)
                  Row()
                    .width(dvBarW(s.temp, 30))
                    .height(6)
                    .backgroundColor(DV.accent)
                    .borderRadius(3)
                }
                .width('100%')
                .margin({ top: 6 })
              }
              .margin({ bottom: 10 })
            })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(DV.card)
          .borderRadius(14)
          .border({ width: 1, color: DV.line })
          .margin({ top: 14 })

          ForEach(this.suits, (s: DvSuit, i: number) => {
            Row({ space: 12 }) {
              Column() {
                Text(s.thickness + 'mm')
                  .fontSize(16)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(Color.White)
                Text(s.style)
                  .fontSize(9)
                  .fontColor('rgba(255,255,255,0.85)')
                  .margin({ top: 3 })
              }
              .width(76)
              .height(76)
              .justifyContent(FlexAlign.Center)
              .backgroundColor(i % 3 === 0 ? DV.deep : (i % 3 === 1 ? DV.primary : DV.glow))
              .borderRadius(14)

              Column() {
                Text(s.name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(DV.text)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Row({ space: 6 }) {
                  Text(s.style)
                    .fontSize(10)
                    .fontColor(DV.primary)
                    .backgroundColor(DV.bg)
                    .borderRadius(8)
                    .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                  Text('适温 ' + s.temp + '℃')
                    .fontSize(10)
                    .fontColor(DV.accent)
                  Text('已售 ' + s.sold)
                    .fontSize(10)
                    .fontColor(DV.sub)
                }
                .margin({ top: 5 })
                Row() {
                  Text(dvPrice(s.price))
                    .fontSize(15)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(DV.primary)
                    .layoutWeight(1)
                  Text('改温标')
                    .fontSize(10)
                    .fontColor(DV.primary)
                    .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                    .borderRadius(10)
                    .border({ width: 1, color: DV.primary })
                    .onClick(() => {
                      this.editIdx = i
                      this.eTemp = ''
                      this.showEdit = true
                    })
                  Text('详情')
                    .fontSize(10)
                    .fontColor(Color.White)
                    .backgroundColor(DV.primary)
                    .borderRadius(10)
                    .padding({ left: 12, right: 12, top: 4, bottom: 4 })
                    .margin({ left: 8 })
                    .onClick(() => {
                      this.selIdx = i
                      this.showDetail = true
                    })
                }
                .width('100%')
                .alignItems(VerticalAlign.Bottom)
                .margin({ top: 8 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
            }
            .width('92%')
            .padding(12)
            .backgroundColor(DV.card)
            .borderRadius(14)
            .border({ width: 1, color: DV.line })
            .margin({ top: 12 })
            .translate({ y: dvFloatY(this.wave, i) })
          })

          Column() {
            Text('厚度销量分布')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
              .alignSelf(ItemAlign.Start)
            Row({ space: 8 }) {
              ForEach(this.suits, (s: DvSuit, i: number) => {
                Column() {
                  Row() {
                    Column()
                      .width(18)
                      .height(dvVBarH(s.sold, dvMaxSuitSold()))
                      .backgroundColor(s.thickness >= 5 ? DV.deep : (s.thickness >= 2 ? DV.primary : DV.glow))
                      .borderRadius(4)
                      .opacity(dvPulse(this.wave + i * 14))
                  }
                  .height(100)
                  .alignItems(VerticalAlign.Bottom)

                  Text(s.thickness + '')
                    .fontSize(9)
                    .fontColor(DV.sub)
                    .margin({ top: 6 })
                }
              })
            }
            .width('100%')
            .margin({ top: 12 })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(DV.card)
          .borderRadius(14)
          .border({ width: 1, color: DV.line })
          .margin({ top: 16 })

          Row() {
            Text('+ 新增潜水服')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(Color.White)
          }
          .width('92%')
          .height(46)
          .justifyContent(FlexAlign.Center)
          .backgroundColor(DV.primary)
          .borderRadius(14)
          .margin({ top: 16, bottom: 24 })
          .opacity(dvPulse(this.wave * 2))
          .onClick(() => {
            this.showAdd = true
          })
        }
        .width('100%')
      }
      .width('100%')
      .height('100%')

      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('潜水服详情')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Column() {
              Text('🩱')
                .fontSize(32)
                .translate({ y: dvFloatY(this.wave, 2) })
            }
            .width(88)
            .height(88)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(DV.bg)
            .borderRadius(14)
            .border({ width: 1, color: DV.line })
            .margin({ top: 14 })

            Row() {
              Text('名称')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text(this.suits.length > this.selIdx ? this.suits[this.selIdx].name : '')
                .fontSize(13)
                .fontColor(DV.text)
            }
            .width('100%')
            .margin({ top: 14 })
            Row() {
              Text('厚度')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text((this.suits.length > this.selIdx ? this.suits[this.selIdx].thickness : 0) + ' mm')
                .fontSize(13)
                .fontColor(DV.text)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Text('适温水温')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text((this.suits.length > this.selIdx ? this.suits[this.selIdx].temp : 0) + ' ℃')
                .fontSize(13)
                .fontColor(DV.accent)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Text('款式')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text(this.suits.length > this.selIdx ? this.suits[this.selIdx].style : '')
                .fontSize(13)
                .fontColor(DV.text)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Text('售价')
                .fontSize(12)
                .fontColor(DV.sub)
                .layoutWeight(1)
              Text(dvPrice(this.suits.length > this.selIdx ? this.suits[this.selIdx].price : 0))
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(DV.primary)
            }
            .width('100%')
            .margin({ top: 8 })

            Text('保温能力')
              .fontSize(11)
              .fontColor(DV.sub)
              .margin({ top: 14 })
              .alignSelf(ItemAlign.Start)
            Stack({ alignContent: Alignment.Start }) {
              Row()
                .width('100%')
                .height(8)
                .backgroundColor(DV.bg)
                .borderRadius(4)
              Row()
                .width(dvBarW(this.suits.length > this.selIdx ? this.suits[this.selIdx].thickness : 0, 7))
                .height(8)
                .backgroundColor(DV.deep)
                .borderRadius(4)
            }
            .width('100%')
            .margin({ top: 6 })

            Row() {
              Text('关闭')
                .fontSize(13)
                .fontColor(DV.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.bg)
                .onClick(() => {
                  this.showDetail = false
                })
              Column()
                .width(12)
              Text('加入购物车')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.primary)
                .onClick(() => {
                  this.showDetail = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .constraintSize({ maxHeight: '80%' })
          .zIndex(1000)
        }
      }

      if (this.showAdd) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('新增潜水服')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Column() {
              Text('名称')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nName = v
                })
            }
            .width('100%')
            .margin({ top: 14 })
            Column() {
              Text('厚度 (mm)')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .type(InputType.Number)
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nThick = v
                })
            }
            .width('100%')
            .margin({ top: 12 })
            Column() {
              Text('售价 (元)')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .type(InputType.Number)
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nPrice = v
                })
            }
            .width('100%')
            .margin({ top: 12 })
            Row() {
              Text('取消')
                .fontSize(13)
                .fontColor(DV.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.bg)
                .onClick(() => {
                  this.showAdd = false
                })
              Column()
                .width(12)
              Text('确认新增')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.primary)
                .onClick(() => {
                  let it: DvSuit = {
                    name: this.nName === '' ? '未命名潜水服' : this.nName,
                    thickness: isNaN(parseInt(this.nThick)) ? 3 : parseInt(this.nThick),
                    temp: 24,
                    style: '套头',
                    price: isNaN(parseInt(this.nPrice)) ? 1580 : parseInt(this.nPrice),
                    sold: 0
                  }
                  this.suits.unshift(it)
                  this.nName = ''
                  this.nThick = ''
                  this.nPrice = ''
                  this.showAdd = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .constraintSize({ maxHeight: '80%' })
          .zIndex(1000)
        }
      }

      if (this.showEdit) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('修改适水温标')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
            Text('正在调整: ' + (this.suits.length > this.editIdx ? this.suits[this.editIdx].name : ''))
              .fontSize(12)
              .fontColor(DV.sub)
              .margin({ top: 8 })
            Column() {
              Text('新的适温水温 (℃)')
                .fontSize(11)
                .fontColor(DV.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .type(InputType.Number)
                .fontSize(13)
                .fontColor(DV.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(DV.sub)
                .backgroundColor(DV.bg)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.eTemp = v
                })
            }
            .width('100%')
            .margin({ top: 14 })
            Row() {
              Text('取消')
                .fontSize(13)
                .fontColor(DV.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.bg)
                .onClick(() => {
                  this.showEdit = false
                })
              Column()
                .width(12)
              Text('保存')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(DV.primary)
                .onClick(() => {
                  if (!isNaN(parseInt(this.eTemp)) && this.suits.length > this.editIdx) {
                    this.suits[this.editIdx].temp = parseInt(this.eTemp)
                  }
                  this.eTemp = ''
                  this.showEdit = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(DV.card)
          .borderRadius(18)
          .zIndex(1000)
        }
      }
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct DvWeightTab {
  @State wave: number = 0
  @State timer: number = -1
  @State weights: DvWeight[] = []
  @State showCalc: boolean = false
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showDel: boolean = false
  @State selIdx: number = 0
  @State delIdx: number = 0
  @State nName: string = ''
  @State nKg: string = ''
  @State nPrice: string = ''
  @State cWeight: string = ''
  @State cSuit: string = '3mm'
  @State calcResult: string = ''

  aboutToAppear(): void {
    this.weights = DV_WEIGHTS.slice()
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }

  @Builder modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(6,30,48,0.62)')
      .position({ x: 0, y: 0 })
      .zIndex(999)
      .onClick(() => {
        this.showCalc = false
        this.showDetail = false
        this.showAdd = false
        this.showDel = false
      })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Text('配重 WEIGHT')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(DV.primary)
            .margin({ top: 16, left: 16 })
          Text('精准配重 · 中性浮力的起点')
            .fontSize(12)
            .fontColor(DV.sub)
            .margin({ top: 4, left: 16 })

          Column() {
            Row() {
              Text('⚓')
                .fontSize(20)
              Text('配重计算器')
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(Color.White)
                .margin({ left: 8 })
              Column()
                .layoutWeight(1)
              Text('去计算')
                .fontSize(11)
                .fontWeight(FontWeight.Bold)
                .fontColor(DV.primary)
                .backgroundColor(Color.White)
                .borderRadius(12)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .opacity(dvPulse(this.wave * 2))
                .onClick(() => {
                  this.showCalc = true
                })
            }
            .width('100%')
            Text('输入体重与潜水服厚度,估算所需配重总量')
              .fontSize(10)
              .fontColor('rgba(255,255,255,0.8)')
              .margin({ top: 8 })
          }
          .width('92%')
          .padding(16)
          .backgroundColor(DV.deep)
          .borderRadius(14)
          .margin({ top: 14 })
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('库存水位')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
              .alignSelf(ItemAlign.Start)
              .margin({ bottom: 10 })
            ForEach(this.weights, (w: DvWeight) => {
              Column() {
                Row() {
                  Text(w.name)
                    .fontSize(11)
                    .fontColor(DV.sub)
                    .layoutWeight(1)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                  Text(w.stock + ' 件')
                    .fontSize(11)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(w.stock < 20 ? DV.accent : DV.primary)
                }
                Stack({ alignContent: Alignment.Start }) {
                  Row()
                    .width('100%')
                    .height(8)
                    .backgroundColor(DV.bg)
                    .borderRadius(4)
                  Row()
                    .width(dvBarW(w.stock, 120))
                    .height(8)
                    .backgroundColor(w.stock < 20 ? DV.accent : DV.primary)
                    .borderRadius(4)
                }
                .width('100%')
                .margin({ top: 4 })
              }
              .margin({ bottom: 10 })
            })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(DV.card)
          .borderRadius(14)
          .border({ width: 1, color: DV.line })
          .margin({ top: 14 })

          Column() {
            Text('配重规格一览')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(DV.text)
              .alignSelf(ItemAlign.Start)
              .margin({ bottom: 8 })
            Row({ space: 8 }) {
              ForEach(this.weights, (w: DvWeight, i: number) => {
                Column() {
                  Text(w.kg + 'kg')
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(Color.White)
                }
                .width(46)
                .height(46)
                .justifyContent(FlexAlign.Center)
                .backgroundColor(dvMaterialColor(w.kind === '铅块' ? '塑胶' : (w.kind === '背心' ? '碳纤' : '玻纤')))
                .borderRadius(12)
                .translate({ y: dvFloatY(this.wave, i) })
              })
            }
            .width('100%')
          }
          .width('92%')
          .padding(14)
          .backgroundColor(DV.card)
          .borderRadius(14)
          .border({ width: 1, color: DV.line })
          .margin({ top: 14 })

          ForEach(this.weights, (w: DvWeight, i: number) => {
            Row({ space: 12 }) {
              Column() {
                Text(w.kg + 'kg')
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(Color.White)
                Text(w.kind)
  


总结

在这里插入图片描述

本文全面解析了一款基于HarmonyOS ArkTS构建的自由潜水装备馆应用,从主题设计、数据模型、动画系统到组件架构,深入探讨了声明式UI在电商类应用中的实践方法。该应用以海洋蓝调为视觉基调,通过气泡动画、深度仪表盘等特色设计,成功营造出专业而富有沉浸感的潜水装备购物体验。

从技术实现的角度来看,该应用展示了多种实用的开发技巧。动画系统方面,通过纯数学函数驱动的方式实现了气泡上升、浮动、脉冲、指针摆动等多种效果,证明了在不依赖第三方动画库的情况下,ArkTS同样能够实现丰富细腻的动态视觉。数据可视化方面,利用基础布局组件结合数据计算函数,灵活构建了销量柱状图、参数对比条等图表组件,为应用增添了专业感。

Logo

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

更多推荐