在移动互联网时代向全场景智慧生活迈进的浪潮中,华为鸿蒙 HarmonyOS 以其独特的分布式架构和统一开发范式,正在重新定义跨设备应用的开发方式。ArkTS 作为鸿蒙生态的主力开发语言,在 TypeScript 的基础上进行了深度定制与扩展,引入了声明式 UI 语法、状态管理装饰器、组件化构建模式等一系列特性,使得开发者能够以更加简洁、高效的方式构建出性能优异、体验流畅的鸿蒙原生应用。本文将以一个完整的"古堡骑士庄园"应用为蓝本,从代码层面逐段剖析 ArkTS 在实际项目中的运用,涵盖数据建模、组件拆分、状态管理、布局系统、弹窗体系、列表渲染等核心技术点,力求为读者呈现一份详尽的技术参考。

鸿蒙开发的核心理念之一是"一次开发,多端部署"。这意味着开发者只需要编写一套 ArkTS 代码,便可以适配手机、平板、智慧屏、车机等多种设备形态。这种能力的实现,离不开 ArkTS 声明式 UI 框架的设计。在 ArkTS 中,开发者通过 build() 方法以声明式的方式描述界面结构,框架负责将描述转化为真实的渲染树。与传统的命令式 UI 开发相比,声明式 UI 大幅减少了状态同步的样板代码,开发者只需关注"界面应该长什么样",而框架自动处理"如何将界面更新到这个样子"。

ArkTS 的组件化开发思想是其另一大核心特征。在鸿蒙应用中,一切可见的界面元素都是组件,小到一个文本标签,大到整个页面,都可以通过 @Component 装饰器封装为独立的结构体。每个组件拥有自己的状态、属性和构建逻辑,组件之间通过参数传递和回调函数实现通信。这种设计使得复杂界面可以被拆分为多个高内聚、低耦合的单元,极大地提升了代码的可维护性和复用性。在本项目中,整个应用被拆分为五个主页面组件和十余个弹窗构建器,每个组件各司其职,共同构成了一个完整的功能体系。

状态管理是 ArkTS 区别于普通 TypeScript 的关键能力之一。通过 @State@Prop@Link@Provide/@Consume 等一系列装饰器,ArkTS 实现了细粒度的响应式状态追踪。当被装饰的状态变量发生变化时,框架会自动触发依赖该状态的 UI 片段重新渲染,无需手动调用 setState 或类似的更新方法。这种机制在处理用户交互、弹窗开关、数据切换等场景时尤为重要,它使得状态与视图始终保持在同步状态。

声明式 UI 的精髓在于:开发者描述界面与状态之间的映射关系,框架负责在状态变化时自动更新界面。ArkTS 通过装饰器系统将这一理念落地到了代码层面,每一个 @State 变量都是一个被框架追踪的响应式数据源,每一次赋值都可能触发精确的局部渲染更新。

一、颜色系统与数据建模

1.1 色彩调色板接口定义

interface ColorPalette {
  primary: string;
  primaryLight: string;
  gold: string;
  goldLight: string;
  parchment: string;
  bg: string;
  cardBg: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  green: string;
  red: string;
  purple: string;
  blue: string;
  danger: string;
  white: string;
}

在这里插入图片描述

在这段代码中,开发者定义了一个名为 ColorPalette 的接口(interface)。在 TypeScript/ArkTS 中,接口是一种纯粹的类型声明,它本身不产生运行时对象,仅用于在编译阶段进行类型检查。ColorPalette 接口规定了十六个字符串类型的字段,每一个字段对应应用中一种语义化的颜色角色。这种设计的优点在于将"颜色值"与"颜色用途"解耦:当需要调整应用的整体配色方案时,只需修改常量定义处的值,而不必在代码各处逐个替换十六进制色值。

从字段命名可以读出,这套配色体系围绕"中世纪深棕金"的主题展开。primary 代表主色调,goldgoldLight 用于金色点缀,parchment 对应羊皮纸底色,greenredpurpleblue 分别用于不同语义状态的视觉区分。textPrimarytextSecondarytextHint 构成了三级文字层次体系,这是移动端 UI 设计中的常见实践——主要文字高对比度,次要文字降低视觉权重,提示文字进一步弱化,从而引导用户的视觉焦点。

良好的颜色管理系统是应用视觉一致性的基石。通过接口约束字段名、通过常量集中管理色值,开发者可以在数百处 UI 代码中保持统一的色彩语言,同时为后续的主题切换(如深色模式适配)预留了扩展空间。

1.2 颜色常量实例化

const COLORS: ColorPalette = {
  primary: '#3E2723',
  primaryLight: '#6D4C41',
  gold: '#C9A227',
  goldLight: '#FFD54F',
  parchment: '#F5E6C8',
  bg: '#F7F0E3',
  cardBg: '#FFFFFF',
  textPrimary: '#2B1B12',
  textSecondary: '#8A6D4B',
  textHint: '#BFA98A',
  green: '#2E4A3A',
  red: '#7B1E2B',
  purple: '#4A2A6A',
  blue: '#1F3B5C',
  danger: '#C62828',
  white: '#FFFFFF'
};

这里通过 const 关键字声明了一个名为 COLORS 的常量对象,其类型被标注为 ColorPalette,确保了对象必须实现接口中定义的所有字段。const 在 TypeScript 中表示"不可重新赋值的变量引用",即 COLORS 这个标识符不能被重新指向另一个对象,但对象内部的属性仍然可以修改(若要完全冻结,需要使用 Object.freeze)。在实际项目中,将颜色常量声明为 const 是一种最佳实践,它防止了意外覆盖颜色配置的风险。

观察这些色值,#3E2723 是一种极深的棕黑色,常用于中世纪或复古风格的主色调;#C9A227 是一种沉稳的暗金色,比纯金色 #FFD700 更有质感,适合营造贵族庄园的氛围;#F5E6C8 是温暖的羊皮纸色调,用于卡片背景可以营造古典书卷的视觉感受。这些色值的选择体现了设计师对主题氛围的精准把控——不是随意使用亮色,而是通过低饱和度、暖色系的搭配,构建出一种沉浸式的古堡骑士世界。

ColorPalette 接口

COLORS 常量

primary 主色 #3E2723

gold 金色 #C9A227

parchment 羊皮纸 #F5E6C8

bg 背景 #F7F0E3

green 成功色 #2E4A3A

red 警告色 #7B1E2B

textPrimary 主文字 #2B1B12

textSecondary 次文字 #8A6D4B

textHint 提示文字 #BFA98A

所有组件统一引用

1.3 数据模型接口群

interface Knight {
  id: number;
  name: string;
  icon: string;
  title: string;
  rank: string;
  power: number;
  honor: number;
  status: string;
  desc: string;
}

Knight 接口定义了骑士实体的数据结构。id 是唯一标识符,类型为 numbernametitle 分别表示骑士姓名和称号;icon 字段存储的是 Emoji 字符(如 '🛡️'),这是一种巧妙的设计选择——使用 Emoji 替代图片资源,既免去了资源文件的加载和管理,又能在不同设备上保持一致的显示效果。rank 表示骑士的等级阶层(如"团长"、“精英”、"见习"等),powerhonor 是两个数值型属性,分别代表战力和荣誉值,status 表示骑士当前状态,desc 是描述性文字。

在 ArkTS 中,接口的使用方式与 TypeScript 一致。接口本身不编译为 JavaScript 运行时代码,它们只在编译期提供类型安全保障。当开发者试图给 power 赋一个字符串值时,编译器会立即报错,从而在开发阶段捕获类型错误。这种静态类型检查是 ArkTS 相对于 JavaScript 的重要优势之一,它使得大型项目的代码维护更加可靠。

接口是数据建模的蓝图。在鸿蒙应用开发中,合理地定义接口体系可以让数据流在组件之间传递时有据可依、有型可循。每一个接口字段都应该对应一个明确的业务语义,避免模糊不清的命名和冗余字段的设计。

interface Quest {
  id: number;
  name: string;
  icon: string;
  star: number;
  reward: number;
  status: string;
  desc: string;
}

在这里插入图片描述

Quest 接口描述了委托任务的数据结构。与 Knight 类似,它同样包含 idnameicondesc 等基础字段。特别值得注意的是 star 字段——它是一个数字类型,表示任务的星级(难度等级),在 UI 展示时通过 '★'.repeat(q.star) 的方式将数字转化为对应数量的星号字符,这是 ArkTS/TypeScript 中字符串与数字互操作的经典手法。reward 字段表示任务完成后的功勋奖励值,status 字段的值域为"可接"、“进行中”、"已完成"三种状态,这个字段在后续的过滤函数中起到了核心作用。

接下来是一系列结构类似的接口定义:

interface ArenaBattle {
  id: number;
  name: string;
  icon: string;
  result: string;
  score: number;
  field: string;
  time: string;
  desc: string;
}

interface Gear {
  id: number;
  name: string;
  icon: string;
  part: string;
  quality: string;
  level: number;
  attack: number;
  desc: string;
}

interface Material {
  id: number;
  name: string;
  icon: string;
  rarity: string;
  stock: number;
  price: number;
  desc: string;
}

在这里插入图片描述

ArenaBattle 接口用于竞技场对决记录,其中 result 字段取值为"胜"、“负”、“平”,score 表示积分变化(正数表示得分,负数表示失分),field 是对决场地名称。Gear 接口描述装备信息,part 表示装备部位(武器、防具、饰品),quality 表示品质等级(传说、史诗、稀有、精良、普通),level 是装备等级,attack 是攻击力数值。Material 接口定义锻造材料,rarity 表示稀有度,stock 是当前库存数量,price 是单份价格。

这三个接口共同体现了数据驱动 UI 的设计思想。每个实体都包含了足够的信息来独立渲染一张卡片或一个详情弹窗,组件只需要接收一个实体对象作为参数,就能完整地展示其所有信息,而无需额外的数据查询。这种"自包含数据"的设计模式使得组件的复用性极高——同一套卡片组件可以用于不同的列表场景,只要传入的数据结构符合接口约束即可。

组件渲染层

数据模型层

Knight 骑士

Quest 委托

ArenaBattle 对决

Gear 装备

Material 材料

GuildTask 团任务

Drink 酒水

Tale 传闻

Honor 荣誉

KnightSkill 技能

Notice 公告

QuickIcon 快捷入口

KnightTab

ArenaTab

GearTab

TavernTab

MineTab

在这里插入图片描述

二、Mock 数据与全局过滤函数

2.1 骑士数据集

const KNIGHTS: Knight[] = [
  { id: 1, name: '兰斯洛特', icon: '🛡️', title: '圣剑骑士', rank: '团长', power: 980, honor: 12800, status: '在岗', desc: '手持圣剑,守卫城堡北门百年。' },
  { id: 2, name: '亚瑟', icon: '⚔️', title: '王者之剑', rank: '副团长', power: 950, honor: 11600, status: '在岗', desc: '圆桌会议首席,统领全体骑士。' },
  // ... 更多数据
];

在这里插入图片描述

这里声明了一个 Knight[] 类型的数组常量 KNIGHTS,其中包含十二名骑士的完整数据。在 ArkTS 中,当声明数组常量时,类型标注 Knight[] 会被编译器用来校验每个对象字面量是否符合 Knight 接口的字段定义。如果某个对象缺少 power 字段或 power 的值类型不是 number,编译器会在构建阶段报错。这种编译期保障是 ArkTS 类型系统的核心价值。

这些 Mock 数据在真实项目中通常来自后端 API 返回。但在开发阶段,使用硬编码的 Mock 数据可以让前端开发与后端开发并行进行,互不阻塞。开发者可以先基于约定的数据结构编写 Mock 数据,构建完整的 UI 和交互逻辑,待后端 API 就绪后,只需将数据源从常量替换为网络请求即可。这种"Mock First"的开发模式在前后端分离架构中非常普遍。

2.2 过滤函数群

function getReadyQuests(): Quest[] {
  let arr: Quest[] = [];
  for (let i = 0; i < QUESTS.length; i++) {
    if (QUESTS[i].status === '可接') {
      arr.push(QUESTS[i]);
    }
  }
  return arr;
}

在这里插入图片描述

这是一个典型的过滤函数。它创建一个空数组 arr,遍历 QUESTS 数组,将 status 为"可接"的任务推入新数组并返回。这种模式在整个项目中被反复使用:getDoingQuests() 过滤"进行中"状态的任务,getDoneQuests() 过滤"已完成"状态的任务,getWinBattles() 过滤结果为"胜"的对决,getLegendGears() 过滤品质为"传说"的装备,等等。

值得注意的是,这些函数定义在 struct 之外,是全局级的纯函数。在 ArkTS 中,全局函数不依赖于任何组件实例,可以在任何组件的 build() 方法中被直接调用。由于它们不涉及状态管理,每次调用都会重新执行过滤逻辑并返回新数组,这在性能上可能不是最优的(尤其是在数据量大时),但在本项目的数据规模下(每个数据集仅十余条记录),这种简化设计是完全可接受的。

将过滤逻辑提取为独立的纯函数是一种良好的编程习惯。纯函数的输出完全由输入决定,不依赖也不修改外部状态,这使得它们易于测试、易于推理、易于组合。当需求变化时,只需修改一处过滤函数,所有依赖该函数的组件都会自动获得更新后的结果。

function getReadyCount(): number {
  return getReadyQuests().length;
}

在这里插入图片描述

计数函数是对过滤函数的进一步封装。getReadyCount() 调用 getReadyQuests() 并返回其数组长度。这种"函数组合"的设计避免了在 UI 代码中直接操作数组长度,使得调用更加语义化。类似的计数函数还有 getDoingCount()getWinCount()getLegendCount()getUnlockedCount() 等,它们分别对应不同的统计需求。

function getEvenDrinks(): Drink[] {
  let arr: Drink[] = [];
  for (let i = 0; i < DRINKS.length; i++) {
    if (i % 2 === 0) {
      arr.push(DRINKS[i]);
    }
  }
  return arr;
}

这个函数展示了一种特殊的过滤模式——奇偶分割。getEvenDrinks() 返回索引为偶数的饮品,getOddDrinks() 返回索引为奇数的饮品。这种分割的目的是在 UI 层面实现"双列瀑布流"布局:左列渲染偶数索引的元素,右列渲染奇数索引的元素,从而在不依赖 CSS Grid 的前提下实现两列等高排列的效果。同样的模式也应用于技能列表的 getEvenSkills()getOddSkills()

status=可接

status=进行中

status=已完成

result=胜

quality=传说

quality=史诗

stock<30

score>=4.8

locked=false

locked=true

i%2===0

i%2===1

原始数据数组

过滤函数

getReadyQuests

getDoingQuests

getDoneQuests

getWinBattles

getLegendGears

getEpicGears

getLowStockMats

getHotDrinks

getUnlockedHonors

getLockedHonors

getEvenDrinks

getOddDrinks

UI 双列布局渲染

三、主入口组件 Index 的状态管理

3.1 @Entry 与 @Component 装饰器

@Entry
@Component
struct Index {

在这里插入图片描述

这段代码是整个应用的入口。@Entry 装饰器标记 Index 结构体为应用的根组件——在鸿蒙应用启动时,框架会查找被 @Entry 修饰的组件,将其作为页面渲染的起点。一个页面只能有一个 @Entry 组件。@Component 装饰器则标记 Index 为一个自定义组件,使其拥有独立的 build() 方法来描述 UI 结构。

在 ArkTS 中,struct 是定义组件的关键字,它类似于 TypeScript 中的 class,但有一些重要区别:struct 不支持继承,不能被实例化为对象,它的生命周期完全由框架管理。每个 @Component 修饰的 struct 必须实现 build() 方法,该方法返回一个或多个 UI 组件,构成该组件的可视内容。

@Entry@Component 的组合是鸿蒙应用页面的标准入口模式。@Entry 赋予组件"页面根"的身份,使其可以被路由系统识别和加载;@Component 赋予结构体"自定义组件"的能力,使其拥有独立的构建方法和状态空间。两者的配合构成了鸿蒙声明式 UI 的基础单元。

3.2 @State 状态变量声明

@State tabIndex1: number = 0;
@State showToast: boolean = false;
@State toast: string = '';
@State honorScore: number = 12800;
@State knightLevel: number = 8;
@State winStreak: number = 12;

@State 是 ArkTS 中最基础的状态管理装饰器。被 @State 修饰的变量会成为组件的"本地状态",当这些变量的值发生变化时,框架会自动重新执行 build() 方法中依赖这些变量的部分,更新对应的 UI。

tabIndex1 是一个 number 类型变量,初始值为 0,用于控制当前激活的 Tab 页面索引。当用户点击底部导航栏切换 Tab 时,这个值会改变,触发主内容区重新渲染对应页面。showToast 是一个布尔型开关,控制 Toast 提示框的显示与隐藏。toast 存储 Toast 的文本内容。honorScoreknightLevelwinStreak 分别存储荣誉值、骑士等级和连胜场次,这些值通过参数传递给子组件进行展示。

在 ArkTS 的状态管理体系中,@State 变量的改变是触发 UI 更新的根因。当 this.tabIndex1 = 1 被执行时,框架检测到 tabIndex1 的新旧值不同,于是重新评估 build() 中所有引用了 tabIndex1 的代码分支,将渲染结果从 KnightTab 切换为 ArenaTab。这种"状态驱动视图"的机制是声明式 UI 的核心原理。

3.3 弹窗开关状态群

// 弹框开关
@State showKnight: boolean = false;
@State showQuest: boolean = false;
@State showBattle: boolean = false;
@State showGear: boolean = false;
@State showForge: boolean = false;
@State showMat: boolean = false;
@State showGuildTask: boolean = false;
@State showGuildSign: boolean = false;
@State showDrink: boolean = false;
@State showTale: boolean = false;
@State showHonor: boolean = false;
@State showSkill: boolean = false;
@State showBuy: boolean = false;
@State showRank: boolean = false;
@State showLoot: boolean = false;
@State showMedal: boolean = false;

这里声明了十六个布尔型的 @State 变量,每一个对应一种弹窗的显示开关。这种"一弹窗一开关"的设计模式虽然看起来有些冗长,但它具有极高的可读性和可维护性——每一个开关的名称直接表达了它控制的是哪个弹窗。当 showKnighttrue 时,骑士详情弹窗显示;为 false 时隐藏。

在更复杂的场景中,如果弹窗数量更多,可以考虑使用枚举类型来统一管理:例如 @State currentModal: ModalType = ModalType.None,一次只显示一个弹窗。但本项目中有少量场景需要弹窗叠加(如从装备详情弹窗跳转到锻造弹窗),因此使用独立的布尔开关更为灵活。

3.4 选中对象状态群

// 选中对象
@State selKnight: Knight | null = null;
@State selQuest: Quest | null = null;
@State selBattle: ArenaBattle | null = null;
@State selGear: Gear | null = null;
// ... 更多选中对象

这里声明了一系列"选中对象"状态变量,它们的类型都是"接口类型联合 null"。在 ArkTS 中,Knight | null 表示这个变量可以是一个 Knight 对象,也可以是 null。初始值为 null,表示初始状态下没有选中任何对象。当用户点击列表中的某一项时,对应的 sel 变量被赋值为该项的数据对象,同时对应的 show 开关被设为 true,从而触发弹窗渲染并展示该对象的详细信息。

Knight | null 这种联合类型是 ArkTS(TypeScript)类型系统中的重要特性。它强制开发者在访问 selKnight 的属性之前先进行空值检查,否则编译器会报错。在本项目中,开发者使用了非空断言操作符 !(如 this.selKnight!.name)来绕过空值检查,这是一种在确信变量非空时使用的快捷写法,但需要开发者自行保证运行时该变量确实不为 null

3.5 表单字段状态

// 表单字段
@State fQty: number = 1;
@State fStarIdx: number = 0;
@State fGradeIdx: number = 0;
@State fMsg: string = '';

这些变量用于弹窗中的表单交互。fQty 是军需购买弹窗中的兑换数量,初始为 1,通过 +/- 按钮在 19 之间调整。fStarIdx 是委托接取弹窗中选择的星级(1、2 或 3),初始为 0(未选择)。fGradeIdxfMsg 是预留的表单字段。每当这些值变化时,弹窗中依赖它们的 UI 元素(如数量显示、星级高亮、合计金额计算)会自动更新。

状态变量的设计应遵循"最小粒度"原则——每个 @State 变量应该对应一个独立的、可观察的 UI 状态单元。将多个不相关的状态合并到一个对象中虽然可以减少变量数量,但会导致不必要的大范围重渲染。本项目通过拆分为独立的原子状态变量,实现了精确的局部更新。

四、build 方法与页面骨架

4.1 Stack 容器布局

build() {
  Stack() {
    Column() {
      // 主内容区
      if (this.tabIndex1 === 0) {
        KnightTab({ ... })
      } else if (this.tabIndex1 === 1) {
        ArenaTab({ ... })
      }
      // ...
      // 底部 Tab 单排
      Row() {
        ForEach(this.tabs, (label: string, idx: number) => {
          this.bottomTabItem(this.icons[idx], label, idx)
        }, (label: string, idx: number) => label + idx)
      }
      .width('100%')
      .height(56)
      .backgroundColor('#FFFFFF')
      .shadow({ radius: 8, color: '#1A000000', offsetY: -2 })
    }
    .width('100%')
    .height('100%')

    // 弹框挂载
    if (this.showKnight && this.selKnight !== null) {
      this.modalOverlay(() => { this.showKnight = false })
      this.knightModal()
    }
    // ... 更多弹窗
  }
  .width('100%')
  .height('100%')
}

build() 方法是每个 @Component 组件必须实现的核心方法,它以声明式的方式描述组件的 UI 结构。在这个 Index 组件中,最外层使用了 Stack 容器。Stack 是 ArkTS 中的一个基础布局容器,它的作用是将子元素在 Z 轴方向上层叠排列——后声明的子元素会覆盖在先声明的子元素之上。

Stack 在这里的设计意图非常清晰:第一层是主内容区(Column 包含页面内容和底部导航栏),第二层及之后是各种弹窗。当弹窗的显示开关为 true 时,弹窗内容会覆盖在主内容区之上,形成模态遮罩效果。这种通过条件渲染来控制弹窗显示的方式是 ArkTS 的推荐模式——不需要额外的 Dialog API 或 Portal 机制,一切都在组件树内完成。

Column 容器是垂直线性布局,它的子元素从上到下依次排列。在这里,Column 包含了主内容区(根据 tabIndex1 的值条件渲染不同 Tab 页面)和底部导航栏(Row),两者一上一下构成了页面的基本骨架。.width('100%').height('100%') 确保它占满整个屏幕。

Stack 布局是实现弹窗遮罩效果的理想容器。它天然支持 Z 轴层叠,无需依赖额外的 DOM 操作或 Portal 模式。在 ArkTS 中,通过条件渲染配合 Stack,开发者可以以纯声明式的方式管理所有浮层和弹窗,这种设计哲学与传统命令式 UI 框架形成了鲜明对比。

4.2 条件渲染与 Tab 路由

if (this.tabIndex1 === 0) {
  KnightTab({
    honorScore: this.honorScore,
    onKnight: (k: Knight) => {
      this.selKnight = k;
      this.showKnight = true;
    },
    onQuest: (q: Quest) => {
      this.selQuest = q;
      this.fStarIdx = 0;
      this.showQuest = true;
    },
    onGuildTask: (t: GuildTask) => {
      this.selGuildTask = t;
      this.showGuildTask = true;
    },
    onSign: (t: GuildTask) => {
      this.selSign = t;
      this.showGuildSign = true;
    }
  })
}

这段代码展示了 ArkTS 中的条件渲染语法和组件参数传递机制。if (this.tabIndex1 === 0) 是标准的 JavaScript 条件语句,但在 build() 方法中,它的语义被扩展为"当条件为真时渲染此分支的 UI"。当 tabIndex1 的值变化时,框架会自动卸载旧分支的 UI 并挂载新分支的 UI。

KnightTab 是一个自定义组件,通过大括号 {} 传递参数。参数分为两类:数据参数(如 honorScore)和回调函数(如 onKnight)。数据参数从父组件流向子组件,是单向的;回调函数则允许子组件在特定事件发生时通知父组件并触发状态变更。这种"数据向下、事件向上"的单向数据流模式是 ArkTS 组件通信的核心范式。

观察 onKnight 回调的实现:当子组件 KnightTab 内部发生骑士点击事件时,它会调用 this.onKnight(k) 传入被点击的骑士对象。父组件 Index 接收到这个调用后,将骑士对象赋值给 selKnight 状态变量,同时将 showKnight 设为 true。这两个状态变化会触发 build() 重新评估,在 Stack 层叠中渲染出骑士详情弹窗。这种设计将"事件感知"和"弹窗管理"的责任分离到了不同组件中,实现了关注点分离。

4.3 ForEach 循环渲染底部导航

Row() {
  ForEach(this.tabs, (label: string, idx: number) => {
    this.bottomTabItem(this.icons[idx], label, idx)
  }, (label: string, idx: number) => label + idx)
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
.shadow({ radius: 8, color: '#1A000000', offsetY: -2 })

ForEach 是 ArkTS 中用于列表渲染的核心组件。它接收三个参数:数据源数组、子项生成函数和键值生成函数。在这里,this.tabs 是一个字符串数组 ['骑士团', '竞技场', '装备', '酒馆', '我的']ForEach 会为数组中的每个元素调用子项生成函数,生成一个 bottomTabItem 构建器调用。

子项生成函数 (label: string, idx: number) => { ... } 接收两个参数:当前项的值和索引。idx 在这里非常重要,因为 bottomTabItem 需要知道当前项的索引来判断它是否被选中(this.tabIndex1 === idx),从而应用不同的样式(选中态金色加粗,未选中态灰色常规)。

键值生成函数 (label: string, idx: number) => label + idx 为每个列表项生成一个唯一键。在 ArkTS 中,键值的作用类似于 React 中的 key 属性——它帮助框架在数据变化时精确识别哪些项是新增的、哪些是删除的、哪些是移动的,从而进行高效的差异化更新(diff 算法)。使用 label + idx 作为键值可以保证在标签文本和索引的组合下唯一性。

Row 容器是水平线性布局,子元素从左到右依次排列。底部导航栏的五个 Tab 项在 Row 中水平排列,每个通过 layoutWeight(1) 等分宽度。.shadow() 方法为导航栏添加了向上的投影效果(offsetY: -2 表示阴影向上偏移),在视觉上与主内容区形成分隔。

0

1

2

3

4

Index 组件 build 方法

Stack 最外层容器

Column 主内容层

弹窗层 - 条件渲染

tabIndex1 条件判断

KnightTab 骑士团页面

ArenaTab 竞技场页面

GearTab 装备页面

TavernTab 酒馆页面

MineTab 我的页面

Row 底部导航栏

ForEach 渲染 5 个 Tab 项

modalOverlay 遮罩层

knightModal 骑士弹窗

questModal 委托弹窗

... 其他弹窗

toastBox 提示框

五、弹窗体系与 @Builder 装饰器

5.1 @Builder 装饰器与弹窗遮罩

@Builder
modalOverlay(onClose: () => void) {
  Column() {
    Row() {
      Column() {
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#66000000')
    .onClick(() => {
      onClose();
    })
  }
  .width('100%')
  .height('100%')
}

@Builder 是 ArkTS 中的一个重要装饰器,它用于将一段 UI 结构封装为可复用的构建方法。与 @Component 不同,@Builder 方法不需要声明 struct,它更像是 UI 片段的"宏定义"——在被调用的地方,@Builder 的内容会被内联展开到调用位置的组件树中。

modalOverlay 是一个通用的弹窗遮罩构建器。它接收一个 onClose 回调函数作为参数,渲染一个半透明的全屏覆盖层(背景色 #66000000,即 40% 透明度的黑色)。当用户点击遮罩区域时,调用 onClose() 回调关闭弹窗。这个构建器被所有弹窗共享,实现了统一的"点击外部关闭"交互模式。

在 ArkTS 中,@Builder 方法的参数传递有一些特殊之处。对于函数类型的参数(如 onClose),需要使用箭头函数包装来保持 this 绑定。在调用处:this.modalOverlay(() => { this.showKnight = false }),箭头函数 () => { this.showKnight = false } 捕获了组件实例的 this,确保在遮罩点击时能正确修改 Index 组件的状态。

@Builder@Component 的选择是一个常见的架构决策。@Component 适合封装具有独立状态和复杂逻辑的组件单元;@Builder 适合封装无状态的纯 UI 片段。本项目中的弹窗内容虽然复杂,但它们的状态全部由父组件 Index 管理,弹窗本身不持有状态,因此使用 @Builder 是合适的——它避免了为每个弹窗定义一个完整的 struct,同时保持了 UI 结构的封装和复用。

5.2 骑士详情弹窗

@Builder
knightModal() {
  Column() {
    Row() {
      Text('⚜️ 骑士卷宗').fontSize(11).fontColor(COLORS.gold).margin({ top: 14 })
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)

    Text(this.selKnight!.icon).fontSize(48).margin({ top: 6 })
    Text(this.selKnight!.name)
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .fontColor(COLORS.textPrimary)
      .letterSpacing(2)
      .margin({ top: 4 })

knightModal() 是骑士详情弹窗的 @Builder 方法。它通过 this.selKnight!.icon 等方式访问当前选中的骑士对象属性。非空断言操作符 ! 告诉编译器"我确定此时 selKnight 不为 null",从而跳过空值检查。这种安全性由调用方的条件渲染保证——只有当 this.showKnight && this.selKnight !== null 同时为真时,knightModal() 才会被渲染。

弹窗的 UI 结构使用了 Column 垂直布局,从上到下依次排列:标题行(Row + Text)、骑士图标(大号 Text 显示 Emoji)、骑士名称(粗体大号文字)、称号与等级行、属性统计区(三列等分布局)、描述文字、操作按钮区。

.justifyContent(FlexAlign.Center)Row 容器的弹性布局属性,用于控制子元素在主轴(水平方向)上的对齐方式。FlexAlign.Center 表示居中对齐。ArkTS 中的 FlexAlign 枚举提供了多种对齐选项:Start(起始端对齐)、Center(居中)、End(末端对齐)、SpaceBetween(两端对齐,元素间均分间距)、SpaceAround(每个元素两侧均分间距)、SpaceEvenly(所有间距完全均分)。

.letterSpacing(2) 设置文字的字间距为 2vp(虚拟像素),这在展示标题类文字时是一种常见的排版手法,能够增强文字的辨识度和高级感。

5.3 弹窗的样式与动画

    Row({ space: 8 }) {
      Column({ space: 4 }) {
        Text('战力').fontSize(10).fontColor(COLORS.textSecondary)
        Text(this.selKnight!.power + '').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.red)
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
      Column({ space: 4 }) {
        Text('荣誉值').fontSize(10).fontColor(COLORS.textSecondary)
        Text(this.selKnight!.honor + '').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.gold)
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .padding({ top: 14, bottom: 14 })
    .backgroundColor('#FFF3DC')
    .borderRadius(12)
    .margin({ top: 14 })

这段代码展示了属性统计区的布局。外层是 Row({ space: 8 })space 参数设置子元素之间的间距为 8vp。内层是三个 Column 容器,每个通过 .layoutWeight(1) 获得等分的水平空间。layoutWeight 是 ArkTS 布局系统中的重要概念——它类似于 CSS 中的 flex: 1,表示元素在剩余空间中的分配权重。三个 layoutWeight(1)Column 会平分 Row 的总宽度,实现三等分布局。

每个 Column 内部是垂直排列的标签和数值。数值通过 this.selKnight!.power + '' 将数字转换为字符串(ArkTS 的 Text 组件接受 stringResource 类型的内容,数字需要先转换为字符串)。数值文字使用了不同的颜色来传达语义:战力用红色(COLORS.red),荣誉值用金色(COLORS.gold),状态用绿色(COLORS.green)。

.alignItems(HorizontalAlign.Center) 设置 Column 容器中子元素在交叉轴(水平方向)上的对齐方式为居中。HorizontalAlign 是 ArkTS 中的水平对齐枚举,包含 StartCenterEnd 三个选项。

    .width('84%')
    .backgroundColor(COLORS.parchment)
    .borderRadius(18)
    .border({ width: 2, color: COLORS.gold })
    .padding({ left: 20, right: 20, bottom: 20 })
    .constraintSize({ maxHeight: '82%' })
    .clip(true)
    .scale({ x: 0.95, y: 0.95 })
    .animation({ duration: 220, curve: Curve.EaseOut })

这是骑士弹窗 Column 容器的样式配置。.width('84%') 使弹窗宽度为屏幕宽度的 84%,居中显示(由 Stack 的默认居中行为决定)。.backgroundColor(COLORS.parchment) 使用羊皮纸色作为背景,与骑士主题契合。.borderRadius(18) 设置圆角半径为 18vp,营造出柔和的卡片外观。.border({ width: 2, color: COLORS.gold }) 添加 2vp 宽的金色边框,强化了"骑士卷宗"的视觉印象。

.constraintSize({ maxHeight: '82%' }) 是一个约束尺寸属性,限制弹窗的最大高度为屏幕的 82%。这在弹窗内容可能很长时非常重要——它确保弹窗不会超出屏幕范围,超出部分由内部的 Scrollclip 处理。.clip(true) 启用裁剪,使超出弹窗边界的内容被截断,配合圆角形成"内容在圆角矩形内"的视觉效果。

.scale({ x: 0.95, y: 0.95 }).animation({ duration: 220, curve: Curve.EaseOut }) 组合实现了弹窗出现的缩放动画效果。初始缩放为 95%,当弹窗被渲染时会从 95% 放大到 100%,持续 220 毫秒,使用 EaseOut 缓动曲线(先快后慢)。这种微妙的缩放动画为弹窗的出现赋予了"弹出"的动感,提升了交互的精致度。

5.4 委托接取弹窗与星级选择

@Builder
questModal() {
  Column() {
    Row() {
      Text('📜 委托详情').fontSize(12).fontColor(COLORS.textSecondary)
      Text('X').fontSize(16).fontColor(COLORS.textHint)
        .margin({ left: 8 })
        .onClick(() => { this.showQuest = false })
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)

questModal() 是委托接取弹窗,采用底部抽屉式设计(width('100%') 配合 borderRadius({ topLeft: 22, topRight: 22 }) 只有顶部圆角)。标题行使用了 FlexAlign.SpaceBetween 对齐方式,使得"委托详情"文字和关闭按钮 X 分别位于行的左右两端,这是移动端弹窗标题栏的经典布局模式。

关闭按钮 X 的实现方式值得一提:它不是使用 Button 组件,而是直接在 Text 上添加 .onClick() 事件。在 ArkTS 中,任何组件都可以通过 .onClick() 绑定点击事件,不限于 Button 组件。这种灵活的事件绑定方式使得开发者可以用任何视觉元素作为可点击的交互入口。

    Row({ space: 8 }) {
      ForEach([1, 2, 3], (s: number) => {
        Column() {
          Text('★'.repeat(s)).fontSize(13).fontColor(this.fStarIdx === s ? '#FFFFFF' : COLORS.gold)
          Text(s === 1 ? '普通' : (s === 2 ? '进阶' : '精英')).fontSize(10).fontColor(this.fStarIdx === s ? '#FFFFFF' : COLORS.textSecondary)
        }
        .padding({ left: 14, right: 14, top: 8, bottom: 8 })
        .backgroundColor(this.fStarIdx === s ? COLORS.gold : '#FFF3DC')
        .borderRadius(10)
        .onClick(() => { this.fStarIdx = s })
      }, (s: number) => 'star' + s)
    }
    .width('100%')

星级选择器是委托弹窗中的核心交互元素。ForEach 渲染三个选项(1星、2星、3星),每个选项是一个 Column 包含星号字符串和难度标签。'★'.repeat(s) 利用 JavaScript 的 String.prototype.repeat 方法生成对应数量的星号字符。

选中态通过三元运算符实现:当 this.fStarIdx === s 时(当前选中的星级等于该项的星级),文字颜色变为白色、背景变为金色;否则文字为金色/灰色、背景为浅色。这种"条件样式"是 ArkTS 中实现选中态高亮的标准手法。当用户点击某个星级时,.onClick(() => { this.fStarIdx = s }) 修改 fStarIdx 状态变量,触发 ForEach 重新渲染,所有选项的样式根据新的 fStarIdx 值重新评估。

三元运算符在 ArkTS 声明式 UI 中的地位类似于模板引擎中的条件指令。它允许开发者根据状态动态切换样式值,实现"状态变化即样式变化"的响应式效果。对于更复杂的条件逻辑(多个条件分支),可以使用 if-else 语句或辅助函数来封装条件判断逻辑。

5.5 锻造弹窗与进度条组件

@Builder
forgeModal() {
  Column() {
    Text('🔨 装备锻造').fontSize(12).fontColor('#FFE9A8').margin({ top: 16 })
    Text(this.selForge!.icon).fontSize(42).margin({ top: 6 })
    Text(this.selForge!.name)
      .fontSize(22)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFE9A8')
      .margin({ top: 4 })
    Text('当前 Lv.' + this.selForge!.level + ' → 目标 Lv.' + (this.selForge!.level + 1))
      .fontSize(12)
      .fontColor('#E8C87A')
      .margin({ top: 4 })

forgeModal() 是装备锻造弹窗,采用了深棕色背景(#4E342E)配合金色文字(#FFE9A8)的暗金配色方案,营造出锻造工坊的氛围。弹窗展示了当前装备的图标、名称和等级变化信息(“当前 Lv.X → 目标 Lv.X+1”),让用户清晰地了解锻造操作的效果预期。

字符串拼接 '当前 Lv.' + this.selForge!.level + ' → 目标 Lv.' + (this.selForge!.level + 1) 是 ArkTS/TypeScript 中构建动态文本的常用方式。+ 运算符在字符串上下文中充当连接符,将多个字符串片段和变量值串联为一个完整的展示文本。

    Column({ space: 6 }) {
      Row() {
        Text('锻造进度').fontSize(11).fontColor('#E8C87A').layoutWeight(1)
        Text('68%').fontSize(11).fontColor('#FFE9A8')
      }
      .width('100%')
      Progress({ value: 68, total: 100, type: ProgressType.Linear })
        .width('100%')
        .height(8)
        .color('#FFD54F')
        .backgroundColor('#553D1E')
    }
    .width('100%')
    .margin({ top: 16 })

Progress 是 ArkTS 内置的进度条组件,用于可视化展示进度信息。它接收三个核心参数:value(当前值)、total(总量)、type(进度条类型)。ProgressType.Linear 表示线性进度条(水平条状),此外还有 ProgressType.Ring(环形进度条)和 ProgressType.Eclipse(月食形进度条)可供选择。

.color('#FFD54F') 设置进度条已完成部分的颜色为金色,.backgroundColor('#553D1E') 设置未完成部分的背景色为深棕色。两者的配色搭配强化了锻造主题的视觉一致性。.height(8) 将进度条高度设为 8vp,呈现出纤细精致的视觉效果。

在本项目中,Progress 组件被多处使用:锻造弹窗中的锻造进度、团任务弹窗中的整体进度、技能弹窗中的修炼进度、竞技场页面中的段位晋升进度。每次使用都通过 colorbackgroundColor 属性定制配色,使其融入所在场景的主题氛围。

5.6 Toast 提示框

@Builder
toastBox() {
  Row() {
    Text(this.toast).fontSize(14).fontColor('#FFFFFF')
  }
  .padding({ left: 18, right: 18, top: 10, bottom: 10 })
  .backgroundColor('#CC333333')
  .borderRadius(20)
}

toastBox() 是一个轻量级的 Toast 提示构建器。它渲染一个圆角矩形(borderRadius(20)),内含白色文字。背景色 #CC333333 中的 CC 是十六进制透明度(约 80%),333333 是深灰色,整体呈现出半透明深色的效果。

Toast 的显示与隐藏由 showToast 状态变量控制。当 showTip(msg) 方法被调用时:

showTip(msg: string): void {
  this.toast = msg;
  this.showToast = true;
  setTimeout(() => {
    this.showToast = false;
  }, 1800);
}

showTip 方法设置 Toast 文本为传入的 msg,将 showToast 设为 true 显示提示框,然后通过 setTimeout 在 1800 毫秒后自动将 showToast 设为 false 隐藏提示框。setTimeout 是 JavaScript/ArkTS 中的全局定时器函数,它在指定延迟后执行回调函数。这种"显示后自动延时隐藏"的模式是 Toast 组件的标准交互范式。

Stack 布局中,Toast 位于最上层(因为它在 build() 方法中最后声明),确保它覆盖在所有其他内容之上,包括弹窗。这使得用户在弹窗中执行操作后(如"确认接取"按钮点击后关闭弹窗),Toast 提示能够立即显示在最顶层,给用户即时的操作反馈。

六、KnightTab 骑士团页面组件

6.1 组件声明与参数定义

@Component
struct KnightTab {
  honorScore: number = 0;
  onKnight: (k: Knight) => void = () => {};
  onQuest: (q: Quest) => void = () => {};
  onGuildTask: (t: GuildTask) => void = () => {};
  onSign: (t: GuildTask) => void = () => {};

  build() {
    Scroll() {
      Column({ space: 12 }) {

KnightTab 是骑士团页面的自定义组件。与 Index 组件不同,它没有 @Entry 装饰器,因为它不是页面入口,而是被 Index 在条件渲染中引用的子组件。它通过普通的成员变量(非 @State)接收父组件传递的参数:honorScore 是数据参数,接收荣誉值;其余四个是回调函数参数,用于向父组件发送事件。

回调函数的默认值 () => {} 是一个空函数,这是 ArkTS 中定义可选回调的标准做法。如果父组件不传递该回调,调用时不会报错(执行空函数,无效果)。但在本项目中,所有回调都有父组件传入的实际实现。

build() 方法最外层使用了 Scroll 容器。Scroll 是 ArkTS 的滚动容器组件,当其内容超出可视区域时,用户可以通过滑动手势滚动查看全部内容。在移动端页面开发中,页面内容通常可能超出屏幕高度(尤其是包含多个列表区块时),因此使用 Scroll 包裹是保证内容可达性的标准做法。.scrollBar(BarState.Off) 隐藏了滚动条,使滚动行为更加干净自然。

Scroll 组件与 Column 的关系类似于 CSS 中 overflow-y: scroll 与普通 div 的关系。Scroll 提供了滚动能力,而内部的内容容器(通常是 Column)负责排列子元素。在 ArkTS 中,Scroll 只接受一个子组件,因此需要用 ColumnRow 包裹多个子元素。

6.2 头部横幅与线性渐变

Column({ space: 6 }) {
  Row() {
    Column({ space: 4 }) {
      Text('KNIGHT MANOR').fontSize(12).fontColor('#E8C87A').letterSpacing(2)
      Text('骑士庄园 · 骑士团').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').letterSpacing(1)
      Text('今日在岗 12 人 · 守卫城堡与荣誉').fontSize(11).fontColor('#D9C7A3')
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)
    Text('🏰').fontSize(40)
  }
  .width('100%')
  .padding({ top: 16, left: 16, right: 16, bottom: 12 })
  Row() {
    Text('🔍').fontSize(14).margin({ left: 10 })
    Text('搜索骑士 / 委托 / 团任务').fontSize(12).fontColor('#C8B898').margin({ left: 6 })
  }
  .width('100%')
  .height(38)
  .backgroundColor('#F5E6C8')
  .borderRadius(19)
  .margin({ bottom: 14 })
}
.width('100%')
.backgroundColor('linearGradient({ colors: [["#3E2723", 0], ["#6D4C41", 1]] })')
.clip(true)

头部横幅是页面的视觉焦点,采用了深棕色线性渐变背景。backgroundColor 属性接收一个特殊的字符串值 'linearGradient({ colors: [["#3E2723", 0], ["#6D4C41", 1]] })',这是 ArkTS 中实现线性渐变的方式。colors 数组定义了渐变的色标:["#3E2723", 0] 表示起点颜色为深棕(位置 0%),["#6D4C41", 1] 表示终点颜色为浅棕(位置 100%)。渐变方向默认为从上到下。

横幅内部结构是 Row 水平布局:左侧是文字信息区(Column 包含三行文字),右侧是城堡 Emoji 图标。文字区使用了 .alignItems(HorizontalAlign.Start) 使文字左对齐,.layoutWeight(1) 占据剩余水平空间。三行文字形成视觉层次:英文标题(小号金色字间距宽)、中文主标题(大号白色粗体)、描述信息(小号浅色)。

搜索栏是横幅的第二行,设计为一个圆角矩形(borderRadius(19) 配合 height(38) 形成药丸形外观),背景色为羊皮纸色。内部包含放大镜 Emoji 和占位提示文字。目前这个搜索栏是纯展示性的(未绑定实际的搜索逻辑),在真实项目中可以扩展为 TextInput 组件实现真正的搜索功能。

.clip(true) 确保渐变背景不会溢出 Column 的边界,特别是底部的搜索栏的 margin({ bottom: 14 }) 不会导致渐变背景超出横幅区域。

6.3 三统计卡片

Row({ space: 10 }) {
  Column({ space: 4 }) {
    Text('荣誉值').fontSize(10).fontColor(COLORS.textSecondary)
    Text(this.honorScore + '').fontSize(19).fontWeight(FontWeight.Bold).fontColor(COLORS.gold)
    Text('+260 本周').fontSize(9).fontColor(COLORS.red)
  }
  .layoutWeight(1)
  .padding({ top: 12, bottom: 12 })
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
  .alignItems(HorizontalAlign.Center)
  .shadow({ radius: 6, color: '#11000000', offsetY: 2 })
  // ... 另外两个统计卡片
}
.width('100%')
.padding({ left: 12, right: 12 })
.margin({ top: -10 })

三个统计卡片在 Row 中等分排列,每个通过 layoutWeight(1) 获得三分之一的宽度。每个卡片是一个 Column,内含三行文字:标签(小号灰色)、数值(大号粗体彩色)、趋势信息(最小号)。数值使用不同颜色区分语义:荣誉值用金色、可接委托用绿色、团任务完成度用蓝色。

.shadow({ radius: 6, color: '#11000000', offsetY: 2 }) 为卡片添加阴影效果。radius 控制阴影的模糊半径(6vp),color 是阴影颜色(#11000000 是约 7% 透明度的黑色),offsetY 是垂直偏移量(2vp,向下偏移)。这种轻量级的阴影为白色卡片在浅色背景上创造了微妙的悬浮感,是 Material Design 阴影理念的简化实践。

.margin({ top: -10 }) 是一个有趣的细节:负的 top margin 使统计卡片向上偏移 10vp,与上方的横幅产生部分重叠。这种设计手法在移动端 UI 中常见于创建"卡片浮于横幅之上"的层次效果,打破了平铺直叙的线性布局,增添了视觉趣味性。

6.4 快捷宫格与 ForEach

Row() {
  ForEach(QUICK_ICONS, (qi: QuickIcon, idx: number) => {
    Column({ space: 5 }) {
      Text(qi.icon).fontSize(22)
      Text(qi.name).fontSize(10).fontColor(COLORS.textPrimary)
    }
    .layoutWeight(1)
    .padding({ top: 12, bottom: 12 })
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .alignItems(HorizontalAlign.Center)
    .scale(idx === 0 ? { x: 0.98, y: 0.98 } : { x: 1, y: 1 })
    .animation({ duration: 160, curve: Curve.EaseOut })
  }, (qi: QuickIcon, idx: number) => qi.id + '' + idx)
}
.width('100%')
.padding({ left: 12, right: 12 })

快捷宫格使用 ForEach 渲染 QUICK_ICONS 数组(八个快捷入口),每个入口在 Row 中通过 layoutWeight(1) 等分宽度。每个入口是一个 Column,包含 Emoji 图标和文字标签。.alignItems(HorizontalAlign.Center) 使内容居中对齐。

注意 .scale() 属性使用了三元运算符:第一个入口(idx === 0)的缩放为 98%,其他入口为 100%。配合 .animation() 属性,这创造了一个微妙的视觉差异——第一个入口略微"按下",暗示它是默认选中或推荐的操作。.animation({ duration: 160, curve: Curve.EaseOut }) 定义了 160 毫秒的缓出动画,当 scale 属性变化时会平滑过渡。

键值生成函数 (qi: QuickIcon, idx: number) => qi.id + '' + idxididx 拼接为字符串作为键值。加上 idx 是为了防止在数据更新时键值冲突——即使两个 QuickIcon 有相同的 id(虽然不应该发生),加上索引后也能保证唯一性。

6.5 骑士团横滑列表

Scroll() {
  Row({ space: 10 }) {
    ForEach(KNIGHTS, (k: Knight) => {
      Column({ space: 5 }) {
        Text(k.icon).fontSize(30)
        Text(k.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
        Text(k.title).fontSize(10).fontColor(COLORS.gold)
        Text(k.status).fontSize(9).fontColor(k.status === '在岗' ? COLORS.green : COLORS.blue)
      }
      .width(104)
      .padding({ top: 14, bottom: 14 })
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .alignItems(HorizontalAlign.Center)
      .shadow({ radius: 6, color: '#11000000', offsetY: 2 })
      .onClick(() => {
        this.onKnight(k)
      })
    }, (k: Knight) => k.id + '')
  }
  .padding({ left: 14, right: 14 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.height(150)

这是一个横向滚动列表,用于展示骑士团成员。外层 Scroll 通过 .scrollable(ScrollDirection.Horizontal) 设置为水平滚动方向(默认是垂直滚动)。内层 Row 包含多个骑士卡片,每个卡片宽度固定为 104vp,高度由内容决定。当所有卡片的总宽度超过 Scroll 的可视宽度时,用户可以水平滑动查看所有骑士。

每个骑士卡片是一个 Column,从上到下排列:图标(30号 Emoji)、姓名(13号粗体)、称号(10号金色)、状态(9号,颜色根据是否"在岗"动态切换)。.onClick(() => { this.onKnight(k) }) 绑定了点击事件,调用父组件传入的 onKnight 回调,将骑士对象 k 传递给父组件,由父组件打开骑士详情弹窗。

.height(150) 固定了滚动区域的高度。在 ArkTS 中,Scroll 组件需要明确的高度约束才能正确计算滚动范围。如果不设置高度,Scroll 会尝试占满可用空间,可能导致布局异常。

横向滚动列表是移动端展示"同类实体集合"的常用模式。相比网格布局,横向滚动节省垂直空间,且更符合移动端单手滑动的交互习惯。在 ArkTS 中实现横向滚动只需三个步骤:Scroll 容器 + .scrollable(ScrollDirection.Horizontal) + 内部 Row 包裹内容。

6.6 委托任务双列布局

Row({ space: 10 }) {
  Column({ space: 10 }) {
    ForEach(getReadyQuests(), (q: Quest) => {
      this.questCard(q, true)
    }, (q: Quest) => 'r' + q.id)
  }
  .layoutWeight(1)
  Column({ space: 10 }) {
    ForEach(getDoingQuests(), (q: Quest) => {
      this.questCard(q, false)
    }, (q: Quest) => 'd' + q.id)
  }
  .layoutWeight(1)
}
.width('100%')
.padding({ left: 12, right: 12 })

委托任务采用了双列布局:左列展示"可接"状态的任务,右列展示"进行中"状态的任务。两列通过 layoutWeight(1) 等分宽度,间距为 10vp。每列内部使用 ForEach 渲染对应过滤函数返回的任务列表。

questCard 是一个 @Builder 方法,它接收两个参数:q(任务对象)和 isReady(布尔值,表示是否为"可接"状态)。isReady 参数控制卡片底部按钮的文案和样式:为 true 时显示"去接取"(金色背景),为 false 时显示"进行中"(浅绿色背景)。这种设计避免了为两种状态编写两套卡片代码,通过参数化实现了代码复用。

键值函数在双列布局中使用了前缀区分:左列用 'r' + q.id,右列用 'd' + q.id。这确保了即使两列中有相同 id 的任务,键值也不会冲突。

6.7 团任务列表与斑马纹

Column({ space: 8 }) {
  ForEach(GUILD_TASKS, (t: GuildTask, idx: number) => {
    Row({ space: 10 }) {
      Text(t.icon).fontSize(24)
      Column({ space: 3 }) {
        Row() {
          Text(t.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).layoutWeight(1)
          Text(t.reward + ' 功勋').fontSize(10).fontColor(COLORS.gold)
        }
        .width('100%')
        Row() {
          Text('进度 ' + t.progress + '/' + t.target).fontSize(10).fontColor(COLORS.textSecondary).layoutWeight(1)
          Text('截止 ' + t.deadline).fontSize(10).fontColor(COLORS.textHint)
        }
        .width('100%')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      Text('详情').fontSize(11).fontColor(COLORS.green).padding(6).backgroundColor('#E8F0E8').borderRadius(8)
        .onClick(() => { this.onGuildTask(t) })
      Text('报名').fontSize(11).fontColor('#FFFFFF').padding(6).backgroundColor(COLORS.gold).borderRadius(8)
        .onClick(() => { this.onSign(t) })
    }
    .width('100%')
    .padding(12)
    .backgroundColor(idx % 2 === 0 ? '#FFFFFF' : '#FFF9EE')
    .borderRadius(12)
  }, (t: GuildTask, idx: number) => t.id + '' + idx)
}

团任务列表展示了 GUILD_TASKS 数组中的所有任务。每个任务行是一个 Row,从左到右排列:任务图标、任务信息区(名称+奖励、进度+截止日期)、详情按钮、报名按钮。任务信息区使用嵌套的 Row 实现两行信息:第一行是任务名和奖励值,第二行是进度和截止日期。每行内部通过 layoutWeight(1) 和固定宽度元素实现左右对齐。

.backgroundColor(idx % 2 === 0 ? '#FFFFFF' : '#FFF9EE') 实现了斑马纹效果——偶数索引行为白色背景,奇数索引行为浅米色背景。这种交替背景色是长列表中提升可读性的经典手法,它为每行创造了视觉分隔,帮助用户在快速滚动时不会"跳行"。

"详情"和"报名"两个按钮使用了 Text 组件而非 Button 组件,通过 paddingbackgroundColorborderRadius 手动构造成按钮外观。这种方式提供了比 Button 更细粒度的样式控制,同时减少了组件嵌套层级。两个按钮分别绑定了不同的回调:onGuildTask(t) 打开任务详情弹窗,onSign(t) 打开报名弹窗。

斑马纹(Zebra Striping)是列表 UI 设计中的经典技巧。通过交替的背景色,它为每行建立了视觉边界,在长列表滚动时尤为重要。在 ArkTS 中,利用 idx % 2 的奇偶判断配合三元运算符,可以一行代码实现斑马纹效果。

七、ArenaTab 竞技场页面组件

7.1 竞技场横幅与统计

@Component
struct ArenaTab {
  winStreak: number = 0;
  onBattle: (b: ArenaBattle) => void = () => {};
  onRank: (k: Knight) => void = () => {};

  build() {
    Scroll() {
      Column({ space: 12 }) {
        Column({ space: 6 }) {
          Row() {
            Column({ space: 4 }) {
              Text('ARENA').fontSize(12).fontColor('#FFB74D').letterSpacing(3)
              Text('竞技场 · 荣誉之战').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('S9 赛季 · 王者段位 · 剩余 12 天').fontSize(11).fontColor('#E8C87A')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('⚔️').fontSize(42)
          }
          .width('100%')
          .padding({ top: 16, left: 16, right: 16, bottom: 14 })

ArenaTab 组件的结构与 KnightTab 类似,但配色方案切换为酒红色系(#7B1E2B#B71C1C 的渐变),营造出竞技场的紧张氛围。横幅内部的统计区域使用了三个红色背景(#B71C1C)的圆角小卡片,分别展示连胜场次、胜率和排名。金色文字(#FFB74D)在红色背景上形成了强烈的视觉对比。

组件接收两个回调参数:onBattle 用于打开对决战报弹窗,onRank 用于打开骑士排名弹窗。这两个回调覆盖了竞技场页面的两个核心交互路径:查看历史战报和挑战对手。

7.2 段位进度与对决列表

Column({ space: 6 }) {
  Row() {
    Text('段位晋升').fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).layoutWeight(1)
    Text('王者 → 传奇').fontSize(11).fontColor(COLORS.gold)
  }
  .width('100%')
  Progress({ value: 68, total: 100, type: ProgressType.Linear })
    .width('100%')
    .height(8)
    .color(COLORS.gold)
    .backgroundColor('#EDE0CC')
}
.width('100%')
.padding(14)
.backgroundColor('#FFFFFF')
.borderRadius(12)

段位进度卡片使用 Progress 组件展示了从"王者"到"传奇"段位的晋升进度。value: 68, total: 100 表示当前完成了 68%。进度条颜色为金色(COLORS.gold),背景为浅米色(#EDE0CC),与整体主题配色保持一致。这段代码体现了 Progress 组件的基本使用方式——通过 valuetotal 的比值计算进度比例,自动渲染对应宽度的填充条。

Column({ space: 8 }) {
  ForEach(ARENA_BATTLES, (b: ArenaBattle, idx: number) => {
    Row({ space: 10 }) {
      Column() {
        Text(b.icon).fontSize(24)
      }
      .width(40)
      .alignItems(HorizontalAlign.Center)
      Column({ space: 3 }) {
        Text(b.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
        Text(b.time + ' · ' + b.field).fontSize(10).fontColor(COLORS.textSecondary)
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      Text(b.result).fontSize(12).fontWeight(FontWeight.Bold)
        .fontColor(b.result === '胜' ? COLORS.red : (b.result === '负' ? COLORS.green : COLORS.gold))
        .padding({ left: 10, right: 10, top: 4, bottom: 4 })
        .backgroundColor(b.result === '胜' ? '#FDECEA' : (b.result === '负' ? '#E8F0E8' : '#FFF3DC'))
        .borderRadius(10)
    }
    .width('100%')
    .padding(12)
    .backgroundColor(idx % 2 === 0 ? '#FFFFFF' : '#FFF9EE')
    .borderRadius(12)
    .onClick(() => { this.onBattle(b) })
  }, (b: ArenaBattle, idx: number) => b.id + '' + idx)
}

对决列表渲染了 ARENA_BATTLES 数组中的所有对决记录。每行包含对决图标、对手名称+时间+场地、以及结果标签。结果标签的样式通过嵌套三元运算符实现三态切换:"胜"为红色文字+红色背景、"负"为绿色文字+绿色背景、"平"为金色文字+金色背景。这种根据数据值动态切换样式的方式是声明式 UI 的典型应用场景。

.onClick(() => { this.onBattle(b) }) 绑定了整行的点击事件,点击任意一行对决记录都会打开战报弹窗,展示该场对决的详细信息。整行可点击的设计提供了更大的点击热区,提升了移动端的操作便利性。

八、GearTab 装备页面组件

8.1 部位筛选 Chips

Scroll() {
  Row({ space: 8 }) {
    ForEach(['武器', '防具', '饰品', '传说', '史诗', '稀有'], (part: string, idx: number) => {
      Text(part).fontSize(11)
        .fontColor(idx === 0 ? '#FFFFFF' : COLORS.textPrimary)
        .padding({ left: 14, right: 14, top: 6, bottom: 6 })
        .backgroundColor(idx === 0 ? COLORS.gold : '#FFFFFF')
        .borderRadius(14)
    }, (part: string, idx: number) => part + idx)
  }
  .padding({ left: 12, right: 12 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.height(36)

部位筛选条是一个横向滚动的 Chip 列表,每个 Chip 是一个 Text 组件配以圆角背景。当前选中的 Chip(idx === 0)显示为金色背景白色文字,未选中的显示为白色背景深色文字。这些 Chip 目前是纯展示性的(未绑定 onClick 事件和筛选逻辑),在真实项目中可以通过添加 @State selectedPart: string 状态变量和点击事件来实现实际的筛选功能。

.height(36) 固定了筛选条的高度。这个高度经过精心设计——足以容纳文字和上下 padding,同时不会占用过多垂直空间。在移动端 UI 中,筛选/标签条的典型高度在 32-40vp 之间。

8.2 装备卡片与边框颜色参数

@Builder
gearCard(g: Gear, accent: string) {
  Column({ space: 5 }) {
    Text(g.icon).fontSize(26)
    Text(g.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).maxLines(1)
    Text(g.quality + ' · ' + g.part).fontSize(10).fontColor(accent)
    Text('攻击 ' + g.attack).fontSize(10).fontColor(COLORS.red)
    Text('兑换').fontSize(10).fontColor('#FFFFFF').padding({ left: 16, right: 16, top: 4, bottom: 4 })
      .backgroundColor(COLORS.gold).borderRadius(10)
      .onClick(() => { this.onForge(g) })
  }
  .width('100%')
  .padding(12)
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
  .alignItems(HorizontalAlign.Center)
  .border({ width: 1, color: accent })
  .onClick(() => { this.onGear(g) })
}

gearCard 是装备卡片的 @Builder 方法,它接收一个 accent 参数作为强调色。这种设计允许同一个卡片构建器以不同的边框颜色渲染不同品质的装备:传说品质传入 COLORS.gold(金色边框),史诗品质传入 COLORS.purple(紫色边框)。通过参数化颜色,避免了为每种品质编写独立的卡片代码。

.maxLines(1)Text 组件的属性,限制文字最大行数为 1 行。当装备名称过长时,超出部分会被截断(默认以省略号显示)。这在固定宽度的卡片布局中很重要——它防止长名称撑破卡片布局或导致多行文字影响视觉一致性。

.border({ width: 1, color: accent }) 为卡片添加 1vp 宽的边框,颜色为传入的 accent 参数。这个边框是品质区分的主要视觉手段——金色边框的卡片在列表中一眼可辨为传说品质,紫色边框则为史诗品质。

8.3 材料横滑与库存预警

ForEach(MATERIALS, (m: Material) => {
  Column({ space: 5 }) {
    Text(m.icon).fontSize(26)
    Text(m.name).fontSize(12).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
    Text('库存 ' + m.stock).fontSize(9).fontColor(m.stock < 30 ? COLORS.danger : COLORS.textSecondary)
    Text(m.rarity).fontSize(9).fontColor(COLORS.gold)
  }
  .width(100)
  .padding({ top: 12, bottom: 12 })
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
  .alignItems(HorizontalAlign.Center)
  .shadow({ radius: 5, color: '#11000000', offsetY: 2 })
  .onClick(() => { this.onMat(m) })
}, (m: Material) => m.id + '')

材料横滑列表展示了所有锻造材料。每个材料卡片中,库存数值的颜色通过条件判断实现预警:当 m.stock < 30 时使用红色(COLORS.danger),否则使用灰色(COLORS.textSecondary)。这种基于数据阈值的颜色切换,让用户在浏览列表时能立即识别出库存紧张的材料,无需逐一阅读数字。

页面标题处也提供了库存预警汇总:'库存紧张 ' + getLowStockMats().length + ' 种',调用 getLowStockMats() 过滤函数计算库存低于 30 的材料种类数量,在标题行以红色(COLORS.danger)显示。这种"列表级预警 + 卡片级预警"的双重提示机制,确保了关键信息不会被遗漏。

九、TavernTab 酒馆页面组件

9.1 酒水双列与饮品卡片

Row({ space: 10 }) {
  Column({ space: 10 }) {
    ForEach(getEvenDrinks(), (d: Drink) => {
      this.drinkCard(d)
    }, (d: Drink) => 'l' + d.id)
  }
  .layoutWeight(1)
  Column({ space: 10 }) {
    ForEach(getOddDrinks(), (d: Drink) => {
      this.drinkCard(d)
    }, (d: Drink) => 'r' + d.id)
  }
  .layoutWeight(1)
}
.width('100%')

    .width('100%')
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .alignItems(HorizontalAlign.Center)
    .scale({ x: 0.98, y: 0.98 })
    .animation({ duration: 160, curve: Curve.EaseOut })
  }
}


在这里插入图片描述

结语

本文对一个完整的鸿蒙 ArkTS 应用进行了逐段代码剖析,从颜色系统定义、数据模型接口设计、Mock 数据与全局过滤函数,到主入口组件的状态管理体系、Tab 路由机制、Stack 弹窗架构,再到五个页面组件的具体实现细节和十六个弹窗构建器的交互逻辑,覆盖了 ArkTS 声明式 UI 开发的核心技术维度。

在数据层面,项目通过接口(interface)定义了十二种实体类型,构建了完整的类型安全数据层。全局纯函数负责数据过滤和统计,与组件逻辑分离,保证了数据处理的可测试性和可维护性。Mock 数据的使用使得前端开发不依赖后端 API 即可独立推进,体现了前后端解耦的工程理念。

在组件层面,项目遵循了"数据向下、事件向上"的单向数据流原则。父组件 Index 持有所有状态(Tab 索引、弹窗开关、选中对象、表单字段),通过参数将数据传递给五个子页面组件,子组件通过回调函数将用户交互事件上报给父组件。这种设计使得状态变更的来源清晰可追溯,避免了多组件直接修改共享状态导致的混乱。@Builder 装饰器被广泛用于封装弹窗和卡片等无状态 UI 片段,在保持代码复用性的同时避免了组件过度拆分。

在布局层面,项目综合运用了 Stack(Z 轴层叠弹窗)、Column(垂直排列)、Row(水平排列)、Scroll(滚动容器)四种核心布局容器,构建了从页面骨架到卡片内部的完整布局体系。layoutWeight 实现弹性等分布局,FlexAlignHorizontalAlign 枚举提供了精细的对齐控制。ForEach 配合键值生成函数实现了高效的列表渲染,斑马纹背景和条件样式切换则展示了声明式 UI 在视觉表达上的灵活性。

在交互层面,@State 装饰器的响应式追踪机制是整个应用交互逻辑的基石。从 Tab 切换到弹窗开关,从数量步进到星级选择,每一次状态变化都自动触发精确的 UI 局部更新。Progress 组件、animation 动画属性、shadow 投影属性、linearGradient 渐变背景等视觉增强手段的综合运用,使得应用在功能完备的同时具备了出色的视觉表现力。整个项目展示了 ArkTS 作为鸿蒙原生开发语言在组件化、类型安全、响应式状态管理和声明式 UI 四个维度上的完整能力,为开发者提供了一个可参考的实践范例。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐