一、HarmonyOS 技术背景与 ArkTS 在数字制造场景中的工程价值

在这里插入图片描述

HarmonyOS 6.1.1 作为华为全场景分布式操作系统的最新演进版本,其核心开发语言 ArkTS 已经从早期的实验性框架发展为一套成熟的、类型安全的、声明式的 UI 编程体系。基于 HarmonyOS API 24 所提供的系统能力开放层级,ArkTS 通过 @Entry、@Component、@State、@Builder、@Observed 等装饰器体系,构建了一套从数据模型定义到视图渲染更新的完整响应式管线。开发者只需声明数据与视图之间的绑定关系,框架便能在编译阶段自动生成高效的 Diff 算法和最小化更新策略,无需手动管理视图刷新的时序与粒度。这种声明式编程范式不仅降低了开发者的认知负担,更在 AOT 编译的加持下实现了接近原生语言的运行性能。

在 3D 打印手办与模型上色这一数字制造垂直场景中,HarmonyOS ArkTS API 24 的技术优势体现在多个层面。3D 打印是一个从数字模型到实体制造的完整工艺链路,涉及建模、切片、打印、后处理、上色等多个阶段,每个阶段都有不同的状态参数和进度指标。印语·3D 打印手办与模型上色工坊应用正是围绕这一数字制造流程,构建了从手办橱窗展示、打印任务管理、打印工坊监控、建模课堂教学到创友社区交流的全链路数字化体验。应用采用青色与橙色的配色方案(#3FA8A0 / #E8843C),在浅色清爽主题下营造出科技感与创造力并存的视觉氛围,与夜钓的暗色沉浸和缝纫的暖色温馨形成了鲜明的场景对比。

从技术架构角度审视,该应用同样采用了"底部四主 Tab + 首页七内容 Tab"的双层导航结构。底部四个主 Tab 对应"首页"“橱窗”“课堂”“我的”,而首页的七个内容子 Tab 以"层积阶梯式"的视觉风格呈现。每个 Tab 项的文字下方排列着三个高度递减的竖条(选中时高度 6/4/2,未选中时高度 3/3/3),模拟了 3D 打印中层积堆积的阶梯纹理。这种将制造工艺的核心视觉特征融入导航交互设计的做法,是 ArkTS 声明式 UI 在场景化视觉表达方面的又一典型案例,展示了框架在自定义视觉细节方面的强大灵活性。

在数据驱动层面,应用使用 @Observed 装饰器对 PostItem(创友动态)、FigureItem(手办信息)、PrintItem(打印任务)、GearItem(材料信息)四个核心数据模型进行响应式包装。当用户通过弹框发布新动态、编辑打印任务状态或取消打印任务时,@Observed 类的属性变化会自动触发关联 UI 组件的重新渲染。此外,应用通过 90 毫秒间隔的 setInterval 定时器驱动 tick 计数器,配合纯函数计算打印头扫描横条的坐标与透明度、耗材光点的位置与闪烁状态,实现了与 3D 打印场景高度匹配的特效动画层。整个应用的数据流、视图层和动画层通过 ArkTS 的响应式机制实现了高度的解耦与协同。

二、ColorPalette 接口与青橙科技配色方案

在这里插入图片描述

应用通过 interface 定义了 ColorPalette 接口,约束了所有颜色字段的类型安全。与夜钓和缝纫应用不同,此处的接口中包含了一个 filament(耗材色)字段,取代了夜钓的 lure 和缝纫的 thread,体现了数字制造场景中耗材颜色的特殊语义。

interface ColorPalette {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  bg: string;
  cardBg: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  border: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
  filament: string;
}

ColorPalette 接口定义了十六个颜色字段。filament 字段是 3D 打印场景的专属扩展,用于模拟打印耗材(PLA、树脂等)的特殊色彩。primary 和 primaryDark 分别用于主色调和深色变体,accent 和 accentLight 用于辅色和浅色变体。bg 和 cardBg 分别定义了页面背景色和卡片背景色。文本三级色阶提供了信息层次的颜色依据。语义色用于状态标识和操作按钮的视觉区分。

const COLORS: ColorPalette = {
  primary: '#3FA8A0',
  primaryLight: '#E2F2F1',
  primaryDark: '#2E7E78',
  accent: '#E8843C',
  accentLight: '#FCEDE1',
  bg: '#F2F7F6',
  cardBg: '#FFFFFF',
  textPrimary: '#2E3A38',
  textSecondary: '#64736F',
  textHint: '#AEBCB8',
  border: '#DFEAE8',
  success: '#6FAF8F',
  warning: '#D9973B',
  danger: '#C0503F',
  white: '#FFFFFF',
  filament: '#A78BCC'
};

COLORS 常量的实例化体现了数字制造场景的设计哲学。主色 #3FA8A0 是一种沉稳的青色,呼应了 3D 打印机金属外壳的科技感;辅色 #E8843C 是一种活力橙色,象征了创造力与制造热情;背景色 #F2F7F6 是一种极浅的青灰白,模拟了实验室或工作室的洁净环境;卡片背景色 #FFFFFF 使用纯白色,在浅色背景上形成清爽的层次。耗材色 #A78BCC 是一种淡紫色,用于特效层中的耗材光点闪烁,模拟了打印过程中耗材丝在光线下的折射效果。文本色阶从 #2E3A38(深青灰)到 #AEBCB8(浅灰青),在浅色背景下形成了清晰而科技感十足的信息层次。

三、@Observed 数据模型类定义

在这里插入图片描述

应用定义了四个 @Observed 类来承载 3D 打印场景的不同业务领域数据。每个类都通过构造函数实现属性的全赋值初始化,确保对象创建时所有字段都有确定性的初始值。

@Observed
export class PostItem {
  id: number = 0
  nick: string = ''
  avatar: string = ''
  text: string = ''
  time: string = ''
  likes: number = 0
  constructor(id: number, nick: string, avatar: string, text: string, time: string, likes: number) {
    this.id = id; this.nick = nick; this.avatar = avatar; this.text = text
    this.time = time; this.likes = likes
  }
}

PostItem 是创友动态数据模型,结构与夜钓和缝纫应用的 PostItem 完全一致,但静态数据内容完全不同。avatar 字段使用与 3D 打印相关的 Emoji(如 🐉、⚙️、🎨、🖨️ 等),text 字段存储包含专业 3D 打印术语的动态内容(如"0.12 层高"“树脂支撑”“水磨”"渗线"等)。这种跨场景的数据结构一致性是架构复用的典型体现。

@Observed
export class FigureItem {
  id: number = 0
  name: string = ''
  craft: string = ''
  price: string = ''
  level: string = ''
  hot: number = 0
  constructor(id: number, name: string, craft: string, price: string, level: string, hot: number) {
    this.id = id; this.name = name; this.craft = craft; this.price = price
    this.level = level; this.hot = hot
  }
}

FigureItem 是手办数据模型,name 字段存储手办名称(如"祥云龙形手办 · 180mm"“机动装甲 · 可动款”),craft 字段存储工艺信息(如"0.12 层高 · 树脂"“26 关节 · PLA+”),price 字段以字符串形式存储价格,level 字段是等级标签(如"典藏"“联名”“限定”“人气”“入门”“新品”"手作"等),hot 字段是关注度数值。level 字段通过 levelColor 纯函数映射为不同的标签颜色,实现了手办分类的视觉化。

@Observed
export class PrintItem {
  id: number = 0
  name: string = ''
  item: string = ''
  status: string = ''
  progress: number = 0
  constructor(id: number, name: string, item: string, status: string, progress: number) {
    this.id = id; this.name = name; this.item = item; this.status = status
    this.progress = progress
  }
}

PrintItem 是打印任务数据模型,name 字段包含任务名称(如"祥云龙形手办"“机动装甲右臂”),item 字段描述当前打印状态详情(如"3 号机 · 层高 0.12 · 剩 2 小时 40 分"“切片中 · 支撑自动生成”),status 字段是任务运行状态(如"打印中"“切片中”“排队中”“已完成”"后处理中"等),progress 字段是进度百分比。该模型是"我的打印"页面的核心数据载体,其 status 字段通过 statusColor 和 statusProgress 纯函数映射为不同的颜色和进度值,实现了打印任务全生命周期的状态可视化。

@Observed
export class GearItem {
  id: number = 0
  name: string = ''
  desc: string = ''
  icon: string = ''
  constructor(id: number, name: string, desc: string, icon: string) {
    this.id = id; this.name = name; this.desc = desc; this.icon = icon
  }
}

GearItem 是材料数据模型,静态数据涵盖了 PLA+ 耗材、光敏树脂、硬质合金喷嘴、斜口剪钳、砂纸组、水补土喷雾、丙烯颜料和 UV 手办胶等八种 3D 打印相关材料和工具。每种材料的 desc 字段都包含了专业参数(如"科技青 · ±0.02"“高精度 · 低味”"耐磨 · 长寿命"等),体现了应用在数字制造领域的专业深度。

四、接口定义与静态数据数组

在这里插入图片描述

应用定义了多个 interface 用于图表与课程等辅助数据的类型约束,这些接口在运行时不会发生变化,属于只读的展示型数据。

interface WeekChartItem {
  label: string;
  value: number;
}

interface MatChartItem {
  label: string;
  value: number;
  color: string;
}

interface CourseItem {
  title: string;
  week: string;
  progress: number;
  color: string;
}

interface PlayTypeItem {
  name: string;
  desc: string;
  icon: string;
  hot: boolean;
}

WeekChartItem 用于本周交付单数柱状图,MatChartItem 在此基础上增加 color 字段用于指定耗材类别的独立颜色,CourseItem 包含课程标题、周次、进度和颜色四个字段,PlayTypeItem 用于玩法类型卡片。MatChartItem 是 3D 打印场景特有的接口名称(Material Chart),反映了耗材消耗构成的数据可视化需求。

const POST_LIST: PostItem[] = [
  new PostItem(1, '层积造物主', '🐉', '祥云龙形手办打到 88 层,鳞片细节稳了。', '4分钟前', 118),
  new PostItem(2, '切片参数党', '⚙️', '0.12 层高 + 树脂支撑,过肩甲零瑕疵。', '16分钟前', 95),
  new PostItem(3, '上色小匠', '🎨', '仕女像披风的渐变终于调出来了!', '32分钟前', 88),
  new PostItem(4, '印语主理人', '🖨️', '本周新到六台光固化,排队缩短一半。', '1小时前', 80),
  new PostItem(5, '微缩城主', '🏯', '古城门楼 1:150,灯串还没通电就封神。', '2小时前', 73),
  new PostItem(6, '打磨强迫症', '🔨', '400 目到 1200 目,水磨是真的治愈。', '4小时前', 61),
  new PostItem(7, '晒件小能手', '📷', '夕阳光下的机甲合影,质感像官图。', '6小时前', 52),
  new PostItem(8, '亲子建模团', '👨‍👩‍👦', '孩子画的小恐龙,我帮她建模打出来了。', '昨天', 47)
];

POST_LIST 包含八条创友动态,每条都使用了真实的 3D 打印术语。从"88 层"“0.12 层高"到"树脂支撑”,从"400 目到 1200 目水磨"到"1:150 微缩",这些专业术语使得应用在首次启动时就具有真实的数字制造社区氛围。创友昵称也极具个性化——“层积造物主”“切片参数党”“打磨强迫症”"微缩城主"等,体现了数字制造创客群体的技术热情和自我表达风格。

const FIGURE_LIST: FigureItem[] = [
  new FigureItem(1, '祥云龙形手办 · 180mm', '0.12 层高 · 树脂', '¥399/只', '典藏', 96),
  new FigureItem(2, '机动装甲 · 可动款', '26 关节 · PLA+', '¥288/只', '联名', 91),
  new FigureItem(3, '国风仕女 · 12 头身', '手工上色 · 渗线', '¥459/只', '限定', 87),
  new FigureItem(4, '微缩古城门楼 · 1:150', '整雕 · 可装灯', '¥329/座', '人气', 82),
  new FigureItem(5, '生肖小兽 · 盲盒款', '随款 · 三色可选', '¥59/只', '入门', 70),
  new FigureItem(6, '星舰巡洋 · 320mm', '一体成型 · 磨砂', '¥399/艘', '新品', 77),
  new FigureItem(7, '古树盆景底座', '木纹耗材 · 手绘', '¥129/个', '手作', 58),
  new FigureItem(8, '机甲驾驶员 · 组_set', '1:24 · 6 只套', '¥199/套', '典藏', 84)
];

FIGURE_LIST 涵盖了从祥云龙形手办到星舰巡洋舰的八种手办产品,价格区间从 ¥59 到 ¥459,level 标签包含"典藏"“联名”“限定”“人气”“入门”“新品”“手作"七种类别。每种手办的 craft 字段都包含了层高、材料和工艺的关键信息,如"0.12 层高 · 树脂”“26 关节 · PLA+”"手工上色 · 渗线"等,为用户提供了充分的专业参考。

const PRINT_LIST: PrintItem[] = [
  new PrintItem(1, '祥云龙形手办', '3 号机 · 层高 0.12 · 剩 2 小时 40 分', '打印中', 62),
  new PrintItem(2, '机动装甲右臂', '切片中 · 支撑自动生成', '切片中', 24),
  new PrintItem(3, '微缩古城门楼', '排队 4 号 · 预计今晚开打', '排队中', 8),
  new PrintItem(4, '国风仕女像', '已上色 · 渗线完成', '已完成', 100),
  new PrintItem(5, '古树盆景底座', '打磨中 · 800 目水磨', '后处理中', 85),
  new PrintItem(6, '星舰巡洋舰体', '排队 9 号 · 树脂槽待补', '排队中', 5),
  new PrintItem(7, '生肖小兽 ×3', '1 号机 · 三色混打', '打印中', 41),
  new PrintItem(8, '驾驶员坐姿件', '已完成 · 待自取', '已完成', 100)
];

PRINT_LIST 是打印任务的静态数据,包含了五种不同的运行状态(打印中、切片中、排队中、已完成、后处理中)。每条数据的 item 字段精确描述了当前任务的详细状态(如"3 号机 · 层高 0.12 · 剩 2 小时 40 分"“切片中 · 支撑自动生成”),使得用户能够清晰了解每个打印任务的实时进度。这种包含机号、层高参数和剩余时间的详细状态描述是 3D 打印管理系统区别于其他任务管理系统的核心特征。

五、顶层纯函数与特效计算逻辑

在这里插入图片描述

应用在顶层定义了多个纯函数,用于特效动画的坐标计算和颜色映射。这些纯函数的设计与其他两个应用保持了相同的架构模式,但函数内部的计算逻辑完全针对 3D 打印场景进行了优化。

function barH(v: number, max: number): number {
  return Math.round(118 * v / max);
}

function ccColor(v: number): string {
  if (v > 12) {
    return '#3FA8A0';
  } else if (v > 8) {
    return '#A78BCC';
  }
  return '#E8843C';
}

function scanX(tick: number): number {
  return (tick * 9) % 600;
}

function scanY(i: number): number {
  return 110 + i * 190;
}

function scanA(tick: number): number {
  return 0.22 + (tick % 4) * 0.11;
}

barH 函数与另两个应用完全一致,用于柱状图高度计算。ccColor 函数是 3D 打印场景的柱状图颜色映射:大于 12 返回主色青色,大于 8 返回耗材色淡紫,其余返回辅色橙色。scanX 函数计算打印头扫描横条的 X 坐标,tick 系数为 9(三个应用中最快),使得扫描横条的水平移动速度最快,模拟了 3D 打印头在 X 轴上的快速往复运动。scanY 函数的 Y 坐标仅依赖索引 i,意味着三条扫描线分别在 Y=110、Y=300、Y=490 的高度水平移动,模拟了打印头在不同层高上的扫描路径。scanA 函数计算扫描横条的透明度,返回 0.22 到 0.55 之间的值(0.22 + 0/1/2/3 * 0.11),实现了扫描线的周期性明暗变化。

function spotX(tick: number, i: number): number {
  return 30 + ((tick * 5 + i * 251) % 610);
}

function spotY(tick: number, i: number): number {
  return 60 + ((tick * 4 + i * 167) % 610);
}

function spotA(tick: number, i: number): number {
  if ((tick + i * 2) % 5 === 0) {
    return 0.85;
  }
  return 0.2;
}

spotX、spotY 和 spotA 三个函数计算耗材光点的位置和透明度。spotA 函数与夜钓应用的 blinkA 函数逻辑完全一致——当 (tick + i * 2) % 5 等于 0 时返回高透明度 0.85(亮),否则返回低透明度 0.2(暗)。这种脉冲式闪烁模拟了 3D 打印过程中耗材丝在打印头加热区的闪烁光效。spotX 和 spotY 使用质数系数(251、167)确保六个光点的运动轨迹各不相同。

function statusColor(s: string): string {
  if (s === '已完成') {
    return '#3FA8A0';
  } else if (s === '打印中') {
    return '#E8843C';
  } else if (s === '切片中') {
    return '#A78BCC';
  } else if (s === '排队中') {
    return '#D9973B';
  }
  return '#6FAF8F';
}

function statusProgress(s: string): number {
  if (s === '已完成') {
    return 100;
  } else if (s === '打印中') {
    return 62;
  } else if (s === '切片中') {
    return 24;
  } else if (s === '排队中') {
    return 8;
  }
  return 85;
}

statusColor 和 statusProgress 函数将打印任务状态映射为颜色和进度值。"已完成"映射为主色青色和 100%,"打印中"映射为辅色橙色和 62%,"切片中"映射为耗材色淡紫和 24%,"排队中"映射为警告黄和 8%,默认(后处理中)映射为成功绿和 85%。这种映射关系反映了 3D 打印的标准工序顺序:排队 -> 切片 -> 打印 -> 后处理 -> 完成,进度值从低到高递增(后处理例外,因为后处理是打印完成后的独立阶段,进度较高)。

function figTagColor(g: string): string {
  if (g.indexOf('龙形') >= 0 || g.indexOf('星舰') >= 0) {
    return '#3FA8A0';
  } else if (g.indexOf('限定') >= 0 || g.indexOf('联名') >= 0) {
    return '#E8843C';
  } else if (g.indexOf('仕女') >= 0 || g.indexOf('城门楼') >= 0) {
    return '#A78BCC';
  }
  return '#D9973B';
}

function levelColor(s: string): string {
  if (s === '入门') {
    return '#6FAF8F';
  } else if (s === '人气') {
    return '#3FA8A0';
  } else if (s === '限定') {
    return '#E8843C';
  } else if (s === '联名') {
    return '#A78BCC';
  } else if (s === '新品') {
    return '#D9973B';
  }
  return '#C0503F';
}

figTagColor 函数通过字符串匹配为手办名称分配标签颜色:含"龙形"或"星舰"的标记为青色,含"限定"或"联名"的标记为橙色,含"仕女"或"城门楼"的标记为淡紫色,其余标记为警告黄。levelColor 函数为六种类别(入门、人气、限定、联名、新品、典藏)分别映射不同的颜色,其中"典藏"默认返回 danger 红色,使得典藏标签具有最强的视觉冲击力。

六、@Entry 组件状态声明与生命周期

在这里插入图片描述

应用的主组件 PagePrintFigures 使用 @Entry 和 @Component 装饰器声明为页面入口组件。

@Entry
@Component
struct PagePrintFigures {
  @State mainTab: number = 0
  @State subTab: number = 0
  @State tick: number = 0
  @State glow: number = 0
  @State addOpen: boolean = false
  @State editOpen: boolean = false
  @State delOpen: boolean = false
  @State printOpen: boolean = false
  @State editIdx: number = 0
  @State delTarget: string = 'task'
  @State editName: string = ''
  @State editStyle: string = ''
  @State editNote: string = ''
  @State addTitle: string = ''
  @State addContent: string = ''
  @State printModel: string = ''
  @State printScale: string = ''
  @State postList: PostItem[] = POST_LIST
  @State printList: PrintItem[] = PRINT_LIST
  @State fxTimer: number = -1

@State 变量的声明与其他两个应用保持高度结构一致。差异在于:printOpen 替代了 charterOpen/tailorOpen(打印委托下单替代包船预约/量体裁衣),printModel 和 printScale 替代了 charterTime/charterSpot 和 tailorStyle/tailorDate,printList 替代了 nightList/orderList(打印任务替代夜钓场次/生产单),delTarget 默认值为 ‘task’(替代 ‘night’/‘order’)。这种命名差异精确反映了 3D 打印场景的业务语义。

  aboutToAppear(): void {
    this.fxTimer = setInterval(() => {
      this.tick = this.tick + 1;
      this.glow = (this.glow + 1) % 2;
    }, 90);
  }

  aboutToDisappear(): void {
    if (this.fxTimer > 0) {
      clearInterval(this.fxTimer);
      this.fxTimer = -1;
    }
  }

生命周期管理与另两个应用完全一致。aboutToAppear 中创建 90 毫秒间隔的定时器,aboutToDisappear 中清理定时器资源。这种跨场景的生命周期管理一致性是 ArkTS 应用架构标准化的体现。

七、弹框操作方法与业务逻辑

在这里插入图片描述

  openAdd(): void {
    this.addTitle = '';
    this.addContent = '';
    this.addOpen = true;
  }

  openEdit(index: number): void {
    if (index >= 0 && index < this.printList.length) {
      this.editIdx = index;
      this.editName = this.printList[index].name;
      this.editStyle = this.printList[index].status;
      this.editNote = this.printList[index].item;
    }
    this.editOpen = true;
  }

  openDelA(): void {
    this.delTarget = 'task';
    this.delOpen = true;
  }

  openDelB(): void {
    this.delTarget = 'course';
    this.delOpen = true;
  }

  openPrint(): void {
    this.printModel = '';
    this.printScale = '';
    this.printOpen = true;
  }

openEdit 方法从 printList 中读取打印任务数据填充编辑表单,openPrint 方法清空打印委托的输入缓存并打开弹框。openDelA 设置删除目标为 ‘task’(打印任务),openDelB 设置删除目标为 ‘course’(课程)。

  doAdd(): void {
    if (this.addTitle.length > 0) {
      this.postList.unshift(new PostItem(999, '我的印语', '🖨️', this.addTitle, '刚刚', 0));
    }
    this.addOpen = false;
  }

  doEdit(): void {
    if (this.editIdx >= 0 && this.editIdx < this.printList.length) {
      this.printList.splice(this.editIdx, 1, new PrintItem(this.printList[this.editIdx].id, this.editName, this.editNote, this.editStyle, 40));
    }
    this.editOpen = false;
  }

  doDel(): void {
    if (this.delTarget === 'task' && this.printList.length > 0) {
      this.printList.splice(0, 1);
    } else if (this.delTarget === 'course' && this.postList.length > 0) {
      this.postList.splice(0, 1);
    }
    this.delOpen = false;
  }

  doPrint(): void {
    if (this.printModel.length > 0) {
      this.postList.unshift(new PostItem(998, '我的印语', '🐉', `已提交打印委托:${this.printModel}`, '刚刚', 0));
    }
    this.printOpen = false;
  }

doEdit 方法使用 splice 替换模式更新打印任务数据,将进度重置为 40%。doDel 方法根据 delTarget 的值决定删除 printList 或 postList 的首元素。doPrint 方法在模型名称非空时生成一条打印委托动态并插入到 postList 头部,使用 🐉 Emoji 作为头像标识。所有 do 方法在执行完业务逻辑后关闭对应弹框。

八、fxLayer 打印头扫描特效层

fxLayer 构建器是 3D 打印场景的视觉核心,通过两组 ForEach 实现了打印头扫描横条和耗材光点闪烁两种特效。

  @Builder
  fxLayer() {
    Stack({ alignContent: Alignment.TopStart }) {
      ForEach([0, 1, 2], (i: number) => {
        Column()
          .width(60)
          .height(3)
          .borderRadius(2)
          .backgroundColor(COLORS.accent)
          .opacity(scanA(this.tick))
          .translate({ x: scanX(this.tick), y: scanY(i) })
      }, (i: number) => i.toString())
      ForEach([0, 1, 2, 3, 4, 5], (i: number) => {
        Column()
          .width(4)
          .height(4)
          .borderRadius(2)
          .backgroundColor(COLORS.filament)
          .opacity(spotA(this.tick, i))
          .translate({ x: spotX(this.tick, i), y: spotY(this.tick, i) })
      }, (i: number) => i.toString())
    }
    .width('100%')
    .height('100%')
    .hitTestBehavior(HitTestMode.None)
  }

fxLayer 使用 Stack 容器内嵌两组 ForEach。第一组是三条打印头扫描横条,每个横条是 60x3 像素的 Column 组件,使用辅色(橙色 #E8843C)背景,透明度由 scanA 函数根据 tick 计算(0.22 到 0.55 之间周期性变化),X 坐标由 scanX 函数根据 tick 计算(每 90ms 移动 9 像素,在 600 像素范围内循环),Y 坐标由 scanY 函数根据索引固定排列(110 + i * 190,分别在 Y=110、Y=300、Y=490 三条水平线上)。这种设计精确模拟了 3D 打印头在 X 轴上的快速往复扫描运动。

第二组是六个耗材光点,每个点为 4x4 像素的耗材色(淡紫色 #A78BCC)圆点,透明度由 spotA 函数实现脉冲式闪烁(0.85 或 0.2),位置由 spotX 和 spotY 函数根据 tick 和索引动态计算。这六个光点模拟了 3D 打印过程中耗材丝在打印头加热区的闪烁效果。

与夜钓和缝纫应用的 fxLayer 相比,3D 打印场景的特效层具有以下显著差异:打印头扫描横条的水平移动速度最快(tick 系数 9 vs 8 vs 3),且移动范围限制在 600 像素内通过取模实现循环,模拟了打印头在打印床宽度范围内的往复运动;耗材光点使用 filament 色(淡紫色)而非 accent 色或 primary 色,是三个应用中唯一使用独立特效色的场景;特效层只有两组 ForEach 而非三组,因为 3D 打印场景的视觉核心是打印头的扫描运动,不需要额外的装饰性特效。

九、header 头部区域与打印队列监控

header 构建器渲染了搜索栏、打印队列卡、新手体验横幅和数据四格。

  @Builder
  header() {
    Column({ space: 10 }) {
      Row({ space: 10 }) {
        Row({ space: 6 }) {
          Text('🔍')
            .fontSize(14)
          Text('搜手办 / 耗材 / 上色服务')
            .fontSize(12)
            .fontColor(COLORS.textHint)
            .layoutWeight(1)
        }
        .layoutWeight(1)
        .padding({ left: 12, right: 12, top: 8, bottom: 8 })
        .backgroundColor(COLORS.cardBg)
        .borderRadius(18)
        Stack({ alignContent: Alignment.TopEnd }) {
          Text('🛒')
            .fontSize(22)
          Text('5')
            .fontSize(9)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
            .padding({ left: 4, right: 4, top: 1, bottom: 1 })
            .backgroundColor(COLORS.danger)
            .borderRadius(8)
            .translate({ x: 6, y: -4 })
        }
        .width(34)
        .height(30)
      }
      .width('100%')

头部首行是搜索栏 + 购物车的组合,与缝纫应用的设计模式一致。搜索栏的占位文本为"搜手办 / 耗材 / 上色服务",购物车角标显示数量"5"(比缝纫的"4"多一个)。这种跨场景的搜索栏设计复用体现了应用架构的高度一致性。

      Row({ space: 12 }) {
        Column({ space: 4 }) {
          Text('3 号机 · 祥云龙形手办 62%')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Text('层高 0.12 · 已打 88 层 · 剩 2 小时 40 分')
            .fontSize(10)
            .fontColor('#E6F4F2')
          Row({ space: 6 }) {
            Text('在线')
              .fontSize(9)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.primaryDark)
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .backgroundColor(COLORS.accent)
              .borderRadius(6)
            Text('本周排产 · 余 8 档')
              .fontSize(9)
              .fontColor('#E6F4F2')
          }
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Text('打印委托')
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .backgroundColor(COLORS.accent)
          .borderRadius(14)
          .onClick(() => {
            this.openPrint();
          })
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.primary)
      .borderRadius(16)

打印队列卡使用主色(青色 #3FA8A0)作为背景,内部文字使用白色和浅青色(#E6F4F2)确保可读性。卡片左侧展示了当前打印状态(“3 号机 · 祥云龙形手办 62%”)和详细参数(“层高 0.12 · 已打 88 层 · 剩 2 小时 40 分”),这些参数精确反映了 3D 打印的核心技术指标——机号、模型名称、完成百分比、层高、已打层数和剩余时间。"在线"标签使用辅色背景和深色文字,"本周排产 · 余 8 档"提供了工坊的排产余量信息。右侧的"打印委托"按钮使用辅色(橙色)背景,点击后调用 openPrint 方法打开打印委托弹框。

      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text('在线机组')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('8 台')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.primary)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('本周交付')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('46 件')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('耗材消耗')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('3.2 kg')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.filament)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('打印币')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('¥128')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.warning)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')

数据四格展示了在线机组(8 台,主色青)、本周交付(46 件,辅色橙)、耗材消耗(3.2 kg,耗材色淡紫)和打印币(¥128,警告黄)。值得注意的是"耗材消耗"使用了耗材色(filament 色 #A78BCC)作为强调色,这是三个应用中唯一将特殊语义色用于数据四格的场景,体现了 3D 打印场景对耗材管理的重视。

十、subNav 层积阶梯式子导航

subNav 构建器是 3D 打印场景的特色导航组件,通过三个高度递减的竖条模拟了 3D 打印中层积堆积的阶梯纹理。

  @Builder
  subNav() {
    Scroll() {
      Row({ space: 10 }) {
        ForEach(SUB_NAV_LIST, (item: string, idx: number) => {
          Column({ space: 3 }) {
            Text(item)
              .fontSize(this.subTab === idx ? 13 : 11)
              .fontWeight(this.subTab === idx ? FontWeight.Bold : FontWeight.Normal)
              .fontColor(this.subTab === idx ? COLORS.primaryDark : COLORS.textSecondary)
            Row({ space: 2 }) {
              Column()
                .width(3)
                .height(this.subTab === idx ? 6 : 3)
                .borderRadius(1)
                .backgroundColor(this.subTab === idx ? COLORS.accent : COLORS.border)
              Column()
                .width(3)
                .height(this.subTab === idx ? 4 : 3)
                .borderRadius(1)
                .backgroundColor(this.subTab === idx ? COLORS.primary : COLORS.border)
              Column()
                .width(3)
                .height(this.subTab === idx ? 2 : 3)
                .borderRadius(1)
                .backgroundColor(this.subTab === idx ? COLORS.filament : COLORS.border)
            }
          }
          .padding({ left: 10, right: 10, top: 6, bottom: 6 })
          .onClick(() => {
            this.subTab = idx;
          })
        }, (item: string) => item)
      }
      .width('100%')
      .alignItems(VerticalAlign.Center)
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
    .padding({ top: 6, bottom: 10 })
  }

与夜钓的"鱼漂顶帽式"和缝纫的"针脚虚线式"不同,3D 打印场景的 subNav 采用"层积阶梯式"设计。每个导航项的 Text 下方排列着三个 3 像素宽的竖条,高度在选中时为 6/4/2(递减阶梯),在未选中时为 3/3/3(等高)。三个竖条分别使用辅色(橙色)、主色(青色)和耗材色(淡紫色)。选中时高度递减的竖条排列模拟了 3D 打印中层积堆积的阶梯纹理——每一层的边缘因材料的堆叠而形成阶梯状的高度差。这种将制造工艺的核心视觉特征(层积阶梯)编码到导航交互设计中的做法,是三个应用中最具技术含量的视觉设计。选中文字使用 primaryDark 色(深青色 #2E7E78),比夜钓和缝纫应用的 accent/primary 色更深,在浅色背景下形成了更沉稳的选中状态。

十一、pageFeatured 精选页面与耗材构成图

pageFeatured 集成了玩法四格、柱状图、耗材构成、手办橱窗和创友动态五个模块。

  @Builder
  pageFeatured() {
    Column({ space: 12 }) {
      Column() {
        Text('在印语能玩什么')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .width('100%')
          .margin({ bottom: 10 })
        Row({ space: 8 }) {
          ForEach(TYPE_LIST, (it: PlayTypeItem) => {
            Column({ space: 4 }) {
              Text(it.icon)
                .fontSize(24)
              Text(it.name)
                .fontSize(11)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.textPrimary)
              Text(it.desc)
                .fontSize(9)
                .fontColor(COLORS.textHint)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(it.hot ? '热门' : ' ')
                .fontSize(9)
                .fontWeight(FontWeight.Bold)
                .fontColor(it.hot ? COLORS.danger : COLORS.border)
            }
            .layoutWeight(1)
            .padding({ top: 10, bottom: 8, left: 4, right: 4 })
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
            .alignItems(HorizontalAlign.Center)
          }, (it: PlayTypeItem) => it.name)
        }
        .width('100%')
      }
      .width('100%')

玩法四格展示了手办定制、模型代涂装、建模课堂和创友市集四种玩法。前两种标记为"热门"(hot: true),使用 danger 红色标签突出显示。每种玩法的 desc 字段包含了简短的服务描述(如"建模 · 打印 · 上色""预约 · 邮寄 · 直播"等)。

      Column() {
        Row() {
          Text('本周每日打印交付单数')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
            .layoutWeight(1)
          Text('单位 单')
            .fontSize(10)
            .fontColor(COLORS.textHint)
        }
        .width('100%')
        Row({ space: 10 }) {
          ForEach(WEEK_CHART, (it: WeekChartItem) => {
            Column({ space: 4 }) {
              Column()
                .width(20)
                .height(barH(it.value, 20))
                .borderRadius(5)
                .backgroundColor(ccColor(it.value))
              Text(`${it.value}`)
                .fontSize(9)
                .fontColor(COLORS.textSecondary)
              Text(it.label)
                .fontSize(9)
                .fontColor(COLORS.textHint)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }, (it: WeekChartItem) => it.label + it.value.toString())
        }
        .width('100%')
        .alignItems(VerticalAlign.Bottom)
        .height(140)
        .margin({ top: 12 })
        Row({ space: 6 }) {
          Text('周末交付单数占比')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
          Text('57%')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
        }
        .width('100%')
        .margin({ top: 10 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)

柱状图模块的最大值参数为 20(另两个应用分别为 12 和 9),因为 3D 打印场景的周末交付单数最高可达 20 单,远高于夜钓的渔获重量和缝纫的完成件数。柱条颜色由 ccColor 函数根据数值大小返回三档颜色:大于 12 为青色、大于 8 为淡紫色、其余为橙色。柱状图下方显示了"周末交付单数占比 57%"的统计信息,使用辅色橙色突出显示。

      Column() {
        Text('工坊耗材消耗构成')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .width('100%')
        ForEach(MAT_CHART, (it: MatChartItem) => {
          Row({ space: 8 }) {
            Text(it.label)
              .fontSize(12)
              .fontColor(COLORS.textSecondary)
              .width(76)
            Stack({ alignContent: Alignment.Start }) {
              Row()
                .width('100%')
                .height(8)
                .borderRadius(4)
                .backgroundColor(COLORS.border)
              Row()
                .width(`${it.value}%`)
                .height(8)
                .borderRadius(4)
                .backgroundColor(it.color)
            }
            .layoutWeight(1)
            Text(`${it.value}%`)
              .fontSize(11)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
              .width(44)
              .textAlign(TextAlign.End)
          }
          .width('100%')
          .margin({ top: 8 })
        }, (it: MatChartItem) => it.label)
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)

耗材消耗构成图使用 ForEach 遍历 MAT_CHART 数组,展示了五种耗材的消耗占比:PLA+(38%,青色)、光敏树脂(24%,淡紫色)、TPU 弹性(14%,橙色)、PETG 透明(12%,成功绿)、木纹耗材(12%,警告黄)。每种耗材使用独立的颜色,形成了多色的水平条形图。这个图表是 3D 打印场景特有的数据可视化模块,帮助用户了解工坊的耗材使用结构和成本分布。

十二、pageCrafts 打印任务管理与进度可视化

pageCrafts 页面展示用户的打印任务列表,每条任务包含状态标签、进度条和编辑入口。

  @Builder
  pageCrafts() {
    Column({ space: 10 }) {
      Text('我的打印')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
      ForEach(this.printList, (it: PrintItem, idx: number) => {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            Text('🖨️')
              .fontSize(24)
            Column({ space: 4 }) {
              Row() {
                Text(it.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                  .layoutWeight(1)
                Text(it.status)
                  .fontSize(10)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(statusColor(it.status))
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .backgroundColor(COLORS.accentLight)
                  .borderRadius(6)
              }
              .width('100%')
              Text(`🧾 ${it.item}`)
                .fontSize(11)
                .fontColor(COLORS.textSecondary)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%')
          Stack({ alignContent: Alignment.Start }) {
            Row()
              .width('100%')
              .height(6)
              .borderRadius(3)
              .backgroundColor(COLORS.border)
            Row()
              .width(`${statusProgress(it.status)}%`)
              .height(6)
              .borderRadius(3)
              .backgroundColor(statusColor(it.status))
          }
          .width('100%')
          Row() {
            Text(`任务进度 ${statusProgress(it.status)}%`)
              .fontSize(10)
              .fontColor(COLORS.textHint)
              .layoutWeight(1)
            Text('编辑打印单')
              .fontSize(11)
              .fontColor(COLORS.primary)
              .onClick(() => {
                this.openEdit(idx);
              })
          }
          .width('100%')
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(12)
      }, (it: PrintItem) => it.id.toString())
    }
    .width('100%')
  }

每条打印任务卡片使用 Column 垂直布局,结构与夜钓和缝纫应用的场次/订单卡片保持一致。顶部 Row 展示打印机图标(🖨️)和任务信息(名称、状态标签、详细参数描述),中间是 Stack 实现的进度条,底部 Row 展示进度文字和编辑入口。状态标签使用 accentLight 色作为背景,与 statusColor 返回的文字色形成柔和的标签效果。item 字段的详细参数(如"3 号机 · 层高 0.12 · 剩 2 小时 40 分")提供了打印任务的技术细节,使用户能够实时监控打印进度。"编辑打印单"入口通过 onClick 调用 openEdit(idx) 方法打开编辑弹框。

十三、pageWorkshop 打印工坊与实时监控

pageWorkshop 页面是 3D 打印场景的特色模块,展示了工坊的实时运行参数和耗材排行。

  @Builder
  pageWorkshop() {
    Column({ space: 10 }) {
      Text('打印工坊')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text('车间温度')
            .fontSize(10)
            .fontColor(COLORS.textHint)
          Text('24℃')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.primary)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('树脂槽余量')
            .fontSize(10)
            .fontColor(COLORS.textHint)
          Text('68%')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('今日开机')
            .fontSize(10)
            .fontColor(COLORS.textHint)
          Text('9.5 h')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.filament)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('失败率')
            .fontSize(10)
            .fontColor(COLORS.textHint)
          Text('1.2%')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.success)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')

打印工坊的四格数据展示了车间温度(24℃,主色青)、树脂槽余量(68%,辅色橙)、今日开机时长(9.5 h,耗材色淡紫)和失败率(1.2%,成功绿)。这四个参数是 3D 打印工坊运行监控的核心指标——温度影响打印质量、树脂余量影响连续打印能力、开机时长反映工坊利用率、失败率衡量打印成功率。"今日开机"使用耗材色作为强调色,与 header 数据四格中"耗材消耗"使用耗材色的设计一致,强化了耗材色在 3D 打印场景中的语义关联。

十四、弹框组件构建器群

应用定义了四个 ModalBody 构建器,分别对应发布印语、编辑打印单、取消打印任务和打印委托下单四个弹框。

  @Builder
  addModalBody() {
    Column({ space: 12 }) {
      Text('发布印语')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      TextInput({ placeholder: '一句话造物日常…', text: this.addTitle })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.addTitle = v;
        })
      TextArea({ placeholder: '补充细节:模型、参数、上色心得…', text: this.addContent })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(90)
        .onChange((v: string) => {
          this.addContent = v;
        })
      Row({ space: 10 }) {
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.bg)
          .borderRadius(8)
          .onClick(() => {
            this.addOpen = false;
          })
        Text('取消')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .margin({ left: -52 })
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.primary)
          .borderRadius(8)
          .onClick(() => {
            this.doAdd();
          })
        Text('发布')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .margin({ left: -44 })
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(COLORS.cardBg)
  }

addModalBody 构建器渲染发布印语弹框,占位文本为"一句话造物日常…“和"补充细节:模型、参数、上色心得…”,体现了 3D 打印创客的分享内容特征。发布按钮使用 primary 色背景,与另两个应用的弹框设计保持一致。

  @Builder
  delModalBody() {
    Column({ space: 12 }) {
      Text(this.delTarget === 'task' ? '取消首个打印任务' : '退出建模课堂')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      Text(this.delTarget === 'task' ? '将取消首个打印任务并回收已用耗材,确认执行?' : '将退出首门进行中的课程,确认执行?')
        .fontSize(13)
        .fontColor(COLORS.textSecondary)
      Row({ space: 10 }) {
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.bg)
          .borderRadius(8)
          .onClick(() => {
            this.delOpen = false;
          })
        Text('取消')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .margin({ left: -52 })
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.danger)
          .borderRadius(8)
          .onClick(() => {
            this.doDel();
          })
        Text('确认')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .margin({ left: -44 })
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(COLORS.cardBg)
  }

delModalBody 构建器根据 delTarget 的值动态显示"取消首个打印任务"或"退出建模课堂"的标题和描述文本。描述文本中的"回收已用耗材"是 3D 打印场景的专业术语,体现了取消打印任务后耗材回收的业务逻辑。确认按钮使用 danger 红色背景,强调取消操作的不可逆性。

  @Builder
  printModalBody() {
    Column({ space: 12 }) {
      Text('打印委托下单')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      TextInput({ placeholder: '模型名称(如 祥云龙形手办)', text: this.printModel })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.printModel = v;
        })
      TextInput({ placeholder: '比例尺寸(如 1/8 · 180mm)', text: this.printScale })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.printScale = v;
        })
      Text('工程师审核切片后 30 分钟内回电报价 · 含支撑与后处理')
        .fontSize(11)
        .fontColor(COLORS.textHint)
        .width('100%')
      Row({ space: 10 }) {
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.bg)
          .borderRadius(8)
          .onClick(() => {
            this.printOpen = false;
          })
        Text('取消')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .margin({ left: -52 })
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.accent)
          .borderRadius(8)
          .onClick(() => {
            this.doPrint();
          })
        Text('提交委托')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .margin({ left: -56 })
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(COLORS.cardBg)
  }

printModalBody 构建器渲染打印委托下单弹框,包含模型名称和比例尺寸两个输入框。模型名称的占位文本为"模型名称(如 祥云龙形手办)“,比例尺寸的占位文本为"比例尺寸(如 1/8 · 180mm)”,这些占位示例为用户提供了输入格式的参考。说明文本"工程师审核切片后 30 分钟内回电报价 · 含支撑与后处理"提供了委托流程和包含服务的详细信息。提交按钮使用辅色(橙色)背景,与发布印语弹框的主色背景形成视觉区分。

十五、build 方法主架构与层级渲染

  build() {
    Stack() {
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor(COLORS.bg)
      this.fxLayer()
      Column() {
        this.mainContent()
        this.bottomBar()
      }
      .width('100%')
      .height('100%')
      if (this.addOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.addModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
      if (this.editOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.editModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
      if (this.delOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.delModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
      if (this.printOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.printModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }

build 方法的 Stack 容器结构与另两个应用完全一致:背景层(bg 色 #F2F7F6)、特效层(fxLayer)、主内容层(mainContent + bottomBar)和四个条件渲染的弹框层。modalOverlay 的遮罩透明度为 0.6(与缝纫应用一致,比夜钓的 0.7 低)。四个弹框的条件渲染使用 if 语句检查对应的 @State 布尔值(addOpen、editOpen、delOpen、printOpen),实现了弹框的按需显示。每个弹框层的结构完全一致——Stack 包含 modalOverlay 遮罩和一个居中的弹框内容容器(宽度 88%,最大高度 80%,圆角 16,zIndex 999)。

十六、Mermaid 架构流程图

以下是印语·3D打印手办与模型上色工坊应用的组件架构与数据流图:

渲染错误: Mermaid 渲染失败: Parse error on line 42: ...ft] PL --> O1[@Observed PostIte ---------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'LINK_ID'

十七、数据模型与组件对比表格

维度 PostItem FigureItem PrintItem GearItem
装饰器 @Observed @Observed @Observed @Observed
业务领域 创友社区动态 手办橱窗展示 打印任务管理 材料库管理
字段数量 6 6 5 4
核心字段 text, likes name, price, hot status, progress name, desc
静态数组 POST_LIST (8条) FIGURE_LIST (8条) PRINT_LIST (8条) GEAR_LIST (8条)
可变操作 unshift/splice 只读 splice替换/splice删除 只读
颜色映射函数 - figTagColor, levelColor statusColor, statusProgress -
关联弹框 addModalBody - editModalBody, delModalBody -
状态枚举 - 典藏/联名/限定/人气/入门/新品/手作 打印中/切片中/排队中/已完成/后处理中 -
特殊字段 avatar (Emoji) craft, level item (含机号+层高+剩余时间) icon (Emoji)
维度 fxLayer 特效层 header 头部 subNav 子导航 modalOverlay 遮罩
构建器类型 @Builder @Builder @Builder @Builder
容器组件 Stack Column Scroll > Row Stack
渲染元素 扫描横条+耗材光点 搜索栏+队列卡+体验横幅+数据格 7个层积阶梯式Tab 全屏半透明黑色
动画驱动 tick (90ms)
点击行为 HitTestMode.None 打印委托跳转 subTab切换 关闭所有弹框
视觉特色 accent色横条+filament色光点 primary色队列卡+搜索栏 高度递减竖条模拟层积 opacity 0.6
与其他应用差异 仅两组ForEach+最快扫描速度 增加车间温度等工坊参数 阶梯式高度变化+三色竖条 透明度与缝纫一致
数据依赖 tick, glow 静态文本 SUB_NAV_LIST
对比维度 938 浮语夜钓 939 衣语缝纫 940 印语3D打印
主色/辅色 #4E9DBA / #4CC97E #B5546B / #57876B #3FA8A0 / #E8843C
主题类型 暗色 (bg:#0F1620) 暖色浅色 (bg:#FAF4F2) 科技浅色 (bg:#F2F7F6)
特殊色 lure #8FD8FF (诱饵) thread #D9A08C (线色) filament #A78BCC (耗材)
特效组成 涟漪+信号灯+光点(3组) 针线+线轴+光点(3组) 扫描条+光点(2组)
子导航风格 鱼漂顶帽式(短横+竖条) 针脚虚线式(三色微点) 层积阶梯式(递减竖条)
核心数据模型 NightItem(夜钓场次) OrderItem(生产单) PrintItem(打印任务)
预约弹框 charterModalBody(包船) tailorModalBody(量体) printModalBody(打印委托)
删除弹框 收竿撤场/退课 撤销生产单/退课 取消打印任务/退课
状态枚举 已收竿/搏鱼中/守口中/换饵中/打窝中 已取件/缝制中/裁剪中/制版中/排队中 已完成/打印中/切片中/排队中/后处理中

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 场景:手办定制 / 打印排产 / 模型上色 / 创友社区
// 底部4主tab(首页/橱窗/课堂/我的)+ 首页7内容tab(层积阶梯式)
// 特效:打印头扫描横条 + 耗材光点闪烁(特效层位于底层,不遮挡点击)
// 弹框:新增(发印语) / 编辑(编辑打印单) / 删除(取消打印任务·退课) / 委托(打印委托下单)

interface ColorPalette {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  bg: string;
  cardBg: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  border: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
  filament: string;
}

const COLORS: ColorPalette = {
  primary: '#3FA8A0',
  primaryLight: '#E2F2F1',
  primaryDark: '#2E7E78',
  accent: '#E8843C',
  accentLight: '#FCEDE1',
  bg: '#F2F7F6',
  cardBg: '#FFFFFF',
  textPrimary: '#2E3A38',
  textSecondary: '#64736F',
  textHint: '#AEBCB8',
  border: '#DFEAE8',
  success: '#6FAF8F',
  warning: '#D9973B',
  danger: '#C0503F',
  white: '#FFFFFF',
  filament: '#A78BCC'
};

// ============ 创友动态数据模型 ============
@Observed
export class PostItem {
  id: number = 0
  nick: string = ''
  avatar: string = ''
  text: string = ''
  time: string = ''
  likes: number = 0
  constructor(id: number, nick: string, avatar: string, text: string, time: string, likes: number) {
    this.id = id; this.nick = nick; this.avatar = avatar; this.text = text
    this.time = time; this.likes = likes
  }
}

// ============ 手办数据模型 ============
@Observed
export class FigureItem {
  id: number = 0
  name: string = ''
  craft: string = ''
  price: string = ''
  level: string = ''
  hot: number = 0
  constructor(id: number, name: string, craft: string, price: string, level: string, hot: number) {
    this.id = id; this.name = name; this.craft = craft; this.price = price
    this.level = level; this.hot = hot
  }
}

// ============ 打印任务数据模型 ============
@Observed
export class PrintItem {
  id: number = 0
  name: string = ''
  item: string = ''
  status: string = ''
  progress: number = 0
  constructor(id: number, name: string, item: string, status: string, progress: number) {
    this.id = id; this.name = name; this.item = item; this.status = status
    this.progress = progress
  }
}

// ============ 材料数据模型 ============
@Observed
export class GearItem {
  id: number = 0
  name: string = ''
  desc: string = ''
  icon: string = ''
  constructor(id: number, name: string, desc: string, icon: string) {
    this.id = id; this.name = name; this.desc = desc; this.icon = icon
  }
}

// ============ 图表与课程接口 ============
interface WeekChartItem {
  label: string;
  value: number;
}

interface MatChartItem {
  label: string;
  value: number;
  color: string;
}

interface CourseItem {
  title: string;
  week: string;
  progress: number;
  color: string;
}

interface PlayTypeItem {
  name: string;
  desc: string;
  icon: string;
  hot: boolean;
}

// ============ 顶层静态数据 ============
const POST_LIST: PostItem[] = [
  new PostItem(1, '层积造物主', '🐉', '祥云龙形手办打到 88 层,鳞片细节稳了。', '4分钟前', 118),
  new PostItem(2, '切片参数党', '⚙️', '0.12 层高 + 树脂支撑,过肩甲零瑕疵。', '16分钟前', 95),
  new PostItem(3, '上色小匠', '🎨', '仕女像披风的渐变终于调出来了!', '32分钟前', 88),
  new PostItem(4, '印语主理人', '🖨️', '本周新到六台光固化,排队缩短一半。', '1小时前', 80),
  new PostItem(5, '微缩城主', '🏯', '古城门楼 1:150,灯串还没通电就封神。', '2小时前', 73),
  new PostItem(6, '打磨强迫症', '🔨', '400 目到 1200 目,水磨是真的治愈。', '4小时前', 61),
  new PostItem(7, '晒件小能手', '📷', '夕阳光下的机甲合影,质感像官图。', '6小时前', 52),
  new PostItem(8, '亲子建模团', '👨‍👩‍👦', '孩子画的小恐龙,我帮她建模打出来了。', '昨天', 47)
];

const FIGURE_LIST: FigureItem[] = [
  new FigureItem(1, '祥云龙形手办 · 180mm', '0.12 层高 · 树脂', '¥399/只', '典藏', 96),
  new FigureItem(2, '机动装甲 · 可动款', '26 关节 · PLA+', '¥288/只', '联名', 91),
  new FigureItem(3, '国风仕女 · 12 头身', '手工上色 · 渗线', '¥459/只', '限定', 87),
  new FigureItem(4, '微缩古城门楼 · 1:150', '整雕 · 可装灯', '¥329/座', '人气', 82),
  new FigureItem(5, '生肖小兽 · 盲盒款', '随款 · 三色可选', '¥59/只', '入门', 70),
  new FigureItem(6, '星舰巡洋 · 320mm', '一体成型 · 磨砂', '¥399/艘', '新品', 77),
  new FigureItem(7, '古树盆景底座', '木纹耗材 · 手绘', '¥129/个', '手作', 58),
  new FigureItem(8, '机甲驾驶员 · 组_set', '1:24 · 6 只套', '¥199/套', '典藏', 84)
];

const PRINT_LIST: PrintItem[] = [
  new PrintItem(1, '祥云龙形手办', '3 号机 · 层高 0.12 · 剩 2 小时 40 分', '打印中', 62),
  new PrintItem(2, '机动装甲右臂', '切片中 · 支撑自动生成', '切片中', 24),
  new PrintItem(3, '微缩古城门楼', '排队 4 号 · 预计今晚开打', '排队中', 8),
  new PrintItem(4, '国风仕女像', '已上色 · 渗线完成', '已完成', 100),
  new PrintItem(5, '古树盆景底座', '打磨中 · 800 目水磨', '后处理中', 85),
  new PrintItem(6, '星舰巡洋舰体', '排队 9 号 · 树脂槽待补', '排队中', 5),
  new PrintItem(7, '生肖小兽 ×3', '1 号机 · 三色混打', '打印中', 41),
  new PrintItem(8, '驾驶员坐姿件', '已完成 · 待自取', '已完成', 100)
];

const GEAR_LIST: GearItem[] = [
  new GearItem(1, 'PLA+ 耗材 · 1kg', '科技青 · ±0.02', '🧵'),
  new GearItem(2, '光敏树脂 · 500g', '高精度 · 低味', '🧪'),
  new GearItem(3, '硬质合金喷嘴 · 0.4', '耐磨 · 长寿命', '🔩'),
  new GearItem(4, '斜口剪钳 · 模型', '薄刃 · 断支撑', '✂️'),
  new GearItem(5, '砂纸组 · 400-1200', '水磨 · 干磨两用', '📄'),
  new GearItem(6, '水补土喷雾 · 1000 目', '打底 · 灰白两色', '🥫'),
  new GearItem(7, '丙烯颜料 · 12 色', '手办上色 · 高遮盖', '🎨'),
  new GearItem(8, 'UV 手办胶 · 快固', '缝隙修复 · 5 秒', '💧')
];

const WEEK_CHART: WeekChartItem[] = [
  { label: '周一', value: 5 },
  { label: '周二', value: 7 },
  { label: '周三', value: 6 },
  { label: '周四', value: 9 },
  { label: '周五', value: 12 },
  { label: '周六', value: 20 },
  { label: '周日', value: 16 }
];

const MAT_CHART: MatChartItem[] = [
  { label: 'PLA+', value: 38, color: '#3FA8A0' },
  { label: '光敏树脂', value: 24, color: '#A78BCC' },
  { label: 'TPU 弹性', value: 14, color: '#E8843C' },
  { label: 'PETG 透明', value: 12, color: '#6FAF8F' },
  { label: '木纹耗材', value: 12, color: '#D9973B' }
];

const TYPE_LIST: PlayTypeItem[] = [
  { name: '手办定制', desc: '建模 · 打印 · 上色', icon: '🐉', hot: true },
  { name: '模型代涂装', desc: '预约 · 邮寄 · 直播', icon: '🎨', hot: true },
  { name: '建模课堂', desc: '零基础 · 8 节课', icon: '📐', hot: false },
  { name: '创友市集', desc: '二手模型 · 周末开市', icon: '🏪', hot: false }
];

const COURSE_LIST: CourseItem[] = [
  { title: '切片参数与支撑入门', week: '第 1 周', progress: 100, color: '#3FA8A0' },
  { title: '手办建模与布尔运算', week: '第 2 周', progress: 88, color: '#A78BCC' },
  { title: 'PLA / 树脂材料学', week: '第 3 周', progress: 62, color: '#E8843C' },
  { title: '手办打磨与水补土', week: '第 4 周', progress: 34, color: '#6FAF8F' },
  { title: '丙烯上色与渗线技法', week: '第 5 周', progress: 11, color: '#D9973B' }
];

// ============ 导航配置 ============
interface NavItem {
  icon: string;
  label: string;
}

const NAV_LIST: NavItem[] = [
  { icon: '🖨️', label: '首页' },
  { icon: '🐉', label: '橱窗' },
  { icon: '📐', label: '课堂' },
  { icon: '👤', label: '我的' }
];

const SUB_NAV_LIST: string[] = ['精选', '手办橱窗', '我的打印', '打印工坊', '建模课堂', '创友圈', '材料库'];

// ============ 顶层纯函数 ============
function barH(v: number, max: number): number {
  return Math.round(118 * v / max);
}

function ccColor(v: number): string {
  if (v > 12) {
    return '#3FA8A0';
  } else if (v > 8) {
    return '#A78BCC';
  }
  return '#E8843C';
}

function scanX(tick: number): number {
  return (tick * 9) % 600;
}

function scanY(i: number): number {
  return 110 + i * 190;
}

function scanA(tick: number): number {
  return 0.22 + (tick % 4) * 0.11;
}

function spotX(tick: number, i: number): number {
  return 30 + ((tick * 5 + i * 251) % 610);
}

function spotY(tick: number, i: number): number {
  return 60 + ((tick * 4 + i * 167) % 610);
}

function spotA(tick: number, i: number): number {
  if ((tick + i * 2) % 5 === 0) {
    return 0.85;
  }
  return 0.2;
}

function statusColor(s: string): string {
  if (s === '已完成') {
    return '#3FA8A0';
  } else if (s === '打印中') {
    return '#E8843C';
  } else if (s === '切片中') {
    return '#A78BCC';
  } else if (s === '排队中') {
    return '#D9973B';
  }
  return '#6FAF8F';
}

function statusProgress(s: string): number {
  if (s === '已完成') {
    return 100;
  } else if (s === '打印中') {
    return 62;
  } else if (s === '切片中') {
    return 24;
  } else if (s === '排队中') {
    return 8;
  }
  return 85;
}

function figTagColor(g: string): string {
  if (g.indexOf('龙形') >= 0 || g.indexOf('星舰') >= 0) {
    return '#3FA8A0';
  } else if (g.indexOf('限定') >= 0 || g.indexOf('联名') >= 0) {
    return '#E8843C';
  } else if (g.indexOf('仕女') >= 0 || g.indexOf('城门楼') >= 0) {
    return '#A78BCC';
  }
  return '#D9973B';
}

function levelColor(s: string): string {
  if (s === '入门') {
    return '#6FAF8F';
  } else if (s === '人气') {
    return '#3FA8A0';
  } else if (s === '限定') {
    return '#E8843C';
  } else if (s === '联名') {
    return '#A78BCC';
  } else if (s === '新品') {
    return '#D9973B';
  }
  return '#C0503F';
}

@Entry
@Component
struct PagePrintFigures {
  @State mainTab: number = 0
  @State subTab: number = 0
  @State tick: number = 0
  @State glow: number = 0
  @State addOpen: boolean = false
  @State editOpen: boolean = false
  @State delOpen: boolean = false
  @State printOpen: boolean = false
  @State editIdx: number = 0
  @State delTarget: string = 'task'
  @State editName: string = ''
  @State editStyle: string = ''
  @State editNote: string = ''
  @State addTitle: string = ''
  @State addContent: string = ''
  @State printModel: string = ''
  @State printScale: string = ''
  @State postList: PostItem[] = POST_LIST
  @State printList: PrintItem[] = PRINT_LIST
  @State fxTimer: number = -1

  aboutToAppear(): void {
    this.fxTimer = setInterval(() => {
      this.tick = this.tick + 1;
      this.glow = (this.glow + 1) % 2;
    }, 90);
  }

  aboutToDisappear(): void {
    if (this.fxTimer > 0) {
      clearInterval(this.fxTimer);
      this.fxTimer = -1;
    }
  }

  openAdd(): void {
    this.addTitle = '';
    this.addContent = '';
    this.addOpen = true;
  }

  openEdit(index: number): void {
    if (index >= 0 && index < this.printList.length) {
      this.editIdx = index;
      this.editName = this.printList[index].name;
      this.editStyle = this.printList[index].status;
      this.editNote = this.printList[index].item;
    }
    this.editOpen = true;
  }

  openDelA(): void {
    this.delTarget = 'task';
    this.delOpen = true;
  }

  openDelB(): void {
    this.delTarget = 'course';
    this.delOpen = true;
  }

  openPrint(): void {
    this.printModel = '';
    this.printScale = '';
    this.printOpen = true;
  }

  doAdd(): void {
    if (this.addTitle.length > 0) {
      this.postList.unshift(new PostItem(999, '我的印语', '🖨️', this.addTitle, '刚刚', 0));
    }
    this.addOpen = false;
  }

  doEdit(): void {
    if (this.editIdx >= 0 && this.editIdx < this.printList.length) {
      this.printList.splice(this.editIdx, 1, new PrintItem(this.printList[this.editIdx].id, this.editName, this.editNote, this.editStyle, 40));
    }
    this.editOpen = false;
  }

  doDel(): void {
    if (this.delTarget === 'task' && this.printList.length > 0) {
      this.printList.splice(0, 1);
    } else if (this.delTarget === 'course' && this.postList.length > 0) {
      this.postList.splice(0, 1);
    }
    this.delOpen = false;
  }

  doPrint(): void {
    if (this.printModel.length > 0) {
      this.postList.unshift(new PostItem(998, '我的印语', '🐉', `已提交打印委托:${this.printModel}`, '刚刚', 0));
    }
    this.printOpen = false;
  }

  @Builder
  fxLayer() {
    Stack({ alignContent: Alignment.TopStart }) {
      ForEach([0, 1, 2], (i: number) => {
        Column()
          .width(60)
          .height(3)
          .borderRadius(2)
          .backgroundColor(COLORS.accent)
          .opacity(scanA(this.tick))
          .translate({ x: scanX(this.tick), y: scanY(i) })
      }, (i: number) => i.toString())
      ForEach([0, 1, 2, 3, 4, 5], (i: number) => {
        Column()
          .width(4)
          .height(4)
          .borderRadius(2)
          .backgroundColor(COLORS.filament)
          .opacity(spotA(this.tick, i))
          .translate({ x: spotX(this.tick, i), y: spotY(this.tick, i) })
      }, (i: number) => i.toString())
    }
    .width('100%')
    .height('100%')
    .hitTestBehavior(HitTestMode.None)
  }

  @Builder
  header() {
    Column({ space: 10 }) {
      // 搜索条 + 购物车
      Row({ space: 10 }) {
        Row({ space: 6 }) {
          Text('🔍')
            .fontSize(14)
          Text('搜手办 / 耗材 / 上色服务')
            .fontSize(12)
            .fontColor(COLORS.textHint)
            .layoutWeight(1)
        }
        .layoutWeight(1)
        .padding({ left: 12, right: 12, top: 8, bottom: 8 })
        .backgroundColor(COLORS.cardBg)
        .borderRadius(18)
        Stack({ alignContent: Alignment.TopEnd }) {
          Text('🛒')
            .fontSize(22)
          Text('5')
            .fontSize(9)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
            .padding({ left: 4, right: 4, top: 1, bottom: 1 })
            .backgroundColor(COLORS.danger)
            .borderRadius(8)
            .translate({ x: 6, y: -4 })
        }
        .width(34)
        .height(30)
      }
      .width('100%')

      // 打印队列卡
      Row({ space: 12 }) {
        Column({ space: 4 }) {
          Text('3 号机 · 祥云龙形手办 62%')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Text('层高 0.12 · 已打 88 层 · 剩 2 小时 40 分')
            .fontSize(10)
            .fontColor('#E6F4F2')
          Row({ space: 6 }) {
            Text('在线')
              .fontSize(9)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.primaryDark)
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .backgroundColor(COLORS.accent)
              .borderRadius(6)
            Text('本周排产 · 余 8 档')
              .fontSize(9)
              .fontColor('#E6F4F2')
          }
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Text('打印委托')
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .backgroundColor(COLORS.accent)
          .borderRadius(14)
          .onClick(() => {
            this.openPrint();
          })
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.primary)
      .borderRadius(16)

      // 新手体验横幅
      Row({ space: 10 }) {
        Text('🆕')
          .fontSize(22)
        Column({ space: 2 }) {
          Text('新手体验 · 首单打印 9.9 元')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('周六下午 · 免费 30 分钟建模指导 · 送支撑剪')
            .fontSize(10)
            .fontColor(COLORS.textSecondary)
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        Text('去报名')
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .padding({ left: 14, right: 14, top: 7, bottom: 7 })
          .backgroundColor(COLORS.primary)
          .borderRadius(14)
          .onClick(() => {
            this.openPrint();
          })
      }
      .width('100%')
      .padding(12)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)

      // 数据四格
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text('在线机组')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('8 台')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.primary)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('本周交付')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('46 件')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('耗材消耗')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('3.2 kg')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.filament)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('打印币')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('¥128')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.warning)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
    }
    .width('100%')
    .padding({ top: 10, bottom: 4 })
  }

  @Builder
  subNav() {
    Scroll() {
      Row({ space: 10 }) {
        ForEach(SUB_NAV_LIST, (item: string, idx: number) => {
          Column({ space: 3 }) {
            Text(item)
              .fontSize(this.subTab === idx ? 13 : 11)
              .fontWeight(this.subTab === idx ? FontWeight.Bold : FontWeight.Normal)
              .fontColor(this.subTab === idx ? COLORS.primaryDark : COLORS.textSecondary)
            Row({ space: 2 }) {
              Column()
                .width(3)
                .height(this.subTab === idx ? 6 : 3)
                .borderRadius(1)
                .backgroundColor(this.subTab === idx ? COLORS.accent : COLORS.border)
              Column()
                .width(3)
                .height(this.subTab === idx ? 4 : 3)
                .borderRadius(1)
                .backgroundColor(this.subTab === idx ? COLORS.primary : COLORS.border)
              Column()
                .width(3)
                .height(this.subTab === idx ? 2 : 3)
                .borderRadius(1)
                .backgroundColor(this.subTab === idx ? COLORS.filament : COLORS.border)
            }
          }
          .padding({ left: 10, right: 10, top: 6, bottom: 6 })
          .onClick(() => {
            this.subTab = idx;
          })
        }, (item: string) => item)
      }
      .width('100%')
      .alignItems(VerticalAlign.Center)
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
    .padding({ top: 6, bottom: 10 })
  }

  @Builder
  pageFeatured() {
    Column({ space: 12 }) {
      // 玩法四格
      Column() {
        Text('在印语能玩什么')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .width('100%')
          .margin({ bottom: 10 })
        Row({ space: 8 }) {
          ForEach(TYPE_LIST, (it: PlayTypeItem) => {
            Column({ space: 4 }) {
              Text(it.icon)
                .fontSize(24)
              Text(it.name)
                .fontSize(11)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.textPrimary)
              Text(it.desc)
                .fontSize(9)
                .fontColor(COLORS.textHint)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(it.hot ? '热门' : ' ')
                .fontSize(9)
                .fontWeight(FontWeight.Bold)
                .fontColor(it.hot ? COLORS.danger : COLORS.border)
            }
            .layoutWeight(1)
            .padding({ top: 10, bottom: 8, left: 4, right: 4 })
            .backgroundColor(COLORS.cardBg)
            .borderRadius(12)
            .alignItems(HorizontalAlign.Center)
          }, (it: PlayTypeItem) => it.name)
        }
        .width('100%')
      }
      .width('100%')

      // 本周交付柱状图
      Column() {
        Row() {
          Text('本周每日打印交付单数')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
            .layoutWeight(1)
          Text('单位 单')
            .fontSize(10)
            .fontColor(COLORS.textHint)
        }
        .width('100%')
        Row({ space: 10 }) {
          ForEach(WEEK_CHART, (it: WeekChartItem) => {
            Column({ space: 4 }) {
              Column()
                .width(20)
                .height(barH(it.value, 20))
                .borderRadius(5)
                .backgroundColor(ccColor(it.value))
              Text(`${it.value}`)
                .fontSize(9)
                .fontColor(COLORS.textSecondary)
              Text(it.label)
                .fontSize(9)
                .fontColor(COLORS.textHint)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }, (it: WeekChartItem) => it.label + it.value.toString())
        }
        .width('100%')
        .alignItems(VerticalAlign.Bottom)
        .height(140)
        .margin({ top: 12 })
        Row({ space: 6 }) {
          Text('周末交付单数占比')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
          Text('57%')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
        }
        .width('100%')
        .margin({ top: 10 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)

      // 耗材构成
      Column() {
        Text('工坊耗材消耗构成')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .width('100%')
        ForEach(MAT_CHART, (it: MatChartItem) => {
          Row({ space: 8 }) {
            Text(it.label)
              .fontSize(12)
              .fontColor(COLORS.textSecondary)
              .width(76)
            Stack({ alignContent: Alignment.Start }) {
              Row()
                .width('100%')
                .height(8)
                .borderRadius(4)
                .backgroundColor(COLORS.border)
              Row()
                .width(`${it.value}%`)
                .height(8)
                .borderRadius(4)
                .backgroundColor(it.color)
            }
            .layoutWeight(1)
            Text(`${it.value}%`)
              .fontSize(11)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
              .width(44)
              .textAlign(TextAlign.End)
          }
          .width('100%')
          .margin({ top: 8 })
        }, (it: MatChartItem) => it.label)
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)

      // 人气手办双列
      Column() {
        Row() {
          Text('人气手办橱窗')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
            .layoutWeight(1)
          Text('查看全部')
            .fontSize(11)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        .margin({ bottom: 10 })
        ForEach(FIGURE_LIST, (it: FigureItem, idx: number) => {
          if (idx % 2 === 0) {
            Row({ space: 10 }) {
              if (idx < FIGURE_LIST.length) {
                Column({ space: 6 }) {
                  Row() {
                    Text('🐉')
                      .fontSize(22)
                      .layoutWeight(1)
                    Text(FIGURE_LIST[idx].level)
                      .fontSize(10)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(levelColor(FIGURE_LIST[idx].level))
                      .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                      .backgroundColor(COLORS.accentLight)
                      .borderRadius(6)
                  }
                  .width('100%')
                  Text(FIGURE_LIST[idx].name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(`${FIGURE_LIST[idx].price} · 关注 ${FIGURE_LIST[idx].hot}`)
                    .fontSize(11)
                    .fontColor(COLORS.textSecondary)
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                .padding(12)
                .backgroundColor(COLORS.cardBg)
                .borderRadius(12)
              }
              if (idx + 1 < FIGURE_LIST.length) {
                Column({ space: 6 }) {
                  Row() {
                    Text('🤖')
                      .fontSize(22)
                      .layoutWeight(1)
                    Text(FIGURE_LIST[idx + 1].level)
                      .fontSize(10)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(levelColor(FIGURE_LIST[idx + 1].level))
                      .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                      .backgroundColor(COLORS.accentLight)
                      .borderRadius(6)
                  }
                  .width('100%')
                  Text(FIGURE_LIST[idx + 1].name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(`${FIGURE_LIST[idx + 1].price} · 关注 ${FIGURE_LIST[idx + 1].hot}`)
                    .fontSize(11)
                    .fontColor(COLORS.textSecondary)
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                .padding(12)
                .backgroundColor(COLORS.cardBg)
                .borderRadius(12)
              }
            }
            .width('100%')
          }
        }, (it: FigureItem) => it.id.toString())
      }
      .width('100%')

      // 创友动态
      Column({ space: 10 }) {
        Row() {
          Text('创友动态')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
            .layoutWeight(1)
          Text('发印语')
            .fontSize(12)
            .fontColor(COLORS.primary)
            .onClick(() => {
              this.openAdd();
            })
        }
        .width('100%')
        ForEach(this.postList.slice(0, 3), (it: PostItem) => {
          Row({ space: 10 }) {
            Text(it.avatar)
              .fontSize(22)
            Column({ space: 3 }) {
              Row() {
                Text(it.nick)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                  .layoutWeight(1)
                Text(it.time)
                  .fontSize(10)
                  .fontColor(COLORS.textHint)
              }
              .width('100%')
              Text(it.text)
                .fontSize(12)
                .fontColor(COLORS.textSecondary)
                .maxLines(2)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(`🖨️ ${it.likes}`)
                .fontSize(11)
                .fontColor(COLORS.textHint)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%')
          .padding(12)
          .backgroundColor(COLORS.cardBg)
          .borderRadius(12)
        }, (it: PostItem) => it.id.toString())
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  pageMarket() {
    Column({ space: 10 }) {
      Row() {
        Text('手办橱窗')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .layoutWeight(1)
        Text('上架我的作品')
          .fontSize(12)
          .fontColor(COLORS.primary)
          .onClick(() => {
            this.openAdd();
          })
      }
      .width('100%')
      ForEach(FIGURE_LIST, (it: FigureItem) => {
        Column({ space: 6 }) {
          Row() {
            Column()
              .width(4)
              .height(38)
              .borderRadius(2)
              .backgroundColor(figTagColor(it.name))
            Column({ space: 4 }) {
              Row() {
                Text(it.name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                  .layoutWeight(1)
                Text(it.level)
                  .fontSize(10)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(levelColor(it.level))
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .backgroundColor(COLORS.bg)
                  .borderRadius(6)
              }
              .width('100%')
              Row({ space: 8 }) {
                Text(`💰 ${it.price}`)
                  .fontSize(11)
                  .fontColor(COLORS.textSecondary)
                Text(`🧾 ${it.craft}`)
                  .fontSize(11)
                  .fontColor(COLORS.textSecondary)
              }
              .width('100%')
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%')
          Stack({ alignContent: Alignment.Start }) {
            Row()
              .width('100%')
              .height(6)
              .borderRadius(3)
              .backgroundColor(COLORS.border)
            Row()
              .width(`${it.hot}%`)
              .height(6)
              .borderRadius(3)
              .backgroundColor(figTagColor(it.name))
          }
          .width('100%')
          Row({ space: 10 }) {
            Text(`关注度 ${it.hot}`)
              .fontSize(10)
              .fontColor(COLORS.textHint)
              .layoutWeight(1)
            Text('加入购物车')
              .fontSize(11)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.white)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .backgroundColor(COLORS.primary)
              .borderRadius(8)
              .onClick(() => {
                this.openPrint();
              })
          }
          .width('100%')
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(12)
      }, (it: FigureItem) => it.id.toString())
    }
    .width('100%')
  }

  @Builder
  pageCrafts() {
    Column({ space: 10 }) {
      Text('我的打印')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
      ForEach(this.printList, (it: PrintItem, idx: number) => {
        Column({ space: 8 }) {
          Row({ space: 10 }) {
            Text('🖨️')
              .fontSize(24)
            Column({ space: 4 }) {
              Row() {
                Text(it.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                  .layoutWeight(1)
                Text(it.status)
                  .fontSize(10)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(statusColor(it.status))
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .backgroundColor(COLORS.accentLight)
                  .borderRadius(6)
              }
              .width('100%')
              Text(`🧾 ${it.item}`)
                .fontSize(11)
                .fontColor(COLORS.textSecondary)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
          }
          .width('100%')
          Stack({ alignContent: Alignment.Start }) {
            Row()
              .width('100%')
              .height(6)
              .borderRadius(3)
              .backgroundColor(COLORS.border)
            Row()
              .width(`${statusProgress(it.status)}%`)
              .height(6)
              .borderRadius(3)
              .backgroundColor(statusColor(it.status))
          }
          .width('100%')
          Row() {
            Text(`任务进度 ${statusProgress(it.status)}%`)
              .fontSize(10)
              .fontColor(COLORS.textHint)
              .layoutWeight(1)
            Text('编辑打印单')
              .fontSize(11)
              .fontColor(COLORS.primary)
              .onClick(() => {
                this.openEdit(idx);
              })
          }
          .width('100%')
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(12)
      }, (it: PrintItem) => it.id.toString())
    }
    .width('100%')
  }

  @Builder
  pageWorkshop() {
    Column({ space: 10 }) {
      Text('打印工坊')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text('车间温度')
            .fontSize(10)
            .fontColor(COLORS.textHint)
          Text('24℃')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.primary)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('树脂槽余量')
            .fontSize(10)
            .fontColor(COLORS.textHint)
          Text('68%')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('今日开机')
            .fontSize(10)
            .fontColor(COLORS.textHint)
          Text('9.5 h')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.filament)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('失败率')
            .fontSize(10)
            .fontColor(COLORS.textHint)
          Text('1.2%')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.success)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      ForEach(FIGURE_LIST.slice(0, 5), (it: FigureItem, idx: number) => {
        Row({ space: 10 }) {
          Text(`${idx + 1}`)
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(idx < 3 ? COLORS.primary : COLORS.textHint)
            .width(20)
          Column({ space: 3 }) {
            Text(it.name)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text(`${it.craft} · ${it.price}`)
              .fontSize(10)
              .fontColor(COLORS.textSecondary)
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Text(`关注 ${it.hot}`)
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(figTagColor(it.name))
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
      }, (it: FigureItem) => it.id.toString())
      Text('耗材速购')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
        .margin({ top: 4 })
      ForEach(GEAR_LIST, (it: GearItem, idx: number) => {
        if (idx % 2 === 0) {
          Row({ space: 10 }) {
            if (idx < GEAR_LIST.length) {
              Column({ space: 6 }) {
                Text(GEAR_LIST[idx].icon)
                  .fontSize(26)
                Text(GEAR_LIST[idx].name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                Text(GEAR_LIST[idx].desc)
                  .fontSize(11)
                  .fontColor(COLORS.textSecondary)
                Text('加购')
                  .fontSize(11)
                  .fontColor(COLORS.primary)
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .padding(12)
              .backgroundColor(COLORS.cardBg)
              .borderRadius(12)
            }
            if (idx + 1 < GEAR_LIST.length) {
              Column({ space: 6 }) {
                Text(GEAR_LIST[idx + 1].icon)
                  .fontSize(26)
                Text(GEAR_LIST[idx + 1].name)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                Text(GEAR_LIST[idx + 1].desc)
                  .fontSize(11)
                  .fontColor(COLORS.textSecondary)
                Text('加购')
                  .fontSize(11)
                  .fontColor(COLORS.primary)
                  .margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              .padding(12)
              .backgroundColor(COLORS.cardBg)
              .borderRadius(12)
            }
          }
          .width('100%')
        }
      }, (it: GearItem) => it.id.toString())
    }
    .width('100%')
  }

  @Builder
  pageCourse() {
    Column({ space: 10 }) {
      Text('建模课堂')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
      ForEach(COURSE_LIST, (it: CourseItem) => {
        Column({ space: 8 }) {
          Row() {
            Text(it.title)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
              .layoutWeight(1)
            Text(it.week)
              .fontSize(11)
              .fontColor(COLORS.textHint)
          }
          .width('100%')
          Stack({ alignContent: Alignment.Start }) {
            Row()
              .width('100%')
              .height(8)
              .borderRadius(4)
              .backgroundColor(COLORS.border)
            Row()
              .width(`${it.progress}%`)
              .height(8)
              .borderRadius(4)
              .backgroundColor(it.color)
          }
          .width('100%')
          Row() {
            Text('已学')
              .fontSize(11)
              .fontColor(COLORS.textSecondary)
              .layoutWeight(1)
            Text(`${it.progress}%`)
              .fontSize(11)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.primary)
          }
          .width('100%')
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(12)
      }, (it: CourseItem) => it.title)
      Button()
        .width('100%')
        .height(42)
        .backgroundColor(COLORS.danger)
        .borderRadius(10)
        .opacity(0.9)
        .onClick(() => {
          this.openDelB();
        })
      Text('退课')
        .fontSize(14)
        .fontColor(COLORS.white)
        .margin({ left: -28, top: -32 })
    }
    .width('100%')
  }

  @Builder
  pageCircle() {
    Column({ space: 10 }) {
      Row() {
        Text('创友圈')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .layoutWeight(1)
        Text('发印语')
          .fontSize(12)
          .fontColor(COLORS.primary)
          .onClick(() => {
            this.openAdd();
          })
      }
      .width('100%')
      ForEach(this.postList, (it: PostItem) => {
        Row({ space: 10 }) {
          Text(it.avatar)
            .fontSize(26)
          Column({ space: 4 }) {
            Row() {
              Text(it.nick)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.textPrimary)
                .layoutWeight(1)
              Text(it.time)
                .fontSize(10)
                .fontColor(COLORS.textHint)
            }
            .width('100%')
            Text(it.text)
              .fontSize(13)
              .fontColor(COLORS.textSecondary)
            Text(`🖨️ ${it.likes} · 💬 回复`)
              .fontSize(11)
              .fontColor(COLORS.textHint)
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(12)
      }, (it: PostItem) => it.id.toString())
    }
    .width('100%')
  }

  @Builder
  pageGear() {
    Column({ space: 10 }) {
      Text('材料库')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
      Row({ space: 8 }) {
        Column({ space: 4 }) {
          Text('🧵')
            .fontSize(28)
          Text('线材区')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('PLA+ · PETG')
            .fontSize(10)
            .fontColor(COLORS.textSecondary)
        }
        .layoutWeight(1)
        .padding(12)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(12)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 4 }) {
          Text('🧪')
            .fontSize(28)
          Text('树脂区')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('光敏 · 水洗')
            .fontSize(10)
            .fontColor(COLORS.textSecondary)
        }
        .layoutWeight(1)
        .padding(12)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(12)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 4 }) {
          Text('🎨')
            .fontSize(28)
          Text('涂装区')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('丙烯 · 补土')
            .fontSize(10)
            .fontColor(COLORS.textSecondary)
        }
        .layoutWeight(1)
        .padding(12)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(12)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      ForEach(GEAR_LIST, (it: GearItem) => {
        Row({ space: 10 }) {
          Text(it.icon)
            .fontSize(20)
          Column({ space: 3 }) {
            Text(it.name)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text(it.desc)
              .fontSize(10)
              .fontColor(COLORS.textSecondary)
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Text('加购')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 })
            .backgroundColor(COLORS.primary)
            .borderRadius(8)
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
      }, (it: GearItem) => it.id.toString())
      Button()
        .width('100%')
        .height(42)
        .backgroundColor(COLORS.danger)
        .borderRadius(10)
        .opacity(0.9)
        .onClick(() => {
          this.openDelA();
        })
      Text('取消首个打印任务')
        .fontSize(14)
        .fontColor(COLORS.white)
        .margin({ left: -100, top: -32 })
    }
    .width('100%')
  }

  @Builder
  pageMine() {
    Column({ space: 12 }) {
      Row({ space: 12 }) {
        Text('🖨️')
          .fontSize(40)
        Column({ space: 4 }) {
          Text('微缩造物主')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('金层会员 · 交付 58 件 · 上色 21 次 · 陪伴 120 天')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)
      Column({ space: 10 }) {
        Row() {
          Text('我的打印档案')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
            .layoutWeight(1)
          Text(`${this.printList.length}`)
            .fontSize(12)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        ForEach(this.printList.slice(0, 3), (it: PrintItem) => {
          Row() {
            Text(it.name)
              .fontSize(13)
              .fontColor(COLORS.textSecondary)
              .layoutWeight(1)
            Text(it.status)
              .fontSize(11)
              .fontColor(statusColor(it.status))
          }
          .width('100%')
        }, (it: PrintItem) => it.id.toString())
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)
      Column({ space: 10 }) {
        Row() {
          Text('我的印语')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
            .layoutWeight(1)
          Text(`${this.postList.length}`)
            .fontSize(12)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        ForEach(this.postList.slice(0, 3), (it: PostItem) => {
          Row() {
            Text(it.nick)
              .fontSize(13)
              .fontColor(COLORS.textSecondary)
              .layoutWeight(1)
            Text(it.time)
              .fontSize(11)
              .fontColor(COLORS.textHint)
          }
          .width('100%')
        }, (it: PostItem) => it.id.toString())
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)
    }
    .width('100%')
  }

  @Builder
  modalOverlay() {
    Stack() {
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor('#000000')
        .opacity(0.6)
        .onClick(() => {
          this.addOpen = false;
          this.editOpen = false;
          this.delOpen = false;
          this.printOpen = false;
        })
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  addModalBody() {
    Column({ space: 12 }) {
      Text('发布印语')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      TextInput({ placeholder: '一句话造物日常…', text: this.addTitle })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.addTitle = v;
        })
      TextArea({ placeholder: '补充细节:模型、参数、上色心得…', text: this.addContent })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(90)
        .onChange((v: string) => {
          this.addContent = v;
        })
      Row({ space: 10 }) {
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.bg)
          .borderRadius(8)
          .onClick(() => {
            this.addOpen = false;
          })
        Text('取消')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .margin({ left: -52 })
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.primary)
          .borderRadius(8)
          .onClick(() => {
            this.doAdd();
          })
        Text('发布')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .margin({ left: -44 })
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(COLORS.cardBg)
  }

  @Builder
  editModalBody() {
    Column({ space: 12 }) {
      Text('编辑打印单')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      TextInput({ placeholder: '任务名称', text: this.editName })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.editName = v;
        })
      TextInput({ placeholder: '状态(如 打印中)', text: this.editStyle })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.editStyle = v;
        })
      TextArea({ placeholder: '打印参数与备注', text: this.editNote })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(70)
        .onChange((v: string) => {
          this.editNote = v;
        })
      Row({ space: 10 }) {
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.bg)
          .borderRadius(8)
          .onClick(() => {
            this.editOpen = false;
          })
        Text('取消')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .margin({ left: -52 })
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.primary)
          .borderRadius(8)
          .onClick(() => {
            this.doEdit();
          })
        Text('保存')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .margin({ left: -44 })
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(COLORS.cardBg)
  }

  @Builder
  delModalBody() {
    Column({ space: 12 }) {
      Text(this.delTarget === 'task' ? '取消首个打印任务' : '退出建模课堂')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      Text(this.delTarget === 'task' ? '将取消首个打印任务并回收已用耗材,确认执行?' : '将退出首门进行中的课程,确认执行?')
        .fontSize(13)
        .fontColor(COLORS.textSecondary)
      Row({ space: 10 }) {
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.bg)
          .borderRadius(8)
          .onClick(() => {
            this.delOpen = false;
          })
        Text('取消')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .margin({ left: -52 })
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.danger)
          .borderRadius(8)
          .onClick(() => {
            this.doDel();
          })
        Text('确认')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .margin({ left: -44 })
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(COLORS.cardBg)
  }

  @Builder
  printModalBody() {
    Column({ space: 12 }) {
      Text('打印委托下单')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      TextInput({ placeholder: '模型名称(如 祥云龙形手办)', text: this.printModel })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.printModel = v;
        })
      TextInput({ placeholder: '比例尺寸(如 1/8 · 180mm)', text: this.printScale })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.printScale = v;
        })
      Text('工程师审核切片后 30 分钟内回电报价 · 含支撑与后处理')
        .fontSize(11)
        .fontColor(COLORS.textHint)
        .width('100%')
      Row({ space: 10 }) {
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.bg)
          .borderRadius(8)
          .onClick(() => {
            this.printOpen = false;
          })
        Text('取消')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .margin({ left: -52 })
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.accent)
          .borderRadius(8)
          .onClick(() => {
            this.doPrint();
          })
        Text('提交委托')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .margin({ left: -56 })
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(COLORS.cardBg)
  }

  @Builder
  bottomBar() {
    Row() {
      ForEach(NAV_LIST, (it: NavItem, idx: number) => {
        Column({ space: 2 }) {
          Text(it.icon)
            .fontSize(20)
            .opacity(this.mainTab === idx ? 1 : 0.55)
          Text(it.label)
            .fontSize(10)
            .fontWeight(this.mainTab === idx ? FontWeight.Bold : FontWeight.Normal)
            .fontColor(this.mainTab === idx ? COLORS.primary : COLORS.textHint)
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .onClick(() => {
          this.mainTab = idx;
        })
      }, (it: NavItem) => it.label)
    }
    .width('100%')
    .height(56)
    .backgroundColor(COLORS.cardBg)
    .borderRadius({ topLeft: 16, topRight: 16 })
  }

  @Builder
  mainContent() {
    Column() {
      this.header()
      if (this.mainTab === 0) {
        this.subNav()
        Scroll() {
          Column() {
            if (this.subTab === 0) {
              this.pageFeatured()
            } else if (this.subTab === 1) {
              this.pageMarket()
            } else if (this.subTab === 2) {
              this.pageCrafts()
            } else if (this.subTab === 3) {
              this.pageWorkshop()
            } else if (this.subTab === 4) {
              this.pageCourse()
            } else if (this.subTab === 5) {
              this.pageCircle()
            } else {
              this.pageGear()
            }
          }
          .width('100%')
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .layoutWeight(1)
        .padding({ left: 14, right: 14, bottom: 20 })
      } else if (this.mainTab === 1) {
        Scroll() {
          Column() {
            this.pageMarket()
          }
          .width('100%')
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .layoutWeight(1)
        .padding({ left: 14, right: 14, bottom: 20 })
      } else if (this.mainTab === 2) {
        Scroll() {
          Column() {
            this.pageCourse()
          }
          .width('100%')
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .layoutWeight(1)
        .padding({ left: 14, right: 14, bottom: 20 })
      } else {
        Scroll() {
          Column() {
            this.pageMine()
          }
          .width('100%')
        }
        .scrollable(ScrollDirection.Vertical)
        .scrollBar(BarState.Off)
        .layoutWeight(1)
        .padding({ left: 14, right: 14, bottom: 20 })
      }
    }
    .width('100%')
    .height('100%')
  }

  build() {
    Stack() {
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor(COLORS.bg)
      this.fxLayer()
      Column() {
        this.mainContent()
        this.bottomBar()
      }
      .width('100%')
      .height('100%')
      if (this.addOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.addModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
      if (this.editOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.editModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
      if (this.delOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.delModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
      if (this.printOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.printModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }
}


十八、总结

在这里插入图片描述

印语·3D打印手办与模型上色工坊应用基于 HarmonyOS 6.1.1 ArkTS API 24 的完整实践,展示了声明式 UI 在数字制造垂直场景中的深度应用能力。从 ColorPalette 接口的青橙科技配色管理(青色 #3FA8A0 + 橙色 #E8843C + 耗材色 #A78BCC),到 @Observed 数据模型的打印任务全生命周期管理,再到纯函数驱动的打印头扫描特效,整个应用在保持与浮语系列一致架构模式的同时,通过场景化的数据内容、视觉细节和交互文案,构建了一个高度专业化的 3D 打印工坊数字体验。

在特效动画层面,fxLayer 构建器通过两组 ForEach 实现了打印头扫描横条和耗材光点闪烁两种特效。打印头扫描横条使用辅色(橙色)背景,通过 scanX 函数以 tick 系数 9 的速度水平移动(三个应用中最快),在 600 像素范围内通过取模循环实现往复运动,精确模拟了 3D 打印头在 X 轴上的快速扫描;耗材光点使用 filament 色(淡紫色)和 spotA 函数的脉冲式闪烁(0.85 或 0.2),模拟了耗材丝在打印头加热区的闪烁效果。特效层仅有两组 ForEach 而非三组,因为 3D 打印场景的视觉核心是打印头的扫描运动,不需要额外的装饰性特效,这种"少即是多"的特效设计体现了对场景本质的深刻理解。

在交互架构层面,"层积阶梯式"的子导航设计是该应用最具技术含量的视觉创新。三个 3 像素宽的竖条在选中时高度为 6/4/2(递减阶梯),在未选中时高度为 3/3/3(等高),模拟了 3D 打印中层积堆积的阶梯纹理——每一层的边缘因材料的堆叠而形成阶梯状的高度差。三个竖条分别使用辅色(橙色)、主色(青色)和耗材色(淡紫色),与 COLORS 常量中的三个核心语义色形成了完整的视觉关联。这种将制造工艺的核心物理特征(层积阶梯)编码到导航交互设计中的做法,是三个应用中最具场景辨识度的视觉设计。

在数据管理层面,PrintItem 模型的 status 字段承载了打印任务全生命周期的状态管理功能。从"排队中"(8%)到"切片中"(24%)到"打印中"(62%)到"后处理中"(85%)到"已完成"(100%),五个状态对应五个进度值和五种颜色,完整映射了 3D 打印的标准工序流程。item 字段包含的详细参数(机号、层高、剩余时间、层数等)为用户提供了精确到分钟和层的实时监控能力。doEdit 方法使用 splice 的替换模式更新打印任务,将进度重置为 40%,模拟了任务修改后的进度回退场景。耗材消耗构成图(MAT_CHART)展示了五种耗材的使用占比,帮助用户了解工坊的耗材成本结构。

从三应用横向对比的角度审视,938 夜钓、939 缝纫和 940 3D 打印三个应用共享了完全一致的架构骨架——相同的 @Entry/@Component/@State/@Builder/@Observed 装饰器使用模式、相同的双层导航结构、相同的 modalOverlay + ModalBody 弹框模式、相同的 Stack 嵌套双 Row 进度条实现、相同的 setInterval + 纯函数特效驱动机制、相同的 Button + Text 叠加按钮模式。三个应用的差异完全体现在数据层(数据模型字段名、静态数据内容、状态枚举)、视觉层(COLORS 色值、特效类型、子导航风格)和业务层(弹框标题/占位文本/说明文案)三个维度。这种"一套架构、三套皮肤"的工程实践,证明了 ArkTS 声明式 UI 在垂直场景应用批量开发中的高度复用能力,为 HarmonyOS 生态中的应用工厂模式提供了可落地的工程范式。

@Observed 数据模型可以升级为 @ObservedV2 获得属性级响应式更新,提升大型打印任务列表的渲染性能。特效动画可以引入 animateTo API 实现扫描横条的缓动效果和耗材光点的渐变过渡。此外,可以集成 ArkTS 的 XComponent 组件实现 3D 模型的 WebGL 渲染和交互旋转,将应用从信息管理工具升级为完整的 3D 打印前置处理平台。特效层的纯函数计算可以迁移到 Worker 线程执行,进一步释放主线程的渲染压力,为更复杂的 3D 模型实时预览腾出性能空间。这些演进方向都将基于现有的 ArkTS 声明式 UI 基础设施平滑展开,证明了该架构设计的前瞻性和可扩展性。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐