HarmonyOS 6效果实现:渐变视觉渲染进阶:线性渐变叠加圆角组件的原生UI美化技术
在万物互联的时代,鸿蒙操作系统以其独特的分布式架构和全场景跨设备能力重新定义了操作系统的边界。ArkTS 作为鸿蒙生态的应用开发语言,在 TypeScript 的基础上进行了深度扩展与优化,引入了声明式 UI 范式、状态驱动渲染机制以及一套完善的装饰器系统,使开发者能够以极简的代码量构建出高性能、高可维护性的跨端应用。本文将以一个星际物流港管理平台为案例,逐段剖析 ArkTS 在类型系统、数据建模、组件化拆分、状态管理、布局编排、弹窗交互等各个层面的工程实践,全面揭示鸿蒙声明式 UI 的底层运作逻辑。
鸿蒙的 ArkUI 框架采用声明式编程范式,开发者只需描述界面在任意状态下的样子,框架会自动完成状态变化后的界面重新渲染。这种范式与传统的命令式 UI 编程(如 Android 的 findViewById + setText 模式)有着本质区别——开发者不再需要手动操作 DOM 或控件树,而是通过状态变量驱动视图更新。ArkTS 在此基础上进一步强化了类型安全,所有接口定义、变量声明、函数参数都具备严格的类型约束,编译期即可捕获大量潜在错误,这为大型应用的工程化协作提供了坚实保障。
组件化开发思想是本案例的核心设计哲学。整个星际物流港应用被拆分为一个入口组件和五个业务标签页组件,每个组件职责单一、边界清晰,组件之间通过回调函数实现松耦合通信。入口组件负责全局状态管理、标签页切换和弹窗挂载;各标签页组件负责各自业务区域的内容渲染和事件上报。这种分层架构使得代码可读性极高,任何一块业务逻辑的修改都不会波及其他区域,完美诠释了"高内聚、低耦合"的软件工程原则。
ArkTS 的装饰器系统是其声明式范式的灵魂所在。@Entry 标记入口组件,@Component 声明自定义组件,@State 定义可观察的状态变量,@Builder 封装可复用的 UI 片段——这些装饰器协同工作,构建出从数据到视图的完整映射链路。此外,ArkTS 还提供了丰富的布局容器组件(Column、Row、Stack、Flex、Scroll 等),通过链式属性方法配置尺寸、间距、对齐、颜色等视觉属性,使得 UI 的构建过程如同搭积木般直观且高效。
一、类型系统:接口定义与数据建模
1.1 色彩调色板接口
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 的接口,它是整个应用的色彩管理基石。在 ArkTS 中,interface 关键字用于声明对象的结构类型,与 TypeScript 的接口语法完全一致。接口本身不产生运行时对象,它只是一种编译期的类型契约,确保所有实现该接口的对象都包含指定的属性且类型正确。
这里声明了十六个字符串类型的属性,覆盖了应用所需的全部色彩语义:背景色(bg、cardBg、deepBg)用于构建多层次的深空视觉底景;主色调(primary、secondary、accent)负责强调核心交互元素;状态色(gold、danger、success、orange、purple)用于传递不同业务状态的视觉信号;文本色(textPrimary、textSecondary、textHint)形成三级文字层级,确保信息阅读的层次感和可访问性。
色彩系统的统一管理是专业前端工程的标配做法。通过将所有颜色值集中定义在一个接口和常量对象中,开发者可以在全局范围内保证配色一致性,避免散落在各处的硬编码色值导致视觉风格碎片化。当需要切换主题或调整品牌色调时,只需修改一处即可全局生效,这极大地降低了维护成本。
1.2 色彩常量实例
const COLORS: ColorPalette = {
bg: '#0A0E2A',
cardBg: '#131A45',
deepBg: '#080B22',
primary: '#40C4FF',
secondary: '#7C4DFF',
accent: '#18FFFF',
gold: '#FFD54F',
danger: '#FF5252',
success: '#69F0AE',
textPrimary: '#E8EAF6',
textSecondary: '#9FA8DA',
textHint: '#5C6BC0',
border: '#283593',
white: '#FFFFFF',
orange: '#FF8A65',
purple: '#B388FF'
};

在定义了 ColorPalette 接口之后,代码紧接着声明了一个 const 常量 COLORS 并将其类型标注为 ColorPalette。这个常量是整个应用唯一的色彩数据源。注意 const 关键字在 ArkTS 中的语义与 TypeScript 一致——它声明的是一个不可重新赋值的绑定,但对象本身的属性在运行时仍然是可变的(除非使用 readonly 修饰符)。不过在 ArkTS 的工程惯例中,这种常量对象通常被视为只读配置使用。
从色值来看,这套配色方案采用了一套深空科幻主题:背景色使用极深的藏蓝(#0A0E2A)和靛蓝(#131A45),营造出太空的幽暗深邃感;主色调使用亮蓝(#40C4FF)和紫罗兰(#7C4DFF),呈现科技感与神秘感;强调色使用青色(#18FFFF)和金色(#FFD54F),在深色背景上具有极高的对比度和辨识度。这种配色策略充分考虑了深色主题下的视觉层次,前景元素明亮跳脱,背景元素暗沉退后,形成强烈的景深效果。
1.3 舰船数据接口
interface SpaceShip {
id: number;
name: string;
icon: string;
cls: string;
price: number;
speed: number;
cargo: number;
fuel: number;
crew: number;
rating: number;
status: string;
desc: string;
}
SpaceShip 接口定义了星际舰船的完整数据模型。这个接口包含十二个属性,涵盖了舰船的身份标识(id、name、icon)、分类信息(cls)、经济属性(price、rating)、性能参数(speed、cargo、fuel、crew)、运行状态(status)和文字描述(desc)。每个属性都有明确的类型标注——number 用于数值型数据,string 用于文本型数据,这种精确的类型定义确保了在数据传递和使用过程中的类型安全。
在 ArkTS 的类型系统中,接口定义的属性是必需的(除非使用可选属性标记 ?),这意味着任何被标注为 SpaceShip 类型的对象都必须完整包含这十二个属性,缺少任何一个都会在编译期报错。这种严格性在大型应用开发中尤为重要——它可以防止因数据不完整而导致的运行时崩溃,例如在渲染舰船卡片时如果 name 属性缺失,整个界面可能会显示为 undefined,严重影响用户体验。
icon 属性使用 string 类型存储 emoji 表情符号,这是本应用的一个设计特色。在鸿蒙系统中,Text 组件可以直接渲染 Unicode emoji 字符,无需引入额外的图标资源文件或字体库,这大大简化了图标管理的复杂度。虽然 emoji 在精确度和可定制性上不如矢量图标,但对于快速原型开发和轻量级应用而言,这种方案在开发效率和视觉效果之间取得了良好的平衡。
1.4 航线与货物接口
interface SpaceRoute {
id: number;
from: string;
to: string;
icon: string;
distance: number;
time: string;
fee: number;
risk: string;
ships: number;
}
interface SpaceCargo {
id: number;
name: string;
icon: string;
weight: number;
price: number;
origin: string;
dest: string;
type: string;
urgency: string;
}

SpaceRoute 接口定义了星际航线的模型,包含起点(from)、终点(to)、距离(distance)、航时(time)、运费(fee)、风险等级(risk)和在航舰船数量(ships)等关键信息。航线是物流港运营的核心资产,这个数据模型完整描述了一条商业航线的全部商业要素,使前端能够据此渲染航线卡片、计算运费收益、评估风险水平。
SpaceCargo 接口定义了货物模型,包含货物名称(name)、重量(weight)、货值(price)、产地(origin)、目的地(dest)、类型分类(type)和紧急程度(urgency)。特别值得注意的是 urgency 字段——它使用字符串值如"加急"、“特急”、"普通"来表示紧急程度,这种设计在前端判断逻辑中非常直观,但相比枚举类型(enum)而言,缺乏编译期的拼写检查保护,存在字符串拼写错误导致逻辑失效的风险。
在 ArkTS 中,虽然支持
enum枚举类型,但在实际工程中很多开发者倾向于使用字符串字面量来表示有限状态值,原因是枚举在编译后会生成额外的运行时代码,而字符串字面量更轻量且在调试时可读性更好。不过这需要开发者在编码时格外注意一致性,或者配合联合类型(如type Urgency = '加急' | '特急' | '普通')来获得编译期检查。
1.5 舱段、任务与排行榜接口
interface SpaceModule {
id: number;
name: string;
icon: string;
level: number;
effect: string;
cost: number;
progress: number;
cls: string;
}
interface SpaceTask {
id: number;
title: string;
icon: string;
type: string;
reward: number;
status: string;
time: string;
crew: string;
}
interface SpaceRank {
id: number;
name: string;
icon: string;
title: string;
score: number;
missions: number;
badge: string;
}

SpaceModule 接口描述了舰船的可升级舱段,包含等级(level)、升级效果(effect)、升级费用(cost)、升级进度(progress)和分类(cls)。progress 字段是一个数值型百分比,在 UI 层面会被用于渲染进度条组件,直观展示舱段的升级完成度。这种将业务数据直接映射为视觉元素的设计,是声明式 UI 的典型应用场景。
SpaceTask 接口定义了值班任务模型,包含任务标题(title)、类型(type)、奖励(reward)、状态(status)、执行时间(time)和执行舰船(crew)。status 字段使用"进行中"和"已完成"两种字符串值,在后续的数据过滤函数中会频繁使用这个字段进行条件筛选。
SpaceRank 接口定义了舰员排行榜模型,包含姓名(name)、头衔(title)、积分(score)、任务数(missions)和徽章(badge)。排行榜的 id 字段在渲染领奖台时会作为特殊判断依据——id 为 1 的获得金牌、id 为 2 的获得银牌、id 为 3 的获得铜牌,这种基于 id 的条件渲染在领奖台 UI 中尤为常见。
1.6 通知、订单与技能接口
interface SpaceNotice {
id: number;
title: string;
date: string;
level: string;
content: string;
}
interface SpaceOrder {
id: number;
no: string;
cargo: string;
from: string;
to: string;
status: string;
eta: string;
fee: number;
}
interface SpaceSkill {
id: number;
name: string;
icon: string;
level: number;
exp: number;
type: string;
desc: string;
}

SpaceNotice 接口定义了星港公告的数据结构,其中 level 字段用于标识公告的重要程度(如"紧急"、“活动”、"升级"等),在前端会根据这个字段动态选择不同的背景色标签——紧急公告使用红色背景,其他公告使用蓝色背景。这种基于数据值动态切换样式的模式,是声明式 UI 框架的核心能力之一。
SpaceOrder 接口定义了运单模型,no 字段存储运单编号(如"SP-88213"),eta 字段存储预计到达时间。status 字段支持多种状态值——“运输中”、“待派舰”、“待签收”、“已签收”、“已送达”,在后续的过滤函数和 UI 样式切换中都会用到。
SpaceSkill 接口定义了舰员技能模型,exp 字段表示经验值,在技能柱状图弹窗中会根据 exp 值动态计算柱状图的高度比例(exp / 10 百分比),实现数据的可视化呈现。level 字段则用于技能解锁判断——只有 level 大于等于 3 的技能才会在图表中展示。
1.7 快捷入口、星球与船员接口
interface SpaceQuick {
id: number;
name: string;
icon: string;
color: string;
}
interface SpacePlanet {
id: number;
name: string;
icon: string;
type: string;
temp: string;
resource: string;
visit: number;
}
interface SpaceCrew {
id: number;
name: string;
icon: string;
role: string;
level: number;
exp: number;
skill: string;
rating: number;
}

SpaceQuick 接口定义了快捷功能入口,特别包含了一个 color 字段,每个快捷功能都关联一个主题色。在渲染快捷卡片时,这个颜色值会被拼接上透明度后缀(如 color + '26')作为图标背景色,使得每个功能入口都有独特的色彩标识,提升了视觉识别效率。
SpacePlanet 接口定义了星球模型,包含类型(type)、表面温度(temp)、主要资源(resource)和月度访客数(visit)。这些数据在星球详情弹窗中会以信息网格的形式展示,让用户一目了然地了解目标星球的探索价值。
SpaceCrew 接口定义了船员档案模型,包含角色(role)、等级(level)、经验值(exp)、擅长技能(skill)和评分(rating)。rating 字段在 UI 中会调用 toFixed(1) 方法格式化为一位小数,确保评分显示的一致性。exp 字段在经验进度条中被转换为百分比(exp / 10),可视化展示船员的成长进度。
二、数据层:静态数据源的构建
2.1 舰船数据集
const SHIPS: SpaceShip[] = [
{ id: 1, name: '星鹰号', icon: '🦅', cls: 'S级侦察舰', price: 8800, speed: 9, cargo: 40, fuel: 70, crew: 6, rating: 9.6, status: '待命', desc: '银翼流线舰体,搭载曲率引擎,适合快速突防与情报侦察。' },
{ id: 2, name: '曙光运输舰', icon: '🚀', cls: 'A级货运舰', price: 6200, speed: 6, cargo: 120, fuel: 85, crew: 12, rating: 9.2, status: '航行中', desc: '大容积货舱与双重护盾,星际物流港主力运力担当。' },
// ... 更多舰船数据
];

这段代码声明了一个 SHIPS 常量数组,类型标注为 SpaceShip[],即"SpaceShip 类型的数组"。数组中每个元素都是一个完整的舰船对象,严格按照 SpaceShip 接口定义的结构填充数据。ArkTS 的编译器会在此处进行严格的类型检查——如果任何一个对象缺少必要属性或属性值类型不匹配,编译将失败。
从数据内容来看,这个数据集设计了八艘不同定位的舰船,覆盖了侦察、货运、科考、联络、武装、补给、特勤、客运等多种用途。每艘舰船的数值参数都经过精心平衡:侦察舰速度高但载重低,运输舰载重大但速度慢,武装舰火力强但造价高。这种数据设计不仅丰富了应用的内容深度,也为后续的卡片渲染、排序筛选和详情展示提供了丰富的数据素材。
在实际工程中,这类静态数据通常会从后端 API 动态获取,但在本案例中采用硬编码方式定义。这种做法在原型开发、Demo 演示或离线应用中非常常见,它的优势在于无需网络请求即可立即渲染界面,便于快速验证 UI 设计和交互逻辑。const 关键字确保了这个数组引用不会被重新赋值,保证了数据的稳定性。
2.2 航线与货物数据
const ROUTES: SpaceRoute[] = [
{ id: 1, from: '零号港', to: '月神星', icon: '🌙', distance: 1200, time: '6小时', fee: 2400, risk: '低', ships: 12 },
{ id: 2, from: '零号港', to: '红岩矿星', icon: '⛰', distance: 3800, time: '18小时', fee: 6800, risk: '中', ships: 8 },
// ... 更多航线数据
];
const CARGO_ITEMS: SpaceCargo[] = [
{ id: 1, name: '反物质电池', icon: '🔋', weight: 2, price: 12000, origin: '零号港', dest: '风暴星', type: '能源', urgency: '加急' },
{ id: 2, name: '星尘矿石', icon: '⛏', weight: 48, price: 3600, origin: '红岩矿星', dest: '零号港', type: '矿物', urgency: '普通' },
// ... 更多货物数据
];
ROUTES 数组定义了八条星际航线,所有航线都以"零号港"或其他星球作为起点和终点,构成了一张以零号港为中心的星际航线网络。distance 和 fee 字段呈现明显的正相关关系——距离越远运费越高,这符合现实中的物流定价逻辑。risk 字段使用"低"、“中”、“高”、"极高"四个等级来标注航线风险,在后续的航线卡片渲染中会根据风险等级动态选择不同的文字颜色。
CARGO_ITEMS 数组定义了八种货物,每种货物都有不同的类型分类和紧急程度。urgency 字段的值——“加急”、“特急”、“普通”——在后续的 getHotCargo() 过滤函数中作为筛选条件,只有加急和特急的货物才会被归类为"热货"并在横向滑动区域中展示。这种基于数据属性的内容分类展示策略,使信息密度高的列表页面能够层次分明地呈现核心信息。
2.3 舱段、任务与排行数据
const MODULES: SpaceModule[] = [
{ id: 1, name: '曲率引擎舱', icon: '🌀', level: 5, effect: '航速 +18%', cost: 8800, progress: 82, cls: '动力' },
{ id: 2, name: '光子护盾', icon: '🛡', level: 4, effect: '防御 +15%', cost: 6600, progress: 64, cls: '防御' },
// ... 更多舱段数据
];
const SPACE_TASKS: SpaceTask[] = [
{ id: 1, title: '护航红岩矿星编队', icon: '🛰', type: '护航任务', reward: 3600, status: '进行中', time: '今 22:00', crew: '熔岩重装舰' },
// ... 更多任务数据
];
const SPACE_RANKS: SpaceRank[] = [
{ id: 1, name: '林星野', icon: '🦅', title: '星际舰长', score: 12800, missions: 96, badge: '星冠' },
// ... 更多排行数据
];

这三个数据集分别支撑了装备管理、任务管理和排行榜三大功能模块。MODULES 中的 progress 字段是一个 0-100 的数值,在舱段升级弹窗中会被用于计算进度条的宽度百分比(this.selModule!.progress + '%'),实现数据的实时可视化。SPACE_TASKS 的 status 字段将任务分为"进行中"和"已完成"两类,在"我的"标签页中这两类任务会分别展示在不同的区域。
SPACE_RANKS 数据集的前三条记录(id 1-3)代表领奖台的前三名,在排行榜弹窗中会以特殊的三栏柱状图样式展示——第一名柱子最高且为金色,第二名次高且为紫色,第三名最矮且为橙色。这种视觉差异化完全基于数据的 id 字段动态计算,充分体现了声明式 UI"数据驱动视图"的设计理念。
2.4 通知、订单与技能数据
const SPACE_NOTICES: SpaceNotice[] = [
{ id: 1, title: '风暴星航线临时限航', date: '08-27', level: '紧急', content: '风暴星周边离子风暴增强,今晚 20:00 起该航线暂停派舰。' },
// ... 更多通知数据
];
const SPACE_ORDERS: SpaceOrder[] = [
{ id: 1, no: 'SP-88213', cargo: '反物质电池×2', from: '零号港', to: '风暴星', status: '运输中', eta: '明天 14:00', fee: 24000 },
// ... 更多订单数据
];
const SPACE_SKILLS: SpaceSkill[] = [
{ id: 1, name: '超空间导航', icon: '🧭', level: 5, exp: 800, type: '领航', desc: '精通折叠航路规划,缩短航程 18%。' },
// ... 更多技能数据
];
这三个数据集分别支撑了通知公告、运单追踪和技能图谱功能。SPACE_NOTICES 的 level 字段使用了"紧急"、“活动”、“升级”、“评选”、“安全”、"培训"等多种值,在通知行和通知弹窗中会根据是否为"紧急"来决定使用红色还是蓝色标签背景,实现视觉上的风险分级提示。
SPACE_ORDERS 的 status 字段支持"运输中"、“待派舰”、“待签收”、“已签收”、"已送达"五种状态。在 getOrderActive() 函数中,只有前三种状态(即未完成的订单)才会被筛选出来在运单列表中展示,已完成的订单则被排除在外,保证了列表的内容聚焦性。
SPACE_SKILLS 的 exp 字段表示经验值,范围从 60 到 800。在技能柱状图弹窗中,每个技能的柱子高度按 exp / 10 的百分比计算,经验值越高的技能柱子越高,形成直观的数据对比可视化。level 字段在 getUnlockedSkills() 函数中作为筛选条件——只有 level 大于等于 3 的技能才被视为"已解锁"并展示在图表中。
2.5 快捷入口、星球与船员数据
const SPACE_QUICKS: SpaceQuick[] = [
{ id: 1, name: '出航登记', icon: '🚀', color: '#40C4FF' },
{ id: 2, name: '货运下单', icon: '📦', color: '#7C4DFF' },
// ... 更多快捷入口
];
const PLANETS: SpacePlanet[] = [
{ id: 1, name: '月神星', icon: '🌙', type: '宜居星', temp: '12°C', resource: '水冰矿', visit: 3200 },
// ... 更多星球数据
];
const CREW: SpaceCrew[] = [
{ id: 1, name: '林星野', icon: '🦅', role: '舰长', level: 9, exp: 860, skill: '超空间导航', rating: 9.8 },
// ... 更多船员数据
];
SPACE_QUICKS 数据集定义了八个快捷功能入口,每个入口关联一个独立的主题色。在 quickCard 构建器中,这个颜色值会被拼接透明度后缀 '26'(即约 15% 不透明度的十六进制值)作为图标容器的背景色,使得每个功能卡片都呈现出与自身功能语义相匹配的色彩氛围——出航登记用蓝色、货运下单用紫色、紧急呼叫用红色等,色彩本身就成为了功能识别的辅助线索。
PLANETS 数据集定义了六颗星球,每颗星球都有不同的类型(宜居星、矿业星、冰封星等)和温度特征。这些数据在航线页的星球横向滑动区域和星球详情弹窗中都会被使用。visit 字段表示月度访客数,在星球弹窗的信息网格中展示,帮助用户评估星球的活跃程度。
CREW 数据集定义了六名船员,包含舰长、领航员、轮机长、医官、炮手、通讯官等不同角色。exp 字段在船员档案弹窗中被转换为进度条百分比(exp / 10),配合 exp 的最大值 1000,直观展示船员距离下一级的成长进度。rating 字段在弹窗中同时以 toFixed(1) 格式化展示和直接字符串拼接展示两种方式使用,确保评分数据的显示一致性。
三、全局函数:数据加工与筛选层
3.1 双列拆分函数模式
function getShipLeft(): SpaceShip[] {
let arr: SpaceShip[] = [];
for (let i = 0; i < SHIPS.length; i++) {
if (i % 2 === 0) {
arr.push(SHIPS[i]);
}
}
return arr;
}
function getShipRight(): SpaceShip[] {
let arr: SpaceShip[] = [];
for (let i = 0; i < SHIPS.length; i++) {
if (i % 2 === 1) {
arr.push(SHIPS[i]);
}
}
return arr;
}

这对函数实现了一个典型的"双列拆分"模式——将一个数组按索引奇偶性拆分为两个子数组。getShipLeft() 返回索引为偶数的元素(第 0、2、4、6 个),getShipRight() 返回索引为奇数的元素(第 1、3、5、7 个)。在 UI 层面,这两个子数组分别被注入到左右两列的 ForEach 渲染中,实现瀑布流式的双列布局。
这种拆分策略在移动端内容列表中极为常见。在窄屏设备上,单列展示信息密度过低,而双列布局可以更充分地利用屏幕宽度。通过奇偶拆分而非直接数组切片(slice),代码逻辑更加直观,且不依赖数组的总长度——无论原始数组有多少个元素,左右两列的数量差最多为一,保证了视觉上的平衡感。
函数内部使用了传统的 for 循环和 if 条件判断,这在 ArkTS 中是完全合法的语法。虽然函数式编程风格(如使用 filter 方法)可以更简洁地实现同样的逻辑(SHIPS.filter((_, i) => i % 2 === 0)),但本案例选择了命令式风格,可能是出于代码可读性和执行效率的考量。let arr: SpaceShip[] = [] 声明了一个空数组并标注了类型,然后通过 push 方法逐个添加符合条件的元素,最后返回填充好的数组。
在 ArkTS 中,函数的参数和返回值都需要明确的类型标注。这种强制类型声明的做法虽然增加了代码量,但带来了显著的工程价值:编译器能够进行更严格的类型检查,IDE 能够提供更精准的代码补全,团队协作时函数的输入输出契约一目了然。对于大型项目而言,这种类型安全性是不可妥协的底线。
3.2 条件过滤函数
function getTopShips(): SpaceShip[] {
let arr: SpaceShip[] = [];
for (let i = 0; i < SHIPS.length; i++) {
if (SHIPS[i].rating >= 9.5) {
arr.push(SHIPS[i]);
}
}
return arr;
}
function getHotCargo(): SpaceCargo[] {
let arr: SpaceCargo[] = [];
for (let i = 0; i < CARGO_ITEMS.length; i++) {
if (CARGO_ITEMS[i].urgency === '加急' || CARGO_ITEMS[i].urgency === '特急') {
arr.push(CARGO_ITEMS[i]);
}
}
return arr;
}
function getRunningTasks(): SpaceTask[] {
let arr: SpaceTask[] = [];
for (let i = 0; i < SPACE_TASKS.length; i++) {
if (SPACE_TASKS[i].status === '进行中') {
arr.push(SPACE_TASKS[i]);
}
}
return arr;
}
这三个函数都是基于业务条件的过滤函数。getTopShips() 筛选评分大于等于 9.5 的高分舰船,用于"王牌舰船横向滑动"区域展示;getHotCargo() 筛选紧急程度为"加急"或"特急"的货物,用于"热货横向滑动"和"补给申领"弹窗展示;getRunningTasks() 筛选状态为"进行中"的任务,用于"我的"页面今日值班区域展示。
这三个函数共同体现了同一种设计模式——将数据筛选逻辑从 UI 组件中剥离,集中到独立的函数中处理。这样做的好处是多方面的:首先,UI 组件只需调用函数获取最终数据,代码更加简洁;其次,筛选逻辑可以被多个组件复用(如 getHotCargo() 同时被货运页和补给弹窗使用);最后,当筛选条件需要调整时,只需修改一处函数即可全局生效,维护成本极低。
值得注意的是 getHotCargo() 函数中的条件判断使用了逻辑或运算符 ||,将两个紧急程度条件组合在一起。这种字符串等值比较在 ArkTS 中是值比较(而非引用比较),与 JavaScript 的行为一致。但由于字符串比较是区分大小写的,如果数据源中"加急"被误写为"加急 "(多一个空格),过滤就会失效——这再次说明了字符串字面量作为状态值的潜在风险。
3.3 数量统计与区间截取函数
function getShipCount(): number {
return SHIPS.length;
}
function getRouteCount(): number {
return ROUTES.length;
}
function getCargoCount(): number {
return CARGO_ITEMS.length;
}
function getRankTop(): SpaceRank[] {
let arr: SpaceRank[] = [];
for (let i = 0; i < SPACE_RANKS.length; i++) {
if (i < 3) {
arr.push(SPACE_RANKS[i]);
}
}
return arr;
}
function getRankRest(): SpaceRank[] {
let arr: SpaceRank[] = [];
for (let i = 0; i < SPACE_RANKS.length; i++) {
if (i >= 3) {
arr.push(SPACE_RANKS[i]);
}
}
return arr;
}
数量统计函数(getShipCount、getRouteCount、getCargoCount 等)非常简单——直接返回数组的 length 属性。虽然这些函数的内部实现极其简单,但将它们封装为独立函数具有重要的工程意义:它为 UI 层提供了一个语义化的数据获取接口,调用方代码如 getShipCount() 比直接访问 SHIPS.length 更具可读性,且如果未来数据源发生变化(如改为从 API 获取),只需修改函数内部实现即可,UI 层无需任何改动。
getRankTop() 和 getRankRest() 这对函数实现了排行榜数据的区间截取——前三名和其余名次。这种设计是为了在排行榜弹窗中分别渲染领奖台区域(前三名的特殊视觉处理)和普通列表区域(第四名以后的常规行展示)。通过数据层的拆分,UI 层的 ForEach 可以直接遍历各自的数据集,无需在渲染时进行条件判断,简化了模板代码的复杂度。
3.4 运单与技能过滤函数
function getOrderActive(): SpaceOrder[] {
let arr: SpaceOrder[] = [];
for (let i = 0; i < SPACE_ORDERS.length; i++) {
if (SPACE_ORDERS[i].status === '运输中' || SPACE_ORDERS[i].status === '待派舰' || SPACE_ORDERS[i].status === '待签收') {
arr.push(SPACE_ORDERS[i]);
}
}
return arr;
}
function getUnlockedSkills(): SpaceSkill[] {
let arr: SpaceSkill[] = [];
for (let i = 0; i < SPACE_SKILLS.length; i++) {
if (SPACE_SKILLS[i].level >= 3) {
arr.push(SPACE_SKILLS[i]);
}
}
return arr;
}
getOrderActive() 函数使用三个字符串等值条件通过逻辑或运算符连接,筛选出所有未完成状态的运单。这种多条件组合的筛选逻辑在实际业务中非常常见,比如电商应用中筛选"待付款"、“待发货”、"待收货"的订单。虽然代码看起来略长,但逻辑清晰直观,易于理解和维护。
getUnlockedSkills() 函数使用数值比较 level >= 3 作为筛选条件,这种数值阈值比较比字符串比较更可靠——不存在拼写错误的风险,且比较语义更明确。在技能柱状图弹窗中,只有解锁的技能(level >= 3)才会被渲染为柱状图的柱子,未解锁的技能则被隐藏,保证了图表的信息密度和可读性。
四、入口组件:状态管理与整体架构
4.1 组件声明与状态定义
@Entry
@Component
struct Index {
@State currentTab: number = 0
@State showShip: boolean = false
@State showLaunch: boolean = false
@State showRoute: boolean = false
@State showCargo: boolean = false
@State showUnload: boolean = false
@State showModule: boolean = false
@State showPlanet: boolean = false
@State showCrew: boolean = false
@State showTask: boolean = false
@State showRank: boolean = false
@State showNotice: boolean = false
@State showOrder: boolean = false
@State showSkill: boolean = false
@State showDock: boolean = false
@State showVip: boolean = false
@State showSupply: boolean = false
这段代码是入口组件的核心声明部分。@Entry 装饰器标记此组件为应用的入口页面,一个 ArkTS 页面有且只有一个 @Entry 组件。@Component 装饰器声明这是一个自定义组件——在鸿蒙的 ArkUI 框架中,所有的自定义组件都必须使用 @Component 装饰器标记,框架会据此生成组件的生命周期管理代码和状态观察机制。struct 关键字定义了一个结构体,这是 ArkTS 组件的语法载体——与 TypeScript 中的 class 不同,struct 更适合描述不可变的值类型,但在 ArkTS 中被扩展为支持状态管理的组件容器。
@State 装饰器是 ArkTS 状态管理系统的核心。被 @State 标记的变量会成为可观察的状态变量——当其值发生变化时,ArkUI 框架会自动重新调用 build() 方法,更新所有依赖该状态的 UI 元素。这种响应式机制类似于 Vue 的 data 或 React 的 useState,但 ArkTS 的实现更加轻量,无需显式调用 setState,直接赋值即可触发更新。
这里定义了十七个状态变量,可分为两类:currentTab 控制当前激活的标签页索引(0-4),十六个 show* 布尔变量分别控制十六个弹窗的显示与隐藏。这种"每个弹窗一个布尔状态"的设计模式简单直观,但在弹窗数量较多时会导致状态变量膨胀。在更复杂的场景中,可以考虑使用对象状态(如 { type: 'ship', visible: true, data: selShip })来统一管理弹窗状态,减少状态变量的数量。
@State装饰器的工作原理值得深入理解。当组件被创建时,ArkUI 框架会为每个@State变量创建一个 Proxy 代理对象。当开发者修改变量值时,Proxy 的 set 拦截器会被触发,框架记录变更并调度一次重新渲染。在渲染过程中,框架通过虚拟 DOM 的 diff 算法,只更新真正发生变化的部分,而非全量重建整个组件树。这种细粒度的更新机制保证了状态变化时的渲染性能。
4.2 选中数据引用
selShip: SpaceShip | null = null
selRoute: SpaceRoute | null = null
selCargo: SpaceCargo | null = null
selModule: SpaceModule | null = null
selPlanet: SpacePlanet | null = null
selCrew: SpaceCrew | null = null
selTask: SpaceTask | null = null
selRank: SpaceRank | null = null
selNotice: SpaceNotice | null = null
selOrder: SpaceOrder | null = null
selSkill: SpaceSkill | null = null
selVip: SpaceCrew | null = null
这组变量用于存储当前被选中并需要在弹窗中展示的数据对象。类型标注使用了联合类型 SpaceShip | null——这意味着变量可以是一个 SpaceShip 对象,也可以是 null。初始值为 null,表示页面初始加载时没有任何被选中的数据。
这些变量没有被 @State 装饰器标记,这意味着它们不是可观察的状态变量。当这些变量的值发生变化时,不会直接触发 UI 重新渲染。这是合理的设计——因为这些变量的变化总是伴随着对应的 show* 状态变量的变化(先设置选中数据,再设置 show 为 true),而 show* 状态变量的变化已经足够触发 UI 更新,届时这些选中数据的值也已经被正确设置。
这种"先赋值选中数据,再触发显示状态"的编程模式贯穿了整个应用的事件处理逻辑。它的优点是保证了在弹窗渲染时选中数据已经就绪,避免了空值引用的风险。但同时也要求开发者在每个弹窗的渲染条件中添加 null 检查(如 if (this.showShip && this.selShip !== null)),确保不会在数据未就绪时渲染弹窗。
4.3 build 方法:整体布局骨架
build() {
Column() {
// ============ 头部(星港横幅) ============
Column({ space: 10 }) {
Row() {
Column({ space: 2 }) {
Text('SPACE PORT').fontSize(12).fontColor(COLORS.accent).fontWeight(FontWeight.Bold).letterSpacing(2)
Text('星际物流港 · 员工中心').fontSize(19).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('🔔').fontSize(20)
Text('8').fontSize(10).fontColor(COLORS.white).backgroundColor(COLORS.danger).borderRadius(8).width(16).height(16).textAlign(TextAlign.Center)
}
.width('100%')
build() 方法是每个 ArkTS 组件的必备方法,它返回组件的 UI 结构。在 ArkTS 的声明式语法中,UI 结构不是通过 JSX 或模板字符串描述的,而是通过嵌套的函数调用——Column() 创建一个纵向布局容器,Row() 创建一个横向布局容器,Text() 创建一个文本组件。这种"函数即组件"的语法风格与 SwiftUI 非常相似。
最外层的 Column() 将整个页面分为三个纵向区域:头部横幅、内容区(layoutWeight(1) 使其占据剩余空间)、底部标签栏。Column({ space: 10 }) 的参数对象中的 space 属性定义了子元素之间的间距为 10 像素,这是 ArkTS 布局容器的一个便捷特性——无需为每个子元素单独设置 margin,直接在容器级别统一控制间距。
头部横幅的构建展示了 ArkTS 声明式链式属性方法的精髓。以 Text('SPACE PORT') 为例,它通过链式调用 .fontSize(12) 设置字号、.fontColor(COLORS.accent) 设置颜色、.fontWeight(FontWeight.Bold) 设置粗细、.letterSpacing(2) 设置字符间距。这种链式调用风格使 UI 的视觉属性配置一目了然,可读性远超传统的 XML 属性配置方式。
在头部布局中,Column 使用 .alignItems(HorizontalAlign.Start) 设置子元素左对齐,.layoutWeight(1) 使其占据 Row 中的剩余空间。通知铃铛后的红色数字角标"8"使用了一系列属性方法:backgroundColor(COLORS.danger) 设置红色背景、borderRadius(8) 设置圆角、width(16).height(16) 设置固定尺寸、textAlign(TextAlign.Center) 使文字居中。这种通过样式属性实现角标效果的方式,展示了 ArkTS 在不使用额外图标库的情况下实现复杂视觉效果的能力。
4.4 头部搜索栏与渐变背景
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: [['#0A0E2A', 0.0], ['#131A45', 0.55], ['#7C4DFF', 1.0]] })
搜索栏是一个 Row 容器,包含一个放大镜 emoji 和一段占位提示文字。.backgroundColor('rgba(255,255,255,0.10)') 使用了带透明度的白色背景——rgba 色值中的最后一个分量 0.10 表示 10% 的不透明度,在深色背景上呈现为半透明的浅色蒙层效果。.borderRadius(20) 配合 .height(40) 创建了一个胶囊形(pill-shaped)的搜索框外观。
整个头部 Column 容器使用了 .linearGradient() 方法设置线性渐变背景。渐变参数中,angle: 135 指定了渐变方向为 135 度(从左上到右下),colors 数组定义了三个色标:起点 #0A0E2A(深藏蓝,位置 0.0)、中间 #131A45(靛蓝,位置 0.55)、终点 #7C4DFF(紫罗兰,位置 1.0)。这三个色标沿对角线方向平滑过渡,营造出从深空到星云的视觉纵深感。
linearGradient是 ArkUI 中实现渐变背景的核心方法。它的colors参数是一个二维数组,每个元素是一个[color, position]的元组——color 是 CSS 颜色值字符串,position 是 0.0 到 1.0 之间的位置比例。通过精心设计色标的位置和颜色,可以创建出从简单的双色渐变到复杂的多色星云效果。在深色主题应用中,渐变背景是营造氛围感和视觉层次的关键手段。
4.5 内容区:标签页条件渲染
// ============ 内容区 ============
Column() {
if (this.currentTab === 0) {
DockTab({
onShip: (s: SpaceShip) => {
this.selShip = s
this.showShip = true
},
onLaunch: (s: SpaceShip) => {
this.selShip = s
this.showLaunch = true
},
onCrew: (c: SpaceCrew) => {
this.selCrew = c
this.showCrew = true
},
// ... 更多回调
})
} else if (this.currentTab === 1) {
RouteTab({
onRoute: (r: SpaceRoute) => {
this.selRoute = r
this.showRoute = true
},
// ... 更多回调
})
} else if (this.currentTab === 2) {
// CargoTab
} else if (this.currentTab === 3) {
// EquipTab
} else {
// MineTab
}
}
.layoutWeight(1)
内容区是页面的主体区域,通过 if-else if-else 条件分支根据 this.currentTab 的值渲染不同的标签页组件。这种条件渲染模式是 ArkTS 中实现标签页切换的标准做法——当 currentTab 的值变化时,@State 的响应式机制会触发 build() 重新执行,旧标签页组件被卸载,新标签页组件被挂载。
这种模式的底层机制值得深入理解。ArkUI 框架在执行 build() 时会构建一棵组件树(虚拟 DOM)。当 currentTab 从 0 变为 1 时,框架会比较新旧组件树的差异——发现 DockTab 被替换为 RouteTab,于是销毁 DockTab 实例(触发其 aboutToDisappear 生命周期),创建 RouteTab 实例(触发其 aboutToAppear 生命周期)。这种基于 diff 的更新策略保证了只更新真正发生变化的组件部分。
注意标签页组件的使用方式——组件名后跟参数对象,对象中传递的是回调函数。以 DockTab 为例,它接收 onShip、onLaunch、onCrew、onRank、onNotice、onPlanet 六个回调函数,每个回调在被触发时会设置对应的选中数据和显示状态。这种"子组件通过回调通知父组件"的模式是 ArkTS 组件间通信的标准方式——子组件不直接修改父组件的状态,而是通过回调函数将事件"上报"给父组件,由父组件决定如何更新状态。
.layoutWeight(1) 使内容区占据 Column 中除头部和底部标签栏之外的所有剩余空间。layoutWeight 的值是一个权重比例——当多个子元素都设置了 layoutWeight 时,剩余空间按权重比例分配。这里只有一个元素设置了 layoutWeight(1),所以它独占全部剩余空间。
4.6 底部标签栏
// ============ 底部 Tab ============
Row() {
this.bottomTabItem('🚀', '船坞', 0)
this.bottomTabItem('🛰', '航线', 1)
this.bottomTabItem('📦', '货运', 2)
this.bottomTabItem('⚙', '装备', 3)
this.bottomTabItem('👤', '我的', 4)
}
.width('100%')
.height(64)
.backgroundColor('#080B22')
.border({ width: 1, color: '#283593' })
底部标签栏是一个 Row 容器,包含五个 this.bottomTabItem() 调用——这调用了同一个 @Builder 方法五次,每次传入不同的图标、标签文字和索引值。@Builder 方法是 ArkTS 中实现 UI 片段复用的机制,被 @Builder 装饰的方法返回一个可复用的 UI 结构,可以像函数一样被调用。
底部 Row 容器的样式设置体现了细节的精细控制:.height(64) 给标签栏固定高度,.backgroundColor('#080B22') 设置极深的背景色,.border({ width: 1, color: '#283593' }) 添加一条 1 像素宽的顶部边框线。这条边框线虽然细微,但在视觉上将标签栏与内容区清晰分隔,是移动端底部导航栏的标准视觉处理。
4.7 弹窗挂载区
// ============ 弹窗挂载 ============
if (this.showShip && this.selShip !== null) {
this.modalOverlay(() => { this.showShip = false })
this.shipModal()
}
if (this.showLaunch && this.selShip !== null) {
this.modalOverlay(() => { this.showLaunch = false })
this.launchModal()
}
// ... 更多弹窗条件
弹窗挂载区是整个入口组件中最具特色的架构设计。每个弹窗的渲染由一个 if 条件控制——条件为 show* 状态变量为 true 且对应的选中数据不为 null。当条件为 true 时,先渲染 modalOverlay(半透明遮罩层),再渲染具体的弹窗内容构建器。
modalOverlay 接收一个箭头函数作为参数 () => { this.showShip = false },这个函数被存储为遮罩层的关闭回调。当用户点击遮罩层时,这个回调被触发,将对应的 show 状态设为 false,弹窗随之消失。这种"回调注入"模式使得同一个 modalOverlay 构建器可以被所有弹窗复用——每个弹窗传入自己的关闭逻辑即可。
这种设计模式的优势在于:弹窗的显示/隐藏完全由状态变量驱动,所有弹窗的 UI 结构都内联在 build() 方法中,框架的响应式机制自动管理弹窗的挂载和卸载。相比传统命令式弹窗(如 Android 的 Dialog.show()),这种声明式弹窗不存在"忘记 dismiss"的内存泄漏风险,也不会出现弹窗状态与数据不同步的问题。
注意每个弹窗条件中的双重检查
this.showShip && this.selShip !== null。这种防御性编程确保了即使 show 状态意外变为 true 但选中数据尚未设置时,弹窗也不会被渲染——因为弹窗内部的@Builder方法会使用非空断言!(如this.selShip!.name)访问选中数据的属性,如果选中数据为 null,非空断言会导致运行时崩溃。双重检查是最后一道安全防线。
五、@Builder 构建器:可复用 UI 片段
5.1 底部标签项构建器
@Builder
bottomTabItem(icon: string, label: string, idx: number) {
Column({ space: 2 }) {
Text(icon).fontSize(18)
Text(label).fontSize(10)
}
.width('20%')
.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 装饰器将 bottomTabItem 方法标记为一个可复用的 UI 构建器。它接收三个参数:icon(emoji 图标字符串)、label(标签文字)、idx(标签索引)。构建器内部定义了一个 Column 容器,垂直排列图标和文字,形成标准的移动端底部导航项样式。
这个构建器展示了 ArkTS 中条件样式的高效写法。.scale() 方法使用三元运算符 this.currentTab === idx ? 1.1 : 1.0 动态计算缩放比例——当前激活的标签页放大 1.1 倍,未激活的保持原始大小。.opacity() 方法同理——激活的完全不透明(opacity 为 1),未激活的半透明(opacity 为 0.45)。这种通过条件表达式直接驱动视觉差异的方式,是声明式 UI 最核心的能力体现。
.width('20%') 使用百分比宽度——底部标签栏有五个标签项,每个占据 20% 的宽度,正好铺满整个 Row 容器。.justifyContent(FlexAlign.Center) 设置 Column 内部子元素的主轴(垂直方向)居中对齐。FlexAlign 是 ArkUI 中定义弹性布局对齐方式的枚举类型,其值包括 Start、Center、End、SpaceBetween、SpaceAround、SpaceEvenly 等。
onClick 事件处理器是一个箭头函数,点击时将 this.currentTab 设置为当前标签的索引值 idx。由于 currentTab 是 @State 变量,赋值后会自动触发 build() 重新执行,旧标签页组件被替换为新标签页组件,同时 bottomTabItem 中的三元条件也会重新计算,激活状态视觉切换到新标签。整个交互流程完全由状态驱动,无需任何命令式 DOM 操作。
5.2 弹窗遮罩层构建器
@Builder
modalOverlay(onClose: () => void) {
Column() {
}
.width('100%')
.height('100%')
.backgroundColor('rgba(4,6,20,0.78)')
.onClick(() => {
onClose()
})
.position({ x: 0, y: 0 })
}
modalOverlay 构建器创建了一个全屏半透明遮罩层。它接收一个 onClose 回调函数作为参数,这个函数在用户点击遮罩层时被调用。构建器内部是一个空的 Column() 容器——它不包含任何子元素,纯粹作为一个可点击的区域占位。
遮罩层的样式通过一系列属性方法设置:.width('100%').height('100%') 使其铺满整个父容器;.backgroundColor('rgba(4,6,20,0.78)') 使用极深的蓝色配合 78% 的不透明度,营造出背景被压暗的弹窗遮罩效果;.position({ x: 0, y: 0 }) 使用绝对定位将遮罩层固定在父容器的左上角原点位置。
position方法用于设置元素的绝对定位。在 ArkUI 的布局系统中,默认情况下子元素按照 Flex 布局规则流动排列。但当一个元素设置了position后,它会脱离正常文档流,基于父容器的坐标系进行绝对定位。这里将遮罩层定位到 (0, 0),确保它从父容器左上角开始铺满,覆盖在所有正常流元素之上。弹窗内容随后渲染在遮罩层之上(因为构建顺序在后),形成"遮罩层 → 弹窗内容"的 z-order 叠加关系。
5.3 舰船详情弹窗
@Builder
shipModal() {
Column() {
Column({ space: 10 }) {
Row() {
Text(this.selShip!.icon).fontSize(40).width(76).height(76).textAlign(TextAlign.Center).backgroundColor('rgba(64,196,255,0.14)').borderRadius(20)
Column({ space: 4 }) {
Text(this.selShip!.name).fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text(this.selShip!.cls).fontSize(12).fontColor(COLORS.primary)
Text(this.selShip!.status + ' | 评分 ' + this.selShip!.rating).fontSize(11).fontColor(COLORS.gold)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showShip = false })
}
.width('100%')
shipModal 构建器是舰船详情弹窗的 UI 定义。弹窗的最外层是一个全屏 Column 容器(.width('100%').height('100%')),使用 .justifyContent(FlexAlign.Center) 使其子元素垂直居中——这样弹窗内容卡片就会显示在屏幕中央。内层的 Column({ space: 10 }) 是实际的弹窗卡片容器,宽度设为 88% 居中显示。
弹窗卡片的第一行是头部区域,使用 Row 横向排列三个元素:左侧的舰船图标容器(固定 76x76 尺寸,圆角背景,内含 40 号字体的 emoji 图标)、中间的舰船信息列(名称、类型、状态+评分)、右侧的关闭按钮。中间的 Column 使用 .layoutWeight(1) 占据 Row 中的剩余空间,.alignItems(HorizontalAlign.Start) 使文字左对齐。
值得注意的是所有数据访问都使用了非空断言操作符 !(如 this.selShip!.icon)。这是因为在 TypeScript/ArkTS 中,selShip 的类型是 SpaceShip | null,编译器要求在访问其属性前进行 null 检查。但由于弹窗的渲染条件已经确保了 selShip !== null,开发者使用 ! 断言告诉编译器"我确信这里不为 null"。这是一种有风险的简化写法——如果渲染条件被意外移除或修改,运行时将抛出空引用异常。
5.4 舰船规格四格
Row({ space: 8 }) {
Column({ space: 2 }) {
Text('航速').fontSize(10).fontColor(COLORS.textSecondary)
Text(this.selShip!.speed + ' 光速').fontSize(13).fontColor(COLORS.accent).fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.padding(8)
.backgroundColor('rgba(24,255,255,0.08)')
.borderRadius(10)
Column({ space: 2 }) {
Text('载重').fontSize(10).fontColor(COLORS.textSecondary)
Text(this.selShip!.cargo + ' 吨').fontSize(13).fontColor(COLORS.primary).fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.padding(8)
.backgroundColor('rgba(64,196,255,0.10)')
.borderRadius(10)
// ... 燃料、船员两个格子
}
.width('100%')
这段代码构建了弹窗中的"四格规格"区域——航速、载重、燃料、船员四个指标卡片横向排列在一个 Row 中。每个格子都是一个 Column 容器,包含标签文字(如"航速",10 号字体,次要文字色)和数值文字(如"9 光速",13 号字体,强调色,粗体)。
四个格子使用 .layoutWeight(1) 实现等宽分布——每个格子占据 Row 中相等的份额。.padding(8) 给每个格子添加内边距,.borderRadius(10) 添加圆角,.backgroundColor() 使用不同色相的低透明度背景色区分不同指标——航速用青色背景、载重用蓝色背景、燃料用金色背景、船员用紫色背景。这种色彩编码使四个指标的视觉区分度更高,用户可以快速定位关心的数据。
数值文字的颜色与背景色保持同色系但更饱和——如航速的背景是 rgba(24,255,255,0.08)(青色 8% 透明度),数值文字色是 COLORS.accent(即 #18FFFF 纯青色)。这种"同色系背景+文字"的配色策略,既保证了视觉关联性,又通过透明度差异实现了层次感。
5.5 出航确认弹窗(底部抽屉样式)
@Builder
launchModal() {
Column() {
Column({ space: 12 }) {
Row() {
Text('🚀').fontSize(30).width(56).height(56).textAlign(TextAlign.Center).backgroundColor('rgba(64,196,255,0.16)').borderRadius(14)
Column({ space: 3 }) {
Text('出航确认单').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('舰船:' + this.selShip!.name).fontSize(12).fontColor(COLORS.primary)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showLaunch = false })
}
.width('100%')
launchModal 弹窗与前述的 shipModal 在视觉风格上有一个关键区别——它使用底部抽屉样式而非居中弹窗。这体现在外层 Column 的 justifyContent 设置上:launchModal 使用 .justifyContent(FlexAlign.End) 使弹窗内容贴底显示,模拟从底部滑出的抽屉效果;而 shipModal 使用 FlexAlign.Center 使弹窗居中显示。
内层卡片容器的样式也做了差异化处理:.backgroundColor('#111840') 设置背景色,.borderRadius({ topLeft: 20, topRight: 20 }) 只设置顶部圆角(底部不设圆角,因为抽屉贴底显示,底部圆角无意义),.translate({ y: 18 }) 使用 Y 轴位移使卡片略微下沉,营造从底部弹出的动画暗示感(虽然本案例没有显式动画,但 translate 的视觉偏移暗示了滑入方向)。
出航确认弹窗的信息结构设计得非常合理:头部显示火箭图标和舰船名称、中间是三个等宽信息格(出航时间、目标航线、预计航时)、之后是航线安全评级提示条、最后是确认出航按钮。这种"信息展示 → 风险提示 → 操作确认"的从上到下信息流,符合用户阅读习惯和决策流程。
5.6 航线详情弹窗(航点时间轴)
@Builder
routeModal() {
Column() {
Column({ space: 10 }) {
Row() {
Column({ space: 3 }) {
Text('🛰 航线详情').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text(this.selRoute!.from + ' ⇄ ' + this.selRoute!.to).fontSize(12).fontColor(COLORS.primary)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showRoute = false })
}
.width('100%')
Row() {
Column({ space: 2 }) {
Text(this.selRoute!.from).fontSize(13).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold)
Text('起点').fontSize(10).fontColor(COLORS.textHint)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('────▶').fontSize(16).fontColor(COLORS.gold)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column({ space: 2 }) {
Text(this.selRoute!.to).fontSize(13).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold)
Text('终点').fontSize(10).fontColor(COLORS.textHint)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding(12)
.backgroundColor('rgba(124,77,255,0.12)')
.borderRadius(12)
航线详情弹窗中最具特色的是"航点流程"区域——起点、箭头、终点三栏横向排列,使用 ────▶ 文字符号模拟流程图中的连接箭头。三个 Column 各自使用 .layoutWeight(1) 等宽分布,配合 .alignItems(HorizontalAlign.Center) 使内容居中对齐。紫色半透明背景 rgba(124,77,255,0.12) 将整个流程区域包裹为一个视觉单元。
这种使用文字符号模拟图形元素的做法,是轻量化 UI 设计中的常见技巧。在不需要引入额外图形资源的前提下,通过 Text 组件渲染 Unicode 字符(如箭头 ────▶、勾选标记 ✓、星号 ⭐ 等),可以快速实现流程指示、状态标记等视觉元素。虽然精度和美观度不如专业图标,但在快速开发和原型验证阶段具有极高的效率优势。
航线详情弹窗的下半部分是"航程详情"列表区——五个等高(36 像素)的 Row,每行包含一个 emoji 图标和一行信息文字(航程距离、预计航时、单次运费、风险等级、在航舰船数)。这种固定的行高设计使列表视觉节奏整齐划一,信息呈现结构化。信息区域的背景使用 rgba(64,196,255,0.06) 极淡的蓝色蒙层,与外层卡片背景形成微妙的层次区分。
5.7 卸货确认弹窗(步进器)
@Builder
unloadModal() {
Column() {
Column({ space: 10 }) {
// ... 头部
Row({ space: 10 }) {
Text('−').fontSize(24).fontColor(COLORS.white).width(40).height(40).textAlign(TextAlign.Center).backgroundColor(COLORS.secondary).borderRadius(20)
Text('2').fontSize(20).fontColor(COLORS.gold).fontWeight(FontWeight.Bold).width(48).textAlign(TextAlign.Center)
Text('+').fontSize(24).fontColor(COLORS.white).width(40).height(40).textAlign(TextAlign.Center).backgroundColor(COLORS.secondary).borderRadius(20)
Text('箱').fontSize(13).fontColor(COLORS.textSecondary)
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding(12)
.backgroundColor('rgba(255,213,79,0.08)')
.borderRadius(12)
卸货确认弹窗的核心交互组件是"步进器"(stepper)——减号按钮、数量显示、加号按钮三件套。减号和加号按钮使用固定 40x40 尺寸的 Text 组件,背景色为 COLORS.secondary(紫色),borderRadius(20) 使其呈圆形按钮。中间的数量显示固定宽度 48 像素,居中对齐。
整个步进器行使用 .justifyContent(FlexAlign.Center) 使所有子元素在 Row 中居中排列。这种设计确保步进器始终位于弹窗中央,视觉重心稳定。金色淡背景 rgba(255,213,79,0.08) 将步进器区域包裹为一个视觉单元,与弹窗的其他区域形成色彩区分。
弹窗的底部使用了金色背景的确认按钮——.backgroundColor(COLORS.gold) 和 .fontColor(COLORS.deepBg),金色背景配深色文字,呈现出醒目的高对比度操作按钮。这与舰船详情弹窗中的蓝色确认按钮形成视觉差异,通过色彩区分不同业务场景的操作行为。此外,弹窗容器使用了 .border({ width: 2, color: COLORS.gold, style: BorderStyle.Dashed }) 设置金色虚线边框,进一步强化了卸货场景的视觉标识。
5.8 舱段升级弹窗(进度条)
Column({ space: 6 }) {
Row() {
Text('升级进度').fontSize(12).fontColor(COLORS.textSecondary)
Text(this.selModule!.progress + '%').fontSize(12).fontColor(COLORS.primary).fontWeight(FontWeight.Bold)
}
.width('100%')
Row() {
Row() {
}
.width(this.selModule!.progress + '%')
.height(10)
.backgroundColor(COLORS.primary)
.borderRadius(5)
}
.width('100%')
.height(10)
.backgroundColor('rgba(64,196,255,0.16)')
.borderRadius(5)
Text('材料已就位,可立即升级至 Lv.' + (this.selModule!.level + 1)).fontSize(11).fontColor(COLORS.textHint)
}
.width('100%')
.padding(12)
.backgroundColor('rgba(64,196,255,0.06)')
.borderRadius(12)
舱段升级弹窗中最具技术含量的部分是进度条的实现。进度条由两层 Row 嵌套组成:外层 Row 作为背景轨道,宽度 100%,高度 10 像素,背景色为淡蓝色 rgba(64,196,255,0.16);内层 Row 作为进度填充,宽度通过 this.selModule!.progress + '%' 动态计算(如进度为 82% 时宽度为 '82%'),背景色为 COLORS.primary(亮蓝色)。两层都设置了 borderRadius(5) 使进度条两端圆润。
这种进度条的实现方式是 ArkTS 声明式 UI 的经典范例——进度值是数据层的属性(selModule.progress),UI 层直接通过字符串拼接将进度值转换为百分比宽度。当进度数据发生变化时,只需更新数据源,进度条的宽度会自动更新,无需任何命令式的 DOM 操作。这种"数据 → 样式属性"的直接映射,是声明式编程范式的精髓。
进度条下方是一行提示文字 '材料已就位,可立即升级至 Lv.' + (this.selModule!.level + 1)。这里使用了算术表达式 this.selModule!.level + 1 计算下一级等级,并将数值与字符串拼接。由于 JavaScript/ArkTS 中 + 运算符在字符串上下文中执行拼接操作,'Lv.' + 6 的结果是 'Lv.6'。这种字符串拼接在声明式 UI 中极为常见,用于将动态数据嵌入静态文本模板。
5.9 排行榜弹窗(领奖台柱状图)
@Builder
rankModal() {
Column() {
Column({ space: 10 }) {
// ... 头部
Row({ space: 6 }) {
ForEach(getRankTop(), (r: SpaceRank) => {
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.orange)
.borderRadius({ topLeft: 6, topRight: 6 })
}
.layoutWeight(1)
.padding({ top: 10, bottom: 0 })
.backgroundColor('rgba(64,196,255,0.06)')
.borderRadius(10)
})
}
.width('100%')
.alignItems(VerticalAlign.Bottom)
排行榜弹窗的领奖台区域是整个应用中最精巧的 UI 设计之一。它使用 ForEach 遍历 getRankTop() 返回的前三名数据,为每个人渲染一个 Column 容器,包含图标、姓名、积分和一个空 Column 作为"领奖台柱子"。
柱子的高度通过嵌套三元运算符动态计算:r.id === 1 ? 46 : r.id === 2 ? 32 : 22——第一名 46 像素、第二名 32 像素、第三名 22 像素,模拟了现实中领奖台"中间最高、两侧递减"的视觉规则。柱子的背景色同样基于 id 动态选择:第一名金色、第二名紫色、第三名橙色,对应金牌、银牌、铜牌的色彩语义。borderRadius({ topLeft: 6, topRight: 6 }) 只设置顶部圆角,使柱子看起来像是从地面升起的柱状体。
外层 Row 使用 .alignItems(VerticalAlign.Bottom) 使三个柱子底部对齐——这是领奖台视觉的关键,所有柱子都从同一基线向上延伸。ForEach 的使用使这个领奖台可以动态适配任意数量的前三名数据,如果数据源中只有两条记录,则只渲染两个柱子,不会出现空位。
5.10 技能柱状图弹窗
@Builder
skillModal() {
Column() {
Column({ space: 10 }) {
// ... 头部
Row({ space: 6 }) {
ForEach(getUnlockedSkills(), (s: SpaceSkill) => {
Column({ space: 4 }) {
Text(s.exp + '').fontSize(9).fontColor(COLORS.gold)
Column() {
}
.width('100%')
.height((s.exp / 10) + '%')
.borderRadius(4)
Text(s.icon).fontSize(16)
Text(s.name.slice(0, 2)).fontSize(9).fontColor(COLORS.textSecondary)
}
.layoutWeight(1)
.justifyContent(FlexAlign.End)
})
}
.width('100%')
.height(150)
.padding(10)
.backgroundColor('rgba(64,196,255,0.06)')
.borderRadius(12)
.alignItems(VerticalAlign.Bottom)
技能柱状图弹窗使用 ForEach 遍历 getUnlockedSkills() 返回的已解锁技能(level >= 3),为每个技能渲染一个柱子。每个柱子从上到下包含:经验值数字、柱状条(空 Column)、技能图标、技能名称前两字。柱状条的高度通过 (s.exp / 10) + '%' 动态计算——经验值 800 对应 80% 高度,经验值 240 对应 24% 高度。
外层 Row 容器设置了 .height(150) 固定高度和 .alignItems(VerticalAlign.Bottom) 底部对齐,使所有柱子从容器底部向上延伸,形成标准柱状图的视觉效果。每个柱子内部的 Column 使用 .justifyContent(FlexAlign.End) 使内容从底部开始排列——柱状条在最下方,经验值数字在柱状条上方,图标和名称在柱状条下方。这种排列方式使得柱子高度变化时,视觉上表现为柱状条从底部向上"生长"。
Text(s.name.slice(0, 2)) 使用了 JavaScript 的字符串 slice 方法截取技能名称的前两个字符——如"超空间导航"截取为"超空"。这种文字截断处理是因为柱状图空间有限,完整显示技能名称会溢出或换行。通过截取前两字作为简称,在有限空间内提供了足够的识别信息。
5.11 星球探索弹窗(渐变头部)
@Builder
planetModal() {
Column() {
Column({ space: 0 }) {
Column({ space: 4 }) {
Text(this.selPlanet!.icon).fontSize(44)
Text(this.selPlanet!.name).fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
Text(this.selPlanet!.type).fontSize(12).fontColor(COLORS.gold)
}
.width('100%')
.padding({ top: 26, bottom: 26 })
.linearGradient({ angle: 180, colors: [['#7C4DFF', 0.0], ['#131A45', 1.0]] })
.borderRadius({ topLeft: 16, topRight: 16 })
星球探索弹窗采用了一种独特的"渐变头部 + 信息体"双层结构。头部 Column 使用 .linearGradient({ angle: 180, colors: [['#7C4DFF', 0.0], ['#131A45', 1.0]] }) 设置从上到下的垂直渐变(angle 180 表示从上到下),起点为紫色 #7C4DFF,终点为靛蓝 #131A45。这种渐变方向使头部从顶部的亮紫色过渡到底部的深靛蓝,营造出星球大气层的视觉效果。
头部区域使用 padding({ top: 26, bottom: 26 }) 设置较大的上下内边距,给星球图标和文字充足的呼吸空间。.borderRadius({ topLeft: 16, topRight: 16 }) 只设置顶部圆角,配合下方信息体的直角边缘,形成"头部圆角、体部直角"的视觉结构。这种设计在卡片式 UI 中非常常见——顶部视觉区域有圆角以增强美感,底部信息区域无圆角以保持信息排列的整洁感。
5.12 贵宾会员金卡弹窗
@Builder
vipModal() {
Column() {
Column({ space: 0 }) {
Column({ space: 6 }) {
Text('👑').fontSize(40)
Text('星港贵宾舱').fontSize(19).fontWeight(FontWeight.Bold).fontColor(COLORS.deepBg)
Text(this.selVip!.name + ' · 星钻会员').fontSize(12).fontColor('#5D4A00')
Text('享专属泊位与优先出航权').fontSize(11).fontColor('#5D4A00')
}
.width('100%')
.padding({ top: 22, bottom: 22 })
.linearGradient({ angle: 135, colors: [['#FFE082', 0.0], ['#FFD54F', 0.55], ['#FFB300', 1.0]] })
.borderRadius({ topLeft: 16, topRight: 16 })
贵宾会员弹窗的头部使用金色渐变 linearGradient({ angle: 135, colors: [['#FFE082', 0.0], ['#FFD54F', 0.55], ['#FFB300', 1.0]] }),三个色标从浅金 #FFE082 过渡到标准金 #FFD54F 再到深金 #FFB300,沿 135 度对角线方向呈现金属质感的光泽效果。头部文字使用深色(COLORS.deepBg 和 #5D4A00),与金色背景形成高对比度,模拟实体金卡的印刷效果。
弹窗的权益列表区域使用四个等结构的 Row,每个 Row 包含权益描述(左对齐,layoutWeight(1))和权益状态文字(右对齐)。状态文字根据权益类型使用不同颜色——"已启用"用绿色(COLORS.success),"每月 3 次"用金色(COLORS.gold),"已到账"用红色(COLORS.danger)。这种基于权益状态动态切换文字颜色的方式,使用户能够通过色彩快速识别权益的当前状态。
5.13 通用区块标题构建器
@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 是一个通用的区块标题构建器,接收标题文字和"更多"文字两个参数。它使用 Row 横向排列标题(左对齐,layoutWeight(1))和更多链接(右对齐,金色)。.margin({ top: 10, bottom: 8 }) 设置上下外边距,使区块标题与上下内容保持适当的间距。
这个构建器虽然简单,但它体现了一个重要的工程原则——通用 UI 片段的抽取和复用。在内容密集的列表页面中,多个区块都需要"标题 + 更多"的头部结构,如果每个地方都重复编写相同的 Row 和 Text 代码,不仅代码冗余,而且后续调整样式时需要逐个修改。通过抽取为 @Builder,只需定义一次即可在多处复用,修改时也只需改一处。
六、标签页组件:业务内容渲染
6.1 DockTab 船坞页:组件声明与回调
@Component
struct DockTab {
onShip: (s: SpaceShip) => void = () => {}
onLaunch: (s: SpaceShip) => void = () => {}
onCrew: (c: SpaceCrew) => void = () => {}
onRank: (r: SpaceRank) => void = () => {}
onNotice: (n: SpaceNotice) => void = () => {}
onPlanet: (p: SpacePlanet) => void = () => {}
DockTab 是船坞标签页组件,使用 @Component 装饰器声明。它定义了六个回调函数类型的成员变量,每个回调的默认值是一个空函数 () => {}。这种设计模式使得组件的回调参数是可选的——如果父组件没有传入某个回调,调用时不会报错,只是执行空函数(无效果)。这是一种防御性的默认值策略。
每个回调的类型签名都非常精确:onShip 接收一个 SpaceShip 类型的参数返回 void,onCrew 接收一个 SpaceCrew 类型的参数返回 void,以此类推。这种精确的类型约束确保了在组件内部触发回调时传入的数据类型正确——如果开发者意外传入了一个 SpaceRoute 对象给 onShip 回调,编译器会立即报错。类型安全在组件间通信中尤为重要,它可以防止数据类型不匹配导致的运行时错误。
在 ArkTS 的组件通信模型中,子组件通过回调函数向父组件"上报"事件,而父组件通过回调函数的实现决定如何响应事件。这种模式被称为"提升状态"(state hoisting)——子组件不维护自己的状态,而是将状态变化通知给父组件,由父组件统一管理。这保证了状态的唯一数据源,避免了多组件状态不同步的问题。
6.2 DockTab 的 build 方法:滚动容器
build() {
Scroll() {
Column({ space: 10 }) {
// 星云渐变横幅
Column({ space: 6 }) {
Text('🚀 SPACE DOCK').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
Text('本港在册舰船 ' + getShipCount() + ' 艘 · 船员 ' + getCrewCount() + ' 名').fontSize(12).fontColor('rgba(232,234,246,0.85)')
Row({ space: 8 }) {
Text('🛠 船坞运营中').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(24,255,255,0.20)').borderRadius(10)
Text('⛽ 燃料储备充足').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(255,213,79,0.20)').borderRadius(10)
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding(16)
.linearGradient({ angle: 135, colors: [['#131A45', 0.0], ['#7C4DFF', 1.0]] })
.borderRadius(16)
DockTab 的 build() 方法使用 Scroll 组件作为最外层容器。Scroll 是 ArkUI 中的可滚动容器组件,当其内容超出可视区域时,用户可以通过触摸滑动浏览全部内容。.scrollable(ScrollDirection.Vertical) 设置为垂直滚动方向,.width('100%').height('100%') 使其铺满父容器。
Scroll 内部的 Column({ space: 10 }) 是实际的内容容器,所有业务区块作为 Column 的子元素依次排列。第一个区块是"星云渐变横幅"——一个使用 linearGradient 设置 135 度对角渐变背景的 Column,从靛蓝过渡到紫罗兰。横幅内包含标题文字、统计信息文字和两个状态标签(“船坞运营中"和"燃料储备充足”),每个标签都使用 padding 设置内边距和 borderRadius 设置圆角,形成胶囊形标签样式。
统计信息文字 '本港在册舰船 ' + getShipCount() + ' 艘 · 船员 ' + getCrewCount() + ' 名' 展示了数据函数在 UI 中的使用方式——通过字符串拼接将函数返回值嵌入文本。getShipCount() 返回舰船数量,getCrewCount() 返回船员数量,这些函数调用在 build() 每次执行时都会重新求值,确保显示的数据始终是最新的。
6.3 DockTab:快捷宫格与王牌舰船横滑
// 快捷宫格
Row({ space: 8 }) {
Column({ space: 8 }) {
ForEach(getQuickLeft(), (q: SpaceQuick) => {
this.quickCard(q)
})
}
.layoutWeight(1)
Column({ space: 8 }) {
ForEach(getQuickRight(), (q: SpaceQuick) => {
this.quickCard(q)
})
}
.layoutWeight(1)
}
.width('100%')
// 王牌舰船横滑
Scroll() {
Row({ space: 10 }) {
ForEach(getTopShips(), (s: SpaceShip) => {
this.shipWideCard(s)
})
}
.padding({ right: 4 })
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
.constraintSize({ maxHeight: 150 })
快捷宫格区域使用双列布局,通过 getQuickLeft() 和 getQuickRight() 两个函数将快捷入口数据拆分为左右两列。每列内部使用 ForEach 遍历数据数组,对每个元素调用 this.quickCard(q) 构建器渲染卡片。ForEach 是 ArkTS 中实现列表渲染的核心组件——它接收一个数组和一个渲染函数,为数组中的每个元素调用渲染函数生成对应的 UI 结构。
ForEach 的工作原理与 React 的 map 渲染或 Vue 的 v-for 类似,但 ArkTS 的 ForEach 在底层会进行 diff 优化——当数组数据发生变化时(如插入、删除、重排元素),ForEach 会通过 key 机制最小化 DOM 操作,只更新真正变化的元素,而非全量重建。这种优化对于长列表的性能至关重要。
王牌舰船横滑区域使用了一个嵌套的 Scroll 组件,方向设置为 ScrollDirection.Horizontal(水平滚动)。内部是一个 Row 容器,通过 ForEach 渲染评分大于等于 9.5 的高分舰船卡片。.constraintSize({ maxHeight: 150 }) 设置最大高度约束,限制横滑区域不会因为卡片内容过多而撑高整个页面。这种"水平滑动卡片列表"是移动端内容展示的经典模式,常用于展示精选内容、推荐商品等。
6.4 DockTab:双列舰船列表与船员横滑
// 全部舰船双列
Row({ space: 10 }) {
Column({ space: 10 }) {
ForEach(getShipLeft(), (s: SpaceShip) => {
this.shipCard(s)
})
}
.layoutWeight(1)
Column({ space: 10 }) {
ForEach(getShipRight(), (s: SpaceShip) => {
this.shipCard(s)
})
}
.layoutWeight(1)
}
.width('100%')
// 船员横滑
Scroll() {
Row({ space: 10 }) {
ForEach(CREW, (c: SpaceCrew) => {
this.crewCard(c)
})
}
.padding({ right: 4 })
}
.scrollable(ScrollDirection.Horizontal)
.width('100%')
.constraintSize({ maxHeight: 130 })
全部舰船列表使用与快捷宫格相同的双列布局模式——getShipLeft() 和 getShipRight() 返回的数据分别注入左右两列的 ForEach。这种双列拆分配合 ForEach 的模式在本案例中被反复使用,几乎贯穿了所有标签页的内容列表区域。它的优点是通过函数封装将"数据拆分"逻辑与"UI 渲染"逻辑分离,使代码结构清晰,且双列布局在移动端窄屏上的信息展示效率远高于单列。
船员横滑区域直接使用 CREW 常量数组作为 ForEach 的数据源(而非经过过滤函数处理),因为船员总数只有六人,全部展示在横滑列表中无需筛选。.constraintSize({ maxHeight: 130 }) 限制高度,使横滑区域的卡片不会过高。这里的 constraintSize 与之前的 height 不同——constraintSize 设置的是约束条件(包括最大/最小宽高),而 height 设置的是确定值。使用约束条件的好处是它不会强制覆盖子元素的自然高度,只在子元素超过最大值时才生效。
6.5 DockTab 的构建器:快捷卡片与舰船卡片
@Builder
quickCard(q: SpaceQuick) {
Row({ space: 8 }) {
Text(q.icon).fontSize(20).width(38).height(38).textAlign(TextAlign.Center).backgroundColor(q.color + '26').borderRadius(10)
Text(q.name).fontSize(12).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold)
}
.width('100%')
.padding(10)
.backgroundColor('rgba(255,255,255,0.05)')
.borderRadius(12)
}
quickCard 构建器渲染快捷入口卡片。最值得关注的是图标背景色的计算方式:q.color + '26'。这里 q.color 是一个十六进制颜色值字符串(如 '#40C4FF'),拼接 '26' 后变为 '#40C4FF26'——这是一个 8 位十六进制颜色值,最后两位 26 是 alpha 通道值(十进制 38,约 15% 不透明度)。通过这种字符串拼接,每个快捷入口的图标背景色自动与其主题色匹配,无需在数据源中额外定义背景色。
这种"主题色 + 透明度后缀"的颜色计算技巧在 ArkTS 开发中非常实用。它允许开发者只用一个色值就派生出背景色、边框色、蒙层色等多种变体——color + '26'(15% 透明度)适合做背景,color + '55'(33% 透明度)适合做边框,color + '00'(全透明)可以用于隐藏元素。这种模式使得色彩管理极其简洁高效。
@Builder
shipCard(s: SpaceShip) {
Column({ space: 8 }) {
Row() {
Text(s.icon).fontSize(26)
Text(s.name).fontSize(13).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold).layoutWeight(1).margin({ left: 8 })
Text('⭐' + s.rating).fontSize(11).fontColor(COLORS.gold)
}
.width('100%')
Text(s.cls).fontSize(10).fontColor(COLORS.primary).width('100%').textAlign(TextAlign.Start)
Row() {
Text(s.status).fontSize(10).fontColor(s.status === '待命' ? COLORS.success : COLORS.orange)
Text('出航 ›').fontSize(11).fontColor(COLORS.gold)
.onClick(() => {
this.onLaunch(s)
})
}
.width('100%')
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.onClick(() => {
this.onShip(s)
})
}
shipCard 构建器渲染标准舰船卡片。卡片结构为三层:顶行(图标、名称、评分)、中间分类文字、底行(状态标签和出航链接)。状态标签的颜色使用三元运算符动态切换——"待命"状态用绿色(COLORS.success),其他状态用橙色(COLORS.orange)。这种基于数据值的条件着色是声明式 UI 的基础能力。
卡片整体设置了 .onClick(() => { this.onShip(s) })——点击卡片任意位置触发 onShip 回调,将舰船数据传递给父组件。同时,底行的"出航 ›"链接单独设置了 .onClick(() => { this.onLaunch(s) })——点击出航链接触发 onLaunch 回调。这里存在事件冒泡的考量:点击"出航 ›"时,理论上会同时触发卡片整体的 onClick 和链接自身的 onClick。在实际使用中,ArkUI 框架会先执行子元素的事件处理器,再执行父元素的事件处理器。这意味着点击"出航 ›"会先触发 onLaunch(打开出航弹窗),再触发 onShip(打开舰船详情弹窗),可能导致两个弹窗同时打开。
在 ArkUI 的事件系统中,触摸事件遵循冒泡模型——事件从最内层的目标元素开始触发,然后向外层元素冒泡。开发者可以通过
stopPropagation()方法阻止事件冒泡,但本案例中没有使用这种方法。在实际开发中,如果父子元素的 onClick 存在冲突,通常需要重新设计交互逻辑,例如将子元素的事件处理器中添加event.stopPropagation()调用,或者将子元素移出父元素的点击区域。
6.6 RouteTab 航线页:星球横滑与航线卡片
@Component
struct RouteTab {
onRoute: (r: SpaceRoute) => void = () => {}
onModule: (m: SpaceModule) => void = () => {}
onPlanet: (p: SpacePlanet) => void = () => {}
build() {
Scroll() {
Column({ space: 10 }) {
// 星际蓝横幅
Column({ space: 6 }) {
Text('🛰 星域航线网').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
Text('已开通 ' + getRouteCount() + ' 条航线 · 覆盖 6 大星球').fontSize(12).fontColor('rgba(232,234,246,0.85)')
Row({ space: 8 }) {
Text('🧭 航线规划中').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(64,196,255,0.25)').borderRadius(10)
Text('🪐 探索进行时').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(124,77,255,0.30)').borderRadius(10)
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding(16)
.linearGradient({ angle: 135, colors: [['#0A0E2A', 0.0], ['#40C4FF', 1.0]] })
.borderRadius(16)
RouteTab 航线页的横幅使用蓝色渐变(从深藏蓝到亮蓝),与船坞页的紫色渐变形成色彩区分。这种"每个标签页横幅使用不同色调"的设计策略,使用户在切换标签页时能通过色彩变化感知内容切换,增强了导航体验的反馈感。
航线页的统计信息 '已开通 ' + getRouteCount() + ' 条航线 · 覆盖 6 大星球' 使用了 getRouteCount() 函数获取航线数量。注意"覆盖 6 大星球"这个数字是硬编码的,而非通过 PLANETS.length 动态计算——这是一种轻微的硬编码行为,如果星球数据发生变化,这里的数字需要手动更新。在实际工程中,应避免这种硬编码,改用 PLANETS.length 动态获取。
6.7 RouteTab 的航线卡片与舱段卡片
@Builder
routeCard(r: SpaceRoute) {
Column({ space: 8 }) {
Row() {
Text(r.icon).fontSize(22)
Text(r.from).fontSize(12).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold).layoutWeight(1).margin({ left: 6 })
Text('▶').fontSize(12).fontColor(COLORS.gold)
}
.width('100%')
Text(r.to).fontSize(12).fontColor(COLORS.primary).width('100%').textAlign(TextAlign.Start)
Row() {
Text(r.distance + ' 光年').fontSize(10).fontColor(COLORS.textSecondary)
Text(r.risk).fontSize(10).fontColor(r.risk === '高' || r.risk === '极高' ? COLORS.danger : COLORS.success)
}
.width('100%')
Text('💵 运费 ' + r.fee + ' 币').fontSize(11).fontColor(COLORS.gold).width('100%').textAlign(TextAlign.Start)
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.onClick(() => {
this.onRoute(r)
})
}
航线卡片的结构设计紧凑而信息丰富。第一行展示航线图标、起点名称和一个箭头符号 ▶;第二行展示终点名称;第三行横向排列距离和风险等级;第四行展示运费。风险等级的颜色使用复合条件表达式 r.risk === '高' || r.risk === '极高' ? COLORS.danger : COLORS.success——高风险或极高风险显示红色,低中风险显示绿色。这种条件着色使风险信息一目了然,用户无需仔细阅读文字即可通过颜色感知风险级别。
舱段卡片 moduleCard 的结构类似,展示舱段图标、名称、分类+等级和升级效果。与航线卡片不同,舱段卡片的背景使用半透明的青色 rgba(24,255,255,0.08) 而非纯色背景 COLORS.cardBg,这种视觉差异帮助用户区分不同类型的内容卡片。
6.8 CargoTab 货运页:金色横幅与热货横滑
@Component
struct CargoTab {
onCargo: (c: SpaceCargo) => void = () => {}
onUnload: (c: SpaceCargo) => void = () => {}
onOrder: (o: SpaceOrder) => void = () => {}
build() {
Scroll() {
Column({ space: 10 }) {
// 金色横幅
Column({ space: 6 }) {
Text('📦 星际货运中心').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#3B2B00')
Text('在途货物 ' + getCargoCount() + ' 类 · 月吞吐 ' + (getCargoCount() * 42) + ' 吨').fontSize(12).fontColor('#4A3700')
Row({ space: 8 }) {
Text('⚡ 特急单处理中').fontSize(11).fontColor('#3B2B00').padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(255,255,255,0.55)').borderRadius(10)
Text('🏭 港口运作正常').fontSize(11).fontColor('#3B2B00').padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(255,255,255,0.55)').borderRadius(10)
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding(16)
.linearGradient({ angle: 135, colors: [['#FFE082', 0.0], ['#FFD54F', 1.0]] })
.borderRadius(16)
货运页的横幅使用金色渐变,文字使用深棕色(#3B2B00 和 #4A3700),在金色背景上呈现高对比度的深色文字,模拟物流行业的专业配色。统计信息 '月吞吐 ' + (getCargoCount() * 42) + ' 吨' 使用了算术表达式 getCargoCount() * 42 计算月吞吐量——将货物种类数乘以 42 得到一个估算值。这种基于简单算术的数据衍生计算在 UI 展示中很常见,用于将基础数据转换为更具业务含义的展示指标。
热货横滑区域使用 getHotCargo() 函数获取紧急程度为"加急"或"特急"的货物,通过水平滑动的 Scroll 容器展示。热货卡片 hotCargoCard 的背景使用金色半透明 rgba(255,213,79,0.12),与横幅的金色主题保持一致,形成统一的色彩语言。紧急程度标签的颜色使用三元运算符 '特急' ? COLORS.danger : COLORS.gold——特急用红色,加急用金色,通过色彩差异区分紧急级别。
6.9 CargoTab 的货物卡片与运单卡片
@Builder
cargoCard(c: SpaceCargo) {
Column({ space: 6 }) {
Row() {
Text(c.icon).fontSize(22)
Text(c.name).fontSize(12).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold).layoutWeight(1).margin({ left: 6 })
Text(c.urgency).fontSize(9).fontColor(COLORS.white).padding({ left: 6, right: 6, top: 2, bottom: 2 }).backgroundColor(c.urgency === '特急' ? COLORS.danger : COLORS.orange).borderRadius(6)
}
.width('100%')
Text(c.type + ' | ' + c.weight + ' 吨').fontSize(10).fontColor(COLORS.textSecondary).width('100%').textAlign(TextAlign.Start)
Row() {
Text(c.origin + ' → ' + c.dest).fontSize(10).fontColor(COLORS.primary)
Text('卸货 ›').fontSize(11).fontColor(COLORS.gold)
.onClick(() => {
this.onUnload(c)
})
}
.width('100%')
}
.width('100%')
.padding(12)
.backgroundColor(COLORS.cardBg)
.borderRadius(14)
.onClick(() => {
this.onCargo(c)
})
}
货物卡片的结构层次分明:第一行是图标、名称和紧急程度标签(右侧带圆角背景的小标签);第二行是类型和重量信息;第三行是产地→目的地路线和"卸货"操作链接。紧急程度标签的背景色使用三元运算符动态切换——"特急"用红色背景,其他用橙色背景,且文字始终为白色,确保在任何背景色上都有足够的对比度。
运单卡片 orderCard 的结构更加简洁——顶行展示运单编号(金色)和状态文字(根据状态动态着色),底行展示货物名称。运单卡片没有"更多"操作链接,点击整个卡片触发 onOrder 回调打开运单详情弹窗。这种设计简化了运单卡片的交互——用户点击卡片即可查看完整运单信息,无需寻找特定的操作按钮。
6.10 EquipTab 装备页:泊位入口与技能卡片
@Component
struct EquipTab {
onModule: (m: SpaceModule) => void = () => {}
onDock: (s: SpaceShip) => void = () => {}
onSkill: (sk: SpaceSkill) => void = () => {}
build() {
Scroll() {
Column({ space: 10 }) {
// 紫色横幅
Column({ space: 6 }) {
Text('⚙ 星港装备库').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
Text('核心舱段 ' + getModuleCount() + ' 组 · 技能 ' + getSkillCount() + ' 项').fontSize(12).fontColor('rgba(232,234,246,0.85)')
Row({ space: 8 }) {
Text('🛡 护盾在线').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(24,255,255,0.22)').borderRadius(10)
Text('🧠 技能研修中').fontSize(11).fontColor(COLORS.white).padding({ left: 10, right: 10, top: 4, bottom: 4 }).backgroundColor('rgba(179,136,255,0.30)').borderRadius(10)
}
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding(16)
.linearGradient({ angle: 135, colors: [['#131A45', 0.0], ['#B388FF', 1.0]] })
.borderRadius(16)
// 泊位入口条
Row() {
Text('🅿').fontSize(22)
Column({ space: 2 }) {
Text('泊位登记').fontSize(14).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold)
Text('为舰船预约停靠泊位').fontSize(10).fontColor(COLORS.textSecondary)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text('去登记 ›').fontSize(12).fontColor(COLORS.success)
}
.width('100%')
.padding(14)
.backgroundColor('rgba(105,240,174,0.10)')
.border({ width: 1, color: COLORS.success })
.borderRadius(14)
.onClick(() => {
this.onDock(SHIPS[0])
})
装备页的横幅使用紫色渐变(从靛蓝到浅紫 #B388FF),与装备主题的科技神秘感相匹配。泊位入口条使用绿色半透明背景 rgba(105,240,174,0.10) 和绿色边框 COLORS.success,在深色页面中呈现出醒目的绿色入口,引导用户进行泊位登记操作。点击入口条时,直接传入 SHIPS[0](第一艘舰船)作为泊位登记的目标舰船,触发 onDock 回调。
装备页的核心内容是舱段双列和技能双列,都使用了与之前相同的"双列 ForEach"模式。舱段卡片 moduleCard 和航线页中的实现略有不同——装备页的舱段卡片背景使用 COLORS.cardBg(纯色),而航线页中的使用 rgba(24,255,255,0.08)(半透明青色)。这种同组件在不同标签页中的样式差异,体现了"根据上下文调整视觉"的设计灵活性。
技能卡片 skillCard 展示技能图标、名称、等级和描述。.maxLines(2) 限制描述文字最多两行,超出部分会被截断。maxLines 是控制文本溢出的重要属性——在卡片空间有限的情况下,通过限制行数可以防止长文本撑破卡片布局。配合 .textOverflow 属性(本案例未使用,但可以设置为 TextOverflow.Ellipsis 在截断处显示省略号),可以实现更优雅的文本溢出处理。
6.11 MineTab 我的页:档案卡与 VIP 入口
@Component
struct MineTab {
onTask: (t: SpaceTask) => void = () => {}
onSkill: (sk: SpaceSkill) => void = () => {}
onOrder: (o: SpaceOrder) => void = () => {}
onRank: (r: SpaceRank) => void = () => {}
onNotice: (n: SpaceNotice) => void = () => {}
onVip: (c: SpaceCrew) => void = () => {}
onSupply: (c: SpaceCargo) => void = () => {}
build() {
Scroll() {
Column({ space: 10 }) {
// 档案卡
Column({ space: 10 }) {
Row() {
Text('👤').fontSize(34).width(60).height(60).textAlign(TextAlign.Center).backgroundColor('rgba(24,255,255,0.16)').borderRadius(30)
Column({ space: 3 }) {
Text('CX-2077').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
Text('星际物流港 · 高级调度员').fontSize(11).fontColor(COLORS.textSecondary)
Text('⭐ 出航功勋 12800').fontSize(11).fontColor(COLORS.gold)
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
}
.width('100%')
MineTab 我的页面是所有标签页中内容最丰富的——它集成了档案信息、VIP 入口、今日值班任务、已完成任务、技能列表、运单列表、消息通知和补给申领入口。这个页面的 build() 方法调用了七个回调函数(onTask、onSkill、onOrder、onRank、onNotice、onVip、onSupply),是所有标签页中回调数量最多的。
档案卡区域使用 borderRadius(30) 配合 60x60 的固定尺寸创建圆形头像容器。这里的头像使用 emoji 👤 而非真实头像图片,这在原型开发阶段是常见做法。用户信息展示为三行文字:用户 ID(“CX-2077”)、职位描述、功勋值。功勋值前面的星号 emoji ⭐ 是一种视觉装饰,使数值更具游戏化感觉。
档案卡下方的统计信息使用了三个等宽的 Column,分别展示在册舰船数、协作船员数和本月任务数。每个统计格使用不同颜色的数值文字——蓝色、紫色、金色,配合对应色系的半透明背景,形成色彩编码的信息展示。这种"三色统计格"模式在移动端个人中心页面中非常常见,它以极小的空间展示了用户最关心的三个核心数据。
.padding(10)
.backgroundColor('rgba(64,196,255,0.05)')
.borderRadius(12)
.onClick(() => {
this.onOrder(o)
})
}
}
---

在弹窗系统层面,十六个弹窗全部通过 `@Builder` 构建器定义,由 `@State` 布尔变量控制显示隐藏。每个弹窗的渲染条件包含双重检查(show 状态和选中数据非空),确保了安全性。弹窗样式分为居中弹窗、底部抽屉、渐变头部三种模式,通过 `justifyContent` 和 `linearGradient` 的差异化配置实现视觉区分。
在布局编排层面,Column 和 Row 的组合使用构建了从页面骨架到卡片内部的所有布局结构。`layoutWeight` 实现了弹性空间分配,`ForEach` 实现了动态列表渲染,`linearGradient` 实现了渐变背景效果。这些布局原语的灵活组合,使各种复杂的 UI 结构都能以简洁的声明式代码表达。
在样式系统层面,链式属性方法(fontSize、fontColor、borderRadius、padding、margin 等)使 UI 的视觉配置一目了然。条件样式的三元运算符写法(如 `status === '待命' ? COLORS.success : COLORS.orange`)使数据驱动的动态样式变得极为直观。rgba 透明色值和十六进制色值后缀拼接的技巧使色彩派生极为简洁。
ArkTS 的这套声明式 UI 体系,从类型安全到状态管理,从布局编排到组件通信,形成了一套完整且自洽的开发范式。它既保留了 TypeScript 的类型系统优势,又引入了响应式状态管理和声明式 UI 的开发效率,是鸿蒙生态中构建高质量应用的核心工具。本案例的工程实践——接口化数据建模、函数化数据加工、组件化 UI 架构、状态驱动交互、构建器复用 UI 片段——为鸿蒙应用开发提供了一套可参考的工程模板。
更多推荐


所有评论(0)