一、开头技术描述

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

在这里插入图片描述

在技术栈层面,该工程大量使用了 HarmonyOS API 24 提供的核心装饰器与组件能力。状态管理上,主页面通过 @Entry @Component 暴露为入口,内部以 @State 管理当前激活 Tab、弹框显隐、选中对象、表单字段、粒子特效坐标等十余组状态;数据模型则用 @Observed 修饰,使 ShipOrderItemMechaItemRentalMechItemEventItemTradeLogItem 等类的实例具备可观察能力,配合 ForEach 渲染实现数据驱动视图。布局上采用 ColumnRowStackScroll 等容器组件,通过 layoutWeightpositionzIndex 等属性完成头部、内容区、底部 Tab 栏、粒子特效层、弹框层的层叠与分区。

架构设计上,页面遵循"一个入口主页面 + 六个 Tab 子组件 + 四个弹框 Builder"的清晰分层。主页面 MechaHaulApp 通过 activeTab 状态在 contentArea 中按枚举分发渲染 MeBookingContentMeMechasContentMeRentalContentMeEventContentMeLogContentMeProfileContent 六个独立 @Component;弹框则通过 showBookingModalshowEditModalshowRetireModalshowRentalModal 四个布尔状态在 build() 顶层 Stack 中条件挂载,由 @Builder 方法绘制遮罩与卡片。子组件与主页面之间通过回调函数(如 onBookonEditonRetireonRent)传递事件,再由主页面统一调度弹框显隐与选中对象,职责边界分明。

视觉风格上,工程以赛博暗黑科技风为基调,定义了完整的颜色常量体系:深空黑背景 #0A0E14、卡片底 #131926、电光青主色 #00D4FF、霓虹粉 #FF2E88、警示黄 #FFC94D、成功绿 #3DFFA0、紫 #8B5CF6 等。配合 linearGradient 渐变展示卡、粒子特效层(用 setInterval 驱动十枚"⚡"粒子在百分比坐标系内上升与透明度呼吸)、药丸式横滚筛选器、双色柱状图、进度条等视觉元素,营造出强烈的未来工业感。整页无任何本地图片资源,全部以 Emoji 图标与彩色色块构建视觉,体现了纯代码绘制的极致表现力。

二、整体架构与数据流转

下面用 Mermaid 流程图勾勒整个工程的模块关系与运行时数据流转,帮助读者在进入逐段代码前先建立全局认知。

数据与配置层

Tab子组件层

构建器层

状态中枢

入口层

回调

回调

回调

回调

implements

着色

着色

着色

约束

@Entry @Component
MechaHaulApp

@State 状态集合
activeTab / showXxxModal / selectedXxx / 表单字段 / 粒子坐标

aboutToAppear / aboutToDisappear
粒子定时器启停

headerBuilder
标题+搜索+分类药丸

contentArea
按 activeTab 分发

bottomTabBar
六个 Tab 项

particleLayer
粒子特效

modalOverlay
弹框遮罩

bookingModal
预约托运

editModal
编辑档案

retireModal
报废退役

rentalOrderModal
租赁下单

MeBookingContent
托运

MeMechasContent
机库

MeRentalContent
租赁

MeEventContent
赛事

MeLogContent
台账

MeProfileContent
我的

interface 数据模型

@Observed 类

Record 配置表
状态/包装/分类元数据

mock 静态数据

纯函数工具
formatMoney/calcRentFee 等

enum MechaTab

如上图所示,工程自上而下分为四层:入口层负责把页面注册为 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,其余四个类(MechaItemRentalMechItemEventItemTradeLogItem)结构与思路完全一致。@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') 做千分位格式化并加"¥"前缀;calcRentFeecalcInsurance 分别计算租金与保险费(保险为租金的 8% 并取整);getSortedRentalsslice(0) 浅拷贝后再排序,避免污染原数组,并按价格升序、评分降序、可租优先三种策略分发;toggleIdIntoggleBoolAt 则是数组状态切换的工具,前者在 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 声明,其状态可分为四组:导航类(activeTabselectedCategorysearchKeyword)、弹框显隐类(四个布尔)、选中对象类(selectedMechaselectedRental,均为可空联合类型 | null)、表单字段类(预约托运的 b* 前缀、租赁的 rent* 前缀,共十余个字段)。粒子特效相关状态单独成组,particleXs/Ys/Ops@State 保证重渲染,而 particleSpeedsparticleTimerprivate 修饰——因为它们不参与 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
    }
  }

在这里插入图片描述

aboutToAppearaboutToDisappear 是 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",右侧搜索框用 TextInputlayoutWeight(1) 占满剩余空间,外层套一个带圆角边框的 Row 形成胶囊输入框,onChange 把输入回写 searchKeyword 状态。第二行是横向滚动的分类药丸条:ScrollScrollDirection.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 打开预约弹框;机库页传 onEditonRetire,前者先把选中机甲的字段同步到编辑表单状态(eCodeeModel 等十二个字段逐一赋值,实现"带入编辑"),后者只记录选中并打开退役弹框;租赁页传 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'))与当前状态,让用户一眼确认编辑对象。随后是代号/型号、高度/重量两组双栏输入,每个 TextInputplaceholder 都从 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 布尔。最精彩的是底部"费用明细"卡,它把 calcRentFeecalcInsurance 两个纯函数直接用在模板里,随着天数与保险开关的变化,租金、保险费、合计三个金额实时重算并刷新显示——这是"声明式 + 纯函数"带来的即时响应体验,无需手写任何事件监听。合计行把租金、押金、保险费三者相加,用粉色加粗强调总价,整页构成一个完整的下单闭环。

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 回调与主页面联动打开预约弹框。它持有自己的局部表单状态(formMechformPack 等)与周吨位数据。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 是两列网格中的小卡,顶部色块显示图标,下方依次是代号、型号、高度重量、状态标签(配色取配置表)、管理/退役两个按钮,分别回调 onEditonRetire 把当前机甲传回主页面。主 build 中先横滑三张大展示卡,再展示待命/出租中/维护中三格状态统计,最后用 ForEach([0, 2, 4, 6, 8], (start) => Row { 两张卡 }) 的步进写法把十台机甲按两列网格排布——以索引 0/2/4/6/8 为每行起点,每行渲染 startstart+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)
  }

MeEventContentjoinedIds: 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 的大数字统计格(总支出/总收入/待结算/押金托管),分别用危险红、成功绿、警示黄、主色区分。核心是月度收支柱状图:incomeDataexpenseData 两个数组分别保存六个月的收入与支出(单位万元),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 图,专门刻画四个弹框的打开、填充与关闭流程,以及它们与主页面状态之间的联动关系。

托运页 预约托运

机库页 管理

机库页 退役

租赁页 租用

用户操作

点击卡片/按钮

showBookingModal = true

selectedMecha = m
eCode/eModel... 同步

showEditModal = true

selectedMecha = m

showRetireModal = true

selectedRental = r
rentDays = 1

showRentalModal = true

bookingModal 渲染
表单字段 b* 双向绑定

editModal 渲染
placeholder 取 selectedMecha

retireModal 渲染
警示确认

rentalOrderModal 渲染
费用实时计算

点遮罩/取消/确认
showBookingModal = false

点遮罩/取消/保存
showEditModal = false

点遮罩/取消/确认退役
showRetireModal = false

点遮罩/立即租用
showRentalModal = false

build 重渲染
条件 if 移除弹框节点

上图揭示了弹框系统的两个关键机制:一是"打开前预填"——编辑弹框在打开前先把选中机甲的字段同步到 eCodeeModel@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/caseif/else 分支配置表新增状态只改一处数据;分支式修改需改多处逻辑,易遗漏
数组状态变更纯函数返回新数组再整体赋值(toggleIdIn 等)直接 push/splice 原数组整体赋新引用触发 @State 刷新;直接变异原数组框架可能不感知
定时器setInterval + aboutToDisappearclearIntervalsetTimeout 递归或动画 APIsetInterval 简单直观;但 ArkTS 也提供 animateTo 等动画能力,粒子可改用属性动画更流畅
弹框层叠Stack 顶层 if 条件挂载 + zIndex(999)if 改为 .visibility(Visibility.None)条件挂载关闭后彻底移除节点更省资源;visibility 隐藏仍占渲染树
柱状图Column 高度按数据换算 vp 字符串引入图表库或 Canvas 绘制纯组件拼装零依赖、可读性强;复杂图表仍建议用 Canvas 或图表组件
图标资源全部用 Emoji 字符本地 PNG/SVG 或资源文件Emoji 零资源依赖、跨平台一致;但样式受系统字体影响,正式产品建议用 SVG
网格布局ForEach([0,2,4...]) 步进两列 + layoutWeightGrid/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 生态持续演进的当下,掌握这些底层范式,将帮助开发者更从容地应对从样例到产品的工程化跨越。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐