引言

在这里插入图片描述

在当今移动互联网快速发展的时代,电商类应用已经从单一的购物工具演变为集内容社区、商品展示、互动体验、个性化推荐于一体的综合平台。特别是在美妆个护领域,消费者不仅关注产品本身的价格和功效,还希望能够获得成分分析、色号对比、线下体验预约等增值服务。本文将深度剖析一个名为"颜值研究所"的美妆电商应用,该应用涵盖了美妆集市、护肤专区、彩妆专区、香水小样、体验门店、消息中心和用户中心等七大功能模块,充分展示了 HarmonyOS 声明式 UI 开发框架在构建复杂业务场景下的能力。

从技术架构角度来看,该应用采用了 ArkTS 语言进行开发,这是鸿蒙生态中专为应用开发设计的编程语言。ArkTS 在 TypeScript 的基础上进行了扩展,引入了装饰器语法来声明组件和状态管理机制。应用通过 @Entry@Component 装饰器定义了页面级组件,利用 @State 装饰器实现响应式状态管理,当状态变量发生变化时,框架会自动触发与之绑定的 UI 组件进行重新渲染,从而实现了数据驱动的界面更新模式。同时,应用大量使用了 @Builder 装饰器来封装可复用的 UI 构建方法,将复杂的界面拆分为多个独立的构建单元,有效降低了代码的耦合度。

在业务设计层面,该应用围绕"颜值经济"这一核心概念,构建了一个从线上选购到线下体验的完整闭环。用户可以在美妆集市浏览大牌商品并进行价格比较,在护肤专区查看成分浓度对比图表,在彩妆专区通过色号热度图鉴选择适合的产品,在香水小样模块进行低成本试用订阅,在体验门店模块预约线下试妆服务。整个应用通过丰富的数据可视化组件(如柱状图、进度条、标签胶囊等)来呈现商品信息,并通过弹窗(Modal)机制实现了发布闲置、订阅小样、预约门店、查看详情和删除确认等多种交互操作,充分体现了声明式 UI 在构建复杂交互场景方面的优势。

一、数据模型与接口定义

在这里插入图片描述

在任何应用开发中,数据模型的设计是整个架构的基础。本应用通过定义多个接口(Interface)来规范不同类型数据的结构,确保了类型安全和代码可维护性。

interface BeautyItem {
  name: string
  brand: string
  price: number
  sold: number
  rating: number
  func: string
  tag: string
}

interface SkinItem {
  name: string
  ingredient: string
  conc: number
  price: number
  volume: number
}

interface MakeupItem {
  name: string
  shade: string
  number: number
  price: number
  heat: number
}

interface PerfumeItem {
  name: string
  note: string
  lasting: number
  price: number
  stock: number
}

interface StoreItem {
  name: string
  dist: number
  projects: number
  booked: number
  capacity: number
}

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

上述代码定义了六个核心数据接口,每个接口对应应用中的一个业务实体。BeautyItem 描述了美妆商品的基本信息,包括名称、品牌、价格、销量、好评率、功效分类和营销标签。SkinItem 专注于护肤品的成分数据,记录了有效成分名称、浓度、价格和容量。MakeupItem 针对彩妆产品,特别设计了色号名称、色号数量和热度指数字段。PerfumeItem 则关注香水的香调类型、留香时间和库存信息。StoreItem 用于管理线下门店数据,包括距离、项目数、预约人数和容量上限。MessageItem 则规范了消息推送的数据结构。这种分接口设计方式让不同业务模块的数据独立管理,便于后续扩展和维护。

二、组件状态声明与页面入口

在这里插入图片描述

声明式 UI 的核心在于状态管理。本应用通过 @Entry@Component 装饰器标记页面组件,并通过一系列 @State 变量来管理应用的全局状态。

@Entry
@Component
struct BeautyLabPage {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showSample: boolean = false
  @State showStore: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: BeautyItem | null = null
  @State selectedType: number = 0
  @State selectedSkin: number = 0
  @State selectedNote: number = 0
  @State selectedSize: number = 0
  @State selectedProject: number = 0
  @State selectedSlot: number = 0
  @State qty: number = 1
  @State inputName: string = ''

  private tabs: string[] = ['美妆集市', '护肤专区', '彩妆专区', '香水小样', '体验门店', '消息', '我的']
  private types: string[] = ['口红', '精华', '面霜', '香水', '眼影', '粉底']
  private skins: string[] = ['干皮', '油皮', '混干', '混油', '敏感肌', '中性皮']
  private notes: string[] = ['花香调', '木质调', '柑橘调', '东方调', '海洋调', '美食调']
  private sizes: string[] = ['1.5ml 体验装', '5ml 小样装', '15ml 便携装', '50ml 正装']
  private projects: string[] = ['免费试妆', '肤质检测', 'SPA 护理', '眉形设计']
  private slots: string[] = ['上午 10-12 点', '下午 14-16 点', '傍晚 16-18 点', '晚间 19-21 点']

@State 装饰的变量是响应式数据源,任何对这些变量的修改都会触发依赖它们的 UI 组件自动更新。这里可以看到两类状态变量:一类用于控制 UI 显示状态,如 showPublishshowSampleshowStoreshowDeleteshowDetail 分别控制五个弹窗的显隐;currentTab 控制当前激活的标签页。另一类用于记录用户的选择状态,如 selectedTypeselectedSkinselectedNoteselectedSizeselectedProjectselectedSlot 分别记录用户在弹窗中的选项索引。selectedItem 使用了联合类型 BeautyItem | null,允许为空值,用于存储用户点击查看详情的商品对象。此外,private 修饰的数组和字符串数组作为静态配置数据,不需要响应式更新,因此放在 private 变量中即可。

三、商品数据集合初始化

在这里插入图片描述

应用在组件中预置了大量的商品数据,这些数据以数组形式存储在组件的私有属性中,为各个标签页的内容展示提供数据源。

  private beauties: BeautyItem[] = [
    { name: '小棕瓶精华第七代 50ml', brand: '雅诗兰黛', price: 680, sold: 8942, rating: 98, func: '修护', tag: '爆款' },
    { name: '神仙水精华露 230ml', brand: 'SK-II', price: 1540, sold: 4321, rating: 97, func: '调理', tag: '口碑' },
    { name: '红腰子精华 75ml', brand: '资生堂', price: 1080, sold: 3654, rating: 96, func: '紧致', tag: '热卖' },
    { name: '绿宝瓶精华 50ml', brand: '赫莲娜', price: 1580, sold: 2876, rating: 98, func: '抗老', tag: '贵妇' },
    { name: '蓝胖子防晒 SPF50', brand: '安热沙', price: 238, sold: 9867, rating: 96, func: '防晒', tag: '爆款' },
    { name: '小白伞防晒乳 60ml', brand: '怡思丁', price: 189, sold: 6543, rating: 94, func: '防晒', tag: '热卖' },
    { name: '卸妆膏眼唇温和款', brand: '凡茜', price: 89, sold: 5432, rating: 93, func: '清洁', tag: '实惠' },
    { name: '氨基酸洁颜粉 100g', brand: '芙丽芳丝', price: 150, sold: 4213, rating: 95, func: '清洁', tag: '推荐' },
    { name: '烟酰胺美白身体乳', brand: '凡士林', price: 79, sold: 7654, rating: 92, func: '美白', tag: '平价' },
    { name: '视黄醇晚霜 30g', brand: '珀莱雅', price: 229, sold: 3876, rating: 95, func: '抗皱', tag: '国货' },
    { name: '玻尿酸原液补水', brand: '润百颜', price: 168, sold: 6231, rating: 94, func: '补水', tag: '爆款' },
    { name: '水杨酸棉片刷酸', brand: '博乐达', price: 129, sold: 4567, rating: 93, func: '祛痘', tag: '热卖' }
  ]

  private skinsList: SkinItem[] = [
    { name: '高浓度玻尿酸精华', ingredient: '玻尿酸', conc: 2, price: 168, volume: 30 },
    { name: '烟酰胺焕白精华', ingredient: '烟酰胺', conc: 5, price: 149, volume: 30 },
    { name: '视黄醇抗皱精华', ingredient: '视黄醇', conc: 1, price: 229, volume: 30 },
    { name: 'VC 亮肤安瓶', ingredient: '维C', conc: 15, price: 199, volume: 20 },
    { name: '神经酰胺修护霜', ingredient: '神经酰胺', conc: 3, price: 189, volume: 50 },
    { name: '水杨酸焕肤棉片', ingredient: '水杨酸', conc: 2, price: 129, volume: 60 },
    { name: '寡肽祛痘原液', ingredient: '寡肽', conc: 3, price: 139, volume: 30 },
    { name: '依克多因修护液', ingredient: '依克多因', conc: 1, price: 219, volume: 30 }
  ]

  private makeupList: MakeupItem[] = [
    { name: '哑光唇釉持色不脱色', shade: '枫叶红', number: 12, price: 129, heat: 96 },
    { name: '水光唇泥奶杏色', shade: '奶杏', number: 8, price: 89, heat: 93 },
    { name: '镜面唇冻果冻感', shade: '草莓色', number: 10, price: 99, heat: 90 },
    { name: '十二色眼影大地盘', shade: '大地色', number: 12, price: 159, heat: 95 },
    { name: '九色眼影蜜桃盘', shade: '蜜桃色', number: 9, price: 129, heat: 92 },
    { name: '空气蜜粉定雾面', shade: '透明色', number: 1, price: 119, heat: 88 },
    { name: '持妆粉底液油皮亲妈', shade: '象牙白', number: 6, price: 249, heat: 94 }
  ]

这里展示了美妆商品、护肤成分和彩妆产品三个数据集合的部分内容。每个集合都以强类型数组的形式定义,确保了在编译期就能进行类型检查。数据涵盖了从国际大牌到国货平价的多层次品牌矩阵,包含了修护、防晒、清洁、美白、抗皱等多种功效分类。护肤专区特别标注了有效成分浓度,如 VC 浓度高达 15%、烟酰胺浓度 5%等,这些浓度数据将在后续的柱状图可视化中发挥关键作用。彩妆数据则突出了色号数量和热度指数,为用户提供了直观的产品选择参考。

四、香水与门店数据集合

在这里插入图片描述

除了美妆和护肤产品,应用还为香水小样和线下门店提供了完整的数据支持,构建了从线上选购到线下体验的全链路闭环。

  private perfumes: PerfumeItem[] = [
    { name: '花漾甜心女士淡香水', note: '花香调', lasting: 6, price: 599, stock: 124 },
    { name: '木质绅士男士淡香水', note: '木质调', lasting: 8, price: 689, stock: 98 },
    { name: '海盐鼠尾草中性香', note: '海洋调', lasting: 5, price: 459, stock: 156 },
    { name: '橙花初绽夏日限定', note: '柑橘调', lasting: 4, price: 389, stock: 213 },
    { name: '白茶清冷文艺香', note: '木质调', lasting: 6, price: 499, stock: 87 },
    { name: '玫瑰木质中东风情', note: '东方调', lasting: 9, price: 759, stock: 65 },
    { name: '香草奶杏美食调', note: '美食调', lasting: 7, price: 429, stock: 178 },
    { name: '青柠罗勒沁凉香', note: '柑橘调', lasting: 4, price: 349, stock: 245 },
    { name: '铃兰沐雨清新淡香', note: '花香调', lasting: 5, price: 399, stock: 134 },
    { name: '乌木沉香高定款', note: '东方调', lasting: 10, price: 999, stock: 42 }
  ]

  private stores: StoreItem[] = [
    { name: '国贸美妆体验店', dist: 1.1, projects: 12, booked: 38, capacity: 45 },
    { name: '三里屯旗舰店', dist: 2.5, projects: 18, booked: 52, capacity: 60 },
    { name: '西单大悦城店', dist: 3.2, projects: 15, booked: 41, capacity: 50 },
    { name: '望京凯德店', dist: 4.6, projects: 10, booked: 22, capacity: 40 },
    { name: '五棵松万达店', dist: 5.8, projects: 9, booked: 18, capacity: 35 },
    { name: '合生汇体验店', dist: 3.9, projects: 14, booked: 33, capacity: 42 },
    { name: '颐堤港精品店', dist: 6.2, projects: 16, booked: 47, capacity: 55 },
    { name: '亦庄华联店', dist: 7.4, projects: 8, booked: 12, capacity: 30 },
    { name: '丽泽天街店', dist: 8.1, projects: 11, booked: 26, capacity: 38 },
    { name: '首钢园快闪店', dist: 9.3, projects: 6, booked: 9, capacity: 25 }
  ]

香水数据集合涵盖了花香调、木质调、柑橘调、东方调、海洋调和美食调六大香调类型,留香时间从 4 小时到 10 小时不等。当库存低于 100 时,界面会显示"限量"标签提醒用户。门店数据集合记录了十家线下体验店的信息,包括与用户的距离、可提供的体验项目数量、已预约人数和门店容量上限。其中预约率超过 85% 的门店会以醒目颜色标识进度条,提示用户预约紧张。这些数据为后续的可视化图表和交互逻辑提供了基础。

五、工具方法与数据封装

在这里插入图片描述

应用中封装了一系列私有方法来处理数据查询和状态读取逻辑,这些方法不仅提高了代码的可读性,还实现了空值安全访问。

  private maxConc(): number {
    let m: number = 0
    for (let i = 0; i < this.skinsList.length; i++) {
      if (this.skinsList[i].conc > m) {
        m = this.skinsList[i].conc
      }
    }
    return m
  }

  private maxBooked(): number {
    let m: number = 0
    for (let i = 0; i < this.stores.length; i++) {
      if (this.stores[i].booked > m) {
        m = this.stores[i].booked
      }
    }
    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 selBrand(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.brand
  }

  private selRating(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.rating
  }

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

maxConc 方法遍历护肤成分列表,返回最大浓度值,用于在柱状图中计算各成分柱子的相对高度比例。maxBooked 方法类似,返回门店列表中最大的预约人数,用于预约热度的可视化对比。selNameselPriceselBrandselRatingselSold 这组方法是对 selectedItem 属性的安全访问封装,它们首先检查 selectedItem 是否为 null,如果为空则返回默认值(空字符串或 0),否则返回对应的属性值。这种空值检查机制有效避免了在弹窗中访问空对象属性时可能导致的运行时错误,是 ArkTS 类型安全设计的重要实践。

六、主构建函数与页面骨架

在这里插入图片描述

build 方法是每个组件的核心入口,它定义了整个页面的布局结构和组件层级关系。

  build() {
    Stack() {
      Column() {
        this.headerBar()
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.tabMarket()
            } else if (this.currentTab === 1) {
              this.tabSkin()
            } else if (this.currentTab === 2) {
              this.tabMakeup()
            } else if (this.currentTab === 3) {
              this.tabPerfume()
            } else if (this.currentTab === 4) {
              this.tabStore()
            } 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.showSample) {
        this.modalSample()
      }
      if (this.showStore) {
        this.modalStore()
      }
      if (this.showDelete) {
        this.modalDelete()
      }
      if (this.showDetail) {
        this.modalDetail()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FDF4F8')
  }

页面采用了 Stack 作为根容器,这是一种层叠布局组件,允许子元素在 Z 轴方向上堆叠。Stack 的第一个子元素是 Column,包含头部栏、可滚动内容区和底部导航栏三个部分。内容区使用 Scroll 组件包裹,并通过 if-else 条件判断来根据 currentTab 的值渲染不同的标签页内容。layoutWeight(1) 让内容区占据头部和底部之间的所有剩余空间。scrollBar(BarState.Off) 隐藏了滚动条以保持界面的简洁美观。Stack 之上还层叠了五个条件渲染的弹窗组件,通过布尔状态变量控制其显隐。当弹窗显示时,它会覆盖在主内容之上,形成模态遮罩效果。整个页面的背景色设为淡粉色 #FDF4F8,符合美妆应用的品牌调性。

七、头部栏构建器

头部栏是应用的顶部固定区域,包含品牌名称、搜索入口和促销信息条。

  @Builder
  headerBar() {
    Column() {
      Row() {
        Column() {
          Text('颜值研究所')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
        }
        Column() {
          Row() {
            Text('搜索精华 / 口红 / 香水小样')
              .fontSize(12)
              .fontColor('#F8BBD0')
          }
          .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('美妆节 · 大牌满400减80')
          .fontSize(12)
          .fontColor('#F8BBD0')
          .fontWeight(FontWeight.Medium)
        Text('小样 9.9 试用')
          .fontSize(12)
          .fontColor('#F8BBD0')
          .margin({ left: 14 })
        Text('国货之光专区')
          .fontSize(12)
          .fontColor('#F8BBD0')
          .margin({ left: 14 })
      }
      .width('100%')
      .padding({ left: 14, bottom: 8 })
      .justifyContent(FlexAlign.Start)
    }
    .width('100%')
    .backgroundColor('#D81B60')
  }

头部栏分为上下两行。第一行使用 Row 水平布局,左侧是品牌名称"颜值研究所",使用白色粗体字突出品牌标识。中间是搜索框区域,以白色圆角矩形呈现,内部放置灰色提示文字。搜索框通过 layoutWeight(1) 占据中间的弹性空间,左右分别留出 12 的间距。右侧是"消息"按钮,点击后将 currentTab 设为 5,触发标签页切换到消息中心。第二行是促销信息条,展示了"美妆节满减"、"小样试用"和"国货专区"三条营销文案,使用浅粉色字体在深粉色背景上形成层次感。整个头部栏的背景色设为 #D81B60(品红色),与美妆品牌调性相呼应。

八、底部导航栏构建器

底部导航栏是应用的导航核心,通过七个标签页实现了应用内各功能模块的切换。

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

底部导航栏使用 ForEach 遍历 tabs 数组生成七个导航项。每个导航项是一个 Column,内部包含一个指示器条和一个文字标签。当前选中的标签页其指示器宽度为 16,未选中的为 6,颜色也从全白变为半透明。文字颜色同样区分选中(白色)和未选中(半透明白)状态。每个 Column 通过 layoutWeight(1) 均分水平空间,点击后更新 currentTab 状态变量,触发页面内容切换。底部栏背景色设为 #8E24AA(深紫色),与头部栏的品红色形成同色系深浅搭配。这种通过指示器大小和颜色透明度来标识选中状态的设计方式,既节省了图标资源的引入,又保持了界面的简洁性。

九、美妆集市标签页

美妆集市是应用的首页标签,展示了运营活动横幅、数据统计卡片、品类筛选器和商品列表。

  @Builder
  tabMarket() {
    Column() {
      Row() {
        Column() {
          Text('美妆节 · 大牌补贴')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('雅诗兰黛 / SK-II / 资生堂 直降 20%')
            .fontSize(12)
            .fontColor('#F8BBD0')
            .margin({ top: 6 })
        }
        .layoutWeight(2)
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('9842')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFE082')
          Text('爆款已售')
            .fontSize(10)
            .fontColor('#F8BBD0')
            .margin({ top: 2 })
          Text('领券')
            .fontSize(11)
            .fontColor('#8E24AA')
            .backgroundColor('#FFE082')
            .borderRadius(12)
            .padding({ left: 12, right: 12, top: 4, bottom: 4 })
            .margin({ top: 8 })
            .scale({ x: 1.06, y: 1.06 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#D81B60')
      .borderRadius(12)
      .alignItems(VerticalAlign.Center)

      Row() {
        Column() {
          Text('24')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
          Text('大牌单品')
            .fontSize(10)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(58)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10 })

        Column() {
          Text('98%')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#8E24AA')
          Text('最高好评')
            .fontSize(10)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(58)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })

        Column() {
          Text('9.9')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
          Text('小样试用')
            .fontSize(10)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(58)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })
        .onClick(() => {
          this.showSample = true
        })

        Column() {
          Text('发布')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('闲置转让')
            .fontSize(10)
            .fontColor('#F8BBD0')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(58)
        .backgroundColor('#8E24AA')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })
        .onClick(() => {
          this.showPublish = true
        })
      }
      .width('100%')

该段代码构建了美妆集市标签页的上半部分。首先是活动横幅区域,使用 Row 分为左右两列:左侧占 layoutWeight(2) 展示活动标题和副标题,右侧占 layoutWeight(1) 展示爆款销量数字和"领券"按钮。"领券"按钮添加了 scale 放大和 animation 无限循环动画,持续脉动吸引点击。下方是四个统计卡片,分别展示大牌单品数、最高好评率、小样试用价和闲置发布入口。每个卡片为白色圆角矩形,等宽排列。"小样试用"卡片点击触发 showSample 弹窗,"闲置转让"卡片点击触发 showPublish 弹窗。这种通过卡片入口引导用户进入不同功能模块的设计,既提升了视觉层次感,又优化了交互体验。

十、商品列表与品类筛选

美妆集市的下半部分包含品类筛选器和商品列表,是用户浏览和选购商品的核心区域。

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

      Column() {
        ForEach(this.beauties, (b: BeautyItem) => {
          Row() {
            Column()
              .width(84)
              .height(84)
              .backgroundColor('#F8BBD0')
              .borderRadius(12)
            Column() {
              Text(b.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text(b.brand)
                  .fontSize(10)
                  .fontColor('#8E24AA')
                  .backgroundColor('#F3E5F5')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text(b.func)
                  .fontSize(10)
                  .fontColor('#D81B60')
                  .backgroundColor('#FCE4EC')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .margin({ left: 6 })
                Text(b.tag)
                  .fontSize(10)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#D81B60')
                  .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('#F0F0F0')
                  .borderRadius(3)
                Column()
                  .width(b.rating + '%')
                  .height(6)
                  .backgroundColor('#D81B60')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 8 })

              Row() {
                Text('¥' + b.price)
                  .fontSize(17)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#D81B60')
                Text('已售' + b.sold)
                  .fontSize(11)
                  .fontColor('#999999')
                  .margin({ left: 8 })
                Text('试用装')
                  .fontSize(11)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#8E24AA')
                  .borderRadius(10)
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .margin({ left: 6 })
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceBetween)
              .alignItems(VerticalAlign.Center)
              .margin({ top: 8 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .onClick(() => {
            this.selectedItem = b
            this.showDetail = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

品类筛选器使用水平滚动的 Scroll 组件包裹 Row,内部通过 ForEach 遍历品类数组生成胶囊型标签。选中状态通过文字颜色和背景色进行区分。商品列表使用 ForEach 遍历 beauties 数组,每项为白色圆角卡片。卡片内左侧为粉色占位图区域,右侧为商品信息区,包含名称、品牌标签、功效标签和营销标签。营销标签(如"爆款"、“热卖”)添加了缩放和循环动画,持续吸引注意力。好评率以 Stack 叠加进度条形式呈现,底部灰色条为满进度,上层粉色条宽度按 b.rating + '%' 比例填充。价格行右侧的"试用装"按钮可触发小样订阅弹窗。整个卡片点击后将当前商品赋值给 selectedItem 并显示详情弹窗。

十一、护肤专区标签页

护肤专区以"成分实验室"为核心概念,通过柱状图展示各成分的有效浓度对比,并提供成分详情列表。

  @Builder
  tabSkin() {
    Column() {
      Column() {
        Row() {
          Text('护肤成分实验室')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('有效浓度对比 (%)')
            .fontSize(11)
            .fontColor('#F8BBD0')
            .margin({ left: 8 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Row() {
          ForEach(this.skinsList, (s: SkinItem) => {
            Column() {
              Column()
                .width(15)
                .height(s.conc / this.maxConc() * 78 + 4)
                .backgroundColor('#F48FB1')
                .borderRadius(4)
              Text(s.conc + '')
                .fontSize(9)
                .fontColor('#F8BBD0')
                .margin({ top: 3 })
            }
            .margin({ right: 9 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .height(108)
        .alignItems(VerticalAlign.End)
        .justifyContent(FlexAlign.Start)
        .margin({ top: 10 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#6A1B9A')
      .borderRadius(12)
      .alignItems(HorizontalAlign.Start)

      Row() {
        Column() {
          Text('小样订阅')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('月度盲盒 · 惊喜试用')
            .fontSize(10)
            .fontColor('#F8BBD0')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(52)
        .backgroundColor('#8E24AA')
        .borderRadius(10)
        .justifyContent(FlexAlign.Center)
        .margin({ top: 10 })
        .onClick(() => {
          this.showSample = true
        })

        Column() {
          Text('发布闲置')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('护肤品转让')
            .fontSize(10)
            .fontColor('#F8BBD0')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(52)
        .backgroundColor('#D81B60')
        .borderRadius(10)
        .justifyContent(FlexAlign.Center)
        .margin({ top: 10, left: 8 })
        .onClick(() => {
          this.showPublish = true
        })
      }
      .width('100%')

成分实验室区域使用深紫色背景卡片,标题"护肤成分实验室"搭配"有效浓度对比 (%)"副标题。核心可视化是一个柱状图,通过 ForEach 遍历 skinsList 数组,为每个成分生成一根柱子。柱子高度通过公式 s.conc / this.maxConc() * 78 + 4 计算,即当前浓度除以最大浓度再乘以基准高度 78,加上 4 的最小高度保证可见性。柱子下方标注浓度数值。下方是两个功能入口按钮:左侧"小样订阅"按钮以紫色背景呈现,点击触发订阅弹窗;右侧"发布闲置"按钮以品红色呈现,点击触发发布弹窗。这种将数据可视化与功能入口结合在同一页面区域的设计,让用户在了解成分信息的同时可以直接进行相关操作。

十二、护肤成分详情列表

成分列表是护肤专区的下半部分,为每个成分产品提供了详细的卡片式展示。

      Column() {
        ForEach(this.skinsList, (s: SkinItem) => {
          Row() {
            Column() {
              Text(s.ingredient)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
            }
            .width(64)
            .height(64)
            .backgroundColor('#F3E5F5')
            .borderRadius(10)
            .justifyContent(FlexAlign.Center)

            Column() {
              Text(s.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text('浓度 ' + s.conc + '%')
                  .fontSize(10)
                  .fontColor('#8E24AA')
                  .backgroundColor('#F3E5F5')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text(s.volume + 'ml')
                  .fontSize(10)
                  .fontColor('#999999')
                  .margin({ left: 8 })
                Text('敏感肌可用')
                  .fontSize(10)
                  .fontColor('#D81B60')
                  .margin({ left: 6 })
              }
              .margin({ top: 6 })

              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(5)
                  .backgroundColor('#F0F0F0')
                  .borderRadius(3)
                Column()
                  .width(s.conc / this.maxConc() * 100 + '%')
                  .height(5)
                  .backgroundColor('#F48FB1')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 8 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })

            Column() {
              Text('¥' + s.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D81B60')
              Text('买正装')
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#8E24AA')
                .borderRadius(10)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .margin({ top: 6 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .onClick(() => {
            this.showSample = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

成分列表通过 ForEach 遍历 skinsList 数组生成卡片列表。每张卡片左侧为成分名称的圆形/方形占位区域,中间为产品详细信息:产品名称使用省略号处理超长文本,下方一行展示浓度标签、容量和"敏感肌可用"提示。进度条同样使用 Stack 叠加方式,宽度按 s.conc / this.maxConc() * 100 + '%' 计算,直观展示当前成分与最高浓度的比例关系。右侧为价格和"买正装"按钮,点击卡片触发小样订阅弹窗。这种列表设计让用户快速浏览各成分产品的核心参数,并通过进度条直观了解浓度对比,是数据可视化在电商场景中的典型应用。

十三、彩妆专区标签页

彩妆专区以色号图鉴为核心,通过色号数量柱状图和双列瀑布流展示彩妆产品。

  @Builder
  tabMakeup() {
    Column() {
      Column() {
        Text('彩妆色号图鉴')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Text('热门色号 · 上脸试色 · 无忧退换')
          .fontSize(11)
          .fontColor('#999999')
          .margin({ top: 4 })
      }
      .width('100%')
      .padding({ top: 14, bottom: 14, left: 14 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .alignItems(HorizontalAlign.Start)
      .margin({ top: 10 })

      Column() {
        Text('色号数量对比')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Row() {
          ForEach(this.makeupList, (m: MakeupItem) => {
            Column() {
              Column()
                .width(15)
                .height(m.number / 12 * 78)
                .backgroundColor('#D81B60')
                .borderRadius(4)
              Text(m.number + '')
                .fontSize(9)
                .fontColor('#999999')
                .margin({ top: 4 })
            }
            .margin({ right: 9 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .height(108)
        .alignItems(VerticalAlign.End)
        .justifyContent(FlexAlign.Start)
        .margin({ top: 8 })
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 10 })
      .alignItems(HorizontalAlign.Start)

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.makeupList, (m: MakeupItem) => {
          Column() {
            Row() {
              Column()
                .width(18)
                .height(18)
                .backgroundColor('#D81B60')
                .borderRadius(9)
              Text(m.shade)
                .fontSize(10)
                .fontColor('#8E24AA')
                .backgroundColor('#F3E5F5')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)

            Text(m.name)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .maxLines(2)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
              .margin({ top: 8 })

            Column() {
              Text('热度 ' + m.heat)
                .fontSize(10)
                .fontColor('#999999')
              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(5)
                  .backgroundColor('#F0F0F0')
                  .borderRadius(3)
                Column()
                  .width(m.heat + '%')
                  .height(5)
                  .backgroundColor('#F48FB1')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 4 })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 6 })

            Row() {
              Text('¥' + m.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D81B60')
              Text(m.number + ' 色')
                .fontSize(10)
                .fontColor('#999999')
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)
            .margin({ top: 6 })
          }
          .width('48%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .alignItems(HorizontalAlign.Start)
          .onClick(() => {
            this.showDetail = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

彩妆专区顶部是标题卡片,包含"彩妆色号图鉴"标题和副文案。下方是色号数量柱状图,柱子高度按 m.number / 12 * 78 计算(12 为最大色号数),颜色统一为品红色。产品列表使用 Flex 组件配合 FlexWrap.Wrap 实现自动换行的双列瀑布流布局,每项宽度设为 48%。每个产品卡片包含色号圆点、色号名称标签、产品名称(最多两行带省略号)、热度进度条和价格行。热度进度条宽度直接使用 m.heat + '%',因为热度值范围为 0-100,可以直接映射为百分比。这种双列布局比单列列表更节省垂直空间,同时保持每个产品信息的完整性。

十四、香水小样标签页

香水小样标签页以"星球"主题呈现,提供横向滚动速览和详细列表两种展示方式。

  @Builder
  tabPerfume() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('香水小样星球')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('9.9 元试用 · 盲盒月订 · 正装回购')
              .fontSize(11)
              .fontColor('#F8BBD0')
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Text('立即试用')
            .fontSize(12)
            .fontColor('#6A1B9A')
            .backgroundColor('#FFE082')
            .borderRadius(14)
            .padding({ left: 14, right: 14, top: 6, bottom: 6 })
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            .onClick(() => {
              this.showSample = true
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#8E24AA')
      .borderRadius(12)

      Scroll() {
        Row() {
          ForEach(this.perfumes, (p: PerfumeItem) => {
            Column() {
              Column()
                .width(96)
                .height(60)
                .backgroundColor('#F3E5F5')
                .borderRadius(10)
              Text(p.note)
                .fontSize(10)
                .fontColor('#8E24AA')
                .backgroundColor('#F3E5F5')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .margin({ top: 6 })
              Text('¥' + p.price)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D81B60')
                .margin({ top: 4 })
            }
            .width(96)
            .padding(8)
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .margin({ right: 8 })
            .alignItems(HorizontalAlign.Center)
          })
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .margin({ top: 10 })

顶部横幅使用紫色背景,左侧标题"香水小样星球"搭配副文案,右侧"立即试用"按钮使用黄色背景配深紫色文字,并添加了循环缩放动画。横向滚动区展示所有香水小样的卡片缩略图,每个卡片包含占位图、香调标签和价格。当库存低于 100 时,详细列表中的对应项会显示"限量"标签并添加脉动动画。留香时间以进度条形式展示,宽度为 p.lasting * 10 + '%',将 0-10 小时的留香时间映射为 0-100% 的进度。"9.9 试用"按钮统一触发小样订阅弹窗,实现了从浏览到下单的快速转化路径。

十五、体验门店标签页

体验门店标签页通过预约热度柱状图和门店列表,帮助用户找到最近的线下体验店。

  @Builder
  tabStore() {
    Column() {
      Column() {
        Row() {
          Text('美妆体验门店')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('本周预约热度')
            .fontSize(11)
            .fontColor('#F8BBD0')
            .margin({ left: 8 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Row() {
          ForEach(this.stores, (s: StoreItem) => {
            Column() {
              Column()
                .width(15)
                .height(s.booked / this.maxBooked() * 78)
                .backgroundColor('#CE93D8')
                .borderRadius(4)
              Text(s.booked + '')
                .fontSize(9)
                .fontColor('#F8BBD0')
                .margin({ top: 3 })
            }
            .margin({ right: 9 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .height(108)
        .alignItems(VerticalAlign.End)
        .justifyContent(FlexAlign.Start)
        .margin({ top: 10 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#4A148C')
      .borderRadius(12)
      .alignItems(HorizontalAlign.Start)

      Column() {
        ForEach(this.stores, (s: StoreItem) => {
          Row() {
            Column()
              .width(50)
              .height(50)
              .backgroundColor('#F3E5F5')
              .borderRadius(10)
            Column() {
              Row() {
                Text(s.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#333333')
                Text(s.projects + ' 个项目')
                  .fontSize(11)
                  .fontColor('#8E24AA')
                  .fontWeight(FontWeight.Bold)
                  .margin({ left: 8 })
              }
              Text('距离 ' + s.dist + 'km · 容量 ' + s.capacity + ' 人')
                .fontSize(11)
                .fontColor('#999999')
                .margin({ top: 4 })

              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(6)
                  .backgroundColor('#F0F0F0')
                  .borderRadius(3)
                Column()
                  .width(s.booked / s.capacity * 100 + '%')
                  .height(6)
                  .backgroundColor(s.booked / s.capacity > 0.85 ? '#D81B60' : '#8E24AA')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 6 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })

            Column() {
              Text(s.booked + '/' + s.capacity)
                .fontSize(11)
                .fontColor('#999999')
              Text('预约')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#D81B60')
                .borderRadius(10)
                .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                .margin({ top: 6 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .onClick(() => {
            this.showStore = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

门店列表中每个卡片展示了门店名称、项目数、距离/容量信息和预约进度条。预约进度条宽度按 s.booked / s.capacity * 100 + '%' 计算,颜色根据预约率动态变化:当预约率超过 85% 时进度条变为品红色(#D81B60),表示预约紧张需要尽快行动;否则为紫色(#8E24AA)。右侧显示"已预约/容量"数字和"预约"按钮,点击触发门店预约弹窗。柱状图中柱子高度按 s.booked / this.maxBooked() * 78 计算,通过归一化处理让不同预约量的门店在同一比例尺下展示。这种动态颜色和归一化柱状图的组合设计,为用户提供了直观的预约紧张度感知。

十六、消息中心标签页

消息中心以列表形式展示系统通知、订单状态、活动消息等信息,支持未读标识和删除操作。

  @Builder
  tabMessage() {
    Column() {
      Column() {
        Row() {
          Text('消息中心')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
          Text('8 条新动态')
            .fontSize(11)
            .fontColor('#D81B60')
            .margin({ left: 8 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .width('100%')
      .padding({ top: 14, bottom: 14, left: 14, right: 14 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 10 })

      Column() {
        ForEach(this.messages, (m: MessageItem) => {
          Row() {
            Column()
              .width(46)
              .height(46)
              .backgroundColor('#F3E5F5')
              .borderRadius(23)
            Column() {
              Row() {
                Text(m.title)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#333333')
                if (m.unread > 0) {
                  Text(m.unread + '')
                    .fontSize(10)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#D81B60')
                    .borderRadius(9)
                    .padding({ left: 6, right: 6, top: 1, bottom: 1 })
                    .margin({ left: 6 })
                    .scale({ x: 1.1, y: 1.1 })
                    .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
                }
                Text(m.time)
                  .fontSize(10)
                  .fontColor('#BBBBBB')
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceBetween)

              Text(m.content)
                .fontSize(12)
                .fontColor('#666666')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
                .margin({ top: 6 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(12)
          .margin({ top: 10 })
          .onClick(() => {
            this.showDelete = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

消息列表通过 ForEach 遍历 messages 数组生成消息卡片。每条消息左侧为圆形头像占位,右侧为标题行和内容预览。标题行包含消息标题、未读数角标和时间戳。当 m.unread > 0 时,标题旁显示红色圆形角标并添加缩放脉动动画吸引注意力。内容预览限制为单行,超长部分以省略号截断。点击消息卡片触发删除确认弹窗。整个消息中心设计简洁,通过颜色和动画区分已读未读状态,提供了良好的信息层次感。

十七、用户中心标签页

用户中心展示了用户的个人资料、数据统计和功能菜单列表。

  @Builder
  tabMine() {
    Column() {
      Column() {
        Row() {
          Column()
            .width(60)
            .height(60)
            .backgroundColor('#F3E5F5')
            .borderRadius(30)
          Column() {
            Text('精致女孩_Momo')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('颜值等级:成分党达人 Lv.8')
              .fontSize(11)
              .fontColor('#F8BBD0')
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .padding({ left: 12 })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#6A1B9A')
      .borderRadius(12)

      Row() {
        Column() {
          Text('68')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
          Text('美妆订单')
            .fontSize(11)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(64)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10 })

        Column() {
          Text('6')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#8E24AA')
          Text('到店体验')
            .fontSize(11)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(64)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })

        Column() {
          Text('1240')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
          Text('颜值积分')
            .fontSize(11)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(64)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })

        Column() {
          Text('9')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#8E24AA')
          Text('优惠券')
            .fontSize(11)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(64)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })
      }
      .width('100%')

      Column() {
        ForEach(['我的肤质档案', '小样订阅管理', '空瓶回收记录', '到店体验预约', ' wishlist 心愿单', '帮助与反馈'], (item: string) => {
          Row() {
            Text(item)
              .fontSize(14)
              .fontColor('#333333')
            Text('>')
              .fontSize(14)
              .fontColor('#CCCCCC')
          }
          .width('100%')
          .padding({ top: 14, bottom: 14 })
          .justifyContent(FlexAlign.SpaceBetween)
          .onClick(() => {
            this.showDelete = true
          })
        })
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .padding({ left: 14, right: 14 })
      .margin({ top: 12 })
    }
    .width('100%')
  }

用户中心顶部是个人资料卡片,深紫色背景上展示头像占位、用户名和等级标签。下方四个统计卡片分别展示美妆订单数、到店体验次数、颜值积分和优惠券数,使用不同颜色突出关键数据。最下方是功能菜单列表,使用 ForEach 遍历字符串数组生成菜单项,每项包含菜单名和右箭头。点击菜单项触发删除确认弹窗(此处为演示用途,实际应用中应路由到对应功能页面)。整体设计简洁有序,数据统计卡片和功能菜单的组合构成了完整的用户中心页面。

十八、发布闲置弹窗

发布闲置弹窗是一个从底部滑出的模态面板,用于用户发布闲置美妆产品。

  @Builder
  modalPublish() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('发布闲置美妆')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
          Text('关闭')
            .fontSize(13)
            .fontColor('#999999')
            .padding(6)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Column() {
          Text('产品名称')
            .fontSize(13)
            .fontColor('#666666')
          TextInput({ placeholder: '如:小棕瓶精华 30ml 八成新' })
            .height(40)
            .fontSize(13)
            .backgroundColor('#FDF4F8')
            .borderRadius(8)
            .margin({ top: 6 })
            .onChange((value: string) => {
              this.inputName = value
            })
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('产品品类')
            .fontSize(13)
            .fontColor('#666666')
          Row() {
            ForEach(this.types, (t: string, idx: number) => {
              Text(t)
                .fontSize(12)
                .fontColor(this.selectedType === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedType === idx ? '#D81B60' : '#FDF4F8')
                .borderRadius(14)
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedType = idx
                })
            })
          }
          .width('100%')
          .flexWrap(FlexWrap.Wrap)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('适合肤质')
            .fontSize(13)
            .fontColor('#666666')
          Row() {
            ForEach(this.skins, (s: string, idx: number) => {
              Text(s)
                .fontSize(12)
                .fontColor(this.selectedSkin === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedSkin === idx ? '#8E24AA' : '#FDF4F8')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedSkin = idx
                })
            })
          }
          .width('100%')
          .flexWrap(FlexWrap.Wrap)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('预估价:¥' + ((this.selectedType + 1) * 45 + this.selectedSkin * 12))
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
            .scale({ x: 1.04, y: 1.04 })
            .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .margin({ top: 20 })
        .alignItems(HorizontalAlign.Start)

        Text('确认发布')
          .fontSize(15)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(44)
          .textAlign(TextAlign.Center)
          .backgroundColor('#D81B60')
          .borderRadius(22)
          .margin({ top: 20 })
          .onClick(() => {
            this.showPublish = false
          })
      }
      .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.showPublish = false
    })
  }

发布弹窗使用了从底部弹出的设计模式,顶部使用 Column().layoutWeight(1) 占据空白区域将内容推至底部。弹窗内容包括产品名称输入框(TextInput 组件配合 onChange 回调将输入值同步到 inputName 状态变量)、产品品类选择器(使用 ForEach 遍历品类数组生成胶囊标签,选中态以品红色标识)和适合肤质选择器(类似品类选择器但以紫色标识选中态)。预估价格通过公式 (this.selectedType + 1) * 45 + this.selectedSkin * 12 动态计算,随着用户选择不同品类和肤质实时更新,并添加了缩放脉动动画。constraintSize({ maxHeight: '80%' }) 限制弹窗最大高度不超过屏幕的 80%,borderRadius 只设置上方圆角形成底部弹出的视觉效果。外层 Column 设置半透明黑色背景作为遮罩,点击遮罩区域关闭弹窗,而 onClick 内的 event.stopPropagation() 阻止了点击弹窗内容时的冒泡传播。

十九、小样订阅弹窗

小样订阅弹窗为用户提供香调偏好选择、规格选择和数量调整功能。

  @Builder
  modalSample() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('香水小样订阅')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
          Text('¥' + ((this.selectedSize + 1) * 19 + this.selectedNote * 6) * this.qty)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
            .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('#666666')
          Row() {
            ForEach(this.notes, (n: string, idx: number) => {
              Text(n)
                .fontSize(12)
                .fontColor(this.selectedNote === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedNote === idx ? '#8E24AA' : '#FDF4F8')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedNote = idx
                })
            })
          }
          .width('100%')
          .flexWrap(FlexWrap.Wrap)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('小样规格')
            .fontSize(13)
            .fontColor('#666666')
          Row() {
            ForEach(this.sizes, (s: string, idx: number) => {
              Text(s)
                .fontSize(12)
                .fontColor(this.selectedSize === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedSize === idx ? '#D81B60' : '#FDF4F8')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedSize = idx
                })
            })
          }
          .width('100%')
          .flexWrap(FlexWrap.Wrap)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('盒数:' + this.qty)
            .fontSize(13)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
          Row() {
            Text('-')
              .fontSize(16)
              .fontColor('#8E24AA')
              .width(34)
              .height(34)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F3E5F5')
              .borderRadius(8)
              .onClick(() => {
                if (this.qty > 1) {
                  this.qty -= 1
                }
              })
            Text(this.qty + '')
              .fontSize(15)
              .fontColor('#333333')
              .width(50)
              .textAlign(TextAlign.Center)
            Text('+')
              .fontSize(16)
              .fontColor('#D81B60')
              .width(34)
              .height(34)
              .textAlign(TextAlign.Center)
              .backgroundColor('#FCE4EC')
              .borderRadius(8)
              .onClick(() => {
                if (this.qty < 12) {
                  this.qty += 1
                }
              })
          }
          .margin({ top: 8 })
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Text('每盒 3 支惊喜香样,附回购优惠券')
          .fontSize(11)
          .fontColor('#999999')
          .margin({ top: 10 })

        Text('确认订阅')
          .fontSize(15)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(44)
          .textAlign(TextAlign.Center)
          .backgroundColor('#8E24AA')
          .borderRadius(22)
          .margin({ top: 16 })
          .onClick(() => {
            this.showSample = false
          })
      }
      .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.showSample = false
    })
  }

小样订阅弹窗的价格计算公式为 ((this.selectedSize + 1) * 19 + this.selectedNote * 6) * this.qty,即基础价格由选择的规格决定(1.5ml 为 19 元、5ml 为 38 元、15ml 为 57 元、50ml 为 76 元),加上香调偏好附加费(每档 6 元),再乘以盒数。价格显示添加了缩放脉动动画。数量调节器使用减号、数字和加号的组合,减号限制最小为 1 盒,加号限制最大为 12 盒。底部"确认订阅"按钮以紫色背景呈现,点击关闭弹窗。整个弹窗实现了香调偏好、规格和数量三个维度的选择,配合动态价格计算,为用户提供了灵活的小样订阅体验。

二十、门店预约弹窗

门店预约弹窗允许用户选择体验项目、到店时段,并了解体验权益。

  @Builder
  modalStore() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('到店体验预约')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
          Text('到店礼')
            .fontSize(12)
            .fontColor('#FFFFFF')
            .backgroundColor('#8E24AA')
            .borderRadius(8)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 })
            .scale({ x: 1.06, y: 1.06 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text('体验项目')
            .fontSize(13)
            .fontColor('#666666')
          Row() {
            ForEach(this.projects, (p: string, idx: number) => {
              Text(p)
                .fontSize(12)
                .fontColor(this.selectedProject === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedProject === idx ? '#D81B60' : '#FDF4F8')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedProject = idx
                })
            })
          }
          .width('100%')
          .flexWrap(FlexWrap.Wrap)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('到店时段')
            .fontSize(13)
            .fontColor('#666666')
          Row() {
            ForEach(this.slots, (s: string, idx: number) => {
              Text(s)
                .fontSize(12)
                .fontColor(this.selectedSlot === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedSlot === idx ? '#8E24AA' : '#FDF4F8')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedSlot = idx
                })
            })
          }
          .width('100%')
          .flexWrap(FlexWrap.Wrap)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('体验权益')
            .fontSize(13)
            .fontColor('#666666')
          Text('专业彩妆师 1v1 服务 · 全场小样免费领 · 消费满赠正装')
            .fontSize(12)
            .fontColor('#8E24AA')
            .margin({ top: 6 })
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Text('提交预约')
          .fontSize(15)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(44)
          .textAlign(TextAlign.Center)
          .backgroundColor('#D81B60')
          .borderRadius(22)
          .margin({ top: 20 })
          .onClick(() => {
            this.showStore = false
          })
      }
      .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.showStore = false
    })
  }

门店预约弹窗提供了体验项目选择(免费试妆、肤质检测、SPA 护理、眉形设计)和到店时段选择(上午、下午、傍晚、晚间)两个维度的选择,以及体验权益说明。选中态使用品红和紫色两种颜色区分两个选择器。"到店礼"标签添加了脉动动画吸引用户注意。底部"提交预约"按钮点击关闭弹窗。该弹窗将线上选购与线下服务预约紧密关联,是 O2O(线上到线下)模式在美妆场景中的典型实现。

二十一、删除确认与详情弹窗

删除确认弹窗是一个居中显示的模态对话框,而详情弹窗则从右侧滑入展示商品详细信息。

  @Builder
  modalDelete() {
    Column() {
      Column() {
        Column()
          .width(56)
          .height(56)
          .backgroundColor('#FCE4EC')
          .borderRadius(28)
        Text('确认删除该记录?')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .margin({ top: 14 })
        Text('删除后订阅与预约记录将无法恢复')
          .fontSize(12)
          .fontColor('#999999')
          .margin({ top: 8 })
          .textAlign(TextAlign.Center)

        Row() {
          Text('再想想')
            .fontSize(14)
            .fontColor('#666666')
            .layoutWeight(1)
            .height(42)
            .textAlign(TextAlign.Center)
            .backgroundColor('#FDF4F8')
            .borderRadius(21)
            .onClick(() => {
              this.showDelete = false
            })
          Text('确认删除')
            .fontSize(14)
            .fontColor('#FFFFFF')
            .layoutWeight(1)
            .height(42)
            .textAlign(TextAlign.Center)
            .backgroundColor('#D81B60')
            .borderRadius(21)
            .margin({ left: 12 })
            .scale({ x: 1.03, y: 1.03 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            .onClick(() => {
              this.showDelete = false
            })
        }
        .width('100%')
        .margin({ top: 22 })
      }
      .width('80%')
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .padding(22)
      .alignItems(HorizontalAlign.Center)
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showDelete = false
    })
  }

删除确认弹窗居中显示,内容包含一个圆形图标占位、确认文案、说明文案和两个按钮。"再想想"按钮使用淡粉色背景表示取消操作,"确认删除"按钮使用品红色背景并添加缩放动画强调操作重要性。弹窗宽度设为 80%,居中显示在半透明遮罩之上。外部遮罩点击关闭弹窗,内部内容区域通过 stopPropagation 阻止冒泡。

详情弹窗则从右侧滑入,占据屏幕宽度的 78%。它通过一系列安全访问方法(selNameselBrandselRatingselSoldselPrice)获取当前选中商品的信息。内容包含商品名称、品牌标签、已售数量、好评率进度条和价格。底部提供"立即购买"和"加入心愿单"两个操作按钮。"立即购买"点击后关闭详情弹窗并打开小样订阅弹窗,实现了从浏览到购买的转化路径。

应用交互流程图

下图展示了应用的核心交互流程,从用户进入应用到完成各种操作的全过程。

美妆集市

护肤专区

彩妆专区

香水小样

体验门店

消息

我的

查看详情

立即购买

加入心愿单

点击试用装

点击发布闲置

订阅小样

发布闲置

9.9试用

点击预约

选择项目时段

点击消息

确认删除

再想想

点击菜单项

确认订阅

确认发布

应用启动

加载美妆集市首页

用户选择标签页

浏览商品列表

查看成分浓度对比

浏览色号图鉴

查看香调留香对比

查看门店预约热度

浏览消息通知

查看个人中心

点击商品

详情弹窗

小样订阅弹窗

关闭弹窗

发布闲置弹窗

门店预约弹窗

提交预约

删除确认弹窗

移除消息

完成订阅

完成发布

完成预约

核心技术点对比

下表汇总了应用中各标签页使用的关键技术点和设计模式:

功能模块 数据模型 可视化方式 弹窗交互 动画效果 核心设计特点
美妆集市 BeautyItem 好评率进度条 详情弹窗/小样弹窗 营销标签脉动 品类筛选器+商品卡片列表
护肤专区 SkinItem 浓度柱状图 小样订阅/发布闲置 价格脉动 成分浓度归一化可视化
彩妆专区 MakeupItem 色号数量柱状图 详情弹窗 热销标签脉动 Flex双列瀑布流布局
香水小样 PerfumeItem 留香进度条 小样订阅弹窗 限量标签脉动 横向滚动+纵向列表双视图
体验门店 StoreItem 预约热度柱状图 门店预约弹窗 到店礼脉动 预约率动态颜色进度条
消息中心 MessageItem 未读角标 删除确认弹窗 角标脉动 未读状态视觉区分
用户中心 静态数据 统计数字卡片 删除确认弹窗 优惠券数字脉动 数据卡片+菜单列表组合
发布弹窗 多状态变量 动态价格计算 底部弹出面板 预估价脉动 TextInput+标签选择器组合
小样弹窗 多状态变量 动态价格计算 底部弹出面板 价格脉动 规格选择+数量步进器
门店弹窗 多状态变量 权益说明 底部弹出面板 到店礼脉动 项目+时段双选择器
详情弹窗 selectedItem 好评进度条 右侧滑入面板 价格脉动 安全访问方法+空值保护

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

interface BeautyItem {
  name: string
  brand: string
  price: number
  sold: number
  rating: number
  func: string
  tag: string
}

interface SkinItem {
  name: string
  ingredient: string
  conc: number
  price: number
  volume: number
}

interface MakeupItem {
  name: string
  shade: string
  number: number
  price: number
  heat: number
}

interface PerfumeItem {
  name: string
  note: string
  lasting: number
  price: number
  stock: number
}

interface StoreItem {
  name: string
  dist: number
  projects: number
  booked: number
  capacity: number
}

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

@Entry
@Component
struct BeautyLabPage {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showSample: boolean = false
  @State showStore: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: BeautyItem | null = null
  @State selectedType: number = 0
  @State selectedSkin: number = 0
  @State selectedNote: number = 0
  @State selectedSize: number = 0
  @State selectedProject: number = 0
  @State selectedSlot: number = 0
  @State qty: number = 1
  @State inputName: string = ''

  private tabs: string[] = ['美妆集市', '护肤专区', '彩妆专区', '香水小样', '体验门店', '消息', '我的']
  private types: string[] = ['口红', '精华', '面霜', '香水', '眼影', '粉底']
  private skins: string[] = ['干皮', '油皮', '混干', '混油', '敏感肌', '中性皮']
  private notes: string[] = ['花香调', '木质调', '柑橘调', '东方调', '海洋调', '美食调']
  private sizes: string[] = ['1.5ml 体验装', '5ml 小样装', '15ml 便携装', '50ml 正装']
  private projects: string[] = ['免费试妆', '肤质检测', 'SPA 护理', '眉形设计']
  private slots: string[] = ['上午 10-12 点', '下午 14-16 点', '傍晚 16-18 点', '晚间 19-21 点']

  private beauties: BeautyItem[] = [
    { name: '小棕瓶精华第七代 50ml', brand: '雅诗兰黛', price: 680, sold: 8942, rating: 98, func: '修护', tag: '爆款' },
    { name: '神仙水精华露 230ml', brand: 'SK-II', price: 1540, sold: 4321, rating: 97, func: '调理', tag: '口碑' },
    { name: '红腰子精华 75ml', brand: '资生堂', price: 1080, sold: 3654, rating: 96, func: '紧致', tag: '热卖' },
    { name: '绿宝瓶精华 50ml', brand: '赫莲娜', price: 1580, sold: 2876, rating: 98, func: '抗老', tag: '贵妇' },
    { name: '蓝胖子防晒 SPF50', brand: '安热沙', price: 238, sold: 9867, rating: 96, func: '防晒', tag: '爆款' },
    { name: '小白伞防晒乳 60ml', brand: '怡思丁', price: 189, sold: 6543, rating: 94, func: '防晒', tag: '热卖' },
    { name: '卸妆膏眼唇温和款', brand: '凡茜', price: 89, sold: 5432, rating: 93, func: '清洁', tag: '实惠' },
    { name: '氨基酸洁颜粉 100g', brand: '芙丽芳丝', price: 150, sold: 4213, rating: 95, func: '清洁', tag: '推荐' },
    { name: '烟酰胺美白身体乳', brand: '凡士林', price: 79, sold: 7654, rating: 92, func: '美白', tag: '平价' },
    { name: '视黄醇晚霜 30g', brand: '珀莱雅', price: 229, sold: 3876, rating: 95, func: '抗皱', tag: '国货' },
    { name: '玻尿酸原液补水', brand: '润百颜', price: 168, sold: 6231, rating: 94, func: '补水', tag: '爆款' },
    { name: '水杨酸棉片刷酸', brand: '博乐达', price: 129, sold: 4567, rating: 93, func: '祛痘', tag: '热卖' },
    { name: '寡肽原液修护痘印', brand: '夸迪', price: 199, sold: 3214, rating: 95, func: '修护', tag: '推荐' },
    { name: '咖啡因眼霜去浮肿', brand: 'TheOrdinary', price: 98, sold: 5623, rating: 91, func: '眼部', tag: '平价' },
    { name: 'A 醇眼霜抗初老', brand: '欧莱雅', price: 219, sold: 4321, rating: 94, func: '眼部', tag: '热卖' },
    { name: '泥膜深层清洁 125ml', brand: '科颜氏', price: 290, sold: 3876, rating: 93, func: '清洁', tag: '口碑' },
    { name: '修护屏障面霜 50g', brand: '适乐肤', price: 189, sold: 6891, rating: 96, func: '修护', tag: '爆款' },
    { name: '果酸身体乳改善鸡皮', brand: '阿尔法', price: 139, sold: 2987, rating: 92, func: '焕肤', tag: '推荐' },
    { name: '防晒喷雾补涂便携', brand: '娜丽丝', price: 99, sold: 5123, rating: 90, func: '防晒', tag: '实惠' },
    { name: '晚安冻膜免洗款', brand: '兰芝', price: 195, sold: 3421, rating: 92, func: '补水', tag: '热卖' },
    { name: '虾青素精华抗氧化', brand: '自然堂', price: 179, sold: 4532, rating: 94, func: '抗氧', tag: '国货' },
    { name: '唇膜夜间修护 20g', brand: '兰芝', price: 125, sold: 5678, rating: 95, func: '唇部', tag: '爆款' },
    { name: '头皮精华防脱固发', brand: '馥绿德雅', price: 328, sold: 2143, rating: 93, func: '头皮', tag: '口碑' },
    { name: '美白精华烟酰胺 30ml', brand: 'Olay', price: 199, sold: 7856, rating: 95, func: '美白', tag: '爆款' }
  ]

  private skinsList: SkinItem[] = [
    { name: '高浓度玻尿酸精华', ingredient: '玻尿酸', conc: 2, price: 168, volume: 30 },
    { name: '烟酰胺焕白精华', ingredient: '烟酰胺', conc: 5, price: 149, volume: 30 },
    { name: '视黄醇抗皱精华', ingredient: '视黄醇', conc: 1, price: 229, volume: 30 },
    { name: 'VC 亮肤安瓶', ingredient: '维C', conc: 15, price: 199, volume: 20 },
    { name: '神经酰胺修护霜', ingredient: '神经酰胺', conc: 3, price: 189, volume: 50 },
    { name: '水杨酸焕肤棉片', ingredient: '水杨酸', conc: 2, price: 129, volume: 60 },
    { name: '寡肽祛痘原液', ingredient: '寡肽', conc: 3, price: 139, volume: 30 },
    { name: '依克多因修护液', ingredient: '依克多因', conc: 1, price: 219, volume: 30 },
    { name: '麦角硫因抗氧精华', ingredient: '麦角硫因', conc: 1, price: 299, volume: 30 },
    { name: '补骨脂酚舒缓精华', ingredient: '补骨脂酚', conc: 1, price: 259, volume: 30 },
    { name: '传明酸淡斑精华', ingredient: '传明酸', conc: 3, price: 239, volume: 30 },
    { name: '蓝铜胜肽紧致液', ingredient: '蓝铜胜肽', conc: 1, price: 329, volume: 30 }
  ]

  private makeupList: MakeupItem[] = [
    { name: '哑光唇釉持色不脱色', shade: '枫叶红', number: 12, price: 129, heat: 96 },
    { name: '水光唇泥奶杏色', shade: '奶杏', number: 8, price: 89, heat: 93 },
    { name: '镜面唇冻果冻感', shade: '草莓色', number: 10, price: 99, heat: 90 },
    { name: '十二色眼影大地盘', shade: '大地色', number: 12, price: 159, heat: 95 },
    { name: '九色眼影蜜桃盘', shade: '蜜桃色', number: 9, price: 129, heat: 92 },
    { name: '空气蜜粉定雾面', shade: '透明色', number: 1, price: 119, heat: 88 },
    { name: '持妆粉底液油皮亲妈', shade: '象牙白', number: 6, price: 249, heat: 94 },
    { name: '气垫 BB 轻薄遮瑕', shade: '自然色', number: 4, price: 199, heat: 91 },
    { name: '睫毛膏卷翘纤长', shade: '浓黑', number: 1, price: 89, heat: 87 },
    { name: '染眉膏雾感自然', shade: '棕灰', number: 3, price: 69, heat: 85 }
  ]

  private perfumes: PerfumeItem[] = [
    { name: '花漾甜心女士淡香水', note: '花香调', lasting: 6, price: 599, stock: 124 },
    { name: '木质绅士男士淡香水', note: '木质调', lasting: 8, price: 689, stock: 98 },
    { name: '海盐鼠尾草中性香', note: '海洋调', lasting: 5, price: 459, stock: 156 },
    { name: '橙花初绽夏日限定', note: '柑橘调', lasting: 4, price: 389, stock: 213 },
    { name: '白茶清冷文艺香', note: '木质调', lasting: 6, price: 499, stock: 87 },
    { name: '玫瑰木质中东风情', note: '东方调', lasting: 9, price: 759, stock: 65 },
    { name: '香草奶杏美食调', note: '美食调', lasting: 7, price: 429, stock: 178 },
    { name: '青柠罗勒沁凉香', note: '柑橘调', lasting: 4, price: 349, stock: 245 },
    { name: '铃兰沐雨清新淡香', note: '花香调', lasting: 5, price: 399, stock: 134 },
    { name: '乌木沉香高定款', note: '东方调', lasting: 10, price: 999, stock: 42 }
  ]

  private stores: StoreItem[] = [
    { name: '国贸美妆体验店', dist: 1.1, projects: 12, booked: 38, capacity: 45 },
    { name: '三里屯旗舰店', dist: 2.5, projects: 18, booked: 52, capacity: 60 },
    { name: '西单大悦城店', dist: 3.2, projects: 15, booked: 41, capacity: 50 },
    { name: '望京凯德店', dist: 4.6, projects: 10, booked: 22, capacity: 40 },
    { name: '五棵松万达店', dist: 5.8, projects: 9, booked: 18, capacity: 35 },
    { name: '合生汇体验店', dist: 3.9, projects: 14, booked: 33, capacity: 42 },
    { name: '颐堤港精品店', dist: 6.2, projects: 16, booked: 47, capacity: 55 },
    { name: '亦庄华联店', dist: 7.4, projects: 8, booked: 12, capacity: 30 },
    { name: '丽泽天街店', dist: 8.1, projects: 11, booked: 26, capacity: 38 },
    { name: '首钢园快闪店', dist: 9.3, projects: 6, booked: 9, capacity: 25 }
  ]

  private messages: MessageItem[] = [
    { title: '小样发货', content: '您的香水小样盲盒已发出,顺丰包裹', time: '11:32', unread: 2 },
    { title: '体验预约', content: '三里屯旗舰店免费试妆预约成功', time: '10:20', unread: 1 },
    { title: '会员礼遇', content: '颜值研究所会员日:大牌小样买 3 送 1', time: '09:12', unread: 3 },
    { title: '补货通知', content: '您收藏的哑光唇釉枫叶红补货啦', time: '昨天', unread: 1 },
    { title: '肤质报告', content: '本月肤质检测报告已生成,含护理建议', time: '昨天', unread: 0 },
    { title: '优惠到账', content: '美妆节 400 减 80 券已到卡包', time: '前天', unread: 0 },
    { title: '订单完成', content: '神仙水已签收,记得低温保存', time: '前天', unread: 0 },
    { title: '新品速递', content: '大牌秋冬新品彩妆盘上线预售', time: '周三', unread: 1 },
    { title: '空瓶回收', content: '空瓶回收计划:5 个空瓶换 1 支唇釉', time: '周三', unread: 0 },
    { title: '降价提醒', content: '购物车里的防晒降价 50 元', time: '周二', unread: 0 },
    { title: '直播预告', content: '今晚 8 点大牌美妆直播,整点抽免单', time: '周一', unread: 0 },
    { title: '系统公告', content: '小样订阅服务上线,可随时调整频率', time: '上周', unread: 0 }
  ]

  private maxConc(): number {
    let m: number = 0
    for (let i = 0; i < this.skinsList.length; i++) {
      if (this.skinsList[i].conc > m) {
        m = this.skinsList[i].conc
      }
    }
    return m
  }

  private maxBooked(): number {
    let m: number = 0
    for (let i = 0; i < this.stores.length; i++) {
      if (this.stores[i].booked > m) {
        m = this.stores[i].booked
      }
    }
    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 selBrand(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.brand
  }

  private selRating(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.rating
  }

  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.tabSkin()
            } else if (this.currentTab === 2) {
              this.tabMakeup()
            } else if (this.currentTab === 3) {
              this.tabPerfume()
            } else if (this.currentTab === 4) {
              this.tabStore()
            } 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.showSample) {
        this.modalSample()
      }
      if (this.showStore) {
        this.modalStore()
      }
      if (this.showDelete) {
        this.modalDelete()
      }
      if (this.showDetail) {
        this.modalDetail()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FDF4F8')
  }

  @Builder
  headerBar() {
    Column() {
      Row() {
        Column() {
          Text('颜值研究所')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
        }
        Column() {
          Row() {
            Text('搜索精华 / 口红 / 香水小样')
              .fontSize(12)
              .fontColor('#F8BBD0')
          }
          .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('美妆节 · 大牌满400减80')
          .fontSize(12)
          .fontColor('#F8BBD0')
          .fontWeight(FontWeight.Medium)
        Text('小样 9.9 试用')
          .fontSize(12)
          .fontColor('#F8BBD0')
          .margin({ left: 14 })
        Text('国货之光专区')
          .fontSize(12)
          .fontColor('#F8BBD0')
          .margin({ left: 14 })
      }
      .width('100%')
      .padding({ left: 14, bottom: 8 })
      .justifyContent(FlexAlign.Start)
    }
    .width('100%')
    .backgroundColor('#D81B60')
  }

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

  @Builder
  tabMarket() {
    Column() {
      Row() {
        Column() {
          Text('美妆节 · 大牌补贴')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('雅诗兰黛 / SK-II / 资生堂 直降 20%')
            .fontSize(12)
            .fontColor('#F8BBD0')
            .margin({ top: 6 })
        }
        .layoutWeight(2)
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('9842')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFE082')
          Text('爆款已售')
            .fontSize(10)
            .fontColor('#F8BBD0')
            .margin({ top: 2 })
          Text('领券')
            .fontSize(11)
            .fontColor('#8E24AA')
            .backgroundColor('#FFE082')
            .borderRadius(12)
            .padding({ left: 12, right: 12, top: 4, bottom: 4 })
            .margin({ top: 8 })
            .scale({ x: 1.06, y: 1.06 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#D81B60')
      .borderRadius(12)
      .alignItems(VerticalAlign.Center)

      Row() {
        Column() {
          Text('24')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
          Text('大牌单品')
            .fontSize(10)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(58)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10 })

        Column() {
          Text('98%')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#8E24AA')
          Text('最高好评')
            .fontSize(10)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(58)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })

        Column() {
          Text('9.9')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
          Text('小样试用')
            .fontSize(10)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(58)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })
        .onClick(() => {
          this.showSample = true
        })

        Column() {
          Text('发布')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('闲置转让')
            .fontSize(10)
            .fontColor('#F8BBD0')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(58)
        .backgroundColor('#8E24AA')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })
        .onClick(() => {
          this.showPublish = true
        })
      }
      .width('100%')

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

      Column() {
        ForEach(this.beauties, (b: BeautyItem) => {
          Row() {
            Column()
              .width(84)
              .height(84)
              .backgroundColor('#F8BBD0')
              .borderRadius(12)
            Column() {
              Text(b.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text(b.brand)
                  .fontSize(10)
                  .fontColor('#8E24AA')
                  .backgroundColor('#F3E5F5')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text(b.func)
                  .fontSize(10)
                  .fontColor('#D81B60')
                  .backgroundColor('#FCE4EC')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .margin({ left: 6 })
                Text(b.tag)
                  .fontSize(10)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#D81B60')
                  .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('#F0F0F0')
                  .borderRadius(3)
                Column()
                  .width(b.rating + '%')
                  .height(6)
                  .backgroundColor('#D81B60')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 8 })

              Row() {
                Text('¥' + b.price)
                  .fontSize(17)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#D81B60')
                Text('已售' + b.sold)
                  .fontSize(11)
                  .fontColor('#999999')
                  .margin({ left: 8 })
                Text('试用装')
                  .fontSize(11)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#8E24AA')
                  .borderRadius(10)
                  .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                  .margin({ left: 6 })
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceBetween)
              .alignItems(VerticalAlign.Center)
              .margin({ top: 8 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .onClick(() => {
            this.selectedItem = b
            this.showDetail = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabSkin() {
    Column() {
      Column() {
        Row() {
          Text('护肤成分实验室')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('有效浓度对比 (%)')
            .fontSize(11)
            .fontColor('#F8BBD0')
            .margin({ left: 8 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Row() {
          ForEach(this.skinsList, (s: SkinItem) => {
            Column() {
              Column()
                .width(15)
                .height(s.conc / this.maxConc() * 78 + 4)
                .backgroundColor('#F48FB1')
                .borderRadius(4)
              Text(s.conc + '')
                .fontSize(9)
                .fontColor('#F8BBD0')
                .margin({ top: 3 })
            }
            .margin({ right: 9 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .height(108)
        .justifyContent(FlexAlign.Start)
        .margin({ top: 10 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#6A1B9A')
      .borderRadius(12)
      .alignItems(HorizontalAlign.Start)

      Row() {
        Column() {
          Text('小样订阅')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('月度盲盒 · 惊喜试用')
            .fontSize(10)
            .fontColor('#F8BBD0')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(52)
        .backgroundColor('#8E24AA')
        .borderRadius(10)
        .justifyContent(FlexAlign.Center)
        .margin({ top: 10 })
        .onClick(() => {
          this.showSample = true
        })

        Column() {
          Text('发布闲置')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('护肤品转让')
            .fontSize(10)
            .fontColor('#F8BBD0')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(52)
        .backgroundColor('#D81B60')
        .borderRadius(10)
        .justifyContent(FlexAlign.Center)
        .margin({ top: 10, left: 8 })
        .onClick(() => {
          this.showPublish = true
        })
      }
      .width('100%')

      Column() {
        ForEach(this.skinsList, (s: SkinItem) => {
          Row() {
            Column() {
              Text(s.ingredient)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
            }
            .width(64)
            .height(64)
            .backgroundColor('#F3E5F5')
            .borderRadius(10)
            .justifyContent(FlexAlign.Center)

            Column() {
              Text(s.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text('浓度 ' + s.conc + '%')
                  .fontSize(10)
                  .fontColor('#8E24AA')
                  .backgroundColor('#F3E5F5')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text(s.volume + 'ml')
                  .fontSize(10)
                  .fontColor('#999999')
                  .margin({ left: 8 })
                Text('敏感肌可用')
                  .fontSize(10)
                  .fontColor('#D81B60')
                  .margin({ left: 6 })
              }
              .margin({ top: 6 })

              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(5)
                  .backgroundColor('#F0F0F0')
                  .borderRadius(3)
                Column()
                  .width(s.conc / this.maxConc() * 100 + '%')
                  .height(5)
                  .backgroundColor('#F48FB1')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 8 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })

            Column() {
              Text('¥' + s.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D81B60')
              Text('买正装')
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#8E24AA')
                .borderRadius(10)
                .padding({ left: 10, right: 10, top: 4, bottom: 4 })
                .margin({ top: 6 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .onClick(() => {
            this.showSample = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabMakeup() {
    Column() {
      Column() {
        Text('彩妆色号图鉴')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Text('热门色号 · 上脸试色 · 无忧退换')
          .fontSize(11)
          .fontColor('#999999')
          .margin({ top: 4 })
      }
      .width('100%')
      .padding({ top: 14, bottom: 14, left: 14 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .alignItems(HorizontalAlign.Start)
      .margin({ top: 10 })

      Column() {
        Text('色号数量对比')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Row() {
          ForEach(this.makeupList, (m: MakeupItem) => {
            Column() {
              Column()
                .width(15)
                .height(m.number / 12 * 78)
                .backgroundColor('#D81B60')
                .borderRadius(4)
              Text(m.number + '')
                .fontSize(9)
                .fontColor('#999999')
                .margin({ top: 4 })
            }
            .margin({ right: 9 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .height(108)
        .justifyContent(FlexAlign.Start)
        .margin({ top: 8 })
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 10 })
      .alignItems(HorizontalAlign.Start)

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.makeupList, (m: MakeupItem) => {
          Column() {
            Row() {
              Column()
                .width(18)
                .height(18)
                .backgroundColor('#D81B60')
                .borderRadius(9)
              Text(m.shade)
                .fontSize(10)
                .fontColor('#8E24AA')
                .backgroundColor('#F3E5F5')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)

            Text(m.name)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .maxLines(2)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
              .margin({ top: 8 })

            Column() {
              Text('热度 ' + m.heat)
                .fontSize(10)
                .fontColor('#999999')
              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(5)
                  .backgroundColor('#F0F0F0')
                  .borderRadius(3)
                Column()
                  .width(m.heat + '%')
                  .height(5)
                  .backgroundColor('#F48FB1')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 4 })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 6 })

            Row() {
              Text('¥' + m.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D81B60')
              Text(m.number + ' 色')
                .fontSize(10)
                .fontColor('#999999')
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)
            .margin({ top: 6 })
          }
          .width('48%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .alignItems(HorizontalAlign.Start)
          .onClick(() => {
            this.showDetail = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabPerfume() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('香水小样星球')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('9.9 元试用 · 盲盒月订 · 正装回购')
              .fontSize(11)
              .fontColor('#F8BBD0')
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Text('立即试用')
            .fontSize(12)
            .fontColor('#6A1B9A')
            .backgroundColor('#FFE082')
            .borderRadius(14)
            .padding({ left: 14, right: 14, top: 6, bottom: 6 })
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            .onClick(() => {
              this.showSample = true
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#8E24AA')
      .borderRadius(12)

      Scroll() {
        Row() {
          ForEach(this.perfumes, (p: PerfumeItem) => {
            Column() {
              Column()
                .width(96)
                .height(60)
                .backgroundColor('#F3E5F5')
                .borderRadius(10)
              Text(p.note)
                .fontSize(10)
                .fontColor('#8E24AA')
                .backgroundColor('#F3E5F5')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .margin({ top: 6 })
              Text('¥' + p.price)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D81B60')
                .margin({ top: 4 })
            }
            .width(96)
            .padding(8)
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .margin({ right: 8 })
            .alignItems(HorizontalAlign.Center)
          })
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .margin({ top: 10 })

      Column() {
        ForEach(this.perfumes, (p: PerfumeItem) => {
          Row() {
            Column()
              .width(52)
              .height(52)
              .backgroundColor('#F3E5F5')
              .borderRadius(26)
            Column() {
              Text(p.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text(p.note)
                  .fontSize(10)
                  .fontColor('#8E24AA')
                  .backgroundColor('#F3E5F5')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text('留香 ' + p.lasting + ' 小时')
                  .fontSize(10)
                  .fontColor('#999999')
                  .margin({ left: 8 })
                if (p.stock < 100) {
                  Text('限量')
                    .fontSize(10)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#D81B60')
                    .borderRadius(4)
                    .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                    .margin({ left: 6 })
                    .scale({ x: 1.08, y: 1.08 })
                    .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
                }
              }
              .margin({ top: 6 })

              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(5)
                  .backgroundColor('#F0F0F0')
                  .borderRadius(3)
                Column()
                  .width(p.lasting * 10 + '%')
                  .height(5)
                  .backgroundColor('#8E24AA')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 8 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })

            Column() {
              Text('¥' + p.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#D81B60')
              Text('9.9 试用')
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#8E24AA')
                .borderRadius(10)
                .padding({ left: 8, right: 8, top: 4, bottom: 4 })
                .margin({ top: 6 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .onClick(() => {
            this.showSample = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabStore() {
    Column() {
      Column() {
        Row() {
          Text('美妆体验门店')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('本周预约热度')
            .fontSize(11)
            .fontColor('#F8BBD0')
            .margin({ left: 8 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Row() {
          ForEach(this.stores, (s: StoreItem) => {
            Column() {
              Column()
                .width(15)
                .height(s.booked / this.maxBooked() * 78)
                .backgroundColor('#CE93D8')
                .borderRadius(4)
              Text(s.booked + '')
                .fontSize(9)
                .fontColor('#F8BBD0')
                .margin({ top: 3 })
            }
            .margin({ right: 9 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .height(108)
        .justifyContent(FlexAlign.Start)
        .margin({ top: 10 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#4A148C')
      .borderRadius(12)
      .alignItems(HorizontalAlign.Start)

      Column() {
        ForEach(this.stores, (s: StoreItem) => {
          Row() {
            Column()
              .width(50)
              .height(50)
              .backgroundColor('#F3E5F5')
              .borderRadius(10)
            Column() {
              Row() {
                Text(s.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#333333')
                Text(s.projects + ' 个项目')
                  .fontSize(11)
                  .fontColor('#8E24AA')
                  .fontWeight(FontWeight.Bold)
                  .margin({ left: 8 })
              }
              Text('距离 ' + s.dist + 'km · 容量 ' + s.capacity + ' 人')
                .fontSize(11)
                .fontColor('#999999')
                .margin({ top: 4 })

              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(6)
                  .backgroundColor('#F0F0F0')
                  .borderRadius(3)
                Column()
                  .width(s.booked / s.capacity * 100 + '%')
                  .height(6)
                  .backgroundColor(s.booked / s.capacity > 0.85 ? '#D81B60' : '#8E24AA')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 6 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })

            Column() {
              Text(s.booked + '/' + s.capacity)
                .fontSize(11)
                .fontColor('#999999')
              Text('预约')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#D81B60')
                .borderRadius(10)
                .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                .margin({ top: 6 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .onClick(() => {
            this.showStore = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabMessage() {
    Column() {
      Column() {
        Row() {
          Text('消息中心')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
          Text('8 条新动态')
            .fontSize(11)
            .fontColor('#D81B60')
            .margin({ left: 8 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .width('100%')
      .padding({ top: 14, bottom: 14, left: 14, right: 14 })
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 10 })

      Column() {
        ForEach(this.messages, (m: MessageItem) => {
          Row() {
            Column()
              .width(46)
              .height(46)
              .backgroundColor('#F3E5F5')
              .borderRadius(23)
            Column() {
              Row() {
                Text(m.title)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#333333')
                if (m.unread > 0) {
                  Text(m.unread + '')
                    .fontSize(10)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#D81B60')
                    .borderRadius(9)
                    .padding({ left: 6, right: 6, top: 1, bottom: 1 })
                    .margin({ left: 6 })
                    .scale({ x: 1.1, y: 1.1 })
                    .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
                }
                Text(m.time)
                  .fontSize(10)
                  .fontColor('#BBBBBB')
              }
              .width('100%')
              .justifyContent(FlexAlign.SpaceBetween)

              Text(m.content)
                .fontSize(12)
                .fontColor('#666666')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
                .margin({ top: 6 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(12)
          .margin({ top: 10 })
          .onClick(() => {
            this.showDelete = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabMine() {
    Column() {
      Column() {
        Row() {
          Column()
            .width(60)
            .height(60)
            .backgroundColor('#F3E5F5')
            .borderRadius(30)
          Column() {
            Text('精致女孩_Momo')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('颜值等级:成分党达人 Lv.8')
              .fontSize(11)
              .fontColor('#F8BBD0')
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .padding({ left: 12 })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#6A1B9A')
      .borderRadius(12)

      Row() {
        Column() {
          Text('68')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
          Text('美妆订单')
            .fontSize(11)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(64)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10 })

        Column() {
          Text('6')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#8E24AA')
          Text('到店体验')
            .fontSize(11)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(64)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })

        Column() {
          Text('1240')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
          Text('颜值积分')
            .fontSize(11)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(64)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })

        Column() {
          Text('9')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#8E24AA')
          Text('优惠券')
            .fontSize(11)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(64)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })
      }
      .width('100%')

      Column() {
        ForEach(['我的肤质档案', '小样订阅管理', '空瓶回收记录', '到店体验预约', ' wishlist 心愿单', '帮助与反馈'], (item: string) => {
          Row() {
            Text(item)
              .fontSize(14)
              .fontColor('#333333')
            Text('>')
              .fontSize(14)
              .fontColor('#CCCCCC')
          }
          .width('100%')
          .padding({ top: 14, bottom: 14 })
          .justifyContent(FlexAlign.SpaceBetween)
          .onClick(() => {
            this.showDelete = true
          })
        })
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .padding({ left: 14, right: 14 })
      .margin({ top: 12 })
    }
    .width('100%')
  }

  @Builder
  modalPublish() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('发布闲置美妆')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
          Text('关闭')
            .fontSize(13)
            .fontColor('#999999')
            .padding(6)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Column() {
          Text('产品名称')
            .fontSize(13)
            .fontColor('#666666')
          TextInput({ placeholder: '如:小棕瓶精华 30ml 八成新' })
            .height(40)
            .fontSize(13)
            .backgroundColor('#FDF4F8')
            .borderRadius(8)
            .margin({ top: 6 })
            .onChange((value: string) => {
              this.inputName = value
            })
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('产品品类')
            .fontSize(13)
            .fontColor('#666666')
          Row() {
            ForEach(this.types, (t: string, idx: number) => {
              Text(t)
                .fontSize(12)
                .fontColor(this.selectedType === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedType === idx ? '#D81B60' : '#FDF4F8')
                .borderRadius(14)
                .padding({ left: 14, right: 14, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedType = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('适合肤质')
            .fontSize(13)
            .fontColor('#666666')
          Row() {
            ForEach(this.skins, (s: string, idx: number) => {
              Text(s)
                .fontSize(12)
                .fontColor(this.selectedSkin === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedSkin === idx ? '#8E24AA' : '#FDF4F8')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedSkin = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('预估价:¥' + ((this.selectedType + 1) * 45 + this.selectedSkin * 12))
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
            .scale({ x: 1.04, y: 1.04 })
            .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .margin({ top: 20 })
        .alignItems(HorizontalAlign.Start)

        Text('确认发布')
          .fontSize(15)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(44)
          .textAlign(TextAlign.Center)
          .backgroundColor('#D81B60')
          .borderRadius(22)
          .margin({ top: 20 })
          .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
  modalSample() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('香水小样订阅')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
          Text('¥' + ((this.selectedSize + 1) * 19 + this.selectedNote * 6) * this.qty)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
            .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('#666666')
          Row() {
            ForEach(this.notes, (n: string, idx: number) => {
              Text(n)
                .fontSize(12)
                .fontColor(this.selectedNote === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedNote === idx ? '#8E24AA' : '#FDF4F8')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedNote = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('小样规格')
            .fontSize(13)
            .fontColor('#666666')
          Row() {
            ForEach(this.sizes, (s: string, idx: number) => {
              Text(s)
                .fontSize(12)
                .fontColor(this.selectedSize === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedSize === idx ? '#D81B60' : '#FDF4F8')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedSize = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('盒数:' + this.qty)
            .fontSize(13)
            .fontColor('#333333')
            .fontWeight(FontWeight.Bold)
          Row() {
            Text('-')
              .fontSize(16)
              .fontColor('#8E24AA')
              .width(34)
              .height(34)
              .textAlign(TextAlign.Center)
              .backgroundColor('#F3E5F5')
              .borderRadius(8)
              .onClick(() => {
                if (this.qty > 1) {
                  this.qty -= 1
                }
              })
            Text(this.qty + '')
              .fontSize(15)
              .fontColor('#333333')
              .width(50)
              .textAlign(TextAlign.Center)
            Text('+')
              .fontSize(16)
              .fontColor('#D81B60')
              .width(34)
              .height(34)
              .textAlign(TextAlign.Center)
              .backgroundColor('#FCE4EC')
              .borderRadius(8)
              .onClick(() => {
                if (this.qty < 12) {
                  this.qty += 1
                }
              })
          }
          .margin({ top: 8 })
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Text('每盒 3 支惊喜香样,附回购优惠券')
          .fontSize(11)
          .fontColor('#999999')
          .margin({ top: 10 })

        Text('确认订阅')
          .fontSize(15)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(44)
          .textAlign(TextAlign.Center)
          .backgroundColor('#8E24AA')
          .borderRadius(22)
          .margin({ top: 16 })
          .onClick(() => {
            this.showSample = 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.showSample = false
    })
  }

  @Builder
  modalStore() {
    Column() {
      Column().layoutWeight(1).width('100%')
      Column() {
        Row() {
          Text('到店体验预约')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
          Text('到店礼')
            .fontSize(12)
            .fontColor('#FFFFFF')
            .backgroundColor('#8E24AA')
            .borderRadius(8)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 })
            .scale({ x: 1.06, y: 1.06 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text('体验项目')
            .fontSize(13)
            .fontColor('#666666')
          Row() {
            ForEach(this.projects, (p: string, idx: number) => {
              Text(p)
                .fontSize(12)
                .fontColor(this.selectedProject === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedProject === idx ? '#D81B60' : '#FDF4F8')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedProject = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('到店时段')
            .fontSize(13)
            .fontColor('#666666')
          Row() {
            ForEach(this.slots, (s: string, idx: number) => {
              Text(s)
                .fontSize(12)
                .fontColor(this.selectedSlot === idx ? '#FFFFFF' : '#666666')
                .backgroundColor(this.selectedSlot === idx ? '#8E24AA' : '#FDF4F8')
                .borderRadius(8)
                .padding({ left: 14, right: 14, top: 8, bottom: 8 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selectedSlot = idx
                })
            })
          }
          .width('100%')
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('体验权益')
            .fontSize(13)
            .fontColor('#666666')
          Text('专业彩妆师 1v1 服务 · 全场小样免费领 · 消费满赠正装')
            .fontSize(12)
            .fontColor('#8E24AA')
            .margin({ top: 6 })
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(HorizontalAlign.Start)

        Text('提交预约')
          .fontSize(15)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(44)
          .textAlign(TextAlign.Center)
          .backgroundColor('#D81B60')
          .borderRadius(22)
          .margin({ top: 20 })
          .onClick(() => {
            this.showStore = 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.showStore = false
    })
  }

  @Builder
  modalDelete() {
    Column() {
      Column() {
        Column()
          .width(56)
          .height(56)
          .backgroundColor('#FCE4EC')
          .borderRadius(28)
        Text('确认删除该记录?')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .margin({ top: 14 })
        Text('删除后订阅与预约记录将无法恢复')
          .fontSize(12)
          .fontColor('#999999')
          .margin({ top: 8 })
          .textAlign(TextAlign.Center)

        Row() {
          Text('再想想')
            .fontSize(14)
            .fontColor('#666666')
            .layoutWeight(1)
            .height(42)
            .textAlign(TextAlign.Center)
            .backgroundColor('#FDF4F8')
            .borderRadius(21)
            .onClick(() => {
              this.showDelete = false
            })
          Text('确认删除')
            .fontSize(14)
            .fontColor('#FFFFFF')
            .layoutWeight(1)
            .height(42)
            .textAlign(TextAlign.Center)
            .backgroundColor('#D81B60')
            .borderRadius(21)
            .margin({ left: 12 })
            .scale({ x: 1.03, y: 1.03 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            .onClick(() => {
              this.showDelete = false
            })
        }
        .width('100%')
        .margin({ top: 22 })
      }
      .width('80%')
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .padding(22)
      .alignItems(HorizontalAlign.Center)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showDelete = false
    })
  }

  @Builder
  modalDetail() {
    Row() {
      Column().layoutWeight(1).height('100%')
      Column() {
        Text('单品详情')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Text(this.selName())
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#8E24AA')
          .margin({ top: 14 })
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Row() {
          Text(this.selBrand())
            .fontSize(11)
            .fontColor('#D81B60')
            .backgroundColor('#FCE4EC')
            .borderRadius(4)
            .padding({ left: 8, right: 8, top: 3, bottom: 3 })
          Text('已售 ' + this.selSold())
            .fontSize(11)
            .fontColor('#999999')
            .margin({ left: 10 })
        }
        .margin({ top: 10 })

        Column() {
          Text('好评率 ' + this.selRating() + '%')
            .fontSize(12)
            .fontColor('#666666')
          Stack({ alignContent: Alignment.Start }) {
            Column()
              .width('100%')
              .height(8)
              .backgroundColor('#F0F0F0')
              .borderRadius(4)
            Column()
              .width(this.selRating() + '%')
              .height(8)
              .backgroundColor('#D81B60')
              .borderRadius(4)
          }
          .width('100%')
          .margin({ top: 6 })
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 16 })

        Row() {
          Text('¥' + this.selPrice())
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#D81B60')
            .scale({ x: 1.06, y: 1.06 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('自营正装')
            .fontSize(11)
            .fontColor('#8E24AA')
            .margin({ left: 10 })
        }
        .width('100%')
        .alignItems(VerticalAlign.Bottom)
        .margin({ top: 16 })

        Text('立即购买')
          .fontSize(14)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .height(42)
          .textAlign(TextAlign.Center)
          .backgroundColor('#D81B60')
          .borderRadius(21)
          .margin({ top: 20 })
          .onClick(() => {
            this.showDetail = false
            this.showSample = true
          })

        Text('加入心愿单')
          .fontSize(14)
          .fontColor('#8E24AA')
          .width('100%')
          .height(42)
          .textAlign(TextAlign.Center)
          .backgroundColor('#F3E5F5')
          .borderRadius(21)
          .margin({ top: 10 })
          .onClick(() => {
            this.showDetail = false
          })
      }
      .width('78%')
      .height('100%')
      .backgroundColor('#FFFFFF')
      .padding(16)
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .onClick(() => {
      this.showDetail = false
    })
  }
}


总结

在这里插入图片描述

本文对基于 HarmonyOS 声明式 UI 框架开发的美妆电商应用"颜值研究所"进行了全面的代码级深度解析。该应用通过六个强类型接口定义了美妆商品、护肤成分、彩妆产品、香水小样、线下门店和消息通知的数据结构,通过二十余个 @State 状态变量管理应用的 UI 状态和用户选择状态,通过 @Builder 装饰器封装了七个标签页构建器、两个导航栏构建器和五个弹窗构建器,构建了一个功能完整、交互丰富的电商应用界面。应用充分利用了 ArkTS 的类型系统、装饰器机制和声明式 UI 组件模型,展示了鸿蒙生态在构建复杂业务场景应用方面的强大能力。

在数据可视化方面,应用通过 Stack 组件叠加 Column 实现了进度条效果,通过 ForEach 遍历数组数据动态生成柱状图柱子,通过归一化计算(当前值/最大值)确保不同量级的数据在同一比例尺下展示。这些可视化技术不依赖任何第三方图表库,完全使用 ArkTS 原生组件实现,体现了声明式 UI 在自定义可视化方面的灵活性。同时,应用通过 scaleanimation 属性的组合使用,在多个关键元素上实现了缩放脉动循环动画,有效提升了界面的动态感和用户注意力引导效果。

在交互设计方面,应用采用了底部弹出和居中对话框两种弹窗模式,通过 rgba(0,0,0,0.5) 半透明遮罩实现模态效果,通过 event.stopPropagation() 实现点击穿透阻止。状态管理上,通过多个布尔变量控制弹窗显隐,通过 selectedItem 联合类型变量实现选中商品的空值安全传递。价格计算采用动态公式,根据用户的选择实时更新,配合脉动动画增强反馈感。整体而言,该应用代码结构清晰、功能覆盖全面,是 HarmonyOS 声明式 UI 开发的优秀实践案例,为开发者构建类似复杂电商应用提供了有价值的参考。

Logo

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

更多推荐