引言

在这里插入图片描述

在移动端电商应用日益普及的今天,黄金珠宝类垂直电商应用面临着独特的交互设计挑战。与普通快消品不同,黄金珠宝商品具有客单价高、决策周期长、需要权威认证支撑等特点,用户在浏览和购买时需要更加沉浸式的视觉体验和更加精细化的信息展示。本应用以京东黄金珠宝馆为业务原型,采用HarmonyOS声明式UI开发范式,构建了一个涵盖珠宝集市、黄金专区、钻石婚戒、珍珠玉石、定制工坊、消息中心和用户中心七大功能模块的完整电商场景。

从技术架构层面来看,该应用采用了典型的单页面多标签架构,通过@Entry@Component装饰器将整个应用封装为一个根组件,利用@State状态变量驱动UI响应式更新。所有商品数据通过接口(interface)进行类型约束,确保了数据在传递过程中的类型安全。应用内部通过条件渲染实现七个标签页的切换,每个标签页拥有独立的@Builder构建函数,实现了视图逻辑的高度模块化拆分。同时,应用通过Stack容器叠加实现了五种弹窗模态层的精确控制,涵盖了闲置发布、私人定制、以旧换新、消息删除和商品详情五大核心交互场景。

从业务设计层面来看,该应用深度还原了黄金珠宝行业的核心业务流程。头部区域实时播报金价波动信息,并配以脉冲动画吸引用户关注;珠宝集市页面以列表卡片形式展示典藏甄选商品,每件商品均带有评分进度条和热销标签;黄金专区页面创新性地引入了克重分布柱状图,通过可视化手段帮助用户直观理解不同黄金产品的重量区间;钻石婚戒页面则融合了4C知识科普与克拉柱状图,在购物的同时传递专业知识。此外,定制工坊模块完整覆盖了从设计到成品的全流程,以旧换新模块提供了基于成色的智能估价,充分体现了垂直电商的专业深度。

数据模型与接口定义

在这里插入图片描述

应用首先通过三个接口定义了核心数据模型,这是整个应用类型安全的基石。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字段在不同业务场景中承担不同含义:在黄金商品中表示克重克数,在钻石商品中表示克拉分数,在珍珠商品中表示珠径毫米数。这种设计使得同一个数据结构能够灵活适配多种商品类型,减少了代码冗余。hot布尔字段则用于控制热销标签的显示逻辑,配合脉冲动画形成视觉聚焦效果。

状态管理与组件初始化

在这里插入图片描述

应用根组件Index通过大量@State装饰器声明了响应式状态变量。这些状态变量分为三类:导航控制类(currentTab)、弹窗控制类(showPublishshowCustomshowTradeshowDeleteshowDetail)和业务选择类(selCatselMatselCraftgramNum等)。当任何一个@State变量发生变化时,ArkUI框架会自动重新渲染依赖该状态的UI部分。

@Entry
@Component
struct Index {
  @State currentTab: number = 0
  @State showPublish: boolean = false
  @State showCustom: boolean = false
  @State showTrade: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: GoodsItem | null = null
  @State selCat: number = 0
  @State selMat: number = 0
  @State selCraft: number = 0
  @State gramNum: number = 10
  @State selPurity: number = 0
  @State tradeCat: number = 0
  @State deleteIndex: number = -1
  @State msgs: MsgItem[] = [
    { title: '金价播报', content: '今日足金饰金价 612 元/克,较昨日 +3 元', time: '10:08', read: false },
    { title: '定制进度', content: '您的定制对戒已完成蜡版,进入镶石环节', time: '09:32', read: false },
    { title: '回购报价', content: '您提交的旧金饰估值 5988 元,有效期 48 小时', time: '昨天', read: false },
    { title: '物流更新', content: '您的珍珠项链已全程恒温运输发出,预计明天送达', time: '昨天', read: true },
    { title: '门店邀约', content: 'VIP 专场品鉴会周六 14:00,可携一位亲友', time: '前天', read: true },
    { title: '清洗保养', content: '您有 2 次免费超声波清洗权益待使用', time: '前天', read: true },
    { title: '上新提醒', content: '古法金新品「竹节手镯」已上架,克重 28-52 克', time: '周三', read: true },
    { title: '直播预告', content: '今晚 19:30 珠宝鉴定师直播选钻,整点抽免单', time: '周三', read: true },
    { title: '证书查询', content: '您购买的钻石戒指 GIA 证书已可在线查验', time: '周四', read: true },
    { title: '以旧换新', content: '旧饰换新克减 30 元/克,活动本月截止', time: '上周', read: true },
    { title: '积分兑换', content: '积分可兑换 0.5 克金豆,剩余名额 120 份', time: '上周', read: true },
    { title: '安全提醒', content: '贵重物品请本人签收,谨防调包骗局', time: '上周', read: true }
  ]

消息列表msgs作为可变数组状态,在用户执行删除操作时会通过splice方法动态修改,触发消息列表的实时更新。selectedItem采用可空类型GoodsItem | null,在未选中商品时为null,选中后指向具体的商品对象,这种设计保证了详情弹窗在没有选中商品时不会渲染无效内容。

私有数据与商品列表初始化

在这里插入图片描述

组件内部通过private修饰符声明了多个商品数据数组和配置选项。这些数据在组件创建时初始化,在整个生命周期中保持不变,因此不需要使用@State进行响应式管理。商品列表按品类分为五组:综合精选items、黄金专区golds、钻石婚戒diamonds、珍珠玉石pearls和定制服务customs,每组数据都经过精心编排以覆盖不同价位和风格的需求。

  private tabs: TabInfo[] = [
    { name: '珠宝集市', icon: '💎' },
    { name: '黄金专区', icon: '🥇' },
    { name: '钻石婚戒', icon: '💍' },
    { name: '珍珠玉石', icon: '🦪' },
    { name: '定制工坊', icon: '🔨' },
    { name: '消息', icon: '✉️' },
    { name: '我的', icon: '👤' }
  ]
  private cats: string[] = ['黄金项链', '金手镯', '金豆金条', '古法金', 'K金饰品', '转运珠']
  private mats: string[] = ['足金999', '足金9999', '18K金', '铂金950']
  private crafts: string[] = ['拉丝工艺', '古法磨砂', '镜面抛光', '镶嵌工艺']
  private purities: string[] = ['99.0% 成色', '99.5% 成色', '99.9% 成色', '99.99% 成色']
  private tradeCats: string[] = ['黄金首饰', 'K金首饰', '铂金首饰', '钻石饰品']

  private items: GoodsItem[] = [
    { name: '周生生足金小方链', price: 6820, desc: '约11克 亮面车花', score: 96, num: 11, tag: '畅销款', hot: true },
    { name: '老庙古法金手镯', price: 32800, desc: '约52克 哑光质感', score: 97, num: 52, tag: '古法金', hot: true },
    { name: '周大福传承系列吊坠', price: 5260, desc: '约8.5克 如意锁', score: 96, num: 9, tag: '国潮', hot: false },
    { name: '六福珠宝金豆 1克', price: 618, desc: '投资金豆 攒金神器', score: 95, num: 1, tag: '攒金', hot: true },
    { name: '中国黄金投资金条20g', price: 12360, desc: '上海金交所原料金', score: 98, num: 20, tag: '投资', hot: false },
    { name: '18K金钻石锁骨链', price: 4399, desc: '10分主钻 显白百搭', score: 94, num: 10, tag: '轻奢', hot: false },
    { name: 'PT950铂金对戒', price: 2999, desc: '素圈经典 永不褪色', score: 95, num: 5, tag: '婚戒', hot: false },
    { name: '转运珠红绳手链', price: 688, desc: '足金3克 本命年款', score: 93, num: 3, tag: '本命年', hot: false },
    { name: 'DR 30分求婚钻戒', price: 8999, desc: '18K白金 F色VS', score: 96, num: 30, tag: '求婚款', hot: true },
    { name: 'I DO 雪花钻戒50分', price: 18999, desc: '六爪镶嵌 火彩满分', score: 97, num: 50, tag: '大克拉', hot: false },
    { name: '周生生马眼钻项链', price: 25800, desc: '70分主钻 气场全开', score: 98, num: 70, tag: '高定', hot: false },
    { name: '天然淡水珍珠项链', price: 1280, desc: '7-8mm 正圆强光', score: 94, num: 8, tag: '珍珠', hot: false },
    { name: '大溪地黑珍珠吊坠', price: 3680, desc: '9-10mm 孔雀绿晕', score: 95, num: 10, tag: '稀有', hot: true },
    { name: '和田玉白玉平安扣', price: 2599, desc: '青海料 温润细腻', score: 93, num: 42, tag: '玉石', hot: false },
    { name: '翡翠冰种飘花手镯', price: 16800, desc: '起胶起荧 仙气十足', score: 96, num: 58, tag: '翡翠', hot: false },
    { name: '南红玛瑙柿子红串珠', price: 1880, desc: '满色满肉 手工打磨', score: 92, num: 12, tag: '文玩', hot: false },
    { name: '石榴石紫酒红手串', price: 359, desc: '5mm 通透冰体', score: 90, num: 5, tag: '平价', hot: false },
    { name: '碧玺西瓜多圈手链', price: 968, desc: '缤纷多彩 显白神器', score: 92, num: 6, tag: '彩色宝石', hot: false },
    { name: '琥珀蜜蜡圆珠手串', price: 2280, desc: '鸡油黄 天然原矿', score: 94, num: 16, tag: '蜜蜡', hot: false },
    { name: '珍珠黄金叠戴链', price: 3288, desc: '巴洛克异形珍珠', score: 93, num: 9, tag: '叠戴', hot: true },
    { name: '金镶玉观音吊坠', price: 1899, desc: '足金镶和田玉', score: 91, num: 6, tag: '传统', hot: false },
    { name: '玫瑰金满天星手镯', price: 2380, desc: '18K玫瑰金 细闪', score: 92, num: 12, tag: '少女感', hot: false },
    { name: '古法金八宝罗盘', price: 16800, desc: '约28克 花丝工艺', score: 97, num: 28, tag: '花丝镶嵌', hot: false },
    { name: '足金生肖蛇年本命牌', price: 2680, desc: '约4.2克 3D硬金', score: 94, num: 4, tag: '生肖款', hot: true }
  ]

可以看到,综合商品列表涵盖了从几百元的转运珠到数万元的高定钻石项链,价格梯度覆盖广泛。score字段用于商品评分进度条的可视化展示,num字段在不同品类中语义不同但共用同一个字段,体现了数据模型设计的灵活性。配置选项数组catsmatscraftspuritiestradeCats分别服务于发布闲置、定制预约和以旧换新三个弹窗的选项选择器。

辅助方法与计算函数

在这里插入图片描述

组件内部定义了多个辅助方法,用于从selectedItem中安全提取商品信息,以及进行统计计算。这些方法采用了空值保护机制,当selectedItemnull时返回默认值,避免了空指针异常。

  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方法遍历商品列表找出最大的num值,这个值用于柱状图的高度归一化计算——每个柱子的高度等于g.num / maxOfNum * 最大高度,确保所有柱子按比例缩放到可视范围内。unreadCount方法统计消息列表中未读消息的数量,返回值驱动头部消息图标的未读角标显示,当未读数大于0时角标以脉冲动画形式闪烁,有效吸引用户注意力。

头部构建与金价播报

在这里插入图片描述

buildHeader方法构建了应用的顶部导航区域,整合了品牌标识、实时金价播报、快捷入口和分类标签横滑条。头部采用深紫色#4A148C作为主背景色,配以金色#F5E6C8文字,营造出高端珠宝的奢华视觉氛围。

  @Builder buildHeader() {
    Column() {
      Row() {
        Column() {
          Text('京东黄金珠宝馆').fontSize(18).fontColor('#F5E6C8').fontWeight(FontWeight.Bold)
          Row() {
            Text('今日金价').fontSize(10).fontColor('rgba(245,230,200,0.75)')
            Text('足金 ¥612/克').fontSize(10).fontColor('#F5E6C8').fontWeight(FontWeight.Bold).margin({ left: 6 })
            Text('▲ +3').fontSize(10).fontColor('#FF8A80').margin({ left: 6 })
              .scale({ x: 1.06, y: 1.06 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          }
          .margin({ top: 4 })
          .alignItems(VerticalAlign.Center)
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text('💎').fontSize(20)
        }
        .width(44)
        .onClick(() => {
          this.currentTab = 0
        })

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

      Scroll() {
        Row({ space: 8 }) {
          ForEach(['黄金', '钻石', '珍珠', '翡翠', 'K金', '铂金', '古法金', '金条'], (c: string) => {
            Text(c).fontSize(11).fontColor('#4A148C').backgroundColor('rgba(245,230,200,0.9)').borderRadius(12).padding({ left: 12, right: 12, top: 5, bottom: 5 })
          }, (c: string) => c)
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .margin({ bottom: 12 })
    }
    .width('100%')
    .backgroundColor('#4A148C')
  }

头部区域的金价播报组件使用了scaleanimation属性实现持续脉冲效果,iterations: -1表示动画无限循环,Curve.EaseInOut使动画过渡更加自然。消息图标的未读角标通过position属性精确定位在图标右上角,利用负坐标偏移实现悬浮效果。底部分类标签通过Scroll容器实现横向滚动,scrollBar(BarState.Off)隐藏了滚动条以保持视觉整洁。

珠宝集市标签页构建

在这里插入图片描述

buildTab1方法构建了珠宝集市的商品列表页。页面顶部是一个典藏甄选横幅,配以皇冠动画增强仪式感。下方通过ForEach渲染商品列表,每个商品卡片包含图标、名称、描述、标签、评分进度条、价格和定制咨询按钮,布局紧凑且信息层次分明。

  @Builder buildTab1() {
    Column() {
      Row() {
        Column() {
          Text('典藏甄选').fontSize(18).fontColor('#F5E6C8').fontWeight(FontWeight.Bold)
          Text('每件均附国家级检测证书').fontSize(10).fontColor('rgba(245,230,200,0.7)').margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text('👑').fontSize(30)
            .scale({ x: 1.08, y: 1.08 })
            .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
        }
      }
      .width('100%')
      .padding(18)
      .backgroundColor('#5E1C8A')
      .borderRadius(14)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(VerticalAlign.Center)

      ForEach(this.items, (g: GoodsItem, i: number) => {
        Row() {
          Column() {
            Text(i % 4 === 0 ? '💎' : (i % 4 === 1 ? '🥇' : (i % 4 === 2 ? '💍' : '🦪'))).fontSize(28)
          }
          .width(74)
          .height(74)
          .borderRadius(12)
          .backgroundColor(i % 2 === 0 ? '#F3E8F7' : '#F5E6C8')
          .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('#8D6E63').backgroundColor('#F5E6C8').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 })
              if (g.hot) {
                Text('热销榜TOP').fontSize(9).fontColor(Color.White).backgroundColor('#D81B60').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
                  .scale({ x: 1.06, y: 1.06 })
                  .animation({ duration: 700, 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('#D4A94E').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('#8D6E63')
            .borderRadius(13)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .margin({ top: 8 })
            .onClick(() => {
              this.selectedItem = g
              this.gramNum = 10
              this.showCustom = true
            })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding(12)
        .margin({ left: 12, right: 12, top: 10 })
        .alignItems(VerticalAlign.Center)
        .onClick(() => {
          this.selectedItem = g
          this.showDetail = true
        })
      }, (g: GoodsItem) => g.name)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

商品卡片的评分进度条使用了Stack容器叠加两层Column,底层为灰色背景条,上层为金色进度条,宽度通过g.score + '%'动态绑定。图标采用取模运算i % 4实现四种表情符号的循环交替,背景色通过i % 2实现交替变色效果。点击商品卡片的任何区域都会打开详情弹窗,而点击"定制咨询"按钮则打开定制弹窗,两个点击事件通过onClick分别绑定在卡片容器和按钮组件上。

黄金专区与柱状图可视化

buildTab2方法构建了黄金专区页面,其中最具特色的是克重分布柱状图。该图表通过ForEach遍历黄金商品列表,每个柱子的高度由g.num / this.maxOfNum(this.golds) * 110计算得出,实现了数据的归一化可视化展示。

  @Builder buildTab2() {
    Column() {
      Column() {
        Text('克重分布柱状图').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Bold)
        Row({ space: 8 }) {
          ForEach(this.golds, (g: GoodsItem) => {
            Column() {
              Column()
                .width(16)
                .height(g.num / this.maxOfNum(this.golds) * 110)
                .backgroundColor('#F5E6C8')
                .borderRadius(4)
              Text(g.num.toString() + 'g').fontSize(7).fontColor('rgba(255,255,255,0.85)').margin({ top: 3 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .margin({ top: 12 })
        .alignItems(VerticalAlign.End)
      }
      .width('100%')
      .backgroundColor('#6A1B9A')
      .borderRadius(14)
      .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 })

      Row() {
        Column() {
          Text('足金饰品价').fontSize(10).fontColor('#8A8FA3')
          Text('¥612/克').fontSize(18).fontColor('#B71C1C').fontWeight(FontWeight.Bold).margin({ top: 3 })
            .scale({ x: 1.04, y: 1.04 })
            .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('投资金条价').fontSize(10).fontColor('#8A8FA3')
          Text('¥596/克').fontSize(18).fontColor('#B71C1C').fontWeight(FontWeight.Bold).margin({ top: 3 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('黄金回购价').fontSize(10).fontColor('#8A8FA3')
          Text('¥573/克').fontSize(18).fontColor('#2E7D32').fontWeight(FontWeight.Bold).margin({ top: 3 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12 })
      .alignItems(VerticalAlign.Top)

      Text('黄金精品双列').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.golds, (g: GoodsItem, i: number) => {
          Column() {
            Column() {
              Text('🥇').fontSize(26)
            }
            .width('100%')
            .height(76)
            .borderRadius(10)
            .backgroundColor(i % 2 === 0 ? '#F5E6C8' : '#F3E8F7')
            .justifyContent(FlexAlign.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('#D81B60').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 })
              }
            }
            .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 })
  }

柱状图区域使用了深紫色#6A1B9A背景与金色柱体形成强烈对比,柱体宽度固定为16像素,高度通过除法运算动态计算。Row容器设置了alignItems(VerticalAlign.End)使所有柱子底部对齐,形成标准的柱状图视觉效果。金价三列展示区通过layoutWeight(1)实现等宽三列布局,足金饰品价配有脉冲缩放动画强调实时性。双列商品列表使用Flex容器配合FlexWrap.Wrap实现自动换行,每个商品卡片宽度为48%,通过justifyContent: FlexAlign.SpaceBetween实现两端对齐的瀑布流效果。

应用核心流程

珠宝集市

黄金专区

钻石婚戒

珍珠玉石

定制工坊

消息

我的

点击卡片

点击定制咨询

以旧换新

预约定制

确认删除

应用启动

渲染头部金价播报

加载底部导航栏

用户选择标签页

展示典藏甄选商品列表

展示克重柱状图与双列商品

展示4C课堂与克拉柱状图

展示珍珠玉石列表与珠径图

展示定制服务项目

展示消息中心列表

展示用户中心

用户点击商品

打开商品详情弹窗

打开定制弹窗

选择操作

打开估价弹窗

选择工艺/调整克重/查看估价

选择品类/选择成色/查看回收价

用户点击删除

打开删除确认弹窗

从消息数组移除

关闭弹窗返回列表

钻石婚戒与评分进度

buildTab3方法构建了钻石婚戒页面,融合了4C知识科普和横向滑动的商品展示。页面顶部是克拉柱状图,使用青色#4DD0E1柱体区别于黄金页面的金色柱体。下方通过Scroll容器实现钻石商品的横向滑动浏览,再往下是口碑评分进度条列表。

  @Builder buildTab3() {
    Column() {
      Text('钻石 4C 课堂 · 克拉柱状图').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 14, bottom: 8 })

      Column() {
        Row({ space: 10 }) {
          ForEach(this.diamonds, (g: GoodsItem) => {
            Column() {
              Column()
                .width(18)
                .height(g.num / this.maxOfNum(this.diamonds) * 100)
                .backgroundColor('#4DD0E1')
                .borderRadius(4)
              Text(g.num.toString() + '分').fontSize(8).fontColor('#8A8FA3').margin({ top: 4 }).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.diamonds, (g: GoodsItem, i: number) => {
            Column() {
              Column() {
                Text('💍').fontSize(30)
              }
              .width(96)
              .height(96)
              .borderRadius(12)
              .backgroundColor(i % 2 === 0 ? '#E0F7FA' : '#F5E6C8')
              .justifyContent(FlexAlign.Center)

              Text(g.name).fontSize(11).fontColor('#212121').fontWeight(FontWeight.Medium).margin({ top: 8 }).maxLines(1)
              Text(g.desc).fontSize(9).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
              Text('¥' + g.price).fontSize(14).fontColor('#B71C1C').fontWeight(FontWeight.Bold).margin({ top: 5 })
              if (g.hot) {
                Text('店长推荐').fontSize(9).fontColor(Color.White).backgroundColor('#D81B60').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ top: 4 })
                  .scale({ x: 1.08, y: 1.08 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .width(140)
            .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%')

      Text('口碑评分进度').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      Column() {
        ForEach(this.diamonds, (g: GoodsItem, i: number) => {
          Row() {
            Text((i + 1).toString()).fontSize(12).fontColor(Color.White).backgroundColor(i < 3 ? '#D4A94E' : '#9AA0B5').borderRadius(10).width(20).height(20).textAlign(TextAlign.Center)
            Text(g.tag).fontSize(11).fontColor('#00838F').maxLines(1).margin({ left: 8 }).width(80)
            Stack({ alignContent: Alignment.Start }) {
              Column().width('100%').height(8).backgroundColor('#F0F0F5').borderRadius(4)
              Column().width(g.score + '%').height(8).backgroundColor('#6A1B9A').borderRadius(4)
            }
            .layoutWeight(1)
            .margin({ left: 10, right: 10 })
            Text(g.score.toString()).fontSize(12).fontColor('#D4A94E').fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .alignItems(VerticalAlign.Center)
          .margin({ bottom: 12 })
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12 })
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

口碑评分进度列表使用了排名序号设计,前三名序号背景为金色#D4A94E,之后为灰色#9AA0B5,形成了直观的视觉层次。进度条采用与头部主色一致的紫色#6A1B9A,保持了品牌色的一致性。横向滑动的钻石商品卡片宽度固定为140像素,背景色交替使用青色和金色,既呼应了钻石的冷色调又保持了珠宝的暖色调平衡。

私人定制工坊弹窗

customModal方法构建了预约私人定制的模态弹窗。该弹窗集成了工艺选择器、克重调节器和实时估价计算器,是应用中交互最复杂的弹窗之一。克重通过加减按钮调节(范围1-200克),预估总价按gramNum * 612 + 300公式实时计算,其中612为金价、300为工费。

  @Builder customModal() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('预约私人定制').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
            Text('设计师一对一 · 免费出效果图').fontSize(10).fontColor('#6A1B9A').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.crafts, (c: string, i: number) => {
              Text(c).fontSize(11).fontColor(this.selCraft === i ? Color.White : '#5C6280')
                .backgroundColor(this.selCraft === i ? '#6A1B9A' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selCraft = i
                })
            }, (c: string) => c)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Row() {
          Column() {
            Text('预估克重').fontSize(11).fontColor('#5C6280')
            Row() {
              Text('-').fontSize(15).fontColor('#6A1B9A').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
                .onClick(() => {
                  if (this.gramNum > 1) {
                    this.gramNum--
                  }
                })
              Text(this.gramNum.toString() + ' 克').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).width(70).textAlign(TextAlign.Center)
              Text('+').fontSize(15).fontColor('#6A1B9A').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
                .onClick(() => {
                  if (this.gramNum < 200) {
                    this.gramNum++
                  }
                })
            }
            .margin({ top: 6 })
            .alignItems(VerticalAlign.Center)
          }
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('预估总价').fontSize(11).fontColor('#5C6280')
            Text('¥' + (this.gramNum * 612 + 300)).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('含工费 300 元').fontSize(9).fontColor('#9AA0B5').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.showCustom = false
          })

          Column() {
            Text('预约设计师').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Medium)
          }
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#6A1B9A')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .onClick(() => {
            this.showCustom = false
          })
        }
        .width('100%')
        .margin({ top: 20 })
        .alignItems(VerticalAlign.Center)
      }
      .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.55)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showCustom = false
    })
  }

弹窗的遮罩层使用rgba(0,0,0,0.55)半透明黑色背景,点击遮罩层任意区域可关闭弹窗。内部内容容器通过onClick捕获点击事件并调用event.stopPropagation()阻止事件冒泡,防止点击内容区域时误关闭弹窗。克重调节按钮设置了边界检查:减号按钮在gramNum > 1时才执行递减,加号按钮在gramNum < 200时才执行递增,防止了越界操作。预估总价配有脉冲缩放动画,在视觉上强调价格的动态变化。

以旧换新估价弹窗

tradeModal方法构建了以旧换新估价弹窗。该弹窗通过旧饰品类和成色状况两个选择器驱动估价计算,预估回收价按(selPurity + 1) * 1980公式计算,成色越高回收价越高。

  @Builder tradeModal() {
    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')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.tradeCats, (c: string, i: number) => {
              Text(c).fontSize(11).fontColor(this.tradeCat === i ? Color.White : '#5C6280')
                .backgroundColor(this.tradeCat === i ? '#B7791F' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.tradeCat = i
                })
            }, (c: string) => c)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 16 })

        Column() {
          Text('成色状况').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.purities, (p: string, i: number) => {
              Text(p).fontSize(11).fontColor(this.selPurity === i ? Color.White : '#5C6280')
                .backgroundColor(this.selPurity === i ? '#6A1B9A' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selPurity = i
                })
            }, (p: string) => p)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Column() {
          Text('预估回收价').fontSize(12).fontColor('#5C6280').margin({ top: 8 })
          Text('¥' + (this.selPurity + 1) * 1980).fontSize(30).fontColor('#B71C1C').fontWeight(FontWeight.Bold).margin({ top: 6 })
            .scale({ x: 1.06, y: 1.06 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('换新再补克减 30 元/克').fontSize(10).fontColor('#2E7D32').margin({ top: 6 })
        }
        .width('100%')
        .margin({ top: 10 })
        .alignItems(HorizontalAlign.Start)

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

          Column() {
            Text('预约到店估价').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Medium)
          }
          .layoutWeight(1.6)
          .height(40)
          .backgroundColor('#B7791F')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .onClick(() => {
            this.showTrade = false
          })
        }
        .width('100%')
        .margin({ top: 18 })
        .alignItems(VerticalAlign.Center)
      }
      .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.55)')
    .justifyContent(FlexAlign.End)
    .onClick(() => {
      this.showTrade = false
    })
  }

与定制弹窗不同,以旧换新弹窗使用了justifyContent(FlexAlign.End)使弹窗从底部弹出,符合HarmonyOS的底部弹窗交互规范。两个选择器的选中状态颜色不同:旧饰品类选中为金色#B7791F,成色状况选中为紫色#6A1B9A,通过颜色差异帮助用户区分不同的选择维度。估价金额使用了更大的30号字体和更强烈的1.06倍脉冲动画,突出了价格信息的重要性。

主构建函数与弹窗编排

build方法是整个应用的入口构建函数,通过Stack容器将主界面和五个弹窗层叠在一起。主界面采用Column垂直布局,从上到下依次为头部、可滚动内容区和底部导航栏。内容区通过if-else条件链根据currentTab的值渲染对应的标签页构建函数。

  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.showCustom) {
        this.customModal()
      }
      if (this.showTrade) {
        this.tradeModal()
      }
      if (this.showDelete) {
        this.deleteModal()
      }
      if (this.showDetail) {
        this.detailModal()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FAF6F0')
  }

Stack容器的层叠特性使得弹窗自然覆盖在主界面之上。每个弹窗通过独立的@State布尔变量控制显示,多个弹窗不会同时出现。Scroll容器设置了layoutWeight(1)使其占据头部和底部之间的所有剩余空间,scrollBar(BarState.Off)隐藏了滚动条。底部导航栏buildBottom通过ForEach渲染七个标签按钮,每个按钮的图标和文字颜色根据currentTab的值动态变化,选中状态为紫色#6A1B9A,未选中状态为灰色#9AA0B5

消息删除与列表操作

deleteModal方法构建了消息删除确认弹窗。该弹窗通过deleteIndex记录要删除的消息索引,用户确认后通过splice方法从msgs数组中移除对应的消息项,触发消息列表的响应式更新。

  @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('删除后将无法找回该通知记录').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)
      .onClick((event: ClickEvent) => {
        event.stopPropagation()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('rgba(0,0,0,0.55)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showDelete = false
    })
  }

删除操作前进行了边界检查:this.deleteIndex >= 0 && this.deleteIndex < this.msgs.length,确保索引值有效后才执行splice操作。删除完成后立即将deleteIndex重置为-1并关闭弹窗。垃圾桶图标使用了1.12倍的脉冲缩放动画,通过视觉抖动效果引起用户的注意,强化了删除操作的不可逆性提示。弹窗宽度设置为76%,比其他弹窗更窄,形成居中对话框的视觉形态,区别于底部弹出的操作类弹窗。

技术点对比分析

技术维度 实现方式 设计特点 适用场景
状态管理 @State装饰器 响应式驱动UI更新,支持基本类型和数组 导航切换、弹窗控制、数据选择
组件构建 @Builder方法 视图逻辑模块化拆分,复用性高 头部、标签页、弹窗、底部栏
数据约束 interface接口 编译时类型检查,防止运行时类型错误 商品、标签、消息三类数据模型
弹窗管理 Stack叠加+布尔状态 多弹窗互斥控制,事件冒泡阻止 发布、定制、估价、删除、详情
柱状图 ForEach+动态高度计算 无第三方图表库,纯声明式UI绘制 克重分布、克拉分布、珠径分布
动画效果 scale+animation属性 脉冲缩放循环动画,iterations:-1 金价播报、热销标签、未读角标
列表渲染 ForEach+keyGenerator 基于商品名称的唯一键渲染 商品列表、消息列表、标签列表
横向滚动 Scroll+ScrollDirection 隐藏滚动条,手势驱动 分类标签、钻石商品、纸张本册
事件处理 onClick+stopPropagation 遮罩层关闭+内容层阻止冒泡 所有弹窗的点击交互
数据计算 辅助方法+空值保护 安全提取选中项属性,统计聚合 估价计算、未读统计、最大值查找

安装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 showCustom: boolean = false
  @State showTrade: boolean = false
  @State showDelete: boolean = false
  @State showDetail: boolean = false
  @State selectedItem: GoodsItem | null = null
  @State selCat: number = 0
  @State selMat: number = 0
  @State selCraft: number = 0
  @State gramNum: number = 10
  @State selPurity: number = 0
  @State tradeCat: number = 0
  @State deleteIndex: number = -1
  @State msgs: MsgItem[] = [
    { title: '金价播报', content: '今日足金饰金价 612 元/克,较昨日 +3 元', time: '10:08', read: false },
    { title: '定制进度', content: '您的定制对戒已完成蜡版,进入镶石环节', time: '09:32', read: false },
    { title: '回购报价', content: '您提交的旧金饰估值 5988 元,有效期 48 小时', time: '昨天', read: false },
    { title: '物流更新', content: '您的珍珠项链已全程恒温运输发出,预计明天送达', time: '昨天', read: true },
    { title: '门店邀约', content: 'VIP 专场品鉴会周六 14:00,可携一位亲友', time: '前天', read: true },
    { title: '清洗保养', content: '您有 2 次免费超声波清洗权益待使用', time: '前天', read: true },
    { title: '上新提醒', content: '古法金新品「竹节手镯」已上架,克重 28-52 克', time: '周三', read: true },
    { title: '直播预告', content: '今晚 19:30 珠宝鉴定师直播选钻,整点抽免单', time: '周三', read: true },
    { title: '证书查询', content: '您购买的钻石戒指 GIA 证书已可在线查验', time: '周四', read: true },
    { title: '以旧换新', content: '旧饰换新克减 30 元/克,活动本月截止', time: '上周', read: true },
    { title: '积分兑换', content: '积分可兑换 0.5 克金豆,剩余名额 120 份', 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 cats: string[] = ['黄金项链', '金手镯', '金豆金条', '古法金', 'K金饰品', '转运珠']
  private mats: string[] = ['足金999', '足金9999', '18K金', '铂金950']
  private crafts: string[] = ['拉丝工艺', '古法磨砂', '镜面抛光', '镶嵌工艺']
  private purities: string[] = ['99.0% 成色', '99.5% 成色', '99.9% 成色', '99.99% 成色']
  private tradeCats: string[] = ['黄金首饰', 'K金首饰', '铂金首饰', '钻石饰品']

  private items: GoodsItem[] = [
    { name: '周生生足金小方链', price: 6820, desc: '约11克 亮面车花', score: 96, num: 11, tag: '畅销款', hot: true },
    { name: '老庙古法金手镯', price: 32800, desc: '约52克 哑光质感', score: 97, num: 52, tag: '古法金', hot: true },
    { name: '周大福传承系列吊坠', price: 5260, desc: '约8.5克 如意锁', score: 96, num: 9, tag: '国潮', hot: false },
    { name: '六福珠宝金豆 1克', price: 618, desc: '投资金豆 攒金神器', score: 95, num: 1, tag: '攒金', hot: true },
    { name: '中国黄金投资金条20g', price: 12360, desc: '上海金交所原料金', score: 98, num: 20, tag: '投资', hot: false },
    { name: '18K金钻石锁骨链', price: 4399, desc: '10分主钻 显白百搭', score: 94, num: 10, tag: '轻奢', hot: false },
    { name: 'PT950铂金对戒', price: 2999, desc: '素圈经典 永不褪色', score: 95, num: 5, tag: '婚戒', hot: false },
    { name: '转运珠红绳手链', price: 688, desc: '足金3克 本命年款', score: 93, num: 3, tag: '本命年', hot: false },
    { name: 'DR 30分求婚钻戒', price: 8999, desc: '18K白金 F色VS', score: 96, num: 30, tag: '求婚款', hot: true },
    { name: 'I DO 雪花钻戒50分', price: 18999, desc: '六爪镶嵌 火彩满分', score: 97, num: 50, tag: '大克拉', hot: false },
    { name: '周生生马眼钻项链', price: 25800, desc: '70分主钻 气场全开', score: 98, num: 70, tag: '高定', hot: false },
    { name: '天然淡水珍珠项链', price: 1280, desc: '7-8mm 正圆强光', score: 94, num: 8, tag: '珍珠', hot: false },
    { name: '大溪地黑珍珠吊坠', price: 3680, desc: '9-10mm 孔雀绿晕', score: 95, num: 10, tag: '稀有', hot: true },
    { name: '和田玉白玉平安扣', price: 2599, desc: '青海料 温润细腻', score: 93, num: 42, tag: '玉石', hot: false },
    { name: '翡翠冰种飘花手镯', price: 16800, desc: '起胶起荧 仙气十足', score: 96, num: 58, tag: '翡翠', hot: false },
    { name: '南红玛瑙柿子红串珠', price: 1880, desc: '满色满肉 手工打磨', score: 92, num: 12, tag: '文玩', hot: false },
    { name: '石榴石紫酒红手串', price: 359, desc: '5mm 通透冰体', score: 90, num: 5, tag: '平价', hot: false },
    { name: '碧玺西瓜多圈手链', price: 968, desc: '缤纷多彩 显白神器', score: 92, num: 6, tag: '彩色宝石', hot: false },
    { name: '琥珀蜜蜡圆珠手串', price: 2280, desc: '鸡油黄 天然原矿', score: 94, num: 16, tag: '蜜蜡', hot: false },
    { name: '珍珠黄金叠戴链', price: 3288, desc: '巴洛克异形珍珠', score: 93, num: 9, tag: '叠戴', hot: true },
    { name: '金镶玉观音吊坠', price: 1899, desc: '足金镶和田玉', score: 91, num: 6, tag: '传统', hot: false },
    { name: '玫瑰金满天星手镯', price: 2380, desc: '18K玫瑰金 细闪', score: 92, num: 12, tag: '少女感', hot: false },
    { name: '古法金八宝罗盘', price: 16800, desc: '约28克 花丝工艺', score: 97, num: 28, tag: '花丝镶嵌', hot: false },
    { name: '足金生肖蛇年本命牌', price: 2680, desc: '约4.2克 3D硬金', score: 94, num: 4, tag: '生肖款', hot: true }
  ]
  private golds: GoodsItem[] = [
    { name: '足金光面手镯 30g', price: 18500, desc: '经典圆镯 日常百搭', score: 96, num: 30, tag: '克重30g', hot: true },
    { name: '古法金手镯 52g', price: 32800, desc: '磨砂哑光 显贵气', score: 97, num: 52, tag: '克重52g', hot: false },
    { name: '实心小方链 11g', price: 6820, desc: '车花亮面 不勾头发', score: 96, num: 11, tag: '克重11g', hot: true },
    { name: '麻绳链 15g', price: 9280, desc: '立体编织 挺括有型', score: 95, num: 15, tag: '克重15g', hot: false },
    { name: '投资金条 20g', price: 12360, desc: '回购畅通 攒金首选', score: 98, num: 20, tag: '克重20g', hot: false },
    { name: '金豆盲盒 1g', price: 618, desc: '每月一颗 积少成多', score: 95, num: 1, tag: '克重1g', hot: true },
    { name: '金瓜子 2g', price: 1226, desc: '小巧玲珑 串珠搭配', score: 93, num: 2, tag: '克重2g', hot: false },
    { name: '黄金貔貅手链 8g', price: 4980, desc: '招财纳福 男款女款', score: 94, num: 8, tag: '克重8g', hot: false },
    { name: '龙凤镯对装 60g', price: 37800, desc: '婚嫁三金之首', score: 97, num: 60, tag: '克重60g', hot: false },
    { name: '宝宝长命锁 6g', price: 3720, desc: '满月礼 百日礼', score: 92, num: 6, tag: '克重6g', hot: false },
    { name: '金算盘摆件 25g', price: 15500, desc: '聚财招财 办公摆件', score: 93, num: 25, tag: '克重25g', hot: false },
    { name: '足金耳钉 3g', price: 1860, desc: '素圈耳钉 舒适轻巧', score: 91, num: 3, tag: '克重3g', hot: false }
  ]
  private diamonds: GoodsItem[] = [
    { name: '经典六爪 30分', price: 8999, desc: 'F色 VS2 3EX切工', score: 96, num: 30, tag: '求婚入门', hot: true },
    { name: '经典六爪 50分', price: 18999, desc: 'E色 VS1 火彩突出', score: 97, num: 50, tag: '主流之选', hot: true },
    { name: '围镶款 60分', price: 25800, desc: '碎钻围镶 显大一倍', score: 96, num: 60, tag: '显大款', hot: false },
    { name: '主钻1克拉', price: 58800, desc: 'D色 VVS2 收藏级', score: 98, num: 100, tag: '一克拉', hot: false },
    { name: '雪花扭臂 40分', price: 15800, desc: '扭臂设计 修饰手型', score: 95, num: 40, tag: '设计款', hot: false },
    { name: '群镶排钻 eternity', price: 9800, desc: '整圈满钻 永恒经典', score: 94, num: 45, tag: '满圈钻', hot: false },
    { name: '男款方钻 20分', price: 5999, desc: '阳极硬朗 商务百搭', score: 92, num: 20, tag: '男戒', hot: false },
    { name: '祖母绿切 70分', price: 32800, desc: '阶梯切工 复古贵气', score: 96, num: 70, tag: '复古', hot: true },
    { name: '彩色黄钻 30分', price: 26800, desc: '浓彩黄钻 稀有色系', score: 97, num: 30, tag: '彩钻', hot: false },
    { name: '对戒素圈镶钻款', price: 6600, desc: '一对价格 内壁刻字', score: 95, num: 15, tag: '对戒', hot: false }
  ]
  private pearls: GoodsItem[] = [
    { name: '淡水珍珠项链7-8mm', price: 1280, desc: '正圆强光 媚而不俗', score: 94, num: 8, tag: '淡水珍珠', hot: true },
    { name: 'akoya海水珠8mm', price: 4880, desc: '小灯泡光泽 日系精致', score: 96, num: 8, tag: 'akoya', hot: false },
    { name: '大溪地黑珍珠吊坠', price: 3680, desc: '孔雀绿伴彩 高级感', score: 95, num: 10, tag: '黑珍珠', hot: false },
    { name: '南洋金珠耳钉', price: 5980, desc: '浓金色泽 贵妇标配', score: 96, num: 11, tag: '金珠', hot: true },
    { name: '巴洛克异形珠链', price: 1680, desc: '独一无二 设计感强', score: 92, num: 9, tag: '巴洛克', hot: false },
    { name: '珍珠黄金叠戴链', price: 3288, desc: '混搭风潮 博主同款', score: 93, num: 9, tag: '叠戴款', hot: false },
    { name: '和田玉平安扣', price: 2599, desc: '白玉青海料 温润', score: 93, num: 38, tag: '和田玉', hot: false },
    { name: '冰糯翡翠手镯', price: 9800, desc: '飘阳绿 起胶感', score: 95, num: 56, tag: '翡翠', hot: false },
    { name: '蜜蜡圆珠108颗', price: 3880, desc: '鸡油黄 手工抛光', score: 92, num: 16, tag: '蜜蜡', hot: false },
    { name: '南红玛瑙手串', price: 1880, desc: '柿子红 满色满肉', score: 92, num: 14, tag: '南红', hot: false }
  ]
  private customs: GoodsItem[] = [
    { name: '情侣对戒刻字定制', price: 1999, desc: '免费刻字 内壁专属', score: 96, num: 15, tag: '对戒定制', hot: true },
    { name: '名字项链定制', price: 899, desc: '手写体名字 足金', score: 94, num: 6, tag: '姓名链', hot: false },
    { name: '宝宝手足印金牌', price: 1580, desc: '定格成长 纪念款', score: 95, num: 5, tag: '纪念款', hot: false },
    { name: '婚嫁三金套装定制', price: 38800, desc: '一对一定制顾问', score: 97, num: 60, tag: '婚嫁定制', hot: true },
    { name: '钻戒改圈服务', price: 300, desc: '专业技师 不伤主钻', score: 93, num: 1, tag: '改圈', hot: false },
    { name: '旧金翻新重塑', price: 200, desc: '旧貌换新颜 按克计', score: 92, num: 1, tag: '翻新', hot: false },
    { name: '生日石镶嵌定制', price: 2599, desc: '12月生辰石可选', score: 94, num: 8, tag: '生辰石', hot: false },
    { name: '金镶玉私人定制', price: 3999, desc: '选玉选金 一件起定', score: 95, num: 10, tag: '金镶玉', hot: false },
    { name: '企业纪念章定制', price: 999, desc: '批量定制 开模费低', score: 91, num: 20, tag: '企业定制', hot: false },
    { name: '家传首饰修复', price: 500, desc: '古董修复 大师坐镇', score: 96, num: 1, 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() {
          Text('京东黄金珠宝馆').fontSize(18).fontColor('#F5E6C8').fontWeight(FontWeight.Bold)
          Row() {
            Text('今日金价').fontSize(10).fontColor('rgba(245,230,200,0.75)')
            Text('足金 ¥612/克').fontSize(10).fontColor('#F5E6C8').fontWeight(FontWeight.Bold).margin({ left: 6 })
            Text('▲ +3').fontSize(10).fontColor('#FF8A80').margin({ left: 6 })
              .scale({ x: 1.06, y: 1.06 })
              .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          }
          .margin({ top: 4 })
          .alignItems(VerticalAlign.Center)
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text('💎').fontSize(20)
        }
        .width(44)
        .onClick(() => {
          this.currentTab = 0
        })

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

      Scroll() {
        Row({ space: 8 }) {
          ForEach(['黄金', '钻石', '珍珠', '翡翠', 'K金', '铂金', '古法金', '金条'], (c: string) => {
            Text(c).fontSize(11).fontColor('#4A148C').backgroundColor('rgba(245,230,200,0.9)').borderRadius(12).padding({ left: 12, right: 12, top: 5, bottom: 5 })
          }, (c: string) => c)
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .margin({ bottom: 12 })
    }
    .width('100%')
    .backgroundColor('#4A148C')
  }

  @Builder buildTab1() {
    Column() {
      Row() {
        Column() {
          Text('典藏甄选').fontSize(18).fontColor('#F5E6C8').fontWeight(FontWeight.Bold)
          Text('每件均附国家级检测证书').fontSize(10).fontColor('rgba(245,230,200,0.7)').margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)

        Column() {
          Text('👑').fontSize(30)
            .scale({ x: 1.08, y: 1.08 })
            .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
        }
      }
      .width('100%')
      .padding(18)
      .backgroundColor('#5E1C8A')
      .borderRadius(14)
      .margin({ left: 12, right: 12, top: 12 })
      .alignItems(VerticalAlign.Center)

      ForEach(this.items, (g: GoodsItem, i: number) => {
        Row() {
          Column() {
            Text(i % 4 === 0 ? '💎' : (i % 4 === 1 ? '🥇' : (i % 4 === 2 ? '💍' : '🦪'))).fontSize(28)
          }
          .width(74)
          .height(74)
          .borderRadius(12)
          .backgroundColor(i % 2 === 0 ? '#F3E8F7' : '#F5E6C8')
          .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('#8D6E63').backgroundColor('#F5E6C8').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 })
              if (g.hot) {
                Text('热销榜TOP').fontSize(9).fontColor(Color.White).backgroundColor('#D81B60').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
                  .scale({ x: 1.06, y: 1.06 })
                  .animation({ duration: 700, 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('#D4A94E').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('#8D6E63')
            .borderRadius(13)
            .padding({ left: 10, right: 10, top: 5, bottom: 5 })
            .margin({ top: 8 })
            .onClick(() => {
              this.selectedItem = g
              this.gramNum = 10
              this.showCustom = true
            })
          }
          .alignItems(HorizontalAlign.End)
        }
        .width('100%')
        .backgroundColor(Color.White)
        .borderRadius(12)
        .padding(12)
        .margin({ left: 12, right: 12, top: 10 })
        .alignItems(VerticalAlign.Center)
        .onClick(() => {
          this.selectedItem = g
          this.showDetail = true
        })
      }, (g: GoodsItem) => g.name)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildTab2() {
    Column() {
      Column() {
        Text('克重分布柱状图').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Bold)
        Row({ space: 8 }) {
          ForEach(this.golds, (g: GoodsItem) => {
            Column() {
              Column()
                .width(16)
                .height(g.num / this.maxOfNum(this.golds) * 110)
                .backgroundColor('#F5E6C8')
                .borderRadius(4)
              Text(g.num.toString() + 'g').fontSize(7).fontColor('rgba(255,255,255,0.85)').margin({ top: 3 }).maxLines(1)
            }
            .alignItems(HorizontalAlign.Start)
          }, (g: GoodsItem) => g.name)
        }
        .width('100%')
        .margin({ top: 12 })
      }
      .width('100%')
      .backgroundColor('#6A1B9A')
      .borderRadius(14)
      .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 })

      Row() {
        Column() {
          Text('足金饰品价').fontSize(10).fontColor('#8A8FA3')
          Text('¥612/克').fontSize(18).fontColor('#B71C1C').fontWeight(FontWeight.Bold).margin({ top: 3 })
            .scale({ x: 1.04, y: 1.04 })
            .animation({ duration: 800, iterations: -1, curve: Curve.EaseInOut })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('投资金条价').fontSize(10).fontColor('#8A8FA3')
          Text('¥596/克').fontSize(18).fontColor('#B71C1C').fontWeight(FontWeight.Bold).margin({ top: 3 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)

        Column() {
          Text('黄金回购价').fontSize(10).fontColor('#8A8FA3')
          Text('¥573/克').fontSize(18).fontColor('#2E7D32').fontWeight(FontWeight.Bold).margin({ top: 3 })
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12 })
      .alignItems(VerticalAlign.Top)

      Text('黄金精品双列').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.golds, (g: GoodsItem, i: number) => {
          Column() {
            Column() {
              Text('🥇').fontSize(26)
            }
            .width('100%')
            .height(76)
            .borderRadius(10)
            .backgroundColor(i % 2 === 0 ? '#F5E6C8' : '#F3E8F7')
            .justifyContent(FlexAlign.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('#D81B60').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 })
              }
            }
            .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() {
      Text('钻石 4C 课堂 · 克拉柱状图').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 14, bottom: 8 })

      Column() {
        Row({ space: 10 }) {
          ForEach(this.diamonds, (g: GoodsItem) => {
            Column() {
              Column()
                .width(18)
                .height(g.num / this.maxOfNum(this.diamonds) * 100)
                .backgroundColor('#4DD0E1')
                .borderRadius(4)
              Text(g.num.toString() + '分').fontSize(8).fontColor('#8A8FA3').margin({ top: 4 }).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.diamonds, (g: GoodsItem, i: number) => {
            Column() {
              Column() {
                Text('💍').fontSize(30)
              }
              .width(96)
              .height(96)
              .borderRadius(12)
              .backgroundColor(i % 2 === 0 ? '#E0F7FA' : '#F5E6C8')
              .justifyContent(FlexAlign.Center)

              Text(g.name).fontSize(11).fontColor('#212121').fontWeight(FontWeight.Medium).margin({ top: 8 }).maxLines(1)
              Text(g.desc).fontSize(9).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)
              Text('¥' + g.price).fontSize(14).fontColor('#B71C1C').fontWeight(FontWeight.Bold).margin({ top: 5 })
              if (g.hot) {
                Text('店长推荐').fontSize(9).fontColor(Color.White).backgroundColor('#D81B60').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ top: 4 })
                  .scale({ x: 1.08, y: 1.08 })
                  .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .width(140)
            .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%')

      Text('口碑评分进度').fontSize(15).fontColor('#212121').fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 8 })

      Column() {
        ForEach(this.diamonds, (g: GoodsItem, i: number) => {
          Row() {
            Text((i + 1).toString()).fontSize(12).fontColor(Color.White).backgroundColor(i < 3 ? '#D4A94E' : '#9AA0B5').borderRadius(10).width(20).height(20).textAlign(TextAlign.Center)
            Text(g.tag).fontSize(11).fontColor('#00838F').maxLines(1).margin({ left: 8 }).width(80)
            Stack({ alignContent: Alignment.Start }) {
              Column().width('100%').height(8).backgroundColor('#F0F0F5').borderRadius(4)
              Column().width(g.score + '%').height(8).backgroundColor('#6A1B9A').borderRadius(4)
            }
            .layoutWeight(1)
            .margin({ left: 10, right: 10 })
            Text(g.score.toString()).fontSize(12).fontColor('#D4A94E').fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .alignItems(VerticalAlign.Center)
          .margin({ bottom: 12 })
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(14)
      .margin({ left: 12, right: 12 })
    }
    .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() {
        ForEach(this.pearls, (g: GoodsItem, i: number) => {
          Row() {
            Column()
              .width(4)
              .height(50)
              .backgroundColor(i % 3 === 0 ? '#6A1B9A' : (i % 3 === 1 ? '#D4A94E' : '#00838F'))
              .borderRadius(2)

            Column() {
              Row() {
                Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
                Text(g.tag).fontSize(9).fontColor('#00838F').backgroundColor('#E0F7FA').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 }).margin({ left: 6 })
              }
              .alignItems(VerticalAlign.Center)

              Text(g.desc).fontSize(10).fontColor('#8A8FA3').margin({ top: 3 }).maxLines(1)

              Row() {
                Text('光泽度').fontSize(9).fontColor('#9AA0B5')
                Stack({ alignContent: Alignment.Start }) {
                  Column().width(80).height(6).backgroundColor('#F0F0F5').borderRadius(3)
                  Column().width(g.score * 0.8).height(6).backgroundColor('#D4A94E').borderRadius(3)
                }
                .margin({ left: 6 })
                Text(g.score + '%').fontSize(9).fontColor('#D4A94E').margin({ left: 6 })
              }
              .margin({ top: 5 })
              .alignItems(VerticalAlign.Center)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .margin({ left: 10 })

            Column() {
              Text('¥' + g.price).fontSize(15).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
              if (g.hot) {
                Text('🔥 镇店之宝').fontSize(8).fontColor('#D81B60').margin({ top: 3 })
                  .scale({ x: 1.1, y: 1.1 })
                  .animation({ duration: 600, iterations: -1, curve: Curve.EaseInOut })
              }
            }
            .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 })

      Column() {
        Text('珠径分布柱状图(mm)').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Bold)
        Row({ space: 12 }) {
          ForEach(this.pearls, (g: GoodsItem) => {
            Column() {
              Column()
                .width(16)
                .height(g.num / this.maxOfNum(this.pearls) * 90)
                .backgroundColor('#6A1B9A')
                .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: 4 })
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .padding({ bottom: 12 })
  }

  @Builder buildTab5() {
    Column() {
      Column() {
        Text('私人定制工坊').fontSize(17).fontColor('#F5E6C8').fontWeight(FontWeight.Bold)
        Text('设计 · 打版 · 镶嵌 · 成品 全流程可视').fontSize(10).fontColor('rgba(245,230,200,0.7)').margin({ top: 4 })
        Row() {
          Text('大师坐镇').fontSize(10).fontColor('#4A148C').backgroundColor('#F5E6C8').borderRadius(10).padding({ left: 8, right: 8, top: 3, bottom: 3 })
          Text('30天交付').fontSize(10).fontColor('#4A148C').backgroundColor('#F5E6C8').borderRadius(10).padding({ left: 8, right: 8, top: 3, bottom: 3 }).margin({ left: 8 })
          Text('终身保养').fontSize(10).fontColor('#4A148C').backgroundColor('#F5E6C8').borderRadius(10).padding({ left: 8, right: 8, top: 3, bottom: 3 }).margin({ left: 8 })
        }
        .margin({ top: 10 })
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .backgroundColor('#6A1B9A')
      .borderRadius(14)
      .padding(18)
      .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 })

      Column() {
        ForEach(this.customs, (g: GoodsItem, i: number) => {
          Row() {
            Column() {
              Text(i % 5 === 0 ? '💍' : (i % 5 === 1 ? '🔨' : (i % 5 === 2 ? '✨' : (i % 5 === 3 ? '🎁' : '🛠️')))).fontSize(24)
            }
            .width(52)
            .height(52)
            .borderRadius(12)
            .backgroundColor('#F5E6C8')
            .justifyContent(FlexAlign.Center)

            Column() {
              Row() {
                Text(g.name).fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).maxLines(1)
                if (g.hot) {
                  Text('人气王').fontSize(9).fontColor(Color.White).backgroundColor('#D81B60').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.score + '% · 已服务 ' + g.num * 12 + ' 单').fontSize(9).fontColor('#9AA0B5').margin({ top: 3 })
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            .margin({ left: 10 })

            Column() {
              Text('¥' + g.price + ' 起').fontSize(13).fontColor('#B71C1C').fontWeight(FontWeight.Bold)
              Column() {
                Text('预约定制').fontSize(10).fontColor(Color.White)
              }
              .backgroundColor('#6A1B9A')
              .borderRadius(13)
              .padding({ left: 10, right: 10, top: 5, bottom: 5 })
              .margin({ top: 6 })
              .onClick(() => {
                this.selectedItem = g
                this.gramNum = 10
                this.showCustom = true
              })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('100%')
          .backgroundColor(Color.White)
          .borderRadius(12)
          .padding(12)
          .margin({ bottom: 10 })
          .alignItems(VerticalAlign.Center)
        }, (g: GoodsItem) => g.name)
      }
      .width('100%')
      .padding({ left: 12, right: 12 })
    }
    .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('#F5E6C8')
        .justifyContent(FlexAlign.Center)

        Column() {
          Text('金玉良缘_收藏家').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
          Row() {
            Text('黑金会员').fontSize(9).fontColor('#4A148C').backgroundColor('#F5E6C8').borderRadius(4).padding({ left: 5, right: 5, top: 2, bottom: 2 })
            Text('积分 18600').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('5').fontSize(18).fontColor('#6A1B9A').fontWeight(FontWeight.Bold)
          Text('定制订单').fontSize(10).fontColor('#8A8FA3').margin({ top: 3 })
        }
        .layoutWeight(1)

        Column() {
          Text('18').fontSize(18).fontColor('#D4A94E').fontWeight(FontWeight.Bold)
          Text('收藏珠宝').fontSize(10).fontColor('#8A8FA3').margin({ top: 3 })
        }
        .layoutWeight(1)

        Column() {
          Text('126g').fontSize(18).fontColor('#B7791F').fontWeight(FontWeight.Bold)
          Text('持有黄金').fontSize(10).fontColor('#8A8FA3').margin({ top: 3 })
        }
        .layoutWeight(1)

        Column() {
          Text('2').fontSize(18).fontColor('#D81B60').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('全部 16 ›'.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('克减 30 ›'.slice(0, 8)).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('GIA/NGTC 4 张 ›').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('免费 2 次 ›').fontSize(11).fontColor('#2E7D32')
        }
        .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('#6A1B9A')
        .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.selCat = 0
          this.selMat = 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 ? '#6A1B9A' : '#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: '如:周大福古法金手镯 约45克 无磨损' })
            .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.cats, (c: string, i: number) => {
              Text(c).fontSize(11).fontColor(this.selCat === i ? Color.White : '#5C6280')
                .backgroundColor(this.selCat === i ? '#6A1B9A' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selCat = 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.mats, (m: string, i: number) => {
              Text(m).fontSize(11).fontColor(this.selMat === i ? Color.White : '#5C6280')
                .backgroundColor(this.selMat === i ? '#B7791F' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selMat = i
                })
            }, (m: string) => m)
          }
          .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('#6A1B9A')
          .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.55)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showPublish = false
    })
  }

  @Builder customModal() {
    Column() {
      Column() {
        Row() {
          Column() {
            Text('预约私人定制').fontSize(16).fontColor('#212121').fontWeight(FontWeight.Bold)
            Text('设计师一对一 · 免费出效果图').fontSize(10).fontColor('#6A1B9A').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.crafts, (c: string, i: number) => {
              Text(c).fontSize(11).fontColor(this.selCraft === i ? Color.White : '#5C6280')
                .backgroundColor(this.selCraft === i ? '#6A1B9A' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selCraft = i
                })
            }, (c: string) => c)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Row() {
          Column() {
            Text('预估克重').fontSize(11).fontColor('#5C6280')
            Row() {
              Text('-').fontSize(15).fontColor('#6A1B9A').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
                .onClick(() => {
                  if (this.gramNum > 1) {
                    this.gramNum--
                  }
                })
              Text(this.gramNum.toString() + ' 克').fontSize(13).fontColor('#212121').fontWeight(FontWeight.Medium).width(70).textAlign(TextAlign.Center)
              Text('+').fontSize(15).fontColor('#6A1B9A').width(28).height(28).textAlign(TextAlign.Center).backgroundColor('#F5F6FA').borderRadius(8)
                .onClick(() => {
                  if (this.gramNum < 200) {
                    this.gramNum++
                  }
                })
            }
            .margin({ top: 6 })
            .alignItems(VerticalAlign.Center)
          }
          .alignItems(HorizontalAlign.Start)

          Column() {
            Text('预估总价').fontSize(11).fontColor('#5C6280')
            Text('¥' + (this.gramNum * 612 + 300)).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('含工费 300 元').fontSize(9).fontColor('#9AA0B5').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.showCustom = false
          })

          Column() {
            Text('预约设计师').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Medium)
          }
          .layoutWeight(1)
          .height(40)
          .backgroundColor('#6A1B9A')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .onClick(() => {
            this.showCustom = 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.55)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showCustom = false
    })
  }

  @Builder tradeModal() {
    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')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.tradeCats, (c: string, i: number) => {
              Text(c).fontSize(11).fontColor(this.tradeCat === i ? Color.White : '#5C6280')
                .backgroundColor(this.tradeCat === i ? '#B7791F' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.tradeCat = i
                })
            }, (c: string) => c)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 16 })

        Column() {
          Text('成色状况').fontSize(12).fontColor('#5C6280')
          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(this.purities, (p: string, i: number) => {
              Text(p).fontSize(11).fontColor(this.selPurity === i ? Color.White : '#5C6280')
                .backgroundColor(this.selPurity === i ? '#6A1B9A' : '#F5F6FA')
                .borderRadius(13)
                .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                .margin({ right: 8, top: 8 })
                .onClick(() => {
                  this.selPurity = i
                })
            }, (p: string) => p)
          }
          .width('100%')
        }
        .width('100%')
        .alignItems(HorizontalAlign.Start)
        .margin({ top: 14 })

        Column() {
          Text('预估回收价').fontSize(12).fontColor('#5C6280').margin({ top: 8 })
          Text('¥' + (this.selPurity + 1) * 1980).fontSize(30).fontColor('#B71C1C').fontWeight(FontWeight.Bold).margin({ top: 6 })
            .scale({ x: 1.06, y: 1.06 })
            .animation({ duration: 700, iterations: -1, curve: Curve.EaseInOut })
          Text('换新再补克减 30 元/克').fontSize(10).fontColor('#2E7D32').margin({ top: 6 })
        }
        .width('100%')
        .margin({ top: 10 })
        .alignItems(HorizontalAlign.Start)

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

          Column() {
            Text('预约到店估价').fontSize(13).fontColor(Color.White).fontWeight(FontWeight.Medium)
          }
          .layoutWeight(1.6)
          .height(40)
          .backgroundColor('#B7791F')
          .borderRadius(20)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .onClick(() => {
            this.showTrade = false
          })
        }
        .width('100%')
        .margin({ top: 18 })
        .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.55)')
    .justifyContent(FlexAlign.End)
    .onClick(() => {
      this.showTrade = 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('删除后将无法找回该通知记录').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.55)')
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      this.showDelete = false
    })
  }

  @Builder detailModal() {
    Column() {
      Column() {
        Column() {
          Column() {
            Text('💎').fontSize(40)
          }
          .width(96)
          .height(96)
          .borderRadius(16)
          .backgroundColor('#F5E6C8')
          .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('#6A1B9A').backgroundColor('#F3E8F7').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('NGTC 国检 · 一物一证').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('终身免费清洗保养').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('12期免息').fontSize(10).fontColor(Color.White).backgroundColor('#D81B60').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('#B7791F')
          }
          .layoutWeight(1)
          .height(38)
          .borderRadius(19)
          .border({ width: 1, color: '#B7791F' })
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.showDetail = false
            this.showTrade = true
          })

          Column() {
            Text('预约定制').fontSize(12).fontColor(Color.White)
          }
          .layoutWeight(1)
          .height(38)
          .backgroundColor('#6A1B9A')
          .borderRadius(19)
          .justifyContent(FlexAlign.Center)
          .margin({ left: 10 })
          .onClick(() => {
            this.showDetail = false
            this.showCustom = 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.55)')
    .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.showCustom) {
        this.customModal()
      }
      if (this.showTrade) {
        this.tradeModal()
      }
      if (this.showDelete) {
        this.deleteModal()
      }
      if (this.showDetail) {
        this.detailModal()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FAF6F0')
  }
}


总结

在这里插入图片描述

本文详细解析了一个基于HarmonyOS声明式UI的黄金珠宝电商应用的完整源码实现。从数据模型定义到状态管理,从多标签页构建到弹窗交互,从柱状图可视化到动画效果设计,该应用涵盖了电商类应用开发中的核心技术点。应用通过接口约束保证了数据的类型安全,通过@State状态管理实现了UI的响应式更新,通过@Builder方法拆分实现了视图逻辑的高度模块化。

在交互设计层面,应用展现了极高的完成度。五种弹窗覆盖了发布、定制、估价、删除和详情五大核心业务场景,每种弹窗都根据业务特点采用了不同的弹出方式和视觉设计。柱状图的可视化方案完全基于声明式UI组件实现,无需引入第三方图表库,体现了ArkTS在数据可视化方面的原生能力。脉冲动画在关键信息节点(金价、热销标签、未读角标、估价金额)的广泛应用,有效提升了用户的视觉关注度和交互体验。

在架构层面,Stack容器叠加弹窗的设计简洁而高效,通过布尔状态变量实现了弹窗的精确控制,避免了复杂的状态机管理。ForEachkeyGenerator参数确保了列表渲染的高效性,当数据变化时框架能够精确匹配和复用已有的UI组件。事件冒泡控制机制使得弹窗的交互逻辑清晰明了——点击遮罩关闭、点击内容阻止冒泡,这一模式在所有弹窗中保持一致。整体而言,该应用的源码为HarmonyOS平台上的垂直电商开发提供了一个结构完整、交互丰富、可扩展性强的实践参考。

Logo

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

更多推荐