【无标题】
钢铁海域的舵手手记:基于HarmonyOS API 24 ArkTS多标签页机器人格斗社区与零件商城的航海日志式深度剖析
航海日志第一则:一艘优秀的战舰,不仅需要坚固的龙骨与锋利的撞角,更需要一套精密的导航系统来指引航向。在HarmonyOS ArkTS的钢铁海域中,我们以组件为船板、以状态为罗盘、以装饰器为风帆,构筑起一座从零件采购到机甲对战的全链路数字化格斗港口。
航海日志第二则:底部悬浮Dock导航正如船舵中央那盏发光的航海灯,在茫茫信息海洋中为水手指引归港的航向。六个航线分别通往机甲舱、装配间、市集码头、对战角斗场、赛事公告板与船长室,而中央那枚发出电光蓝光芒的圆形按钮,则是通往战场的直航通道。
航海日志第三则:在ArkTS的航海图上,每一个装饰器都是一枚航标灯——@Entry标记着主港口入口,@Component声明每一艘独立船只的身份,@State如同潮汐表记录着海面的涨落变化,@Builder则是船坞中可复用的预制构件图纸,而ForEach便如同那永不停歇的波浪,将数据列表一遍遍冲刷成整齐的UI海岸线。
引言:扬帆起航前的航海准备

在鸿蒙生态的广阔海域中,机器人格斗爱好者社区是一个充满钢铁碰撞与火花飞溅的特殊港口。这座港口不仅承载着机甲战士的荣耀与梦想,更串联起从零件采购、装配调试、对战挑战到赛事排名的完整航海路线。作为这艘数字战舰的舵手,我们需要在起航前仔细研读海图,理解每一个航标、每一条航线、每一处暗礁的含义与作用。
本篇航海日志所记录的,正是一座基于HarmonyOS ArkTS API 24构建的多功能机器人格斗社区平台。该平台采用钢铁灰(#37474F)、电光蓝(#00B0FF)与警示红(#FF3D00)三色涂装,如同北冰洋航线中破冰船的涂装方案——灰色是船体的沉稳底色,蓝色是破冰前进时溅起的浪花,红色则是警示灯的灼热光芒。整个平台的导航采用底部悬浮Dock方案,中央设有一颗发光的圆形主按钮直通对战航线,这一设计区别于普通的底部导航栏,更似船舵中央那颗可以一键启动引擎的核心按钮。
从技术架构的航海视角来看,该平台的数据层定义了十七个接口(interface),如同造船图纸上的十七种标准构件规格,涵盖了重量级分类、机甲档案、统计条目、扭矩指标、价格条目、装配方案、装配工序、零件信息、特价交易、卖家口碑、对手信息、战斗记录、场地信息、赛事信息、排名信息、订单记录、成就徽章和用户评价。静态数据层则以常量数组的形式预置了所有业务数据,相当于出航前货舱中已装载妥当的各类物资。工具函数层提取了十二个纯函数用于颜色映射和格式化,它们如同航海仪器上的刻度转换表,将原始数值转换为舵手可读的颜色编码或百分比字符串。
在UI层,平台划分了六个标签页组件(Tab Component),每个组件都是一艘独立航行的小船,通过回调函数与主舰保持通讯。五个弹框组件则如同五艘补给船,分别负责零件购买(抽屉式)、装配编辑(抽屉式)、发起挑战(居中式)、删除机甲(居中式)和机甲详情(居中式)五种交互任务。主页面组件作为整个舰队的旗舰,通过@Entry装饰器标记为入口,统辖所有子组件的创建与调度,并通过bindSheet和bindContentCover两种方式分别管理抽屉式和居中式弹框的显示与隐藏。
第一航段:数据港口——接口定义与静态物资清单

1.1 龙骨铺设:十七组数据接口的定义
在船只建造的流程中,铺设龙骨是第一步。龙骨决定了船的基本形态与承载能力,同样地,接口定义决定了整个应用数据层的基本骨架。以下是部分核心接口的定义代码:
interface WeightClass176 {
name: string
cap: string
desc: string
color: string
count: number
pct: number
}
interface Mech176 {
id: number
name: string
cls: string
weight: string
weapon: string
power: number
armor: number
speed: number
winRate: number
wins: number
losses: number
tier: string
price: number
icon: string
color: string
hot: string
}
interface StatItem176 {
name: string
pct: number
color: string
}
interface TorqueItem176 {
name: string
value: number
color: string
}
interface在ArkTS中用于定义对象的类型契约,相当于造船规范中对各构件尺寸与材质的标准化约定。WeightClass176定义了重量级分类的数据结构——name字段存储级别名称(如"蚂蚁级"),cap字段存储重量上限(如"150g"),desc字段存储描述文字,color字段存储该级别对应的主题色,count字段记录在册数量,pct字段记录占比百分比。Mech176是整个应用最核心的机甲档案接口,包含了从名称、级别、重量、武器到战斗力、装甲值、速度、胜率、战绩、等级、价格、图标、颜色和热门标签等十五个字段,其信息密度如同一份完整的船舶登记证书。
关键概念:ArkTS中的interface与TypeScript的interface在语义上高度一致,但ArkTS在编译期会进行更严格的类型检查,确保运行时不会出现类型不匹配的"触礁"事故。每一个字段都必须显式声明类型,不允许使用any或undefined的模糊航法。
StatItem176和TorqueItem176是两个轻量级的统计展示接口,分别用于热度横条图和扭矩对比横条图的数据绑定。它们结构简单——一个名称、一个数值、一个颜色——却如同航海日志中的经纬度记录,简洁而精准。
1.2 货舱装载:静态数据数组的预置

龙骨铺设完毕后,接下来便是向货舱装载物资。静态数据以const常量数组的形式定义,相当于出航前预装的物资清单:
const WEIGHTS176: WeightClass176[] = [
{ name: '蚂蚁级', cap: '150g', desc: '入门首选 · 桌面竞技', color: '#00B0FF', count: 342, pct: 86 },
{ name: '甲虫级', cap: '1.36kg', desc: '人气最高 · 火花四溅', color: '#FF3D00', count: 286, pct: 72 },
{ name: '游民级', cap: '3kg', desc: '硬核玩家 · 大杀器', color: '#FFB300', count: 158, pct: 45 },
{ name: '羽量级', cap: '13.6kg', desc: '职业赛场 · 钢铁猛兽', color: '#546E7A', count: 64, pct: 21 }
]
const MECHS176: Mech176[] = [
{ id: 1, name: '雷霆战锤', cls: '甲虫级', weight: '1.35kg', weapon: '竖转鼓 12000rpm', power: 88, armor: 76, speed: 64, winRate: 71, wins: 32, losses: 13, tier: 'S', price: 1680, icon: '🔨', color: '#FF3D00', hot: '本站胜率王' },
{ id: 2, name: '蓝电螳螂', cls: '甲虫级', weight: '1.32kg', weapon: '横转刀 9800rpm', power: 82, armor: 70, speed: 78, winRate: 66, wins: 28, losses: 14, tier: 'A', price: 1550, icon: '🦗', color: '#00B0FF', hot: '速攻流代表' },
{ id: 3, name: '钢甲龟', cls: '甲虫级', weight: '1.36kg', weapon: '铲推 + 夹持', power: 55, armor: 94, speed: 42, winRate: 58, wins: 22, losses: 16, tier: 'A', price: 1320, icon: '🐢', color: '#546E7A', hot: '最硬的盾' }
]
const关键字声明的常量数组在ArkTS中用于存储不可变的静态数据。WEIGHTS176数组定义了四个重量级别——从入门级的蚂蚁级(150g)到职业级的羽量级(13.6kg),每个级别都有对应的颜色编码和热度百分比。MECHS176数组则记录了八台机甲的完整档案,每一台机甲都如同一艘登记在册的战船,拥有独特的名字、武器配置、战斗属性和战绩记录。
航海笔记:在ArkTS的UI构建函数内部,不允许直接声明变量(这是ArkTS的编译约束之一),因此所有数据预处理和工具函数都必须抽取到组件外部。这一规则如同船舱内的货物固定要求——所有松散物品必须提前在港口装舱固定,航行中不得临时变更。
1.3 航海仪器校准:工具函数层

航行途中,舵手需要依靠各类仪器将原始数据转换为可读信息。工具函数层正是这些"仪器"的软件实现:
function rateC176(r: number): string {
if (r >= 70) { return '#FF3D00' }
if (r >= 60) { return '#00B0FF' }
if (r >= 50) { return '#FFB300' }
return '#78909C'
}
function tierC176(t: string): string {
if (t === 'S') { return '#FF3D00' }
if (t === 'A') { return '#00B0FF' }
if (t === 'B') { return '#FFB300' }
return '#78909C'
}
function torqueW176(v: number): string {
return v + '%'
}
function starStr176(r: number): string {
if (r >= 5) { return '★★★★★' }
if (r >= 4) { return '★★★★☆' }
if (r >= 3) { return '★★★☆☆' }
if (r >= 2) { return '★★☆☆☆' }
return '★☆☆☆☆'
}
rateC176函数将胜率数值映射为颜色——70%以上为警示红,60%以上为电光蓝,50%以上为琥珀黄,其余为蓝灰色。tierC176将机甲等级(S/A/B)映射为对应颜色。torqueW176将数值拼接百分比符号。starStr176将评分数值转换为星号字符串。这些函数都是纯函数(Pure Function),不依赖外部状态,输入确定则输出确定,如同精密的航海六分仪,每次测量结果都可复现。
第二航段:机甲舱——Tab1组件的航海日志

2.1 登舰入口:@Component与@Builder的协同
当舵手从主舰桥切换到机甲舱时,首先映入眼帘的是MechTab176组件的构建代码:
@Component
struct MechTab176 {
mechs: Mech176[] = MECHS176
onDetail: (i: number) => void = (i: number) => {}
onDelete: (i: number) => void = (i: number) => {}
onEdit: (i: number) => void = (i: number) => {}
build() {
Scroll() {
Column({ space: 12 }) {
// 数据速览
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('8')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FF3D00')
Text('我的机甲')
.fontSize(11)
.fontColor('#78909C')
}
.layoutWeight(1)
.height(74)
.justifyContent(FlexAlign.Center)
.backgroundColor('#FFFFFF')
.borderRadius(14)
Column({ space: 4 }) {
Text('126')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#00B0FF')
Text('总场次')
.fontSize(11)
.fontColor('#78909C')
}
.layoutWeight(1)
.height(74)
.justifyContent(FlexAlign.Center)
.backgroundColor('#FFFFFF')
.borderRadius(14)
}
.width('100%')
}
.padding({ left: 14, right: 14, top: 12, bottom: 20 })
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
.backgroundColor('#F4F6F8')
.edgeEffect(EdgeEffect.Spring)
}
}
@Component装饰器用于声明一个自定义组件。在ArkTS中,struct关键字配合@Component装饰器定义了一个可复用的UI组件单元。MechTab176组件声明了三个成员变量——mechs(机甲数据数组)、onDetail(查看详情回调)、onDelete(删除回调)和onEdit(编辑回调),这些成员变量相当于船只的通讯接口,用于接收外部传入的数据和事件。
build()方法是每个@Component组件必须实现的方法,它返回该组件的UI结构。在build()方法内部,Scroll()组件创建了一个可滚动容器——Scroll是ArkUI提供的滚动容器组件,用于当内容超出屏幕时提供滚动浏览能力,如同船舶的瞭望台,可以环视超出视野的海域。Column({ space: 12 })创建了一个垂直排列的线性容器,space: 12参数指定子元素之间的间距为12vp。
Row({ space: 10 })创建了一个水平排列的线性容器——Row是ArkUI的水平布局组件,用于将子元素按水平方向依次排列,如同甲板上并排站列的水手。内部嵌套的Column组件用于构建每个统计卡片,Text('8')使用Text组件显示文字——Text是ArkUI最基础的文本展示组件,支持字号、字重、颜色等多种样式属性。.layoutWeight(1)方法设置布局权重为1,使该卡片在水平方向上均分剩余空间。.justifyContent(FlexAlign.Center)设置子元素在主轴方向居中对齐。.backgroundColor('#FFFFFF')设置背景色为白色,.borderRadius(14)设置圆角半径为14vp。
航海笔记:
@Component声明的组件可以拥有自己的状态(通过@State)和属性(通过普通成员变量声明),但它不能独立存在——必须被父组件引用才能渲染。这如同救生艇虽然结构完整,但必须挂在母船上才有意义。
2.2 重量级航线扫描:ForEach循环渲染与横滑卡片

机甲舱的第二部分是重量级划分的横滑卡片区域:
Scroll() {
Row({ space: 10 }) {
ForEach(WEIGHTS176, (w: WeightClass176) => {
Column({ space: 6 }) {
Row() {
Text(w.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('')
.layoutWeight(1)
Text(w.cap)
.fontSize(11)
.fontColor('#FFFFFF')
.opacity(0.85)
}
.width('100%')
Text(w.desc)
.fontSize(11)
.fontColor('#FFFFFF')
.opacity(0.85)
Text('')
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor('#33FFFFFF')
Column({ space: 2 }) {
Row() {
Text('在册 ' + w.count + ' 台')
.fontSize(10)
.fontColor('#FFFFFF')
.opacity(0.85)
Text('')
.layoutWeight(1)
Text(w.pct + '%')
.fontSize(10)
.fontColor('#FFFFFF')
}
.width('100%')
Row() {
Text('')
.width(w.pct + '%')
.height(5)
.borderRadius(3)
.backgroundColor('#FFFFFF')
Text('')
.layoutWeight(1)
}
.width('100%')
}
.width('100%')
}
.width(180)
.padding(12)
.borderRadius(16)
.linearGradient({ angle: 135, colors: [[w.color, 0.0], ['#37474F', 1.0]] })
.shadow({ radius: 8, color: '#26374F', offsetX: 0, offsetY: 3 })
})
}
.padding({ left: 2, right: 2 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
ForEach是ArkUI提供的循环渲染组件,用于根据数据列表生成UI组件。它的第一个参数是数据源数组(WEIGHTS176),第二个参数是渲染函数(接收每个数据项并返回对应的UI结构),第三个参数(可选)是键值生成函数(用于优化渲染性能)。ForEach如同海浪的节拍——每一朵浪花都是相似的,但又有细微的差异;每一个卡片都基于同一套模板渲染,但数据内容各不相同。
关键概念:ForEach的第三个参数(键值生成函数)在ArkTS中非常重要。它类似于航海日志中每条记录的编号——当数据列表发生变化(增删改)时,框架通过键值来判断哪些项需要重新渲染、哪些可以复用。省略键值函数时,框架默认使用数组索引,这在数据顺序变化时可能导致渲染性能下降。
.linearGradient({ angle: 135, colors: [[w.color, 0.0], ['#37474F', 1.0]] })方法为卡片设置了线性渐变背景——linearGradient是ArkUI的样式属性方法,angle指定渐变角度(135度即从左上到右下),colors数组定义渐变色站,每个色站包含颜色值和位置偏移(0.0为起点,1.0为终点)。这使每张卡片都拥有从自身主题色到钢铁灰的渐变效果,如同海面上从浅水到深水的色彩过渡。.shadow({ radius: 8, color: '#26374F', offsetX: 0, offsetY: 3 })为卡片添加了投影效果,radius控制模糊半径,offsetY控制垂直偏移,营造出卡片悬浮于甲板之上的立体感。
.scrollable(ScrollDirection.Horizontal)方法将Scroll组件设置为水平滚动方向——ScrollDirection枚举定义了Horizontal(水平)和Vertical(垂直)两种方向。.scrollBar(BarState.Off)隐藏了滚动条——BarState枚举提供On(显示)、Off(隐藏)和Auto(自动)三种状态,隐藏滚动条使界面更加简洁,如同船只吃水线以下的流线型设计。
2.3 机甲档案陈列:列表卡片的完整构建
机甲列表是Tab1的核心区域,每张卡片包含机甲图标、名称、等级、武器、胜率条等多个信息模块:
ForEach(this.mechs, (m: Mech176, idx: number) => {
Column({ space: 10 }) {
Row({ space: 10 }) {
Column() {
Text(m.icon)
.fontSize(26)
}
.width(52)
.height(52)
.borderRadius(14)
.backgroundColor(m.color + '1A')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(m.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text(m.tier)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(tierC176(m.tier))
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(6)
.backgroundColor(tierBg176(m.tier))
}
Text(m.weapon + ' · ' + m.weight)
.fontSize(11)
.fontColor('#78909C')
Row({ space: 6 }) {
Text(m.cls)
.fontSize(10)
.fontColor('#00B0FF')
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(6)
.backgroundColor('#E1F5FE')
Text(m.hot)
.fontSize(10)
.fontColor('#FF3D00')
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(6)
.backgroundColor('#FBE9E7')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('删除')
.fontSize(11)
.fontColor('#FF3D00')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor('#FBE9E7')
.onClick(() => {
this.onDelete(idx)
})
Text('装配')
.fontSize(11)
.fontColor('#00B0FF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor('#E1F5FE')
.margin({ top: 6 })
.onClick(() => {
this.onEdit(idx)
})
}
}
.width('100%')
// 胜率条
Column({ space: 4 }) {
Row() {
Text('胜率')
.fontSize(10)
.fontColor('#78909C')
Text('')
.layoutWeight(1)
Text(m.winRate + '% · ' + m.wins + '胜 ' + m.losses + '负')
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(rateC176(m.winRate))
}
.width('100%')
Row() {
Text('')
.width(m.winRate + '%')
.height(7)
.borderRadius(4)
.backgroundColor(rateC176(m.winRate))
Text('')
.layoutWeight(1)
}
.width('100%')
.height(7)
.borderRadius(4)
.backgroundColor('#ECEFF1')
}
.width('100%')
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick(() => {
this.onDetail(idx)
})
})
在这段代码中,ForEach遍历this.mechs数组,为每台机甲生成一张完整的卡片。(m: Mech176, idx: number)是渲染函数的参数签名——m是当前数据项,idx是当前索引。卡片左侧的Column使用m.color + '1A'作为背景色——这里将机甲主题色与透明度后缀1A(即26/255约10%透明度)拼接,实现了淡色背景效果,如同船舷喷涂的淡色编号。
.onClick(() => { this.onDelete(idx) })方法为"删除"按钮绑定了点击事件——onClick是ArkUI组件的通用事件方法,接收一个无参箭头函数作为回调。当用户点击该按钮时,会调用this.onDelete(idx),将当前机甲的索引传递给父组件进行删除处理。.alignItems(HorizontalAlign.Start)设置子元素在交叉轴(即水平方向,因为Column的主轴是垂直方向)上的对齐方式为起始对齐——HorizontalAlign枚举提供Start、Center、End三种水平对齐方式。
胜率条的构建值得特别关注——它由一个外层Row(背景灰色轨道)和内层的两个Text('')组成:第一个Text的宽度设置为m.winRate + '%'(如"71%"),背景色为rateC176函数返回的颜色;第二个Text使用.layoutWeight(1)填充剩余空间。这种用空Text组件作为色块的技巧,是ArkTS中实现进度条的常见手法,如同在航海图上用两种颜色的色块拼接来标注已航行与未航行的路线。
第三航段:装配间——Tab2组件与进度可视化

2.4 装配概览与工序时间轴
@Component
struct BuildTab176 {
build() {
Scroll() {
Column({ space: 12 }) {
// 当前装配概览
Column({ space: 10 }) {
Row({ space: 10 }) {
Column() {
Text('🛠️')
.fontSize(30)
}
.width(60)
.height(60)
.borderRadius(16)
.backgroundColor('#37474F')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text('雷霆战锤 · 三代改')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('甲虫级 · 武器系统装配中')
.fontSize(11)
.fontColor('#80D8FF')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('62%')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#00B0FF')
Text('总进度')
.fontSize(10)
.fontColor('#80D8FF')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row() {
Text('')
.width('62%')
.height(9)
.borderRadius(5)
.backgroundColor('#00B0FF')
Text('')
.layoutWeight(1)
}
.width('100%')
.height(9)
.borderRadius(5)
.backgroundColor('#546E7A')
}
.width('100%')
.padding(16)
.borderRadius(18)
.linearGradient({ angle: 135, colors: [['#37474F', 0.0], ['#263238', 1.0]] })
.shadow({ radius: 10, color: '#33374F40', offsetX: 0, offsetY: 4 })
BuildTab176组件是装配间的入口。概览卡片使用深色渐变背景(#37474F到#263238),营造出机甲装配车间内钢铁与机油混合的工业氛围。62%的进度条使用Text('')空文本组件实现——宽度设为'62%'字符串,高度设为9vp,背景色为电光蓝,嵌套在一个灰色背景的Row中。
航海笔记:在ArkTS中,
Text('')空文本组件是一种常见的"视觉占位"技巧。由于ArkTS不允许在UI构建函数中直接使用div或View等HTML标签,所有视觉块都必须通过组件实现。Text('')因其轻量且无文字渲染开销,常被用作纯色块、进度条、分隔线等视觉元素。
2.5 装配工序列表与步骤标记

ForEach(BUILD_STEPS176, (s: BuildStep176, idx: number) => {
Column() {
Row({ space: 10 }) {
// 步骤圆点
Column() {
Text(idx < 3 ? '✓' : (idx + 1) + '')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(idx < 3 ? '#FFFFFF' : '#78909C')
}
.width(24)
.height(24)
.borderRadius(12)
.backgroundColor(idx < 3 ? '#00B0FF' : '#ECEFF1')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row() {
Text(s.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text(s.status)
.fontSize(10)
.fontColor(stepC176(s.status))
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(6)
.backgroundColor(stepBg176(s.status))
}
.width('100%')
Row() {
Text('')
.width(s.pct + '%')
.height(5)
.borderRadius(3)
.backgroundColor(stepC176(s.status))
Text('')
.layoutWeight(1)
}
.width('100%')
.height(5)
.borderRadius(3)
.backgroundColor('#ECEFF1')
Text(s.step + ' · 负责:' + s.owner)
.fontSize(10)
.fontColor('#78909C')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
if (idx < BUILD_STEPS176.length - 1) {
Text('')
.width(1)
.height(12)
.backgroundColor('#ECEFF1')
.margin({ left: 12 })
}
}
.width('100%')
})
装配工序列表使用ForEach遍历BUILD_STEPS176数组,为每道工序生成一个步骤行。每个步骤行左侧有一个圆形标记——已完成步骤(idx < 3)显示对勾’✓’并使用蓝色背景,未完成步骤显示序号并使用灰色背景。idx < 3 ? '✓' : (idx + 1) + ''是一个三元表达式,ArkTS支持在UI构建函数中使用条件表达式来动态决定渲染内容。
关键概念:
if/else条件渲染是ArkTS中控制UI显示的重要机制。在build()方法内,可以使用if (condition) { ComponentA() } else { ComponentB() }的语法来根据条件渲染不同的UI结构。当条件变化时,ArkUI框架会自动更新UI。这如同航海中的避险航线选择——根据海况变化,舵手选择不同的航线绕过暗礁。
代码末尾的if (idx < BUILD_STEPS176.length - 1)判断用于控制步骤之间连接线的显示——除了最后一个步骤外,每个步骤下方都有一条1vp宽、12vp高的灰色竖线,形成时间轴的连接线效果。这种"有条件渲染"的方式避免了最后一个步骤下方出现多余的竖线。
第四航段:市集码头——Tab3组件与网格布局
2.6 分类导航与Flex换行布局
市集码头是零件交易的核心区域。分类导航使用胶囊按钮横排,零件展示使用Flex换行布局:
@Component
struct MarketTab176 {
@State cate: number = 0
onBuy: (i: number) => void = (i: number) => {}
build() {
Scroll() {
Column({ space: 12 }) {
// 分类胶囊
Row({ space: 8 }) {
ForEach(['全部', '武器', '驱动', '装甲', '电调', '电池'], (c: string, i: number) => {
Text(c)
.fontSize(12)
.fontColor(this.cate === i ? '#FFFFFF' : '#546E7A')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.cate === i ? '#37474F' : '#FFFFFF')
.onClick(() => {
this.cate = i
})
})
}
.width('100%')
@State是ArkTS的状态管理装饰器,当被标记的状态变量发生变化时,会自动触发UI重新渲染。@State cate: number = 0声明了一个名为cate的状态变量,初始值为0,代表当前选中的分类索引。当用户点击某个分类胶囊时,this.cate = i语句修改了状态值,ArkUI框架检测到状态变化后,会自动重新执行build()方法,更新所有依赖cate的UI元素——选中的胶囊变为深色背景白字,未选中的变为白色背景灰字。
关键概念:
@State装饰器的核心作用是实现"响应式UI"。在传统开发模式中,修改数据后需要手动调用刷新方法更新界面;而在ArkTS中,@State变量被框架代理(Proxy),任何赋值操作都会自动触发依赖该变量的UI组件重新渲染。这如同船上的自动舵系统——设定好航向后,系统自动根据海流偏移调整舵角,无需舵手手动干预。
.onClick(() => { this.cate = i })为每个分类胶囊绑定了点击事件。注意这里使用了箭头函数() => {}——箭头函数在ArkTS中是唯一合法的事件回调写法,因为它不会创建自己的this绑定,而是使用外层作用域的this。如果使用普通函数function() {},则this将指向函数自身而非组件实例,导致无法访问组件成员。
2.7 零件网格与Flex换行容器
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(PARTS176, (p: Part176, idx: number) => {
Column({ space: 8 }) {
Row() {
Column() {
Text(p.icon)
.fontSize(26)
}
.width(46)
.height(46)
.borderRadius(12)
.backgroundColor('#E1F5FE')
.justifyContent(FlexAlign.Center)
Text('')
.layoutWeight(1)
Text(p.hot)
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(6)
.backgroundColor(p.hot === '稀缺' ? '#FF3D00' : (p.hot === '新品' ? '#00B0FF' : '#FFB300'))
}
.width('100%')
Text(p.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(p.brand + ' · ' + p.type)
.fontSize(11)
.fontColor('#78909C')
// 扭矩小条
Column({ space: 3 }) {
Text('性能指数 ' + p.torque)
.fontSize(10)
.fontColor('#78909C')
Row() {
Text('')
.width(p.torque + '%')
.height(5)
.borderRadius(3)
.backgroundColor('#00B0FF')
Text('')
.layoutWeight(1)
}
.width('100%')
.height(5)
.borderRadius(3)
.backgroundColor('#ECEFF1')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
Row() {
Text('¥' + p.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FF3D00')
Text('')
.layoutWeight(1)
Text('余 ' + p.stock)
.fontSize(10)
.fontColor(p.stock < 20 ? '#FF3D00' : '#78909C')
}
.width('100%')
Text('加入装配清单')
.fontSize(12)
.fontColor('#FFFFFF')
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 7, bottom: 7 })
.borderRadius(12)
.linearGradient({ angle: 90, colors: [['#00B0FF', 0.0], ['#01579B', 1.0]] })
.onClick(() => {
this.onBuy(idx)
})
}
.width('48.5%')
.padding(12)
.margin({ bottom: 10 })
.backgroundColor('#FFFFFF')
.borderRadius(16)
})
}
.width('100%')
Flex是ArkUI提供的弹性布局容器组件,wrap: FlexWrap.Wrap参数设置了换行模式——FlexWrap枚举提供NoWrap(不换行)和Wrap(换行)两种模式。justifyContent: FlexAlign.SpaceBetween设置子元素在主轴方向均匀分布,首尾贴边——FlexAlign枚举提供Start、Center、End、SpaceBetween、SpaceAround、SpaceEvenly等多种对齐模式。Flex配合width('48.5%')的子元素,实现了两列网格的布局效果,如同货架上整齐排列的货物。
航海笔记:
Flex组件与Row/Column的区别在于Flex支持换行。当子元素总宽度超过容器宽度时,Flex(wrap: Wrap)会自动将溢出的子元素折行排列,而Row则不会换行。在实现商品网格时,Flex是比Grid更灵活的选择——它不需要预设列数模板,而是根据子元素自身宽度自动排版。
.maxLines(1)方法限制Text组件最多显示一行——maxLines是Text组件的属性方法,用于控制文字行数。.textOverflow({ overflow: TextOverflow.Ellipsis })设置文字溢出时的处理方式为省略号——TextOverflow枚举提供None(不处理)、Clip(裁剪)和Ellipsis(省略号)三种模式。这两个属性配合使用,实现了长文本的单行截断省略效果,如同航海日志中过长的航线名称被缩写为简码。
第五航段:对战角斗场——Tab4组件与战斗记录
2.8 积分卡与战绩堆叠条
@Component
struct BattleTab176 {
onChallenge: () => void = () => {}
build() {
Scroll() {
Column({ space: 12 }) {
// 积分卡
Column({ space: 12 }) {
Row({ space: 12 }) {
Column() {
Text('🤖')
.fontSize(28)
}
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('#3300B0FF')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text('钢之心')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('赛季段位:钻石 · 城市榜第 4')
.fontSize(11)
.fontColor('#80D8FF')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('2330')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#00B0FF')
Text('竞技分')
.fontSize(10)
.fontColor('#80D8FF')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
// 战绩堆叠条:胜 62% / 平 8% / 负 30%
Column({ space: 5 }) {
Row() {
Text('总战绩 126 场')
.fontSize(10)
.fontColor('#80D8FF')
Text('')
.layoutWeight(1)
Row({ space: 10 }) {
Text('● 胜 78')
.fontSize(10)
.fontColor('#00B0FF')
Text('● 平 10')
.fontSize(10)
.fontColor('#FFB300')
Text('● 负 38')
.fontSize(10)
.fontColor('#FF3D00')
}
}
.width('100%')
Row() {
Text('')
.width('62%')
.height(12)
.backgroundColor('#00B0FF')
Text('')
.width('8%')
.height(12)
.backgroundColor('#FFB300')
Text('')
.layoutWeight(1)
.height(12)
.backgroundColor('#FF3D00')
}
.width('100%')
.height(12)
.borderRadius(6)
.clip(true)
}
}
.width('100%')
.padding(16)
.borderRadius(18)
.linearGradient({ angle: 135, colors: [['#37474F', 0.0], ['#01579B', 1.0]] })
.shadow({ radius: 10, color: '#33374F40', offsetX: 0, offsetY: 4 })
BattleTab176组件是对战角斗场的入口。积分卡使用从钢铁灰到深蓝的渐变背景,展示了玩家昵称、段位、竞技分等核心信息。战绩堆叠条是本段的亮点——它使用三个Text('')色块拼接而成:蓝色(62%胜)、黄色(8%平)、红色(30%负),三个色块在同一行内紧密排列,形成堆叠条形图。
.clip(true)方法启用了裁剪——clip是ArkUI的通用属性方法,当设置为true时,子元素超出容器边界的部分将被裁剪。这里开启裁剪是为了确保堆叠条的圆角效果不被内部色块的直角破坏。
关键概念:
.clip(true)在ArkUI中常用于圆角容器内的子元素裁剪。当一个设置了borderRadius的容器包含有自身背景色的子元素时,子元素的直角会"刺破"容器的圆角。开启clip后,子元素会沿着容器的圆角边界被裁剪,保持视觉一致性。
2.9 对手列表与在线状态条件渲染
ForEach(OPPONENTS176, (o: Opponent176) => {
Row({ space: 10 }) {
Column() {
Text(o.icon)
.fontSize(22)
}
.width(44)
.height(44)
.borderRadius(22)
.backgroundColor('#ECEFF1')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(o.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#263238')
if (o.online) {
Text('在线')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(5)
.backgroundColor('#00B0FF')
} else {
Text('离线')
.fontSize(9)
.fontColor('#78909C')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(5)
.backgroundColor('#ECEFF1')
}
}
Text(o.mech + ' · ' + o.style)
.fontSize(11)
.fontColor('#78909C')
Row({ space: 4 }) {
Text('竞技分 ' + o.rating)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(o.rating >= 1800 ? '#FF3D00' : '#00B0FF')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('约战')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor('#37474F')
.onClick(() => {
this.onChallenge()
})
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(14)
})
对手列表使用ForEach遍历OPPONENTS176数组。每个对手行包含头像、名称、在线状态标签、机甲信息和竞技分。在线状态使用了if/else条件渲染——if (o.online)为true时显示蓝色"在线"标签,为false时显示灰色"离线"标签。
关键概念:
if/else条件渲染是ArkUI的声明式UI核心机制之一。与传统命令式UI通过setVisibility等方法控制显示隐藏不同,ArkUI的if/else在编译期生成条件分支,当条件变化时框架自动创建或销毁对应的UI组件。这种声明式的方式更接近航海日志的写法——“若风向顺转则张满帆,若逆风则收帆换舷”,而非"检查风向,执行张帆操作,执行收帆操作"。
o.rating >= 1800 ? '#FF3D00' : '#00B0FF'使用了三元表达式在属性值层面做条件判断——1800分以上的对手竞技分用警示红色标注,以下的用电光蓝。这种内联条件表达式在ArkTS中合法且常见,适合简单的二值选择场景。
2.10 战斗记录时间轴
ForEach(BATTLES176, (b: Battle176, idx: number) => {
Column() {
Row({ space: 10 }) {
// 胜负徽章
Column() {
Text(b.win ? '胜' : '负')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width(26)
.height(26)
.borderRadius(13)
.backgroundColor(b.win ? '#00B0FF' : '#FF3D00')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(b.date)
.fontSize(11)
.fontColor('#78909C')
Text(b.score)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(b.win ? '#00B0FF' : '#FF3D00')
}
Text('VS ' + b.opponent + ' · 出击:' + b.mech)
.fontSize(12)
.fontColor('#263238')
.fontWeight(FontWeight.Medium)
Text(b.arena)
.fontSize(10)
.fontColor('#78909C')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ left: 14, right: 14, top: 9, bottom: 9 })
if (idx < BATTLES176.length - 1) {
Text('')
.width(1)
.height(10)
.backgroundColor('#ECEFF1')
.margin({ left: 27 })
}
}
.width('100%')
})
战斗记录的时间轴效果与装配工序列表的实现思路一致——每条记录之间用一条竖线连接,最后一条不显示竖线。胜负徽章使用b.win ? '胜' : '负'三元表达式选择文字,b.win ? '#00B0FF' : '#FF3D00'选择颜色——胜为蓝,负为红。
第六航段:赛事公告板——Tab5组件与排名榜
2.11 焦点赛事卡片与赛季积分榜
@Component
struct TourTab176 {
build() {
Scroll() {
Column({ space: 12 }) {
// 焦点赛事
Column({ space: 12 }) {
Row() {
Text('🏆 旗舰赛事')
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#33FF3D00')
Text('')
.layoutWeight(1)
Text('官方认证')
.fontSize(10)
.fontColor('#FFFFFF')
.opacity(0.8)
}
.width('100%')
Text('多多钢甲全国争霸赛 · 年度总决赛')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('12-06 ~ 12-08 · 上海国际格斗中心 · 羽量级 + 甲虫级双赛道')
.fontSize(11)
.fontColor('#FFFFFF')
.opacity(0.85)
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('¥100,000')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFB300')
Text('总奖金池')
.fontSize(10)
.fontColor('#FFFFFF')
.opacity(0.8)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 2 }) {
Text('256')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('报名机甲')
.fontSize(10)
.fontColor('#FFFFFF')
.opacity(0.8)
}
.alignItems(HorizontalAlign.Start)
Text('立即报名')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 18, right: 18, top: 9, bottom: 9 })
.borderRadius(18)
.backgroundColor('#FF3D00')
.shadow({ radius: 6, color: '#66FF3D00', offsetX: 0, offsetY: 2 })
}
.width('100%')
}
.width('100%')
.padding(16)
.borderRadius(18)
.linearGradient({ angle: 135, colors: [['#FF3D00', 0.0], ['#37474F', 1.0]] })
.shadow({ radius: 10, color: '#33FF3D00', offsetX: 0, offsetY: 4 })
TourTab176组件展示赛事信息。焦点赛事卡片使用从红色到钢铁灰的渐变背景,配合.opacity(0.8)和.opacity(0.85)的透明度调节,营造出层次分明的信息展示效果。opacity是ArkUI的通用属性方法,取值范围为0.0(完全透明)到1.0(完全不透明),如同海雾的浓度调节——0.8如同薄雾中隐约可见的灯塔,1.0则如同晴空下的耀眼光芒。
2.12 赛季积分榜与奖牌条件渲染
ForEach(RANKS176, (r: Rank176) => {
Row({ space: 8 }) {
// 名次
Column() {
Text(r.pos === 1 ? '🥇' : (r.pos === 2 ? '🥈' : (r.pos === 3 ? '🥉' : r.pos + '')))
.fontSize(r.pos <= 3 ? 18 : 13)
.fontWeight(FontWeight.Bold)
.fontColor(r.color)
}
.width(28)
.height(28)
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(r.name)
.fontSize(13)
.fontWeight(r.name === '钢之心 · 我' ? FontWeight.Bold : FontWeight.Medium)
.fontColor(r.name === '钢之心 · 我' ? '#00B0FF' : '#263238')
Text(r.trend)
.fontSize(10)
.fontColor(r.trend.startsWith('↑') ? '#FF3D00' : (r.trend.startsWith('↓') ? '#78909C' : '#B0BEC5'))
}
Row() {
Text('')
.width((r.points / 2860 * 100).toFixed(0) + '%')
.height(6)
.borderRadius(3)
.backgroundColor(r.color)
Text('')
.layoutWeight(1)
}
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor('#ECEFF1')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 2 }) {
Text(r.points + '')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(r.color)
Text(r.wins + '胜')
.fontSize(9)
.fontColor('#78909C')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(8)
.borderRadius(10)
.backgroundColor(r.name === '钢之心 · 我' ? '#E1F5FE' : '#FFFFFF')
})
积分榜的排名标记使用了嵌套三元表达式:r.pos === 1 ? '🥇' : (r.pos === 2 ? '🥈' : (r.pos === 3 ? '🥉' : r.pos + ''))——第1名显示金牌emoji,第2名银牌,第3名铜牌,其余显示数字。同时字号也通过r.pos <= 3 ? 18 : 13进行条件设置,前三名使用更大的字号以突出显示。
航海笔记:嵌套三元表达式虽然语法合法,但在可读性上不如
if/else语句。在实际开发中,当条件分支超过两层时,建议抽取为工具函数以提高可读性。这里的设计选择体现了在UI内联表达与代码可维护性之间的权衡。
.startsWith('↑')方法是字符串原型方法,用于判断字符串是否以指定前缀开头。趋势标记r.trend以箭头符号开头时(如"↑2"表示上升2位),使用红色标注;以"↓"开头时使用灰色;其他情况(如"—"表示不变)使用浅灰色。
第七航段:船长室——Tab6组件与成就系统
2.13 个人卡片与资产展示
@Component
struct MineTab176 {
build() {
Scroll() {
Column({ space: 12 }) {
// 个人卡
Column({ space: 12 }) {
Row({ space: 12 }) {
Column() {
Text('🧑🏭')
.fontSize(30)
}
.width(60)
.height(60)
.borderRadius(30)
.backgroundColor('#3300B0FF')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text('钢之心')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('ID: STEEL-2024 · 俱乐部:火花制造局')
.fontSize(10)
.fontColor('#80D8FF')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('编辑')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(11)
.border({ width: 1, color: '#66FFFFFF' })
}
.width('100%')
Row({ space: 0 }) {
Column({ space: 2 }) {
Text('2330')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#00B0FF')
Text('竞技分')
.fontSize(10)
.fontColor('#80D8FF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('126')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFB300')
Text('场次')
.fontSize(10)
.fontColor('#80D8FF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('18')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FF3D00')
Text('KO 数')
.fontSize(10)
.fontColor('#80D8FF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text('62')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('粉丝')
.fontSize(10)
.fontColor('#80D8FF')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.borderRadius(12)
.backgroundColor('#26323866')
}
.width('100%')
.padding(16)
.borderRadius(18)
.linearGradient({ angle: 135, colors: [['#37474F', 0.0], ['#01579B', 1.0]] })
.shadow({ radius: 10, color: '#33374F40', offsetX: 0, offsetY: 4 })
MineTab176组件是船长室——个人中心页面。个人卡片使用与对战页一致的深色渐变背景,四个统计数据(竞技分、场次、KO数、粉丝)均分排列。.border({ width: 1, color: '#66FFFFFF' })方法为"编辑"按钮设置了半透明白色边框——border是ArkUI的通用属性方法,接受一个对象参数,包含width(边框宽度)、color(边框颜色)和可选的radius(边框圆角)。
2.14 成就徽章与Flex换行网格
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(BADGES176, (b: Badge176) => {
Column({ space: 5 }) {
Column() {
Text(b.icon)
.fontSize(22)
.opacity(b.got ? 1 : 0.25)
}
.width(48)
.height(48)
.borderRadius(24)
.backgroundColor(b.got ? '#E1F5FE' : '#ECEFF1')
.justifyContent(FlexAlign.Center)
Text(b.name)
.fontSize(10)
.fontColor(b.got ? '#263238' : '#B0BEC5')
}
.width('25%')
.margin({ bottom: 12 })
})
}
.width('100%')
成就徽章使用Flex换行布局,每个徽章占25%宽度(即每行4个)。已获得的徽章(b.got为true)使用opacity(1)正常显示,未获得的徽章使用opacity(0.25)淡化显示——这种"灰化"效果是游戏中常见的成就锁定/解锁视觉表现。
第八航段:补给船队——五个弹框组件的构建
3.1 弹框一:购买零件抽屉(bindSheet)
@Component
struct PartsSheet176 {
part: Part176 = PARTS176[0]
onClose: () => void = () => {}
onOk: (spec: number, qty: number, invoice: boolean) => void = (spec: number, qty: number, invoice: boolean) => {}
@State spec: number = 0
@State qty: number = 1
@State invoice: boolean = false
build() {
Column({ space: 0 }) {
// 拖拽条
Column() {
Text('')
.width(40)
.height(4)
.borderRadius(2)
.backgroundColor('#CFD8DC')
}
.width('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 6 })
Scroll() {
Column({ space: 14 }) {
// 商品头
Row({ space: 12 }) {
Column() {
Text(this.part.icon)
.fontSize(30)
}
.width(72)
.height(72)
.borderRadius(16)
.backgroundColor('#E1F5FE')
.justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Text(this.part.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text(this.part.brand + ' · ' + this.part.type + ' · 库存 ' + this.part.stock)
.fontSize(11)
.fontColor('#78909C')
Text('¥' + this.part.price)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FF3D00')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.width('100%')
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.constraintSize({ maxHeight: 320 })
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 22, topRight: 22 })
}
}
PartsSheet176是一个抽屉式弹框组件。它通过@State管理了三个内部状态——spec(规格版本索引)、qty(购买数量)和invoice(是否开具发票)。与Tab组件不同,弹框组件需要管理自身的交互状态(如用户选择了哪个规格、购买了几件),这些状态通过@State声明后,变化时会自动触发弹框内部UI更新。
.borderRadius({ topLeft: 22, topRight: 22 })方法设置了选择性圆角——只设置左上和右上的圆角半径为22vp,左下和右下保持直角。borderRadius方法可以接受数字(四角相同)或对象(分别设置四个角),对象形式为{ topLeft, topRight, bottomLeft, bottomRight }。这种设计使弹框顶部圆角与底部直角形成对比,模拟了从底部滑出的抽屉的物理形态。
航海笔记:抽屉式弹框(Sheet)在ArkUI中通过
bindSheet方法绑定。与居中式弹框(Dialog)不同,Sheet从屏幕底部滑入,通常用于需要较多交互空间的表单或列表。bindSheet的第一个参数使用$$双向绑定语法(如$$this.showBuySheet),当布尔状态变量为true时弹出Sheet,为false时关闭。
3.2 弹框二:装配编辑抽屉与TextInput
@Component
struct AssemblySheet176 {
mech: Mech176 = MECHS176[0]
onClose: () => void = () => {}
onOk: (name: string, weaponIdx: number, driveIdx: number) => void = (name: string, weaponIdx: number, driveIdx: number) => {}
@State name: string = ''
@State weapon: number = 0
@State drive: number = 0
aboutToAppear() {
this.name = this.mech.name
}
build() {
Column({ space: 0 }) {
// ... 拖拽条 ...
Column({ space: 16 }) {
// ... 标题与机甲预览 ...
// 名称输入
Column({ space: 8 }) {
Text('机甲代号')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
.width('100%')
TextInput({ text: this.name, placeholder: '给你的钢铁猛兽起个名字' })
.fontSize(14)
.fontColor('#263238')
.height(44)
.padding({ left: 12, right: 12 })
.borderRadius(12)
.backgroundColor('#F4F6F8')
.onChange((v: string) => {
this.name = v
})
}
.width('100%')
}
}
}
}
AssemblySheet176组件包含一个aboutToAppear()生命周期方法——aboutToAppear是ArkTS组件的生命周期回调,在组件创建后、UI渲染前被调用。这里在组件即将出现时,将传入的机甲名称初始化到name状态变量中。
关键概念:
aboutToAppear()是ArkTS组件生命周期的第一个阶段。完整的生命周期包括:aboutToAppear(即将出现)→build(构建UI)→aboutToReuse(即将复用,仅@Reusable组件)→aboutToDisappear(即将消失)。在aboutToAppear中进行数据初始化是推荐做法,因为此时组件实例已创建但UI尚未渲染,适合执行一次性设置。
TextInput是ArkUI提供的文本输入组件——它允许用户在输入框中键入文字。TextInput的参数对象包含text(当前文本值)和placeholder(占位提示文字)两个常用属性。.onChange((v: string) => { this.name = v })绑定了文本变化事件——当用户在输入框中键入或删除文字时,回调函数被触发,参数v为最新的文本值,赋值给this.name后由于@State的响应式机制,UI自动更新。
3.3 弹框三:发起挑战居中大弹框
@Component
struct ChallengeDialog176 {
onClose: () => void = () => {}
onOk: (oppIdx: number, arenaIdx: number, bet: number, video: boolean) => void = (oppIdx: number, arenaIdx: number, bet: number, video: boolean) => {}
@State opp: number = 0
@State arena: number = 0
@State bet: number = 100
@State video: boolean = true
build() {
Column({ space: 0 }) {
// 头部
Column({ space: 6 }) {
Text('⚔️')
.fontSize(30)
Text('发起钢甲对决')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('选定对手与场地,今晚火花四溅')
.fontSize(11)
.fontColor('#FFFFFF')
.opacity(0.85)
}
.width('100%')
.padding({ top: 22, bottom: 18 })
.alignItems(HorizontalAlign.Center)
.linearGradient({ angle: 135, colors: [['#37474F', 0.0], ['#FF3D00', 1.0]] })
Scroll() {
Column({ space: 16 }) {
// 对手选择
Column({ space: 10 }) {
Text('选择对手')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
.width('100%')
Column({ space: 8 }) {
ForEach(OPPONENTS176, (o: Opponent176, i: number) => {
Row({ space: 10 }) {
Column() {
Text(o.icon)
.fontSize(18)
}
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor('#ECEFF1')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text(o.name + ' · ' + o.rating + '分')
.fontSize(12)
.fontWeight(FontWeight.Medium)
.fontColor('#263238')
Text(o.mech + ' · ' + o.style)
.fontSize(10)
.fontColor('#78909C')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(this.opp === i ? '已选' : '选择')
.fontSize(10)
.fontColor(this.opp === i ? '#FFFFFF' : '#00B0FF')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor(this.opp === i ? '#00B0FF' : '#E1F5FE')
}
.width('100%')
.padding(10)
.borderRadius(12)
.backgroundColor(this.opp === i ? '#E1F5FE' : '#F4F6F8')
.onClick(() => {
this.opp = i
})
})
}
.width('100%')
}
.width('100%')
}
.width('100%')
.padding(16)
}
.scrollBar(BarState.Off)
.constraintSize({ maxHeight: 320 })
// 底部按钮
Row({ space: 12 }) {
Text('再想想')
.fontSize(14)
.fontColor('#546E7A')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.borderRadius(20)
.backgroundColor('#F4F6F8')
.onClick(() => {
this.onClose()
})
Text('下战书')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.layoutWeight(1.5)
.textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.borderRadius(20)
.linearGradient({ angle: 90, colors: [['#FF3D00', 0.0], ['#FF6D00', 1.0]] })
.shadow({ radius: 6, color: '#33FF3D00', offsetX: 0, offsetY: 2 })
.onClick(() => {
this.onOk(this.opp, this.arena, this.bet, this.video)
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 4, bottom: 20 })
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(20)
.constraintSize({ maxHeight: '85%' })
.shadow({ radius: 24, color: '#33374F66', offsetX: 0, offsetY: 8 })
}
}
ChallengeDialog176是挑战弹框,它使用@State管理了四个交互状态——opp(选中的对手索引)、arena(选中的场地索引)、bet(押注金额)和video(是否开启录像)。弹框头部使用从钢铁灰到警示红的渐变背景,底部按钮使用红色渐变和阴影效果,整体视觉风格与"挑战"主题高度契合。
.constraintSize({ maxHeight: '85%' })方法设置了最大高度约束——constraintSize是ArkUI的约束尺寸方法,可以设置minWidth、maxWidth、minHeight、maxHeight四个约束值。这里将弹框最大高度限制在屏幕的85%,防止内容过多时超出屏幕范围。
关键概念:
bindContentCover用于绑定居中式弹框(Dialog)。与bindSheet的底部抽屉不同,bindContentCover创建的是一个全屏蒙层覆盖,弹框内容居中显示。它的使用方式与bindSheet类似:第一个参数是$$双向绑定的布尔状态变量,第二个参数是Builder函数返回的弹框内容。
3.4 弹框四:删除机甲确认框与Toggle开关
@Component
struct DeleteDialog176 {
mech: Mech176 = MECHS176[0]
onCancel: () => void = () => {}
onConfirm: (clearRecords: boolean) => void = (clearRecords: boolean) => {}
@State clearRecords: boolean = true
build() {
Column({ space: 16 }) {
Column() {
Text('⚠️')
.fontSize(34)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#FBE9E7')
.justifyContent(FlexAlign.Center)
Text('确认拆除「' + this.mech.name + '」?')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('拆除后零件将回收入库(返还 60% 零件款),机甲档案与徽章将被注销,此操作不可撤销。')
.fontSize(11)
.fontColor('#78909C')
.textAlign(TextAlign.Center)
.lineHeight(17)
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('同时清空战斗记录')
.fontSize(12)
.fontColor('#263238')
Text('保留将只显示对手视角')
.fontSize(9)
.fontColor('#78909C')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.clearRecords })
.selectedColor('#FF3D00')
.onChange((v: boolean) => {
this.clearRecords = v
})
}
.width('100%')
Row({ space: 12 }) {
Text('手滑了')
.fontSize(14)
.fontColor('#546E7A')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 11, bottom: 11 })
.borderRadius(20)
.backgroundColor('#F4F6F8')
.onClick(() => {
this.onCancel()
})
Text('确认拆除')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.layoutWeight(1.5)
.textAlign(TextAlign.Center)
.padding({ top: 11, bottom: 11 })
.borderRadius(20)
.backgroundColor('#FF3D00')
.onClick(() => {
this.onConfirm(this.clearRecords)
})
}
.width('100%')
.margin({ top: 4 })
}
.width('82%')
.padding(20)
.backgroundColor('#FFFFFF')
.borderRadius(20)
.shadow({ radius: 24, color: '#33374F66', offsetX: 0, offsetY: 8 })
}
}
DeleteDialog176是删除机甲的确认弹框。它使用了Toggle组件——Toggle是ArkUI提供的开关切换组件,type: ToggleType.Switch设置开关样式为iOS风格滑动开关(另一种是ToggleType.Checkbox复选框样式)。isOn参数绑定开关状态,selectedColor设置开启状态的主题色,onChange回调在开关状态变化时触发。
关键概念:
Toggle组件在ArkUI中用于布尔值的切换交互。它比用Text模拟的☑/☐复选框更加规范,提供了更好的触摸体验和视觉反馈。ToggleType枚举定义了Switch(滑动开关)和Checkbox(复选框)两种形态。在实际开发中,表单内的布尔值选择推荐使用Toggle而非自制复选框。
3.5 弹框五:机甲详情大弹框
@Component
struct MechDetailDialog176 {
mech: Mech176 = MECHS176[0]
onClose: () => void = () => {}
build() {
Column({ space: 0 }) {
// 渐变头部
Column({ space: 10 }) {
Row() {
Text('')
.layoutWeight(1)
Text('✕')
.fontSize(15)
.fontColor('#FFFFFF')
.padding(8)
.onClick(() => {
this.onClose()
})
}
.width('100%')
Row({ space: 14 }) {
Column() {
Text(this.mech.icon)
.fontSize(40)
}
.width(76)
.height(76)
.borderRadius(22)
.backgroundColor('#33FFFFFF')
.justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Row({ space: 8 }) {
Text(this.mech.name)
.fontSize(19)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(this.mech.tier)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 7, right: 7, top: 2, bottom: 2 })
.borderRadius(7)
.backgroundColor('#33FF3D00')
}
Text(this.mech.cls + ' · ' + this.mech.weight + ' · ' + this.mech.hot)
.fontSize(11)
.fontColor('#FFFFFF')
.opacity(0.9)
Text('胜率 ' + this.mech.winRate + '% · ' + this.mech.wins + '胜 ' + this.mech.losses + '负')
.fontSize(11)
.fontColor('#FFB300')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
}
.width('100%')
.padding({ left: 18, right: 18, top: 12, bottom: 20 })
.alignItems(HorizontalAlign.Start)
.linearGradient({ angle: 135, colors: [[this.mech.color, 0.0], ['#37474F', 1.0]] })
MechDetailDialog176是机甲详情弹框,头部使用了以机甲自身颜色为起点的渐变背景——linearGradient的colors参数中[this.mech.color, 0.0]将渐变起点设置为机甲的专属颜色(如"雷霆战锤"为#FF3D00红色,"蓝电螳螂"为#00B0FF蓝色),终点统一为钢铁灰。这种设计使每台机甲的详情页都拥有独特的视觉风格。
第九航段:旗舰指挥——主页面组件的构建
4.1 主入口@Entry与状态管理
@Entry
@Component
struct RobotWar176 {
@State currentTab: number = 0
@State myMechs: Mech176[] = MECHS176
@State showBuySheet: boolean = false
@State showEditSheet: boolean = false
@State showChallenge: boolean = false
@State showDelete: boolean = false
@State showDetail: boolean = false
@State buyIdx: number = 0
@State editIdx: number = 0
@State detailIdx: number = 0
@State toastText: string = ''
showToast(msg: string) {
this.toastText = msg
setTimeout(() => {
this.toastText = ''
}, 1800)
}
@Entry装饰器标记该组件为页面入口组件——在一个ArkTS页面文件中,有且仅有一个组件可以被@Entry标记。被标记的组件会被框架自动识别为页面的根组件,负责整个页面的渲染和生命周期管理。@Entry如同港口的灯塔——它是整个港口的标志,指引着所有进港船只的航向。
RobotWar176组件声明了十一个@State状态变量——currentTab(当前标签页索引)、myMechs(我的机甲列表)、五个弹框显隐状态(showBuySheet、showEditSheet、showChallenge、showDelete、showDetail)、三个弹框数据索引(buyIdx、editIdx、detailIdx)和一个Toast文本(toastText)。这些状态变量构成了整个应用的"状态中枢"——任何状态的变化都会触发对应的UI更新。
关键概念:
@State装饰的数组类型变量在ArkTS中有一个重要的限制——直接修改数组元素属性(如this.myMechs[0].name = '新名称')不会触发UI更新。正确的做法是使用splice替换数组元素或整体重新赋值。但在本代码中,由于修改的是对象属性而非数组结构,ArkUI框架的@State对数组元素的属性变化做了观察(Observe)处理,可以触发更新。这是ArkTS状态管理的一个重要细节。
showToast方法是一个自定义的工具方法——它设置toastText状态变量来显示Toast消息,并通过setTimeout在1800毫秒后自动清空。setTimeout是JavaScript/TypeScript的全局定时器函数,在ArkTS中同样可用。当toastText被设置为非空字符串时,UI中的if (this.toastText.length > 0)条件成立,Toast组件被渲染;1800毫秒后被清空,条件变为false,Toast组件被销毁。
4.2 底部Dock导航的@Builder实现
// ---- 底部 Dock 普通项 ----
@Builder
dockItem(idx: number, icon: string, label: string) {
Column({ space: 3 }) {
Text(icon)
.fontSize(21)
.fontColor(this.currentTab === idx ? '#00B0FF' : '#78909C')
Text(label)
.fontSize(9)
.fontColor(this.currentTab === idx ? '#00B0FF' : '#78909C')
}
.layoutWeight(1)
.height(58)
.justifyContent(FlexAlign.Center)
.borderRadius(16)
.backgroundColor(this.currentTab === idx ? '#E1F5FE' : '#FFFFFF00')
.margin({ left: 3, right: 3 })
.onClick(() => {
this.currentTab = idx
})
}
// ---- 底部 Dock 悬浮主按钮 ----
@Builder
dockFloat(idx: number, icon: string, label: string) {
Column({ space: 2 }) {
Column() {
Text(icon)
.fontSize(26)
.fontColor('#FFFFFF')
}
.width(54)
.height(54)
.borderRadius(27)
.linearGradient({ angle: 135, colors: [['#00B0FF', 0.0], ['#01579B', 1.0]] })
.shadow({ radius: 14, color: '#6600B0FF', offsetX: 0, offsetY: 5 })
.justifyContent(FlexAlign.Center)
.offset({ y: -14 })
.onClick(() => {
this.currentTab = idx
})
Text(label)
.fontSize(9)
.fontColor(this.currentTab === idx ? '#00B0FF' : '#78909C')
.offset({ y: -8 })
}
}
.layoutWeight(1.2)
.height(58)
.justifyContent(FlexAlign.End)
.onClick(() => {
this.currentTab = idx
})
}
@Builder装饰器用于定义可复用的UI构建函数。与@Component不同,@Builder函数不是独立组件,而是组件内部的UI片段——它不能拥有自己的状态和生命周期,但可以访问所在组件的this上下文。dockItem和dockFloat两个Builder函数分别构建了Dock栏的普通项和悬浮主按钮。
关键概念:
@Builder与@Component的核心区别在于——@Builder定义的是UI片段(Fragment),没有独立的this和状态管理;@Component定义的是完整组件,拥有独立的成员变量和状态。@Builder适合提取重复的UI结构以提高代码复用性,而@Component适合封装独立的功能模块。
.offset({ y: -14 })方法设置了视觉偏移——offset是ArkUI的布局属性方法,通过x和y参数设置元素在视觉上的偏移量,但不影响其他元素的布局位置(即不影响兄弟元素的位置计算)。悬浮主按钮通过offset({ y: -14 })向上偏移14vp,营造出从Dock栏"浮起"的视觉效果。#FFFFFF00是8位十六进制颜色——前6位FFFFFF是RGB颜色(白色),后2位00是Alpha透明度通道(0%即完全透明),用于实现未选中状态的透明背景。
4.3 弹框Builder与bindSheet/bindContentCover
@Builder
buySheetBuilder() {
PartsSheet176({
part: PARTS176[this.buyIdx],
onClose: () => {
this.showBuySheet = false
},
onOk: (spec: number, qty: number, invoice: boolean) => {
this.showBuySheet = false
this.showToast('已下单 ' + PARTS176[this.buyIdx].name + ' ×' + qty)
}
})
}
@Builder
challengeBuilder() {
ChallengeDialog176({
onClose: () => {
this.showChallenge = false
},
onOk: (oppIdx: number, arenaIdx: number, bet: number, video: boolean) => {
this.showChallenge = false
this.showToast('战书已发送:' + OPPONENTS176[oppIdx].name)
}
})
}
主页面为每个弹框定义了对应的@Builder函数。Builder函数内部创建弹框组件实例,并通过参数传递数据和回调。PartsSheet176组件接收part(零件数据)、onClose(关闭回调)和onOk(确认回调,携带规格、数量、发票三个参数)。ChallengeDialog176接收onClose和onOk(携带对手、场地、押注、录像四个参数)。
4.4 主页面build方法与弹框绑定
build() {
Column() {
// ===== 头部:钢铁顶栏 =====
Column({ space: 0 }) {
Row({ space: 10 }) {
Column() {
Text('⚔️')
.fontSize(20)
}
.width(36)
.height(36)
.borderRadius(12)
.backgroundColor('#3300B0FF')
.justifyContent(FlexAlign.Center)
Column() {
Text('多多机器人格斗')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🔍')
.fontSize(17)
.padding(5)
Column() {
Text('🔔')
.fontSize(17)
Text('')
.width(7)
.height(7)
.borderRadius(4)
.backgroundColor('#FF3D00')
.offset({ x: 8, y: -16 })
}
}
.width('100%')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
// 搜索条
Row({ space: 8 }) {
Text('🔎')
.fontSize(13)
Text('搜机甲 / 零件 / 对手')
.fontSize(12)
.fontColor('#80D8FF')
Text('')
.layoutWeight(1)
Text('热门:竖转鼓')
.fontSize(10)
.fontColor('#FFB300')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(9)
.backgroundColor('#26323866')
}
.width('100%')
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.margin({ left: 14, right: 14, bottom: 10 })
.borderRadius(18)
.backgroundColor('#26323899')
}
.width('100%')
.linearGradient({ angle: 135, colors: [['#37474F', 0.0], ['#01579B', 1.0]] })
// ===== Toast =====
if (this.toastText.length > 0) {
Text(this.toastText)
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.borderRadius(16)
.backgroundColor('#37474FEE')
.margin({ top: 8 })
.shadow({ radius: 8, color: '#33374F55', offsetX: 0, offsetY: 2 })
}
// ===== 内容区 =====
Column() {
if (this.currentTab === 0) {
MechTab176({
mechs: this.myMechs,
onDetail: (i: number) => {
this.detailIdx = i
this.showDetail = true
},
onDelete: (i: number) => {
this.editIdx = i
this.showDelete = true
},
onEdit: (i: number) => {
this.editIdx = i
this.showEditSheet = true
}
})
} else if (this.currentTab === 1) {
BuildTab176()
} else if (this.currentTab === 2) {
MarketTab176({
onBuy: (i: number) => {
this.buyIdx = i
this.showBuySheet = true
}
})
} else if (this.currentTab === 3) {
BattleTab176({
onChallenge: () => {
this.showChallenge = true
}
})
} else if (this.currentTab === 4) {
TourTab176()
} else {
MineTab176()
}
}
.layoutWeight(1)
.width('100%')
// ===== 底部悬浮 Dock =====
Row({ space: 0 }) {
this.dockItem(0, '🤖', '机甲')
this.dockItem(1, '🔧', '装配')
this.dockItem(2, '🏪', '市集')
this.dockFloat(3, '⚔️', '对战')
this.dockItem(4, '🏆', '赛事')
this.dockItem(5, '👤', '我的')
}
.width('100%')
.height(62)
.backgroundColor('#FFFFFF')
.shadow({ radius: 12, color: '#26374F22', offsetX: 0, offsetY: -4 })
}
.width('100%')
.height('100%')
.backgroundColor('#F4F6F8')
.bindSheet($$this.showBuySheet, this.buySheetBuilder(), {
height: 520,
dragBar: false,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindSheet($$this.showEditSheet, this.editSheetBuilder(), {
height: 620,
dragBar: false,
showClose: false,
backgroundColor: '#FFFFFF'
})
.bindContentCover($$this.showChallenge, this.challengeBuilder(), {
})
.bindContentCover($$this.showDelete, this.deleteBuilder(), {
})
.bindContentCover($$this.showDetail, this.detailBuilder(), {
})
}
主页面build()方法是整个应用的"指挥中枢"。它由四个主要区块组成:顶部钢铁顶栏(含搜索条)、Toast提示区、内容区和底部悬浮Dock栏。内容区通过if/else if/else条件渲染,根据currentTab的值切换显示对应的Tab组件。每个Tab组件在创建时通过参数传入数据和回调函数——MechTab176接收机甲列表和三个回调(查看详情、删除、编辑),MarketTab176接收购买回调,BattleTab176接收挑战回调。
$$this.showBuySheet是$$双向绑定语法——$$是ArkTS的状态绑定符号,用于将@State变量与组件的属性进行双向绑定。在bindSheet和bindContentCover方法中,$$绑定的布尔变量控制弹框的显示和隐藏:当变量为true时弹出,为false时关闭。弹框内部可以通过回调函数修改这些变量来关闭弹框(如this.showBuySheet = false)。
关键概念:
$$(双向绑定)与普通参数传递的区别——普通参数传递(如this.mechs传入MechTab176)是单向的,子组件无法直接修改父组件的状态;而$$绑定的变量是双向的,框架可以同时修改绑定双方的值。在bindSheet场景中,当用户下滑关闭抽屉时,框架自动将绑定的状态变量设置为false,无需开发者手动处理关闭逻辑。
bindSheet的第三个参数是配置对象——height指定抽屉高度,dragBar控制是否显示拖拽条,showClose控制是否显示关闭按钮,backgroundColor设置背景色。bindContentCover的配置对象为空对象{},表示使用默认配置。
航线流程图
技术对比表:ArkTS关键组件与装饰器一览
| 序号 | 装饰器/组件 | 中文名称 | 作用描述 | 使用场景 | 航海比喻 |
|---|---|---|---|---|---|
| 1 | @Entry | 入口装饰器 | 标记组件为页面入口 | 每个页面有且仅有一个入口组件 | 港口灯塔 |
| 2 | @Component | 组件装饰器 | 声明自定义组件 | 可复用的UI功能模块 | 独立船只 |
| 3 | @State | 状态装饰器 | 管理组件内部响应式状态 | 需要触发UI更新的数据变量 | 潮汐表 |
| 4 | @Builder | 构建函数装饰器 | 定义可复用UI片段 | 提取重复UI结构 | 预制构件图纸 |
| 5 | @Prop | 属性装饰器 | 父向子单向传值 | 子组件接收只读数据 | 货物清单传递 |
| 6 | ForEach | 循环渲染组件 | 根据数组生成UI列表 | 数据列表展示 | 海浪重复冲刷 |
| 7 | if/else | 条件渲染 | 根据条件显示不同UI | 状态切换显示内容 | 避险航线选择 |
| 8 | Scroll | 滚动容器 | 内容超出时提供滚动 | 长内容页面 | 瞭望台 |
| 9 | Column | 垂直布局 | 子元素垂直排列 | 纵向列表布局 | 桅杆纵向排列 |
| 10 | Row | 水平布局 | 子元素水平排列 | 横向并排布局 | 甲板并排水手 |
| 11 | Flex | 弹性布局 | 支持换行的弹性排列 | 商品网格、标签流 | 可变货舱隔板 |
| 12 | Grid | 网格布局 | 规则行列排列 | 规整网格展示 | 集装箱甲板 |
| 13 | Text | 文本组件 | 显示文字内容 | 标题、正文、标签 | 航海日志文字 |
| 14 | TextInput | 输入组件 | 接收用户文本输入 | 表单输入框 | 航海口令输入 |
| 15 | Toggle | 开关组件 | 布尔值切换交互 | 开关选项 | 电路闸刀 |
| 16 | bindSheet | 抽屉绑定 | 绑定底部滑出弹框 | 购买、编辑等表单 | 底舱滑出补给 |
| 17 | bindContentCover | 蒙层绑定 | 绑定居中覆盖弹框 | 确认、详情等对话框 | 信号旗覆盖 |
| 18 | linearGradient | 线性渐变 | 设置渐变背景色 | 卡片、头部美化 | 海面色彩过渡 |
| 19 | ForEach键值函数 | 渲染键值 | 优化列表渲染性能 | 数据增删改场景 | 货物编号追踪 |
| 20 | aboutToAppear | 生命周期 | 组件创建后初始化 | 数据预加载 | 出航前检查 |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
// ============================================================================
// 176.ets 多多机器人格斗
// 场景:机器人格斗爱好者社区 + 机甲零件商城
// 配色:钢铁灰 #37474F × 电光蓝 #00B0FF × 警示红 #FF3D00
// 布局:底部悬浮主按钮 Dock 导航(中间发光圆形按钮 = 对战,区别于普通底部栏)
// 弹框:购买零件(抽屉) / 装配编辑(抽屉) / 发起挑战(居中) / 删除机甲(居中) / 机甲详情(居中)
// ============================================================================
// ---------------------------------------------------------------------------
// 数据结构定义
// ---------------------------------------------------------------------------
interface WeightClass176 {
name: string
cap: string
desc: string
color: string
count: number
pct: number
}
interface Mech176 {
id: number
name: string
cls: string
weight: string
weapon: string
power: number
armor: number
speed: number
winRate: number
wins: number
losses: number
tier: string
price: number
icon: string
color: string
hot: string
}
interface StatItem176 {
name: string
pct: number
color: string
}
interface TorqueItem176 {
name: string
value: number
color: string
}
interface PriceItem176 {
label: string
value: number
color: string
}
interface Build176 {
name: string
desc: string
parts: number
price: number
hot: string
icon: string
}
interface BuildStep176 {
name: string
step: string
owner: string
pct: number
status: string
}
interface Part176 {
name: string
type: string
brand: string
price: number
torque: number
stock: number
icon: string
hot: string
}
interface Deal176 {
name: string
off: string
price: number
old: number
tag: string
}
interface Seller176 {
name: string
score: number
orders: number
tag: string
}
interface Opponent176 {
name: string
rating: number
mech: string
style: string
online: boolean
icon: string
}
interface Battle176 {
date: string
opponent: string
score: string
arena: string
mech: string
win: boolean
}
interface Arena176 {
name: string
city: string
size: string
fee: string
busy: string
hot: string
}
interface Tournament176 {
name: string
date: string
level: string
players: number
prize: string
status: string
}
interface Rank176 {
pos: number
name: string
points: number
wins: number
trend: string
color: string
}
interface Order176 {
id: string
name: string
price: number
qty: number
status: string
date: string
}
interface Badge176 {
name: string
icon: string
got: boolean
}
interface Review176 {
user: string
rating: number
text: string
date: string
}
// ---------------------------------------------------------------------------
// 静态数据
// ---------------------------------------------------------------------------
const WEIGHTS176: WeightClass176[] = [
{ name: '蚂蚁级', cap: '150g', desc: '入门首选 · 桌面竞技', color: '#00B0FF', count: 342, pct: 86 },
{ name: '甲虫级', cap: '1.36kg', desc: '人气最高 · 火花四溅', color: '#FF3D00', count: 286, pct: 72 },
{ name: '游民级', cap: '3kg', desc: '硬核玩家 · 大杀器', color: '#FFB300', count: 158, pct: 45 },
{ name: '羽量级', cap: '13.6kg', desc: '职业赛场 · 钢铁猛兽', color: '#546E7A', count: 64, pct: 21 }
]
const CLASS_BARS176: StatItem176[] = [
{ name: '蚂蚁级 热度', pct: 86, color: '#00B0FF' },
{ name: '甲虫级 热度', pct: 72, color: '#FF3D00' },
{ name: '游民级 热度', pct: 45, color: '#FFB300' },
{ name: '羽量级 热度', pct: 21, color: '#546E7A' }
]
const MECHS176: Mech176[] = [
{ id: 1, name: '雷霆战锤', cls: '甲虫级', weight: '1.35kg', weapon: '竖转鼓 12000rpm', power: 88, armor: 76, speed: 64, winRate: 71, wins: 32, losses: 13, tier: 'S', price: 1680, icon: '🔨', color: '#FF3D00', hot: '本站胜率王' },
{ id: 2, name: '蓝电螳螂', cls: '甲虫级', weight: '1.32kg', weapon: '横转刀 9800rpm', power: 82, armor: 70, speed: 78, winRate: 66, wins: 28, losses: 14, tier: 'A', price: 1550, icon: '🦗', color: '#00B0FF', hot: '速攻流代表' },
{ id: 3, name: '钢甲龟', cls: '甲虫级', weight: '1.36kg', weapon: '铲推 + 夹持', power: 55, armor: 94, speed: 42, winRate: 58, wins: 22, losses: 16, tier: 'A', price: 1320, icon: '🐢', color: '#546E7A', hot: '最硬的盾' },
{ id: 4, name: '小钢炮', cls: '蚂蚁级', weight: '148g', weapon: '竖转盘 15000rpm', power: 74, armor: 52, speed: 82, winRate: 63, wins: 19, losses: 11, tier: 'A', price: 880, icon: '💥', color: '#FFB300', hot: '桌面之王' },
{ id: 5, name: '闪电苍蝇', cls: '蚂蚁级', weight: '142g', weapon: '楔子 + 电锯', power: 48, armor: 58, speed: 91, winRate: 55, wins: 16, losses: 13, tier: 'B', price: 760, icon: '🪰', color: '#78909C', hot: '极限速度' },
{ id: 6, name: '粉碎者', cls: '游民级', weight: '2.98kg', weapon: '大锤冲程 40J', power: 92, armor: 85, speed: 38, winRate: 69, wins: 11, losses: 5, tier: 'S', price: 2400, icon: '⚒️', color: '#37474F', hot: '一锤定音' },
{ id: 7, name: '量子幽灵', cls: '游民级', weight: '2.85kg', weapon: '全向轮 + 弹射', power: 62, armor: 72, speed: 74, winRate: 52, wins: 9, losses: 8, tier: 'B', price: 1980, icon: '👻', color: '#00B0FF', hot: '赛场滑步' },
{ id: 8, name: '暴风铁拳', cls: '羽量级', weight: '13.2kg', weapon: '气动拳 60J', power: 95, armor: 88, speed: 55, winRate: 74, wins: 14, losses: 5, tier: 'S', price: 5600, icon: '🥊', color: '#FF3D00', hot: '重装核弹' }
]
const TORQUES176: TorqueItem176[] = [
{ name: '竖转鼓 M1200', value: 92, color: '#FF3D00' },
{ name: '横转刀 D800', value: 78, color: '#00B0FF' },
{ name: '竖转盘 S450', value: 65, color: '#FFB300' },
{ name: '大锤冲程 H40', value: 52, color: '#546E7A' },
{ name: '气动拳 P60', value: 88, color: '#37474F' }
]
const PART_PRICES176: PriceItem176[] = [
{ label: '武器', value: 420, color: '#FF3D00' },
{ label: '驱动', value: 260, color: '#00B0FF' },
{ label: '装甲', value: 180, color: '#FFB300' },
{ label: '电调', value: 210, color: '#546E7A' },
{ label: '遥控', value: 330, color: '#00B0FF' },
{ label: '电池', value: 150, color: '#FF3D00' }
]
const BUILDS176: Build176[] = [
{ name: '新手入门套装', desc: '蚂蚁级 · 一周可完成', parts: 12, price: 399, hot: '销量王', icon: '📦' },
{ name: '甲虫级竞技方案', desc: '含备用武器 + 遥控', parts: 28, price: 1680, hot: '热门', icon: '🛠️' },
{ name: '游民级重装方案', desc: '大锤核心 · 电池×2', parts: 35, price: 3280, hot: '旗舰', icon: '⚙️' }
]
const BUILD_STEPS176: BuildStep176[] = [
{ name: '底盘焊接与校平', step: '第 1 步', owner: '阿钢师傅', pct: 100, status: '已完成' },
{ name: '驱动电机安装', step: '第 2 步', owner: '阿钢师傅', pct: 100, status: '已完成' },
{ name: '武器系统装配', step: '第 3 步', owner: '本人', pct: 72, status: '进行中' },
{ name: '装甲板贴合', step: '第 4 步', owner: '本人', pct: 35, status: '进行中' },
{ name: '整机调校与称重', step: '第 5 步', owner: '待定', pct: 0, status: '未开始' }
]
const PARTS176: Part176[] = [
{ name: '竖转鼓 M1200', type: '武器', brand: 'SteelCore', price: 420, torque: 92, stock: 36, icon: '🌀', hot: '爆款' },
{ name: '无刷电机 2300KV', type: '驱动', brand: 'TorqueX', price: 158, torque: 76, stock: 120, icon: '⚙️', hot: '热卖' },
{ name: '钛合金装甲板', type: '装甲', brand: 'IronSkin', price: 286, torque: 88, stock: 18, icon: '🛡️', hot: '新品' },
{ name: '电调 60A 双向', type: '电调', brand: 'FluxLine', price: 210, torque: 70, stock: 64, icon: '🔌', hot: '热卖' },
{ name: '横转刀 D800', type: '武器', brand: 'SteelCore', price: 368, torque: 78, stock: 22, icon: '🗡️', hot: '补货' },
{ name: '2.4G 遥控器 6通道', type: '遥控', brand: 'PilotPro', price: 330, torque: 66, stock: 41, icon: '🎮', hot: '热卖' },
{ name: '锂电 3S 450mAh', type: '电池', brand: 'VoltEdge', price: 96, torque: 62, stock: 230, icon: '🔋', hot: '爆款' },
{ name: '全向轮 ×4', type: '驱动', brand: 'GlideMax', price: 128, torque: 58, stock: 88, icon: '🛞', hot: '常规' },
{ name: '气动拳模组 P60', type: '武器', brand: 'PunchLab', price: 520, torque: 88, stock: 9, icon: '🥊', hot: '稀缺' },
{ name: '称重器 0.1g 精度', type: '工具', brand: 'FineScale', price: 118, torque: 50, stock: 76, icon: '⚖️', hot: '常规' }
]
const DEALS176: Deal176[] = [
{ name: '甲虫级武器组合包', off: '7.5折', price: 688, old: 918, tag: '限时' },
{ name: '备用电池 ×3 特惠', off: '8折', price: 230, old: 288, tag: '秒杀' },
{ name: '碳纤维底板 1.2mm', off: '6.8折', price: 149, old: 219, tag: '清仓' },
{ name: '维修工具全家桶', off: '8.5折', price: 168, old: 198, tag: '热销' },
{ name: '训练用泡沫靶车', off: '9折', price: 89, old: 99, tag: '上新' }
]
const SELLERS176: Seller176[] = [
{ name: '钢核工坊旗舰店', score: 98, orders: 8620, tag: '官方' },
{ name: '极客机甲屋', score: 96, orders: 5310, tag: '老牌' },
{ name: '火花实验室', score: 93, orders: 3140, tag: '口碑' },
{ name: '螺丝与钢铁', score: 90, orders: 2080, tag: '新锐' },
{ name: '格斗军火库', score: 87, orders: 1460, tag: '实惠' }
]
const OPPONENTS176: Opponent176[] = [
{ name: '铁拳阿豪', rating: 1912, mech: '碎星者', style: '重击流', online: true, icon: '🥷' },
{ name: '螺丝刀小姐', rating: 1876, mech: '旋风钻', style: '速攻流', online: true, icon: '👩🔧' },
{ name: '老焊枪', rating: 1795, mech: '焊武帝', style: '防御流', online: false, icon: '👨🏭' },
{ name: '疯狂齿轮', rating: 1754, mech: '齿轮暴龙', style: '均衡流', online: true, icon: '🧑🎤' },
{ name: '绝缘子', rating: 1688, mech: '绝缘体', style: '游走流', online: false, icon: '🧑🔬' },
{ name: '小钢豆', rating: 1632, mech: '豆豆锤', style: '爆发流', online: true, icon: '🧒' }
]
const BATTLES176: Battle176[] = [
{ date: '08-24 20:30', opponent: '螺丝刀小姐', score: 'KO 1:47', arena: '火花竞技场 3 号笼', mech: '雷霆战锤', win: true },
{ date: '08-23 19:05', opponent: '老焊枪', score: '判定 2:1', arena: '钢铁花园 A 场', mech: '雷霆战锤', win: true },
{ date: '08-22 21:10', opponent: '铁拳阿豪', score: '被 KO 0:58', arena: '火花竞技场 1 号笼', mech: '蓝电螳螂', win: false },
{ date: '08-18 20:00', opponent: '疯狂齿轮', score: 'KO 2:12', arena: '城东格斗馆', mech: '雷霆战锤', win: true },
{ date: '08-17 15:40', opponent: '绝缘子', score: '判定 1:2', arena: '钢铁花园 B 场', mech: '小钢炮', win: false },
{ date: '08-15 19:30', opponent: '小钢豆', score: 'KO 0:44', arena: '火花竞技场 3 号笼', mech: '小钢炮', win: true },
{ date: '08-10 20:15', opponent: '螺丝刀小姐', score: '被 KO 1:23', arena: '城东格斗馆', mech: '钢甲龟', win: false },
{ date: '08-08 21:00', opponent: '疯狂齿轮', score: 'KO 1:58', arena: '钢铁花园 A 场', mech: '雷霆战锤', win: true }
]
const ARENAS176: Arena176[] = [
{ name: '火花竞技场', city: '上海 · 杨浦', size: '3×3m 聚碳酸笼', fee: '¥60/时', busy: '爆满', hot: '今晚余 0 场' },
{ name: '钢铁花园', city: '北京 · 朝阳', size: '2.5×2.5m 标准笼', fee: '¥45/时', busy: '较多', hot: '今晚余 3 场' },
{ name: '城东格斗馆', city: '广州 · 天河', size: '4×4m 大笼', fee: '¥80/时', busy: '适中', hot: '今晚余 6 场' },
{ name: '螺丝湾工坊', city: '深圳 · 南山', size: '2×2m 桌面笼', fee: '¥30/时', busy: '空闲', hot: '今晚余 12 场' },
{ name: '北境钢窟', city: '成都 · 金牛', size: '3×3m 标准笼', fee: '¥50/时', busy: '较多', hot: '今晚余 2 场' }
]
const TOURNAMENTS176: Tournament176[] = [
{ name: '多多钢甲秋季公开赛', date: '09-20 ~ 09-21', level: '甲虫级', players: 96, prize: '¥20000', status: '报名中' },
{ name: '蚂蚁级新手慈善赛', date: '09-06', level: '蚂蚁级', players: 64, prize: '¥3000', status: '报名中' },
{ name: '城市钢拳巡回 · 站三', date: '08-30', level: '甲虫级', players: 48, prize: '¥8000', status: '进行中' },
{ name: '羽量级王者之夜', date: '10-02', level: '羽量级', players: 24, prize: '¥50000', status: '预告' },
{ name: '高校机甲联赛决赛', date: '08-16', level: '甲虫级', players: 32, prize: '¥6000', status: '已结束' },
{ name: '游民级硬核邀请赛', date: '07-28', level: '游民级', players: 16, prize: '¥12000', status: '已结束' }
]
interface PrizeItem176 {
label: string
value: number
color: string
}
const RANKS176: Rank176[] = [
{ pos: 1, name: '铁拳阿豪', points: 2860, wins: 41, trend: '↑2', color: '#FF3D00' },
{ pos: 2, name: '螺丝刀小姐', points: 2745, wins: 38, trend: '↓1', color: '#FF3D00' },
{ pos: 3, name: '疯狂齿轮', points: 2512, wins: 35, trend: '↑1', color: '#FFB300' },
{ pos: 4, name: '钢之心 · 我', points: 2330, wins: 31, trend: '↑3', color: '#00B0FF' },
{ pos: 5, name: '老焊枪', points: 2288, wins: 30, trend: '↓2', color: '#546E7A' },
{ pos: 6, name: '绝缘子', points: 2104, wins: 27, trend: '—', color: '#546E7A' },
{ pos: 7, name: '小钢豆', points: 1956, wins: 24, trend: '↑1', color: '#546E7A' },
{ pos: 8, name: '齿轮暴龙', points: 1843, wins: 22, trend: '↓1', color: '#78909C' }
]
const PRIZES176: PrizeItem176[] = [
{ label: '冠军', value: 100, color: '#FF3D00' },
{ label: '亚军', value: 50, color: '#00B0FF' },
{ label: '季军', value: 25, color: '#FFB300' },
{ label: '四强', value: 12, color: '#546E7A' },
{ label: '八强', value: 6, color: '#78909C' }
]
const ORDERS176: Order176[] = [
{ id: 'NO.88231071', name: '竖转鼓 M1200 ×1', price: 420, qty: 1, status: '已签收', date: '08-20' },
{ id: 'NO.88230992', name: '锂电 3S 450mAh ×3', price: 288, qty: 3, status: '已发货', date: '08-22' },
{ id: 'NO.88229754', name: '钛合金装甲板 ×1', price: 286, qty: 1, status: '已发货', date: '08-23' },
{ id: 'NO.88228610', name: '全向轮 ×4 套装', price: 128, qty: 1, status: '待发货', date: '08-24' },
{ id: 'NO.88227005', name: '甲虫级武器组合包', price: 688, qty: 1, status: '已签收', date: '08-15' },
{ id: 'NO.88226138', name: '维修工具全家桶', price: 168, qty: 1, status: '退款', date: '08-11' }
]
const BADGES176: Badge176[] = [
{ name: '首胜', icon: '🥇', got: true },
{ name: '十连胜', icon: '🔥', got: true },
{ name: 'KO 达人', icon: '💥', got: true },
{ name: '装甲大师', icon: '🛡️', got: false },
{ name: '装配 3 台', icon: '🔧', got: true },
{ name: '赛场常客', icon: '🏟️', got: true },
{ name: '导师', icon: '🎓', got: false },
{ name: '冠军', icon: '👑', got: false }
]
const DETAIL_ROWS176: string[][] = [
['武器系统', '竖转鼓 · 12000rpm · 92N·cm'],
['驱动方案', '4×2300KV 无刷 · 全向轮'],
['装甲材质', '钛合金 1.2mm + 前铲 Hardox'],
['电池规格', '3S 450mAh 45C ×2'],
['称重', '1.35kg / 上限 1.36kg']
]
const SCENES176: string[][] = [
['速攻克制', '对上防御流机甲,先手撞铲掀翻'],
['武器对拼', '转鼓对横刀,转速占优时可正面交换'],
['劣势对局', '遇到气动拳保持距离,等冲程空档'],
['维护提示', '每 3 场检查轴承与刀齿磨损']
]
const REVIEWS176: Review176[] = [
{ user: '铁拳阿豪', rating: 5, text: '和它打过一次,转鼓转速是真的猛,被掀了。', date: '08-22' },
{ user: '螺丝刀小姐', rating: 5, text: '做工非常扎实,装甲缝隙处理得很细。', date: '08-19' },
{ user: '老焊枪', rating: 4, text: '速度稍慢但稳,判定局很难缠。', date: '08-14' },
{ user: '小钢豆', rating: 5, text: '外观帅到犯规,涂装党狂喜!', date: '08-09' }
]
const WEAPONS176: string[] = ['竖转鼓', '横转刀', '气动拳', '弹射板']
const DRIVES176: string[] = ['四轮驱动', '全向轮', '双履带']
const DRIVE_DESCS176: string[] = ['直线加速强 · 转向一般', '横移灵活 · 适合游走', '越障强 · 速度较慢']
const SPECS176: string[] = ['标准版', '轻量化版', '加强版']
const MINE_ICONS176: string[] = ['📍', '🏛️', '🛡️', '💬', 'ℹ️']
// ---------------------------------------------------------------------------
// 全局工具函数(UI 容器内禁止变量声明,全部抽到这里)
// ---------------------------------------------------------------------------
function rateC176(r: number): string {
if (r >= 70) { return '#FF3D00' }
if (r >= 60) { return '#00B0FF' }
if (r >= 50) { return '#FFB300' }
return '#78909C'
}
function tierC176(t: string): string {
if (t === 'S') { return '#FF3D00' }
if (t === 'A') { return '#00B0FF' }
if (t === 'B') { return '#FFB300' }
return '#78909C'
}
function tierBg176(t: string): string {
if (t === 'S') { return '#FBE9E7' }
if (t === 'A') { return '#E1F5FE' }
if (t === 'B') { return '#FFF8E1' }
return '#ECEFF1'
}
function torqueW176(v: number): string {
return v + '%'
}
function priceH176(v: number): string {
if (v >= 400) { return '100%' }
if (v >= 330) { return '80%' }
if (v >= 250) { return '62%' }
if (v >= 200) { return '50%' }
if (v >= 170) { return '40%' }
return '32%'
}
function statusC176(s: string): string {
if (s === '报名中') { return '#00B0FF' }
if (s === '进行中') { return '#FF3D00' }
if (s === '预告') { return '#FFB300' }
return '#78909C'
}
function orderC176(s: string): string {
if (s === '已签收') { return '#00B0FF' }
if (s === '已发货') { return '#FFB300' }
if (s === '待发货') { return '#FF3D00' }
return '#78909C'
}
function stepC176(s: string): string {
if (s === '已完成') { return '#00B0FF' }
if (s === '进行中') { return '#FF3D00' }
return '#78909C'
}
function busyC176(b: string): string {
if (b === '爆满') { return '#FF3D00' }
if (b === '较多') { return '#FFB300' }
if (b === '适中') { return '#00B0FF' }
return '#78909C'
}
function stepBg176(s: string): string {
if (s === '已完成') { return '#E1F5FE' }
if (s === '进行中') { return '#FBE9E7' }
return '#ECEFF1'
}
function starStr176(r: number): string {
if (r >= 5) { return '★★★★★' }
if (r >= 4) { return '★★★★☆' }
if (r >= 3) { return '★★★☆☆' }
if (r >= 2) { return '★★☆☆☆' }
return '★☆☆☆☆'
}
// ---------------------------------------------------------------------------
// Tab 1:机甲
// ---------------------------------------------------------------------------
@Component
struct MechTab176 {
mechs: Mech176[] = MECHS176
onDetail: (i: number) => void = (i: number) => {}
onDelete: (i: number) => void = (i: number) => {}
onEdit: (i: number) => void = (i: number) => {}
build() {
Scroll() {
Column({ space: 12 }) {
// 数据速览
Row({ space: 10 }) {
Column({ space: 4 }) {
Text('8')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FF3D00')
Text('我的机甲')
.fontSize(11)
.fontColor('#78909C')
}
.layoutWeight(1)
.height(74)
.justifyContent(FlexAlign.Center)
.backgroundColor('#FFFFFF')
.borderRadius(14)
Column({ space: 4 }) {
Text('126')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#00B0FF')
Text('总场次')
.fontSize(11)
.fontColor('#78909C')
}
.layoutWeight(1)
.height(74)
.justifyContent(FlexAlign.Center)
.backgroundColor('#FFFFFF')
.borderRadius(14)
Column({ space: 4 }) {
Text('61%')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#37474F')
Text('综合胜率')
.fontSize(11)
.fontColor('#78909C')
}
.layoutWeight(1)
.height(74)
.justifyContent(FlexAlign.Center)
.backgroundColor('#FFFFFF')
.borderRadius(14)
}
.width('100%')
// 重量级横滑卡
Row() {
Text('重量级划分')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('共 850 台在册')
.fontSize(11)
.fontColor('#78909C')
}
.width('100%')
Scroll() {
Row({ space: 10 }) {
ForEach(WEIGHTS176, (w: WeightClass176) => {
Column({ space: 6 }) {
Row() {
Text(w.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('')
.layoutWeight(1)
Text(w.cap)
.fontSize(11)
.fontColor('#FFFFFF')
.opacity(0.85)
}
.width('100%')
Text(w.desc)
.fontSize(11)
.fontColor('#FFFFFF')
.opacity(0.85)
Text('')
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor('#33FFFFFF')
Column({ space: 2 }) {
Row() {
Text('在册 ' + w.count + ' 台')
.fontSize(10)
.fontColor('#FFFFFF')
.opacity(0.85)
Text('')
.layoutWeight(1)
Text(w.pct + '%')
.fontSize(10)
.fontColor('#FFFFFF')
}
.width('100%')
Row() {
Text('')
.width(w.pct + '%')
.height(5)
.borderRadius(3)
.backgroundColor('#FFFFFF')
Text('')
.layoutWeight(1)
}
.width('100%')
}
.width('100%')
}
.width(180)
.padding(12)
.borderRadius(16)
.linearGradient({ angle: 135, colors: [[w.color, 0.0], ['#37474F', 1.0]] })
.shadow({ radius: 8, color: '#26374F', offsetX: 0, offsetY: 3 })
})
}
.padding({ left: 2, right: 2 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
// 重量级热度横条图
Column({ space: 10 }) {
Row() {
Text('重量级热度榜')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('按在册占比')
.fontSize(10)
.fontColor('#78909C')
}
.width('100%')
ForEach(CLASS_BARS176, (b: StatItem176) => {
Row({ space: 8 }) {
Text(b.name)
.fontSize(11)
.fontColor('#546E7A')
.width(86)
Row() {
Text('')
.width(torqueW176(b.pct))
.height(10)
.borderRadius(5)
.backgroundColor(b.color)
Text('')
.layoutWeight(1)
}
.layoutWeight(1)
.height(10)
.borderRadius(5)
.backgroundColor('#ECEFF1')
Text(b.pct + '%')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(b.color)
.width(38)
.textAlign(TextAlign.End)
}
.width('100%')
})
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(16)
// 我的战动机甲列表
Row() {
Text('我的战动机甲')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('点击查看档案')
.fontSize(11)
.fontColor('#78909C')
}
.width('100%')
ForEach(this.mechs, (m: Mech176, idx: number) => {
Column({ space: 10 }) {
Row({ space: 10 }) {
Column() {
Text(m.icon)
.fontSize(26)
}
.width(52)
.height(52)
.borderRadius(14)
.backgroundColor(m.color + '1A')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(m.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text(m.tier)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(tierC176(m.tier))
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(6)
.backgroundColor(tierBg176(m.tier))
}
Text(m.weapon + ' · ' + m.weight)
.fontSize(11)
.fontColor('#78909C')
Row({ space: 6 }) {
Text(m.cls)
.fontSize(10)
.fontColor('#00B0FF')
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(6)
.backgroundColor('#E1F5FE')
Text(m.hot)
.fontSize(10)
.fontColor('#FF3D00')
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(6)
.backgroundColor('#FBE9E7')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('删除')
.fontSize(11)
.fontColor('#FF3D00')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor('#FBE9E7')
.onClick(() => {
this.onDelete(idx)
})
Text('装配')
.fontSize(11)
.fontColor('#00B0FF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor('#E1F5FE')
.margin({ top: 6 })
.onClick(() => {
this.onEdit(idx)
})
}
}
.width('100%')
// 胜率条
Column({ space: 4 }) {
Row() {
Text('胜率')
.fontSize(10)
.fontColor('#78909C')
Text('')
.layoutWeight(1)
Text(m.winRate + '% · ' + m.wins + '胜 ' + m.losses + '负')
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(rateC176(m.winRate))
}
.width('100%')
Row() {
Text('')
.width(m.winRate + '%')
.height(7)
.borderRadius(4)
.backgroundColor(rateC176(m.winRate))
Text('')
.layoutWeight(1)
}
.width('100%')
.height(7)
.borderRadius(4)
.backgroundColor('#ECEFF1')
}
.width('100%')
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.onClick(() => {
this.onDetail(idx)
})
})
}
.padding({ left: 14, right: 14, top: 12, bottom: 20 })
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
.backgroundColor('#F4F6F8')
.edgeEffect(EdgeEffect.Spring)
}
}
// ---------------------------------------------------------------------------
// Tab 2:装配
// ---------------------------------------------------------------------------
@Component
struct BuildTab176 {
build() {
Scroll() {
Column({ space: 12 }) {
// 当前装配概览
Column({ space: 10 }) {
Row({ space: 10 }) {
Column() {
Text('🛠️')
.fontSize(30)
}
.width(60)
.height(60)
.borderRadius(16)
.backgroundColor('#37474F')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text('雷霆战锤 · 三代改')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('甲虫级 · 武器系统装配中')
.fontSize(11)
.fontColor('#80D8FF')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('62%')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#00B0FF')
Text('总进度')
.fontSize(10)
.fontColor('#80D8FF')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Row() {
Text('')
.width('62%')
.height(9)
.borderRadius(5)
.backgroundColor('#00B0FF')
Text('')
.layoutWeight(1)
}
.width('100%')
.height(9)
.borderRadius(5)
.backgroundColor('#546E7A')
}
.width('100%')
.padding(16)
.borderRadius(18)
.linearGradient({ angle: 135, colors: [['#37474F', 0.0], ['#263238', 1.0]] })
.shadow({ radius: 10, color: '#33374F40', offsetX: 0, offsetY: 4 })
// 装配进度列表
Column({ space: 0 }) {
Row() {
Text('装配工序')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('5 道工序')
.fontSize(10)
.fontColor('#78909C')
}
.width('100%')
.padding(14)
ForEach(BUILD_STEPS176, (s: BuildStep176, idx: number) => {
Column() {
Row({ space: 10 }) {
// 步骤圆点
Column() {
Text(idx < 3 ? '✓' : (idx + 1) + '')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(idx < 3 ? '#FFFFFF' : '#78909C')
}
.width(24)
.height(24)
.borderRadius(12)
.backgroundColor(idx < 3 ? '#00B0FF' : '#ECEFF1')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row() {
Text(s.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text(s.status)
.fontSize(10)
.fontColor(stepC176(s.status))
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(6)
.backgroundColor(stepBg176(s.status))
}
.width('100%')
Row() {
Text('')
.width(s.pct + '%')
.height(5)
.borderRadius(3)
.backgroundColor(stepC176(s.status))
Text('')
.layoutWeight(1)
}
.width('100%')
.height(5)
.borderRadius(3)
.backgroundColor('#ECEFF1')
Text(s.step + ' · 负责:' + s.owner)
.fontSize(10)
.fontColor('#78909C')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ top: 10, bottom: 10 })
if (idx < BUILD_STEPS176.length - 1) {
Text('')
.width(1)
.height(12)
.backgroundColor('#ECEFF1')
.margin({ left: 12 })
}
}
.width('100%')
})
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
// 武器扭矩横条图
Column({ space: 10 }) {
Row() {
Text('武器扭矩对比')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('单位 N·cm')
.fontSize(10)
.fontColor('#78909C')
}
.width('100%')
ForEach(TORQUES176, (t: TorqueItem176) => {
Row({ space: 8 }) {
Text(t.name)
.fontSize(11)
.fontColor('#546E7A')
.width(110)
Row() {
Text('')
.width(torqueW176(t.value))
.height(12)
.borderRadius(6)
.backgroundColor(t.color)
Text('')
.layoutWeight(1)
}
.layoutWeight(1)
.height(12)
.borderRadius(6)
.backgroundColor('#ECEFF1')
Text(t.value + '')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(t.color)
.width(30)
.textAlign(TextAlign.End)
}
.width('100%')
})
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(16)
// 零件均价柱状图
Column({ space: 10 }) {
Row() {
Text('零件分类均价')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('单位 ¥')
.fontSize(10)
.fontColor('#78909C')
}
.width('100%')
Row({ space: 8 }) {
ForEach(PART_PRICES176, (p: PriceItem176) => {
Column({ space: 6 }) {
Text(p.value + '')
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(p.color)
Text('')
.width('100%')
.height(priceH176(p.value))
.borderRadius(5)
.backgroundColor(p.color)
.opacity(0.9)
Text(p.label)
.fontSize(10)
.fontColor('#78909C')
}
.layoutWeight(1)
.height(150)
.justifyContent(FlexAlign.End)
})
}
.width('100%')
.alignItems(VerticalAlign.Bottom)
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(16)
// 推荐方案
Row() {
Text('推荐装配方案')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('3 套方案')
.fontSize(10)
.fontColor('#78909C')
}
.width('100%')
ForEach(BUILDS176, (b: Build176) => {
Row({ space: 12 }) {
Column() {
Text(b.icon)
.fontSize(24)
}
.width(52)
.height(52)
.borderRadius(14)
.backgroundColor('#E1F5FE')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(b.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text(b.hot)
.fontSize(10)
.fontColor('#FF3D00')
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(6)
.backgroundColor('#FBE9E7')
}
Text(b.desc)
.fontSize(11)
.fontColor('#78909C')
Text('含 ' + b.parts + ' 种零件')
.fontSize(10)
.fontColor('#00B0FF')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('¥' + b.price)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FF3D00')
Text('查看')
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 10, right: 10, top: 3, bottom: 3 })
.borderRadius(10)
.backgroundColor('#00B0FF')
.margin({ top: 5 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(16)
})
}
.padding({ left: 14, right: 14, top: 12, bottom: 20 })
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
.backgroundColor('#F4F6F8')
.edgeEffect(EdgeEffect.Spring)
}
}
// ---------------------------------------------------------------------------
// Tab 3:市集
// ---------------------------------------------------------------------------
@Component
struct MarketTab176 {
@State cate: number = 0
onBuy: (i: number) => void = (i: number) => {}
build() {
Scroll() {
Column({ space: 12 }) {
// 分类胶囊
Row({ space: 8 }) {
ForEach(['全部', '武器', '驱动', '装甲', '电调', '电池'], (c: string, i: number) => {
Text(c)
.fontSize(12)
.fontColor(this.cate === i ? '#FFFFFF' : '#546E7A')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.cate === i ? '#37474F' : '#FFFFFF')
.onClick(() => {
this.cate = i
})
})
}
.width('100%')
// 热门零件 Grid
Row() {
Text('热门零件')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('每半小时更新库存')
.fontSize(10)
.fontColor('#78909C')
}
.width('100%')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(PARTS176, (p: Part176, idx: number) => {
Column({ space: 8 }) {
Row() {
Column() {
Text(p.icon)
.fontSize(26)
}
.width(46)
.height(46)
.borderRadius(12)
.backgroundColor('#E1F5FE')
.justifyContent(FlexAlign.Center)
Text('')
.layoutWeight(1)
Text(p.hot)
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(6)
.backgroundColor(p.hot === '稀缺' ? '#FF3D00' : (p.hot === '新品' ? '#00B0FF' : '#FFB300'))
}
.width('100%')
Text(p.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(p.brand + ' · ' + p.type)
.fontSize(11)
.fontColor('#78909C')
// 扭矩小条
Column({ space: 3 }) {
Text('性能指数 ' + p.torque)
.fontSize(10)
.fontColor('#78909C')
Row() {
Text('')
.width(p.torque + '%')
.height(5)
.borderRadius(3)
.backgroundColor('#00B0FF')
Text('')
.layoutWeight(1)
}
.width('100%')
.height(5)
.borderRadius(3)
.backgroundColor('#ECEFF1')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
Row() {
Text('¥' + p.price)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FF3D00')
Text('')
.layoutWeight(1)
Text('余 ' + p.stock)
.fontSize(10)
.fontColor(p.stock < 20 ? '#FF3D00' : '#78909C')
}
.width('100%')
Text('加入装配清单')
.fontSize(12)
.fontColor('#FFFFFF')
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 7, bottom: 7 })
.borderRadius(12)
.linearGradient({ angle: 90, colors: [['#00B0FF', 0.0], ['#01579B', 1.0]] })
.onClick(() => {
this.onBuy(idx)
})
}
.width('48.5%')
.padding(12)
.margin({ bottom: 10 })
.backgroundColor('#FFFFFF')
.borderRadius(16)
})
}
.width('100%')
// 本周特价
Column({ space: 0 }) {
Row() {
Text('本周特价')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('⚡ 限时抢购')
.fontSize(10)
.fontColor('#FF3D00')
}
.width('100%')
.padding(14)
ForEach(DEALS176, (d: Deal176, idx: number) => {
Row({ space: 10 }) {
Column() {
Text(d.off)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width(44)
.height(44)
.borderRadius(12)
.backgroundColor(idx % 2 === 0 ? '#FF3D00' : '#00B0FF')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(d.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#263238')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(d.tag)
.fontSize(9)
.fontColor('#FF3D00')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(5)
.backgroundColor('#FBE9E7')
}
Row({ space: 6 }) {
Text('¥' + d.price)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FF3D00')
Text('¥' + d.old)
.fontSize(10)
.fontColor('#B0BEC5')
.decoration({ type: TextDecorationType.LineThrough })
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('抢')
.fontSize(12)
.fontColor('#FFFFFF')
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor('#FF3D00')
.onClick(() => {
this.onBuy(0)
})
}
.width('100%')
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
})
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
// 卖家口碑
Column({ space: 10 }) {
Row() {
Text('卖家口碑榜')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('好评率 %')
.fontSize(10)
.fontColor('#78909C')
}
.width('100%')
ForEach(SELLERS176, (s: Seller176, idx: number) => {
Row({ space: 10 }) {
Text((idx + 1) + '')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(idx < 3 ? '#FF3D00' : '#78909C')
.width(20)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(s.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#263238')
Text(s.tag)
.fontSize(9)
.fontColor('#00B0FF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(5)
.backgroundColor('#E1F5FE')
}
Row() {
Text('')
.width(s.score + '%')
.height(6)
.borderRadius(3)
.backgroundColor(idx < 3 ? '#FF3D00' : '#00B0FF')
Text('')
.layoutWeight(1)
}
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor('#ECEFF1')
Text('成交 ' + s.orders + ' 单')
.fontSize(10)
.fontColor('#78909C')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(s.score + '%')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(idx < 3 ? '#FF3D00' : '#00B0FF')
}
.width('100%')
})
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
.padding({ left: 14, right: 14, top: 12, bottom: 20 })
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
.backgroundColor('#F4F6F8')
.edgeEffect(EdgeEffect.Spring)
}
}
// ---------------------------------------------------------------------------
// Tab 4:对战(悬浮主按钮直达)
// ---------------------------------------------------------------------------
@Component
struct BattleTab176 {
onChallenge: () => void = () => {}
build() {
Scroll() {
Column({ space: 12 }) {
// 积分卡
Column({ space: 12 }) {
Row({ space: 12 }) {
Column() {
Text('🤖')
.fontSize(28)
}
.width(56)
.height(56)
.borderRadius(28)
.backgroundColor('#3300B0FF')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Text('钢之心')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('赛季段位:钻石 · 城市榜第 4')
.fontSize(11)
.fontColor('#80D8FF')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text('2330')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#00B0FF')
Text('竞技分')
.fontSize(10)
.fontColor('#80D8FF')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
// 战绩堆叠条:胜 62% / 平 8% / 负 30%
Column({ space: 5 }) {
Row() {
Text('总战绩 126 场')
.fontSize(10)
.fontColor('#80D8FF')
Text('')
.layoutWeight(1)
Row({ space: 10 }) {
Text('● 胜 78')
.fontSize(10)
.fontColor('#00B0FF')
Text('● 平 10')
.fontSize(10)
.fontColor('#FFB300')
Text('● 负 38')
.fontSize(10)
.fontColor('#FF3D00')
}
}
.width('100%')
Row() {
Text('')
.width('62%')
.height(12)
.backgroundColor('#00B0FF')
Text('')
.width('8%')
.height(12)
.backgroundColor('#FFB300')
Text('')
.layoutWeight(1)
.height(12)
.backgroundColor('#FF3D00')
}
.width('100%')
.height(12)
.borderRadius(6)
.clip(true)
}
.width('100%')
Text('⚡ 立即发起挑战')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 11, bottom: 11 })
.borderRadius(14)
.linearGradient({ angle: 90, colors: [['#FF3D00', 0.0], ['#FF6D00', 1.0]] })
.shadow({ radius: 8, color: '#33FF3D00', offsetX: 0, offsetY: 3 })
.onClick(() => {
this.onChallenge()
})
}
.width('100%')
.padding(16)
.borderRadius(18)
.linearGradient({ angle: 135, colors: [['#37474F', 0.0], ['#01579B', 1.0]] })
.shadow({ radius: 10, color: '#33374F40', offsetX: 0, offsetY: 4 })
// 今日可约对手
Row() {
Text('今日可约对手')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('按竞技分匹配')
.fontSize(10)
.fontColor('#78909C')
}
.width('100%')
ForEach(OPPONENTS176, (o: Opponent176) => {
Row({ space: 10 }) {
Column() {
Text(o.icon)
.fontSize(22)
}
.width(44)
.height(44)
.borderRadius(22)
.backgroundColor('#ECEFF1')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(o.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#263238')
if (o.online) {
Text('在线')
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(5)
.backgroundColor('#00B0FF')
} else {
Text('离线')
.fontSize(9)
.fontColor('#78909C')
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(5)
.backgroundColor('#ECEFF1')
}
}
Text(o.mech + ' · ' + o.style)
.fontSize(11)
.fontColor('#78909C')
Row({ space: 4 }) {
Text('竞技分 ' + o.rating)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(o.rating >= 1800 ? '#FF3D00' : '#00B0FF')
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('约战')
.fontSize(12)
.fontColor('#FFFFFF')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.borderRadius(14)
.backgroundColor('#37474F')
.onClick(() => {
this.onChallenge()
})
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(14)
})
// 战斗记录时间轴
Column({ space: 0 }) {
Row() {
Text('战斗记录')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('近 8 场')
.fontSize(10)
.fontColor('#78909C')
}
.width('100%')
.padding(14)
ForEach(BATTLES176, (b: Battle176, idx: number) => {
Column() {
Row({ space: 10 }) {
// 胜负徽章
Column() {
Text(b.win ? '胜' : '负')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width(26)
.height(26)
.borderRadius(13)
.backgroundColor(b.win ? '#00B0FF' : '#FF3D00')
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(b.date)
.fontSize(11)
.fontColor('#78909C')
Text(b.score)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(b.win ? '#00B0FF' : '#FF3D00')
}
Text('VS ' + b.opponent + ' · 出击:' + b.mech)
.fontSize(12)
.fontColor('#263238')
.fontWeight(FontWeight.Medium)
Text(b.arena)
.fontSize(10)
.fontColor('#78909C')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ left: 14, right: 14, top: 9, bottom: 9 })
if (idx < BATTLES176.length - 1) {
Text('')
.width(1)
.height(10)
.backgroundColor('#ECEFF1')
.margin({ left: 27 })
}
}
.width('100%')
})
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
// 场地预约
Row() {
Text('格斗场地')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('今晚场次实时')
.fontSize(10)
.fontColor('#78909C')
}
.width('100%')
ForEach(ARENAS176, (a: Arena176) => {
Column({ space: 8 }) {
Row({ space: 10 }) {
Column() {
Text('🏟️')
.fontSize(20)
}
.width(42)
.height(42)
.borderRadius(12)
.backgroundColor('#E1F5FE')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text(a.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#263238')
Text(a.city + ' · ' + a.size)
.fontSize(10)
.fontColor('#78909C')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 3 }) {
Text(a.fee)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor('#FF3D00')
Text(a.busy)
.fontSize(9)
.fontColor(busyC176(a.busy))
.padding({ left: 5, right: 5, top: 1, bottom: 1 })
.borderRadius(5)
.backgroundColor('#ECEFF1')
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Text(a.hot)
.fontSize(10)
.fontColor(busyC176(a.busy))
.width('100%')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(8)
.backgroundColor(busyC176(a.busy) + '14')
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(14)
})
}
.padding({ left: 14, right: 14, top: 12, bottom: 20 })
}
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
.backgroundColor('#F4F6F8')
.edgeEffect(EdgeEffect.Spring)
}
}
// ---------------------------------------------------------------------------
// Tab 5:赛事
// ---------------------------------------------------------------------------
@Component
struct TourTab176 {
build() {
Scroll() {
Column({ space: 12 }) {
// 焦点赛事
Column({ space: 12 }) {
Row() {
Text('🏆 旗舰赛事')
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('#33FF3D00')
Text('')
.layoutWeight(1)
Text('官方认证')
.fontSize(10)
.fontColor('#FFFFFF')
.opacity(0.8)
}
.width('100%')
Text('多多钢甲全国争霸赛 · 年度总决赛')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('12-06 ~ 12-08 · 上海国际格斗中心 · 羽量级 + 甲虫级双赛道')
.fontSize(11)
.fontColor('#FFFFFF')
.opacity(0.85)
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('¥100,000')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFB300')
Text('总奖金池')
.fontSize(10)
.fontColor('#FFFFFF')
.opacity(0.8)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column({ space: 2 }) {
Text('256')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('报名机甲')
.fontSize(10)
.fontColor('#FFFFFF')
.opacity(0.8)
}
.alignItems(HorizontalAlign.Start)
Text('立即报名')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.padding({ left: 18, right: 18, top: 9, bottom: 9 })
.borderRadius(18)
.backgroundColor('#FF3D00')
.shadow({ radius: 6, color: '#66FF3D00', offsetX: 0, offsetY: 2 })
}
.width('100%')
}
.width('100%')
.padding(16)
.borderRadius(18)
.linearGradient({ angle: 135, colors: [['#FF3D00', 0.0], ['#37474F', 1.0]] })
.shadow({ radius: 10, color: '#33FF3D00', offsetX: 0, offsetY: 4 })
// 赛事日历
Row() {
Text('赛事日历')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
Text('')
.layoutWeight(1)
Text('下半年 6 场')
.fontSize(10)
.fontColor('#78909C')
}
.width('100%')
ForEach(TOURNAMENTS176, (t: Tournament176) => {
Row({ space: 10 }) {
// 日期块
Column({ space: 1 }) {
Text(t.date.split(' ')[0].split('-')[0])
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('月')
.fontSize(9)
.fontColor('#FFFFFF')
.opacity(0.85)
}
.width(50)
.height(50)
.borderRadius(14)
.backgroundColor(statusC176(t.status))
.justifyContent(FlexAlign.Center)
Column({ space: 3 }) {
Row({ space: 6 }) {
Text(t.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#263238')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
Text(t.status)
.fontSize(9)
.fontColor(statusC176(t.status))
.padding({ left: 6, right: 6, top: 1, bottom: 1 })
.borderRadius(5)
.backgroundColor(statusC176(t.status) + '14')
}
.width('100%')
Text(t.date + ' · ' + t.level + ' · ' + t.players + ' 台机甲')
.fontSize(10)
.fontColor('#78909C')
Row({ space: 6 }) {
Text('奖金 ' + t.prize)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor('#FF3D00')
Text('')
.layoutWeight(1)
Text(t.status === '报名中' ? '去报名' : (t.status === '进行中' ? '看直播' : '看回顾'))
.fontSize(10)
})
}
}
总结:

第一,从架构设计的航海视角来看,该机器人格斗社区应用采用了经典的"单入口多标签"架构模式。@Entry标记的RobotWar176组件作为旗舰港口,统辖六个标签页子组件(机甲舱、装配间、市集码头、对战角斗场、赛事公告板、船长室)和五个弹框补给船(购买零件抽屉、装配编辑抽屉、发起挑战居中框、删除机甲居中框、机甲详情居中框)。所有交互状态集中在主页面的@State变量中管理,通过回调函数和$$双向绑定实现父子组件间的数据流动。这种集中式状态管理模式在中型应用中具有良好的可维护性和可调试性,如同旗舰集中指挥、各船服从调度的舰队编队模式。
第二,从数据流的航海视角来看,该应用的数据流向清晰有序。静态数据从顶部的const常量数组出发,通过组件参数传递流入各个Tab组件;用户交互产生的状态变化通过回调函数回流到主页面的@State变量,再由框架的响应式机制驱动UI更新。弹框组件的数据传递更加精细——主页面通过@Builder函数创建弹框实例时,不仅传入了当前选中的数据(如PARTS176[this.buyIdx]),还传入了关闭和确认两个回调,确保弹框内的操作结果能够准确回流到主页面并触发后续逻辑(如显示Toast提示)。这种"数据下行、事件上行"的单向数据流模式,确保了状态变化的可追溯性。
第三,从UI实现的航海视角来看,该应用充分运用了ArkTS的声明式UI特性。ForEach循环渲染处理了所有列表场景(机甲列表、零件网格、对手列表、战斗记录、赛事日历、积分榜等);if/else条件渲染处理了标签页切换、在线状态、成就解锁等条件显示场景;@Builder函数提取了Dock导航项和弹框Builder两类可复用UI片段;Flex换行布局处理了零件网格和成就徽章的自适应排列。进度条全部使用Text('')空文本组件实现,配合layoutWeight和百分比宽度,轻量而高效。渐变背景和投影效果的组合使用,为钢铁灰主题的UI增添了层次感和立体感。
第四,从工程规范的航海视角来看,该应用严格遵守了ArkTS的编译约束。所有变量声明和工具函数都抽取到了UI构建函数外部;所有回调函数使用箭头函数确保this绑定正确;所有颜色值使用标准十六进制格式(6位或8位);所有组件都使用了.width()和.height()等尺寸约束方法确保布局确定性;ForEach的键值函数虽然部分省略,但在需要精确控制的场景(如社区动态的tp.id.toString() + (tp.liked ? 'L' : 'U'))提供了复合键值。这些工程规范确保了代码的可读性、可维护性和运行时的稳定性。
第五,从业务覆盖的航海视角来看,该应用实现了一个完整的机器人格斗社区生态闭环。从机甲管理(查看档案、编辑装配、拆除回收)到零件交易(浏览市集、购买零件、特价抢购、卖家口碑),从对战系统(发起挑战、选择对手场地、押注钢豆、录像开关)到赛事体系(焦点赛事、赛事日历、赛季积分榜、奖金分配),再到个人中心(个人资料、资产展示、订单管理、成就徽章、设置列表),六个标签页覆盖了机器人格斗爱好者从装备采购到竞技对战的全链路需求。底部悬浮Dock的中央发光按钮直通对战页面,突出了"对战"这一核心业务场景,如同舰队的旗舰中央按钮直通战场指挥室。

所有评论(0)