HarmonyOS 6.1 实战:通过TextInput的onChange回调实现装备名称输入的双向绑定
引言

越野改装文化在国内汽车消费升级浪潮中正从小众圈层走向大众视野。一位越野爱好者从入门到进阶,需要经历装备选购、悬挂升高、轮胎升级、车顶架加装、路线探索和车队社交等多个阶段,每个阶段都涉及专业参数比对、价格决策和社区互动。传统的电商应用难以覆盖越野改装这一垂直领域的全部需求,因此构建一个集改装件交易、装备租赁、路线报名和车队社交于一体的聚合型应用显得尤为必要。本文将围绕一套完整的越野改装俱乐部主页面源码,逐段拆解其声明式UI架构、状态管理策略、业务计算逻辑和弹层交互设计。
从技术架构层面来看,本应用采用HarmonyOS ArkTS声明式UI范式,以@Entry和@Component装饰器构建单文件入口组件。组件内部管理了十五个@State响应式状态变量,包括Tab导航索引、五种弹层显示布尔值、选中数据项引用、五组Chip选择索引、步进计数器和文本输入值。特别值得关注的是,该应用引入了基于索引的颜色循环和字符循环方法(catColorOf、catLetterOf、msgColorOf、msgLetterOf),通过取模运算实现了列表项头像的颜色和标识字符的自动分配。同时,rentUnit()、rentDays()、rentTotal()、depositOf()和joinTotal()这组方法构成了装备租赁和路线报名两个核心业务场景的费用计算引擎。
从业务设计层面来看,应用围绕越野改装俱乐部这一核心场景,构建了七大功能Tab:改装集市、悬挂升高、越野轮胎、车顶架、越野路线和消息及我的。改装集市Tab以24条改装件数据为核心,通过统计卡片、发布入口和列表展示构建交易场景;悬挂升高Tab以柱状图和双列网格展示12套悬挂升高套件;越野轮胎Tab以胎纹深度排行和横向滚动画廊展示10款越野轮胎;车顶架Tab以承重排行和双列网格展示10款车顶架装备;越野路线Tab以难度指数柱状图和路线列表展示10条越野穿越路线。配色采用深灰#455A64为主色调,琥珀黄#FFC400为强调色,越野绿#558B2F和警示红#E53935分别用于积极和警示状态标识,形成了硬朗专业的越野改装视觉风格。
一、数据模型与状态管理基础
1.1 GoodsItem接口定义

应用首先定义了全局接口GoodsItem作为所有列表项的统一数据契约,与整体应用的列表渲染逻辑保持一致。
interface GoodsItem {
name: string
price: number
desc: string
score: number
num: number
tag: string
hot: string
}
在越野改装应用中,GoodsItem的字段语义在不同Tab下有着不同的含义。在改装集市中,price表示装备价格(以元为单位),num表示热度值,score表示适配率百分比,hot使用"爆"“热”"荐"等标识热门程度。在悬挂升高Tab中,num表示升高量(毫米),是用户选择悬挂套件的核心参数。在越野轮胎Tab中,num表示胎纹深度(0.1毫米为单位),直接影响轮胎的越野性能评估。在车顶架Tab中,num表示承重能力(千克),score表示承重利用率。在越野路线Tab中,num表示难度指数,score表示报名率。在消息列表中,num表示消息热度值。这种字段语义复用策略减少了接口定义数量,同时要求每个Tab的渲染逻辑做精确的语义映射。
1.2 组件声明与状态变量

页面核心组件通过@Entry和@Component装饰器声明,内部定义了大量响应式状态变量。
@Entry
@Component
struct Index {
@State currentTab: number = 0
@State showAddModal: boolean = false
@State showRentModal: boolean = false
@State showJoinModal: boolean = false
@State showDeleteModal: boolean = false
@State showDetailModal: boolean = false
@State selectedItem: GoodsItem | null = null
@State chipA: number = 0
@State chipB: number = 0
@State chipC: number = 0
@State chipD: number = 0
@State chipE: number = 0
@State stepNum: number = 1
@State inputName: string = ''
状态变量体系分为四类。第一类是导航状态currentTab,控制七个Tab间的切换。第二类是弹层显示状态,包括发布弹层、租赁弹层、报名弹层、删除弹层和详情弹层,五个布尔变量初始值均为false。第三类是选中项引用selectedItem,类型为GoodsItem | null。第四类是交互选择状态,chipA到chipE共五组Chip索引变量分别用于不同弹层中的品类选择、车型选择、租期选择、路线选择和车辆类型选择。stepNum用于报名弹层中出发车辆数量的步进控制。inputName是字符串类型状态变量,通过TextInput的onChange回调实现装备名称输入的双向绑定。
1.3 Chip配置与菜单定义

组件内部定义了丰富的Chip配置数组和菜单列表,为弹层交互和底部导航提供数据支撑。
private tabs: string[] = ['改装集市', '悬挂升高', '越野轮胎', '车顶架', '越野路线', '消息', '我的']
private kindChips: string[] = ['悬挂', '轮胎', '护杠', '绞盘', '灯光']
private carChips: string[] = ['坦克300', 'BJ40', '普拉多', '牧马人', '哈弗H9']
private rentChips: string[] = ['日租', '周租', '月租']
private routeChips: string[] = ['库布齐沙漠', '乌兰布统', '老掌沟', '浑善达克', '阿尔山']
private vehicleChips: string[] = ['越野车', '城市SUV', '皮卡']
private menuNames: string[] = ['我的收藏', '租赁订单', '车队勋章', '改装档案', '押金钱包', '俱乐部客服']
kindChips定义了五个改装件品类用于发布弹层中的品类选择。carChips列出了五款主流越野车型,在发布弹层和详情弹层中用于适配车型选择。rentChips定义了三种租期选项(日租、周租、月租),routeChips列出了五条经典越野路线,vehicleChips定义了三种车辆类型用于报名弹层。menuNames定义了个人中心的六个菜单项,涵盖了收藏、订单、勋章、档案、押金和客服等俱乐部核心功能。
1.4 改装集市主列表数据

改装集市Tab的主列表包含24条改装件数据,覆盖了绞盘、护杠、车顶架、射灯、侧踏杠、涉水喉、差速锁等多个品类。
private mainList: GoodsItem[] = [
{ name: 'WARN VR EVO 10-S 绞盘', price: 4899, desc: '10000磅拉力 · 钢缆版', score: 95, num: 458, tag: '俱乐部补贴', hot: '爆' },
{ name: 'ROADSHOCK 前保险杠', price: 2380, desc: '5系铝合金 · 雾灯位预留', score: 92, num: 326, tag: '包安装', hot: '热' },
{ name: '拓乐 Traverse 车顶架', price: 3299, desc: '静音横杆 · 承重75kg', score: 88, num: 291, tag: '晒单返现', hot: '荐' },
{ name: '绞杠者 32寸 LED 射灯', price: 1280, desc: 'IP68防水 · 5700流明', score: 90, num: 512, tag: '限时直降', hot: '爆' },
{ name: '曜石 铝合金侧踏杠', price: 1180, desc: '一体式 · 防滑踏板', score: 86, num: 203, tag: '免费打孔', hot: '热' },
{ name: '通用型高位涉水喉', price: 960, desc: '高位进气 · 附赠滤芯', score: 84, num: 167, tag: '含安装支架', hot: '荐' },
{ name: 'ARB 气动差速锁', price: 6800, desc: '前桥 · 牙嵌式结构', score: 97, num: 389, tag: '老带新减200', hot: '爆' },
{ name: 'Smittybilt X20 绞盘', price: 5600, desc: '10000磅 · 合成纤维缆', score: 93, num: 441, tag: '战队同款', hot: '热' },
// ... 更多改装件数据
]
每条数据包含装备名称(含品牌和型号)、价格(以元为单位,从320元到7900元覆盖不同价位段)、规格描述(拉力、材质、防水等级等核心参数)、适配率评分、热度值、营销标签和热度标识。数据覆盖了WARN、ARB、拓乐、伊顿等国际品牌以及曙光、顶火、越野者等国内品牌,为越野改装爱好者提供了丰富的选择空间。hot字段使用"爆"“热”"荐"三个等级标识热门程度,在列表渲染时通过条件判断决定是否附加脉冲动画。
二、辅助方法与颜色字符循环
2.1 选中项取值方法

应用定义了五个辅助方法来安全地从selectedItem中提取字段值,每个方法都进行空值检查后返回对应字段。
selName(): string {
if (this.selectedItem !== null) {
return this.selectedItem.name
}
return ''
}
selPrice(): number {
if (this.selectedItem !== null) {
return this.selectedItem.price
}
return 0
}
selDesc(): string {
if (this.selectedItem !== null) {
return this.selectedItem.desc
}
return '暂无'
}
selScore(): number {
if (this.selectedItem !== null) {
return this.selectedItem.score
}
return 0
}
selNum(): number {
if (this.selectedItem !== null) {
return this.selectedItem.num
}
return 0
}
与同类应用不同的是,selDesc()方法在selectedItem为空时返回"暂无"而非空字符串,这是因为越野改装装备的规格描述是用户决策的关键信息,显示"暂无"比空白更能引导用户主动选择查看详情。selNum()方法返回选中项的num字段值,在详情弹层中用于展示热度值。五个方法共同构成了弹层数据绑定的安全层,确保在用户未通过列表点击进入弹层时不会出现运行时错误。
2.2 索引颜色循环与字符循环

catColorOf和catLetterOf方法通过取模运算实现了列表项头像的颜色和标识字符的自动循环分配。
catColorOf(index: number): string {
let colors: string[] = ['#455A64', '#FFC400', '#558B2F', '#E53935', '#795548']
return colors[index % colors.length]
}
catLetterOf(index: number): string {
let letters: string[] = ['绞', '杠', '架', '灯', '踏', '喉', '锁']
return letters[index % letters.length]
}
msgColorOf(index: number): string {
let colors: string[] = ['#455A64', '#FFC400', '#558B2F', '#795548', '#E53935', '#37474F']
return colors[index % colors.length]
}
msgLetterOf(index: number): string {
let letters: string[] = ['张', '群', '妹', '队', '侠', '哥', '姐', '坊', '白', '管', '师', '王']
return letters[index % letters.length]
}
catColorOf方法定义了五种颜色(深灰、琥珀黄、越野绿、警示红、棕色),通过index % colors.length取模循环分配。catLetterOf方法定义了七个品类字符(绞、杠、架、灯、踏、喉、锁),对应绞盘、护杠、车顶架、射灯、侧踏杠、涉水喉、差速锁等核心改装品类。msgColorOf和msgLetterOf分别为消息列表定义了六种颜色和十二个字符(对应消息发送者的姓氏或角色标识)。这种基于索引的循环分配方法确保了列表项头像在视觉上有足够的色彩多样性,同时通过字符标识提供了品类或角色的快速辨识。
2.3 难度颜色分级
diffColorOf方法根据路线难度数值返回不同级别的颜色,实现了数据驱动的颜色分级渲染。
diffColorOf(num: number): string {
if (num >= 420) {
return '#E53935'
}
if (num >= 300) {
return '#FF8F00'
}
if (num >= 200) {
return '#FFC400'
}
return '#558B2F'
}
方法通过四级阈值判断返回颜色:难度大于等于420返回警示红#E53935(极限难度),大于等于300返回橙色#FF8F00(高难度),大于等于200返回琥珀黄#FFC400(中等难度),低于200返回越野绿#558B2F(低难度)。这种颜色分级在路线列表的难度指数柱状图和路线头像中均有使用,通过颜色直觉化地传达路线难度信息,帮助用户快速做出参与决策。
2.4 租赁与报名费用计算
应用定义了一组互相关联的方法来计算装备租赁和路线报名的费用。
rentUnit(): number {
if (this.chipC === 0) {
return 288
}
if (this.chipC === 1) {
return 260
}
return 198
}
rentDays(): number {
if (this.chipC === 0) {
return 1
}
if (this.chipC === 1) {
return 7
}
return 30
}
rentTotal(): number {
return Math.floor(this.rentUnit() * this.rentDays())
}
depositOf(): number {
return Math.floor(this.selPrice() * 0.2)
}
joinTotal(): number {
return Math.floor(680 * this.stepNum)
}
rentUnit()根据chipC(租期选择索引)返回日租金:日租288元、周租260元/天、月租198元/天,体现了租期越长日均越低的阶梯定价策略。rentDays()返回对应天数:1天、7天、30天。rentTotal()计算总租金为日租金乘以天数。depositOf()计算押金为选中装备价格的20%。joinTotal()计算报名费为680元乘以stepNum(出发车辆数)。这些方法在各自的弹层中被调用,由于依赖的chipC和stepNum都是@State变量,当用户切换租期或调整车辆数时,ArkUI框架自动重新计算并更新弹层中显示的费用金额,实现了实时的费用预估反馈。
三、头部与底部导航构建
3.1 头部Builder
头部区域集成了品牌Logo、标题、搜索框和会员统计信息四个功能区块。
@Builder
headerBuilder() {
Column() {
Row() {
Column() {
Text('越')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}
.width(34).height(34).borderRadius(8).backgroundColor('#FFC400')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Text('越野改装俱乐部')
.fontSize(19).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
.margin({ left: 10 })
Column().layoutWeight(1)
Text('⇪')
.fontSize(18).fontColor('#FFFFFF').padding({ left: 8, right: 4 })
}
.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
.alignItems(VerticalAlign.Center)
Row() {
Text('🔍')
.fontSize(14)
Text('搜改装件/车型')
.fontSize(13).fontColor('#90A4AE').margin({ left: 6 })
}
.width('100%').height(36).borderRadius(18).backgroundColor('#212121')
.padding({ left: 14 }).margin({ left: 16, right: 16 })
.alignItems(VerticalAlign.Center)
Row() {
Text('注册会员 2.4万 · 本月团购改装件 386 件')
.fontSize(11).fontColor('#4E342E')
}
.width('100%').padding({ left: 12, top: 6, bottom: 6 }).backgroundColor('#FFC400')
.margin({ top: 10 }).alignItems(VerticalAlign.Center)
}
.width('100%').backgroundColor('#455A64').padding({ bottom: 12 })
}
头部采用三层垂直布局,整体使用深灰#455A64背景色。第一层是品牌行,左侧是34x34的琥珀黄圆角方块内显示"越"字作为Logo,紧邻是白色加粗的"越野改装俱乐部"标题,右侧是上传图标。第二层是搜索框,使用更深的黑色#212121背景和圆角设计,内部包含搜索图标和灰色占位文字。第三层是琥珀黄色的会员统计条,显示"注册会员2.4万·本月团购改装件386件"的运营数据,通过琥珀黄背景和深棕色文字形成高亮提示效果。这种深灰底配琥珀黄强调的配色方案,传达了越野改装的硬朗和专业调性。
3.2 底部导航栏Builder
底部导航栏通过ForEach遍历tabs数组渲染七个Tab项,选中状态使用深灰标识。
@Builder
bottomBuilder() {
Row() {
ForEach(this.tabs, (tab: string, index: number) => {
Column({ space: 2 }) {
Text(tab)
.fontSize(10).fontColor(this.currentTab === index ? '#455A64' : '#999999')
Column()
.width(18).height(3).borderRadius(2)
.backgroundColor(this.currentTab === index ? '#FFC400' : '#FFFFFF00')
}
.layoutWeight(1).justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentTab = index
})
}, (tab: string) => tab)
}
.width('100%').height(56).backgroundColor('#FFFFFF')
}
底部导航栏与头部配色形成呼应:选中状态下文字使用深灰#455A64,指示条使用琥珀黄#FFC400;未选中状态下文字为灰色,指示条透明。每个Tab项通过layoutWeight(1)等分宽度,justifyContent(FlexAlign.Center)使文字和指示条在垂直方向居中对齐。点击事件更新currentTab触发页面内容切换。
四、改装集市Tab构建
4.1 统计卡片与发布入口
改装集市Tab顶部展示今日上新、成团中和已售罄三项统计指标,下方是发布改装件的团购入口。
@Builder
tab0() {
Column({ space: 12 }) {
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('32')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#455A64')
Text('今日上新')
.fontSize(11).fontColor('#999999')
}
.layoutWeight(1).height(64).borderRadius(10).backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('18')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF8F00')
Text('成团中')
.fontSize(11).fontColor('#999999')
}
.layoutWeight(1).height(64).borderRadius(10).backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('7')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E53935')
Text('已售罄')
.fontSize(11).fontColor('#999999')
}
.layoutWeight(1).height(64).borderRadius(10).backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
}
.width('100%')
Row({ space: 8 }) {
Text('+')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#455A64')
Text('发布改装件 · 团购拼单')
.fontSize(13).fontWeight(FontWeight.Bold).fontColor('#455A64')
Column().layoutWeight(1)
Text('车友发布 128 条')
.fontSize(11).fontColor('#999999')
}
.width('100%').height(44).borderRadius(10).backgroundColor('#FFFFFF')
.padding({ left: 14, right: 14 }).alignItems(VerticalAlign.Center)
.onClick(() => {
this.showAddModal = true
})
统计卡片采用三等分布局,每个卡片高度64vp、白色背景圆角设计。三项数据分别使用深灰(今日上新32件)、橙色(成团中18个)和红色(已售罄7个)三种颜色,通过颜色传达不同状态的紧迫感。发布入口是一个白色背景的横向条,左侧加号图标和"发布改装件·团购拼单"文字,右侧显示"车友发布128条"的社会化证明。点击后触发showAddModal为true打开发布弹层。
4.2 改装件列表卡片
改装件列表通过ForEach遍历mainList渲染24个装备卡片,每个卡片采用头像加内容的横向布局。
ForEach(this.mainList, (item: GoodsItem, index: number) => {
Row({ space: 10 }) {
Column() {
Text(this.catLetterOf(index))
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}
.width(52).height(52).borderRadius(10).backgroundColor(this.catColorOf(index))
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 5 }) {
Text(item.name)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.desc)
.fontSize(11).fontColor('#999999').maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 6 }) {
Row() {
Column()
.width(item.score + '%').height(5).borderRadius(3)
.backgroundColor('#558B2F')
}
.width(112).height(5).borderRadius(3).backgroundColor('#ECEFF1')
Text('适配 ' + item.score + '%')
.fontSize(9).fontColor('#558B2F')
}
.alignItems(VerticalAlign.Center)
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column({ space: 5 }) {
Row({ space: 2 }) {
Text('¥')
.fontSize(10).fontColor('#E53935')
Text(item.price)
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E53935')
}
.alignItems(VerticalAlign.Bottom)
Text(item.tag)
.fontSize(9).fontColor('#FF8F00')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#FFF8E1')
if (item.hot === '爆') {
Text(item.hot)
.fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#E53935').scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
} else {
Text(item.hot)
.fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#455A64')
}
Text('热度 ' + item.num)
.fontSize(9).fontColor('#999999')
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12).borderRadius(12).backgroundColor('#FFFFFF')
.margin({ bottom: 10 }).alignItems(VerticalAlign.Center)
.onClick(() => {
this.selectedItem = item
this.showDetailModal = true
})
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(12).alignItems(HorizontalAlign.Start)
}
列表卡片采用三段式横向布局。左侧是52x52的圆角方块头像,通过catLetterOf(index)获取品类字符(如"绞"“杠”"架"等)作为内容,背景色通过catColorOf(index)循环分配五种颜色。中间区域展示装备名称(单行省略)、规格描述(单行省略)和适配率进度条。进度条使用112vp宽度的嵌套Row+Column结构,底层灰色背景加上层绿色填充,宽度通过item.score + '%'动态设置。右侧区域以红色显示价格,item.price直接作为Text的内容(ArkTS会自动将number转换为字符串),下方是琥珀色背景的营销标签和热度标识。热度标识通过条件渲染if (item.hot === '爆')判断:爆款附加脉冲缩放动画,非爆款使用深灰背景静态显示。
五、悬挂升高与越野轮胎Tab构建
5.1 悬挂升高量分布柱状图
悬挂升高Tab顶部展示本月升高量分布柱状图,通过计算每个套件的升高量占最大值的比例来渲染柱形高度。
@Builder
tab1() {
Column({ space: 14 }) {
Text('本月升高量分布 (mm)')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
Row({ space: 8 }) {
ForEach(this.liftList, (item: GoodsItem) => {
Column({ space: 6 }) {
Column()
.width(22).height(item.num / this.maxNumOf(this.liftList) * 120)
.borderRadius({ topLeft: 4, topRight: 4 })
.backgroundColor(item.num >= 60 ? '#E53935' : '#558B2F')
Text(item.num + '')
.fontSize(9).fontColor('#666666')
Text(item.name)
.fontSize(8).fontColor('#999999').maxLines(1).width(24)
}
.width(24).alignItems(HorizontalAlign.Center)
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(14).borderRadius(12).backgroundColor('#FFFFFF')
.alignItems(VerticalAlign.End)
柱状图通过ForEach遍历liftList的12个套件,柱形高度通过item.num / this.maxNumOf(this.liftList) * 120计算。num字段在此场景中表示升高量(毫米),最大值通过maxNumOf方法获取。柱子颜色根据item.num >= 60判断:高升量(60mm以上)使用警示红,低升量使用越野绿。柱子顶部设置borderRadius({ topLeft: 4, topRight: 4 })实现上方圆角效果。整个Row容器设置alignItems(VerticalAlign.End)使所有柱子底部对齐。
5.2 悬挂套件双列网格
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.liftList, (item: GoodsItem) => {
Column({ space: 8 }) {
Row({ space: 8 }) {
Column()
.width(4).height(16).borderRadius(2).backgroundColor('#FFC400')
Text(item.name)
.fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121').maxLines(1)
.layoutWeight(1)
}
.width('100%').alignItems(VerticalAlign.Center)
Text(item.desc)
.fontSize(11).fontColor('#999999').width('100%').maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 4 }) {
Text('¥')
.fontSize(10).fontColor('#E53935')
Text(item.price)
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E53935')
Column().layoutWeight(1)
Text(item.hot)
.fontSize(9).fontColor('#FF8F00')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#FFF8E1')
}
.width('100%').alignItems(VerticalAlign.Bottom)
Text(item.tag)
.fontSize(10).fontColor('#455A64').width('100%')
.padding({ left: 8, top: 4, bottom: 4 }).borderRadius(6)
.backgroundColor('#ECEFF1')
}
.width('48%').padding(12).borderRadius(12).backgroundColor('#FFFFFF')
.margin({ bottom: 10 })
.onClick(() => {
this.selectedItem = item
this.showRentModal = true
})
}, (item: GoodsItem) => item.name)
}
.width('100%')
双列网格卡片宽度设为48%,使用Flex的SpaceBetween间距分配。每张卡片顶部是4px宽的琥珀黄竖条加套件名称的行标题,下方依次展示规格描述、价格行(红色价格加琥珀色热度标签)和适配车型标签(灰色背景圆角条)。点击卡片打开装备租赁弹层,将选中项设为当前套件。textOverflow({ overflow: TextOverflow.Ellipsis })确保长名称以省略号方式截断,保持卡片布局整齐。
5.3 越野轮胎Tab数据统计与胎纹排行
越野轮胎Tab以数据统计卡片、胎纹深度排行柱状图和横向滚动画廊三个模块构建。
@Builder
tab2() {
Column({ space: 14 }) {
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('46')
.fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFC400')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('在租轮胎(套)')
.fontSize(11).fontColor('#B0BEC5')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column()
.width(1).height(34).backgroundColor('#37474F')
Column({ space: 4 }) {
Text('12')
.fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('本周归还')
.fontSize(11).fontColor('#B0BEC5')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column()
.width(1).height(34).backgroundColor('#37474F')
Column({ space: 4 }) {
Text('8.9')
.fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('综合口碑分')
.fontSize(11).fontColor('#B0BEC5')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').padding(16).borderRadius(12).backgroundColor('#212121')
.alignItems(VerticalAlign.Center)
数据统计卡片使用纯黑#212121背景,三等分布局展示在租轮胎(46套,琥珀黄并附带脉冲动画)、本周归还(12套,白色)和综合口碑分(8.9分,白色)。分割线使用#37474F深灰色,在黑色背景上呈现柔和的分隔效果。在租数量使用琥珀黄并附带脉冲动画,突出当前轮胎租赁的活跃状态。
六、越野改装交互架构流程图
以下流程图展示了用户从浏览改装件到完成装备租赁和路线报名的完整交互流程。
七、车顶架与越野路线Tab构建
7.1 车顶架承重排行与双列网格
车顶架Tab以承重排行柱状图和双列网格展示10款车顶架装备。
@Builder
tab3() {
Column({ space: 14 }) {
Text('顶架承重排行 (kg)')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
Row({ space: 8 }) {
ForEach(this.rackList, (item: GoodsItem) => {
Column({ space: 6 }) {
Column()
.width(20).height(item.num / this.maxNumOf(this.rackList) * 120)
.borderRadius({ topLeft: 4, topRight: 4 })
.backgroundColor(item.num >= 150 ? '#FFC400' : '#455A64')
Text(item.num + '')
.fontSize(9).fontColor('#666666')
Text(item.hot)
.fontSize(8).fontColor('#999999')
}
.width(22).alignItems(HorizontalAlign.Center)
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(14).borderRadius(12).backgroundColor('#FFFFFF')
.alignItems(VerticalAlign.End)
承重排行柱状图遍历rackList的10款车顶架,柱形高度通过item.num / this.maxNumOf(this.rackList) * 120计算。num字段在此场景中表示承重能力(千克)。柱子颜色根据item.num >= 150判断:高承重(150kg以上)使用琥珀黄#FFC400,低承重使用深灰#455A64。每个柱子底部显示承重数值和热度标识。
7.2 车顶架双列网格卡片
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.rackList, (item: GoodsItem) => {
Column({ space: 8 }) {
Text(item.name)
.fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121').maxLines(1)
.width('100%')
Text(item.desc)
.fontSize(11).fontColor('#999999').maxLines(1).width('100%')
Row({ space: 4 }) {
Text('¥')
.fontSize(10).fontColor('#E53935')
Text(item.price)
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E53935')
Column().layoutWeight(1)
if (item.num >= 180) {
Text('重载王')
.fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#E53935').scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
} else {
Text(item.tag)
.fontSize(9).fontColor('#455A64')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#ECEFF1')
}
}
.width('100%').alignItems(VerticalAlign.Bottom)
Column({ space: 4 }) {
Row() {
Column()
.width(item.score + '%').height(6).borderRadius(3)
.backgroundColor('#FFC400')
}
.width('100%').height(6).borderRadius(3).backgroundColor('#ECEFF1')
Text('承重利用率 ' + item.score + '%')
.fontSize(9).fontColor('#FF8F00')
}
.width('100%').alignItems(HorizontalAlign.Start)
}
.width('48%').padding(12).borderRadius(12).backgroundColor('#FFFFFF')
.margin({ bottom: 10 }).alignItems(HorizontalAlign.Start)
}, (item: GoodsItem) => item.name)
}
.width('100%')
车顶架双列网格卡片的特色在于条件渲染的"重载王"标签:当item.num >= 180时显示红色脉冲动画的"重载王"标签,否则显示普通灰色标签。承重利用率进度条使用琥珀黄#FFC400作为填充色,与承重排行柱状图的配色保持一致。每张卡片展示装备名称、规格描述、价格、承重标签和承重利用率进度条。
7.3 越野路线难度指数与路线列表
越野路线Tab以难度指数柱状图和路线列表为核心,展示10条越野穿越路线。
@Builder
tab4() {
Column({ space: 14 }) {
Text('路线难度指数')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
Row({ space: 8 }) {
ForEach(this.routeList, (item: GoodsItem) => {
Column({ space: 6 }) {
Column()
.width(20).height(item.num / this.maxNumOf(this.routeList) * 120)
.borderRadius({ topLeft: 4, topRight: 4 })
.backgroundColor(this.diffColorOf(item.num))
Text(item.num + '')
.fontSize(9).fontColor('#666666')
}
.width(22).alignItems(HorizontalAlign.Center)
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(14).borderRadius(12).backgroundColor('#FFFFFF')
.alignItems(VerticalAlign.End)
难度指数柱状图遍历routeList的10条路线,柱形高度通过item.num / this.maxNumOf(this.routeList) * 120计算。num字段在此场景中表示难度指数。柱子颜色通过diffColorOf(item.num)方法根据难度值返回不同级别的颜色,实现了数据驱动的颜色分级渲染。
7.4 路线列表卡片
ForEach(this.routeList, (item: GoodsItem) => {
Row({ space: 10 }) {
Column() {
Text(item.num + '')
.fontSize(12).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}
.width(44).height(44).borderRadius(10).backgroundColor(this.diffColorOf(item.num))
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 5 }) {
Row({ space: 6 }) {
Text(item.name)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').maxLines(1)
if (item.score >= 85) {
Text('满员预警')
.fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#E53935').scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
}
.alignItems(VerticalAlign.Center)
Text(item.desc)
.fontSize(11).fontColor('#999999').maxLines(1)
Row({ space: 6 }) {
Row() {
Column()
.width(item.score + '%').height(5).borderRadius(3)
.backgroundColor('#E53935')
}
.width(112).height(5).borderRadius(3).backgroundColor('#ECEFF1')
Text('报名 ' + item.score + '%')
.fontSize(9).fontColor('#E53935')
}
.alignItems(VerticalAlign.Center)
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column({ space: 5 }) {
Text('¥' + item.price)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#455A64')
Text(item.tag)
.fontSize(9).fontColor('#FF8F00')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#FFF8E1')
Text(item.hot)
.fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#558B2F')
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12).borderRadius(12).backgroundColor('#FFFFFF')
.margin({ bottom: 10 }).alignItems(VerticalAlign.Center)
.onClick(() => {
this.selectedItem = item
this.showJoinModal = true
})
}, (item: GoodsItem) => item.name)
路线列表卡片的左侧头像显示难度指数数值,背景色通过diffColorOf(item.num)根据难度返回颜色。路线名称右侧通过条件渲染if (item.score >= 85)显示"满员预警"红色脉冲标签,当报名率超过85%时提醒用户尽快报名。报名率进度条使用红色填充,与满员预警的警示色调一致。右侧区域展示报名费、出发时间和路线热度标识。点击路线卡片打开报名弹层。
八、消息与个人中心Tab构建
8.1 消息列表与未读标识
消息Tab展示车队消息列表,通过红点标识和脉冲动画区分未读与已读消息。
@Builder
tab5() {
Column({ space: 12 }) {
Text('车队消息')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
ForEach(this.msgList, (item: GoodsItem, index: number) => {
Row({ space: 12 }) {
Column() {
Text(this.msgLetterOf(index))
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}
.width(46).height(46).borderRadius(23).backgroundColor(this.msgColorOf(index))
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 5 }) {
Row({ space: 6 }) {
Text(item.name)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').maxLines(1)
Column().layoutWeight(1)
Text(item.tag)
.fontSize(9).fontColor('#999999')
}
.width('100%').alignItems(VerticalAlign.Center)
Text(item.desc)
.fontSize(11).fontColor('#999999').maxLines(1).width('100%')
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
if (item.hot === '未读') {
Column()
.width(10).height(10).borderRadius(5).backgroundColor('#E53935')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
} else {
Column()
.width(10).height(10).borderRadius(5).backgroundColor('#FFFFFF00')
}
}
.width('100%').padding(12).borderRadius(12).backgroundColor('#FFFFFF')
.margin({ bottom: 10 }).alignItems(VerticalAlign.Center)
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(12).alignItems(HorizontalAlign.Start)
}
消息列表的头像通过msgLetterOf(index)和msgColorOf(index)获取循环分配的字符和颜色,形成了12种不同色彩和姓氏标识的头像。未读消息右侧显示红色脉冲圆点(item.hot === '未读'),已读消息显示透明圆点(#FFFFFF00),通过条件渲染if-else实现二态切换。消息描述使用textOverflow({ overflow: TextOverflow.Ellipsis })确保长消息以省略号截断。
8.2 个人中心Tab
个人中心Tab展示用户信息、统计数据和带编号的菜单列表。
@Builder
tab6() {
Column({ space: 14 }) {
Row({ space: 12 }) {
Column({ space: 4 }) {
Text('David')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('车队职位 · 装备管理组组长')
.fontSize(11).fontColor('#B0BEC5')
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Text('会员码')
.fontSize(11).fontColor('#FFC400')
.padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(10)
.backgroundColor('#37474F')
}
.width('100%').padding(16).borderRadius(14).backgroundColor('#212121')
.alignItems(VerticalAlign.Center)
Row() {
Column() {
Text('David')
.fontSize(22).fontWeight(FontWeight.Bold).fontColor('#212121')
}
.width(70).height(70).borderRadius(35).backgroundColor('#FFC400')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.border({ width: 3, color: '#ECEFF1' }).scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%').justifyContent(FlexAlign.Center)
个人中心顶部是纯黑背景的用户信息卡片,展示用户名、车队职位和会员码标签。下方是70x70的琥珀黄圆形头像,附带3px灰色边框和脉冲缩放动画。统计数据区三等分布局展示我的装备(23件)、租赁中(3件)和路线打卡(16次),分别使用深灰、绿色和橙色区分数据类型。
8.3 带编号的菜单列表
Column({ space: 0 }) {
ForEach(this.menuNames, (menu: string, menuIndex: number) => {
Row({ space: 10 }) {
Column() {
Text(menuIndex + 1 + '')
.fontSize(12).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}
.width(30).height(30).borderRadius(15)
.backgroundColor(this.catColorOf(menuIndex)).justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Text(menu)
.fontSize(14).fontColor('#212121')
Column().layoutWeight(1)
Text('›')
.fontSize(18).fontColor('#B0BEC5')
}
.width('100%').height(52).padding({ left: 14, right: 14 })
.backgroundColor('#FFFFFF').alignItems(VerticalAlign.Center)
.onClick(() => {
if (menuIndex === 0) {
this.showDeleteModal = true
} else if (menuIndex === 1) {
this.currentTab = 0
} else if (menuIndex === 3) {
this.currentTab = 1
}
})
}, (menu: string) => menu)
}
.width('100%').borderRadius(12).clip(true)
}
.width('100%').padding(12).alignItems(HorizontalAlign.Start)
}
菜单列表复用了catColorOf方法为每个菜单项的编号头像分配不同颜色,编号通过menuIndex + 1 + ''生成序号字符串。容器设置borderRadius(12).clip(true)确保内部菜单项的圆角被容器裁剪,形成整体圆角列表效果。点击事件通过menuIndex索引分发:索引0(我的收藏)打开删除弹层,索引1(租赁订单)跳转到改装集市Tab,索引3(改装档案)跳转到悬挂升高Tab。
九、弹层交互构建
9.1 发布改装件弹层
发布弹层从底部弹出,提供品类选择、车型选择和装备名称输入功能。
@Builder
addModal() {
Column() {
Column().layoutWeight(1)
Column({ space: 14 }) {
Row() {
Text('发布改装件')
.fontSize(17).fontWeight(FontWeight.Bold).fontColor('#212121')
Column().layoutWeight(1)
Text('×')
.fontSize(18).fontColor('#999999').padding({ left: 8, right: 8 })
.onClick(() => {
this.showAddModal = false
})
}
.width('100%').alignItems(VerticalAlign.Center)
Text('选择品类')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.kindChips, (kind: string, kindIndex: number) => {
Text(kind)
.fontSize(12).fontColor(this.chipA === kindIndex ? '#FFFFFF' : '#455A64')
.padding({ left: 14, right: 14, top: 7, bottom: 7 }).borderRadius(15)
.backgroundColor(this.chipA === kindIndex ? '#455A64' : '#ECEFF1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipA = kindIndex
})
}, (kind: string) => kind)
}
.width('100%')
Text('适用车型')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.carChips, (car: string, carIndex: number) => {
Text(car)
.fontSize(12).fontColor(this.chipB === carIndex ? '#FFFFFF' : '#558B2F')
.padding({ left: 14, right: 14, top: 7, bottom: 7 }).borderRadius(15)
.backgroundColor(this.chipB === carIndex ? '#558B2F' : '#ECEFF1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipB = carIndex
})
}, (car: string) => car)
}
.width('100%')
TextInput({ placeholder: '请输入装备名称', text: this.inputName })
.height(44).fontSize(13).backgroundColor('#ECEFF1').borderRadius(8)
.onChange((value: string) => {
this.inputName = value
})
Text('确认上架')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').width('100%')
.height(46).textAlign(TextAlign.Center).borderRadius(23)
.backgroundColor('#455A64')
.onClick(() => {
this.showAddModal = false
})
}
.width('100%').padding(16).backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 }).constraintSize({ maxHeight: '80%' })
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
this.showAddModal = false
})
}
发布弹层中品类Chip使用深灰#455A64选中态,车型Chip使用越野绿#558B2F选中态,通过不同颜色区分选择层级。TextInput通过text: this.inputName参数实现初始值绑定,onChange回调将输入值同步到状态变量。确认上架按钮使用深灰背景配白字,与弹层的整体色调一致。
9.2 装备租赁弹层
租赁弹层是本应用业务逻辑最密集的弹层,集成了租期选择、租金计算、押金计算和下单确认功能。
@Builder
rentModal() {
Column() {
Column().layoutWeight(1)
Column({ space: 14 }) {
Row() {
Text('装备租赁')
.fontSize(17).fontWeight(FontWeight.Bold).fontColor('#212121')
Column().layoutWeight(1)
Text('×')
.fontSize(18).fontColor('#999999').padding({ left: 8, right: 8 })
.onClick(() => {
this.showRentModal = false
})
}
.width('100%').alignItems(VerticalAlign.Center)
Text(this.selName())
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#455A64').width('100%')
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text('选择租期')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.rentChips, (rent: string, rentIndex: number) => {
Text(rent + ' ¥' + this.rentUnit() + '/天')
.fontSize(12).fontColor(this.chipC === rentIndex ? '#FFFFFF' : '#FF8F00')
.padding({ left: 14, right: 14, top: 7, bottom: 7 }).borderRadius(15)
.backgroundColor(this.chipC === rentIndex ? '#FF8F00' : '#FFF8E1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipC = rentIndex
})
}, (rent: string) => rent)
}
.width('100%')
Row({ space: 10 }) {
Text('¥' + this.rentTotal())
.fontSize(30).fontWeight(FontWeight.Bold).fontColor('#E53935')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Column().layoutWeight(1)
Column({ space: 2 }) {
Text(this.rentDays() + ' 天合计租金')
.fontSize(11).fontColor('#999999')
Text('会员日租九折')
.fontSize(10).fontColor('#FF8F00')
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(14).borderRadius(10).backgroundColor('#FFF8E1')
.alignItems(VerticalAlign.Bottom)
Row({ space: 6 }) {
Text('押金')
.fontSize(12).fontColor('#999999')
Text('¥' + this.depositOf())
.fontSize(12).fontWeight(FontWeight.Bold).fontColor('#455A64')
Column().layoutWeight(1)
Text('归还验收后原路退回')
.fontSize(11).fontColor('#999999')
}
.width('100%').alignItems(VerticalAlign.Center)
Text('立即下单')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
.height(46).textAlign(TextAlign.Center).borderRadius(23)
.backgroundColor('#FFC400')
.onClick(() => {
this.showRentModal = false
})
}
.width('100%').padding(16).backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 }).constraintSize({ maxHeight: '80%' })
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
this.showRentModal = false
})
}
租赁弹层的租期Chip文本是动态生成的:rent + ' ¥' + this.rentUnit() + '/天',将租期名称和当前日租金组合显示。当用户切换租期Chip时,chipC变化触发rentUnit()、rentDays()和rentTotal()三个方法重新计算,弹层中的总租金和天数描述即时更新。总租金使用30px大字号红色并附带脉冲动画,押金通过depositOf()计算为选中装备价格的20%。下单按钮使用琥珀黄背景配深灰文字,与弹层的整体琥珀色调一致。
9.3 路线报名弹层
报名弹层集成了路线选择、车辆类型选择、车辆数步进和费用计算功能。
@Builder
joinModal() {
Column() {
Column().layoutWeight(1)
Column({ space: 14 }) {
Row() {
Text('越野路线报名')
.fontSize(17).fontWeight(FontWeight.Bold).fontColor('#212121')
Column().layoutWeight(1)
Text('×')
.fontSize(18).fontColor('#999999').padding({ left: 8, right: 8 })
.onClick(() => {
this.showJoinModal = false
})
}
.width('100%').alignItems(VerticalAlign.Center)
Text('选择路线')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.routeChips, (route: string, routeIndex: number) => {
Text(route)
.fontSize(12).fontColor(this.chipD === routeIndex ? '#FFFFFF' : '#558B2F')
.padding({ left: 14, right: 14, top: 7, bottom: 7 }).borderRadius(15)
.backgroundColor(this.chipD === routeIndex ? '#558B2F' : '#ECEFF1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipD = routeIndex
})
}, (route: string) => route)
}
.width('100%')
Text('车辆类型')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.vehicleChips, (vehicle: string, vehicleIndex: number) => {
Text(vehicle)
.fontSize(12).fontColor(this.chipE === vehicleIndex ? '#FFFFFF' : '#455A64')
.padding({ left: 14, right: 14, top: 7, bottom: 7 }).borderRadius(15)
.backgroundColor(this.chipE === vehicleIndex ? '#455A64' : '#ECEFF1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipE = vehicleIndex
})
}, (vehicle: string) => vehicle)
}
.width('100%')
Row() {
Text('出发车辆数')
.fontSize(13).fontColor('#212121')
Column().layoutWeight(1)
Row({ space: 12 }) {
Column() {
Text('-')
.fontSize(15).fontColor('#455A64')
}
.width(30).height(30).borderRadius(15).backgroundColor('#ECEFF1')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.onClick(() => {
if (this.stepNum > 1) {
this.stepNum = this.stepNum - 1
}
})
Text(this.stepNum + ' 辆')
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#212121').width(44)
.textAlign(TextAlign.Center)
Column() {
Text('+')
.fontSize(15).fontColor('#FFFFFF')
}
.width(30).height(30).borderRadius(15).backgroundColor('#455A64')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.onClick(() => {
if (this.stepNum < 8) {
this.stepNum = this.stepNum + 1
}
})
}
.alignItems(VerticalAlign.Center)
}
.width('100%').alignItems(VerticalAlign.Center)
Row({ space: 10 }) {
Text('¥' + this.joinTotal())
.fontSize(30).fontWeight(FontWeight.Bold).fontColor('#E53935')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Column().layoutWeight(1)
Column({ space: 2 }) {
Text('报名费合计')
.fontSize(11).fontColor('#999999')
Text('含领队及保险')
.fontSize(10).fontColor('#FF8F00')
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(14).borderRadius(10).backgroundColor('#FFF8E1')
.alignItems(VerticalAlign.Bottom)
Text('提交报名')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').width('100%')
.height(46).textAlign(TextAlign.Center).borderRadius(23)
.backgroundColor('#558B2F')
.onClick(() => {
this.showJoinModal = false
})
}
.width('100%').padding(16).backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 }).constraintSize({ maxHeight: '80%' })
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
this.showJoinModal = false
})
}
报名弹层使用了chipD和chipE两组Chip索引,分别控制路线选择和车辆类型选择。路线Chip使用越野绿选中态,车辆类型Chip使用深灰选中态。出发车辆数步进器的减号按钮在stepNum > 1时递减,加号按钮在stepNum < 8时递增。报名费通过joinTotal()计算为680元乘以车辆数,使用30px大字号红色并附带脉冲动画。提交按钮使用越野绿背景配白字,传达了"出发"的积极语义。
9.4 详情弹层与主构建方法
详情弹层从右侧滑出,展示装备完整信息、适配车型和口碑评分。
@Builder
detailModal() {
Column() {
Column() {
Column() {
Text('装')
.fontSize(42).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('OFF-ROAD CLUB')
.fontSize(10).fontColor('#B0BEC5').margin({ top: 4 })
}
.width('100%').height(170).backgroundColor('#455A64')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Column({ space: 12 }) {
Row() {
Text(this.selName())
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121').layoutWeight(1)
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
Text('×')
.fontSize(18).fontColor('#999999').padding({ left: 8 })
.onClick(() => {
this.showDetailModal = false
})
}
.width('100%').alignItems(VerticalAlign.Top)
Row() {
Text('装备规格')
.fontSize(12).fontColor('#999999')
Column().layoutWeight(1)
Text(this.selDesc())
.fontSize(12).fontColor('#212121')
}
.width('100%').padding({ left: 10, right: 10, top: 10, bottom: 10 })
.borderRadius(8).backgroundColor('#ECEFF1').alignItems(VerticalAlign.Center)
Row({ space: 4 }) {
Text('¥')
.fontSize(14).fontColor('#E53935')
Text(this.selPrice())
.fontSize(28).fontWeight(FontWeight.Bold).fontColor('#E53935')
Column().layoutWeight(1)
Text('热度 ' + this.selNum())
.fontSize(11).fontColor('#999999')
}
.width('100%').alignItems(VerticalAlign.Bottom)
Text('适配车型')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.carChips, (car: string, carIndex: number) => {
Text(car)
.fontSize(11).fontColor(this.chipB === carIndex ? '#FFFFFF' : '#455A64')
.padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(14)
.backgroundColor(this.chipB === carIndex ? '#455A64' : '#ECEFF1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipB = carIndex
})
}, (car: string) => car)
}
.width('100%')
Text('口碑评分')
.fontSize(12).fontColor('#999999').width('100%')
Column({ space: 6 }) {
Row() {
Column()
.width(this.selScore() + '%').height(6).borderRadius(3)
.backgroundColor('#FFC400')
}
.width('100%').height(6).borderRadius(3).backgroundColor('#ECEFF1')
Text(this.selScore() + ' 分 · 来自 1268 条车友口碑')
.fontSize(10).fontColor('#FF8F00')
}
.width('100%').alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Row({ space: 10 }) {
Text('移除收藏')
.fontSize(13).fontColor('#E53935').height(42).layoutWeight(1)
.textAlign(TextAlign.Center).borderRadius(21)
.border({ width: 1, color: '#E53935' })
.onClick(() => {
this.showDetailModal = false
this.showDeleteModal = true
})
Text('立即租赁')
.fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121').height(42)
.layoutWeight(1).textAlign(TextAlign.Center).borderRadius(21)
.backgroundColor('#FFC400')
.onClick(() => {
this.showDetailModal = false
this.showRentModal = true
})
}
.width('100%').alignItems(VerticalAlign.Center)
}
.width('100%').layoutWeight(1).padding(16).alignItems(HorizontalAlign.Start)
}
.width('80%').height('100%').backgroundColor('#FFFFFF')
.onClick((event: ClickEvent) => {
event.stopPropagation()
})
}
.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.End).alignItems(HorizontalAlign.End)
.onClick(() => {
this.showDetailModal = false
})
}
详情弹层顶部170vp高的深灰区域展示"装"字和"OFF-ROAD CLUB"英文标识,附带脉冲缩放动画。装备规格使用灰色背景的行内信息条展示,价格使用28px大字号红色显示,热度值使用selNum()方法获取。适配车型Chip在详情弹层中也可交互(chipB控制选中态),允许用户在查看详情时切换适配车型。口碑评分进度条使用琥珀黄填充色,底部显示评分和口碑来源数量。双按钮设计:左侧"移除收藏"使用红色描边空心按钮打开删除弹层,右侧"立即租赁"使用琥珀黄实心按钮打开租赁弹层。
9.5 主构建方法build()
build() {
Stack() {
Column() {
this.headerBuilder()
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tab0()
} else if (this.currentTab === 1) {
this.tab1()
} else if (this.currentTab === 2) {
this.tab2()
} else if (this.currentTab === 3) {
this.tab3()
} else if (this.currentTab === 4) {
this.tab4()
} else if (this.currentTab === 5) {
this.tab5()
} else {
this.tab6()
}
}
.width('100%')
}
.layoutWeight(1).width('100%')
this.bottomBuilder()
}
.width('100%').height('100%')
if (this.showAddModal) {
this.addModal()
}
if (this.showRentModal) {
this.rentModal()
}
if (this.showJoinModal) {
this.joinModal()
}
if (this.showDeleteModal) {
this.deleteModal()
}
if (this.showDetailModal) {
this.detailModal()
}
}
.width('100%').height('100%')
}
}
build()方法使用Stack作为最外层容器,主页面Column包含头部、Scroll内容区和底部导航栏。内容区通过if-else条件分支根据currentTab渲染对应Tab。五个弹层通过各自的条件渲染语句控制显示,层叠在主页面之上。弹层按声明顺序排列,后声明的弹层覆盖在先声明的之上,确保用户最后触发的弹层显示在最顶层。
十、技术点对比分析
| 技术维度 | 改装集市Tab | 悬挂升高Tab | 越野轮胎Tab | 车顶架Tab | 越野路线Tab | 消息Tab | 个人中心Tab |
|---|---|---|---|---|---|---|---|
| 布局容器 | ForEach列表 | Flex双列网格 | Scroll画廊 | Flex双列网格 | ForEach列表 | ForEach列表 | Column + ForEach |
| 数据量 | 24条 | 12条 | 10条 | 10条 | 10条 | 12条 | 静态 |
| 可视化组件 | 适配率进度条 | 升高量柱状图 | 胎纹柱状图 | 承重柱状图 | 难度柱状图 | 未读红点 | 统计三等分 |
| 头像颜色 | catColorOf循环 | - | - | - | diffColorOf分级 | msgColorOf循环 | catColorOf循环 |
| 主色调 | #455A64/#E53935 | #E53935/#558B2F | #212121/#FFC400 | #FFC400/#455A64 | diffColorOf四级 | #E53935 | #212121/#FFC400 |
| 动画效果 | 爆款标签脉冲 | - | 在租数量脉冲 | 重载王脉冲 | 满员预警脉冲 | 红点脉冲 | 头像脉冲 |
| 点击交互 | 打开详情弹层 | 打开租赁弹层 | 打开详情弹层 | 打开详情弹层 | 打开报名弹层 | 无 | 菜单跳转弹层 |
| 费用计算 | 无 | rentTotal() | 无 | 无 | joinTotal() | 无 | 无 |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

设置API为24的模板项目:
初始化项目,自动下载相关依赖:

完整代码:
// 越野改装俱乐部 主页面
// 汽车之家 App 风格的越野改装场景单文件实现
interface GoodsItem {
name: string
price: number
desc: string
score: number
num: number
tag: string
hot: string
}
@Entry
@Component
struct Index {
@State currentTab: number = 0
@State showAddModal: boolean = false
@State showRentModal: boolean = false
@State showJoinModal: boolean = false
@State showDeleteModal: boolean = false
@State showDetailModal: boolean = false
@State selectedItem: GoodsItem | null = null
@State chipA: number = 0
@State chipB: number = 0
@State chipC: number = 0
@State chipD: number = 0
@State chipE: number = 0
@State stepNum: number = 1
@State inputName: string = ''
private tabs: string[] = ['改装集市', '悬挂升高', '越野轮胎', '车顶架', '越野路线', '消息', '我的']
private kindChips: string[] = ['悬挂', '轮胎', '护杠', '绞盘', '灯光']
private carChips: string[] = ['坦克300', 'BJ40', '普拉多', '牧马人', '哈弗H9']
private rentChips: string[] = ['日租', '周租', '月租']
private routeChips: string[] = ['库布齐沙漠', '乌兰布统', '老掌沟', '浑善达克', '阿尔山']
private vehicleChips: string[] = ['越野车', '城市SUV', '皮卡']
private menuNames: string[] = ['我的收藏', '租赁订单', '车队勋章', '改装档案', '押金钱包', '俱乐部客服']
private mainList: GoodsItem[] = [
{ name: 'WARN VR EVO 10-S 绞盘', price: 4899, desc: '10000磅拉力 · 钢缆版', score: 95, num: 458, tag: '俱乐部补贴', hot: '爆' },
{ name: 'ROADSHOCK 前保险杠', price: 2380, desc: '5系铝合金 · 雾灯位预留', score: 92, num: 326, tag: '包安装', hot: '热' },
{ name: '拓乐 Traverse 车顶架', price: 3299, desc: '静音横杆 · 承重75kg', score: 88, num: 291, tag: '晒单返现', hot: '荐' },
{ name: '绞杠者 32寸 LED 射灯', price: 1280, desc: 'IP68防水 · 5700流明', score: 90, num: 512, tag: '限时直降', hot: '爆' },
{ name: '曜石 铝合金侧踏杠', price: 1180, desc: '一体式 · 防滑踏板', score: 86, num: 203, tag: '免费打孔', hot: '热' },
{ name: '通用型高位涉水喉', price: 960, desc: '高位进气 · 附赠滤芯', score: 84, num: 167, tag: '含安装支架', hot: '荐' },
{ name: 'ARB 气动差速锁', price: 6800, desc: '前桥 · 牙嵌式结构', score: 97, num: 389, tag: '老带新减200', hot: '爆' },
{ name: 'Smittybilt X20 绞盘', price: 5600, desc: '10000磅 · 合成纤维缆', score: 93, num: 441, tag: '战队同款', hot: '热' },
{ name: '迪玛仕 后保险杠', price: 1980, desc: '拖车方口 · 备胎位', score: 87, num: 158, tag: '全国包邮', hot: '荐' },
{ name: '顶火 行李拓展框', price: 4280, desc: '侧帐兼容 · 静音胶条', score: 91, num: 275, tag: '团购进行中', hot: '热' },
{ name: '阿帕 舞台射灯 32寸', price: 1560, desc: '远光增强 · 聚焦透镜', score: 89, num: 318, tag: '赠开关线组', hot: '爆' },
{ name: '曙光 侧杠护板', price: 1380, desc: '树脂踏板 · 防冻防滑', score: 82, num: 142, tag: '以旧换新', hot: '荐' },
{ name: '澳版涉水喉 坦克300', price: 1080, desc: '原厂位 · 免打孔', score: 94, num: 233, tag: '车友验货', hot: '热' },
{ name: '伊顿 电控差速锁', price: 7900, desc: '前后桥 · 免气源', score: 96, num: 365, tag: '俱乐部专享', hot: '爆' },
{ name: 'WARN VR EVO 8-S 绞盘', price: 3980, desc: '8000磅 · 入门款', score: 85, num: 277, tag: '分期免息', hot: '荐' },
{ name: '曙光 轻量化前杠', price: 2260, desc: '龙门架 · 绞盘位预留', score: 88, num: 194, tag: '包物流', hot: '热' },
{ name: '越野者 顶框拓展板', price: 890, desc: '横杆适配 · 铝合金板', score: 80, num: 121, tag: '满减优惠', hot: '荐' },
{ name: '火焰 20寸射灯组', price: 860, desc: '三色温 · 遥控变光', score: 87, num: 405, tag: '爆款返场', hot: '爆' },
{ name: '踏界 电动侧踏板', price: 2680, desc: '原车协议 · 感应伸缩', score: 92, num: 246, tag: '含上门安装', hot: '热' },
{ name: '鲨鱼鳃 涉水喉罩', price: 320, desc: '防雨雪 · 通用口径', score: 78, num: 98, tag: '清仓特惠', hot: '荐' },
{ name: 'TOD 强力脱困板 12吨', price: 1150, desc: '软卸扣 · 三件套装', score: 90, num: 352, tag: '穿越必备', hot: '爆' },
{ name: '牧马人 后杠备胎架', price: 3480, desc: '侧开式 · 集成爬梯', score: 91, num: 210, tag: '预售订金', hot: '热' },
{ name: '谷米 车顶帐导轨', price: 690, desc: '横竖两用 · 快装', score: 83, num: 134, tag: '新品首发', hot: '荐' },
{ name: '北极狼 尾门梯', price: 1480, desc: '铝合金 · 承重150kg', score: 88, num: 187, tag: '车友推荐', hot: '热' }
]
private liftList: GoodsItem[] = [
{ name: 'OME 2寸升高套件', price: 3280, desc: '升高50mm · 含四支减震', score: 95, num: 50, tag: '适配坦克300', hot: '热' },
{ name: 'Dobinsons 3寸套件', price: 4980, desc: '升高75mm · 重载弹簧', score: 93, num: 75, tag: '适配普拉多', hot: '爆' },
{ name: 'TERAFLEX 2.5寸套件', price: 5860, desc: '升高63mm · 可配FOX', score: 96, num: 63, tag: '适配牧马人', hot: '热' },
{ name: 'Eibach 1.5寸短簧', price: 2960, desc: '升高38mm · 街野兼顾', score: 88, num: 38, tag: '适配BJ40', hot: '荐' },
{ name: '曙光 2寸升高套件', price: 2680, desc: '升高50mm · 短弹簧', score: 85, num: 50, tag: '适配哈弗H9', hot: '荐' },
{ name: 'KING 3寸绞牙套装', price: 8900, desc: '升高76mm · 双向可调', score: 97, num: 76, tag: '适配坦克300', hot: '爆' },
{ name: 'OME 加重后簧', price: 1280, desc: '升高30mm · 载重加三百', score: 84, num: 30, tag: '适配普拉多', hot: '热' },
{ name: '顶火 2.5寸长臂套件', price: 7600, desc: '升高63mm · 校正角度', score: 94, num: 63, tag: '适配牧马人', hot: '爆' },
{ name: 'RC 2寸经济套装', price: 2180, desc: '升高50mm · 入门首选', score: 80, num: 50, tag: '适配BJ40', hot: '荐' },
{ name: 'icon 车队版 3寸', price: 9600, desc: '升高78mm · 定制调校', score: 98, num: 78, tag: '适配坦克300', hot: '爆' },
{ name: '域外 1寸扭齿垫片', price: 560, desc: '升高25mm · 低成本方案', score: 76, num: 25, tag: '通用车型', hot: '荐' },
{ name: 'FOX 2.0 减震四支', price: 6200, desc: '升高40mm · 旁路罐体', score: 92, num: 40, tag: '适配哈弗H9', hot: '热' }
]
private tireList: GoodsItem[] = [
{ name: '百路驰 KO2 265/70R17', price: 1480, desc: '全地形 · 胎纹14.5mm', score: 95, num: 486, tag: '耐磨之王', hot: '爆' },
{ name: '固特异 Wrangler MT', price: 1720, desc: '泥地胎 · 胎纹17.2mm', score: 93, num: 352, tag: '泥地首选', hot: '热' },
{ name: '库珀 STT PRO', price: 1690, desc: '泥地胎 · 胎纹16.8mm', score: 91, num: 298, tag: '自清洁纹', hot: '热' },
{ name: '朝阳 SU318', price: 760, desc: '全路况 · 胎纹12.1mm', score: 84, num: 405, tag: '性价比高', hot: '荐' },
{ name: '佳通 AT70', price: 890, desc: '全地形 · 胎纹13.6mm', score: 86, num: 331, tag: '静音舒适', hot: '荐' },
{ name: '米其林 LTX Force', price: 1580, desc: '公路越野 · 胎纹11.8mm', score: 88, num: 267, tag: '长途舒适', hot: '荐' },
{ name: '东洋 Open Country', price: 1390, desc: '全地形 · 胎纹14.2mm', score: 90, num: 314, tag: '均衡之选', hot: '热' },
{ name: '芬纳德 沙漠专用胎', price: 2350, desc: '沙漠胎 · 胎纹15.5mm', score: 92, num: 189, tag: '撒沙利器', hot: '爆' },
{ name: '锦湖 MT51', price: 990, desc: '泥地胎 · 胎纹15.9mm', score: 83, num: 226, tag: '入门泥地', hot: '荐' },
{ name: '诺记洛特 冬季AT', price: 1250, desc: '冰雪混合 · 胎纹13.1mm', score: 87, num: 358, tag: '北方推荐', hot: '热' }
]
private rackList: GoodsItem[] = [
{ name: '拓乐 翼型横杆', price: 1120, desc: '静音风阻 · 承重75kg', score: 92, num: 75, tag: '城市友好', hot: '热' },
{ name: '顶火 拓展行李框', price: 3299, desc: '侧帐位 · 承重150kg', score: 95, num: 150, tag: '过载之王', hot: '爆' },
{ name: '曙光 平台顶架', price: 2580, desc: '低风阻 · 承重120kg', score: 90, num: 120, tag: '长途低噪', hot: '热' },
{ name: 'ROLA 运动筐', price: 1980, desc: '弹力网 · 承重90kg', score: 86, num: 90, tag: '实用之选', hot: '荐' },
{ name: '铁马 全铝框架', price: 3860, desc: '免打孔 · 承重180kg', score: 96, num: 180, tag: '重装穿越', hot: '爆' },
{ name: '骆驼 侧帐横梁', price: 760, desc: '两米侧帐 · 承重60kg', score: 84, num: 60, tag: '侧帐伴侣', hot: '荐' },
{ name: '山猫 车顶梯架', price: 680, desc: '尾门梯 · 承重100kg', score: 88, num: 100, tag: '上下自如', hot: '热' },
{ name: '越野者 备胎架顶装', price: 1480, desc: '33寸胎 · 承重80kg', score: 89, num: 80, tag: '大胎必备', hot: '热' },
{ name: '极道 短横杆对', price: 520, desc: '轻装载 · 承重45kg', score: 80, num: 45, tag: '轻量化派', hot: '荐' },
{ name: '荒原 重型顶筐', price: 4690, desc: '带爬梯 · 承重200kg', score: 97, num: 200, tag: '极限重载', hot: '爆' }
]
private routeList: GoodsItem[] = [
{ name: '库布齐沙漠穿越线', price: 680, desc: '全程86km · 星空营地', score: 86, num: 420, tag: '周六 07:30', hot: '满' },
{ name: '乌兰布统草原环线', price: 520, desc: '全程132km · 公路越野', score: 78, num: 260, tag: '周日 08:00', hot: '热' },
{ name: '老掌沟原始穿越', price: 860, desc: '全程45km · 交叉轴密集', score: 93, num: 480, tag: '周六 06:30', hot: '爆' },
{ name: '浑善达克沙地线', price: 590, desc: '全程68km · 沙板教学', score: 82, num: 310, tag: '周六 08:30', hot: '热' },
{ name: '阿尔山森林防火道', price: 990, desc: '全程210km · 两日穿越', score: 95, num: 390, tag: '五一假期', hot: '爆' },
{ name: '张北草原天路', price: 420, desc: '全程100km · 摄影路线', score: 70, num: 180, tag: '周日 07:00', hot: '荐' },
{ name: '大青山碎石坡', price: 760, desc: '全程28km · 技术路段', score: 90, num: 440, tag: '每月首周六', hot: '满' },
{ name: '翡翠湖盐碱地', price: 480, desc: '全程55km · 拍照打卡', score: 74, num: 220, tag: '周六 09:00', hot: '荐' },
{ name: '白河峡谷溯溪线', price: 640, desc: '全程38km · 水路驾驶', score: 85, num: 350, tag: '周日 06:00', hot: '热' },
{ name: '坝上无人区穿越', price: 1180, desc: '全程260km · 三日重装', score: 98, num: 460, tag: '国庆出发', hot: '爆' }
]
private msgList: GoodsItem[] = [
{ name: '越野老张', price: 0, desc: '你关注的绞盘到货了,速来仓库验货', score: 100, num: 452, tag: '10:24', hot: '未读' },
{ name: '车队群 · 装备部', price: 0, desc: '本周团购护杠截单,还差3人成团', score: 100, num: 388, tag: '09:51', hot: '未读' },
{ name: '改件小妹', price: 0, desc: '您预订的升高套件已到店,可约安装', score: 100, num: 336, tag: '09:12', hot: '未读' },
{ name: '俱乐部队长', price: 0, desc: '周六库布齐集合点改到东门加油站', score: 100, num: 301, tag: '昨天', hot: '未读' },
{ name: '轮胎侠', price: 0, desc: '四条KO2已经装车平衡完毕,来取车', score: 0, num: 274, tag: '昨天', hot: '已读' },
{ name: '悬挂哥', price: 0, desc: '升高后记得做一次四轮定位预约', score: 0, num: 231, tag: '周一', hot: '已读' },
{ name: '顶架姐', price: 0, desc: '侧帐横梁补发配件已寄出顺丰', score: 0, num: 198, tag: '周一', hot: '已读' },
{ name: '差速锁工坊', price: 0, desc: '安装工位紧张,建议提前三天预约', score: 0, num: 165, tag: '周二', hot: '已读' },
{ name: '沙漠向导老白', price: 0, desc: '气压放到1.2再进沙,切记带气泵', score: 100, num: 142, tag: '周二', hot: '未读' },
{ name: '押金管理员', price: 0, desc: '射灯租金结算成功,押金已退回', score: 0, num: 118, tag: '周三', hot: '已读' },
{ name: '路线规划师', price: 0, desc: '老掌沟新开备用岔路,绕开塌方段', score: 0, num: 96, tag: '周三', hot: '已读' },
{ name: '老王修车', price: 0, desc: '半轴防尘套老化,穿越前建议更换', score: 0, num: 72, tag: '周四', hot: '已读' }
]
selName(): string {
if (this.selectedItem !== null) {
return this.selectedItem.name
}
return ''
}
selPrice(): number {
if (this.selectedItem !== null) {
return this.selectedItem.price
}
return 0
}
selDesc(): string {
if (this.selectedItem !== null) {
return this.selectedItem.desc
}
return '暂无'
}
selScore(): number {
if (this.selectedItem !== null) {
return this.selectedItem.score
}
return 0
}
selNum(): number {
if (this.selectedItem !== null) {
return this.selectedItem.num
}
return 0
}
maxNumOf(list: GoodsItem[]): number {
let m: number = 1
for (let i = 0; i < list.length; i++) {
if (list[i].num > m) {
m = list[i].num
}
}
return m
}
catColorOf(index: number): string {
let colors: string[] = ['#455A64', '#FFC400', '#558B2F', '#E53935', '#795548']
return colors[index % colors.length]
}
catLetterOf(index: number): string {
let letters: string[] = ['绞', '杠', '架', '灯', '踏', '喉', '锁']
return letters[index % letters.length]
}
msgColorOf(index: number): string {
let colors: string[] = ['#455A64', '#FFC400', '#558B2F', '#795548', '#E53935', '#37474F']
return colors[index % colors.length]
}
msgLetterOf(index: number): string {
let letters: string[] = ['张', '群', '妹', '队', '侠', '哥', '姐', '坊', '白', '管', '师', '王']
return letters[index % letters.length]
}
diffColorOf(num: number): string {
if (num >= 420) {
return '#E53935'
}
if (num >= 300) {
return '#FF8F00'
}
if (num >= 200) {
return '#FFC400'
}
return '#558B2F'
}
rentUnit(): number {
if (this.chipC === 0) {
return 288
}
if (this.chipC === 1) {
return 260
}
return 198
}
rentDays(): number {
if (this.chipC === 0) {
return 1
}
if (this.chipC === 1) {
return 7
}
return 30
}
rentTotal(): number {
return Math.floor(this.rentUnit() * this.rentDays())
}
depositOf(): number {
return Math.floor(this.selPrice() * 0.2)
}
joinTotal(): number {
return Math.floor(680 * this.stepNum)
}
@Builder
headerBuilder() {
Column() {
Row() {
Column() {
Text('越')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
}
.width(34).height(34).borderRadius(8).backgroundColor('#FFC400')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Text('越野改装俱乐部')
.fontSize(19).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
.margin({ left: 10 })
Column().layoutWeight(1)
Text('⇪')
.fontSize(18).fontColor('#FFFFFF').padding({ left: 8, right: 4 })
}
.width('100%').padding({ left: 16, right: 16, top: 12, bottom: 12 })
.alignItems(VerticalAlign.Center)
Row() {
Text('🔍')
.fontSize(14)
Text('搜改装件/车型')
.fontSize(13).fontColor('#90A4AE').margin({ left: 6 })
}
.width('100%').height(36).borderRadius(18).backgroundColor('#212121')
.padding({ left: 14 }).margin({ left: 16, right: 16 })
.alignItems(VerticalAlign.Center)
Row() {
Text('注册会员 2.4万 · 本月团购改装件 386 件')
.fontSize(11).fontColor('#4E342E')
}
.width('100%').padding({ left: 12, top: 6, bottom: 6 }).backgroundColor('#FFC400')
.margin({ top: 10 }).alignItems(VerticalAlign.Center)
}
.width('100%').backgroundColor('#455A64').padding({ bottom: 12 })
}
@Builder
bottomBuilder() {
Row() {
ForEach(this.tabs, (tab: string, index: number) => {
Column({ space: 2 }) {
Text(tab)
.fontSize(10).fontColor(this.currentTab === index ? '#455A64' : '#999999')
Column()
.width(18).height(3).borderRadius(2)
.backgroundColor(this.currentTab === index ? '#FFC400' : '#FFFFFF00')
}
.layoutWeight(1).justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentTab = index
})
}, (tab: string) => tab)
}
.width('100%').height(56).backgroundColor('#FFFFFF')
}
@Builder
tab0() {
Column({ space: 12 }) {
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('32')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#455A64')
Text('今日上新')
.fontSize(11).fontColor('#999999')
}
.layoutWeight(1).height(64).borderRadius(10).backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('18')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF8F00')
Text('成团中')
.fontSize(11).fontColor('#999999')
}
.layoutWeight(1).height(64).borderRadius(10).backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('7')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E53935')
Text('已售罄')
.fontSize(11).fontColor('#999999')
}
.layoutWeight(1).height(64).borderRadius(10).backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
}
.width('100%')
Row({ space: 8 }) {
Text('+')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#455A64')
Text('发布改装件 · 团购拼单')
.fontSize(13).fontWeight(FontWeight.Bold).fontColor('#455A64')
Column().layoutWeight(1)
Text('车友发布 128 条')
.fontSize(11).fontColor('#999999')
}
.width('100%').height(44).borderRadius(10).backgroundColor('#FFFFFF')
.padding({ left: 14, right: 14 }).alignItems(VerticalAlign.Center)
.onClick(() => {
this.showAddModal = true
})
Text('精选改装件 · 点击查看详情')
.fontSize(13).fontWeight(FontWeight.Bold).fontColor('#455A64').width('100%')
ForEach(this.mainList, (item: GoodsItem, index: number) => {
Row({ space: 10 }) {
Column() {
Text(this.catLetterOf(index))
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}
.width(52).height(52).borderRadius(10).backgroundColor(this.catColorOf(index))
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 5 }) {
Text(item.name)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.desc)
.fontSize(11).fontColor('#999999').maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 6 }) {
Row() {
Column()
.width(item.score + '%').height(5).borderRadius(3)
.backgroundColor('#558B2F')
}
.width(112).height(5).borderRadius(3).backgroundColor('#ECEFF1')
Text('适配 ' + item.score + '%')
.fontSize(9).fontColor('#558B2F')
}
.alignItems(VerticalAlign.Center)
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column({ space: 5 }) {
Row({ space: 2 }) {
Text('¥')
.fontSize(10).fontColor('#E53935')
Text('321321321')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E53935')
}
.alignItems(VerticalAlign.Bottom)
Text(item.tag)
.fontSize(9).fontColor('#FF8F00')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#FFF8E1')
if (item.hot === '爆') {
Text(item.hot)
.fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#E53935').scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
} else {
Text(item.hot)
.fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#455A64')
}
Text('热度 ' + item.num)
.fontSize(9).fontColor('#999999')
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12).borderRadius(12).backgroundColor('#FFFFFF')
.margin({ bottom: 10 }).alignItems(VerticalAlign.Center)
.onClick(() => {
this.selectedItem = item
this.showDetailModal = true
})
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(12).alignItems(HorizontalAlign.Start)
}
@Builder
tab1() {
Column({ space: 14 }) {
Text('本月升高量分布 (mm)')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
Row({ space: 8 }) {
ForEach(this.liftList, (item: GoodsItem) => {
Column({ space: 6 }) {
Column()
.width(22).height(item.num / this.maxNumOf(this.liftList) * 120)
.borderRadius({ topLeft: 4, topRight: 4 })
.backgroundColor(item.num >= 60 ? '#E53935' : '#558B2F')
Text(item.num + '')
.fontSize(9).fontColor('#666666')
Text(item.name)
.fontSize(8).fontColor('#999999').maxLines(1).width(24)
}
.width(24).alignItems(HorizontalAlign.Center)
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(14).borderRadius(12).backgroundColor('#FFFFFF')
Text('悬挂升高套件 · 双列选购')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.liftList, (item: GoodsItem) => {
Column({ space: 8 }) {
Row({ space: 8 }) {
Column()
.width(4).height(16).borderRadius(2).backgroundColor('#FFC400')
Text(item.name)
.fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121').maxLines(1)
.layoutWeight(1)
}
.width('100%').alignItems(VerticalAlign.Center)
Text(item.desc)
.fontSize(11).fontColor('#999999').width('100%').maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 4 }) {
Text('¥')
.fontSize(10).fontColor('#E53935')
Text('313232')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E53935')
Column().layoutWeight(1)
Text(item.hot)
.fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#558B2F')
}
.width('100%').alignItems(VerticalAlign.Bottom)
Text(item.tag)
.fontSize(10).fontColor('#455A64').width('100%')
.padding({ left: 8, top: 4, bottom: 4 }).borderRadius(6)
.backgroundColor('#ECEFF1')
}
.width('48%').padding(12).borderRadius(12).backgroundColor('#FFFFFF')
.margin({ bottom: 10 })
.onClick(() => {
this.selectedItem = item
this.showRentModal = true
})
}, (item: GoodsItem) => item.name)
}
.width('100%')
}
.width('100%').padding(12).alignItems(HorizontalAlign.Start)
}
@Builder
tab2() {
Column({ space: 14 }) {
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('46')
.fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFC400')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Text('在租轮胎(套)')
.fontSize(11).fontColor('#B0BEC5')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column()
.width(1).height(34).backgroundColor('#37474F')
Column({ space: 4 }) {
Text('12')
.fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('本周归还')
.fontSize(11).fontColor('#B0BEC5')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column()
.width(1).height(34).backgroundColor('#37474F')
Column({ space: 4 }) {
Text('8.9')
.fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('综合口碑分')
.fontSize(11).fontColor('#B0BEC5')
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').padding(16).borderRadius(12).backgroundColor('#212121')
.alignItems(VerticalAlign.Center)
Text('胎纹深度排行 (0.1mm)')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
Row({ space: 8 }) {
ForEach(this.tireList, (item: GoodsItem) => {
Column({ space: 6 }) {
Column()
.width(20).height(item.num / this.maxNumOf(this.tireList) * 120)
.borderRadius({ topLeft: 4, topRight: 4 })
.backgroundColor(item.num >= 350 ? '#E53935' : '#455A64')
Text(item.num + '')
.fontSize(9).fontColor('#666666')
Text(item.tag)
.fontSize(8).fontColor('#999999').maxLines(1).width(22)
}
.width(22).alignItems(HorizontalAlign.Center)
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(14).borderRadius(12).backgroundColor('#FFFFFF')
Text('热租轮胎 · 横向滑动')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
Scroll() {
Row({ space: 12 }) {
ForEach(this.tireList, (item: GoodsItem) => {
Column({ space: 8 }) {
Column() {
Text('265')
.fontSize(30).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('/70 R17')
.fontSize(11).fontColor('#CFD8DC')
}
.width('100%').height(90).borderRadius(10)
.backgroundColor(item.num >= 350 ? '#455A64' : '#558B2F')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Text(item.name)
.fontSize(12).fontWeight(FontWeight.Bold).fontColor('#212121').maxLines(1)
Text(item.desc)
.fontSize(10).fontColor('#999999').maxLines(1)
Row({ space: 4 }) {
Text('¥' + item.price)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E53935')
Column().layoutWeight(1)
Text(item.hot)
.fontSize(9).fontColor('#FF8F00')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#FFF8E1')
}
.width('100%').alignItems(VerticalAlign.Bottom)
}
.width(140).padding(10).borderRadius(12).backgroundColor('#FFFFFF')
}, (item: GoodsItem) => item.name)
}
.padding({ left: 2, right: 2 })
}
.scrollable(ScrollDirection.Horizontal).width('100%')
}
.width('100%').padding(12).alignItems(HorizontalAlign.Start)
}
@Builder
tab3() {
Column({ space: 14 }) {
Text('顶架承重排行 (kg)')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
Row({ space: 8 }) {
ForEach(this.rackList, (item: GoodsItem) => {
Column({ space: 6 }) {
Column()
.width(20).height(item.num / this.maxNumOf(this.rackList) * 120)
.borderRadius({ topLeft: 4, topRight: 4 })
.backgroundColor(item.num >= 150 ? '#FFC400' : '#455A64')
Text(item.num + '')
.fontSize(9).fontColor('#666666')
Text(item.hot)
.fontSize(8).fontColor('#999999')
}
.width(22).alignItems(HorizontalAlign.Center)
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(14).borderRadius(12).backgroundColor('#FFFFFF')
Text('顶架装备 · 双列网格')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.rackList, (item: GoodsItem) => {
Column({ space: 8 }) {
Text(item.name)
.fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121').maxLines(1)
.width('100%')
Text(item.desc)
.fontSize(11).fontColor('#999999').maxLines(1).width('100%')
Row({ space: 4 }) {
Text('¥')
.fontSize(10).fontColor('#E53935')
Text('312321321')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E53935')
Column().layoutWeight(1)
if (item.num >= 180) {
Text('重载王')
.fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#E53935').scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
} else {
Text(item.tag)
.fontSize(9).fontColor('#455A64')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#ECEFF1')
}
}
.width('100%').alignItems(VerticalAlign.Bottom)
Column({ space: 4 }) {
Row() {
Column()
.width(item.score + '%').height(6).borderRadius(3)
.backgroundColor('#FFC400')
}
.width('100%').height(6).borderRadius(3).backgroundColor('#ECEFF1')
Text('承重利用率 ' + item.score + '%')
.fontSize(9).fontColor('#FF8F00')
}
.width('100%').alignItems(HorizontalAlign.Start)
}
.width('48%').padding(12).borderRadius(12).backgroundColor('#FFFFFF')
.margin({ bottom: 10 }).alignItems(HorizontalAlign.Start)
}, (item: GoodsItem) => item.name)
}
.width('100%')
}
.width('100%').padding(12).alignItems(HorizontalAlign.Start)
}
@Builder
tab4() {
Column({ space: 14 }) {
Text('路线难度指数')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
Row({ space: 8 }) {
ForEach(this.routeList, (item: GoodsItem) => {
Column({ space: 6 }) {
Column()
.width(20).height(item.num / this.maxNumOf(this.routeList) * 120)
.borderRadius({ topLeft: 4, topRight: 4 })
.backgroundColor(this.diffColorOf(item.num))
Text(item.num + '')
.fontSize(9).fontColor('#666666')
}
.width(22).alignItems(HorizontalAlign.Center)
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(14).borderRadius(12).backgroundColor('#FFFFFF')
Text('本季热门路线 · 点击报名')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
ForEach(this.routeList, (item: GoodsItem) => {
Row({ space: 10 }) {
Column() {
Text(item.num + '')
.fontSize(12).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}
.width(44).height(44).borderRadius(10).backgroundColor(this.diffColorOf(item.num))
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 5 }) {
Row({ space: 6 }) {
Text(item.name)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').maxLines(1)
if (item.score >= 85) {
Text('满员预警')
.fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#E53935').scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
}
.alignItems(VerticalAlign.Center)
Text(item.desc)
.fontSize(11).fontColor('#999999').maxLines(1)
Row({ space: 6 }) {
Row() {
Column()
.width(item.score + '%').height(5).borderRadius(3)
.backgroundColor('#E53935')
}
.width(112).height(5).borderRadius(3).backgroundColor('#ECEFF1')
Text('报名 ' + item.score + '%')
.fontSize(9).fontColor('#E53935')
}
.alignItems(VerticalAlign.Center)
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column({ space: 5 }) {
Text('¥' + item.price)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#455A64')
Text(item.tag)
.fontSize(9).fontColor('#FF8F00')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#FFF8E1')
Text(item.hot)
.fontSize(9).fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(4)
.backgroundColor('#558B2F')
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(12).borderRadius(12).backgroundColor('#FFFFFF')
.margin({ bottom: 10 }).alignItems(VerticalAlign.Center)
.onClick(() => {
this.selectedItem = item
this.showJoinModal = true
})
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(12).alignItems(HorizontalAlign.Start)
}
@Builder
tab5() {
Column({ space: 12 }) {
Text('车队消息')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
ForEach(this.msgList, (item: GoodsItem, index: number) => {
Row({ space: 12 }) {
Column() {
Text(this.msgLetterOf(index))
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}
.width(46).height(46).borderRadius(23).backgroundColor(this.msgColorOf(index))
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 5 }) {
Row({ space: 6 }) {
Text(item.name)
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#212121').maxLines(1)
Column().layoutWeight(1)
Text(item.tag)
.fontSize(9).fontColor('#999999')
}
.width('100%').alignItems(VerticalAlign.Center)
Text(item.desc)
.fontSize(11).fontColor('#999999').maxLines(1).width('100%')
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
if (item.hot === '未读') {
Column()
.width(10).height(10).borderRadius(5).backgroundColor('#E53935')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
} else {
Column()
.width(10).height(10).borderRadius(5).backgroundColor('#FFFFFF00')
}
}
.width('100%').padding(12).borderRadius(12).backgroundColor('#FFFFFF')
.margin({ bottom: 10 }).alignItems(VerticalAlign.Center)
}, (item: GoodsItem) => item.name)
}
.width('100%').padding(12).alignItems(HorizontalAlign.Start)
}
@Builder
tab6() {
Column({ space: 14 }) {
Row({ space: 12 }) {
Column({ space: 4 }) {
Text('David')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('车队职位 · 装备管理组组长')
.fontSize(11).fontColor('#B0BEC5')
}
.layoutWeight(1).alignItems(HorizontalAlign.Start)
Text('会员码')
.fontSize(11).fontColor('#FFC400')
.padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(10)
.backgroundColor('#37474F')
}
.width('100%').padding(16).borderRadius(14).backgroundColor('#212121')
.alignItems(VerticalAlign.Center)
Row() {
Column() {
Text('David')
.fontSize(22).fontWeight(FontWeight.Bold).fontColor('#212121')
}
.width(70).height(70).borderRadius(35).backgroundColor('#FFC400')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.border({ width: 3, color: '#ECEFF1' }).scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
}
.width('100%').justifyContent(FlexAlign.Center)
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('23')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#455A64')
Text('我的装备')
.fontSize(11).fontColor('#999999')
}
.layoutWeight(1).height(64).borderRadius(10).backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('3')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#558B2F')
Text('租赁中')
.fontSize(11).fontColor('#999999')
}
.layoutWeight(1).height(64).borderRadius(10).backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
Column({ space: 4 }) {
Text('16')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FF8F00')
Text('路线打卡')
.fontSize(11).fontColor('#999999')
}
.layoutWeight(1).height(64).borderRadius(10).backgroundColor('#FFFFFF')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
}
.width('100%')
Column({ space: 0 }) {
ForEach(this.menuNames, (menu: string, menuIndex: number) => {
Row({ space: 10 }) {
Column() {
Text(menuIndex + 1 + '')
.fontSize(12).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}
.width(30).height(30).borderRadius(15)
.backgroundColor(this.catColorOf(menuIndex)).justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Text(menu)
.fontSize(14).fontColor('#212121')
Column().layoutWeight(1)
Text('›')
.fontSize(18).fontColor('#B0BEC5')
}
.width('100%').height(52).padding({ left: 14, right: 14 })
.backgroundColor('#FFFFFF').alignItems(VerticalAlign.Center)
.onClick(() => {
if (menuIndex === 0) {
this.showDeleteModal = true
} else if (menuIndex === 1) {
this.currentTab = 0
} else if (menuIndex === 3) {
this.currentTab = 1
}
})
}, (menu: string) => menu)
}
.width('100%').borderRadius(12).clip(true)
}
.width('100%').padding(12).alignItems(HorizontalAlign.Start)
}
@Builder
addModal() {
Column() {
Column().layoutWeight(1)
Column({ space: 14 }) {
Row() {
Text('发布改装件')
.fontSize(17).fontWeight(FontWeight.Bold).fontColor('#212121')
Column().layoutWeight(1)
Text('×')
.fontSize(18).fontColor('#999999').padding({ left: 8, right: 8 })
.onClick(() => {
this.showAddModal = false
})
}
.width('100%').alignItems(VerticalAlign.Center)
Text('选择品类')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.kindChips, (kind: string, kindIndex: number) => {
Text(kind)
.fontSize(12).fontColor(this.chipA === kindIndex ? '#FFFFFF' : '#455A64')
.padding({ left: 14, right: 14, top: 7, bottom: 7 }).borderRadius(15)
.backgroundColor(this.chipA === kindIndex ? '#455A64' : '#ECEFF1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipA = kindIndex
})
}, (kind: string) => kind)
}
.width('100%')
Text('适用车型')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.carChips, (car: string, carIndex: number) => {
Text(car)
.fontSize(12).fontColor(this.chipB === carIndex ? '#FFFFFF' : '#558B2F')
.padding({ left: 14, right: 14, top: 7, bottom: 7 }).borderRadius(15)
.backgroundColor(this.chipB === carIndex ? '#558B2F' : '#ECEFF1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipB = carIndex
})
}, (car: string) => car)
}
.width('100%')
TextInput({ placeholder: '请输入装备名称', text: this.inputName })
.height(44).fontSize(13).backgroundColor('#ECEFF1').borderRadius(8)
.onChange((value: string) => {
this.inputName = value
})
Text('确认上架')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').width('100%')
.height(46).textAlign(TextAlign.Center).borderRadius(23)
.backgroundColor('#455A64')
.onClick(() => {
this.showAddModal = false
})
}
.width('100%').padding(16).backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 }).constraintSize({ maxHeight: '80%' })
}
.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
this.showAddModal = false
})
}
@Builder
rentModal() {
Column() {
Column().layoutWeight(1)
Column({ space: 14 }) {
Row() {
Text('装备租赁')
.fontSize(17).fontWeight(FontWeight.Bold).fontColor('#212121')
Column().layoutWeight(1)
Text('×')
.fontSize(18).fontColor('#999999').padding({ left: 8, right: 8 })
.onClick(() => {
this.showRentModal = false
})
}
.width('100%').alignItems(VerticalAlign.Center)
Text(this.selName())
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#455A64').width('100%')
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text('选择租期')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.rentChips, (rent: string, rentIndex: number) => {
Text(rent + ' ¥' + this.rentUnit() + '/天')
.fontSize(12).fontColor(this.chipC === rentIndex ? '#FFFFFF' : '#FF8F00')
.padding({ left: 14, right: 14, top: 7, bottom: 7 }).borderRadius(15)
.backgroundColor(this.chipC === rentIndex ? '#FF8F00' : '#FFF8E1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipC = rentIndex
})
}, (rent: string) => rent)
}
.width('100%')
Row({ space: 10 }) {
Text('¥' + this.rentTotal())
.fontSize(30).fontWeight(FontWeight.Bold).fontColor('#E53935')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Column().layoutWeight(1)
Column({ space: 2 }) {
Text(this.rentDays() + ' 天合计租金')
.fontSize(11).fontColor('#999999')
Text('会员日租九折')
.fontSize(10).fontColor('#FF8F00')
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(14).borderRadius(10).backgroundColor('#FFF8E1')
.alignItems(VerticalAlign.Bottom)
Row({ space: 6 }) {
Text('押金')
.fontSize(12).fontColor('#999999')
Text('¥' + this.depositOf())
.fontSize(12).fontWeight(FontWeight.Bold).fontColor('#455A64')
Column().layoutWeight(1)
Text('归还验收后原路退回')
.fontSize(11).fontColor('#999999')
}
.width('100%').alignItems(VerticalAlign.Center)
Text('立即下单')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121').width('100%')
.height(46).textAlign(TextAlign.Center).borderRadius(23)
.backgroundColor('#FFC400')
.onClick(() => {
this.showRentModal = false
})
}
.width('100%').padding(16).backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 }).constraintSize({ maxHeight: '80%' })
}
.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
this.showRentModal = false
})
}
@Builder
joinModal() {
Column() {
Column().layoutWeight(1)
Column({ space: 14 }) {
Row() {
Text('越野路线报名')
.fontSize(17).fontWeight(FontWeight.Bold).fontColor('#212121')
Column().layoutWeight(1)
Text('×')
.fontSize(18).fontColor('#999999').padding({ left: 8, right: 8 })
.onClick(() => {
this.showJoinModal = false
})
}
.width('100%').alignItems(VerticalAlign.Center)
Text('选择路线')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.routeChips, (route: string, routeIndex: number) => {
Text(route)
.fontSize(12).fontColor(this.chipD === routeIndex ? '#FFFFFF' : '#558B2F')
.padding({ left: 14, right: 14, top: 7, bottom: 7 }).borderRadius(15)
.backgroundColor(this.chipD === routeIndex ? '#558B2F' : '#ECEFF1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipD = routeIndex
})
}, (route: string) => route)
}
.width('100%')
Text('车辆类型')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.vehicleChips, (vehicle: string, vehicleIndex: number) => {
Text(vehicle)
.fontSize(12).fontColor(this.chipE === vehicleIndex ? '#FFFFFF' : '#455A64')
.padding({ left: 14, right: 14, top: 7, bottom: 7 }).borderRadius(15)
.backgroundColor(this.chipE === vehicleIndex ? '#455A64' : '#ECEFF1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipE = vehicleIndex
})
}, (vehicle: string) => vehicle)
}
.width('100%')
Row() {
Text('出发车辆数')
.fontSize(13).fontColor('#212121')
Column().layoutWeight(1)
Row({ space: 12 }) {
Column() {
Text('-')
.fontSize(15).fontColor('#455A64')
}
.width(30).height(30).borderRadius(15).backgroundColor('#ECEFF1')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.onClick(() => {
if (this.stepNum > 1) {
this.stepNum = this.stepNum - 1
}
})
Text(this.stepNum + ' 辆')
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#212121').width(44)
.textAlign(TextAlign.Center)
Column() {
Text('+')
.fontSize(15).fontColor('#FFFFFF')
}
.width(30).height(30).borderRadius(15).backgroundColor('#455A64')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.onClick(() => {
if (this.stepNum < 8) {
this.stepNum = this.stepNum + 1
}
})
}
.alignItems(VerticalAlign.Center)
}
.width('100%').alignItems(VerticalAlign.Center)
Row({ space: 10 }) {
Text('¥' + this.joinTotal())
.fontSize(30).fontWeight(FontWeight.Bold).fontColor('#E53935')
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Column().layoutWeight(1)
Column({ space: 2 }) {
Text('报名费合计')
.fontSize(11).fontColor('#999999')
Text('含领队及保险')
.fontSize(10).fontColor('#FF8F00')
}
.alignItems(HorizontalAlign.End)
}
.width('100%').padding(14).borderRadius(10).backgroundColor('#FFF8E1')
.alignItems(VerticalAlign.Bottom)
Text('提交报名')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').width('100%')
.height(46).textAlign(TextAlign.Center).borderRadius(23)
.backgroundColor('#558B2F')
.onClick(() => {
this.showJoinModal = false
})
}
.width('100%').padding(16).backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 }).constraintSize({ maxHeight: '80%' })
}
.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)')
.onClick(() => {
this.showJoinModal = false
})
}
@Builder
deleteModal() {
Column() {
Column() {
Column()
.width('100%').height(6).backgroundColor('#E53935')
.borderRadius({ topLeft: 12, topRight: 12 })
Column() {
Text('!')
.fontSize(26).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
}
.width(52).height(52).borderRadius(26).backgroundColor('#E53935')
.margin({ top: 22 }).justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
Text('确认删除该装备收藏?')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121').margin({ top: 14 })
Text('删除后需重新搜索添加,历史订单不受影响')
.fontSize(11).fontColor('#999999').margin({ top: 6 })
Row({ space: 12 }) {
Text('取消')
.fontSize(14).fontColor('#666666').height(40).layoutWeight(1)
.textAlign(TextAlign.Center).borderRadius(20).backgroundColor('#ECEFF1')
.onClick(() => {
this.showDeleteModal = false
})
Text('删除')
.fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').height(40)
.layoutWeight(1).textAlign(TextAlign.Center).borderRadius(20)
.backgroundColor('#E53935')
.onClick(() => {
this.showDeleteModal = false
})
}
.width('100%').margin({ left: 18, right: 18, top: 20, bottom: 20 })
.alignItems(VerticalAlign.Center)
}
.width('72%').borderRadius(12).backgroundColor('#FFFFFF')
}
.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.onClick(() => {
this.showDeleteModal = false
})
}
@Builder
detailModal() {
Column() {
Column() {
Column() {
Text('装')
.fontSize(42).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('OFF-ROAD CLUB')
.fontSize(10).fontColor('#B0BEC5').margin({ top: 4 })
}
.width('100%').height(170).backgroundColor('#455A64')
.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
.scale({ x: 1.05, y: 1.05 })
.animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
Column({ space: 12 }) {
Row() {
Text(this.selName())
.fontSize(18).fontWeight(FontWeight.Bold).fontColor('#212121').layoutWeight(1)
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
Text('×')
.fontSize(18).fontColor('#999999').padding({ left: 8 })
.onClick(() => {
this.showDetailModal = false
})
}
.width('100%').alignItems(VerticalAlign.Top)
Row() {
Text('装备规格')
.fontSize(12).fontColor('#999999')
Column().layoutWeight(1)
Text(this.selDesc())
.fontSize(12).fontColor('#212121')
}
.width('100%').padding({ left: 10, right: 10, top: 10, bottom: 10 })
.borderRadius(8).backgroundColor('#ECEFF1').alignItems(VerticalAlign.Center)
Row({ space: 4 }) {
Text('¥')
.fontSize(14).fontColor('#E53935')
Text('this.selPrice()')
.fontSize(28).fontWeight(FontWeight.Bold).fontColor('#E53935')
Column().layoutWeight(1)
Text('热度 ' + this.selNum())
.fontSize(11).fontColor('#999999')
}
.width('100%').alignItems(VerticalAlign.Bottom)
Text('适配车型')
.fontSize(12).fontColor('#999999').width('100%')
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.carChips, (car: string, carIndex: number) => {
Text(car)
.fontSize(11).fontColor(this.chipB === carIndex ? '#FFFFFF' : '#455A64')
.padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(14)
.backgroundColor(this.chipB === carIndex ? '#455A64' : '#ECEFF1')
.margin({ right: 8, bottom: 8 })
.onClick(() => {
this.chipB = carIndex
})
}, (car: string) => car)
}
.width('100%')
Text('口碑评分')
.fontSize(12).fontColor('#999999').width('100%')
Column({ space: 6 }) {
Row() {
Column()
.width(this.selScore() + '%').height(6).borderRadius(3)
.backgroundColor('#FFC400')
}
.width('100%').height(6).borderRadius(3).backgroundColor('#ECEFF1')
Text(this.selScore() + ' 分 · 来自 1268 条车友口碑')
.fontSize(10).fontColor('#FF8F00')
}
.width('100%').alignItems(HorizontalAlign.Start)
Column().layoutWeight(1)
Row({ space: 10 }) {
Text('移除收藏')
.fontSize(13).fontColor('#E53935').height(42).layoutWeight(1)
.textAlign(TextAlign.Center).borderRadius(21)
.border({ width: 1, color: '#E53935' })
.onClick(() => {
this.showDetailModal = false
this.showDeleteModal = true
})
Text('立即租赁')
.fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121').height(42)
.layoutWeight(1).textAlign(TextAlign.Center).borderRadius(21)
.backgroundColor('#FFC400')
.onClick(() => {
this.showDetailModal = false
this.showRentModal = true
})
}
.width('100%').alignItems(VerticalAlign.Center)
}
.width('100%').layoutWeight(1).padding(16).alignItems(HorizontalAlign.Start)
}
.width('80%').height('100%').backgroundColor('#FFFFFF')
}
.width('100%').height('100%').backgroundColor('rgba(0,0,0,0.5)')
.justifyContent(FlexAlign.End).alignItems(HorizontalAlign.End)
.onClick(() => {
this.showDetailModal = false
})
}
build() {
Stack() {
Column() {
this.headerBuilder()
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tab0()
} else if (this.currentTab === 1) {
this.tab1()
} else if (this.currentTab === 2) {
this.tab2()
} else if (this.currentTab === 3) {
this.tab3()
} else if (this.currentTab === 4) {
this.tab4()
} else if (this.currentTab === 5) {
this.tab5()
} else {
this.tab6()
}
}
.width('100%')
}
.layoutWeight(1).width('100%')
this.bottomBuilder()
}
.width('100%').height('100%')
if (this.showAddModal) {
this.addModal()
}
if (this.showRentModal) {
this.rentModal()
}
if (this.showJoinModal) {
this.joinModal()
}
if (this.showDeleteModal) {
this.deleteModal()
}
if (this.showDetailModal) {
this.detailModal()
}
}
.width('100%').height('100%')
}
}
总结

本文完整拆解了一套基于HarmonyOS API 24声明式UI范式构建的越野改装俱乐部聚合大厅应用。从GoodsItem统一数据接口的定义到十五个@State响应式状态变量的管理,从七大功能Tab的逐段构建到五种弹层交互的详细实现,涵盖了ArkTS声明式UI开发的全部核心环节。应用通过catColorOf、catLetterOf、msgColorOf、msgLetterOf等基于索引取模循环的方法实现了列表项头像的自动颜色和字符分配,通过diffColorOf方法实现了路线难度的四级颜色分级,通过rentUnit、rentDays、rentTotal、depositOf、joinTotal这组关联方法构成了装备租赁和路线报名两个核心业务场景的费用计算引擎,充分展示了声明式UI在垂直领域应用中的数据驱动视图能力和业务逻辑封装能力。
从UI设计层面来看,应用充分利用了ArkUI框架提供的多样化布局容器和样式属性。Flex的FlexWrap.Wrap实现了悬挂套件和车顶架的双列网格自动换行布局,Scroll的Horizontal方向实现了越野轮胎的横向滚动画廊,Stack的层叠布局实现了消息列表的未读红点角标效果。四种柱状图(升高量、胎纹深度、承重能力、难度指数)均通过ForEach遍历数据数组并动态计算柱形高度比例实现,其中难度柱状图通过diffColorOf方法实现了数据驱动的颜色分级渲染,是纯声明式UI实现条件可视化的典型范例。动画方面,通过scale配合animation的iterations: -1参数在爆款标签、重载王标签、满员预警、在租数量、红点、头像等关键视觉元素处实现了脉冲缩放效果,为用户提供了持续性的交互引导。
从交互架构层面来看,build()方法中的Stack层叠容器配合条件渲染if语句构建了完整的弹层管理系统。发布弹层集成了Chip选择和TextInput输入两种交互形式,租赁弹层通过chipC状态变量联动rentUnit、rentDays、rentTotal三个方法实现租期切换时的实时费用重算,报名弹层通过stepNum步进器联动joinTotal方法实现车辆数调整时的费用更新。详情弹层中的双按钮设计实现了向删除弹层和租赁弹层的跳转流转,event.stopPropagation()防止内容区域点击触发遮罩关闭逻辑。整体架构清晰高效,费用计算方法链的实时联动更新机制为类似的电商交易、装备租赁等需要动态计费的应用提供了可直接参考的设计范本和实现路径。
更多推荐


所有评论(0)