## 一、引言:从晨光中开启高效一天 在移动互联网与智能穿戴设备高度普及的今天,“习惯养成“已经从一个模糊的自我管理理念,演变为一个可量化、可追踪、可视化的数字化产品赛道。越来越多的人希望借助手
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'
}
浅色主题与深色主题在设计策略上有着本质的区别。深色主题通过减少光线发射来营造沉浸感,适合娱乐类应用;浅色主题则通过明亮的视觉环境提升可读性和信任感,更适合电商、工具等实用类应用。本应用作为潜水装备选购平台,采用浅色主题能够更好地展示商品细节,同时传递出专业可靠的品牌形象。
主题接口中特别增加了glow和deep两个色值,分别用于发光效果和极深区域的展示,这两个扩展色值进一步丰富了视觉层次。在实际开发中,根据业务需求合理扩展主题字段是一种值得推荐的做法,它能够在保持主题统一性的同时,为特殊视觉效果提供支持。
数据模型设计

应用的数据模型围绕潜水装备的核心品类展开,包括面镜、脚蹼、潜水服、配重四大主要装备类别,以及潜水日志、订单、月度消费等辅助数据。每个数据接口都经过精心设计,完整覆盖了对应品类的关键属性。
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是应用商品展示模块的典型代表,采用了筛选标签 + 销量柱状图 + 双列商品网格的复合布局。这种布局将数据可视化与商品列表有机结合,用户不仅可以浏览商品,还能直观了解各商品的销售情况。
@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同样能够实现丰富细腻的动态视觉。数据可视化方面,利用基础布局组件结合数据计算函数,灵活构建了销量柱状图、参数对比条等图表组件,为应用增添了专业感。
更多推荐



所有评论(0)