在这里插入图片描述

HarmonyOS 6.1.1 代表了华为全场景智能操作系统在分布式架构和开发体验方面的最新突破,其搭载的 ArkTS 语言在 TypeScript 基础上进行了深度的类型系统强化和状态管理机制嵌入。ArkTS 的核心设计哲学在于通过编译期的严格类型约束和运行时的响应式追踪机制,使开发者能够以纯粹的声明式范式构建高性能的用户界面。在蝴蝶温室与放飞仪式这一融合自然教育与情感体验的垂直场景中,HarmonyOS ArkTS API 24 提供的 @Observed 数据模型、@State 状态管理和 @Builder 组件化构建器构成了完整的技术工具链,使得从蝴蝶羽化记录到放飞仪式预约的全流程数字化管理成为可能。

ArkTS 的声明式开发理念与传统的命令式 UI 编程有着本质的区别。在命令式范式中,开发者需要直接操作 DOM 或视图树的节点,手动管理视图的创建、更新和销毁;而在 ArkTS 声明式范式中,开发者只需描述界面在特定状态下的呈现形态,框架自动负责状态变化后的视图更新。在蝶语蝴蝶温室平台中,蝴蝶飘飞特效每 90 毫秒更新一次 tick 值,系统自动检测到 @State tick 的变化后,仅对依赖该变量的 @Builder fxLayer 进行重新渲染,不会波及其他业务组件。这种精准的依赖追踪和局部重绘机制,是 HarmonyOS 6.1.1 ArkTS 运行时在渲染性能上的核心优势。

城市蝴蝶温室与放飞仪式工坊的业务价值在于将自然教育、情感仪式和社区互动融合为一体化平台。蝴蝶温室作为城市中的微型生态系统,承载着羽化观察、幼虫饲养、标本手作和放飞仪式等多维度的体验内容。蝶语平台通过紫色主色调(#8E6BA8)传递自然与艺术的优雅气质,橙色强调色(#E89A4F)用于突出放飞仪式和热度指标,形成了既温柔又富有活力的视觉层次。在 HarmonyOS ArkTS API 24 的框架下,平台将精选、蝶影橱窗、我的蝶单、羽化工坊、养蝶课堂、蝶友圈和器具库七大功能模块有机整合在统一的内容容器中,底部四主 tab(首页、橱窗、课堂、我的)提供快速场景切换。

从技术架构维度来看,蝶语平台的特效系统采用了蝴蝶飘飞与花粉光点的双粒子层设计。蝴蝶飘飞通过 emoji 图标的旋转和位移实现,4 只蝴蝶在屏幕范围内以不同相位飘动;花粉光点则通过 6 个微型 Column 粒子模拟空气中飘浮的花粉,两者的位置和透明度均由纯函数根据 tick 参数计算。这种设计模式确保了动画的确定性——相同的 tick 值永远产生相同的粒子位置,使得调试和性能优化更加直观。特效层通过 hitTestBehavior(HitTestMode.None) 设置为完全不拦截触摸事件,保证了底层动画不会影响用户对上层业务界面的正常交互。

在状态管理层面,蝶语平台定义了 @Observed 装饰的数据模型类 PostItem、ButterflyItem、ReleaseItem 和 GearItem,分别对应蝶友动态、蝴蝶蝶种、放飞蝶单和器具数据。@Observed 装饰器使得这些类的实例在被 splice 或 unshift 操作时能够被框架自动捕获,触发 ForEach 列表的精准更新。组件内部的 @State 变量管理着弹窗开关、编辑索引、用户输入文本和定时器引用等运行时状态,形成了"数据模型持久层 + 组件状态响应层"的双层架构。这种架构在 HarmonyOS 6.1.1 的运行时环境中表现出了卓越的内存调度和渲染性能,为蝴蝶温室场景下的实时数据交互提供了可靠的技术底座。


一、ColorPalette 接口与紫色主题色彩体系

在这里插入图片描述

蝶语平台的色彩体系以紫色和橙色为核心,通过 ColorPalette 接口的 15 个色彩属性定义了完整的视觉语言系统。紫色(#8E6BA8)作为主色调传达自然与艺术的融合气质,橙色(#E89A4F)作为强调色用于突出放飞仪式和热度指标,辅助色 wing(#C9A0DC)专门用于花粉光点特效的渲染。

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

const COLORS: ColorPalette = {
  primary: '#8E6BA8',
  primaryLight: '#F0E8F5',
  primaryDark: '#6E4F86',
  accent: '#E89A4F',
  accentLight: '#FBEFDF',
  bg: '#FBF6EF',
  cardBg: '#FFFFFF',
  textPrimary: '#3A3244',
  textSecondary: '#6E6578',
  textHint: '#B3AABE',
  border: '#EBE2D8',
  success: '#7BA37B',
  warning: '#D9973B',
  danger: '#C0503F',
  white: '#FFFFFF',
  wing: '#C9A0DC'
};

ColorPalette 接口定义了 15 个色彩属性,其中 primary(#8E6BA8)是淡紫色主色调,传达蝴蝶温室的自然优雅感;accent(#E89A4F)是暖橙色强调色,用于放飞仪式按钮和热度指标;wing(#C9A0DC)是蝶语平台特有的色彩属性,命名直接来源于蝴蝶翅膀的意象,专门用于花粉光点粒子特效的渲染。背景色 bg(#FBF6EF)采用了温暖的米白色,配合 cardBg(#FFFFFF)纯白卡片背景,营造出温室花园般的明亮通透氛围。

文字色彩体系分为三级:textPrimary(#3A3244)深紫灰用于标题和重要信息,textSecondary(#6E6578)中紫灰用于正文内容,textHint(#B3AABE)浅紫灰用于辅助提示文字。状态色包括 success(#7BA37B)草绿、warning(#D9973B)琥珀和 danger(#C0503F)砖红,分别对应已放飞、幼虫期和取消操作等业务状态。在 HarmonyOS ArkTS API 24 的类型系统中,COLORS 常量通过 ColorPalette 接口进行严格约束,任何遗漏或类型不匹配都会在编译阶段被捕获,确保了色彩管理的类型安全性。


二、@Observed 数据模型类与领域驱动设计

在这里插入图片描述

蝶语平台定义了四个 @Observed 装饰的数据模型类,分别承载蝶友动态、蝴蝶蝶种、放飞蝶单和器具数据。这些类的设计体现了领域驱动设计(DDD)的理念——每个类的属性命名和结构都直接映射蝴蝶温室业务领域的核心概念。

@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 ButterflyItem {
  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 ReleaseItem {
  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
  }
}

PostItem 类封装蝶友社区动态信息,包含用户昵称、emoji 头像、动态文本、发布时间和点赞数。ButterflyItem 描述蝴蝶蝶种信息,包括蝶种名称(如"大帛斑蝶 · 蛹期套盒")、工艺描述、价格、等级标签(亲子、稀有、仪式、入门、进阶、手作、定制)和热度值。ReleaseItem 对应放飞蝶单,记录蝶单名称、条目内容、状态(已放飞、羽化中、幼虫期、结蛹期、待接单)和进度百分比。GearItem 定义了温室器具的名称、描述和图标标识。

在 HarmonyOS 6.1.1 的运行时环境中,@Observed 装饰器为类实例的属性安装了 Proxy 代理。当 this.releaseList.splice(this.editIdx, 1, new ReleaseItem(...)) 这样的数组替换操作发生时,框架能够自动检测到数组元素的变化,并通知所有通过 ForEach 依赖该数组的列表组件进行精准的局部重渲染。这种机制使得开发者无需手动调用任何刷新方法,只需修改数据即可自动触发 UI 更新,真正实现了数据驱动的声明式编程。


三、静态数据数组与导航配置体系

在这里插入图片描述

蝶语平台在模块顶层定义了多组静态数据数组,通过 @Observed 类的构造函数实例化,作为应用初始化的数据源。这些数据覆盖了蝶友动态、蝶种目录、放飞蝶单、器具清单、图表数据和课程信息等多个业务维度。

const POST_LIST: PostItem[] = [
  new PostItem(1, '羽化观察员', '🦋', '枯叶蝶破蛹第 4 只,翅展完美。', '5分钟前', 128),
  new PostItem(2, '温室奶妈', '🍃', '蜜源植物又补了一批马利筋。', '20分钟前', 102),
  new PostItem(3, '放飞策划师', '🎉', '周六婚礼放飞 88 只菜粉蝶,寓意久久。', '36分钟前', 97),
  new PostItem(4, '蝶语主理人', '🌸', '7 号温室本周新增三种蜜源。', '1小时前', 85),
  new PostItem(5, '幼虫收藏家', '🐛', '三龄大帛斑幼虫胃口惊人。', '2小时前', 74),
  new PostItem(6, '标本修复师', '🖼️', '老标本换瑞典式装裱,翅膀无损。', '3小时前', 66),
  new PostItem(7, '夜观蝶影', '🌙', '留灯引蝶,飞蛾也来了好多。', '5小时前', 58),
  new PostItem(8, '亲子观蝶团', '👨‍👩‍👧', '孩子第一次看羽化,眼睛都在发光。', '昨天', 51)
];

const BUTTERFLY_LIST: ButterflyItem[] = [
  new ButterflyItem(1, '大帛斑蝶 · 蛹期套盒', '含观察笼 · 5 枚', '¥168/盒', '亲子', 93),
  new ButterflyItem(2, '枯叶蝶 · 拟态观察组', '稀有 · 含蜜源', '¥258/组', '稀有', 95),
  new ButterflyItem(3, '菜粉蝶 · 放飞套餐', '88 只 · 婚礼款', '¥520/套', '仪式', 88),
  new ButterflyItem(4, '金斑蝶 · 羽化盲盒', '随机破蛹 · 含解说', '¥98/个', '入门', 82),
  new ButterflyItem(5, '凤蝶 · 幼虫饲养盒', '三龄 · 含食草', '¥138/盒', '进阶', 79),
  new ButterflyItem(6, '玻璃展翅标本框', 'A3 · 可定制', '¥186/框', '手作', 71),
  new ButterflyItem(7, '蜜源植物组合盆', '马利筋 · 长管花', '¥68/盆', '入门', 64),
  new ButterflyItem(8, '羽化延时摄影服务', '72h · 成片交付', '¥299/次', '定制', 60)
];

const RELEASE_LIST: ReleaseItem[] = [
  new ReleaseItem(1, '周六婚礼放飞', '88 只菜粉蝶 · 双井巷', '已放飞', 100),
  new ReleaseItem(2, '枯叶蝶观察单', '7 号温室 · 第 4 只', '羽化中', 72),
  new ReleaseItem(3, '大帛斑蝶饲养盒', '三龄 · 每日记录', '幼虫期', 45),
  new ReleaseItem(4, '金斑蝶盲盒', '随机 · 等待破蛹', '结蛹期', 58),
  new ReleaseItem(5, '标本装裱定制', 'A3 瑞典式装框', '待接单', 15),
  new ReleaseItem(6, '满月礼放飞', '33 只 · 下周日', '待接单', 10),
  new ReleaseItem(7, '延时摄影跟拍', '72h 记录枯叶蝶', '羽化中', 66),
  new ReleaseItem(8, '亲子观察团', '周六上午 · 8 组家庭', '已放飞', 100)
];

const GEAR_LIST: GearItem[] = [
  new GearItem(1, '羽化观察笼', '透明 · 双开门', '🏮'),
  new GearItem(2, '蜜源喷雾壶', '补蜜专用', '🧴'),
  new GearItem(3, '幼虫饲养盒', '透气 · 含食草', '📦'),
  new GearItem(4, '展翅板 · 软木', '标本定型', '🪵'),
  new GearItem(5, '昆虫镊子 · 细头', '无损夹取', '🥢'),
  new GearItem(6, '湿度计 · 温室款', '羽化关键', '🌡️'),
  new GearItem(7, '放大镜 · 折叠', '观察翅脉', '🔍'),
  new GearItem(8, '记录本 · 蝶单', '羽化日志', '📒')
];

POST_LIST 包含 8 条蝶友动态,从羽化观察到放飞策划,从幼虫饲养到标本修复,生动还原了蝴蝶温室社区的日常生态。BUTTERFLY_LIST 定义了 8 种蝴蝶产品,涵盖了从大帛斑蝶蛹期套盒到羽化延时摄影服务的多样化产品线,价格区间从 ¥68 到 ¥520,等级标签包括亲子、稀有、仪式、入门、进阶、手作和定制。RELEASE_LIST 模拟了 8 笔放飞蝶单,状态涵盖已放飞、羽化中、幼虫期、结蛹期和待接单,进度从 10% 到 100% 不等。GEAR_LIST 列举了 8 件温室核心器具,从羽化观察笼到记录本蝶单。

除了四个核心数据数组外,蝶语平台还定义了图表接口数据、课程数据和玩法类型数据。导航配置中,NAV_LIST 定义了底部四个主标签(首页、橱窗、课堂、我的),SUB_NAV_LIST 定义了首页内的七个内容子标签(精选、蝶影橱窗、我的蝶单、羽化工坊、养蝶课堂、蝶友圈、器具库),构成了"四主+七子"的双层导航架构。

const NAV_LIST: NavItem[] = [
  { icon: '🦋', label: '首页' },
  { icon: '🌸', label: '橱窗' },
  { icon: '📚', label: '课堂' },
  { icon: '👤', label: '我的' }
];

const SUB_NAV_LIST: string[] = ['精选', '蝶影橱窗', '我的蝶单', '羽化工坊', '养蝶课堂', '蝶友圈', '器具库'];

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

const BF_CHART: ButterflyChartItem[] = [
  { label: '斑蝶', value: 30, color: '#8E6BA8' },
  { label: '凤蝶', value: 24, color: '#E89A4F' },
  { label: '粉蝶', value: 18, color: '#C9A0DC' },
  { label: '蛱蝶', value: 16, color: '#7BA37B' },
  { label: '珍稀种', value: 12, color: '#D9973B' }
];

WEEK_CHART 展示了一周内每日羽化数量的分布,周末(周六 14 只、周日 11 只)明显高于工作日,反映了温室在周末安排更多羽化观察活动的事实。BF_CHART 则展示了蝶单中五大蝶科的构成比例,斑蝶占 30% 居首,凤蝶 24% 次之,珍稀种占 12%。每个蝶科都有独立的颜色标识,与 COLORS 体系中的色彩保持一致。


四、纯函数驱动的蝴蝶飘飞与花粉光点动画

在这里插入图片描述

蝶语平台的动画特效系统是 HarmonyOS ArkTS API 24 框架下纯函数驱动动画的典型范例。蝴蝶飘飞通过 emoji 图标的旋转和位移实现,花粉光点通过微型粒子模拟空气飘浮,两者的运动参数完全由无副作用的纯函数根据 tick 时间步长和粒子索引计算得出。

function flutterA(tick: number, i: number): number {
  return ((tick * 5 + i * 60) % 60) - 30;
}

function flutterX(tick: number, i: number): number {
  return 60 + ((tick * 4 + i * 223) % 580);
}

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

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

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

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

function statusColor(s: string): string {
  if (s === '已放飞') {
    return '#7BA37B';
  } else if (s === '羽化中') {
    return '#8E6BA8';
  } else if (s === '幼虫期') {
    return '#E89A4F';
  } else if (s === '结蛹期') {
    return '#C9A0DC';
  }
  return '#B3AABE';
}

function statusProgress(s: string): number {
  if (s === '已放飞') {
    return 100;
  } else if (s === '羽化中') {
    return 72;
  } else if (s === '结蛹期') {
    return 58;
  } else if (s === '幼虫期') {
    return 45;
  }
  return 10;
}

flutterA 函数计算蝴蝶的旋转角度,通过取模运算将角度限制在 -30 到 +30 度之间,以 60 为周期实现来回摆动效果。flutterXflutterY 分别计算蝴蝶在水平和垂直方向的位置,使用质数系数(223、187)确保不同蝴蝶之间的运动轨迹互不相同,营造出自然的飘飞效果。三只蝴蝶以不同的相位和速度在 580x560 像素的范围内移动,旋转角度的周期性变化模拟了蝴蝶翅膀的扑动姿态。

pollenApollenXpollenY 函数控制花粉光点的分布。6 个花粉粒子在 610 像素范围内随机分布,透明度在 0.1 到 0.34 之间变化。使用质数系数(257、163)避免了粒子运动的规律性重复,使得花粉飘浮看起来更加自然。

statusColorstatusProgress 函数是状态映射器,将蝶单状态字符串映射为对应的色彩值和进度百分比。已放飞状态使用成功绿色(#7BA37B)和 100% 进度,羽化中使用主色紫(#8E6BA8)和 72% 进度,幼虫期使用强调橙(#E89A4F)和 45% 进度,结蛹期使用 wing 色(#C9A0DC)和 58% 进度,待接单使用 hint 灰(#B3AABE)和 10% 进度。这种集中化的状态映射设计使得当业务流程变化时,只需修改映射函数即可更新所有依赖该状态的 UI 组件,极大降低了维护成本。

bfTagColorlevelColor 函数也采用了类似的模式。bfTagColor 根据蝶种名称中的关键词(斑蝶、凤蝶、放飞、仪式、枯叶蝶、标本)返回对应的色彩值。levelColor 则将等级标签(入门、进阶、亲子、稀有)映射为不同的颜色。这些纯函数的设计在 HarmonyOS 6.1.1 的运行时中具有极低的计算开销,不涉及对象分配和垃圾回收,每 90 毫秒的 tick 更新只需执行简单的算术运算。


五、@Entry 组件状态管理与定时器生命周期

在这里插入图片描述

蝶语平台的核心组件 PageButterflyHouse 通过 @Entry 和 @Component 装饰器声明为应用入口。组件内部定义了 17 个 @State 装饰的状态变量,覆盖导航控制、动画驱动、弹窗管理和用户输入等全部运行时状态。

@Entry
@Component
struct PageButterflyHouse {
  @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 releaseOpen: boolean = false
  @State editIdx: number = 0
  @State delTarget: string = 'release'
  @State editName: string = ''
  @State editStyle: string = ''
  @State editNote: string = ''
  @State addTitle: string = ''
  @State addContent: string = ''
  @State releasePkg: string = ''
  @State releaseDate: string = ''
  @State postList: PostItem[] = POST_LIST
  @State releaseList: ReleaseItem[] = RELEASE_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;
    }
  }
}

@State releaseOpen 是蝶语平台特有的弹窗状态变量,控制放飞仪式预约弹窗的显隐。@State releasePkg@State releaseDate 分别存储放飞套餐和放飞日期的输入值,是放飞仪式预约表单的核心数据。@State delTarget 的默认值为 'release'(而非檐语平台的 'order'),反映了蝴蝶温室场景下删除操作的首选目标是放飞蝶单。@State postList@State releaseList 分别管理蝶友动态列表和放飞蝶单列表,通过 @State 装饰使得 unshift 和 splice 操作能够触发响应式 UI 更新。

aboutToAppear 生命周期回调中启动 90 毫秒间隔的定时器,每次回调递增 tick 并切换 glow 的 0/1 状态。aboutToDisappear 回调中通过 clearInterval 清除定时器,并将 fxTimer 重置为 -1 以标记定时器已被清除。这种严格的生命周期管理确保了即使用户快速在页面间切换,也不会因遗留定时器造成 CPU 占用和内存泄漏。在 HarmonyOS ArkTS API 24 的框架中,aboutToAppear 和 aboutToDisappear 是组件可见性变化的关键钩子,开发者必须在此类回调中完成异步资源的申请与释放。


六、弹窗操作方法与放飞仪式业务流程

在这里插入图片描述

蝶语平台定义了五个弹窗操作方法,封装了打开弹窗、提交数据和关闭弹窗的完整业务流程。其中放飞仪式预约是蝶语平台的核心业务特色,通过 openRelease 和 doRelease 方法实现了从预约到动态发布的完整数据流。

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

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

openRelease(): void {
  this.releasePkg = '';
  this.releaseDate = '';
  this.releaseOpen = 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.releaseList.length) {
    this.releaseList.splice(this.editIdx, 1,
      new ReleaseItem(this.releaseList[this.editIdx].id,
        this.editName, this.editNote, this.editStyle, 40));
  }
  this.editOpen = false;
}

doRelease(): void {
  if (this.releasePkg.length > 0) {
    this.postList.unshift(new PostItem(998, '我的蝶语', '🎉',
      `已预约放飞仪式:${this.releasePkg}`, '刚刚', 0));
  }
  this.releaseOpen = false;
}

openRelease 方法在打开放飞仪式预约弹窗前清空 releasePkg 和 releaseDate 两个输入字段。doRelease 方法在用户提交时,将放飞套餐信息以新蝶语动态的形式插入到蝶友圈列表头部。这里的关键设计是将放飞仪式的预约操作与社区动态联动——预约成功后不仅在后端创建了蝶单记录,还在前端通过 this.postList.unshift(new PostItem(998, '我的蝶语', '🎉', ...)) 将操作结果以社区动态的形式展示给用户,实现了业务操作与社交反馈的融合。

doEdit 方法使用 this.releaseList.splice(this.editIdx, 1, new ReleaseItem(...)) 替换指定索引的蝶单数据。在 ArkTS 的响应式系统中,splice 操作被精确追踪,框架只会更新被替换的那一个列表项组件,而不会触发整个列表的重绘。doDel 方法根据 delTarget 的值决定删除蝶单(release)还是退课(course),通过 splice(0, 1) 移除首条记录。这种设计使得删除操作具有灵活性,可以根据用户在不同页面发起的删除请求自动选择正确的数据源。


七、fxLayer 蝴蝶飘飞与花粉光点特效层

在这里插入图片描述

fxLayer 是蝶语平台的特效渲染层,通过 @Builder 装饰器定义为可复用的 UI 构建函数。该层包含两组动画元素:4 只蝴蝶飘飞和 6 个花粉光点,所有位置和透明度参数由纯函数根据 tick 和索引计算。

@Builder
fxLayer() {
  Stack({ alignContent: Alignment.TopStart }) {
    ForEach([0, 1, 2, 3], (i: number) => {
      Text('🦋')
        .fontSize(20)
        .rotate({ angle: flutterA(this.tick, i) })
        .opacity(0.7)
        .translate({ x: flutterX(this.tick, i), y: flutterY(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.wing)
        .opacity(pollenA(this.tick, i))
        .translate({ x: pollenX(this.tick, i), y: pollenY(this.tick, i) })
    }, (i: number) => i.toString())
  }
  .width('100%')
  .height('100%')
  .hitTestBehavior(HitTestMode.None)
}

第一组 ForEach 生成 4 只蝴蝶(Text 组件,内容为 🦋 emoji,字号 20px),每只蝴蝶通过 .rotate({ angle: flutterA(this.tick, i) }) 实现旋转,角度范围在 -30 到 +30 度之间;通过 .translate({ x: flutterX(this.tick, i), y: flutterY(this.tick, i) }) 控制位置,在 580x560 像素范围内飘动;透明度固定为 0.7,确保蝴蝶在背景中可见但不喧宾夺主。4 只蝴蝶使用不同的质数系数(223、187)确保运动轨迹各异,营造出自然群飞的效果。

第二组 ForEach 生成 6 个花粉光点(3x3 像素的 Column 组件),使用 COLORS.wing 颜色(#C9A0DC 淡紫色),透明度由 pollenA(this.tick, i) 函数在 0.1 到 0.34 之间动态调整,位置由 pollenXpollenY 函数在 610 像素范围内计算。花粉光点的运动轨迹同样使用质数系数(257、163)避免规律性重复,模拟了温室空气中自然飘浮的花粉和光斑效果。

整个特效层通过 .hitTestBehavior(HitTestMode.None) 设置为完全不拦截触摸事件,确保底层动画不会影响用户对上层业务界面的交互操作。HitTestMode.None 是 HarmonyOS ArkTS API 24 中处理装饰性动画层的标准做法,通过该枚举值可以精确控制事件传递行为,使特效层在视觉上存在但在交互上透明。


八、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('2')
          .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('7 号温室 · 枯叶蝶羽化中')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
        Text('28℃ · 湿度 75% · 今日破蛹 4 只')
          .fontSize(10)
          .fontColor('#F3EBF8')
        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('拟态大师 · 观察位余 3')
            .fontSize(9)
            .fontColor('#F3EBF8')
        }
      }
      .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.openRelease();
        })
    }
    .width('100%')
    .padding(14)
    .backgroundColor(COLORS.primary)
    .borderRadius(16)
  }
  .width('100%')
  .padding({ top: 10, bottom: 4 })
}

搜索栏使用 Row 布局,内含搜索图标和占位文字,背景为 cardBg 白色,圆角 18px 呈胶囊形态。购物车图标使用 Stack 叠加设计,在 🛒 图标右上角通过 .translate({ x: 6, y: -4 }) 偏移放置一个红色背景的数字徽章"2",表示购物车中有 2 件商品。这种设计在 HarmonyOS 6.1.1 的 ArkUI 框架下通过 Stack 的 alignContent: Alignment.TopEnd 实现右上角对齐,再通过 translate 微调精确位置,是电商类应用中常见的购物车角标实现方式。

温室环境卡以 COLORS.primary 紫色为背景,白色文字展示当前温室状态(“7 号温室 · 枯叶蝶羽化中”),下方显示环境参数(28℃ · 湿度 75% · 今日破蛹 4 只)和稀有标签。右侧"预约放飞"按钮使用 COLORS.accent 橙色,触发 openRelease 方法打开放飞仪式预约弹窗。周末放飞会横幅以白底卡片展示报名信息,"去报名"按钮使用 primary 紫色触发同样的 openRelease 方法。数据四格分别展示在展蝶种(46 种)、今日羽化(12 只)、待放飞(3 场)和蝶币(¥180),使用主色、强调色、成功色和警告色进行视觉区分。


九、subNav 左右对称短翼式子导航

蝶语平台的子导航采用了"左右对称短翼式"设计,这是其区别于檐语平台(左圆环锚点式)和幕语平台(上下夹条式)的重要视觉特征。每个子标签项由左侧短翼、文字和右侧短翼三部分组成,形成对称的视觉结构。

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

每个子标签项是一个 Row 容器,内含三个元素:左侧短翼(8x3 像素的 Column,选中时 primary 紫色,未选中透明)、文字标签和右侧短翼(8x3 像素的 Column,选中时 accent 橙色,未选中透明)。这种左右对称的设计形成了"短翼"夹持文字的视觉效果——选中状态下,左侧短翼为紫色、右侧短翼为橙色,形成主色与强调色的对称呼应;未选中状态下,两侧短翼均为透明,只有文字显示。

文字标签的字体大小在选中时为 13px Bold primary 紫色,未选中时为 11px Normal textSecondary 灰色。整个子导航包裹在水平可滚动的 Scroll 组件中,通过 .scrollable(ScrollDirection.Horizontal) 启用水平滚动,.scrollBar(BarState.Off) 隐藏滚动条。点击事件更新 this.subTab = idx,触发 ArkTS 框架自动重绘子导航和对应内容页。这种对称短翼式设计在视觉上比简单的圆点或下划线更加精致,体现了蝴蝶温室场景对美感的追求。


十、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%')

    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, 14))
              .borderRadius(5)
              .backgroundColor(bfColor(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)
  }
  .width('100%')
}

玩法四格遍历 TYPE_LIST 数组,展示四种业务入口(放飞仪式、羽化观察、幼虫饲养、标本手作),每个卡片包含 emoji 图标、名称、描述和热度标签。描述文字通过 .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) 限制为单行省略,防止过长文字破坏四列等分布局。

本周羽化柱状图遍历 WEEK_CHART 的 7 天数据,每根柱子通过 Column 组件实现,宽度固定 20 像素,高度由 barH(it.value, 14) 函数计算(最大值 14 对应 118 像素高度)。柱子颜色由 bfColor(it.value) 函数动态分配:大于 9 使用 primary 紫色、大于 6 使用 accent 橙色、其余使用 wing 淡紫色。底部展示"周末羽化高峰占比 55%"的汇总信息。蝶种构成进度条遍历 BF_CHART 的 5 种蝶科数据,通过 Stack 叠加实现彩色填充进度条,每条进度条宽度根据蝶科占比百分比动态计算。


十一、pageMarket 蝶影橱窗与 pageCrafts 蝶单管理

pageMarket 是蝶影橱窗页面,以列表形式展示所有蝴蝶蝶种产品,每个产品项包含色条指示器、蝶种名称、等级标签、价格和关注度进度条。pageCrafts 管理用户的放飞蝶单列表。

@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(BUTTERFLY_LIST, (it: ButterflyItem) => {
      Column({ space: 6 }) {
        Row() {
          Column()
            .width(4)
            .height(38)
            .borderRadius(2)
            .backgroundColor(bfTagColor(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(bfTagColor(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.openRelease();
            })
        }
        .width('100%')
      }
      .width('100%')
      .padding(12)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(12)
    }, (it: ButterflyItem) => it.id.toString())
  }
  .width('100%')
}

每个蝶种产品项左侧有 4x38 像素的色条指示器,颜色由 bfTagColor(it.name) 函数根据蝶种名称关键词动态分配。名称行右侧显示等级标签,颜色由 levelColor(it.level) 函数映射。关注度进度条使用 Stack 叠加方式实现。右侧"加入购物车"按钮使用 primary 紫色,点击后触发 openRelease 打开放飞仪式预约弹窗,实现了从橱窗浏览到预约下单的转化路径。

pageCrafts 蝶单管理页面遍历 this.releaseList 数组(而非静态 RELEASE_LIST),因为蝶单支持编辑和删除操作。每个蝶单项展示蝶单名称、状态标签(颜色由 statusColor 函数映射)、条目内容和进度条(宽度由 statusProgress 函数计算)。右侧"编辑蝶单"文字按钮触发 openEdit 打开编辑弹窗。


十二、弹框组件体系(四弹窗架构)

蝶语平台定义了四个弹窗组件:发蝶语(addModalBody)、编辑蝶单(editModalBody)、取消蝶单/退课(delModalBody)和放飞仪式预约(releaseModalBody)。所有弹窗共享 modalOverlay 遮罩层,通过 build 方法中的条件渲染实现显隐控制。

@Builder
modalOverlay() {
  Stack() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('#000000')
      .opacity(0.6)
      .onClick(() => {
        this.addOpen = false;
        this.editOpen = false;
        this.delOpen = false;
        this.releaseOpen = 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)
}

modalOverlay 遮罩层是一个全屏 Stack,内含黑色半透明 Column(opacity 0.6),点击时关闭所有四个弹窗。addModalBody 是发蝶语弹窗,包含标题、TextInput 单行输入框、TextArea 多行输入框和双按钮行。TextInput 和 TextArea 通过 .onChange((v: string) => { this.addTitle = v; }) 将用户输入实时绑定到 @State 变量。

@Builder
releaseModalBody() {
  Column({ space: 12 }) {
    Text('放飞仪式预约')
      .fontSize(17)
      .fontWeight(FontWeight.Bold)
      .fontColor(COLORS.textPrimary)
    TextInput({ placeholder: '放飞套餐(如 88 只菜粉蝶)', text: this.releasePkg })
      .fontSize(13)
      .fontColor(COLORS.textPrimary)
      .backgroundColor(COLORS.bg)
      .borderRadius(8)
      .height(40)
      .onChange((v: string) => {
        this.releasePkg = v;
      })
    TextInput({ placeholder: '放飞日期(如 周六上午)', text: this.releaseDate })
      .fontSize(13)
      .fontColor(COLORS.textPrimary)
      .backgroundColor(COLORS.bg)
      .borderRadius(8)
      .height(40)
      .onChange((v: string) => {
        this.releaseDate = v;
      })
    Text('温室管家确认天气与蝶况后短信通知 · 含跟拍记录')
      .fontSize(11)
      .fontColor(COLORS.textHint)
      .width('100%')
    Row({ space: 10 }) {
      Button()
        .layoutWeight(1)
        .height(38)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .onClick(() => {
          this.releaseOpen = false;
        })
      Text('取消')
        .fontSize(13)
        .fontColor(COLORS.textSecondary)
        .margin({ left: -52 })
      Button()
        .layoutWeight(1)
        .height(38)
        .backgroundColor(COLORS.accent)
        .borderRadius(8)
        .onClick(() => {
          this.doRelease();
        })
      Text('提交预约')
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.white)
        .margin({ left: -56 })
    }
    .width('100%')
  }
  .width('100%')
  .padding(16)
  .backgroundColor(COLORS.cardBg)
}

releaseModalBody 放飞仪式预约弹窗是蝶语平台的核心业务弹窗,包含两个 TextInput(放飞套餐和放飞日期)和一条提示文字"温室管家确认天气与蝶况后短信通知 · 含跟拍记录"。提交按钮使用 COLORS.accent 橙色以区别于发蝶语弹窗的 primary 紫色,体现了放飞仪式作为核心业务操作的视觉优先级。提示文字使用 textHint 灰色小字,强调了放飞仪式需要专业管家确认天气和蝶况的安全流程。


十三、build 方法与整体页面架构组装

build 方法是 HarmonyOS ArkTS API 24 组件渲染的入口,蝶语平台通过 Stack 布局将背景层、特效层、主内容层和弹窗层有序叠加。

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

Stack 的层叠顺序从底到顶依次为:背景色层(COLORS.bg #FBF6EF 温暖米白)、特效层(fxLayer 蝴蝶飘飞+花粉光点)、主内容层(mainContent + bottomBar)和条件渲染的弹窗层。四个弹窗分别通过 if (this.addOpen)if (this.editOpen)if (this.delOpen)if (this.releaseOpen) 条件判断决定是否渲染,每个弹窗内容体通过 .width('88%') 占据屏幕 88% 宽度,.constraintSize({ maxHeight: '80%' }) 限制最大高度,.borderRadius(16) 圆角,.zIndex(999) 置顶显示。

mainContent 方法根据 mainTab 的值切换不同主页面,当 mainTab 为 0 时嵌套 subTab 的七子标签切换逻辑。每个内容页都包裹在 .scrollable(ScrollDirection.Vertical) 的 Scroll 组件中,支持垂直滚动。bottomBar 底部导航栏使用 Row 布局均匀排列四个主标签,选中项通过 opacity(1) 和 FontWeight.Bold 突出。


组件架构与数据流 Mermaid 流程图

@Entry PageButterflyHouse

Stack 根布局

背景层 COLORS.bg

fxLayer 特效层

主内容层 mainContent

弹窗层 条件渲染

蝴蝶飘飞 ForEach x4

花粉光点 ForEach x6

hitTestBehavior None

header 头部组件

subNav 子导航 7标签

内容页 切换

bottomBar 底部导航 4标签

搜索栏 + 购物车

温室环境卡

周末放飞会横幅

数据四格统计

pageFeatured 精选

pageMarket 蝶影橱窗

pageCrafts 我的蝶单

pageWorkshop 羽化工坊

pageCourse 养蝶课堂

pageCircle 蝶友圈

pageGear 器具库

pageMine 我的

modalOverlay 遮罩层

addModalBody 发蝶语

editModalBody 编辑蝶单

delModalBody 取消/退课

releaseModalBody 放飞预约

@State 状态管理

mainTab/subTab 导航状态

tick 动画驱动

弹窗开关 x4

postList 蝶友动态

releaseList 放飞蝶单

@Observed 数据模型

PostItem 蝶友动态

ButterflyItem 蝶种数据

ReleaseItem 放飞蝶单

GearItem 器具数据

纯函数系统

flutterA/flutterX/flutterY 蝴蝶飘飞

pollenA/pollenX/pollenY 花粉光点

statusColor/statusProgress 状态映射

bfTagColor/levelColor 标签映射


数据模型与组件对比表格

维度PostItem 蝶友动态ButterflyItem 蝶种数据ReleaseItem 放飞蝶单GearItem 器具数据
用途社区动态信息流蝶种产品展示放飞蝶单进度管理器具目录展示
核心字段nick, avatar, text, likesname, craft, price, level, hotname, item, status, progressname, desc, icon
响应式装饰@Observed@Observed@Observed@Observed
静态数据源POST_LIST (8条)BUTTERFLY_LIST (8条)RELEASE_LIST (8条)GEAR_LIST (8条)
操作方法doAdd (unshift)无(只读展示)doEdit (splice替换), doDel (splice删除)无(只读展示)
展示组件pageCircle, pageFeatured, pageMinepageMarket, pageFeatured, pageWorkshoppageCrafts, pageMinepageGear, pageWorkshop
状态映射函数bfTagColor, levelColorstatusColor, statusProgress
弹窗关联addModalBody 发蝶语editModalBody 编辑, delModalBody 取消
弹窗组件触发方法提交方法输入字段提交按钮颜色业务说明
addModalBodyopenAdddoAddaddTitle, addContentprimary 紫发布蝶友动态
editModalBodyopenEditdoEditeditName, editStyle, editNoteprimary 紫编辑放飞蝶单
delModalBodyopenDelA/openDelBdoDeldelTargetdanger 红取消蝶单或退课
releaseModalBodyopenReleasedoReleasereleasePkg, releaseDateaccent 橙放飞仪式预约

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

const COLORS: ColorPalette = {
  primary: '#8E6BA8',
  primaryLight: '#F0E8F5',
  primaryDark: '#6E4F86',
  accent: '#E89A4F',
  accentLight: '#FBEFDF',
  bg: '#FBF6EF',
  cardBg: '#FFFFFF',
  textPrimary: '#3A3244',
  textSecondary: '#6E6578',
  textHint: '#B3AABE',
  border: '#EBE2D8',
  success: '#7BA37B',
  warning: '#D9973B',
  danger: '#C0503F',
  white: '#FFFFFF',
  wing: '#C9A0DC'
};

// ============ 蝶友动态数据模型 ============
@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 ButterflyItem {
  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 ReleaseItem {
  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 ButterflyChartItem {
  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, '羽化观察员', '🦋', '枯叶蝶破蛹第 4 只,翅展完美。', '5分钟前', 128),
  new PostItem(2, '温室奶妈', '🍃', '蜜源植物又补了一批马利筋。', '20分钟前', 102),
  new PostItem(3, '放飞策划师', '🎉', '周六婚礼放飞 88 只菜粉蝶,寓意久久。', '36分钟前', 97),
  new PostItem(4, '蝶语主理人', '🌸', '7 号温室本周新增三种蜜源。', '1小时前', 85),
  new PostItem(5, '幼虫收藏家', '🐛', '三龄大帛斑幼虫胃口惊人。', '2小时前', 74),
  new PostItem(6, '标本修复师', '🖼️', '老标本换瑞典式装裱,翅膀无损。', '3小时前', 66),
  new PostItem(7, '夜观蝶影', '🌙', '留灯引蝶,飞蛾也来了好多。', '5小时前', 58),
  new PostItem(8, '亲子观蝶团', '👨‍👩‍👧', '孩子第一次看羽化,眼睛都在发光。', '昨天', 51)
];

const BUTTERFLY_LIST: ButterflyItem[] = [
  new ButterflyItem(1, '大帛斑蝶 · 蛹期套盒', '含观察笼 · 5 枚', '¥168/盒', '亲子', 93),
  new ButterflyItem(2, '枯叶蝶 · 拟态观察组', '稀有 · 含蜜源', '¥258/组', '稀有', 95),
  new ButterflyItem(3, '菜粉蝶 · 放飞套餐', '88 只 · 婚礼款', '¥520/套', '仪式', 88),
  new ButterflyItem(4, '金斑蝶 · 羽化盲盒', '随机破蛹 · 含解说', '¥98/个', '入门', 82),
  new ButterflyItem(5, '凤蝶 · 幼虫饲养盒', '三龄 · 含食草', '¥138/盒', '进阶', 79),
  new ButterflyItem(6, '玻璃展翅标本框', 'A3 · 可定制', '¥186/框', '手作', 71),
  new ButterflyItem(7, '蜜源植物组合盆', '马利筋 · 长管花', '¥68/盆', '入门', 64),
  new ButterflyItem(8, '羽化延时摄影服务', '72h · 成片交付', '¥299/次', '定制', 60)
];

const RELEASE_LIST: ReleaseItem[] = [
  new ReleaseItem(1, '周六婚礼放飞', '88 只菜粉蝶 · 双井巷', '已放飞', 100),
  new ReleaseItem(2, '枯叶蝶观察单', '7 号温室 · 第 4 只', '羽化中', 72),
  new ReleaseItem(3, '大帛斑蝶饲养盒', '三龄 · 每日记录', '幼虫期', 45),
  new ReleaseItem(4, '金斑蝶盲盒', '随机 · 等待破蛹', '结蛹期', 58),
  new ReleaseItem(5, '标本装裱定制', 'A3 瑞典式装框', '待接单', 15),
  new ReleaseItem(6, '满月礼放飞', '33 只 · 下周日', '待接单', 10),
  new ReleaseItem(7, '延时摄影跟拍', '72h 记录枯叶蝶', '羽化中', 66),
  new ReleaseItem(8, '亲子观察团', '周六上午 · 8 组家庭', '已放飞', 100)
];

const GEAR_LIST: GearItem[] = [
  new GearItem(1, '羽化观察笼', '透明 · 双开门', '🏮'),
  new GearItem(2, '蜜源喷雾壶', '补蜜专用', '🧴'),
  new GearItem(3, '幼虫饲养盒', '透气 · 含食草', '📦'),
  new GearItem(4, '展翅板 · 软木', '标本定型', '🪵'),
  new GearItem(5, '昆虫镊子 · 细头', '无损夹取', '🥢'),
  new GearItem(6, '湿度计 · 温室款', '羽化关键', '🌡️'),
  new GearItem(7, '放大镜 · 折叠', '观察翅脉', '🔍'),
  new GearItem(8, '记录本 · 蝶单', '羽化日志', '📒')
];

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

const BF_CHART: ButterflyChartItem[] = [
  { label: '斑蝶', value: 30, color: '#8E6BA8' },
  { label: '凤蝶', value: 24, color: '#E89A4F' },
  { label: '粉蝶', value: 18, color: '#C9A0DC' },
  { label: '蛱蝶', value: 16, color: '#7BA37B' },
  { label: '珍稀种', value: 12, color: '#D9973B' }
];

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: '#8E6BA8' },
  { title: '蜜源植物搭配养护', week: '第 2 周', progress: 82, color: '#E89A4F' },
  { title: '羽化观察与记录法', week: '第 3 周', progress: 55, color: '#C9A0DC' },
  { title: '放飞仪式策划实务', week: '第 4 周', progress: 27, 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 bfColor(v: number): string {
  if (v > 9) {
    return '#8E6BA8';
  } else if (v > 6) {
    return '#E89A4F';
  }
  return '#C9A0DC';
}

function flutterA(tick: number, i: number): number {
  return ((tick * 5 + i * 60) % 60) - 30;
}

function flutterX(tick: number, i: number): number {
  return 60 + ((tick * 4 + i * 223) % 580);
}

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

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

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

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

function statusColor(s: string): string {
  if (s === '已放飞') {
    return '#7BA37B';
  } else if (s === '羽化中') {
    return '#8E6BA8';
  } else if (s === '幼虫期') {
    return '#E89A4F';
  } else if (s === '结蛹期') {
    return '#C9A0DC';
  }
  return '#B3AABE';
}

function statusProgress(s: string): number {
  if (s === '已放飞') {
    return 100;
  } else if (s === '羽化中') {
    return 72;
  } else if (s === '结蛹期') {
    return 58;
  } else if (s === '幼虫期') {
    return 45;
  }
  return 10;
}

function bfTagColor(g: string): string {
  if (g.indexOf('斑蝶') >= 0 || g.indexOf('凤蝶') >= 0) {
    return '#8E6BA8';
  } else if (g.indexOf('放飞') >= 0 || g.indexOf('仪式') >= 0) {
    return '#E89A4F';
  } else if (g.indexOf('枯叶蝶') >= 0 || g.indexOf('标本') >= 0) {
    return '#C9A0DC';
  }
  return '#D9973B';
}

function levelColor(s: string): string {
  if (s === '入门') {
    return '#7BA37B';
  } else if (s === '进阶') {
    return '#8E6BA8';
  } else if (s === '亲子') {
    return '#E89A4F';
  } else if (s === '稀有') {
    return '#D9973B';
  }
  return '#C0503F';
}

@Entry
@Component
struct PageButterflyHouse {
  @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 releaseOpen: boolean = false
  @State editIdx: number = 0
  @State delTarget: string = 'release'
  @State editName: string = ''
  @State editStyle: string = ''
  @State editNote: string = ''
  @State addTitle: string = ''
  @State addContent: string = ''
  @State releasePkg: string = ''
  @State releaseDate: string = ''
  @State postList: PostItem[] = POST_LIST
  @State releaseList: ReleaseItem[] = RELEASE_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.releaseList.length) {
      this.editIdx = index;
      this.editName = this.releaseList[index].name;
      this.editStyle = this.releaseList[index].status;
      this.editNote = this.releaseList[index].item;
    }
    this.editOpen = true;
  }

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

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

  openRelease(): void {
    this.releasePkg = '';
    this.releaseDate = '';
    this.releaseOpen = 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.releaseList.length) {
      this.releaseList.splice(this.editIdx, 1, new ReleaseItem(this.releaseList[this.editIdx].id, this.editName, this.editNote, this.editStyle, 40));
    }
    this.editOpen = false;
  }

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

  doRelease(): void {
    if (this.releasePkg.length > 0) {
      this.postList.unshift(new PostItem(998, '我的蝶语', '🎉', `已预约放飞仪式:${this.releasePkg}`, '刚刚', 0));
    }
    this.releaseOpen = false;
  }

  @Builder
  fxLayer() {
    Stack({ alignContent: Alignment.TopStart }) {
      ForEach([0, 1, 2, 3], (i: number) => {
        Text('🦋')
          .fontSize(20)
          .rotate({ angle: flutterA(this.tick, i) })
          .opacity(0.7)
          .translate({ x: flutterX(this.tick, i), y: flutterY(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.wing)
          .opacity(pollenA(this.tick, i))
          .translate({ x: pollenX(this.tick, i), y: pollenY(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('2')
            .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('7 号温室 · 枯叶蝶羽化中')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Text('28℃ · 湿度 75% · 今日破蛹 4 只')
            .fontSize(10)
            .fontColor('#F3EBF8')
          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('拟态大师 · 观察位余 3')
              .fontSize(9)
              .fontColor('#F3EBF8')
          }
        }
        .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.openRelease();
          })
      }
      .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('周六 10:00 · 亲子场 · 33 只粉蝶集体放飞')
            .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.openRelease();
          })
      }
      .width('100%')
      .padding(12)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)

      // 数据四格
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text('在展蝶种')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('46 种')
            .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('12 只')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.accent)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
        Column({ space: 2 }) {
          Text('待放飞')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('3 场'
          )
            .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('¥180')
            .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) => {
          Row({ space: 5 }) {
            Column()
              .width(8)
              .height(3)
              .borderRadius(2)
              .backgroundColor(this.subTab === idx ? COLORS.primary : '#00000000')
            Text(item)
              .fontSize(this.subTab === idx ? 13 : 11)
              .fontWeight(this.subTab === idx ? FontWeight.Bold : FontWeight.Normal)
              .fontColor(this.subTab === idx ? COLORS.primary : COLORS.textSecondary)
            Column()
              .width(8)
              .height(3)
              .borderRadius(2)
              .backgroundColor(this.subTab === idx ? COLORS.accent : '#00000000')
          }
          .padding({ top: 6, bottom: 6 })
          .onClick(() => {
            this.subTab = idx;
          })
        }, (item: string) => item)
      }
      .width('100%')
      .alignItems(VerticalAlign.Center)
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
    .padding({ top: 6, bottom: 10 })
  }

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

      // 本周羽化柱状图
      Column() {
        Row() {
          Text('本周每日羽化数')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
            .layoutWeight(1)
          Text('单位 只')
            .fontSize(10)
            .fontColor(COLORS.textHint)
        }
        .width('100%')
        Row({ space: 10 }) {
          ForEach(WEEK_CHART, (it: WeekChartItem) => {
            Column({ space: 4 }) {
              Column()
                .width(20)
                .height(barH(it.value, 14))
                .borderRadius(5)
                .backgroundColor(bfColor(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(BF_CHART, (it: ButterflyChartItem) => {
          Row({ space: 8 }) {
            Text(it.label)
              .fontSize(12)
              .fontColor(COLORS.textSecondary)
              .width(70)
            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: ButterflyChartItem) => 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(BUTTERFLY_LIST, (it: ButterflyItem, idx: number) => {
          if (idx % 2 === 0) {
            Row({ space: 10 }) {
              if (idx < BUTTERFLY_LIST.length) {
                Column({ space: 6 }) {
                  Row() {
                    Text('🦋')
                      .fontSize(22)
                      .layoutWeight(1)
                    Text(BUTTERFLY_LIST[idx].level)
                      .fontSize(10)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(levelColor(BUTTERFLY_LIST[idx].level))
                      .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                      .backgroundColor(COLORS.accentLight)
                      .borderRadius(6)
                  }
                  .width('100%')
                  Text(BUTTERFLY_LIST[idx].name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(`${BUTTERFLY_LIST[idx].price} · 关注 ${BUTTERFLY_LIST[idx].hot}`)
                    .fontSize(11)
                    .fontColor(COLORS.textSecondary)
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                .padding(12)
                .backgroundColor(COLORS.cardBg)
                .borderRadius(12)
              }
              if (idx + 1 < BUTTERFLY_LIST.length) {
                Column({ space: 6 }) {
                  Row() {
                    Text('🐛')
                      .fontSize(22)
                      .layoutWeight(1)
                    Text(BUTTERFLY_LIST[idx + 1].level)
                      .fontSize(10)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(levelColor(BUTTERFLY_LIST[idx + 1].level))
                      .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                      .backgroundColor(COLORS.accentLight)
                      .borderRadius(6)
                  }
                  .width('100%')
                  Text(BUTTERFLY_LIST[idx + 1].name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(`${BUTTERFLY_LIST[idx + 1].price} · 关注 ${BUTTERFLY_LIST[idx + 1].hot}`)
                    .fontSize(11)
                    .fontColor(COLORS.textSecondary)
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                .padding(12)
                .backgroundColor(COLORS.cardBg)
                .borderRadius(12)
              }
            }
            .width('100%')
          }
        }, (it: ButterflyItem) => 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(BUTTERFLY_LIST, (it: ButterflyItem) => {
        Column({ space: 6 }) {
          Row() {
            Column()
              .width(4)
              .height(38)
              .borderRadius(2)
              .backgroundColor(bfTagColor(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(bfTagColor(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.openRelease();
              })
          }
          .width('100%')
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(12)
      }, (it: ButterflyItem) => it.id.toString())
    }
    .width('100%')
  }

  @Builder
  pageCrafts() {
    Column({ space: 10 }) {
      Text('我的蝶单')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
      ForEach(this.releaseList, (it: ReleaseItem, 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: ReleaseItem) => 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('7 号 · 枯叶蝶')
            .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('28℃')
            .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('破蛹中')
            .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('2.5 h')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.success)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      ForEach(BUTTERFLY_LIST.slice(0, 5), (it: ButterflyItem, 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(bfTagColor(it.name))
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
      }, (it: ButterflyItem) => 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: -84, 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('金翅会员 · 记录 218 只 · 放飞 6 场 · 陪伴 92 天')
            .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.releaseList.length}`)
            .fontSize(12)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        ForEach(this.releaseList.slice(0, 3), (it: ReleaseItem) => {
          Row() {
            Text(it.name)
              .fontSize(13)
              .fontColor(COLORS.textSecondary)
              .layoutWeight(1)
            Text(it.status)
              .fontSize(11)
              .fontColor(statusColor(it.status))
          }
          .width('100%')
        }, (it: ReleaseItem) => 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.releaseOpen = 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 === 'release' ? '取消放飞蝶单' : '退出养蝶课堂')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      Text(this.delTarget === 'release' ? '将取消首笔蝶单并安排幼虫回温室,确认执行?' : '将退出首门进行中的课程,确认执行?')
        .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
  releaseModalBody() {
    Column({ space: 12 }) {
      Text('放飞仪式预约')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      TextInput({ placeholder: '放飞套餐(如 88 只菜粉蝶)', text: this.releasePkg })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.releasePkg = v;
        })
      TextInput({ placeholder: '放飞日期(如 周六上午)', text: this.releaseDate })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.releaseDate = v;
        })
      Text('温室管家确认天气与蝶况后短信通知 · 含跟拍记录')
        .fontSize(11)
        .fontColor(COLORS.textHint)
        .width('100%')
      Row({ space: 10 }) {
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.bg)
          .borderRadius(8)
          .onClick(() => {
            this.releaseOpen = false;
          })
        Text('取消')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .margin({ left: -52 })
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.accent)
          .borderRadius(8)
          .onClick(() => {
            this.doRelease();
          })
        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.releaseOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.releaseModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }
}


总结

在这里插入图片描述

通过对蝶语城市蝴蝶温室与放飞仪式工坊源码的逐段深度剖析,我们全面展现了 HarmonyOS 6.1.1 与 ArkTS API 24 在自然教育与情感体验类应用开发中的技术能力。从 ColorPalette 紫色主题色彩体系到 @Observed 数据模型,从纯函数驱动的蝴蝶飘飞动画到 @Builder 组件化构建器,从 @State 状态管理到条件渲染弹窗架构,每一个技术环节都体现了声明式 UI 范式在复杂业务场景中的适用性。蝶语平台不仅实现了蝴蝶蝶种管理、放飞蝶单调度、羽化工坊监控和养蝶课堂培训等核心业务功能,更通过蝴蝶飘飞和花粉光点双粒子特效营造出了温室花园般的沉浸式视觉氛围。

在架构设计层面,蝶语平台的"左右对称短翼式"子导航是其区别于其他平台的重要视觉创新。每个子标签由左右两侧的短翼指示器夹持文字组成,选中时左翼为 primary 紫色、右翼为 accent 橙色,形成了主色与强调色的对称呼应。这种设计在 HarmonyOS ArkTS API 24 的框架下通过简单的 Column 组件和条件背景色实现,却达到了精致优雅的视觉效果,体现了 ArkTS 声明式范式在 UI 创意表达方面的灵活性。双层导航结构(四主 tab + 七子标签)通过条件渲染机制实现了高效的内容切换,每个内容页通过 @Builder 装饰器封装为独立的构建函数,使框架能够精准识别需要重绘的组件子树。

从数据流的角度来看,蝶语平台的 @Observed + @State 双层数据管理架构展现了 ArkTS 响应式编程在蝴蝶温室场景下的完整实践。@Observed 装饰的 PostItem 和 ReleaseItem 实例在数组操作时被自动追踪,@State 装饰的 postList 和 releaseList 数组作为响应式容器将变更通知传递给 ForEach 列表组件。放飞仪式预约的完整流程——从 openRelease 打开弹窗、用户填写 releasePkg 和 releaseDate、doRelease 提交后通过 postList.unshift 将预约信息以社区动态形式展示——体现了"用户交互 -> 状态更新 -> 数据变更 -> UI 刷新"的完整闭环,消除了命令式开发中手动管理视图更新的复杂性。

Logo

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

更多推荐