引言

在这里插入图片描述

集换式卡牌游戏(TCG,Trading Card Game)作为数字娱乐产业的重要分支,融合了收藏、策略对战和社交交易三大核心玩法。在HarmonyOS生态中,如何利用ArkTS声明式UI框架构建一个完整的TCG应用,是一个兼具技术挑战与业务复杂度的课题。本文将深入剖析一个名为"多多卡牌"的HarmonyOS ArkTS应用,该应用以清新潮玩青绿调为设计主题,采用潮玩青(#00838F)与稀有金(#FFB300)的双色搭配方案,底色使用淡青绿(#E0F2F1),构建出一个集卡包购买、集换交易、竞技对战、图鉴收集、社区交流和个人管理于一体的卡牌生态平台。

从技术架构角度来看,该应用基于HarmonyOS ArkTS声明式开发范式,通过@Entry@Component装饰器构建组件化体系。整个应用由主入口组件DuoDuoCardGameApp和六个Tab子组件构成,分别对应卡包、集换、竞技、图鉴、社区和我的六大业务模块。主组件通过@State装饰器管理当前Tab索引、弹窗显示状态、选中卡包ID、购买数量、开包方式等十余项响应式状态,同时通过@Builder装饰器定义了购买弹窗、取消交易弹窗、编辑卡组弹窗和删除收藏确认弹窗四个自定义构建器。

在业务设计层面,该应用构建了一套完整的TCG生命周期管理。卡包购买支持邮寄到家、线上开包和门店自提三种方式,集换市场提供按稀有度筛选的交易列表和买卖记录追踪,竞技模块包含积分趋势柱状图、赛事报名进度条和对战历史记录,图鉴模块实现了按稀有度分级的收藏进度统计和三列网格展示。此外,卡组管理系统支持卡组名称、核心卡牌和战术备注的编辑维护,为竞技对战提供策略支持。整个应用的数据模型定义了十五个接口类型,覆盖了从卡包、交易、赛事到社区、订单、收藏的全部业务实体。

一、类型定义体系:构建卡牌世界数据模型

在这里插入图片描述

ArkTS的接口(interface)机制为应用提供了强类型的数据约束。该应用定义了十五个接口类型,构成了完整的TCG业务数据模型。每个接口都以数字后缀"81"结尾,实现了跨文件的命名空间隔离。

interface CardPack81 {
  id: number
  name: string
  series: string
  price: number
  originalPrice: number
  cards: number
  rarity: string
  stock: number
  rating: number
  sales: number
  tags: string[]
  color: string
  releaseDate: string
  desc: string
}

interface TradeItem81 {
  id: number
  cardName: string
  rarity: string
  seller: string
  price: number
  condition: string
  edition: string
  status: string
  color: string
  desc: string
}

interface Tournament81 {
  id: number
  name: string
  format: string
  date: string
  venue: string
  participants: number
  maxParticipants: number
  prize: string
  level: string
  status: string
  color: string
  desc: string
}

interface CollectionCard81 {
  id: number
  name: string
  rarity: string
  series: string
  owned: boolean
  count: number
  color: string
  power: number
}

interface RarityStat81 {
  rarity: string
  total: number
  owned: number
  color: string
}

CardPack81定义了卡包实体结构,包含名称、系列、价格、原价、卡片数量、稀有度保底信息、库存、评分、销量、标签数组、主题色、发布日期和描述等字段。其中rarity字段存储保底规则(如"SR以上保底"、“SSR保底”),直接决定卡包的价值定位。TradeItem81定义了集换市场中的单卡交易信息,包含卡名、稀有度、卖家、价格、品相、版本、状态等字段,status字段区分"出售中"和"已预订"两种交易状态。

Tournament81定义了赛事信息,特别值得注意的是participantsmaxParticipants两个字段,前端利用这两个数值的比例渲染报名进度条。CollectionCard81定义了图鉴中的卡牌信息,owned布尔字段标识是否拥有,count字段记录拥有数量,power字段存储攻击力数值。RarityStat81定义了稀有度统计信息,totalowned的比值即为收藏进度。

二、稀有度体系与静态数据层

在这里插入图片描述

TCG应用的核心在于稀有度体系的设计。该应用定义了从UR到R共六个稀有度等级,每个等级对应不同的色彩标识和获取难度。

const CARD_PACKS_81: CardPack81[] = [
  { id: 1, name: '星海觉醒·第一弹', series: '星海觉醒', price: 35, originalPrice: 48, cards: 8, rarity: 'SR以上保底', stock: 3562, rating: 4.9, sales: 12543, tags: ['热销', '新品'], color: '#00838F', releaseDate: '08-20', desc: '每包含8张卡,SR以上保底1张' },
  { id: 2, name: '龙之试炼·典藏包', series: '龙之试炼', price: 88, originalPrice: 128, cards: 15, rarity: 'SSR保底', stock: 854, rating: 5.0, sales: 6234, tags: ['典藏', 'SSR'], color: '#FFB300', releaseDate: '08-15', desc: '含15张卡,SSR保底1张+闪卡1张' },
  { id: 5, name: '神域降临·典藏包', series: '神域降临', price: 128, originalPrice: 188, cards: 20, rarity: 'SSR+UR保底', stock: 156, rating: 5.0, sales: 2876, tags: ['典藏', 'UR'], color: '#6A1B9A', releaseDate: '08-05', desc: '含20张卡,SSR+UR保底+全闪卡' }
]

const RARITY_STATS_81: RarityStat81[] = [
  { rarity: 'UR', total: 12, owned: 3, color: '#6A1B9A' },
  { rarity: 'SSR闪', total: 24, owned: 8, color: '#FFB300' },
  { rarity: 'SSR', total: 48, owned: 12, color: '#C2185B' },
  { rarity: 'SR闪', total: 96, owned: 35, color: '#FF6F00' },
  { rarity: 'SR', total: 192, owned: 87, color: '#00838F' },
  { rarity: 'R', total: 384, owned: 256, color: '#2E7D32' }
]

const COLLECTION_CARDS_81: CollectionCard81[] = [
  { id: 1, name: '星海之龙', rarity: 'UR', series: '星海觉醒', owned: true, count: 2, color: '#00838F', power: 9500 },
  { id: 2, name: '龙之帝王', rarity: 'UR', series: '龙之试炼', owned: true, count: 1, color: '#FFB300', power: 9800 },
  { id: 7, name: '龙之公主', rarity: 'UR', series: '龙之试炼', owned: false, count: 0, color: '#FFB300', power: 9600 },
  { id: 12, name: '神域之主', rarity: 'UR', series: '神域降临', owned: false, count: 0, color: '#6A1B9A', power: 9900 }
]

const BATTLE_RECORDS_81: BattleRecord81[] = [
  { id: 1, opponent: '棋王降临', result: '胜利', deck: '星海觉醒·龙骑', date: '08-22', turn: 8, duration: '25分钟', color: '#4CAF50' },
  { id: 2, opponent: '卡牌大师', result: '失败', deck: '机械纪元·战神', date: '08-20', turn: 12, duration: '35分钟', color: '#FF5252' },
  { id: 3, opponent: '幻兽猎人', result: '胜利', deck: '幻兽图鉴·白虎', date: '08-18', turn: 6, duration: '18分钟', color: '#4CAF50' }
]

CARD_PACKS_81数组包含十组卡包数据,覆盖了从18元新手包到188元大礼包的完整价格梯度。每个卡包的rarity字段定义了保底规则——从最低的"R保底"到最高的"SSR+UR保底",保底规则越高级,卡包价格越高。color字段为每个卡包分配了独立的主色调,从潮玩青到稀有金再到深紫,色彩与稀有度形成视觉映射。

RARITY_STATS_81数组是图鉴收藏进度的核心数据源。UR(Ultra Rare)总量仅12张、已拥有3张,是最高稀有度;R(Rare)总量384张、已拥有256张,是基础稀有度。每个稀有度等级使用不同的主题色——UR用深紫、SSR闪用金色、SSR用粉红、SR闪用橙色、SR用潮玩青、R用绿色,形成从稀有到常见的色彩光谱。

COLLECTION_CARDS_81数组中,owned字段为false的卡牌(如龙之公主、神域之主)在图鉴网格中以半透明灰色显示,标识"未拥有"状态。power字段存储的攻击力数值从6200到9900不等,为卡组构筑提供策略参考。

三、主入口组件:状态管理与Tab路由

在这里插入图片描述

主入口组件DuoDuoCardGameApp是整个应用的调度中枢,通过十四个@State变量管理全局状态。

@Entry
@Component
struct DuoDuoCardGameApp {
  @State currentTab: number = 0
  @State showBuyDialog: boolean = false
  @State showCancelDialog: boolean = false
  @State showEditDeckDialog: boolean = false
  @State showDeleteFavDialog: boolean = false
  @State selectedPackId: number = 1
  @State buyQuantity: number = 1
  @State openMethod: number = 0
  @State buyNotes: string = ''
  @State editDeckName: string = ''
  @State editDeckCore: string = ''
  @State editDeckStrategy: string = ''
  @State cancelTargetId: number = 0

  private tabs: string[] = ['卡包', '集换', '竞技', '图鉴', '社区', '我的']
  private openMethods: string[] = ['邮寄到家', '线上开包', '门店自提+开包']

  build() {
    Column() {
      // 顶部头部
      Column() {
        Row() {
          Column() {
            Text('多多卡牌')
              .fontSize(22)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
            Text('TCG·集换·竞技精选')
              .fontSize(11)
              .fontColor('rgba(255,255,255,0.8)')
              .margin({ top: 2 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Row() {
            Text('🃏').fontSize(18).fontColor('#FFFFFF')
          }
          .width(36).height(36).justifyContent(FlexAlign.Center)
          .backgroundColor('rgba(255,255,255,0.2)')
          .borderRadius(18)
        }
        .width('100%').height(56).padding({ left: 16, right: 16 })
        .alignItems(VerticalAlign.Center)

        Row() {
          Text('搜卡包、卡牌、赛事...').fontSize(13).fontColor('rgba(255,255,255,0.6)').layoutWeight(1)
          Text('🔍').fontSize(16).fontColor('rgba(255,255,255,0.6)')
        }
        .width('100%').height(36).margin({ top: 4 })
        .padding({ left: 16, right: 16 })
        .backgroundColor('rgba(255,255,255,0.15)')
        .borderRadius(20)
      }
      .width('100%')
      .linearGradient({ angle: 135, colors: [['#00838F', 0], ['#006064', 1]] })
      .padding({ top: 8, bottom: 12, left: 16, right: 16 })

      // Tab内容区
      Stack({ alignContent: Alignment.TopStart }) {
        Column() {
          if (this.currentTab === 0) {
            PackTab81({
              onBuy: (id: number) => {
                this.selectedPackId = id
                this.showBuyDialog = true
              }
            })
          }
          if (this.currentTab === 1) {
            TradeTab81({
              onTrade: (id: number) => {
                this.cancelTargetId = id
                this.showCancelDialog = true
              }
            })
          }
          if (this.currentTab === 2) {
            CompetitiveTab81()
          }
          if (this.currentTab === 3) {
            CollectionTab81()
          }
          if (this.currentTab === 4) {
            CommunityTab81()
          }
          if (this.currentTab === 5) {
            ProfileTab81({
              onEditDeck: () => {
                this.showEditDeckDialog = true
              }, onCancelOrder: (id: number) => {
                this.cancelTargetId = id
                this.showCancelDialog = true
              }, onDeleteFav: (id: number) => {
                this.cancelTargetId = id
                this.showDeleteFavDialog = true
              }
            })
          }
        }
        .width('100%').height('100%')
      }
      .layoutWeight(1)
      .width('100%')

      // 底部Tab栏
      Row() {
        ForEach(this.tabs, (tab: string, idx: number) => {
          Column() {
            Text(this.getTabIcon(idx))
              .fontSize(20)
              .fontColor(this.currentTab === idx ? '#00838F' : '#999999')
            Text(tab)
              .fontSize(10)
              .fontColor(this.currentTab === idx ? '#00838F' : '#999999')
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .height(56)
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.currentTab = idx
          })
        }, (tab: string) => tab)
      }
      .width('100%')
      .height(56)
      .backgroundColor('#FFFFFF')
      .border({ width: 1, color: '#E0E0E0' })
    }
    .width('100%').height('100%')
    .backgroundColor('#E0F2F1')
    .bindContentCover(this.showCancelDialog, this.buildCancelCover())
    .bindContentCover(this.showDeleteFavDialog, this.buildDeleteFavCover())
  }

  private getTabIcon(idx: number): string {
    const icons: string[] = ['📦', '🔄', '🏆', '📚', '💬', '👤']
    return idx < icons.length ? icons.length ? icons[idx] : '📋' : '📋'
  }
}

主组件的状态管理覆盖了弹窗控制(四个布尔状态)、选中项(selectedPackId)、数量调节(buyQuantity)、开包方式(openMethod)、表单数据(buyNoteseditDeckNameeditDeckCoreeditDeckStrategy)和操作目标ID(cancelTargetId)六大维度。这种集中式状态管理确保了各子Tab和弹窗之间的数据一致性。

Tab路由采用if条件渲染实现页面切换。每个Tab子组件接收回调函数作为参数——PackTab81接收onBuy回调,TradeTab81接收onTrade回调,ProfileTab81接收onEditDeckonCancelOrderonDeleteFav三个回调。当子组件内部触发相应操作时,通过回调函数将事件和数据传递给主组件,主组件更新状态后触发弹窗显示。这种"回调上传+状态下发"的通信模式是ArkTS组件间协作的标准范式。

顶部头部使用135度角渐变,从潮玩青#00838F过渡到深青#006064,底部Tab栏的选中色同样使用#00838F,保持了整体色彩的统一性。六个Tab图标使用Emoji字符——📦(卡包)、🔄(集换)、🏆(竞技)、📚(图鉴)、💬(社区)、👤(我的),每个图标都与其业务含义高度契合。

四、卡包Tab:新卡首发与双列展示

在这里插入图片描述

卡包Tab是应用的首屏,包含分类标签、新卡首发大卡和双列卡包列表三个区域,是用户购买卡包的入口。

@Component
struct PackTab81 {
  onBuy: (id: number) => void = () => {}

  build() {
    Scroll() {
      Column() {
        // 分类标签
        Text('卡包分类').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 12 })
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(PACK_TAGS_81, (tag: PackTag81) => {
            Row() {
              Text(tag.label).fontSize(12).fontColor(tag.color)
              Text(tag.count + '款').fontSize(9).fontColor('#AAAAAA').margin({ left: 4 })
            }
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .margin({ right: 8, top: 8 })
            .backgroundColor(tag.color + '15')
            .borderRadius(16)
            .border({ width: 1, color: tag.color + '30' })
          }, (tag: PackTag81) => tag.label)
        }
        .width('100%').padding({ left: 16, right: 16 })

        // 新卡首发大卡
        Row() {
          Text('新卡首发').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333')
          Text('').layoutWeight(1)
          Text('更多 >').fontSize(12).fontColor('#00838F')
        }
        .width('100%').padding({ left: 16, right: 16, top: 16 })

        Column() {
          Row() {
            Column() {
              Text('星海觉醒·第一弹').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('8张/包 · SR以上保底').fontSize(11).fontColor('rgba(255,255,255,0.8)').margin({ top: 4 })
              Row() {
                Text('¥').fontSize(12).fontColor('#FFFFFF')
                Text('35').fontSize(22).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text('¥48').fontSize(11).fontColor('rgba(255,255,255,0.6)')
                  .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 4 })
                Text('').layoutWeight(1)
                Text('08-20首发').fontSize(10).fontColor('#FFFFFF')
                  .backgroundColor('rgba(255,255,255,0.25)')
                  .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                  .borderRadius(10)
              }
              .width('100%').margin({ top: 12 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('🃏').fontSize(48)
          }
          .width('100%').padding(16)
        }
        .linearGradient({ angle: 135, colors: [['#00838F', 0], ['#006064', 1]] })
        .borderRadius(16)

        // 双列卡包
        Text('全部卡包').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 16 })
        Grid() {
          ForEach(CARD_PACKS_81, (pack: CardPack81) => {
            GridItem() {
              Column() {
                Column() {
                  Text('🃏').fontSize(32)
                }
                .width('100%').height(64)
                .backgroundColor(pack.color + '20')
                .borderRadius({ topLeft: 12, topRight: 12 })
                .justifyContent(FlexAlign.Center)

                Column() {
                  Text(pack.name).fontSize(12).fontColor('#333333').fontWeight(FontWeight.Bold).maxLines(2)
                  Text(pack.series).fontSize(9).fontColor('#AAAAAA').margin({ top: 2 })
                  Text(pack.rarity).fontSize(9).fontColor(pack.color)
                    .backgroundColor(pack.color + '15')
                    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                    .borderRadius(4).margin({ top: 4 })

                  Flex({ wrap: FlexWrap.Wrap }) {
                    ForEach(pack.tags, (tag: string) => {
                      Text(tag).fontSize(9).fontColor(pack.color)
                        .backgroundColor(pack.color + '15')
                        .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                        .borderRadius(4).margin({ right: 4, top: 4 })
                    }, (tag: string) => tag)
                  }

                  Row() {
                    Text(pack.cards + '张').fontSize(9).fontColor('#AAAAAA')
                    Text('库存' + pack.stock).fontSize(9).fontColor('#AAAAAA').margin({ left: 8 })
                  }.margin({ top: 4 })

                  Row() {
                    Text('★').fontSize(10).fontColor('#FFB300')
                    Text(pack.rating.toString()).fontSize(10).fontColor('#FFB300').margin({ left: 2 })
                    Text(pack.sales + '人购买').fontSize(9).fontColor('#AAAAAA').margin({ left: 8 })
                  }.margin({ top: 4 })

                  Row() {
                    Text('¥').fontSize(11).fontColor('#FFB300')
                    Text(pack.price.toString()).fontSize(15).fontColor('#FFB300').fontWeight(FontWeight.Bold)
                    Text('¥' + pack.originalPrice).fontSize(9).fontColor('#CCCCCC')
                      .decoration({ type: TextDecorationType.LineThrough }).margin({ left: 4 })
                  }.margin({ top: 4 })

                  Button() {
                    Text('购买').fontSize(11).fontColor('#FFFFFF')
                  }
                  .width('100%').height(26).margin({ top: 6 })
                  .backgroundColor(pack.color)
                  .borderRadius(13)
                  .onClick(() => { this.onBuy(pack.id) })
                }
                .padding(8)
              }
              .backgroundColor('#FFFFFF')
              .borderRadius(12)
              .border({ width: 1, color: '#F0F0F0' })
            }
          }, (pack: CardPack81) => pack.id.toString())
        }
        .columnsTemplate('1fr 1fr')
        .rowsGap(12).columnsGap(12)
        .padding(16)
      }
    }
    .scrollable(ScrollDirection.Vertical)
    .width('100%').height('100%')
    .backgroundColor('#E0F2F1')
  }
}

分类标签区域使用Flex({ wrap: FlexWrap.Wrap })实现自动换行布局。每个标签的背景色使用tag.color + '15'(约8%透明度),边框使用tag.color + '30'(约19%透明度),通过颜色拼接实现数据驱动的动态配色。

新卡首发大卡是一个独立的渐变卡片,使用与顶部头部相同的135度潮玩青渐变。卡内展示首发卡包的名称、保底规则、价格对比和首发日期标签。原价使用TextDecorationType.LineThrough添加删除线,与现价形成促销对比。

双列卡包列表使用Grid配合columnsTemplate('1fr 1fr')实现两列等宽布局。每个卡包卡片的信息层次丰富——从上到下依次为卡牌Emoji图标区(使用pack.color + '20'背景色)、名称系列、保底标签、业务标签、卡片数量和库存、评分和销量、价格对比和购买按钮。Flex容器内嵌的标签列表使用FlexWrap.Wrap实现自动换行,确保标签数量变化时不会溢出。

五、集换Tab:稀有度筛选与交易记录

在这里插入图片描述

集换Tab是卡牌交易市场的核心模块,包含稀有度筛选、在售卡牌列表和交易记录三个区域。

@Component
struct TradeTab81 {
  onTrade: (id: number) => void = () => {}

  build() {
    Scroll() {
      Column() {
        // 稀有度筛选
        Text('稀有度筛选').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 12 })
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(['全部', 'UR', 'SSR闪卡', 'SSR', 'SR闪卡', 'SR'], (rarity: string) => {
            Text(rarity).fontSize(12).fontColor('#00838F')
              .padding({ left: 10, right: 10, top: 6, bottom: 6 })
              .margin({ right: 8, top: 8 })
              .backgroundColor('#E0F2F1')
              .borderRadius(16)
          }, (rarity: string) => rarity)
        }
        .width('100%').padding({ left: 16, right: 16 })

        // 集换市场列表
        Text('在售卡牌').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 16 })
        Column() {
          ForEach(TRADE_ITEMS_81, (item: TradeItem81) => {
            Row() {
              Column() {
                Text('🃏').fontSize(28)
              }
              .width(72).height(72)
              .backgroundColor(item.color + '20')
              .borderRadius(12)
              .justifyContent(FlexAlign.Center)

              Column() {
                Row() {
                  Text(item.cardName).fontSize(13).fontColor('#333333').fontWeight(FontWeight.Bold).maxLines(1).layoutWeight(1)
                  Text(item.rarity).fontSize(9).fontColor('#FFFFFF')
                    .backgroundColor(item.color)
                    .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                    .borderRadius(4)
                }
                Text(item.desc).fontSize(10).fontColor('#999999').margin({ top: 2 }).maxLines(2)
                Row() {
                  Text('品相:' + item.condition).fontSize(10).fontColor('#AAAAAA')
                  Text('版本:' + item.edition).fontSize(10).fontColor('#AAAAAA').margin({ left: 8 })
                }.margin({ top: 4 })
                Row() {
                  Text('卖家:' + item.seller).fontSize(10).fontColor('#AAAAAA')
                  Text(item.status).fontSize(10)
                    .fontColor(item.status === '出售中' ? '#4CAF50' : '#FF9800')
                    .margin({ left: 8 })
                }.margin({ top: 4 })

                Row() {
                  Text('¥').fontSize(12).fontColor(item.color)
                  Text(item.price.toString()).fontSize(16).fontColor(item.color).fontWeight(FontWeight.Bold)
                  Text('').layoutWeight(1)
                  Button() {
                    Text(item.status === '出售中' ? '买入' : '已预订').fontSize(11).fontColor('#FFFFFF')
                  }
                  .height(28)
                  .backgroundColor(item.status === '出售中' ? item.color : '#BDBDBD')
                  .borderRadius(14)
                  .onClick(() => { if (item.status === '出售中') { this.onTrade(item.id) } })
                }
                .width('100%').margin({ top: 4 })
              }
              .margin({ left: 10 })
              .layoutWeight(1)
            }
            .width('100%').padding(10).margin({ top: 8, left: 16, right: 16 })
            .backgroundColor('#FFFFFF')
            .borderRadius(12)
            .border({ width: 1, color: '#F0F0F0' })
          }, (item: TradeItem81) => item.id.toString())
        }

        // 交易记录
        Text('交易记录').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 16 })
        Column() {
          ForEach(TRADE_RECORDS_81, (record: TradeRecord81) => {
            Row() {
              Column() {
                Text(record.cardName).fontSize(12).fontColor('#333333').fontWeight(FontWeight.Medium).maxLines(1)
                Row() {
                  Text(record.type).fontSize(10).fontColor(record.color)
                  Text(record.date).fontSize(10).fontColor('#AAAAAA').margin({ left: 8 })
                }.margin({ top: 2 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Column() {
                Text((record.type === '买入' ? '-' : '+') + '¥' + record.amount)
                  .fontSize(14).fontColor(record.color).fontWeight(FontWeight.Bold)
                Text(record.status).fontSize(9).fontColor('#AAAAAA').margin({ top: 2 })
              }
              .alignItems(HorizontalAlign.End)
            }
            .width('100%').padding(10).margin({ top: 6, left: 16, right: 16 })
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .border({ width: 1, color: '#F0F0F0' })
          }, (record: TradeRecord81) => record.id.toString())
        }
      }
    }
    .scrollable(ScrollDirection.Vertical)
    .width('100%').height('100%')
    .backgroundColor('#E0F2F1')
  }
}

稀有度筛选区域以按钮组形式展示六个稀有度选项,所有按钮使用统一的潮玩青色调和淡青绿背景。在实际应用中,这些筛选按钮可以配合@State变量实现点击过滤逻辑,当前实现展示了静态UI结构。

在售卡牌列表项采用左右布局——左侧72x72的Emoji图标区域使用item.color + '20'背景色,右侧信息区域包含卡名和稀有度标签(稀有度标签使用主题色实色背景白字,视觉冲击力强)、描述文字、品相和版本信息、卖家和状态信息、价格和买入按钮。买入按钮的状态根据item.status动态变化——"出售中"时显示主题色背景和"买入"文字,"已预订"时显示灰色背景和"已预订"文字,并禁用点击事件。

交易记录区域展示了用户的买卖历史。每条记录的金额使用三元运算符record.type === '买入' ? '-' : '+'判断前缀符号——买入显示减号(支出),卖出显示加号(收入)。金额颜色通过record.color绑定,买入记录使用潮玩青、卖出记录使用不同色系,使收支方向一目了然。

六、竞技Tab:积分柱状图与赛事进度

在这里插入图片描述

竞技Tab集成了积分趋势柱状图、赛事列表和对战记录三大模块,是TCG应用竞技属性的集中体现。

@Component
struct CompetitiveTab81 {
  build() {
    Scroll() {
      Column() {
        Text('竞技赛事').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 12 })

        // 排名柱状图
        Text('近8周积分趋势').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 12 })
        Column() {
          Row() {
            ForEach(BAR_DATA_RANK_81, (bar: BarData81) => {
              Column() {
                Text(bar.value.toString()).fontSize(8).fontColor('#999999')
                Column() {}
                .width(18).height(bar.value / 15)
                .backgroundColor(bar.color)
                .borderRadius({ topLeft: 3, topRight: 3 })
                Text(bar.label).fontSize(8).fontColor('#999999').margin({ top: 4 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }, (bar: BarData81) => bar.label)
          }
          .width('100%').height(120)
          .alignItems(VerticalAlign.Bottom)
        }
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .padding(12)

        // 赛事列表
        Text('赛事列表').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 16 })
        Column() {
          ForEach(TOURNAMENTS_81, (tourn: Tournament81) => {
            Column() {
              Row() {
                Column() {
                  Text('🏆').fontSize(28)
                }
                .width(48).height(48).borderRadius(12)
                .backgroundColor(tourn.color + '20')

                Column() {
                  Row() {
                    Text(tourn.name).fontSize(13).fontColor('#333333').fontWeight(FontWeight.Bold).layoutWeight(1)
                    Text(tourn.level).fontSize(9).fontColor('#FFFFFF')
                      .backgroundColor(tourn.color)
                      .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                      .borderRadius(4)
                  }
                  Text(tourn.format + ' · ' + tourn.prize).fontSize(10).fontColor('#AAAAAA').margin({ top: 2 })
                  Row() {
                    Text('📅').fontSize(10)
                    Text(tourn.date).fontSize(10).fontColor('#666666').margin({ left: 2 })
                    Text('📍').fontSize(10).margin({ left: 8 })
                    Text(tourn.venue).fontSize(10).fontColor('#AAAAAA').margin({ left: 2 }).maxLines(1)
                  }.margin({ top: 4 })
                }
                .margin({ left: 10 })
                .layoutWeight(1)
                Text(tourn.status).fontSize(10)
                  .fontColor(tourn.status === '报名中' ? '#4CAF50' :
                    (tourn.status === '即将开放' ? '#FF9800' : '#2196F3'))
              }

              Text(tourn.desc).fontSize(11).fontColor('#999999').margin({ top: 8 })

              // 报名进度
              Row() {
                Text('参赛').fontSize(10).fontColor('#666666')
                Text(tourn.participants + '/' + tourn.maxParticipants).fontSize(10).fontColor(tourn.color).margin({ left: 4 })
                Text('').layoutWeight(1)
                Row() {
                  Column() {}
                  .layoutWeight(tourn.participants).height(4)
                  .backgroundColor(tourn.color)
                  .borderRadius({ topLeft: 2, bottomLeft: 2 })
                  Column() {}
                  .layoutWeight(tourn.maxParticipants - tourn.participants).height(4)
                  .backgroundColor('#E0E0E0')
                  .borderRadius({ topRight: 2, bottomRight: 2 })
                }
                .width('40%')
              }.margin({ top: 8 })

              Button() {
                Text(tourn.status === '报名中' ? '立即报名' :
                  (tourn.status === '即将开放' ? '预约提醒' : '查看详情'))
                  .fontSize(12).fontColor('#FFFFFF')
              }
              .width('100%').height(32).margin({ top: 8 })
              .backgroundColor(tourn.color)
              .borderRadius(16)
            }
            .backgroundColor('#FFFFFF')
            .borderRadius(12)
            .border({ width: 1, color: '#F0F0F0' })
          }, (tourn: Tournament81) => tourn.id.toString())
        }

        // 对战记录
        Text('对战记录').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 16 })
        Column() {
          ForEach(BATTLE_RECORDS_81, (battle: BattleRecord81) => {
            Row() {
              Column() {
                Text('⚔️').fontSize(20)
              }
              .width(36).height(36).borderRadius(18)
              .backgroundColor(battle.color + '20')

              Column() {
                Text('VS ' + battle.opponent).fontSize(12).fontColor('#333333').fontWeight(FontWeight.Medium)
                Text(battle.deck).fontSize(10).fontColor('#AAAAAA').margin({ top: 2 })
                Row() {
                  Text(battle.date).fontSize(9).fontColor('#AAAAAA')
                  Text(battle.turn + '回合').fontSize(9).fontColor('#AAAAAA').margin({ left: 8 })
                  Text(battle.duration).fontSize(9).fontColor('#AAAAAA').margin({ left: 8 })
                }.margin({ top: 2 })
              }
              .margin({ left: 8 })
              .layoutWeight(1)

              Text(battle.result).fontSize(13).fontColor(battle.color).fontWeight(FontWeight.Bold)
            }
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .border({ width: 1, color: '#F0F0F0' })
          }, (battle: BattleRecord81) => battle.id.toString())
        }
      }
    }
    .scrollable(ScrollDirection.Vertical)
    .width('100%').height('100%')
    .backgroundColor('#E0F2F1')
  }
}

积分趋势柱状图通过ForEach遍历八周数据,每根柱子的高度通过bar.value / 15计算得出。前四周数据使用浅青色#4DB6AC,后四周使用主色#00838F,通过颜色深浅区分近期和远期数据。父级Row设置alignItems(VerticalAlign.Bottom)确保柱子底部对齐,形成标准的柱状图视觉效果。

赛事列表中的报名进度条是该组件的技术亮点。通过两个Column组件分别使用layoutWeight(tourn.participants)layoutWeight(tourn.maxParticipants - tourn.participants),按比例分割进度条宽度。已报名部分使用赛事主题色,未满部分使用灰色,通过borderRadius的定向圆角实现左右不同方向的圆角效果。这种利用layoutWeight实现比例进度条的技巧,在ArkTS中无需额外计算像素值即可实现动态进度展示。

赛事状态的颜色采用嵌套三元运算符:tourn.status === '报名中' ? '#4CAF50' : (tourn.status === '即将开放' ? '#FF9800' : '#2196F3'),三种状态对应绿色、橙色和蓝色。按钮文字同样使用三元运算符动态生成——“报名中"显示"立即报名”,“即将开放"显示"预约提醒”,其他状态显示"查看详情"。

对战记录列表展示每场对战的结果。battle.color字段在数据层即被定义为胜利对应绿色#4CAF50、失败对应红色#FF5252,使得结果标签和图标背景色都直接由数据驱动。每条记录还包含使用的卡组名称、对战回合数和持续时间,为用户复盘对战策略提供数据支持。

卡牌生命周期流程图

以下流程图展示了从购买卡包到竞技对战的完整卡牌生命周期:

用户进入卡包Tab

浏览分类标签

查看新卡首发大卡

是否购买?

选择卡包

弹出购买弹窗

选择开包方式

设置购买数量

填写备注

确认购买

进入集换Tab

按稀有度筛选

查看在售卡牌

是否买入?

点击买入按钮

查看交易记录

弹出取消交易确认

进入竞技Tab

查看积分趋势

浏览赛事列表

是否报名?

立即报名

查看对战记录

进入图鉴Tab

查看收藏进度

浏览卡牌图鉴网格

是否编辑卡组?

进入我的Tab

进入社区Tab

编辑卡组信息

保存卡组

浏览社区动态

七、图鉴Tab:收藏进度与三列网格

图鉴Tab是收藏管理的核心页面,包含按稀有度分级的收藏进度统计和三列卡牌图鉴网格两个区域。

@Component
struct CollectionTab81 {
  build() {
    Scroll() {
      Column() {
        Text('卡牌图鉴').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 12 })

        // 收藏进度
        Text('收藏进度').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 12 })
        Column() {
          ForEach(RARITY_STATS_81, (stat: RarityStat81) => {
            Row() {
              Text(stat.rarity).fontSize(12).fontColor('#333333')
              Text(stat.owned + '/' + stat.total).fontSize(11).fontColor(stat.color).margin({ left: 8 })
              Text((stat.owned / stat.total * 100).toFixed(0) + '%').fontSize(10).fontColor('#AAAAAA').margin({ left: 8 })
              Text('').layoutWeight(1)
              Row() {
                Column() {}
                .layoutWeight(stat.owned).height(6)
                .backgroundColor(stat.color)
                .borderRadius({ topLeft: 3, bottomLeft: 3 })
                Column() {}
                .layoutWeight(stat.total - stat.owned).height(6)
                .backgroundColor('#E0E0E0')
                .borderRadius({ topRight: 3, bottomRight: 3 })
              }
              .width('30%')
            }
            .backgroundColor('#FFFFFF')
            .borderRadius(8)
            .border({ width: 1, color: '#F0F0F0' })
          }, (stat: RarityStat81) => stat.rarity)
        }
        .backgroundColor('#FFFFFF')
        .borderRadius(12)
        .padding(8)

        // 卡牌图鉴网格
        Text('卡牌图鉴').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 16 })
        Grid() {
          ForEach(COLLECTION_CARDS_81, (card: CollectionCard81) => {
            GridItem() {
              Column() {
                Column() {
                  Text('🃏').fontSize(28)
                }
                .width('100%').height(60)
                .backgroundColor(card.owned ? card.color + '20' : '#F5F5F5')
                .borderRadius({ topLeft: 10, topRight: 10 })
                .justifyContent(FlexAlign.Center)
                .opacity(card.owned ? 1 : 0.4)

                Column() {
                  Text(card.name).fontSize(10).fontColor(card.owned ? '#333333' : '#CCCCCC').fontWeight(FontWeight.Medium).maxLines(1)
                  Text(card.rarity).fontSize(8).fontColor(card.owned ? card.color : '#CCCCCC').margin({ top: 2 })
                  Text('攻击力 ' + card.power).fontSize(8).fontColor(card.owned ? '#AAAAAA' : '#CCCCCC').margin({ top: 2 })
                  Text(card.owned ? '×' + card.count : '未拥有').fontSize(8)
                    .fontColor(card.owned ? '#4CAF50' : '#CCCCCC').margin({ top: 2 })
                }
                .padding(6)
              }
              .backgroundColor('#FFFFFF')
              .borderRadius(10)
              .border({ width: 1, color: '#F0F0F0' })
            }
          }, (card: CollectionCard81) => card.id.toString())
        }
        .columnsTemplate('1fr 1fr 1fr')
        .rowsGap(8).columnsGap(8)
        .padding(16)
      }
    }
    .scrollable(ScrollDirection.Vertical)
    .width('100%').height('100%')
    .backgroundColor('#E0F2F1')
  }
}

收藏进度区域遍历RARITY_STATS_81数组,为每个稀有度等级渲染一行进度条。进度条的实现方式与赛事报名进度条相同——使用两个Column配合layoutWeight按比例分割。百分比通过(stat.owned / stat.total * 100).toFixed(0)动态计算,保留整数显示。进度条宽度限制为30%,通过.width('30%')控制,避免在不同稀有度数值差异较大时进度条长度不一的问题。

卡牌图鉴网格使用columnsTemplate('1fr 1fr 1fr')实现三列等宽布局。未拥有的卡牌通过opacity(0.4)降低不透明度,同时图标区域使用灰色背景#F5F5F5而非主题色背景,文字颜色全部使用#CCCCCC灰色,从视觉上明确标识"未拥有"状态。已拥有的卡牌显示拥有数量(如"×2"),未拥有的显示"未拥有"文字,这一设计使图鉴的收藏状态一目了然。

八、购买弹窗与卡组编辑

购买弹窗和卡组编辑弹窗是应用的两大核心交互组件,分别通过@Builder装饰器定义为buildBuySheetbuildEditDeckSheet

@Builder
buildBuySheet() {
  Column() {
    Row() {
      Text('购买卡包').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333')
      Text('').layoutWeight(1)
      Text('✕').fontSize(20).fontColor('#999999').onClick(() => { this.showBuyDialog = false })
    }
    .width('100%').padding(16)

    Scroll() {
      Column() {
        // 选择卡包
        Text('选择卡包').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333')
        Scroll() {
          Row() {
            ForEach(PACK_SELECTS_81, (pack: PackSelect81) => {
              Column() {
                Text(pack.name).fontSize(11)
                  .fontColor(this.selectedPackId === pack.id ? '#FFFFFF' : '#666666').maxLines(1)
                Text(pack.cards + '张/包').fontSize(9)
                  .fontColor(this.selectedPackId === pack.id ? 'rgba(255,255,255,0.8)' : '#AAAAAA').margin({ top: 2 })
                Text('¥' + pack.price).fontSize(12)
                  .fontColor(this.selectedPackId === pack.id ? '#FFFFFF' : pack.color)
                  .fontWeight(FontWeight.Bold).margin({ top: 2 })
              }
              .padding(8).margin({ right: 8 })
              .borderRadius(10)
              .backgroundColor(this.selectedPackId === pack.id ? pack.color : '#F5F5F5')
              .onClick(() => { this.selectedPackId = pack.id })
            }, (pack: PackSelect81) => pack.id.toString())
          }
        }
        .scrollable(ScrollDirection.Horizontal)

        // 开包方式
        Text('开包方式').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ top: 16 })
        Column() {
          ForEach(this.openMethods, (method: string, idx: number) => {
            Row() {
              Radio({ value: idx.toString(), group: 'open' })
                .checked(this.openMethod === idx)
                .onChange((checked: boolean) => { if (checked) { this.openMethod = idx } })
              Text(method).fontSize(12).fontColor('#333333').margin({ left: 8 })
              Text('').layoutWeight(1)
              Text(this.openMethod === idx ? '✓' : '').fontSize(14).fontColor('#00838F')
            }
            .borderRadius(8)
            .backgroundColor(this.openMethod === idx ? '#E0F2F1' : '#F5F5F5')
          }, (method: string, idx: number) => idx.toString())
        }

        // 数量
        Row() {
          Text('购买数量').fontSize(14).fontColor('#333333')
          Text('').layoutWeight(1)
          Row() {
            Button() { Text('-').fontSize(16).fontColor('#666666') }
            .width(32).height(32).backgroundColor('#F5F5F5').borderRadius(16)
            .onClick(() => { if (this.buyQuantity > 1) { this.buyQuantity-- } })
            Text(this.buyQuantity.toString()).fontSize(14).fontColor('#333333').width(40).textAlign(TextAlign.Center)
            Button() { Text('+').fontSize(16).fontColor('#666666') }
            .width(32).height(32).backgroundColor('#F5F5F5').borderRadius(16)
            .onClick(() => { if (this.buyQuantity < 50) { this.buyQuantity++ } })
          }
        }
        .margin({ top: 16 })

        // 备注
        Text('备注').fontSize(14).fontColor('#333333').margin({ top: 16 })
        TextArea({ text: this.buyNotes, placeholder: '如有特殊需求请备注...' })
          .width('100%').height(60).margin({ top: 8 })
          .borderRadius(10).backgroundColor('#F5F5F5')
          .onChange((val: string) => { this.buyNotes = val })

        // 合计
        Row() {
          Text('合计').fontSize(13).fontColor('#666666')
          Text('').layoutWeight(1)
          Text('¥' + this.getTotalPrice()).fontSize(20).fontColor('#FFB300').fontWeight(FontWeight.Bold)
        }
        .margin({ top: 20, bottom: 12 })
      }
    }
    .constraintSize({ maxHeight: '55%' })

    Row() {
      Button() {
        Text('确认购买').fontSize(16).fontColor('#FFFFFF').fontWeight(FontWeight.Medium)
      }
      .layoutWeight(1).height(48)
      .backgroundColor('#00838F')
      .borderRadius(24)
      .onClick(() => { this.showBuyDialog = false })
    }
    .padding(16)
  }
}

private getTotalPrice(): number {
  let pack: PackSelect81 | undefined = PACK_SELECTS_81.find((p: PackSelect81) => p.id === this.selectedPackId)
  let packPrice: number = pack ? pack.price : 0
  return packPrice * this.buyQuantity
}

购买弹窗的卡包选择区域使用横向滚动的卡片列表,选中项使用卡包主题色作为背景色配合白色文字,未选中项使用灰色背景配合深色文字。开包方式选择使用Radio组件实现单选,选中项使用淡青绿#E0F2F1背景并显示勾选标记。购买数量使用减号和加号按钮控制,上限设为50(this.buyQuantity < 50),下限为1。

getTotalPrice方法通过find函数从PACK_SELECTS_81数组中查找当前选中的卡包,计算单价乘以数量。这里使用了TypeScript的联合类型PackSelect81 | undefined和空值检查pack ? pack.price : 0,确保类型安全。

@Builder
buildEditDeckSheet() {
  Column() {
    Row() {
      Text('编辑卡组').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333')
      Text('').layoutWeight(1)
      Text('✕').fontSize(20).fontColor('#999999').onClick(() => { this.showEditDeckDialog = false })
    }
    .width('100%').padding(16)

    Scroll() {
      Column() {
        Text('卡组名称').fontSize(14).fontColor('#666666').margin({ top: 8 })
        TextInput({ text: this.editDeckName, placeholder: '请输入卡组名称' })
          .width('100%').height(44).margin({ top: 8 })
          .borderRadius(10).backgroundColor('#F5F5F5')
          .onChange((val: string) => { this.editDeckName = val })

        Text('核心卡牌').fontSize(14).fontColor('#666666').margin({ top: 12 })
        TextInput({ text: this.editDeckCore, placeholder: '如:星海之龙×3' })
          .width('100%').height(44).margin({ top: 8 })
          .borderRadius(10).backgroundColor('#F5F5F5')
          .onChange((val: string) => { this.editDeckCore = val })

        Text('战术备注').fontSize(14).fontColor('#666666').margin({ top: 12 })
        TextArea({ text: this.editDeckStrategy, placeholder: '请输入战术思路和注意事项...' })
          .width('100%').height(80).margin({ top: 8 })
          .borderRadius(10).backgroundColor('#F5F5F5')
          .onChange((val: string) => { this.editDeckStrategy = val })

        Text('已有卡组').fontSize(14).fontColor('#666666').margin({ top: 16 })
        Column() {
          ForEach(DECKS_81, (deck: DeckItem81) => {
            Column() {
              Row() {
                Text(deck.name).fontSize(13).fontColor('#333333').fontWeight(FontWeight.Medium)
                Text('').layoutWeight(1)
                Text(deck.format).fontSize(9).fontColor(deck.color)
                  .backgroundColor(deck.color + '15')
                  .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                  .borderRadius(4)
              }
              Text(deck.desc).fontSize(11).fontColor('#AAAAAA').margin({ top: 4 })
              Row() {
                Text(deck.cards + '张').fontSize(10).fontColor('#AAAAAA')
                Text('胜率' + deck.winRate + '%').fontSize(10).fontColor(deck.color).margin({ left: 12 })
                Text('').layoutWeight(1)
                Text('编辑').fontSize(10).fontColor('#00838F')
              }.margin({ top: 4 })
            }
            .backgroundColor('#FFFFFF')
            .borderRadius(8)
            .border({ width: 1, color: '#F0F0F0' })
          }, (deck: DeckItem81) => deck.id.toString())
        }
      }
    }
    .constraintSize({ maxHeight: '50%' })

    Row() {
      Button() {
        Text('保存卡组').fontSize(16).fontColor('#FFFFFF').fontWeight(FontWeight.Medium)
      }
      .layoutWeight(1).height(48)
      .backgroundColor('#00838F')
      .borderRadius(24)
      .onClick(() => { this.showEditDeckDialog = false })
    }
    .padding(16)
  }
}

卡组编辑弹窗包含三个表单字段——卡组名称使用TextInput单行输入框,核心卡牌使用TextInput并给出占位提示"如:星海之龙×3",战术备注使用TextArea多行输入框,高度设为80px以容纳较长的战术说明。表单下方展示已有卡组列表,每个卡组项包含名称、赛制标签、描述、卡牌数量和胜率信息,胜率使用主题色显示,"编辑"文字使用潮玩青色作为操作入口。

九、我的Tab:段位展示与消费统计

个人中心Tab集成了渐变头部、消费统计、段位展示、卡组列表、订单管理和收藏管理六大模块。

@Component
struct ProfileTab81 {
  onEditDeck: () => void = () => {}
  onCancelOrder: (id: number) => void = () => {}
  onDeleteFav: (id: number) => void = () => {}

  build() {
    Scroll() {
      Column() {
        // 渐变头部
        Column() {
          Row() {
            Column() {
              Text('🃏').fontSize(40)
            }
            .width(64).height(64).borderRadius(32)
            .backgroundColor('rgba(255,255,255,0.3)')

            Column() {
              Text('卡牌玩家').fontSize(18).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
              Text('ID: CG20260824').fontSize(11).fontColor('rgba(255,255,255,0.8)').margin({ top: 4 })
              Row() {
                Text('铂金I').fontSize(10).fontColor('#FFFFFF')
                  .backgroundColor('rgba(255,255,255,0.25)')
                  .padding({ left: 6, right: 6, top: 1, bottom: 1 })
                  .borderRadius(4)
                Text('积分 7980').fontSize(10).fontColor('rgba(255,255,255,0.8)').margin({ left: 8 })
              }.margin({ top: 4 })
            }
            .margin({ left: 12 })
            .layoutWeight(1)
            Text('编辑卡组').fontSize(11).fontColor('#FFFFFF')
              .backgroundColor('rgba(255,255,255,0.25)')
              .padding({ left: 8, right: 8, top: 4, bottom: 4 })
              .borderRadius(12)
              .onClick(() => { this.onEditDeck() })
          }
        }
        .linearGradient({ angle: 135, colors: [['#00838F', 0], ['#006064', 1]] })

        // 消费统计
        Row() {
          Column() {
            Text('¥3,860').fontSize(20).fontColor('#00838F').fontWeight(FontWeight.Bold)
            Text('总消费').fontSize(10).fontColor('#AAAAAA').margin({ top: 2 })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
          Column() {
            Text('48').fontSize(20).fontColor('#FFB300').fontWeight(FontWeight.Bold)
            Text('拥有卡牌').fontSize(10).fontColor('#AAAAAA').margin({ top: 2 })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
          Column() {
            Text('12').fontSize(20).fontColor('#C2185B').fontWeight(FontWeight.Bold)
            Text('SSR以上').fontSize(10).fontColor('#AAAAAA').margin({ top: 2 })
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Center)
        }
        .backgroundColor('#FFFFFF')
        .borderRadius(12)

        // 段位展示
        Row() {
          Column() {
            Text('🏆').fontSize(32)
          }
          .width(56).height(56).borderRadius(28)
          .backgroundColor('#FFB300')

          Column() {
            Text('铂金I').fontSize(16).fontColor('#333333').fontWeight(FontWeight.Bold)
            Text('距钻石 1020分').fontSize(11).fontColor('#FFB300').margin({ top: 4 })
          }
          .margin({ left: 12 })
          .layoutWeight(1)
          Text('查看 >').fontSize(11).fontColor('#00838F')
        }
        .backgroundColor('#FFFFFF')
        .borderRadius(12)

        // 我的卡组
        Column() {
          ForEach(DECKS_81, (deck: DeckItem81) => {
            Row() {
              Column() {
                Text('🃏').fontSize(20)
              }
              .width(36).height(36).borderRadius(8)
              .backgroundColor(deck.color + '20')

              Column() {
                Text(deck.name).fontSize(13).fontColor('#333333').fontWeight(FontWeight.Medium)
                Text(deck.format + ' · ' + deck.cards + '张').fontSize(10).fontColor('#AAAAAA').margin({ top: 2 })
                Text(deck.desc).fontSize(10).fontColor('#999999').margin({ top: 2 }).maxLines(1)
              }
              .margin({ left: 8 })
              .layoutWeight(1)
              Column() {
                Text(deck.winRate + '%').fontSize(14).fontColor(deck.color).fontWeight(FontWeight.Bold)
                Text('胜率').fontSize(9).fontColor('#AAAAAA')
              }
              .alignItems(HorizontalAlign.End)
            }
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .border({ width: 1, color: '#F0F0F0' })
          }, (deck: DeckItem81) => deck.id.toString())
        }

        // 订单列表
        Column() {
          ForEach(ORDERS_81, (order: OrderItem81) => {
            Column() {
              Row() {
                Text(order.product).fontSize(13).fontColor('#333333').fontWeight(FontWeight.Medium).maxLines(1).layoutWeight(1)
                Text(order.status).fontSize(11).fontColor(order.color)
              }
              Row() {
                Text(order.date + ' · ' + order.quantity + '件').fontSize(10).fontColor('#AAAAAA')
                Text('').layoutWeight(1)
                Text('¥' + order.amount).fontSize(14).fontColor('#FFB300').fontWeight(FontWeight.Bold)
              }.margin({ top: 4 })
              Row() {
                Text('').layoutWeight(1)
                if (order.status === '待发货' || order.status === '已报名') {
                  Text('取消').fontSize(10).fontColor('#FF5252').onClick(() => { this.onCancelOrder(order.id) })
                }
              }.margin({ top: 4 })
            }
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .border({ width: 1, color: '#F0F0F0' })
          }, (order: OrderItem81) => order.id.toString())
        }

        // 收藏列表
        Column() {
          ForEach(FAVORITES_81, (fav: FavoriteItem81) => {
            Row() {
              Column() {
                Text('⭐').fontSize(20)
              }
              .width(40).height(40).borderRadius(8)
              .backgroundColor(fav.color + '15')

              Column() {
                Text(fav.name).fontSize(13).fontColor('#333333')
                Text(fav.rarity).fontSize(10).fontColor('#AAAAAA').margin({ top: 2 })
              }
              .margin({ left: 8 })
              .layoutWeight(1)

              Text('删除').fontSize(11).fontColor('#FF5252').onClick(() => { this.onDeleteFav(fav.id) })
            }
            .backgroundColor('#FFFFFF')
            .borderRadius(10)
            .border({ width: 1, color: '#F0F0F0' })
          }, (fav: FavoriteItem81) => fav.id.toString())
        }
      }
    }
    .scrollable(ScrollDirection.Vertical)
    .backgroundColor('#E0F2F1')
  }
}

渐变头部使用135度潮玩青渐变,与顶部导航栏保持一致。头部右侧的"编辑卡组"按钮使用半透明白色背景,点击触发onEditDeck回调打开卡组编辑弹窗。

消费统计区域使用三等分布局展示总消费(¥3,860)、拥有卡牌数(48)和SSR以上数量(12)三个关键指标。每个指标使用不同的主题色——总消费用潮玩青、卡牌数用稀有金、SSR数用粉红色,形成视觉对比。

段位展示区域以金色奖杯Emoji为核心视觉元素,展示当前段位"铂金I"和距下一段位"钻石"的积分差距。我的卡组列表中每张卡组显示胜率,使用deck.color绑定颜色,胜率以百分比形式用加粗大字号展示。

订单列表中的"取消"操作仅在状态为"待发货"或"已报名"时显示,通过if条件判断实现状态感知的UI渲染。收藏列表中的"删除"操作触发onDeleteFav回调,弹出删除确认弹窗。

十、社区Tab:开包实况与攻略分享

社区Tab是卡牌玩家交流的核心场所,包含热门话题横滑和最新动态信息流。

@Component
struct CommunityTab81 {
  build() {
    Scroll() {
      Column() {
        Column() {
          Text('卡牌社区').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333')
          Text('分享开包,交流战术').fontSize(12).fontColor('#999999').margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Center)

        Text('热门话题').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16 })
        Scroll() {
          Row() {
            ForEach(TOPICS_81, (topic: TopicItem81) => {
              Column() {
                Row() {
                  if (topic.hot) {
                    Text('HOT').fontSize(8).fontColor('#FFFFFF')
                      .backgroundColor('#FF5252')
                      .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                      .borderRadius(4)
                  }
                  Text(topic.title).fontSize(12).fontColor('#333333').fontWeight(FontWeight.Medium)
                    .margin({ left: topic.hot ? 4 : 0 })
                }
                Text(topic.posts + '人参与').fontSize(9).fontColor('#AAAAAA').margin({ top: 4 })
              }
              .padding(12).margin({ right: 8 })
              .backgroundColor('#FFFFFF')
              .borderRadius(10)
              .border({ width: 1, color: topic.color + '30' })
            }, (topic: TopicItem81) => topic.id.toString())
          }
        }
        .scrollable(ScrollDirection.Horizontal)

        Text('最新动态').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 16 })
        Column() {
          ForEach(POSTS_81, (post: CommunityPost81) => {
            Column() {
              Row() {
                Column() {
                  Text('🃏').fontSize(20)
                }
                .width(36).height(36).borderRadius(18)
                .backgroundColor(post.avatarColor + '20')

                Column() {
                  Text(post.author).fontSize(13).fontColor('#333333').fontWeight(FontWeight.Medium)
                  Text(post.timeAgo).fontSize(10).fontColor('#AAAAAA').margin({ top: 2 })
                }
                .margin({ left: 8 })
                .layoutWeight(1)
                Text(post.topic).fontSize(10).fontColor('#00838F')
                  .backgroundColor('#E0F2F1')
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .borderRadius(4)
              }

              Text(post.content).fontSize(12).fontColor('#333333').margin({ top: 10 })

              if (post.images > 0) {
                Row() {
                  ForEach([0, 1, 2], (idx: number) => {
                    if (idx < post.images) {
                      Column() {
                        Text('🖼️').fontSize(18)
                      }
                      .width(72).height(72).margin({ right: 8, top: 8 })
                      .backgroundColor('#F5F5F5')
                      .borderRadius(8)
                    }
                  }, (idx: number) => idx.toString())
                }
              }

              Row() {
                Row() {
                  Text('❤').fontSize(14).fontColor('#E91E63')
                  Text(post.likes.toString()).fontSize(11).fontColor('#999999').margin({ left: 4 })
                }
                Row() {
                  Text('💬').fontSize(14).fontColor('#999999')
                  Text(post.comments.toString()).fontSize(11).fontColor('#999999').margin({ left: 4 })
                }.margin({ left: 24 })
                Row() {
                  Text('📤').fontSize(14).fontColor('#999999')
                  Text(post.shares.toString()).fontSize(11).fontColor('#999999').margin({ left: 4 })
                }.margin({ left: 24 })
                Text('').layoutWeight(1)
                Text('关注').fontSize(11).fontColor('#00838F')
              }.margin({ top: 12 })
            }
            .backgroundColor('#FFFFFF')
            .borderRadius(12)
            .border({ width: 1, color: '#F0F0F0' })
          }, (post: CommunityPost81) => post.id.toString())
        }
      }
    }
    .scrollable(ScrollDirection.Vertical)
    .backgroundColor('#E0F2F1')
  }
}

社区Tab的结构与典型社交媒体的信息流布局一致。热门话题区使用横向滚动展示,热门话题通过topic.hot条件渲染"HOT"标签。话题卡片的边框颜色使用topic.color + '30',将话题主题色融入视觉标识。

动态信息流中每条帖子的话题标签使用潮玩青色文字和淡青绿背景,与应用整体色彩保持一致。图片预览区域使用if条件判断结合嵌套ForEach实现,最多展示三张图片。互动按钮区域使用三个Row容器水平排列点赞、评论和分享按钮,每个按钮包含Emoji图标和数量文本,间距通过margin({ left: 24 })控制。

技术点对比表格

技术维度卡包Tab集换Tab竞技Tab图鉴Tab社区Tab我的Tab
主布局方式双列Grid+横向轮播纵向列表+交易记录柱状图+赛事列表+对战记录进度条+三列Grid横向话题+纵向信息流渐变头部+多模块纵向
核心数据结构CardPack81TradeItem81+TradeRecord81Tournament81+BattleRecord81CollectionCard81+RarityStat81CommunityPost81OrderItem81+DeckItem81
进度条实现layoutWeight报名进度layoutWeight收藏进度
价格展示现价+原价删除线单卡价格+状态赛事奖金订单金额+取消入口
交互回调onBuy(id)onTrade(id)onEditDeck/onCancelOrder/onDeleteFav
视觉特色渐变首发大卡稀有度标签着色8周积分柱状图未拥有卡牌半透明图片预览+互动按钮三色统计+段位奖杯
数据驱动配色pack.color拼接透明度item.color稀有度映射tourn.color赛事色card.owned条件渲染post.avatarColordeck.color胜率色

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 主题:潮玩青 #00838F × 稀有金 #FFB300,底色 #E0F2F1(清新潮玩青绿调)
// 布局差异:分类标签+新卡大卡+双列卡包 / 稀有度筛选+集换列表+交易记录 / 排行柱状图+赛事+对战记录 / 图鉴网格+稀有度统计+收藏进度 / 卡牌动态+热门话题 / 渐变头+消费统计+段位+订单+收藏

// ============ 类型定义 ============
interface CardPack81 {
  id: number
  name: string
  series: string
  price: number
  originalPrice: number
  cards: number
  rarity: string
  stock: number
  rating: number
  sales: number
  tags: string[]
  color: string
  releaseDate: string
  desc: string
}

interface PackTag81 {
  label: string
  color: string
  count: number
}

interface TradeItem81 {
  id: number
  cardName: string
  rarity: string
  seller: string
  price: number
  condition: string
  edition: string
  status: string
  color: string
  desc: string
}

interface TradeRecord81 {
  id: number
  cardName: string
  type: string
  amount: number
  date: string
  status: string
  color: string
}

interface Tournament81 {
  id: number
  name: string
  format: string
  date: string
  venue: string
  participants: number
  maxParticipants: number
  prize: string
  level: string
  status: string
  color: string
  desc: string
}

interface BattleRecord81 {
  id: number
  opponent: string
  result: string
  deck: string
  date: string
  turn: number
  duration: string
  color: string
}

interface BarData81 {
  label: string
  value: number
  color: string
}

interface CollectionCard81 {
  id: number
  name: string
  rarity: string
  series: string
  owned: boolean
  count: number
  color: string
  power: number
}

interface RarityStat81 {
  rarity: string
  total: number
  owned: number
  color: string
}

interface CommunityPost81 {
  id: number
  author: string
  avatarColor: string
  content: string
  likes: number
  comments: number
  shares: number
  images: number
  timeAgo: string
  topic: string
}

interface TopicItem81 {
  id: number
  title: string
  posts: number
  hot: boolean
  color: string
}

interface OrderItem81 {
  id: number
  product: string
  date: string
  amount: number
  status: string
  quantity: number
  color: string
}

interface FavoriteItem81 {
  id: number
  name: string
  rarity: string
  color: string
}

interface DeckItem81 {
  id: number
  name: string
  format: string
  cards: number
  winRate: number
  color: string
  desc: string
}

interface PackSelect81 {
  id: number
  name: string
  price: number
  cards: number
  color: string
}

// ============ 静态数据 ============
const PACK_TAGS_81: PackTag81[] = [
  { label: 'TCG', color: '#00838F', count: 128 },
  { label: '盲盒卡', color: '#FFB300', count: 96 },
  { label: '限量版', color: '#C2185B', count: 85 },
  { label: '新手包', color: '#2E7D32', count: 72 },
  { label: '纪念版', color: '#6A1B9A', count: 64 },
  { label: '典藏版', color: '#8D6E63', count: 58 }
]

const CARD_PACKS_81: CardPack81[] = [
  { id: 1, name: '星海觉醒·第一弹', series: '星海觉醒', price: 35, originalPrice: 48, cards: 8, rarity: 'SR以上保底', stock: 3562, rating: 4.9, sales: 12543, tags: ['热销', '新品'], color: '#00838F', releaseDate: '08-20', desc: '每包含8张卡,SR以上保底1张' },
  { id: 2, name: '龙之试炼·典藏包', series: '龙之试炼', price: 88, originalPrice: 128, cards: 15, rarity: 'SSR保底', stock: 854, rating: 5.0, sales: 6234, tags: ['典藏', 'SSR'], color: '#FFB300', releaseDate: '08-15', desc: '含15张卡,SSR保底1张+闪卡1张' },
  { id: 3, name: '机械纪元·新手包', series: '机械纪元', price: 18, originalPrice: 25, cards: 5, rarity: 'R保底', stock: 5682, rating: 4.6, sales: 18543, tags: ['新手', '平价'], color: '#2E7D32', releaseDate: '07-01', desc: '新手入门首选,5张卡R保底' },
  { id: 4, name: '幻兽图鉴·限定包', series: '幻兽图鉴', price: 68, originalPrice: 98, cards: 12, rarity: 'SR保底+闪卡', stock: 423, rating: 4.8, sales: 4567, tags: ['限定', '闪卡'], color: '#C2185B', releaseDate: '08-10', desc: '限定版含12张卡+闪卡1张' },
  { id: 5, name: '神域降临·典藏包', series: '神域降临', price: 128, originalPrice: 188, cards: 20, rarity: 'SSR+UR保底', stock: 156, rating: 5.0, sales: 2876, tags: ['典藏', 'UR'], color: '#6A1B9A', releaseDate: '08-05', desc: '含20张卡,SSR+UR保底+全闪卡' },
  { id: 6, name: '万灵序曲·纪念包', series: '万灵序曲', price: 48, originalPrice: 68, cards: 10, rarity: 'SR保底', stock: 2876, rating: 4.7, sales: 8543, tags: ['纪念', '热销'], color: '#8D6E63', releaseDate: '07-20', desc: '纪念版含10张卡+SR保底' },
  { id: 7, name: '星海觉醒·补充包', series: '星海觉醒', price: 25, originalPrice: 35, cards: 6, rarity: 'R保底', stock: 6854, rating: 4.7, sales: 15623, tags: ['热销', '补充'], color: '#00838F', releaseDate: '08-20', desc: '6张卡R保底,日常开包首选' },
  { id: 8, name: '龙之试炼·大礼包', series: '龙之试炼', price: 188, originalPrice: 288, cards: 40, rarity: 'SSR+3闪卡', stock: 89, rating: 4.9, sales: 1542, tags: ['超值', '大包'], color: '#FFB300', releaseDate: '08-15', desc: '40张卡+SSR+3闪卡+收藏盒' },
  { id: 9, name: '机械纪元·精英包', series: '机械纪元', price: 58, originalPrice: 78, cards: 10, rarity: 'SR保底', stock: 2345, rating: 4.8, sales: 6754, tags: ['精英'], color: '#1565C0', releaseDate: '07-01', desc: '10张卡SR保底+闪卡概率' },
  { id: 10, name: '幻兽图鉴·稀有包', series: '幻兽图鉴', price: 98, originalPrice: 138, cards: 15, rarity: 'SSR保底+闪卡', stock: 678, rating: 4.9, sales: 3456, tags: ['稀有'], color: '#C2185B', releaseDate: '08-10', desc: '15张卡SSR保底+闪卡2张' }
]

const TRADE_ITEMS_81: TradeItem81[] = [
  { id: 1, cardName: '星海之龙·SSR闪', rarity: 'SSR闪卡', seller: '卡牌收藏家', price: 188, condition: '完美', edition: '初版', status: '出售中', color: '#FFB300', desc: '初版SSR闪卡,品相完美' },
  { id: 2, cardName: '龙之帝王·UR', rarity: 'UR', seller: '神域玩家', price: 588, condition: '完美', edition: '限定版', status: '出售中', color: '#6A1B9A', desc: '限定UR卡,仅出1000张' },
  { id: 3, cardName: '机械神官·SR闪', rarity: 'SR闪卡', seller: '机械大师', price: 45, condition: '近完美', edition: '第一弹', status: '出售中', color: '#00838F', desc: 'SR闪卡,近完美状态' },
  { id: 4, cardName: '幻兽·白虎·SSR', rarity: 'SSR', seller: '幻兽猎人', price: 128, condition: '完美', edition: '限定版', status: '出售中', color: '#C2185B', desc: '限定版SSR,四大神兽系列' },
  { id: 5, cardName: '万灵·凤凰·SSR闪', rarity: 'SSR闪卡', seller: '万灵使', price: 268, condition: '完美', edition: '纪念版', status: '出售中', color: '#FF6F00', desc: '纪念版SSR闪卡' },
  { id: 6, cardName: '星海之龙·SR', rarity: 'SR', seller: '星海玩家', price: 28, condition: '良好', edition: '初版', status: '出售中', color: '#00838F', desc: '初版SR卡,良好状态' },
  { id: 7, cardName: '龙之公主·UR闪', rarity: 'UR闪卡', seller: '龙族守护', price: 888, condition: '完美', edition: '限定版', status: '已预订', color: '#FFB300', desc: '限定UR闪卡,极稀有' },
  { id: 8, cardName: '机械战神·SSR', rarity: 'SSR', seller: '机械工厂', price: 98, condition: '完美', edition: '第二弹', status: '出售中', color: '#1565C0', desc: '第二弹SSR,完美品相' }
]

const TRADE_RECORDS_81: TradeRecord81[] = [
  { id: 1, cardName: '星海之龙·SR', type: '买入', amount: 28, date: '08-22', status: '已完成', color: '#00838F' },
  { id: 2, cardName: '机械神官·SR闪', type: '卖出', amount: 45, date: '08-20', status: '已完成', color: '#FF6F00' },
  { id: 3, cardName: '幻兽·白虎·SSR', type: '买入', amount: 128, date: '08-18', status: '已完成', color: '#C2185B' },
  { id: 4, cardName: '龙之公主·UR闪', type: '卖出', amount: 888, date: '08-15', status: '已完成', color: '#FFB300' },
  { id: 5, cardName: '万灵·凤凰·SSR闪', type: '买入', amount: 268, date: '08-12', status: '运输中', color: '#FF6F00' },
  { id: 6, cardName: '星海之龙·SSR闪', type: '卖出', amount: 188, date: '08-10', status: '已完成', color: '#00838F' }
]

const TOURNAMENTS_81: Tournament81[] = [
  { id: 1, name: '2026全国TCG公开赛', format: '标准赛', date: '09-15~09-16', venue: '上海展览中心', participants: 512, maxParticipants: 1024, prize: '10万元', level: 'S级', status: '报名中', color: '#00838F', desc: '全国最大TCG赛事,总奖金10万' },
  { id: 2, name: '星海觉醒·地区赛', format: '限定赛', date: '09-08', venue: '北京·国贸商城', participants: 128, maxParticipants: 256, prize: '2万元', level: 'A级', status: '报名中', color: '#FFB300', desc: '星海觉醒系列限定赛' },
  { id: 3, name: '龙之试炼·杯赛', format: '淘汰赛', date: '09-01', venue: '广州·天河城', participants: 64, maxParticipants: 128, prize: '5千元', level: 'B级', status: '报名中', color: '#C2185B', desc: '龙之试炼主题杯赛' },
  { id: 4, name: '新手友谊赛', format: '友谊赛', date: '08-30', venue: '线上赛', participants: 256, maxParticipants: 512, prize: '卡包奖励', level: 'C级', status: '报名中', color: '#2E7D32', desc: '新手友好,线上进行' },
  { id: 5, name: '机械纪元·团战赛', format: '团队赛', date: '09-22', venue: '成都·IFS', participants: 32, maxParticipants: 64, prize: '3万元', level: 'A级', status: '即将开放', color: '#1565C0', desc: '3人组队团队赛' },
  { id: 6, name: '万灵序曲·纪念赛', format: '标准赛', date: '10-05', venue: '杭州·大厦', participants: 0, maxParticipants: 128, prize: '1万元', level: 'B级', status: '预告中', color: '#8D6E63', desc: '万灵序曲纪念赛事' }
]

const BATTLE_RECORDS_81: BattleRecord81[] = [
  { id: 1, opponent: '棋王降临', result: '胜利', deck: '星海觉醒·龙骑', date: '08-22', turn: 8, duration: '25分钟', color: '#4CAF50' },
  { id: 2, opponent: '卡牌大师', result: '失败', deck: '机械纪元·战神', date: '08-20', turn: 12, duration: '35分钟', color: '#FF5252' },
  { id: 3, opponent: '幻兽猎人', result: '胜利', deck: '幻兽图鉴·白虎', date: '08-18', turn: 6, duration: '18分钟', color: '#4CAF50' },
  { id: 4, opponent: '神域玩家', result: '胜利', deck: '星海觉醒·龙骑', date: '08-15', turn: 10, duration: '30分钟', color: '#4CAF50' },
  { id: 5, opponent: '龙族守护', result: '失败', deck: '龙之试炼·帝王', date: '08-12', turn: 15, duration: '42分钟', color: '#FF5252' },
  { id: 6, opponent: '万灵使', result: '胜利', deck: '幻兽图鉴·白虎', date: '08-10', turn: 7, duration: '20分钟', color: '#4CAF50' },
  { id: 7, opponent: '星海玩家', result: '胜利', deck: '机械纪元·战神', date: '08-08', turn: 9, duration: '28分钟', color: '#4CAF50' },
  { id: 8, opponent: '机械工厂', result: '失败', deck: '机械纪元·战神', date: '08-05', turn: 11, duration: '33分钟', color: '#FF5252' }
]

const BAR_DATA_RANK_81: BarData81[] = [
  { label: '1周', value: 650, color: '#4DB6AC' },
  { label: '2周', value: 820, color: '#4DB6AC' },
  { label: '3周', value: 780, color: '#4DB6AC' },
  { label: '4周', value: 950, color: '#4DB6AC' },
  { label: '5周', value: 1080, color: '#4DB6AC' },
  { label: '6周', value: 1320, color: '#00838F' },
  { label: '7周', value: 1450, color: '#00838F' },
  { label: '8周', value: 1280, color: '#00838F' }
]

const COLLECTION_CARDS_81: CollectionCard81[] = [
  { id: 1, name: '星海之龙', rarity: 'UR', series: '星海觉醒', owned: true, count: 2, color: '#00838F', power: 9500 },
  { id: 2, name: '龙之帝王', rarity: 'UR', series: '龙之试炼', owned: true, count: 1, color: '#FFB300', power: 9800 },
  { id: 3, name: '机械神官', rarity: 'SSR', series: '机械纪元', owned: true, count: 3, color: '#00838F', power: 8200 },
  { id: 4, name: '幻兽·白虎', rarity: 'SSR', series: '幻兽图鉴', owned: true, count: 1, color: '#C2185B', power: 8500 },
  { id: 5, name: '万灵·凤凰', rarity: 'SSR', series: '万灵序曲', owned: true, count: 2, color: '#FF6F00', power: 8400 },
  { id: 6, name: '机械战神', rarity: 'SSR', series: '机械纪元', owned: true, count: 1, color: '#1565C0', power: 8800 },
  { id: 7, name: '龙之公主', rarity: 'UR', series: '龙之试炼', owned: false, count: 0, color: '#FFB300', power: 9600 },
  { id: 8, name: '星海守护者', rarity: 'SR', series: '星海觉醒', owned: true, count: 4, color: '#00838F', power: 6500 },
  { id: 9, name: '机械士兵', rarity: 'SR', series: '机械纪元', owned: true, count: 6, color: '#00838F', power: 6200 },
  { id: 10, name: '幻兽·朱雀', rarity: 'SSR', series: '幻兽图鉴', owned: false, count: 0, color: '#C2185B', power: 8300 },
  { id: 11, name: '万灵·玄武', rarity: 'SSR', series: '万灵序曲', owned: true, count: 1, color: '#FF6F00', power: 8600 },
  { id: 12, name: '神域之主', rarity: 'UR', series: '神域降临', owned: false, count: 0, color: '#6A1B9A', power: 9900 }
]

const RARITY_STATS_81: RarityStat81[] = [
  { rarity: 'UR', total: 12, owned: 3, color: '#6A1B9A' },
  { rarity: 'SSR闪', total: 24, owned: 8, color: '#FFB300' },
  { rarity: 'SSR', total: 48, owned: 12, color: '#C2185B' },
  { rarity: 'SR闪', total: 96, owned: 35, color: '#FF6F00' },
  { rarity: 'SR', total: 192, owned: 87, color: '#00838F' },
  { rarity: 'R', total: 384, owned: 256, color: '#2E7D32' }
]

const POSTS_81: CommunityPost81[] = [
  { id: 1, author: '卡牌收藏家', avatarColor: '#00838F', content: '星海觉醒第一弹开包实况!15包连开出3张SSR+1张UR闪!附单卡评级和卡组搭配建议~', likes: 892, comments: 156, shares: 67, images: 5, timeAgo: '2小时前', topic: '#开包实况#' },
  { id: 2, author: '神域玩家', avatarColor: '#6A1B9A', content: '龙之试炼典藏包开箱测评:开出龙之帝王UR闪!品相评分9.8分,附详细概率分析', likes: 768, comments: 134, shares: 45, images: 6, timeAgo: '5小时前', topic: '#开箱测评#' },
  { id: 3, author: '机械大师', avatarColor: '#1565C0', content: '机械纪元新版卡组构筑分享:以机械战神为核心的t1卡组,胜率78%!附详细攻略', likes: 945, comments: 187, shares: 78, images: 4, timeAgo: '8小时前', topic: '#卡组攻略#' },
  { id: 4, author: '幻兽猎人', avatarColor: '#C2185B', content: '幻兽图鉴限定包开到白虎SSR闪卡了!四大神兽系列已收集齐3张,只差朱雀了~', likes: 1234, comments: 234, shares: 89, images: 4, timeAgo: '12小时前', topic: '#收集分享#' },
  { id: 5, author: '万灵使', avatarColor: '#FF6F00', content: '集换市场攻略:如何用低价收高价值卡?分享3个捡漏技巧和2个防骗指南', likes: 678, comments: 123, shares: 45, images: 3, timeAgo: '1天前', topic: '#集换攻略#' },
  { id: 6, author: '龙族守护', avatarColor: '#FFB300', content: '全国TCG公开赛备战分享:当前meta分析+主流卡组克制+参赛准备清单', likes: 856, comments: 167, shares: 56, images: 5, timeAgo: '2天前', topic: '#赛事备战#' }
]

const TOPICS_81: TopicItem81[] = [
  { id: 1, title: '星海觉醒开包', posts: 2156, hot: true, color: '#00838F' },
  { id: 2, title: 'SSR闪卡交易', posts: 1876, hot: true, color: '#FFB300' },
  { id: 3, title: '全国赛备战', posts: 1562, hot: true, color: '#6A1B9A' },
  { id: 4, title: '卡组构筑', posts: 854, hot: false, color: '#1565C0' },
  { id: 5, title: '卡牌评级', posts: 654, hot: false, color: '#C2185B' },
  { id: 6, title: '限定卡收集', posts: 432, hot: false, color: '#FF6F00' }
]

const ORDERS_81: OrderItem81[] = [
  { id: 1, product: '星海觉醒·第一弹', date: '08-22', amount: 105, status: '已发货', quantity: 3, color: '#FF9800' },
  { id: 2, product: '龙之试炼·典藏包', date: '08-18', amount: 88, status: '已签收', quantity: 1, color: '#4CAF50' },
  { id: 3, product: '星海之龙·SR闪', date: '08-22', amount: 28, status: '已完成', quantity: 1, color: '#4CAF50' },
  { id: 4, product: '幻兽·白虎·SSR', date: '08-18', amount: 128, status: '已完成', quantity: 1, color: '#4CAF50' },
  { id: 5, product: '机械纪元·精英包', date: '09-01', amount: 116, status: '待发货', quantity: 2, color: '#2196F3' },
  { id: 6, product: '全国赛报名费', date: '09-15', amount: 50, status: '已报名', quantity: 1, color: '#2196F3' }
]

const FAVORITES_81: FavoriteItem81[] = [
  { id: 1, name: '星海觉醒·第一弹', rarity: '卡包', color: '#00838F' },
  { id: 2, name: '龙之帝王·UR', rarity: 'UR', color: '#FFB300' },
  { id: 3, name: '幻兽·朱雀·SSR', rarity: 'SSR', color: '#C2185B' },
  { id: 4, name: '全国TCG公开赛', rarity: '赛事', color: '#00838F' },
  { id: 5, name: '机械战神·SSR', rarity: 'SSR', color: '#1565C0' }
]

const DECKS_81: DeckItem81[] = [
  { id: 1, name: '星海龙骑', format: '标准赛', cards: 40, winRate: 72, color: '#00838F', desc: '以星海之龙为核心的中速卡组' },
  { id: 2, name: '机械战神', format: '标准赛', cards: 40, winRate: 68, color: '#1565C0', desc: '以机械战神为核心的快攻卡组' },
  { id: 3, name: '幻兽白虎', format: '限定赛', cards: 40, winRate: 65, color: '#C2185B', desc: '幻兽图鉴限定中速卡组' }
]

const PACK_SELECTS_81: PackSelect81[] = [
  { id: 1, name: '星海觉醒·第一弹', price: 35, cards: 8, color: '#00838F' },
  { id: 2, name: '龙之试炼·典藏包', price: 88, cards: 15, color: '#FFB300' },
  { id: 3, name: '机械纪元·精英包', price: 58, cards: 10, color: '#1565C0' },
  { id: 4, name: '幻兽图鉴·限定包', price: 68, cards: 12, color: '#C2185B' },
  { id: 5, name: '神域降临·典藏包', price: 128, cards: 20, color: '#6A1B9A' }
]

// ============ 主入口组件 ============
@Entry
@Component
struct DuoDuoCardGameApp {
  @State currentTab: number = 0
  @State showBuyDialog: boolean = false
  @State showCancelDialog: boolean = false
  @State showEditDeckDialog: boolean = false
  @State showDeleteFavDialog: boolean = false
  @State selectedPackId: number = 1
  @State buyQuantity: number = 1
  @State openMethod: number = 0
  @State buyNotes: string = ''
  @State editDeckName: string = ''
  @State editDeckCore: string = ''
  @State editDeckStrategy: string = ''
  @State cancelTargetId: number = 0

  private tabs: string[] = ['卡包', '集换', '竞技', '图鉴', '社区', '我的']
  private openMethods: string[] = ['邮寄到家', '线上开包', '门店自提+开包']

  build() {
    Column() {
      // ====== 顶部头部 ======
      Column() {
        Row() {
          Column() {
            Text('多多卡牌')
              .fontSize(22)
              .fontColor('#FFFFFF')
              .fontWeight(FontWeight.Bold)
            Text('TCG·集换·竞技精选')
              .fontSize(11)
              .fontColor('rgba(255,255,255,0.8)')
              .margin({ top: 2 })
          }
          .alignItems(HorizontalAlign.Start)
          .layoutWeight(1)
          Row() {
            Text('🃏').fontSize(18).fontColor('#FFFFFF')
          }
          .width(36).height(36).justifyContent(FlexAlign.Center)
          .backgroundColor('rgba(255,255,255,0.2)')
          .borderRadius(18)
        }
        .width('100%').height(56).padding({ left: 16, right: 16 })
        .alignItems(VerticalAlign.Center)

        Row() {
          Text('搜卡包、卡牌、赛事...').fontSize(13).fontColor('rgba(255,255,255,0.6)').layoutWeight(1)
          Text('🔍').fontSize(16).fontColor('rgba(255,255,255,0.6)')
        }
        .width('100%').height(36).margin({ top: 4 })
        .padding({ left: 16, right: 16 })
        .backgroundColor('rgba(255,255,255,0.15)')
        .borderRadius(20)
        .alignItems(VerticalAlign.Center)
      }
      .width('100%')
      .linearGradient({ angle: 135, colors: [['#00838F', 0], ['#006064', 1]] })
      .padding({ top: 8, bottom: 12, left: 16, right: 16 })

      // ====== Tab内容区 ======
      Stack({ alignContent: Alignment.TopStart }) {
        Column() {
          if (this.currentTab === 0) {
            PackTab81({
              onBuy: (id: number) => {
                this.selectedPackId = id
                this.showBuyDialog = true
              }
            })
          }
          if (this.currentTab === 1) {
            TradeTab81({
              onTrade: (id: number) => {
                this.cancelTargetId = id
                this.showCancelDialog = true
              }
            })
          }
          if (this.currentTab === 2) {
            CompetitiveTab81()
          }
          if (this.currentTab === 3) {
            CollectionTab81()
          }
          if (this.currentTab === 4) {
            CommunityTab81()
          }
          if (this.currentTab === 5) {
            ProfileTab81({
              onEditDeck: () => {
                this.showEditDeckDialog = true
              }, onCancelOrder: (id: number) => {
                this.cancelTargetId = id
                this.showCancelDialog = true
              }, onDeleteFav: (id: number) => {
                this.cancelTargetId = id
                this.showDeleteFavDialog = true
              }
            })
          }
        }
        .width('100%').height('100%')
      }
      .layoutWeight(1)
      .width('100%')

      // ====== 底部Tab栏 ======
      Row() {
        ForEach(this.tabs, (tab: string, idx: number) => {
          Column() {
            Text(this.getTabIcon(idx))
              .fontSize(20)
              .fontColor(this.currentTab === idx ? '#00838F' : '#999999')
            Text(tab)
              .fontSize(10)
              .fontColor(this.currentTab === idx ? '#00838F' : '#999999')
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .height(56)
          .justifyContent(FlexAlign.Center)
          .onClick(() => {
            this.currentTab = idx
          })
        }, (tab: string) => tab)
      }
      .width('100%')
      .height(56)
      .backgroundColor('#FFFFFF')
      .border({ width: 1, color: '#E0E0E0' })
    }
    .width('100%').height('100%')
    .backgroundColor('#E0F2F1')
    .bindContentCover(this.showCancelDialog, this.buildCancelCover())
    .bindContentCover(this.showDeleteFavDialog, this.buildDeleteFavCover())
  }

  private getTabIcon(idx: number): string {
    const icons: string[] = ['📦', '🔄', '🏆', '📚', '💬', '👤']
    return idx < icons.length ? icons[idx] : '📋'
  }

  @Builder
  buildBuySheet() {
    Column() {
      Row() {
        Text('购买卡包').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333')
        Text('').layoutWeight(1)
        Text('✕').fontSize(20).fontColor('#999999').onClick(() => { this.showBuyDialog = false })
      }
      .width('100%').padding(16)

      Scroll() {
        Column() {
          Text('选择卡包').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333')
          Scroll() {
            Row() {
              ForEach(PACK_SELECTS_81, (pack: PackSelect81) => {
                Column() {
                  Text(pack.name).fontSize(11).fontColor(this.selectedPackId === pack.id ? '#FFFFFF' : '#666666').maxLines(1)
                  Text(pack.cards + '张/包').fontSize(9).fontColor(this.selectedPackId === pack.id ? 'rgba(255,255,255,0.8)' : '#AAAAAA').margin({ top: 2 })
                  Text('¥' + pack.price).fontSize(12).fontColor(this.selectedPackId === pack.id ? '#FFFFFF' : pack.color).fontWeight(FontWeight.Bold).margin({ top: 2 })
                }
                .padding(8).margin({ right: 8 })
                .borderRadius(10)
                .backgroundColor(this.selectedPackId === pack.id ? pack.color : '#F5F5F5')
                .onClick(() => { this.selectedPackId = pack.id })
              }, (pack: PackSelect81) => pack.id.toString())
            }
          }
          .scrollable(ScrollDirection.Horizontal)
          .width('100%').margin({ top: 8 })

          Text('开包方式').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ top: 16 })
          Column() {
            ForEach(this.openMethods, (method: string, idx: number) => {
              Row() {
                Radio({ value: idx.toString(), group: 'open' })
                  .checked(this.openMethod === idx)
                  .onChange((checked: boolean) => { if (checked) { this.openMethod = idx } })
                Text(method).fontSize(12).fontColor('#333333').margin({ left: 8 })
                Text('').layoutWeight(1)
                Text(this.openMethod === idx ? '✓' : '').fontSize(14).fontColor('#00838F')
              }
              .width('100%').padding(10).margin({ top: 4 })
              .borderRadius(8)
              .backgroundColor(this.openMethod === idx ? '#E0F2F1' : '#F5F5F5')
            }, (method: string, idx: number) => idx.toString())
          }
          .width('100%')

          Row() {
            Text('购买数量').fontSize(14).fontColor('#333333')
            Text('').layoutWeight(1)
            Row() {
              Button() { Text('-').fontSize(16).fontColor('#666666') }
              .width(32).height(32).backgroundColor('#F5F5F5').borderRadius(16)
              .onClick(() => { if (this.buyQuantity > 1) { this.buyQuantity-- } })
              Text(this.buyQuantity.toString()).fontSize(14).fontColor('#333333').width(40).textAlign(TextAlign.Center)
              Button() { Text('+').fontSize(16).fontColor('#666666') }
              .width(32).height(32).backgroundColor('#F5F5F5').borderRadius(16)
              .onClick(() => { if (this.buyQuantity < 50) { this.buyQuantity++ } })
            }
          }
          .width('100%').margin({ top: 16 })

          Text('备注').fontSize(14).fontColor('#333333').margin({ top: 16 })
          TextArea({ text: this.buyNotes, placeholder: '如有特殊需求请备注...' })
            .width('100%').height(60).margin({ top: 8 })
            .borderRadius(10).backgroundColor('#F5F5F5')
            .onChange((val: string) => { this.buyNotes = val })

          Row() {
            Text('合计').fontSize(13).fontColor('#666666')
            Text('').layoutWeight(1)
            Text('¥' + this.getTotalPrice()).fontSize(20).fontColor('#FFB300').fontWeight(FontWeight.Bold)
          }
          .width('100%').margin({ top: 20, bottom: 12 })
        }
        .width('100%').padding({ left: 16, right: 16, bottom: 16 })
      }
      .constraintSize({ maxHeight: '55%' })

      Row() {
        Button() {
          Text('确认购买').fontSize(16).fontColor('#FFFFFF').fontWeight(FontWeight.Medium)
        }
        .layoutWeight(1).height(48)
        .backgroundColor('#00838F')
        .borderRadius(24)
        .onClick(() => { this.showBuyDialog = false })
      }
      .width('100%').padding(16)
    }
    .width('100%')
  }

  @Builder
  buildCancelCover() {
    Column() {
      Column() {
        Text('取消交易').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333')
        Text('确认取消此交易?取消后已支付金额将原路退回').fontSize(13).fontColor('#999999').margin({ top: 12 }).textAlign(TextAlign.Center)
        Row() {
          Button() { Text('再想想').fontSize(14).fontColor('#666666') }
          .layoutWeight(1).height(44).backgroundColor('#F5F5F5').borderRadius(22).margin({ right: 12 })
          .onClick(() => { this.showCancelDialog = false })
          Button() { Text('确认取消').fontSize(14).fontColor('#FFFFFF') }
          .layoutWeight(1).height(44).backgroundColor('#FF5252').borderRadius(22)
          .onClick(() => { this.showCancelDialog = false })
        }
        .width('100%').margin({ top: 24 })
      }
      .width('80%').padding(24).backgroundColor('#FFFFFF').borderRadius(20)
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('rgba(0,0,0,0.5)')
  }

  @Builder
  buildEditDeckSheet() {
    Column() {
      Row() {
        Text('编辑卡组').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333')
        Text('').layoutWeight(1)
        Text('✕').fontSize(20).fontColor('#999999').onClick(() => { this.showEditDeckDialog = false })
      }
      .width('100%').padding(16)

      Scroll() {
        Column() {
          Text('卡组名称').fontSize(14).fontColor('#666666').margin({ top: 8 })
          TextInput({ text: this.editDeckName, placeholder: '请输入卡组名称' })
            .width('100%').height(44).margin({ top: 8 })
            .borderRadius(10).backgroundColor('#F5F5F5')
            .onChange((val: string) => { this.editDeckName = val })

          Text('核心卡牌').fontSize(14).fontColor('#666666').margin({ top: 12 })
          TextInput({ text: this.editDeckCore, placeholder: '如:星海之龙×3' })
            .width('100%').height(44).margin({ top: 8 })
            .borderRadius(10).backgroundColor('#F5F5F5')
            .onChange((val: string) => { this.editDeckCore = val })

          Text('战术备注').fontSize(14).fontColor('#666666').margin({ top: 12 })
          TextArea({ text: this.editDeckStrategy, placeholder: '请输入战术思路和注意事项...' })
            .width('100%').height(80).margin({ top: 8 })
            .borderRadius(10).backgroundColor('#F5F5F5')
            .onChange((val: string) => { this.editDeckStrategy = val })

          Text('已有卡组').fontSize(14).fontColor('#666666').margin({ top: 16 })
          Column() {
            ForEach(DECKS_81, (deck: DeckItem81) => {
              Column() {
                Row() {
                  Text(deck.name).fontSize(13).fontColor('#333333').fontWeight(FontWeight.Medium)
                  Text('').layoutWeight(1)
                  Text(deck.format).fontSize(9).fontColor(deck.color).backgroundColor(deck.color + '15').padding({ left: 4, right: 4, top: 1, bottom: 1 }).borderRadius(4)
                }
                .width('100%')
                Text(deck.desc).fontSize(11).fontColor('#AAAAAA').margin({ top: 4 })
                Row() {
                  Text(deck.cards + '张').fontSize(10).fontColor('#AAAAAA')
                  Text('胜率' + deck.winRate + '%').fontSize(10).fontColor(deck.color).margin({ left: 12 })
                  Text('').layoutWeight(1)
                  Text('编辑').fontSize(10).fontColor('#00838F')
                }
                .width('100%').margin({ top: 4 })
              }
              .width('100%').padding(10).margin({ top: 6 })
              .backgroundColor('#FFFFFF')
              .borderRadius(8)
              .border({ width: 1, color: '#F0F0F0' })
            }, (deck: DeckItem81) => deck.id.toString())
          }
          .width('100%')
        }
        .width('100%').padding({ left: 16, right: 16, bottom: 16 })
      }
      .constraintSize({ maxHeight: '50%' })

      Row() {
        Button() {
          Text('保存卡组').fontSize(16).fontColor('#FFFFFF').fontWeight(FontWeight.Medium)
        }
        .layoutWeight(1).height(48)
        .backgroundColor('#00838F')
        .borderRadius(24)
        .onClick(() => { this.showEditDeckDialog = false })
      }
      .width('100%').padding(16)
    }
    .width('100%')
  }

  @Builder
  buildDeleteFavCover() {
    Column() {
      Column() {
        Text('删除收藏').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#333333')
        Text('确认从收藏中移除?').fontSize(13).fontColor('#999999').margin({ top: 12 })
        Row() {
          Button() { Text('取消').fontSize(14).fontColor('#666666') }
          .layoutWeight(1).height(44).backgroundColor('#F5F5F5').borderRadius(22).margin({ right: 12 })
          .onClick(() => { this.showDeleteFavDialog = false })
          Button() { Text('删除').fontSize(14).fontColor('#FFFFFF') }
          .layoutWeight(1).height(44).backgroundColor('#FF5252').borderRadius(22)
          .onClick(() => { this.showDeleteFavDialog = false })
        }
        .width('100%').margin({ top: 24 })
      }
      .width('80%').padding(24).backgroundColor('#FFFFFF').borderRadius(20)
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('rgba(0,0,0,0.5)')
  }

  private getTotalPrice(): number {
    let pack: PackSelect81 | undefined = PACK_SELECTS_81.find((p: PackSelect81) => p.id === this.selectedPackId)
    let packPrice: number = pack ? pack.price : 0
    return packPrice * this.buyQuantity
  }
}

// ============ 卡包Tab ============
@Component
struct PackTab81 {
  onBuy: (id: number) => void = () => {}

  build() {
    Scroll() {
      Column() {
        // 分类标签
        Text('卡包分类').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 12 })
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(PACK_TAGS_81, (tag: PackTag81) => {
            Row() {
              Text(tag.label).fontSize(12).fontColor(tag.color)
              Text(tag.count + '款').fontSize(9).fontColor('#AAAAAA').margin({ left: 4 })
            }
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .margin({ right: 8, top: 8 })
            .backgroundColor(tag.color + '15')
            .borderRadius(16)
            .border({ width: 1, color: tag.color + '30' })
          }, (tag: PackTag81) => tag.label)
        }
        .width('100%').padding({ left: 16, right: 16 })

        // 新卡首发大卡
        Row() {
          Text('新卡首发').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333')
          Text('').layoutWeight(1)
          Text('更多 >').fontSize(12).fontColor('#00838F')
        }
        .width('100%').padding({ left: 16, right: 16, top: 16 })
        Column() {
          Row() {
            Column() {
              Text('星海觉醒·第一弹').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
              Text('8张/包 · SR以上保底').fontSize(11).fontColor('rgba(255,255,255,0.8)').margin({ top: 4 })
              Row() {
                Text('¥').fontSize(12).fontColor('#FFFFFF')
                Text('35').fontSize(22).fontColor('#FFFFFF').fontWeight(FontWeight.Bold)
                Text('¥48').fontSize(11).fontColor('rgba(255,255,255,0.6)').decoration({ type: TextDecorationType.LineThrough }).margin({ left: 4 })
                Text('').layoutWeight(1)
                Text('08-20首发').fontSize(10).fontColor('#FFFFFF').backgroundColor('rgba(255,255,255,0.25)').padding({ left: 8, right: 8, top: 2, bottom: 2 }).borderRadius(10)
              }
              .width('100%').margin({ top: 12 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('🃏').fontSize(48)
          }
          .width('100%').padding(16)
        }
        .width('100%').margin({ left: 16, right: 16, top: 8 })
        .linearGradient({ angle: 135, colors: [['#00838F', 0], ['#006064', 1]] })
        .borderRadius(16)

        // 双列卡包
        Text('全部卡包').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ left: 16, top: 16 })
        Grid() {
          ForEach(CARD_PACKS_81, (pack: CardPack81) => {
            GridItem() {
              Column() {
                Column() {
                  Text('🃏').fontSize(32)
                }
                .width('100%').height(64)
                .backgroundColor(pack.color + '20')
                .borderRadius({ topLeft: 12, topRight: 12 })
                .justifyContent(FlexAlign.Center)

                Column() {
                  Text(pack.name).fontSize(12).fontColor('#333333').fontWeight(FontWeight.Bold).maxLines(2)
                  Text(pack.series).fontSize(9).fontColor('#AAAAAA').margin({ top: 2 })
                  Text(pack.rarity).fontSize(9).fontColor(pack.color).backgroundColor(pack.color + '15').padding({ left: 4, right: 4, top: 1, bottom: 1 }).borderRadius(4).margin({ top: 4 })

                  Flex({ wrap: FlexWrap.Wrap }) {
                    ForEach(pack.tags, (tag: string) => {
                      Text(tag).fontSize(9).fontColor(pack.color).backgroundColor(pack.color + '15').padding({ left: 4, right: 4, top: 1, bottom: 1 }).borderRadius(4).margin({ right: 4, top: 4 })
                    }, (tag: string) => tag)
                  }
                  .margin({ top: 4 })

                  Row() {
                    Text(pack.cards + '张').fontSize(9).fontColor('#AAAAAA')
                    Text('库存' + pack.stock).fontSize(9).fontColor('#AAAAAA').margin({ left: 8 })
                  }
                  .margin({ top: 4 })

                  Row() {
                    Text('★').fontSize(10).fontColor('#FFB300')
                    Text(pack.rating.toString()).fontSize(10).fontColor('#FFB300').margin({ left: 2 })
                    Text(pack.sales + '人购买').fontSize(9).fontColor('#AAAAAA').margin({ left: 8 })
                  }
                  .margin({ top: 4 })

                  Row() {
                    Text('¥').fontSize(11).fontColor('#FFB300')
                    Text(pack.price.toString()).fontSize(15).fontColor('#FFB300').fontWeight(FontWeight.Bold)
                    Text('¥' + pack.originalPrice).fontSize(9).fontColor('#CCCCCC').decoration({ type: TextDecorationType.LineThrough }).margin({ left: 4 })
                    Text('').layoutWeight(1)
                  }
                  .margin({ top: 4 })

                  Button() {
                    Text('购买').fontSize(11).fontColor('#FFFFFF')
                  }
                  .width('100%').height(26).margin({ top: 6 })
                  .backgroundColor(pack.color)
                  .borderRadius(13)
                  .onClick(() => { this.onBuy(pack.id) })
                }
                .padding(8)
              }
              .backgroundColor('#FFFFFF')
              .borderRadius(12)
              .border({ width: 1, color: '#F0F0F0' })
            }
          }, (pack: CardPack81) => pack.id.toString())
        }
        .columnsTemplate('1fr 1fr')
        .rowsGap(12).columnsGap(12)
        .padding(16)
      }
      .width('100%')
    }
    .scrollable(ScrollDirection.Vertical)
    .width('100%').height('100%')
    .backgroundColor('#E0F2F1')
  }
}



总结

在这里插入图片描述

本文详细解析了一个基于HarmonyOS ArkTS的集换式卡牌游戏应用,该应用以"多多卡牌"为品牌名,覆盖了卡包购买、集换交易、竞技对战、图鉴收集、社区交流和个人管理六大功能模块。从十五个接口类型定义到十余组静态数据数组,从主入口组件的十四项状态管理到六个Tab子组件的差异化布局实现,从购买弹窗的多步骤交互到卡组编辑弹窗的表单处理,整个应用展现了一个完整的TCG生态系统的技术架构。

在架构设计上,应用延续了"主组件状态管理+子组件事件回调"的经典模式,但针对TCG业务特点做了多处优化。集换Tab的onTrade回调仅在卡牌状态为"出售中"时触发,避免了已预订卡牌的误操作。竞技Tab的赛事报名进度和对战记录完全由数据驱动,layoutWeight的比例分割技巧使得进度条无需额外计算像素值即可动态渲染。图鉴Tab通过owned布尔字段和opacity透明度控制,实现了已拥有和未拥有卡牌的视觉区分。

每个等级的收藏进度通过layoutWeight进度条可视化展示。积分趋势柱状图通过颜色深浅区分近期和远期数据,赛事状态通过嵌套三元运算符实现三色动态渲染。卡组管理模块支持核心卡牌和战术备注的编辑,为竞技对战提供策略支持。整体而言,该应用为HarmonyOS平台上的TCG类应用开发提供了丰富的技术参考和设计范例。

Logo

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

更多推荐