基于HarmonyOS API 24的ArkTS集换卡牌对战馆卡组管理与交易系统深度解析——HarmonyOS 6.1.1声明式UI暗夜鎏金视觉实战

在数字收藏与集换式卡牌游戏蓬勃发展的今天,一款集卡组管理、卡牌鉴定、对战社区于一体的移动应用成为卡牌爱好者的刚需。本文将深入剖析一款采用暗夜紫与鎏金配色风格的集换卡牌对战馆应用,从架构设计、数据模型、动画交互到状态管理,全方位展示HarmonyOS ArkTS声明式UI的强大表现力。

该应用以"ARCANA VAULT"为品牌核心,围绕卡牌收藏生态构建了首页、卡组、单卡、牌垫、卡套、个人中心六大模块,通过精心设计的深色主题和鎏金点缀,营造出神秘而高贵的收藏氛围。技术层面采用纯函数工具集与组件化分层架构,确保代码的可维护性与性能表现。

引言

在这里插入图片描述

集换式卡牌游戏(TCG)自诞生以来便在全球范围内拥有庞大的用户群体,从实体卡牌到数字化转型,玩家对卡牌管理、交易、对战的需求日益增长。在HarmonyOS生态下,利用ArkTS声明式UI框架构建一款兼具视觉冲击力与实用功能的卡牌对战馆应用,不仅能够满足玩家的核心诉求,更能展示出HarmonyOS在复杂交互场景下的技术优势。

本应用采用暗夜紫为主色调,搭配鎏金作为强调色,构建出神秘而奢华的视觉体验。整体架构遵循HarmonyOS推荐的组件化设计思想,将应用拆分为多个独立的Tab组件,每个组件负责一个业务领域的完整功能实现。数据层通过TypeScript接口定义严格的数据模型,结合纯函数工具集实现数据过滤、排序、统计等操作,确保业务逻辑与视图层的有效解耦。

在交互设计层面,应用大量运用动态视觉效果增强用户体验,包括发光呼吸动画、浮动位移、闪烁脉冲、旋转光效等。这些动画均通过setInterval驱动的状态变量结合数学函数实现,无需依赖额外的动画库,充分体现了ArkTS声明式UI的灵活表达能力。同时,应用还提供了完整的增删改查功能,用户可以上架新卡组、编辑卡组信息、查看详情、删除卡组,形成闭环的数据管理体验。

主题系统与数据模型设计

在这里插入图片描述

主题配置与接口定义

应用的视觉一致性通过统一的主题系统来保障。主题对象采用接口TgTheme定义,包含背景色、主色调、强调色、卡片色、文字色等十一个核心色值字段,确保整个应用的配色体系严格遵循设计规范。

interface TgTheme {
  bg: string
  primary: string
  accent: string
  card: string
  text: string
  sub: string
  line: string
  glow: string
  dark: string
}

const TG: TgTheme = {
  bg: '#201826',
  primary: '#C9A227',
  accent: '#B84A9E',
  card: '#2C2233',
  text: '#F5EFFF',
  sub: '#9C8FB0',
  line: '#3D3047',
  glow: '#E8C25A',
  dark: '#171020'
}

主题系统的核心价值在于将视觉变量与组件逻辑分离。当需要调整配色方案时,只需修改主题常量对象,整个应用的视觉风格即可统一变更,大幅降低了维护成本。这种设计模式在大型应用中尤为重要,它确保了多团队协作下的视觉一致性。

除主题接口外,应用还定义了多个业务数据接口,包括卡组TgDeck、单卡TgCard、牌垫TgMat、卡套TgSleeve、订单TgOrder、收藏TgFav等。每个接口都严格规定了字段名称和类型,为后续的数据操作提供了类型安全保障。以卡组接口为例,它包含了名称、阵营、胜率、价格、梯队、使用人次等核心属性,完整覆盖了卡牌游戏中卡组的关键信息维度。

interface TgDeck {
  name: string
  faction: string
  winrate: number
  price: number
  tier: string
  uses: number
}

interface TgCard {
  name: string
  rarity: string
  atk: number
  hp: number
  set: string
  price: number
  stock: number
}

数据常量与初始化

在这里插入图片描述

应用采用常量数组的方式预置了丰富的模拟数据,涵盖卡组、单卡、牌垫、卡套、订单、收藏、月度消费等多个业务维度。这些数据不仅为UI展示提供了真实的内容支撑,也为数据操作功能的测试提供了便利。

const TG_DECKS: TgDeck[] = [
  { name: '赤焰速攻 RUSH', faction: '红速', winrate: 62, price: 899, tier: 'T1', uses: 3210 },
  { name: '深渊控场 CONTROL', faction: '蓝控', winrate: 58, price: 1299, tier: 'T1', uses: 2754 },
  { name: '翠绿铺场 TOKEN', faction: '绿铺场', winrate: 55, price: 759, tier: 'T2', uses: 2418 },
  { name: '暗夜破坏 DISRUPT', faction: '黑破坏', winrate: 60, price: 1080, tier: 'T1', uses: 1962 },
  { name: '圣光回复 HEAL', faction: '白回复', winrate: 52, price: 659, tier: 'T2', uses: 1735 },
  // ... 更多卡组数据
]

数据常量的设计遵循了业务真实性原则,卡组数据中包含了五大阵营(红速、蓝控、绿铺场、黑破坏、白回复)和三个梯队等级(T0、T1、T2、T3),价格区间从数百元到上千元不等,使用人次也有明显的梯度差异。这种精心设计的数据分布使得应用的图表展示和排序筛选功能能够呈现出丰富的视觉层次。

工具函数层:纯函数的数据处理

在这里插入图片描述

动画计算函数

应用的动画效果全部基于纯函数计算实现。这些函数接收当前帧计数器和索引参数,返回对应的样式属性值。这种设计使得动画逻辑完全可预测且易于测试,同时避免了在组件内部编写复杂的动画代码。

function tgGlow(w: number): number {
  return 0.45 + Math.abs(Math.sin(w / 8)) * 0.55
}

function tgShineO(w: number): number {
  return 0.4 + Math.abs(Math.sin(w / 12)) * 0.5
}

function tgFloatY(w: number, i: number): number {
  return Math.sin((w + i * 9) / 8) * 4
}

function tgBlink(w: number): number {
  return w % 16 < 8 ? 1 : 0.35
}

function tgSpin(w: number, i: number): number {
  return (w * 5 + i * 40) % 360
}

纯函数动画的核心优势在于可组合性和可测试性。每个动画函数只负责计算单一属性的变化,通过在组件中将多个函数的返回值组合到不同的样式属性上,可以创造出复杂而协调的动画效果。例如,将tgGlow用于透明度、tgSpin用于旋转角度、tgFloatY用于位移,三者结合便能产生悬浮发光旋转的复合动画。

上述函数分别实现了发光呼吸、闪烁、浮动、脉冲和旋转等基础动画效果。它们都基于三角函数或模运算,确保动画在循环播放时具有平滑的过渡效果。参数w代表全局的动画帧计数器,i代表元素索引,通过索引偏移可以实现同组元素的错峰动画效果,避免视觉上的单调感。

颜色映射函数

在这里插入图片描述

在卡牌游戏中,不同稀有度、阵营、梯队通常需要用不同颜色来区分。应用通过一系列颜色映射函数将业务枚举值转换为对应的色值,实现了数据与视觉的自动映射。

function tgRarityColor(r: string): string {
  if (r === 'UR') { return TG.primary }
  if (r === 'SSR') { return TG.accent }
  if (r === 'H') { return '#E86A4A' }
  return '#7A9BC2'
}

function tgFactionColor(f: string): string {
  if (f === '红速') { return '#D9534F' }
  if (f === '蓝控') { return '#4A90C2' }
  if (f === '绿铺场') { return '#5B9C6E' }
  if (f === '黑破坏') { return '#8A6FC2' }
  return '#E8D9A0'
}

function tgTierColor(t: string): string {
  if (t === 'T0') { return TG.primary }
  if (t === 'T1') { return TG.accent }
  return TG.sub
}

颜色映射函数采用条件判断的方式实现,虽然实现方式简单直接,但在业务场景中非常实用。当需要新增阵营或稀有度时,只需在对应函数中添加一个分支即可,维护成本很低。

数据过滤与统计函数

在这里插入图片描述

数据操作层提供了过滤、排序、统计等常用功能,这些函数都接收原始数据数组作为参数,返回处理后的结果,完全符合函数式编程的纯函数特性。

function tgDeckFilter(arr: TgDeck[], t: string): TgDeck[] {
  let r: TgDeck[] = []
  for (let i = 0; i < arr.length; i++) {
    if (t === '全部' || arr[i].faction === t) {
      r.push(arr[i])
    }
  }
  return r
}

function tgDeckTop(): TgDeck[] {
  let r: TgDeck[] = TG_DECKS.slice()
  r.sort((a: TgDeck, b: TgDeck) => b.uses - a.uses)
  return r.slice(0, 6)
}

function tgTotalPay(): number {
  let r: number = 0
  for (let i = 0; i < TG_MONTHS.length; i++) {
    r = r + TG_MONTHS[i].pay
  }
  return r
}

纯函数数据处理的设计理念使得业务逻辑与UI组件完全解耦。组件只需要调用对应的工具函数即可获得所需的数据,无需关心数据处理的具体实现细节。这种分层架构不仅提高了代码的可读性和可维护性,也为单元测试提供了便利——每个工具函数都可以独立测试,无需依赖组件环境。

这些函数涵盖了按阵营筛选卡组、按使用人次排序取Top 6、计算月度消费总额等常见业务场景。值得注意的是,在排序函数中使用了slice()方法创建数组副本,避免了对原始数据的直接修改,这是函数式编程中"数据不可变"原则的具体体现。

主应用架构与导航系统

在这里插入图片描述

根组件结构

应用的根组件TgApp采用@Entry装饰器标记,是整个应用的入口。它通过@State装饰器维护当前选中的Tab索引,并构建了包含顶部品牌栏、搜索栏、内容区和底部Tab栏的经典移动端布局结构。

@Entry
@Component
struct TgApp {
  @State tabIdx: number = 0

  build() {
    Stack() {
      Column() {
        // 顶部品牌与搜索栏
        Column() {
          Row() {
            Column() {
              Text('ARCANA VAULT')
                .fontSize(21)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.primary)
                .letterSpacing(2)
              Text('集换卡牌对战馆 · 鉴定 & 对战')
                .fontSize(10)
                .fontColor(TG.sub)
                .margin({ top: 2 })
            }
            // ... 右侧大赛入口
          }
          // ... 搜索栏
        }
        .width('100%')
        .backgroundColor(TG.dark)

        // 内容区域
        Column() {
          if (this.tabIdx === 0) {
            TgHomeTab()
          } else if (this.tabIdx === 1) {
            TgDeckTab()
          } else if (this.tabIdx === 2) {
            TgCardTab()
          } else if (this.tabIdx === 3) {
            TgMatTab()
          } else if (this.tabIdx === 4) {
            TgSleeveTab()
          } else {
            TgMineTab()
          }
        }
        .layoutWeight(1)
        .width('100%')

        // 底部Tab栏
        Row() {
          ForEach(TG_TABS, (t: TgTab, i: number) => {
            Column() {
              Text(t.icon)
                .fontSize(18)
                .opacity(this.tabIdx === i ? 1 : 0.55)
                .scale(this.tabIdx === i ? { x: 1.15, y: 1.15 } : { x: 1, y: 1 })
              Text(t.label)
                .fontSize(10)
                .fontColor(this.tabIdx === i ? TG.primary : TG.sub)
                .fontWeight(this.tabIdx === i ? FontWeight.Bold : FontWeight.Normal)
                .margin({ top: 2 })
            }
            .layoutWeight(1)
            .onClick(() => {
              this.tabIdx = i
            })
          })
        }
        // ... Tab栏样式
      }
    }
  }
}

根组件的设计体现了声明式UI的核心思想:通过状态变量tabIdx驱动视图切换。当用户点击底部Tab时,只需修改tabIdx的值,框架会自动重新执行build()方法,根据新的状态值渲染对应的Tab组件。这种"状态驱动视图"的模式大大简化了UI交互的代码复杂度。

底部导航栏实现

底部导航栏通过ForEach循环遍历TG_TABS数组生成,每个Tab项包含图标和文字两个元素。选中态通过三元表达式判断,应用不同的透明度、缩放和文字颜色,视觉反馈清晰明确。

const TG_TABS: TgTab[] = [
  { icon: '🃏', label: '首页' },
  { icon: '🎴', label: '卡组' },
  { icon: '✨', label: '单卡' },
  { icon: '🟪', label: '牌垫' },
  { icon: '🛡', label: '卡套' },
  { icon: '👤', label: '我的' }
]

六个Tab项分别对应应用的六大功能模块,采用emoji作为图标既简化了开发又保持了良好的视觉效果。选中态的缩放动画(scale 1.15倍)为用户点击提供了即时的视觉反馈,提升了交互体验。

业务流程图

每日签到

新弹预购

卡牌置换

鉴定服务

卡组Tab

单卡Tab

牌垫Tab

卡套Tab

我的Tab

编辑

删除

新增

用户启动应用

加载TgApp根组件

初始化tabIdx=0显示首页

用户浏览首页活动

点击功能入口?

打开签到弹窗

打开预购弹窗

打开置换弹窗

打开鉴定弹窗

用户操作完成后关闭

用户切换Tab

选择哪个Tab?

加载TgDeckTab组件

加载TgCardTab组件

加载TgMatTab组件

加载TgSleeveTab组件

加载TgMineTab组件

按阵营筛选卡组

点击卡组卡片

打开详情弹窗

用户操作?

编辑卡组信息

确认删除卡组

上架新卡组

数据更新后刷新列表

卡组Tab:完整的CRUD交互

列表展示与筛选

卡组Tab是应用中功能最完整的模块之一,它包含了横向筛选标签、卡组列表、使用人次柱状图以及完整的增删改查弹窗。组件内部维护了多个状态变量,包括当前选中的筛选标签、卡组列表数据、动画帧计数器以及各种弹窗的显示状态。

@Component
struct TgDeckTab {
  @State wave: number = 0
  @State timer: number = -1
  @State chip: string = '全部'
  @State decks: TgDeck[] = []
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showEdit: boolean = false
  @State showDel: boolean = false
  @State selIdx: number = 0
  @State editIdx: number = 0
  @State delIdx: number = 0
  @State nName: string = ''
  @State nFaction: string = '红速'
  @State nPrice: string = ''
  @State eName: string = ''
  @State eWin: string = ''

  aboutToAppear(): void {
    this.decks = TG_DECKS.slice()
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }
  // ... build方法
}

组件的生命周期管理是ArkTS开发中的重要环节。aboutToAppear在组件即将出现时执行,适合进行数据初始化和定时器启动;aboutToDisappear在组件即将消失时执行,必须及时清理定时器等资源,防止内存泄漏。本应用在每个包含动画的组件中都严格遵循了这一模式,体现了良好的编程规范。

组件初始化时通过TG_DECKS.slice()创建数据副本,确保组件内部的数据操作不会影响全局常量。动画定时器设置为每60毫秒递增一次帧计数器,配合前面介绍的动画函数实现流畅的动态效果。

卡组卡片设计

每个卡组卡片采用横向布局,左侧展示梯队等级,中间是卡组名称和阵营信息,右侧是价格和操作按钮。卡片底部还包含胜率进度条,直观展示卡组的强度水平。

ForEach(this.decks, (d: TgDeck, i: number) => {
  Column() {
    Row() {
      Column() {
        Text(d.tier)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(tgTierColor(d.tier))
        Text('梯队')
          .fontSize(8)
          .fontColor(TG.sub)
          .margin({ top: 2 })
      }
      .width(52)
      .height(52)
      .borderRadius(12)
      .backgroundColor(TG.dark)
      .border({ width: 1, color: tgFactionColor(d.faction) })
      .justifyContent(FlexAlign.Center)

      Column() {
        Text(d.name)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(TG.text)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        Row() {
          Text(d.faction)
            .fontSize(9)
            .fontColor('#FFFFFF')
            .backgroundColor(tgFactionColor(d.faction))
            .borderRadius(6)
            .padding({ left: 5, right: 5, top: 1, bottom: 1 })
          Text('使用 ' + d.uses + ' 人次')
            .fontSize(9)
            .fontColor(TG.sub)
            .margin({ left: 8 })
        }
        .margin({ top: 5 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .margin({ left: 12 })

      Column() {
        Text(tgPrice(d.price))
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor(TG.primary)
        Row() {
          Text('详情')
            .fontSize(9)
            .fontColor(TG.sub)
            .onClick(() => {
              this.selIdx = i
              this.showDetail = true
            })
          Text('编辑')
            .fontSize(9)
            .fontColor(TG.accent)
            .margin({ left: 8 })
            .onClick(() => {
              this.editIdx = i
              this.eName = d.name
              this.eWin = String(d.winrate)
              this.showEdit = true
            })
        }
        .margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width('100%')

    Row() {
      Text('胜率 ' + d.winrate + '%')
        .fontSize(9)
        .fontColor(TG.sub)
        .width(64)
      Stack({ alignContent: Alignment.Start }) {
        Row()
          .width('100%')
          .height(8)
          .borderRadius(4)
          .backgroundColor(TG.dark)
        Row()
          .width(tgBarW(d.winrate, 100))
          .height(8)
          .borderRadius(4)
          .backgroundColor(tgFactionColor(d.faction))
          .opacity(0.5 + tgBlink(this.wave + i * 4) * 0.4)
      }
      .layoutWeight(1)
      Text(d.winrate >= 58 ? '强势' : '常规')
        .fontSize(9)
        .fontColor(d.winrate >= 58 ? TG.primary : TG.sub)
        .width(36)
        .textAlign(TextAlign.End)
    }
    .width('100%')
    .margin({ top: 8 })
  }
  .width('100%')
  .backgroundColor(TG.card)
  .borderRadius(16)
  .border({ width: 1, color: TG.line })
  .padding(12)
  .margin({ left: 12, right: 12, top: 10 })
})

卡组卡片的设计细节体现了对卡牌游戏用户体验的深入理解。梯队标签采用方形徽章设计,边框颜色与阵营对应,直观传达卡组的定位。胜率进度条结合了闪烁动画,高胜率卡组还会额外显示"强势"标签,帮助用户快速识别优质卡组。

弹窗系统与数据操作

应用的弹窗采用Stack布局叠加实现,通过@State变量控制显示与隐藏。每个弹窗都包含半透明遮罩层和内容区域,点击遮罩层可关闭弹窗。新增、编辑、删除等操作均通过弹窗完成,形成完整的数据管理闭环。

if (this.showAdd) {
  Stack() {
    this.modalOverlay()
    Column() {
      Text('上架卡组')
        .fontSize(17)
        .fontWeight(FontWeight.Bold)
        .fontColor(TG.text)

      Column() {
        Text('名称')
          .fontSize(11)
          .fontColor(TG.sub)
        TextInput({ placeholder: '如 星辉连锁 BURN' })
          .fontSize(13)
          .height(40)
          .borderRadius(10)
          .backgroundColor(TG.dark)
          .margin({ top: 6 })
          .onChange((v: string) => {
            this.nName = v
          })
      }
      .width('100%')
      .alignItems(HorizontalAlign.Start)
      .margin({ top: 14 })
      
      // ... 流派选择、价格输入

      Row() {
        Text('取消')
          .fontSize(14)
          .fontColor(TG.sub)
          .layoutWeight(1)
          .onClick(() => {
            this.showAdd = false
          })
        Text('确认上架')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor(TG.dark)
          .backgroundColor(TG.primary)
          .layoutWeight(1)
          .borderRadius(12)
          .onClick(() => {
            let it: TgDeck = {
              name: this.nName === '' ? '自定义卡组' : this.nName,
              faction: this.nFaction,
              winrate: 50,
              price: isNaN(parseInt(this.nPrice)) ? 799 : parseInt(this.nPrice),
              tier: 'T3',
              uses: 0
            }
            this.decks.push(it)
            this.showAdd = false
          })
      }
    }
  }
}

弹窗表单的输入校验是用户体验的重要保障。在新增卡组的逻辑中,对用户输入做了优雅的降级处理:名称为空时使用默认值"自定义卡组",价格解析失败时使用默认价格799。这种设计避免了因用户输入不规范而导致的程序异常,同时也减少了不必要的错误提示,提升了操作流畅度。

新增功能中包含了对用户输入的容错处理,确保即使输入不规范也能正常完成操作。新增的卡组默认设置为T3梯队、50%胜率、0使用人次,符合新卡组的初始状态设定。

首页设计:活动入口与功能导航

横幅与功能入口

首页是用户进入应用后看到的第一个页面,承担着活动展示和功能导航的双重职责。顶部采用深色卡片作为活动横幅,配合发光闪烁的装饰元素营造氛围感。横幅下方是四个圆形功能入口,分别对应每日签到、新弹预购、卡牌置换和鉴定服务。

@Component
struct TgHomeTab {
  @State wave: number = 0
  @State timer: number = -1
  @State showEvent: boolean = false
  @State showSign: boolean = false
  @State showPre: boolean = false
  @State showTrade: boolean = false
  @State signDays: number = 12

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }
  // ...
}

首页组件维护了四个弹窗的显示状态,每个功能入口点击后都会弹出对应的交互窗口。这种设计将复杂的操作流程收纳在弹窗中,保持了首页的简洁性,同时也减少了页面跳转带来的视觉中断。

动画效果实现

首页的动画效果丰富而细腻,包括横幅上的旋转发光装饰、功能图标的上下浮动、以及各种闪烁呼吸效果。这些动画都基于同一个wave状态变量驱动,确保了动画的同步性和协调性。

Stack() {
  Column()
    .width('100%')
    .height(165)
    .backgroundColor(TG.dark)
    .borderRadius(18)
    .border({ width: 1, color: TG.line })

  Column() {
    Text('城市巡回赛 · 站点赛报名')
      .fontSize(17)
      .fontWeight(FontWeight.Bold)
      .fontColor(TG.text)
    // ... 活动信息与报名按钮
  }
  .alignItems(HorizontalAlign.Start)
  .padding(16)

  Column() {
    Text('✨')
      .fontSize(26)
      .opacity(tgGlow(this.wave))
      .rotate({ angle: tgSpin(this.wave, 0) * 0.2 })
  }
  .position({ x: 30, y: 26 })
}

通过将tgGlow函数的返回值应用于opacity属性,将tgSpin函数的返回值应用于rotate属性,实现了星星图标的发光旋转效果。这种将数学计算结果直接绑定到样式属性的方式,正是ArkTS声明式UI的精髓所在。

技术点对比表格

技术维度 实现方案 优势 适用场景
状态管理 @State 装饰器 + 组件内部状态 轻量、直观、无需额外依赖 单组件内状态流转
动画实现 setInterval + 数学纯函数 灵活可控、可组合性强、性能优异 复杂定制化动画效果
数据过滤 纯函数工具集(函数式编程) 可测试性强、逻辑复用度高、无副作用 列表筛选、排序、统计
弹窗交互 Stack 叠加 + @State 控制显隐 实现简单、层级清晰、过渡自然 表单填写、详情展示、确认操作
主题管理 常量对象 + 接口约束 统一管控、易于扩展、维护成本低 多页面风格一致性保障
列表渲染 ForEach + 数据数组 声明式渲染、自动diff、性能优化 长列表、动态数据展示
生命周期 aboutToAppear / aboutToDisappear 精准控制资源创建与释放 定时器管理、数据预加载
布局方式 Column/Row/Stack + Flex 组合灵活、表达力强、适应性广 各种复杂页面结构

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

interface TgTheme {
  bg: string
  primary: string
  accent: string
  card: string
  text: string
  sub: string
  line: string
  glow: string
  dark: string
}

const TG: TgTheme = {
  bg: '#201826',
  primary: '#C9A227',
  accent: '#B84A9E',
  card: '#2C2233',
  text: '#F5EFFF',
  sub: '#9C8FB0',
  line: '#3D3047',
  glow: '#E8C25A',
  dark: '#171020'
}

interface TgDeck {
  name: string
  faction: string
  winrate: number
  price: number
  tier: string
  uses: number
}

interface TgCard {
  name: string
  rarity: string
  atk: number
  hp: number
  set: string
  price: number
  stock: number
}

interface TgMat {
  name: string
  size: string
  price: number
  stock: number
  art: string
}

interface TgSleeve {
  name: string
  colorHex: string
  price: number
  count: number
  finish: string
}

interface TgOrder {
  id: string
  name: string
  price: number
  status: string
  date: string
}

interface TgFav {
  name: string
  price: number
  kind: string
}

interface TgMonth {
  m: string
  pay: number
}

interface TgTab {
  icon: string
  label: string
}

const TG_DECKS: TgDeck[] = [
  { name: '赤焰速攻 RUSH', faction: '红速', winrate: 62, price: 899, tier: 'T1', uses: 3210 },
  { name: '深渊控场 CONTROL', faction: '蓝控', winrate: 58, price: 1299, tier: 'T1', uses: 2754 },
  { name: '翠绿铺场 TOKEN', faction: '绿铺场', winrate: 55, price: 759, tier: 'T2', uses: 2418 },
  { name: '暗夜破坏 DISRUPT', faction: '黑破坏', winrate: 60, price: 1080, tier: 'T1', uses: 1962 },
  { name: '圣光回复 HEAL', faction: '白回复', winrate: 52, price: 659, tier: 'T2', uses: 1735 },
  { name: '双色风暴 STORM', faction: '蓝控', winrate: 64, price: 1599, tier: 'T0', uses: 1287 },
  { name: '龙裔觉醒 DRAGON', faction: '红速', winrate: 57, price: 1180, tier: 'T2', uses: 1524 },
  { name: '机械军团 MECHA', faction: '黑破坏', winrate: 56, price: 940, tier: 'T2', uses: 1408 },
  { name: '精灵游击 ELVEN', faction: '绿铺场', winrate: 53, price: 720, tier: 'T3', uses: 1169 },
  { name: '天启审判 JUDGE', faction: '白回复', winrate: 59, price: 1350, tier: 'T1', uses: 986 }
]

const TG_CARDS: TgCard[] = [
  { name: '深渊主宰·夜临', rarity: 'UR', atk: 3200, hp: 2800, set: '第三弹·暗夜', price: 388, stock: 3 },
  { name: '赤焰龙皇·焚天', rarity: 'UR', atk: 3000, hp: 2500, set: '第二弹·龙裔', price: 299, stock: 5 },
  { name: '时之贤者·墨临', rarity: 'SSR', atk: 1800, hp: 2200, set: '第一弹·起源', price: 168, stock: 8 },
  { name: '机械暴君·零式', rarity: 'SSR', atk: 2400, hp: 2000, set: '第三弹·暗夜', price: 158, stock: 12 },
  { name: '圣光神官·缇娅', rarity: 'SR', atk: 1200, hp: 1800, set: '第一弹·起源', price: 68, stock: 26 },
  { name: '翠绿守卫·根须', rarity: 'SR', atk: 1600, hp: 2400, set: '第二弹·龙裔', price: 72, stock: 31 },
  { name: '暗影刺客·影缝', rarity: 'SR', atk: 2000, hp: 1200, set: '第三弹·暗夜', price: 88, stock: 18 },
  { name: '起源新星·辉光', rarity: 'H', atk: 999, hp: 999, set: '限定·周年', price: 666, stock: 2 }
]

const TG_MATS: TgMat[] = [
  { name: '暗夜星辰垫 XL', size: '60×35cm', price: 189, stock: 42, art: '夜空星轨' },
  { name: '鎏金纹章垫 L', size: '56×32cm', price: 159, stock: 35, art: '烫金纹样' },
  { name: '深渊之眼垫 XL', size: '60×35cm', price: 199, stock: 28, art: '深渊凝视' },
  { name: '便携折叠垫 M', size: '48×28cm', price: 129, stock: 55, art: '极简线条' },
  { name: '龙裔浮雕垫 L', size: '56×32cm', price: 179, stock: 22, art: '立体压纹' },
  { name: '周年限定垫 XXL', size: '70×40cm', price: 259, stock: 9, art: '周年纪念' }
]

const TG_SLEEVES: TgSleeve[] = [
  { name: '曜黑哑光套', colorHex: '#1D1A22', price: 45, count: 100, finish: '哑光' },
  { name: '鎏金闪面套', colorHex: '#C9A227', price: 59, count: 100, finish: '亮面' },
  { name: '樱粉透明套', colorHex: '#F0A8C8', price: 49, count: 100, finish: '透明' },
  { name: '深紫磨砂套', colorHex: '#5D3F7A', price: 52, count: 100, finish: '磨砂' },
  { name: '暗红压纹套', colorHex: '#8A2B2B', price: 55, count: 100, finish: '压纹' },
  { name: '青蓝冰面套', colorHex: '#2E6E8A', price: 53, count: 100, finish: '亮面' }
]

const TG_ORDERS: TgOrder[] = [
  { id: 'AV88264001', name: '深渊主宰·夜临 UR', price: 388, status: '已鉴定', date: '08-24' },
  { id: 'AV88264002', name: '暗夜星辰垫 XL', price: 189, status: '已发货', date: '08-18' },
  { id: 'AV88264003', name: '曜黑哑光套 ×3', price: 135, status: '待收货', date: '08-12' },
  { id: 'AV88264004', name: '双色风暴 STORM 卡组', price: 1599, status: '已完成', date: '07-30' },
  { id: 'AV88264005', name: '起源新星·辉光 H', price: 666, status: '已完成', date: '07-21' },
  { id: 'AV88264006', name: '龙裔浮雕垫 L', price: 179, status: '售后中', date: '07-09' }
]

const TG_FAVS: TgFav[] = [
  { name: '起源新星·辉光 H', price: 666, kind: '单卡' },
  { name: '周年限定垫 XXL', price: 259, kind: '牌垫' },
  { name: '鎏金闪面套', price: 59, kind: '卡套' },
  { name: '时之贤者·墨临 SSR', price: 168, kind: '单卡' },
  { name: '天启审判 JUDGE 卡组', price: 1350, kind: '卡组' },
  { name: '暗红压纹套', price: 55, kind: '卡套' }
]

const TG_MONTHS: TgMonth[] = [
  { m: '3月', pay: 680 },
  { m: '4月', pay: 290 },
  { m: '5月', pay: 1120 },
  { m: '6月', pay: 540 },
  { m: '7月', pay: 860 },
  { m: '8月', pay: 1650 }
]

const TG_TABS: TgTab[] = [
  { icon: '🃏', label: '首页' },
  { icon: '🎴', label: '卡组' },
  { icon: '✨', label: '单卡' },
  { icon: '🟪', label: '牌垫' },
  { icon: '🛡', label: '卡套' },
  { icon: '👤', label: '我的' }
]

function tgPrice(p: number): string {
  return '¥' + p.toLocaleString()
}

function tgBarW(v: number, max: number): string {
  return Math.min(100, Math.round(v / max * 100)) + '%'
}

function tgVBarH(v: number, max: number): number {
  return Math.max(8, Math.round(v / max * 92))
}

function tgGlow(w: number): number {
  return 0.45 + Math.abs(Math.sin(w / 8)) * 0.55
}

function tgShineO(w: number): number {
  return 0.4 + Math.abs(Math.sin(w / 12)) * 0.5
}

function tgFloatY(w: number, i: number): number {
  return Math.sin((w + i * 9) / 8) * 4
}

function tgBlink(w: number): number {
  return w % 16 < 8 ? 1 : 0.35
}

function tgSpin(w: number, i: number): number {
  return (w * 5 + i * 40) % 360
}

function tgRarityColor(r: string): string {
  if (r === 'UR') { return TG.primary }
  if (r === 'SSR') { return TG.accent }
  if (r === 'H') { return '#E86A4A' }
  return '#7A9BC2'
}

function tgFactionColor(f: string): string {
  if (f === '红速') { return '#D9534F' }
  if (f === '蓝控') { return '#4A90C2' }
  if (f === '绿铺场') { return '#5B9C6E' }
  if (f === '黑破坏') { return '#8A6FC2' }
  return '#E8D9A0'
}

function tgTierColor(t: string): string {
  if (t === 'T0') { return TG.primary }
  if (t === 'T1') { return TG.accent }
  return TG.sub
}

function tgStatusColor(s: string): string {
  if (s === '已发货' || s === '已鉴定') { return TG.primary }
  if (s === '待收货') { return TG.accent }
  if (s === '售后中') { return '#E86A4A' }
  return '#5B9C6E'
}

function tgDeckFilter(arr: TgDeck[], t: string): TgDeck[] {
  let r: TgDeck[] = []
  for (let i = 0; i < arr.length; i++) {
    if (t === '全部' || arr[i].faction === t) {
      r.push(arr[i])
    }
  }
  return r
}

function tgCardFilter(arr: TgCard[], t: string): TgCard[] {
  let r: TgCard[] = []
  for (let i = 0; i < arr.length; i++) {
    if (t === '全部' || arr[i].rarity === t) {
      r.push(arr[i])
    }
  }
  return r
}

function tgDeckTop(): TgDeck[] {
  let r: TgDeck[] = TG_DECKS.slice()
  r.sort((a: TgDeck, b: TgDeck) => b.uses - a.uses)
  return r.slice(0, 6)
}

function tgMaxPrice(): number {
  let r: number = 0
  for (let i = 0; i < TG_CARDS.length; i++) {
    if (TG_CARDS[i].price > r) {
      r = TG_CARDS[i].price
    }
  }
  return r
}

function tgTotalPay(): number {
  let r: number = 0
  for (let i = 0; i < TG_MONTHS.length; i++) {
    r = r + TG_MONTHS[i].pay
  }
  return r
}

function tgMaxMonth(): number {
  let r: number = 0
  for (let i = 0; i < TG_MONTHS.length; i++) {
    if (TG_MONTHS[i].pay > r) {
      r = TG_MONTHS[i].pay
    }
  }
  return r
}

@Entry
@Component
struct TgApp {
  @State tabIdx: number = 0

  build() {
    Stack() {
      Column() {
        Column() {
          Row() {
            Column() {
              Text('ARCANA VAULT')
                .fontSize(21)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.primary)
                .letterSpacing(2)
              Text('集换卡牌对战馆 · 鉴定 & 对战')
                .fontSize(10)
                .fontColor(TG.sub)
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start)

            Column() {
              Text('🏆')
                .fontSize(20)
              Text('大赛')
                .fontSize(9)
                .fontColor(TG.sub)
            }
            .onClick(() => {
              this.tabIdx = 0
            })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .padding({ left: 16, right: 16, top: 12, bottom: 10 })

          Row() {
            Text('🔍')
              .fontSize(14)
              .margin({ left: 12 })
            Text('搜索单卡 / 卡组 / 补给')
              .fontSize(12)
              .fontColor('#7A6C90')
              .margin({ left: 8 })
            Column()
              .layoutWeight(1)
            Text('今晚 8 点 新弹预购')
              .fontSize(10)
              .fontColor(TG.dark)
              .backgroundColor(TG.primary)
              .borderRadius(10)
              .padding({ left: 8, right: 8, top: 3, bottom: 3 })
              .margin({ right: 10 })
          }
          .width('100%')
          .height(36)
          .backgroundColor(TG.dark)
          .borderRadius(18)
          .margin({ left: 16, right: 16, bottom: 10 })
        }
        .width('100%')
        .backgroundColor(TG.dark)

        Column() {
          if (this.tabIdx === 0) {
            TgHomeTab()
          } else if (this.tabIdx === 1) {
            TgDeckTab()
          } else if (this.tabIdx === 2) {
            TgCardTab()
          } else if (this.tabIdx === 3) {
            TgMatTab()
          } else if (this.tabIdx === 4) {
            TgSleeveTab()
          } else {
            TgMineTab()
          }
        }
        .layoutWeight(1)
        .width('100%')

        Row() {
          ForEach(TG_TABS, (t: TgTab, i: number) => {
            Column() {
              Text(t.icon)
                .fontSize(18)
                .opacity(this.tabIdx === i ? 1 : 0.55)
                .scale(this.tabIdx === i ? { x: 1.15, y: 1.15 } : { x: 1, y: 1 })
              Text(t.label)
                .fontSize(10)
                .fontColor(this.tabIdx === i ? TG.primary : TG.sub)
                .fontWeight(this.tabIdx === i ? FontWeight.Bold : FontWeight.Normal)
                .margin({ top: 2 })
            }
            .layoutWeight(1)
            .padding({ top: 6, bottom: 6 })
            .onClick(() => {
              this.tabIdx = i
            })
          })
        }
        .width('100%')
        .backgroundColor(TG.dark)
        .border({ width: { top: 1 }, color: TG.line })
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor(TG.bg)
  }
}

@Component
struct TgHomeTab {
  @State wave: number = 0
  @State timer: number = -1
  @State showEvent: boolean = false
  @State showSign: boolean = false
  @State showPre: boolean = false
  @State showTrade: boolean = false
  @State signDays: number = 12

  aboutToAppear(): void {
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }

  @Builder modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(10,6,16,0.72)')
      .position({ x: 0, y: 0 })
      .zIndex(999)
      .onClick(() => {
        this.showEvent = false
        this.showSign = false
        this.showPre = false
        this.showTrade = false
      })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Stack() {
            Column()
              .width('100%')
              .height(165)
              .backgroundColor(TG.dark)
              .borderRadius(18)
              .border({ width: 1, color: TG.line })

            Column() {
              Text('城市巡回赛 · 站点赛报名')
                .fontSize(17)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.text)
              Text('9 月 15 日 · 128 人 · 标准构筑 · 冠军得周年 H 卡')
                .fontSize(11)
                .fontColor(TG.sub)
                .margin({ top: 6 })
              Row() {
                Text('立即报名')
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(TG.dark)
                  .backgroundColor(TG.primary)
                  .borderRadius(14)
                  .padding({ left: 14, right: 14, top: 6, bottom: 6 })
              }
              .margin({ top: 10 })
              .onClick(() => {
                this.showEvent = true
              })
            }
            .alignItems(HorizontalAlign.Start)
            .padding(16)

            Column() {
              Text('✨')
                .fontSize(26)
                .opacity(tgGlow(this.wave))
                .rotate({ angle: tgSpin(this.wave, 0) * 0.2 })
            }
            .position({ x: 30, y: 26 })
          }
          .width('100%')
          .margin({ left: 12, right: 12, top: 12 })

          Row() {
            ForEach([['每日签到', '📅'], ['新弹预购', '🆕'], ['卡牌置换', '🔁'], ['鉴定服务', '🔍']], (g: string[], i: number) => {
              Column() {
                Column() {
                  Text(g[1])
                    .fontSize(21)
                }
                .width(50)
                .height(50)
                .borderRadius(25)
                .backgroundColor(TG.card)
                .border({ width: 1, color: TG.line })
                .justifyContent(FlexAlign.Center)
                .margin({ bottom: 6 })
                .translate({ y: tgFloatY(this.wave, i) })

                Text(g[0])
                  .fontSize(11)
                  .fontColor(TG.text)
              }
              .layoutWeight(1)
              .onClick(() => {
                if (i === 0) {
                  this.showSign = true
                } else if (i === 1) {
                  this.showPre = true
                } else if (i === 2) {
                  this.showTrade = true
                }
              })
            })
          }
          .width('100%')
          .backgroundColor(TG.card)
          .borderRadius(16)
          .border({ width: 1, color: TG.line })
          .padding({ top: 14, bottom: 14 })
          .margin({ left: 12, right: 12, top: 10 })

          Row() {
            Column() {
              Text('1,842')
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.primary)
              Text('收藏卡片')
                .fontSize(10)
                .fontColor(TG.sub)
                .margin({ top: 2 })
            }
            .layoutWeight(1)

            Column() {
              Text('36')
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.accent)
              Text('完成卡组')
                .fontSize(10)
                .fontColor(TG.sub)
                .margin({ top: 2 })
            }
            .layoutWeight(1)

            Column() {
              Text('T0'
              )
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor('#E86A4A')
              Text('主力梯队')
                .fontSize(10)
                .fontColor(TG.sub)
                .margin({ top: 2 })
            }
            .layoutWeight(1)

            Column() {
              Text('72%'
              )
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor('#5B9C6E')
              Text('对战胜率')
                .fontSize(10)
                .fontColor(TG.sub)
                .margin({ top: 2 })
            }
            .layoutWeight(1)
          }
          .width('100%')
          .backgroundColor(TG.card)
          .borderRadius(16)
          .border({ width: 1, color: TG.line })
          .padding({ top: 14, bottom: 14 })
          .margin({ left: 12, right: 12, top: 10 })

          Row() {
            Text('卡组热度榜')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Column()
              .layoutWeight(1)
            Text('按使用人次')
              .fontSize(10)
              .fontColor(TG.sub)
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 16, bottom: 8 })

          Column() {
            ForEach(tgDeckTop(), (d: TgDeck, i: number) => {
              Row() {
                Text(String(i + 1))
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(i === 0 ? TG.dark : TG.text)
                  .width(24)
                  .height(24)
                  .borderRadius(12)
                  .backgroundColor(i === 0 ? TG.primary : TG.card)
                  .textAlign(TextAlign.Center)

                Column() {
                  Text(d.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Medium)
                    .fontColor(TG.text)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                  Row() {
                    Text(d.faction)
                      .fontSize(8)
                      .fontColor('#FFFFFF')
                      .backgroundColor(tgFactionColor(d.faction))
                      .borderRadius(6)
                      .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                    Text(d.tier)
                      .fontSize(8)
                      .fontColor(tgTierColor(d.tier))
                      .margin({ left: 6 })
                    Text('胜率 ' + d.winrate + '%')
                      .fontSize(9)
                      .fontColor(TG.sub)
                      .margin({ left: 6 })
                  }
                  .margin({ top: 3 })
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                .margin({ left: 8 })

                Stack({ alignContent: Alignment.Start }) {
                  Row()
                    .width('100%')
                    .height(8)
                    .borderRadius(4)
                    .backgroundColor(TG.dark)
                  Row()
                    .width(tgBarW(d.uses, 3210))
                    .height(8)
                    .borderRadius(4)
                    .backgroundColor(tgFactionColor(d.faction))
                }
                .width(88)
              }
              .width('100%')
              .padding({ top: 8, bottom: 8 })
              .border({ width: { bottom: 1 }, color: TG.line })
            })
          }
          .width('100%')
          .backgroundColor(TG.card)
          .borderRadius(16)
          .border({ width: 1, color: TG.line })
          .padding({ left: 14, right: 14, top: 4, bottom: 6 })
          .margin({ left: 12, right: 12 })

          Row() {
            Text('稀有单卡行情')
              .fontSize(15)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Column()
              .layoutWeight(1)
            Text('实时')
              .fontSize(10)
              .fontColor(TG.primary)
              .opacity(tgBlink(this.wave))
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 16, bottom: 8 })

          Column() {
            ForEach(TG_CARDS.slice(0, 6), (c: TgCard, i: number) => {
              Row() {
                Text(c.rarity)
                  .fontSize(10)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(TG.dark)
                  .backgroundColor(tgRarityColor(c.rarity))
                  .borderRadius(6)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                  .width(38)
                  .textAlign(TextAlign.Center)

                Column() {
                  Text(c.name)
                    .fontSize(12)
                    .fontColor(TG.text)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                  Text(c.set + ' · 库存 ' + c.stock)
                    .fontSize(9)
                    .fontColor(TG.sub)
                    .margin({ top: 2 })
                }
                .layoutWeight(1)
                .alignItems(HorizontalAlign.Start)
                .margin({ left: 8 })

                Text(tgPrice(c.price))
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(tgRarityColor(c.rarity))
              }
              .width('100%')
              .padding({ top: 8, bottom: 8 })
              .border({ width: { bottom: 1 }, color: TG.line })
            })
          }
          .width('100%')
          .backgroundColor(TG.card)
          .borderRadius(16)
          .border({ width: 1, color: TG.line })
          .padding({ left: 14, right: 14, top: 4, bottom: 6 })
          .margin({ left: 12, right: 12, bottom: 16 })
        }
        .width('100%')
      }
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .height('100%')

      if (this.showEvent) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('城市巡回赛报名')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Text('9 月 15 日 · 128 人 · 标准构筑赛制')
              .fontSize(11)
              .fontColor(TG.sub)
              .margin({ top: 6 })

            Row() {
              Text('报名费')
                .fontSize(11)
                .fontColor(TG.sub)
                .width(60)
              Text('¥120(含 6 包新弹)')
                .fontSize(12)
                .fontColor(TG.primary)
                .fontWeight(FontWeight.Bold)
            }
            .width('100%')
            .margin({ top: 14 })

            Row() {
              Text('奖品')
                .fontSize(11)
                .fontColor(TG.sub)
                .width(60)
              Text('冠军 周年 H 卡 + 奖金 ¥3000')
                .fontSize(12)
                .fontColor(TG.text)
            }
            .width('100%')
            .margin({ top: 8 })

            Row() {
              Text('地点')
                .fontSize(11)
                .fontColor(TG.sub)
                .width(60)
              Text('上海市静安区 ARCANA 主场')
                .fontSize(12)
                .fontColor(TG.text)
            }
            .width('100%')
            .margin({ top: 8 })

            Row() {
              Text('取消')
                .fontSize(14)
                .fontColor(TG.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .onClick(() => {
                  this.showEvent = false
                })
              Text('确认报名')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.dark)
                .backgroundColor(TG.primary)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .onClick(() => {
                  this.showEvent = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .constraintSize({ maxHeight: '80%' })
          .backgroundColor(TG.card)
          .borderRadius(18)
          .padding(18)
          .zIndex(1000)
        }
      }

      if (this.showSign) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('📅 每日签到')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Text('已连续签到 ' + this.signDays + ' 天')
              .fontSize(11)
              .fontColor(TG.sub)
              .margin({ top: 6 })

            ForEach([['第 13 天', '稀有卡包 ×1'], ['第 20 天', '卡套 ×1 盒'], ['第 30 天', 'SSR 随机 ×1']], (r: string[], i: number) => {
              Row() {
                Text(r[0])
                  .fontSize(13)
                  .fontColor(TG.text)
                  .layoutWeight(1)
                Text(r[1])
                  .fontSize(12)
                  .fontColor(tgRarityColor(i === 2 ? 'SSR' : 'SR'))
              }
              .width('100%')
              .padding({ top: 12, bottom: 12 })
              .border({ width: { bottom: 1 }, color: TG.line })
            })

            Text('签到')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.dark)
              .backgroundColor(TG.primary)
              .borderRadius(12)
              .padding({ left: 24, right: 24, top: 10, bottom: 10 })
              .margin({ top: 14 })
              .onClick(() => {
                this.signDays = this.signDays + 1
                this.showSign = false
              })
          }
          .width('86%')
          .constraintSize({ maxHeight: '80%' })
          .backgroundColor(TG.card)
          .borderRadius(18)
          .padding(18)
          .zIndex(1000)
        }
      }

      if (this.showPre) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('🆕 第四弹预购')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Text('今晚 8 点开抢 · 限定闪卡附赠')
              .fontSize(11)
              .fontColor(TG.sub)
              .margin({ top: 6 })

            ForEach([['整盒(36 包)', '¥432'], ['补充包 ×5', '¥75'], ['收藏礼盒', '¥699']], (r: string[], i: number) => {
              Row() {
                Text(r[0])
                  .fontSize(13)
                  .fontColor(TG.text)
                  .layoutWeight(1)
                Text(r[1])
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(TG.primary)
                Text('订')
                  .fontSize(11)
                  .fontColor(TG.dark)
                  .backgroundColor(TG.primary)
                  .borderRadius(10)
                  .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                  .margin({ left: 8 })
                  .opacity(tgBlink(this.wave + i * 5))
                  .onClick(() => {
                    this.showPre = false
                  })
              }
              .width('100%')
              .padding({ top: 12, bottom: 12 })
              .border({ width: { bottom: 1 }, color: TG.line })
            })

            Text('预购享 95 折 · 尾款可退')
              .fontSize(10)
              .fontColor(TG.sub)
              .margin({ top: 10 })
          }
          .width('86%')
          .constraintSize({ maxHeight: '80%' })
          .backgroundColor(TG.card)
          .borderRadius(18)
          .padding(18)
          .zIndex(1000)
        }
      }

      if (this.showTrade) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('🔁 卡牌置换')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Text('重复卡按 60% 估值兑换积分')
              .fontSize(11)
              .fontColor(TG.sub)
              .margin({ top: 6 })

            Row() {
              Text('可置换卡:23 张')
                .fontSize(13)
                .fontColor(TG.text)
                .layoutWeight(1)
              Text('估值 ¥1,146')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.primary)
            }
            .width('100%')
            .margin({ top: 14 })

            Row() {
              Text('置换比例')
                .fontSize(11)
                .fontColor(TG.sub)
                .width(64)
              Stack({ alignContent: Alignment.Start }) {
                Row()
                  .width('100%')
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor(TG.dark)
                Row()
                  .width('60%')
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor(TG.accent)
              }
              .layoutWeight(1)
              Text('60%')
                .fontSize(11)
                .fontColor(TG.text)
                .width(36)
                .textAlign(TextAlign.End)
            }
            .width('100%')
            .margin({ top: 12 })

            Text('一键置换')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.dark)
              .backgroundColor(TG.primary)
              .borderRadius(12)
              .padding({ left: 24, right: 24, top: 10, bottom: 10 })
              .margin({ top: 16 })
              .onClick(() => {
                this.showTrade = false
              })
          }
          .width('86%')
          .constraintSize({ maxHeight: '80%' })
          .backgroundColor(TG.card)
          .borderRadius(18)
          .padding(18)
          .zIndex(1000)
        }
      }
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct TgDeckTab {
  @State wave: number = 0
  @State timer: number = -1
  @State chip: string = '全部'
  @State decks: TgDeck[] = []
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showEdit: boolean = false
  @State showDel: boolean = false
  @State selIdx: number = 0
  @State editIdx: number = 0
  @State delIdx: number = 0
  @State nName: string = ''
  @State nFaction: string = '红速'
  @State nPrice: string = ''
  @State eName: string = ''
  @State eWin: string = ''

  aboutToAppear(): void {
    this.decks = TG_DECKS.slice()
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }

  @Builder modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(10,6,16,0.72)')
      .position({ x: 0, y: 0 })
      .zIndex(999)
      .onClick(() => {
        this.showDetail = false
        this.showAdd = false
        this.showEdit = false
        this.showDel = false
      })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Scroll() {
            Row({ space: 8 }) {
              ForEach(['全部', '红速', '蓝控', '绿铺场', '黑破坏', '白回复'], (t: string, i: number) => {
                Text(t)
                  .fontSize(12)
                  .fontColor(this.chip === t ? TG.dark : TG.text)
                  .backgroundColor(this.chip === t ? tgFactionColor(t) : TG.card)
                  .borderRadius(16)
                  .padding({ left: 14, right: 14, top: 7, bottom: 7 })
                  .onClick(() => {
                    this.chip = t
                    this.decks = tgDeckFilter(TG_DECKS, t)
                  })
              })
            }
          }
          .scrollable(ScrollDirection.Horizontal)
          .width('100%')
          .margin({ left: 12, right: 12, top: 12 })

          Row() {
            Text('共 ' + this.decks.length + ' 套卡组')
              .fontSize(11)
              .fontColor(TG.sub)
            Column()
              .layoutWeight(1)
            Text('+ 上架卡组')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.primary)
              .onClick(() => {
                this.showAdd = true
              })
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 12, bottom: 4 })

          ForEach(this.decks, (d: TgDeck, i: number) => {
            Column() {
              Row() {
                Column() {
                  Text(d.tier)
                    .fontSize(16)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(tgTierColor(d.tier))
                  Text('梯队')
                    .fontSize(8)
                    .fontColor(TG.sub)
                    .margin({ top: 2 })
                }
                .width(52)
                .height(52)
                .borderRadius(12)
                .backgroundColor(TG.dark)
                .border({ width: 1, color: tgFactionColor(d.faction) })
                .justifyContent(FlexAlign.Center)

                Column() {
                  Text(d.name)
                    .fontSize(14)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(TG.text)
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                  Row() {
                    Text(d.faction)
                      .fontSize(9)
                      .fontColor('#FFFFFF')
                      .backgroundColor(tgFactionColor(d.faction))
                      .borderRadius(6)
                      .padding({ left: 5, right: 5, top: 1, bottom: 1 })
                    Text('使用 ' + d.uses + ' 人次')
                      .fontSize(9)
                      .fontColor(TG.sub)
                      .margin({ left: 8 })
                  }
                  .margin({ top: 5 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 12 })

                Column() {
                  Text(tgPrice(d.price))
                    .fontSize(15)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(TG.primary)
                  Row() {
                    Text('详情')
                      .fontSize(9)
                      .fontColor(TG.sub)
                      .onClick(() => {
                        this.selIdx = i
                        this.showDetail = true
                      })
                    Text('编辑')
                      .fontSize(9)
                      .fontColor(TG.accent)
                      .margin({ left: 8 })
                      .onClick(() => {
                        this.editIdx = i
                        this.eName = d.name
                        this.eWin = String(d.winrate)
                        this.showEdit = true
                      })
                  }
                  .margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.End)
              }
              .width('100%')

              Row() {
                Text('胜率 ' + d.winrate + '%')
                  .fontSize(9)
                  .fontColor(TG.sub)
                  .width(64)
                Stack({ alignContent: Alignment.Start }) {
                  Row()
                    .width('100%')
                    .height(8)
                    .borderRadius(4)
                    .backgroundColor(TG.dark)
                  Row()
                    .width(tgBarW(d.winrate, 100))
                    .height(8)
                    .borderRadius(4)
                    .backgroundColor(tgFactionColor(d.faction))
                    .opacity(0.5 + tgBlink(this.wave + i * 4) * 0.4)
                }
                .layoutWeight(1)
                Text(d.winrate >= 58 ? '强势' : '常规')
                  .fontSize(9)
                  .fontColor(d.winrate >= 58 ? TG.primary : TG.sub)
                  .width(36)
                  .textAlign(TextAlign.End)
              }
              .width('100%')
              .margin({ top: 8 })
            }
            .width('100%')
            .backgroundColor(TG.card)
            .borderRadius(16)
            .border({ width: 1, color: TG.line })
            .padding(12)
            .margin({ left: 12, right: 12, top: 10 })
          })

          Row() {
            Text('使用人次柱状图')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Column()
              .layoutWeight(1)
            Text('Top 6')
              .fontSize(10)
              .fontColor(TG.sub)
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 16, bottom: 8 })

          Row({ space: 10 }) {
            ForEach(tgDeckTop(), (d: TgDeck, i: number) => {
              Column() {
                Text(String(d.uses))
                  .fontSize(8)
                  .fontColor(TG.sub)
                Column()
                  .width(26)
                  .height(tgVBarH(d.uses, 3210))
                  .borderRadius({ topLeft: 6, topRight: 6 })
                  .backgroundColor(tgFactionColor(d.faction))
                  .opacity(0.55 + tgBlink(this.wave + i * 3) * 0.35)
                Text(d.tier)
                  .fontSize(8)
                  .fontColor(tgTierColor(d.tier))
                  .margin({ top: 4 })
              }
              .justifyContent(FlexAlign.End)
            })
          }
          .width('100%')
          .alignItems(VerticalAlign.Bottom)
          .padding({ left: 16, right: 16, bottom: 16 })
          .height(140)
        }
        .width('100%')
      }
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .height('100%')

      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text(this.decks[this.selIdx].name)
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Row() {
              Text(this.decks[this.selIdx].faction)
                .fontSize(10)
                .fontColor('#FFFFFF')
                .backgroundColor(tgFactionColor(this.decks[this.selIdx].faction))
                .borderRadius(6)
                .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              Text(this.decks[this.selIdx].tier + ' 梯队')
                .fontSize(10)
                .fontColor(tgTierColor(this.decks[this.selIdx].tier))
                .margin({ left: 10 })
            }
            .margin({ top: 8 })

            Row() {
              Text(tgPrice(this.decks[this.selIdx].price))
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.primary)
              Column()
                .layoutWeight(1)
              Text('使用 ' + this.decks[this.selIdx].uses)
                .fontSize(11)
                .fontColor(TG.sub)
            }
            .width('100%')
            .margin({ top: 10 })

            Row() {
              Text('胜率')
                .fontSize(11)
                .fontColor(TG.sub)
                .width(48)
              Stack({ alignContent: Alignment.Start }) {
                Row()
                  .width('100%')
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor(TG.dark)
                Row()
                  .width(tgBarW(this.decks[this.selIdx].winrate, 100))
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor(TG.primary)
              }
              .layoutWeight(1)
              Text(this.decks[this.selIdx].winrate + '%')
                .fontSize(11)
                .fontColor(TG.text)
                .width(36)
                .textAlign(TextAlign.End)
            }
            .width('100%')
            .margin({ top: 14 })

            Text('含 40 张主卡 + 12 张备牌 · 附赠卡盒与分隔页')
              .fontSize(10)
              .fontColor(TG.sub)
              .margin({ top: 12 })

            Row() {
              Text('删除')
                .fontSize(13)
                .fontColor('#E86A4A')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .onClick(() => {
                  this.delIdx = this.selIdx
                  this.showDel = true
                })
              Text('关闭')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.dark)
                .backgroundColor(TG.primary)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .onClick(() => {
                  this.showDetail = false
                })
            }
            .width('100%')
            .margin({ top: 16 })
          }
          .width('86%')
          .constraintSize({ maxHeight: '80%' })
          .backgroundColor(TG.card)
          .borderRadius(18)
          .padding(18)
          .zIndex(1000)
        }
      }

      if (this.showAdd) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('上架卡组')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)

            Column() {
              Text('名称')
                .fontSize(11)
                .fontColor(TG.sub)
              TextInput({ placeholder: '如 星辉连锁 BURN' })
                .fontSize(13)
                .height(40)
                .borderRadius(10)
                .backgroundColor(TG.dark)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nName = v
                })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 14 })

            Column() {
              Text('流派')
                .fontSize(11)
                .fontColor(TG.sub)
              Row({ space: 6 }) {
                ForEach(['红速', '蓝控', '绿铺场', '黑破坏', '白回复'], (t: string, i: number) => {
                  Text(t)
                    .fontSize(10)
                    .fontColor(this.nFaction === t ? '#FFFFFF' : TG.text)
                    .backgroundColor(this.nFaction === t ? tgFactionColor(t) : TG.dark)
                    .borderRadius(10)
                    .padding({ left: 8, right: 8, top: 5, bottom: 5 })
                    .onClick(() => {
                      this.nFaction = t
                    })
                })
              }
              .margin({ top: 6 })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 12 })

            Column() {
              Text('价格(¥)')
                .fontSize(11)
                .fontColor(TG.sub)
              TextInput({ placeholder: '如 899' })
                .fontSize(13)
                .height(40)
                .borderRadius(10)
                .backgroundColor(TG.dark)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nPrice = v
                })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 12 })

            Row() {
              Text('取消')
                .fontSize(14)
                .fontColor(TG.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .onClick(() => {
                  this.showAdd = false
                })
              Text('确认上架')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.dark)
                .backgroundColor(TG.primary)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .onClick(() => {
                  let it: TgDeck = {
                    name: this.nName === '' ? '自定义卡组' : this.nName,
                    faction: this.nFaction,
                    winrate: 50,
                    price: isNaN(parseInt(this.nPrice)) ? 799 : parseInt(this.nPrice),
                    tier: 'T3',
                    uses: 0
                  }
                  this.decks.push(it)
                  this.showAdd = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .constraintSize({ maxHeight: '80%' })
          .backgroundColor(TG.card)
          .borderRadius(18)
          .padding(18)
          .zIndex(1000)
        }
      }

      if (this.showEdit) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('编辑卡组')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Text('正在编辑:' + this.decks[this.editIdx].name)
              .fontSize(11)
              .fontColor(TG.sub)
              .margin({ top: 6 })

            Column() {
              Text('新名称')
                .fontSize(11)
                .fontColor(TG.sub)
              TextInput({ text: this.eName })
                .fontSize(13)
                .height(40)
                .borderRadius(10)
                .backgroundColor(TG.dark)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.eName = v
                })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 14 })

            Column() {
              Text('新胜率(%)')
                .fontSize(11)
                .fontColor(TG.sub)
              TextInput({ text: this.eWin })
                .fontSize(13)
                .height(40)
                .borderRadius(10)
                .backgroundColor(TG.dark)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.eWin = v
                })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 12 })

            Row() {
              Text('取消')
                .fontSize(14)
                .fontColor(TG.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .onClick(() => {
                  this.showEdit = false
                })
              Text('保存修改')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.dark)
                .backgroundColor(TG.primary)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .onClick(() => {
                  let it: TgDeck = {
                    name: this.eName === '' ? this.decks[this.editIdx].name : this.eName,
                    faction: this.decks[this.editIdx].faction,
                    winrate: isNaN(parseInt(this.eWin)) ? this.decks[this.editIdx].winrate : Math.min(100, Math.max(0, parseInt(this.eWin))),
                    price: this.decks[this.editIdx].price,
                    tier: this.decks[this.editIdx].tier,
                    uses: this.decks[this.editIdx].uses
                  }
                  this.decks[this.editIdx] = it
                  this.showEdit = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .constraintSize({ maxHeight: '80%' })
          .backgroundColor(TG.card)
          .borderRadius(18)
          .padding(18)
          .zIndex(1000)
        }
      }

      if (this.showDel) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('⚠ 删除卡组')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E86A4A')
            Text('确定要删除「' + this.decks[this.delIdx].name + '」吗?')
              .fontSize(12)
              .fontColor(TG.text)
              .margin({ top: 12 })
            Text('删除后从卡组库移除,不可撤销')
              .fontSize(10)
              .fontColor(TG.sub)
              .margin({ top: 6 })

            Row() {
              Text('再想想')
                .fontSize(14)
                .fontColor(TG.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .onClick(() => {
                  this.showDel = false
                })
              Text('确认删除')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
                .backgroundColor('#E86A4A')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .onClick(() => {
                  this.decks.splice(this.delIdx, 1)
                  this.showDel = false
                  this.showDetail = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('80%')
          .constraintSize({ maxHeight: '70%' })
          .backgroundColor(TG.card)
          .borderRadius(18)
          .padding(18)
          .zIndex(1000)
        }
      }
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct TgCardTab {
  @State wave: number = 0
  @State timer: number = -1
  @State chip: string = '全部'
  @State cards: TgCard[] = []
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showDel: boolean = false
  @State selIdx: number = 0
  @State delIdx: number = 0
  @State nName: string = ''
  @State nRarity: string = 'SR'
  @State nPrice: string = ''

  aboutToAppear(): void {
    this.cards = TG_CARDS.slice()
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }

  @Builder modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(10,6,16,0.72)')
      .position({ x: 0, y: 0 })
      .zIndex(999)
      .onClick(() => {
        this.showDetail = false
        this.showAdd = false
        this.showDel = false
      })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Scroll() {
            Row({ space: 8 }) {
              ForEach(['全部', 'UR', 'SSR', 'SR', 'H'], (t: string, i: number) => {
                Text(t)
                  .fontSize(12)
                  .fontColor(this.chip === t ? TG.dark : TG.text)
                  .backgroundColor(this.chip === t ? tgRarityColor(t) : TG.card)
                  .borderRadius(16)
                  .padding({ left: 14, right: 14, top: 7, bottom: 7 })
                  .onClick(() => {
                    this.chip = t
                    this.cards = tgCardFilter(TG_CARDS, t)
                  })
              })
            }
          }
          .scrollable(ScrollDirection.Horizontal)
          .width('100%')
          .margin({ left: 12, right: 12, top: 12 })

          Row() {
            Text('共 ' + this.cards.length + ' 张单卡')
              .fontSize(11)
              .fontColor(TG.sub)
            Column()
              .layoutWeight(1)
            Text('+ 收录单卡')
              .fontSize(12)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.primary)
              .onClick(() => {
                this.showAdd = true
              })
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 12, bottom: 4 })

          Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
            ForEach(this.cards, (c: TgCard, i: number) => {
              Column() {
                Column() {
                  Text(c.rarity)
                    .fontSize(18)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(tgRarityColor(c.rarity))
                    .opacity(tgGlow(this.wave + i * 6))
                  Text(c.name.slice(0, 2))
                    .fontSize(10)
                    .fontColor(TG.sub)
                    .margin({ top: 4 })
                }
                .width('100%')
                .height(84)
                .borderRadius(12)
                .backgroundColor(TG.dark)
                .border({ width: 1, color: tgRarityColor(c.rarity) })
                .justifyContent(FlexAlign.Center)
                .translate({ y: tgFloatY(this.wave, i) })

                Text(c.name)
                  .fontSize(11)
                  .fontWeight(FontWeight.Medium)
                  .fontColor(TG.text)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .margin({ top: 8 })

                Text('ATK ' + c.atk + ' / HP ' + c.hp)
                  .fontSize(9)
                  .fontColor(TG.sub)
                  .margin({ top: 3 })

                Text(tgPrice(c.price))
                  .fontSize(13)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(tgRarityColor(c.rarity))
                  .margin({ top: 3 })

                Text('库存 ' + c.stock)
                  .fontSize(8)
                  .fontColor(c.stock < 5 ? '#E86A4A' : TG.sub)
                  .margin({ top: 2 })

                Text('详情')
                  .fontSize(9)
                  .fontColor(TG.accent)
                  .margin({ top: 4 })
                  .onClick(() => {
                    this.selIdx = i
                    this.showDetail = true
                  })
              }
              .width('48.5%')
              .backgroundColor(TG.card)
              .borderRadius(14)
              .border({ width: 1, color: TG.line })
              .padding(10)
              .margin({ bottom: 10 })
            })
          }
          .width('100%')
          .padding({ left: 12, right: 12 })

          Row() {
            Text('单卡价格对比')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Column()
              .layoutWeight(1)
            Text('单位:¥')
              .fontSize(10)
              .fontColor(TG.sub)
          }
          .width('100%')
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })

          Column() {
            ForEach(TG_CARDS, (c: TgCard, i: number) => {
              Row() {
                Text(c.name.slice(0, 6))
                  .fontSize(10)
                  .fontColor(TG.sub)
                  .width(84)
                Stack({ alignContent: Alignment.Start }) {
                  Row()
                    .width('100%')
                    .height(9)
                    .borderRadius(5)
                    .backgroundColor(TG.dark)
                  Row()
                    .width(tgBarW(c.price, tgMaxPrice()))
                    .height(9)
                    .borderRadius(5)
                    .backgroundColor(tgRarityColor(c.rarity))
                    .opacity(0.5 + tgBlink(this.wave + i * 4) * 0.4)
                }
                .layoutWeight(1)
                Text(String(c.price))
                  .fontSize(10)
                  .fontColor(TG.text)
                  .width(36)
                  .textAlign(TextAlign.End)
              }
              .width('100%')
              .margin({ top: 9 })
            })
          }
          .width('100%')
          .backgroundColor(TG.card)
          .borderRadius(16)
          .border({ width: 1, color: TG.line })
          .padding(14)
          .margin({ left: 12, right: 12, top: 4, bottom: 16 })
        }
        .width('100%')
      }
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .height('100%')

      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text(this.cards[this.selIdx].name)
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Text(this.cards[this.selIdx].rarity + ' · ' + this.cards[this.selIdx].set)
              .fontSize(11)
              .fontColor(tgRarityColor(this.cards[this.selIdx].rarity))
              .margin({ top: 6 })

            Text(tgPrice(this.cards[this.selIdx].price))
              .fontSize(22)
              .fontWeight(FontWeight.Bold)
              .fontColor(tgRarityColor(this.cards[this.selIdx].rarity))
              .margin({ top: 10 })

            Row() {
              Column() {
                Text(String(this.cards[this.selIdx].atk))
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#E86A4A')
                Text('攻击 ATK')
                  .fontSize(9)
                  .fontColor(TG.sub)
                  .margin({ top: 2 })
              }
              .layoutWeight(1)

              Column() {
                Text(String(this.cards[this.selIdx].hp))
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#5B9C6E')
                Text('生命 HP')
                  .fontSize(9)
                  .fontColor(TG.sub)
                  .margin({ top: 2 })
              }
              .layoutWeight(1)

              Column() {
                Text(String(this.cards[this.selIdx].stock))
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(this.cards[this.selIdx].stock < 5 ? '#E86A4A' : TG.text)
                Text('库存')
                  .fontSize(9)
                  .fontColor(TG.sub)
                  .margin({ top: 2 })
              }
              .layoutWeight(1)
            }
            .width('100%')
            .margin({ top: 16 })

            Row() {
              Text('攻击')
                .fontSize(11)
                .fontColor(TG.sub)
                .width(48)
              Stack({ alignContent: Alignment.Start }) {
                Row()
                  .width('100%')
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor(TG.dark)
                Row()
                  .width(tgBarW(this.cards[this.selIdx].atk, 3200))
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor('#E86A4A')
              }
              .layoutWeight(1)
            }
            .width('100%')
            .margin({ top: 12 })

            Row() {
              Text('生命')
                .fontSize(11)
                .fontColor(TG.sub)
                .width(48)
              Stack({ alignContent: Alignment.Start }) {
                Row()
                  .width('100%')
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor(TG.dark)
                Row()
                  .width(tgBarW(this.cards[this.selIdx].hp, 2800))
                  .height(10)
                  .borderRadius(5)
                  .backgroundColor('#5B9C6E')
              }
              .layoutWeight(1)
            }
            .width('100%')
            .margin({ top: 8 })

            Row() {
              Text('下架')
                .fontSize(13)
                .fontColor('#E86A4A')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .onClick(() => {
                  this.delIdx = this.selIdx
                  this.showDel = true
                })
              Text('关闭')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.dark)
                .backgroundColor(TG.primary)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .onClick(() => {
                  this.showDetail = false
                })
            }
            .width('100%')
            .margin({ top: 16 })
          }
          .width('86%')
          .constraintSize({ maxHeight: '80%' })
          .backgroundColor(TG.card)
          .borderRadius(18)
          .padding(18)
          .zIndex(1000)
        }
      }

      if (this.showAdd) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('收录单卡')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)

            Column() {
              Text('名称')
                .fontSize(11)
                .fontColor(TG.sub)
              TextInput({ placeholder: '如 星辉龙·晨曦' })
                .fontSize(13)
                .height(40)
                .borderRadius(10)
                .backgroundColor(TG.dark)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nName = v
                })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 14 })

            Column() {
              Text('稀有度')
                .fontSize(11)
                .fontColor(TG.sub)
              Row({ space: 8 }) {
                ForEach(['UR', 'SSR', 'SR', 'H'], (t: string, i: number) => {
                  Text(t)
                    .fontSize(11)
                    .fontColor(this.nRarity === t ? TG.dark : TG.text)
                    .backgroundColor(this.nRarity === t ? tgRarityColor(t) : TG.dark)
                    .borderRadius(12)
                    .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                    .onClick(() => {
                      this.nRarity = t
                    })
                })
              }
              .margin({ top: 6 })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 12 })

            Column() {
              Text('价格(¥)')
                .fontSize(11)
                .fontColor(TG.sub)
              TextInput({ placeholder: '如 168' })
                .fontSize(13)
                .height(40)
                .borderRadius(10)
                .backgroundColor(TG.dark)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nPrice = v
                })
            }
            .width('100%')
            .alignItems(HorizontalAlign.Start)
            .margin({ top: 12 })

            Row() {
              Text('取消')
                .fontSize(14)
                .fontColor(TG.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .onClick(() => {
                  this.showAdd = false
                })
              Text('确认收录')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.dark)
                .backgroundColor(TG.primary)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .onClick(() => {
                  let it: TgCard = {
                    name: this.nName === '' ? '自定义单卡' : this.nName,
                    rarity: this.nRarity,
                    atk: 1500,
                    hp: 1800,
                    set: '第四弹·星辉',
                    price: isNaN(parseInt(this.nPrice)) ? 98 : parseInt(this.nPrice),
                    stock: 10
                  }
                  this.cards.push(it)
                  this.showAdd = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .constraintSize({ maxHeight: '80%' })
          .backgroundColor(TG.card)
          .borderRadius(18)
          .padding(18)
          .zIndex(1000)
        }
      }

      if (this.showDel) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('⚠ 下架单卡')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E86A4A')
            Text('「' + this.cards[this.delIdx].name + '」')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
              .margin({ top: 12 })
            Text(this.cards[this.delIdx].rarity + ' · 库存 ' + this.cards[this.delIdx].stock + ' 张 · 下架后不可恢复')
              .fontSize(10)
              .fontColor(TG.sub)
              .margin({ top: 6 })

            Row() {
              Text('保留')
                .fontSize(14)
                .fontColor(TG.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .onClick(() => {
                  this.showDel = false
                })
              Text('下架')
                .fontSize(14)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
                .backgroundColor('#E86A4A')
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .onClick(() => {
                  this.cards.splice(this.delIdx, 1)
                  this.showDel = false
                  this.showDetail = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('80%')
          .constraintSize({ maxHeight: '70%' })
          .backgroundColor(TG.card)
          .borderRadius(18)
          .padding(18)
          .zIndex(1000)
        }
      }
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct TgMatTab {
  @State wave: number = 0
  @State timer: number = -1
  @State mats: TgMat[] = []
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State showDel: boolean = false
  @State selIdx: number = 0
  @State delIdx: number = 0
  @State nName: string = ''
  @State nSize: string = ''
  @State nPrice: string = ''
  @State nArt: string = ''

  aboutToAppear(): void {
    this.mats = TG_MATS.slice()
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }

  @Builder modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(10,6,16,0.72)')
      .position({ x: 0, y: 0 })
      .zIndex(999)
      .onClick(() => {
        this.showDetail = false
        this.showAdd = false
        this.showDel = false
      })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Text('牌垫 PLAYMAT')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(TG.glow)
            .margin({ top: 16, left: 16 })
          Text('对局仪式感 从一块好垫开始')
            .fontSize(12)
            .fontColor(TG.sub)
            .margin({ top: 4, left: 16 })

          Column() {
            Text('库存水位')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
              .margin({ bottom: 10 })
            ForEach(TG_MATS, (m: TgMat, i: number) => {
              Column() {
                Text(m.name)
                  .fontSize(11)
                  .fontColor(TG.sub)
                  .alignSelf(ItemAlign.Start)
                Stack({ alignContent: Alignment.Start }) {
                  Row()
                    .width('100%')
                    .height(8)
                    .backgroundColor(TG.dark)
                    .borderRadius(4)
                  Row()
                    .width(tgBarW(m.stock, 60))
                    .height(8)
                    .backgroundColor(m.stock < 15 ? TG.accent : TG.primary)
                    .borderRadius(4)
                }
                .width('100%')
                .margin({ top: 4 })
              }
              .margin({ bottom: 10 })
            })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(TG.card)
          .borderRadius(14)
          .border({ width: 1, color: TG.line })
          .margin({ top: 14 })

          ForEach(this.mats, (m: TgMat, i: number) => {
            Row({ space: 12 }) {
              Column() {
                Text(m.art.charAt(0))
                  .fontSize(26)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(TG.glow)
                  .opacity(tgGlow(this.wave + i * 9))
                Text(m.size)
                  .fontSize(9)
                  .fontColor(TG.sub)
                  .margin({ top: 2 })
              }
              .width(88)
              .height(88)
              .justifyContent(FlexAlign.Center)
              .backgroundColor(TG.dark)
              .borderRadius(12)
              .border({ width: 1, color: TG.line })

              Column() {
                Text(m.name)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(TG.text)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Row({ space: 6 }) {
                  Text(m.size)
                    .fontSize(10)
                    .fontColor(TG.primary)
                    .backgroundColor(TG.dark)
                    .borderRadius(8)
                    .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                  Text(m.art)
                    .fontSize(10)
                    .fontColor(TG.accent)
                    .backgroundColor(TG.dark)
                    .borderRadius(8)
                    .padding({ left: 8, right: 8, top: 3, bottom: 3 })
                }
                .margin({ top: 6 })
                Text('库存 ' + m.stock + ' 件')
                  .fontSize(11)
                  .fontColor(m.stock < 15 ? TG.accent : TG.sub)
                  .margin({ top: 6 })
                Row() {
                  Column() {
                    Text(tgPrice(m.price))
                      .fontSize(16)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(TG.glow)
                  }
                  .alignItems(HorizontalAlign.Start)
                  .layoutWeight(1)
                  Text('删除')
                    .fontSize(11)
                    .fontColor(TG.accent)
                    .padding({ left: 10, right: 10, top: 5, bottom: 5 })
                    .borderRadius(10)
                    .border({ width: 1, color: TG.accent })
                    .onClick(() => {
                      this.delIdx = i
                      this.showDel = true
                    })
                  Text('详情')
                    .fontSize(11)
                    .fontColor(TG.dark)
                    .backgroundColor(TG.primary)
                    .borderRadius(10)
                    .padding({ left: 12, right: 12, top: 5, bottom: 5 })
                    .onClick(() => {
                      this.selIdx = i
                      this.showDetail = true
                    })
                }
                .width('100%')
                .alignItems(VerticalAlign.Bottom)
                .margin({ top: 8 })
              }
              .layoutWeight(1)
              .alignItems(HorizontalAlign.Start)
            }
            .width('92%')
            .padding(12)
            .backgroundColor(TG.card)
            .borderRadius(14)
            .border({ width: 1, color: TG.line })
            .margin({ top: 12 })
            .translate({ y: tgFloatY(this.wave, i) })
          })

          Row() {
            Text('+ 上架新牌垫')
              .fontSize(13)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.dark)
          }
          .width('92%')
          .height(46)
          .justifyContent(FlexAlign.Center)
          .backgroundColor(TG.primary)
          .borderRadius(14)
          .margin({ top: 16, bottom: 24 })
          .opacity(tgGlow(this.wave * 2))
          .onClick(() => {
            this.showAdd = true
          })
        }
        .width('100%')
      }
      .width('100%')
      .height('100%')

      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('牌垫详情')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Column() {
              Text(TG_MATS[this.selIdx].art.charAt(0))
                .fontSize(30)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.glow)
              Text(TG_MATS[this.selIdx].art)
                .fontSize(11)
                .fontColor(TG.sub)
                .margin({ top: 4 })
            }
            .width(90)
            .padding(12)
            .backgroundColor(TG.dark)
            .borderRadius(12)
            .border({ width: 1, color: TG.line })
            .margin({ top: 14 })

            Row() {
              Text('名称')
                .fontSize(12)
                .fontColor(TG.sub)
                .layoutWeight(1)
              Text(TG_MATS[this.selIdx].name)
                .fontSize(13)
                .fontColor(TG.text)
            }
            .width('100%')
            .margin({ top: 14 })
            Row() {
              Text('尺寸')
                .fontSize(12)
                .fontColor(TG.sub)
                .layoutWeight(1)
              Text(TG_MATS[this.selIdx].size)
                .fontSize(13)
                .fontColor(TG.text)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Text('图案')
                .fontSize(12)
                .fontColor(TG.sub)
                .layoutWeight(1)
              Text(TG_MATS[this.selIdx].art)
                .fontSize(13)
                .fontColor(TG.text)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Text('售价')
                .fontSize(12)
                .fontColor(TG.sub)
                .layoutWeight(1)
              Text(tgPrice(TG_MATS[this.selIdx].price))
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.glow)
            }
            .width('100%')
            .margin({ top: 8 })
            Row() {
              Text('库存')
                .fontSize(12)
                .fontColor(TG.sub)
                .layoutWeight(1)
              Text(TG_MATS[this.selIdx].stock + ' 件')
                .fontSize(13)
                .fontColor(TG_MATS[this.selIdx].stock < 15 ? TG.accent : TG.text)
            }
            .width('100%')
            .margin({ top: 8 })

            Row() {
              Text('关闭')
                .fontSize(13)
                .fontColor(TG.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(TG.dark)
                .onClick(() => {
                  this.showDetail = false
                })
              Column()
                .width(12)
              Text('立即购买')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.dark)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(TG.primary)
                .onClick(() => {
                  this.showDetail = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(TG.card)
          .borderRadius(18)
          .border({ width: 1, color: TG.line })
          .constraintSize({ maxHeight: '80%' })
          .zIndex(1000)
        }
      }

      if (this.showAdd) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('上架新牌垫')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Column() {
              Text('垫面名称')
                .fontSize(11)
                .fontColor(TG.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .fontSize(13)
                .fontColor(TG.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(TG.sub)
                .backgroundColor(TG.dark)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nName = v
                })
            }
            .width('100%')
            .margin({ top: 14 })
            Column() {
              Text('尺寸规格')
                .fontSize(11)
                .fontColor(TG.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .fontSize(13)
                .fontColor(TG.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(TG.sub)
                .backgroundColor(TG.dark)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nSize = v
                })
            }
            .width('100%')
            .margin({ top: 12 })
            Column() {
              Text('售价 (元)')
                .fontSize(11)
                .fontColor(TG.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .type(InputType.Number)
                .fontSize(13)
                .fontColor(TG.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(TG.sub)
                .backgroundColor(TG.dark)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nPrice = v
                })
            }
            .width('100%')
            .margin({ top: 12 })
            Column() {
              Text('图案主题')
                .fontSize(11)
                .fontColor(TG.sub)
                .alignSelf(ItemAlign.Start)
              TextInput()
                .fontSize(13)
                .fontColor(TG.text)
                .placeholderFont({ size: 12 })
                .placeholderColor(TG.sub)
                .backgroundColor(TG.dark)
                .borderRadius(10)
                .padding({ left: 12, right: 12 })
                .height(40)
                .margin({ top: 6 })
                .onChange((v: string) => {
                  this.nArt = v
                })
            }
            .width('100%')
            .margin({ top: 12 })
            Row() {
              Text('取消')
                .fontSize(13)
                .fontColor(TG.sub)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(TG.dark)
                .onClick(() => {
                  this.showAdd = false
                })
              Column()
                .width(12)
              Text('确认上架')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.dark)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(TG.primary)
                .onClick(() => {
                  let it: TgMat = {
                    name: this.nName === '' ? '未命名牌垫' : this.nName,
                    size: this.nSize === '' ? '60×35cm' : this.nSize,
                    price: isNaN(parseInt(this.nPrice)) ? 159 : parseInt(this.nPrice),
                    stock: 30,
                    art: this.nArt === '' ? '极简线条' : this.nArt
                  }
                  this.mats.unshift(it)
                  this.nName = ''
                  this.nSize = ''
                  this.nPrice = ''
                  this.nArt = ''
                  this.showAdd = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('86%')
          .padding(20)
          .backgroundColor(TG.card)
          .borderRadius(18)
          .border({ width: 1, color: TG.line })
          .constraintSize({ maxHeight: '80%' })
          .zIndex(1000)
        }
      }

      if (this.showDel) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('确认移除')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Text('将从货架移除「' + (this.mats.length > this.delIdx ? this.mats[this.delIdx].name : '') + '」,该操作不可撤销')
              .fontSize(12)
              .fontColor(TG.sub)
              .textAlign(TextAlign.Center)
              .margin({ top: 12 })
            Row() {
              Text('再想想')
                .fontSize(13)
                .fontColor(TG.text)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(TG.dark)
                .onClick(() => {
                  this.showDel = false
                })
              Column()
                .width(12)
              Text('移除')
                .fontSize(13)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.dark)
                .layoutWeight(1)
                .textAlign(TextAlign.Center)
                .padding({ top: 10, bottom: 10 })
                .borderRadius(12)
                .backgroundColor(TG.accent)
                .onClick(() => {
                  this.mats.splice(this.delIdx, 1)
                  this.showDel = false
                })
            }
            .width('100%')
            .margin({ top: 18 })
          }
          .width('76%')
          .padding(20)
          .backgroundColor(TG.card)
          .borderRadius(18)
          .border({ width: 1, color: TG.line })
          .zIndex(1000)
        }
      }
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct TgSleeveTab {
  @State wave: number = 0
  @State timer: number = -1
  @State sleeves: TgSleeve[] = []
  @State showDetail: boolean = false
  @State showAdd: boolean = false
  @State selIdx: number = 0
  @State nName: string = ''
  @State nColor: string = ''
  @State nPrice: string = ''
  @State nFinish: string = '哑光'

  aboutToAppear(): void {
    this.sleeves = TG_SLEEVES.slice()
    this.timer = setInterval(() => {
      this.wave = (this.wave + 1) % 100
    }, 60)
  }

  aboutToDisappear(): void {
    clearInterval(this.timer)
  }

  @Builder modalOverlay() {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('rgba(10,6,16,0.72)')
      .position({ x: 0, y: 0 })
      .zIndex(999)
      .onClick(() => {
        this.showDetail = false
        this.showAdd = false
      })
  }

  build() {
    Stack() {
      Scroll() {
        Column() {
          Row() {
            Column() {
              Text('卡套 SLEEVE')
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor(TG.glow)
              Text('护卡如护卡组的一生')
                .fontSize(12)
                .fontColor(TG.sub)
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)
            Text('+ 新增')
              .fontSize(12)
              .fontColor(TG.dark)
              .backgroundColor(TG.primary)
              .borderRadius(14)
              .padding({ left: 14, right: 14, top: 7, bottom: 7 })
              .onClick(() => {
                this.showAdd = true
              })
          }
          .width('92%')
          .margin({ top: 16 })

          Column() {
            Text('价格对比')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
              .margin({ bottom: 10 })
            ForEach(this.sleeves, (s: TgSleeve, i: number) => {
              Row() {
                Row() {
                  Circle()
                    .width(10)
                    .height(10)
                    .fill(s.colorHex)
                }
                .width(18)
                Text(s.name)
                  .fontSize(10)
                  .fontColor(TG.sub)
                  .layoutWeight(1)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Stack({ alignContent: Alignment.Start }) {
                  Row()
                    .width('100%')
                    .height(8)
                    .backgroundColor(TG.dark)
                    .borderRadius(4)
                  Row()
                    .width(tgBarW(s.price, 60))
                    .height(8)
                    .backgroundColor(s.colorHex)
                    .borderRadius(4)
                }
                .width(120)
              }
              .margin({ bottom: 8 })
            })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(TG.card)
          .borderRadius(14)
          .border({ width: 1, color: TG.line })
          .margin({ top: 14 })

          Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
            ForEach(this.sleeves, (s: TgSleeve, i: number) => {
              Column() {
                Column() {
                  Text('◆')
                    .fontSize(22)
                    .fontColor(TG.dark)
                    .opacity(tgGlow(this.wave + i * 7))
                  Text(s.finish)
                    .fontSize(9)
                    .fontColor(TG.dark)
                    .backgroundColor('rgba(255,255,255,0.35)')
                    .borderRadius(8)
                    .padding({ left: 8, right: 8, top: 2, bottom: 2 })
                    .margin({ top: 6 })
                }
                .width('100%')
                .height(64)
                .justifyContent(FlexAlign.Center)
                .backgroundColor(s.colorHex)
                .borderRadius(12)

                Text(s.name)
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(TG.text)
                  .margin({ top: 8 })
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                Text('每包 ' + s.count + ' 张')
                  .fontSize(10)
                  .fontColor(TG.sub)
                  .margin({ top: 3 })
                Text(tgPrice(s.price))
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(TG.glow)
                  .margin({ top: 4 })
                Text('查看详情')
                  .fontSize(10)
                  .fontColor(TG.primary)
                  .margin({ top: 6 })
                  .padding({ left: 12, right: 12, top: 4, bottom: 4 })
                  .borderRadius(10)
                  .border({ width: 1, color: TG.primary })
                  .onClick(() => {
                    this.selIdx = i
                    this.showDetail = true
                  })
              }
              .width('31%')
              .padding(10)
              .backgroundColor(TG.card)
              .borderRadius(14)
              .border({ width: 1, color: TG.line })
              .margin({ top: 12 })
              .translate({ y: tgFloatY(this.wave, i) })
            })
          }
          .width('92%')

          Column() {
            Text('装套小贴士')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Text('先装内套再装外壳,开口方向与卡组保持一致;磨砂面朝外可减少灯光反射,直播对局画面更干净。')
              .fontSize(11)
              .fontColor(TG.sub)
              .lineHeight(18)
              .margin({ top: 8 })
          }
          .width('92%')
          .padding(14)
          .backgroundColor(TG.card)
          .borderRadius(14)
          .border({ width: 1, color: TG.line })
          .margin({ top: 14, bottom: 24 })
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
      }
      .width('100%')
      .height('100%')

      if (this.showDetail) {
        Stack() {
          this.modalOverlay()
          Column() {
            Text('卡套详情')
              .fontSize(17)
              .fontWeight(FontWeight.Bold)
              .fontColor(TG.text)
            Row({ space: 12 }) {
              Column() {
                Text('◆')
                  .fontSize(24)
                  .fontColor(TG.dark)
                  .opacity(tgGlow(this.wave * 3))
              }
              .width(74)
              .height(74)
              .justifyContent(FlexAlign.Center)
              .backgroundColor(this.sleeves.length > this.selIdx ? this.sleeves[this.selIdx].colorHex : TG.dark)
              .borderRadius(12)

              Column() {
                Text(this.sleeves.length > this.selIdx ? this.sleeves[this.selIdx].name : '')
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(TG.text)
                Text(this.sleeves.length > this.selIdx ? this.sleeves[this.selIdx].finish + '工艺' : '')
                  .fontSize(11)
                  .fontColor(TG.primary)
                  .margin({ top: 6 })
                Text(tgPrice(this.sleeves.length > this.selIdx ? this.sleeves[this.selIdx].price : 0))
                  .fontSize(16)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(TG.glow)
                  .margin({ top: 8 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
            }
            .width('100%')
            .margin({ top: 16 })

            Row() {
              Text('每包数量')
                .fontSize(12)
                .fontColor(TG.sub)
                .layoutWeight(1)
              Text((this.sleeves.length > this.selIdx ? this.sleeves[this.selIdx].count : 0) + ' 张')
                .fontSize(13)
                .fontColor(TG.text)
         
}


总结

在这里插入图片描述

本文深入剖析了一款基于HarmonyOS ArkTS构建的集换卡牌对战馆应用,从主题系统、数据模型、工具函数到组件架构,全方位展示了声明式UI框架在复杂业务场景下的应用实践。该应用以暗夜鎏金的视觉风格为特色,通过精心设计的动画效果和交互体验,为卡牌游戏爱好者打造了沉浸式的数字收藏空间。

从技术架构角度看,应用采用了清晰的分层设计思想:数据模型层通过TypeScript接口确保类型安全,工具函数层以纯函数方式封装数据处理逻辑,组件层则专注于视图构建和交互响应。这种分层架构不仅提高了代码的可读性和可维护性,也为团队协作提供了良好的基础。特别是动画系统的设计,通过数学纯函数与状态驱动相结合的方式,实现了丰富而流畅的视觉效果,同时保持了代码的简洁性。

Logo

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

更多推荐