HarmonyOS 6.1 实战:卡牌卡片中Stack({ alignContent: Alignment.TopEnd })放置在卡牌图片区域的右上角
潮玩卡牌对战应用是集卡牌收集、系列图鉴、实时对战、交易市场、排名竞技与个人收藏于一体的综合性游戏化社交平台。在HarmonyOS声明式UI范式下,通过ArkTS的强类型系统和组件化设计模式,将复杂的卡牌数据模型——稀有度分级(SSR/UR/SR/R)、攻防属性、系列关联、对战记录——映射为类型安全的interface接口,配合静态数据常量实现数据与视图的彻底解耦。
应用采用钻石蓝(#0277BD)与璀璨金(#FFD700)的经典配色组合,通过底部6Tab单排导航实现卡牌商店、系列图鉴、对战竞技场、交易市场、排行榜和个人中心六大核心模块的快速切换。每个Tab页面拥有差异化的布局策略——网格列表、时间线卡片、竞技场战绩、市场交易列表、领奖台排行、收藏管理——共同构成丰富多元的视觉体验。
在弹框交互层面,应用实现了5个弹框组件:购买卡牌底部抽屉(含数量选择、补充包选项、卡牌保险)、出售卡牌居中弹窗(含市场参考价和自定义定价)、编辑卡组底部抽屉(含卡牌列表和卡槽选择)、删除卡牌确认弹窗、对战记录详情弹窗。这些弹框覆盖了卡牌游戏从购买到收藏到交易到对战的完整生命周期。

引言
卡牌对战类游戏是移动游戏市场中长盛不衰的品类,其核心吸引力在于"收集-构筑-对战-交易"的完整闭环。玩家通过购买卡包获取随机卡牌,根据稀有度和属性构筑个人卡组,在对战中与对手竞技获取排名积分,并在交易市场中买卖卡牌实现资产流通。本应用将这一完整闭环浓缩在单一的HarmonyOS应用中,通过ArkTS声明式UI范式实现了高度模块化的组件架构。
在技术架构上,应用采用"入口组件 + Tab内容组件 + 弹框组件"的三层分层设计。入口组件DuoDuoCardApp作为根组件,管理着9个@State状态变量,包括Tab索引、5个弹框的显隐控制、选中卡牌对象以及购买抽屉中的数量、补充包和保险选项等用户选择状态。卡牌数据通过CardItem109接口定义,包含了名称、系列、稀有度、价格、原价、销量、攻击力、防御力、生命值、表情符号和标签等11个字段,为UI渲染提供完整的数据支撑。
应用的工具函数层提供了getRarityColor109、getRarityBg109、getResultColor109、getStatusColor109和getRankColor109五个辅助函数,分别处理稀有度颜色映射、稀有度背景色映射、对战结果颜色映射、订单状态颜色映射和排名颜色映射。这些函数在多个组件中被反复调用,通过条件判断返回对应的十六进制颜色值,实现了视觉表现的统一管理和集中维护。
配色体系与数据模型定义

应用的配色体系以钻石蓝和璀璨金为核心,通过interface ColorPalette109定义了16个颜色字段。主色#0277BD是Material Design的Light Blue 800色值,传达科技感和竞技属性;金色#FFD700作为辅助色用于强调装饰和稀有度标识,两者搭配营造出"宝藏与竞技"的游戏化视觉氛围。
interface ColorPalette109 {
primary: string;
primaryLight: string;
primaryDark: string;
accent: string;
accentLight: string;
bg: string;
card: string;
textMain: string;
textSub: string;
textHint: string;
border: string;
success: string;
warning: string;
danger: string;
gold: string;
white: string;
}
const COLORS109: ColorPalette109 = {
primary: '#0277BD',
primaryLight: '#4FC3F7',
primaryDark: '#01579B',
accent: '#FFD700',
accentLight: '#FFE082',
bg: '#E1F5FE',
card: '#FFFFFF',
textMain: '#01579B',
textSub: '#0288D1',
textHint: '#81D4FA',
border: '#B3E5FC',
success: '#66BB6A',
warning: '#FFA726',
danger: '#EF5350',
gold: '#FFB300',
white: '#FFFFFF'
};
颜色字段中特别值得注意的是
gold字段,它不同于accent的纯金色#FFD700,而是采用了更深沉的#FFB300。这种区分设计使得金色在不同使用场景下有不同的视觉效果——accent用于明亮的装饰性金色(如标签、图标),gold用于需要与背景形成强对比的文本和数值(如积分、排名)。
数据接口层定义了卡牌游戏所需的核心数据模型。CardItem109接口是应用的核心实体,包含了卡牌的全部属性信息;SeriesMeta109定义了卡牌系列的元数据,包括系列名称、描述、总卡数、UR数量、隐藏卡数量和发布日期;BattleRecord109记录了对战的对手、结果、使用卡组、回合数和得分。
interface CardItem109 {
id: string;
name: string;
series: string;
rarity: string;
price: number;
oldPrice: number;
sales: number;
atk: number;
def: number;
hp: number;
emoji: string;
tag: string;
}
interface SeriesMeta109 {
id: string;
name: string;
desc: string;
total: number;
ultra: number;
secret: number;
releaseDate: string;
emoji: string;
color1: string;
color2: string;
}
interface BattleRecord109 {
id: string;
opponent: string;
result: string;
deck: string;
turns: number;
time: string;
score: number;
}
interface RankItem109 {
id: string;
rank: number;
name: string;
score: number;
winRate: number;
battles: number;
emoji: string;
}
SeriesMeta109接口中的color1和color2字段为每个系列定义了专属的渐变色调,如龙族传说系列使用深红到金色渐变(#B71C1C到#FFD700),魔法师系列使用深紫到浅紫渐变。这种按系列定制的配色方案使得每个系列卡片在视觉上具有独特的识别性,增强了收集体验的沉浸感。
工具函数与入口组件

工具函数层通过条件判断实现数据到颜色的映射,其中getRarityColor109根据稀有度字符串返回对应颜色——SSR返回深红色#D32F2F、UR返回橙色#FF6F00、SR返回深蓝色#1565C0、R返回灰色#78909C。getRarityBg109则返回对应的浅色背景,用于卡片背景区域的差异化渲染。
function getRarityColor109(rarity: string): string {
if (rarity === 'SSR') return '#D32F2F';
if (rarity === 'UR') return '#FF6F00';
if (rarity === 'SR') return '#1565C0';
return '#78909C';
}
function getRarityBg109(rarity: string): string {
if (rarity === 'SSR') return '#FFEBEE';
if (rarity === 'UR') return '#FFF3E0';
if (rarity === 'SR') return '#E3F2FD';
return '#ECEFF1';
}
function getResultColor109(result: string): string {
if (result === '胜利') return '#43A047';
return '#EF5350';
}
function getRankColor109(rank: number): string {
if (rank <= 3) return '#FFD700';
if (rank <= 6) return '#9E9E9E';
return '#CD7F32';
}
入口组件DuoDuoCardApp管理着应用的全局状态。头部区域使用深蓝到浅蓝的90度线性渐变,标题"卡牌对决"使用金色字体,右侧搜索和背包图标同样使用金色,整体营造出游戏化的竞技氛围。Tab内容区通过if-else if条件分支实现6个Tab页面的条件渲染。
@Entry
@Component
struct DuoDuoCardApp {
@State currentTab: number = 0;
@State showBuySheet: boolean = false;
@State showSellDialog: boolean = false;
@State showDeckSheet: boolean = false;
@State showDeleteDialog: boolean = false;
@State showBattleDialog: boolean = false;
@State selectedCard: CardItem109 | null = null;
@State selQty: number = 1;
@State selBooster: boolean = false;
@State selInsurance: boolean = false;
@State sellPrice: number = 0;
@State selDeckSlot: number = 0;
build() {
Column() {
Row() {
Text('卡牌对决')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.gold)
Text('').layoutWeight(1)
Text('🔍')
.fontSize(22)
.fontColor(COLORS109.gold)
.margin({ right: 12 })
Text('🎒')
.fontSize(22)
.fontColor(COLORS109.gold)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.linearGradient({ angle: 90, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })
if (this.currentTab === 0) {
CardShopTab109({
onBuy: (c: CardItem109) => {
this.selectedCard = c;
this.showBuySheet = true;
},
onDetail: (c: CardItem109) => {
this.selectedCard = c;
this.showSellDialog = true;
}
})
} else if (this.currentTab === 2) {
BattleTab109({
onRecord: () => { this.showBattleDialog = true; }
})
} else {
MyCardTab109({
onDeck: () => { this.showDeckSheet = true; },
onDelete: () => { this.showDeleteDialog = true; }
})
}
Row() {
TabBtn109({icon:'🃏', label:'卡牌', active: this.currentTab === 0, onTap: () => { this.currentTab = 0; }})
TabBtn109({icon:'📦', label:'系列', active: this.currentTab === 1, onTap: () => { this.currentTab = 1; }})
TabBtn109({icon:'⚔️', label:'对战', active: this.currentTab === 2, onTap: () => { this.currentTab = 2; }})
TabBtn109({icon:'🔄', label:'交易', active: this.currentTab === 3, onTap: () => { this.currentTab = 3; }})
TabBtn109({icon:'🏆', label:'排行', active: this.currentTab === 4, onTap: () => { this.currentTab = 4; }})
TabBtn109({icon:'👤', label:'我的', active: this.currentTab === 5, onTap: () => { this.currentTab = 5; }})
}
.width('100%')
.height(56)
.backgroundColor(COLORS109.card)
.border({ width: 1, color: COLORS109.border })
.justifyContent(FlexAlign.SpaceAround)
}
.width('100%')
.height('100%')
.backgroundColor(COLORS109.bg)
}
}
底部Tab导航组件
TabBtn109的设计有一个独特之处——它在激活状态下会在顶部显示一条3vp高度的金色横条,通过if (this.active)条件渲染实现。这条横条为Tab导航增加了视觉层次感,使激活状态更加醒目。这种"顶部金边"设计灵感来自游戏UI中常见的标签选中标识。
卡牌商店与系列图鉴

卡牌商店Tab(CardShopTab109)是应用的主页面,集成了搜索框、横幅公告、金刚区分类入口、对战统计、热卖卡牌横滑和全部卡牌网格六个区域。搜索框采用圆角胶囊式设计,横幅公告使用深蓝背景展示新系列上架信息。金刚区通过4列网格展示8个卡牌分类入口(龙族、魔法师、战士、机械、天使、恶魔、兽族、限定),每个入口的背景色来自数据对象。
@Component
struct CardShopTab109 {
onBuy: (c: CardItem109) => void = () => {};
onDetail: (c: CardItem109) => void = () => {};
build() {
Scroll() {
Column() {
Row() {
Text('🔍 搜索卡牌、系列...')
.fontSize(13)
.fontColor(COLORS109.textHint)
.layoutWeight(1)
}
.width('92%')
.height(36)
.backgroundColor(COLORS109.card)
.borderRadius(18)
.padding({ left: 16, right: 16 })
.margin({ top: 12, bottom: 8 })
Row() {
Text('🎴 新系列「野兽传说」上架 · 限时折扣')
.fontSize(11)
.fontColor(COLORS109.gold)
.layoutWeight(1)
}
.width('100%')
.height(28)
.backgroundColor(COLORS109.primaryDark)
.justifyContent(FlexAlign.Center)
Grid() {
ForEach(CAT_ENTRIES_109, (cat: CatEntry109) => {
GridItem() {
Column() {
Column() {
Text(cat.emoji)
.fontSize(24)
}
.width(44)
.height(44)
.backgroundColor(cat.color)
.borderRadius(22)
.justifyContent(FlexAlign.Center)
Text(cat.name)
.fontSize(9)
.fontColor(COLORS109.textSub)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Center)
}
}, (cat: CatEntry109) => cat.id)
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsGap(8)
.columnsGap(8)
.width('92%')
.margin({ top: 12 })
.backgroundColor(COLORS109.card)
.borderRadius(12)
.padding(12)
// 热卖卡牌横滑区域
Scroll() {
Row() {
ForEach(CARDS_109.slice(0, 6), (c: CardItem109) => {
Column() {
Stack({ alignContent: Alignment.TopEnd }) {
Column() {
Text(c.emoji)
.fontSize(36)
}
.width(72)
.height(72)
.backgroundColor(getRarityBg109(c.rarity))
.borderRadius(12)
.justifyContent(FlexAlign.Center)
Text(c.rarity)
.fontSize(8)
.fontColor(COLORS109.white)
.backgroundColor(getRarityColor109(c.rarity))
.borderRadius(3)
.padding({ left: 3, right: 3, top: 1, bottom: 1 })
.margin({ top: 3, right: 3 })
}
Text(c.name)
.fontSize(10)
.fontColor(COLORS109.textMain)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width(72)
.margin({ top: 4 })
Text('ATK ' + c.atk + ' / DEF ' + c.def)
.fontSize(8)
.fontColor(COLORS109.textSub)
Row() {
Text('¥' + c.price)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
Text('¥' + c.oldPrice)
.fontSize(9)
.fontColor(COLORS109.textHint)
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 4 })
}
.margin({ top: 2 })
}
.width(88)
.margin({ right: 8 })
.onClick(() => { this.onDetail(c); })
}, (c: CardItem109) => c.id)
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 12 })
// 全部卡牌网格
Grid() {
ForEach(CARDS_109, (c: CardItem109) => {
GridItem() {
Column() {
Stack({ alignContent: Alignment.TopEnd }) {
Column() {
Text(c.emoji)
.fontSize(40)
}
.width('100%')
.height(80)
.backgroundColor(getRarityBg109(c.rarity))
.borderRadius(12)
.justifyContent(FlexAlign.Center)
Text(c.rarity)
.fontSize(8)
.fontColor(COLORS109.white)
.backgroundColor(getRarityColor109(c.rarity))
.borderRadius(3)
.padding({ left: 3, right: 3, top: 1, bottom: 1 })
.margin({ top: 3, right: 3 })
}
Text(c.name)
.fontSize(11)
.fontColor(COLORS109.textMain)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 6 })
Row() {
Text('ATK')
.fontSize(8)
.fontColor(COLORS109.textHint)
Text(c.atk.toString())
.fontSize(9)
.fontColor(COLORS109.danger)
.margin({ left: 2 })
Text('DEF')
.fontSize(8)
.fontColor(COLORS109.textHint)
.margin({ left: 4 })
Text(c.def.toString())
.fontSize(9)
.fontColor(COLORS109.primary)
.margin({ left: 2 })
}
.margin({ top: 2 })
Row() {
Text('¥' + c.price)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
Text('¥' + c.oldPrice)
.fontSize(9)
.fontColor(COLORS109.textHint)
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 4 })
Text('').layoutWeight(1)
Text('销' + c.sales)
.fontSize(8)
.fontColor(COLORS109.textHint)
}
.width('100%')
.margin({ top: 4 })
Row() {
Text('购买')
.fontSize(11)
.fontColor(COLORS109.white)
.backgroundColor(COLORS109.primary)
.borderRadius(6)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.layoutWeight(1)
.textAlign(TextAlign.Center)
}
.width('100%')
.margin({ top: 6 })
.onClick(() => { this.onBuy(c); })
}
.padding(10)
.backgroundColor(COLORS109.card)
.borderRadius(12)
}
}, (c: CardItem109) => c.id)
}
.columnsTemplate('1fr 1fr')
.rowsGap(8)
.columnsGap(8)
.width('92%')
.margin({ bottom: 16 })
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.constraintSize({ maxHeight: '80%' })
}
}
卡牌卡片中
Stack({ alignContent: Alignment.TopEnd })的使用是一个关键技巧——它将稀有度标签精确放置在卡牌图片区域的右上角。Stack堆叠容器配合TopEnd对齐方式实现了"角标"效果,这是移动端UI中常见的角标实现方案。通过getRarityColor109和getRarityBg109两个函数,同一张卡牌的稀有度标签和背景区域获得了协调的颜色映射。
系列图鉴Tab(SeriesTab109)采用时间线卡片式布局展示6个卡牌系列。每个系列卡片使用color1和color2字段定义的135度渐变作为头像背景,并展示系列名称、描述、总卡数、UR数量、隐藏卡数量和发布日期。卡片底部提供"购买整盒"和"查看图鉴"两个操作按钮,分别触发购买流程和图鉴浏览。
对战竞技场与排行榜

对战Tab(BattleTab109)是应用的竞技核心,整合了战绩统计、本周对战柱状图和对战记录列表三个区域。战绩统计通过4列等分布局展示胜率、胜场、败场和积分,每个数据项使用不同的颜色标识——胜率用金色、胜场用绿色、败场用红色、积分用蓝色,视觉上直观传达竞技状态。
@Component
struct BattleTab109 {
onRecord: () => void = () => {};
build() {
Scroll() {
Column() {
Column() {
Text('⚔️ 对战竞技场')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.gold)
Text('实时对战·排名攀升')
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: 24, bottom: 24 })
.linearGradient({ angle: 135, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })
Row() {
ForEach(STATS_109, (s: StatCard109) => {
Column() {
Text(s.emoji)
.fontSize(20)
Text(s.value)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(s.color)
.margin({ top: 2 })
Text(s.label)
.fontSize(9)
.fontColor(COLORS109.textSub)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (s: StatCard109) => s.id)
}
.width('92%')
.padding({ top: 12, bottom: 12 })
.backgroundColor(COLORS109.card)
.borderRadius(12)
.margin({ top: 12 })
// 本周对战柱状图
Row() {
ForEach(WEEK_BARS_109, (bar: BarItem109) => {
Column() {
Text(bar.value.toString())
.fontSize(9)
.fontColor(bar.color)
Column() {
Text('')
.width('100%')
.height(bar.value * 10)
.backgroundColor(bar.color)
.borderRadius(3)
}
.width(24)
.height(80)
.justifyContent(FlexAlign.End)
.margin({ top: 4, bottom: 4 })
Text(bar.name)
.fontSize(9)
.fontColor(COLORS109.textSub)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (bar: BarItem109) => bar.id)
}
.width('92%')
.padding(16)
.backgroundColor(COLORS109.card)
.borderRadius(12)
.margin({ bottom: 12 })
// 对战记录
ForEach(BATTLES_109, (b: BattleRecord109) => {
Row() {
Column() {
Text(b.result === '胜利' ? '🏆' : '💀')
.fontSize(20)
}
.width(40)
.height(40)
.backgroundColor(b.result === '胜利' ? '#E8F5E9' : '#FFEBEE')
.borderRadius(20)
.justifyContent(FlexAlign.Center)
Column() {
Text('vs ' + b.opponent)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Row() {
Text(b.result)
.fontSize(9)
.fontColor(getResultColor109(b.result))
Text(b.deck)
.fontSize(9)
.fontColor(COLORS109.textSub)
.margin({ left: 6 })
Text(b.turns + '回合')
.fontSize(9)
.fontColor(COLORS109.textHint)
.margin({ left: 6 })
}
.margin({ top: 2 })
Text(b.time)
.fontSize(9)
.fontColor(COLORS109.textHint)
.margin({ top: 2 })
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('+' + b.score)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(b.score > 0 ? COLORS109.gold : COLORS109.textHint)
}
.width('92%')
.padding(10)
.backgroundColor(COLORS109.card)
.borderRadius(10)
.margin({ left: 12, right: 12, bottom: 6 })
}, (b: BattleRecord109) => b.id)
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.constraintSize({ maxHeight: '80%' })
}
}
对战记录列表中每条记录的左侧头像区域根据对战结果动态渲染不同颜色——胜利时使用浅绿色背景
#E8F5E9配合奖杯emoji,失败时使用浅红色背景#FFEBEE配合骷髅emoji。右侧的得分+b.score在胜利时显示金色,失败时(score为0)显示浅灰色,通过b.score > 0条件判断实现颜色切换。
排行榜Tab(RankTab109)的特色设计是前三名领奖台布局——第二名(银牌)居左、第一名(金牌)居中且尺寸更大、第三名(铜牌)居右。通过margin({ top: 12 })和margin({ top: 20 })的差异化设置,使第一名卡片位置最高,第二名次之,第三名最低,营造出奥运会领奖台的视觉效果。4-10名则采用常规列表布局,每行展示排名序号、用户名、胜率和积分数。
// 前三领奖台
Row() {
Column() {
Text('🥈')
.fontSize(28)
Column() {
Text(RANKS_109[1].name)
.fontSize(11)
.fontColor(COLORS109.textMain)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(RANKS_109[1].score.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#9E9E9E')
}
.width(72)
.height(80)
.backgroundColor('#F5F5F5')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.padding(8)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.margin({ top: 12 })
Column() {
Text('🥇')
.fontSize(36)
Column() {
Text(RANKS_109[0].name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(RANKS_109[0].score.toString())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.gold)
}
.width(80)
.height(100)
.backgroundColor('#FFF8E1')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.padding(8)
.border({ width: 2, color: COLORS109.gold })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}
.width('92%')
.margin({ top: 12, bottom: 16 })
应用架构流程图

购买与出售弹框组件

购买卡牌弹框BuyCardSheet109是底部抽屉式组件,展示了选中卡牌的完整信息——emoji头像、名称、系列、稀有度、攻击力和防御力。弹框提供数量增减控制(通过selQty状态和+/−按钮实现),并包含"购买补充包"(随机5张含UR概率UP)和"卡牌保险"(运输损坏包赔)两个附加选项的勾选开关。底部实时计算合计金额¥ + (card.price * selQty)并展示确认购买按钮。
@Component
struct BuyCardSheet109 {
card: CardItem109 | null = null;
selQty: number = 1;
selBooster: boolean = false;
selInsurance: boolean = false;
onQty: (q: number) => void = () => {};
onBooster: (b: boolean) => void = () => {};
onInsurance: (i: boolean) => void = () => {};
onConfirm: () => void = () => {};
onCancel: () => void = () => {};
build() {
Scroll() {
Column() {
Row() {
Column() {
Text(this.card?.emoji ?? '🃏')
.fontSize(36)
}
.width(60)
.height(60)
.backgroundColor(getRarityBg109(this.card?.rarity ?? 'R'))
.borderRadius(12)
.justifyContent(FlexAlign.Center)
Column() {
Text(this.card?.name ?? '')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Text(this.card?.series ?? '')
.fontSize(10)
.fontColor(COLORS109.textSub)
.margin({ top: 2 })
Row() {
Text(this.card?.rarity ?? '')
.fontSize(9)
.fontColor(getRarityColor109(this.card?.rarity ?? 'R'))
Text('ATK ' + (this.card?.atk ?? 0))
.fontSize(9)
.fontColor(COLORS109.danger)
.margin({ left: 8 })
Text('DEF ' + (this.card?.def ?? 0))
.fontSize(9)
.fontColor(COLORS109.primary)
.margin({ left: 8 })
}
.margin({ top: 4 })
}
.margin({ left: 12 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('✕')
.fontSize(18)
.fontColor(COLORS109.textHint)
.onClick(() => { this.onCancel(); })
}
.width('100%')
.padding(16)
Divider().color(COLORS109.border)
Row() {
Text('¥' + (this.card?.price ?? 0))
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
Text('¥' + (this.card?.oldPrice ?? 0))
.fontSize(12)
.fontColor(COLORS109.textHint)
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 6 })
Text('').layoutWeight(1)
Text('销' + (this.card?.sales ?? 0))
.fontSize(10)
.fontColor(COLORS109.textHint)
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
Row() {
Text('数量')
.fontSize(13)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
Text('−')
.fontSize(16)
.fontColor(COLORS109.primary)
.padding({ left: 12, right: 12 })
.onClick(() => { if (this.selQty > 1) { this.onQty(this.selQty - 1); } })
Text(this.selQty.toString())
.fontSize(14)
.fontColor(COLORS109.textMain)
.padding({ left: 12, right: 12 })
Text('+')
.fontSize(16)
.fontColor(COLORS109.primary)
.padding({ left: 12, right: 12 })
.onClick(() => { this.onQty(this.selQty + 1); })
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
Row() {
Column() {
Text('📦 购买补充包')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Text('随机5张·含UR概率UP')
.fontSize(10)
.fontColor(COLORS109.textSub)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text(this.selBooster ? '☑' : '☐')
.fontSize(20)
.fontColor(this.selBooster ? COLORS109.primary : COLORS109.textHint)
.onClick(() => { this.onBooster(!this.selBooster); })
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
Row() {
Text('合计')
.fontSize(13)
.fontColor(COLORS109.textSub)
Text('¥' + ((this.card?.price ?? 0) * this.selQty))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
.margin({ left: 8 })
.layoutWeight(1)
Text('确认购买')
.fontSize(14)
.fontColor(COLORS109.white)
.backgroundColor(COLORS109.primary)
.borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.onClick(() => { this.onConfirm(); })
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 20 })
}
.width('100%')
.backgroundColor(COLORS109.white)
}
.scrollBar(BarState.Off)
}
}
购买弹框中"数量减"按钮的
onClick回调包含条件判断if (this.selQty > 1),防止数量减到0以下。而"数量加"按钮则无上限限制,允许用户自由增加购买数量。合计金额的计算公式¥ + ((card.price ?? 0) * selQty)利用了空值合并运算符??确保在卡牌数据为null时显示0,避免了运行时空指针异常。
出售卡牌弹框SellCardDialog109采用居中弹窗式设计,展示卡牌信息和市场参考价,提供价格输入框和上架出售按钮。用户通过TextInput输入自定义出售价格,onChange回调将输入值通过parseInt转换后传递给父组件的sellPrice状态。这种设计允许用户根据市场行情自主定价,体现了卡牌交易市场的灵活性。
技术点对比分析
| 技术维度 | 实现方案 | 设计优势 | 适用场景 |
|---|---|---|---|
| 稀有度渲染 | 双函数颜色映射 | 颜色与背景分离管理,复用性强 | 多稀有度等级的卡牌/装备系统 |
| 领奖台布局 | 差异化高度+尺寸 | 视觉冲击力强,竞技感突出 | 排行榜、赛事排名等场景 |
| 柱状图 | FlexAlign.End对齐 | 无需图表库,轻量高效 | 简单数据趋势可视化 |
| 卡片角标 | Stack + TopEnd | 精确定位角标位置,布局稳定 | 商品标签、状态标识 |
| 渐变头像 | linearGradient + 数据色值 | 每个系列视觉独立,辨识度高 | 多系列/多分类的卡片展示 |
| 条件渲染Tab | if-else if分支 | 仅渲染当前页,内存效率高 | 中等数量Tab页面 |
| 数量控制 | +/−按钮 + 条件保护 | 防止越界操作,交互安全 | 购物车、数量选择场景 |
| 删除线效果 | TextDecorationType.LineThrough | 原生支持,无需自定义绘制 | 价格对比、促销展示 |
安装DevEco Studio程序

选择目标安装目录:

设置环境变量,但是需要重启一下:

新建一个空白模板:

设置API为24的模板项目:
初始化项目,自动下载相关依赖:

完整代码:
// 6 tabs: 卡牌/系列/对战/交易/排行/我的 · 5弹框: 购买(抽屉)/出售(居中)/编辑卡组(抽屉)/删除卡牌(居中)/对战记录(居中)
// 合规:无Blank、Button无文字、constraintSize、SLIDE、interface全覆盖、无UI变量声明
// ============ 配色 ============
interface ColorPalette109 {
primary: string;
primaryLight: string;
primaryDark: string;
accent: string;
accentLight: string;
bg: string;
card: string;
textMain: string;
textSub: string;
textHint: string;
border: string;
success: string;
warning: string;
danger: string;
gold: string;
white: string;
}
const COLORS109: ColorPalette109 = {
primary: '#0277BD',
primaryLight: '#4FC3F7',
primaryDark: '#01579B',
accent: '#FFD700',
accentLight: '#FFE082',
bg: '#E1F5FE',
card: '#FFFFFF',
textMain: '#01579B',
textSub: '#0288D1',
textHint: '#81D4FA',
border: '#B3E5FC',
success: '#66BB6A',
warning: '#FFA726',
danger: '#EF5350',
gold: '#FFB300',
white: '#FFFFFF'
};
// ============ 数据接口 ============
interface CardItem109 {
id: string;
name: string;
series: string;
rarity: string;
price: number;
oldPrice: number;
sales: number;
atk: number;
def: number;
hp: number;
emoji: string;
tag: string;
}
interface SeriesMeta109 {
id: string;
name: string;
desc: string;
total: number;
ultra: number;
secret: number;
releaseDate: string;
emoji: string;
color1: string;
color2: string;
}
interface BattleRecord109 {
id: string;
opponent: string;
result: string;
deck: string;
turns: number;
time: string;
score: number;
}
interface TradeItem109 {
id: string;
name: string;
rarity: string;
price: number;
seller: string;
city: string;
emoji: string;
type: string;
}
interface RankItem109 {
id: string;
rank: number;
name: string;
score: number;
winRate: number;
battles: number;
emoji: string;
}
interface TopicMeta109 {
id: string;
name: string;
count: number;
color: string;
}
interface CommunityPost109 {
id: string;
user: string;
time: string;
title: string;
content: string;
likes: number;
tag: string;
tagColor: string;
}
interface DeckCard109 {
id: string;
name: string;
type: string;
count: number;
emoji: string;
color: string;
}
interface OrderItem109 {
id: string;
name: string;
price: number;
status: string;
time: string;
}
interface FavCard109 {
id: string;
name: string;
rarity: string;
price: number;
emoji: string;
}
interface BarItem109 {
id: string;
name: string;
value: number;
color: string;
}
interface StatCard109 {
id: string;
label: string;
value: string;
emoji: string;
color: string;
}
interface CatEntry109 {
id: string;
name: string;
emoji: string;
color: string;
}
// ============ 数据 ============
const CAT_ENTRIES_109: CatEntry109[] = [
{ id: 'c1', name: '龙族', emoji: '🐉', color: '#D32F2F' },
{ id: 'c2', name: '魔法师', emoji: '🧙', color: '#7B1FA2' },
{ id: 'c3', name: '战士', emoji: '⚔️', color: '#0277BD' },
{ id: 'c4', name: '机械', emoji: '🤖', color: '#37474F' },
{ id: 'c5', name: '天使', emoji: '😇', color: '#FFD700' },
{ id: 'c6', name: '恶魔', emoji: '😈', color: '#4A148C' },
{ id: 'c7', name: '兽族', emoji: '🐺', color: '#2E7D32' },
{ id: 'c8', name: '限定', emoji: '💎', color: '#E91E63' }
];
const CARDS_109: CardItem109[] = [
{ id: 'cd1', name: '青眼白龙·极龙', series: '龙族传说', rarity: 'UR', price: 128, oldPrice: 168, sales: 2345, atk: 3000, def: 2500, hp: 3000, emoji: '🐉', tag: '传说' },
{ id: 'cd2', name: '黑魔术师·少女', series: '魔法师的试炼', rarity: 'SR', price: 68, oldPrice: 88, sales: 4567, atk: 2000, def: 1700, hp: 2000, emoji: '🧙', tag: '热卖' },
{ id: 'cd3', name: '暗黑骑士·统帅', series: '钢铁军团', rarity: 'SR', price: 55, oldPrice: 78, sales: 3456, atk: 2300, def: 1900, hp: 2100, emoji: '⚔️', tag: '新品' },
{ id: 'cd4', name: '机甲神·零式', series: '机械纪元', rarity: 'UR', price: 158, oldPrice: 198, sales: 1234, atk: 2800, def: 2400, hp: 2700, emoji: '🤖', tag: '限定' },
{ id: 'cd5', name: '光明使者·大天使', series: '圣光降临', rarity: 'SSR', price: 298, oldPrice: 388, sales: 678, atk: 3200, def: 2800, hp: 3500, emoji: '😇', tag: '隐藏' },
{ id: 'cd6', name: '深渊魔王·路西法', series: '暗黑降临', rarity: 'SSR', price: 328, oldPrice: 428, sales: 567, atk: 3500, def: 2600, hp: 3800, emoji: '😈', tag: '隐藏' },
{ id: 'cd7', name: '银狼·月之守护', series: '野兽传说', rarity: 'SR', price: 48, oldPrice: 68, sales: 5678, atk: 2100, def: 1800, hp: 2000, emoji: '🐺', tag: '热卖' },
{ id: 'cd8', name: '红莲龙·爆裂', series: '龙族传说', rarity: 'UR', price: 98, oldPrice: 128, sales: 2345, atk: 2800, def: 2300, hp: 2900, emoji: '🐲', tag: '传说' },
{ id: 'cd9', name: '时之魔法师', series: '魔法师的试炼', rarity: 'R', price: 18, oldPrice: 28, sales: 8901, atk: 1200, def: 1000, hp: 1500, emoji: '⏰', tag: '普通' },
{ id: 'cd10', name: '钢铁巨兵·改', series: '钢铁军团', rarity: 'R', price: 22, oldPrice: 32, sales: 6789, atk: 1500, def: 1800, hp: 2000, emoji: '🛡️', tag: '普通' },
{ id: 'cd11', name: '圣剑·誓约胜利', series: '圣光降临', rarity: 'UR', price: 118, oldPrice: 158, sales: 1567, atk: 2600, def: 2000, hp: 2500, emoji: '🗡️', tag: '限定' },
{ id: 'cd12', name: '暗影刺客·疾风', series: '暗黑降临', rarity: 'SR', price: 58, oldPrice: 78, sales: 3456, atk: 2200, def: 1500, hp: 1900, emoji: '🥷', tag: '新品' }
];
const SERIES_109: SeriesMeta109[] = [
{ id: 's1', name: '龙族传说·第一弹', desc: '20种+3隐藏·传说龙族登场', total: 23, ultra: 3, secret: 1, releaseDate: '2026-03', emoji: '🐉', color1: '#B71C1C', color2: '#FFD700' },
{ id: 's2', name: '魔法师的试炼', desc: '18种+2隐藏·魔法世界', total: 20, ultra: 2, secret: 1, releaseDate: '2026-04', emoji: '🧙', color1: '#4A148C', color2: '#E1BEE7' },
{ id: 's3', name: '钢铁军团', desc: '15种+3隐藏·机械时代', total: 18, ultra: 3, secret: 0, releaseDate: '2026-05', emoji: '🤖', color1: '#37474F', color2: '#90A4AE' },
{ id: 's4', name: '圣光降临', desc: '16种+2隐藏·天使降临', total: 18, ultra: 2, secret: 1, releaseDate: '2026-06', emoji: '😇', color1: '#FF6F00', color2: '#FFE082' },
{ id: 's5', name: '暗黑降临', desc: '14种+3隐藏·深渊觉醒', total: 17, ultra: 3, secret: 1, releaseDate: '2026-07', emoji: '😈', color1: '#311B92', color2: '#7E57C2' },
{ id: 's6', name: '野兽传说', desc: '20种+2隐藏·野性觉醒', total: 22, ultra: 2, secret: 0, releaseDate: '2026-08', emoji: '🐺', color1: '#1B5E20', color2: '#A5D6A7' }
];
const BATTLES_109: BattleRecord109[] = [
{ id: 'b1', opponent: '决斗者小明', result: '胜利', deck: '龙族传说', turns: 8, time: '10分钟前', score: 120 },
{ id: 'b2', opponent: '魔法师小红', result: '失败', deck: '魔法试炼', turns: 12, time: '1小时前', score: 0 },
{ id: 'b3', opponent: '钢铁之心', result: '胜利', deck: '钢铁军团', turns: 6, time: '2小时前', score: 150 },
{ id: 'b4', opponent: '暗影刺客', result: '胜利', deck: '暗黑降临', turns: 10, time: '3小时前', score: 100 },
{ id: 'b5', opponent: '圣光守护者', result: '失败', deck: '圣光降临', turns: 15, time: '5小时前', score: 0 },
{ id: 'b6', opponent: '野兽之王', result: '胜利', deck: '野兽传说', turns: 7, time: '1天前', score: 130 },
{ id: 'b7', opponent: '龙骑士', result: '胜利', deck: '龙族传说', turns: 9, time: '2天前', score: 110 },
{ id: 'b8', opponent: '时间操控者', result: '失败', deck: '魔法试炼', turns: 14, time: '3天前', score: 0 }
];
const TRADES_109: TradeItem109[] = [
{ id: 't1', name: '青眼白龙·极龙', rarity: 'UR', price: 128, seller: '龙族收藏家', city: '上海', emoji: '🐉', type: '出售' },
{ id: 't2', name: '光明使者·大天使', rarity: 'SSR', price: 298, seller: '圣光骑士', city: '北京', emoji: '😇', type: '出售' },
{ id: 't3', name: '深渊魔王·路西法', rarity: 'SSR', price: 328, seller: '暗黑领主', city: '广州', emoji: '😈', type: '出售' },
{ id: 't4', name: '机甲神·零式', rarity: 'UR', price: 158, seller: '机械师', city: '深圳', emoji: '🤖', type: '求购' },
{ id: 't5', name: '红莲龙·爆裂', rarity: 'UR', price: 98, seller: '火焰操控者', city: '成都', emoji: '🐲', type: '出售' },
{ id: 't6', name: '圣剑·誓约胜利', rarity: 'UR', price: 118, seller: '剑士王', city: '杭州', emoji: '🗡️', type: '求购' },
{ id: 't7', name: '暗影刺客·疾风', rarity: 'SR', price: 58, seller: '影之刃', city: '南京', emoji: '🥷', type: '出售' },
{ id: 't8', name: '银狼·月之守护', rarity: 'SR', price: 48, seller: '月之猎人', city: '武汉', emoji: '🐺', type: '出售' }
];
const RANKS_109: RankItem109[] = [
{ id: 'r1', rank: 1, name: '决斗王·龙骑士', score: 9876, winRate: 89, battles: 156, emoji: '👑' },
{ id: 'r2', rank: 2, name: '魔法大师·红', score: 8765, winRate: 85, battles: 142, emoji: '🥈' },
{ id: 'r3', rank: 3, name: '钢铁之心', score: 7654, winRate: 82, battles: 134, emoji: '🥉' },
{ id: 'r4', rank: 4, name: '暗影刺客', score: 6543, winRate: 78, battles: 128, emoji: '🎖️' },
{ id: 'r5', rank: 5, name: '圣光守护者', score: 5432, winRate: 75, battles: 120, emoji: '🎖️' },
{ id: 'r6', rank: 6, name: '野兽之王', score: 4321, winRate: 71, battles: 112, emoji: '🎖️' },
{ id: 'r7', rank: 7, name: '时间操控者', score: 3210, winRate: 68, battles: 105, emoji: '🎖️' },
{ id: 'r8', rank: 8, name: '深渊领主', score: 2109, winRate: 65, battles: 98, emoji: '🎖️' },
{ id: 'r9', rank: 9, name: '龙族收藏家', score: 1098, winRate: 62, battles: 90, emoji: '🎖️' },
{ id: 'r10', rank: 10, name: '火焰操控者', score: 987, winRate: 58, battles: 85, emoji: '🎖️' }
];
const DECK_CARDS_109: DeckCard109[] = [
{ id: 'd1', name: '青眼白龙·极龙', type: '怪兽', count: 3, emoji: '🐉', color: '#D32F2F' },
{ id: 'd2', name: '黑魔术师·少女', type: '魔法师', count: 2, emoji: '🧙', color: '#7B1FA2' },
{ id: 'd3', name: '暗黑骑士·统帅', type: '战士', count: 2, emoji: '⚔️', color: '#0277BD' },
{ id: 'd4', name: '红莲龙·爆裂', type: '怪兽', count: 1, emoji: '🐲', color: '#FF6F00' },
{ id: 'd5', name: '圣剑·誓约胜利', type: '装备', count: 1, emoji: '🗡️', color: '#FFD700' },
{ id: 'd6', name: '时之魔法师', type: '魔法', count: 1, emoji: '⏰', color: '#4FC3F7' }
];
const ORDERS_109: OrderItem109[] = [
{ id: 'o1', name: '青眼白龙·极龙×3', price: 384, status: '已发货', time: '2026-08-20' },
{ id: 'o2', name: '龙族传说整盒×1', price: 288, status: '待发货', time: '2026-08-22' },
{ id: 'o3', name: '黑魔术师×5', price: 340, status: '已完成', time: '2026-08-15' },
{ id: 'o4', name: '红莲龙×2', price: 196, status: '已取消', time: '2026-08-10' },
{ id: 'o5', name: '机甲神·零式×1', price: 158, status: '已发货', time: '2026-08-18' }
];
const FAVS_109: FavCard109[] = [
{ id: 'f1', name: '光明使者·大天使', rarity: 'SSR', price: 298, emoji: '😇' },
{ id: 'f2', name: '深渊魔王·路西法', rarity: 'SSR', price: 328, emoji: '😈' },
{ id: 'f3', name: '机甲神·零式', rarity: 'UR', price: 158, emoji: '🤖' },
{ id: 'f4', name: '圣剑·誓约胜利', rarity: 'UR', price: 118, emoji: '🗡️' }
];
const STATS_109: StatCard109[] = [
{ id: 's1', label: '胜率', value: '76%', emoji: '🏆', color: '#FFD700' },
{ id: 's2', label: '胜场', value: '42', emoji: '✅', color: '#4CAF50' },
{ id: 's3', label: '败场', value: '13', emoji: '❌', color: '#EF5350' },
{ id: 's4', label: '积分', value: '5432', emoji: '⭐', color: '#0277BD' }
];
const TOPICS_109: TopicMeta109[] = [
{ id: 't1', name: '卡组构筑', count: 567, color: '#0277BD' },
{ id: 't2', name: '稀有卡交流', count: 345, color: '#FFD700' },
{ id: 't3', name: '对战攻略', count: 234, color: '#D32F2F' },
{ id: 't4', name: '新手问答', count: 456, color: '#4CAF50' }
];
const POSTS_109: CommunityPost109[] = [
{ id: 'p1', user: '龙骑士', time: '2小时前', title: '龙族传说卡组最强构筑', content: '三张青眼白龙+红莲龙,进攻型卡组胜率85%', likes: 234, tag: '攻略', tagColor: '#0277BD' },
{ id: 'p2', user: '魔法师红', time: '4小时前', title: '黑魔术师卡组实战心得', content: '魔法陷阱配合是关键,记住每张卡的效果', likes: 345, tag: '心得', tagColor: '#FFD700' },
{ id: 'p3', user: '钢铁之心', time: '6小时前', title: '机械卡组防守反击', content: '先防守后反击,机械卡组后期能力强', likes: 167, tag: '攻略', tagColor: '#D32F2F' },
{ id: 'p4', user: '圣光守护', time: '10小时前', title: '新手如何选择初始卡组', content: '推荐龙族传说,攻击力高容易上手', likes: 89, tag: '问答', tagColor: '#4CAF50' },
{ id: 'p5', user: '暗影刺客', time: '1天前', title: 'SSR隐藏卡获取技巧', content: '整盒购买概率最高,隐藏卡约3%', likes: 456, tag: '技巧', tagColor: '#FFD700' }
];
const WEEK_BARS_109: BarItem109[] = [
{ id: 'w1', name: '一', value: 3, color: '#4FC3F7' },
{ id: 'w2', name: '二', value: 5, color: '#4FC3F7' },
{ id: 'w3', name: '三', value: 2, color: '#4FC3F7' },
{ id: 'w4', name: '四', value: 7, color: '#0277BD' },
{ id: 'w5', name: '五', value: 4, color: '#0277BD' },
{ id: 'w6', name: '六', value: 8, color: '#FFD700' },
{ id: 'w7', name: '日', value: 6, color: '#FFD700' }
];
// ============ 工具函数 ============
function getRarityColor109(rarity: string): string {
if (rarity === 'SSR') return '#D32F2F';
if (rarity === 'UR') return '#FF6F00';
if (rarity === 'SR') return '#1565C0';
return '#78909C';
}
function getRarityBg109(rarity: string): string {
if (rarity === 'SSR') return '#FFEBEE';
if (rarity === 'UR') return '#FFF3E0';
if (rarity === 'SR') return '#E3F2FD';
return '#ECEFF1';
}
function getResultColor109(result: string): string {
if (result === '胜利') return '#43A047';
return '#EF5350';
}
function getStatusColor109(status: string): string {
if (status === '已发货') return '#1976D2';
if (status === '待发货') return '#FF6F00';
if (status === '已完成') return '#43A047';
return '#9E9E9E';
}
function getRankColor109(rank: number): string {
if (rank <= 3) return '#FFD700';
if (rank <= 6) return '#9E9E9E';
return '#CD7F32';
}
// ============ 入口 ============
@Entry
@Component
struct DuoDuoCardApp {
@State currentTab: number = 0;
@State showBuySheet: boolean = false;
@State showSellDialog: boolean = false;
@State showDeckSheet: boolean = false;
@State showDeleteDialog: boolean = false;
@State showBattleDialog: boolean = false;
@State selectedCard: CardItem109 | null = null;
@State selQty: number = 1;
@State selBooster: boolean = false;
@State selInsurance: boolean = false;
@State sellPrice: number = 0;
@State selDeckSlot: number = 0;
build() {
Column() {
// ===== 游戏风头部 =====
Row() {
Text('卡牌对决')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.gold)
Text('').layoutWeight(1)
Text('🔍')
.fontSize(22)
.fontColor(COLORS109.gold)
.margin({ right: 12 })
Text('🎒')
.fontSize(22)
.fontColor(COLORS109.gold)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.linearGradient({ angle: 90, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })
// ===== Tab 内容区 =====
if (this.currentTab === 0) {
CardShopTab109({
onBuy: (c: CardItem109) => {
this.selectedCard = c;
this.showBuySheet = true;
},
onDetail: (c: CardItem109) => {
this.selectedCard = c;
this.showSellDialog = true;
}
})
} else if (this.currentTab === 1) {
} else if (this.currentTab === 2) {
BattleTab109({
onRecord: () => { this.showBattleDialog = true; }
})
} else if (this.currentTab === 3) {
} else if (this.currentTab === 4) {
} else {
MyCardTab109({
onDeck: () => { this.showDeckSheet = true; },
onDelete: () => { this.showDeleteDialog = true; }
})
}
// ===== 底部6 Tab =====
Row() {
TabBtn109({icon:'🃏', label:'卡牌', active: this.currentTab === 0, onTap: () => { this.currentTab = 0; }})
TabBtn109({icon:'📦', label:'系列', active: this.currentTab === 1, onTap: () => { this.currentTab = 1; }})
TabBtn109({icon:'⚔️', label:'对战', active: this.currentTab === 2, onTap: () => { this.currentTab = 2; }})
TabBtn109({icon:'🔄', label:'交易', active: this.currentTab === 3, onTap: () => { this.currentTab = 3; }})
TabBtn109({icon:'🏆', label:'排行', active: this.currentTab === 4, onTap: () => { this.currentTab = 4; }})
TabBtn109({icon:'👤', label:'我的', active: this.currentTab === 5, onTap: () => { this.currentTab = 5; }})
}
.width('100%')
.height(56)
.backgroundColor(COLORS109.card)
.border({ width: 1, color: COLORS109.border })
.justifyContent(FlexAlign.SpaceAround)
}
.width('100%')
.height('100%')
.backgroundColor(COLORS109.bg)
}
}
// ============ 底部 Tab(顶部金边样式) ============
@Component
struct TabBtn109 {
icon: string = '🃏';
label: string = '';
active: boolean = false;
onTap: () => void = () => {};
build() {
Column() {
if (this.active) {
Row()
.width('100%')
.height(3)
.backgroundColor(COLORS109.gold)
.margin({ bottom: 4 })
} else {
Row().width('100%').height(3).backgroundColor(COLORS109.card).margin({ bottom: 4 })
}
Text(this.icon)
.fontSize(20)
.fontColor(this.active ? COLORS109.primary : COLORS109.textHint)
Text(this.label)
.fontSize(10)
.fontColor(this.active ? COLORS109.primary : COLORS109.textHint)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.onClick(() => { this.onTap(); })
}
}
// ============ Tab0:卡牌商店(大卡+网格风) ============
@Component
struct CardShopTab109 {
onBuy: (c: CardItem109) => void = () => {};
onDetail: (c: CardItem109) => void = () => {};
build() {
Scroll() {
Column() {
// 搜索框
Row() {
Text('🔍 搜索卡牌、系列...')
.fontSize(13)
.fontColor(COLORS109.textHint)
.layoutWeight(1)
}
.width('92%')
.height(36)
.backgroundColor(COLORS109.card)
.borderRadius(18)
.padding({ left: 16, right: 16 })
.margin({ top: 12, bottom: 8 })
// 横幅
Row() {
Text('🎴 新系列「野兽传说」上架 · 限时折扣')
.fontSize(11)
.fontColor(COLORS109.gold)
.layoutWeight(1)
}
.width('100%')
.height(28)
.backgroundColor(COLORS109.primaryDark)
.justifyContent(FlexAlign.Center)
// 金刚区
Grid() {
ForEach(CAT_ENTRIES_109, (cat: CatEntry109) => {
GridItem() {
Column() {
Column() {
Text(cat.emoji)
.fontSize(24)
}
.width(44)
.height(44)
.backgroundColor(cat.color)
.borderRadius(22)
.justifyContent(FlexAlign.Center)
Text(cat.name)
.fontSize(9)
.fontColor(COLORS109.textSub)
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Center)
}
}, (cat: CatEntry109) => cat.id)
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsGap(8)
.columnsGap(8)
.width('92%')
.margin({ top: 12 })
.backgroundColor(COLORS109.card)
.borderRadius(12)
.padding(12)
// 本周对战统计
Row() {
Text('⚔️ 本周对战统计')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
}
.width('92%')
.margin({ top: 16, bottom: 8 })
Row() {
ForEach(STATS_109, (s: StatCard109) => {
Column() {
Text(s.emoji)
.fontSize(18)
Text(s.value)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(s.color)
.margin({ top: 2 })
Text(s.label)
.fontSize(9)
.fontColor(COLORS109.textSub)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (s: StatCard109) => s.id)
}
.width('92%')
.padding({ top: 12, bottom: 12 })
.backgroundColor(COLORS109.card)
.borderRadius(12)
.margin({ bottom: 12 })
// 热卖卡牌横滚
Row() {
Text('🔥 热卖卡牌')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
Text('更多 >')
.fontSize(12)
.fontColor(COLORS109.primary)
}
.width('92%')
.margin({ bottom: 8 })
Scroll() {
Row() {
ForEach(CARDS_109.slice(0, 6), (c: CardItem109) => {
Column() {
Stack({ alignContent: Alignment.TopEnd }) {
Column() {
Text(c.emoji)
.fontSize(36)
}
.width(72)
.height(72)
.backgroundColor(getRarityBg109(c.rarity))
.borderRadius(12)
.justifyContent(FlexAlign.Center)
Text(c.rarity)
.fontSize(8)
.fontColor(COLORS109.white)
.backgroundColor(getRarityColor109(c.rarity))
.borderRadius(3)
.padding({ left: 3, right: 3, top: 1, bottom: 1 })
.margin({ top: 3, right: 3 })
}
Text(c.name)
.fontSize(10)
.fontColor(COLORS109.textMain)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width(72)
.margin({ top: 4 })
Text('ATK ' + c.atk + ' / DEF ' + c.def)
.fontSize(8)
.fontColor(COLORS109.textSub)
Row() {
Text('¥' + c.price)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
Text('¥' + c.oldPrice)
.fontSize(9)
.fontColor(COLORS109.textHint)
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 4 })
}
.margin({ top: 2 })
}
.width(88)
.margin({ right: 8 })
.onClick(() => { this.onDetail(c); })
}, (c: CardItem109) => c.id)
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 12 })
// 全部卡牌网格
Row() {
Text('🃏 全部卡牌')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
}
.width('92%')
.margin({ bottom: 8 })
Grid() {
ForEach(CARDS_109, (c: CardItem109) => {
GridItem() {
Column() {
Stack({ alignContent: Alignment.TopEnd }) {
Column() {
Text(c.emoji)
.fontSize(40)
}
.width('100%')
.height(80)
.backgroundColor(getRarityBg109(c.rarity))
.borderRadius(12)
.justifyContent(FlexAlign.Center)
Text(c.rarity)
.fontSize(8)
.fontColor(COLORS109.white)
.backgroundColor(getRarityColor109(c.rarity))
.borderRadius(3)
.padding({ left: 3, right: 3, top: 1, bottom: 1 })
.margin({ top: 3, right: 3 })
}
Text(c.name)
.fontSize(11)
.fontColor(COLORS109.textMain)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 6 })
Row() {
Text('ATK')
.fontSize(8)
.fontColor(COLORS109.textHint)
Text(c.atk.toString())
.fontSize(9)
.fontColor(COLORS109.danger)
.margin({ left: 2 })
Text('DEF')
.fontSize(8)
.fontColor(COLORS109.textHint)
.margin({ left: 4 })
Text(c.def.toString())
.fontSize(9)
.fontColor(COLORS109.primary)
.margin({ left: 2 })
}
.margin({ top: 2 })
Row() {
Text('¥' + c.price)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
Text('¥' + c.oldPrice)
.fontSize(9)
.fontColor(COLORS109.textHint)
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 4 })
Text('').layoutWeight(1)
Text('销' + c.sales)
.fontSize(8)
.fontColor(COLORS109.textHint)
}
.width('100%')
.margin({ top: 4 })
Row() {
Text('购买')
.fontSize(11)
.fontColor(COLORS109.white)
.backgroundColor(COLORS109.primary)
.borderRadius(6)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.layoutWeight(1)
.textAlign(TextAlign.Center)
}
.width('100%')
.margin({ top: 6 })
.onClick(() => { this.onBuy(c); })
}
.padding(10)
.backgroundColor(COLORS109.card)
.borderRadius(12)
}
}, (c: CardItem109) => c.id)
}
.columnsTemplate('1fr 1fr')
.rowsGap(8)
.columnsGap(8)
.width('92%')
.margin({ bottom: 16 })
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.constraintSize({ maxHeight: '80%' })
}
}
// ============ Tab1:系列(时间线风) ============
@Component
struct SeriesTab109 {
build() {
Scroll() {
Column() {
Column() {
Text('📦 卡牌系列')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.white)
Text('收集图鉴·全系列收录')
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: 20, bottom: 20 })
.linearGradient({ angle: 90, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })
ForEach(SERIES_109, (s: SeriesMeta109) => {
Column() {
Row() {
Column() {
Text(s.emoji)
.fontSize(36)
}
.width(64)
.height(64)
.linearGradient({ angle: 135, colors: [[s.color1, 0], [s.color2, 1]] })
.borderRadius(12)
.justifyContent(FlexAlign.Center)
Column() {
Text(s.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Text(s.desc)
.fontSize(11)
.fontColor(COLORS109.textSub)
.margin({ top: 4 })
Row() {
Text('共' + s.total + '款')
.fontSize(10)
.fontColor(COLORS109.textHint)
Text('UR×' + s.ultra)
.fontSize(10)
.fontColor(getRarityColor109('UR'))
.margin({ left: 8 })
if (s.secret > 0) {
Text('Secret×' + s.secret)
.fontSize(10)
.fontColor(getRarityColor109('SSR'))
.margin({ left: 8 })
}
Text(s.releaseDate)
.fontSize(10)
.fontColor(COLORS109.textHint)
.margin({ left: 8 })
}
.margin({ top: 4 })
}
.margin({ left: 12 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
Row() {
Text('购买整盒')
.fontSize(11)
.fontColor(COLORS109.white)
.backgroundColor(COLORS109.primary)
.borderRadius(14)
.padding({ left: 16, right: 16, top: 4, bottom: 4 })
Text('').layoutWeight(1)
Text('查看图鉴')
.fontSize(11)
.fontColor(COLORS109.primary)
.backgroundColor(COLORS109.bg)
.borderRadius(14)
.padding({ left: 16, right: 16, top: 4, bottom: 4 })
}
.width('100%')
.margin({ top: 10 })
}
.width('92%')
.padding(14)
.backgroundColor(COLORS109.card)
.borderRadius(12)
.margin({ left: 12, right: 12, top: 6, bottom: 6 })
}, (s: SeriesMeta109) => s.id)
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.constraintSize({ maxHeight: '80%' })
}
}
// ============ Tab2:对战(竞技场风) ============
@Component
struct BattleTab109 {
onRecord: () => void = () => {};
build() {
Scroll() {
Column() {
Column() {
Text('⚔️ 对战竞技场')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.gold)
Text('实时对战·排名攀升')
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: 24, bottom: 24 })
.linearGradient({ angle: 135, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })
// 战绩概览
Row() {
ForEach(STATS_109, (s: StatCard109) => {
Column() {
Text(s.emoji)
.fontSize(20)
Text(s.value)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(s.color)
.margin({ top: 2 })
Text(s.label)
.fontSize(9)
.fontColor(COLORS109.textSub)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (s: StatCard109) => s.id)
}
.width('92%')
.padding({ top: 12, bottom: 12 })
.backgroundColor(COLORS109.card)
.borderRadius(12)
.margin({ top: 12 })
// 本周对战柱状图
Row() {
Text('📊 本周对战次数')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
}
.width('92%')
.margin({ top: 16, bottom: 8 })
Row() {
ForEach(WEEK_BARS_109, (bar: BarItem109) => {
Column() {
Text(bar.value.toString())
.fontSize(9)
.fontColor(bar.color)
Column() {
Text('')
.width('100%')
.height(bar.value * 10)
.backgroundColor(bar.color)
.borderRadius(3)
}
.width(24)
.height(80)
.justifyContent(FlexAlign.End)
.margin({ top: 4, bottom: 4 })
Text(bar.name)
.fontSize(9)
.fontColor(COLORS109.textSub)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (bar: BarItem109) => bar.id)
}
.width('92%')
.padding(16)
.backgroundColor(COLORS109.card)
.borderRadius(12)
.margin({ bottom: 12 })
// 对战记录
Row() {
Text('📜 对战记录')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
Text('查看全部')
.fontSize(11)
.fontColor(COLORS109.primary)
.onClick(() => { this.onRecord(); })
}
.width('92%')
.margin({ bottom: 8 })
ForEach(BATTLES_109, (b: BattleRecord109) => {
Row() {
Column() {
Text(b.result === '胜利' ? '🏆' : '💀')
.fontSize(20)
}
.width(40)
.height(40)
.backgroundColor(b.result === '胜利' ? '#E8F5E9' : '#FFEBEE')
.borderRadius(20)
.justifyContent(FlexAlign.Center)
Column() {
Text('vs ' + b.opponent)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Row() {
Text(b.result)
.fontSize(9)
.fontColor(getResultColor109(b.result))
Text(b.deck)
.fontSize(9)
.fontColor(COLORS109.textSub)
.margin({ left: 6 })
Text(b.turns + '回合')
.fontSize(9)
.fontColor(COLORS109.textHint)
.margin({ left: 6 })
}
.margin({ top: 2 })
Text(b.time)
.fontSize(9)
.fontColor(COLORS109.textHint)
.margin({ top: 2 })
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('+' + b.score)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(b.score > 0 ? COLORS109.gold : COLORS109.textHint)
}
.width('92%')
.padding(10)
.backgroundColor(COLORS109.card)
.borderRadius(10)
.margin({ left: 12, right: 12, bottom: 6 })
}, (b: BattleRecord109) => b.id)
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.constraintSize({ maxHeight: '80%' })
}
}
// ============ Tab3:交易(市场风) ============
@Component
struct TradeTab109 {
build() {
Scroll() {
Column() {
Column() {
Text('🔄 交易市场')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.white)
Text('安全交易·平台担保')
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: 20, bottom: 20 })
.backgroundColor(COLORS109.primaryDark)
ForEach(TRADES_109, (t: TradeItem109) => {
Row() {
Column() {
Text(t.emoji)
.fontSize(28)
}
.width(52)
.height(52)
.backgroundColor(getRarityBg109(t.rarity))
.borderRadius(12)
.justifyContent(FlexAlign.Center)
Column() {
Text(t.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Row() {
Text(t.rarity)
.fontSize(9)
.fontColor(getRarityColor109(t.rarity))
.backgroundColor(getRarityBg109(t.rarity))
.borderRadius(4)
.padding({ left: 4, right: 4, top: 1, bottom: 1 })
Text(t.type)
.fontSize(9)
.fontColor(t.type === '出售' ? COLORS109.primary : COLORS109.danger)
.margin({ left: 6 })
}
.margin({ top: 4 })
Text(t.seller + ' · ' + t.city)
.fontSize(9)
.fontColor(COLORS109.textHint)
.margin({ top: 2 })
}
.margin({ left: 10 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Column() {
Text('¥' + t.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
Text('交易')
.fontSize(11)
.fontColor(COLORS109.white)
.backgroundColor(COLORS109.primary)
.borderRadius(14)
.padding({ left: 14, right: 14, top: 4, bottom: 4 })
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.End)
}
.width('92%')
.padding(12)
.backgroundColor(COLORS109.card)
.borderRadius(12)
.margin({ left: 12, right: 12, bottom: 8 })
}, (t: TradeItem109) => t.id)
// 社区帖子
Row() {
Text('💬 卡牌社区')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
}
.width('92%')
.margin({ top: 16, bottom: 8 })
Scroll() {
Row() {
ForEach(TOPICS_109, (t: TopicMeta109) => {
Column() {
Text('#' + t.name)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(t.color)
Text(t.count + ' 帖')
.fontSize(8)
.fontColor(COLORS109.textHint)
.margin({ top: 2 })
}
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.backgroundColor(COLORS109.card)
.borderRadius(8)
.margin({ right: 6 })
}, (t: TopicMeta109) => t.id)
}
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 8 })
ForEach(POSTS_109, (p: CommunityPost109) => {
Column() {
Row() {
Column() {
Text(p.user.substring(0, 1))
.fontSize(14)
.fontColor(COLORS109.white)
.fontWeight(FontWeight.Bold)
}
.width(32)
.height(32)
.backgroundColor(COLORS109.primary)
.borderRadius(16)
.justifyContent(FlexAlign.Center)
Column() {
Text(p.user)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Text(p.time)
.fontSize(9)
.fontColor(COLORS109.textHint)
.margin({ top: 1 })
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text(p.tag)
.fontSize(8)
.fontColor(p.tagColor)
.backgroundColor(COLORS109.bg)
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
}
.width('100%')
Text(p.title)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.margin({ top: 6 })
Text(p.content)
.fontSize(11)
.fontColor(COLORS109.textSub)
.margin({ top: 4 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text('👍 ' + p.likes)
.fontSize(10)
.fontColor(COLORS109.textSub)
Text('💬')
.fontSize(10)
.fontColor(COLORS109.textSub)
.margin({ left: 16 })
}
.width('100%')
.margin({ top: 6 })
}
.width('92%')
.padding(10)
.backgroundColor(COLORS109.card)
.borderRadius(10)
.margin({ left: 12, right: 12, bottom: 6 })
}, (p: CommunityPost109) => p.id)
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.constraintSize({ maxHeight: '80%' })
}
}
// ============ Tab4:排行(排行榜风) ============
@Component
struct RankTab109 {
build() {
Scroll() {
Column() {
Column() {
Text('🏆 决斗者排行')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.gold)
Text('本周最强决斗者')
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: 24, bottom: 24 })
.linearGradient({ angle: 135, colors: [[COLORS109.primaryDark, 0], [COLORS109.gold, 1]] })
// 前三领奖台
Row() {
Column() {
Text('🥈')
.fontSize(28)
Column() {
Text(RANKS_109[1].name)
.fontSize(11)
.fontColor(COLORS109.textMain)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(RANKS_109[1].score.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#9E9E9E')
}
.width(72)
.height(80)
.backgroundColor('#F5F5F5')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.padding(8)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.margin({ top: 12 })
Column() {
Text('🥇')
.fontSize(36)
Column() {
Text(RANKS_109[0].name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(RANKS_109[0].score.toString())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.gold)
}
.width(80)
.height(100)
.backgroundColor('#FFF8E1')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.padding(8)
.border({ width: 2, color: COLORS109.gold })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Column() {
Text('🥉')
.fontSize(28)
Column() {
Text(RANKS_109[2].name)
.fontSize(11)
.fontColor(COLORS109.textMain)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(RANKS_109[2].score.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#CD7F32')
}
.width(72)
.height(72)
.backgroundColor('#FFF3E0')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.padding(8)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.margin({ top: 20 })
}
.width('92%')
.margin({ top: 12, bottom: 16 })
// 4-10名列表
ForEach(RANKS_109.slice(3), (r: RankItem109) => {
Row() {
Text(r.rank.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(getRankColor109(r.rank))
.width(32)
Column() {
Text(r.emoji + ' ' + r.name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Row() {
Text('胜率 ' + r.winRate + '%')
.fontSize(9)
.fontColor(COLORS109.success)
Text(r.battles + '场')
.fontSize(9)
.fontColor(COLORS109.textHint)
.margin({ left: 6 })
}
.margin({ top: 2 })
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text(r.score.toString())
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
}
.width('92%')
.padding(10)
.backgroundColor(COLORS109.card)
.borderRadius(10)
.margin({ left: 12, right: 12, bottom: 6 })
}, (r: RankItem109) => r.id)
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.constraintSize({ maxHeight: '80%' })
}
}
// ============ Tab5:我的(卡牌收藏风) ============
@Component
struct MyCardTab109 {
onDeck: () => void = () => {};
onDelete: () => void = () => {};
build() {
Scroll() {
Column() {
// 个人卡
Row() {
Column() {
Text('卡')
.fontSize(24)
.fontColor(COLORS109.gold)
.fontWeight(FontWeight.Bold)
}
.width(60)
.height(60)
.linearGradient({ angle: 135, colors: [[COLORS109.primaryDark, 0], [COLORS109.primary, 1]] })
.borderRadius(30)
.justifyContent(FlexAlign.Center)
Column() {
Text('决斗者·龙骑士')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Text('Lv.15 · 钻石段位')
.fontSize(11)
.fontColor(COLORS109.textSub)
.margin({ top: 2 })
Row() {
Text('5432积分')
.fontSize(9)
.fontColor(COLORS109.gold)
.margin({ right: 8 })
Text('42胜')
.fontSize(9)
.fontColor(COLORS109.success)
}
.margin({ top: 2 })
}
.margin({ left: 12 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('>')
.fontSize(16)
.fontColor(COLORS109.textHint)
}
.width('92%')
.padding(16)
.backgroundColor(COLORS109.card)
.borderRadius(12)
.margin({ top: 12 })
// 统计
Row() {
ForEach(STATS_109, (s: StatCard109) => {
Column() {
Text(s.emoji)
.fontSize(16)
Text(s.value)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(s.color)
.margin({ top: 2 })
Text(s.label)
.fontSize(9)
.fontColor(COLORS109.textSub)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (s: StatCard109) => s.id)
}
.width('92%')
.padding({ top: 12, bottom: 12 })
.backgroundColor(COLORS109.card)
.borderRadius(12)
.margin({ top: 8 })
// 我的卡组
Row() {
Text('🎴 我的卡组')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
Text('编辑')
.fontSize(11)
.fontColor(COLORS109.primary)
.onClick(() => { this.onDeck(); })
}
.width('92%')
.margin({ top: 16, bottom: 8 })
Column() {
Row() {
Text('龙族传说卡组')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Text('10/40')
.fontSize(10)
.fontColor(COLORS109.textSub)
.margin({ left: 8 })
}
.width('100%')
Row() {
ForEach(DECK_CARDS_109, (d: DeckCard109) => {
Column() {
Text(d.emoji)
.fontSize(20)
Text('×' + d.count)
.fontSize(8)
.fontColor(d.color)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (d: DeckCard109) => d.id)
}
.width('100%')
.margin({ top: 8 })
}
.width('92%')
.padding(12)
.backgroundColor(COLORS109.card)
.borderRadius(12)
// 订单
Row() {
Text('📋 我的订单')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
}
.width('92%')
.margin({ top: 16, bottom: 8 })
ForEach(ORDERS_109, (o: OrderItem109) => {
Row() {
Text(o.name)
.fontSize(12)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('¥' + o.price)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
Text(o.status)
.fontSize(9)
.fontColor(getStatusColor109(o.status))
.margin({ left: 8 })
}
.width('100%')
.padding(10)
.backgroundColor(COLORS109.card)
.borderRadius(10)
.margin({ left: 12, right: 12, bottom: 6 })
}, (o: OrderItem109) => o.id)
// 收藏
Row() {
Text('⭐ 卡牌收藏')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
Text('删除')
.fontSize(11)
.fontColor(COLORS109.danger)
.onClick(() => { this.onDelete(); })
}
.width('92%')
.margin({ top: 16, bottom: 8 })
ForEach(FAVS_109, (f: FavCard109) => {
Row() {
Column() {
Text(f.emoji)
.fontSize(24)
}
.width(48)
.height(48)
.backgroundColor(getRarityBg109(f.rarity))
.borderRadius(10)
.justifyContent(FlexAlign.Center)
Column() {
Text(f.name)
.fontSize(12)
.fontColor(COLORS109.textMain)
Text(f.rarity)
.fontSize(9)
.fontColor(getRarityColor109(f.rarity))
.margin({ top: 2 })
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('¥' + f.price)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
}
.width('100%')
.padding(10)
.backgroundColor(COLORS109.card)
.borderRadius(10)
.margin({ left: 12, right: 12, bottom: 6 })
}, (f: FavCard109) => f.id)
Text('v2.0 · 卡牌对决 · 2026')
.fontSize(10)
.fontColor(COLORS109.textHint)
.alignSelf(ItemAlign.Center)
.margin({ top: 16, bottom: 16 })
}
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.constraintSize({ maxHeight: '80%' })
}
}
// ============ 弹框1:购买卡牌(底部抽屉) ============
@Component
struct BuyCardSheet109 {
card: CardItem109 | null = null;
selQty: number = 1;
selBooster: boolean = false;
selInsurance: boolean = false;
onQty: (q: number) => void = () => {};
onBooster: (b: boolean) => void = () => {};
onInsurance: (i: boolean) => void = () => {};
onConfirm: () => void = () => {};
onCancel: () => void = () => {};
build() {
Scroll() {
Column() {
Row() {
Column() {
Text(this.card?.emoji ?? '🃏')
.fontSize(36)
}
.width(60)
.height(60)
.backgroundColor(getRarityBg109(this.card?.rarity ?? 'R'))
.borderRadius(12)
.justifyContent(FlexAlign.Center)
Column() {
Text(this.card?.name ?? '')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Text(this.card?.series ?? '')
.fontSize(10)
.fontColor(COLORS109.textSub)
.margin({ top: 2 })
Row() {
Text(this.card?.rarity ?? '')
.fontSize(9)
.fontColor(getRarityColor109(this.card?.rarity ?? 'R'))
Text('ATK ' + (this.card?.atk ?? 0))
.fontSize(9)
.fontColor(COLORS109.danger)
.margin({ left: 8 })
Text('DEF ' + (this.card?.def ?? 0))
.fontSize(9)
.fontColor(COLORS109.primary)
.margin({ left: 8 })
}
.margin({ top: 4 })
}
.margin({ left: 12 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('✕')
.fontSize(18)
.fontColor(COLORS109.textHint)
.onClick(() => { this.onCancel(); })
}
.width('100%')
.padding(16)
Divider().color(COLORS109.border)
Row() {
Text('¥' + (this.card?.price ?? 0))
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
Text('¥' + (this.card?.oldPrice ?? 0))
.fontSize(12)
.fontColor(COLORS109.textHint)
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 6 })
Text('').layoutWeight(1)
Text('销' + (this.card?.sales ?? 0))
.fontSize(10)
.fontColor(COLORS109.textHint)
}
.width('100%')
.padding({ left: 16, right: 16, top: 12 })
// 数量
Row() {
Text('数量')
.fontSize(13)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
Text('−')
.fontSize(16)
.fontColor(COLORS109.primary)
.padding({ left: 12, right: 12 })
.onClick(() => { if (this.selQty > 1) { this.onQty(this.selQty - 1); } })
Text(this.selQty.toString())
.fontSize(14)
.fontColor(COLORS109.textMain)
.padding({ left: 12, right: 12 })
Text('+')
.fontSize(16)
.fontColor(COLORS109.primary)
.padding({ left: 12, right: 12 })
.onClick(() => { this.onQty(this.selQty + 1); })
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
// 补充包选项
Row() {
Column() {
Text('📦 购买补充包')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Text('随机5张·含UR概率UP')
.fontSize(10)
.fontColor(COLORS109.textSub)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text(this.selBooster ? '☑' : '☐')
.fontSize(20)
.fontColor(this.selBooster ? COLORS109.primary : COLORS109.textHint)
.onClick(() => { this.onBooster(!this.selBooster); })
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
// 保险
Row() {
Column() {
Text('🛡️ 卡牌保险')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Text('运输损坏包赔')
.fontSize(10)
.fontColor(COLORS109.textSub)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text(this.selInsurance ? '☑' : '☐')
.fontSize(20)
.fontColor(this.selInsurance ? COLORS109.primary : COLORS109.textHint)
.onClick(() => { this.onInsurance(!this.selInsurance); })
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 16 })
Row() {
Text('合计')
.fontSize(13)
.fontColor(COLORS109.textSub)
Text('¥' + ((this.card?.price ?? 0) * this.selQty))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.primary)
.margin({ left: 8 })
.layoutWeight(1)
Text('确认购买')
.fontSize(14)
.fontColor(COLORS109.white)
.backgroundColor(COLORS109.primary)
.borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.onClick(() => { this.onConfirm(); })
}
.width('100%')
.padding({ left: 16, right: 16, bottom: 20 })
}
.width('100%')
.backgroundColor(COLORS109.white)
}
.scrollBar(BarState.Off)
}
}
// ============ 弹框2:出售卡牌(居中) ============
@Component
struct SellCardDialog109 {
card: CardItem109 | null = null;
sellPrice: number = 0;
onPrice: (p: number) => void = () => {};
onConfirm: () => void = () => {};
onCancel: () => void = () => {};
build() {
Column() {
Column() {
Text('💰')
.fontSize(40)
.margin({ top: 24 })
Text('出售卡牌')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.margin({ top: 8 })
Row() {
Column() {
Text(this.card?.emoji ?? '🃏')
.fontSize(24)
}
.width(48)
.height(48)
.backgroundColor(getRarityBg109(this.card?.rarity ?? 'R'))
.borderRadius(10)
.justifyContent(FlexAlign.Center)
Column() {
Text(this.card?.name ?? '')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Text(this.card?.rarity ?? '')
.fontSize(9)
.fontColor(getRarityColor109(this.card?.rarity ?? 'R'))
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding({ left: 20, right: 20, top: 12 })
Text('市场参考价:¥' + (this.card?.price ?? 0))
.fontSize(11)
.fontColor(COLORS109.textSub)
.padding({ left: 20, top: 8 })
Text('出售价格')
.fontSize(12)
.fontColor(COLORS109.textSub)
.width('100%')
.padding({ left: 20, top: 12 })
TextInput({ placeholder: '输入出售价格' })
.placeholderColor(COLORS109.textHint)
.fontSize(14)
.width('85%')
.backgroundColor(COLORS109.bg)
.borderRadius(10)
.margin({ top: 8 })
.onChange((v: string) => { this.onPrice(parseInt(v) || 0); })
Row() {
Text('取消')
.fontSize(14)
.fontColor(COLORS109.textSub)
.backgroundColor(COLORS109.border)
.borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.onClick(() => { this.onCancel(); })
Text('上架出售')
.fontSize(14)
.fontColor(COLORS109.white)
.backgroundColor(COLORS109.primary)
.borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => { this.onConfirm(); })
}
.justifyContent(FlexAlign.Center)
.padding({ top: 20, bottom: 20 })
}
.width('80%')
.backgroundColor(COLORS109.white)
.borderRadius(16)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor('rgba(0,0,0,0.5)')
}
}
// ============ 弹框3:编辑卡组(底部抽屉) ============
@Component
struct EditDeckSheet109 {
selSlot: number = 0;
onSlot: (s: number) => void = () => {};
onConfirm: () => void = () => {};
onCancel: () => void = () => {};
build() {
Scroll() {
Column() {
Row() {
Text('🎴 编辑卡组')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
Text('✕')
.fontSize(18)
.fontColor(COLORS109.textHint)
.onClick(() => { this.onCancel(); })
}
.width('100%')
.padding(16)
Divider().color(COLORS109.border)
Text('当前卡组:龙族传说')
.fontSize(13)
.fontColor(COLORS109.textSub)
.width('100%')
.padding({ left: 16, top: 12 })
Text('10/40 张 · 需要添加30张')
.fontSize(11)
.fontColor(COLORS109.warning)
.width('100%')
.padding({ left: 16, top: 4 })
Text('卡牌列表')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.width('100%')
.padding({ left: 16, top: 16 })
ForEach(DECK_CARDS_109, (d: DeckCard109, idx: number) => {
Row() {
Column() {
Text(d.emoji)
.fontSize(24)
}
.width(44)
.height(44)
.backgroundColor(d.color)
.borderRadius(10)
.justifyContent(FlexAlign.Center)
Column() {
Text(d.name)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Text(d.type + ' · ×' + d.count)
.fontSize(10)
.fontColor(COLORS109.textSub)
.margin({ top: 2 })
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text(this.selSlot === idx ? '◉' : '○')
.fontSize(18)
.fontColor(this.selSlot === idx ? COLORS109.primary : COLORS109.textHint)
.onClick(() => { this.onSlot(idx); })
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
}, (d: DeckCard109) => d.id)
Row() {
Text('取消')
.fontSize(14)
.fontColor(COLORS109.textSub)
.backgroundColor(COLORS109.border)
.borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => { this.onCancel(); })
Text('保存卡组')
.fontSize(14)
.fontColor(COLORS109.white)
.backgroundColor(COLORS109.primary)
.borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.margin({ left: 12 })
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(() => { this.onConfirm(); })
}
.width('100%')
.padding({ left: 16, right: 16, top: 16, bottom: 20 })
}
.width('100%')
.backgroundColor(COLORS109.white)
}
.scrollBar(BarState.Off)
}
}
// ============ 弹框4:删除卡牌(居中) ============
@Component
struct DeleteCardDialog109 {
onConfirm: () => void = () => {};
onCancel: () => void = () => {};
build() {
Column() {
Column() {
Text('🗑️')
.fontSize(40)
.margin({ top: 24 })
Text('移除卡牌')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.margin({ top: 8 })
Text('确认从收藏中移除?')
.fontSize(12)
.fontColor(COLORS109.textSub)
.margin({ top: 4 })
Row() {
Text('取消')
.fontSize(14)
.fontColor(COLORS109.textSub)
.backgroundColor(COLORS109.border)
.borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.onClick(() => { this.onCancel(); })
Text('确认移除')
.fontSize(14)
.fontColor(COLORS109.white)
.backgroundColor(COLORS109.danger)
.borderRadius(20)
.padding({ left: 24, right: 24, top: 10, bottom: 10 })
.margin({ left: 12 })
.onClick(() => { this.onConfirm(); })
}
.justifyContent(FlexAlign.Center)
.padding({ top: 24, bottom: 24 })
}
.width('75%')
.backgroundColor(COLORS109.white)
.borderRadius(16)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor('rgba(0,0,0,0.5)')
}
}
// ============ 弹框5:对战记录(居中) ============
@Component
struct BattleRecordDialog109 {
onClose: () => void = () => {};
build() {
Column() {
Column() {
Row() {
Text('📜 对战记录')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
.layoutWeight(1)
Text('✕')
.fontSize(18)
.fontColor(COLORS109.textHint)
.onClick(() => { this.onClose(); })
}
.width('100%')
.padding(16)
Divider().color(COLORS109.border)
Scroll() {
Column() {
ForEach(BATTLES_109, (b: BattleRecord109) => {
Row() {
Column() {
Text(b.result === '胜利' ? '🏆' : '💀')
.fontSize(18)
}
.width(36)
.height(36)
.backgroundColor(b.result === '胜利' ? '#E8F5E9' : '#FFEBEE')
.borderRadius(18)
.justifyContent(FlexAlign.Center)
Column() {
Text('vs ' + b.opponent)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(COLORS109.textMain)
Row() {
Text(b.result)
.fontSize(9)
.fontColor(getResultColor109(b.result))
Text(b.deck)
.fontSize(9)
.fontColor(COLORS109.textSub)
.margin({ left: 6 })
Text(b.turns + '回合')
.fontSize(9)
.fontColor(COLORS109.textHint)
.margin({ left: 6 })
Text(b.time)
.fontSize(9)
.fontColor(COLORS109.textHint)
.margin({ left: 6 })
}
.margin({ top: 2 })
}
.margin({ left: 8 })
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Text('+' + b.score)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(b.score > 0 ? COLORS109.gold : COLORS109.textHint)
}
.width('100%')
.padding(8)
.margin({ bottom: 6 })
}, (b: BattleRecord109) => b.id)
}
.width('100%')
.padding(12)
}
.constraintSize({ maxHeight: '60%' })
Text('关闭')
.fontSize(14)
.fontColor(COLORS109.white)
.backgroundColor(COLORS109.primary)
.borderRadius(20)
.padding({ left: 32, right: 32, top: 10, bottom: 10 })
.alignSelf(ItemAlign.Center)
.margin({ top: 8, bottom: 20 })
.onClick(() => { this.onClose(); })
}
.width('85%')
.backgroundColor(COLORS109.white)
.borderRadius(16)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor('rgba(0,0,0,0.5)')
}
}
总结

本文深入解析了一款基于HarmonyOS ArkTS声明式UI范式开发的潮玩卡牌对战应用,从配色体系与数据模型设计、工具函数与入口组件架构、卡牌商店与系列图鉴布局、对战竞技场与排行榜实现、购买与出售弹框组件等维度,完整呈现了卡牌游戏类应用的开发实践。应用通过CardItem109、SeriesMeta109、BattleRecord109、RankItem109等完整的interface接口体系,将卡牌的稀有度、攻防属性、系列关联、对战记录等复杂业务数据映射为类型安全的数据模型,配合静态数据常量实现了数据与视图的彻底解耦。
在UI实现层面,应用展现了丰富的布局策略——卡牌商店采用搜索+横幅+金刚区+横滑+网格的多层信息流布局、系列图鉴采用时间线卡片+渐变头像的差异化展示、对战竞技场采用统计+柱状图+记录列表的复合布局、排行榜采用领奖台+列表的分层展示。每种布局都针对其业务场景做了专门优化,避免了千篇一律的列表式设计。稀有度颜色映射通过getRarityColor109和getRarityBg109双函数实现颜色与背景的分离管理,使得同一种稀有度在标签和背景区域获得协调的色彩表现。
从工程规范角度,应用严格遵循了"无Blank、Button无文字、constraintSize约束、interface全覆盖、UI区无变量声明"的ArkTS编码规范,通过Stack堆叠容器实现卡牌角标定位、通过linearGradient实现系列专属渐变头像、通过TextDecorationType.LineThrough实现原价删除线效果、通过FlexAlign.End实现柱状图自底向上渲染。这些技术手段均为ArkTS声明式UI的内置能力,无需引入第三方库即可实现专业级的游戏化界面效果。随着HarmonyOS生态在游戏类应用领域的持续拓展,本文所解析的卡牌数据建模和组件化开发方案可为同类应用提供直接的工程参考。
更多推荐


所有评论(0)