HarmonyOS 6 粒子特效particleXs/Ys/Ops 用 @State 保证重渲染,而 particleSpeeds 与 particleTimer 用 private 修饰
一、开头技术描述
「机甲重载 MECHA HAUL」是一个面向人形机器人与机甲装备运输、机甲租赁业务的垂直行业应用页面,运行于 HarmonyOS 6.1.1 设备之上,基于 HarmonyOS ArkTS API 24 的声明式开发范式构建。整个页面采用单文件 ArkTS 页面的组织方式,将颜色设计系统、数据模型、可观察类、静态 Mock 数据、纯函数工具集、底部 Tab 枚举、入口主页面以及六个子内容组件,全部收纳进一个源文件中。这种"单文件即应用"的写法,既便于在学习与样例分发时整体搬运,也能让读者在一个文件内完整看到从状态声明、生命周期、装饰器到布局声明的全链路,是理解 HarmonyOS 声明式 UI 的极佳样本。

在技术栈层面,该工程大量使用了 HarmonyOS API 24 提供的核心装饰器与组件能力。状态管理上,主页面通过 @Entry @Component 暴露为入口,内部以 @State 管理当前激活 Tab、弹框显隐、选中对象、表单字段、粒子特效坐标等十余组状态;数据模型则用 @Observed 修饰,使 ShipOrderItem、MechaItem、RentalMechItem、EventItem、TradeLogItem 等类的实例具备可观察能力,配合 ForEach 渲染实现数据驱动视图。布局上采用 Column、Row、Stack、Scroll 等容器组件,通过 layoutWeight、position、zIndex 等属性完成头部、内容区、底部 Tab 栏、粒子特效层、弹框层的层叠与分区。
架构设计上,页面遵循"一个入口主页面 + 六个 Tab 子组件 + 四个弹框 Builder"的清晰分层。主页面 MechaHaulApp 通过 activeTab 状态在 contentArea 中按枚举分发渲染 MeBookingContent、MeMechasContent、MeRentalContent、MeEventContent、MeLogContent、MeProfileContent 六个独立 @Component;弹框则通过 showBookingModal、showEditModal、showRetireModal、showRentalModal 四个布尔状态在 build() 顶层 Stack 中条件挂载,由 @Builder 方法绘制遮罩与卡片。子组件与主页面之间通过回调函数(如 onBook、onEdit、onRetire、onRent)传递事件,再由主页面统一调度弹框显隐与选中对象,职责边界分明。
视觉风格上,工程以赛博暗黑科技风为基调,定义了完整的颜色常量体系:深空黑背景 #0A0E14、卡片底 #131926、电光青主色 #00D4FF、霓虹粉 #FF2E88、警示黄 #FFC94D、成功绿 #3DFFA0、紫 #8B5CF6 等。配合 linearGradient 渐变展示卡、粒子特效层(用 setInterval 驱动十枚"⚡"粒子在百分比坐标系内上升与透明度呼吸)、药丸式横滚筛选器、双色柱状图、进度条等视觉元素,营造出强烈的未来工业感。整页无任何本地图片资源,全部以 Emoji 图标与彩色色块构建视觉,体现了纯代码绘制的极致表现力。
二、整体架构与数据流转
下面用 Mermaid 流程图勾勒整个工程的模块关系与运行时数据流转,帮助读者在进入逐段代码前先建立全局认知。
如上图所示,工程自上而下分为四层:入口层负责把页面注册为 HarmonyOS 应用入口;状态中枢持有所有可变状态与生命周期;构建器层用 @Builder 方法绘制头部、内容区、底部栏、粒子层与四个弹框;Tab 子组件层是六个业务页面;最底层的数据与配置层提供类型、可观察类、配置表、Mock 数据与纯函数。回调函数将子组件的事件向上传递给主页面,再由主页面统一操控弹框显隐,形成单向数据流。
三、逐段代码分析
1. 颜色常量与设计系统
const COLOR_BG: string = '#0A0E14'
const COLOR_CARD: string = '#131926'
const COLOR_CARD_LIGHT: string = '#1B2334'
const COLOR_PRIMARY: string = '#00D4FF'
const COLOR_PRIMARY_LIGHT: string = '#66E5FF'
const COLOR_PINK: string = '#FF2E88'
const COLOR_WARN: string = '#FFC94D'
const COLOR_SUCCESS: string = '#3DFFA0'
const COLOR_PURPLE: string = '#8B5CF6'
const COLOR_TEXT_MAIN: string = '#E6F1FF'
const COLOR_TEXT_SUB: string = '#8FA3BF'
const COLOR_TEXT_HINT: string = '#4A5A74'
const COLOR_BORDER: string = '#223048'
const COLOR_DANGER: string = '#FF4D6A'
const PARTICLE_COLORS: string[] = ['#00D4FF', '#FF2E88', '#8B5CF6']

工程在文件最顶部以一组 const string 常量定义了完整的赛博暗黑科技风配色体系。这套设计系统的核心思路是"深底亮线":背景与卡片使用极深的蓝黑色(#0A0E14、#131926、#1B2334 三级灰阶),而文本与强调色则采用高饱和的霓虹色,电光青 #00D4FF 作为主色贯穿按钮、选中态与主数据;霓虹粉 #FF2E88 用于租赁业务与危险确认;警示黄 #FFC94D 表示待办与维护;成功绿 #3DFFA0 表示交付与可租;紫 #8B5CF6 用于仿生系与特殊状态。文本色分三级(主 #E6F1FF、次 #8FA3BF、提示 #4A5A74)形成清晰的信息层级。PARTICLE_COLORS 数组则供粒子特效层循环取色,使三色粒子在背景中交错闪烁。将颜色全部抽为常量,既保证了全站视觉一致性,也避免了魔法字符串散落各处带来的维护负担。
2. 数据模型 interface 契约
interface ShipOrderModel {
id: number
orderNo: string
mechName: string
pickup: string
destination: string
weight: number
insured: number
status: string
date: string
}
interface MechaModel {
id: number
code: string
model: string
height: number
weight: number
status: string
color: string
tag: string
}
interface RentalMechModel {
id: number
model: string
dailyRate: number
deposit: number
available: boolean
rating: number
color: string
}
interface EventModel {
id: number
name: string
date: string
venue: string
prizePool: number
teamCount: number
status: string
color: string
}
interface TradeLogModel {
id: number
type: string
amount: number
date: string
status: string
}

在进入具体实现前,工程先用五个 interface 定义了五大业务实体的数据契约:托运单 ShipOrderModel 记录订单号、机甲名、起终点、吨位、保价与状态;机甲档案 MechaModel 记录代号、型号、高度、重量、状态与标签;租赁机甲 RentalMechModel 记录日租金、押金、可租性、评分;赛事 EventModel 记录奖金池、队伍数与状态;交易日志 TradeLogModel 记录收支类型、金额与结算状态。这些接口本身不产生运行时对象,但它们为后续 @Observed 类提供了 implements 的契约约束,也让 ArkTS 的类型检查在编译期发挥作用。值得注意的是,状态字段统一使用 string 而非联合字面量类型(如 '待装载' | '运输中'),这是为了与后续的 Record<string, StatusMeta> 配置表以字符串键直接匹配,换取更高的灵活性与可读性。
3. @Observed 可观察类的实现
@Observed
class ShipOrderItem implements ShipOrderModel {
id: number = 0
orderNo: string = ''
mechName: string = ''
pickup: string = ''
destination: string = ''
weight: number = 0
insured: number = 0
status: string = '待装载'
date: string = ''
constructor(id: number, orderNo: string, mechName: string, pickup: string, destination: string,
weight: number, insured: number, status: string, date: string) {
this.id = id; this.orderNo = orderNo; this.mechName = mechName
this.pickup = pickup; this.destination = destination
this.weight = weight; this.insured = insured; this.status = status; this.date = date
}
}

这里展示的是 ShipOrderItem,其余四个类(MechaItem、RentalMechItem、EventItem、TradeLogItem)结构与思路完全一致。@Observed 是 HarmonyOS API 24 提供的类装饰器,被它修饰的类其属性变更会被框架观测,配合 @State、@Link 等可实现数据驱动刷新。每个类都 implements 对应的 interface,并给所有属性提供默认值(如 status: string = '待装载'),再通过构造函数逐字段赋值。构造函数体里采用紧凑的分号连写风格,把多个赋值压缩在一行,既减少行数又便于对照参数顺序。这种"接口定义契约 + @Observed 类提供可观察实例 + 构造函数注入初值"的三段式,是 ArkTS 中典型的可观察数据建模范式,比直接用对象字面量更利于类型推导与后续扩展。
4. Record 配置表驱动的元数据映射
interface StatusMeta {
label: string
color: string
bg: string
}
interface PackMeta {
label: string
icon: string
desc: string
}
const ORDER_STATUS_CONFIG: Record<string, StatusMeta> = {
'待装载': { label: '待装载', color: '#FFC94D', bg: '#33290F' },
'运输中': { label: '运输中', color: '#00D4FF', bg: '#0B2B33' },
'已交付': { label: '已交付', color: '#3DFFA0', bg: '#0B3324' }
}
const PACK_CONFIG: Record<string, PackMeta> = {
'标准': { label: '标准', icon: '📦', desc: '常规固定托运' },
'防震': { label: '防震', icon: '🛡️', desc: '防震支架固定' },
'精密': { label: '精密', icon: '🔬', desc: '精密仪器级固定' },
'恒温': { label: '恒温', icon: '🌡️', desc: '恒温货舱运输' }
}
const CATEGORY_CONFIG: Record<string, CategoryMeta> = {
'全部': { label: '全部', icon: '🌐' },
'人形机器人': { label: '人形机器人', icon: '🤖' },
'外骨骼': { label: '外骨骼', icon: '🦾' }
}

工程把"状态文本到视觉表现"的映射抽象为 Record<string, StatusMeta> 这类键值表,包括订单状态、机甲状态、赛事状态三套状态配色,以及包装等级 PACK_CONFIG、机甲分类 CATEGORY_CONFIG 两套图标描述。每条记录不仅存了显示文本,还存了前景色 color、背景色 bg 与图标,让消费侧只需 ORDER_STATUS_CONFIG[o.status]?.color 即可拿到对应配色,避免了在渲染时堆砌 if/else 分支。这种"配置表驱动"的设计使新增一种状态只需在表中加一行,极大降低了维护成本。同时使用可选链 ?. 与空合并 ?? 提供兜底(如 ?? COLOR_TEXT_SUB),保证未命中的状态也能优雅降级显示,体现了防御式编程的细节。
5. 常量选项与静态 Mock 数据
const CATEGORY_FILTER: string[] = ['全部', '人形机器人', '外骨骼', '仿生机械', '格斗机甲', '工程机甲']
const PACK_LEVELS: string[] = ['标准', '防震', '精密', '恒温']
const MECHA_NAME_OPTIONS: string[] = ['泰坦-T7', '夜枭-X3', '磐石-G9', '苍穹-S1', '烈焰-F5', '银月-M2']
const SERVICE_OPTIONS: string[] = ['防震支架', '恒温舱', 'GPS追踪', '安保跟车']
const mockOrders: ShipOrderItem[] = [
new ShipOrderItem(1, 'MH-2026-0823-01', '泰坦-T7', '上海临港基地', '北京亦庄竞技场', 2.8, 580000, '运输中', '08-23'),
new ShipOrderItem(2, 'MH-2026-0822-14', '夜枭-X3', '深圳湾机库', '广州天河展馆', 1.9, 320000, '运输中', '08-22')
]

常量选项部分定义了分类筛选、包装等级、机甲名候选、服务增项等字符串数组,它们既是表单与筛选器的数据源,也充当枚举的替代品。静态 Mock 数据则是用前述 @Observed 类构造出的一批实例数组:mockOrders 含八条托运单覆盖三种状态,mockMechas 含十台机甲覆盖待命/出租/维护,mockRentals 含八台可租机甲,mockEvents 含六场赛事覆盖报名/进行/结束,mockTrades 含六条收支记录。这些数据以 const 数组形式存在,配合 ForEach 即可渲染列表。在真实工程中,它们会被网络请求与数据库替换,但作为单文件样例,Mock 数据让页面"开箱即满",便于直观感受视觉效果与交互逻辑,也展示了构造函数的批量调用方式。
6. 纯函数工具集
function formatMoney(n: number): string {
return '¥' + n.toLocaleString('en-US')
}
function calcRentFee(daily: number, days: number): number {
return daily * days
}
function calcInsurance(withInsurance: boolean, daily: number, days: number): number {
if (withInsurance) {
return Math.round(daily * days * 0.08)
}
return 0
}
function getSortedRentals(sortKey: string): RentalMechItem[] {
let list: RentalMechItem[] = mockRentals.slice(0)
if (sortKey === '价格') {
list.sort((a: RentalMechItem, b: RentalMechItem) => a.dailyRate - b.dailyRate)
} else if (sortKey === '评分') {
list.sort((a: RentalMechItem, b: RentalMechItem) => b.rating - a.rating)
} else {
list.sort((a: RentalMechItem, b: RentalMechItem) => (b.available ? 1 : 0) - (a.available ? 1 : 0))
}
return list
}
function toggleIdIn(ids: number[], id: number): number[] {
let out: number[] = ids.slice(0)
if (out.indexOf(id) >= 0) {
out.splice(out.indexOf(id), 1)
} else {
out.push(id)
}
return out
}

工程把所有可复用的计算逻辑抽成顶层纯函数,不依赖任何组件状态,便于测试与复用。formatMoney 利用 toLocaleString('en-US') 做千分位格式化并加"¥"前缀;calcRentFee 与 calcInsurance 分别计算租金与保险费(保险为租金的 8% 并取整);getSortedRentals 用 slice(0) 浅拷贝后再排序,避免污染原数组,并按价格升序、评分降序、可租优先三种策略分发;toggleIdIn 与 toggleBoolAt 则是数组状态切换的工具,前者在 id 数组中增删元素(用于赛事报名切换),后者翻转布尔数组某位(用于服务增项勾选)。这种"状态变更返回新数组"的不可变风格,非常契合 ArkTS @State 的赋值触发刷新机制——直接 this.joinedIds = toggleIdIn(...) 即可让框架感知变化并重渲染列表。
7. Tab 枚举与主入口状态声明
enum MechaTab {
BOOKING = 0,
MECHAS = 1,
RENTAL = 2,
EVENT = 3,
LOG = 4,
PROFILE = 5
}
@Entry
@Component
struct MechaHaulApp {
@State activeTab: MechaTab = MechaTab.BOOKING
@State selectedCategory: string = '全部'
@State searchKeyword: string = ''
@State showBookingModal: boolean = false
@State showEditModal: boolean = false
@State showRetireModal: boolean = false
@State showRentalModal: boolean = false
@State selectedMecha: MechaItem | null = null
@State selectedRental: RentalMechItem | null = null
@State bMech: string = '泰坦-T7'
@State bWeight: string = ''
@State bPack: string = '防震'
@State bServices: boolean[] = [true, false, true, false]
@State rentDays: number = 1
@State rentDelivery: string = '押运上门'
@State rentInsurance: boolean = true
@State particleXs: number[] = [8, 22, 37, 52, 66, 81, 12, 45, 74, 92]
@State particleYs: number[] = [90, 72, 58, 84, 44, 68, 28, 14, 54, 38]
@State particleOps: number[] = [0.4, 0.7, 0.5, 0.8, 0.6, 0.45, 0.75, 0.55, 0.65, 0.5]
private particleSpeeds: number[] = [3, 4, 2, 5, 3, 4, 2, 5, 3, 4]
private particleTimer: number = -1
}

MechaTab 枚举为六个 Tab 项赋予有序的数值,使 activeTab 的比较与切换既类型安全又高效。主入口 MechaHaulApp 用 @Entry @Component 声明,其状态可分为四组:导航类(activeTab、selectedCategory、searchKeyword)、弹框显隐类(四个布尔)、选中对象类(selectedMecha、selectedRental,均为可空联合类型 | null)、表单字段类(预约托运的 b* 前缀、租赁的 rent* 前缀,共十余个字段)。粒子特效相关状态单独成组,particleXs/Ys/Ops 用 @State 保证重渲染,而 particleSpeeds 与 particleTimer 用 private 修饰——因为它们不参与 UI 刷新,只是定时器内部用的速度常量与句柄,无需走状态观测通道。这种对"是否需要触发刷新"的精准区分,是 ArkTS 性能优化的关键意识。
8. 生命周期与粒子定时器
aboutToAppear() {
this.particleTimer = setInterval(() => {
let xs: number[] = []
let ys: number[] = []
let ops: number[] = []
for (let i = 0; i < 10; i++) {
let y: number = this.particleYs[i] - this.particleSpeeds[i]
if (y < -5) {
y = 105
}
ys.push(y)
xs.push(this.particleXs[i])
ops.push(0.3 + Math.abs(Math.sin(y / 12 + i)) * 0.6)
}
this.particleXs = xs
this.particleYs = ys
this.particleOps = ops
}, 260)
}
aboutToDisappear() {
if (this.particleTimer >= 0) {
clearInterval(this.particleTimer)
this.particleTimer = -1
}
}

aboutToAppear 与 aboutToDisappear 是 HarmonyOS API 24 的组件生命周期回调,分别在组件创建后与销毁前触发。这里在 aboutToAppear 中启动一个 260 毫秒间隔的 setInterval,循环驱动十枚粒子的动画:每轮把每枚粒子的纵坐标 y 减去各自速度实现上升,当 y 跌出顶部(小于 -5)则回绕到底部(105)继续上升,形成连绵不断的流动;透明度则用 Math.abs(Math.sin(y / 12 + i)) * 0.6 + 0.3 制造呼吸式明暗变化,相位偏移 i 让各粒子异步闪烁。更新时一次性把新数组整体赋给三个 @State,框架感知到引用变化后重渲染粒子层。aboutToDisappear 中务必 clearInterval 并把句柄置 -1,防止组件销毁后定时器仍在回调已失效的实例,这是 ArkTS 定时器使用的标准防泄漏范式。
9. 头部构建器:搜索与分类药丸
@Builder headerBuilder() {
Column() {
Row() {
Column() {
Text('机甲重载').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
Text('MECHA HAUL').fontSize(8).fontColor(COLOR_TEXT_HINT).margin({ top: 1 })
}
.alignItems(HorizontalAlign.Start).padding({ right: 10 })
Row() {
Text('🔍').fontSize(13).margin({ left: 10 })
TextInput({ placeholder: '搜索机甲型号' })
.placeholderColor(COLOR_TEXT_HINT).fontSize(12).layoutWeight(1)
.backgroundColor(COLOR_CARD_LIGHT).borderRadius(14)
.fontColor(COLOR_TEXT_MAIN).height(30).margin({ left: 6, right: 6 })
.onChange((v: string) => { this.searchKeyword = v })
Text('⚡').fontSize(14).fontColor(COLOR_WARN)
.width(28).height(28).borderRadius(14)
.backgroundColor(COLOR_CARD_LIGHT).textAlign(TextAlign.Center)
}
.layoutWeight(1).backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 15 }).height(34)
}
.width('100%').padding({ left: 14, right: 14, top: 12 })
Scroll() {
Row() {
ForEach(CATEGORY_FILTER, (cat: string) => {
if (this.selectedCategory === cat) {
Text((CATEGORY_CONFIG[cat]?.icon ?? '') + ' ' + cat)
.fontSize(11).fontColor(COLOR_BG).backgroundColor(COLOR_PRIMARY)
.fontWeight(FontWeight.Bold)
.padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(13)
} else {
Text((CATEGORY_CONFIG[cat]?.icon ?? '') + ' ' + cat)
.fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD)
.padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(13)
.onClick(() => { this.selectedCategory = cat })
}
})
}.padding({ left: 10, right: 10 })
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(36).margin({ top: 8 })
}.width('100%').backgroundColor(COLOR_BG).padding({ bottom: 6 })
}
headerBuilder 用 @Builder 装饰为一个可复用的构建方法,负责绘制页面顶栏。第一行是标题区与搜索框的组合:左侧用主色加粗显示"机甲重载"与极小号的英文副标题"MECHA HAUL",右侧搜索框用 TextInput 配 layoutWeight(1) 占满剩余空间,外层套一个带圆角边框的 Row 形成胶囊输入框,onChange 把输入回写 searchKeyword 状态。第二行是横向滚动的分类药丸条:Scroll 设 ScrollDirection.Horizontal 并关掉滚动条,内部 ForEach 遍历 CATEGORY_FILTER,对当前选中项与非选中项分别渲染高亮(主色底深色字加粗)与常态(卡片底次色字)两种样式,点击切换 selectedCategory。每个药丸文本都从 CATEGORY_CONFIG 取图标与标签拼接,用可选链兜底空图标,体现了配置驱动的渲染思路。
10. 内容区分发与底部 Tab 栏
@Builder contentArea() {
Column() {
if (this.activeTab === MechaTab.BOOKING) {
MeBookingContent({ onBook: () => { this.showBookingModal = true } })
} else if (this.activeTab === MechaTab.MECHAS) {
MeMechasContent({
onEdit: (m: MechaItem) => {
this.selectedMecha = m
this.eCode = m.code; this.eModel = m.model
this.eHeight = m.height.toString(); this.eWeight = m.weight.toString()
this.eStatus = m.status; this.showEditModal = true
},
onRetire: (m: MechaItem) => {
this.selectedMecha = m; this.showRetireModal = true
}
})
} else if (this.activeTab === MechaTab.RENTAL) {
MeRentalContent({
onRent: (r: RentalMechItem) => {
this.selectedRental = r; this.rentDays = 1; this.showRentalModal = true
}
})
} else if (this.activeTab === MechaTab.EVENT) {
MeEventContent()
} else if (this.activeTab === MechaTab.LOG) {
MeLogContent()
} else {
MeProfileContent()
}
}.layoutWeight(1)
}
@Builder bottomTabItem(icon: string, label: string, tab: MechaTab) {
Column() {
Text(icon).fontSize(19).opacity(this.activeTab === tab ? 1.0 : 0.4)
Text(label).fontSize(9)
.fontColor(this.activeTab === tab ? COLOR_PRIMARY : COLOR_TEXT_HINT)
.fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal).margin({ top: 1 })
if (this.activeTab === tab) {
Column().width(16).height(2).backgroundColor(COLOR_PRIMARY).borderRadius(1).margin({ top: 2 })
}
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 6, bottom: 6 })
.onClick(() => { this.activeTab = tab })
}
@Builder bottomTabBar() {
Row() {
this.bottomTabItem('🚛', '托运', MechaTab.BOOKING)
this.bottomTabItem('🤖', '机库', MechaTab.MECHAS)
this.bottomTabItem('📋', '租赁', MechaTab.RENTAL)
this.bottomTabItem('🏆', '赛事', MechaTab.EVENT)
this.bottomTabItem('📊', '台账', MechaTab.LOG)
this.bottomTabItem('👤', '我的', MechaTab.PROFILE)
}.width('100%').backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 0 })
}
contentArea 是整个页面的路由分发中心,用一串 if / else if 根据 activeTab 渲染对应的子组件,并把回调函数通过参数对象传入:托运页传 onBook 打开预约弹框;机库页传 onEdit 与 onRetire,前者先把选中机甲的字段同步到编辑表单状态(eCode、eModel 等十二个字段逐一赋值,实现"带入编辑"),后者只记录选中并打开退役弹框;租赁页传 onRent 并重置天数为一。底部 bottomTabBar 用六个 bottomTabItem 横向均分(layoutWeight(1)),每个项根据是否当前 Tab 调整图标透明度、文字颜色与字重,并条件渲染一条主色下划线作为选中指示,点击即切换 activeTab。这种"子组件只管渲染与触发事件、主页面统一调度弹框与选中态"的职责划分,让子组件保持纯粹,状态收敛在一处。
11. 粒子特效层与弹框遮罩
@Builder particleLayer() {
Stack() {
ForEach(this.particleYs, (y: number, i: number) => {
Text('⚡')
.fontSize(9 + (i % 3) * 4)
.fontColor(PARTICLE_COLORS[i % PARTICLE_COLORS.length])
.opacity(this.particleOps[i])
.position({ x: this.particleXs[i] + '%', y: y + '%' })
})
}.width('100%').height('100%').hitTestBehavior(HitTestMode.None)
}
@Builder modalOverlay(onClose: () => void) {
Column()
.width('100%').height('100%')
.backgroundColor('rgba(4,7,12,0.72)')
.onClick(onClose)
}
particleLayer 用一个铺满屏幕的 Stack 叠放十枚"⚡"粒子,通过 position 以百分比坐标定位,字号按 9 + (i % 3) * 4 在 9/13/17 间轮换形成大小层次,颜色从 PARTICLE_COLORS 循环取青/粉/紫三色,透明度直接读 particleOps[i]。关键的一行是 hitTestBehavior(HitTestMode.None)——它让粒子层不拦截触摸事件,从而底层按钮与列表仍可正常点击,这是覆盖层不打断交互的关键技巧。modalOverlay 则是所有弹框共用的半透明遮罩,用 rgba(4,7,12,0.72) 制造暗化效果,并把传入的 onClose 回调绑定到点击事件,实现"点遮罩即关弹框"的通用关闭行为。把它抽成带参 @Builder,四个弹框都能复用,避免重复书写遮罩样式。
12. 弹框一:预约托运表单
@Builder bookingModal() {
Column() {
this.modalOverlay(() => { this.showBookingModal = false })
Column() {
Row() {
Text('🤖 预约机甲托运').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
Column().layoutWeight(1)
Text('✕').fontSize(16).fontColor(COLOR_TEXT_SUB)
.width(28).height(28).borderRadius(14)
.backgroundColor(COLOR_CARD_LIGHT).textAlign(TextAlign.Center)
.onClick(() => { this.showBookingModal = false })
}.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
Scroll() {
Column() {
Text('选择机甲').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 6 })
Scroll() {
Row() {
ForEach(MECHA_NAME_OPTIONS, (name: string) => {
if (this.bMech === name) {
Text('🤖 ' + name).fontSize(11).fontColor(COLOR_BG).backgroundColor(COLOR_PRIMARY)
.fontWeight(FontWeight.Bold)
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(12)
} else {
Text('🤖 ' + name).fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD_LIGHT)
.padding({ left: 10, right: 10, top: 5, bottom: 5 }).borderRadius(12)
.onClick(() => { this.bMech = name })
}
})
}
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
Row() {
Column() {
Text('重量(吨)').fontSize(11).fontColor(COLOR_TEXT_SUB)
TextInput({ placeholder: '如 2.8' }).placeholderColor(COLOR_TEXT_HINT).fontSize(13).width('100%')
.fontColor(COLOR_TEXT_MAIN).backgroundColor(COLOR_CARD_LIGHT).borderRadius(8).margin({ top: 4 })
.onChange((v: string) => { this.bWeight = v })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%').margin({ top: 10 })
Text('特殊要求').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
Row() {
ForEach(SERVICE_OPTIONS, (s: string, i: number) => {
if (this.bServices[i]) {
Text('☑ ' + s).fontSize(11).fontColor(COLOR_PRIMARY).backgroundColor('#0B2B33')
.border({ width: 1, color: COLOR_PRIMARY, radius: 12 })
.onClick(() => { this.bServices = toggleBoolAt(this.bServices, i) })
} else {
Text('☐ ' + s).fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD_LIGHT)
.onClick(() => { this.bServices = toggleBoolAt(this.bServices, i) })
}
})
}
}.padding({ left: 16, right: 16, bottom: 12 })
}.layoutWeight(1).scrollBar(BarState.Off)
Row() {
Text('取消').fontSize(14).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD_LIGHT).borderRadius(20)
.padding({ left: 26, right: 26, top: 10, bottom: 10 })
.onClick(() => { this.showBookingModal = false })
Text('确认托运').fontSize(14).fontColor(COLOR_BG).backgroundColor(COLOR_PRIMARY).borderRadius(20)
.fontWeight(FontWeight.Bold).padding({ left: 26, right: 26, top: 10, bottom: 10 }).margin({ left: 12 })
.onClick(() => { this.showBookingModal = false })
}.width('100%').justifyContent(FlexAlign.Center).padding({ top: 12, bottom: 16 })
}
.width('90%').height('75%').backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_PRIMARY, radius: 16 })
.position({ x: '5%', y: '12%' })
}.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
bookingModal 是四个弹框中最复杂的一个,承载了完整的预约托运表单。结构上先调 modalOverlay 铺遮罩,再叠一个 width('90%').height('75%') 的卡片,用 position 居中并 zIndex(999) 置顶。卡片内部三段式:标题行含图标标题与右上角关闭圆钮;中部 Scroll 承载表单,包括机甲名横滑选择(ForEach + MECHA_NAME_OPTIONS,选中态主色高亮)、重量与保价双栏输入、包装等级药丸组(PACK_LEVELS + 图标)、出发地/目的地/时间三个 TextInput、特殊要求多选(SERVICE_OPTIONS + bServices 布尔数组,用 toggleBoolAt 翻转)、备注 TextArea。表单字段全部绑到 b* 前缀的 @State,关闭弹框只需把 showBookingModal 置 false。底部是取消与确认两个按钮,用 FlexAlign.Center 居中。整个表单展示了 ArkTS 中"声明式表单"的典型写法:每个输入控件都通过 onChange 把值回写状态,状态变化又驱动选中态样式刷新。
13. 弹框二:编辑机甲档案(侧滑卡片式)
@Builder editModal() {
Column() {
this.modalOverlay(() => { this.showEditModal = false })
Column() {
Row() {
Text('✏️ 编辑机甲档案').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY_LIGHT)
Column().layoutWeight(1)
Text('✕').fontSize(16).fontColor(COLOR_TEXT_SUB)
.width(28).height(28).borderRadius(14).backgroundColor(COLOR_CARD_LIGHT).textAlign(TextAlign.Center)
.onClick(() => { this.showEditModal = false })
}.width('100%').padding({ left: 18, right: 16, top: 16, bottom: 10 })
Scroll() {
Column() {
Row() {
Column() {
Text('🤖').fontSize(30)
}.width(54).height(54).borderRadius(14)
.backgroundColor(this.selectedMecha?.color ?? COLOR_PRIMARY)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text('ID: MH-' + (this.selectedMecha?.id ?? 0).toString().padStart(3, '0'))
.fontSize(10).fontColor(COLOR_TEXT_HINT)
Text('当前状态: ' + (this.selectedMecha?.status ?? '待命'))
.fontSize(12).fontColor(COLOR_TEXT_SUB).margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
}.width('100%').margin({ top: 4, bottom: 8 })
Row() {
Column() {
Text('代号').fontSize(11).fontColor(COLOR_TEXT_SUB)
TextInput({ placeholder: this.selectedMecha?.code ?? '' })
.placeholderColor(COLOR_TEXT_HINT).fontSize(13).width('100%')
.fontColor(COLOR_TEXT_MAIN).backgroundColor(COLOR_CARD_LIGHT).borderRadius(8).margin({ top: 4 })
.onChange((v: string) => { this.eCode = v })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}.width('100%').margin({ top: 8 })
Text('状态切换').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 12 })
Row() {
ForEach(MECHA_STATUS_OPTIONS, (s: string) => {
if (this.eStatus === s) {
Text(s).fontSize(11).fontColor(COLOR_BG)
.backgroundColor(MECHA_STATUS_CONFIG[s]?.color ?? COLOR_PRIMARY)
.fontWeight(FontWeight.Bold)
.padding({ left: 14, right: 14, top: 5, bottom: 5 }).borderRadius(12)
} else {
Text(s).fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD_LIGHT)
.padding({ left: 14, right: 14, top: 5, bottom: 5 }).borderRadius(12)
.onClick(() => { this.eStatus = s })
}
})
}
}.padding({ left: 18, right: 16, bottom: 12 })
}.layoutWeight(1).scrollBar(BarState.Off)
Row() {
Text('取消').fontSize(14).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD_LIGHT).borderRadius(20)
.padding({ left: 26, right: 26, top: 10, bottom: 10 })
.onClick(() => { this.showEditModal = false })
Text('保存').fontSize(14).fontColor(COLOR_BG).backgroundColor(COLOR_PRIMARY_LIGHT).borderRadius(20)
.fontWeight(FontWeight.Bold).padding({ left: 30, right: 30, top: 10, bottom: 10 }).margin({ left: 12 })
.onClick(() => { this.showEditModal = false })
}.width('100%').justifyContent(FlexAlign.Center).padding({ top: 12, bottom: 16 })
}
.width('88%').height('65%').backgroundColor(COLOR_CARD)
.borderRadius({ topLeft: 6, bottomLeft: 6, topRight: 26, bottomRight: 26 })
.position({ x: '6%', y: '18%' })
}.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
editModal 在视觉上与前一个弹框做了差异化处理:标题色用 COLOR_PRIMARY_LIGHT 而非主色,卡片圆角设为左小右大的非对称(topLeft:6, topRight:26),营造"侧滑卡片"的不对称感。内容上先展示一个机甲摘要行——左边是带选中机甲颜色的圆角图标块(backgroundColor(this.selectedMecha?.color)),右边显示补零的 ID(padStart(3, '0'))与当前状态,让用户一眼确认编辑对象。随后是代号/型号、高度/重量两组双栏输入,每个 TextInput 的 placeholder 都从 selectedMecha 取值,实现"带入原值编辑"。状态切换是一组药丸,选中态背景色取自 MECHA_STATUS_CONFIG 配置表对应状态的颜色,点击切换 eStatus。这里大量使用可选链 ?. 与空合并 ??,是因为 selectedMecha 是可空类型,必须在每个取值处兜底,防止空指针。
14. 弹框三:报废退役确认(警示式)
@Builder retireModal() {
Column() {
this.modalOverlay(() => { this.showRetireModal = false })
Column() {
Text('⚠️').fontSize(44).margin({ top: 22 })
Text('确认退役此机甲?').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN).margin({ top: 8 })
Text('退役后档案将锁定,恢复需联系平台工程师').fontSize(11).fontColor(COLOR_DANGER).margin({ top: 4 })
Row() {
Column() {
Text('🤖').fontSize(26)
}.width(48).height(48).borderRadius(12)
.backgroundColor(this.selectedMecha?.color ?? COLOR_PRIMARY)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(this.selectedMecha?.code ?? '').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Text(this.selectedMecha?.model ?? '').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
Text('残值估值 ¥86,000').fontSize(10).fontColor(COLOR_WARN).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
}.width('100%').backgroundColor(COLOR_CARD_LIGHT)
.border({ width: 1, color: COLOR_BORDER, radius: 12 }).padding(12).margin({ top: 16 })
Row() {
Text('取消').fontSize(14).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD_LIGHT).borderRadius(20)
.padding({ left: 28, right: 28, top: 10, bottom: 10 })
.onClick(() => { this.showRetireModal = false })
Text('确认退役').fontSize(14).fontColor('#FFFFFF').backgroundColor(COLOR_DANGER).borderRadius(20)
.fontWeight(FontWeight.Bold).padding({ left: 28, right: 28, top: 10, bottom: 10 }).margin({ left: 12 })
.onClick(() => { this.showRetireModal = false })
}.width('100%').justifyContent(FlexAlign.Center).padding({ top: 20, bottom: 22 })
}
.width('80%').backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_DANGER, radius: 16 })
.alignItems(HorizontalAlign.Center).position({ x: '10%', y: '32%' })
}.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
retireModal 是一个警示式确认弹框,与表单弹框相比刻意做得更小更聚焦:宽度仅 80%,居中偏下(y: '32%'),卡片边框用危险红 COLOR_DANGER 描边。内容极简:顶部一个大号警告图标"⚠️",接着标题与红色提示文案说明后果;中间一块机甲摘要卡显示代号、型号与残值估值(用警示黄);底部是取消与"确认退役"两按钮,确认按钮用危险红底白字加粗,视觉上明确提示不可逆操作。这种"用颜色与文案强调危险、用居中布局聚焦注意力、按钮颜色与操作严重性匹配"的设计,是危险操作确认框的通用范式。整页只有一个交互目的(确认或取消),不掺杂其它表单,避免用户分心。
15. 弹框四:租赁下单(含费用实时计算)
@Builder rentalOrderModal() {
Column() {
this.modalOverlay(() => { this.showRentalModal = false })
Column() {
Row() {
Text('📋 租赁下单').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_PINK)
Column().layoutWeight(1)
Text('✕').fontSize(16).fontColor(COLOR_TEXT_SUB)
.width(28).height(28).borderRadius(14).backgroundColor(COLOR_CARD_LIGHT).textAlign(TextAlign.Center)
.onClick(() => { this.showRentalModal = false })
}.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
Scroll() {
Column() {
Row() {
Column() {
Text('🤖').fontSize(30)
}.width(58).height(58).borderRadius(14)
.backgroundColor(this.selectedRental?.color ?? COLOR_PINK)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(this.selectedRental?.model ?? '').fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Row() {
Text('日租金').fontSize(10).fontColor(COLOR_TEXT_SUB)
Text(formatMoney(this.selectedRental?.dailyRate ?? 0))
.fontSize(13).fontColor(COLOR_PRIMARY).fontWeight(FontWeight.Bold).margin({ left: 4 })
Text('押金').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ left: 10 })
Text(formatMoney(this.selectedRental?.deposit ?? 0))
.fontSize(12).fontColor(COLOR_WARN).margin({ left: 4 })
}.margin({ top: 4 })
Text('评分 ' + (this.selectedRental?.rating ?? 5.0).toFixed(1) + ' ★')
.fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
}.width('100%')
Text('租赁天数').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 14 })
Row() {
Text('−').fontSize(20).fontColor(COLOR_TEXT_SUB).width(36).height(36).borderRadius(18)
.backgroundColor(COLOR_CARD_LIGHT).textAlign(TextAlign.Center)
.onClick(() => { if (this.rentDays > 1) { this.rentDays -= 1 } })
Text(this.rentDays.toString() + ' 天').fontSize(16).fontWeight(FontWeight.Bold)
.fontColor(COLOR_TEXT_MAIN).width(90).textAlign(TextAlign.Center)
Text('+').fontSize(20).fontColor(COLOR_PINK).width(36).height(36).borderRadius(18)
.backgroundColor('#33101F').textAlign(TextAlign.Center)
.onClick(() => { if (this.rentDays < 30) { this.rentDays += 1 } })
}.width('100%').justifyContent(FlexAlign.Center).margin({ top: 6 })
Text('保险选项').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 14 })
if (this.rentInsurance) {
Text('☑ 全额设备险(租金的8%,运输损坏免赔)').fontSize(12).fontColor(COLOR_SUCCESS)
.backgroundColor('#0B3324').border({ width: 1, color: COLOR_SUCCESS, radius: 10 })
.onClick(() => { this.rentInsurance = false })
} else {
Text('☐ 全额设备险(租金的8%,运输损坏免赔)').fontSize(12).fontColor(COLOR_TEXT_SUB)
.backgroundColor(COLOR_CARD_LIGHT)
.onClick(() => { this.rentInsurance = true })
}
Column() {
Text('费用明细').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN).width('100%')
Row() {
Text('租金 (' + this.rentDays.toString() + '天 × ' + formatMoney(this.selectedRental?.dailyRate ?? 0) + ')')
.fontSize(11).fontColor(COLOR_TEXT_SUB).layoutWeight(1)
Text(formatMoney(calcRentFee(this.selectedRental?.dailyRate ?? 0, this.rentDays)))
.fontSize(11).fontColor(COLOR_TEXT_MAIN)
}.width('100%').margin({ top: 8 })
Row() {
Text('保险费').fontSize(11).fontColor(COLOR_TEXT_SUB).layoutWeight(1)
Text(formatMoney(calcInsurance(this.rentInsurance, this.selectedRental?.dailyRate ?? 0, this.rentDays)))
.fontSize(11).fontColor(COLOR_SUCCESS)
}.width('100%').margin({ top: 6 })
Divider().color(COLOR_BORDER).margin({ top: 8 })
Row() {
Text('合计').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN).layoutWeight(1)
Text(formatMoney(calcRentFee(this.selectedRental?.dailyRate ?? 0, this.rentDays) +
(this.selectedRental?.deposit ?? 0) +
calcInsurance(this.rentInsurance, this.selectedRental?.dailyRate ?? 0, this.rentDays)))
.fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLOR_PINK)
}.width('100%').margin({ top: 8 })
}.width('100%').backgroundColor(COLOR_CARD_LIGHT)
.border({ width: 1, color: COLOR_BORDER, radius: 12 }).padding(12).margin({ top: 14 })
}.padding({ left: 16, right: 16, bottom: 12 })
}.layoutWeight(1).scrollBar(BarState.Off)
Text('立即租用').fontSize(15).fontColor('#FFFFFF').backgroundColor(COLOR_PINK).borderRadius(22)
.fontWeight(FontWeight.Bold).padding({ left: 60, right: 60, top: 12, bottom: 12 }).margin({ bottom: 16 })
.onClick(() => { this.showRentalModal = false })
}
.width('92%').height('70%').backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_PINK, radius: 18 })
.position({ x: '4%', y: '14%' })
}.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
rentalOrderModal 以霓虹粉为主色调,承载租赁下单全流程。顶部是机甲摘要行,显示日租金、押金与评分。核心交互是"天数步进器"——减号与加号两个圆形按钮,中间显示天数,分别带上下限保护(最少 1 天、最多 30 天),点击即增减 rentDays 状态。取送方式用 DELIVERY_OPTIONS 药丸组选择,保险用单个切换按钮控制 rentInsurance 布尔。最精彩的是底部"费用明细"卡,它把 calcRentFee、calcInsurance 两个纯函数直接用在模板里,随着天数与保险开关的变化,租金、保险费、合计三个金额实时重算并刷新显示——这是"声明式 + 纯函数"带来的即时响应体验,无需手写任何事件监听。合计行把租金、押金、保险费三者相加,用粉色加粗强调总价,整页构成一个完整的下单闭环。
16. 主 build() 入口与层叠装配
build() {
Stack() {
// 主内容层
Column() {
this.headerBuilder()
this.contentArea()
this.bottomTabBar()
}
.width('100%').height('100%').backgroundColor(COLOR_BG)
// 粒子特效层(不阻挡点击)
this.particleLayer()
// 弹框层
if (this.showBookingModal) { this.bookingModal() }
if (this.showEditModal) { this.editModal() }
if (this.showRetireModal) { this.retireModal() }
if (this.showRentalModal) { this.rentalOrderModal() }
}
.width('100%').height('100%')
}
}
build() 是每个 @Component 必须实现的构建方法,这里用一个根 Stack 把四层内容层叠装配:最底层是主内容 Column,纵向排列头部、内容区、底部 Tab 栏,铺满屏幕并设背景色;其上叠粒子特效层(hitTestBehavior(None) 不挡点击);最上层按四个布尔状态条件挂载弹框——if (this.showBookingModal) 等条件语句让弹框在状态为 false 时根本不进入渲染树,而非只是隐藏,这样既节省渲染开销又保证关闭后完全不留残影。Stack 默认的层叠顺序是后写的在上,因此弹框天然覆盖在粒子与主内容之上。整个 build 极其简洁,只有十几行,却清晰表达了"主内容 + 特效 + 弹框"三层结构,这正是声明式 UI 把视图结构化的力量。
17. Tab1 托运页:统计、监控、表单与柱状图
@Component
struct MeBookingContent {
onBook: () => void = () => {}
@State formMech: string = ''
@State formPack: string = '防震'
weekTons: number[] = [8.2, 12.5, 6.8, 15.3, 9.7, 18.6, 11.2]
weekDays: string[] = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
@Builder orderCard(o: ShipOrderItem) {
Column() {
Row() {
Text('🚛 ' + o.orderNo).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Column().layoutWeight(1)
Text(ORDER_STATUS_CONFIG[o.status]?.label ?? o.status)
.fontSize(10).fontColor(ORDER_STATUS_CONFIG[o.status]?.color ?? COLOR_TEXT_SUB)
.backgroundColor(ORDER_STATUS_CONFIG[o.status]?.bg ?? COLOR_CARD_LIGHT)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(9)
}.width('100%')
Row() {
Text('🤖 ' + o.mechName).fontSize(11).fontColor(COLOR_PRIMARY)
Text(o.weight.toFixed(1) + 't').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ left: 8 })
Text('保额 ' + formatMoney(o.insured)).fontSize(10).fontColor(COLOR_TEXT_HINT).margin({ left: 8 })
}.width('100%').margin({ top: 5 })
Row() {
Text('📍 ' + o.pickup).fontSize(10).fontColor(COLOR_TEXT_SUB)
Text('→').fontSize(10).fontColor(COLOR_PRIMARY).margin({ left: 6, right: 6 })
Text(o.destination).fontSize(10).fontColor(COLOR_TEXT_SUB)
Column().layoutWeight(1)
Text(o.date).fontSize(10).fontColor(COLOR_TEXT_HINT)
}.width('100%').margin({ top: 4 })
}.width('100%').backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 }).padding(12).margin({ top: 8 })
}
MeBookingContent 是托运 Tab 的内容组件,通过 onBook 回调与主页面联动打开预约弹框。它持有自己的局部表单状态(formMech、formPack 等)与周吨位数据。orderCard 是一个 @Builder,把单条托运单渲染成卡片:第一行订单号与状态标签(状态配色全部从 ORDER_STATUS_CONFIG 配置表取),第二行机甲名、吨位、保额,第三行起点→终点与日期,用箭头"→"串联。组件主 build 中还包含四格统计条(本月托运/在途/已交付/总吨位)、货舱环境实时监控卡(温度/湿度/防震等级/GPS 四指标)、快速下单表单卡(机甲型号/重量/包装等级/起终点,点击预约触发 onBook)、本周吨位柱状图(用 ForEach 遍历 weekTons,每根柱高度按 t / 20 * 70 换算为 vp 字符串,周六用粉色其余青色),最后是八条托运单列表。一个组件内集成了统计、监控、表单、图表、列表五种形态,展示了 ArkTS 在单页内承载丰富信息密度的能力。
18. Tab2 机库页:展示卡、状态统计与网格
@Component
struct MeMechasContent {
onEdit: (m: MechaItem) => void = (m: MechaItem) => {}
onRetire: (m: MechaItem) => void = (m: MechaItem) => {}
@Builder showcaseCard(m: MechaItem) {
Column() {
Column().layoutWeight(1)
Text('🤖').fontSize(34)
Column().layoutWeight(1)
Text(m.code).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('NO.MH-' + m.id.toString().padStart(3, '0')).fontSize(9).fontColor('#FFFFFF').opacity(0.8).margin({ top: 2 })
}
.width(200).height(130).borderRadius(16)
.linearGradient({ angle: 135, colors: [[m.color, 0.0], ['#1B2334', 1.0]] })
.alignItems(HorizontalAlign.Center).padding({ top: 12, bottom: 12 }).margin({ left: 5, right: 5 })
}
@Builder mechaGridCard(m: MechaItem) {
Column() {
Column() {
Text('🤖').fontSize(26)
}.width('100%').height(64).borderRadius(10).backgroundColor(m.color)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Text(m.code).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN).margin({ top: 6 })
Text(m.model).fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ top: 2 })
Row() {
Text('高 ' + m.height.toString() + 'm').fontSize(9).fontColor(COLOR_TEXT_SUB)
Text('重 ' + m.weight.toString() + 't').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ left: 6 })
}.margin({ top: 3 })
Text(MECHA_STATUS_CONFIG[m.status]?.label ?? m.status)
.fontSize(9).fontColor(MECHA_STATUS_CONFIG[m.status]?.color ?? COLOR_TEXT_SUB)
.backgroundColor(MECHA_STATUS_CONFIG[m.status]?.bg ?? COLOR_CARD_LIGHT)
.padding({ left: 8, right: 8, top: 2, bottom: 2 }).borderRadius(8).margin({ top: 5 })
Row() {
Text('管理').fontSize(11).fontColor(COLOR_BG).backgroundColor(COLOR_PRIMARY).borderRadius(10)
.padding({ left: 14, right: 14, top: 5, bottom: 5 }).onClick(() => { this.onEdit(m) })
Text('退役').fontSize(11).fontColor(COLOR_DANGER).border({ width: 1, color: COLOR_DANGER, radius: 10 })
.padding({ left: 10, right: 10, top: 4, bottom: 4 }).margin({ left: 6 }).onClick(() => { this.onRetire(m) })
}.margin({ top: 8 })
}.layoutWeight(1).backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 }).padding(10).margin({ left: 3, right: 3, top: 8 })
.alignItems(HorizontalAlign.Center)
}
MeMechasContent 用两个 @Builder 实现"大展示卡"与"网格小卡"两种机甲呈现。showcaseCard 是横滑区的大卡,用 linearGradient 从机甲主色到深底做 135 度渐变,内部用两个空白 Column().layoutWeight(1) 把图标居中、代号与编号压底,营造海报感。mechaGridCard 是两列网格中的小卡,顶部色块显示图标,下方依次是代号、型号、高度重量、状态标签(配色取配置表)、管理/退役两个按钮,分别回调 onEdit 与 onRetire 把当前机甲传回主页面。主 build 中先横滑三张大展示卡,再展示待命/出租中/维护中三格状态统计,最后用 ForEach([0, 2, 4, 6, 8], (start) => Row { 两张卡 }) 的步进写法把十台机甲按两列网格排布——以索引 0/2/4/6/8 为每行起点,每行渲染 start 与 start+1 两张卡,巧妙地用 ForEach 实现了二维网格。
19. Tab3 租赁页:排序与租赁卡
@Component
struct MeRentalContent {
onRent: (r: RentalMechItem) => void = (r: RentalMechItem) => {}
@State sortKey: string = '热门'
@Builder rentalCard(r: RentalMechItem) {
Row() {
Column() {
Text('🤖').fontSize(28)
}.width(84).height(84).borderRadius(12).backgroundColor(r.color)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(r.model).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Row() {
Text(formatMoney(r.dailyRate)).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
Text('/日').fontSize(9).fontColor(COLOR_TEXT_HINT)
Text('押金 ' + formatMoney(r.deposit)).fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ left: 8 })
}.margin({ top: 4 })
Row() {
Text('★ ' + r.rating.toFixed(1)).fontSize(10).fontColor(COLOR_WARN)
if (r.available) {
Text('可租').fontSize(9).fontColor(COLOR_SUCCESS).backgroundColor('#0B3324')
.padding({ left: 8, right: 8, top: 2, bottom: 2 }).borderRadius(8).margin({ left: 8 })
} else {
Text('已租满').fontSize(9).fontColor(COLOR_DANGER).backgroundColor('#331016')
.padding({ left: 8, right: 8, top: 2, bottom: 2 }).borderRadius(8).margin({ left: 8 })
}
}.margin({ top: 4 })
Text(r.available ? '租用' : '预约候补').fontSize(12).fontColor(COLOR_BG)
.backgroundColor(r.available ? COLOR_PINK : COLOR_WARN).borderRadius(14).fontWeight(FontWeight.Bold)
.padding({ left: 22, right: 22, top: 6, bottom: 6 }).margin({ top: 6 })
.onClick(() => { this.onRent(r) })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
}.width('100%').backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 14 }).padding(12).margin({ top: 8 })
}
build() {
Column() {
Row() {
ForEach(RENTAL_SORT_OPTIONS, (s: string) => {
if (this.sortKey === s) {
Text(s).fontSize(11).fontColor(COLOR_BG).backgroundColor(COLOR_PINK).fontWeight(FontWeight.Bold)
.padding({ left: 18, right: 18, top: 6, bottom: 6 }).borderRadius(14).margin({ left: 4, right: 4 })
} else {
Text(s).fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD)
.padding({ left: 18, right: 18, top: 6, bottom: 6 }).borderRadius(14).margin({ left: 4, right: 4 })
.onClick(() => { this.sortKey = s })
}
})
}.width('100%').padding({ left: 12, right: 12, top: 10, bottom: 4 })
Scroll() {
Column() {
ForEach(getSortedRentals(this.sortKey), (r: RentalMechItem) => {
this.rentalCard(r)
})
}.padding({ left: 12, right: 12, bottom: 20 })
}.layoutWeight(1).scrollBar(BarState.Off)
}.width('100%').height('100%')
}
}
MeRentalContent 顶部是一组排序药丸(热门/价格/评分),选中态粉色高亮,点击切换 sortKey。列表区直接调用纯函数 getSortedRentals(this.sortKey),该函数会根据排序键对 mockRentals 浅拷贝后按价格升序、评分降序或可租优先排序,返回新数组交给 ForEach 渲染。每张 rentalCard 是横向布局:左侧色块图标,右侧型号、日租金(formatMoney 格式化)、押金、评分星、可租/已租满标签、租用/预约候补按钮。按钮文案与颜色都随 r.available 切换——可租时粉色"租用",已满时黄色"预约候补",点击都触发 onRent 回调打开下单弹框。这里展示了"排序状态驱动纯函数重算、纯函数返回新数组驱动列表重渲染"的典型响应式链路,排序切换即时生效。
20. Tab4 赛事页:进度条与报名切换
@Component
struct MeEventContent {
@State joinedIds: number[] = []
@Builder eventCard(e: EventItem) {
Column() {
Column() {
Text('🏆').fontSize(30)
Column().layoutWeight(1)
Row() {
Text('奖金池 ' + formatWan(e.prizePool)).fontSize(10).fontColor('#FFFFFF')
.backgroundColor('rgba(10,14,20,0.55)').padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
Text(EVENT_STATUS_CONFIG[e.status]?.label ?? e.status).fontSize(10)
.fontColor(EVENT_STATUS_CONFIG[e.status]?.color ?? COLOR_TEXT_SUB)
.backgroundColor('rgba(10,14,20,0.55)').padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
.margin({ left: 6 })
}.width('100%').justifyContent(FlexAlign.End).padding(8)
}.width('100%').height(84).borderRadius(12)
.linearGradient({ angle: 120, colors: [[e.color, 0.0], ['#1B2334', 1.0]] })
Text(e.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN).margin({ top: 8 })
Row() {
Text('📅 ' + e.date).fontSize(10).fontColor(COLOR_TEXT_SUB)
Text('📍 ' + e.venue).fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ left: 10 })
}.margin({ top: 4 })
Column() {
Row() {
Text('报名队伍 ' + e.teamCount.toString() + '/64').fontSize(9).fontColor(COLOR_TEXT_SUB).layoutWeight(1)
Text(getEventCapacity(e.teamCount).toString() + '%').fontSize(9).fontColor(COLOR_PRIMARY)
}.width('100%')
Row() {
Column().width(getEventCapacity(e.teamCount).toString() + '%').height(4)
.backgroundColor(e.color).borderRadius(2)
Column().layoutWeight(1)
}.width('100%').height(4).backgroundColor(COLOR_CARD_LIGHT).borderRadius(2).margin({ top: 4 })
}.width('100%').margin({ top: 8 })
Row() {
Text(this.joinedIds.indexOf(e.id) >= 0 ? '✅ 已报名' : '报名')
.fontSize(12).fontColor(this.joinedIds.indexOf(e.id) >= 0 ? COLOR_SUCCESS : COLOR_BG)
.backgroundColor(this.joinedIds.indexOf(e.id) >= 0 ? '#0B3324' : COLOR_PRIMARY)
.borderRadius(14).fontWeight(FontWeight.Bold)
.padding({ left: 28, right: 28, top: 6, bottom: 6 })
Column().layoutWeight(1)
Text('详情 >').fontSize(10).fontColor(COLOR_TEXT_HINT)
}.width('100%').margin({ top: 10 })
.onClick(() => { this.joinedIds = toggleIdIn(this.joinedIds, e.id) })
}.width('100%').backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 14 }).padding(12).margin({ top: 10 })
.alignItems(HorizontalAlign.Start)
}
MeEventContent 用 joinedIds: number[] 记录已报名赛事的 id 集合。eventCard 顶部是渐变头图区,叠放奖金池与状态两个半透明标签;中部是赛事名、日期、场地;亮点是报名进度条——用 getEventCapacity(teamCount) 把队伍数换算为百分比(Math.min(Math.round(teamCount / 64 * 100), 100)),再用 Column().width(百分比 + '%') 作为进度填充条、外层灰底条作为轨道,纯代码绘制出进度条。底部报名按钮根据 joinedIds.indexOf(e.id) >= 0 判断是否已报名,已报名显示绿色"✅ 已报名"、未报名显示主色"报名",点击整行触发 toggleIdIn 在 id 数组中增删当前赛事,实现报名/取消报名的切换。由于 toggleIdIn 返回新数组并整体赋给 @State,框架感知引用变化后立即重渲染相关卡片,交互即时生效。
21. Tab5 台账页:双色柱状图与交易记录
@Component
struct MeLogContent {
incomeData: number[] = [6.2, 7.8, 5.4, 9.1, 8.3, 10.6]
expenseData: number[] = [5.1, 6.5, 4.9, 7.2, 6.8, 8.4]
monthLabels: string[] = ['3月', '4月', '5月', '6月', '7月', '8月']
@Builder tradeRow(t: TradeLogItem) {
Row() {
Column() {
Text(t.type).fontSize(12).fontWeight(FontWeight.Medium).fontColor(COLOR_TEXT_MAIN)
Text(t.date).fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ top: 3 })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text((t.amount >= 0 ? '+' : '') + formatMoney(t.amount)).fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(t.type.indexOf('支出') >= 0 ? COLOR_DANGER : COLOR_SUCCESS)
Text(t.status).fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ top: 3 })
}.alignItems(HorizontalAlign.End)
}.width('100%').backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 10 })
.padding({ left: 12, right: 12, top: 10, bottom: 10 }).margin({ top: 6 })
}
MeLogContent 是财务台账页。顶部是 2x2 的大数字统计格(总支出/总收入/待结算/押金托管),分别用危险红、成功绿、警示黄、主色区分。核心是月度收支柱状图:incomeData 与 expenseData 两个数组分别保存六个月的收入与支出(单位万元),ForEach 遍历 monthLabels,每月渲染一组并列的双柱——收入柱青色、支出柱粉色,高度按 数据 / 12 * 80 换算为 vp 字符串,底部标注月份。这是用纯组件拼出的双色柱状图,无需任何图表库。tradeRow 把每条交易记录渲染成一行:左侧类型与日期,右侧金额与状态,金额前缀根据是否含"支出"二字决定正负号与红绿色,巧妙地用字符串包含判断代替专门的收支字段。这种"以数据特征推断视觉"的做法在样例中简洁有效,真实工程则建议用枚举字段显式区分。
22. Tab6 我的页:资料卡与设置列表
@Component
struct MeProfileContent {
@State settingsExpanded: boolean = false
@Builder settingRow(icon: string, label: string, hint: string) {
Row() {
Text(icon).fontSize(17)
Text(label).fontSize(13).fontColor(COLOR_TEXT_MAIN).layoutWeight(1).margin({ left: 12 })
Text(hint).fontSize(10).fontColor(COLOR_TEXT_HINT)
Text('>').fontSize(12).fontColor(COLOR_TEXT_HINT).margin({ left: 6 })
}.width('100%').padding({ top: 13, bottom: 13, left: 14, right: 14 })
.onClick(() => { this.settingsExpanded = !this.settingsExpanded })
}
build() {
Column() {
Scroll() {
Column() {
Row() {
Column() {
Text('🧑🚀').fontSize(34)
}.width(62).height(62).borderRadius(31)
.backgroundColor(COLOR_CARD_LIGHT).border({ width: 1, color: COLOR_PRIMARY, radius: 31 })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text('凌霄机甲师').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Row() {
Text('Lv.9 王牌机甲师').fontSize(10).fontColor(COLOR_BG).backgroundColor(COLOR_PRIMARY)
.borderRadius(8).padding({ left: 8, right: 8, top: 2, bottom: 2 })
Text('ID: MH-PILOT-0887').fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ left: 8 })
}.margin({ top: 5 })
Text('注册 826 天 · 已认证专业机甲师').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 4 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
}.width('100%').backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 14 }).padding(16).margin({ top: 10 })
Column() {
Text('⚙️ 常用功能').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
.width('100%').padding({ left: 14, top: 12, bottom: 4 })
Column() {
this.settingRow('🤖', '我的机甲', '10台')
Divider().color(COLOR_BORDER)
this.settingRow('📍', '常用地址', '5个')
Divider().color(COLOR_BORDER)
this.settingRow('🛡️', '保额管理', '累计¥320万')
Divider().color(COLOR_BORDER)
this.settingRow('🎧', '客服', '7×24h')
Divider().color(COLOR_BORDER)
this.settingRow('🔧', '设置', '')
}.width('100%').backgroundColor(COLOR_CARD_LIGHT)
.border({ width: 1, color: COLOR_BORDER, radius: 12 }).margin({ top: 6, bottom: 8 })
}
Text('MECHA HAUL v2.6 · 赛博暗黑科技风 · 2026')
.fontSize(9).fontColor(COLOR_TEXT_HINT)
.alignSelf(ItemAlign.Center).margin({ top: 16, bottom: 16 })
}.padding({ left: 8, right: 8, top: 6 })
}.layoutWeight(1).scrollBar(BarState.Off)
}.width('100%').height('100%')
}
}
MeProfileContent 是个人中心页。顶部是用户资料卡:左边是圆形头像(用主色边框与浅底组合),右边是昵称"凌霄机甲师"、等级徽章"Lv.9 王牌机甲师"(主色底深色字)、用户 ID、注册天数与认证信息。下方是 2x2 的统计网格(总托运/总租赁/赛事参与/积分),每格用不同强调色区分。核心是"常用功能"设置列表,用 settingRow 这个带参 @Builder 渲染每一行——图标、标签、提示文案、右箭头">“四段式,行间用 Divider 分隔。点击任意行翻转 settingsExpanded 状态(此处仅作演示,实际可展开二级面板)。底部用 alignSelf(ItemAlign.Center) 居中显示版本信息"MECHA HAUL v2.6 · 赛博暗黑科技风 · 2026”。整个页面结构清晰,展示了用户中心常见的"资料卡 + 统计网格 + 功能列表 + 版本尾注"四段式布局范式。
四、弹框与状态联动流程
下面再用一张 Mermaid 图,专门刻画四个弹框的打开、填充与关闭流程,以及它们与主页面状态之间的联动关系。
上图揭示了弹框系统的两个关键机制:一是"打开前预填"——编辑弹框在打开前先把选中机甲的字段同步到 eCode、eModel 等 @State,租赁弹框在打开前重置 rentDays = 1,确保用户看到的表单带有正确初值;二是"条件挂载"——build 中的 if (showXxxModal) 让弹框在关闭时彻底从渲染树移除,而非仅隐藏,这与传统前端的 display:none 有本质区别,是 ArkTS 声明式条件渲染的性能优势所在。
五、关键技术点对比
下面用表格对比本工程中几组关键的技术选型与替代方案,帮助理解每种取舍的利弊。
| 技术点 | 本工程做法 | 替代做法 | 优劣分析 |
|---|---|---|---|
| 状态管理 | @State 单一主页持有,子组件通过回调上报 | 全局 AppStorage 或 @Provide/@Consume | 单页样例用 @State 足够直观;多页面共享数据时改用 AppStorage 更优 |
| 可观察数据 | @Observed class + implements interface | 直接用对象字面量 | class 有默认值与构造函数,便于批量构造;interface 提供类型契约;字面量更简洁但缺类型约束 |
| 状态配置 | Record<string, StatusMeta> 配置表 + 可选链兜底 | switch/case 或 if/else 分支 | 配置表新增状态只改一处数据;分支式修改需改多处逻辑,易遗漏 |
| 数组状态变更 | 纯函数返回新数组再整体赋值(toggleIdIn 等) | 直接 push/splice 原数组 | 整体赋新引用触发 @State 刷新;直接变异原数组框架可能不感知 |
| 定时器 | setInterval + aboutToDisappear 中 clearInterval | setTimeout 递归或动画 API | setInterval 简单直观;但 ArkTS 也提供 animateTo 等动画能力,粒子可改用属性动画更流畅 |
| 弹框层叠 | Stack 顶层 if 条件挂载 + zIndex(999) | if 改为 .visibility(Visibility.None) | 条件挂载关闭后彻底移除节点更省资源;visibility 隐藏仍占渲染树 |
| 柱状图 | Column 高度按数据换算 vp 字符串 | 引入图表库或 Canvas 绘制 | 纯组件拼装零依赖、可读性强;复杂图表仍建议用 Canvas 或图表组件 |
| 图标资源 | 全部用 Emoji 字符 | 本地 PNG/SVG 或资源文件 | Emoji 零资源依赖、跨平台一致;但样式受系统字体影响,正式产品建议用 SVG |
| 网格布局 | ForEach([0,2,4...]) 步进两列 + layoutWeight | Grid/GridItem 组件 | 步进写法灵活但不支持滚动;数据多时应用 Grid 获得虚拟滚动 |
| 排序 | 纯函数 getSortedRentals 浅拷贝后排序 | 组件内直接 mockRentals.sort() | 浅拷贝保护原数据不被污染;直接排序会改变全局 Mock,副作用大 |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

设置API为24的模板项目:

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

完整代码:
// ============================================================
// 机甲重载 MECHA HAUL — 人形机器人/机甲装备运输 + 机甲租赁平台
// 赛博暗黑科技风 · 单文件 ArkTS 页面
// Tab: 托运 / 机库 / 租赁 / 赛事 / 台账 / 我的
// 弹框: 预约托运 / 编辑档案 / 报废退役 / 租赁下单
// ============================================================
// ============ 颜色常量 ============
const COLOR_BG: string = '#0A0E14'
const COLOR_CARD: string = '#131926'
const COLOR_CARD_LIGHT: string = '#1B2334'
const COLOR_PRIMARY: string = '#00D4FF'
const COLOR_PRIMARY_LIGHT: string = '#66E5FF'
const COLOR_PINK: string = '#FF2E88'
const COLOR_WARN: string = '#FFC94D'
const COLOR_SUCCESS: string = '#3DFFA0'
const COLOR_PURPLE: string = '#8B5CF6'
const COLOR_TEXT_MAIN: string = '#E6F1FF'
const COLOR_TEXT_SUB: string = '#8FA3BF'
const COLOR_TEXT_HINT: string = '#4A5A74'
const COLOR_BORDER: string = '#223048'
const COLOR_DANGER: string = '#FF4D6A'
const PARTICLE_COLORS: string[] = ['#00D4FF', '#FF2E88', '#8B5CF6']
// ============ 数据模型 interface ============
interface ShipOrderModel {
id: number
orderNo: string
mechName: string
pickup: string
destination: string
weight: number
insured: number
status: string
date: string
}
interface MechaModel {
id: number
code: string
model: string
height: number
weight: number
status: string
color: string
tag: string
}
interface RentalMechModel {
id: number
model: string
dailyRate: number
deposit: number
available: boolean
rating: number
color: string
}
interface EventModel {
id: number
name: string
date: string
venue: string
prizePool: number
teamCount: number
status: string
color: string
}
interface TradeLogModel {
id: number
type: string
amount: number
date: string
status: string
}
// ============ @Observed class ============
@Observed
class ShipOrderItem implements ShipOrderModel {
id: number = 0
orderNo: string = ''
mechName: string = ''
pickup: string = ''
destination: string = ''
weight: number = 0
insured: number = 0
status: string = '待装载'
date: string = ''
constructor(id: number, orderNo: string, mechName: string, pickup: string, destination: string,
weight: number, insured: number, status: string, date: string) {
this.id = id; this.orderNo = orderNo; this.mechName = mechName
this.pickup = pickup; this.destination = destination
this.weight = weight; this.insured = insured; this.status = status; this.date = date
}
}
@Observed
class MechaItem implements MechaModel {
id: number = 0
code: string = ''
model: string = ''
height: number = 0
weight: number = 0
status: string = '待命'
color: string = '#00D4FF'
tag: string = ''
constructor(id: number, code: string, model: string, height: number, weight: number,
status: string, color: string, tag: string) {
this.id = id; this.code = code; this.model = model
this.height = height; this.weight = weight
this.status = status; this.color = color; this.tag = tag
}
}
@Observed
class RentalMechItem implements RentalMechModel {
id: number = 0
model: string = ''
dailyRate: number = 0
deposit: number = 0
available: boolean = true
rating: number = 5.0
color: string = '#00D4FF'
constructor(id: number, model: string, dailyRate: number, deposit: number,
available: boolean, rating: number, color: string) {
this.id = id; this.model = model; this.dailyRate = dailyRate
this.deposit = deposit; this.available = available
this.rating = rating; this.color = color
}
}
@Observed
class EventItem implements EventModel {
id: number = 0
name: string = ''
date: string = ''
venue: string = ''
prizePool: number = 0
teamCount: number = 0
status: string = '报名中'
color: string = '#00D4FF'
constructor(id: number, name: string, date: string, venue: string,
prizePool: number, teamCount: number, status: string, color: string) {
this.id = id; this.name = name; this.date = date; this.venue = venue
this.prizePool = prizePool; this.teamCount = teamCount
this.status = status; this.color = color
}
}
@Observed
class TradeLogItem implements TradeLogModel {
id: number = 0
type: string = ''
amount: number = 0
date: string = ''
status: string = '已完成'
constructor(id: number, type: string, amount: number, date: string, status: string) {
this.id = id; this.type = type; this.amount = amount; this.date = date; this.status = status
}
}
@Builder retireModal() {
Column() {
this.modalOverlay(() => { this.showRetireModal = false })
Column() {
Text('⚠️').fontSize(44).margin({ top: 22 })
Text('确认退役此机甲?').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
.margin({ top: 8 })
Text('退役后档案将锁定,恢复需联系平台工程师').fontSize(11).fontColor(COLOR_DANGER)
.margin({ top: 4 })
Row() {
Column() {
Text('🤖').fontSize(26)
}
.width(48).height(48).borderRadius(12)
.backgroundColor(this.selectedMecha?.color ?? COLOR_PRIMARY)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(this.selectedMecha?.code ?? '').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLOR_TEXT_MAIN)
Text(this.selectedMecha?.model ?? '').fontSize(10).fontColor(COLOR_TEXT_SUB)
.margin({ top: 2 })
Text('残值估值 ¥86,000').fontSize(10).fontColor(COLOR_WARN).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
}
.width('100%')
.backgroundColor(COLOR_CARD_LIGHT)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(12).margin({ top: 16 })
Row() {
Text('取消').fontSize(14).fontColor(COLOR_TEXT_SUB)
.backgroundColor(COLOR_CARD_LIGHT).borderRadius(20)
.padding({ left: 28, right: 28, top: 10, bottom: 10 })
.onClick(() => { this.showRetireModal = false })
Text('确认退役').fontSize(14).fontColor('#FFFFFF')
.backgroundColor(COLOR_DANGER).borderRadius(20)
.fontWeight(FontWeight.Bold)
.padding({ left: 28, right: 28, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => { this.showRetireModal = false })
}
.width('100%').justifyContent(FlexAlign.Center)
.padding({ top: 20, bottom: 22 })
}
.width('80%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_DANGER, radius: 16 })
.alignItems(HorizontalAlign.Center)
.position({ x: '10%', y: '32%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
// ========== 弹框4:机甲租赁下单(订单式) ==========
@Builder rentalOrderModal() {
Column() {
this.modalOverlay(() => { this.showRentalModal = false })
Column() {
Row() {
Text('📋 租赁下单').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_PINK)
Column().layoutWeight(1)
Text('✕').fontSize(16).fontColor(COLOR_TEXT_SUB)
.width(28).height(28).borderRadius(14)
.backgroundColor(COLOR_CARD_LIGHT).textAlign(TextAlign.Center)
.onClick(() => { this.showRentalModal = false })
}
.width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
Scroll() {
Column() {
Row() {
Column() {
Text('🤖').fontSize(30)
}
.width(58).height(58).borderRadius(14)
.backgroundColor(this.selectedRental?.color ?? COLOR_PINK)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(this.selectedRental?.model ?? '').fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(COLOR_TEXT_MAIN)
Row() {
Text('日租金').fontSize(10).fontColor(COLOR_TEXT_SUB)
Text(formatMoney(this.selectedRental?.dailyRate ?? 0))
.fontSize(13).fontColor(COLOR_PRIMARY).fontWeight(FontWeight.Bold)
.margin({ left: 4 })
Text('押金').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ left: 10 })
Text(formatMoney(this.selectedRental?.deposit ?? 0))
.fontSize(12).fontColor(COLOR_WARN).margin({ left: 4 })
}
.margin({ top: 4 })
Text('评分 ' + (this.selectedRental?.rating ?? 5.0).toFixed(1) + ' ★')
.fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
}
.width('100%')
Text('租赁天数').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 14 })
Row() {
Text('−').fontSize(20).fontColor(COLOR_TEXT_SUB)
.width(36).height(36).borderRadius(18)
.backgroundColor(COLOR_CARD_LIGHT).textAlign(TextAlign.Center)
.onClick(() => {
if (this.rentDays > 1) {
this.rentDays -= 1
}
})
Text(this.rentDays.toString() + ' 天')
.fontSize(16).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
.width(90).textAlign(TextAlign.Center)
Text('+').fontSize(20).fontColor(COLOR_PINK)
.width(36).height(36).borderRadius(18)
.backgroundColor('#33101F').textAlign(TextAlign.Center)
.onClick(() => {
if (this.rentDays < 30) {
this.rentDays += 1
}
})
}
.width('100%').justifyContent(FlexAlign.Center)
.margin({ top: 6 })
Text('取送方式').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 14 })
Row() {
ForEach(DELIVERY_OPTIONS, (d: string) => {
if (this.rentDelivery === d) {
Text('🚚 ' + d).fontSize(11).fontColor(COLOR_BG).backgroundColor(COLOR_PINK)
.fontWeight(FontWeight.Bold)
.padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14)
.margin({ left: 4, right: 4 })
} else {
Text('🚚 ' + d).fontSize(11).fontColor(COLOR_TEXT_SUB)
.backgroundColor(COLOR_CARD_LIGHT)
.padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14)
.margin({ left: 4, right: 4 })
.onClick(() => { this.rentDelivery = d })
}
})
}
.width('100%').margin({ top: 6 })
Text('保险选项').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 14 })
if (this.rentInsurance) {
Text('☑ 全额设备险(租金的8%,运输损坏免赔)')
.fontSize(12).fontColor(COLOR_SUCCESS)
.backgroundColor('#0B3324')
.border({ width: 1, color: COLOR_SUCCESS, radius: 10 })
.padding({ left: 12, right: 12, top: 8, bottom: 8 }).borderRadius(10)
.margin({ top: 6 })
.onClick(() => { this.rentInsurance = false })
} else {
Text('☐ 全额设备险(租金的8%,运输损坏免赔)')
.fontSize(12).fontColor(COLOR_TEXT_SUB)
.backgroundColor(COLOR_CARD_LIGHT)
.padding({ left: 12, right: 12, top: 8, bottom: 8 }).borderRadius(10)
.margin({ top: 6 })
.onClick(() => { this.rentInsurance = true })
}
Column() {
Text('费用明细').fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
.width('100%')
Row() {
Text('租金 (' + this.rentDays.toString() + '天 × ' +
formatMoney(this.selectedRental?.dailyRate ?? 0) + ')')
.fontSize(11).fontColor(COLOR_TEXT_SUB).layoutWeight(1)
Text(formatMoney(calcRentFee(this.selectedRental?.dailyRate ?? 0, this.rentDays)))
.fontSize(11).fontColor(COLOR_TEXT_MAIN)
}
.width('100%').margin({ top: 8 })
Row() {
Text('押金(还机退还)').fontSize(11).fontColor(COLOR_TEXT_SUB).layoutWeight(1)
Text(formatMoney(this.selectedRental?.deposit ?? 0))
.fontSize(11).fontColor(COLOR_WARN)
}
.width('100%').margin({ top: 6 })
Row() {
Text('保险费').fontSize(11).fontColor(COLOR_TEXT_SUB).layoutWeight(1)
Text(formatMoney(calcInsurance(this.rentInsurance,
this.selectedRental?.dailyRate ?? 0, this.rentDays)))
.fontSize(11).fontColor(COLOR_SUCCESS)
}
.width('100%').margin({ top: 6 })
Divider().color(COLOR_BORDER).margin({ top: 8 })
Row() {
Text('合计').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
.layoutWeight(1)
Text(formatMoney(calcRentFee(this.selectedRental?.dailyRate ?? 0, this.rentDays) +
(this.selectedRental?.deposit ?? 0) +
calcInsurance(this.rentInsurance, this.selectedRental?.dailyRate ?? 0, this.rentDays)))
.fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLOR_PINK)
}
.width('100%').margin({ top: 8 })
}
.width('100%')
.backgroundColor(COLOR_CARD_LIGHT)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(12).margin({ top: 14 })
}
.padding({ left: 16, right: 16, bottom: 12 })
}
.layoutWeight(1).scrollBar(BarState.Off)
Text('立即租用').fontSize(15).fontColor('#FFFFFF')
.backgroundColor(COLOR_PINK).borderRadius(22)
.fontWeight(FontWeight.Bold)
.padding({ left: 60, right: 60, top: 12, bottom: 12 })
.margin({ bottom: 16 })
.onClick(() => { this.showRentalModal = false })
}
.width('92%').height('70%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_PINK, radius: 18 })
.alignItems(HorizontalAlign.Start)
.position({ x: '4%', y: '14%' })
}
.width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}
build() {
Stack() {
// 主内容层
Column() {
this.headerBuilder()
this.contentArea()
this.bottomTabBar()
}
.width('100%').height('100%')
.backgroundColor(COLOR_BG)
// 粒子特效层(不阻挡点击)
this.particleLayer()
// 弹框层
if (this.showBookingModal) { this.bookingModal() }
if (this.showEditModal) { this.editModal() }
if (this.showRetireModal) { this.retireModal() }
if (this.showRentalModal) { this.rentalOrderModal() }
}
.width('100%').height('100%')
}
}
// ============ Tab1 托运 BOOKING ============
@Component
struct MeBookingContent {
onBook: () => void = () => {}
@State formMech: string = ''
@State formWeight: string = ''
@State formPack: string = '防震'
@State formFrom: string = ''
@State formTo: string = ''
weekTons: number[] = [8.2, 12.5, 6.8, 15.3, 9.7, 18.6, 11.2]
weekDays: string[] = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
@Builder orderCard(o: ShipOrderItem) {
Column() {
Row() {
Text('🚛 ' + o.orderNo).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Column().layoutWeight(1)
Text(ORDER_STATUS_CONFIG[o.status]?.label ?? o.status)
.fontSize(10).fontColor(ORDER_STATUS_CONFIG[o.status]?.color ?? COLOR_TEXT_SUB)
.backgroundColor(ORDER_STATUS_CONFIG[o.status]?.bg ?? COLOR_CARD_LIGHT)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(9)
}
.width('100%')
Row() {
Text('🤖 ' + o.mechName).fontSize(11).fontColor(COLOR_PRIMARY)
Text(o.weight.toFixed(1) + 't').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ left: 8 })
Text('保额 ' + formatMoney(o.insured)).fontSize(10).fontColor(COLOR_TEXT_HINT)
.margin({ left: 8 })
}
.width('100%').margin({ top: 5 })
Row() {
Text('📍 ' + o.pickup).fontSize(10).fontColor(COLOR_TEXT_SUB)
Text('→').fontSize(10).fontColor(COLOR_PRIMARY).margin({ left: 6, right: 6 })
Text(o.destination).fontSize(10).fontColor(COLOR_TEXT_SUB)
Column().layoutWeight(1)
Text(o.date).fontSize(10).fontColor(COLOR_TEXT_HINT)
}
.width('100%').margin({ top: 4 })
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(12).margin({ top: 8 })
}
build() {
Column() {
Scroll() {
Column() {
// 顶部统计条
Row() {
Column() {
Text('12').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
Text('本月托运').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('3').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLOR_WARN)
Text('在途').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('9').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLOR_SUCCESS)
Text('已交付').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('86.4t').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLOR_PINK)
Text('总吨位').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding({ top: 12, bottom: 12 })
// 运输环境监控卡
Column() {
Row() {
Text('🛰️ 货舱环境实时监控').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Column().layoutWeight(1)
Text('● LIVE').fontSize(9).fontColor(COLOR_SUCCESS)
}
.width('100%')
Row() {
Column() {
Text('🌡️').fontSize(14)
Text('18℃').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
.margin({ top: 2 })
Text('货舱温度').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('💧').fontSize(14)
Text('42%').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY_LIGHT)
.margin({ top: 2 })
Text('湿度').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('🛡️').fontSize(14)
Text('A+').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLOR_WARN)
.margin({ top: 2 })
Text('防震等级').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('📡').fontSize(14)
Text('在线').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLOR_SUCCESS)
.margin({ top: 2 })
Text('GPS定位').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
.width('100%').margin({ top: 10 })
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_PRIMARY, radius: 12 })
.padding(14).margin({ top: 10 })
// 托运表单卡
Column() {
Text('⚡ 快速下单').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
.width('100%')
Text('机甲型号').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 10 })
TextInput({ placeholder: '如 泰坦-T7' })
.placeholderColor(COLOR_TEXT_HINT).fontSize(13).width('100%')
.fontColor(COLOR_TEXT_MAIN)
.backgroundColor(COLOR_CARD_LIGHT).borderRadius(8)
.margin({ top: 4 })
.onChange((v: string) => { this.formMech = v })
Text('重量(吨)').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 10 })
TextInput({ placeholder: '如 2.8' })
.placeholderColor(COLOR_TEXT_HINT).fontSize(13).width('100%')
.fontColor(COLOR_TEXT_MAIN)
.backgroundColor(COLOR_CARD_LIGHT).borderRadius(8)
.margin({ top: 4 })
.onChange((v: string) => { this.formWeight = v })
Text('包装等级').fontSize(11).fontColor(COLOR_TEXT_SUB).width('100%').margin({ top: 10 })
Row() {
ForEach(PACK_LEVELS, (p: string) => {
if (this.formPack === p) {
Text((PACK_CONFIG[p]?.icon ?? '') + ' ' + p)
.fontSize(11).fontColor(COLOR_BG).backgroundColor(COLOR_PRIMARY)
.fontWeight(FontWeight.Bold)
.padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
.margin({ left: 3, right: 3 })
} else {
Text((PACK_CONFIG[p]?.icon ?? '') + ' ' + p)
.fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD_LIGHT)
.padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
.margin({ left: 3, right: 3 })
.onClick(() => { this.formPack = p })
}
})
}
.width('100%').margin({ top: 4 })
Row() {
Column() {
Text('出发地').fontSize(11).fontColor(COLOR_TEXT_SUB)
TextInput({ placeholder: '上海临港' })
.placeholderColor(COLOR_TEXT_HINT).fontSize(12).width('100%')
.fontColor(COLOR_TEXT_MAIN)
.backgroundColor(COLOR_CARD_LIGHT).borderRadius(8)
.margin({ top: 4 })
.onChange((v: string) => { this.formFrom = v })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Column() {
Text('目的地').fontSize(11).fontColor(COLOR_TEXT_SUB)
TextInput({ placeholder: '北京亦庄' })
.placeholderColor(COLOR_TEXT_HINT).fontSize(12).width('100%')
.fontColor(COLOR_TEXT_MAIN)
.backgroundColor(COLOR_CARD_LIGHT).borderRadius(8)
.margin({ top: 4, left: 8 })
.onChange((v: string) => { this.formTo = v })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
}
.width('100%').margin({ top: 10 })
Text('预约托运').fontSize(14).fontColor(COLOR_BG)
.backgroundColor(COLOR_PRIMARY).borderRadius(20)
.fontWeight(FontWeight.Bold)
.padding({ left: 0, right: 0, top: 10, bottom: 10 })
.width('100%').textAlign(TextAlign.Center)
.margin({ top: 14 })
.onClick(() => { this.onBook() })
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(14).margin({ top: 10 })
// 本周托运吨位柱状图
Column() {
Text('📊 本周托运吨位').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
.width('100%')
Row() {
ForEach(this.weekTons, (t: number, d: number) => {
Column() {
Text(t.toFixed(1)).fontSize(9).fontColor(COLOR_PRIMARY).margin({ bottom: 3 })
Column()
.width(20)
.height((t / 20 * 70).toFixed(0) + 'vp')
.backgroundColor(d === 5 ? COLOR_PINK : COLOR_PRIMARY)
.borderRadius({ topLeft: 3, topRight: 3 })
Text(this.weekDays[d]).fontSize(8).fontColor(COLOR_TEXT_HINT).margin({ top: 3 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
})
}
.width('100%').margin({ top: 10 })
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(14).margin({ top: 10 })
// 托运单列表
Row() {
Text('🧾 托运单').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Column().layoutWeight(1)
Text('共8单').fontSize(10).fontColor(COLOR_TEXT_HINT)
}
.width('100%').margin({ top: 14, bottom: 2 })
ForEach(mockOrders, (o: ShipOrderItem) => {
this.orderCard(o)
})
}
.padding({ left: 12, right: 12, top: 10, bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
}
}
// ============ Tab2 机库 MECHAS ============
@Component
struct MeMechasContent {
onEdit: (m: MechaItem) => void = (m: MechaItem) => {}
onRetire: (m: MechaItem) => void = (m: MechaItem) => {}
@Builder showcaseCard(m: MechaItem) {
Column() {
Column().layoutWeight(1)
Text('🤖').fontSize(34)
Column().layoutWeight(1)
Text(m.code).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('NO.MH-' + m.id.toString().padStart(3, '0'))
.fontSize(9).fontColor('#FFFFFF').opacity(0.8).margin({ top: 2 })
}
.width(200).height(130)
.borderRadius(16)
.linearGradient({
angle: 135,
colors: [[m.color, 0.0], ['#1B2334', 1.0]]
})
.alignItems(HorizontalAlign.Center)
.padding({ top: 12, bottom: 12 })
.margin({ left: 5, right: 5 })
}
@Builder mechaGridCard(m: MechaItem) {
Column() {
Column() {
Text('🤖').fontSize(26)
}
.width('100%').height(64).borderRadius(10)
.backgroundColor(m.color)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Text(m.code).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
.margin({ top: 6 })
Text(m.model).fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ top: 2 })
Row() {
Text('高 ' + m.height.toString() + 'm').fontSize(9).fontColor(COLOR_TEXT_SUB)
Text('重 ' + m.weight.toString() + 't').fontSize(9).fontColor(COLOR_TEXT_SUB)
.margin({ left: 6 })
}
.margin({ top: 3 })
Text(MECHA_STATUS_CONFIG[m.status]?.label ?? m.status)
.fontSize(9).fontColor(MECHA_STATUS_CONFIG[m.status]?.color ?? COLOR_TEXT_SUB)
.backgroundColor(MECHA_STATUS_CONFIG[m.status]?.bg ?? COLOR_CARD_LIGHT)
.padding({ left: 8, right: 8, top: 2, bottom: 2 }).borderRadius(8)
.margin({ top: 5 })
Row() {
Text('管理').fontSize(11).fontColor(COLOR_BG)
.backgroundColor(COLOR_PRIMARY).borderRadius(10)
.padding({ left: 14, right: 14, top: 5, bottom: 5 })
.onClick(() => { this.onEdit(m) })
Text('退役').fontSize(11).fontColor(COLOR_DANGER)
.border({ width: 1, color: COLOR_DANGER, radius: 10 })
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.margin({ left: 6 })
.onClick(() => { this.onRetire(m) })
}
.margin({ top: 8 })
}
.layoutWeight(1)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(10).margin({ left: 3, right: 3, top: 8 })
.alignItems(HorizontalAlign.Center)
}
build() {
Column() {
Scroll() {
Column() {
// 横滑大型展示卡
Scroll() {
Row() {
this.showcaseCard(mockMechas[0])
this.showcaseCard(mockMechas[4])
this.showcaseCard(mockMechas[6])
}
.padding({ left: 7, right: 7 })
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)
.width('100%')
// 状态分布3格统计
Row() {
Column() {
Text('5').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLOR_SUCCESS)
Text('待命').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding({ top: 10, bottom: 10 }).margin({ left: 4, right: 4, top: 12 })
Column() {
Text('3').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
Text('出租中').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding({ top: 10, bottom: 10 }).margin({ left: 4, right: 4, top: 12 })
Column() {
Text('2').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLOR_WARN)
Text('维护中').fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding({ top: 10, bottom: 10 }).margin({ left: 4, right: 4, top: 12 })
}
.width('100%')
Row() {
Text('🤖 机甲档案').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Column().layoutWeight(1)
Text('10台').fontSize(10).fontColor(COLOR_TEXT_HINT)
}
.width('100%').margin({ top: 14, bottom: 2 })
// 2列网格
ForEach([0, 2, 4, 6, 8], (start: number) => {
Row() {
this.mechaGridCard(mockMechas[start])
this.mechaGridCard(mockMechas[start + 1])
}
.width('100%')
})
}
.padding({ left: 8, right: 8, top: 10, bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
}
}
// ============ Tab3 租赁 RENTAL ============
@Component
struct MeRentalContent {
onRent: (r: RentalMechItem) => void = (r: RentalMechItem) => {}
@State sortKey: string = '热门'
@Builder rentalCard(r: RentalMechItem) {
Row() {
Column() {
Text('🤖').fontSize(28)
}
.width(84).height(84).borderRadius(12)
.backgroundColor(r.color)
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text(r.model).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Row() {
Text(formatMoney(r.dailyRate)).fontSize(15).fontWeight(FontWeight.Bold)
.fontColor(COLOR_PRIMARY)
Text('/日').fontSize(9).fontColor(COLOR_TEXT_HINT)
Text('押金 ' + formatMoney(r.deposit)).fontSize(10).fontColor(COLOR_TEXT_SUB)
.margin({ left: 8 })
}
.margin({ top: 4 })
Row() {
Text('★ ' + r.rating.toFixed(1)).fontSize(10).fontColor(COLOR_WARN)
if (r.available) {
Text('可租').fontSize(9).fontColor(COLOR_SUCCESS)
.backgroundColor('#0B3324')
.padding({ left: 8, right: 8, top: 2, bottom: 2 }).borderRadius(8)
.margin({ left: 8 })
} else {
Text('已租满').fontSize(9).fontColor(COLOR_DANGER)
.backgroundColor('#331016')
.padding({ left: 8, right: 8, top: 2, bottom: 2 }).borderRadius(8)
.margin({ left: 8 })
}
}
.margin({ top: 4 })
Text(r.available ? '租用' : '预约候补')
.fontSize(12).fontColor(COLOR_BG)
.backgroundColor(r.available ? COLOR_PINK : COLOR_WARN)
.borderRadius(14)
.fontWeight(FontWeight.Bold)
.padding({ left: 22, right: 22, top: 6, bottom: 6 })
.margin({ top: 6 })
.onClick(() => { this.onRent(r) })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 14 })
.padding(12).margin({ top: 8 })
}
build() {
Column() {
// 排序药丸
Row() {
ForEach(RENTAL_SORT_OPTIONS, (s: string) => {
if (this.sortKey === s) {
Text(s).fontSize(11).fontColor(COLOR_BG).backgroundColor(COLOR_PINK)
.fontWeight(FontWeight.Bold)
.padding({ left: 18, right: 18, top: 6, bottom: 6 }).borderRadius(14)
.margin({ left: 4, right: 4 })
} else {
Text(s).fontSize(11).fontColor(COLOR_TEXT_SUB).backgroundColor(COLOR_CARD)
.padding({ left: 18, right: 18, top: 6, bottom: 6 }).borderRadius(14)
.margin({ left: 4, right: 4 })
.onClick(() => { this.sortKey = s })
}
})
Column().layoutWeight(1)
Text('8 台可租').fontSize(10).fontColor(COLOR_TEXT_HINT)
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 4 })
Scroll() {
Column() {
ForEach(getSortedRentals(this.sortKey), (r: RentalMechItem) => {
this.rentalCard(r)
})
}
.padding({ left: 12, right: 12, bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
}
}
// ============ Tab4 赛事 EVENT ============
@Component
struct MeEventContent {
@State joinedIds: number[] = []
@Builder eventCard(e: EventItem) {
Column() {
Column() {
Text('🏆').fontSize(30)
Column().layoutWeight(1)
Row() {
Text('奖金池 ' + formatWan(e.prizePool)).fontSize(10).fontColor('#FFFFFF')
.backgroundColor('rgba(10,14,20,0.55)')
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
Text(EVENT_STATUS_CONFIG[e.status]?.label ?? e.status).fontSize(10)
.fontColor(EVENT_STATUS_CONFIG[e.status]?.color ?? COLOR_TEXT_SUB)
.backgroundColor('rgba(10,14,20,0.55)')
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(8)
.margin({ left: 6 })
}
.width('100%').justifyContent(FlexAlign.End)
.padding(8)
}
.width('100%').height(84).borderRadius(12)
.linearGradient({ angle: 120, colors: [[e.color, 0.0], ['#1B2334', 1.0]] })
.alignItems(HorizontalAlign.Center)
Text(e.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
.margin({ top: 8 })
Row() {
Text('📅 ' + e.date).fontSize(10).fontColor(COLOR_TEXT_SUB)
Text('📍 ' + e.venue).fontSize(10).fontColor(COLOR_TEXT_SUB).margin({ left: 10 })
}
.margin({ top: 4 })
// 报名进度条
Column() {
Row() {
Text('报名队伍 ' + e.teamCount.toString() + '/64')
.fontSize(9).fontColor(COLOR_TEXT_SUB).layoutWeight(1)
Text(getEventCapacity(e.teamCount).toString() + '%')
.fontSize(9).fontColor(COLOR_PRIMARY)
}
.width('100%')
Row() {
Column()
.width(getEventCapacity(e.teamCount).toString() + '%')
.height(4).backgroundColor(e.color).borderRadius(2)
Column().layoutWeight(1)
}
.width('100%').height(4)
.backgroundColor(COLOR_CARD_LIGHT).borderRadius(2).margin({ top: 4 })
}
.width('100%').margin({ top: 8 })
Row() {
Text(this.joinedIds.indexOf(e.id) >= 0 ? '✅ 已报名' : '报名')
.fontSize(12).fontColor(this.joinedIds.indexOf(e.id) >= 0 ? COLOR_SUCCESS : COLOR_BG)
.backgroundColor(this.joinedIds.indexOf(e.id) >= 0 ? '#0B3324' : COLOR_PRIMARY)
.borderRadius(14)
.fontWeight(FontWeight.Bold)
.padding({ left: 28, right: 28, top: 6, bottom: 6 })
Column().layoutWeight(1)
Text('详情 >').fontSize(10).fontColor(COLOR_TEXT_HINT)
}
.width('100%').margin({ top: 10 })
.onClick(() => {
this.joinedIds = toggleIdIn(this.joinedIds, e.id)
})
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 14 })
.padding(12).margin({ top: 10 })
.alignItems(HorizontalAlign.Start)
}
build() {
Column() {
Scroll() {
Column() {
// 本月赛事数大数字
Row() {
Column() {
Text('6').fontSize(38).fontWeight(FontWeight.Bold).fontColor(COLOR_WARN)
Text('本月赛事').fontSize(11).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
Column().layoutWeight(1)
Column() {
Text('4').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
Text('可报名').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.alignItems(HorizontalAlign.Center)
Column() {
Text('1').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLOR_PINK)
Text('进行中').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.alignItems(HorizontalAlign.Center)
.margin({ left: 20 })
Column() {
Text('263').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLOR_SUCCESS)
Text('总报名队伍').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.alignItems(HorizontalAlign.Center)
.margin({ left: 20 })
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(16).margin({ top: 10 })
ForEach(mockEvents, (e: EventItem) => {
this.eventCard(e)
})
}
.padding({ left: 12, right: 12, bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
}
}
// ============ Tab5 台账 LOG ============
@Component
struct MeLogContent {
incomeData: number[] = [6.2, 7.8, 5.4, 9.1, 8.3, 10.6]
expenseData: number[] = [5.1, 6.5, 4.9, 7.2, 6.8, 8.4]
monthLabels: string[] = ['3月', '4月', '5月', '6月', '7月', '8月']
@Builder tradeRow(t: TradeLogItem) {
Row() {
Column() {
Text(t.type).fontSize(12).fontWeight(FontWeight.Medium).fontColor(COLOR_TEXT_MAIN)
Text(t.date).fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text((t.amount >= 0 ? '+' : '') + formatMoney(t.amount))
.fontSize(13).fontWeight(FontWeight.Bold)
.fontColor(t.type.indexOf('支出') >= 0 ? COLOR_DANGER : COLOR_SUCCESS)
Text(t.status).fontSize(9).fontColor(COLOR_TEXT_HINT).margin({ top: 3 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 10 })
.padding({ left: 12, right: 12, top: 10, bottom: 10 }).margin({ top: 6 })
}
build() {
Column() {
Scroll() {
Column() {
// 2x2 财务统计大数字
Row() {
Column() {
Text('总支出').fontSize(10).fontColor(COLOR_TEXT_SUB)
Text('¥128,400').fontSize(19).fontWeight(FontWeight.Bold).fontColor(COLOR_DANGER)
.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(14).margin({ left: 4, right: 4, top: 8 })
Column() {
Text('总收入').fontSize(10).fontColor(COLOR_TEXT_SUB)
Text('¥96,200').fontSize(19).fontWeight(FontWeight.Bold).fontColor(COLOR_SUCCESS)
.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(14).margin({ left: 4, right: 4, top: 8 })
}
.width('100%')
Row() {
Column() {
Text('待结算').fontSize(10).fontColor(COLOR_TEXT_SUB)
Text('¥18,600').fontSize(19).fontWeight(FontWeight.Bold).fontColor(COLOR_WARN)
.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(14).margin({ left: 4, right: 4, top: 8 })
Column() {
Text('押金托管').fontSize(10).fontColor(COLOR_TEXT_SUB)
Text('¥56,000').fontSize(19).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
.margin({ top: 3 })
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(14).margin({ left: 4, right: 4, top: 8 })
}
.width('100%')
// 月度收支柱状图(青粉双色)
Column() {
Text('📊 月度收支(万元)').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
.width('100%')
Row() {
Text('■ 收入').fontSize(9).fontColor(COLOR_PRIMARY)
Text('■ 支出').fontSize(9).fontColor(COLOR_PINK).margin({ left: 10 })
}
.margin({ top: 6 })
Row() {
ForEach(this.monthLabels, (m: string, i: number) => {
Column() {
Row() {
Column()
.width(9)
.height((this.incomeData[i] / 12 * 80).toFixed(0) + 'vp')
.backgroundColor(COLOR_PRIMARY)
.borderRadius({ topLeft: 3, topRight: 3 })
Column()
.width(9)
.height((this.expenseData[i] / 12 * 80).toFixed(0) + 'vp')
.backgroundColor(COLOR_PINK)
.borderRadius({ topLeft: 3, topRight: 3 })
.margin({ left: 3 })
}
Text(m).fontSize(8).fontColor(COLOR_TEXT_HINT).margin({ top: 4 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.End)
})
}
.width('100%').height(110).margin({ top: 8 })
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding(14).margin({ top: 12 })
// 交易记录
Row() {
Text('🧾 交易记录').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Column().layoutWeight(1)
Text('本月6笔').fontSize(10).fontColor(COLOR_TEXT_HINT)
}
.width('100%').margin({ top: 14, bottom: 2 })
ForEach(mockTrades, (t: TradeLogItem) => {
this.tradeRow(t)
})
}
.padding({ left: 8, right: 8, top: 6, bottom: 20 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
}
}
// ============ Tab6 我的 PROFILE ============
@Component
struct MeProfileContent {
@State settingsExpanded: boolean = false
@Builder settingRow(icon: string, label: string, hint: string) {
Row() {
Text(icon).fontSize(17)
Text(label).fontSize(13).fontColor(COLOR_TEXT_MAIN).layoutWeight(1).margin({ left: 12 })
Text(hint).fontSize(10).fontColor(COLOR_TEXT_HINT)
Text('>').fontSize(12).fontColor(COLOR_TEXT_HINT).margin({ left: 6 })
}
.width('100%')
.padding({ top: 13, bottom: 13, left: 14, right: 14 })
.onClick(() => { this.settingsExpanded = !this.settingsExpanded })
}
build() {
Column() {
Scroll() {
Column() {
// 资料卡
Row() {
Column() {
Text('🧑🚀').fontSize(34)
}
.width(62).height(62).borderRadius(31)
.backgroundColor(COLOR_CARD_LIGHT)
.border({ width: 1, color: COLOR_PRIMARY, radius: 31 })
.alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
Column() {
Text('凌霄机甲师').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
Row() {
Text('Lv.9 王牌机甲师').fontSize(10).fontColor(COLOR_BG)
.backgroundColor(COLOR_PRIMARY).borderRadius(8)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
Text('ID: MH-PILOT-0887').fontSize(9).fontColor(COLOR_TEXT_HINT)
.margin({ left: 8 })
}
.margin({ top: 5 })
Text('注册 826 天 · 已认证专业机甲师').fontSize(10).fontColor(COLOR_TEXT_SUB)
.margin({ top: 4 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 14 })
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 14 })
.padding(16).margin({ top: 10 })
// 统计网格 2x2
Row() {
Column() {
Text('47').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLOR_PRIMARY)
Text('总托运').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding({ top: 12, bottom: 12 }).margin({ left: 4, right: 4, top: 10 })
Column() {
Text('23').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLOR_PINK)
Text('总租赁').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding({ top: 12, bottom: 12 }).margin({ left: 4, right: 4, top: 10 })
}
.width('100%')
Row() {
Column() {
Text('12').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLOR_WARN)
Text('赛事参与').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding({ top: 12, bottom: 12 }).margin({ left: 4, right: 4, top: 8 })
Column() {
Text('8,920').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLOR_SUCCESS)
Text('积分').fontSize(9).fontColor(COLOR_TEXT_SUB).margin({ top: 2 })
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.padding({ top: 12, bottom: 12 }).margin({ left: 4, right: 4, top: 8 })
}
.width('100%')
// 设置列表
Column() {
Text('⚙️ 常用功能').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLOR_TEXT_MAIN)
.width('100%').padding({ left: 14, top: 12, bottom: 4 })
Column() {
this.settingRow('🤖', '我的机甲', '10台')
Divider().color(COLOR_BORDER)
this.settingRow('📍', '常用地址', '5个')
Divider().color(COLOR_BORDER)
this.settingRow('🛡️', '保额管理', '累计¥320万')
Divider().color(COLOR_BORDER)
this.settingRow('🎧', '客服', '7×24h')
Divider().color(COLOR_BORDER)
this.settingRow('🔧', '设置', '')
}
.width('100%')
.backgroundColor(COLOR_CARD_LIGHT)
.border({ width: 1, color: COLOR_BORDER, radius: 12 })
.margin({ top: 6, bottom: 8 })
}
.width('100%')
.backgroundColor(COLOR_CARD)
.border({ width: 1, color: COLOR_BORDER, radius: 14 })
.padding({ bottom: 12 })
.margin({ top: 12 })
Text('MECHA HAUL v2.6 · 赛博暗黑科技风 · 2026')
.fontSize(9).fontColor(COLOR_TEXT_HINT)
.alignSelf(ItemAlign.Center).margin({ top: 16, bottom: 16 })
}
.padding({ left: 8, right: 8, top: 6 })
}
.layoutWeight(1).scrollBar(BarState.Off)
}
.width('100%').height('100%')
}
}
六、总结
「机甲重载 MECHA HAUL」这份单文件 ArkTS 工程,以 HarmonyOS 6.1.1 为运行底座、基于 HarmonyOS API 24 的声明式范式,完整展示了一个垂直行业应用从数据建模到视觉呈现的全链路实现。它用五个 interface 与五个 @Observed class 搭建了托运、机甲、租赁、赛事、交易五类实体的类型与可观察层,用 Record 配置表把状态到视觉的映射收敛为数据驱动,用一组顶层纯函数承载费用计算、排序、数组切换等可复用逻辑,结构层次分明、职责边界清晰。整个工程没有引入任何第三方库,柱状图、进度条、药丸选择器、渐变展示卡、粒子特效等视觉元素全部用 ArkTS 内置组件拼装而成,体现了声明式 UI 在纯代码绘图上的强大表现力。

在交互与状态管理上,工程展示了 ArkTS 的几项核心能力:@State 对基础类型、数组、可空对象的响应式观测;aboutToAppear/aboutToDisappear 生命周期对定时器的安全启停;@Builder 方法对头部、内容区、底部栏、粒子层、四个弹框的模块化拆分;子组件通过回调函数把事件上抛给主页面、再由主页面统一调度弹框显隐的单向数据流。特别是弹框系统,采用 Stack 顶层 if 条件挂载的写法,让弹框关闭后彻底从渲染树移除,既节省资源又避免残影;编辑弹框在打开前把选中对象字段同步到表单状态、租赁弹框在打开前重置天数,展示了"预填初值"的细节意识。费用明细卡把纯函数直接用在模板中实现实时计算,是声明式响应式的精髓演示。
从工程实践角度,这份样例也为真实项目提供了多处可借鉴与可改进的启示。颜色常量全量抽为 const 保证了视觉一致性,但更大型工程可考虑用 @Style 或资源文件统一管理;Mock 数据以 const 数组形式存在,接入真实后端时只需替换为网络请求与持久化;hitTestBehavior(HitTestMode.None) 让粒子覆盖层不打断交互,是覆盖层的必备技巧;可选链 ?. 与空合并 ?? 的兜底写法值得在所有配置表消费处推广。同时也有可优化空间:网格用 ForEach 步进写法不支持虚拟滚动,数据量大时应换 Grid;粒子用 setInterval 驱动虽简单,但改用 animateTo 属性动画会更流畅且无需手动管理定时器;状态字段用 string 而非联合字面量,换取了配置表匹配的灵活性,但牺牲了编译期类型检查。
涵盖六大业务 Tab、四类弹框、十余组状态、五类数据模型、八组纯函数,却始终保持着清晰的分层与可读性。对于希望深入理解 HarmonyOS API 24 声明式 UI 的开发者,它提供了从数据建模、状态管理、生命周期、构建器拆分到布局装配、条件渲染、配置驱动的一条龙参考;对于希望构建行业应用的产品团队,它展示了一种"纯代码绘图 + 单文件分发"的快速原型范式。在 HarmonyOS 生态持续演进的当下,掌握这些底层范式,将帮助开发者更从容地应对从样例到产品的工程化跨越。
更多推荐



所有评论(0)