引言

在这里插入图片描述

潮玩盲盒市场近年来在年轻消费群体中呈现出爆发式增长态势,MOLLY、LABUBU、SKULLPANDA、DIMOO等热门IP系列持续引领潮流文化消费浪潮。传统的电商架构往往难以兼顾盲盒特有的概率公示机制、限时抢购体验以及社区拼盒社交属性。本文将深入剖析一个基于HarmonyOS ArkTS声明式UI框架构建的潮玩盲盒交易社区应用,该应用覆盖了盲盒购买、拼盒组队、概率公示、二手交易四大核心业务场景,通过七Tab底部导航架构实现了完整的用户消费闭环。

在技术架构层面,该应用采用了HarmonyOS ArkTS API 24提供的声明式UI开发范式,通过@Entry@Component@State@Builder等核心装饰器构建组件化界面体系。应用以@Observed装饰的可观察数据类驱动UI响应式更新,结合LinearGradientOptions线性渐变设计令牌系统实现潮玩紫(#6A1B9A)与荧光粉(#E91E63)的视觉品牌一致性。多层级弹窗系统通过position定位与zIndex层叠管理实现了模拟抽盒、盲盒详情、发售预约、拼盒邀请、编辑挂卖、删除确认等复杂交互场景的优雅管理。

在业务设计层面,应用围绕"概率透明、拼团社交、二手流通"三大产品理念展开。概率公示模块通过柱状图与进度条可视化展示各系列稀有度分布,保障消费者知情权;拼盒组队模块支持多人实时参团进度跟踪与成员头像槽位动态渲染;二手交易模块集成官方鉴定标签与编辑挂卖功能,构建了从购买到流通的完整生态闭环。整个应用通过精心设计的全局写死数据集(mock数据)模拟真实业务场景,为后续接入后端API提供了清晰的数据契约定义。

一、接口定义与数据模型层

在这里插入图片描述

应用首先定义了一系列TypeScript接口来规范数据结构,确保类型安全。这些接口覆盖了盲盒商品、拼盒组队、二手交易、消息通知等核心业务实体。

interface BlindBoxSeries {
  id: number
  name: string
  tag: string
  price: number
  originalPrice: number
  sales: number
  icon: string
  bg: string
  series: string
  rating: number
  stock: number
  hiddenCount: number
}

interface GroupItem {
  id: number
  boxName: string
  icon: string
  bg: string
  price: number
  joinedCount: number
  totalCount: number
  endTime: string
  members: string[]
}

interface SecondHandItem {
  id: number
  name: string
  condition: string
  price: number
  icon: string
  bg: string
  seller: string
  verified: boolean
  views: number
}

interface MessageItem {
  id: number
  type: string
  title: string
  content: string
  icon: string
  time: string
  unread: boolean
}

BlindBoxSeries接口定义了盲盒商品的核心属性模型,其中hiddenCount字段记录隐藏款数量,tag字段支持"拼团爆款"、"限量发售"等营销标签分类,originalPriceprice的差价设计驱动了折扣视觉呈现。GroupItem接口中members数组存储成员头像emoji,joinedCounttotalCount的比值驱动拼团进度条渲染。SecondHandItem接口的verified布尔值决定是否展示"已鉴定"绿色标签,views字段用于浏览量热度展示。

可观察数据类

在这里插入图片描述

应用使用@Observed装饰器创建可观察的数据类,当数据发生变化时能够自动触发UI刷新。

@Observed
export class BoxItem {
  id: number = 0
  name: string = ''
  tag: string = ''
  price: number = 0
  originalPrice: number = 0
  sales: number = 0
  icon: string = ''
  bg: string = ''
  series: string = ''
  rating: number = 0
  stock: number = 0
  hiddenCount: number = 0
  constructor(id: number, name: string, tag: string, price: number,
    originalPrice: number, sales: number, icon: string, bg: string,
    series: string, rating: number, stock: number, hiddenCount: number) {
    this.id = id
    this.name = name
    this.tag = tag
    this.price = price
    this.originalPrice = originalPrice
    this.sales = sales
    this.icon = icon
    this.bg = bg
    this.series = series
    this.rating = rating
    this.stock = stock
    this.hiddenCount = hiddenCount
  }
}

@Observed装饰器是ArkUI响应式系统的核心机制之一,它使得BoxItem实例的属性变更能够被框架追踪。当组件中引用了该实例的属性并在UI中渲染时,任何对属性的修改都会自动触发对应组件的重新渲染。构造函数接收12个参数完整初始化商品信息,所有属性都设有默认值以防止空值异常。

二、设计令牌与全局配置

在这里插入图片描述

应用通过常量定义统一的设计令牌(Design Tokens),确保视觉风格的全局一致性。这包括渐变色配置、快捷导航配置、稀有度配置、系列配置等。

const PURPLE_GRADIENT: LinearGradientOptions = {
  angle: 135,
  colors: [['#6A1B9A', 0.0], ['#E91E63', 1.0]]
}

const DARK_GRADIENT: LinearGradientOptions = {
  angle: 160,
  colors: [['#1A1A2E', 0.0], ['#4A148C', 1.0]]
}

const QUICK_NAVS: QuickNavMeta[] = [
  { label: '今日开盒', icon: '🎁', bg: '#F3E5F5' },
  { label: '隐藏概率', icon: '💎', bg: '#FFF8E1' },
  { label: '拼盒组队', icon: '🤝', bg: '#FCE4EC' },
  { label: '二手回收', icon: '♻️', bg: '#E8F5E9' }
]

const RARITY_CONFIG: Record<string, RarityMeta> = {
  '隐藏款': { label: '隐藏款', icon: '💎', color: '#FFD54F', bg: '#FFF8E1', probability: '1/144' },
  '稀有款': { label: '稀有款', icon: '👑', color: '#E91E63', bg: '#FCE4EC', probability: '1/36' },
  '普通款': { label: '普通款', icon: '🎯', color: '#6A1B9A', bg: '#F3E5F5', probability: '1/12' }
}

const SERIES_CONFIG: Record<string, SeriesMeta> = {
  'MOLLY': { label: 'MOLLY', icon: '🎀', color: '#E91E63' },
  'LABUBU': { label: 'LABUBU', icon: '🦷', color: '#6A1B9A' },
  'SKULLPANDA': { label: 'SKULLPANDA', icon: '💀', color: '#424242' },
  'DIMOO': { label: 'DIMOO', icon: '☁️', color: '#42A5F5' },
  'HIRONO': { label: 'HIRONO', icon: '🌙', color: '#7B1FA2' },
  'CRYBABY': { label: 'CRYBABY', icon: '😢', color: '#FF7043' }
}

PURPLE_GRADIENT定义了135度角从潮玩紫到荧光粉的主品牌渐变,用于头部区域和强调按钮。DARK_GRADIENT则用于限量预售等暗色风格场景,营造高级感。RARITY_CONFIG采用Record类型以稀有度名称为键,存储了图标、颜色、背景色和概率值,其中隐藏款概率1/144符合行业标准的整箱出货概率。SERIES_CONFIG映射了六大IP系列的专属图标与品牌色,确保每个系列在视觉上具有辨识度。

弹窗配置与模拟数据

const MODAL_INFO: Record<string, ModalConfig> = {
  'draw': { title: '🎁 模拟抽盒', desc: '消耗 1 次免费模拟机会' },
  'group': { title: '🤝 拼盒邀请', desc: '邀请好友一起拼盒更划算' },
  'delete': { title: '⚠️ 确认删除', desc: '删除后不可恢复' },
  'edit': { title: '✏️ 编辑潮玩', desc: '编辑收藏的潮玩信息' },
  'detail': { title: '📦 盲盒详情', desc: '查看完整系列信息' }
}

const mockBoxes: BoxItem[] = [
  new BoxItem(1, 'MOLLY 星际旅行系列', '拼团爆款', 59, 99, 23000, '🎀', '#FCE4EC', 'MOLLY', 4.9, 144, 2),
  new BoxItem(2, 'LABUBU 精灵三园系列', '限量发售', 69, 119, 18000, '🦷', '#F3E5F5', 'LABUBU', 4.8, 72, 1),
  new BoxItem(3, 'SKULLPANDA 暗夜嘉年华', '热门IP', 79, 139, 15000, '💀', '#ECEFF1', 'SKULLPANDA', 4.7, 96, 3),
  new BoxItem(4, 'DIMOO 云朵马戏团系列', '新品上架', 59, 109, 12000, '☁️', '#E3F2FD', 'DIMOO', 4.8, 120, 2),
  new BoxItem(5, 'HIRONO 小野系列五', '经典复刻', 49, 89, 28000, '🌙', '#F3E5F5', 'HIRONO', 4.6, 200, 4),
  new BoxItem(6, 'CRYBABY 眼泪公主系列', '女生最爱', 55, 95, 16000, '😢', '#FBE9E7', 'CRYBABY', 4.7, 88, 1)
]

const DRAW_SIMULATE: string[] = ['🎀', '☁️', '🦷', '💀', '🌙', '😢', '🎉', '🐼', '🚀', '🍰', '💎', '👑']

MODAL_INFO以弹窗类型为键集中管理标题与描述文案,便于国际化与文案维护。mockBoxes数组通过BoxItem构造函数实例化12款盲盒商品数据,每款商品包含价格、销量、库存、隐藏款数量等完整业务字段。DRAW_SIMULATE数组存储了模拟抽盒的随机结果池,包含普通款与稀有款emoji,抽盒时通过Math.random()随机选取。

三、工具函数与底部Tab枚举

在这里插入图片描述

应用定义了格式化工具函数和导航Tab枚举,为组件层提供统一的格式化逻辑与路由控制。

function fmtPrice(p: number): string {
  return '¥' + p.toFixed(2)
}

function fmtSales(s: number): string {
  if (s >= 10000) {
    return (s / 10000).toFixed(1) + '万'
  }
  return s.toString()
}

function getSeriesIcon(s: string): string {
  const meta = SERIES_CONFIG[s]
  if (meta) {
    return meta.icon
  }
  return '📦'
}

function getRarityColor(r: string): string {
  const meta = RARITY_CONFIG[r]
  if (meta) {
    return meta.color
  }
  return '#6A1B9A'
}

enum MainTab {
  HOME = 0,
  PRESALE = 1,
  GROUP = 2,
  SECONDHAND = 3,
  PROBABILITY = 4,
  MESSAGE = 5,
  PROFILE = 6
}

fmtPrice函数将数字价格格式化为带"¥"符号且保留两位小数的字符串,fmtSales函数实现万级数字的自动转换显示。getSeriesIcongetRarityColor通过查找配置映射表获取对应系列的图标和稀有度颜色,提供安全的默认值回退。MainTab枚举定义了七个底部Tab的索引值,使Tab切换逻辑更加清晰可读。

四、入口页面与底部导航架构

在这里插入图片描述

入口组件BlindBoxApp通过@Entry装饰器标记为应用入口,采用@State管理当前激活的Tab索引,并通过@Builder方法构建内容区域和底部导航栏。

@Entry
@Component
struct BlindBoxApp {
  @State activeTab: MainTab = MainTab.HOME

  @Builder contentArea() {
    Column() {
      if (this.activeTab === MainTab.HOME) {
        HomeContent()
      } else if (this.activeTab === MainTab.PRESALE) {
        PresaleContent()
      } else if (this.activeTab === MainTab.GROUP) {
        GroupContent()
      } else if (this.activeTab === MainTab.SECONDHAND) {
        SecondHandContent()
      } else if (this.activeTab === MainTab.PROBABILITY) {
        ProbabilityContent()
      } else if (this.activeTab === MainTab.MESSAGE) {
        MessageContent()
      } else {
        ProfileContent()
      }
    }
    .layoutWeight(1)
  }

  @Builder bottomTabItem(icon: string, label: string, tab: MainTab) {
    Column() {
      Text(icon).fontSize(18).opacity(this.activeTab === tab ? 1.0 : 0.45)
      Text(label).fontSize(8)
        .fontColor(this.activeTab === tab ? '#6A1B9A' : '#999999')
        .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 1 })
      if (this.activeTab === tab) {
        Column().width(14).height(3).backgroundColor('#6A1B9A').borderRadius(2).margin({ top: 2 })
      }
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 4, bottom: 4 })
    .onClick(() => { this.activeTab = tab })
  }

  build() {
    Column() {
      this.contentArea()
      Row() {
        this.bottomTabItem('🏠', '广场', MainTab.HOME)
        this.bottomTabItem('⏰', '预售', MainTab.PRESALE)
        this.bottomTabItem('🤝', '拼盒', MainTab.GROUP)
        this.bottomTabItem('♻️', '二手', MainTab.SECONDHAND)
        this.bottomTabItem('📊', '概率', MainTab.PROBABILITY)
        this.bottomTabItem('💬', '消息', MainTab.MESSAGE)
        this.bottomTabItem('👤', '我的', MainTab.PROFILE)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .padding({ top: 4, bottom: 6 })
      .shadow({ radius: 8, color: '#1A000000', offsetY: -2 })
    }
    .width('100%').height('100%')
    .backgroundColor('#F5F5F7')
  }
}

入口组件的核心设计模式是"条件渲染路由",contentArea Builder根据activeTab状态值动态加载对应的子组件。每个底部Tab项通过bottomTabItem Builder统一构建,利用透明度(opacity)、字体颜色(fontColor)和字重(fontWeight)三重视觉反馈区分激活与非激活状态。激活状态下额外渲染一个14x3的紫色圆角指示条,增强了选中状态的视觉确认感。底部导航栏通过shadow属性添加向上的阴影投射,实现与内容区的视觉分离。

五、盲盒广场首页与弹窗系统

在这里插入图片描述

首页组件HomeContent集成了渐变头部、快捷入口、热门系列横滑、瀑布双列商品列表,以及抽盒模拟和盲盒详情两个弹窗。

@Component
struct HomeContent {
  @State showDetail: boolean = false
  @State showDraw: boolean = false
  @State selectedBox: BoxItem | null = null
  @State drawResult: string = '🎀'

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.55)')
      .onClick(onClose)
  }

  @Builder drawModal() {
    Column() {
      this.modalOverlay(() => { this.showDraw = false })
      Column() {
        Column() {
          Text('🎁').fontSize(56).margin({ top: 24 })
          Text('模拟抽盒').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            .margin({ top: 8 })
          Text('单盒 ¥' + (this.selectedBox?.price ?? 59).toString()).fontSize(12)
            .fontColor('rgba(255,255,255,0.8)').margin({ top: 4 })
        }
        .width('100%').padding({ bottom: 24 })
        .linearGradient(PURPLE_GRADIENT)
        .borderRadius({ topLeft: 16, topRight: 16 })

        Column() {
          Text(this.drawResult).fontSize(72).margin({ top: 20 })
          Text('模拟结果').fontSize(11).fontColor('#999999').margin({ top: 8 })
          Text('概率仅供参考,实际以公示为准').fontSize(9).fontColor('#CCCCCC').margin({ top: 4 })

          Row() {
            Column() {
              Text('💎').fontSize(20)
              Text('隐藏款').fontSize(9).fontColor('#FFD54F').margin({ top: 2 })
              Text('1/144').fontSize(8).fontColor('#BBBBBB').margin({ top: 1 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            .backgroundColor('#FFF8E1').borderRadius(10).padding({ top: 10, bottom: 10 })

            Column() {
              Text('👑').fontSize(20)
              Text('稀有款').fontSize(9).fontColor('#E91E63').margin({ top: 2 })
              Text('1/36').fontSize(8).fontColor('#BBBBBB').margin({ top: 1 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            .backgroundColor('#FCE4EC').borderRadius(10).padding({ top: 10, bottom: 10 })
            .margin({ left: 8 })

            Column() {
              Text('🎯').fontSize(20)
              Text('普通款').fontSize(9).fontColor('#6A1B9A').margin({ top: 2 })
              Text('1/12').fontSize(8).fontColor('#BBBBBB').margin({ top: 1 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            .backgroundColor('#F3E5F5').borderRadius(10).padding({ top: 10, bottom: 10 })
            .margin({ left: 8 })
          }
          .width('100%').margin({ top: 20 })

          Row() {
            Text('再抽一次').fontSize(13).fontColor('#6A1B9A').backgroundColor('#F3E5F5')
              .borderRadius(18).padding({ left: 24, right: 24, top: 10, bottom: 10 })
              .onClick(() => {
                const idx = Math.floor(Math.random() * DRAW_SIMULATE.length)
                this.drawResult = DRAW_SIMULATE[idx]
              })
            Text('立即购买').fontSize(13).fontColor('#FFFFFF').backgroundColor('#E91E63')
              .borderRadius(18).padding({ left: 24, right: 24, top: 10, bottom: 10 })
              .margin({ left: 10 })
              .onClick(() => { this.showDraw = false })
          }
          .width('100%').justifyContent(FlexAlign.Center)
          .margin({ top: 20, bottom: 20 })
        }
        .padding({ left: 16, right: 16 })
      }
      .width('82%').backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '9%', y: '12%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }
}

弹窗系统采用了"遮罩层+内容层"的双层叠加架构。modalOverlay是一个可复用的遮罩Builder,接收onClose回调函数,点击遮罩区域即触发关闭。抽盒模拟弹窗drawModal的头部使用PURPLE_GRADIENT渐变背景配合圆角顶部裁切,展示商品图标与价格信息。弹窗主体展示72号字体的抽盒结果emoji,下方三栏分别展示隐藏款(1/144)、稀有款(1/36)和普通款(1/12)的概率信息,每栏采用对应的主题色背景。

"再抽一次"按钮通过Math.floor(Math.random() * DRAW_SIMULATE.length)从12个emoji中随机选取结果,实现了模拟抽盒的随机性体验。整个弹窗通过position绝对定位居中显示,zIndex(999)确保浮于所有内容之上。

商品卡片与瀑布流布局

@Builder boxCard(b: BoxItem) {
  Column() {
    Stack() {
      Column() {
        Text(b.icon).fontSize(42)
      }
      .width('100%').height(96)
      .backgroundColor(b.bg)
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
      if (b.tag !== '') {
        Text(b.tag).fontSize(8).fontColor('#FFFFFF').backgroundColor('#E91E63')
          .padding({ left: 5, right: 5, top: 2, bottom: 2 })
          .borderRadius({ topRight: 8, bottomLeft: 8 })
      }
    }
    .width('100%').height(96)

    Column() {
      Text(b.name).fontSize(12).fontWeight(FontWeight.Medium).fontColor('#212121')
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%')
      Row() {
        Text(fmtPrice(b.price)).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#E91E63')
        Text(fmtPrice(b.originalPrice)).fontSize(9).fontColor('#BBBBBB')
          .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 3 })
        Column().layoutWeight(1)
      }
      .width('100%').margin({ top: 4 })

      Row() {
        Text('💎x' + b.hiddenCount.toString()).fontSize(9).fontColor('#FFD54F')
          .backgroundColor('#FFF8E1').padding({ left: 4, right: 4, top: 1, bottom: 1 }).borderRadius(6)
        Column().layoutWeight(1)
        Text(fmtSales(b.sales) + '人拼').fontSize(9).fontColor('#999999')
      }
      .width('100%').margin({ top: 5 })
    }
    .padding(8).alignItems(HorizontalAlign.Start)
  }
  .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
  .margin({ left: 6, right: 6, top: 6 })
  .shadow({ radius: 6, color: '#0D000000', offsetY: 2 })
  .onClick(() => { this.selectedBox = b; this.showDetail = true })
}

boxCard Builder封装了商品卡片的完整渲染逻辑。Stack容器实现图标与营销标签的层叠效果,标签通过borderRadius({ topRight: 8, bottomLeft: 8 })实现左上角圆角贴片效果。价格区域使用TextDecorationType.LineThrough为原价添加删除线,突出折扣力度。隐藏款数量和拼团人数通过彩色标签展示,点击卡片设置selectedBox并打开详情弹窗。瀑布流通过两个Column容器并排实现,奇偶索引的商品分别放入左右列。

六、限时预售与倒计时模块

预售模块PresaleContent实现了暗色风格的发售预约体验,包含倒计时展示和预约弹窗。

@Component
struct PresaleContent {
  @State showPresaleModal: boolean = false
  @State selectedPresale: BoxItem | null = null
  @State presaleHours: string = '12'
  @State presaleMins: string = '45'
  @State presaleSecs: string = '30'

  @Builder presaleModal() {
    Column() {
      this.modalOverlay(() => { this.showPresaleModal = false })
      Column() {
        Column() {
          Text('⏰').fontSize(48).margin({ top: 20 })
          Text('限量发售预约').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 8 })
          Text(this.selectedPresale?.name ?? '').fontSize(11).fontColor('rgba(255,255,255,0.8)').margin({ top: 4 })
        }
        .width('100%').linearGradient(DARK_GRADIENT)
        .borderRadius({ topLeft: 16, topRight: 16 })
        .padding({ bottom: 20 })

        Column() {
          Text('预约享优先购资格').fontSize(12).fontColor('#666666').margin({ top: 14 })
          Text('⏰ 12:45:30 后开抢').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E91E63')
            .margin({ top: 8 })

          Text('选择尺码').fontSize(11).fontColor('#888888').width('100%').margin({ top: 14 })
          Row() {
            Text('端盒').fontSize(11).fontColor('#FFFFFF').backgroundColor('#6A1B9A')
              .padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14)
            Text('单盒').fontSize(11).fontColor('#6A1B9A').backgroundColor('#F3E5F5')
              .padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14).margin({ left: 8 })
            Text('套装').fontSize(11).fontColor('#6A1B9A').backgroundColor('#F3E5F5')
              .padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14).margin({ left: 8 })
          }
          .width('100%').margin({ top: 6 })

          Text('⚠️ 预约不等于购买成功,开抢时请准时参与').fontSize(9).fontColor('#CCCCCC')
            .width('100%').margin({ top: 12 })

          Text('立即预约').fontSize(15).fontColor('#FFFFFF')
            .backgroundColor('#E91E63').borderRadius(22)
            .padding({ left: 60, right: 60, top: 11, bottom: 11 })
            .margin({ top: 16, bottom: 20 })
            .onClick(() => { this.showPresaleModal = false })
        }
        .padding({ left: 16, right: 16 })
      }
      .width('84%').backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '8%', y: '15%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }
}

预售弹窗头部采用DARK_GRADIENT暗色渐变背景,营造限量发售的高级氛围。倒计时以"时:分:秒"格式展示,每个数字单元使用半透明黑色背景的圆角方块。预约弹窗提供"端盒"、“单盒”、"套装"三种尺码选择,选中项以紫色填充背景区分。底部"立即预约"按钮使用荧光粉色背景和高圆角设计,配合底部警示文案"预约不等于购买成功"提醒用户抢购规则。

预售列表头部展示倒计时数字方块,每个数字以独立的Text组件渲染,通过半透明黑色背景圆角方块呈现,分隔符使用冒号文本。预售卡片presaleCard采用横向布局,左侧图标区与右侧信息区并排,根据index参数判断商品状态为"即将开抢"(金色)还是"预约中"(紫色)。

七、拼盒组队与进度可视化

拼盒组队模块GroupContent实现了社交拼团功能,包含成员头像槽位渲染和拼团进度条。

@Builder groupCard(g: GroupItem) {
  Column() {
    Row() {
      Column() {
        Text(g.icon).fontSize(30)
      }
      .width(60).height(60).backgroundColor(g.bg).borderRadius(12)
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        Text(g.boxName).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%')
        Row() {
          Text('¥' + g.price.toString()).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#E91E63')
          Text('/人').fontSize(10).fontColor('#999999')
          Column().layoutWeight(1)
          Text('截止 ' + g.endTime).fontSize(9).fontColor('#999999')
        }
        .width('100%').margin({ top: 4 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
    }
    .width('100%')

    Row() {
      ForEach([0, 1, 2, 3, 4, 5, 6, 7], (i: number) => {
        if (i < g.totalCount) {
          Column() {
            if (i < g.joinedCount) {
              Text(g.members[i % g.members.length]).fontSize(14)
                .width(28).height(28).borderRadius(14).backgroundColor('#F3E5F5')
                .textAlign(TextAlign.Center)
            } else {
              Text('+').fontSize(14).fontColor('#CCCCCC')
                .width(28).height(28).borderRadius(14).backgroundColor('#F5F5F5')
                .textAlign(TextAlign.Center)
            }
          }
          .margin({ left: 4, right: 4 })
        }
      })
      Column().layoutWeight(1)
      Text('去参团').fontSize(11).fontColor('#FFFFFF').backgroundColor('#6A1B9A')
        .padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14)
        .onClick(() => { this.selectedGroup = g; this.showGroupModal = true })
    }
    .width('100%').margin({ top: 10 })

    Row() {
      Column()
        .width((g.joinedCount / g.totalCount * 100).toFixed(0) + '%')
        .height(4).backgroundColor('#6A1B9A').borderRadius(2)
      Column().layoutWeight(1)
    }
    .width('100%').height(4).backgroundColor('#F3E5F5').borderRadius(2).margin({ top: 8 })

    Text('已拼 ' + g.joinedCount.toString() + '/' + g.totalCount.toString() + ' 人')
      .fontSize(9).fontColor('#6A1B9A').margin({ top: 4 })
  }
  .width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(12)
  .margin({ left: 12, right: 12, top: 6 })
  .shadow({ radius: 5, color: '#0D000000', offsetY: 2 })
  .onClick(() => { this.selectedGroup = g; this.showGroupModal = true })
}

拼盒卡片的核心亮点是成员头像槽位的动态渲染逻辑。通过ForEach遍历0到7的索引,首先判断i < g.totalCount决定是否渲染槽位,再判断i < g.joinedCount决定渲染已参团成员头像(紫色背景emoji)还是待加入占位符(灰色背景"+"号)。已参团成员头像通过g.members[i % g.members.length]循环取值,确保成员数组的灵活复用。

进度条采用嵌套Row容器实现,外层Row作为轨道设置浅紫色背景,内层Column宽度通过(g.joinedCount / g.totalCount * 100).toFixed(0) + '%'动态计算百分比,呈现紫色填充进度。底部文字显示"已拼 X/Y 人"的明确数字反馈。拼盒邀请弹窗groupModal从屏幕55%位置滑入,采用底部弹出的交互模式,展示完整的团员列表与邀请按钮。

八、二手交易与编辑挂卖

二手交易模块SecondHandContent集成了商品列表、编辑弹窗和删除确认弹窗。

@Builder secondHandCard(s: SecondHandItem) {
  Row() {
    Column() {
      Text(s.icon).fontSize(34)
    }
    .width(68).height(68).backgroundColor(s.bg).borderRadius(14)
    .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

    Column() {
      Row() {
        Text(s.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).layoutWeight(1)
        if (s.verified) {
          Text('✅ 已鉴定').fontSize(9).fontColor('#4CAF50')
            .backgroundColor('#E8F5E9').padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6)
        }
      }
      .width('100%')

      Row() {
        Text(s.condition).fontSize(10).fontColor('#6A1B9A')
          .backgroundColor('#F3E5F5').padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6)
        Text(s.seller).fontSize(10).fontColor('#999999').margin({ left: 8 })
      }
      .width('100%').margin({ top: 5 })

      Row() {
        Text(fmtPrice(s.price)).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E91E63')
        Column().layoutWeight(1)
        Text(s.views.toString() + '人浏览').fontSize(9).fontColor('#999999')
        Text('✏️').fontSize(14).margin({ left: 10 })
          .onClick(() => { this.selectedItem = s; this.showEditModal = true })
        Text('🗑️').fontSize(14).margin({ left: 8 })
          .onClick(() => { this.selectedItem = s; this.showDeleteConfirm = true })
      }
      .width('100%').margin({ top: 5 })
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
  }
  .width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(12)
  .margin({ left: 12, right: 12, top: 6 })
  .shadow({ radius: 5, color: '#0D000000', offsetY: 2 })
}

二手卡片采用横向布局,左侧图标区与右侧信息区并排展示。verified布尔值条件渲染"已鉴定"绿色标签,增强交易信任度。成色信息以紫色标签形式展示,支持"全新未拆"、“已拆展示”、"整套出"等成色分类。操作区集成编辑和删除两个图标按钮,分别触发编辑弹窗和删除确认弹窗。

编辑挂卖弹窗

@Builder editModal() {
  Column() {
    this.modalOverlay(() => { this.showEditModal = false })
    Column() {
      Row() {
        Text('✏️ 编辑挂卖信息').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
        Column().layoutWeight(1)
        Text('✕').fontSize(18).fontColor('#999999')
          .onClick(() => { this.showEditModal = false })
      }
      .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
      Divider().color('#F0F0F0')

      Column() {
        Text('挂卖价格').fontSize(11).fontColor('#888888').width('100%').margin({ top: 12 })
        TextInput({ placeholder: '¥' + (this.selectedItem?.price ?? 0).toString() })
          .placeholderColor('#BBBBBB').fontSize(14).type(InputType.Number)
          .backgroundColor('#F5F5F7').borderRadius(8).height(40).width('100%').margin({ top: 4 })
          .onChange((v: string) => { this.formPrice = v })

        Text('成色选择').fontSize(11).fontColor('#888888').width('100%').margin({ top: 12 })
        Row() {
          Text('全新未拆').fontSize(11)
            .fontColor(this.selectedCondition === '全新未拆' ? '#FFFFFF' : '#6A1B9A')
            .backgroundColor(this.selectedCondition === '全新未拆' ? '#6A1B9A' : '#F3E5F5')
            .padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(12)
            .onClick(() => { this.selectedCondition = '全新未拆' })
          Text('已拆展示').fontSize(11)
            .fontColor(this.selectedCondition === '已拆展示' ? '#FFFFFF' : '#6A1B9A')
            .backgroundColor(this.selectedCondition === '已拆展示' ? '#6A1B9A' : '#F3E5F5')
            .padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(12).margin({ left: 8 })
            .onClick(() => { this.selectedCondition = '已拆展示' })
          Text('整套出').fontSize(11)
            .fontColor(this.selectedCondition === '整套出' ? '#FFFFFF' : '#6A1B9A')
            .backgroundColor(this.selectedCondition === '整套出' ? '#6A1B9A' : '#F3E5F5')
            .padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(12).margin({ left: 8 })
            .onClick(() => { this.selectedCondition = '整套出' })
        }
        .width('100%').margin({ top: 6 })

        Text('💡 建议定价:¥' + ((this.selectedItem?.price ?? 100) * 0.9).toFixed(0) + ' 更容易成交')
          .fontSize(10).fontColor('#FFD54F').width('100%').margin({ top: 10 })
      }
      .padding({ left: 16, right: 16 })

      Row() {
        Text('取消').fontSize(13).fontColor('#888888').backgroundColor('#F5F5F5')
          .borderRadius(18).padding({ left: 24, right: 24, top: 9, bottom: 9 })
          .onClick(() => { this.showEditModal = false })
        Text('保存修改').fontSize(13).fontColor('#FFFFFF').backgroundColor('#6A1B9A')
          .borderRadius(18).padding({ left: 24, right: 24, top: 9, bottom: 9 })
          .margin({ left: 10 })
          .onClick(() => { this.showEditModal = false })
      }
      .width('100%').justifyContent(FlexAlign.Center)
      .padding({ left: 16, right: 16, top: 16, bottom: 16 })
    }
    .width('88%').backgroundColor('#FFFFFF').borderRadius(16)
    .alignItems(HorizontalAlign.Center)
    .position({ x: '6%', y: '18%' })
  }
  .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
}

编辑弹窗集成了TextInput数字输入框用于价格修改,通过onChange回调实时更新formPrice状态。成色选择采用三选一切换按钮组,选中项以紫色填充背景、白色文字区分。智能定价建议通过((this.selectedItem?.price ?? 100) * 0.9).toFixed(0)计算原价九折的建议价,以金色提示文案展示,引导用户合理定价以提升成交率。

九、概率公示与柱状图可视化

概率公示模块ProbabilityContent是应用的合规核心,通过进度条和柱状图可视化展示各系列稀有度分布。

@Component
struct ProbabilityContent {
  @State showRuleModal: boolean = false
  probabilityData: number[] = [2, 8, 15, 25, 35, 45, 55]
  weekLabels: string[] = ['一', '二', '三', '四', '五', '六', '日']

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Text('📊').fontSize(20)
            Text('概率公示').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ left: 6 })
            Column().layoutWeight(1)
            Text('规则说明').fontSize(11).fontColor('#FFFFFF')
              .backgroundColor('rgba(255,255,255,0.2)')
              .padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(10)
              .onClick(() => { this.showRuleModal = true })
          }
          .width('100%').padding({ left: 14, right: 14, top: 12, bottom: 14 })
        }
        .width('100%').linearGradient(PURPLE_GRADIENT)

        Scroll() {
          Column() {
            Column() {
              Text('💎 各系列稀有度分布').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
                .width('100%').padding({ left: 16, top: 12, bottom: 8 })

              Column() {
                Row() { Text('🎀 MOLLY 星际旅行').fontSize(11).fontColor('#E91E63').layoutWeight(1)
                  Text('隐藏 1/144').fontSize(11).fontColor('#888888') }
                Row() { Column().width('45%').height(6).backgroundColor('#E91E63').borderRadius(3)
                  Column().layoutWeight(1) }
                .width('100%').margin({ top: 4, bottom: 10 })

                Row() { Text('🦷 LABUBU 精灵三园').fontSize(11).fontColor('#6A1B9A').layoutWeight(1)
                  Text('隐藏 1/144').fontSize(11).fontColor('#888888') }
                Row() { Column().width('38%').height(6).backgroundColor('#6A1B9A').borderRadius(3)
                  Column().layoutWeight(1) }
                .width('100%').margin({ top: 4, bottom: 10 })

                Row() { Text('💀 SKULLPANDA 暗夜').fontSize(11).fontColor('#424242').layoutWeight(1)
                  Text('隐藏 1/144').fontSize(11).fontColor('#888888') }
                Row() { Column().width('30%').height(6).backgroundColor('#424242').borderRadius(3)
                  Column().layoutWeight(1) }
                .width('100%').margin({ top: 4, bottom: 10 })
              }
              .padding({ left: 16, right: 16, bottom: 14 })
            }
            .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
            .margin({ left: 12, right: 12, top: 8 })

            Column() {
              Text('📈 本周开出隐藏款数').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
                .width('100%').padding({ left: 16, top: 12, bottom: 8 })
              Row() {
                ForEach([0, 1, 2, 3, 4, 5, 6], (d: number) => {
                  Column() {
                    Text(this.probabilityData[d].toString()).fontSize(9).fontColor('#6A1B9A')
                    Column()
                      .width(22)
                      .height((this.probabilityData[d] / 60 * 64).toFixed(0) + 'vp')
                      .backgroundColor(d === 4 ? '#6A1B9A' : '#CE93D8')
                      .borderRadius({ topLeft: 4, topRight: 4 })
                    Text(this.weekLabels[d]).fontSize(8).fontColor('#999999').margin({ top: 3 })
                  }
                  .layoutWeight(1).alignItems(HorizontalAlign.Center)
                })
              }
              .width('100%').padding({ left: 12, right: 12, bottom: 12 })
            }
            .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
            .margin({ left: 12, right: 12, top: 8 })
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showRuleModal) { this.ruleModal() }
    }
    .width('100%').height('100%')
  }
}

概率公示页面通过双重视觉呈现稀有度数据。各系列稀有度分布采用进度条形式,每个系列以品牌色填充对应宽度比例的进度条(MOLLY 45%、LABUBU 38%、SKULLPANDA 30%等),直观展示不同系列隐藏款的开出概率差异。本周开盒统计柱状图通过ForEach遍历7天数据,柱子高度通过(this.probabilityData[d] / 60 * 64).toFixed(0) + 'vp'动态计算,星期五(索引4)以深紫色高亮区分其他天的浅紫色。柱状图底部使用中文数字标签"一"到"日"标注星期,符合中文用户的阅读习惯。

十、消息中心与个人中心

消息中心MessageContent以列表形式展示拼盒通知、开盒结果、系统消息等通知。个人中心ProfileContent集成了用户信息展示、订单状态宫格、功能菜单和编辑资料弹窗。

@Component
struct ProfileContent {
  @State showEditProfile: boolean = false
  @State formNick: string = ''
  orderStatus: string[] = ['待付款', '待发货', '待收货', '待评价', '售后']
  orderIcons: string[] = ['💰', '📦', '🚚', '⭐', '🛠️']
  menuLabels: string[] = ['我的拼盒', '我的收藏', '开盒记录', '二手挂卖', '优惠券', '地址管理', '客服中心', '设置']
  menuIcons: string[] = ['🤝', '❤️', '🎁', '♻️', '🎟️', '📍', '💬', '⚙️']

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Column() {
              Text('🎁').fontSize(34)
            }
            .width(60).height(60).backgroundColor('rgba(255,255,255,0.25)').borderRadius(30)
            .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

            Column() {
              Row() {
                Text('盲盒收藏家').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
                Text('LV.6').fontSize(9).fontColor('#FFFFFF').backgroundColor('rgba(0,0,0,0.25)')
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(8).margin({ left: 8 })
              }
              Text('已开盒 234 次 · 隐藏款 5 个').fontSize(10).fontColor('rgba(255,255,255,0.9)').margin({ top: 4 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })

            Text('编辑').fontSize(11).fontColor('#FFFFFF').backgroundColor('rgba(255,255,255,0.25)')
              .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
              .onClick(() => { this.showEditProfile = true })
          }
          .width('100%').padding({ left: 16, right: 16, top: 16, bottom: 16 })
        }
        .width('100%').linearGradient(PURPLE_GRADIENT)

        Column() {
          Row() {
            Text('我的订单').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            Column().layoutWeight(1)
            Text('全部订单 >').fontSize(10).fontColor('#999999')
          }
          .width('100%').padding({ left: 16, top: 12, bottom: 8 })
          Row() {
            ForEach([0, 1, 2, 3, 4], (i: number) => {
              Column() {
                Text(this.orderIcons[i]).fontSize(20)
                Text(this.orderStatus[i]).fontSize(9).fontColor('#555555').margin({ top: 4 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
            })
          }
          .width('100%').padding({ left: 8, right: 8, bottom: 12 })
        }
        .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
        .margin({ left: 12, right: 12, top: 8 })

        Column() {
          Row() {
            ForEach([0, 1, 2, 3], (i: number) => {
              Column() {
                Text(this.menuIcons[i]).fontSize(20)
                Text(this.menuLabels[i]).fontSize(9).fontColor('#555555').margin({ top: 4 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              .padding({ top: 14, bottom: 14 })
            })
          }
          .width('100%')
          Divider().color('#F5F5F5')
          Row() {
            ForEach([4, 5, 6, 7], (i: number) => {
              Column() {
                Text(this.menuIcons[i]).fontSize(20)
                Text(this.menuLabels[i]).fontSize(9).fontColor('#555555').margin({ top: 4 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              .padding({ top: 14, bottom: 14 })
            })
          }
          .width('100%')
        }
        .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
        .margin({ left: 12, right: 12, top: 8 })

        Text('潮玩盲盒星球 v1.0 · 概率透明 · 2026').fontSize(9).fontColor('#CCCCCC')
          .width('100%').textAlign(TextAlign.Center).margin({ top: 14, bottom: 10 })
      }
      .width('100%').height('100%')

      if (this.showEditProfile) { this.editProfileModal() }
    }
    .width('100%').height('100%')
  }
}

个人中心头部采用渐变背景展示用户头像、昵称、等级和开盒统计。订单状态区通过数组索引遍历渲染5个状态入口,功能宫格分为两行各4个入口,中间以Divider分隔。编辑资料弹窗提供头像选择和昵称输入功能,通过TextInputonChange回调实时捕获输入值。页面底部展示应用版本与"概率透明"的品牌理念标语。

应用核心流程图

HOME

PRESALE

GROUP

SECONDHAND

PROBABILITY

MESSAGE

PROFILE

编辑

删除

应用启动 BlindBoxApp

底部Tab导航 activeTab

当前Tab判断

盲盒广场 HomeContent

限量预售 PresaleContent

拼盒组队 GroupContent

二手交易 SecondHandContent

概率公示 ProbabilityContent

消息中心 MessageContent

个人中心 ProfileContent

渐变头部 + 快捷入口

热门系列横滑

瀑布双列商品

点击商品

详情弹窗 detailModal

模拟抽盒

抽盒弹窗 drawModal

Math.random随机结果

暗色头部 + 倒计时

预售卡片列表

预约弹窗 presaleModal

拼盒列表

成员槽位 + 进度条

拼盒邀请弹窗 groupModal

二手商品列表

编辑/删除

编辑弹窗 editModal

删除确认弹窗

稀有度分布进度条

本周柱状图

规则说明弹窗

用户信息 + 订单宫格

功能菜单宫格

编辑资料弹窗

技术点对比分析

技术维度 盲盒广场模块 限量预售模块 拼盒组队模块 概率公示模块
核心装饰器 @State + @Builder @State + @Builder @State + @Builder @State + 普通属性
弹窗类型 居中卡片(drawModal/detailModal) 居中卡片(presaleModal) 底部滑出(groupModal) 居中卡片(ruleModal)
弹窗定位 position百分比居中 position百分比居中 position y:55%底部 position百分比居中
渐变方案 PURPLE_GRADIENT(紫粉) DARK_GRADIENT(暗紫) PURPLE_GRADIENT(紫粉) PURPLE_GRADIENT(紫粉)
数据驱动 BoxItem实例 + null安全 BoxItem实例 + 倒计时字符串 GroupItem接口 + members数组 静态数组 + 百分比计算
交互特色 Math.random模拟抽盒 倒计时数字方块 成员槽位动态渲染+进度条 进度条+柱状图双可视化
列表布局 瀑布双列(Row+Column) 垂直列表 垂直列表 垂直分组
状态管理 showDetail/showDraw showPresaleModal showGroupModal showRuleModal
null安全 ?.操作符+默认值 ?.操作符+默认值 ?.操作符+默认值 直接数组索引
概率展示 三栏概率卡(1/144/1/36/1/12) 进度条百分比 进度条+柱状图

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============================================================================
// 拼多多风格·潮玩盲盒星球
// 潮玩盲盒交易社区:盲盒购买 / 拼盒组队 / 概率公示 / 二手交易
// 配色:潮玩紫 #6A1B9A / 荧光粉 #E91E63 / 潮玩金 #FFD54F / 暗夜紫 #1A1A2E
// ============================================================================

// ============================ Interface 定义 ================================

interface QuickNavMeta {
  label: string
  icon: string
  bg: string
}

interface RarityMeta {
  label: string
  icon: string
  color: string
  bg: string
  probability: string
}

interface SeriesMeta {
  label: string
  icon: string
  color: string
}

interface BlindBoxSeries {
  id: number
  name: string
  tag: string
  price: number
  originalPrice: number
  sales: number
  icon: string
  bg: string
  series: string
  rating: number
  stock: number
  hiddenCount: number
}

interface GroupItem {
  id: number
  boxName: string
  icon: string
  bg: string
  price: number
  joinedCount: number
  totalCount: number
  endTime: string
  members: string[]
}

interface SecondHandItem {
  id: number
  name: string
  condition: string
  price: number
  icon: string
  bg: string
  seller: string
  verified: boolean
  views: number
}

interface MessageItem {
  id: number
  type: string
  title: string
  content: string
  icon: string
  time: string
  unread: boolean
}

interface ModalConfig {
  title: string
  desc: string
}

// ============================ 数据模型 ================================

@Observed
export class BoxItem {
  id: number = 0
  name: string = ''
  tag: string = ''
  price: number = 0
  originalPrice: number = 0
  sales: number = 0
  icon: string = ''
  bg: string = ''
  series: string = ''
  rating: number = 0
  stock: number = 0
  hiddenCount: number = 0
  constructor(id: number, name: string, tag: string, price: number, originalPrice: number, sales: number, icon: string, bg: string, series: string, rating: number, stock: number, hiddenCount: number) {
    this.id = id
    this.name = name
    this.tag = tag
    this.price = price
    this.originalPrice = originalPrice
    this.sales = sales
    this.icon = icon
    this.bg = bg
    this.series = series
    this.rating = rating
    this.stock = stock
    this.hiddenCount = hiddenCount
  }
}

// ============================ 设计令牌 ================================

const PURPLE_GRADIENT: LinearGradientOptions = {
  angle: 135,
  colors: [['#6A1B9A', 0.0], ['#E91E63', 1.0]]
}

const DARK_GRADIENT: LinearGradientOptions = {
  angle: 160,
  colors: [['#1A1A2E', 0.0], ['#4A148C', 1.0]]
}

const QUICK_NAVS: QuickNavMeta[] = [
  { label: '今日开盒', icon: '🎁', bg: '#F3E5F5' },
  { label: '隐藏概率', icon: '💎', bg: '#FFF8E1' },
  { label: '拼盒组队', icon: '🤝', bg: '#FCE4EC' },
  { label: '二手回收', icon: '♻️', bg: '#E8F5E9' }
]

const RARITY_CONFIG: Record<string, RarityMeta> = {
  '隐藏款': { label: '隐藏款', icon: '💎', color: '#FFD54F', bg: '#FFF8E1', probability: '1/144' },
  '稀有款': { label: '稀有款', icon: '👑', color: '#E91E63', bg: '#FCE4EC', probability: '1/36' },
  '普通款': { label: '普通款', icon: '🎯', color: '#6A1B9A', bg: '#F3E5F5', probability: '1/12' }
}

const SERIES_CONFIG: Record<string, SeriesMeta> = {
  'MOLLY': { label: 'MOLLY', icon: '🎀', color: '#E91E63' },
  'LABUBU': { label: 'LABUBU', icon: '🦷', color: '#6A1B9A' },
  'SKULLPANDA': { label: 'SKULLPANDA', icon: '💀', color: '#424242' },
  'DIMOO': { label: 'DIMOO', icon: '☁️', color: '#42A5F5' },
  'HIRONO': { label: 'HIRONO', icon: '🌙', color: '#7B1FA2' },
  'CRYBABY': { label: 'CRYBABY', icon: '😢', color: '#FF7043' }
}

const SERIES_FILTER: string[] = ['全部', 'MOLLY', 'LABUBU', 'SKULLPANDA', 'DIMOO', 'HIRONO']

const MODAL_INFO: Record<string, ModalConfig> = {
  'draw': { title: '🎁 模拟抽盒', desc: '消耗 1 次免费模拟机会' },
  'group': { title: '🤝 拼盒邀请', desc: '邀请好友一起拼盒更划算' },
  'delete': { title: '⚠️ 确认删除', desc: '删除后不可恢复' },
  'edit': { title: '✏️ 编辑潮玩', desc: '编辑收藏的潮玩信息' },
  'detail': { title: '📦 盲盒详情', desc: '查看完整系列信息' }
}

// ============================ 全局写死数据 ================================

const mockBoxes: BoxItem[] = [
  new BoxItem(1, 'MOLLY 星际旅行系列', '拼团爆款', 59, 99, 23000, '🎀', '#FCE4EC', 'MOLLY', 4.9, 144, 2),
  new BoxItem(2, 'LABUBU 精灵三园系列', '限量发售', 69, 119, 18000, '🦷', '#F3E5F5', 'LABUBU', 4.8, 72, 1),
  new BoxItem(3, 'SKULLPANDA 暗夜嘉年华', '热门IP', 79, 139, 15000, '💀', '#ECEFF1', 'SKULLPANDA', 4.7, 96, 3),
  new BoxItem(4, 'DIMOO 云朵马戏团系列', '新品上架', 59, 109, 12000, '☁️', '#E3F2FD', 'DIMOO', 4.8, 120, 2),
  new BoxItem(5, 'HIRONO 小野系列五', '经典复刻', 49, 89, 28000, '🌙', '#F3E5F5', 'HIRONO', 4.6, 200, 4),
  new BoxItem(6, 'CRYBABY 眼泪公主系列', '女生最爱', 55, 95, 16000, '😢', '#FBE9E7', 'CRYBABY', 4.7, 88, 1),
  new BoxItem(7, 'MOLLY 婚礼系列二代', '婚庆主题', 75, 129, 9800, '💍', '#F8BBD0', 'MOLLY', 4.9, 60, 2),
  new BoxItem(8, 'LABUBU 坐坐派对系列', '超值拼团', 39, 69, 45000, '🎉', '#FFF3E0', 'LABUBU', 4.5, 300, 5),
  new BoxItem(9, 'SKULLPANDA 熊喵热潮', '联名款', 89, 149, 8600, '🐼', '#EFEBE9', 'SKULLPANDA', 4.8, 48, 1),
  new BoxItem(10, 'DIMOO 太空漫游系列', '科幻迷', 65, 115, 11000, '🚀', '#E1F5FE', 'DIMOO', 4.7, 100, 3),
  new BoxItem(11, 'HIRONO 海边度假系列', '夏日限定', 59, 99, 14000, '🏖️', '#E0F7FA', 'HIRONO', 4.6, 110, 2),
  new BoxItem(12, 'CRYBABY 午茶时光系列', '下午茶', 45, 79, 21000, '🍰', '#FFF8E1', 'CRYBABY', 4.5, 180, 4)
]

const mockGroups: GroupItem[] = [
  { id: 1, boxName: 'LABUBU 坐坐派对系列', icon: '🎉', bg: '#FFF3E0', price: 29, joinedCount: 3, totalCount: 6, endTime: '23:59:59', members: ['🦷', '🎀', '☁️', '💀', '🌙', '😢'] },
  { id: 2, boxName: 'MOLLY 星际旅行系列', icon: '🎀', bg: '#FCE4EC', price: 39, joinedCount: 5, totalCount: 8, endTime: '22:30:00', members: ['🎀', '☁️', '🌙', '😢', '💀'] },
  { id: 3, boxName: 'DIMOO 云朵马戏团', icon: '☁️', bg: '#E3F2FD', price: 35, joinedCount: 2, totalCount: 5, endTime: '23:45:00', members: ['☁️', '🌙'] },
  { id: 4, boxName: 'HIRONO 小野系列五', icon: '🌙', bg: '#F3E5F5', price: 25, joinedCount: 4, totalCount: 6, endTime: '21:15:00', members: ['🌙', '🎀', '🦷', '☁️', '💀', '😢'] },
  { id: 5, boxName: 'SKULLPANDA 暗夜嘉年华', icon: '💀', bg: '#ECEFF1', price: 45, joinedCount: 1, totalCount: 4, endTime: '23:59:00', members: ['💀'] },
  { id: 6, boxName: 'CRYBABY 眼泪公主系列', icon: '😢', bg: '#FBE9E7', price: 32, joinedCount: 6, totalCount: 10, endTime: '20:00:00', members: ['😢', '🎀', '🦷', '☁️', '🌙', '💀', '🎉', '🍰', '🐼', '🚀'] }
]

const mockSecondHand: SecondHandItem[] = [
  { id: 1, name: 'MOLLY 星际隐藏款', condition: '全新未拆', price: 399, icon: '🎀', bg: '#FCE4EC', seller: '泡泡玛特收藏家', verified: true, views: 2341 },
  { id: 2, name: 'LABUBU 精灵三园隐藏', condition: '已拆展示', price: 289, icon: '🦷', bg: '#F3E5F5', seller: '盲盒小姐姐', verified: true, views: 1876 },
  { id: 3, name: 'SKULLPANDA 稀有款', condition: '全新未拆', price: 189, icon: '💀', bg: '#ECEFF1', seller: '潮玩达人', verified: false, views: 987 },
  { id: 4, name: 'DIMOO 马戏团全套', condition: '整套出', price: 459, icon: '☁️', bg: '#E3F2FD', seller: '收藏家小王', verified: true, views: 3102 },
  { id: 5, name: 'HIRONO 小野系列隐藏', condition: '已拆完好', price: 159, icon: '🌙', bg: '#F3E5F5', seller: '夜猫子', verified: false, views: 654 },
  { id: 6, name: 'CRYBABY 眼泪公主隐藏', condition: '全新未拆', price: 269, icon: '😢', bg: '#FBE9E7', seller: '爱哭鬼', verified: true, views: 1498 },
  { id: 7, name: 'MOLLY 婚礼系列全套', condition: '整套带盒', price: 599, icon: '💍', bg: '#F8BBD0', seller: '婚庆潮人', verified: true, views: 2234 },
  { id: 8, name: 'LABUBU 坐坐全套12个', condition: '整套出', price: 399, icon: '🎉', bg: '#FFF3E0', seller: '拼团王者', verified: false, views: 876 }
]

const mockMessages: MessageItem[] = [
  { id: 1, type: '拼盒', title: '拼盒成功通知', content: 'LABUBU 坐坐派对系列 已成团,发货中', icon: '🤝', time: '3分钟前', unread: true },
  { id: 2, type: '开盒', title: '恭喜开出稀有款', content: '您在 MOLLY 星际旅行系列中开出稀有款', icon: '💎', time: '25分钟前', unread: true },
  { id: 3, type: '系统', title: '新品上架提醒', content: 'SKULLPANDA 熊喵热潮系列 已上架', icon: '🔔', time: '1小时前', unread: false },
  { id: 4, type: '交易', title: '二手交易通知', content: '您的收藏 MOLLY 隐藏款有新的出价', icon: '💰', time: '2小时前', unread: false },
  { id: 5, type: '拼盒', title: '拼盒邀请', content: '好友邀请您加入 DIMOO 云朵马戏团拼盒', icon: '🎉', time: '3小时前', unread: false },
  { id: 6, type: '系统', title: '概率公示更新', content: '本周各系列概率已更新,请查看', icon: '📊', time: '昨天', unread: false }
]

const PROBABILITY_DATA: number[] = [2, 8, 15, 25, 35, 45, 55]
const WEEK_LABELS: string[] = ['一', '二', '三', '四', '五', '六', '日']
const DRAW_SIMULATE: string[] = ['🎀', '☁️', '🦷', '💀', '🌙', '😢', '🎉', '🐼', '🚀', '🍰', '💎', '👑']

// ============================ 工具函数 ================================

function fmtPrice(p: number): string {
  return '¥' + p.toFixed(2)
}

function fmtSales(s: number): string {
  if (s >= 10000) {
    return (s / 10000).toFixed(1) + '万'
  }
  return s.toString()
}

function getSeriesIcon(s: string): string {
  const meta = SERIES_CONFIG[s]
  if (meta) {
    return meta.icon
  }
  return '📦'
}

function getRarityColor(r: string): string {
  const meta = RARITY_CONFIG[r]
  if (meta) {
    return meta.color
  }
  return '#6A1B9A'
}

// ============================ 底部 Tab 枚举 ================================

enum MainTab {
  HOME = 0,
  PRESALE = 1,
  GROUP = 2,
  SECONDHAND = 3,
  PROBABILITY = 4,
  MESSAGE = 5,
  PROFILE = 6
}

// ============================ 入口页面 ================================

@Entry
@Component
struct BlindBoxApp {
  @State activeTab: MainTab = MainTab.HOME

  @Builder contentArea() {
    Column() {
      if (this.activeTab === MainTab.HOME) {
        HomeContent()
      } else if (this.activeTab === MainTab.PRESALE) {
        PresaleContent()
      } else if (this.activeTab === MainTab.GROUP) {
        GroupContent()
      } else if (this.activeTab === MainTab.SECONDHAND) {
        SecondHandContent()
      } else if (this.activeTab === MainTab.PROBABILITY) {
        ProbabilityContent()
      } else if (this.activeTab === MainTab.MESSAGE) {
        MessageContent()
      } else {
        ProfileContent()
      }
    }
    .layoutWeight(1)
  }

  @Builder bottomTabItem(icon: string, label: string, tab: MainTab) {
    Column() {
      Text(icon).fontSize(18).opacity(this.activeTab === tab ? 1.0 : 0.45)
      Text(label).fontSize(8)
        .fontColor(this.activeTab === tab ? '#6A1B9A' : '#999999')
        .fontWeight(this.activeTab === tab ? FontWeight.Bold : FontWeight.Normal)
        .margin({ top: 1 })
      if (this.activeTab === tab) {
        Column().width(14).height(3).backgroundColor('#6A1B9A').borderRadius(2).margin({ top: 2 })
      }
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .padding({ top: 4, bottom: 4 })
    .onClick(() => { this.activeTab = tab })
  }

  build() {
    Column() {
      this.contentArea()
      Row() {
        this.bottomTabItem('🏠', '广场', MainTab.HOME)
        this.bottomTabItem('⏰', '预售', MainTab.PRESALE)
        this.bottomTabItem('🤝', '拼盒', MainTab.GROUP)
        this.bottomTabItem('♻️', '二手', MainTab.SECONDHAND)
        this.bottomTabItem('📊', '概率', MainTab.PROBABILITY)
        this.bottomTabItem('💬', '消息', MainTab.MESSAGE)
        this.bottomTabItem('👤', '我的', MainTab.PROFILE)
      }
      .width('100%')
      .backgroundColor('#FFFFFF')
      .padding({ top: 4, bottom: 6 })
      .shadow({ radius: 8, color: '#1A000000', offsetY: -2 })
    }
    .width('100%').height('100%')
    .backgroundColor('#F5F5F7')
  }
}

// ============================ Tab1:盲盒广场 ================================

@Component
struct HomeContent {
  @State showDetail: boolean = false
  @State showDraw: boolean = false
  @State selectedBox: BoxItem | null = null
  @State drawResult: string = '🎀'

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.55)')
      .onClick(onClose)
  }

  // 弹框1:抽盒模拟(居中卡片+概率展示)
  @Builder drawModal() {
    Column() {
      this.modalOverlay(() => { this.showDraw = false })
      Column() {
        Column() {
          Text('🎁').fontSize(56).margin({ top: 24 })
          Text('模拟抽盒').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
            .margin({ top: 8 })
          Text('单盒 ¥' + (this.selectedBox?.price ?? 59).toString()).fontSize(12)
            .fontColor('rgba(255,255,255,0.8)').margin({ top: 4 })
        }
        .width('100%').padding({ bottom: 24 })
        .linearGradient(PURPLE_GRADIENT)
        .borderRadius({ topLeft: 16, topRight: 16 })

        Column() {
          Text(this.drawResult).fontSize(72).margin({ top: 20 })
          Text('模拟结果').fontSize(11).fontColor('#999999').margin({ top: 8 })
          Text('概率仅供参考,实际以公示为准').fontSize(9).fontColor('#CCCCCC').margin({ top: 4 })

          Row() {
            Column() {
              Text('💎').fontSize(20)
              Text('隐藏款').fontSize(9).fontColor('#FFD54F').margin({ top: 2 })
              Text('1/144').fontSize(8).fontColor('#BBBBBB').margin({ top: 1 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            .backgroundColor('#FFF8E1').borderRadius(10).padding({ top: 10, bottom: 10 })

            Column() {
              Text('👑').fontSize(20)
              Text('稀有款').fontSize(9).fontColor('#E91E63').margin({ top: 2 })
              Text('1/36').fontSize(8).fontColor('#BBBBBB').margin({ top: 1 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            .backgroundColor('#FCE4EC').borderRadius(10).padding({ top: 10, bottom: 10 })
            .margin({ left: 8 })

            Column() {
              Text('🎯').fontSize(20)
              Text('普通款').fontSize(9).fontColor('#6A1B9A').margin({ top: 2 })
              Text('1/12').fontSize(8).fontColor('#BBBBBB').margin({ top: 1 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Center)
            .backgroundColor('#F3E5F5').borderRadius(10).padding({ top: 10, bottom: 10 })
            .margin({ left: 8 })
          }
          .width('100%').margin({ top: 20 })

          Row() {
            Text('再抽一次').fontSize(13).fontColor('#6A1B9A').backgroundColor('#F3E5F5')
              .borderRadius(18).padding({ left: 24, right: 24, top: 10, bottom: 10 })
              .onClick(() => {
                const idx = Math.floor(Math.random() * DRAW_SIMULATE.length)
                this.drawResult = DRAW_SIMULATE[idx]
              })
            Text('立即购买').fontSize(13).fontColor('#FFFFFF').backgroundColor('#E91E63')
              .borderRadius(18).padding({ left: 24, right: 24, top: 10, bottom: 10 })
              .margin({ left: 10 })
              .onClick(() => { this.showDraw = false })
          }
          .width('100%').justifyContent(FlexAlign.Center)
          .margin({ top: 20, bottom: 20 })
        }
        .padding({ left: 16, right: 16 })
      }
      .width('82%').backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '9%', y: '12%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  // 弹框2:盲盒详情
  @Builder detailModal() {
    Column() {
      this.modalOverlay(() => { this.showDetail = false })
      Column() {
        Stack() {
          Column() {
            Text(this.selectedBox?.icon ?? '🎁').fontSize(64)
          }
          .width('100%').height(140)
          .backgroundColor(this.selectedBox?.bg ?? '#F3E5F5')
          .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
          if (this.selectedBox?.tag !== '') {
            Text(this.selectedBox?.tag ?? '').fontSize(10).fontColor('#FFFFFF').backgroundColor('#E91E63')
              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .borderRadius({ topRight: 10, bottomLeft: 10 })
              .position({ x: 0, y: 0 })
          }
        }
        .width('100%').height(140).borderRadius({ topLeft: 16, topRight: 16 })

        Column() {
          Text(this.selectedBox?.name ?? '').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
            .width('100%')
          Row() {
            Text(fmtPrice(this.selectedBox?.price ?? 0)).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#E91E63')
            Text(fmtPrice(this.selectedBox?.originalPrice ?? 0)).fontSize(11).fontColor('#BBBBBB')
              .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 6 })
            Column().layoutWeight(1)
            Text('已拼' + fmtSales(this.selectedBox?.sales ?? 0) + '件').fontSize(10).fontColor('#999999')
          }
          .width('100%').margin({ top: 6 })

          Row() {
            Text(getSeriesIcon(this.selectedBox?.series ?? '') + ' ' + (this.selectedBox?.series ?? ''))
              .fontSize(11).fontColor('#6A1B9A').backgroundColor('#F3E5F5')
              .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(10)
            Text('💎 隐藏x' + (this.selectedBox?.hiddenCount ?? 0).toString())
              .fontSize(11).fontColor('#FFD54F').backgroundColor('#FFF8E1')
              .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(10).margin({ left: 6 })
            Text('⭐' + (this.selectedBox?.rating ?? 0).toString())
              .fontSize(11).fontColor('#66BB6A').backgroundColor('#E8F5E9')
              .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(10).margin({ left: 6 })
          }
          .width('100%').margin({ top: 10 })

          Row() {
            Text('模拟抽盒').fontSize(13).fontColor('#6A1B9A').backgroundColor('#F3E5F5')
              .borderRadius(18).padding({ left: 20, right: 20, top: 9, bottom: 9 })
              .onClick(() => { this.showDetail = false; this.showDraw = true })
            Text('立即拼单').fontSize(13).fontColor('#FFFFFF').backgroundColor('#E91E63')
              .borderRadius(18).padding({ left: 20, right: 20, top: 9, bottom: 9 })
              .margin({ left: 10 })
              .onClick(() => { this.showDetail = false })
          }
          .width('100%').justifyContent(FlexAlign.Center).margin({ top: 16 })
        }
        .padding({ left: 16, right: 16, top: 12, bottom: 16 })
        .alignItems(HorizontalAlign.Start)
      }
      .width('88%').backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '6%', y: '15%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder boxCard(b: BoxItem) {
    Column() {
      Stack() {
        Column() {
          Text(b.icon).fontSize(42)
        }
        .width('100%').height(96)
        .backgroundColor(b.bg)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
        if (b.tag !== '') {
          Text(b.tag).fontSize(8).fontColor('#FFFFFF').backgroundColor('#E91E63')
            .padding({ left: 5, right: 5, top: 2, bottom: 2 })
            .borderRadius({ topRight: 8, bottomLeft: 8 })
        }
      }
      .width('100%').height(96)

      Column() {
        Text(b.name).fontSize(12).fontWeight(FontWeight.Medium).fontColor('#212121')
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%')
        Row() {
          Text(fmtPrice(b.price)).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#E91E63')
          Text(fmtPrice(b.originalPrice)).fontSize(9).fontColor('#BBBBBB')
            .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 3 })
          Column().layoutWeight(1)
        }
        .width('100%').margin({ top: 4 })
        Row() {
          Text('💎x' + b.hiddenCount.toString()).fontSize(9).fontColor('#FFD54F')
            .backgroundColor('#FFF8E1').padding({ left: 4, right: 4, top: 1, bottom: 1 }).borderRadius(6)
          Column().layoutWeight(1)
          Text(fmtSales(b.sales) + '人拼').fontSize(9).fontColor('#999999')
        }
        .width('100%').margin({ top: 5 })
      }
      .padding(8).alignItems(HorizontalAlign.Start)
    }
    .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
    .margin({ left: 6, right: 6, top: 6 })
    .shadow({ radius: 6, color: '#0D000000', offsetY: 2 })
    .onClick(() => { this.selectedBox = b; this.showDetail = true })
  }

  build() {
    Stack() {
      Column() {
        // 渐变头部
        Column() {
          Row() {
            Text('🎁').fontSize(20)
            Text('潮玩盲盒星球').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ left: 6 })
            Column().layoutWeight(1)
            Text('🔔').fontSize(19).margin({ left: 10 })
            Text('🛒').fontSize(19).margin({ left: 10 })
          }
          .width('100%').padding({ left: 12, right: 12, top: 12 })

          Row() {
            Text('🔍').fontSize(13).margin({ left: 8 })
            Text('搜索盲盒、系列、IP名称').fontSize(12).fontColor('rgba(255,255,255,0.6)').margin({ left: 6 })
          }
          .layoutWeight(1).height(34).backgroundColor('rgba(255,255,255,0.15)').borderRadius(17)
          .margin({ left: 12, right: 12, top: 8 })

          Row() {
            Text('💎').fontSize(14)
            Text('本周开出隐藏款 87 个').fontSize(11).fontWeight(FontWeight.Bold).fontColor('#FFD54F').margin({ left: 6 })
            Column().layoutWeight(1)
            Text('查看概率 >').fontSize(10).fontColor('rgba(255,255,255,0.8)')
          }
          .width('100%').padding({ left: 12, right: 12, top: 10, bottom: 12 })
        }
        .width('100%').linearGradient(PURPLE_GRADIENT)

        Scroll() {
          Column() {
            // 快捷入口
            Row() {
              ForEach(QUICK_NAVS, (q: QuickNavMeta) => {
                Column() {
                  Column() {
                    Text(q.icon).fontSize(20)
                  }
                  .width(40).height(40).backgroundColor(q.bg).borderRadius(20)
                  .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
                  Text(q.label).fontSize(10).fontColor('#555555').margin({ top: 4 })
                }
                .layoutWeight(1).alignItems(HorizontalAlign.Center).margin({ top: 10 })
              })
            }
            .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
            .margin({ left: 12, right: 12, top: 10 }).padding({ top: 6, bottom: 10 })

            // 热门系列横向滚动
            Row() {
              Text('🔥 热门系列').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
              Column().layoutWeight(1)
              Text('更多 >').fontSize(10).fontColor('#999999')
            }
            .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 6 })

            Scroll() {
              Row() {
                ForEach(SERIES_FILTER, (s: string) => {
                  Column() {
                    Column() {
                      Text(getSeriesIcon(s)).fontSize(28)
                    }
                    .width(60).height(60).backgroundColor('#F3E5F5').borderRadius(30)
                    .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)
                    Text(s).fontSize(10).fontColor('#333333').margin({ top: 5 })
                  }
                  .padding(8).backgroundColor('#FFFFFF').borderRadius(12)
                  .margin({ left: 6, right: 6 })
                  .shadow({ radius: 5, color: '#0D000000', offsetY: 2 })
                  .onClick(() => { this.selectedBox = mockBoxes[0]; this.showDetail = true })
                })
              }
              .padding({ left: 10, right: 10 })
            }
            .scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).height(110)
            .width('100%')

            // 瀑布双列
            Row() {
              Text('⚡ 拼团爆款').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
              Column().layoutWeight(1)
              Text('全部 >').fontSize(10).fontColor('#999999')
            }
            .width('100%').padding({ left: 16, right: 16, top: 10, bottom: 4 })

            Row() {
              Column() {
                this.boxCard(mockBoxes[0])
                this.boxCard(mockBoxes[2])
                this.boxCard(mockBoxes[4])
                this.boxCard(mockBoxes[6])
                this.boxCard(mockBoxes[8])
                this.boxCard(mockBoxes[10])
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              Column() {
                this.boxCard(mockBoxes[1])
                this.boxCard(mockBoxes[3])
                this.boxCard(mockBoxes[5])
                this.boxCard(mockBoxes[7])
                this.boxCard(mockBoxes[9])
                this.boxCard(mockBoxes[11])
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
            }
            .width('100%').alignItems(VerticalAlign.Top)
            .padding({ left: 6, right: 6, bottom: 16 })
          }
          .width('100%')
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showDetail) { this.detailModal() }
      if (this.showDraw) { this.drawModal() }
    }
    .width('100%').height('100%')
  }
}

// ============================ Tab2:限量预售 ================================

@Component
struct PresaleContent {
  @State showPresaleModal: boolean = false
  @State selectedPresale: BoxItem | null = null
  @State presaleHours: string = '12'
  @State presaleMins: string = '45'
  @State presaleSecs: string = '30'

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.55)')
      .onClick(onClose)
  }

  // 弹框:发售预约(暗色风格)
  @Builder presaleModal() {
    Column() {
      this.modalOverlay(() => { this.showPresaleModal = false })
      Column() {
        Column() {
          Text('⏰').fontSize(48).margin({ top: 20 })
          Text('限量发售预约').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ top: 8 })
          Text(this.selectedPresale?.name ?? '').fontSize(11).fontColor('rgba(255,255,255,0.8)').margin({ top: 4 })
        }
        .width('100%').linearGradient(DARK_GRADIENT)
        .borderRadius({ topLeft: 16, topRight: 16 })
        .padding({ bottom: 20 })

        Column() {
          Text('预约享优先购资格').fontSize(12).fontColor('#666666').margin({ top: 14 })

          Text('⏰ 12:45:30 后开抢').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E91E63')
            .margin({ top: 8 })

          Text('选择尺码').fontSize(11).fontColor('#888888').width('100%').margin({ top: 14 })
          Row() {
            Text('端盒').fontSize(11).fontColor('#FFFFFF').backgroundColor('#6A1B9A')
              .padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14)
            Text('单盒').fontSize(11).fontColor('#6A1B9A').backgroundColor('#F3E5F5')
              .padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14).margin({ left: 8 })
            Text('套装').fontSize(11).fontColor('#6A1B9A').backgroundColor('#F3E5F5')
              .padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14).margin({ left: 8 })
          }
          .width('100%').margin({ top: 6 })

          Text('⚠️ 预约不等于购买成功,开抢时请准时参与').fontSize(9).fontColor('#CCCCCC')
            .width('100%').margin({ top: 12 })

          Text('立即预约').fontSize(15).fontColor('#FFFFFF')
            .backgroundColor('#E91E63').borderRadius(22)
            .padding({ left: 60, right: 60, top: 11, bottom: 11 })
            .margin({ top: 16, bottom: 20 })
            .onClick(() => { this.showPresaleModal = false })
        }
        .padding({ left: 16, right: 16 })
      }
      .width('84%').backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '8%', y: '15%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder presaleCard(b: BoxItem, index: number) {
    Row() {
      Column() {
        Text(b.icon).fontSize(36)
      }
      .width(76).height(76).backgroundColor(b.bg).borderRadius(16)
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        Text(b.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%')
        Row() {
          Text('💎 隐藏x' + b.hiddenCount.toString()).fontSize(9).fontColor('#FFD54F')
            .backgroundColor('#FFF8E1').padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6)
          Text('限量' + b.stock.toString() + '份').fontSize(9).fontColor('#E91E63')
            .backgroundColor('#FCE4EC').padding({ left: 5, right: 5, top: 2, bottom: 2 })
            .borderRadius(6).margin({ left: 6 })
        }
        .margin({ top: 5 })

        Row() {
          Text(fmtPrice(b.price)).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E91E63')
          Text(fmtPrice(b.originalPrice)).fontSize(10).fontColor('#BBBBBB')
            .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 4 })
          Column().layoutWeight(1)
          Text(index < 3 ? '即将开抢' : '预约中').fontSize(10)
            .fontColor(index < 3 ? '#FFD54F' : '#6A1B9A')
            .backgroundColor(index < 3 ? '#FFF8E1' : '#F3E5F5')
            .padding({ left: 8, right: 8, top: 4, bottom: 4 }).borderRadius(10)
        }
        .width('100%').margin({ top: 5 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
    }
    .width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(12)
    .margin({ left: 12, right: 12, top: 6 })
    .shadow({ radius: 5, color: '#0D000000', offsetY: 2 })
    .onClick(() => { this.selectedPresale = b; this.showPresaleModal = true })
  }

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Text('⏰').fontSize(20)
            Text('限量预售').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ left: 6 })
            Column().layoutWeight(1)
            Text('今日 18:00 开抢').fontSize(10).fontColor('rgba(255,255,255,0.85)')
          }
          .width('100%')
          Row() {
            Text('距开抢').fontSize(10).fontColor('rgba(255,255,255,0.9)')
            Text(this.presaleHours).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              .width(30).height(26).backgroundColor('rgba(0,0,0,0.3)').borderRadius(6)
              .textAlign(TextAlign.Center).margin({ left: 6 })
            Text(':').fontSize(14).fontColor('#FFFFFF').margin({ left: 3 })
            Text(this.presaleMins).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              .width(30).height(26).backgroundColor('rgba(0,0,0,0.3)').borderRadius(6)
              .textAlign(TextAlign.Center).margin({ left: 3 })
            Text(':').fontSize(14).fontColor('#FFFFFF').margin({ left: 3 })
            Text(this.presaleSecs).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              .width(30).height(26).backgroundColor('rgba(0,0,0,0.3)').borderRadius(6)
              .textAlign(TextAlign.Center).margin({ left: 3 })
            Column().layoutWeight(1)
            Text('💎 限量抢购').fontSize(10).fontColor('#FFD54F')
          }
          .width('100%').margin({ top: 8 })
        }
        .width('100%').linearGradient(DARK_GRADIENT).padding({ left: 14, right: 14, top: 12, bottom: 14 })

        Scroll() {
          Column() {
            this.presaleCard(mockBoxes[0], 0)
            this.presaleCard(mockBoxes[1], 1)
            this.presaleCard(mockBoxes[2], 2)
            this.presaleCard(mockBoxes[3], 3)
            this.presaleCard(mockBoxes[4], 4)
            this.presaleCard(mockBoxes[5], 5)
            this.presaleCard(mockBoxes[6], 6)
            this.presaleCard(mockBoxes[7], 7)
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showPresaleModal) { this.presaleModal() }
    }
    .width('100%').height('100%')
  }
}

// ============================ Tab3:拼盒组队 ================================

@Component
struct GroupContent {
  @State showGroupModal: boolean = false
  @State selectedGroup: GroupItem | null = null

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.55)')
      .onClick(onClose)
  }

  // 弹框:拼盒邀请(底部滑出)
  @Builder groupModal() {
    Column() {
      this.modalOverlay(() => { this.showGroupModal = false })
      Column() {
        Row() {
          Text('🤝 拼盒邀请').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
          Column().layoutWeight(1)
          Text('✕').fontSize(18).fontColor('#999999')
            .onClick(() => { this.showGroupModal = false })
        }
        .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
        Divider().color('#F0F0F0')

        Column() {
          Row() {
            Column() {
              Text(this.selectedGroup?.icon ?? '🎁').fontSize(32)
            }
            .width(64).height(64).backgroundColor(this.selectedGroup?.bg ?? '#F3E5F5').borderRadius(12)
            .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

            Column() {
              Text(this.selectedGroup?.boxName ?? '').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
              Text('拼单价 ¥' + (this.selectedGroup?.price ?? 0).toString()).fontSize(14)
                .fontWeight(FontWeight.Bold).fontColor('#E91E63').margin({ top: 3 })
              Text('截止 ' + (this.selectedGroup?.endTime ?? '')).fontSize(10).fontColor('#999999').margin({ top: 2 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
          }
          .width('100%')

          Row() {
            ForEach([0, 1, 2, 3, 4, 5], (i: number) => {
              Column() {
                if (i < (this.selectedGroup?.joinedCount ?? 0)) {
                  Text(this.selectedGroup?.members[i] ?? '🎁').fontSize(18)
                    .width(36).height(36).borderRadius(18).backgroundColor('#F3E5F5')
                    .textAlign(TextAlign.Center)
                } else {
                  Text('+').fontSize(18).fontColor('#CCCCCC')
                    .width(36).height(36).borderRadius(18).backgroundColor('#F5F5F5')
                    .textAlign(TextAlign.Center)
                }
                Text(i < (this.selectedGroup?.joinedCount ?? 0) ? '已参团' : '待加入')
                  .fontSize(8).fontColor('#999999').margin({ top: 3 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
            })
          }
          .width('100%').margin({ top: 16 })

          Text('还差 ' + ((this.selectedGroup?.totalCount ?? 6) - (this.selectedGroup?.joinedCount ?? 0)).toString() + ' 人成团')
            .fontSize(13).fontColor('#E91E63').margin({ top: 10 })

          Row() {
            Text('取消').fontSize(13).fontColor('#888888').backgroundColor('#F5F5F5')
              .borderRadius(18).padding({ left: 28, right: 28, top: 10, bottom: 10 })
              .onClick(() => { this.showGroupModal = false })
            Text('邀请好友参团').fontSize(13).fontColor('#FFFFFF').backgroundColor('#E91E63')
              .borderRadius(18).padding({ left: 28, right: 28, top: 10, bottom: 10 })
              .margin({ left: 10 })
              .onClick(() => { this.showGroupModal = false })
          }
          .width('100%').justifyContent(FlexAlign.Center)
          .margin({ top: 16, bottom: 20 })
        }
        .padding({ left: 16, right: 16 })
      }
      .width('100%').backgroundColor('#FFFFFF').borderRadius({ topLeft: 16, topRight: 16 })
      .position({ x: 0, y: '55%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder groupCard(g: GroupItem) {
    Column() {
      Row() {
        Column() {
          Text(g.icon).fontSize(30)
        }
        .width(60).height(60).backgroundColor(g.bg).borderRadius(12)
        .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

        Column() {
          Text(g.boxName).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%')
          Row() {
            Text('¥' + g.price.toString()).fontSize(15).fontWeight(FontWeight.Bold).fontColor('#E91E63')
            Text('/人').fontSize(10).fontColor('#999999')
            Column().layoutWeight(1)
            Text('截止 ' + g.endTime).fontSize(9).fontColor('#999999')
          }
          .width('100%').margin({ top: 4 })
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 10 })
      }
      .width('100%')

      Row() {
        ForEach([0, 1, 2, 3, 4, 5, 6, 7], (i: number) => {
          if (i < g.totalCount) {
            Column() {
              if (i < g.joinedCount) {
                Text(g.members[i % g.members.length]).fontSize(14)
                  .width(28).height(28).borderRadius(14).backgroundColor('#F3E5F5')
                  .textAlign(TextAlign.Center)
              } else {
                Text('+').fontSize(14).fontColor('#CCCCCC')
                  .width(28).height(28).borderRadius(14).backgroundColor('#F5F5F5')
                  .textAlign(TextAlign.Center)
              }
            }
            .margin({ left: 4, right: 4 })
          }
        })
        Column().layoutWeight(1)
        Text('去参团').fontSize(11).fontColor('#FFFFFF').backgroundColor('#6A1B9A')
          .padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14)
          .onClick(() => { this.selectedGroup = g; this.showGroupModal = true })
      }
      .width('100%').margin({ top: 10 })

      Row() {
        Column()
          .width((g.joinedCount / g.totalCount * 100).toFixed(0) + '%')
          .height(4).backgroundColor('#6A1B9A').borderRadius(2)
        Column().layoutWeight(1)
      }
      .width('100%').height(4).backgroundColor('#F3E5F5').borderRadius(2).margin({ top: 8 })

      Text('已拼 ' + g.joinedCount.toString() + '/' + g.totalCount.toString() + ' 人')
        .fontSize(9).fontColor('#6A1B9A').margin({ top: 4 })
    }
    .width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(12)
    .margin({ left: 12, right: 12, top: 6 })
    .shadow({ radius: 5, color: '#0D000000', offsetY: 2 })
    .onClick(() => { this.selectedGroup = g; this.showGroupModal = true })
  }

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Text('🤝').fontSize(20)
            Text('拼盒组队').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ left: 6 })
            Column().layoutWeight(1)
            Text('人多力量大').fontSize(10).fontColor('rgba(255,255,255,0.85)')
          }
          .width('100%').padding({ left: 14, right: 14, top: 12, bottom: 14 })
        }
        .width('100%').linearGradient(PURPLE_GRADIENT)

        Scroll() {
          Column() {
            Row() {
              Text('🔥 进行中的拼盒').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
              Column().layoutWeight(1)
              Text('共 ' + mockGroups.length.toString() + ' 个').fontSize(10).fontColor('#999999')
            }
            .width('100%').padding({ left: 16, right: 16, top: 12, bottom: 4 })

            this.groupCard(mockGroups[0])
            this.groupCard(mockGroups[1])
            this.groupCard(mockGroups[2])
            this.groupCard(mockGroups[3])
            this.groupCard(mockGroups[4])
            this.groupCard(mockGroups[5])
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showGroupModal) { this.groupModal() }
    }
    .width('100%').height('100%')
  }
}

// ============================ Tab4:潮玩二手 ================================

@Component
struct SecondHandContent {
  @State showDeleteConfirm: boolean = false
  @State showEditModal: boolean = false
  @State selectedItem: SecondHandItem | null = null
  @State formPrice: string = ''
  @State selectedCondition: string = '全新未拆'

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.55)')
      .onClick(onClose)
  }

  // 弹框:删除确认(警示)
  @Builder deleteConfirmModal() {
    Column() {
      this.modalOverlay(() => { this.showDeleteConfirm = false })
      Column() {
        Text('⚠️').fontSize(46).margin({ top: 22 })
        Text('确认删除该收藏?').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#212121')
        Text('删除后无法恢复交易记录').fontSize(11).fontColor('#999999').margin({ top: 5 })

        Row() {
          Text(this.selectedItem?.icon ?? '🎁').fontSize(20)
          Text(this.selectedItem?.name ?? '').fontSize(12).fontColor('#333333')
            .fontWeight(FontWeight.Bold).margin({ left: 8 })
        }
        .backgroundColor('#FCE4EC').borderRadius(10)
        .padding({ left: 16, right: 16, top: 10, bottom: 10 }).margin({ top: 14 })

        Row() {
          Text('取消').fontSize(13).fontColor('#888888').backgroundColor('#F5F5F5')
            .borderRadius(18).padding({ left: 26, right: 26, top: 9, bottom: 9 })
            .onClick(() => { this.showDeleteConfirm = false })
          Text('确认删除').fontSize(13).fontColor('#FFFFFF').backgroundColor('#E53935')
            .borderRadius(18).padding({ left: 26, right: 26, top: 9, bottom: 9 })
            .margin({ left: 10 })
            .onClick(() => { this.showDeleteConfirm = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 20, right: 20, top: 18, bottom: 20 })
      }
      .width('80%').backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '10%', y: '36%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  // 弹框:编辑价格
  @Builder editModal() {
    Column() {
      this.modalOverlay(() => { this.showEditModal = false })
      Column() {
        Row() {
          Text('✏️ 编辑挂卖信息').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
          Column().layoutWeight(1)
          Text('✕').fontSize(18).fontColor('#999999')
            .onClick(() => { this.showEditModal = false })
        }
        .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
        Divider().color('#F0F0F0')

        Column() {
          Text('挂卖价格').fontSize(11).fontColor('#888888').width('100%').margin({ top: 12 })
          TextInput({ placeholder: '¥' + (this.selectedItem?.price ?? 0).toString() })
            .placeholderColor('#BBBBBB').fontSize(14).type(InputType.Number)
            .backgroundColor('#F5F5F7').borderRadius(8).height(40).width('100%').margin({ top: 4 })
            .onChange((v: string) => { this.formPrice = v })

          Text('成色选择').fontSize(11).fontColor('#888888').width('100%').margin({ top: 12 })
          Row() {
            Text('全新未拆').fontSize(11)
              .fontColor(this.selectedCondition === '全新未拆' ? '#FFFFFF' : '#6A1B9A')
              .backgroundColor(this.selectedCondition === '全新未拆' ? '#6A1B9A' : '#F3E5F5')
              .padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(12)
              .onClick(() => { this.selectedCondition = '全新未拆' })
            Text('已拆展示').fontSize(11)
              .fontColor(this.selectedCondition === '已拆展示' ? '#FFFFFF' : '#6A1B9A')
              .backgroundColor(this.selectedCondition === '已拆展示' ? '#6A1B9A' : '#F3E5F5')
              .padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(12).margin({ left: 8 })
              .onClick(() => { this.selectedCondition = '已拆展示' })
            Text('整套出').fontSize(11)
              .fontColor(this.selectedCondition === '整套出' ? '#FFFFFF' : '#6A1B9A')
              .backgroundColor(this.selectedCondition === '整套出' ? '#6A1B9A' : '#F3E5F5')
              .padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(12).margin({ left: 8 })
              .onClick(() => { this.selectedCondition = '整套出' })
          }
          .width('100%').margin({ top: 6 })

          Text('💡 建议定价:¥' + ((this.selectedItem?.price ?? 100) * 0.9).toFixed(0) + ' 更容易成交')
            .fontSize(10).fontColor('#FFD54F').width('100%').margin({ top: 10 })
        }
        .padding({ left: 16, right: 16 })

        Row() {
          Text('取消').fontSize(13).fontColor('#888888').backgroundColor('#F5F5F5')
            .borderRadius(18).padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .onClick(() => { this.showEditModal = false })
          Text('保存修改').fontSize(13).fontColor('#FFFFFF').backgroundColor('#6A1B9A')
            .borderRadius(18).padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .margin({ left: 10 })
            .onClick(() => { this.showEditModal = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 16, right: 16, top: 16, bottom: 16 })
      }
      .width('88%').backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '6%', y: '18%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  @Builder secondHandCard(s: SecondHandItem) {
    Row() {
      Column() {
        Text(s.icon).fontSize(34)
      }
      .width(68).height(68).backgroundColor(s.bg).borderRadius(14)
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        Row() {
          Text(s.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).layoutWeight(1)
          if (s.verified) {
            Text('✅ 已鉴定').fontSize(9).fontColor('#4CAF50')
              .backgroundColor('#E8F5E9').padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6)
          }
        }
        .width('100%')

        Row() {
          Text(s.condition).fontSize(10).fontColor('#6A1B9A')
            .backgroundColor('#F3E5F5').padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(6)
          Text(s.seller).fontSize(10).fontColor('#999999').margin({ left: 8 })
        }
        .width('100%').margin({ top: 5 })

        Row() {
          Text(fmtPrice(s.price)).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#E91E63')
          Column().layoutWeight(1)
          Text(s.views.toString() + '人浏览').fontSize(9).fontColor('#999999')
          Text('✏️').fontSize(14).margin({ left: 10 })
            .onClick(() => { this.selectedItem = s; this.showEditModal = true })
          Text('🗑️').fontSize(14).margin({ left: 8 })
            .onClick(() => { this.selectedItem = s; this.showDeleteConfirm = true })
        }
        .width('100%').margin({ top: 5 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })
    }
    .width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(12)
    .margin({ left: 12, right: 12, top: 6 })
    .shadow({ radius: 5, color: '#0D000000', offsetY: 2 })
  }

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Text('♻️').fontSize(20)
            Text('潮玩二手').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ left: 6 })
            Column().layoutWeight(1)
            Text('✅ 官方鉴定').fontSize(10).fontColor('rgba(255,255,255,0.85)')
          }
          .width('100%').padding({ left: 14, right: 14, top: 12, bottom: 14 })
        }
        .width('100%').linearGradient(DARK_GRADIENT)

        Scroll() {
          Column() {
            Row() {
              Text('🔥 热门挂卖').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
              Column().layoutWeight(1)
              Text('全部 >').fontSize(10).fontColor('#999999')
            }
            .width('100%').padding({ left: 16, right: 16, top: 12, bottom: 4 })

            this.secondHandCard(mockSecondHand[0])
            this.secondHandCard(mockSecondHand[1])
            this.secondHandCard(mockSecondHand[2])
            this.secondHandCard(mockSecondHand[3])
            this.secondHandCard(mockSecondHand[4])
            this.secondHandCard(mockSecondHand[5])
            this.secondHandCard(mockSecondHand[6])
            this.secondHandCard(mockSecondHand[7])
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showDeleteConfirm) { this.deleteConfirmModal() }
      if (this.showEditModal) { this.editModal() }
    }
    .width('100%').height('100%')
  }
}

// ============================ Tab5:概率公示 ================================

@Component
struct ProbabilityContent {
  @State showRuleModal: boolean = false
  probabilityData: number[] = [2, 8, 15, 25, 35, 45, 55]
  weekLabels: string[] = ['一', '二', '三', '四', '五', '六', '日']

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.55)')
      .onClick(onClose)
  }

  // 弹框:概率规则说明
  @Builder ruleModal() {
    Column() {
      this.modalOverlay(() => { this.showRuleModal = false })
      Column() {
        Row() {
          Text('📊 概率规则说明').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
          Column().layoutWeight(1)
          Text('✕').fontSize(18).fontColor('#999999')
            .onClick(() => { this.showRuleModal = false })
        }
        .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
        Divider().color('#F0F0F0')

        Column() {
          Text('1. 每个系列的概率在商品详情页公示').fontSize(12).fontColor('#333333')
            .width('100%').margin({ top: 10 })
          Text('2. 隐藏款概率一般为 1/144').fontSize(12).fontColor('#333333')
            .width('100%').margin({ top: 8 })
          Text('3. 稀有款概率一般为 1/36').fontSize(12).fontColor('#333333')
            .width('100%').margin({ top: 8 })
          Text('4. 普通款概率一般为 1/12').fontSize(12).fontColor('#333333')
            .width('100%').margin({ top: 8 })
          Text('5. 概率数据每周更新一次').fontSize(12).fontColor('#333333')
            .width('100%').margin({ top: 8 })
          Text('6. 所有概率经过第三方审核').fontSize(12).fontColor('#333333')
            .width('100%').margin({ top: 8 })

          Text('知道了').fontSize(13).fontColor('#FFFFFF').backgroundColor('#6A1B9A')
            .borderRadius(18).padding({ left: 32, right: 32, top: 10, bottom: 10 })
            .alignSelf(ItemAlign.Center).margin({ top: 18, bottom: 20 })
            .onClick(() => { this.showRuleModal = false })
        }
        .padding({ left: 16, right: 16 })
      }
      .width('82%').backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '9%', y: '22%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Text('📊').fontSize(20)
            Text('概率公示').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ left: 6 })
            Column().layoutWeight(1)
            Text('规则说明').fontSize(11).fontColor('#FFFFFF')
              .backgroundColor('rgba(255,255,255,0.2)')
              .padding({ left: 10, right: 10, top: 4, bottom: 4 }).borderRadius(10)
              .onClick(() => { this.showRuleModal = true })
          }
          .width('100%').padding({ left: 14, right: 14, top: 12, bottom: 14 })
        }
        .width('100%').linearGradient(PURPLE_GRADIENT)

        Scroll() {
          Column() {
            // 各系列概率分布
            Column() {
              Text('💎 各系列稀有度分布').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
                .width('100%').padding({ left: 16, top: 12, bottom: 8 })

              Column() {
                Row() { Text('🎀 MOLLY 星际旅行').fontSize(11).fontColor('#E91E63').layoutWeight(1); Text('隐藏 1/144').fontSize(11).fontColor('#888888') }
                Row() { Column().width('45%').height(6).backgroundColor('#E91E63').borderRadius(3); Column().layoutWeight(1) }
                .width('100%').margin({ top: 4, bottom: 10 })

                Row() { Text('🦷 LABUBU 精灵三园').fontSize(11).fontColor('#6A1B9A').layoutWeight(1); Text('隐藏 1/144').fontSize(11).fontColor('#888888') }
                Row() { Column().width('38%').height(6).backgroundColor('#6A1B9A').borderRadius(3); Column().layoutWeight(1) }
                .width('100%').margin({ top: 4, bottom: 10 })

                Row() { Text('💀 SKULLPANDA 暗夜').fontSize(11).fontColor('#424242').layoutWeight(1); Text('隐藏 1/144').fontSize(11).fontColor('#888888') }
                Row() { Column().width('30%').height(6).backgroundColor('#424242').borderRadius(3); Column().layoutWeight(1) }
                .width('100%').margin({ top: 4, bottom: 10 })

                Row() { Text('☁️ DIMOO 马戏团').fontSize(11).fontColor('#42A5F5').layoutWeight(1); Text('隐藏 1/144').fontSize(11).fontColor('#888888') }
                Row() { Column().width('25%').height(6).backgroundColor('#42A5F5').borderRadius(3); Column().layoutWeight(1) }
                .width('100%').margin({ top: 4, bottom: 10 })

                Row() { Text('🌙 HIRONO 小野五').fontSize(11).fontColor('#7B1FA2').layoutWeight(1); Text('隐藏 1/144').fontSize(11).fontColor('#888888') }
                Row() { Column().width('22%').height(6).backgroundColor('#7B1FA2').borderRadius(3); Column().layoutWeight(1) }
                .width('100%').margin({ top: 4 })
              }
              .padding({ left: 16, right: 16, bottom: 14 })
            }
            .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
            .margin({ left: 12, right: 12, top: 8 })

            // 本周开盒统计柱状图
            Column() {
              Text('📈 本周开出隐藏款数').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
                .width('100%').padding({ left: 16, top: 12, bottom: 8 })
              Row() {
                ForEach([0, 1, 2, 3, 4, 5, 6], (d: number) => {
                  Column() {
                    Text(this.probabilityData[d].toString()).fontSize(9).fontColor('#6A1B9A')
                    Column()
                      .width(22)
                      .height((this.probabilityData[d] / 60 * 64).toFixed(0) + 'vp')
                      .backgroundColor(d === 4 ? '#6A1B9A' : '#CE93D8')
                      .borderRadius({ topLeft: 4, topRight: 4 })
                    Text(this.weekLabels[d]).fontSize(8).fontColor('#999999').margin({ top: 3 })
                  }
                  .layoutWeight(1).alignItems(HorizontalAlign.Center)
                })
              }
              .width('100%').padding({ left: 12, right: 12, bottom: 12 })
            }
            .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
            .margin({ left: 12, right: 12, top: 8 })

            // 三格统计
            Row() {
              Column() {
                Text('💎').fontSize(20).margin({ top: 4 })
                Text('87').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FFD54F')
                Text('本周隐藏').fontSize(10).fontColor('#888888')
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 12, bottom: 12 })
              .backgroundColor('#FFFFFF').borderRadius(10).margin({ left: 6, right: 3, top: 8 })

              Column() {
                Text('👑').fontSize(20).margin({ top: 4 })
                Text('234').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#E91E63')
                Text('本周稀有').fontSize(10).fontColor('#888888')
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 12, bottom: 12 })
              .backgroundColor('#FFFFFF').borderRadius(10).margin({ left: 3, right: 3, top: 8 })

              Column() {
                Text('🎯').fontSize(20).margin({ top: 4 })
                Text('5678').fontSize(22).fontWeight(FontWeight.Bold).fontColor('#6A1B9A')
                Text('本周普通').fontSize(10).fontColor('#888888')
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 12, bottom: 12 })
              .backgroundColor('#FFFFFF').borderRadius(10).margin({ left: 3, right: 6, top: 8 })
            }
            .width('100%').padding({ left: 6, right: 6 })
          }
          .padding({ bottom: 20 })
        }
        .layoutWeight(1).scrollBar(BarState.Off)
      }
      .width('100%').height('100%')

      if (this.showRuleModal) { this.ruleModal() }
    }
    .width('100%').height('100%')
  }
}

// ============================ Tab6:消息 ================================

@Component
struct MessageContent {
  @Builder messageCard(m: MessageItem) {
    Row() {
      Column() {
        Text(m.icon).fontSize(22)
      }
      .width(48).height(48).backgroundColor('#F3E5F5').borderRadius(24)
      .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

      Column() {
        Row() {
          Text(m.title).fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            .layoutWeight(1)
          Text(m.time).fontSize(9).fontColor('#CCCCCC')
        }
        .width('100%')
        Text(m.content).fontSize(11).fontColor('#666666')
          .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).width('100%').margin({ top: 3 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })

      if (m.unread) {
        Column().width(8).height(8).backgroundColor('#E91E63').borderRadius(4)
      }
    }
    .width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(12)
    .margin({ left: 12, right: 12, top: 6 })
    .shadow({ radius: 4, color: '#0D000000', offsetY: 1 })
  }

  build() {
    Column() {
      Column() {
        Row() {
          Text('💬').fontSize(20)
          Text('消息中心').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF').margin({ left: 6 })
          Column().layoutWeight(1)
          Text('全部已读').fontSize(11).fontColor('rgba(255,255,255,0.85)')
        }
        .width('100%').padding({ left: 14, right: 14, top: 12, bottom: 14 })
      }
      .width('100%').linearGradient(PURPLE_GRADIENT)

      Scroll() {
        Column() {
          this.messageCard(mockMessages[0])
          this.messageCard(mockMessages[1])
          this.messageCard(mockMessages[2])
          this.messageCard(mockMessages[3])
          this.messageCard(mockMessages[4])
          this.messageCard(mockMessages[5])
        }
        .padding({ bottom: 20 })
      }
      .layoutWeight(1).scrollBar(BarState.Off)
    }
    .width('100%').height('100%')
  }
}

// ============================ Tab7:我的 ================================

@Component
struct ProfileContent {
  @State showEditProfile: boolean = false
  @State formNick: string = ''
  orderStatus: string[] = ['待付款', '待发货', '待收货', '待评价', '售后']
  orderIcons: string[] = ['💰', '📦', '🚚', '⭐', '🛠️']
  menuLabels: string[] = ['我的拼盒', '我的收藏', '开盒记录', '二手挂卖', '优惠券', '地址管理', '客服中心', '设置']
  menuIcons: string[] = ['🤝', '❤️', '🎁', '♻️', '🎟️', '📍', '💬', '⚙️']

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%').height('100%')
      .backgroundColor('rgba(0,0,0,0.55)')
      .onClick(onClose)
  }

  // 弹框:编辑资料
  @Builder editProfileModal() {
    Column() {
      this.modalOverlay(() => { this.showEditProfile = false })
      Column() {
        Row() {
          Text('✏️ 编辑个人资料').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#212121')
          Column().layoutWeight(1)
          Text('✕').fontSize(18).fontColor('#999999')
            .onClick(() => { this.showEditProfile = false })
        }
        .width('100%').padding({ left: 16, right: 16, top: 14, bottom: 10 })
        Divider().color('#F0F0F0')

        Column() {
          Text('选择头像').fontSize(11).fontColor('#888888').width('100%')
          Row() {
            ForEach(['🎁', '🎀', '🦷', '💀', '☁️'], (a: string) => {
              Text(a).fontSize(24).width(44).height(44).borderRadius(22).backgroundColor('#F3E5F5')
                .textAlign(TextAlign.Center).margin({ left: 6, right: 6 })
            })
          }
          .width('100%').margin({ top: 8 })

          Text('昵称').fontSize(11).fontColor('#888888').width('100%').margin({ top: 14 })
          TextInput({ placeholder: '盲盒收藏家' }).placeholderColor('#BBBBBB').fontSize(13)
            .backgroundColor('#F5F5F7').borderRadius(8).height(38).width('100%').margin({ top: 4 })
            .onChange((v: string) => { this.formNick = v })
        }
        .padding({ left: 16, right: 16, top: 12 })
        .constraintSize({ maxHeight: '58%' })

        Row() {
          Text('取消').fontSize(13).fontColor('#888888').backgroundColor('#F5F5F5')
            .borderRadius(18).padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .onClick(() => { this.showEditProfile = false })
          Text('保存资料').fontSize(13).fontColor('#FFFFFF').backgroundColor('#6A1B9A')
            .borderRadius(18).padding({ left: 24, right: 24, top: 9, bottom: 9 })
            .margin({ left: 10 })
            .onClick(() => { this.showEditProfile = false })
        }
        .width('100%').justifyContent(FlexAlign.Center)
        .padding({ left: 16, right: 16, top: 14, bottom: 14 })
      }
      .width('88%').backgroundColor('#FFFFFF').borderRadius(16)
      .alignItems(HorizontalAlign.Center)
      .position({ x: '6%', y: '18%' })
    }
    .width('100%').height('100%').position({ x: 0, y: 0 }).zIndex(999)
  }

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Column() {
              Text('🎁').fontSize(34)
            }
            .width(60).height(60).backgroundColor('rgba(255,255,255,0.25)').borderRadius(30)
            .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

            Column() {
              Row() {
                Text('盲盒收藏家').fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
                Text('LV.6').fontSize(9).fontColor('#FFFFFF').backgroundColor('rgba(0,0,0,0.25)')
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 }).borderRadius(8).margin({ left: 8 })
              }
              Text('已开盒 234 次 · 隐藏款 5 个').fontSize(10).fontColor('rgba(255,255,255,0.9)').margin({ top: 4 })
            }
            .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 12 })

            Text('编辑').fontSize(11).fontColor('#FFFFFF').backgroundColor('rgba(255,255,255,0.25)')
              .padding({ left: 12, right: 12, top: 5, bottom: 5 }).borderRadius(12)
              .onClick(() => { this.showEditProfile = true })
          }
          .width('100%').padding({ left: 16, right: 16, top: 16, bottom: 16 })
        }
        .width('100%').linearGradient(PURPLE_GRADIENT)

        // 订单状态
        Column() {
          Row() {
            Text('我的订单').fontSize(13).fontWeight(FontWeight.Bold).fontColor('#212121')
            Column().layoutWeight(1)
            Text('全部订单 >').fontSize(10).fontColor('#999999')
          }
          .width('100%').padding({ left: 16, top: 12, bottom: 8 })
          Row() {
            ForEach([0, 1, 2, 3, 4], (i: number) => {
              Column() {
                Text(this.orderIcons[i]).fontSize(20)
                Text(this.orderStatus[i]).fontSize(9).fontColor('#555555').margin({ top: 4 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
            })
          }
          .width('100%').padding({ left: 8, right: 8, bottom: 12 })
        }
        .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
        .margin({ left: 12, right: 12, top: 8 })

        // 功能宫格
        Column() {
          Row() {
            ForEach([0, 1, 2, 3], (i: number) => {
              Column() {
                Text(this.menuIcons[i]).fontSize(20)
                Text(this.menuLabels[i]).fontSize(9).fontColor('#555555').margin({ top: 4 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              .padding({ top: 14, bottom: 14 })
            })
          }
          .width('100%')
          Divider().color('#F5F5F5')
          Row() {
            ForEach([4, 5, 6, 7], (i: number) => {
              Column() {
                Text(this.menuIcons[i]).fontSize(20)
                Text(this.menuLabels[i]).fontSize(9).fontColor('#555555').margin({ top: 4 })
              }
              .layoutWeight(1).alignItems(HorizontalAlign.Center)
              .padding({ top: 14, bottom: 14 })
            })
          }
          .width('100%')
        }
        .width('100%').backgroundColor('#FFFFFF').borderRadius(12)
        .margin({ left: 12, right: 12, top: 8 })

        Text('潮玩盲盒星球 v1.0 · 概率透明 · 2026').fontSize(9).fontColor('#CCCCCC')
          .width('100%').textAlign(TextAlign.Center).margin({ top: 14, bottom: 10 })
      }
      .width('100%').height('100%')

      if (this.showEditProfile) { this.editProfileModal() }
    }
    .width('100%').height('100%')
  }
}


总结

在这里插入图片描述

本文深入剖析了一个基于HarmonyOS ArkTS声明式UI框架的潮玩盲盒交易社区应用,从接口定义、数据模型、设计令牌到七大功能模块的完整实现进行了逐段代码分析。应用通过@Observed可观察数据类和@State状态管理构建了响应式UI体系,@Builder方法封装实现了高度可复用的组件化开发模式。多层级弹窗系统通过统一的modalOverlay遮罩Builder配合position绝对定位和zIndex层叠管理,优雅地处理了抽盒模拟、商品详情、发售预约、拼盒邀请、编辑挂卖、删除确认等复杂交互场景。

在业务架构层面,应用围绕"概率透明、拼团社交、二手流通"三大产品理念实现了完整的用户消费闭环。概率公示模块通过进度条和柱状图双重视觉呈现保障了消费者知情权,拼盒组队模块通过成员头像槽位动态渲染和进度条百分比计算实现了社交拼团的实时反馈,二手交易模块通过官方鉴定标签和智能定价建议构建了可信的流通环境。应用的瀑布双列布局、横滑系列导航、条件渲染路由等UI设计模式充分展现了ArkTS声明式UI在复杂电商场景下的开发效率优势。

从技术演进角度来看,该应用的代码架构为后续接入真实后端API提供了清晰的数据契约定义,mock数据集的结构可直接映射为API响应模型。@Observed装饰器的响应式机制为实时数据更新场景(如拼团人数变化、倒计时同步)奠定了基础。整体代码遵循了高内聚低耦合的组件化原则,每个Tab模块独立管理自己的状态和弹窗,通过入口组件的条件渲染路由实现模块间的无缝切换,体现了HarmonyOS ArkTS在构建复杂多模块移动应用时的架构优势。

Logo

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

更多推荐