潮玩卡牌对战应用是集卡牌收集、系列图鉴、实时对战、交易市场、排名竞技与个人收藏于一体的综合性游戏化社交平台。在HarmonyOS声明式UI范式下,通过ArkTS的强类型系统和组件化设计模式,将复杂的卡牌数据模型——稀有度分级(SSR/UR/SR/R)、攻防属性、系列关联、对战记录——映射为类型安全的interface接口,配合静态数据常量实现数据与视图的彻底解耦。

应用采用钻石蓝(#0277BD)与璀璨金(#FFD700)的经典配色组合,通过底部6Tab单排导航实现卡牌商店、系列图鉴、对战竞技场、交易市场、排行榜和个人中心六大核心模块的快速切换。每个Tab页面拥有差异化的布局策略——网格列表、时间线卡片、竞技场战绩、市场交易列表、领奖台排行、收藏管理——共同构成丰富多元的视觉体验。

在弹框交互层面,应用实现了5个弹框组件:购买卡牌底部抽屉(含数量选择、补充包选项、卡牌保险)、出售卡牌居中弹窗(含市场参考价和自定义定价)、编辑卡组底部抽屉(含卡牌列表和卡槽选择)、删除卡牌确认弹窗、对战记录详情弹窗。这些弹框覆盖了卡牌游戏从购买到收藏到交易到对战的完整生命周期。

在这里插入图片描述

引言

卡牌对战类游戏是移动游戏市场中长盛不衰的品类,其核心吸引力在于"收集-构筑-对战-交易"的完整闭环。玩家通过购买卡包获取随机卡牌,根据稀有度和属性构筑个人卡组,在对战中与对手竞技获取排名积分,并在交易市场中买卖卡牌实现资产流通。本应用将这一完整闭环浓缩在单一的HarmonyOS应用中,通过ArkTS声明式UI范式实现了高度模块化的组件架构。

在技术架构上,应用采用"入口组件 + Tab内容组件 + 弹框组件"的三层分层设计。入口组件DuoDuoCardApp作为根组件,管理着9个@State状态变量,包括Tab索引、5个弹框的显隐控制、选中卡牌对象以及购买抽屉中的数量、补充包和保险选项等用户选择状态。卡牌数据通过CardItem109接口定义,包含了名称、系列、稀有度、价格、原价、销量、攻击力、防御力、生命值、表情符号和标签等11个字段,为UI渲染提供完整的数据支撑。

应用的工具函数层提供了getRarityColor109getRarityBg109getResultColor109getStatusColor109getRankColor109五个辅助函数,分别处理稀有度颜色映射、稀有度背景色映射、对战结果颜色映射、订单状态颜色映射和排名颜色映射。这些函数在多个组件中被反复调用,通过条件判断返回对应的十六进制颜色值,实现了视觉表现的统一管理和集中维护。

配色体系与数据模型定义

在这里插入图片描述

应用的配色体系以钻石蓝和璀璨金为核心,通过interface ColorPalette109定义了16个颜色字段。主色#0277BD是Material Design的Light Blue 800色值,传达科技感和竞技属性;金色#FFD700作为辅助色用于强调装饰和稀有度标识,两者搭配营造出"宝藏与竞技"的游戏化视觉氛围。

interface ColorPalette109 {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  bg: string;
  card: string;
  textMain: string;
  textSub: string;
  textHint: string;
  border: string;
  success: string;
  warning: string;
  danger: string;
  gold: string;
  white: string;
}

const COLORS109: ColorPalette109 = {
  primary: '#0277BD',
  primaryLight: '#4FC3F7',
  primaryDark: '#01579B',
  accent: '#FFD700',
  accentLight: '#FFE082',
  bg: '#E1F5FE',
  card: '#FFFFFF',
  textMain: '#01579B',
  textSub: '#0288D1',
  textHint: '#81D4FA',
  border: '#B3E5FC',
  success: '#66BB6A',
  warning: '#FFA726',
  danger: '#EF5350',
  gold: '#FFB300',
  white: '#FFFFFF'
};

颜色字段中特别值得注意的是gold字段,它不同于accent的纯金色#FFD700,而是采用了更深沉的#FFB300。这种区分设计使得金色在不同使用场景下有不同的视觉效果——accent用于明亮的装饰性金色(如标签、图标),gold用于需要与背景形成强对比的文本和数值(如积分、排名)。

数据接口层定义了卡牌游戏所需的核心数据模型。CardItem109接口是应用的核心实体,包含了卡牌的全部属性信息;SeriesMeta109定义了卡牌系列的元数据,包括系列名称、描述、总卡数、UR数量、隐藏卡数量和发布日期;BattleRecord109记录了对战的对手、结果、使用卡组、回合数和得分。

interface CardItem109 {
  id: string;
  name: string;
  series: string;
  rarity: string;
  price: number;
  oldPrice: number;
  sales: number;
  atk: number;
  def: number;
  hp: number;
  emoji: string;
  tag: string;
}

interface SeriesMeta109 {
  id: string;
  name: string;
  desc: string;
  total: number;
  ultra: number;
  secret: number;
  releaseDate: string;
  emoji: string;
  color1: string;
  color2: string;
}

interface BattleRecord109 {
  id: string;
  opponent: string;
  result: string;
  deck: string;
  turns: number;
  time: string;
  score: number;
}

interface RankItem109 {
  id: string;
  rank: number;
  name: string;
  score: number;
  winRate: number;
  battles: number;
  emoji: string;
}

SeriesMeta109接口中的color1color2字段为每个系列定义了专属的渐变色调,如龙族传说系列使用深红到金色渐变(#B71C1C#FFD700),魔法师系列使用深紫到浅紫渐变。这种按系列定制的配色方案使得每个系列卡片在视觉上具有独特的识别性,增强了收集体验的沉浸感。

工具函数与入口组件

在这里插入图片描述

工具函数层通过条件判断实现数据到颜色的映射,其中getRarityColor109根据稀有度字符串返回对应颜色——SSR返回深红色#D32F2F、UR返回橙色#FF6F00、SR返回深蓝色#1565C0、R返回灰色#78909CgetRarityBg109则返回对应的浅色背景,用于卡片背景区域的差异化渲染。

function getRarityColor109(rarity: string): string {
  if (rarity === 'SSR') return '#D32F2F';
  if (rarity === 'UR') return '#FF6F00';
  if (rarity === 'SR') return '#1565C0';
  return '#78909C';
}

function getRarityBg109(rarity: string): string {
  if (rarity === 'SSR') return '#FFEBEE';
  if (rarity === 'UR') return '#FFF3E0';
  if (rarity === 'SR') return '#E3F2FD';
  return '#ECEFF1';
}

function getResultColor109(result: string): string {
  if (result === '胜利') return '#43A047';
  return '#EF5350';
}

function getRankColor109(rank: number): string {
  if (rank <= 3) return '#FFD700';
  if (rank <= 6) return '#9E9E9E';
  return '#CD7F32';
}

入口组件DuoDuoCardApp管理着应用的全局状态。头部区域使用深蓝到浅蓝的90度线性渐变,标题"卡牌对决"使用金色字体,右侧搜索和背包图标同样使用金色,整体营造出游戏化的竞技氛围。Tab内容区通过if-else if条件分支实现6个Tab页面的条件渲染。

@Entry
@Component
struct DuoDuoCardApp {
  @State currentTab: number = 0;
  @State showBuySheet: boolean = false;
  @State showSellDialog: boolean = false;
  @State showDeckSheet: boolean = false;
  @State showDeleteDialog: boolean = false;
  @State showBattleDialog: boolean = false;
  @State selectedCard: CardItem109 | null = null;
  @State selQty: number = 1;
  @State selBooster: boolean = false;
  @State selInsurance: boolean = false;
  @State sellPrice: number = 0;
  @State selDeckSlot: number = 0;

  build() {
    Column() {
      Row() {
        Text('卡牌对决')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS109.gold)
        Text('').layoutWeight(1)
        Text('🔍')
          .fontSize(22)
          .fontColor(COLORS109.gold)
          .margin({ right: 12 })
        Text('🎒')
          .fontSize(22)
          .fontColor(COLORS109.gold)
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .linearGradient({ angle: 90, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })

      if (this.currentTab === 0) {
        CardShopTab109({
          onBuy: (c: CardItem109) => {
            this.selectedCard = c;
            this.showBuySheet = true;
          },
          onDetail: (c: CardItem109) => {
            this.selectedCard = c;
            this.showSellDialog = true;
          }
        })
      } else if (this.currentTab === 2) {
        BattleTab109({
          onRecord: () => { this.showBattleDialog = true; }
        })
      } else {
        MyCardTab109({
          onDeck: () => { this.showDeckSheet = true; },
          onDelete: () => { this.showDeleteDialog = true; }
        })
      }

      Row() {
        TabBtn109({icon:'🃏', label:'卡牌', active: this.currentTab === 0, onTap: () => { this.currentTab = 0; }})
        TabBtn109({icon:'📦', label:'系列', active: this.currentTab === 1, onTap: () => { this.currentTab = 1; }})
        TabBtn109({icon:'⚔️', label:'对战', active: this.currentTab === 2, onTap: () => { this.currentTab = 2; }})
        TabBtn109({icon:'🔄', label:'交易', active: this.currentTab === 3, onTap: () => { this.currentTab = 3; }})
        TabBtn109({icon:'🏆', label:'排行', active: this.currentTab === 4, onTap: () => { this.currentTab = 4; }})
        TabBtn109({icon:'👤', label:'我的', active: this.currentTab === 5, onTap: () => { this.currentTab = 5; }})
      }
      .width('100%')
      .height(56)
      .backgroundColor(COLORS109.card)
      .border({ width: 1, color: COLORS109.border })
      .justifyContent(FlexAlign.SpaceAround)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS109.bg)
  }
}

底部Tab导航组件TabBtn109的设计有一个独特之处——它在激活状态下会在顶部显示一条3vp高度的金色横条,通过if (this.active)条件渲染实现。这条横条为Tab导航增加了视觉层次感,使激活状态更加醒目。这种"顶部金边"设计灵感来自游戏UI中常见的标签选中标识。

卡牌商店与系列图鉴

在这里插入图片描述

卡牌商店Tab(CardShopTab109)是应用的主页面,集成了搜索框、横幅公告、金刚区分类入口、对战统计、热卖卡牌横滑和全部卡牌网格六个区域。搜索框采用圆角胶囊式设计,横幅公告使用深蓝背景展示新系列上架信息。金刚区通过4列网格展示8个卡牌分类入口(龙族、魔法师、战士、机械、天使、恶魔、兽族、限定),每个入口的背景色来自数据对象。

@Component
struct CardShopTab109 {
  onBuy: (c: CardItem109) => void = () => {};
  onDetail: (c: CardItem109) => void = () => {};
  build() {
    Scroll() {
      Column() {
        Row() {
          Text('🔍 搜索卡牌、系列...')
            .fontSize(13)
            .fontColor(COLORS109.textHint)
            .layoutWeight(1)
        }
        .width('92%')
        .height(36)
        .backgroundColor(COLORS109.card)
        .borderRadius(18)
        .padding({ left: 16, right: 16 })
        .margin({ top: 12, bottom: 8 })

        Row() {
          Text('🎴 新系列「野兽传说」上架 · 限时折扣')
            .fontSize(11)
            .fontColor(COLORS109.gold)
            .layoutWeight(1)
        }
        .width('100%')
        .height(28)
        .backgroundColor(COLORS109.primaryDark)
        .justifyContent(FlexAlign.Center)

        Grid() {
          ForEach(CAT_ENTRIES_109, (cat: CatEntry109) => {
            GridItem() {
              Column() {
                Column() {
                  Text(cat.emoji)
                    .fontSize(24)
                }
                .width(44)
                .height(44)
                .backgroundColor(cat.color)
                .borderRadius(22)
                .justifyContent(FlexAlign.Center)
                Text(cat.name)
                  .fontSize(9)
                  .fontColor(COLORS109.textSub)
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Center)
            }
          }, (cat: CatEntry109) => cat.id)
        }
        .columnsTemplate('1fr 1fr 1fr 1fr')
        .rowsGap(8)
        .columnsGap(8)
        .width('92%')
        .margin({ top: 12 })
        .backgroundColor(COLORS109.card)
        .borderRadius(12)
        .padding(12)

        // 热卖卡牌横滑区域
        Scroll() {
          Row() {
            ForEach(CARDS_109.slice(0, 6), (c: CardItem109) => {
              Column() {
                Stack({ alignContent: Alignment.TopEnd }) {
                  Column() {
                    Text(c.emoji)
                      .fontSize(36)
                  }
                  .width(72)
                  .height(72)
                  .backgroundColor(getRarityBg109(c.rarity))
                  .borderRadius(12)
                  .justifyContent(FlexAlign.Center)
                  Text(c.rarity)
                    .fontSize(8)
                    .fontColor(COLORS109.white)
                    .backgroundColor(getRarityColor109(c.rarity))
                    .borderRadius(3)
                    .padding({ left: 3, right: 3, top: 1, bottom: 1 })
                    .margin({ top: 3, right: 3 })
                }
                Text(c.name)
                  .fontSize(10)
                  .fontColor(COLORS109.textMain)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .width(72)
                  .margin({ top: 4 })
                Text('ATK ' + c.atk + ' / DEF ' + c.def)
                  .fontSize(8)
                  .fontColor(COLORS109.textSub)
                Row() {
                  Text('¥' + c.price)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS109.primary)
                  Text('¥' + c.oldPrice)
                    .fontSize(9)
                    .fontColor(COLORS109.textHint)
                    .decoration({ type: TextDecorationType.LineThrough })
                    .margin({ left: 4 })
                }
                .margin({ top: 2 })
              }
              .width(88)
              .margin({ right: 8 })
              .onClick(() => { this.onDetail(c); })
            }, (c: CardItem109) => c.id)
          }
          .padding({ left: 16, right: 16 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')
        .margin({ bottom: 12 })

        // 全部卡牌网格
        Grid() {
          ForEach(CARDS_109, (c: CardItem109) => {
            GridItem() {
              Column() {
                Stack({ alignContent: Alignment.TopEnd }) {
                  Column() {
                    Text(c.emoji)
                      .fontSize(40)
                  }
                  .width('100%')
                  .height(80)
                  .backgroundColor(getRarityBg109(c.rarity))
                  .borderRadius(12)
                  .justifyContent(FlexAlign.Center)
                  Text(c.rarity)
                    .fontSize(8)
                    .fontColor(COLORS109.white)
                    .backgroundColor(getRarityColor109(c.rarity))
                    .borderRadius(3)
                    .padding({ left: 3, right: 3, top: 1, bottom: 1 })
                    .margin({ top: 3, right: 3 })
                }
                Text(c.name)
                  .fontSize(11)
                  .fontColor(COLORS109.textMain)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .margin({ top: 6 })
                Row() {
                  Text('ATK')
                    .fontSize(8)
                    .fontColor(COLORS109.textHint)
                  Text(c.atk.toString())
                    .fontSize(9)
                    .fontColor(COLORS109.danger)
                    .margin({ left: 2 })
                  Text('DEF')
                    .fontSize(8)
                    .fontColor(COLORS109.textHint)
                    .margin({ left: 4 })
                  Text(c.def.toString())
                    .fontSize(9)
                    .fontColor(COLORS109.primary)
                    .margin({ left: 2 })
                }
                .margin({ top: 2 })
                Row() {
                  Text('¥' + c.price)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS109.primary)
                  Text('¥' + c.oldPrice)
                    .fontSize(9)
                    .fontColor(COLORS109.textHint)
                    .decoration({ type: TextDecorationType.LineThrough })
                    .margin({ left: 4 })
                  Text('').layoutWeight(1)
                  Text('销' + c.sales)
                    .fontSize(8)
                    .fontColor(COLORS109.textHint)
                }
                .width('100%')
                .margin({ top: 4 })
                Row() {
                  Text('购买')
                    .fontSize(11)
                    .fontColor(COLORS109.white)
                    .backgroundColor(COLORS109.primary)
                    .borderRadius(6)
                    .padding({ left: 16, right: 16, top: 6, bottom: 6 })
                    .layoutWeight(1)
                    .textAlign(TextAlign.Center)
                }
                .width('100%')
                .margin({ top: 6 })
                .onClick(() => { this.onBuy(c); })
              }
              .padding(10)
              .backgroundColor(COLORS109.card)
              .borderRadius(12)
            }
          }, (c: CardItem109) => c.id)
        }
        .columnsTemplate('1fr 1fr')
        .rowsGap(8)
        .columnsGap(8)
        .width('92%')
        .margin({ bottom: 16 })
      }
      .width('100%')
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .constraintSize({ maxHeight: '80%' })
  }
}

卡牌卡片中Stack({ alignContent: Alignment.TopEnd })的使用是一个关键技巧——它将稀有度标签精确放置在卡牌图片区域的右上角。Stack堆叠容器配合TopEnd对齐方式实现了"角标"效果,这是移动端UI中常见的角标实现方案。通过getRarityColor109getRarityBg109两个函数,同一张卡牌的稀有度标签和背景区域获得了协调的颜色映射。

系列图鉴Tab(SeriesTab109)采用时间线卡片式布局展示6个卡牌系列。每个系列卡片使用color1color2字段定义的135度渐变作为头像背景,并展示系列名称、描述、总卡数、UR数量、隐藏卡数量和发布日期。卡片底部提供"购买整盒"和"查看图鉴"两个操作按钮,分别触发购买流程和图鉴浏览。

对战竞技场与排行榜

在这里插入图片描述

对战Tab(BattleTab109)是应用的竞技核心,整合了战绩统计、本周对战柱状图和对战记录列表三个区域。战绩统计通过4列等分布局展示胜率、胜场、败场和积分,每个数据项使用不同的颜色标识——胜率用金色、胜场用绿色、败场用红色、积分用蓝色,视觉上直观传达竞技状态。

@Component
struct BattleTab109 {
  onRecord: () => void = () => {};
  build() {
    Scroll() {
      Column() {
        Column() {
          Text('⚔️ 对战竞技场')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.gold)
          Text('实时对战·排名攀升')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.8)')
            .margin({ top: 4 })
        }
        .width('100%')
        .padding({ top: 24, bottom: 24 })
        .linearGradient({ angle: 135, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })

        Row() {
          ForEach(STATS_109, (s: StatCard109) => {
            Column() {
              Text(s.emoji)
                .fontSize(20)
              Text(s.value)
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .fontColor(s.color)
                .margin({ top: 2 })
              Text(s.label)
                .fontSize(9)
                .fontColor(COLORS109.textSub)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }, (s: StatCard109) => s.id)
        }
        .width('92%')
        .padding({ top: 12, bottom: 12 })
        .backgroundColor(COLORS109.card)
        .borderRadius(12)
        .margin({ top: 12 })

        // 本周对战柱状图
        Row() {
          ForEach(WEEK_BARS_109, (bar: BarItem109) => {
            Column() {
              Text(bar.value.toString())
                .fontSize(9)
                .fontColor(bar.color)
              Column() {
                Text('')
                  .width('100%')
                  .height(bar.value * 10)
                  .backgroundColor(bar.color)
                  .borderRadius(3)
              }
              .width(24)
              .height(80)
              .justifyContent(FlexAlign.End)
              .margin({ top: 4, bottom: 4 })
              Text(bar.name)
                .fontSize(9)
                .fontColor(COLORS109.textSub)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }, (bar: BarItem109) => bar.id)
        }
        .width('92%')
        .padding(16)
        .backgroundColor(COLORS109.card)
        .borderRadius(12)
        .margin({ bottom: 12 })

        // 对战记录
        ForEach(BATTLES_109, (b: BattleRecord109) => {
          Row() {
            Column() {
              Text(b.result === '胜利' ? '🏆' : '💀')
                .fontSize(20)
            }
            .width(40)
            .height(40)
            .backgroundColor(b.result === '胜利' ? '#E8F5E9' : '#FFEBEE')
            .borderRadius(20)
            .justifyContent(FlexAlign.Center)
            Column() {
              Text('vs ' + b.opponent)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS109.textMain)
              Row() {
                Text(b.result)
                  .fontSize(9)
                  .fontColor(getResultColor109(b.result))
                Text(b.deck)
                  .fontSize(9)
                  .fontColor(COLORS109.textSub)
                  .margin({ left: 6 })
                Text(b.turns + '回合')
                  .fontSize(9)
                  .fontColor(COLORS109.textHint)
                  .margin({ left: 6 })
              }
              .margin({ top: 2 })
              Text(b.time)
                .fontSize(9)
                .fontColor(COLORS109.textHint)
                .margin({ top: 2 })
            }
            .margin({ left: 8 })
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            Text('+' + b.score)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(b.score > 0 ? COLORS109.gold : COLORS109.textHint)
          }
          .width('92%')
          .padding(10)
          .backgroundColor(COLORS109.card)
          .borderRadius(10)
          .margin({ left: 12, right: 12, bottom: 6 })
        }, (b: BattleRecord109) => b.id)
      }
      .width('100%')
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .constraintSize({ maxHeight: '80%' })
  }
}

对战记录列表中每条记录的左侧头像区域根据对战结果动态渲染不同颜色——胜利时使用浅绿色背景#E8F5E9配合奖杯emoji,失败时使用浅红色背景#FFEBEE配合骷髅emoji。右侧的得分+b.score在胜利时显示金色,失败时(score为0)显示浅灰色,通过b.score > 0条件判断实现颜色切换。

排行榜Tab(RankTab109)的特色设计是前三名领奖台布局——第二名(银牌)居左、第一名(金牌)居中且尺寸更大、第三名(铜牌)居右。通过margin({ top: 12 })margin({ top: 20 })的差异化设置,使第一名卡片位置最高,第二名次之,第三名最低,营造出奥运会领奖台的视觉效果。4-10名则采用常规列表布局,每行展示排名序号、用户名、胜率和积分数。

// 前三领奖台
Row() {
  Column() {
    Text('🥈')
      .fontSize(28)
    Column() {
      Text(RANKS_109[1].name)
        .fontSize(11)
        .fontColor(COLORS109.textMain)
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Text(RANKS_109[1].score.toString())
        .fontSize(14)
        .fontWeight(FontWeight.Bold)
        .fontColor('#9E9E9E')
    }
    .width(72)
    .height(80)
    .backgroundColor('#F5F5F5')
    .borderRadius(12)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .padding(8)
  }
  .layoutWeight(1)
  .alignItems(HorizontalAlign.Center)
  .margin({ top: 12 })

  Column() {
    Text('🥇')
      .fontSize(36)
    Column() {
      Text(RANKS_109[0].name)
        .fontSize(12)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS109.textMain)
        .maxLines(1)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
      Text(RANKS_109[0].score.toString())
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS109.gold)
    }
    .width(80)
    .height(100)
    .backgroundColor('#FFF8E1')
    .borderRadius(12)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .padding(8)
    .border({ width: 2, color: COLORS109.gold })
  }
  .layoutWeight(1)
  .alignItems(HorizontalAlign.Center)
}
.width('92%')
.margin({ top: 12, bottom: 16 })

应用架构流程图

在这里插入图片描述

onBuy

onDetail

onRecord

onDeck

onDelete

入口组件 DuoDuoCardApp

全局状态管理

currentTab索引

5个弹框显隐

selectedCard选中卡牌

购买选项状态

Tab0: CardShopTab109 卡牌商店

Tab1: SeriesTab109 系列图鉴

Tab2: BattleTab109 对战竞技场

Tab3: TradeTab109 交易市场

Tab4: RankTab109 排行榜

Tab5: MyCardTab109 个人中心

弹框1: BuyCardSheet109 购买卡牌

弹框2: SellCardDialog109 出售卡牌

弹框3: EditDeckSheet109 编辑卡组

弹框4: DeleteCardDialog109 删除确认

弹框5: BattleRecordDialog109 对战记录

工具函数层

稀有度颜色映射

购买与出售弹框组件

在这里插入图片描述

购买卡牌弹框BuyCardSheet109是底部抽屉式组件,展示了选中卡牌的完整信息——emoji头像、名称、系列、稀有度、攻击力和防御力。弹框提供数量增减控制(通过selQty状态和+/按钮实现),并包含"购买补充包"(随机5张含UR概率UP)和"卡牌保险"(运输损坏包赔)两个附加选项的勾选开关。底部实时计算合计金额¥ + (card.price * selQty)并展示确认购买按钮。

@Component
struct BuyCardSheet109 {
  card: CardItem109 | null = null;
  selQty: number = 1;
  selBooster: boolean = false;
  selInsurance: boolean = false;
  onQty: (q: number) => void = () => {};
  onBooster: (b: boolean) => void = () => {};
  onInsurance: (i: boolean) => void = () => {};
  onConfirm: () => void = () => {};
  onCancel: () => void = () => {};
  build() {
    Scroll() {
      Column() {
        Row() {
          Column() {
            Text(this.card?.emoji ?? '🃏')
              .fontSize(36)
          }
          .width(60)
          .height(60)
          .backgroundColor(getRarityBg109(this.card?.rarity ?? 'R'))
          .borderRadius(12)
          .justifyContent(FlexAlign.Center)
          Column() {
            Text(this.card?.name ?? '')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.textMain)
            Text(this.card?.series ?? '')
              .fontSize(10)
              .fontColor(COLORS109.textSub)
              .margin({ top: 2 })
            Row() {
              Text(this.card?.rarity ?? '')
                .fontSize(9)
                .fontColor(getRarityColor109(this.card?.rarity ?? 'R'))
              Text('ATK ' + (this.card?.atk ?? 0))
                .fontSize(9)
                .fontColor(COLORS109.danger)
                .margin({ left: 8 })
              Text('DEF ' + (this.card?.def ?? 0))
                .fontSize(9)
                .fontColor(COLORS109.primary)
                .margin({ left: 8 })
            }
            .margin({ top: 4 })
          }
          .margin({ left: 12 })
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Text('✕')
            .fontSize(18)
            .fontColor(COLORS109.textHint)
            .onClick(() => { this.onCancel(); })
        }
        .width('100%')
        .padding(16)

        Divider().color(COLORS109.border)

        Row() {
          Text('¥' + (this.card?.price ?? 0))
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.primary)
          Text('¥' + (this.card?.oldPrice ?? 0))
            .fontSize(12)
            .fontColor(COLORS109.textHint)
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 6 })
          Text('').layoutWeight(1)
          Text('销' + (this.card?.sales ?? 0))
            .fontSize(10)
            .fontColor(COLORS109.textHint)
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 12 })

        Row() {
          Text('数量')
            .fontSize(13)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
          Text('−')
            .fontSize(16)
            .fontColor(COLORS109.primary)
            .padding({ left: 12, right: 12 })
            .onClick(() => { if (this.selQty > 1) { this.onQty(this.selQty - 1); } })
          Text(this.selQty.toString())
            .fontSize(14)
            .fontColor(COLORS109.textMain)
            .padding({ left: 12, right: 12 })
          Text('+')
            .fontSize(16)
            .fontColor(COLORS109.primary)
            .padding({ left: 12, right: 12 })
            .onClick(() => { this.onQty(this.selQty + 1); })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 16 })

        Row() {
          Column() {
            Text('📦 购买补充包')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.textMain)
            Text('随机5张·含UR概率UP')
              .fontSize(10)
              .fontColor(COLORS109.textSub)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Text(this.selBooster ? '☑' : '☐')
            .fontSize(20)
            .fontColor(this.selBooster ? COLORS109.primary : COLORS109.textHint)
            .onClick(() => { this.onBooster(!this.selBooster); })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 16 })

        Row() {
          Text('合计')
            .fontSize(13)
            .fontColor(COLORS109.textSub)
          Text('¥' + ((this.card?.price ?? 0) * this.selQty))
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.primary)
            .margin({ left: 8 })
            .layoutWeight(1)
          Text('确认购买')
            .fontSize(14)
            .fontColor(COLORS109.white)
            .backgroundColor(COLORS109.primary)
            .borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .onClick(() => { this.onConfirm(); })
        }
        .width('100%')
        .padding({ left: 16, right: 16, bottom: 20 })
      }
      .width('100%')
      .backgroundColor(COLORS109.white)
    }
    .scrollBar(BarState.Off)
  }
}

购买弹框中"数量减"按钮的onClick回调包含条件判断if (this.selQty > 1),防止数量减到0以下。而"数量加"按钮则无上限限制,允许用户自由增加购买数量。合计金额的计算公式¥ + ((card.price ?? 0) * selQty)利用了空值合并运算符??确保在卡牌数据为null时显示0,避免了运行时空指针异常。

出售卡牌弹框SellCardDialog109采用居中弹窗式设计,展示卡牌信息和市场参考价,提供价格输入框和上架出售按钮。用户通过TextInput输入自定义出售价格,onChange回调将输入值通过parseInt转换后传递给父组件的sellPrice状态。这种设计允许用户根据市场行情自主定价,体现了卡牌交易市场的灵活性。

技术点对比分析

技术维度 实现方案 设计优势 适用场景
稀有度渲染 双函数颜色映射 颜色与背景分离管理,复用性强 多稀有度等级的卡牌/装备系统
领奖台布局 差异化高度+尺寸 视觉冲击力强,竞技感突出 排行榜、赛事排名等场景
柱状图 FlexAlign.End对齐 无需图表库,轻量高效 简单数据趋势可视化
卡片角标 Stack + TopEnd 精确定位角标位置,布局稳定 商品标签、状态标识
渐变头像 linearGradient + 数据色值 每个系列视觉独立,辨识度高 多系列/多分类的卡片展示
条件渲染Tab if-else if分支 仅渲染当前页,内存效率高 中等数量Tab页面
数量控制 +/−按钮 + 条件保护 防止越界操作,交互安全 购物车、数量选择场景
删除线效果 TextDecorationType.LineThrough 原生支持,无需自定义绘制 价格对比、促销展示

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 6 tabs: 卡牌/系列/对战/交易/排行/我的 · 5弹框: 购买(抽屉)/出售(居中)/编辑卡组(抽屉)/删除卡牌(居中)/对战记录(居中)
// 合规:无Blank、Button无文字、constraintSize、SLIDE、interface全覆盖、无UI变量声明

// ============ 配色 ============
interface ColorPalette109 {
  primary: string;
  primaryLight: string;
  primaryDark: string;
  accent: string;
  accentLight: string;
  bg: string;
  card: string;
  textMain: string;
  textSub: string;
  textHint: string;
  border: string;
  success: string;
  warning: string;
  danger: string;
  gold: string;
  white: string;
}

const COLORS109: ColorPalette109 = {
  primary: '#0277BD',
  primaryLight: '#4FC3F7',
  primaryDark: '#01579B',
  accent: '#FFD700',
  accentLight: '#FFE082',
  bg: '#E1F5FE',
  card: '#FFFFFF',
  textMain: '#01579B',
  textSub: '#0288D1',
  textHint: '#81D4FA',
  border: '#B3E5FC',
  success: '#66BB6A',
  warning: '#FFA726',
  danger: '#EF5350',
  gold: '#FFB300',
  white: '#FFFFFF'
};

// ============ 数据接口 ============
interface CardItem109 {
  id: string;
  name: string;
  series: string;
  rarity: string;
  price: number;
  oldPrice: number;
  sales: number;
  atk: number;
  def: number;
  hp: number;
  emoji: string;
  tag: string;
}

interface SeriesMeta109 {
  id: string;
  name: string;
  desc: string;
  total: number;
  ultra: number;
  secret: number;
  releaseDate: string;
  emoji: string;
  color1: string;
  color2: string;
}

interface BattleRecord109 {
  id: string;
  opponent: string;
  result: string;
  deck: string;
  turns: number;
  time: string;
  score: number;
}

interface TradeItem109 {
  id: string;
  name: string;
  rarity: string;
  price: number;
  seller: string;
  city: string;
  emoji: string;
  type: string;
}

interface RankItem109 {
  id: string;
  rank: number;
  name: string;
  score: number;
  winRate: number;
  battles: number;
  emoji: string;
}

interface TopicMeta109 {
  id: string;
  name: string;
  count: number;
  color: string;
}

interface CommunityPost109 {
  id: string;
  user: string;
  time: string;
  title: string;
  content: string;
  likes: number;
  tag: string;
  tagColor: string;
}

interface DeckCard109 {
  id: string;
  name: string;
  type: string;
  count: number;
  emoji: string;
  color: string;
}

interface OrderItem109 {
  id: string;
  name: string;
  price: number;
  status: string;
  time: string;
}

interface FavCard109 {
  id: string;
  name: string;
  rarity: string;
  price: number;
  emoji: string;
}

interface BarItem109 {
  id: string;
  name: string;
  value: number;
  color: string;
}

interface StatCard109 {
  id: string;
  label: string;
  value: string;
  emoji: string;
  color: string;
}

interface CatEntry109 {
  id: string;
  name: string;
  emoji: string;
  color: string;
}

// ============ 数据 ============
const CAT_ENTRIES_109: CatEntry109[] = [
  { id: 'c1', name: '龙族', emoji: '🐉', color: '#D32F2F' },
  { id: 'c2', name: '魔法师', emoji: '🧙', color: '#7B1FA2' },
  { id: 'c3', name: '战士', emoji: '⚔️', color: '#0277BD' },
  { id: 'c4', name: '机械', emoji: '🤖', color: '#37474F' },
  { id: 'c5', name: '天使', emoji: '😇', color: '#FFD700' },
  { id: 'c6', name: '恶魔', emoji: '😈', color: '#4A148C' },
  { id: 'c7', name: '兽族', emoji: '🐺', color: '#2E7D32' },
  { id: 'c8', name: '限定', emoji: '💎', color: '#E91E63' }
];

const CARDS_109: CardItem109[] = [
  { id: 'cd1', name: '青眼白龙·极龙', series: '龙族传说', rarity: 'UR', price: 128, oldPrice: 168, sales: 2345, atk: 3000, def: 2500, hp: 3000, emoji: '🐉', tag: '传说' },
  { id: 'cd2', name: '黑魔术师·少女', series: '魔法师的试炼', rarity: 'SR', price: 68, oldPrice: 88, sales: 4567, atk: 2000, def: 1700, hp: 2000, emoji: '🧙', tag: '热卖' },
  { id: 'cd3', name: '暗黑骑士·统帅', series: '钢铁军团', rarity: 'SR', price: 55, oldPrice: 78, sales: 3456, atk: 2300, def: 1900, hp: 2100, emoji: '⚔️', tag: '新品' },
  { id: 'cd4', name: '机甲神·零式', series: '机械纪元', rarity: 'UR', price: 158, oldPrice: 198, sales: 1234, atk: 2800, def: 2400, hp: 2700, emoji: '🤖', tag: '限定' },
  { id: 'cd5', name: '光明使者·大天使', series: '圣光降临', rarity: 'SSR', price: 298, oldPrice: 388, sales: 678, atk: 3200, def: 2800, hp: 3500, emoji: '😇', tag: '隐藏' },
  { id: 'cd6', name: '深渊魔王·路西法', series: '暗黑降临', rarity: 'SSR', price: 328, oldPrice: 428, sales: 567, atk: 3500, def: 2600, hp: 3800, emoji: '😈', tag: '隐藏' },
  { id: 'cd7', name: '银狼·月之守护', series: '野兽传说', rarity: 'SR', price: 48, oldPrice: 68, sales: 5678, atk: 2100, def: 1800, hp: 2000, emoji: '🐺', tag: '热卖' },
  { id: 'cd8', name: '红莲龙·爆裂', series: '龙族传说', rarity: 'UR', price: 98, oldPrice: 128, sales: 2345, atk: 2800, def: 2300, hp: 2900, emoji: '🐲', tag: '传说' },
  { id: 'cd9', name: '时之魔法师', series: '魔法师的试炼', rarity: 'R', price: 18, oldPrice: 28, sales: 8901, atk: 1200, def: 1000, hp: 1500, emoji: '⏰', tag: '普通' },
  { id: 'cd10', name: '钢铁巨兵·改', series: '钢铁军团', rarity: 'R', price: 22, oldPrice: 32, sales: 6789, atk: 1500, def: 1800, hp: 2000, emoji: '🛡️', tag: '普通' },
  { id: 'cd11', name: '圣剑·誓约胜利', series: '圣光降临', rarity: 'UR', price: 118, oldPrice: 158, sales: 1567, atk: 2600, def: 2000, hp: 2500, emoji: '🗡️', tag: '限定' },
  { id: 'cd12', name: '暗影刺客·疾风', series: '暗黑降临', rarity: 'SR', price: 58, oldPrice: 78, sales: 3456, atk: 2200, def: 1500, hp: 1900, emoji: '🥷', tag: '新品' }
];

const SERIES_109: SeriesMeta109[] = [
  { id: 's1', name: '龙族传说·第一弹', desc: '20种+3隐藏·传说龙族登场', total: 23, ultra: 3, secret: 1, releaseDate: '2026-03', emoji: '🐉', color1: '#B71C1C', color2: '#FFD700' },
  { id: 's2', name: '魔法师的试炼', desc: '18种+2隐藏·魔法世界', total: 20, ultra: 2, secret: 1, releaseDate: '2026-04', emoji: '🧙', color1: '#4A148C', color2: '#E1BEE7' },
  { id: 's3', name: '钢铁军团', desc: '15种+3隐藏·机械时代', total: 18, ultra: 3, secret: 0, releaseDate: '2026-05', emoji: '🤖', color1: '#37474F', color2: '#90A4AE' },
  { id: 's4', name: '圣光降临', desc: '16种+2隐藏·天使降临', total: 18, ultra: 2, secret: 1, releaseDate: '2026-06', emoji: '😇', color1: '#FF6F00', color2: '#FFE082' },
  { id: 's5', name: '暗黑降临', desc: '14种+3隐藏·深渊觉醒', total: 17, ultra: 3, secret: 1, releaseDate: '2026-07', emoji: '😈', color1: '#311B92', color2: '#7E57C2' },
  { id: 's6', name: '野兽传说', desc: '20种+2隐藏·野性觉醒', total: 22, ultra: 2, secret: 0, releaseDate: '2026-08', emoji: '🐺', color1: '#1B5E20', color2: '#A5D6A7' }
];

const BATTLES_109: BattleRecord109[] = [
  { id: 'b1', opponent: '决斗者小明', result: '胜利', deck: '龙族传说', turns: 8, time: '10分钟前', score: 120 },
  { id: 'b2', opponent: '魔法师小红', result: '失败', deck: '魔法试炼', turns: 12, time: '1小时前', score: 0 },
  { id: 'b3', opponent: '钢铁之心', result: '胜利', deck: '钢铁军团', turns: 6, time: '2小时前', score: 150 },
  { id: 'b4', opponent: '暗影刺客', result: '胜利', deck: '暗黑降临', turns: 10, time: '3小时前', score: 100 },
  { id: 'b5', opponent: '圣光守护者', result: '失败', deck: '圣光降临', turns: 15, time: '5小时前', score: 0 },
  { id: 'b6', opponent: '野兽之王', result: '胜利', deck: '野兽传说', turns: 7, time: '1天前', score: 130 },
  { id: 'b7', opponent: '龙骑士', result: '胜利', deck: '龙族传说', turns: 9, time: '2天前', score: 110 },
  { id: 'b8', opponent: '时间操控者', result: '失败', deck: '魔法试炼', turns: 14, time: '3天前', score: 0 }
];

const TRADES_109: TradeItem109[] = [
  { id: 't1', name: '青眼白龙·极龙', rarity: 'UR', price: 128, seller: '龙族收藏家', city: '上海', emoji: '🐉', type: '出售' },
  { id: 't2', name: '光明使者·大天使', rarity: 'SSR', price: 298, seller: '圣光骑士', city: '北京', emoji: '😇', type: '出售' },
  { id: 't3', name: '深渊魔王·路西法', rarity: 'SSR', price: 328, seller: '暗黑领主', city: '广州', emoji: '😈', type: '出售' },
  { id: 't4', name: '机甲神·零式', rarity: 'UR', price: 158, seller: '机械师', city: '深圳', emoji: '🤖', type: '求购' },
  { id: 't5', name: '红莲龙·爆裂', rarity: 'UR', price: 98, seller: '火焰操控者', city: '成都', emoji: '🐲', type: '出售' },
  { id: 't6', name: '圣剑·誓约胜利', rarity: 'UR', price: 118, seller: '剑士王', city: '杭州', emoji: '🗡️', type: '求购' },
  { id: 't7', name: '暗影刺客·疾风', rarity: 'SR', price: 58, seller: '影之刃', city: '南京', emoji: '🥷', type: '出售' },
  { id: 't8', name: '银狼·月之守护', rarity: 'SR', price: 48, seller: '月之猎人', city: '武汉', emoji: '🐺', type: '出售' }
];

const RANKS_109: RankItem109[] = [
  { id: 'r1', rank: 1, name: '决斗王·龙骑士', score: 9876, winRate: 89, battles: 156, emoji: '👑' },
  { id: 'r2', rank: 2, name: '魔法大师·红', score: 8765, winRate: 85, battles: 142, emoji: '🥈' },
  { id: 'r3', rank: 3, name: '钢铁之心', score: 7654, winRate: 82, battles: 134, emoji: '🥉' },
  { id: 'r4', rank: 4, name: '暗影刺客', score: 6543, winRate: 78, battles: 128, emoji: '🎖️' },
  { id: 'r5', rank: 5, name: '圣光守护者', score: 5432, winRate: 75, battles: 120, emoji: '🎖️' },
  { id: 'r6', rank: 6, name: '野兽之王', score: 4321, winRate: 71, battles: 112, emoji: '🎖️' },
  { id: 'r7', rank: 7, name: '时间操控者', score: 3210, winRate: 68, battles: 105, emoji: '🎖️' },
  { id: 'r8', rank: 8, name: '深渊领主', score: 2109, winRate: 65, battles: 98, emoji: '🎖️' },
  { id: 'r9', rank: 9, name: '龙族收藏家', score: 1098, winRate: 62, battles: 90, emoji: '🎖️' },
  { id: 'r10', rank: 10, name: '火焰操控者', score: 987, winRate: 58, battles: 85, emoji: '🎖️' }
];

const DECK_CARDS_109: DeckCard109[] = [
  { id: 'd1', name: '青眼白龙·极龙', type: '怪兽', count: 3, emoji: '🐉', color: '#D32F2F' },
  { id: 'd2', name: '黑魔术师·少女', type: '魔法师', count: 2, emoji: '🧙', color: '#7B1FA2' },
  { id: 'd3', name: '暗黑骑士·统帅', type: '战士', count: 2, emoji: '⚔️', color: '#0277BD' },
  { id: 'd4', name: '红莲龙·爆裂', type: '怪兽', count: 1, emoji: '🐲', color: '#FF6F00' },
  { id: 'd5', name: '圣剑·誓约胜利', type: '装备', count: 1, emoji: '🗡️', color: '#FFD700' },
  { id: 'd6', name: '时之魔法师', type: '魔法', count: 1, emoji: '⏰', color: '#4FC3F7' }
];

const ORDERS_109: OrderItem109[] = [
  { id: 'o1', name: '青眼白龙·极龙×3', price: 384, status: '已发货', time: '2026-08-20' },
  { id: 'o2', name: '龙族传说整盒×1', price: 288, status: '待发货', time: '2026-08-22' },
  { id: 'o3', name: '黑魔术师×5', price: 340, status: '已完成', time: '2026-08-15' },
  { id: 'o4', name: '红莲龙×2', price: 196, status: '已取消', time: '2026-08-10' },
  { id: 'o5', name: '机甲神·零式×1', price: 158, status: '已发货', time: '2026-08-18' }
];

const FAVS_109: FavCard109[] = [
  { id: 'f1', name: '光明使者·大天使', rarity: 'SSR', price: 298, emoji: '😇' },
  { id: 'f2', name: '深渊魔王·路西法', rarity: 'SSR', price: 328, emoji: '😈' },
  { id: 'f3', name: '机甲神·零式', rarity: 'UR', price: 158, emoji: '🤖' },
  { id: 'f4', name: '圣剑·誓约胜利', rarity: 'UR', price: 118, emoji: '🗡️' }
];

const STATS_109: StatCard109[] = [
  { id: 's1', label: '胜率', value: '76%', emoji: '🏆', color: '#FFD700' },
  { id: 's2', label: '胜场', value: '42', emoji: '✅', color: '#4CAF50' },
  { id: 's3', label: '败场', value: '13', emoji: '❌', color: '#EF5350' },
  { id: 's4', label: '积分', value: '5432', emoji: '⭐', color: '#0277BD' }
];

const TOPICS_109: TopicMeta109[] = [
  { id: 't1', name: '卡组构筑', count: 567, color: '#0277BD' },
  { id: 't2', name: '稀有卡交流', count: 345, color: '#FFD700' },
  { id: 't3', name: '对战攻略', count: 234, color: '#D32F2F' },
  { id: 't4', name: '新手问答', count: 456, color: '#4CAF50' }
];

const POSTS_109: CommunityPost109[] = [
  { id: 'p1', user: '龙骑士', time: '2小时前', title: '龙族传说卡组最强构筑', content: '三张青眼白龙+红莲龙,进攻型卡组胜率85%', likes: 234, tag: '攻略', tagColor: '#0277BD' },
  { id: 'p2', user: '魔法师红', time: '4小时前', title: '黑魔术师卡组实战心得', content: '魔法陷阱配合是关键,记住每张卡的效果', likes: 345, tag: '心得', tagColor: '#FFD700' },
  { id: 'p3', user: '钢铁之心', time: '6小时前', title: '机械卡组防守反击', content: '先防守后反击,机械卡组后期能力强', likes: 167, tag: '攻略', tagColor: '#D32F2F' },
  { id: 'p4', user: '圣光守护', time: '10小时前', title: '新手如何选择初始卡组', content: '推荐龙族传说,攻击力高容易上手', likes: 89, tag: '问答', tagColor: '#4CAF50' },
  { id: 'p5', user: '暗影刺客', time: '1天前', title: 'SSR隐藏卡获取技巧', content: '整盒购买概率最高,隐藏卡约3%', likes: 456, tag: '技巧', tagColor: '#FFD700' }
];

const WEEK_BARS_109: BarItem109[] = [
  { id: 'w1', name: '一', value: 3, color: '#4FC3F7' },
  { id: 'w2', name: '二', value: 5, color: '#4FC3F7' },
  { id: 'w3', name: '三', value: 2, color: '#4FC3F7' },
  { id: 'w4', name: '四', value: 7, color: '#0277BD' },
  { id: 'w5', name: '五', value: 4, color: '#0277BD' },
  { id: 'w6', name: '六', value: 8, color: '#FFD700' },
  { id: 'w7', name: '日', value: 6, color: '#FFD700' }
];

// ============ 工具函数 ============
function getRarityColor109(rarity: string): string {
  if (rarity === 'SSR') return '#D32F2F';
  if (rarity === 'UR') return '#FF6F00';
  if (rarity === 'SR') return '#1565C0';
  return '#78909C';
}

function getRarityBg109(rarity: string): string {
  if (rarity === 'SSR') return '#FFEBEE';
  if (rarity === 'UR') return '#FFF3E0';
  if (rarity === 'SR') return '#E3F2FD';
  return '#ECEFF1';
}

function getResultColor109(result: string): string {
  if (result === '胜利') return '#43A047';
  return '#EF5350';
}

function getStatusColor109(status: string): string {
  if (status === '已发货') return '#1976D2';
  if (status === '待发货') return '#FF6F00';
  if (status === '已完成') return '#43A047';
  return '#9E9E9E';
}

function getRankColor109(rank: number): string {
  if (rank <= 3) return '#FFD700';
  if (rank <= 6) return '#9E9E9E';
  return '#CD7F32';
}

// ============ 入口 ============
@Entry
@Component
struct DuoDuoCardApp {
  @State currentTab: number = 0;
  @State showBuySheet: boolean = false;
  @State showSellDialog: boolean = false;
  @State showDeckSheet: boolean = false;
  @State showDeleteDialog: boolean = false;
  @State showBattleDialog: boolean = false;
  @State selectedCard: CardItem109 | null = null;
  @State selQty: number = 1;
  @State selBooster: boolean = false;
  @State selInsurance: boolean = false;
  @State sellPrice: number = 0;
  @State selDeckSlot: number = 0;

  build() {
    Column() {
      // ===== 游戏风头部 =====
      Row() {
        Text('卡牌对决')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS109.gold)
        Text('').layoutWeight(1)
        Text('🔍')
          .fontSize(22)
          .fontColor(COLORS109.gold)
          .margin({ right: 12 })
        Text('🎒')
          .fontSize(22)
          .fontColor(COLORS109.gold)
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })
      .linearGradient({ angle: 90, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })

      // ===== Tab 内容区 =====
      if (this.currentTab === 0) {
        CardShopTab109({
          onBuy: (c: CardItem109) => {
            this.selectedCard = c;
            this.showBuySheet = true;
          },
          onDetail: (c: CardItem109) => {
            this.selectedCard = c;
            this.showSellDialog = true;
          }
        })
      } else if (this.currentTab === 1) {
      } else if (this.currentTab === 2) {
        BattleTab109({
          onRecord: () => { this.showBattleDialog = true; }
        })
      } else if (this.currentTab === 3) {
      } else if (this.currentTab === 4) {
      } else {
        MyCardTab109({
          onDeck: () => { this.showDeckSheet = true; },
          onDelete: () => { this.showDeleteDialog = true; }
        })
      }

      // ===== 底部6 Tab =====
      Row() {
        TabBtn109({icon:'🃏', label:'卡牌', active: this.currentTab === 0, onTap: () => { this.currentTab = 0; }})
        TabBtn109({icon:'📦', label:'系列', active: this.currentTab === 1, onTap: () => { this.currentTab = 1; }})
        TabBtn109({icon:'⚔️', label:'对战', active: this.currentTab === 2, onTap: () => { this.currentTab = 2; }})
        TabBtn109({icon:'🔄', label:'交易', active: this.currentTab === 3, onTap: () => { this.currentTab = 3; }})
        TabBtn109({icon:'🏆', label:'排行', active: this.currentTab === 4, onTap: () => { this.currentTab = 4; }})
        TabBtn109({icon:'👤', label:'我的', active: this.currentTab === 5, onTap: () => { this.currentTab = 5; }})
      }
      .width('100%')
      .height(56)
      .backgroundColor(COLORS109.card)
      .border({ width: 1, color: COLORS109.border })
      .justifyContent(FlexAlign.SpaceAround)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS109.bg)
  }
}

// ============ 底部 Tab(顶部金边样式) ============
@Component
struct TabBtn109 {
  icon: string = '🃏';
  label: string = '';
  active: boolean = false;
  onTap: () => void = () => {};
  build() {
    Column() {
      if (this.active) {
        Row()
          .width('100%')
          .height(3)
          .backgroundColor(COLORS109.gold)
          .margin({ bottom: 4 })
      } else {
        Row().width('100%').height(3).backgroundColor(COLORS109.card).margin({ bottom: 4 })
      }
      Text(this.icon)
        .fontSize(20)
        .fontColor(this.active ? COLORS109.primary : COLORS109.textHint)
      Text(this.label)
        .fontSize(10)
        .fontColor(this.active ? COLORS109.primary : COLORS109.textHint)
        .margin({ top: 2 })
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Center)
    .onClick(() => { this.onTap(); })
  }
}

// ============ Tab0:卡牌商店(大卡+网格风) ============
@Component
struct CardShopTab109 {
  onBuy: (c: CardItem109) => void = () => {};
  onDetail: (c: CardItem109) => void = () => {};
  build() {
    Scroll() {
      Column() {
        // 搜索框
        Row() {
          Text('🔍 搜索卡牌、系列...')
            .fontSize(13)
            .fontColor(COLORS109.textHint)
            .layoutWeight(1)
        }
        .width('92%')
        .height(36)
        .backgroundColor(COLORS109.card)
        .borderRadius(18)
        .padding({ left: 16, right: 16 })
        .margin({ top: 12, bottom: 8 })

        // 横幅
        Row() {
          Text('🎴 新系列「野兽传说」上架 · 限时折扣')
            .fontSize(11)
            .fontColor(COLORS109.gold)
            .layoutWeight(1)
        }
        .width('100%')
        .height(28)
        .backgroundColor(COLORS109.primaryDark)
        .justifyContent(FlexAlign.Center)

        // 金刚区
        Grid() {
          ForEach(CAT_ENTRIES_109, (cat: CatEntry109) => {
            GridItem() {
              Column() {
                Column() {
                  Text(cat.emoji)
                    .fontSize(24)
                }
                .width(44)
                .height(44)
                .backgroundColor(cat.color)
                .borderRadius(22)
                .justifyContent(FlexAlign.Center)
                Text(cat.name)
                  .fontSize(9)
                  .fontColor(COLORS109.textSub)
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Center)
            }
          }, (cat: CatEntry109) => cat.id)
        }
        .columnsTemplate('1fr 1fr 1fr 1fr')
        .rowsGap(8)
        .columnsGap(8)
        .width('92%')
        .margin({ top: 12 })
        .backgroundColor(COLORS109.card)
        .borderRadius(12)
        .padding(12)

        // 本周对战统计
        Row() {
          Text('⚔️ 本周对战统计')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
        }
        .width('92%')
        .margin({ top: 16, bottom: 8 })

        Row() {
          ForEach(STATS_109, (s: StatCard109) => {
            Column() {
              Text(s.emoji)
                .fontSize(18)
              Text(s.value)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor(s.color)
                .margin({ top: 2 })
              Text(s.label)
                .fontSize(9)
                .fontColor(COLORS109.textSub)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }, (s: StatCard109) => s.id)
        }
        .width('92%')
        .padding({ top: 12, bottom: 12 })
        .backgroundColor(COLORS109.card)
        .borderRadius(12)
        .margin({ bottom: 12 })

        // 热卖卡牌横滚
        Row() {
          Text('🔥 热卖卡牌')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
          Text('更多 >')
            .fontSize(12)
            .fontColor(COLORS109.primary)
        }
        .width('92%')
        .margin({ bottom: 8 })

        Scroll() {
          Row() {
            ForEach(CARDS_109.slice(0, 6), (c: CardItem109) => {
              Column() {
                Stack({ alignContent: Alignment.TopEnd }) {
                  Column() {
                    Text(c.emoji)
                      .fontSize(36)
                  }
                  .width(72)
                  .height(72)
                  .backgroundColor(getRarityBg109(c.rarity))
                  .borderRadius(12)
                  .justifyContent(FlexAlign.Center)
                  Text(c.rarity)
                    .fontSize(8)
                    .fontColor(COLORS109.white)
                    .backgroundColor(getRarityColor109(c.rarity))
                    .borderRadius(3)
                    .padding({ left: 3, right: 3, top: 1, bottom: 1 })
                    .margin({ top: 3, right: 3 })
                }
                Text(c.name)
                  .fontSize(10)
                  .fontColor(COLORS109.textMain)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .width(72)
                  .margin({ top: 4 })
                Text('ATK ' + c.atk + ' / DEF ' + c.def)
                  .fontSize(8)
                  .fontColor(COLORS109.textSub)
                Row() {
                  Text('¥' + c.price)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS109.primary)
                  Text('¥' + c.oldPrice)
                    .fontSize(9)
                    .fontColor(COLORS109.textHint)
                    .decoration({ type: TextDecorationType.LineThrough })
                    .margin({ left: 4 })
                }
                .margin({ top: 2 })
              }
              .width(88)
              .margin({ right: 8 })
              .onClick(() => { this.onDetail(c); })
            }, (c: CardItem109) => c.id)
          }
          .padding({ left: 16, right: 16 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')
        .margin({ bottom: 12 })

        // 全部卡牌网格
        Row() {
          Text('🃏 全部卡牌')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
        }
        .width('92%')
        .margin({ bottom: 8 })

        Grid() {
          ForEach(CARDS_109, (c: CardItem109) => {
            GridItem() {
              Column() {
                Stack({ alignContent: Alignment.TopEnd }) {
                  Column() {
                    Text(c.emoji)
                      .fontSize(40)
                  }
                  .width('100%')
                  .height(80)
                  .backgroundColor(getRarityBg109(c.rarity))
                  .borderRadius(12)
                  .justifyContent(FlexAlign.Center)
                  Text(c.rarity)
                    .fontSize(8)
                    .fontColor(COLORS109.white)
                    .backgroundColor(getRarityColor109(c.rarity))
                    .borderRadius(3)
                    .padding({ left: 3, right: 3, top: 1, bottom: 1 })
                    .margin({ top: 3, right: 3 })
                }
                Text(c.name)
                  .fontSize(11)
                  .fontColor(COLORS109.textMain)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .margin({ top: 6 })
                Row() {
                  Text('ATK')
                    .fontSize(8)
                    .fontColor(COLORS109.textHint)
                  Text(c.atk.toString())
                    .fontSize(9)
                    .fontColor(COLORS109.danger)
                    .margin({ left: 2 })
                  Text('DEF')
                    .fontSize(8)
                    .fontColor(COLORS109.textHint)
                    .margin({ left: 4 })
                  Text(c.def.toString())
                    .fontSize(9)
                    .fontColor(COLORS109.primary)
                    .margin({ left: 2 })
                }
                .margin({ top: 2 })
                Row() {
                  Text('¥' + c.price)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS109.primary)
                  Text('¥' + c.oldPrice)
                    .fontSize(9)
                    .fontColor(COLORS109.textHint)
                    .decoration({ type: TextDecorationType.LineThrough })
                    .margin({ left: 4 })
                  Text('').layoutWeight(1)
                  Text('销' + c.sales)
                    .fontSize(8)
                    .fontColor(COLORS109.textHint)
                }
                .width('100%')
                .margin({ top: 4 })
                Row() {
                  Text('购买')
                    .fontSize(11)
                    .fontColor(COLORS109.white)
                    .backgroundColor(COLORS109.primary)
                    .borderRadius(6)
                    .padding({ left: 16, right: 16, top: 6, bottom: 6 })
                    .layoutWeight(1)
                    .textAlign(TextAlign.Center)
                }
                .width('100%')
                .margin({ top: 6 })
                .onClick(() => { this.onBuy(c); })
              }
              .padding(10)
              .backgroundColor(COLORS109.card)
              .borderRadius(12)
            }
          }, (c: CardItem109) => c.id)
        }
        .columnsTemplate('1fr 1fr')
        .rowsGap(8)
        .columnsGap(8)
        .width('92%')
        .margin({ bottom: 16 })
      }
      .width('100%')
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .constraintSize({ maxHeight: '80%' })
  }
}

// ============ Tab1:系列(时间线风) ============
@Component
struct SeriesTab109 {
  build() {
    Scroll() {
      Column() {
        Column() {
          Text('📦 卡牌系列')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.white)
          Text('收集图鉴·全系列收录')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.8)')
            .margin({ top: 4 })
        }
        .width('100%')
        .padding({ top: 20, bottom: 20 })
        .linearGradient({ angle: 90, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })

        ForEach(SERIES_109, (s: SeriesMeta109) => {
          Column() {
            Row() {
              Column() {
                Text(s.emoji)
                  .fontSize(36)
              }
              .width(64)
              .height(64)
              .linearGradient({ angle: 135, colors: [[s.color1, 0], [s.color2, 1]] })
              .borderRadius(12)
              .justifyContent(FlexAlign.Center)
              Column() {
                Text(s.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS109.textMain)
                Text(s.desc)
                  .fontSize(11)
                  .fontColor(COLORS109.textSub)
                  .margin({ top: 4 })
                Row() {
                  Text('共' + s.total + '款')
                    .fontSize(10)
                    .fontColor(COLORS109.textHint)
                  Text('UR×' + s.ultra)
                    .fontSize(10)
                    .fontColor(getRarityColor109('UR'))
                    .margin({ left: 8 })
                  if (s.secret > 0) {
                    Text('Secret×' + s.secret)
                      .fontSize(10)
                      .fontColor(getRarityColor109('SSR'))
                      .margin({ left: 8 })
                  }
                  Text(s.releaseDate)
                    .fontSize(10)
                    .fontColor(COLORS109.textHint)
                    .margin({ left: 8 })
                }
                .margin({ top: 4 })
              }
              .margin({ left: 12 })
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
            }
            .width('100%')
            Row() {
              Text('购买整盒')
                .fontSize(11)
                .fontColor(COLORS109.white)
                .backgroundColor(COLORS109.primary)
                .borderRadius(14)
                .padding({ left: 16, right: 16, top: 4, bottom: 4 })
              Text('').layoutWeight(1)
              Text('查看图鉴')
                .fontSize(11)
                .fontColor(COLORS109.primary)
                .backgroundColor(COLORS109.bg)
                .borderRadius(14)
                .padding({ left: 16, right: 16, top: 4, bottom: 4 })
            }
            .width('100%')
            .margin({ top: 10 })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(COLORS109.card)
          .borderRadius(12)
          .margin({ left: 12, right: 12, top: 6, bottom: 6 })
        }, (s: SeriesMeta109) => s.id)
      }
      .width('100%')
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .constraintSize({ maxHeight: '80%' })
  }
}

// ============ Tab2:对战(竞技场风) ============
@Component
struct BattleTab109 {
  onRecord: () => void = () => {};
  build() {
    Scroll() {
      Column() {
        Column() {
          Text('⚔️ 对战竞技场')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.gold)
          Text('实时对战·排名攀升')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.8)')
            .margin({ top: 4 })
        }
        .width('100%')
        .padding({ top: 24, bottom: 24 })
        .linearGradient({ angle: 135, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })

        // 战绩概览
        Row() {
          ForEach(STATS_109, (s: StatCard109) => {
            Column() {
              Text(s.emoji)
                .fontSize(20)
              Text(s.value)
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .fontColor(s.color)
                .margin({ top: 2 })
              Text(s.label)
                .fontSize(9)
                .fontColor(COLORS109.textSub)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }, (s: StatCard109) => s.id)
        }
        .width('92%')
        .padding({ top: 12, bottom: 12 })
        .backgroundColor(COLORS109.card)
        .borderRadius(12)
        .margin({ top: 12 })

        // 本周对战柱状图
        Row() {
          Text('📊 本周对战次数')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
        }
        .width('92%')
        .margin({ top: 16, bottom: 8 })

        Row() {
          ForEach(WEEK_BARS_109, (bar: BarItem109) => {
            Column() {
              Text(bar.value.toString())
                .fontSize(9)
                .fontColor(bar.color)
              Column() {
                Text('')
                  .width('100%')
                  .height(bar.value * 10)
                  .backgroundColor(bar.color)
                  .borderRadius(3)
              }
              .width(24)
              .height(80)
              .justifyContent(FlexAlign.End)
              .margin({ top: 4, bottom: 4 })
              Text(bar.name)
                .fontSize(9)
                .fontColor(COLORS109.textSub)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }, (bar: BarItem109) => bar.id)
        }
        .width('92%')
        .padding(16)
        .backgroundColor(COLORS109.card)
        .borderRadius(12)
        .margin({ bottom: 12 })

        // 对战记录
        Row() {
          Text('📜 对战记录')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
          Text('查看全部')
            .fontSize(11)
            .fontColor(COLORS109.primary)
            .onClick(() => { this.onRecord(); })
        }
        .width('92%')
        .margin({ bottom: 8 })

        ForEach(BATTLES_109, (b: BattleRecord109) => {
          Row() {
            Column() {
              Text(b.result === '胜利' ? '🏆' : '💀')
                .fontSize(20)
            }
            .width(40)
            .height(40)
            .backgroundColor(b.result === '胜利' ? '#E8F5E9' : '#FFEBEE')
            .borderRadius(20)
            .justifyContent(FlexAlign.Center)
            Column() {
              Text('vs ' + b.opponent)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS109.textMain)
              Row() {
                Text(b.result)
                  .fontSize(9)
                  .fontColor(getResultColor109(b.result))
                Text(b.deck)
                  .fontSize(9)
                  .fontColor(COLORS109.textSub)
                  .margin({ left: 6 })
                Text(b.turns + '回合')
                  .fontSize(9)
                  .fontColor(COLORS109.textHint)
                  .margin({ left: 6 })
              }
              .margin({ top: 2 })
              Text(b.time)
                .fontSize(9)
                .fontColor(COLORS109.textHint)
                .margin({ top: 2 })
            }
            .margin({ left: 8 })
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            Text('+' + b.score)
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(b.score > 0 ? COLORS109.gold : COLORS109.textHint)
          }
          .width('92%')
          .padding(10)
          .backgroundColor(COLORS109.card)
          .borderRadius(10)
          .margin({ left: 12, right: 12, bottom: 6 })
        }, (b: BattleRecord109) => b.id)
      }
      .width('100%')
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .constraintSize({ maxHeight: '80%' })
  }
}

// ============ Tab3:交易(市场风) ============
@Component
struct TradeTab109 {
  build() {
    Scroll() {
      Column() {
        Column() {
          Text('🔄 交易市场')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.white)
          Text('安全交易·平台担保')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.8)')
            .margin({ top: 4 })
        }
        .width('100%')
        .padding({ top: 20, bottom: 20 })
        .backgroundColor(COLORS109.primaryDark)

        ForEach(TRADES_109, (t: TradeItem109) => {
          Row() {
            Column() {
              Text(t.emoji)
                .fontSize(28)
            }
            .width(52)
            .height(52)
            .backgroundColor(getRarityBg109(t.rarity))
            .borderRadius(12)
            .justifyContent(FlexAlign.Center)
            Column() {
              Text(t.name)
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS109.textMain)
              Row() {
                Text(t.rarity)
                  .fontSize(9)
                  .fontColor(getRarityColor109(t.rarity))
                  .backgroundColor(getRarityBg109(t.rarity))
                  .borderRadius(4)
                  .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                Text(t.type)
                  .fontSize(9)
                  .fontColor(t.type === '出售' ? COLORS109.primary : COLORS109.danger)
                  .margin({ left: 6 })
              }
              .margin({ top: 4 })
              Text(t.seller + ' · ' + t.city)
                .fontSize(9)
                .fontColor(COLORS109.textHint)
                .margin({ top: 2 })
            }
            .margin({ left: 10 })
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            Column() {
              Text('¥' + t.price)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS109.primary)
              Text('交易')
                .fontSize(11)
                .fontColor(COLORS109.white)
                .backgroundColor(COLORS109.primary)
                .borderRadius(14)
                .padding({ left: 14, right: 14, top: 4, bottom: 4 })
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.End)
          }
          .width('92%')
          .padding(12)
          .backgroundColor(COLORS109.card)
          .borderRadius(12)
          .margin({ left: 12, right: 12, bottom: 8 })
        }, (t: TradeItem109) => t.id)

        // 社区帖子
        Row() {
          Text('💬 卡牌社区')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
        }
        .width('92%')
        .margin({ top: 16, bottom: 8 })

        Scroll() {
          Row() {
            ForEach(TOPICS_109, (t: TopicMeta109) => {
              Column() {
                Text('#' + t.name)
                  .fontSize(11)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(t.color)
                Text(t.count + ' 帖')
                  .fontSize(8)
                  .fontColor(COLORS109.textHint)
                  .margin({ top: 2 })
              }
              .padding({ left: 10, right: 10, top: 6, bottom: 6 })
              .backgroundColor(COLORS109.card)
              .borderRadius(8)
              .margin({ right: 6 })
            }, (t: TopicMeta109) => t.id)
          }
          .padding({ left: 16, right: 16 })
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
        .width('100%')
        .margin({ bottom: 8 })

        ForEach(POSTS_109, (p: CommunityPost109) => {
          Column() {
            Row() {
              Column() {
                Text(p.user.substring(0, 1))
                  .fontSize(14)
                  .fontColor(COLORS109.white)
                  .fontWeight(FontWeight.Bold)
              }
              .width(32)
              .height(32)
              .backgroundColor(COLORS109.primary)
              .borderRadius(16)
              .justifyContent(FlexAlign.Center)
              Column() {
                Text(p.user)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS109.textMain)
                Text(p.time)
                  .fontSize(9)
                  .fontColor(COLORS109.textHint)
                  .margin({ top: 1 })
              }
              .margin({ left: 8 })
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
              Text(p.tag)
                .fontSize(8)
                .fontColor(p.tagColor)
                .backgroundColor(COLORS109.bg)
                .borderRadius(4)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
            }
            .width('100%')
            Text(p.title)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.textMain)
              .margin({ top: 6 })
            Text(p.content)
              .fontSize(11)
              .fontColor(COLORS109.textSub)
              .margin({ top: 4 })
              .maxLines(2)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Row() {
              Text('👍 ' + p.likes)
                .fontSize(10)
                .fontColor(COLORS109.textSub)
              Text('💬')
                .fontSize(10)
                .fontColor(COLORS109.textSub)
                .margin({ left: 16 })
            }
            .width('100%')
            .margin({ top: 6 })
          }
          .width('92%')
          .padding(10)
          .backgroundColor(COLORS109.card)
          .borderRadius(10)
          .margin({ left: 12, right: 12, bottom: 6 })
        }, (p: CommunityPost109) => p.id)
      }
      .width('100%')
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .constraintSize({ maxHeight: '80%' })
  }
}

// ============ Tab4:排行(排行榜风) ============
@Component
struct RankTab109 {
  build() {
    Scroll() {
      Column() {
        Column() {
          Text('🏆 决斗者排行')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.gold)
          Text('本周最强决斗者')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.8)')
            .margin({ top: 4 })
        }
        .width('100%')
        .padding({ top: 24, bottom: 24 })
        .linearGradient({ angle: 135, colors: [[COLORS109.primaryDark, 0], [COLORS109.gold, 1]] })

        // 前三领奖台
        Row() {
          Column() {
            Text('🥈')
              .fontSize(28)
            Column() {
              Text(RANKS_109[1].name)
                .fontSize(11)
                .fontColor(COLORS109.textMain)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(RANKS_109[1].score.toString())
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#9E9E9E')
            }
            .width(72)
            .height(80)
            .backgroundColor('#F5F5F5')
            .borderRadius(12)
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)
            .padding(8)
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          .margin({ top: 12 })

          Column() {
            Text('🥇')
              .fontSize(36)
            Column() {
              Text(RANKS_109[0].name)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS109.textMain)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(RANKS_109[0].score.toString())
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS109.gold)
            }
            .width(80)
            .height(100)
            .backgroundColor('#FFF8E1')
            .borderRadius(12)
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)
            .padding(8)
            .border({ width: 2, color: COLORS109.gold })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)

          Column() {
            Text('🥉')
              .fontSize(28)
            Column() {
              Text(RANKS_109[2].name)
                .fontSize(11)
                .fontColor(COLORS109.textMain)
                .maxLines(1)
                .textOverflow({ overflow: TextOverflow.Ellipsis })
              Text(RANKS_109[2].score.toString())
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#CD7F32')
            }
            .width(72)
            .height(72)
            .backgroundColor('#FFF3E0')
            .borderRadius(12)
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)
            .padding(8)
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Center)
          .margin({ top: 20 })
        }
        .width('92%')
        .margin({ top: 12, bottom: 16 })

        // 4-10名列表
        ForEach(RANKS_109.slice(3), (r: RankItem109) => {
          Row() {
            Text(r.rank.toString())
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(getRankColor109(r.rank))
              .width(32)
            Column() {
              Text(r.emoji + ' ' + r.name)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS109.textMain)
              Row() {
                Text('胜率 ' + r.winRate + '%')
                  .fontSize(9)
                  .fontColor(COLORS109.success)
                Text(r.battles + '场')
                  .fontSize(9)
                  .fontColor(COLORS109.textHint)
                  .margin({ left: 6 })
              }
              .margin({ top: 2 })
            }
            .margin({ left: 8 })
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            Text(r.score.toString())
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.primary)
          }
          .width('92%')
          .padding(10)
          .backgroundColor(COLORS109.card)
          .borderRadius(10)
          .margin({ left: 12, right: 12, bottom: 6 })
        }, (r: RankItem109) => r.id)
      }
      .width('100%')
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .constraintSize({ maxHeight: '80%' })
  }
}

// ============ Tab5:我的(卡牌收藏风) ============
@Component
struct MyCardTab109 {
  onDeck: () => void = () => {};
  onDelete: () => void = () => {};
  build() {
    Scroll() {
      Column() {
        // 个人卡
        Row() {
          Column() {
            Text('卡')
              .fontSize(24)
              .fontColor(COLORS109.gold)
              .fontWeight(FontWeight.Bold)
          }
          .width(60)
          .height(60)
          .linearGradient({ angle: 135, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })
          .borderRadius(30)
          .justifyContent(FlexAlign.Center)
          Column() {
            Text('决斗者·龙骑士')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.textMain)
            Text('Lv.15 · 钻石段位')
              .fontSize(11)
              .fontColor(COLORS109.textSub)
              .margin({ top: 2 })
            Row() {
              Text('5432积分')
                .fontSize(9)
                .fontColor(COLORS109.gold)
                .margin({ right: 8 })
              Text('42胜')
                .fontSize(9)
                .fontColor(COLORS109.success)
            }
            .margin({ top: 2 })
          }
          .margin({ left: 12 })
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Text('>')
            .fontSize(16)
            .fontColor(COLORS109.textHint)
        }
        .width('92%')
        .padding(16)
        .backgroundColor(COLORS109.card)
        .borderRadius(12)
        .margin({ top: 12 })

        // 统计
        Row() {
          ForEach(STATS_109, (s: StatCard109) => {
            Column() {
              Text(s.emoji)
                .fontSize(16)
              Text(s.value)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor(s.color)
                .margin({ top: 2 })
              Text(s.label)
                .fontSize(9)
                .fontColor(COLORS109.textSub)
            }
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Center)
          }, (s: StatCard109) => s.id)
        }
        .width('92%')
        .padding({ top: 12, bottom: 12 })
        .backgroundColor(COLORS109.card)
        .borderRadius(12)
        .margin({ top: 8 })

        // 我的卡组
        Row() {
          Text('🎴 我的卡组')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
          Text('编辑')
            .fontSize(11)
            .fontColor(COLORS109.primary)
            .onClick(() => { this.onDeck(); })
        }
        .width('92%')
        .margin({ top: 16, bottom: 8 })

        Column() {
          Row() {
            Text('龙族传说卡组')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.textMain)
            Text('10/40')
              .fontSize(10)
              .fontColor(COLORS109.textSub)
              .margin({ left: 8 })
          }
          .width('100%')
          Row() {
            ForEach(DECK_CARDS_109, (d: DeckCard109) => {
              Column() {
                Text(d.emoji)
                  .fontSize(20)
                Text('×' + d.count)
                  .fontSize(8)
                  .fontColor(d.color)
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Center)
            }, (d: DeckCard109) => d.id)
          }
          .width('100%')
          .margin({ top: 8 })
        }
        .width('92%')
        .padding(12)
        .backgroundColor(COLORS109.card)
        .borderRadius(12)

        // 订单
        Row() {
          Text('📋 我的订单')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
        }
        .width('92%')
        .margin({ top: 16, bottom: 8 })

        ForEach(ORDERS_109, (o: OrderItem109) => {
          Row() {
            Text(o.name)
              .fontSize(12)
              .fontColor(COLORS109.textMain)
              .layoutWeight(1)
              .maxLines(1)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Text('¥' + o.price)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.primary)
            Text(o.status)
              .fontSize(9)
              .fontColor(getStatusColor109(o.status))
              .margin({ left: 8 })
          }
          .width('100%')
          .padding(10)
          .backgroundColor(COLORS109.card)
          .borderRadius(10)
          .margin({ left: 12, right: 12, bottom: 6 })
        }, (o: OrderItem109) => o.id)

        // 收藏
        Row() {
          Text('⭐ 卡牌收藏')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
          Text('删除')
            .fontSize(11)
            .fontColor(COLORS109.danger)
            .onClick(() => { this.onDelete(); })
        }
        .width('92%')
        .margin({ top: 16, bottom: 8 })

        ForEach(FAVS_109, (f: FavCard109) => {
          Row() {
            Column() {
              Text(f.emoji)
                .fontSize(24)
            }
            .width(48)
            .height(48)
            .backgroundColor(getRarityBg109(f.rarity))
            .borderRadius(10)
            .justifyContent(FlexAlign.Center)
            Column() {
              Text(f.name)
                .fontSize(12)
                .fontColor(COLORS109.textMain)
              Text(f.rarity)
                .fontSize(9)
                .fontColor(getRarityColor109(f.rarity))
                .margin({ top: 2 })
            }
            .margin({ left: 8 })
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            Text('¥' + f.price)
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.primary)
          }
          .width('100%')
          .padding(10)
          .backgroundColor(COLORS109.card)
          .borderRadius(10)
          .margin({ left: 12, right: 12, bottom: 6 })
        }, (f: FavCard109) => f.id)

        Text('v2.0 · 卡牌对决 · 2026')
          .fontSize(10)
          .fontColor(COLORS109.textHint)
          .alignSelf(ItemAlign.Center)
          .margin({ top: 16, bottom: 16 })
      }
      .width('100%')
    }
    .scrollable(ScrollDirection.Vertical)
    .scrollBar(BarState.Off)
    .constraintSize({ maxHeight: '80%' })
  }
}

// ============ 弹框1:购买卡牌(底部抽屉) ============
@Component
struct BuyCardSheet109 {
  card: CardItem109 | null = null;
  selQty: number = 1;
  selBooster: boolean = false;
  selInsurance: boolean = false;
  onQty: (q: number) => void = () => {};
  onBooster: (b: boolean) => void = () => {};
  onInsurance: (i: boolean) => void = () => {};
  onConfirm: () => void = () => {};
  onCancel: () => void = () => {};
  build() {
    Scroll() {
      Column() {
        Row() {
          Column() {
            Text(this.card?.emoji ?? '🃏')
              .fontSize(36)
          }
          .width(60)
          .height(60)
          .backgroundColor(getRarityBg109(this.card?.rarity ?? 'R'))
          .borderRadius(12)
          .justifyContent(FlexAlign.Center)
          Column() {
            Text(this.card?.name ?? '')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.textMain)
            Text(this.card?.series ?? '')
              .fontSize(10)
              .fontColor(COLORS109.textSub)
              .margin({ top: 2 })
            Row() {
              Text(this.card?.rarity ?? '')
                .fontSize(9)
                .fontColor(getRarityColor109(this.card?.rarity ?? 'R'))
              Text('ATK ' + (this.card?.atk ?? 0))
                .fontSize(9)
                .fontColor(COLORS109.danger)
                .margin({ left: 8 })
              Text('DEF ' + (this.card?.def ?? 0))
                .fontSize(9)
                .fontColor(COLORS109.primary)
                .margin({ left: 8 })
            }
            .margin({ top: 4 })
          }
          .margin({ left: 12 })
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Text('✕')
            .fontSize(18)
            .fontColor(COLORS109.textHint)
            .onClick(() => { this.onCancel(); })
        }
        .width('100%')
        .padding(16)

        Divider().color(COLORS109.border)

        Row() {
          Text('¥' + (this.card?.price ?? 0))
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.primary)
          Text('¥' + (this.card?.oldPrice ?? 0))
            .fontSize(12)
            .fontColor(COLORS109.textHint)
            .decoration({ type: TextDecorationType.LineThrough })
            .margin({ left: 6 })
          Text('').layoutWeight(1)
          Text('销' + (this.card?.sales ?? 0))
            .fontSize(10)
            .fontColor(COLORS109.textHint)
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 12 })

        // 数量
        Row() {
          Text('数量')
            .fontSize(13)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
          Text('−')
            .fontSize(16)
            .fontColor(COLORS109.primary)
            .padding({ left: 12, right: 12 })
            .onClick(() => { if (this.selQty > 1) { this.onQty(this.selQty - 1); } })
          Text(this.selQty.toString())
            .fontSize(14)
            .fontColor(COLORS109.textMain)
            .padding({ left: 12, right: 12 })
          Text('+')
            .fontSize(16)
            .fontColor(COLORS109.primary)
            .padding({ left: 12, right: 12 })
            .onClick(() => { this.onQty(this.selQty + 1); })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 16 })

        // 补充包选项
        Row() {
          Column() {
            Text('📦 购买补充包')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.textMain)
            Text('随机5张·含UR概率UP')
              .fontSize(10)
              .fontColor(COLORS109.textSub)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Text(this.selBooster ? '☑' : '☐')
            .fontSize(20)
            .fontColor(this.selBooster ? COLORS109.primary : COLORS109.textHint)
            .onClick(() => { this.onBooster(!this.selBooster); })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 16 })

        // 保险
        Row() {
          Column() {
            Text('🛡️ 卡牌保险')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.textMain)
            Text('运输损坏包赔')
              .fontSize(10)
              .fontColor(COLORS109.textSub)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
          Text(this.selInsurance ? '☑' : '☐')
            .fontSize(20)
            .fontColor(this.selInsurance ? COLORS109.primary : COLORS109.textHint)
            .onClick(() => { this.onInsurance(!this.selInsurance); })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 16, bottom: 16 })

        Row() {
          Text('合计')
            .fontSize(13)
            .fontColor(COLORS109.textSub)
          Text('¥' + ((this.card?.price ?? 0) * this.selQty))
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.primary)
            .margin({ left: 8 })
            .layoutWeight(1)
          Text('确认购买')
            .fontSize(14)
            .fontColor(COLORS109.white)
            .backgroundColor(COLORS109.primary)
            .borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .onClick(() => { this.onConfirm(); })
        }
        .width('100%')
        .padding({ left: 16, right: 16, bottom: 20 })
      }
      .width('100%')
      .backgroundColor(COLORS109.white)
    }
    .scrollBar(BarState.Off)
  }
}

// ============ 弹框2:出售卡牌(居中) ============
@Component
struct SellCardDialog109 {
  card: CardItem109 | null = null;
  sellPrice: number = 0;
  onPrice: (p: number) => void = () => {};
  onConfirm: () => void = () => {};
  onCancel: () => void = () => {};
  build() {
    Column() {
      Column() {
        Text('💰')
          .fontSize(40)
          .margin({ top: 24 })
        Text('出售卡牌')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS109.textMain)
          .margin({ top: 8 })

        Row() {
          Column() {
            Text(this.card?.emoji ?? '🃏')
              .fontSize(24)
          }
          .width(48)
          .height(48)
          .backgroundColor(getRarityBg109(this.card?.rarity ?? 'R'))
          .borderRadius(10)
          .justifyContent(FlexAlign.Center)
          Column() {
            Text(this.card?.name ?? '')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS109.textMain)
            Text(this.card?.rarity ?? '')
              .fontSize(9)
              .fontColor(getRarityColor109(this.card?.rarity ?? 'R'))
          }
          .margin({ left: 8 })
          .layoutWeight(1)
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .padding({ left: 20, right: 20, top: 12 })

        Text('市场参考价:¥' + (this.card?.price ?? 0))
          .fontSize(11)
          .fontColor(COLORS109.textSub)
          .padding({ left: 20, top: 8 })

        Text('出售价格')
          .fontSize(12)
          .fontColor(COLORS109.textSub)
          .width('100%')
          .padding({ left: 20, top: 12 })
        TextInput({ placeholder: '输入出售价格' })
          .placeholderColor(COLORS109.textHint)
          .fontSize(14)
          .width('85%')
          .backgroundColor(COLORS109.bg)
          .borderRadius(10)
          .margin({ top: 8 })
          .onChange((v: string) => { this.onPrice(parseInt(v) || 0); })

        Row() {
          Text('取消')
            .fontSize(14)
            .fontColor(COLORS109.textSub)
            .backgroundColor(COLORS109.border)
            .borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .onClick(() => { this.onCancel(); })
          Text('上架出售')
            .fontSize(14)
            .fontColor(COLORS109.white)
            .backgroundColor(COLORS109.primary)
            .borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.onConfirm(); })
        }
        .justifyContent(FlexAlign.Center)
        .padding({ top: 20, bottom: 20 })
      }
      .width('80%')
      .backgroundColor(COLORS109.white)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('rgba(0,0,0,0.5)')
  }
}

// ============ 弹框3:编辑卡组(底部抽屉) ============
@Component
struct EditDeckSheet109 {
  selSlot: number = 0;
  onSlot: (s: number) => void = () => {};
  onConfirm: () => void = () => {};
  onCancel: () => void = () => {};
  build() {
    Scroll() {
      Column() {
        Row() {
          Text('🎴 编辑卡组')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
          Text('✕')
            .fontSize(18)
            .fontColor(COLORS109.textHint)
            .onClick(() => { this.onCancel(); })
        }
        .width('100%')
        .padding(16)

        Divider().color(COLORS109.border)

        Text('当前卡组:龙族传说')
          .fontSize(13)
          .fontColor(COLORS109.textSub)
          .width('100%')
          .padding({ left: 16, top: 12 })
        Text('10/40 张 · 需要添加30张')
          .fontSize(11)
          .fontColor(COLORS109.warning)
          .width('100%')
          .padding({ left: 16, top: 4 })

        Text('卡牌列表')
          .fontSize(13)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS109.textMain)
          .width('100%')
          .padding({ left: 16, top: 16 })

        ForEach(DECK_CARDS_109, (d: DeckCard109, idx: number) => {
          Row() {
            Column() {
              Text(d.emoji)
                .fontSize(24)
            }
            .width(44)
            .height(44)
            .backgroundColor(d.color)
            .borderRadius(10)
            .justifyContent(FlexAlign.Center)
            Column() {
              Text(d.name)
                .fontSize(12)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS109.textMain)
              Text(d.type + ' · ×' + d.count)
                .fontSize(10)
                .fontColor(COLORS109.textSub)
                .margin({ top: 2 })
            }
            .margin({ left: 8 })
            .layoutWeight(1)
            .alignItems(HorizontalAlign.Start)
            Text(this.selSlot === idx ? '◉' : '○')
              .fontSize(18)
              .fontColor(this.selSlot === idx ? COLORS109.primary : COLORS109.textHint)
              .onClick(() => { this.onSlot(idx); })
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
        }, (d: DeckCard109) => d.id)

        Row() {
          Text('取消')
            .fontSize(14)
            .fontColor(COLORS109.textSub)
            .backgroundColor(COLORS109.border)
            .borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .onClick(() => { this.onCancel(); })
          Text('保存卡组')
            .fontSize(14)
            .fontColor(COLORS109.white)
            .backgroundColor(COLORS109.primary)
            .borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
            .onClick(() => { this.onConfirm(); })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 16, bottom: 20 })
      }
      .width('100%')
      .backgroundColor(COLORS109.white)
    }
    .scrollBar(BarState.Off)
  }
}

// ============ 弹框4:删除卡牌(居中) ============
@Component
struct DeleteCardDialog109 {
  onConfirm: () => void = () => {};
  onCancel: () => void = () => {};
  build() {
    Column() {
      Column() {
        Text('🗑️')
          .fontSize(40)
          .margin({ top: 24 })
        Text('移除卡牌')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS109.textMain)
          .margin({ top: 8 })
        Text('确认从收藏中移除?')
          .fontSize(12)
          .fontColor(COLORS109.textSub)
          .margin({ top: 4 })
        Row() {
          Text('取消')
            .fontSize(14)
            .fontColor(COLORS109.textSub)
            .backgroundColor(COLORS109.border)
            .borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .onClick(() => { this.onCancel(); })
          Text('确认移除')
            .fontSize(14)
            .fontColor(COLORS109.white)
            .backgroundColor(COLORS109.danger)
            .borderRadius(20)
            .padding({ left: 24, right: 24, top: 10, bottom: 10 })
            .margin({ left: 12 })
            .onClick(() => { this.onConfirm(); })
        }
        .justifyContent(FlexAlign.Center)
        .padding({ top: 24, bottom: 24 })
      }
      .width('75%')
      .backgroundColor(COLORS109.white)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('rgba(0,0,0,0.5)')
  }
}

// ============ 弹框5:对战记录(居中) ============
@Component
struct BattleRecordDialog109 {
  onClose: () => void = () => {};
  build() {
    Column() {
      Column() {
        Row() {
          Text('📜 对战记录')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS109.textMain)
            .layoutWeight(1)
          Text('✕')
            .fontSize(18)
            .fontColor(COLORS109.textHint)
            .onClick(() => { this.onClose(); })
        }
        .width('100%')
        .padding(16)

        Divider().color(COLORS109.border)

        Scroll() {
          Column() {
            ForEach(BATTLES_109, (b: BattleRecord109) => {
              Row() {
                Column() {
                  Text(b.result === '胜利' ? '🏆' : '💀')
                    .fontSize(18)
                }
                .width(36)
                .height(36)
                .backgroundColor(b.result === '胜利' ? '#E8F5E9' : '#FFEBEE')
                .borderRadius(18)
                .justifyContent(FlexAlign.Center)
                Column() {
                  Text('vs ' + b.opponent)
                    .fontSize(12)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS109.textMain)
                  Row() {
                    Text(b.result)
                      .fontSize(9)
                      .fontColor(getResultColor109(b.result))
                    Text(b.deck)
                      .fontSize(9)
                      .fontColor(COLORS109.textSub)
                      .margin({ left: 6 })
                    Text(b.turns + '回合')
                      .fontSize(9)
                      .fontColor(COLORS109.textHint)
                      .margin({ left: 6 })
                    Text(b.time)
                      .fontSize(9)
                      .fontColor(COLORS109.textHint)
                      .margin({ left: 6 })
                  }
                  .margin({ top: 2 })
                }
                .margin({ left: 8 })
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                Text('+' + b.score)
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(b.score > 0 ? COLORS109.gold : COLORS109.textHint)
              }
              .width('100%')
              .padding(8)
              .margin({ bottom: 6 })
            }, (b: BattleRecord109) => b.id)
          }
          .width('100%')
          .padding(12)
        }
        .constraintSize({ maxHeight: '60%' })

        Text('关闭')
          .fontSize(14)
          .fontColor(COLORS109.white)
          .backgroundColor(COLORS109.primary)
          .borderRadius(20)
          .padding({ left: 32, right: 32, top: 10, bottom: 10 })
          .alignSelf(ItemAlign.Center)
          .margin({ top: 8, bottom: 20 })
          .onClick(() => { this.onClose(); })
      }
      .width('85%')
      .backgroundColor(COLORS109.white)
      .borderRadius(16)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('rgba(0,0,0,0.5)')
  }
}


总结

在这里插入图片描述

本文深入解析了一款基于HarmonyOS ArkTS声明式UI范式开发的潮玩卡牌对战应用,从配色体系与数据模型设计、工具函数与入口组件架构、卡牌商店与系列图鉴布局、对战竞技场与排行榜实现、购买与出售弹框组件等维度,完整呈现了卡牌游戏类应用的开发实践。应用通过CardItem109SeriesMeta109BattleRecord109RankItem109等完整的interface接口体系,将卡牌的稀有度、攻防属性、系列关联、对战记录等复杂业务数据映射为类型安全的数据模型,配合静态数据常量实现了数据与视图的彻底解耦。

在UI实现层面,应用展现了丰富的布局策略——卡牌商店采用搜索+横幅+金刚区+横滑+网格的多层信息流布局、系列图鉴采用时间线卡片+渐变头像的差异化展示、对战竞技场采用统计+柱状图+记录列表的复合布局、排行榜采用领奖台+列表的分层展示。每种布局都针对其业务场景做了专门优化,避免了千篇一律的列表式设计。稀有度颜色映射通过getRarityColor109getRarityBg109双函数实现颜色与背景的分离管理,使得同一种稀有度在标签和背景区域获得协调的色彩表现。

从工程规范角度,应用严格遵循了"无Blank、Button无文字、constraintSize约束、interface全覆盖、UI区无变量声明"的ArkTS编码规范,通过Stack堆叠容器实现卡牌角标定位、通过linearGradient实现系列专属渐变头像、通过TextDecorationType.LineThrough实现原价删除线效果、通过FlexAlign.End实现柱状图自底向上渲染。这些技术手段均为ArkTS声明式UI的内置能力,无需引入第三方库即可实现专业级的游戏化界面效果。随着HarmonyOS生态在游戏类应用领域的持续拓展,本文所解析的卡牌数据建模和组件化开发方案可为同类应用提供直接的工程参考。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐