在鸿蒙(HarmonyOS)应用开发生态中,ArkTS作为核心声明式开发语言,凭借其类型安全、声明式UI范式和高效的状态管理机制,正在重新定义跨设备应用的开发体验。本文将以一个完整的精灵养成类游戏应用为切入点,从数据模型定义、静态数据组织、工具函数封装、多页面组件构建、主组件状态管理到弹窗交互系统,逐层拆解ArkTS在复杂业务场景下的工程实践,帮助开发者深入理解声明式UI的精髓与鸿蒙原生开发的核心技术要点。

一、技术背景与整体架构概览

鸿蒙操作系统的应用开发框架提供了一套基于ArkTS的声明式UI开发范式。ArkTS在TypeScript的基础上进行了扩展和优化,加入了@Component@State@Builder@Entry等装饰器,使得开发者可以用更简洁、更类型安全的方式构建用户界面。与传统的命令式UI开发不同,声明式UI的核心思想是"UI是状态的函数映射"——开发者只需关注状态数据的变化,框架会自动驱动UI的更新与重渲染。

本应用是一个名为"幻灵秘境"的精灵养成管理系统,属于典型的游戏化运营类应用。它涵盖了首页概览、精灵培养、秘境探索、任务系统、商城购物、图鉴收集和个人中心共七大功能模块,同时集成了十六种不同类型的弹窗交互,包括详情展示、表单编辑、底部抽屉、确认操作和奖励领取等多种交互形态。

从架构层面来看,整个应用采用了"数据层-逻辑层-视图层"三层分离的设计思想。数据层通过接口定义和常量数组管理所有业务数据;逻辑层通过一组纯函数处理数据拆分、颜色映射和查找检索;视图层则由一个主入口组件和七个独立子组件构成,通过回调函数实现父子组件间的通信。

在技术栈选型上,本应用充分运用了ArkTS的以下核心能力:使用interface进行严格的类型约束,确保数据在编译期就具备类型安全性;使用struct配合@Component装饰器构建可复用的UI组件;使用@State装饰器管理组件内部的可变状态,实现响应式数据绑定;使用@Builder装饰器封装可复用的UI片段,减少代码冗余;使用ForEach进行列表渲染,配合键值生成器实现高效的差量更新;使用Scroll组件处理内容滚动;使用linearGradientshadowborderRadius等样式属性实现精致的视觉效果。

应用入口 Index 主组件

HomeTab 首页

SpiritTab 精灵页

RealmTab 秘境页

QuestTab 任务页

ShopTab 商店页

CodexTab 图鉴页

MineTab 我的页

16种弹窗交互系统

在这里插入图片描述

整个应用的状态管理采用集中式模式。主组件Index作为状态中枢,持有所有弹窗的显隐状态、选中数据对象以及表单填写状态。当用户在任意子页面触发交互时,子组件通过回调函数将事件冒泡到主组件,由主组件统一处理状态变更和弹窗展示。这种设计模式确保了状态的可追踪性和单向数据流,避免了多组件间状态同步的混乱问题。

二、数据模型层:接口定义与类型约束

在任何严肃的ArkTS应用中,数据模型的定义都是工程的第一步。良好的类型定义不仅能帮助开发者在编码阶段捕获错误,还能显著提升代码的可读性和可维护性。本应用定义了十个核心接口,覆盖了精灵、秘境、任务、商品、图鉴、收藏、成就等全部业务实体。

2.1 核心业务实体接口

interface SpiritItem {
  id: number;
  name: string;
  elem: string;
  rare: string;
  level: number;
  power: number;
  img: string;
  intro: string;
  tags: string;
}

interface RealmItem {
  id: number;
  name: string;
  diff: string;
  power: number;
  reward: string;
  desc: string;
}

interface QuestItem {
  id: number;
  name: string;
  type: string;
  point: number;
  progress: number;
  total: number;
  reward: string;
}

在这里插入图片描述

SpiritItem接口定义了精灵实体的完整数据结构。其中id是精灵的唯一标识符,用于在列表渲染和查找检索中作为主键。nameimg分别存储精灵的显示名称和Emoji图标——在本应用中,图片资源统一使用Emoji字符代替,这是一种轻量化的资源管理策略,避免了外部图片资源的加载开销。elem字段表示元素属性(光、雷、暗、火等),rare字段表示稀有度等级(传说、史诗、稀有、普通),这两个字段将用于后续的颜色映射逻辑。

RealmItem接口定义了秘境探索区域的数据模型。diff字段存储难度等级,power表示推荐的战力门槛,reward是完成探索后的奖励描述。desc字段提供秘境的背景描述文本,用于在详情弹窗中展示沉浸式的世界观内容。

QuestItem接口定义了任务条目的数据结构。特别值得关注的是progresstotal这一对字段,它们构成了任务进度的核心数据模型——progress表示当前完成进度,total表示总目标量。这对字段将在UI层被用于计算进度条百分比,公式为progress * 100 / total,实现动态的进度可视化。

在ArkTS的类型系统中,接口(interface)不仅用于声明对象的结构,还用于约束数组元素的类型。通过SpiritItem[]这样的类型注解,编译器能在数组初始化时对每个元素的字段进行逐项检查,确保不会出现字段缺失或类型不匹配的运行时错误。这是声明式开发范式中数据可靠性的第一道防线。

2.2 辅助实体与配置接口

interface ShopItem {
  id: number;
  name: string;
  price: number;
  oldPrice: number;
  img: string;
  tag: string;
}

interface CodexItem {
  id: number;
  name: string;
  elem: string;
  rare: string;
  img: string;
  desc: string;
}

interface FavItem {
  id: number;
  name: string;
  elem: string;
  level: number;
  img: string;
  time: string;
}

interface AchItem {
  id: number;
  name: string;
  desc: string;
  point: number;
  done: boolean;
  img: string;
}

在这里插入图片描述

ShopItem接口中priceoldPrice的双价格设计是一个值得注意的建模决策。通过同时存储现价和原价,UI层可以直接计算折扣差额(oldPrice - price),在商品卡片上展示"立省 ¥XX"的促销信息。这种数据冗余虽增加了少量存储,但简化了UI层的计算逻辑,是一种典型的以空间换时间的设计权衡。

AchItem接口中的done字段是一个布尔类型,用于标记成就是否已达成。这个字段在UI层有着重要的作用——它不仅决定了成就图标的透明度(已达成为完全不透明,未达成为35%透明度),还控制了文字颜色的变化(已达成为深色,未达成为灰色),实现了视觉上的状态区分。

2.3 统计与配置数据接口

interface ElemPower {
  name: string;
  value: number;
}

interface GrowthItem {
  name: string;
  cost: string;
  add: string;
}

interface QuestType {
  name: string;
  color: string;
}

ElemPower接口用于元素之力分布柱状图的数据源,name是元素名称,value是对应的战力数值。GrowthItem接口定义了精灵培养选项的数据模型,costadd分别以字符串形式存储消耗和收益描述,这种设计使得UI层可以直接渲染文本而无需进行额外的格式化拼接。QuestType接口将任务类型名称与其对应的颜色值绑定,形成了类型-颜色的映射配置。

三、静态数据层:业务数据的组织与管理

在ArkTS应用中,静态业务数据通常以const常量数组的形式定义在模块顶层。这种方式的优势在于数据在编译期就已确定,运行时无需额外的初始化开销,且全局可访问。本应用定义了十二组静态数据常量,构成了整个应用的"数据底座"。

3.1 精灵列表与首页精灵数据

const SPIRIT_LIST: SpiritItem[] = [
  { id: 1, name: '萤火小仙', elem: '光', rare: '传说', level: 45, power: 860, img: '🧚', intro: '栖息在萤火森林深处的光系精灵,擅长治愈队友并驱散暗影。', tags: '治愈 驱散 光盾' },
  { id: 2, name: '岩壳小宝', elem: '土', rare: '史诗', level: 38, power: 720, img: '🪨', intro: '背负岩石铠甲的憨厚精灵,防御力出众,是团队最可靠的盾。', tags: '护盾 嘲讽 壁垒' },
  { id: 3, name: '风铃雀', elem: '风', rare: '稀有', level: 32, power: 610, img: '🐦', intro: '飞行速度极快的风系精灵,能在秘境中为团队探路引航。', tags: '加速 侦查 闪避' },
  // ... 更多精灵数据
];

const HOME_SPIRITS: SpiritItem[] = [
  { id: 1, name: '萤火小仙', elem: '光', rare: '传说', level: 45, power: 860, img: '🧚', intro: '', tags: '' },
  { id: 6, name: '雷纹豹', elem: '雷', rare: '传说', level: 48, power: 920, img: '🐆', intro: '', tags: '' },
  // ... 首页精选精灵
];

在这里插入图片描述

SPIRIT_LIST是应用的核心数据源,包含了十二只精灵的完整信息。每只精灵都有独特的元素属性、稀有度、等级和战力数值。值得注意的是,introtags字段存储了精灵的背景故事和技能标签,这些文本数据将在精灵详情弹窗中展示,为用户提供沉浸式的阅读体验。

HOME_SPIRITS是首页横滑展示的精选精灵子集,从SPIRIT_LIST中按特定策略筛选了八只高稀有度精灵。这个数组的特殊之处在于introtags字段被置为空字符串——因为首页卡片仅需展示精灵的名称、图标、元素、等级和稀有度,不需要完整的介绍信息,空字符串的设计避免了首页场景下不必要的数据传输和渲染开销。

从数据组织策略来看,这种"全量数据+精选子集"的设计模式在游戏应用中非常常见。全量数据服务于详情场景(需要展示完整信息),精选子集服务于概览场景(仅需展示摘要信息)。两者共享同一套接口定义但使用不同的数据填充策略,实现了数据复用与场景适配的平衡。

3.2 秘境、任务与商品数据

const REALM_LIST: RealmItem[] = [
  { id: 1, name: '萤火森林', diff: '简单', power: 300, reward: '灵晶×200', desc: '光系精灵栖息地,适合新手采集灵晶与光之碎片。' },
  { id: 2, name: '雷鸣峡谷', diff: '普通', power: 480, reward: '雷纹石×150', desc: '雷云常年不散,谷内闪电不断,是雷系精灵的试炼场。' },
  { id: 6, name: '熔岩王座', diff: '噩梦', power: 1050, reward: '火种圣物×1', desc: '远古火精灵的沉睡之地,只有强者才能踏入王座。' },
  // ... 更多秘境数据
];

const QUEST_LIST: QuestItem[] = [
  { id: 1, name: '喂食精灵三次', type: '日常', point: 20, progress: 3, total: 3, reward: '灵晶×30' },
  { id: 4, name: '收集图鉴 5 种精灵', type: '周常', point: 60, progress: 5, total: 8, reward: '唤灵券×1' },
  { id: 7, name: '完成十次探索', type: '成就', point: 150, progress: 8, total: 10, reward: '限定称号' },
  // ... 更多任务数据
];

REALM_LIST定义了八个秘境区域,难度从"简单"到"噩梦"递进。power字段表示推荐战力,这个数值在UI层将通过getDiffColor函数映射为不同的颜色标签——简单为绿色、普通为橙色、困难为红色、噩梦为深紫色,形成了直观的难度视觉梯度。

QUEST_LIST定义了十项任务,涵盖日常、周常和成就三种类型。每项任务的progresstotal字段构成了进度模型。例如"收集图鉴 5 种精灵"的progress为5、total为8,意味着已完成5/8,进度条将显示62.5%的填充比例。任务类型通过getTaskColor函数映射为不同颜色——日常为绿色(#43A047)、周常为橙色(#FF8F00)、成就为玫红色(#C2185B)。

3.3 图鉴、收藏与成就数据

const CODEX_LIST: CodexItem[] = [
  { id: 1, name: '萤火小仙', elem: '光', rare: '传说', img: '🧚', desc: '光系治愈精灵,技能「萤光雨」可驱散全队负面状态。' },
  { id: 2, name: '雷纹豹', elem: '雷', rare: '传说', img: '🐆', desc: '雷系爆发精灵,「雷霆突袭」可对单体造成巨额伤害。' },
  // ... 更多图鉴数据
];

const ACH_LIST: AchItem[] = [
  { id: 1, name: '初入秘境', desc: '首次完成秘境探索', point: 50, done: true, img: '🏅' },
  { id: 5, name: '噩梦征服者', desc: '首次挑战噩梦秘境', point: 300, done: false, img: '🔥' },
  // ... 更多成就数据
];

const ELEM_POWER: ElemPower[] = [
  { name: '光', value: 860 },
  { name: '雷', value: 920 },
  { name: '暗', value: 980 },
  { name: '火', value: 790 },
  { name: '星', value: 840 },
  { name: '木', value: 700 }
];

在这里插入图片描述

CODEX_LIST是图鉴页的数据源,每个图鉴条目都包含精灵的技能描述。ACH_LIST是成就系统的数据源,done字段决定成就的展示状态。ELEM_POWER是秘境页柱状图的数据源,六个元素类型的战力值将转化为柱状图的高度——通过getBarHeight函数将战力值除以10得到以vp为单位的柱体高度(如980/10=98vp),实现数据的可视化映射。

四、工具函数层:数据拆分与映射逻辑

本应用定义了三十余个工具函数,它们是连接数据层和视图层的桥梁。这些函数主要分为三类:数据拆分函数(将扁平数组按列拆分为二维结构以适配网格布局)、颜色映射函数(将业务属性映射为颜色值)和数据查找函数(根据ID从数组中检索特定项)。

4.1 数据拆分函数群

function getSpiritRows(): SpiritItem[] {
  let arr: SpiritItem[] = [];
  for (let i = 0; i < SPIRIT_LIST.length; i += 2) {
    arr.push(SPIRIT_LIST[i]);
  }
  return arr;
}

function getSpiritRows2(): SpiritItem[] {
  let arr: SpiritItem[] = [];
  for (let i = 1; i < SPIRIT_LIST.length; i += 2) {
    arr.push(SPIRIT_LIST[i]);
  }
  return arr;
}

在这里插入图片描述

getSpiritRowsgetSpiritRows2是一对配对使用的数据拆分函数,它们的目的是将一维的精灵列表拆分为两个子数组,分别用于双列网格布局的左列和右列。getSpiritRows从索引0开始,步长为2,提取偶数位置(第1、3、5…个)的精灵;getSpiritRows2从索引1开始,步长为2,提取奇数位置(第2、4、6…个)的精灵。

这种拆分策略在ArkTS的声明式UI中是必要的。因为ArkTS的ForEach组件本质上是线性渲染的,它不像CSS Grid或Flutter的GridView那样能自动将一维数据排列为多列网格。要实现双列瀑布流布局,开发者需要手动将数据拆分为多个一维数组,然后在每个Column容器中分别使用ForEach渲染。这种模式虽然略显繁琐,但赋予了开发者对布局的完全控制力。

值得注意的是,同样的拆分模式被重复应用于精灵、秘境、任务、商品、图鉴、收藏、成就共七组数据,每组都根据其网格列数定义了相应数量的拆分函数。例如图鉴页使用三列布局,因此定义了getCodexRowsgetCodexRows2getCodexRows3三个函数,分别从索引0、1、2开始以步长3提取数据。这种高度一致的代码模式暗示了抽象的潜力——在更成熟的工程中,可以通过泛型函数splitRows<T>(list: T[], colCount: number, colIndex: number): T[]来统一处理所有拆分逻辑,大幅减少样板代码。

4.2 颜色映射函数群

function getElemColor(e: string): string {
  if (e === '光') {
    return '#FFC107';
  }
  if (e === '雷') {
    return '#5C6BC0';
  }
  if (e === '暗') {
    return '#7E57C2';
  }
  if (e === '火') {
    return '#EF5350';
  }
  if (e === '星') {
    return '#26C6DA';
  }
  if (e === '木') {
    return '#66BB6A';
  }
  if (e === '冰') {
    return '#4FC3F7';
  }
  if (e === '风') {
    return '#AED581';
  }
  if (e === '水') {
    return '#42A5F5';
  }
  if (e === '土') {
    return '#A1887F';
  }
  return '#9E9E9E';
}

function getRareColor(r: string): string {
  if (r === '传说') {
    return '#FF8F00';
  }
  if (r === '史诗') {
    return '#7E57C2';
  }
  if (r === '稀有') {
    return '#29B6F6';
  }
  return '#78909C';
}

getElemColor函数将十种元素属性映射为对应的十六进制颜色值。光系为琥珀色(#FFC107)、雷系为靛蓝色(#5C6BC0)、暗系为紫色(#7E57C2)、火系为红色(#EF5350)等。这些颜色值的选择遵循了游戏UI设计中"元素-色彩"的直觉映射原则——火系用红色、水系用蓝色、木系用绿色,使玩家能够通过颜色快速识别元素属性。

getRareColor函数将四种稀有度映射为颜色——传说为金色(#FF8F00)、史诗为紫色(#7E57C2)、稀有为蓝色(#29B6F6)、普通为灰蓝色(#78909C)。这种"金-紫-蓝-灰"的稀有度色彩体系是游戏行业通用的设计规范,能够唤起玩家对稀有度等级的本能认知。

在ArkTS中,颜色值统一使用十六进制字符串(如’#FFC107’)或rgba字符串(如’rgba(255,255,255,0.15)')表示。这些颜色值通过.fontColor().backgroundColor().borderColor()等样式属性应用到组件上。值得注意的是,ArkTS的颜色系统支持透明度——既可以通过八位十六进制(如#80FFC107)也可以通过rgba函数表达,这为UI设计提供了精细的透明度控制能力。

4.3 难度与任务类型颜色映射

function getDiffColor(d: string): string {
  if (d === '简单') {
    return '#43A047';
  }
  if (d === '普通') {
    return '#FF8F00';
  }
  if (d === '困难') {
    return '#C62828';
  }
  return '#4A148C';
}

function getTaskColor(t: string): string {
  for (let i = 0; i < QUEST_TYPE.length; i++) {
    if (QUEST_TYPE[i].name === t) {
      return QUEST_TYPE[i].color;
    }
  }
  return '#9E9E9E';
}

getDiffColor将秘境难度映射为从绿到紫的颜色梯度——简单为绿色(#43A047)、普通为橙色(#FF8F00)、困难为深红色(#C62828)、噩梦为深紫色(#4A148C)。这种颜色梯度设计遵循了"绿-黄-红-紫"的难度递进色彩心理学,使玩家能够直观感知挑战的严峻程度。

getTaskColor采用了与前面函数不同的实现方式——它不是通过if-else链式判断,而是通过遍历QUEST_TYPE常量数组来查找匹配的颜色。这种设计将颜色配置与映射逻辑解耦:如果未来需要新增任务类型,只需在QUEST_TYPE数组中添加新条目,而无需修改函数体,体现了开闭原则的设计思想。

4.4 UI辅助与选项数据函数

function getQuickEntries(): string[] {
  let arr: string[] = ['秘境探索', '精灵培养', '每日任务', '唤灵召唤'];
  return arr;
}

function getQuickIcon(i: number): string {
  if (i === 0) {
    return '🗺️';
  }
  if (i === 1) {
    return '🪄';
  }
  if (i === 2) {
    return '📜';
  }
  return '🎴';
}

function getBarHeight(v: number): number {
  return v / 10;
}

getQuickEntriesgetQuickIcon是配对使用的函数,前者返回快捷入口的名称数组,后者根据索引返回对应的Emoji图标。getBarHeight是一个简洁但关键的函数——它将战力数值转化为柱状图的像素高度,除数10是一个经过UI调试确定的缩放系数,确保最高战力值(980)对应的柱体高度为98vp,在视觉上不会溢出柱状图容器。

function getPayLevels(): number[] {
  let arr: number[] = [6, 30, 68, 128, 328, 648];
  return arr;
}

function getPayGifts(): string[] {
  let arr: string[] = ['灵晶×60', '灵晶×330', '灵晶×780', '灵晶×1580', '灵晶×3880', '灵晶×7880'];
  return arr;
}

function getTabIcon(label: string): string {
  if (label === '首页') {
    return '🏠';
  }
  if (label === '精灵') {
    return '🧚';
  }
  if (label === '秘境') {
    return '🗺️';
  }
  if (label === '任务') {
    return '📜';
  }
  if (label === '商店') {
    return '🛒';
  }
  if (label === '图鉴') {
    return '📖';
  }
  return '👤';
}

getPayLevelsgetPayGifts返回充值档位和对应奖励的数组。六个充值档位(6/30/68/128/328/648元)对应了移动游戏行业的标准定价阶梯。getTabIcon将底部Tab标签映射为Emoji图标,通过if-else链式判断实现了一对一的名称-图标映射关系。

五、首页组件:HomeTab的结构与交互

首页是用户进入应用后看到的第一个页面,承担着信息概览和快捷导航的双重职责。HomeTab组件的设计充分体现了ArkTS声明式UI的组件化思想——通过Column嵌套Scroll再嵌套Column的层级结构,构建了一个可滚动的纵向内容流。

5.1 组件声明与回调接口

struct HomeTab {
  onOpenSpirit: (id: number) => void = () => {
  }
  onOpenRealm: (id: number) => void = () => {
  }
  onOpenQuest: (id: number) => void = () => {
  }
  onToast: (msg: string) => void = () => {
  }

  build() {
    // ...
  }
}

HomeTab是一个struct结构体组件,它没有使用@Component装饰器(注意:在ArkTS中,struct本身即是组件的声明,@Component装饰器用于需要特殊处理的复杂组件)。组件定义了四个回调函数类型的属性:onOpenSpiritonOpenRealmonOpenQuestonToast。这些回调函数是子组件向父组件通信的唯一通道。

每个回调属性都有一个默认的空函数实现() => {},这是一种防御性编程策略——如果父组件没有传入某个回调,子组件调用时不会报错,而是静默忽略。这种设计使得HomeTab可以独立测试和预览,不需要依赖父组件的完整实现。

在ArkTS的组件通信模型中,数据流是单向的:父组件通过属性向下传递数据和回调,子组件通过调用回调向上通知事件。子组件不能直接修改父组件的状态,只能通过回调函数间接请求父组件执行状态变更。这种单向数据流的设计避免了双向绑定带来的状态追踪困难,使得应用的状态变化路径清晰可预测。

5.2 顶部渐变横幅

Column() {
  Row() {
    Text('🌿')
      .fontSize(26)
    Text('')
      .layoutWeight(1)
    Text('Lv.42 · 幻灵师')
      .fontSize(11)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFC107')
  }
  .width('100%')
  Text('幻灵秘境')
    .fontSize(24)
    .fontWeight(FontWeight.Bold)
    .fontColor('#FFFFFF')
    .width('100%')
    .margin({ top: 10 })
  Text('员工精灵养成馆 · 与灵相伴')
    .fontSize(12)
    .fontColor('#C8E6C9')
    .width('100%')
    .margin({ top: 6 })
  Row() {
    Text('🪙 灵晶 12860')
      .fontSize(11)
      .fontColor('#FFFFFF')
      .padding({ left: 12, right: 12, top: 6, bottom: 6 })
      .backgroundColor('rgba(255,255,255,0.15)')
      .borderRadius(12)
    Text('')
      .layoutWeight(1)
    Text('✨ 去唤灵')
      .fontSize(11)
      .fontWeight(FontWeight.Bold)
      .fontColor('#1B5E20')
      .padding({ left: 14, right: 14, top: 6, bottom: 6 })
      .backgroundColor('#FFC107')
      .borderRadius(12)
      .onClick(() => {
        this.onToast('唤灵召唤(每日 1 次免费)');
      })
  }
  .width('100%')
  .margin({ top: 14 })
}
.width('100%')
.padding({ left: 18, right: 18, top: 18, bottom: 18 })
.linearGradient({ angle: 135, colors: [['#0E3B2E', 0], ['#1B5E20', 0.6], ['#2E7D32', 1]] })
.borderRadius(16)
.margin({ left: 16, right: 16, top: 12 })
.shadow({ radius: 10, color: 'rgba(14,59,46,0.35)', offsetY: 5 })

这段代码构建了首页的顶部横幅区域。最外层的Column容器通过.linearGradient设置了135度角的线性渐变背景,颜色从深绿(#0E3B2E)到中绿(#1B5E20)再到亮绿(#2E7D32),营造了森林秘境的视觉氛围。.borderRadius(16)设置了16vp的圆角,.shadow添加了带垂直偏移的阴影效果,使横幅具有浮起于背景之上的层次感。

横幅内部的内容分为三层结构。第一层是Row容器中的叶子Emoji、弹性占位和等级标签——Text('').layoutWeight(1)是一个常用的布局技巧,通过一个空文本组件占据剩余空间,将两侧的元素推到容器的两端。第二层是应用标题"幻灵秘境"和副标题,使用不同的字号和颜色形成视觉层次。第三层是灵晶余额和唤灵按钮,灵晶余额使用半透明白色背景的圆角标签,唤灵按钮使用琥珀色背景和深绿色文字,形成鲜明的视觉对比。

linearGradient是ArkTS中实现渐变背景的核心属性。其参数angle控制渐变方向(以度为单位,135度表示从左上到右下),colors数组定义了渐变断点——每个断点是一个[颜色值, 位置]的元组,位置值范围0到1。通过多个断点的组合,可以实现从双色到多色的复杂渐变效果。在本应用中,三色渐变被大量用于横幅、卡片头部和弹窗背景,是营造"森林游戏风"视觉风格的关键技术手段。

5.3 快捷入口宫格

Row() {
  ForEach(getQuickEntries(), (qe: string, qi: number) => {
    Column() {
      Text(getQuickIcon(qi))
        .fontSize(22)
      Text(qe)
        .fontSize(10)
        .fontColor('#424242')
        .margin({ top: 4 })
    }
    .layoutWeight(1)
    .padding({ top: 12, bottom: 12 })
    .backgroundColor('#F1F8E9')
    .borderRadius(12)
    .onClick(() => {
      this.onToast(qe + '入口');
    })
  }, (qe: string, qi: number) => qe + String(qi))
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
.justifyContent(FlexAlign.SpaceBetween)

这段代码实现了首页的四个快捷入口宫格。ForEach遍历getQuickEntries()返回的四个入口名称,为每个入口渲染一个Column容器——内含Emoji图标和文字标签。.layoutWeight(1)使四个宫格等宽分布,.justifyContent(FlexAlign.SpaceBetween)确保宫格之间的间距均匀分配。

ForEach的第二个参数是键值生成器(key generator)(qe: string, qi: number) => qe + String(qi)。键值生成器为每个列表项生成唯一标识符,ArkTS框架使用这个标识符进行差量更新——当数据变化时,框架会比较新旧键值集合,只重新渲染变化的项,而非整个列表。将名称和索引拼接为键值是一种常见的策略,既保证了唯一性又便于调试。

FlexAlign是ArkTS中控制Flex布局主轴对齐方式的枚举类型。SpaceBetween表示首尾贴边、中间均匀分布;SpaceAround表示每个元素两侧有相等的间距(包括首尾);SpaceEvenly表示所有间隙完全相等(包括首尾)。在本例中,四个宫格使用SpaceBetween对齐,使它们在Row容器中均匀撑满宽度,形成了标准的四宫格布局。

5.4 精灵横滑列表

Scroll() {
  Row() {
    ForEach(getHomeSpirits(), (sp: SpiritItem, si: number) => {
      Column() {
        Text(sp.img)
          .fontSize(36)
        Text(sp.name)
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor('#212121')
          .margin({ top: 6 })
        Text(sp.elem + ' · Lv.' + sp.level)
          .fontSize(10)
          .fontColor('#757575')
          .margin({ top: 3 })
        Text(sp.rare)
          .fontSize(9)
          .fontColor('#FFFFFF')
          .padding({ left: 8, right: 8, top: 2, bottom: 2 })
          .backgroundColor(getRareColor(sp.rare))
          .borderRadius(8)
          .margin({ top: 6 })
      }
      .width(110)
      .padding({ top: 14, bottom: 14 })
      .backgroundColor('#F9FBE7')
      .borderRadius(14)
      .margin({ right: 10 })
      .onClick(() => {
        this.onOpenSpirit(sp.id);
      })
    }, (sp: SpiritItem, si: number) => String(sp.id) + String(si))
  }
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.padding({ left: 16, right: 4 })

这段代码实现了首页的精灵横向滑动列表。外层Scroll组件通过.scrollable(ScrollDirection.Horizontal)设置为水平滚动方向,.scrollBar(BarState.Off)隐藏了滚动条,使界面更加简洁。内部的Row容器中,ForEach遍历八只精选精灵,每只精灵渲染为一个固定宽度(110vp)的Column卡片。

每张精灵卡片包含四个元素:Emoji图标(36号字体)、精灵名称、元素和等级信息、稀有度标签。稀有度标签通过getRareColor函数动态设置背景色——传说精灵为金色、史诗为紫色、稀有为蓝色。点击卡片时调用this.onOpenSpirit(sp.id),将精灵ID传递给父组件,触发精灵详情弹窗的展示。

Scroll组件是ArkTS中处理内容溢出的核心容器。它支持Horizontal(水平)、Vertical(垂直)和None三种滚动方向。BarState枚举控制滚动条的显示策略:On表示始终显示、Off表示始终隐藏、Auto表示根据交互状态自动显示/隐藏。在游戏类应用中,隐藏滚动条是一种常见的视觉优化——它使得滑动列表看起来更像原生的轮播组件,提升了沉浸感。

5.5 今日任务横条与秘境推荐

Row() {
  Text('📜')
    .fontSize(20)
  Column() {
    Text('今日任务 3/4')
      .fontSize(12)
      .fontWeight(FontWeight.Bold)
      .fontColor('#1B5E20')
    Text('完成可领灵晶 ×50')
      .fontSize(10)
      .fontColor('#757575')
      .margin({ top: 2 })
  }
  .alignItems(HorizontalAlign.Start)
  .layoutWeight(1)
  .margin({ left: 10 })
  Text('去完成 ›')
    .fontSize(11)
    .fontWeight(FontWeight.Bold)
    .fontColor('#FFC107')
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 12 })
.backgroundColor('#FFF8E1')
.borderRadius(12)
.margin({ left: 16, right: 16, top: 14 })
.onClick(() => {
  this.onToast('前往任务页');
})

这段代码构建了首页的"今日任务"横条。整个横条使用淡黄色背景(#FFF8E1)和12vp圆角,形成了一个柔和的提示条。横条内部从左到右依次是Emoji图标、任务标题和进度文字组成的Column、弹性占位、以及右侧的"去完成 ›"行动按钮。Column容器的.alignItems(HorizontalAlign.Start)将文字左对齐,使两行文字的起始位置对齐。

秘境推荐部分使用ForEach遍历getRealmRows()返回的偶数索引秘境项,为每个秘境渲染一行卡片。卡片内通过三元表达式rm.diff === '简单' ? '🌲' : (rm.diff === '普通' ? '⛰️' : '🔥')根据难度选择不同的Emoji图标。难度标签使用getDiffColor函数动态着色,形成了视觉上的难度区分。

点击精灵

点击秘境

点击任务

点击快捷入口

首页加载

渲染渐变横幅

渲染快捷入口宫格

渲染精灵横滑列表

渲染今日任务横条

渲染秘境推荐列表

用户交互

交互类型

调用 onOpenSpirit

调用 onOpenRealm

调用 onToast

调用 onToast

六、精灵页组件:SpiritTab的双列卡片布局

精灵页是应用的核心功能页面之一,负责展示和管理玩家拥有的所有精灵。它采用双列卡片瀑布流布局,每张卡片包含精灵的完整信息和操作按钮组。

6.1 组件声明与页面头部

struct SpiritTab {
  onOpenSpirit: (id: number) => void = () => {
  }
  onAddSpirit: () => void = () => {
  }
  onEditSpirit: (id: number) => void = () => {
  }
  onDelSpirit: (id: number) => void = () => {
  }
  onLevelUp: (id: number) => void = () => {
  }
  onEvolve: (id: number) => void = () => {
  }

  build() {
    Column() {
      Row() {
        Text('🧚 精灵培养室')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Text('')
          .layoutWeight(1)
        Text('+ 新增')
          .fontSize(11)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .backgroundColor('#1B5E20')
          .borderRadius(12)
          .onClick(() => {
            this.onAddSpirit();
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 8 })
      // ...
    }
  }
}

SpiritTab定义了六个回调接口,覆盖了精灵管理的全部交互行为:查看详情(onOpenSpirit)、新增(onAddSpirit)、编辑(onEditSpirit)、删除(onDelSpirit)、培养(onLevelUp)和进化(onEvolve)。这六个回调的声明体现了精灵养成系统的完整业务闭环——从新增到培养、从进化到编辑、最终到删除(在游戏语境中称为"放归")。

页面头部使用Row容器构建了一个标题栏,左侧是页面标题"🧚 精灵培养室",右侧是"+新增"按钮。新增按钮使用深绿色背景(#1B5E20)和白色文字,通过.onClick调用this.onAddSpirit()触发新增精灵弹窗。

6.2 双列卡片布局主体

Scroll() {
  Column() {
    Row() {
      Column() {
        ForEach(getSpiritRows(), (sp: SpiritItem) => {
          this.spiritCell(sp)
        }, (sp: SpiritItem) => String(sp.id))
      }
      .layoutWeight(1)
      Column() {
        ForEach(getSpiritRows2(), (sp: SpiritItem) => {
          this.spiritCell(sp)
        }, (sp: SpiritItem) => String(sp.id))
      }
      .layoutWeight(1)
    }
    .width('100%')
    .padding({ left: 12, right: 12, top: 4, bottom: 16 })
    .alignItems(VerticalAlign.Top)
  }
  .width('100%')
}
.scrollBar(BarState.Off)
.layoutWeight(1)

这段代码实现了精灵页的双列布局主体。最外层的Scroll组件提供了垂直滚动能力,.layoutWeight(1)使其占据头部下方的所有剩余空间。内部的Row容器包含两个等宽的Column(通过.layoutWeight(1)实现等宽分配),左列遍历getSpiritRows()返回的偶数索引精灵,右列遍历getSpiritRows2()返回的奇数索引精灵。

.alignItems(VerticalAlign.Top)是一个关键的布局属性——它使两个Column的顶部对齐,确保左右两列的卡片从同一水平线开始排列。如果没有这个属性,当两列卡片数量不等时,较短的一列可能会垂直居中,导致视觉上的错位。

这里使用this.spiritCell(sp)调用了@Builder装饰的方法来渲染每张精灵卡片。这是ArkTS组件化开发的核心模式之一——@Builder方法允许开发者将可复用的UI片段封装为一个函数,通过参数传入数据,实现UI的模块化组合。与直接在ForEach中内联UI代码相比,@Builder方法使代码结构更清晰,也便于在多个位置复用同一套UI模板。

6.3 精灵卡片Builder

@Builder
spiritCell(sp: SpiritItem) {
  Column() {
    Row() {
      Text(sp.img)
        .fontSize(30)
      Text('')
        .layoutWeight(1)
      Text(sp.rare)
        .fontSize(9)
        .fontColor('#FFFFFF')
        .padding({ left: 8, right: 8, top: 2, bottom: 2 })
        .backgroundColor(getRareColor(sp.rare))
        .borderRadius(8)
    }
    .width('100%')
    Text(sp.name)
      .fontSize(13)
      .fontWeight(FontWeight.Bold)
      .fontColor('#212121')
      .width('100%')
      .margin({ top: 6 })
    Row() {
      Text(sp.elem)
        .fontSize(9)
        .fontColor('#FFFFFF')
        .padding({ left: 8, right: 8, top: 2, bottom: 2 })
        .backgroundColor(getElemColor(sp.elem))
        .borderRadius(8)
      Text('Lv.' + sp.level)
        .fontSize(9)
        .fontColor('#757575')
        .margin({ left: 6 })
    }
    .width('100%')
    .margin({ top: 4 })
    Text('战力 ' + sp.power)
      .fontSize(12)
      .fontWeight(FontWeight.Bold)
      .fontColor('#1B5E20')
      .width('100%')
      .margin({ top: 6 })
  }
  .width('100%')
  .padding(12)
  .backgroundColor('#FFFFFF')
  .borderRadius(14)
  .margin({ bottom: 10 })
  .shadow({ radius: 6, color: 'rgba(0,0,0,0.06)', offsetY: 2 })
  .onClick(() => {
    this.onOpenSpirit(sp.id);
  })
}

spiritCell是一个@Builder装饰的方法,它接收一个SpiritItem参数并渲染完整的精灵卡片。卡片的结构分为三个主要区域:顶部行(图标+稀有度标签)、中部信息区(名称+元素标签+等级+战力)和底部操作按钮区。

卡片的顶部Row使用Text('').layoutWeight(1)在图标和稀有度标签之间插入弹性占位,将稀有度标签推到右侧。稀有度标签通过getRareColor函数动态着色,使不同稀有度的精灵在视觉上立刻可区分。元素标签同样通过getElemColor函数着色,与稀有度标签形成了双层的颜色编码体系。

卡片整体的视觉风格由几个关键样式属性定义:白色背景(#FFFFFF)、14vp圆角、6vp模糊半径的轻量阴影(rgba(0,0,0,0.06),2vp垂直偏移)。这些属性组合产生了一个浮起于浅绿色背景(#F4F9F4)之上的白色圆角卡片,具有Material Design卡片组件的视觉特征。

6.4 卡片操作按钮组

Row() {
  Text('培养')
    .fontSize(10)
    .fontColor('#FFFFFF')
    .padding({ left: 10, right: 10, top: 4, bottom: 4 })
    .backgroundColor('#FFC107')
    .borderRadius(10)
    .onClick(() => {
      this.onLevelUp(sp.id);
    })
  Text('进化')
    .fontSize(10)
    .fontColor('#FFFFFF')
    .padding({ left: 10, right: 10, top: 4, bottom: 4 })
    .backgroundColor('#7E57C2')
    .borderRadius(10)
    .onClick(() => {
      this.onEvolve(sp.id);
    })
}
.width('100%')
.margin({ top: 8 })
Row() {
  Text('编辑')
    .fontSize(10)
    .fontColor('#1B5E20')
    .padding({ left: 10, right: 10, top: 4, bottom: 4 })
    .backgroundColor('#E8F5E9')
    .borderRadius(10)
    .onClick(() => {
      this.onEditSpirit(sp.id);
    })
  Text('删除')
    .fontSize(10)
    .fontColor('#C62828')
    .padding({ left: 10, right: 10, top: 4, bottom: 4 })
    .backgroundColor('#FFEBEE')
    .borderRadius(10)
    .onClick(() => {
      this.onDelSpirit(sp.id);
    })
}
.width('100%')
.margin({ top: 6 })

卡片底部包含两行操作按钮。第一行是主要操作——“培养”(琥珀色背景)和"进化"(紫色背景),这两个按钮使用深色背景配白色文字,在视觉上突出,引导用户进行核心养成操作。第二行是次要操作——“编辑”(浅绿色背景配深绿色文字)和"删除"(浅红色背景配深红色文字),这两个按钮使用浅色背景配深色文字,在视觉上弱化,避免与主要操作混淆。

按钮的颜色编码遵循了交互设计中的"行动-危险"色彩语义:培养和进化是积极的行动操作,使用暖色调(琥珀色和紫色)吸引用户注意;编辑是中性操作,使用浅绿色保持低调;删除是破坏性操作,使用红色系传达警示含义。这种色彩语义体系在游戏UI和通用应用UI中都被广泛采用,能够降低用户的认知负荷。

七、秘境页组件:RealmTab的柱状图与秘境卡片

秘境页是应用的探索功能页面,它的特色是顶部嵌入了一个元素之力分布柱状图,下方是秘境卡片列表。这个页面的设计展示了ArkTS在数据可视化方面的能力。

7.1 元素之力柱状图

Column() {
  Row() {
    Text('元素之力分布')
      .fontSize(13)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')
    Text('')
      .layoutWeight(1)
    Text('本月')
      .fontSize(10)
      .fontColor('#C8E6C9')
  }
  .width('100%')
  Row() {
    ForEach(getElemPower(), (ep: ElemPower) => {
      Column() {
        Column() {
          Text('')
        }
        .width(20)
        .height(getBarHeight(ep.value))
        .backgroundColor(getElemColor(ep.name))
        .borderRadius({ topLeft: 6, topRight: 6 })
        .justifyContent(FlexAlign.End)
        Text(ep.value)
          .fontSize(9)
          .fontColor('#C8E6C9')
          .margin({ top: 4 })
        Text(ep.name)
          .fontSize(10)
          .fontColor('#FFFFFF')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Center)
    }, (ep: ElemPower) => ep.name)
  }
  .width('100%')
  .alignItems(VerticalAlign.Bottom)
  .margin({ top: 12 })
}
.width('100%')
.padding({ left: 14, right: 14, top: 14, bottom: 14 })
.linearGradient({ angle: 135, colors: [['#0E3B2E', 0], ['#1B5E20', 1]] })
.borderRadius(14)
.margin({ left: 16, right: 16, top: 4 })

这段代码实现了一个纯ArkTS的柱状图组件,无需任何第三方图表库。柱状图的数据源是getElemPower()返回的六个元素战力值。外层Column容器使用深绿渐变背景,与首页横幅保持视觉一致性。

每个柱子是一个嵌套的Column结构:最内层的空Column(只有一个空Text)是柱体本身,其.height(getBarHeight(ep.value))通过工具函数将战力值转换为像素高度(如980战力对应98vp高度),.backgroundColor(getElemColor(ep.name))根据元素名称设置柱体颜色。柱体上方是数值标签,下方是元素名称标签。

外层Row容器的.alignItems(VerticalAlign.Bottom)是一个关键属性——它确保所有柱子底部对齐,使不同高度的柱子从同一基线向上延伸,形成标准的柱状图视觉效果。每个柱子的外层Column使用.layoutWeight(1)等宽分配空间,.alignItems(HorizontalAlign.Center)使柱体在其分配的空间内水平居中。

这个柱状图的实现是ArkTS声明式UI能力的典型展示。在传统的前端开发中,柱状图通常依赖Chart.js或ECharts等第三方库。但在ArkTS中,通过组合ColumnRowForEach和样式属性,开发者可以用纯声明式代码构建数据可视化组件。这种方式的优点是零依赖、高性能、完全可控,缺点是需要手动处理坐标计算和动画效果。对于简单的柱状图、进度条等可视化需求,纯声明式实现是一个轻量而高效的选择。

7.2 秘境卡片Builder

@Builder
realmCard(rm: RealmItem) {
  Column() {
    Row() {
      Text(rm.diff === '简单' ? '🌲' : (rm.diff === '普通' ? '⛰️' : (rm.diff === '困难' ? '🌀' : '🔥')))
        .fontSize(26)
      Column() {
        Row() {
          Text(rm.name)
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text(rm.diff)
            .fontSize(9)
            .fontColor('#FFFFFF')
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .backgroundColor(getDiffColor(rm.diff))
            .borderRadius(8)
            .margin({ left: 6 })
        }
        Text(rm.desc)
          .fontSize(10)
          .fontColor('#757575')
          .margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 10 })
    }
    .width('100%')
    Row() {
      Text('推荐战力 ' + rm.power)
        .fontSize(10)
        .fontColor('#1B5E20')
      Text('')
        .layoutWeight(1)
      Text('奖励:' + rm.reward)
        .fontSize(10)
        .fontColor('#FF8F00')
    }
    .width('100%')
    .margin({ top: 8 })
    Row() {
      Text('查看详情')
        .fontSize(11)
        .fontColor('#1B5E20')
        .padding({ left: 12, right: 12, top: 5, bottom: 5 })
        .backgroundColor('#E8F5E9')
        .borderRadius(10)
        .onClick(() => {
          this.onOpenRealm(rm.id);
        })
      Text('')
        .layoutWeight(1)
      Text('立即探索')
        .fontSize(11)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .padding({ left: 14, right: 14, top: 5, bottom: 5 })
        .backgroundColor('#1B5E20')
        .borderRadius(10)
        .onClick(() => {
          this.onExplore(rm.id);
        })
    }
    .width('100%')
    .margin({ top: 10 })
  }
  .width('100%')
  .padding(14)
  .backgroundColor('#FFFFFF')
  .borderRadius(14)
  .margin({ left: 16, right: 16, top: 8 })
  .shadow({ radius: 6, color: 'rgba(0,0,0,0.06)', offsetY: 2 })
}

realmCard是秘境卡片的@Builder方法。卡片内部嵌套了多层RowColumn,形成了一个信息密集的卡片布局。最顶部是图标行,使用嵌套的三元表达式根据难度选择Emoji(简单为🌲、普通为⛰️、困难为🌀、噩梦为🔥)。图标右侧是秘境名称和难度标签的Row,以及描述文字的Column

卡片中部是推荐战力和奖励信息的Row,使用弹性占位将战力和奖励分置两端。底部是操作按钮行——左侧"查看详情"使用浅色风格,右侧"立即探索"使用深色主色调风格,与精灵页卡片的按钮设计模式保持一致。

八、任务页组件:QuestTab的进度条设计

任务页是应用的日常运营功能页面,展示了任务列表和活跃度进度。它的核心技术点是自定义进度条的实现。

8.1 活跃度横幅与进度条

Column() {
  Row() {
    Text('🔥 本周活跃度')
      .fontSize(14)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')
    Text('')
      .layoutWeight(1)
    Text('415 / 600')
      .fontSize(11)
      .fontColor('#FFC107')
  }
  .width('100%')
  Row() {
    Text('')
  }
  .width('69%')
  .height(8)
  .backgroundColor('#FFC107')
  .borderRadius(4)
  .margin({ top: 10 })
}
.width('100%')
.padding(14)
.linearGradient({ angle: 135, colors: [['#1B5E20', 0], ['#2E7D32', 1]] })
.borderRadius(14)
.margin({ left: 16, right: 16, top: 4 })

活跃度横幅使用深绿渐变背景,内部包含标题行和进度条。进度条是一个只含空TextRow容器,其.width('69%')直接以百分比字符串设置宽度——415/600约等于69%,因此进度条填充了69%的宽度。.height(8)设置了8vp的细条高度,.backgroundColor('#FFC107')使用琥珀色填充,.borderRadius(4)设置了半圆形的端角。

这种进度条实现方式的精妙之处在于其简洁性——不使用任何动画或复杂的布局计算,仅通过百分比宽度字符串就能精确表达进度比例。但这种方式的局限是进度值是硬编码的(‘69%’),如果需要动态更新进度,需要将百分比计算为字符串并绑定到状态变量。

8.2 任务卡片与动态进度条

@Builder
questCell(qs: QuestItem) {
  Column() {
    Row() {
      Column() {
        Row() {
          Text(qs.name)
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text(qs.type)
            .fontSize(9)
            .fontColor('#FFFFFF')
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .backgroundColor(getTaskColor(qs.type))
            .borderRadius(8)
            .margin({ left: 6 })
        }
        Text(qs.progress + ' / ' + qs.total + ' · 奖励 ' + qs.reward)
          .fontSize(10)
          .fontColor('#757575')
          .margin({ top: 4 })
        Row() {
          Row() {
            Text('')
          }
          .width(String(qs.progress * 100 / qs.total) + '%')
          .height(6)
          .backgroundColor(getTaskColor(qs.type))
          .borderRadius(3)
        }
        .width('70%')
        .height(6)
        .backgroundColor('#EEEEEE')
        .borderRadius(3)
        .margin({ top: 6 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      Text('领取')
        .fontSize(11)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .padding({ left: 14, right: 14, top: 6, bottom: 6 })
        .backgroundColor('#FF8F00')
        .borderRadius(10)
        .onClick(() => {
          this.onClaim(qs.id);
        })
    }
    .width('100%')
    Row() {
      Text('详情 ›')
        .fontSize(10)
        .fontColor('#1B5E20')
        .onClick(() => {
          this.onOpenQuest(qs.id);
        })
      Text('')
        .layoutWeight(1)
      Text('+' + qs.point + ' 活跃度')
        .fontSize(10)
        .fontColor('#9E9E9E')
    }
    .width('100%')
    .margin({ top: 8 })
  }
  .width('100%')
  .padding(12)
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
  .margin({ left: 16, right: 16, top: 8 })
  .shadow({ radius: 4, color: 'rgba(0,0,0,0.05)', offsetY: 2 })
}

任务卡片的核心技术是动态进度条的实现。进度条由两层Row嵌套构成:外层Row是灰色轨道(#EEEEEE),固定宽度70%和高度6vp;内层Row是彩色填充,其宽度通过String(qs.progress * 100 / qs.total) + '%'动态计算——将进度比例(如3/3=100%、5/8=62.5%)转换为百分比字符串。

进度条的填充颜色通过getTaskColor(qs.type)根据任务类型动态设置——日常任务为绿色、周常为橙色、成就为玫红色。这种设计使得进度条不仅是进度可视化,还承载了类型信息,形成了双重的视觉编码。

动态进度条的实现展示了ArkTS中"数据驱动UI"的核心理念。进度条的宽度不是通过命令式代码设置的(如element.style.width = '62.5%'),而是通过表达式String(qs.progress * 100 / qs.total) + '%'在声明式代码中直接计算。当qs.progressqs.total发生变化时,框架会自动重新评估表达式并更新UI。这就是声明式UI的本质——开发者描述"UI应该是什么样子",框架负责"如何让UI变成那个样子"。

九、商店页组件:ShopTab的双列商品卡片

商店页使用金色横幅和双列商品卡片的布局,视觉风格从森林绿转向了金色商业氛围。

9.1 金色充值横幅

Column() {
  Row() {
    Text('💰')
      .fontSize(28)
    Column() {
      Text('灵晶充值特惠')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#3E2723')
      Text('首充双倍 · 限时开启')
        .fontSize(10)
        .fontColor('#6D4C41')
        .margin({ top: 3 })
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)
    .margin({ left: 10 })
    Text('去充值')
      .fontSize(11)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')
      .padding({ left: 14, right: 14, top: 6, bottom: 6 })
      .backgroundColor('#3E2723')
      .borderRadius(10)
      .onClick(() => {
        this.onRecharge();
      })
  }
  .width('100%')
}
.width('100%')
.padding(14)
.linearGradient({ angle: 90, colors: [['#FFD54F', 0], ['#FFC107', 0.6], ['#FFB300', 1]] })
.borderRadius(14)
.margin({ left: 16, right: 16, top: 4 })
.shadow({ radius: 8, color: 'rgba(255,193,7,0.4)', offsetY: 3 })

金色横幅使用90度(从左到右)的三色渐变——浅金(#FFD54F)到琥珀(#FFC107)到深金(#FFB300),营造了充值促销的金色氛围。横幅的阴影使用了rgba(255,193,7,0.4)的金色阴影,与背景色协调,形成了一种"发光"的视觉效果,而非传统的灰色投影。

横幅内的文字颜色全部使用了棕色系——深棕(#3E2723)用于标题、中棕(#6D4C41)用于副标题、深棕(#3E2723)用于按钮背景。这种"金底棕字"的配色方案是金融和充值类UI的常见设计,能够在金色背景上保持文字的可读性。

9.2 商品卡片Builder

@Builder
shopCell(sg: ShopItem) {
  Column() {
    Row() {
      Text(sg.img)
        .fontSize(30)
      Text('')
        .layoutWeight(1)
      Text(sg.tag)
        .fontSize(9)
        .fontColor('#FFFFFF')
        .padding({ left: 8, right: 8, top: 2, bottom: 2 })
        .backgroundColor('#FF8F00')
        .borderRadius(8)
    }
    .width('100%')
    Text(sg.name)
      .fontSize(12)
      .fontWeight(FontWeight.Bold)
      .fontColor('#212121')
      .width('100%')
      .margin({ top: 6 })
    Row() {
      Text('¥' + sg.price)
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#C62828')
      Text('¥' + sg.oldPrice)
        .fontSize(10)
        .fontColor('#BDBDBD')
        .decoration({ type: TextDecorationType.LineThrough })
        .margin({ left: 6 })
    }
    .width('100%')
    .margin({ top: 4 })
    Text('立即购买')
      .fontSize(10)
      .fontColor('#FFFFFF')
      .padding({ left: 12, right: 12, top: 5, bottom: 5 })
      .backgroundColor('#1B5E20')
      .borderRadius(10)
      .margin({ top: 6 })
      .onClick(() => {
        this.onOpenShop(sg.id);
      })
  }
  .width('100%')
  .padding(12)
  .backgroundColor('#FFFFFF')
  .borderRadius(14)
  .margin({ bottom: 10 })
  .shadow({ radius: 6, color: 'rgba(0,0,0,0.06)', offsetY: 2 })
}

商品卡片的技术亮点是.decoration({ type: TextDecorationType.LineThrough })的使用——这个属性为原价文字添加了删除线效果,使现价和原价形成了"折扣对比"的视觉关系。现价使用14号字体和深红色(#C62828)突出显示,原价使用10号灰色字体加删除线弱化显示,这种"大号红字+小号灰字删除线"的价格展示模式是电商类应用的标准设计。

商品标签(热卖、超值、新人、限时等)统一使用琥珀色(#FF8F00)背景,但通过不同的文字内容传达不同的促销语义。"立即购买"按钮使用深绿色背景,与商店页整体的金色调形成对比,引导用户进行购买操作。

十、图鉴页组件:CodexTab的三列网格与筛选

图鉴页使用三列网格布局展示所有精灵的图鉴信息,顶部嵌入了横向滚动的稀有度筛选条。

10.1 稀有度筛选条

Scroll() {
  Row() {
    ForEach(getCodexFilter(), (cf: string, ci: number) => {
      Text(cf)
        .fontSize(11)
        .padding({ left: 12, right: 12, top: 5, bottom: 5 })
        .backgroundColor(ci === 0 ? '#1B5E20' : '#FFFFFF')
        .fontColor(ci === 0 ? '#FFFFFF' : '#616161')
        .borderRadius(12)
        .margin({ right: 8 })
    }, (cf: string) => cf)
  }
  .width('100%')
  .padding({ left: 16, top: 6 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')

筛选条使用横向Scroll容器包裹Row,使五个筛选选项(全部、传说、史诗、稀有、普通)可以横向滚动。每个筛选项是一个Text组件,通过三元表达式ci === 0 ? '#1B5E20' : '#FFFFFF'判断是否为第一个(“全部”)——第一个使用深绿色背景和白色文字表示选中态,其余使用白色背景和灰色文字表示未选中态。

这种筛选条的交互模式是"静态选中"——选中态在初始化时固定为第一项,不随用户点击变化。在完整的实现中,应该将选中索引绑定到@State变量,在onClick中更新状态,使筛选条能够动态响应用户的选择。这种简化处理在原型阶段是常见的,通过静态数据展示UI效果,后续再补全交互逻辑。

10.2 三列网格布局

Row() {
  Column() {
    ForEach(getCodexRows(), (cd: CodexItem) => {
      this.codexCell(cd)
    }, (cd: CodexItem) => String(cd.id))
  }
  .layoutWeight(1)
  Column() {
    ForEach(getCodexRows2(), (cd: CodexItem) => {
      this.codexCell(cd)
    }, (cd: CodexItem) => String(cd.id))
  }
  .layoutWeight(1)
  Column() {
    ForEach(getCodexRows3(), (cd: CodexItem) => {
      this.codexCell(cd)
    }, (cd: CodexItem) => String(cd.id))
  }
  .layoutWeight(1)
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 16 })
.alignItems(VerticalAlign.Top)

三列网格布局的实现方式与精灵页的双列布局类似——三个等宽Column通过.layoutWeight(1)分配空间,每个Column内使用ForEach遍历对应的数据拆分函数返回的子数组。getCodexRowsgetCodexRows2getCodexRows3分别从索引0、1、2开始以步长3提取图鉴数据,将12个图鉴条目均匀分配到三列中。

图鉴卡片codexCell的结构比精灵卡片更简洁——只有Emoji图标、名称、元素和稀有度信息、以及一个稀有度标签。这种简洁设计适应了三列布局的狭窄空间,确保每张卡片在有限宽度内仍然清晰可读。

十一、我的页组件:MineTab的会员卡与成就

我的页是应用的个人中心,集成了会员卡、装备槽位、收藏列表和成就列表四个功能区块。

11.1 幻灵师会员卡

Column() {
  Row() {
    Text('🧙')
      .fontSize(30)
    Column() {
      Text('幻灵师 · 星耀段位')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
      Text('ID: 8842 · 服务幻灵 3 年')
        .fontSize(10)
        .fontColor('#C8E6C9')
        .margin({ top: 3 })
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)
    .margin({ left: 10 })
  }
  .width('100%')
  Row() {
    Column() {
      Text('12860')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFC107')
      Text('灵晶')
        .fontSize(9)
        .fontColor('#C8E6C9')
    }
    .layoutWeight(1)
    Column() {
      Text('42')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFC107')
      Text('等级')
        .fontSize(9)
        .fontColor('#C8E6C9')
    }
    .layoutWeight(1)
    Column() {
      Text('12')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFC107')
      Text('图鉴')
        .fontSize(9)
        .fontColor('#C8E6C9')
    }
    .layoutWeight(1)
    Column() {
      Text('8')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
      Text('成就')
        .fontSize(9)
        .fontColor('#C8E6C9')
    }
    .layoutWeight(1)
  }
  .width('100%')
  .padding({ top: 14 })
}
.width('100%')
.padding(14)
.linearGradient({ angle: 135, colors: [['#0E3B2E', 0], ['#1B5E20', 0.7], ['#2E7D32', 1]] })
.borderRadius(14)
.margin({ left: 16, right: 16, top: 4 })
.shadow({ radius: 8, color: 'rgba(14,59,46,0.3)', offsetY: 3 })

会员卡的设计分为上下两层。上层是用户身份信息——Emoji头像、段位名称和ID信息。下层是四列统计数据——灵晶、等级、图鉴、成就,每列包含一个琥珀色的数值标签和浅绿色的名称标签,通过.layoutWeight(1)等宽分布。

会员卡的视觉风格使用了与首页横幅相同的深绿三色渐变背景,形成了一致的"幻灵师"视觉身份。数值标签使用琥珀色(#FFC107),在深绿背景上形成了金色的亮点,使关键数据一眼可辨。

11.2 装备槽位与收藏列表

Row() {
  ForEach(getSlotList(), (sl: string, si: number) => {
    Column() {
      Text(si === 0 ? '🧚' : (si === 1 ? '🐆' : '🐈⬛'))
        .fontSize(22)
      Text(sl)
        .fontSize(9)
        .fontColor('#757575')
        .margin({ top: 3 })
    }
    .layoutWeight(1)
    .padding({ top: 10, bottom: 10 })
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .margin({ right: 8 })
  }, (sl: string, si: number) => sl + String(si))
}
.width('100%')
.padding({ left: 16, right: 8 })

装备槽位行使用ForEach遍历getSlotList()返回的三个槽位名称。每个槽位是一个Column容器,内含Emoji图标和槽位名称。Emoji图标通过索引三元表达式si === 0 ? '🧚' : (si === 1 ? '🐆' : '🐈⬛')动态选择——第一个槽位显示光系精灵,第二个显示雷系精灵,第三个显示暗系精灵。三个槽位通过.layoutWeight(1)等宽分配,每个槽位之间有8vp的右间距。

收藏列表使用双列布局展示收藏的精灵,每张收藏卡片favCell包含Emoji图标、名称、元素和等级信息,以及一个红色的"✕"删除按钮。点击卡片主体调用onOpenFav打开精灵详情,点击"✕"按钮调用onDelFav弹出取消收藏确认框。

11.3 成就列表与状态视觉

@Builder
achCell(ac: AchItem) {
  Row() {
    Text(ac.img)
      .fontSize(22)
      .opacity(ac.done ? 1 : 0.35)
    Column() {
      Row() {
        Text(ac.name)
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor(ac.done ? '#212121' : '#9E9E9E')
        Text('+' + ac.point + ' 点')
          .fontSize(9)
          .fontColor('#FF8F00')
          .margin({ left: 6 })
      }
      Text(ac.desc)
        .fontSize(9)
        .fontColor('#9E9E9E')
        .margin({ top: 3 })
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)
    .margin({ left: 10 })
    Text(ac.done ? '已达成' : '未达成')
      .fontSize(9)
      .fontColor(ac.done ? '#1B5E20' : '#BDBDBD')
      .onClick(() => {
        this.onOpenAch(ac.id);
      })
  }
  .width('100%')
  .padding(12)
  .backgroundColor('#FFFFFF')
  .borderRadius(12)
  .margin({ left: 16, right: 16, top: 8 })
  .shadow({ radius: 4, color: 'rgba(0,0,0,0.04)', offsetY: 2 })
}

成就卡片的技术亮点是通过.opacity(ac.done ? 1 : 0.35)实现了成就图标的状态视觉——已达成的成就图标完全不透明,未达成的成就图标半透明(35%不透明度),形成了一种"灰色未解锁"的视觉效果。同时,成就名称的文字颜色也通过三元表达式动态设置——已达成为深色(#212121)、未达成为灰色(#9E9E9E)。

状态标签"已达成"/"未达成"同样使用颜色编码——已达成为绿色(#1B5E20)、未达成为浅灰色(#BDBDBD)。这种多维度(透明度+文字颜色+标签颜色)的状态视觉编码,使玩家能够从任意角度快速识别成就的完成状态。

十二、主组件Index:状态中枢与路由控制

主组件Index是整个应用的状态中枢和路由控制器。它使用@Entry@Component装饰器声明为应用入口组件,管理着当前激活的Tab页、所有弹窗的显隐状态、选中的数据对象以及表单填写状态。

12.1 组件声明与状态定义

@Entry
@Component
struct Index {
  @State curTab: number = 0
  private tabs1: string[] = ['首页', '精灵', '秘境', '任务']
  private tabs2: string[] = ['商店', '图鉴', '我的']

  @State showSpiritDetail: boolean = false
  @State selSpirit: SpiritItem | null = null
  @State showLevelUp: boolean = false
  @State showEvolve: boolean = false
  @State showAddSpirit: boolean = false
  @State showEditSpirit: boolean = false
  @State showDelSpirit: boolean = false
  @State showRealmDetail: boolean = false
  @State selRealm: RealmItem | null = null
  @State showExplore: boolean = false
  @State showQuestDetail: boolean = false
  @State selQuest: QuestItem | null = null
  @State showClaim: boolean = false
  @State showShopDetail: boolean = false
  @State selShop: ShopItem | null = null
  @State showRecharge: boolean = false
  @State showCodexDetail: boolean = false
  @State selCodex: CodexItem | null = null
  @State showDelFav: boolean = false
  @State selFav: FavItem | null = null
  @State showAchDetail: boolean = false
  @State selAch: AchItem | null = null
  @State showSwap: boolean = false

  @State fName: string = '花灵兔'
  @State fElem: string = '木'
  @State fRare: string = '普通'
  @State fLevel: number = 22
  @State fStatus: string = '休息'
  @State fPower: number = 420
  @State fSlot: string = '装备位·1'
  @State fCount: number = 1
  @State fPayLevel: number = 30
  @State toast: string = ''

@Entry装饰器将Index标记为应用的入口组件——每个ArkTS应用有且仅有一个@Entry组件。@Component装饰器将其标记为一个可复用的自定义组件,使其可以在其他组件中被引用。

@State装饰器是ArkTS状态管理的核心。被@State修饰的变量是可观察的——当变量值发生变化时,框架会自动重新渲染引用了该变量的UI部分。curTab是页面路由状态,控制当前显示哪个Tab页。十六个show*布尔变量控制十六个弹窗的显隐。七个sel*变量存储当前选中的数据对象,供弹窗展示使用。十个f*变量是表单填写状态,用于新增精灵、编辑精灵、购买商品和充值等表单交互。

@State变量的类型使用了联合类型SpiritItem | null——这意味着变量可以是一个SpiritItem对象或null。这种设计允许在未选中任何数据时将变量设为null,在弹窗渲染时通过this.selSpirit!.img的非空断言操作符(!)安全访问属性。这是ArkTS处理可选数据的常见模式,类似于Swift的可选类型和Kotlin的可空类型。

12.2 页面路由与条件渲染

if (this.curTab === 0) {
  HomeTab({
    onOpenSpirit: (id: number) => {
      this.selSpirit = this.findSpirit(id);
      this.showSpiritDetail = true;
    },
    onOpenRealm: (id: number) => {
      this.selRealm = this.findRealm(id);
      this.showRealmDetail = true;
    },
    onOpenQuest: (id: number) => {
      this.selQuest = this.findQuest(id);
      this.showQuestDetail = true;
    },
    onToast: (msg: string) => {
      this.toast = msg;
    }
  })
} else if (this.curTab === 1) {
  SpiritTab({
    onOpenSpirit: (id: number) => {
      this.selSpirit = this.findSpirit(id);
      this.showSpiritDetail = true;
    },
    onAddSpirit: () => {
      this.fName = '花灵兔';
      this.fElem = '木';
      this.fRare = '普通';
      this.fLevel = 22;
      this.fPower = 420;
      this.fStatus = '休息';
      this.showAddSpirit = true;
    },
    onEditSpirit: (id: number) => {
      this.selSpirit = this.findSpirit(id);
      this.showEditSpirit = true;
    },
    onDelSpirit: (id: number) => {
      this.selSpirit = this.findSpirit(id);
      this.showDelSpirit = true;
    },
    onLevelUp: (id: number) => {
      this.selSpirit = this.findSpirit(id);
      this.showLevelUp = true;
    },
    onEvolve: (id: number) => {
      this.selSpirit = this.findSpirit(id);
      this.showEvolve = true;
    }
  })
} else if (this.curTab === 2) {
  RealmTab({
    onOpenRealm: (id: number) => {
      this.selRealm = this.findRealm(id);
      this.showRealmDetail = true;
    },
    onExplore: (id: number) => {
      this.selRealm = this.findRealm(id);
      this.showExplore = true;
    }
  })
}

页面路由通过if-else if条件渲染实现——根据curTab的值决定渲染哪个子组件。每个子组件在被实例化时接收一组回调函数,这些回调函数在内部执行三步操作:第一步通过findSpirit/findRealm等查找方法从静态数组中检索数据对象;第二步将检索到的对象赋值给对应的sel*状态变量;第三步将对应的show*状态变量设为true触发弹窗显示。

这种"查找-赋值-触发"的三步模式在整个应用中被一致地执行。它的设计优势在于将数据检索和状态变更集中在主组件中处理,子组件只需关心"发生了什么事件",而不用关心"如何处理事件",实现了关注点分离。

用户点击精灵卡片

SpiritTab 调用 onOpenSpirit 回调

Index 主组件执行回调

findSpirit 从 SPIRIT_LIST 查找

selSpirit 赋值为查找到的对象

showSpiritDetail 设为 true

条件渲染触发 spiritDetailModal 弹窗

弹窗通过 selSpirit 渲染详情

12.3 底部双排Tab栏

Column() {
  Row() {
    ForEach(this.tabs1, (tb: string, ti: number) => {
      this.bottomTabItem(getTabIcon(tb), tb, ti)
    }, (tb: string, ti: number) => tb + String(ti))
  }
  .width('100%')
  .padding({ top: 6, bottom: 2 })

  Row() {
    ForEach(this.tabs2, (tb: string, ti: number) => {
      this.bottomTabItem(getTabIcon(tb), tb, ti + 4)
    }, (tb: string, ti: number) => tb + String(ti))
  }
  .width('100%')
  .padding({ top: 2, bottom: 6 })
}
.width('100%')
.backgroundColor('#FFFFFF')
.shadow({ radius: 8, color: 'rgba(0,0,0,0.08)', offsetY: -2 })

底部Tab栏采用双排布局——第一排四个Tab(首页、精灵、秘境、任务),第二排三个Tab(商店、图鉴、我的)。两排Tab使用相同的bottomTabItem Builder方法渲染,但传入的索引值不同——第一排直接使用ti(0-3),第二排使用ti + 4(4-6),使得七个Tab的索引值在0-6范围内连续分布,与curTab的取值范围对应。

Tab栏的阴影使用了offsetY: -2——负值的垂直偏移使阴影投射向上方,这与Tab栏位于底部的位置相匹配,形成了"Tab栏浮起于内容区之上"的视觉效果。

12.4 Tab项Builder与选中态

@Builder
bottomTabItem(icon: string, label: string, idx: number) {
  Column() {
    Text(icon)
      .fontSize(17)
      .fontColor(this.curTab === idx ? '#1B5E20' : '#9E9E9E')
    Text(label)
      .fontSize(10)
      .fontColor(this.curTab === idx ? '#1B5E20' : '#9E9E9E')
      .margin({ top: 1 })
  }
  .layoutWeight(1)
  .onClick(() => {
    this.curTab = idx;
  })
}

bottomTabItem是Tab项的@Builder方法。它接收图标、标签和索引三个参数,渲染一个包含图标和文字的Column容器。选中态通过三元表达式this.curTab === idx ? '#1B5E20' : '#9E9E9E'实现——当前选中的Tab使用深绿色(#1B5E20),未选中的Tab使用灰色(#9E9E9E)。

点击Tab项时,this.curTab = idx将当前Tab索引设为被点击项的索引。由于curTab@State变量,赋值会触发条件渲染的重评估——旧的Tab页面被卸载,新的Tab页面被挂载,实现了页面切换。

值得注意的是,Tab项的颜色直接绑定到this.curTab状态变量。这意味着当curTab变化时,不仅条件渲染会切换页面内容,所有Tab项的颜色也会自动更新——选中项变为深绿色,原选中项变为灰色。这种"一处状态变更,多处UI响应"的行为是声明式UI的核心优势,开发者无需手动管理UI同步,框架会自动处理状态到UI的映射。

十三、弹窗系统:遮罩层与十六种弹窗

本应用的弹窗系统由一个遮罩层Builder和十六个弹窗Builder组成。每个弹窗在显示时都配对一个遮罩层,遮罩层捕获点击事件以关闭弹窗。

13.1 遮罩层与弹窗条件渲染

if (this.showSpiritDetail && this.selSpirit !== null) {
  this.modalOverlay(() => {
    this.showSpiritDetail = false;
  })
  this.spiritDetailModal()
}
if (this.showLevelUp && this.selSpirit !== null) {
  this.modalOverlay(() => {
    this.showLevelUp = false;
  })
  this.levelUpModal()
}

每个弹窗的渲染由两个条件共同控制:show*布尔状态和sel*非空判断。只有当弹窗需要显示且选中数据不为null时,才会渲染遮罩层和弹窗内容。遮罩层接收一个关闭回调() => { this.show* = false },点击遮罩层任意区域即调用该回调,将弹窗状态设为false从而关闭弹窗。

@Builder
modalOverlay(onClose: () => void) {
  Column() {
  }
  .width('100%')
  .height('100%')
  .backgroundColor('rgba(0,0,0,0.55)')
  .onClick(onClose)
}

modalOverlay是遮罩层的@Builder方法。它是一个空的Column容器,通过rgba(0,0,0,0.55)设置了55%不透明度的黑色背景,覆盖整个屏幕。.onClick(onClose)将点击事件绑定到传入的关闭回调,实现了"点击遮罩层关闭弹窗"的交互模式。

遮罩层的rgba(0,0,0,0.55)是一个经过精心调试的透明度值。过高的不透明度(如0.8)会使背景完全不可见,破坏上下文感;过低的不透明度(如0.3)则无法突出弹窗的层级关系。55%的不透明度既保留了背景内容的可感知性,又使弹窗成为视觉焦点,是移动端弹窗设计的推荐值。

13.2 精灵详情弹窗(居中卡)

@Builder
spiritDetailModal() {
  Column() {
    Text(this.selSpirit!.img)
      .fontSize(44)
    Text(this.selSpirit!.name)
      .fontSize(19)
      .fontWeight(FontWeight.Bold)
      .fontColor('#212121')
      .margin({ top: 8 })
    Text(this.selSpirit!.elem + '系 · ' + this.selSpirit!.rare)
      .fontSize(11)
      .fontColor('#757575')
      .margin({ top: 4 })
    Row() {
      Column() {
        Text('Lv.' + this.selSpirit!.level)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1B5E20')
        Text('等级')
          .fontSize(9)
          .fontColor('#9E9E9E')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      Column() {
        Text(String(this.selSpirit!.power))
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FF8F00')
        Text('战力')
          .fontSize(9)
          .fontColor('#9E9E9E')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
    }
    .width('100%')
    .padding({ top: 12, bottom: 12 })
    .backgroundColor('#F1F8E9')
    .borderRadius(12)
    .margin({ top: 12 })
    Text(this.selSpirit!.intro)
      .fontSize(11)
      .fontColor('#616161')
      .width('100%')
      .margin({ top: 10 })
    Text(this.selSpirit!.tags)
      .fontSize(10)
      .fontColor('#1B5E20')
      .width('100%')
      .margin({ top: 8 })
    Text('关闭')
      .fontSize(13)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')
      .width('100%')
      .textAlign(TextAlign.Center)
      .padding({ top: 10, bottom: 10 })
      .backgroundColor('#1B5E20')
      .borderRadius(24)
      .margin({ top: 14 })
      .onClick(() => {
        this.showSpiritDetail = false;
      })
  }
  .width('82%')
  .padding(18)
  .backgroundColor('#FFFFFF')
  .borderRadius(18)
  .clip(true)
  .transition(TransitionEffect.OPACITY.animation({ duration: 200 }))
}

精灵详情弹窗是一个居中展示的白色圆角卡片,宽度为屏幕的82%。弹窗通过this.selSpirit!.imgthis.selSpirit!.name等非空断言访问选中精灵的数据。弹窗内部从上到下依次是:大号Emoji图标(44号字体)、精灵名称、元素和稀有度信息、双列统计数据(等级和战力)、背景介绍文字、技能标签、关闭按钮。

.clip(true)是一个关键的视觉属性——它将卡片内容裁剪到圆角边界内,确保内部元素不会溢出圆角区域。.transition(TransitionEffect.OPACITY.animation({ duration: 200 }))添加了200毫秒的透明度过渡动画,使弹窗出现和消失时有渐入渐出的效果,而非生硬的瞬间显示/隐藏。

TransitionEffect是ArkTS中定义组件过渡效果的API。OPACITY表示透明度过渡,配合.animation({ duration: 200 })指定动画时长为200毫秒。除了透明度过渡,ArkTS还支持TransitionEffect.translate(平移过渡)、TransitionEffect.scale(缩放过渡)等效果。在弹窗场景中,透明度过渡是最常用的——它轻量、自然,不会因为位移或缩放造成视觉干扰。

13.3 培养升级弹窗(底部抽屉)

@Builder
levelUpModal() {
  Column() {
    Row() {
      Text('培养 ' + this.selSpirit!.name)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#212121')
      Text('')
        .layoutWeight(1)
      Text('✕')
        .fontSize(15)
        .fontColor('#9E9E9E')
        .padding(4)
        .onClick(() => {
          this.showLevelUp = false;
        })
    }
    .width('100%')
    Text('当前等级 Lv.' + this.selSpirit!.level + ' → 目标 Lv.' + (this.selSpirit!.level + 1))
      .fontSize(11)
      .fontColor('#757575')
      .width('100%')
      .margin({ top: 4 })

    Row() {
      ForEach(getLevelUpOptions(), (lv: string, li: number) => {
        Text(lv)
          .fontSize(11)
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .backgroundColor(li === 0 ? '#1B5E20' : '#F5F5F5')
          .fontColor(li === 0 ? '#FFFFFF' : '#616161')
          .borderRadius(12)
          .margin({ right: 8 })
      }, (lv: string, li: number) => lv + String(li))
    }
    .width('100%')
    .margin({ top: 12 })

    ForEach(getGrowthRows(), (gr: GrowthItem, gi: number) => {
      Row() {
        Text(gr.name)
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor('#212121')
        Text('')
          .layoutWeight(1)
        Text(gr.add)
          .fontSize(11)
          .fontColor('#1B5E20')
        Text(gr.cost)
          .fontSize(10)
          .fontColor('#9E9E9E')
          .margin({ left: 10 })
      }
      .width('100%')
      .padding({ left: 12, right: 12, top: 10, bottom: 10 })
      .backgroundColor('#F9FBE7')
      .borderRadius(10)
      .margin({ top: 8 })
      .onClick(() => {
        this.showLevelUp = false;
        this.toast = '培养完成,经验 ' + gr.add;
      })
    }, (gr: GrowthItem, gi: number) => gr.name + String(gi))
  }
  .width('100%')
  .padding(18)
  .backgroundColor('#FFFFFF')
  .borderRadius({ topLeft: 20, topRight: 20 })
  .constraintSize({ maxHeight: '80%' })
  .transition(TransitionEffect.OPACITY.animation({ duration: 200 }))
}

培养升级弹窗是一个底部抽屉式弹窗。与居中卡弹窗不同,底部抽屉的宽度为100%(铺满屏幕宽度),圆角只设置在上方两角(borderRadius({ topLeft: 20, topRight: 20 })),模拟了从底部滑出的抽屉效果。.constraintSize({ maxHeight: '80%' })限制了弹窗最大高度为屏幕的80%,确保内容过多时不会覆盖整个屏幕。

弹窗内部的培养选项使用ForEach遍历getLevelUpOptions()返回的四个选项(灵晶培养、经验药水、星辉石、圣物祝福),第一个选项默认选中(深绿色背景)。下方使用ForEach遍历getGrowthRows()返回的培养方式列表,每行包含名称、经验增量、消耗描述,点击后关闭弹窗并显示Toast提示。

13.4 进化预览弹窗(上下拼接)

@Builder
evolveModal() {
  Column() {
    Column() {
      Text('✨ 进化预览')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
      Text(this.selSpirit!.name + ' → 觉醒形态')
        .fontSize(11)
        .fontColor('#E1BEE7')
        .margin({ top: 4 })
    }
    .width('100%')
    .padding({ top: 18, bottom: 18 })
    .linearGradient({ angle: 135, colors: [['#4A148C', 0], ['#7E57C2', 1]] })
    Column() {
      Row() {
        Column() {
          Text(this.selSpirit!.img)
            .fontSize(38)
          Text('当前形态')
            .fontSize(9)
            .fontColor('#9E9E9E')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        Text('➜')
          .fontSize(24)
          .fontColor('#FFC107')
        Column() {
          Text('👑')
            .fontSize(38)
          Text('觉醒形态')
            .fontSize(9)
            .fontColor('#9E9E9E')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding({ top: 16, bottom: 16 })
      Row() {
        Column() {
          Text('战力 ' + this.selSpirit!.power)
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor('#616161')
          Text('当前')
            .fontSize(9)
            .fontColor('#BDBDBD')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('战力 ' + (this.selSpirit!.power + 360))
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor('#7E57C2')
          Text('进化后')
            .fontSize(9)
            .fontColor('#BDBDBD')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding({ top: 10, bottom: 10 })
      .backgroundColor('#F3E5F5')
      .borderRadius(10)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
  }
  .width('82%')
  .clip(true)
  .borderRadius(18)
  .transition(TransitionEffect.OPACITY.animation({ duration: 200 }))
}

进化预览弹窗采用了上下拼接的视觉结构——上部是紫色渐变头部(深紫#4A148C到浅紫#7E57C2),下部是白色内容区。头部展示标题和进化方向,内容区展示"当前形态→觉醒形态"的对比预览。

内容区的对比布局使用三个等宽Column(通过layoutWeight(1))——左侧当前形态、中间箭头、右侧觉醒形态。下方的战力对比区使用了相同的三列布局,展示进化前后的战力数值变化(当前战力 vs 当前战力+360)。进化后的战力通过(this.selSpirit!.power + 360)动态计算,使每只精灵的进化增益都有具体的数值展示。

13.5 新增精灵弹窗(表单交互)

@Builder
addSpiritModal() {
  Column() {
    Row() {
      Text('新增精灵')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#212121')
      Text('')
        .layoutWeight(1)
      Text('✕')
        .fontSize(15)
        .fontColor('#9E9E9E')
        .padding(4)
        .onClick(() => {
          this.showAddSpirit = false;
        })
    }
    .width('100%')
    Text('精灵名称')
      .fontSize(11)
      .fontColor('#616161')
      .width('100%')
      .margin({ top: 10 })
    Text(this.fName)
      .fontSize(13)
      .fontColor('#212121')
      .width('100%')
      .padding(10)
      .backgroundColor('#F5F5F5')
      .borderRadius(10)
      .margin({ top: 4 })
      .onClick(() => {
        this.fName = '新精灵·' + this.fCount;
      })
    Text('选择元素')
      .fontSize(11)
      .fontColor('#616161')
      .width('100%')
      .margin({ top: 10 })
    Row() {
      ForEach(getElemOptions(), (eo: string, ei: number) => {
        Text(eo)
          .fontSize(11)
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .backgroundColor(this.fElem === eo ? '#1B5E20' : '#F5F5F5')
          .fontColor(this.fElem === eo ? '#FFFFFF' : '#616161')
          .borderRadius(12)
          .margin({ right: 8 })
          .onClick(() => {
            this.fElem = eo;
          })
      }, (eo: string, ei: number) => eo + String(ei))
    }
    .width('100%')
    .margin({ top: 4 })
  }
  .width('100%')
  .padding(18)
  .backgroundColor('#FFFFFF')
  .borderRadius({ topLeft: 20, topRight: 20 })
  .constraintSize({ maxHeight: '80%' })
  .transition(TransitionEffect.OPACITY.animation({ duration: 200 }))
}

新增精灵弹窗是一个表单型底部抽屉。表单包含名称输入(通过点击切换预设名称)、元素选择(六个选项的单选标签组)、稀有度选择(四个选项的单选标签组)和等级步进器(加减按钮控制数值)。

元素选择标签组的技术实现是表单交互的核心。ForEach遍历getElemOptions()返回的六个元素选项,每个选项通过this.fElem === eo判断是否为当前选中值——选中时显示深绿色背景和白色文字,未选中时显示浅灰色背景和深灰色文字。点击选项时this.fElem = eo更新选中状态,由于fElem@State变量,框架会自动重新渲染标签组,更新选中态的视觉表现。

这种"标签组单选"交互模式在ArkTS中是一种常见的表单选择器实现。与下拉选择器(Select)相比,标签组的优点是所有选项可见、选择操作仅需一次点击、选中态可视化清晰。缺点是当选项较多时会占用较大的屏幕空间。在本应用中,元素选项(6个)和稀有度选项(4个)的数量适中,适合使用标签组。

13.6 等级步进器

Row() {
  Text('-')
    .fontSize(16)
    .fontColor('#1B5E20')
    .padding({ left: 14, right: 14, top: 4, bottom: 4 })
    .backgroundColor('#E8F5E9')
    .borderRadius(8)
    .onClick(() => {
      if (this.fLevel > 1) {
        this.fLevel = this.fLevel - 1;
      }
    })
  Text(String(this.fLevel))
    .fontSize(15)
    .fontWeight(FontWeight.Bold)
    .fontColor('#212121')
    .layoutWeight(1)
    .textAlign(TextAlign.Center)
  Text('+')
    .fontSize(16)
    .fontColor('#1B5E20')
    .padding({ left: 14, right: 14, top: 4, bottom: 4 })
    .backgroundColor('#E8F5E9')
    .borderRadius(8)
    .onClick(() => {
      this.fLevel = this.fLevel + 1;
    })
}
.width('100%')
.margin({ top: 4 })

等级步进器由减号按钮、数值显示和加号按钮三部分组成。减号按钮在onClick中先判断this.fLevel > 1确保等级不低于1,然后执行this.fLevel = this.fLevel - 1。加号按钮直接执行this.fLevel = this.fLevel + 1,没有上限检查。数值显示通过Text(String(this.fLevel))将数字转换为字符串渲染。

String(this.fLevel)的类型转换是必要的——ArkTS的Text组件接收字符串参数,而fLevelnumber类型,必须通过String()函数进行显式转换。这种严格的类型检查是ArkTS类型安全体系的体现,避免了JavaScript中隐式类型转换带来的潜在问题。

13.7 删除确认弹窗(深色警示)

@Builder
delSpiritModal() {
  Column() {
    Text('⚠️')
      .fontSize(40)
    Text('确认放归 ' + this.selSpirit!.name + '?')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')
      .margin({ top: 10 })
    Text('放归后将无法找回,培养资源不返还')
      .fontSize(11)
      .fontColor('#B0BEC5')
      .margin({ top: 6 })
    Row() {
      Text('取消')
        .fontSize(13)
        .fontColor('#FFFFFF')
        .padding({ left: 20, right: 20, top: 10, bottom: 10 })
        .backgroundColor('rgba(255,255,255,0.15)')
        .borderRadius(22)
        .onClick(() => {
          this.showDelSpirit = false;
        })
      Text('')
        .layoutWeight(1)
      Text('确认放归')
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1B5E20')
        .padding({ left: 20, right: 20, top: 10, bottom: 10 })
        .backgroundColor('#FFC107')
        .borderRadius(22)
        .onClick(() => {
          this.showDelSpirit = false;
          this.toast = '已放归 ' + this.selSpirit!.name;
        })
    }
    .width('100%')
    .margin({ top: 16 })
  }
  .width('82%')
  .padding(20)
  .backgroundColor('#263238')
  .borderRadius(16)
  .clip(true)
  .transition(TransitionEffect.OPACITY.animation({ duration: 200 }))
}

删除确认弹窗使用了深色背景(#263238),与其他白色背景弹窗形成鲜明对比。这种深色警示设计是确认类弹窗的常见模式——通过深色背景传达"这是一个需要谨慎的操作"的视觉暗示。

弹窗底部有两个操作按钮:左侧"取消"使用半透明白色背景(rgba(255,255,255,0.15)),视觉上弱化;右侧"确认放归"使用琥珀色(#FFC107)背景和深绿色文字,视觉上突出但不像红色那样过于激进。这种"弱化取消+突出确认但不极端"的按钮设计,在传达警示的同时避免了过度恐吓用户。

13.8 充值弹窗(档位选择)

@Builder
rechargeModal() {
  Column() {
    Row() {
      Text('💰 灵晶充值')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#212121')
      Text('')
        .layoutWeight(1)
      Text('✕')
        .fontSize(15)
        .fontColor('#9E9E9E')
        .padding(4)
        .onClick(() => {
          this.showRecharge = false;
        })
    }
    .width('100%')
    Text('当前余额:12860 灵晶')
      .fontSize(11)
      .fontColor('#757575')
      .width('100%')
      .margin({ top: 4 })
    Row() {
      ForEach(getPayLevels(), (pl: number, pi: number) => {
        Column() {
          Text('¥' + pl)
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.fPayLevel === pl ? '#FFFFFF' : '#212121')
          Text(getPayGifts()[pi])
            .fontSize(9)
            .fontColor(this.fPayLevel === pl ? '#FFF8E1' : '#9E9E9E')
            .margin({ top: 3 })
        }
        .layoutWeight(1)
        .padding({ top: 10, bottom: 10 })
        .backgroundColor(this.fPayLevel === pl ? '#1B5E20' : '#F5F5F5')
        .borderRadius(10)
        .margin({ right: 8 })
        .onClick(() => {
          this.fPayLevel = pl;
        })
      }, (pl: number, pi: number) => String(pl) + String(pi))
    }
    .width('100%')
    .margin({ top: 12 })
    Text('首充双倍 · 本档赠送 ' + getPayGifts()[3])
      .fontSize(10)
      .fontColor('#FF8F00')
      .width('100%')
      .margin({ top: 10 })
    Text('立即充值 ¥' + this.fPayLevel)
      .fontSize(13)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FFFFFF')
      .width('100%')
      .textAlign(TextAlign.Center)
      .padding({ top: 10, bottom: 10 })
      .backgroundColor('#FF8F00')
      .borderRadius(24)
      .margin({ top: 12 })
      .onClick(() => {
        this.showRecharge = false;
        this.toast = '充值成功,灵晶到账';
      })
  }
  .width('100%')
  .padding(18)
  .backgroundColor('#FFFFFF')
  .borderRadius({ topLeft: 20, topRight: 20 })
  .constraintSize({ maxHeight: '80%' })
  .transition(TransitionEffect.OPACITY.animation({ duration: 200 }))
}

充值弹窗使用ForEach遍历getPayLevels()返回的六个充值档位,每个档位显示价格(¥6/¥30/¥68/¥128/¥328/¥648)和对应的灵晶奖励。选中态通过this.fPayLevel === pl判断——选中时使用深绿色背景和白色文字,未选中时使用浅灰色背景和深色文字。

底部确认按钮的文字'立即充值 ¥' + this.fPayLevel动态显示当前选中的充值金额,使用橙色(#FF8F00)背景与深绿色弹窗形成对比,引导用户完成充值操作。

13.9 商品详情弹窗(票根式)

@Builder
shopDetailModal() {
  Column() {
    Column() {
      Text(this.selShop!.img)
        .fontSize(38)
      Text(this.selShop!.name)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .margin({ top: 6 })
      Text(this.selShop!.tag + ' · 限时上架')
        .fontSize(10)
        .fontColor('#FFF8E1')
        .margin({ top: 3 })
    }
    .width('100%')
    .padding({ top: 18, bottom: 18 })
    .linearGradient({ angle: 135, colors: [['#1B5E20', 0], ['#2E7D32', 1]] })
    Row() {
      Text('¥' + this.selShop!.price)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#C62828')
      Text('¥' + this.selShop!.oldPrice)
        .fontSize(11)
        .fontColor('#BDBDBD')
        .decoration({ type: TextDecorationType.LineThrough })
        .margin({ left: 8 })
      Text('')
        .layoutWeight(1)
      Text('立省 ¥' + (this.selShop!.oldPrice - this.selShop!.price))
        .fontSize(11)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1B5E20')
    }
    .width('100%')
    .padding({ left: 14, right: 14, top: 12, bottom: 12 })
    .backgroundColor('#FFF8E1')
    .margin({ top: 12 })
    Row() {
      Text('购买数量')
        .fontSize(12)
        .fontColor('#212121')
      Text('')
        .layoutWeight(1)
      Text('-')
        .fontSize(15)
        .fontColor('#1B5E20')
        .padding({ left: 12, right: 12, top: 3, bottom: 3 })
        .backgroundColor('#E8F5E9')
        .borderRadius(8)
        .onClick(() => {
          if (this.fCount > 1) {
            this.fCount = this.fCount - 1;
          }
        })
      Text(String(this.fCount))
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#212121')
        .width(36)
        .textAlign(TextAlign.Center)
      Text('+')
        .fontSize(15)
        .fontColor('#1B5E20')
        .padding({ left: 12, right: 12, top: 3, bottom: 3 })
        .backgroundColor('#E8F5E9')
        .borderRadius(8)
        .onClick(() => {
          this.fCount = this.fCount + 1;
        })
    }
    .width('100%')
    .padding({ top: 10, bottom: 10 })
    Text('合计 ¥' + (this.selShop!.price * this.fCount))
      .fontSize(12)
      .fontWeight(FontWeight.Bold)
      .fontColor('#C62828')
      .width('100%')
      .margin({ top: 6 })
  }
  .width('82%')
  .clip(true)
  .borderRadius(18)
  .transition(TransitionEffect.OPACITY.animation({ duration: 200 }))
}

商品详情弹窗采用了"票根式"设计——上部是深绿渐变头部(商品图标、名称、标签),下部是白色和淡黄色交替的信息区。价格行展示了现价、原价(带删除线)和折扣信息("立省 ¥XX"通过this.selShop!.oldPrice - this.selShop!.price动态计算)。

购买数量步进器与等级步进器的实现方式相同,但额外添加了合计金额的计算'合计 ¥' + (this.selShop!.price * this.fCount)——将商品单价与购买数量相乘,实时显示总价。当用户点击加号或减号调整数量时,fCount状态变化触发合计金额的自动更新,这是数据驱动UI的典型体现。

13.10 出战装备弹窗(底部抽屉)

@Builder
swapModal() {
  Column() {
    Row() {
      Text('⚔️ 出战装备调整')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#212121')
      Text('')
        .layoutWeight(1)
      Text('✕')
        .fontSize(15)
        .fontColor('#9E9E9E')
        .padding(4)
        .onClick(() => {
          this.showSwap = false;
        })
    }
    .width('100%')
    ForEach(getSlotList(), (sl: string, si: number) => {
      Row() {
        Text(si === 0 ? '🧚' : (si === 1 ? '🐆' : '🐈⬛'))
          .fontSize(24)
        Column() {
          Text(sl)
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor('#212121')
          Text('当前出战 · 点击更换')
            .fontSize(9)
            .fontColor('#9E9E9E')
            .margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        .margin({ left: 10 })
        Text(this.fSlot === sl ? '使用中' : '替换')
          .fontSize(10)
          .fontColor(this.fSlot === sl ? '#1B5E20' : '#FFFFFF')
          .padding({ left: 12, right: 12, top: 5, bottom: 5 })
          .backgroundColor(this.fSlot === sl ? '#E8F5E9' : '#1B5E20')
          .borderRadius(10)
          .onClick(() => {
            this.fSlot = sl;
          })
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#F9FBE7')
      .borderRadius(12)
      .margin({ top: 10 })
    }, (sl: string, si: number) => sl + String(si))
  }
  .width('100%')
  .padding(18)
  .backgroundColor('#FFFFFF')
  .borderRadius({ topLeft: 20, topRight: 20 })
  .constraintSize({ maxHeight: '80%' })
  .transition(TransitionEffect.OPACITY.animation({ duration: 200 }))
}

出战装备弹窗使用ForEach遍历getSlotList()返回的三个装备槽位。每个槽位行的右侧有一个状态按钮,通过this.fSlot === sl判断当前槽位是否为选中状态——选中时显示"使用中"文字和浅绿色背景,未选中时显示"替换"文字和深绿色背景。点击按钮时this.fSlot = sl更新选中状态,实现单选切换效果。

十四、Toast提示系统

Toast是应用的全局轻量级提示组件,用于在操作完成时给用户即时反馈。

if (this.toast.length > 0) {
  Column() {
    Text(this.toast)
      .fontSize(12)
      .fontColor('#FFFFFF')
      .padding({ left: 16, right: 16, top: 8, bottom: 8 })
      .backgroundColor('rgba(30,30,30,0.85)')
      .borderRadius(18)
  }
  .width('100%')
  .position({ y: '72%' })
  .alignItems(HorizontalAlign.Center)
  .onAppear(() => {
    setTimeout(() => {
      this.toast = '';
    }, 1600)
  })
}

Toast的条件渲染由this.toast.length > 0控制——当toast字符串非空时渲染,为空时不渲染。Toast使用85%不透明度的深灰色背景(rgba(30,30,30,0.85))和白色文字,通过.position({ y: '72%' })定位在屏幕72%高度处,使其浮于内容区上方但不遮挡顶部信息。

.onAppear()是组件的生命周期回调,在组件首次渲染完成后触发。在onAppear中调用setTimeout(() => { this.toast = ''; }, 1600)设置1.6秒后清空toast字符串——由于toast@State变量,赋值为空字符串会触发条件重新评估,使Toast组件从DOM中移除,实现了"自动消失"的Toast行为。

Toast的实现展示了ArkTS中"状态驱动生命周期"的设计理念。Toast的显示由toast状态变量控制,Toast的消失由onAppear生命周期中的setTimeout触发状态变更来实现。整个过程不需要任何命令式的DOM操作(如element.remove()),完全通过状态变化驱动UI的挂载和卸载。这种设计使得Toast的逻辑简洁、可预测、易于测试。

十五、数据查找方法群

主组件Index定义了七个查找方法,用于根据ID从静态数组中检索特定的数据对象。

findSpirit(id: number): SpiritItem | null {
  for (let i = 0; i < SPIRIT_LIST.length; i++) {
    if (SPIRIT_LIST[i].id === id) {
      return SPIRIT_LIST[i];
    }
  }
  return null;
}

findRealm(id: number): RealmItem | null {
  for (let i = 0; i < REALM_LIST.length; i++) {
    if (REALM_LIST[i].id === id) {
      return REALM_LIST[i];
    }
  }
  return null;
}

findQuest(id: number): QuestItem | null {
  for (let i = 0; i < QUEST_LIST.length; i++) {
    if (QUEST_LIST[i].id === id) {
      return QUEST_LIST[i];
    }
  }
  return null;
}

findShop(id: number): ShopItem | null {
  for (let i = 0; i < SHOP_LIST.length; i++) {
    if (SHOP_LIST[i].id === id) {
      return SHOP_LIST[i];
    }
  }
  return null;
}

findCodex(id: number): CodexItem | null {
  for (let i = 0; i < CODEX_LIST.length; i++) {
    if (CODEX_LIST[i].id === id) {
      return CODEX_LIST[i];
    }
  }
  return null;
}

findFav(id: number): FavItem | null {
  for (let i = 0; i < FAV_LIST.length; i++) {
    if (FAV_LIST[i].id === id) {
      return FAV_LIST[i];
    }
  }
  return null;
}

findAch(id: number): AchItem | null {
  for (let i = 0; i < ACH_LIST.length; i++) {
    if (ACH_LIST[i].id === id) {
      return ACH_LIST[i];
    }
  }
  return null;
}

七个查找方法的实现模式完全一致——通过for循环遍历对应的静态数组,使用严格相等===比较ID字段,找到匹配项则返回该对象,遍历结束未找到则返回null。返回类型使用了联合类型SpiritItem | null,明确表达了"可能找不到"的语义,强制调用方处理null情况。

这些查找方法是子组件回调到弹窗展示之间的关键桥梁。当用户在子组件中点击某个精灵时,子组件调用onOpenSpirit(id)将精灵ID传递给主组件,主组件调用findSpirit(id)从静态数组中检索精灵对象,将检索到的对象赋值给selSpirit状态变量,然后设置showSpiritDetail = true触发弹窗渲染。弹窗通过this.selSpirit!.img等非空断言访问选中精灵的数据,完成数据的闭环传递。

在实际工程中,如果数据量较大或查找频繁,应该考虑使用Map<number, SpiritItem>等哈希表结构替代线性查找,将查找复杂度从O(n)降低到O(1)。但在本应用中,每组的元素数量不超过12个,线性查找的性能开销可以忽略不计,使用简单的for循环反而更直观、更易维护。

十六、技术要点总结与对比

在整个应用的实现过程中,我们涵盖了ArkTS声明式UI开发的诸多核心技术点。以下通过表格形式对这些技术点进行系统性的对比和总结。

16.1 核心组件容器对比

容器组件 布局方向 典型用途 关键属性 本应用使用场景
Column 垂直 纵向内容排列 alignItems(HorizontalAlign) 页面主体、卡片内部、弹窗内容
Row 水平 横向内容排列 justifyContent(FlexAlign) 标题栏、按钮组、统计数据行
Scroll 可配置 内容溢出滚动 scrollable(ScrollDirection) 页面滚动、精灵横滑、筛选条横滑
Stack 层叠 重叠元素 zIndex/position 遮罩层与弹窗的层叠(本应用使用Column替代)

16.2 装饰器功能对比

装饰器 作用 使用位置 触发机制 本应用使用场景
@Entry 标记入口组件 struct声明 应用启动 Index主组件
@Component 标记自定义组件 struct声明 组件引用时实例化 Index主组件
@State 响应式状态绑定 成员变量 赋值时触发重渲染 curTab、show状态、sel数据、f*表单
@Builder 封装可复用UI片段 成员方法 方法调用时渲染 卡片Builder、弹窗Builder、Tab项Builder

16.3 列表渲染与状态管理对比

技术点 作用 数据驱动方式 性能特征 本应用使用场景
ForEach 列表渲染 数据数组+键值生成器 差量更新,仅重渲染变化的项 精灵列表、秘境列表、任务列表等所有列表
条件渲染(if) 条件展示 布尔表达式 真值切换时挂载/卸载组件 页面路由切换、弹窗显隐、Toast显示
layoutWeight 弹性分配 数值权重 父容器空间按权重分配 等宽列分布、弹性占位
transition 过渡动画 TransitionEffect 组件出现/消失时触发 弹窗的透明度渐入渐出

16.4 弹窗类型对比

弹窗类型 布局特征 圆角设置 宽度 典型场景 本应用实例
居中卡 居中展示 四角圆角(18vp) 82% 详情展示、确认操作 精灵详情、秘境详情、图鉴详情、成就详情
底部抽屉 底部贴边 上两角圆角(20vp) 100% 表单填写、选项选择 培养升级、新增精灵、编辑精灵、充值、出战装备
深色警示 居中展示 四角圆角(16vp) 82% 危险操作确认 删除精灵、取消收藏
渐变头部 居中展示 四角圆角(18vp)+clip 82% 预览展示、商品详情 进化预览、商品详情

十七、全文总结

本文以一个完整的精灵养成系统为案例,深入剖析了鸿蒙ArkTS声明式UI开发的核心技术实践。从数据模型定义到静态数据组织,从工具函数封装到多页面组件构建,从主组件状态管理到弹窗交互系统,我们完整地走过了整个应用的技术实现路径。

在数据模型层面,应用通过十个接口定义了严格的类型约束,确保了数据在编译期的类型安全性。这种以接口为先的开发模式,使得业务实体的结构在编码之初就被明确界定,为后续的视图渲染和逻辑处理提供了可靠的类型基础。十二组静态数据常量构成了应用的数据底座,覆盖了精灵、秘境、任务、商品、图鉴、收藏、成就等全部业务实体,每组数据都经过精心设计,字段含义清晰、数据冗余适度。

在逻辑处理层面,三十余个工具函数承担了数据拆分、颜色映射和数据查找三大职责。数据拆分函数通过步长遍历将一维数组拆分为多列子数组,适配了ArkTS的线性ForEach渲染模型。颜色映射函数将元素属性、稀有度、难度、任务类型等业务概念映射为视觉色彩,建立了"业务语义-视觉表现"的直觉映射关系。这些函数虽然实现简单,但它们的统一命名模式(getXxxRows、getXxxColor)为整个应用提供了一致的代码风格。

在视图层,七个独立子组件(HomeTab、SpiritTab、RealmTab、QuestTab、ShopTab、CodexTab、MineTab)分别负责七个功能页面的UI渲染。每个子组件通过回调函数与父组件通信,自身不持有任何状态,仅负责"将数据渲染为UI"和"将用户事件冒泡给父组件"两个职责。这种设计使得子组件高度可复用、可独立测试。

主组件Index作为状态中枢,通过三十个@State变量管理着页面路由、十六个弹窗的显隐、七个选中数据对象和十个表单填写状态。所有状态变更都集中在主组件中处理,通过"查找-赋值-触发"三步模式响应子组件的回调事件。这种集中式状态管理确保了状态的可追踪性和单向数据流,使得整个应用的状态变化路径清晰可预测。

弹窗系统由一个遮罩层Builder和十六个弹窗Builder组成,覆盖了居中卡、底部抽屉、深色警示和渐变头部四种弹窗类型。每种类型都有其适用的交互场景——居中卡用于详情展示、底部抽屉用于表单填写、深色警示用于危险操作确认、渐变头部用于视觉强调的预览展示。所有弹窗都通过TransitionEffect.OPACITY.animation({ duration: 200 })添加了透明度过渡动画,使交互体验流畅自然。


安装DevEco Studio程序

在这里插入图片描述
选择目标安装目录:

在这里插入图片描述
设置环境变量,但是需要重启一下:

在这里插入图片描述
新建一个空白模板:

在这里插入图片描述
设置API为24的模板项目:
在这里插入图片描述
初始化项目,自动下载相关依赖:

在这里插入图片描述


完整代码:

interface SpiritItem {
  id: number;
  name: string;
  elem: string;
  rare: string;
  level: number;
  power: number;
  img: string;
  intro: string;
  tags: string;
}

interface RealmItem {
  id: number;
  name: string;
  diff: string;
  power: number;
  reward: string;
  desc: string;
}

interface QuestItem {
  id: number;
  name: string;
  type: string;
  point: number;
  progress: number;
  total: number;
  reward: string;
}

interface ShopItem {
  id: number;
  name: string;
  price: number;
  oldPrice: number;
  img: string;
  tag: string;
}

interface CodexItem {
  id: number;
  name: string;
  elem: string;
  rare: string;
  img: string;
  desc: string;
}

interface FavItem {
  id: number;
  name: string;
  elem: string;
  level: number;
  img: string;
  time: string;
}

interface AchItem {
  id: number;
  name: string;
  desc: string;
  point: number;
  done: boolean;
  img: string;
}

interface ElemPower {
  name: string;
  value: number;
}

interface GrowthItem {
  name: string;
  cost: string;
  add: string;
}

interface QuestType {
  name: string;
  color: string;
}

const SPIRIT_LIST: SpiritItem[] = [
  { id: 1, name: '萤火小仙', elem: '光', rare: '传说', level: 45, power: 860, img: '🧚', intro: '栖息在萤火森林深处的光系精灵,擅长治愈队友并驱散暗影。', tags: '治愈 驱散 光盾' },
  { id: 2, name: '岩壳小宝', elem: '土', rare: '史诗', level: 38, power: 720, img: '🪨', intro: '背负岩石铠甲的憨厚精灵,防御力出众,是团队最可靠的盾。', tags: '护盾 嘲讽 壁垒' },
  { id: 3, name: '风铃雀', elem: '风', rare: '稀有', level: 32, power: 610, img: '🐦', intro: '飞行速度极快的风系精灵,能在秘境中为团队探路引航。', tags: '加速 侦查 闪避' },
  { id: 4, name: '火苗狐', elem: '火', rare: '史诗', level: 41, power: 790, img: '🦊', intro: '尾巴燃着火焰的狐狸精灵,热情而忠诚,输出极其凶猛。', tags: '爆发 灼烧 狂暴' },
  { id: 5, name: '雾沼蛙', elem: '水', rare: '稀有', level: 29, power: 540, img: '🐸', intro: '生活在迷雾沼泽的水系精灵,擅长施放减速与潮湿效果。', tags: '减速 潮湿 治疗' },
  { id: 6, name: '雷纹豹', elem: '雷', rare: '传说', level: 48, power: 920, img: '🐆', intro: '身上密布雷纹的猎豹精灵,速度与爆发兼备,一击致命。', tags: '麻痹 突袭 连锁' },
  { id: 7, name: '藤蔓鹿', elem: '木', rare: '史诗', level: 36, power: 700, img: '🦌', intro: '鹿角缠绕藤蔓的森林守护者,能为全队持续恢复生命。', tags: '缠绕 回春 森林' },
  { id: 8, name: '冰晶蝶', elem: '冰', rare: '稀有', level: 27, power: 510, img: '🦋', intro: '翅膀凝结冰晶的美丽精灵,可冻结敌人并降低其攻速。', tags: '冻结 减速 暴风雪' },
  { id: 9, name: '暗影猫', elem: '暗', rare: '传说', level: 50, power: 980, img: '🐈⬛', intro: '游走于阴影中的神秘精灵,隐匿身形后出手必中要害。', tags: '隐匿 背刺 夜行' },
  { id: 10, name: '星光鲸', elem: '星', rare: '史诗', level: 43, power: 840, img: '🐳', intro: '在星海中遨游的巨鲸精灵,喷出的星尘可为全队充能。', tags: '充能 星光 巨兽' },
  { id: 11, name: '沙尘狮', elem: '土', rare: '稀有', level: 31, power: 590, img: '🦁', intro: '沙漠之王的幼崽精灵,掀起沙暴遮蔽敌人的视野。', tags: '沙暴 致盲 荒野' },
  { id: 12, name: '花灵兔', elem: '木', rare: '普通', level: 22, power: 420, img: '🐰', intro: '乖巧可爱的新手精灵,随身携带的小花能回复少量体力。', tags: '新手 采集 开花' }
];

const HOME_SPIRITS: SpiritItem[] = [
  { id: 1, name: '萤火小仙', elem: '光', rare: '传说', level: 45, power: 860, img: '🧚', intro: '', tags: '' },
  { id: 6, name: '雷纹豹', elem: '雷', rare: '传说', level: 48, power: 920, img: '🐆', intro: '', tags: '' },
  { id: 9, name: '暗影猫', elem: '暗', rare: '传说', level: 50, power: 980, img: '🐈⬛', intro: '', tags: '' },
  { id: 4, name: '火苗狐', elem: '火', rare: '史诗', level: 41, power: 790, img: '🦊', intro: '', tags: '' },
  { id: 10, name: '星光鲸', elem: '星', rare: '史诗', level: 43, power: 840, img: '🐳', intro: '', tags: '' },
  { id: 2, name: '岩壳小宝', elem: '土', rare: '史诗', level: 38, power: 720, img: '🪨', intro: '', tags: '' },
  { id: 7, name: '藤蔓鹿', elem: '木', rare: '史诗', level: 36, power: 700, img: '🦌', intro: '', tags: '' },
  { id: 3, name: '风铃雀', elem: '风', rare: '稀有', level: 32, power: 610, img: '🐦', intro: '', tags: '' }
];

const REALM_LIST: RealmItem[] = [
  { id: 1, name: '萤火森林', diff: '简单', power: 300, reward: '灵晶×200', desc: '光系精灵栖息地,适合新手采集灵晶与光之碎片。' },
  { id: 2, name: '雷鸣峡谷', diff: '普通', power: 480, reward: '雷纹石×150', desc: '雷云常年不散,谷内闪电不断,是雷系精灵的试炼场。' },
  { id: 3, name: '冰晶冻原', diff: '普通', power: 520, reward: '冰晶×180', desc: '终年冰雪覆盖的冻原,藏着冻结万物的古老力量。' },
  { id: 4, name: '暗影深渊', diff: '困难', power: 760, reward: '暗影结晶×220', desc: '阳光永远无法到达的深渊,暗系精灵在此蛰伏。' },
  { id: 5, name: '星海遗迹', diff: '困难', power: 820, reward: '星辉石×260', desc: '坠落的星辰化为遗迹,星光之力充盈每一块砖石。' },
  { id: 6, name: '熔岩王座', diff: '噩梦', power: 1050, reward: '火种圣物×1', desc: '远古火精灵的沉睡之地,只有强者才能踏入王座。' },
  { id: 7, name: '迷雾沼泽', diff: '普通', power: 460, reward: '水灵珠×160', desc: '浓雾弥漫的湿地,深处藏着稀有水系精灵的巢穴。' },
  { id: 8, name: '风啸高塔', diff: '困难', power: 880, reward: '风翎×240', desc: '直插云霄的古老高塔,塔顶的风能撕裂一切。' }
];

const QUEST_LIST: QuestItem[] = [
  { id: 1, name: '喂食精灵三次', type: '日常', point: 20, progress: 3, total: 3, reward: '灵晶×30' },
  { id: 2, name: '完成一次秘境探索', type: '日常', point: 30, progress: 1, total: 1, reward: '灵晶×50' },
  { id: 3, name: '培养任意精灵', type: '日常', point: 20, progress: 2, total: 2, reward: '经验药水×2' },
  { id: 4, name: '收集图鉴 5 种精灵', type: '周常', point: 60, progress: 5, total: 8, reward: '唤灵券×1' },
  { id: 5, name: '挑战噩梦秘境', type: '周常', point: 100, progress: 1, total: 1, reward: '传说碎片×3' },
  { id: 6, name: '签到七日', type: '周常', point: 80, progress: 5, total: 7, reward: '星辉石×5' },
  { id: 7, name: '完成十次探索', type: '成就', point: 150, progress: 8, total: 10, reward: '限定称号' },
  { id: 8, name: '拥有传说精灵', type: '成就', point: 200, progress: 3, total: 3, reward: '金色头像框' },
  { id: 9, name: '给好友送祝福', type: '日常', point: 15, progress: 1, total: 3, reward: '友谊花×3' },
  { id: 10, name: '升级到 40 级', type: '成就', point: 120, progress: 1, total: 1, reward: '豪华礼包' }
];

const SHOP_LIST: ShopItem[] = [
  { id: 1, name: '唤灵券(单次)', price: 300, oldPrice: 400, img: '🎴', tag: '热卖' },
  { id: 2, name: '唤灵券(十连)', price: 2800, oldPrice: 4000, img: '🎴', tag: '超值' },
  { id: 3, name: '灵晶小袋', price: 680, oldPrice: 900, img: '💎', tag: '新人' },
  { id: 4, name: '灵晶大袋', price: 3280, oldPrice: 4500, img: '💎', tag: '限时' },
  { id: 5, name: '经验药水(中)', price: 120, oldPrice: 160, img: '🧪', tag: '日常' },
  { id: 6, name: '经验药水(大)', price: 320, oldPrice: 420, img: '🧪', tag: '热卖' },
  { id: 7, name: '精灵口粮礼盒', price: 500, oldPrice: 680, img: '🍖', tag: '必买' },
  { id: 8, name: '星辉石礼包', price: 1980, oldPrice: 2600, img: '⭐', tag: '限时' },
  { id: 9, name: '限定皮肤·星夜', price: 1680, oldPrice: 2200, img: '🎭', tag: '限定' },
  { id: 10, name: '精灵小屋装饰', price: 880, oldPrice: 1200, img: '🏡', tag: '新品' }
];

const CODEX_LIST: CodexItem[] = [
  { id: 1, name: '萤火小仙', elem: '光', rare: '传说', img: '🧚', desc: '光系治愈精灵,技能「萤光雨」可驱散全队负面状态。' },
  { id: 2, name: '雷纹豹', elem: '雷', rare: '传说', img: '🐆', desc: '雷系爆发精灵,「雷霆突袭」可对单体造成巨额伤害。' },
  { id: 3, name: '暗影猫', elem: '暗', rare: '传说', img: '🐈⬛', desc: '暗系刺客精灵,隐匿后「背刺」必定暴击。' },
  { id: 4, name: '岩壳小宝', elem: '土', rare: '史诗', img: '🪨', desc: '土系坦克精灵,「岩之壁垒」可抵挡三回合伤害。' },
  { id: 5, name: '火苗狐', elem: '火', rare: '史诗', img: '🦊', desc: '火系输出精灵,「烈焰狐火」附带灼烧效果。' },
  { id: 6, name: '藤蔓鹿', elem: '木', rare: '史诗', img: '🦌', desc: '木系辅助精灵,「生命藤蔓」持续回复全队生命。' },
  { id: 7, name: '星光鲸', elem: '星', rare: '史诗', img: '🐳', desc: '星系充能精灵,「星尘喷涌」为全队恢复技能值。' },
  { id: 8, name: '风铃雀', elem: '风', rare: '稀有', img: '🐦', desc: '风系侦查精灵,「疾风之翼」大幅提升队伍速度。' },
  { id: 9, name: '雾沼蛙', elem: '水', rare: '稀有', img: '🐸', desc: '水系控制精灵,「迷雾泡泡」减速并削弱敌人。' },
  { id: 10, name: '冰晶蝶', elem: '冰', rare: '稀有', img: '🦋', desc: '冰系控制精灵,「霜冻之舞」可冻结敌人一回合。' },
  { id: 11, name: '沙尘狮', elem: '土', rare: '稀有', img: '🦁', desc: '土系干扰精灵,「沙暴迷眼」降低敌方命中率。' },
  { id: 12, name: '花灵兔', elem: '木', rare: '普通', img: '🐰', desc: '木系采集精灵,「花蜜采集」在秘境中收益翻倍。' }
];

const FAV_LIST: FavItem[] = [
  { id: 1, name: '萤火小仙', elem: '光', level: 45, img: '🧚', time: '本月 8 日' },
  { id: 2, name: '雷纹豹', elem: '雷', level: 48, img: '🐆', time: '本月 8 日' },
  { id: 3, name: '暗影猫', elem: '暗', level: 50, img: '🐈⬛', time: '本月 7 日' },
  { id: 4, name: '星光鲸', elem: '星', level: 43, img: '🐳', time: '本月 5 日' },
  { id: 5, name: '火苗狐', elem: '火', level: 41, img: '🦊', time: '上周' },
  { id: 6, name: '藤蔓鹿', elem: '木', level: 36, img: '🦌', time: '上周' },
  { id: 7, name: '岩壳小宝', elem: '土', level: 38, img: '🪨', time: '两周前' },
  { id: 8, name: '风铃雀', elem: '风', level: 32, img: '🐦', time: '两周前' }
];

const ACH_LIST: AchItem[] = [
  { id: 1, name: '初入秘境', desc: '首次完成秘境探索', point: 50, done: true, img: '🏅' },
  { id: 2, name: '精灵收藏家', desc: '图鉴收集达到 10 种', point: 100, done: true, img: '📖' },
  { id: 3, name: '传说缔造者', desc: '拥有 3 只传说精灵', point: 200, done: true, img: '👑' },
  { id: 4, name: '秘境开拓者', desc: '通关全部困难秘境', point: 150, done: false, img: '🗺️' },
  { id: 5, name: '噩梦征服者', desc: '首次挑战噩梦秘境', point: 300, done: false, img: '🔥' },
  { id: 6, name: '星辉猎手', desc: '累计获得星辉石 100 颗', point: 120, done: false, img: '⭐' },
  { id: 7, name: '驯养大师', desc: '培养精灵达到 50 级', point: 250, done: false, img: '🪄' },
  { id: 8, name: '七日打卡', desc: '连续签到 7 天', point: 80, done: true, img: '📅' }
];

const ELEM_POWER: ElemPower[] = [
  { name: '光', value: 860 },
  { name: '雷', value: 920 },
  { name: '暗', value: 980 },
  { name: '火', value: 790 },
  { name: '星', value: 840 },
  { name: '木', value: 700 }
];

const GROWTH_LIST: GrowthItem[] = [
  { name: '灵晶培养', cost: '200 灵晶', add: '+120 经验' },
  { name: '经验药水', cost: '药水×1', add: '+300 经验' },
  { name: '星辉石', cost: '星辉石×5', add: '+800 经验' },
  { name: '圣物祝福', cost: '圣物×1', add: '等级 +1' }
];

const QUEST_TYPE: QuestType[] = [
  { name: '日常', color: '#43A047' },
  { name: '周常', color: '#FF8F00' },
  { name: '成就', color: '#C2185B' }
];

function getHomeSpirits(): SpiritItem[] {
  return HOME_SPIRITS;
}

function getSpiritRows(): SpiritItem[] {
  let arr: SpiritItem[] = [];
  for (let i = 0; i < SPIRIT_LIST.length; i += 2) {
    arr.push(SPIRIT_LIST[i]);
  }
  return arr;
}

function getSpiritRows2(): SpiritItem[] {
  let arr: SpiritItem[] = [];
  for (let i = 1; i < SPIRIT_LIST.length; i += 2) {
    arr.push(SPIRIT_LIST[i]);
  }
  return arr;
}

function getRealmRows(): RealmItem[] {
  let arr: RealmItem[] = [];
  for (let i = 0; i < REALM_LIST.length; i += 2) {
    arr.push(REALM_LIST[i]);
  }
  return arr;
}

function getRealmRows2(): RealmItem[] {
  let arr: RealmItem[] = [];
  for (let i = 1; i < REALM_LIST.length; i += 2) {
    arr.push(REALM_LIST[i]);
  }
  return arr;
}

function getQuestRows(): QuestItem[] {
  let arr: QuestItem[] = [];
  for (let i = 0; i < QUEST_LIST.length; i += 2) {
    arr.push(QUEST_LIST[i]);
  }
  return arr;
}

function getQuestRows2(): QuestItem[] {
  let arr: QuestItem[] = [];
  for (let i = 1; i < QUEST_LIST.length; i += 2) {
    arr.push(QUEST_LIST[i]);
  }
  return arr;
}

function getQuestTop(): QuestItem[] {
  let arr: QuestItem[] = [];
  for (let i = 0; i < 4; i++) {
    arr.push(QUEST_LIST[i]);
  }
  return arr;
}

function getShopRows(): ShopItem[] {
  let arr: ShopItem[] = [];
  for (let i = 0; i < SHOP_LIST.length; i += 2) {
    arr.push(SHOP_LIST[i]);
  }
  return arr;
}

function getShopRows2(): ShopItem[] {
  let arr: ShopItem[] = [];
  for (let i = 1; i < SHOP_LIST.length; i += 2) {
    arr.push(SHOP_LIST[i]);
  }
  return arr;
}

function getCodexRows(): CodexItem[] {
  let arr: CodexItem[] = [];
  for (let i = 0; i < CODEX_LIST.length; i += 3) {
    arr.push(CODEX_LIST[i]);
  }
  return arr;
}

function getCodexRows2(): CodexItem[] {
  let arr: CodexItem[] = [];
  for (let i = 1; i < CODEX_LIST.length; i += 3) {
    arr.push(CODEX_LIST[i]);
  }
  return arr;
}

function getCodexRows3(): CodexItem[] {
  let arr: CodexItem[] = [];
  for (let i = 2; i < CODEX_LIST.length; i += 3) {
    arr.push(CODEX_LIST[i]);
  }
  return arr;
}

function getFavRows(): FavItem[] {
  let arr: FavItem[] = [];
  for (let i = 0; i < FAV_LIST.length; i += 2) {
    arr.push(FAV_LIST[i]);
  }
  return arr;
}

function getFavRows2(): FavItem[] {
  let arr: FavItem[] = [];
  for (let i = 1; i < FAV_LIST.length; i += 2) {
    arr.push(FAV_LIST[i]);
  }
  return arr;
}

function getAchRows(): AchItem[] {
  let arr: AchItem[] = [];
  for (let i = 0; i < ACH_LIST.length; i += 2) {
    arr.push(ACH_LIST[i]);
  }
  return arr;
}

function getAchRows2(): AchItem[] {
  let arr: AchItem[] = [];
  for (let i = 1; i < ACH_LIST.length; i += 2) {
    arr.push(ACH_LIST[i]);
  }
  return arr;
}

function getElemPower(): ElemPower[] {
  return ELEM_POWER;
}

function getGrowthRows(): GrowthItem[] {
  return GROWTH_LIST;
}

function getBarHeight(v: number): number {
  return v / 10;
}

function getElemColor(e: string): string {
  if (e === '光') {
    return '#FFC107';
  }
  if (e === '雷') {
    return '#5C6BC0';
  }
  if (e === '暗') {
    return '#7E57C2';
  }
  if (e === '火') {
    return '#EF5350';
  }
  if (e === '星') {
    return '#26C6DA';
  }
  if (e === '木') {
    return '#66BB6A';
  }
  if (e === '冰') {
    return '#4FC3F7';
  }
  if (e === '风') {
    return '#AED581';
  }
  if (e === '水') {
    return '#42A5F5';
  }
  if (e === '土') {
    return '#A1887F';
  }
  return '#9E9E9E';
}

function getRareColor(r: string): string {
  if (r === '传说') {
    return '#FF8F00';
  }
  if (r === '史诗') {
    return '#7E57C2';
  }
  if (r === '稀有') {
    return '#29B6F6';
  }
  return '#78909C';
}

function getDiffColor(d: string): string {
  if (d === '简单') {
    return '#43A047';
  }
  if (d === '普通') {
    return '#FF8F00';
  }
  if (d === '困难') {
    return '#C62828';
  }
  return '#4A148C';
}

function getTaskColor(t: string): string {
  for (let i = 0; i < QUEST_TYPE.length; i++) {
    if (QUEST_TYPE[i].name === t) {
      return QUEST_TYPE[i].color;
    }
  }
  return '#9E9E9E';
}

function getQuickEntries(): string[] {
  let arr: string[] = ['秘境探索', '精灵培养', '每日任务', '唤灵召唤'];
  return arr;
}

function getQuickIcon(i: number): string {
  if (i === 0) {
    return '🗺️';
  }
  if (i === 1) {
    return '🪄';
  }
  if (i === 2) {
    return '📜';
  }
  return '🎴';
}

function getPayLevels(): number[] {
  let arr: number[] = [6, 30, 68, 128, 328, 648];
  return arr;
}

function getPayGifts(): string[] {
  let arr: string[] = ['灵晶×60', '灵晶×330', '灵晶×780', '灵晶×1580', '灵晶×3880', '灵晶×7880'];
  return arr;
}

function getLevelUpOptions(): string[] {
  let arr: string[] = ['灵晶培养', '经验药水', '星辉石', '圣物祝福'];
  return arr;
}

function getElemOptions(): string[] {
  let arr: string[] = ['光', '雷', '暗', '火', '星', '木'];
  return arr;
}

function getRareOptions(): string[] {
  let arr: string[] = ['普通', '稀有', '史诗', '传说'];
  return arr;
}

function getStatusOptions(): string[] {
  let arr: string[] = ['出战', '休息', '培养中'];
  return arr;
}

function getSlotList(): string[] {
  let arr: string[] = ['装备位·1', '装备位·2', '装备位·3'];
  return arr;
}

function getCodexFilter(): string[] {
  let arr: string[] = ['全部', '传说', '史诗', '稀有', '普通'];
  return arr;
}

function getTabIcon(label: string): string {
  if (label === '首页') {
    return '🏠';
  }
  if (label === '精灵') {
    return '🧚';
  }
  if (label === '秘境') {
    return '🗺️';
  }
  if (label === '任务') {
    return '📜';
  }
  if (label === '商店') {
    return '🛒';
  }
  if (label === '图鉴') {
    return '📖';
  }
  return '👤';
}

// ============ 首页 ============
@Component
struct HomeTab {
  onOpenSpirit: (id: number) => void = () => {
  }
  onOpenRealm: (id: number) => void = () => {
  }
  onOpenQuest: (id: number) => void = () => {
  }
  onToast: (msg: string) => void = () => {
  }

  build() {
    Column() {
      Scroll() {
        Column() {
          // 深绿渐变横幅(森林秘境风)
          Column() {
            Row() {
              Text('🌿')
                .fontSize(26)
              Text('')
                .layoutWeight(1)
              Text('Lv.42 · 幻灵师')
                .fontSize(11)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFC107')
            }
            .width('100%')
            Text('幻灵秘境')
              .fontSize(24)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .width('100%')
              .margin({ top: 10 })
            Text('员工精灵养成馆 · 与灵相伴')
              .fontSize(12)
              .fontColor('#C8E6C9')
              .width('100%')
              .margin({ top: 6 })
            Row() {
              Text('🪙 灵晶 12860')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .backgroundColor('rgba(255,255,255,0.15)')
                .borderRadius(12)
              Text('')
                .layoutWeight(1)
              Text('✨ 去唤灵')
                .fontSize(11)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1B5E20')
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .backgroundColor('#FFC107')
                .borderRadius(12)
                .onClick(() => {
                  this.onToast('唤灵召唤(每日 1 次免费)');
                })
            }
            .width('100%')
            .margin({ top: 14 })
          }
          .width('100%')
          .padding({ left: 18, right: 18, top: 18, bottom: 18 })
          .linearGradient({ angle: 135, colors: [['#0E3B2E', 0], ['#1B5E20', 0.6], ['#2E7D32', 1]] })
          .borderRadius(16)
          .margin({ left: 16, right: 16, top: 12 })
          .shadow({ radius: 10, color: 'rgba(14,59,46,0.35)', offsetY: 5 })

          // 宫格
          Row() {
            ForEach(getQuickEntries(), (qe: string, qi: number) => {
              Column() {
                Text(getQuickIcon(qi))
                  .fontSize(22)
                Text(qe)
                  .fontSize(10)
                  .fontColor('#424242')
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .padding({ top: 12, bottom: 12 })
              .backgroundColor('#F1F8E9')
              .borderRadius(12)
              .onClick(() => {
                this.onToast(qe + '入口');
              })
      

  findAch(id: number): AchItem | null {
    for (let i = 0; i < ACH_LIST.length; i++) {
      if (ACH_LIST[i].id === id) {
        return ACH_LIST[i];
      }
    }
    return null;
  }
}


在这里插入图片描述

在视觉设计层面,应用采用了一套以森林绿(#1B5E20、#2E7D32、#0E3B2E)为主色调、琥珀金(#FFC107、#FF8F00)为强调色、紫色(#7E57C2、#4A148C)为进化色的配色体系。这套配色通过linearGradient渐变、shadow阴影、borderRadius圆角等样式属性的组合,营造了统一的"森林游戏风"视觉氛围。

从技术深度来看,本应用充分展示了ArkTS声明式UI的以下核心能力:通过ForEach的键值生成器实现高效的列表差量更新;通过@State装饰器实现响应式数据绑定;通过@Builder装饰器实现UI片段的模块化封装;通过Scroll组件实现内容滚动;通过条件渲染实现页面路由和弹窗显隐;通过linearGradient实现渐变背景;通过transition实现过渡动画。这些技术的组合运用,使得一个包含七个功能页面和十六种弹窗交互的复杂应用,能够以纯声明式代码实现,无需任何命令式DOM操作。

Logo

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

更多推荐