引言

在这里插入图片描述

游戏虚拟商品交易市场近年来呈现高速增长态势,涵盖游戏点券充值、点卡购买、礼包选购、玩家间寄售交易等多种业务形态。构建一个覆盖充值、点卡、礼包、交易、订单、个人中心全链路的综合服务平台,对UI架构的模块化能力、状态管理的精细度以及交互流程的完整性提出了极高要求。本文将深度剖析一个基于HarmonyOS ArkTS声明式UI框架构建的游戏点卡充值中心应用,该应用采用暗紫(#6C3BFF)与荧光绿(#39FF88)双色主题,通过底部六Tab导航实现了多业务模块的有机整合。

从技术架构层面分析,该应用采用了ArkUI框架的组件化开发范式,以@Component装饰器封装了RechargeTab、CardTab、GiftTab、TradeTab、OrderTab、MineTab六个独立Tab组件,由主入口GameCardPage统一调度。每个Tab组件内部通过多个@State变量管理本地状态,包括选中索引、弹窗显隐、当前操作对象等,形成组件内自治的状态管理。数据层定义了GameMeta、DenomMeta、GiftMeta、CardMeta、TradeMeta、TrendMeta、RecMeta等十余种TypeScript interface,覆盖了游戏、档位、礼包、点卡、交易、行情、订单等全部业务实体,构建了强类型的领域模型。

在业务设计层面,该应用实现了多个创新性的交互模式。充值Tab通过游戏选择、档位宫格、支付方式三个维度构建了完整的充值订单确认流程。点卡Tab实现了分类筛选与横卡列表的联动。礼包Tab采用双列瀑布流布局展示礼包商品,配合SSR/SR/R稀有度分级标签实现差异化视觉呈现。交易Tab是业务复杂度最高的模块,实现了卖家上架寄售、买家议价申请、改价、下架等C2C交易核心流程,并集成了7日价格行情柱状图作为交易参考。订单和我的Tab则覆盖了充值记录管理、余额提现、玩家等级、至尊会员、道具库存、签到等完整的用户运营体系。

整体应用的UI设计遵循暗紫荧光绿系视觉规范,大量使用linearGradient双色渐变、shadow紫色系阴影、FlexWrap弹性布局等视觉技术。所有模态弹窗统一采用rgba(20,10,50,0.6)紫色遮罩层配合position绝对定位的弹窗内容,通过constraintSize设置maxHeight限制弹窗最大高度,实现了沉浸式的模态交互体验。

一、数据模型层设计

在这里插入图片描述

1.1 游戏与充值档位数据模型

游戏充值场景的核心数据实体包括游戏元数据和充值档位元数据两类,通过interface进行精确的类型约束。

interface GameMeta {
  id: number
  name: string
  type: string
  discount: string
  hot: boolean
  pic: string
}

const GAMES: GameMeta[] = [
  { id: 1, name: '王者荣耀', type: 'MOBA', discount: '92 折', hot: true, pic: '👑' },
  { id: 2, name: '原神', type: '开放世界', discount: '95 折', hot: true, pic: '🌌' },
  { id: 3, name: '和平精英', type: '射击', discount: '93 折', hot: true, pic: '🎯' },
  { id: 4, name: '蛋仔派对', type: '休闲', discount: '9 折', hot: true, pic: '🥚' },
  { id: 5, name: '明日方舟', type: '策略', discount: '94 折', hot: false, pic: '🛡️' },
  { id: 6, name: '崩坏:星穹铁道', type: 'RPG', discount: '95 折', hot: true, pic: '🚄' },
  { id: 7, name: '第五人格', type: '非对称', discount: '96 折', hot: false, pic: '🎭' },
  { id: 8, name: '阴阳师', type: '卡牌', discount: '93 折', hot: false, pic: '👻' }
]

interface DenomMeta {
  id: number
  coins: number
  price: number
  bonus: number
  tag: string
}

const DENOMS: DenomMeta[] = [
  { id: 1, coins: 6, price: 6, bonus: 0, tag: '' },
  { id: 2, coins: 30, price: 30, bonus: 0, tag: '' },
  { id: 3, coins: 98, price: 98, bonus: 10, tag: '送10' },
  { id: 4, coins: 198, price: 198, bonus: 25, tag: '送25' },
  { id: 5, coins: 328, price: 328, bonus: 50, tag: '超值' },
  { id: 6, coins: 648, price: 648, bonus: 128, tag: '人气' }
]

GameMeta定义了游戏的基础属性,其中hot布尔字段用于在热门游戏横滑区域控制HOT标签的显示,discount字段以字符串形式存储折扣信息。DenomMeta定义了充值档位,bonus字段表示额外赠送的点券数量,tag字段用于档位卡片的角标标签——空字符串表示普通档位,非空字符串则渲染为红色角标。

1.2 点卡与礼包数据模型

在这里插入图片描述

点卡模块定义了CardMeta实体,其设计亮点在于cardOff辅助函数封装了折扣率的计算逻辑。

interface CardMeta {
  id: number
  name: string
  face: number
  price: number
  stock: number
  kind: string
  pic: string
}

const CARD_LIST: CardMeta[] = [
  { id: 1, name: '通用游戏点卡 100 元', face: 100, price: 96.5, stock: 328, kind: '通用卡', pic: '💳' },
  { id: 2, name: 'Q 币充值卡 50 元', face: 50, price: 47.8, stock: 512, kind: 'Q币', pic: '🐧' },
  { id: 3, name: 'STEAM 礼品卡 100 美元', face: 100, price: 688, stock: 46, kind: '国际卡', pic: '🎮' },
  { id: 4, name: 'App Store 充值卡 200 元', face: 200, price: 193, stock: 89, kind: '苹果卡', pic: '🍏' },
  { id: 5, name: '任天堂 eShop 点卡 1000 日元', face: 1000, price: 49.5, stock: 134, kind: '国际卡', pic: '🕹️' },
  { id: 6, name: 'PSN 港服点卡 100 港币', face: 100, price: 92, stock: 67, kind: '国际卡', pic: '🎯' },
  { id: 7, name: '某手游月卡兑换券', face: 30, price: 28.6, stock: 890, kind: '通用卡', pic: '📅' },
  { id: 8, name: '通用游戏点卡 500 元', face: 500, price: 481, stock: 22, kind: '通用卡', pic: '💎' }
]

function cardOff(price: number, face: number): string {
  return (price / face * 10).toFixed(1) + ' 折'
}

cardOff函数将售价与面值的比值乘以10并保留一位小数,生成如"9.7 折"格式的折扣字符串。该函数在点卡列表的折扣标签和购买弹窗的折扣信息中被多处复用。kind字段作为分类标识,配合SNACK_CATS分类数组实现点卡的分类筛选功能。

礼包模块定义了GiftMeta实体,引入了rare稀有度字段(SSR/SR/R)和worth价值字段,rareColor辅助函数将稀有度映射为颜色值。

1.3 交易与行情数据模型

在这里插入图片描述

交易Tab的数据模型包含交易商品元数据和价格行情数据两类,是整个应用中业务关系最复杂的数据层。

interface TradeMeta {
  id: number
  item: string
  game: string
  price: number
  seller: string
  state: string
  hot: boolean
  pic: string
}

const TRADE_LIST: TradeMeta[] = [
  { id: 1, item: '限定皮肤「星穹之约」', game: '王者荣耀', price: 328, seller: '峡谷收藏家', state: '在售', hot: true, pic: '✨' },
  { id: 2, item: '五星角色账号 60 级', game: '原神', price: 1280, seller: '提瓦特旅人', state: '在售', hot: true, pic: '🗺️' },
  { id: 3, item: '赛季限定枪械皮肤', game: '和平精英', price: 168, seller: '钢枪小王子', state: '在售', hot: false, pic: '🔫' },
  { id: 4, item: '全套 SSR 阵容初始号', game: '明日方舟', price: 666, seller: '刀客塔一号', state: '在售', hot: true, pic: '🃏' },
  { id: 5, item: '盲盒限定染色套装', game: '蛋仔派对', price: 88, seller: '圆滚滚大师', state: '已售', hot: false, pic: '🥚' },
  { id: 6, item: '典藏皮肤「夜之乐章」', game: '第五人格', price: 458, seller: '庄园老玩家', state: '在售', hot: false, pic: '🎻' },
  { id: 7, item: '满命角色成品号', game: '崩坏:星穹铁道', price: 2888, seller: '星穹商人', state: '在售', hot: true, pic: '🌟' },
  { id: 8, item: '绝版限定头像框', game: '阴阳师', price: 199, seller: '平安京代购', state: '锁定', hot: false, pic: '🏮' }
]

interface TrendMeta {
  day: string
  price: number
}

const PRICE_TREND: TrendMeta[] = [
  { day: '周一', price: 302 },
  { day: '周二', price: 315 },
  { day: '周三', price: 289 },
  { day: '周四', price: 328 },
  { day: '周五', price: 345 },
  { day: '周六', price: 318 },
  { day: '周日', price: 336 }
]

function trendBarH(p: number): number {
  let h: number = Math.round((p - 260) / 2)
  if (h > 88) {
    h = 88
  }
  if (h < 18) {
    h = 18
  }
  return h
}

TradeMeta的state字段支持三种状态:‘在售’、‘已售’、‘锁定’,对应不同的UI表现和可操作按钮。seller字段在值为’我’时表示当前用户上架的商品,会显示改价和下架按钮;其他卖家的商品则显示议价和立即购买按钮。TrendMeta定义了7日价格行情数据,trendBarH函数将价格映射为柱状条高度,以260为基准值进行线性映射,同时设置了18到88的上下限范围保证可视性。

二、充值Tab组件深度解析

在这里插入图片描述

2.1 游戏选择与充值确认弹窗

充值Tab作为应用的首屏,承担了游戏充值的核心交易流程。组件通过gameIdx、denomIdx、payIdx三个索引状态变量分别管理选中的游戏、档位和支付方式。

@Component
struct RechargeTab {
  @State gameIdx: number = 0
  @State denomIdx: number = 3
  @State payIdx: number = 0
  @State showRecharge: boolean = false
  @State showGame: boolean = false
  @State showOk: boolean = false
  @State okText: string = '充值成功'
  @State curGame: GameMeta = GAMES[0]

  @Builder rechargeModal() {
    Column({ space: 14 }) {
      Text('💳 确认充值订单')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Column({ space: 6 }) {
        Row() {
          Text('游戏')
            .fontSize(13)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text(this.curGame.pic + ' ' + this.curGame.name)
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
        }
        .width('100%')
        Row() {
          Text('档位')
            .fontSize(13)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text(DENOMS[this.denomIdx].coins + ' 点券' + (DENOMS[this.denomIdx].bonus > 0 ? ' + 赠 ' + DENOMS[this.denomIdx].bonus : ''))
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
        }
        .width('100%')
        Row() {
          Text('折扣')
            .fontSize(13)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text(this.curGame.discount)
            .fontSize(13)
            .fontColor('#39C27E')
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        Row() {
          Text('实付')
            .fontSize(13)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text('¥ ' + DENOMS[this.denomIdx].price)
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#6C3BFF')
        }
        .width('100%')
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#F6F4FF')
      .borderRadius(12)

      Column({ space: 8 }) {
        Text('支付方式')
          .fontSize(12)
          .fontColor('#999999')
          .width('100%')
        ForEach(['微信支付', 'QQ 钱包', '苹果内购', '余额支付(¥ 268.50)'], (p: string, idx: number) => {
          Row({ space: 8 }) {
            Text(idx === 0 ? '💚' : (idx === 1 ? '🐧' : (idx === 2 ? '🍎' : '💰')))
              .fontSize(18)
            Text(p)
              .fontSize(13)
              .fontColor('#333333')
              .layoutWeight(1)
            Text(this.payIdx === idx ? '◉' : '○')
              .fontSize(16)
              .fontColor('#6C3BFF')
          }
          .width('100%')
          .padding({ top: 9, bottom: 9 })
          .borderRadius(10)
          .backgroundColor(this.payIdx === idx ? '#F0EBFF' : '#FFFFFF')
          .onClick(() => {
            this.payIdx = idx
          })
        })
      }
      .width('100%')

      Row({ space: 10 }) {
        Text('再看看')
          .fontSize(14)
          .fontColor('#999999')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#F6F4FF')
          .borderRadius(22)
          .onClick(() => {
            this.showRecharge = false
          })
        Text('立即支付 ¥' + DENOMS[this.denomIdx].price)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .layoutWeight(1.4)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#9F7BFF', 1.0]] })
          .borderRadius(22)
          .onClick(() => {
            this.showRecharge = false
            this.okText = '充值成功,点券已到账'
            this.showOk = true
          })
      }
      .width('100%')
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('88%')
    .position({ x: '6%', y: '10%' })
    .constraintSize({ maxHeight: '84%' })
  }

rechargeModal构建器是充值流程的核心弹窗,采用了信息摘要+支付方式选择+操作按钮的三段式结构。信息摘要区使用Column嵌套Row的布局,每行包含左侧标签、中间弹性填充Row和右侧数值,通过layoutWeight(1)实现右侧数值的右对齐。档位信息通过条件表达式DENOMS[this.denomIdx].bonus > 0 ? ' + 赠 ' + DENOMS[this.denomIdx].bonus : ''动态拼接赠送信息,仅当存在赠送时才显示。

支付方式列表通过ForEach渲染四个支付选项,每个选项的单选状态通过this.payIdx === idx ? '◉' : '○'的字符切换实现——选中时显示实心圆,未选中时显示空心圆。同时通过backgroundColor的切换为选中项添加浅紫色背景,形成视觉区分。支付按钮使用layoutWeight(1.4)占据比"再看看"按钮更大的宽度,并动态显示当前档位的实付金额。

2.2 充值档位宫格与游戏切换

在这里插入图片描述

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 当前游戏卡
          Row({ space: 12 }) {
            Text(this.curGame.pic)
              .fontSize(36)
              .width(62)
              .height(62)
              .textAlign(TextAlign.Center)
              .backgroundColor('rgba(255,255,255,0.2)')
              .borderRadius(16)
            Column({ space: 4 }) {
              Text(this.curGame.name)
                .fontSize(19)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text(this.curGame.type + ' · 点券 ' + this.curGame.discount)
                .fontSize(11)
                .fontColor('rgba(255,255,255,0.8)')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('切换')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .padding({ left: 14, right: 14, top: 7, bottom: 7 })
              .backgroundColor('rgba(255,255,255,0.22)')
              .borderRadius(15)
              .onClick(() => {
                this.showGame = true
              })
          }
          .width('94%')
          .padding(14)
          .linearGradient({ angle: 135, colors: [['#6C3BFF', 0.0], ['#4B1FB8', 1.0]] })
          .borderRadius(16)
          .shadow({ radius: 14, color: 'rgba(108,59,255,0.3)', offsetX: 0, offsetY: 6 })

          // 档位宫格
          Column({ space: 10 }) {
            Text('选择充值档位')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1A1A1A')
              .width('100%')
            Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
              ForEach(DENOMS, (d: DenomMeta, idx: number) => {
                Column({ space: 4 }) {
                  if (d.tag.length > 0) {
                    Text(d.tag)
                      .fontSize(9)
                      .fontColor('#FFFFFF')
                      .padding({ left: 7, right: 7, top: 2, bottom: 2 })
                      .backgroundColor('#FF5A5F')
                      .borderRadius(8)
                  } else {
                    Text(' ')
                      .fontSize(9)
                      .fontColor('#FFFFFF00')
                      .padding({ top: 2, bottom: 2 })
                  }
                  Text(d.coins + ' 点券')
                    .fontSize(16)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(this.denomIdx === idx ? '#6C3BFF' : '#1A1A1A')
                  if (d.bonus > 0) {
                    Text('赠 ' + d.bonus + ' 点券')
                      .fontSize(10)
                      .fontColor('#39C27E')
                  } else {
                    Text('标准档位')
                      .fontSize(10)
                      .fontColor('#BBBBBB')
                  }
                  Text('¥' + d.price)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(this.denomIdx === idx ? '#FFFFFF' : '#6C3BFF')
                    .padding({ left: 14, right: 14, top: 3, bottom: 3 })
                    .backgroundColor(this.denomIdx === idx ? '#6C3BFF' : '#F0EBFF')
                    .borderRadius(12)
                    .margin({ top: 3 })
                }
                .width('31%')
                .padding({ top: 10, bottom: 10 })
                .alignItems(HorizontalAlign.Center)
                .backgroundColor(this.denomIdx === idx ? '#F0EBFF' : '#FFFFFF')
                .borderRadius(14)
                .border({ width: this.denomIdx === idx ? 2 : 1, color: this.denomIdx === idx ? '#6C3BFF' : '#EDE8FB' })
                .margin({ bottom: 10 })
                .onClick(() => {
                  this.denomIdx = idx
                })
              })
            }
            .width('100%')

            Text('立即充值')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .width('100%')
              .textAlign(TextAlign.Center)
              .padding({ top: 13, bottom: 13 })
              .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#39C27E', 1.0]] })
              .borderRadius(24)
              .shadow({ radius: 10, color: 'rgba(108,59,255,0.35)', offsetX: 0, offsetY: 4 })
              .onClick(() => {
                this.showRecharge = true
              })
          }
          .width('94%')
          .padding(14)
          .backgroundColor('#FFFFFF')
          .borderRadius(14)
          .margin({ top: 12 })
        }
        .width('100%')
      }

当前游戏卡使用135度线性渐变从暗紫到深紫,配合14号半径的紫色阴影营造立体悬浮效果。游戏图标使用半透明白色背景容器,文字使用纯白色和80%透明白色形成主次层级。

档位宫格使用Flex配合FlexWrap.Wrap和FlexAlign.SpaceBetween实现自动换行的三列布局,每个档位宽度设为31%。选中态通过多层视觉属性联动实现:边框宽度从1变为2、边框颜色变为紫色、背景色变为浅紫色、价格文字背景色从浅紫色变为紫色、价格文字颜色从紫色变为白色。这种多维度的选中态视觉反馈确保用户能够清晰辨识当前选择。

档位卡片的角标标签使用条件渲染:当d.tag.length > 0时渲染红色角标,否则渲染一个透明的占位Text,保证布局高度的一致性。赠送信息也使用类似的条件渲染模式,bonus > 0时显示绿色赠送文字,否则显示灰色的"标准档位"。

三、礼包Tab双列瀑布流与稀有度系统

3.1 瀑布流布局与礼包卡片

在这里插入图片描述

礼包Tab采用了双列瀑布流布局展示礼包商品,通过getLeftGifts和getRightGifts两个辅助函数将GIFTS数组按奇偶索引拆分为左右两列。

function getLeftGifts(): GiftMeta[] {
  const r: GiftMeta[] = []
  for (let i = 0; i < GIFTS.length; i += 2) {
    r.push(GIFTS[i])
  }
  return r
}

function getRightGifts(): GiftMeta[] {
  const r: GiftMeta[] = []
  for (let i = 1; i < GIFTS.length; i += 2) {
    r.push(GIFTS[i])
  }
  return r
}

function rareColor(rare: string): string {
  if (rare === 'SSR') {
    return '#FF5A5F'
  }
  if (rare === 'SR') {
    return '#FFB800'
  }
  return '#9F7BFF'
}

@Component
struct GiftTab {
  @State showDetail: boolean = false
  @State showOk: boolean = false
  @State okText: string = '已加入购物车'
  @State curGift: GiftMeta = GIFTS[0]

  @Builder giftCard(g: GiftMeta) {
    Column({ space: 8 }) {
      Stack({ alignContent: Alignment.TopEnd }) {
        Text(g.pic)
          .fontSize(40)
          .width('100%')
          .height(88)
          .textAlign(TextAlign.Center)
          .linearGradient({ angle: 135, colors: [['#F0EBFF', 0.0], ['#E0F8EC', 1.0]] })
          .borderRadius(12)
        Text(g.rare)
          .fontSize(9)
          .fontColor('#FFFFFF')
          .padding({ left: 7, right: 7, top: 2, bottom: 2 })
          .backgroundColor(rareColor(g.rare))
          .borderRadius(8)
          .margin(6)
      }
      .width('100%')

      Text(g.name)
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .width('100%')

      Text(g.game)
        .fontSize(10)
        .fontColor('#9F7BFF')
        .width('100%')

      Row({ space: 6 }) {
        Text('¥' + g.price)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#6C3BFF')
        Text('¥' + g.oldPrice)
          .fontSize(10)
          .fontColor('#BBBBBB')
          .decoration({ type: TextDecorationType.LineThrough })
        Row().layoutWeight(1)
        Text('已售' + g.sold)
          .fontSize(9)
          .fontColor('#999999')
      }
      .width('100%')
    }
    .width('100%')
    .padding(10)
    .backgroundColor('#FFFFFF')
    .borderRadius(14)
    .shadow({ radius: 6, color: 'rgba(108,59,255,0.06)', offsetX: 0, offsetY: 2 })
    .onClick(() => {
      this.curGift = g
      this.showDetail = true
    })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 顶部横幅
          Row({ space: 10 }) {
            Text('🎇')
              .fontSize(28)
            Column({ space: 3 }) {
              Text('周末礼包狂欢节')
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('全场礼包 5 折起,SSR 福袋限量抢')
                .fontSize(11)
                .fontColor('rgba(255,255,255,0.85)')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
          }
          .width('94%')
          .padding(16)
          .linearGradient({ angle: 135, colors: [['#4B1FB8', 0.0], ['#39C27E', 1.0]] })
          .borderRadius(16)
          .shadow({ radius: 12, color: 'rgba(75,31,184,0.25)', offsetX: 0, offsetY: 5 })

          // 双列瀑布
          Row({ space: 10 }) {
            Column({ space: 10 }) {
              ForEach(getLeftGifts(), (g: GiftMeta) => {
                this.giftCard(g)
              })
            }
            .layoutWeight(1)

            Column({ space: 10 }) {
              ForEach(getRightGifts(), (g: GiftMeta) => {
                this.giftCard(g)
              })
            }
            .layoutWeight(1)
          }
          .width('94%')
          .alignItems(VerticalAlign.Top)
          .margin({ top: 12, bottom: 20 })
        }
        .width('100%')
      }

双列瀑布流通过两个等宽的Column(各layoutWeight(1))分别渲染奇偶索引的礼包卡片。外层Row使用alignItems(VerticalAlign.Top)确保两列从顶部对齐。这种简单的瀑布流实现虽然不等高,但通过卡片间space(10)的统一间距保证了视觉的一致性。

giftCard构建器使用Stack配合Alignment.TopEnd在礼包图标区域的右上角叠加稀有度标签,标签的backgroundColor通过rareColor函数动态设置:SSR为红色、SR为黄色、R为紫色。图标区域使用135度双色渐变从浅紫到浅绿,形成与主题色呼应的视觉基调。整个卡片的onClick回调设置curGift为当前礼包并打开详情弹窗。

四、交易Tab寄售系统全链路解析

4.1 上架寄售与议价流程

交易Tab是整个应用业务复杂度最高的模块,实现了C2C寄售交易的核心流程。TradeTab组件通过tradeList状态数组管理交易列表,支持上架、议价、改价、下架四种操作。

@Component
struct TradeTab {
  @State tradeList: TradeMeta[] = TRADE_LIST
  @State showSell: boolean = false
  @State showBargain: boolean = false
  @State showOff: boolean = false
  @State showEdit: boolean = false
  @State showOk: boolean = false
  @State okText: string = '操作成功'
  @State curTrade: TradeMeta = TRADE_LIST[0]
  @State inputPrice: string = ''
  @State inputItem: string = ''
  @State bargainIdx: number = 0

  applySell() {
    if (this.inputItem.length === 0 || this.inputPrice.length === 0) {
      this.okText = '请填写商品名称和售价'
      this.showOk = true
      return
    }
    const p: number = Number(this.inputPrice)
    const r: TradeMeta[] = []
    for (let i = 0; i < this.tradeList.length; i++) {
      r.push(this.tradeList[i])
    }
    r.unshift({ id: this.tradeList.length + 1, item: this.inputItem, game: GAMES[this.bargainIdx].name, price: p, seller: '我', state: '在售', hot: false, pic: '🆕' })
    this.tradeList = r
    this.showSell = false
    this.inputItem = ''
    this.inputPrice = ''
    this.okText = '上架成功,等待买家咨询'
    this.showOk = true
  }

  applyOff() {
    const r: TradeMeta[] = []
    for (let i = 0; i < this.tradeList.length; i++) {
      if (this.tradeList[i].id === this.curTrade.id) {
        r.push({ id: this.tradeList[i].id, item: this.tradeList[i].item, game: this.tradeList[i].game, price: this.tradeList[i].price, seller: this.tradeList[i].seller, state: '已下架', hot: false, pic: this.tradeList[i].pic })
      } else {
        r.push(this.tradeList[i])
      }
    }
    this.tradeList = r
    this.showOff = false
    this.okText = '商品已下架'
    this.showOk = true
  }

  applyEdit() {
    const p: number = Number(this.inputPrice)
    if (p <= 0) {
      this.okText = '请输入有效价格'
      this.showOk = true
      return
    }
    const r: TradeMeta[] = []
    for (let i = 0; i < this.tradeList.length; i++) {
      if (this.tradeList[i].id === this.curTrade.id) {
        r.push({ id: this.tradeList[i].id, item: this.tradeList[i].item, game: this.tradeList[i].game, price: p, seller: this.tradeList[i].seller, state: this.tradeList[i].state, hot: this.tradeList[i].hot, pic: this.tradeList[i].pic })
      } else {
        r.push(this.tradeList[i])
      }
    }
    this.tradeList = r
    this.showEdit = false
    this.inputPrice = ''
    this.okText = '价格已更新为 ¥' + p
    this.showOk = true
  }

applySell方法实现了上架寄售的核心逻辑:首先校验商品名称和售价是否为空,通过Number函数将价格字符串转为数值,然后通过unshift将新商品插入列表头部。新商品的seller字段设为’我’,state设为’在售’,pic使用’🆕’标识。这种头部插入方式确保用户上架后能立即在列表顶部看到自己的商品。

applyOff和applyEdit方法分别实现下架和改价操作,都采用了遍历数组创建新副本的模式。applyOff将匹配商品的state改为’已下架’,applyEdit将price替换为新值。两者的关键设计在于只修改匹配项的字段,其余字段保持不变,通过展开式对象字面量确保所有属性都被显式赋值。

4.2 议价弹窗与快捷出价

  @Builder bargainModal() {
    Column({ space: 14 }) {
      Text('🤝 议价申请')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Text(this.curTrade.item)
        .fontSize(13)
        .fontColor('#666666')
        .width('100%')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Row({ space: 8 }) {
        Text('卖家报价')
          .fontSize(13)
          .fontColor('#999999')
        Row().layoutWeight(1)
        Text('¥' + this.curTrade.price)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1A1A1A')
      }
      .width('100%')
      Text('我的出价')
        .fontSize(12)
        .fontColor('#999999')
        .width('100%')
      TextInput({ placeholder: '输入你愿意支付的价格(¥)', text: this.inputPrice })
        .fontSize(14)
        .height(44)
        .backgroundColor('#F6F4FF')
        .borderRadius(12)
        .onChange((v: string) => {
          this.inputPrice = v
        })
      Column({ space: 8 }) {
        Text('快捷出价(卖家报价的折扣)')
          .fontSize(11)
          .fontColor('#999999')
          .width('100%')
        Row({ space: 8 }) {
          ForEach(['9 折', '85 折', '8 折'], (d: string) => {
            Text(d)
              .fontSize(12)
              .fontColor('#6C3BFF')
              .padding({ left: 16, right: 16, top: 8, bottom: 8 })
              .backgroundColor('#F0EBFF')
              .borderRadius(14)
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                if (d === '9 折') {
                  this.inputPrice = Math.round(this.curTrade.price * 0.9).toString()
                } else if (d === '85 折') {
                  this.inputPrice = Math.round(this.curTrade.price * 0.85).toString()
                } else {
                  this.inputPrice = Math.round(this.curTrade.price * 0.8).toString()
                }
              })
          })
        }
        .width('100%')
      }
      .width('100%')

      Text('提交议价')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .width('100%')
        .textAlign(TextAlign.Center)
        .padding({ top: 12, bottom: 12 })
        .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#9F7BFF', 1.0]] })
        .borderRadius(22)
        .onClick(() => {
          this.showBargain = false
          this.okText = '议价已发送,等待卖家回复'
          this.showOk = true
        })
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('86%')
    .position({ x: '7%', y: '12%' })
  }

议价弹窗展示了C2C交易中买方主动出价的交互流程。弹窗顶部显示商品名称和卖家报价作为参考信息,中部提供TextInput输入框供用户输入自定义出价金额。快捷出价区域通过ForEach渲染三个折扣选项(9折、85折、8折),点击时通过Math.round(this.curTrade.price * 0.9).toString()等表达式计算折后价格并自动填入输入框,省去了用户手动计算的步骤。

快捷出价的onClick回调使用了if-else if链判断点击的折扣选项,对应不同的乘数因子。这种设计虽然代码较为直白,但有效地将业务逻辑与用户交互绑定,提升了议价流程的操作效率。

4.3 行情柱状图与交易列表

交易列表的顶部渲染了7日价格行情柱状图,通过ForEach遍历PRICE_TREND数组生成7个纵向柱状条。

          // 行情卡
          Column({ space: 12 }) {
            Row() {
              Text('📊 热门皮肤 7 日行情')
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1A1A1A')
              Row().layoutWeight(1)
              Text('均值 ¥319')
                .fontSize(11)
                .fontColor('#39C27E')
            }
            .width('100%')

            Row({ space: 6 }) {
              ForEach(PRICE_TREND, (t: TrendMeta) => {
                Column({ space: 5 }) {
                  Text('¥' + t.price)
                    .fontSize(9)
                    .fontColor(t.price >= 336 ? '#FF5A5F' : '#999999')
                  Column()
                    .width(16)
                    .height(trendBarH(t.price))
                    .borderRadius(6)
                    .linearGradient({ angle: 180, colors: t.price >= 336 ? [['#FF5A5F', 0.0], ['#FF9F43', 1.0]] : [['#6C3BFF', 0.0], ['#9F7BFF', 1.0]] })
                  Text(t.day)
                    .fontSize(9)
                    .fontColor('#BBBBBB')
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Center)
              })
            }
            .width('100%')
            .alignItems(VerticalAlign.Bottom)
          }

行情柱状图采用了价格阈值判断:当价格大于等于336时,柱状条使用红橙色渐变并显示红色价格文字,表示价格偏高;低于336时使用紫色渐变并显示灰色文字。柱状条高度通过trendBarH函数计算,以260为基准值进行线性映射并限制在18-88像素范围内。

交易列表的渲染逻辑中,商品的操作按钮根据seller字段和state字段动态生成。当seller为’我’时显示改价和下架按钮,其他卖家的在售商品显示议价和立即购买按钮。已售和锁定状态的商品通过opacity(0.6)降低视觉权重,表示不可操作。

五、我的Tab与用户运营体系

5.1 钱包卡与道具库存

我的Tab构建了完整的用户运营界面,包含头部用户信息、钱包卡、道具库存、功能宫格、签到卡等模块。钱包卡部分嵌套在用户信息卡内部,使用了半透明白色背景区分层级。

          // 头部
          Column({ space: 12 }) {
            Row({ space: 12 }) {
              Text('🕹️')
                .fontSize(40)
                .width(62)
                .height(62)
                .textAlign(TextAlign.Center)
                .backgroundColor('rgba(255,255,255,0.22)')
                .borderRadius(31)
              Column({ space: 3 }) {
                Text('夜之主宰')
                  .fontSize(19)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#FFFFFF')
                Text('Lv.42 · 成长值 4,180')
                  .fontSize(11)
                  .fontColor('rgba(255,255,255,0.85)')
                  .onClick(() => {
                    this.showLevel = true
                  })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Text('👑')
                .fontSize(26)
                .onClick(() => {
                  this.showVip = true
                })
            }
            .width('100%')

            // 钱包卡
            Column({ space: 12 }) {
              Row() {
                Column({ space: 3 }) {
                  Text('¥ 268.50')
                    .fontSize(22)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#FFFFFF')
                  Text('账户余额(可提现)')
                    .fontSize(11)
                    .fontColor('rgba(255,255,255,0.8)')
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                Text('提现')
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#6C3BFF')
                  .padding({ left: 18, right: 18, top: 8, bottom: 8 })
                  .backgroundColor('#FFFFFF')
                  .borderRadius(17)
                  .onClick(() => {
                    this.showWithdraw = true
                  })
              }
              .width('100%')

              Row() {
                Column({ space: 2 }) {
                  Text('12,860')
                    .fontSize(15)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#FFFFFF')
                  Text('累计点券')
                    .fontSize(10)
                    .fontColor('rgba(255,255,255,0.8)')
                }
                .layoutWeight(1)
                Column()
                  .width(1)
                  .height(24)
                  .backgroundColor('rgba(255,255,255,0.3)')
                Column({ space: 2 }) {
                  Text('36')
                    .fontSize(15)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#FFFFFF')
                  Text('交易笔数')
                    .fontSize(10)
                    .fontColor('rgba(255,255,255,0.8)')
                }
                .layoutWeight(1)
                Column()
                  .width(1)
                  .height(24)
                  .backgroundColor('rgba(255,255,255,0.3)')
                Column({ space: 2 }) {
                  Text('5.0')
                    .fontSize(15)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#FFFFFF')
                  Text('卖家评分')
                    .fontSize(10)
                    .fontColor('rgba(255,255,255,0.8)')
                }
                .layoutWeight(1)
              }
              .width('100%')
              .padding({ top: 10, bottom: 10 })
              .backgroundColor('rgba(255,255,255,0.12)')
              .borderRadius(12)
            }
            .width('100%')
            .padding(14)
            .backgroundColor('rgba(255,255,255,0.1)')
            .borderRadius(14)
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 20, bottom: 18 })
          .linearGradient({ angle: 135, colors: [['#4B1FB8', 0.0], ['#6C3BFF', 1.0]] })

头部区域使用135度渐变从深紫到暗紫作为背景,用户头像使用半透明白色背景的圆形容器。钱包卡嵌套在头部内部,使用rgba(255,255,255,0.1)的半透明白色背景进一步区分层级。钱包卡底部统计区使用三个等宽Column展示累计点券、交易笔数和卖家评分,列间通过1像素宽24像素高的半透明白色分隔线实现视觉分割。

5.2 余额提现与会员等级弹窗

  @Builder withdrawModal() {
    Column({ space: 14 }) {
      Text('💰 余额提现')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Row({ space: 8 }) {
        Text('可提余额')
          .fontSize(13)
          .fontColor('#999999')
        Row().layoutWeight(1)
        Text('¥ 268.50')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#6C3BFF')
      }
      .width('100%')
      TextInput({ placeholder: '提现金额(不低于 ¥10)', text: this.drawAmount })
        .fontSize(14)
        .height(44)
        .backgroundColor('#F6F4FF')
        .borderRadius(12)
        .onChange((v: string) => {
          this.drawAmount = v
        })
      Column({ space: 8 }) {
        Text('到账方式')
          .fontSize(12)
          .fontColor('#999999')
          .width('100%')
        ForEach(['微信零钱(实时到账)', '银行卡 ****8866(1-2 个工作日)'], (m: string, idx: number) => {
          Row({ space: 8 }) {
            Text(idx === 0 ? '💚' : '🏦')
              .fontSize(16)
            Text(m)
              .fontSize(12)
              .fontColor('#333333')
              .layoutWeight(1)
            Text(idx === 0 ? '◉' : '○')
              .fontSize(15)
              .fontColor('#6C3BFF')
          }
          .width('100%')
          .padding({ top: 9, bottom: 9 })
          .borderRadius(10)
          .backgroundColor(idx === 0 ? '#F0EBFF' : '#FFFFFF')
        })
      }
      .width('100%')

      Row({ space: 10 }) {
        Text('全部提现')
          .fontSize(13)
          .fontColor('#6C3BFF')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#F0EBFF')
          .borderRadius(22)
          .onClick(() => {
            this.drawAmount = '268.50'
          })
        Text('申请提现')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#39C27E', 1.0]] })
          .borderRadius(22)
          .onClick(() => {
            if (this.drawAmount.length === 0) {
              this.okText = '请输入提现金额'
              this.showOk = true
              return
            }
            this.showWithdraw = false
            this.drawAmount = ''
            this.okText = '提现申请已提交,预计 24h 内到账'
            this.showOk = true
          })
      }
      .width('100%')
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('86%')
    .position({ x: '7%', y: '14%' })
  }

提现弹窗集成了金额输入、到账方式选择和快捷操作三个功能。"全部提现"按钮的onClick回调直接将drawAmount设为’268.50’,省去了用户手动输入的步骤。申请提现前进行非空校验,通过后关闭弹窗、清空输入、显示成功提示。到账方式列表使用ForEach渲染两个选项,选中态通过’◉’/'○’字符和背景色变化体现。

六、主页面架构与全局弹窗

6.1 主入口与Tab导航

主入口GameCardPage采用Stack+Column结构,分为头部搜索区、内容区和底部导航三个区域。内容区通过if-else条件链根据activeTab渲染对应Tab组件。

@Entry
@Component
struct GameCardPage {
  @State activeTab: number = 0
  @State showSearch: boolean = false
  @State showMsg: boolean = false
  @State searchKey: string = ''
  @State inputKey: string = ''

  build() {
    Stack() {
      Column() {
        // 头部
        Column({ space: 12 }) {
          Row() {
            Column({ space: 2 }) {
              Text('游戏点卡充值中心')
                .fontSize(19)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('官方直连 · 极速到账')
                .fontSize(11)
                .fontColor('rgba(255,255,255,0.8)')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Stack({ alignContent: Alignment.TopEnd }) {
              Text('🔔')
                .fontSize(24)
                .onClick(() => {
                  this.showMsg = true
                })
              Text('6')
                .fontSize(9)
                .fontColor('#FFFFFF')
                .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                .backgroundColor('#FF5A5F')
                .borderRadius(8)
                .offset({ x: 8, y: -6 })
            }
            .width(40)
            .height(40)
          }
          .width('100%')

          Row({ space: 8 }) {
            Text('🔍')
              .fontSize(15)
            Text(this.searchKey.length > 0 ? this.searchKey : '搜索游戏 · 点卡 · 礼包')
              .fontSize(13)
              .fontColor(this.searchKey.length > 0 ? '#333333' : '#999999')
              .layoutWeight(1)
            Text('搜索')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#6C3BFF')
              .padding({ left: 12, right: 12, top: 5, bottom: 5 })
              .backgroundColor('#FFFFFF')
              .borderRadius(14)
          }
          .width('100%')
          .padding({ left: 14, right: 6, top: 8, bottom: 8 })
          .backgroundColor('rgba(255,255,255,0.92)')
          .borderRadius(22)
          .onClick(() => {
            this.showSearch = true
          })
        }
        .width('100%')
        .padding({ left: 14, right: 14, top: 14, bottom: 14 })
        .linearGradient({ angle: 135, colors: [['#4B1FB8', 0.0], ['#6C3BFF', 1.0]] })

        // 内容区
        Column() {
          if (this.activeTab === 0) {
            RechargeTab()
          } else if (this.activeTab === 1) {
            CardTab()
          } else if (this.activeTab === 2) {
            GiftTab()
          } else if (this.activeTab === 3) {
            TradeTab()
          } else if (this.activeTab === 4) {
            OrderTab()
          } else {
            MineTab()
          }
        }
        .layoutWeight(1)
        .width('100%')

        // 底部导航
        Row() {
          ForEach(MAIN_TABS, (tab: TabMeta) => {
            Column({ space: 4 }) {
              Text(tab.icon)
                .fontSize(22)
                .opacity(this.activeTab === tab.id ? 1 : 0.55)
              Text(tab.name)
                .fontSize(10)
                .fontWeight(this.activeTab === tab.id ? FontWeight.Bold : FontWeight.Normal)
                .fontColor(this.activeTab === tab.id ? '#6C3BFF' : '#999999')
              Column()
                .width(16)
                .height(3)
                .borderRadius(2)
                .backgroundColor(this.activeTab === tab.id ? '#39C27E' : '#FFFFFF00')
            }
            .layoutWeight(1)
            .padding({ top: 6, bottom: 6 })
            .onClick(() => {
              this.activeTab = tab.id
            })
          })
        }
        .width('100%')
        .backgroundColor('#FFFFFF')
        .shadow({ radius: 12, color: 'rgba(0,0,0,0.06)', offsetX: 0, offsetY: -4 })
      }
      .width('100%')
      .height('100%')

      if (this.showSearch) {
        this.modalOverlay(() => {
          this.showSearch = false
        })
        this.searchModal()
      }
      if (this.showMsg) {
        this.modalOverlay(() => {
          this.showMsg = false
        })
        this.msgModal()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F6F4FF')
  }
}

头部搜索区使用半透明白色背景的圆角容器,搜索框文字通过searchKey的长度判断显示用户输入内容还是占位提示。消息通知图标使用Stack配合Alignment.TopEnd叠加未读数角标,角标通过offset属性向右上偏移定位。

底部导航的选中指示条使用荧光绿色(#39C27E)作为选中态颜色,未选中时使用透明色(#FFFFFF00)。图标使用opacity属性区分选中(1.0)和未选中(0.55)状态,配合文字粗细和颜色的变化形成多维度的选中态反馈。

七、核心业务流程

0

1

2

3

4

5

我的商品

他人商品

提现

等级

会员

签到

道具

应用启动

主页面渲染

activeTab

充值Tab

点卡Tab

礼包Tab

交易Tab

订单Tab

我的Tab

选择游戏

选择档位

点击立即充值

弹出充值确认弹窗

选择支付方式

确认支付

显示成功Toast

查看7日行情

浏览寄售列表

卖家身份

改价/下架

议价/购买

applyEdit/applyOff

弹出议价弹窗

输入出价/快捷折扣

提交议价

显示成功Toast

查看钱包余额

操作类型

弹出提现弹窗

弹出等级弹窗

弹出会员弹窗

显示签到成功

提示可寄售

输入金额/选择方式

提交提现申请

显示成功Toast

八、技术点对比总结

技术维度充值Tab点卡Tab礼包Tab交易Tab订单Tab我的Tab
核心状态数8个@State7个@State4个@State10个@State6个@State6个@State
列表渲染Flex档位宫格纵向ForEach双列瀑布流纵向ForEach纵向ForEach横向Scroll+宫格
模态弹窗数4个4个3个6个3个4个
数据更新模式只读展示只读展示只读展示unshift+不可变替换只读展示不可变替换
核心交互逻辑游戏选择+档位+支付分类筛选+购买瀑布流+详情上架/议价/改价/下架筛选+详情+退款提现/等级/会员/签到
视觉特色紫绿渐变充值按钮横卡布局双列瀑布+稀有度标签行情柱状图状态色彩映射钱包卡嵌套层
表单输入商品名+售价+议价价提现金额
条件渲染复杂度档位角标+赠送信息分类筛选联动SSR/SR/R颜色映射卖家身份+状态按钮状态筛选+售后入口多弹窗并联

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// ============================================================
// 底部 6 Tab:充值 / 点卡 / 礼包 / 交易 / 订单 / 我的
// ============================================================

// ---------------- 数据模型 ----------------
interface GameMeta {
  id: number
  name: string
  type: string
  discount: string
  hot: boolean
  pic: string
}

const GAMES: GameMeta[] = [
  { id: 1, name: '王者荣耀', type: 'MOBA', discount: '92 折', hot: true, pic: '👑' },
  { id: 2, name: '原神', type: '开放世界', discount: '95 折', hot: true, pic: '🌌' },
  { id: 3, name: '和平精英', type: '射击', discount: '93 折', hot: true, pic: '🎯' },
  { id: 4, name: '蛋仔派对', type: '休闲', discount: '9 折', hot: true, pic: '🥚' },
  { id: 5, name: '明日方舟', type: '策略', discount: '94 折', hot: false, pic: '🛡️' },
  { id: 6, name: '崩坏:星穹铁道', type: 'RPG', discount: '95 折', hot: true, pic: '🚄' },
  { id: 7, name: '第五人格', type: '非对称', discount: '96 折', hot: false, pic: '🎭' },
  { id: 8, name: '阴阳师', type: '卡牌', discount: '93 折', hot: false, pic: '👻' }
]

interface DenomMeta {
  id: number
  coins: number
  price: number
  bonus: number
  tag: string
}

const DENOMS: DenomMeta[] = [
  { id: 1, coins: 6, price: 6, bonus: 0, tag: '' },
  { id: 2, coins: 30, price: 30, bonus: 0, tag: '' },
  { id: 3, coins: 98, price: 98, bonus: 10, tag: '送10' },
  { id: 4, coins: 198, price: 198, bonus: 25, tag: '送25' },
  { id: 5, coins: 328, price: 328, bonus: 50, tag: '超值' },
  { id: 6, coins: 648, price: 648, bonus: 128, tag: '人气' }
]

interface GiftMeta {
  id: number
  name: string
  game: string
  price: number
  oldPrice: number
  worth: number
  sold: number
  pic: string
  rare: string
}

const GIFTS: GiftMeta[] = [
  { id: 1, name: '赛季战令豪华包', game: '王者荣耀', price: 88, oldPrice: 128, worth: 168, sold: 2341, pic: '🏆', rare: 'SSR' },
  { id: 2, name: '月卡 + 大月卡组合', game: '原神', price: 96, oldPrice: 130, worth: 198, sold: 3120, pic: '🌙', rare: 'SR' },
  { id: 3, name: '新手狂欢十连包', game: '崩坏:星穹铁道', price: 45, oldPrice: 68, worth: 90, sold: 1866, pic: '🎉', rare: 'SR' },
  { id: 4, name: '限定皮肤福袋', game: '王者荣耀', price: 158, oldPrice: 226, worth: 298, sold: 987, pic: '🎁', rare: 'SSR' },
  { id: 5, name: '每日补给月卡', game: '明日方舟', price: 39, oldPrice: 49, worth: 88, sold: 1420, pic: '📦', rare: 'R' },
  { id: 6, name: '赛季手册进阶版', game: '和平精英', price: 68, oldPrice: 98, worth: 128, sold: 2055, pic: '📖', rare: 'SR' },
  { id: 7, name: '盲盒派对礼遇包', game: '蛋仔派对', price: 26, oldPrice: 42, worth: 60, sold: 4321, pic: '🥚', rare: 'R' },
  { id: 8, name: '皮肤自选宝箱', game: '第五人格', price: 118, oldPrice: 168, worth: 236, sold: 654, pic: '🎭', rare: 'SSR' }
]

// ---------------- 充值 Tab ----------------
@Component
struct RechargeTab {
  @State gameIdx: number = 0
  @State denomIdx: number = 3
  @State payIdx: number = 0
  @State showRecharge: boolean = false
  @State showGame: boolean = false
  @State showOk: boolean = false
  @State okText: string = '充值成功'
  @State curGame: GameMeta = GAMES[0]

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(20,10,50,0.6)')
      .onClick(() => {
        onClose()
      })
  }

  @Builder okToast() {
    Column({ space: 8 }) {
      Text('✅')
        .fontSize(34)
      Text(this.okText)
        .fontSize(14)
        .fontColor('#333333')
    }
    .padding(24)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('56%')
    .position({ x: '22%', y: '44%' })
    .shadow({ radius: 20, color: 'rgba(108,59,255,0.2)', offsetX: 0, offsetY: 6 })
  }

  @Builder gameModal() {
    Column({ space: 12 }) {
      Text('🎮 选择要充值的游戏')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(GAMES, (g: GameMeta) => {
          Column({ space: 5 }) {
            Text(g.pic)
              .fontSize(26)
            Text(g.name)
              .fontSize(11)
              .fontColor(this.curGame.id === g.id ? '#6C3BFF' : '#666666')
              .fontWeight(this.curGame.id === g.id ? FontWeight.Bold : FontWeight.Normal)
            Text(g.discount)
              .fontSize(9)
              .fontColor('#39C27E')
          }
          .width('29%')
          .padding({ top: 12, bottom: 12 })
          .backgroundColor(this.curGame.id === g.id ? '#F0EBFF' : '#F6F4FF')
          .borderRadius(12)
          .border({ width: this.curGame.id === g.id ? 1.5 : 0, color: '#6C3BFF' })
          .margin({ right: 8, bottom: 8 })
          .onClick(() => {
            this.curGame = g
            this.showGame = false
            this.okText = '已切换为「' + g.name + '」'
            this.showOk = true
          })
        })
      }
      .width('100%')
      Text('关闭')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#666666')
        .width('100%')
        .textAlign(TextAlign.Center)
        .padding({ top: 11, bottom: 11 })
        .backgroundColor('#F6F4FF')
        .borderRadius(22)
        .onClick(() => {
          this.showGame = false
        })
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('88%')
    .position({ x: '6%', y: '12%' })
    .constraintSize({ maxHeight: '80%' })
  }

  @Builder rechargeModal() {
    Column({ space: 14 }) {
      Text('💳 确认充值订单')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Column({ space: 6 }) {
        Row() {
          Text('游戏')
            .fontSize(13)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text(this.curGame.pic + ' ' + this.curGame.name)
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
        }
        .width('100%')
        Row() {
          Text('档位')
            .fontSize(13)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text(DENOMS[this.denomIdx].coins + ' 点券' + (DENOMS[this.denomIdx].bonus > 0 ? ' + 赠 ' + DENOMS[this.denomIdx].bonus : ''))
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
        }
        .width('100%')
        Row() {
          Text('折扣')
            .fontSize(13)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text(this.curGame.discount)
            .fontSize(13)
            .fontColor('#39C27E')
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        Row() {
          Text('实付')
            .fontSize(13)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text('¥ ' + DENOMS[this.denomIdx].price)
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor('#6C3BFF')
        }
        .width('100%')
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#F6F4FF')
      .borderRadius(12)

      Column({ space: 8 }) {
        Text('支付方式')
          .fontSize(12)
          .fontColor('#999999')
          .width('100%')
        ForEach(['微信支付', 'QQ 钱包', '苹果内购', '余额支付(¥ 268.50)'], (p: string, idx: number) => {
          Row({ space: 8 }) {
            Text(idx === 0 ? '💚' : (idx === 1 ? '🐧' : (idx === 2 ? '🍎' : '💰')))
              .fontSize(18)
            Text(p)
              .fontSize(13)
              .fontColor('#333333')
              .layoutWeight(1)
            Text(this.payIdx === idx ? '◉' : '○')
              .fontSize(16)
              .fontColor('#6C3BFF')
          }
          .width('100%')
          .padding({ top: 9, bottom: 9 })
          .borderRadius(10)
          .backgroundColor(this.payIdx === idx ? '#F0EBFF' : '#FFFFFF')
          .onClick(() => {
            this.payIdx = idx
          })
        })
      }
      .width('100%')

      Row({ space: 10 }) {
        Text('再看看')
          .fontSize(14)
          .fontColor('#999999')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#F6F4FF')
          .borderRadius(22)
          .onClick(() => {
            this.showRecharge = false
          })
        Text('立即支付 ¥' + DENOMS[this.denomIdx].price)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .layoutWeight(1.4)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#9F7BFF', 1.0]] })
          .borderRadius(22)
          .onClick(() => {
            this.showRecharge = false
            this.okText = '充值成功,点券已到账'
            this.showOk = true
          })
      }
      .width('100%')
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('88%')
    .position({ x: '6%', y: '10%' })
    .constraintSize({ maxHeight: '84%' })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 当前游戏卡
          Row({ space: 12 }) {
            Text(this.curGame.pic)
              .fontSize(36)
              .width(62)
              .height(62)
              .textAlign(TextAlign.Center)
              .backgroundColor('rgba(255,255,255,0.2)')
              .borderRadius(16)
            Column({ space: 4 }) {
              Text(this.curGame.name)
                .fontSize(19)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text(this.curGame.type + ' · 点券 ' + this.curGame.discount)
                .fontSize(11)
                .fontColor('rgba(255,255,255,0.8)')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('切换')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .padding({ left: 14, right: 14, top: 7, bottom: 7 })
              .backgroundColor('rgba(255,255,255,0.22)')
              .borderRadius(15)
              .onClick(() => {
                this.showGame = true
              })
          }
          .width('94%')
          .padding(14)
          .linearGradient({ angle: 135, colors: [['#6C3BFF', 0.0], ['#4B1FB8', 1.0]] })
          .borderRadius(16)
          .shadow({ radius: 14, color: 'rgba(108,59,255,0.3)', offsetX: 0, offsetY: 6 })

          // 首充横幅
          Row({ space: 10 }) {
            Text('🚀')
              .fontSize(26)
            Column({ space: 3 }) {
              Text('首充双倍 · 限时返场')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1A1A1A')
              Text('任意档位首充,点券 100% 返还,多充多送')
                .fontSize(11)
                .fontColor('#666666')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('去抢购')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .padding({ left: 14, right: 14, top: 7, bottom: 7 })
              .linearGradient({ angle: 90, colors: [['#39C27E', 0.0], ['#6C3BFF', 1.0]] })
              .borderRadius(15)
              .onClick(() => {
                this.okText = '首充活动已锁定名额'
                this.showOk = true
              })
          }
          .width('94%')
          .padding(14)
          .backgroundColor('#FFFFFF')
          .borderRadius(14)
          .margin({ top: 12 })
          .shadow({ radius: 8, color: 'rgba(108,59,255,0.08)', offsetX: 0, offsetY: 3 })

          // 档位宫格
          Column({ space: 10 }) {
            Text('选择充值档位')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1A1A1A')
              .width('100%')
            Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
              ForEach(DENOMS, (d: DenomMeta, idx: number) => {
                Column({ space: 4 }) {
                  if (d.tag.length > 0) {
                    Text(d.tag)
                      .fontSize(9)
                      .fontColor('#FFFFFF')
                      .padding({ left: 7, right: 7, top: 2, bottom: 2 })
                      .backgroundColor('#FF5A5F')
                      .borderRadius(8)
                  } else {
                    Text(' ')
                      .fontSize(9)
                      .fontColor('#FFFFFF00')
                      .padding({ top: 2, bottom: 2 })
                  }
                  Text(d.coins + ' 点券')
                    .fontSize(16)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(this.denomIdx === idx ? '#6C3BFF' : '#1A1A1A')
                  if (d.bonus > 0) {
                    Text('赠 ' + d.bonus + ' 点券')
                      .fontSize(10)
                      .fontColor('#39C27E')
                  } else {
                    Text('标准档位')
                      .fontSize(10)
                      .fontColor('#BBBBBB')
                  }
                  Text('¥' + d.price)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(this.denomIdx === idx ? '#FFFFFF' : '#6C3BFF')
                    .padding({ left: 14, right: 14, top: 3, bottom: 3 })
                    .backgroundColor(this.denomIdx === idx ? '#6C3BFF' : '#F0EBFF')
                    .borderRadius(12)
                    .margin({ top: 3 })
                }
                .width('31%')
                .padding({ top: 10, bottom: 10 })
                .alignItems(HorizontalAlign.Center)
                .backgroundColor(this.denomIdx === idx ? '#F0EBFF' : '#FFFFFF')
                .borderRadius(14)
                .border({ width: this.denomIdx === idx ? 2 : 1, color: this.denomIdx === idx ? '#6C3BFF' : '#EDE8FB' })
                .margin({ bottom: 10 })
                .onClick(() => {
                  this.denomIdx = idx
                })
              })
            }
            .width('100%')

            Text('立即充值')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .width('100%')
              .textAlign(TextAlign.Center)
              .padding({ top: 13, bottom: 13 })
              .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#39C27E', 1.0]] })
              .borderRadius(24)
              .shadow({ radius: 10, color: 'rgba(108,59,255,0.35)', offsetX: 0, offsetY: 4 })
              .onClick(() => {
                this.showRecharge = true
              })
          }
          .width('94%')
          .padding(14)
          .backgroundColor('#FFFFFF')
          .borderRadius(14)
          .margin({ top: 12 })

          // 热门游戏横滑
          Column({ space: 10 }) {
            Text('🔥 热门游戏')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1A1A1A')
              .width('100%')
            Scroll() {
              Row({ space: 10 }) {
                ForEach(GAMES, (g: GameMeta) => {
                  Column({ space: 6 }) {
                    if (g.hot) {
                      Text('HOT')
                        .fontSize(8)
                        .fontColor('#FFFFFF')
                        .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                        .backgroundColor('#FF5A5F')
                        .borderRadius(6)
                    } else {
                      Text(' ')
                        .fontSize(8)
                        .fontColor('#FFFFFF00')
                        .padding({ top: 2, bottom: 2 })
                    }
                    Text(g.pic)
                      .fontSize(34)
                      .width(58)
                      .height(58)
                      .textAlign(TextAlign.Center)
                      .backgroundColor('#F6F4FF')
                      .borderRadius(14)
                    Text(g.name)
                      .fontSize(12)
                      .fontWeight(FontWeight.Bold)
                      .fontColor('#333333')
                    Text(g.discount)
                      .fontSize(10)
                      .fontColor('#39C27E')
                  }
                  .width(94)
                  .alignItems(HorizontalAlign.Center)
                  .padding({ top: 8, bottom: 10 })
                  .backgroundColor('#FFFFFF')
                  .borderRadius(14)
                  .border({ width: 1, color: '#EDE8FB' })
                  .onClick(() => {
                    this.curGame = g
                    this.okText = '已切换为「' + g.name + '」'
                    this.showOk = true
                  })
                })
              }
            }
            .scrollable(ScrollDirection.Horizontal)
            .scrollBar(BarState.Off)
            .width('100%')
          }
          .width('94%')
          .padding(14)
          .backgroundColor('#FFFFFF')
          .borderRadius(14)
          .margin({ top: 12, bottom: 20 })
        }
        .width('100%')
      }
      .scrollBar(BarState.Off)
      .width('100%')
      .height('100%')
      .backgroundColor('#F6F4FF')

      if (this.showGame) {
        this.modalOverlay(() => {
          this.showGame = false
        })
        this.gameModal()
      }
      if (this.showRecharge) {
        this.modalOverlay(() => {
          this.showRecharge = false
        })
        this.rechargeModal()
      }
      if (this.showOk) {
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor('rgba(0,0,0,0.3)')
          .onClick(() => {
            this.showOk = false
          })
        this.okToast()
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ---------------- 点卡 Tab ----------------
interface CardMeta {
  id: number
  name: string
  face: number
  price: number
  stock: number
  kind: string
  pic: string
}

const CARD_LIST: CardMeta[] = [
  { id: 1, name: '通用游戏点卡 100 元', face: 100, price: 96.5, stock: 328, kind: '通用卡', pic: '💳' },
  { id: 2, name: 'Q 币充值卡 50 元', face: 50, price: 47.8, stock: 512, kind: 'Q币', pic: '🐧' },
  { id: 3, name: 'STEAM 礼品卡 100 美元', face: 100, price: 688, stock: 46, kind: '国际卡', pic: '🎮' },
  { id: 4, name: 'App Store 充值卡 200 元', face: 200, price: 193, stock: 89, kind: '苹果卡', pic: '🍏' },
  { id: 5, name: '任天堂 eShop 点卡 1000 日元', face: 1000, price: 49.5, stock: 134, kind: '国际卡', pic: '🕹️' },
  { id: 6, name: 'PSN 港服点卡 100 港币', face: 100, price: 92, stock: 67, kind: '国际卡', pic: '🎯' },
  { id: 7, name: '某手游月卡兑换券', face: 30, price: 28.6, stock: 890, kind: '通用卡', pic: '📅' },
  { id: 8, name: '通用游戏点卡 500 元', face: 500, price: 481, stock: 22, kind: '通用卡', pic: '💎' }
]

function cardOff(price: number, face: number): string {
  return (price / face * 10).toFixed(1) + ' 折'
}

@Component
struct CardTab {
  @State kindIdx: number = 0
  @State showBuy: boolean = false
  @State showKey: boolean = false
  @State showOk: boolean = false
  @State okText: string = '下单成功'
  @State curCard: CardMeta = CARD_LIST[0]
  @State buyNum: number = 1

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(20,10,50,0.6)')
      .onClick(() => {
        onClose()
      })
  }

  @Builder okToast() {
    Column({ space: 8 }) {
      Text('✅')
        .fontSize(34)
      Text(this.okText)
        .fontSize(14)
        .fontColor('#333333')
    }
    .padding(24)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('56%')
    .position({ x: '22%', y: '44%' })
    .shadow({ radius: 20, color: 'rgba(108,59,255,0.2)', offsetX: 0, offsetY: 6 })
  }

  @Builder buyModal() {
    Column({ space: 14 }) {
      Text('🛒 购买点卡')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Row({ space: 10 }) {
        Text(this.curCard.pic)
          .fontSize(30)
          .width(52)
          .height(52)
          .textAlign(TextAlign.Center)
          .backgroundColor('#F0EBFF')
          .borderRadius(12)
        Column({ space: 3 }) {
          Text(this.curCard.name)
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1A1A1A')
          Text('面值 ¥' + this.curCard.face + ' · ' + cardOff(this.curCard.price, this.curCard.face))
            .fontSize(11)
            .fontColor('#39C27E')
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .width('100%')

      Row({ space: 12 }) {
        Text('购买数量')
          .fontSize(13)
          .fontColor('#666666')
        Row().layoutWeight(1)
        Text('-')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.buyNum <= 1 ? '#CCCCCC' : '#6C3BFF')
          .width(30)
          .height(30)
          .textAlign(TextAlign.Center)
          .backgroundColor('#F6F4FF')
          .borderRadius(15)
          .onClick(() => {
            if (this.buyNum > 1) {
              this.buyNum -= 1
            }
          })
        Text(this.buyNum.toString())
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1A1A1A')
          .width(36)
          .textAlign(TextAlign.Center)
        Text('+')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#6C3BFF')
          .width(30)
          .height(30)
          .textAlign(TextAlign.Center)
          .backgroundColor('#F0EBFF')
          .borderRadius(15)
          .onClick(() => {
            if (this.buyNum < 10) {
              this.buyNum += 1
            }
          })
      }
      .width('100%')

      Row() {
        Text('合计')
          .fontSize(13)
          .fontColor('#999999')
        Row().layoutWeight(1)
        Text('¥ ' + (this.curCard.price * this.buyNum).toFixed(2))
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#6C3BFF')
      }
      .width('100%')

      Text('提交订单')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .width('100%')
        .textAlign(TextAlign.Center)
        .padding({ top: 12, bottom: 12 })
        .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#9F7BFF', 1.0]] })
        .borderRadius(22)
        .onClick(() => {
          this.showBuy = false
          this.showKey = true
        })
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('86%')
    .position({ x: '7%', y: '18%' })
  }

  @Builder keyModal() {
    Column({ space: 14 }) {
      Text('🔐 卡号卡密(请妥善保存)')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Column({ space: 8 }) {
        Row() {
          Text('卡号')
            .fontSize(12)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text('GC-8827-' + (this.curCard.id * 137) + '-' + (this.buyNum * 209))
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
        }
        .width('100%')
        Row() {
          Text('卡密')
            .fontSize(12)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text('K' + (this.curCard.face * 7) + 'X' + (this.buyNum * 361) + 'M9')
            .fontSize(13)
            .fontWeight(FontWeight.Bold)
            .fontColor('#6C3BFF')
        }
        .width('100%')
        Text('有效期至 2027-08-24,充值前请先阅读使用说明')
          .fontSize(11)
          .fontColor('#999999')
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#F6F4FF')
      .borderRadius(12)

      Row({ space: 10 }) {
        Text('复制卡密')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#39C27E')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 11, bottom: 11 })
          .backgroundColor('#E9FBF2')
          .borderRadius(22)
          .onClick(() => {
            this.showKey = false
            this.okText = '卡密已复制到剪贴板'
            this.showOk = true
          })
        Text('我已保存')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 11, bottom: 11 })
          .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#39C27E', 1.0]] })
          .borderRadius(22)
          .onClick(() => {
            this.showKey = false
          })
      }
      .width('100%')
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('86%')
    .position({ x: '7%', y: '22%' })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 分类 chips
          Scroll() {
            Row({ space: 8 }) {
              ForEach(['全部', '通用卡', 'Q币', '国际卡', '苹果卡'], (k: string, idx: number) => {
                Text(k)
                  .fontSize(12)
                  .fontWeight(this.kindIdx === idx ? FontWeight.Bold : FontWeight.Normal)
                  .fontColor(this.kindIdx === idx ? '#FFFFFF' : '#6C3BFF')
                  .padding({ left: 16, right: 16, top: 7, bottom: 7 })
                  .backgroundColor(this.kindIdx === idx ? '#6C3BFF' : '#F0EBFF')
                  .borderRadius(16)
                  .onClick(() => {
                    this.kindIdx = idx
                  })
              })
            }
          }
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .width('100%')

          // 横卡列表
          Column({ space: 10 }) {
            ForEach(CARD_LIST, (c: CardMeta) => {
              if (this.kindIdx === 0 || c.kind === ['全部', '通用卡', 'Q币', '国际卡', '苹果卡'][this.kindIdx]) {
                Row({ space: 12 }) {
                  Column({ space: 3 }) {
                    Text(c.pic)
                      .fontSize(30)
                      .width(56)
                      .height(56)
                      .textAlign(TextAlign.Center)
                      .backgroundColor('#F0EBFF')
                      .borderRadius(14)
                    Text(c.kind)
                      .fontSize(9)
                      .fontColor('#9F7BFF')
                  }
                  .alignItems(HorizontalAlign.Center)

                  Column({ space: 5 }) {
                    Text(c.name)
                      .fontSize(14)
                      .fontWeight(FontWeight.Bold)
                      .fontColor('#1A1A1A')
                      .maxLines(1)
                      .textOverflow({ overflow: TextOverflow.Ellipsis })
                    Row({ space: 8 }) {
                      Text('面值 ¥' + c.face)
                        .fontSize(10)
                        .fontColor('#999999')
                      Text(cardOff(c.price, c.face))
                        .fontSize(10)
                        .fontColor('#39C27E')
                        .padding({ left: 6, right: 6, top: 1, bottom: 1 })
                        .backgroundColor('#E9FBF2')
                        .borderRadius(6)
                      Text('库存 ' + c.stock)
                        .fontSize(10)
                        .fontColor('#FF9F43')
                    }
                    .width('100%')
                  }
                  .alignItems(HorizontalAlign.Start)
                  .layoutWeight(1)

                  Column({ space: 6 }) {
                    Text('¥' + c.price)
                      .fontSize(17)
                      .fontWeight(FontWeight.Bold)
                      .fontColor('#6C3BFF')
                    Text('购买')
                      .fontSize(11)
                      .fontWeight(FontWeight.Bold)
                      .fontColor('#FFFFFF')
                      .padding({ left: 14, right: 14, top: 5, bottom: 5 })
                      .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#9F7BFF', 1.0]] })
                      .borderRadius(13)
                      .onClick(() => {
                        this.curCard = c
                        this.buyNum = 1
                        this.showBuy = true
                      })
                  }
                  .alignItems(HorizontalAlign.End)
                }
                .width('100%')
                .padding(12)
                .backgroundColor('#FFFFFF')
                .borderRadius(14)
                .shadow({ radius: 6, color: 'rgba(108,59,255,0.06)', offsetX: 0, offsetY: 2 })
              }
            })
          }
          .width('94%')
          .margin({ top: 12, bottom: 20 })
        }
        .width('100%')
      }
      .scrollBar(BarState.Off)
      .width('100%')
      .height('100%')
      .backgroundColor('#F6F4FF')

      if (this.showBuy) {
        this.modalOverlay(() => {
          this.showBuy = false
        })
        this.buyModal()
      }
      if (this.showKey) {
        this.modalOverlay(() => {
          this.showKey = false
        })
        this.keyModal()
      }
      if (this.showOk) {
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor('rgba(0,0,0,0.3)')
          .onClick(() => {
            this.showOk = false
          })
        this.okToast()
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ---------------- 礼包 Tab ----------------
function getLeftGifts(): GiftMeta[] {
  const r: GiftMeta[] = []
  for (let i = 0; i < GIFTS.length; i += 2) {
    r.push(GIFTS[i])
  }
  return r
}

function getRightGifts(): GiftMeta[] {
  const r: GiftMeta[] = []
  for (let i = 1; i < GIFTS.length; i += 2) {
    r.push(GIFTS[i])
  }
  return r
}

function rareColor(rare: string): string {
  if (rare === 'SSR') {
    return '#FF5A5F'
  }
  if (rare === 'SR') {
    return '#FFB800'
  }
  return '#9F7BFF'
}

@Component
struct GiftTab {
  @State showDetail: boolean = false
  @State showOk: boolean = false
  @State okText: string = '已加入购物车'
  @State curGift: GiftMeta = GIFTS[0]

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(20,10,50,0.6)')
      .onClick(() => {
        onClose()
      })
  }

  @Builder okToast() {
    Column({ space: 8 }) {
      Text('✅')
        .fontSize(34)
      Text(this.okText)
        .fontSize(14)
        .fontColor('#333333')
    }
    .padding(24)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('56%')
    .position({ x: '22%', y: '44%' })
    .shadow({ radius: 20, color: 'rgba(108,59,255,0.2)', offsetX: 0, offsetY: 6 })
  }

  @Builder detailModal() {
    Column({ space: 14 }) {
      Text('🎁 礼包详情')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Row({ space: 10 }) {
        Text(this.curGift.pic)
          .fontSize(32)
          .width(56)
          .height(56)
          .textAlign(TextAlign.Center)
          .backgroundColor('#F0EBFF')
          .borderRadius(14)
        Column({ space: 3 }) {
          Text(this.curGift.name)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor('#1A1A1A')
          Text(this.curGift.game + ' · 稀有度 ' + this.curGift.rare)
            .fontSize(11)
            .fontColor('#9F7BFF')
        }
        .alignItems(HorizontalAlign.Start)
        .layoutWeight(1)
      }
      .width('100%')

      Column({ space: 8 }) {
        Text('礼包内容')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1A1A1A')
          .width('100%')
        Text('· 限定头像框 × 1')
          .fontSize(12)
          .fontColor('#666666')
          .width('100%')
        Text('· 高级道具自选箱 × 3')
          .fontSize(12)
          .fontColor('#666666')
          .width('100%')
        Text('· 皮肤碎片 × 10')
          .fontSize(12)
          .fontColor('#666666')
          .width('100%')
        Text('· 专属称号「夜之主宰」× 1')
          .fontSize(12)
          .fontColor('#666666')
          .width('100%')
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#F6F4FF')
      .borderRadius(12)

      Row({ space: 8 }) {
        Text('价值 ¥' + this.curGift.worth)
          .fontSize(12)
          .fontColor('#999999')
        Text('已售 ' + this.curGift.sold)
          .fontSize(12)
          .fontColor('#999999')
        Row().layoutWeight(1)
        Text('¥' + this.curGift.price)
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#6C3BFF')
      }
      .width('100%')

      Row({ space: 10 }) {
        Text('加入购物车')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#6C3BFF')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#F0EBFF')
          .borderRadius(22)
          .onClick(() => {
            this.showDetail = false
            this.okText = '「' + this.curGift.name + '」已加入购物车'
            this.showOk = true
          })
        Text('立即购买')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#39C27E', 1.0]] })
          .borderRadius(22)
          .onClick(() => {
            this.showDetail = false
            this.okText = '购买成功,道具已发放到邮箱'
            this.showOk = true
          })
      }
      .width('100%')
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('88%')
    .position({ x: '6%', y: '12%' })
    .constraintSize({ maxHeight: '82%' })
  }

  @Builder giftCard(g: GiftMeta) {
    Column({ space: 8 }) {
      Stack({ alignContent: Alignment.TopEnd }) {
        Text(g.pic)
          .fontSize(40)
          .width('100%')
          .height(88)
          .textAlign(TextAlign.Center)
          .linearGradient({ angle: 135, colors: [['#F0EBFF', 0.0], ['#E0F8EC', 1.0]] })
          .borderRadius(12)
        Text(g.rare)
          .fontSize(9)
          .fontColor('#FFFFFF')
          .padding({ left: 7, right: 7, top: 2, bottom: 2 })
          .backgroundColor(rareColor(g.rare))
          .borderRadius(8)
          .margin(6)
      }
      .width('100%')

      Text(g.name)
        .fontSize(13)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .width('100%')

      Text(g.game)
        .fontSize(10)
        .fontColor('#9F7BFF')
        .width('100%')

      Row({ space: 6 }) {
        Text('¥' + g.price)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#6C3BFF')
        Text('¥' + g.oldPrice)
          .fontSize(10)
          .fontColor('#BBBBBB')
          .decoration({ type: TextDecorationType.LineThrough })
        Row().layoutWeight(1)
        Text('已售' + g.sold)
          .fontSize(9)
          .fontColor('#999999')
      }
      .width('100%')
    }
    .width('100%')
    .padding(10)
    .backgroundColor('#FFFFFF')
    .borderRadius(14)
    .shadow({ radius: 6, color: 'rgba(108,59,255,0.06)', offsetX: 0, offsetY: 2 })
    .onClick(() => {
      this.curGift = g
      this.showDetail = true
    })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 顶部横幅
          Row({ space: 10 }) {
            Text('🎇')
              .fontSize(28)
            Column({ space: 3 }) {
              Text('周末礼包狂欢节')
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
              Text('全场礼包 5 折起,SSR 福袋限量抢')
                .fontSize(11)
                .fontColor('rgba(255,255,255,0.85)')
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
          }
          .width('94%')
          .padding(16)
          .linearGradient({ angle: 135, colors: [['#4B1FB8', 0.0], ['#39C27E', 1.0]] })
          .borderRadius(16)
          .shadow({ radius: 12, color: 'rgba(75,31,184,0.25)', offsetX: 0, offsetY: 5 })

          // 双列瀑布
          Row({ space: 10 }) {
            Column({ space: 10 }) {
              ForEach(getLeftGifts(), (g: GiftMeta) => {
                this.giftCard(g)
              })
            }
            .layoutWeight(1)

            Column({ space: 10 }) {
              ForEach(getRightGifts(), (g: GiftMeta) => {
                this.giftCard(g)
              })
            }
            .layoutWeight(1)
          }
          .width('94%')
          .alignItems(VerticalAlign.Top)
          .margin({ top: 12, bottom: 20 })
        }
        .width('100%')
      }
      .scrollBar(BarState.Off)
      .width('100%')
      .height('100%')
      .backgroundColor('#F6F4FF')

      if (this.showDetail) {
        this.modalOverlay(() => {
          this.showDetail = false
        })
        this.detailModal()
      }
      if (this.showOk) {
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor('rgba(0,0,0,0.3)')
          .onClick(() => {
            this.showOk = false
          })
        this.okToast()
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ---------------- 交易 Tab ----------------
interface TradeMeta {
  id: number
  item: string
  game: string
  price: number
  seller: string
  state: string
  hot: boolean
  pic: string
}

const TRADE_LIST: TradeMeta[] = [
  { id: 1, item: '限定皮肤「星穹之约」', game: '王者荣耀', price: 328, seller: '峡谷收藏家', state: '在售', hot: true, pic: '✨' },
  { id: 2, item: '五星角色账号 60 级', game: '原神', price: 1280, seller: '提瓦特旅人', state: '在售', hot: true, pic: '🗺️' },
  { id: 3, item: '赛季限定枪械皮肤', game: '和平精英', price: 168, seller: '钢枪小王子', state: '在售', hot: false, pic: '🔫' },
  { id: 4, item: '全套 SSR 阵容初始号', game: '明日方舟', price: 666, seller: '刀客塔一号', state: '在售', hot: true, pic: '🃏' },
  { id: 5, item: '盲盒限定染色套装', game: '蛋仔派对', price: 88, seller: '圆滚滚大师', state: '已售', hot: false, pic: '🥚' },
  { id: 6, item: '典藏皮肤「夜之乐章」', game: '第五人格', price: 458, seller: '庄园老玩家', state: '在售', hot: false, pic: '🎻' },
  { id: 7, item: '满命角色成品号', game: '崩坏:星穹铁道', price: 2888, seller: '星穹商人', state: '在售', hot: true, pic: '🌟' },
  { id: 8, item: '绝版限定头像框', game: '阴阳师', price: 199, seller: '平安京代购', state: '锁定', hot: false, pic: '🏮' }
]

interface TrendMeta {
  day: string
  price: number
}

const PRICE_TREND: TrendMeta[] = [
  { day: '周一', price: 302 },
  { day: '周二', price: 315 },
  { day: '周三', price: 289 },
  { day: '周四', price: 328 },
  { day: '周五', price: 345 },
  { day: '周六', price: 318 },
  { day: '周日', price: 336 }
]

function trendBarH(p: number): number {
  let h: number = Math.round((p - 260) / 2)
  if (h > 88) {
    h = 88
  }
  if (h < 18) {
    h = 18
  }
  return h
}

@Component
struct TradeTab {
  @State tradeList: TradeMeta[] = TRADE_LIST
  @State showSell: boolean = false
  @State showBargain: boolean = false
  @State showOff: boolean = false
  @State showEdit: boolean = false
  @State showOk: boolean = false
  @State okText: string = '操作成功'
  @State curTrade: TradeMeta = TRADE_LIST[0]
  @State inputPrice: string = ''
  @State inputItem: string = ''
  @State bargainIdx: number = 0

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(20,10,50,0.6)')
      .onClick(() => {
        onClose()
      })
  }

  @Builder okToast() {
    Column({ space: 8 }) {
      Text('✅')
        .fontSize(34)
      Text(this.okText)
        .fontSize(14)
        .fontColor('#333333')
    }
    .padding(24)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('56%')
    .position({ x: '22%', y: '44%' })
    .shadow({ radius: 20, color: 'rgba(108,59,255,0.2)', offsetX: 0, offsetY: 6 })
  }

  @Builder sellModal() {
    Column({ space: 14 }) {
      Text('📤 上架寄售')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      TextInput({ placeholder: '商品名称(如:限定皮肤 xxx)', text: this.inputItem })
        .fontSize(14)
        .height(44)
        .backgroundColor('#F6F4FF')
        .borderRadius(12)
        .onChange((v: string) => {
          this.inputItem = v
        })
      TextInput({ placeholder: '期望售价(¥)', text: this.inputPrice })
        .fontSize(14)
        .height(44)
        .backgroundColor('#F6F4FF')
        .borderRadius(12)
        .onChange((v: string) => {
          this.inputPrice = v
        })
      Column({ space: 8 }) {
        Text('选择所属游戏')
          .fontSize(12)
          .fontColor('#999999')
          .width('100%')
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(GAMES, (g: GameMeta, idx: number) => {
            Text(g.name)
              .fontSize(11)
              .fontColor(this.bargainIdx === idx ? '#FFFFFF' : '#6C3BFF')
              .padding({ left: 10, right: 10, top: 6, bottom: 6 })
              .backgroundColor(this.bargainIdx === idx ? '#6C3BFF' : '#F0EBFF')
              .borderRadius(12)
              .margin({ right: 8, bottom: 8 })
              .onClick(() => {
                this.bargainIdx = idx
              })
          })
        }
        .width('100%')
      }
      .width('100%')

      Text('平台手续费 5%,成交后自动结算到余额')
        .fontSize(11)
        .fontColor('#999999')
        .width('100%')

      Row({ space: 10 }) {
        Text('取消')
          .fontSize(14)
          .fontColor('#999999')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#F6F4FF')
          .borderRadius(22)
          .onClick(() => {
            this.showSell = false
          })
        Text('确认上架')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .layoutWeight(1.2)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .borderRadius(22)
          .onClick(() => {
            this.applySell()
          })
      }
      .width('100%')
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('88%')
    .position({ x: '6%', y: '8%' })
    .constraintSize({ maxHeight: '84%' })
  }

  @Builder bargainModal() {
    Column({ space: 14 }) {
      Text('🤝 议价申请')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Text(this.curTrade.item)
        .fontSize(13)
        .fontColor('#666666')
        .width('100%')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Row({ space: 8 }) {
        Text('卖家报价')
          .fontSize(13)
          .fontColor('#999999')
        Row().layoutWeight(1)
        Text('¥' + this.curTrade.price)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1A1A1A')
      }
      .width('100%')
      Text('我的出价')
        .fontSize(12)
        .fontColor('#999999')
        .width('100%')
      TextInput({ placeholder: '输入你愿意支付的价格(¥)', text: this.inputPrice })
        .fontSize(14)
        .height(44)
        .backgroundColor('#F6F4FF')
        .borderRadius(12)
        .onChange((v: string) => {
          this.inputPrice = v
        })
      Column({ space: 8 }) {
        Text('快捷出价(卖家报价的折扣)')
          .fontSize(11)
          .fontColor('#999999')
          .width('100%')
        Row({ space: 8 }) {
          ForEach(['9 折', '85 折', '8 折'], (d: string) => {
            Text(d)
              .fontSize(12)
              .fontColor('#6C3BFF')
              .padding({ left: 16, right: 16, top: 8, bottom: 8 })
              .backgroundColor('#F0EBFF')
              .borderRadius(14)
              .layoutWeight(1)
              .textAlign(TextAlign.Center)
              .onClick(() => {
                if (d === '9 折') {
                  this.inputPrice = Math.round(this.curTrade.price * 0.9).toString()
                } else if (d === '85 折') {
                  this.inputPrice = Math.round(this.curTrade.price * 0.85).toString()
                } else {
                  this.inputPrice = Math.round(this.curTrade.price * 0.8).toString()
                }
              })
          })
        }
        .width('100%')
      }
      .width('100%')

      Text('提交议价')
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .width('100%')
        .textAlign(TextAlign.Center)
        .padding({ top: 12, bottom: 12 })
        .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#9F7BFF', 1.0]] })
        .borderRadius(22)
        .onClick(() => {
          this.showBargain = false
          this.okText = '议价已发送,等待卖家回复'
          this.showOk = true
        })
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('86%')
    .position({ x: '7%', y: '12%' })
  }

  @Builder offModal() {
    Column({ space: 16 }) {
      Text('📥 下架确认')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Text('确定下架「' + this.curTrade.item + '」吗?下架后买家将无法再看到该商品')
        .fontSize(13)
        .fontColor('#666666')
        .textAlign(TextAlign.Center)
        .lineHeight(20)
      Row({ space: 10 }) {
        Text('再想想')
          .fontSize(14)
          .fontColor('#999999')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 11, bottom: 11 })
          .backgroundColor('#F6F4FF')
          .borderRadius(22)
          .onClick(() => {
            this.showOff = false
          })
        Text('确认下架')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 11, bottom: 11 })
          .backgroundColor('#FF5A5F')
          .borderRadius(22)
          .onClick(() => {
            this.applyOff()
          })
      }
      .width('100%')
    }
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('78%')
    .position({ x: '11%', y: '30%' })
  }

  @Builder editModal() {
    Column({ space: 14 }) {
      Text('✏️ 修改售价')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Text(this.curTrade.item)
        .fontSize(13)
        .fontColor('#666666')
        .width('100%')
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Row({ space: 8 }) {
        Text('当前售价')
          .fontSize(13)
          .fontColor('#999999')
        Row().layoutWeight(1)
        Text('¥' + this.curTrade.price)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1A1A1A')
      }
      .width('100%')
      TextInput({ placeholder: '新的售价(¥)', text: this.inputPrice })
        .fontSize(14)
        .height(44)
        .backgroundColor('#F6F4FF')
        .borderRadius(12)
        .onChange((v: string) => {
          this.inputPrice = v
        })
      Row({ space: 10 }) {
        Text('取消')
          .fontSize(14)
          .fontColor('#999999')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#F6F4FF')
          .borderRadius(22)
          .onClick(() => {
            this.showEdit = false
          })
        Text('保存新价格')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .layoutWeight(1.2)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .borderRadius(22)
          .onClick(() => {
            this.applyEdit()
          })
      }
      .width('100%')
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('84%')
    .position({ x: '8%', y: '20%' })
  }

  applySell() {
    if (this.inputItem.length === 0 || this.inputPrice.length === 0) {
      this.okText = '请填写商品名称和售价'
      this.showOk = true
      return
    }
    const p: number = Number(this.inputPrice)
    const r: TradeMeta[] = []
    for (let i = 0; i < this.tradeList.length; i++) {
      r.push(this.tradeList[i])
    }
    r.unshift({ id: this.tradeList.length + 1, item: this.inputItem, game: GAMES[this.bargainIdx].name, price: p, seller: '我', state: '在售', hot: false, pic: '🆕' })
    this.tradeList = r
    this.showSell = false
    this.inputItem = ''
    this.inputPrice = ''
    this.okText = '上架成功,等待买家咨询'
    this.showOk = true
  }

  applyOff() {
    const r: TradeMeta[] = []
    for (let i = 0; i < this.tradeList.length; i++) {
      if (this.tradeList[i].id === this.curTrade.id) {
        r.push({ id: this.tradeList[i].id, item: this.tradeList[i].item, game: this.tradeList[i].game, price: this.tradeList[i].price, seller: this.tradeList[i].seller, state: '已下架', hot: false, pic: this.tradeList[i].pic })
      } else {
        r.push(this.tradeList[i])
      }
    }
    this.tradeList = r
    this.showOff = false
    this.okText = '商品已下架'
    this.showOk = true
  }

  applyEdit() {
    const p: number = Number(this.inputPrice)
    if (p <= 0) {
      this.okText = '请输入有效价格'
      this.showOk = true
      return
    }
    const r: TradeMeta[] = []
    for (let i = 0; i < this.tradeList.length; i++) {
      if (this.tradeList[i].id === this.curTrade.id) {
        r.push({ id: this.tradeList[i].id, item: this.tradeList[i].item, game: this.tradeList[i].game, price: p, seller: this.tradeList[i].seller, state: this.tradeList[i].state, hot: this.tradeList[i].hot, pic: this.tradeList[i].pic })
      } else {
        r.push(this.tradeList[i])
      }
    }
    this.tradeList = r
    this.showEdit = false
    this.inputPrice = ''
    this.okText = '价格已更新为 ¥' + p
    this.showOk = true
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 行情卡
          Column({ space: 12 }) {
            Row() {
              Text('📊 热门皮肤 7 日行情')
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1A1A1A')
              Row().layoutWeight(1)
              Text('均值 ¥319')
                .fontSize(11)
                .fontColor('#39C27E')
            }
            .width('100%')

            Row({ space: 6 }) {
              ForEach(PRICE_TREND, (t: TrendMeta) => {
                Column({ space: 5 }) {
                  Text('¥' + t.price)
                    .fontSize(9)
                    .fontColor(t.price >= 336 ? '#FF5A5F' : '#999999')
                  Column()
                    .width(16)
                    .height(trendBarH(t.price))
                    .borderRadius(6)
                    .linearGradient({ angle: 180, colors: t.price >= 336 ? [['#FF5A5F', 0.0], ['#FF9F43', 1.0]] : [['#6C3BFF', 0.0], ['#9F7BFF', 1.0]] })
                  Text(t.day)
                    .fontSize(9)
                    .fontColor('#BBBBBB')
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Center)
              })
            }
            .width('100%')
            .alignItems(VerticalAlign.Bottom)
          }
          .width('94%')
          .padding(14)
          .backgroundColor('#FFFFFF')
          .borderRadius(14)
          .shadow({ radius: 8, color: 'rgba(108,59,255,0.08)', offsetX: 0, offsetY: 3 })

          // 寄售列表
          Row() {
            Text('🛒 玩家寄售')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1A1A1A')
            Row().layoutWeight(1)
            Text('+ 我要上架')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FFFFFF')
              .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              .borderRadius(14)
              .onClick(() => {
                this.inputItem = ''
                this.inputPrice = ''
                this.showSell = true
              })
          }
          .width('94%')
          .margin({ top: 14, bottom: 8 })

          Column({ space: 10 }) {
            ForEach(this.tradeList, (t: TradeMeta) => {
              Column({ space: 10 }) {
                Row({ space: 12 }) {
                  Text(t.pic)
                    .fontSize(30)
                    .width(54)
                    .height(54)
                    .textAlign(TextAlign.Center)
                    .backgroundColor('#F0EBFF')
                    .borderRadius(14)
                  Column({ space: 4 }) {
                    Row({ space: 6 }) {
                      Text(t.item)
                        .fontSize(14)
                        .fontWeight(FontWeight.Bold)
                        .fontColor('#1A1A1A')
                        .maxLines(1)
                        .textOverflow({ overflow: TextOverflow.Ellipsis })
                        .layoutWeight(1)
                      if (t.hot) {
                        Text('热')
                          .fontSize(9)
                          .fontColor('#FFFFFF')
                          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                          .backgroundColor('#FF5A5F')
                          .borderRadius(6)
                      }
                    }
                    .width('100%')
                    Text(t.game + ' · 卖家:' + t.seller)
                      .fontSize(11)
                      .fontColor('#9F7BFF')
                      .maxLines(1)
                      .textOverflow({ overflow: TextOverflow.Ellipsis })
                      .width('100%')
                    Text(t.state === '在售' ? '✅ ' + t.state + ' · 可议价' : (t.state === '锁定' ? '🔒 ' + t.state + ' · 交易中' : '⏹️ ' + t.state))
                      .fontSize(10)
                      .fontColor(t.state === '在售' ? '#39C27E' : (t.state === '锁定' ? '#FF9F43' : '#BBBBBB'))
                  }
                  .alignItems(HorizontalAlign.Start)
                  .layoutWeight(1)

                  Text('¥' + t.price)
                    .fontSize(17)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#6C3BFF')
                }
                .width('100%')

                Row({ space: 8 }) {
                  Row().layoutWeight(1)
                  if (t.seller === '我') {
                    Text('改价')
                      .fontSize(11)
                      .fontColor('#6C3BFF')
                      .padding({ left: 14, right: 14, top: 5, bottom: 5 })
                      .backgroundColor('#F0EBFF')
                      .borderRadius(12)
                      .onClick(() => {
                        this.curTrade = t
                        this.inputPrice = ''
                        this.showEdit = true
                      })
                    Text('下架')
                      .fontSize(11)
                      .fontColor('#FF5A5F')
                      .padding({ left: 14, right: 14, top: 5, bottom: 5 })
                      .backgroundColor('#FFEDEE')
                      .borderRadius(12)
                      .onClick(() => {
                        this.curTrade = t
                        this.showOff = true
                      })
                  } else {
                    Text('议价')
                      .fontSize(11)
                      .fontColor('#39C27E')
                      .padding({ left: 14, right: 14, top: 5, bottom: 5 })
                      .backgroundColor('#E9FBF2')
                      .borderRadius(12)
                      .onClick(() => {
                        this.curTrade = t
                        this.inputPrice = ''
                        this.showBargain = true
                      })
                    Text('立即购买')
                      .fontSize(11)
                      .fontWeight(FontWeight.Bold)
                      .fontColor('#FFFFFF')
                      .padding({ left: 14, right: 14, top: 5, bottom: 5 })
                      .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#9F7BFF', 1.0]] })
                      .borderRadius(12)
                      .onClick(() => {
                        this.okText = '已下单「' + t.item + '」,请尽快付款'
                        this.showOk = true
                      })
                  }
                }
                .width('100%')
              }
              .width('100%')
              .padding(12)
              .backgroundColor('#FFFFFF')
              .borderRadius(14)
              .opacity(t.state === '在售' ? 1 : 0.6)
              .shadow({ radius: 6, color: 'rgba(108,59,255,0.06)', offsetX: 0, offsetY: 2 })
            })
          }
          .width('94%')
          .margin({ bottom: 20 })
        }
        .width('100%')
      }
      .scrollBar(BarState.Off)
      .width('100%')
      .height('100%')
      .backgroundColor('#F6F4FF')

      if (this.showSell) {
        this.modalOverlay(() => {
          this.showSell = false
        })
        this.sellModal()
      }
      if (this.showBargain) {
        this.modalOverlay(() => {
          this.showBargain = false
        })
        this.bargainModal()
      }
      if (this.showOff) {
        this.modalOverlay(() => {
          this.showOff = false
        })
        this.offModal()
      }
      if (this.showEdit) {
        this.modalOverlay(() => {
          this.showEdit = false
        })
        this.editModal()
      }
      if (this.showOk) {
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor('rgba(0,0,0,0.3)')
          .onClick(() => {
            this.showOk = false
          })
        this.okToast()
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ---------------- 订单 Tab ----------------
interface RecMeta {
  id: number
  game: string
  coins: number
  amount: number
  state: string
  time: string
  channel: string
}

const REC_LIST: RecMeta[] = [
  { id: 1, game: '王者荣耀', coins: 648, amount: 648, state: '成功', time: '08-24 09:12', channel: '微信支付' },
  { id: 2, game: '原神', coins: 198, amount: 198, state: '成功', time: '08-23 21:45', channel: '余额支付' },
  { id: 3, game: '蛋仔派对', coins: 30, amount: 27, state: '成功', time: '08-23 19:02', channel: 'QQ 钱包' },
  { id: 4, game: '和平精英', coins: 98, amount: 92, state: '处理中', time: '08-24 10:05', channel: '苹果内购' },
  { id: 5, game: '崩坏:星穹铁道', coins: 328, amount: 311, state: '成功', time: '08-22 13:28', channel: '微信支付' },
  { id: 6, game: '明日方舟', coins: 6, amount: 5.7, state: '失败', time: '08-22 08:31', channel: 'QQ 钱包' },
  { id: 7, game: '王者荣耀', coins: 30, amount: 28.5, state: '成功', time: '08-21 22:17', channel: '微信支付' },
  { id: 8, game: '第五人格', coins: 648, amount: 615, state: '成功', time: '08-20 20:44', channel: '余额支付' }
]

@Component
struct OrderTab {
  @State filterIdx: number = 0
  @State showDetail: boolean = false
  @State showRefund: boolean = false
  @State showOk: boolean = false
  @State okText: string = '操作成功'
  @State curRec: RecMeta = REC_LIST[0]
  @State refundIdx: number = 0

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(20,10,50,0.6)')
      .onClick(() => {
        onClose()
      })
  }

  @Builder okToast() {
    Column({ space: 8 }) {
      Text('✅')
        .fontSize(34)
      Text(this.okText)
        .fontSize(14)
        .fontColor('#333333')
    }
    .padding(24)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('56%')
    .position({ x: '22%', y: '44%' })
    .shadow({ radius: 20, color: 'rgba(108,59,255,0.2)', offsetX: 0, offsetY: 6 })
  }

  @Builder detailModal() {
    Column({ space: 12 }) {
      Text('🧾 订单详情')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Column({ space: 8 }) {
        Row() {
          Text('订单号')
            .fontSize(12)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text('R' + (this.curRec.id * 90210 + 10086))
            .fontSize(12)
            .fontColor('#333333')
        }
        .width('100%')
        Row() {
          Text('游戏')
            .fontSize(12)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text(this.curRec.game)
            .fontSize(12)
            .fontWeight(FontWeight.Bold)
            .fontColor('#333333')
        }
        .width('100%')
        Row() {
          Text('充值内容')
            .fontSize(12)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text(this.curRec.coins + ' 点券')
            .fontSize(12)
            .fontColor('#333333')
        }
        .width('100%')
        Row() {
          Text('支付渠道')
            .fontSize(12)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text(this.curRec.channel)
            .fontSize(12)
            .fontColor('#333333')
        }
        .width('100%')
        Row() {
          Text('下单时间')
            .fontSize(12)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text(this.curRec.time)
            .fontSize(12)
            .fontColor('#333333')
        }
        .width('100%')
        Row() {
          Text('实付金额')
            .fontSize(12)
            .fontColor('#999999')
          Row().layoutWeight(1)
          Text('¥' + this.curRec.amount)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#6C3BFF')
        }
        .width('100%')
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#F6F4FF')
      .borderRadius(12)

      if (this.curRec.state === '成功') {
        Text('对订单有疑问?申请售后')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .borderRadius(22)
          .onClick(() => {
            this.showDetail = false
            this.showRefund = true
          })
      } else {
        Text('知道了')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#666666')
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#F6F4FF')
          .borderRadius(22)
          .onClick(() => {
            this.showDetail = false
          })
      }
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('86%')
    .position({ x: '7%', y: '16%' })
  }

  @Builder refundModal() {
    Column({ space: 14 }) {
      Text('💬 申请售后')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Text('请选择售后原因')
        .fontSize(12)
        .fontColor('#999999')
        .width('100%')
      Column({ space: 8 }) {
        ForEach(['点券未到账', '充值金额错误', '误充值 / 重复下单', '其他问题'], (r: string, idx: number) => {
          Row({ space: 8 }) {
            Text(this.refundIdx === idx ? '◉' : '○')
              .fontSize(15)
              .fontColor('#6C3BFF')
            Text(r)
              .fontSize(13)
              .fontColor('#333333')
              .layoutWeight(1)
          }
          .width('100%')
          .padding({ top: 8, bottom: 8 })
          .borderRadius(10)
          .backgroundColor(this.refundIdx === idx ? '#F0EBFF' : '#FFFFFF')
          .onClick(() => {
            this.refundIdx = idx
          })
        })
      }
      .width('100%')
      Text('提交申请')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .width('100%')
        .textAlign(TextAlign.Center)
        .padding({ top: 12, bottom: 12 })
        .borderRadius(22)
        .onClick(() => {
          this.showRefund = false
          this.okText = '售后工单已提交,客服 24h 内响应'
          this.showOk = true
        })
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('82%')
    .position({ x: '9%', y: '20%' })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          // 筛选 chips
          Scroll() {
            Row({ space: 8 }) {
              ForEach(['全部', '成功', '处理中', '失败'], (s: string, idx: number) => {
                Text(s)
                  .fontSize(12)
                  .fontWeight(this.filterIdx === idx ? FontWeight.Bold : FontWeight.Normal)
                  .fontColor(this.filterIdx === idx ? '#FFFFFF' : '#6C3BFF')
                  .padding({ left: 16, right: 16, top: 7, bottom: 7 })
                  .backgroundColor(this.filterIdx === idx ? '#6C3BFF' : '#F0EBFF')
                  .borderRadius(16)
                  .onClick(() => {
                    this.filterIdx = idx
                  })
              })
            }
          }
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .width('100%')

          // 订单列表
          Column({ space: 10 }) {
            ForEach(REC_LIST, (r: RecMeta) => {
              if (this.filterIdx === 0 || r.state === ['全部', '成功', '处理中', '失败'][this.filterIdx]) {
                Column({ space: 10 }) {
                  Row() {
                    Text(r.time)
                      .fontSize(11)
                      .fontColor('#999999')
                    Row().layoutWeight(1)
                    Text(r.state)
                      .fontSize(11)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(r.state === '成功' ? '#39C27E' : (r.state === '处理中' ? '#FF9F43' : '#FF5A5F'))
                  }
                  .width('100%')

                  Row({ space: 12 }) {
                    Column({ space: 3 }) {
                      Text('🎮')
                        .fontSize(24)
                        .width(44)
                        .height(44)
                        .textAlign(TextAlign.Center)
                        .backgroundColor('#F0EBFF')
                        .borderRadius(12)
                    }
                    Column({ space: 3 }) {
                      Text(r.game + ' · ' + r.coins + ' 点券')
                        .fontSize(14)
                        .fontWeight(FontWeight.Bold)
                        .fontColor('#1A1A1A')
                      Text(r.channel)
                        .fontSize(11)
                        .fontColor('#999999')
                    }
                    .alignItems(HorizontalAlign.Start)
                    .layoutWeight(1)
                    Text('¥' + r.amount)
                      .fontSize(16)
                      .fontWeight(FontWeight.Bold)
                      .fontColor('#6C3BFF')
                  }
                  .width('100%')

                  Row({ space: 8 }) {
                    Row().layoutWeight(1)
                    Text('查看详情')
                      .fontSize(11)
                      .fontColor('#6C3BFF')
                      .padding({ left: 14, right: 14, top: 5, bottom: 5 })
                      .backgroundColor('#F0EBFF')
                      .borderRadius(12)
                      .onClick(() => {
                        this.curRec = r
                        this.showDetail = true
                      })
                    if (r.state === '成功') {
                      Text('再来一单')
                        .fontSize(11)
                        .fontWeight(FontWeight.Bold)
                        .fontColor('#FFFFFF')
                        .padding({ left: 14, right: 14, top: 5, bottom: 5 })
                        .borderRadius(12)
                        .onClick(() => {
                          this.okText = '已复制同款充值档位到首页'
                          this.showOk = true
                        })
                    }
                  }
                  .width('100%')
                }
                .width('100%')
                .padding(12)
                .backgroundColor('#FFFFFF')
                .borderRadius(14)
                .shadow({ radius: 6, color: 'rgba(108,59,255,0.06)', offsetX: 0, offsetY: 2 })
              }
            })
          }
          .width('94%')
          .margin({ top: 12, bottom: 20 })
        }
        .width('100%')
      }
      .scrollBar(BarState.Off)
      .width('100%')
      .height('100%')
      .backgroundColor('#F6F4FF')

      if (this.showDetail) {
        this.modalOverlay(() => {
          this.showDetail = false
        })
        this.detailModal()
      }
      if (this.showRefund) {
        this.modalOverlay(() => {
          this.showRefund = false
        })
        this.refundModal()
      }
      if (this.showOk) {
        Column()
          .width('100%')
          .height('100%')
          .backgroundColor('rgba(0,0,0,0.3)')
          .onClick(() => {
            this.showOk = false
          })
        this.okToast()
      }
    }
    .width('100%')
    .height('100%')
  }
}

// ---------------- 我的 Tab ----------------
@Component
struct MineTab {
  @State showWithdraw: boolean = false
  @State showLevel: boolean = false
  @State showVip: boolean = false
  @State showOk: boolean = false
  @State okText: string = '操作成功'
  @State drawAmount: string = ''

  @Builder modalOverlay(onClose: () => void) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(20,10,50,0.6)')
      .onClick(() => {
        onClose()
      })
  }

  @Builder okToast() {
    Column({ space: 8 }) {
      Text('✅')
        .fontSize(34)
      Text(this.okText)
        .fontSize(14)
        .fontColor('#333333')
    }
    .padding(24)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('56%')
    .position({ x: '22%', y: '44%' })
    .shadow({ radius: 20, color: 'rgba(108,59,255,0.2)', offsetX: 0, offsetY: 6 })
  }

  @Builder withdrawModal() {
    Column({ space: 14 }) {
      Text('💰 余额提现')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Row({ space: 8 }) {
        Text('可提余额')
          .fontSize(13)
          .fontColor('#999999')
        Row().layoutWeight(1)
        Text('¥ 268.50')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#6C3BFF')
      }
      .width('100%')
      TextInput({ placeholder: '提现金额(不低于 ¥10)', text: this.drawAmount })
        .fontSize(14)
        .height(44)
        .backgroundColor('#F6F4FF')
        .borderRadius(12)
        .onChange((v: string) => {
          this.drawAmount = v
        })
      Column({ space: 8 }) {
        Text('到账方式')
          .fontSize(12)
          .fontColor('#999999')
          .width('100%')
        ForEach(['微信零钱(实时到账)', '银行卡 ****8866(1-2 个工作日)'], (m: string, idx: number) => {
          Row({ space: 8 }) {
            Text(idx === 0 ? '💚' : '🏦')
              .fontSize(16)
            Text(m)
              .fontSize(12)
              .fontColor('#333333')
              .layoutWeight(1)
            Text(idx === 0 ? '◉' : '○')
              .fontSize(15)
              .fontColor('#6C3BFF')
          }
          .width('100%')
          .padding({ top: 9, bottom: 9 })
          .borderRadius(10)
          .backgroundColor(idx === 0 ? '#F0EBFF' : '#FFFFFF')
        })
      }
      .width('100%')

      Row({ space: 10 }) {
        Text('全部提现')
          .fontSize(13)
          .fontColor('#6C3BFF')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .backgroundColor('#F0EBFF')
          .borderRadius(22)
          .onClick(() => {
            this.drawAmount = '268.50'
          })
        Text('申请提现')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)
          .padding({ top: 12, bottom: 12 })
          .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#39C27E', 1.0]] })
          .borderRadius(22)
          .onClick(() => {
            if (this.drawAmount.length === 0) {
              this.okText = '请输入提现金额'
              this.showOk = true
              return
            }
            this.showWithdraw = false
            this.drawAmount = ''
            this.okText = '提现申请已提交,预计 24h 内到账'
            this.showOk = true
          })
      }
      .width('100%')
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('86%')
    .position({ x: '7%', y: '14%' })
  }

  @Builder levelModal() {
    Column({ space: 12 }) {
      Text('🏅 玩家等级 Lv.42')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Column({ space: 8 }) {
        Text('当前成长值 4,180 / 5,000')
          .fontSize(12)
          .fontColor('#666666')
          .width('100%')
        Column() {
          Column()
            .width('83.6%')
            .height(8)
            .borderRadius(4)
            .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#39C27E', 1.0]] })
        }
        .width('100%')
        .height(8)
        .borderRadius(4)
        .backgroundColor('#F0EBFF')
      }
      .width('100%')

      Column({ space: 8 }) {
        Text('升级特权(Lv.43 解锁)')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1A1A1A')
          .width('100%')
        Text('· 充值额外 1% 返点券')
          .fontSize(12)
          .fontColor('#666666')
          .width('100%')
        Text('· 寄售手续费降至 4%')
          .fontSize(12)
          .fontColor('#666666')
          .width('100%')
        Text('· 专属客服极速通道')
          .fontSize(12)
          .fontColor('#666666')
          .width('100%')
      }
      .width('100%')
      .padding(14)
      .backgroundColor('#F6F4FF')
      .borderRadius(12)

      Text('继续加油')
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .width('100%')
        .textAlign(TextAlign.Center)
        .padding({ top: 12, bottom: 12 })
        .linearGradient({ angle: 90, colors: [['#6C3BFF', 0.0], ['#9F7BFF', 1.0]] })
        .borderRadius(22)
        .onClick(() => {
          this.showLevel = false
        })
    }
    .padding(18)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .width('84%')
    .position({ x: '8%', y: '16%' })
  }

  @Builder vipModal() {
    Column({ space: 14 }) {
      Text('👑 至尊会员特权')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#1A1A1A')
      Column({ space: 10 }) {
        Row({ space: 10 }) {
          Text('🎮')
            .fontSize(22)
          Column({ space: 2 }) {
            Text('全游戏充值 88 折')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1A1A1A')
            Text('覆盖站内 200+ 款游戏')
              .fontSize(11)
              .fontColor('#999999')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('100%')
        Row({ space: 10 }) {
          Text('⚡')
            .fontSize(22)
          Column({ space: 2 }) {
            Text('充值秒到账')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1A1A1A')
            Text('专属加速通道,高峰期不掉队')
              .fontSize(11)
              .fontColor('#999999')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('100%')
        Row({ space: 10 }) {
          Text('🎁')
            .fontSize(22)
          Column({ space: 2 }) {
            Text('每月礼包码 × 4')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1A1A1A')
            Text('SSR 福袋优先购资格')
              .fontSize(11)
              .fontColor('#999999')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('100%')
        Row({ space: 10 }) {
          Text('🛡️')
            .fontSize(22)
          Column({ space: 2 }) {
            Text('交易双保障')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1A1A1A')
            Text('寄售手续费全免,账号包赔')
              .fontSize(11)
              .fontColor('#999999')
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
        }
        .width('100%')
      }

总结

在这里插入图片描述

本文深入解析了一个基于HarmonyOS ArkTS声明式UI的游戏点卡充值中心应用的完整架构。该应用通过六个功能Tab构建了从游戏充值到个人中心的完整虚拟商品交易平台,涵盖了充值、点卡、礼包、交易、订单和用户中心六大核心业务场景。每个Tab组件都采用了@Component封装的独立struct结构,通过@State状态管理驱动UI的动态渲染,实现了组件内自治的状态管理范式。

在交易系统的设计上,应用实现了完整的C2C寄售交易闭环。从卖家的上架寄售(applySell通过unshift插入新商品)、改价(applyEdit通过不可变替换更新price字段)、下架(applyOff修改state字段),到买家的议价申请(bargainModal支持自定义出价和快捷折扣)、立即购买,覆盖了C2C交易的核心流程。所有数据操作都采用创建新数组的不可变更新模式,确保ArkUI状态管理系统能够正确检测变化并触发重新渲染。议价弹窗的快捷出价功能通过Math.round计算折后价格,将复杂的数学运算封装在简洁的交互中。

在视觉设计方面,应用全面贯彻了暗紫荧光绿双色主题。linearGradient双色渐变贯穿所有强调按钮和头部区域,shadow阴影使用rgba紫色系营造统一的深度层次。礼包卡片通过Stack叠加rareColor动态着色的稀有度标签,交易列表通过opacity区分可操作与不可操作状态,行情柱状图通过价格阈值切换渐变色系。这些视觉技术的综合运用,使得整个应用在保持品牌一致性的同时,各模块又能通过视觉差异化帮助用户快速辨识当前所处的业务场景。Flex弹性布局在档位宫格、功能宫格等场景中通过FlexWrap.Wrap和FlexAlign.SpaceBetween实现了自适应的多列布局,充分展现了ArkUI声明式布局在复杂排列场景下的表达能力。

Logo

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

更多推荐