## 一、引言:从晨光中开启高效一天 在移动互联网与智能穿戴设备高度普及的今天,“习惯养成“已经从一个模糊的自我管理理念,演变为一个可量化、可追踪、可视化的数字化产品赛道。越来越多的人希望借助手
基于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:完整的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接口确保类型安全,工具函数层以纯函数方式封装数据处理逻辑,组件层则专注于视图构建和交互响应。这种分层架构不仅提高了代码的可读性和可维护性,也为团队协作提供了良好的基础。特别是动画系统的设计,通过数学纯函数与状态驱动相结合的方式,实现了丰富而流畅的视觉效果,同时保持了代码的简洁性。
更多推荐



所有评论(0)