引言

在这里插入图片描述

咖啡与茗茶作为现代人日常生活中不可或缺的饮品品类,其消费场景已从简单的商品购买演变为涵盖产地溯源、烘焙度选择、周期订阅、门店体验和器具冲泡等复合维度的深度消费。本应用立足于HarmonyOS 6.1.1声明式开发范式,采用ArkTS语言和ArkUI声明式组件框架,构建了一个集咖啡集市、庄园咖啡豆、茶叶专区、器具冲泡、门店体验、消息中心和用户中心于一体的综合性咖啡茗茶生活服务平台。

在技术架构层面,本应用以@Entry@Component装饰器为入口,通过@State状态管理机制实现UI与数据的响应式绑定。应用采用单页面多Tab架构,通过currentTab状态变量驱动条件渲染切换七个功能模块。在业务深度上,本应用引入了咖啡豆周期订阅流程(支持豆种选择、研磨粗细和订阅周期配置)、门店手冲体验预约(支持门店选择、到店人数和体验内容展示)和闲置咖啡茶品发布等复合业务交互。五个模态弹窗通过Stack布局叠加在主内容之上,实现了复杂业务交互而无需页面跳转。

从数据可视化设计的角度来看,该应用将庄园咖啡豆的海拔高度、茶叶发酵度、器具容量和门店预约热度等关键参数通过纯ArkUI柱状图进行直观展示。每个功能Tab都内置了不同维度的数据可视化组件,让用户在选购咖啡豆和茶叶时能够快速把握产品间的参数差异。此外,应用还实现了杯测评分进度条、海拔进度条、好评率进度条、门店预约率进度条等多种自定义进度条组件,通过Stack嵌套Column的双层叠加模式实现轨道与填充效果。庄园咖啡豆列表还创新性地引入了带序号编号的排名展示设计,前三名使用橙色编号高亮,增强了竞技感和品质导向。

逐段代码分析

在这里插入图片描述

数据模型接口定义

应用首先定义了六个核心数据接口,分别对应咖啡商品、庄园咖啡豆、茶叶、冲泡器具、门店和系统消息等不同业务领域的数据结构。

interface CoffeeItem {
  name: string
  price: number
  sold: number
  origin: string
  roast: string
  score: number
}

interface BeanItem {
  name: string
  farm: string
  altitude: number
  price: number
  process: string
}

interface TeaItem {
  name: string
  type: string
  ferment: number
  price: number
  rating: number
}

interface GearItem {
  name: string
  material: string
  capacity: number
  price: number
  stock: number
}

interface ShopItem {
  name: string
  dist: number
  score: number
  seats: number
  booked: number
}

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

CoffeeItem接口定义了咖啡商品的核心属性,其中origin字段标识产区(如埃塞俄比亚、哥伦比亚、云南保山等),roast字段标识烘焙度,score字段表示杯测评分。BeanItem中的farm字段标识庄园名称,altitude字段表示种植海拔,process字段标识处理法(水洗、日晒、蜜处理等),这三个字段是精品咖啡溯源的核心参数。TeaItemtype字段标识茶类(绿茶、红茶、乌龙、普洱等),ferment字段表示发酵度百分比,是区分茶类的关键指标。GearItemmaterialcapacity字段分别标识器具材质和容量。ShopItemdist字段表示门店距离,seatsbooked字段分别表示总席位和已预约数,通过两者的比值可以计算预约率。

组件状态声明与配置数据

在这里插入图片描述

组件CoffeeTeaPage通过丰富的状态变量和配置数组,构建了应用运行时的数据基础和用户交互状态。

@Entry
@Component
struct CoffeeTeaPage {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showSubscribe: boolean = false
  @State showVisit: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: CoffeeItem | null = null
  @State selectedOrigin: number = 0
  @State selectedRoast: number = 0
  @State selectedBean: number = 0
  @State selectedGrind: number = 0
  @State selectedShop: number = 0
  @State selectedGuests: number = 0
  @State weeks: number = 4
  @State inputName: string = ''

  private tabs: string[] = ['咖啡集市', '咖啡豆', '茶叶专区', '器具冲泡', '门店体验', '消息', '我的']
  private origins: string[] = ['埃塞俄比亚', '哥伦比亚', '云南保山', '危地马拉', '巴拿马']
  private roasts: string[] = ['浅烘焙', '中烘焙', '中深烘焙', '深烘焙']
  private beans: string[] = ['瑰夏', '耶加雪菲', '蓝山', '曼特宁', '云南小粒']
  private grinds: string[] = ['手冲粗度', '法压中度', '摩卡细度', '意式极细']
  private shops: string[] = ['国贸旗舰店', '三里屯店', '望京小街店', '陆家嘴店']
  private guestOptions: string[] = ['1 人', '2 人', '3-4 人', '5 人以上']

currentTab驱动七个Tab页签的条件渲染。五个布尔状态变量分别控制发布闲置、订阅咖啡豆、门店预约、删除确认和商品详情五个模态弹窗。selectedItem采用CoffeeItem | null联合类型。应用还声明了selectedOriginselectedRoast用于咖啡集市的产区筛选,selectedBeanselectedGrind用于订阅弹窗的豆种和研磨度选择,selectedShopselectedGuests用于门店预约的门店和人数选择。weeks变量控制订阅周期,范围限制在1到24周之间。

咖啡集市数据集

咖啡数据集包含了24款不同产区和烘焙度的咖啡产品,从挂耳到冻干、从云南小粒到巴拿马瑰夏全品类覆盖。

private coffees: CoffeeItem[] = [
  { name: '瑰夏手冲挂耳 10 袋礼盒', price: 128, sold: 3214, origin: '巴拿马', roast: '浅烘焙', score: 97 },
  { name: '耶加雪菲日晒挂耳 20 袋', price: 88, sold: 5623, origin: '埃塞俄比亚', roast: '浅烘焙', score: 95 },
  { name: '蓝山一号咖啡豆 227g', price: 168, sold: 2134, origin: '哥伦比亚', roast: '中烘焙', score: 94 },
  { name: '云南保山小粒日晒豆', price: 58, sold: 4876, origin: '云南保山', roast: '中烘焙', score: 91 },
  { name: '危地马拉花神咖啡豆', price: 98, sold: 1856, origin: '危地马拉', roast: '中深烘焙', score: 93 },
  { name: '曼特宁黄金曼咖啡豆', price: 76, sold: 2341, origin: '巴拿马', roast: '深烘焙', score: 92 },
  { name: '冷萃冻干咖啡粉 3 秒即溶', price: 69, sold: 7842, origin: '云南保山', roast: '中烘焙', score: 96 },
  { name: '拿铁冻干咖啡礼盒 24 杯', price: 139, sold: 3267, origin: '哥伦比亚', roast: '中深烘焙', score: 94 },
  { name: '西达摩水洗挂耳 10 袋', price: 79, sold: 4521, origin: '埃塞俄比亚', roast: '浅烘焙', score: 95 },
  { name: '翡翠庄园红标瑰夏 100g', price: 299, sold: 876, origin: '巴拿马', roast: '浅烘焙', score: 99 },
  { name: '云南日晒卡迪姆 200g', price: 45, sold: 6234, origin: '云南保山', roast: '中烘焙', score: 89 },
  { name: '安提瓜火山咖啡豆', price: 108, sold: 1457, origin: '危地马拉', roast: '中深烘焙', score: 93 },
  { name: '椰香拿铁冻干条 30 条', price: 99, sold: 4312, origin: '海南', roast: '中深烘焙', score: 92 },
  { name: '瑰夏村竞标批次 50g', price: 388, sold: 432, origin: '埃塞俄比亚', roast: '浅烘焙', score: 98 },
  { name: '云南铁皮卡蜜处理 200g', price: 72, sold: 1987, origin: '云南保山', roast: '浅烘焙', score: 90 },
  { name: '哥伦比亚蕙兰咖啡豆', price: 68, sold: 2876, origin: '哥伦比亚', roast: '中烘焙', score: 91 },
  { name: '意式拼配金杯豆 1kg', price: 128, sold: 5163, origin: '巴拿马', roast: '深烘焙', score: 95 },
  { name: '低因瑞士水处理豆 227g', price: 118, sold: 965, origin: '哥伦比亚', roast: '中烘焙', score: 92 },
  { name: '手冲单品入门三豆组', price: 108, sold: 2743, origin: '云南保山', roast: '中烘焙', score: 93 },
  { name: '冷萃浓缩液 1L 大瓶装', price: 89, sold: 3562, origin: '云南保山', roast: '中深烘焙', score: 94 },
  { name: '挂耳咖啡混合口味 30 袋', price: 119, sold: 6987, origin: '埃塞俄比亚', roast: '中烘焙', score: 96 },
  { name: '麝香猫风味咖啡豆 100g', price: 158, sold: 654, origin: '巴拿马', roast: '中深烘焙', score: 88 },
  { name: '云南厌氧发酵黑蜜 200g', price: 96, sold: 1234, origin: '云南保山', roast: '浅烘焙', score: 95 },
  { name: '经典意式浓缩豆 500g', price: 85, sold: 4231, origin: '危地马拉', roast: '深烘焙', score: 92 }
]

每件咖啡商品的score杯测评分字段在UI渲染时通过Stack嵌套Column实现进度条可视化,评分越高进度条填充越满。origin产区标签使用橙色浅色背景,roast烘焙度标签使用绿色背景,两种颜色的交叉使用在视觉上区分产地信息和工艺信息。score字段配合scale缩放和animation动画实现脉冲式强调效果,杯测评分达到99分的翡翠庄园红标瑰夏是数据集中评分最高的产品。

庄园咖啡豆数据集

在这里插入图片描述

庄园咖啡豆数据集包含12款来自世界知名庄园的精品咖啡豆原豆,每款都标注了庄园名称、种植海拔和处理法。

private beanList: BeanItem[] = [
  { name: '翡翠庄园绿标瑰夏', farm: '翡翠庄园', altitude: 1650, price: 198, process: '水洗' },
  { name: '90+ 烛芒日晒', farm: 'Ninety Plus', altitude: 1800, price: 268, process: '日晒' },
  { name: '花魁 7.0 日晒原豆', farm: 'Hambela', altitude: 2100, price: 128, process: '日晒' },
  { name: '云南保山铁皮卡', farm: '佐园咖啡', altitude: 1500, price: 62, process: '水洗' },
  { name: '蓝山一号认证豆', farm: 'Clifton Mount', altitude: 1300, price: 168, process: '水洗' },
  { name: '黄金曼特宁 G1', farm: 'Takengon', altitude: 1400, price: 76, process: '湿刨' },
  { name: '哥斯达黎加黑蜜', farm: 'Tarrazu', altitude: 1600, price: 92, process: '黑蜜' },
  { name: '巴拿马驴子庄园', farm: 'El Burro', altitude: 1750, price: 156, process: '水洗' },
  { name: '肯尼亚 SL28 水洗', farm: 'Nyeri', altitude: 1900, price: 112, process: '水洗' },
  { name: '云南日晒卡杜拉', farm: '孟连庄园', altitude: 1350, price: 48, process: '日晒' },
  { name: '危地马拉薇薇特南果', farm: 'Huehuetenango', altitude: 1850, price: 98, process: '水洗' },
  { name: '巴拿马哈特曼庄园', farm: 'Hartmann', altitude: 1700, price: 142, process: '蜜处理' }
]

庄园咖啡豆的altitude海拔字段在海拔柱状图中通过b.altitude / this.maxAltitude() * 84公式计算柱高比例。花魁7.0日晒原豆的海拔2100m是数据集中最高的,柱状图中对应的柱子也最高。farm庄园名称和处理法process在列表中分别使用不同颜色的标签展示。庄园咖啡豆列表还创新性地引入了序号排名设计,前三名使用橙色编号高亮。

茶叶与器具数据集

茶叶数据集包含10款六大茶类的代表产品,器具数据集包含10款不同材质和容量的冲泡器具。

private teas: TeaItem[] = [
  { name: '明前西湖龙井 50g 罐装', type: '绿茶', ferment: 5, price: 158, rating: 96 },
  { name: '金骏眉红茶 250g 礼盒', type: '红茶', ferment: 85, price: 198, rating: 95 },
  { name: '凤凰单丛蜜兰香 100g', type: '乌龙', ferment: 60, price: 128, rating: 94 },
  { name: '云南古树普洱生茶 357g', type: '普洱', ferment: 20, price: 268, rating: 97 },
  { name: '福鼎白毫银针 50g', type: '白茶', ferment: 10, price: 218, rating: 95 },
  { name: '安溪铁观音清香型 250g', type: '乌龙', ferment: 55, price: 118, rating: 92 },
  { name: '正山小种传统松烟香', type: '红茶', ferment: 90, price: 148, rating: 93 },
  { name: '君山银针黄茶 50g', type: '黄茶', ferment: 30, price: 188, rating: 91 },
  { name: '茉莉花茶特级 200g', type: '花茶', ferment: 70, price: 88, rating: 92 },
  { name: '武夷岩茶大红袍 160g', type: '乌龙', ferment: 65, price: 238, rating: 96 }
]

private gears: GearItem[] = [
  { name: 'Hario V60 树脂滤杯 02 号', material: '树脂', capacity: 400, price: 68, stock: 421 },
  { name: 'Kalita 蛋糕杯 155 不锈钢', material: '不锈钢', capacity: 500, price: 158, stock: 187 },
  { name: '聪明杯浸泡式滤杯 350ml', material: 'PP 树脂', capacity: 350, price: 88, stock: 312 },
  { name: '法压壶耐热玻璃 600ml', material: '玻璃', capacity: 600, price: 98, stock: 265 },
  { name: '摩卡壶经典八角 3 杯份', material: '铝合金', capacity: 150, price: 128, stock: 198 },
  { name: '爱乐压便携压滤壶', material: 'PP+硅胶', capacity: 250, price: 218, stock: 156 },
  { name: '手冲鹅颈壶 0.9L 细嘴', material: '不锈钢', capacity: 900, price: 189, stock: 234 },
  { name: '陶瓷手冲套装五件套', material: '陶瓷', capacity: 400, price: 268, stock: 87 },
  { name: '意式咖啡机家用半自动', material: '不锈钢机身', capacity: 1500, price: 1999, stock: 45 },
  { name: '便携电动磨豆机 USB 充电', material: 'ABS+合金', capacity: 30, price: 328, stock: 176 }
]

茶叶的ferment发酵度字段在发酵度柱状图中通过t.ferment / 100 * 80 + 4公式计算柱高,正山小种的发酵度90%对应最高柱。rating好评率在进度条中直接作为百分比使用。器具的capacity容量字段在容量柱状图中通过g.capacity / 1500 * 80 + 3公式计算柱高,意式咖啡机的1500ml容量对应最高柱。当器具库存低于100时,通过条件渲染显示"少量"标签并配合脉冲动画提醒用户。

门店数据集

门店数据集包含10家不同城市的线下体验门店,每家门店标注了距离、评分、总席位和已预约数。

private shopList: ShopItem[] = [
  { name: '国贸旗舰店', dist: 1.2, score: 4.9, seats: 40, booked: 34 },
  { name: '三里屯店', dist: 2.8, score: 4.8, seats: 56, booked: 30 },
  { name: '望京小街店', dist: 3.5, score: 4.7, seats: 32, booked: 21 },
  { name: '陆家嘴店', dist: 5.1, score: 4.9, seats: 48, booked: 42 },
  { name: '中关村店', dist: 4.2, score: 4.6, seats: 28, booked: 12 },
  { name: '环贸 iapm 店', dist: 2.1, score: 4.8, seats: 36, booked: 25 },
  { name: '万象城店', dist: 6.3, score: 4.7, seats: 44, booked: 28 },
  { name: '太古里店', dist: 3.9, score: 4.9, seats: 52, booked: 48 },
  { name: '滨江天街店', dist: 7.5, score: 4.5, seats: 24, booked: 10 },
  { name: '首钢园店', dist: 9.8, score: 4.6, seats: 30, booked: 16 }
]

门店的booked已预约数在预约热度柱状图中通过s.booked / this.maxScore() * 80公式计算柱高。太古里店的已预约数48是数据集中最高的,柱状图对应柱子也最高。门店预约率进度条通过s.booked / s.seats * 100计算填充百分比,当预约率超过80%时进度条使用橙色#E65100填充表示即将满员,否则使用绿色#7CB342表示充裕。这种基于阈值的动态颜色切换是本应用的一大交互亮点。

消息数据集

在这里插入图片描述

消息数据集包含12条不同类型的系统通知,覆盖发货、订阅、预约、烘焙出炉、会员日等场景。

private messages: MessageItem[] = [
  { title: '发货通知', content: '您的瑰夏挂耳礼盒已由顺丰揽收,全程冷链', time: '11:02', unread: 2 },
  { title: '订阅提醒', content: '下期云南小粒豆将在周四烘焙,周五发出', time: '10:15', unread: 1 },
  { title: '门店预约', content: '国贸旗舰店手冲体验课已预约成功,请准时到店', time: '09:30', unread: 3 },
  { title: '烘焙出炉', content: '本周三支新豆出炉:花魁 7.0 已上架', time: '昨天', unread: 0 },
  { title: '会员日', content: '每月 18 日会员日,全场咖啡豆 88 折', time: '昨天', unread: 0 },
  { title: '品鉴邀请', content: '您获得 1 张杯测品鉴会入场券,本周六开场', time: '前天', unread: 1 },
  { title: '优惠到账', content: '咖啡节专享券 3 张已放入卡包,满 99 可用', time: '前天', unread: 0 },
  { title: '器具保养', content: '您的磨豆机已到建议清洁周期,查看教程', time: '周三', unread: 0 },
  { title: '茶叶上新', content: '明前龙井头采开售,限量 200 罐', time: '周三', unread: 1 },
  { title: '订单完成', content: '法压壶已签收,晒图可返 10 元京豆', time: '周二', unread: 0 },
  { title: '降价通知', content: '购物车里的聪明杯降价 20 元', time: '周一', unread: 0 },
  { title: '系统公告', content: '咖啡订阅服务升级,可随时暂停配送', time: '上周', unread: 0 }
]

消息的unread字段大于0时在UI中显示未读数量徽标,使用橙色背景配合600ms脉冲动画。消息头像使用浅棕色#EFEBE9圆形占位区,与咖啡主题的棕色系配色保持一致。

数据聚合计算方法

在这里插入图片描述

应用定义了两个私有方法用于在数据集合中计算最大值,为柱状图可视化提供比例基准。

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

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

maxAltitude方法遍历庄园咖啡豆数组找到最高海拔值,作为海拔柱状图的比例基准。maxScore方法遍历门店数组找到最大已预约数,作为预约热度柱状图的比例基准。两个方法均采用for循环加条件比较的经典算法,时间复杂度为O(n)。

选中项安全访问方法

在这里插入图片描述

由于selectedItem可能为null,应用定义了五个安全访问方法防止空指针异常。

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 selOrigin(): string {
  if (this.selectedItem === null) {
    return ''
  }
  return this.selectedItem.origin
}

private selScore(): number {
  if (this.selectedItem === null) {
    return 0
  }
  return this.selectedItem.score
}

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

这些方法统一采用相同的空值检查模式。selOrigin返回选中咖啡的产区,selScore返回杯测评分,这两个字段是咖啡详情弹窗中展示的核心信息。在ArkTS的严格类型系统中,联合类型CoffeeItem | null要求在访问属性前必须进行显式空值检查,这是语言层面的安全约束。

主页面build方法与布局架构

build方法是组件的渲染入口,采用Stack作为根容器,内部嵌套Column实现头部、滚动内容和底部导航栏的三段式布局。

build() {
  Stack() {
    Column() {
      this.headerBar()
      Scroll() {
        Column() {
          if (this.currentTab === 0) {
            this.tabMarket()
          } else if (this.currentTab === 1) {
            this.tabBean()
          } else if (this.currentTab === 2) {
            this.tabTea()
          } else if (this.currentTab === 3) {
            this.tabGear()
          } else if (this.currentTab === 4) {
            this.tabShop()
          } 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.showSubscribe) {
      this.modalSubscribe()
    }
    if (this.showVisit) {
      this.modalVisit()
    }
    if (this.showDelete) {
      this.modalDelete()
    }
    if (this.showDetail) {
      this.modalDetail()
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#FAF6F0')
}

Stack布局将五个模态弹窗层叠在主内容之上。背景色#FAF6F0为温暖的浅棕色调,呼应咖啡的主题色彩。Tab切换通过if-else条件链实现,当currentTab的值改变时ArkUI框架自动触发重新渲染。

头部与底部导航栏

头部栏包含标题、搜索框、消息入口和热门关键词标签,底部导航栏通过ForEach渲染七个Tab按钮。

@Builder
headerBar() {
  Column() {
    Row() {
      Column() {
        Text('咖啡 · 茗茶')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Text('京东生活馆')
          .fontSize(10)
          .fontColor('#D7CCC8')
          .margin({ top: 2 })
      }
      Column() {
        Row() {
          Text('搜索咖啡豆 / 茶叶 / 手冲器具')
            .fontSize(12)
            .fontColor('#BCAAA4')
        }
        .width('100%')
        .height(30)
        .backgroundColor('#FFFFFF')
        .borderRadius(15)
        .justifyContent(FlexAlign.Start)
        .padding({ left: 14 })
      }
      .layoutWeight(1)
      .margin({ left: 12, right: 12 })
      Column() {
        Text('消息')
          .fontSize(13)
          .fontColor('#FFFFFF')
      }
      .onClick(() => {
        this.currentTab = 5
      })
    }
    .width('100%')
    .height(54)
    .padding({ left: 14, right: 14 })
    .alignItems(VerticalAlign.Center)

    Row() {
      ForEach(['咖啡豆', '挂耳', '意式', '龙井', '普洱', '白茶', '手冲壶'], (k: string) => {
        Text(k)
          .fontSize(11)
          .fontColor('#D7CCC8')
          .backgroundColor('rgba(255,255,255,0.12)')
          .borderRadius(12)
          .padding({ left: 10, right: 10, top: 4, bottom: 4 })
          .margin({ right: 8 })
      })
    }
    .width('100%')
    .padding({ left: 14, bottom: 8 })
    .justifyContent(FlexAlign.Start)
  }
  .width('100%')
  .backgroundColor('#5D4037')
}

头部使用深棕色#5D4037背景配合白色标题和浅棕色副标题"京东生活馆"。第二行通过ForEach渲染七个热门关键词标签,使用半透明白色背景rgba(255,255,255,0.12)和圆角设计。这种热门关键词标签栏是前两个应用所没有的特色功能,为用户提供了快速搜索入口。底部导航栏选中状态使用琥珀色#FFB300指示条和文字,与头部棕色背景形成冷暖对比。

咖啡集市Tab与产区筛选

咖啡集市Tab包含活动横幅、数据统计卡片、产区筛选器和商品列表,是应用的核心展示页面。

@Builder
tabMarket() {
  Column() {
    Row() {
      Column() {
        Text('早春豆单上新')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Text('埃塞俄比亚 · 瑰夏村直发 · 满159减40')
          .fontSize(11)
          .fontColor('#D7CCC8')
          .margin({ top: 6 })
        Text('查看豆单')
          .fontSize(12)
          .fontColor('#4E342E')
          .backgroundColor('#FFB300')
          .borderRadius(14)
          .padding({ left: 14, right: 14, top: 6, bottom: 6 })
          .margin({ top: 12 })
      }
      .layoutWeight(2)
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text('7842')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFB300')
        Text('爆款已售')
          .fontSize(10)
          .fontColor('#D7CCC8')
          .margin({ top: 2 })
        Text('订阅豆单')
          .fontSize(11)
          .fontColor('#4E342E')
          .backgroundColor('#FFB300')
          .borderRadius(12)
          .padding({ left: 10, right: 10, 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)
      .margin({ left: 10 })
      .onClick(() => {
        this.showSubscribe = true
      })
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#5D4037')
    .borderRadius(12)
    .alignItems(VerticalAlign.Center)

活动横幅采用左右分栏布局:左侧layoutWeight(2)展示活动信息和"查看豆单"按钮,右侧layoutWeight(1)展示爆款已售数7842和"订阅豆单"按钮。"订阅豆单"按钮使用700ms脉冲动画引导用户点击,点击打开订阅弹窗。横幅背景使用深棕色#5D4037与头部背景一致,按钮使用琥珀色#FFB300形成视觉焦点。

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

      Column() {
        Text('99')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E65100')
        Text('最高杯测分')
          .fontSize(10)
          .fontColor('#999999')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .height(58)
      .backgroundColor('#FFFFFF')
      .borderRadius(10)
      .margin({ top: 10, left: 8 })

      Column() {
        Text('5 大')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#5D4037')
        Text('著名产区')
          .fontSize(10)
          .fontColor('#999999')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .height(58)
      .backgroundColor('#FFFFFF')
      .borderRadius(10)
      .margin({ top: 10, left: 8 })

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

数据统计区域使用四等分卡片布局:在售单品数24(棕色数字)、最高杯测分99(橙色数字)、著名产区数5大(棕色文字)和发布闲置入口(橙色背景按钮)。前三个卡片使用白色背景展示数据,第四个卡片使用橙色#E65100背景作为行动召唤按钮,点击打开发布弹窗。

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

    Column() {
      ForEach(this.coffees, (c: CoffeeItem) => {
        Row() {
          Column()
            .width(84)
            .height(84)
            .backgroundColor('#D7CCC8')
            .borderRadius(42)
          Column() {
            Text(c.name)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Row() {
              Text(c.origin)
                .fontSize(10)
                .fontColor('#E65100')
                .backgroundColor('#FFF3E0')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              Text(c.roast)
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#7CB342')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .margin({ left: 6 })
              Text('杯测 ' + c.score)
                .fontSize(10)
                .fontColor('#5D4037')
                .fontWeight(FontWeight.Bold)
                .margin({ left: 6 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
            }
            .margin({ top: 6 })

            Stack({ alignContent: Alignment.Start }) {
              Column()
                .width('100%')
                .height(6)
                .backgroundColor('#F0F0F0')
                .borderRadius(3)
              Column()
                .width(c.score + '%')
                .height(6)
                .backgroundColor('#E65100')
                .borderRadius(3)
            }
            .width('100%')
            .margin({ top: 8 })

            Row() {
              Text('¥' + c.price)
                .fontSize(17)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E65100')
              Text('已售' + c.sold)
                .fontSize(11)
                .fontColor('#999999')
                .margin({ left: 8 })
              Text('订阅价')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#5D4037')
                .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 = c
          this.showDetail = true
        })
      })
    }
    .width('100%')
  }
}

产区筛选器选中时使用棕色背景白色文字。商品图片占位区使用了圆形设计(borderRadius(42)即84/2),与前两个应用的方形图片占位区形成差异。产区标签使用橙色浅色背景,烘焙度标签使用绿色背景,杯测评分文本使用棕色字体配合800ms脉冲动画。杯测评分进度条使用橙色#E65100填充。"订阅价"按钮使用棕色背景,区别于"立即购买"的橙色按钮,体现了订阅业务的差异化定位。

咖啡豆Tab与海拔柱状图及排名设计

咖啡豆Tab的核心特色是海拔柱状图可视化和带序号编号的排名列表设计,是精品咖啡溯源信息的深度展示页面。

@Builder
tabBean() {
  Column() {
    Column() {
      Text('庄园豆海拔对比 (m)')
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
      Row() {
        ForEach(this.beanList, (b: BeanItem) => {
          Column() {
            Column()
              .width(15)
              .height(b.altitude / this.maxAltitude() * 84)
              .backgroundColor('#FFB300')
              .borderRadius(4)
            Text(b.altitude + '')
              .fontSize(8)
              .fontColor('#D7CCC8')
              .margin({ top: 3 })
          }
          .margin({ right: 9 })
          .alignItems(HorizontalAlign.Center)
        })
      }
      .width('100%')
      .height(112)
      .alignItems(VerticalAlign.End)
      .justifyContent(FlexAlign.Start)
      .margin({ top: 10 })
    }
    .width('100%')
    .padding(14)
    .backgroundColor('#4E342E')
    .borderRadius(12)
    .alignItems(HorizontalAlign.Start)

海拔柱状图使用琥珀色#FFB300柱体,每根柱子宽度15px,高度通过b.altitude / this.maxAltitude() * 84公式计算。柱子下方显示海拔数值,使用8px小字号避免遮挡。外层Row设置height(112)alignItems(VerticalAlign.End)确保柱子从底部对齐生长。

    Row() {
      Column() {
        Text('订阅咖啡豆')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Text('周期烘焙 · 定时配送')
          .fontSize(10)
          .fontColor('#FFCC80')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .height(52)
      .backgroundColor('#5D4037')
      .borderRadius(10)
      .justifyContent(FlexAlign.Center)
      .margin({ top: 10 })
      .onClick(() => {
        this.showSubscribe = true
      })

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

"订阅咖啡豆"和"发布转让"两个按钮使用等分布局,分别使用棕色和橙色背景,每个按钮包含主标题和副标题两行文字。

    Column() {
      ForEach(this.beanList, (b: BeanItem, idx: number) => {
        Row() {
          Column() {
            Text('No.' + (idx + 1))
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(idx < 3 ? '#E65100' : '#BCAAA4')
          }
          .width(44)
          .height(64)
          .backgroundColor('#EFEBE9')
          .borderRadius(10)
          .justifyContent(FlexAlign.Center)

          Column() {
            Text(b.name)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Row() {
              Text(b.farm)
                .fontSize(10)
                .fontColor('#5D4037')
                .backgroundColor('#EFEBE9')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              Text(b.process)
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#7CB342')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .margin({ left: 6 })
              Text('海拔 ' + b.altitude + 'm')
                .fontSize(10)
                .fontColor('#999999')
                .margin({ left: 6 })
            }
            .margin({ top: 6 })

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

          Column() {
            Text('¥' + b.price)
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E65100')
            Text('200g/袋')
              .fontSize(10)
              .fontColor('#999999')
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .padding(10)
        .margin({ top: 10 })
        .onClick(() => {
          this.selectedItem = this.coffees[idx % this.coffees.length]
          this.showDetail = true
        })
      })
    }
    .width('100%')
  }
}

庄园咖啡豆列表引入了带序号编号的排名设计:每个卡片左侧有一个44px宽的编号区,显示"No.1"、"No.2"等排名编号。前三名(idx < 3)使用橙色#E65100编号高亮,第四名及以后使用浅棕色#BCAAA4,通过颜色差异突出排名前三的精品咖啡豆。庄园名称标签使用浅棕色背景,处理法标签使用绿色背景,海拔文本使用灰色字体。海拔进度条使用琥珀色#FFB300填充,与海拔柱状图的柱体颜色一致。点击咖啡豆卡片通过this.coffees[idx % this.coffees.length]将对应的咖啡商品赋值给selectedItem,取模运算确保索引不越界。

茶叶专区Tab与发酵度柱状图

茶叶Tab展示六大茶类发酵度柱状图和茶叶列表,列表中使用双列网格布局展示茶叶卡片。

@Builder
tabTea() {
  Column() {
    Column() {
      Text('茗茶专区')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#33691E')
      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.teas, (t: TeaItem) => {
          Column() {
            Column()
              .width(15)
              .height(t.ferment / 100 * 80 + 4)
              .backgroundColor('#7CB342')
              .borderRadius(4)
            Text(t.ferment + '')
              .fontSize(9)
              .fontColor('#999999')
              .margin({ top: 4 })
          }
          .margin({ right: 9 })
          .alignItems(HorizontalAlign.Center)
        })
      }
      .width('100%')
      .height(110)
      .alignItems(VerticalAlign.End)
      .justifyContent(FlexAlign.Start)
      .margin({ top: 8 })
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .margin({ top: 10 })
    .alignItems(HorizontalAlign.Start)

发酵度柱状图使用绿色#7CB342柱体,高度通过t.ferment / 100 * 80 + 4公式计算。发酵度为90%的正山小种对应最高柱(76px),发酵度仅5%的西湖龙井对应最矮柱(8px)。+ 4的基础高度确保即使发酵度为0的茶叶也有最低可见的柱高。

    Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
      ForEach(this.teas, (t: TeaItem) => {
        Column() {
          Row() {
            Text(t.type)
              .fontSize(10)
              .fontColor('#FFFFFF')
              .backgroundColor('#7CB342')
              .borderRadius(4)
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            Text('发酵 ' + t.ferment + '%')
              .fontSize(10)
              .fontColor('#999999')
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)

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

          Row() {
            Text('¥' + t.price)
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E65100')
            Text('好评 ' + t.rating + '%')
              .fontSize(10)
              .fontColor('#5D4037')
              .margin({ left: 6 })
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 750, iterations: -1, curve: Curve.EaseInOut })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ top: 8 })

          Stack({ alignContent: Alignment.Start }) {
            Column()
              .width('100%')
              .height(5)
              .backgroundColor('#F0F0F0')
              .borderRadius(3)
            Column()
              .width(t.rating + '%')
              .height(5)
              .backgroundColor('#7CB342')
              .borderRadius(3)
          }
          .width('100%')
          .margin({ top: 6 })
        }
        .width('48%')
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .padding(10)
        .margin({ top: 10 })
        .alignItems(HorizontalAlign.Start)
        .onClick(() => {
          this.showSubscribe = true
        })
      })
    }
    .width('100%')
  }
}

茶叶列表使用Flex双列网格布局,每个卡片宽度48%。茶类标签使用绿色背景,发酵度文本使用灰色字体。茶叶名称使用maxLines(2)允许两行显示。好评率文本使用750ms脉冲动画强调,好评率进度条使用绿色填充。点击茶叶卡片打开订阅弹窗(此处复用了订阅弹窗作为通用下单交互),体现了茶叶与咖啡在订阅业务上的共享流程。

器具冲泡Tab与容量柱状图

器具Tab展示器具容量柱状图、水平滚动器具卡片和垂直器具列表,列表中通过库存进度条和少量标签提示库存状态。

@Builder
tabGear() {
  Column() {
    Column() {
      Row() {
        Text('器具冲泡实验室')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Text('容量对比 (ml)')
          .fontSize(11)
          .fontColor('#D7CCC8')
          .margin({ left: 8 })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        ForEach(this.gears, (g: GearItem) => {
          Column() {
            Column()
              .width(15)
              .height(g.capacity / 1500 * 80 + 3)
              .backgroundColor('#FF7043')
              .borderRadius(4)
            Text(g.capacity + '')
              .fontSize(8)
              .fontColor('#D7CCC8')
              .margin({ top: 3 })
          }
          .margin({ right: 9 })
          .alignItems(HorizontalAlign.Center)
        })
      }
      .width('100%')
      .height(110)
      .alignItems(VerticalAlign.End)
      .justifyContent(FlexAlign.Start)
      .margin({ top: 10 })
    }
    .width('100%')
    .padding(14)
    .backgroundColor('#6D4C41')
    .borderRadius(12)
    .alignItems(HorizontalAlign.Start)

容量柱状图使用橙红色#FF7043柱体,高度通过g.capacity / 1500 * 80 + 3公式计算,1500ml为基准值。意式咖啡机的1500ml容量对应最高柱(83px),便携电动磨豆机的30ml容量对应最矮柱(4.6px)。+ 3的基础高度确保小容量器具也有最低可见柱高。

    Scroll() {
      Row() {
        ForEach(this.gears, (g: GearItem) => {
          Column() {
            Column()
              .width(96)
              .height(60)
              .backgroundColor('#D7CCC8')
              .borderRadius(10)
            Text(g.material)
              .fontSize(10)
              .fontColor('#6D4C41')
              .backgroundColor('#EFEBE9')
              .borderRadius(4)
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .margin({ top: 6 })
            Text('¥' + g.price)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E65100')
              .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.gears, (g: GearItem) => {
        Row() {
          Column()
            .width(58)
            .height(58)
            .backgroundColor('#EFEBE9')
            .borderRadius(10)
          Column() {
            Text(g.name)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Row() {
              Text(g.capacity + 'ml')
                .fontSize(10)
                .fontColor('#6D4C41')
                .backgroundColor('#EFEBE9')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              Text('库存 ' + g.stock)
                .fontSize(10)
                .fontColor('#999999')
                .margin({ left: 8 })
              if (g.stock < 100) {
                Text('少量')
                  .fontSize(10)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#E65100')
                  .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 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .padding({ left: 10 })

          Column() {
            Text('¥' + g.price)
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E65100')
            Text('加入购物车')
              .fontSize(10)
              .fontColor('#FFFFFF')
              .backgroundColor('#6D4C41')
              .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.showDelete = true
        })
      })
    }
    .width('100%')
  }
}

器具列表中每个卡片左侧有58px的方形图片占位区。容量标签使用浅棕色背景深棕色文字。当库存低于100时通过if (g.stock < 100)条件渲染"少量"标签,使用橙色背景配合600ms脉冲动画。"加入购物车"按钮使用棕色#6D4C41背景,与咖啡集市Tab的"订阅价"按钮使用相同棕色保持一致。

门店体验Tab与预约热度柱状图

门店Tab展示门店体验预约横幅、预约热度柱状图和门店列表,列表中通过预约率进度条和动态颜色切换展示门店热度。

@Builder
tabShop() {
  Column() {
    Column() {
      Row() {
        Column() {
          Text('门店手冲体验')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('大师现场冲煮 · 免费品鉴三杯')
            .fontSize(11)
            .fontColor('#FFCC80')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
        Text('立即预约')
          .fontSize(12)
          .fontColor('#4E342E')
          .backgroundColor('#FFB300')
          .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.showVisit = true
          })
      }
      .width('100%')
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#5D4037')
    .borderRadius(12)

    Column() {
      Text('到店预约热度 (本周)')
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
      Row() {
        ForEach(this.shopList, (s: ShopItem) => {
          Column() {
            Column()
              .width(15)
              .height(s.booked / this.maxScore() * 80)
              .backgroundColor('#8D6E63')
              .borderRadius(4)
            Text(s.booked + '')
              .fontSize(9)
              .fontColor('#999999')
              .margin({ top: 4 })
          }
          .margin({ right: 9 })
          .alignItems(HorizontalAlign.Center)
        })
      }
      .width('100%')
      .height(110)
      .alignItems(VerticalAlign.End)
      .justifyContent(FlexAlign.Start)
      .margin({ top: 8 })
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .margin({ top: 10 })
    .alignItems(HorizontalAlign.Start)

预约热度柱状图使用棕色#8D6E63柱体,高度通过s.booked / this.maxScore() * 80公式计算。太古里店的已预约数48对应最高柱(80px)。"立即预约"按钮使用琥珀色#FFB300背景配合700ms脉冲动画,与头部导航栏的选中色一致。

    Column() {
      ForEach(this.shopList, (s: ShopItem) => {
        Row() {
          Column()
            .width(50)
            .height(50)
            .backgroundColor('#D7CCC8')
            .borderRadius(10)
          Column() {
            Row() {
              Text(s.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
              Text(s.score + ' 分')
                .fontSize(11)
                .fontColor('#E65100')
                .fontWeight(FontWeight.Bold)
                .margin({ left: 8 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
            }
            Text('距离 ' + s.dist + 'km · ' + s.seats + ' 席位')
              .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.seats * 100 + '%')
                .height(6)
                .backgroundColor(s.booked / s.seats > 0.8 ? '#E65100' : '#7CB342')
                .borderRadius(3)
            }
            .width('100%')
            .margin({ top: 6 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .padding({ left: 10 })

          Column() {
            Text(s.booked + '/' + s.seats)
              .fontSize(11)
              .fontColor('#999999')
            Text('预约')
              .fontSize(11)
              .fontColor('#FFFFFF')
              .backgroundColor('#5D4037')
              .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.showVisit = true
        })
      })
    }
    .width('100%')
  }
}

门店列表中的预约率进度条是本应用的技术亮点之一。填充宽度通过s.booked / s.seats * 100计算,填充颜色通过三元表达式s.booked / s.seats > 0.8 ? '#E65100' : '#7CB342'动态切换:当预约率超过80%时使用橙色#E65100表示即将满员,否则使用绿色#7CB342表示充裕。这种基于阈值的动态颜色设计让用户能够直观感知门店的热度状态,无需阅读具体数字即可判断是否需要尽快预约。评分文本使用800ms脉冲动画强调展示,s.booked + '/' + s.seats格式直观展示已预约/总席位的比例关系。

消息中心与我的页面

消息中心通过ForEach渲染12条消息列表,我的页面展示用户信息、统计数据和功能菜单。

@Builder
tabMessage() {
  Column() {
    Column() {
      Row() {
        Text('消息中心')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Text('8 条新动态')
          .fontSize(11)
          .fontColor('#E65100')
          .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('#EFEBE9')
            .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('#E65100')
                  .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%')
  }
}

消息头像使用浅棕色#EFEBE9圆形占位区,未读徽标使用橙色#E65100背景配合600ms脉冲动画。消息布局采用justifyContent(FlexAlign.SpaceBetween)将标题、未读徽标和时间分置两端。

@Builder
tabMine() {
  Column() {
    Column() {
      Row() {
        Column()
          .width(60)
          .height(60)
          .backgroundColor('#D7CCC8')
          .borderRadius(30)
        Column() {
          Text('手冲玩家_Leo')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('咖啡品味等级:银杯杯测师')
            .fontSize(11)
            .fontColor('#FFCC80')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
        .padding({ left: 12 })
      }
      .width('100%')
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#4E342E')
    .borderRadius(12)

    Row() {
      Column() {
        Text('52')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#5D4037')
        Text('咖啡订单')
          .fontSize(11)
          .fontColor('#999999')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .height(64)
      .backgroundColor('#FFFFFF')
      .borderRadius(10)
      .margin({ top: 10 })

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

      Column() {
        Text('680')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#5D4037')
        Text('品味积分')
          .fontSize(11)
          .fontColor('#999999')
          .margin({ top: 2 })
      }
      .layoutWeight(1)
      .height(64)
      .backgroundColor('#FFFFFF')
      .borderRadius(10)
      .margin({ top: 10, left: 8 })

      Column() {
        Text('2')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E65100')
        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(['我的豆单订阅', '常购茶单', '器具收藏夹', '门店体验记录', '杯测笔记', '帮助与反馈'], (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 })
  }
}

用户信息区域使用深棕色#4E342E背景配合白色用户名和浅琥珀色#FFCC80等级文字。统计区域通过四等分卡片展示咖啡订单、门店体验、品味积分和品鉴券数据,交替使用棕色和橙色数字。功能菜单列表使用ForEach渲染六个菜单项,包括"我的豆单订阅"、“常购茶单”、"杯测笔记"等咖啡特色菜单项。

模态弹窗——咖啡豆周期订阅

订阅弹窗是咖啡业务的核心交互界面,支持豆种选择、研磨粗细和订阅周期配置。

@Builder
modalSubscribe() {
  Column() {
    Column().layoutWeight(1).width('100%')
    Column() {
      Row() {
        Text('咖啡豆周期订阅')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Text('¥' + ((this.selectedBean + 1) * 19 + 12) * this.weeks)
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E65100')
          .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.beans, (b: string, idx: number) => {
            Text(b)
              .fontSize(12)
              .fontColor(this.selectedBean === idx ? '#FFFFFF' : '#666666')
              .backgroundColor(this.selectedBean === idx ? '#5D4037' : '#FAF6F0')
              .borderRadius(8)
              .padding({ left: 14, right: 14, top: 8, bottom: 8 })
              .margin({ right: 8, top: 8 })
              .onClick(() => {
                this.selectedBean = idx
              })
          })
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(HorizontalAlign.Start)

订阅费用计算公式为((this.selectedBean + 1) * 19 + 12) * this.weeks,将豆种索引加1后乘以19元基础价、加上12元固定费、再乘以订阅周数。豆种选择器选中时使用棕色#5D4037背景,五个选项为瑰夏、耶加雪菲、蓝山、曼特宁和云南小粒。订阅费用使用700ms脉冲动画强调展示。

      Column() {
        Text('研磨粗细')
          .fontSize(13)
          .fontColor('#666666')
        Row() {
          ForEach(this.grinds, (g: string, idx: number) => {
            Text(g)
              .fontSize(12)
              .fontColor(this.selectedGrind === idx ? '#FFFFFF' : '#666666')
              .backgroundColor(this.selectedGrind === idx ? '#7CB342' : '#FAF6F0')
              .borderRadius(8)
              .padding({ left: 14, right: 14, top: 8, bottom: 8 })
              .margin({ right: 8, top: 8 })
              .onClick(() => {
                this.selectedGrind = idx
              })
          })
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text('订阅周期:' + this.weeks + ' 周')
          .fontSize(13)
          .fontColor('#333333')
          .fontWeight(FontWeight.Bold)
        Row() {
          Text('-')
            .fontSize(16)
            .fontColor('#5D4037')
            .width(34)
            .height(34)
            .textAlign(TextAlign.Center)
            .backgroundColor('#EFEBE9')
            .borderRadius(8)
            .onClick(() => {
              if (this.weeks > 1) {
                this.weeks -= 1
              }
            })
          Text(this.weeks + '')
            .fontSize(15)
            .fontColor('#333333')
            .width(50)
            .textAlign(TextAlign.Center)
          Text('+')
            .fontSize(16)
            .fontColor('#E65100')
            .width(34)
            .height(34)
            .textAlign(TextAlign.Center)
            .backgroundColor('#FFF3E0')
            .borderRadius(8)
            .onClick(() => {
              if (this.weeks < 24) {
                this.weeks += 1
              }
            })
        }
        .margin({ top: 8 })
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(HorizontalAlign.Start)

      Text('每周新鲜烘焙直达,可随时暂停')
        .fontSize(11)
        .fontColor('#999999')
        .margin({ top: 10 })

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

研磨粗细选择器选中时使用绿色#7CB342背景,四个选项为手冲粗度、法压中度、摩卡细度和意式极细,不同研磨度对应不同的冲泡方式。订阅周期调节器范围限制在1到24周之间,减号使用浅棕色背景加号使用浅橙色背景。底部提示"每周新鲜烘焙直达,可随时暂停"增强了用户对订阅服务的信任感。确认订阅按钮使用橙色全宽胶囊设计。

模态弹窗——门店体验预约

门店预约弹窗支持门店选择、到店人数和体验内容展示,是线下体验服务的核心交互界面。

@Builder
modalVisit() {
  Column() {
    Column().layoutWeight(1).width('100%')
    Column() {
      Row() {
        Text('门店体验预约')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Text('免费')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .backgroundColor('#7CB342')
          .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.shops, (s: string, idx: number) => {
            Text(s)
              .fontSize(12)
              .fontColor(this.selectedShop === idx ? '#FFFFFF' : '#666666')
              .backgroundColor(this.selectedShop === idx ? '#5D4037' : '#FAF6F0')
              .borderRadius(8)
              .padding({ left: 14, right: 14, top: 8, bottom: 8 })
              .margin({ right: 8, top: 8 })
              .onClick(() => {
                this.selectedShop = idx
              })
          })
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text('到店人数')
          .fontSize(13)
          .fontColor('#666666')
        Row() {
          ForEach(this.guestOptions, (g: string, idx: number) => {
            Text(g)
              .fontSize(12)
              .fontColor(this.selectedGuests === idx ? '#FFFFFF' : '#666666')
              .backgroundColor(this.selectedGuests === idx ? '#E65100' : '#FAF6F0')
              .borderRadius(8)
              .padding({ left: 14, right: 14, top: 8, bottom: 8 })
              .margin({ right: 8, top: 8 })
              .onClick(() => {
                this.selectedGuests = idx
              })
          })
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text('体验内容')
          .fontSize(13)
          .fontColor('#666666')
        Text('大师手冲演示 · 三款庄园豆品鉴 · 拉花体验')
          .fontSize(12)
          .fontColor('#5D4037')
          .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('#5D4037')
        .borderRadius(22)
        .margin({ top: 20 })
        .onClick(() => {
          this.showVisit = 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.showVisit = false
  })
}

门店预约弹窗的"免费"标签使用绿色背景配合700ms脉冲动画,强调体验服务的免费属性吸引用户预约。门店选择器选中时使用棕色背景,到店人数选择器选中时使用橙色背景,两个选择器使用不同颜色区分维度。体验内容区域以纯文本形式展示三项体验内容:“大师手冲演示”、“三款庄园豆品鉴"和"拉花体验”。提交预约按钮使用棕色全宽胶囊设计。

模态弹窗——发布闲置与商品详情

发布闲置弹窗支持商品名称输入、豆种选择和烘焙度选择,商品详情弹窗采用右侧滑出面板设计。

@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: '如:翡翠庄园瑰夏 200g' })
          .height(40)
          .fontSize(13)
          .backgroundColor('#FAF6F0')
          .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.beans, (b: string, idx: number) => {
            Text(b)
              .fontSize(12)
              .fontColor(this.selectedBean === idx ? '#FFFFFF' : '#666666')
              .backgroundColor(this.selectedBean === idx ? '#5D4037' : '#FAF6F0')
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .margin({ right: 8, top: 8 })
              .onClick(() => {
                this.selectedBean = idx
              })
          })
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text('烘焙度')
          .fontSize(13)
          .fontColor('#666666')
        Row() {
          ForEach(this.roasts, (r: string, idx: number) => {
            Text(r)
              .fontSize(12)
              .fontColor(this.selectedRoast === idx ? '#FFFFFF' : '#666666')
              .backgroundColor(this.selectedRoast === idx ? '#E65100' : '#FAF6F0')
              .borderRadius(8)
              .padding({ left: 14, right: 14, top: 8, bottom: 8 })
              .margin({ right: 8, top: 8 })
              .onClick(() => {
                this.selectedRoast = idx
              })
          })
        }
        .width('100%')
        .flexWrap(FlexWrap.Wrap)
      }
      .width('100%')
      .margin({ top: 16 })
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text('预估价:¥' + ((this.selectedBean + 1) * 28 + this.selectedRoast * 10))
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#E65100')
          .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('#5D4037')
        .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
  })
}

发布闲置弹窗的预估价计算公式为(this.selectedBean + 1) * 28 + this.selectedRoast * 10,将豆种索引和烘焙度索引映射为价格系数。豆种选择器选中时使用棕色背景圆角14,烘焙度选择器选中时使用橙色背景圆角8,两种选择器使用不同的圆角大小进一步区分维度。确认发布按钮使用棕色全宽胶囊设计。

@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('#5D4037')
        .margin({ top: 14 })
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      Row() {
        Text(this.selOrigin())
          .fontSize(11)
          .fontColor('#E65100')
          .backgroundColor('#FFF3E0')
          .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.selScore())
          .fontSize(12)
          .fontColor('#666666')
        Stack({ alignContent: Alignment.Start }) {
          Column()
            .width('100%')
            .height(8)
            .backgroundColor('#F0F0F0')
            .borderRadius(4)
          Column()
            .width(this.selScore() + '%')
            .height(8)
            .backgroundColor('#FFB300')
            .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('#E65100')
          .scale({ x: 1.06, y: 1.06 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        Text('庄园直发')
          .fontSize(11)
          .fontColor('#5D4037')
          .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('#E65100')
        .borderRadius(21)
        .margin({ top: 20 })
        .onClick(() => {
          this.showDetail = false
          this.showSubscribe = true
        })

      Text('加入豆单')
        .fontSize(14)
        .fontColor('#5D4037')
        .width('100%')
        .height(42)
        .textAlign(TextAlign.Center)
        .backgroundColor('#EFEBE9')
        .borderRadius(21)
        .margin({ top: 10 })
        .onClick(() => {
          this.showDetail = false
        })
    }
    .width('78%')
    .height('100%')
    .backgroundColor('#FFFFFF')
    .padding(16)
    .alignItems(HorizontalAlign.Start)
    .onClick((event: ClickEvent) => {
      event.stopPropagation()
    })
  }
  .width('100%')
  .height('100%')
  .backgroundColor('rgba(0,0,0,0.5)')
  .onClick(() => {
    this.showDetail = false
  })
}

详情弹窗采用Row布局将屏幕分为22%透明关闭区和78%白色内容面板。杯测评分进度条使用琥珀色#FFB300填充,与前两个应用使用绿色或粉色填充的设计形成差异。价格使用700ms脉冲动画强调。详情弹窗提供"立即下单"和"加入豆单"两个操作按钮:点击"立即下单"先关闭详情弹窗再打开订阅弹窗(this.showDetail = false; this.showSubscribe = true),实现弹窗级联切换;点击"加入豆单"仅关闭详情弹窗。"加入豆单"按钮使用浅棕色#EFEBE9背景深棕色文字,与"立即下单"的橙色实心按钮形成主次区分。

流程图

以下是咖啡茗茶生活馆应用的核心业务流程,展示了用户从浏览咖啡到完成订阅或门店预约的完整交互路径。

0

1

2

3

4

5

6

立即下单

加入豆单

应用启动

渲染主页面

头部导航栏

滚动内容区

底部导航栏

currentTab判断

咖啡集市Tab

咖啡豆Tab

茶叶专区Tab

器具冲泡Tab

门店体验Tab

消息中心Tab

我的页面Tab

点击商品卡片

打开详情弹窗

选择操作

关闭详情-打开订阅弹窗

关闭详情弹窗

选择首选豆种

选择研磨粗细

调节订阅周期

计算订阅费用

确认订阅

点击立即预约

打开门店预约弹窗

选择门店

选择到店人数

查看体验内容

提交预约

点击订阅豆单

技术点对比表格

技术维度 实现方式 关键API/特性 优势 适用场景
状态管理 @State装饰器 currentTab、showSubscribe等状态变量 单一数据源驱动UI,自动触发重渲染 Tab切换、弹窗显隐、多维选择
布局架构 Stack + Column Stack叠加模态弹窗 弹窗层叠在主内容之上,无需页面跳转 多弹窗管理系统
数据可视化 纯ArkUI柱状图 Column动态height + ForEach 无需图表库,纯声明式实现数据对比 海拔、发酵度、容量、预约热度对比
进度条组件 Stack嵌套Column alignContent: Alignment.Start 双层叠加实现轨道与填充效果 杯测评分、海拔、好评率、预约率展示
动态颜色 三元表达式阈值判断 booked/seats>0.8?橙:绿 基于数据阈值自动切换颜色 门店预约率热度可视化
排名设计 序号编号+条件颜色 idx<3?橙:浅棕 前三名高亮,竞技感强 庄园咖啡豆品质排名展示
动画效果 scale + animation iterations: -1, Curve.EaseInOut 无限循环脉冲动画,吸引用户注意 营销标签、价格强调、未读徽标
弹窗级联 顺序状态修改 showDetail=false再showSubscribe=true 弹窗间无缝切换,无闪烁过渡 详情到订阅的场景跳转
空值安全 联合类型+条件检查 CoffeeItem | null, if null判断 编译期类型安全,防止运行时空指针 详情弹窗数据访问
取模索引 模运算防越界 coffees[idx % coffees.length] 确保索引不越界,安全映射数据 咖啡豆到咖啡商品的关联访问

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

interface CoffeeItem {
  name: string
  price: number
  sold: number
  origin: string
  roast: string
  score: number
}

interface BeanItem {
  name: string
  farm: string
  altitude: number
  price: number
  process: string
}

interface TeaItem {
  name: string
  type: string
  ferment: number
  price: number
  rating: number
}

interface GearItem {
  name: string
  material: string
  capacity: number
  price: number
  stock: number
}

interface ShopItem {
  name: string
  dist: number
  score: number
  seats: number
  booked: number
}

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

@Entry
@Component
struct CoffeeTeaPage {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showSubscribe: boolean = false
  @State showVisit: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: CoffeeItem | null = null
  @State selectedOrigin: number = 0
  @State selectedRoast: number = 0
  @State selectedBean: number = 0
  @State selectedGrind: number = 0
  @State selectedShop: number = 0
  @State selectedGuests: number = 0
  @State weeks: number = 4
  @State inputName: string = ''

  private tabs: string[] = ['咖啡集市', '咖啡豆', '茶叶专区', '器具冲泡', '门店体验', '消息', '我的']
  private origins: string[] = ['埃塞俄比亚', '哥伦比亚', '云南保山', '危地马拉', '巴拿马']
  private roasts: string[] = ['浅烘焙', '中烘焙', '中深烘焙', '深烘焙']
  private beans: string[] = ['瑰夏', '耶加雪菲', '蓝山', '曼特宁', '云南小粒']
  private grinds: string[] = ['手冲粗度', '法压中度', '摩卡细度', '意式极细']
  private shops: string[] = ['国贸旗舰店', '三里屯店', '望京小街店', '陆家嘴店']
  private guestOptions: string[] = ['1 人', '2 人', '3-4 人', '5 人以上']

  private coffees: CoffeeItem[] = [
    { name: '瑰夏手冲挂耳 10 袋礼盒', price: 128, sold: 3214, origin: '巴拿马', roast: '浅烘焙', score: 97 },
    { name: '耶加雪菲日晒挂耳 20 袋', price: 88, sold: 5623, origin: '埃塞俄比亚', roast: '浅烘焙', score: 95 },
    { name: '蓝山一号咖啡豆 227g', price: 168, sold: 2134, origin: '哥伦比亚', roast: '中烘焙', score: 94 },
    { name: '云南保山小粒日晒豆', price: 58, sold: 4876, origin: '云南保山', roast: '中烘焙', score: 91 },
    { name: '危地马拉花神咖啡豆', price: 98, sold: 1856, origin: '危地马拉', roast: '中深烘焙', score: 93 },
    { name: '曼特宁黄金曼咖啡豆', price: 76, sold: 2341, origin: '巴拿马', roast: '深烘焙', score: 92 },
    { name: '冷萃冻干咖啡粉 3 秒即溶', price: 69, sold: 7842, origin: '云南保山', roast: '中烘焙', score: 96 },
    { name: '拿铁冻干咖啡礼盒 24 杯', price: 139, sold: 3267, origin: '哥伦比亚', roast: '中深烘焙', score: 94 },
    { name: '西达摩水洗挂耳 10 袋', price: 79, sold: 4521, origin: '埃塞俄比亚', roast: '浅烘焙', score: 95 },
    { name: '翡翠庄园红标瑰夏 100g', price: 299, sold: 876, origin: '巴拿马', roast: '浅烘焙', score: 99 },
    { name: '云南日晒卡迪姆 200g', price: 45, sold: 6234, origin: '云南保山', roast: '中烘焙', score: 89 },
    { name: '安提瓜火山咖啡豆', price: 108, sold: 1457, origin: '危地马拉', roast: '中深烘焙', score: 93 },
    { name: '椰香拿铁冻干条 30 条', price: 99, sold: 4312, origin: '海南', roast: '中深烘焙', score: 92 },
    { name: '瑰夏村竞标批次 50g', price: 388, sold: 432, origin: '埃塞俄比亚', roast: '浅烘焙', score: 98 },
    { name: '云南铁皮卡蜜处理 200g', price: 72, sold: 1987, origin: '云南保山', roast: '浅烘焙', score: 90 },
    { name: '哥伦比亚蕙兰咖啡豆', price: 68, sold: 2876, origin: '哥伦比亚', roast: '中烘焙', score: 91 },
    { name: '意式拼配金杯豆 1kg', price: 128, sold: 5163, origin: '巴拿马', roast: '深烘焙', score: 95 },
    { name: '低因瑞士水处理豆 227g', price: 118, sold: 965, origin: '哥伦比亚', roast: '中烘焙', score: 92 },
    { name: '手冲单品入门三豆组', price: 108, sold: 2743, origin: '云南保山', roast: '中烘焙', score: 93 },
    { name: '冷萃浓缩液 1L 大瓶装', price: 89, sold: 3562, origin: '云南保山', roast: '中深烘焙', score: 94 },
    { name: '挂耳咖啡混合口味 30 袋', price: 119, sold: 6987, origin: '埃塞俄比亚', roast: '中烘焙', score: 96 },
    { name: '麝香猫风味咖啡豆 100g', price: 158, sold: 654, origin: '巴拿马', roast: '中深烘焙', score: 88 },
    { name: '云南厌氧发酵黑蜜 200g', price: 96, sold: 1234, origin: '云南保山', roast: '浅烘焙', score: 95 },
    { name: '经典意式浓缩豆 500g', price: 85, sold: 4231, origin: '危地马拉', roast: '深烘焙', score: 92 }
  ]

  private beanList: BeanItem[] = [
    { name: '翡翠庄园绿标瑰夏', farm: '翡翠庄园', altitude: 1650, price: 198, process: '水洗' },
    { name: '90+ 烛芒日晒', farm: 'Ninety Plus', altitude: 1800, price: 268, process: '日晒' },
    { name: '花魁 7.0 日晒原豆', farm: 'Hambela', altitude: 2100, price: 128, process: '日晒' },
    { name: '云南保山铁皮卡', farm: '佐园咖啡', altitude: 1500, price: 62, process: '水洗' },
    { name: '蓝山一号认证豆', farm: 'Clifton Mount', altitude: 1300, price: 168, process: '水洗' },
    { name: '黄金曼特宁 G1', farm: 'Takengon', altitude: 1400, price: 76, process: '湿刨' },
    { name: '哥斯达黎加黑蜜', farm: 'Tarrazu', altitude: 1600, price: 92, process: '黑蜜' },
    { name: '巴拿马驴子庄园', farm: 'El Burro', altitude: 1750, price: 156, process: '水洗' },
    { name: '肯尼亚 SL28 水洗', farm: 'Nyeri', altitude: 1900, price: 112, process: '水洗' },
    { name: '云南日晒卡杜拉', farm: '孟连庄园', altitude: 1350, price: 48, process: '日晒' },
    { name: '危地马拉薇薇特南果', farm: 'Huehuetenango', altitude: 1850, price: 98, process: '水洗' },
    { name: '巴拿马哈特曼庄园', farm: 'Hartmann', altitude: 1700, price: 142, process: '蜜处理' }
  ]

  private teas: TeaItem[] = [
    { name: '明前西湖龙井 50g 罐装', type: '绿茶', ferment: 5, price: 158, rating: 96 },
    { name: '金骏眉红茶 250g 礼盒', type: '红茶', ferment: 85, price: 198, rating: 95 },
    { name: '凤凰单丛蜜兰香 100g', type: '乌龙', ferment: 60, price: 128, rating: 94 },
    { name: '云南古树普洱生茶 357g', type: '普洱', ferment: 20, price: 268, rating: 97 },
    { name: '福鼎白毫银针 50g', type: '白茶', ferment: 10, price: 218, rating: 95 },
    { name: '安溪铁观音清香型 250g', type: '乌龙', ferment: 55, price: 118, rating: 92 },
    { name: '正山小种传统松烟香', type: '红茶', ferment: 90, price: 148, rating: 93 },
    { name: '君山银针黄茶 50g', type: '黄茶', ferment: 30, price: 188, rating: 91 },
    { name: '茉莉花茶特级 200g', type: '花茶', ferment: 70, price: 88, rating: 92 },
    { name: '武夷岩茶大红袍 160g', type: '乌龙', ferment: 65, price: 238, rating: 96 }
  ]

  private gears: GearItem[] = [
    { name: 'Hario V60 树脂滤杯 02 号', material: '树脂', capacity: 400, price: 68, stock: 421 },
    { name: ' Kalita 蛋糕杯 155 不锈钢', material: '不锈钢', capacity: 500, price: 158, stock: 187 },
    { name: '聪明杯浸泡式滤杯 350ml', material: 'PP 树脂', capacity: 350, price: 88, stock: 312 },
    { name: '法压壶耐热玻璃 600ml', material: '玻璃', capacity: 600, price: 98, stock: 265 },
    { name: '摩卡壶经典八角 3 杯份', material: '铝合金', capacity: 150, price: 128, stock: 198 },
    { name: '爱乐压便携压滤壶', material: 'PP+硅胶', capacity: 250, price: 218, stock: 156 },
    { name: '手冲鹅颈壶 0.9L 细嘴', material: '不锈钢', capacity: 900, price: 189, stock: 234 },
    { name: '陶瓷手冲套装五件套', material: '陶瓷', capacity: 400, price: 268, stock: 87 },
    { name: '意式咖啡机家用半自动', material: '不锈钢机身', capacity: 1500, price: 1999, stock: 45 },
    { name: '便携电动磨豆机 USB 充电', material: 'ABS+合金', capacity: 30, price: 328, stock: 176 }
  ]

  private shopList: ShopItem[] = [
    { name: '国贸旗舰店', dist: 1.2, score: 4.9, seats: 40, booked: 34 },
    { name: '三里屯店', dist: 2.8, score: 4.8, seats: 56, booked: 30 },
    { name: '望京小街店', dist: 3.5, score: 4.7, seats: 32, booked: 21 },
    { name: '陆家嘴店', dist: 5.1, score: 4.9, seats: 48, booked: 42 },
    { name: '中关村店', dist: 4.2, score: 4.6, seats: 28, booked: 12 },
    { name: '环贸 iapm 店', dist: 2.1, score: 4.8, seats: 36, booked: 25 },
    { name: '万象城店', dist: 6.3, score: 4.7, seats: 44, booked: 28 },
    { name: '太古里店', dist: 3.9, score: 4.9, seats: 52, booked: 48 },
    { name: '滨江天街店', dist: 7.5, score: 4.5, seats: 24, booked: 10 },
    { name: '首钢园店', dist: 9.8, score: 4.6, seats: 30, booked: 16 }
  ]

  private messages: MessageItem[] = [
    { title: '发货通知', content: '您的瑰夏挂耳礼盒已由顺丰揽收,全程冷链', time: '11:02', unread: 2 },
    { title: '订阅提醒', content: '下期云南小粒豆将在周四烘焙,周五发出', time: '10:15', unread: 1 },
    { title: '门店预约', content: '国贸旗舰店手冲体验课已预约成功,请准时到店', time: '09:30', unread: 3 },
    { title: '烘焙出炉', content: '本周三支新豆出炉:花魁 7.0 已上架', time: '昨天', unread: 0 },
    { title: '会员日', content: '每月 18 日会员日,全场咖啡豆 88 折', time: '昨天', unread: 0 },
    { title: '品鉴邀请', content: '您获得 1 张杯测品鉴会入场券,本周六开场', time: '前天', unread: 1 },
    { title: '优惠到账', content: '咖啡节专享券 3 张已放入卡包,满 99 可用', time: '前天', unread: 0 },
    { title: '器具保养', content: '您的磨豆机已到建议清洁周期,查看教程', time: '周三', unread: 0 },
    { title: '茶叶上新', content: '明前龙井头采开售,限量 200 罐', time: '周三', unread: 1 },
    { title: '订单完成', content: '法压壶已签收,晒图可返 10 元京豆', time: '周二', unread: 0 },
    { title: '降价通知', content: '购物车里的聪明杯降价 20 元', time: '周一', unread: 0 },
    { title: '系统公告', content: '咖啡订阅服务升级,可随时暂停配送', time: '上周', unread: 0 }
  ]

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

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

  private selScore(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.score
  }

  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.tabBean()
            } else if (this.currentTab === 2) {
              this.tabTea()
            } else if (this.currentTab === 3) {
              this.tabGear()
            } else if (this.currentTab === 4) {
              this.tabShop()
            } 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.showSubscribe) {
        this.modalSubscribe()
      }
      if (this.showVisit) {
        this.modalVisit()
      }
      if (this.showDelete) {
        this.modalDelete()
      }
      if (this.showDetail) {
        this.modalDetail()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FAF6F0')
  }

  @Builder
  headerBar() {
    Column() {
      Row() {
        Column() {
          Text('咖啡 · 茗茶')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('京东生活馆')
            .fontSize(10)
            .fontColor('#D7CCC8')
            .margin({ top: 2 })
        }
        Column() {
          Row() {
            Text('搜索咖啡豆 / 茶叶 / 手冲器具')
              .fontSize(12)
              .fontColor('#BCAAA4')
          }
          .width('100%')
          .height(30)
          .backgroundColor('#FFFFFF')
          .borderRadius(15)
          .justifyContent(FlexAlign.Start)
          .padding({ left: 14 })
        }
        .layoutWeight(1)
        .margin({ left: 12, right: 12 })
        Column() {
          Text('消息')
            .fontSize(13)
            .fontColor('#FFFFFF')
        }
        .onClick(() => {
          this.currentTab = 5
        })
      }
      .width('100%')
      .height(54)
      .padding({ left: 14, right: 14 })
      .alignItems(VerticalAlign.Center)

      Row() {
        ForEach(['咖啡豆', '挂耳', '意式', '龙井', '普洱', '白茶', '手冲壶'], (k: string) => {
          Text(k)
            .fontSize(11)
            .fontColor('#D7CCC8')
            .backgroundColor('rgba(255,255,255,0.12)')
            .borderRadius(12)
            .padding({ left: 10, right: 10, top: 4, bottom: 4 })
            .margin({ right: 8 })
        })
      }
      .width('100%')
      .padding({ left: 14, bottom: 8 })
      .justifyContent(FlexAlign.Start)
    }
    .width('100%')
    .backgroundColor('#5D4037')
  }

  @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 ? '#FFB300' : 'rgba(255,255,255,0.35)')
          Text(tab)
            .fontSize(11)
            .fontColor(index === this.currentTab ? '#FFB300' : 'rgba(255,255,255,0.6)')
            .margin({ top: 5 })
        }
        .layoutWeight(1)
        .height(54)
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.currentTab = index
        })
      })
    }
    .width('100%')
    .backgroundColor('#4E342E')
  }

  @Builder
  tabMarket() {
    Column() {
      Row() {
        Column() {
          Text('早春豆单上新')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('埃塞俄比亚 · 瑰夏村直发 · 满159减40')
            .fontSize(11)
            .fontColor('#D7CCC8')
            .margin({ top: 6 })
          Text('查看豆单')
            .fontSize(12)
            .fontColor('#4E342E')
            .backgroundColor('#FFB300')
            .borderRadius(14)
            .padding({ left: 14, right: 14, top: 6, bottom: 6 })
            .margin({ top: 12 })
        }
        .layoutWeight(2)
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('7842')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFB300')
          Text('爆款已售')
            .fontSize(10)
            .fontColor('#D7CCC8')
            .margin({ top: 2 })
          Text('订阅豆单')
            .fontSize(11)
            .fontColor('#4E342E')
            .backgroundColor('#FFB300')
            .borderRadius(12)
            .padding({ left: 10, right: 10, 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)
        .margin({ left: 10 })
        .onClick(() => {
          this.showSubscribe = true
        })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#5D4037')
      .borderRadius(12)
      .alignItems(VerticalAlign.Center)

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

        Column() {
          Text('99')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#E65100')
          Text('最高杯测分')
            .fontSize(10)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(58)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })

        Column() {
          Text('5 大')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#5D4037')
          Text('著名产区')
            .fontSize(10)
            .fontColor('#999999')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(58)
        .backgroundColor('#FFFFFF')
        .borderRadius(10)
        .margin({ top: 10, left: 8 })

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

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

      Column() {
        ForEach(this.coffees, (c: CoffeeItem) => {
          Row() {
            Column()
              .width(84)
              .height(84)
              .backgroundColor('#D7CCC8')
              .borderRadius(42)
            Column() {
              Text(c.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text(c.origin)
                  .fontSize(10)
                  .fontColor('#E65100')
                  .backgroundColor('#FFF3E0')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text(c.roast)
                  .fontSize(10)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#7CB342')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .margin({ left: 6 })
                Text('杯测 ' + c.score)
                  .fontSize(10)
                  .fontColor('#5D4037')
                  .fontWeight(FontWeight.Bold)
                  .margin({ left: 6 })
                  .scale({ x: 1.05, y: 1.05 })
                  .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
              }
              .margin({ top: 6 })

              Stack({ alignContent: Alignment.Start }) {
                Column()
                  .width('100%')
                  .height(6)
                  .backgroundColor('#F0F0F0')
                  .borderRadius(3)
                Column()
                  .width(c.score + '%')
                  .height(6)
                  .backgroundColor('#E65100')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 8 })

              Row() {
                Text('¥' + c.price)
                  .fontSize(17)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#E65100')
                Text('已售' + c.sold)
                  .fontSize(11)
                  .fontColor('#999999')
                  .margin({ left: 8 })
                Text('订阅价')
                  .fontSize(11)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#5D4037')
                  .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 = c
            this.showDetail = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabBean() {
    Column() {
      Column() {
        Text('庄园豆海拔对比 (m)')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
        Row() {
          ForEach(this.beanList, (b: BeanItem) => {
            Column() {
              Column()
                .width(15)
                .height(b.altitude / this.maxAltitude() * 84)
                .backgroundColor('#FFB300')
                .borderRadius(4)
              Text(b.altitude + '')
                .fontSize(8)
                .fontColor('#D7CCC8')
                .margin({ top: 3 })
            }
            .margin({ right: 9 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .height(112)
        .justifyContent(FlexAlign.Start)
        .margin({ top: 10 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#4E342E')
      .borderRadius(12)
      .alignItems(HorizontalAlign.Start)

      Row() {
        Column() {
          Text('订阅咖啡豆')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('周期烘焙 · 定时配送')
            .fontSize(10)
            .fontColor('#FFCC80')
            .margin({ top: 2 })
        }
        .layoutWeight(1)
        .height(52)
        .backgroundColor('#5D4037')
        .borderRadius(10)
        .justifyContent(FlexAlign.Center)
        .margin({ top: 10 })
        .onClick(() => {
          this.showSubscribe = true
        })

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

      Column() {
        ForEach(this.beanList, (b: BeanItem, idx: number) => {
          Row() {
            Column() {
              Text('No.' + (idx + 1))
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor(idx < 3 ? '#E65100' : '#BCAAA4')
            }
            .width(44)
            .height(64)
            .backgroundColor('#EFEBE9')
            .borderRadius(10)
            .justifyContent(FlexAlign.Center)

            Column() {
              Text(b.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text(b.farm)
                  .fontSize(10)
                  .fontColor('#5D4037')
                  .backgroundColor('#EFEBE9')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text(b.process)
                  .fontSize(10)
                  .fontColor('#FFFFFF')
                  .backgroundColor('#7CB342')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .margin({ left: 6 })
                Text('海拔 ' + b.altitude + 'm')
                  .fontSize(10)
                  .fontColor('#999999')
                  .margin({ left: 6 })
              }
              .margin({ top: 6 })

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

            Column() {
              Text('¥' + b.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E65100')
              Text('200g/袋')
                .fontSize(10)
                .fontColor('#999999')
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .onClick(() => {
            this.selectedItem = this.coffees[idx % this.coffees.length]
            this.showDetail = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabTea() {
    Column() {
      Column() {
        Text('茗茶专区')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#33691E')
        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.teas, (t: TeaItem) => {
            Column() {
              Column()
                .width(15)
                .height(t.ferment / 100 * 80 + 4)
                .backgroundColor('#7CB342')
                .borderRadius(4)
              Text(t.ferment + '')
                .fontSize(9)
                .fontColor('#999999')
                .margin({ top: 4 })
            }
            .margin({ right: 9 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .height(110)
        .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.teas, (t: TeaItem) => {
          Column() {
            Row() {
              Text(t.type)
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#7CB342')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              Text('发酵 ' + t.ferment + '%')
                .fontSize(10)
                .fontColor('#999999')
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)

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

            Row() {
              Text('¥' + t.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E65100')
              Text('好评 ' + t.rating + '%')
                .fontSize(10)
                .fontColor('#5D4037')
                .margin({ left: 6 })
                .scale({ x: 1.05, y: 1.05 })
                .animation({ duration: 750, iterations: -1, curve: Curve.EaseInOut })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)
            .margin({ top: 8 })

            Stack({ alignContent: Alignment.Start }) {
              Column()
                .width('100%')
                .height(5)
                .backgroundColor('#F0F0F0')
                .borderRadius(3)
              Column()
                .width(t.rating + '%')
                .height(5)
                .backgroundColor('#7CB342')
                .borderRadius(3)
            }
            .width('100%')
            .margin({ top: 6 })
          }
          .width('48%')
          .backgroundColor('#FFFFFF')
          .borderRadius(12)
          .padding(10)
          .margin({ top: 10 })
          .alignItems(HorizontalAlign.Start)
          .onClick(() => {
            this.showSubscribe = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabGear() {
    Column() {
      Column() {
        Row() {
          Text('器具冲泡实验室')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
          Text('容量对比 (ml)')
            .fontSize(11)
            .fontColor('#D7CCC8')
            .margin({ left: 8 })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)

        Row() {
          ForEach(this.gears, (g: GearItem) => {
            Column() {
              Column()
                .width(15)
                .height(g.capacity / 1500 * 80 + 3)
                .backgroundColor('#FF7043')
                .borderRadius(4)
              Text(g.capacity + '')
                .fontSize(8)
                .fontColor('#D7CCC8')
                .margin({ top: 3 })
            }
            .margin({ right: 9 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .height(110)
        .justifyContent(FlexAlign.Start)
        .margin({ top: 10 })
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#6D4C41')
      .borderRadius(12)
      .alignItems(HorizontalAlign.Start)

      Scroll() {
        Row() {
          ForEach(this.gears, (g: GearItem) => {
            Column() {
              Column()
                .width(96)
                .height(60)
                .backgroundColor('#D7CCC8')
                .borderRadius(10)
              Text(g.material)
                .fontSize(10)
                .fontColor('#6D4C41')
                .backgroundColor('#EFEBE9')
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                .margin({ top: 6 })
              Text('¥' + g.price)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E65100')
                .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.gears, (g: GearItem) => {
          Row() {
            Column()
              .width(58)
              .height(58)
              .backgroundColor('#EFEBE9')
              .borderRadius(10)
            Column() {
              Text(g.name)
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#333333')
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Row() {
                Text(g.capacity + 'ml')
                  .fontSize(10)
                  .fontColor('#6D4C41')
                  .backgroundColor('#EFEBE9')
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                Text('库存 ' + g.stock)
                  .fontSize(10)
                  .fontColor('#999999')
                  .margin({ left: 8 })
                if (g.stock < 100) {
                  Text('少量')
                    .fontSize(10)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#E65100')
                    .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 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })

            Column() {
              Text('¥' + g.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E65100')
              Text('加入购物车')
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor('#6D4C41')
                .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.showDelete = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabShop() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('门店手冲体验')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('大师现场冲煮 · 免费品鉴三杯')
              .fontSize(11)
              .fontColor('#FFCC80')
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Text('立即预约')
            .fontSize(12)
            .fontColor('#4E342E')
            .backgroundColor('#FFB300')
            .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.showVisit = true
            })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#5D4037')
      .borderRadius(12)

      Column() {
        Text('到店预约热度 (本周)')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
        Row() {
          ForEach(this.shopList, (s: ShopItem) => {
            Column() {
              Column()
                .width(15)
                .height(s.booked / this.maxScore() * 80)
                .backgroundColor('#8D6E63')
                .borderRadius(4)
              Text(s.booked + '')
                .fontSize(9)
                .fontColor('#999999')
                .margin({ top: 4 })
            }
            .margin({ right: 9 })
            .alignItems(HorizontalAlign.Center)
          })
        }
        .width('100%')
        .height(110)
        .justifyContent(FlexAlign.Start)
        .margin({ top: 8 })
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
      .margin({ top: 10 })
      .alignItems(HorizontalAlign.Start)

      Column() {
        ForEach(this.shopList, (s: ShopItem) => {
          Row() {
            Column()
              .width(50)
              .height(50)
              .backgroundColor('#D7CCC8')
              .borderRadius(10)
            Column() {
              Row() {
                Text(s.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#333333')
                Text(s.score + ' 分')
                  .fontSize(11)
                  .fontColor('#E65100')
                  .fontWeight(FontWeight.Bold)
                  .margin({ left: 8 })
                  .scale({ x: 1.05, y: 1.05 })
                  .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
              }
              Text('距离 ' + s.dist + 'km · ' + s.seats + ' 席位')
                .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.seats * 100 + '%')
                  .height(6)
                  .backgroundColor(s.booked / s.seats > 0.8 ? '#E65100' : '#7CB342')
                  .borderRadius(3)
              }
              .width('100%')
              .margin({ top: 6 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .padding({ left: 10 })

            Column() {
              Text(s.booked + '/' + s.seats)
                .fontSize(11)
                .fontColor('#999999')
              Text('预约')
                .fontSize(11)
                .fontColor('#FFFFFF')
                .backgroundColor('#5D4037')
                .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.showVisit = true
          })
        })
      }
      .width('100%')
    }
    .width('100%')
  }

  @Builder
  tabMessage() {
    Column() {
      Column() {
        Row() {
          Text('消息中心')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
          Text('8 条新动态')
            .fontSize(11)
            .fontColor('#E65100')
            .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('#EFEBE9')
              .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('#E65100')
                    .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('#D7CCC8')
            .borderRadius(30)
          Column() {
            Text('手冲玩家_Leo')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
            Text('咖啡品味等级:银杯杯测师')
              .fontSize(11)
              .fontColor('#FFCC80')
              .margin({ top: 4 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .padding({ left: 12 })
        }
        
---

## 总结
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/1cb16f26ef3649edb812dbff95ff1116.png#pic_center)

本文深入剖析了一个基于HarmonyOS 6.1.1声明式UI框架的咖啡茗茶生活馆应用的完整源码实现。从六个数据接口定义到状态管理机制,从七个功能Tab的详细构建到五个模态弹窗的交互逻辑,全面覆盖了ArkTS声明式开发在咖啡茗茶垂直电商领域的核心技术实践。应用通过24款咖啡商品、12款庄园咖啡豆、10款茶叶、10款冲泡器具和10家门店的数据集,构建了一个覆盖咖啡购买、豆单订阅、茶叶选购、器具冲泡和门店体验全链路的服务平台。

在技术实现层面,该应用展示了ArkUI声明式框架的丰富表达能力。海拔柱状图、发酵度柱状图、容量柱状图和预约热度柱状图均通过Column组件的动态height计算实现,无需引入任何第三方图表库。门店预约率进度条通过三元表达式`s.booked / s.seats > 0.8 ? '#E65100' : '#7CB342'`实现了基于阈值的动态颜色切换,当预约率超过80%时自动从绿色切换为橙色,这种数据驱动的视觉反馈是本应用的一大交互创新。庄园咖啡豆列表的序号排名设计通过`idx < 3`条件判断为前三名分配橙色编号、其余使用浅棕色编号,在视觉上突出了品质排名。取模索引`coffees[idx % coffees.length]`的运用确保了咖啡豆到咖啡商品数据的安全映射,防止数组越界。

门店体验预约通过"免费"标签的脉冲动画强调服务的零成本属性,配合体验内容文本展示有效提升了预约转化率。弹窗级联切换通过顺序修改两个状态变量实现场景无缝过渡。颜色编码设计贯穿整个应用:棕色系用于咖啡品类、绿色用于茶叶和烘焙度标签、琥珀色用于评分和排名、橙色用于价格和行动按钮,形成了一套完整的视觉语言系统。整体而言,该应用是学习HarmonyOS ArkTS声明式开发在垂直电商场景下应用的一个综合性范例,涵盖了数据建模、状态管理、布局设计、数据可视化、动画交互、动态颜色和多维度业务流程编排等核心技术领域。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐