鸿蒙操作系统(HarmonyOS)作为华为推出的面向万物互联时代的分布式操作系统,其应用开发框架ArkUI提供了一套全新的声明式UI开发范式。ArkTS作为ArkUI的主力开发语言,在TypeScript的基础上进行了深度定制与扩展,引入了诸如装饰器系统、状态管理机制、声明式组件构建语法等语言级特性,使得开发者能够以更加简洁、高效、类型安全的方式构建复杂的应用界面。本文将以一个恐龙乐园主题的员工管理应用为例,从类型定义、数据建模、状态管理、组件化架构、弹窗系统、列表渲染等多个维度,逐段剖析ArkTS在鸿蒙生态中的实践方法与设计哲学。

ArkTS语言的核心设计理念在于"声明式编程"与"状态驱动UI"。开发者只需声明界面在任意状态下的最终呈现形态,框架便会自动追踪状态变量的变化并在底层执行最小化的差异更新,从而保证UI渲染的一致性与性能。这一理念贯穿了鸿蒙应用开发的全部环节——从最基础的数据类型定义,到组件的状态注入,再到多层级组件树的编排与交互,无一不体现着这一思想。在本文解析的恐龙乐园应用中,我们可以清晰地看到这一设计哲学如何在一个包含六个功能Tab、十六个弹窗模块、十余种数据结构的复杂业务场景中落地生根。

鸿蒙的组件化开发思想深受现代前端框架的影响,但又具有自身鲜明的特色。ArkUI将界面元素分为两大类:容器组件与基础组件。容器组件(如Column、Row、Stack、Flex、Scroll等)负责组织子组件的布局结构,它们决定了子元素在屏幕空间中的排列方式、对齐策略与尺寸分配;基础组件(如Text、Image、Button等)则负责具体内容的呈现。通过容器与基础组件的嵌套组合,开发者可以构建出任意复杂的界面层级。此外,ArkUI还提供了@Builder装饰器用于抽取可复用的UI片段,@Component装饰器用于定义自定义组件,@Entry装饰器用于标记入口组件,这些语言级特性共同构成了鸿蒙声明式UI开发的核心工具链。

在状态管理方面,ArkTS提供了多层级的响应式状态装饰器。@State用于组件内部私有状态的管理,当被装饰的变量发生变化时,框架会自动重新渲染依赖该状态的UI部分;@Prop用于父组件向子组件单向传递数据;@Link用于父子组件间的双向数据绑定;@Provide与@Consume则用于跨层级的祖先-后代组件间的数据共享。在本应用中,主入口组件大量使用了@State来管理当前选中的Tab索引以及各个弹窗的显示/隐藏状态,通过布尔状态变量与条件渲染的配合,实现了一套完整的弹窗调度系统。

ArkTS 应用架构

类型定义层

数据层

全局函数层

组件层

接口定义 interface

颜色常量 COLORS

PARK_ZONES 园区数据

DINOS 恐龙数据

RIDES 游乐设施数据

DINNER_MENUS 菜品数据

其他业务数据集

计数函数 getXxxCount

过滤函数 getXxxLeft/Right

筛选函数 getTopXxx

Index 主入口组件

ParkTab 园区Tab

DinoTab 恐龙Tab

FunTab 游乐Tab

FoodTab 餐厅Tab

GiftTab 纪念Tab

MineTab 我的Tab

一、类型定义层:interface接口的系统性设计

在任何严肃的应用开发中,类型定义都是构建可靠系统的第一步。ArkTS继承了TypeScript强大的类型系统,通过interface关键字定义接口类型,为整个应用的数据流建立了一套严格的类型契约。本应用在文件开头集中定义了十三个接口类型,每一个接口都对应着一个具体的业务实体,这种集中式的类型定义方式有利于团队协作时的类型审查与代码维护。

ColorPalette:全局色彩体系接口

interface ColorPalette {
  bg: string;
  cardBg: string;
  deepBg: string;
  primary: string;
  secondary: string;
  accent: string;
  gold: string;
  danger: string;
  success: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  border: string;
  white: string;
  orange: string;
  purple: string;
}

在这里插入图片描述

ColorPalette接口定义了应用全局使用的色彩体系。这个接口包含了十六个字符串类型的颜色属性,覆盖了背景色、卡片背景色、深层背景色、主色调、辅助色、强调色、金色、危险色、成功色、文本主色、文本次色、文本提示色、边框色、白色、橙色和紫色等全部色彩维度。

这种将色彩体系抽象为接口的设计方式具有多重优势。首先,它使得色彩管理高度集中化,当需要切换主题或调整配色方案时,只需修改一处常量定义即可全局生效。其次,通过接口的类型约束,开发者在使用色彩时能够获得IDE的智能提示与类型检查,避免了拼写错误或使用未定义颜色的问题。最后,这种设计为后续的主题切换功能(如暗色模式、节日主题等)预留了扩展空间——只需提供不同的ColorPalette实例即可实现整套配色的切换。

在鸿蒙ArkUI开发中,色彩管理是一个容易被忽视却至关重要的工程实践。许多初学者习惯于在组件内直接硬编码颜色值(如’#4CAF50’),这种做法在小型项目中尚可接受,但随着应用规模增长,颜色散落在各处会导致维护成本急剧攀升。将色彩抽象为集中管理的常量对象,并辅以接口类型约束,是中大型ArkTS应用的最佳实践之一。

ParkZone:园区区域实体接口

interface ParkZone {
  id: number;
  name: string;
  icon: string;
  area: string;
  capacity: string;
  status: string;
  level: number;
  desc: string;
}

在这里插入图片描述

ParkZone接口描述了恐龙乐园中一个功能区域的数据结构。每个区域拥有数字类型的唯一标识id、字符串类型的名称name、图标emoji字符icon、所属区域分类area、接待能力capacity、运营状态status、等级level以及详细描述desc共八个字段。

这里值得注意的是icon字段使用字符串类型存储emoji表情符号。在ArkTS中,emoji字符可以直接作为Text组件的文本内容显示,无需引入额外的图标资源文件。这种做法在原型开发和内容丰富的应用中非常高效,既减少了资源管理的复杂度,又保证了视觉表现的一致性。level字段使用number类型而非string,体现了对数值型数据进行数值型存储的设计原则——当后续需要根据等级进行排序、筛选或数值计算时,number类型能够避免反复的类型转换。

Dino与Ride:恐龙与游乐设施接口

interface Dino {
  id: number;
  name: string;
  icon: string;
  kind: string;
  era: string;
  size: string;
  rating: number;
  protect: string;
  desc: string;
}

interface Ride {
  id: number;
  name: string;
  icon: string;
  area: string;
  wait: string;
  status: string;
  level: number;
  desc: string;
}

在这里插入图片描述

Dino接口定义了恐龙实体的数据结构,包含id、name、icon、kind(种类)、era(生存时代)、size(体长)、rating(人气评分)、protect(保护等级)和desc(描述)九个字段。Ride接口则定义了游乐设施的数据结构,包含id、name、icon、area(所在区域)、wait(排队时长)、status(运营状态)、level(刺激等级)和desc(描述)八个字段。

这两个接口的设计体现了一个共同的模式:数值型字段(如id、rating、level)使用number类型,文本型字段使用string类型,而排队时长wait和体长size这种在显示时需要带单位的字段则使用string类型存储(如"45 分"、“12m”)。这种设计选择是有道理的——带单位的字符串在直接显示时更为便捷,无需在模板中进行字符串拼接,但同时也牺牲了数值比较的能力(例如无法直接对wait字段进行数值排序)。这是业务需求与技术实现之间的一种折中取舍。

DinnerMenu与DinoTask:餐饮与任务接口

interface DinnerMenu {
  id: number;
  name: string;
  icon: string;
  price: number;
  sales: number;
  wait: string;
  type: string;
  desc: string;
}

interface DinoTask {
  id: number;
  title: string;
  icon: string;
  type: string;
  reward: number;
  status: string;
  time: string;
  zone: string;
}

在这里插入图片描述

DinnerMenu接口描述了餐厅菜品的数据结构。与前面的接口不同,这里price和sales两个字段都使用了number类型,这是因为价格和销量都需要参与数值计算——例如在弹窗中计算"月售 = sales * 12"时,number类型能够直接进行算术运算。DinoTask接口定义了园区任务的数据结构,reward字段使用number类型同样是为了在UI中进行数值运算(如reward % 100计算紧急程度等级)。

DinoRank、DinoNotice与DinoOrder接口

interface DinoRank {
  id: number;
  name: string;
  icon: string;
  title: string;
  score: number;
  feed: number;
  badge: string;
}

interface DinoNotice {
  id: number;
  title: string;
  date: string;
  level: string;
  content: string;
}

interface DinoOrder {
  id: number;
  no: string;
  item: string;
  from: string;
  to: string;
  status: string;
  eta: string;
  fee: number;
}

在这里插入图片描述

DinoRank接口定义了驯养师排行榜的数据结构,包含积分score和喂养次数feed两个数值型字段,分别用于排名计算和贡献度展示。DinoNotice接口定义了园区公告的数据结构,level字段存储公告级别(“重要”、“公告”、"提醒"等),在UI渲染时会根据该字段的值动态决定标签的背景颜色。DinoOrder接口定义了纪念品运单的数据结构,no字段是运单编号字符串,fee字段是配送费用数值,from和to分别表示发货门店和配送目标。

DinoSkill、DinoQuick、Souvenir与Keeper接口

interface DinoSkill {
  id: number;
  name: string;
  icon: string;
  level: number;
  exp: number;
  type: string;
  desc: string;
}

interface DinoQuick {
  id: number;
  name: string;
  icon: string;
  color: string;
}

interface Souvenir {
  id: number;
  name: string;
  icon: string;
  source: string;
  price: number;
  stock: number;
  rarity: string;
}

interface Keeper {
  id: number;
  name: string;
  icon: string;
  role: string;
  level: number;
  exp: number;
  feed: number;
  skill: string;
}

在这里插入图片描述

DinoSkill接口定义了驯养技能的数据结构,exp(经验值)字段使用number类型,在UI中直接作为柱状图高度的百分比值使用,体现了数据到视觉呈现的直接映射设计。DinoQuick接口是最简洁的接口之一,仅包含id、name、icon和color四个字段,用于快捷功能宫格的渲染,其中color字段存储十六进制颜色值,使每个快捷功能都能拥有独立的主题色调。Souvenir接口定义了纪念品的数据结构,rarity字段表示稀有度等级(“明星”、“稀有”、“常见”、"传说"等)。Keeper接口定义了驯养师档案的数据结构,是字段最多的接口之一,包含了等级level、经验值exp、喂养次数feed和擅长技能skill等关键信息。

接口的系统化定义是大型ArkTS应用的基石。通过为每个业务实体定义专门的接口,开发者建立了从数据源到UI渲染的完整类型安全链路。在编译阶段,TypeScript编译器能够检查所有数据访问是否符合接口定义,从而在早期发现潜在的类型错误。这种"类型先行"的开发模式在鸿蒙生态中尤为重要,因为ArkTS的声明式UI语法使得数据与视图的绑定关系更加紧密,任何类型不匹配都可能导致运行时异常。

二、颜色常量与数据集定义

COLORS常量:色彩体系的实例化

const COLORS: ColorPalette = {
  bg: '#122B1A',
  cardBg: '#1B3A24',
  deepBg: '#0A1E10',
  primary: '#4CAF50',
  secondary: '#8BC34A',
  accent: '#FF7043',
  gold: '#FFD166',
  danger: '#FF6B6B',
  success: '#4CD97B',
  textPrimary: '#EAF5EA',
  textSecondary: '#9FC49F',
  textHint: '#5E8A5E',
  border: '#2B5234',
  white: '#FFFFFF',
  orange: '#FFA94D',
  purple: '#B39DDB'
};

COLORS常量是ColorPalette接口的唯一实例,它为整个应用提供了一套完整的深绿色主题色彩方案。背景色bg使用深绿色’#122B1A’,卡片背景cardBg使用稍浅的’#1B3A24’,深层背景deepBg使用更深的’#0A1E10’,这三种背景色共同构建了一个具有层次感的暗色视觉空间。主色调primary使用Material Design的绿色’#4CAF50’,辅助色secondary使用更浅的草绿色’#8BC34A’,强调色accent使用橙红色’#FF7043’,金色gold使用暖黄’#FFD166’。

这套配色方案的设计灵感明显来自自然丛林主题——深绿色的背景模拟了密林深处的幽暗环境,而橙色和金色的强调色则代表了火山、阳光和恐龙蛋等温暖元素。文本色彩的层级设计也很有层次感:textPrimary使用接近白色的浅绿色’#EAF5EA’,保证在深色背景上的高对比度可读性;textSecondary使用中等灰绿色’#9FC49F’,用于次要信息;textHint使用较暗的’#5E8A5E’,用于提示性文字。这种三级文本色彩体系是Material Design暗色主题的最佳实践。

园区区域数据集

const PARK_ZONES: ParkZone[] = [
  { id: 1, name: '苍翠丛林区', icon: '🌴', area: '户外', capacity: '1200 人', status: '开放中', level: 5, desc: '园区主入口,模拟白垩纪雨林,巨型蕨类与恐龙雕塑掩映其间。' },
  { id: 2, name: '火山谷地', icon: '🌋', area: '主题区', capacity: '800 人', status: '开放中', level: 4, desc: '真实喷烟火山造景,过山车沿火山口呼啸而过。' },
  { id: 3, name: '恐龙化石馆', icon: '🦴', area: '室内馆', capacity: '600 人', status: '开放中', level: 4, desc: '馆藏 40 具完整化石骨架,含互动修复体验区。' },
  { id: 4, name: '孵蛋实验室', icon: '🥚', area: '科教区', capacity: '300 人', status: '维护中', level: 3, desc: '恒温孵化间展示恐龙蛋保育全程,本周设备升级暂停开放。' },
  // ...更多区域
];

PARK_ZONES是一个ParkZone类型的数组常量,包含了八个园区区域的完整数据。每条数据都严格按照ParkZone接口定义的字段结构填充,TypeScript编译器会在编译阶段检查每条数据是否符合接口约束。这里使用const声明确保数据集在运行时不可被重新赋值,提供了一层额外的安全性保障。

数据集的设计遵循了"id从1开始递增"的约定,这一约定在后续的全局函数中起到了关键作用——许多函数通过id的奇偶性来将数据集拆分为左右两列,通过id是否小于等于某个值来筛选Top N数据。这种基于id的数据分片策略简洁高效,避免了引入额外的分组逻辑。

恐龙与游乐设施数据集

const DINOS: Dino[] = [
  { id: 1, name: '霸王龙', icon: '🦖', kind: '兽脚类', era: '白垩纪', size: '12m', rating: 9.9, protect: '明星', desc: '园区人气之王,每日 3 场投喂秀场场爆满。' },
  { id: 2, name: '剑龙', icon: '🦕', kind: '鸟臀类', era: '侏罗纪', size: '9m', rating: 9.2, protect: '一级', desc: '背板如帆的素食者,性格温顺爱晒太阳。' },
  // ...更多恐龙
];

const RIDES: Ride[] = [
  { id: 1, name: '火山过山车', icon: '🎢', area: '火山谷', wait: '45 分', status: '运营中', level: 5, desc: '穿越火山口的双环过山车,全程 3 分钟。' },
  { id: 2, name: '翼龙飞行模拟', icon: '🕶', area: '翼龙峡谷', wait: '20 分', status: '运营中', level: 4, desc: 'VR 体感飞行,体验翼龙视角俯冲峡谷。' },
  // ...更多游乐设施
];

在这里插入图片描述

DINOS数组包含了八种恐龙的数据,涵盖了霸王龙、剑龙、三角龙、翼龙、腕龙、迅猛龙、甲龙和副栉龙等经典物种。每条数据都包含了种类、生存时代、体长、人气评分、保护等级和描述信息,为UI渲染提供了丰富的展示素材。RIDES数组同样包含八条游乐设施数据,从刺激等级最高的火山过山车到适合低龄儿童的蹦床恐龙蛋,覆盖了不同年龄层和刺激偏好的需求。

在鸿蒙应用开发中,静态数据集通常使用const常量数组的方式定义。这种做法的优势在于数据与逻辑分离、类型安全、易于维护。当数据需要从网络请求获取时,只需将const常量替换为@State变量并在生命周期函数中异步赋值即可,UI层的渲染逻辑无需任何改动。这种"数据源无关"的设计是声明式UI框架的核心优势之一。

餐饮、任务、排行与其他数据集

const DINNER_MENUS: DinnerMenu[] = [
  { id: 1, name: '霸王龙烤肉套餐', icon: '🍖', price: 88, sales: 92, wait: '15 分', type: '主食', desc: '炭烤厚切肉排,配火山熔岩薯角与丛林沙拉。' },
  // ...更多菜品
];

const DINO_TASKS: DinoTask[] = [
  { id: 1, title: '喂食霸王龙幼崽', icon: '🦖', type: '饲养任务', reward: 460, status: '进行中', time: '今 17:00', zone: '保育院' },
  // ...更多任务
];

const DINO_RANKS: DinoRank[] = [
  { id: 1, name: '雷恩·火山', icon: '🌋', title: '首席驯养师', score: 17200, feed: 1890, badge: '火山勋章' },
  // ...更多排行数据
];

在这里插入图片描述

DINNER_MENUS数组定义了八款餐厅菜品数据,包含主食、小食、甜品和饮品四个类别。每条数据中sales字段表示热销度(0-100的数值),在UI中直接映射为进度条的宽度百分比,是一个数据到视觉的直接映射设计。DINO_TASKS数组定义了八条园区任务数据,status字段区分"进行中"和"已完成"两种状态,用于在"我的"页面中分别展示为任务卡片和已完成任务行。DINO_RANKS数组定义了驯养师排行榜数据,score字段用于排名,feed字段表示喂养次数贡献。

其余的DINO_NOTICES(公告)、DINO_ORDERS(运单)、DINO_SKILLS(技能)、DINO_QUICKS(快捷功能)、SOUVENIRS(纪念品)和KEEPER(驯养师)数据集也以相同的模式定义,每个数据集都严格遵循对应的接口类型约束,为应用的不同功能模块提供数据支撑。

组件消费

全局函数

数据集

PARK_ZONES

DINOS

RIDES

DINNER_MENUS

DINO_TASKS

DINO_RANKS

DINO_NOTICES

DINO_ORDERS

DINO_SKILLS

DINO_QUICKS

SOUVENIRS

KEEPER

getXxxCount 计数

getXxxLeft/Right 分列

getTopXxx 筛选

getRunningTasks 等 状态过滤

ParkTab

DinoTab

FunTab

FoodTab

GiftTab

MineTab

三、全局函数层:数据预处理与视图适配

计数函数与分列函数

function getZoneCount(): number {
  return PARK_ZONES.length
}
function getZoneLeft(): ParkZone[] {
  return PARK_ZONES.filter((z: ParkZone) => z.id % 2 === 1)
}
function getZoneRight(): ParkZone[] {
  return PARK_ZONES.filter((z: ParkZone) => z.id % 2 === 0)
}

全局函数层是数据集与UI组件之间的适配层。getZoneCount函数简单地返回PARK_ZONES数组的长度,用于在横幅文字中显示"开放区域 N 个"的统计信息。getZoneLeft和getZoneRight函数使用数组的filter方法,通过id的奇偶性将数据集拆分为两个子数组,分别用于双列布局的左列和右列渲染。

这种基于id奇偶性的分列策略是一种简洁但有效的数据分片方法。它假设数据集的id是从1开始连续递增的整数,因此filter后左右两列的数据数量大致均等。这种方法的优点是无需额外的分页参数,逻辑简单直观;缺点是当数据集id不连续时可能导致两列数量不均。在本应用中,由于所有数据集的id都是严格从1递增的连续整数,这一策略能够完美工作。

在ArkTS中,数组的filter方法接收一个谓词函数(predicate function)作为参数,返回一个满足条件的新数组。filter不会修改原数组,而是产生一个浅拷贝,这保证了数据集的不可变性(immutability)。在声明式UI框架中,数据不可变性是一个重要的原则——当数据需要被多个组件共享读取时,不可变性确保了任何组件都无法意外修改原始数据,从而避免了难以追踪的状态污染问题。

Top筛选函数

function getTopZones(): ParkZone[] {
  return PARK_ZONES.filter((z: ParkZone) => z.level >= 4)
}
function getTopDinos(): Dino[] {
  return DINOS.filter((d: Dino) => d.rating >= 9.4)
}
function getTopRides(): Ride[] {
  return RIDES.filter((r: Ride) => r.level >= 4)
}
function getTopMenus(): DinnerMenu[] {
  return DINNER_MENUS.filter((m: DinnerMenu) => m.sales >= 80)
}

getTopZones、getTopDinos、getTopRides和getTopMenus这四个函数分别从各自的数据集中筛选出"王牌"或"热销"项目。它们的共同模式是使用filter方法配合一个数值阈值条件:区域筛选level >= 4的高等级区域,恐龙筛选rating >= 9.4的高人气恐龙,游乐设施筛选level >= 4的高刺激设施,菜品筛选sales >= 80的高热销菜品。

这些筛选函数的返回值直接用于各个Tab页面中的"横滑卡片"区域——即那些可以水平滚动的精选内容区。这种设计将"什么是精选"的判断逻辑集中在了函数层,而非散落在组件的build方法中,当筛选标准需要调整时(例如将rating阈值从9.4改为9.0),只需修改一处函数实现即可全局生效。

状态过滤与排名拆分函数

function getRunningTasks(): DinoTask[] {
  return DINO_TASKS.filter((t: DinoTask) => t.status === '进行中')
}
function getDoneTasks(): DinoTask[] {
  return DINO_TASKS.filter((t: DinoTask) => t.status === '已完成')
}
function getRankTop(): DinoRank[] {
  return DINO_RANKS.filter((r: DinoRank) => r.id <= 3)
}
function getRankRest(): DinoRank[] {
  return DINO_RANKS.filter((r: DinoRank) => r.id > 3)
}

getRunningTasks和getDoneTasks函数通过status字段将任务数据集拆分为"进行中"和"已完成"两个子集,分别用于"我的"页面中的值班任务卡片区域和已完成任务行区域。getRankTop和getRankRest函数通过id是否小于等于3将排行榜数据拆分为"前三名"和"其余名次"两个子集——前三名使用领奖台样式渲染(不同高度的柱状图),其余名次使用列表行样式渲染。

这种"同数据不同视图"的设计模式在UI开发中非常常见。同一份排行榜数据,根据排名的不同区间,采用了完全不同的视觉呈现方式:前三名使用领奖台柱状图强调荣誉感,四名以后使用简洁的列表行展示信息密度。这种设计将数据拆分逻辑封装在函数中,使得组件代码更加专注于视图表达。

技能解锁与订单过滤函数

function getUnlockedSkills(): DinoSkill[] {
  return DINO_SKILLS.filter((s: DinoSkill) => s.exp >= 30)
}
function getOrderActive(): DinoOrder[] {
  return DINO_ORDERS.filter((o: DinoOrder) => o.status !== '已完成')
}
function getQuickLeft(): DinoQuick[] {
  return DINO_QUICKS.filter((q: DinoQuick) => q.id <= 4)
}
function getQuickRight(): DinoQuick[] {
  return DINO_QUICKS.filter((q: DinoQuick) => q.id > 4)
}

getUnlockedSkills函数筛选经验值大于等于30的已解锁技能,用于技能弹窗中的柱状图展示。getOrderActive函数筛选所有未完成的订单(状态不等于"已完成"),用于在纪念品Tab和我的页面中展示进行中的运单。getQuickLeft和getQuickRight函数将八个快捷功能按id前四个和后四个拆分,分别渲染在物资申领弹窗的左右两列中。

这些全局函数共同构成了一个数据预处理层。它们的存在使得组件的build方法可以保持简洁——组件只需调用对应的函数获取已经过筛选和分片的数据,然后直接渲染即可,无需在视图代码中嵌入数据过滤逻辑。这种关注点分离(Separation of Concerns)的实践是软件工程中经典的设计原则,在ArkTS的声明式UI开发中同样适用。

四、主入口组件Index:应用骨架与状态中枢

组件声明与状态变量

@Entry
@Component
struct Index {
  @State currentTab: number = 0
  @State showZone: boolean = false
  @State showVisit: boolean = false
  @State showDino: boolean = false
  @State showFeed: boolean = false
  @State showRide: boolean = false
  @State showRank: boolean = false
  @State showNotice: boolean = false
  @State showOrder: boolean = false
  @State showSkill: boolean = false
  @State showMenu: boolean = false
  @State showSouvenir: boolean = false
  @State showBook: boolean = false
  @State showKeeper: boolean = false
  @State showVip: boolean = false
  @State showSupply: boolean = false
  @State showTask: boolean = false

@Entry装饰器标记Index为应用的入口组件——每个ArkTS页面有且仅有一个被@Entry标记的组件,它是组件树的根节点。@Component装饰器声明Index为一个自定义组件,使其可以被其他组件引用或在build方法中使用。

Index组件声明了十七个@State状态变量。currentTab是数字类型,初始值为0,表示当前激活的Tab索引(0到5分别对应六个底部Tab)。其余十六个showXxx变量都是布尔类型,初始值为false,分别控制十六个弹窗的显示与隐藏。

@State是ArkTS中最基础的状态管理装饰器。当被@State装饰的变量发生变化时,框架会自动触发该组件build方法的重新执行,并对比新旧UI树的差异,仅更新发生变化的部分。这种基于状态驱动的响应式更新机制是ArkUI声明式范式的核心。在本应用中,十六个布尔状态变量构成了一套完整的弹窗调度系统——当用户点击某个列表项时,对应的回调函数会将selXxx设为被点击的数据项,同时将showXxx设为true,弹窗随即渲染显示;当用户点击遮罩层或关闭按钮时,showXxx被设为false,弹窗消失。

选中数据变量

  selZone: ParkZone | null = null
  selDino: Dino | null = null
  selRide: Ride | null = null
  selRank: DinoRank | null = null
  selNotice: DinoNotice | null = null
  selOrder: DinoOrder | null = null
  selSkill: DinoSkill | null = null
  selMenu: DinnerMenu | null = null
  selSouvenir: Souvenir | null = null
  selKeeper: Keeper | null = null
  selVip: Keeper | null = null
  selQuick: DinoQuick | null = null
  selTask: DinoTask | null = null

除了@State状态变量外,Index组件还声明了十三个selXxx变量,类型为对应的接口类型联合null(如ParkZone | null)。这些变量没有被@State装饰,因此它们的变化不会直接触发UI更新。它们的作用是存储当前被选中以供弹窗显示的数据项——当showXxx为true时,对应的selXxx应该已被赋值为非null的数据项,弹窗Builder通过this.selXxx!.xxx的方式读取数据并渲染。

这种"show布尔变量 + sel数据变量"的成对设计模式是本应用弹窗系统的核心设计。show变量控制弹窗是否显示,sel变量存储弹窗需要渲染的具体数据。两者在回调函数中同时被赋值:先设置sel为被点击的数据项,再设置show为true。在弹窗关闭时,只需设置show为false即可,sel变量的值会在下次打开时被覆盖。

使用联合类型(如ParkZone | null)而非简单的ParkZone类型,使得这些变量可以在初始状态下安全地为null。在弹窗的build方法中,通过非空断言操作符(!)来访问属性:this.selZone!.name。这种模式要求开发者在逻辑上确保showXxx为true时selXxx必定不为null,虽然非空断言绕过了编译器的null检查,但通过showXxx && this.selXxx !== null的条件判断在弹窗挂载点保证了这一约束。

build方法:整体布局结构

  build() {
    Column() {
      // 头部横幅
      // 内容区
      // 底部Tab
      // 弹窗挂载
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS.bg)
  }

build方法是每个@Component组件必须实现的方法,它描述了组件的UI结构。Index组件的build方法使用Column容器作为最外层布局,将整个界面从上到下分为三个主要区域:头部横幅、内容区和底部Tab栏,并在最外层Column的末尾挂载所有弹窗组件。

Column是ArkUI中最基础的垂直布局容器组件。它将其子组件按照从上到下的顺序垂直排列,子组件之间可以通过space参数指定间距。在Index的最外层Column中,没有设置space参数,因为三个主要区域的间距由各自的内边距(padding)来控制。最外层Column设置了width(‘100%’)和height(‘100%’),确保它占满整个屏幕,并使用COLORS.bg作为背景色。

头部横幅区域

      Column({ space: 10 }) {
        Row() {
          Column({ space: 2 }) {
            Text('DINO PARK').fontSize(12).fontColor(COLORS.gold).fontWeight(FontWeight.Bold).letterSpacing(2)
            Text('恐龙乐园 · 员工亲子之家').fontSize(19).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Text('🔔').fontSize(20)
          Text('4').fontSize(10).fontColor(COLORS.white).backgroundColor(COLORS.danger).borderRadius(8).width(16).height(16).textAlign(TextAlign.Center)
        }
        .width('100%')
        Row({ space: 8 }) {
          Text('🔍').fontSize(14)
          Text('搜索园区 / 恐龙 / 美食').fontSize(13).fontColor(COLORS.textSecondary)
        }
        .width('100%')
        .height(40)
        .padding({ left: 14, right: 14 })
        .backgroundColor('rgba(255,255,255,0.10)')
        .borderRadius(20)
      }
      .padding({ left: 16, right: 16, top: 14, bottom: 14 })
      .width('100%')
      .linearGradient({ angle: 135, colors: [['#0A1E10', 0.0], ['#1B3A24', 0.55], ['#4CAF50', 1.0]] })

头部横幅区域是一个嵌套了两个Row的Column容器。外层Column设置了space为10,控制两个Row之间的垂直间距。第一个Row是标题行,包含一个嵌套的Column(标题文字区)、一个铃铛emoji和一个消息数角标。标题区的Column使用alignItems(HorizontalAlign.Start)使文字左对齐,layoutWeight(1)使其占据剩余水平空间,将铃铛和角标推到右侧。

这里Text组件的链式属性调用方式是ArkTS声明式UI的标志性语法。Text(‘DINO PARK’).fontSize(12).fontColor(COLORS.gold).fontWeight(FontWeight.Bold).letterSpacing(2)这一行代码同时设置了文本内容、字号、颜色、粗细和字间距五个属性。这种链式调用风格使得UI描述紧凑而富有表现力,每个Text组件的视觉规格都在一行代码内完整声明。

第二个Row是搜索框区域,使用半透明白色背景(rgba(255,255,255,0.10))和20的圆角值,模拟了一个搜索输入框的外观。虽然这里只是用Text组件显示了占位文字而非真正的TextInput输入框,但视觉上已经达到了搜索框的效果。

linearGradient属性为整个头部Column设置了线性渐变背景。angle: 135表示渐变方向从左上到右下135度角,colors数组定义了三个渐变停止点:0%位置为深色’#0A1E10’,55%位置为中等深绿’#1B3A24’,100%位置为主色调绿色’#4CAF50’。这种三段式渐变创造了从暗到亮的视觉过渡,增强了头部区域的空间深度感。

内容区与Tab切换逻辑

      Column() {
        if (this.currentTab === 0) {
          ParkTab({
            onZone: (z: ParkZone) => {
              this.selZone = z
              this.showZone = true
            },
            onVisit: (z: ParkZone) => {
              this.selZone = z
              this.showVisit = true
            },
            onRank: (r: DinoRank) => {
              this.selRank = r
              this.showRank = true
            },
            onNotice: (n: DinoNotice) => {
              this.selNotice = n
              this.showNotice = true
            }
          })
        } else if (this.currentTab === 1) {
          DinoTab({
            onDino: (d: Dino) => {
              this.selDino = d
              this.showDino = true
            },
            onFeed: (d: Dino) => {
              this.selDino = d
              this.showFeed = true
            },
            onTask: (t: DinoTask) => {
              this.selTask = t
              this.showTask = true
            }
          })
        } else if (this.currentTab === 2) {
          // FunTab...
        }
        // ...其他Tab
      }
      .layoutWeight(1)

内容区是一个Column容器,通过layoutWeight(1)占据头部和底部之间的所有剩余垂直空间。Column内部使用if-else if条件语句,根据this.currentTab的值决定渲染哪个Tab组件。当currentTab为0时渲染ParkTab(园区页),为1时渲染DinoTab(恐龙页),以此类推。

这里展示了ArkTS声明式UI中条件渲染的核心语法。在build方法内部,可以使用标准的JavaScript if-else语句来控制组件的条件渲染。当currentTab的值发生变化时(通过点击底部Tab触发),框架会自动重新执行build方法,销毁之前的Tab组件实例并创建新的Tab组件实例。

更值得关注的是父子组件间的回调通信模式。以ParkTab为例,它接收四个回调函数参数:onZone、onVisit、onRank和onNotice。当ParkTab内部的某个区域卡片被点击时,它会调用this.onZone(z)将点击的区域数据传递回父组件Index。父组件在回调函数中执行两步操作:将数据项赋值给selZone状态变量,将showZone设为true触发弹窗显示。这种"子组件触发回调 -> 父组件更新状态 -> 弹窗渲染"的数据流是ArkTS组件间通信的标准模式。

弹窗Builder Index主组件 Tab组件 用户 弹窗Builder Index主组件 Tab组件 用户 @State变化触发重渲染 @State变化触发重渲染 点击列表项卡片 调用回调函数 onXxx(data) selXxx = data showXxx = true 条件判断 showXxx && selXxx !== null 渲染弹窗内容 点击遮罩层或关闭按钮 onClose回调 showXxx = false 条件不满足,弹窗消失

底部Tab栏

      Row() {
        this.bottomTabItem('🏕', '园区', 0)
        this.bottomTabItem('🦖', '恐龙', 1)
        this.bottomTabItem('🎢', '游乐', 2)
        this.bottomTabItem('🍖', '餐厅', 3)
        this.bottomTabItem('🎁', '纪念', 4)
        this.bottomTabItem('👤', '我的', 5)
      }
      .width('100%')
      .height(64)
      .backgroundColor('#0A1E10')
      .border({ width: 1, color: '#2B5234' })

底部Tab栏是一个Row容器,高度64像素,背景色为最深的’#0A1E10’,顶部有一条1像素的边框线。Row内部调用了六次this.bottomTabItem方法,每次传入不同的图标emoji、标签文字和索引值。这里使用了@Builder方法来复用Tab项的UI结构——bottomTabItem是一个被@Builder装饰的方法,它根据传入的参数生成一个Tab项的UI。

底部Tab栏是移动应用中最常见的导航模式之一。在本应用中,六个Tab分别对应园区、恐龙、游乐、餐厅、纪念和我的六个功能页面。Tab项的激活状态通过bottomTabItem Builder内部的scale和opacity属性来视觉区分——激活的Tab项会被放大1.1倍并完全不透明,未激活的Tab项保持原始大小并只有45%的不透明度。

弹窗挂载系统

      if (this.showZone && this.selZone !== null) {
        this.modalOverlay(() => { this.showZone = false })
        this.zoneModal()
      }
      if (this.showVisit && this.selZone !== null) {
        this.modalOverlay(() => { this.showVisit = false })
        this.visitModal()
      }
      if (this.showDino && this.selDino !== null) {
        this.modalOverlay(() => { this.showDino = false })
        this.dinoModal()
      }
      // ...其余弹窗挂载点

弹窗挂载系统位于最外层Column的末尾,是所有弹窗的统一入口。每个弹窗由两个@Builder方法组合而成:modalOverlay(半透明遮罩层)和对应的xxxModal(弹窗内容)。每个弹窗的挂载条件都是"对应的show布尔变量为true且sel数据变量不为null"——只有同时满足这两个条件,弹窗才会被渲染到界面上。

modalOverlay接收一个关闭回调函数作为参数,当用户点击遮罩层时执行该回调将show变量设为false。弹窗内容Builder则通过this.selXxx!.xxx的方式读取选中数据项的属性进行渲染。这种"遮罩 + 内容"的分离设计使得遮罩层逻辑可以完全复用——十六个弹窗共用同一个modalOverlay Builder,只需传入不同的关闭回调即可。

这种弹窗系统的设计有一个巧妙之处:弹窗挂载点位于最外层Column的末尾,而最外层Column没有设置justifyContent属性(默认为FlexAlign.Start,即从顶部开始排列)。弹窗的遮罩层使用了position({ x: 0, y: 0 })绝对定位属性,使其脱离文档流覆盖整个屏幕。弹窗内容则根据各自的justifyContent设置决定在遮罩层中的位置——居中弹窗使用FlexAlign.Center,底部抽屉使用FlexAlign.End。这种设计巧妙地利用了绝对定位和Flex对齐的组合来实现弹窗的定位。

五、@Builder方法详解

bottomTabItem:底部Tab项构建器

  @Builder
  bottomTabItem(icon: string, label: string, idx: number) {
    Column({ space: 2 }) {
      Text(icon).fontSize(18)
      Text(label).fontSize(10)
    }
    .width('16.6%')
    .justifyContent(FlexAlign.Center)
    .scale({ x: this.currentTab === idx ? 1.1 : 1.0, y: this.currentTab === idx ? 1.1 : 1.0 })
    .opacity(this.currentTab === idx ? 1 : 0.45)
    .onClick(() => {
      this.currentTab = idx
    })
  }

@Builder装饰器用于将一段UI构建逻辑抽取为可复用的方法。bottomTabItem方法接收三个参数:icon(图标emoji字符串)、label(标签文字)和idx(Tab索引值)。方法体内部构建了一个Column容器,包含图标和标签两个Text组件。

这个Builder的核心亮点在于动态样式绑定。scale属性接收一个包含x和y分量的对象,当this.currentTab === idx为true时(即当前Tab是此Tab项对应的Tab),缩放比例为1.1(放大10%);否则为1.0(原始大小)。opacity属性同样根据当前Tab索引与传入索引的比较结果,在1.0(完全不透明)和0.45(半透明)之间切换。这种根据状态动态改变样式的能力是声明式UI框架的核心特性。

onClick事件处理器将this.currentTab设置为被点击Tab项的索引值idx。由于currentTab是@State变量,赋值后框架会自动触发build方法的重新执行,内容区的条件渲染语句会渲染新的Tab组件,底部Tab栏的各项样式也会更新——被选中的Tab项放大且不透明,其余Tab项恢复正常大小且半透明。

@Builder与@Component的区别在于:@Builder用于抽取组件内部的UI片段,它不能独立存在,必须依附于某个@Component组件;@Component则是独立的可复用组件,可以拥有自己的状态和生命周期。在本应用中,bottomTabItem被定义为Index组件的@Builder方法,因为它需要直接访问Index的@State变量currentTab,且只在Index组件内部使用。如果这个Tab项需要在多个组件中复用且不依赖父组件状态,则应该定义为独立的@Component。

modalOverlay:弹窗遮罩层构建器

  @Builder
  modalOverlay(onClose: () => void) {
    Column() {
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(6,18,10,0.78)')
    .onClick(() => {
      onClose()
    })
    .position({ x: 0, y: 0 })
  }

modalOverlay是一个高度复用的@Builder方法,它接收一个onClose回调函数作为参数,生成一个覆盖全屏的半透明遮罩层。遮罩层是一个空的Column容器,设置了100%的宽高、深色半透明背景(rgba(6,18,10,0.78))和绝对定位(position: { x: 0, y: 0 })。

绝对定位是遮罩层实现的关键。在ArkUI中,position属性将组件从正常的文档流中脱离出来,使其相对于父容器的左上角进行定位。通过设置x: 0, y: 0以及100%的宽高,遮罩层能够覆盖整个父容器(即最外层Column占满的整个屏幕区域)。遮罩层的onClick事件绑定了onClose回调,当用户点击遮罩层空白区域时触发关闭逻辑。

这个Builder是整个弹窗系统中复用频率最高的方法——十六个弹窗中的每一个都调用了this.modalOverlay(…)来生成遮罩层,只需传入各自不同的关闭回调函数。这种设计极大地减少了代码重复,同时也保证了所有弹窗的遮罩层视觉行为一致性。

sectionTitle:通用区块标题构建器

  @Builder
  sectionTitle(title: string, more: string) {
    Row() {
      Text(title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary).layoutWeight(1)
      Text(more).fontSize(11).fontColor(COLORS.gold)
    }
    .width('100%')
    .margin({ top: 10, bottom: 8 })
  }

sectionTitle是一个通用的区块标题Builder,接收标题文字和"更多"文字两个参数,生成一个左右分布的Row——左侧是较粗的标题文字,右侧是较小的金色"更多"链接文字。layoutWeight(1)使标题占据剩余空间,将"更多"文字推到右侧。

这个Builder体现了@Builder方法在UI一致性方面的价值。在内容丰富的列表页面中,每个内容区块通常都有一个标题行,如果每次都手动编写Row + Text的结构,不仅代码冗余,还容易导致样式不一致。通过将标题行抽取为Builder方法,可以确保所有区块标题的字号、颜色、间距保持统一,同时也方便后续的全局样式调整。

六、弹窗系统逐一解析

弹窗1:园区区域详情弹窗

  @Builder
  zoneModal() {
    Column() {
      Column({ space: 10 }) {
        Row() {
          Text(this.selZone!.icon).fontSize(38).width(70).height(70).textAlign(TextAlign.Center).backgroundColor('rgba(76,175,80,0.16)').borderRadius(18)
          Column({ space: 3 }) {
            Text(this.selZone!.name).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
            Text(this.selZone!.area + ' | Lv.' + this.selZone!.level).fontSize(12).fontColor(COLORS.secondary)
            Text('🕐 当前状态:' + this.selZone!.status).fontSize(11).fontColor(COLORS.gold)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
          Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showZone = false })
        }
        .width('100%')

zoneModal是园区区域详情弹窗的内容Builder。弹窗整体使用居中布局(justifyContent: FlexAlign.Center),内容卡片宽度为88%,背景色为COLORS.cardBg,圆角16,内边距16。

弹窗内容的第一行是一个Row,包含三个部分:左侧的图标方块、中间的文字信息和右侧的关闭按钮。图标方块使用70x70的尺寸、圆角18和半透明绿色背景,通过textAlign(TextAlign.Center)使emoji居中显示。中间的Column包含区域名称(18号粗体字)、区域分类与等级信息(12号辅助色)和当前状态(11号金色)三行文字,通过字符串拼接将多个数据字段组合为一行展示文本。

右侧的关闭按钮是一个简单的’✕’文字,绑定了onClick事件将this.showZone设为false。这是弹窗关闭的另一种方式——除了点击遮罩层关闭外,用户也可以通过点击弹窗内的关闭按钮来关闭弹窗。

        Row({ space: 8 }) {
          Column({ space: 3 }) {
            Text('接待能力').fontSize(10).fontColor(COLORS.textSecondary)
            Text(this.selZone!.capacity).fontSize(15).fontColor(COLORS.primary).fontWeight(FontWeight.Bold)
          }
          .layoutWeight(1)
          .padding(10)
          .backgroundColor('rgba(76,175,80,0.10)')
          .borderRadius(10)
          Column({ space: 3 }) {
            Text('所属区域').fontSize(10).fontColor(COLORS.textSecondary)
            Text(this.selZone!.area).fontSize(13).fontColor(COLORS.secondary).fontWeight(FontWeight.Bold)
          }
          .layoutWeight(1)
          .padding(10)
          .backgroundColor('rgba(139,195,74,0.10)')
          .borderRadius(10)
          Column({ space: 3 }) {
            Text('开放状态').fontSize(10).fontColor(COLORS.textSecondary)
            Text(this.selZone!.status).fontSize(12).fontColor(COLORS.gold).fontWeight(FontWeight.Bold)
          }
          .layoutWeight(1)
          .padding(10)
          .backgroundColor('rgba(255,209,102,0.10)')
          .borderRadius(10)
        }
        .width('100%')

弹窗的第二行是三个等宽的信息卡片,使用Row容器水平排列,每个Column通过layoutWeight(1)等分宽度。每个信息卡片的上半部分是10号灰色标签文字,下半部分是较大字号的粗体数值文字。三个卡片分别使用不同的半透明背景色——绿色、浅绿色和金色——对应主色调、辅助色和金色三个色彩维度,形成了视觉上的区分。

这种"标签 + 数值"的信息卡片模式在移动应用的信息展示中非常常见。它将数据以结构化的方式呈现,每个卡片聚焦一个维度的信息,通过统一的布局和差异化的色彩让用户快速扫描和定位所需信息。layoutWeight(1)确保三个卡片等宽分布,padding(10)提供了卡片内部的呼吸空间。

        Text(this.selZone!.desc).fontSize(12).fontColor(COLORS.textSecondary).lineHeight(20)
        Row({ space: 8 }) {
          Text('🎫 预约参观').fontSize(13).fontColor(COLORS.white).fontWeight(FontWeight.Bold).textAlign(TextAlign.Center).width('50%')
            .padding({ top: 12, bottom: 12 })
            .backgroundColor(COLORS.primary)
            .borderRadius(22)
            .onClick(() => {
              this.showZone = false
            })
          Text('🗺 查看导览').fontSize(13).fontColor(COLORS.white).fontWeight(FontWeight.Bold).textAlign(TextAlign.Center).width('50%')
            .padding({ top: 12, bottom: 12 })
            .backgroundColor(COLORS.accent)
            .borderRadius(22)
            .onClick(() => {
              this.showZone = false
            })
        }
        .width('100%')

弹窗的底部是描述文字和操作按钮区。描述文字使用12号灰色字体,lineHeight(20)设置了20的行高,使多行文字的阅读体验更加舒适。操作按钮区包含两个等宽按钮——"预约参观"使用主色调绿色背景,"查看导览"使用强调色橙色背景——两个按钮都设置了22的圆角值和上下12的内边距,形成了胶囊形状的按钮外观。

两个按钮的onClick事件目前都是简单地关闭弹窗(将showZone设为false),这是原型开发阶段的常见做法。在实际应用中,这里应该分别导航到预约页面和导览地图页面。

弹窗2:参观预约底部抽屉

  @Builder
  visitModal() {
    Column() {
      Column({ space: 12 }) {
        Row() {
          Column({ space: 2 }) {
            Text('🎫 参观预约').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
            Text('选择到访时段,入园核销预约码').fontSize(11).fontColor(COLORS.gold)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showVisit = false })
        }
        .width('100%')

visitModal与zoneModal在布局策略上有一个关键区别:visitModal使用justifyContent(FlexAlign.End)将弹窗内容推到屏幕底部,形成"底部抽屉"的交互模式;而zoneModal使用justifyContent(FlexAlign.Center)使弹窗居中显示。此外,visitModal的内容Column设置了borderRadius({ topLeft: 20, topRight: 20 })仅圆角化顶部两个角,并使用translate({ y: 18 })在Y轴方向偏移18像素,模拟了底部抽屉略微悬浮于屏幕底部边缘的效果。

底部抽屉是移动端应用中非常流行的交互模式,特别适合表单填写、选项选择等需要用户主动操作的场景。与居中弹窗相比,底部抽屉更接近拇指操作区域(在手机屏幕下方),符合人体工程学;同时它不遮挡屏幕上方的上下文信息,用户可以在查看内容的同时进行操作。

弹窗中的时段三档选择

        Row({ space: 8 }) {
          Column({ space: 3 }) {
            Text('上午场').fontSize(11).fontColor(COLORS.primary).fontWeight(FontWeight.Bold)
            Text('09:00-12:00').fontSize(10).fontColor(COLORS.textSecondary)
          }
          .layoutWeight(1)
          .padding(10)
          .backgroundColor('rgba(76,175,80,0.10)')
          .borderRadius(10)
          Column({ space: 3 }) {
            Text('下午场').fontSize(11).fontColor(COLORS.secondary).fontWeight(FontWeight.Bold)
            Text('13:00-17:00').fontSize(10).fontColor(COLORS.textSecondary)
          }
          .layoutWeight(1)
          .padding(10)
          .backgroundColor('rgba(139,195,74,0.10)')
          .borderRadius(10)
          Column({ space: 3 }) {
            Text('夜光场').fontSize(11).fontColor(COLORS.accent).fontWeight(FontWeight.Bold)
            Text('18:00-21:00').fontSize(10).fontColor(COLORS.textSecondary)
          }
          .layoutWeight(1)
          .padding(10)
          .backgroundColor('rgba(255,112,67,0.10)')
          .borderRadius(10)
        }
        .width('100%')

参观预约弹窗中的时段选择区域展示了三档选项:上午场、下午场和夜光场。三个选项卡片使用与园区详情弹窗信息卡片相同的布局模式——等宽Column + 标签 + 描述——但色彩搭配更有语义性:上午场使用主色调绿色(代表新生与活力),下午场使用辅助色浅绿色(代表延续与稳定),夜光场使用强调色橙色(代表夜晚与特殊体验)。这种通过色彩传递语义信息的设计是视觉设计中的常见手法。

弹窗3至弹窗5:恐龙档案、喂食确认与游乐设施

  @Builder
  dinoModal() {
    Column() {
      Column({ space: 10 }) {
        Row() {
          Text(this.selDino!.icon).fontSize(40).width(72).height(72).textAlign(TextAlign.Center).backgroundColor('rgba(139,195,74,0.16)').borderRadius(36)
          Column({ space: 3 }) {
            Text(this.selDino!.name).fontSize(19).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
            Text(this.selDino!.kind + ' | ' + this.selDino!.era).fontSize(12).fontColor(COLORS.secondary)
            Text('⭐ 人气 ' + this.selDino!.rating + ' | 🛡 保护 ' + this.selDino!.protect).fontSize(11).fontColor(COLORS.gold)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
          Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showDino = false })
        }
        .width('100%')

dinoModal(恐龙档案弹窗)的整体结构与zoneModal高度一致——居中布局、88%宽度、卡片背景、圆角16。区别在于数据来源从selZone变为selDino,且图标方块使用了72x72的更大尺寸和36的圆角值(完全圆形),视觉上更加突出。中间的文字信息包含恐龙名称(19号粗体)、种类与时代(12号辅助色)、人气评分与保护等级(11号金色),通过emoji前缀(⭐和🛡)增加了信息的辨识度。

  @Builder
  feedModal() {
    Column() {
      Column({ space: 10 }) {
        Row() {
          Column({ space: 2 }) {
            Text('🍽 喂食确认').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
            Text(this.selDino!.name + ' | 营养餐兑换').fontSize(11).fontColor(COLORS.gold)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showFeed = false })
        }
        .width('100%')
        Row({ space: 8 }) {
          Row({ space: 10 }) {
            Text(this.selDino!.icon).fontSize(28).width(52).height(52).textAlign(TextAlign.Center).backgroundColor('rgba(255,209,102,0.14)').borderRadius(26)
            Column({ space: 2 }) {
              Text(this.selDino!.name).fontSize(14).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
              Text('每次投喂 2 份恐龙营养餐').fontSize(10).fontColor(COLORS.textSecondary)
            }
            .alignItems(HorizontalAlign.Start)
          }
          .layoutWeight(1)
          Text('−').fontSize(20).fontColor(COLORS.gold).textAlign(TextAlign.Center).width(38).height(38)
            .border({ width: 2, color: COLORS.gold, style: BorderStyle.Dashed })
            .borderRadius(19)
          Text('+').fontSize(20).fontColor(COLORS.gold).textAlign(TextAlign.Center).width(38).height(38)
            .border({ width: 2, color: COLORS.gold, style: BorderStyle.Dashed })
            .borderRadius(19)
        }
        .width('100%')

feedModal(喂食确认弹窗)引入了步进器(stepper)交互元素。‘−’和’+'两个按钮使用38x38的圆形(borderRadius: 19)外观,边框设置为2像素宽的金色虚线(BorderStyle.Dashed),视觉上形成了一个可操作的步进控制器。这种虚线圆形按钮的设计与实心按钮形成了视觉区分,暗示了其"可调节"的交互语义。

弹窗中的数值计算逻辑值得关注:营养餐单价为this.selDino!.rating * 25(恐龙评分乘以25),本次共需为this.selDino!.rating * 25 * 2(单价乘以2份)。这种在UI模板中直接进行数值计算的方式在原型开发中可以快速验证逻辑,但在生产环境中建议将计算逻辑抽取为独立函数以提高可测试性和可维护性。

BorderStyle.Dashed是ArkUI边框样式枚举之一,它将边框渲染为虚线效果。与之对应的还有BorderStyle.Solid(实线,默认值)和BorderStyle.Dotted(点线)。在本应用中,虚线边框被用于喂食弹窗的步进器按钮和弹窗卡片外框,创造了一种"优惠券"或"票据"的视觉效果,与喂食营养餐兑换的业务场景相呼应。

弹窗6:驯养师荣誉榜领奖台

  @Builder
  rankModal() {
    Column() {
      Column({ space: 10 }) {
        Row() {
          Column({ space: 2 }) {
            Text('🦖 驯养师荣誉榜').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
            Text('本月喂养贡献与驯养积分排名').fontSize(11).fontColor(COLORS.gold)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showRank = false })
        }
        .width('100%')
        Row({ space: 6 }) {
          ForEach(getRankTop(), (r: DinoRank) => {
            Column({ space: 4 }) {
              Text(r.icon).fontSize(24)
              Text(r.name).fontSize(11).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold)
              Text(r.score + '').fontSize(10).fontColor(COLORS.gold)
              Column() {
              }
              .width('100%')
              .height(r.id === 1 ? 46 : r.id === 2 ? 32 : 22)
              .backgroundColor(r.id === 1 ? COLORS.gold : r.id === 2 ? COLORS.secondary : COLORS.accent)
              .borderRadius({ topLeft: 6, topRight: 6 })
            }
            .layoutWeight(1)
            .padding({ top: 10, bottom: 0 })
            .backgroundColor('rgba(76,175,80,0.06)')
            .borderRadius(10)
          })
        }
        .width('100%')
        .alignItems(VerticalAlign.Bottom)
      }

rankModal(驯养师荣誉榜弹窗)中最具特色的部分是领奖台(podium)的渲染。领奖台使用ForEach遍历getRankTop()返回的前三名数据,每个名次渲染为一个Column,包含图标、姓名、积分和一个高度可变的空Column(模拟领奖台的柱子)。

领奖台柱子的高度通过三元条件表达式动态设置:第一名46像素、第二名32像素、第三名22像素。柱子的颜色也根据名次动态变化:第一名金色(COLORS.gold)、第二名辅助色绿色(COLORS.secondary)、第三名强调色橙色(COLORS.accent)。Row容器设置了alignItems(VerticalAlign.Bottom),使三个柱子底部对齐,形成了经典的领奖台视觉效果——第一名最高且居中,两侧的第二名和第三名依次降低。

ForEach是ArkUI中用于列表渲染的核心组件。它接收三个参数:数据源数组、子组件生成函数和(可选的)键值生成函数。ForEach会遍历数据源数组的每个元素,调用子组件生成函数为每个元素创建对应的UI组件。当数据源发生变化时,ForEach会进行差异对比(diff),仅更新发生变化的列表项,从而保证列表渲染的性能。在本应用中,ForEach被广泛用于各种列表和网格的渲染——从公告列表到任务列表,从技能展示到纪念品网格,几乎每个Tab页面都大量使用了ForEach。

弹窗7至弹窗10:公告、订单、技能与菜品

  @Builder
  skillModal() {
    Column() {
      Column({ space: 10 }) {
        // ...标题行
        Row({ space: 6 }) {
          ForEach(getUnlockedSkills(), (s: DinoSkill) => {
            Column({ space: 4 }) {
              Text(s.icon).fontSize(20)
              Text(s.name).fontSize(9).fontColor(COLORS.textPrimary)
              Row() {
                Row() {
                }
                .width('100%')
                .height((s.exp) + '%')
                .backgroundColor(s.exp >= 80 ? COLORS.secondary : s.exp >= 50 ? COLORS.primary : COLORS.accent)
                .borderRadius({ topLeft: 4, topRight: 4 })
              }
              .width('100%')
              .height(100)
              .alignItems(VerticalAlign.Bottom)
              .backgroundColor('rgba(255,255,255,0.05)')
              .borderRadius(6)
              Text(s.exp + '').fontSize(9).fontColor(COLORS.gold)
            }
            .layoutWeight(1)
          })
        }
        .width('100%')

skillModal(技能成长弹窗)中使用ForEach渲染了一个柱状图。每个技能项被渲染为一个等宽的Column,包含图标、名称、柱状条和经验值数字。柱状条的实现方式值得深入分析:外层Row设置固定高度100像素和半透明背景,内层空Row的height属性直接绑定为(s.exp) + ‘%’——即经验值百分比作为高度百分比。例如,当s.exp为88时,内层Row的高度为88%,在100像素的外层Row中占据88像素。

柱状条的颜色也通过三元条件表达式动态设置:经验值大于等于80时使用辅助色绿色(表示精通),大于等于50时使用主色调绿色(表示熟练),否则使用强调色橙色(表示初学)。外层Row设置了alignItems(VerticalAlign.Bottom),使内层柱状条从底部向上生长,符合柱状图的标准视觉约定。

  @Builder
  menuModal() {
    Column() {
      Column({ space: 10 }) {
        // ...标题行
        Column({ space: 6 }) {
          Row() {
            Text('🔥 热销度').fontSize(11).fontColor(COLORS.textSecondary)
            Text(this.selMenu!.sales + ' / 100').fontSize(11).fontColor(COLORS.danger)
          }
          .width('100%')
          Row() {
            Row() {
            }
            .width(this.selMenu!.sales + '%')
            .height(9)
            .backgroundColor(COLORS.accent)
            .borderRadius(5)
          }
          .width('100%')
          .height(9)
          .backgroundColor('rgba(255,255,255,0.08)')
          .borderRadius(5)
        }
        .width('100%')

menuModal(菜品详情弹窗)中的热销度进度条是另一种数据可视化方式。与技能弹窗的柱状图不同,进度条使用水平方向——外层Row宽度100%、高度9像素,内层空Row的width属性绑定为this.selMenu!.sales + ‘%’。例如,当sales为92时,进度条填充92%的宽度。这种水平进度条的设计在移动应用中极为常见,用于直观地展示一个数值在0-100范围内的相对大小。

弹窗11至弹窗16:纪念品、餐厅预订、驯养师档案、年卡、物资与任务

  @Builder
  souvenirModal() {
    Column() {
      Column({ space: 0 }) {
        Column({ space: 4 }) {
          Text(this.selSouvenir!.icon).fontSize(42)
          Text(this.selSouvenir!.name).fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
          Text(this.selSouvenir!.rarity + ' | 在售 ' + this.selSouvenir!.source).fontSize(12).fontColor(COLORS.gold)
        }
        .width('100%')
        .padding({ top: 26, bottom: 26 })
        .linearGradient({ angle: 180, colors: [['#FF7043', 0.0], ['#0A1E10', 1.0]] })
        .borderRadius({ topLeft: 16, topRight: 16 })

souvenirModal(纪念品详情弹窗)采用了"上下拼接渐变头"的设计。弹窗内容Column的space设为0(子组件之间无间距),头部区域使用从’#FF7043’(橙色)到’#0A1E10’(深绿黑)的180度垂直渐变,仅圆角化顶部两角(borderRadius: { topLeft: 16, topRight: 16 }),而底部区域使用纯色背景。这种设计使头部和底部在视觉上形成了一个整体卡片,同时通过渐变效果突出了头部的展示焦点。

  @Builder
  vipModal() {
    Column() {
      Column({ space: 10 }) {
        Column({ space: 6 }) {
          Text('🦖 恐龙乐园年卡').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.gold).letterSpacing(1)
          Text('DINO PARK VIP CARD').fontSize(11).fontColor('rgba(255,209,102,0.80)').letterSpacing(2)
          Row({ space: 10 }) {
            Text(this.selVip!.icon).fontSize(40).width(68).height(68).textAlign(TextAlign.Center).backgroundColor('rgba(255,209,102,0.20)').borderRadius(34)
            Column({ space: 3 }) {
              Text(this.selVip!.name).fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
              Text(this.selVip!.role + ' | Lv.' + this.selVip!.level).fontSize(12).fontColor(COLORS.gold)
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            .margin({ left: 8 })
          }
          .width('100%')
          .margin({ top: 10 })
        }
        .width('100%')
        .padding(18)
        .linearGradient({ angle: 135, colors: [['#FFD166', 0.0], ['#E8A33D', 0.6], ['#8A5A12', 1.0]] })
        .borderRadius(16)

vipModal(乐园年卡弹窗)是所有弹窗中视觉效果最华丽的一个。年卡头部使用了从金色’#FFD166’到深棕’#8A5A12’的三段135度渐变,模拟了金属质感的效果。年卡标题"DINO PARK VIP CARD"使用了letterSpacing(2)增加字母间距,配合金色字体颜色,营造了高端会员卡的视觉气质。持卡人信息使用22号粗体白色字体显示姓名,下方12号金色显示角色和等级。

布局策略

弹窗类型分类

居中弹窗

zoneModal 园区详情

dinoModal 恐龙档案

feedModal 喂食确认

rideModal 游乐设施

rankModal 荣誉榜

noticeModal 公告

orderModal 运单

skillModal 技能

menuModal 菜品

souvenirModal 纪念品

keeperModal 驯养师档案

vipModal 年卡

supplyModal 物资

taskModal 任务

底部抽屉

visitModal 参观预约

bookModal 餐厅预订

FlexAlign.Center 居中

FlexAlign.End 底部

弹窗15:物资申领宫格

  @Builder
  supplyModal() {
    Column() {
      Column({ space: 10 }) {
        // ...标题行
        Row({ space: 8 }) {
          Column({ space: 8 }) {
            ForEach(getQuickLeft(), (q: DinoQuick) => {
              Row({ space: 8 }) {
                Text(q.icon).fontSize(22).width(40).height(40).textAlign(TextAlign.Center).backgroundColor('rgba(76,175,80,0.12)').borderRadius(20)
                Column({ space: 2 }) {
                  Text(q.name).fontSize(12).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold)
                  Text('可申领').fontSize(9).fontColor(COLORS.textSecondary)
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                Text('领').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .backgroundColor(COLORS.primary).borderRadius(10)
              }
              .width('100%')
              .padding(10)
              .backgroundColor('rgba(255,255,255,0.05)')
              .borderRadius(12)
            })
          }
          .layoutWeight(1)
          Column({ space: 8 }) {
            ForEach(getQuickRight(), (q: DinoQuick) => {
              Row({ space: 8 }) {
                Text(q.icon).fontSize(22).width(40).height(40).textAlign(TextAlign.Center).backgroundColor('rgba(255,112,67,0.12)').borderRadius(20)
                Column({ space: 2 }) {
                  Text(q.name).fontSize(12).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold)
                  Text('可申领').fontSize(9).fontColor(COLORS.textSecondary)
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                Text('领').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .backgroundColor(COLORS.accent).borderRadius(10)
              }
              .width('100%')
              .padding(10)
              .backgroundColor('rgba(255,255,255,0.05)')
              .borderRadius(12)
            })
          }
          .layoutWeight(1)
        }
        .width('100%')

supplyModal(物资申领弹窗)将八个快捷功能项分为左右两列渲染,左列使用绿色系背景图标和主色调绿色"领"按钮,右列使用橙色系背景图标和强调色橙色"领"按钮。这种左右双列、色彩区分的布局使物资申领的宫格界面在视觉上更加有层次感。弹窗内容还设置了constraintSize({ maxHeight: ‘80%’ }),限制了弹窗的最大高度为屏幕的80%,当内容超出时可以在弹窗内部滚动。

弹窗16:任务详情卡

  @Builder
  taskModal() {
    Column() {
      Column({ space: 10 }) {
        Row() {
          Text(this.selTask!.icon).fontSize(36).width(64).height(64).textAlign(TextAlign.Center).backgroundColor('rgba(255,112,67,0.16)').borderRadius(32)
          Column({ space: 3 }) {
            Text(this.selTask!.title).fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
            Text(this.selTask!.type + ' | ' + this.selTask!.zone).fontSize(12).fontColor(COLORS.accent)
            Text('🕐 ' + this.selTask!.time + ' | ' + this.selTask!.status).fontSize(11).fontColor(COLORS.gold)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
          Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showTask = false })
        }
        .width('100%')

taskModal(任务详情弹窗)的结构与其他居中弹窗保持一致。弹窗底部按钮的文本通过三元条件表达式动态决定:当任务状态为"进行中"时显示"立即接单",否则显示"查看回顾"。紧急程度进度条的宽度绑定为this.selTask!.reward % 100 + ‘%’——用任务奖励值对100取模的结果作为紧急程度百分比,这是一种巧妙但略显随意的数值映射方式,在原型开发中可接受,但在生产环境中应该使用专门的紧急程度字段。

七、Tab组件逐一解析

ParkTab:园区导览页面

@Component
struct ParkTab {
  onZone: (z: ParkZone) => void = () => {}
  onVisit: (z: ParkZone) => void = () => {}
  onRank: (r: DinoRank) => void = void = () => {}
  onNotice: (n: DinoNotice) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        // 丛林渐变横幅
        // 快捷宫格
        // 王牌区域横滑
        // 全部区域双列
        // 榜单入口条
        // 公告斑马纹列
      }
      .padding({ left: 16, right: 16, bottom: 20 })
    }
    .scrollable(ScrollDirection.Vertical)
    .width('100%')
    .height('100%')
  }

ParkTab是第一个Tab页面,定义为@Component struct。它声明了四个回调函数属性:onZone、onVisit、onRank和onNotice,默认值都是空函数(() => {})。这些回调函数由父组件Index在实例化ParkTab时传入,用于在子组件内部发生交互事件时通知父组件。

ParkTab的build方法使用Scroll容器作为最外层,使内容可以垂直滚动。Scroll内部是一个设置了space: 10的Column容器,将六个内容区块按从上到下的顺序排列:丛林渐变横幅、快捷宫格、王牌区域横滑、全部区域双列、榜单入口条和公告斑马纹列。

Scroll组件是ArkUI中用于实现可滚动内容区域的核心容器。它通过scrollable参数指定滚动方向——ScrollDirection.Vertical为垂直滚动(默认值),ScrollDirection.Horizontal为水平滚动。Scroll组件的子组件如果内容超出Scroll的可视区域,就会产生滚动行为。在本应用中,每个Tab页面的最外层都是垂直Scroll,确保内容超出屏幕时可以滚动查看。

丛林渐变横幅

        Column({ space: 6 }) {
          Text('🏕 园区导览').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
          Text('开放区域 ' + getZoneCount() + ' 个 · 今日接待 ' + (getZoneCount() * 860) + ' 人').fontSize(12).fontColor('rgba(234,245,234,0.85)')
          Row({ space: 8 }) {
            Text('🌿 全园开放').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(139,195,74,0.30)').borderRadius(10)
            Text('🥚 孵蛋馆升级中').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(255,209,102,0.25)').borderRadius(10)
          }
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .padding(16)
        .linearGradient({ angle: 135, colors: [['#0A1E10', 0.0], ['#4CAF50', 1.0]] })
        .borderRadius(16)

横幅区域使用Column容器,包含标题、统计信息和两个状态标签。标题"🏕 园区导览"使用22号粗体白色字体。统计信息通过调用全局函数getZoneCount()获取区域数量,并进行简单的数值运算(getZoneCount() * 860)估算今日接待人数。两个状态标签使用半透明背景色和10的圆角值,形成胶囊形状的标签外观——绿色标签表示"全园开放"的正常状态,金色标签提示"孵蛋馆升级中"的异常状态。

横幅整体使用从深色’#0A1E10’到主色调’#4CAF50’的135度线性渐变背景,配合16的圆角值,形成了与头部横幅一致的视觉风格。alignItems(HorizontalAlign.Start)使所有子组件左对齐,与中文文本从左到右的阅读习惯一致。

快捷宫格与王牌区域横滑

        Row({ space: 8 }) {
          Column({ space: 8 }) {
            ForEach(getQuickLeft(), (q: DinoQuick) => {
              this.quickCard(q)
            })
          }
          .layoutWeight(1)
          Column({ space: 8 }) {
            ForEach(getQuickRight(), (q: DinoQuick) => {
              this.quickCard(q)
            })
          }
          .layoutWeight(1)
        }
        .width('100%')

快捷宫格区域使用Row容器,内含两个等宽Column,每个Column内使用ForEach遍历getQuickLeft()和getQuickRight()返回的四个快捷功能项,调用this.quickCard(q)生成每个功能项的卡片UI。@Builder方法quickCard接收一个DinoQuick参数,生成一个包含图标和名称的Row卡片。

        Scroll() {
          Row({ space: 10 }) {
            ForEach(getTopZones(), (z: ParkZone) => {
              this.zoneWideCard(z)
            })
          }
          .padding({ right: 4 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .width('100%')
        .constraintSize({ maxHeight: 160 })

王牌区域横滑区域使用水平Scroll容器,内部是一个Row,通过ForEach遍历getTopZones()返回的高等级区域数据。constraintSize({ maxHeight: 160 })限制了横滑区域的最大高度为160像素,确保横滑卡片不会占据过多垂直空间。zoneWideCard Builder为每个区域生成一个宽度140像素的卡片,包含图标、名称、分类、等级和状态信息。

水平滚动(ScrollDirection.Horizontal)在移动应用中常用于展示精选内容或横向列表。与垂直滚动不同,水平滚动的Scroll容器需要设置一个固定或约束的高度(通过constraintSize或height属性),否则容器会尝试占据所有可用高度。在本应用中,水平滚动的卡片宽度通常固定(如140或130像素),通过Row的space参数控制卡片间距,为用户提供横向滑动浏览的交互体验。

全部区域双列与公告斑马纹

        Row({ space: 10 }) {
          Column({ space: 10 }) {
            ForEach(getZoneLeft(), (z: ParkZone) => {
              this.zoneCard(z)
            })
          }
          .layoutWeight(1)
          Column({ space: 10 }) {
            ForEach(getZoneRight(), (z: ParkZone) => {
              this.zoneCard(z)
            })
          }
          .layoutWeight(1)
        }
        .width('100%')

全部区域使用双列布局,与快捷宫格的结构完全相同——Row内含两个等宽Column,每个Column内使用ForEach遍历分列后的数据。zoneCard Builder生成的卡片比zoneWideCard更复杂,包含图标行、信息行和两个操作按钮(“详情"和"预约”),分别绑定onZone和onVisit回调。

        Column({ space: 8 }) {
          ForEach(DINO_NOTICES, (n: DinoNotice) => {
            this.noticeRow(n)
          })
        }
        .width('100%')

公告区域使用ForEach遍历DINO_NOTICES数据集,为每条公告生成一个noticeRow行。noticeRow Builder中有一个巧妙的斑马纹设计:

  @Builder
  noticeRow(n: DinoNotice) {
    Row({ space: 10 }) {
      Text(n.level).fontSize(10).fontColor(COLORS.white).padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .backgroundColor(n.level === '重要' ? COLORS.danger : COLORS.primary)
        .borderRadius(8)
      Column({ space: 2 }) {
        Text(n.title).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
        Text(n.date).fontSize(10).fontColor(COLORS.textSecondary)
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      Text('›').fontSize(16).fontColor(COLORS.textSecondary)
    }
    .width('100%')
    .padding(12)
    .backgroundColor(n.id % 2 === 0 ? 'rgba(255,255,255,0.06)' : 'rgba(76,175,80,0.06)')
    .borderRadius(12)
    .onClick(() => {
      this.onNotice(n)
    })
  }

noticeRow的backgroundColor属性通过n.id % 2 === 0的条件判断,使奇数行和偶数行使用不同的背景色——偶数行使用白色半透明背景,奇数行使用绿色半透明背景。这种交替背景色就是经典的"斑马纹"(zebra striping)设计,它通过视觉上的交替区分帮助用户在长列表中更容易地定位和追踪某一行。公告级别的标签也使用条件色彩:级别为"重要"时使用危险色红色背景,其他级别使用主色调绿色背景。

DinoTab:恐龙家族页面

@Component
struct DinoTab {
  onDino: (d: Dino) => void = () => {}
  onFeed: (d: Dino) => void = () => {}
  onTask: (t: DinoTask) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        Column({ space: 6 }) {
          Text('🦖 恐龙家族').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
          Text('馆藏恐龙 ' + getDinoCount() + ' 种 · 明星 2 种').fontSize(12).fontColor('rgba(234,245,234,0.85)')
          Row({ space: 8 }) {
            Text('🦖 霸王龙秀场').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(255,112,67,0.30)').borderRadius(10)
            Text('🥚 新蛋孵化倒计时').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(139,195,74,0.25)').borderRadius(10)
          }
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .padding(16)
        .linearGradient({ angle: 135, colors: [['#0A1E10', 0.0], ['#8BC34A', 1.0]] })
        .borderRadius(16)

DinoTab的结构与ParkTab高度相似,但横幅渐变色从绿色变为了辅助色浅绿色’#8BC34A’,为不同Tab页面提供了视觉区分。恐龙Tab的横幅展示了"恐龙家族"标题、馆藏恐龙种类数量(通过getDinoCount()获取)以及两个活动标签。

恐龙Tab的回调函数有三个:onDino(查看恐龙档案)、onFeed(投喂恐龙)和onTask(查看任务)。dinoCard Builder中为每张恐龙卡片提供了"档案"和"投喂"两个操作按钮,分别绑定onDino和onFeed回调。页面底部还有一个任务提示条,点击后调用onTask回调展示任务详情弹窗。

FunTab与FoodTab:游乐与餐厅页面

@Component
struct FunTab {
  onRide: (r: Ride) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        Column({ space: 6 }) {
          Text('🎢 游乐天地').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
          Text('游乐项目 ' + getRideCount() + ' 个 · 今日场次 120 场').fontSize(12).fontColor('rgba(255,255,255,0.85)')
          // ...标签
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .padding(16)
        .linearGradient({ angle: 135, colors: [['#0A1E10', 0.0], ['#4FC3F7', 1.0]] })
        .borderRadius(16)

FunTab(游乐页)的横幅渐变色使用了天蓝色’#4FC3F7’,与园区页的绿色和恐龙页的浅绿色形成鲜明对比。游乐页的布局顺序与园区页略有不同——先展示全部游乐设施的双列卡片,再展示王牌设施的横滑区域。rideCard Builder中只有一个"线上排号"按钮,绑定onRide回调。

@Component
struct FoodTab {
  onMenu: (m: DinnerMenu) => void = () => {}
  onBook: (m: DinnerMenu) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        Column({ space: 6 }) {
          Text('🍖 恐龙餐厅').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
          Text('精选菜品 ' + getMenuCount() + ' 款 · 全场 9 折起').fontSize(12).fontColor('rgba(255,255,255,0.85)')
          // ...标签
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .padding(16)
        .linearGradient({ angle: 135, colors: [['#0A1E10', 0.0], ['#FFA94D', 1.0]] })
        .borderRadius(16)

FoodTab(餐厅页)的横幅渐变色使用了暖橙色’#FFA94D’,营造了美食与温暖的视觉氛围。餐厅页的menuCard Builder中包含了一个热销度进度条——与menuModal弹窗中的进度条设计完全一致,但尺寸更小(高度6像素 vs 9像素),适应了卡片中更紧凑的布局空间。每个菜品卡片提供"详情"和"预订"两个按钮,分别绑定onMenu和onBook回调。

GiftTab与MineTab:纪念品与个人中心

@Component
struct GiftTab {
  onSouvenir: (s: Souvenir) => void = () => {}
  onOrder: (o: DinoOrder) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        Column({ space: 6 }) {
          Text('🎁 纪念品商店').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
          Text('在售纪念品 ' + getSouvenirCount() + ' 款 · 满 200 包邮').fontSize(12).fontColor('rgba(255,255,255,0.85)')
          // ...标签
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .padding(16)
        .linearGradient({ angle: 135, colors: [['#0A1E10', 0.0], ['#B39DDB', 1.0]] })
        .borderRadius(16)

GiftTab(纪念品页)的横幅渐变色使用了紫色’#B39DDB’,为纪念品商店营造了一种精致、独特的视觉气质。纪念品页包含纪念品双列卡片、纪念币提示条和进行中订单列表三个主要区块。souvenirCard Builder中点击卡片触发onSouvenir回调展示纪念品详情弹窗,orderRow Builder中点击订单行触发onOrder回调展示运单详情弹窗。

@Component
struct MineTab {
  onTask: (t: DinoTask) => void = () => {}
  onSkill: (sk: DinoSkill) => void = () => {}
  onOrder: (o: DinoOrder) => void = () => {}
  onRank: (r: DinoRank) => void = () => {}
  onNotice: (n: DinoNotice) => void = () => {}
  onKeeper: (k: Keeper) => void = () => {}
  onVip: (k: Keeper) => void = () => {}
  onSupply: (q: DinoQuick) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        // 驯养师档案卡
        // 乐园年卡金卡
        // 值班任务双列
        // 已完成列
        // 我的技能双列
        // 进行中订单
        // 园区公告
        // 物资申领入口
      }

MineTab(我的页)是所有Tab中内容最丰富的一个,拥有八个回调函数,是六个Tab中交互入口最多的页面。页面没有渐变横幅,而是以驯养师档案卡和年卡金卡作为头部区域。驯养师档案卡使用半透明白色背景展示当前用户的头像、姓名、角色、等级、喂养次数和星级评分。年卡金卡使用了与vipModal弹窗一致的金色渐变背景,展示年卡标题和持卡人信息。

值班任务区域使用双列布局展示getTaskLeft()和getTaskRight()分列后的任务数据,每个任务卡片显示图标、标题、类型、状态标签和奖励值。已完成任务区域使用getDoneTasks()筛选已完成任务,以简洁的行样式展示,右侧带有一个绿色勾选标记。技能区域同样使用双列布局,每个技能卡片包含一个经验值进度条——与skillModal弹窗和menuCard卡片中的进度条设计模式一致。

MineTab中的任务卡片与技能卡片

  @Builder
  taskCard(t: DinoTask) {
    Column({ space: 8 }) {
      Row({ space: 8 }) {
        Text(t.icon).fontSize(24).width(44).height(44).textAlign(TextAlign.Center).backgroundColor('rgba(255,112,67,0.12)').borderRadius(22)
        Column({ space: 2 }) {
          Text(t.title).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
          Text(t.type + ' | ' + t.zone).fontSize(10).fontColor(COLORS.textSecondary)
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      Row({ space: 6 }) {
        Text(t.status).fontSize(10).fontColor(COLORS.white).padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .backgroundColor(t.status === '进行中' ? COLORS.success : COLORS.textHint)
          .borderRadius(8)
        Text('+' + t.reward + ' 币').fontSize(11).fontWeight(FontWeight.Bold).fontColor(COLORS.gold).layoutWeight(1).textAlign(TextAlign.End)
      }
      .width('100%')
      Text('🕐 ' + t.time).fontSize(10).fontColor(COLORS.textSecondary).width('100%')
    }
    .width('100%')
    .padding(12)
    .backgroundColor('rgba(255,255,255,0.05)')
    .borderRadius(14)
    .onClick(() => {
      this.onTask(t)
    })
  }

taskCard Builder是MineTab中最核心的卡片组件之一。它的结构包含三行内容:第一行是图标和标题信息行,第二行是状态标签和奖励值行,第三行是时间信息。状态标签的背景色通过条件表达式动态设置——“进行中"状态使用成功色绿色背景,其他状态使用提示色灰色背景。奖励值使用金色字体显示,前缀加号”+"表示获得,layoutWeight(1)和textAlign(TextAlign.End)使其占据剩余空间并右对齐,与左侧的状态标签形成对称布局。

  @Builder
  skillCard(sk: DinoSkill) {
    Column({ space: 8 }) {
      Row({ space: 8 }) {
        Text(sk.icon).fontSize(24).width(44).height(44).textAlign(TextAlign.Center).backgroundColor('rgba(76,175,80,0.12)').borderRadius(22)
        Column({ space: 2 }) {
          Text(sk.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
          Text(sk.type).fontSize(10).fontColor(COLORS.textSecondary)
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Text('Lv.' + sk.level).fontSize(11).fontWeight(FontWeight.Bold).fontColor(COLORS.primary)
      }
      Column({ space: 4 }) {
        Row() {
          Text('经验').fontSize(9).fontColor(COLORS.textSecondary)
          Text(sk.exp + '/100').fontSize(9).fontColor(COLORS.primary)
        }
        .width('100%')
        Row() {
          Row() {
          }
          .width(sk.exp + '%')
          .height(6)
          .backgroundColor(COLORS.primary)
          .borderRadius(3)
        }
        .width('100%')
        .height(6)
        .backgroundColor('rgba(255,255,255,0.08)')
        .borderRadius(3)
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
    .backgroundColor('rgba(76,175,80,0.06)')
    .borderRadius(14)
    .onClick(() => {
      this.onSkill(sk)
    })
  }

skillCard Builder的结构包含两个主要部分:上半部分是图标、名称、类型和等级的行,下半部分是经验值进度条。经验值进度条的实现方式与menuCard中的热销度进度条完全一致——外层Row固定高度6像素、半透明背景,内层空Row的width绑定为sk.exp + ‘%’。这里可以看到,进度条这一UI模式在应用中被多次复用:menuModal中的热销度进度条(高度9像素、橙色)、menuCard中的热销度进度条(高度6像素、橙色)、keeperModal中的经验值进度条(高度9像素、金色)、taskCard中的紧急程度进度条(高度9像素、红色)以及skillCard中的经验值进度条(高度6像素、绿色)。虽然这些进度条的尺寸和颜色各不相同,但它们的实现模式完全一致,体现了ArkUI声明式UI的高度一致性。

实现结构

进度条组件复用统计

9px 橙色

6px 橙色

9px 金色

9px 红色

6px 绿色

menuModal 热销度

水平进度条模式

menuCard 热销度

keeperModal 经验值

taskModal 紧急程度

skillCard 经验值

外层Row: 固定高度 + 半透明背景

内层空Row: width绑定百分比 + 纯色背景

八、关键技术点深度解析

Column容器组件

Column是ArkUI中最常用的垂直布局容器组件。它将子组件按照从上到下的顺序垂直排列,子组件之间可以通过构造函数的space参数指定固定间距。Column支持通过alignItems属性设置子组件的水平对齐方式——HorizontalAlign.Start(左对齐,默认值)、HorizontalAlign.Center(居中对齐)和HorizontalAlign.End(右对齐)。在本应用中,Column被广泛用于构建垂直方向的内容堆叠,从弹窗内容到Tab页面布局,从信息卡片到列表行,几乎每个UI区块都以Column作为基础容器。

Column还支持justifyContent属性设置子组件在垂直方向的整体分布方式——FlexAlign.Start(从顶部开始,默认值)、FlexAlign.Center(垂直居中)、FlexAlign.End(从底部开始)等。在本应用的弹窗系统中,居中弹窗使用FlexAlign.Center使弹窗内容在遮罩层中垂直居中,底部抽屉使用FlexAlign.End使弹窗内容贴底显示。

Row容器组件

Row是ArkUI中的水平布局容器组件,与Column相对应。它将子组件按照从左到右的顺序水平排列,同样支持space参数设置子组件间距。Row的alignItems属性设置子组件的垂直对齐方式——VerticalAlign.Top、VerticalAlign.Center(默认值)和VerticalAlign.Bottom。在本应用中,Row被大量用于构建水平方向的信息行,如弹窗标题行(图标 + 信息 + 关闭按钮)、信息卡片行(多个等宽卡片)和操作按钮行(两个等宽按钮)。

Row的justifyContent属性在水平方向上的分布方式与Column的justifyContent在垂直方向上类似。在本应用的领奖台弹窗中,Row设置了alignItems(VerticalAlign.Bottom)使三个领奖台柱子底部对齐。在弹窗关闭按钮行中,Row设置了justifyContent(FlexAlign.End)使关闭按钮靠右显示。

layoutWeight属性

layoutWeight是ArkUI中用于实现弹性布局的核心属性。当一个容器组件内有多个子组件设置了layoutWeight时,容器会在分配完未设置layoutWeight的子组件的尺寸后,将剩余空间按layoutWeight值的比例分配给设置了layoutWeight的子组件。在本应用中,layoutWeight(1)被大量用于实现等宽布局——三个信息卡片的Column都设置layoutWeight(1)使它们等分宽度,两个操作按钮都设置width(‘50%’)实现等宽布局。

layoutWeight与width(‘50%’)的区别在于:width(‘50%’)是相对于父容器宽度的50%固定值,而layoutWeight(1)是与其他设置了layoutWeight的子组件按比例分配剩余空间。当容器内有space间距时,width(‘50%’)可能导致子组件总宽度超出容器宽度(因为间距没有被考虑),而layoutWeight则会自动扣除间距后分配剩余空间。因此,在有间距的等分布局中,layoutWeight是更安全的选择。

Flex布局与FlexAlign

ArkUI的Flex布局模型通过FlexAlign枚举提供了丰富的对齐和分布选项。FlexAlign.Start表示子组件从容器起始端开始排列;FlexAlign.Center表示子组件在容器中居中排列;FlexAlign.End表示子组件从容器末端开始排列;FlexAlign.SpaceBetween表示子组件均匀分布,首尾两个子组件紧贴容器边缘,其余子组件间距相等;FlexAlign.SpaceAround表示子组件均匀分布,每个子组件两侧的间距相等;FlexAlign.SpaceEvenly表示子组件完全均匀分布,所有间距(包括首尾到容器边缘的间距)都相等。

在本应用中,bottomTabItem Builder使用了justifyContent(FlexAlign.Center)使Tab项的图标和文字在垂直方向居中排列。各种弹窗的遮罩层通过justifyContent的不同设置实现了居中和贴底两种弹窗定位策略。

@State状态管理

@State是ArkTS中最核心的状态管理装饰器。它用于装饰组件内部的私有状态变量,当被装饰的变量值发生变化时,框架会自动重新执行组件的build方法,对比新旧组件树的差异,并仅更新发生变化的DOM节点。这种基于虚拟DOM差异对比的更新策略保证了UI渲染的性能——即使整个build方法被重新执行,只有真正发生变化的UI部分才会在屏幕上更新。

在本应用的Index组件中,十七个@State变量共同管理着整个应用的状态。currentTab控制当前显示的Tab页面,十六个showXxx变量控制十六个弹窗的显示与隐藏。当用户点击底部Tab项时,currentTab的值变化触发build方法重新执行,内容区的条件渲染语句根据新的currentTab值渲染对应的Tab组件,同时底部Tab栏的各项样式也会更新。当用户点击列表项触发回调时,对应的showXxx变量被设为true,弹窗挂载点的条件判断通过,弹窗开始渲染。

@State的响应式更新机制有一个重要特性:只有通过赋值操作改变@State变量的值时才会触发更新,直接修改对象的属性不会触发更新。例如,如果selZone是一个@State变量,执行this.selZone.name = '新名称’不会触发UI更新,但执行this.selZone = newZone(赋值整个对象)会触发更新。在本应用中,selXxx变量没有被@State装饰,因此它们的变化不会直接触发UI更新——真正触发弹窗显示的是showXxx布尔变量的变化。这种设计将"显示控制"与"数据存储"分离,showXxx控制弹窗的显示与隐藏,selXxx在弹窗显示时提供数据。

@Builder装饰器

@Builder装饰器用于将一段UI构建逻辑抽取为可复用的方法。被@Builder装饰的方法可以在组件的build方法内部被调用,每次调用都会生成一段UI组件树。@Builder方法可以接收参数,使相同的UI结构能够根据不同的参数渲染不同的内容。

在本应用中,@Builder被广泛使用。Index组件定义了bottomTabItem(底部Tab项)、modalOverlay(遮罩层)、sectionTitle(区块标题)和十六个xxxModal(弹窗内容)共十九个Builder方法。六个Tab组件也各自定义了自己的Builder方法——ParkTab有quickCard、zoneWideCard、zoneCard和noticeRow四个Builder,DinoTab有dinoWideCard和dinoCard两个Builder,其余Tab组件类似。

@Builder方法与独立@Component组件的选择标准在于:如果UI片段需要访问父组件的@State变量(如bottomTabItem需要访问currentTab),且只在父组件内部使用,则使用@Builder;如果UI片段需要独立管理自己的状态和生命周期,且可能被多个组件引用,则使用@Component。在本应用中,所有Tab页面都被定义为独立的@Component,因为它们有各自的build逻辑和回调函数属性;而弹窗和卡片被定义为@Builder,因为它们依附于父组件的状态变量和回调函数。

@Component与@Entry装饰器

@Component装饰器将一个struct声明为自定义组件。自定义组件拥有自己的build方法、状态变量和生命周期。在本应用中,Index、ParkTab、DinoTab、FunTab、FoodTab、GiftTab和MineTab都是@Component自定义组件。每个@Component组件都可以被其他组件在其build方法中引用,引用时可以传入属性参数(如回调函数)。

@Entry装饰器是@Component的特殊变体,它标记一个组件为页面的入口组件。每个ArkTS页面文件有且仅有一个@Entry组件。@Entry组件是页面组件树的根节点,框架会为它创建页面级别的渲染上下文和生命周期。在本应用中,Index是唯一的@Entry组件,它管理着整个页面的状态和布局,六个Tab组件和十六个弹窗Builder都直接或间接地被Index组件引用。

ForEach循环渲染

ForEach是ArkUI中用于列表渲染的核心组件。它接收数据源数组和子组件生成函数两个主要参数(第三个键值生成函数为可选参数),为数据源的每个元素生成对应的UI组件。当数据源发生变化时,ForEach会进行差异对比,仅更新发生变化的列表项。

在本应用中,ForEach被大量使用。ParkTab中使用ForEach渲染公告列表、区域双列和快捷宫格;DinoTab中使用ForEach渲染恐龙双列和明星恐龙横滑;FunTab中使用ForEach渲染游乐设施双列和王牌设施横滑;FoodTab中使用ForEach渲染菜品双列和热销菜品横滑;GiftTab中使用ForEach渲染纪念品双列和订单列表;MineTab中使用ForEach渲染任务双列、已完成任务、技能双列、订单列表和公告列表。各种弹窗中也大量使用ForEach——rankModal中的领奖台和排名列表、supplyModal中的物资宫格等。

ForEach与@Builder的组合是本应用中最常见的代码模式。例如,在ParkTab的全部区域双列中:

ForEach(getZoneLeft(), (z: ParkZone) => {
  this.zoneCard(z)
})

ForEach遍历getZoneLeft()返回的区域数据数组,为每个元素调用this.zoneCard(z)生成一个区域卡片的UI。这种"数据源 + ForEach + Builder方法"的三元组合是ArkTS列表渲染的标准模式,它将数据获取、列表遍历和UI构建三个关注点清晰分离,使代码结构清晰可维护。

条件渲染

ArkTS中的条件渲染通过标准的JavaScript if-else语句实现。在build方法内部,可以使用if语句根据状态变量的值决定渲染哪些组件。当条件表达式的值变化时,框架会自动增删对应的UI组件子树。

在本应用中,条件渲染被用于两个核心场景。第一个场景是Tab切换:内容区的if-else if链根据currentTab的值决定渲染哪个Tab组件。当currentTab从0变为1时,ParkTab组件子树被销毁,DinoTab组件子树被创建。第二个场景是弹窗挂载:每个弹窗挂载点的if条件同时检查showXxx布尔变量和selXxx !== null,只有两者同时满足时才渲染遮罩层和弹窗内容。

条件渲染与ForEach的区别在于:条件渲染控制的是"是否渲染"(渲染或不渲染),适合二选一或多选一的场景;ForEach控制的是"渲染多少次"(遍历数据源为每个元素渲染一次),适合列表渲染场景。在本应用中,两者经常配合使用——弹窗挂载点使用条件渲染控制弹窗是否显示,弹窗内部使用ForEach渲染列表内容。

linearGradient线性渐变

linearGradient是ArkUI组件的背景线性渐变属性。它接收一个包含angle和colors两个属性的对象:angle指定渐变方向的角度(0度表示从下到上,90度表示从左到右,135度表示从左上到右下),colors是一个包含多个[颜色值, 停止位置]二元组的数组,定义了渐变的颜色停止点。

在本应用中,linearGradient被广泛用于营造视觉氛围。头部横幅使用了135度三段渐变(深色 -> 中绿 -> 主色调绿),为整个应用定下了深绿色丛林基调。六个Tab页面的横幅分别使用了不同终止色的135度渐变——园区页绿色、恐龙页浅绿色、游乐页天蓝色、餐厅页暖橙色、纪念页紫色——通过色彩区分不同功能区域。vipModal弹窗的金卡使用了135度三段金色渐变(金色 -> 暗金 -> 深棕),模拟了金属质感。souvenirModal弹窗的头部使用了180度垂直渐变(橙色 -> 深色),配合仅圆角化顶部两角的设计,形成了上下拼接的视觉效果。

Scroll与scrollable

Scroll组件是ArkUI中实现可滚动区域的核心容器。它通过scrollable参数指定滚动方向——ScrollDirection.Vertical(垂直滚动)或ScrollDirection.Horizontal(水平滚动)。Scroll容器会自动检测其子组件的内容尺寸是否超出容器的可视区域,如果超出则启用滚动行为。

在本应用中,Scroll被用于两个场景。第一个场景是Tab页面的整体垂直滚动:每个Tab组件的build方法最外层都是一个垂直Scroll容器,确保当页面内容超出屏幕高度时可以上下滑动查看。第二个场景是精选内容的水平横滑:在ParkTab、DinoTab、FunTab和FoodTab中,都有一个水平Scroll容器用于展示王牌/热销内容的横向滑动卡片。constraintSize({ maxHeight: 160 })限制了横滑区域的最大高度,防止它占据过多垂直空间。

border与borderRadius

border属性用于设置组件的边框样式,它接收一个包含width(宽度)、color(颜色)和style(样式)属性的对象。BorderStyle枚举提供了Solid(实线)、Dashed(虚线)和Dotted(点线)三种边框样式。borderRadius属性用于设置组件的圆角半径,可以是一个数字(四角相同)或一个包含topLeft、topRight、bottomLeft、bottomRight四个属性的对象(四角分别设置)。

在本应用中,border和borderRadius的组合使用创造了丰富的视觉效果。feedModal弹窗的卡片外框使用了金色虚线边框(border: { width: 2, color: COLORS.gold, style: BorderStyle.Dashed }),配合金色背景的确认按钮,形成了"优惠券"或"票据"的视觉风格。souvenirModal弹窗的头部仅圆角化顶部两角(borderRadius: { topLeft: 16, topRight: 16 }),使头部和底部在视觉上形成了一个整体卡片。各种按钮使用了较大的borderRadius值(如22)配合上下内边距,形成了胶囊形状的按钮外观。

backgroundColor与rgba色彩

backgroundColor属性用于设置组件的背景色。ArkUI支持多种颜色格式:十六进制色值(如’#4CAF50’)、具名颜色(如’white’)和rgba函数(如’rgba(255,255,255,0.10)')。rgba格式的最后一个参数是alpha透明度通道,取值范围0.0(完全透明)到1.0(完全不透明)。

在本应用中,半透明背景色被大量用于创建层次感。信息卡片使用’rgba(76,175,80,0.10)‘(10%透明度的主色调绿色)作为背景,使卡片在深色背景上隐约可见但不喧宾夺主。弹窗遮罩层使用’rgba(6,18,10,0.78)’(78%透明度的深色)覆盖整个屏幕,既遮挡了底层内容又保留了微弱的透视感。搜索框使用’rgba(255,255,255,0.10)'(10%透明度的白色)模拟了磨砂玻璃效果。

九、综合对比分析表

以下表格对本应用中涉及的核心组件、属性、装饰器和技术模式进行系统对比:

技术/组件/属性 类别 核心作用 关键参数/属性 应用场景 典型值示例
Column 容器组件 垂直排列子组件 space, alignItems, justifyContent 弹窗内容、卡片堆叠、页面布局 space: 10, alignItems: Start
Row 容器组件 水平排列子组件 space, alignItems, justifyContent 标题行、按钮行、双列布局 space: 8, layoutWeight: 1
Scroll 容器组件 可滚动区域 scrollable, constraintSize 页面垂直滚动、精选横滑 ScrollDirection.Vertical/Horizontal
Stack 容器组件 子组件层叠堆放 alignContent 遮罩层与弹窗叠加 (本应用以position替代)
Text 基础组件 显示文本 fontSize, fontColor, fontWeight 标题、标签、描述、数值 fontSize: 18, fontWeight: Bold
@Entry 装饰器 标记页面入口组件 Index组件 @Entry struct Index
@Component 装饰器 声明自定义组件 所有Tab组件和Index @Component struct ParkTab
@State 状态装饰器 组件内部响应式状态 变量类型和初始值 currentTab, showXxx @State currentTab: number = 0
@Builder UI装饰器 抽取可复用UI片段 方法参数 弹窗、卡片、标题 @Builder zoneModal()
@Provide 状态装饰器 跨层级向下注入数据 变量类型和初始值 祖先组件向后代传值 (本应用未使用)
@Consume 状态装饰器 接收祖先注入数据 变量类型 后代组件接收数据 (本应用未使用)
ForEach 渲染组件 遍历数据源渲染列表 数据源, 生成函数 列表、网格、双列布局 ForEach(getZoneLeft(), (z) => {…})
layoutWeight 布局属性 弹性分配剩余空间 数值权重 等宽卡片、信息填充 layoutWeight(1)
linearGradient 样式属性 线性渐变背景 angle, colors 横幅、年卡、纪念品头部 angle: 135, colors: […]
borderRadius 样式属性 圆角半径 数字或四角对象 卡片、按钮、图标方块 16, { topLeft: 16, topRight: 16 }
justifyContent 布局属性 主轴方向分布 FlexAlign枚举 弹窗居中/贴底、按钮分布 FlexAlign.Center/End
alignItems 布局属性 交叉轴对齐 HorizontalAlign/VerticalAlign 文字左对齐、柱状图底对齐 HorizontalAlign.Start
scale 变换属性 缩放变换 x, y Tab项激活放大 { x: 1.1, y: 1.1 }
opacity 样式属性 不透明度 0.0-1.0 Tab项未激活半透明 0.45
position 布局属性 绝对定位 x, y 遮罩层全屏覆盖 { x: 0, y: 0 }
border 样式属性 边框样式 width, color, style 虚线边框、底部边框线 { width: 2, style: Dashed }
onClick 事件属性 点击事件绑定 回调函数 Tab切换、弹窗关闭、卡片点击 () => { this.showZone = false }
filter 数组方法 数据筛选过滤 谓词函数 全局函数数据预处理 .filter((z) => z.id % 2 === 1)
translate 变换属性 平移变换 x, y 底部抽屉悬浮效果 { y: 18 }
constraintSize 布局属性 约束尺寸 maxWidth, maxHeight 横滑区域限高、弹窗限高 { maxHeight: 160 }

十、总结

本文对一套基于鸿蒙ArkTS语言开发的恐龙乐园主题员工管理应用进行了全方位、逐段落的深度技术解析。从最基础的interface类型定义开始,我们详细分析了ColorPalette、ParkZone、Dino、Ride、DinnerMenu、DinoTask、DinoRank、DinoNotice、DinoOrder、DinoSkill、DinoQuick、Souvenir和Keeper共十三个接口类型的设计意图与字段语义,理解了数值型字段与字符串型字段在不同业务场景下的类型选择策略。随后,我们解析了COLORS色彩常量的深绿色丛林主题配色方案,以及十二个静态数据集的结构设计和数据填充模式。

在全局函数层,我们深入剖析了计数函数、分列函数、Top筛选函数和状态过滤函数四类数据预处理函数的实现逻辑,理解了基于id奇偶性的双列分片策略、基于数值阈值的精选筛选策略,以及基于状态字段的数据子集划分策略。这些函数构成了数据集与UI组件之间的适配层,将数据过滤逻辑从组件代码中分离出来,实现了关注点分离的软件工程原则。


安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============ 类型定义 ============
interface ColorPalette {
  bg: string;
  cardBg: string;
  deepBg: string;
  primary: string;
  secondary: string;
  accent: string;


    })
  }
}

在这里插入图片描述

在主入口组件Index部分,我们详细解析了@Entry和@Component装饰器的作用、十七个@State状态变量的设计意图、十三个selXxx数据变量的成对模式,以及build方法中的头部横幅、内容区Tab切换、底部Tab栏和弹窗挂载系统四个核心区域。我们深入分析了十六个弹窗Builder的实现细节,包括居中弹窗与底部抽屉的布局差异、领奖台柱状图的条件高度渲染、技能柱状图的百分比高度绑定、热销度进度条的水平百分比宽度绑定、纪念品弹窗的渐变头部设计、年卡弹窗的金属质感渐变效果等。

在Tab组件部分,我们逐一解析了ParkTab、DinoTab、FunTab、FoodTab、GiftTab和MineTab六个功能页面的结构布局、数据消费方式和回调通信模式。我们深入理解了Scroll容器的垂直滚动与水平滚动用法、ForEach列表渲染与@Builder方法的三元组合模式、斑马纹列表的交替背景色设计、横幅linearGradient渐变在不同Tab中的色彩区分策略,以及各种卡片组件(zoneCard、dinoCard、rideCard、menuCard、souvenirCard、taskCard、skillCard等)的结构差异和共性模式。

本应用展示了一套完整的ArkTS中大型应用的开发实践:通过接口类型系统建立数据层类型契约,通过const常量数组提供数据源,通过全局函数实现数据预处理与视图适配,通过@State状态变量管理应用状态,通过@Component自定义组件实现功能模块的解耦,通过@Builder方法实现UI片段的复用,通过条件渲染控制弹窗的显示与隐藏,通过ForEach实现列表数据的渲染,通过回调函数实现父子组件间的通信。这些技术点的系统组合构成了鸿蒙ArkTS应用开发的核心方法论。

Logo

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

更多推荐