一、技术背景与开发理念

在这里插入图片描述

HarmonyOS 6.1.1作为华为面向全场景智慧生活的分布式操作系统,在应用开发框架层面实现了从可用到好用的关键跨越。基于HarmonyOS ArkTS API 24,开发者得以在更加成熟的声明式UI范式下构建高性能、高一致性、高可维护性的原生应用。ArkTS语言在TypeScript基础上进行了严格的静态类型检查和编译期优化,配合@Entry、@Component、@State、@Observed、@Builder等装饰器体系,为开发者提供了一套完整的响应式编程模型。在HarmonyOS 6.1.1的环境下,这套编程模型不仅在性能上达到了原生效能,更在开发体验上实现了"所写即所得"的流畅感——开发者只需关注状态与视图的映射关系,框架自动处理状态变化时的UI差分更新和渲染优化。

声明式UI的精髓在于"数据驱动视图"的编程范式。在HarmonyOS ArkTS API 24中,@State装饰器声明的变量一旦发生变化,框架会自动检测哪些UI组件引用了该变量,并仅对这些组件进行重新渲染。这种精确到属性级别的差分更新机制,使得即使应用中存在高频变化的状态变量(如setInterval驱动的动效tick值),也不会导致全局UI的重新构建。在阡语屋顶农场应用中,fxLayer动效系统每90毫秒更新一次tick值,但只有fxLayer构建器内部的粒子组件会随之更新,header、subNav、pageFeatured等其他构建器完全不受影响。这种隔离式的更新机制是HarmonyOS声明式UI在复杂应用场景下保持流畅性能的关键保障。

城市屋顶农场与采收共享是一个极具社会价值和生态意义的业务场景。在城市化进程加速的今天,屋顶农场不仅是一种绿色生活方式的回归,更是一种社区化农业共享经济的实践。在这个场景中,用户既是城市农夫——认领菜畦、播种浇水、观察生长、采收分享,也是社区成员——发布农事动态、交流种植心得、参与农夫市集、学习农业课程。这种多元化的角色需求对应用的功能广度和信息深度都提出了很高的要求。基于HarmonyOS API 24的ArkTS开发模式,通过底部4主Tab+首页7内容子Tab的双层导航架构,将丰富的功能模块组织在清晰的层级结构中,涵盖了从精选推荐到采收橱窗、从菜畦管理到农事工坊、从种植课堂到农友社区、再到农具库的完整业务链路。

从技术架构的维度来看,本应用的核心特色在于其精心设计的双粒子动效系统——浇灌水滴下落与阳光光点闪烁。这两种粒子效果分别模拟了屋顶农场中最常见的两个视觉元素:灌溉时的水滴和照射植物的目光。水滴粒子以竖向长方形(3x8px)的形态从屏幕上方下落,透明度周期性变化模拟水滴的透明感;阳光粒子以小正方形(4x4px)的形态在屏幕中闪烁,大部分时间保持低透明度,偶尔瞬间高亮模拟阳光的反射。两种粒子在同一Stack容器中协同工作,通过hitTestBehavior(HitTestMode.None)确保不干扰用户的任何交互操作,实现了视觉效果与功能操作的完美平衡。

本应用的色彩系统以绿色(#5E8C4F)和金色(#E0A83C)为双主色调,这一配色方案深刻呼应了屋顶农场的自然主题。绿色代表着植物的生命力与农场的生态底色,金色代表着阳光的温暖与丰收的喜悦。辅助色包括嫩芽绿(#A8C686)用于阳光粒子和部分状态标签,以及成功绿(#7BA37B)、警告橙(#D9973B)和危险红(#C0503F)等语义色。整个色彩系统通过ColorPalette接口进行类型约束,以COLORS常量统一管理。与晶语应用的紫青配色和翱语应用的橙蓝配色相比,阡语应用的绿金配色在视觉上最为贴近自然,在情感上最为温暖,是三个应用中最具场景适配性的色彩方案。

二、色彩系统与接口定义

在这里插入图片描述

2.1 ColorPalette接口与COLORS常量

阡语应用的色彩系统延续了接口定义+常量实例化的架构模式,但在字段命名和色值选择上完全围绕屋顶农场主题展开。

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

const COLORS: ColorPalette = {
  primary: '#5E8C4F',
  primaryLight: '#EAF2E4',
  primaryDark: '#44683A',
  accent: '#E0A83C',
  accentLight: '#FBF3E0',
  bg: '#F5F7F0',
  cardBg: '#FFFFFF',
  textPrimary: '#333A2E',
  textSecondary: '#68705F',
  textHint: '#B4BCA8',
  border: '#E6ECDD',
  success: '#7BA37B',
  warning: '#D9973B',
  danger: '#C0503F',
  white: '#FFFFFF',
  sprout: '#A8C686'
};

ColorPalette接口定义了16个颜色字段。与前两个应用不同的是,阡语应用将特色色字段命名为sprout(嫩芽),值为#A8C686——一种介于绿色和黄绿色之间的嫩芽色。这个颜色在应用中有两个关键用途:一是作为fxLayer中阳光光点粒子的背景色,模拟阳光照射在嫩芽上的反射光;二是作为部分作物状态(如"生长期")的标签颜色,表示植物正在生长的活跃状态。primary字段为#5E8C4F,这是一种沉稳的自然绿,比纯绿色更加内敛,适合作为大面积使用的主色调。accent字段为#E0A83C,这是一种温暖的金黄色,象征着阳光和丰收,在应用中用于采收预约按钮、关注度进度条和警告级数据展示。

textPrimary为深绿灰色#333A2E,textSecondary为中绿灰色#68705F,textHint为浅绿灰色#B4BCA8,这三级文本色都带有绿色调,与主色调和谐统一。bg背景色为#F5F7F0,是一种带有极淡绿色调的灰白色,比纯白色更加柔和自然。border边框色为#E6ECDD,是一种接近背景色的浅绿灰色。整个色彩系统从文本到背景都渗透着绿色调,形成了一个高度统一的视觉体系。

2.2 图表与课程接口

在这里插入图片描述

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

interface CropChartItem {
  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;
}

CropChartItem接口用于作物种类构成图的展示,包含label(作物类别名称)、value(占比百分比)和color(对应颜色)三个字段。在阡语应用中,CropChartItem展示五种作物类别的构成——茄果类(30%,主色绿)、叶菜类(26%,嫩芽绿)、瓜豆类(18%,金色)、香草类(14%,警告橙)和根茎类(12%,成功绿)。CourseItem接口用于种植课堂的五门课程进度展示。PlayTypeItem接口用于首页玩法四格——菜畦认领(🌱热门)、采收体验(🍅热门)、堆肥课堂(🍂)和农夫市集(🧺)。

三、@Observed数据模型类深度分析

在这里插入图片描述

3.1 PostItem——农友动态数据模型

@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的结构与前两个应用完全一致,但在数据内容上完全围绕屋顶农场场景展开。POST_LIST中的8条动态涵盖了农场生活的方方面面——"屋顶园丁"分享樱桃番茄的采收喜悦,"堆肥管理员"汇报堆肥仓的温度数据,"雨水收集员"记录雨夜的收集量,"阡语主理人"宣布秋播窗口开启,"蜜蜂邻居"观察授粉箱的进出量,"城市农夫"感慨鸡毛菜的快速生长,"晒图小能手"拍摄晚霞下的菜园美景,"亲子农团"记录孩子拔萝卜的欢乐时光。这些动态数据不仅丰富了应用的内容,更展现了屋顶农场社区的生态多样性。

当用户发布新的阡语动态时,系统创建id为999的PostItem实例,avatar设置为🌱嫩芽emoji,插入到postList头部。当用户预约采收份额后,系统创建id为998的PostItem实例,avatar设置为🧺藤篮emoji,text字段包含预约信息,同样插入到postList头部。这种将操作反馈以动态形式展示的设计,使得用户的每一次操作都能在社区中留下痕迹,增强了应用的社交属性。

3.2 CropItem——作物数据模型

在这里插入图片描述

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

CropItem是采收橱窗的核心数据模型。name字段存储作物名称(如"樱桃番茄·采收篮"),craft字段存储品质描述(如"现摘500g·沙瓤"),price字段以字符串形式存储价格(如"¥18/篮"),level字段存储分类标签(“当季”、“健康”、“限定”、“入门”、“人气”、“手作”),hot字段为关注度数值。CROP_LIST中的8种作物涵盖了屋顶农场的主要产出——从当季的樱桃番茄和水果黄瓜,到限定的秋季头茬草莓和屋顶蜂蜜,从入门级的芝麻菜到人气的贝贝南瓜,从健康类的羽衣甘蓝到手作类的香草盆栽。这些数据的设计充分考虑了城市屋顶农场的产出特点和社区共享的商业模式。

3.3 PlotItem与GearItem——菜畦与农具数据模型

在这里插入图片描述

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

PlotItem的status字段使用"采收中"、“结果期”、“生长期”、“播种期”、"休耕中"五种状态值来描述菜畦的当前生长阶段。这些状态构成了一个完整的作物生命周期——从播种期到生长期到结果期到采收中,最后到休耕中(养地阶段)。每个状态通过statusColor和statusProgress函数映射为不同的颜色和进度百分比,用户可以在"我的菜畦"页面直观地了解每块菜畦的生长状态。PLOT_LIST中的8块菜畦各有不同的位置编号(如A-07、B-02、C-11等),这种编号方式模拟了真实屋顶农场的分区管理方式。

GearItem的GEAR_LIST包含8种农具——折叠小铲三件套、洒水壶长嘴、园艺手套防刺、堆肥桶密封、标签牌可擦写、遮阳网三针、温度湿度计和采收篮藤编。这些农具涵盖了屋顶农场从整地到采收的全流程工具需求,每种农具都有emoji图标和功能描述,用户可以在"农具库"页面浏览和加购。

四、静态数据数组与导航配置

在这里插入图片描述

4.1 顶层静态数据

const PLOT_LIST: PlotItem[] = [
  new PlotItem(1, 'A-07 樱桃番茄', '第 62 天 · 垂熟期', '采收中', 88),
  new PlotItem(2, 'B-02 水果黄瓜', '搭架完成 · 追肥一次', '结果期', 62),
  new PlotItem(3, 'C-11 菠菜', '秋播第 9 天 · 出苗齐', '生长期', 35),
  new PlotItem(4, 'A-03 草莓', '覆膜 · 等头茬', '生长期', 28),
  new PlotItem(5, 'D-05 贝贝南瓜', '人工授粉 · 单果 600g', '结果期', 70),
  new PlotItem(6, 'B-07 芝麻菜', '昨播 · 未出苗', '播种期', 8),
  new PlotItem(7, 'C-03 香草角', '三味拼 · 可随采', '采收中', 90),
  new PlotItem(8, 'D-01 休耕绿肥', '苕子翻压 · 养地中', '休耕中', 12)
];

PLOT_LIST数组展示了8块菜畦的当前状态,每块菜畦的名称中包含了位置编号(如A-07)和作物名称(如樱桃番茄),item字段记录了详细的生长信息(如"第62天·垂熟期"),status字段标识了当前生长阶段。这8块菜畦覆盖了从播种到采收再到休耕的完整生命周期,为statusColor和statusProgress函数的展示提供了丰富的数据样本。值得注意的是D-01休耕绿肥菜畦——它的item字段记录了"苕子翻压·养地中",status为"休耕中",progress仅为12%,这体现了屋顶农场中轮作休耕的农业智慧。

4.2 导航配置

在这里插入图片描述

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

const SUB_NAV_LIST: string[] = ['精选', '采收橱窗', '我的菜畦', '农事工坊', '种植课堂', '农友圈', '农具库'];

NAV_LIST的4个主Tab图标分别使用嫩芽🌱、藤篮🧺、书本📚和人像👤emoji。SUB_NAV_LIST的7个子Tab覆盖了屋顶农场的完整业务流程——从精选推荐到采收橱窗浏览,从菜畦管理到农事工坊监控,从种植课堂学习到农友社区互动,最后到农具库准备。这种业务流程驱动的导航设计使得用户能够按照"选-看-管-做-学-聊-备"的自然顺序使用应用,降低了功能发现的认知成本。

五、纯函数体系深度解析

5.1 柱状图与颜色映射函数

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

function ccColor(v: number): string {
  if (v > 12) {
    return '#5E8C4F';
  } else if (v > 8) {
    return '#A8C686';
  }
  return '#E0A83C';
}

barH函数与前面两个应用的实现完全一致。ccColor函数是阡语应用特有的颜色映射函数,根据采收人次返回不同颜色——超过12人次返回主色绿#5E8C4F,超过8人次返回嫩芽绿#A8C686,其余返回金色#E0A83C。与前两个应用的阈值不同(晶语应用阈值为9和6,翱语应用阈值为9和6),阡语应用的阈值为12和8,这反映了屋顶农场采收人次的数据范围更大(WEEK_CHART最大值为22),需要更高的阈值才能产生合理的颜色分级。

5.2 浇灌水滴动效函数

浇灌水滴下落动效是阡语应用的视觉特色之一,通过三个纯函数控制水滴粒子的位置和透明度。

function dropX(tick: number, i: number): number {
  return 50 + ((tick * 3 + i * 223) % 600);
}

function dropY(tick: number, i: number): number {
  return 30 + ((tick * 9 + i * 167) % 620);
}

function dropA(tick: number, i: number): number {
  return 0.15 + ((tick + i) % 4) * 0.07;
}

dropX函数使用tick3+i223的取模运算计算水滴的x坐标,系数3使得水滴水平移动较慢,模拟水滴在下落过程中的轻微横向漂移。dropY函数使用tick9+i167的取模运算计算y坐标,系数9是所有动效函数中最大的y方向系数,这意味着水滴的下落速度最快——这与真实物理现象一致,水滴在重力作用下的下落速度远快于气球的升空速度。dropA函数计算透明度,范围为0.15到0.36,通过(tick+i)%4产生0-3的循环值,使得水滴的透明度周期性变化,模拟水滴在下落过程中的光影变化。

5.3 阳光光点动效函数

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

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

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

sunX和sunY函数计算阳光光点的位置,系数5和4使得光点在屏幕中缓慢移动。sunA函数是阳光光点动效的核心——通过(tick+i*2)%6===0的条件判断,每6个tick中有且仅有一次返回高透明度0.85,其余5次返回低透明度0.2。这种1:5的高低比例使得光点大部分时间处于暗淡状态,偶尔瞬间闪亮,完美模拟了阳光透过云层或叶片缝隙时的闪烁效果。与晶语应用中starO函数的1:4比例(每5个tick闪一次)相比,sunA函数的1:5比例使得闪烁频率更低,更符合阳光闪烁的自然节奏。

5.4 状态映射函数

function statusColor(s: string): string {
  if (s === '采收中') {
    return '#5E8C4F';
  } else if (s === '结果期') {
    return '#E0A83C';
  } else if (s === '生长期') {
    return '#A8C686';
  } else if (s === '播种期') {
    return '#D9973B';
  }
  return '#B4BCA8';
}

function statusProgress(s: string): number {
  if (s === '采收中') {
    return 90;
  } else if (s === '结果期') {
    return 65;
  } else if (s === '生长期') {
    return 32;
  } else if (s === '播种期') {
    return 8;
  }
  return 12;
}

statusColor和statusProgress函数将菜畦状态映射为颜色和进度。采收中对应主色绿和90%进度,结果期对应金色和65%进度,生长期对应嫩芽绿和32%进度,播种期对应警告橙和8%进度,休耕中对应灰色和12%进度。这种从"播种期"到"生长期"到"结果期"到"采收中"的四阶段生长状态机(加上休耕中状态),完整覆盖了作物的全生命周期。值得注意的是采收中的进度为90%而非100%——这是因为采收本身是一个过程,100%进度意味着采收完毕即将进入休耕期,90%表示正在采收中但尚未完成。

5.5 标签颜色映射函数

function cropTagColor(g: string): string {
  if (g.indexOf('番茄') >= 0 || g.indexOf('南瓜') >= 0) {
    return '#5E8C4F';
  } else if (g.indexOf('限定') >= 0 || g.indexOf('蜂蜜') >= 0) {
    return '#E0A83C';
  } else if (g.indexOf('草莓') >= 0 || g.indexOf('香草') >= 0) {
    return '#A8C686';
  }
  return '#D9973B';
}

function levelColor(s: string): string {
  if (s === '入门') {
    return '#7BA37B';
  } else if (s === '当季') {
    return '#5E8C4F';
  } else if (s === '限定') {
    return '#E0A83C';
  } else if (s === '健康') {
    return '#A8C686';
  } else if (s === '手作') {
    return '#D9973B';
  }
  return '#C0503F';
}

cropTagColor函数通过字符串搜索为作物分配颜色——包含"番茄"或"南瓜"的返回主色绿(茄果类),包含"限定"或"蜂蜜"的返回金色(稀缺品类),包含"草莓"或"香草"的返回嫩芽绿(特色品类),其余返回警告橙。levelColor函数处理6种level标签的颜色映射——入门对应成功绿(易上手)、当季对应主色绿(应时令)、限定对应金色(稀缺)、健康对应嫩芽绿(养生)、手作对应警告橙(人工),其余对应红色。这种将作物类别和标签等级分别映射为颜色的设计,使得每个作物在列表中都能通过颜色快速传达其品类和定位信息。

六、组件状态与生命周期

6.1 @State变量声明

@Entry
@Component
struct PageRooftopFarm {
  @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 harvestOpen: boolean = false
  @State editIdx: number = 0
  @State delTarget: string = 'plot'
  @State editName: string = ''
  @State editStyle: string = ''
  @State editNote: string = ''
  @State addTitle: string = ''
  @State addContent: string = ''
  @State harvestBox: string = ''
  @State harvestDate: string = ''
  @State postList: PostItem[] = POST_LIST
  @State plotList: PlotItem[] = PLOT_LIST
  @State fxTimer: number = -1

PageRooftopFarm组件的@State变量结构与前两个应用高度一致,但在具体字段上反映了屋顶农场的业务特征。harvestOpen替代了cureOpen/liftOpen,harvestBox和harvestDate替代了cureStyle/cureQty和liftSlot/liftNum,delTarget的默认值为’plot’,plotList替代了workList/tripList。harvestBox字段用于存储采收份额内容(如"樱桃番茄一篮"),harvestDate字段用于存储到园日期(如"周六上午"),这两个字段的设计直接映射了采收预约的业务需求。

6.2 生命周期与弹窗操作方法

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.plotList.length) {
    this.editIdx = index;
    this.editName = this.plotList[index].name;
    this.editStyle = this.plotList[index].status;
    this.editNote = this.plotList[index].item;
  }
  this.editOpen = true;
}

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

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

openHarvest(): void {
  this.harvestBox = '';
  this.harvestDate = '';
  this.harvestOpen = true;
}

生命周期管理与前两个应用完全一致——90ms定时器驱动tick递增和glow切换。open系列方法的逻辑也高度一致——openAdd清空发布表单、openEdit预填菜畦数据、openDelA/openDelB设置删除目标、openHarvest清空采收预约表单。openEdit方法从plotList中读取指定索引的菜畦数据,将名称、状态和农事内容预填到编辑表单中。openDelA设置delTarget为’plot’(弃耕菜畦),openDelB设置delTarget为’course’(退课),两种删除场景共用同一个delModalBody弹窗。

6.3 业务执行方法

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

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

doHarvest(): void {
  if (this.harvestBox.length > 0) {
    this.postList.unshift(new PostItem(998, '我的阡语', '🧺', `已预约采收份额:${this.harvestBox}`, '刚刚', 0));
  }
  this.harvestOpen = false;
}

doAdd方法将新阡语以🌱嫩芽emoji为头像插入postList头部。doEdit方法使用splice替换模式更新plotList中指定索引的菜畦信息,新建的PlotItem的progress硬编码为40%。doDel方法根据delTarget决定删除plotList的首块菜畦或postList的首条动态。doHarvest方法在提交采收预约后,将预约信息以🧺藤篮emoji为头像插入postList,text字段格式为"已预约采收份额:xxx",实现操作反馈。这种将操作结果以社区动态形式展示的设计模式,在三个应用中保持了一致性——doAdd/doCure/doLift/doHarvest都会在执行后向postList插入反馈动态。

七、fxLayer双粒子动效系统

fxLayer构建器是阡语应用视觉系统的核心,集成了浇灌水滴下落和阳光光点闪烁两种粒子效果。

@Builder
fxLayer() {
  Stack({ alignContent: Alignment.TopStart }) {
    ForEach([0, 1, 2, 3, 4], (i: number) => {
      Column()
        .width(3)
        .height(8)
        .borderRadius(2)
        .backgroundColor(COLORS.accent)
        .opacity(dropA(this.tick, i))
        .translate({ x: dropX(this.tick, i), y: dropY(this.tick, i) })
    }, (i: number) => i.toString())
    ForEach([0, 1, 2, 3, 4, 5], (i: number) => {
      Column()
        .width(4)
        .height(4)
        .borderRadius(2)
        .backgroundColor(COLORS.sprout)
        .opacity(sunA(this.tick, i))
        .translate({ x: sunX(this.tick, i), y: sunY(this.tick, i) })
    }, (i: number) => i.toString())
  }
  .width('100%')
  .height('100%')
  .hitTestBehavior(HitTestMode.None)
}

fxLayer使用Stack容器从底到顶依次渲染两组粒子。第一组是5个水滴粒子,使用3x8px的Column组件(宽3高8,borderRadius(2))模拟水滴的竖向形态。水滴的背景色为accent金色#E0A83C,透明度由dropA函数计算(0.15到0.36的周期变化),位置由dropX和dropY函数动态计算。水滴粒子的竖向长方形设计是三个应用中最具物理真实感的粒子形态——前两个应用的粒子都是圆形或正方形,而阡语应用的水滴粒子通过3:8的宽高比模拟了水滴在重力作用下的拉伸形态。

第二组是6个阳光光点,使用4x4px的小正方形Column组件模拟阳光的闪烁。光点的背景色为sprout嫩芽绿#A8C686——这个颜色选择颇具巧思,它模拟的是阳光透过植物叶片后反射的绿色光斑,而非直接的阳光颜色。光点的透明度由sunA函数控制,大部分时间为0.2的低透明度,每6个tick中有一次0.85的高亮闪烁。两组粒子共11个元素,与晶语应用的粒子总数一致(气泡5+星芒6=11),但视觉效果完全不同——水滴的金色下落感与光点的嫩芽绿闪烁感共同营造了屋顶农场的自然氛围。

hitTestBehavior(HitTestMode.None)确保了整个fxLayer不拦截任何触摸事件,所有用户交互都能穿透到下层的内容层。这一设置是三个应用的共同设计模式,确保了动效层与交互层的分离。

八、header头部构建器分析

8.1 搜索条与购物车

阡语应用的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%')

搜索条的placeholder为"搜作物/份额/农事服务",涵盖了用户可能搜索的三类内容。购物车角标显示数字2(晶语应用为3),表示购物车中有2件商品。搜索条和购物车的实现方式与晶语应用完全一致——Stack叠加emoji图标和角标Text,通过translate偏移角标位置。

8.2 采收进度卡

    Row({ space: 12 }) {
      Column({ space: 4 }) {
        Text('A-07 菜畦 · 樱桃番茄全红')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
        Text('第 62 天 · 沙瓤头茬 · 今晨已采 3 篮')
          .fontSize(10)
          .fontColor('#F0F5EC')
        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('余 12 篮 · 手慢无')
            .fontSize(9)
            .fontColor('#F0F5EC')
        }
      }
      .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.openHarvest();
        })
    }
    .width('100%')
    .padding(14)
    .backgroundColor(COLORS.primary)
    .borderRadius(16)

采收进度卡使用primary绿色为背景,白色文字确保对比度。卡片展示了A-07菜畦的樱桃番茄采收信息——"第62天·沙瓤头茬·今晨已采3篮"的详细描述使得用户能够了解作物的具体生长状态。"当季"标签使用accent金色背景+primaryDark深绿色文字,在绿色卡片上形成醒目的对比。"余12篮·手慢无"的紧迫感描述引导用户尽快预约采收。"采收预约"按钮使用accent金色背景,点击后调用openHarvest方法打开采收预约弹窗。

8.3 农夫市集横幅与数据四格

    Row({ space: 10 }) {
      Text('🧺')
        .fontSize(22)
      Column({ space: 2 }) {
        Text('周末屋顶农夫市集 · 28 摊位')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Text('周六 9:00-12:00 · 屋顶直供 · 会员 9 折')
          .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.openHarvest();
        })
    }
    .width('100%')
    .padding(12)
    .backgroundColor(COLORS.cardBg)
    .borderRadius(14)

农夫市集横幅使用白色卡片背景,展示周末市集的时间、摊位数量和会员折扣信息。"去逛逛"按钮使用primary绿色背景,点击后同样调用openHarvest方法——这是一个设计决策,将市集逛逛与采收预约统一到同一个弹窗流程中,简化了交互路径。

数据四格展示共享菜畦数(86块,primary绿)、本周采收量(120kg,accent金)、雨水收集量(2.4t,success绿)和农事币余额(¥95,warning橙)。这四格数据从土地资源、产出效率、环保贡献和经济收益四个维度量化了屋顶农场的运营状况,每个维度使用不同的颜色进行区分。

九、subNav子导航——花盆方块式设计

阡语应用的subNav构建器采用了独特的"花盆方块式"设计,与前两个应用的子导航风格截然不同。

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

花盆方块式设计的核心特征是每个子Tab项呈现为一个圆角方块(borderRadius(6)),通过背景色的变化来标识选中状态。选中状态下,文字为白色、背景为accent金色#E0A83C、字号13加粗;非选中状态下,文字为textSecondary灰色、背景为cardBg白色、字号11常规。这种设计灵感来源于花盆的方块形态——每个子Tab就像一个排列在花架上的花盆,选中的花盆被"金色阳光"照亮(accent金色背景),未选中的花盆保持自然本色(白色背景)。

与晶语应用的"圆盘序号式"(圆点+文字)和翱语应用的"吊篮圆点竖线式"(圆点+竖线+文字)相比,阡语应用的"花盆方块式"是最简洁的设计——每个子Tab仅由一个Text组件构成,通过padding和borderRadius形成方块外观。这种简洁性使得子导航占用更少的垂直空间,为内容区域留出更多展示空间。同时,金色选中态的视觉冲击力也最强——在绿色主调的应用中,金色方块格外醒目,用户能够一眼定位当前所在的子Tab。

十、pageFeatured精选页面深度分析

10.1 玩法四格与柱状图

@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%')

玩法四格展示四种屋顶农场玩法:菜畦认领(🌱热门)、采收体验(🍅热门)、堆肥课堂(🍂)和农夫市集(🧺)。前两种玩法标记为热门(danger红色),后两种不标记热门。每个格子的结构与前两个应用一致——emoji图标、名称、描述和热门标签,通过layoutWeight(1)等分宽度。

本周采收柱状图展示一周内每日的采收人次,柱子高度由barH函数计算(最大值22人次对应118px),颜色由ccColor函数根据人次决定——超过12人次为主色绿,超过8人次为嫩芽绿,其余为金色。柱状图下方展示"周末采收人次占比58%"的统计数据,使用accent金色高亮。从数据来看,周六22人次和周日18人次是明显的周末高峰,这与城市居民周末到屋顶农场参与采收的活动规律高度吻合。

10.2 作物构成图与人气作物双列

    Column() {
      Text('我的菜畦作物构成')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
      ForEach(CROP_CHART, (it: CropChartItem) => {
        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: CropChartItem) => it.label)
    }
    .width('100%')
    .padding(14)
    .backgroundColor(COLORS.cardBg)
    .borderRadius(14)

作物构成图使用ForEach遍历CROP_CHART数组,通过Stack+双层Row构建水平进度条展示五种作物类别的占比。每行包含类别标签(width(70)固定宽度)、进度条(layoutWeight(1)弹性宽度)和百分比数值(width(44)固定宽度),三部分通过Row的space:8统一间距。进度条的实现方式与前面两个应用完全一致——Stack容器内底层Row为灰色轨道,上层Row的宽度通过${it.value}%动态设置为占比百分比,背景色由it.color决定。

人气作物双列使用ForEach遍历CROP_LIST数组,通过idx%2===0的条件判断实现双列布局。每行包含两个作物卡片,每个卡片展示emoji图标(🍅或🥬)、level标签、作物名称和价格关注度信息。level标签的颜色由levelColor函数动态决定——"当季"标签为主色绿、"限定"标签为金色、"入门"标签为成功绿、"健康"标签为嫩芽绿、"人气"标签为主色绿、"手作"标签为警告橙。

十一、pageCrafts菜畦进度页面分析

pageCrafts构建器展示用户的菜畦列表,每块菜畦包含状态标签、进度条和编辑入口。

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

每块菜畦使用🌱嫩芽emoji作为标识图标,状态标签的背景色为accentLight浅金色#FBF3E0,文字颜色由statusColor函数根据状态动态决定——采收中为主色绿、结果期为金色、生长期为嫩芽绿、播种期为警告橙、休耕中为灰色。进度条通过Stack+双层Row实现,上层Row的宽度为${statusProgress(it.status)}%,颜色与状态标签一致。"编辑农事单"链接使用primary绿色文字,点击后调用openEdit(idx)打开编辑弹窗。

十二、pageWorkshop农事工坊分析

pageWorkshop构建器展示农事工坊的实时数据和热门作物排行。

@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('26℃')
          .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('63%')
          .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('58℃')
          .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('1.8 h')
          .fontSize(11)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.success)
      }
      .layoutWeight(1)
      .padding(8)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(10)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')

农事工坊的数据四格展示了今日气温(26℃,primary绿)、土壤湿度(63%,accent金)、堆肥温度(58℃,warning橙)和今日工时(1.8h,success绿)。这四个数据从气候环境、土壤状态、堆肥进程和劳动投入四个维度量化了农事工坊的运营状况。气温使用主色绿表示"适宜",土壤湿度使用金色表示"需要关注"(63%可能偏高或偏低),堆肥温度使用警告橙表示"高温活跃"(58℃是堆肥发酵的理想温度),工时使用成功绿表示"已完成"。

十三、模态弹窗系统分析

13.1 harvestModalBody采收份额预约弹窗

@Builder
harvestModalBody() {
  Column({ space: 12 }) {
    Text('采收份额预约')
      .fontSize(17)
      .fontWeight(FontWeight.Bold)
      .fontColor(COLORS.textPrimary)
    TextInput({ placeholder: '份额内容(如 樱桃番茄一篮)', text: this.harvestBox })
      .fontSize(13)
      .fontColor(COLORS.textPrimary)
      .backgroundColor(COLORS.bg)
      .borderRadius(8)
      .height(40)
      .onChange((v: string) => {
        this.harvestBox = v;
      })
    TextInput({ placeholder: '到园日期(如 周六上午)', text: this.harvestDate })
      .fontSize(13)
      .fontColor(COLORS.textPrimary)
      .backgroundColor(COLORS.bg)
      .borderRadius(8)
      .height(40)
      .onChange((v: string) => {
        this.harvestDate = v;
      })
    Text('农场管家确认成熟度后短信通知 · 可到园现摘或冷链直送')
      .fontSize(11)
      .fontColor(COLORS.textHint)
      .width('100%')
    Row({ space: 10 }) {
      Button()
        .layoutWeight(1)
        .height(38)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .onClick(() => {
          this.harvestOpen = false;
        })
      Text('取消')
        .fontSize(13)
        .fontColor(COLORS.textSecondary)
        .margin({ left: -52 })
      Button()
        .layoutWeight(1)
        .height(38)
        .backgroundColor(COLORS.accent)
        .borderRadius(8)
        .onClick(() => {
          this.doHarvest();
        })
      Text('提交预约')
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.white)
        .margin({ left: -56 })
    }
    .width('100%')
  }
  .width('100%')
  .padding(16)
  .backgroundColor(COLORS.cardBg)
}

harvestModalBody弹窗包含份额内容输入、到园日期输入和说明文字。份额内容的placeholder为"份额内容(如樱桃番茄一篮)“,到园日期的placeholder为"到园日期(如周六上午)”。说明文字"农场管家确认成熟度后短信通知·可到园现摘或冷链直送"使用textHint浅色显示,传达了预约确认流程和两种取货方式。提交按钮使用accent金色背景,与header中"采收预约"按钮和"去逛逛"按钮的颜色一致,形成了采收操作色彩的一致性。

13.2 delModalBody弃耕/退课弹窗

@Builder
delModalBody() {
  Column({ space: 12 }) {
    Text(this.delTarget === 'plot' ? '弃耕首块菜畦' : '退出种植课堂')
      .fontSize(17)
      .fontWeight(FontWeight.Bold)
      .fontColor(COLORS.textPrimary)
    Text(this.delTarget === 'plot' ? '将退租首块菜畦并移植现有作物,确认执行?' : '将退出首门进行中的课程,确认执行?')
      .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变量区分两种删除场景——delTarget为’plot’时显示"弃耕首块菜畦"和退租说明(“将退租首块菜畦并移植现有作物”),为’course’时显示"退出种植课堂"和退课说明。确认按钮使用danger红色背景,传达破坏性操作的视觉警示。弃耕说明中的"移植现有作物"体现了屋顶农场的生态责任——即使用户弃耕,现有作物也会被移植而非丢弃。

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

build方法的结构与前面两个应用完全一致——Stack根容器从底到顶依次堆叠背景层(bg浅绿灰色背景Column)、fxLayer特效层(水滴+阳光双粒子)、mainContent+bottomBar内容层和四个条件渲染的弹窗层。harvestOpen弹窗的条件渲染与addOpen、editOpen、delOpen并列。每个弹窗容器宽度88%、最大高度80%、圆角16、zIndex(999),确保弹窗始终位于最顶层且不会撑满全屏。

mainContent构建器的双层Tab路由逻辑也与前两个应用一致——mainTab为0时渲染subNav+7个子页面之一,mainTab为1渲染pageMarket(采收橱窗),mainTab为2渲染pageCourse(种植课堂),mainTab为3渲染pageMine(我的)。所有内容区域统一使用垂直Scroll容器,隐藏滚动条,左右padding 14,底部padding 20。

十五、Mermaid架构流程图

渲染错误: Mermaid 渲染失败: Parse error on line 37: ... --> |doDel| PTL或PL splice删除 F4 --> -----------------------^ Expecting 'SEMI', 'NEWLINE', 'EOF', 'AMP', 'START_LINK', 'LINK', 'LINK_ID', got 'NODE_STRING'

上图完整展示了PageRooftopFarm组件的架构。fxLayer特效层包含两组粒子——5个金色水滴粒子(3x8px竖条形态,模拟下落)和6个嫩芽绿阳光光点(4x4px方块形态,模拟闪烁),共11个元素。数据流方面,四种弹窗的操作最终归结为对postList和plotList的增删改,操作结果通过unshift反馈到postList中展示为社区动态。header中的采收卡和市集横幅都通过openHarvest方法连接到采收预约弹窗,形成了"从信息展示到操作执行"的完整交互闭环。

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

维度PostItemCropItemPlotItemGearItem
业务语义农友社区动态采收橱窗作物菜畦生长进度单农具库物资
属性数量6个6个5个4个
核心字段nick, text, likesname, price, levelstatus, progressname, desc, icon
装饰器@Observed@Observed@Observed@Observed
可变性可增(unshift)只读展示可改(splice替换)只读展示
使用页面pageCircle, pageFeaturedpageMarket, pageWorkshoppageCrafts, pageMinepageGear, pageWorkshop
颜色映射levelColor, cropTagColorstatusColor, statusProgress
弹窗关联addModalBody, harvestModalBodyeditModalBody, delModalBody
状态枚举当季/健康/限定/入门/人气/手作采收中/结果期/生长期/播种期/休耕中
维度fxLayer双粒子header头部subNav子导航bottomBar底部导航
构建器类型@Builder@Builder@Builder@Builder
核心功能水滴下落+阳光闪烁搜索+采收卡+市集+数据四格7子Tab花盆方块式4主Tab底部导航
粒子数量水滴5+光点6=11
粒子形态3x8竖条+4x4方块
粒子颜色accent金+sprout嫩芽绿
交互行为HitTestMode.None(穿透)可点击(采收预约)可点击(切换subTab)可点击(切换mainTab)
状态依赖tick, glow无(静态)subTabmainTab
动效驱动setInterval 90ms
设计特色竖条水滴模拟重力下落气温+湿度+堆肥温度工坊数据金色方块选中态最简洁opacity透明度选中态

十七、三应用横向对比总结

对比维度晶语(935)翱语(936)阡语(937)
业务场景UV滴胶手作工坊热气球观光俱乐部屋顶农场采收共享
主色/辅色紫色#7B6CAE/青色#5FB8A0橙色#C96A3B/蓝色#3E8FB8绿色#5E8C4F/金色#E0A83C
特色色字段glitter #F2B8C6(粉色)sky #A8CFE3(天空蓝)sprout #A8C686(嫩芽绿)
粒子类型气泡5+星芒6=11气球4+云絮3+光点6=13水滴5+光点6=11
粒子形态圆形+文字emojiemoji+横条+小方块竖条+小方块
子导航风格圆盘序号式吊篮圆点竖线式花盆方块式
数据模型PostItem/ResinItem/WorkItem/GearItemPostItem/FlyItem/TripItem/GearItemPostItem/CropItem/PlotItem/GearItem
弹窗种类发晶语/编辑作品单/撤销退课/定制下单发翱语/编辑飞行单/取消退课/系留预约发阡语/编辑农事单/弃耕退课/采收预约
状态枚举固化中/打磨中/脱模中/已交付/待开工已完飞/飞行中/候飞中/备航中/待排期采收中/结果期/生长期/播种期/休耕中
入口组件名PageResinCraftPageBalloonSafariPageRooftopFarm

十八、总结与展望

本文对基于HarmonyOS 6.1.1和ArkTS API 24的阡语城市屋顶农场与采收共享应用进行了全面而深入的架构解析。从ColorPalette色彩系统中绿色与金色的双主色设计到sprout嫩芽绿特色色的巧妙运用,从@Observed数据模型类PostItem、CropItem、PlotItem、GearItem的业务实体映射到静态数据数组的场景化初始化,从纯函数体系dropX/Y/A、sunX/Y/A的动效数学计算到ccColor、statusColor、statusProgress、cropTagColor、levelColor的颜色映射逻辑,从fxLayer双粒子动效系统的水滴下落与阳光闪烁协同渲染到header头部搜索条+采收卡+市集横幅+数据四格的信息架构,从subNav花盆方块式的简洁导航设计到四种模态弹窗的条件渲染架构,从pageCrafts菜畦进度页面的生长状态可视化到pageWorkshop农事工坊的实时环境数据展示,我们逐层剖析了每一个技术实现细节。

从动效系统设计的角度来看,阡语应用的双粒子动效在三个应用中具有独特的物理真实感。水滴粒子使用3x8px的竖向长方形形态,通过较大的y方向运动系数(tick*9)实现快速下落,模拟了灌溉水滴在重力作用下的真实运动轨迹。阳光光点使用4x4px的小方块形态,通过1:5的高低透明度比例(每6个tick中有一次0.85高亮,其余0.2低亮)实现了阳光闪烁的自然节奏。两种粒子的颜色选择也颇具深意——水滴使用accent金色#E0A83C而非蓝色,这是因为屋顶农场的水滴是在阳光照射下呈现的金色反光;阳光光点使用sprout嫩芽绿#A8C686而非黄色,这是因为阳光透过植物叶片后反射的是绿色光斑。这种将物理真实感与场景语义融入粒子设计的思路,使得阡语应用的动效系统不仅是一个视觉装饰,更是一个场景叙事的组成部分。


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

const COLORS: ColorPalette = {
  primary: '#5E8C4F',
  primaryLight: '#EAF2E4',
  primaryDark: '#44683A',
  accent: '#E0A83C',
  accentLight: '#FBF3E0',
  bg: '#F5F7F0',
  cardBg: '#FFFFFF',
  textPrimary: '#333A2E',
  textSecondary: '#68705F',
  textHint: '#B4BCA8',
  border: '#E6ECDD',
  success: '#7BA37B',
  warning: '#D9973B',
  danger: '#C0503F',
  white: '#FFFFFF',
  sprout: '#A8C686'
};

// ============ 农友动态数据模型 ============
@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 CropItem {
  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 PlotItem {
  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 CropChartItem {
  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, '屋顶园丁', '🍅', 'A-07 的樱桃番茄全红了,采收走起。', '5分钟前', 102),
  new PostItem(2, '堆肥管理员', '🍂', '本周堆肥仓翻堆两次,温度到 58℃。', '18分钟前', 84),
  new PostItem(3, '雨水收集员', '💧', '昨夜一场雨,收集桶进水 240 升。', '35分钟前', 76),
  new PostItem(4, '阡语主理人', '🌾', '秋播窗口开启:菠菜、樱桃萝卜上架。', '1小时前', 71),
  new PostItem(5, '蜜蜂邻居', '🐝', '授粉箱今天进出量翻倍,坐果稳了。', '2小时前', 66),
  new PostItem(6, '城市农夫', '🥬', '鸡毛菜 25 天从种到收,太有成就感。', '3小时前', 58),
  new PostItem(7, '晒图小能手', '📷', '晚霞下的屋顶菜园,美成一幅画。', '5小时前', 49),
  new PostItem(8, '亲子农团', '👨‍👩‍👧', '孩子第一次拔萝卜,比吃零食开心。', '昨天', 44)
];

const CROP_LIST: CropItem[] = [
  new CropItem(1, '樱桃番茄 · 采收篮', '现摘 500g · 沙瓤', '¥18/篮', '当季', 95),
  new CropItem(2, '水果黄瓜 · 现摘', '脆嫩 · 免削皮', '¥12/斤', '当季', 90),
  new CropItem(3, '羽衣甘蓝 · 份额装', '250g · 冷链', '¥15/份', '健康', 82),
  new CropItem(4, '草莓 · 秋季头茬', 'A 区 · 限 40 篮', '¥39/篮', '限定', 88),
  new CropItem(5, '芝麻菜 · 香草拼', '微辣 · 沙拉搭档', '¥9/袋', '入门', 64),
  new CropItem(6, '南瓜 · 贝贝小果', '单个 600g · 粉糯', '¥16/个', '人气', 72),
  new CropItem(7, '香草盆 · 三味', '罗勒 · 薄荷 · 迷迭香', '¥28/盆', '手作', 60),
  new CropItem(8, '屋顶蜂蜜 · 授粉蜜', '2026 秋 · 限量', '¥68/瓶', '限定', 77)
];

const PLOT_LIST: PlotItem[] = [
  new PlotItem(1, 'A-07 樱桃番茄', '第 62 天 · 垂熟期', '采收中', 88),
  new PlotItem(2, 'B-02 水果黄瓜', '搭架完成 · 追肥一次', '结果期', 62),
  new PlotItem(3, 'C-11 菠菜', '秋播第 9 天 · 出苗齐', '生长期', 35),
  new PlotItem(4, 'A-03 草莓', '覆膜 · 等头茬', '生长期', 28),
  new PlotItem(5, 'D-05 贝贝南瓜', '人工授粉 · 单果 600g', '结果期', 70),
  new PlotItem(6, 'B-07 芝麻菜', '昨播 · 未出苗', '播种期', 8),
  new PlotItem(7, 'C-03 香草角', '三味拼 · 可随采', '采收中', 90),
  new PlotItem(8, 'D-01 休耕绿肥', '苕子翻压 · 养地中', '休耕中', 12)
];

const GEAR_LIST: GearItem[] = [
  new GearItem(1, '折叠小铲 · 三件套', '松土 · 移苗', '🛠️'),
  new GearItem(2, '洒水壶 · 长嘴', '2L · 细雨喷洒', '🚿'),
  new GearItem(3, '园艺手套 · 防刺', '加胶掌面', '🧤'),
  new GearItem(4, '堆肥桶 · 密封', '30L · 免踩踏', '🪣'),
  new GearItem(5, '标签牌 · 可擦写', '品名 + 日期', '🏷️'),
  new GearItem(6, '遮阳网 · 三针', '防晒 · 透气', '⛱️'),
  new GearItem(7, '温度湿度计', '插土双显', '🌡️'),
  new GearItem(8, '采收篮 · 藤编', '透气 · 可折叠', '🧺')
];

const WEEK_CHART: WeekChartItem[] = [
  { label: '周一', value: 6 },
  { label: '周二', value: 8 },
  { label: '周三', value: 7 },
  { label: '周四', value: 10 },
  { label: '周五', value: 13 },
  { label: '周六', value: 22 },
  { label: '周日', value: 18 }
];

const CROP_CHART: CropChartItem[] = [
  { label: '茄果类', value: 30, color: '#5E8C4F' },
  { label: '叶菜类', value: 26, color: '#A8C686' },
  { label: '瓜豆类', value: 18, color: '#E0A83C' },
  { label: '香草类', value: 14, color: '#D9973B' },
  { label: '根茎类', value: 12, color: '#7BA37B' }
];

const TYPE_LIST: PlayTypeItem[] = [
  { name: '菜畦认领', desc: '3㎡ · 全程托管', 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: '#5E8C4F' },
  { title: '秋播时令与种子处理', week: '第 2 周', progress: 88, color: '#A8C686' },
  { title: '水肥一体与雨水灌溉', week: '第 3 周', progress: 62, color: '#E0A83C' },
  { title: '常见虫害绿色防治', week: '第 4 周', progress: 34, color: '#7BA37B' },
  { title: '堆肥发酵与土壤改良', week: '第 5 周', progress: 11, color: '#D9973B' }
];

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

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

const SUB_NAV_LIST: string[] = ['精选', '采收橱窗', '我的菜畦', '农事工坊', '种植课堂', '农友圈', '农具库'];

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

function ccColor(v: number): string {
  if (v > 12) {
    return '#5E8C4F';
  } else if (v > 8) {
    return '#A8C686';
  }
  return '#E0A83C';
}

function dropX(tick: number, i: number): number {
  return 50 + ((tick * 3 + i * 223) % 600);
}

function dropY(tick: number, i: number): number {
  return 30 + ((tick * 9 + i * 167) % 620);
}

function dropA(tick: number, i: number): number {
  return 0.15 + ((tick + i) % 4) * 0.07;
}

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

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

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

function statusColor(s: string): string {
  if (s === '采收中') {
    return '#5E8C4F';
  } else if (s === '结果期') {
    return '#E0A83C';
  } else if (s === '生长期') {
    return '#A8C686';
  } else if (s === '播种期') {
    return '#D9973B';
  }
  return '#B4BCA8';
}

function statusProgress(s: string): number {
  if (s === '采收中') {
    return 90;
  } else if (s === '结果期') {
    return 65;
  } else if (s === '生长期') {
    return 32;
  } else if (s === '播种期') {
    return 8;
  }
  return 12;
}

function cropTagColor(g: string): string {
  if (g.indexOf('番茄') >= 0 || g.indexOf('南瓜') >= 0) {
    return '#5E8C4F';
  } else if (g.indexOf('限定') >= 0 || g.indexOf('蜂蜜') >= 0) {
    return '#E0A83C';
  } else if (g.indexOf('草莓') >= 0 || g.indexOf('香草') >= 0) {
    return '#A8C686';
  }
  return '#D9973B';
}

function levelColor(s: string): string {
  if (s === '入门') {
    return '#7BA37B';
  } else if (s === '当季') {
    return '#5E8C4F';
  } else if (s === '限定') {
    return '#E0A83C';
  } else if (s === '健康') {
    return '#A8C686';
  } else if (s === '手作') {
    return '#D9973B';
  }
  return '#C0503F';
}

@Entry
@Component
struct PageRooftopFarm {
  @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 harvestOpen: boolean = false
  @State editIdx: number = 0
  @State delTarget: string = 'plot'
  @State editName: string = ''
  @State editStyle: string = ''
  @State editNote: string = ''
  @State addTitle: string = ''
  @State addContent: string = ''
  @State harvestBox: string = ''
  @State harvestDate: string = ''
  @State postList: PostItem[] = POST_LIST
  @State plotList: PlotItem[] = PLOT_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.plotList.length) {
      this.editIdx = index;
      this.editName = this.plotList[index].name;
      this.editStyle = this.plotList[index].status;
      this.editNote = this.plotList[index].item;
    }
    this.editOpen = true;
  }

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

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

  openHarvest(): void {
    this.harvestBox = '';
    this.harvestDate = '';
    this.harvestOpen = 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.plotList.length) {
      this.plotList.splice(this.editIdx, 1, new PlotItem(this.plotList[this.editIdx].id, this.editName, this.editNote, this.editStyle, 40));
    }
    this.editOpen = false;
  }

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

  doHarvest(): void {
    if (this.harvestBox.length > 0) {
      this.postList.unshift(new PostItem(998, '我的阡语', '🧺', `已预约采收份额:${this.harvestBox}`, '刚刚', 0));
    }
    this.harvestOpen = false;
  }

  @Builder
  fxLayer() {
    Stack({ alignContent: Alignment.TopStart }) {
      ForEach([0, 1, 2, 3, 4], (i: number) => {
        Column()
          .width(3)
          .height(8)
          .borderRadius(2)
          .backgroundColor(COLORS.accent)
          .opacity(dropA(this.tick, i))
          .translate({ x: dropX(this.tick, i), y: dropY(this.tick, i) })
      }, (i: number) => i.toString())
      ForEach([0, 1, 2, 3, 4, 5], (i: number) => {
        Column()
          .width(4)
          .height(4)
          .borderRadius(2)
          .backgroundColor(COLORS.sprout)
          .opacity(sunA(this.tick, i))
          .translate({ x: sunX(this.tick, i), y: sunY(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('A-07 菜畦 · 樱桃番茄全红')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Text('第 62 天 · 沙瓤头茬 · 今晨已采 3 篮')
            .fontSize(10)
            .fontColor('#F0F5EC')
          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('余 12 篮 · 手慢无')
              .fontSize(9)
              .fontColor('#F0F5EC')
          }
        }
        .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.openHarvest();
          })
      }
      .width('100%')
      .padding(14)
      .backgroundColor(COLORS.primary)
      .borderRadius(16)

      // 农夫市集横幅
      Row({ space: 10 }) {
        Text('🧺')
          .fontSize(22)
        Column({ space: 2 }) {
          Text('周末屋顶农夫市集 · 28 摊位')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('周六 9:00-12:00 · 屋顶直供 · 会员 9 折')
            .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.openHarvest();
          })
      }
      .width('100%')
      .padding(12)
      .backgroundColor(COLORS.cardBg)
      .borderRadius(14)

      // 数据四格
      Row({ space: 8 }) {
        Column({ space: 2 }) {
          Text('共享菜畦')
            .fontSize(9)
            .fontColor(COLORS.textHint)
          Text('86 块')
            .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('120 kg')
            .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('2.4 t')
            .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('¥95')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.warning)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
    }
    .width('100%')
    .padding({ top: 10, bottom: 4 })
  }

  @Builder
  subNav() {
    Scroll() {
      Row({ space: 10 }) {
        ForEach(SUB_NAV_LIST, (item: string, idx: number) => {
          Text(item)
            .fontSize(this.subTab === idx ? 13 : 11)
            .fontWeight(this.subTab === idx ? FontWeight.Bold : FontWeight.Normal)
            .fontColor(this.subTab === idx ? COLORS.white : COLORS.textSecondary)
            .padding({ left: 10, right: 10, top: 6, bottom: 6 })
            .backgroundColor(this.subTab === idx ? COLORS.accent : COLORS.cardBg)
            .borderRadius(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, 22))
                .borderRadius(5)
                .backgroundColor(ccColor(it.value))
              Text(`${it.value}`)
                .fontSize(9)
                .fontColor(COLORS.textSecondary)
              Text(it.label)
                .fontSize(9)
                .fontColor(COLORS.textHint)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }, (it: WeekChartItem) => it.label + it.value.toString())
        }
        .width('100%')
        .alignItems(VerticalAlign.Bottom)
        .height(140)
        .margin({ top: 12 })
        Row({ space: 6 }) {
          Text('周末采收人次占比')
            .fontSize(11)
            .fontColor(COLORS.textSecondary)
          Text('58%'
          )
            .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(CROP_CHART, (it: CropChartItem) => {
          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: CropChartItem) => 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(CROP_LIST, (it: CropItem, idx: number) => {
          if (idx % 2 === 0) {
            Row({ space: 10 }) {
              if (idx < CROP_LIST.length) {
                Column({ space: 6 }) {
                  Row() {
                    Text('🍅')
                      .fontSize(22)
                      .layoutWeight(1)
                    Text(CROP_LIST[idx].level)
                      .fontSize(10)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(levelColor(CROP_LIST[idx].level))
                      .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                      .backgroundColor(COLORS.accentLight)
                      .borderRadius(6)
                  }
                  .width('100%')
                  Text(CROP_LIST[idx].name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(`${CROP_LIST[idx].price} · 关注 ${CROP_LIST[idx].hot}`)
                    .fontSize(11)
                    .fontColor(COLORS.textSecondary)
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                .padding(12)
                .backgroundColor(COLORS.cardBg)
                .borderRadius(12)
              }
              if (idx + 1 < CROP_LIST.length) {
                Column({ space: 6 }) {
                  Row() {
                    Text('🥬')
                      .fontSize(22)
                      .layoutWeight(1)
                    Text(CROP_LIST[idx + 1].level)
                      .fontSize(10)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(levelColor(CROP_LIST[idx + 1].level))
                      .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                      .backgroundColor(COLORS.accentLight)
                      .borderRadius(6)
                  }
                  .width('100%')
                  Text(CROP_LIST[idx + 1].name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(`${CROP_LIST[idx + 1].price} · 关注 ${CROP_LIST[idx + 1].hot}`)
                    .fontSize(11)
                    .fontColor(COLORS.textSecondary)
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                .padding(12)
                .backgroundColor(COLORS.cardBg)
                .borderRadius(12)
              }
            }
            .width('100%')
          }
        }, (it: CropItem) => 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(CROP_LIST, (it: CropItem) => {
        Column({ space: 6 }) {
          Row() {
            Column()
              .width(4)
              .height(38)
              .borderRadius(2)
              .backgroundColor(cropTagColor(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(cropTagColor(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.openHarvest();
              })
          }
          .width('100%')
        }
        .width('100%')
        .padding(12)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(12)
      }, (it: CropItem) => it.id.toString())
    }
    .width('100%')
  }

  @Builder
  pageCrafts() {
    Column({ space: 10 }) {
      Text('我的菜畦')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .width('100%')
      ForEach(this.plotList, (it: PlotItem, 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: PlotItem) => 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('26℃')
            .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('63%'
          )
            .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('58℃')
            .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('1.8 h')
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.success)
        }
        .layoutWeight(1)
        .padding(8)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      ForEach(CROP_LIST.slice(0, 5), (it: CropItem, 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(cropTagColor(it.name))
        }
        .width('100%')
        .padding(10)
        .backgroundColor(COLORS.cardBg)
        .borderRadius(10)
      }, (it: CropItem) => 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('金畦会员 · 认领 6 畦 · 采收 42 次 · 陪伴 96 天')
            .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.plotList.length}`)
            .fontSize(12)
            .fontColor(COLORS.primary)
        }
        .width('100%')
        ForEach(this.plotList.slice(0, 3), (it: PlotItem) => {
          Row() {
            Text(it.name)
              .fontSize(13)
              .fontColor(COLORS.textSecondary)
              .layoutWeight(1)
            Text(it.status)
              .fontSize(11)
              .fontColor(statusColor(it.status))
          }
          .width('100%')
        }, (it: PlotItem) => 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.harvestOpen = 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 === 'plot' ? '弃耕首块菜畦' : '退出种植课堂')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      Text(this.delTarget === 'plot' ? '将退租首块菜畦并移植现有作物,确认执行?' : '将退出首门进行中的课程,确认执行?')
        .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
  harvestModalBody() {
    Column({ space: 12 }) {
      Text('采收份额预约')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
      TextInput({ placeholder: '份额内容(如 樱桃番茄一篮)', text: this.harvestBox })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.harvestBox = v;
        })
      TextInput({ placeholder: '到园日期(如 周六上午)', text: this.harvestDate })
        .fontSize(13)
        .fontColor(COLORS.textPrimary)
        .backgroundColor(COLORS.bg)
        .borderRadius(8)
        .height(40)
        .onChange((v: string) => {
          this.harvestDate = v;
        })
      Text('农场管家确认成熟度后短信通知 · 可到园现摘或冷链直送')
        .fontSize(11)
        .fontColor(COLORS.textHint)
        .width('100%')
      Row({ space: 10 }) {
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.bg)
          .borderRadius(8)
          .onClick(() => {
            this.harvestOpen = false;
          })
        Text('取消')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .margin({ left: -52 })
        Button()
          .layoutWeight(1)
          .height(38)
          .backgroundColor(COLORS.accent)
          .borderRadius(8)
          .onClick(() => {
            this.doHarvest();
          })
        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.harvestOpen) {
        Stack() {
          this.modalOverlay()
          Column() {
            this.harvestModalBody()
          }
          .width('88%')
          .constraintSize({ maxHeight: '80%' })
          .borderRadius(16)
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
      }
    }
    .width('100%')
    .height('100%')
  }
}


在这里插入图片描述

从子导航设计的角度来看,阡语应用的"花盆方块式"subNav是三个应用中最简洁的设计方案。每个子Tab仅由一个Text组件构成,通过padding和borderRadius形成方块外观,选中态通过accent金色背景+白色文字实现高对比度标识。与晶语应用的"圆盘序号式"(圆点+序号+文字三元素组合)和翱语应用的"吊篮圆点竖线式"(圆点+竖线+文字三元素组合)相比,花盆方块式只有文字一个元素,在视觉上最为干净利落。这种简洁性并非偷懒,而是有意为之——屋顶农场场景本身充满了绿色的植物和自然元素,子导航作为功能入口应当尽量简洁,避免与内容区域的视觉元素竞争注意力。金色选中态在绿色主调的应用中具有最强的视觉冲击力,确保了用户在快速滑动子导航时能够准确识别当前所在的位置。

从色彩系统的角度来看,绿色#5E8C4F与金色#E0A83C的双主色搭配在屋顶农场场景中具有深层的语义关联。绿色是植物的生命色,代表着农场的基础——菜畦、作物、生长、生态;金色是阳光和丰收的象征色,代表着农场的产出——阳光照射、果实成熟、采收喜悦、市集交易。这两种颜色在自然界中本就相伴出现——金色的阳光照射在绿色的植物上,是农业最基本的视觉意象。辅助色sprout嫩芽绿#A8C686作为介于绿色和黄色之间的过渡色,既呼应了嫩芽初生的色彩,又在视觉上连接了绿色主色和金色辅色,使得整个色彩系统在色相上形成了一个连续的自然光谱。

从状态机设计的角度来看,阡语应用的五阶段作物生长状态机(播种期-生长期-结果期-采收中-休耕中)在三个应用中最为贴近自然生命周期。晶语应用的作品状态机(待开工-固化中-脱模中-打磨中-已交付)描述的是人工制作流程,翱语应用的飞行状态机(待排期-备航中-候飞中-飞行中-已完飞)描述的是服务执行流程,而阡语应用的生长状态机描述的是自然生长过程——从种子播种到幼苗生长到开花结果到成熟采收到休耕养地,每个阶段都对应着不同的自然规律和农事操作。这种将自然生命周期映射为状态机的设计,使得应用不仅是一个管理工具,更是一个帮助用户理解农业规律的科普平台。

从信息架构的角度来看,阡语应用的header设计在信息密度和场景适配之间取得了良好的平衡。搜索条支持"搜作物/份额/农事服务"三类搜索,采收进度卡展示了具体菜畦的采收信息和剩余份额,农夫市集横幅提供了周末活动的预告,数据四格从菜畦数、采收量、雨水收集和农事币四个维度量化了农场运营。这四个模块从搜索、操作、活动和统计四个层面覆盖了用户的核心信息需求,且每个模块都紧密围绕屋顶农场场景展开,没有冗余的通用化设计。

从三个应用的横向比较来看,阡语应用在技术架构上与前两个应用保持了高度的一致性——相同的@Entry+@Component入口模式、相同的@State变量分类方式、相同的aboutToAppear/aboutToDisappear生命周期管理、相同的fxLayer+header+subNav+mainContent+bottomBar+modalOverlay的@Builder拆分模式、相同的build方法Stack分层架构、相同的条件渲染弹窗系统。这种架构一致性体现了HarmonyOS ArkTS API 24声明式UI开发范式的成熟度和通用性——同一套架构模式可以适配完全不同的业务场景,只需替换数据模型、调整色彩系统、修改动效粒子的视觉表现和子导航的交互形态,就能快速构建出风格统一但特色鲜明的应用。这种"架构统一、场景多变"的开发模式,正是HarmonyOS全场景应用生态的核心价值所在。

Logo

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

更多推荐