在万物互联的时代背景下,华为鸿蒙 HarmonyOS 以其分布式软总线、原子化服务和统一 IDE 为核心,构建了一套覆盖全场景的操作系统生态。ArkTS 作为鸿蒙应用开发的首选语言,在 TypeScript 的基础上进行了面向声明式 UI 的深度改造,引入了 @Component@Builder@State@Entry 等一系列编译时装饰器,将组件化、状态驱动和声明式构建范式融为一体。本文将以一个功能完备的"魔法学院"应用为分析对象,从类型系统、数据建模、全局函数、主入口组件、多标签页架构、弹窗体系、卡片构建器等维度,逐段剖析 ArkTS 在复杂业务场景中的落地实践,力求为读者呈现一份颗粒度极细的技术解剖报告。

鸿蒙开发的核心方法论可以概括为"声明式描述 + 响应式驱动"。在 ArkTS 中,开发者通过在 build() 方法中书写嵌套的组件调用链来描述界面结构,这种写法本质上是一棵组件树的声明。框架在运行时会将这棵声明树编译为高效的渲染指令,并通过虚拟 DOM diff 算法实现精确的局部更新。与传统的 Android XML 布局或 iOS Storyboard 相比,ArkTS 的声明式写法将界面逻辑与业务逻辑统一在同一语言环境中,消除了标记语言与编程语言之间的上下文切换成本。

ArkTS 的组件化思想深度借鉴了现代前端框架的最佳实践。每一个被 @Component 装饰的 struct 都是一个独立的 UI 单元,拥有自己的构建方法、状态变量和属性参数。组件之间通过构造参数进行数据下行传递,通过回调函数实现事件上行通知。这种"props down, events up"的单向数据流模式,保证了组件树的渲染可预测性,也使得状态变更的溯源路径清晰可追踪。在本项目中,主入口组件 Index 管理着六个标签页子组件和十六个弹窗构建器,每个子组件又内含若干卡片构建器,形成了一个层次分明的组件森林。

状态管理是 ArkTS 的灵魂能力。@State 装饰器将一个普通变量转化为响应式状态源,当该变量被重新赋值时,框架会自动收集所有在 build() 方法中读取过该状态的组件片段,并触发它们的重新渲染。这种基于依赖追踪的细粒度更新机制,避免了全局重绘的性能开销。在本项目中,currentTab 状态驱动着六个标签页的条件渲染切换,十余个 showXxx 布尔状态控制着十六个弹窗的显隐,而 selXxx 选中型变量则承载着弹窗内容的动态数据填充。这种状态设计模式在实际开发中极具代表性。

ArkTS 的装饰器系统并非运行时的反射机制,而是编译时的代码变换。编译器在处理 @State@Builder 等装饰器时,会自动注入状态追踪、依赖收集、渲染调度等底层逻辑,开发者只需在业务层面书写声明,框架层面的机制由编译器保驾护航。这种"编译时魔法"既保留了代码的简洁性,又确保了运行时的性能。

一、类型系统与色彩架构

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/TypeScript 中,interface 是一种纯类型层面的声明,不产生运行时对象,仅在编译阶段参与类型检查。ColorPalette 规定了十六个 string 类型字段,每个字段对应一种语义化的颜色角色。

从字段构成来看,这套配色体系采用了"语义命名"策略而非"外观命名"策略。也就是说,字段名表达的是颜色的用途而非颜色的外观。bg 代表页面背景色,cardBg 代表卡片背景色,deepBg 代表更深层级的背景色,三者构成了一个三级背景层次体系。primarysecondaryaccent 是三个层级的主题色,分别用于主要交互元素、次要交互元素和点缀交互元素。goldorangepurple 是三种装饰性强调色,用于特殊视觉场景。dangersuccess 是语义状态色,分别映射危险和成功两种操作结果。

textPrimarytextSecondarytextHint 构成了三级文字层次。这种分级设计是移动端 UI 设计的通用实践:主要文字使用高对比度颜色确保可读性,次要文字降低视觉权重以突出主体信息,提示文字进一步弱化以避免干扰。border 用于分割线与卡片边框,white 是纯白色常量。通过接口将所有颜色角色固化,开发者在书写 UI 代码时只需引用语义名而不必记忆十六进制值,极大地降低了出错概率。

1.2 色彩常量实例化

const COLORS: ColorPalette = {
  bg: '#1A1030',
  cardBg: '#241A4A',
  deepBg: '#120A24',
  primary: '#5B8CFF',
  secondary: '#9C6BFF',
  accent: '#7BE0FF',
  gold: '#FFD76A',
  danger: '#FF6B6B',
  success: '#4CD964',
  textPrimary: '#EDE7F6',
  textSecondary: '#B39DDB',
  textHint: '#7E6FA8',
  border: '#3D2E6E',
  white: '#FFFFFF',
  orange: '#FFA94D',
  purple: '#C77DFF'
};

在这里插入图片描述

COLORS 常量是 ColorPalette 接口的具体实现。const 关键字确保该标识符不可被重新赋值,防止了全局色彩配置被意外覆盖的风险。观察这些色值,#1A1030 是一种极深的紫黑色,作为页面背景营造出魔法世界的神秘氛围;#241A4A 是深紫色,用于卡片背景,与页面背景形成微妙但可辨的层次差异;#120A24 更深一层,用于底部导航栏等需要"沉底"视觉效果的容器。

主题色方面,#5B8CFF 是一种明亮的蓝色,#9C6BFF 是紫色,#7BE0FF 是青色,三者在色相环上形成蓝-紫-青的渐变关系,视觉上和谐统一。#FFD76A 金色用于奖励、评分等高价值信息的标注。文字层次从 #EDE7F6(接近白色的淡紫)到 #B39DDB(中等紫灰)再到 #7E6FA8(深紫灰),在深色背景上形成了清晰的三级对比。整体配色方案体现了"暗色魔法主题"的设计思路,所有色值都经过精心调配以确保在深色背景上具有足够的对比度和辨识度。

色彩管理是应用视觉一致性的基石。通过接口定义语义角色、通过常量集中管理色值,开发者可以在数百处 UI 代码中保持统一的色彩语言。这种模式还为后续的主题切换(如日间/夜间模式)预留了扩展空间——只需替换常量值即可实现全局换肤。

1.3 Mermaid:色彩体系架构图

ColorPalette 接口

COLORS 常量实例

背景层: bg / cardBg / deepBg

主题色: primary / secondary / accent

语义色: danger / success / gold

文字层: textPrimary / textSecondary / textHint

装饰色: orange / purple / white / border

页面容器 / 卡片容器 / 底部栏

按钮 / 标签 / 交互元素

状态提示 / 奖励标注 / 警告

三级文字层次渲染

边框 / 特殊强调 / 分割

在这里插入图片描述

二、数据模型与接口定义

2.1 课程数据模型

interface MagicCourse {
  id: number;
  name: string;
  icon: string;
  teacher: string;
  credit: number;
  students: number;
  rating: number;
  level: string;
  time: string;
  desc: string;
}

在这里插入图片描述

MagicCourse 接口定义了魔法课程的数据结构。十个字段涵盖了课程的标识、展示、评价和描述信息。id 是数字类型的唯一标识,用于列表渲染时的 key 管理和数据查找。icon 使用 emoji 字符串作为图标,这是一种轻量级的图标方案,无需引入图片资源即可实现视觉区分。credit(学分)、students(已选人数)、rating(评分)是三个数值型字段,分别用于课程的量化属性展示。level 用字符串表达课程难度等级,如"入门"“中级”“高阶”,这种设计比枚举更灵活但牺牲了类型安全。

2.2 咒语与魔药数据模型

interface MagicSpell {
  id: number;
  name: string;
  icon: string;
  power: number;
  cost: number;
  cooldown: string;
  type: string;
  chant: string;
}

interface MagicPotion {
  id: number;
  name: string;
  icon: string;
  effect: string;
  brewTime: string;
  price: number;
  progress: number;
  level: string;
}

MagicSpell 接口建模了咒语数据。power(威力)和 cost(魔力消耗)是两个核心数值属性,它们在施法确认弹窗中被用于计算总消耗。cooldown 以字符串形式存储冷却时间,而非数值加单位分开存储,这说明冷却时间仅用于展示而不参与计算。chant(咏唱文)是咒语的特色字段,存储一段吟唱文本,在弹窗中以斜体金色文字呈现,营造魔法仪式的沉浸感。

MagicPotion 接口建模了魔药数据。progress 字段表示熬制进度百分比,在魔药详情弹窗中被用于驱动进度条组件的宽度。brewTime 以字符串形式存储熬制时长。level 表示药剂等级(“学徒级”“专家级”“大师级”),在卡片展示时用于颜色区分——大师级显示为危险红色,其他显示为金色。这种基于数据值的条件着色在 ArkTS 中通过三元表达式实现,体现了声明式 UI 对动态渲染的良好支持。

2.3 任务、竞技与排行数据模型

interface MagicTask {
  id: number;
  title: string;
  icon: string;
  type: string;
  reward: number;
  status: string;
  time: string;
  tower: string;
}

interface MagicArena {
  id: number;
  name: string;
  icon: string;
  rule: string;
  reward: number;
  slots: number;
  joined: number;
  time: string;
}

interface MagicRank {
  id: number;
  name: string;
  icon: string;
  title: string;
  score: number;
  duels: number;
  badge: string;
}

在这里插入图片描述

MagicTask 接口中 status 字段使用字符串值"进行中"和"已完成"来区分任务状态,全局函数 getRunningTasksgetDoneTasks 基于这个字段进行过滤。tower 字段标识任务所在的塔层,将任务与游戏世界中的空间位置关联起来。MagicArena 接口包含 slots(总名额)和 joined(已报名人数),两者之差即为剩余名额,getOpenArenas 函数据此筛选仍有空位的竞技场。MagicRank 接口的 scoreduels 是两个数值属性,分别用于排名展示和竞技次数统计。

2.4 公告、委托、技能数据模型

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

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

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

在这里插入图片描述

MagicNotice 接口的 level 字段存储公告级别(“紧急”“活动”“升级"等),在 UI 渲染时通过条件判断决定标签颜色——“紧急"显示为危险红色,其他显示为蓝色。MagicOrder 接口使用 no 字段存储委托编号(如"MG-33021”),fromto 构成物流路线,status 的值包括"熬制中”“待熬制”“已送达”“已签收”,在展示时根据状态值动态着色。MagicSkillexp 字段是经验值数值,在技能图谱弹窗中被用于计算柱状图高度(exp / 10 + '%')。

2.5 快捷入口、魔宠与塔层数据模型

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

interface Familiar {
  id: number;
  name: string;
  icon: string;
  kind: string;
  level: number;
  exp: number;
  skill: string;
  loyalty: number;
}

interface MagicTower {
  id: number;
  name: string;
  icon: string;
  floor: number;
  guard: string;
  reward: string;
  difficulty: string;
}

在这里插入图片描述

MagicQuick 接口的设计特点是包含一个 color 字段,每个快捷入口项自带主题色,在卡片渲染时通过 q.color + '26'(追加 26 作为十六进制透明度后缀)生成带透明度的背景色。这种设计让每个快捷入口拥有独立的视觉标识。Familiar 接口建模了魔宠数据,loyalty(忠诚度)和 exp(经验值)是两个进度型数值,分别在弹窗中驱动忠诚度百分比和经验进度条的渲染。MagicTower 接口的 floor 字段是塔层数,在探索弹窗中被用于计算"本周探索次数"(floor * 2),展示了数据字段之间的派生计算关系。

2.6 Mermaid:数据模型关系图

课程Tab

咒语Tab

魔药Tab

任务Tab

竞技Tab

技能展示

排行榜

公告

委托

快捷入口

魔宠

塔层

全局色值

全局色值

全局色值

ColorPalette

COLORS 常量

MagicCourse

CourseTab

MagicSpell

SpellTab

MagicPotion

PotionTab

MagicTask

TaskTab

MagicArena

ArenaTab

MagicSkill

MagicRank

MagicNotice

MagicOrder

MagicQuick

Familiar

MagicTower

三、数据常量与静态数据源

3.1 课程数据集

const COURSES: MagicCourse[] = [
  { id: 1, name: '高阶元素魔法', icon: '🔥', teacher: '梅林教授', credit: 5, students: 42, rating: 9.7, level: '高阶', time: '周一 09:00', desc: '系统讲授火水风土四大元素的精微控制与复合施法。' },
  { id: 2, name: '星象占卜入门', icon: '🔮', teacher: '星语导师', credit: 3, students: 58, rating: 9.4, level: '入门', time: '周二 14:00', desc: '通过星轨与月相解读命运征兆,练习水晶球凝视。' },
  // ...
];

COURSES 常量是一个 MagicCourse[] 类型的数组,包含八条课程记录。每条记录严格按照 MagicCourse 接口的字段定义填充数据。在 ArkTS 中,这种静态数据数组通常作为应用的初始数据源或演示数据。数组的每个元素都是一个对象字面量,字段名与接口定义一一对应,TypeScript 编译器会在此处执行结构化类型检查,确保没有遗漏或多余的字段。

值得注意的是 icon 字段使用了 emoji 字符。在鸿蒙系统中,Text 组件可以直接渲染 Unicode emoji 字符,无需额外字体配置。这种方案的优势在于零资源开销和跨平台一致性,劣势在于不同设备的 emoji 渲染风格可能存在差异。rating 字段使用小数(如 9.7),在 UI 中通过模板字符串拼接展示为"⭐ 9.7",students 字段是整数,在课程横幅中通过 getCourseCount() * 46 的派生计算展示总学员数。

3.2 咒语与魔药数据集

const SPELLS: MagicSpell[] = [
  { id: 1, name: '烈焰风暴', icon: '🔥', power: 92, cost: 60, cooldown: '6 秒', type: '攻击', chant: '炎之灵听我号令' },
  { id: 2, name: '寒冰护盾', icon: '🧊', power: 78, cost: 45, cooldown: '8 秒', type: '防御', chant: '凝霜成壁' },
  // ...
];

const POTIONS: MagicPotion[] = [
  { id: 1, name: '法力回春药', icon: '💙', effect: '回复法力 300 点', brewTime: '4 小时', price: 120, progress: 88, level: '大师级' },
  // ...
];

在这里插入图片描述

SPELLS 数组包含八条咒语记录,type 字段的值包括"攻击"“防御”“治愈”"辅助"四种。在卡片渲染时,通过 s.type === '攻击' ? COLORS.danger : s.type === '治愈' ? COLORS.success : COLORS.primary 的嵌套三元表达式实现类型标签的条件着色——攻击类红色、治愈类绿色、其他类蓝色。这种在声明式 UI 中内联条件逻辑的写法是 ArkTS 的常见模式。

POTIONS 数组中 progress 字段的值范围是 0-100,代表百分比。在魔药详情弹窗中,进度条的内层 Row 组件宽度被设置为 this.selPotion!.progress + '%',外层容器固定高度为 10vp,从而形成了一个纯 ArkTS 实现的进度条控件,无需引入第三方组件库。

3.3 其他数据集概述

项目还定义了 MAGIC_TASKS(8 条任务)、ARENAS(6 条竞技赛事)、MAGIC_RANKS(8 条排行)、MAGIC_NOTICES(6 条公告)、MAGIC_ORDERS(5 条委托)、MAGIC_SKILLS(8 条技能)、MAGIC_QUICKS(8 条快捷入口)、FAMILIARS(6 条魔宠)和 TOWERS(6 条塔层)等静态数据数组。这些数组共同构成了应用的完整数据层,所有 UI 组件的渲染都直接或间接依赖这些数据源。

在实际工程中,这类静态数据通常会被替换为网络请求的返回值。但将静态数据定义在文件顶部有一个显著优势:它使得组件的 UI 开发可以与后端 API 开发完全解耦并行。开发者只需约定好接口的数据结构(即 interface 定义),前端就可以用静态数据先行开发,待后端就绪后替换数据来源即可。

四、全局工具函数体系

4.1 课程数据筛选函数

function getCourseCount(): number {
  return COURSES.length;
}

function getCourseLeft(): MagicCourse[] {
  let arr: MagicCourse[] = [];
  for (let i = 0; i < COURSES.length; i++) {
    if (i % 2 === 0) {
      arr.push(COURSES[i]);
    }
  }
  return arr;
}

function getCourseRight(): MagicCourse[] {
  let arr: MagicCourse[] = [];
  for (let i = 0; i < COURSES.length; i++) {
    if (i % 2 === 1) {
      arr.push(COURSES[i]);
    }
  }
  return arr;
}

getCourseCount 是一个极简的函数,返回课程数组的长度。在 ArkTS 组件的 build() 方法中,这类函数被直接调用,其返回值通过模板字符串嵌入到 Text 组件的内容中。由于这些函数是纯函数——不依赖任何外部可变状态,相同的输入永远产生相同的输出——它们在渲染过程中可以安全地多次调用而不会引发副作用。

getCourseLeftgetCourseRight 是一对分列函数,它们将课程数组按奇偶索引拆分为两个子数组。这种设计的目的是支持双列瀑布流布局:左列渲染偶数索引的课程卡片,右列渲染奇数索引的课程卡片。使用 for 循环遍历并条件 push 的写法虽然可以用 filter 更简洁地实现,但 for 循环的执行性能在数据量较大时略有优势,且对 ArkTS 编译器更友好。

4.2 高分课程筛选函数

function getTopCourses(): MagicCourse[] {
  let arr: MagicCourse[] = [];
  for (let i = 0; i < COURSES.length; i++) {
    if (COURSES[i].rating >= 9.5) {
      arr.push(COURSES[i]);
    }
  }
  return arr;
}

getTopCourses 函数筛选评分大于等于 9.5 的课程,用于课程页面顶部的横向滚动"王牌课程"区域。这种基于属性阈值的筛选在电商类应用中极为常见——例如"精选好物""高分推荐"等模块都是通过类似逻辑实现的。该函数在每次组件渲染时都会被调用,对于八条数据来说性能开销可以忽略,但如果数据量增大到数百条,建议将筛选结果缓存到状态变量中,避免每次渲染都重新遍历。

4.3 任务状态过滤函数

function getRunningTasks(): MagicTask[] {
  let arr: MagicTask[] = [];
  for (let i = 0; i < MAGIC_TASKS.length; i++) {
    if (MAGIC_TASKS[i].status === '进行中') {
      arr.push(MAGIC_TASKS[i]);
    }
  }
  return arr;
}

function getDoneTasks(): MagicTask[] {
  let arr: MagicTask[] = [];
  for (let i = 0; i < MAGIC_TASKS.length; i++) {
    if (MAGIC_TASKS[i].status === '已完成') {
      arr.push(MAGIC_TASKS[i]);
    }
  }
  return arr;
}

getRunningTasksgetDoneTasks 基于字符串等值比较进行过滤。在 ArkTS 中,字符串等值比较使用 === 运算符,执行严格的值和类型双重检查。这两个函数分别返回"进行中"和"已完成"的任务列表,前者展示在任务页面的双列区域,后者以简化行布局展示在页面底部的"已完成"列表中。同一数据集通过不同过滤条件呈现出不同的 UI 形态,这是数据驱动 UI 理念的直接体现。

4.4 竞技名额与排行拆分函数

function getOpenArenas(): MagicArena[] {
  let arr: MagicArena[] = [];
  for (let i = 0; i < ARENAS.length; i++) {
    if (ARENAS[i].joined < ARENAS[i].slots) {
      arr.push(ARENAS[i]);
    }
  }
  return arr;
}

function getRankTop(): MagicRank[] {
  let arr: MagicRank[] = [];
  for (let i = 0; i < MAGIC_RANKS.length; i++) {
    if (i < 3) {
      arr.push(MAGIC_RANKS[i]);
    }
  }
  return arr;
}

function getRankRest(): MagicRank[] {
  let arr: MagicRank[] = [];
  for (let i = 0; i < MAGIC_RANKS.length; i++) {
    if (i >= 3) {
      arr.push(MAGIC_RANKS[i]);
    }
  }
  return arr;
}

getOpenArenas 通过比较 joinedslots 字段筛选仍有空位的竞技场。getRankTopgetRankRest 将排行榜数据按索引前三和三以后拆分为两组,前者在排行榜弹窗中以领奖台样式(不同高度的柱状图)展示,后者以列表行样式展示。这种同一数据集的多种视图呈现,是组件化设计的典型应用——数据不变,视图随场景变化。

全局函数在本项目中承担了"数据准备层"的角色。它们位于组件之外,不参与 ArkTS 的状态追踪和渲染调度,仅在组件 build() 方法被调用时同步执行并返回结果。这种设计使得数据筛选逻辑与 UI 渲染逻辑保持了清晰的边界分离。如果未来需要引入响应应式数据(如网络请求结果),这些函数的调用方式可以平滑过渡为对状态变量的访问。

4.5 Mermaid:全局函数数据流图

数据源层

函数过滤层

组件渲染层

COURSES

getCourseCount

getCourseLeft

getCourseRight

getTopCourses

SPELLS

getSpellCount

getSpellLeft

getSpellRight

MAGIC_TASKS

getRunningTasks

getDoneTasks

getTaskLeft

getTaskRight

ARENAS

getOpenArenas

getArenaCount

MAGIC_RANKS

getRankTop

getRankRest

MAGIC_SKILLS

getUnlockedSkills

getSkillLeft

getSkillRight

CourseTab 双列

CourseTab 横滑

TaskTab 双列

TaskTab 已完成

ArenaTab 横滑

排行榜弹窗 领奖台

排行榜弹窗 列表

五、主入口组件与状态管理

5.1 组件声明与状态变量

@Entry
@Component
struct Index {
  @State currentTab: number = 0
  @State showCourse: boolean = false
  @State showEnroll: boolean = false
  @State showSpell: boolean = false
  @State showCast: boolean = false
  @State showPotion: boolean = false
  @State showBrew: boolean = false
  @State showTask: boolean = false
  @State showRank: boolean = false
  @State showArena: boolean = false
  @State showFamiliar: boolean = false
  @State showTower: boolean = false
  @State showNotice: boolean = false
  @State showOrder: boolean = false
  @State showSkill: boolean = false
  @State showVip: boolean = false
  @State showSupply: boolean = false

@Entry 装饰器标记 Index 为应用的入口组件,即页面树的根节点。每个鸿蒙页面有且仅有一个 @Entry 组件。@Component 装饰器声明 Index 为一个自定义组件,编译器会为其生成组件元数据、生命周期方法和渲染调度逻辑。struct 关键字是 ArkTS 的组件载体,与 TypeScript 中的 class 不同,struct 是值类型,但在此处主要用于承载组件定义。

状态变量方面,currentTab 是一个 number 类型的响应式状态,初始值为 0,代表默认显示第一个标签页。当用户点击底部 Tab 时,该变量被更新为对应索引值,触发条件渲染逻辑切换显示的标签页组件。十六个 showXxx 布尔状态变量分别控制十六个弹窗的显示与隐藏。每个弹窗需要两个状态配合工作:一个 showXxx 控制显隐,一个 selXxx 承载弹窗内容数据。这种"开关 + 数据"的双状态模式是弹窗管理的标准实践。

5.2 选中型变量声明

  selCourse: MagicCourse | null = null
  selSpell: MagicSpell | null = null
  selPotion: MagicPotion | null = null
  selTask: MagicTask | null = null
  selRank: MagicRank | null = null
  selArena: MagicArena | null = null
  selFamiliar: Familiar | null = null
  selTower: MagicTower | null = null
  selNotice: MagicNotice | null = null
  selOrder: MagicOrder | null = null
  selSkill: MagicSkill | null = null
  selVip: Familiar | null = null

这些 selXxx 变量没有使用 @State 装饰器,而是作为普通成员属性声明。它们的类型都是联合类型 T | null,初始值为 null。当用户点击某个列表项时,对应的 selXxx 被赋值为该项的数据对象,同时对应的 showXxx 被设为 true,弹窗随之显示。

这里有一个微妙的设计细节:这些变量没有被 @State 装饰,意味着它们的变更不会直接触发 UI 重绘。弹窗的显示实际上是由 showXxx@State 变化驱动的。当 showXxx 变为 true 时,框架重新执行 build() 方法,此时读取到的 selXxx 已经是最新赋值的数据对象。这种设计依赖于 ArkTS 的渲染时序——状态变更后的重绘会读取到最新的成员变量值。selVip 的类型是 Familiar | null,与荣誉巫师金卡弹窗的内容绑定,虽然语义上是 VIP 信息但复用了 Familiar 数据结构。

5.3 build 方法与头部布局

  build() {
    Column() {
      // ============ 头部(魔法学院横幅) ============
      Column({ space: 10 }) {
        Row() {
          Column({ space: 2 }) {
            Text('MAGIC ACADEMY').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('6').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: [['#1A1030', 0.0], ['#241A4A', 0.55], ['#9C6BFF', 1.0]] })

build() 方法是组件的核心,所有 UI 声明都在此方法内完成。最外层是一个 Column 容器,垂直排列头部、内容区和底部 Tab 三大部分。

头部区域是一个嵌套的 Column,通过 linearGradient 属性设置了 135 度角的线性渐变背景,从 #1A1030(深紫黑)经 #241A4A(深紫)到 #9C6BFF(紫色),营造出魔法学院的氛围感。linearGradient 接收一个对象参数,angle 指定渐变角度,colors 是一个颜色-位置对的数组,每个子数组的第一个元素是颜色值,第二个元素是该颜色在渐变线上的停止位置(0.0 到 1.0)。

头部第一行使用 Row 水平排列:左侧是标题区域(嵌套 Column),通过 layoutWeight(1) 占据剩余空间;右侧是通知图标和未读数角标。角标是一个 16x16 的 Text,背景色为 COLORS.danger(红色),通过 borderRadius(8) 形成圆形,通过 textAlign(TextAlign.Center) 使文字居中。第二行是搜索框区域,使用半透明白色背景和 20 的圆角,模拟一个输入框外观(实际是静态展示,没有输入功能)。

ColumnRow 是 ArkTS 布局系统的基础容器组件。Column 将子元素垂直排列,Row 将子元素水平排列。它们都接受一个可选的配置对象参数,如 { space: 10 } 设置子元素之间的间距。通过 ColumnRow 的嵌套组合,可以构建出任意复杂的二维布局。layoutWeight 属性是弹性布局的核心——它让子元素按权重分配父容器的剩余空间,类似 CSS Flexbox 中的 flex-grow

5.4 内容区与条件标签页切换

      // ============ 内容区 ============
      Column() {
        if (this.currentTab === 0) {
          CourseTab({
            onCourse: (c: MagicCourse) => {
              this.selCourse = c
              this.showCourse = true
            },
            onEnroll: (c: MagicCourse) => {
              this.selCourse = c
              this.showEnroll = true
            },
            onFamiliar: (f: Familiar) => {
              this.selFamiliar = f
              this.showFamiliar = true
            },
            onRank: (r: MagicRank) => {
              this.selRank = r
              this.showRank = true
            },
            onNotice: (n: MagicNotice) => {
              this.selNotice = n
              this.showNotice = true
            }
          })
        } else if (this.currentTab === 1) {
          SpellTab({ ... })
        } else if (this.currentTab === 2) {
          PotionTab({ ... })
        } else if (this.currentTab === 3) {
          TaskTab({ ... })
        } else if (this.currentTab === 4) {
          ArenaTab({ ... })
        } else {
          MineTab({ ... })
        }
      }
      .layoutWeight(1)

内容区是一个 Column 容器,通过 layoutWeight(1) 占据头部和底部之间的全部剩余空间。内部使用 if-else if-else 链根据 this.currentTab 的值条件渲染不同的标签页组件。ArkTS 的 build() 方法支持 if 条件语句,编译器会将其转化为条件渲染指令——当条件表达式的值变化时,框架自动插入或移除对应的组件子树。

每个标签页组件在构造时接收一组回调函数。以 CourseTab 为例,它接收五个回调:onCourse(查看课程详情)、onEnroll(选课确认)、onFamiliar(查看魔宠)、onRank(查看排行榜)、onNotice(查看公告)。每个回调函数的函数体都遵循相同的模式:先将数据赋值给对应的 selXxx 变量,再将对应的 showXxx 状态设为 true。这种"数据赋值 + 状态切换"的回调模式是父子组件通信的标准写法——子组件通过参数接收回调,在用户交互时调用回调并传入数据,父组件在回调中更新状态触发弹窗显示。

5.5 底部 Tab 导航栏

      // ============ 底部 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('#120A24')
      .border({ width: 1, color: '#3D2E6E' })

底部 Tab 栏是一个 Row 容器,高度固定为 64vp,背景色为最深的 #120A24,顶部有一像素的 #3D2E6E 边框作为分割线。六个 bottomTabItem 通过 this 引用调用 @Builder 方法,每个传入图标、标签和索引三个参数。使用 @Builder 方法封装 Tab 项的好处是避免代码重复——六个 Tab 项的布局逻辑完全一致,只是数据不同。

5.6 弹窗挂载机制

      // ============ 弹窗挂载 ============
      if (this.showCourse && this.selCourse !== null) {
        this.modalOverlay(() => { this.showCourse = false })
        this.courseModal()
      }
      if (this.showEnroll && this.selCourse !== null) {
        this.modalOverlay(() => { this.showEnroll = false })
        this.enrollModal()
      }
      // ... (共 16 组弹窗挂载条件)
      if (this.showSupply && this.selPotion !== null) {
        this.modalOverlay(() => { this.showSupply = false })
        this.supplyModal()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS.bg)
  }

弹窗挂载区位于 build() 方法的末尾、最外层 Column 的内部底部。十六组 if 条件块按照统一的模式书写:先检查 showXxx 状态是否为 trueselXxx 数据不为 null,条件满足时渲染遮罩层和弹窗内容。每个弹窗由两部分组成——modalOverlay 负责渲染半透明遮罩并处理点击关闭,具体的 xxxModal 负责渲染弹窗卡片内容。

这种弹窗架构的设计优势在于其声明式特性:弹窗的显隐完全由状态变量驱动,无需命令式的 show() / hide() 调用。当 showXxxtrue 变为 false 时,框架自动从渲染树中移除对应的弹窗组件。这种模式也确保了弹窗状态的持久性——即使用户在弹窗间切换,每个弹窗的显隐状态都独立维护,不会互相干扰。弹窗挂载区位于 Column 的最后一个子元素位置,但由于 modalOverlay 使用了 position({ x: 0, y: 0 }) 绝对定位,弹窗实际上覆盖在整个页面之上而非影响正常布局流。

这种基于条件渲染的弹窗管理模式是 ArkTS 的推荐做法。与命令式的 DialogController 相比,声明式弹窗的状态更可追踪、更易调试,且天然支持弹窗内容的响应式更新。当 selXxx 数据变化时,弹窗内容会自动重新渲染,无需手动刷新。

5.7 bottomTabItem 构建器

  @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 构建器。构建器方法与 build() 方法类似,都返回 UI 声明,但可以被多次调用以实现 UI 复用。bottomTabItem 接收三个参数:icon(emoji 图标)、label(文字标签)、idx(标签索引)。

内部布局是一个 Column,垂直排列图标和文字,宽度设为 16.6%(即 1/6 近似值,六个 Tab 项平分宽度)。justifyContent(FlexAlign.Center) 使子元素在主轴(垂直方向)上居中对齐。scale 属性通过三元表达式实现选中态的放大效果——当前选中的 Tab 项缩放为 1.1 倍,未选中的保持 1.0 倍。opacity 属性同理,选中态完全不透明,未选中态半透明(0.45),通过视觉差异引导用户识别当前位置。onClick 回调将 currentTab 状态更新为当前项索引,触发标签页切换。

5.8 modalOverlay 构建器

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

modalOverlay 是弹窗遮罩层的构建器,接收一个 onClose 回调函数作为参数。它渲染一个全屏的空 Column,背景色为 rgba(10,6,24,0.78)——极深紫色带 78% 透明度,覆盖在页面内容之上形成暗化效果。position({ x: 0, y: 0 }) 将遮罩层从正常文档流中脱离,绝对定位到页面左上角,使其覆盖整个屏幕。onClick 事件绑定到 onClose 回调,实现点击遮罩区域关闭弹窗的交互效果。

这个构建器是所有十六个弹窗的公共基础组件。每个弹窗挂载时都先调用 modalOverlay 传入关闭回调,再调用具体的弹窗内容构建器。这种分层设计使得遮罩逻辑与弹窗内容逻辑完全分离,修改遮罩样式(如透明度、动画)只需修改一处。

六、弹窗体系深度剖析

6.1 课程详情弹窗

  @Builder
  courseModal() {
    Column() {
      Column({ space: 10 }) {
        Row() {
          Text(this.selCourse!.icon).fontSize(38).width(70).height(70)
            .textAlign(TextAlign.Center)
            .backgroundColor('rgba(91,140,255,0.16)').borderRadius(18)
          Column({ space: 3 }) {
            Text(this.selCourse!.name).fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
            Text(this.selCourse!.teacher + ' | ' + this.selCourse!.level).fontSize(12).fontColor(COLORS.secondary)
            Text('⭐ ' + this.selCourse!.rating + ' | ' + this.selCourse!.students + ' 人已选').fontSize(11).fontColor(COLORS.gold)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
          Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showCourse = false })
        }
        .width('100%')

courseModal 是课程详情弹窗的构建器。外层 Column 全屏占满并设置 justifyContent(FlexAlign.Center),使弹窗卡片在屏幕中央居中显示。内层 Column 是弹窗卡片本体,宽度为 88%,背景色为 COLORS.cardBg,带 16 的圆角和 16 的内边距。

卡片首行使用 Row 水平排列三个元素:左侧是 70x70 的图标容器,使用半透明蓝色背景和 18 的圆角;中间是课程信息区域,通过 layoutWeight(1) 占据剩余空间,内含课程名称(18号粗体)、教师和级别信息(12号紫色)、评分和选课人数(11号金色);右侧是关闭按钮,点击后设置 showCourse = false 关闭弹窗。

代码中大量使用了 this.selCourse!.icon 这样的非空断言操作符 !。这是因为 selCourse 的类型是 MagicCourse | null,TypeScript 要求在使用前进行空值检查。但由于弹窗的挂载条件已经包含了 this.selCourse !== null 的判断,此处的 ! 断言在逻辑上是安全的——只有非空时弹窗才会被渲染。

6.2 课程详情弹窗的数据卡片与操作按钮

        Row({ space: 8 }) {
          Column({ space: 3 }) {
            Text('学分').fontSize(10).fontColor(COLORS.textSecondary)
            Text(this.selCourse!.credit + ' 分').fontSize(14).fontColor(COLORS.accent).fontWeight(FontWeight.Bold)
          }
          .layoutWeight(1)
          .padding(10)
          .backgroundColor('rgba(123,224,255,0.10)')
          .borderRadius(10)
          Column({ space: 3 }) {
            Text('上课时间').fontSize(10).fontColor(COLORS.textSecondary)
            Text(this.selCourse!.time).fontSize(13).fontColor(COLORS.primary).fontWeight(FontWeight.Bold)
          }
          .layoutWeight(1)
          .padding(10)
          .backgroundColor('rgba(91,140,255,0.10)')
          .borderRadius(10)
        }
        .width('100%')
        Text(this.selCourse!.desc).fontSize(12).fontColor(COLORS.textSecondary).lineHeight(20)

弹窗的第二行是两个等宽的数据卡片,使用 layoutWeight(1) 实现均分。每个卡片内部是垂直排列的标签和值——标签使用 10 号次要文字色,值使用 14 号或 13 号的主题色粗体。卡片背景使用对应主题色的低透明度变体(0.10 透明度),形成一种"着色玻璃"效果。这种"标签-值"卡片在信息展示类弹窗中极为常见,是电商产品详情页、订单信息卡等场景的标准设计模式。

课程描述文本使用 lineHeight(20) 设置行高为 20vp,比默认行高更宽松,提升了多行文本的可读性。在 ArkTS 中,lineHeight 属性控制文本的行间距,对于描述类长文本,适当增加行高可以显著改善阅读体验。

        Row({ space: 10 }) {
          Text('立即选课 ›').fontSize(13).fontColor(COLORS.white).textAlign(TextAlign.Center).layoutWeight(1)
            .padding({ top: 11, bottom: 11 })
            .backgroundColor(COLORS.primary)
            .borderRadius(20)
            .onClick(() => {
              this.showCourse = false
              this.showEnroll = true
            })
          Text('课程大纲').fontSize(13).fontColor(COLORS.textSecondary).textAlign(TextAlign.Center).layoutWeight(1)
            .padding({ top: 11, bottom: 11 })
            .backgroundColor('rgba(255,255,255,0.08)')
            .borderRadius(20)
            .onClick(() => {
              this.showCourse = false
            })
        }
        .width('100%')

操作按钮区域使用 Row 水平排列两个等宽按钮。"立即选课"按钮使用主题色背景、白色文字,点击后先关闭当前弹窗(showCourse = false)再打开选课确认弹窗(showEnroll = true),实现弹窗间的跳转链式调用。"课程大纲"按钮使用半透明白色背景、次要文字色,点击后仅关闭当前弹窗。两个按钮的圆角均为 20,与搜索框的圆角一致,保持了视觉风格的统一。

6.3 施法确认弹窗的步进器交互

  @Builder
  castModal() {
    Column() {
      Column({ space: 10 }) {
        Row() {
          Text('🎯').fontSize(30).width(54).height(54).textAlign(TextAlign.Center)
            .backgroundColor('rgba(255,215,106,0.18)').borderRadius(14)
          Column({ space: 3 }) {
            Text('施法设置').fontSize(18).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
            Text('咒语:' + this.selSpell!.name).fontSize(12).fontColor(COLORS.gold)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })
          Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showCast = false })
        }
        .width('100%')
        Row({ space: 10 }) {
          Text('−').fontSize(24).fontColor(COLORS.white).width(40).height(40)
            .textAlign(TextAlign.Center).backgroundColor(COLORS.secondary).borderRadius(20)
          Text('3').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,215,106,0.08)')
        .borderRadius(12)

施法确认弹窗的特色在于其步进器设计——减号按钮、数值显示、加号按钮和单位标签水平排列。减号和加号按钮都是 40x40 的圆形按钮(borderRadius(20)),背景色为 COLORS.secondary。中间的数值"3"使用 20 号金色粗体显示。整个步进器区域使用金色低透明度背景和 12 的圆角,与弹窗的金色主题呼应。总消耗通过 this.selSpell!.cost * 3 的乘法计算得出,展示了弹窗内的派生计算能力。

这个弹窗的边框使用了 BorderStyle.Dashed(虚线样式),这是 ArkTS 边框系统的一个特性——通过 border({ width: 2, color: COLORS.gold, style: BorderStyle.Dashed }) 设置金色虚线边框,在视觉上与实线边框弹窗形成风格区分,暗示该弹窗的特殊操作性质。

6.4 排行榜弹窗的领奖台布局

        Row({ space: 6 }) {
          ForEach(getRankTop(), (r: MagicRank) => {
            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(91,140,255,0.06)')
            .borderRadius(10)
          })
        }
        .width('100%')
        .alignItems(VerticalAlign.Bottom)

排行榜弹窗的领奖台区域是整个应用中最具视觉创意的布局之一。ForEach 遍历 getRankTop() 返回的前三名数据,每个排名项是一个 Column,底部附带一个空 Column 作为"台柱"。台柱的高度通过嵌套三元表达式动态设置:第一名 46vp、第二名 32vp、第三名 22vp,形成阶梯递减的领奖台效果。台柱颜色同理:第一名金色、第二名紫色、第三名橙色,台柱顶部使用 borderRadius({ topLeft: 6, topRight: 6 }) 仅圆角化顶部两个角,模拟实体领奖台的造型。

Row 容器设置 alignItems(VerticalAlign.Bottom) 使三个排名项在交叉轴(垂直方向)上底部对齐,这是领奖台效果的关键——三个不同高度的台柱从底部对齐向上延伸,形成经典的颁奖台造型。这种通过空容器高度变化实现图表化视觉效果的手法,展示了 ArkTS 声明式 UI 在不引入图表库的情况下实现复杂视觉设计的能力。

6.5 技能图谱弹窗的柱状图

        Row({ space: 6 }) {
          ForEach(getUnlockedSkills(), (s: MagicSkill) => {
            Column({ space: 4 }) {
              Text(s.exp + '').fontSize(9).fontColor(COLORS.gold)
              Column() {
              }
              .width('100%')
              .height((s.exp / 10) + '%')
              .linearGradient({ angle: 180, colors: [[COLORS.primary, 0.0], [COLORS.purple, 1.0]] })
              .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(91,140,255,0.06)')
        .borderRadius(12)
        .alignItems(VerticalAlign.Bottom)

技能图谱弹窗使用纯 ArkTS 组件实现了一个柱状图。ForEach 遍历 getUnlockedSkills() 返回的已解锁技能(level >= 3),每个柱子是一个 Column,其中包含一个空 Column 作为柱体。柱体高度通过 (s.exp / 10) + '%' 计算——将经验值除以 10 转换为百分比字符串,例如 800 经验值对应 80% 高度。柱体使用 linearGradient 设置从蓝色到紫色的垂直渐变,增强视觉表现力。

外层 Row 设置 height(150) 固定图表区域高度,alignItems(VerticalAlign.Bottom) 使柱子从底部对齐向上生长,模拟真实柱状图的视觉效果。每个柱子内部使用 justifyContent(FlexAlign.End) 使内容从底部开始排列——经验值数值在柱顶,柱体居中,图标和名称在柱底。s.name.slice(0, 2) 截取技能名称前两个字符,防止长名称溢出柱子宽度。

6.6 荣誉巫师金卡弹窗

  @Builder
  vipModal() {
    Column() {
      Column({ space: 0 }) {
        Column({ space: 6 }) {
          Text('👑').fontSize(40)
          Text('荣誉巫师会').fontSize(19).fontWeight(FontWeight.Bold).fontColor('#3B2B00')
          Text(this.selVip!.name + ' · 与魔宠 ' + this.selVip!.name + ' 共冕').fontSize(12).fontColor('#5D4A00')
          Text('享学院贵宾特权').fontSize(11).fontColor('#5D4A00')
        }
        .width('100%')
        .padding({ top: 22, bottom: 22 })
        .linearGradient({ angle: 135, colors: [['#FFE9B0', 0.0], ['#FFD76A', 0.55], ['#F5A623', 1.0]] })
        .borderRadius({ topLeft: 16, topRight: 16 })

荣誉巫师金卡弹窗采用了与所有其他弹窗不同的配色方案——金色渐变头部配合深棕色文字。头部区域的 linearGradient 从浅金 #FFE9B0 经金色 #FFD76A 到深金 #F5A623,模拟实体金卡的金属光泽。文字颜色使用 #3B2B00(极深棕黑)和 #5D4A00(深棕),在金色背景上形成高对比度,这种深字浅底的配色在深色主题应用中形成了强烈的视觉反差,突出了 VIP 弹窗的尊贵感。

头部只圆角化顶部两角 borderRadius({ topLeft: 16, topRight: 16 }),底部两角保持直角,与下方的特权列表区域无缝拼接。这种局部圆角化技巧在卡片设计中极为常用——通过分别设置四个角的圆角值,实现卡片头身分离的视觉效果。

6.7 Mermaid:弹窗体系架构图

true

true

true

true

true

true

true

true

遮罩绝对定位

Index build 方法

弹窗挂载区

showCourse && selCourse != null

showEnroll && selCourse != null

showSpell && selSpell != null

showCast && selSpell != null

... 共16组条件

modalOverlay 遮罩

courseModal 课程详情

modalOverlay 遮罩

enrollModal 选课确认

modalOverlay 遮罩

spellModal 咒语卡

modalOverlay 遮罩

castModal 施法确认

showCourse = false

跳转 enrollModal

showCast = false

覆盖全屏

弹窗内容居中/底部

七、标签页组件体系

7.1 CourseTab 课程页结构

@Component
struct CourseTab {
  onCourse: (c: MagicCourse) => void = () => {}
  onEnroll: (c: MagicCourse) => void = () => {}
  onFamiliar: (f: Familiar) => void = () => {}
  onRank: (r: MagicRank) => void = () => {}
  onNotice: (n: MagicNotice) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        // 魔法横幅
        Column({ space: 6 }) {
          Text('📚 魔法课程中心').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
          Text('本学期开课 ' + getCourseCount() + ' 门 · 学员 ' + (getCourseCount() * 46) + ' 人')
            .fontSize(12).fontColor('rgba(237,231,246,0.85)')
          Row({ space: 8 }) {
            Text('🪄 选课通道开放').fontSize(11).fontColor(COLORS.white)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .backgroundColor('rgba(156,107,255,0.30)').borderRadius(10)
            Text('🔮 占星课热选').fontSize(11).fontColor(COLORS.white)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .backgroundColor('rgba(255,215,106,0.25)').borderRadius(10)
          }
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .padding(16)
        .linearGradient({ angle: 135, colors: [['#1A1030', 0.0], ['#9C6BFF', 1.0]] })
        .borderRadius(16)

CourseTab 是第一个标签页组件,接收五个回调函数作为属性参数。这些回调的默认值都是空箭头函数 () => {},这是一种防御性编程实践——即使父组件没有传入某个回调,调用时也不会报错。

build() 方法的最外层是 Scroll 组件,使页面内容可以垂直滚动。内部是 Column({ space: 10 }) 作为内容容器,各区块之间保持 10vp 的间距。第一个区块是魔法横幅,使用 135 度紫色渐变背景,内部垂直排列标题(22 号白色粗体)、统计信息(12 号半透明白色)和标签行。统计信息通过 getCourseCount() * 46 的派生计算展示估算学员数,这种将静态数据通过函数调用和算术运算组合生成展示信息的手法,是数据驱动 UI 的典型应用。

标签行中的每个标签都是一个 Text 组件,通过 padding 设置内边距形成胶囊形状,borderRadius(10) 圆角化,backgroundColor 使用对应主题色的半透明变体。两个标签分别使用紫色和金色的半透明背景,在横幅渐变上形成可辨识的色彩区分。

7.2 课程页快捷宫格与横滑列表

        // 快捷宫格
        Row({ space: 8 }) {
          Column({ space: 8 }) {
            ForEach(getQuickLeft(), (q: MagicQuick) => {
              this.quickCard(q)
            })
          }
          .layoutWeight(1)
          Column({ space: 8 }) {
            ForEach(getQuickRight(), (q: MagicQuick) => {
              this.quickCard(q)
            })
          }
          .layoutWeight(1)
        }
        .width('100%')

        // 王牌课程横滑
        Scroll() {
          Row({ space: 10 }) {
            ForEach(getTopCourses(), (c: MagicCourse) => {
              this.courseWideCard(c)
            })
          }
          .padding({ right: 4 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .width('100%')
        .constraintSize({ maxHeight: 150 })

快捷宫格区域使用双列布局,通过 getQuickLeft()getQuickRight() 分别获取左右两列的快捷入口数据。每列内部使用 ForEach 遍历数据数组,调用 this.quickCard(q) 构建器渲染每个入口项。ForEach 是 ArkTS 的核心列表渲染指令,它接收三个参数:数据数组、项生成函数和可选的键值生成函数。框架通过键值追踪每个项的身份,当数据变化时实现高效的差量更新。

王牌课程区域使用了嵌套的 Scroll 组件,通过 scrollable(ScrollDirection.Horizontal) 设置为水平滚动方向。内部 Row 水平排列 getTopCourses() 返回的高分课程卡片。constraintSize({ maxHeight: 150 }) 限制滚动区域的最大高度为 150vp,防止卡片内容过多时撑开过高。padding({ right: 4 }) 在右侧留出微小间距,使最后一张卡片不完全贴边。

ForEach 是 ArkTS 列表渲染的核心机制。与直接使用 map 不同,ForEach 被框架特殊编译,能够根据数据变化进行精确的增删改差量渲染。在 ForEach 的项生成函数中,每个数据项都通过闭包捕获了自身的数据引用,因此即使列表重排,每张卡片也能保持正确的数据绑定。

7.3 课程页双列瀑布流与公告列表

        // 全部课程双列
        Row({ space: 10 }) {
          Column({ space: 10 }) {
            ForEach(getCourseLeft(), (c: MagicCourse) => {
              this.courseCard(c)
            })
          }
          .layoutWeight(1)
          Column({ space: 10 }) {
            ForEach(getCourseRight(), (c: MagicCourse) => {
              this.courseCard(c)
            })
          }
          .layoutWeight(1)
        }
        .width('100%')

        // 公告斑马纹列
        Column({ space: 8 }) {
          ForEach(MAGIC_NOTICES, (n: MagicNotice) => {
            this.noticeRow(n)
          })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('rgba(36,26,74,0.85)')
        .borderRadius(14)

全部课程区域采用与快捷宫格相同的双列布局模式,通过 getCourseLeft()getCourseRight() 将课程数据按奇偶索引拆分到左右两列。这种"手动分列"的方式虽然不如 CSS Grid 或 Flex 换行优雅,但在 ArkTS 中是一种稳定可靠的瀑布流实现方案。每张课程卡片的高度由内容决定,左右两列的卡片数量相等但高度可能不同,形成参差错落的瀑布流视觉效果。

公告列表区域直接遍历 MAGIC_NOTICES 数组(未经过滤函数),通过 noticeRow 构建器渲染每一行。整个公告区域使用半透明深紫色背景和 14 的圆角,与页面背景形成微妙的层次区分。

7.4 课程卡片构建器

  @Builder
  courseCard(c: MagicCourse) {
    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.rating).fontSize(10).fontColor(COLORS.gold)
      }
      .width('100%')
      Text(c.teacher + ' | ' + c.level).fontSize(10).fontColor(COLORS.secondary)
        .width('100%').textAlign(TextAlign.Start)
      Row() {
        Text(c.credit + ' 学分').fontSize(10).fontColor(COLORS.accent)
        Text('选课 ›').fontSize(11).fontColor(COLORS.gold)
          .onClick(() => {
            this.onEnroll(c)
          })
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
    .backgroundColor(COLORS.cardBg)
    .borderRadius(14)
    .onClick(() => {
      this.onCourse(c)
    })
  }

courseCard 构建器是课程列表中的标准卡片。卡片整体是一个 Column,背景色为 COLORS.cardBg,圆角 14,内边距 12。卡片首行使用 Row 水平排列图标、名称和评分,名称通过 layoutWeight(1) 占据中间空间,评分固定在右侧。第二行是教师和级别的组合文本。第三行是学分信息和选课操作入口。

卡片的点击事件绑定到 this.onCourse(c) 回调——点击卡片任意区域打开课程详情弹窗。而"选课"操作绑定到 this.onEnroll(c) 回调——仅点击"选课 ›"文本时触发选课确认弹窗。这种"整卡点击 + 局部按钮"的双重交互模式是列表卡片的常见设计:整卡点击查看详情,局部按钮执行操作。在 ArkTS 中,子组件的 onClick 会拦截事件,不会冒泡到父组件的 onClick(如果事件冒泡会导致两个弹窗同时触发)。

7.5 SpellTab 咒语页与塔层展示

@Component
struct SpellTab {
  onSpell: (s: MagicSpell) => void = () => {}
  onCast: (s: MagicSpell) => void = () => {}
  onTower: (t: MagicTower) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        Column({ space: 6 }) {
          Text('✨ 咒语典籍馆').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
          Text('收录咒语 ' + getSpellCount() + ' 条 · 每日默诵 3 遍')
            .fontSize(12).fontColor('rgba(237,231,246,0.85)')
          Row({ space: 8 }) {
            Text('🗣 速咏训练中').fontSize(11).fontColor(COLORS.white)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .backgroundColor('rgba(123,224,255,0.22)').borderRadius(10)
            Text('📖 咏唱规范').fontSize(11).fontColor(COLORS.white)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .backgroundColor('rgba(91,140,255,0.25)').borderRadius(10)
          }
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .padding(16)
        .linearGradient({ angle: 135, colors: [['#1A1030', 0.0], ['#5B8CFF', 1.0]] })
        .borderRadius(16)

SpellTab 的结构与 CourseTab 高度一致——横幅、双列列表、辅助信息条。差异在于横幅的渐变色从紫色变为蓝色(#5B8CFF),标签内容从课程相关变为咒语相关,统计信息从课程数和学员数变为咒语条数和默诵频次。这种结构一致但内容差异化的设计模式使得六个标签页在视觉上保持了统一的节奏感,同时通过横幅渐变色的变化实现了页面间的视觉区分。

咒语卡片 spellCard 的特色在于类型标签的条件着色逻辑:攻击类红色、治愈类绿色、其他类蓝色。这个三元嵌套表达式 s.type === '攻击' ? COLORS.danger : s.type === '治愈' ? COLORS.success : COLORS.primary 是 ArkTS 中内联条件渲染的典型写法。类型标签通过 paddingborderRadius 形成胶囊形状,白色文字在彩色背景上形成高对比度。

7.6 PotionTab 魔药页与委托列表

@Component
struct PotionTab {
  onPotion: (p: MagicPotion) => void = () => {}
  onBrew: (p: MagicPotion) => void = () => {}
  onOrder: (o: MagicOrder) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        Column({ space: 6 }) {
          Text('🧪 炼金魔药坊').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
          Text('在售药方 ' + getPotionCount() + ' 种 · 今日出货 ' + (getPotionCount() * 12) + ' 瓶')
            .fontSize(12).fontColor('rgba(237,231,246,0.85)')
          Row({ space: 8 }) {
            Text('⚗ 三炉熬制中').fontSize(11).fontColor(COLORS.white)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .backgroundColor('rgba(76,217,100,0.25)').borderRadius(10)
            Text('🌿 草药库存充足').fontSize(11).fontColor(COLORS.white)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .backgroundColor('rgba(255,215,106,0.22)').borderRadius(10)
          }
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .padding(16)
        .linearGradient({ angle: 135, colors: [['#1A1030', 0.0], ['#4CD964', 1.0]] })
        .borderRadius(16)

PotionTab 的横幅渐变色为绿色(#4CD964),与魔药的草药属性呼应。统计信息中的"今日出货"通过 getPotionCount() * 12 派生计算,将魔药种类数乘以 12 得到估算的出货瓶数。这种派生计算虽然不是真实数据,但在演示类应用中为界面提供了丰富的数字信息,增强了视觉充实度。

魔药页除了双列魔药卡片外,还包含一个"熬制进度提醒"信息条和一个委托列表面板。委托列表通过 getOrderActive() 过滤出"熬制中"和"待熬制"状态的委托,每条委托卡片展示编号、状态、物品和路线信息。状态文本通过条件着色——"待熬制"显示为橙色,其他状态显示为绿色,帮助用户快速识别需要关注的委托。

7.7 TaskTab 任务页与竞技横滑

@Component
struct TaskTab {
  onTask: (t: MagicTask) => void = () => {}
  onArena: (a: MagicArena) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        Column({ space: 6 }) {
          Text('📜 学院任务板').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#3B2B00')
          Text('今日任务 ' + getTaskCount() + ' 项 · 进行中 ' + getRunningTasks().length + ' 项')
            .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: [['#FFE9B0', 0.0], ['#FFD76A', 1.0]] })
        .borderRadius(16)

TaskTab 的横幅是唯一一个使用浅色渐变的标签页横幅——从浅金 #FFE9B0 到金色 #FFD76A。相应地,横幅中的文字颜色从白色变为深棕色 #3B2B00#4A3700,标签背景也变为半透明白色。这种深字浅底的配色在六个标签页中独树一帜,为任务页面赋予了"羊皮纸公告板"的视觉隐喻。

任务页的统计信息使用了 getRunningTasks().length 来获取进行中任务的数量。这里 getRunningTasks() 返回一个数组,.length 取其长度。这种将过滤函数的返回值直接用于 UI 展示的写法虽然方便,但每次渲染都会执行一次过滤操作。在数据量较小时可以接受,数据量增大时需要考虑性能优化。

7.8 ArenaTab 竞技页与技能展示

@Component
struct ArenaTab {
  onArena: (a: MagicArena) => void = () => {}
  onSkill: (sk: MagicSkill) => void = () => {}
  onRank: (r: MagicRank) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        Column({ space: 6 }) {
          Text('🏆 魔法竞技场').fontSize(22).fontWeight(FontWeight.Bold).fontColor(COLORS.white)
          Text('本月赛事 ' + getArenaCount() + ' 场 · 冠军奖金最高 1000 币')
            .fontSize(12).fontColor('rgba(237,231,246,0.85)')
          Row({ space: 8 }) {
            Text('⚔ 报名通道开启').fontSize(11).fontColor(COLORS.white)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .backgroundColor('rgba(255,107,107,0.28)').borderRadius(10)
            Text('🥇 上届冠军已决出').fontSize(11).fontColor(COLORS.white)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .backgroundColor('rgba(255,215,106,0.25)').borderRadius(10)
          }
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .padding(16)
        .linearGradient({ angle: 135, colors: [['#4A1020', 0.0], ['#FF6B6B', 1.0]] })
        .borderRadius(16)

ArenaTab 的横幅渐变从深红 #4A1020 到红色 #FF6B6B,传达竞技场的热血氛围。竞技页面包含竞技横滑列表(使用 getOpenArenas() 过滤仍有空位的赛事)和技能双列展示。底部还有一个金色渐变的"巫师榜"入口条,点击后通过 this.onRank(MAGIC_RANKS[0]) 回调打开排行榜弹窗。

竞技卡片 arenaCardarenaWideCard 的区别在于布局密度——前者是全宽双列卡片,包含规则、报名人数和奖励信息;后者是固定宽度的横滑卡片,仅展示图标、名称和简要信息。同一数据源通过不同构建器产生不同视图密度,是列表设计的常见模式。

7.9 MineTab 我的页与综合信息展示

@Component
struct MineTab {
  onTask: (t: MagicTask) => void = () => {}
  onSkill: (sk: MagicSkill) => void = () => {}
  onOrder: (o: MagicOrder) => void = () => {}
  onRank: (r: MagicRank) => void = () => {}
  onNotice: (n: MagicNotice) => void = () => {}
  onVip: (f: Familiar) => void = () => {}
  onSupply: (p: MagicPotion) => void = () => {}

  build() {
    Scroll() {
      Column({ space: 10 }) {
        // 档案卡
        Column({ space: 10 }) {
          Row() {
            Text('🧙').fontSize(34).width(60).height(60).textAlign(TextAlign.Center)
              .backgroundColor('rgba(156,107,255,0.18)').borderRadius(30)
            Column({ space: 3 }) {
              Text('MA-1096').fontSize(17).fontWeight(FontWeight.Bold).fontColor(COLORS.textPrimary)
              Text('魔法学院 · 高阶学徒').fontSize(11).fontColor(COLORS.textSecondary)
              Text('⭐ 魔力成就 9800').fontSize(11).fontColor(COLORS.gold)
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            .margin({ left: 12 })
          }
          .width('100%')
          Row({ space: 8 }) {
            Column({ space: 2 }) {
              Text(getTaskCount() + '').fontSize(16).fontColor(COLORS.orange).fontWeight(FontWeight.Bold)
              Text('本月任务').fontSize(10).fontColor(COLORS.textSecondary)
            }
            .layoutWeight(1)
            .padding(10)
            .backgroundColor('rgba(255,169,77,0.10)')
            .borderRadius(10)
            // ... 两个更多统计卡片
          }
          .width('100%')
        }
        .width('100%')
        .padding(14)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(16)

MineTab 是六个标签页中信息最密集的一个,接收七个回调函数。页面顶部是用户档案卡,展示头像、ID、等级和成就值,下方是三个统计卡片(本月任务、研修技能、契约魔宠),分别使用橙色、紫色和金色的半透明背景。档案卡之后是荣誉巫师金卡入口、任务双列、已完成任务列表、技能双列、委托列表、消息列表和材料申领入口,几乎汇总了应用所有数据类型的信息切片。

"我的"页的档案卡头像使用 borderRadius(30) 形成 60vp 直径的圆形,这是 ArkTS 中实现圆形容器的标准方式——将 borderRadius 设为宽高值的一半即可。统计卡片使用 getTaskCount() + '' 将数字转为字符串,这是 ArkTS/TypeScript 中数字到字符串的快速转换写法,等价于 String(getTaskCount())`${getTaskCount()}`

7.10 MineTab 的消息列表与条件图标

        // 消息列
        Column({ space: 8 }) {
          ForEach(MAGIC_NOTICES, (n: MagicNotice) => {
            Row() {
              Text(n.level === '紧急' ? '🔴' : '🔵').fontSize(12).width(24)
              Text(n.title).fontSize(12).fontColor(COLORS.textPrimary).layoutWeight(1).maxLines(1)
              Text(n.date).fontSize(10).fontColor(COLORS.textHint)
            }
            .width('100%')
            .padding(10)
            .backgroundColor('rgba(91,140,255,0.05)')
            .borderRadius(10)
            .onClick(() => {
              this.onNotice(n)
            })
          })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('rgba(36,26,74,0.85)')
        .borderRadius(14)

"我的"页的消息列表与课程页的公告列表使用了相同的数据源 MAGIC_NOTICES,但渲染方式不同。课程页的公告行使用标签-标题-日期的三段式布局,标签使用文字(“紧急”"活动"等)配彩色背景。"我的"页的消息行使用图标-标题-日期的布局,图标通过 n.level === '紧急' ? '🔴' : '🔵' 的条件表达式选择——紧急消息显示红点,其他显示蓝点。maxLines(1) 限制标题只显示一行,超出的文字被截断,适应消息列表的单行布局需求。

这种同一数据在不同页面以不同视图呈现的设计,充分体现了组件化开发的灵活性——数据与视图完全解耦,同一个数据源可以根据上下文产生多种视觉表达。

7.11 Mermaid:组件层级与通信图

currentTab=0

currentTab=1

currentTab=2

currentTab=3

currentTab=4

currentTab=5

回调

回调

回调

回调

回调

回调

@Entry Index

@State 状态层

currentTab

showXxx × 16

selXxx × 12

build() 方法

头部横幅

内容区

底部 Tab

弹窗挂载区 × 16

CourseTab

SpellTab

PotionTab

TaskTab

ArenaTab

MineTab

modalOverlay × 16

内容弹窗 × 16

@Builder: quickCard, courseCard, familiarCard, noticeRow...

@Builder: spellCard, towerCard

@Builder: potionCard, orderCard

@Builder: taskCard, arenaCard

@Builder: arenaCard, arenaWideCard, skillCard

@Builder: taskCard, skillCard, orderCard

八、通用构建器与复用模式

8.1 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 是主入口组件中定义的通用区块标题构建器,接收标题文本和"更多"文本两个参数。标题使用 15 号粗体主文字色,通过 layoutWeight(1) 占据左侧空间;"更多"使用 11 号金色,固定在右侧。上下边距通过 margin({ top: 10, bottom: 8 }) 设置,使标题与上下内容区块保持适当的呼吸空间。

这个构建器虽然在 Index 组件中定义,但在当前代码中并未被直接调用——这可能是预留的通用工具,或者在代码演化过程中被内联替代。在 ArkTS 中,@Builder 方法定义在组件内部,只能被该组件的 build() 方法或其他 @Builder 方法调用。如果需要在多个组件间共享构建器,可以考虑使用 @Builder 全局函数(使用 function 关键字在组件外部定义并添加 @Builder 装饰器)。

8.2 quickCard 快捷入口构建器

  @Builder
  quickCard(q: MagicQuick) {
    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)
  }

quickCardCourseTab 中定义的快捷入口卡片构建器。它的特色在于图标背景色使用了 q.color + '26' 的字符串拼接——MagicQuick 数据模型中每个项自带一个十六进制色值(如 #5B8CFF),通过追加 26(十六进制的 38,约 15% 透明度)生成对应的低透明度变体。这种设计让每个快捷入口拥有与自身主题色匹配的图标背景,增强了视觉辨识度。

在 ArkTS 中,颜色字符串支持 #RRGGBB#RRGGBBAA 两种格式。q.color + '26' 的拼接实际上是生成了一个 #RRGGBBAA 格式的八位色值,其中 AA 部分为 26(约 15% 不透明度)。这是一种灵活的颜色透明度控制技巧,无需使用 rgba() 函数。

8.3 familiarCard 魔宠卡片构建器

  @Builder
  familiarCard(f: Familiar) {
    Column({ space: 4 }) {
      Text(f.icon).fontSize(24)
      Text(f.name).fontSize(12).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold)
      Text(f.kind + ' · Lv.' + f.level).fontSize(10).fontColor(COLORS.gold)
    }
    .width(96)
    .padding(10)
    .backgroundColor('rgba(255,215,106,0.10)')
    .borderRadius(14)
    .onClick(() => {
      this.onFamiliar(f)
    })
  }

familiarCard 是魔宠横滑列表中的卡片构建器,固定宽度为 96vp。卡片使用金色低透明度背景,内部垂直排列图标(24 号)、名称(12 号粗体)和种类+等级(10 号金色)。点击卡片触发 this.onFamiliar(f) 回调,打开魔宠档案弹窗。固定宽度的卡片在横滑列表中是标准做法——所有卡片宽度一致,滚动时形成整齐的视觉节奏。

8.4 noticeRow 公告行构建器

  @Builder
  noticeRow(n: MagicNotice) {
    Row() {
      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)
      Text(n.title).fontSize(12).fontColor(COLORS.textPrimary)
        .layoutWeight(1).margin({ left: 8 }).maxLines(1)
      Text(n.date).fontSize(10).fontColor(COLORS.textHint)
    }
    .width('100%')
    .padding({ top: 6, bottom: 6 })
    .onClick(() => {
      this.onNotice(n)
    })
  }

noticeRow 是公告列表行构建器。每行包含三个元素:级别标签(胶囊形状,紧急为红色、其他为蓝色)、标题文本(通过 layoutWeight(1) 占据中间空间,maxLines(1) 限制为单行)和日期(10 号提示色)。行的上下内边距为 6vp,使各行之间保持紧凑但不拥挤的间距。点击整行触发 this.onNotice(n) 回调打开公告详情弹窗。

8.5 orderCard 委托卡片构建器

  @Builder
  orderCard(o: MagicOrder) {
    Column({ space: 6 }) {
      Row() {
        Text(o.no).fontSize(11).fontColor(COLORS.gold).layoutWeight(1)
        Text(o.status).fontSize(10)
          .fontColor(o.status === '待熬制' ? COLORS.orange : COLORS.success)
      }
      .width('100%')
      Text(o.item).fontSize(12).fontColor(COLORS.textPrimary).fontWeight(FontWeight.Bold)
        .width('100%').textAlign(TextAlign.Start)
      Text(o.from + ' → ' + o.to + ' | ' + o.eta).fontSize(10).fontColor(COLORS.textSecondary)
        .width('100%').textAlign(TextAlign.Start)
    }
    .width('100%')
    .padding(10)
    .backgroundColor('rgba(76,217,100,0.06)')
    .borderRadius(12)
    .onClick(() => {
      this.onOrder(o)
    })
  }

orderCard 委托卡片展示了编号、状态、物品和路线信息。状态文本通过 o.status === '待熬制' ? COLORS.orange : COLORS.success 条件着色——待熬制显示橙色表示等待中,其他状态(熬制中、已送达、已签收)显示绿色表示进行中或已完成。路线信息通过 o.from + ' → ' + o.to + ' | ' + o.eta 的字符串拼接展示,使用箭头符号"→"直观表达物流方向。

九、核心技术与设计模式总结

      .margin({ left: 12 })
      Text('✕').fontSize(18).fontColor(COLORS.textSecondary).onClick(() => { this.showSpell = false })
    }
    .width('100%')
    Row({ space: 8 }) 
.borderRadius(12)
.onClick(() => {
  this.onOrder(o)
})

}
}


---
在这里插入图片描述

从 UI 设计层面来看,本项目充分展示了 ArkTS 声明式 UI 的表达能力。纯组件实现的功能包括进度条(空容器宽度百分比+圆角)、柱状图(空容器高度百分比+渐变)、领奖台(不同高度柱体+底部对齐)、步进器(按钮+数值+按钮)、胶囊标签(padding+borderRadius)等,无需引入任何第三方图表库或 UI 组件库。渐变背景通过 `linearGradient` 属性实现,条件着色通过三元表达式内联实现,列表渲染通过 `ForEach` 指令实现,这些能力共同构成了 ArkTS 声明式 UI 的核心工具箱。

从代码复用层面来看,`@Builder` 构建器是本项目最重要的复用机制。三十余个构建器方法覆盖了 Tab 项、遮罩层、各种卡片、各种弹窗、区块标题等 UI 单元。每个标签页组件内部都定义了自己的卡片构建器,同类型的构建器在不同组件中可能有细微差异(如 `TaskTab` 和 `MineTab` 各自定义了 `taskCard`,内容略有不同),体现了组件封装的粒度控制——在功能差异较大时不强行抽象公共构建器,而是保持各组件的独立性。

从通信模式层面来看,本项目采用了纯粹的"props down, events up"单向数据流模式。父组件通过构造参数向子组件传递回调函数,子组件在交互时调用回调传入数据。这种模式虽然需要书写大量的回调函数声明和绑定代码,但保证了数据流的透明性和可预测性。在更复杂的场景中,可以考虑引入 `@Provide`/`@Consume` 跨层级数据传递或 `AppStorage` 全局状态管理来减少回调函数的层数传递。

综上所述,本项目作为一个 ArkTS 中型应用的技术标本,完整地展示了鸿蒙声明式 UI 开发的核心实践:接口驱动的类型安全、静态数据与过滤函数的数据层设计、`@State` 驱动的响应式状态管理、`@Builder` 实现的 UI 复用机制、`Column`/`Row`/`Scroll` 构成的布局体系、`ForEach` 驱动的列表渲染、条件渲染支持的弹窗架构,以及回调函数实现的父子组件通信。这些技术点共同构成了一个可运行、可维护、可扩展的鸿蒙原生应用的技术骨架。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐