引言:一款面向新手父母的垂直电商应用

在移动互联网深度渗透家庭生活的今天,母婴垂直电商已经成为零售行业中一个极具潜力的细分赛道。新手父母在迎接新生命到来的过程中,面临着奶粉辅食、纸尿裤、婴儿服饰、洗护用品、玩具早教等多品类的密集采购需求,而传统的综合电商平台往往信息过载、筛选成本高,难以满足这一人群"专业、安全、高效"的购物诉求。本文将深入剖析一款基于 HarmonyOS ArkTS 声明式开发范式构建的母婴商城应用,从顶层架构到组件细节,全面解读其设计思想与工程实现。

在这里插入图片描述

这款母婴商城应用定位为一站式母婴用品购物平台,涵盖了商品浏览、分类检索、购物车管理、订单追踪、个人中心五大核心功能模块。应用以"首页"为流量入口,通过 Banner 轮播位展示换季大促、奶粉专区、纸尿裤囤货等营销活动,引导用户进入商品瀑布流进行选购;"分类"页签以列表形式呈现奶粉辅食、纸尿裤、宝宝服饰、玩具早教、洗护用品、喂养用品、孕产用品、推车座椅八大品类,帮助用户快速定位目标商品;"购物车"页签支持商品选中状态管理与数量增减,并实时计算结算金额;"订单"页签展示已发货、待发货、已签收、已取消等多种状态的订单记录,并支持待发货订单的修改与取消操作;"我的"页签则聚合了用户档案、收货地址、收藏、优惠券、积分商城、宝宝档案、客服中心等个人服务入口。

在视觉设计理念上,这款应用采用了"婴儿粉 + 天空蓝"的双主题色方案,主色为婴儿粉 #F48FB1,强调色为天空蓝 #4FC3F7,整体色调温暖、柔和、富有亲和力,与母婴产品的天然属性高度契合。背景色采用淡粉色调 #FFF5F8,营造出温馨而不刺眼的浏览氛围;卡片背景统一使用纯白 #FFFFFF,通过细腻的阴影效果(radius: 2, color: '#0D000000', offsetY: 1)实现层次感;文本颜色细分为主要文本 #4A2C3A(深紫调)、次要文本 #9C7A8A(灰紫调)、提示文本 #D4B5C4(浅紫调)三级,确保信息层级的清晰可读。此外,应用还引入了成功绿、警告橙、危险红等功能色,分别用于订单状态标识、评分星级、价格高亮等场景,构建了一套完整的语义化色彩体系。

从技术架构角度来看,该应用采用了 HarmonyOS ArkTS 声明式 UI 开发范式,基于 @Entry@Component@State@Builder 等装饰器构建单页面多 Tab 的应用结构。整个应用以一个 BabyShopApp 结构体作为入口组件,通过 @State 管理当前激活的 Tab 页签以及三个弹窗的显隐状态,利用条件渲染在 build() 函数中切换不同页签的内容,并通过 @Builder 装饰的方法将各个页签的 UI 拆分为独立的可复用单元。这种"单组件多 Builder"的架构方式在中小型应用中具有开发效率高、状态集中、调试方便的优势,同时也为后续向多组件拆分演进预留了空间。

在数据架构方面,应用采用了"接口定义 + 硬编码常量 + 纯函数计算"的三层设计。首先通过 interface 定义了 ProductMetaCartMetaOrderMetaCategoryMetaBannerMeta 等类型契约,明确了各业务实体的字段结构;然后将 Banner 列表、分类列表、商品列表、购物车列表、订单列表等业务数据以 const 常量数组的形式硬编码在文件中,便于原型阶段的快速迭代与展示;最后将折扣计算、状态颜色映射、购物车合计、星级生成等业务逻辑封装为纯函数,保证逻辑的可测试性与可复用性。这种分层设计使得数据结构、数据内容、业务逻辑三者清晰分离,体现了良好的工程素养。

在应用场景上,这款应用主要面向 0-3 岁宝宝的新手父母群体。这类用户通常具有"高频次、多品类、重品质"的购物特征:奶粉、纸尿裤等刚需品需要定期复购,服饰、玩具等非刚需品需要根据宝宝成长阶段精准匹配,而洗护用品、喂养用品则对安全性与材质有较高要求。应用通过 ageRange(适用月龄/年龄)、tags(商品标签,如"海外直邮"“正品”“A类纯棉”“食品级硅胶”)、rating(评分)等字段,为用户提供了多维度的商品筛选与决策辅助信息,有效降低了选购的认知负担。下面,我们将逐段深入分析代码实现。


一、类型定义:业务实体的契约化建模

1.1 商品元数据接口 ProductMeta

interface ProductMeta {
  id: number
  name: string
  category: string
  price: number
  originalPrice: number
  sales: number
  stock: number
  imageColor: string
  tags: string[]
  ageRange: string
  rating: number
}

在这里插入图片描述

ProductMeta 是整个应用中最核心的业务实体接口,它完整地描述了一件母婴商品的元数据信息。其中 id 作为唯一标识,name 是商品全称(通常包含品牌、规格、段位等信息),category 标识商品所属分类,priceoriginalPrice 分别代表当前售价与原价,二者配合可计算折扣力度。sales 字段记录累计销量,用于营造热销氛围;stock 字段记录库存数量,为后续库存预警功能预留接口。imageColor 是一个颇具巧思的设计——由于原型阶段未接入真实图片资源,这里用一个色值字符串代替商品主图背景色,既保证了视觉差异化,又避免了外部资源依赖。tags 数组承载了"海外直邮"“正品”"A类纯棉"等营销标签,ageRange 明确了适用月龄或体重范围,rating 则以浮点数形式记录商品评分。这些字段共同构成了商品卡片的完整数据支撑。

1.2 购物车与订单接口 CartMeta、OrderMeta

interface CartMeta {
  id: number
  productName: string
  price: number
  quantity: number
  imageColor: string
  selected: boolean
}

interface OrderMeta {
  id: number
  items: string
  amount: number
  status: string
  date: string
  count: number
  logistics: string
}

在这里插入图片描述

CartMeta 接口建模了购物车条目,其中 selected 布尔字段用于标记该条目是否被勾选参与结算,这是电商购物车"批量结算"场景的标配字段。quantity 记录购买数量,配合 price 即可计算小计金额。值得注意的是,购物车条目冗余存储了 productNameimageColor,而非通过 productId 关联回商品表——这种反范式设计在购物车场景下是合理的,因为它避免了每次渲染购物车时都需要遍历商品列表查找名称,同时也兼容了商品下架后购物车仍能展示历史信息的业务需求。

OrderMeta 接口则建模了订单实体,items 字段以字符串形式简要描述订单包含的商品(如"奶粉+纸尿裤"),amount 是订单总金额,status 是订单状态文本(“已发货”“待发货”“已签收”“已取消”),date 是下单日期,count 是商品总件数,logistics 是物流公司名称。这种扁平化的订单结构适合展示型场景,但在真实业务中通常会进一步拆分为订单主表与订单明细表,以支持更复杂的退换货与售后流程。

1.3 分类与轮播接口 CategoryMeta、BannerMeta

interface CategoryMeta {
  name: string
  icon: string
  color: string
  count: number
}

interface BannerMeta {
  title: string
  subtitle: string
  color: string
  icon: string
}

在这里插入图片描述

CategoryMeta 描述了商品分类,包含分类名称、图标(Emoji 字符)、主题色、商品数量四个字段。count 字段在分类页中会以"128件"的形式展示,帮助用户感知各品类的丰富程度。BannerMeta 描述了首页轮播位的营销活动,title 是活动主标题(如"宝宝换季大促"),subtitle 是副标题(如"满200减50 · 全场包邮"),color 是 Banner 的背景色,icon 是装饰性 Emoji。这两个接口结构简洁,但字段语义清晰,足以驱动首页的视觉呈现。

1.4 设计令牌接口 ColorPalette

interface ColorPalette {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  accentDark: string;
  bg: string;
  cardBg: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  border: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
}

在这里插入图片描述

ColorPalette 接口定义了一套完整的设计令牌(Design Token)类型契约。它将色彩分为六个语义分组:主色系(primary/primaryLight/primaryDark)、强调色系(accent/accentLight/accentDark)、背景色(bg/cardBg)、文本色(textPrimary/textSecondary/textHint)、功能色(success/warning/danger)以及纯白。通过接口约束,编译器能够在使用 COLORS.xxx 时提供智能提示与类型检查,避免拼写错误导致的运行时异常。这种"接口 + 常量"的设计令牌管理模式,是大型应用实现主题统一与暗黑模式适配的基础。


二、设计令牌与配置常量

2.1 全局色彩常量 COLORS

const COLORS: ColorPalette = {
  primary: '#F48FB1',
  primaryLight: '#FCE4EC',
  primaryDark: '#EC407A',
  accent: '#4FC3F7',
  accentLight: '#E1F5FE',
  accentDark: '#0288D1',
  bg: '#FFF5F8',
  cardBg: '#FFFFFF',
  textPrimary: '#4A2C3A',
  textSecondary: '#9C7A8A',
  textHint: '#D4B5C4',
  border: '#FCE4EC',
  success: '#66BB6A',
  warning: '#FFA726',
  danger: '#EF5350',
  white: '#FFFFFF'
};

在这里插入图片描述

COLORS 常量是整个应用的视觉基石。主色系采用 Material Design 风格的三阶色值:primary#F48FB1)是标准的婴儿粉,用于标题栏渐变与选中态高亮;primaryLight#FCE4EC)是极浅粉,用于标签背景与边框;primaryDark#EC407A)是深玫红,用于强调文字与主按钮背景。强调色系以天空蓝为基调,accent#4FC3F7)用于订单"已发货"状态,accentDark#0288D1)可用于链接与可交互元素。背景色 #FFF5F8 是带有极淡粉调的米白色,相比纯白更能缓解长时间浏览的视觉疲劳。文本色采用紫调灰阶,比纯灰更有温度感,符合母婴场景的情感诉求。功能色中,success#66BB6A)用于"已签收"状态,warning#FFA726)用于"待发货"状态与星级评分,danger#EF5350)用于价格、折扣与危险操作按钮。这套色彩体系既保证了视觉统一性,又通过语义化命名实现了"所见即所意"的代码可读性。

2.2 底部 Tab 配置 TAB_CONFIG

const TAB_CONFIG: Record<string, string> = {
  'home': '首页',
  'category': '分类',
  'cart': '购物车',
  'orders': '订单',
  'mine': '我的'
}

在这里插入图片描述

TAB_CONFIG 使用 Record<string, string> 类型定义了底部导航栏的配置映射,键为 Tab 的内部标识(英文),值为展示文本(中文)。这种"键值对映射 + ForEach 遍历"的方式相比逐个硬编码 Tab 项具有显著优势:新增或调整 Tab 只需修改一处配置,底部导航栏的渲染逻辑无需改动。在 bottomTabBar Builder 中,应用通过 Object.keys(TAB_CONFIG) 获取所有 Tab 键并遍历渲染,实现了配置与渲染的解耦。需要注意的是,Record 类型是 ArkTS 对 TypeScript 工具类型的支持,它等价于定义一个字符串键到字符串值的索引签名对象。


三、硬编码业务数据

3.1 Banner 轮播列表

const BANNER_LIST: BannerMeta[] = [
  { title: '宝宝换季大促', subtitle: '满200减50 · 全场包邮', color: '#F8BBD0', icon: '👶' },
  { title: '奶粉专区', subtitle: '海外直邮 · 正品保障', color: '#B3E5FC', icon: '🍼' },
  { title: '纸尿裤囤货', subtitle: '买2送1 · 限时特惠', color: '#FFE0B2', icon: '🧷' }
]

在这里插入图片描述

BANNER_LIST 定义了首页顶部水平轮播的三张 Banner。每张 Banner 都有独立的背景色——粉色、蓝色、橙色,形成视觉节奏感,避免单调。文案上,主标题突出活动主题,副标题强调优惠力度与保障承诺,符合电商营销文案"利益点前置"的撰写原则。Emoji 图标作为装饰元素,在不依赖图片资源的前提下增加了趣味性与识别度。在真实项目中,这些数据通常来自运营后台的 CMS 配置接口,并通过定时刷新保持活动信息的时效性。

3.2 分类列表

const CATEGORY_LIST: CategoryMeta[] = [
  { name: '奶粉辅食', icon: '🍼', color: '#FFE082', count: 128 },
  { name: '纸尿裤', icon: '🧷', color: '#FFAB91', count: 86 },
  { name: '宝宝服饰', icon: '👕', color: '#F48FB1', count: 215 },
  { name: '玩具早教', icon: '🧸', color: '#A5D6A7', count: 156 },
  { name: '洗护用品', icon: '🧴', color: '#90CAF9', count: 92 },
  { name: '喂养用品', icon: '🍽', color: '#CE93D8', count: 73 },
  { name: '孕产用品', icon: '🤰', color: '#FFCC80', count: 64 },
  { name: '推车座椅', icon: '🚼', color: '#80CBC4', count: 45 }
]

CATEGORY_LIST 定义了八大商品分类,每个分类配有独立的主题色,颜色取自 Material Design 调色板的 200 色阶,饱和度适中、明度统一,保证了网格中八个分类图标背景的和谐并存。count 字段反映了各品类的商品丰富度,从"推车座椅"的 45 件到"宝宝服饰"的 215 件,跨度较大,这也符合母婴品类中服饰 SKU 多、大件耐用品 SKU 少的实际情况。这些分类数据同时服务于首页的分类网格与分类 Tab 的列表展示,实现了"一份数据,多处复用"。

3.3 商品列表

const PRODUCT_LIST: ProductMeta[] = [
  { id: 1, name: '荷兰牛栏奶粉 3段 900g', category: '奶粉辅食', price: 268, originalPrice: 328, sales: 5632, stock: 500, imageColor: '#FFE082', tags: ['海外直邮', '正品'], ageRange: '6-12月', rating: 4.9 },
  { id: 2, name: '花王纸尿裤 L码 54片', category: '纸尿裤', price: 98, originalPrice: 139, sales: 8721, stock: 800, imageColor: '#FFAB91', tags: ['日本进口', '透气'], ageRange: '9-14kg', rating: 4.8 },
  { id: 3, name: '纯棉婴儿连体衣 0-3月', category: '宝宝服饰', price: 59, originalPrice: 89, sales: 3456, stock: 300, imageColor: '#F48FB1', tags: ['A类纯棉', '无荧光'], ageRange: '0-3月', rating: 4.9 },
  // ... 其余商品省略展示
]

PRODUCT_LIST 是首页商品瀑布流的数据源,包含十件覆盖八大分类中主要品类的商品。每件商品的命名遵循"品牌+品类+规格+适用对象"的规范,信息密度高且便于搜索。价格设计上,所有商品均设置了低于原价的当前售价,折扣力度从约 18% 到 41% 不等,营造出"全场促销"的购物氛围。tags 标签精选了最能体现商品卖点的关键词,如"海外直邮"“正品”“A类纯棉”“食品级硅胶”"EDI纯水"等,这些标签直接关系到母婴用户最关心的安全性与品质问题,是促转化的关键信息。ageRange 字段针对不同品类采用了不同的度量方式——奶粉用月龄、纸尿裤用体重、服饰用月龄、玩具用年龄——体现了对母婴品类特性的深入理解。

3.4 购物车与订单列表

const CART_LIST: CartMeta[] = [
  { id: 1, productName: '荷兰牛栏奶粉 3段 900g', price: 268, quantity: 2, imageColor: '#FFE082', selected: true },
  { id: 2, productName: '花王纸尿裤 L码 54片', price: 98, quantity: 3, imageColor: '#FFAB91', selected: true },
  { id: 3, productName: '纯棉婴儿连体衣 0-3月', price: 59, quantity: 2, imageColor: '#F48FB1', selected: false },
  { id: 4, productName: '费雪益智积木 100粒', price: 128, quantity: 1, imageColor: '#A5D6A7', selected: true },
  { id: 5, productName: '婴儿洗发沐浴二合一', price: 45, quantity: 2, imageColor: '#90CAF9', selected: false }
]

const ORDER_LIST: OrderMeta[] = [
  { id: 3001, items: '奶粉+纸尿裤', amount: 830, status: '已发货', date: '2024-04-15', count: 5, logistics: '顺丰快递' },
  { id: 3002, items: '连体衣+口水巾', amount: 157, status: '待发货', date: '2024-04-15', count: 2, logistics: '中通快递' },
  { id: 3003, items: '益智积木+湿巾', amount: 177, status: '已签收', date: '2024-04-12', count: 2, logistics: '圆通快递' },
  { id: 3004, items: '推车', amount: 599, status: '已签收', date: '2024-04-08', count: 1, logistics: '德邦物流' },
  { id: 3005, items: '米粉+奶嘴', amount: 99, status: '已取消', date: '2024-04-05', count: 2, logistics: '--' },
  { id: 3006, items: '洗护套装', amount: 134, status: '已签收', date: '2024-04-01', count: 3, logistics: '韵达快递' }
]

CART_LIST 包含五条购物车记录,其中三条 selectedtrue,两条为 false,这种混合状态正好可以演示底部结算栏的合计计算逻辑。ORDER_LIST 包含六条订单记录,覆盖了"已发货"“待发货”“已签收”“已取消"四种状态,其中"待发货"状态的订单会展示"修改"与"取消"操作按钮,触发相应的弹窗交互。物流公司字段涵盖了顺丰、中通、圆通、德邦、韵达等主流快递,体现了真实业务的多样性;已取消订单的物流字段为”–",表示无物流信息,这种边界情况的处理体现了数据建模的严谨性。


四、纯函数业务逻辑

4.1 折扣百分比计算

function getDiscountPercent(original: number, current: number): number {
  return Math.floor((1 - current / original) * 100)
}

getDiscountPercent 函数接收原价与现价两个参数,返回向下取整的折扣百分比。例如原价 328、现价 268,计算结果为 Math.floor((1 - 268/328) * 100) = Math.floor(18.29) = 18,在商品卡片上展示为"-18%"。使用 Math.floor 而非 Math.round 是电商行业的惯例——折扣力度向下取整可以避免"夸大优惠"的合规风险。这是一个无副作用的纯函数,输入相同则输出必然相同,易于单元测试与复用。

4.2 订单状态颜色映射

function getStatusColor(status: string): string {
  if (status === '已签收') {
    return COLORS.success
  }
  if (status === '已发货') {
    return COLORS.accent
  }
  if (status === '待发货') {
    return COLORS.warning
  }
  return COLORS.textHint
}

getStatusColor 函数将订单状态文本映射为语义化的颜色值:"已签收"对应成功绿,传递积极完成的信号;"已发货"对应天空蓝,传递在途进行中的信号;“待发货"对应警告橙,提示用户关注发货进度;其余状态(如"已取消”)对应浅紫灰提示色,弱化视觉权重。这种"状态-颜色"的映射规则使用户无需细读文字即可通过颜色快速感知订单进展,是信息可视化在列表场景下的典型应用。函数采用 if 语句链而非 switch 或对象映射,在状态数量较少时可读性更佳。

4.3 购物车合计与件数计算

function getSelectedCartTotal(): number {
  let total: number = 0
  for (const c of CART_LIST) {
    if (c.selected) {
      total += c.price * c.quantity
    }
  }
  return total
}

function getSelectedCartCount(): number {
  let count: number = 0
  for (const c of CART_LIST) {
    if (c.selected) {
      count += c.quantity
    }
  }
  return count
}

getSelectedCartTotalgetSelectedCartCount 两个函数分别计算购物车中已选中商品的总金额与总件数。二者均采用 for...of 循环遍历 CART_LIST,通过 if (c.selected) 过滤未选中条目,累加 price * quantityquantity。以当前数据为例,选中的三条记录为奶粉(268×2=536)、纸尿裤(98×3=294)、积木(128×1=128),合计 958 元、6 件,因此底部结算栏会显示"合计:¥958"与"结算(6)"。这两个函数在购物车页面的底部结算栏中被调用,实现了金额与件数的实时联动。需要指出的是,由于这里遍历的是硬编码的 CART_LIST 常量而非响应式状态,因此在真实项目中应将其改造为接收数组参数的纯函数,或绑定到 @State 状态以实现响应式更新。

4.4 星级评分生成

function getRatingStars(rating: number): string {
  let stars: string = ''
  for (let i = 0; i < 5; i++) {
    if (i < rating) {
      stars += '★'
    } else {
      stars += '☆'
    }
  }
  return stars
}

getRatingStars 函数将数值评分转换为五角星字符串。它循环五次,前 rating 个位置填充实心星"★",其余位置填充空心星"☆"。例如评分 4.9 会生成"★★★★☆"(因为 i < 4.9 在 i=0,1,2,3,4 时,前四个为 true,i=4 时 4<4.9 为 true 实际生成五个实心星——这里需要注意浮点比较的细节,评分 4.9 实际会显示为五个实心星)。这个函数用最简洁的方式实现了星级可视化,避免了引入图标资源的开销。在更高保真的实现中,可以考虑支持半星显示,使用 Unicode 字符"⯨"或自定义 SVG 图标。


五、入口组件与状态管理

5.1 组件声明与状态定义

@Entry
@Component
struct BabyShopApp {
  @State currentTab: string = 'home'
  @State showAddDialog: boolean = false
  @State showEditDialog: boolean = false
  @State showDeleteDialog: boolean = false

BabyShopApp 是应用的入口组件,通过 @Entry 装饰器标记为页面入口,通过 @Component 装饰器声明为自定义组件。组件内部定义了四个 @State 状态变量:currentTab 记录当前激活的 Tab 页签标识,初始值为 'home'showAddDialogshowEditDialogshowDeleteDialog 三个布尔值分别控制加入购物车弹窗、修改订单弹窗、取消订单弹窗的显隐。@State 装饰器使得这些变量成为响应式状态——当它们的值发生变化时,ArkUI 框架会自动触发依赖这些状态的 UI 重新渲染。这种"状态驱动 UI"的声明式范式,相比传统的命令式 DOM 操作,大幅降低了状态与视图同步的心智负担。值得注意的是,三个弹窗状态相互独立,意味着理论上可以同时显示多个弹窗,但在实际交互中由于每次只触发一个,不会出现叠加情况。

5.2 build 函数:整体布局骨架

build() {
  Stack() {
    Column() {
      Row() {
        Text('👶 母婴商城')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.white)
        Column().layoutWeight(1)
        Text('🔍')
          .fontSize(20)
          .fontColor(COLORS.white)
      }
      .width('100%')
      .height(52)
      .padding({ left: 20, right: 20 })
      .linearGradient({ angle: 135, colors: [[COLORS.primaryDark, 0.0], [COLORS.primary, 1.0]] })

      Column() {
        if (this.currentTab === 'home') {
          this.homeContent()
        } else if (this.currentTab === 'category') {
          this.categoryContent()
        } else if (this.currentTab === 'cart') {
          this.cartContent()
        } else if (this.currentTab === 'orders') {
          this.ordersContent()
        } else {
          this.mineContent()
        }
      }
      .layoutWeight(1)
      .backgroundColor(COLORS.bg)

      this.bottomTabBar()
    }
    .width('100%')
    .height('100%')

    if (this.showAddDialog) {
      this.addDialog()
    }
    if (this.showEditDialog) {
      this.editDialog()
    }
    if (this.showDeleteDialog) {
      this.deleteDialog()
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor(COLORS.bg)
}

build() 函数是组件的 UI 描述入口,它构建了一个 Stack(层叠布局)作为根容器。Stack 内部分为两层:底层是一个纵向 Column,包含顶部标题栏、Tab 内容区、底部导航栏三部分;顶层是三个条件渲染的弹窗,通过 if 判断 @State 状态决定是否显示。这种"内容在下、弹窗在上"的层叠结构是移动端弹窗交互的标准模式。

顶部标题栏是一个 Row,左侧是"👶 母婴商城"标题文字,中间用一个 Column().layoutWeight(1) 占位撑开空间,右侧是搜索图标。标题栏高度 52vp,左右内边距 20vp,背景采用 135 度角的双色线性渐变,从 primaryDark(深玫红)渐变到 primary(婴儿粉),营造出柔和而有层次的视觉效果。

Tab 内容区是一个 Column,通过 if...else if 链根据 this.currentTab 的值调用对应的 @Builder 方法渲染不同页签的内容。layoutWeight(1) 使其占据标题栏与底部导航栏之间的全部剩余空间,backgroundColor(COLORS.bg) 设置淡粉背景。底部导航栏通过 this.bottomTabBar() 调用 Builder 方法渲染。整个 build 函数结构清晰,职责分明,体现了"骨架 + 内容 + 浮层"的三段式布局思想。


六、底部导航栏 Builder

6.1 bottomTabBar 实现

@Builder
bottomTabBar() {
  Row() {
    ForEach(Object.keys(TAB_CONFIG), (key: string) => {
      Column() {
        Text(this.getTabIcon(key))
          .fontSize(20)
        Text(TAB_CONFIG[key])
          .fontSize(11)
          .margin({ top: 2 })
          .fontColor(this.currentTab === key ? COLORS.primaryDark : COLORS.textHint)
      }
      .layoutWeight(1)
      .height(56)
      .justifyContent(FlexAlign.Center)
      .onClick(() => {
        this.currentTab = key
      })
    })
  }
  .width('100%')
  .height(56)
  .backgroundColor(COLORS.white)
  .border({ width: { top: 1 }, color: COLORS.border })
}

bottomTabBar Builder 负责渲染底部导航栏。它通过 ForEach(Object.keys(TAB_CONFIG), ...) 遍历 Tab 配置的所有键,为每个 Tab 渲染一个 Column,内含图标与文字两行。每个 Tab 项使用 layoutWeight(1) 等分宽度,高度 56vp,内容居中对齐。文字颜色通过三元表达式 this.currentTab === key ? COLORS.primaryDark : COLORS.textHint 实现"选中态高亮、未选中态弱化"的视觉反馈。onClick 回调将 this.currentTab 赋值为当前点击的 Tab 键,触发 @State 状态变更,进而驱动 build 函数中 Tab 内容区的条件渲染切换。底部导航栏顶部有一条 1px 的浅粉边框线,实现了与内容区的视觉分隔。这种基于 ForEach + 配置映射的导航栏实现方式,扩展性极强——新增 Tab 只需在 TAB_CONFIG 中添加一项即可。

6.2 Tab 图标映射方法

getTabIcon(key: string): string {
  if (key === 'home') {
    return this.currentTab === key ? '🏠' : '🏚'
  }
  if (key === 'category') {
    return this.currentTab === key ? '📋' : '📑'
  }
  if (key === 'cart') {
    return this.currentTab === key ? '🛒' : '🛍'
  }
  if (key === 'orders') {
    return this.currentTab === key ? '📦' : '📭'
  }
  return this.currentTab === key ? '👤' : '🙆'
}

getTabIcon 方法为每个 Tab 返回选中态与未选中态两种 Emoji 图标。例如首页选中时显示"🏠"(带烟囱的房子),未选中时显示"🏚"(废弃的房子);购物车选中时显示"🛒"(购物车),未选中时显示"🛍"(购物袋)。这种"同主题不同形态"的图标对,在视觉上既能保持一致性,又能清晰传达选中状态。方法采用 if 链逐一判断,最后用 return 兜底返回"我的"Tab 的图标。虽然这里使用 Emoji 而非矢量图标在跨平台一致性上存在风险(不同系统的 Emoji 渲染样式可能不同),但在原型阶段这种方案零资源依赖、开发效率极高。


七、首页 Tab 内容

7.1 homeContent:Banner + 分类网格 + 商品瀑布流

@Builder
homeContent() {
  Scroll() {
    Column() {
      // Banner轮播
      Scroll() {
        Row() {
          ForEach(BANNER_LIST, (banner: BannerMeta) => {
            Row() {
              Column() {
                Text(banner.title)
                  .fontSize(16)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                Text(banner.subtitle)
                  .fontSize(12)
                  .fontColor(COLORS.textSecondary)
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Text(banner.icon)
                .fontSize(40)
            }
            .width(280)
            .height(100)
            .padding(16)
            .backgroundColor(banner.color)
            .borderRadius(16)
            .margin({ right: 10 })
            .alignItems(VerticalAlign.Center)
          })
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .height(116)
      .margin({ top: 12, bottom: 12 })

homeContent Builder 构建了首页的完整内容,整体包裹在一个纵向 Scroll 中以支持内容超出屏幕时的滚动。首屏区域是 Banner 横向轮播——这里巧妙地使用了一个嵌套的横向 Scrollscrollable(ScrollDirection.Horizontal))配合 Row 容器实现水平滑动效果,并关闭了滚动条(scrollBar(BarState.Off))以保证视觉简洁。每张 Banner 是一个 280×100 的圆角卡片,左侧是标题与副标题的纵向排列,右侧是 40 号字号的装饰性 Emoji,背景色取自 Banner 数据。Banner 区域整体高度 116vp,上下各留 12vp 间距,与后续的分类网格形成节奏感。这种"横向 Scroll 模拟轮播"的方案在无需引入 Swiper 组件的前提下实现了类似效果,是轻量级实现的优选。

7.2 分类网格区域

      // 分类网格
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(CATEGORY_LIST, (cat: CategoryMeta) => {
          Column() {
            Stack() {
              Text(cat.icon)
                .fontSize(24)
            }
            .width(48)
            .height(48)
            .borderRadius(12)
            .backgroundColor(cat.color)
            Text(cat.name)
              .fontSize(11)
              .fontColor(COLORS.textPrimary)
              .margin({ top: 6 })
            Text(cat.count + '件')
              .fontSize(9)
              .fontColor(COLORS.textHint)
              .margin({ top: 2 })
          }
          .width('23%')
          .padding({ top: 10, bottom: 10 })
          .margin({ right: '2%', bottom: 8 })
          .alignItems(HorizontalAlign.Center)
        })
      }
      .width('92%')
      .margin({ bottom: 12 })

分类网格使用 Flex({ wrap: FlexWrap.Wrap }) 实现自动换行的网格布局。每个分类项宽度设为 23%,右侧间距 2%,这样一行恰好容纳四个分类(4×23% + 3×2% = 98%,剩余 2% 作为最右侧间距),八个分类分为两行展示。每个分类项内部是一个 Column:顶部是 48×48 的圆角彩色方块包裹 Emoji 图标,中间是分类名称,底部是商品数量(“128件”)。字号从上到下递减(24、11、9),形成了清晰的视觉层级。使用百分比宽度而非固定像素,保证了在不同屏幕尺寸下的自适应布局。FlexWrap.Wrap 的换行能力使得分类数量变化时无需调整布局代码即可自动重排。

7.3 今日特卖标题与商品瀑布流

      // 热门商品
      Row() {
        Text('🔥 今日特卖')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
        Column().layoutWeight(1)
        Text('更多 ›')
          .fontSize(12)
          .fontColor(COLORS.primaryDark)
      }
      .width('92%')
      .margin({ bottom: 12 })

      // 商品瀑布流(双列)
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(PRODUCT_LIST, (product: ProductMeta) => {
          this.productCard(product)
        })
      }
      .width('92%')
    }
    .width('100%')
    .padding({ bottom: 20 })
  }
  .width('100%')
  .height('100%')
  .scrollBar(BarState.Off)
}

商品区上方是一个"🔥 今日特卖"标题行,左侧标题加粗,右侧"更多 ›"链接使用主色深色,引导用户查看更多商品。标题行宽度 92%,与上下内容保持对齐。下方的商品瀑布流同样使用 Flex({ wrap: FlexWrap.Wrap }),遍历 PRODUCT_LIST 调用 this.productCard(product) 渲染每个商品卡片。由于每个卡片宽度为 48%(在 productCard 中定义),因此自动形成双列布局。整个首页内容通过外层 Scroll 包裹,支持纵向滚动浏览全部商品,滚动条被关闭以保持界面整洁。底部 padding({ bottom: 20 }) 避免最后一个商品卡片紧贴底部导航栏。


八、商品卡片 Builder

8.1 productCard:商品信息的高密度呈现

@Builder
productCard(product: ProductMeta) {
  Column() {
    Stack() {
      Column() {
        Text('🛍')
          .fontSize(32)
        Text('-' + getDiscountPercent(product.originalPrice, product.price) + '%')
          .fontSize(10)
          .fontColor(COLORS.white)
          .fontWeight(FontWeight.Bold)
          .padding({ left: 4, right: 4, top: 1, bottom: 1 })
          .backgroundColor(COLORS.danger)
          .borderRadius(4)
          .margin({ top: 4 })
      }
    }
    .width('100%')
    .height(90)
    .backgroundColor(product.imageColor)
    .borderRadius({ topLeft: 12, topRight: 12 })

productCard 是商品瀑布流中单个商品卡片的渲染逻辑,接收一个 ProductMeta 参数。卡片整体是一个 Column,分为图片区和信息区两部分。图片区是一个 Stack,高度 90vp,背景色取自商品的 imageColor,顶部圆角 12vp。图片区内是一个 Column,居中展示"🛍"购物袋 Emoji(32 号字号)作为商品图占位,下方是红色背景的折扣标签(如"-18%"),通过 getDiscountPercent 函数实时计算。这种"色块 + Emoji + 折扣角标"的占位方案,在缺少真实图片资源时仍能传递商品的色彩个性与促销信息,是原型阶段的高效做法。

8.2 商品信息区:名称、适用范围、标签

    Column() {
      Text(product.name)
        .fontSize(12)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      Text('适用:' + product.ageRange)
        .fontSize(10)
        .fontColor(COLORS.textSecondary)
        .margin({ top: 4 })

      Row() {
        ForEach(product.tags, (tag: string) => {
          Text(tag)
            .fontSize(9)
            .padding({ left: 4, right: 4, top: 1, bottom: 1 })
            .backgroundColor(COLORS.primaryLight)
            .fontColor(COLORS.primaryDark)
            .borderRadius(3)
            .margin({ right: 3 })
        })
      }
      .margin({ top: 4 })

信息区是一个 Column,内边距 10vp。首先是商品名称,使用 maxLines(2) 限制为两行,textOverflow({ overflow: TextOverflow.Ellipsis }) 使超长文本以省略号结尾,保证卡片高度一致。其次是适用范围(“适用:6-12月”),用次要文本色弱化呈现。然后是标签行,通过 ForEach 遍历 product.tags 数组,每个标签是一个浅粉背景、深玫红文字的小圆角块,字号 9,紧凑排列。这些标签是母婴用户决策的关键信息,因此虽然字号小但通过色彩对比保证了可读性。

8.3 价格行与操作行

      Row() {
        Text('¥' + product.price)
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.danger)
        Text(' ¥' + product.originalPrice)
          .fontSize(10)
          .fontColor(COLORS.textHint)
          .decoration({ type: TextDecorationType.LineThrough })
        Column().layoutWeight(1)
        Text('已售' + product.sales)
          .fontSize(9)
          .fontColor(COLORS.textHint)
      }
      .width('100%')
      .margin({ top: 6 })
      .alignItems(VerticalAlign.Bottom)

      Row() {
        Text(getRatingStars(product.rating))
          .fontSize(10)
          .fontColor(COLORS.warning)
        Text(' ' + product.rating)
          .fontSize(10)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('+ 购物车')
          .fontSize(10)
          .fontColor(COLORS.white)
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .backgroundColor(COLORS.primaryDark)
          .borderRadius(4)
          .onClick(() => {
            this.showAddDialog = true
          })
      }
      .width('100%')
      .margin({ top: 6 })
    }
    .padding(10)
    .alignItems(HorizontalAlign.Start)
  }
  .width('48%')
  .backgroundColor(COLORS.white)
  .borderRadius(12)
  .margin({ right: '2%', bottom: 10 })
  .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
}

价格行是一个 Row,左侧是红色的当前售价(15 号字号加粗),紧随其后是带删除线的原价(10 号字号浅色),中间用 Column().layoutWeight(1) 撑开空间,右侧是"已售5632"的销量提示。alignItems(VerticalAlign.Bottom) 使价格与原价底部对齐,视觉上更协调。操作行同样是 Row,左侧是橙色星级字符串与评分数值,右侧是"+ 购物车"按钮——一个深玫红背景、白字的小圆角块,点击后触发 this.showAddDialog = true,弹出加入购物车确认弹窗。卡片整体宽度 48%,白色背景,12vp 圆角,右侧与底部留有间距,并带有轻微阴影(radius: 2, color: '#0D000000', offsetY: 1),形成了悬浮于淡粉背景之上的卡片质感。


九、分类 Tab 内容

9.1 categoryContent:列表式分类导航

@Builder
categoryContent() {
  Scroll() {
    Column() {
      Text('全部分类')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .margin({ top: 16, left: 16, bottom: 12 })
        .alignSelf(ItemAlign.Start)

      ForEach(CATEGORY_LIST, (cat: CategoryMeta) => {
        Row() {
          Stack() {
            Text(cat.icon)
              .fontSize(24)
          }
          .width(48)
          .height(48)
          .borderRadius(12)
          .backgroundColor(cat.color)

          Column() {
            Text(cat.name)
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text(cat.count + '件商品')
              .fontSize(11)
              .fontColor(COLORS.textSecondary)
              .margin({ top: 2 })
          }
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 12 })
          .layoutWeight(1)

          Text('›')
            .fontSize(20)
            .fontColor(COLORS.textHint)
        }
        .width('92%')
        .padding(12)
        .backgroundColor(COLORS.white)
        .borderRadius(12)
        .margin({ bottom: 6 })
        .alignItems(VerticalAlign.Center)
        .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
      })
    }
    .width('100%')
    .padding({ bottom: 20 })
  }
  .width('100%')
  .height('100%')
  .scrollBar(BarState.Off)
}

categoryContent Builder 构建了分类 Tab 的内容,与首页的分类网格不同,这里采用列表式布局展示分类。顶部是"全部分类"标题,使用 alignSelf(ItemAlign.Start) 左对齐。下方通过 ForEach 遍历 CATEGORY_LIST,每个分类项是一个 Row:左侧是 48×48 的圆角彩色图标方块,中间是分类名称与商品数量的纵向排列(layoutWeight(1) 占据剩余空间),右侧是"›"箭头指示符,暗示可点击进入分类详情。每个分类项是白色卡片,12vp 圆角,带轻微阴影,项间距 6vp。这种列表式分类导航相比网格布局,能展示更丰富的辅助信息(如商品数量),且更适合单手操作场景下的纵向滑动浏览。两种布局方式(首页网格 + 分类页列表)复用同一份数据源,为用户提供了不同的浏览路径选择。


十、购物车 Tab 内容

10.1 cartContent:商品列表与底部结算栏

@Builder
cartContent() {
  Column() {
    Scroll() {
      Column() {
        ForEach(CART_LIST, (cart: CartMeta) => {
          Row() {
            Stack() {
              Text(cart.selected ? '✓' : '')
                .fontSize(16)
                .fontColor(COLORS.white)
                .fontWeight(FontWeight.Bold)
            }
            .width(24)
            .height(24)
            .borderRadius(12)
            .backgroundColor(cart.selected ? COLORS.primaryDark : COLORS.border)

            Stack() {
              Text('🛍')
                .fontSize(24)
            }
            .width(56)
            .height(56)
            .borderRadius(10)
            .backgroundColor(cart.imageColor)
            .margin({ left: 10 })

            Column() {
              Text(cart.productName)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.textPrimary)
                .maxLines(2)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text('¥' + cart.price)
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.danger)
                Column().layoutWeight(1)
                Text('- ' + cart.quantity + ' +')
                  .fontSize(13)
                  .fontColor(COLORS.textPrimary)
              }
              .width('100%')
              .margin({ top: 6 })
              .alignItems(VerticalAlign.Bottom)
            }
            .layoutWeight(1)
            .margin({ left: 10 })
            .alignItems(HorizontalAlign.Start)
          }
          .width('92%')
          .padding(12)
          .backgroundColor(COLORS.white)
          .borderRadius(12)
          .margin({ bottom: 6 })
          .alignItems(VerticalAlign.Center)
          .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
        })
      }
      .width('100%')
      .padding({ top: 16, bottom: 80 })
    }
    .width('100%')
    .layoutWeight(1)
    .scrollBar(BarState.Off)

cartContent Builder 构建了购物车 Tab,整体是一个 Column,分为上方的商品列表 Scroll 与下方的底部结算栏。商品列表通过 ForEach 遍历 CART_LIST,每个购物车条目是一个 Row:左侧是 24×24 的圆形勾选框,选中时显示"✓"且背景为深玫红,未选中时背景为浅粉边框色;接着是 56×56 的圆角商品图占位方块;右侧是商品名称(最多两行)、价格(红色加粗)与数量调节器("- 2 +"格式)。数量调节器以文本形式呈现,虽然未绑定实际的增减逻辑,但视觉上清晰地表达了可交互意图。列表底部留有 80vp 的内边距,避免最后一个条目被结算栏遮挡。

10.2 底部结算栏

    // 底部结算栏
    Row() {
      Text('✓ 全选')
        .fontSize(12)
        .fontColor(COLORS.textSecondary)
      Column().layoutWeight(1)
      Text('合计:¥' + getSelectedCartTotal())
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.danger)
      Button('结算(' + getSelectedCartCount() + ')')
        .fontSize(14)
        .fontColor(COLORS.white)
        .backgroundColor(COLORS.primaryDark)
        .borderRadius(20)
        .height(36)
        .padding({ left: 20, right: 20 })
        .margin({ left: 12 })
    }
    .width('100%')
    .height(56)
    .padding({ left: 16, right: 16 })
    .backgroundColor(COLORS.white)
    .border({ width: { top: 1 }, color: COLORS.border })
    .alignItems(VerticalAlign.Center)
  }
  .width('100%')
  .height('100%')
}

底部结算栏是一个固定在购物车页面底部的 Row,高度 56vp,白色背景,顶部有 1px 浅粉边框线。左侧是"✓ 全选"文字,中间用 layoutWeight(1) 撑开,右侧是"合计:¥958"的金额显示(调用 getSelectedCartTotal() 实时计算)与"结算(6)"按钮(调用 getSelectedCartCount() 实时计算件数)。结算按钮使用深玫红背景、白字、20vp 圆角的胶囊形样式,高度 36vp,左右内边距 20vp,视觉上突出且易于点击。结算栏与商品列表通过 ColumnlayoutWeight(1) 协作实现了"列表可滚动、结算栏固定"的经典电商布局。这种固定底部操作栏的模式在移动端电商应用中几乎成为标准,因为它保证了用户在任何滚动位置都能快速发起结算。


十一、订单 Tab 内容

11.1 ordersContent:订单卡片列表

@Builder
ordersContent() {
  Scroll() {
    Column() {
      Text('我的订单')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .margin({ top: 16, left: 16, bottom: 12 })
        .alignSelf(ItemAlign.Start)

      ForEach(ORDER_LIST, (order: OrderMeta) => {
        Column() {
          Row() {
            Text('订单 #' + order.id)
              .fontSize(12)
              .fontColor(COLORS.textHint)
            Column().layoutWeight(1)
            Text(order.status)
              .fontSize(12)
              .fontColor(getStatusColor(order.status))
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')

          Divider().color(COLORS.border).margin({ top: 8, bottom: 8 })

ordersContent Builder 构建了订单 Tab,顶部是"我的订单"标题,下方通过 ForEach 遍历 ORDER_LIST 渲染订单卡片。每个订单卡片是一个 Column,顶部是一个 Row,左侧是"订单 #3001"的订单编号(浅色弱化),右侧是订单状态文字,颜色通过 getStatusColor 函数动态映射——"已签收"为绿色、"已发货"为蓝色、"待发货"为橙色、"已取消"为浅紫灰。状态文字加粗显示,确保用户一眼即可识别。订单编号与状态之间用 layoutWeight(1) 隔开,形成左右分布。标题行下方是一条浅粉分隔线(Divider),上下各留 8vp 间距,将订单头部与商品信息区视觉分离。

11.2 订单商品与物流信息

          Row() {
            Text('📦 ' + order.items + ' (共' + order.count + '件)')
              .fontSize(13)
              .fontColor(COLORS.textPrimary)
              .layoutWeight(1)
          }
          .width('100%')

          Row() {
            Text('🚚 ' + order.logistics)
              .fontSize(11)
              .fontColor(COLORS.textSecondary)
            Column().layoutWeight(1)
            Text('📅 ' + order.date)
              .fontSize(11)
              .fontColor(COLORS.textHint)
          }
          .width('100%')
          .margin({ top: 6 })

商品信息行展示"📦 奶粉+纸尿裤 (共5件)"的概要描述,用包裹 Emoji 增加视觉辨识度,字号 13,主文本色。物流信息行左侧是"🚚 顺丰快递"的物流公司,右侧是"📅 2024-04-15"的下单日期,字号 11,分别使用次要与提示文本色弱化呈现。这两行信息通过 Emoji 前缀实现了"图标 + 文字"的轻量化信息展示,无需引入图标资源即可传达丰富的语义。物流与日期分列左右,信息密度高且布局平衡。

11.3 订单金额与操作按钮

          Row() {
            Text('合计')
              .fontSize(12)
              .fontColor(COLORS.textSecondary)
            Text(' ¥' + order.amount)
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.danger)
            Column().layoutWeight(1)
            if (order.status === '待发货') {
              Text('修改')
                .fontSize(11)
                .fontColor(COLORS.primaryDark)
                .onClick(() => {
                  this.showEditDialog = true
                })
              Text('  取消')
                .fontSize(11)
                .fontColor(COLORS.danger)
                .onClick(() => {
                  this.showDeleteDialog = true
                })
            }
          }
          .width('100%')
          .margin({ top: 8 })
        }
        .width('92%')
        .padding(14)
        .backgroundColor(COLORS.white)
        .borderRadius(14)
        .margin({ bottom: 8 })
        .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
      })
    }
    .width('100%')
    .padding({ bottom: 20 })
  }
  .width('100%')
  .height('100%')
  .scrollBar(BarState.Off)
}

订单卡片底部是金额与操作行。左侧是"合计 ¥830"的金额显示,"合计"二字用次要色弱化,金额数值用红色加粗 16 号字号突出。右侧通过 if (order.status === '待发货') 条件渲染,仅当订单状态为"待发货"时才显示"修改"与"取消"两个操作按钮——"修改"触发 showEditDialog 弹窗,"取消"触发 showDeleteDialog 弹窗。这种基于状态的差异化操作展示,体现了"按需呈现"的交互设计原则:已发货订单无法修改(物流已发出),已签收订单无需操作,已取消订单已是终态,只有待发货订单才需要提供修改与取消入口。订单卡片整体是白色圆角卡片,14vp 内边距,8vp 项间距,带轻微阴影。


十二、我的 Tab 内容

12.1 mineContent:用户档案与服务入口

@Builder
mineContent() {
  Scroll() {
    Column() {
      Row() {
        Stack() {
          Text('👶')
            .fontSize(32)
        }
        .width(60)
        .height(60)
        .borderRadius(30)
        .backgroundColor(COLORS.primaryLight)

        Column() {
          Text('宝妈用户')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Text('宝宝 8个月 · VIP会员')
            .fontSize(12)
            .fontColor(COLORS.textSecondary)
            .margin({ top: 4 })
          Text('积分 1280 · 优惠券 5张')
            .fontSize(12)
            .fontColor(COLORS.primaryDark)
            .margin({ top: 2 })
        }
        .margin({ left: 16 })
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .width('92%')
      .padding(16)
      .backgroundColor(COLORS.white)
      .borderRadius(16)
      .margin({ top: 16, bottom: 16 })
      .alignItems(VerticalAlign.Center)
      .shadow({ radius: 4, color: '#0D000000', offsetY: 2 })

mineContent Builder 构建了"我的"个人中心页面。顶部是用户档案卡片,左侧是 60×60 的圆形头像(浅粉背景 + “👶” Emoji),右侧是用户信息纵向排列:昵称"宝妈用户"(18 号加粗)、宝宝月龄与会员等级(“宝宝 8个月 · VIP会员”,次要色)、积分与优惠券(“积分 1280 · 优惠券 5张”,主色深色)。档案卡片使用 16vp 圆角、4vp 阴影半径(比商品卡片的 2vp 更深),突出了个人信息的主体地位。卡片内的文本层级通过字号(18/12/12)、字重(Bold/Regular/Regular)、颜色(主色/次要色/深玫红)三个维度的差异化,构建了清晰的信息优先级。

12.2 服务入口列表

      ForEach(['收货地址', '我的收藏', '优惠券', '积分商城', '宝宝档案', '客服中心'], (item: string) => {
        Row() {
          Text(item)
            .fontSize(14)
            .fontColor(COLORS.textPrimary)
          Column().layoutWeight(1)
          Text('›')
            .fontSize(20)
            .fontColor(COLORS.textHint)
        }
        .width('92%')
        .padding({ left: 16, right: 16, top: 14, bottom: 14 })
        .backgroundColor(COLORS.white)
        .borderRadius(12)
        .margin({ bottom: 8 })
        .alignItems(VerticalAlign.Center)
      })
    }
    .width('100%')
    .padding({ bottom: 20 })
  }
  .width('100%')
  .height('100%')
  .scrollBar(BarState.Off)
}

用户档案下方是六个服务入口的列表,通过 ForEach 遍历字符串数组直接渲染——这种"数据即视图"的方式在纯展示型列表中非常高效。每个入口项是一个 Row:左侧是服务名称(14 号字号,主文本色),右侧是"›"箭头指示符(20 号字号,提示色)。项与项之间以 8vp 间距分隔,每项是独立的白色圆角卡片,而非共用一个卡片容器——这种"卡片化列表"风格相比传统分组列表更具现代感与呼吸感。六个入口涵盖了地址管理、收藏、优惠、积分、宝宝信息、客服等母婴用户的高频服务需求,其中"宝宝档案"是母婴场景的特色入口,可用于记录宝宝的成长数据以获得个性化推荐。


十三、弹窗组件

13.1 addDialog:加入购物车确认弹窗

@Builder
addDialog() {
  Column() {
    Column() {
      Text('加入购物车')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)

      Row() {
        Text('商品名称')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('花王纸尿裤 L码')
          .fontSize(13)
          .fontColor(COLORS.textPrimary)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .padding({ top: 12, bottom: 12 })
      .border({ width: { bottom: 1 }, color: COLORS.border })

      Row() {
        Text('商品单价')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('¥98')
          .fontSize(13)
          .fontColor(COLORS.danger)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .padding({ top: 12, bottom: 12 })
      .border({ width: { bottom: 1 }, color: COLORS.border })

      Row() {
        Text('购买数量')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('3 包')
          .fontSize(13)
          .fontColor(COLORS.textPrimary)
      }
      .width('100%')
      .padding({ top: 12, bottom: 12 })
      .border({ width: { bottom: 1 }, color: COLORS.border })

      Row() {
        Text('合计金额')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('¥294')
          .fontSize(16)
          .fontColor(COLORS.danger)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .padding({ top: 12, bottom: 12 })

      Row() {
        Button('取消')
          .fontSize(14)
          .fontColor(COLORS.textSecondary)
          .backgroundColor(COLORS.white)
          .border({ width: 1, color: COLORS.border })
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .onClick(() => {
            this.showAddDialog = false
          })
        Button('加入购物车')
          .fontSize(14)
          .fontColor(COLORS.white)
          .backgroundColor(COLORS.primaryDark)
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .margin({ left: 12 })
          .onClick(() => {
            this.showAddDialog = false
          })
      }
      .width('100%')
      .margin({ top: 16 })
    }
    .width('80%')
    .padding(20)
    .backgroundColor(COLORS.white)
    .borderRadius(20)
    .onClick(() => {})
  }
  .width('100%')
  .height('100%')
  .backgroundColor('rgba(0,0,0,0.5)')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
}

addDialog Builder 构建了加入购物车的确认弹窗。弹窗的最外层是一个全屏 Column,背景色为半透明黑色(rgba(0,0,0,0.5))作为遮罩层,通过 justifyContent(FlexAlign.Center)alignItems(HorizontalAlign.Center) 将弹窗内容居中显示。弹窗主体是一个宽度 80%、白色背景、20vp 圆角的 Column,内含标题"加入购物车"与四行信息行——商品名称、商品单价、购买数量、合计金额,每行之间用浅粉底边框分隔。合计金额行使用 16 号字号红色加粗,视觉权重最高,引导用户关注最终支付金额。底部是"取消"与"加入购物车"两个按钮,各占一半宽度(layoutWeight(1)),取消按钮为白底带边框的次按钮样式,加入购物车按钮为深玫红底白字的主按钮样式,两个按钮的 onClick 回调都将 showAddDialog 设为 false 以关闭弹窗。内层 Column 上的 onClick(() => {}) 是一个空操作,其作用是阻止点击弹窗内部时事件冒泡到遮罩层,避免误关闭——这是一种常见的事件拦截技巧。

13.2 editDialog:修改订单弹窗

@Builder
editDialog() {
  Column() {
    Column() {
      Text('修改订单')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)
        .margin({ bottom: 16 })

      Row() {
        Text('订单编号')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('#3002')
          .fontSize(13)
          .fontColor(COLORS.textPrimary)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .padding({ top: 10, bottom: 10 })
      .border({ width: { bottom: 1 }, color: COLORS.border })

      Row() {
        Text('商品内容')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('连体衣+口水巾')
          .fontSize(13)
          .fontColor(COLORS.textPrimary)
      }
      .width('100%')
      .padding({ top: 10, bottom: 10 })
      .border({ width: { bottom: 1 }, color: COLORS.border })

      Row() {
        Text('收货地址')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('上海市浦东新区...')
          .fontSize(12)
          .fontColor(COLORS.textPrimary)
      }
      .width('100%')
      .padding({ top: 10, bottom: 10 })

      Row() {
        Button('取消')
          .fontSize(14)
          .fontColor(COLORS.textSecondary)
          .backgroundColor(COLORS.white)
          .border({ width: 1, color: COLORS.border })
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .onClick(() => {
            this.showEditDialog = false
          })
        Button('保存')
          .fontSize(14)
          .fontColor(COLORS.white)
          .backgroundColor(COLORS.primaryDark)
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .margin({ left: 12 })
          .onClick(() => {
            this.showEditDialog = false
          })
      }
      .width('100%')
      .margin({ top: 16 })
    }
    .width('80%')
    .padding(20)
    .backgroundColor(COLORS.white)
    .borderRadius(20)
    .onClick(() => {})
  }
  .width('100%')
  .height('100%')
  .backgroundColor('rgba(0,0,0,0.5)')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
}

editDialog Builder 构建了修改订单的弹窗,结构与加入购物车弹窗高度一致,但信息字段不同。弹窗展示订单编号(“#3002”)、商品内容(“连体衣+口水巾”)、收货地址(“上海市浦东新区…”)三项信息,其中收货地址以省略号结尾,暗示地址较长且可编辑。底部是"取消"与"保存"按钮,分别关闭弹窗与模拟保存操作。在真实业务中,"保存"按钮的回调应当先校验地址等字段的合法性,再调用后端接口更新订单,最后根据接口结果决定关闭弹窗或提示错误。这里为了原型演示,两个按钮都直接关闭弹窗。该弹窗的复用性体现在其布局结构与 addDialog 几乎相同——如果后续重构,可以抽取一个通用的"表单弹窗"Builder,通过参数传入标题、字段列表与按钮配置。

13.3 deleteDialog:取消订单确认弹窗

@Builder
deleteDialog() {
  Column() {
    Column() {
      Stack() {
        Text('⚠')
          .fontSize(40)
          .fontColor(COLORS.danger)
      }
      .width(64)
      .height(64)
      .borderRadius(32)
      .backgroundColor('#FFEBEE')
      .margin({ bottom: 16 })

      Text('取消订单确认')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS.textPrimary)

      Text('确定要取消此订单吗?取消后款项将在3-5个工作日内退回原支付账户。')
        .fontSize(13)
        .fontColor(COLORS.textSecondary)
        .textAlign(TextAlign.Center)
        .lineHeight(20)
        .margin({ top: 12 })

      Row() {
        Button('再想想')
          .fontSize(14)
          .fontColor(COLORS.textSecondary)
          .backgroundColor(COLORS.white)
          .border({ width: 1, color: COLORS.border })
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .onClick(() => {
            this.showDeleteDialog = false
          })
        Button('确认取消')
          .fontSize(14)
          .fontColor(COLORS.white)
          .backgroundColor(COLORS.danger)
          .borderRadius(10)
          .height(40)
          .layoutWeight(1)
          .margin({ left: 12 })
          .onClick(() => {
            this.showDeleteDialog = false
          })
      }
      .width('100%')
      .margin({ top: 20 })
    }
    .width('76%')
    .padding(24)
    .backgroundColor(COLORS.white)
    .borderRadius(20)
    .onClick(() => {})
  }
  .width('100%')
  .height('100%')
  .backgroundColor('rgba(0,0,0,0.5)')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
}

deleteDialog Builder 构建了取消订单的确认弹窗,与前两个弹窗的表单式布局不同,这里采用了"警示型"布局。弹窗顶部是一个 64×64 的圆形红色警告图标(浅红背景 #FFEBEE + 红色"⚠"符号),强烈警示用户此操作的严肃性。下方是"取消订单确认"标题与一段说明文字——“确定要取消此订单吗?取消后款项将在3-5个工作日内退回原支付账户。”,说明文字居中对齐,行高 20vp,确保多行文本的可读性。这段文案既明确了操作的不可逆性,又告知了退款时效,降低了用户的决策焦虑。底部是"再想想"与"确认取消"两个按钮——"再想想"是温和的取消措辞,使用次按钮样式;"确认取消"使用红色(COLORS.danger)背景的主按钮样式,色彩上的红色警示与操作的危险性相呼应。弹窗宽度 76%(比前两个弹窗的 80% 略窄),内边距 24vp,视觉上更紧凑聚焦。三个弹窗共同构成了应用的交互闭环:商品卡片触发加入购物车弹窗,待发货订单触发修改与取消弹窗。


十四、模块与组件特点对比

下表对应用中的各个功能模块与组件进行了多维度的横向对比,以便于理解各自的设计定位与技术特征。

模块/组件 核心职责 布局容器 数据来源 交互复杂度 视觉重点 状态依赖 可复用性
顶部标题栏 品牌标识与搜索入口 Row + 渐变背景 静态文本 低(仅搜索图标) 渐变色品牌色
Banner 轮播 营销活动展示 横向 Scroll + Row BANNER_LIST 中(横向滑动) 多色卡片节奏感
分类网格(首页) 快速品类导航 Flex Wrap CATEGORY_LIST 低(点击跳转) 彩色图标方块
商品瀑布流 商品浏览与加购 Flex Wrap + productCard PRODUCT_LIST 高(加购弹窗) 折扣角标与价格 showAddDialog
分类列表(分类页) 品类详情导航 Scroll + Row 列表 CATEGORY_LIST 低(点击跳转) 列表项箭头引导
购物车列表 商品选择与数量管理 Scroll + Row 列表 CART_LIST 高(选择与结算) 勾选状态与结算栏 selected 字段
底部结算栏 金额合计与结算入口 Row 固定底部 CART_LIST 计算 中(结算按钮) 红色金额与主色按钮 选中商品数据
订单列表 订单状态追踪与操作 Scroll + Column 卡片 ORDER_LIST 高(修改与取消) 状态色与条件按钮 showEdit/DeleteDialog
个人中心 用户档案与服务入口 Scroll + Row 列表 静态文本数组 低(点击跳转) 头像卡片与列表项
加入购物车弹窗 加购确认 Stack 遮罩 + Column 静态文本 中(确认/取消) 金额高亮 showAddDialog 高(可抽象)
修改订单弹窗 订单信息编辑 Stack 遮罩 + Column 静态文本 中(保存/取消) 表单分隔线 showEditDialog 高(可抽象)
取消订单弹窗 危险操作确认 Stack 遮罩 + Column 静态文本 中(确认/取消) 红色警示图标 showDeleteDialog 高(可抽象)
底部导航栏 Tab 页签切换 Row + ForEach TAB_CONFIG 中(切换页签) 选中态图标与文字 currentTab

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 250.ets - 母婴商城APP
// 主题色:婴儿粉 #F48FB1 + 天空蓝 #4FC3F7
// 布局:Banner轮播 + 分类网格 + 商品瀑布流 + 购物车列表 + 订单追踪

// ==================== 类型定义 ====================
interface ProductMeta {
  id: number
  name: string
  category: string
  price: number
  originalPrice: number
  sales: number
  stock: number
  imageColor: string
  tags: string[]
  ageRange: string
  rating: number
}

interface CartMeta {
  id: number
  productName: string
  price: number
  quantity: number
  imageColor: string
  selected: boolean
}

interface OrderMeta {
  id: number
  items: string
  amount: number
  status: string
  date: string
  count: number
  logistics: string
}

interface CategoryMeta {
  name: string
  icon: string
  color: string
  count: number
}

interface BannerMeta {
  title: string
  subtitle: string
  color: string
  icon: string
}

// ==================== 设计令牌 ====================
interface ColorPalette {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  accentDark: string;
  bg: string;
  cardBg: string;
  textPrimary: string;
  textSecondary: string;
  textHint: string;
  border: string;
  success: string;
  warning: string;
  danger: string;
  white: string;
}

const COLORS: ColorPalette = {
  primary: '#F48FB1',
  primaryLight: '#FCE4EC',
  primaryDark: '#EC407A',
  accent: '#4FC3F7',
  accentLight: '#E1F5FE',
  accentDark: '#0288D1',
  bg: '#FFF5F8',
  cardBg: '#FFFFFF',
  textPrimary: '#4A2C3A',
  textSecondary: '#9C7A8A',
  textHint: '#D4B5C4',
  border: '#FCE4EC',
  success: '#66BB6A',
  warning: '#FFA726',
  danger: '#EF5350',
  white: '#FFFFFF'
};

const TAB_CONFIG: Record<string, string> = {
  'home': '首页',
  'category': '分类',
  'cart': '购物车',
  'orders': '订单',
  'mine': '我的'
}

// ==================== 硬编码数据 ====================
const BANNER_LIST: BannerMeta[] = [
  { title: '宝宝换季大促', subtitle: '满200减50 · 全场包邮', color: '#F8BBD0', icon: '👶' },
  { title: '奶粉专区', subtitle: '海外直邮 · 正品保障', color: '#B3E5FC', icon: '🍼' },
  { title: '纸尿裤囤货', subtitle: '买2送1 · 限时特惠', color: '#FFE0B2', icon: '🧷' }
]

const CATEGORY_LIST: CategoryMeta[] = [
  { name: '奶粉辅食', icon: '🍼', color: '#FFE082', count: 128 },
  { name: '纸尿裤', icon: '🧷', color: '#FFAB91', count: 86 },
  { name: '宝宝服饰', icon: '👕', color: '#F48FB1', count: 215 },
  { name: '玩具早教', icon: '🧸', color: '#A5D6A7', count: 156 },
  { name: '洗护用品', icon: '🧴', color: '#90CAF9', count: 92 },
  { name: '喂养用品', icon: '🍽', color: '#CE93D8', count: 73 },
  { name: '孕产用品', icon: '🤰', color: '#FFCC80', count: 64 },
  { name: '推车座椅', icon: '🚼', color: '#80CBC4', count: 45 }
]

const PRODUCT_LIST: ProductMeta[] = [
  { id: 1, name: '荷兰牛栏奶粉 3段 900g', category: '奶粉辅食', price: 268, originalPrice: 328, sales: 5632, stock: 500, imageColor: '#FFE082', tags: ['海外直邮', '正品'], ageRange: '6-12月', rating: 4.9 },
  { id: 2, name: '花王纸尿裤 L码 54片', category: '纸尿裤', price: 98, originalPrice: 139, sales: 8721, stock: 800, imageColor: '#FFAB91', tags: ['日本进口', '透气'], ageRange: '9-14kg', rating: 4.8 },
  { id: 3, name: '纯棉婴儿连体衣 0-3月', category: '宝宝服饰', price: 59, originalPrice: 89, sales: 3456, stock: 300, imageColor: '#F48FB1', tags: ['A类纯棉', '无荧光'], ageRange: '0-3月', rating: 4.9 },
  { id: 4, name: '费雪益智积木 100粒', category: '玩具早教', price: 128, originalPrice: 199, sales: 2187, stock: 200, imageColor: '#A5D6A7', tags: ['益智', '环保材质'], ageRange: '1-3岁', rating: 4.7 },
  { id: 5, name: '婴儿洗发沐浴二合一 300ml', category: '洗护用品', price: 45, originalPrice: 68, sales: 4521, stock: 600, imageColor: '#90CAF9', tags: ['温和无刺激', '无泪配方'], ageRange: '0-3岁', rating: 4.8 },
  { id: 6, name: '硅胶安抚奶嘴 0-6月', category: '喂养用品', price: 29, originalPrice: 49, sales: 6789, stock: 1000, imageColor: '#CE93D8', tags: ['食品级硅胶', '仿母乳'], ageRange: '0-6月', rating: 4.6 },
  { id: 7, name: '婴儿推车 可折叠轻便', category: '推车座椅', price: 599, originalPrice: 899, sales: 892, stock: 80, imageColor: '#80CBC4', tags: ['一键折叠', '避震'], ageRange: '0-3岁', rating: 4.9 },
  { id: 8, name: '嘉宝米粉 1阶段 225g', category: '奶粉辅食', price: 35, originalPrice: 52, sales: 7821, stock: 700, imageColor: '#FFE082', tags: ['美国进口', '强化铁'], ageRange: '4-6月', rating: 4.8 },
  { id: 9, name: '宝宝口水巾 10条装', category: '宝宝服饰', price: 39, originalPrice: 69, sales: 3210, stock: 400, imageColor: '#F48FB1', tags: ['纯棉', '透气'], ageRange: '0-1岁', rating: 4.7 },
  { id: 10, name: '婴儿湿巾 80抽x5包', category: '洗护用品', price: 49, originalPrice: 79, sales: 9876, stock: 900, imageColor: '#90CAF9', tags: ['EDI纯水', '加厚'], ageRange: '全龄段', rating: 4.9 }
]

const CART_LIST: CartMeta[] = [
  { id: 1, productName: '荷兰牛栏奶粉 3段 900g', price: 268, quantity: 2, imageColor: '#FFE082', selected: true },
  { id: 2, productName: '花王纸尿裤 L码 54片', price: 98, quantity: 3, imageColor: '#FFAB91', selected: true },
  { id: 3, productName: '纯棉婴儿连体衣 0-3月', price: 59, quantity: 2, imageColor: '#F48FB1', selected: false },
  { id: 4, productName: '费雪益智积木 100粒', price: 128, quantity: 1, imageColor: '#A5D6A7', selected: true },
  { id: 5, productName: '婴儿洗发沐浴二合一', price: 45, quantity: 2, imageColor: '#90CAF9', selected: false }
]

const ORDER_LIST: OrderMeta[] = [
  { id: 3001, items: '奶粉+纸尿裤', amount: 830, status: '已发货', date: '2024-04-15', count: 5, logistics: '顺丰快递' },
  { id: 3002, items: '连体衣+口水巾', amount: 157, status: '待发货', date: '2024-04-15', count: 2, logistics: '中通快递' },
  { id: 3003, items: '益智积木+湿巾', amount: 177, status: '已签收', date: '2024-04-12', count: 2, logistics: '圆通快递' },
  { id: 3004, items: '推车', amount: 599, status: '已签收', date: '2024-04-08', count: 1, logistics: '德邦物流' },
  { id: 3005, items: '米粉+奶嘴', amount: 99, status: '已取消', date: '2024-04-05', count: 2, logistics: '--' },
  { id: 3006, items: '洗护套装', amount: 134, status: '已签收', date: '2024-04-01', count: 3, logistics: '韵达快递' }
]

function getDiscountPercent(original: number, current: number): number {
  return Math.floor((1 - current / original) * 100)
}

function getStatusColor(status: string): string {
  if (status === '已签收') {
    return COLORS.success
  }
  if (status === '已发货') {
    return COLORS.accent
  }
  if (status === '待发货') {
    return COLORS.warning
  }
  return COLORS.textHint
}

function getSelectedCartTotal(): number {
  let total: number = 0
  for (const c of CART_LIST) {
    if (c.selected) {
      total += c.price * c.quantity
    }
  }
  return total
}

function getSelectedCartCount(): number {
  let count: number = 0
  for (const c of CART_LIST) {
    if (c.selected) {
      count += c.quantity
    }
  }
  return count
}

function getRatingStars(rating: number): string {
  let stars: string = ''
  for (let i = 0; i < 5; i++) {
    if (i < rating) {
      stars += '★'
    } else {
      stars += '☆'
    }
  }
  return stars
}

// ==================== 入口组件 ====================
@Entry
@Component
struct BabyShopApp {
  @State currentTab: string = 'home'
  @State showAddDialog: boolean = false
  @State showEditDialog: boolean = false
  @State showDeleteDialog: boolean = false

  build() {
    Stack() {
      Column() {
        Row() {
          Text('👶 母婴商城')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.white)
          Column().layoutWeight(1)
          Text('🔍')
            .fontSize(20)
            .fontColor(COLORS.white)
        }
        .width('100%')
        .height(52)
        .padding({ left: 20, right: 20 })
        .linearGradient({ angle: 135, colors: [[COLORS.primaryDark, 0.0], [COLORS.primary, 1.0]] })

        Column() {
          if (this.currentTab === 'home') {
            this.homeContent()
          } else if (this.currentTab === 'category') {
            this.categoryContent()
          } else if (this.currentTab === 'cart') {
            this.cartContent()
          } else if (this.currentTab === 'orders') {
            this.ordersContent()
          } else {
            this.mineContent()
          }
        }
        .layoutWeight(1)
        .backgroundColor(COLORS.bg)

        this.bottomTabBar()
      }
      .width('100%')
      .height('100%')

      if (this.showAddDialog) {
        this.addDialog()
      }
      if (this.showEditDialog) {
        this.editDialog()
      }
      if (this.showDeleteDialog) {
        this.deleteDialog()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS.bg)
  }

  @Builder
  bottomTabBar() {
    Row() {
      ForEach(Object.keys(TAB_CONFIG), (key: string) => {
        Column() {
          Text(this.getTabIcon(key))
            .fontSize(20)
          Text(TAB_CONFIG[key])
            .fontSize(11)
            .margin({ top: 2 })
            .fontColor(this.currentTab === key ? COLORS.primaryDark : COLORS.textHint)
        }
        .layoutWeight(1)
        .height(56)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.currentTab = key
        })
      })
    }
    .width('100%')
    .height(56)
    .backgroundColor(COLORS.white)
    .border({ width: { top: 1 }, color: COLORS.border })
  }

  getTabIcon(key: string): string {
    if (key === 'home') {
      return this.currentTab === key ? '🏠' : '🏚'
    }
    if (key === 'category') {
      return this.currentTab === key ? '📋' : '📑'
    }
    if (key === 'cart') {
      return this.currentTab === key ? '🛒' : '🛍'
    }
    if (key === 'orders') {
      return this.currentTab === key ? '📦' : '📭'
    }
    return this.currentTab === key ? '👤' : '🙆'
  }

  // ========== 首页Tab ==========
  @Builder
  homeContent() {
    Scroll() {
      Column() {
        // Banner轮播
        Scroll() {
          Row() {
            ForEach(BANNER_LIST, (banner: BannerMeta) => {
              Row() {
                Column() {
                  Text(banner.title)
                    .fontSize(16)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.textPrimary)
                  Text(banner.subtitle)
                    .fontSize(12)
                    .fontColor(COLORS.textSecondary)
                    .margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                Text(banner.icon)
                  .fontSize(40)
              }
              .width(280)
              .height(100)
              .padding(16)
              .backgroundColor(banner.color)
              .borderRadius(16)
              .margin({ right: 10 })
              .alignItems(VerticalAlign.Center)
            })
          }
          .padding({ left: 16, right: 16 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')
        .height(116)
        .margin({ top: 12, bottom: 12 })

        // 分类网格
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(CATEGORY_LIST, (cat: CategoryMeta) => {
            Column() {
              Stack() {
                Text(cat.icon)
                  .fontSize(24)
              }
              .width(48)
              .height(48)
              .borderRadius(12)
              .backgroundColor(cat.color)
              Text(cat.name)
                .fontSize(11)
                .fontColor(COLORS.textPrimary)
                .margin({ top: 6 })
              Text(cat.count + '件')
                .fontSize(9)
                .fontColor(COLORS.textHint)
                .margin({ top: 2 })
            }
            .width('23%')
            .padding({ top: 10, bottom: 10 })
            .margin({ right: '2%', bottom: 8 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('92%')
        .margin({ bottom: 12 })

        // 热门商品
        Row() {
          Text('🔥 今日特卖')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.textPrimary)
          Column().layoutWeight(1)
          Text('更多 ›')
            .fontSize(12)
            .fontColor(COLORS.primaryDark)
        }
        .width('92%')
        .margin({ bottom: 12 })

        // 商品瀑布流(双列)
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(PRODUCT_LIST, (product: ProductMeta) => {
            this.productCard(product)
          })
        }
        .width('92%')
      }
      .width('100%')
      .padding({ bottom: 20 })
    }
    .width('100%')
    .height('100%')
    .scrollBar(BarState.Off)
  }

  @Builder
  productCard(product: ProductMeta) {
    Column() {
      Stack() {
        Column() {
          Text('🛍')
            .fontSize(32)
          Text('-' + getDiscountPercent(product.originalPrice, product.price) + '%')
            .fontSize(10)
            .fontColor(COLORS.white)
            .fontWeight(FontWeight.Bold)
            .padding({ left: 4, right: 4, top: 1, bottom: 1 })
            .backgroundColor(COLORS.danger)
            .borderRadius(4)
            .margin({ top: 4 })
        }
      }
      .width('100%')
      .height(90)
      .backgroundColor(product.imageColor)
      .borderRadius({ topLeft: 12, topRight: 12 })

      Column() {
        Text(product.name)
          .fontSize(12)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text('适用:' + product.ageRange)
          .fontSize(10)
          .fontColor(COLORS.textSecondary)
          .margin({ top: 4 })

        Row() {
          ForEach(product.tags, (tag: string) => {
            Text(tag)
              .fontSize(9)
              .padding({ left: 4, right: 4, top: 1, bottom: 1 })
              .backgroundColor(COLORS.primaryLight)
              .fontColor(COLORS.primaryDark)
              .borderRadius(3)
              .margin({ right: 3 })
          })
        }
        .margin({ top: 4 })

        Row() {
          Text('¥' + product.price)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS.danger)
          Text(' ¥' + product.originalPrice)
            .fontSize(10)
            .fontColor(COLORS.textHint)
            .decoration({ type: TextDecorationType.LineThrough })
          Column().layoutWeight(1)
          Text('已售' + product.sales)
            .fontSize(9)
            .fontColor(COLORS.textHint)
        }
        .width('100%')
        .margin({ top: 6 })
        .alignItems(VerticalAlign.Bottom)

        Row() {
          Text(getRatingStars(product.rating))
            .fontSize(10)
            .fontColor(COLORS.warning)
          Text(' ' + product.rating)
            .fontSize(10)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text('+ 购物车')
            .fontSize(10)
            .fontColor(COLORS.white)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            .backgroundColor(COLORS.primaryDark)
            .borderRadius(4)
            .onClick(() => {
              this.showAddDialog = true
            })
        }
        .width('100%')
        .margin({ top: 6 })
      }
      .padding(10)
      .alignItems(HorizontalAlign.Start)
    }
    .width('48%')
    .backgroundColor(COLORS.white)
    .borderRadius(12)
    .margin({ right: '2%', bottom: 10 })
    .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
  }

  // ========== 分类Tab ==========
  @Builder
  categoryContent() {
    Scroll() {
      Column() {
        Text('全部分类')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .margin({ top: 16, left: 16, bottom: 12 })
          .alignSelf(ItemAlign.Start)

        ForEach(CATEGORY_LIST, (cat: CategoryMeta) => {
          Row() {
            Stack() {
              Text(cat.icon)
                .fontSize(24)
            }
            .width(48)
            .height(48)
            .borderRadius(12)
            .backgroundColor(cat.color)

            Column() {
              Text(cat.name)
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.textPrimary)
              Text(cat.count + '件商品')
                .fontSize(11)
                .fontColor(COLORS.textSecondary)
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start)
            .margin({ left: 12 })
            .layoutWeight(1)

            Text('›')
              .fontSize(20)
              .fontColor(COLORS.textHint)
          }
          .width('92%')
          .padding(12)
          .backgroundColor(COLORS.white)
          .borderRadius(12)
          .margin({ bottom: 6 })
          .alignItems(VerticalAlign.Center)
          .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
        })
      }
      .width('100%')
      .padding({ bottom: 20 })
    }
    .width('100%')
    .height('100%')
    .scrollBar(BarState.Off)
  }

  // ========== 购物车Tab ==========
  @Builder
  cartContent() {
    Column() {
      Scroll() {
        Column() {
          ForEach(CART_LIST, (cart: CartMeta) => {
            Row() {
              Stack() {
                Text(cart.selected ? '✓' : '')
                  .fontSize(16)
                  .fontColor(COLORS.white)
                  .fontWeight(FontWeight.Bold)
              }
              .width(24)
              .height(24)
              .borderRadius(12)
              .backgroundColor(cart.selected ? COLORS.primaryDark : COLORS.border)

              Stack() {
                Text('🛍')
                  .fontSize(24)
              }
              .width(56)
              .height(56)
              .borderRadius(10)
              .backgroundColor(cart.imageColor)
              .margin({ left: 10 })

              Column() {
                Text(cart.productName)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS.textPrimary)
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Row() {
                  Text('¥' + cart.price)
                    .fontSize(15)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS.danger)
                  Column().layoutWeight(1)
                  Text('- ' + cart.quantity + ' +')
                    .fontSize(13)
                    .fontColor(COLORS.textPrimary)
                }
                .width('100%')
                .margin({ top: 6 })
                .alignItems(VerticalAlign.Bottom)
              }
              .layoutWeight(1)
              .margin({ left: 10 })
              .alignItems(HorizontalAlign.Start)
            }
            .width('92%')
            .padding(12)
            .backgroundColor(COLORS.white)
            .borderRadius(12)
            .margin({ bottom: 6 })
            .alignItems(VerticalAlign.Center)
            .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
          })
        }
        .width('100%')
        .padding({ top: 16, bottom: 80 })
      }
      .width('100%')
      .layoutWeight(1)
      .scrollBar(BarState.Off)

      // 底部结算栏
      Row() {
        Text('✓ 全选')
          .fontSize(12)
          .fontColor(COLORS.textSecondary)
        Column().layoutWeight(1)
        Text('合计:¥' + getSelectedCartTotal())
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.danger)
        Button('结算(' + getSelectedCartCount() + ')')
          .fontSize(14)
          .fontColor(COLORS.white)
          .backgroundColor(COLORS.primaryDark)
          .borderRadius(20)
          .height(36)
          .padding({ left: 20, right: 20 })
          .margin({ left: 12 })
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .backgroundColor(COLORS.white)
      .border({ width: { top: 1 }, color: COLORS.border })
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .height('100%')
  }

  // ========== 订单Tab ==========
  @Builder
  ordersContent() {
    Scroll() {
      Column() {
        Text('我的订单')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .margin({ top: 16, left: 16, bottom: 12 })
          .alignSelf(ItemAlign.Start)

        ForEach(ORDER_LIST, (order: OrderMeta) => {
          Column() {
            Row() {
              Text('订单 #' + order.id)
                .fontSize(12)
                .fontColor(COLORS.textHint)
              Column().layoutWeight(1)
              Text(order.status)
                .fontSize(12)
                .fontColor(getStatusColor(order.status))
                .fontWeight(FontWeight.Bold)
            }
            .width('100%')

            Divider().color(COLORS.border).margin({ top: 8, bottom: 8 })

            Row() {
              Text('📦 ' + order.items + ' (共' + order.count + '件)')
                .fontSize(13)
                .fontColor(COLORS.textPrimary)
                .layoutWeight(1)
            }
            .width('100%')

            Row() {
              Text('🚚 ' + order.logistics)
                .fontSize(11)
                .fontColor(COLORS.textSecondary)
              Column().layoutWeight(1)
              Text('📅 ' + order.date)
                .fontSize(11)
                .fontColor(COLORS.textHint)
            }
            .width('100%')
            .margin({ top: 6 })

            Row() {
              Text('合计')
                .fontSize(12)
                .fontColor(COLORS.textSecondary)
              Text(' ¥' + order.amount)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS.danger)
              Column().layoutWeight(1)
              if (order.status === '待发货') {
                Text('修改')
                  .fontSize(11)
                  .fontColor(COLORS.primaryDark)
                  .onClick(() => {
                    this.showEditDialog = true
                  })
                Text('  取消')
                  .fontSize(11)
                  .fontColor(COLORS.danger)
                  .onClick(() => {
                    this.showDeleteDialog = true
                  })
              }
            }
            .width('100%')
            .margin({ top: 8 })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(COLORS.white)
          .borderRadius(14)
          .margin({ bottom: 8 })
          .shadow({ radius: 2, color: '#0D000000', offsetY: 1 })
        })
      }
      .width('100%')
      .padding({ bottom: 20 })
    }
    .width('100%')
    .height('100%')
    .scrollBar(BarState.Off)
  }

  // ========== 我的Tab ==========
  @Builder
  mineContent() {
    Scroll() {
      Column() {
        Row() {
          Stack() {
            Text('👶')
              .fontSize(32)
          }
          .width(60)
          .height(60)
          .borderRadius(30)
          .backgroundColor(COLORS.primaryLight)

          Column() {
            Text('宝妈用户')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS.textPrimary)
            Text('宝宝 8个月 · VIP会员')
              .fontSize(12)
              .fontColor(COLORS.textSecondary)
              .margin({ top: 4 })
            Text('积分 1280 · 优惠券 5张')
              .fontSize(12)
              .fontColor(COLORS.primaryDark)
              .margin({ top: 2 })
          }
          .margin({ left: 16 })
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('92%')
        .padding(16)
        .backgroundColor(COLORS.white)
        .borderRadius(16)
        .margin({ top: 16, bottom: 16 })
        .alignItems(VerticalAlign.Center)
        .shadow({ radius: 4, color: '#0D000000', offsetY: 2 })

        ForEach(['收货地址', '我的收藏', '优惠券', '积分商城', '宝宝档案', '客服中心'], (item: string) => {
          Row() {
            Text(item)
              .fontSize(14)
              .fontColor(COLORS.textPrimary)
            Column().layoutWeight(1)
            Text('›')
              .fontSize(20)
              .fontColor(COLORS.textHint)
          }
          .width('92%')
          .padding({ left: 16, right: 16, top: 14, bottom: 14 })
          .backgroundColor(COLORS.white)
          .borderRadius(12)
          .margin({ bottom: 8 })
          .alignItems(VerticalAlign.Center)
        })
      }
      .width('100%')
      .padding({ bottom: 20 })
    }
    .width('100%')
    .height('100%')
    .scrollBar(BarState.Off)
  }

  // ==================== 弹窗 ====================
  @Builder
  addDialog() {
    Column() {
      Column() {
        Text('加入购物车')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)

        Row() {
          Text('商品名称')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text('花王纸尿裤 L码')
            .fontSize(13)
            .fontColor(COLORS.textPrimary)
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .border({ width: { bottom: 1 }, color: COLORS.border })

        Row() {
          Text('商品单价')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text('¥98')
            .fontSize(13)
            .fontColor(COLORS.danger)
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .border({ width: { bottom: 1 }, color: COLORS.border })

        Row() {
          Text('购买数量')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text('3 包')
            .fontSize(13)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .border({ width: { bottom: 1 }, color: COLORS.border })

        Row() {
          Text('合计金额')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text('¥294')
            .fontSize(16)
            .fontColor(COLORS.danger)
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })

        Row() {
          Button('取消')
            .fontSize(14)
            .fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.white)
            .border({ width: 1, color: COLORS.border })
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .onClick(() => {
              this.showAddDialog = false
            })
          Button('加入购物车')
            .fontSize(14)
            .fontColor(COLORS.white)
            .backgroundColor(COLORS.primaryDark)
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .margin({ left: 12 })
            .onClick(() => {
              this.showAddDialog = false
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('80%')
      .padding(20)
      .backgroundColor(COLORS.white)
      .borderRadius(20)
      .onClick(() => {})
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  editDialog() {
    Column() {
      Column() {
        Text('修改订单')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)
          .margin({ bottom: 16 })

        Row() {
          Text('订单编号')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text('#3002')
            .fontSize(13)
            .fontColor(COLORS.textPrimary)
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .padding({ top: 10, bottom: 10 })
        .border({ width: { bottom: 1 }, color: COLORS.border })

        Row() {
          Text('商品内容')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text('连体衣+口水巾')
            .fontSize(13)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding({ top: 10, bottom: 10 })
        .border({ width: { bottom: 1 }, color: COLORS.border })

        Row() {
          Text('收货地址')
            .fontSize(13)
            .fontColor(COLORS.textSecondary)
          Column().layoutWeight(1)
          Text('上海市浦东新区...')
            .fontSize(12)
            .fontColor(COLORS.textPrimary)
        }
        .width('100%')
        .padding({ top: 10, bottom: 10 })

        Row() {
          Button('取消')
            .fontSize(14)
            .fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.white)
            .border({ width: 1, color: COLORS.border })
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .onClick(() => {
              this.showEditDialog = false
            })
          Button('保存')
            .fontSize(14)
            .fontColor(COLORS.white)
            .backgroundColor(COLORS.primaryDark)
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .margin({ left: 12 })
            .onClick(() => {
              this.showEditDialog = false
            })
        }
        .width('100%')
        .margin({ top: 16 })
      }
      .width('80%')
      .padding(20)
      .backgroundColor(COLORS.white)
      .borderRadius(20)
      .onClick(() => {})
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  deleteDialog() {
    Column() {
      Column() {
        Stack() {
          Text('⚠')
            .fontSize(40)
            .fontColor(COLORS.danger)
        }
        .width(64)
        .height(64)
        .borderRadius(32)
        .backgroundColor('#FFEBEE')
        .margin({ bottom: 16 })

        Text('取消订单确认')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS.textPrimary)

        Text('确定要取消此订单吗?取消后款项将在3-5个工作日内退回原支付账户。')
          .fontSize(13)
          .fontColor(COLORS.textSecondary)
          .textAlign(TextAlign.Center)
          .lineHeight(20)
          .margin({ top: 12 })

        Row() {
          Button('再想想')
            .fontSize(14)
            .fontColor(COLORS.textSecondary)
            .backgroundColor(COLORS.white)
            .border({ width: 1, color: COLORS.border })
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .onClick(() => {
              this.showDeleteDialog = false
            })
          Button('确认取消')
            .fontSize(14)
            .fontColor(COLORS.white)
            .backgroundColor(COLORS.danger)
            .borderRadius(10)
            .height(40)
            .layoutWeight(1)
            .margin({ left: 12 })
            .onClick(() => {
              this.showDeleteDialog = false
            })
        }
        .width('100%')
        .margin({ top: 20 })
      }
      .width('76%')
      .padding(24)
      .backgroundColor(COLORS.white)
      .borderRadius(20)
      .onClick(() => {})
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}


十五、总结与技术反思

15.1 技术要点回顾

纵观整个母婴商城应用的实现,我们可以提炼出若干值得借鉴的技术要点。首先是声明式 UI 范式的深度运用——通过 @Entry@Component@State@Builder 四个核心装饰器,应用构建了一个状态驱动、Builder 分治的组件架构,build 函数仅负责骨架组装,具体内容下沉到各个 @Builder 方法,实现了关注点分离。其次是设计令牌体系的建立——ColorPalette 接口与 COLORS 常量构成了应用的视觉契约,所有颜色引用均通过语义化键名访问,杜绝了魔法字符串,为主题统一与未来暗黑模式适配奠定了基础。再次是数据建模的严谨性——五个 interface 明确界定了业务实体的字段结构,配合 Record 类型的配置映射与硬编码常量数组,形成了"类型-数据-配置"的三层数据架构。最后是纯函数的合理运用——折扣计算、状态颜色映射、购物车合计、星级生成等业务逻辑均封装为无副作用的纯函数,既保证了可测试性,又避免了 UI 层的逻辑污染。

15.2 设计思想剖析

在设计思想层面,这款应用体现了"原型优先、数据驱动、体验细腻"三大原则。"原型优先"体现在广泛使用 Emoji 字符与色块代替图片资源,在不引入外部依赖的前提下快速构建了可交互的高保真原型,这种做法在需求验证阶段具有极高的效率优势。"数据驱动"体现在 UI 渲染高度依赖数据遍历——ForEach 配合常量数组驱动了 Banner、分类、商品、购物车、订单、服务入口等几乎所有列表区域的渲染,新增数据项即可自动扩展 UI,无需修改布局代码。"体验细腻"体现在诸多细节处理上:商品名称的 maxLines(2) + 省略号保证了卡片高度一致;价格行的 alignItems(VerticalAlign.Bottom) 使现价与原价底部对齐;弹窗内层的空 onClick 阻止了事件冒泡;订单状态的条件按钮实现了"按需呈现"的操作引导;折扣百分比的 Math.floor 向下取整规避了合规风险——这些细节共同塑造了成熟、专业的产品质感。

15.3 可改进方向

尽管应用整体完成度较高,但仍存在若干可改进方向。其一,状态管理的响应式改造——当前购物车合计与件数计算依赖硬编码的 CART_LIST 常量,无法响应用户的选择与数量变更操作,应将购物车数据升级为 @State@Observed/@ObjectLink 响应式状态,使合计金额与结算按钮件数实时联动。其二,组件的进一步拆分——当前所有 Builder 集中在 BabyShopApp 一个结构体中,随着功能增长会导致文件膨胀,应将商品卡片、订单卡片、弹窗等抽取为独立的 @Component 自定义组件,通过 @Prop/@Link/@Provide/@Consume 管理跨组件状态。其三,导航架构的升级——当前 Tab 切换通过 if...else 条件渲染实现,每次切换都会重建组件、丢失滚动位置,应考虑使用 Navigation 组件或 Tabs 容器实现真正的页面级导航与状态保留。其四,真实资源的接入——Emoji 与色块应替换为真实的商品图片、矢量图标与品牌字体,并引入图片加载库实现懒加载与缓存。其五,无障碍与性能优化——应为交互元素补充 accessibilityText 等无障碍属性,为长列表引入 LazyForEach 实现按需加载,避免大数据量下的渲染性能瓶颈。

15.4 工程价值与展望

在这里插入图片描述

从"单组件多 Builder"到"多组件 + 状态管理"再到"Navigation 导航 + 分布式数据"的升级路线是平滑可行的,前期的类型定义、设计令牌、纯函数等基础设施可以无缝复用。应用所展示的"声明式 UI + 数据驱动 + 设计令牌"三位一体的开发模式,正是 HarmonyOS ArkTS 推荐的最佳实践,对于希望入门鸿蒙原生开发的工程师具有很好的参考价值。展望未来,随着鸿蒙生态的成熟与分布式能力的释放,这类垂直电商应用还可以进一步接入 HarmonyOS 的服务卡片、原子化服务、跨设备协同等特性,例如在智能手表上展示订单物流进度、在智慧屏上浏览商品大图、在车机上语音加购——这些场景都将得益于 ArkTS 统一的开发范式与鸿蒙的全场景分布式能力。希望本文的逐行解析能够帮助读者深入理解 HarmonyOS ArkTS 的开发模式,并在自己的项目中灵活运用这些设计思想与技术技巧。

更多推荐