一、技术背景与平台概述

HarmonyOS 6.1.1 作为华为全场景分布式操作系统的最新迭代版本,为开发者提供了更为成熟的应用开发框架与更为丰富的系统能力。在本项目中,我们基于 HarmonyOS ArkTS API 24 这一套声明式开发范式,构建了一个名为"绿植运输 GREEN HAUL"的大型绿植盆栽运输加苗圃商城综合平台。该平台覆盖了大型绿植、盆栽树、花卉的专业运输场景,包括防倾倒、保温保湿等核心运输保障能力,同时融合了苗圃商城选购与养护社区互动两大垂直业务模块,形成了一个完整的绿色生活服务生态闭环。整个应用采用植物自然清新风格,以森林绿、泥土棕、阳光黄为主色调,营造出贴近自然的视觉体验。

在这里插入图片描述

ArkTS API 24 是 HarmonyOS 6.1.1 时代声明式 UI 开发的核心语言规范,它在 TypeScript 的基础上进行了严格的静态类型约束与运行时优化,使得开发者能够以更安全、更高效的方式构建复杂界面。本项目的全部源码采用单一的 ArkTS 文件编写,通过 @Entry、@Component、@State、@Observed、@Builder 等装饰器构建出层次分明、职责清晰的组件体系。文件中定义了五个 Tab 页面——叫车、苗圃、养护、社区、我的,以及四个弹框——预约绿植运输、编辑养护计划、取消运输订单、购买绿植,共计约三千行代码,涵盖了数据建模、配置映射、纯函数辅助、粒子动画、表单交互、弹框管理、列表渲染等移动应用开发的典型场景。

从架构设计的角度来看,本项目采用了"配置驱动加纯函数辅助加组件化构建"的三层架构模式。最底层是一组颜色常量与数据模型接口,通过 @Observed 装饰的类实现可观察的数据对象;中间层是大量静态配置映射与无副作用的纯函数,负责将业务逻辑与视图渲染解耦;最上层是六个 @Component 组件,包括一个 @Entry 主页面与五个 Tab 内容组件,每个组件通过 @Builder 方法将 UI 拆分为可复用的构建单元。这种架构模式使得代码具备高度的可读性与可维护性,同时也为后续的功能扩展预留了充足的弹性空间。

在交互体验方面,本项目特别注重动态视觉效果的营造。主页面通过 setInterval 定时器驱动一套漂浮叶子粒子系统,以三百五十毫秒为周期不断更新十片叶子的位置坐标,在页面顶层以不阻挡用户点击的方式漂浮游动,为整个应用注入了植物自然生长的动态生命力。此外,每个 Tab 页面都设计了丰富的交互反馈机制,包括统计条数字滚动、柱状图可视化、日历月历养护标记、社区帖子点赞收藏、商品详情弹框购买流程等,充分体现了基于 HarmonyOS API 24 声明式 UI 范式在构建复杂业务场景时的表达能力与开发效率优势。

二、整体架构流程图

下面的 Mermaid 流程图展示了绿植运输平台从启动到各 Tab 业务模块的完整调用关系与数据流向,帮助读者在进入代码细节之前建立全局认知。

BOOKING

NURSERY

CARE

COMMUNITY

PROFILE

应用启动 aboutToAppear

初始化叶子粒子定时器

GreenHaulPage 主页面渲染

headerBuilder 头部构建

contentArea 内容区构建

tabBarBuilder 底部Tab栏

particleLayer 粒子层

activeTab 判断

BookingContent 叫车

NurseryContent 苗圃

CareContent 养护

CommunityContent 社区

ProfileContent 我的

统计条 + 柱状图

叫车表单

最近运输列表

预约绿植运输弹框

取消运输订单弹框

推荐横滑大卡

排序药丸 + 难度分布

商品网格

购买绿植弹框

今日待养护提醒

日历月历标记

养护计划卡列表

编辑养护计划弹框

发布按钮 + 热门话题

帖子Feed列表

点赞评论交互

自然风资料卡

大数字统计网格

设置列表

定时器驱动 advanceParticles

三、逐段代码深度解析

3.1 颜色常量体系:植物自然清新风格的设计基石

const COLOR_BG: string = '#F5F3EE'
const COLOR_CARD: string = '#FFFFFF'
const COLOR_PRIMARY: string = '#2D6A4F'
const COLOR_PRIMARY_LIGHT: string = '#52B788'
const COLOR_PRIMARY_DARK: string = '#1B4332'
const COLOR_BROWN: string = '#774936'
const COLOR_BROWN_LIGHT: string = '#B08968'
const COLOR_YELLOW: string = '#E6C227'
const COLOR_PINK: string = '#E07A5F'
const COLOR_TEXT_MAIN: string = '#1B3329'
const COLOR_TEXT_SUB: string = '#6B8E7B'
const COLOR_TEXT_HINT: string = '#A8C5B5'
const COLOR_BORDER: string = '#D8E4DC'
const COLOR_SUCCESS: string = '#40916C'
const COLOR_WARNING: string = '#E6C227'
const COLOR_DANGER: string = '#BC4749'

在这里插入图片描述

本段代码定义了整个应用的全局颜色常量体系,这是植物自然清新风格视觉设计的基石。设计团队精心选取了一组以森林绿为主色调、泥土棕为辅助色、阳光黄为点缀色的配色方案,营造出贴近自然、清新舒适的视觉氛围。其中 COLOR_PRIMARY 森林绿 #2D6A4F 作为主品牌色贯穿全应用的按钮、选中态、标题强调等场景;COLOR_PRIMARY_LIGHT #52B788 与 COLOR_PRIMARY_DARK #1B4332 分别用于浅色高亮与深色强调,形成层次分明的绿色梯度。泥土棕色系 COLOR_BROWN 与 COLOR_BROWN_LIGHT 用于价格、运输等与土壤花盆相关的场景,强化主题关联。文字色被细分为三级——COLOR_TEXT_MAIN 主文本、COLOR_TEXT_SUB 次文本、COLOR_TEXT_HINT 提示文本,确保信息层级的清晰可读。背景色 COLOR_BG #F5F3EE 采用温暖的米白色而非纯白,让长时间阅读不疲劳,这与卡片背景的纯白形成微妙对比。将这些颜色统一提取为常量,既保证了全应用视觉一致性,也使得后期换肤或主题调整只需修改一处即可全局生效,体现了良好的工程化思维。

3.2 运输订单数据模型:@Observed 可观察对象

interface TransportOrderModel {
  id: number
  orderNo: string
  plantName: string
  pickup: string
  destination: string
  price: number
  status: string
  date: string
  potSize: string
}

@Observed
class TransportOrderItem implements TransportOrderModel {
  id: number = 0
  orderNo: string = ''
  plantName: string = ''
  pickup: string = ''
  destination: string = ''
  price: number = 0
  status: string = '待接单'
  date: string = ''
  potSize: string = '中'
  constructor(id: number, orderNo: string, plantName: string, pickup: string,
    destination: string, price: number, status: string, date: string, potSize: string) {
    this.id = id; this.orderNo = orderNo; this.plantName = plantName
    this.pickup = pickup; this.destination = destination; this.price = price
    this.status = status; this.date = date; this.potSize = potSize
  }
}

在这里插入图片描述

这段代码展示了基于 HarmonyOS API 24 的数据建模模式,采用了 interface 与 @Observed class 双层结构。首先定义 TransportOrderModel 接口声明了运输订单的完整字段契约,包含订单编号、植物名称、出发地、目的地、价格、状态、日期、花盆尺寸等九个核心属性。随后通过 @Observed 装饰器修饰的 TransportOrderItem 类实现该接口,@Observed 的作用是使该类的实例成为可观察对象——当其属性发生变化时,绑定了该对象的 UI 组件会自动触发重新渲染。值得注意的是,类中所有属性都显式声明了默认值,这是 ArkTS 严格类型系统的要求,确保对象在构造前就具备合法的初始状态。构造函数接收全部参数并逐一赋值,这种全量构造的设计使得静态数据初始化时能够以一行代码创建完整的订单对象。status 属性默认值为"待接单",potSize 默认值为"中",这些默认值的选取基于业务场景中最常见的情况,降低了调用方在不需要特殊指定时的参数传递负担。

3.3 苗圃商品数据模型与养护计划模型

interface NurseryProductModel {
  id: number
  name: string
  price: number
  height: string
  careDifficulty: number
  stock: number
  color: string
  tag: string
  desc: string
  isHot: boolean
}

@Observed
class NurseryProductItem implements NurseryProductModel {
  id: number = 0
  name: string = ''
  price: number = 0
  height: string = ''
  careDifficulty: number = 1
  stock: number = 0
  color: string = '#52B788'
  tag: string = ''
  desc: string = ''
  isHot: boolean = false
  constructor(id: number, name: string, price: number, height: string,
    careDifficulty: number, stock: number, color: string, tag: string,
    desc: string, isHot: boolean) {
    this.id = id; this.name = name; this.price = price; this.height = height
    this.careDifficulty = careDifficulty; this.stock = stock; this.color = color
    this.tag = tag; this.desc = desc; this.isHot = isHot
  }
}

在这里插入图片描述

苗圃商品模型 NurseryProductItem 同样遵循 interface 加 @Observed class 的建模范式,但字段设计更加贴近电商商品管理的业务需求。其中 careDifficulty 养护难度是一个数字类型,取值范围为一到四,分别对应简单、中等、困难三个等级,这种用数值表示难度而非直接用字符串的设计便于后续的排序、过滤与统计运算。color 属性存储一个十六进制颜色字符串,用于在商品卡片中以纯色色块代替图片展示商品的主色调,这种做法在没有真实图片资源的场景下是一种高效的视觉占位策略。isHot 布尔标记用于区分热卖商品与普通商品,驱动推荐横滑区的渲染逻辑。养护计划模型 CarePlanItem 则定义了浇水频率、光照需求、施肥频率、最近浇水日期、健康百分比等养护相关字段,其中 healthPercent 是一个核心指标,后续在养护卡片中通过彩色进度条直观展示植物当前的健康状态,数值越低颜色越偏危险红色,提醒用户及时干预。

3.4 社区帖子模型与叶子粒子模型

interface PostModel {
  id: number
  userName: string
  userAvatar: string
  content: string
  likeCount: number
  commentCount: number
  color: string
  tag: string
  time: string
}

@Observed
class PostItem implements PostModel {
  id: number = 0
  userName: string = ''
  userAvatar: string = '🌿'
  content: string = ''
  likeCount: number = 0
  commentCount: number = 0
  color: string = '#52B788'
  tag: string = ''
  time: string = ''
  constructor(id: number, userName: string, userAvatar: string, content: string,
    likeCount: number, commentCount: number, color: string, tag: string, time: string) {
    this.id = id; this.userName = userName; this.userAvatar = userAvatar
    this.content = content; this.likeCount = likeCount; this.commentCount = commentCount
    this.color = color; this.tag = tag; this.time = time
  }
}

interface LeafParticleModel {
  x: number
  y: number
  size: number
  opacity: number
  drift: number
  sway: number
}

在这里插入图片描述

社区帖子模型 PostItem 的设计体现了社交内容的典型结构:用户名、头像(此处用 Emoji 表情符号代替真实头像图片)、正文内容、点赞数、评论数、主题色、标签与发布时间。userAvatar 默认值为一个绿叶 Emoji,与应用主题高度契合。叶子粒子模型 LeafParticleModel 则是整个应用动态视觉效果的核心数据结构,它定义了每片漂浮叶子的六个物理属性:x 与 y 坐标定位叶子在屏幕上的位置,size 控制叶子 Emoji 的字号大小,opacity 决定叶子的透明度营造远近层次感,drift 是叶子上飘的速度,sway 是叶子左右摆动的幅度。这种用纯数据模型驱动动画的方式是声明式 UI 框架的典型做法,通过定时更新数据模型中的坐标值,框架自动完成 UI 的差异比对与重绘,开发者无需手动操作 DOM 或调用绘制接口,极大地简化了动画实现复杂度。

3.5 配置映射体系:Record 类型的查表式配置

interface OrderStatusMeta {
  label: string
  color: string
  bg: string
  icon: string
}

const ORDER_STATUS_CONFIG: Record<string, OrderStatusMeta> = {
  '待接单': { label: '待接单', color: COLOR_BROWN, bg: '#F0E6DC', icon: '🕐' },
  '运输中': { label: '运输中', color: COLOR_PRIMARY, bg: '#E3F0E9', icon: '🚐' },
  '已完成': { label: '已完成', color: COLOR_SUCCESS, bg: '#E1F2E8', icon: '✅' },
  '已取消': { label: '已取消', color: COLOR_DANGER, bg: '#F6E3E4', icon: '❌' }
}

const POT_SIZE_CONFIG: Record<string, PotSizeMeta> = {
  '小': { label: '小', icon: '🪴', desc: '直径<20cm' },
  '中': { label: '中', icon: '🪴', desc: '20-35cm' },
  '大': { label: '大', icon: '🌳', desc: '35-55cm' },
  '特大': { label: '特大', icon: '🌲', desc: '>55cm 需吊装' }
}

在这里插入图片描述

本段代码展示了基于 HarmonyOS API 24 的 Record 类型的查表式配置模式,这是整个应用配置驱动架构的核心体现。ORDER_STATUS_CONFIG 将四种订单状态——待接单、运输中、已完成、已取消——各自映射到包含标签文字、前景色、背景色、图标 Emoji 的元数据对象,使得状态展示的视觉样式完全由配置驱动,新增或修改状态只需在映射表中增减条目即可。POT_SIZE_CONFIG 则定义了四种花盆尺寸的展示信息,包括尺寸名称、图标与直径描述,"特大"尺寸还特别标注了"需吊装"的业务提示,体现了对运输场景的细致考量。这种 Record 查表模式的优势在于:将业务数据的展示逻辑从组件代码中彻底剥离,组件只需调用 getStatusMeta 等纯函数传入状态字符串即可获取完整的展示元数据,实现了数据与视图的完全解耦。后续的 PLANT_CATEGORY_CONFIG 植物分类配置、CARE_DIFFICULTY_CONFIG 难度配置、HOT_TOPIC_CONFIG 热门话题配置、TAB_META_CONFIG 底部 Tab 配置均采用相同模式,构成了统一的配置体系。

3.6 静态数据集:运输订单与苗圃商品

const ORDER_LIST: TransportOrderItem[] = [
  new TransportOrderItem(1, 'GH2026082301', '琴叶榕', '朝阳花木市场', '望京SOHO T1', 268, '运输中', '08-24 14:00', '大'),
  new TransportOrderItem(2, 'GH2026082202', '龟背竹', '顺义苗圃基地', '三里屯绿洲公寓', 328, '已完成', '08-22 09:30', '特大'),
  new TransportOrderItem(3, 'GH2026082103', '天堂鸟', '通州花卉园', '亦庄金地格林', 398, '已完成', '08-21 16:00', '特大'),
  new TransportOrderItem(4, 'GH2026082004', '散尾葵', '大兴绿植批发', '丽都悦府', 216, '已取消', '08-20 11:00', '大'),
  new TransportOrderItem(5, 'GH2026081905', '龙血树', '丰台花乡', '西红门荟聚', 188, '已完成', '08-19 10:30', '中'),
  new TransportOrderItem(6, 'GH2026081806', '发财树', '石景山苗圃', '苹果园社区', 156, '已完成', '08-18 15:00', '中'),
  new TransportOrderItem(7, 'GH2026082407', '橡皮树', '海淀温室大棚', '中关村软件园', 288, '待接单', '08-25 08:00', '大'),
  new TransportOrderItem(8, 'GH2026082408', '虎皮兰', '房山多肉基地', '长阳半岛', 128, '待接单', '08-25 13:00', '小')
]

在这里插入图片描述

本段代码定义了运输订单的静态数据集,包含八条覆盖不同状态的订单记录。每条订单通过 TransportOrderItem 构造函数创建,包含了真实的北京地区地址——朝阳花木市场、望京SOHO、顺义苗圃基地、三里屯绿洲公寓等,使数据具备真实业务场景的代入感。订单状态分布涵盖了待接单两单、运输中一单、已完成四单、已取消一单,这种分布既展示了各状态的视觉样式,也为统计函数提供了合理的计算样本。价格区间从一百二十八元的小型虎皮兰到三百九十八元的特大天堂鸟,体现了不同盆栽尺寸对运输成本的合理定价差异。苗圃商品数据集 PRODUCT_LIST 则包含十二款商品,从九十六元的鹤望兰小苗到四百二十八元的油橄榄树,careDifficulty 字段分布从一(简单)到四(困难),每款商品都配有标签、描述文案与是否热卖标记,为商品网格渲染与难度分布统计提供了完整的数据支撑。

3.7 纯函数辅助体系:业务逻辑与视图的解耦

function getStars(n: number): string {
  if (n >= 5) {
    return '★★★★★'
  }
  if (n === 4) {
    return '★★★★☆'
  }
  if (n === 3) {
    return '★★★☆☆'
  }
  if (n === 2) {
    return '★★☆☆☆'
  }
  return '★☆☆☆☆'
}

function getDifficultyLabel(n: number): string {
  if (n <= 2) {
    return '简单'
  }
  if (n === 3) {
    return '中等'
  }
  return '困难'
}

function getDifficultyColor(n: number): string {
  if (n <= 2) {
    return CARE_DIFFICULTY_CONFIG['简单'].color
  }
  if (n === 3) {
    return CARE_DIFFICULTY_CONFIG['中等'].color
  }
  return CARE_DIFFICULTY_CONFIG['困难'].color
}

在这里插入图片描述

本段代码定义了一组无副作用的纯函数,负责将数值化的业务字段转换为人类可读的展示文本与视觉样式。getStars 函数接收难度数值,返回对应的五角星字符串——满星用实心★表示,空缺用空心☆填充,这种用文本字符代替图标组件的做法在轻量级评分展示中既高效又直观。getDifficultyLabel 与 getDifficultyColor 两个函数分别将数值难度转换为难度的中文标签与对应的颜色值,它们内部都引用了 CARE_DIFFICULTY_CONFIG 配置映射表,实现了逻辑与配置的联动。这些纯函数的核心特征是:输入相同必定输出相同、不产生副作用、不依赖外部可变状态。在基于 HarmonyOS API 24 的声明式 UI 中,纯函数被广泛用于 @Builder 方法内部的属性计算与条件渲染,因为框架的渲染管线能够安全地缓存纯函数的返回值,在依赖数据未变化时跳过重复计算,从而优化渲染性能。后续的 getStatusMeta、getMonthOrderTotal、getInProgressCount、getCompletedCount、getTotalCost 等函数也都遵循同样的纯函数设计原则。

3.8 粒子动画核心:advanceParticles 驱动叶子漂浮

function advanceParticles(list: LeafParticle[]): LeafParticle[] {
  const next: LeafParticle[] = []
  for (let i = 0; i < list.length; i++) {
    const p: LeafParticle = list[i]
    let ny: number = p.y - p.drift
    let nx: number = p.x + p.sway
    if (ny < -40) {
      ny = 820
    }
    if (nx > 350) {
      nx = 5
    }
    if (nx < 0) {
      nx = 345
    }
    next.push(new LeafParticle(nx, ny, p.size, p.opacity, p.drift, p.sway))
  }
  return next
}

在这里插入图片描述

advanceParticles 是整个应用动态视觉效果的核心引擎函数,它接收当前粒子数组,返回一个新的粒子数组,实现了不可变数据的更新模式。函数遍历每个粒子,将其 y 坐标减去 drift 上飘速度实现向上漂浮,将 x 坐标加上 sway 实现左右摆动。边界处理逻辑确保粒子在视觉范围内循环:当叶子向上飘出屏幕顶部(y 小于负四十)时,将其重置到屏幕底部八百二十的位置重新开始上飘;当叶子向右飘出屏幕右边缘(x 大于三百五十)时,将其传送回左侧五的位置;当叶子向左飘出屏幕左边缘时,将其传送回右侧三百四十五的位置。这种环形边界处理使得十片叶子始终保持可见且持续运动,不会因为飘出屏幕而消失。关键设计在于函数返回的是一个全新的数组而非原地修改,每次都通过 new LeafParticle 创建新的粒子对象,这与 ArkTS 中 @State 的不可变更新原则完美契合——只有赋予 @State 变量全新的引用,框架才能检测到变化并触发 UI 重绘。

3.9 不可变列表操作:toggleInList 与 toggleIdInList

function toggleInList(list: string[], item: string): string[] {
  const idx: number = list.indexOf(item)
  if (idx >= 0) {
    const next: string[] = list.slice(0, idx).concat(list.slice(idx + 1))
    return next
  }
  return list.concat([item])
}

function toggleIdInList(list: number[], id: number): number[] {
  const idx: number = list.indexOf(id)
  if (idx >= 0) {
    return list.slice(0, idx).concat(list.slice(idx + 1))
  }
  return list.concat([id])
}

在这里插入图片描述

这两个函数是 ArkTS 声明式 UI 中处理列表状态变更的标准模式。toggleInList 用于字符串列表的切换操作——如果列表中已包含某项则移除它,如果未包含则添加它。在绿植运输平台中,它被用于运输要求勾选(防倾倒、保温、保湿、加急、夜间)与花盆尺寸选择等场景。toggleIdInList 则用于数字 ID 列表的切换,主要服务于社区帖子的点赞收藏功能。两个函数的核心设计理念是不可变更新:它们从不直接修改传入的数组,而是通过 slice 截取与 concat 拼接创建一个全新的数组返回。这种做法在基于 HarmonyOS API 24 的开发中至关重要,因为 @State 装饰的状态变量需要被赋予全新的引用才能触发依赖该状态的组件重新渲染。如果直接调用 push 或 splice 原地修改数组,虽然数组内容确实变了,但引用地址未变,框架的渲染管线可能无法感知到这一变化,导致 UI 不更新。这种不可变数据模式虽然在创建新数组时会有微小的内存开销,但它保证了状态变更的可追踪性与渲染的可靠性,是函数式编程思想在 ArkTS 中的典型应用。

3.10 主页面生命周期与粒子定时器

@Entry
@Component
struct GreenHaulPage {
  @State activeTab: number = GreenTab.BOOKING
  @State selectedCategory: string = '全部'
  @State searchKeyword: string = ''
  @State messageCount: number = 3
  @State leafParticles: LeafParticle[] = [
    new LeafParticle(30, 120, 16, 0.35, 1.6, 0.9),
    new LeafParticle(80, 320, 13, 0.28, 1.2, -0.7),
    new LeafParticle(140, 560, 18, 0.32, 1.8, 0.6),
    new LeafParticle(200, 80, 12, 0.25, 1.0, -1.0),
    new LeafParticle(250, 420, 20, 0.30, 2.0, 0.8),
    new LeafParticle(300, 700, 14, 0.27, 1.4, -0.9),
    new LeafParticle(330, 220, 15, 0.33, 1.5, 1.1),
    new LeafParticle(60, 680, 17, 0.29, 1.7, -0.6),
    new LeafParticle(170, 180, 11, 0.24, 1.1, 1.2),
    new LeafParticle(230, 520, 19, 0.31, 1.9, -0.8)
  ]
  private particleTimer: number = -1

  aboutToAppear(): void {
    this.particleTimer = setInterval(() => {
      this.leafParticles = advanceParticles(this.leafParticles)
    }, 350)
  }

  aboutToDisappear(): void {
    if (this.particleTimer >= 0) {
      clearInterval(this.particleTimer)
    }
  }

本段代码是整个应用的入口组件 GreenHaulPage 的状态定义与生命周期管理。@Entry 装饰器标记此组件为页面入口,@Component 声明它是一个自定义组件。组件内部通过 @State 定义了五个响应式状态变量:activeTab 控制当前激活的 Tab 页面,selectedCategory 记录当前选中的植物分类,searchKeyword 绑定搜索框文本,messageCount 管理消息角标数字,leafParticles 存储十片漂浮叶子的粒子数据。leafParticles 的初始值在声明时直接给定,十片叶子的坐标、大小、透明度、漂移速度、摆动幅度各不相同,确保动画效果自然不重复。particleTimer 是一个 private 定时器句柄,初始值为负一表示尚未启动。aboutToAppear 是 HarmonyOS API 24 组件生命周期的关键回调,在组件创建后、UI 渲染前被调用,此处通过 setInterval 注册了一个三百五十毫秒周期的定时器,每次触发时调用 advanceParticles 计算新的粒子位置并赋值给 leafParticles,触发粒子层的重新渲染。aboutToDisappear 在组件销毁时被调用,此处必须 clearInterval 清除定时器,否则组件销毁后定时器仍在运行会造成内存泄漏与无效渲染,这是 ArkTS 开发中资源管理的基本纪律。

3.11 粒子层构建器:hitTestBehavior 透传点击

@Builder
particleLayer() {
  Column() {
    ForEach(this.leafParticles, (p: LeafParticle) => {
      Text('🍃')
        .fontSize(p.size)
        .opacity(p.opacity)
        .position({ x: p.x, y: p.y })
    }, (p: LeafParticle, index: number) => index.toString() + '_' + p.x.toString() + '_' + p.y.toString())
  }
  .width('100%')
  .height('100%')
  .hitTestBehavior(HitTestMode.None)
}

particleLayer 是粒子动画的视觉渲染层,它通过 @Builder 装饰器定义为一个可复用的构建方法。内部使用 ForEach 遍历 leafParticles 数组,为每个粒子渲染一个叶子 Emoji 文本,fontSize 设置为粒子的 size 属性控制大小,opacity 设置透明度营造层次感,position 以绝对定位将叶子放置到计算好的坐标位置。ForEach 的第三个参数是键值生成器,它将索引与坐标拼接为唯一键——这里的设计非常精妙,因为粒子位置每三百五十毫秒变化一次,键值中包含坐标信息能够确保每次坐标变化都被 ForEach 识别为元素更新而非元素复用,从而正确触发位置重绘。最为关键的是 Column 容器上设置的 hitTestBehavior(HitTestMode.None),这是 HarmonyOS API 24 提供的点击测试模式控制属性。HitTestMode.None 表示该容器及其子节点完全不参与点击命中测试,所有触摸事件会穿透该层传递到下方的实际内容区域。这一设计确保了漂浮叶子虽然覆盖在内容之上,但绝不阻挡用户对底部内容的点击操作,实现了装饰性动画与功能交互的完美共存。

3.12 底部 Tab 栏构建:ForEach 驱动的动态渲染

@Builder
tabBarBuilder() {
  Row() {
    ForEach(TAB_KEY_LIST, (key: string) => {
      Column() {
        Text(TAB_META_CONFIG[key].icon)
          .fontSize(20)
        Text(TAB_META_CONFIG[key].label)
          .fontSize(10)
          .fontColor(this.activeTab === getTabIndex(key) ? COLOR_PRIMARY : COLOR_TEXT_SUB)
          .margin({ top: 2 })
        Column()
          .width(16)
          .height(2)
          .borderRadius(1)
          .backgroundColor(this.activeTab === getTabIndex(key) ? COLOR_PRIMARY_LIGHT : Color.Transparent)
          .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.Center)
      .layoutWeight(1)
      .onClick(() => {
        this.activeTab = getTabIndex(key)
      })
    }, (key: string) => key + '_' + activeTab.toString())
  }
  .width('100%')
  .height(56)
  .backgroundColor(COLOR_CARD)
  .border({ width: 1, color: COLOR_BORDER })
}

底部 Tab 栏的构建采用了 ForEach 遍历 TAB_KEY_LIST 数组的动态渲染模式,相比静态书写五个 Column 代码量大幅减少且扩展性更强。每个 Tab 项由图标 Emoji、标签文字与底部指示条三部分组成的 Column 构成。激活状态通过三元表达式动态控制:选中 Tab 的文字颜色为森林绿 COLOR_PRIMARY,底部指示条为浅绿 COLOR_PRIMARY_LIGHT 且可见;未选中 Tab 的文字为灰色 COLOR_TEXT_SUB,指示条透明不可见。layoutWeight(1) 使五个 Tab 项在 Row 中等宽分布。onClick 回调调用 getTabIndex 将 Tab 的字符串键转换为枚举数值赋给 activeTab,触发页面内容区的条件渲染切换。ForEach 的键值生成器将 Tab 键与当前 activeTab 拼接,这意味着每次切换 Tab 时,所有 Tab 项的键值都会变化,强制 ForEach 重新渲染全部项以更新选中态视觉。这种设计虽然会在切换时多渲染五个节点,但保证了选中态的视觉绝对正确,在 Tab 数量有限的场景下性能开销完全可控。

3.13 叫车 Tab 统计条与柱状图构建

@Builder
statCellBuilder(label: string, value: string, unit: string, color: string) {
  Column() {
    Text(value)
      .fontSize(17)
      .fontWeight(FontWeight.Bold)
      .fontColor(color)
    Text(unit)
      .fontSize(8)
      .fontColor(COLOR_TEXT_SUB)
      .margin({ top: 1 })
    Text(label)
      .fontSize(9)
      .fontColor(COLOR_TEXT_SUB)
      .margin({ top: 2 })
  }
  .alignItems(HorizontalAlign.Center)
  .layoutWeight(1)
}

@Builder
chartBuilder() {
  Column() {
    Text('📈 月度运输次数')
      .fontSize(13)
      .fontWeight(FontWeight.Bold)
      .fontColor(COLOR_TEXT_MAIN)
    Row() {
      ForEach(MONTHLY_LABELS, (m: string, index: number) => {
        Column() {
          Column()
            .width(14)
            .height(getBarHeight(MONTHLY_ORDER_COUNT[index]))
            .borderRadius({ topLeft: 4, topRight: 4 })
            .backgroundColor(index === MONTHLY_ORDER_COUNT.length - 1 ? COLOR_PRIMARY : COLOR_PRIMARY_LIGHT)
          Text(MONTHLY_ORDER_COUNT[index].toString())
            .fontSize(8)
            .fontColor(COLOR_TEXT_SUB)
            .margin({ top: 3 })
          Text(m)
            .fontSize(8)
            .fontColor(COLOR_TEXT_HINT)
        }
        .alignItems(HorizontalAlign.Center)
        .layoutWeight(1)
      }, (m: string) => m)
    }
    .alignItems(VerticalAlign.Bottom)
    .width('100%')
    .height(110)
    .margin({ top: 10 })
  }

叫车 Tab 的统计条 statCellBuilder 是一个参数化的 @Builder 方法,接收标签、数值、单位、颜色四个参数,通过 layoutWeight 等宽分布在 Row 中,四个统计格之间以一像素宽的分隔线 Column 隔开,分别展示本月运输总单数、进行中单数、已完成单数与总费用金额。柱状图 chartBuilder 则用纯 ArkTS 组件模拟了一个简单的条形图:ForEach 遍历六个月份标签,每个月份渲染一个 Column 柱子,柱子高度由 getBarHeight 纯函数根据订单数量按比例计算得出,最高二十六单对应七十六像素高度。当月(数组最后一项)的柱子使用深色 COLOR_PRIMARY,其他月份使用浅色 COLOR_PRIMARY_LIGHT,形成视觉强调。柱子下方依次显示数值与月份标签,Row 容器设置 alignItems(VerticalAlign.Bottom) 确保所有柱子底部对齐。这种用基础组件搭建图表的方式无需引入第三方图表库,在数据量不大且样式简单的场景下是一种轻量高效的方案。

3.14 运输要求勾选与花盆尺寸药丸

@Builder
reqPillBuilder(req: string) {
  Row() {
    Text(isReqChecked(this.formReqs, req) ? '☑' : '☐')
      .fontSize(12)
      .fontColor(isReqChecked(this.formReqs, req) ? COLOR_PRIMARY : COLOR_TEXT_HINT)
    Text(req)
      .fontSize(11)
      .fontColor(isReqChecked(this.formReqs, req) ? COLOR_PRIMARY : COLOR_TEXT_SUB)
      .margin({ left: 3 })
  }
  .alignItems(VerticalAlign.Center)
  .padding({ left: 10, right: 10, top: 6, bottom: 6 })
  .borderRadius(10)
  .backgroundColor(isReqChecked(this.formReqs, req) ? '#E3F0E9' : COLOR_CARD)
  .border({ width: 1, color: isReqChecked(this.formReqs, req) ? COLOR_PRIMARY : COLOR_BORDER })
  .margin({ right: 8, top: 6 })
  .onClick(() => {
    this.formReqs = toggleInList(this.formReqs, req)
  })
}

运输要求勾选药丸 reqPillBuilder 是一个典型的参数化选择器组件,通过 isReqChecked 纯函数判断当前要求是否在已选列表中,据此动态切换复选框图标(☑ 或 ☐)、文字颜色、背景色与边框色。onClick 回调调用 toggleInList 对 formReqs 数组执行不可变的切换操作——已选则移除、未选则添加,赋值回 @State 触发视图更新。整个药丸的视觉反馈完全由数据状态驱动,无需维护额外的布尔标记。Flex({ wrap: FlexWrap.Wrap }) 容器使这些药丸在空间不足时自动换行排列。花盆尺寸药丸 potPillBuilder 采用类似的设计模式,但额外展示了尺寸描述文字(如"直径小于20cm"),并通过 Scroll 横向滚动容器包裹,使得四个尺寸药丸在小屏上可横向滑动查看。这种参数化 @Builder 配合纯函数判断与不可变列表操作的模式,在表单选择类交互中复用频率极高,是绿植运输平台表单系统的统一设计语言。

3.15 预约绿植运输弹框:表单式弹框架构

@Builder
bookingModalBuilder() {
  Column() {
    Row() {
      Text('🌿 预约绿植运输')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.White)
      Column()
        .layoutWeight(1)
      Text('✕')
        .fontSize(16)
        .fontColor(Color.White)
        .padding(6)
        .onClick(() => {
          this.showBookingModal = false
        })
    }
    .alignItems(VerticalAlign.Center)
    .width('100%')
    .padding({ left: 16, right: 12, top: 14, bottom: 14 })
    .backgroundColor(COLOR_PRIMARY)
    .borderRadius({ topLeft: 16, topRight: 16 })

    Scroll() {
      Column() {
        this.modalFieldBuilder('植物名称', '如:琴叶榕', this.bookingPlantName, (v: string) => {
          this.bookingPlantName = v
        })
        // ... 花盆尺寸选择、出发地、目的地、运输时间、特殊要求、备注
      }
      .width('100%')
      .padding(14)
      .constraintSize({ maxHeight: '80%' })
    }
    .scrollBar(BarState.Off)
    .layoutWeight(1)

    Row() {
      Text('取消')
        .onClick(() => { this.showBookingModal = false })
      Text('确认预约')
        .backgroundColor(COLOR_PRIMARY)
        .onClick(() => { this.showBookingModal = false })
    }
    .padding(14)
  }
  .width('90%')
  .height('75%')
  .borderRadius(16)
  .backgroundColor(COLOR_CARD)
}

预约绿植运输弹框是四个弹框中结构最复杂的一个,采用了头部标题栏加中间滚动表单区加底部操作按钮的三段式架构。头部标题栏使用深绿 COLOR_PRIMARY 背景配白色文字,顶部圆角与弹框容器圆角对齐,包含标题与关闭按钮。中间表单区使用 Scroll 包裹以应对表单内容超出弹框高度的场景,constraintSize({ maxHeight: ‘80%’ }) 限制表单区最大高度,scrollBar(BarState.Off) 隐藏滚动条保持视觉简洁。表单内部通过 modalFieldBuilder 参数化构建器复用输入框组件,每个字段包含标签、占位文本、当前值与 onChange 回调。特殊要求区域使用 Flex 换行布局包裹运输要求勾选药丸,备注区域使用 TextArea 支持多行文本输入。底部按钮栏包含"取消"与"确认预约"两个按钮,分别使用浅色与深色样式区分主次操作。弹框的显示与隐藏完全由 @State 布尔变量 showBookingModal 控制,配合 modalOverlay 蒙层组件实现点击遮罩关闭的交互。

3.16 取消运输订单弹框:警示式确认交互

@Builder
cancelModalBuilder() {
  Column() {
    Text('⚠️')
      .fontSize(34)
      .margin({ top: 18 })
    Text('确认取消此运输订单?')
      .fontSize(15)
      .fontWeight(FontWeight.Bold)
      .fontColor(COLOR_TEXT_MAIN)
      .margin({ top: 8 })

    Column() {
      Row() {
        Text('植物名称').fontSize(11).fontColor(COLOR_TEXT_SUB)
        Column().layoutWeight(1)
        Text(this.selectedOrder.plantName).fontSize(11).fontColor(COLOR_TEXT_MAIN)
      }
      Row() {
        Text('运输路线').fontSize(11).fontColor(COLOR_TEXT_SUB)
        Column().layoutWeight(1)
        Text(this.selectedOrder.pickup + ' → ' + this.selectedOrder.destination)
      }
      Row() {
        Text('订单金额').fontSize(11).fontColor(COLOR_TEXT_SUB)
        Column().layoutWeight(1)
        Text('¥' + this.selectedOrder.price.toString()).fontColor(COLOR_BROWN)
      }
    }
    .padding(14)
    .borderRadius(12)
    .backgroundColor('#F5F3EE')
    .border({ width: 1, color: COLOR_BORDER })

    Text('取消后无法恢复,请谨慎操作')
      .fontSize(10)
      .fontColor(COLOR_TEXT_HINT)

    Row() {
      Text('再想想')
        .backgroundColor('#F5F3EE')
        .onClick(() => { this.showCancelModal = false })
      Text('确认取消')
        .backgroundColor(COLOR_DANGER)
        .onClick(() => { this.showCancelModal = false })
    }
  }
  .width('80%')
  .borderRadius(16)
  .backgroundColor(COLOR_CARD)
}

取消运输订单弹框采用了与预约弹框截然不同的警示式设计风格。顶部以大号警告 Emoji ⚠️ 视觉提醒,紧接加粗的确认问句,下方用卡片列出选中订单的关键信息——植物名称、运输路线、订单金额、运输日期,让用户在确认前充分审视将要取消的订单内容。每行信息采用标签左对齐加值右对齐的布局,中间用 layoutWeight(1) 的空 Column 撑开,信息结构清晰易读。底部温馨提示"取消后无法恢复,请谨慎操作"以灰色小字呈现,强化警示效果。操作按钮区设计为"再想想"(次要操作,浅色背景)与"确认取消"(主要操作,危险红 COLOR_DANGER 背景)的双按钮组合,危险操作使用红色是通用 UI 设计规范。selectedOrder 是一个 @State 变量,当用户在订单列表中点击某条订单时被赋值为该订单对象,随后 showCancelModal 设为 true 弹出此弹框,实现了从列表点击到弹框展示的数据传递链路。

3.17 苗圃商品卡构建:难度星级与库存预警

@Builder
productCardBuilder(p: NurseryProductItem) {
  Column() {
    Stack() {
      Column()
        .width('100%')
        .height(90)
        .borderRadius({ topLeft: 12, topRight: 12 })
        .backgroundColor(p.color)
      Text('🪴')
        .fontSize(32)
      if (p.isHot) {
        Text('热卖')
          .fontSize(8)
          .fontColor(Color.White)
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .borderRadius(6)
          .backgroundColor(COLOR_PINK)
          .position({ x: 8, y: 8 })
      }
    }
    .width('100%')
    .height(90)

    Column() {
      Text(p.name)
        .fontSize(12)
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Row() {
        Text('¥' + p.price.toString()).fontColor(COLOR_BROWN)
        Column().layoutWeight(1)
        Text(p.height).fontColor(COLOR_TEXT_HINT)
      }
      Row() {
        Text('难度')
        Text(getStars(p.careDifficulty)).fontColor(getDifficultyColor(p.careDifficulty))
        Text(getDifficultyLabel(p.careDifficulty)).fontColor(getDifficultyColor(p.careDifficulty))
      }
      Row() {
        Text('库存 ' + p.stock.toString() + ' 盆')
          .fontColor(p.stock > 15 ? COLOR_TEXT_HINT : COLOR_PINK)
        Column().layoutWeight(1)
        Text('购买')
          .backgroundColor(COLOR_PRIMARY)
          .onClick(() => {
            this.selectedProduct = p
            this.buyQty = 1
            this.showPurchaseModal = true
          })
      }
    }
  }
  .layoutWeight(1)
  .borderRadius(12)
  .backgroundColor(COLOR_CARD)
}

苗圃商品卡 productCardBuilder 是苗圃 Tab 中复用频率最高的构建器,每张卡片由商品色块区与信息区两部分组成。色块区使用 Stack 叠层布局,底层是一个以商品 color 属性着色的纯色 Column,上层居中放置一个花盆 Emoji 作为视觉占位,热卖商品额外在左上角 position 绝对定位一个粉色"热卖"标签。信息区包含商品名称(设置 maxLines(1) 与 textOverflow 省略号确保单行不溢出)、价格与高度行、难度星级行与库存购买行。难度展示调用 getStars 与 getDifficultyLabel 两个纯函数将数值难度转换为星级字符串与中文标签,颜色由 getDifficultyColor 动态匹配。库存预警设计颇为精巧:当库存大于十五盆时文字为灰色提示色,小于等于十五盆时切换为粉色 COLOR_PINK 表示库存紧张,引导用户尽快购买。购买按钮点击时将选中商品赋值给 selectedProduct、重置购买数量为一、打开购买弹框,完成了从商品卡到购买弹框的数据传递与流程衔接。

3.18 购买绿植弹框:数量步进器与花盆选择

Text('−')
  .fontSize(16)
  .fontColor(COLOR_PRIMARY)
  .textAlign(TextAlign.Center)
  .width(30)
  .height(30)
  .borderRadius(15)
  .backgroundColor('#E3F0E9')
  .onClick(() => {
    if (this.buyQty > 1) {
      this.buyQty = this.buyQty - 1
    }
  })
Text(this.buyQty.toString())
  .fontSize(14)
  .fontWeight(FontWeight.Bold)
Text('+')
  .onClick(() => {
    if (this.buyQty < this.selectedProduct.stock) {
      this.buyQty = this.buyQty + 1
    }
  })

购买绿植弹框中的数量步进器是电商购买流程的标准交互组件。减号按钮 onClick 时先判断 buyQty 是否大于一,若大于一才执行减一操作,防止数量降至零以下。加号按钮 onClick 时先判断 buyQty 是否小于选中商品的 stock 库存上限,若小于才执行加一操作,防止购买数量超过库存。这两个边界检查确保了购买数量始终在一到库存上限的合法区间内。中间的数量显示文本绑定 buyQty 的 toString 结果,步进器操作后 @State 变化自动触发数字更新。花盆选择区域通过 ForEach 遍历 POT_STYLE_OPTIONS 常量数组(红陶盆、陶瓷盆、水泥盆、自带盆),每个选项是一个参数化的药丸文本,选中态使用 COLOR_PRIMARY 背景白色文字,未选中态使用白色背景灰色文字,点击切换 buyPot 状态。底部操作区提供"加入购物车"与"立即购买"两个按钮,分别使用浅绿与深绿背景区分主次,点击后均关闭弹框。整个弹框结构复用了预约弹框的三段式架构——头部标题栏、中间滚动内容区、底部按钮栏,保证了四个弹框的视觉一致性。

3.19 养护日历单元格构建:月历标记系统

@Builder
calendarCellBuilder(cell: number) {
  Column() {
    Text(getCalDay(cell) > 0 ? getCalDay(cell).toString() : ' ')
      .fontSize(10)
      .fontWeight(getCalDay(cell) === 23 ? FontWeight.Bold : FontWeight.Normal)
      .fontColor(getCalDay(cell) === 23 ? Color.White : COLOR_TEXT_MAIN)
      .textAlign(TextAlign.Center)
      .width(18)
      .height(18)
      .borderRadius(9)
      .backgroundColor(getCalDay(cell) === 23 ? COLOR_PRIMARY : Color.Transparent)
    Row() {
      ForEach(getCalMarks(getCalDay(cell)), (mark: string) => {
        Text(mark)
          .fontSize(7)
          .margin({ left: 1, right: 1 })
      }, (mark: string) => cell.toString() + '_' + mark)
    }
    .alignItems(VerticalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .height(9)
  }
  .alignItems(HorizontalAlign.Center)
  .justifyContent(FlexAlign.Center)
  .layoutWeight(1)
  .height(40)
}

养护日历的单元格构建器 calendarCellBuilder 实现了一个信息密度极高的月历格子。每个格子接收一个 cell 编号参数,通过 getCalDay 纯函数将其转换为日期数字——cell 编号从零到三十四覆盖五乘七的日历网格,getCalDay 将零及以下转为零(显示空格),三十二以上转为零(当月无此日),一到三十一正常显示日期。今日(二十三日)的日期数字以深绿圆形背景白色加粗文字高亮,其他日期为普通样式。日期数字下方是养护标记行,通过 getCalMarks 从 CALENDAR_MARKS 配置映射中获取该日的标记 Emoji 列表——💧表示浇水、🌱表示施肥、✂️表示修剪,一天可能同时有多个标记。ForEach 遍历标记数组渲染为七号字的小 Emoji 横排。整个单元格通过 layoutWeight(1) 在行内等宽分布,固定高度四十像素确保整齐。日历通过外层 ForEach(CAL_ROW_LIST) 渲五行、内层 ForEach(CAL_COL_LIST) 渲七列的双层循环构建,每格传入 row 乘七加 col 的编号,完成了完整月历的渲染。

3.20 养护计划卡:健康进度条可视化

@Builder
carePlanCardBuilder(c: CarePlanItem) {
  Column() {
    Row() {
      Text('🪴 ' + c.plantName)
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLOR_TEXT_MAIN)
      Column()
        .layoutWeight(1)
      Text('编辑')
        .onClick(() => {
          this.editingPlan = c
          this.planPlantName = c.plantName
          this.planWaterFreq = c.waterFreq
          this.planLightNeed = c.lightNeed
          this.planFertFreq = c.fertilizeFreq
          this.showEditModal = true
        })
    }

    Row() {
      Text('💧 ' + c.waterFreq + '一浇').backgroundColor('#E3F0E9')
      Text('☀️ ' + c.lightNeed).backgroundColor('#F0E6DC')
      Text('🌱 ' + c.fertilizeFreq + '施肥').backgroundColor('#F5F3EE')
    }

    Row() {
      Text('最近浇水:' + c.lastWatered)
      Column().layoutWeight(1)
      Text('健康 ' + c.healthPercent.toString() + '%')
        .fontColor(getHealthColor(c.healthPercent))
    }

    Column() {
      Column()
        .width(c.healthPercent.toString() + '%')
        .height(5)
        .borderRadius(3)
        .backgroundColor(getHealthColor(c.healthPercent))
    }
    .width('100%')
    .height(5)
    .borderRadius(3)
    .backgroundColor('#E8EFE9')
  }
}

养护计划卡 carePlanCardBuilder 是养护 Tab 的核心展示组件,每张卡片对应一盆植物的养护档案。卡片顶部为植物名称与编辑按钮,编辑按钮点击时将当前计划对象赋给 editingPlan 并将各属性同步到表单状态变量,随后打开编辑弹框,实现了从卡片到弹框的完整数据回填。中间一行是三个养护参数药丸——浇水频率、光照需求、施肥频率,分别用蓝绿、棕色、灰色的背景与对应 Emoji 区分。底部是健康状态进度条,这是本卡片的视觉亮点:进度条宽度通过 width(c.healthPercent.toString() + ‘%’) 将百分比直接转换为宽度字符串,背景色由 getHealthColor 纯函数根据百分比动态返回——八十五以上为成功绿、六十以上为警告黄、六十以下为危险红,使植物健康状况一目了然。进度条容器固定高度五像素、圆角三像素、浅灰底色,内部彩色填充条的宽度与颜色均由数据驱动,构成了一种无需图表库即可实现的数据可视化方案。

3.21 社区帖子卡:点赞交互与不可变列表更新

Row() {
  Text(isPostLiked(this.likedPostIds, p.id) ? '❤️' : '🤍')
    .fontSize(13)
  Text((p.likeCount + (isPostLiked(this.likedPostIds, p.id) ? 1 : 0)).toString())
    .fontSize(10)
    .fontColor(isPostLiked(this.likedPostIds, p.id) ? COLOR_PINK : COLOR_TEXT_SUB)
    .margin({ left: 3 })
}
.onClick(() => {
  this.likedPostIds = toggleIdInList(this.likedPostIds, p.id)
})

// ForEach 键值生成器
}, (p: PostItem) => p.id.toString() + '_' + isPostLiked(this.likedPostIds, p.id).toString())

社区帖子卡的点赞交互是整个应用中状态驱动视图的典型范例。likedPostIds 是一个 @State 数字数组,存储当前用户已点赞的帖子 ID 列表,初始值为 [3](即默认已点赞第三条帖子)。isPostLiked 纯函数判断某帖子 ID 是否在已赞列表中,据此切换心形 Emoji(已赞❤️红心、未赞🤍空心)、点赞数(基础赞数加一或加零)、文字颜色(粉色或灰色)。onClick 调用 toggleIdInList 对 likedPostIds 执行不可变切换操作并赋值回 @State,触发该帖子的点赞状态重绘。特别值得注意的是 ForEach 的键值生成器——它将帖子 ID 与 isPostLiked 的布尔结果拼接为键值,这意味着当用户点赞或取消赞时,该帖子的键值会从 “3_false” 变为 “3_true”,ForEach 检测到键值变化后会对该项进行重新渲染,确保心形图标与数字立即更新。这种通过键值包含状态信息来强制 ForEach 响应内部状态变化的设计技巧,在 ArkTS 开发中处理列表项交互时非常实用。

3.22 我的页面资料卡与大数字统计网格

@Builder
bigStatCellBuilder(icon: string, value: string, label: string, color: string) {
  Column() {
    Text(icon)
      .fontSize(20)
    Text(value)
      .fontSize(22)
      .fontWeight(FontWeight.Bold)
      .fontColor(color)
      .margin({ top: 4 })
    Text(label)
      .fontSize(10)
      .fontColor(COLOR_TEXT_SUB)
      .margin({ top: 2 })
  }
  .alignItems(HorizontalAlign.Center)
  .justifyContent(FlexAlign.Center)
  .layoutWeight(1)
  .height(88)
  .borderRadius(14)
  .backgroundColor(COLOR_CARD)
  .border({ width: 1, color: COLOR_BORDER })
}

我的页面的大数字统计格 bigStatCellBuilder 是一个高度参数化的展示组件,接收图标、数值、标签、颜色四个参数,渲染为居中对齐的 Column。数值使用二十二号加粗字体并配以主题色,视觉冲击力强,适合展示总植物数、总运输次数、养护天数、积分等关键指标。四格统计以二乘二的网格排列——通过两个 Row 各含两个 layoutWeight(1) 的统计格实现,每格固定高度八十八像素。资料卡区域使用 Stack 叠层布局将头像与等级标签组合,头像为六十四像素圆形带浅绿边框,等级标签"Lv.6"以小号绿色圆角标签 position 绝对定位在头像右下角。资料卡下方是注册天数、关注数、粉丝数的三列横向统计,三组数字分别使用绿色、棕色、粉色区分。设置列表区域通过 settingItemBuilder 参数化构建器渲染五条设置项——我的植物、地址管理、养护计划、客服、设置,每项包含图标圆角容器、标题、描述与右箭头,点击后将标签名赋给 clickedSetting 显示一条"已打开"的反馈文本,提供了基础的交互反馈闭环。

四、Tab 切换与弹框管理流程

下面的 Mermaid 流程图展示了用户在叫车 Tab 中从填写表单到打开预约弹框、再到确认预约的完整状态流转过程,以及订单列表点击触发取消弹框的并行流程。

粒子动画循环

setInterval 350ms

advanceParticles 计算新坐标

leafParticles 赋新值

particleLayer 重绘

取消订单流程

点击再想想/遮罩

点击确认取消

用户点击订单条目

selectedOrder = 当前订单

showCancelModal = true

取消弹框渲染

用户操作

showCancelModal = false

叫车表单流程

点击遮罩/取消/✕

填写弹框内表单

点击确认预约

用户填写植物名称

选择花盆尺寸药丸

勾选运输要求

填写出发地与目的地

点击预约运输按钮

showBookingModal = true

预约弹框渲染

用户操作

showBookingModal = false

更新 booking* 状态

五、核心设计模式对比分析

下表对绿植运输平台中采用的关键设计模式进行了横向对比,帮助开发者理解不同模式在特定场景下的适用性与权衡。

设计模式实现场景优势适用条件本项目实例
@Observed 数据模型可观察对象类属性变化自动触发 UI 重绘需要响应式更新的业务对象TransportOrderItem、NurseryProductItem
Record 配置查表配置映射表数据与展示逻辑解耦、扩展性强状态值有限且展示样式固定ORDER_STATUS_CONFIG、POT_SIZE_CONFIG
纯函数辅助无副作用计算可缓存、可测试、可复用输入输出确定的转换逻辑getStars、getHealthColor、advanceParticles
不可变列表更新数组状态变更触发 @State 渲染、可追踪列表的增删切换操作toggleInList、toggleIdInList
参数化 @Builder可复用 UI 片段减少重复代码、统一样式同结构不同数据的组件statCellBuilder、productCardBuilder
布尔弹框控制弹框显示隐藏状态简单、互斥清晰弹框数量有限、无嵌套showBookingModal、showCancelModal
定时器粒子动画周期性状态更新实现连续动画效果装饰性动态视觉leafParticles 与 setInterval
ForEach 键值编码列表项精准更新控制重绘粒度、响应内部状态列表项有交互且需局部刷新帖子点赞键值含 isPostLiked 结果

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============================================================================
// 绿植运输 GREEN HAUL — 大型绿植/盆栽运输 + 苗圃商城平台
// 场景:大型绿植、盆栽树、花卉的专业运输(防倾倒、保温保湿)
//       + 苗圃商城 + 养护社区
// 风格:植物自然清新风(森林绿 + 泥土棕 + 阳光黄)
// Tab:叫车 / 苗圃 / 养护 / 社区 / 我的
// 弹框:预约绿植运输 / 编辑养护计划 / 取消运输订单 / 购买绿植
// ============================================================================

// ============ 颜色常量 ============
const COLOR_BG: string = '#F5F3EE'
const COLOR_CARD: string = '#FFFFFF'
const COLOR_PRIMARY: string = '#2D6A4F'
const COLOR_PRIMARY_LIGHT: string = '#52B788'
const COLOR_PRIMARY_DARK: string = '#1B4332'
const COLOR_BROWN: string = '#774936'
const COLOR_BROWN_LIGHT: string = '#B08968'
const COLOR_YELLOW: string = '#E6C227'
const COLOR_PINK: string = '#E07A5F'
const COLOR_TEXT_MAIN: string = '#1B3329'
const COLOR_TEXT_SUB: string = '#6B8E7B'
const COLOR_TEXT_HINT: string = '#A8C5B5'
const COLOR_BORDER: string = '#D8E4DC'
const COLOR_SUCCESS: string = '#40916C'
const COLOR_WARNING: string = '#E6C227'
const COLOR_DANGER: string = '#BC4749'

// ============ 运输订单数据模型 ============
interface TransportOrderModel {
  id: number
  orderNo: string
  plantName: string
  pickup: string
  destination: string
  price: number
  status: string
  date: string
  potSize: string
}

@Observed
class TransportOrderItem implements TransportOrderModel {
  id: number = 0
  orderNo: string = ''
  plantName: string = ''
  pickup: string = ''
  destination: string = ''
  price: number = 0
  status: string = '待接单'
  date: string = ''
  potSize: string = '中'
  constructor(id: number, orderNo: string, plantName: string, pickup: string, destination: string,
    price: number, status: string, date: string, potSize: string) {
    this.id = id; this.orderNo = orderNo; this.plantName = plantName
    this.pickup = pickup; this.destination = destination; this.price = price
    this.status = status; this.date = date; this.potSize = potSize
  }
}

// ============ 苗圃商品数据模型 ============
interface NurseryProductModel {
  id: number
  name: string
  price: number
  height: string
  careDifficulty: number
  stock: number
  color: string
  tag: string
  desc: string
  isHot: boolean
}

@Observed
class NurseryProductItem implements NurseryProductModel {
  id: number = 0
  name: string = ''
  price: number = 0
  height: string = ''
  careDifficulty: number = 1
  stock: number = 0
  color: string = '#52B788'
  tag: string = ''
  desc: string = ''
  isHot: boolean = false
  constructor(id: number, name: string, price: number, height: string, careDifficulty: number,
    stock: number, color: string, tag: string, desc: string, isHot: boolean) {
    this.id = id; this.name = name; this.price = price; this.height = height
    this.careDifficulty = careDifficulty; this.stock = stock; this.color = color
    this.tag = tag; this.desc = desc; this.isHot = isHot
  }
}

// ============ 养护计划数据模型 ============
interface CarePlanModel {
  id: number
  plantName: string
  waterFreq: string
  lightNeed: string
  fertilizeFreq: string
  lastWatered: string
  healthPercent: number
}

@Observed
class CarePlanItem implements CarePlanModel {
  id: number = 0
  plantName: string = ''
  waterFreq: string = '3天'
  lightNeed: string = '半阴'
  fertilizeFreq: string = '每月'
  lastWatered: string = ''
  healthPercent: number = 90
  constructor(id: number, plantName: string, waterFreq: string, lightNeed: string,
    fertilizeFreq: string, lastWatered: string, healthPercent: number) {
    this.id = id; this.plantName = plantName; this.waterFreq = waterFreq
    this.lightNeed = lightNeed; this.fertilizeFreq = fertilizeFreq
    this.lastWatered = lastWatered; this.healthPercent = healthPercent
  }
}

// ============ 社区帖子数据模型 ============
interface PostModel {
  id: number
  userName: string
  userAvatar: string
  content: string
  likeCount: number
  commentCount: number
  color: string
  tag: string
  time: string
}

@Observed
class PostItem implements PostModel {
  id: number = 0
  userName: string = ''
  userAvatar: string = '🌿'
  content: string = ''
  likeCount: number = 0
  commentCount: number = 0
  color: string = '#52B788'
  tag: string = ''
  time: string = ''
  constructor(id: number, userName: string, userAvatar: string, content: string,
    likeCount: number, commentCount: number, color: string, tag: string, time: string) {
    this.id = id; this.userName = userName; this.userAvatar = userAvatar
    this.content = content; this.likeCount = likeCount; this.commentCount = commentCount
    this.color = color; this.tag = tag; this.time = time
  }
}

// ============ 漂浮叶子粒子数据模型 ============
interface LeafParticleModel {
  x: number
  y: number
  size: number
  opacity: number
  drift: number
  sway: number
}

@Observed
class LeafParticle implements LeafParticleModel {
  x: number = 0
  y: number = 0
  size: number = 14
  opacity: number = 0.3
  drift: number = 1.2
  sway: number = 0.8
  constructor(x: number, y: number, size: number, opacity: number, drift: number, sway: number) {
    this.x = x; this.y = y; this.size = size
    this.opacity = opacity; this.drift = drift; this.sway = sway
  }
}

// ============ 配置 interface ============
interface OrderStatusMeta {
  label: string
  color: string
  bg: string
  icon: string
}

interface PotSizeMeta {
  label: string
  icon: string
  desc: string
}

interface PlantCategoryMeta {
  label: string
  icon: string
  color: string
  bg: string
}

interface DifficultyMeta {
  label: string
  color: string
  icon: string
}

interface TopicMeta {
  label: string
  icon: string
  heat: string
}

interface TabMeta {
  label: string
  icon: string
}

// ============ 配置映射 ============
const ORDER_STATUS_CONFIG: Record<string, OrderStatusMeta> = {
  '待接单': { label: '待接单', color: COLOR_BROWN, bg: '#F0E6DC', icon: '🕐' },
  '运输中': { label: '运输中', color: COLOR_PRIMARY, bg: '#E3F0E9', icon: '🚐' },
  '已完成': { label: '已完成', color: COLOR_SUCCESS, bg: '#E1F2E8', icon: '✅' },
  '已取消': { label: '已取消', color: COLOR_DANGER, bg: '#F6E3E4', icon: '❌' }
}

const POT_SIZE_CONFIG: Record<string, PotSizeMeta> = {
  '小': { label: '小', icon: '🪴', desc: '直径<20cm' },
  '中': { label: '中', icon: '🪴', desc: '20-35cm' },
  '大': { label: '大', icon: '🌳', desc: '35-55cm' },
  '特大': { label: '特大', icon: '🌲', desc: '>55cm 需吊装' }
}

const PLANT_CATEGORY_CONFIG: Record<string, PlantCategoryMeta> = {
  '全部': { label: '全部', icon: '🌏', color: COLOR_PRIMARY, bg: '#E3F0E9' },
  '大型绿植': { label: '大型绿植', icon: '🌳', color: COLOR_PRIMARY_DARK, bg: '#E1EBE5' },
  '多肉': { label: '多肉', icon: '🌵', color: COLOR_BROWN_LIGHT, bg: '#F0E6DC' },
  '花卉': { label: '花卉', icon: '🌸', color: COLOR_PINK, bg: '#FAE8E2' },
  '果树': { label: '果树', icon: '🍋', color: COLOR_YELLOW, bg: '#F8F0D4' },
  '室内': { label: '室内', icon: '🏡', color: COLOR_SUCCESS, bg: '#E1F2E8' },
  '户外': { label: '户外', icon: '⛰️', color: COLOR_BROWN, bg: '#F0E6DC' }
}

const CARE_DIFFICULTY_CONFIG: Record<string, DifficultyMeta> = {
  '简单': { label: '简单', color: COLOR_SUCCESS, icon: '🌱' },
  '中等': { label: '中等', color: COLOR_WARNING, icon: '🌿' },
  '困难': { label: '困难', color: COLOR_PINK, icon: '🥀' }
}

const HOT_TOPIC_CONFIG: Record<string, TopicMeta> = {
  '新手养植': { label: '新手养植', icon: '🌱', heat: '128万' },
  '琴叶榕拯救': { label: '琴叶榕拯救', icon: '🌳', heat: '86万' },
  '阳台改造': { label: '阳台改造', icon: '🏡', heat: '73万' },
  '多肉拼盘': { label: '多肉拼盘', icon: '🌵', heat: '65万' },
  '花期记录': { label: '花期记录', icon: '🌸', heat: '52万' }
}

const TAB_META_CONFIG: Record<string, TabMeta> = {
  'BOOKING': { label: '叫车', icon: '🚐' },
  'NURSERY': { label: '苗圃', icon: '🌿' },
  'CARE': { label: '养护', icon: '💧' },
  'COMMUNITY': { label: '社区', icon: '📱' },
  'PROFILE': { label: '我的', icon: '👤' }
}

// ============ 静态数据:运输订单(8条) ============
const ORDER_LIST: TransportOrderItem[] = [
  new TransportOrderItem(1, 'GH2026082301', '琴叶榕', '朝阳花木市场', '望京SOHO T1', 268, '运输中', '08-24 14:00', '大'),
  new TransportOrderItem(2, 'GH2026082202', '龟背竹', '顺义苗圃基地', '三里屯绿洲公寓', 328, '已完成', '08-22 09:30', '特大'),
  new TransportOrderItem(3, 'GH2026082103', '天堂鸟', '通州花卉园', '亦庄金地格林', 398, '已完成', '08-21 16:00', '特大'),
  new TransportOrderItem(4, 'GH2026082004', '散尾葵', '大兴绿植批发', '丽都悦府', 216, '已取消', '08-20 11:00', '大'),
  new TransportOrderItem(5, 'GH2026081905', '龙血树', '丰台花乡', '西红门荟聚', 188, '已完成', '08-19 10:30', '中'),
  new TransportOrderItem(6, 'GH2026081806', '发财树', '石景山苗圃', '苹果园社区', 156, '已完成', '08-18 15:00', '中'),
  new TransportOrderItem(7, 'GH2026082407', '橡皮树', '海淀温室大棚', '中关村软件园', 288, '待接单', '08-25 08:00', '大'),
  new TransportOrderItem(8, 'GH2026082408', '虎皮兰', '房山多肉基地', '长阳半岛', 128, '待接单', '08-25 13:00', '小')
]

// ============ 静态数据:苗圃商品(12个) ============
const PRODUCT_LIST: NurseryProductItem[] = [
  new NurseryProductItem(1, '琴叶榕 大苗', 268, '1.5-1.8m', 3, 24, '#2D6A4F', '客厅霸主', '叶形如小提琴,姿态挺拔,客厅镇宅首选,需充足散射光。', true),
  new NurseryProductItem(2, '龟背竹 老桩', 328, '1.2-1.5m', 2, 18, '#40916C', '网红裂叶', '天然裂叶纹理,气质高级,耐阴好养,新手友好。', true),
  new NurseryProductItem(3, '天堂鸟', 398, '1.8-2.2m', 4, 12, '#1B4332', '热带风情', '大叶片如鸟羽,热带度假感拉满,需大空间与光照。', true),
  new NurseryProductItem(4, '散尾葵', 216, '1.5-1.8m', 3, 30, '#52B788', '天然加湿器', '羽状复叶轻盈飘逸,蒸腾作用强,天然空气加湿器。', false),
  new NurseryProductItem(5, '量天尺', 188, '1.0-1.2m', 1, 45, '#774936', '极简风', '仙人掌科柱状植物,极简北欧风,一个月浇一次水。', false),
  new NurseryProductItem(6, '百合竹', 256, '1.2-1.5m', 2, 22, '#B08968', '金边叶', '金边条纹叶片,耐旱耐阴,办公室绿植常青款。', false),
  new NurseryProductItem(7, '龙血树', 178, '1.5-1.8m', 2, 28, '#2D6A4F', '千年木', '细长剑形叶丛生,寓意长寿,光照充足时叶色更艳。', false),
  new NurseryProductItem(8, '橡皮树 黑金刚', 148, '0.8-1.0m', 1, 60, '#1B3329', '厚实叶片', '墨绿厚叶,净化能力强,水培土培皆可,懒人之友。', false),
  new NurseryProductItem(9, '鹤望兰 小苗', 96, '0.4-0.6m', 3, 36, '#E6C227', '开花款', '小苗期超高性价比,养护得当可赏"天堂鸟"花。', false),
  new NurseryProductItem(10, '油橄榄树', 428, '1.6-2.0m', 3, 8, '#6B8E7B', '地中海风', '灰绿细叶,地中海慵懒气质,阳台庭院皆宜。', false),
  new NurseryProductItem(11, '幸福树', 138, '1.0-1.3m', 2, 40, '#40916C', '吉利款', '羽状复叶翠绿浓密,寓意幸福美满,乔迁送礼热门。', false),
  new NurseryProductItem(12, '旅人蕉', 368, '1.4-1.6m', 4, 10, '#E07A5F', '扇形叶', '扇形排列大叶片,极具造型感,需要温暖湿润环境。', false)
]

// ============ 静态数据:养护计划(6条) ============
const CARE_PLAN_LIST: CarePlanItem[] = [
  new CarePlanItem(1, '琴叶榕·客厅', '7天', '充足', '每月', '08-18', 92),
  new CarePlanItem(2, '龟背竹·书房', '10天', '半阴', '每两月', '08-16', 86),
  new CarePlanItem(3, '天堂鸟·阳台', '5天', '充足', '每月', '08-21', 95),
  new CarePlanItem(4, '散尾葵·玄关', '4天', '半阴', '每月', '08-17', 68),
  new CarePlanItem(5, '虎皮兰·卧室', '14天', '阴凉', '每季度', '08-09', 88),
  new CarePlanItem(6, '多肉拼盘·飘窗', '12天', '充足', '每季度', '08-12', 54)
]

// ============ 静态数据:社区帖子(8条) ============
const POST_LIST: PostItem[] = [
  new PostItem(1, '绿手指阿岚', '🌱', '琴叶榕到家第三个月,终于冒出新叶了!防倾倒支架果然有用,运输一点没伤根。', 328, 46, '#2D6A4F', '新手养植', '2小时前'),
  new PostItem(2, '王老板的苗圃', '🧑‍🌾', '本周天堂鸟特惠,基地直发,全程保温保湿车厢,两天可达北方城市。', 156, 23, '#1B4332', '苗圃直发', '5小时前'),
  new PostItem(3, '多肉少女桃', '🌸', '飘窗多肉拼盘大改造!十二卷+法师+熊童子,阳光黄配泥土棕的盆真的绝。', 892, 134, '#E6C227', '多肉拼盘', '8小时前'),
  new PostItem(4, '老周养龟背', '🌿', '龟背竹裂叶经验:光照够+控水狠,裂叶率直接翻倍。附半年对比图。', 564, 88, '#40916C', '花期记录', '12小时前'),
  new PostItem(5, '阳台改造中', '🏡', '5平米北阳台改绿植角完工!散尾葵当背景,龟背竹做主角,成本低到哭。', 431, 67, '#52B788', '阳台改造', '1天前'),
  new PostItem(6, '植物医生小林', '🩺', '橡皮树叶片发黄下垂?先查根再看水,九成是闷根了。急救三步法见长图。', 726, 152, '#774936', '植物急诊', '1天前'),
  new PostItem(7, '深夜浇花人', '🌙', '夜间运输绿植真的好,叶片一点不蔫,师傅还帮忙套了保温膜,好评!', 213, 31, '#E07A5F', '运输日记', '2天前'),
  new PostItem(8, '北方绿植党', '❄️', '冬天买大型绿植一定要选保温车厢!去年冻伤一棵旅人蕉的教训太惨痛。', 348, 59, '#B08968', '冬季养护', '3天前')
]

// ============ 静态数据:月度运输柱状图(6个月) ============
const MONTHLY_ORDER_COUNT: number[] = [12, 18, 9, 22, 15, 26]
const MONTHLY_LABELS: string[] = ['3月', '4月', '5月', '6月', '7月', '8月']

// ============ 选项常量(供 ForEach 使用) ============
const BOOKING_REQ_OPTIONS: string[] = ['防倾倒', '保温', '保湿', '加急', '夜间']
const WEEKDAY_LIST: string[] = ['一', '二', '三', '四', '五', '六', '日']
const CAL_ROW_LIST: number[] = [0, 1, 2, 3, 4]
const CAL_COL_LIST: number[] = [0, 1, 2, 3, 4, 5, 6]
const WATER_FREQ_OPTIONS: string[] = ['每天', '2天', '3天', '每周']
const LIGHT_NEED_OPTIONS: string[] = ['充足', '半阴', '阴凉']
const FERT_FREQ_OPTIONS: string[] = ['每月', '每两月', '每季度']
const POT_STYLE_OPTIONS: string[] = ['红陶盆', '陶瓷盆', '水泥盆', '自带盆']
const PLANT_CATEGORY_KEYS: string[] = ['全部', '大型绿植', '多肉', '花卉', '果树', '室内', '户外']
const POT_SIZE_KEYS: string[] = ['小', '中', '大', '特大']
const HOT_TOPIC_KEYS: string[] = ['新手养植', '琴叶榕拯救', '阳台改造', '多肉拼盘', '花期记录']

// ============ 静态数据:日历养护标记 ============
const CALENDAR_MARKS: Record<string, string[]> = {
  '2': ['💧'],
  '4': ['💧', '🌱'],
  '6': ['✂️'],
  '8': ['💧'],
  '11': ['💧', '🌱'],
  '13': ['💧'],
  '15': ['✂️'],
  '18': ['💧', '🌱'],
  '20': ['💧'],
  '22': ['💧'],
  '23': ['💧', '🌱', '✂️'],
  '25': ['💧'],
  '27': ['🌱'],
  '29': ['💧'],
  '30': ['✂️']
}

// ============ 辅助纯函数 ============
function getStars(n: number): string {
  if (n >= 5) {
    return '★★★★★'
  }
  if (n === 4) {
    return '★★★★☆'
  }
  if (n === 3) {
    return '★★★☆☆'
  }
  if (n === 2) {
    return '★★☆☆☆'
  }
  return '★☆☆☆☆'
}

function getDifficultyLabel(n: number): string {
  if (n <= 2) {
    return '简单'
  }
  if (n === 3) {
    return '中等'
  }
  return '困难'
}

function getDifficultyColor(n: number): string {
  if (n <= 2) {
    return CARE_DIFFICULTY_CONFIG['简单'].color
  }
  if (n === 3) {
    return CARE_DIFFICULTY_CONFIG['中等'].color
  }
  return CARE_DIFFICULTY_CONFIG['困难'].color
}

function isReqChecked(list: string[], req: string): boolean {
  return list.indexOf(req) >= 0
}

function getStatusMeta(status: string): OrderStatusMeta {
  const meta = ORDER_STATUS_CONFIG[status]
  if (meta) {
    return meta
  }
  return ORDER_STATUS_CONFIG['待接单']
}

function getMonthOrderTotal(): number {
  let sum: number = 0
  for (let i = 0; i < MONTHLY_ORDER_COUNT.length; i++) {
    sum += MONTHLY_ORDER_COUNT[i]
  }
  return sum
}

function getInProgressCount(): number {
  let count: number = 0
  for (let i = 0; i < ORDER_LIST.length; i++) {
    if (ORDER_LIST[i].status === '运输中' || ORDER_LIST[i].status === '待接单') {
      count++
    }
  }
  return count
}

function getCompletedCount(): number {
  let count: number = 0
  for (let i = 0; i < ORDER_LIST.length; i++) {
    if (ORDER_LIST[i].status === '已完成') {
      count++
    }
  }
  return count
}

function getTotalCost(): number {
  let sum: number = 0
  for (let i = 0; i < ORDER_LIST.length; i++) {
    if (ORDER_LIST[i].status !== '已取消') {
      sum += ORDER_LIST[i].price
    }
  }
  return sum
}

function getBarHeight(count: number): number {
  if (count >= 26) {
    return 76
  }
  return Math.round(count * 76 / 26)
}

function getCalDay(cell: number): number {
  if (cell < 1) {
    return 0
  }
  if (cell > 31) {
    return 0
  }
  return cell
}

function getCalMarks(day: number): string[] {
  const marks = CALENDAR_MARKS[day.toString()]
  if (marks) {
    return marks
  }
  return []
}

function getTodayReminders(): number {
  return getCalMarks(23).length + 2
}

function getSimpleCount(): number {
  let count: number = 0
  for (let i = 0; i < PRODUCT_LIST.length; i++) {
    if (PRODUCT_LIST[i].careDifficulty <= 2) {
      count++
    }
  }
  return count
}

function getMediumCount(): number {
  let count: number = 0
  for (let i = 0; i < PRODUCT_LIST.length; i++) {
    if (PRODUCT_LIST[i].careDifficulty === 3) {
      count++
    }
  }
  return count
}

function getHardCount(): number {
  let count: number = 0
  for (let i = 0; i < PRODUCT_LIST.length; i++) {
    if (PRODUCT_LIST[i].careDifficulty >= 4) {
      count++
    }
  }
  return count
}

function getHealthColor(percent: number): string {
  if (percent >= 85) {
    return COLOR_SUCCESS
  }
  if (percent >= 60) {
    return COLOR_WARNING
  }
  return COLOR_DANGER
}

function advanceParticles(list: LeafParticle[]): LeafParticle[] {
  const next: LeafParticle[] = []
  for (let i = 0; i < list.length; i++) {
    const p: LeafParticle = list[i]
    let ny: number = p.y - p.drift
    let nx: number = p.x + p.sway
    if (ny < -40) {
      ny = 820
    }
    if (nx > 350) {
      nx = 5
    }
    if (nx < 0) {
      nx = 345
    }
    next.push(new LeafParticle(nx, ny, p.size, p.opacity, p.drift, p.sway))
  }
  return next
}

function toggleInList(list: string[], item: string): string[] {
  const idx: number = list.indexOf(item)
  if (idx >= 0) {
    const next: string[] = list.slice(0, idx).concat(list.slice(idx + 1))
    return next
  }
  return list.concat([item])
}

function toggleIdInList(list: number[], id: number): number[] {
  const idx: number = list.indexOf(id)
  if (idx >= 0) {
    return list.slice(0, idx).concat(list.slice(idx + 1))
  }
  return list.concat([id])
}

// ============ Tab 枚举 ============
enum GreenTab {
  BOOKING = 0,
  NURSERY = 1,
  CARE = 2,
  COMMUNITY = 3,
  PROFILE = 4
}

const TAB_KEY_LIST: string[] = ['BOOKING', 'NURSERY', 'CARE', 'COMMUNITY', 'PROFILE']

function getTabIndex(key: string): number {
  if (key === 'BOOKING') {
    return GreenTab.BOOKING
  }
  if (key === 'NURSERY') {
    return GreenTab.NURSERY
  }
  if (key === 'CARE') {
    return GreenTab.CARE
  }
  if (key === 'COMMUNITY') {
    return GreenTab.COMMUNITY
  }
  return GreenTab.PROFILE
}

// ============================================================================
// 主页面
// ============================================================================
@Entry
@Component
struct GreenHaulPage {
  @State activeTab: number = GreenTab.BOOKING
  @State selectedCategory: string = '全部'
  @State searchKeyword: string = ''
  @State messageCount: number = 3
  @State leafParticles: LeafParticle[] = [
    new LeafParticle(30, 120, 16, 0.35, 1.6, 0.9),
    new LeafParticle(80, 320, 13, 0.28, 1.2, -0.7),
    new LeafParticle(140, 560, 18, 0.32, 1.8, 0.6),
    new LeafParticle(200, 80, 12, 0.25, 1.0, -1.0),
    new LeafParticle(250, 420, 20, 0.30, 2.0, 0.8),
    new LeafParticle(300, 700, 14, 0.27, 1.4, -0.9),
    new LeafParticle(330, 220, 15, 0.33, 1.5, 1.1),
    new LeafParticle(60, 680, 17, 0.29, 1.7, -0.6),
    new LeafParticle(170, 180, 11, 0.24, 1.1, 1.2),
    new LeafParticle(230, 520, 19, 0.31, 1.9, -0.8)
  ]
  private particleTimer: number = -1

  aboutToAppear(): void {
    this.particleTimer = setInterval(() => {
      this.leafParticles = advanceParticles(this.leafParticles)
    }, 350)
  }

  aboutToDisappear(): void {
    if (this.particleTimer >= 0) {
      clearInterval(this.particleTimer)
    }
  }

  // ---------- 头部(无动画,园艺中心风格) ----------
  @Builder
  headerBuilder() {
    Column() {
      Row() {
        Row() {
          Text('🌿')
            .fontSize(18)
          Column() {
            Text('绿植运输')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLOR_PRIMARY_DARK)
            Text('GREEN HAUL')
              .fontSize(8)
              .fontColor(COLOR_PRIMARY_LIGHT)
              .letterSpacing(1)
          }
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 6 })
        }
        .alignItems(VerticalAlign.Center)

        Row() {
          Text('🔍')
            .fontSize(12)
            .margin({ left: 8 })
          TextInput({ placeholder: '搜植物 / 苗圃 / 城市', text: this.searchKeyword })
            .onChange((v: string) => {
              this.searchKeyword = v
            })
            .fontSize(11)
            .fontColor(COLOR_TEXT_MAIN)
            .placeholderColor(COLOR_TEXT_HINT)
            .placeholderFont({ size: 11 })
            .backgroundColor(Color.Transparent)
            .padding({ left: 4, right: 4 })
            .layoutWeight(1)
        }
        .alignItems(VerticalAlign.Center)
        .layoutWeight(1)
        .height(34)
        .margin({ left: 10, right: 10 })
        .padding({ right: 6 })
        .borderRadius(17)
        .backgroundColor('#EFEBE2')
        .border({ width: 1, color: COLOR_BORDER })

        Stack() {
          Text('✉️')
            .fontSize(20)
          Text(this.messageCount.toString())
            .fontSize(8)
            .fontColor(Color.White)
            .textAlign(TextAlign.Center)
            .width(13)
            .height(13)
            .borderRadius(7)
            .backgroundColor(COLOR_DANGER)
            .position({ x: 10, y: -3 })
        }
        .width(30)
        .height(30)
        .onClick(() => {
          this.messageCount = 0
        })
      }
      .alignItems(VerticalAlign.Center)
      .width('100%')
      .padding({ left: 14, right: 14, top: 8, bottom: 8 })

      // 第二行:植物分类药丸横向滚动
      Scroll() {
        Row() {
          ForEach(Object.keys(PLANT_CATEGORY_CONFIG), (key: string) => {
            Row() {
              Text(PLANT_CATEGORY_CONFIG[key].icon)
                .fontSize(12)
              Text(PLANT_CATEGORY_CONFIG[key].label)
                .fontSize(11)
                .fontColor(this.selectedCategory === key ? Color.White : COLOR_TEXT_SUB)
                .margin({ left: 3 })
            }
            .alignItems(VerticalAlign.Center)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .borderRadius(14)
            .backgroundColor(this.selectedCategory === key ? COLOR_PRIMARY : COLOR_CARD)
            .border({ width: 1, color: this.selectedCategory === key ? COLOR_PRIMARY : COLOR_BORDER })
            .margin({ right: 8 })
            .onClick(() => {
              this.selectedCategory = key
            })
          }, (key: string) => key + '_' + this.selectedCategory)
        }
        .padding({ left: 14, right: 8 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .margin({ top: 2, bottom: 8 })
    }
    .width('100%')
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
  }

  // ---------- 内容区 ----------
  @Builder
  contentArea() {
    Column() {
      if (this.activeTab === GreenTab.BOOKING) {
        BookingContent()
      }
      if (this.activeTab === GreenTab.NURSERY) {
        NurseryContent()
      }
      if (this.activeTab === GreenTab.CARE) {
        CareContent()
      }
      if (this.activeTab === GreenTab.COMMUNITY) {
        CommunityContent()
      }
      if (this.activeTab === GreenTab.PROFILE) {
        ProfileContent()
      }
    }
    .layoutWeight(1)
    .width('100%')
  }

  // ---------- 底部Tab栏 ----------
  @Builder
  tabBarBuilder() {
    Row() {
      ForEach(TAB_KEY_LIST, (key: string) => {
        Column() {
          Text(TAB_META_CONFIG[key].icon)
            .fontSize(20)
          Text(TAB_META_CONFIG[key].label)
            .fontSize(10)
            .fontColor(this.activeTab === getTabIndex(key) ? COLOR_PRIMARY : COLOR_TEXT_SUB)
            .margin({ top: 2 })
          Column()
            .width(16)
            .height(2)
            .borderRadius(1)
            .backgroundColor(this.activeTab === getTabIndex(key) ? COLOR_PRIMARY_LIGHT : Color.Transparent)
            .margin({ top: 3 })
        }
        .alignItems(HorizontalAlign.Center)
        .layoutWeight(1)
        .onClick(() => {
          this.activeTab = getTabIndex(key)
        })
      }, (key: string) => key + '_' + this.activeTab.toString())
    }
    .width('100%')
    .height(56)
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
  }

  // ---------- 粒子层(不阻挡点击) ----------
  @Builder
  particleLayer() {
    Column() {
      ForEach(this.leafParticles, (p: LeafParticle) => {
        Text('🍃')
          .fontSize(p.size)
          .opacity(p.opacity)
          .position({ x: p.x, y: p.y })
      }, (p: LeafParticle, index: number) => index.toString() + '_' + p.x.toString() + '_' + p.y.toString())
    }
    .width('100%')
    .height('100%')
    .hitTestBehavior(HitTestMode.None)
  }

  build() {
    Stack() {
      Column() {
        this.headerBuilder()
        this.contentArea()
        this.tabBarBuilder()
      }
      .width('100%')
      .height('100%')

      this.particleLayer()
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLOR_BG)
  }
}

// ============================================================================
// Tab1 叫车 BOOKING:统计条 + 柱状图 + 叫车表单 + 最近运输列表
// ============================================================================
@Component
struct BookingContent {
  @State formPlantName: string = ''
  @State formHeight: string = ''
  @State formPotSize: string = '中'
  @State formPickup: string = ''
  @State formDest: string = ''
  @State formReqs: string[] = ['防倾倒']
  @State showBookingModal: boolean = false
  @State showCancelModal: boolean = false
  @State selectedOrder: TransportOrderItem = new TransportOrderItem(0, '', '', '', '', 0, '待接单', '', '中')

  // ---------- 弹框1:预约绿植运输(自然风表单式) ----------
  @Builder
  modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(27,51,41,0.55)')
      .onClick(() => {
        onClose()
      })
  }

  @State bookingPlantName: string = ''
  @State bookingHeight: string = ''
  @State bookingPotSize: string = '中'
  @State bookingPickup: string = ''
  @State bookingDest: string = ''
  @State bookingTime: string = ''
  @State bookingReqs: string[] = ['防倾倒']
  @State bookingNotes: string = ''

  // ---------- 统计条 ----------
  @Builder
  statCellBuilder(label: string, value: string, unit: string, color: string) {
    Column() {
      Text(value)
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(color)
      Text(unit)
        .fontSize(8)
        .fontColor(COLOR_TEXT_SUB)
        .margin({ top: 1 })
      Text(label)
        .fontSize(9)
        .fontColor(COLOR_TEXT_SUB)
        .margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Center)
    .layoutWeight(1)
  }

  // ---------- 月度运输柱状图 ----------
  @Builder
  chartBuilder() {
    Column() {
      Text('📈 月度运输次数')
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLOR_TEXT_MAIN)
      Row() {
        ForEach(MONTHLY_LABELS, (m: string, index: number) => {
          Column() {
            Column()
              .width(14)
              .height(getBarHeight(MONTHLY_ORDER_COUNT[index]))
              .borderRadius({ topLeft: 4, topRight: 4 })
              .backgroundColor(index === MONTHLY_ORDER_COUNT.length - 1 ? COLOR_PRIMARY : COLOR_PRIMARY_LIGHT)
            Text(MONTHLY_ORDER_COUNT[index].toString())
              .fontSize(8)
              .fontColor(COLOR_TEXT_SUB)
              .margin({ top: 3 })
            Text(m)
              .fontSize(8)
              .fontColor(COLOR_TEXT_HINT)
          }
          .alignItems(HorizontalAlign.Center)
          .layoutWeight(1)
        }, (m: string) => m)
      }
      .alignItems(VerticalAlign.Bottom)
      .width('100%')
      .height(110)
      .margin({ top: 10 })
    }
    .width('100%')
    .padding(12)
    .borderRadius(14)
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
  }

  // ---------- 表单输入行 ----------
  @Builder
  formFieldBuilder(label: string, placeholder: string, value: string, onChange: (v: string) => void) {
    Column() {
      Text(label)
        .fontSize(11)
        .fontColor(COLOR_TEXT_SUB)
        .alignSelf(ItemAlign.Start)
      TextInput({ placeholder: placeholder, text: value })
        .onChange((v: string) => {
          onChange(v)
        })
        .fontSize(12)
        .fontColor(COLOR_TEXT_MAIN)
        .placeholderColor(COLOR_TEXT_HINT)
        .placeholderFont({ size: 11 })
        .height(36)
        .margin({ top: 5 })
        .padding({ left: 10, right: 10 })
        .borderRadius(10)
        .backgroundColor('#F5F3EE')
        .border({ width: 1, color: COLOR_BORDER })
    }
    .alignItems(HorizontalAlign.Start)
    .width('100%')
    .margin({ top: 10 })
  }

  // ---------- 花盆尺寸药丸 ----------
  @Builder
  potPillBuilder(size: string) {
    Row() {
      Text(POT_SIZE_CONFIG[size].icon)
        .fontSize(11)
      Column() {
        Text(size)
          .fontSize(11)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.formPotSize === size ? Color.White : COLOR_TEXT_MAIN)
        Text(POT_SIZE_CONFIG[size].desc)
          .fontSize(7)
          .fontColor(this.formPotSize === size ? '#D8E4DC' : COLOR_TEXT_HINT)
      }
      .alignItems(HorizontalAlign.Start)
      .margin({ left: 4 })
    }
    .alignItems(VerticalAlign.Center)
    .padding({ left: 10, right: 10, top: 6, bottom: 6 })
    .borderRadius(10)
    .backgroundColor(this.formPotSize === size ? COLOR_PRIMARY : COLOR_CARD)
    .border({ width: 1, color: this.formPotSize === size ? COLOR_PRIMARY : COLOR_BORDER })
    .margin({ right: 8 })
    .onClick(() => {
      this.formPotSize = size
    })
  }

  // ---------- 运输要求勾选 ----------
  @Builder
  reqPillBuilder(req: string) {
    Row() {
      Text(isReqChecked(this.formReqs, req) ? '☑' : '☐')
        .fontSize(12)
        .fontColor(isReqChecked(this.formReqs, req) ? COLOR_PRIMARY : COLOR_TEXT_HINT)
      Text(req)
        .fontSize(11)
        .fontColor(isReqChecked(this.formReqs, req) ? COLOR_PRIMARY : COLOR_TEXT_SUB)
        .margin({ left: 3 })
    }
    .alignItems(VerticalAlign.Center)
    .padding({ left: 10, right: 10, top: 6, bottom: 6 })
    .borderRadius(10)
    .backgroundColor(isReqChecked(this.formReqs, req) ? '#E3F0E9' : COLOR_CARD)
    .border({ width: 1, color: isReqChecked(this.formReqs, req) ? COLOR_PRIMARY : COLOR_BORDER })
    .margin({ right: 8, top: 6 })
    .onClick(() => {
      this.formReqs = toggleInList(this.formReqs, req)
    })
  }

  // ---------- 订单条目 ----------
  @Builder
  orderItemBuilder(o: TransportOrderItem) {
    Row() {
      Column() {
        Row() {
          Text('🪴 ' + o.plantName)
            .fontSize(13)
            .fontWeight(FontWeight.Medium)
            .fontColor(COLOR_TEXT_MAIN)
          Text(o.potSize + '盆')
            .fontSize(8)
            .fontColor(COLOR_BROWN)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .borderRadius(6)
            .backgroundColor('#F0E6DC')
            .margin({ left: 6 })
        }
        .alignItems(VerticalAlign.Center)

        Row() {
          Text(o.pickup)
            .fontSize(10)
            .fontColor(COLOR_TEXT_SUB)
          Text(' → ')
            .fontSize(10)
            .fontColor(COLOR_PRIMARY_LIGHT)
          Text(o.destination)
            .fontSize(10)
            .fontColor(COLOR_TEXT_SUB)
        }
        .alignItems(VerticalAlign.Center)
        .margin({ top: 4 })

        Row() {
          Text('🚚 ' + o.orderNo)
            .fontSize(8)
            .fontColor(COLOR_TEXT_HINT)
          Text(o.date)
            .fontSize(8)
            .fontColor(COLOR_TEXT_HINT)
            .margin({ left: 10 })
        }
        .alignItems(VerticalAlign.Center)
        .margin({ top: 3 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      Column() {
        Text('¥' + o.price.toString())
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_BROWN)
        Text(getStatusMeta(o.status).icon + ' ' + getStatusMeta(o.status).label)
          .fontSize(9)
          .fontColor(getStatusMeta(o.status).color)
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .borderRadius(8)
          .backgroundColor(getStatusMeta(o.status).bg)
          .margin({ top: 6 })
      }
      .alignItems(HorizontalAlign.End)
    }
    .alignItems(VerticalAlign.Center)
    .width('100%')
    .padding(12)
    .borderRadius(12)
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
    .margin({ top: 8 })
    .onClick(() => {
      this.selectedOrder = o
      this.showCancelModal = true
    })
  }

  // ---------- 弹框1:预约绿植运输 ----------
  @Builder
  bookingModalBuilder() {
    Column() {
      // 头部:绿色标题栏
      Row() {
        Text('🌿 预约绿植运输')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
        Column()
          .layoutWeight(1)
        Text('✕')
          .fontSize(16)
          .fontColor(Color.White)
          .padding(6)
          .onClick(() => {
            this.showBookingModal = false
          })
      }
      .alignItems(VerticalAlign.Center)
      .width('100%')
      .padding({ left: 16, right: 12, top: 14, bottom: 14 })
      .backgroundColor(COLOR_PRIMARY)
      .borderRadius({ topLeft: 16, topRight: 16 })

      Scroll() {
        Column() {
          this.modalFieldBuilder('植物名称', '如:琴叶榕', this.bookingPlantName, (v: string) => {
            this.bookingPlantName = v
          })
          this.modalFieldBuilder('植物高度', '如:1.6米', this.bookingHeight, (v: string) => {
            this.bookingHeight = v
          })

          Text('花盆尺寸')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
            .alignSelf(ItemAlign.Start)
            .margin({ top: 12 })
          Row() {
            ForEach(Object.keys(POT_SIZE_CONFIG), (size: string) => {
              Row() {
                Text(POT_SIZE_CONFIG[size].icon)
                  .fontSize(11)
                Text(size)
                  .fontSize(11)
                  .fontColor(this.bookingPotSize === size ? Color.White : COLOR_TEXT_MAIN)
                  .margin({ left: 3 })
              }
              .alignItems(VerticalAlign.Center)
              .padding({ left: 12, right: 12, top: 6, bottom: 6 })
              .borderRadius(10)
              .backgroundColor(this.bookingPotSize === size ? COLOR_PRIMARY : COLOR_CARD)
              .border({ width: 1, color: this.bookingPotSize === size ? COLOR_PRIMARY : COLOR_BORDER })
              .margin({ right: 8, top: 8 })
              .onClick(() => {
                this.bookingPotSize = size
              })
            }, (size: string) => size + '_' + this.bookingPotSize)
          }
          .width('100%')

          this.modalFieldBuilder('出发地', '如:朝阳花木市场', this.bookingPickup, (v: string) => {
            this.bookingPickup = v
          })
          this.modalFieldBuilder('目的地', '如:望京SOHO T1', this.bookingDest, (v: string) => {
            this.bookingDest = v
          })
          this.modalFieldBuilder('运输时间', '如:08-25 14:00', this.bookingTime, (v: string) => {
            this.bookingTime = v
          })

          Text('特殊要求')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
            .alignSelf(ItemAlign.Start)
            .margin({ top: 12 })
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(BOOKING_REQ_OPTIONS, (req: string) => {
              Row() {
                Text(isReqChecked(this.bookingReqs, req) ? '☑' : '☐')
                  .fontSize(12)
                  .fontColor(isReqChecked(this.bookingReqs, req) ? COLOR_PRIMARY : COLOR_TEXT_HINT)
                Text(req)
                  .fontSize(11)
                  .fontColor(isReqChecked(this.bookingReqs, req) ? COLOR_PRIMARY : COLOR_TEXT_SUB)
                  .margin({ left: 3 })
              }
              .alignItems(VerticalAlign.Center)
              .padding({ left: 10, right: 10, top: 6, bottom: 6 })
              .borderRadius(10)
              .backgroundColor(isReqChecked(this.bookingReqs, req) ? '#E3F0E9' : COLOR_CARD)
              .border({ width: 1, color: isReqChecked(this.bookingReqs, req) ? COLOR_PRIMARY : COLOR_BORDER })
              .margin({ right: 8, top: 8 })
              .onClick(() => {
                this.bookingReqs = toggleInList(this.bookingReqs, req)
              })
            }, (req: string) => req + '_' + isReqChecked(this.bookingReqs, req).toString())
          }
          .width('100%')

          Text('备注')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
            .alignSelf(ItemAlign.Start)
            .margin({ top: 12 })
          TextArea({ placeholder: '补充说明:楼层、电梯、是否需搬运上楼等', text: this.bookingNotes })
            .onChange((v: string) => {
              this.bookingNotes = v
            })
            .fontSize(11)
            .fontColor(COLOR_TEXT_MAIN)
            .placeholderColor(COLOR_TEXT_HINT)
            .height(64)
            .width('100%')
            .margin({ top: 6 })
            .padding(8)
            .borderRadius(10)
            .backgroundColor('#F5F3EE')
            .border({ width: 1, color: COLOR_BORDER })
        }
        .width('100%')
        .padding(14)
        .constraintSize({ maxHeight: '80%' })
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .width('100%')

      // 底部按钮
      Row() {
        Text('取消')
          .fontSize(13)
          .fontColor(COLOR_TEXT_SUB)
          .textAlign(TextAlign.Center)
          .width(110)
          .height(40)
          .borderRadius(20)
          .backgroundColor('#F5F3EE')
          .border({ width: 1, color: COLOR_BORDER })
          .margin({ right: 10 })
          .onClick(() => {
            this.showBookingModal = false
          })
        Text('确认预约')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
          .textAlign(TextAlign.Center)
          .width(110)
          .height(40)
          .borderRadius(20)
          .backgroundColor(COLOR_PRIMARY)
          .onClick(() => {
            this.showBookingModal = false
          })
      }
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Center)
      .width('100%')
      .padding(14)
    }
    .width('90%')
    .height('75%')
    .borderRadius(16)
    .backgroundColor(COLOR_CARD)
  }

  @Builder
  modalFieldBuilder(label: string, placeholder: string, value: string, onChange: (v: string) => void) {
    Column() {
      Text(label)
        .fontSize(11)
        .fontColor(COLOR_TEXT_SUB)
        .alignSelf(ItemAlign.Start)
      TextInput({ placeholder: placeholder, text: value })
        .onChange((v: string) => {
          onChange(v)
        })
        .fontSize(12)
        .fontColor(COLOR_TEXT_MAIN)
        .placeholderColor(COLOR_TEXT_HINT)
        .placeholderFont({ size: 11 })
        .height(36)
        .margin({ top: 5 })
        .padding({ left: 10, right: 10 })
        .borderRadius(10)
        .backgroundColor('#F5F3EE')
        .border({ width: 1, color: COLOR_BORDER })
    }
    .alignItems(HorizontalAlign.Start)
    .width('100%')
    .margin({ top: 10 })
  }

  // ---------- 弹框3:取消运输订单(警示式) ----------
  @Builder
  cancelModalBuilder() {
    Column() {
      Text('⚠️')
        .fontSize(34)
        .margin({ top: 18 })
      Text('确认取消此运输订单?')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLOR_TEXT_MAIN)
        .margin({ top: 8 })

      Column() {
        Row() {
          Text('植物名称')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
          Column()
            .layoutWeight(1)
          Text(this.selectedOrder.plantName)
            .fontSize(11)
            .fontWeight(FontWeight.Medium)
            .fontColor(COLOR_TEXT_MAIN)
        }
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .margin({ top: 6 })

        Row() {
          Text('运输路线')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
          Column()
            .layoutWeight(1)
          Text(this.selectedOrder.pickup + ' → ' + this.selectedOrder.destination)
            .fontSize(11)
            .fontWeight(FontWeight.Medium)
            .fontColor(COLOR_TEXT_MAIN)
        }
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .margin({ top: 8 })

        Row() {
          Text('订单金额')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
          Column()
            .layoutWeight(1)
          Text('¥' + this.selectedOrder.price.toString())
            .fontSize(11)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLOR_BROWN)
        }
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .margin({ top: 8 })

        Row() {
          Text('运输日期')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
          Column()
            .layoutWeight(1)
          Text(this.selectedOrder.date)
            .fontSize(11)
            .fontWeight(FontWeight.Medium)
            .fontColor(COLOR_TEXT_MAIN)
        }
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .margin({ top: 8 })
      }
      .alignItems(HorizontalAlign.Start)
      .width('100%')
      .padding(14)
      .borderRadius(12)
      .backgroundColor('#F5F3EE')
      .border({ width: 1, color: COLOR_BORDER })
      .margin({ top: 14 })

      Text('取消后无法恢复,请谨慎操作')
        .fontSize(10)
        .fontColor(COLOR_TEXT_HINT)
        .margin({ top: 12 })

      Row() {
        Text('再想想')
          .fontSize(13)
          .fontColor(COLOR_TEXT_SUB)
          .textAlign(TextAlign.Center)
          .width(105)
          .height(40)
          .borderRadius(20)
          .backgroundColor('#F5F3EE')
          .border({ width: 1, color: COLOR_BORDER })
          .margin({ right: 10 })
          .onClick(() => {
            this.showCancelModal = false
          })
        Text('确认取消')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
          .textAlign(TextAlign.Center)
          .width(105)
          .height(40)
          .borderRadius(20)
          .backgroundColor(COLOR_DANGER)
          .onClick(() => {
            this.showCancelModal = false
          })
      }
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Center)
      .margin({ top: 16, bottom: 18 })
    }
    .width('80%')
    .borderRadius(16)
    .backgroundColor(COLOR_CARD)
    .alignItems(HorizontalAlign.Center)
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 顶部统计条
          Row() {
            this.statCellBuilder('本月运输', getMonthOrderTotal().toString(), '单', COLOR_PRIMARY)
            Column().width(1).height(30).backgroundColor(COLOR_BORDER)
            this.statCellBuilder('进行中', getInProgressCount().toString(), '单', COLOR_WARNING)
            Column().width(1).height(30).backgroundColor(COLOR_BORDER)
            this.statCellBuilder('已完成', getCompletedCount().toString(), '单', COLOR_SUCCESS)
            Column().width(1).height(30).backgroundColor(COLOR_BORDER)
            this.statCellBuilder('总费用', getTotalCost().toString(), '元', COLOR_BROWN)
          }
          .alignItems(VerticalAlign.Center)
          .width('100%')
          .padding({ top: 12, bottom: 12 })
          .borderRadius(14)
          .backgroundColor(COLOR_CARD)
          .border({ width: 1, color: COLOR_BORDER })
          .margin({ top: 10 })

          this.chartBuilder()

          // 叫车表单卡
          Column() {
            Text('🚐 绿植叫车')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLOR_TEXT_MAIN)

            this.formFieldBuilder('植物名称', '如:天堂鸟、龟背竹', this.formPlantName, (v: string) => {
              this.formPlantName = v
            })
            this.formFieldBuilder('植物高度', '如:1.8米', this.formHeight, (v: string) => {
              this.formHeight = v
            })

            Text('花盆尺寸')
              .fontSize(11)
              .fontColor(COLOR_TEXT_SUB)
              .alignSelf(ItemAlign.Start)
              .margin({ top: 12 })
            Scroll() {
              Row() {
                this.potPillBuilder('小')
                this.potPillBuilder('中')
                this.potPillBuilder('大')
                this.potPillBuilder('特大')
              }
            }
            .scrollable(ScrollDirection.Horizontal)
            .scrollBar(BarState.Off)
            .width('100%')
            .margin({ top: 8 })

            this.formFieldBuilder('出发地', '如:顺义苗圃基地', this.formPickup, (v: string) => {
              this.formPickup = v
            })
            this.formFieldBuilder('目的地', '如:三里屯绿洲公寓', this.formDest, (v: string) => {
              this.formDest = v
            })

            Text('运输要求')
              .fontSize(11)
              .fontColor(COLOR_TEXT_SUB)
              .alignSelf(ItemAlign.Start)
              .margin({ top: 12 })
            Flex({ wrap: FlexWrap.Wrap }) {
              this.reqPillBuilder('防倾倒')
              this.reqPillBuilder('保温')
              this.reqPillBuilder('保湿')
              this.reqPillBuilder('夜间')
            }
            .width('100%')

            Text('预约运输')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(Color.White)
              .textAlign(TextAlign.Center)
              .width('100%')
              .height(44)
              .borderRadius(22)
              .backgroundColor(COLOR_PRIMARY)
              .margin({ top: 16 })
              .onClick(() => {
                this.showBookingModal = true
              })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')
          .padding(14)
          .borderRadius(14)
          .backgroundColor(COLOR_CARD)
          .border({ width: 1, color: COLOR_BORDER })
          .margin({ top: 10 })

          // 最近运输
          Row() {
            Text('最近运输')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLOR_TEXT_MAIN)
            Column()
              .layoutWeight(1)
            Text('共' + ORDER_LIST.length.toString() + '单 ›')
              .fontSize(10)
              .fontColor(COLOR_TEXT_HINT)
          }
          .alignItems(VerticalAlign.Center)
          .width('100%')
          .margin({ top: 16, bottom: 2, left: 4, right: 4 })

          ForEach(ORDER_LIST, (o: TransportOrderItem) => {
            this.orderItemBuilder(o)
          }, (o: TransportOrderItem) => o.id.toString())

          Column()
            .height(20)
        }
        .width('100%')
        .padding({ left: 12, right: 12 })
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .width('100%')

      if (this.showBookingModal) {
        this.modalOverlay(() => {
          this.showBookingModal = false
        })
        Column() {
          this.bookingModalBuilder()
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }

      if (this.showCancelModal) {
        this.modalOverlay(() => {
          this.showCancelModal = false
        })
        Column() {
          this.cancelModalBuilder()
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ============================================================================
// Tab2 苗圃 NURSERY:推荐横滑 + 排序药丸 + 商品网格 + 难度分布
// ============================================================================
@Component
struct NurseryContent {
  @State sortMode: string = '推荐'
  @State showPurchaseModal: boolean = false
  @State selectedProduct: NurseryProductItem = new NurseryProductItem(0, '', 0, '', 1, 0, COLOR_PRIMARY_LIGHT, '', '', false)
  @State buyQty: number = 1
  @State buyPot: string = '红陶盆'

  // ---------- 排序药丸 ----------
  @Builder
  sortPillBuilder(mode: string) {
    Text(mode)
      .fontSize(11)
      .fontColor(this.sortMode === mode ? Color.White : COLOR_TEXT_SUB)
      .textAlign(TextAlign.Center)
      .padding({ left: 14, right: 14, top: 6, bottom: 6 })
      .borderRadius(14)
      .backgroundColor(this.sortMode === mode ? COLOR_PRIMARY : COLOR_CARD)
      .border({ width: 1, color: this.sortMode === mode ? COLOR_PRIMARY : COLOR_BORDER })
      .margin({ right: 8 })
      .onClick(() => {
        this.sortMode = mode
      })
  }

  // ---------- 推荐大卡 ----------
  @Builder
  hotCardBuilder(p: NurseryProductItem) {
    Column() {
      Stack() {
        Column()
          .width('100%')
          .height(110)
          .borderRadius({ topLeft: 14, topRight: 14 })
          .backgroundColor(p.color)
        Text('🌿')
          .fontSize(40)
          .position({ x: 70, y: 30 })
        Text('🔥 ' + p.tag)
          .fontSize(9)
          .fontColor(Color.White)
          .padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .borderRadius(8)
          .backgroundColor('rgba(27,51,41,0.6)')
          .position({ x: 10, y: 10 })
      }
      .width('100%')
      .height(110)

      Column() {
        Text(p.name)
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_TEXT_MAIN)
        Row() {
          Text('¥' + p.price.toString())
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLOR_BROWN)
          Column()
            .layoutWeight(1)
          Text(p.height)
            .fontSize(9)
            .fontColor(COLOR_TEXT_HINT)
        }
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .margin({ top: 6 })
      }
      .alignItems(HorizontalAlign.Start)
      .width('100%')
      .padding(10)
    }
    .width(180)
    .borderRadius(14)
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
    .margin({ right: 10 })
    .onClick(() => {
      this.selectedProduct = p
      this.buyQty = 1
      this.showPurchaseModal = true
    })
  }

  // ---------- 商品卡 ----------
  @Builder
  productCardBuilder(p: NurseryProductItem) {
    Column() {
      Stack() {
        Column()
          .width('100%')
          .height(90)
          .borderRadius({ topLeft: 12, topRight: 12 })
          .backgroundColor(p.color)
        Text('🪴')
          .fontSize(32)
        if (p.isHot) {
          Text('热卖')
            .fontSize(8)
            .fontColor(Color.White)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .borderRadius(6)
            .backgroundColor(COLOR_PINK)
            .position({ x: 8, y: 8 })
        }
      }
      .width('100%')
      .height(90)

      Column() {
        Text(p.name)
          .fontSize(12)
          .fontWeight(FontWeight.Medium)
          .fontColor(COLOR_TEXT_MAIN)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        Row() {
          Text('¥' + p.price.toString())
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLOR_BROWN)
          Column()
            .layoutWeight(1)
          Text(p.height)
            .fontSize(8)
            .fontColor(COLOR_TEXT_HINT)
        }
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .margin({ top: 4 })

        Row() {
          Text('难度')
            .fontSize(8)
            .fontColor(COLOR_TEXT_SUB)
          Text(getStars(p.careDifficulty))
            .fontSize(8)
            .fontColor(getDifficultyColor(p.careDifficulty))
            .margin({ left: 3 })
          Text(getDifficultyLabel(p.careDifficulty))
            .fontSize(8)
            .fontColor(getDifficultyColor(p.careDifficulty))
            .margin({ left: 3 })
        }
        .alignItems(VerticalAlign.Center)
        .margin({ top: 4 })

        Row() {
          Text('库存 ' + p.stock.toString() + ' 盆')
            .fontSize(8)
            .fontColor(p.stock > 15 ? COLOR_TEXT_HINT : COLOR_PINK)
          Column()
            .layoutWeight(1)
          Text('购买')
            .fontSize(10)
            .fontColor(Color.White)
            .textAlign(TextAlign.Center)
            .width(46)
            .height(24)
            .borderRadius(12)
            .backgroundColor(COLOR_PRIMARY)
            .onClick(() => {
              this.selectedProduct = p
              this.buyQty = 1
              this.showPurchaseModal = true
            })
        }
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .margin({ top: 8 })
      }
      .alignItems(HorizontalAlign.Start)
      .width('100%')
      .padding(8)
    }
    .layoutWeight(1)
    .borderRadius(12)
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
    .onClick(() => {
      this.selectedProduct = p
      this.buyQty = 1
      this.showPurchaseModal = true
    })
  }

  // ---------- 难度分布统计(3格) ----------
  @Builder
  difficultyStatBuilder(label: string, count: string, icon: string, color: string) {
    Column() {
      Text(icon)
        .fontSize(16)
      Text(count)
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(color)
        .margin({ top: 3 })
      Text(label + '养护')
        .fontSize(9)
        .fontColor(COLOR_TEXT_SUB)
        .margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Center)
    .layoutWeight(1)
    .padding({ top: 12, bottom: 12 })
    .borderRadius(12)
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
    .margin({ right: 8 })
  }

  // ---------- 弹框4:购买绿植(商品详情式) ----------
  @Builder
  modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(27,51,41,0.55)')
      .onClick(() => {
        onClose()
      })
  }

  @Builder
  purchaseModalBuilder() {
    Column() {
      Row() {
        Text('🛒 商品详情')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_TEXT_MAIN)
        Column()
          .layoutWeight(1)
        Text('✕')
          .fontSize(15)
          .fontColor(COLOR_TEXT_SUB)
          .padding(6)
          .onClick(() => {
            this.showPurchaseModal = false
          })
      }
      .alignItems(VerticalAlign.Center)
      .width('100%')
      .padding(12)

      Scroll() {
        Column() {
          Stack() {
            Column()
              .width('100%')
              .height(140)
              .borderRadius(14)
              .backgroundColor(this.selectedProduct.color)
            Text('🌿')
              .fontSize(52)
            Text(this.selectedProduct.tag)
              .fontSize(9)
              .fontColor(Color.White)
              .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              .borderRadius(10)
              .backgroundColor('rgba(27,51,41,0.55)')
              .position({ x: 10, y: 10 })
          }
          .width('100%')
          .height(140)

          Text(this.selectedProduct.name)
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLOR_TEXT_MAIN)
            .margin({ top: 12 })

          Row() {
            Text('¥' + this.selectedProduct.price.toString())
              .fontSize(20)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLOR_BROWN)
            Column()
              .layoutWeight(1)
            Text('规格 ' + this.selectedProduct.height)
              .fontSize(10)
              .fontColor(COLOR_TEXT_SUB)
            Text(' | ')
              .fontSize(10)
              .fontColor(COLOR_TEXT_HINT)
            Text('难度 ' + getDifficultyLabel(this.selectedProduct.careDifficulty))
              .fontSize(10)
              .fontColor(getDifficultyColor(this.selectedProduct.careDifficulty))
          }
          .alignItems(VerticalAlign.Bottom)
          .width('100%')
          .margin({ top: 8 })

          Text(this.selectedProduct.desc)
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
            .lineHeight(18)
            .margin({ top: 10 })
            .padding(10)
            .borderRadius(10)
            .backgroundColor('#F5F3EE')
            .width('100%')

          // 数量选择
          Row() {
            Text('购买数量')
              .fontSize(12)
              .fontColor(COLOR_TEXT_MAIN)
            Column()
              .layoutWeight(1)
            Text('−')
              .fontSize(16)
              .fontColor(COLOR_PRIMARY)
              .textAlign(TextAlign.Center)
              .width(30)
              .height(30)
              .borderRadius(15)
              .backgroundColor('#E3F0E9')
              .onClick(() => {
                if (this.buyQty > 1) {
                  this.buyQty = this.buyQty - 1
                }
              })
            Text(this.buyQty.toString())
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLOR_TEXT_MAIN)
              .width(40)
              .textAlign(TextAlign.Center)
            Text('+')
              .fontSize(16)
              .fontColor(COLOR_PRIMARY)
              .textAlign(TextAlign.Center)
              .width(30)
              .height(30)
              .borderRadius(15)
              .backgroundColor('#E3F0E9')
              .onClick(() => {
                if (this.buyQty < this.selectedProduct.stock) {
                  this.buyQty = this.buyQty + 1
                }
              })
          }
          .alignItems(VerticalAlign.Center)
          .width('100%')
          .margin({ top: 14 })

          Text('花盆选择')
            .fontSize(12)
            .fontColor(COLOR_TEXT_MAIN)
            .alignSelf(ItemAlign.Start)
            .margin({ top: 14 })
          Row() {
            ForEach(POT_STYLE_OPTIONS, (pot: string) => {
              Text(pot)
                .fontSize(11)
                .fontColor(this.buyPot === pot ? Color.White : COLOR_TEXT_SUB)
                .textAlign(TextAlign.Center)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .borderRadius(12)
                .backgroundColor(this.buyPot === pot ? COLOR_PRIMARY : COLOR_CARD)
                .border({ width: 1, color: this.buyPot === pot ? COLOR_PRIMARY : COLOR_BORDER })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.buyPot = pot
                })
            }, (pot: string) => pot + '_' + this.buyPot)
          }
          .width('100%')

          Column()
            .height(16)
        }
        .width('100%')
        .padding({ left: 14, right: 14 })
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .width('100%')

      Row() {
        Text('加入购物车')
          .fontSize(12)
          .fontWeight(FontWeight.Medium)
          .fontColor(COLOR_PRIMARY)
          .textAlign(TextAlign.Center)
          .width(110)
          .height(40)
          .borderRadius(20)
          .backgroundColor('#E3F0E9')
          .margin({ right: 10 })
          .onClick(() => {
            this.showPurchaseModal = false
          })
        Text('立即购买')
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
          .textAlign(TextAlign.Center)
          .width(110)
          .height(40)
          .borderRadius(20)
          .backgroundColor(COLOR_PRIMARY)
          .onClick(() => {
            this.showPurchaseModal = false
          })
      }
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Center)
      .width('100%')
      .padding(14)
    }
    .width('92%')
    .height('70%')
    .borderRadius(16)
    .backgroundColor(COLOR_CARD)
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 推荐横滑区
          Column() {
            Row() {
              Text('🔥 本周特色')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLOR_TEXT_MAIN)
              Column()
                .layoutWeight(1)
              Text('查看全部 ›')
                .fontSize(10)
                .fontColor(COLOR_TEXT_HINT)
            }
            .alignItems(VerticalAlign.Center)
            .width('100%')
            .margin({ bottom: 10 })

            Scroll() {
              Row() {
                this.hotCardBuilder(PRODUCT_LIST[0])
                this.hotCardBuilder(PRODUCT_LIST[1])
                this.hotCardBuilder(PRODUCT_LIST[2])
              }
            }
            .scrollable(ScrollDirection.Horizontal)
            .scrollBar(BarState.Off)
            .width('100%')
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')
          .padding(12)
          .borderRadius(14)
          .backgroundColor(COLOR_CARD)
          .border({ width: 1, color: COLOR_BORDER })
          .margin({ top: 10 })

          // 排序药丸
          Row() {
            this.sortPillBuilder('推荐')
            this.sortPillBuilder('价格')
            this.sortPillBuilder('高度')
            this.sortPillBuilder('难度')
          }
          .alignItems(VerticalAlign.Center)
          .width('100%')
          .margin({ top: 12, left: 4 })

          // 难度分布(3格统计)
          Row() {
            this.difficultyStatBuilder('简单', getSimpleCount().toString() + ' 款', '🌱', CARE_DIFFICULTY_CONFIG['简单'].color)
            this.difficultyStatBuilder('中等', getMediumCount().toString() + ' 款', '🌿', CARE_DIFFICULTY_CONFIG['中等'].color)
            this.difficultyStatBuilder('困难', getHardCount().toString() + ' 款', '🥀', CARE_DIFFICULTY_CONFIG['困难'].color)
          }
          .alignItems(VerticalAlign.Center)
          .width('100%')
          .margin({ top: 10 })

          // 商品网格:每行2个
          Row() {
            this.productCardBuilder(PRODUCT_LIST[0])
            this.productCardBuilder(PRODUCT_LIST[1])
          }
          .alignItems(VerticalAlign.Top)
          .width('100%')
          .margin({ top: 10 })
          Row() {
            this.productCardBuilder(PRODUCT_LIST[2])
            this.productCardBuilder(PRODUCT_LIST[3])
          }
          .alignItems(VerticalAlign.Top)
          .width('100%')
          .margin({ top: 10 })
          Row() {
            this.productCardBuilder(PRODUCT_LIST[4])
            this.productCardBuilder(PRODUCT_LIST[5])
          }
          .alignItems(VerticalAlign.Top)
          .width('100%')
          .margin({ top: 10 })
          Row() {
            this.productCardBuilder(PRODUCT_LIST[6])
            this.productCardBuilder(PRODUCT_LIST[7])
          }
          .alignItems(VerticalAlign.Top)
          .width('100%')
          .margin({ top: 10 })
          Row() {
            this.productCardBuilder(PRODUCT_LIST[8])
            this.productCardBuilder(PRODUCT_LIST[9])
          }
          .alignItems(VerticalAlign.Top)
          .width('100%')
          .margin({ top: 10 })
          Row() {
            this.productCardBuilder(PRODUCT_LIST[10])
            this.productCardBuilder(PRODUCT_LIST[11])
          }
          .alignItems(VerticalAlign.Top)
          .width('100%')
          .margin({ top: 10 })

          Column()
            .height(20)
        }
        .width('100%')
        .padding({ left: 12, right: 12 })
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .width('100%')

      if (this.showPurchaseModal) {
        this.modalOverlay(() => {
          this.showPurchaseModal = false
        })
        Column() {
          this.purchaseModalBuilder()
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ============================================================================
// Tab3 养护 CARE:待办提醒 + 日历月历 + 养护计划卡(进度条)
// ============================================================================
@Component
struct CareContent {
  @State showEditModal: boolean = false
  @State editingPlan: CarePlanItem = new CarePlanItem(0, '', '3天', '半阴', '每月', '', 90)
  @State planPlantName: string = ''
  @State planWaterFreq: string = '3天'
  @State planLightNeed: string = '半阴'
  @State planFertFreq: string = '每月'
  @State planPrune: string = ''
  @State planNotes: string = ''

  // ---------- 日历单元格 ----------
  @Builder
  calendarCellBuilder(cell: number) {
    Column() {
      Text(getCalDay(cell) > 0 ? getCalDay(cell).toString() : ' ')
        .fontSize(10)
        .fontWeight(getCalDay(cell) === 23 ? FontWeight.Bold : FontWeight.Normal)
        .fontColor(getCalDay(cell) === 23 ? Color.White : COLOR_TEXT_MAIN)
        .textAlign(TextAlign.Center)
        .width(18)
        .height(18)
        .borderRadius(9)
        .backgroundColor(getCalDay(cell) === 23 ? COLOR_PRIMARY : Color.Transparent)
      Row() {
        ForEach(getCalMarks(getCalDay(cell)), (mark: string) => {
          Text(mark)
            .fontSize(7)
            .margin({ left: 1, right: 1 })
        }, (mark: string) => cell.toString() + '_' + mark)
      }
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Center)
      .height(9)
    }
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .layoutWeight(1)
    .height(40)
  }

  // ---------- 养护计划卡 ----------
  @Builder
  carePlanCardBuilder(c: CarePlanItem) {
    Column() {
      Row() {
        Text('🪴 ' + c.plantName)
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_TEXT_MAIN)
        Column()
          .layoutWeight(1)
        Text('编辑')
          .fontSize(10)
          .fontColor(COLOR_PRIMARY)
          .padding({ left: 10, right: 10, top: 4, bottom: 4 })
          .borderRadius(10)
          .backgroundColor('#E3F0E9')
          .onClick(() => {
            this.editingPlan = c
            this.planPlantName = c.plantName
            this.planWaterFreq = c.waterFreq
            this.planLightNeed = c.lightNeed
            this.planFertFreq = c.fertilizeFreq
            this.showEditModal = true
          })
      }
      .alignItems(VerticalAlign.Center)
      .width('100%')

      Row() {
        Text('💧 ' + c.waterFreq + '一浇')
          .fontSize(9)
          .fontColor(COLOR_PRIMARY)
          .padding({ left: 6, right: 6, top: 3, bottom: 3 })
          .borderRadius(6)
          .backgroundColor('#E3F0E9')
        Text('☀️ ' + c.lightNeed)
          .fontSize(9)
          .fontColor(COLOR_BROWN)
          .padding({ left: 6, right: 6, top: 3, bottom: 3 })
          .borderRadius(6)
          .backgroundColor('#F0E6DC')
          .margin({ left: 6 })
        Text('🌱 ' + c.fertilizeFreq + '施肥')
          .fontSize(9)
          .fontColor(COLOR_TEXT_SUB)
          .padding({ left: 6, right: 6, top: 3, bottom: 3 })
          .borderRadius(6)
          .backgroundColor('#F5F3EE')
          .margin({ left: 6 })
      }
      .alignItems(VerticalAlign.Center)
      .margin({ top: 8 })

      Row() {
        Text('最近浇水:' + c.lastWatered)
          .fontSize(9)
          .fontColor(COLOR_TEXT_HINT)
        Column()
          .layoutWeight(1)
        Text('健康 ' + c.healthPercent.toString() + '%')
          .fontSize(9)
          .fontColor(getHealthColor(c.healthPercent))
      }
      .alignItems(VerticalAlign.Center)
      .width('100%')
      .margin({ top: 8 })

      // 健康状态进度条
      Column() {
        Column()
          .width(c.healthPercent.toString() + '%')
          .height(5)
          .borderRadius(3)
          .backgroundColor(getHealthColor(c.healthPercent))
      }
      .alignItems(HorizontalAlign.Start)
      .width('100%')
      .height(5)
      .borderRadius(3)
      .backgroundColor('#E8EFE9')
      .margin({ top: 6 })
    }
    .width('100%')
    .padding(12)
    .borderRadius(12)
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
    .margin({ top: 10 })
  }

  // ---------- 弹框2:编辑养护计划(卡片式) ----------
  @Builder
  modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(27,51,41,0.55)')
      .onClick(() => {
        onClose()
      })
  }

  @Builder
  editModalBuilder() {
    Column() {
      Row() {
        Text('✏️ 编辑养护计划')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_TEXT_MAIN)
        Column()
          .layoutWeight(1)
        Text('✕')
          .fontSize(15)
          .fontColor(COLOR_TEXT_SUB)
          .padding(6)
          .onClick(() => {
            this.showEditModal = false
          })
      }
      .alignItems(VerticalAlign.Center)
      .width('100%')
      .padding(14)

      Scroll() {
        Column() {
          Column() {
            Text('植物名')
              .fontSize(11)
              .fontColor(COLOR_TEXT_SUB)
              .alignSelf(ItemAlign.Start)
            TextInput({ placeholder: '如:琴叶榕·客厅', text: this.planPlantName })
              .onChange((v: string) => {
                this.planPlantName = v
              })
              .fontSize(12)
              .fontColor(COLOR_TEXT_MAIN)
              .placeholderColor(COLOR_TEXT_HINT)
              .height(36)
              .margin({ top: 5 })
              .padding({ left: 10, right: 10 })
              .borderRadius(10)
              .backgroundColor('#F5F3EE')
              .border({ width: 1, color: COLOR_BORDER })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')

          Text('浇水频率')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
            .alignSelf(ItemAlign.Start)
            .margin({ top: 12 })
          Row() {
            ForEach(WATER_FREQ_OPTIONS, (f: string) => {
              Text(f)
                .fontSize(11)
                .fontColor(this.planWaterFreq === f ? Color.White : COLOR_TEXT_SUB)
                .textAlign(TextAlign.Center)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .borderRadius(10)
                .backgroundColor(this.planWaterFreq === f ? COLOR_PRIMARY : COLOR_CARD)
                .border({ width: 1, color: this.planWaterFreq === f ? COLOR_PRIMARY : COLOR_BORDER })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.planWaterFreq = f
                })
            }, (f: string) => f + '_' + this.planWaterFreq)
          }
          .width('100%')

          Text('光照需求')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
            .alignSelf(ItemAlign.Start)
            .margin({ top: 12 })
          Row() {
            ForEach(LIGHT_NEED_OPTIONS, (l: string) => {
              Text(l)
                .fontSize(11)
                .fontColor(this.planLightNeed === l ? Color.White : COLOR_TEXT_SUB)
                .textAlign(TextAlign.Center)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .borderRadius(10)
                .backgroundColor(this.planLightNeed === l ? COLOR_PRIMARY : COLOR_CARD)
                .border({ width: 1, color: this.planLightNeed === l ? COLOR_PRIMARY : COLOR_BORDER })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.planLightNeed = l
                })
            }, (l: string) => l + '_' + this.planLightNeed)
          }
          .width('100%')

          Text('施肥频率')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
            .alignSelf(ItemAlign.Start)
            .margin({ top: 12 })
          Row() {
            ForEach(FERT_FREQ_OPTIONS, (f: string) => {
              Text(f)
                .fontSize(11)
                .fontColor(this.planFertFreq === f ? Color.White : COLOR_TEXT_SUB)
                .textAlign(TextAlign.Center)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .borderRadius(10)
                .backgroundColor(this.planFertFreq === f ? COLOR_PRIMARY : COLOR_CARD)
                .border({ width: 1, color: this.planFertFreq === f ? COLOR_PRIMARY : COLOR_BORDER })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.planFertFreq = f
                })
            }, (f: string) => f + '_' + this.planFertFreq)
          }
          .width('100%')

          Text('修剪需求')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
            .alignSelf(ItemAlign.Start)
            .margin({ top: 12 })
          TextArea({ placeholder: '如:黄叶及时剪除,春季轻剪塑形', text: this.planPrune })
            .onChange((v: string) => {
              this.planPrune = v
            })
            .fontSize(11)
            .fontColor(COLOR_TEXT_MAIN)
            .placeholderColor(COLOR_TEXT_HINT)
            .height(56)
            .width('100%')
            .margin({ top: 6 })
            .padding(8)
            .borderRadius(10)
            .backgroundColor('#F5F3EE')
            .border({ width: 1, color: COLOR_BORDER })

          Text('备注')
            .fontSize(11)
            .fontColor(COLOR_TEXT_SUB)
            .alignSelf(ItemAlign.Start)
            .margin({ top: 12 })
          TextArea({ placeholder: '其他养护注意事项', text: this.planNotes })
            .onChange((v: string) => {
              this.planNotes = v
            })
            .fontSize(11)
            .fontColor(COLOR_TEXT_MAIN)
            .placeholderColor(COLOR_TEXT_HINT)
            .height(56)
            .width('100%')
            .margin({ top: 6 })
            .padding(8)
            .borderRadius(10)
            .backgroundColor('#F5F3EE')
            .border({ width: 1, color: COLOR_BORDER })
        }
        .width('100%')
        .padding({ left: 14, right: 14 })
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .width('100%')

      Row() {
        Text('取消')
          .fontSize(13)
          .fontColor(COLOR_TEXT_SUB)
          .textAlign(TextAlign.Center)
          .width(105)
          .height(40)
          .borderRadius(20)
          .backgroundColor('#F5F3EE')
          .border({ width: 1, color: COLOR_BORDER })
          .margin({ right: 10 })
          .onClick(() => {
            this.showEditModal = false
          })
        Text('保存')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
          .textAlign(TextAlign.Center)
          .width(105)
          .height(40)
          .borderRadius(20)
          .backgroundColor(COLOR_PRIMARY)
          .onClick(() => {
            this.showEditModal = false
          })
      }
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Center)
      .width('100%')
      .padding(14)
    }
    .width('85%')
    .height('60%')
    .borderRadius(16)
    .backgroundColor(COLOR_CARD)
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 待养护提醒
          Row() {
            Text('🔔')
              .fontSize(24)
            Column() {
              Text('今日待养护')
                .fontSize(12)
                .fontColor(Color.White)
              Text('有 ' + getTodayReminders().toString() + ' 项养护任务等你处理')
                .fontSize(10)
                .fontColor('#D8E4DC')
                .margin({ top: 3 })
            }
            .alignItems(HorizontalAlign.Start)
            .margin({ left: 10 })

            Column()
              .layoutWeight(1)
            Text(getTodayReminders().toString())
              .fontSize(26)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLOR_YELLOW)
          }
          .alignItems(VerticalAlign.Center)
          .width('100%')
          .padding(14)
          .borderRadius(14)
          .backgroundColor(COLOR_PRIMARY)
          .margin({ top: 10 })

          // 日历月历
          Column() {
            Row() {
              Text('‹')
                .fontSize(16)
                .fontColor(COLOR_TEXT_HINT)
              Text('2026年8月')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLOR_TEXT_MAIN)
                .margin({ left: 12, right: 12 })
              Text('›')
                .fontSize(16)
                .fontColor(COLOR_TEXT_HINT)
            }
            .alignItems(VerticalAlign.Center)
            .justifyContent(FlexAlign.Center)
            .width('100%')

            Row() {
              ForEach(WEEKDAY_LIST, (w: string) => {
                Text(w)
                  .fontSize(9)
                  .fontColor(COLOR_TEXT_HINT)
                  .textAlign(TextAlign.Center)
                  .layoutWeight(1)
              }, (w: string) => w)
            }
            .width('100%')
            .margin({ top: 10 })

            ForEach(CAL_ROW_LIST, (row: number) => {
              Row() {
                ForEach(CAL_COL_LIST, (col: number) => {
                  this.calendarCellBuilder(row * 7 + col)
                }, (col: number) => row.toString() + '_' + col.toString())
              }
              .width('100%')
            }, (row: number) => 'row' + row.toString())

            Row() {
              Text('💧 浇水')
                .fontSize(8)
                .fontColor(COLOR_PRIMARY)
              Text('🌱 施肥')
                .fontSize(8)
                .fontColor(COLOR_BROWN)
                .margin({ left: 10 })
              Text('✂️ 修剪')
                .fontSize(8)
                .fontColor(COLOR_PINK)
                .margin({ left: 10 })
            }
            .alignItems(VerticalAlign.Center)
            .justifyContent(FlexAlign.Center)
            .margin({ top: 8 })
          }
          .width('100%')
          .padding(12)
          .borderRadius(14)
          .backgroundColor(COLOR_CARD)
          .border({ width: 1, color: COLOR_BORDER })
          .margin({ top: 10 })

          // 我的植物养护卡列表
          Row() {
            Text('我的植物养护卡')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLOR_TEXT_MAIN)
            Column()
              .layoutWeight(1)
            Text(CARE_PLAN_LIST.length.toString() + ' 盆')
              .fontSize(10)
              .fontColor(COLOR_TEXT_HINT)
          }
          .alignItems(VerticalAlign.Center)
          .width('100%')
          .margin({ top: 16, left: 4, right: 4 })

          ForEach(CARE_PLAN_LIST, (c: CarePlanItem) => {
            this.carePlanCardBuilder(c)
          }, (c: CarePlanItem) => c.id.toString())

          Column()
            .height(20)
        }
        .width('100%')
        .padding({ left: 12, right: 12 })
      }
      .scrollBar(BarState.Off)
      .layoutWeight(1)
      .width('100%')

      if (this.showEditModal) {
        this.modalOverlay(() => {
          this.showEditModal = false
        })
        Column() {
          this.editModalBuilder()
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ============================================================================
// Tab4 社区 COMMUNITY:发布按钮 + 热门话题 + 帖子Feed
// ============================================================================
@Component
struct CommunityContent {
  @State likedPostIds: number[] = [3]
  @State myPostCount: number = 0
  @State activePostId: number = 0

  // ---------- 话题药丸 ----------
  @Builder
  topicPillBuilder(key: string) {
    Row() {
      Text(HOT_TOPIC_CONFIG[key].icon)
        .fontSize(11)
      Text('#' + HOT_TOPIC_CONFIG[key].label)
        .fontSize(10)
        .fontColor(COLOR_PRIMARY)
        .margin({ left: 3 })
      Text(HOT_TOPIC_CONFIG[key].heat)
        .fontSize(8)
        .fontColor(COLOR_TEXT_HINT)
        .margin({ left: 3 })
    }
    .alignItems(VerticalAlign.Center)
    .padding({ left: 10, right: 10, top: 6, bottom: 6 })
    .borderRadius(14)
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
    .margin({ right: 8 })
  }

  // ---------- 帖子条目 ----------
  @Builder
  postCardBuilder(p: PostItem) {
    Column() {
      Row() {
        Text(p.userAvatar)
          .fontSize(18)
          .textAlign(TextAlign.Center)
          .width(36)
          .height(36)
          .borderRadius(18)
          .backgroundColor('#E3F0E9')
        Column() {
          Text(p.userName)
            .fontSize(12)
            .fontWeight(FontWeight.Medium)
            .fontColor(COLOR_TEXT_MAIN)
          Text(p.time + ' · 关注')
            .fontSize(9)
            .fontColor(COLOR_TEXT_HINT)
            .margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
        .margin({ left: 8 })
        Text('+')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_PRIMARY)
          .textAlign(TextAlign.Center)
          .width(54)
          .height(26)
          .borderRadius(13)
          .backgroundColor('#E3F0E9')
          .onClick(() => {
            this.likedPostIds = toggleIdInList(this.likedPostIds, p.id)
          })
      }
      .alignItems(VerticalAlign.Center)
      .width('100%')

      Text(p.content)
        .fontSize(11)
        .fontColor(COLOR_TEXT_MAIN)
        .lineHeight(18)
        .margin({ top: 10 })

      Column()
        .width('100%')
        .height(120)
        .borderRadius(12)
        .backgroundColor(p.color)
        .margin({ top: 10 })

      Row() {
        Text('#' + p.tag)
          .fontSize(9)
          .fontColor(COLOR_PRIMARY)
          .padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .borderRadius(8)
          .backgroundColor('#E3F0E9')
        Column()
          .layoutWeight(1)
        Row() {
          Text(isPostLiked(this.likedPostIds, p.id) ? '❤️' : '🤍')
            .fontSize(13)
          Text((p.likeCount + (isPostLiked(this.likedPostIds, p.id) ? 1 : 0)).toString())
            .fontSize(10)
            .fontColor(isPostLiked(this.likedPostIds, p.id) ? COLOR_PINK : COLOR_TEXT_SUB)
            .margin({ left: 3 })
        }
        .alignItems(VerticalAlign.Center)
        .onClick(() => {
          this.likedPostIds = toggleIdInList(this.likedPostIds, p.id)
        })
        .margin({ right: 14 })

        Row() {
          Text('💬')
            .fontSize(13)
          Text(p.commentCount.toString())
            .fontSize(10)
            .fontColor(COLOR_TEXT_SUB)
            .margin({ left: 3 })
        }
        .alignItems(VerticalAlign.Center)
        .onClick(() => {
          this.activePostId = p.id
        })
        .margin({ right: 14 })

        Text('↗')
          .fontSize(13)
          .fontColor(COLOR_TEXT_SUB)
          .onClick(() => {
            this.activePostId = p.id
          })
      }
      .alignItems(VerticalAlign.Center)
      .width('100%')
      .margin({ top: 10 })
    }
    .alignItems(HorizontalAlign.Start)
    .width('100%')
    .padding(12)
    .borderRadius(14)
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
    .margin({ top: 10 })
  }

  build() {
    Scroll() {
      Column() {
        // 顶部:发布按钮 + 话题
        Column() {
          Row() {
            Text('🌱')
              .fontSize(16)
              .textAlign(TextAlign.Center)
              .width(34)
              .height(34)
              .borderRadius(17)
              .backgroundColor('#E3F0E9')
            Text('分享你的绿植日常…')
              .fontSize(11)
              .fontColor(COLOR_TEXT_HINT)
              .layoutWeight(1)
              .margin({ left: 10 })
            Text('发布')
              .fontSize(11)
              .fontWeight(FontWeight.Bold)
              .fontColor(Color.White)
              .textAlign(TextAlign.Center)
              .width(60)
              .height(30)
              .borderRadius(15)
              .backgroundColor(COLOR_PRIMARY)
              .onClick(() => {
                this.myPostCount = this.myPostCount + 1
              })
          }
          .alignItems(VerticalAlign.Center)
          .width('100%')

          Text(this.myPostCount > 0 ? '已发布 ' + this.myPostCount.toString() + ' 条绿植动态' : ' ')
            .fontSize(9)
            .fontColor(COLOR_TEXT_HINT)
            .alignSelf(ItemAlign.Start)
            .margin({ top: 6 })

          Scroll() {
            Row() {
              ForEach(Object.keys(HOT_TOPIC_CONFIG), (key: string) => {
                this.topicPillBuilder(key)
              }, (key: string) => key)
            }
          }
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .width('100%')
          .margin({ top: 10 })
        }
        .width('100%')
        .padding(12)
        .borderRadius(14)
        .backgroundColor(COLOR_CARD)
        .border({ width: 1, color: COLOR_BORDER })
        .margin({ top: 10 })

        // Feed列表
        ForEach(POST_LIST, (p: PostItem) => {
          this.postCardBuilder(p)
        }, (p: PostItem) => p.id.toString() + '_' + isPostLiked(this.likedPostIds, p.id).toString())

        Column()
          .height(20)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })
    }
    .scrollBar(BarState.Off)
    .layoutWeight(1)
    .width('100%')
  }
}

// 辅助:判断帖子是否已点赞
function isPostLiked(list: number[], id: number): boolean {
  return list.indexOf(id) >= 0
}

// ============================================================================
// Tab5 我的 PROFILE:自然风资料卡 + 大数字统计网格 + 设置列表
// ============================================================================
@Component
struct ProfileContent {
  @State clickedSetting: string = ''

  // ---------- 大数字统计格 ----------
  @Builder
  bigStatCellBuilder(icon: string, value: string, label: string, color: string) {
    Column() {
      Text(icon)
        .fontSize(20)
      Text(value)
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor(color)
        .margin({ top: 4 })
      Text(label)
        .fontSize(10)
        .fontColor(COLOR_TEXT_SUB)
        .margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .layoutWeight(1)
    .height(88)
    .borderRadius(14)
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
  }

  // ---------- 设置条目 ----------
  @Builder
  settingItemBuilder(icon: string, label: string, desc: string) {
    Row() {
      Text(icon)
        .fontSize(16)
        .textAlign(TextAlign.Center)
        .width(34)
        .height(34)
        .borderRadius(17)
        .backgroundColor('#E3F0E9')
      Column() {
        Text(label)
          .fontSize(12)
          .fontWeight(FontWeight.Medium)
          .fontColor(COLOR_TEXT_MAIN)
        Text(desc)
          .fontSize(9)
          .fontColor(COLOR_TEXT_HINT)
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 10 })
      Text('›')
        .fontSize(16)
        .fontColor(COLOR_TEXT_HINT)
    }
    .alignItems(VerticalAlign.Center)
    .width('100%')
    .padding(12)
    .borderRadius(12)
    .backgroundColor(COLOR_CARD)
    .border({ width: 1, color: COLOR_BORDER })
    .margin({ top: 10 })
    .onClick(() => {
      this.clickedSetting = label
    })
  }

  build() {
    Scroll() {
      Column() {
        // 自然风资料卡
        Column() {
          Stack() {
            Text('🧑‍🌾')
              .fontSize(30)
              .textAlign(TextAlign.Center)
              .width(64)
              .height(64)
              .borderRadius(32)
              .backgroundColor('#E3F0E9')
              .border({ width: 2, color: COLOR_PRIMARY_LIGHT })
            Text('Lv.6')
              .fontSize(8)
              .fontColor(Color.White)
              .textAlign(TextAlign.Center)
              .width(30)
              .height(14)
              .borderRadius(7)
              .backgroundColor(COLOR_PRIMARY)
              .position({ x: 40, y: 48 })
          }
          .width(64)
          .height(64)

          Text('林间植友')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLOR_TEXT_MAIN)
            .margin({ top: 10 })
          Text('资深园丁 · 自然生活家')
            .fontSize(10)
            .fontColor(COLOR_TEXT_SUB)
            .margin({ top: 4 })

          Row() {
            Column() {
              Text('628')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLOR_PRIMARY)
              Text('注册天数')
                .fontSize(8)
                .fontColor(COLOR_TEXT_HINT)
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Center)
            .margin({ right: 24 })

            Column() {
              Text('32')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLOR_BROWN)
              Text('关注')
                .fontSize(8)
                .fontColor(COLOR_TEXT_HINT)
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Center)
            .margin({ right: 24 })

            Column() {
              Text('128')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLOR_PINK)
              Text('粉丝')
                .fontSize(8)
                .fontColor(COLOR_TEXT_HINT)
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Center)
          }
          .alignItems(VerticalAlign.Center)
          .margin({ top: 14 })
        }
        .alignItems(HorizontalAlign.Center)
        .width('100%')
        .padding(20)
        .borderRadius(16)
        .backgroundColor(COLOR_CARD)
        .border({ width: 1, color: COLOR_BORDER })
        .margin({ top: 10 })

        // 花园统计 2x2 大数字网格
        Row() {
          this.bigStatCellBuilder('🪴', '32', '总植物', COLOR_PRIMARY)
          this.bigStatCellBuilder('🚐', '18', '总运输', COLOR_BROWN)
        }
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .margin({ top: 12 })
        Row() {
          this.bigStatCellBuilder('💧', '286', '养护天数', COLOR_SUCCESS)
          this.bigStatCellBuilder('⭐', '2480', '积分', COLOR_YELLOW)
        }
        .alignItems(VerticalAlign.Center)
        .width('100%')
        .margin({ top: 10 })

        // 设置列表
        Text('设置')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLOR_TEXT_MAIN)
          .alignSelf(ItemAlign.Start)
          .margin({ top: 16, left: 4 })

        this.settingItemBuilder('🪴', '我的植物', '32盆植物档案')
        this.settingItemBuilder('📍', '地址管理', '常用收货与发货地址')
        this.settingItemBuilder('📅', '养护计划', '6条进行中的计划')
        this.settingItemBuilder('💬', '客服', '运输与养护问题咨询')
        this.settingItemBuilder('⚙️', '设置', '通知、隐私与通用')

        if (this.clickedSetting !== '') {
          Text('已打开「' + this.clickedSetting + '」')
            .fontSize(10)
            .fontColor(COLOR_TEXT_HINT)
            .margin({ top: 14 })
        }

        Column()
          .height(20)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })
    }
    .scrollBar(BarState.Off)
    .layoutWeight(1)
    .width('100%')
  }
}


六、总结

回顾整个绿植运输 GREEN HAUL 平台的开发实践,我们可以清晰地看到基于 HarmonyOS 6.1.1 与 HarmonyOS ArkTS API 24 构建复杂移动应用时的完整方法论。本项目以一个约三千行的单文件 ArkTS 源码,完整实现了一个包含五个 Tab 页面与四个弹框的大型绿植运输加苗圃商城综合平台,涵盖了数据建模、配置映射、纯函数辅助、粒子动画、表单交互、弹框管理、列表渲染、图表可视化等移动应用开发的全谱系技术场景。整个应用采用植物自然清新风格的视觉设计,以森林绿、泥土棕、阳光黄构建了一套完整的设计语言体系,通过十六个颜色常量统一管控全应用的视觉一致性,这种设计令牌(design token)的工程化实践在大型项目中尤为关键。

在这里插入图片描述

从架构层面来看,本项目验证了"配置驱动加纯函数辅助加组件化构建"三层架构模式在 ArkTS 开发中的有效性。最底层通过 @Observed 装饰的数据模型类实现可观察的对象状态,中间层通过 Record 类型的配置映射表与大量无副作用纯函数将业务逻辑与视图渲染彻底解耦,最上层通过 @Entry 与 @Component 组件配合参数化 @Builder 方法构建出层次分明、职责清晰的 UI 体系。特别值得强调的是不可变数据更新模式——toggleInList 与 toggleIdInList 等函数通过 slice 与 concat 创建新数组而非原地修改,确保了 @State 状态变化能被框架渲染管线正确检测,这是 ArkTS 声明式 UI 开发中必须遵守的核心纪律。advanceParticles 粒子动画函数同样遵循不可变更新原则,每次返回全新数组赋予 leafParticles 触发重绘。

在交互体验方面,本项目展现了丰富的动态视觉效果与用户反馈机制。漂浮叶子粒子系统通过 setInterval 定时器以三百五十毫秒周期驱动 advanceParticles 函数计算十片叶子的新坐标,配合 hitTestBehavior(HitTestMode.None) 实现装饰层不阻挡底层点击的精巧设计,为应用注入了植物自然生长的动态生命力。柱状图通过纯组件搭建实现了月度运输次数的可视化,养护日历通过双层 ForEach 循环渲染五乘七网格并通过配置映射标记每日的浇水、施肥、修剪任务,健康进度条通过百分比宽度与动态颜色实现了无图表库的数据可视化。这些可视化方案证明了基于 HarmonyOS API 24 的基础组件在数据展示场景下具备充分的表达能力,无需依赖第三方图表库即可实现丰富的视觉呈现。

展望未来,绿植运输平台在当前基础上仍有多个方向的扩展空间。在数据层面,当前的静态数据集可替换为 HTTP 网络请求获取的动态数据,配合 @Observed 模型实现与服务端的实时同步;在交互层面,弹框可升级为 Navigation 路由实现页面级跳转,支持返回栈管理与转场动画;在能力层面,可接入 HarmonyOS 的位置服务实现运输路线地图可视化、接入通知服务实现养护提醒推送、接入分布式能力实现跨设备绿植档案同步。这些扩展方向均可在当前架构基础上平滑接入,因为配置驱动与纯函数辅助的架构已经为功能扩展预留了充足的弹性空间。基于 HarmonyOS 6.1.1 与 ArkTS API 24 的声明式开发范式,以其类型安全、渲染高效、架构清晰的特点,正成为构建全场景移动应用的有力工具,绿植运输平台的实践为这一开发范式在垂直行业应用中的落地提供了详实的参考样本。

Logo

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

更多推荐