鸿蒙ArkTS实战:幻灵秘境精灵养成系统的架构与实现深度解析
在鸿蒙(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组件处理内容滚动;使用linearGradient、shadow、borderRadius等样式属性实现精致的视觉效果。

整个应用的状态管理采用集中式模式。主组件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是精灵的唯一标识符,用于在列表渲染和查找检索中作为主键。name和img分别存储精灵的显示名称和Emoji图标——在本应用中,图片资源统一使用Emoji字符代替,这是一种轻量化的资源管理策略,避免了外部图片资源的加载开销。elem字段表示元素属性(光、雷、暗、火等),rare字段表示稀有度等级(传说、史诗、稀有、普通),这两个字段将用于后续的颜色映射逻辑。
RealmItem接口定义了秘境探索区域的数据模型。diff字段存储难度等级,power表示推荐的战力门槛,reward是完成探索后的奖励描述。desc字段提供秘境的背景描述文本,用于在详情弹窗中展示沉浸式的世界观内容。
QuestItem接口定义了任务条目的数据结构。特别值得关注的是progress和total这一对字段,它们构成了任务进度的核心数据模型——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接口中price和oldPrice的双价格设计是一个值得注意的建模决策。通过同时存储现价和原价,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接口定义了精灵培养选项的数据模型,cost和add分别以字符串形式存储消耗和收益描述,这种设计使得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是应用的核心数据源,包含了十二只精灵的完整信息。每只精灵都有独特的元素属性、稀有度、等级和战力数值。值得注意的是,intro和tags字段存储了精灵的背景故事和技能标签,这些文本数据将在精灵详情弹窗中展示,为用户提供沉浸式的阅读体验。
HOME_SPIRITS是首页横滑展示的精选精灵子集,从SPIRIT_LIST中按特定策略筛选了八只高稀有度精灵。这个数组的特殊之处在于intro和tags字段被置为空字符串——因为首页卡片仅需展示精灵的名称、图标、元素、等级和稀有度,不需要完整的介绍信息,空字符串的设计避免了首页场景下不必要的数据传输和渲染开销。
从数据组织策略来看,这种"全量数据+精选子集"的设计模式在游戏应用中非常常见。全量数据服务于详情场景(需要展示完整信息),精选子集服务于概览场景(仅需展示摘要信息)。两者共享同一套接口定义但使用不同的数据填充策略,实现了数据复用与场景适配的平衡。
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定义了十项任务,涵盖日常、周常和成就三种类型。每项任务的progress和total字段构成了进度模型。例如"收集图鉴 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;
}

getSpiritRows和getSpiritRows2是一对配对使用的数据拆分函数,它们的目的是将一维的精灵列表拆分为两个子数组,分别用于双列网格布局的左列和右列。getSpiritRows从索引0开始,步长为2,提取偶数位置(第1、3、5…个)的精灵;getSpiritRows2从索引1开始,步长为2,提取奇数位置(第2、4、6…个)的精灵。
这种拆分策略在ArkTS的声明式UI中是必要的。因为ArkTS的ForEach组件本质上是线性渲染的,它不像CSS Grid或Flutter的GridView那样能自动将一维数据排列为多列网格。要实现双列瀑布流布局,开发者需要手动将数据拆分为多个一维数组,然后在每个Column容器中分别使用ForEach渲染。这种模式虽然略显繁琐,但赋予了开发者对布局的完全控制力。
值得注意的是,同样的拆分模式被重复应用于精灵、秘境、任务、商品、图鉴、收藏、成就共七组数据,每组都根据其网格列数定义了相应数量的拆分函数。例如图鉴页使用三列布局,因此定义了
getCodexRows、getCodexRows2、getCodexRows3三个函数,分别从索引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;
}
getQuickEntries和getQuickIcon是配对使用的函数,前者返回快捷入口的名称数组,后者根据索引返回对应的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 '👤';
}
getPayLevels和getPayGifts返回充值档位和对应奖励的数组。六个充值档位(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装饰器用于需要特殊处理的复杂组件)。组件定义了四个回调函数类型的属性:onOpenSpirit、onOpenRealm、onOpenQuest和onToast。这些回调函数是子组件向父组件通信的唯一通道。
每个回调属性都有一个默认的空函数实现() => {},这是一种防御性编程策略——如果父组件没有传入某个回调,子组件调用时不会报错,而是静默忽略。这种设计使得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函数动态着色,形成了视觉上的难度区分。
六、精灵页组件: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中,通过组合
Column、Row、ForEach和样式属性,开发者可以用纯声明式代码构建数据可视化组件。这种方式的优点是零依赖、高性能、完全可控,缺点是需要手动处理坐标计算和动画效果。对于简单的柱状图、进度条等可视化需求,纯声明式实现是一个轻量而高效的选择。
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方法。卡片内部嵌套了多层Row和Column,形成了一个信息密集的卡片布局。最顶部是图标行,使用嵌套的三元表达式根据难度选择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 })
活跃度横幅使用深绿渐变背景,内部包含标题行和进度条。进度条是一个只含空Text的Row容器,其.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.progress或qs.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遍历对应的数据拆分函数返回的子数组。getCodexRows、getCodexRows2、getCodexRows3分别从索引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触发弹窗显示。
这种"查找-赋值-触发"的三步模式在整个应用中被一致地执行。它的设计优势在于将数据检索和状态变更集中在主组件中处理,子组件只需关心"发生了什么事件",而不用关心"如何处理事件",实现了关注点分离。
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!.img、this.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组件接收字符串参数,而fLevel是number类型,必须通过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操作。
更多推荐




所有评论(0)