鸿蒙 ArkTS 实战解析:云端牧场全功能模拟经营应用架构与实现
鸿蒙(HarmonyOS)的声明式 UI 框架 ArkUI,以 ArkTS 为开发语言,提供了一套高度组件化、状态驱动的界面构建范式。
@Component、@State、@Builder等装饰器构成了声明式开发的核心骨架,配合Column、Row、Stack、Scroll、ForEach等布局与渲染能力,开发者可以用极少的模板代码实现复杂的交互式应用。本文将以一个完整的多模块模拟经营应用为案例,逐段拆解其数据建模、状态管理、组件拆分、弹窗体系与六页面导航架构的设计思路与实现细节。
一、整体架构与技术栈概览
本案例是一款名为"云端牧场"的模拟经营类应用,涵盖牧场管理、动物领养、作物种植、集市交易、任务系统、个人中心六大功能模块。应用采用单页面多 Tab 架构,底部导航栏切换六个一级页面,每个页面承载独立的业务逻辑与视觉风格。
在技术选型上,该应用完全基于鸿蒙 ArkTS 声明式 UI 框架构建。数据层使用 TypeScript 接口(interface)定义强类型数据模型,配合常量数组作为 Mock 数据源。UI 层通过 @Component 装饰器将页面拆分为六个独立子组件,主入口 Index 组件负责 Tab 路由和全局弹窗状态管理。所有弹窗均采用 @Builder 函数封装,通过 @State 布尔开关控制显隐,实现了一套轻量级的模态对话框体系。

从架构图可以看出,整个应用的数据流是单向的:用户在子组件页面中触发交互回调,回调将选中数据通过闭包传递给主入口 Index,Index 更新对应的 @State 选中对象和弹窗开关状态,进而触发 @Builder 弹窗的渲染。这种"子组件负责展示、父组件负责状态"的分层模式,是鸿蒙声明式 UI 中非常典型的状态管理范式。
在鸿蒙 ArkUI 中,状态管理遵循"单向数据流"原则:父组件通过
@Prop或参数向子组件传递数据,子组件通过回调函数将事件冒泡给父组件。这种模式避免了状态在多组件间的无序共享,使得数据流向始终可追溯。本案例将所有弹窗状态集中在Index组件中管理,正是这一原则的最佳实践。
二、数据模型层:接口定义与类型安全
2.1 核心业务实体接口
应用首先定义了一组 TypeScript 接口来描述业务实体。这些接口覆盖了动物、作物、商品、任务、装饰、饲料、等级、好友、技能和快捷入口等十大业务领域。
interface Animal {
id: number;
name: string;
icon: string;
breed: string;
level: number;
mood: number;
product: string;
value: number;
feed: string;
desc: string;
}
interface Crop {
id: number;
name: string;
icon: string;
days: number;
yieldNum: number;
price: number;
season: string;
desc: string;
}
interface MarketGoods {
id: number;
name: string;
icon: string;
price: number;
tag: string;
stock: number;
seller: string;
hot: number;
}

Animal 接口是动物领域的核心数据结构。其中 id 作为唯一标识符,name 是展示名称,icon 使用 Emoji 字符串作为图标(这种做法免去了图片资源的加载开销),breed 表示品种,level 和 mood 分别是等级和心情值,product 描述每日产出,value 是出售价值,feed 是最爱饲料,desc 是描述文案。每一个字段都服务于具体的 UI 展示需求。
Crop 接口面向种植模块,days 表示生长周期天数,yieldNum 是单次收获数量,price 是种子价格,season 标注适合的种植季节。MarketGoods 接口面向集市交易,tag 是商品标签(如"热销"“新品”“稀有”),stock 是库存量,hot 是热销指数,seller 是卖家信息。
鸿蒙 ArkTS 保留了 TypeScript 的
interface类型系统,支持在编译期进行类型检查。这意味着如果开发者在代码中拼错了字段名或传入了错误类型的值,编译器会在构建阶段就报错,而不是等到运行时才暴露问题。这在大型应用开发中显著降低了调试成本。
2.2 任务、装饰与饲料接口
interface RanchTask {
id: number;
name: string;
icon: string;
reward: number;
type: string;
progress: number;
total: number;
desc: string;
}
interface Decor {
id: number;
name: string;
icon: string;
price: number;
type: string;
level: number;
desc: string;
}
interface Feed {
id: number;
name: string;
icon: string;
price: number;
effect: string;
moodAdd: number;
desc: string;
}

RanchTask 接口描述任务系统的数据结构,type 字段区分"日常"“周常”“活动"三种任务类型,progress 和 total 组成进度对,用于驱动进度条渲染。Decor 接口描述装饰品信息,type 字段将装饰品分为"围栏”“景观”“设施”“照明”"建筑"等类别。Feed 接口定义饲料数据,moodAdd 字段记录该饲料能提升的心情值,是喂养交互中计算心情增益的核心依据。
2.3 等级、好友、技能与快捷入口接口
interface RanchLevel {
id: number;
name: string;
icon: string;
exp: number;
unlock: string;
bonus: string;
}
interface RanchFriend {
id: number;
name: string;
icon: string;
level: number;
crops: string;
visitCount: number;
online: boolean;
}
interface RanchSkill {
id: number;
name: string;
icon: string;
level: number;
maxLevel: number;
desc: string;
}
interface QuickIcon {
id: number;
name: string;
icon: string;
color: string;
}

RanchLevel 接口定义牧场等级体系,exp 是所需经验值,unlock 描述该等级解锁的内容,bonus 是等级福利文案。RanchFriend 接口描述好友信息,online 是布尔值表示在线状态,visitCount 记录拜访次数。RanchSkill 接口定义技能系统,level 和 maxLevel 组成技能等级对。QuickIcon 接口用于首页快捷入口,color 字段为每个入口指定主题色。
这些接口共同构成了应用的类型层。值得注意的是,鸿蒙 ArkTS 的类型系统在编译期会被擦除,不会带来运行时开销。接口定义的另一个好处是为 IDE 提供了智能提示,开发者在使用 this.selAnimal!.name 等表达式时可以获得自动补全。
三、Mock 数据源:常量数组的定义与组织
3.1 动物与作物数据
应用在接口定义之后,立即声明了大量常量数组作为数据源。这些数组使用 const 修饰,在全局作用域中定义,供所有组件直接引用。
const ANIMALS: Animal[] = [
{ id: 1, name: '奶糖奶牛', icon: '🐄', breed: '荷斯坦奶牛', level: 12, mood: 92, product: '鲜牛奶 ×2', value: 680, feed: '牧草', desc: '云牧场的明星奶牛,产奶量全场第一,性格温顺。' },
{ id: 2, name: '绒球绵羊', icon: '🐑', breed: '云朵细毛羊', level: 10, mood: 85, product: '羊毛 ×3', value: 520, feed: '干草', desc: '毛发蓬松如云,剪下的羊毛是集市抢手货。' },
{ id: 3, name: '咯咯鸡', icon: '🐔', breed: '彩虹蛋鸡', level: 9, mood: 78, product: '鸡蛋 ×4', value: 380, feed: '谷粒', desc: '每天清晨打鸣,能下出三种颜色的彩蛋。' },
// ... 更多动物
];
ANIMALS 数组包含 14 条动物记录,每条都严格遵循 Animal 接口的字段定义。这里使用 Emoji 作为 icon 值是一个非常巧妙的设计决策——Emoji 字符天然支持跨平台渲染,无需准备任何图片资源,极大简化了应用的资源管理。同时 Emoji 的色彩表现力强,能直观传达动物的视觉特征。
const CROPS: Crop[] = [
{ id: 1, name: '黄金麦', icon: '🌾', days: 3, yieldNum: 8, price: 120, season: '四季', desc: '牧草的主原料,麦穗金黄如阳光。' },
{ id: 2, name: '彩虹萝卜', icon: '🥕', days: 2, yieldNum: 5, price: 90, season: '秋季', desc: '拔出来时会发出彩虹色的光。' },
// ... 更多作物
];
CROPS 数组定义了 12 种作物,season 字段涵盖"四季"“春季”“夏季”“秋季”"冬季"五个值,用于后续种植页面中的季节筛选展示。days 字段从 2 天到 8 天不等,配合 yieldNum 和 price 可以计算出每种作物的收益效率,为玩家的种植策略提供数据支撑。
在实际项目开发中,这类数据通常来自后端 API 返回的 JSON。但本案例采用前端硬编码常量的方式,一方面是为了演示纯前端 UI 实现能力,另一方面也让整个应用可以在无网络环境下独立运行。这种"数据即代码"的方式在原型开发和演示场景中非常实用。
3.2 商品、任务、装饰与饲料数据
const MARKET: MarketGoods[] = [
{ id: 1, name: '鲜牛奶 1L', icon: '🥛', price: 28, tag: '热销', stock: 120, seller: '奶糖牧场', hot: 98 },
{ id: 2, name: '羊毛毯', icon: '🧣', price: 320, tag: '手工', stock: 30, seller: '绒球牧场', hot: 76 },
// ... 更多商品
];
const TASKS: RanchTask[] = [
{ id: 1, name: '清晨挤奶', icon: '🥛', reward: 200, type: '日常', progress: 1, total: 2, desc: '给奶牛们挤两次奶,凑满两桶鲜奶。' },
// ... 更多任务
];

MARKET 数组定义了 10 件集市商品,tag 字段的设计尤其值得注意——它既用于商品卡片上的标签徽章展示,又在集市页面的分类筛选 chips 中作为分类维度复用。这种一字段多用途的设计减少了数据冗余。hot 字段是一个 0-100 的数值,驱动了热销榜单的排序逻辑。
TASKS 数组包含 10 条任务记录,type 字段将任务分为"日常"“周常”"活动"三类,任务页面据此将任务分组展示。progress 和 total 的比值用于渲染进度条和计算完成百分比,当 progress >= total 时任务进入"可领奖"状态。
3.3 等级、好友、技能与快捷入口数据
const RANCH_LEVELS: RanchLevel[] = [
{ id: 1, name: '小草地', icon: '🌱', exp: 0, unlock: '初始牧场', bonus: '解锁 2 块田地' },
{ id: 2, name: '绿草坡', icon: '🌿', exp: 500, unlock: '等级 2', bonus: '解锁动物领养' },
// ... 更多等级
];
const RANCH_SKILLS: RanchSkill[] = [
{ id: 1, name: '喂养大师', icon: '🍎', level: 3, maxLevel: 5, desc: '饲料效果提升 15%' },
// ... 更多技能
];
const QUICK_ICONS: QuickIcon[] = [
{ id: 1, name: '签到', icon: '📅', color: '#FFD54F' },
{ id: 2, name: '领养', icon: '🐣', color: '#FFB74D' },
// ... 更多快捷入口
];
RANCH_LEVELS 数组定义了 8 个牧场等级,经验值从 0 递增到 20000,每个等级解锁不同的游戏内容。RANCH_SKILLS 数组定义了 8 项技能,每项技能有当前等级和最大等级,用于驱动技能升级弹窗中的星级展示和进度条。QUICK_ICONS 数组定义了 8 个首页快捷入口,每个入口都有独立的主题色。
四、工具函数:数据分列与格式化
4.1 双列分列函数群
应用在 UI 展示中大量使用双列网格布局,因此定义了一组分列函数将数组拆分为奇数列和偶数列。
function getAnimalRows(): Animal[] {
let rows: Animal[] = [];
for (let i = 0; i < ANIMALS.length; i += 2) {
rows.push(ANIMALS[i]);
}
return rows;
}
function getAnimalRows2(): Animal[] {
let rows: Animal[] = [];
for (let i = 1; i < ANIMALS.length; i += 2) {
rows.push(ANIMALS[i]);
}
return rows;
}
getAnimalRows 函数从索引 0 开始,步长为 2,提取数组中的奇数位元素(第 0、2、4…项)。getAnimalRows2 函数从索引 1 开始,步长为 2,提取偶数位元素(第 1、3、5…项)。这两个函数的返回值分别用于双列布局的左列和右列的 ForEach 渲染。
这种分列方式在鸿蒙 ArkUI 中是一种常见的网格布局替代方案。虽然 ArkUI 提供了 Grid 组件,但在某些场景下使用 Row 嵌套两个 ForEach 更灵活——可以精确控制每列的间距、背景色和布局权重,也更容易实现交错排列等特殊效果。
值得注意的是,这套分列函数为每种数据类型都定义了一对(如
getCropRows/getCropRows2、getMarketRows/getMarketRows2等),共 10 对 20 个函数。从工程角度看,这里存在优化空间——可以抽象为一个泛型分列函数splitArray<T>(arr: T[], offset: number): T[],通过参数控制起始偏移。但当前实现的优势在于类型明确、可读性高,且编译期类型安全。
4.2 截取函数与格式化函数
function getTopAnimals(): Animal[] {
let rows: Animal[] = [];
for (let i = 0; i < 6 && i < ANIMALS.length; i++) {
rows.push(ANIMALS[i]);
}
return rows;
}
function getBarHeight(val: number, max: number): string {
let p = max > 0 ? (val / max) * 80 : 0;
return p.toFixed(0) + 'vp';
}
function getPercent(val: number, total: number): string {
let p = total > 0 ? (val / total) * 100 : 0;
return p.toFixed(0) + '%';
}
function formatMoney(n: number): string {
let s = n.toString();
if (n >= 10000) {
s = (n / 10000).toFixed(1) + 'w';
}
return s;
}

getTopAnimals 截取前 6 条动物数据用于首页横滑展示。getBarHeight 将数值映射为 0-80vp 的高度字符串,用于柱状图高度计算。getPercent 计算百分比并格式化为整数字符串。formatMoney 实现了金额的万级缩写——超过 10000 的数值会被转换为 1.3w 这样的紧凑形式,在小尺寸 UI 中节省显示空间。
4.3 语义化映射函数
function getMoodText(m: number): string {
if (m >= 90) {
return '超开心';
} else if (m >= 70) {
return '开心';
} else if (m >= 50) {
return '一般';
}
return '低落';
}
function getMoodColor(m: number): string {
if (m >= 90) {
return '#E53935';
} else if (m >= 70) {
return '#FB8C00';
} else if (m >= 50) {
return '#FDD835';
}
return '#90A4AE';
}
function getTaskTypeColor(t: string): string {
if (t === '日常') {
return '#7CB342';
} else if (t === '周常') {
return '#FB8C00';
}
return '#E53935';
}
getMoodText 将 0-100 的心情数值映射为四个语义等级文字。getMoodColor 为每个心情区间返回对应的颜色值——90 分以上用草莓红表示"超开心",70 分以上用橙色表示"开心",50 分以上用黄色表示"一般",50 分以下用灰色表示"低落"。getTaskTypeColor 为任务类型分配主题色:日常任务用草甸绿、周常任务用橙色、活动任务用红色。
这些语义化映射函数将数值到展示文案和颜色的转换逻辑集中管理,避免了在 UI 代码中散落大量的 if-else 判断。当需要调整心情等级的阈值或颜色方案时,只需修改一处即可全局生效。

五、主入口组件:状态管理与 Tab 路由
5.1 状态声明体系
主入口组件 Index 是整个应用的状态中枢,通过 @Entry 和 @Component 装饰器标记为应用入口。
@Entry
@Component
struct Index {
@State tabIndex: number = 0;
@State coin: number = 12880;
@State exp: number = 3860;
@State showToast: boolean = false;
@State toast: string = '';
// 弹框开关
@State showAnimal: boolean = false;
@State showAdopt: boolean = false;
@State showFeed: boolean = false;
@State showHarvest: boolean = false;
@State showPlant: boolean = false;
@State showSell: boolean = false;
@State showBuy: boolean = false;
@State showTask: boolean = false;
@State showReward: boolean = false;
@State showDecor: boolean = false;
@State showLevel: boolean = false;
@State showFriend: boolean = false;
@State showWeather: boolean = false;
@State showSkill: boolean = false;
@State showCraft: boolean = false;
@State showDelete: boolean = false;
@State 是鸿蒙 ArkUI 中最核心的状态装饰器。被 @State 修饰的变量一旦发生变化,框架会自动触发依赖该变量的 UI 组件重新渲染。这里声明了三类状态变量:应用级数据(tabIndex、coin、exp)、Toast 状态(showToast、toast)和弹窗开关(16 个布尔值)。
tabIndex 初始值为 0,对应"牧场"首页。coin 初始值 12880 代表玩家拥有的金币数量,exp 初始值 3860 代表当前经验值。这两个值会在后续交互中被传递给子组件展示。16 个弹窗开关布尔值分别对应 16 个 @Builder 弹窗函数,通过条件渲染实现弹窗的显示与隐藏。
@State装饰器的工作原理是劫持变量的 setter。当开发者执行this.showAnimal = true时,框架的响应式系统检测到值变化,会重新执行build()方法中依赖this.showAnimal的条件分支,从而触发弹窗的渲染。这一过程是自动的、声明式的,开发者无需手动调用setState或invalidate。
5.2 选中对象与表单字段状态
// 选中对象
@State selAnimal: Animal | null = null;
@State selAdopt: Animal | null = null;
@State selFeed: Animal | null = null;
@State selHarvest: Crop | null = null;
@State selPlant: Crop | null = null;
@State selSell: MarketGoods | null = null;
@State selBuy: MarketGoods | null = null;
@State selTask: RanchTask | null = null;
@State selReward: RanchTask | null = null;
@State selDecor: Decor | null = null;
@State selLevel: RanchLevel | null = null;
@State selFriend: RanchFriend | null = null;
@State selSkill: RanchSkill | null = null;
@State selDelete: Animal | null = null;
// 表单字段
@State fQty: number = 1;
@State fPrice: number = 28;
@State fDays: number = 3;
@State fFeedId: number = 1;
@State fPlot: number = 1;
@State fMsg: string = '';
// 田地状态 0 空地 1 生长中 2 可收获
@State plotStates: number[] = [0, 1, 1, 2, 0, 1, 2, 0, 1];
@State cropNames: string[] = ['黄金麦', '彩虹萝卜', '云朵棉', '蜜瓜', '紫晶葡萄', '星星番茄', '甜玉米', '月光南瓜', '香脆苹果'];
选中对象状态使用联合类型 Animal | null 声明,初始值为 null。这种设计允许弹窗在未选中任何对象时保持关闭状态,只有当用户点击某个动物/作物/商品后,对应的 sel 变量才会被赋值,同时弹窗开关被置为 true。弹窗内的 @Builder 函数通过非空断言 ! 访问选中对象的字段(如 this.selAnimal!.name),因为弹窗只在 selAnimal !== null 时才渲染,所以非空断言是安全的。
表单字段状态(fQty、fPrice、fDays、fFeedId、fPlot)用于弹窗内的交互控件——数量步进器、价格输入、天数选择、饲料选择、地块选择。plotStates 数组记录 9 块田地的状态(0=空地、1=生长中、2=可收获),cropNames 数组记录每块田地种植的作物名称,这两个数组通过 @Prop 传递给 CropTab 子组件。
5.3 Tab 配置与辅助方法
private tabs: string[] = ['牧场', '动物', '种植', '集市', '任务', '我的'];
private tabIcons: string[] = ['🏡', '🐄', '🌾', '🏪', '📋', '👤'];
showTip(msg: string): void {
this.toast = msg;
this.showToast = true;
setTimeout(() => {
this.showToast = false;
}, 1800);
}
setPlot(idx: number): void {
let arr: number[] = [];
for (let i = 0; i < this.plotStates.length; i++) {
arr.push(i === idx ? 2 : this.plotStates[i]);
}
this.plotStates = arr;
}
tabs 和 tabIcons 是两个私有数组,分别存储 Tab 标签文字和图标 Emoji。这里使用 private 修饰符而非 @State,因为这两个数组在应用生命周期内不会变化,不需要触发响应式更新。
showTip 方法是全局 Toast 提示的触发入口。它先设置提示文案和显示开关为 true,然后通过 setTimeout 在 1800 毫秒后自动关闭。这种"自动消失"的 Toast 交互模式在移动端非常常见,鸿蒙的 @State 响应式系统会确保 showToast 状态变化后 toastBox 组件的渲染和移除都是平滑的。
setPlot 方法用于更新田地状态——它遍历 plotStates 数组,将指定索引的田地状态设置为 2(可收获),其余保持不变。这里没有使用原地修改(this.plotStates[idx] = 2),而是构建了一个全新数组再赋值。这是因为 @State 对数组的变更检测依赖于引用变化,直接修改数组元素不会触发 UI 更新,必须创建新数组引用才能正确触发响应式刷新。
这是鸿蒙 ArkUI 响应式编程中一个容易踩坑的点:
@State对基本类型(number、string、boolean)的变更能自动感知,但对数组和对象的"内容修改"需要通过"引用替换"来触发。setPlot方法的实现方式——构建新数组再整体赋值——正是解决这一问题的标准做法。
六、build 方法:Stack 布局与条件路由
6.1 Stack 容器与主内容区
build() {
Stack() {
Column() {
if (this.tabIndex === 0) {
RanchTab({
coin: this.coin,
exp: this.exp,
onOpenAnimal: (a: Animal) => {
this.selAnimal = a;
this.showAnimal = true;
},
onOpenTask: (t: RanchTask) => {
this.selTask = t;
this.showTask = true;
},
onAdopt: () => {
this.selAdopt = ANIMALS[0];
this.showAdopt = true;
},
onOpenMarket: () => {
this.tabIndex = 3;
},
onWeather: () => {
this.showWeather = true;
},
onCraft: () => {
this.fQty = 1;
this.showCraft = true;
},
onFeed: (a: Animal) => {
this.selFeed = a;
this.fFeedId = 1;
this.showFeed = true;
}
})
} else if (this.tabIndex === 1) {
AnimalTab({
onOpenAnimal: (a: Animal) => {
this.selAnimal = a;
this.showAnimal = true;
},
onFeed: (a: Animal) => {
this.selFeed = a;
this.fFeedId = 1;
this.showFeed = true;
},
onDelete: (a: Animal) => {
this.selDelete = a;
this.showDelete = true;
},
onAdopt: () => {
this.selAdopt = ANIMALS[0];
this.showAdopt = true;
}
})
}
build() 方法是 @Component 组件的 UI 描述入口,所有可视内容都在这里声明。最外层使用 Stack 容器——Stack 是鸿蒙 ArkUI 中的层叠布局容器,子元素按声明顺序堆叠,后声明的元素覆盖在先声明的元素之上。这里用 Stack 是为了实现弹窗覆盖在主内容之上的层叠效果。
Stack 的第一个子元素是 Column,它承载主内容区和底部 Tab 栏。Column 内部使用 if-else if 链根据 this.tabIndex 的值条件渲染不同的子组件。当 tabIndex === 0 时渲染 RanchTab,tabIndex === 1 时渲染 AnimalTab,以此类推。这种条件渲染是鸿蒙 ArkUI 中实现页面路由的轻量级方案——无需引入路由框架,仅靠状态变量切换即可。
每个子组件在实例化时通过参数对象传入回调和数据。以 RanchTab 为例,coin 和 exp 是数据参数,而 onOpenAnimal、onOpenTask、onAdopt 等是以箭头函数形式传入的回调。这些回调的函数体内修改的是 Index 组件的 @State 变量——这种"子组件触发回调、父组件更新状态"的模式是 ArkUI 中父子组件通信的标准范式。
6.2 底部 Tab 栏与 ForEach 渲染
// ========== 底部 Tab ==========
Row() {
ForEach(this.tabs, (label: string, idx: number) => {
this.bottomTabItem(this.tabIcons[idx], label, idx)
}, (label: string, idx: number) => label + idx)
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
.shadow({ radius: 8, color: '#1A000000', offsetY: -2 })
}
.width('100%')
.height('100%')
底部 Tab 栏使用 Row 容器水平排列 6 个 Tab 项。每个 Tab 项通过 ForEach 遍历 this.tabs 数组生成。ForEach 是鸿蒙 ArkUI 中的列表渲染组件,接受三个参数:数据源数组、子项生成函数和键值生成函数。
ForEach 的第三个参数是键值生成函数 (label: string, idx: number) => label + idx,它为每个列表项生成唯一标识符。这个键值在框架的 Diff 算法中至关重要——当数据源变化时,框架通过比对键值来决定哪些项需要新增、删除或更新,从而最小化 DOM 操作。这里使用 label + idx 拼接字符串作为键值,保证了每个 Tab 项的全局唯一性。
Row 容器设置了 shadow 属性,通过 offsetY: -2 实现阴影向上投射的效果,模拟 Tab 栏浮在内容之上的视觉层次。.height(56) 将 Tab 栏高度固定为 56vp(virtual pixel,鸿蒙的虚拟像素单位),这是移动端底部导航栏的常见高度。
6.3 弹窗挂载层
// ========== 弹窗挂载 ==========
if (this.showAnimal && this.selAnimal !== null) {
this.modalOverlay(() => { this.showAnimal = false })
this.animalModal()
}
if (this.showAdopt && this.selAdopt !== null) {
this.modalOverlay(() => { this.showAdopt = false })
this.adoptModal()
}
if (this.showFeed && this.selFeed !== null) {
this.modalOverlay(() => { this.showFeed = false })
this.feedModal()
}
弹窗挂载区位于 Stack 的第二层,所有弹窗都在这里通过条件渲染挂载。每个弹窗由两部分组成:modalOverlay(半透明遮罩)和具体的弹窗内容(如 animalModal)。条件表达式同时检查弹窗开关和选中对象是否非空(如 this.showAnimal && this.selAnimal !== null),这种双重检查确保了弹窗内容在访问 this.selAnimal!.name 时的安全性。
modalOverlay 接受一个关闭回调 () => { this.showAnimal = false },当用户点击遮罩区域时触发,实现"点击遮罩关闭弹窗"的交互。弹窗内容组件通过 @Builder 函数封装,后文将逐一分析。
Stack层叠布局是弹窗体系的技术基石。由于Stack的子元素后声明者覆盖先声明者,弹窗组件(在Column之后声明)天然位于主内容之上。而遮罩层在弹窗内容之前声明,使得遮罩位于弹窗内容之下、主内容之上。这种声明顺序精确控制了三层视觉层次:主内容(最底)→ 遮罩(中间)→ 弹窗内容(最顶)。
七、@Builder 弹窗体系:16 种模态对话框
7.1 动物档案弹窗:居中卡片风格
@Builder
animalModal() {
Column() {
Column() {
Row() {
Text(this.selAnimal!.icon)
.fontSize(56)
Column() {
Text(this.selAnimal!.name)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
Text(this.selAnimal!.breed + ' · Lv.' + this.selAnimal!.level)
.fontSize(13)
.fontColor('#7CB342')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 14 })
Text('ID ' + this.selAnimal!.id)
.fontSize(11)
.fontColor('#90A4AE')
}
.width('100%')
@Builder 是鸿蒙 ArkUI 中的构建器装饰器,用于将 UI 片段封装为可复用的函数。animalModal 是第一个弹窗——动物档案弹窗,采用居中卡片风格。外层 Column 设置 .justifyContent(FlexAlign.Center) 实现垂直居中,内层 Column 是卡片本体,宽度设为 84%,圆角 18,白色背景。
FlexAlign 是鸿蒙 ArkUI 中的弹性布局对齐枚举。FlexAlign.Center 表示主轴居中对齐——在 Column 中主轴是垂直方向,因此效果是卡片在屏幕垂直方向居中。其他常用的值包括 FlexAlign.Start(顶部对齐)、FlexAlign.End(底部对齐)和 FlexAlign.SpaceBetween(两端对齐,首尾无间距)。
卡片头部使用 Row 水平排列动物 Emoji 图标、名称品种信息和 ID 标签。名称使用 FontWeight.Bold 加粗,主色 #2E5D1F(深草甸绿),品种信息使用辅色 #7CB342(浅草甸绿)。.layoutWeight(1) 让中间的 Column 占据剩余空间,将 ID 标签推到右侧。
7.2 心情进度条与数据展示区
Text('心情值 ' + getMoodText(this.selAnimal!.mood))
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#E53935')
.width('100%')
.textAlign(TextAlign.Start)
Row() {
Text('😊')
.fontSize(14)
Progress({ value: this.selAnimal!.mood, total: 100 })
.color('#E53935')
.backgroundColor('#FFEBEE')
.height(8)
.layoutWeight(1)
.margin({ left: 8, right: 8 })
Text(this.selAnimal!.mood + '%')
.fontSize(12)
.fontColor('#E53935')
}
.width('100%')
.margin({ top: 8 })
心情值区域展示了从数值到语义文案再到可视化的完整数据呈现链路。首先通过 getMoodText 函数将心情数值转换为"超开心""开心"等文字描述,然后使用 Progress 组件渲染线性进度条。Progress 是鸿蒙 ArkUI 内置的进度组件,接受 value(当前值)和 total(最大值)两个参数,自动计算百分比并渲染填充条。
进度条的颜色配置遵循应用主题色体系:填充色 #E53935(草莓红)表示高心情值,背景色 #FFEBEE(浅红)作为未填充部分的底色。.height(8) 将进度条高度限制为 8vp,配合 .layoutWeight(1) 让它在 Row 中占据表情图标和百分比文字之间的所有剩余空间。
Progress组件是鸿蒙 ArkUI 提供的内置原子组件,支持线性、环形和秤砣三种样式。本案例中使用的是默认的线性样式(ProgressType.Linear)。通过.color()设置填充色、.backgroundColor()设置轨道色,开发者可以快速实现与品牌主题一致的进度可视化。
7.3 底部操作按钮与过渡动画
Row() {
Row() {
Text('🍎 喂养')
.fontSize(14)
.fontColor('#FFFFFF')
.padding({ left: 20, right: 20, top: 10, bottom: 10 })
}
.backgroundColor('#7CB342')
.borderRadius(20)
.onClick(() => {
this.showAnimal = false;
this.selFeed = this.selAnimal;
this.fFeedId = 1;
this.showFeed = true;
})
Row() {
Text('关闭')
.fontSize(14)
.fontColor('#7CB342')
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
}
.backgroundColor('#E8F5E9')
.borderRadius(20)
.margin({ left: 12 })
.onClick(() => { this.showAnimal = false })
}
.margin({ top: 18 })
}
.padding(20)
.width('84%')
.backgroundColor('#FFFFFF')
.borderRadius(18)
.clip(true)
.transition(TransitionEffect.OPACITY.animation({ duration: 200 }))
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
卡片底部是两个操作按钮:"喂养"按钮使用草甸绿实心背景搭配白色文字,点击后先关闭当前动物档案弹窗(this.showAnimal = false),再将当前动物赋值给喂养选中对象(this.selFeed = this.selAnimal),最后打开喂养弹窗(this.showFeed = true)。这种"关闭一个弹窗同时打开另一个"的链式交互在应用中非常常见。
"关闭"按钮使用浅绿色背景搭配草甸绿文字,形成与"喂养"按钮的主次对比。两个按钮都设置了 .borderRadius(20) 实现圆角胶囊形状,.padding() 控制内边距使按钮有足够的点击区域。
.transition(TransitionEffect.OPACITY.animation({ duration: 200 })) 是弹窗的入场/出场动画配置。TransitionEffect.OPACITY 表示透明度过渡效果,animation({ duration: 200 }) 设置动画时长为 200 毫秒。当弹窗通过条件渲染出现时,会从透明渐变为不透明;消失时反向播放。这种 200ms 的淡入淡出动画为弹窗交互增添了流畅感。
7.4 领养弹窗:底部抽屉风格与 Scroll 滚动
@Builder
adoptModal() {
Column() {
Column() {
Row() {
Text('🐣 领养小动物')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
.layoutWeight(1)
Text('✕')
.fontSize(18)
.fontColor('#90A4AE')
.onClick(() => { this.showAdopt = false })
}
.width('100%')
Text('选择一只可爱的小动物带回家吧')
.fontSize(13)
.fontColor('#90A4AE')
.width('100%')
.margin({ top: 6 })
Scroll() {
Column() {
Row() {
ForEach(getAnimalRows(), (a: Animal) => {
Column() {
Text(a.icon)
.fontSize(40)
Text(a.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#2E5D1F')
.margin({ top: 8 })
Text(a.breed)
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 2 })
Row() {
Text('领养 ¥' + a.value)
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
}
.backgroundColor('#7CB342')
.borderRadius(10)
.margin({ top: 8 })
.onClick(() => {
this.showAdopt = false;
this.showTip('已领养 ' + a.name);
})
}
.layoutWeight(1)
.padding({ top: 14, bottom: 14 })
.backgroundColor('#F1F8E9')
.borderRadius(14)
.margin({ right: 8 })
}, (a: Animal) => 'a' + a.id)
}
领养弹窗采用底部抽屉风格,与动物档案弹窗的居中卡片风格形成对比。外层 Column 设置 .justifyContent(FlexAlign.End) 实现内容靠底部对齐,内层 Column 设置 .borderRadius({ topLeft: 22, topRight: 22 }) 只对顶部两角设置圆角,模拟从屏幕底部滑出的抽屉效果。
抽屉头部使用 Row 水平排列标题和关闭按钮(✕)。关闭按钮通过 .onClick 绑定关闭逻辑。标题下方的副标题文字使用灰色 #90A4AE 提供辅助说明。
内容区使用 Scroll 组件包裹,这是因为动物列表内容可能超出屏幕高度。Scroll 是鸿蒙 ArkUI 中的滚动容器,支持垂直和水平两种滚动方向。这里通过 .scrollable(ScrollDirection.Vertical) 指定垂直滚动,.scrollBar(BarState.Off) 隐藏滚动条以保持视觉简洁。.constraintSize({ maxHeight: '56%' }) 限制滚动区域最大高度为屏幕的 56%,确保抽屉不会占据整个屏幕。
动物卡片使用 ForEach 遍历 getAnimalRows() 返回的奇数列数据渲染。每个卡片是一个 Column,垂直排列 Emoji 图标、名称、品种和领养价格按钮。.layoutWeight(1) 让每个卡片在 Row 中等宽分布,.margin({ right: 8 }) 在卡片右侧添加间距。领养按钮点击后关闭弹窗并触发 Toast 提示。
Scroll组件的.scrollBar(BarState.Off)是移动端 UI 设计中的常见选择——隐藏滚动条可以减少视觉干扰,尤其在内容区域较小的弹窗内部。BarState枚举有三个值:Off(隐藏)、On(显示)、Auto(按需显示)。在需要保持界面简洁的场景下,Off是最佳选择。
7.5 底部抽屉的过渡动画
.padding(20)
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 22, topRight: 22 })
.transition(TransitionEffect.translate({ y: 300 }).animation({ duration: 220 }))
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.End)
}
领养弹窗的过渡动画使用了 TransitionEffect.translate({ y: 300 }),这是位移过渡效果。y: 300 表示组件初始位置在 Y 轴方向偏移 300vp(即向下平移 300vp),动画播放时组件从屏幕下方 300vp 处向上滑入到正常位置。动画时长 220 毫秒,比动物档案弹窗的 200ms 稍长,因为位移动画需要更长的视觉持续时间才能看起来自然。
这种从底部滑入的动画效果与底部抽屉的交互隐喻完美匹配——用户感知到抽屉是从屏幕底部"抽出"的,符合现实世界中抽屉的物理直觉。鸿蒙 ArkUI 的 TransitionEffect 还支持 OPACITY(透明度)、SCALE(缩放)、ROTATE(旋转)等效果,开发者可以根据弹窗风格选择最合适的过渡方式。
7.6 喂养弹窗:饲料选择与数量步进器
@Builder
feedModal() {
Column() {
Column() {
Row() {
Text('🍎 喂养 ' + this.selFeed!.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
.layoutWeight(1)
Text('✕')
.fontSize(18)
.fontColor('#90A4AE')
.onClick(() => { this.showFeed = false })
}
.width('100%')
Text('当前心情:' + getMoodText(this.selFeed!.mood) + '(' + this.selFeed!.mood + '%)')
.fontSize(13)
.fontColor('#E53935')
.width('100%')
.margin({ top: 6 })
喂养弹窗同样是底部抽屉风格。标题动态拼接了选中动物的名称,使弹窗具有上下文感知能力。标题下方的当前心情展示使用了 getMoodText 函数,将数值映射为语义化文案。
Column() {
ForEach(getFeedRows(), (f: Feed, fi: number) => {
Row() {
Row() {
Text(f.icon)
.fontSize(22)
.margin({ right: 6 })
Column() {
Text(f.name)
.fontSize(13)
.fontWeight(FontWeight.Medium)
.fontColor('#2E5D1F')
Text('心情 +' + f.moodAdd + ' · ¥' + f.price)
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
}
.layoutWeight(1)
.padding(8)
.backgroundColor(this.fFeedId === f.id ? '#DCEDC8' : '#F5F5F5')
.borderRadius(10)
.onClick(() => { this.fFeedId = f.id })
饲料选择区域使用 ForEach 遍历 getFeedRows() 返回的奇数列饲料数据。每个饲料选项是一个可点击的 Row,包含 Emoji 图标、名称和效果信息。选中状态通过背景色变化来体现——当选中某饲料时(this.fFeedId === f.id),背景色变为 #DCEDC8(浅绿色高亮),否则为 #F5F5F5(浅灰色默认)。这种通过条件表达式动态切换样式的方式是 ArkUI 声明式 UI 的核心特性之一。
每个饲料项展示了名称、心情增加值和价格三条信息。HorizontalAlign.Start 确保文字左对齐,与 Emoji 图标在视觉上形成整齐的左边缘。饲料项的点击事件将选中饲料的 ID 赋值给 this.fFeedId,触发响应式更新使选中样式切换。
Row() {
Text('本次花费')
.fontSize(14)
.fontColor('#607D8B')
Text('¥' + (this.fQty * 20).toString())
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FB8C00')
.margin({ left: 8 })
Text('')
.layoutWeight(1)
Row() {
Text('−')
.fontSize(20)
.fontColor('#7CB342')
.width(32)
.height(32)
.textAlign(TextAlign.Center)
}
.backgroundColor('#E8F5E9')
.borderRadius(16)
.onClick(() => {
if (this.fQty > 1) {
this.fQty = this.fQty - 1;
}
})
Text(this.fQty.toString())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
.width(40)
.textAlign(TextAlign.Center)
Row() {
Text('+')
.fontSize(20)
.fontColor('#FFFFFF')
.width(32)
.height(32)
.textAlign(TextAlign.Center)
}
.backgroundColor('#7CB342')
.borderRadius(16)
.onClick(() => {
if (this.fQty < 10) {
this.fQty = this.fQty + 1;
}
})
}
数量步进器是喂养弹窗中最精巧的交互组件。它由"本次花费"文字、动态计算的总价、减号按钮、数量显示和加号按钮五部分组成,使用 Row 水平排列。中间的 Text('').layoutWeight(1) 是一个弹性占位符,将左侧的花费信息和右侧的步进控件推到两端。
减号按钮的 onClick 回调中有一个边界保护:if (this.fQty > 1) 确保数量不会减到 0 以下。加号按钮同样有上限保护:if (this.fQty < 10) 确保数量不超过 10。两个按钮都使用圆形背景(.borderRadius(16) 配合 32x32 的尺寸),减号使用浅绿色背景配草甸绿文字,加号使用草甸绿背景配白色文字,形成视觉上的主次区分。
总价计算是实时的——this.fQty * 20 表达式在 fQty 变化时自动重新计算,由于 fQty 是 @State 变量,其变化会触发包含该表达式的 Text 组件重新渲染。这就是声明式 UI 的核心优势:开发者只需声明 UI 与状态的映射关系,框架负责在状态变化时自动更新 UI。
7.7 收获弹窗:渐变头部与上下拼接卡
@Builder
harvestModal() {
Column() {
Column() {
Column() {
Text('🌾 收获确认')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(this.selHarvest!.name + ' 已成熟,可以收获了')
.fontSize(13)
.fontColor('#E8F5E9')
.margin({ top: 6 })
Text(this.selHarvest!.icon)
.fontSize(60)
.margin({ top: 8 })
}
.width('100%')
.padding({ top: 26, bottom: 26 })
.backgroundColor('#7CB342')
.borderRadius({ topLeft: 18, topRight: 18 })
.linearGradient({ colors: [['#7CB342', 0], ['#2E5D1F', 1]], angle: 135 })
收获弹窗采用了与前面两个底部抽屉不同的风格——居中卡片搭配渐变头部。头部 Column 使用 linearGradient 属性实现线性渐变背景。colors 参数接受一个二维数组,每个子数组包含颜色值和停止位置(0-1)。这里从 #7CB342(浅草甸绿,位置 0)渐变到 #2E5D1F(深草甸绿,位置 1),angle: 135 指定渐变角度为 135 度(从左上到右下)。
渐变头部只对顶部两角设置圆角(borderRadius({ topLeft: 18, topRight: 18 })),与下方白色内容区的底部圆角配合,形成完整的圆角卡片。头部内含标题、描述文字和作物 Emoji 图标,白色文字在绿色渐变背景上具有良好的可读性。
Column() {
Row() {
Column() {
Text('预计收获')
.fontSize(11)
.fontColor('#90A4AE')
Text(this.selHarvest!.yieldNum + ' 个')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('单果售价')
.fontSize(11)
.fontColor('#90A4AE')
Text('¥' + this.selHarvest!.price)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FB8C00')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('预计总价')
.fontSize(11)
.fontColor('#90A4AE')
Text('¥' + (this.selHarvest!.yieldNum * this.selHarvest!.price).toString())
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#E53935')
.margin({ top: 4 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.End)
}
.width('100%')
白色内容区使用三列等宽布局展示收获数据。三个 Column 各设 .layoutWeight(1) 实现等宽分布。每列包含标签(灰色小字)和数值(大号加粗字),数值颜色按语义区分:收获数量用草甸绿、单果售价用橙色、预计总价用草莓红。预计总价通过 yieldNum * price 实时计算,让玩家直观了解收获收益。
7.8 集市购买弹窗:票根风格设计
@Builder
buyModal() {
Column() {
Column() {
Column() {
Text(this.selBuy!.tag + ' · 订单票根')
.fontSize(12)
.fontColor('#7CB342')
Text('云端牧场集市')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 6 })
Text('CLOUD RANCH MARKET')
.fontSize(10)
.fontColor('#DCEDC8')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: 22, bottom: 22 })
.backgroundColor('#7CB342')
.linearGradient({ colors: [['#7CB342', 0], ['#2E5D1F', 1]], angle: 135 })
集市购买弹窗采用了独特的"票根"视觉风格,模拟真实世界中的购物小票或入场券。头部渐变区展示商品标签、集市名称(中英文双语)和品牌标语。这种设计让交易弹窗具有仪式感,增强了"购买"行为的心理权重。
Divider()
.color('#90A4AE')
.strokeWidth(1)
.dashArray([6, 6])
.margin({ top: 2, bottom: 12 })
头部与内容区之间使用 Divider 组件添加了一条虚线分隔线。.dashArray([6, 6]) 设置虚线模式——6vp 实线段后跟 6vp 间隔,循环重复。这条虚线是票根设计的标志性元素,模拟了真实票据上的撕撕线,增强了"票根"的视觉隐喻。Divider 是鸿蒙 ArkUI 的内置分隔线组件,通过 dashArray 属性可以从实线切换为虚线。
7.9 任务详情弹窗:时间轴与进度可视化
@Builder
taskModal() {
Column() {
Column() {
Row() {
Text(this.selTask!.icon)
.fontSize(40)
Column() {
Text(this.selTask!.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
Row() {
Text(this.selTask!.type)
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
}
.backgroundColor(getTaskTypeColor(this.selTask!.type))
.borderRadius(8)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
任务详情弹窗的头部使用 Row 水平排列任务 Emoji 图标和任务信息 Column。任务信息包含名称和类型标签——类型标签通过 getTaskTypeColor 函数获取对应的主题色作为背景,白色文字在小圆角标签上形成彩色徽章效果。
Progress({ value: this.selTask!.progress, total: this.selTask!.total })
.color('#7CB342')
.backgroundColor('#E8F5E9')
.height(8)
.width('100%')
Row() {
Column() {
Text('① 领取')
.fontSize(11)
.fontColor(this.selTask!.progress >= 1 ? '#7CB342' : '#BDBDBD')
}
.layoutWeight(1)
Column() {
Text('② 完成')
.fontSize(11)
.fontColor(this.selTask!.progress >= this.selTask!.total ? '#7CB342' : '#BDBDBD')
}
.layoutWeight(1)
Column() {
Text('③ 领奖')
.fontSize(11)
.fontColor(this.selTask!.progress >= this.selTask!.total ? '#FB8C00' : '#BDBDBD')
}
.layoutWeight(1)
}
.width('100%')
.margin({ top: 14 })
进度可视化区域由 Progress 进度条和三步时间轴组成。进度条使用 progress 和 total 的比值自动计算填充比例。时间轴将任务流程分为"领取"“完成”"领奖"三个阶段,每个阶段的文字颜色根据当前进度动态变化:已达成用草甸绿或橙色高亮,未达成用浅灰色 #BDBDBD 弱化。这种"进度条+时间轴"的双重视觉反馈让用户对任务进度有直观而全面的理解。
7.10 奖励弹窗:金色庆祝卡与缩放动画
@Builder
rewardModal() {
Column() {
Column() {
Column() {
Text('🎉')
.fontSize(64)
Text('任务完成!')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 8 })
Text(this.selReward!.name)
.fontSize(14)
.fontColor('#FFF8E1')
.margin({ top: 6 })
}
.width('100%')
.padding({ top: 28, bottom: 28 })
.backgroundColor('#FFB300')
.borderRadius({ topLeft: 18, topRight: 18 })
.linearGradient({ colors: [['#FFB300', 0], ['#E65100', 1]], angle: 135 })
奖励弹窗是所有弹窗中视觉最华丽的一个。头部使用金色到深橙色的渐变(#FFB300 到 #E65100),配合大尺寸的庆祝 Emoji(🎉,64 号字),营造出"任务完成"的庆祝氛围。头部展示庆祝图标、"任务完成!"大标题和任务名称。
.transition(TransitionEffect.scale({ x: 0.8, y: 0.8 }).animation({ duration: 220 }))
奖励弹窗使用了与前面弹窗不同的过渡动画——TransitionEffect.scale({ x: 0.8, y: 0.8 }),即缩放过渡。弹窗出现时从 80% 大小放大到 100%,营造出"弹出"的庆祝效果。220ms 的动画时长让缩放过程既有视觉冲击力又不显得拖沓。三种弹窗过渡效果(透明度淡入、位移滑入、缩放弹出)的差异化运用,使每个弹窗都有独特的视觉个性。
7.11 天气预警弹窗:深色警示风格
@Builder
weatherModal() {
Column() {
Column() {
Column() {
Text('⛈️')
.fontSize(56)
Text('暴风雨预警')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 10 })
Text('未来 2 小时将有强降雨')
.fontSize(13)
.fontColor('#B0BEC5')
.margin({ top: 6 })
}
.width('100%')
.padding({ top: 26, bottom: 26 })
.backgroundColor('#37474F')
.linearGradient({ colors: [['#37474F', 0], ['#1A237E', 1]], angle: 160 })
天气预警弹窗是唯一使用深色主题的弹窗。头部渐变从 #37474F(深蓝灰)到 #1A237E(深靛蓝),角度 160 度,营造出暴风雨来临前的阴沉氛围。白色标题文字在深色背景上形成强烈对比,灰色 #B0BEC5 的辅助文字则提供预警详情。
Column() {
Row() {
Text('🌬️')
.fontSize(30)
Column() {
Text('大风 8 级')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('请加固围栏和风车')
.fontSize(11)
.fontColor('#90A4AE')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
.padding(12)
.backgroundColor('#455A64')
.borderRadius(10)
内容区每条预警信息使用深色卡片(#455A64)承载,包含天气图标、预警标题和建议措施。深色卡片在 #263238 的深色内容区背景上形成层次感。底部操作按钮中,"我知道了"使用橙色 #FF8F00 强调按钮引导用户确认,"加固牧场"使用灰色弱化处理,形成主次操作区分。
深色主题在警示类弹窗中具有独特的心理效应——深色背景天然传达"严肃""紧急"的情绪信号,与暴风雨预警的场景高度契合。这也是为什么天气预警弹窗刻意打破了应用的绿色主题色体系,转而使用深蓝灰色调。在设计实践中,弹窗的色彩选择应当服务于内容语义,而非机械地统一主题色。
7.12 奶制品工坊弹窗:配方可视化
@Builder
craftModal() {
Column() {
Column() {
Row() {
Text('🧀 奶制品工坊')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
.layoutWeight(1)
Text('✕')
.fontSize(18)
.fontColor('#90A4AE')
.onClick(() => { this.showCraft = false })
}
.width('100%')
Row() {
Column() {
Text('🥛')
.fontSize(34)
Text('鲜牛奶 ×2')
.fontSize(11)
.fontColor('#2E5D1F')
.margin({ top: 6 })
}
.layoutWeight(1)
.padding({ top: 14, bottom: 14 })
.backgroundColor('#FFF8E1')
.borderRadius(12)
Text('➕')
.fontSize(20)
.fontColor('#90A4AE')
.margin({ left: 10, right: 10 })
Column() {
Text('🍯')
.fontSize(34)
Text('蜂蜜 ×1')
.fontSize(11)
.fontColor('#2E5D1F')
.margin({ top: 6 })
}
.layoutWeight(1)
.padding({ top: 14, bottom: 14 })
.backgroundColor('#FFF8E1')
.borderRadius(12)
Text('➡️')
.fontSize(20)
.fontColor('#90A4AE')
.margin({ left: 10, right: 10 })
Column() {
Text('🧈')
.fontSize(34)
Text('黄油蛋糕')
.fontSize(11)
.fontColor('#2E5D1F')
.margin({ top: 6 })
}
.layoutWeight(1)
.padding({ top: 14, bottom: 14 })
.backgroundColor('#E8F5E9')
.borderRadius(12)
}
.width('100%')
.margin({ top: 14 })
奶制品工坊弹窗的亮点在于配方可视化——使用"原料1 + 原料2 → 产物"的视觉结构展示制作配方。三个 Column 分别代表鲜牛奶、蜂蜜和黄油蛋糕,中间用 ➕ 和 ➡️ Emoji 作为连接符。原料卡片使用奶油黄背景 #FFF8E1,产物卡片使用浅绿色背景 #E8F5E9,通过颜色区分输入和输出。这种可视化方式让玩家一目了然地理解制作所需的材料和最终产物。
7.13 出售确认弹窗:深红警示风格
@Builder
deleteModal() {
Column() {
Column() {
Column() {
Text('⚠️')
.fontSize(52)
Text('确认出售 ' + this.selDelete!.name + ' ?')
.fontSize(19)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ top: 10 })
Text('出售后将无法找回这只小动物')
.fontSize(12)
.fontColor('#EF9A9A')
.margin({ top: 6 })
}
.width('100%')
.padding({ top: 24, bottom: 24 })
.backgroundColor('#C62828')
.linearGradient({ colors: [['#C62828', 0], ['#8E0000', 1]], angle: 160 })
出售确认弹窗使用深红色渐变头部(#C62828 到 #8E0000),配合 ⚠️ 警告图标,传达"危险操作"的视觉信号。标题动态拼接了待出售动物的名称,副标题用浅红色 #EF9A9A 明确告知"出售后将无法找回"。这种强警示设计在不可逆操作(如删除、出售)中是必要的 UX 实践——通过视觉强度提醒用户三思而后行。
内容区展示出售价格和当前好感度信息,底部操作按钮中"再想想"使用灰色弱化处理引导用户取消,"确认出售"使用深红色 #C62828 强调按钮匹配头部色调。阴影颜色也特意使用了半透明红色 #66C62828,使整个弹窗的红色警示氛围保持一致。
八、公共 @Builder 组件
8.1 底部 Tab 项组件
@Builder
bottomTabItem(icon: string, label: string, idx: number) {
Column() {
Text(icon)
.fontSize(20)
.opacity(this.tabIndex === idx ? 1 : 0.55)
Text(label)
.fontSize(11)
.fontWeight(this.tabIndex === idx ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.tabIndex === idx ? '#2E5D1F' : '#90A4AE')
.margin({ top: 2 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.height('100%')
.onClick(() => { this.tabIndex = idx })
}
bottomTabItem 是底部 Tab 栏的单项组件,接受 icon(图标 Emoji)、label(标签文字)和 idx(Tab 索引)三个参数。它通过 this.tabIndex === idx 判断当前项是否被选中,据此动态设置三个视觉属性:图标透明度(选中 1.0、未选中 0.55)、标签字重(选中 Bold、未选中 Normal)和标签颜色(选中草甸绿、未选中灰色)。
Column 容器使用 .justifyContent(FlexAlign.Center) 实现图标和文字垂直居中,.layoutWeight(1) 让每个 Tab 项在 Row 中等宽分布,.height('100%') 填满 Tab 栏的 56vp 高度。点击事件将 this.tabIndex 设置为当前项索引,触发整个主内容区的条件渲染切换。
8.2 遮罩层组件
@Builder
modalOverlay(onClose: () => void) {
Column() {
Text('')
.width('100%')
.height('100%')
.onClick(() => { onClose() })
}
.width('100%')
.height('100%')
.backgroundColor('#66000000')
}
modalOverlay 是所有弹窗共享的半透明遮罩层。它接受一个 onClose 回调函数作为参数,点击遮罩任意区域时触发。遮罩背景色 #66000000 是 40% 透明度的黑色(66 是十六进制的 40% 透明度前缀),这种半透明遮罩既能突出弹窗内容,又不会完全遮蔽底层页面的上下文信息。
这个组件的设计体现了 @Builder 函数的参数化能力——通过传入不同的 onClose 回调,同一个遮罩组件可以服务于所有 16 个弹窗,无需为每个弹窗单独编写遮罩代码。这种"参数化复用"是 @Builder 相比直接内联 UI 代码的核心优势之一。
8.3 Toast 提示组件
@Builder
toastBox() {
Column() {
Text(this.toast)
.fontSize(13)
.fontColor('#FFFFFF')
.padding({ left: 18, right: 18, top: 10, bottom: 10 })
}
.backgroundColor('#CC37474F')
.borderRadius(18)
.position({ x: 0, y: '72%' })
.transition(TransitionEffect.OPACITY.animation({ duration: 180 }))
}
toastBox 是全局 Toast 提示组件,展示 this.toast 状态变量中的提示文案。它使用绝对定位 .position({ x: 0, y: '72%' }) 固定在屏幕 72% 高度处(略低于屏幕中央),背景色 #CC37474F 是 80% 透明度的深蓝灰色。180ms 的透明度过渡动画使 Toast 的出现和消失都柔和自然。
Toast 与弹窗的区别在于交互层级:弹窗是模态的(Modal),会阻止用户与底层页面交互直到关闭;Toast 是非模态的(Non-modal),仅展示信息不阻断操作。本案例中所有操作反馈(如"领养成功"“喂养成功”“购买成功”)都使用 Toast 而非弹窗,这符合移动端 UX 设计中"轻量反馈优先"的原则。
九、子组件:RanchTab 牧场首页
9.1 组件声明与 @Prop 数据传递
@Component
struct RanchTab {
@Prop coin: number;
@Prop exp: number;
onOpenAnimal: (a: Animal) => void = () => {};
onOpenTask: (t: RanchTask) => void = () => {};
onAdopt: () => void = () => {};
onOpenMarket: () => void = () => {};
onWeather: () => void = () => {};
onCraft: () => void = () => {};
onFeed: (a: Animal) => void = () => {};
RanchTab 是牧场首页子组件,使用 @Component 装饰器声明。它通过 @Prop 接收父组件传递的 coin 和 exp 数据。@Prop 是鸿蒙 ArkUI 中的单向数据传递装饰器——父组件的数据变化会同步到子组件,但子组件对 @Prop 变量的修改不会回传给父组件。这种单向同步机制确保了数据流的清晰可追溯。
七个回调函数(onOpenAnimal、onOpenTask 等)都初始化为空箭头函数 () => {},这是一种防御性编程实践——即使父组件没有传入某个回调,子组件调用它也不会报错。这些回调的命名遵循"on + 动词 + 名词"的约定,清晰表达了每个回调的语义。
@Prop与@State的关键区别在于所有权:@State是组件自己拥有的可变状态,@Prop是从父组件接收的只读副本。当父组件的对应@State变化时,框架会自动将新值同步到子组件的@Prop,并触发子组件依赖该变量的 UI 重新渲染。但在子组件内部修改@Prop变量不会影响父组件——这种"只读同步"机制是 ArkUI 状态管理金字塔的基础。
9.2 电商风格头部与搜索栏
build() {
Column() {
Column() {
Row() {
Text('🏡')
.fontSize(22)
Text('云端牧场')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ left: 8 })
Text('')
.layoutWeight(1)
Text('¥' + formatMoney(this.coin))
.fontSize(13)
.fontColor('#FFD54F')
.fontWeight(FontWeight.Bold)
Text('📷')
.fontSize(16)
.margin({ left: 12 })
Text('🔔')
.fontSize(16)
.margin({ left: 10 })
}
.width('100%')
Row() {
Text('🔍')
.fontSize(14)
.margin({ left: 12 })
Text('搜索动物、作物、集市好物')
.fontSize(12)
.fontColor('#E8F5E9')
.margin({ left: 8 })
Text('')
.layoutWeight(1)
Text('搜索')
.fontSize(12)
.fontColor('#2E5D1F')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ right: 10 })
}
.width('100%')
.height(38)
.backgroundColor('#55FFFFFF')
.borderRadius(18)
.margin({ top: 12 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 16 })
.backgroundColor('#7CB342')
.linearGradient({ colors: [['#7CB342', 0], ['#2E5D1F', 1]], angle: 90 })
牧场首页的头部采用电商风格设计——绿色渐变背景上排列标题、金币余额、相机和通知图标,下方是半透明搜索栏。金币余额使用 formatMoney 函数格式化,奶油黄色 #FFD54F 突出显示。搜索栏背景使用 #55FFFFFF(33% 透明度白色),在绿色渐变上形成磨砂玻璃效果。
头部使用 linearGradient 设置 90 度水平渐变,从 #7CB342(左)到 #2E5D1F(右),营造出从浅到深的色彩层次。padding 设置四周内边距确保内容不贴边。
9.3 牧场横幅大卡与快捷宫格
Column() {
Row() {
Text('☁️')
.fontSize(40)
.margin({ right: 6 })
Text('🐄')
.fontSize(40)
.margin({ right: 6 })
Text('🌳')
.fontSize(40)
.margin({ right: 6 })
Text('🏠')
.fontSize(40)
Text('')
.layoutWeight(1)
Column() {
Text('牧场 Lv.' + 4)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text('经验 ' + this.exp + ' / 4500')
.fontSize(10)
.fontColor('#E8F5E9')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
Progress({ value: this.exp, total: 4500 })
.color('#FFD54F')
.backgroundColor('#33FFFFFF')
.height(8)
.width('100%')
.margin({ top: 10 })
Row() {
Text('🌤️ 晴 24°C')
.fontSize(11)
.fontColor('#E8F5E9')
Text('')
.layoutWeight(1)
Text('今日营收 ¥' + formatMoney(1280))
.fontSize(11)
.fontColor('#FFD54F')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(16)
.backgroundColor('#2E7D32')
.borderRadius(16)
.linearGradient({ colors: [['#388E3C', 0], ['#1B5E20', 1]], angle: 135 })
.margin({ top: 12 })
.onClick(() => { this.onWeather() })
牧场横幅大卡是首页的视觉焦点。它使用 135 度对角渐变背景,左侧排列四个 Emoji(云朵、奶牛、树木、房屋)模拟牧场全景,右侧展示等级和经验信息。经验进度条使用奶油黄填充色配 20% 透明度白色背景,在深绿色卡片上形成明亮的视觉锚点。整个卡片可点击,触发天气预警弹窗。
Row() {
ForEach(getQuickRows(), (q: QuickIcon) => {
Column() {
Text(q.icon)
.fontSize(24)
Text(q.name)
.fontSize(11)
.fontColor('#2E5D1F')
.margin({ top: 6 })
}
.layoutWeight(1)
.padding({ top: 12, bottom: 12 })
.backgroundColor('#F1F8E9')
.borderRadius(12)
.margin({ right: 8 })
.onClick(() => {
if (q.id === 1) {
this.onWeather();
} else if (q.id === 2) {
this.onAdopt();
} else if (q.id === 3) {
this.onCraft();
} else if (q.id === 4) {
this.onOpenMarket();
}
})
}, (q: QuickIcon) => 'q1' + q.id)
}
.width('100%')
.margin({ top: 12 })
快捷宫格区域使用 ForEach 遍历 getQuickRows() 返回的前 4 个快捷入口。每个入口是一个 Column,包含 Emoji 图标和名称文字,浅绿色 #F1F8E9 背景配圆角。点击事件通过 q.id 判断触发不同的回调:ID 1 触发天气预警、ID 2 触发领养、ID 3 触发奶制品工坊、ID 4 跳转集市。第二行使用 getQuickRows2() 返回后 4 个入口,结构完全一致。
9.4 动物横滑列表与今日任务
Scroll() {
Row() {
ForEach(getTopAnimals(), (a: Animal) => {
Column() {
Text(a.icon)
.fontSize(42)
Text(a.name)
.fontSize(12)
.fontWeight(FontWeight.Medium)
.fontColor('#2E5D1F')
.margin({ top: 6 })
Text('Lv.' + a.level + ' · 心情' + a.mood)
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 2 })
Row() {
Text('喂养')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
}
.backgroundColor('#7CB342')
.borderRadius(10)
.margin({ top: 6 })
.onClick(() => { this.onFeed(a) })
}
.width(110)
.padding({ top: 12, bottom: 12 })
.backgroundColor('#F9FBE7')
.borderRadius(12)
.margin({ right: 10 })
.onClick(() => { this.onOpenAnimal(a) })
}, (a: Animal) => 'top' + a.id)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ top: 10 })
动物横滑列表使用 Scroll 容器配合 ScrollDirection.Horizontal 实现水平滚动。每个动物卡片固定宽度 110vp,内部垂直排列 Emoji 图标、名称、等级心情和喂养按钮。卡片整体可点击触发动物档案弹窗,喂养按钮单独绑定 onFeed 回调。.scrollBar(BarState.Off) 隐藏横向滚动条,保持列表的视觉简洁。
水平滚动列表(Horizontal Scroll)是移动端电商和社交应用中最常见的列表形态之一。鸿蒙 ArkUI 中实现横滑只需将
Scroll的.scrollable()设为ScrollDirection.Horizontal,配合内部Row容器即可。相比传统的List组件,Scroll + Row的组合更加灵活,可以自由设置每个子项的固定宽度和间距。
十、子组件:AnimalTab 动物页
9.1 双列卡片网格
@Component
struct AnimalTab {
onOpenAnimal: (a: Animal) => void = () => {};
onFeed: (a: Animal) => void = () => {};
onDelete: (a: Animal) => void = () => {};
onAdopt: () => void = () => {};
build() {
Column() {
Column() {
Row() {
Text('🐄')
.fontSize(22)
Text('我的动物')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ left: 8 })
Text('')
.layoutWeight(1)
Text('+ 领养')
.fontSize(12)
.fontColor('#2E5D1F')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.onClick(() => { this.onAdopt() })
}
.width('100%')
Text('共 ' + ANIMALS.length + ' 只小动物 · 平均心情 ' + 86 + '%')
.fontSize(12)
.fontColor('#E8F5E9')
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 16 })
.backgroundColor('#388E3C')
.linearGradient({ colors: [['#388E3C', 0], ['#1B5E20', 1]], angle: 90 })
AnimalTab 是动物管理页面,头部使用与首页不同的绿色渐变(#388E3C 到 #1B5E20),标题旁有"+ 领养"按钮直接触发领养弹窗。副标题展示动物总数和平均心情值。这种每个 Tab 使用不同渐变色调的设计,让用户在切换页面时能通过色彩变化感知页面切换。
Row() {
ForEach(getAnimalRows(), (a: Animal) => {
Column() {
Row() {
Text(a.icon)
.fontSize(46)
Text('Lv.' + a.level)
.fontSize(10)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FB8C00')
.borderRadius(8)
}
.width('100%')
Text(a.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
.width('100%')
.margin({ top: 8 })
Text(a.breed)
.fontSize(10)
.fontColor('#90A4AE')
.width('100%')
.margin({ top: 2 })
Row() {
Text('😊')
.fontSize(12)
Progress({ value: a.mood, total: 100 })
.color(getMoodColor(a.mood))
.backgroundColor('#F5F5F5')
.height(6)
.layoutWeight(1)
.margin({ left: 4, right: 4 })
Text(a.mood + '%')
.fontSize(10)
.fontColor(getMoodColor(a.mood))
}
.width('100%')
.margin({ top: 8 })
动物卡片使用双列网格布局,通过 getAnimalRows() 和 getAnimalRows2() 分别渲染左右两列。每张卡片是一个白色背景圆角容器,内部从上到下依次排列:图标和等级徽章行、名称、品种、心情进度条、产出信息和操作按钮行。
心情进度条使用 getMoodColor 函数动态获取颜色——心情值 92 的奶牛使用草莓红 #E53935,心情值 70 的猪使用橙色 #FB8C00。进度条颜色与右侧百分比文字颜色保持一致,形成颜色编码的视觉关联。这种"数据驱动颜色"的设计让用户无需阅读数字就能通过颜色快速判断动物心情状态。
操作按钮行有两个按钮:左侧的 🍎 喂养按钮(浅绿背景)触发 onFeed 回调,右侧的按钮在左右两列中不同——左列是 💧 详情按钮(浅绿背景)触发 onOpenAnimal,右列是 🚫 出售按钮(浅红背景 #FFEBEE)触发 onDelete。这种差异化的操作布局让两列卡片在保持结构一致的同时拥有不同的功能入口。
十一、子组件:CropTab 种植页与田地系统
11.1 田地网格与状态驱动渲染
@Component
struct CropTab {
@Prop plotStates: number[];
@Prop cropNames: string[];
onPlant: (c: Crop) => void = () => {};
onHarvest: (c: Crop) => void = () => {};
onOpenMarket: () => void = () => {};
build() {
Column() {
// ... 头部省略
Scroll() {
Column() {
Text('🗺️ 田地地图')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
.width('100%')
Row() {
ForEach(this.plotStates, (st: number, pi: number) => {
Column() {
Text(st === 0 ? '🟫' : (st === 1 ? '🌱' : '🌾'))
.fontSize(26)
Text(this.cropNames[pi])
.fontSize(10)
.fontColor('#2E5D1F')
.margin({ top: 4 })
Text(st === 0 ? '空地' : (st === 1 ? '生长中' : '可收'))
.fontSize(9)
.fontColor(st === 2 ? '#E53935' : '#90A4AE')
.margin({ top: 2 })
}
.layoutWeight(1)
.padding({ top: 10, bottom: 10 })
.backgroundColor(st === 2 ? '#FFF3E0' : '#F1F8E9')
.borderRadius(10)
.margin({ right: 6, bottom: 6 })
.onClick(() => {
if (st === 2) {
this.onHarvest(CROPS[pi]);
} else {
this.onPlant(CROPS[pi]);
}
})
}, (st: number, pi: number) => 'g' + st + pi)
}
.width('100%')
CropTab 是种植管理页面,通过 @Prop 接收 plotStates(田地状态数组)和 cropNames(作物名称数组)两个数据源。田地网格使用 ForEach 遍历 plotStates 数组渲染 9 块田地。
每块田地的视觉表现完全由状态值 st 驱动:状态 0(空地)显示 🟫 Emoji 和灰色"空地"文字,背景为浅绿色;状态 1(生长中)显示 🌱 Emoji 和灰色"生长中"文字,背景为浅绿色;状态 2(可收获)显示 🌾 Emoji 和红色"可收"文字,背景为浅橙色 #FFF3E0。这种"状态-视觉"映射通过三元条件表达式实现,代码简洁而表达力强。
点击事件也由状态驱动:状态 2 触发 onHarvest 回调打开收获弹窗,其他状态触发 onPlant 回调打开种植弹窗。这种"同一交互入口根据状态执行不同操作"的设计简化了用户操作流程——用户无需先选择操作类型再选择田地,直接点击田地即可执行当前状态对应的操作。
11.2 热门种子横滑与待收获清单
Scroll() {
Row() {
ForEach(getTopCrops(), (c: Crop) => {
Column() {
Text(c.icon)
.fontSize(38)
Text(c.name)
.fontSize(12)
.fontWeight(FontWeight.Medium)
.fontColor('#2E5D1F')
.margin({ top: 6 })
Text(c.days + ' 天 · ¥' + c.price)
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 2 })
Row() {
Text('种植')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
}
.backgroundColor('#7CB342')
.borderRadius(10)
.margin({ top: 6 })
.onClick(() => { this.onPlant(c) })
}
.width(108)
.padding({ top: 12, bottom: 12 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ right: 10 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
}, (c: Crop) => 'tc' + c.id)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
热门种子横滑区域展示前 5 种作物,每个卡片固定宽度 108vp,白色背景配阴影效果 shadow({ radius: 4, color: '#0D000000', offsetY: 2 }) 形成立体卡片感。阴影参数中 radius 控制模糊半径,color 使用 5% 透明度黑色,offsetY: 2 让阴影向下偏移 2vp 模拟自然光照下的投影效果。
待收获清单使用纵向列表布局,每行展示一种作物的图标、名称、预计产量和单价,右侧有橙色"收获"按钮。清单背景使用奶油黄 #FFF8E1 与白色卡片形成色彩区分。
十二、子组件:MarketTab 集市页与 TaskTab 任务页
12.1 集市热销榜单与分类筛选
@Component
struct MarketTab {
@Prop coin: number;
onSell: (g: MarketGoods) => void = () => {};
onBuy: (g: MarketGoods) => void = () => {};
build() {
Column() {
Column() {
Row() {
Text('🏪')
.fontSize(22)
Text('云端集市')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ left: 8 })
Text('')
.layoutWeight(1)
Text('¥' + formatMoney(this.coin))
.fontSize(13)
.fontColor('#FFD54F')
.fontWeight(FontWeight.Bold)
}
.width('100%')
MarketTab 集市页头部使用橙色渐变(#FB8C00 到 #E65100),与前面的绿色页面形成鲜明对比。这种色彩切换不仅服务于视觉多样性,也在心理层面帮助用户区分功能模块——绿色系对应"牧场/种植",橙色系对应"集市/交易"。
Row() {
ForEach(['全部', '乳品', '手工', '稀有', '饲料'], (tag: string, ti: number) => {
Text(tag)
.fontSize(12)
.fontColor(ti === 0 ? '#FFFFFF' : '#7CB342')
.padding({ left: 14, right: 14, top: 6, bottom: 6 })
.backgroundColor(ti === 0 ? '#7CB342' : '#E8F5E9')
.borderRadius(14)
.margin({ right: 8 })
}, (tag: string, ti: number) => 'tag' + ti)
}
.width('100%')
.margin({ top: 14 })
分类筛选区域使用 chips(胶囊标签)设计。ForEach 遍历字符串数组 ['全部', '乳品', '手工', '稀有', '饲料'],第一个标签(“全部”)使用草甸绿实心背景配白色文字表示选中状态,其余标签使用浅绿色背景配草甸绿文字表示未选中状态。.borderRadius(14) 配合 padding 形成胶囊形状。这种 chips 设计是移动端分类筛选的主流交互模式。
12.2 双列商品卡片与出售/购买操作
Column() {
Row() {
ForEach(getMarketRows(), (g: MarketGoods) => {
Column() {
Row() {
Text(g.tag)
.fontSize(9)
.fontColor('#FFFFFF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor('#FB8C00')
.borderRadius(6)
Text('')
.layoutWeight(1)
Text(g.icon)
.fontSize(40)
}
.width('100%')
Text(g.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
.width('100%')
.margin({ top: 6 })
Text(g.seller)
.fontSize(10)
.fontColor('#90A4AE')
.width('100%')
.margin({ top: 2 })
Row() {
Text('¥' + g.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#E53935')
Text('')
.layoutWeight(1)
Text('库存' + g.stock)
.fontSize(9)
.fontColor('#90A4AE')
}
.width('100%')
.margin({ top: 8 })
Row() {
Text('出售')
.fontSize(11)
.fontColor('#FB8C00')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('#FFF3E0')
.borderRadius(10)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => { this.onSell(g) })
Text('购买')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.backgroundColor('#7CB342')
.borderRadius(10)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.margin({ left: 8 })
.onClick(() => { this.onBuy(g) })
}
.width('100%')
.margin({ top: 8 })
}
.layoutWeight(1)
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ right: 8 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
}, (g: MarketGoods) => 'g1' + g.id)
}
商品卡片使用双列网格布局,每张卡片包含标签徽章、商品图标、名称、卖家、价格、库存和出售/购买按钮行。标签徽章使用橙色背景配白色文字,放在卡片左上角。价格使用草莓红 #E53935 加粗显示,是卡片中最醒目的信息。
操作按钮行使用 .layoutWeight(1) 让"出售"和"购买"按钮等宽分布。"出售"按钮使用浅橙色背景配橙色文字(次要操作),"购买"按钮使用草甸绿背景配白色文字(主要操作)。.textAlign(TextAlign.Center) 确保按钮文字水平居中。这种主次按钮的视觉对比引导用户优先关注"购买"操作。
12.3 任务页的日常/周常分组与技能入口
@Component
struct TaskTab {
onDetail: (t: RanchTask) => void = () => {};
onReward: (t: RanchTask) => void = () => {};
onSkill: (s: RanchSkill) => void = () => {};
build() {
Column() {
// ... 头部省略
Scroll() {
Column() {
Text('🌞 日常任务')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
.width('100%')
Column() {
ForEach(getTaskRows(), (t: RanchTask) => {
Row() {
Text(t.icon)
.fontSize(32)
Column() {
Text(t.name)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#2E5D1F')
Text('进度 ' + t.progress + '/' + t.total)
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text('¥' + t.reward)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FB8C00')
Text(t.progress >= t.total ? '可领' : '进行中')
.fontSize(9)
.fontColor(t.progress >= t.total ? '#E53935' : '#90A4AE')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
Row() {
Text(t.progress >= t.total ? '领取' : '查看')
.fontSize(11)
.fontColor('#FFFFFF')
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
}
.backgroundColor(t.progress >= t.total ? '#FB8C00' : '#7CB342')
.borderRadius(10)
.margin({ left: 8 })
.onClick(() => {
if (t.progress >= t.total) {
this.onReward(t);
} else {
this.onDetail(t);
}
})
}
.width('100%')
.padding(12)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ top: 8 })
.shadow({ radius: 4, color: '#0D000000', offsetY: 2 })
}, (t: RanchTask) => 't1' + t.id)
}
TaskTab 任务页将任务分为"日常任务"和"周常任务"两组分别展示。每个任务行包含图标、名称、进度、奖励金额、状态标签和操作按钮。状态标签根据 progress >= total 判断显示"可领"(红色)或"进行中"(灰色)。操作按钮的文字和背景色也由状态驱动——已完成时显示橙色"领取"按钮触发 onReward 回调,未完成时显示绿色"查看"按钮触发 onDetail 回调。
这种"状态驱动的条件渲染"模式在鸿蒙 ArkUI 中无处不在。通过三元条件表达式
condition ? valueA : valueB,开发者可以在声明式代码中优雅地表达"同一元素在不同状态下呈现不同外观"的需求。相比命令式 UI 框架中需要手动调用setText()、setBackgroundColor()等方法,声明式的方式大幅减少了样板代码。
任务页底部还有技能入口区域,使用 ForEach 遍历 getSkillRows() 渲染 4 个技能快捷入口。每个入口展示技能 Emoji、名称和等级,点击触发 onSkill 回调打开技能升级弹窗。
十三、子组件:MineTab 我的页面
13.1 个人中心头部与统计三格
@Component
struct MineTab {
@Prop coin: number;
@Prop exp: number;
onLevel: (l: RanchLevel) => void = () => {};
onSkill: (s: RanchSkill) => void = () => {};
onFriend: (f: RanchFriend) => void = () => {};
onCraft: () => void = () => {};
build() {
Column() {
Column() {
Row() {
Text('👤')
.fontSize(22)
Text('我的牧场')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ left: 8 })
Text('')
.layoutWeight(1)
Text('⚙️')
.fontSize(16)
}
.width('100%')
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 16 })
.backgroundColor('#33691E')
.linearGradient({ colors: [['#33691E', 0], ['#1B5E20', 1]], angle: 90 })
Scroll() {
Column() {
Column() {
Row() {
Text('🐄')
.fontSize(64)
Column() {
Text('牧场主「麦浪」')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
Text('Lv.4 花田牧场 · 金牌牧场主')
.fontSize(11)
.fontColor('#7CB342')
.margin({ top: 4 })
Row() {
Text('🪙 ' + formatMoney(this.coin))
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor('#FB8C00')
Text(' · 声望 860')
.fontSize(12)
.fontColor('#90A4AE')
.margin({ left: 8 })
}
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 14 })
}
.width('100%')
Progress({ value: this.exp, total: 4500 })
.color('#FFB300')
.backgroundColor('#E8F5E9')
.height(8)
.width('100%')
.margin({ top: 14 })
Text('距离下一等级还差 ' + (4500 - this.exp) + ' 经验')
.fontSize(10)
.fontColor('#90A4AE')
.width('100%')
.margin({ top: 6 })
}
.width('100%')
.padding(18)
.backgroundColor('#FFFFFF')
.borderRadius(16)
.shadow({ radius: 6, color: '#14000000', offsetY: 3 })
.margin({ top: 12 })
MineTab 是个人中心页面,头部使用深绿色渐变(#33691E 到 #1B5E20),是所有 Tab 中色调最深的——这种"最深层级=最个人化页面"的色彩隐喻在设计中颇为常见。头部下方是玩家信息大卡片,使用白色背景配阴影效果,展示 64 号字的大奶牛 Emoji、牧场主名称、等级头衔、金币和声望信息。
经验进度条使用金色 #FFB300 填充色,与前面的草甸绿进度条形成区分。"距离下一等级还差 N 经验"的文字通过 (4500 - this.exp) 实时计算剩余经验值,由于 exp 是 @Prop,当父组件更新经验值时,这里的文字会自动刷新。
Row() {
Column() {
Text('14')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#2E5D1F')
Text('动物数量')
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 4 })
}
.layoutWeight(1)
Column() {
Text('9')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#7CB342')
Text('田地数量')
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 4 })
}
.layoutWeight(1)
Column() {
Text('128')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#FB8C00')
Text('好友互动')
.fontSize(10)
.fontColor('#90A4AE')
.margin({ top: 4 })
}
.layoutWeight(1)
}
.width('100%')
.padding(14)
.backgroundColor('#F1F8E9')
.borderRadius(14)
.margin({ top: 12 })
统计三格区域展示三个关键数据:动物数量(14,草甸绿)、田地数量(9,浅草甸绿)、好友互动次数(128,橙色)。三个 Column 各设 .layoutWeight(1) 等宽分布,大号数字配小号标签的组合让数据一目了然。浅绿色背景 #F1F8E9 使统计区与上方的白色信息卡形成层次区分。
13.2 等级横滑与好友列表
Scroll() {
Row() {
ForEach(RANCH_LEVELS, (l: RanchLevel) => {
Column() {
Text(l.icon)
.fontSize(34)
Text(l.name)
.fontSize(11)
.fontColor('#2E5D1F')
.margin({ top: 4 })
Text('Lv.' + l.id)
.fontSize(9)
.fontColor(l.id <= 4 ? '#7CB342' : '#BDBDBD')
.margin({ top: 2 })
}
.width(92)
.padding({ top: 12, bottom: 12 })
.backgroundColor(l.id <= 4 ? '#F1F8E9' : '#F5F5F5')
.borderRadius(12)
.margin({ right: 10 })
.onClick(() => { this.onLevel(l) })
}, (l: RanchLevel) => 'lv' + l.id)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
牧场等级横滑区域展示全部 8 个等级。已解锁的等级(l.id <= 4)使用浅绿色背景配草甸绿文字,未解锁的等级使用灰色背景配浅灰文字。这种通过条件表达式控制视觉状态的设计让用户一眼就能区分已解锁和未解锁内容。每个等级卡片宽度固定为 92vp,点击触发 onLevel 回调打开等级详情弹窗。
好友列表同样采用横滑布局,每个好友卡片展示头像 Emoji、名称和在线状态(🟢 在线 / ⚪ 离线)。在线状态使用 f.online ? '#7CB342' : '#90A4AE' 动态设置颜色——在线好友的在线标记用草甸绿,离线好友用灰色,与 Emoji 标识形成双重颜色编码。卡片使用白色背景配轻阴影,点击触发 onFriend 回调打开好友拜访弹窗。
十四、弹窗交互流程总览
上图展示了六个 Tab 页面中所有弹窗的触发路径。可以看到,16 个弹窗覆盖了应用的所有核心交互场景,每个弹窗都有明确的触发来源和视觉风格。弹窗体系的设计遵循了"操作即反馈"的原则——用户的每一次点击都会触发一个视觉反馈(弹窗或 Toast),确保交互的确定性和可感知性。
十五、弹窗风格对比总结
| 弹窗名称 | 视觉风格 | 出现位置 | 过渡动画 | 主题色调 | 使用场景 |
|---|---|---|---|---|---|
| 动物档案 | 居中卡片 | 屏幕中央 | 透明度淡入 200ms | 草甸绿 | 查看动物详情 |
| 领养动物 | 底部抽屉 | 屏幕底部 | 向上滑入 220ms | 草甸绿 | 选择领养动物 |
| 喂养饲料 | 底部抽屉 | 屏幕底部 | 向上滑入 220ms | 草甸绿 | 选择饲料喂养 |
| 收获确认 | 上下拼接卡 | 屏幕中央 | 透明度淡入 200ms | 渐变绿 | 确认作物收获 |
| 种植播种 | 底部抽屉 | 屏幕底部 | 向上滑入 220ms | 草甸绿 | 选择田地种植 |
| 集市出售 | 居中卡片 | 屏幕中央 | 透明度淡入 200ms | 橙色 | 上架商品出售 |
| 集市购买 | 票根卡片 | 屏幕中央 | 透明度淡入 200ms | 渐变绿 | 确认购买商品 |
| 任务详情 | 时间轴卡 | 屏幕中央 | 透明度淡入 200ms | 草甸绿 | 查看任务进度 |
| 任务奖励 | 金色庆祝卡 | 屏幕中央 | 缩放弹出 220ms | 金色渐变 | 领取任务奖励 |
| 牧场装饰 | 三列宫格抽屉 | 屏幕底部 | 向上滑入 220ms | 草甸绿 | 选择装饰放置 |
| 牧场升级 | 星级卡 | 屏幕中央 | 透明度淡入 200ms | 金色 | 查看等级福利 |
| 好友拜访 | 居中卡片 | 屏幕中央 | 透明度淡入 200ms | 草甸绿 | 拜访好友牧场 |
| 天气预警 | 深色警示卡 | 屏幕中央 | 透明度淡入 200ms | 深蓝灰 | 暴风雨预警 |
| 技能升级 | 星级卡 | 屏幕中央 | 透明度淡入 200ms | 草甸绿 | 查看升级技能 |
| 奶制品工坊 | 底部抽屉 | 屏幕底部 | 向上滑入 220ms | 奶油黄 | 制作奶制品 |
| 出售确认 | 深红警示卡 | 屏幕中央 | 透明度淡入 200ms | 深红渐变 | 确认出售动物 |
十六、技术总结
本文对一个完整的鸿蒙 ArkTS 模拟经营应用进行了逐段代码拆解和深度技术分析。从整体架构来看,该应用采用了"单入口多 Tab + 集中式弹窗管理"的架构模式,主入口 Index 组件作为状态中枢,统一管理 Tab 路由状态和 16 个弹窗的开关状态,六个子组件(RanchTab、AnimalTab、CropTab、MarketTab、TaskTab、MineTab)各司其职负责具体页面的 UI 展示,通过回调函数将用户交互事件冒泡给父组件处理。这种"状态上移、展示下沉"的分层设计是鸿蒙声明式 UI 架构的典型范式。
在数据层面,应用使用 TypeScript 接口定义了 10 个业务实体类型,配合 10 个常量数组作为 Mock 数据源,实现了完整的类型安全保障。20 个分列工具函数和 5 个格式化/映射函数将数据处理逻辑从 UI 代码中解耦,使组件代码专注于视图描述。@State 装饰器驱动了整个响应式更新链路——从 Tab 切换到弹窗显隐,从数量步进到总价计算,所有 UI 更新都由状态变化自动触发,开发者无需手动操作 DOM。
在 UI 层面,Column、Row、Stack 三大布局容器构成了应用的布局骨架。Column 用于垂直排列内容(如卡片内部的文字列表),Row 用于水平排列内容(如按钮组、标签栏),Stack 用于层叠布局(如弹窗覆盖在主内容之上)。ForEach 负责列表渲染,通过键值生成函数实现高效的 Diff 更新。Scroll 提供滚动能力,支持垂直和水平两种方向。Progress 内置组件简化了进度条的实现。Divider 配合 dashArray 实现了票根风格的虚线分隔。
弹窗体系是本应用最精彩的设计部分。16 个 @Builder 弹窗函数覆盖了所有核心交互场景,采用了居中卡片、底部抽屉、上下拼接卡、票根卡、时间轴卡、金色庆祝卡、深色警示卡、星级卡、配方卡、深红警示卡等 10 种不同的视觉风格。每种风格都匹配了对应的过渡动画——居中弹窗使用透明度淡入(200ms)、底部抽屉使用向上位移滑入(220ms)、庆祝弹窗使用缩放弹出(220ms)。三种动画的差异化运用让每个弹窗都有独特的视觉个性,避免了千篇一律的交互体验。
@Builder 装饰器在这个弹窗体系中发挥了关键作用。它将复杂的 UI 片段封装为可参数化的函数,modalOverlay 通过接受不同的 onClose 回调服务于所有 16 个弹窗的遮罩需求,bottomTabItem 通过接受不同的 icon、label、idx 参数生成 6 个 Tab 项。这种"参数化复用"大幅减少了代码重复,提高了可维护性。
@Prop 装饰器实现了父子组件之间的单向数据同步。RanchTab 接收 coin 和 exp,CropTab 接收 plotStates 和 cropNames,MarketTab 接收 coin——当父组件 Index 中的对应 @State 变化时,新值自动同步到子组件的 @Prop,触发子组件 UI 重新渲染。配合回调函数的"事件向上冒泡"机制,构成了完整的单向数据流闭环。
在视觉设计层面,应用围绕"草甸绿"主色调(#2E5D1F 到 #7CB342)构建了完整的色彩体系,辅以奶油黄(#FFD54F)、草莓红(#E53935)和橙色(#FB8C00)作为强调色。六个 Tab 页面使用不同色调的绿色渐变头部区分功能模块,集市页切换为橙色渐变强化"交易"语义,天气预警弹窗和出售确认弹窗分别使用深蓝灰和深红色调传达警示信息。linearGradient 属性被广泛用于头部背景、卡片背景和弹窗头部,通过角度和颜色停止点的精确控制实现了丰富的渐变效果。
Emoji 作为图标方案是这个应用的一个特色设计决策。所有动物、作物、商品、装饰、饲料、天气等视觉元素都使用 Emoji 字符表示,无需准备任何图片资源。这不仅极大简化了资源管理,还让应用具有天然的跨平台一致性——Emoji 在所有支持 Unicode 的设备上都能正确渲染。配合 fontSize 控制尺寸、fontColor 控制文字颜色,Emoji 方案在开发效率和视觉表现力之间取得了良好的平衡。
安装DevEco Studio程序
.padding({ top: 14, bottom: 14 })
.backgroundColor('#F1F8E9')
.borderRadius(14)
.margin({ right: 8 })
}, (a: Animal) => 'b' + a.id)
}
.width('100%')
.margin({ top: 8 })
}
}
.width('100%')
.height('100%')
.backgroundColor('#F4F9F0')
}
}
---

综上所述,这个云端牧场应用完整展示了鸿蒙 ArkTS 声明式 UI 开发的核心技术实践:接口驱动的类型安全数据建模、`@State`/`@Prop`/`@Builder` 装饰器构成的状态管理与组件复用体系、`Column`/`Row`/`Stack`/`Scroll`/`ForEach` 组合的布局与渲染能力、以及多风格弹窗体系的设计与实现。这些技术要点的组合运用,构成了一个功能完整、交互丰富、视觉精致的鸿蒙原生应用的开发全景。
更多推荐





所有评论(0)