HarmonyOS 6效果实现:多种渐变色函数(rankGradient、hotGradient、cyanGradient)来区分不同功能区域的视觉风格
引言

电竞产业的高速发展催生了丰富的游戏社交服务场景,其中"开黑陪玩"作为一种连接高水平玩家与普通用户的桥梁服务,已经形成了庞大的商业生态。在移动端实现一个集开黑房间管理、大神陪玩下单、战队招募、战绩展示和赛事战报于一体的综合电竞社交应用,对 UI 框架的组件化能力、状态管理效率和数据可视化表达提出了极高要求。HarmonyOS 6.1.1 搭载的 ArkTS 声明式 UI 框架凭借其强类型系统、高效的 @Builder 方法复用机制和丰富的原生组件能力,为构建此类复杂社交应用提供了坚实的技术基座。
本文所分析的应用以深蓝电竞荧光风为视觉基调,将游戏世界的科技感与竞技氛围融入界面设计的每一个细节。整体架构基于 HarmonyOS ArkTS API 24 的声明式编程范式,采用 @Entry 和 @Component 装饰器构建入口组件,通过 @State 状态管理驱动 UI 自动刷新。应用共包含六个底部 Tab 页面——开黑大厅、大神陪玩、战队招募、上分之路、赛事战报、我的,以及五个功能弹窗——创建房间、陪玩下单、入队申请、解散房间确认和战绩详情,形成了一套覆盖电竞社交全链路的用户体验闭环。
在业务设计层面,该应用深入模拟了电竞社交平台的核心功能场景:语音房间大卡横滑展示提供了沉浸式的开黑房间浏览体验,陪玩大神列表以横滑大卡和纵向列表两种形式展示玩家信息,战队招募页面通过结构化的招募帖子呈现战队需求,上分之路页面集成了 KDA 柱状图、段位晋级时间轴和英雄胜率横条三种数据可视化组件,赛事战报页面则通过 VS 对战卡片展示比赛结果。此外,应用还巧妙运用了多种渐变色函数(rankGradient、hotGradient、cyanGradient)来区分不同功能区域的视觉风格,增强了界面的层次感和辨识度。
一、类型系统与主题设计

1.1 接口定义体系
应用通过丰富的 interface 定义构建了完整的电竞数据模型,从游戏类型到语音房间、从陪玩大神到战队招募帖、从 KDA 对局到英雄胜率,每一个业务实体都有精确的类型约束。
interface GameType {
id: number
name: string
icon: string
}
interface VoiceRoom {
id: number
name: string
game: string
online: number
maxSlots: number
tag: string
avatars: string[]
}
interface ProItem {
id: number
name: string
icon: string
game: string
rank: string
score: number
stars: number
orders: number
price: number
win: string
tag: string
}
interface KdaMatch {
id: number
hero: string
k: number
d: number
a: number
win: boolean
time: string
mode: string
rank: string
}
interface MatchGame {
id: number
teamA: string
teamB: string
scoreA: number
scoreB: number
result: string
resultColor: string
time: string
tag: string
}
interface RankHistoryItem {
id: number
season: string
rank: string
time: string
note: string
}
类型系统的设计充分考虑了电竞场景的特殊性。VoiceRoom 接口中的 avatars 字段使用字符串数组来存储房间内玩家的头像 Emoji,支持动态渲染叠放头像组。ProItem 接口包含了评分(score)、星级(stars)、接单数(orders)和胜率文案(win)等多个维度的大神评价数据。KdaMatch 接口则记录了单局比赛的击杀数(k)、死亡数(d)、助攻数(a)和胜负结果(win),这是 MOBA 游戏中最核心的战绩指标。MatchGame 接口中的 resultColor 字段直接存储颜色值,使得比赛结果的标签可以在 UI 中直接应用颜色而无需额外的映射函数。
1.2 主题色与渐变函数

应用采用了深蓝电竞荧光配色方案,配合多组渐变色函数来区分不同功能区域的视觉特征。
const BG_DARK: string = '#0B0F1E'
const CARD_DARK: string = '#151B31'
const CARD_DARK2: string = '#1D2540'
const CYAN: string = '#00E5FF'
const MAGENTA: string = '#FF3E9D'
const TEXT_LIGHT: string = '#FFFFFF'
const TEXT_DIM: string = '#8C93AB'
const LOSS_GRAY: string = '#5A6276'
const LINE_DARK: string = '#232C4A'
const GREEN_OK: string = '#00E676'
function barHeight(k: number): number {
return 20 + k * 7
}
function rateWidth(r: number): string {
return r + '%'
}
function orderTotal(price: number, count: number): number {
return price * count + 2
}
function rankGradient(): LinearGradient {
return { angle: 135, colors: [['#00E5FF', 0], ['#0085FF', 1]] }
}
function hotGradient(): LinearGradient {
return { angle: 135, colors: [['#FF3E9D', 0], ['#FF7A00', 1]] }
}
function cyanGradient(): LinearGradient {
return { angle: 135, colors: [['#00E5FF', 0], ['#00A3FF', 1]] }
}
配色体系以 BG_DARK 深蓝黑为主背景,CARD_DARK 和 CARD_DARK2 两级深蓝卡片背景,CYAN 青色和 MAGENTA 洋红为两大主题色——青色代表段位、胜率和正向数据,洋红代表热度、陪玩价格和警示信息。LOSS_GRAY 灰色用于败局柱状图,与青色的胜局柱状图形成鲜明对比。三个渐变函数分别服务于不同场景:rankGradient 用于段位徽章和进度条,hotGradient 用于大神头像框和热门标签,cyanGradient 用于操作按钮和互动元素。barHeight 函数将击杀数映射为柱状图高度(基准20px加每点击杀7px),orderTotal 函数计算陪玩订单总价(单价乘局数加2元服务费)。
二、静态数据与组件状态

2.1 电竞场景数据建模
应用预置了丰富的电竞场景数据来模拟真实的陪玩平台环境,涵盖了多款热门游戏和丰富的陪玩大神信息。
const GAME_TYPES: GameType[] = [
{ id: 1, name: '王者荣耀', icon: '🐲' },
{ id: 2, name: '英雄联盟', icon: '🦁' },
{ id: 3, name: '和平精英', icon: '🪂' },
{ id: 4, name: '无畏契约', icon: '🔫' }
]
const PRO_SLIDERS: ProSlider[] = [
{ id: 1, name: '辰砂', game: '王者荣耀', rank: '最强王者', rankIcon: '👑', score: 9.9, price: 25, desc: '国服打野 · 五连绝世' },
{ id: 2, name: '冰镇汽水', game: '英雄联盟', rank: '最强王者', rankIcon: '👑', score: 9.8, price: 30, desc: '中路杀神 · 韩服千分' },
{ id: 3, name: '暴龙战士', game: '和平精英', rank: '无敌战神', rankIcon: '🪖', score: 9.7, price: 22, desc: '钢枪猛男 · 场均15杀' },
{ id: 4, name: '夜枭', game: '无畏契约', rank: '神话段位', rankIcon: '🎯', score: 9.6, price: 28, desc: '枪法怪 · 残局大师' },
{ id: 5, name: '奶盖', game: '王者荣耀', rank: '荣耀王者', rankIcon: '🌟', score: 9.9, price: 35, desc: '全能辅助 · 温柔指挥' },
{ id: 6, name: '闪电', game: '英雄联盟', rank: '宗师段位', rankIcon: '⚡', score: 9.5, price: 18, desc: '打野节奏大师' }
]
const KDA_MATCHES: KdaMatch[] = [
{ id: 1, hero: '澜', k: 12, d: 3, a: 8, win: true, time: '18分24秒', mode: '排位赛', rank: '星耀III' },
{ id: 2, hero: '镜', k: 8, d: 5, a: 6, win: false, time: '22分10秒', mode: '排位赛', rank: '星耀III' },
{ id: 3, hero: '露娜', k: 15, d: 4, a: 10, win: true, time: '26分35秒', mode: '巅峰赛', rank: '巅峰1800' },
{ id: 4, hero: '马可波罗', k: 9, d: 6, a: 5, win: false, time: '19分58秒', mode: '排位赛', rank: '星耀II' },
{ id: 5, hero: '孙悟空', k: 11, d: 2, a: 7, win: true, time: '21分40秒', mode: '排位赛', rank: '星耀II' },
{ id: 6, hero: '李白', k: 7, d: 7, a: 4, win: false, time: '17分23秒', mode: '排位赛', rank: '星耀II' },
{ id: 7, hero: '曜', k: 10, d: 3, a: 9, win: true, time: '24分02秒', mode: '巅峰赛', rank: '巅峰1900' },
{ id: 8, hero: '后羿', k: 13, d: 6, a: 8, win: true, time: '28分15秒', mode: '排位赛', rank: '星耀I' },
{ id: 9, hero: '裴擒虎', k: 5, d: 4, a: 7, win: false, time: '16分47秒', mode: '排位赛', rank: '星耀I' },
{ id: 10, hero: '东方曜', k: 14, d: 2, a: 11, win: true, time: '23分36秒', mode: '巅峰赛', rank: '巅峰1950' }
]
GAME_TYPES 定义了四款主流电竞游戏,每款游戏配有专属 Emoji 图标。PRO_SLIDERS 是明星大神的横滑展示数据,每位大神都携带段位图标(rankIcon)、评分(score)、单价(price)和一句话描述(desc)。KDA_MATCHES 则是近10局比赛记录,包含了英雄名称、KDA 数据、胜负结果、对局时长、游戏模式和当前段位。这些数据的结构化组织为后续 UI 渲染提供了清晰的数据源。
2.2 组件状态声明

主页面组件通过 @State 声明了一系列状态变量,覆盖了 Tab 切换、游戏选择、弹窗控制和表单输入等交互状态。
@Entry
@Component
struct EsportsPlayPage {
@State currentTab: number = 0
@State selectedGameIndex: number = 0
@State showCreateModal: boolean = false
@State showOrderModal: boolean = false
@State showJoinModal: boolean = false
@State showDissolveModal: boolean = false
@State showKdaModal: boolean = false
@State selectedRoom: VoiceRoom = VOICE_ROOMS[0]
@State selectedPro: ProItem = PRO_LIST[0]
@State selectedTeam: RecruitPost = RECRUIT_POSTS[0]
@State selectedMatch: KdaMatch = KDA_MATCHES[0]
@State roomName: string = '五排上分语音房'
@State roomGameIndex: number = 0
@State roomModeIndex: number = 0
@State roomCountIndex: number = 0
@State orderCount: number = 3
@State orderTimeIndex: number = 0
@State posMask: number = 0
@State introText: string = ''
}
状态管理的设计体现了应用对用户交互的全面覆盖。五个布尔型状态变量分别控制五个弹窗的显隐。四个对象型状态变量(selectedRoom、selectedPro、selectedTeam、selectedMatch)用于在弹窗中展示用户选中的具体数据项。posMask 变量是一个位掩码(bitmask),用于战队招募弹窗中多选擅长位置——通过位运算 this.posMask ^ (1 << i) 实现位置的切换,每个位代表一个位置是否被选中。这种位掩码方式比使用数组更加紧凑高效,是处理多选场景的经典技巧。
三、电竞风头部与底部导航
3.1 头部区域构建

头部区域集成了用户段位徽章、个人信息、搜索框和游戏分类横滑四个功能模块,是应用视觉风格的核心体现。
@Builder
esportsHeader() {
Column() {
Row() {
Column() {
Column() {
Text('👑')
.fontSize(22)
}
.width(46)
.height(46)
.borderRadius(23)
.linearGradient(rankGradient())
.border({ width: 2, color: CYAN })
}
.margin({ right: 10 })
Column() {
Row() {
Text('夜光丶')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Text('星耀III')
.fontSize(10)
.fontColor(CYAN)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('rgba(0,229,255,0.12)')
.margin({ left: 8 })
}
Text('王者荣耀 · 打野 · 巅峰1950')
.fontSize(11)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Row() {
Text('🔍')
.fontSize(14)
Text('搜房间 / 大神 / 战队')
.fontSize(12)
.fontColor(TEXT_DIM)
.margin({ left: 6 })
}
.width(140)
.height(36)
.padding({ left: 10, right: 10 })
.borderRadius(18)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.onClick(() => {
this.currentTab = 0
})
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 10 })
Scroll() {
Row() {
ForEach(GAME_TYPES, (g: GameType, i: number) => {
Row() {
Text(g.icon)
.fontSize(14)
Text(g.name)
.fontSize(12)
.fontWeight(this.selectedGameIndex === i ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.selectedGameIndex === i ? BG_DARK : TEXT_LIGHT)
.margin({ left: 5 })
}
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(16)
.backgroundColor(this.selectedGameIndex === i ? CYAN : CARD_DARK)
.margin({ right: 8 })
.onClick(() => {
this.selectedGameIndex = i
})
}, (g: GameType) => 'game' + g.id)
}
.padding({ left: 12, right: 12 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 10 })
}
.width('100%')
.backgroundColor('#0E1428')
}
段位徽章使用 linearGradient(rankGradient()) 渲染青蓝色渐变背景,配合2px青色边框,呈现出电竞徽章的科技质感。用户名称使用白色粗体,段位标签使用青色文字配半透明青色背景。游戏分类横滑条通过 ForEach 渲染四款游戏的标签按钮,选中态使用青色背景配深色文字,未选中态使用深色卡片背景配白色文字。这种颜色反转的设计手法在电竞类应用中非常常见,能够快速引导用户的视觉焦点。
3.2 底部 Tab 栏
底部 Tab 栏通过状态驱动的样式切换实现导航反馈,与头部区域的电竞风格保持一致。
@Builder
bottomTabs() {
Row() {
ForEach(TAB_LABELS, (lb: string, i: number) => {
Column() {
Text(TAB_EMOJIS[i])
.fontSize(19)
Text(lb)
.fontSize(10)
.fontColor(this.currentTab === i ? CYAN : TEXT_DIM)
.fontWeight(this.currentTab === i ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
Column()
.width(this.currentTab === i ? 20 : 0)
.height(3)
.borderRadius(2)
.backgroundColor(CYAN)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 8, bottom: 6 })
.onClick(() => {
this.currentTab = i
})
}, (lb: string, i: number) => lb + i)
}
.width('100%')
.backgroundColor('#0E1428')
.border({ width: 1, color: LINE_DARK })
.shadow({
radius: 10,
color: 'rgba(0,0,0,0.5)',
offsetX: 0,
offsetY: -4
})
}
Tab 栏使用两个常量数组 TAB_LABELS 和 TAB_EMOJIS 来存储标签文字和图标,通过索引 i 关联两者。选中态的特征包括青色文字、粗体字重和底部20px宽的青色指示条。指示条的宽度通过三元表达式 this.currentTab === i ? 20 : 0 动态控制,实现了选中态的平滑过渡。Tab 栏整体添加了向上的阴影效果(offsetY: -4),使其在视觉上浮于内容区域之上。
四、开黑大厅与陪玩列表
4.1 语音房间大卡
开黑大厅页面的核心是语音房间大卡横滑区域,每张大卡展示了房间名称、游戏标签、在线人数和麦位状态。
Scroll() {
Row() {
ForEach(VOICE_ROOMS, (r: VoiceRoom) => {
Column() {
Row() {
Text(r.game)
.fontSize(10)
.fontColor(CYAN)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('rgba(0,229,255,0.12)')
Column().layoutWeight(1)
Text(r.tag)
.fontSize(9)
.fontColor(MAGENTA)
}
.width('100%')
Text(r.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.maxLines(1)
.width('100%')
.margin({ top: 8 })
Row() {
ForEach(r.avatars, (av: string, i: number) => {
Text(av)
.fontSize(14)
.width(24)
.height(24)
.textAlign(TextAlign.Center)
.borderRadius(12)
.backgroundColor(CARD_DARK2)
.border({ width: 1, color: LINE_DARK })
.margin({ left: i === 0 ? 0 : -8 })
}, (av: string, i: number) => 'av' + i + av)
}
.margin({ top: 10 })
Row() {
Text('🎧')
.fontSize(11)
Text(r.online + '/' + r.maxSlots + ' 麦位')
.fontSize(10)
.fontColor(TEXT_LIGHT)
.margin({ left: 5 })
Column().layoutWeight(1)
Text('解散')
.fontSize(10)
.fontColor(MAGENTA)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.border({ width: 1, color: MAGENTA })
.onClick(() => {
this.selectedRoom = r
this.showDissolveModal = true
})
}
.width('100%')
.padding(8)
.borderRadius(10)
.borderStyle(BorderStyle.Dashed)
.border({ width: 1, color: '#334065' })
.margin({ top: 10 })
}
.width(220)
.padding(14)
.borderRadius(16)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ right: 10 })
.onClick(() => {
this.selectedRoom = r
this.currentTab = 3
})
}, (r: VoiceRoom) => 'vr' + r.id)
}
.padding({ left: 2, right: 2 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
语音房间卡片中有几个值得注意的 UI 技巧。首先是头像叠放效果:通过 margin({ left: i === 0 ? 0 : -8 }) 实现——第一个头像无左边距,后续头像向左偏移8px,形成了重叠的叠放效果。其次是麦位区域的虚线边框:通过 borderStyle(BorderStyle.Dashed) 设置虚线边框样式,模拟了语音房间的"虚拟座位"感。解散按钮使用洋红色边框配洋红色文字,与卡片的青色主题形成对比,暗示了该操作的风险性。
4.2 陪玩大神列表
陪玩大神列表以纵向列表形式展示,每行包含头像、名称、段位、评分、星级、接单数和下单按钮。
ForEach(PRO_LIST, (p: ProItem) => {
Row() {
Text(p.icon)
.fontSize(26)
.width(48)
.height(48)
.textAlign(TextAlign.Center)
.borderRadius(24)
.backgroundColor(CARD_DARK2)
.border({ width: 1, color: LINE_DARK })
Column() {
Row() {
Text(p.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Text(p.rank)
.fontSize(9)
.fontColor(MAGENTA)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(7)
.backgroundColor('rgba(255,62,157,0.12)')
.margin({ left: 6 })
}
Row() {
ForEach([1, 2, 3, 4, 5], (s: number) => {
Text(s <= p.stars ? '⭐' : '☆')
.fontSize(9)
.margin({ right: 1 })
}, (s: number) => 'star' + s)
Text(p.score + ' 分')
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ left: 5 })
}
.margin({ top: 3 })
Row() {
Text(p.win)
.fontSize(10)
.fontColor(GREEN_OK)
Text('· 接单 ' + p.orders)
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ left: 4 })
}
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text(p.tag)
.fontSize(9)
.fontColor(CYAN)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(7)
.backgroundColor('rgba(0,229,255,0.10)')
Text('¥' + p.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(MAGENTA)
.margin({ top: 4 })
Text('/局')
.fontSize(9)
.fontColor(TEXT_DIM)
Text('立即下单')
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.linearGradient(cyanGradient())
.margin({ top: 6 })
.onClick(() => {
this.selectedPro = p
this.showOrderModal = true
})
}
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ bottom: 10 })
.onClick(() => {
this.selectedPro = p
this.showOrderModal = true
})
}, (p: ProItem) => 'pro' + p.id)
星级评分的实现利用了 ForEach 遍历 [1, 2, 3, 4, 5] 数组,通过 s <= p.stars ? '⭐' : '☆' 的条件判断渲染实心星或空心星。这种声明式的星级渲染方式简洁优雅,避免了使用图标库的额外依赖。段位标签使用半透明洋红色背景配洋红色文字,胜率文案使用绿色 GREEN_OK 突出正向数据,接单数使用灰色文字保持信息层级。下单按钮使用 linearGradient(cyanGradient()) 渐变背景,深色文字在青色渐变上具有高对比度,吸引用户点击。
五、上分之路数据可视化
5.1 KDA 柱状图
上分之路页面的核心是近10局 KDA 柱状图,它通过纯声明式 UI 组件实现了数据可视化效果。
@Builder
tabRank() {
Scroll() {
Column() {
Row() {
Text('📊 近10局 KDA')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('胜局青色 / 败局灰色')
.fontSize(10)
.fontColor(TEXT_DIM)
}
.width('100%')
.margin({ top: 6, bottom: 8 })
Column() {
Row() {
ForEach(KDA_MATCHES, (m: KdaMatch, i: number) => {
Column() {
Text(String(i + 1))
.fontSize(9)
.fontColor(TEXT_DIM)
Column()
.width(14)
.height(barHeight(m.k))
.borderRadius(7)
.linearGradient(m.win ? cyanGradient() : rankGradient())
.backgroundColor(m.win ? CYAN : LOSS_GRAY)
.margin({ top: 4 })
Text(String(m.k))
.fontSize(9)
.fontWeight(FontWeight.Bold)
.fontColor(m.win ? CYAN : LOSS_GRAY)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.End)
.onClick(() => {
this.selectedMatch = m
this.showKdaModal = true
})
}, (m: KdaMatch) => 'kda' + m.id)
}
.width('100%')
.height(168)
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
Text('点击柱子查看单局战绩详情')
.fontSize(9)
.fontColor(TEXT_DIM)
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 6 })
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 8 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
}
KDA 柱状图的实现原理非常精巧。每根柱子是一个 Column 组件,宽度固定14px,高度通过 barHeight(m.k) 函数计算(20 + 击杀数 * 7),最大击杀数15对应高度125px。柱子的颜色根据胜负动态切换——胜局使用 cyanGradient() 渐变和 CYAN 背景色,败局使用 rankGradient() 渐变和 LOSS_GRAY 背景色。柱子上方显示局序号,下方显示击杀数,击杀数的文字颜色也跟随胜负状态变化。整个柱状图容器固定高度168px,使用 justifyContent(FlexAlign.End) 使柱子底部对齐。点击任意柱子可以触发战绩详情弹窗,查看该局的完整 KDA 数据、出装方案和对局统计。
5.2 段位晋级进度
段位晋级进度区域通过步骤节点和进度条展示了用户从青铜到王家的晋级路径。
Column() {
Row() {
ForEach(RANK_STEPS, (s: RankStep, i: number) => {
Column() {
Column() {
Text(s.done ? '✓' : (i === 3 ? '👑' : '★'))
.fontSize(14)
.fontColor(s.done ? BG_DARK : CYAN)
.fontWeight(FontWeight.Bold)
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor(s.done ? CYAN : CARD_DARK2)
.border({ width: 1, color: s.done ? CYAN : LINE_DARK })
Text(s.label)
.fontSize(10)
.fontWeight(i === 2 ? FontWeight.Bold : FontWeight.Normal)
.fontColor(i === 2 ? CYAN : TEXT_DIM)
.margin({ top: 6 })
Text(s.desc)
.fontSize(9)
.fontColor(TEXT_DIM)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (s: RankStep) => 'step' + s.id)
}
.width('100%')
Row() {
Row() { }
.layoutWeight(87)
.height(6)
.borderRadius(3)
.linearGradient(cyanGradient())
Row() { }
.layoutWeight(13)
.height(6)
.borderRadius(3)
.backgroundColor(CARD_DARK2)
.margin({ left: 4 })
}
.width('100%')
.margin({ top: 12 })
}
进度条的实现利用了 layoutWeight 的比例分配特性。已完成的进度部分使用 layoutWeight(87) 配合 cyanGradient() 渐变,未完成部分使用 layoutWeight(13) 配合深色背景,两者宽度比为87:13,精确对应了87%的晋级进度。步骤节点的图标根据状态动态变化——已完成节点显示青色背景的"✓",目标节点(索引3)显示"👑",未完成节点显示"★"。当前进行中的节点(索引2)的标签文字使用青色粗体,以视觉上的高亮引导用户关注当前进度。
六、核心交互流程
上述流程图展示了应用的完整交互路径。用户进入应用后,在开黑大厅可以创建房间或解散已有房间;在大神陪玩页面可以选择陪玩并下单;在战队招募页面可以申请加入战队;在上分之路页面可以查看单局战绩详情。每个弹窗都承载了特定的表单交互——创建房间弹窗需要填写房间名称、选择游戏和模式,陪玩下单弹窗需要选择局数和时段,入队申请弹窗需要勾选擅长位置和填写自我介绍。
七、战队招募与赛事战报
7.1 战队招募帖子
战队招募页面以结构化的卡片形式展示招募帖子,每张帖子包含战队信息、招募位置、入队要求和回复数。
ForEach(RECRUIT_POSTS, (t: RecruitPost) => {
Column() {
Row() {
Text(t.gameIcon)
.fontSize(20)
.width(40)
.height(40)
.textAlign(TextAlign.Center)
.borderRadius(20)
.backgroundColor(CARD_DARK2)
Column() {
Row() {
Text(t.team)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.maxLines(1)
Text(t.tag)
.fontSize(9)
.fontColor(MAGENTA)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(7)
.backgroundColor('rgba(255,62,157,0.12)')
.margin({ left: 6 })
}
Text(t.game + ' · ' + t.level)
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text(t.leader)
.fontSize(10)
.fontColor(CYAN)
}
.width('100%')
Row() {
Text('招募位置')
.fontSize(10)
.fontColor(TEXT_DIM)
Text(t.position)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(CYAN)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('rgba(0,229,255,0.10)')
.border({ width: 1, color: '#0E3D4A' })
.margin({ left: 8 })
}
.width('100%')
.margin({ top: 10 })
Row() {
Text('入队要求')
.fontSize(10)
.fontColor(TEXT_DIM)
Text(t.require)
.fontSize(10)
.fontColor(TEXT_LIGHT)
.layoutWeight(1)
.margin({ left: 8 })
}
.width('100%')
.padding(10)
.borderRadius(10)
.backgroundColor(CARD_DARK2)
.margin({ top: 8 })
Row() {
Text('💬 回复 ' + t.replies)
.fontSize(10)
.fontColor(TEXT_DIM)
Column().layoutWeight(1)
Text('申请入队')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.padding({ left: 18, right: 18, top: 8, bottom: 8 })
.borderRadius(16)
.linearGradient(cyanGradient())
.onClick(() => {
this.selectedTeam = t
this.posMask = 0
this.introText = ''
this.showJoinModal = true
})
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ bottom: 12 })
}, (t: RecruitPost) => 'team' + t.id)
招募帖子卡片的信息层级清晰:顶部是战队图标、名称、标签和队长信息;中部是招募位置标签和入队要求;底部是回复数和申请入队按钮。招募位置标签使用了青色文字配半透明青色背景和深色边框,形成了一个醒目的标签按钮。入队要求区域使用深色二级卡片背景,将要求文案与帖子其他信息视觉分离。申请入队按钮在点击时会重置位置掩码(posMask = 0)和自我介绍文本(introText = ''),确保每次打开弹窗都是干净的初始状态。
7.2 VS 对战卡片
赛事战报页面通过 VS 对战卡片展示比赛结果,每张卡片以三方布局呈现两支队伍的对阵信息。
ForEach(MATCH_LIST, (m: MatchGame) => {
Column() {
Row() {
Column() {
Text(m.teamA)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.maxLines(1)
Text('点击查看')
.fontSize(9)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
Column() {
Row() {
Text(String(m.scoreA))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(CYAN)
Text(' : ')
.fontSize(14)
.fontColor(TEXT_DIM)
Text(String(m.scoreB))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(MAGENTA)
}
Text('VS 对阵')
.fontSize(9)
.fontColor(TEXT_DIM)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
Column() {
Text(m.teamB)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.maxLines(1)
Text(m.tag)
.fontSize(9)
.fontColor(CYAN)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
}
.width('100%')
.padding({ top: 4, bottom: 10 })
Row() {
Text(m.result)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor(m.resultColor)
Text('比赛时间 ' + m.time)
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ left: 10 })
Column().layoutWeight(1)
Text('查看战报 >')
.fontSize(10)
.fontColor(CYAN)
}
.width('100%')
.padding({ top: 8, bottom: 4 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ bottom: 10 })
}, (m: MatchGame) => 'match' + m.id)
VS 对战卡片采用三列等分布局(layoutWeight(1) 各占三分之一):左列为A队名称,中间列为比分和"VS 对阵"文案,右列为B队名称和赛事标签。比分数字使用大号粗体字(20px),A队分数使用青色,B队分数使用洋红色,通过颜色差异增强对抗感。比赛结果标签直接使用 m.resultColor 中存储的颜色作为背景,深色文字在高饱和度的结果色上具有良好的可读性。这种将颜色值直接存储在数据结构中的方式,避免了在 UI 渲染时进行条件判断,简化了代码逻辑。
八、弹窗系统详解
8.1 陪玩下单弹窗
陪玩下单弹窗是应用中功能最丰富的模态组件,它集成了大神信息展示、局数选择、时段选择、费用明细和确认下单等完整流程。
@Builder
orderProOverlay(onClose: () => void = () => {}) {
Column() {
Column() {
this.grabHandle()
Row() {
Text('🦾 下单陪玩')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
this.closeBtn(onClose)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
Scroll() {
Column() {
Row() {
Text(this.selectedPro.icon)
.fontSize(26)
.width(52)
.height(52)
.textAlign(TextAlign.Center)
.borderRadius(26)
.backgroundColor(CARD_DARK2)
Column() {
Row() {
Text(this.selectedPro.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column() {
Text('👑')
.fontSize(13)
}
.width(22)
.height(22)
.borderRadius(11)
.linearGradient(hotGradient())
.margin({ left: 6 })
}
Text(this.selectedPro.rank + ' · ' + this.selectedPro.game)
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
Row() {
Text('⭐ ' + this.selectedPro.score + ' 分')
.fontSize(10)
.fontColor('#FFD54F')
Text(this.selectedPro.tag)
.fontSize(9)
.fontColor(CYAN)
.margin({ left: 6 })
}
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text(this.selectedPro.win)
.fontSize(10)
.fontColor(GREEN_OK)
Text('已接 ' + this.selectedPro.orders + ' 单')
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(CARD_DARK2)
.margin({ top: 12 })
Row() {
Text('−')
.fontSize(18)
.fontColor(TEXT_LIGHT)
.width(38)
.height(38)
.textAlign(TextAlign.Center)
.borderRadius(19)
.backgroundColor(CARD_DARK2)
.onClick(() => {
if (this.orderCount > 1) {
this.orderCount = this.orderCount - 1
}
})
Column() {
Text(String(this.orderCount))
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(CYAN)
Text('局 · 可分段结算')
.fontSize(9)
.fontColor(TEXT_DIM)
.margin({ top: 1 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
Text('+')
.fontSize(18)
.fontColor(BG_DARK)
.width(38)
.height(38)
.textAlign(TextAlign.Center)
.borderRadius(19)
.backgroundColor(CYAN)
.onClick(() => {
if (this.orderCount < 6) {
this.orderCount = this.orderCount + 1
}
})
}
.width('100%')
.padding(10)
.borderRadius(14)
.backgroundColor(CARD_DARK2)
.margin({ top: 6 })
Column() {
Row() {
Text('陪玩单价')
.fontSize(11)
.fontColor(TEXT_DIM)
Column().layoutWeight(1)
Text('¥' + this.selectedPro.price + ' × ' + this.orderCount + '局')
.fontSize(11)
.fontColor(TEXT_LIGHT)
}
.width('100%')
.margin({ bottom: 8 })
Row() {
Text('平台服务费')
.fontSize(11)
.fontColor(TEXT_DIM)
Column().layoutWeight(1)
Text('¥2')
.fontSize(11)
.fontColor(TEXT_LIGHT)
}
.width('100%')
.margin({ bottom: 8 })
Row() {
Text('费用合计')
.fontSize(11)
.fontColor(TEXT_DIM)
Column().layoutWeight(1)
Text('¥' + orderTotal(this.selectedPro.price, this.orderCount))
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(MAGENTA)
}
.width('100%')
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(CARD_DARK2)
.margin({ top: 6 })
Row() {
Text('确认下单 ¥' + orderTotal(this.selectedPro.price, this.orderCount))
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 13, bottom: 13 })
.borderRadius(24)
.linearGradient(cyanGradient())
.onClick(() => {
onClose()
})
}
.width('100%')
.margin({ top: 16, bottom: 20 })
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
}
.width('100%')
.constraintSize({ maxHeight: '86%' })
.backgroundColor(CARD_DARK)
.borderRadius({ topLeft: 24, topRight: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.7)')
.justifyContent(FlexAlign.End)
.onClick(() => {
onClose()
})
}
陪玩下单弹窗从底部滑出,通过 constraintSize({ maxHeight: '86%' }) 限制最大高度。顶部使用了 grabHandle 构建器渲染拖拽条。局数选择器采用减号按钮+数字+加号按钮的布局,减号按钮使用深色背景配白色文字,加号按钮使用青色背景配深色文字,通过颜色对比引导用户增加局数。局数限制在1到6之间,通过 if (this.orderCount > 1) 和 if (this.orderCount < 6) 进行边界控制。费用明细区域清晰展示了陪玩单价、平台服务费和合计金额,合计金额通过 orderTotal 函数实时计算。确认按钮的文案直接包含金额,使用青色渐变背景配深色文字,提供了明确的支付预期。
8.2 战绩详情弹窗
战绩详情弹窗展示了单局比赛的完整 KDA 数据、出装方案和对局统计。
@Builder
kdaDetailOverlay(onClose: () => void = () => {}) {
Column() {
Column() {
this.grabHandle()
Row() {
Text('📊 对局详情')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
this.closeBtn(onClose)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
Scroll() {
Column() {
Row() {
Column() {
Text(this.selectedMatch.win ? '✓ 胜利' : '✗ 失败')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(this.selectedMatch.win ? CYAN : LOSS_GRAY)
Text(this.selectedMatch.hero + ' · ' + this.selectedMatch.mode)
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
Text(this.selectedMatch.rank)
.fontSize(10)
.fontColor(MAGENTA)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Row() {
Column() {
Text(String(this.selectedMatch.k))
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor(CYAN)
Text('击杀')
.fontSize(9)
.fontColor(TEXT_DIM)
}
Column() {
Text('/')
.fontSize(24)
.fontColor(TEXT_DIM)
}
.margin({ left: 8, right: 8 })
Column() {
Text(String(this.selectedMatch.d))
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor(MAGENTA)
Text('死亡')
.fontSize(9)
.fontColor(TEXT_DIM)
}
.margin({ left: 8, right: 8 })
Column() {
Text(String(this.selectedMatch.a))
.fontSize(30)
.fontWeight(FontWeight.Bold)
Text('助攻')
.fontSize(9)
.fontColor(TEXT_DIM)
}
.margin({ left: 8, right: 8 })
}
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor(CARD_DARK2)
.margin({ top: 12 })
Text('出装方案')
.fontSize(13)
.fontColor(TEXT_DIM)
.width('100%')
.margin({ top: 14 })
Scroll() {
Row() {
ForEach(EQUIP_ITEMS, (e: EquipItem) => {
Column() {
Text(e.icon)
.fontSize(20)
Text(e.name)
.fontSize(9)
.fontColor(TEXT_LIGHT)
.margin({ top: 4 })
}
.width(62)
.padding({ top: 10, bottom: 10 })
.borderRadius(12)
.backgroundColor(CARD_DARK2)
.border({ width: 1, color: e.color === CYAN ? '#0E4A55' : '#5C1F3C' })
.alignItems(HorizontalAlign.Center)
.margin({ right: 8 })
}, (e: EquipItem) => 'eq' + e.id)
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
Row() {
ForEach(KDA_STATS, (s: StatGridItem) => {
Column() {
Text(s.value)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(CYAN)
Text(s.label)
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
Text(s.unit)
.fontSize(9)
.fontColor(TEXT_DIM)
.margin({ top: 1 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 12, bottom: 12 })
.borderRadius(12)
.backgroundColor(CARD_DARK2)
.margin({ left: 3, right: 3 })
}, (s: StatGridItem) => 'kstats' + s.id)
}
.width('100%')
.margin({ top: 6 })
Row() {
Text('⏱ 对局时长:' + this.selectedMatch.time + ' · 综合评分 13.5')
.fontSize(10)
.fontColor(TEXT_DIM)
}
.width('100%')
.padding(10)
.borderRadius(10)
.backgroundColor('#0F1B33')
.margin({ top: 12 })
Row() {
Text('知道了')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 13, bottom: 13 })
.borderRadius(24)
.linearGradient(cyanGradient())
.onClick(() => {
onClose()
})
}
.width('100%')
.margin({ top: 16, bottom: 20 })
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
}
.width('100%')
.constraintSize({ maxHeight: '86%' })
.backgroundColor(CARD_DARK)
.borderRadius({ topLeft: 24, topRight: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.7)')
.justifyContent(FlexAlign.End)
.onClick(() => {
onClose()
})
}
战绩详情弹窗的信息呈现层次分明。顶部信息区域左侧显示胜负结果(胜利用青色"✓ 胜利",失败用灰色"✗ 失败")、英雄名称和游戏模式,右侧以大号数字展示 KDA 三项数据——击杀数用青色、死亡数用洋红色、助攻数用青色,通过颜色编码快速区分正向和负向指标。出装方案区域通过横向滚动展示8件装备,每件装备卡片的边框颜色根据装备的 color 属性动态切换(青色或洋红色),实现了装备分类的视觉区分。对局统计区域使用四宫格展示伤害、承伤、经济和参团率四项数据。底部信息条显示对局时长和综合评分,使用深色背景 #0F1B33 与主卡片背景形成微妙的层次差异。
技术对比总结
| 技术维度 | 本应用实现方案 | 传统方案 | 优势分析 |
|---|---|---|---|
| KDA柱状图 | Column组件+渐变色+条件渲染 | ECharts/MPAndroidChart | 零依赖,声明式构建,自动响应状态 |
| 星级评分 | ForEach+条件Emoji渲染 | 自定义RatingBar组件 | 纯声明式,无原生组件依赖 |
| 头像叠放 | 负margin实现重叠效果 | 自定义ViewGroup | CSS式布局,简洁直观 |
| 多选位置 | 位掩码(posMask)+位运算 | Boolean数组或Set | 内存紧凑,运算高效 |
| 渐变色管理 | 函数返回LinearGradient对象 | XML渐变定义文件 | 类型安全,编译期检查 |
| 弹窗管理 | modalOverlay条件渲染链 | BottomSheetDialog库 | 集中管理,状态可控 |
| 进度条 | layoutWeight比例分配 | ProgressBar组件 | 声明式比例,无需计算像素 |
| 虚线边框 | borderStyle(Dashed) | 自定义Drawable | 原生属性,零额外代码 |
安装DevEco Studio程序

选择目标安装目录:

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

新建一个空白模板:

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

完整代码:
// 场景:游戏开黑房间、大神陪玩下单、战队招募、上分战绩、赛事战报
// 布局特色:深蓝电竞荧光风头部 + 语音房间大卡 + 双列房间 + 陪玩横滑大卡 + KDA柱状图 + VS对战卡 + 段位时间轴
// 6个底部Tab:开黑大厅 / 大神陪玩 / 战队招募 / 上分之路 / 赛事战报 / 我的
// 5个弹框:创建房间 / 陪玩下单 / 入队申请 / 解散房间确认 / 战绩详情
// ==================== 数据结构 ====================
interface GameType {
id: number
name: string
icon: string
}
interface VoiceRoom {
id: number
name: string
game: string
online: number
maxSlots: number
tag: string
avatars: string[]
}
interface RoomCard {
id: number
name: string
gameIcon: string
game: string
rank: string
need: string
online: number
tag: string
}
interface ProSlider {
id: number
name: string
game: string
rank: string
rankIcon: string
score: number
price: number
desc: string
}
interface ProItem {
id: number
name: string
icon: string
game: string
rank: string
score: number
stars: number
orders: number
price: number
win: string
tag: string
}
interface RecruitPost {
id: number
team: string
game: string
gameIcon: string
position: string
require: string
replies: number
leader: string
level: string
tag: string
}
interface KdaMatch {
id: number
hero: string
k: number
d: number
a: number
win: boolean
time: string
mode: string
rank: string
}
interface HeroWin {
id: number
hero: string
icon: string
rate: number
plays: number
}
interface RankStep {
id: number
label: string
desc: string
done: boolean
}
interface MatchGame {
id: number
teamA: string
teamB: string
scoreA: number
scoreB: number
result: string
resultColor: string
time: string
tag: string
}
interface ScheduleItem {
id: number
game: string
desc: string
time: string
tag: string
}
interface StatPanelItem {
id: number
label: string
value: string
unit: string
color: string
}
interface HeroGridItem {
id: number
hero: string
icon: string
plays: number
win: string
}
interface RankHistoryItem {
id: number
season: string
rank: string
time: string
note: string
}
interface TimeSlot {
id: number
label: string
tip: string
}
interface EquipItem {
id: number
name: string
icon: string
color: string
}
interface StatGridItem {
id: number
label: string
value: string
unit: string
}
// ==================== 主题色 ====================
const BG_DARK: string = '#0B0F1E'
const CARD_DARK: string = '#151B31'
const CARD_DARK2: string = '#1D2540'
const CYAN: string = '#00E5FF'
const MAGENTA: string = '#FF3E9D'
const TEXT_LIGHT: string = '#FFFFFF'
const TEXT_DIM: string = '#8C93AB'
const LOSS_GRAY: string = '#5A6276'
const LINE_DARK: string = '#232C4A'
const GREEN_OK: string = '#00E676'
// ==================== 全局纯函数 ====================
function barHeight(k: number): number {
return 20 + k * 7
}
function rateWidth(r: number): string {
return r + '%'
}
function orderTotal(price: number, count: number): number {
return price * count + 2
}
function rankGradient(): LinearGradient {
return { angle: 135, colors: [['#00E5FF', 0], ['#0085FF', 1]] }
}
function hotGradient(): LinearGradient {
return { angle: 135, colors: [['#FF3E9D', 0], ['#FF7A00', 1]] }
}
function cyanGradient(): LinearGradient {
return { angle: 135, colors: [['#00E5FF', 0], ['#00A3FF', 1]] }
}
// ==================== 静态数据 ====================
const GAME_TYPES: GameType[] = [
{ id: 1, name: '王者荣耀', icon: '🐲' },
{ id: 2, name: '英雄联盟', icon: '🦁' },
{ id: 3, name: '和平精英', icon: '🪂' },
{ id: 4, name: '无畏契约', icon: '🔫' }
]
const VOICE_ROOMS: VoiceRoom[] = [
{ id: 1, name: '钻石五排冲星房', game: '王者荣耀', online: 12, maxSlots: 5, tag: '钻石冲星', avatars: ['🧑🚀', '👩🎤', '🧑✈️', '👨💻', '🧑🔧'] },
{ id: 2, name: '大乱斗欢乐语音屋', game: '英雄联盟', online: 8, maxSlots: 5, tag: '欢乐开黑', avatars: ['👨🎨', '🧑🏫', '👩🍳', '🧑🚒', '👨💼'] },
{ id: 3, name: '四排刚枪吃鸡队', game: '和平精英', online: 10, maxSlots: 4, tag: '钢枪上分', avatars: ['🦸', '🧑🎤', '👨🚀', '🧑🍳'] },
{ id: 4, name: '瓦罗兰传奇车队', game: '无畏契约', online: 6, maxSlots: 5, tag: '冲传奇', avatars: ['🧑💻', '👩🚀', '🧑🎨', '👨🎤', '🧑🏭'] },
{ id: 5, name: '巅峰赛2000分房', game: '王者荣耀', online: 15, maxSlots: 5, tag: '巅峰车队', avatars: ['👑', '🧑✈️', '👨🚀', '👩🎤', '🧑🔬'] },
{ id: 6, name: '峡谷之巅大师局', game: '英雄联盟', online: 7, maxSlots: 5, tag: '大师冲分', avatars: ['🧑🏫', '👨💼', '🧑🚒', '👩💻', '🧑🎓'] },
{ id: 7, name: '决赛圈狙击小队', game: '和平精英', online: 9, maxSlots: 4, tag: '决赛圈', avatars: ['🎯', '🧑🚀', '👨🎨', '🧑🏭'] },
{ id: 8, name: '赛季末冲刺房', game: '王者荣耀', online: 11, maxSlots: 5, tag: '冲刺王者', avatars: ['🚀', '👩🎤', '🧑✈️', '👨💻', '🧑🍳'] }
]
const ROOM_CARDS: RoomCard[] = [
{ id: 1, name: '星耀五排 · 差一人', gameIcon: '🐲', game: '王者荣耀', rank: '星耀III', need: '缺打野', online: 4, tag: '热门' },
{ id: 2, name: '巅峰2000车队', gameIcon: '🐲', game: '王者荣耀', rank: '巅峰2000', need: '缺辅助', online: 3, tag: '车队' },
{ id: 3, name: '峡谷大师组', gameIcon: '🦁', game: '英雄联盟', rank: '大师段位', need: '缺上单', online: 5, tag: '新开' },
{ id: 4, name: '四排吃鸡冲分', gameIcon: '🪂', game: '和平精英', rank: '王牌段位', need: '缺指挥', online: 2, tag: '稳定' },
{ id: 5, name: '铂金上分车队', gameIcon: '🐲', game: '王者荣耀', rank: '铂金I', need: '缺射手', online: 4, tag: '轻松' },
{ id: 6, name: '大乱斗速开局', gameIcon: '🦁', game: '英雄联盟', rank: '无段位要求', need: '缺ADC', online: 6, tag: '欢乐' },
{ id: 7, name: '瓦罗兰冲钻房', gameIcon: '🔫', game: '无畏契约', rank: '钻石段位', need: '缺决斗', online: 3, tag: '强势' },
{ id: 8, name: '双排默契上分', gameIcon: '🐲', game: '王者荣耀', rank: '钻石段位', need: '缺中单', online: 2, tag: '安静' },
{ id: 9, name: '决赛圈突击队', gameIcon: '🪂', game: '和平精英', rank: '星钻段位', need: '缺突击', online: 4, tag: '语音' },
{ id: 10, name: '王者冲分队', gameIcon: '🐲', game: '王者荣耀', rank: '最强王者', need: '缺打野', online: 5, tag: '冲分' }
]
const PRO_SLIDERS: ProSlider[] = [
{ id: 1, name: '辰砂', game: '王者荣耀', rank: '最强王者', rankIcon: '👑', score: 9.9, price: 25, desc: '国服打野 · 五连绝世' },
{ id: 2, name: '冰镇汽水', game: '英雄联盟', rank: '最强王者', rankIcon: '👑', score: 9.8, price: 30, desc: '中路杀神 · 韩服千分' },
{ id: 3, name: '暴龙战士', game: '和平精英', rank: '无敌战神', rankIcon: '🪖', score: 9.7, price: 22, desc: '钢枪猛男 · 场均15杀' },
{ id: 4, name: '夜枭', game: '无畏契约', rank: '神话段位', rankIcon: '🎯', score: 9.6, price: 28, desc: '枪法怪 · 残局大师' },
{ id: 5, name: '奶盖', game: '王者荣耀', rank: '荣耀王者', rankIcon: '🌟', score: 9.9, price: 35, desc: '全能辅助 · 温柔指挥' },
{ id: 6, name: '闪电', game: '英雄联盟', rank: '宗师段位', rankIcon: '⚡', score: 9.5, price: 18, desc: '打野节奏大师' }
]
const PRO_LIST: ProItem[] = [
{ id: 1, name: '辰砂', icon: '🥷', game: '王者荣耀', rank: '最强王者', score: 9.9, stars: 5, orders: 1024, price: 25, win: '胜率68%', tag: '国服打野' },
{ id: 2, name: '苏黎', icon: '🧙', game: '英雄联盟', rank: '大师段位', score: 9.7, stars: 5, orders: 860, price: 30, win: '胜率65%', tag: '中路法王' },
{ id: 3, name: '暴龙战士', icon: '🦖', game: '和平精英', rank: '无敌战神', score: 9.8, stars: 5, orders: 731, price: 22, win: '胜率72%', tag: '刚枪猛男' },
{ id: 4, name: '夜枭', icon: '🦉', game: '无畏契约', rank: '神话段位', score: 9.6, stars: 4, orders: 592, price: 28, win: '胜率63%', tag: '残局大师' },
{ id: 5, name: '奶盖', icon: '🍼', game: '王者荣耀', rank: '荣耀王者', score: 9.9, stars: 5, orders: 1203, price: 35, win: '胜率70%', tag: '全能辅助' },
{ id: 6, name: '大福', icon: '🧸', game: '和平精英', rank: '王牌段位', score: 9.5, stars: 4, orders: 465, price: 18, win: '胜率61%', tag: '指挥位' },
{ id: 7, name: '闪电', icon: '⚡', game: '英雄联盟', rank: '宗师段位', score: 9.4, stars: 4, orders: 380, price: 20, win: '胜率59%', tag: '野区节奏' },
{ id: 8, name: '阿布', icon: '🏹', game: '王者荣耀', rank: '最强王者', score: 9.3, stars: 3, orders: 298, price: 15, win: '胜率57%', tag: '射手一哥' },
{ id: 9, name: '芝士', icon: '🧀', game: '无畏契约', rank: '钻石段位', score: 9.2, stars: 3, orders: 205, price: 19, win: '胜率55%', tag: '先锋突破' },
{ id: 10, name: '老K', icon: '🃏', game: '英雄联盟', rank: '钻石段位', score: 9.1, stars: 3, orders: 176, price: 16, win: '胜率54%', tag: '上单战神' }
]
const RECRUIT_POSTS: RecruitPost[] = [
{ id: 1, team: 'TSG电竞俱乐部', game: '王者荣耀', gameIcon: '🐲', position: '招募打野/辅助', require: '王者25星以上 或 巅峰赛1900分+', replies: 132, leader: '队长·星辰', level: '王者战队', tag: '热门' },
{ id: 2, team: 'VG星辰', game: '英雄联盟', gameIcon: '🦁', position: '招募中单/ADC', require: '大师段位+ 有比赛经验', replies: 87, leader: '队长·凌风', level: '钻石战队', tag: '急招' },
{ id: 3, team: '吃鸡大队', game: '和平精英', gameIcon: '🪂', position: '招募指挥/自由人', require: '王牌4星+ KDA 3.5以上', replies: 156, leader: '队长·钢枪', level: '王牌战队', tag: '稳定' },
{ id: 4, team: 'Valo先锋', game: '无畏契约', gameIcon: '🔫', position: '招募决斗/哨位', require: '钻石段位+ 枪法过硬', replies: 64, leader: '队长·夜枭', level: '钻石战队', tag: '新队' },
{ id: 5, team: '巅峰王者', game: '王者荣耀', gameIcon: '🐲', position: '招募边路/中单', require: '巅峰赛2200分+', replies: 98, leader: '队长·天梯', level: '荣耀战队', tag: '冲分' },
{ id: 6, team: '峡谷天团', game: '英雄联盟', gameIcon: '🦁', position: '招募辅助/上单', require: '峡谷之巅钻1段位+', replies: 45, leader: '队长·波比', level: '大师战队', tag: '娱乐' },
{ id: 7, team: '空投小队', game: '和平精英', gameIcon: '🪂', position: '招募狙击手', require: 'KD 4.0+ 场均伤害8000', replies: 73, leader: '队长·八倍镜', level: '王牌战队', tag: '刚枪' },
{ id: 8, team: '闪击队', game: '无畏契约', gameIcon: '🔫', position: '招募烟位', require: '超凡段位+ 赛事经验优先', replies: 29, leader: '队长·闪光', level: '钻石战队', tag: '备赛' },
{ id: 9, team: '荣耀战盟', game: '王者荣耀', gameIcon: '🐲', position: '招募打野', require: '荣耀王者 主玩野核英雄', replies: 52, leader: '队长·野王', level: '王者战队', tag: '战队赛' },
{ id: 10, team: '高校联盟', game: '英雄联盟', gameIcon: '🦁', position: '招募全位置', require: '在校大学生 每周固定训练', replies: 41, leader: '队长·校队', level: '钻石战队', tag: '学生' }
]
const KDA_MATCHES: KdaMatch[] = [
{ id: 1, hero: '澜', k: 12, d: 3, a: 8, win: true, time: '18分24秒', mode: '排位赛', rank: '星耀III' },
{ id: 2, hero: '镜', k: 8, d: 5, a: 6, win: false, time: '22分10秒', mode: '排位赛', rank: '星耀III' },
{ id: 3, hero: '露娜', k: 15, d: 4, a: 10, win: true, time: '26分35秒', mode: '巅峰赛', rank: '巅峰1800' },
{ id: 4, hero: '马可波罗', k: 9, d: 6, a: 5, win: false, time: '19分58秒', mode: '排位赛', rank: '星耀II' },
{ id: 5, hero: '孙悟空', k: 11, d: 2, a: 7, win: true, time: '21分40秒', mode: '排位赛', rank: '星耀II' },
{ id: 6, hero: '李白', k: 7, d: 7, a: 4, win: false, time: '17分23秒', mode: '排位赛', rank: '星耀II' },
{ id: 7, hero: '曜', k: 10, d: 3, a: 9, win: true, time: '24分02秒', mode: '巅峰赛', rank: '巅峰1900' },
{ id: 8, hero: '后羿', k: 13, d: 6, a: 8, win: true, time: '28分15秒', mode: '排位赛', rank: '星耀I' },
{ id: 9, hero: '裴擒虎', k: 5, d: 4, a: 7, win: false, time: '16分47秒', mode: '排位赛', rank: '星耀I' },
{ id: 10, hero: '东方曜', k: 14, d: 2, a: 11, win: true, time: '23分36秒', mode: '巅峰赛', rank: '巅峰1950' }
]
const HERO_WINS: HeroWin[] = [
{ id: 1, hero: '澜', icon: '🌀', rate: 68, plays: 132 },
{ id: 2, hero: '镜', icon: '🪞', rate: 66, plays: 98 },
{ id: 3, hero: '露娜', icon: '🌙', rate: 64, plays: 85 },
{ id: 4, hero: '曜', icon: '⚔️', rate: 62, plays: 76 },
{ id: 5, hero: '孙悟空', icon: '🐵', rate: 58, plays: 210 },
{ id: 6, hero: '马可波罗', icon: '🔫', rate: 55, plays: 143 },
{ id: 7, hero: '李白', icon: '🍶', rate: 52, plays: 120 },
{ id: 8, hero: '裴擒虎', icon: '🐯', rate: 50, plays: 64 }
]
const RANK_STEPS: RankStep[] = [
{ id: 1, label: '倔强青铜', desc: '赛季起点', done: true },
{ id: 2, label: '钻石段位', desc: '已通过', done: true },
{ id: 3, label: '星耀段位', desc: '晋级中 87%', done: false },
{ id: 4, label: '最强王者', desc: '赛季目标', done: false }
]
const MATCH_LIST: MatchGame[] = [
{ id: 1, teamA: 'TSG', teamB: 'VG星辰', scoreA: 2, scoreB: 1, result: 'TSG胜', resultColor: '#00E5FF', time: '08-25 19:00', tag: '半决赛' },
{ id: 2, teamA: '巅峰王者', teamB: '荣耀战盟', scoreA: 1, scoreB: 2, result: '荣耀战盟胜', resultColor: '#FF3E9D', time: '08-25 20:30', tag: '八强赛' },
{ id: 3, teamA: '吃鸡大队', teamB: '空投小队', scoreA: 4, scoreB: 1, result: '吃鸡大队胜', resultColor: '#00E5FF', time: '08-24 21:00', tag: '决赛' },
{ id: 4, teamA: 'Valo先锋', teamB: '闪击队', scoreA: 13, scoreB: 11, result: 'Valo先锋胜', resultColor: '#00E5FF', time: '08-24 19:30', tag: '常规赛' },
{ id: 5, teamA: '峡谷天团', teamB: '高校联盟', scoreA: 1, scoreB: 1, result: '握手言和', resultColor: '#8C93AB', time: '08-23 20:00', tag: '小组赛' },
{ id: 6, teamA: '高校联盟', teamB: '巅峰王者', scoreA: 0, scoreB: 2, result: '巅峰王者胜', resultColor: '#FF3E9D', time: '08-23 18:00', tag: '小组赛' },
{ id: 7, teamA: 'TSG', teamB: '峡谷天团', scoreA: 2, scoreB: 0, result: 'TSG胜', resultColor: '#00E5FF', time: '08-22 21:30', tag: '半决赛' },
{ id: 8, teamA: '荣耀战盟', teamB: 'VG星辰', scoreA: 2, scoreB: 1, result: '荣耀战盟胜', resultColor: '#FF3E9D', time: '08-22 19:00', tag: '八强赛' }
]
const SCHEDULE_LIST: ScheduleItem[] = [
{ id: 1, game: '王者荣耀', desc: 'TSG vs 荣耀战盟 · 巅峰对决', time: '今日 20:00', tag: '焦点' },
{ id: 2, game: '英雄联盟', desc: '峡谷天团 vs 高校联盟', time: '今日 21:30', tag: '直播' },
{ id: 3, game: '和平精英', desc: '吃鸡大队 vs 空投小队 · 决赛', time: '明日 19:00', tag: '决赛' },
{ id: 4, game: '无畏契约', desc: 'Valo先锋 vs 闪击队', time: '明日 20:30', tag: '直播' },
{ id: 5, game: '王者荣耀', desc: 'VG星辰 vs 巅峰王者', time: '周六 18:00', tag: '常规赛' },
{ id: 6, game: '英雄联盟', desc: '高校联盟 vs TSG', time: '周日 20:00', tag: '常规赛' }
]
const STAT_PANEL: StatPanelItem[] = [
{ id: 1, label: '综合胜率', value: '64%', unit: 'S23赛季', color: '#00E5FF' },
{ id: 2, label: '总场次', value: '1,286', unit: '排位+巅峰', color: '#FF3E9D' },
{ id: 3, label: '综合KDA', value: '6.8', unit: '击杀/死亡/助攻', color: '#00E5FF' }
]
const HERO_GRID: HeroGridItem[] = [
{ id: 1, hero: '澜', icon: '🌀', plays: 132, win: '68%' },
{ id: 2, hero: '镜', icon: '🪞', plays: 98, win: '66%' },
{ id: 3, hero: '露娜', icon: '🌙', plays: 85, win: '64%' },
{ id: 4, hero: '曜', icon: '⚔️', plays: 76, win: '62%' },
{ id: 5, hero: '孙悟空', icon: '🐵', plays: 210, win: '58%' },
{ id: 6, hero: '马可波罗', icon: '🔫', plays: 143, win: '55%' },
{ id: 7, hero: '李白', icon: '🍶', plays: 120, win: '52%' },
{ id: 8, hero: '裴擒虎', icon: '🐯', plays: 64, win: '50%' }
]
const RANK_HISTORY: RankHistoryItem[] = [
{ id: 1, season: 'S18赛季', rank: '钻石II', time: '2024-04', note: '上分起步' },
{ id: 2, season: 'S19赛季', rank: '星耀V', time: '2024-10', note: '突破钻石' },
{ id: 3, season: 'S20赛季', rank: '星耀I', time: '2025-06', note: '晋级失利' },
{ id: 4, season: 'S21赛季', rank: '最强王者', time: '2026-01', note: '首登王者' },
{ id: 5, season: 'S22赛季', rank: '王者20星', time: '2026-04', note: '冲击荣耀' },
{ id: 6, season: 'S23赛季', rank: '荣耀王者', time: '2026-08', note: '当前段位' }
]
const TIME_SLOTS: TimeSlot[] = [
{ id: 1, label: '19:00-21:00', tip: '黄金时段' },
{ id: 2, label: '21:00-23:00', tip: '排位高峰' },
{ id: 3, label: '13:00-15:00', tip: '午间上分' },
{ id: 4, label: '23:00-01:00', tip: '深夜冲分' }
]
const EQUIP_ITEMS: EquipItem[] = [
{ id: 1, name: '破军', icon: '⚔️', color: '#00E5FF' },
{ id: 2, name: '无尽战刃', icon: '🗡️', color: '#FF3E9D' },
{ id: 3, name: '碎星锤', icon: '🔨', color: '#00E5FF' },
{ id: 4, name: '名刀司命', icon: '🕳️', color: '#FF3E9D' },
{ id: 5, name: '贤者庇护', icon: '🛡️', color: '#00E5FF' },
{ id: 6, name: '宗师之力', icon: '💪', color: '#FF3E9D' },
{ id: 7, name: '电刀', icon: '⚡', color: '#00E5FF' },
{ id: 8, name: '黑切', icon: '🖤', color: '#FF3E9D' }
]
const KDA_STATS: StatGridItem[] = [
{ id: 1, label: '伤害', value: '24.1k', unit: '输出' },
{ id: 2, label: '承伤', value: '18.6k', unit: '抗压' },
{ id: 3, label: '经济', value: '12.3k', unit: '发育' },
{ id: 4, label: '参团率', value: '78%', unit: '联动' }
]
const POSITION_TAGS: string[] = ['打野', '中单', '辅助', '上单', '射手']
const TAB_LABELS: string[] = ['开黑大厅', '大神陪玩', '战队招募', '上分之路', '赛事战报', '我的']
const TAB_EMOJIS: string[] = ['🎮', '🦾', '🛡️', '🚀', '🏆', '👤']
@Entry
@Component
struct EsportsPlayPage {
@State currentTab: number = 0
@State selectedGameIndex: number = 0
@State showCreateModal: boolean = false
@State showOrderModal: boolean = false
@State showJoinModal: boolean = false
@State showDissolveModal: boolean = false
@State showKdaModal: boolean = false
@State selectedRoom: VoiceRoom = VOICE_ROOMS[0]
@State selectedPro: ProItem = PRO_LIST[0]
@State selectedTeam: RecruitPost = RECRUIT_POSTS[0]
@State selectedMatch: KdaMatch = KDA_MATCHES[0]
@State roomName: string = '五排上分语音房'
@State roomGameIndex: number = 0
@State roomModeIndex: number = 0
@State roomCountIndex: number = 0
@State orderCount: number = 3
@State orderTimeIndex: number = 0
@State posMask: number = 0
@State introText: string = ''
// ============ 通用弹框外层 ============
@Builder
modalOverlay1(onClose: () => void = () => {}) {
Column() {
Text('')
.fontSize(1)
.visibility(Visibility.None)
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.65)')
.onClick(() => {
onClose()
})
.justifyContent(FlexAlign.End)
}
@Builder
grabHandle() {
Column()
.width(42)
.height(4)
.borderRadius(2)
.backgroundColor('#334065')
.margin({ top: 10 })
}
@Builder
closeBtn(onClose: () => void = () => {}) {
Text('✕')
.fontSize(16)
.fontColor(TEXT_DIM)
.width(30)
.height(30)
.textAlign(TextAlign.Center)
.borderRadius(15)
.backgroundColor(CARD_DARK2)
.onClick(() => {
onClose()
})
}
build() {
Stack({ alignContent: Alignment.Bottom }) {
Column() {
this.esportsHeader()
Column() {
if (this.currentTab === 0) {
this.tabLobby()
} else if (this.currentTab === 1) {
this.tabPro()
} else if (this.currentTab === 2) {
this.tabTeam()
} else if (this.currentTab === 3) {
this.tabRank()
} else if (this.currentTab === 4) {
this.tabMatch()
} else {
this.tabMine()
}
}
.layoutWeight(1)
.width('100%')
this.bottomTabs()
}
.width('100%')
.height('100%')
if (this.showCreateModal || this.showOrderModal || this.showJoinModal || this.showDissolveModal || this.showKdaModal) {
this.modalOverlay(() => {
this.showCreateModal = false
this.showOrderModal = false
this.showJoinModal = false
this.showDissolveModal = false
this.showKdaModal = false
})
}
}
.width('100%')
.height('100%')
.backgroundColor(BG_DARK)
}
// ==================== 弹窗统一入口(modalOverlay模式) ====================
@Builder
modalOverlay(onClose: () => void) {
if (this.showCreateModal) {
this.createRoomOverlay(() => {
this.showCreateModal = false
})
} else if (this.showOrderModal) {
this.orderProOverlay(() => {
this.showOrderModal = false
})
} else if (this.showJoinModal) {
this.joinTeamOverlay(() => {
this.showJoinModal = false
})
} else if (this.showDissolveModal) {
this.dissolveRoomOverlay(() => {
this.showDissolveModal = false
})
} else if (this.showKdaModal) {
this.kdaDetailOverlay(() => {
this.showKdaModal = false
})
}
}
// ==================== 电竞风头部 ====================
@Builder
esportsHeader() {
Column() {
Row() {
// 段位徽章
Column() {
Column() {
Text('👑')
.fontSize(22)
}
.width(46)
.height(46)
.borderRadius(23)
.linearGradient(rankGradient())
.border({ width: 2, color: CYAN })
}
.margin({ right: 10 })
Column() {
Row() {
Text('夜光丶')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Text('星耀III')
.fontSize(10)
.fontColor(CYAN)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(8)
.backgroundColor('rgba(0,229,255,0.12)')
.margin({ left: 8 })
}
Text('王者荣耀 · 打野 · 巅峰1950')
.fontSize(11)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 搜索框
Row() {
Text('🔍')
.fontSize(14)
Text('搜房间 / 大神 / 战队')
.fontSize(12)
.fontColor(TEXT_DIM)
.margin({ left: 6 })
}
.width(140)
.height(36)
.padding({ left: 10, right: 10 })
.borderRadius(18)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.onClick(() => {
this.currentTab = 0
})
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 10 })
// 游戏分类横滑
Scroll() {
Row() {
ForEach(GAME_TYPES, (g: GameType, i: number) => {
Row() {
Text(g.icon)
.fontSize(14)
Text(g.name)
.fontSize(12)
.fontWeight(this.selectedGameIndex === i ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.selectedGameIndex === i ? BG_DARK : TEXT_LIGHT)
.margin({ left: 5 })
}
.padding({ left: 14, right: 14, top: 7, bottom: 7 })
.borderRadius(16)
.backgroundColor(this.selectedGameIndex === i ? CYAN : CARD_DARK)
.margin({ right: 8 })
.onClick(() => {
this.selectedGameIndex = i
})
}, (g: GameType) => 'game' + g.id)
}
.padding({ left: 12, right: 12 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 10 })
}
.width('100%')
.backgroundColor('#0E1428')
}
// ==================== 底部6个Tab ====================
@Builder
bottomTabs() {
Row() {
ForEach(TAB_LABELS, (lb: string, i: number) => {
Column() {
Text(TAB_EMOJIS[i])
.fontSize(19)
Text(lb)
.fontSize(10)
.fontColor(this.currentTab === i ? CYAN : TEXT_DIM)
.fontWeight(this.currentTab === i ? FontWeight.Bold : FontWeight.Normal)
.margin({ top: 2 })
Column()
.width(this.currentTab === i ? 20 : 0)
.height(3)
.borderRadius(2)
.backgroundColor(CYAN)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 8, bottom: 6 })
.onClick(() => {
this.currentTab = i
})
}, (lb: string, i: number) => lb + i)
}
.width('100%')
.backgroundColor('#0E1428')
.border({ width: 1, color: LINE_DARK })
.shadow({
radius: 10,
color: 'rgba(0,0,0,0.5)',
offsetX: 0,
offsetY: -4
})
}
// ==================== Tab0 开黑大厅 ====================
@Builder
tabLobby() {
Scroll() {
Column() {
// 创建房间按钮
Row() {
Text('+')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
Text('创建开黑房间')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.margin({ left: 6 })
}
.width('100%')
.padding({ top: 12, bottom: 12 })
.borderRadius(22)
.linearGradient(cyanGradient())
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showCreateModal = true
})
// 语音房间大卡横滑
Row() {
Text('🎙️ 语音开黑房')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('全部语音房 >')
.fontSize(10)
.fontColor(TEXT_DIM)
.onClick(() => {
this.currentTab = 0
})
}
.width('100%')
.margin({ top: 16, bottom: 8 })
Scroll() {
Row() {
ForEach(VOICE_ROOMS, (r: VoiceRoom) => {
Column() {
Row() {
Text(r.game)
.fontSize(10)
.fontColor(CYAN)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('rgba(0,229,255,0.12)')
Column().layoutWeight(1)
Text(r.tag)
.fontSize(9)
.fontColor(MAGENTA)
}
.width('100%')
Text(r.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.maxLines(1)
.width('100%')
.margin({ top: 8 })
// 在线头像叠放
Row() {
ForEach(r.avatars, (av: string, i: number) => {
Text(av)
.fontSize(14)
.width(24)
.height(24)
.textAlign(TextAlign.Center)
.borderRadius(12)
.backgroundColor(CARD_DARK2)
.border({ width: 1, color: LINE_DARK })
.margin({ left: i === 0 ? 0 : -8 })
}, (av: string, i: number) => 'av' + i + av)
}
.margin({ top: 10 })
// 麦位虚线框
Row() {
Text('🎧')
.fontSize(11)
Text(r.online + '/' + r.maxSlots + ' 麦位')
.fontSize(10)
.fontColor(TEXT_LIGHT)
.margin({ left: 5 })
Column().layoutWeight(1)
Text('解散')
.fontSize(10)
.fontColor(MAGENTA)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.border({ width: 1, color: MAGENTA })
.onClick(() => {
this.selectedRoom = r
this.showDissolveModal = true
})
}
.width('100%')
.padding(8)
.borderRadius(10)
.borderStyle(BorderStyle.Dashed)
.border({ width: 1, color: '#334065' })
.margin({ top: 10 })
}
.width(220)
.padding(14)
.borderRadius(16)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ right: 10 })
.onClick(() => {
this.selectedRoom = r
this.currentTab = 3
})
}, (r: VoiceRoom) => 'vr' + r.id)
}
.padding({ left: 2, right: 2 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
// 双列房间卡片
Row() {
Text('🏠 全部房间')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('共 ' + ROOM_CARDS.length + ' 间')
.fontSize(10)
.fontColor(TEXT_DIM)
}
.width('100%')
.margin({ top: 18, bottom: 8 })
Row() {
ForEach(ROOM_CARDS, (c: RoomCard, i: number) => {
if (i % 2 === 0) {
this.roomCardCell(c)
}
}, (c: RoomCard) => 'lc' + c.id)
}
.width('100%')
.alignItems(VerticalAlign.Top)
Row() {
ForEach(ROOM_CARDS, (c: RoomCard, i: number) => {
if (i % 2 === 1) {
this.roomCardCell(c)
}
}, (c: RoomCard) => 'rc' + c.id)
}
.width('100%')
.alignItems(VerticalAlign.Top)
.margin({ top: 10 })
Row() {
Text('更多房间加载中…')
.fontSize(11)
.fontColor(TEXT_DIM)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 14, bottom: 14 })
.borderRadius(12)
.backgroundColor(CARD_DARK)
.onClick(() => {
this.currentTab = 0
})
}
.width('100%')
.margin({ top: 14, bottom: 16 })
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 8 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
}
@Builder
roomCardCell(c: RoomCard) {
Column() {
Row() {
Text(c.gameIcon)
.fontSize(18)
.width(34)
.height(34)
.textAlign(TextAlign.Center)
.borderRadius(17)
.backgroundColor(CARD_DARK2)
Column() {
Text(c.name)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.maxLines(1)
Text(c.game)
.fontSize(9)
.fontColor(TEXT_DIM)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 8 })
}
.width('100%')
Row() {
Text('段位 ' + c.rank)
.fontSize(10)
.fontColor(TEXT_DIM)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(8)
.backgroundColor(CARD_DARK2)
Column().layoutWeight(1)
Text(c.online + '人')
.fontSize(10)
.fontColor(CYAN)
}
.width('100%')
.margin({ top: 10 })
Row() {
Text('缺 ' + c.need)
.fontSize(10)
.fontColor(MAGENTA)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('rgba(255,62,157,0.12)')
Column().layoutWeight(1)
Text(c.tag)
.fontSize(9)
.fontColor(CYAN)
}
.width('100%')
.margin({ top: 8 })
}
.layoutWeight(1)
.padding(12)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ right: 10 })
.alignItems(HorizontalAlign.Start)
}
// ==================== Tab1 大神陪玩 ====================
@Builder
tabPro() {
Scroll() {
Column() {
// 大神横滑大卡
Row() {
Text('🌟 明星大神')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('查看全部 >')
.fontSize(10)
.fontColor(TEXT_DIM)
.onClick(() => {
this.currentTab = 1
})
}
.width('100%')
.margin({ top: 6, bottom: 8 })
Scroll() {
Row() {
ForEach(PRO_SLIDERS, (p: ProSlider) => {
Column() {
// 段位徽章
Column() {
Text(p.rankIcon)
.fontSize(24)
}
.width(54)
.height(54)
.borderRadius(27)
.linearGradient(hotGradient())
.border({ width: 2, color: MAGENTA })
Text(p.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.margin({ top: 8 })
Text(p.rank)
.fontSize(10)
.fontColor(CYAN)
.margin({ top: 2 })
Text('⭐ ' + p.score + ' 分')
.fontSize(11)
.fontColor('#FFD54F')
.margin({ top: 6 })
Text(p.desc)
.fontSize(10)
.fontColor(TEXT_DIM)
.maxLines(1)
.margin({ top: 4 })
Row() {
Text('¥' + p.price + '/局')
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 8, bottom: 8 })
.borderRadius(14)
.linearGradient(cyanGradient())
.onClick(() => {
this.selectedPro = PRO_LIST[0]
this.showOrderModal = true
})
}
.width('100%')
.margin({ top: 8 })
}
.width(150)
.padding(12)
.borderRadius(16)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.alignItems(HorizontalAlign.Center)
.margin({ right: 10 })
.onClick(() => {
this.selectedPro = PRO_LIST[0]
this.showOrderModal = true
})
}, (p: ProSlider) => 'ps' + p.id)
}
.padding({ left: 2, right: 2 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
// 陪玩列表
Row() {
Text('🔥 热门陪玩')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('共 ' + PRO_LIST.length + ' 位')
.fontSize(10)
.fontColor(TEXT_DIM)
}
.width('100%')
.margin({ top: 18, bottom: 8 })
ForEach(PRO_LIST, (p: ProItem) => {
Row() {
Text(p.icon)
.fontSize(26)
.width(48)
.height(48)
.textAlign(TextAlign.Center)
.borderRadius(24)
.backgroundColor(CARD_DARK2)
.border({ width: 1, color: LINE_DARK })
Column() {
Row() {
Text(p.name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Text(p.rank)
.fontSize(9)
.fontColor(MAGENTA)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(7)
.backgroundColor('rgba(255,62,157,0.12)')
.margin({ left: 6 })
}
Row() {
ForEach([1, 2, 3, 4, 5], (s: number) => {
Text(s <= p.stars ? '⭐' : '☆')
.fontSize(9)
.margin({ right: 1 })
}, (s: number) => 'star' + s)
Text(p.score + ' 分')
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ left: 5 })
}
.margin({ top: 3 })
Row() {
Text(p.win)
.fontSize(10)
.fontColor(GREEN_OK)
Text('· 接单 ' + p.orders)
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ left: 4 })
}
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text(p.tag)
.fontSize(9)
.fontColor(CYAN)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(7)
.backgroundColor('rgba(0,229,255,0.10)')
Text('¥' + p.price)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(MAGENTA)
.margin({ top: 4 })
Text('/局')
.fontSize(9)
.fontColor(TEXT_DIM)
Text('立即下单')
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.linearGradient(cyanGradient())
.margin({ top: 6 })
.onClick(() => {
this.selectedPro = p
this.showOrderModal = true
})
}
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ bottom: 10 })
.onClick(() => {
this.selectedPro = p
this.showOrderModal = true
})
}, (p: ProItem) => 'pro' + p.id)
Row() {
Text('更多大神已就位,滑动加载')
.fontSize(11)
.fontColor(TEXT_DIM)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.borderRadius(12)
.backgroundColor(CARD_DARK)
.onClick(() => {
this.currentTab = 1
})
}
.width('100%')
.margin({ top: 4, bottom: 16 })
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 8 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
}
// ==================== Tab2 战队招募 ====================
@Builder
tabTeam() {
Scroll() {
Column() {
// 招募提示横幅
Row() {
Text('🛡️')
.fontSize(18)
Column() {
Text('正在招募的战队 ' + RECRUIT_POSTS.length + ' 支')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Text('上传你的段位战绩,一键申请入队')
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text('发布招募')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.padding({ left: 12, right: 12, top: 7, bottom: 7 })
.borderRadius(14)
.linearGradient(hotGradient())
.onClick(() => {
this.currentTab = 2
})
}
.width('100%')
.padding(14)
.borderRadius(14)
.linearGradient(rankGradient())
.margin({ top: 6, bottom: 14 })
ForEach(RECRUIT_POSTS, (t: RecruitPost) => {
Column() {
Row() {
Text(t.gameIcon)
.fontSize(20)
.width(40)
.height(40)
.textAlign(TextAlign.Center)
.borderRadius(20)
.backgroundColor(CARD_DARK2)
Column() {
Row() {
Text(t.team)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.maxLines(1)
Text(t.tag)
.fontSize(9)
.fontColor(MAGENTA)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(7)
.backgroundColor('rgba(255,62,157,0.12)')
.margin({ left: 6 })
}
Text(t.game + ' · ' + t.level)
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Text(t.leader)
.fontSize(10)
.fontColor(CYAN)
}
.width('100%')
// 招募位置 chip
Row() {
Text('招募位置')
.fontSize(10)
.fontColor(TEXT_DIM)
Text(t.position)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(CYAN)
.padding({ left: 10, right: 10, top: 5, bottom: 5 })
.borderRadius(12)
.backgroundColor('rgba(0,229,255,0.10)')
.border({ width: 1, color: '#0E3D4A' })
.margin({ left: 8 })
}
.width('100%')
.margin({ top: 10 })
Row() {
Text('入队要求')
.fontSize(10)
.fontColor(TEXT_DIM)
Text(t.require)
.fontSize(10)
.fontColor(TEXT_LIGHT)
.layoutWeight(1)
.margin({ left: 8 })
}
.width('100%')
.padding(10)
.borderRadius(10)
.backgroundColor(CARD_DARK2)
.margin({ top: 8 })
Row() {
Text('💬 回复 ' + t.replies)
.fontSize(10)
.fontColor(TEXT_DIM)
Column().layoutWeight(1)
Text('申请入队')
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.padding({ left: 18, right: 18, top: 8, bottom: 8 })
.borderRadius(16)
.linearGradient(cyanGradient())
.onClick(() => {
this.selectedTeam = t
this.posMask = 0
this.introText = ''
this.showJoinModal = true
})
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ bottom: 12 })
.onClick(() => {
this.selectedTeam = t
this.posMask = 0
this.introText = ''
this.showJoinModal = true
})
}, (t: RecruitPost) => 'team' + t.id)
Row() {
Text('没有心仪战队?点击发布你的招募需求')
.fontSize(11)
.fontColor(TEXT_DIM)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.borderRadius(12)
.backgroundColor(CARD_DARK)
.onClick(() => {
this.currentTab = 2
})
}
.width('100%')
.margin({ top: 4, bottom: 16 })
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 8 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
}
// ==================== Tab3 上分之路 ====================
@Builder
tabRank() {
Scroll() {
Column() {
// 近10局KDA柱状图
Row() {
Text('📊 近10局 KDA')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('胜局青色 / 败局灰色')
.fontSize(10)
.fontColor(TEXT_DIM)
}
.width('100%')
.margin({ top: 6, bottom: 8 })
Column() {
Row() {
ForEach(KDA_MATCHES, (m: KdaMatch, i: number) => {
Column() {
Text(String(i + 1))
.fontSize(9)
.fontColor(TEXT_DIM)
Column()
.width(14)
.height(barHeight(m.k))
.borderRadius(7)
.linearGradient(m.win ? cyanGradient() : rankGradient())
.backgroundColor(m.win ? CYAN : LOSS_GRAY)
.margin({ top: 4 })
Text(String(m.k))
.fontSize(9)
.fontWeight(FontWeight.Bold)
.fontColor(m.win ? CYAN : LOSS_GRAY)
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.End)
.onClick(() => {
this.selectedMatch = m
this.showKdaModal = true
})
}, (m: KdaMatch) => 'kda' + m.id)
}
.width('100%')
.height(168)
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
Text('点击柱子查看单局战绩详情')
.fontSize(9)
.fontColor(TEXT_DIM)
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 6 })
// 段位晋级进度
Row() {
Text('🚀 段位晋级进度')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('星耀 → 王者 · 87%')
.fontSize(10)
.fontColor(CYAN)
}
.width('100%')
.margin({ top: 18, bottom: 8 })
Column() {
Row() {
ForEach(RANK_STEPS, (s: RankStep, i: number) => {
Column() {
Column() {
Text(s.done ? '✓' : (i === 3 ? '👑' : '★'))
.fontSize(14)
.fontColor(s.done ? BG_DARK : CYAN)
.fontWeight(FontWeight.Bold)
}
.width(34)
.height(34)
.borderRadius(17)
.backgroundColor(s.done ? CYAN : CARD_DARK2)
.border({ width: 1, color: s.done ? CYAN : LINE_DARK })
Text(s.label)
.fontSize(10)
.fontWeight(i === 2 ? FontWeight.Bold : FontWeight.Normal)
.fontColor(i === 2 ? CYAN : TEXT_DIM)
.margin({ top: 6 })
Text(s.desc)
.fontSize(9)
.fontColor(TEXT_DIM)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
}, (s: RankStep) => 'step' + s.id)
}
.width('100%')
Row() {
Row() { }
.layoutWeight(87)
.height(6)
.borderRadius(3)
.linearGradient(cyanGradient())
Row() { }
.layoutWeight(13)
.height(6)
.borderRadius(3)
.backgroundColor(CARD_DARK2)
.margin({ left: 4 })
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
// 英雄胜率横条
Row() {
Text('⚔️ 英雄胜率')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('共 ' + HERO_WINS.length + ' 位常用')
.fontSize(10)
.fontColor(TEXT_DIM)
}
.width('100%')
.margin({ top: 18, bottom: 8 })
ForEach(HERO_WINS, (h: HeroWin) => {
Row() {
Text(h.icon)
.fontSize(18)
.width(32)
.textAlign(TextAlign.Center)
Column() {
Row() {
Text(h.hero)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text(h.rate + '%')
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(h.rate >= 60 ? CYAN : TEXT_DIM)
}
.width('100%')
Row() {
Row() {
Column()
.width(rateWidth(h.rate))
.height(6)
.borderRadius(3)
.linearGradient(cyanGradient())
}
.width('100%')
.height(6)
.borderRadius(3)
.backgroundColor(CARD_DARK2)
}
.width('100%')
.margin({ top: 5 })
Text(h.plays + ' 场 · 当前赛季')
.fontSize(9)
.fontColor(TEXT_DIM)
.width('100%')
.margin({ top: 3 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
}
.width('100%')
.padding(10)
.borderRadius(12)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ bottom: 8 })
.onClick(() => {
this.selectedMatch = KDA_MATCHES[0]
this.showKdaModal = true
})
}, (h: HeroWin) => 'hw' + h.id)
Row() {
Text('查看完整战绩数据 >')
.fontSize(11)
.fontColor(CYAN)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.borderRadius(12)
.backgroundColor(CARD_DARK)
.onClick(() => {
this.currentTab = 5
})
}
.width('100%')
.margin({ top: 4, bottom: 16 })
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 8 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
}
// ==================== Tab4 赛事战报 ====================
@Builder
tabMatch() {
Scroll() {
Column() {
// 赛程横滑
Row() {
Text('🗓️ 赛事赛程')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('完整赛程表 >')
.fontSize(10)
.fontColor(TEXT_DIM)
.onClick(() => {
this.currentTab = 4
})
}
.width('100%')
.margin({ top: 6, bottom: 8 })
Scroll() {
Row() {
ForEach(SCHEDULE_LIST, (s: ScheduleItem) => {
Column() {
Row() {
Text(s.game)
.fontSize(10)
.fontColor(CYAN)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.borderRadius(8)
.backgroundColor('rgba(0,229,255,0.12)')
Column().layoutWeight(1)
Text(s.tag)
.fontSize(9)
.fontColor(MAGENTA)
}
.width('100%')
Text(s.desc)
.fontSize(12)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.maxLines(1)
.width('100%')
.margin({ top: 8 })
Text('⏰ ' + s.time)
.fontSize(10)
.fontColor(TEXT_DIM)
.width('100%')
.margin({ top: 6 })
Row() {
Text('预约观赛')
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 6, bottom: 6 })
.borderRadius(10)
.linearGradient(cyanGradient())
.onClick(() => {
this.currentTab = 4
})
}
.width('100%')
.margin({ top: 8 })
}
.width(170)
.padding(12)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ right: 10 })
.onClick(() => {
this.currentTab = 4
})
}, (s: ScheduleItem) => 'sch' + s.id)
}
.padding({ left: 2, right: 2 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
// 比赛列表
Row() {
Text('🏆 最新战报')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('共 ' + MATCH_LIST.length + ' 场')
.fontSize(10)
.fontColor(TEXT_DIM)
}
.width('100%')
.margin({ top: 18, bottom: 8 })
ForEach(MATCH_LIST, (m: MatchGame) => {
Column() {
Row() {
Column() {
Text(m.teamA)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.maxLines(1)
Text('点击查看')
.fontSize(9)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
Column() {
Row() {
Text(String(m.scoreA))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(CYAN)
Text(' : ')
.fontSize(14)
.fontColor(TEXT_DIM)
Text(String(m.scoreB))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(MAGENTA)
}
Text('VS 对阵')
.fontSize(9)
.fontColor(TEXT_DIM)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
Column() {
Text(m.teamB)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.maxLines(1)
Text(m.tag)
.fontSize(9)
.fontColor(CYAN)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Center)
.layoutWeight(1)
}
.width('100%')
.padding({ top: 4, bottom: 10 })
Row() {
Text(m.result)
.fontSize(10)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(10)
.backgroundColor(m.resultColor)
Text('比赛时间 ' + m.time)
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ left: 10 })
Column().layoutWeight(1)
Text('查看战报 >')
.fontSize(10)
.fontColor(CYAN)
.onClick(() => {
this.currentTab = 4
})
}
.width('100%')
.padding({ top: 8, bottom: 4 })
}
.width('100%')
.padding(14)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ bottom: 10 })
.onClick(() => {
this.currentTab = 4
})
}, (m: MatchGame) => 'match' + m.id)
Row() {
Text('数据来源:官方赛事中心 · 点击展开更多战报')
.fontSize(10)
.fontColor(TEXT_DIM)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.borderRadius(12)
.backgroundColor(CARD_DARK)
.onClick(() => {
this.currentTab = 4
})
}
.width('100%')
.margin({ top: 4, bottom: 16 })
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 8 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
}
// ==================== Tab5 我的 ====================
@Builder
tabMine() {
Scroll() {
Column() {
// 个人卡片
Row() {
Column() {
Text('👑')
.fontSize(24)
}
.width(52)
.height(52)
.borderRadius(26)
.linearGradient(hotGradient())
.border({ width: 2, color: MAGENTA })
Column() {
Text('夜光丶')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Text('荣耀王者 · 打野 · 巅峰 2160')
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
Column() {
Text('✏️')
.fontSize(14)
.width(32)
.height(32)
.textAlign(TextAlign.Center)
.borderRadius(16)
.backgroundColor(CARD_DARK2)
.onClick(() => {
this.currentTab = 5
})
Text('编辑')
.fontSize(8)
.fontColor(TEXT_DIM)
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Center)
}
.width('100%')
.padding(14)
.borderRadius(14)
.linearGradient(rankGradient())
.margin({ top: 6 })
// 战绩数据面板
Row() {
ForEach(STAT_PANEL, (s: StatPanelItem) => {
Column() {
Text(s.value)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(s.color)
Text(s.label)
.fontSize(11)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
Text(s.unit)
.fontSize(9)
.fontColor(TEXT_DIM)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 14, bottom: 14 })
.onClick(() => {
this.selectedMatch = KDA_MATCHES[0]
this.showKdaModal = true
})
}, (s: StatPanelItem) => 'stat' + s.id)
}
.width('100%')
.padding(10)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
.margin({ top: 14 })
// 常玩英雄网格
Row() {
Text('🎮 常玩英雄')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('查看全部 >')
.fontSize(10)
.fontColor(TEXT_DIM)
.onClick(() => {
this.currentTab = 3
})
}
.width('100%')
.margin({ top: 18, bottom: 8 })
Column() {
Row() {
ForEach(HERO_GRID, (h: HeroGridItem, i: number) => {
if (i < 4) {
this.heroGridCell(h)
}
}, (h: HeroGridItem) => 'hg1' + h.id)
}
.width('100%')
Row() {
ForEach(HERO_GRID, (h: HeroGridItem, i: number) => {
if (i >= 4) {
this.heroGridCell(h)
}
}, (h: HeroGridItem) => 'hg2' + h.id)
}
.width('100%')
.margin({ top: 10 })
}
.width('100%')
.padding(10)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
// 历史段位时间轴
Row() {
Text('⏳ 历史段位')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
Text('6个赛季')
.fontSize(10)
.fontColor(TEXT_DIM)
}
.width('100%')
.margin({ top: 18, bottom: 8 })
Column() {
ForEach(RANK_HISTORY, (r: RankHistoryItem, i: number) => {
Row() {
Column() {
Column()
.width(12)
.height(12)
.borderRadius(6)
.backgroundColor(i === RANK_HISTORY.length - 1 ? MAGENTA : CYAN)
.border({ width: 2, color: '#0B3A45' })
if (i !== RANK_HISTORY.length - 1) {
Column()
.width(2)
.height(38)
.backgroundColor('#232C4A')
.margin({ top: 4 })
} else {
Column()
.width(2)
.height(10)
.backgroundColor('#232C4A')
.margin({ top: 4 })
}
}
.alignItems(HorizontalAlign.Center)
.width(30)
Column() {
Row() {
Text(r.season)
.fontSize(13)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Text(r.rank)
.fontSize(11)
.fontColor(CYAN)
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(9)
.backgroundColor('rgba(0,229,255,0.10)')
.margin({ left: 8 })
}
Row() {
Text(r.time + ' · ' + r.note)
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ top: 4 })
}
.width('100%')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.padding({ top: 2 })
}
.width('100%')
.margin({ bottom: 2 })
.onClick(() => {
this.currentTab = 3
})
}, (r: RankHistoryItem) => 'rankHis' + r.id)
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(CARD_DARK)
.border({ width: 1, color: LINE_DARK })
Row() {
Text('我的陪玩订单 · 战队收藏 · 实名认证')
.fontSize(11)
.fontColor(TEXT_DIM)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 12, bottom: 12 })
.borderRadius(12)
.backgroundColor(CARD_DARK)
.onClick(() => {
this.currentTab = 5
})
}
.width('100%')
.margin({ top: 14, bottom: 16 })
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 8 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
}
@Builder
heroGridCell(h: HeroGridItem) {
Column() {
Text(h.icon)
.fontSize(22)
.width(44)
.height(44)
.textAlign(TextAlign.Center)
.borderRadius(22)
.backgroundColor(CARD_DARK2)
Text(h.hero)
.fontSize(11)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
.margin({ top: 5 })
Text(h.plays + '场 · ' + h.win)
.fontSize(9)
.fontColor(TEXT_DIM)
.margin({ top: 2 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 8, bottom: 8 })
.borderRadius(10)
.backgroundColor(CARD_DARK2)
.margin({ right: 8 })
.onClick(() => {
this.selectedMatch = KDA_MATCHES[0]
this.showKdaModal = true
})
}
// ==================== 弹框1:创建房间 ====================
@Builder
createRoomOverlay(onClose: () => void = () => {}) {
Column() {
Column() {
this.grabHandle()
Row() {
Text('🎙️ 创建开黑房间')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
this.closeBtn(onClose)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
Scroll() {
Column() {
Text('房间名称')
.fontSize(13)
.fontColor(TEXT_DIM)
.width('100%')
.margin({ top: 10 })
TextInput({ placeholder: '输入房间名称(限20字)', text: this.roomName })
.fontSize(13)
.fontColor(TEXT_LIGHT)
.placeholderColor(TEXT_DIM)
.backgroundColor(CARD_DARK2)
.borderRadius(12)
.height(44)
.width('100%')
.margin({ top: 6 })
.onChange((v: string) => {
this.roomName = v
})
Text('选择游戏')
.fontSize(13)
.fontColor(TEXT_DIM)
.width('100%')
.margin({ top: 14 })
Row() {
ForEach(GAME_TYPES, (g: GameType, i: number) => {
Row() {
Text(g.icon)
.fontSize(12)
Text(g.name)
.fontSize(11)
.fontColor(this.roomGameIndex === i ? BG_DARK : TEXT_LIGHT)
.fontWeight(this.roomGameIndex === i ? FontWeight.Bold : FontWeight.Normal)
.margin({ left: 4 })
}
.padding({ left: 10, right: 10, top: 7, bottom: 7 })
.borderRadius(14)
.backgroundColor(this.roomGameIndex === i ? CYAN : CARD_DARK2)
.margin({ right: 8 })
.onClick(() => {
this.roomGameIndex = i
})
}, (g: GameType) => 'rg' + g.id)
}
.width('100%')
.margin({ top: 6 })
Text('游戏模式')
.fontSize(13)
.fontColor(TEXT_DIM)
.width('100%')
.margin({ top: 14 })
Row() {
ForEach(['排位模式', '娱乐模式', '大乱斗'], (md: string, i: number) => {
Column() {
Text(md)
.fontSize(11)
.fontColor(this.roomModeIndex === i ? CYAN : TEXT_DIM)
.fontWeight(this.roomModeIndex === i ? FontWeight.Bold : FontWeight.Normal)
Column()
.width(this.roomModeIndex === i ? 14 : 14)
.height(14)
.borderRadius(7)
.backgroundColor(this.roomModeIndex === i ? CYAN : CARD_DARK2)
.border({ width: 1, color: this.roomModeIndex === i ? CYAN : LINE_DARK })
.margin({ top: 5 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 10 })
.borderRadius(12)
.backgroundColor(this.roomModeIndex === i ? 'rgba(0,229,255,0.08)' : CARD_DARK2)
.margin({ left: 3, right: 3 })
.onClick(() => {
this.roomModeIndex = i
})
}, (md: string) => 'mode' + md)
}
.width('100%')
.margin({ top: 6 })
Text('房间人数')
.fontSize(13)
.fontColor(TEXT_DIM)
.width('100%')
.margin({ top: 14 })
Row() {
ForEach([5, 6, 8, 10], (n: number, i: number) => {
Column() {
Text(String(n))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(this.roomCountIndex === i ? BG_DARK : TEXT_LIGHT)
Text('人房')
.fontSize(9)
.fontColor(this.roomCountIndex === i ? BG_DARK : TEXT_DIM)
.margin({ top: 1 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Center)
.padding({ top: 10, bottom: 10 })
.borderRadius(12)
.backgroundColor(this.roomCountIndex === i ? CYAN : CARD_DARK2)
.margin({ left: 3, right: 3 })
.onClick(() => {
this.roomCountIndex = i
})
}, (n: number) => 'cnt' + n)
}
.width('100%')
.margin({ top: 6 })
Row() {
Text('🛡 房间创建后自动上麦,队友可通过房间号一键加入')
.fontSize(10)
.fontColor(TEXT_DIM)
}
.width('100%')
.padding(10)
.borderRadius(10)
.backgroundColor('#0F1B33')
.margin({ top: 14 })
Row() {
Text('立即创建')
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(BG_DARK)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.padding({ top: 13, bottom: 13 })
.borderRadius(24)
.linearGradient(cyanGradient())
.onClick(() => {
onClose()
})
}
.width('100%')
.margin({ top: 16, bottom: 20 })
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.layoutWeight(1)
.width('100%')
}
.width('100%')
.constraintSize({ maxHeight: '86%' })
.backgroundColor(CARD_DARK)
.borderRadius({ topLeft: 24, topRight: 24 })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.7)')
.justifyContent(FlexAlign.End)
.onClick(() => {
onClose()
})
}
// ==================== 弹框2:陪玩下单 ====================
@Builder
orderProOverlay(onClose: () => void = () => {}) {
Column() {
Column() {
this.grabHandle()
Row() {
Text('🦾 下单陪玩')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column().layoutWeight(1)
this.closeBtn(onClose)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14 })
Scroll() {
Column() {
// 大神信息头
Row() {
Text(this.selectedPro.icon)
.fontSize(26)
.width(52)
.height(52)
.textAlign(TextAlign.Center)
.borderRadius(26)
.backgroundColor(CARD_DARK2)
Column() {
Row() {
Text(this.selectedPro.name)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor(TEXT_LIGHT)
Column() {
Text('👑')
.fontSize(13)
}
.width(22)
.height(22)
.borderRadius(11)
.linearGradient(hotGradient())
.margin({ left: 6 })
}
Text(this.selectedPro.rank + ' · ' + this.selectedPro.game)
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
Row() {
Text('⭐ ' + this.selectedPro.score + ' 分')
.fontSize(10)
.fontColor('#FFD54F')
Text(this.selectedPro.tag)
.fontSize(9)
.fontColor(CYAN)
.margin({ left: 6 })
}
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 10 })
Column() {
Text(this.selectedPro.win)
.fontSize(10)
.fontColor(GREEN_OK)
Text('已接 ' + this.selectedPro.orders + ' 单')
.fontSize(10)
.fontColor(TEXT_DIM)
.margin({ top: 3 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.borderRadius(14)
.backgroundColor(CARD_DARK2)
.margin({ top: 12 })
Text('下单局数')
.fontSize(13)
.fontColor(TEXT_DIM)
.width('100%')
.margin({ top: 14 })
Row() {
Text('−')
.fontSize(18)
.fontColor(TEXT_LIGHT)
.width(38)
.height(38)
.textAlign(TextAlign.Center)
.borderRadius(19)
.backgroundColor(CARD_DARK2)
.onClick(() => {
if (this.orderCount > 1) {
this.orderCount = this.orderCount - 1
}
})
Column() {
Text(String(this.orderCount))
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(CYAN)
Text('局 · 可分段结算')
.fontSize(9)
.
总结

本文深入分析了基于 HarmonyOS ArkTS API 24 构建的电竞开黑陪玩应用,从类型系统设计、主题色配置、头部导航构建到 KDA 柱状图可视化、战队招募卡片、VS 对战卡片和五个功能弹窗的完整实现进行了逐一剖析。该应用充分展现了 ArkTS 在构建电竞类复杂社交应用方面的技术优势:通过 @State 状态管理实现了弹窗、列表和图表的自动刷新,通过 @Builder 方法拆分实现了大量 UI 组件的复用,通过纯声明式 Column 组件实现了柱状图、进度条和星级评分等数据可视化效果而无需引入任何第三方图表库。
从架构设计角度来看,该应用的数据层通过 TypeScript 接口精确定义了电竞场景中的各类业务实体,逻辑层通过纯函数封装了柱状图高度计算、费用合计和胜率宽度转换等业务逻辑,视图层通过细粒度的 @Builder 方法实现了语音房间卡片、陪玩列表行、招募帖子卡片和对战卡片等复杂 UI 组件。三层之间通过 @State 状态变量进行数据传递和 UI 驱动,形成了清晰的数据流和职责边界。特别是 posMask 位掩码多选机制和 layoutWeight 比例进度条等技巧,展示了开发者在解决实际问题时对 ArkTS 框架能力的深入理解和灵活运用。
三组渐变色函数(rankGradient、hotGradient、cyanGradient)分别服务于段位、热度和操作三个语义场景,使不同功能区域在视觉上具有明确的辨识度。KDA 柱状图通过胜负颜色的差异化(青色胜局、灰色败局)实现了数据的直观传达,VS 对战卡片通过比分数字的颜色编码(A队青色、B队洋红)增强了对抗感的视觉表达。这些精心设计的视觉细节充分证明了 HarmonyOS 6.1.1 的 ArkTS 框架在构建视觉表现力丰富的移动端应用方面的成熟度和竞争力。
更多推荐


所有评论(0)