一、HarmonyOS 技术背景与 ArkTS 在手工创客场景中的价值

在这里插入图片描述

HarmonyOS 6.1.1 搭载的 ArkTS 语言框架,作为华为在全场景分布式操作系统生态中推出的核心应用开发语言,已经发展为一套兼具类型安全、声明式 UI 表达力和高性能编译优化的成熟编程体系。基于 HarmonyOS API 24 所开放的系统能力,ArkTS 通过 @Entry、@Component、@State、@Builder、@Observed 等装饰器构建了一套从数据模型到视图渲染的完整响应式管线。开发者只需声明数据与视图之间的绑定关系,框架便能在编译期自动生成高效的 Diff 算法和最小化更新策略,无需手动管理视图刷新的时序与粒度。这种设计哲学使得开发者能够将注意力集中在业务逻辑和用户体验设计上,而非底层的渲染机制。

在服装打版与缝纫 DIY 这一垂直创客场景中,HarmonyOS ArkTS API 24 的技术优势体现在多个维度。服装制作是一个从设计到成衣的多阶段工艺流程,涉及量体、打版、排料、裁剪、缝制、后整理等多个环节,每个环节都有不同的状态和进度。衣语·服装打版与缝纫 DIY 工坊应用正是围绕这一业务流程,构建了从成衣橱窗展示、订单管理、打版工坊、裁缝课堂到织友社区的全链路数字化体验。应用采用粉红与草绿的配色方案(#B5546B / #57876B),在浅色暖色调主题下营造出温馨的手工创作氛围,与夜钓等户外场景的暗色风格形成鲜明对比。

从技术架构角度审视,该应用采用了与浮语系列一致的双层导航结构——“底部四主 Tab + 首页七内容 Tab”。底部四个主 Tab 对应"首页"“橱窗”“课堂”“我的”,而首页的七个内容子 Tab 以"针脚虚线式"的视觉风格呈现。每个 Tab 项的文字下方排列着三个微小的色点(主色、辅色、线色),模拟了缝纫中针脚留下的虚线痕迹。这种将工艺元素融入导航设计的做法,是 ArkTS 声明式 UI 在场景化视觉表达方面的典型实践。通过 Column、Row、Stack 等基础布局组件的精确嵌套,开发者能够在不引入任何自定义绘制 API 的情况下,实现高度场景化的视觉细节。

在数据驱动层面,应用使用 @Observed 装饰器对 PostItem(织友动态)、GarmentItem(成衣信息)、OrderItem(生产单)、GearItem(工具信息)四个核心数据模型进行响应式包装。当用户通过弹框发布新动态、编辑生产单状态或撤销订单时,@Observed 类的属性变化会自动触发关联 UI 组件的重新渲染。配合 @State 变量对数组的引用管理,unshift 和 splice 操作能够精确地驱动列表视图的增量更新。此外,应用通过 90 毫秒间隔的 setInterval 定时器驱动 tick 计数器,配合一组纯函数计算针线穿梭、线轴旋转和线色光点的坐标与透明度,实现了与缝纫场景高度匹配的特效动画层。

二、ColorPalette 接口与暗色暖调配色方案

在这里插入图片描述

应用首先定义了 ColorPalette 接口,约束了所有颜色字段的类型安全。与夜钓应用不同,此处的接口中包含了一个 thread(线色)字段,取代了夜钓中的 lure(诱饵色)字段,体现了不同场景下色彩语义的差异化设计。

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;
  thread: string;
}

ColorPalette 接口定义了十六个颜色字段。其中 thread 字段是缝纫场景的专属扩展,用于模拟缝纫线的特殊色彩。primary 和 primaryDark 分别用于主色调和深色变体,accent 和 accentLight 用于辅色和浅色变体。bg 和 cardBg 分别定义了页面背景色和卡片背景色。文本三级色阶(textPrimary / textSecondary / textHint)提供了信息层次的颜色依据。语义色(success / warning / danger)用于状态标识和操作按钮的视觉区分。

const COLORS: ColorPalette = {
  primary: '#B5546B',
  primaryLight: '#F7E8EC',
  primaryDark: '#8E3D52',
  accent: '#57876B',
  accentLight: '#E7F0E9',
  bg: '#FAF4F2',
  cardBg: '#FFFFFF',
  textPrimary: '#3C3236',
  textSecondary: '#6E6167',
  textHint: '#C0B2B8',
  border: '#F0E4E7',
  success: '#7BA37B',
  warning: '#D9973B',
  danger: '#C0503F',
  white: '#FFFFFF',
  thread: '#D9A08C'
};

COLORS 常量的实例化体现了暖色调手工创作场景的设计哲学。主色 #B5546B 是一种沉稳的玫红,呼应了布料与缝线的温暖色调;辅色 #57876B 是一种草绿色,象征了手工制作的自然环保属性;背景色 #FAF4F2 是一种极浅的暖白,模拟了缝纫工作台上的亚麻台布;卡片背景色 #FFFFFF 使用纯白色,在暖色背景上形成清爽的层次。线色 #D9A08C 是一种浅赤陶色,用于特效层中的针线穿梭线条。文本色阶从 #3C3236(深棕灰)到 #C0B2B8(浅灰粉),在浅色背景下形成了清晰而柔和的信息层次。

三、@Observed 数据模型类定义

在这里插入图片描述

应用定义了四个 @Observed 类来承载不同业务领域的数据,每个类都通过构造函数实现属性的全赋值初始化。

@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 字段使用 Emoji 表情(如 📐、♻️、🧵 等)代表不同类型的织友,text 字段存储包含专业缝纫术语的动态内容(如"省道转移"“隐形拉链”"亚麻自然褶"等)。这种数据结构的一致性设计使得应用的模板代码可以在不同场景间复用,而内容数据则完全场景化。

@Observed
export class GarmentItem {
  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
  }
}

GarmentItem 是成衣数据模型,name 字段存储成衣名称(如"亚麻衬衫 · 自然褶"“法式茶歇裙”),craft 字段存储工艺信息(如"刺绣 · 短袖"“碎花 · 收腰”),price 字段以字符串形式存储价格,level 字段是等级标签(如"手作"“人气”“定制”“亲子”"入门"等),hot 字段是关注度数值。level 字段通过 levelColor 纯函数映射为不同的标签颜色,实现了成衣分类的视觉化。

@Observed
export class OrderItem {
  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
  }
}

OrderItem 是生产单数据模型,name 字段包含订单名称(如"亚麻衬衫 × 2"“法式茶歇裙”),item 字段描述当前工序状态(如"打版完成 · 排料中"“缝制 · 上隐形拉链”),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 是工具数据模型,结构简洁,包含名称、描述和 Emoji 图标三个业务字段。静态数据涵盖了缝纫机、打版尺、划粉、珠针、裁布剪刀、人台、隐形拉链压脚和线卷等八种缝纫工具,每种工具的 desc 字段都包含了专业参数(如"吃厚 · 26 种线迹""73-90cm · 网面"等),体现了应用在缝纫领域的专业深度。

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

在这里插入图片描述

应用定义了多个 interface 用于图表与课程等辅助数据的类型约束,这些接口不使用 @Observed 装饰器,因为它们在运行时不会发生变化。

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

interface GarmentChartItem {
  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 用于本周完成件数柱状图,GarmentChartItem 在此基础上增加 color 字段用于指定每个类别的独立颜色,CourseItem 包含课程标题、周次、进度和颜色四个字段,PlayTypeItem 用于玩法类型卡片。这些接口的设计与夜钓应用保持了高度的结构一致性,体现了跨场景应用的接口设计规范。

const POST_LIST: PostItem[] = [
  new PostItem(1, '打版强迫症', '📐', '省道转移三次,腰线终于顺了。', '5分钟前', 98),
  new PostItem(2, '旧衣改造派', '♻️', '牛仔裤拆了做成托特包,帅炸。', '20分钟前', 92),
  new PostItem(3, '隐形拉链杀手', '🤐', '第一次装隐形拉链,居然看不出痕。', '36分钟前', 85),
  new PostItem(4, '衣语主理人', '✂️', '周末亲子工坊开 6 组双工位,来玩。', '1小时前', 78),
  new PostItem(5, '亚麻爱好者', '🧵', '亚麻自然褶太美,就是有点费熨斗。', '2小时前', 66),
  new PostItem(6, '人台守护者', '🧍', '新的人台肩斜调好,立裁省事一半。', '3小时前', 59),
  new PostItem(7, '碎花收藏家', '🌸', '又囤了三米棉麻碎花,理性失效。', '5小时前', 52),
  new PostItem(8, '亲子手作团', '👨‍👩‍👧', '孩子的第一件小围裙,针脚歪但可爱。', '昨天', 46)
];

POST_LIST 包含八条织友动态,每条都使用了真实的缝纫术语。从"省道转移"到"隐形拉链",从"亚麻自然褶"到"人台肩斜",这些专业术语使得应用在首次启动时就具有真实的手工社区氛围。织友的昵称也极具个性化——“打版强迫症”“隐形拉链杀手”"碎花收藏家"等,体现了手工创客群体的趣味性和自我表达欲望。

const GARMENT_LIST: GarmentItem[] = [
  new GarmentItem(1, '亚麻衬衫 · 自然褶', '刺绣 · 短袖', '¥368/件', '手作', 90),
  new GarmentItem(2, '法式茶歇裙', '碎花 · 收腰', '¥428/条', '人气', 94),
  new GarmentItem(3, '工装背带裤', '帆布 · 补强', '¥299/条', '新款', 82),
  new GarmentItem(4, '双面呢大衣', '羊毛 · 可拆内胆', '¥1280/件', '定制', 78),
  new GarmentItem(5, '亲子围裙套装', '防水 · 印花', '¥98/套', '亲子', 74),
  new GarmentItem(6, '改良罩衫 · 汉元素', '对襟 · 日常', '¥268/件', '人气', 86),
  new GarmentItem(7, '宠物雨衣 · 定制', '量体 · 反光条', '¥79/件', '定制', 62),
  new GarmentItem(8, '帆布托特包', '13 安 · 可印字', '¥59/只', '入门', 68)
];

GARMENT_LIST 涵盖了从亚麻衬衫到双面呢大衣的八种成衣产品,价格区间从 ¥59 到 ¥1280,level 标签包含"手作"“人气”“新款”“定制”“亲子”“入门"六种类别。每种成衣的 craft 字段都包含了面料和工艺的关键信息,如"刺绣 · 短袖”“碎花 · 收腰”"帆布 · 补强"等,为用户提供了充分的购买决策参考。

const ORDER_LIST: OrderItem[] = [
  new OrderItem(1, '亚麻衬衫 × 2', '打版完成 · 排料中', '裁剪中', 48),
  new OrderItem(2, '法式茶歇裙', '缝制 · 上隐形拉链', '缝制中', 66),
  new OrderItem(3, '旧牛仔裤改造', '拆解 · 裁托特包片', '裁剪中', 40),
  new OrderItem(4, '亲子围裙 × 3', '印花裁片 · 待缝', '排队中', 12),
  new OrderItem(5, '双面呢大衣', '量体完成 · 制版中', '制版中', 25),
  new OrderItem(6, '改良罩衫', '缝制 · 缝第四道', '缝制中', 70),
  new OrderItem(7, '帆布托特包', '印字 · 免费刻名', '已取件', 100),
  new OrderItem(8, '宠物雨衣', '客户狗 · 胸围 52', '已取件', 100)
];

ORDER_LIST 是生产单的静态数据,包含了四种不同的运行状态(裁剪中、缝制中、制版中、已取件、排队中)。每条数据的 item 字段精确描述了当前工序的进度(如"缝制 · 上隐形拉链"“打版完成 · 排料中”),使得用户能够清晰了解每个订单的生产进度。这种工序级别的状态管理是服装生产管理系统的核心功能点。

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

在这里插入图片描述

应用在顶层定义了多个纯函数,用于特效动画的坐标计算和颜色映射。这些纯函数的设计与夜钓应用保持了相同的架构模式,但函数内部的计算逻辑完全不同。

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

function gcColor(v: number): string {
  if (v > 6) {
    return '#B5546B';
  } else if (v > 3) {
    return '#57876B';
  }
  return '#D9A08C';
}

function shuttleX(tick: number, i: number): number {
  return 40 + ((tick * 8 + i * 210) % 580);
}

function shuttleY(i: number): number {
  return 120 + i * 180;
}

function spoolA(tick: number, i: number): number {
  return ((tick * 10 + i * 90) % 360);
}

function spoolX(tick: number, i: number): number {
  return 70 + ((tick * 2 + i * 233) % 560);
}

function spoolY(tick: number, i: number): number {
  return 90 + ((tick * 3 + i * 187) % 560);
}

barH 函数与夜钓应用完全一致,用于柱状图高度计算。gcColor 函数是服装场景的柱状图颜色映射:大于 6 返回主色玫红,大于 3 返回辅色草绿,其余返回线色赤陶。shuttleX 函数计算针线穿梭的 X 坐标,tick 系数为 8(比夜钓的 3 更快),使得针线的水平移动速度更快,模拟了缝纫时针的快速穿梭。shuttleY 函数的 Y 坐标仅依赖索引 i 而非 tick,意味着针线在固定的水平线上穿梭,更符合缝纫的实际物理特征。spoolA 函数计算线轴的旋转角度,通过 tick * 10 的系数实现每 90 毫秒旋转 10 度的持续旋转效果。spoolX 和 spoolY 函数计算线轴的位置坐标。

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

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

function stitchA(tick: number, i: number): number {
  return 0.12 + ((tick + i * 2) % 5) * 0.06;
}

stitchX、stitchY 和 stitchA 三个函数计算线色光点的位置和透明度。stitchA 函数返回 0.12 到 0.36 之间的值(0.12 + 0/1/2/3/4 * 0.06),实现了光点的微弱呼吸效果。与夜钓应用的 blinkA 函数(返回 0.2 或 0.9 的强闪烁)不同,stitchA 的低透明度范围更符合缝纫场景中针脚痕迹的柔和视觉效果。

function statusColor(s: string): string {
  if (s === '已取件') {
    return '#7BA37B';
  } else if (s === '缝制中') {
    return '#B5546B';
  } else if (s === '裁剪中') {
    return '#D9A08C';
  } else if (s === '制版中') {
    return '#57876B';
  }
  return '#C0B2B8';
}

function statusProgress(s: string): number {
  if (s === '已取件') {
    return 100;
  } else if (s === '缝制中') {
    return 70;
  } else if (s === '裁剪中') {
    return 48;
  } else if (s === '制版中') {
    return 25;
  }
  return 12;
}

statusColor 和 statusProgress 函数将生产单状态映射为颜色和进度值。"已取件"映射为成功绿和 100%,"缝制中"映射为主色玫红和 70%,"裁剪中"映射为线色赤陶和 48%,"制版中"映射为辅色草绿和 25%,默认(排队中)映射为浅灰粉和 12%。这种映射关系反映了服装生产的标准工序顺序:制版 -> 裁剪 -> 缝制 -> 取件,进度值从低到高递增。

function garTagColor(g: string): string {
  if (g.indexOf('衬衫') >= 0 || g.indexOf('罩衫') >= 0) {
    return '#B5546B';
  } else if (g.indexOf('裙') >= 0 || g.indexOf('汉元素') >= 0) {
    return '#57876B';
  } else if (g.indexOf('呢大衣') >= 0 || g.indexOf('背带裤') >= 0) {
    return '#D9A08C';
  }
  return '#D9973B';
}

function levelColor(s: string): string {
  if (s === '入门') {
    return '#7BA37B';
  } else if (s === '人气') {
    return '#B5546B';
  } else if (s === '定制') {
    return '#D9973B';
  } else if (s === '亲子') {
    return '#57876B';
  } else if (s === '手作') {
    return '#D9A08C';
  }
  return '#C0503F';
}

garTagColor 函数通过字符串匹配为成衣名称分配标签颜色:含"衬衫"或"罩衫"的标记为玫红,含"裙"或"汉元素"的标记为草绿,含"呢大衣"或"背带裤"的标记为赤陶,其余标记为警告黄。levelColor 函数为六种类别(入门、人气、定制、亲子、手作、新款)分别映射不同的颜色,其中"新款"默认返回 danger 红色,使得新品标签具有更强的视觉冲击力。

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

在这里插入图片描述

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

@Entry
@Component
struct PageSewingAtelier {
  @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 tailorOpen: boolean = false
  @State editIdx: number = 0
  @State delTarget: string = 'order'
  @State editName: string = ''
  @State editStyle: string = ''
  @State editNote: string = ''
  @State addTitle: string = ''
  @State addContent: string = ''
  @State tailorStyle: string = ''
  @State tailorDate: string = ''
  @State postList: PostItem[] = POST_LIST
  @State orderList: OrderItem[] = ORDER_LIST
  @State fxTimer: number = -1

@State 变量的声明与夜钓应用保持了高度的结构一致性。差异在于:tailorOpen 替代了 charterOpen(量体裁衣预约替代包船预约),tailorStyle 和 tailorDate 替代了 charterTime 和 charterSpot,orderList 替代了 nightList(生产单替代夜钓场次)。这种命名上的差异反映了不同场景的业务语义,但状态管理的架构模式完全一致。

  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 毫秒间隔的定时器驱动 tick 和 glow 变量,aboutToDisappear 中清理定时器资源。这种一致的生命周期管理模式是跨场景应用架构复用的典型体现。

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

在这里插入图片描述

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

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

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

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

  openTailor(): void {
    this.tailorStyle = '';
    this.tailorDate = '';
    this.tailorOpen = true;
  }

openEdit 方法从 orderList 中读取生产单数据填充编辑表单,openTailor 方法清空量体裁衣预约的输入缓存并打开弹框。这些方法的设计模式与夜钓应用保持一致,但操作的底层数据不同。

  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.orderList.length) {
      this.orderList.splice(this.editIdx, 1, new OrderItem(this.orderList[this.editIdx].id, this.editName, this.editNote, this.editStyle, 40));
    }
    this.editOpen = false;
  }

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

  doTailor(): void {
    if (this.tailorStyle.length > 0) {
      this.postList.unshift(new PostItem(998, '我的衣语', '📐', `已预约量体裁衣:${this.tailorStyle}`, '刚刚', 0));
    }
    this.tailorOpen = false;
  }

doEdit 方法使用 splice 替换模式更新生产单数据,将进度重置为 40%。doDel 方法根据 delTarget 的值决定删除 orderList 或 postList 的首元素。doTailor 方法在款式非空时生成一条量体裁衣预约动态并插入到 postList 头部。所有 do 方法在执行完业务逻辑后关闭对应弹框。

八、fxLayer 针线穿梭特效层

fxLayer 构建器是服装场景的视觉核心,通过三组 ForEach 实现了针线穿梭、线轴旋转和线色光点三种特效。

  @Builder
  fxLayer() {
    Stack({ alignContent: Alignment.TopStart }) {
      ForEach([0, 1, 2], (i: number) => {
        Row()
          .width(64)
          .height(2)
          .borderRadius(1)
          .backgroundColor(COLORS.thread)
          .opacity(0.55)
          .translate({ x: shuttleX(this.tick, i), y: shuttleY(i) })
      }, (i: number) => i.toString())
      ForEach([0, 1, 2, 3], (i: number) => {
        Text('🧵')
          .fontSize(18)
          .rotate({ angle: spoolA(this.tick, i) })
          .opacity(0.6)
          .translate({ x: spoolX(this.tick, i), y: spoolY(this.tick, i) })
      }, (i: number) => i.toString())
      ForEach([0, 1, 2, 3, 4, 5], (i: number) => {
        Column()
          .width(3)
          .height(3)
          .borderRadius(2)
          .backgroundColor(COLORS.primary)
          .opacity(stitchA(this.tick, i))
          .translate({ x: stitchX(this.tick, i), y: stitchY(this.tick, i) })
      }, (i: number) => i.toString())
    }
    .width('100%')
    .height('100%')
    .hitTestBehavior(HitTestMode.None)
  }

fxLayer 使用 Stack 容器内嵌三组 ForEach。第一组是三个针线穿梭线条,每个线条是 64x2 像素的 Row 组件,使用线色(thread 色 #D9A08C)背景和 0.55 的透明度,X 坐标由 shuttleX 函数根据 tick 计算(每 90ms 移动 8 像素),Y 坐标由 shuttleY 函数根据索引固定排列(120 + i * 180),形成三条水平穿梭线。第二组是四个线轴旋转元素,使用 Text 组件渲染 🧵 Emoji,通过 rotate 属性设置旋转角度(由 spoolA 函数计算,每 90ms 旋转 10 度),实现线轴的持续旋转效果。第三组是六个线色光点,每个点为 3x3 像素的主色(玫红色)圆点,透明度由 stitchA 函数计算实现微弱呼吸效果,位置由 stitchX 和 stitchY 函数动态计算。

与夜钓应用的 fxLayer 相比,服装场景的特效层具有以下差异:针线穿梭线条的水平移动速度更快(tick 系数 8 vs 3),模拟了缝纫针的快速运动;线轴旋转使用 Emoji + rotate 属性而非纯几何图形,更直观地表达了缝纫线轴的视觉形象;线色光点使用主色而非辅色,且透明度范围更低(0.12-0.36 vs 0.2-0.9),形成了更柔和的视觉效果。整个特效层同样设置了 hitTestBehavior(HitTestMode.None),确保不阻挡用户交互。

九、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('4')
            .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%')

与夜钓应用的 header 不同,服装场景的头部首行是搜索栏 + 购物车的组合。搜索栏使用 Row 嵌套 Row 的结构,内层 Row 包含放大镜图标和占位文本,使用 cardBg 白色背景和 18 的圆角形成胶囊形状。购物车图标使用 Stack 容器叠加 Emoji 和红色数量角标,角标通过 translate 属性偏移到右上角,形成经典的购物车角标效果。数量"4"使用 danger 红色背景和白色文字,在浅色背景上形成醒目的视觉提示。

      Row({ space: 12 }) {
        Column({ space: 4 }) {
          Text('亚麻衬衫 × 2 · 排料中')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Text('3 天后开裁 · 工坊余 2 个工位')
            .fontSize(10)
            .fontColor('#FBF0F3')
          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('免费刻名 · 旧衣可抵扣')
              .fontSize(9)
              .fontColor('#FBF0F3')
          }
        }
        .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.openTailor();
          })
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.primary)
      .borderRadius(16)

排产进度卡使用主色(玫红色 #B5546B)作为背景,内部文字使用白色和浅粉色(#FBF0F3)确保在玫红背景上的可读性。卡片左侧展示了当前排产状态(“亚麻衬衫 × 2 · 排料中”)和工坊余量信息(“3 天后开裁 · 工坊余 2 个工位”),右侧的"预约量体"按钮使用辅色(草绿色)背景。"今日开裁"标签使用辅色背景和深色文字,形成高对比度的状态标识。"免费刻名 · 旧衣可抵扣"作为营销信息增强了用户的预约动力。

      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text('月度成衣')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('68 件')
            .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('7 单')
            .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('42 种')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.success)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('针脚币')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('¥320')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.warning)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')

数据四格展示了月度成衣(68 件,主色)、在制订单(7 单,辅色)、面料种类(42 种,成功色)和针脚币(¥320,警告色)。四个数据卡使用相同的布局结构,通过不同的强调色区分数据类型。针脚币作为应用内的虚拟货币,使用警告黄色突出显示,鼓励用户通过完成成衣制作来积累。

十、subNav 针脚虚线式子导航

subNav 构建器是服装场景的特色导航组件,通过三色微点的排列模拟缝纫针脚的虚线效果。

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

与夜钓应用的"鱼漂顶帽式"导航不同,服装场景的 subNav 采用"针脚虚线式"设计。每个导航项的 Text 下方排列着三个 4x4 像素的微点,分别使用主色(玫红)、辅色(草绿)和线色(赤陶)。当选中时三个微点显示对应颜色,未选中时使用透明色(#00000000)隐藏。这种三色微点的排列模拟了缝纫中针脚留下的虚线痕迹,将缝纫工艺的核心视觉元素融入了导航交互。七个导航项(精选、成衣橱窗、我的订单、打版工坊、裁缝课堂、织友圈、工具库)通过水平滚动展示,滚动条被隐藏以保持视觉简洁。

十一、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 字段包含了简短的服务描述(如"打版 · 缝制 · 成衣""拆解 · 重构 · 新生"等),使用 maxLines(1) 和 textOverflow 确保单行省略。

      Column() {
        Text('我的订单品类构成')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .width('100%')
        ForEach(GAR_CHART, (it: GarmentChartItem) => {
          Row({ space: 8 }) {
            Text(it.label)
              .fontSize(12)
              .fontColor(COLORS.textSecondary)
              .width(82)
            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: GarmentChartItem) => it.label)
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)

品类构成图使用 ForEach 遍历 GAR_CHART 数组,每个品类通过 Stack 嵌套双 Row 实现水平进度条。底层 Row 宽度 100% 作为轨道(border 色),上层 Row 宽度通过模板字符串 ${it.value}% 动态设置百分比,背景色使用每个品类独立的 color 字段。五个品类(上衣衬衫 30%、连衣裙 24%、裤装背带 16%、外套大衣 14%、配饰小物 16%)的进度条颜色各不相同,形成了一个多色的水平条形图。右侧的百分比数值使用加粗字体和右对齐,提供了精确的数值参考。

十二、pageCrafts 生产单管理与进度条

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

  @Builder
  pageCrafts() {
    Column({ space: 10 }) {
      Text('我的订单')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
      ForEach(this.orderList, (it: OrderItem, 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: OrderItem) => it.id.toString())
    }
    .width('100%')
  }

每条订单卡片使用 Column 垂直布局,包含三个主要区域:顶部 Row 展示缝纫图标(🧵)和订单信息(名称、状态标签、工序描述),中间是 Stack 实现的进度条,底部 Row 展示进度文字和编辑入口。状态标签使用 accentLight 色作为背景,与 statusColor 返回的文字色形成柔和的标签效果。进度条的设计与夜钓应用完全一致——Stack 嵌套双 Row 实现轨道 + 填充条的效果。"编辑生产单"入口通过 onClick 调用 openEdit(idx) 方法打开编辑弹框。

十三、弹框组件构建器群

应用定义了四个 *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 构建器渲染发布衣语弹框,包含标题输入框、详情文本域和底部按钮行。TextInput 和 TextArea 的 onChange 回调实现了与 @State 变量的双向绑定。底部按钮使用 Button + Text 叠加模式,取消按钮使用 bg 色背景(与弹框背景融合),发布按钮使用 primary 色背景。

  @Builder
  delModalBody() {
    Column({ space: 12 }) {
      Text(this.delTarget === 'order' ? '撤销生产单' : '退出裁缝课堂')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      Text(this.delTarget === 'order' ? '将撤销首笔在制订单并归还裁片,确认执行?' : '将退出首门进行中的课程,确认执行?')
        .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 的值动态显示"撤销生产单"或"退出裁缝课堂"的标题和描述文本。确认按钮使用 danger 红色背景,强调了删除操作的不可逆性。描述文本中的"归还裁片"是服装生产场景的专业术语,体现了应用在业务语义表达方面的准确性。

  @Builder
  tailorModalBody() {
    Column({ space: 12 }) {
      Text('量体裁衣预约')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      TextInput({ placeholder: '款式(如 法式茶歇裙)', text: this.tailorStyle })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.tailorStyle = v;
        })
      TextInput({ placeholder: '量体时间(如 周六下午)', text: this.tailorDate })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.tailorDate = v;
        })
      Text('裁缝师确认面料与排期后短信通知 · 免费刻名与一次改衣')
        .fontSize(11)
        .fontColor(COLORS.textHint)
        .width('100%')
      Row({ space: 10 }) {
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.bg)
          .borderRadius(8)
          .onClick(() => {
            this.tailorOpen = false;
          })
        Text('取消')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .margin({ left: -52 })
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.accent)
          .borderRadius(8)
          .onClick(() => {
            this.doTailor();
          })
        Text('提交预约')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
          .margin({ left: -56 })
      }
      .width('100%')
    }
    .width('100%')
    .padding(16)
    .backgroundColor(COLORS.cardBg)
  }

tailorModalBody 构建器渲染量体裁衣预约弹框,包含款式和量体时间两个输入框,以及一条说明文本(“裁缝师确认面料与排期后短信通知 · 免费刻名与一次改衣”)。提交按钮使用辅色(草绿色)背景,与发布衣语弹框的主色背景形成视觉区分。说明文本中的"免费刻名与一次改衣"是附加服务信息,增强了用户的预约意愿。

十四、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.tailorOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.tailorModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }

build 方法的 Stack 容器中,依次叠加了背景层(bg 色)、特效层(fxLayer)、主内容层(mainContent + bottomBar)和四个条件渲染的弹框层。每个弹框层的结构完全一致:Stack 包含 modalOverlay 遮罩和一个居中的弹框内容容器(宽度 88%,最大高度 80%,圆角 16,zIndex 999)。modalOverlay 的遮罩透明度为 0.6(比夜钓的 0.7 低),在浅色主题下形成了更柔和的遮罩效果。四个弹框的条件渲染使用 if 语句检查对应的 @State 布尔值,实现了弹框的按需显示。

十五、Mermaid 架构流程图

以下是衣语·服装打版与缝纫DIY工坊应用的组件架构与数据流图:

渲染错误: Mermaid 渲染失败: Parse error on line 44: ...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 GarmentItem OrderItem GearItem
装饰器 @Observed @Observed @Observed @Observed
业务领域 织友社区动态 成衣橱窗展示 生产单管理 工具库管理
字段数量 6 6 5 4
核心字段 text, likes name, price, hot status, progress name, desc
静态数组 POST_LIST (8条) GARMENT_LIST (8条) ORDER_LIST (8条) GEAR_LIST (8条)
可变操作 unshift/splice 只读 splice替换/splice删除 只读
颜色映射函数 - garTagColor, 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切换 关闭所有弹框
视觉特色 thread色线条+线轴旋转 primary色排产卡+搜索栏 三色微点模拟针脚 opacity 0.6
与夜钓差异 线条水平穿梭+Emoji旋转 增加搜索栏+购物车 微点替代短横条 透明度降低
数据依赖 tick, glow 静态文本 SUB_NAV_LIST

安装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;
  thread: string;
}

const COLORS: ColorPalette = {
  primary: '#B5546B',
  primaryLight: '#F7E8EC',
  primaryDark: '#8E3D52',
  accent: '#57876B',
  accentLight: '#E7F0E9',
  bg: '#FAF4F2',
  cardBg: '#FFFFFF',
  textPrimary: '#3C3236',
  textSecondary: '#6E6167',
  textHint: '#C0B2B8',
  border: '#F0E4E7',
  success: '#7BA37B',
  warning: '#D9973B',
  danger: '#C0503F',
  white: '#FFFFFF',
  thread: '#D9A08C'
};

// ============ 织友动态数据模型 ============
@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 GarmentItem {
  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 OrderItem {
  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 GarmentChartItem {
  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, '打版强迫症', '📐', '省道转移三次,腰线终于顺了。', '5分钟前', 98),
  new PostItem(2, '旧衣改造派', '♻️', '牛仔裤拆了做成托特包,帅炸。', '20分钟前', 92),
  new PostItem(3, '隐形拉链杀手', '🤐', '第一次装隐形拉链,居然看不出痕。', '36分钟前', 85),
  new PostItem(4, '衣语主理人', '✂️', '周末亲子工坊开 6 组双工位,来玩。', '1小时前', 78),
  new PostItem(5, '亚麻爱好者', '🧵', '亚麻自然褶太美,就是有点费熨斗。', '2小时前', 66),
  new PostItem(6, '人台守护者', '🧍', '新的人台肩斜调好,立裁省事一半。', '3小时前', 59),
  new PostItem(7, '碎花收藏家', '🌸', '又囤了三米棉麻碎花,理性失效。', '5小时前', 52),
  new PostItem(8, '亲子手作团', '👨‍👩‍👧', '孩子的第一件小围裙,针脚歪但可爱。', '昨天', 46)
];

const GARMENT_LIST: GarmentItem[] = [
  new GarmentItem(1, '亚麻衬衫 · 自然褶', '刺绣 · 短袖', '¥368/件', '手作', 90),
  new GarmentItem(2, '法式茶歇裙', '碎花 · 收腰', '¥428/条', '人气', 94),
  new GarmentItem(3, '工装背带裤', '帆布 · 补强', '¥299/条', '新款', 82),
  new GarmentItem(4, '双面呢大衣', '羊毛 · 可拆内胆', '¥1280/件', '定制', 78),
  new GarmentItem(5, '亲子围裙套装', '防水 · 印花', '¥98/套', '亲子', 74),
  new GarmentItem(6, '改良罩衫 · 汉元素', '对襟 · 日常', '¥268/件', '人气', 86),
  new GarmentItem(7, '宠物雨衣 · 定制', '量体 · 反光条', '¥79/件', '定制', 62),
  new GarmentItem(8, '帆布托特包', '13 安 · 可印字', '¥59/只', '入门', 68)
];

const ORDER_LIST: OrderItem[] = [
  new OrderItem(1, '亚麻衬衫 × 2', '打版完成 · 排料中', '裁剪中', 48),
  new OrderItem(2, '法式茶歇裙', '缝制 · 上隐形拉链', '缝制中', 66),
  new OrderItem(3, '旧牛仔裤改造', '拆解 · 裁托特包片', '裁剪中', 40),
  new OrderItem(4, '亲子围裙 × 3', '印花裁片 · 待缝', '排队中', 12),
  new OrderItem(5, '双面呢大衣', '量体完成 · 制版中', '制版中', 25),
  new OrderItem(6, '改良罩衫', '缝制 · 缝第四道', '缝制中', 70),
  new OrderItem(7, '帆布托特包', '印字 · 免费刻名', '已取件', 100),
  new OrderItem(8, '宠物雨衣', '客户狗 · 胸围 52', '已取件', 100)
];

const GEAR_LIST: GearItem[] = [
  new GearItem(1, '缝纫机 · 家用直驱', '吃厚 · 26 种线迹', '🧵'),
  new GearItem(2, '打版尺 · 六件套', '曲线 · 弧线', '📏'),
  new GearItem(3, '划粉 · 水消', '双色 · 双头', '🖍️'),
  new GearItem(4, '珠针 · 玻璃头', '防滑 · 100 支', '📌'),
  new GearItem(5, '裁布剪刀 · 9 寸', '高碳钢 · 锋利', '✂️'),
  new GearItem(6, '人台 · 可调肩', '73-90cm · 网面', '🧍'),
  new GearItem(7, '隐形拉链压脚', '专配 · 3/5 号', '⚙️'),
  new GearItem(8, '线卷 · 涤纶 402', '60 色 · 不起毛', '🧶')
];

const WEEK_CHART: WeekChartItem[] = [
  { label: '周一', value: 3 },
  { label: '周二', value: 2 },
  { label: '周三', value: 4 },
  { label: '周四', value: 3 },
  { label: '周五', value: 6 },
  { label: '周六', value: 9 },
  { label: '周日', value: 7 }
];

const GAR_CHART: GarmentChartItem[] = [
  { label: '上衣衬衫', value: 30, color: '#B5546B' },
  { label: '连衣裙', value: 24, color: '#57876B' },
  { label: '裤装背带', value: 16, color: '#D9A08C' },
  { label: '外套大衣', value: 14, color: '#D9973B' },
  { label: '配饰小物', value: 16, color: '#7BA37B' }
];

const TYPE_LIST: PlayTypeItem[] = [
  { name: '量体裁衣', desc: '打版 · 缝制 · 成衣', icon: '📐', hot: true },
  { name: '旧衣改造', desc: '拆解 · 重构 · 新生', icon: '♻️', hot: true },
  { name: '布艺手作', desc: '围裙 · 托特 · 玩偶', icon: '🧺', hot: false },
  { name: '亲子工坊', desc: '周末 · 双人工位', icon: '👨‍👩‍👧', hot: false }
];

const COURSE_LIST: CourseItem[] = [
  { title: '量体与松量设计', week: '第 1 周', progress: 100, color: '#B5546B' },
  { title: '原型版型与省道转移', week: '第 2 周', progress: 78, color: '#57876B' },
  { title: '面料识别与排料', week: '第 3 周', progress: 52, color: '#D9A08C' },
  { title: '缝纫机基础与线迹', week: '第 4 周', progress: 36, color: '#7BA37B' },
  { title: '隐形拉链与归拔工艺', week: '第 5 周', progress: 8, 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 gcColor(v: number): string {
  if (v > 6) {
    return '#B5546B';
  } else if (v > 3) {
    return '#57876B';
  }
  return '#D9A08C';
}

function shuttleX(tick: number, i: number): number {
  return 40 + ((tick * 8 + i * 210) % 580);
}

function shuttleY(i: number): number {
  return 120 + i * 180;
}

function spoolA(tick: number, i: number): number {
  return ((tick * 10 + i * 90) % 360);
}

function spoolX(tick: number, i: number): number {
  return 70 + ((tick * 2 + i * 233) % 560);
}

function spoolY(tick: number, i: number): number {
  return 90 + ((tick * 3 + i * 187) % 560);
}

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

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

function stitchA(tick: number, i: number): number {
  return 0.12 + ((tick + i * 2) % 5) * 0.06;
}

function statusColor(s: string): string {
  if (s === '已取件') {
    return '#7BA37B';
  } else if (s === '缝制中') {
    return '#B5546B';
  } else if (s === '裁剪中') {
    return '#D9A08C';
  } else if (s === '制版中') {
    return '#57876B';
  }
  return '#C0B2B8';
}

function statusProgress(s: string): number {
  if (s === '已取件') {
    return 100;
  } else if (s === '缝制中') {
    return 70;
  } else if (s === '裁剪中') {
    return 48;
  } else if (s === '制版中') {
    return 25;
  }
  return 12;
}

function garTagColor(g: string): string {
  if (g.indexOf('衬衫') >= 0 || g.indexOf('罩衫') >= 0) {
    return '#B5546B';
  } else if (g.indexOf('裙') >= 0 || g.indexOf('汉元素') >= 0) {
    return '#57876B';
  } else if (g.indexOf('呢大衣') >= 0 || g.indexOf('背带裤') >= 0) {
    return '#D9A08C';
  }
  return '#D9973B';
}

function levelColor(s: string): string {
  if (s === '入门') {
    return '#7BA37B';
  } else if (s === '人气') {
    return '#B5546B';
  } else if (s === '定制') {
    return '#D9973B';
  } else if (s === '亲子') {
    return '#57876B';
  } else if (s === '手作') {
    return '#D9A08C';
  }
  return '#C0503F';
}

@Entry
@Component
struct PageSewingAtelier {
  @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 tailorOpen: boolean = false
  @State editIdx: number = 0
  @State delTarget: string = 'order'
  @State editName: string = ''
  @State editStyle: string = ''
  @State editNote: string = ''
  @State addTitle: string = ''
  @State addContent: string = ''
  @State tailorStyle: string = ''
  @State tailorDate: string = ''
  @State postList: PostItem[] = POST_LIST
  @State orderList: OrderItem[] = ORDER_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.orderList.length) {
      this.editIdx = index;
      this.editName = this.orderList[index].name;
      this.editStyle = this.orderList[index].status;
      this.editNote = this.orderList[index].item;
    }
    this.editOpen = true;
  }

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

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

  openTailor(): void {
    this.tailorStyle = '';
    this.tailorDate = '';
    this.tailorOpen = 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.orderList.length) {
      this.orderList.splice(this.editIdx, 1, new OrderItem(this.orderList[this.editIdx].id, this.editName, this.editNote, this.editStyle, 40));
    }
    this.editOpen = false;
  }

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

  doTailor(): void {
    if (this.tailorStyle.length > 0) {
      this.postList.unshift(new PostItem(998, '我的衣语', '📐', `已预约量体裁衣:${this.tailorStyle}`, '刚刚', 0));
    }
    this.tailorOpen = false;
  }

  @Builder
  fxLayer() {
    Stack({ alignContent: Alignment.TopStart }) {
      ForEach([0, 1, 2], (i: number) => {
        Row()
          .width(64)
          .height(2)
          .borderRadius(1)
          .backgroundColor(COLORS.thread)
          .opacity(0.55)
          .translate({ x: shuttleX(this.tick, i), y: shuttleY(i) })
      }, (i: number) => i.toString())
      ForEach([0, 1, 2, 3], (i: number) => {
        Text('🧵')
          .fontSize(18)
          .rotate({ angle: spoolA(this.tick, i) })
          .opacity(0.6)
          .translate({ x: spoolX(this.tick, i), y: spoolY(this.tick, i) })
      }, (i: number) => i.toString())
      ForEach([0, 1, 2, 3, 4, 5], (i: number) => {
        Column()
          .width(3)
          .height(3)
          .borderRadius(2)
          .backgroundColor(COLORS.primary)
          .opacity(stitchA(this.tick, i))
          .translate({ x: stitchX(this.tick, i), y: stitchY(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('4')
            .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('亚麻衬衫 × 2 · 排料中')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Text('3 天后开裁 · 工坊余 2 个工位')
            .fontSize(10)
            .fontColor('#FBF0F3')
          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('免费刻名 · 旧衣可抵扣'
            )
              .fontSize(9)
              .fontColor('#FBF0F3')
          }
        }
        .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.openTailor();
          })
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.primary)
      .borderRadius(16)

      // 旧衣改造横幅
      Row({ space: 10 }) {
        Text('♻️')
          .fontSize(22)
        Column({ space: 2 }) {
          Text('旧衣改造月 · 牛仔裤换新计划')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('送改造送定制刻字 · 布料边角做小物'
          )
            .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.openTailor();
          })
      }
      .width('100%')
      .padding(12)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)

      // 数据四格
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text('月度成衣')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('68 件')
            .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('7 单')
            .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('42 种')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.success)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('针脚币')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('¥320')
            .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: 13 }) {
        ForEach(SUB_NAV_LIST, (item: string, idx: number) => {
          Column({ space: 4 }) {
            Text(item)
              .fontSize(this.subTab === idx ? 13 : 11)
              .fontWeight(this.subTab === idx ? FontWeight.Bold : FontWeight.Normal)
              .fontColor(this.subTab === idx ? COLORS.primary : COLORS.textSecondary)
            Row({ space: 4 }) {
              Column()
                .width(4)
                .height(4)
                .borderRadius(2)
                .backgroundColor(this.subTab === idx ? COLORS.primary : '#00000000')
              Column()
                .width(4)
                .height(4)
                .borderRadius(2)
                .backgroundColor(this.subTab === idx ? COLORS.accent : '#00000000')
              Column()
                .width(4)
                .height(4)
                .borderRadius(2)
                .backgroundColor(this.subTab === idx ? COLORS.thread : '#00000000')
            }
          }
          .padding({ top: 4, bottom: 6 })
          .onClick(() => {
            this.subTab = idx;
          })
        }, (item: string) => item)
      }
      .width('100%')
      .alignItems(VerticalAlign.Top)
    }
    .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, 9))
                .borderRadius(5)
                .backgroundColor(gcColor(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('55%'
          )
            .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(GAR_CHART, (it: GarmentChartItem) => {
          Row({ space: 8 }) {
            Text(it.label)
              .fontSize(12)
              .fontColor(COLORS.textSecondary)
              .width(82)
            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: GarmentChartItem) => 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(GARMENT_LIST, (it: GarmentItem, idx: number) => {
          if (idx % 2 === 0) {
            Row({ space: 10 }) {
              if (idx < GARMENT_LIST.length) {
                Column({ space: 6 }) {
                  Row() {
                    Text('👗')
                      .fontSize(22)
                      .layoutWeight(1)
                    Text(GARMENT_LIST[idx].level)
                      .fontSize(10)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(levelColor(GARMENT_LIST[idx].level))
                      .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                      .backgroundColor(COLORS.accentLight)
                      .borderRadius(6)
                  }
                  .width('100%')
                  Text(GARMENT_LIST[idx].name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(`${GARMENT_LIST[idx].price} · 关注 ${GARMENT_LIST[idx].hot}`)
                    .fontSize(11)
                    .fontColor(COLORS.textSecondary)
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                .padding(12)
                .backgroundColor(COLORS.cardBg)
                .borderRadius(12)
              }
              if (idx + 1 < GARMENT_LIST.length) {
                Column({ space: 6 }) {
                  Row() {
                    Text('✂️')
                      .fontSize(22)
                      .layoutWeight(1)
                    Text(GARMENT_LIST[idx + 1].level)
                      .fontSize(10)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(levelColor(GARMENT_LIST[idx + 1].level))
                      .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                      .backgroundColor(COLORS.accentLight)
                      .borderRadius(6)
                  }
                  .width('100%')
                  Text(GARMENT_LIST[idx + 1].name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(`${GARMENT_LIST[idx + 1].price} · 关注 ${GARMENT_LIST[idx + 1].hot}`)
                    .fontSize(11)
                    .fontColor(COLORS.textSecondary)
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                .padding(12)
                .backgroundColor(COLORS.cardBg)
                .borderRadius(12)
              }
            }
            .width('100%')
          }
        }, (it: GarmentItem) => 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(GARMENT_LIST, (it: GarmentItem) => {
        Column({ space: 6 }) {
          Row() {
            Column()
              .width(4)
              .height(38)
              .borderRadius(2)
              .backgroundColor(garTagColor(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(garTagColor(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.openTailor();
              })
          }
          .width('100%')
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(12)
      }, (it: GarmentItem) => it.id.toString())
    }
    .width('100%')
  }

  @Builder
  pageCrafts() {
    Column({ space: 10 }) {
      Text('我的订单')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
      ForEach(this.orderList, (it: OrderItem, 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: OrderItem) => 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('2 位')
            .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('6 轴')
            .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('48 片'
          )
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.warning)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('缝纫工时')
            .fontSize(10)
            .fontColor(COLORS.textHint)
          Text('4.2 h')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.success)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      ForEach(GARMENT_LIST.slice(0, 5), (it: GarmentItem, 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(garTagColor(it.name))
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
      }, (it: GarmentItem) => 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('打版尺 · 划粉')
            .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: -70, 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('银尺会员 · 成衣 84 件 · 接单 21 次 · 陪伴 104 天')
            .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.orderList.length}`)
            .fontSize(12)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        ForEach(this.orderList.slice(0, 3), (it: OrderItem) => {
          Row() {
            Text(it.name)
              .fontSize(13)
              .fontColor(COLORS.textSecondary)
              .layoutWeight(1)
            Text(it.status)
              .fontSize(11)
              .fontColor(statusColor(it.status))
          }
          .width('100%')
        }, (it: OrderItem) => 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.tailorOpen = 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 === 'order' ? '撤销生产单' : '退出裁缝课堂')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      Text(this.delTarget === 'order' ? '将撤销首笔在制订单并归还裁片,确认执行?' : '将退出首门进行中的课程,确认执行?')
        .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
  tailorModalBody() {
    Column({ space: 12 }) {
      Text('量体裁衣预约')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      TextInput({ placeholder: '款式(如 法式茶歇裙)', text: this.tailorStyle })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.tailorStyle = v;
        })
      TextInput({ placeholder: '量体时间(如 周六下午)', text: this.tailorDate })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.tailorDate = v;
        })
      Text('裁缝师确认面料与排期后短信通知 · 免费刻名与一次改衣'
      )
        .fontSize(11)
        .fontColor(COLORS.textHint)
        .width('100%')
      Row({ space: 10 }) {
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.bg)
          .borderRadius(8)
          .onClick(() => {
            this.tailorOpen = false;
          })
        Text('取消')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .margin({ left: -52 })
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.accent)
          .borderRadius(8)
          .onClick(() => {
            this.doTailor();
          })
        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.tailorOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.tailorModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }
}


十七、总结

在这里插入图片描述

衣语·服装打版与缝纫 DIY 工坊应用基于 HarmonyOS ArkTS API 24 的完整实践,展示了声明式 UI 在手工创客垂直场景中的深度应用能力。从 ColorPalette 接口的暖色调色彩管理(玫红 #B5546B + 草绿 #57876B + 线色 #D9A08C),到 @Observed 数据模型的生产单全生命周期管理,再到纯函数驱动的针线穿梭特效,整个应用在保持与浮语系列一致架构模式的同时,通过场景化的数据内容、视觉细节和交互文案,构建了一个高度专业化的缝纫工坊数字体验。

在特效动画层面,fxLayer 构建器通过三组 ForEach 实现了针线穿梭、线轴旋转和线色光点三种特效。针线穿梭线条使用 thread 色和 0.55 透明度,通过 shuttleX 函数以 tick 系数 8 的速度水平移动;线轴旋转使用 🧵 Emoji 配合 spoolA 函数的持续角度计算,实现了线轴每 90ms 旋转 10 度的效果;线色光点使用 primary 色和 stitchA 函数的微弱透明度范围(0.12-0.36),形成了柔和的针脚呼吸效果。这些特效与缝纫场景的物理特征高度匹配——针的快速水平运动、线轴的持续旋转、针脚的微弱痕迹。

在交互架构层面,"针脚虚线式"的子导航设计是该应用的视觉亮点。三个 4x4 像素的微点(主色、辅色、线色)排列在文字下方,模拟了缝纫针脚的虚线痕迹。这种将工艺核心元素融入导航交互的设计,在 ArkTS 声明式 UI 框架下仅通过 Column、Row 和条件渲染即可实现,无需引入任何自定义绘制 API。四个弹框(发布衣语、编辑生产单、撤销订单、量体裁衣预约)通过统一的 modalOverlay + *ModalBody 模式实现了一致的弹框体验,每个弹框的输入控件和按钮配色都与具体业务场景紧密关联。

在数据管理层面,OrderItem 模型的 status 字段承载了生产单全生命周期的状态管理功能。从"排队中"(12%)到"制版中"(25%)到"裁剪中"(48%)到"缝制中"(70%)到"已取件"(100%),五个状态对应五个进度值和五种颜色,完整映射了服装生产的标准工序流程。doEdit 方法使用 splice 的替换模式更新生产单,将进度重置为 40%,模拟了订单修改后的进度回退场景。这种精细化的状态管理为用户提供了清晰的生产进度可视化。

从工程复用的角度审视,该应用与浮语系列应用共享了高度一致的架构骨架——相同的 @Entry/@Component/@State/@Builder 装饰器使用模式、相同的双层导航结构、相同的 modalOverlay + *ModalBody 弹框模式、相同的 Stack 嵌套双 Row 进度条实现、相同的 setInterval + 纯函数特效驱动机制。这种架构一致性证明了 ArkTS 声明式 UI 在跨场景应用开发中的高度复用能力。开发者只需替换数据模型的具体字段、纯函数的计算逻辑、COLORS 常量的色彩值和静态数据数组的内容,即可在相同的架构骨架上构建出视觉风格和业务逻辑完全不同的应用。这种"一套架构、多套皮肤"的开发模式,为 HarmonyOS 生态中的垂直场景应用批量开发提供了可落地的工程范式。

Logo

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

更多推荐