一、技术背景

1.1 HarmonyOS 声明式 UI 框架

HarmonyOS 是华为推出的分布式操作系统,其声明式 UI 开发范式是 ArkUI 框架的核心特性。与传统的命令式 UI 开发不同,声明式 UI 允许开发者通过描述 UI 的"目标状态"来构建界面,框架会自动处理状态变化到视图更新的映射过程。这种开发模式极大地简化了 UI 代码的复杂度,开发者无需手动操作 DOM 或 View 树,只需关注数据状态与视图的绑定关系。

在 ArkUI 声明式开发范式中,@Component 装饰器标记的结构体是 UI 的基本构建单元,每个组件都有自己的 build() 方法来描述其渲染内容。组件之间可以嵌套组合,形成完整的界面树。@Entry 装饰器则标记页面的入口组件,它是整个页面的根节点,负责承载页面的完整生命周期。

声明式 UI 的核心优势在于其数据驱动的渲染机制。当状态变量发生变化时,框架会自动检测依赖关系,并最小化地重新渲染受影响的组件部分。这种细粒度的更新机制不仅提升了开发效率,也保证了应用的运行性能。对于复杂的电商类应用而言,声明式 UI 能够让开发者更专注于业务逻辑的实现,而非繁琐的界面操作。

ArkUI 还提供了丰富的内置组件和属性方法,如 Column、Row、Stack 等布局组件,以及 Text、Button、Image、Scroll 等基础组件。这些组件都支持链式调用的属性设置方式,开发者可以通过连续的 . 调用来配置组件的样式和行为。这种 API 设计风格简洁直观,与现代前端框架的开发体验高度一致。
在这里插入图片描述

1.2 ArkTS 语言特性

ArkTS 是 HarmonyOS 应用开发的首选编程语言,它基于 TypeScript 扩展而来,保留了 TypeScript 的类型系统和语法特性,同时增加了声明式 UI、状态管理等专属能力。ArkTS 的类型系统在编译期就能发现大量潜在错误,为大型应用的可维护性提供了坚实保障。

在类与对象方面,ArkTS 完全支持面向对象编程范式,包括类的定义、继承、封装、多态等特性。开发者可以定义数据模型类来封装业务实体,通过构造函数初始化对象状态,通过方法封装业务逻辑。结合 @Observed 装饰器,类的实例可以成为响应式数据源,当属性变化时自动触发视图更新。

ArkTS 的装饰器系统是其最具特色的语言扩展。除了 @Component@Entry 之外,还有 @State@Prop@Link@Observed@ObjectLink 等一系列状态管理装饰器,以及 @Builder@Styles@Extend 等 UI 复用装饰器。这些装饰器极大地丰富了语言的表达能力,让开发者可以用声明式的方式描述复杂的 UI 逻辑和状态流转。

在函数式编程方面,ArkTS 也支持箭头函数、高阶函数、闭包等特性。代码中大量使用的 ForEach 组件就是函数式思想在 UI 渲染中的体现,它通过遍历数组数据来生成对应的 UI 元素。此外,顶层纯函数的使用也是 ArkTS 开发中的常见模式,它们不依赖组件状态,只根据输入参数计算返回值,具有良好的可测试性和可复用性。

1.3 HarmonyOS 生态与跨设备能力

HarmonyOS 的核心设计理念是"分布式",它致力于构建一个跨设备、跨场景的统一操作系统生态。从手机、平板、智慧屏到智能穿戴、车载系统,HarmonyOS 能够在不同形态的设备上提供一致的开发体验和用户体验。这种分布式能力对于电商类应用尤为重要,用户可以在手机上浏览商品,在平板上查看详情,在智慧屏上分享给家人。

跨设备能力的技术基础是分布式软总线和分布式数据管理。分布式软总线提供了设备间的高速通信通道,支持自动发现、自动连接和数据传输。分布式数据管理则实现了数据在多设备间的同步和共享,确保用户在任何设备上都能获取一致的数据状态。对于采菌电商平台来说,这意味着用户的收藏、购物车、订单等数据可以在多端无缝同步。

ArkUI 的自适应布局能力也是跨设备开发的重要支撑。通过百分比布局、弹性布局、栅格系统等机制,应用界面可以自动适配不同尺寸的屏幕。代码中大量使用的 layoutWeight、百分比宽度等属性,就是实现自适应布局的关键手段。在野生菌电商这样的垂直领域应用中,良好的多设备适配能够覆盖更广泛的用户群体。

HarmonyOS 还提供了原子化服务(Atomic Service)的能力,应用可以拆分为多个独立的服务卡片,用户无需安装完整应用即可使用核心功能。对于采菌平台来说,可以提供"今日菌情"、"毒菌预警"等服务卡片,让用户在负一屏就能快速获取关键信息。这种轻量化的服务形态,特别适合时效性强的菌情资讯类内容。

1.4 @Observed 装饰器与状态管理机制

状态管理是声明式 UI 开发中的核心议题。在 ArkUI 中,状态管理通过一系列装饰器来实现,其中 @Observed 装饰器在类层面的状态管理中扮演着关键角色。当一个类被 @Observed 装饰后,其实例的属性变化能够被框架追踪,从而触发依赖这些属性的视图进行更新。

@Observed 装饰器通常与 @ObjectLink 配合使用,实现对象级别的双向数据绑定。但在本项目中,由于数据列表存储在组件的 @State 变量中,且通过数组的 splicepush 等方法进行修改,框架能够检测到数组引用的变化或数组内容的变更,从而触发 ForEach 的重新渲染。这种机制保证了数据操作的直观性和视图更新的及时性。

@State 装饰器管理基本类型和简单对象不同,@Observed 专注于类实例的深层观察。当类的嵌套属性发生变化时,@Observed 能够追踪到这些深层变化,而 @State 只能追踪到对象引用的变化。对于电商应用中的商品、订单、用户等复杂业务实体,使用 @Observed 类能够实现更精细的状态管理。

状态管理的性能考量也是一个重要话题。过多的状态变量或过深的观察层级都会增加框架的追踪开销。在本项目中,开发者将弹框状态、编辑数据、列表数据分别用独立的 @State 变量管理,这种适度的状态拆分既保证了功能的清晰性,又避免了不必要的性能损耗。同时,ForEach 的第二个参数(键生成函数)也对列表渲染性能有重要影响,合理的键值能够让框架准确识别增删改的位置,减少 DOM 操作。

1.5 野生菌电商行业的技术特点与挑战

野生菌电商是一个极具特色的垂直电商领域,它融合了农产品电商、季节性商品运营、社区内容运营等多重属性。与标准电商不同,野生菌具有强季节性、产地依赖性、品质变异性等特点,这对技术方案提出了独特的挑战。

首先是季节性波动带来的流量压力。野生菌的出菌期集中在每年的 6 月至 10 月,其中 7 至 9 月是旺季高峰期。在这几个月里,平台的访问量和交易量会急剧攀升,而在淡季则相对冷清。这要求系统架构具备良好的弹性伸缩能力,能够在旺季承载高并发访问,同时在淡季保持合理的资源利用率。前端层面也需要通过缓存策略、懒加载、图片优化等手段来提升加载性能。

其次是品控与安全问题。野生菌的识别和安全是行业的核心痛点,误食毒菌可能导致严重的健康后果。因此,平台不仅是一个交易场所,还承担着科普教育和安全警示的责任。代码中的毒性预警模块、菌谱图鉴、识菌指南等功能,都是为了解决这一痛点而设计的。技术上需要实现丰富的图文展示、清晰的危险等级标识、便捷的查询检索等能力。

第三是供应链的特殊性。野生菌大多是当天采摘、当天发货,冷链物流要求高,库存管理难度大。价格也会随着产量、天气、市场行情等因素频繁波动。因此,系统需要支持灵活的价格调整、实时的库存同步、快速的订单处理等功能。代码中山货页面的库存展示、热度标识、下架操作等,都是供应链管理在前端的体现。

最后是社区运营的重要性。野生菌爱好者是一个高度垂直且粘性很强的群体,他们乐于分享采菌经历、交流识菌经验、讨论菌情动态。菌友圈功能就是为了满足这一需求,通过 UGC 内容增强用户粘性和平台活跃度。技术上需要支持动态发布、点赞评论、分类标签、排序筛选等社交功能。

二、项目概览

本项目是一个面向野生菌爱好者和消费者的山货电商采菌平台,采用 HarmonyOS ArkTS 声明式开发范式构建。应用整体采用深绿色森林主题配色,营造出浓厚的自然山野氛围,与野生菌的生长环境高度契合。界面结构清晰,交互流畅,集资讯、科普、电商、社交于一体,是一个功能完整的垂直领域移动应用。

应用的整体架构采用底部 Tab 导航 + 内容区切换的经典布局模式。底部设有 4 个主 Tab,分别是首页、菌山、山货、我的,覆盖了内容浏览、产地信息、商品交易、个人中心四大核心模块。其中首页是内容最丰富的页面,内部又包含 6 个子 Tab,分别为精选、菌谱图鉴、毒性预警、菌季指数、采菌向导、菌友圈,形成了多级导航的信息架构。

视觉设计方面,应用采用深色主题设计,以深森林绿为基调,配合苔藓绿、酸橙绿、土壤棕、天空蓝等辅助色,构建了一套完整且富有特色的色彩体系。头部区域集成了定位信息、搜索购物入口、今日菌情大卡和热门菌种横滚,信息密度高但布局有序。内容区根据不同 Tab 展示不同的内容形态,包括卡片列表、瀑布流、时间轴、图表等多种展示方式。

特效层是应用的一大亮点,采用孢子飘散动画和蘑菇呼吸缩放效果,增强了页面的生动性和趣味性。特效通过 setInterval 定时器驱动,仅覆盖内容区域而不遮挡头部,既保证了视觉效果又不影响核心功能的使用。弹框系统采用条件渲染 + 遮罩层的方式实现,支持发布动态、编辑计划、删除确认等多种交互场景。

从技术架构来看,应用采用单文件组件化的开发方式,所有 UI 构建逻辑都通过 @Builder 装饰器封装为独立的构建方法。数据模型层使用 @Observed 类定义,全局常量与模拟数据在文件顶部统一定义。状态管理采用 @State 装饰器结合组件内部方法的模式,弹框的开闭、Tab 的切换、列表的增删改都通过状态变量驱动。整体代码结构清晰,层次分明,具有良好的可读性和可维护性。

三、逐段代码分析

3.1 颜色体系定义:森林主题的视觉基石

interface ColorPalette {
  bg: string;
  card: string;
  card2: string;
  title: string;
  sub: string;
  line: string;
  forest: string;
  forestDeep: string;
  moss: string;
  lime: string;
  spore: string;
  terra: string;
  red: string;
  amber: string;
  sky: string;
  mask: string;
  white: string;
}

const COLORS: ColorPalette = {
  bg: '#1E2A1E',
  card: '#28362A',
  card2: '#31422F',
  title: '#EAF2E4',
  sub: '#9BB194',
  line: '#3C4E3A',
  forest: '#4E7A46',
  forestDeep: '#35542F',
  moss: '#6B9B57',
  lime: '#A8C97F',
  spore: '#D8E8C0',
  terra: '#A8763E',
  red: '#C94F4F',
  amber: '#D9A441',
  sky: '#6FA8B8',
  mask: '#0F170FC2',
  white: '#FFFFFF'
};

颜色体系是整个应用视觉风格的基石,代码首先定义了一个 ColorPalette 接口来规范颜色字段的类型,然后通过 COLORS 常量提供具体的颜色值实现。这种接口 + 常量的模式是 TypeScript/ArkTS 中定义全局配置的常用方式,既保证了类型安全,又方便在代码中统一引用。

从色彩规划来看,整个配色方案围绕"森林"主题展开,采用了分层递进的绿色系设计。背景色 bg 是深墨绿色 #1E2A1E,为整个应用奠定了暗色基调。卡片色有两个层级:card 是稍亮的深绿 #28362A,用于主要卡片背景;card2 更亮一些 #31422F,用于次级元素如标签、按钮背景。这种多层级的卡片色设计增强了界面的层次感和深度感。

文字颜色也做了精细化的分层处理。主标题色 title 是浅灰白色 #EAF2E4,带有微妙的绿色倾向,与整体色调协调统一。次要文字色 sub 是灰绿色 #9BB194,用于辅助信息和说明文字。spore 色则是更浅的孢子白 #D8E8C0,用于渐变卡片上的文字和高亮信息。

功能色方面,应用定义了森林绿 forest、深森林绿 forestDeep、苔藓绿 moss、酸橙绿 lime 等多个绿色调的变体,分别用于不同的强调场景。土壤棕 terra、天空蓝 sky、琥珀黄 amber 等辅助色则丰富了视觉表现,为不同的功能模块(如菌山、采菌计划、鲜菌冷链等)提供了色彩区分。警示红 red 专门用于毒性预警和危险提示,在深绿基调中格外醒目。

值得一提的是遮罩色 mask 的定义,它使用了 8 位十六进制颜色值 #0F170FC2,其中后两位 C2 表示 alpha 透明度(约 76% 的不透明度)。这种半透明遮罩在弹框场景中非常常见,既能清晰地分隔弹框与背景,又不会完全遮挡底层内容,保持了视觉的连续性。

3.2 菌种数据模型:@Observed 装饰的业务实体

@Observed
export class ShroomItem {
  id: number = 0;
  name: string = '';
  latin: string = '';
  region: string = '';
  season: string = '';
  toxicity: string = '';
  price: string = '';
  icon: string = '';
  constructor(id: number, name: string, latin: string, region: string,
              season: string, toxicity: string, price: string, icon: string) {
    this.id = id; this.name = name; this.latin = latin; this.region = region;
    this.season = season; this.toxicity = toxicity; this.price = price; this.icon = icon;
  }
}

ShroomItem 类是菌种信息的数据模型,使用 @Observed 装饰器标记,使其成为可观察对象。这意味着当 ShroomItem 实例的属性发生变化时,ArkUI 框架能够自动检测到变化并更新相关的视图。这种响应式数据机制是声明式 UI 的核心支撑。

类的属性涵盖了菌种展示所需的全部信息:id 是唯一标识符,用于列表渲染的键值生成;name 是中文名称,如"松茸"、"鸡枞菌"等;latin 是拉丁学名,体现了应用的专业性;region 是产地信息,标注了菌种的生长海拔和地理位置;season 是出菌季节,反映了商品的时令特性;toxicity 是毒性标识,用于安全提示;price 是价格信息;icon 是图标 emoji,用于视觉展示。

构造函数采用了全参数初始化的方式,所有属性都在构造函数中一一赋值。这种方式虽然略显繁琐,但保证了对象创建时的完整性和可控性。每个属性都有默认值(数字为 0,字符串为空串),这是 ArkTS 类定义的良好实践,可以避免未初始化属性带来的运行时问题。

@Observed 装饰器的使用值得深入探讨。在 ArkUI 中,@Observed 用于装饰类,表示该类的实例需要被框架深度观察。当类的属性变化时,即使是嵌套属性,框架也能追踪到变化并触发视图更新。这与 @State 装饰器不同——@State 只能追踪变量引用的变化,而 @Observed 可以追踪对象内部属性的变化。在本项目中,虽然主要操作的是数组的增删,但 @Observed 为后续的属性级更新预留了扩展空间。

3.3 采菌计划与山货数据模型:电商核心实体

@Observed
export class ForageItem {
  id: number = 0;
  hill: string = '';
  date: string = '';
  price: string = '';
  quota: string = '';
  guide: string = '';
  note: string = '';
  status: string = '';
  constructor(id: number, hill: string, date: string, price: string,
              quota: string, guide: string, note: string, status: string) {
    this.id = id; this.hill = hill; this.date = date; this.price = price;
    this.quota = quota; this.guide = guide; this.note = note; this.status = status;
  }
}

ForageItem 是采菌跟团计划的数据模型,封装了一次采菌活动的全部信息。hill 字段表示菌山线路名称,date 是出行日期和时间,price 是跟团费用,quota 是名额剩余情况,guide 是带队向导,note 是行程说明和注意事项,status 则表示报名状态(如"报名中"、“即将满员”、“热抢”、"预告"等)。

这个数据模型体现了 OTA(在线旅游)类产品的典型特征——以时间和名额为核心资源的服务型商品。与传统实物电商不同,采菌跟团是一种体验式服务,其库存不是实物而是名额,价格也会随季节和热度动态调整。status 字段的多状态设计(报名中、即将满员、热抢、预告、常规)反映了运营的精细化程度,不同状态用不同颜色标识,帮助用户快速识别机会和风险。

@Observed
export class GoodsItem {
  id: number = 0;
  name: string = '';
  cat: string = '';
  price: string = '';
  origin: string = '';
  unit: string = '';
  stock: number = 0;
  hot: string = '';
  icon: string = '';
  constructor(id: number, name: string, cat: string, price: string, origin: string,
              unit: string, stock: number, hot: string, icon: string) {
    this.id = id; this.name = name; this.cat = cat; this.price = price; this.origin = origin;
    this.unit = unit; this.stock = stock; this.hot = hot; this.icon = icon;
  }
}

GoodsItem 是山货商品的数据模型,代表电商平台上的实物商品。cat 字段表示商品分类(鲜菌、干菌、加工品、干货组合、冻菌、周边等),origin 是产地,unit 是规格单位,stock 是库存数量(数字类型),hot 是热度标识(如 🔥)。

ForageItem 不同,GoodsItem 更接近传统电商的商品模型,具有明确的分类、规格、库存等属性。stock 使用数字类型而非字符串,便于进行库存计算和缺货判断。hot 字段则是运营手段,通过火焰 emoji 标识热门商品,引导用户注意力。

这两个数据模型共同构成了平台的交易核心——服务型商品(采菌跟团)和实物型商品(山货商城)的双轨并行。这种混合电商模式在垂直领域越来越常见,既卖产品又卖服务,能够提升用户粘性和客单价。

3.4 菌友圈与图表数据模型:社区与数据可视化

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

ShroomFriendItem 是菌友圈动态的数据模型,封装了社交内容的核心要素。nickavatar 是发布者信息,text 是动态正文,likesreplies 是互动数据(点赞数和回复数),tag 是内容分类标签(如"菌情播报"、“山货上新”、“毒菌警示”、"科普"等),time 是发布时间。

这个数据模型体现了社区产品的典型特征。标签系统是内容组织的重要手段,通过 tag 字段可以对动态进行分类展示和筛选。互动数据(点赞、回复)是社区活跃度的重要指标,也是内容排序和推荐的依据。时间字段使用相对时间表述(如"1小时前"、“昨天”、“2天前”),符合移动应用的用户习惯。

interface RainChartItem {
  week: string;
  val: number;
  color: string;
}

const rainCharts: RainChartItem[] = [
  { week: '7月', val: 35, color: '#6FA8B8' },
  { week: '8月', val: 68, color: '#4E7A46' },
  { week: '9月', val: 92, color: '#6B9B57' },
  { week: '10月', val: 78, color: '#A8C97F' },
  { week: '11月', val: 44, color: '#D9A441' },
  { week: '12月', val: 16, color: '#A8763E' }
];

RainChartItem 是雨量柱状图的数据项接口,用于菌季指数页面的图表展示。每个数据项包含月份标签、数值和柱体颜色。有趣的是,颜色是随数值变化的——从 7 月的天空蓝(雨季初期),到 8-10 月的各种绿色(出菌旺季),再到 11-12 月的黄棕色(秋冬衰退)。这种颜色渐变的设计不仅美观,也直观地传达了季节感。

图表数据采用接口定义而非 @Observed 类,因为这些数据是静态的展示数据,不需要响应式更新。这体现了数据模型设计中的一个重要原则——根据数据的使用场景选择合适的定义方式。静态配置数据使用接口 + 常量即可,而需要动态修改的业务实体则使用 @Observed 类。

3.5 毒性预警数据:安全科普的结构化表达

interface ToxicItem {
  label: string;
  pct: number;
  color: string;
}

const toxicList: ToxicItem[] = [
  { label: '致命鹅膏·白毒伞', pct: 99, color: '#C94F4F' },
  { label: '亚稀褶红菇·火炭菌', pct: 94, color: '#D96A4F' },
  { label: '条盖盔孢伞·假鸡枞', pct: 88, color: '#D9A441' },
  { label: '大青褶伞·铅绿褶菇', pct: 76, color: '#A8763E' }
];

毒性预警数据是应用安全科普功能的重要组成部分。ToxicItem 接口定义了毒菌信息的数据结构,label 是毒菌名称(包含俗名和学名对照),pct 是危险程度百分比,color 是对应的警示色。

从数据内容来看,四种毒菌按危险程度从高到低排列,颜色也从鲜红色逐渐过渡到橙黄色再到土棕色。这种设计既符合用户的认知习惯(红色 = 危险),又通过颜色渐变体现了危险等级的差异。pct 字段虽然名为百分比,但实际上是一个相对危险指数,用于可视化展示和直观对比。

毒性预警功能的设计体现了野生菌电商平台的社会责任意识。在推广野生菌消费的同时,平台也承担着安全教育的义务。通过清晰的危险等级展示、详细的毒性说明和应急处理指南,帮助用户建立正确的食菌安全意识。这不仅是功能设计的考量,也是品牌信任度建设的重要一环。

3.6 全局模拟数据:从菌种到菌友的完整数据生态

const shroomList: ShroomItem[] = [
  new ShroomItem(1, '松茸', 'Tricholoma matsutake', '香格里拉·3300m', '7-10月', '无毒·珍稀', '¥580/500g', '🍄'),
  new ShroomItem(2, '鸡枞菌', 'Termitomyces', '云南·蚁巢共生', '6-10月', '无毒·鲜甜', '¥128/500g', '🍄'),
  new ShroomItem(3, '牛肝菌', 'Boletus', '楚雄·松林地', '6-10月', '需熟食', '¥88/500g', '🍄‍🟫'),
  new ShroomItem(4, '干巴菌', 'Thelephora ganbajun', '易门·云南松林', '7-9月', '无毒·浓香', '¥498/500g', '🍄'),
  new ShroomItem(5, '见手青', 'Lanmaoa asiatica', '昆明·红土地', '6-9月', '需熟透', '¥158/500g', '🍄‍🟫'),
  new ShroomItem(6, '青头菌', 'Russula virescens', '大理·栎树林', '6-10月', '无毒', '¥68/500g', '🍄'),
  new ShroomItem(7, '竹荪', 'Dictyophora', '昭通·竹林', '5-8月', '无毒', '¥218/500g', '🎍'),
  new ShroomItem(8, '羊肚菌', 'Morchella', '会泽·高海拔', '3-5月', '无毒', '¥688/500g', '🍄‍🟫')
];

全局模拟数据是应用开发初期的重要基础设施。这段代码定义了 8 种云南常见食用菌的详细信息,涵盖了从高端的松茸、羊肚菌到亲民的青头菌、牛肝菌等多个品种。每种菌子都包含了拉丁学名,体现了内容的专业性;产地信息精确到具体地理位置和海拔高度,增加了真实感;毒性标识分为"无毒"、“无毒·珍稀”、“无毒·鲜甜”、“无毒·浓香”、“需熟食”、"需熟透"等多个等级,比简单的二元分类更加精细。

价格体系也值得关注,从 ¥68/500g 的青头菌到 ¥688/500g 的羊肚菌,价格跨度达到 10 倍,覆盖了不同消费层级的用户需求。这种价格梯度设计是电商运营的常见策略,既提供了引流款的低价商品,也有利润款的高端商品。

const friendList: ShroomFriendItem[] = [
  new ShroomFriendItem(1, '菌向导老普', '🧭', '易门老黑山昨夜一场透雨,今早松针下牛肝菌出得跟种的一样,带团十人全采满筐。', 389, 31, '菌情播报', '1小时前'),
  new ShroomFriendItem(2, '菌嫂阿芝', '🧺', '南华五街的松茸窝今天出了 42 朵,菌帽紧实未开伞,今晚下单明天一早冷链发出。', 342, 26, '山货上新', '3小时前'),
  new ShroomFriendItem(3, '急诊科李医生', '🏥', '又接诊一家三口误食火炭菌,亚稀褶红菇没有民间说的"生蛆无毒"规律,别再信偏方。', 456, 52, '毒菌警示', '5小时前'),
  // ... 更多动态
];

菌友圈的模拟数据设计尤为用心,创建了多个具有鲜明人设的账号:菌向导、菌嫂、急诊科医生、真菌学博士生、藏族向导、菌农、新手、市场管理员等。每个人的发言内容都与其身份高度契合,形成了一个生态丰富的虚拟社区。这种数据设计不仅是为了展示 UI,更是在构建产品的内容生态和社区氛围。

从内容角度看,这些动态涵盖了菌情播报、山货上新、毒菌警示、科普知识、高原菌事、年景预测、翻车现场、市场行情等多个维度,信息密度很高。对于真实产品而言,这些内容类型也为社区运营指明了方向——专业人士贡献权威内容,普通用户分享个人经历,形成内容的层次感和多样性。

3.7 导航常量定义:两级导航体系的配置化

interface NavItem {
  icon: string;
  label: string;
}

const SUB_NAV_LIST: NavItem[] = [
  { icon: '✨', label: '精选' },
  { icon: '🍄', label: '菌谱图鉴' },
  { icon: '⚠️', label: '毒性预警' },
  { icon: '🌧️', label: '菌季指数' },
  { icon: '🧭', label: '采菌向导' },
  { icon: '👥', label: '菌友圈' }
];

const NAV_LIST: NavItem[] = [
  { icon: '🍄', label: '首页' },
  { icon: '⛰️', label: '菌山' },
  { icon: '🛒', label: '山货' },
  { icon: '🧑‍🌾', label: '我的' }
];

导航系统是应用信息架构的骨架。本应用采用两级导航体系:底部主导航(4 个 Tab)和首页内的次级导航(6 个 Tab)。两级导航都通过 NavItem 接口统一定义,包含 icon(图标)和 label(文字标签)两个属性。使用 emoji 作为图标是一种轻量且有趣的做法,无需额外的图标资源文件,同时保持了跨平台的一致性。

底部主导航 NAV_LIST 的 4 个 Tab 对应了应用的四大功能模块:首页(内容聚合)、菌山(产地信息)、山货(商品交易)、我的(个人中心)。这是一个典型的电商 + 内容混合应用的导航结构——首页承载内容运营,山货承担交易转化,我的负责用户管理。

次级导航 SUB_NAV_LIST 的 6 个 Tab 则构成了首页的内容矩阵:精选是运营聚合页,菌谱图鉴是知识库,毒性预警是安全科普,菌季指数是数据看板,采菌向导是服务预订,菌友圈是社区互动。这种内容布局覆盖了用户从认知(了解菌子)到决策(看菌季、选线路)再到社交(分享交流)的完整路径。

将导航项配置化为常量数组有诸多好处:一是便于维护和修改,调整导航顺序或增减项目只需修改数组即可;二是可以与 ForEach 配合使用,实现数据驱动的 UI 渲染;三是类型安全,NavItem 接口确保了每个导航项都包含必要的字段。这种配置化的思想在中大型应用中非常重要,能够有效降低代码的耦合度。

3.8 顶层纯函数:工具方法的功能化组织

function rainBarH(val: number): string {
  return (val * 0.84).toString() + '%';
}

function toxColor(t: string): string {
  if (t === '无毒' || t === '无毒·珍稀' || t === '无毒·鲜甜' || t === '无毒·浓香') { return COLORS.moss; }
  if (t === '需熟食' || t === '需熟透') { return COLORS.amber; }
  return COLORS.red;
}

顶层纯函数是 ArkTS 开发中常见的代码组织方式。这些函数不依赖任何组件状态,只根据输入参数计算返回值,具有良好的可测试性和可复用性。将它们定义在组件外部,可以被多个 Builder 方法共享使用,避免重复代码。

rainBarH 函数用于计算雨量柱状图的柱体高度,它将原始数值乘以 0.84 的系数后转换为百分比字符串。这个 0.84 系数是一个视觉调节参数,用于控制柱体的最大高度,防止柱子过高超出容器范围。这种比例换算在数据可视化中非常常见,是数据域到视觉域的映射。

toxColor 函数根据毒性等级返回对应的颜色值。函数采用多条件分支的方式,将毒性分为三个等级:无毒类(返回苔藓绿)、需熟食/需熟透类(返回琥珀黄)、其他(返回警示红)。这种颜色编码方式直观易懂,用户可以通过颜色快速判断菌子的安全等级。值得注意的是,函数对"无毒"的多种变体都做了匹配,体现了对数据多样性的容错处理。

function sporeX(seed: number, tick: number): number {
  return ((seed * 59 + tick * 4) % 93) + 4;
}

function sporeY(seed: number, tick: number): number {
  return ((seed * 37 + tick * 8) % 88) + 6;
}

function shroomScale(glow: boolean): number {
  return glow ? 1.15 : 0.9;
}

孢子动画相关的三个函数是特效层的计算核心。sporeXsporeY 函数通过种子值(seed)和时间刻度(tick)计算每个孢子的位置,使用了乘法、加法和取模运算的组合来生成伪随机的运动轨迹。种子值确保每个孢子有不同的初始位置和运动速度,时间刻度则驱动位置随时间变化。取模运算将位置限制在一定范围内(X 方向 4-97,Y 方向 6-94),确保孢子始终在可视区域内。

shroomScale 函数根据 glow 布尔值返回缩放比例——呼吸效果的"吸气"状态放大到 1.15 倍,"呼气"状态缩小到 0.9 倍。这种在 1.0 上下波动的缩放方式模拟了生物体呼吸的视觉效果,比简单的显隐动画更加生动自然。

function goodsIconBg(seed: number): string {
  const arr: string[] = ['#31422F', '#2B3B2E', '#354733', '#2E3F2C'];
  return arr[seed % 4];
}

goodsIconBg 函数用于为商品图标背景色提供变化,通过索引取模的方式从 4 种深绿色调中循环选取。这种微小的视觉变化能够打破完全一致带来的单调感,同时又保持了整体色调的统一性。这是 UI 设计中"统一中有变化"原则的具体体现。

这些顶层纯函数的共同特点是:输入明确、输出可预测、无副作用。这种函数式的设计风格使得代码更加清晰易懂,也便于单元测试。在实际项目中,可以将这些工具函数抽取到独立的工具模块中,进一步提升代码的组织性和可维护性。

3.9 主组件与状态定义:页面的核心数据结构

@Entry
@Component
struct Page {
  @State mainTab: number = 0;
  @State subTab: number = 0;
  @State tick: number = 0;
  @State glow: boolean = false;
  // 弹框状态
  @State addModal: boolean = false;
  @State editModal: boolean = false;
  @State delModal: boolean = false;
  @State editIdx: number = -1;
  @State delTarget: string = 'goods';
  @State editHill: string = '';
  @State editDate: string = '';
  @State addNick: string = '';
  @State addText: string = '';
  // ...
}

Page 结构体是应用的入口组件,使用 @Entry@Component 双重装饰器标记。@Entry 表示这是页面的根组件,负责承载整个页面的生命周期;@Component 则表示这是一个可复用的 UI 组件。在 ArkUI 中,每个页面都有且仅有一个 @Entry 组件。

组件的状态变量全部使用 @State 装饰器定义,这些状态变量是驱动 UI 渲染的数据源。当 @State 变量的值发生变化时,框架会自动重新渲染依赖该变量的 UI 部分。状态变量可以分为以下几类:

导航状态mainTabsubTab 分别控制底部主导航和首页次级导航的选中状态,都是数字类型,默认值为 0(第一个 Tab)。通过改变这两个变量的值,可以实现页面间的切换。

动画状态tick 是动画的时间刻度,每帧递增,用于驱动孢子飘散的位置计算;glow 是呼吸效果的开关状态,在 true 和 false 之间切换,控制蘑菇的缩放和孢子的透明度。这两个状态由定时器周期性更新。

弹框状态addModaleditModaldelModal 三个布尔变量分别控制三种弹框的显示与隐藏。editIdx 记录当前编辑或删除的列表项索引,delTarget 标记删除操作的目标类型(采菌计划/山货/菌友动态),editHilleditDate 是编辑弹框的表单数据,addNickaddText 是发布动态弹框的表单数据。

这种将所有状态集中定义在组件顶部的方式,使得组件的数据流非常清晰。开发者可以一眼看出这个组件管理了哪些状态,从而更好地理解组件的行为。同时,每个状态变量的职责单一,避免了一个状态变量承载过多含义的情况。

3.10 列表状态与定时器:数据与特效的存储

  @State forages: ForageItem[] = [
    new ForageItem(1, '易门·老黑山松林线', '本周六 06:30', '¥268/人', '余 5/10', '菌向导老普', '含竹筐+手套+鉴定', '报名中'),
    // ... 更多采菌计划
  ];
  @State goods: GoodsItem[] = [
    new GoodsItem(1, '香格里拉鲜松茸', '鲜菌', '¥580/500g', '云南·迪庆', '500g·顺丰冷链', 32, '🔥', '🍄'),
    // ... 更多山货
  ];
  @State friends: ShroomFriendItem[] = [
    new ShroomFriendItem(1, '菌向导老普', '🧭', '易门老黑山昨夜一场透雨...', 389, 31, '菌情播报', '1小时前'),
    // ... 更多菌友动态
  ];
  fxTimer: number = -1;

列表数据是应用内容的主要载体,三个列表变量分别存储了采菌计划、山货商品和菌友动态的数据。这些列表使用 @State 装饰器管理,当列表内容发生变化(增加、删除、修改)时,对应的列表 UI 会自动更新。

值得注意的是,虽然数据模型类使用了 @Observed 装饰器,但列表本身是通过 @State 管理的。在 ArkUI 中,当对数组执行 pushsplice 等方法时,框架能够检测到数组内容的变化并触发更新。但如果只是修改数组中某个对象的属性,则需要 @Observed 的配合才能被检测到。本项目中同时使用了这两种修改方式——增删操作通过数组方法触发,编辑操作则通过修改对象属性触发。

fxTimer 变量用于存储特效定时器的 ID,类型为 number,默认值为 -1(表示定时器未启动)。这个变量没有使用 @State 装饰器,因为它只是一个内部句柄,不参与 UI 渲染,不需要响应式追踪。这种区分是状态管理中的重要实践——只有真正影响 UI 的数据才需要用 @State 管理,纯粹的内部变量保持普通属性即可,可以减少不必要的追踪开销。

3.11 生命周期管理:定时器的创建与销毁

  aboutToAppear(): void {
    this.fxTimer = setInterval(() => {
      this.tick = this.tick + 1;
      this.glow = !this.glow;
    }, 660);
  }

  aboutToDisappear(): void {
    clearInterval(this.fxTimer);
  }

生命周期方法是组件管理副作用的关键钩子。aboutToAppear 在组件即将渲染前调用,是初始化资源、启动动画、加载数据的最佳时机;aboutToDisappear 在组件即将销毁前调用,用于清理资源、取消订阅、停止动画等。

aboutToAppear 中,代码启动了一个 setInterval 定时器,间隔为 660 毫秒。定时器的回调函数做了两件事:一是将 tick 加 1,驱动孢子飘散的位置计算;二是切换 glow 的布尔值,控制蘑菇呼吸缩放的节奏。660 毫秒的间隔是一个经过调试的视觉参数——太快会显得急促,太慢又缺乏动感,这个节奏恰好模拟了自然呼吸的频率。

aboutToDisappear 中调用 clearInterval 清理定时器,这是一个非常重要的资源管理实践。如果不及时清理定时器,组件销毁后定时器仍会在后台运行,导致内存泄漏和性能损耗。在 ArkUI 中,@State 变量的变更会触发视图更新,但如果组件已经销毁,这些更新就是无效的,甚至可能导致异常。因此,配对使用 aboutToAppearaboutToDisappear 来管理定时器、事件监听等资源是必不可少的。

这种基于 setInterval 的动画驱动方式属于帧动画的一种变体——不是按屏幕刷新率逐帧更新,而是以固定的时间间隔更新状态。相比使用 animateTo 的属性动画,这种方式更适合需要精确控制节奏且计算逻辑较复杂的场景。孢子飘散的位置计算涉及伪随机函数,蘑菇呼吸则是离散的两态切换,用定时器驱动更加直接可控。

3.12 弹框工具方法:状态驱动的交互逻辑

  openAdd(): void {
    this.addNick = '';
    this.addText = '';
    this.addModal = true;
    this.editModal = false;
    this.delModal = false;
  }

  openEdit(idx: number): void {
    this.editIdx = idx;
    this.editHill = this.forages[idx].hill;
    this.editDate = this.forages[idx].date;
    this.addModal = false;
    this.editModal = true;
    this.delModal = false;
  }

弹框工具方法是连接用户交互与状态变化的桥梁。每个打开弹框的方法都遵循相似的模式:设置相关的表单数据、将目标弹框的状态设为 true、将其他弹框的状态设为 false。这种显式关闭其他弹框的做法确保了同一时间只有一个弹框显示,避免了弹框堆叠的混乱。

openAdd 方法用于打开发布菌友动态的弹框。在打开之前,它会先清空 addNickaddText 两个表单字段,确保每次打开都是空白的初始状态。这是表单交互的基本设计原则——每次打开新表单都应该是干净的初始状态,避免残留上一次的输入内容。

openEdit 方法用于打开编辑采菌计划的弹框。与新增不同,编辑操作需要先加载现有数据。方法接收一个索引参数 idx,将其保存到 editIdx 中供后续保存使用,然后从 forages 数组中取出对应项的 hilldate 属性,赋值给表单状态变量 editHilleditDate。这种先加载再编辑的模式是 CRUD 操作中 Update 的标准实现方式。

  openDelForage(idx: number): void {
    this.delTarget = 'forage';
    this.editIdx = idx;
    this.addModal = false;
    this.editModal = false;
    this.delModal = true;
  }

  openDelGoods(idx: number): void {
    this.delTarget = 'goods';
    this.editIdx = idx;
    this.addModal = false;
    this.editModal = false;
    this.delModal = true;
  }

删除操作有两个入口方法:openDelForage 用于删除采菌计划,openDelGoods 用于下架山货商品。它们共享同一个删除确认弹框 delModal,通过 delTarget 字段区分操作类型。这种设计复用了弹框的 UI 结构和确认逻辑,减少了代码重复。

两个方法的实现模式完全一致:设置目标类型、保存索引、关闭其他弹框、打开删除弹框。这种统一的模式使得代码具有良好的可读性和可维护性,新增其他删除场景时也可以轻松套用相同的模式。

  doAdd(): void {
    if (this.addText.length > 0) {
      const n = this.friends.length + 1;
      this.friends.push(new ShroomFriendItem(n,
        this.addNick.length > 0 ? this.addNick : '新菌友', '🍄', this.addText,
        0, 0, '新动态', '刚刚'));
      this.addModal = false;
    }
  }

doAdd 方法处理发布动态的确认操作。它首先进行表单验证——检查正文内容 addText 是否非空,这是最基本的校验逻辑。昵称可以为空,此时使用默认值"新菌友"。验证通过后,创建一个新的 ShroomFriendItem 实例并 push 到 friends 数组中,然后关闭弹框。

新动态的 ID 简单地使用数组长度 + 1,这在前端模拟数据中是常见的做法。点赞数和回复数初始化为 0,标签设为"新动态",时间显示为"刚刚"。这些细节都符合真实社交应用的用户预期。

  doEdit(): void {
    if (this.editIdx >= 0 && this.editIdx < this.forages.length) {
      this.forages[this.editIdx].hill = this.editHill;
      this.forages[this.editIdx].date = this.editDate;
      this.editModal = false;
    }
  }

  doDel(): void {
    if (this.delTarget === 'forage') {
      if (this.editIdx >= 0 && this.editIdx < this.forages.length) {
        this.forages.splice(this.editIdx, 1);
        this.delModal = false;
      }
    } else {
      if (this.editIdx >= 0 && this.editIdx < this.goods.length) {
        this.goods.splice(this.editIdx, 1);
        this.delModal = false;
      }
    }
  }

doEditdoDel 分别处理编辑和删除的确认操作。doEdit 通过索引定位到具体的采菌计划项,直接修改其 hilldate 属性。由于 ForageItem 类使用了 @Observed 装饰器,属性修改会被框架追踪到,从而触发对应列表项的重新渲染。

doDel 方法根据 delTarget 的值执行不同的删除逻辑:如果是采菌计划,则从 forages 数组中删除;如果是山货商品,则从 goods 数组中删除。使用 splice 方法删除数组元素,框架能够检测到数组内容的变化并更新列表。方法中包含了索引边界检查,防止数组越界错误,这是健壮代码的基本要求。

这些弹框工具方法整体呈现出清晰的 CQRS(命令查询职责分离)风格——打开弹框的方法是命令,执行操作的方法也是命令,它们都通过修改状态来间接影响 UI。这种模式的好处是逻辑集中、流程清晰,所有的状态变更都在这些方法中完成,便于追踪和调试。

3.13 特效层 Builder:孢子飘散与蘑菇呼吸的实现

  @Builder
  fxLayer() {
    Stack({ alignContent: Alignment.TopStart }) {
      ForEach([0, 1, 2], (s: number) => {
        Column() {
          Text('🍄')
            .fontSize(14 + s * 5)
            .scale({ x: shroomScale(this.glow), y: shroomScale(this.glow) })
            .opacity(0.85)
        }
        .position({ x: 8 + s * 36, y: 66 + s * 12 })
      }, (s: number) => s.toString())
      // ... 孢子粒子
    }
    .width('100%')
    .height('100%')
    .clip(true)
  }

特效层是应用视觉表现的亮点之一,使用 @Builder 装饰器封装为独立的构建方法。@Builder 是 ArkUI 提供的 UI 复用机制,它可以将一段 UI 逻辑封装为可调用的方法,在 build() 中通过 this.xxx() 的方式调用。与自定义组件相比,@Builder 更轻量,适合组件内部的 UI 片段复用。

特效层使用 Stack 布局作为容器,所有特效元素都通过绝对定位放置。StackalignContent 设为 TopStart,表示子元素默认从左上角开始定位。每个子元素再通过 .position() 方法设置具体的偏移量。

蘑菇呼吸效果由 3 个大小递增的蘑菇 emoji 组成,通过 ForEach 遍历 [0, 1, 2] 数组生成。fontSize 随索引递增(14 + s * 5),形成近大远小的层次感。position 也随索引偏移,模拟了蘑菇散布在不同位置的效果。核心的呼吸动画通过 scale 属性实现,调用 shroomScale(this.glow) 函数根据 glow 状态返回不同的缩放比例。当 glow 为 true 时放大到 1.15 倍,为 false 时缩小到 0.9 倍,配合定时器的周期性切换,形成了呼吸般的缩放效果。

      ForEach([3, 4, 5, 6, 7, 8, 9, 10, 11, 12], (s: number) => {
        Column() {
          Text('·')
            .fontSize(s % 3 === 0 ? 12 : 8)
            .fontColor(s % 3 === 0 ? COLORS.lime : COLORS.spore)
            .opacity(this.glow ? 0.9 : 0.2)
        }
        .position({ x: sporeX(s, this.tick), y: sporeY(s + 2, this.tick) })
      }, (s: number) => s.toString())

孢子飘散效果由 10 个粒子组成,使用点号(·)作为孢子的视觉符号。粒子的大小和颜色有细微差异——每 3 个中有一个更大更亮(酸橙绿,12px),其余较小较暗(孢子白,8px)。这种差异增加了粒子系统的层次感和真实感。

位置计算是孢子动画的核心。每个孢子的 X 坐标通过 sporeX(s, this.tick) 计算,Y 坐标通过 sporeY(s + 2, this.tick) 计算。种子参数 ss + 2 的差异使得 X 和 Y 使用不同的种子值,避免了运动轨迹的线性相关。this.tick 随时间递增,驱动位置不断变化,形成飘散的动态效果。

孢子的透明度也与 glow 状态关联——glow 为 true 时透明度为 0.9(清晰可见),为 false 时为 0.2(若隐若现)。这使得孢子的明暗变化与蘑菇的呼吸节奏同步,形成了统一的视觉韵律。

特效层的容器设置了 .clip(true),确保超出容器范围的粒子被裁剪掉,不会溢出到其他区域。这是 Stack + 绝对定位布局中的常见做法,可以防止特效元素影响页面其他部分的布局和交互。

3.14 头部 Builder:山货电商风格的信息聚合

  @Builder
  header() {
    Column({ space: 10 }) {
      Row({ space: 8 }) {
        Column() { Text('📍').fontSize(15) }
        .width(32).height(32).borderRadius(16).backgroundColor(COLORS.card)
        .justifyContent(FlexAlign.Center)
        Column({ space: 2 }) {
          Text('昆明·木水花菌市').fontSize(11).fontColor(COLORS.sub)
          Text('阵雨 21°C · 菌子大年·出菌旺盛').fontSize(9).fontColor(COLORS.lime)
        }
        .alignItems(HorizontalAlign.Start)
        Column().layoutWeight(1)
        Row({ space: 6 }) {
          Column() { Text('🔍').fontSize(14) }
          .width(30).height(30).borderRadius(15).backgroundColor(COLORS.card)
          .justifyContent(FlexAlign.Center)
          Column() { Text('🛒').fontSize(14) }
          .width(30).height(30).borderRadius(15).backgroundColor(COLORS.card)
          .justifyContent(FlexAlign.Center)
        }
      }
      .width('100%')

头部区域是应用信息密度最高的区域之一,采用 Column 垂直布局,包含了定位信息栏、今日菌情大卡和热门菌种横滚三个主要部分。

定位信息栏采用 Row 水平布局,从左到右依次是定位图标、位置与天气信息、搜索和购物车图标。定位图标是一个圆形的卡片容器,使用 borderRadius(16) 实现圆形效果(32x32 的尺寸,圆角 16 即为正圆)。位置信息分为两行,主行显示地点,次行显示天气和菌情概要,使用不同的字号和颜色形成视觉层级。

中间的空白区域通过 Column().layoutWeight(1) 实现弹性占位,将左右两侧的内容推开。这是弹性布局中的常见技巧——用一个空的、权重为 1 的元素来填充剩余空间。

搜索和购物车图标同样是圆形卡片设计,与定位图标形成视觉呼应。两个图标之间有 6px 的间距,整体构成了功能操作区。这种"左定位 + 中间信息 + 右操作"的头部布局是移动应用的经典模式,符合用户的使用习惯。

      // 今日菌情大卡
      Row({ space: 12 }) {
        Column({ space: 3 }) {
          Text('今日市场进场量').fontSize(10).fontColor(COLORS.spore)
          Text('486').fontSize(30).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
          Text('吨 · 松茸特级缺口').fontSize(9).fontColor(COLORS.spore)
        }
        .alignItems(HorizontalAlign.Start)
        Column().layoutWeight(1)
        Column({ space: 5 }) {
          Row({ space: 4 }) {
            ForEach([0, 1, 2, 3, 4, 5], (b: number) => {
              Column()
                .width(9)
                .height(b < 4 ? 8 + b * 7 : 0)
                .borderRadius(4)
                .backgroundColor(b < 3 ? COLORS.lime : COLORS.moss)
            }, (b: number) => b.toString())
          }
          .alignItems(VerticalAlign.Bottom)
          Text('出菌指数 · 极旺').fontSize(8).fontColor(COLORS.spore)
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')
      .padding(14)
      .borderRadius(16)
      .backgroundColor(COLORS.forestDeep)
      .linearGradient({
        angle: 135,
        colors: [['#35542F', 0], ['#4E7A46', 1]]
      })

今日菌情大卡是头部最醒目的元素,使用深森林绿渐变背景,营造出数据看板的专业感。卡片内部采用左右分栏布局:左侧展示市场进场量的核心数据,右侧展示出菌指数的可视化图表。

左侧数据区采用"标签 + 大数字 + 补充说明"的三级文字结构。数字"486"使用 30px 的大字号和粗体,成为视觉焦点。吨数和缺口信息作为补充说明,使用较小的字号。这种数据展示方式能够让用户在一瞥之间获取最关键的信息。

右侧是一个简化的柱状图,使用 6 个 Column 元素模拟柱状。前 4 根柱子有高度(0-3 号递增),后 2 根高度为 0(不显示)。柱子的颜色也有区分——前 3 根是酸橙绿(代表已达到),第 4 根是苔藓绿(代表当前水平)。柱状图下方还有"出菌指数 · 极旺"的文字标签,明确图表含义。

渐变背景通过 linearGradient 实现,角度为 135 度(从左上到右下),从深森林绿过渡到森林绿。这种渐变效果增加了卡片的深度感和质感,比纯色背景更加生动。

      // 热门菌种横滚
      Scroll() {
        Row({ space: 10 }) {
          ForEach(shroomList, (s: ShroomItem) => {
            Column({ space: 4 }) {
              Text(s.icon).fontSize(18)
              Text(s.name).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
              Text(s.price).fontSize(8).fontColor(COLORS.lime)
            }
            .width(88).padding({ top: 8, bottom: 8 }).borderRadius(10)
            .backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
          }, (s: ShroomItem) => s.id.toString())
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .height(72)

热门菌种横滚是头部的第三个模块,使用 Scroll 组件实现横向滚动的菌种列表。每个菌种项是一个固定宽度(88px)的卡片,包含图标、名称和价格三行信息。卡片使用 COLORS.card 背景色,与头部整体的深色调协调一致。

横向滚动通过 .scrollable(ScrollDirection.Horizontal) 设置,.scrollBar(BarState.Off) 隐藏了滚动条,使界面更加简洁。固定高度 72px 确保了滚动区域的稳定性。这种横向滚动的热门分类/商品入口是电商应用的常见模式,能够在有限的空间内展示更多选择,同时引导用户进入详细页面。

头部整体使用 COLORS.bg 背景色,内边距设置为左右 12px、上下 8px,布局紧凑而不拥挤。头部作为应用的门面,其设计质量直接影响用户的第一印象。本应用的头部设计在信息密度和视觉舒适度之间取得了良好的平衡。

3.15 内容 Tab 导航:六宫格横滚的次级导航

在这里插入图片描述

  @Builder
  subNav() {
    Scroll() {
      Row({ space: 14 }) {
        ForEach(SUB_NAV_LIST, (ni: NavItem, i: number) => {
          Column({ space: 4 }) {
            Text(ni.icon).fontSize(16)
              .opacity(this.subTab === i ? 1 : 0.5)
            Text(ni.label)
              .fontSize(9)
              .fontColor(this.subTab === i ? COLORS.lime : COLORS.sub)
              .fontWeight(this.subTab === i ? FontWeight.Bold : FontWeight.Normal)
          }
          .padding({ left: 12, right: 12, top: 8, bottom: 8 })
          .borderRadius(14)
          .backgroundColor(this.subTab === i ? COLORS.card2 : COLORS.card)
          .alignItems(HorizontalAlign.Center)
          .onClick(() => { this.subTab = i; })
        }, (ni: NavItem) => ni.label)
      }
      .padding({ left: 12, right: 12 })
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
    .width('100%')
  }

次级导航栏是首页内容切换的核心控件,承载了 6 个内容 Tab 的切换功能。由于 Tab 数量较多(6 个),采用横向滚动的方式展示,用户可以左右滑动查看全部 Tab。

每个导航项是一个垂直排列的 Column,包含图标和文字两部分。选中态和未选中态有明显的视觉区分:选中时图标不透明(opacity: 1),文字为酸橙绿且加粗,背景为更亮的 card2 色;未选中时图标半透明(opacity: 0.5),文字为灰绿色且常规字重,背景为 card 色。这种多维度的状态区分(透明度 + 颜色 + 字重 + 背景色)确保了选中状态的清晰可辨。

导航项的点击事件通过 .onClick(() => { this.subTab = i; }) 实现,点击后将 subTab 状态变量设为对应的索引值。由于 subTab@State 变量,赋值后框架会自动重新渲染导航栏,更新选中态样式,同时触发内容区的 Tab 切换。

外层 Scroll 组件提供横向滚动能力,隐藏滚动条保持视觉简洁。内部 Row 的左右各有 12px 的内边距,与页面整体的边距系统保持一致。导航项之间有 14px 的间距,既保证了触控区域的舒适度,又能在一屏内展示尽可能多的 Tab。

这种数据驱动的导航栏实现方式非常优雅——导航项的数量、顺序、图标、文字都由 SUB_NAV_LIST 常量决定,UI 渲染通过 ForEach 自动生成。如果需要调整导航,只需修改数据数组即可,无需改动 UI 逻辑。这是声明式开发的典型优势:数据决定 UI,逻辑专注于状态。

3.16 Tab0 精选页:运营聚合的内容矩阵

  @Builder
  tabPick() {
    Scroll() {
      Column({ space: 10 }) {
        // 毒菌警示横幅(置顶)
        Row({ space: 10 }) {
          Text('⚠️').fontSize(18)
          Column({ space: 2 }) {
            Text('毒菌预警:白毒伞已致 2 起中毒').fontSize(11).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
            Text('头上戴帽·腰间系裙·脚上穿靴 全占的别吃').fontSize(8).fontColor('#F5D9D9')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('100%').padding(12).borderRadius(12)
        .backgroundColor(COLORS.red)

精选页是首页的默认 Tab,也是内容最丰富的页面。它采用垂直滚动的单列布局,从上到下依次排列了毒菌警示横幅、运营 Banner 横滚、菌山数据卡、双列菌种瀑布等多个模块,形成了一个运营聚合的内容矩阵。

毒菌警示横幅置顶显示,使用醒目的红色背景,体现了安全第一的设计理念。横幅左侧是警示图标,右侧是标题和说明文字,采用左图右文的经典信息布局。标题"毒菌预警:白毒伞已致 2 起中毒"直接点明事件和风险,副标题则给出了识毒口诀"头上戴帽·腰间系裙·脚上穿靴 全占的别吃",简洁易记。这种将重要安全信息放在最显眼位置的做法,体现了平台的社会责任感。

        Scroll() {
          Row({ space: 10 }) {
            ForEach([0, 1, 2], (b: number) => {
              Column({ space: 6 }) {
                Text(b === 0 ? '🍄 菌子季鲜直达' : (b === 1 ? '⛰️ 跟菌农上山' : '📖 识菌必修课'))
                  .fontSize(16).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
                Text(b === 0 ? '凌晨采 冷链 48h 到家' : (b === 1 ? '松茸窝点直采体验' : '毒菌图鉴免费学'))
                  .fontSize(9).fontColor(COLORS.spore)
                Row({ space: 4 }) {
                  Text(b === 0 ? '去抢鲜' : (b === 1 ? '去报名' : '去学习')).fontSize(9)
                    .fontColor(COLORS.forestDeep).padding({ left: 10, right: 10, top: 4, bottom: 4 })
                    .borderRadius(10)
                    .backgroundColor(COLORS.white)
                }
              }
              .width(200).padding(14).borderRadius(14).alignItems(HorizontalAlign.Start)
              .backgroundColor(b === 0 ? COLORS.forest : (b === 1 ? COLORS.terra : COLORS.sky))
            }, (b: number) => b.toString())
          }
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')

运营 Banner 横滚是电商应用的标准配置,用于展示核心运营活动。三个 Banner 分别对应"鲜菌直达"、“跟团上山”、"识菌学习"三个核心业务方向,每个 Banner 有不同的背景色(森林绿、土壤棕、天空蓝),形成了丰富的视觉节奏。

每个 Banner 采用左对齐的文字布局,包含大标题、副标题和行动按钮三层信息。按钮使用白色背景 + 深森林绿文字,在彩色背景上非常醒目。Banner 的固定宽度为 200px,高度自适应内容,通过横向滚动展示更多选项。

这种 Banner 设计有几个值得注意的细节:一是每个 Banner 对应不同的业务方向且颜色各异,形成了视觉区分;二是按钮文案各不相同(去抢鲜、去报名、去学习),与业务场景精准匹配;三是整体信息层级清晰,用户可以快速理解每个 Banner 的主题和价值。

        Row({ space: 10 }) {
          Column({ space: 6 }) {
            Text('🌧️').fontSize(26)
            Text('出菌条件').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text('极旺 · 昨夜透雨').fontSize(9).fontColor(COLORS.lime)
            Text('今明两日为窗口期').fontSize(8).fontColor(COLORS.sub)
          }
          .layoutWeight(1).padding(12).borderRadius(12)
          .backgroundColor(COLORS.card).alignItems(HorizontalAlign.Start)

          Column({ space: 6 }) {
            Text('🚑').fontSize(26)
            Text('中毒救助').fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text('误食立即催吐就医').fontSize(9).fontColor(COLORS.red)
            Text('保留样品供鉴定').fontSize(8).fontColor(COLORS.sub)
          }
          .layoutWeight(1).padding(12).borderRadius(12)
          .backgroundColor(COLORS.card).alignItems(HorizontalAlign.Start)
        }
        .width('100%')

"今日菌山"部分包含两个并排的信息卡片:出菌条件和中毒救助。两张卡片使用相同的布局结构(图标 + 标题 + 状态 + 说明),通过 .layoutWeight(1) 平分宽度,形成对称的双列布局。

出菌条件卡片使用绿色状态文字,传递正面信息(极旺);中毒救助卡片使用红色状态文字,传递警示信息。这种颜色编码与整体的语义色彩系统保持一致。卡片左上角的大 emoji 图标(26px)增加了视觉趣味性,也帮助用户快速识别卡片主题。

        // 双列菌种瀑布
        ForEach([0, 2, 4, 6], (r: number) => {
          Row({ space: 10 }) {
            ForEach([r, r + 1], (si: number) => {
              if (si < shroomList.length) {
                Column({ space: 6 }) {
                  Column() { Text(shroomList[si].icon).fontSize(28) }
                  .width(52).height(52).borderRadius(26)
                  .backgroundColor(goodsIconBg(si))
                  .justifyContent(FlexAlign.Center)
                  Text(shroomList[si].name).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                  Text(shroomList[si].latin).fontSize(7).fontColor(COLORS.sub)
                  Text(shroomList[si].season + ' · ' + shroomList[si].region).fontSize(8).fontColor(COLORS.sub)
                  Row({ space: 4 }) {
                    Text(shroomList[si].toxicity).fontSize(8).fontColor(toxColor(shroomList[si].toxicity))
                      .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6)
                      .backgroundColor(COLORS.card2)
                    Text(shroomList[si].price).fontSize(9).fontColor(COLORS.lime).fontWeight(FontWeight.Bold)
                  }
                }
                .layoutWeight(1).padding(12).borderRadius(12)
                .backgroundColor(COLORS.card)
                .alignItems(HorizontalAlign.Center)
              }
            }, (si: number) => si.toString())
          }
          .width('100%')
          .alignItems(VerticalAlign.Top)
        }, (r: number) => 'row' + r.toString())

双列菌种瀑布是精选页的主要内容区,以两列瀑布流的形式展示菌种卡片。实现方式是通过外层 ForEach 遍历行索引(0, 2, 4, 6),内层 ForEach 遍历每行的两个元素(r 和 r+1),根据索引从 shroomList 中取出对应的菌种数据。

每个菌种卡片是一个垂直居中的 Column,包含图标圆形背景、名称、拉丁学名、季节产地、毒性标签和价格等信息。图标背景使用 goodsIconBg(si) 函数选取不同的深绿色调,增加了视觉变化。毒性标签使用 toxColor 函数动态计算颜色,价格则统一使用酸橙绿粗体突出显示。

卡片的信息层级非常清晰:图标是视觉焦点,名称是主要信息,拉丁学名和产地是辅助信息,毒性和价格是行动相关信息。这种信息组织方式符合用户浏览商品的认知习惯——先看外观,再看名称,了解详情,最后关注安全和价格。

双列瀑布流的实现方式虽然简单(固定每行两个),但在菌种卡片高度一致的情况下效果很好。如果未来卡片高度差异较大,可以考虑使用更复杂的瀑布流布局方案。当前的实现简洁高效,完全满足展示需求。

3.17 Tab1 菌谱图鉴:编号列表的知识库呈现

  @Builder
  tabShroom() {
    Scroll() {
      Column({ space: 10 }) {
        Text('云南常见食用菌 TOP8').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
        ForEach(shroomList, (s: ShroomItem, i: number) => {
          Row({ space: 12 }) {
            Column() {
              Text(s.icon).fontSize(20)
            }
            .width(46).height(46).borderRadius(23)
            .border({ width: 2, color: i < 3 ? COLORS.lime : COLORS.line })
            .backgroundColor(COLORS.card2)
            .justifyContent(FlexAlign.Center)

菌谱图鉴页是一个菌种知识库,以列表形式展示云南常见的 8 种食用菌。页面顶部有一个标题"云南常见食用菌 TOP8",明确了内容主题和数量。

每个菌种项采用左图右文的水平布局。左侧是一个圆形图标容器,直径 46px,边框 2px。有趣的是,前 3 名(i < 3)的边框使用酸橙绿色,其余使用分割线色。这种设计巧妙地用边框颜色区分了 TOP3 和其他名次,为榜单增加了排名的仪式感。图标背景使用 card2 色,比列表项背景稍亮,形成了层次感。

            Column({ space: 3 }) {
              Row({ space: 6 }) {
                Text(s.name).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                Text('No.' + (i + 1).toString()).fontSize(7).fontColor(COLORS.lime)
                  .padding({ left: 5, right: 5, top: 1, bottom: 1 }).borderRadius(6)
                  .backgroundColor(COLORS.card2)
              }
              Text(s.latin).fontSize(7).fontColor(COLORS.sub)
              Row({ space: 6 }) {
                Text(s.region).fontSize(8).fontColor(COLORS.sub)
                Text(s.season).fontSize(8).fontColor(COLORS.sky)
              }
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Column({ space: 3 }) {
              Text(s.toxicity).fontSize(9).fontColor(toxColor(s.toxicity)).fontWeight(FontWeight.Bold)
              Text(s.price).fontSize(9).fontColor(COLORS.lime)
            }
            .alignItems(HorizontalAlign.End)

中间内容区是菌种的主要信息,分为三行:第一行是菌名 + 编号标签,第二行是拉丁学名,第三行是产地 + 季节。编号标签"No.X"使用小字号的酸橙绿文字,背景为 card2 色,形成一个小巧的编号徽章。这种编号设计增强了榜单的排名感,也方便用户快速定位和讨论。

拉丁学名使用 7px 的极小字号和次要文字色,作为专业信息的补充展示。虽然字号很小,但对于有专业需求的用户来说,这些信息是有价值的。产地和季节并排显示,产地用灰色,季节用天空蓝,颜色区分有助于快速识别不同类型的信息。

右侧是毒性和价格信息,采用右对齐布局。毒性标签使用 toxColor 函数动态着色,价格使用酸橙绿。这种将安全信息和价格信息并列展示的设计,符合用户的决策逻辑——先确认安全性,再看价格是否合适。

          }
          .width('100%').padding(12).borderRadius(12)
          .backgroundColor(COLORS.card)
          .border({ width: i < 3 ? 1 : 0, color: COLORS.moss })
        }, (s: ShroomItem) => s.id.toString())

列表项的卡片设计还有一个细节:前 3 名的卡片有 1px 的苔藓绿边框,其他卡片没有边框。这与左侧图标的边框颜色设计相呼应,强化了 TOP3 的视觉区分。这种多层次的强调(图标边框 + 卡片边框)使得排名差异更加明显,也增加了榜单的趣味性。

菌谱图鉴页整体呈现出清晰的信息层级和专业的知识库气质。与精选页的瀑布流不同,图鉴页使用标准列表形式,便于用户逐条浏览和对比。每种菌的信息展示全面而有序,既有通俗的中文名称,也有专业的拉丁学名;既有产地季节等实用信息,也有毒性和价格等决策信息。

3.18 Tab2 毒性预警:安全科普的视觉化表达

在这里插入图片描述

  @Builder
  tabToxic() {
    Scroll() {
      Column({ space: 12 }) {
        Column({ space: 8 }) {
          Text('☠️ 云南毒菌杀手榜 ☠️').fontSize(15).fontColor(COLORS.white).fontWeight(FontWeight.Bold)
          Text('以下菌种每年占致死病例 90% 以上').fontSize(10).fontColor('#F5D9D9')
          Text('口诀:头上戴帽、腰间系裙、脚上穿靴——同时具备的菌子绝对不碰').fontSize(9).fontColor(COLORS.spore)
        }
        .width('100%').padding(16).borderRadius(14)
        .backgroundColor(COLORS.red)
        .alignItems(HorizontalAlign.Center)

毒性预警页是应用中安全科普功能的核心页面,以醒目的红色为主色调,营造出警示氛围。页面顶部是一个全宽的红色大横幅,标题"云南毒菌杀手榜"使用骷髅头 emoji 装饰,强化了危险警示的视觉冲击力。

横幅包含三层信息:主标题点明主题,副标题说明数据依据(“每年占致死病例 90% 以上”),第三行给出识毒口诀(“头上戴帽、腰间系裙、脚上穿靴”)。这个口诀是民间总结的毒菌识别经验——鹅膏属毒菌通常具有菌盖(帽)、菌环(裙)、菌托(靴)三个特征。将专业知识提炼为朗朗上口的口诀,是科普内容的常用技巧,便于用户记忆和传播。

        ForEach(toxicList, (t: ToxicItem, i: number) => {
          Column({ space: 6 }) {
            Row({ space: 8 }) {
              Text('☠️').fontSize(14)
              Text(t.label).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
              Column().layoutWeight(1)
              Text('危险 ' + t.pct.toString()).fontSize(10).fontColor(t.color).fontWeight(FontWeight.Bold)
            }
            .width('100%')
            Row() {
              Column().height(8).borderRadius(4).backgroundColor(t.color).width(t.pct.toString() + '%')
            }
            .width('100%')
            Text(i === 0 ? '看似无菌环菌托的"白蘑菇",一朵可致成人肝衰竭,生长于壳斗科林下。'
              : (i === 1 ? '民间称火炭菌,与可食稀褶红菇极难区分,误食致横纹肌溶解。'
                : (i === 2 ? '常被误认作鸡枞,夏秋季腐木上群生,含致命鹅膏毒素。'
                  : '最常见中毒菌种,误食后胃肠炎症状剧烈,城市绿地也常见。'))).fontSize(8).fontColor(COLORS.sub).width('100%').lineHeight(14)
          }
          .width('100%').padding(12).borderRadius(12)
          .backgroundColor(COLORS.card)
        }, (t: ToxicItem) => t.label)

毒菌列表是页面的核心内容,每种毒菌的卡片包含危险图标、菌名、危险指数、进度条和详细说明。危险指数使用百分比数值 + 进度条的可视化方式,直观展示每种毒菌的危险程度。进度条的颜色与危险等级对应——最危险的鲜红色,依次过渡到橙色、黄色、棕色。

每种毒菌的说明文字都不同,针对该菌种的特点进行描述:白毒伞强调"一朵可致肝衰竭"的致命性和"看似普通白蘑菇"的迷惑性;火炭菌强调"极难区分"和"横纹肌溶解"的严重后果;条盖盔孢伞强调"被误认作鸡枞"的常见误区;大青褶伞则强调"最常见"和"城市绿地也有"的普遍性。这种针对性的描述比泛泛而谈更有教育意义。

说明文字设置了 lineHeight(14),对于 8px 的字号来说,行高约为字号的 1.75 倍,保证了多行文字的可读性。在小字号文本中,合适的行高尤为重要,可以避免文字过于拥挤而难以阅读。

        Text('误食应急四步').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
        Row({ space: 8 }) {
          Column({ space: 4 }) {
            Text('1️⃣').fontSize(20)
            Text('立即催吐').fontSize(10).fontColor(COLORS.title)
            Text('淡盐水刺激咽后壁').fontSize(8).fontColor(COLORS.sub)
          }
          .layoutWeight(1).padding(10).borderRadius(10).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
          // ... 共4步
        }
        .width('100%')

页面底部是"误食应急四步"指南,以四列并排的卡片形式展示。每步包含序号 emoji、步骤名称和操作说明三个层次。四个步骤分别是:立即催吐、保留样品、紧急就医、补液护肝。这四步涵盖了从现场急救到医疗救治的完整流程,具有很强的实用价值。

四列等宽布局通过 .layoutWeight(1) 实现,每张卡片宽度相同,信息密度均匀。序号 emoji 使用 20px 的大字号,增强了步骤的序列感。这种网格布局的信息展示方式,比纯文字列表更加直观,也更便于用户快速扫描和记忆。

毒性预警页整体设计兼顾了警示性和实用性——红色主题唤起用户的安全意识,详细的毒菌信息帮助用户识别危险,应急指南则提供了实际的行动指导。这种"警示 + 知识 + 行动"的内容结构,是安全教育类页面的经典范式。

3.19 Tab3 菌季指数:数据可视化的季节看板

  @Builder
  tabSeason() {
    Scroll() {
      Column({ space: 12 }) {
        Text('月度出菌指数(雨量相关)').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
        Row({ space: 10 }) {
          ForEach(rainCharts, (c: RainChartItem) => {
            Column({ space: 4 }) {
              Column()
                .width(22)
                .height(rainBarH(c.val))
                .borderRadius(6)
                .backgroundColor(c.color)
              Text(c.week).fontSize(8).fontColor(COLORS.sub)
            }
            .alignItems(HorizontalAlign.Center)
          }, (c: RainChartItem) => c.week)
        }
        .width('100%')
        .height(150)
        .padding(12)
        .borderRadius(12)
        .backgroundColor(COLORS.card)
        .alignItems(VerticalAlign.Bottom)

菌季指数页是一个数据驱动的季节看板,通过图表和列表展示出菌的季节规律和当前状态。页面顶部是一个月度雨量柱状图,展示 7 月至 12 月的出菌指数变化。图表的实现非常巧妙——完全使用基础组件 Column 和百分比高度来模拟柱状图,无需引入额外的图表库。

柱状图的容器固定高度为 150px,内部通过 alignItems(VerticalAlign.Bottom) 让柱子从底部向上生长。每个柱子的宽度为 22px,高度通过 rainBarH(c.val) 函数计算,将数值转换为百分比高度。柱子底部是月份标签,使用次要文字色,不喧宾夺主。

柱子的颜色设计是图表的亮点:7 月是天空蓝(雨季开始),8 月是森林绿,9 月是苔藓绿(旺季高峰),10 月是酸橙绿,11 月是琥珀黄,12 月是土壤棕(衰退期)。这种颜色随季节变化的设计不仅美观,更传递了丰富的语义信息——绿色系代表出菌旺盛期,黄棕色代表衰退期,蓝色代表初期。用户仅凭颜色就能大致判断出菌的季节规律。

        Text('本周采菌窗口期').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
        ForEach([['易门·老黑山', '极旺', '昨夜透雨·松针下大量牛肝菌', '96'], ['南华·五街', '旺盛', '松茸窗口期仅剩 48 小时', '88'], ['楚雄·紫溪山', '旺盛', '牛肝菌提前 12 天上市', '82'], ['香格里拉·吉沙', '中', '海拔高·出菌慢但品质佳', '64']], (w: string[]) => {
          Column({ space: 6 }) {
            Row({ space: 8 }) {
              Text('⛰️').fontSize(14)
              Text(w[0]).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Medium)
              Text(w[1]).fontSize(8).fontColor(COLORS.lime)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(6)
                .backgroundColor(COLORS.card2)
              Column().layoutWeight(1)
              Text(w[3] + '%').fontSize(10).fontColor(COLORS.lime).fontWeight(FontWeight.Bold)
            }
            .width('100%')
            Row() {
              Column().height(8).borderRadius(4).backgroundColor(COLORS.moss).width(w[3] + '%')
            }
            .width('100%')
            Text(w[2]).fontSize(8).fontColor(COLORS.sub).width('100%')
          }
          .width('100%').padding(12).borderRadius(12)
          .backgroundColor(COLORS.card2)
        }, (w: string[]) => w[0])

本周采菌窗口期列表是页面的第二个模块,展示各主要菌山的当前出菌状态。每条数据使用二维字符串数组存储,包含菌山名称、旺盛程度、详细说明和出菌指数四个字段。使用数组而非对象的方式简化了数据定义,适合这种临时的、结构简单的数据展示场景。

每条记录的布局与毒菌列表类似:顶部一行包含图标、名称、状态标签和指数值,中间是进度条,底部是详细说明。进度条统一使用苔藓绿色,因为这些都是可采菌的山,指数越高代表出菌越旺盛。状态标签使用酸橙绿文字,标注"极旺"、“旺盛”、"中"等等级。

卡片背景使用 card2 色,比页面背景稍亮,形成了"卡片中的卡片"的层次感。这种嵌套的视觉层级有助于区分不同模块的内容,也让页面结构更加清晰。

        Text('选菌三看').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
        Row({ space: 8 }) {
          Column({ space: 4 }) {
            Text('🔎').fontSize(20)
            Text('看菌褶').fontSize(10).fontColor(COLORS.title)
            Text('无绿色孢子印').fontSize(8).fontColor(COLORS.sub)
          }
          .layoutWeight(1).padding(10).borderRadius(10).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
          // ... 共3看
        }
        .width('100%')

页面底部是"选菌三看"实用指南,以三列卡片的形式展示选菌的三个要点:看菌褶、闻气味、试手感。每点都有图标、标题和说明,结构简洁清晰。这种知识卡片式的展示方式,便于用户快速掌握选菌技巧,也体现了平台的专业性和实用性。

菌季指数页整体呈现出"数据 + 列表 + 知识"的三层结构:月度图表展示宏观规律,窗口列表展示当前状态,选菌指南提供实用知识。这种从宏观到微观、从数据到行动的内容组织方式,符合用户获取信息的认知路径。

3.20 Tab4 采菌向导:时间轴式的跟团计划

  @Builder
  tabForage() {
    Scroll() {
      Column({ space: 0 }) {
        Text('跟团采菌计划').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%').padding({ bottom: 10 })
        ForEach(this.forages, (f: ForageItem, i: number) => {
          Row({ space: 10 }) {
            Column({ space: 4 }) {
              Text(f.date.length > 5 ? f.date.slice(0, 5) : f.date).fontSize(10).fontColor(COLORS.lime).fontWeight(FontWeight.Bold)
              Text('第' + (i + 1).toString() + '期').fontSize(8).fontColor(COLORS.sub)
              Column().width(2).layoutWeight(1).backgroundColor(COLORS.line).margin({ top: 4 })
            }
            .width(64)
            .constraintSize({ minHeight: 118 })
            .alignItems(HorizontalAlign.Center)

采菌向导页以时间轴的形式展示跟团采菌计划,是 OTA 服务预订功能的核心页面。页面标题为"跟团采菌计划",明确了内容主题。列表采用左侧时间轴 + 右侧内容卡片的经典时间轴布局。

左侧时间轴列固定宽度 64px,包含日期、期数和连接线三部分。日期只取前 5 个字符(如"本周六"变为"本周六",“下周五 06:00"变为"下周五”),保持时间轴的简洁性。日期下方是"第 X 期"的期数标签,再下方是一条垂直线,通过 .layoutWeight(1) 填充剩余空间,形成连接各期的时间轴线。

.constraintSize({ minHeight: 118 }) 设置了时间轴列的最小高度,确保即使右侧内容较少,时间轴线也有足够的长度,保持视觉的连续性。这是时间轴布局中的关键技巧——通过最小高度保证轴线的连接效果。

            Row({ space: 10 }) {
              Column({ space: 4 }) {
                Row({ space: 6 }) {
                  Text(f.hill).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                  Text(f.status).fontSize(8)
                    .fontColor(forageStatusColor(f.status))
                    .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                    .borderRadius(6)
                    .backgroundColor(COLORS.card2)
                }
                Text('须知:' + f.note).fontSize(8).fontColor(COLORS.sub)
                Row({ space: 8 }) {
                  Text('💰 ' + f.price).fontSize(10).fontColor(COLORS.lime).fontWeight(FontWeight.Bold)
                  Text('🎫 ' + f.quota).fontSize(8).fontColor(COLORS.sky)
                  Text('🧭 ' + f.guide).fontSize(8).fontColor(COLORS.sub)
                }
                Row({ space: 8 }) {
                  Button().width(64).height(26).borderRadius(13).backgroundColor(COLORS.card2)
                    .onClick(() => { this.openEdit(i); })
                  Text('调整').fontSize(9).fontColor(COLORS.sub).margin({ left: -52 })
                  Button().width(64).height(26).borderRadius(13).backgroundColor(COLORS.card2)
                    .onClick(() => { this.openDelForage(i); })
                  Text('取消').fontSize(9).fontColor(COLORS.red).margin({ left: -52 })
                }
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
            }
            .layoutWeight(1)
            .padding(12)
            .borderRadius(12)
            .backgroundColor(COLORS.card)

右侧内容卡片包含线路名称、状态标签、须知说明、价格名额向导信息,以及调整和取消两个操作按钮。状态标签的颜色通过 forageStatusColor 函数动态计算——热抢/即将满员为红色,报名中为苔藓绿,预告为天空蓝,其他为灰色。这种颜色编码让用户可以快速识别每条线路的报名状态。

价格信息使用酸橙绿粗体突出显示,是卡片中最醒目的元素之一。名额信息使用天空蓝,向导信息使用灰色,三者通过 emoji 前缀和颜色差异形成清晰的信息区分。

操作按钮的实现方式比较特别——使用空的 Button 组件作为可点击区域,再用 Text 组件通过负边距(margin({ left: -52 }))覆盖在按钮上方显示文字。这是一种在 ArkUI 中自定义按钮样式的技巧——Button 提供点击交互和背景,Text 提供文字内容。调整按钮文字为灰色,取消按钮文字为红色,符合常见的操作习惯。

          }
          .width('100%')
          .alignItems(VerticalAlign.Top)
        }, (f: ForageItem) => f.id.toString())
      }
      .width('100%')
      .padding(12)
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .width('100%')
    .height('100%')
  }

整行使用 alignItems(VerticalAlign.Top) 让时间轴列和内容卡片都顶部对齐,确保时间点与卡片顶部对应。外层 Columnspace 设为 0,因为卡片之间的间距已经通过卡片内边距和时间轴线的视觉连接来处理,不需要额外的间距。

采菌向导页的时间轴设计是页面的一大特色,它将时间维度可视化,让用户可以直观地看到各期采菌计划的时间顺序和当前状态。对于采菌这种时效性很强的活动来说,时间轴的呈现方式比普通列表更加贴切。

3.21 Tab5 菌友圈:社交动态的信息流

  @Builder
  tabFriends() {
    Scroll() {
      Column({ space: 10 }) {
        Row({ space: 8 }) {
          Button().width(110).height(32).borderRadius(16).backgroundColor(COLORS.forest)
            .onClick(() => { this.openAdd(); })
          Text('📝 发布动态').fontSize(10).fontColor(COLORS.white).margin({ left: -102 })
          Column().layoutWeight(1)
          Text('按最新排序').fontSize(9).fontColor(COLORS.sub)
        }
        .width('100%')

菌友圈页是应用的社区功能模块,以信息流的形式展示菌友们发布的动态。页面顶部是操作栏,左侧是"发布动态"按钮,右侧是排序说明"按最新排序"。发布按钮使用森林绿背景 + 白色文字,在深色背景上非常醒目,鼓励用户参与内容创作。

发布按钮的实现同样采用了"Button + Text 叠加"的模式——Button 提供点击区域和背景,Text 提供图标和文字。负边距 margin({ left: -102 }) 将文字定位到按钮上方。这种方式虽然看起来有些"技巧化",但在需要自定义按钮内部布局时是一种实用的方案。

        ForEach(this.friends, (f: ShroomFriendItem) => {
          Column({ space: 8 }) {
            Row({ space: 10 }) {
              Column() { Text(f.avatar).fontSize(24) }
              .width(44).height(44).borderRadius(12)
              .backgroundColor(COLORS.card2)
              .justifyContent(FlexAlign.Center)
              Column({ space: 2 }) {
                Text(f.nick).fontSize(11).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                Text(f.time + ' · ' + f.tag).fontSize(8).fontColor(COLORS.sub)
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Text('🪪 实名菌友').fontSize(7).fontColor(COLORS.moss)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .borderRadius(8).backgroundColor(COLORS.card2)
            }
            .width('100%')

每条动态的头部是发布者信息行,包含头像、昵称、时间标签和实名认证标识。头像使用 44x44 的圆角方形(圆角 12px),比正圆头像更有特色。昵称使用主标题色 + 粗体,时间和标签用次要色,形成清晰的视觉层级。

"实名菌友"标签是一个值得注意的设计细节——它使用苔藓绿色 + 证件 emoji,传递了"真实可信"的信号。在野生菌这样一个专业度和安全性都很重要的领域,实名认证能够显著提升内容的可信度和社区的整体质量。虽然这只是一个 UI 展示,但它反映了产品对社区信任度建设的重视。

            Text(f.text).fontSize(10).fontColor(COLORS.sub).width('100%').lineHeight(16)
            Row({ space: 16 }) {
              Text('❤️ ' + f.likes.toString()).fontSize(9).fontColor(COLORS.lime)
              Text('💬 ' + f.replies.toString()).fontSize(9).fontColor(COLORS.sub)
              Text('⤴ 分享').fontSize(9).fontColor(COLORS.sub)
              Column().layoutWeight(1)
              Button().width(48).height(20).borderRadius(10).backgroundColor(COLORS.card2)
                .onClick(() => { this.openDelGoods(-1); this.editIdx = f.id - 1; this.delTarget = 'friend'; })
              Text('删').fontSize(8).fontColor(COLORS.red).margin({ left: -38 })
            }
            .width('100%')

动态正文使用 10px 字号和 16px 行高,行高约为字号的 1.6 倍,保证了多行文字的舒适度。文字颜色使用次要色,避免与头部信息产生视觉竞争。

底部互动栏包含点赞、评论、分享三个操作,以及一个删除按钮。点赞数使用酸橙绿,评论和分享使用灰色,突出点赞的交互优先级。删除按钮使用红色文字,是管理功能的入口。值得注意的是,删除操作复用了山货下架的弹框方法 openDelGoods,但在调用前手动修改了 delTargeteditIdx。这种复用方式虽然节省了代码,但从设计角度看,为菌友圈动态单独创建一个删除方法会更清晰。

菌友圈页的整体设计遵循了社交信息流的经典模式——头部信息 + 正文内容 + 底部互动。这种模式经过了大量产品的验证,用户学习成本低,使用效率高。在此基础上,结合野生菌领域的特色(实名标识、专业标签等),形成了差异化的社区体验。

3.22 主 Tab 菌山页与山货页:产地与交易的双核心

  @Builder
  pageHill() {
    Scroll() {
      Column({ space: 12 }) {
        Text('⛰️ 云南菌山地图').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
        ForEach([['易门·老黑山', '牛肝菌·干巴菌', '极旺', '¥268'], ['南华·五街', '松茸·鸡枞', '旺盛', '¥298'], ['楚雄·紫溪山', '牛肝菌·青头菌', '旺盛', '¥238'], ['香格里拉·吉沙', '松茸·出口级', '中', '¥588'], ['会泽·大海草山', '羊肚菌·春季', '预告', '¥398'], ['富民·西山', '鸡枞·松露', '旺盛', '¥188']], (h: string[]) => {
          Column({ space: 8 }) {
            Row({ space: 10 }) {
              Column() { Text('⛰️').fontSize(24) }
              .width(48).height(48).borderRadius(12)
              .backgroundColor(COLORS.forestDeep)
              .justifyContent(FlexAlign.Center)
              Column({ space: 3 }) {
                Text(h[0]).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
                Text('主产:' + h[1]).fontSize(9).fontColor(COLORS.sub)
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Column({ space: 3 }) {
                Text(h[2]).fontSize(10).fontColor(COLORS.lime).fontWeight(FontWeight.Bold)
                Text('跟团 ' + h[3]).fontSize(8).fontColor(COLORS.sky)
              }
              .alignItems(HorizontalAlign.End)
            }
            .width('100%')
          }
          .width('100%').padding(14).borderRadius(14)
          .backgroundColor(COLORS.card)
        }, (h: string[]) => h[0])

菌山页是底部第二个主 Tab,展示云南主要产菌山区的信息。页面标题为"云南菌山地图",使用山 emoji 作为图标。每座菌山的信息卡片采用左中右三栏布局:左侧是山形图标(深森林绿背景),中间是山名和主产菌种,右侧是出菌状态和跟团价格。

数据使用二维字符串数组存储,每座山包含 4 个信息字段。这种数据结构简洁直观,适合展示型列表。图标背景使用 COLORS.forestDeep,比卡片背景更深,形成了"暗底亮图"的效果,让图标更加突出。

右侧的状态信息使用酸橙绿粗体显示旺盛程度,天空蓝小字显示跟团价格。价格前加上"跟团"前缀,明确了价格的含义,避免用户误解为门票或其他费用。

菌山页的定位是产地信息聚合页,让用户了解云南主要的产菌地区及其特色菌种。对于想要深度参与采菌活动的用户来说,这个页面提供了选择目的地的参考依据。

  @Builder
  pageGoods() {
    Scroll() {
      Column({ space: 12 }) {
        Text('🛒 菌子山货商城').fontSize(15).fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
        ForEach(this.goods, (g: GoodsItem) => {
          Row({ space: 12 }) {
            Column() { Text(g.icon).fontSize(26) }
            .width(56).height(56).borderRadius(14)
            .backgroundColor(COLORS.card2)
            .justifyContent(FlexAlign.Center)
            Column({ space: 4 }) {
              Text(g.name).fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
              Text(g.origin + ' · ' + g.unit).fontSize(8).fontColor(COLORS.sub)
              Row({ space: 6 }) {
                Text(g.cat).fontSize(8).fontColor(COLORS.lime)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .borderRadius(6).backgroundColor(COLORS.card2)
                Text('库存 ' + g.stock.toString()).fontSize(8).fontColor(COLORS.sky)
                Text(g.hot).fontSize(9)
              }
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Column({ space: 4 }) {
              Text(g.price).fontSize(13).fontColor(COLORS.lime).fontWeight(FontWeight.Bold)
              Button().width(52).height(22).borderRadius(11).backgroundColor(COLORS.card2)
                .onClick(() => { this.openDelGoods(this.goods.indexOf(g)); })
              Text('下架').fontSize(8).fontColor(COLORS.red).margin({ top: -18 })
            }
            .alignItems(HorizontalAlign.End)

山货页是底部第三个主 Tab,也是电商交易的核心页面。页面标题为"菌子山货商城",使用购物车 emoji。商品列表采用左图右文的标准电商列表布局,每行展示一个商品。

商品图标区域为 56x56 的圆角方形,使用 card2 色背景。中间内容区从上到下依次是商品名称(粗体主色)、产地规格(次要色)、分类标签 + 库存 + 热度标识。分类标签使用酸橙绿文字 + card2 背景,库存使用天空蓝,热度使用火焰 emoji。三个信息使用不同的颜色和形式,形成了丰富的信息层次。

右侧是价格和操作区,价格使用 13px 酸橙绿粗体,是列表中最醒目的元素。下方是下架按钮,使用红色文字,表明这是一个管理端视角的商品列表(可以下架商品)。在真实的消费者端应用中,这个位置通常会是"加入购物车"或"立即购买"按钮。

山货页的整体设计符合电商列表页的标准模式——商品图、名称、规格、价格、操作按钮,信息排列有序,用户可以快速浏览和比较商品。多种标签(分类、库存、热度)的运用则丰富了列表的信息维度,帮助用户做出更全面的购买决策。

3.23 我的页:个人中心的数据化呈现

  @Builder
  pageMine() {
    Scroll() {
      Column({ space: 12 }) {
        Row({ space: 12 }) {
          Column() { Text('🧑‍🌾').fontSize(30) }
          .width(64).height(64).borderRadius(18)
          .backgroundColor(COLORS.forestDeep)
          .justifyContent(FlexAlign.Center)
          Column({ space: 4 }) {
            Text('拾菌人·小敏').fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
            Text('识菌学员 · 上山 16 次').fontSize(9).fontColor(COLORS.sub)
            Text('菌龄 2 年 · 零误食记录').fontSize(8).fontColor(COLORS.lime)
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('100%').padding(16).borderRadius(16)
        .backgroundColor(COLORS.card)

我的页是底部第四个主 Tab,即个人中心页面。页面顶部是用户信息卡片,采用左头像右信息的布局。头像区域为 64x64 的圆角方形(圆角 18px),深森林绿背景,农民 emoji 作为头像。这种大圆角的头像设计比正圆更柔和,也更符合整体的圆角设计语言。

用户信息有三行:昵称(“拾菌人·小敏”,使用花名式的昵称,增加了趣味性)、身份和上山次数、菌龄和安全记录。第三行"零误食记录"使用酸橙绿色,是一个亮点设计——它将安全记录作为个人成就来展示,既鼓励了用户的安全行为,也强化了平台重视安全的品牌形象。

        Row({ space: 10 }) {
          Column({ space: 3 }) {
            Text('16').fontSize(18).fontColor(COLORS.lime).fontWeight(FontWeight.Bold)
            Text('上山次数').fontSize(8).fontColor(COLORS.sub)
          }
          .layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
          Column({ space: 3 }) {
            Text('23').fontSize(18).fontColor(COLORS.sky).fontWeight(FontWeight.Bold)
            Text('识别菌种').fontSize(8).fontColor(COLORS.sub)
          }
          .layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
          Column({ space: 3 }) {
            Text('4').fontSize(18).fontColor(COLORS.moss).fontWeight(FontWeight.Bold)
            Text('跟团记录').fontSize(8).fontColor(COLORS.sub)
          }
          .layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
          Column({ space: 3 }) {
            Text('86').fontSize(18).fontColor(COLORS.amber).fontWeight(FontWeight.Bold)
            Text('圈内积分').fontSize(8).fontColor(COLORS.sub)
          }
          .layoutWeight(1).padding(12).borderRadius(12).backgroundColor(COLORS.card).alignItems(HorizontalAlign.Center)
        }
        .width('100%')

用户数据统计区是个人中心的核心展示模块,以四列等宽的卡片形式展示四个关键数据:上山次数(酸橙绿)、识别菌种(天空蓝)、跟团记录(苔藓绿)、圈内积分(琥珀黄)。每个数据使用不同的颜色编码,既丰富了视觉效果,也帮助用户快速区分不同维度的数据。

这种数据化的个人中心设计是游戏化思维在产品中的应用——将用户的行为转化为可量化的数字成就,激发用户的成就感和参与动力。对于采菌平台来说,上山次数、识别菌种数等数据既是用户的使用记录,也是用户专业度的体现,能够有效提升用户粘性。

        ForEach(['我的跟团', '采菌日记', '识别记录', '常购山货', '急救手册', '设置'], (s: string) => {
          Row({ space: 10 }) {
            Text('›').fontSize(16).fontColor(COLORS.moss)
            Text(s).fontSize(11).fontColor(COLORS.title)
            Column().layoutWeight(1)
            Text('>').fontSize(12).fontColor(COLORS.sub)
          }
          .width('100%').padding(14).borderRadius(12)
          .backgroundColor(COLORS.card)
        }, (s: string) => s)

功能菜单列表是个人中心的标准配置,包含 6 个菜单项:我的跟团、采菌日记、识别记录、常购山货、急救手册、设置。每个菜单项采用左箭头 + 文字 + 右箭头的经典列表样式。左侧的 “›” 符号使用苔藓绿色,作为每个菜单项的视觉引导;右侧的 “>” 符号使用灰色,表示可进入下一级。

这 6 个菜单项覆盖了用户的主要需求:跟团管理(我的跟团)、内容创作(采菌日记)、学习记录(识别记录)、消费管理(常购山货)、安全保障(急救手册)、系统设置(设置)。功能布局合理,优先级清晰。

我的页整体呈现出"用户信息 + 数据统计 + 功能菜单"的三层结构,是个人中心页面的经典范式。数据统计区的游戏化设计和"零误食记录"的安全成就展示,是本页面的两个亮点,体现了产品对用户成长和安全的重视。

3.24 弹框系统:遮罩与三种弹框的条件渲染

  @Builder
  modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor(COLORS.mask)
      .onClick(() => { onClose(); })
  }

弹框系统是应用交互的重要组成部分,采用遮罩层 + 弹框体的经典结构。modalOverlay 是遮罩层的构建方法,它接收一个 onClose 回调函数作为参数,点击遮罩时调用该回调关闭弹框。这种参数化的设计使得遮罩层可以被不同的弹框复用,同时保持关闭逻辑的灵活性。

遮罩层使用一个全屏的 Column 组件,背景色为半透明的 COLORS.mask(深墨绿色,76% 不透明度)。点击遮罩层关闭弹框是移动端的标准交互模式,用户已经形成了使用习惯。遮罩层的另一个重要作用是阻挡对底层内容的交互,确保弹框显示时用户只能与弹框内容交互。

  @Builder
  addModalBody() {
    Column({ space: 12 }) {
      Text('发布菌友动态').fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
      TextInput({ placeholder: '昵称(可留空)', text: this.addNick })
        .fontSize(11).height(38).borderRadius(10)
        .backgroundColor(COLORS.card2).fontColor(COLORS.title)
        .onChange((v: string) => { this.addNick = v; })
      TextInput({ placeholder: '分享你的上山战果或识菌心得…', text: this.addText })
        .fontSize(11).height(60).borderRadius(10)
        .backgroundColor(COLORS.card2).fontColor(COLORS.title)
        .onChange((v: string) => { this.addText = v; })

发布动态弹框包含标题、昵称输入框、正文输入框和操作按钮四个部分。标题使用 14px 粗体,是弹框的视觉焦点。昵称输入框高度 38px,为单行输入;正文输入框高度 60px,为多行输入。两个输入框都使用 card2 色背景,圆角 10px,与整体设计语言一致。

TextInput 组件通过 text 属性与状态变量双向绑定——this.addNickthis.addText 分别存储昵称和正文内容。onChange 回调在输入内容变化时更新状态变量,确保 UI 显示与数据状态的同步。这种受控组件的模式是声明式 UI 中表单处理的标准做法。

输入框的 placeholder 文案也值得关注——昵称框标注"可留空",降低了用户的发布门槛;正文框给出了内容方向提示"分享你的上山战果或识菌心得…",引导用户发布相关内容。好的 placeholder 文案既是提示,也是引导。

      Row({ space: 10 }) {
        Button().layoutWeight(1).height(36).borderRadius(18).backgroundColor(COLORS.card2)
          .onClick(() => { this.addModal = false; })
        Text('取消').fontSize(11).fontColor(COLORS.sub).margin({ left: -66 })
        Button().layoutWeight(1).height(36).borderRadius(18).backgroundColor(COLORS.forest)
          .onClick(() => { this.doAdd(); })
        Text('发布').fontSize(11).fontColor(COLORS.white).fontWeight(FontWeight.Bold).margin({ left: -66 })
      }
      .width('100%')
    }
    .width('86%')
    .padding(18)
    .borderRadius(16)
    .backgroundColor(COLORS.card)
  }

操作按钮区采用左右双按钮布局,两个按钮平分宽度(各占 layoutWeight(1))。取消按钮使用 card2 背景 + 次要色文字,发布按钮使用森林绿背景 + 白色粗体文字。这种"次按钮 + 主按钮"的视觉区分,引导用户关注主要操作,同时保留取消选项。

弹框体的宽度设为 86%,在屏幕两侧留出一定边距,避免弹框过于压迫。内边距 18px,圆角 16px,背景为 card 色。这些尺寸参数共同构成了弹框的视觉规格,与页面中卡片的设计语言保持一致。

  @Builder
  editModalBody() {
    Column({ space: 12 }) {
      Text('调整采菌计划').fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
      TextInput({ placeholder: '菌山线路名称', text: this.editHill })
        .fontSize(11).height(38).borderRadius(10)
        .backgroundColor(COLORS.card2).fontColor(COLORS.title)
        .onChange((v: string) => { this.editHill = v; })
      TextInput({ placeholder: '出行时间 如 本周六 06:30', text: this.editDate })
        .fontSize(11).height(38).borderRadius(10)
        .backgroundColor(COLORS.card2).fontColor(COLORS.title)
        .onChange((v: string) => { this.editDate = v; })
      Text('提示:出菌窗口期多变,出发前请再次确认菌情').fontSize(8).fontColor(COLORS.sub).width('100%')

编辑采菌计划弹框与发布动态弹框结构类似,但内容更简洁——只有线路名称和出行时间两个输入字段。两个输入框高度相同(38px),都是单行输入。

弹框中增加了一行提示文字:“出菌窗口期多变,出发前请再次确认菌情”。这行提示使用 8px 小字和次要色,不会干扰主要操作,但又能在用户修改计划时提醒其注意出菌条件的变化。这种微提示设计体现了对用户场景的深入理解——采菌计划受天气影响大,调整时需要特别注意。

  @Builder
  delModalBody() {
    Column({ space: 12 }) {
      Text('确认操作').fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
      Text(this.delTarget === 'forage' ? '取消跟团后竹筐与手套配额将释放,雨天顺延优先权作废,确认吗?' : '下架该山货后将从商城列表移除,冷链库存照常保留,确认继续吗?')
        .fontSize(10).fontColor(COLORS.sub).textAlign(TextAlign.Center)

删除确认弹框是三种弹框中最简单的一种,只有标题、确认文字和操作按钮。确认文字根据 delTarget 的不同而变化——删除采菌计划时,提示"竹筐与手套配额将释放,雨天顺延优先权作废";下架山货时,提示"从商城列表移除,冷链库存照常保留"。

这种差异化的确认文案设计非常重要。好的删除确认不应该只是简单的"确认删除吗?",而应该告诉用户删除操作的具体后果,帮助用户做出明智的决策。特别是对于采菌跟团这样的服务型商品,取消可能涉及配额、优先权等隐性成本,明确告知这些后果可以减少用户的误操作和后续纠纷。

确认按钮使用红色背景 + 白色文字,与删除操作的危险性相匹配。取消按钮的文案为"再想想",比简单的"取消"更加人性化,也给了用户一个心理缓冲的台阶。

3.25 底部导航栏:四 Tab 的主框架

  @Builder
  bottomBar() {
    Row({ space: 0 }) {
      ForEach(NAV_LIST, (ni: NavItem, i: number) => {
        Column({ space: 3 }) {
          Text(ni.icon).fontSize(18)
          Text(ni.label)
            .fontSize(9)
            .fontColor(this.mainTab === i ? COLORS.lime : COLORS.sub)
            .fontWeight(this.mainTab === i ? FontWeight.Bold : FontWeight.Normal)
        }
        .layoutWeight(1)
        .padding({ top: 6, bottom: 6 })
        .alignItems(HorizontalAlign.Center)
        .onClick(() => { this.mainTab = i; this.subTab = 0; })
      }, (ni: NavItem) => ni.label)
    }
    .width('100%')
    .backgroundColor(COLORS.card)
    .border({ width: 1, color: COLORS.line })
  }

底部导航栏是应用的主导航框架,使用 @Builder 封装为独立的构建方法。导航栏采用 Row 水平布局,4 个 Tab 项通过 ForEach 遍历 NAV_LIST 数组生成,每个 Tab 平分宽度(layoutWeight(1))。

每个 Tab 项是一个垂直排列的 Column,包含图标和文字两部分。选中态和未选中态通过文字颜色和字重区分——选中时文字为酸橙绿且加粗,未选中时为灰绿色且常规字重。图标没有使用颜色区分,保持了 emoji 的原始外观。这种只有文字变化的状态切换简洁明了,不会因为图标颜色变化而分散用户注意力。

点击事件的处理有一个细节:this.mainTab = i; this.subTab = 0;。切换主 Tab 时,同时将次级 Tab 重置为 0(精选页)。这确保了用户每次回到首页时都看到默认的精选内容,避免了次级 Tab 状态残留带来的困惑。这种状态联动是多 Tab 导航中的常见设计模式。

底部导航栏使用 card 色背景,顶部有 1px 的分割线(COLORS.line),与内容区域形成视觉分隔。导航栏的上下内边距为 6px,保证了触控区域的舒适度,同时不会占用过多屏幕空间。

3.26 主构建函数:Stack 分层布局的完整架构

  build() {
    Stack() {
      Column() {
        this.header()
        if (this.mainTab === 0) {
          Column() {
            this.subNav()
            if (this.subTab === 0) {
              this.tabPick()
            } else if (this.subTab === 1) {
              this.tabShroom()
            } else if (this.subTab === 2) {
              this.tabToxic()
            } else if (this.subTab === 3) {
              this.tabSeason()
            } else if (this.subTab === 4) {
              this.tabForage()
            } else {
              this.tabFriends()
            }
          }
          .layoutWeight(1)
          .width('100%')
          .alignItems(HorizontalAlign.Center)
          .backgroundColor(COLORS.bg)
        } else if (this.mainTab === 1) {
          this.pageHill()
        } else if (this.mainTab === 2) {
          this.pageGoods()
        } else {
          this.pageMine()
        }
        this.bottomBar()
      }
      .width('100%')
      .height('100%')

build() 函数是组件的核心渲染方法,描述了整个页面的 UI 结构。最外层使用 Stack 布局,实现了三层叠加:内容层(底部)、特效层(中间)、弹框层(顶部)。这种 Stack 分层布局是 ArkUI 中处理叠加视图的标准方式。

第一层(底层)是主内容区,使用 Column 垂直布局,从上到下依次是头部、内容区和底部导航。内容区通过 if...else if... 条件渲染,根据 mainTab 的值显示不同的页面。当 mainTab 为 0(首页)时,内容区又嵌套了一层次级 Tab 的条件渲染,根据 subTab 的值显示不同的内容页面。

这种"条件 + Builder"的 Tab 切换方式是 ArkUI 中的常见做法。与使用 Tabs 组件相比,这种方式更加灵活,可以精确控制每个 Tab 的渲染逻辑和过渡效果。同时,条件渲染确保了只有当前 Tab 的内容被构建,未选中的 Tab 不参与渲染,节省了渲染资源。

首页的内容区使用 .layoutWeight(1) 填充剩余空间,确保头部和底部导航之间的区域被内容填满。内容区背景为 COLORS.bg,与头部和底部导航的背景色形成统一。

      // 特效层仅覆盖内容区(头部之下)
      Column() {
        this.fxLayer()
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.End)
      .hitTestBehavior(HitTestMode.None)

第二层是特效层,位于内容层之上。特效层使用一个全屏的 Column 容器,通过 .justifyContent(FlexAlign.End) 将特效内容推到底部,确保特效只覆盖内容区域(头部下方),不会遮挡头部信息。

.hitTestBehavior(HitTestMode.None) 是一个关键属性,它设置特效层不响应任何触摸事件,所有触摸事件直接穿透到下层的内容区。这对于覆盖全屏但又不影响用户交互的特效层来说是必不可少的。如果没有这个设置,特效层会拦截所有触摸事件,导致下层的列表滚动、按钮点击等交互失效。

      // 弹框层
      if (this.addModal) {
        Column() {
          this.modalOverlay(() => { this.addModal = false; })
          Column() {
            this.addModalBody()
          }
          .constraintSize({ maxHeight: '80%' })
          .zIndex(999)
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }
      // ... editModal 和 delModal 类似
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS.bg)
  }

第三层(顶层)是弹框层,包含三种弹框的条件渲染。每个弹框的结构相同:外层是一个全屏的 Column,垂直居中对齐;内部包含遮罩层和弹框体。遮罩层覆盖全屏用于背景变暗和点击关闭,弹框体位于遮罩层上方,居中显示。

弹框体使用了 .constraintSize({ maxHeight: '80%' }) 来限制最大高度,防止内容过多时弹框超出屏幕。不同弹框的最大高度略有不同——发布动态弹框为 80%,编辑弹框为 70%,删除确认弹框为 60%。这种差异化的高度限制反映了各弹框内容量的差异。

.zIndex(999) 确保弹框体在遮罩层之上显示。虽然在 Stack 布局中,后声明的元素默认在上层,但显式设置 zIndex 可以更加明确地控制层级关系,特别是在层级复杂的场景中。

主构建函数整体呈现出清晰的三层架构:底层是完整的应用内容(头部 + 内容区 + 底部导航),中层是视觉特效(孢子飘散 + 蘑菇呼吸),顶层是交互弹框(新增 + 编辑 + 删除)。这种分层设计将不同功能的视图元素在空间上分离,既保证了视觉效果的丰富性,又保证了交互逻辑的清晰性。


四、Mermaid 流程图

以下流程图展示了应用从生命周期初始化到页面渲染,再到用户交互与数据更新的完整流程:

mainTab=0

mainTab=1

mainTab=2

mainTab=3

subTab=0

subTab=1

subTab=2

subTab=3

subTab=4

subTab=5

触发重新渲染

触发重新渲染

触发重新渲染

触发重新渲染

aboutToAppear 生命周期

启动 setInterval 定时器

每 660ms 更新 tick 和 glow 状态

触发特效层重新渲染

孢子位置重新计算 / 蘑菇呼吸缩放

build 主构建函数执行

Stack 三层布局初始化

第一层: 主内容 Column

第二层: 特效层 fxLayer

第三层: 弹框层 条件渲染

头部 header 渲染

主 Tab 条件判断

首页: subNav + 子 Tab

菌山页 pageHill

山货页 pageGoods

我的页 pageMine

底部导航 bottomBar

子 Tab 条件判断

精选 tabPick

菌谱 tabShroom

毒性预警 tabToxic

菌季指数 tabSeason

采菌向导 tabForage

菌友圈 tabFriends

用户点击底部 Tab

mainTab 状态更新

用户点击次级 Tab

subTab 状态更新

用户点击调整/取消按钮

用户点击发布动态/删除

用户点击下架按钮

openEdit/openDelForage 方法调用

openAdd/openDelGoods 方法调用

openDelGoods 方法调用

设置 editIdx / delTarget

对应 modal 状态设为 true

弹框显示, 用户输入/确认

doAdd/doEdit/doDel 方法调用

friends/forages/goods 数组更新

对应 modal 状态设为 false

流程图展示了应用的核心运行机制:状态驱动的渲染循环。应用启动后进入 aboutToAppear,初始化定时器和状态,然后执行 build() 构建完整的 UI 树。当用户进行交互(点击 Tab、点击按钮、输入文字等)时,状态变量发生变化,触发 build() 的重新执行,UI 随之更新。特效定时器则周期性地更新动画状态,驱动特效层的持续渲染。

整个流程体现了声明式 UI 的核心思想——数据是源,UI 是数据的映射。开发者只需关注状态的变化和 UI 的描述,框架负责高效地完成状态到视图的同步。这种模式大大简化了复杂交互的开发,也使得代码结构更加清晰可维护。


五、技术对比表格

技术维度 实现方式 设计特点 性能考量
状态管理 @State + @Observed 装饰器 组件内部状态集中管理,类实例深层观察 适度拆分状态变量,避免过度追踪;ForEach 键值优化列表渲染
导航架构 底部 4 Tab + 首页 6 子 Tab 两级导航 数据驱动(NAV_LIST / SUB_NAV_LIST),条件渲染切换 条件渲染仅构建当前 Tab,节省渲染资源;切换时重置子 Tab 状态
动画方案 setInterval 驱动状态更新 孢子飘散伪随机计算 + 蘑菇呼吸两态缩放 660ms 间隔平衡流畅度与性能;hitTestBehavior 避免特效拦截事件
列表渲染 ForEach 遍历数据数组 数据驱动 UI,键函数优化差异更新 使用唯一 ID 作为键值,提升增删操作效率;Scroll 隐藏滚动条
弹框系统 条件渲染 + Stack 分层 遮罩层复用,三种弹框共享确认逻辑 constraintSize 限制弹框最大高度;zIndex 明确层级关系
色彩体系 ColorPalette 接口 + COLORS 常量 森林主题深绿色系,多层级语义色 统一颜色引用,便于主题切换和维护;遮罩色使用 8 位 hex 透明度
数据模型 @Observed 类 + interface 接口 业务实体用类,静态配置用接口 @Observed 支持深层属性观察;接口轻量无运行时开销
图表实现 Column + 百分比高度模拟柱状图 纯基础组件实现,无需图表库 颜色随数据变化,兼具可视化与语义化;固定容器高度保证布局稳定
时间轴布局 左侧时间线 + 右侧内容卡 constraintSize 保证最小高度,垂直线连接 最小高度确保时间轴连续性;顶部对齐保持时间点对应
表单交互 TextInput + onChange 双向绑定 受控组件模式,状态驱动输入显示 实时验证与反馈;弹框打开时重置表单状态
特效层设计 Stack 绝对定位 + clip 裁剪 仅覆盖内容区,不遮挡头部 HitTestMode.None 穿透触摸事件;clip 防止内容溢出
按钮样式 Button + Text 叠加模式 空 Button 提供点击区域,Text 自定义内容 负边距定位文字;灵活控制按钮内布局
图标方案 Emoji 字符图标 零资源依赖,跨平台一致 无需额外图标库;不同系统 emoji 样式略有差异
生命周期 aboutToAppear / aboutToDisappear 定时器成对创建销毁,防止内存泄漏 组件销毁时清理定时器,避免无效状态更新
工具函数 顶层纯函数 无状态、无副作用、可复用 集中管理计算逻辑;便于单元测试和跨组件复用
卡片设计 多层级卡片色 + 圆角体系 card / card2 / forestDeep 多层级背景 统一圆角规格(10/12/14/16px),形成设计语言
文字层级 多字号 + 多颜色 + 多字重 从 7px 到 30px 多档字号,title/sub/spore 多色 清晰的信息层级,引导用户视觉路径;行高优化小字号可读性
社区内容 信息流列表 + 实名标识 游戏化数据统计,安全成就展示 实名认证提升信任度;数据化个人中心增强粘性

六、详细总结

6.1 架构设计:分层清晰的声明式组件架构

本应用采用了典型的 ArkUI 声明式组件架构,整体结构清晰,层次分明。最外层是 @Entry 标记的 Page 组件,作为页面的根节点承载完整的生命周期。内部通过多个 @Builder 方法将 UI 拆分为独立的构建单元——头部、导航栏、各个 Tab 页面、特效层、弹框等,每个 Builder 职责单一,便于理解和维护。

架构的核心是 Stack 三层布局模式:底层是完整的应用内容(头部 + 内容区 + 底部导航),中层是视觉特效(孢子飘散 + 蘑菇呼吸),顶层是交互弹框(新增 + 编辑 + 删除)。这种分层设计将不同功能的视图在空间维度上分离,底层负责主要内容和交互,中层增强视觉体验,顶层处理模态交互。每一层都有明确的职责边界,互不干扰又协同工作。

数据与视图的分离也是架构设计的亮点。颜色常量、导航配置、模拟数据、工具函数都定义在组件外部,作为独立的数据层和逻辑层存在。组件内部只管理与 UI 渲染直接相关的状态,通过调用外部数据和函数来构建界面。这种分离使得数据配置的修改不影响组件逻辑,组件逻辑的调整也不影响数据结构,降低了模块间的耦合度。

从扩展角度看,当前架构具有良好的可扩展性。新增 Tab 页面只需添加新的 Builder 方法并在条件渲染中增加分支;新增弹框只需添加对应的状态变量和 Builder;新增数据类型只需添加新的 @Observed 类和模拟数据。架构的开放性为后续功能迭代预留了充足的空间。

6.2 状态管理:响应式数据驱动的统一范式

应用的状态管理采用了 ArkUI 标准的装饰器方案,以 @State 为核心,配合 @Observed 类实现响应式数据更新。所有状态变量集中定义在组件顶部,分为导航状态、动画状态、弹框状态、列表数据四大类,分类清晰,一目了然。

状态管理的核心机制是"状态变化 → 视图更新"的响应式范式。当用户点击 Tab 切换页面时,mainTabsubTab 的值改变,触发 build 函数的条件渲染分支切换;当用户在弹框中提交数据时,列表数组被修改(push/splice),触发 ForEach 的重新渲染;当定时器触发时,tickglow 更新,触发特效层的动画刷新。所有 UI 变化都可以追溯到某个状态变量的变更,数据流清晰可追踪。

@Observed 装饰器的使用体现了对深层状态管理的考量。虽然在当前实现中,列表的增删操作主要依赖数组方法触发更新,但 @Observed 为属性级别的更新提供了基础。例如编辑采菌计划时,直接修改对象的 hilldate 属性就能触发视图更新,无需重新赋值整个数组元素。这种细粒度的状态观察能力在复杂表单场景中尤为重要。

状态管理的另一个优点是表单状态与列表状态的分离。弹框中的表单数据(addNick、addText、editHill、editDate 等)使用独立的状态变量管理,提交时才同步到列表数据中。这种设计避免了编辑过程中对原始数据的干扰,也为取消操作提供了天然的支持——用户取消编辑时,只需关闭弹框即可,原始数据不受任何影响。

6.3 性能优化:从渲染机制到交互体验的多维考量

性能优化贯穿了应用设计的多个层面。在渲染层面,条件渲染确保了只有当前可见的 Tab 内容被构建,未选中的页面不参与渲染过程,显著减少了初始渲染和切换时的计算量。ForEach 的键函数(第二个参数)使用数据的唯一标识(如 id、label)作为键值,使得框架能够准确识别列表项的增删改位置,最小化 DOM 操作的范围。

特效层的性能设计也值得称道。hitTestBehavior(HitTestMode.None) 属性确保特效层不消耗任何触摸事件,所有交互直接穿透到底层内容,既不影响用户操作,也减少了事件处理的开销。.clip(true) 则确保特效元素不会溢出容器范围,避免了不必要的离屏渲染。定时器的 660ms 间隔也是一个平衡的选择——既保证了动画的视觉流畅度,又不会因为频率过高而增加 CPU 负担。

在布局层面,弹性布局(layoutWeight)和百分比尺寸的广泛使用,确保了界面在不同屏幕尺寸下的自适应性,减少了布局计算的复杂度。统一的卡片规格、圆角体系、间距系统等设计规范,不仅提升了视觉一致性,也减少了布局计算的差异性,有助于渲染性能的稳定。

数据层面的优化同样不可忽视。工具函数全部采用纯函数形式,输入输出可预测,没有副作用,不会引发意外的状态更新。颜色常量和配置数据的集中管理,减少了运行时的对象创建和属性查找。静态数据使用接口(interface)定义而非类(class),避免了类实例化的开销,对于不需要响应式的数据来说更加轻量高效。

6.4 交互设计:以用户为中心的多场景交互体系

应用的交互设计覆盖了多种用户场景,形成了完整的交互体系。导航交互方面,底部 4 主 Tab + 首页 6 子 Tab 的两级导航结构,既保证了主要功能的快速可达,又在首页内部提供了丰富的内容切换。点击底部 Tab 时自动重置子 Tab 到首页的设计,体现了对用户心理模型的理解——切换到大分类后,用户期望看到该分类的默认概览页面。

弹框交互方面,三种弹框(新增、编辑、删除)分别对应不同的操作场景,每种弹框都有明确的标题、表单内容和操作按钮。弹框的打开和关闭都通过状态变量控制,动画流畅自然。删除确认弹框的差异化文案设计,针对不同的删除对象给出具体的后果说明,有效降低了误操作的风险。"再想想"的取消按钮文案,比生硬的"取消"更加温和,体现了对用户心理的细腻关怀。

列表交互方面,采菌向导的时间轴设计将抽象的时间信息可视化,使用户能够直观地理解各期计划的先后顺序和时间间隔。菌谱图鉴的 TOP3 特殊标记(边框 + 编号徽章),为列表增加了排名感和趣味性。毒性预警页的红警主题和进度条设计,通过视觉强化了危险信息的警示效果。

表单交互方面,输入框的 placeholder 文案既提示了输入内容,又给出了格式示例(如"出行时间 如 本周六 06:30"),降低了用户的认知负担。编辑弹框中的提示文字"出菌窗口期多变,出发前请再次确认菌情",在恰当的时机给予用户专业提醒,体现了产品的温度和专业性。


安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:



6.5 总结:

在这里插入图片描述

从可扩展性的角度审视,应用的代码结构为后续的功能迭代预留了充足的空间。导航系统采用数据驱动的实现方式,新增导航项只需在 NAV_LISTSUB_NAV_LIST 中添加配置,并在条件渲染中增加对应的分支即可,改动量小且模式统一。

数据模型的设计也考虑了扩展性。@Observed 类的使用使得数据模型可以独立演进,新增属性不会影响现有逻辑,只需在 UI 中增加对应的展示即可。不同类型的数据(菌种、采菌计划、山货、菌友动态)各自独立,互不耦合,新增业务实体只需定义新的类和对应的数据即可。

弹框系统采用了模式化的设计——新增弹框类型只需添加新的状态变量、新的 open 方法、新的 Builder 方法,并在 build 的弹框层增加条件渲染即可。遮罩层和弹框布局的实现模式可以直接复用,开发效率高,一致性好。

特效系统也具备良好的可扩展性。孢子动画通过种子值 + 时间函数的方式生成,新增孢子粒子只需在 ForEach 的数组中添加新的种子值即可。蘑菇呼吸效果的参数(缩放比例、呼吸频率)都可以通过常量或参数灵活调整,为后续的动效调优提供了便利。

从更大的视角看,当前的单文件组件架构适合中小规模的应用。如果应用规模持续增长,可以进一步将各 Tab 页面拆分为独立的自定义组件,将数据模型和工具函数抽取到独立的模块文件中,形成更清晰的目录结构和模块化体系。ArkUI 的组件化和模块化机制为这种演进提供了坚实的技术基础。

Logo

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

更多推荐