引言

在这里插入图片描述

酒水饮料作为电商垂直品类中极具特色的赛道,其移动端应用开发面临着独特的业务挑战。酒类商品不仅需要正品溯源保障和恒温运输配送,还涉及团购拼单、私人酒窖管理、酒品陈化估值等专业服务场景。本应用以京东酒水饮料馆为业务原型,基于HarmonyOS声明式UI开发框架,构建了一个覆盖酒水集市、精酿啤酒、红酒庄园、白酒陈酿、私人酒窖、消息中心和用户中心七大功能模块的完整电商系统。

从技术架构层面来看,该应用采用了与典型电商应用相似的单页面多标签架构,但在状态管理的复杂度上有所提升。除了常规的导航控制和弹窗控制状态外,应用还引入了酒窖存放位置选择(selCellar)、存放年限选择(selYears)、瓶数调节(bottleNum)等酒水专属业务状态。所有商品数据同样通过interface接口进行类型约束,但GoodsItemnum字段在酒水场景中统一表示度数(酒精度)或年份(陈化年数),使得同一数据结构能够灵活适配不同维度的可视化需求。

从业务设计层面来看,该应用深度还原了酒水行业的核心业务流程。头部区域展示恒温配送承诺和年货酒水节促销信息,配以脉冲动画引导用户关注抢购入口;精酿啤酒页面引入度数柱状图,通过可视化手段帮助用户理解不同啤酒的酒精度分布;红酒庄园页面创新性地使用评分进度条列表,将WS/帕克评分折算为进度条进行直观对比;私人酒窖模块更是应用的亮点功能,实时展示窖温、湿度数据,并提供陈化年份柱状图和藏酒清单管理,充分体现了酒类电商的专业深度。

从交互设计层面来看,应用在弹窗设计上做了差异化处理。整箱团购弹窗和酒窖存入弹窗均从底部弹出(justifyContent: FlexAlign.End),符合移动端操作习惯;而发布闲置和删除确认弹窗则从中央弹出。酒窖存入弹窗还创新性地提供了陈年后价值估算功能,通过Math.floor(selPrice() * bottleNum * 1.4)公式按1.4倍增值系数计算预估价值,为用户的藏酒投资决策提供了参考依据。

数据模型与接口定义

在这里插入图片描述

应用通过三个接口定义了核心数据模型,确保了整个数据流的类型安全。GoodsItem接口描述了商品实体结构,包含名称、价格、描述、评分、数值参数、标签和热销标记七个字段。TabInfo接口定义了导航标签的结构。MsgItem接口规范了消息通知的数据格式,包含标题、内容、时间和已读状态。

interface GoodsItem {
  name: string
  price: number
  desc: string
  score: number
  num: number
  tag: string
  hot: boolean
}

interface TabInfo {
  name: string
  icon: string
}

interface MsgItem {
  title: string
  content: string
  time: string
  read: boolean
}

GoodsItemnum字段在酒水应用中承担了多维度的业务语义:在精酿啤酒列表中表示酒精度数(如7、11.3等),在红酒列表中表示酒体容量数值,在白酒列表中表示度数(如53、52等),在酒窖藏酒列表中表示已陈化年数。这种复用设计使得同一套数据驱动逻辑能够适配不同的柱状图可视化场景,极大提升了代码的复用率。

状态管理与组件初始化

在这里插入图片描述

根组件Index通过大量@State装饰器声明了响应式状态变量。与黄金珠宝应用相比,酒水应用的状态管理引入了团购规格选择(selSpec)、酒窖位置选择(selCellar)、存放年限选择(selYears)和瓶数调节(bottleNum)等酒水专属业务状态。

@Entry
@Component
struct Index {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showGroup: boolean = false
  @State showCellar: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: GoodsItem | null = null
  @State selType: number = 0
  @State selDegree: number = 0
  @State selSpec: number = 0
  @State boxNum: number = 1
  @State selCellar: number = 0
  @State selYears: number = 0
  @State bottleNum: number = 6
  @State deleteIndex: number = -1
  @State msgs: MsgItem[] = [
    { title: '抢购提醒', content: '茅台放量 100 瓶,10:00 准时开抢,先到先得', time: '09:55', read: false },
    { title: '酒窖温控警告', content: '您的 3 号酒柜温度偏高 1.5℃,已自动调节', time: '09:12', read: false },
    { title: '团购成团', content: '您发起的精酿 6 箱团购已成团,差价已返还', time: '昨天', read: false },
    { title: '物流更新', content: '您的红酒已发出,恒温车运输全程温控', time: '昨天', read: true },
    { title: '品鉴会邀约', content: '周六晚勃艮第垂直品鉴,席位仅剩 4 个', time: '前天', read: true },
    { title: '存酒到期', content: '您存的两瓶白酒已满 3 年,可随时提货', time: '前天', read: true },
    { title: '价格保护', content: '您购买的清酒降价 20 元,价保差额已到账', time: '周三', read: true },
    { title: '直播预告', content: '今晚 20:00 酒馆老板娘直播盲品挑战', time: '周三', read: true },
    { title: '券包到期', content: '满 199 减 30 洋酒券将于 3 天后过期', time: '周四', read: true },
    { title: '新品上架', content: '帝国世涛限量 500 箱已上架,每人限购 2 箱', time: '上周', read: true },
    { title: '评价有礼', content: '晒酒柜照片评价可领 25 元精酿券', time: '上周', read: true },
    { title: '温馨提示', content: '未成年人禁止饮酒,孕妇请勿饮酒', time: '上周', read: true }
  ]

消息列表中特别值得关注的是"酒窖温控警告"消息,它暗示了应用具有智能酒窖温控的物联网集成能力。bottleNum的初始值为6,对应一标准箱红酒的瓶数,体现了酒水行业的标准计量单位选择。boxNum初始值为1,用于团购弹窗中的箱数选择器。

私有数据与商品列表

在这里插入图片描述

组件内部通过private声明了多个配置选项和商品数据数组。配置选项包括酒品类型、度数区间、团购规格、酒窖位置和存放年限五组选项数组,分别服务于发布闲置、团购和酒窖存入弹窗。商品数据按品类分为五组:综合精选items、精酿啤酒beers、红酒wines、白酒烈酒spirits和酒窖藏酒cellarGoods

  private tabs: TabInfo[] = [
    { name: '酒水集市', icon: '🍻' },
    { name: '精酿啤酒', icon: '🍺' },
    { name: '红酒庄园', icon: '🍷' },
    { name: '白酒陈酿', icon: '🍶' },
    { name: '私人酒窖', icon: '🗝️' },
    { name: '消息', icon: '✉️' },
    { name: '我的', icon: '👤' }
  ]
  private types: string[] = ['精酿啤酒', '红葡萄酒', '白葡萄酒', '威士忌', '清酒', '洋酒']
  private degrees: string[] = ['低度 3-8°', '中度 9-20°', '高度 40°+', '无醇 0°']
  private specs: string[] = ['330ml×24听', '500ml×12瓶', '750ml×6瓶', '1.5L×2瓶']
  private cellars: string[] = ['恒温酒柜A', '地下酒窖B区', '私人藏酒间', '门店寄存柜']
  private yearChips: string[] = ['存 1 年', '存 3 年', '存 5 年', '存 10 年']

  private items: GoodsItem[] = [
    { name: '福佳白啤 330ml×24', price: 108, desc: '比利时风味 香菜籽橘皮', score: 95, num: 5, tag: '销量王', hot: true },
    { name: '青岛经典 500ml×12', price: 66, desc: '麦香浓郁 国民口粮', score: 94, num: 4, tag: '口粮款', hot: true },
    { name: '鹅岛IPA 355ml×6', price: 78, desc: '美式酒花 苦香平衡', score: 93, num: 7, tag: '进阶款', hot: false },
    { name: '迷失海岸幽灵浑浊', price: 128, desc: '浑浊IPA 果汁感强', score: 95, num: 7, tag: '爆款', hot: true },
    { name: '罗斯福10号 330ml', price: 32, desc: '比利时四料 修道院传奇', score: 96, num: 11, tag: '神级', hot: false },
    { name: '智美红帽 330ml×6', price: 168, desc: '修道院认证 深琥珀色', score: 95, num: 9, tag: '经典', hot: false },
    { name: '奔富BIN389 750ml', price: 468, desc: '澳洲西拉赤霞珠', score: 96, num: 14, tag: '宴请款', hot: true },
    { name: '拉菲传奇波尔多', price: 128, desc: '法定产区 级别餐酒', score: 92, num: 13, tag: '入门红酒', hot: false },
    { name: '杰卡斯霞多丽白', price: 98, desc: '南澳白葡萄酒 清爽果香', score: 93, num: 13, tag: '白葡萄酒', hot: false },
    { name: '茅台飞天 53° 500ml', price: 2699, desc: '酱香鼻祖 硬通货', score: 98, num: 53, tag: '限量抢', hot: true },
    { name: '五粮液第八代 52°', price: 1099, desc: '浓香王者 宴席首选', score: 97, num: 52, tag: '宴席', hot: false },
    { name: '汾酒青花20 53°', price: 468, desc: '清香典范 一清到底', score: 96, num: 53, tag: '清香型', hot: false },
    { name: '山崎12年威士忌', price: 1580, desc: '日威标杆 雪莉桶', score: 98, num: 12, tag: '收藏级', hot: true },
    { name: '麦卡伦12双雪莉', price: 688, desc: '单一麦芽 入门收藏', score: 96, num: 12, tag: '单一麦芽', hot: false },
    { name: '獭祭二割三分纯米', price: 358, desc: '精米步合23% 果香四溢', score: 95, num: 23, tag: '清酒', hot: false },
    { name: '芝华士12年 700ml', price: 168, desc: '苏格兰调和 套餐友好', score: 92, num: 12, tag: '调和威士忌', hot: false },
    { name: '百龄坛特酿 700ml', price: 138, desc: '顺滑易饮 调酒基底', score: 91, num: 12, tag: '基酒', hot: false },
    { name: '摩根船长白朗姆', price: 98, desc: '鸡尾酒必备 加勒比风', score: 90, num: 12, tag: '朗姆', hot: false },
    { name: '三得利微醺 350ml', price: 12, desc: '3度低醇 气泡清爽', score: 93, num: 3, tag: '微醺', hot: true },
    { name: '锐澳预调酒 330ml', price: 10, desc: '果味低度 聚会百搭', score: 89, num: 3, tag: '聚会', hot: false },
    { name: '长城桑干酒庄西拉', price: 288, desc: '国产精品酒庄 烟台', score: 92, num: 14, tag: '国产酒庄', hot: false },
    { name: '张裕解百纳特选级', price: 128, desc: '百年张裕 窖藏橡木', score: 91, num: 14, tag: '老字号', hot: false },
    { name: '健力士黑啤 440ml', price: 15, desc: '爱尔兰世涛 绵密泡沫', score: 92, num: 4, tag: '世涛', hot: false },
    { name: '保拉纳小麦啤 500ml', price: 16, desc: '德式小麦 香蕉酯香', score: 93, num: 5, tag: '德式', hot: false }
  ]

综合商品列表覆盖了从10元的预调酒到2699元的茅台飞天,价格跨度极大。精酿啤酒列表的tag字段存储了酒精度信息(如"酒精度7%“),在柱状图中通过g.tag.slice(3, 8)提取数值部分进行标注。酒窖藏酒列表cellarGoods是应用独有的数据集,每件商品都标记为"私藏中”,并记录了已陈化年数和当前估值。

辅助方法与计算逻辑

在这里插入图片描述

组件定义了与黄金珠宝应用结构一致的辅助方法,包括安全提取选中项属性的selNameselDescselTagselPriceselScore方法,以及统计计算用的maxOfNumunreadCount方法。

  selName(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.name
  }
  selDesc(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.desc
  }
  selTag(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.tag
  }
  selPrice(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.price
  }
  selScore(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.score
  }
  maxOfNum(list: GoodsItem[]): number {
    let m: number = 0
    for (let i = 0; i < list.length; i++) {
      if (list[i].num > m) {
        m = list[i].num
      }
    }
    return m
  }
  unreadCount(): number {
    let c: number = 0
    for (let i = 0; i < this.msgs.length; i++) {
      if (!this.msgs[i].read) {
        c++
      }
    }
    return c
  }

这些方法的设计模式完全一致:空值保护返回默认值,确保弹窗在未选中商品时不会渲染无效内容。maxOfNum方法接收商品列表参数,使得不同品类列表(啤酒、红酒、白酒、酒窖)都能复用同一个最大值计算逻辑,避免了代码重复。

头部构建与促销信息

在这里插入图片描述

buildHeader方法构建了应用的顶部导航区域,采用了与酒水主题契合的深红色#B71C1C背景。头部整合了恒温配送承诺、品牌标识、团购入口和消息中心入口,底部还展示了年货酒水节的促销信息条。

  @Builder buildHeader() {
    Column() {
      Row() {
        Column() {
          Row() {
            Text('🚚').fontSize(12)
            Text('最快 30 分钟达 · 全程恒温配送').fontSize(10).fontColor('rgba(255,255,255,0.8)').margin({ left: 4 })
          }
          .alignItems(VerticalAlign.Center)
          Text('京东酒水馆').fontSize(19).fontColor(Color.White).fontWeight(FontWeight.Bold).margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text('🛒').fontSize(20)
          Text('团购').fontSize(9).fontColor('rgba(255,255,255,0.8)').margin({ top: 2 })
        }
        .width(48)
        .onClick(() => {
          this.showGroup = true
        })

        Column() {
          Text('✉️').fontSize(20)
          if (this.unreadCount() > 0) {
            Text(this.unreadCount().toString())
              .fontSize(8)
              .fontColor(Color.White)
              .backgroundColor('#FF3B30')
              .borderRadius(7)
              .padding({ left: 4, right: 4 })
              .position({ x: 12, y: -4 })
              .scale({ x: 1.12, y: 1.12 })
              .animation({ duration: 650, iterations: -1, curve: Curve.EaseInOut })
          }
        }
        .width(48)
        .onClick(() => {
          this.currentTab = 5
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 12 })
      .alignItems(VerticalAlign.Center)

      Row() {
        Text('年货酒水节 · 满 299 减 60').fontSize(11).fontColor('#FFD3C2')
        Text('抢茅台 ›').fontSize(11).fontColor('#FF8A65').margin({ left: 10 })
          .scale({ x: 1.05, y: 1.05 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 12 })
      .justifyContent(FlexAlign.Start)
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .backgroundColor('#B71C1C')
  }

头部区域与黄金珠宝应用的显著区别在于:团购入口直接放置在头部而非底部导航栏中,突出了团购业务在酒水品类中的重要性。未读角标使用了红色#FF3B30背景配合白色文字,与头部深红色形成对比,通过1.12倍脉冲动画吸引用户注意。促销信息条中"抢茅台"文字配有1.05倍缩放动画,引导用户点击参与限量抢购活动。

酒水集市与限时闪购

在这里插入图片描述

buildTab1方法构建了酒水集市的商品列表页。页面顶部是限时闪购横幅,以茅台飞天为核心引流商品,配有脉冲动画的"马上抢"按钮。下方商品列表与黄金珠宝应用结构类似,但图标使用了啤酒、红酒、白酒和威士忌杯四种表情符号循环交替。

  @Builder buildTab1() {
    Column() {
      Row() {
        Column() {
          Text('限时闪购').fontSize(16).fontColor(Color.White).fontWeight(FontWeight.Bold)
          Text('整点放量 · 手慢无').fontSize(10).fontColor('rgba(255,255,255,0.75)').margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text('茅台飞天 53°').fontSize(12).fontColor('#FFD54F').fontWeight(FontWeight.Bold)
          Text('¥2699 限量100瓶').fontSize(10).fontColor('rgba(255,255,255,0.85)').margin({ top: 4 })
          Column() {
            Text('马上抢').fontSize(11).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
          }
          .backgroundColor('#FFD54F')
          .borderRadius(13)
          .padding({ left: 14, right: 14, top: 5, bottom: 5 })
          .margin({ top: 8 })
          .scale({ x: 1.06, y: 1.06 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          .onClick(() => {
            this.boxNum = 1
            this.showGroup = true
          })
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')
      .backgroundColor('#7F1D1D')
      .borderRadius(14)
      .padding(16)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(VerticalAlign.Top)

      Row() {
        Text('酒水精选').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        Text('正品溯源 · 破损包赔').fontSize(10).fontColor('#9AA0B5').margin({ left: 8 })
      }
      .width('100%')
      .padding({ left: 16, top: 14, bottom: 8 })
      .justifyContent(FlexAlign.Start)
      .alignItems(VerticalAlign.Center)

      ForEach(this.items, (g: GoodsItem, i: number) => {
        Row() {
          Column() {
            Text(i % 4 === 0 ? '🍺' : (i % 4 === 1 ? '🍷' : (i % 4 === 2 ? '🍶' : '🥃'))).fontSize(26)
          }
          .width(72)
          .height(72)
          .borderRadius(12)
          .backgroundColor(i % 2 === 0 ? '#FDEBE7' : '#FBE9E7')
          .justifyContent(FlexAlign.Center)

          Column() {
            Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
            Text(g.desc).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)

            Row() {
              Text(g.tag).fontSize(9).fontColor('#B71C1C').backgroundColor('#FDEBE7').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 })
              if (g.hot) {
                Text('秒杀中').fontSize(9).fontColor(Color.White).backgroundColor('#FF3B30').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
                  .scale({ x: 1.06, y: 1.06 })
                  .animation({ duration: 650, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .margin({ top: 5 })
            .alignItems(VerticalAlign.Center)

            Stack({ alignContent: Alignment.Start }) {
              Column().width('100%').height(6).backgroundColor('#F0F0F5').borderRadius(3)
              Column().width(g.score + '%').height(6).backgroundColor('#B71C1C').borderRadius(3)
            }
            .width('100%')
            .margin({ top: 6 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 10 })

          Column() {
            Text('¥' + g.price).fontSize(16).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
            Column() {
              Text('整箱团购').fontSize(10).fontColor(Color.White)
            }
            .backgroundColor('#B71C1C')
            .borderRadius(13)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .margin({ top: 8 })
            .onClick(() => {
              this.selectedItem = g
              this.boxNum = 1
              this.showGroup = true
            })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding(12)
        .margin({ left: 12, right: 12, bottom: 10 })
        .alignItems(VerticalAlign.Center)
        .onClick(() => {
          this.selectedItem = g
          this.showDetail = true
        })
      }, (g: GoodsItem) => g.name)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

限时闪购横幅采用了更深的红色#7F1D1D背景,与头部红色形成层次感。“马上抢"按钮使用了金黄色#FFD54F背景配红色文字,与整体红色调形成鲜明对比,通过1.06倍脉冲动画吸引用户点击。点击该按钮直接打开团购弹窗并重置箱数为1。商品卡片中"正品溯源 · 破损包赔"的文案标签突出了酒水品类的正品保障需求,热销标签显示为"秒杀中"而非黄金珠宝的"热销榜TOP”,体现了不同品类的营销话术差异。

精酿啤酒度数可视化

buildTab2方法构建了精酿啤酒页面,核心特色是度数柱状图和双列口碑榜。柱状图使用橙色#FF7043柱体展示各啤酒的酒精度分布,标签通过g.tag.slice(3, 8)从"酒精度7%"格式中提取数值部分。

  @Builder buildTab2() {
    Column() {
      Text('精酿啤酒 · 度数柱状图').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 14, bottom: 8 })

      Column() {
        Row({ space: 10 }) {
          ForEach(this.beers, (g: GoodsItem) => {
            Column() {
              Column()
                .width(16)
                .height(g.num / this.maxOfNum(this.beers) * 100)
                .backgroundColor('#FF7043')
                .borderRadius(4)
              Text(g.tag.slice(3, 8)).fontSize(7).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .alignItems(VerticalAlign.End)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('口碑榜 TOP 双列').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.beers, (g: GoodsItem, i: number) => {
          Column() {
            Row() {
              Column() {
                Text('🍺').fontSize(24)
              }
              .width(46)
              .height(46)
              .borderRadius(23)
              .backgroundColor(i % 2 === 0 ? '#FFF3E0' : '#FBE9E7')
              .justifyContent(FlexAlign.Center)

              Column() {
                Text(g.tag).fontSize(9).fontColor('#BF360C').fontWeight(FontWeight.Bold)
                Text('好评 ' + g.score + '%').fontSize(9).fontColor('#8A8FA3').margin({ top: 3 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.End)
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

            Text(g.name).fontSize(12).fontColor('#212121').fontWeight(FontWeight.Medium).margin({ top: 8 }).maxLines(1)
            Text(g.desc).fontSize(9).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)

            Row() {
              Text('¥' + g.price).fontSize(14).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
              if (g.hot) {
                Text('酒头推荐').fontSize(9).fontColor(Color.White).backgroundColor('#FF3B30').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
                  .scale({ x: 1.08, y: 1.08 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .width('100%')
            .margin({ top: 6 })
            .justifyContent(FlexAlign.SpaceBetween)
            .alignItems(VerticalAlign.Center)
          }
          .width('48%')
          .backgroundColor(Color.White)
          .borderRadius(12)
          .padding(12)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.selectedItem = g
            this.showDetail = true
          })
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

双列口碑榜的卡片设计与黄金珠宝应用的双列布局不同,每个卡片顶部增加了圆形图标和右侧标签的并排布局,使得酒精度信息和好评率在视觉上更加突出。热销标签显示为"酒头推荐"而非通用的"热销",使用了精酿啤酒行业的专业术语,增强了品类的专业感。柱状图标签通过slice(3, 8)提取字符串片段的技术手段,巧妙地从"酒精度7%"格式中提取出"7%"显示在柱子下方。

红酒庄园评分进度

buildTab3方法构建了红酒庄园页面,采用了与啤酒页面不同的可视化方案。红酒页面使用排名序号加评分进度条的列表形式展示评分排名,前三名序号使用金色#FFD54F背景,其余使用半透明白色背景,形成了直观的排行榜视觉层次。

  @Builder buildTab3() {
    Column() {
      Column() {
        Text('红酒庄园 · 舌尖上的风土').fontSize(16).fontColor(Color.White).fontWeight(FontWeight.Bold)
        Text('评分进度一览(WS/帕克评分折算)').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 4 })

        Column() {
          ForEach(this.wines, (g: GoodsItem, i: number) => {
            Row() {
              Text((i + 1).toString()).fontSize(11).fontColor(Color.White).backgroundColor(i < 3 ? '#FFD54F' : 'rgba(255,255,255,0.3)').borderRadius(10).width(18).height(18).textAlign(TextAlign.Center)
              Text(g.name).fontSize(11).fontColor(Color.White).width(96).maxLines(1).margin({ left: 8 })
              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(7).backgroundColor('rgba(255,255,255,0.2)').borderRadius(4)
                Column().width(g.score + '%').height(7).backgroundColor('#FF8A65').borderRadius(4)
              }
              .layoutWeight(1)
              .margin({ left: 8, right: 8 })
              Text(g.score.toString()).fontSize(11).fontColor('#FFD54F').fontWeight(FontWeight.Bold)
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)
            .margin({ bottom: 10 })
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .margin({ top: 14 })
      }
      .width('100%')
      .backgroundColor('#5D1010')
      .borderRadius(14)
      .padding(16)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('酒体容量柱状图(cl)').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      Column() {
        Row({ space: 12 }) {
          ForEach(this.wines, (g: GoodsItem) => {
            Column() {
              Column()
                .width(18)
                .height(g.num / this.maxOfNum(this.wines) * 90)
                .backgroundColor('#8D6E63')
                .borderRadius(4)
              Text(g.num.toString()).fontSize(8).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .alignItems(VerticalAlign.End)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('整箱购 · 横向速览').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      Scroll() {
        Row({ space: 10 }) {
          ForEach(this.wines, (g: GoodsItem, i: number) => {
            Column() {
              Column() {
                Text('🍷').fontSize(30)
              }
              .width(84)
              .height(84)
              .borderRadius(12)
              .backgroundColor(i % 2 === 0 ? '#FBE9E7' : '#FFF3E0')
              .justifyContent(FlexAlign.Center)

              Text(g.name).fontSize(11).fontColor('#212121').margin({ top: 8 }).maxLines(1)
              Text(g.tag).fontSize(9).fontColor('#8D6E63').margin({ top: 3 })
              Text('¥' + g.price).fontSize(14).fontColor('#B71C1C').fontWeight(FontWeight.Bold).margin({ top: 4 })
            }
            .width(120)
            .backgroundColor(Color.White)
            .borderRadius(12)
            .padding(10)
            .onClick(() => {
              this.selectedItem = g
              this.showDetail = true
            })
          }, (g: GoodsItem) => g.name)
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

红酒庄园页面的评分进度区域使用了深酒红色#5D1010背景,营造了葡萄酒窖的氛围感。评分进度条采用橙色#FF8A65填充色,与深色背景形成良好的视觉对比。容量柱状图使用棕色#8D6E63柱体,呼应了红酒的酒体颜色。横向滑动区的红酒卡片宽度为120像素,比钻石页面的140像素更窄,适应了红酒商品信息更简洁的展示需求。

私人酒窖管理系统

buildTab5方法是整个应用最核心的特色功能页面,构建了一个完整的私人酒窖管理界面。页面顶部展示了酒窖的实时数据面板——在窖藏酒数量、实时窖温、实时湿度,配以钥匙图标动画。下方是陈化年份柱状图和藏酒清单列表。

  @Builder buildTab5() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('私人酒窖').fontSize(17).fontColor('#FFD54F').fontWeight(FontWeight.Bold)
            Text('恒温恒湿 · 每日巡检 · 免费保险').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          Text('🗝️').fontSize(32)
            .scale({ x: 1.08, y: 1.08 })
            .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Row() {
          Column() {
            Text('38').fontSize(22).fontColor('#FFD54F').fontWeight(FontWeight.Bold)
            Text('在窖藏酒').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('13℃').fontSize(22).fontColor('#81C784').fontWeight(FontWeight.Bold).margin({ top: 2 })
              .scale({ x: 1.04, y: 1.04 })
              .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
            Text('实时窖温').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('72%').fontSize(22).fontColor('#81C784').fontWeight(FontWeight.Bold).margin({ top: 2 })
            Text('实时湿度').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(VerticalAlign.Top)
      }
      .width('100%')
      .backgroundColor('#3E2723')
      .borderRadius(14)
      .padding(18)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text('藏酒陈化年份柱状图(年)').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Bold)
        Row({ space: 12 }) {
          ForEach(this.cellarGoods, (g: GoodsItem) => {
            Column() {
              Column()
                .width(18)
                .height(g.num / this.maxOfNum(this.cellarGoods) * 90)
                .backgroundColor('#B71C1C')
                .borderRadius(4)
              Text(g.num.toString() + '年').fontSize(8).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .margin({ top: 12 })
        .alignItems(VerticalAlign.End)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('窖藏清单').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      ForEach(this.cellarGoods, (g: GoodsItem, i: number) => {
        Row() {
          Column() {
            Text('🍷').fontSize(24)
          }
          .width(54)
          .height(54)
          .borderRadius(12)
          .backgroundColor(i % 2 === 0 ? '#EFEBE9' : '#FBE9E7')
          .justifyContent(FlexAlign.Center)

          Column() {
            Row() {
              Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
              Text(g.tag).fontSize(9).fontColor('#B7791F').backgroundColor('#FFF8E1').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
              if (g.hot) {
                Text('增值中').fontSize(9).fontColor(Color.White).backgroundColor('#2E7D32').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
                  .scale({ x: 1.08, y: 1.08 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .alignItems(VerticalAlign.Center)

            Text(g.desc).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
            Text('已陈化 ' + g.num + ' 年 · 估值涨至 ¥' + g.price).fontSize(9).fontColor('#9AA0B5').margin({ top: 3 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 10 })

          Column() {
            Column() {
              Text('存入新酒').fontSize(10).fontColor(Color.White)
            }
            .backgroundColor('#B71C1C')
            .borderRadius(13)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .onClick(() => {
              this.selectedItem = g
              this.bottleNum = 6
              this.showCellar = true
            })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding(12)
        .margin({ left: 12, right: 12, bottom: 10 })
        .alignItems(VerticalAlign.Center)
      }, (g: GoodsItem) => g.name)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

酒窖数据面板使用了深棕色#3E2723背景,模拟了酒窖的暗色环境。窖温和湿度数值使用了绿色#81C784,传达了"正常范围内"的视觉信号。窖温数值配有1.04倍的脉冲动画,暗示了温度数据的实时更新特性。藏酒清单中每项商品都显示了"已陈化 X 年 · 估值涨至 ¥X"的信息,热销标签替换为"增值中"并使用绿色背景,强调了酒窖藏酒的投资增值属性。

应用核心流程

渲染错误: Mermaid 渲染失败: Parse error on line 32: ... V U --> V``## 整箱团购弹窗`groupModal` ---------------------^ Expecting 'SEMI', 'NEWLINE', 'EOF', 'AMP', 'START_LINK', 'LINK', 'LINK_ID', got 'UNICODE_TEXT'

团购弹窗从底部弹出(justifyContent: FlexAlign.End),符合酒水团购作为快速决策行为的交互习惯。箱数调节器设置了1-50的范围限制,防止了异常数量的提交。已省金额以绿色#2E7D32显示,通过正向激励引导用户增加购买量。底部双按钮设计中,左侧"单瓶购买"使用描边样式作为次要操作,右侧"发起团购 · 免运费"使用红色实心样式作为主要操作,视觉层次清晰。

酒窖存入与陈化估值弹窗

cellarModal方法是应用中最具特色的弹窗,集成了存放位置选择、存放年限选择、瓶数调节和陈化价值估算四大功能。预估陈年后价值通过Math.floor(selPrice() * bottleNum * 1.4)公式计算,1.4的增值系数基于历史增值曲线进行估算。

  @Builder cellarModal() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('存入私人酒窖').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
            Text('恒温 13℃ · 湿度 72% · 免保管费').fontSize(10).fontColor('#5D4037').margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start)

          Text('✕').fontSize(16).fontColor('#9AA0B5')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text(this.selName()).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
          Text(this.selDesc()).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 })
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Column() {
          Text('存放位置').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.cellars, (c: string, i: number) => {
              Text(c).fontSize(11).fontColor(this.selCellar === i ? Color.White : '#5C6280')
                .backgroundColor(this.selCellar === i ? '#5D4037' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selCellar = i
                })
            }, (c: string) => c)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Column() {
          Text('存放年限').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.yearChips, (y: string, i: number) => {
              Text(y).fontSize(11).fontColor(this.selYears === i ? Color.White : '#5C6280')
                .backgroundColor(this.selYears === i ? '#B71C1C' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selYears = i
                })
            }, (y: string) => y)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Row() {
          Text('存酒瓶数').fontSize(12).fontColor('#5C6280')
          Row({ space: 10 }) {
            Text('-').fontSize(15).fontColor('#5D4037').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
              .onClick(() => {
                if (this.bottleNum > 1) {
                  this.bottleNum--
                }
              })
            Text(this.bottleNum.toString() + ' 瓶').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium)
            Text('+').fontSize(15).fontColor('#5D4037').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
              .onClick(() => {
                if (this.bottleNum < 60) {
                  this.bottleNum++
                }
              })
          }
          .alignItems(VerticalAlign.Center)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
        .margin({ top: 16 })

        Column() {
          Text('预估陈年后价值 ¥' + (Math.floor(this.selPrice() * this.bottleNum * 1.4))).fontSize(15).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('按历史增值曲线估算,仅供参考'.slice(0, 14)).fontSize(9).fontColor('#9AA0B5').margin({ top: 4 })
        }
        .width('100%')
        .margin({ top: 14 })

        Column() {
          Text('确认存入').fontSize(14).fontColor(Color.White).fontWeight(FontWeight.Medium)
        }
        .width('100%')
        .height(42)
        .backgroundColor('#5D4037')
        .borderRadius(21)
        .justifyContent(FlexAlign.Center)
        .margin({ top: 14 })
        .onClick(() => {
          this.showCellar = false
        })
      }
      .width('92%')
      .backgroundColor(Color.White)
      .borderRadius(16)
      .padding(16)
      .constraintSize({ maxHeight: '80%' })
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showCellar = false
    })
  }

酒窖弹窗的设计亮点在于两个选择器使用了不同的选中颜色:存放位置选中为棕色#5D4037(呼应酒窖木质材质),存放年限选中为红色#B71C1C(呼应酒水品牌色),通过颜色差异帮助用户区分不同维度的选择。瓶数调节器的范围设置为1-60,60瓶对应5箱的标准整箱存储上限。陈化价值估算使用了Math.floor函数确保结果为整数,1.4的增值系数虽然简化但提供了直观的投资回报参考。确认按钮使用了棕色#5D4037背景和更大的42像素高度,区别于其他弹窗的40像素按钮,强调了酒窖存入操作的庄重感。

主构建函数与弹窗编排

build方法作为应用入口构建函数,通过Stack容器将主界面与五个弹窗叠加。主界面由头部、可滚动内容区和底部导航栏三部分组成,内容区通过if-else条件链渲染七个标签页。

  build() {
    Stack() {
      Column() {
        this.buildHeader()
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.buildTab1()
            } else if (this.currentTab === 1) {
              this.buildTab2()
            } else if (this.currentTab === 2) {
              this.buildTab3()
            } else if (this.currentTab === 3) {
              this.buildTab4()
            } else if (this.currentTab === 4) {
              this.buildTab5()
            } else if (this.currentTab === 5) {
              this.buildTab6()
            } else {
              this.buildTab7()
            }
          }
          .width('100%')
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)
        .width('100%')
        .align(Alignment.Top)

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

      if (this.showPublish) {
        this.publishModal()
      }
      if (this.showGroup) {
        this.groupModal()
      }
      if (this.showCellar) {
        this.cellarModal()
      }
      if (this.showDelete) {
        this.deleteModal()
      }
      if (this.showDetail) {
        this.detailModal()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FAF5F2')
  }

整体背景色使用了#FAF5F2——一种偏暖的米白色,比黄金珠宝应用的#FAF6F0略偏粉,营造出更适合酒水品类的温暖基调。底部导航栏的选中状态颜色为红色#B71C1C,与头部品牌色一致。弹窗的渲染顺序按照业务优先级排列:发布、团购、酒窖、删除、详情,确保多个弹窗状态同时为true时(虽然实际不会发生)的渲染优先级合理。

技术点对比分析

技术维度 实现方式 设计特点 适用场景
状态管理 @State装饰器+多维度状态 团购规格、酒窖位置、存放年限等业务状态 导航、弹窗、业务选择
数据模型 interface接口+num字段复用 num在不同品类中表示度数/容量/年份 啤酒度数、红酒容量、酒窖年份
柱状图 ForEach+slice提取标签 tag.slice(3,8)提取酒精度数值 啤酒度数图、白酒度数图、酒窖年份图
弹窗弹出 justifyContent差异化 居中弹出(发布/删除) vs 底部弹出(团购/酒窖) 五种弹窗的交互定位
估值计算 Math.floor+增值系数 selPricebottleNum1.4估算陈化价值 酒窖存入弹窗
选择器配色 多色选中状态 位置选中棕色、年限选中红色 酒窖弹窗双维度选择
动画效果 scale+animation脉冲 iterations:-1无限循环 抢购按钮、未读角标、窖温数值
温湿度面板 实时数据展示 绿色数值表示正常范围 酒窖管理页面
事件冒泡 stopPropagation 遮罩关闭+内容阻止冒泡 所有弹窗交互
列表渲染 ForEach+keyGenerator 基于商品名称唯一键 商品列表、消息列表

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

interface GoodsItem {
  name: string
  price: number
  desc: string
  score: number
  num: number
  tag: string
  hot: boolean
}

interface TabInfo {
  name: string
  icon: string
}

interface MsgItem {
  title: string
  content: string
  time: string
  read: boolean
}

@Entry
@Component
struct Index {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showGroup: boolean = false
  @State showCellar: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: GoodsItem | null = null
  @State selType: number = 0
  @State selDegree: number = 0
  @State selSpec: number = 0
  @State boxNum: number = 1
  @State selCellar: number = 0
  @State selYears: number = 0
  @State bottleNum: number = 6
  @State deleteIndex: number = -1
  @State msgs: MsgItem[] = [
    { title: '抢购提醒', content: '茅台放量 100 瓶,10:00 准时开抢,先到先得', time: '09:55', read: false },
    { title: '酒窖温控警告', content: '您的 3 号酒柜温度偏高 1.5℃,已自动调节', time: '09:12', read: false },
    { title: '团购成团', content: '您发起的精酿 6 箱团购已成团,差价已返还', time: '昨天', read: false },
    { title: '物流更新', content: '您的红酒已发出,恒温车运输全程温控', time: '昨天', read: true },
    { title: '品鉴会邀约', content: '周六晚勃艮第垂直品鉴,席位仅剩 4 个', time: '前天', read: true },
    { title: '存酒到期', content: '您存的两瓶白酒已满 3 年,可随时提货', time: '前天', read: true },
    { title: '价格保护', content: '您购买的清酒降价 20 元,价保差额已到账', time: '周三', read: true },
    { title: '直播预告', content: '今晚 20:00 酒馆老板娘直播盲品挑战', time: '周三', read: true },
    { title: '券包到期', content: '满 199 减 30 洋酒券将于 3 天后过期', time: '周四', read: true },
    { title: '新品上架', content: '帝国世涛限量 500 箱已上架,每人限购 2 箱', time: '上周', read: true },
    { title: '评价有礼', content: '晒酒柜照片评价可领 25 元精酿券', time: '上周', read: true },
    { title: '温馨提示', content: '未成年人禁止饮酒,孕妇请勿饮酒', time: '上周', read: true }
  ]

  private tabs: TabInfo[] = [
    { name: '酒水集市', icon: '🍻' },
    { name: '精酿啤酒', icon: '🍺' },
    { name: '红酒庄园', icon: '🍷' },
    { name: '白酒陈酿', icon: '🍶' },
    { name: '私人酒窖', icon: '🗝️' },
    { name: '消息', icon: '✉️' },
    { name: '我的', icon: '👤' }
  ]
  private types: string[] = ['精酿啤酒', '红葡萄酒', '白葡萄酒', '威士忌', '清酒', '洋酒']
  private degrees: string[] = ['低度 3-8°', '中度 9-20°', '高度 40°+', '无醇 0°']
  private specs: string[] = ['330ml×24听', '500ml×12瓶', '750ml×6瓶', '1.5L×2瓶']
  private cellars: string[] = ['恒温酒柜A', '地下酒窖B区', '私人藏酒间', '门店寄存柜']
  private yearChips: string[] = ['存 1 年', '存 3 年', '存 5 年', '存 10 年']

  private items: GoodsItem[] = [
    { name: '福佳白啤 330ml×24', price: 108, desc: '比利时风味 香菜籽橘皮', score: 95, num: 5, tag: '销量王', hot: true },
    { name: '青岛经典 500ml×12', price: 66, desc: '麦香浓郁 国民口粮', score: 94, num: 4, tag: '口粮款', hot: true },
    { name: '鹅岛IPA 355ml×6', price: 78, desc: '美式酒花 苦香平衡', score: 93, num: 7, tag: '进阶款', hot: false },
    { name: '迷失海岸幽灵浑浊', price: 128, desc: '浑浊IPA 果汁感强', score: 95, num: 7, tag: '爆款', hot: true },
    { name: '罗斯福10号 330ml', price: 32, desc: '比利时四料 修道院传奇', score: 96, num: 11, tag: '神级', hot: false },
    { name: '智美红帽 330ml×6', price: 168, desc: '修道院认证 深琥珀色', score: 95, num: 9, tag: '经典', hot: false },
    { name: '奔富BIN389 750ml', price: 468, desc: '澳洲西拉赤霞珠', score: 96, num: 14, tag: '宴请款', hot: true },
    { name: '拉菲传奇波尔多', price: 128, desc: '法定产区 级别餐酒', score: 92, num: 13, tag: '入门红酒', hot: false },
    { name: '杰卡斯霞多丽白', price: 98, desc: '南澳白葡萄酒 清爽果香', score: 93, num: 13, tag: '白葡萄酒', hot: false },
    { name: '茅台飞天 53° 500ml', price: 2699, desc: '酱香鼻祖 硬通货', score: 98, num: 53, tag: '限量抢', hot: true },
    { name: '五粮液第八代 52°', price: 1099, desc: '浓香王者 宴席首选', score: 97, num: 52, tag: '宴席', hot: false },
    { name: '汾酒青花20 53°', price: 468, desc: '清香典范 一清到底', score: 96, num: 53, tag: '清香型', hot: false },
    { name: '山崎12年威士忌', price: 1580, desc: '日威标杆 雪莉桶', score: 98, num: 12, tag: '收藏级', hot: true },
    { name: '麦卡伦12双雪莉', price: 688, desc: '单一麦芽 入门收藏', score: 96, num: 12, tag: '单一麦芽', hot: false },
    { name: '獭祭二割三分纯米', price: 358, desc: '精米步合23% 果香四溢', score: 95, num: 23, tag: '清酒', hot: false },
    { name: '芝华士12年 700ml', price: 168, desc: '苏格兰调和 套餐友好', score: 92, num: 12, tag: '调和威士忌', hot: false },
    { name: '百龄坛特酿 700ml', price: 138, desc: '顺滑易饮 调酒基底', score: 91, num: 12, tag: '基酒', hot: false },
    { name: '摩根船长白朗姆', price: 98, desc: '鸡尾酒必备 加勒比风', score: 90, num: 12, tag: '朗姆', hot: false },
    { name: '三得利微醺 350ml', price: 12, desc: '3度低醇 气泡清爽', score: 93, num: 3, tag: '微醺', hot: true },
    { name: '锐澳预调酒 330ml', price: 10, desc: '果味低度 聚会百搭', score: 89, num: 3, tag: '聚会', hot: false },
    { name: '长城桑干酒庄西拉', price: 288, desc: '国产精品酒庄 烟台', score: 92, num: 14, tag: '国产酒庄', hot: false },
    { name: '张裕解百纳特选级', price: 128, desc: '百年张裕 窖藏橡木', score: 91, num: 14, tag: '老字号', hot: false },
    { name: '健力士黑啤 440ml', price: 15, desc: '爱尔兰世涛 绵密泡沫', score: 92, num: 4, tag: '世涛', hot: false },
    { name: '保拉纳小麦啤 500ml', price: 16, desc: '德式小麦 香蕉酯香', score: 93, num: 5, tag: '德式', hot: false }
  ]
  private beers: GoodsItem[] = [
    { name: '迷失海岸幽灵浑浊IPA', price: 128, desc: '果香炸弹 苦度适中', score: 95, num: 7, tag: '酒精度7%', hot: true },
    { name: '罗斯福10号四料', price: 32, desc: '焦糖深色 修道院啤酒', score: 96, num: 11, tag: '酒精度11.3%', hot: false },
    { name: '鹅岛GOIPA', price: 78, desc: '西海岸风格 经典苦香', score: 93, num: 6, tag: '酒精度6%', hot: false },
    { name: '保拉纳小麦啤', price: 16, desc: '巴伐利亚 绵柔麦香', score: 93, num: 5, tag: '酒精度5.5%', hot: true },
    { name: '健力士世涛', price: 15, desc: '氮气泡沫 咖啡烘焙', score: 92, num: 4, tag: '酒精度4.2%', hot: false },
    { name: '角鲨头60min', price: 25, desc: '连续投放酒花 60分钟', score: 94, num: 6, tag: '酒精度6%', hot: false },
    { name: '酿酒狗朋克IPA', price: 18, desc: '苏格兰传奇 叛逆之选', score: 93, num: 5, tag: '酒精度5.6%', hot: false },
    { name: '巨石混沌毁灭IPA', price: 22, desc: '重酒花 苦度爱好者', score: 92, num: 8, tag: '酒精度8.5%', hot: false },
    { name: '帝国世涛限量款', price: 68, desc: '过波本桶 香草咖啡', score: 96, num: 12, tag: '酒精度13%', hot: true },
    { name: '兰比克樱桃酸啤', price: 45, desc: '比利时酸 野生发酵', score: 91, num: 5, tag: '酒精度5.2%', hot: false },
    { name: '柏林酸小麦', price: 28, desc: '清爽酸感 夏日救星', score: 90, num: 4, tag: '酒精度3.8%', hot: false },
    { name: '皮尔森经典黄啤', price: 12, desc: '捷克原型 清脆干爽', score: 91, num: 4, tag: '酒精度4.4%', hot: false }
  ]
  private wines: GoodsItem[] = [
    { name: '奔富BIN389', price: 468, desc: '南澳混酿 橡木桶18月', score: 96, num: 14, tag: '干红·重酒体', hot: true },
    { name: '拉菲传奇', price: 128, desc: '波尔多AOC 餐酒首选', score: 92, num: 13, tag: '干红·中等', hot: false },
    { name: '杰卡斯霞多丽', price: 98, desc: '冷凉产区 柑橘矿物', score: 93, num: 13, tag: '干白·轻盈', hot: false },
    { name: '桑干酒庄西拉', price: 288, desc: '中国风土 贺兰山东麓', score: 92, num: 14, tag: '干红·重酒体', hot: false },
    { name: '德国雷司令半甜', price: 138, desc: '摩泽尔 青苹果甜润', score: 94, num: 10, tag: '半甜白', hot: true },
    { name: '普罗旺斯桃红', price: 158, desc: '南法颜值 清新莓果', score: 91, num: 12, tag: '桃红·轻盈', hot: false },
    { name: '香槟凯歌黄牌', price: 328, desc: '经典香槟 庆祝必备', score: 95, num: 12, tag: '起泡酒', hot: false },
    { name: '莫斯卡托甜白', price: 88, desc: '意大利小甜水 微气泡', score: 92, num: 5, tag: '甜白起泡', hot: false },
    { name: '黑皮诺勃艮第级', price: 398, desc: '优雅细腻 红果香气', score: 95, num: 13, tag: '干红·优雅', hot: false },
    { name: '赤霞珠纳帕谷', price: 568, desc: '美国经典 力量感强', score: 95, num: 14, tag: '干红·重酒体', hot: false }
  ]
  private spirits: GoodsItem[] = [
    { name: '茅台飞天53°', price: 2699, desc: '酱香巅峰 一瓶难求', score: 98, num: 53, tag: '酱香型', hot: true },
    { name: '五粮液第八代', price: 1099, desc: '浓香旗舰 五粮配方', score: 97, num: 52, tag: '浓香型', hot: false },
    { name: '汾酒青花20', price: 468, desc: '清香优雅 地缸发酵', score: 96, num: 53, tag: '清香型', hot: false },
    { name: '泸州老窖1573', price: 1180, desc: '浓香鼻祖 单粮酿造', score: 96, num: 52, tag: '浓香型', hot: false },
    { name: '洋河梦之蓝M3', price: 598, desc: '绵柔型 白酒新贵', score: 94, num: 52, tag: '绵柔型', hot: false },
    { name: '剑南春水晶剑', price: 419, desc: '性价比之王 宴请', score: 95, num: 52, tag: '浓香型', hot: true },
    { name: '郎酒青花郎', price: 978, desc: '青花郎 赤水河左岸', score: 95, num: 53, tag: '酱香型', hot: false },
    { name: '山崎12年', price: 1580, desc: '日威标杆 雪莉桶', score: 98, num: 12, tag: '威士忌', hot: true },
    { name: '麦卡伦12双桶', price: 688, desc: '双雪莉桶 顺滑蜂蜜', score: 96, num: 12, tag: '威士忌', hot: false },
    { name: '獭祭二割三分', price: 358, desc: '纯米大吟酿 果香', score: 95, num: 23, tag: '清酒', hot: false }
  ]
  private cellarGoods: GoodsItem[] = [
    { name: '勃艮第一级园黑皮诺', price: 1280, desc: '2019年份 陈年潜力', score: 97, num: 3, tag: '私藏中', hot: true },
    { name: '茅台羊年生肖', price: 3680, desc: '收藏级 已升值40%', score: 98, num: 5, tag: '私藏中', hot: false },
    { name: '山崎18年雪莉桶', price: 3280, desc: '日威珍品 市面难寻', score: 98, num: 8, tag: '私藏中', hot: false },
    { name: '奔富葛兰许', price: 2380, desc: '澳洲酒王 陈年20年', score: 97, num: 10, tag: '私藏中', hot: false },
    { name: '五粮液老版萝卜瓶', price: 1980, desc: '90年代老酒 存放佳', score: 96, num: 15, tag: '私藏中', hot: false },
    { name: '波尔多二级庄', price: 880, desc: '2016好年份 适饮期', score: 95, num: 6, tag: '私藏中', hot: true },
    { name: '限量帝国世涛套装', price: 666, desc: '过桶系列 6瓶装', score: 94, num: 12, tag: '私藏中', hot: false },
    { name: '香槟沙龙', price: 2888, desc: '白中白 年份香槟', score: 97, num: 12, tag: '私藏中', hot: false },
    { name: '老版汾酒玻瓶', price: 599, desc: '80年代 老酒回收', score: 93, num: 20, tag: '私藏中', hot: false },
    { name: '意大利巴罗洛', price: 768, desc: '雾酒之王 陈年', score: 95, num: 14, tag: '私藏中', hot: false }
  ]

  selName(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.name
  }
  selDesc(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.desc
  }
  selTag(): string {
    if (this.selectedItem === null) {
      return ''
    }
    return this.selectedItem.tag
  }
  selPrice(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.price
  }
  selScore(): number {
    if (this.selectedItem === null) {
      return 0
    }
    return this.selectedItem.score
  }
  maxOfNum(list: GoodsItem[]): number {
    let m: number = 0
    for (let i = 0; i < list.length; i++) {
      if (list[i].num > m) {
        m = list[i].num
      }
    }
    return m
  }
  unreadCount(): number {
    let c: number = 0
    for (let i = 0; i < this.msgs.length; i++) {
      if (!this.msgs[i].read) {
        c++
      }
    }
    return c
  }

  @Builder buildHeader() {
    Column() {
      Row() {
        Column() {
          Row() {
            Text('🚚').fontSize(12)
            Text('最快 30 分钟达 · 全程恒温配送').fontSize(10).fontColor('rgba(255,255,255,0.8)').margin({ left: 4 })
          }
          .alignItems(VerticalAlign.Center)
          Text('京东酒水馆').fontSize(19).fontColor(Color.White).fontWeight(FontWeight.Bold).margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text('🛒').fontSize(20)
          Text('团购').fontSize(9).fontColor('rgba(255,255,255,0.8)').margin({ top: 2 })
        }
        .width(48)
        .onClick(() => {
          this.showGroup = true
        })

        Column() {
          Text('✉️').fontSize(20)
          if (this.unreadCount() > 0) {
            Text(this.unreadCount().toString())
              .fontSize(8)
              .fontColor(Color.White)
              .backgroundColor('#FF3B30')
              .borderRadius(7)
              .padding({ left: 4, right: 4 })
              .position({ x: 12, y: -4 })
              .scale({ x: 1.12, y: 1.12 })
              .animation({ duration: 650, iterations: -1, curve: Curve.EaseInOut })
          }
        }
        .width(48)
        .onClick(() => {
          this.currentTab = 5
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 12 })
      .alignItems(VerticalAlign.Center)

      Row() {
        Text('年货酒水节 · 满 299 减 60').fontSize(11).fontColor('#FFD3C2')
        Text('抢茅台 ›').fontSize(11).fontColor('#FF8A65').margin({ left: 10 })
          .scale({ x: 1.05, y: 1.05 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
      }
      .width('100%')
      .padding({ left: 16, right: 16, bottom: 12 })
      .justifyContent(FlexAlign.Start)
      .alignItems(VerticalAlign.Center)
    }
    .width('100%')
    .backgroundColor('#B71C1C')
  }

  @Builder buildTab1() {
    Column() {
      Row() {
        Column() {
          Text('限时闪购').fontSize(16).fontColor(Color.White).fontWeight(FontWeight.Bold)
          Text('整点放量 · 手慢无').fontSize(10).fontColor('rgba(255,255,255,0.75)').margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text('茅台飞天 53°').fontSize(12).fontColor('#FFD54F').fontWeight(FontWeight.Bold)
          Text('¥2699 限量100瓶').fontSize(10).fontColor('rgba(255,255,255,0.85)').margin({ top: 4 })
          Column() {
            Text('马上抢').fontSize(11).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
          }
          .backgroundColor('#FFD54F')
          .borderRadius(13)
          .padding({ left: 14, right: 14, top: 5, bottom: 5 })
          .margin({ top: 8 })
          .scale({ x: 1.06, y: 1.06 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          .onClick(() => {
            this.boxNum = 1
            this.showGroup = true
          })
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')
      .backgroundColor('#7F1D1D')
      .borderRadius(14)
      .padding(16)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(VerticalAlign.Top)

      Row() {
        Text('酒水精选').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        Text('正品溯源 · 破损包赔').fontSize(10).fontColor('#9AA0B5').margin({ left: 8 })
      }
      .width('100%')
      .padding({ left: 16, top: 14, bottom: 8 })
      .justifyContent(FlexAlign.Start)
      .alignItems(VerticalAlign.Center)

      ForEach(this.items, (g: GoodsItem, i: number) => {
        Row() {
          Column() {
            Text(i % 4 === 0 ? '🍺' : (i % 4 === 1 ? '🍷' : (i % 4 === 2 ? '🍶' : '🥃'))).fontSize(26)
          }
          .width(72)
          .height(72)
          .borderRadius(12)
          .backgroundColor(i % 2 === 0 ? '#FDEBE7' : '#FBE9E7')
          .justifyContent(FlexAlign.Center)

          Column() {
            Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
            Text(g.desc).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)

            Row() {
              Text(g.tag).fontSize(9).fontColor('#B71C1C').backgroundColor('#FDEBE7').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 })
              if (g.hot) {
                Text('秒杀中').fontSize(9).fontColor(Color.White).backgroundColor('#FF3B30').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
                  .scale({ x: 1.06, y: 1.06 })
                  .animation({ duration: 650, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .margin({ top: 5 })
            .alignItems(VerticalAlign.Center)

            Stack({ alignContent: Alignment.Start }) {
              Column().width('100%').height(6).backgroundColor('#F0F0F5').borderRadius(3)
              Column().width(g.score + '%').height(6).backgroundColor('#B71C1C').borderRadius(3)
            }
            .width('100%')
            .margin({ top: 6 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 10 })

          Column() {
            Text('¥' + g.price).fontSize(16).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
            Column() {
              Text('整箱团购').fontSize(10).fontColor(Color.White)
            }
            .backgroundColor('#B71C1C')
            .borderRadius(13)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .margin({ top: 8 })
            .onClick(() => {
              this.selectedItem = g
              this.boxNum = 1
              this.showGroup = true
            })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding(12)
        .margin({ left: 12, right: 12, bottom: 10 })
        .alignItems(VerticalAlign.Center)
        .onClick(() => {
          this.selectedItem = g
          this.showDetail = true
        })
      }, (g: GoodsItem) => g.name)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildTab2() {
    Column() {
      Text('精酿啤酒 · 度数柱状图').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 14, bottom: 8 })

      Column() {
        Row({ space: 10 }) {
          ForEach(this.beers, (g: GoodsItem) => {
            Column() {
              Column()
                .width(16)
                .height(g.num / this.maxOfNum(this.beers) * 100)
                .backgroundColor('#FF7043')
                .borderRadius(4)
              Text(g.tag.slice(3, 8)).fontSize(7).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('口碑榜 TOP 双列').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
        ForEach(this.beers, (g: GoodsItem, i: number) => {
          Column() {
            Row() {
              Column() {
                Text('🍺').fontSize(24)
              }
              .width(46)
              .height(46)
              .borderRadius(23)
              .backgroundColor(i % 2 === 0 ? '#FFF3E0' : '#FBE9E7')
              .justifyContent(FlexAlign.Center)

              Column() {
                Text(g.tag).fontSize(9).fontColor('#BF360C').fontWeight(FontWeight.Bold)
                Text('好评 ' + g.score + '%').fontSize(9).fontColor('#8A8FA3').margin({ top: 3 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.End)
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)

            Text(g.name).fontSize(12).fontColor('#212121').fontWeight(FontWeight.Medium).margin({ top: 8 }).maxLines(1)
            Text(g.desc).fontSize(9).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)

            Row() {
              Text('¥' + g.price).fontSize(14).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
              if (g.hot) {
                Text('酒头推荐').fontSize(9).fontColor(Color.White).backgroundColor('#FF3B30').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
                  .scale({ x: 1.08, y: 1.08 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .width('100%')
            .margin({ top: 6 })
            .justifyContent(FlexAlign.SpaceBetween)
            .alignItems(VerticalAlign.Center)
          }
          .width('48%')
          .backgroundColor(Color.White)
          .borderRadius(12)
          .padding(12)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.selectedItem = g
            this.showDetail = true
          })
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildTab3() {
    Column() {
      Column() {
        Text('红酒庄园 · 舌尖上的风土').fontSize(16).fontColor(Color.White).fontWeight(FontWeight.Bold)
        Text('评分进度一览(WS/帕克评分折算)').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 4 })

        Column() {
          ForEach(this.wines, (g: GoodsItem, i: number) => {
            Row() {
              Text((i + 1).toString()).fontSize(11).fontColor(Color.White).backgroundColor(i < 3 ? '#FFD54F' : 'rgba(255,255,255,0.3)').borderRadius(10).width(18).height(18).textAlign(TextAlign.Center)
              Text(g.name).fontSize(11).fontColor(Color.White).width(96).maxLines(1).margin({ left: 8 })
              Stack({ alignContent: Alignment.Start }) {
                Column().width('100%').height(7).backgroundColor('rgba(255,255,255,0.2)').borderRadius(4)
                Column().width(g.score + '%').height(7).backgroundColor('#FF8A65').borderRadius(4)
              }
              .layoutWeight(1)
              .margin({ left: 8, right: 8 })
              Text(g.score.toString()).fontSize(11).fontColor('#FFD54F').fontWeight(FontWeight.Bold)
            }
            .width('100%')
            .alignItems(VerticalAlign.Center)
            .margin({ bottom: 10 })
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .margin({ top: 14 })
      }
      .width('100%')
      .backgroundColor('#5D1010')
      .borderRadius(14)
      .padding(16)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('酒体容量柱状图(cl)').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      Column() {
        Row({ space: 12 }) {
          ForEach(this.wines, (g: GoodsItem) => {
            Column() {
              Column()
                .width(18)
                .height(g.num / this.maxOfNum(this.wines) * 90)
                .backgroundColor('#8D6E63')
                .borderRadius(4)
              Text(g.num.toString()).fontSize(8).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('整箱购 · 横向速览').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      Scroll() {
        Row({ space: 10 }) {
          ForEach(this.wines, (g: GoodsItem, i: number) => {
            Column() {
              Column() {
                Text('🍷').fontSize(30)
              }
              .width(84)
              .height(84)
              .borderRadius(12)
              .backgroundColor(i % 2 === 0 ? '#FBE9E7' : '#FFF3E0')
              .justifyContent(FlexAlign.Center)

              Text(g.name).fontSize(11).fontColor('#212121').margin({ top: 8 }).maxLines(1)
              Text(g.tag).fontSize(9).fontColor('#8D6E63').margin({ top: 3 })
              Text('¥' + g.price).fontSize(14).fontColor('#B71C1C').fontWeight(FontWeight.Bold).margin({ top: 4 })
            }
            .width(120)
            .backgroundColor(Color.White)
            .borderRadius(12)
            .padding(10)
            .onClick(() => {
              this.selectedItem = g
              this.showDetail = true
            })
          }, (g: GoodsItem) => g.name)
        }
        .padding({ left: 12, right: 12 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildTab4() {
    Column() {
      Text('白酒陈酿 · 度数柱状图').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 14, bottom: 8 })

      Column() {
        Row({ space: 10 }) {
          ForEach(this.spirits, (g: GoodsItem) => {
            Column() {
              Column()
                .width(16)
                .height(g.num / this.maxOfNum(this.spirits) * 100)
                .backgroundColor('#3E2723')
                .borderRadius(4)
              Text(g.num.toString() + '°').fontSize(8).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('香型分类精选').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      Column() {
        ForEach(this.spirits, (g: GoodsItem, i: number) => {
          Row() {
            Column()
              .width(4)
              .height(48)
              .backgroundColor(i % 4 === 0 ? '#B71C1C' : (i % 4 === 1 ? '#3E2723' : (i % 4 === 2 ? '#BF360C' : '#8D6E63')))
              .borderRadius(2)

            Column() {
              Row() {
                Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
                Text(g.tag).fontSize(9).fontColor('#5D4037').backgroundColor('#EFEBE9').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
                if (g.hot) {
                  Text('回购王').fontSize(9).fontColor(Color.White).backgroundColor('#FF3B30').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
                    .scale({ x: 1.08, y: 1.08 })
                    .animation({ duration: 650, iterations: -1, curve: Curve.EaseInOut })
                }
              }
              .alignItems(VerticalAlign.Center)

              Text(g.desc).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
              Text('好评 ' + g.score + '% · 度数 ' + g.num + '°').fontSize(9).fontColor('#9AA0B5').margin({ top: 3 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .margin({ left: 10 })

            Column() {
              Text('¥' + g.price).fontSize(15).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
              Column() {
                Text('存入酒窖').fontSize(10).fontColor(Color.White)
              }
              .backgroundColor('#5D4037')
              .borderRadius(13)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .margin({ top: 6 })
              .onClick(() => {
                this.selectedItem = g
                this.bottleNum = 6
                this.showCellar = true
              })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .backgroundColor(Color.White)
          .borderRadius(12)
          .padding(12)
          .margin({ bottom: 10 })
          .alignItems(VerticalAlign.Center)
          .onClick(() => {
            this.selectedItem = g
            this.showDetail = true
          })
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildTab5() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('私人酒窖').fontSize(17).fontColor('#FFD54F').fontWeight(FontWeight.Bold)
            Text('恒温恒湿 · 每日巡检 · 免费保险').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)

          Text('🗝️').fontSize(32)
            .scale({ x: 1.08, y: 1.08 })
            .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        Row() {
          Column() {
            Text('38').fontSize(22).fontColor('#FFD54F').fontWeight(FontWeight.Bold)
            Text('在窖藏酒').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('13℃').fontSize(22).fontColor('#81C784').fontWeight(FontWeight.Bold).margin({ top: 2 })
              .scale({ x: 1.04, y: 1.04 })
              .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
            Text('实时窖温').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('72%').fontSize(22).fontColor('#81C784').fontWeight(FontWeight.Bold).margin({ top: 2 })
            Text('实时湿度').fontSize(10).fontColor('rgba(255,255,255,0.7)').margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(VerticalAlign.Top)
      }
      .width('100%')
      .backgroundColor('#3E2723')
      .borderRadius(14)
      .padding(18)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(HorizontalAlign.Start)

      Column() {
        Text('藏酒陈化年份柱状图(年)').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Bold)
        Row({ space: 12 }) {
          ForEach(this.cellarGoods, (g: GoodsItem) => {
            Column() {
              Column()
                .width(18)
                .height(g.num / this.maxOfNum(this.cellarGoods) * 90)
                .backgroundColor('#B71C1C')
                .borderRadius(4)
              Text(g.num.toString() + '年').fontSize(8).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(HorizontalAlign.Start)

      Text('窖藏清单').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      ForEach(this.cellarGoods, (g: GoodsItem, i: number) => {
        Row() {
          Column() {
            Text('🍷').fontSize(24)
          }
          .width(54)
          .height(54)
          .borderRadius(12)
          .backgroundColor(i % 2 === 0 ? '#EFEBE9' : '#FBE9E7')
          .justifyContent(FlexAlign.Center)

          Column() {
            Row() {
              Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
              Text(g.tag).fontSize(9).fontColor('#B7791F').backgroundColor('#FFF8E1').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
              if (g.hot) {
                Text('增值中').fontSize(9).fontColor(Color.White).backgroundColor('#2E7D32').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
                  .scale({ x: 1.08, y: 1.08 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .alignItems(VerticalAlign.Center)

            Text(g.desc).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
            Text('已陈化 ' + g.num + ' 年 · 估值涨至 ¥' + g.price).fontSize(9).fontColor('#9AA0B5').margin({ top: 3 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 10 })

          Column() {
            Column() {
              Text('存入新酒').fontSize(10).fontColor(Color.White)
            }
            .backgroundColor('#B71C1C')
            .borderRadius(13)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .onClick(() => {
              this.selectedItem = g
              this.bottleNum = 6
              this.showCellar = true
            })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding(12)
        .margin({ left: 12, right: 12, bottom: 10 })
        .alignItems(VerticalAlign.Center)
      }, (g: GoodsItem) => g.name)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildTab6() {
    Column() {
      Row() {
        Text('消息中心').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
        Text('未读 ' + this.unreadCount() + ' 条').fontSize(11).fontColor('#FF3B30').margin({ left: 8 })
          .scale({ x: 1.05, y: 1.05 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 14, bottom: 10 })
      .justifyContent(FlexAlign.Start)
      .alignItems(VerticalAlign.Center)

      ForEach(this.msgs, (m: MsgItem, i: number) => {
        Row() {
          Column() {
            Text(m.read ? '📭' : '🍷').fontSize(20)
            if (!m.read) {
              Column()
                .width(8)
                .height(8)
                .borderRadius(4)
                .backgroundColor('#FF3B30')
                .position({ x: 30, y: -2 })
                .scale({ x: 1.2, y: 1.2 })
                .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
            }
          }
          .width(44)

          Column() {
            Row() {
              Text(m.title).fontSize(13).fontColor(m.read ? '#5C6280' : '#212121').fontWeight(m.read ? FontWeight.Normal : FontWeight.Bold).maxLines(1)
              Text(m.time).fontSize(9).fontColor('#9AA0B5').margin({ left: 6 })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)
            .alignItems(VerticalAlign.Center)

            Text(m.content).fontSize(11).fontColor('#8A8FA3').margin({ top: 4 }).maxLines(2)
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          .margin({ left: 8 })

          Column() {
            Text('删除').fontSize(10).fontColor('#9AA0B5')
          }
          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
          .border({ width: 1, color: '#E5E5EC' })
          .borderRadius(10)
          .onClick(() => {
            this.selectedItem = null
            this.deleteIndex = i
            this.showDelete = true
          })
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding(12)
        .margin({ left: 12, right: 12, bottom: 10 })
        .alignItems(VerticalAlign.Center)
      }, (m: MsgItem) => m.title + m.time)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildTab7() {
    Column() {
      Row() {
        Column() {
          Text('🍶').fontSize(34)
        }
        .width(64)
        .height(64)
        .borderRadius(32)
        .backgroundColor('#EFEBE9')
        .justifyContent(FlexAlign.Center)

        Column() {
          Text('微醺收藏家').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
          Row() {
            Text('酒瘾中级').fontSize(9).fontColor('#B71C1C').backgroundColor('#FDEBE7').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 })
            Text('品鉴 86 款').fontSize(10).fontColor('#9AA0B5').margin({ left: 8 })
          }
          .margin({ top: 6 })
          .alignItems(VerticalAlign.Center)
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
        .margin({ left: 12 })

        Text('›').fontSize(20).fontColor('#C5C9D9')
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(14)
      .padding(16)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(VerticalAlign.Center)

      Row() {
        Column() {
          Text('6').fontSize(18).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
          Text('团购中').fontSize(10).fontColor('#8A8FA3').margin({ top: 3 })
        }
        .layoutWeight(1)

        Column() {
          Text('38').fontSize(18).fontColor('#5D4037').fontWeight(FontWeight.Bold)
          Text('酒窖藏酒').fontSize(10).fontColor('#8A8FA3').margin({ top: 3 })
        }
        .layoutWeight(1)

        Column() {
          Text('21').fontSize(18).fontColor('#BF360C').fontWeight(FontWeight.Bold)
          Text('收藏酒款').fontSize(10).fontColor('#8A8FA3').margin({ top: 3 })
        }
        .layoutWeight(1)

        Column() {
          Text('4').fontSize(18).fontColor('#2E7D32').fontWeight(FontWeight.Bold)
          Text('优惠券').fontSize(10).fontColor('#8A8FA3').margin({ top: 3 })
            .scale({ x: 1.08, y: 1.08 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        }
        .layoutWeight(1)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(14)
      .padding({ top: 14, bottom: 14 })
      .margin({ left: 12, right: 12, top: 12 })

      Column() {
        Row() {
          Text('🧾').fontSize(18)
          Text('我的订单').fontSize(13).fontColor('#212121').margin({ left: 10 })
          Text('全部 33 ›'.slice(0, 9)).fontSize(11).fontColor('#9AA0B5')
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Row() {
          Text('🗝️').fontSize(18)
          Text('酒窖管理').fontSize(13).fontColor('#212121').margin({ left: 10 })
          Text('38 瓶在窖 ›').fontSize(11).fontColor('#9AA0B5')
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Row() {
          Text('🎯').fontSize(18)
          Text('我的品鉴笔记').fontSize(13).fontColor('#212121').margin({ left: 10 })
          Text('共 86 篇 ›').fontSize(11).fontColor('#9AA0B5')
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Row() {
          Text('🎟️').fontSize(18)
          Text('品鉴会门票').fontSize(13).fontColor('#212121').margin({ left: 10 })
          Text('周六场待使用 ›').fontSize(11).fontColor('#B71C1C')
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Row() {
          Text('⚙️').fontSize(18)
          Text('设置').fontSize(13).fontColor('#212121').margin({ left: 10 })
          Text('›').fontSize(14).fontColor('#C5C9D9')
        }
        .width('100%')
        .padding({ top: 12, bottom: 12 })
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(14)
      .padding({ left: 16, right: 16 })
      .margin({ left: 12, right: 12, top: 12 })

      Column() {
        Text('闲置好酒转让给懂酒的人').fontSize(12).fontColor('#8A8FA3')
        Column() {
          Text('发布闲置酒水').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Medium)
        }
        .width(180)
        .height(40)
        .backgroundColor('#B71C1C')
        .borderRadius(20)
        .justifyContent(FlexAlign.Center)
        .margin({ top: 12 })
        .scale({ x: 1.04, y: 1.04 })
        .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
        .onClick(() => {
          this.selType = 0
          this.selDegree = 0
          this.showPublish = true
        })
      }
      .width('100%')
      .padding({ top: 20, bottom: 20 })
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildBottom() {
    Row() {
      ForEach(this.tabs, (t: TabInfo, i: number) => {
        Column() {
          Text(t.icon).fontSize(18)
          Text(t.name).fontSize(9).fontColor(this.currentTab === i ? '#B71C1C' : '#9AA0B5').margin({ top: 2 })
        }
        .layoutWeight(1)
        .padding({ top: 6, bottom: 6 })
        .justifyContent(FlexAlign.Center)
        .onClick(() => {
          this.currentTab = i
        })
      }, (t: TabInfo) => t.name)
    }
    .width('100%')
    .backgroundColor(Color.White)
    .border({ width: 1, color: '#EDEDF2' })
  }

  @Builder publishModal() {
    Column() {
      Column() {
        Row() {
          Text('发布闲置酒水').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
          Text('✕').fontSize(16).fontColor('#9AA0B5')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text('酒品描述').fontSize(12).fontColor('#5C6280')
          TextInput({ placeholder: '如:山崎12年 未开封 带原盒' })
            .height(40)
            .backgroundColor('#F5F6FA')
            .borderRadius(8)
            .margin({ top: 6 })
            .fontSize(12)
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 16 })

        Column() {
          Text('酒品类型').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.types, (t: string, i: number) => {
              Text(t).fontSize(11).fontColor(this.selType === i ? Color.White : '#5C6280')
                .backgroundColor(this.selType === i ? '#B71C1C' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selType = i
                })
            }, (t: string) => t)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Column() {
          Text('度数区间').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.degrees, (d: string, i: number) => {
              Text(d).fontSize(11).fontColor(this.selDegree === i ? Color.White : '#5C6280')
                .backgroundColor(this.selDegree === i ? '#5D4037' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selDegree = i
                })
            }, (d: string) => d)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Row() {
          Column() {
            Text('暂不发布').fontSize(13).fontColor('#5C6280')
          }
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#F5F6FA')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.showPublish = false
          })

          Column() {
            Text('确认发布').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Medium)
          }
          .layoutWeight(1.6)
          .height(40)
          .backgroundColor('#B71C1C')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .scale({ x: 1.02, y: 1.02 })
          .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
          .onClick(() => {
            this.showPublish = false
          })
        }
        .width('100%')
        .margin({ top: 20 })
        .alignItems(VerticalAlign.Center)
      }
      .width('92%')
      .backgroundColor(Color.White)
      .borderRadius(16)
      .padding(16)
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showPublish = false
    })
  }

  @Builder groupModal() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('整箱团购').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
            Text('拼团立减 · 好友帮砍价').fontSize(10).fontColor('#B71C1C').margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start)

          Text('✕').fontSize(16).fontColor('#9AA0B5')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text(this.selName()).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
          Text(this.selDesc()).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 })
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Column() {
          Text('团购规格').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.specs, (s: string, i: number) => {
              Text(s).fontSize(11).fontColor(this.selSpec === i ? Color.White : '#5C6280')
                .backgroundColor(this.selSpec === i ? '#B71C1C' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selSpec = i
                })
            }, (s: string) => s)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Row() {
          Column() {
            Text('团购箱数').fontSize(11).fontColor('#5C6280')
            Row() {
              Text('-').fontSize(15).fontColor('#B71C1C').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
                .onClick(() => {
                  if (this.boxNum > 1) {
                    this.boxNum--
                  }
                })
              Text(this.boxNum.toString() + ' 箱').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).width(60).textAlign(TextAlign.Center)
              Text('+').fontSize(15).fontColor('#B71C1C').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
                .onClick(() => {
                  if (this.boxNum < 50) {
                    this.boxNum++
                  }
                })
            }
            .margin({ top: 6 })
            .alignItems(VerticalAlign.Center)
          }
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('团购总价').fontSize(11).fontColor('#5C6280')
            Text('¥' + (this.selPrice() * this.boxNum)).fontSize(22).fontColor('#B71C1C').fontWeight(FontWeight.Bold).margin({ top: 6 })
              .scale({ x: 1.05, y: 1.05 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
            Text('已省 ¥' + this.boxNum * 12).fontSize(9).fontColor('#2E7D32').margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .margin({ top: 16 })
        .alignItems(VerticalAlign.Top)

        Row() {
          Column() {
            Text('单瓶购买').fontSize(11).fontColor('#8A8FA3')
          }
          .padding({ left: 10, right: 10, top: 5, bottom: 5 })
          .border({ width: 1, color: '#E5E5EC' })
          .borderRadius(12)
          .onClick(() => {
            this.showGroup = false
          })

          Column() {
            Text('发起团购 · 免运费').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Medium)
          }
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#B71C1C')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .onClick(() => {
            this.showGroup = false
          })
        }
        .width('100%')
        .margin({ top: 20 })
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(16)
      .padding(16)
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.End)
    .onClick(() => {
      this.showGroup = false
    })
  }

  @Builder cellarModal() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('存入私人酒窖').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
            Text('恒温 13℃ · 湿度 72% · 免保管费').fontSize(10).fontColor('#5D4037').margin({ top: 3 })
          }
          .alignItems(HorizontalAlign.Start)

          Text('✕').fontSize(16).fontColor('#9AA0B5')
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)

        Column() {
          Text(this.selName()).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
          Text(this.selDesc()).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 })
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Column() {
          Text('存放位置').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.cellars, (c: string, i: number) => {
              Text(c).fontSize(11).fontColor(this.selCellar === i ? Color.White : '#5C6280')
                .backgroundColor(this.selCellar === i ? '#5D4037' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selCellar = i
                })
            }, (c: string) => c)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Column() {
          Text('存放年限').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.yearChips, (y: string, i: number) => {
              Text(y).fontSize(11).fontColor(this.selYears === i ? Color.White : '#5C6280')
                .backgroundColor(this.selYears === i ? '#B71C1C' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selYears = i
                })
            }, (y: string) => y)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Row() {
          Text('存酒瓶数').fontSize(12).fontColor('#5C6280')
          Row({ space: 10 }) {
            Text('-').fontSize(15).fontColor('#5D4037').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
              .onClick(() => {
                if (this.bottleNum > 1) {
                  this.bottleNum--
                }
              })
            Text(this.bottleNum.toString() + ' 瓶').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium)
            Text('+').fontSize(15).fontColor('#5D4037').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
              .onClick(() => {
                if (this.bottleNum < 60) {
                  this.bottleNum++
                }
              })
          }
          .alignItems(VerticalAlign.Center)
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .alignItems(VerticalAlign.Center)
        .margin({ top: 16 })

        Column() {
          Text('预估陈年后价值 ¥' + (Math.floor(this.selPrice() * this.bottleNum * 1.4))).fontSize(15).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('按历史增值曲线估算,仅供参考'.slice(0, 14)).fontSize(9).fontColor('#9AA0B5').margin({ top: 4 })
        }
        .width('100%')
        .margin({ top: 14 })

        Column() {
          Text('确认存入').fontSize(14).fontColor(Color.White).fontWeight(FontWeight.Medium)
        }
        .width('100%')
        .height(42)
        .backgroundColor('#5D4037')
        .borderRadius(21)
        .justifyContent(FlexAlign.Center)
        .margin({ top: 14 })
        .onClick(() => {
          this.showCellar = false
        })
      }
      .width('92%')
      .backgroundColor(Color.White)
      .borderRadius(16)
      .padding(16)
      .constraintSize({ maxHeight: '80%' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showCellar = false
    })
  }

  @Builder deleteModal() {
    Column() {
      Column() {
        Text('🗑️').fontSize(38)
          .scale({ x: 1.12, y: 1.12 })
          .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
        Text('确认删除这条消息?').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold).margin({ top: 12 })
        Text('删除后该通知将不可恢复'.slice(0, 13)).fontSize(11).fontColor('#9AA0B5').margin({ top: 6 })

        Row() {
          Column() {
            Text('再想想').fontSize(13).fontColor('#5C6280')
          }
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#F5F6FA')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.showDelete = false
          })

          Column() {
            Text('确认删除').fontSize(13).fontColor(Color.White)
          }
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#B71C1C')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .onClick(() => {
            if (this.deleteIndex >= 0 && this.deleteIndex < this.msgs.length) {
              this.msgs.splice(this.deleteIndex, 1)
            }
            this.deleteIndex = -1
            this.showDelete = false
          })
        }
        .width('100%')
        .margin({ top: 20 })
        .alignItems(VerticalAlign.Center)
      }
      .width('76%')
      .backgroundColor(Color.White)
      .borderRadius(16)
      .padding(20)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showDelete = false
    })
  }

  @Builder detailModal() {
    Column() {
      Column() {
        Column() {
          Column() {
            Text('🍷').fontSize(40)
          }
          .width(96)
          .height(96)
          .borderRadius(16)
          .backgroundColor('#FBE9E7')
          .justifyContent(FlexAlign.Center)

          Text(this.selName()).fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold).margin({ top: 12 }).maxLines(2)
          Text(this.selDesc()).fontSize(11).fontColor('#8A8FA3').margin({ top: 6 }).maxLines(2)
          Row() {
            Text(this.selTag()).fontSize(10).fontColor('#B71C1C').backgroundColor('#FDEBE7').borderRadius(4).padding({ left: 6, right: 6, top: 3, bottom: 3 })
            Text('好评 ' + this.selScore() + '%').fontSize(10).fontColor('#2E7D32').margin({ left: 8 })
          }
          .margin({ top: 8 })
          .alignItems(VerticalAlign.Center)
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('商品参数').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Bold)
          Row() {
            Text('正品溯源').fontSize(11).fontColor('#8A8FA3')
            Text('一物一码 · 假一赔十').fontSize(11).fontColor('#212121').margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ top: 10 })
          Row() {
            Text('配送方式').fontSize(11).fontColor('#8A8FA3')
            Text('恒温配送 · 破损包赔').fontSize(11).fontColor('#212121').margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ top: 8 })
          Row() {
            Text('温馨提示').fontSize(11).fontColor('#8A8FA3')
            Text('理性饮酒 · 未满18岁禁购').fontSize(11).fontColor('#212121').margin({ left: 8 })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .margin({ top: 8 })
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 18 })

        Row() {
          Text('¥' + this.selPrice()).fontSize(22).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
            .scale({ x: 1.05, y: 1.05 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('满299减60'.slice(0, 7)).fontSize(10).fontColor(Color.White).backgroundColor('#FF3B30').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 8 })
        }
        .width('100%')
        .margin({ top: 16 })
        .alignItems(VerticalAlign.Center)

        Row() {
          Column() {
            Text('存入酒窖').fontSize(12).fontColor('#5D4037')
          }
          .layoutWeight(1)
          .height(38)
          .borderRadius(19)
          .border({ width: 1, color: '#5D4037' })
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.showDetail = false
            this.showCellar = true
          })

          Column() {
            Text('整箱团购').fontSize(12).fontColor(Color.White)
          }
          .layoutWeight(1)
          .height(38)
          .backgroundColor('#B71C1C')
          .borderRadius(19)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .onClick(() => {
            this.showDetail = false
            this.showGroup = true
          })
        }
        .width('100%')
        .margin({ top: 18 })
        .alignItems(VerticalAlign.Center)
      }
      .width('80%')
      .height('100%')
      .backgroundColor(Color.White)
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.5)')
    .justifyContent(FlexAlign.End)
    .alignItems(HorizontalAlign.End)
    .onClick(() => {
      this.showDetail = false
    })
  }

  build() {
    Stack() {
      Column() {
        this.buildHeader()
        Scroll() {
          Column() {
            if (this.currentTab === 0) {
              this.buildTab1()
            } else if (this.currentTab === 1) {
              this.buildTab2()
            } else if (this.currentTab === 2) {
              this.buildTab3()
            } else if (this.currentTab === 3) {
              this.buildTab4()
            } else if (this.currentTab === 4) {
              this.buildTab5()
            } else if (this.currentTab === 5) {
              this.buildTab6()
            } else {
              this.buildTab7()
            }
          }
          .width('100%')
        }
        .layoutWeight(1)
        .scrollBar(BarState.Off)
        .width('100%')
        .align(Alignment.Top)

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

      if (this.showPublish) {
        this.publishModal()
      }
      if (this.showGroup) {
        this.groupModal()
      }
      if (this.showCellar) {
        this.cellarModal()
      }
      if (this.showDelete) {
        this.deleteModal()
      }
      if (this.showDetail) {
        this.detailModal()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FAF5F2')
  }
}


总结

在这里插入图片描述

本文深入解析了一个基于HarmonyOS声明式UI的酒水饮料电商应用的完整源码实现。从数据模型定义到多标签页构建,从柱状图可视化到酒窖管理系统,从团购弹窗到陈化估值计算,该应用涵盖了酒水垂直电商开发中的全部核心技术点。应用通过@State状态管理实现了UI的响应式更新,通过@Builder方法拆分实现了视图逻辑的模块化组织,通过interface接口保证了数据的类型安全。

在业务功能层面,应用展现了酒水品类的专业深度。私人酒窖模块是最大的亮点功能,实时展示窖温和湿度数据,提供陈化年份柱状图和藏酒清单管理,并通过增值系数估算陈化后的投资价值。整箱团购弹窗集成了规格选择、箱数调节和实时总价计算,配合"已省金额"的正向激励设计,有效促进了转化率。五种弹窗根据业务特点采用了不同的弹出方式和配色方案,体现了精细化的交互设计思维。

在技术实现层面,GoodsItem接口的num字段在啤酒中表示酒精度、在红酒中表示容量、在酒窖中表示年份的复用设计,使得同一套柱状图渲染逻辑能够适配多种数据维度,极大提升了代码的复用率。tag.slice(3, 8)的字符串截取技术巧妙地从"酒精度7%"格式中提取数值标注,展示了ArkTS在数据处理方面的灵活性。酒窖弹窗的双色选择器设计——位置选中棕色、年限选中红色——通过颜色差异帮助用户区分不同维度的选择,是交互设计中值得借鉴的细节处理。整体而言,该应用的源码为HarmonyOS平台上的酒水垂直电商开发提供了一个功能完整、交互丰富、专业度高的实践参考。

Logo

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

更多推荐