引言

在这里插入图片描述

母婴电商赛道是一个对专业度、信任感和安全性要求极高的垂直领域。从婴儿奶粉的段位选择到疫苗接种的时间规划,从童装面料的 A 类标准到玩具的适龄推荐,每一个购买决策都承载着家长对宝宝健康成长的深切关注。如何在一个移动端应用中将商品展示、智能推荐、疫苗预约、消息推送等多元业务有机融合,同时保持界面的温暖柔和与交互的流畅自然,是本应用在 HarmonyOS 6.1.1 平台上需要解决的核心工程问题。

本应用以"母婴童趣城"为业务场景,构建了一个涵盖七大功能 Tab 的母婴综合服务平台。应用在数据层定义了 KidItem、MilkItem、ClothItem、ToyItem、VaccineItem、MessageItem 六组强类型接口,分别对应童品集市商品、婴儿奶粉、童装童鞋、玩具绘本、疫苗记录和站内消息。这些接口的设计紧贴母婴行业的专业参数体系——MilkItem 包含段位(stage)、产地(origin)、保质期(shelf)等奶粉专属字段,VaccineItem 包含月龄(month)、剂次(dose)、接种状态(status)和接种医院(hospital)等疫苗专属字段,每一条数据模型都是对真实业务实体的精确映射。

在视觉设计上,应用采用了与智能家电应用截然不同的色彩体系。主色调为柔和的天蓝色(#42A5F5)搭配粉色强调色(#FF80AB)和琥珀色辅助色(#FFD54F),整体背景为暖白色(#FFF9F5),营造出温馨、柔和、亲近的母婴视觉氛围。文字颜色以深棕色(#5D4037)和浅棕色(#BCAAA4)为主,与冷色调的科技感应用形成鲜明对比,更贴合母婴用户群体的审美偏好和情感诉求。

在架构层面,应用同样采用 Stack 根容器叠加主页面与弹窗模态的设计模式,但在弹窗类型上做了差异化设计。除了通用的发布闲置弹窗(modalPublish)、删除确认弹窗(modalDelete)和商品详情弹窗(modalDetail)外,应用专门为母婴场景设计了智能推荐弹窗(modalMilk)和疫苗预约弹窗(modalVaccine)。智能推荐弹窗集成了月龄步进器、段位选择和订阅罐数计算,实现了"输入宝宝月龄-推荐奶粉段位-计算订阅价格"的一体化流程;疫苗预约弹窗则提供了疫苗选择、时间选择和接种点展示,构成了完整的疫苗接种预约闭环。

逐段代码分析

一、母婴业务数据接口定义

在这里插入图片描述

应用首先定义了六组强类型接口,每组成员都精确映射了对应母婴业务实体的核心属性。与通用电商商品模型不同,这些接口融入了大量母婴行业的专业参数字段,体现了垂直领域应用对数据精度的特殊要求。

interface KidItem {
  name: string
  price: number
  sold: number
  quality: number
  category: string
  tag: string
}

interface MilkItem {
  name: string
  stage: number
  origin: string
  kg: number
  price: number
  shelf: number
}

interface ClothItem {
  name: string
  size: number
  fabric: string
  price: number
  rating: number
}

interface ToyItem {
  name: string
  ageMin: number
  type: string
  price: number
  stock: number
}

interface VaccineItem {
  name: string
  month: number
  dose: string
  status: string
  hospital: string
}

interface MessageItem {
  title: string
  content: string
  time: string
  unread: number
}

KidItem 作为童品集市的核心商品模型,结构与通用电商商品一致,包含名称、价格、销量、品质评分、品类和标签六个字段。MilkItem 专为婴儿奶粉建模,引入了 stage(段位,1-4段对应不同月龄段)、origin(产地,如德国、荷兰、新西兰)、kg(净重公斤数)和 shelf(保质期月数)四个奶粉专属字段,这些参数是家长选购奶粉时的核心决策依据。ClothItem 聚焦童装童鞋,包含 size(尺码码数)、fabric(面料材质,如"A 类纯棉")和 rating(好评率),其中 fabric 字段承载了"A 类"这一婴幼儿纺织品安全标准的表达。ToyItem 定义了 ageMin(最小适龄月龄)、type(玩具类型,如益智、绘本、运动)和 stock(库存量),ageMin 字段是适龄推荐功能的数据基础。

VaccineItem 是应用中最具行业特色的接口,包含了 name(疫苗名称)、month(接种月龄)、dose(剂次,如"第 3 剂")、status(接种状态:已完成/待接种/可预约/未到龄)和 hospital(接种医院)五个字段。status 字段的多值枚举设计使得疫苗列表可以按状态分色渲染,帮助家长一目了然地掌握宝宝的接种进度。这些接口的设计充分体现了垂直领域应用对业务理解的深度,每一个字段都对应着真实的育儿决策场景。

二、组件状态与业务配置声明

在这里插入图片描述

主组件 KidsCityPage 的状态管理设计体现了母婴应用的特殊交互需求。除了通用的 Tab 切换和弹窗显隐状态外,应用引入了 monthAge(宝宝月龄)和 selectedStage(推荐段位)两个特色状态,专门支撑奶粉智能推荐功能。

@Entry
@Component
struct KidsCityPage {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showMilk: boolean = false
  @State showVaccine: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: KidItem | null = null
  @State selectedCategory: number = 0
  @State selectedStage: number = 0
  @State selectedVaccine: number = 0
  @State selectedDate: number = 0
  @State qty: number = 1
  @State monthAge: number = 12
  @State inputName: string = ''

  private tabs: string[] = ['童品集市', '婴儿奶粉', '童装童鞋', '玩具绘本', '疫苗提醒', '消息', '我的']
  private categories: string[] = ['喂养用品', '洗护用品', '婴儿车床', '安全座椅', '纸尿裤', '辅食']
  private stages: string[] = ['1 段 0-6月', '2 段 6-12月', '3 段 12-36月', '4 段 3-6岁']
  private vaccineNames: string[] = ['乙肝疫苗', '脊灰疫苗', '百白破', '麻腮风', '流感疫苗', '水痘疫苗']
  private dateOptions: string[] = ['本周六 上午', '本周六 下午', '本周日 上午', '下周三 上午']

状态变量中,monthAge 初始值为 12(对应 1 岁宝宝),这是奶粉段位推荐的核心输入参数。当用户在智能推荐弹窗中调整月龄步进器时,monthAge 的变化会触发推荐段位的联动计算和订阅价格的实时更新。selectedStage 记录用户在弹窗中选择的奶粉段位索引,初始为 0(对应 1 段)。selectedVaccine 和 selectedDate 分别记录疫苗预约弹窗中选择的疫苗索引和时间索引,两者共同确定了预约的具体内容。

配置数据数组中,stages 数组定义了奶粉段位与月龄的映射关系:“1 段 0-6月”、“2 段 6-12月”、“3 段 12-36月”、“4 段 3-6岁”,这组映射关系是智能推荐功能的业务规则基础。vaccineNames 数组列出了六种常见疫苗名称,dateOptions 数组提供了四个预约时段选项(本周六上午/下午、本周日上午、下周三上午),这些配置数据直接驱动了疫苗预约弹窗的选择器渲染。

三、童品商品数据集

在这里插入图片描述

kids 数组是童品集市 Tab 的数据源,包含 24 条商品记录,覆盖了喂养用品、洗护用品、婴儿车床、安全座椅、纸尿裤和辅食六个品类。每条记录都携带完整的展示参数,价格从 39 元到 2399 元,覆盖了从辅食小件到安全座椅大件的全价格带。

private kids: KidItem[] = [
  { name: '宽口径玻璃奶瓶 240ml', price: 89, sold: 5231, quality: 95, category: '喂养用品', tag: '爆款' },
  { name: '防胀气奶瓶 PES 材质', price: 129, sold: 3042, quality: 93, category: '喂养用品', tag: '推荐' },
  { name: '恒温调奶器 316 不锈钢', price: 199, sold: 1876, quality: 96, category: '喂养用品', tag: '热卖' },
  { name: '硅胶围兜 一体成型', price: 45, sold: 4318, quality: 91, category: '喂养用品', tag: '实惠' },
  { name: '儿童餐椅 可折叠便携', price: 349, sold: 1234, quality: 94, category: '喂养用品', tag: '推荐' },
  { name: '婴儿洗发沐浴二合一', price: 59, sold: 2875, quality: 92, category: '洗护用品', tag: '热卖' },
  { name: '儿童防晒乳 SPF30', price: 79, sold: 2109, quality: 90, category: '洗护用品', tag: '新品' },
  { name: '婴儿抚触油 温和保湿', price: 69, sold: 1643, quality: 91, category: '洗护用品', tag: '推荐' },
  { name: '高景观婴儿车 可坐可躺', price: 1299, sold: 987, quality: 97, category: '婴儿车床', tag: '旗舰' },
  { name: '轻便伞车 4.9kg 一键收', price: 699, sold: 2054, quality: 93, category: '婴儿车床', tag: '爆款' },
  { name: '多功能婴儿床 实木拼接', price: 1699, sold: 654, quality: 96, category: '婴儿车床', tag: '推荐' },
  { name: '婴儿提篮 安全认证', price: 899, sold: 543, quality: 95, category: '安全座椅', tag: '新品' },
  { name: '0-4-12 岁安全座椅 360 旋转', price: 2399, sold: 876, quality: 98, category: '安全座椅', tag: '爆款' },
  { name: 'i-Size 认证座椅 增高垫', price: 799, sold: 1320, quality: 94, category: '安全座椅', tag: '实惠' },
  { name: '超薄纸尿裤 L 码 68 片', price: 99, sold: 8432, quality: 95, category: '纸尿裤', tag: '爆款' },
  { name: '拉拉裤 XL 码 56 片', price: 109, sold: 7654, quality: 94, category: '纸尿裤', tag: '热卖' },
  { name: '游泳纸尿裤 M 码 24 片', price: 39, sold: 3412, quality: 90, category: '纸尿裤', tag: '实惠' },
  { name: '婴儿米粉 原味强化铁', price: 68, sold: 4567, quality: 93, category: '辅食', tag: '爆款' },
  { name: '儿童面条 碎碎面组合', price: 49, sold: 3120, quality: 91, category: '辅食', tag: '热卖' },
  { name: '宝宝肉酥 猪肉松罐装', price: 39, sold: 2876, quality: 90, category: '辅食', tag: '实惠' },
  { name: '果泥条 无添加 6 支', price: 55, sold: 3987, quality: 92, category: '辅食', tag: '推荐' },
  { name: '磨牙饼干 米饼 2 罐', price: 42, sold: 2564, quality: 89, category: '辅食', tag: '实惠' },
  { name: '儿童酸奶 每日鲜 4 周装', price: 128, sold: 1543, quality: 92, category: '辅食', tag: '新品' },
  { name: 'DHA 藻油软糖 60 粒', price: 138, sold: 1876, quality: 95, category: '辅食', tag: '推荐' }
]

从数据分布来看,纸尿裤品类的销量最为突出——超薄纸尿裤 L 码 68 片以 8432 的销量高居榜首,拉拉裤 XL 码以 7654 紧随其后,这反映了纸尿裤作为高频消耗品的市场特征。安全座椅品类虽然销量相对较低(最高 876),但单品价格最高(2399 元),是典型的低频高客单商品。品质评分方面,0-4-12 岁安全座椅以 98 分位居品质榜首,体现了高端安全座椅的质量门槛。商品标签覆盖了爆款、推荐、热卖、新品、旗舰、实惠六种类型,在卡片渲染时以不同颜色和动画效果区分。

四、奶粉与疫苗专业数据集

在这里插入图片描述

milks 数组是婴儿奶粉 Tab 的数据源,包含 12 款奶粉记录,覆盖了 1 段到 3 段的完整段位范围,产地横跨德国、荷兰、新西兰、法国、爱尔兰、丹麦、澳大利亚以及国产的黑龙江、河北、内蒙古。

private milks: MilkItem[] = [
  { name: '爱他美卓萃幼儿配方', stage: 3, origin: '德国', kg: 1.8, price: 419, shelf: 24 },
  { name: '飞鹤星飞帆 3 段', stage: 3, origin: '黑龙江', kg: 1.5, price: 285, shelf: 24 },
  { name: '皇家美素佳儿 3 段', stage: 3, origin: '荷兰', kg: 1.8, price: 399, shelf: 24 },
  { name: 'a2 至初 2 段', stage: 2, origin: '新西兰', kg: 1.6, price: 449, shelf: 24 },
  { name: '君乐宝乐铂 2 段', stage: 2, origin: '河北', kg: 1.5, price: 176, shelf: 18 },
  { name: '合生元派星 2 段', stage: 2, origin: '法国', kg: 1.6, price: 368, shelf: 24 },
  { name: '惠氏启赋蓝钻 2 段', stage: 2, origin: '爱尔兰', kg: 1.7, price: 428, shelf: 24 },
  { name: '佳贝艾特羊奶粉 2 段', stage: 2, origin: '荷兰', kg: 1.5, price: 459, shelf: 24 },
  { name: '诺优蕴 1 段', stage: 1, origin: '荷兰', kg: 1.3, price: 319, shelf: 24 },
  { name: '贝拉米有机 1 段', stage: 1, origin: '澳大利亚', kg: 1.4, price: 389, shelf: 24 },
  { name: '伊利金领冠 1 段', stage: 2, origin: '内蒙古', kg: 1.2, price: 219, shelf: 18 },
  { name: '雅培菁挚有机 1 段', stage: 1, origin: '丹麦', kg: 1.3, price: 409, shelf: 24 }
]

vaccines 数组是疫苗提醒 Tab 的数据源,包含 10 条疫苗接种记录,覆盖了从 6 月龄到 48 月龄的完整接种时间线。每条记录的 status 字段取值为"已完成"、“待接种”、"可预约"或"未到龄"四种状态之一,这一字段直接驱动了疫苗列表项的状态标签分色渲染。

private vaccines: VaccineItem[] = [
  { name: '乙肝疫苗 第 3 针', month: 6, dose: '第 3 剂', status: '已完成', hospital: '社区服务中心' },
  { name: 'A 群流脑多糖疫苗', month: 6, dose: '第 1 剂', status: '已完成', hospital: '社区服务中心' },
  { name: '麻腮风疫苗', month: 8, dose: '第 1 剂', status: '待接种', hospital: '区妇幼保健院' },
  { name: '乙脑减毒活疫苗', month: 8, dose: '第 1 剂', status: '待接种', hospital: '区妇幼保健院' },
  { name: 'A 群流脑第 2 剂', month: 9, dose: '第 2 剂', status: '待接种', hospital: '社区服务中心' },
  { name: '水痘疫苗', month: 12, dose: '第 1 剂', status: '可预约', hospital: '儿童医院' },
  { name: '甲肝灭活疫苗', month: 18, dose: '第 1 剂', status: '可预约', hospital: '儿童医院' },
  { name: '百白破加强', month: 18, dose: '第 4 剂', status: '可预约', hospital: '社区服务中心' },
  { name: '脊灰加强针', month: 48, dose: '第 4 剂', status: '未到龄', hospital: '社区服务中心' },
  { name: '流感疫苗 每年一针', month: 36, dose: '年度', status: '可预约', hospital: '儿童医院' }
]

疫苗数据的 month 字段从 6 到 48,跨越了宝宝出生后半年到四岁的接种周期。status 的分布反映了 13 个月宝宝的真实接种进度:6 月龄的乙肝和流脑已完成,8-9 月龄的麻腮风、乙脑和流脑第二剂待接种,12 月龄以上的水痘、甲肝等可预约,48 月龄的脊灰加强尚未到龄。这种基于月龄的疫苗接种时间线设计,使得家长可以在一个视图中看到完整的接种规划和当前进度,极大地降低了信息获取成本。

五、数据计算辅助方法

在这里插入图片描述

应用定义了 maxStage、maxSold、maxAge、maxMonth 四个最大值计算方法,分别用于奶粉段位柱状图、商品销量参考、玩具适龄柱状图和疫苗月龄柱状图的归一化计算。

private maxStage(): number {
  let m: number = 0
  this.milks.forEach((m2: MilkItem) => {
    if (m2.stage > m) {
      m = m2.stage
    }
  })
  return m
}

private maxSold(): number {
  let m: number = 0
  this.kids.forEach((k: KidItem) => {
    if (k.sold > m) {
      m = k.sold
    }
  })
  return m
}

private maxAge(): number {
  let m: number = 0
  this.toys.forEach((t: ToyItem) => {
    if (t.ageMin > m) {
      m = t.ageMin
    }
  })
  return m
}

private maxMonth(): number {
  let m: number = 0
  this.vaccines.forEach((v: VaccineItem) => {
    if (v.month > m) {
      m = v.month
    }
  })
  return m
}

maxStage 返回 milks 数组中的最大段位值(3),用于奶粉段位柱状图中将每个段位值归一化为柱体高度。maxSold 返回 kids 数组中的最大销量值(8432),maxAge 返回 toys 数组中的最大适龄月龄(96),maxMonth 返回 vaccines 数组中的最大月龄(48)。这些方法的实现模式统一:初始化变量 m 为 0,遍历数组逐项比较更新最大值,最后返回。虽然在数据量较小时这种遍历方式的开销可以忽略,但在真实场景中如果商品列表动态加载且数量庞大,可以考虑在数据更新时缓存最大值,避免每次渲染都重新遍历。

sel 系列访问器方法(selName、selPrice、selCategory、selTag、selQuality、selSold)的设计与智能家电应用一致,统一处理 selectedItem 为 null 的边界情况。这些方法将空值判断收敛到方法内部,使得详情弹窗的 Builder 模板可以专注于布局和样式表达,不必在每一处数据访问点重复编写 if (this.selectedItem !== null) 的防御代码。

六、页面骨架与路由渲染

在这里插入图片描述

build 方法采用 Stack 作为根容器,主页面 Column 与五种弹窗模态叠加在同一视觉层级。主页面内部纵向排列 headerBar、可滚动内容区和 bottomBar,内容区根据 currentTab 动态切换七个 Tab 视图。

build() {
  Stack() {
    Column() {
      this.headerBar()
      Scroll() {
        Column() {
          if (this.currentTab === 0) {
            this.tabMarket()
          } else if (this.currentTab === 1) {
            this.tabMilk()
          } else if (this.currentTab === 2) {
            this.tabCloth()
          } else if (this.currentTab === 3) {
            this.tabToy()
          } else if (this.currentTab === 4) {
            this.tabVaccine()
          } else if (this.currentTab === 5) {
            this.tabMessage()
          } else {
            this.tabMine()
          }
        }
        .width('100%')
        .padding({ left: 12, right: 12, top: 10, bottom: 10 })
      }
      .layoutWeight(1)
      .width('100%')
      .scrollBar(BarState.Off)
      this.bottomBar()
    }
    .width('100%')
    .height('100%')

    if (this.showPublish) {
      this.modalPublish()
    }
    if (this.showMilk) {
      this.modalMilk()
    }
    if (this.showVaccine) {
      this.modalVaccine()
    }
    if (this.showDelete) {
      this.modalDelete()
    }
    if (this.showDetail) {
      this.modalDetail()
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#FFF9F5')
}

整体架构与智能家电应用保持一致,体现了 ArkUI 声明式范式在多应用间的架构复用性。差异之处在于背景色设置为暖白色(#FFF9F5),与母婴应用的温馨视觉定位匹配。弹窗模态的种类从家电应用的 publish/trade/warranty/delete/detail 变为母婴应用的 publish/milk/vaccine/delete/detail,其中 modalMilk 对应智能推荐弹窗(替代了延保弹窗),modalVaccine 对应疫苗预约弹窗(替代了以旧换新弹窗),这种弹窗类型的差异化设计精确反映了两个垂直领域的不同业务需求。

七、顶部导航栏与促销信息

headerBar 在母婴应用中承担了品牌展示、搜索入口、消息快捷导航和促销信息滚动展示的功能。与家电应用的深蓝色导航栏不同,母婴应用的导航栏采用天蓝色(#42A5F5),并在导航栏底部增加了一行促销信息文字。

@Builder
headerBar() {
  Column() {
    Row() {
      Column() {
        Text('母婴童趣城')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
      }
      Column() {
        Row() {
          Text('搜索奶粉 / 童装 / 玩具')
            .fontSize(12)
            .fontColor('#BCAAA4')
        }
        .width('100%')
        .height(30)
        .backgroundColor('#FFFFFF')
        .borderRadius(15)
        .justifyContent(FlexAlign.Start)
        .padding({ left: 14 })
      }
      .layoutWeight(1)
      .margin({ left: 12, right: 12 })
      Column() {
        Text('消息')
          .fontSize(13)
          .fontColor('#FFFFFF')
      }
      .onClick(() => {
        this.currentTab = 5
      })
    }
    .width('100%')
    .height(50)
    .padding({ left: 14, right: 14 })
    .alignItems(VerticalAlign.Center)

    Row() {
      Text('奶粉 8 折起 · 满 3 罐再减 60')
        .fontSize(12)
        .fontColor('#FFECB3')
        .fontWeight(FontWeight.Medium)
      Text('童装 2 件 75 折')
        .fontSize(12)
        .fontColor('#FFECB3')
        .margin({ left: 14 })
      Text('疫苗预约通道')
        .fontSize(12)
        .fontColor('#FFECB3')
        .margin({ left: 14 })
    }
    .width('100%')
    .padding({ left: 14, bottom: 8 })
    .justifyContent(FlexAlign.Start)
  }
  .width('100%')
  .backgroundColor('#42A5F5')
}

导航栏顶部的 Row 采用品牌标题、搜索框、消息入口的三列布局,搜索框占位文字改为"搜索奶粉 / 童装 / 玩具",直接反映母婴品类的搜索范围。底部促销信息行以浅琥珀色(#FFECB3)文字横向排列三条促销信息:奶粉折扣、童装折扣和疫苗预约通道。这种将促销信息嵌入导航栏底部的设计,使得用户在任何 Tab 页面都能看到当前的优惠活动,提升了促销信息的曝光率。值得注意的是"疫苗预约通道"作为促销信息出现在导航栏中,这在传统电商应用中是不常见的,体现了母婴应用将医疗服务与商品交易融合的特殊定位。

八、底部 Tab 导航栏

bottomBar 的设计与家电应用结构一致,但在配色上做了母婴化调整。选中态指示条和文字使用琥珀色(#FFD54F),非选中态使用半透明白色,整体视觉温暖柔和。

@Builder
bottomBar() {
  Row() {
    ForEach(this.tabs, (tab: string, index: number) => {
      Column() {
        Column()
          .width(index === this.currentTab ? 18 : 6)
          .height(3)
          .borderRadius(2)
          .backgroundColor(index === this.currentTab ? '#FFD54F' : 'rgba(255,255,255,0.4)')
        Text(tab)
          .fontSize(11)
          .fontColor(index === this.currentTab ? '#FFD54F' : 'rgba(255,255,255,0.75)')
          .margin({ top: 5 })
      }
      .layoutWeight(1)
      .height(54)
      .justifyContent(FlexAlign.Center)
      .onClick(() => {
        this.currentTab = index
      })
    })
  }
  .width('100%')
  .backgroundColor('#42A5F5')
}

七个 Tab 分别为童品集市、婴儿奶粉、童装童鞋、玩具绘本、疫苗提醒、消息和我的。与家电应用的 Tab 相比,母婴应用的 Tab 命名更加生活化和场景化——“童品集市"而非"数码集市”,“玩具绘本"而非"扫地机器人”,“疫苗提醒"而非"以旧换新”,这些命名差异直接反映了两个应用服务的人群和业务场景的根本不同。底部栏背景色与导航栏保持一致的天蓝色,形成了统一的顶部和底部视觉框架。非选中态文字颜色的透明度设为 0.75(高于家电应用的 0.6),使得未选中的 Tab 文字在浅色背景上仍保持较好的可读性。

九、童品集市 Tab 与商品卡片

tabMarket 是母婴应用的首屏 Tab,包含活动横幅、品类筛选横滑和商品列表三个区块。活动横幅以天蓝色卡片承载"母婴好物节"活动信息,强调正品溯源和会员日折扣。

@Builder
tabMarket() {
  Column() {
    Column() {
      Text('母婴好物节 · 妈妈放心购')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
      Row() {
        Text('全场正品溯源 · 缺一赔十')
          .fontSize(11)
          .fontColor('#FFECB3')
        Text('会员日奶粉 8 折')
          .fontSize(11)
          .fontColor('#FF80AB')
          .backgroundColor('#FFFFFF')
          .borderRadius(10)
          .padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .margin({ left: 10 })
          .scale({ x: 1.05, y: 1.05 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
      }
      .margin({ top: 8 })
    }
    .width('100%')
    .padding({ top: 18, bottom: 18, left: 16 })
    .backgroundColor('#42A5F5')
    .borderRadius(14)
    .alignItems(HorizontalAlign.Start)

    Scroll() {
      Row() {
        ForEach(this.categories, (c: string, idx: number) => {
          Text(c)
            .fontSize(12)
            .fontColor(this.selectedCategory === idx ? '#FFFFFF' : '#8D6E63')
            .backgroundColor(this.selectedCategory === idx ? '#FF80AB' : '#FFFFFF')
            .borderRadius(14)
            .padding({ left: 14, right: 14, top: 6, bottom: 6 })
            .margin({ right: 8 })
            .onClick(() => {
              this.selectedCategory = idx
            })
        })
      }
    }
    .scrollable(ScrollDirection.Horizontal)
    .scrollBar(BarState.Off)
    .width('100%')
    .margin({ top: 12 })

活动横幅中"会员日奶粉 8 折"标签使用了粉色文字(#FF80AB)配白色背景,与家电应用中青色强调色形成差异化。该标签应用了 1.05 倍循环缩放动画,吸引用户关注折扣信息。品类筛选横滑区与家电应用的品类横滑设计一致,但选中态颜色改为粉色(#FF80AB),非选中态文字颜色改为棕色(#8D6E63),背景为白色,整体色调更加柔和温暖。

商品卡片列表的布局结构与家电应用的数码集市卡片类似,但在颜色细节上做了母婴化调整。卡片占位图背景色改为浅粉色(#FFEBEE),商品名称文字颜色改为深棕色(#5D4037),品类标签使用粉色文字配浅粉色背景(#FCE4EC),营销标签使用深棕色文字配琥珀色背景(#FFD54F)。品质进度条的颜色从蓝色改为粉色(#FF80AB),背景条从灰色改为浅暖色(#F5EFE9),整体视觉温暖柔和,与母婴应用的视觉定位高度一致。

十、奶粉段位柱状图与智能推荐入口

tabMilk Tab 的核心亮点是顶部的奶粉段位柱状图和每款奶粉卡片上的"智能推荐"入口。柱状图展示各款奶粉的段位分布,帮助用户直观了解产品线覆盖情况。

@Builder
tabMilk() {
  Column() {
    Column() {
      Row() {
        Text('奶粉段位分布')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#5D4037')
        Text('1-4 段全覆盖')
          .fontSize(10)
          .fontColor('#BCAAA4')
          .margin({ left: 10 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        ForEach(this.milks, (m: MilkItem) => {
          Column() {
            Column()
              .width(15)
              .height(m.stage / this.maxStage() * 88)
              .backgroundColor('#FF80AB')
              .borderRadius({ topLeft: 3, topRight: 3 })
          }
          .height(92)
          .justifyContent(FlexAlign.End)
          .margin({ right: 5 })
        })
      }
      .width('100%')
      .alignItems(VerticalAlign.End)
      .margin({ top: 12 })
    }
    .width('100%')
    .padding(14)
    .backgroundColor('#FFFFFF')
    .borderRadius(14)
    .margin({ top: 4 })

段位柱状图的归一化计算为 m.stage / this.maxStage() * 88,其中 maxStage 返回 3。由于段位值只有 1、2、3 三种取值,柱体高度分别为 1/388≈29、2/388≈59、88,呈现为三个明显的高度阶梯,直观反映了不同奶粉产品的段位归属。柱体颜色统一为粉色(#FF80AB),在白色卡片背景上形成柔和的视觉对比。

奶粉列表中每款奶粉卡片右侧除了价格外,还提供了一个"智能推荐"按钮,点击后将 selectedStage 设为该奶粉的段位减 1(m.stage - 1),并弹出智能推荐弹窗。这里的 m.stage - 1 是一个索引转换操作:段位值 1-3 对应 stages 数组的索引 0-2,因此需要减 1 将段位值映射为数组索引。这种基于当前浏览奶粉的段位预填推荐弹窗的设计,减少了用户的操作步骤,提升了交互效率。

十一、童装尺码对照与好评率

tabCloth Tab 展示童装童鞋商品列表,顶部提供尺码对照与好评率柱状图。柱状图以蓝色柱体展示每款童装的好评率,帮助用户快速识别高品质商品。

@Builder
tabCloth() {
  Column() {
    Column() {
      Text('尺码对照 · 码数覆盖率 (%)')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#5D4037')
      Row() {
        ForEach(this.cloths, (c: ClothItem) => {
          Column() {
            Column()
              .width(16)
              .height(c.rating - 4)
              .backgroundColor('#42A5F5')
              .borderRadius({ topLeft: 3, topRight: 3 })
          }
          .height(96)
          .justifyContent(FlexAlign.End)
          .margin({ right: 5 })
        })
      }
      .width('100%')
      .alignItems(VerticalAlign.End)
      .margin({ top: 12 })
      Text('覆盖 59-130 码段 · A 类面料优先')
        .fontSize(10)
        .fontColor('#BCAAA4')
        .margin({ top: 6 })
    }
    .width('100%')
    .padding(14)
    .backgroundColor('#FFFFFF')
    .borderRadius(14)
    .margin({ top: 4 })

好评率柱状图的高度计算方式为 c.rating - 4,这是一种简化的归一化方案:好评率数据集中在 90-97 之间,直接减去 4 后得到 86-93 的柱体高度范围。虽然这种方案没有使用 max 归一化,但由于数据范围较窄,视觉效果依然合理。柱体颜色为蓝色(#42A5F5),与粉色奶粉柱状图形成色彩区分。柱状图底部标注"覆盖 59-130 码段 · A 类面料优先",传达了两个关键信息:尺码覆盖范围和面料安全标准。

童装列表项中每条记录展示名称、面料与尺码、好评率进度条和价格。好评率进度条采用与家电应用口碑进度条相同的双层 Stack 方案,但进度条颜色改为蓝色(#42A5F5),进度条背景改为浅暖色(#F5EFE9)。面料信息以"A 类纯棉"、“A 类夹棉”、"A 类莫代尔"等格式展示,其中"A 类"是中国婴幼儿纺织品安全标准的最高等级,这一信息的展示对家长选购具有极强的参考价值。每条童装卡片还标注了"7 天可退"标签,降低了家长的购买顾虑。

十二、玩具适龄推荐与瀑布流

tabToy Tab 展示玩具绘本商品,顶部提供适龄推荐柱状图,底部使用 Flex 瀑布流布局展示玩具卡片。柱状图以琥珀色柱体展示每款玩具的最小适龄月龄。

@Builder
tabToy() {
  Column() {
    Column() {
      Row() {
        Text('适龄推荐 (月起)')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Text('安全材质认证')
          .fontSize(10)
          .fontColor('#FFECB3')
          .margin({ left: 10 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      Row() {
        ForEach(this.toys, (t: ToyItem) => {
          Column() {
            Column()
              .width(14)
              .height(t.ageMin / this.maxAge() * 70 + 8)
              .backgroundColor('#FFD54F')
              .borderRadius({ topLeft: 3, topRight: 3 })
          }
          .height(82)
          .justifyContent(FlexAlign.End)
          .margin({ right: 5 })
        })
      }
      .width('100%')
      .alignItems(VerticalAlign.End)
      .margin({ top: 12 })
    }
    .width('100%')
    .padding(14)
    .backgroundColor('#5D4037')
    .borderRadius(14)
    .margin({ top: 4 })

适龄柱状图的归一化计算为 t.ageMin / this.maxAge() * 70 + 8,其中 maxAge 返回 96(8 岁)。与之前柱状图不同的是,这里在归一化结果上额外加了 8,作为最小柱体高度偏移量。这是考虑到部分玩具的 ageMin 为 0(如蒙氏早教布书适合 0 月+),如果直接使用 0/maxAge*70=0,柱体高度为 0 不可见,加 8 后最小柱体也有 8 的高度,保证视觉可见性。这种"归一化值+偏移量"的方案在数据包含 0 值时特别实用。柱体颜色为琥珀色(#FFD54F),背景为深棕色(#5D4037),暖色调组合契合玩具品类的活泼感。

玩具卡片使用 Flex 瀑布流双列布局,每张卡片展示适龄月龄标签、玩具类型、色块占位图、名称、价格和库存,底部提供"加入清单"入口。适龄月龄标签以"0月+"、“3月+”、"12月+"等格式展示,使用粉色背景配白色文字,帮助家长快速判断玩具是否适合自己宝宝的月龄。"加入清单"按钮应用了 1.05 倍循环缩放动画,点击后重置 qty 为 1 并弹出智能推荐弹窗(此处复用 modalMilk 弹窗作为购买清单入口)。

十三、疫苗接种计划总览与状态分色

tabVaccine Tab 是母婴应用中最具特色的 Tab,展示了宝宝的完整疫苗接种计划。顶部提供接种计划总览卡片,包含宝宝月龄信息、已完成剂次数和柱状图,底部列出每条疫苗记录及其状态标签。

@Builder
tabVaccine() {
  Column() {
    Column() {
      Row() {
        Column() {
          Text('接种计划总览')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('宝宝 13 个月 · 已完成 4 剂')
            .fontSize(11)
            .fontColor('#FFECB3')
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Text('预约')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#5D4037')
          .backgroundColor('#FFD54F')
          .borderRadius(16)
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
          .scale({ x: 1.06, y: 1.06 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          .onClick(() => {
            this.selectedVaccine = 0
            this.showVaccine = true
          })
      }
      .width('100%')
      .alignItems(VerticalAlign.Center)

      Row() {
        ForEach(this.vaccines, (v: VaccineItem) => {
          Column() {
            Column()
              .width(15)
              .height(v.month / this.maxMonth() * 70 + 6)
              .backgroundColor(v.status === '已完成' ? '#FFD54F' : '#4FC3F7')
              .borderRadius({ topLeft: 3, topRight: 3 })
          }
          .height(82)
          .justifyContent(FlexAlign.End)
          .margin({ right: 5 })
        })
      }
      .width('100%')
      .alignItems(VerticalAlign.End)
      .margin({ top: 12 })
      Text('接种月龄 (月) · 黄色为已完成')
        .fontSize(10)
        .fontColor('#FFECB3')
        .margin({ top: 6 })
    }
    .width('100%')
    .padding(14)
    .backgroundColor('#5D4037')
    .borderRadius(14)
    .margin({ top: 4 })

总览卡片左侧展示"宝宝 13 个月 · 已完成 4 剂"的摘要信息,右侧"预约"按钮使用琥珀色背景配深棕色文字,应用了 1.06 倍循环缩放动画。柱状图的设计颇具特色:柱体高度通过 v.month / this.maxMonth() * 70 + 6 计算(同样使用 +6 偏移量保证最小可见性),而柱体颜色根据 status 字段动态选择——已完成状态为琥珀色(#FFD54F),其他状态为浅蓝色(#4FC3F7)。这种基于数据属性的条件着色使得用户一眼就能从柱状图中区分已接种和未接种的疫苗,是数据可视化与业务语义融合的优秀实践。

疫苗列表项的状态标签渲染使用了 if-else if-else 三段条件渲染,针对三种不同的状态值分别渲染不同颜色的标签:

            Column() {
              if (v.status === '已完成') {
                Text(v.status)
                  .fontSize(11)
                  .fontColor('#42A5F5')
                  .backgroundColor('#E3F2FD')
                  .borderRadius(10)
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              } else if (v.status === '待接种') {
                Text(v.status)
                  .fontSize(11)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#FF80AB')
                  .borderRadius(10)
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .scale({ x: 1.05, y: 1.05 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              } else {
                Text(v.status)
                  .fontSize(11)
                  .fontColor('#8D6E63')
                  .backgroundColor('#F5EFE9')
                  .borderRadius(10)
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              }
              Text('去预约')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#42A5F5')
                .borderRadius(10)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .margin({ top: 6 })
                .onClick(() => {
                  this.selectedVaccine = 0
                  this.showVaccine = true
                })
            }
            .alignItems(HorizontalAlign.End)

"已完成"状态使用蓝色文字配浅蓝色背景,传达平静和确认感;"待接种"状态使用白色文字配粉色背景,并应用 1.05 倍循环缩放动画,通过动态效果提醒用户及时处理;"可预约"和"未到龄"状态统一使用棕色文字配浅暖色背景,传达中性的信息提示。这种基于业务语义的多级状态分色方案,使得复杂的疫苗数据变得清晰易读。每条疫苗记录下方还提供了"去预约"按钮,点击后弹出疫苗预约弹窗。

十四、智能推荐弹窗与月龄步进器

modalMilk 是智能推荐弹窗,集成了月龄步进器、段位选择器和订阅罐数计算,是母婴应用中最复杂的交互组件。弹窗根据用户输入的宝宝月龄推荐合适的奶粉段位,并实时计算订阅价格。

@Builder
modalMilk() {
  Column() {
    Column().layoutWeight(1).width('100%')
    Column() {
      Row() {
        Text('智能推荐')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#5D4037')
        Text('月龄 ' + this.monthAge + ' 个月')
          .fontSize(13)
          .fontColor('#42A5F5')
          .fontWeight(FontWeight.Bold)
          .scale({ x: 1.05, y: 1.05 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .alignItems(VerticalAlign.Center)

      Column() {
        Text('宝宝月龄')
          .fontSize(13)
          .fontColor('#8D6E63')
        Row() {
          Text('-')
            .fontSize(15)
            .fontColor('#8D6E63')
            .width(30)
            .height(30)
            .textAlign(TextAlign.Center)
            .backgroundColor('#F5EFE9')
            .borderRadius(6)
            .onClick(() => {
              if (this.monthAge > 0) {
                this.monthAge -= 1
              }
            })
          Text(this.monthAge + ' 月')
            .fontSize(14)
            .fontColor('#5D4037')
            .width(70)
            .height(30)
            .textAlign(TextAlign.Center)
          Text('+')
            .fontSize(15)
            .fontColor('#8D6E63')
            .width(30)
            .height(30)
            .textAlign(TextAlign.Center)
            .backgroundColor('#F5EFE9')
            .borderRadius(6)
            .onClick(() => {
              if (this.monthAge < 72) {
                this.monthAge += 1
              }
            })
        }
        .margin({ top: 8 })
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text('推荐段位')
          .fontSize(13)
          .fontColor('#8D6E63')
        Row() {
          ForEach(this.stages, (s: string, idx: number) => {
            Text(s)
              .fontSize(12)
              .fontColor(this.selectedStage === idx ? '#FFFFFF' : '#8D6E63')
              .backgroundColor(this.selectedStage === idx ? '#42A5F5' : '#F5EFE9')
              .borderRadius(8)
              .padding({ left: 14, right: 14, top: 8, bottom: 8 })
              .margin({ right: 8, top: 10 })
              .onClick(() => {
                this.selectedStage = idx
              })
          })
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(HorizontalAlign.Start)

      Row() {
        Text('订阅罐数')
          .fontSize(13)
          .fontColor('#8D6E63')
        Column().layoutWeight(1)
        Row() {
          Text('-')
            .fontSize(15)
            .fontColor('#8D6E63')
            .width(28)
            .height(28)
            .textAlign(TextAlign.Center)
            .backgroundColor('#F5EFE9')
            .borderRadius(6)
            .onClick(() => {
              if (this.qty > 1) {
                this.qty -= 1
              }
            })
          Text(this.qty + '')
            .fontSize(14)
            .fontColor('#5D4037')
            .width(36)
            .height(28)
            .textAlign(TextAlign.Center)
          Text('+')
            .fontSize(15)
            .fontColor('#8D6E63')
            .width(28)
            .height(28)
            .textAlign(TextAlign.Center)
            .backgroundColor('#F5EFE9')
            .borderRadius(6)
            .onClick(() => {
              if (this.qty < 12) {
                this.qty += 1
              }
            })
        }
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(VerticalAlign.Center)

      Text('订阅价:¥' + ((this.selectedStage + 1) * 96 * this.qty) + ' / 月,每期自动配送')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FF80AB')
        .margin({ top: 16 })

      Row() {
        Text('单次购买')
          .fontSize(14)
          .fontColor('#8D6E63')
          .layoutWeight(1)
          .height(42)
          .textAlign(TextAlign.Center)
          .backgroundColor('#F5EFE9')
          .borderRadius(21)
          .onClick(() => {
            this.showMilk = false
          })
        Text('开始订阅')
          .fontSize(14)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)
          .height(42)
          .textAlign(TextAlign.Center)
          .backgroundColor('#FF80AB')
          .borderRadius(21)
          .margin({ left: 10 })
          .onClick(() => {
            this.showMilk = false
          })
      }
      .width('100%')
      .margin({ top: 18 })
    }
    .width('100%')
    .constraintSize({ maxHeight: '80%' })
    .backgroundColor('#FFFFFF')
    .borderRadius({ topLeft: 16, topRight: 16 })
    .padding(16)
    .onClick((event: ClickEvent) => {
      event.stopPropagation()
    })
  }
  .width('100%')
  .height('100%')
  .backgroundColor('rgba(0,0,0,0.5)')
  .onClick(() => {
    this.showMilk = false
  })
}

月龄步进器是弹窗中的核心交互组件,减号按钮的边界检查为 if (this.monthAge > 0),确保月龄不为负数;加号按钮为 if (this.monthAge < 72),上限设为 72 个月(6 岁),覆盖了奶粉 1-4 段的完整月龄范围。中间显示"X 月"的文本,宽度设为 70 以容纳"72 月"这样的最长文本。月龄数值变化后,标题右侧的"月龄 X 个月"文本会自动更新,并应用了 1.05 倍循环缩放动画持续吸引视觉焦点。

订阅价格的计算公式为 (this.selectedStage + 1) * 96 * this.qty。段位索引加 1 后乘以 96 元(基础单价),再乘以订阅罐数。例如选择 3 段(索引 2)和 2 罐订阅,价格为 3 * 96 * 2 = 576 元/月。这个公式体现了"段位越高单价越高、罐数越多总价越高"的定价逻辑。弹窗底部提供"单次购买"和"开始订阅"两个操作,分别以浅暖色和粉色背景区分,形成主次操作按钮的视觉层次。

十五、疫苗预约弹窗

modalVaccine 是疫苗预约弹窗,采用居中弹出样式。弹窗提供疫苗选择、时间选择和接种点信息展示,是疫苗预约功能的完成节点。

@Builder
modalVaccine() {
  Column() {
    Column() {
      Text('疫苗预约')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#5D4037')
        .margin({ top: 18 })

      Column() {
        Text('选择疫苗')
          .fontSize(13)
          .fontColor('#8D6E63')
        Row() {
          ForEach(this.vaccineNames, (v: string, idx: number) => {
            Text(v)
              .fontSize(12)
              .fontColor(this.selectedVaccine === idx ? '#FFFFFF' : '#8D6E63')
              .backgroundColor(this.selectedVaccine === idx ? '#42A5F5' : '#F5EFE9')
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 8, bottom: 8 })
              .margin({ right: 8, top: 10 })
              .onClick(() => {
                this.selectedVaccine = idx
              })
          })
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text('接种时间')
          .fontSize(13)
          .fontColor('#8D6E63')
        Row() {
          ForEach(this.dateOptions, (d: string, idx: number) => {
            Text(d)
              .fontSize(12)
              .fontColor(this.selectedDate === idx ? '#FFFFFF' : '#8D6E63')
              .backgroundColor(this.selectedDate === idx ? '#FF80AB' : '#F5EFE9')
              .borderRadius(8)
              .padding({ left: 12, right: 12, top: 8, bottom: 8 })
              .margin({ right: 8, top: 10 })
              .onClick(() => {
                this.selectedDate = idx
              })
          })
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(HorizontalAlign.Start)

      Text('接种点:区妇幼保健院 2 号诊室')
        .fontSize(12)
        .fontColor('#42A5F5')
        .margin({ top: 14 })

      Text('提交预约')
        .fontSize(14)
        .fontColor('#FFFFFF')
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .height(42)
        .textAlign(TextAlign.Center)
        .backgroundColor('#42A5F5')
        .borderRadius(21)
        .margin({ top: 20, bottom: 18 })
        .onClick(() => {
          this.showVaccine = false
        })
    }
    .width('86%')
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .padding({ left: 18, right: 18 })
    .onClick((event: ClickEvent) => {
      event.stopPropagation()
    })
  }
  .width('100%')
  .height('100%')
  .backgroundColor('rgba(0,0,0,0.5)')
  .justifyContent(FlexAlign.Center)
  .onClick(() => {
    this.showVaccine = false
  })
}

疫苗选择器使用 vaccineNames 数组渲染六个疫苗选项标签,选中态为蓝色背景配白色文字,非选中态为浅暖色背景配棕色文字。时间选择器使用 dateOptions 数组渲染四个时段选项,选中态为粉色背景配白色文字,与疫苗选择器的蓝色选中态形成区分。这种"不同选择器使用不同选中色"的设计,帮助用户在视觉上区分当前正在选择的是疫苗还是时间。

接种点信息以蓝色文字静态展示"区妇幼保健院 2 号诊室",在实际应用中这一信息应根据用户选择的疫苗和接种地点动态获取。弹窗底部的"提交预约"按钮使用蓝色背景,点击后关闭弹窗。整个疫苗预约流程从疫苗列表的"去预约"入口触发,经过疫苗选择和时间选择,最终通过"提交预约"完成,构成了一个完整的医疗服务预约闭环。

十六、商品详情侧滑面板

modalDetail 是商品详情弹窗,采用从右侧滑入的面板样式,占据屏幕 78% 宽度。面板内部展示商品大图占位区、价格、名称、品类标签、品质评分进度条、销量信息、操作按钮和删除入口。

@Builder
modalDetail() {
  Row() {
    Scroll() {
      Column() {
        Column()
          .width('100%')
          .height(180)
          .backgroundColor('#FFEBEE')
          .borderRadius({ topLeft: 16, bottomLeft: 16 })

        Column() {
          Text('¥' + this.selPrice())
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FF80AB')
          Text(this.selName())
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#5D4037')
            .margin({ top: 8 })

          Row() {
            Text(this.selCategory())
              .fontSize(10)
              .fontColor('#FF80AB')
              .backgroundColor('#FCE4EC')
              .borderRadius(4)
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            Text(this.selTag())
              .fontSize(10)
              .fontColor('#5D4037')
              .backgroundColor('#FFD54F')
              .borderRadius(4)
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .margin({ left: 6 })
          }
          .margin({ top: 8 })

          Column() {
            Text('品质评分 ' + this.selQuality() + '%')
              .fontSize(12)
              .fontColor('#8D6E63')
            Stack({ alignContent: Alignment.Start }) {
              Column()
                .width('100%')
                .height(8)
                .backgroundColor('#F5EFE9')
                .borderRadius(4)
              Column()
                .width(this.selQuality() + '%')
                .height(8)
                .backgroundColor('#FF80AB')
                .borderRadius(4)
            }
            .width('100%')
            .margin({ top: 6 })
          }
          .width('100%')
          .alignItems(HorizontalAlign.Start)
          .margin({ top: 14 })

          Row() {
            Text('已售 ' + this.selSold())
              .fontSize(12)
              .fontColor('#BCAAA4')
            Text('正品溯源')
              .fontSize(12)
              .fontColor('#BCAAA4')
              .margin({ left: 12 })
            Text('极速退款')
              .fontSize(12)
              .fontColor('#42A5F5')
              .margin({ left: 12 })
          }
          .margin({ top: 12 })

          Row() {
            Text('加入清单')
              .fontSize(14)
              .fontColor('#42A5F5')
              .fontWeight(FontWeight.Bold)
              .layoutWeight(1)
              .height(40)
              .textAlign(TextAlign.Center)
              .backgroundColor('#E3F2FD')
              .borderRadius(20)
              .onClick(() => {
                this.qty = 1
                this.showMilk = true
              })
            Text('立即购买')
              .fontSize(14)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
              .layoutWeight(1)
              .height(40)
              .textAlign(TextAlign.Center)
              .backgroundColor('#FF80AB')
              .borderRadius(20)
              .margin({ left: 10 })
              .onClick(() => {
                this.showPublish = false
              })
          }
          .width('100%')
          .margin({ top: 18 })

          Row() {
            Text('删除该商品')
              .fontSize(13)
              .fontColor('#FF80AB')
              .padding({ top: 12 })
          }
          .width('100%')
          .onClick(() => {
            this.showDelete = true
          })
        }
        .width('100%')
        .padding(16)
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
    }
    .width('78%')
    .height('100%')
    .backgroundColor('#FFFFFF')
    .onClick((event: ClickEvent) => {
      event.stopPropagation()
    })

    Column()
      .layoutWeight(1)
      .height('100%')
      .onClick(() => {
        this.showDetail = false
      })
  }
  .width('100%')
  .height('100%')
  .backgroundColor('rgba(0,0,0,0.5)')
  .onClick(() => {
    this.showDetail = false
  })
}

详情面板的布局结构与家电应用一致,但在配色上做了母婴化调整。大图占位区背景色改为浅粉色(#FFEBEE),价格文字改为粉色(#FF80AB),商品名称改为深棕色(#5D4037),品质评分进度条改为粉色前景配浅暖色背景。服务标签展示了"正品溯源"和"极速退款"两项保障,其中"极速退款"使用蓝色文字突出,降低家长的购买风险感知。

底部操作区提供"加入清单"和"立即购买"两个入口。"加入清单"使用浅蓝色背景配蓝色文字,点击后重置 qty 并弹出智能推荐弹窗。"立即购买"使用粉色背景配白色文字,形成主操作按钮。"删除该商品"入口位于最底部,使用粉色文字,点击后触发确认删除弹窗。这种将购买、清单和删除操作集中在一个面板中的设计,为用户提供了完整的商品操作入口。

流程图

以下流程图展示了从商品浏览到疫苗预约的完整用户交互链路,涵盖了 Tab 切换、奶粉智能推荐、疫苗状态查看和预约提交等母婴应用特有的核心业务流程。

切换底部 Tab

点击商品卡片

加入清单

单次购买

开始订阅

立即购买

删除商品

进入疫苗提醒 Tab

已完成

待接种

可预约/未到龄

点击去预约

应用启动

渲染童品集市 Tab

用户操作

currentTab 更新

渲染对应 Tab

selectedItem 赋值

showDetail = true

弹出商品详情面板

详情面板操作

qty = 1

showMilk = true

弹出智能推荐弹窗

调整月龄步进器

选择推荐段位

设置订阅罐数

实时计算订阅价格

用户选择

showMilk = false

关闭详情面板

showDelete = true

弹出确认删除弹窗

展示接种计划总览

查看疫苗状态列表

疫苗状态

蓝色标签展示

粉色标签脉冲动画

棕色标签展示

selectedVaccine = 0

showVaccine = true

弹出疫苗预约弹窗

选择疫苗种类

选择接种时间

展示接种点信息

提交预约

showVaccine = false

技术点对比表格

技术维度童品集市商品卡片奶粉段位柱状图童装好评率柱状图玩具适龄柱状图疫苗月龄柱状图智能推荐弹窗疫苗预约弹窗
布局容器Row + ColumnRow + ColumnRow + ColumnRow + ColumnRow + ColumnColumn + RowColumn + Row
数据驱动KidItem 数组MilkItem.stageClothItem.ratingToyItem.ageMinVaccineItem.monthmonthAge, selectedStage, qtyselectedVaccine, selectedDate
归一化算法quality/100stage/maxStage*88rating-4ageMin/maxAge*70+8month/maxMonth*70+6(stage+1)96qty
条件着色status 决定颜色
动画效果tag 1.06 倍缩放加入清单 1.05 倍预约按钮 1.06 倍月龄文本 1.05 倍
状态分色三级 if-else 分色选中态蓝色疫苗蓝色/时间粉色
步进器月龄+罐数双步进器
弹窗触发showDetailshowMilkshowMilkshowVaccineshowMilk 自身showVaccine 自身
背景色白色卡片白色卡片白色卡片深棕色卡片深棕色卡片白色弹窗白色弹窗
柱体颜色无(进度条粉色)粉色 #FF80AB蓝色 #42A5F5琥珀色 #FFD54F琥珀/浅蓝双色

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

interface KidItem {
  name: string
  price: number
  sold: number
  quality: number
  category: string
  tag: string
}

interface MilkItem {
  name: string
  stage: number
  origin: string
  kg: number
  price: number
  shelf: number
}

interface ClothItem {
  name: string
  size: number
  fabric: string
  price: number
  rating: number
}

interface ToyItem {
  name: string
  ageMin: number
  type: string
  price: number
  stock: number
}

interface VaccineItem {
  name: string
  month: number
  dose: string
  status: string
  hospital: string
}

interface MessageItem {
  title: string
  content: string
  time: string
  unread: number
}

@Entry
@Component
struct KidsCityPage {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showMilk: boolean = false
  @State showVaccine: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: KidItem | null = null
  @State selectedCategory: number = 0
  @State selectedStage: number = 0
  @State selectedVaccine: number = 0
  @State selectedDate: number = 0
  @State qty: number = 1
  @State monthAge: number = 12
  @State inputName: string = ''

  private tabs: string[] = ['童品集市', '婴儿奶粉', '童装童鞋', '玩具绘本', '疫苗提醒', '消息', '我的']
  private categories: string[] = ['喂养用品', '洗护用品', '婴儿车床', '安全座椅', '纸尿裤', '辅食']
  private stages: string[] = ['1 段 0-6月', '2 段 6-12月', '3 段 12-36月', '4 段 3-6岁']
  private vaccineNames: string[] = ['乙肝疫苗', '脊灰疫苗', '百白破', '麻腮风', '流感疫苗', '水痘疫苗']
  private dateOptions: string[] = ['本周六 上午', '本周六 下午', '本周日 上午', '下周三 上午']

  private kids: KidItem[] = [
    { name: '宽口径玻璃奶瓶 240ml', price: 89, sold: 5231, quality: 95, category: '喂养用品', tag: '爆款' },
    { name: '防胀气奶瓶 PES 材质', price: 129, sold: 3042, quality: 93, category: '喂养用品', tag: '推荐' },
    { name: '恒温调奶器 316 不锈钢', price: 199, sold: 1876, quality: 96, category: '喂养用品', tag: '热卖' },
    { name: '硅胶围兜 一体成型', price: 45, sold: 4318, quality: 91, category: '喂养用品', tag: '实惠' },
    { name: '儿童餐椅 可折叠便携', price: 349, sold: 1234, quality: 94, category: '喂养用品', tag: '推荐' },
    { name: '婴儿洗发沐浴二合一', price: 59, sold: 2875, quality: 92, category: '洗护用品', tag: '热卖' },
    { name: '儿童防晒乳 SPF30', price: 79, sold: 2109, quality: 90, category: '洗护用品', tag: '新品' },
    { name: '婴儿抚触油 温和保湿', price: 69, sold: 1643, quality: 91, category: '洗护用品', tag: '推荐' },
    { name: '高景观婴儿车 可坐可躺', price: 1299, sold: 987, quality: 97, category: '婴儿车床', tag: '旗舰' },
    { name: '轻便伞车 4.9kg 一键收', price: 699, sold: 2054, quality: 93, category: '婴儿车床', tag: '爆款' },
    { name: '多功能婴儿床 实木拼接', price: 1699, sold: 654, quality: 96, category: '婴儿车床', tag: '推荐' },
    { name: '婴儿提篮 安全认证', price: 899, sold: 543, quality: 95, category: '安全座椅', tag: '新品' },
    { name: '0-4-12 岁安全座椅 360 旋转', price: 2399, sold: 876, quality: 98, category: '安全座椅', tag: '爆款' },
    { name: 'i-Size 认证座椅 增高垫', price: 799, sold: 1320, quality: 94, category: '安全座椅', tag: '实惠' },
    { name: '超薄纸尿裤 L 码 68 片', price: 99, sold: 8432, quality: 95, category: '纸尿裤', tag: '爆款' },
    { name: '拉拉裤 XL 码 56 片', price: 109, sold: 7654, quality: 94, category: '纸尿裤', tag: '热卖' },
    { name: '游泳纸尿裤 M 码 24 片', price: 39, sold: 3412, quality: 90, category: '纸尿裤', tag: '实惠' },
    { name: '婴儿米粉 原味强化铁', price: 68, sold: 4567, quality: 93, category: '辅食', tag: '爆款' },
    { name: '儿童面条 碎碎面组合', price: 49, sold: 3120, quality: 91, category: '辅食', tag: '热卖' },
    { name: '宝宝肉酥 猪肉松罐装', price: 39, sold: 2876, quality: 90, category: '辅食', tag: '实惠' },
    { name: '果泥条 无添加 6 支', price: 55, sold: 3987, quality: 92, category: '辅食', tag: '推荐' },
    { name: '磨牙饼干 米饼 2 罐', price: 42, sold: 2564, quality: 89, category: '辅食', tag: '实惠' },
    { name: '儿童酸奶 每日鲜 4 周装', price: 128, sold: 1543, quality: 92, category: '辅食', tag: '新品' },
    { name: 'DHA 藻油软糖 60 粒', price: 138, sold: 1876, quality: 95, category: '辅食', tag: '推荐' }
  ]

  private milks: MilkItem[] = [
    { name: '爱他美卓萃幼儿配方', stage: 3, origin: '德国', kg: 1.8, price: 419, shelf: 24 },
    { name: '飞鹤星飞帆 3 段', stage: 3, origin: '黑龙江', kg: 1.5, price: 285, shelf: 24 },
    { name: '皇家美素佳儿 3 段', stage: 3, origin: '荷兰', kg: 1.8, price: 399, shelf: 24 },
    { name: 'a2 至初 2 段', stage: 2, origin: '新西兰', kg: 1.6, price: 449, shelf: 24 },
    { name: '君乐宝乐铂 2 段', stage: 2, origin: '河北', kg: 1.5, price: 176, shelf: 18 },
    { name: '合生元派星 2 段', stage: 2, origin: '法国', kg: 1.6, price: 368, shelf: 24 },
    { name: '惠氏启赋蓝钻 2 段', stage: 2, origin: '爱尔兰', kg: 1.7, price: 428, shelf: 24 },
    { name: '佳贝艾特羊奶粉 2 段', stage: 2, origin: '荷兰', kg: 1.5, price: 459, shelf: 24 },
    { name: '诺优蕴 1 段', stage: 1, origin: '荷兰', kg: 1.3, price: 319, shelf: 24 },
    { name: '贝拉米有机 1 段', stage: 1, origin: '澳大利亚', kg: 1.4, price: 389, shelf: 24 },
    { name: '伊利金领冠 1 段', stage: 1, origin: '内蒙古', kg: 1.2, price: 219, shelf: 18 },
    { name: '雅培菁挚有机 1 段', stage: 1, origin: '丹麦', kg: 1.3, price: 409, shelf: 24 }
  ]

  private cloths: ClothItem[] = [
    { name: '纯棉连体衣 和尚服', size: 59, fabric: 'A 类纯棉', price: 69, rating: 96 },
    { name: '夹棉分体套装 冬款', size: 90, fabric: 'A 类夹棉', price: 139, rating: 95 },
    { name: '儿童机能鞋 学步', size: 130, fabric: '超纤鞋面', price: 189, rating: 97 },
    { name: '防走失背包 带绳', size: 100, fabric: '锦纶', price: 79, rating: 93 },
    { name: '夏季短袖套装 3 件', size: 110, fabric: '冰丝棉', price: 99, rating: 94 },
    { name: '女童纱裙 礼服款', size: 120, fabric: '网纱+棉里', price: 159, rating: 92 },
    { name: '男童运动卫衣 连帽', size: 130, fabric: '纯棉抓绒', price: 129, rating: 95 },
    { name: '儿童内裤 5 条装', size: 120, fabric: 'A 类莫代尔', price: 59, rating: 96 },
    { name: '防滑地板袜 6 双', size: 110, fabric: '毛圈棉', price: 49, rating: 91 },
    { name: '雨衣雨靴 套装', size: 115, fabric: 'EVA 无味', price: 89, rating: 90 }
  ]

  private toys: ToyItem[] = [
    { name: '蒙氏早教布书 6 册', ageMin: 0, type: '绘本', price: 89, stock: 432 },
    { name: '电动声光拨浪鼓', ageMin: 3, type: '哄睡', price: 39, stock: 560 },
    { name: '叠叠乐彩虹圈', ageMin: 6, type: '益智', price: 49, stock: 388 },
    { name: '儿童积木 100 粒装', ageMin: 12, type: '益智', price: 99, stock: 245 },
    { name: '磁力片 108 件套', ageMin: 36, type: '益智', price: 199, stock: 176 },
    { name: '绘本英文启蒙 20 册', ageMin: 24, type: '绘本', price: 129, stock: 312 },
    { name: '合金小汽车 12 辆', ageMin: 36, type: '车模', price: 79, stock: 421 },
    { name: '过家家厨房玩具组', ageMin: 36, type: '角色', price: 159, stock: 198 },
    { name: '儿童滑板车 闪光轮', ageMin: 36, type: '运动', price: 189, stock: 154 },
    { name: '儿童平衡车 12 寸', ageMin: 24, type: '运动', price: 299, stock: 87 },
    { name: '拼图 1000 片世界地图', ageMin: 72, type: '益智', price: 69, stock: 265 },
    { name: '科学实验套装 50 实验', ageMin: 96, type: '科教', price: 139, stock: 143 }
  ]

  private vaccines: VaccineItem[] = [
    { name: '乙肝疫苗 第 3 针', month: 6, dose: '第 3 剂', status: '已完成', hospital: '社区服务中心' },
    { name: 'A 群流脑多糖疫苗', month: 6, dose: '第 1 剂', status: '已完成', hospital: '社区服务中心' },
    { name: '麻腮风疫苗', month: 8, dose: '第 1 剂', status: '待接种', hospital: '区妇幼保健院' },
    { name: '乙脑减毒活疫苗', month: 8, dose: '第 1 剂', status: '待接种', hospital: '区妇幼保健院' },
    { name: 'A 群流脑第 2 剂', month: 9, dose: '第 2 剂', status: '待接种', hospital: '社区服务中心' },
    { name: '水痘疫苗', month: 12, dose: '第 1 剂', status: '可预约', hospital: '儿童医院' },
    { name: '甲肝灭活疫苗', month: 18, dose: '第 1 剂', status: '可预约', hospital: '儿童医院' },
    { name: '百白破加强', month: 18, dose: '第 4 剂', status: '可预约', hospital: '社区服务中心' },
    { name: '脊灰加强针', month: 48, dose: '第 4 剂', status: '未到龄', hospital: '社区服务中心' },
    { name: '流感疫苗 每年一针', month: 36, dose: '年度', status: '可预约', hospital: '儿童医院' }
  ]

  private messages: MessageItem[] = [
    { title: '疫苗提醒', content: '麻腮风疫苗已到接种月龄,请尽快预约', time: '09:41', unread: 1 },
    { title: '发货通知', content: '您的纸尿裤 3 箱已发货预计后天送达', time: '昨天', unread: 1 },
    { title: '奶粉保质期', content: '您囤的 2 段奶粉还有 45 天进入建议饮用期', time: '昨天', unread: 2 },
    { title: '成长曲线', content: '宝宝本月身高体重记录已生成', time: '08-22', unread: 0 },
    { title: '降价提醒', content: '您收藏的安全座椅降价 300 元', time: '08-21', unread: 0 },
    { title: '辅食食谱', content: '今日推荐:山药瘦肉粥 10M+', time: '08-20', unread: 1 },
    { title: '会员日', content: '周三母婴会员日 奶粉满 3 罐 8 折', time: '08-19', unread: 0 },
    { title: '物流更新', content: '您的童装包裹已到小区驿站', time: '08-18', unread: 0 },
    { title: '评价有礼', content: '晒单评价送宝宝口水巾一条', time: '08-17', unread: 1 },
    { title: '育儿课堂', content: '今晚 8 点直播:幼儿急疹家庭护理', time: '08-16', unread: 0 },
    { title: '预约成功', content: '周四上午儿童医院体检已预约成功', time: '08-15', unread: 0 },
    { title: '新品上架', content: '夏季新品儿童防晒帽 UVP50+ 上架', time: '08-14', unread: 0 }
  ]

  private maxStage(): number {
    let m: number = 0
    this.milks.forEach((m2: MilkItem) => {
      if (m2.stage > m) {
        m = m2.stage
      }
    })
    return m
  }

  private maxSold(): number {
    let m: number = 0
    this.kids.forEach((k: KidItem) => {
      if (k.sold > m) {
        m = k.sold
      }
    })
    return m
  }

  private maxAge(): number {
    let m: number = 0
    this.toys.forEach((t: ToyItem) => {
      if (t.ageMin > m) {
        m = t.ageMin
      }
    })
    return m
  }

  private maxMonth(): number {
    let m: number = 0
    this.vaccines.forEach((v: VaccineItem) => {
      if (v.month > m) {
        m = v.month
      }
    })
    return m
  }

  private selName(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.name
  }

  private selPrice(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.price
  }

  private selCategory(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.category
  }

  private selTag(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.tag
  }

  private selQuality(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.quality
  }

  private selSold(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.sold
  }

  build() {
    Stack() {
      Column() {
        this.headerBar()
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tabMarket()
            } else if (this.currentTab === 1) {
              this.tabMilk()
            } else if (this.currentTab === 2) {
              this.tabCloth()
            } else if (this.currentTab === 3) {
              this.tabToy()
            } else if (this.currentTab === 4) {
              this.tabVaccine()
            } else if (this.currentTab === 5) {
              this.tabMessage()
            } else {
              this.tabMine()
            }
          }
          .width('100%')
          .padding({ left: 12, right: 12, top: 10, bottom: 10 })
        }
        .layoutWeight(1)
        .width('100%')
        .scrollBar(BarState.Off)
        this.bottomBar()
      }
      .width('100%')
      .height('100%')

      if (this.showPublish) {
        this.modalPublish()
      }
      if (this.showMilk) {
        this.modalMilk()
      }
      if (this.showVaccine) {
        this.modalVaccine()
      }
      if (this.showDelete) {
        this.modalDelete()
      }
      if (this.showDetail) {
        this.modalDetail()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFF9F5')
  }

  @Builder
  headerBar() {
    Column() {
      Row() {
        Column() {
          Text('母婴童趣城')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
        }
        Column() {
          Row() {
            Text('搜索奶粉 / 童装 / 玩具')
              .fontSize(12)
              .fontColor('#BCAAA4')
          }
          .width('100%')
          .height(30)
          .backgroundColor('#FFFFFF')
          .borderRadius(15)
          .justifyContent(FlexAlign.Start)
          .padding({ left: 14 })
        }
        .layoutWeight(1)
        .margin({ left: 12, right: 12 })
        Column() {
          Text('消息')
            .fontSize(13)
            .fontColor('#FFFFFF')
        }
        .onClick(() => {
          this.currentTab = 5
        })
      }
      .width('100%')
      .height(50)
      .padding({ left: 14, right: 14 })
      .alignItems(VerticalAlign.Center)

      Row() {
        Text('奶粉 8 折起 · 满 3 罐再减 60')
          .fontSize(12)
          .fontColor('#FFECB3')
          .fontWeight(FontWeight.Medium)
        Text('童装 2 件 75 折')
          .fontSize(12)
          .fontColor('#FFECB3')
          .margin({ left: 14 })
        Text('疫苗预约通道')
          .fontSize(12)
          .fontColor('#FFECB3')
          .margin({ left: 14 })
      }
      .width('100%')
      .padding({ left: 14, bottom: 8 })
      .justifyContent(FlexAlign.Start)
    }
    .width('100%')
    .backgroundColor('#42A5F5')
  }

  @Builder
  bottomBar() {
    Row() {
      ForEach(this.tabs, (tab: string, index: number) => {
        Column() {
          Column()
            .width(index === this.currentTab ? 18 : 6)
            .height(3)
            .borderRadius(2)
            .backgroundColor(index === this.currentTab ? '#FFD54F' : 'rgba(255,255,255,0.4)')
          Text(tab)
            .fontSize(11)
            .fontColor(index === this.currentTab ? '#FFD54F' : 'rgba(255,255,255,0.75)')
            .margin({ top: 5 })
        }
        .layoutWeight(1)
        .height(54)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.currentTab = index
        })
      })
    }
    .width('100%')
    .backgroundColor('#42A5F5')
  }

  @Builder
  tabMarket() {
    Column() {
      Column() {
        Text('母婴好物节 · 妈妈放心购')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Row() {
          Text('全场正品溯源 · 缺一赔十')
            .fontSize(11)
            .fontColor('#FFECB3')
          Text('会员日奶粉 8 折')
            .fontSize(11)
            .fontColor('#FF80AB')
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 })
            .margin({ left: 10 })
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .margin({ top: 8 })
      }
      .width('100%')
      .padding({ top: 18, bottom: 18, left: 16 })
      .backgroundColor('#42A5F5')
      .borderRadius(14)
      .alignItems(HorizontalAlign.Start)

      Scroll() {
        Row() {
          ForEach(this.categories, (c: string, idx: number) => {
            Text(c)
              .fontSize(12)
              .fontColor(this.selectedCategory === idx ? '#FFFFFF' : '#8D6E63')
              .backgroundColor(this.selectedCategory === idx ? '#FF80AB' : '#FFFFFF')
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .margin({ right: 8 })
              .onClick(() => {
                this.selectedCategory = idx
              })
          })
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .margin({ top: 12 })

      Column() {
        ForEach(this.kids, (k: KidItem) => {
          Row() {
            Column()
              .width(86)
              .height(86)
              .backgroundColor('#FFEBEE')
              .borderRadius(12)
            Column() {
              Text(k.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#5D4037')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text(k.category)
                  .fontSize(10)
                  .fontColor('#FF80AB')
                  .backgroundColor('#FCE4EC')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text(k.tag)
                  .fontSize(10)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#FFD54F')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .margin({ left: 6 })
                  .scale({ x: 1.06, y: 1.06 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              }
              .margin({ top: 6 })

              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(6)
                  .backgroundColor('#F5EFE9')
                  .borderRadius(3)
                Column()
                  .width(k.quality + '%')
                  .height(6)
                  .backgroundColor('#FF80AB')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 8 })

              Row() {
                Text('¥' + k.price)
                  .fontSize(17)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FF80AB')
                Text('已售' + k.sold)
                  .fontSize(11)
                  .fontColor('#BCAAA4')
                  .margin({ left: 8 })
                Column().layoutWeight(1)
                Text('看详情')
                  .fontSize(11)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#FF80AB')
                  .borderRadius(10)
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              }
              .width('100%')
              .alignItems(VerticalAlign.Center)
              .margin({ top: 8 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(14)
          .padding(10)
          .margin({ top: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            this.selectedItem = k
            this.showDetail = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabMilk() {
    Column() {
      Column() {
        Row() {
          Text('奶粉段位分布')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#5D4037')
          Text('1-4 段全覆盖')
            .fontSize(10)
            .fontColor('#BCAAA4')
            .margin({ left: 10 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Row() {
          ForEach(this.milks, (m: MilkItem) => {
            Column() {
              Column()
                .width(15)
                .height(m.stage / this.maxStage() * 88)
                .backgroundColor('#FF80AB')
                .borderRadius({ topLeft: 3, topRight: 3 })
            }
            .height(92)
            .justifyContent(FlexAlign.End)
            .margin({ right: 5 })
          })
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .margin({ top: 4 })

      Column() {
        ForEach(this.milks, (m: MilkItem) => {
          Row() {
            Column() {
              Text(m.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#5D4037')
              Row() {
                Text(m.stage + ' 段')
                  .fontSize(10)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#FF80AB')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text(m.origin)
                  .fontSize(10)
                  .fontColor('#8D6E63')
                  .margin({ left: 8 })
                Text(m.kg + 'kg')
                  .fontSize(10)
                  .fontColor('#8D6E63')
                  .margin({ left: 8 })
                Text('保质' + m.shelf + '月')
                  .fontSize(10)
                  .fontColor('#8D6E63')
                  .margin({ left: 8 })
              }
              .margin({ top: 5 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Column() {
              Text('¥' + m.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FF80AB')
              Text('智能推荐')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#42A5F5')
                .borderRadius(10)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .margin({ top: 6 })
                .onClick(() => {
                  this.selectedStage = m.stage - 1
                  this.showMilk = true
                })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(14)
          .margin({ top: 10 })
          .alignItems(VerticalAlign.Center)
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabCloth() {
    Column() {
      Column() {
        Text('尺码对照 · 码数覆盖率 (%)')
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#5D4037')
        Row() {
          ForEach(this.cloths, (c: ClothItem) => {
            Column() {
              Column()
                .width(16)
                .height(c.rating - 4)
                .backgroundColor('#42A5F5')
                .borderRadius({ topLeft: 3, topRight: 3 })
            }
            .height(96)
            .justifyContent(FlexAlign.End)
            .margin({ right: 5 })
          })
        }
        .width('100%')
        .margin({ top: 12 })
        Text('覆盖 59-130 码段 · A 类面料优先')
          .fontSize(10)
          .fontColor('#BCAAA4')
          .margin({ top: 6 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .margin({ top: 4 })

      Column() {
        ForEach(this.cloths, (c: ClothItem) => {
          Column() {
            Row() {
              Column() {
                Text(c.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#5D4037')
                Text(c.fabric + ' · ' + c.size + ' 码')
                  .fontSize(11)
                  .fontColor('#BCAAA4')
                  .margin({ top: 3 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)

              Text('¥' + c.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FF80AB')
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

            Row() {
              Text('好评')
                .fontSize(10)
                .fontColor('#BCAAA4')
              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(6)
                  .backgroundColor('#F5EFE9')
                  .borderRadius(3)
                Column()
                  .width(c.rating + '%')
                  .height(6)
                  .backgroundColor('#42A5F5')
                  .borderRadius(3)
              }
              .width(110)
              .margin({ left: 6 })
              Text(c.rating + '%')
                .fontSize(10)
                .fontColor('#42A5F5')
                .fontWeight(FontWeight.Bold)
                .margin({ left: 6 })
              Column().layoutWeight(1)
              Text('7天可退')
                .fontSize(10)
                .fontColor('#FF80AB')
                .margin({ left: 6 })
            }
            .width('100%')
            .margin({ top: 8 })
            .alignItems(VerticalAlign.Center)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(14)
          .margin({ top: 10 })
          .onClick(() => {
            this.qty = 1
            this.showMilk = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabToy() {
    Column() {
      Column() {
        Row() {
          Text('适龄推荐 (月起)')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('安全材质认证')
            .fontSize(10)
            .fontColor('#FFECB3')
            .margin({ left: 10 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        Row() {
          ForEach(this.toys, (t: ToyItem) => {
            Column() {
              Column()
                .width(14)
                .height(t.ageMin / this.maxAge() * 70 + 8)
                .backgroundColor('#FFD54F')
                .borderRadius({ topLeft: 3, topRight: 3 })
            }
            .height(82)
            .justifyContent(FlexAlign.End)
            .margin({ right: 5 })
          })
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#5D4037')
      .borderRadius(14)
      .margin({ top: 4 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.toys, (t: ToyItem) => {
          Column() {
            Row() {
              Text(t.ageMin + '月+')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#FF80AB')
                .borderRadius(8)
                .padding({ left: 8, right: 8, top: 2, bottom: 2 })
              Text(t.type)
                .fontSize(10)
                .fontColor('#BCAAA4')
                .margin({ left: 8 })
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

            Column()
              .width('100%')
              .height(64)
              .backgroundColor('#FFF3E0')
              .borderRadius(10)
              .margin({ top: 8 })

            Text(t.name)
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#5D4037')
              .margin({ top: 6 })
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })

            Row() {
              Text('¥' + t.price)
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FF80AB')
              Text('库存' + t.stock)
                .fontSize(10)
                .fontColor('#BCAAA4')
                .margin({ left: 6 })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)
            .margin({ top: 6 })

            Text('加入清单')
              .fontSize(11)
              .fontColor('#FFFFFF')
              .backgroundColor('#FFD54F')
              .borderRadius(12)
              .padding({ left: 12, right: 12, top: 4, bottom: 4 })
              .margin({ top: 6 })
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
              .onClick(() => {
                this.qty = 1
                this.showMilk = true
              })
          }
          .width('48%')
          .backgroundColor('#FFFFFF')
          .borderRadius(14)
          .padding(10)
          .margin({ top: 10 })
          .alignItems(HorizontalAlign.Start)
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabVaccine() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('接种计划总览')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('宝宝 13 个月 · 已完成 4 剂')
              .fontSize(11)
              .fontColor('#FFECB3')
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          Text('预约')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#5D4037')
            .backgroundColor('#FFD54F')
            .borderRadius(16)
            .padding({ left: 16, right: 16, top: 8, bottom: 8 })
            .scale({ x: 1.06, y: 1.06 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            .onClick(() => {
              this.selectedVaccine = 0
              this.showVaccine = true
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Row() {
          ForEach(this.vaccines, (v: VaccineItem) => {
            Column() {
              Column()
                .width(15)
                .height(v.month / this.maxMonth() * 70 + 6)
                .backgroundColor(v.status === '已完成' ? '#FFD54F' : '#4FC3F7')
                .borderRadius({ topLeft: 3, topRight: 3 })
            }
            .height(82)
            .justifyContent(FlexAlign.End)
            .margin({ right: 5 })
          })
        }
        .width('100%')
        .margin({ top: 12 })
        Text('接种月龄 (月) · 黄色为已完成')
          .fontSize(10)
          .fontColor('#FFECB3')
          .margin({ top: 6 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#5D4037')
      .borderRadius(14)
      .margin({ top: 4 })

      Column() {
        ForEach(this.vaccines, (v: VaccineItem) => {
          Row() {
            Column() {
              Text(v.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#5D4037')
              Text(v.dose + ' · ' + v.month + ' 月龄 · ' + v.hospital)
                .fontSize(11)
                .fontColor('#BCAAA4')
                .margin({ top: 3 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)

            Column() {
              if (v.status === '已完成') {
                Text(v.status)
                  .fontSize(11)
                  .fontColor('#42A5F5')
                  .backgroundColor('#E3F2FD')
                  .borderRadius(10)
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              } else if (v.status === '待接种') {
                Text(v.status)
                  .fontSize(11)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#FF80AB')
                  .borderRadius(10)
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .scale({ x: 1.05, y: 1.05 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              } else {
                Text(v.status)
                  .fontSize(11)
                  .fontColor('#8D6E63')
                  .backgroundColor('#F5EFE9')
                  .borderRadius(10)
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
              }
              Text('去预约')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#42A5F5')
                .borderRadius(10)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .margin({ top: 6 })
                .onClick(() => {
                  this.selectedVaccine = 0
                  this.showVaccine = true
                })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(14)
          .margin({ top: 10 })
          .alignItems(VerticalAlign.Center)
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabMessage() {
    Column() {
      Row() {
        Text('消息中心')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#5D4037')
        Text('6条未读')
          .fontSize(11)
          .fontColor('#FF80AB')
          .margin({ left: 8 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .margin({ top: 4 })

      Column() {
        ForEach(this.messages, (m: MessageItem) => {
          Row() {
            Column() {
              Text(m.title.substring(0, 1))
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
            }
            .width(44)
            .height(44)
            .backgroundColor('#FF80AB')
            .borderRadius(22)
            .justifyContent(FlexAlign.Center)

            Column() {
              Row() {
                Text(m.title)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#5D4037')
                if (m.unread > 0) {
                  Text(m.unread + '')
                    .fontSize(10)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#FF80AB')
                    .borderRadius(8)
                    .padding({ left: 6, right: 6, top: 1, bottom: 1 })
                    .margin({ left: 6 })
                    .scale({ x: 1.1, y: 1.1 })
                    .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
                }
              }
              Text(m.content)
                .fontSize(12)
                .fontColor('#BCAAA4')
                .margin({ top: 3 })
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })

            Text(m.time)
              .fontSize(11)
              .fontColor('#D7CCC8')
          }
          .width('100%')
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(14)
          .margin({ top: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            m.unread = 0
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabMine() {
    Column() {
      Row() {
        Column()
          .width(64)
          .height(64)
          .backgroundColor('#FFCCBC')
          .borderRadius(10)
        Column() {
          Text('柠檬妈妈')
            .fontSize(17)
            .fontWeight(FontWeight.Bold)
            .fontColor('#5D4037')
          Text('PLUS 会员 · 宝宝 13 个月')
            .fontSize(11)
            .fontColor('#BCAAA4')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
        .padding({ left: 12 })
        Text('宝宝档案')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#FFD54F')
          .borderRadius(12)
          .padding({ left: 12, right: 12, top: 6, bottom: 6 })
          .scale({ x: 1.05, y: 1.05 })
          .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
          .onClick(() => {
            this.showVaccine = true
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .margin({ top: 4 })
      .alignItems(VerticalAlign.Center)

      Row() {
        Column() {
          Text('22')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#5D4037')
          Text('待收货')
            .fontSize(11)
            .fontColor('#BCAAA4')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('8')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#5D4037')
          Text('优惠券')
            .fontSize(11)
            .fontColor('#BCAAA4')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('64')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#5D4037')
          Text('收藏夹')
            .fontSize(11)
            .fontColor('#BCAAA4')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        Column() {
          Text('4')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#5D4037')
          Text('已接种')
            .fontSize(11)
            .fontColor('#BCAAA4')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding({ top: 14, bottom: 14 })
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .margin({ top: 10 })

      Column() {
        ForEach(['我的订单', '奶粉订阅', '接种记录', '成长曲线', '客服中心', '清除浏览记录'], (m: string, idx: number) => {
          Row() {
            Text(m)
              .fontSize(14)
              .fontColor('#5D4037')
            Column().layoutWeight(1)
            Text('>')
              .fontSize(14)
              .fontColor('#D7CCC8')
          }
          .width('100%')
          .padding({ top: 14, bottom: 14 })
          .border({ width: 1, color: '#F5EFE9' })
          .onClick(() => {
            if (idx === 5) {
              this.showDelete = true
            }
          })
        })
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(14)
      .padding({ left: 14, right: 14 })
      .margin({ top: 10 })
    }
    .width('100%')
  }

  @Builder
  modalPublish() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('发布闲置母婴用品')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#5D4037')
          Text('关闭')
            .fontSize(13)
            .fontColor('#BCAAA4')
            .padding(6)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Column() {
          Text('用品名称')
            .fontSize(13)
            .fontColor('#8D6E63')
          TextInput({ placeholder: '例如:婴儿车 九成新 已消毒' })
            .height(40)
            .fontSize(13)
            .backgroundColor('#F5EFE9')
            .borderRadius(8)
            .margin({ top: 6 })
            .onChange((value: string) => {
              this.inputName = value
            })
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('用品品类')
            .fontSize(13)
            .fontColor('#8D6E63')
          Row() {
            ForEach(this.categories, (c: string, idx: number) => {
              Text(c)
                .fontSize(12)
                .fontColor(this.selectedCategory === idx ? '#FFFFFF' : '#8D6E63')
                .backgroundColor(this.selectedCategory === idx ? '#FF80AB' : '#F5EFE9')
                .borderRadius(14)
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedCategory = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Text('预估回收价:¥' + ((this.selectedCategory + 1) * 49))
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FF80AB')
          .margin({ top: 18 })
          .scale({ x: 1.04, y: 1.04 })
          .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })

        Text('确认发布')
          .fontSize(15)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(44)
          .textAlign(TextAlign.Center)
          .backgroundColor('#FF80AB')
          .borderRadius(22)
          .margin({ top: 18 })
          .onClick(() => {
            this.showPublish = false
          })
      }
      .width('100%')
      .constraintSize({ maxHeight: '80%' })
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showPublish = false
    })
  }

  @Builder
  modalMilk() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('智能推荐')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#5D4037')
          Text('月龄 ' + this.monthAge + ' 个月')
            .fontSize(13)
            .fontColor('#42A5F5')
            .fontWeight(FontWeight.Bold)
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text('宝宝月龄')
            .fontSize(13)
            .fontColor('#8D6E63')
          Row() {
            Text('-')
              .fontSize(15)
              .fontColor('#8D6E63')
              .width(30)
              .height(30)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5EFE9')
              .borderRadius(6)
              .onClick(() => {
                if (this.monthAge > 0) {
                  this.monthAge -= 1
                }
              })
            Text(this.monthAge + ' 月')
              .fontSize(14)
              .fontColor('#5D4037')
              .width(70)
              .height(30)
              .textAlign(TextAlign.Center)
            Text('+')
              .fontSize(15)
              .fontColor('#8D6E63')
              .width(30)
              .height(30)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5EFE9')
              .borderRadius(6)
              .onClick(() => {
                if (this.monthAge < 72) {
                  this.monthAge += 1
                }
              })
          }
          .margin({ top: 8 })
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('推荐段位')
            .fontSize(13)
            .fontColor('#8D6E63')
          Row() {
            ForEach(this.stages, (s: string, idx: number) => {
              Text(s)
                .fontSize(12)
                .fontColor(this.selectedStage === idx ? '#FFFFFF' : '#8D6E63')
                .backgroundColor(this.selectedStage === idx ? '#42A5F5' : '#F5EFE9')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 10 })
                .onClick(() => {
                  this.selectedStage = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Row() {
          Text('订阅罐数')
            .fontSize(13)
            .fontColor('#8D6E63')
          Column().layoutWeight(1)
          Row() {
            Text('-')
              .fontSize(15)
              .fontColor('#8D6E63')
              .width(28)
              .height(28)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5EFE9')
              .borderRadius(6)
              .onClick(() => {
                if (this.qty > 1) {
                  this.qty -= 1
                }
              })
            Text(this.qty + '')
              .fontSize(14)
              .fontColor('#5D4037')
              .width(36)
              .height(28)
              .textAlign(TextAlign.Center)
            Text('+')
              .fontSize(15)
              .fontColor('#8D6E63')
              .width(28)
              .height(28)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F5EFE9')
              .borderRadius(6)
              .onClick(() => {
                if (this.qty < 12) {
                  this.qty += 1
                }
              })
          }
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(VerticalAlign.Center)

        Text('订阅价:¥' + ((this.selectedStage + 1) * 96 * this.qty) + ' / 月,每期自动配送')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FF80AB')
          .margin({ top: 16 })

        Row() {
          Text('单次购买')
            .fontSize(14)
            .fontColor('#8D6E63')
            .layoutWeight(1)
            .height(42)
            .textAlign(TextAlign.Center)
            .backgroundColor('#F5EFE9')
            .borderRadius(21)
            .onClick(() => {
              this.showMilk = false
            })
          Text('开始订阅')
            .fontSize(14)
            .fontColor('#FFFFFF')
            .fontWeight(FontWeight.Bold)
            .layoutWeight(1)
            .height(42)
            .textAlign(TextAlign.Center)
            .backgroundColor('#FF80AB')
            .borderRadius(21)
            .margin({ left: 10 })
            .onClick(() => {
              this.showMilk = false
            })
        }
        .width('100%')
        .margin({ top: 18 })
      }
      .width('100%')
      .constraintSize({ maxHeight: '80%' })
      .backgroundColor('#FFFFFF')
      .borderRadius({ topLeft: 16, topRight: 16 })
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showMilk = false
    })
  }

  @Builder
  modalVaccine() {
    Column() {
      Column() {
        Text('疫苗预约')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#5D4037')
          .margin({ top: 18 })

        Column() {
          Text('选择疫苗')
            .fontSize(13)
            .fontColor('#8D6E63')
          Row() {
            ForEach(this.vaccineNames, (v: string, idx: number) => {
              Text(v)
                .fontSize(12)
                .fontColor(this.selectedVaccine === idx ? '#FFFFFF' : '#8D6E63')
                .backgroundColor(this.selectedVaccine === idx ? '#42A5F5' : '#F5EFE9')
                .borderRadius(14)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 10 })
                .onClick(() => {
                  this.selectedVaccine = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('接种时间')
            .fontSize(13)
            .fontColor('#8D6E63')
          Row() {
            ForEach(this.dateOptions, (d: string, idx: number) => {
              Text(d)
                .fontSize(12)
                .fontColor(this.selectedDate === idx ? '#FFFFFF' : '#8D6E63')
                .backgroundColor(this.selectedDate === idx ? '#FF80AB' : '#F5EFE9')
                .borderRadius(8)
                .padding({ left: 12, right: 12, top: 8, bottom: 8 })
                .margin({ right: 8, top: 10 })
                .onClick(() => {
                  this.selectedDate = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Text('接种点:区妇幼保健院 2 号诊室')
          .fontSize(12)
          .fontColor('#42A5F5')
          .margin({ top: 14 })

        Text('提交预约')
          .fontSize(14)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(42)
          .textAlign(TextAlign.Center)
          .backgroundColor('#42A5F5')
          .borderRadius(21)
          .margin({ top: 20, bottom: 18 })
          .onClick(() => {
            this.showVaccine = false
          })
      }
      .width('86%')
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .padding({ left: 18, right: 18 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showVaccine = false
    })
  }

  @Builder
  modalDelete() {
    Column() {
      Column() {
        Text('!')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .width(56)
          .height(56)
          .textAlign(TextAlign.Center)
          .backgroundColor('#FF80AB')
          .borderRadius(28)
        Text('确认删除')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#5D4037')
          .margin({ top: 14 })
        Text('删除后浏览记录不可恢复,是否继续?')
          .fontSize(13)
          .fontColor('#BCAAA4')
          .margin({ top: 8 })

        Row() {
          Text('取消')
            .fontSize(14)
            .fontColor('#8D6E63')
            .layoutWeight(1)
            .height(40)
            .textAlign(TextAlign.Center)
            .backgroundColor('#F5EFE9')
            .borderRadius(20)
            .onClick(() => {
              this.showDelete = false
            })
          Text('确认删除')
            .fontSize(14)
            .fontColor('#FFFFFF')
            .layoutWeight(1)
            .height(40)
            .textAlign(TextAlign.Center)
            .backgroundColor('#FF80AB')
            .borderRadius(20)
            .margin({ left: 10 })
            .onClick(() => {
              this.showDelete = false
              this.showDetail = false
            })
        }
        .width('100%')
        .margin({ top: 22, bottom: 20 })
      }
      .width('78%')
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .padding(20)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showDelete = false
    })
  }

  @Builder
  modalDetail() {
    Row() {
      Scroll() {
        Column() {
          Column()
            .width('100%')
            .height(180)
            .backgroundColor('#FFEBEE')
            .borderRadius({ topLeft: 16, bottomLeft: 16 })

          Column() {
            Text('¥' + this.selPrice())
              .fontSize(24)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF80AB')
            Text(this.selName())
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#5D4037')
              .margin({ top: 8 })

            Row() {
              Text(this.selCategory())
                .fontSize(10)
                .fontColor('#FF80AB')
                .backgroundColor('#FCE4EC')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              Text(this.selTag())
                .fontSize(10)
                .fontColor('#5D4037')
                .backgroundColor('#FFD54F')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .margin({ left: 6 })
            }
            .margin({ top: 8 })

            Column() {
              Text('品质评分 ' + this.selQuality() + '%')
                .fontSize(12)
                .fontColor('#8D6E63')
              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(8)
                  .backgroundColor('#F5EFE9')
                  .borderRadius(4)
                Column()
                  .width(this.selQuality() + '%')
                  .height(8)
                  .backgroundColor('#FF80AB')
                  .borderRadius(4)
              }
              .width('100%')
              .margin({ top: 6 })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 14 })

            Row() {
              Text('已售 ' + this.selSold())
                .fontSize(12)
                .fontColor('#BCAAA4')
              Text('正品溯源')
                .fontSize(12)
                .fontColor('#BCAAA4')
                .margin({ left: 12 })
              Text('极速退款')
                .fontSize(12)
                .fontColor('#42A5F5')
                .margin({ left: 12 })
            }
            .margin({ top: 12 })

            Row() {
              Text('加入清单')
                .fontSize(14)
                .fontColor('#42A5F5')
                .fontWeight(FontWeight.Bold)
                .layoutWeight(1)
                .height(40)
                .textAlign(TextAlign.Center)
                .backgroundColor('#E3F2FD')
                .borderRadius(20)
                .onClick(() => {
                  this.qty = 1
                  this.showMilk = true
                })
              Text('立即购买')
                .fontSize(14)
                .fontColor('#FFFFFF')
                .fontWeight(FontWeight.Bold)
                .layoutWeight(1)
                .height(40)
                .textAlign(TextAlign.Center)
                .backgroundColor('#FF80AB')
                .borderRadius(20)
                .margin({ left: 10 })
                .onClick(() => {
                  this.showPublish = false
                })
            }
            .width('100%')
            .margin({ top: 18 })

            Row() {
              Text('删除该商品')
                .fontSize(13)
                .fontColor('#FF80AB')
                .padding({ top: 12 })
            }
            .width('100%')
            .onClick(() => {
              this.showDelete = true
            })
          }
          .width('100%')
          .padding(16)
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
      }
      .width('78%')
      .height('100%')
      .backgroundColor('#FFFFFF')

      Column()
        .layoutWeight(1)
        .height('100%')
        .onClick(() => {
          this.showDetail = false
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showDetail = false
    })
  }
}


总结

在这里插入图片描述

本文围绕基于 HarmonyOS ArkTS API 24 的母婴童趣城应用进行了深度的源码级技术解析。从六组母婴专业数据接口的定义到七大功能 Tab 的差异化实现,从奶粉段位柱状图到疫苗状态分色列表,从智能推荐弹窗的月龄步进器到疫苗预约弹窗的选择器,完整剖析了 ArkTS 声明式开发范式在垂直母婴领域的工程实践。应用通过 MilkItem 的 stage/origin/shelf 字段、ClothItem 的 fabric 字段(承载"A 类"安全标准)、ToyItem 的 ageMin 字段和 VaccineItem 的 status 字段,将母婴行业的专业参数体系深度融入数据模型,使得每一个 UI 组件的渲染都建立在精确的业务语义之上。

在数据可视化层面,应用展现了多种柱状图归一化策略的灵活运用。奶粉段位柱状图使用 stage/maxStage*88 的标准归一化方案;童装好评率柱状图使用 rating-4 的简化减法方案;玩具适龄柱状图和疫苗月龄柱状图使用"归一化值+偏移量"方案(*70+8 和 *70+6),巧妙解决了数据包含 0 值时的柱体可见性问题。疫苗月龄柱状图更进一步引入了基于 status 字段的条件着色——已完成状态为琥珀色、其他状态为浅蓝色,将数据可视化与业务语义深度融合,这是本应用在可视化设计上最具创新性的实践。多层 Stack 进度条方案在商品卡片和详情面板中反复使用,配合不同的前景色(粉色、蓝色)实现了品质评分和好评率的直观展示。

在交互设计层面,应用构建了两个母婴场景特有的核心交互流程。智能推荐弹窗通过月龄步进器(0-72 月边界检查)、段位选择器和罐数步进器(1-12 边界检查)的三重交互,实现了"输入宝宝月龄-推荐奶粉段位-计算订阅价格"的一体化流程,订阅价格公式 (selectedStage+1)96qty 实时响应三个输入变量的变化。疫苗预约弹窗通过疫苗选择器(蓝色选中态)和时间选择器(粉色选中态)的双选择器设计,配合接种点信息展示,构成了从疫苗状态查看到预约提交的完整医疗服务闭环。这两个流程的设计充分体现了母婴应用"商品交易+医疗服务"双轮驱动的业务定位,也为 HarmonyOS 平台上构建垂直领域专业应用提供了可复用的交互范式和工程参考。

Logo

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

更多推荐