在万物互联的时代浪潮中,鸿蒙操作系统以其独特的分布式架构与一次开发多端部署的理念,重新定义了跨设备应用开发的范式。ArkTS 作为鸿蒙生态的核心应用开发语言,在 TypeScript 的静态类型体系基础上融入了声明式 UI 编程范式,使开发者能够以更简洁、更安全、更高效的方式构建出富有表现力的用户界面。本文将以一个完整的梦境编织工坊应用为载体,从数据建模、状态管理、组件拆分、布局编排、弹窗交互到事件通信,逐段剖析 ArkTS 在真实业务场景下的工程实践。

鸿蒙开发背景远不止于一个新的移动操作系统。HarmonyOS 从底层内核到应用框架进行了全栈式重构,其应用开发框架 ArkUI 提供了一套基于声明式语法、状态驱动渲染的完整解决方案。与传统的命令式 UI 开发不同,声明式 UI 让开发者只需描述界面"应该是什么样子",而由框架自动完成"如何从当前状态过渡到目标状态"的渲染逻辑。这种理念的转变,极大地降低了复杂界面的维护成本,也使得状态与视图之间的一致性得到了根本保障。

ArkTS 语言特性集中体现在几个方面。首先是严格的类型系统,interface 接口定义、联合类型(Union Types,如 DreamOrder | null)、可选属性等 TypeScript 特性被完整保留,在编译期即可捕获大量潜在的类型错误。其次是装饰器(Decorator)体系的引入,@Component@Entry@State@Builder@Prop@Link 等装饰器各司其职,分别负责组件声明、入口标记、状态管理、UI 片段复用以及父子组件数据传递。再者是 ArkUI 声明式 DSL 的内嵌式集成,开发者可以在 build() 方法中以链式调用方式描述组件树结构、样式属性和事件绑定,形成一种既像 HTML 又超越 HTML 的结构化界面描述能力。

组件化开发思想是整个鸿蒙应用架构的基石。一个复杂的应用不应是一个庞大的单体结构,而应被拆解为若干职责单一、可复用、可组合的独立组件。每个组件拥有自己的状态空间、构建逻辑和内部方法,组件之间通过明确定义的接口(属性参数与回调函数)进行通信。这种"高内聚、低耦合"的设计原则,不仅让代码更易理解和测试,也让团队协作开发成为可能——不同开发者可以并行开发不同组件,只要接口契约不变即可。

在本应用中,我们可以清晰地看到组件化思想的落地实践。整个应用被拆分为主入口组件 Index 以及七个功能各异的标签页组件:HomeTab(首页)、WorkTab(梦工坊)、DreamTab(梦境画廊)、MaterialTab(素材仓)、ClientTab(客户档案)、RankTab(排行榜)、MineTab(个人中心)。主入口组件负责全局状态管理、标签页切换调度和弹窗挂载,而各标签页组件则专注于各自领域的内容渲染。此外,主入口还通过 @Builder 装饰器定义了十六个弹窗构建器和若干公共辅助组件,形成了一个层次分明、职责清晰的多层组件架构。

声明式 UI 的精髓在于:状态是唯一的真相来源(Single Source of Truth)。界面只是状态在某一时刻的投影,任何状态的变更都会自动触发相关界面的重渲染。开发者无需手动操作 DOM 或调用 setTextsetVisibility 等命令式 API,只需改变状态,框架便会高效地计算出最小变更集并应用到视图树。


一、数据模型层:接口定义与类型约束

1.1 DreamOrder 梦境订单接口

interface DreamOrder {
  id: number;
  name: string;
  icon: string;
  client: string;
  type: string;
  time: string;
  status: string;
  statusColor: string;
  desc: string;
  price: number;
}

在这里插入图片描述

这是整个应用中最核心的数据结构之一——梦境订单。interface 是 ArkTS(以及 TypeScript)中用于定义对象形状的语法。与 class 不同,interface 在编译后不会产生运行时代码,它纯粹是一种编译期的类型约束工具。这意味着它不会带来任何运行时开销,却能在开发阶段提供强大的类型安全保证。

在这个接口中,我们看到了十个字段的定义。id 字段类型为 number,用于唯一标识每一条订单记录,这在 ForEach 循环渲染时的键值生成中至关重要——ArkUI 的 ForEach 需要一个唯一的键值来追踪列表项的增删改,避免不必要的全量重建。nameicon 分别用文本字符串存储梦境名称和 Emoji 图标,这里巧妙地利用了 Emoji 作为图标方案,既避免了引入图片资源文件的体积开销,又保持了视觉上的丰富表现力。

client 存储客户名称,type 标识梦境系别(如治愈系、童话系、冒险系等),time 记录下单时间,statusstatusColor 则是成对出现的字段——状态文本与其对应的主题色。将颜色直接存储在数据模型中是一种实用的做法,它让渲染层无需维护一个状态到颜色的映射逻辑,只需直接读取数据字段即可。desc 是梦境的详细描述文本,price 则以数值类型存储订单金额。

在 ArkTS 中,interface 的每个字段必须显式声明类型,且不支持 TypeScript 的 ? 可选属性语法在所有场景下的使用。这种严格性虽然看似增加了编码负担,但实质上消除了"属性可能不存在"带来的运行时风险,让代码的行为更加可预测。

1.2 DreamProcess 梦境工序接口

interface DreamProcess {
  id: number;
  name: string;
  icon: string;
  step: number;
  time: string;
  done: boolean;
  desc: string;
}

梦境工序接口描述的是梦境编织过程中的每一道工序。与订单接口相比,它引入了两个值得关注的字段类型。step 字段类型为 number,用于标识工序在整条流水线中的序号,这个序号决定了工序在时间轴上的排列顺序。在 UI 渲染时,开发者通过 step 值来展示"第 N 道工序"的序号信息。

done 字段类型为 boolean,这是布尔类型在数据模型中的典型应用。布尔字段天然适合表达"已完成 / 未完成"这类二元状态。在工序时间轴的渲染逻辑中,done 的值直接决定了工序节点的视觉表现:已完成的工序显示实心圆点 和对勾 ,使用紫色 #7C4DFF 标识;未完成的工序则显示空心圆点 和预计时间,使用灰色 #B0BEC5 标识。这种"数据驱动样式"的模式是声明式 UI 的核心优势——一个布尔值的翻转就能自动引发视觉呈现的更新,无需手动编写样式切换逻辑。

1.3 DreamGallery 梦境画廊接口

interface DreamGallery {
  id: number;
  name: string;
  icon: string;
  style: string;
  like: number;
  time: string;
  desc: string;
  color: string;
}

梦境画廊接口用于展示已完成的优秀梦境作品。其中 like 字段以数值类型存储点赞数,在渲染时会与 "💗 " 前缀拼接显示为"💗 1280"的形式。color 字段存储的是十六进制颜色字符串,每个画廊作品都有自己独特的主题色,这使得每个画廊卡片在视觉上都能呈现出差异化的色彩个性。

值得注意的是,style 字段与 DreamOrder 中的 type 字段在语义上高度相似,都用来标识梦境的系别分类。这种设计体现了数据模型设计的一个原则:不同业务上下文中,即使是相似的概念也可以使用不同的字段名,以保持各数据结构在各自上下文中的自描述性。

1.4 Material 素材接口

interface Material {
  id: number;
  name: string;
  icon: string;
  type: string;
  stock: number;
  price: number;
  desc: string;
}

在这里插入图片描述

素材接口描述的是梦境编织所使用的原材料。这里出现了两个数值字段:stock(库存数量)和 price(单价价格)。在素材仓的 UI 渲染中,stock 的值不仅用于直接显示库存数字,还被用于条件判断——当库存低于 40 时,库存标签会显示为红色 #E53935 以发出预警;高于 40 时则显示为紫色 #7C4DFF 或绿色 #43A047,表示库存充足。

这种"数据阈值驱动视觉状态"的模式在实际业务中极为常见。开发者无需维护一个独立的状态变量来标记"库存告急",只需在渲染时直接对数据字段进行条件比较即可。声明式框架会在 stock 值发生变化时自动重新评估条件并更新对应的样式属性。

1.5 Client、RankItem、Skill、Achievement 等其余接口

interface Client {
  id: number;
  name: string;
  icon: string;
  level: string;
  orders: number;
  score: number;
  need: string;
  online: boolean;
}

在这里插入图片描述

客户接口中,online 布尔字段用于标记客户当前的在线状态。在客户卡片渲染中,在线客户头像旁会显示绿色实心圆点 ,离线客户则显示灰色空心圆点 。同时,在线状态的标签会使用绿色 #26A69A 背景显示"在线"文字,离线则使用灰色 #ECEFF1 背景显示"离线"文字。这种设计让用户能够一目了然地识别客户状态。

interface Skill {
  id: number;
  name: string;
  icon: string;
  level: number;
  maxLevel: number;
  progress: number;
  desc: string;
}

技能接口引入了 maxLevelprogress 两个字段。maxLevel 定义了技能的等级上限(在本应用中固定为 10),progress 则是一个 0 到 100 的数值,表示当前等级内的升级进度。在技能升级弹窗中,progress 的值被直接用作进度条的宽度百分比——width(this.selSkill!.progress + '%'),这种将数值直接映射为样式属性的写法是 ArkUI 声明式语法的简洁之处。

interface Achievement {
  id: number;
  name: string;
  icon: string;
  locked: boolean;
  desc: string;
  date: string;
}

interface Notice {
  id: number;
  title: string;
  icon: string;
  time: string;
  tag: string;
  content: string;
}

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

在这里插入图片描述

成就接口的 locked 布尔字段控制成就是否已解锁。在成就墙的渲染中,未解锁的成就会以 0.35 的透明度和灰色背景 #ECEFF1 显示,营造出"未点亮"的视觉暗示;已解锁的成就则以满透明度和金色背景 #FFF8E1 展现,配合 Emoji 图标的鲜明色彩,形成强烈的视觉对比。通知接口的 tag 字段用于分类标签(如"活动"“上新”"公告"等),在通知弹窗中以金色 #FFD54F 背景的徽章形式展示。快捷图标接口的 color 字段为每个图标预设了主题色。

数据模型设计是整个应用架构的第一道工序。好的接口定义应做到:字段命名自描述、类型选择恰当、不含冗余字段。在本应用中,所有接口都遵循了这一原则,每个字段都有明确的业务语义,为后续的渲染逻辑和交互逻辑奠定了坚实的基础。


二、静态数据层:常量定义与初始数据

2.1 DREAM_ORDERS 梦境订单数据

const DREAM_ORDERS: DreamOrder[] = [
  { id: 1, name: '星海之梦', icon: '🌌', client: '林小梦', type: '治愈系', time: '08-27 09:20', status: '编织中', statusColor: '#7C4DFF', desc: '在星海上漂浮,感受宇宙的温柔拥抱。', price: 388 },
  { id: 2, name: '糖果云朵', icon: '🍬', client: '苏糖糖', type: '童话系', time: '08-27 08:10', status: '设计中', statusColor: '#FF80AB', desc: '在棉花糖云朵上奔跑,天空下着糖果雨。', price: 268 },
  // ...
];

在这里插入图片描述

const 关键字声明了一个不可重新赋值的常量。这里声明的是一个 DreamOrder[] 类型的数组,即在类型标注中使用了方括号 [] 来表示数组类型。整个数组包含了十二条订单记录,每条记录都是一个符合 DreamOrder 接口形状的对象字面量。

const 声明的常量在编译期即被确定,在应用运行期间不会被重新赋值。这些数据充当了应用的"初始数据库",在没有后端服务支持的情况下,为界面渲染提供了全部所需的数据。在实际工程项目中,这些数据通常通过网络请求从远程服务器获取,但在本应用中采用了静态数据的方式,简化了网络层的设计复杂度,让读者能更专注于 UI 架构本身。

每条数据记录都严格遵循接口定义的字段顺序和类型。例如 status 字段的值为 '编织中''设计中''待接单''已完成' 这几种之一,虽然接口类型标注为 string 而非联合字面量类型(如 '编织中' | '设计中' | ...),但在实际使用中始终保持了取值的一致性。如果追求更严格的类型安全,可以将 status 改为联合字面量类型,这样任何拼写错误都能在编译期被捕获。

2.2 DREAM_PROCESS 工序数据

const DREAM_PROCESS: DreamProcess[] = [
  { id: 1, name: '倾听需求', icon: '👂', step: 1, time: '08-27 09:20', done: true, desc: '与客户沟通梦境细节与期望感受' },
  { id: 2, name: '主题构思', icon: '💡', step: 2, time: '08-27 10:05', done: true, desc: '确定梦境主题与核心场景元素' },
  // ...
  { id: 6, name: '感官注入', icon: '✨', step: 6, time: '08-27 19:00', done: false, desc: '注入温度、气味与风感等感官细节' },
  // ...
];

工序数据定义了十道完整的梦境编织流程。从"倾听需求"到"回访安抚",每一道工序都有明确的步骤序号、时间安排和完成状态。前五道工序的 done 值为 true,后五道为 false,这构成了"当前进度 50%"的数据基础。

工序数据中的 time 字段格式为"月-日 时:分",如 '08-27 09:20'。这种简化的时间格式虽然不包含年份信息,但在本应用的展示场景中已经足够。如果需要在完整的时间线分析中进行时间排序或时间差计算,则需要使用标准的 Date 对象或 ISO 时间字符串格式。

2.3 其余静态数据概览

const DREAM_GALLERY: DreamGallery[] = [ /* 10 条画廊作品 */ ];
const MATERIALS: Material[] = [ /* 12 种素材 */ ];
const CLIENTS: Client[] = [ /* 10 位客户 */ ];
const RANK_LIST: RankItem[] = [ /* 10 位排行 */ ];
const SKILLS: Skill[] = [ /* 8 项技能 */ ];
const ACHIEVEMENTS: Achievement[] = [ /* 8 项成就 */ ];
const NOTICES: Notice[] = [ /* 8 条通知 */ ];
const QUICK_ICONS: QuickIcon[] = [ /* 8 个快捷图标 */ ];

在这里插入图片描述

这些静态数据集合覆盖了应用的所有业务域。每个集合都使用 const 声明,并标注了明确的数组类型。数据的数量经过了精心设计:12 条订单、10 道工序、10 件画廊作品、12 种素材、10 位客户、10 位排行、8 项技能、8 项成就、8 条通知、8 个快捷图标。这些数字既保证了界面内容的丰富度,又不会因为数据量过大而影响渲染性能。

静态数据是应用开发的"脚手架"。在开发初期,使用真实风格的静态数据可以让开发者在没有后端的情况下完整地开发调试界面逻辑。当后端就绪后,只需将数据来源从常量切换为异步请求即可,组件层的渲染逻辑无需改动。这种数据层与渲染层的解耦,是工程化开发的重要实践。


三、辅助函数层:数据处理与工具方法

3.1 数组拆分函数

function getOrderRows(): DreamOrder[] {
  let rows: DreamOrder[] = [];
  for (let i = 0; i < DREAM_ORDERS.length; i += 2) {
    rows.push(DREAM_ORDERS[i]);
  }
  return rows;
}

function getOrderRows2(): DreamOrder[] {
  let rows: DreamOrder[] = [];
  for (let i = 1; i < DREAM_ORDERS.length; i += 2) {
    rows.push(DREAM_ORDERS[i]);
  }
  return rows;
}

在这里插入图片描述

这对函数实现了一个常见的需求——将一个一维数组拆分为两个子集。getOrderRows 从索引 0 开始,以步长 2 遍历原数组,提取所有偶数索引的元素;getOrderRows2 则从索引 1 开始,提取所有奇数索引的元素。两个函数的返回值合在一起,恰好覆盖原数组的全部元素。

这种拆分方式在双列瀑布流布局中被广泛使用。当需要将一个列表渲染为左右两列时,开发者将偶数索引的数据分给左列,奇数索引的数据分给右列。这种做法虽然简单,但存在一个潜在问题:两列的高度可能不均匀(如果数据项的渲染高度不一致),导致瀑布流效果不够理想。在更完善的实现中,可以使用动态高度计算或虚拟列表来优化。

函数内部使用了一个局部变量 rows 来累积结果,通过 for 循环和 push 方法逐个添加元素。在 ArkTS 中,for 循环的语法与标准 TypeScript 完全一致。值得注意的是 i += 2 的步进写法,它等价于 i = i + 2,但在代码简洁度上更优。

3.2 getTopOrders 函数

function getTopOrders(): DreamOrder[] {
  let rows: DreamOrder[] = [];
  for (let i = 0; i < 6 && i < DREAM_ORDERS.length; i++) {
    rows.push(DREAM_ORDERS[i]);
  }
  return rows;
}

这个函数从订单数组中提取前六条记录。循环条件 i < 6 && i < DREAM_ORDERS.length 使用了逻辑与运算符 &&,确保即使原数组不足六条元素时也不会越界访问。这种防御性编程实践在任何语言中都是值得提倡的——它防止了运行时的数组越界异常,保证了函数在边界条件下的健壮性。

返回前六条记录用于首页"待接新单"横滑卡片的展示。横滑列表通常只展示有限数量的卡片,用户可以通过左右滑动浏览更多。通过函数封装提取逻辑,而不是在组件中直接硬编码索引,使得数据数量可以灵活调整,只需修改函数中的 6 即可。

3.3 尺寸与百分比计算函数

function getBarHeight(val: number, max: number): string {
  let p = max > 0 ? (val / max) * 80 : 0;
  return p.toFixed(0) + 'vp';
}

function getPercent(val: number, total: number): string {
  let p = total > 0 ? (val / total) * 100 : 0;
  return p.toFixed(0) + '%';
}

在这里插入图片描述

这两个函数处理的是数值到样式字符串的转换。getBarHeight 接收一个值和最大值,计算出该值在 80vp 高度范围内的占比,返回带 vp 单位的字符串。vp(virtual pixel)是鸿蒙系统中的虚拟像素单位,它是一种密度无关的长度单位,类似于 Android 中的 dp 或 Web 中的 px 配合视口缩放。使用 vp 单位可以确保界面在不同屏幕密度的设备上保持一致的物理尺寸。

三元运算符 max > 0 ? ... : 0 防止了除零错误。当 max 为零时,直接返回 0 而不执行除法运算,这是一种重要的防御性编程技巧。toFixed(0) 方法将浮点数四舍五入为整数位数的字符串,确保返回值不带小数点。

getPercent 的逻辑类似,将一个值转换为占总数的百分比字符串。这些函数将数值计算逻辑从组件中提取出来,保持了组件 build() 方法的简洁性。

3.4 状态颜色映射函数

function getStatusColor(s: string): string {
  if (s === '编织中') {
    return '#7C4DFF';
  } else if (s === '设计中') {
    return '#FF80AB';
  } else if (s === '待接单') {
    return '#00BCD4';
  } else if (s === '已完成') {
    return '#90A4AE';
  }
  return '#FF8F00';
}

这个函数通过一系列 if-else if 条件分支,将订单状态文本映射为对应的十六进制颜色值。"编织中"使用梦幻紫 #7C4DFF,"设计中"使用樱粉 #FF80AB,"待接单"使用青蓝 #00BCD4,"已完成"使用灰蓝 #90A4AE,默认值使用琥珀金 #FF8F00

虽然这种基于字符串相等比较的映射方式在分支较少时足够实用,但当状态种类增多时,更优雅的做法是使用 Record<string, string> 对象进行映射查找,将时间复杂度从 O(n) 降为 O(1)。不过在本应用的状态种类有限的情况下,if-else 链的可读性反而更好。

值得注意的是,虽然 DreamOrder 接口中已经包含了 statusColor 字段,但这个函数的存在提供了一个独立于数据的颜色解析能力。当数据源中没有预存颜色时(例如从后端获取的数据只有 status 而没有 statusColor),此函数可以作为备选方案。

3.5 排名颜色映射与过滤函数

function getRankColor(r: number): string {
  if (r === 1) {
    return '#FFD54F';
  } else if (r === 2) {
    return '#B0BEC5';
  } else if (r === 3) {
    return '#FF8F00';
  }
  return '#7C4DFF';
}

function getWorkingOrders(): DreamOrder[] {
  let rows: DreamOrder[] = [];
  for (let i = 0; i < DREAM_ORDERS.length; i++) {
    if (DREAM_ORDERS[i].status === '编织中' || DREAM_ORDERS[i].status === '设计中') {
      rows.push(DREAM_ORDERS[i]);
    }
  }
  return rows;
}

getRankColor 将排名数字映射为奖牌色:第一名金色 #FFD54F,第二名银色 #B0BEC5,第三名铜色 #FF8F00,其余排名使用紫色 #7C4DFFgetWorkingOrders 则是一个过滤函数,它遍历所有订单,仅保留状态为"编织中"或"设计中"的记录。逻辑或运算符 || 表示两个条件满足其一即为真。

function getPendingCount(): number {
  let n = 0;
  for (let i = 0; i < DREAM_ORDERS.length; i++) {
    if (DREAM_ORDERS[i].status === '待接单') {
      n = n + 1;
    }
  }
  return n;
}

function getWorkingCount(): number {
  let n = 0;
  for (let i = 0; i < DREAM_ORDERS.length; i++) {
    if (DREAM_ORDERS[i].status === '编织中' || DREAM_ORDERS[i].status === '设计中') {
      n = n + 1;
    }
  }
  return n;
}

function getAchieveCount(): number {
  let n = 0;
  for (let i = 0; i < ACHIEVEMENTS.length; i++) {
    if (!ACHIEVEMENTS[i].locked) {
      n = n + 1;
    }
  }
  return n;
}

这三个计数函数分别统计"待接单"订单数、“编织中/设计中"订单数以及已解锁成就数。getAchieveCount 中的逻辑非运算符 ! 用于判断 locked 字段为 false(即已解锁)。这些函数的返回值直接用于界面上的数字展示,如"3 单可接”“4 单进行中”"4/8 已点亮"等。

函数是代码复用的最小单元。将数据处理逻辑封装为独立的函数,而不是散落在组件内部,是保持代码整洁的基本做法。在本应用中,所有的数据预处理逻辑都位于组件定义之外的全局函数中,组件只需调用函数获取结果,实现了数据逻辑与视图逻辑的关注点分离。


四、主入口组件 Index:状态管理与布局调度

4.1 装饰器与组件声明

@Entry
@Component
struct Index {
  @State tabIndex1: number = 0;
  @State showToast: boolean = false;
  @State toast: string = '';
  @State dreamScore: number = 9860;
  @State memberLevel: number = 3;
  @State dreamCount: number = 126;

@Entry 装饰器标记此组件为应用的入口页面组件。在一个 ArkTS 应用中,有且只有一个组件被标记为 @Entry,它对应着应用启动后加载的第一个页面。@Component 装饰器声明了一个自定义组件——struct Index 定义了一个名为 Index 的结构体组件。struct 关键字是 ArkTS 中定义组件的特殊语法,不同于 TypeScript 的 classstruct 在编译时会被转换为特定的组件工厂函数。

@State 装饰器是 ArkTS 状态管理体系中最基础的状态装饰器。被 @State 标注的成员变量成为组件的响应式状态——当其值发生变化时,ArkUI 框架会自动检测到变化,并重新执行 build() 方法中依赖于该状态的 UI 片段,实现界面的自动更新。这种"状态变更驱动视图刷新"的机制是声明式 UI 的核心。

tabIndex1 初始值为 0,代表当前激活的标签页索引。它的取值范围是 0 到 6,分别对应七个标签页。showToasttoast 共同管理着全局 Toast 提示框的显示状态和文本内容。dreamScore(工坊星币)、memberLevel(会员等级)和 dreamCount(已织梦境数)则是贯穿全局的用户数据状态。

4.2 弹窗状态与选中对象

  // 弹窗开关
  @State showOrder: boolean = false;
  @State showAccept: boolean = false;
  @State showIdea: boolean = false;
  @State showMaterial: boolean = false;
  @State showPeriod: boolean = false;
  @State showUrgent: boolean = false;
  @State showChat: boolean = false;
  @State showRate: boolean = false;
  @State showRank: boolean = false;
  @State showAchieve: boolean = false;
  @State showSkill: boolean = false;
  @State showShare: boolean = false;
  @State showNotice: boolean = false;
  @State showBuy: boolean = false;
  @State showClient: boolean = false;
  @State showSummary: boolean = false;

这里定义了十六个布尔类型的 @State 变量,每个对应一种弹窗的显示开关。当某个开关的值从 false 变为 true 时,对应的弹窗就会被渲染并显示在界面上。这种设计模式——一个布尔状态对应一个弹窗——虽然简单直观,但在弹窗数量较多时会导致状态变量数量的膨胀。

一种替代方案是使用一个联合类型状态来标识当前显示的弹窗类型,如 @State currentModal: 'order' | 'accept' | null = null,这样可以减少状态变量数量。但当前方案的优势在于:它允许同时显示多个弹窗(虽然在实际使用中通常不会这样做),且每个弹窗的开关逻辑完全独立,修改一个弹窗的显示逻辑不会影响其他弹窗。

  // 选中对象
  @State selOrder: DreamOrder | null = null;
  @State selIdea: DreamGallery | null = null;
  @State selMat: Material | null = null;
  @State selChat: Client | null = null;
  @State selRate: DreamOrder | null = null;
  @State selRank: RankItem | null = null;
  @State selAchieve: Achievement | null = null;
  @State selSkill: Skill | null = null;
  @State selShare: DreamGallery | null = null;
  @State selNotice: Notice | null = null;
  @State selBuy: Material | null = null;
  @State selClient: Client | null = null;
  @State selSummary: DreamOrder | null = null;

选中对象状态使用联合类型 Type | null 来标注。null 表示尚未选中任何对象,此时弹窗不应显示。在 ArkTS 中,联合类型 DreamOrder | null 表示该变量既可以持有一个 DreamOrder 实例,也可以为 null。这种类型标注在编译期强制开发者在访问该变量属性时进行空值检查。

在组件的 build() 方法中,弹窗的渲染条件被写成 if (this.showOrder && this.selOrder !== null),双重条件确保了只有当开关打开且选中对象不为空时才渲染弹窗。在弹窗内部访问选中对象属性时,使用了非空断言操作符 !,如 this.selOrder!.name,这告诉编译器"在此上下文中该变量一定不为 null"。

  // 表单字段
  @State fQty: number = 1;
  @State fDayIdx: number = 0;
  @State fTimeIdx: number = 0;
  @State fMsg: string = '';
  @State fStars: number = 5;
  @State fDreamName: string = '';
  @State fTypeIdx: number = 0;

表单字段状态用于弹窗内的交互表单。fQty 是购买数量,fDayIdxfTimeIdx 是工期选择中日期和时段的索引,fMsg 是聊天输入框的文本,fStars 是评价星级(默认五星),fTypeIdx 是梦境系别选择的索引。这些状态在弹窗打开时会被重置为初始值,确保每次打开弹窗时表单都是干净的。

4.3 标签配置与 Toast 方法

  private tabs1: string[] = ['首页', '梦工坊', '梦境', '素材'];
  private tabs2: string[] = ['客户', '排行', '我的'];
  private icons1: string[] = ['🏠', '🧵', '🌌', '🧺'];
  private icons2: string[] = ['💬', '🏆', '🧚'];

这两组数组定义了底部导航栏的标签文本和图标。private 关键字表示这些成员是组件私有的,不可以通过外部访问。底部导航栏被设计为两排——第一排四个标签(首页、梦工坊、梦境、素材),第二排三个标签(客户、排行、我的),合计七个标签页。这种两排底栏布局在移动应用中并不常见,但在功能较多时可以有效利用屏幕空间。

  showTip(msg: string): void {
    this.toast = msg;
    this.showToast = true;
    setTimeout(() => {
      this.showToast = false;
    }, 1800);
  }

showTip 是一个组件方法,用于显示全局 Toast 提示。它接收一条消息字符串,将其赋值给 toast 状态,并将 showToast 设为 true 以显示提示。随后使用 setTimeout 在 1800 毫秒后自动将 showToast 设为 false,使提示消失。

setTimeout 是 JavaScript/TypeScript 中标准的定时器 API,在 ArkTS 中同样可用。返回值类型标注为 void 表示此方法不返回值。这个方法是整个应用中用户反馈的核心通道——从接单成功、灵感保存到奖励领取,所有的操作反馈都通过此方法以 Toast 形式呈现给用户。

方法定义在 struct 内部时,参数和返回值的类型标注是强制的。ArkTS 不支持 TypeScript 的类型推断在方法签名上的省略——每个参数必须显式标注类型,返回值也必须标注。这种"零推断"的设计虽然增加了编码工作量,但消除了因类型推断失败而导致的运行时类型错误。

4.4 build 方法与 Stack 布局

  build() {
    Stack() {
      Column() {

build() 方法是每个组件必须实现的核心方法。它返回一个由 ArkUI 组件构成的声明式 UI 树。在此方法内,开发者使用链式调用方式描述组件的嵌套结构、样式属性和事件绑定。

最外层使用的是 Stack 组件。Stack 是一种堆叠布局容器,它将其子组件按顺序从下到上堆叠——后声明的子组件会覆盖在前面的子组件之上。在本应用中,Stack 被用作弹窗层的容器:主内容区作为第一个子组件铺满整个屏幕,而各个弹窗则作为后续子组件叠加在主内容之上,实现"弹窗覆盖在内容上方"的视觉效果。

Stack 组件常用于需要叠加层级的场景,如悬浮按钮、遮罩层、弹窗等。与 ColumnRow 不同,Stack 不会对其子组件进行线性排列,而是允许它们占据同一空间。开发者可以通过 alignContent 属性控制子组件在 Stack 内的对齐方式。

Column 组件是垂直线性布局容器,它将子组件从上到下依次排列。这里 Column 作为主内容容器,包含了上方的标签页内容区和下方的底栏导航区。

Stack 根容器

Column 主内容

弹窗层1 modalOverlay

弹窗层2 具体弹窗内容

Toast 层

标签页内容区

底部导航栏

第一排 Row: 首页/梦工坊/梦境/素材

第二排 Row: 客户/排行/我的

4.5 标签页内容区的条件渲染

        if (this.tabIndex1 === 0) {
          HomeTab({
            dreamScore: this.dreamScore,
            dreamCount: this.dreamCount,
            onOrder: (o: DreamOrder) => {
              this.selOrder = o;
              this.showOrder = true;
            },
            onAccept: (o: DreamOrder) => {
              this.selOrder = o;
              this.showAccept = true;
            },
            onIdea: (g: DreamGallery) => {
              this.selIdea = g;
              this.showIdea = true;
            },
            onNotice: (n: Notice) => {
              this.selNotice = n;
              this.showNotice = true;
            }
          })
        }

if-else if 条件分支在 build() 方法中实现了标签页的切换渲染。当 tabIndex1 的值为 0 时,渲染 HomeTab 组件;值为 1 时渲染 WorkTab,以此类推。这种基于条件分支的标签页切换方式,只有在当前标签索引变化时才渲染对应的标签页组件,其他标签页不会被实例化,节省了内存和渲染开销。

HomeTab 组件通过参数传递接收数据。dreamScoredreamCount 是从父组件传递到子组件的数值型数据,这种从父到子的数据流向是 ArkTS 组件通信的基本模式。在 HomeTab 的定义中,这些参数被声明为普通成员变量(非 @State),子组件通过读取这些变量来获取父组件传入的数据。

更值得关注的是四个回调函数参数:onOrderonAcceptonIdeaonNotice。这些参数的类型是箭头函数类型(如 (o: DreamOrder) => void),它们定义了子组件向父组件通信的接口。当子组件内部发生特定交互(如用户点击了某个订单卡片)时,子组件调用对应的回调函数,将交互数据传递回父组件。父组件在回调中更新自己的状态(如 this.selOrder = othis.showOrder = true),从而触发弹窗的显示。

这种"父传数据向下,子传事件向上"的模式是 ArkTS 组件通信的标准范式。父组件通过属性参数向子组件传递数据和回调,子组件通过调用回调向父组件传递事件和数据。这种设计保证了数据流的单向性,使得状态变更的来源可追踪、可调试。

子组件 HomeTab

父组件 Index

触发更新

用户点击订单

@State selOrder

@State showOrder

onOrder 回调参数

onOrder(o)

弹窗 orderModal 渲染

4.6 底部两排导航栏

        Column() {
          Row() {
            ForEach(this.tabs1, (label: string, idx: number) => {
              this.bottomTabItem(this.icons1[idx], label, idx)
            }, (label: string, idx: number) => label + idx)
          }
          .width('100%')
          .height(46)
          Row() {
            ForEach(this.tabs2, (label: string, idx: number) => {
              this.bottomTabItem(this.icons2[idx], label, idx + 4)
            }, (label: string, idx: number) => label + idx)
          }
          .width('100%')
          .height(46)
        }
        .width('100%')
        .height(92)
        .backgroundColor('#FFFFFF')
        .shadow({ radius: 8, color: '#1A000000', offsetY: -2 })

底部导航栏由一个 Column 包裹两个 Row 组成。每个 Row 的宽度为 100%(width('100%')),高度为 46vp,两排合计 92vp。ColumnbackgroundColor 设置为纯白 #FFFFFF,并通过 shadow 属性添加了向上的阴影效果——offsetY: -2 表示阴影向 Y 轴负方向偏移 2vp,即向上偏移,配合半透明黑色 #1A0000001A 为十六进制透明度,约 10% 不透明度)和半径 8vp 的模糊,营造了底栏浮于内容之上的层次感。

ForEach 是 ArkUI 中用于列表渲染的核心组件函数。它的第一个参数是待遍历的数组,第二个参数是渲染每一项的函数(接收元素和索引两个参数),第三个参数是键值生成函数(用于框架追踪列表项的唯一性)。这里第一排使用 this.tabs1 数组(4 个标签),第二排使用 this.tabs2 数组(3 个标签)。

注意第二排的索引使用了 idx + 4,即第二排的第一个标签的索引为 4,而非 0。这是因为 tabIndex1 的取值范围是 0 到 6,第二排的标签对应索引 4、5、6。bottomTabItem 是一个 @Builder 方法,它接收图标、标签文本和索引三个参数,返回一个标签项的 UI 片段。

键值生成函数 (label: string, idx: number) => label + idx 将标签文本与索引拼接为唯一键值。虽然在当前场景中标签文本本身就足够唯一,但加上索引可以进一步保证键值的绝对唯一性,避免因数据重复导致的渲染异常。

4.7 弹窗挂载逻辑

      // ========== 弹窗挂载 ==========
      if (this.showOrder && this.selOrder !== null) {
        this.modalOverlay(() => { this.showOrder = false })
        this.orderModal()
      }

弹窗的挂载逻辑在 Stack 的第二层。每个弹窗的渲染条件都是"开关为 true 且选中对象不为 null"。当条件满足时,先调用 this.modalOverlay() 渲染半透明遮罩层,再调用具体的弹窗构建方法(如 this.orderModal())渲染弹窗内容。

modalOverlay 接收一个关闭回调函数 () => { this.showOrder = false },当用户点击遮罩层时,此回调被执行,弹窗关闭。这种通过 @Builder 方法接收回调参数的设计,实现了遮罩层关闭逻辑的复用——所有十六个弹窗共用同一个遮罩层构建逻辑,只是关闭回调不同。

Stack 布局中,后声明的子组件覆盖在先声明的子组件之上。因此遮罩层和弹窗内容会叠加在主内容区之上,实现了弹窗的视觉效果。遮罩层的半透明黑色背景 #6600000066 约 40% 不透明度)部分遮挡了底层内容,引导用户关注弹窗。

      if (this.showToast) {
        this.toastBox()
      }
    }
    .width('100%')
    .height('100%')
  }

Toast 提示框的渲染条件是 this.showToast。它被放在 Stack 的最顶层,确保提示框覆盖在所有内容(包括弹窗)之上。Stackwidthheight 均设置为 100%,使其铺满整个屏幕。

@Builder 方法是 ArkTS 中实现 UI 片段复用的核心机制。与普通方法不同,@Builder 方法内部使用的是 ArkUI 声明式 DSL 语法,返回的是一个 UI 组件树片段而非普通返回值。通过将重复的 UI 结构提取为 @Builder 方法,可以显著减少代码重复,提高可维护性。


五、弹窗构建器:十六种交互弹窗的精细剖析

5.1 orderModal 梦境订单详情弹窗

  @Builder
  orderModal() {
    Column() {
      Row() {
        Text(this.selOrder!.icon).fontSize(40)
        Column({ space: 4 }) {
          Text(this.selOrder!.name).fontSize(19).fontWeight(FontWeight.Bold).fontColor('#2A0A4A')
          Text('客户 ' + this.selOrder!.client + ' · ' + this.selOrder!.type).fontSize(12).fontColor('#90A4AE')
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        .margin({ left: 12 })

@Builder 装饰器标注 orderModal 为一个 UI 构建器方法。在此方法内部,使用 Column 作为弹窗的根容器。弹窗的第一行使用 Row 水平排列:左侧是 Emoji 图标(fontSize(40)),右侧是包含梦境名称和客户信息的 Column

Column({ space: 4 }) 的构造参数 space 设置了子组件之间的间距为 4vp。这是 ColumnRow 组件提供的便捷属性——无需为每个子组件单独设置 margin,即可实现统一的间距控制。alignItems(HorizontalAlign.Start)Column 内的子组件左对齐。layoutWeight(1) 分配了剩余空间权重,使此 Column 占据 Row 中的主要空间。margin({ left: 12 }) 在左侧添加 12vp 的间距,使图标和文字之间保持视觉呼吸感。

        Row() {
          Text(this.selOrder!.status)
            .fontSize(12)
            .fontColor('#FFFFFF')
            .backgroundColor(this.selOrder!.statusColor)
            .borderRadius(12)
            .padding({ left: 10, right: 10, top: 4, bottom: 4 })
        }
      }
      .width('100%')

Row 的右侧是状态标签,使用白色字体配主题色背景,圆角 12vp,四周内边距均匀分布。状态标签的颜色直接从数据模型的 statusColor 字段获取,无需额外映射。

      Column({ space: 10 }) {
        this.detailLine('🌌', '梦境系别', this.selOrder!.type)
        this.detailLine('🕐', '下单时间', this.selOrder!.time)
        this.detailLine('📝', '梦境描述', this.selOrder!.desc)
        this.detailLine('💰', '订单金额', '¥ ' + this.selOrder!.price)
      }
      .width('100%')
      .backgroundColor('#F7F3FF')
      .borderRadius(14)
      .padding(14)
      .margin({ top: 14 })

详情区域使用了 detailLine 辅助构建器,连续调用四次,分别展示系别、时间、描述和金额。整个详情区域包裹在浅紫色背景 #F7F3FF 中,圆角 14vp,内边距 14vp,顶部外边距 14vp。backgroundColorborderRadiuspaddingmargin 这些样式属性以链式调用的方式附加在组件上,这是 ArkUI 声明式语法的核心特征。

      Row({ space: 10 }) {
        Button() {
          Text('开始编织').fontSize(14).fontColor('#FFFFFF')
        }
        .backgroundColor('#7C4DFF')
        .borderRadius(20)
        .height(40)
        .layoutWeight(1)
        .onClick(() => {
          this.showOrder = false;
          this.tabIndex1 = 1;
        })

按钮区域包含两个按钮。"开始编织"按钮使用 Button 组件,内部嵌套 Text 设置文字。onClick 事件处理器在点击时关闭订单弹窗(this.showOrder = false)并切换到梦工坊标签页(this.tabIndex1 = 1),实现了从"查看订单"到"开始编织"的业务流程跳转。

Button 组件可以接受一个子组件作为其内容,这里嵌入了 Text 组件来设置按钮文字。这种"容器+内容"的组合模式在 ArkUI 中非常普遍——许多组件都可以通过嵌套子组件来定制其内部内容。

5.2 acceptModal 接单确认弹窗

  @Builder
  acceptModal() {
    Column() {
      Row() {
        Text('接单确认').fontSize(19).fontWeight(FontWeight.Bold).fontColor('#2A0A4A')
        Text('✕')
          .fontSize(16)
          .fontColor('#90A4AE')
          .padding(6)
          .onClick(() => { this.showAccept = false })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

接单确认弹窗采用底部抽屉样式。标题行使用 justifyContent(FlexAlign.SpaceBetween) 让标题和关闭按钮分别靠左和靠右排列。FlexAlign.SpaceBetween 是弹性布局的对齐模式之一,它将子组件之间的间距均匀分配,首尾子组件紧贴容器两端。

      Row({ space: 8 }) {
        ForEach(['治愈系', '童话系', '沉浸系', '冒险系', '梦幻系'], (t: string, ti: number) => {
          Text(t)
            .fontSize(12)
            .fontColor(this.fTypeIdx === ti ? '#FFFFFF' : '#2A0A4A')
            .backgroundColor(this.fTypeIdx === ti ? '#7C4DFF' : '#F0EBFA')
            .borderRadius(14)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .onClick(() => { this.fTypeIdx = ti })
        }, (t: string) => t)
      }

梦境系别选择使用了 ForEach 渲染五个可选项。每个选项的字体颜色和背景色通过三元条件表达式动态决定:当选中时(this.fTypeIdx === ti)显示白字紫底,未选中时显示紫字浅紫底。onClick 事件将选中索引赋值给 fTypeIdx 状态变量,由于 fTypeIdx@State 标注的,其值变化会自动触发五个选项的重新渲染,实现选中态的切换。

      Button() {
        Text('确认接单').fontSize(15).fontColor('#FFFFFF')
      }
      .width('100%')
      .height(44)
      .backgroundColor('linearGradient({ colors: [["#7C4DFF", 0], ["#B388FF", 1]] })')
      .borderRadius(22)
      .margin({ top: 14 })
      .scale({ x: 1, y: 1 })
      .animation({ duration: 500, curve: Curve.EaseOut })
      .onClick(() => {
        this.showAccept = false;
        this.showTip('接单成功,开始编织');
      })

"确认接单"按钮使用了线性渐变背景 linearGradient。渐变从 #7C4DFF#B388FF,从上到下过渡。scaleanimation 属性组合实现了按钮的动画效果——当按钮状态变化时,以 500 毫秒的 EaseOut 曲线进行缩放过渡。Curve.EaseOut 是一种先快后慢的缓动曲线,适合于"进入"类动画。

点击事件关闭弹窗并显示成功 Toast。showTip 方法的调用触发了 toastshowToast 状态的更新,由于 Toast 渲染在 Stack 的最顶层,提示框会覆盖在所有内容之上显示。

5.3 ideaModal 设计灵感弹窗

  @Builder
  ideaModal() {
    Column() {
      Text('✨ 设计灵感').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
      Text(this.selIdea!.style + ' · 灵感来源').fontSize(12).fontColor('#D1C4E9').margin({ top: 4 })

      Row() {
        Text(this.selIdea!.icon).fontSize(52)
      }
      .width(120)
      .height(110)
      .justifyContent(FlexAlign.Center)
      .backgroundColor('#1AFFFFFF')
      .borderRadius(16)
      .margin({ top: 14 })
      .opacity(1)
      .animation({ duration: 800, curve: Curve.EaseInOut })

设计灵感弹窗采用深紫色渐变背景,营造星空般的视觉氛围。图标展示区域使用了半透明白色背景 #1AFFFFFF1A 约 10% 不透明度的白色),在深色背景上呈现出一种"磨砂玻璃"效果。animation 属性配置了 800 毫秒的 EaseInOut 动画,这种"先慢后快再慢"的缓动曲线适合于循环类或过渡类动画。

    .width('84%')
    .backgroundColor('linearGradient({ colors: [["#1A0B2E", 0], ["#4A148C", 1]] })')
    .borderRadius(20)
    .padding(20)
    .constraintSize({ maxHeight: '84%' })
    .clip(true)

弹窗的宽度为屏幕宽度的 84%,居中显示。constraintSize({ maxHeight: '84%' }) 限制了弹窗的最大高度为屏幕的 84%,防止内容过多时超出屏幕。clip(true) 启用裁剪,确保弹窗内容不会超出圆角边界——这在圆角容器中非常重要,没有 clip 的话,内部内容的直角会"溢出"容器的圆角。

5.4 periodModal 工期确认弹窗

  @Builder
  periodModal() {
    Column() {
      Column() {
        Row() {
          Text(this.selOrder!.icon).fontSize(34)
          Column({ space: 3 }) {
            Text(this.selOrder!.name).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text('工期协商中').fontSize(12).fontColor('#D1C4E9')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 12 })
        }
        .width('100%')
      }
      .width('100%')
      .backgroundColor('linearGradient({ colors: [["#7C4DFF", 0], ["#2A0A4A", 1]] })')
      .padding(18)

工期确认弹窗采用上下拼接的结构——上半部分是紫色渐变头部,下半部分是白色内容区。这种"卡片头+卡片身"的设计模式在移动端 UI 中非常常见。头部区域展示了梦境图标、名称和"工期协商中"的状态提示,使用白色字体配渐变紫色背景。

        Row({ space: 8 }) {
          ForEach(['3 天后', '5 天后', '7 天后', '10 天后'], (d: string, di: number) => {
            Column({ space: 2 }) {
              Text(d).fontSize(12).fontColor(this.fDayIdx === di ? '#FFFFFF' : '#2A0A4A')
              Text(['08-30', '09-01', '09-03', '09-06'][di]).fontSize(10).fontColor(this.fDayIdx === di ? '#EDE7F6' : '#90A4AE')
            }
            .width(64)
            .height(50)
            .justifyContent(FlexAlign.Center)
            .backgroundColor(this.fDayIdx === di ? '#7C4DFF' : '#F0EBFA')
            .borderRadius(10)
            .onClick(() => { this.fDayIdx = di })
          }, (d: string) => d)
        }

交付日期选择使用四列卡片式选择器。每个卡片显示天数和具体日期,选中时使用紫色背景白字,未选中时使用浅紫底深紫字。注意这里使用了一个内联数组 ['08-30', '09-01', '09-03', '09-06'] 配合索引 di 来获取对应日期——这是一种简洁但耦合度较高的写法,因为日期数组与标签数组必须保持长度一致和顺序对应。

periodModal 弹窗结构

Column 根容器

Column 紫色头部

Column 白色内容区

图标+名称+状态

日期选择卡片组

时段选择标签组

工期提示+价格

确认按钮

5.5 urgentModal 加急确认弹窗

  @Builder
  urgentModal() {
    Column() {
      Text('⚡').fontSize(36)
      Text('加急编织').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 6 })
      Text('「' + this.selOrder!.name + '」提前至明晨交付').fontSize(12).fontColor('#FFD8E4').margin({ top: 4 })

加急确认弹窗使用粉红色渐变背景 linearGradient({ colors: [["#AD1457", 0], ["#E91E63", 1]] }),传达紧迫感。弹窗以一个大号闪电图标 开头,配以"加急编织"标题和加急说明文字。#FFD8E4 是浅粉色,在深粉背景上形成了柔和的辅助文字效果。

      Row() {
        Text('加急费用').fontSize(14).fontColor('#FFFFFF').layoutWeight(1)
        Text('¥ ' + (this.selOrder!.price * 0.3))
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFD54F')
      }

加急费用通过 this.selOrder!.price * 0.3 计算得出——原价的 30% 作为加急费。这个表达式直接在 UI 描述中执行运算,ArkTS 支持在 build() 方法和 @Builder 方法中书写任意合法的 TypeScript 表达式。计算结果显示为金色 #FFD54F、22 号字体的醒目金额。

5.6 chatModal 客户沟通弹窗

  @Builder
  chatModal() {
    Column() {
      Row() {
        Text(this.selChat!.icon).fontSize(30)
        Column({ space: 2 }) {
          Text(this.selChat!.name).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#2A0A4A')
          Text(this.selChat!.level + ' · ' + this.selChat!.need).fontSize(11).fontColor('#90A4AE')
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        .margin({ left: 10 })
        Text(this.selChat!.online ? '在线' : '离线')
          .fontSize(11)

客户沟通弹窗模拟了一个即时通讯界面。头部展示了客户头像、名称、等级和在线状态。在线状态的文字和背景色通过三元表达式动态选择:在线时显示绿色 #26A69A 背景的白字"在线",离线时显示灰色 #ECEFF1 背景的灰字"离线"。

      Column({ space: 8 }) {
        Row() {
          Text('客户:「' + this.selChat!.need + '」希望加入萤火虫元素').fontSize(12).fontColor('#37474F')
        }
        .width('88%')
        .backgroundColor('#F0EBFA')
        .borderRadius(12)
        .padding(10)
        Row() {
          Text('您:收到,会在感官注入阶段加入').fontSize(12).fontColor('#37474F')
        }
        .width('88%')
        .margin({ left: '12%' })
        .backgroundColor('#E8F5E9')
        .borderRadius(12)
        .padding(10)

消息区域模拟了聊天气泡。客户消息使用浅紫色背景 #F0EBFA 靠左对齐,自己的回复使用浅绿色背景 #E8F5E9 并通过 margin({ left: '12%' }) 向右偏移,模拟了"对方消息在左,自己消息在右"的聊天布局。注意 margin 的值支持百分比字符串,left: '12%' 表示左边距为容器宽度的 12%。

      TextInput({ placeholder: '输入消息...', text: this.fMsg })
        .fontSize(13)
        .height(40)
        .backgroundColor('#F5F3FB')
        .borderRadius(10)
        .margin({ top: 10 })
        .onChange((v: string) => {
          this.fMsg = v;
        })

TextInput 是 ArkUI 的文本输入组件。构造参数接收一个对象,placeholder 设置占位提示文字,text 设置当前文本值。onChange 事件在输入内容变化时触发,回调参数 v 是当前输入框的最新文本值。在回调中,将输入值赋给 fMsg 状态变量,实现了输入内容的响应式绑定。由于 fMsg@State 标注的,如果其他地方也引用了此变量,它们也会同步更新。

5.7 rateModal 满意度评价弹窗

      Row({ space: 8 }) {
        ForEach([1, 2, 3, 4, 5], (s: number) => {
          Text(s <= this.fStars ? '⭐' : '☆')
            .fontSize(32)
            .scale({ x: 1, y: 1 })
            .animation({ duration: 400, curve: Curve.EaseOut })
            .onClick(() => {
              this.fStars = s;
            })
        }, (s: number) => 'st' + s)
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)

星级评价组件使用 ForEach 渲染五个星星。每个星星根据 s <= this.fStars 的条件决定显示实心星 还是空心星 。点击星星时,将对应的数值 s 赋给 fStars 状态变量,触发五个星星的重新渲染,实现了"点击第 N 颗星,前 N 颗星变为实心"的经典评分交互。

justifyContent(FlexAlign.Center) 让五颗星水平居中排列。ForEach 的第三个参数键值生成函数使用 'st' + s 来确保每个星星的唯一标识,前缀 st 避免了与其他 ForEach 列表项键值的冲突。

5.8 rankModal 工坊排行奖励弹窗

  @Builder
  rankModal() {
    Column() {
      Row() {
        Text('🏆').fontSize(40)
        Column({ space: 3 }) {
          Text(this.selRank!.title).fontSize(18).fontWeight(FontWeight.Bold).fontColor('#5D4037')
          Text('第 ' + this.selRank!.rank + ' 名 · ' + this.selRank!.name).fontSize(12).fontColor('#8D6E63')
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        .margin({ left: 12 })
      }
      .width('100%')
      .backgroundColor('linearGradient({ colors: [["#FFE082", 0], ["#FFCA28", 1]] })')
      .borderRadius(16)
      .padding(14)

排行奖励弹窗采用金色系设计,头部区域使用金色渐变背景 #FFE082#FFCA28,文字使用棕色系 #5D4037#8D6E63,营造出"奖杯"般的金质质感。

      Row() {
        Row() {
          Column() {
          }
          .height(8)
          .backgroundColor('linearGradient({ colors: [["#FFB300", 0], ["#FF8F00", 1]] })')
          .borderRadius(4)
        }
        .width('100%')
        .height(8)
        .backgroundColor('#FFF3E0')
        .borderRadius(4)
      }

这里实现了一个进度条效果。外层 Row 作为进度条容器,背景色为浅橙 #FFF3E0;内层 Row 包含一个空 Column(高度为 8vp,渐变背景色从 #FFB300#FF8F00),代表进度填充。虽然在此段代码中内层 Row 的宽度被设为 100%(即满进度),但通过修改内层宽度百分比,即可实现动态进度条效果。这种"容器+填充"的双层结构是 ArkUI 中实现进度条的经典模式。

5.9 achieveModal 成就解锁弹窗

      Text(this.selAchieve!.locked ? '🔒' : '🏅').fontSize(44)
      Text(this.selAchieve!.name).fontSize(19).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 8 })
      Text(this.selAchieve!.desc).fontSize(13).fontColor('#D1C4E9').margin({ top: 6 })
      Text(this.selAchieve!.locked ? '尚未达成' : '达成于 ' + this.selAchieve!.date)
        .fontSize(12)
        .fontColor(this.selAchieve!.locked ? '#FFCDD2' : '#FFD54F')

成就弹窗使用深紫到亮紫的渐变背景。根据 locked 布尔值,图标在锁 🔒 和奖牌 🏅 之间切换,日期文字在"尚未达成"和具体日期之间切换,字体颜色在浅红 #FFCDD2 和金色 #FFD54F 之间切换。这种"一个布尔值驱动多个视觉元素"的模式展示了声明式 UI 的强大表现力——数据状态与视觉呈现之间的映射关系被简洁而清晰地表达。

5.10 skillModal 技能升级弹窗

      Row() {
        Row() {
          Column() {
          }
          .width(this.selSkill!.progress + '%')
          .height(9)
          .backgroundColor('linearGradient({ colors: [["#7C4DFF", 0], ["#B388FF", 1]] })')
          .borderRadius(5)
        }
        .width('100%')
        .height(9)
        .backgroundColor('#F0EBFA')
        .borderRadius(5)
      }

技能升级弹窗的进度条使用了与排行奖励弹窗相同的"容器+填充"模式,但这里内层 Column 的宽度被设为 this.selSkill!.progress + '%'——即进度值直接映射为宽度百分比。当 progress 为 62 时,进度条填充宽度为 62%。这种将数据值直接拼接为样式字符串的写法虽然简洁,但需要注意字符串拼接的正确性——progress 必须是数值类型,62 + '%' 的结果才是 "62%"

技能升级弹窗数据流

selSkill.progress = 62

width: '62%'

进度条填充 62%

selSkill.level = 6

显示 'Lv.6/10'

点击升级按钮

dreamScore -= 500

关闭弹窗

显示Toast提示

5.11 shareModal 梦境分享弹窗

      Text(this.selShare!.name)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#2A0A4A')
        .letterSpacing(4)

分享弹窗设计为"梦境入场券"票根样式。letterSpacing(4) 属性设置了字间距为 4vp,这种宽字距排版在票据、标题等场景中常见,赋予了文字一种"印刷感"和"仪式感"。

      Divider()
        .strokeWidth(1)
        .color('#E0D5F5')
        .margin({ top: 14, bottom: 14 })

Divider 是 ArkUI 的分割线组件,strokeWidth 设置线宽,color 设置颜色。在这里用作票根的"虚切线"效果,配合上下 14vp 的间距,形成了票据撕口的视觉暗示。

5.12 buyModal 素材购买弹窗

        Text('-')
          .fontSize(20)
          .fontColor('#FF8F00')
          .width(32)
          .height(32)
          .textAlign(TextAlign.Center)
          .backgroundColor('#FFF3E0')
          .borderRadius(16)
          .onClick(() => {
            if (this.fQty > 1) {
              this.fQty = this.fQty - 1;
            }
          })
        Text(this.fQty + '').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#2A0A4A').width(48).textAlign(TextAlign.Center)
        Text('+')
          .fontSize(20)
          .fontColor('#FF8F00')
          .width(32)
          .height(32)
          .textAlign(TextAlign.Center)
          .backgroundColor('#FFF3E0')
          .borderRadius(16)
          .onClick(() => {
            this.fQty = this.fQty + 1;
          })

数量选择器使用"-"和"+"两个文本按钮,中间显示当前数量。减号按钮的点击事件中有一个保护条件 if (this.fQty > 1),确保数量不会减到零以下。加号按钮则无条件递增。每次点击都会更新 fQty 状态,触发数量显示和合计金额的自动更新。

textAlign(TextAlign.Center) 让文本在指定宽度的容器中水平居中。注意这里的减号使用了全角字符 而非半角 -,这在视觉上更宽更平衡。

5.13 summaryModal 收工总结弹窗

      Row({ space: 8 }) {
        ForEach([42, 68, 55, 80, 62, 90], (v: number, vi: number) => {
          Column({ space: 4 }) {
            Row() {
              Column() {
              }
              .width(16)
              .height(getBarHeight(v, 90))
              .backgroundColor(vi === 5 ? '#FF80AB' : '#B388FF')
              .borderRadius({ topLeft: 4, topRight: 4 })
            }
            .width('100%')
            .justifyContent(FlexAlign.Center)
            .height(80)
            .backgroundColor('#F7F3FF')
            .borderRadius(8)
            Text(['周一', '周二', '周三', '周四', '周五', '今日'][vi]).fontSize(9).fontColor('#90A4AE')
          }
          .layoutWeight(1)
        }, (v: number, vi: number) => 'sv' + vi)
      }

收工总结弹窗内嵌了一个柱状图组件。ForEach 遍历六个数值(代表六天的编织数据),每根柱子是一个 Column 内部的空 Column,其高度由 getBarHeight(v, 90) 函数计算得出。vi === 5 条件让第六根柱子(“今日”)使用樱粉色 #FF80AB,其余使用浅紫色 #B388FF,突出当前日的数据。柱子顶部圆角 borderRadius({ topLeft: 4, topRight: 4 }) 只设置上方两角圆角,模拟了柱状图的标准视觉。

layoutWeight(1) 让六根柱子等分容器宽度。这是 Flex 布局权重的典型应用——每个子组件的权重相同,因此每个占据容器宽度的六分之一。

弹窗是移动端应用中信息展示和用户交互的重要载体。在本应用中,十六种弹窗覆盖了订单查看、接单确认、灵感展示、素材选择、工期协商、加急处理、客户沟通、满意度评价、排行奖励、成就展示、技能升级、梦境分享、公告通知、素材购买、客户档案和收工总结等全部核心业务场景,形成了一个完整的弹窗交互体系。

5.14 公共辅助构建器

  @Builder
  detailLine(label: string, name: string, value: string) {
    Row() {
      Text(label).fontSize(15)
      Text(name).fontSize(13).fontColor('#90A4AE').width(76).margin({ left: 6 })
      Text(value).fontSize(13).fontColor('#37474F').layoutWeight(1)
    }
    .width('100%')
  }

detailLine 是一个带参数的 @Builder 方法,接收三个字符串参数:图标、标签名和值。它在弹窗中被多次调用来构建信息行,如"梦境系别""下单时间"等。通过将重复的 UI 结构提取为参数化构建器,避免了大量代码重复。固定宽度 width(76) 让标签名对齐,layoutWeight(1) 让值占据剩余空间。

  @Builder
  modalOverlay(onClose: () => void) {
    Column() {
      Row() {
        Column() {
        }
        .width('100%')
        .height('100%')
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#66000000')
      .onClick(() => {
        onClose();
      })
    }
    .width('100%')
    .height('100%')
  }

modalOverlay 接收一个 onClose 回调函数参数,构建了一个全屏半透明遮罩。点击遮罩时调用 onClose 关闭弹窗。这种将关闭逻辑通过参数传入的方式,使得同一个遮罩构建器可以被所有弹窗复用,每个弹窗只需传入自己的关闭逻辑即可。

  @Builder
  toastBox() {
    Row() {
      Text(this.toast).fontSize(14).fontColor('#FFFFFF')
    }
    .padding({ left: 18, right: 18, top: 10, bottom: 10 })
    .backgroundColor('#CC333333')
    .borderRadius(20)
  }

toastBox 构建 Toast 提示框,使用半透明深色背景 #CC333333CC 约 80% 不透明度的深灰),圆角 20vp,内部白色文字。Row 作为容器确保 Toast 宽度自适应内容长度。

  @Builder
  bottomTabItem(icon: string, label: string, idx: number) {
    Column({ space: 2 }) {
      Text(icon).fontSize(19)
      Text(label)
        .fontSize(10)
        .fontColor(this.tabIndex1 === idx ? '#7C4DFF' : '#90A4AE')
        .fontWeight(this.tabIndex1 === idx ? FontWeight.Bold : FontWeight.Normal)
    }
    .layoutWeight(1)
    .height(46)
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.tabIndex1 = idx;
    })
  }

bottomTabItem 构建底部导航栏的单个标签项。图标和文字垂直排列,文字的颜色和粗细根据 tabIndex1 === idx 条件动态切换——选中时紫色粗体,未选中时灰色常规体。点击时更新 tabIndex1 为当前索引,触发标签页切换。layoutWeight(1) 让每个标签项等分导航栏宽度。

底部导航栏渲染流程

tabs1/tabs2 数组

ForEach 遍历

bottomTabItem 构建

tabIndex1 === idx?

紫色粗体文字

灰色常规文字

点击标签项

tabIndex1 = idx

触发 build 重渲染

显示对应标签页组件


六、HomeTab 首页组件

6.1 组件声明与参数定义

@Component
struct HomeTab {
  dreamScore: number = 0;
  dreamCount: number = 0;
  onOrder: (o: DreamOrder) => void = () => {};
  onAccept: (o: DreamOrder) => void = () => {};
  onIdea: (g: DreamGallery) => void = () => {};
  onNotice: (n: Notice) => void = () => {};

  build() {
    Scroll() {
      Column() {

HomeTab 被标记为 @Component(但不是 @Entry),它是一个子组件。组件定义了五个成员变量:两个数值型数据参数和四个回调函数参数。数据参数的默认值为 0,回调函数的默认值为空箭头函数 () => {}

回调函数参数的类型标注如 (o: DreamOrder) => void 定义了函数的签名——接收一个 DreamOrder 参数,返回 void。这种将函数作为组件参数的设计是 ArkTS 子父组件通信的核心机制。父组件在实例化子组件时传入具体的回调函数实现,子组件在内部交互发生时调用这些回调。

Scroll 组件作为 build() 的根容器,使其内部内容可以纵向滚动。这在内容超出屏幕高度时非常重要。Scroll 内部嵌套 Column 作为内容容器。

6.2 霓虹横幅头部

        Column() {
          Row() {
            Column({ space: 4 }) {
              Text('幻梦工坊 · 早安织梦师').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('今日有 4 个新梦境订单等待接单').fontSize(11).fontColor('#D1C4E9')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('🧚').fontSize(36)
          }
          .width('100%')

首页顶部是一个紫色渐变横幅,包含标题行和数据行。标题行使用 Row 水平排列:左侧是包含标题和副标题的 Column(左对齐,权重 1),右侧是织梦师 Emoji 图标。HorizontalAlign.Start 确保文字左对齐。

          Row({ space: 10 }) {
            Column({ space: 2 }) {
              Text(this.dreamScore + '').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFD54F')
              Text('工坊星币').fontSize(10).fontColor('#D1C4E9')
            }
            .layoutWeight(1)
            .height(56)
            .justifyContent(FlexAlign.Center)
            .backgroundColor('#1AFFFFFF')
            .borderRadius(12)
            Column({ space: 2 }) {
              Text(this.dreamCount + '').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFD54F')
              Text('已织梦境').fontSize(10).fontColor('#D1C4E9')
            }
            .layoutWeight(1)
            .height(56)
            .justifyContent(FlexAlign.Center)
            .backgroundColor('#1AFFFFFF')
            .borderRadius(12)
            Column({ space: 2 }) {
              Text('Lv.' + '3').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFD54F')
              Text('织梦等级').fontSize(10).fontColor('#D1C4E9')
            }
            .layoutWeight(1)
            .height(56)
            .justifyContent(FlexAlign.Center)
            .backgroundColor('#1AFFFFFF')
            .borderRadius(12)
          }
          .width('100%')
          .margin({ top: 12 })
        }
        .width('100%')
        .backgroundColor('linearGradient({ colors: [["#2A0A4A", 0], ["#7C4DFF", 1]] })')
        .borderRadius(16)
        .padding(16)
        .margin({ top: 10 })

数据行包含三个等宽数据卡片,使用 Row({ space: 10 }) 水平排列,间距 10vp。每个卡片使用 layoutWeight(1) 等分宽度,高度 56vp,内容居中对齐。卡片背景使用半透明白色 #1AFFFFFF,在紫色渐变背景上形成"磨砂玻璃"效果。数值用金色 #FFD54F 粗体大字,标签用浅紫色 #D1C4E9 小字。this.dreamScore + '' 将数值转换为字符串用于显示。

整个横幅容器使用紫色渐变背景从深紫 #2A0A4A 到亮紫 #7C4DFF,圆角 16vp,内边距 16vp,顶部外边距 10vp。

6.3 快捷宫格

        Row({ space: 10 }) {
          ForEach(getQuickRows(), (q: QuickIcon) => {
            this.quickItem(q)
          }, (q: QuickIcon) => q.id + 'qa')
        }
        .width('100%')
        .margin({ top: 12 })
        Row({ space: 10 }) {
          ForEach(getQuickRows2(), (q: QuickIcon) => {
            this.quickItem(q)
          }, (q: QuickIcon) => q.id + 'qb')
        }
        .width('100%')
        .margin({ top: 10 })

快捷宫格使用两行 Row,每行四个图标。getQuickRows() 返回前四个快捷图标,getQuickRows2() 返回后四个。每个图标通过 this.quickItem(q) 调用 @Builder 方法渲染。键值使用 q.id + 'qa'q.id + 'qb',前缀区分了两行列表,避免键值冲突。

6.4 待接订单横滑列表

        Scroll() {
          Row({ space: 10 }) {
            ForEach(getTopOrders(), (o: DreamOrder) => {
              this.pendingCard(o)
            }, (o: DreamOrder) => o.id + 'pc')
          }
          .padding({ right: 10 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')
        .height(150)
        .margin({ top: 8 })

待接订单使用横向滚动列表展示。Scroll 组件通过 scrollable(ScrollDirection.Horizontal) 设置为横向滚动模式。scrollBar(BarState.Off) 隐藏滚动条,保持界面整洁。内部 Row 以 10vp 间距水平排列卡片,右侧 10vp 内边距让最后一张卡片与屏幕右缘保持间距。

ForEach 遍历 getTopOrders() 返回的前六条订单,每条订单通过 this.pendingCard(o) 渲染为卡片。卡片高度被 Scroll 容器限制为 150vp。

6.5 quickItem、pendingCard、ideaCard 等子构建器

  @Builder
  quickItem(q: QuickIcon) {
    Column({ space: 4 }) {
      Text(q.icon).fontSize(22)
      Text(q.name).fontSize(11).fontColor('#37474F')
    }
    .layoutWeight(1)
    .height(60)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#F7F3FF')
    .borderRadius(12)
    .onClick(() => {
      if (q.name === '接新单') {
        this.onAccept(DREAM_ORDERS[2]);
      } else if (q.name === '设计灵感') {
        this.onIdea(DREAM_GALLERY[0]);
      } else if (q.name === '工坊公告') {
        this.onNotice(NOTICES[0]);
      } else {
        this.onOrder(DREAM_ORDERS[0]);
      }
    })
  }

quickItem 构建快捷图标卡片。点击事件中通过 if-else if 链根据图标名称分发不同的回调:点击"接新单"调用 onAccept 并传入第三条订单数据,点击"设计灵感"调用 onIdea 并传入第一条画廊数据,以此类推。这种基于名称的事件分发虽然直观,但也可以使用 switch 语句或策略映射来优化。

  @Builder
  pendingCard(o: DreamOrder) {
    Column() {
      Row() {
        Text(o.icon).fontSize(26)
        Text(o.type)
          .fontSize(10)
          .fontColor('#FFFFFF')
          .backgroundColor(o.statusColor)
          .borderRadius(8)
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .margin({ left: 6 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

pendingCard 构建待接订单卡片。卡片顶部行使用 SpaceBetween 对齐让图标和类型标签分居左右两端。类型标签的背景色直接取自订单数据的 statusColor 字段。卡片宽度固定 140vp,高度 128vp,在横滑列表中形成统一尺寸的卡片序列。

  @Builder
  ideaCard(g: DreamGallery) {
    Row() {
      Text(g.icon).fontSize(28)
      Column({ space: 2 }) {
        Text(g.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#2A0A4A')
        Text(g.style + ' · ' + g.like + ' 赞').fontSize(10).fontColor('#90A4AE')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 8 })
    }
    .width(150)
    .height(86)
    .backgroundColor(g.color)
    .borderRadius(12)
    .padding(12)
    .opacity(0.95)
    .onClick(() => {
      this.onIdea(g);
    })
  }

ideaCard 构建灵感推荐卡片,其特色在于卡片背景色直接取自画廊数据的 color 字段,使每张卡片都有独特的主题色。opacity(0.95) 设置 95% 不透明度,略微透出底层背景,增加层次感。

HomeTab 首页组件结构

Scroll 滚动容器

Column 内容容器

霓虹横幅头部

快捷宫格 两行四列

待接订单横滑列表

灵感推荐横滑列表

工坊公告列表

底部标语

标题行

三个数据卡片

ForEach pendingCard

ForEach ideaCard

ForEach noticeRow


七、WorkTab 梦工坊组件

7.1 当前进度头部与工序时间轴

@Component
struct WorkTab {
  onOrder: (o: DreamOrder) => void = () => {};
  onPeriod: (o: DreamOrder) => void = () => {};
  onUrgent: (o: DreamOrder) => void = () => {};
  onSkill: (s: Skill) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Column({ space: 3 }) {
              Text('正在编织 · 星海之梦').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('当前进度 50% · 第 5 道工序').fontSize(11).fontColor('#D1C4E9')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('🧵').fontSize(32)
          }
          .width('100%')
          Row() {
            Row() {
              Column() {
              }
              .width('50%')
              .height(8)
              .backgroundColor('linearGradient({ colors: [["#FF80AB", 0], ["#FFD54F", 1]] })')
              .borderRadius(4)
            }
            .width('100%')
            .height(8)
            .backgroundColor('#33FFFFFF')
            .borderRadius(4)
          }
          .width('100%')
          .margin({ top: 12 })
        }

WorkTab 组件定义了四个回调参数:onOrder(查看订单)、onPeriod(工期协商)、onUrgent(加急处理)、onSkill(技能升级)。头部区域展示当前编织中的梦境名称和进度百分比,配以一个从樱粉到金色的渐变进度条。进度条内层宽度固定为 50%,对应"当前进度 50%"的数据。

        Column() {
          ForEach(getProcessRows(), (p: DreamProcess) => {
            this.processLine(p)
          }, (p: DreamProcess) => p.id + 'pa')
          ForEach(getProcessRows2(), (p: DreamProcess) => {
            this.processLine(p)
          }, (p: DreamProcess) => p.id + 'pb')
        }
        .width('100%')
        .backgroundColor('#F7F3FF')
        .borderRadius(12)
        .padding({ top: 4, bottom: 4 })
        .margin({ top: 8 })

工序时间轴使用两次 ForEach 分别渲染偶数索引和奇数索引的工序。虽然这种拆分在单列列表中并非必需(因为单列列表可以直接用一个 ForEach 遍历全部数据),但在此处保持了与其他双列布局一致的代码风格。

7.2 processLine 工序行构建器

  @Builder
  processLine(p: DreamProcess) {
    Row() {
      Column() {
        Text(p.done ? '●' : '○').fontSize(12).fontColor(p.done ? '#7C4DFF' : '#B0BEC5')
        Row() {
          Column() {
          }
          .width(2)
          .height(36)
          .backgroundColor('#E0D5F5')
        }
      }
      .width(18)
      .margin({ left: 10 })
      Column({ space: 3 }) {
        Text(p.step + '. ' + p.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(p.done ? '#2A0A4A' : '#90A4AE')
        Text(p.desc).fontSize(11).fontColor('#90A4AE')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 6 })
      Text(p.done ? '✓' : p.time).fontSize(10).fontColor(p.done ? '#7C4DFF' : '#B0BEC5')
    }
    .width('100%')
    .height(54)
    .margin({ right: 10 })
  }

processLine 构建单个工序行,采用时间轴设计:左侧是状态节点(实心圆 或空心圆 ),下方延伸出一条 2vp 宽、36vp 高的竖线 #E0D5F5 连接到下一道工序。中间是工序序号和名称,右侧是完成标记 或预计时间。p.done 布尔值同时驱动了圆点样式、文字颜色和右侧标记的视觉表现。

7.3 workOrderRow 与 skillCard

  @Builder
  workOrderRow(o: DreamOrder) {
    Row() {
      Text(o.icon).fontSize(22)
      Column({ space: 2 }) {
        Text(o.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#2A0A4A')
        Text(o.client + ' · ' + o.time).fontSize(10).fontColor('#90A4AE')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 8 })
      Text(o.status)
        .fontSize(11)
        .fontColor('#FFFFFF')
        .backgroundColor(o.statusColor)
        .borderRadius(8)
        .padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .onClick(() => {
          this.onOrder(o);
        })
      Text('⏱')
        .fontSize(16)
        .margin({ left: 8 })
        .onClick(() => {
          this.onUrgent(o);
        })
    }
    .width('100%')
    .height(52)
    .backgroundColor('#FFFFFF')
    .borderRadius(10)
    .padding({ left: 10, right: 10 })
    .onClick(() => {
      this.onOrder(o);
    })
  }

workOrderRow 构建编织中订单的行卡片。状态标签点击触发 onOrder 回调(查看订单详情),加急图标 点击触发 onUrgent 回调(加急确认)。整行点击也触发 onOrder。这种"不同元素绑定不同事件"的设计使得同一行卡片上的不同交互区域可以触发不同的业务流程。

  @Builder
  skillCard(s: Skill) {
    Column({ space: 4 }) {
      Text(s.icon).fontSize(24)
      Text(s.name).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#2A0A4A')
      Text('Lv.' + s.level).fontSize(10).fontColor('#FF80AB')
    }
    .width(72)
    .height(92)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#F7F3FF')
    .borderRadius(12)
    .onClick(() => {
      this.onSkill(s);
    })
  }

skillCard 构建技能卡片,固定宽度 72vp,高度 92vp,在横滑列表中展示。点击卡片触发 onSkill 回调,打开技能升级弹窗。


八、DreamTab 梦境画廊组件

8.1 画廊横幅与精选大卡

@Component
struct DreamTab {
  onShare: (g: DreamGallery) => void = () => {};
  onSummary: (o: DreamOrder) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Row() {
          Text('🖼️').fontSize(30)
          Column({ space: 2 }) {
            Text('梦境画廊').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text('收藏大家的治愈梦境').fontSize(11).fontColor('#D1C4E9')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })
        }
        .width('100%')
        .height(60)
        .backgroundColor('linearGradient({ colors: [["#2A0A4A", 0], ["#7C4DFF", 1]] })')
        .borderRadius(16)
        .padding(14)
        .margin({ top: 10 })

DreamTab 定义了两个回调参数:onShare(分享梦境)和 onSummary(收工总结)。头部横幅使用紫色渐变背景,高度 60vp,左侧是画廊 Emoji 图标,右侧是标题和副标题。

        Scroll() {
          Row({ space: 10 }) {
            ForEach(getGalleryRows(), (g: DreamGallery) => {
              this.galleryBigCard(g)
            }, (g: DreamGallery) => g.id + 'gb')
          }
          .padding({ right: 10 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')
        .height(150)
        .margin({ top: 8 })

精选大卡使用横滑列表,卡片高度 150vp。galleryBigCard 构建器将卡片背景设为画廊作品的主题色,使用白色文字确保在彩色背景上的可读性。

8.2 双列瀑布流布局

        Row({ space: 10 }) {
          Column({ space: 10 }) {
            ForEach(getGalleryRows(), (g: DreamGallery) => {
              this.galleryCard(g)
            }, (g: DreamGallery) => g.id + 'ga')
          }
          .layoutWeight(1)
          Column({ space: 10 }) {
            ForEach(getGalleryRows2(), (g: DreamGallery) => {
              this.galleryCard(g)
            }, (g: DreamGallery) => g.id + 'g2')
          }
          .layoutWeight(1)
        }
        .width('100%')
        .margin({ top: 8 })

全部作品使用双列布局实现。外层 Row 包含两个 Column,每个 Column 使用 layoutWeight(1) 等分宽度。左列渲染偶数索引的画廊作品,右列渲染奇数索引。每个 Column 内部使用 ForEach 渲染作品卡片,space: 10 设置卡片间的垂直间距。

这种双列布局的关键在于两个 Column 都使用了 layoutWeight(1),使它们在 Row 中占据相等的宽度。每个 Column 内部的卡片宽度为 100%(相对于 Column 自身宽度),实现了自适应的双列网格。

8.3 galleryBigCard、galleryCard、doneRow

  @Builder
  galleryBigCard(g: DreamGallery) {
    Column() {
      Row() {
        Text(g.icon).fontSize(30)
        Text('💗 ' + g.like).fontSize(11).fontColor('#FFFFFF').margin({ left: 6 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      Text(g.name).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 14 })
      Text(g.style + ' · ' + g.desc).fontSize(11).fontColor('#EDE7F6').margin({ top: 6 })
    }
    .width(180)
    .height(130)
    .backgroundColor(g.color)
    .borderRadius(14)
    .padding(14)
    .onClick(() => {
      this.onShare(g);
    })
  }

galleryBigCard 构建精选大卡片,固定宽度 180vp,高度 130vp。卡片背景使用画廊主题色 g.color,文字使用白色和浅紫色 #EDE7F6 确保在彩色背景上的对比度。点击卡片触发 onShare 回调,打开梦境分享弹窗。

  @Builder
  galleryCard(g: DreamGallery) {
    Column() {
      Text(g.icon).fontSize(30)
      Text(g.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#2A0A4A').margin({ top: 6 })
      Text(g.style).fontSize(10).fontColor('#90A4AE').margin({ top: 3 })
      Text('💗 ' + g.like).fontSize(10).fontColor('#FF80AB').margin({ top: 6 })
    }
    .width('100%')
    .height(110)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({ radius: 4, color: '#12000000', offsetY: 2 })
    .onClick(() => {
      this.onShare(g);
    })
  }

galleryCard 构建双列布局中的小卡片,宽度为 100%(相对于父 Column),高度 110vp。白色背景配以 shadow 阴影效果(半径 4vp,颜色 #12000000 约 7% 不透明度的黑色,Y 轴偏移 2vp),营造卡片悬浮于背景之上的视觉效果。

  @Builder
  doneRow(o: DreamOrder) {
    Row() {
      Text(o.icon).fontSize(18)
      Column({ space: 2 }) {
        Text(o.name).fontSize(13).fontWeight(FontWeight.Medium).fontColor('#37474F')
        Text(o.client + ' · ' + o.time).fontSize(10).fontColor('#90A4AE')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 8 })
      Text('总结 ›')
        .fontSize(12)
        .fontColor('#7C4DFF')
        .onClick(() => {
          this.onSummary(o);
        })
    }
    .width('100%')
    .height(46)
    .onClick(() => {
      this.onSummary(o);
    })
  }

doneRow 构建已交付梦境行,右侧"总结 ›"文字点击触发 onSummary 回调,打开收工总结弹窗。整行点击也触发相同回调。


九、MaterialTab 素材仓组件

9.1 素材横幅与分类标签

@Component
struct MaterialTab {
  onMaterial: (m: Material) => void = () => {};
  onBuy: (m: Material) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Row() {
          Text('🧺').fontSize(30)
          Column({ space: 2 }) {
            Text('素材仓').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text('12 种织梦材料 · 随时取用').fontSize(11).fontColor('#D1C4E9')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })
          Text('满 300 免配送').fontSize(11).fontColor('#FFD54F')
        }

MaterialTab 定义了两个回调:onMaterial(素材选择)和 onBuy(素材购买)。头部横幅右侧显示"满 300 免配送"的金色提示文字。

        Scroll() {
          Row({ space: 8 }) {
            ForEach(['全部', '织梦材料', '装饰材料', '感官材料', '容器材料'], (c: string) => {
              Text(c)
                .fontSize(12)
                .fontColor('#7C4DFF')
                .backgroundColor('#EDE7F6')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 5, bottom: 5 })
            }, (c: string) => c)
          }
          .padding({ right: 10 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)

分类标签使用横滑列表展示五个分类选项。每个标签使用浅紫色背景 #EDE7F6 和紫色文字 #7C4DFF,圆角 13vp。注意这里的分类标签没有绑定选中状态逻辑(没有 onClick),仅做展示用途。在实际项目中,可以通过添加 @State 变量来追踪选中分类,并根据分类过滤素材列表。

9.2 库存状态条与素材双列

        Row({ space: 10 }) {
          this.stockMini('🫙', '梦忆瓶', 28)
          this.stockMini('☀️', '暖阳晶', 36)
          this.stockMini('🌕', '月光珠', 42)
          this.stockMini('✨', '星光纱', 86)
        }
        .width('100%')
        .margin({ top: 12 })

库存状态条使用四个 stockMini 构建器展示关键素材的库存。每个素材项通过 stockMini 内部的条件判断显示不同颜色——库存低于 40 显示红色 #E53935,高于 40 显示绿色 #43A047

  @Builder
  stockMini(icon: string, name: string, num: number) {
    Column({ space: 2 }) {
      Text(icon).fontSize(18)
      Text(name).fontSize(10).fontColor('#37474F')
      Text(num + ' 份').fontSize(10).fontWeight(FontWeight.Bold).fontColor(num < 40 ? '#E53935' : '#43A047')
    }
    .layoutWeight(1)
    .height(58)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#F7F3FF')
    .borderRadius(10)
  }

stockMini 接收图标、名称和数量三个参数。数量低于 40 时数字显示红色预警色,否则显示绿色安全色。这种基于阈值的条件着色是数据驱动 UI 的典型应用。

9.3 matCard 与 matRow

  @Builder
  matCard(m: Material) {
    Column() {
      Text(m.icon).fontSize(30)
      Text(m.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#2A0A4A').margin({ top: 6 })
      Text(m.type).fontSize(10).fontColor('#90A4AE').margin({ top: 3 })
      Row() {
        Text('¥' + m.price).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#FF80AB')
        Text(' 购买').fontSize(11).fontColor('#7C4DFF')
      }
      .width('100%')
      .justifyContent(FlexAlign.End)
      .margin({ top: 6 })
      .onClick(() => {
        this.onBuy(m);
      })
    }
    .width('100%')
    .height(124)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({ radius: 4, color: '#12000000', offsetY: 2 })
    .onClick(() => {
      this.onMaterial(m);
    })
  }

matCard 构建素材卡片,整卡片点击触发 onMaterial(素材选择),底部"购买"文字点击触发 onBuy(素材购买)。同一卡片上的不同交互区域绑定不同的回调,实现了从同一入口进入不同业务流程的交互设计。

  @Builder
  matRow(m: Material) {
    Row() {
      Text(m.icon).fontSize(18)
      Column({ space: 2 }) {
        Text(m.name).fontSize(13).fontWeight(FontWeight.Medium).fontColor('#37474F')
        Text(m.desc).fontSize(10).fontColor('#90A4AE')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 8 })
      Text('库存 ' + m.stock)
        .fontSize(11)
        .fontColor('#FFFFFF')
        .backgroundColor(m.stock < 40 ? '#E53935' : '#7C4DFF')
        .borderRadius(8)
        .padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .onClick(() => {
          this.onMaterial(m);
        })
    }
    .width('100%')
    .height(48)
    .onClick(() => {
      this.onMaterial(m);
    })
  }

matRow 构建素材行,库存标签的背景色通过 m.stock < 40 条件动态选择——低于 40 红色预警,高于 40 紫色正常。


十、ClientTab 客户档案组件

@Component
struct ClientTab {
  onChat: (c: Client) => void = () => {};
  onRate: (o: DreamOrder) => void = () => {};
  onClient: (c: Client) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Row() {
          Text('💬').fontSize(30)
          Column({ space: 2 }) {
            Text('客户星档').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            Text('10 位常客 · 满意度 4.9').fontSize(11).fontColor('#D1C4E9')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          .margin({ left: 10 })
          Text('🎁').fontSize(26)
        }
        .width('100%')
        .height(60)
        .backgroundColor('linearGradient({ colors: [["#FF80AB", 0], ["#7C4DFF", 1]] })')
        .borderRadius(16)
        .padding(14)
        .margin({ top: 10 })

ClientTab 定义了三个回调:onChat(客户沟通)、onRate(订单评价)、onClient(客户档案)。头部横幅使用从樱粉 #FF80AB 到紫色 #7C4DFF 的渐变,与其他标签页的紫色渐变形成差异化。

10.1 客户双列卡片与评价行

  @Builder
  clientCard(c: Client) {
    Column() {
      Row() {
        Text(c.icon).fontSize(26)
        Text(c.online ? '●' : '○').fontSize(10).fontColor(c.online ? '#43A047' : '#B0BEC5')
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      Text(c.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#2A0A4A').margin({ top: 6 })
      Text(c.level + ' · ' + c.score + ' 分').fontSize(10).fontColor('#FF80AB').margin({ top: 3 })
      Text(c.orders + ' 单').fontSize(10).fontColor('#90A4AE').margin({ top: 4 })
    }
    .width('100%')
    .height(104)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .padding(10)
    .shadow({ radius: 4, color: '#12000000', offsetY: 2 })
    .onClick(() => {
      this.onClient(c);
    })
  }

clientCard 构建客户卡片,顶部行使用 SpaceBetween 让头像和在线状态指示灯分居两端。在线状态指示灯使用绿色实心圆点或灰色空心圆点。点击卡片触发 onClient 回调,打开客户档案弹窗。

  @Builder
  rateRow(o: DreamOrder) {
    Row() {
      Text(o.icon).fontSize(18)
      Column({ space: 2 }) {
        Text(o.name).fontSize(13).fontWeight(FontWeight.Medium).fontColor('#37474F')
        Text('客户 ' + o.client).fontSize(10).fontColor('#90A4AE')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 8 })
      Text('评价 ›')
        .fontSize(12)
        .fontColor('#7C4DFF')
        .onClick(() => {
          this.onRate(o);
        })
    }
    .width('100%')
    .height(46)
    .onClick(() => {
      this.onRate(o);
    })
  }

rateRow 构建待评价行,"评价 ›"文字点击触发 onRate 回调,打开满意度评价弹窗。


十一、RankTab 排行榜组件

11.1 领奖台与榜单

@Component
struct RankTab {
  onRank: (r: RankItem) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('🏆').fontSize(30)
            Text('工坊月度排行').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.Center)
          Text('本月共织造 286 个梦境 · 累计 38600 星币').fontSize(11).fontColor('#D1C4E9').margin({ top: 4 })
        }
        .width('100%')
        .backgroundColor('linearGradient({ colors: [["#2A0A4A", 0], ["#7C4DFF", 1]] })')
        .borderRadius(16)
        .padding(14)
        .margin({ top: 10 })

        Row({ space: 10 }) {
          this.podiumCard(RANK_LIST[1], 2)
          this.podiumCard(RANK_LIST[0], 1)
          this.podiumCard(RANK_LIST[2], 3)
        }
        .width('100%')
        .margin({ top: 14 })

RankTab 只定义了一个回调 onRank。领奖台使用三个 podiumCard 构建器,注意调用顺序是亚军(索引1)、冠军(索引0)、季军(索引2),让冠军卡片居中显示。这种"2-1-3"的排列方式是奖台领奖台的经典布局。

11.2 podiumCard 与 rankRow

  @Builder
  podiumCard(r: RankItem, rank: number) {
    Column({ space: 4 }) {
      Text(r.icon).fontSize(30)
      Text(r.name).fontSize(11).fontWeight(FontWeight.Bold).fontColor('#2A0A4A')
      Text(r.score + '').fontSize(10).fontColor('#FF80AB')
      Text(rank === 1 ? '👑 冠军' : (rank === 2 ? '🥈 亚军' : '🥉 季军'))
        .fontSize(10)
        .fontColor('#FFFFFF')
        .backgroundColor(rank === 1 ? '#FFD54F' : (rank === 2 ? '#B0BEC5' : '#FF8F00'))
        .borderRadius(8)
        .padding({ left: 6, right: 6, top: 2, bottom: 2 })
    }
    .layoutWeight(1)
    .height(rank === 1 ? 128 : 108)
    .justifyContent(FlexAlign.End)
    .padding({ bottom: 8 })
    .backgroundColor(rank === 1 ? '#FFF8E1' : '#F7F3FF')
    .borderRadius(12)
    .onClick(() => {
      this.onRank(r);
    })
  }

podiumCard 接收一个 RankItem 和排名数字。冠军卡片高度 128vp(比亚军和季军的 108vp 高出 20vp),背景使用金色 #FFF8E1(其余使用浅紫 #F7F3FF),形成"冠军最高"的视觉效果。justifyContent(FlexAlign.End) 让内容靠底部对齐,模拟领奖台"站在台上"的效果。嵌套三元表达式 rank === 1 ? ... : (rank === 2 ? ... : ...) 根据排名选择不同的奖牌 Emoji 和背景色。

  @Builder
  rankRow(r: RankItem) {
    Row() {
      Text(r.rank + '').fontSize(13).fontWeight(FontWeight.Bold).fontColor(getRankColor(r.rank)).width(24)
      Text(r.icon).fontSize(20)
      Column({ space: 2 }) {
        Text(r.name).fontSize(13).fontWeight(FontWeight.Medium).fontColor('#37474F')
        Text(r.title).fontSize(10).fontColor('#90A4AE')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 8 })
      Text(r.score + '')
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor('#7C4DFF')
        .onClick(() => {
          this.onRank(r);
        })
    }
    .width('100%')
    .height(48)
    .onClick(() => {
      this.onRank(r);
    })
  }

rankRow 构建完整榜单的行。排名数字的颜色通过 getRankColor(r.rank) 函数获取,前三名分别使用金银铜色。固定宽度 width(24) 确保排名数字列对齐。


十二、MineTab 我的组件

12.1 织梦师档案头部

@Component
struct MineTab {
  dreamScore: number = 0;
  memberLevel: number = 1;
  onAchieve: (a: Achievement) => void = () => {};
  onSkill: (s: Skill) => void = () => {};
  onSummary: (o: DreamOrder) => void = () => {};

  build() {
    Scroll() {
      Column() {
        Column() {
          Row() {
            Text('🧚').fontSize(38)
            Column({ space: 3 }) {
              Text('星梦织女').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('Lv.' + this.memberLevel + ' 织梦师 · 工坊编号 DR-026').fontSize(11).fontColor('#D1C4E9')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            .margin({ left: 12 })
            Text(this.dreamScore + '')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFD54F')
          }
          .width('100%')
          Row({ space: 10 }) {
            this.mineStat('🌌', '织梦数', '126')
            this.mineStat('⭐', '好评率', '98%')
            this.mineStat('🏆', '排名', 'No.6')
            this.mineStat('🎯', '连续签到', '12 天')
          }
          .width('100%')
          .margin({ top: 12 })
        }

MineTab 接收两个数据参数(dreamScorememberLevel)和三个回调参数。头部档案区域展示了织梦师头像、名称、等级和工坊编号,以及四个统计数据。this.memberLevelthis.dreamScore 从父组件传入,实现了跨层级的数据传递。

12.2 mineStat、skillRow 与成就墙

  @Builder
  mineStat(icon: string, name: string, value: string) {
    Column({ space: 2 }) {
      Text(icon).fontSize(16)
      Text(value).fontSize(14).fontWeight(FontWeight.Bold).fontColor('#FFD54F')
      Text(name).fontSize(9).fontColor('#D1C4E9')
    }
    .layoutWeight(1)
    .height(58)
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#1AFFFFFF')
    .borderRadius(10)
  }

mineStat 构建统计数据卡片,接收图标、名称和值三个字符串参数。layoutWeight(1) 让四个统计卡片等分宽度,高度 58vp,半透明白色背景在紫色渐变上形成磨砂效果。

  @Builder
  skillRow(s: Skill) {
    Column() {
      Row() {
        Text(s.icon).fontSize(20)
        Text(s.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#2A0A4A').layoutWeight(1).margin({ left: 6 })
        Text('Lv.' + s.level).fontSize(11).fontColor('#FF80AB')
      }
      .width('100%')
      Row() {
        Row() {
          Column() {
          }
          .width(s.progress + '%')
          .height(6)
          .backgroundColor('linearGradient({ colors: [["#7C4DFF", 0], ["#B388FF", 1]] })')
          .borderRadius(3)
        }
        .width('100%')
        .height(6)
        .backgroundColor('#F0EBFA')
        .borderRadius(3)
      }
      .width('100%')
      .margin({ top: 6 })
    }
    .width('100%')
    .height(58)
    .backgroundColor('#FFFFFF')
    .borderRadius(10)
    .padding(10)
    .onClick(() => {
      this.onSkill(s);
    })
  }

skillRow 构建技能行,包含技能图标、名称、等级和进度条。进度条宽度由 s.progress + '%' 动态决定。点击整行触发 onSkill 回调,打开技能升级弹窗。

        Scroll() {
          Row({ space: 10 }) {
            ForEach(ACHIEVEMENTS, (a: Achievement) => {
              Column({ space: 4 }) {
                Text(a.icon).fontSize(26).opacity(a.locked ? 0.35 : 1)
                Text(a.name).fontSize(10).fontColor(a.locked ? '#B0BEC5' : '#37474F')
              }
              .width(64)
              .height(64)
              .justifyContent(FlexAlign.Center)
              .backgroundColor(a.locked ? '#ECEFF1' : '#FFF8E1')
              .borderRadius(12)
              .onClick(() => {
                this.onAchieve(a);
              })
            }, (a: Achievement) => a.id + 'ac')
          }
          .padding({ right: 10 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')


    .height(46)
    .onClick(() => {
      this.onSummary(o);
    })
  }
}


在这里插入图片描述

在交互设计层面,onClick 事件绑定为每个可交互元素定义了精确的响应逻辑。从标签页切换到弹窗开关,从表单输入到星级评分,每一次用户交互都通过状态更新驱动界面刷新。forEach 中的键值生成确保了列表项在数据变化时的正确追踪和高效更新。setTimeout 实现了 Toast 提示的自动消失。非空断言操作符 ! 在弹窗内部安全地访问已确认不为 null 的选中对象。这些细节共同构成了应用的交互逻辑骨架。

在视觉设计层面,linearGradient 渐变背景为头部横幅和弹窗营造了梦幻的氛围。shadow 阴影属性为卡片和导航栏添加了层次感。borderRadius 圆角让所有界面元素呈现出柔和的视觉风格。clip(true) 确保圆角容器内的内容不会溢出边界。opacity 透明度为未解锁成就和磨砂效果提供了视觉暗示。animationscale 为按钮点击和图标展示提供了流畅的过渡反馈。整个应用围绕"梦幻紫粉"色彩体系,通过深紫 #2A0A4A、亮紫 #7C4DFF、樱粉 #FF80AB 和星辉金 #FFD54F 四种核心色的组合,构建了一致的视觉品牌识别。

ForEach 列表渲染层面,键值生成函数的设计体现了对框架差异更新机制的理解。每个 ForEach 的键值都由数据 ID 和场景前缀组成(如 o.id + 'pc'g.id + 'gb'),既保证了列表内的唯一性,又避免了不同 ForEach 之间的键值冲突。当数组数据发生变化时,框架通过键值对比新旧列表,只更新发生变化的列表项,而非全量重建,这种高效的差异更新机制是列表操作性能的保障。

在弹窗体系层面,十六个 @Builder 弹窗构建器覆盖了全部核心业务场景。每个弹窗都有独立的显示开关和选中对象,通过 if (this.showXxx && this.selXxx !== null) 的双重条件确保了渲染安全。遮罩层 modalOverlay 作为公共构建器被所有弹窗复用,通过参数化的关闭回调实现了"点击遮罩关闭弹窗"的统一交互。弹窗的视觉风格各异——居中卡片、底部抽屉、星空深色卡、警示红卡、金色奖励卡、票根卡等——但都遵循了统一的布局规范:宽度 84%、圆角 20vp、内边距 20vp、最大高度约束 82%-86%。

总而言之,这份源码完整地展示了 ArkTS 在鸿蒙生态下的应用开发范式。从 interface 的类型约束到 const 的数据声明,从 @State 的响应式状态到 @Builder 的 UI 复用,从 Column/Row/Stack 的布局组合到 ForEach 的列表渲染,从父子组件的参数传递到回调函数的事件冒泡,每一个技术点都在真实的业务场景中得到了应用和验证。声明式 UI 的核心理念——状态驱动渲染、组件化拆分、单向数据流——贯穿了整个应用的架构设计。这份源码不仅是一个功能完整的梦境编织工坊应用,更是一份 ArkTS 工程实践的参考范本,为理解和掌握鸿蒙应用开发提供了详实的案例支撑。

Logo

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

更多推荐