基于HarmonyOS ArkTS API 24逻辑层则封装了一系列私有方法用于数据计算与选中项提取实现“数据-视图-逻辑“三层分离的设计
引言

随着潜水运动在国内沿海城市的快速普及,潜水装备的二手流转需求日益旺盛。从面镜、调节器到气瓶、潜水电脑表,专业潜水装备品类繁多、单价较高,二手交易平台能够帮助潜水爱好者以更低的成本获取品质装备,同时也让闲置装备重新进入流通环节。然而,传统的通用二手交易平台往往缺乏对潜水装备专业属性的精细化管理,例如下水次数磨损度、气瓶水压检测有效期、面镜视野角度等关键参数难以直观展示。本项目正是基于这一痛点,打造了一个专注于潜水装备领域的垂直二手交易应用"潜望角",通过声明式UI范式将装备数据可视化呈现,让买家在决策前即可全面了解装备状态。
在技术架构层面,本应用采用HarmonyOS ArkTS声明式开发范式,以单一@Entry @Component组件为根节点,通过@State状态驱动机制管理全部交互逻辑。整体架构分为三层:数据模型层负责定义六大装备接口类型与静态数据集;视图层通过七个Tab页面与五个弹窗组件构建完整用户界面;逻辑层则封装了一系列私有方法用于数据计算与选中项提取。这种"数据-视图-逻辑"三层分离的设计使得代码结构清晰、可维护性强,同时也符合ArkTS声明式UI的最佳实践。颜色方案采用深海蓝青风格,以#01579B深海蓝为主色调、#00BCD4青碧为强调色、#E1F5FE浅海为背景色、#FF7043珊瑚橙为行动色,营造出与潜水主题高度契合的视觉氛围。
在业务设计层面,应用围绕"浏览-详情-交易-增值服务"四个环节构建完整闭环。浏览环节提供装备集市、面镜专区、潜水服专区、气瓶调节器专区、潜点日志、消息中心和个人中心共七个功能模块;交易环节涵盖发布装备、装备租赁、课程报名三种弹窗交互;增值服务则包括免费检修、水压检测查询、装备消毒烘干等特色功能。特别值得一提的是,应用在多个Tab页面中集成了柱状图和进度条等数据可视化组件,通过ArkTS的布局计算能力动态渲染装备参数对比,为用户提供直观的数据参考。这种将数据可视化深度融入电商列表的设计理念,是本应用在交互层面的一大亮点。
接口定义与数据模型层

本应用首先定义了六个接口类型,分别对应六大装备品类,构建起完整的数据模型层。每个接口都精确刻画了该品类装备的关键属性,为后续的列表渲染和数据计算提供类型安全的约束基础。
interface DiveItem {
name: string
brand: string
price: number
originPrice: number
dives: number
city: string
tag: string
}
interface MaskItem {
name: string
lens: string
frameless: boolean
price: number
degree: number
}
interface SuitItem {
name: string
thickness: number
size: string
price: number
warmth: number
}
interface TankItem {
name: string
volume: number
material: string
price: number
hydro: string
}
interface LogItem {
site: string
depth: number
temp: number
visibility: number
date: string
}
interface NoteItem {
name: string
content: string
time: string
unread: number
type: string
}
DiveItem接口定义了装备集市中的核心数据结构,包含名称、品牌、当前售价、原价、下水次数、所在城市和标签七个字段。其中dives下水次数是潜水装备独有的磨损指标,直接反映装备的使用频率和剩余寿命。MaskItem接口针对潜水面镜增加了lens镜面类型、frameless是否无框和degree视野角度等专业参数。SuitItem接口关注潜水服的thickness厚度和warmth保暖度。TankItem接口则特别记录了volume容量、material材质和hydro水压检测有效期,这三项是气瓶安全交易的核心参考指标。LogItem和NoteItem分别用于潜水日志记录和消息通知。这套接口体系完整覆盖了潜水装备交易全链路的数据需求。
组件状态声明与静态数据集

在DiveLoopApp主组件中,通过@State装饰器声明了十个响应式状态变量,这些变量构成了应用全部交互逻辑的状态中枢。同时,组件内部以private修饰符定义了七个静态数据数组,为各Tab页面提供数据源。
@Entry
@Component
struct DiveLoopApp {
@State currentTab: number = 0
@State showPublish: boolean = false
@State showRent: boolean = false
@State showCourse: boolean = false
@State showDelete: boolean = false
@State showDetail: boolean = false
@State selectedItem: DiveItem | null = null
@State rentDays: number = 3
@State courseLevel: number = 0
@State itemKind: number = 0
@State priceField: string = ''
private tabs: string[] = ['装备集市', '潜水面镜', '潜水服', '气瓶调节', '潜点日志', '消息', '我的']
private diveList: DiveItem[] = [
{ name: 'Gull Mantis 面镜 太阳镜面', brand: 'Gull', price: 380, originPrice: 860, dives: 12, city: '三亚', tag: '包邮' },
{ name: 'Scubapro 齿轮调节器 MK25', brand: 'Scubapro', price: 2680, originPrice: 5400, dives: 35, city: '深圳', tag: '热门' },
{ name: 'AquaLung Helix Pro 一级头', brand: 'AquaLung', price: 1980, originPrice: 4200, dives: 20, city: '上海', tag: '精品' },
{ name: 'Mares Avanti Quattro 脚蹼', brand: 'Mares', price: 520, originPrice: 1080, dives: 45, city: '青岛', tag: '自提' },
{ name: 'Cressi Start BCD 浮力背心', brand: 'Cressi', price: 1280, originPrice: 2800, dives: 28, city: '厦门', tag: '95新' },
{ name: 'Apeks XL4+ 调节器套装', brand: 'Apeks', price: 3280, originPrice: 6600, dives: 15, city: '大连', tag: '精品' },
{ name: 'Gull Barry 3mm 湿衣 男款', brand: 'Gull', price: 680, originPrice: 1480, dives: 18, city: '万宁', tag: '包邮' },
{ name: 'Shearwater Teric 潜水表', brand: 'Shearwater', price: 3980, originPrice: 7800, dives: 22, city: '北海', tag: '热门' },
{ name: 'Suunto D5 潜水电脑', brand: 'Suunto', price: 2880, originPrice: 5600, dives: 30, city: '珠海', tag: '95新' },
{ name: 'Tusa 潜水刀 Mares-style', brand: 'Tusa', price: 220, originPrice: 480, dives: 40, city: '惠州', tag: '急出' },
{ name: 'Apeks MTX-R 冷水调节器', brand: 'Apeks', price: 4280, originPrice: 8200, dives: 8, city: '烟台', tag: '收藏' },
{ name: 'Scubapro 气瓶 12L 钢瓶', brand: 'Scubapro', price: 880, originPrice: 1900, dives: 50, city: '日照', tag: '自提' },
{ name: 'Halcyon Infinity 背飞', brand: 'Halcyon', price: 3680, originPrice: 7200, dives: 25, city: '深圳', tag: '技术潜' },
{ name: 'AquaLung 2mm 短袖湿衣', brand: 'AquaLung', price: 320, originPrice: 720, dives: 60, city: '陵水', tag: '急出' },
{ name: 'Oceanic 象拔 + 线轮', brand: 'Oceanic', price: 180, originPrice: 398, dives: 33, city: '深圳', tag: '包邮' },
{ name: 'Suunto SK-8 罗盘', brand: 'Suunto', price: 420, originPrice: 880, dives: 27, city: '青岛', tag: '95新' },
{ name: 'Cressi 5mm 头套背心', brand: 'Cressi', price: 260, originPrice: 580, dives: 19, city: '威海', tag: '包邮' },
{ name: 'Mares 潜水手电 Xtar 1200', brand: 'Mares', price: 350, originPrice: 760, dives: 24, city: '漳州', tag: '热门' },
{ name: 'Scubapro Jet Fin 经典黑', brand: 'Scubapro', price: 480, originPrice: 980, dives: 70, city: '宁波', tag: '急出' },
{ name: 'Shearwater Peregrine 电脑表', brand: 'Shearwater', price: 2180, originPrice: 4300, dives: 10, city: '舟山', tag: '精品' },
{ name: 'Gull Venus 面镜 女款', brand: 'Gull', price: 350, originPrice: 780, dives: 16, city: '北海', tag: '包邮' },
{ name: 'Apeks 双头高压表', brand: 'Apeks', price: 620, originPrice: 1300, dives: 38, city: '青岛', tag: '自提' },
{ name: 'Tusa Impression 三面镜', brand: 'Tusa', price: 560, originPrice: 1180, dives: 13, city: '深圳', tag: '95新' },
{ name: '潜水日志本 + 潜水证夹', brand: '手作社', price: 65, originPrice: 148, dives: 0, city: '全国', tag: '全新' }
]
currentTab状态变量控制当前激活的Tab索引,通过简单的数值比较即可在build()方法中切换对应的@Builder视图。五个show开头的布尔变量分别控制五个弹窗的显示与隐藏,形成一套轻量级的弹窗管理机制。selectedItem使用联合类型DiveItem | null,在用户点击列表项时被赋值,随后在详情弹窗中通过selName()、selPrice()等安全提取方法读取属性。rentDays、courseLevel、itemKind和priceField则分别服务于装备租赁弹窗的天数选择、课程报名的等级选择、发布弹窗的类别选择和价格输入。diveList数组包含24条装备数据,覆盖面镜、调节器、BCD、湿衣、潜水电脑表、脚蹼、气瓶、背飞等主要品类,每条数据都包含了完整的品牌、价格、下水次数和城市信息。
私有方法与数据计算逻辑

组件中封装了一系列私有方法,用于从数据集中计算最大值和从选中项中安全提取属性。这些方法体现了ArkTS组件中业务逻辑与视图渲染分离的设计思想。
private maxDepth(): number {
let max: number = 0
for (let i = 0; i < this.logList.length; i++) {
if (this.logList[i].depth > max) {
max = this.logList[i].depth
}
}
return max
}
private maxVis(): number {
let max: number = 0
for (let i = 0; i < this.logList.length; i++) {
if (this.logList[i].visibility > max) {
max = this.logList[i].visibility
}
}
return max
}
private selName(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.name
}
private selPrice(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.price + ''
}
private selBrand(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.brand
}
private selDives(): number {
if (this.selectedItem === null) {
return 0
}
return this.selectedItem.dives
}
private selCity(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.city
}
private selOrigin(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.originPrice + ''
}
maxDepth()和maxVis()两个方法通过遍历日志列表计算最大深度和最大能见度,返回值用于柱状图和进度条的高度比例计算。这种动态计算最大值的方式确保了数据变化时可视化组件自动适配,无需硬编码上限值。六个sel前缀的方法实现了空值安全提取模式:当selectedItem为null时返回空字符串或零值,避免在详情弹窗渲染时出现空指针异常。这种防御性编程策略在ArkTS中尤为重要,因为声明式UI的渲染是同步的,任何运行时错误都会直接导致界面崩溃。selPrice()和selOrigin()方法中使用了+ ''的字符串转换技巧,这是ArkTS中将数字转为字符串的简洁写法。
主构建函数与页面骨架
build()方法是组件的入口函数,通过嵌套的Column和Row容器构建出三段式页面布局:顶部头部区域、中间可滚动内容区和底部Tab导航栏。同时在尾部条件渲染五个弹窗组件。
build() {
Column() {
// 头部:静态潜水装备电商风
Column() {
Row() {
Column() {
Text('潜望角')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('二手潜水装备 深海流转')
.fontSize(11)
.fontColor('#B3E5FC')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Row() {
Text('🤿')
.fontSize(15)
Text('搜装备 / 品牌')
.fontSize(12)
.fontColor('#7A9BB5')
.margin({ left: 6 })
}
.width(145)
.height(32)
.justifyContent(FlexAlign.Center)
.backgroundColor('#E1F5FE')
.borderRadius(16)
.margin({ left: 12 })
Text('发布')
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#FFFFFF')
.width(52)
.height(30)
.textAlign(TextAlign.Center)
.backgroundColor('#FF7043')
.borderRadius(15)
.margin({ left: 10 })
.onClick(() => {
this.showPublish = true
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 10 })
Row() {
Text('🔧 免费检修')
.fontSize(11)
.fontColor('#E1F5FE')
.margin({ right: 12 })
Text('📋 水压检测可查')
.fontSize(11)
.fontColor('#E1F5FE')
.margin({ right: 12 })
Text('🤝 潜店直供')
.fontSize(11)
.fontColor('#E1F5FE')
}
.width('100%')
.padding({ left: 16, bottom: 12 })
}
.width('100%')
.backgroundColor('#01579B')
.borderRadius({ bottomLeft: 18, bottomRight: 18 })
Scroll() {
Column() {
if (this.currentTab === 0) {
this.MarketTab()
} else if (this.currentTab === 1) {
this.MaskTab()
} else if (this.currentTab === 2) {
this.SuitTab()
} else if (this.currentTab === 3) {
this.TankTab()
} else if (this.currentTab === 4) {
this.LogTab()
} else if (this.currentTab === 5) {
this.NoteTab()
} else {
this.MineTab()
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.width('100%')
Row() {
ForEach(this.tabs, (tab: string, index: number) => {
Column() {
Text(this.tabIcon(index))
.fontSize(18)
.fontColor(this.currentTab === index ? '#01579B' : '#9E9E9E')
Text(tab)
.fontSize(10)
.fontColor(this.currentTab === index ? '#01579B' : '#9E9E9E')
.margin({ top: 2 })
if (this.currentTab === index) {
Column()
.width(20)
.height(3)
.backgroundColor('#00BCD4')
.borderRadius(2)
.margin({ top: 3 })
} else {
Column()
.width(20)
.height(3)
.backgroundColor('#00000000')
.margin({ top: 3 })
}
}
.justifyContent(FlexAlign.Center)
.layoutWeight(1)
.onClick(() => {
this.currentTab = index
})
}, (tab: string) => tab)
}
.width('100%')
.height(58)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#E1F5FE', radius: 0 })
}
.width('100%')
.height('100%')
.backgroundColor('#F0F8FC')
if (this.showPublish) {
this.PublishDialog()
}
if (this.showRent) {
this.RentDialog()
}
if (this.showCourse) {
this.CourseDialog()
}
if (this.showDelete) {
this.DeleteDialog()
}
if (this.showDetail) {
this.DetailDialog()
}
}
头部区域采用深蓝色背景#01579B并设置底部圆角borderRadius({ bottomLeft: 18, bottomRight: 18 }),营造出从屏幕顶部"潜入"深海的视觉隐喻。头部内部分为两行:第一行是应用名称"潜望角"、副标题、搜索框和发布按钮的组合,第二行是三个服务标签(免费检修、水压检测可查、潜店直供)。发布按钮使用珊瑚橙#FF7043背景突出显示,点击后触发showPublish = true弹出发布弹窗。中间内容区使用Scroll容器包裹,通过layoutWeight(1)占据头部和底部导航之间的全部剩余空间,scrollBar(BarState.Off)隐藏滚动条以保持界面简洁。内容区内部通过if-else链式条件判断根据currentTab值调用对应的@Builder方法。底部Tab导航栏使用ForEach遍历tabs数组渲染七个导航项,每个项包含图标和文字,当前选中的Tab以深海蓝高亮显示并附带青色指示条。五个弹窗在build()最末尾通过if条件渲染,这种方式确保弹窗始终覆盖在主内容之上。
Tab1 装备集市:横幅与列表渲染
装备集市是应用的首页Tab,包含一个渐变横幅广告区和一个装备列表。列表通过ForEach遍历diveList数据数组,为每个装备渲染包含图标、名称、品牌标签、磨损进度条和价格信息的卡片。
// Tab1 装备集市:行式列表
@Builder
MarketTab() {
Column() {
Row() {
Column() {
Text('🤿 潜旅装备季')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('老装备折抵新装备 8 折')
.fontSize(11)
.fontColor('#B3E5FC')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🐠')
.fontSize(36)
.scale({ x: 1.12, y: 1.12 })
.animation({ duration: 1000, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%')
.padding(16)
.linearGradient({
direction: GradientDirection.Right,
colors: [['#01579B', 0], ['#00BCD4', 1]]
})
.borderRadius(14)
.margin({ top: 12, left: 12, right: 12 })
Row() {
Text('装备精选')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('下水次数 ▾')
.fontSize(12)
.fontColor('#01579B')
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
ForEach(this.diveList, (item: DiveItem) => {
Row() {
Column() {
Text('🤿')
.fontSize(26)
}
.width(66)
.height(66)
.justifyContent(FlexAlign.Center)
.backgroundColor('#E1F5FE')
.borderRadius(12)
Column() {
Text(item.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#013A6B')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(item.brand)
.fontSize(9)
.fontColor('#01579B')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#B3E5FC')
.borderRadius(4)
Text(item.dives + ' 潜')
.fontSize(9)
.fontColor('#7A9BB5')
.margin({ left: 6 })
Text('· ' + item.city)
.fontSize(9)
.fontColor('#7A9BB5')
}
.margin({ top: 4 })
Row() {
Text('磨损')
.fontSize(9)
.fontColor('#7A9BB5')
Column()
.width((100 - item.dives) / 100 * 55)
.height(3)
.backgroundColor(item.dives < 25 ? '#66BB6A' : '#FFB74D')
.borderRadius(2)
.margin({ left: 4 })
Text((100 - item.dives) + '%')
.fontSize(9)
.fontColor('#7A9BB5')
.margin({ left: 4 })
}
.margin({ top: 5 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text('¥' + item.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
Text('¥' + item.originPrice)
.fontSize(9)
.fontColor('#9DBACB')
.decoration({ type: TextDecorationType.LineThrough })
.margin({ top: 2 })
Text(item.tag)
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor('#00BCD4')
.borderRadius(3)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(11)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 12, right: 12, top: 5 })
.onClick(() => {
this.selectedItem = item
this.showDetail = true
})
}, (item: DiveItem) => item.name)
Column()
.height(20)
}
.width('100%')
}
横幅区域使用linearGradient属性创建从深海蓝#01579B到青碧#00BCD4的横向渐变效果,右侧的鱼 emoji 通过scale和animation属性实现了持续的缩放呼吸动画,iterations: -1表示无限循环。列表卡片布局采用经典的三栏结构:左侧66x66的图标占位区使用浅海蓝背景;中间区域通过layoutWeight(1)自适应占据剩余空间,展示装备名称、品牌标签和磨损进度条;右侧显示价格、原价和标签。磨损进度条的设计尤为精巧:宽度计算公式为(100 - item.dives) / 100 * 55,即以55为基准宽度乘以剩余寿命百分比,背景色根据下水次数动态变化——低于25次显示绿色#66BB6A表示磨损较小,高于25次显示橙色#FFB74D表示需要注意。每张卡片的onClick事件将当前item赋值给selectedItem并打开详情弹窗,实现了列表到详情的跳转链路。ForEach的第三个参数使用item.name作为键值,确保列表渲染时的高效更新。
Tab2 潜水面镜:视野角度柱状图

面镜专区Tab的核心特色是视野角度柱状图。通过ForEach遍历maskList数据,将每款面镜的degree视野角度值映射为柱状图高度,实现数据可视化呈现。
// Tab2 潜水面镜:横向滚动卡 + 视野角度
@Builder
MaskTab() {
Column() {
Row() {
Text('🥽 潜水面镜专区')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('共 ' + this.maskList.length + ' 款')
.fontSize(11)
.fontColor('#7A9BB5')
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
Text('视野广角度数(°)')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 12, left: 16 })
Row() {
ForEach(this.maskList, (item: MaskItem) => {
Column() {
Column()
.width(17)
.height(item.degree / 500 * 115)
.backgroundColor(item.degree > 250 ? '#00BCD4' : (item.degree > 150 ? '#01579B' : '#FF7043'))
.borderRadius(5)
Text(item.degree + '')
.fontSize(8)
.fontColor('#7A9BB5')
.margin({ top: 3 })
}
.justifyContent(FlexAlign.End)
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
}, (item: MaskItem) => item.name)
}
.width('100%')
.height(155)
.padding({ left: 10, right: 10 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 8, left: 12, right: 12 })
.justifyContent(FlexAlign.End)
ForEach(this.maskList, (item: MaskItem) => {
Row() {
Column() {
Text('🥽')
.fontSize(26)
}
.width(54)
.height(54)
.justifyContent(FlexAlign.Center)
.backgroundColor('#E1F5FE')
.borderRadius(10)
Column() {
Text(item.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#013A6B')
.maxLines(1)
Row() {
Text(item.lens)
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor('#01579B')
.borderRadius(3)
if (item.frameless) {
Text('无框')
.fontSize(9)
.fontColor('#00838F')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor('#B2EBF2')
.borderRadius(3)
.margin({ left: 6 })
}
Text('视野 ' + item.degree + '°')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ left: 6 })
}
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text('¥' + item.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
}
.width('100%')
.padding(11)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 12, right: 12, top: 6 })
}, (item: MaskItem) => item.name)
Column()
.height(20)
}
.width('100%')
}
柱状图区域整体高度为155,每根柱子的宽度固定为17,高度通过item.degree / 500 * 115计算得出——以500度作为满刻度、115作为最大像素高度。柱子颜色采用三段式分级:大于250度使用青碧#00BCD4表示广角视野,大于150度使用深海蓝#01579B表示标准视野,低于150度使用珊瑚橙#FF7043表示窄视野。整个柱状图容器设置justifyContent(FlexAlign.End)使柱子从底部向上生长,符合数据可视化的直觉认知。柱状图下方是面镜列表,每行卡片展示面镜名称、镜面类型标签、无框标识和视野角度数值。if (item.frameless)条件渲染逻辑在面镜为无框设计时额外显示"无框"标签,使用青碧色系标签样式与有框面镜区分。这种将数据可视化图表与列表卡片融合的布局模式,让用户在浏览装备时即可获得直观的参数对比信息。
Tab3 潜水服与Tab4 气瓶调节器

潜水服Tab和气瓶Tab分别展示了厚度柱状图和容量柱状图,两者的设计模式与面镜Tab高度一致,但在列表卡片中融入了不同的专业参数展示。
// Tab3 潜水服:保暖度进度 + 厚度柱状图
@Builder
SuitTab() {
Column() {
Text('湿衣厚度分布(mm)')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 14, left: 16 })
Row() {
ForEach(this.suitList, (item: SuitItem) => {
Column() {
Column()
.width(22)
.height(item.thickness / 8 * 115)
.backgroundColor(item.thickness >= 5 ? '#01579B' : (item.thickness >= 3 ? '#00BCD4' : '#FF7043'))
.borderRadius(5)
Text(item.thickness + '')
.fontSize(8)
.fontColor('#7A9BB5')
.margin({ top: 3 })
}
.justifyContent(FlexAlign.End)
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
}, (item: SuitItem) => item.name)
}
.width('100%')
.height(155)
.padding({ left: 10, right: 10 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 8, left: 12, right: 12 })
.justifyContent(FlexAlign.End)
ForEach(this.suitList, (item: SuitItem) => {
Row() {
Column() {
Text('🩱')
.fontSize(26)
}
.width(54)
.height(54)
.justifyContent(FlexAlign.Center)
.backgroundColor('#E1F5FE')
.borderRadius(10)
Column() {
Text(item.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#013A6B')
.maxLines(1)
Row() {
Text(item.thickness + 'mm')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor('#00BCD4')
.borderRadius(3)
Text(item.size + ' 码')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ left: 6 })
}
.margin({ top: 4 })
Row() {
Text('保暖')
.fontSize(9)
.fontColor('#7A9BB5')
Column()
.width(item.warmth / 100 * 55)
.height(3)
.backgroundColor(item.warmth > 80 ? '#01579B' : '#00BCD4')
.borderRadius(2)
.margin({ left: 4 })
Text(item.warmth + '%')
.fontSize(9)
.fontColor('#7A9BB5')
.margin({ left: 4 })
}
.margin({ top: 5 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text('¥' + item.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
}
.width('100%')
.padding(11)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 12, right: 12, top: 6 })
}, (item: SuitItem) => item.name)
Column()
.height(20)
}
.width('100%')
}
潜水服Tab的柱状图以厚度8mm为满刻度,高度计算公式为item.thickness / 8 * 115。颜色分为三档:5mm及以上使用深海蓝表示厚款,3-4mm使用青碧表示中款,1-2mm使用珊瑚橙表示薄款。列表卡片中特别增加了保暖度进度条,宽度为item.warmth / 100 * 55,保暖度大于80的显示深海蓝,其余显示青碧。这种双重可视化(厚度柱状图+保暖度进度条)让用户可以从宏观和微观两个层面了解潜水服的保暖性能。
气瓶Tab的设计类似,但在列表卡片底部增加了"检测报告可查·支持同城气站充气"的辅助信息行,以及独立的"租"按钮。气瓶柱状图以18L为满刻度,颜色根据材质区分:钢瓶使用深海蓝#01579B,铝瓶使用青碧#00BCD4。每张气瓶卡片额外展示hydro水压检测有效期,以绿色文字#2E7D32显示,强调安全合规性。气瓶区域的"装备租赁"入口按钮设置在区域标题行右侧,使用珊瑚橙背景突出显示。
Tab5 潜点日志:深度与能见度可视化

潜点日志Tab将潜水记录以柱状图和进度条两种形式可视化呈现,深度数据映射为柱状图高度,能见度数据映射为进度条填充比例。
// Tab5 潜点日志:深度柱状图 + 能见度进度
@Builder
LogTab() {
Column() {
Row() {
Text('📖 潜点日志墙')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('分享日志')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 4, bottom: 4 })
.backgroundColor('#00BCD4')
.borderRadius(12)
.onClick(() => {
this.showCourse = true
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
Text('最大深度分布(米)')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 12, left: 16 })
Row() {
ForEach(this.logList, (item: LogItem) => {
Column() {
Column()
.width(22)
.height(item.depth / this.maxDepth() * 115)
.backgroundColor(item.depth > 20 ? '#01579B' : '#00BCD4')
.borderRadius(5)
Text(item.depth + '')
.fontSize(8)
.fontColor('#7A9BB5')
.margin({ top: 3 })
}
.justifyContent(FlexAlign.End)
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
}, (item: LogItem) => item.site)
}
.width('100%')
.height(155)
.padding({ left: 10, right: 10 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 8, left: 12, right: 12 })
.justifyContent(FlexAlign.End)
ForEach(this.logList, (item: LogItem) => {
Column() {
Row() {
Text('📍')
.fontSize(20)
Text(item.site)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ left: 6 })
.layoutWeight(1)
.maxLines(1)
Text(item.date)
.fontSize(10)
.fontColor('#7A9BB5')
}
.width('100%')
Row() {
Text('深度 ' + item.depth + 'm')
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#01579B')
.borderRadius(3)
Text('水温 ' + item.temp + '℃')
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FF7043')
.borderRadius(3)
.margin({ left: 6 })
}
.margin({ top: 6 })
Row() {
Text('能见度')
.fontSize(10)
.fontColor('#7A9BB5')
Stack({ alignContent: Alignment.Start }) {
Column()
.width(130)
.height(5)
.backgroundColor('#E1F5FE')
.borderRadius(3)
Column()
.width(item.visibility / this.maxVis() * 130)
.height(5)
.backgroundColor('#00BCD4')
.borderRadius(3)
}
.width(130)
.height(5)
.margin({ left: 8 })
Text(item.visibility + 'm')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor('#00838F')
.margin({ left: 8 })
.scale({ x: 1.04, y: 1.04 })
.animation({ duration: 900, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(11)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 12, right: 12, top: 6 })
}, (item: LogItem) => item.site)
Column()
.height(20)
}
.width('100%')
}
深度柱状图的高度计算使用了this.maxDepth()方法返回值作为分母:item.depth / this.maxDepth() * 115,这种动态归一化处理确保了柱状图始终能够填满预设高度区域。柱子颜色以20米为分界线,超过20米的深潜记录使用深海蓝,浅于20米的使用青碧。每条日志卡片包含三行信息:第一行是潜点名称和日期,第二行是深度和水温标签(深度标签用蓝色背景,水温用橙色背景),第三行是能见度进度条。能见度进度条使用了Stack容器叠放两层Column:底层是浅蓝色#E1F5FE的背景轨道,上层是青碧色#00BCD4的填充条,填充宽度为item.visibility / this.maxVis() * 130。能见度数值文字附带scale和animation属性,实现了持续的微缩放呼吸动画效果,增强了视觉吸引力。
Tab6 消息与Tab7 个人中心
消息Tab通过ForEach渲染消息列表,每条消息包含头像图标、发送者名称、时间、内容和未读数徽标。个人中心Tab展示用户信息卡片、统计数据和服务入口列表。
// Tab6 消息
@Builder
NoteTab() {
Column() {
Text('消息')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 14, left: 16 })
ForEach(this.noteList, (item: NoteItem) => {
Row() {
Column() {
Text(this.noteIcon(item.type))
.fontSize(22)
}
.width(46)
.height(46)
.justifyContent(FlexAlign.Center)
.backgroundColor(item.type === '官方' ? '#E1F5FE' : '#E0F7FA')
.borderRadius(23)
Column() {
Row() {
Text(item.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#013A6B')
.maxLines(1)
Column()
.layoutWeight(1)
Text(item.time)
.fontSize(10)
.fontColor('#9DBACB')
}
.width('100%')
Row() {
Text(item.content)
.fontSize(11)
.fontColor('#7A9BB5')
.maxLines(1)
.layoutWeight(1)
if (item.unread > 0) {
Text(item.unread + '')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor('#FF7043')
.borderRadius(8)
.margin({ left: 6 })
}
}
.width('100%')
.margin({ top: 3 })
}
.layoutWeight(1)
.margin({ left: 10 })
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 12, right: 12, top: 5 })
.onClick(() => {
this.showDelete = true
})
}, (item: NoteItem) => item.name)
Column()
.height(20)
}
.width('100%')
}
private noteIcon(type: string): string {
if (type === '官方') {
return '📢'
}
if (type === '商家') {
return '🏪'
}
return '💬'
}
消息卡片的头像区域使用borderRadius(23)实现了圆形效果,背景色根据消息类型动态变化:官方通知使用浅海蓝#E1F5FE,其他类型使用#E0F7FA。noteIcon()方法通过简单的条件判断返回对应的emoji图标,官方消息显示喇叭📢,商家消息显示店铺🏪,买家消息显示对话气泡💬。未读数徽标通过if (item.unread > 0)条件渲染,当存在未读消息时显示珊瑚橙圆形徽标。每条消息卡片的onClick事件触发删除确认弹窗,这里复用了删除弹窗作为消息操作入口。
// Tab7 我的
@Builder
MineTab() {
Column() {
Row() {
Column() {
Text('🤿')
.fontSize(36)
}
.width(64)
.height(64)
.justifyContent(FlexAlign.Center)
.backgroundColor('#B3E5FC')
.borderRadius(32)
Column() {
Text('深蓝潜友')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('AOW 持证 · 128 潜 · 信用极好')
.fontSize(11)
.fontColor('#B3E5FC')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
}
.width('100%')
.padding(16)
.linearGradient({
direction: GradientDirection.Right,
colors: [['#01579B', 0], ['#00BCD4', 1]]
})
.borderRadius(14)
.margin({ top: 12, left: 12, right: 12 })
Row() {
Column() {
Text('10')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Text('在售')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('56')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Text('卖出')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('4.9')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Text('评分')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('128')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
Text('累计气瓶')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 2 })
}
.layoutWeight(1)
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 10, left: 12, right: 12 })
个人中心的用户卡片使用与首页横幅相同的渐变配色方案,头像区域64x64圆形背景。统计数据区域使用四等分布局展示在售、卖出、评分和累计气瓶四个指标,每个指标包含大号数字和灰色说明文字。服务入口列表包含升级课程报名、装备租赁记录、我的钱包和我的订单四个选项行,每行通过onClick事件跳转到对应的弹窗或页面。
弹窗系统:发布、租赁与课程报名
应用包含五个弹窗组件,通过布尔状态变量控制显隐。发布弹窗从底部滑出,租赁弹窗居中显示,课程报名弹窗提供等级选择,删除确认弹窗为警示小卡,详情弹窗从右侧滑出。
// 弹框1:发布装备(底部)
@Builder
PublishDialog() {
Column() {
Column() {
Row() {
Text('发布闲置装备')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('✕')
.fontSize(16)
.fontColor('#9E9E9E')
.onClick(() => {
this.showPublish = false
})
}
.width('100%')
Text('装备类别')
.fontSize(12)
.fontColor('#7A9BB5')
.margin({ top: 14 })
Row() {
ForEach(this.itemKinds, (k: string, index: number) => {
Text(k)
.fontSize(11)
.fontColor(this.itemKind === index ? '#FFFFFF' : '#01579B')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor(this.itemKind === index ? '#01579B' : '#E1F5FE')
.borderRadius(13)
.margin({ right: 8 })
.onClick(() => {
this.itemKind = index
})
}, (k: string) => k)
}
.margin({ top: 8 })
Text('期望售价(¥)')
.fontSize(12)
.fontColor('#7A9BB5')
.margin({ top: 14 })
Text(this.priceField === '' ? '输入价格,如 680' : '¥ ' + this.priceField)
.fontSize(14)
.fontColor(this.priceField === '' ? '#9DBACB' : '#013A6B')
.padding(12)
.backgroundColor('#E1F5FE')
.borderRadius(10)
.width('100%')
.margin({ top: 8 })
.onClick(() => {
this.priceField = '680'
})
Text('调节器交易附赠免费检修 · 气瓶需提供水压检测报告')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 10 })
Text('立即发布')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.backgroundColor('#01579B')
.borderRadius(24)
.margin({ top: 16 })
.onClick(() => {
this.showPublish = false
})
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 20, topRight: 20 })
.constraintSize({ maxHeight: '80%' })
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.End)
.backgroundColor('#66000000')
.onClick(() => {
this.showPublish = false
})
}
发布弹窗采用底部滑出式设计,外层Column设置justifyContent(FlexAlign.End)使内容卡片贴底显示,半透明遮罩#66000000覆盖全屏。内容卡片顶部圆角borderRadius({ topLeft: 20, topRight: 20 }),高度限制constraintSize({ maxHeight: '80%' })防止内容过长超出屏幕。装备类别选择区通过ForEach遍历itemKinds数组渲染标签按钮,选中状态的背景色为深海蓝、文字为白色,未选中状态背景色为浅海蓝、文字为深海蓝。价格输入区使用Text组件模拟输入框,点击后填充预设值。弹窗的事件冒泡控制是一个关键设计:内容卡片设置onClick((event: ClickEvent) => { event.stopPropagation() })阻止点击事件冒泡到外层遮罩,而外层遮罩的onClick关闭弹窗,实现了"点击遮罩关闭、点击内容不关闭"的标准弹窗交互模式。
// 弹框2:装备租赁(天数 + 费用脉冲)
@Builder
RentDialog() {
Column() {
Column() {
Text('🔧 装备短期租赁')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Text('选择租赁天数')
.fontSize(12)
.fontColor('#7A9BB5')
.margin({ top: 14 })
.width('100%')
Row() {
ForEach([1, 3, 5, 7, 15], (d: number) => {
Text(d + ' 天')
.fontSize(11)
.fontColor(this.rentDays === d ? '#FFFFFF' : '#01579B')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.rentDays === d ? '#01579B' : '#E1F5FE')
.borderRadius(14)
.margin({ right: 8 })
.onClick(() => {
this.rentDays = d
})
}, (d: number) => d + '')
}
.margin({ top: 8 })
Row() {
Column() {
Row() {
Text('预估费用:')
.fontSize(12)
.fontColor('#8D6E63')
Text('¥ ' + (this.rentDays * 58))
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
.scale({ x: 1.06, y: 1.06 })
.animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
}
}
.alignItems(HorizontalAlign.Start)
}
.margin({ top: 12 })
Column() {
Text('含装备险 · 归还时免费消毒烘干')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 4 })
}
.width('100%')
.padding(12)
.backgroundColor('#E1F5FE')
.borderRadius(10)
.margin({ top: 12 })
Row() {
Text('预约租赁')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor('#FF7043')
.borderRadius(20)
.onClick(() => {
this.showRent = false
})
Text('取消')
.fontSize(14)
.fontColor('#7A9BB5')
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor('#E1F5FE')
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => {
this.showRent = false
})
}
.margin({ top: 18 })
}
.width('86%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
.onClick(() => {
this.showRent = false
})
}
租赁弹窗与发布弹窗不同,采用居中显示模式(justifyContent(FlexAlign.Center)),宽度为86%。租赁天数选择区使用ForEach遍历内联数组[1, 3, 5, 7, 15]渲染五个天数选项。预估费用通过this.rentDays * 58动态计算,以每日58元的租赁费率为基准。费用数字附带scale和animation属性实现持续脉冲动画效果,吸引用户注意。底部操作区包含"预约租赁"和"取消"两个按钮,分别使用珊瑚橙和浅海蓝背景,形成明确的主次操作层级。
弹窗系统:课程报名、删除确认与详情侧滑
// 弹框3:课程报名(等级选择)
@Builder
CourseDialog() {
Column() {
Column() {
Text('🎓 潜水课程报名')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Text('选择进阶等级')
.fontSize(12)
.fontColor('#7A9BB5')
.margin({ top: 14 })
.width('100%')
Row() {
ForEach(this.courseLevels, (l: string, index: number) => {
Text(l)
.fontSize(10)
.fontColor(this.courseLevel === index ? '#FFFFFF' : '#00838F')
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.backgroundColor(this.courseLevel === index ? '#00BCD4' : '#B2EBF2')
.borderRadius(12)
.margin({ right: 6, top: 6 })
.onClick(() => {
this.courseLevel = index
})
}, (l: string) => l)
}
.width('100%')
.flexWrap(FlexWrap.Wrap)
Column() {
Row() {
Text('目标等级:' + this.courseLevels[this.courseLevel])
.fontSize(12)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('送二手装备券')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
}
.width('100%')
Text('完成课程可领 300 元装备抵扣券,全场通用')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 6 })
}
.width('100%')
.padding(12)
.backgroundColor('#F0F8FC')
.borderRadius(10)
.margin({ top: 14 })
Row() {
Text('立即报名')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor('#00BCD4')
.borderRadius(20)
.onClick(() => {
this.showCourse = false
})
Text('再想想')
.fontSize(14)
.fontColor('#7A9BB5')
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor('#B2EBF2')
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => {
this.showCourse = false
})
}
.margin({ top: 18 })
}
.width('86%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
.onClick(() => {
this.showCourse = false
})
}
课程报名弹窗使用flexWrap(FlexWrap.Wrap)让等级标签自动换行,因为五个课程等级名称较长(如"OW 开放水域"、“技术潜入门”),横向排列可能溢出。选中等级使用青碧色#00BCD4背景,未选中使用浅青#B2EBF2。信息卡片中动态显示当前选中的目标等级名称this.courseLevels[this.courseLevel],并附带优惠提示。
// 弹框5:装备详情(右侧滑出)
@Builder
DetailDialog() {
Column() {
Column() {
Row() {
Text('装备详情')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('✕')
.fontSize(16)
.fontColor('#9E9E9E')
.onClick(() => {
this.showDetail = false
})
}
.width('100%')
Column() {
Text('🤿')
.fontSize(54)
}
.width('100%')
.height(120)
.justifyContent(FlexAlign.Center)
.backgroundColor('#E1F5FE')
.borderRadius(12)
.margin({ top: 14 })
Text(this.selName())
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 12 })
.width('100%')
Row() {
Text(this.selBrand())
.fontSize(11)
.fontColor('#01579B')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#B3E5FC')
.borderRadius(4)
Text('· ' + this.selCity())
.fontSize(11)
.fontColor('#7A9BB5')
}
.margin({ top: 8 })
Row() {
Text('¥' + this.selPrice())
.fontSize(26)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
Text('原价 ¥' + this.selOrigin())
.fontSize(12)
.fontColor('#9DBACB')
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 10 })
}
.margin({ top: 12 })
Column() {
Row() {
Text('装备剩余寿命')
.fontSize(12)
.fontColor('#7A9BB5')
Column()
.layoutWeight(1)
Text((100 - this.selDives()) + '%')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(this.selDives() < 25 ? '#2E7D32' : '#EF6C00')
}
.width('100%')
Stack({ alignContent: Alignment.Start }) {
Column()
.width('100%')
.height(6)
.backgroundColor('#E1F5FE')
.borderRadius(3)
Column()
.width((100 - this.selDives()) + '%')
.height(6)
.backgroundColor(this.selDives() < 25 ? '#66BB6A' : '#FFB74D')
.borderRadius(3)
}
.width('100%')
.height(6)
.margin({ top: 6 })
Text('共 ' + this.selDives() + ' 潜 · 平台检修消毒后发货')
.fontSize(9)
.fontColor('#7A9BB5')
.margin({ top: 4 })
}
.width('100%')
.padding(12)
.backgroundColor('#F0F8FC')
.borderRadius(10)
.margin({ top: 14 })
Row() {
Text('租 7 天')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#FF7043', radius: 20 })
.onClick(() => {
this.showDetail = false
this.showRent = true
})
Text('聊卖家')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.backgroundColor('#01579B')
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => {
this.showDetail = false
})
}
.margin({ top: 16 })
}
.width('88%')
.height('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 20, bottomLeft: 20 })
.constraintSize({ maxHeight: '80%' })
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.End)
.backgroundColor('#66000000')
.onClick(() => {
this.showDetail = false
})
}
详情弹窗采用右侧滑出式设计,通过justifyContent(FlexAlign.End)使卡片贴右显示,borderRadius({ topLeft: 20, bottomLeft: 20 })设置左上和左下圆角营造从右侧滑入的视觉感。弹窗内所有数据均通过selName()、selPrice()等安全方法提取,避免空指针异常。装备剩余寿命进度条使用Stack容器叠放背景轨道和填充条,填充宽度为(100 - this.selDives()) + '%',颜色根据下水次数动态变化。底部操作区包含"租7天"和"聊卖家"两个按钮,"租7天"按钮使用边框样式(border属性)而非纯色填充,与"聊卖家"的实心按钮形成视觉区分。"租7天"按钮的onClick事件先关闭详情弹窗再打开租赁弹窗,实现了弹窗间的跳转联动。
应用交互流程图
技术点对比表
| 技术点 | 实现方式 | 适用场景 | 优势分析 | 注意事项 |
|---|---|---|---|---|
| @State状态管理 | 布尔变量控制弹窗显隐 | 弹窗开关、Tab切换、选中项存储 | 响应式自动更新UI,代码简洁 | 状态变量过多时需考虑提取到全局 |
| @Builder视图构建 | 七个Tab页面+五个弹窗均使用@Builder | 复杂多页面应用的视图拆分 | 代码结构清晰,可复用性强 | @Builder方法内不能直接访问组件外部状态 |
| ForEach列表渲染 | 遍历数据数组生成列表项 | 装备列表、消息列表、Tab导航 | 高效的diff算法,支持键值优化 | 键值函数需返回唯一标识 |
| 柱状图可视化 | Column高度按数据比例计算 | 面镜视野、湿衣厚度、气瓶容量、潜水深度 | 无需第三方图表库,纯原生实现 | 需处理数据为零或负值的边界情况 |
| 进度条可视化 | Stack叠放两层Column | 磨损度、保暖度、能见度、剩余寿命 | 轻量高效,样式可高度定制 | 宽度计算需注意单位百分比与像素的转换 |
| linearGradient渐变 | colors数组定义起止色值 | 头部背景、横幅、用户卡片 | 视觉层次丰富,主题氛围感强 | 颜色值格式需统一为十六进制 |
| animation动画 | duration+iterations+curve参数组合 | 鱼呼吸动画、费用脉冲、能见度文字缩放 | 声明式动画,代码简洁 | 无限循环动画需注意性能影响 |
| 条件渲染弹窗 | if语句块在build末尾 | 五个独立弹窗的显隐控制 | 弹窗覆盖层级清晰,互不干扰 | 需处理弹窗叠加场景的事件冒泡 |
| stopPropagation事件控制 | onClick回调中调用event.stopPropagation | 弹窗内容区阻止冒泡到遮罩 | 实现标准弹窗交互模式 | 仅在ClickEvent类型回调中可用 |
| constraintSize约束 | maxHeight限制弹窗高度 | 发布弹窗、详情弹窗 | 防止内容过长超出屏幕范围 | 需配合Scroll组件实现内部滚动 |
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,但是需要重启一下:

新建一个空白模板:

设置API为24的模板项目:

初始化项目,自动下载相关依赖:

完整代码:
// 深海蓝青风格:深海蓝 #01579B / 青碧 #00BCD4 / 浅海 #E1F5FE / 珊瑚橙 #FF7043
// 7 Tab:装备集市 / 潜水面镜 / 潜水服 / 气瓶调节器 / 潜点日志 / 消息 / 我的
// 5 弹框:发布装备(底部) / 装备租赁(天数+费用脉冲) / 课程报名(等级选择) / 删除确认(警示) / 装备详情(右侧滑出)
interface DiveItem {
name: string
brand: string
price: number
originPrice: number
dives: number
city: string
tag: string
}
interface MaskItem {
name: string
lens: string
frameless: boolean
price: number
degree: number
}
interface SuitItem {
name: string
thickness: number
size: string
price: number
warmth: number
}
interface TankItem {
name: string
volume: number
material: string
price: number
hydro: string
}
interface LogItem {
site: string
depth: number
temp: number
visibility: number
date: string
}
interface NoteItem {
name: string
content: string
time: string
unread: number
type: string
}
@Entry
@Component
struct DiveLoopApp {
@State currentTab: number = 0
@State showPublish: boolean = false
@State showRent: boolean = false
@State showCourse: boolean = false
@State showDelete: boolean = false
@State showDetail: boolean = false
@State selectedItem: DiveItem | null = null
@State rentDays: number = 3
@State courseLevel: number = 0
@State itemKind: number = 0
@State priceField: string = ''
private tabs: string[] = ['装备集市', '潜水面镜', '潜水服', '气瓶调节', '潜点日志', '消息', '我的']
private diveList: DiveItem[] = [
{ name: 'Gull Mantis 面镜 太阳镜面', brand: 'Gull', price: 380, originPrice: 860, dives: 12, city: '三亚', tag: '包邮' },
{ name: 'Scubapro 齿轮调节器 MK25', brand: 'Scubapro', price: 2680, originPrice: 5400, dives: 35, city: '深圳', tag: '热门' },
{ name: 'AquaLung Helix Pro 一级头', brand: 'AquaLung', price: 1980, originPrice: 4200, dives: 20, city: '上海', tag: '精品' },
{ name: 'Mares Avanti Quattro 脚蹼', brand: 'Mares', price: 520, originPrice: 1080, dives: 45, city: '青岛', tag: '自提' },
{ name: 'Cressi Start BCD 浮力背心', brand: 'Cressi', price: 1280, originPrice: 2800, dives: 28, city: '厦门', tag: '95新' },
{ name: 'Apeks XL4+ 调节器套装', brand: 'Apeks', price: 3280, originPrice: 6600, dives: 15, city: '大连', tag: '精品' },
{ name: 'Gull Barry 3mm 湿衣 男款', brand: 'Gull', price: 680, originPrice: 1480, dives: 18, city: '万宁', tag: '包邮' },
{ name: 'Shearwater Teric 潜水表', brand: 'Shearwater', price: 3980, originPrice: 7800, dives: 22, city: '北海', tag: '热门' },
{ name: 'Suunto D5 潜水电脑', brand: 'Suunto', price: 2880, originPrice: 5600, dives: 30, city: '珠海', tag: '95新' },
{ name: 'Tusa 潜水刀 Mares-style', brand: 'Tusa', price: 220, originPrice: 480, dives: 40, city: '惠州', tag: '急出' },
{ name: 'Apeks MTX-R 冷水调节器', brand: 'Apeks', price: 4280, originPrice: 8200, dives: 8, city: '烟台', tag: '收藏' },
{ name: 'Scubapro 气瓶 12L 钢瓶', brand: 'Scubapro', price: 880, originPrice: 1900, dives: 50, city: '日照', tag: '自提' },
{ name: 'Halcyon Infinity 背飞', brand: 'Halcyon', price: 3680, originPrice: 7200, dives: 25, city: '深圳', tag: '技术潜' },
{ name: 'AquaLung 2mm 短袖湿衣', brand: 'AquaLung', price: 320, originPrice: 720, dives: 60, city: '陵水', tag: '急出' },
{ name: 'Oceanic 象拔 + 线轮', brand: 'Oceanic', price: 180, originPrice: 398, dives: 33, city: '深圳', tag: '包邮' },
{ name: 'Suunto SK-8 罗盘', brand: 'Suunto', price: 420, originPrice: 880, dives: 27, city: '青岛', tag: '95新' },
{ name: 'Cressi 5mm 头套背心', brand: 'Cressi', price: 260, originPrice: 580, dives: 19, city: '威海', tag: '包邮' },
{ name: 'Mares 潜水手电 Xtar 1200', brand: 'Mares', price: 350, originPrice: 760, dives: 24, city: '漳州', tag: '热门' },
{ name: 'Scubapro Jet Fin 经典黑', brand: 'Scubapro', price: 480, originPrice: 980, dives: 70, city: '宁波', tag: '急出' },
{ name: 'Shearwater Peregrine 电脑表', brand: 'Shearwater', price: 2180, originPrice: 4300, dives: 10, city: '舟山', tag: '精品' },
{ name: 'Gull Venus 面镜 女款', brand: 'Gull', price: 350, originPrice: 780, dives: 16, city: '北海', tag: '包邮' },
{ name: 'Apeks 双头高压表', brand: 'Apeks', price: 620, originPrice: 1300, dives: 38, city: '青岛', tag: '自提' },
{ name: 'Tusa Impression 三面镜', brand: 'Tusa', price: 560, originPrice: 1180, dives: 13, city: '深圳', tag: '95新' },
{ name: '潜水日志本 + 潜水证夹', brand: '手作社', price: 65, originPrice: 148, dives: 0, city: '全国', tag: '全新' }
]
private maskList: MaskItem[] = [
{ name: 'Gull Mantis 太阳镜面', lens: '单面', frameless: false, price: 380, degree: 150 },
{ name: 'Tusa Impression 三面镜', lens: '三面', frameless: false, price: 560, degree: 300 },
{ name: 'Gull Vader AF 无框', lens: '单面', frameless: true, price: 420, degree: 180 },
{ name: 'Scubapro Frameless 无框', lens: '单面', frameless: true, price: 580, degree: 200 },
{ name: 'Cressi Big Eyes 大视野', lens: '双面', frameless: false, price: 360, degree: 120 },
{ name: 'AquaLung MicroMask', lens: '单面', frameless: false, price: 460, degree: 160 },
{ name: 'Mares X-Vision Liquid', lens: '单面', frameless: false, price: 390, degree: 140 },
{ name: 'SEAC Libera 近视面镜', lens: '近视', frameless: false, price: 480, degree: 500 },
{ name: 'Gull Venus 女款粉面', lens: '单面', frameless: false, price: 350, degree: 130 },
{ name: 'Apeks 双镜片技术镜', lens: '双面', frameless: false, price: 620, degree: 220 }
]
private suitList: SuitItem[] = [
{ name: 'Gull Barry 3mm 全湿衣', thickness: 3, size: 'M', price: 680, warmth: 62 },
{ name: 'AquaLung 2mm 短袖款', thickness: 2, size: 'L', price: 320, warmth: 45 },
{ name: 'Cressi Morea 1.5mm 女款', thickness: 1, size: 'S', price: 380, warmth: 35 },
{ name: 'Mares Flexa 5mm 男款', thickness: 5, size: 'XL', price: 880, warmth: 82 },
{ name: 'Scubapro Nova 3mm 女款', thickness: 3, size: 'M', price: 720, warmth: 60 },
{ name: 'Gull Path 5mm 干式内胆', thickness: 5, size: 'L', price: 980, warmth: 90 },
{ name: 'Pinnacle 7mm 极寒款', thickness: 7, size: 'L', price: 1280, warmth: 96 },
{ name: 'Cressi 3mm 头套背心', thickness: 3, size: 'M', price: 260, warmth: 40 },
{ name: 'AquaLung SolAfx 8mm', thickness: 8, size: 'XL', price: 1580, warmth: 98 },
{ name: 'Mares 1.5mm 冲浪防晒衣', thickness: 1, size: 'M', price: 180, warmth: 25 }
]
private tankList: TankItem[] = [
{ name: 'Scubapro 12L 钢瓶', volume: 12, material: '钢', price: 880, hydro: '2027-05' },
{ name: 'Catalina 10L 铝瓶', volume: 10, material: '铝', price: 680, hydro: '2026-11' },
{ name: 'Luxfer 11.1L 铝瓶', volume: 11, material: '铝', price: 760, hydro: '2028-02' },
{ name: '国产华辰 15L 钢瓶', volume: 15, material: '钢', price: 980, hydro: '2027-09' },
{ name: 'Apeks 双瓶组 2×7L', volume: 14, material: '钢', price: 1680, hydro: '2026-08' },
{ name: 'Catalina S80 11L', volume: 11, material: '铝', price: 720, hydro: '2027-01' },
{ name: 'Scubapro 18L 大容量钢', volume: 18, material: '钢', price: 1280, hydro: '2028-06' },
{ name: 'Luxfer 6L 侧挂瓶', volume: 6, material: '铝', price: 520, hydro: '2027-03' }
]
private logList: LogItem[] = [
{ site: '蜈支洲岛 · 珊瑚礁', depth: 18, temp: 27, visibility: 20, date: '08-12' },
{ site: '分界洲岛 · 沉船点', depth: 24, temp: 26, visibility: 15, date: '08-03' },
{ site: '涠洲岛 · 鳄鱼山', depth: 15, temp: 28, visibility: 18, date: '07-26' },
{ site: '千岛湖 · 中心湖区', depth: 28, temp: 16, visibility: 8, date: '07-18' },
{ site: '大连 · 老偏岛', depth: 22, temp: 14, visibility: 10, date: '07-05' },
{ site: '青岛 · 鲅鱼圈', depth: 12, temp: 18, visibility: 6, date: '06-28' },
{ site: '惠州 · 双月湾', depth: 16, temp: 27, visibility: 12, date: '06-15' },
{ site: '万宁 · 大洲岛', depth: 25, temp: 28, visibility: 22, date: '06-02' },
{ site: '珠海 · 庙湾岛', depth: 20, temp: 28, visibility: 25, date: '05-22' },
{ site: '三亚 · 太阳湾', depth: 14, temp: 29, visibility: 16, date: '05-08' }
]
private noteList: NoteItem[] = [
{ name: '深蓝潜友', content: '调节器能试气吗', time: '12:20', unread: 2, type: '买家' },
{ name: '系统通知', content: '气瓶水压检测报告已生成', time: '11:35', unread: 1, type: '官方' },
{ name: '老船长', content: '脚蹼今晚能发货吗', time: '10:47', unread: 0, type: '买家' },
{ name: '潜点合作社', content: '本周六双月湾组队', time: '昨天', unread: 1, type: '官方' },
{ name: '潜店严选', content: '回收闲置潜水装备', time: '昨天', unread: 0, type: '商家' },
{ name: '气泡少女', content: '湿衣 M 码还有吗', time: '昨天', unread: 0, type: '买家' },
{ name: '交易助手', content: '请确认收货并评价', time: '前天', unread: 0, type: '官方' },
{ name: '技术潜小组', content: '背飞已收到,很棒', time: '前天', unread: 0, type: '买家' },
{ name: '安全提醒', content: '气瓶交易需查水压检测', time: '3天前', unread: 0, type: '官方' },
{ name: '装备管家', content: '您的租赁装备已归还', time: '3天前', unread: 0, type: '官方' },
{ name: '潜友会', content: '9月红船沉船潜旅集合', time: '4天前', unread: 0, type: '官方' },
{ name: '阿凯教练', content: '电脑表数据导出了吗', time: '4天前', unread: 0, type: '买家' }
]
private courseLevels: string[] = ['OW 开放水域', 'AOW 进阶', 'Rescue 救援', 'DM 长官', '技术潜入门']
private itemKinds: string[] = ['面镜呼吸管', '调节器', 'BCD 浮力', '湿衣干衣', '脚蹼手套', '潜水电脑']
private maxDepth(): number {
let max: number = 0
for (let i = 0; i < this.logList.length; i++) {
if (this.logList[i].depth > max) {
max = this.logList[i].depth
}
}
return max
}
private maxVis(): number {
let max: number = 0
for (let i = 0; i < this.logList.length; i++) {
if (this.logList[i].visibility > max) {
max = this.logList[i].visibility
}
}
return max
}
private selName(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.name
}
private selPrice(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.price + ''
}
private selBrand(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.brand
}
private selDives(): number {
if (this.selectedItem === null) {
return 0
}
return this.selectedItem.dives
}
private selCity(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.city
}
private selOrigin(): string {
if (this.selectedItem === null) {
return ''
}
return this.selectedItem.originPrice + ''
}
build() {
Column() {
Column() {
// 头部:静态潜水装备电商风
Column() {
Row() {
Column() {
Text('潜望角')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('二手潜水装备 深海流转')
.fontSize(11)
.fontColor('#B3E5FC')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Row() {
Text('🤿')
.fontSize(15)
Text('搜装备 / 品牌')
.fontSize(12)
.fontColor('#7A9BB5')
.margin({ left: 6 })
}
.width(145)
.height(32)
.justifyContent(FlexAlign.Center)
.backgroundColor('#E1F5FE')
.borderRadius(16)
.margin({ left: 12 })
Text('发布')
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#FFFFFF')
.width(52)
.height(30)
.textAlign(TextAlign.Center)
.backgroundColor('#FF7043')
.borderRadius(15)
.margin({ left: 10 })
.onClick(() => {
this.showPublish = true
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 10 })
Row() {
Text('🔧 免费检修')
.fontSize(11)
.fontColor('#E1F5FE')
.margin({ right: 12 })
Text('📋 水压检测可查')
.fontSize(11)
.fontColor('#E1F5FE')
.margin({ right: 12 })
Text('🤝 潜店直供')
.fontSize(11)
.fontColor('#E1F5FE')
}
.width('100%')
.padding({ left: 16, bottom: 12 })
}
.width('100%')
.backgroundColor('#01579B')
.borderRadius({ bottomLeft: 18, bottomRight: 18 })
Scroll() {
Column() {
if (this.currentTab === 0) {
this.MarketTab()
} else if (this.currentTab === 1) {
this.MaskTab()
} else if (this.currentTab === 2) {
this.SuitTab()
} else if (this.currentTab === 3) {
this.TankTab()
} else if (this.currentTab === 4) {
this.LogTab()
} else if (this.currentTab === 5) {
this.NoteTab()
} else {
this.MineTab()
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.width('100%')
Row() {
ForEach(this.tabs, (tab: string, index: number) => {
Column() {
Text(this.tabIcon(index))
.fontSize(18)
.fontColor(this.currentTab === index ? '#01579B' : '#9E9E9E')
Text(tab)
.fontSize(10)
.fontColor(this.currentTab === index ? '#01579B' : '#9E9E9E')
.margin({ top: 2 })
if (this.currentTab === index) {
Column()
.width(20)
.height(3)
.backgroundColor('#00BCD4')
.borderRadius(2)
.margin({ top: 3 })
} else {
Column()
.width(20)
.height(3)
.backgroundColor('#00000000')
.margin({ top: 3 })
}
}
.justifyContent(FlexAlign.Center)
.layoutWeight(1)
.onClick(() => {
this.currentTab = index
})
}, (tab: string) => tab)
}
.width('100%')
.height(58)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#E1F5FE', radius: 0 })
}
.width('100%')
.height('100%')
.backgroundColor('#F0F8FC')
if (this.showPublish) {
this.PublishDialog()
}
if (this.showRent) {
this.RentDialog()
}
if (this.showCourse) {
this.CourseDialog()
}
if (this.showDelete) {
this.DeleteDialog()
}
if (this.showDetail) {
this.DetailDialog()
}
}
}
private tabIcon(index: number): string {
let icons: string[] = ['🛍️', '🥽', '🩱', '🫙', '📖', '💬', '👤']
return icons[index]
}
// Tab1 装备集市:行式列表
@Builder
MarketTab() {
Column() {
Row() {
Column() {
Text('🤿 潜旅装备季')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('老装备折抵新装备 8 折')
.fontSize(11)
.fontColor('#B3E5FC')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🐠')
.fontSize(36)
.scale({ x: 1.12, y: 1.12 })
.animation({ duration: 1000, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%')
.padding(16)
.linearGradient({
direction: GradientDirection.Right,
colors: [['#01579B', 0], ['#00BCD4', 1]]
})
.borderRadius(14)
.margin({ top: 12, left: 12, right: 12 })
Row() {
Text('装备精选')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('下水次数 ▾')
.fontSize(12)
.fontColor('#01579B')
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
ForEach(this.diveList, (item: DiveItem) => {
Row() {
Column() {
Text('🤿')
.fontSize(26)
}
.width(66)
.height(66)
.justifyContent(FlexAlign.Center)
.backgroundColor('#E1F5FE')
.borderRadius(12)
Column() {
Text(item.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#013A6B')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(item.brand)
.fontSize(9)
.fontColor('#01579B')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#B3E5FC')
.borderRadius(4)
Text(item.dives + ' 潜')
.fontSize(9)
.fontColor('#7A9BB5')
.margin({ left: 6 })
Text('· ' + item.city)
.fontSize(9)
.fontColor('#7A9BB5')
}
.margin({ top: 4 })
Row() {
Text('磨损')
.fontSize(9)
.fontColor('#7A9BB5')
Column()
.width((100 - item.dives) / 100 * 55)
.height(3)
.backgroundColor(item.dives < 25 ? '#66BB6A' : '#FFB74D')
.borderRadius(2)
.margin({ left: 4 })
Text((100 - item.dives) + '%')
.fontSize(9)
.fontColor('#7A9BB5')
.margin({ left: 4 })
}
.margin({ top: 5 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text('¥' + item.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
Text('¥' + item.originPrice)
.fontSize(9)
.fontColor('#9DBACB')
.decoration({ type: TextDecorationType.LineThrough })
.margin({ top: 2 })
Text(item.tag)
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor('#00BCD4')
.borderRadius(3)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(11)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 12, right: 12, top: 5 })
.onClick(() => {
this.selectedItem = item
this.showDetail = true
})
}, (item: DiveItem) => item.name)
Column()
.height(20)
}
.width('100%')
}
// Tab2 潜水面镜:横向滚动卡 + 视野角度
@Builder
MaskTab() {
Column() {
Row() {
Text('🥽 潜水面镜专区')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('共 ' + this.maskList.length + ' 款')
.fontSize(11)
.fontColor('#7A9BB5')
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
Text('视野广角度数(°)')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 12, left: 16 })
Row() {
ForEach(this.maskList, (item: MaskItem) => {
Column() {
Column()
.width(17)
.height(item.degree / 500 * 115)
.backgroundColor(item.degree > 250 ? '#00BCD4' : (item.degree > 150 ? '#01579B' : '#FF7043'))
.borderRadius(5)
Text(item.degree + '')
.fontSize(8)
.fontColor('#7A9BB5')
.margin({ top: 3 })
}
.justifyContent(FlexAlign.End)
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
}, (item: MaskItem) => item.name)
}
.width('100%')
.height(155)
.padding({ left: 10, right: 10 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 8, left: 12, right: 12 })
.justifyContent(FlexAlign.End)
ForEach(this.maskList, (item: MaskItem) => {
Row() {
Column() {
Text('🥽')
.fontSize(26)
}
.width(54)
.height(54)
.justifyContent(FlexAlign.Center)
.backgroundColor('#E1F5FE')
.borderRadius(10)
Column() {
Text(item.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#013A6B')
.maxLines(1)
Row() {
Text(item.lens)
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor('#01579B')
.borderRadius(3)
if (item.frameless) {
Text('无框')
.fontSize(9)
.fontColor('#00838F')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor('#B2EBF2')
.borderRadius(3)
.margin({ left: 6 })
}
Text('视野 ' + item.degree + '°')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ left: 6 })
}
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text('¥' + item.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
}
.width('100%')
.padding(11)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 12, right: 12, top: 6 })
}, (item: MaskItem) => item.name)
Column()
.height(20)
}
.width('100%')
}
// Tab3 潜水服:保暖度进度 + 厚度柱状图
@Builder
SuitTab() {
Column() {
Text('湿衣厚度分布(mm)')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 14, left: 16 })
Row() {
ForEach(this.suitList, (item: SuitItem) => {
Column() {
Column()
.width(22)
.height(item.thickness / 8 * 115)
.backgroundColor(item.thickness >= 5 ? '#01579B' : (item.thickness >= 3 ? '#00BCD4' : '#FF7043'))
.borderRadius(5)
Text(item.thickness + '')
.fontSize(8)
.fontColor('#7A9BB5')
.margin({ top: 3 })
}
.justifyContent(FlexAlign.End)
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
}, (item: SuitItem) => item.name)
}
.width('100%')
.height(155)
.padding({ left: 10, right: 10 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 8, left: 12, right: 12 })
.justifyContent(FlexAlign.End)
ForEach(this.suitList, (item: SuitItem) => {
Row() {
Column() {
Text('🩱')
.fontSize(26)
}
.width(54)
.height(54)
.justifyContent(FlexAlign.Center)
.backgroundColor('#E1F5FE')
.borderRadius(10)
Column() {
Text(item.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#013A6B')
.maxLines(1)
Row() {
Text(item.thickness + 'mm')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor('#00BCD4')
.borderRadius(3)
Text(item.size + ' 码')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ left: 6 })
}
.margin({ top: 4 })
Row() {
Text('保暖')
.fontSize(9)
.fontColor('#7A9BB5')
Column()
.width(item.warmth / 100 * 55)
.height(3)
.backgroundColor(item.warmth > 80 ? '#01579B' : '#00BCD4')
.borderRadius(2)
.margin({ left: 4 })
Text(item.warmth + '%')
.fontSize(9)
.fontColor('#7A9BB5')
.margin({ left: 4 })
}
.margin({ top: 5 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text('¥' + item.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
}
.width('100%')
.padding(11)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 12, right: 12, top: 6 })
}, (item: SuitItem) => item.name)
Column()
.height(20)
}
.width('100%')
}
// Tab4 气瓶调节器:水压检测 + 容量柱状图
@Builder
TankTab() {
Column() {
Row() {
Text('🫙 气瓶调节专区')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('装备租赁')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 4, bottom: 4 })
.backgroundColor('#FF7043')
.borderRadius(12)
.onClick(() => {
this.showRent = true
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
Text('气瓶容量分布(L)')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 12, left: 16 })
Row() {
ForEach(this.tankList, (item: TankItem) => {
Column() {
Column()
.width(24)
.height(item.volume / 18 * 115)
.backgroundColor(item.material === '钢' ? '#01579B' : '#00BCD4')
.borderRadius(5)
Text(item.volume + 'L')
.fontSize(8)
.fontColor('#7A9BB5')
.margin({ top: 3 })
}
.justifyContent(FlexAlign.End)
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
}, (item: TankItem) => item.name)
}
.width('100%')
.height(155)
.padding({ left: 10, right: 10 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 8, left: 12, right: 12 })
.justifyContent(FlexAlign.End)
ForEach(this.tankList, (item: TankItem) => {
Column() {
Row() {
Column() {
Text('🫙')
.fontSize(28)
}
.width(58)
.height(58)
.justifyContent(FlexAlign.Center)
.backgroundColor('#E1F5FE')
.borderRadius(10)
Column() {
Text(item.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.maxLines(1)
Row() {
Text(item.material + '瓶')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor('#01579B')
.borderRadius(3)
Text(item.volume + ' 升')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ left: 6 })
}
.margin({ top: 3 })
Text('水压检测至 ' + item.hydro)
.fontSize(10)
.fontColor('#2E7D32')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text('¥' + item.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
}
.width('100%')
Row() {
Text('检测报告可查 · 支持同城气站充气')
.fontSize(9)
.fontColor('#7A9BB5')
Column()
.layoutWeight(1)
Text('租')
.fontSize(10)
.fontColor('#FF7043')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.border({ width: 1, color: '#FF7043', radius: 9 })
.onClick(() => {
this.showRent = true
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(11)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 12, right: 12, top: 6 })
}, (item: TankItem) => item.name)
Column()
.height(20)
}
.width('100%')
}
// Tab5 潜点日志:深度柱状图 + 能见度进度
@Builder
LogTab() {
Column() {
Row() {
Text('📖 潜点日志墙')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('分享日志')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 12, right: 12, top: 4, bottom: 4 })
.backgroundColor('#00BCD4')
.borderRadius(12)
.onClick(() => {
this.showCourse = true
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
Text('最大深度分布(米)')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 12, left: 16 })
Row() {
ForEach(this.logList, (item: LogItem) => {
Column() {
Column()
.width(22)
.height(item.depth / this.maxDepth() * 115)
.backgroundColor(item.depth > 20 ? '#01579B' : '#00BCD4')
.borderRadius(5)
Text(item.depth + '')
.fontSize(8)
.fontColor('#7A9BB5')
.margin({ top: 3 })
}
.justifyContent(FlexAlign.End)
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
}, (item: LogItem) => item.site)
}
.width('100%')
.height(155)
.padding({ left: 10, right: 10 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 8, left: 12, right: 12 })
.justifyContent(FlexAlign.End)
ForEach(this.logList, (item: LogItem) => {
Column() {
Row() {
Text('📍')
.fontSize(20)
Text(item.site)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ left: 6 })
.layoutWeight(1)
.maxLines(1)
Text(item.date)
.fontSize(10)
.fontColor('#7A9BB5')
}
.width('100%')
Row() {
Text('深度 ' + item.depth + 'm')
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#01579B')
.borderRadius(3)
Text('水温 ' + item.temp + '℃')
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FF7043')
.borderRadius(3)
.margin({ left: 6 })
}
.margin({ top: 6 })
Row() {
Text('能见度')
.fontSize(10)
.fontColor('#7A9BB5')
Stack({ alignContent: Alignment.Start }) {
Column()
.width(130)
.height(5)
.backgroundColor('#E1F5FE')
.borderRadius(3)
Column()
.width(item.visibility / this.maxVis() * 130)
.height(5)
.backgroundColor('#00BCD4')
.borderRadius(3)
}
.width(130)
.height(5)
.margin({ left: 8 })
Text(item.visibility + 'm')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor('#00838F')
.margin({ left: 8 })
.scale({ x: 1.04, y: 1.04 })
.animation({ duration: 900, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding(11)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 12, right: 12, top: 6 })
}, (item: LogItem) => item.site)
Column()
.height(20)
}
.width('100%')
}
// Tab6 消息
@Builder
NoteTab() {
Column() {
Text('消息')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 14, left: 16 })
ForEach(this.noteList, (item: NoteItem) => {
Row() {
Column() {
Text(this.noteIcon(item.type))
.fontSize(22)
}
.width(46)
.height(46)
.justifyContent(FlexAlign.Center)
.backgroundColor(item.type === '官方' ? '#E1F5FE' : '#E0F7FA')
.borderRadius(23)
Column() {
Row() {
Text(item.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#013A6B')
.maxLines(1)
Column()
.layoutWeight(1)
Text(item.time)
.fontSize(10)
.fontColor('#9DBACB')
}
.width('100%')
Row() {
Text(item.content)
.fontSize(11)
.fontColor('#7A9BB5')
.maxLines(1)
.layoutWeight(1)
if (item.unread > 0) {
Text(item.unread + '')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.backgroundColor('#FF7043')
.borderRadius(8)
.margin({ left: 6 })
}
}
.width('100%')
.margin({ top: 3 })
}
.layoutWeight(1)
.margin({ left: 10 })
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: 12, right: 12, top: 5 })
.onClick(() => {
this.showDelete = true
})
}, (item: NoteItem) => item.name)
Column()
.height(20)
}
.width('100%')
}
private noteIcon(type: string): string {
if (type === '官方') {
return '📢'
}
if (type === '商家') {
return '🏪'
}
return '💬'
}
// Tab7 我的
@Builder
MineTab() {
Column() {
Row() {
Column() {
Text('🤿')
.fontSize(36)
}
.width(64)
.height(64)
.justifyContent(FlexAlign.Center)
.backgroundColor('#B3E5FC')
.borderRadius(32)
Column() {
Text('深蓝潜友')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('AOW 持证 · 128 潜 · 信用极好')
.fontSize(11)
.fontColor('#B3E5FC')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
}
.width('100%')
.padding(16)
.linearGradient({
direction: GradientDirection.Right,
colors: [['#01579B', 0], ['#00BCD4', 1]]
})
.borderRadius(14)
.margin({ top: 12, left: 12, right: 12 })
Row() {
Column() {
Text('10')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Text('在售')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('56')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Text('卖出')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('4.9')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Text('评分')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 2 })
}
.layoutWeight(1)
Column() {
Text('128')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
Text('累计气瓶')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 2 })
}
.layoutWeight(1)
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 10, left: 12, right: 12 })
Text('我的服务')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 16, left: 16 })
Column() {
Row() {
Text('🎓')
.fontSize(18)
Text('升级课程报名')
.fontSize(13)
.fontColor('#013A6B')
.margin({ left: 10 })
Column()
.layoutWeight(1)
Text('Rescue 可报')
.fontSize(11)
.fontColor('#2E7D32')
.margin({ right: 6 })
Text('>')
.fontSize(14)
.fontColor('#9DBACB')
}
.padding(14)
.onClick(() => {
this.showCourse = true
})
Row() {
Text('🔧')
.fontSize(18)
Text('装备租赁记录')
.fontSize(13)
.fontColor('#013A6B')
.margin({ left: 10 })
Column()
.layoutWeight(1)
Text('3 次租用')
.fontSize(11)
.fontColor('#01579B')
.margin({ right: 6 })
Text('>')
.fontSize(14)
.fontColor('#9DBACB')
}
.padding(14)
.onClick(() => {
this.showRent = true
})
Row() {
Text('💰')
.fontSize(18)
Text('我的钱包')
.fontSize(13)
.fontColor('#013A6B')
.margin({ left: 10 })
Column()
.layoutWeight(1)
Text('¥1,280.00')
.fontSize(12)
.fontColor('#FF7043')
.margin({ right: 6 })
Text('>')
.fontSize(14)
.fontColor('#9DBACB')
}
.padding(14)
Row() {
Text('📦')
.fontSize(18)
Text('我的订单')
.fontSize(13)
.fontColor('#013A6B')
.margin({ left: 10 })
Column()
.layoutWeight(1)
Text('1 待收货')
.fontSize(11)
.fontColor('#EF6C00')
.margin({ right: 6 })
Text('>')
.fontSize(14)
.fontColor('#9DBACB')
}
.padding(14)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 8, left: 12, right: 12 })
Column()
.height(20)
}
.width('100%')
}
// 弹框1:发布装备(底部)
@Builder
PublishDialog() {
Column() {
Column() {
Row() {
Text('发布闲置装备')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('✕')
.fontSize(16)
.fontColor('#9E9E9E')
.onClick(() => {
this.showPublish = false
})
}
.width('100%')
Text('装备类别')
.fontSize(12)
.fontColor('#7A9BB5')
.margin({ top: 14 })
Row() {
ForEach(this.itemKinds, (k: string, index: number) => {
Text(k)
.fontSize(11)
.fontColor(this.itemKind === index ? '#FFFFFF' : '#01579B')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor(this.itemKind === index ? '#01579B' : '#E1F5FE')
.borderRadius(13)
.margin({ right: 8 })
.onClick(() => {
this.itemKind = index
})
}, (k: string) => k)
}
.margin({ top: 8 })
Text('期望售价(¥)')
.fontSize(12)
.fontColor('#7A9BB5')
.margin({ top: 14 })
Text(this.priceField === '' ? '输入价格,如 680' : '¥ ' + this.priceField)
.fontSize(14)
.fontColor(this.priceField === '' ? '#9DBACB' : '#013A6B')
.padding(12)
.backgroundColor('#E1F5FE')
.borderRadius(10)
.width('100%')
.margin({ top: 8 })
.onClick(() => {
this.priceField = '680'
})
Text('调节器交易附赠免费检修 · 气瓶需提供水压检测报告')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 10 })
Text('立即发布')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.backgroundColor('#01579B')
.borderRadius(24)
.margin({ top: 16 })
.onClick(() => {
this.showPublish = false
})
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 20, topRight: 20 })
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.End)
.backgroundColor('#66000000')
.onClick(() => {
this.showPublish = false
})
}
// 弹框2:装备租赁(天数 + 费用脉冲)
@Builder
RentDialog() {
Column() {
Column() {
Text('🔧 装备短期租赁')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Text('选择租赁天数')
.fontSize(12)
.fontColor('#7A9BB5')
.margin({ top: 14 })
.width('100%')
Row() {
ForEach([1, 3, 5, 7, 15], (d: number) => {
Text(d + ' 天')
.fontSize(11)
.fontColor(this.rentDays === d ? '#FFFFFF' : '#01579B')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(this.rentDays === d ? '#01579B' : '#E1F5FE')
.borderRadius(14)
.margin({ right: 8 })
.onClick(() => {
this.rentDays = d
})
}, (d: number) => d + '')
}
.margin({ top: 8 })
Row() {
Column() {
Row() {
Text('预估费用:')
.fontSize(12)
.fontColor('#8D6E63')
Text('¥ ' + (this.rentDays * 58))
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
.scale({ x: 1.06, y: 1.06 })
.animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
}
}
.alignItems(HorizontalAlign.Start)
}
.margin({ top: 12 })
Column() {
Text('含装备险 · 归还时免费消毒烘干')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 4 })
}
.width('100%')
.padding(12)
.backgroundColor('#E1F5FE')
.borderRadius(10)
.margin({ top: 12 })
Row() {
Text('预约租赁')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor('#FF7043')
.borderRadius(20)
.onClick(() => {
this.showRent = false
})
Text('取消')
.fontSize(14)
.fontColor('#7A9BB5')
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor('#E1F5FE')
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => {
this.showRent = false
})
}
.margin({ top: 18 })
}
.width('86%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
.onClick(() => {
this.showRent = false
})
}
// 弹框3:课程报名(等级选择)
@Builder
CourseDialog() {
Column() {
Column() {
Text('🎓 潜水课程报名')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Text('选择进阶等级')
.fontSize(12)
.fontColor('#7A9BB5')
.margin({ top: 14 })
.width('100%')
Row() {
ForEach(this.courseLevels, (l: string, index: number) => {
Text(l)
.fontSize(10)
.fontColor(this.courseLevel === index ? '#FFFFFF' : '#00838F')
.padding({ left: 8, right: 8, top: 5, bottom: 5 })
.backgroundColor(this.courseLevel === index ? '#00BCD4' : '#B2EBF2')
.borderRadius(12)
.margin({ right: 6, top: 6 })
.onClick(() => {
this.courseLevel = index
})
}, (l: string) => l)
}
.width('100%')
Column() {
Row() {
Text('目标等级:' + this.courseLevels[this.courseLevel])
.fontSize(12)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('送二手装备券')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
}
.width('100%')
Text('完成课程可领 300 元装备抵扣券,全场通用')
.fontSize(10)
.fontColor('#7A9BB5')
.margin({ top: 6 })
}
.width('100%')
.padding(12)
.backgroundColor('#F0F8FC')
.borderRadius(10)
.margin({ top: 14 })
Row() {
Text('立即报名')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor('#00BCD4')
.borderRadius(20)
.onClick(() => {
this.showCourse = false
})
Text('再想想')
.fontSize(14)
.fontColor('#7A9BB5')
.padding({ left: 22, right: 22, top: 10, bottom: 10 })
.backgroundColor('#B2EBF2')
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => {
this.showCourse = false
})
}
.margin({ top: 18 })
}
.width('86%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
.onClick(() => {
this.showCourse = false
})
}
// 弹框4:删除确认(警示小卡)
@Builder
DeleteDialog() {
Column() {
Column() {
Text('🌊')
.fontSize(36)
.margin({ top: 8 })
Text('删除该会话?')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 10 })
Text('删除后聊天记录不可恢复哦')
.fontSize(12)
.fontColor('#7A9BB5')
.margin({ top: 6 })
Row() {
Text('取消')
.fontSize(14)
.fontColor('#7A9BB5')
.padding({ left: 24, right: 24, top: 9, bottom: 9 })
.backgroundColor('#E1F5FE')
.borderRadius(18)
.onClick(() => {
this.showDelete = false
})
Text('删除')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 24, right: 24, top: 9, bottom: 9 })
.backgroundColor('#FF7043')
.borderRadius(18)
.margin({ left: 12 })
.onClick(() => {
this.showDelete = false
})
}
.margin({ top: 16 })
}
.width('70%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#66000000')
.onClick(() => {
this.showDelete = false
})
}
// 弹框5:装备详情(右侧滑出)
@Builder
DetailDialog() {
Column() {
Column() {
Row() {
Text('装备详情')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
Column()
.layoutWeight(1)
Text('✕')
.fontSize(16)
.fontColor('#9E9E9E')
.onClick(() => {
this.showDetail = false
})
}
.width('100%')
Column() {
Text('🤿')
.fontSize(54)
}
.width('100%')
.height(120)
.justifyContent(FlexAlign.Center)
.backgroundColor('#E1F5FE')
.borderRadius(12)
.margin({ top: 14 })
Text(this.selName())
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#013A6B')
.margin({ top: 12 })
.width('100%')
Row() {
Text(this.selBrand())
.fontSize(11)
.fontColor('#01579B')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor('#B3E5FC')
.borderRadius(4)
Text('· ' + this.selCity())
.fontSize(11)
.fontColor('#7A9BB5')
}
.margin({ top: 8 })
Row() {
Text('¥' + this.selPrice())
.fontSize(26)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
Text('原价 ¥' + this.selOrigin())
.fontSize(12)
.fontColor('#9DBACB')
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 10 })
}
.margin({ top: 12 })
Column() {
Row() {
Text('装备剩余寿命')
.fontSize(12)
.fontColor('#7A9BB5')
Column()
.layoutWeight(1)
Text((100 - this.selDives()) + '%')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(this.selDives() < 25 ? '#2E7D32' : '#EF6C00')
}
.width('100%')
Stack({ alignContent: Alignment.Start }) {
Column()
.width('100%')
.height(6)
.backgroundColor('#E1F5FE')
.borderRadius(3)
Column()
.width((100 - this.selDives()) + '%')
.height(6)
.backgroundColor(this.selDives() < 25 ? '#66BB6A' : '#FFB74D')
.borderRadius(3)
}
.width('100%')
.height(6)
.margin({ top: 6 })
Text('共 ' + this.selDives() + ' 潜 · 平台检修消毒后发货')
.fontSize(9)
.fontColor('#7A9BB5')
.margin({ top: 4 })
}
.width('100%')
.padding(12)
.backgroundColor('#F0F8FC')
.borderRadius(10)
.margin({ top: 14 })
Row() {
Text('租 7 天')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FF7043')
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#FF7043', radius: 20 })
.onClick(() => {
this.showDetail = false
this.showRent = true
})
Text('聊卖家')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.backgroundColor('#01579B')
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => {
this.showDetail = false
})
}
.margin({ top: 16 })
}
.width('88%')
.height('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 20, bottomLeft: 20 })
.constraintSize({ maxHeight: '80%' })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.End)
.backgroundColor('#66000000')
.onClick(() => {
this.showDetail = false
})
}
}
总结

本文详细解析了一个基于HarmonyOS ArkTS声明式UI范式开发的二手潜水装备交易平台"潜望角"。该应用以单一@Entry @Component组件为架构核心,通过十个@State状态变量驱动全部交互逻辑,七个@Builder方法构建七个功能Tab页面,五个@Builder方法实现五种不同交互模式的弹窗组件。整体架构遵循"数据模型-视图渲染-逻辑计算"三层分离的设计原则,代码结构清晰且可维护性强。六大装备接口类型完整覆盖了面镜、潜水服、气瓶、日志、消息等核心数据需求,为类型安全的开发提供了坚实基础。
在数据可视化方面,本应用展现了ArkTS声明式UI的强大布局能力。通过Column组件的高度按比例计算,实现了面镜视野角度、湿衣厚度、气瓶容量、潜水深度四种柱状图;通过Stack容器叠放两层Column,实现了磨损度、保暖度、能见度、剩余寿命四种进度条。这些可视化组件完全基于原生ArkTS组件构建,无需引入任何第三方图表库,体现了"轻量即强大"的技术理念。同时,linearGradient渐变背景、animation无限循环动画、scale缩放变换等视觉增强技术的运用,使得应用在保持高性能的同时也拥有出色的视觉表现力。
在交互设计方面,五个弹窗组件各具特色:发布弹窗底部滑出、租赁弹窗居中显示附带费用脉冲动画、课程报名弹窗标签自动换行、删除确认弹窗警示小卡、详情弹窗右侧滑出附带寿命进度条。弹窗间通过状态变量联动实现跳转(如详情弹窗的"租7天"按钮跳转到租赁弹窗),通过stopPropagation事件控制实现"点击遮罩关闭、点击内容保留"的标准交互模式。这些设计模式为HarmonyOS应用开发中的弹窗交互提供了可复用的参考范式。整体而言,本应用充分展示了ArkTS声明式UI在电商类应用开发中的技术潜力和工程实践价值。
更多推荐


所有评论(0)