引言

在这里插入图片描述

随着移动电竞产业的蓬勃发展,游戏陪玩、代练、技能交易等垂直服务需求日益增长。一个优秀的游戏陪玩平台不仅需要涵盖陪玩大厅、代练服务、技能市场、社区互动、订单管理和个人中心等完整业务链路,还需要在移动端实现流畅的交互体验和精致的可视化呈现。本文将深入剖析一个基于ArkTS声明式UI范式构建的多模块游戏陪玩应用,该应用以"陪玩蓝×能量绿"的电竞清新蓝调为视觉主线,通过六大功能Tab全面覆盖了从陪玩发现到订单完成的全流程业务场景。

在技术架构层面,该应用采用HarmonyOS ArkTS声明式开发范式,以@Entry@Component装饰器为核心构建组件树,通过@State状态管理驱动UI响应式更新。应用整体由一个主入口组件统一管理Tab切换与弹层调度,下设六个独立Tab组件和四个辅助弹层组件,各组件之间通过回调函数和状态绑定实现松耦合通信。数据层采用纯静态数据集合与TypeScript接口类型约束,保证了类型安全与可维护性。布局上大量运用ScrollFlexRow/Column嵌套以及layoutWeight弹性分配,实现了自适应多分辨率的能力。

从业务设计角度来看,该应用围绕"多多游戏陪玩"这一核心产品形态,精心设计了陪玩推荐大卡+双列瀑布流列表的发现页、带进度跟踪的代练服务页、分类标签+横滑热门+纵向列表的技能市场页、话题卡片+动态信息流的社区页、以及渐变头部+消费柱状图+段位信息+成就徽章+订单收藏的个人中心页。每个Tab都有差异化的布局策略,既保持了视觉一致性,又针对不同业务场景做了定制化优化。此外,应用还集成了底部弹出的预约面板(BookingSheet)、编辑资料面板(EditProfileSheet)、取消预约对话框和删除收藏对话框,通过bindSheetbindContentCover两种模态展示方式,构建了完整的交互闭环。

一、类型定义与色彩体系

1.1 核心业务接口定义

在这里插入图片描述

应用首先通过一系列interface定义了完整的业务数据模型,这是整个应用的类型基石。

interface Companion84 {
  id: number
  name: string
  avatar: string
  game: string
  rank: string
  tags: string[]
  price: number
  unit: string
  rating: number
  orders: number
  online: boolean
  gender: string
  intro: string
  level: number
  voice: string
}

interface Game84 {
  id: number
  name: string
  icon: string
  color: string
  bg: string
  players: string
}

interface BoostItem84 {
  id: number
  game: string
  title: string
  fromRank: string
  toRank: string
  price: number
  duration: string
  booster: string
  rating: number
  status: string
  progress: number
  image: string
  color: string
  desc: string
}

这里定义了Companion84(陪玩师)、Game84(游戏)、BoostItem84(代练项)三个核心接口。Companion84包含了陪玩师的完整画像信息:头像、所属游戏、段位、标签、价格、评分、接单量、在线状态、性别、简介、等级和声音特征,字段设计非常贴合真实业务场景。Game84定义了游戏分类的图标、主题色、背景色和参与人数模式。BoostItem84则描述了代练服务的起点段位、目标段位、价格、时长、代练师、状态和进度等关键信息。所有字段均使用TypeScript基础类型,确保了类型推断的精确性。

1.2 技能、社区与个人中心接口

在这里插入图片描述

interface SkillItem84 {
  id: number
  name: string
  category: string
  provider: string
  avatar: string
  price: number
  unit: string
  rating: number
  orders: number
  tags: string[]
  desc: string
  color: string
}

interface PostItem84 {
  id: number
  user: string
  avatar: string
  title: string
  content: string
  likes: number
  comments: number
  time: string
  tag: string
}

interface TopicItem84 {
  id: number
  title: string
  hot: number
  posts: number
}

interface Achievement84 {
  id: number
  name: string
  icon: string
  desc: string
  unlocked: boolean
  color: string
}

SkillItem84定义了技能市场中的服务项,包含分类、提供者、标签和描述等字段。PostItem84TopicItem84分别定义了社区动态和热门话题的数据结构。Achievement84则用于个人中心的成就徽章展示,其中unlocked布尔字段控制徽章的显示状态(已解锁为彩色,未解锁为灰色半透明)。这些接口共同构成了应用的数据契约层,使得每个组件在消费数据时都能获得完整的类型保障。

1.3 统一色彩体系

在这里插入图片描述

interface ColorPalette84 {
  primary: string
  primaryLight: string
  primaryDark: string
  accent: string
  bg: string
  cardBg: string
  textPrimary: string
  textSecondary: string
  textHint: string
  border: string
  white: string
  success: string
  warning: string
  danger: string
}

const COLORS84: ColorPalette84 = {
  primary: '#1565C0',
  primaryLight: '#42A5F5',
  primaryDark: '#0D47A1',
  accent: '#00C853',
  bg: '#E3F2FD',
  cardBg: '#FFFFFF',
  textPrimary: '#0D47A1',
  textSecondary: '#5C7B8A',
  textHint: '#B0C4DE',
  border: '#BBDEFB',
  white: '#FFFFFF',
  success: '#00C853',
  warning: '#FF8F00',
  danger: '#E53935'
}

应用采用了统一的色彩管理体系,通过ColorPalette84接口定义颜色字段,再以COLORS84常量实例化。主色#1565C0(陪玩蓝)与强调色#00C853(能量绿)构成了电竞清新蓝调的核心配色,背景色#E3F2FD提供柔和的视觉底色。这种集中式色彩管理的优势在于:所有组件统一引用COLORS84常量,当需要切换主题或调整配色时,只需修改一处即可全局生效,极大地提升了可维护性。同时,语义化的字段命名(如textPrimarytextSecondarytextHint)使开发者在编写UI时能直观地选择正确的颜色层级。

二、静态数据层

2.1 游戏分类与陪玩师数据

在这里插入图片描述

const GAMES_84: Game84[] = [
  { id: 1, name: '王者荣耀', icon: '⚔️', color: '#1565C0', bg: '#E3F2FD', players: '5v5' },
  { id: 2, name: '英雄联盟', icon: '🛡️', color: '#0D47A1', bg: '#E8EAF6', players: '5v5' },
  { id: 3, name: '原神', icon: '⚔️', color: '#00838F', bg: '#E0F7FA', players: '4人' },
  { id: 4, name: '和平精英', icon: '🎯', color: '#2E7D32', bg: '#E8F5E9', players: '4人' },
  { id: 5, name: '永劫无间', icon: '🗡️', color: '#37474F', bg: '#ECEFF1', players: '3人' },
  { id: 6, name: 'DNF手游', icon: '💥', color: '#C62828', bg: '#FFEBEE', players: '4人' },
  { id: 7, name: '第五人格', icon: '🎭', color: '#4A148C', bg: '#F3E5F5', players: '1v4' },
  { id: 8, name: '金铲铲之战', icon: '🛡️', color: '#1565C0', bg: '#E3F2FD', players: '8人' }
]

const COMPANIONS_84: Companion84[] = [
  { id: 1, name: '小鹿姐姐', avatar: '🦌', game: '王者荣耀', rank: '王者50星', tags: ['国服前100', '温柔', '教学'], price: 30, unit: '元/局', rating: 4.9, orders: 3421, online: true, gender: '女', intro: '国服前100打野,耐心教学,带飞全场', level: 15, voice: '甜妹音' },
  { id: 2, name: '阿凯老师', avatar: '🦁', game: '英雄联盟', rank: '钻石一', tags: ['国服前500', '战术大师', '稳定'], price: 25, unit: '元/局', rating: 4.8, orders: 2156, online: true, gender: '男', intro: '十年老玩家,擅长打野和辅助,带你稳定上分', level: 12, voice: '青年音' },
  { id: 3, name: '糖糖喵', avatar: '🐱', game: '原神', rank: '满级玩家', tags: ['深渊满星', '温柔', '攻略'], price: 20, unit: '元/小时', rating: 5.0, orders: 1987, online: false, gender: '女', intro: '原神深渊满星攻略达人,带你轻松通关', level: 18, voice: '萝莉音' }
  // ... 更多陪玩师数据
]

GAMES_84定义了8款热门游戏分类,每个游戏都有专属的图标emoji、主题色和背景色,用于游戏分类横滑区域的视觉区分。COMPANIONS_84则包含了10位陪玩师的完整信息,每位陪玩师都有不同的游戏专长、段位、标签、价格和声音特征。这些数据使用Emoji作为头像,既节省了图片资源加载开销,又保持了视觉趣味性。在实际项目中,这些静态数组可以轻松替换为网络请求返回的动态数据,而接口类型定义保证了数据结构的稳定性。

2.2 代练、技能、社区与个人数据

在这里插入图片描述

const BOOSTS_84: BoostItem84[] = [
  { id: 1, game: '王者荣耀', title: '星耀→王者', fromRank: '星耀3', toRank: '王者0星', price: 288, duration: '3天', booster: '小鹿姐姐', rating: 4.9, status: '进行中', progress: 60, image: '⚔️', color: '#1565C0', desc: '星耀3到王者0星,含3星保险' },
  { id: 4, game: 'DNF手游', title: '困难团本代打', fromRank: '普通', toRank: '困难通关', price: 68, duration: '1天', booster: '暴击大师', rating: 4.8, status: '已完成', progress: 100, image: '💥', color: '#C62828', desc: '困难团本代打,保证掉落装备' }
  // ... 更多代练项
]

const SKILLS_84: SkillItem84[] = [
  { id: 1, name: '王者荣耀打野教学', category: '教学', provider: '小鹿姐姐', avatar: '🦌', price: 50, unit: '元/小时', rating: 4.9, orders: 876, tags: ['国服前100', '一对一', '实战'], desc: '国服前100打野一对一教学,从英雄选择到打野路线全教学', color: '#1565C0' }
  // ... 更多技能项
]

const POSTS_84: PostItem84[] = [
  { id: 1, user: '小鹿姐姐', avatar: '🦌', title: 'S35赛季打野英雄T度排行', content: '新赛季打野英雄强度分析,从T0到T3全面解读', likes: 3456, comments: 234, time: '1小时前', tag: '攻略' }
  // ... 更多社区动态
]

const ACHIEVEMENTS_84: Achievement84[] = [
  { id: 1, name: '初次下单', icon: '🎯', desc: '完成第一笔陪玩订单', unlocked: true, color: '#1565C0' },
  { id: 5, name: '赛季王者', icon: '👑', desc: '达到王者段位', unlocked: false, color: '#E53935' }
  // ... 更多成就徽章
]

代练数据BOOSTS_84包含了8个代练服务项,每个项目都有status(进行中/待接单/已完成)和progress(进度百分比)字段,用于代练Tab中的进度条展示。SKILLS_84定义了8项技能服务,覆盖教学、攻略、陪玩、陪聊等分类。POSTS_84包含社区动态信息,ACHIEVEMENTS_84则定义了9个成就徽章,其中部分已解锁、部分未解锁,通过unlocked字段控制UI上的彩色/灰色显示效果。这些静态数据的组织方式清晰明了,每个数据集合都与其对应的Tab组件一一映射。

三、预约弹层组件

在这里插入图片描述

3.1 预约陪玩面板

预约面板是应用中最复杂的辅助组件之一,它以底部弹层的形式提供完整的下单流程。

@Component
export struct BookingSheet84 {
  @State companionName: string = ''
  @State companionGame: string = ''
  @State companionPrice: number = 30
  @State selectedGame: number = 0
  @State selectedSlot: number = 0
  @State duration: number = 1
  @State remark: string = ''
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Column()
        .width(40)
        .height(4)
        .borderRadius(2)
        .backgroundColor(COLORS84.border)
        .margin({ top: 12, bottom: 16 })

      Row() {
        Text('预约陪玩')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS84.textPrimary)
        Text('').layoutWeight(1)
        Text('✕')
          .fontSize(22)
          .fontColor(COLORS84.textHint)
          .onClick(() => { this.onClose() })
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 16 })

      // 陪玩信息
      Row() {
        Text('🦌')
          .fontSize(36)
          .width(56)
          .height(56)
          .textAlign(TextAlign.Center)
          .backgroundColor(COLORS84.bg)
          .borderRadius(12)
        Column() {
          Text(this.companionName)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS84.textPrimary)
          Text(this.companionGame)
            .fontSize(12)
            .fontColor(COLORS84.textSecondary)
            .margin({ top: 4 })
          Row() {
            Text('⭐' + 4.9.toString())
              .fontSize(12)
              .fontColor(COLORS84.warning)
            Text('  在线')
              .fontSize(11)
              .fontColor(COLORS84.success)
          }
          .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .margin({ left: 12 })
        .layoutWeight(1)
        Column() {
          Text('¥' + this.companionPrice.toString())
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS84.accent)
          Text('元/局')
            .fontSize(11)
            .fontColor(COLORS84.textSecondary)
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 20 })
    }
    .width('100%')
    .backgroundColor(COLORS84.cardBg)
    .borderRadius({ topLeft: 24, topRight: 24 })
  }
}

BookingSheet84组件顶部使用了一个40×4的圆角条作为拖拽指示器,这是HarmonyOS底部弹层的标准设计语言。组件通过@State管理了六个内部状态:陪玩师名称、游戏、价格、选中的游戏索引、选中的时段索引和预约时长。同时定义了onConfirmonClose两个回调函数属性,供父组件在确认或取消时执行相应逻辑。陪玩信息区域通过Row+Column嵌套展示了头像、名称、游戏、评分和价格,布局上使用layoutWeight(1)实现弹性空间分配,使右侧价格列自动靠右对齐。

3.2 游戏选择与时段选择

      // 游戏选择
      Column() {
        Text('选择游戏')
          .fontSize(14)
          .fontColor(COLORS84.textSecondary)
          .margin({ bottom: 10 })
        Scroll() {
          Row() {
            ForEach(GAMES_84.slice(0, 6), (game: Game84, index: number) => {
              Column() {
                Text(game.icon)
                  .fontSize(24)
                  .width(40)
                  .height(40)
                  .textAlign(TextAlign.Center)
                  .borderRadius(10)
                  .backgroundColor(this.selectedGame === index ? COLORS84.white : game.bg)
                Text(game.name)
                  .fontSize(10)
                  .fontColor(this.selectedGame === index ? COLORS84.white : COLORS84.textPrimary)
                  .margin({ top: 4 })
              }
              .margin({ right: 10 })
              .onClick(() => { this.selectedGame = index })
            }, (game: Game84) => game.id.toString())
          }
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 16 })

      // 时段选择
      Column() {
        Text('选择时段')
          .fontSize(14)
          .fontColor(COLORS84.textSecondary)
          .margin({ bottom: 10 })
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(['09:00-12:00', '12:00-15:00', '15:00-18:00', '18:00-21:00', '21:00-24:00', '00:00-03:00'], (slot: string, index: number) => {
            Text(slot)
              .fontSize(12)
              .fontColor(this.selectedSlot === index ? COLORS84.white : COLORS84.textSecondary)
              .padding({ left: 12, right: 12, top: 8, bottom: 8 })
              .borderRadius(16)
              .backgroundColor(this.selectedSlot === index ? COLORS84.primary : COLORS84.bg)
              .margin({ right: 8, bottom: 8 })
              .onClick(() => { this.selectedSlot = index })
          }, (slot: string) => slot)
        }
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 16 })

游戏选择区域使用横向Scroll+Row+ForEach构建可滚动列表,通过this.selectedGame === index三元表达式动态切换选中项的背景色和文字色,实现了单选交互效果。时段选择则使用Flex组件配合FlexWrap.Wrap实现自动换行布局,6个时段标签会根据屏幕宽度自动排列。两种选择器都采用相同的设计模式:选中状态使用主色背景+白色文字,未选中状态使用浅色背景+深色文字,视觉反馈清晰直观。

3.3 时长控制与价格合计

      // 时长
      Row() {
        Text('预约时长')
          .fontSize(14)
          .fontColor(COLORS84.textSecondary)
        Text('').layoutWeight(1)
        Row() {
          Text('−')
            .fontSize(18)
            .fontColor(COLORS84.textPrimary)
            .width(36)
            .height(36)
            .textAlign(TextAlign.Center)
            .backgroundColor(COLORS84.bg)
            .borderRadius({ topLeft: 8, bottomLeft: 8 })
            .onClick(() => { if (this.duration > 1) { this.duration-- } })
          Text(this.duration.toString() + '局')
            .fontSize(15)
            .fontColor(COLORS84.textPrimary)
            .width(60)
            .height(36)
            .textAlign(TextAlign.Center)
            .backgroundColor(COLORS84.white)
          Text('+')
            .fontSize(18)
            .fontColor(COLORS84.textPrimary)
            .width(36)
            .height(36)
            .textAlign(TextAlign.Center)
            .backgroundColor(COLORS84.bg)
            .borderRadius({ topRight: 8, bottomRight: 8 })
            .onClick(() => { if (this.duration < 20) { this.duration++ } })
        }
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 16 })

      // 备注
      TextInput({ text: this.remark, placeholder: '选填:备注信息(如指定英雄、打法等)' })
        .fontSize(14)
        .height(44)
        .backgroundColor(COLORS84.bg)
        .borderRadius(10)
        .padding({ left: 12, right: 12 })
        .margin({ left: 20, right: 20, bottom: 12 })
        .onChange((val: string) => { this.remark = val })

      // 价格合计
      Row() {
        Text('合计')
          .fontSize(14)
          .fontColor(COLORS84.textSecondary)
        Text('').layoutWeight(1)
        Text('¥' + (this.companionPrice * this.duration).toString())
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS84.accent)
      }
      .width('92%')
      .margin({ bottom: 20 })

      Button() {
        Text('确认预约')
          .fontSize(16)
          .fontColor(COLORS84.white)
          .fontWeight(FontWeight.Bold)
      }
      .width('90%')
      .height(48)
      .backgroundColor(COLORS84.accent)
      .borderRadius(24)
      .margin({ bottom: 20 })
      .onClick(() => { this.onConfirm() })

时长控制区域采用减号-数字-加号的三段式步进器设计,通过borderRadius的定向设置(topLeft/bottomLefttopRight/bottomRight)实现了左圆角和右圆角的拼接效果。onClick回调中加入边界检查(duration > 1duration < 20),防止数值越界。价格合计区域通过this.companionPrice * this.duration实时计算总价,当用户调整时长时,@State duration的变化会自动触发UI重新渲染,合计金额实时更新。确认按钮使用Button组件包裹Text,采用强调色背景和较大圆角(24),视觉上引导用户完成下单操作。

四、对话框与编辑面板组件

4.1 取消预约对话框

@Component
export struct CancelBookingDialog84 {
  orderNo: string = ''
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Text('取消预约')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS84.textPrimary)
        .margin({ bottom: 12 })
      Text('确认取消预约 ' + this.orderNo + ' ?取消后费用将原路退回。')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .textAlign(TextAlign.Center)
        .margin({ bottom: 24, left: 20, right: 20 })
      Row() {
        Button() {
          Text('再想想')
            .fontSize(15)
            .fontColor(COLORS84.textSecondary)
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor(COLORS84.bg)
        .borderRadius(22)
        .margin({ right: 8 })
        .onClick(() => { this.onClose() })
        Button() {
          Text('确认取消')
            .fontSize(15)
            .fontColor(COLORS84.white)
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor(COLORS84.danger)
        .borderRadius(22)
        .onClick(() => { this.onConfirm() })
      }
      .width('100%')
    }
    .width('80%')
    .padding(24)
    .backgroundColor(COLORS84.cardBg)
    .borderRadius(20)
  }
}

CancelBookingDialog84是一个确认型对话框组件,接收orderNo(订单号)属性和两个回调。对话框采用居中弹出的设计,宽度为屏幕的80%,圆角20,内边距24。按钮区域使用Row+layoutWeight(1)实现等分布局,"再想想"按钮使用浅色背景表示次要操作,"确认取消"按钮使用danger红色背景表示破坏性操作,通过颜色语义化引导用户谨慎操作。该组件通过主入口的bindContentCover进行展示,采用ModalTransition.SLIDE滑入动画。

4.2 编辑玩家资料面板

@Component
export struct EditProfileSheet84 {
  @State nickName: string = ''
  @State gameID: string = ''
  @State mainGame: string = ''
  @State bio: string = ''
  @State selectedGame: number = 0
  @State allowVoice: boolean = true
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Column()
        .width(40)
        .height(4)
        .borderRadius(2)
        .backgroundColor(COLORS84.border)
        .margin({ top: 12, bottom: 16 })

      Row() {
        Text('编辑玩家资料')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS84.textPrimary)
        Text('').layoutWeight(1)
        Text('✕')
          .fontSize(22)
          .fontColor(COLORS84.textHint)
          .onClick(() => { this.onClose() })
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 16 })

      // 昵称
      Text('昵称')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .margin({ left: 20, bottom: 6 })
      TextInput({ text: this.nickName, placeholder: '请输入昵称' })
        .fontSize(14)
        .height(44)
        .backgroundColor(COLORS84.bg)
        .borderRadius(10)
        .padding({ left: 12, right: 12 })
        .margin({ left: 20, right: 20, bottom: 12 })
        .onChange((val: string) => { this.nickName = val })

      // 游戏ID
      Text('游戏ID')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .margin({ left: 20, bottom: 6 })
      TextInput({ text: this.gameID, placeholder: '请输入游戏ID' })
        .fontSize(14)
        .height(44)
        .backgroundColor(COLORS84.bg)
        .borderRadius(10)
        .padding({ left: 12, right: 12 })
        .margin({ left: 20, right: 20, bottom: 12 })
        .onChange((val: string) => { this.gameID = val })

EditProfileSheet84是一个资料编辑底部弹层,管理了昵称、游戏ID、主玩游戏、个人简介和语音开关五个状态字段。每个输入字段都使用Text标签+TextInput输入框的组合模式:Text作为字段标题使用次要文字色,TextInput使用背景色填充和圆角边框,onChange回调将输入值同步到@State。这种设计模式在HarmonyOS表单组件中非常常见,通过@State的双向绑定保证了输入内容的实时响应。

4.3 主玩游戏选择与语音开关

      // 主玩游戏
      Text('主玩游戏')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .margin({ left: 20, bottom: 6 })
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(GAMES_84.slice(0, 6), (game: Game84, index: number) => {
          Text(game.name)
            .fontSize(12)
            .fontColor(this.selectedGame === index ? COLORS84.white : COLORS84.textSecondary)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .borderRadius(16)
            .backgroundColor(this.selectedGame === index ? COLORS84.primary : COLORS84.bg)
            .margin({ right: 8, bottom: 8 })
            .onClick(() => { this.selectedGame = index })
        }, (game: Game84) => game.id.toString())
      }
      .width('92%')
      .margin({ left: 20, right: 20, bottom: 12 })

      // 个人简介
      Text('个人简介')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .margin({ left: 20, bottom: 6 })
      TextArea({ text: this.bio, placeholder: '介绍一下自己吧...' })
        .fontSize(14)
        .height(80)
        .backgroundColor(COLORS84.bg)
        .borderRadius(10)
        .padding({ left: 12, right: 12, top: 8, bottom: 8 })
        .margin({ left: 20, right: 20, bottom: 12 })
        .onChange((val: string) => { this.bio = val })

      // 允许语音
      Row() {
        Text('允许语音通话')
          .fontSize(14)
          .fontColor(COLORS84.textPrimary)
        Text('').layoutWeight(1)
        Toggle({ type: ToggleType.Switch, isOn: this.allowVoice })
          .selectedColor(COLORS84.accent)
          .onChange((val: boolean) => { this.allowVoice = val })
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 20 })

      Button() {
        Text('保存资料')
          .fontSize(16)
          .fontColor(COLORS84.white)
          .fontWeight(FontWeight.Bold)
      }
      .width('90%')
      .height(48)
      .backgroundColor(COLORS84.primary)
      .borderRadius(24)
      .margin({ bottom: 20 })
      .onClick(() => { this.onConfirm() })
    }
    .width('100%')
    .backgroundColor(COLORS84.cardBg)
    .borderRadius({ topLeft: 24, topRight: 24 })
  }
}

主玩游戏选择使用Flex+FlexWrap.Wrap实现自动换行的标签选择器,与预约面板中的时段选择器采用了相同的交互模式。个人简介使用TextArea组件而非TextInput,因为它需要支持多行文本输入,高度设为80以提供足够的输入空间。语音开关使用Toggle组件的Switch类型,通过selectedColor属性将开关颜色设为强调色(能量绿),onChange回调同步布尔值到@State allowVoice。保存按钮使用主色(陪玩蓝)背景,与预约面板的强调色按钮形成区分,暗示不同的操作语义。

4.4 删除收藏对话框

@Component
export struct DeleteFavoriteDialog84 {
  companionName: string = ''
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Text('删除收藏')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS84.textPrimary)
        .margin({ bottom: 12 })
      Text('确认将 ' + this.companionName + ' 从收藏移除?')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .textAlign(TextAlign.Center)
        .margin({ bottom: 24, left: 20, right: 20 })
      Row() {
        Button() {
          Text('取消')
            .fontSize(15)
            .fontColor(COLORS84.textSecondary)
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor(COLORS84.bg)
        .borderRadius(22)
        .margin({ right: 8 })
        .onClick(() => { this.onClose() })
        Button() {
          Text('删除')
            .fontSize(15)
            .fontColor(COLORS84.white)
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor(COLORS84.danger)
        .borderRadius(22)
        .onClick(() => { this.onConfirm() })
      }
      .width('100%')
    }
    .width('80%')
    .padding(24)
    .backgroundColor(COLORS84.cardBg)
    .borderRadius(20)
  }
}

DeleteFavoriteDialog84与取消预约对话框结构高度相似,都采用"标题+提示文本+双按钮"的布局模式。区别在于提示文本动态插入了companionName(陪玩师名称),使提示信息更加具体。两个对话框组件都遵循了相同的视觉规范:80%宽度、20圆角、24内边距,以及"次要操作浅色+破坏性操作红色"的按钮配色策略。这种组件复用思路减少了代码冗余,同时保证了交互一致性。

五、陪玩大厅Tab组件

5.1 搜索栏与游戏分类横滑

@Component
export struct CompanionTab84 {
  build() {
    Column() {
      // 搜索栏
      Row() {
        Text('🔍')
          .fontSize(18)
          .margin({ right: 8 })
        Text('搜索陪玩、游戏、段位...')
          .fontSize(14)
          .fontColor(COLORS84.textHint)
          .layoutWeight(1)
      }
      .width('92%')
      .height(40)
      .backgroundColor(COLORS84.cardBg)
      .borderRadius(20)
      .padding({ left: 16, right: 16 })
      .margin({ top: 12, bottom: 12 })

      // 游戏分类横滑
      Scroll() {
        Row() {
          ForEach(GAMES_84, (game: Game84) => {
            Column() {
              Text(game.icon)
                .fontSize(28)
                .width(48)
                .height(48)
                .textAlign(TextAlign.Center)
                .borderRadius(12)
                .backgroundColor(game.bg)
              Text(game.name)
                .fontSize(11)
                .fontColor(COLORS84.textPrimary)
                .margin({ top: 6 })
            }
            .margin({ right: 12 })
          }, (game: Game84) => game.id.toString())
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .margin({ bottom: 12 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS84.bg)
  }
}

CompanionTab84是应用的首页Tab,以"发现陪玩"为核心场景。搜索栏使用Text组件模拟占位符效果(实际项目中可替换为TextInput),通过textHint颜色和搜索图标Emoji营造搜索框的视觉暗示。游戏分类横滑区域使用Scroll+Row+ForEach构建,每个游戏项由图标(48×48圆角方块)和名称组成,使用各自的bg背景色进行视觉区分。scrollBar(BarState.Off)隐藏了滚动条,保持了界面的简洁感。

5.2 人气陪玩推荐大卡

          // 人气陪玩大卡
          Column() {
            Row() {
              Text('🔥 人气陪玩推荐')
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS84.textPrimary)
              Text('').layoutWeight(1)
              Text('换一批 🔄')
                .fontSize(13)
                .fontColor(COLORS84.accent)
            }
            .width('100%')
            .margin({ bottom: 12 })

            // 大卡片
            Column() {
              Row() {
                Text(COMPANIONS_84[0].avatar)
                  .fontSize(48)
                  .width(72)
                  .height(72)
                  .textAlign(TextAlign.Center)
                  .backgroundColor(COLORS84.bg)
                  .borderRadius(36)
                Column() {
                  Row() {
                    Text(COMPANIONS_84[0].name)
                      .fontSize(16)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(COLORS84.textPrimary)
                    Text('LV' + COMPANIONS_84[0].level.toString())
                      .fontSize(10)
                      .fontColor(COLORS84.white)
                      .backgroundColor(COLORS84.accent)
                      .borderRadius(4)
                      .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                      .margin({ left: 8 })
                  }
                  Text(COMPANIONS_84[0].game + ' · ' + COMPANIONS_84[0].rank)
                    .fontSize(12)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 4 })
                  Text(COMPANIONS_84[0].intro)
                    .fontSize(12)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 4 })
                    .maxLines(1)
                  Row() {
                    ForEach(COMPANIONS_84[0].tags, (tag: string) => {
                      Text(tag)
                        .fontSize(10)
                        .fontColor(COLORS84.primary)
                        .backgroundColor(COLORS84.bg)
                        .borderRadius(4)
                        .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                        .margin({ right: 4 })
                    }, (tag: string) => tag)
                  }
                  .margin({ top: 6 })
                }
                .alignItems(HorizontalAlign.Start)
                .margin({ left: 12 })
                .layoutWeight(1)
                Column() {
                  Text('¥' + COMPANIONS_84[0].price.toString())
                    .fontSize(20)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS84.accent)
                  Text(COMPANIONS_84[0].unit)
                    .fontSize(10)
                    .fontColor(COLORS84.textSecondary)
                  Text(COMPANIONS_84[0].online ? '●在线' : '●离线')
                    .fontSize(11)
                    .fontColor(COMPANIONS_84[0].online ? COLORS84.success : COLORS84.textHint)
                    .margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.End)
              }
              .width('100%')

              Row() {
                Text('⭐ ' + COMPANIONS_84[0].rating.toString())
                  .fontSize(12)
                  .fontColor(COLORS84.warning)
                Text('  成交' + COMPANIONS_84[0].orders.toString() + '单')
                  .fontSize(12)
                  .fontColor(COLORS84.textSecondary)
                Text('').layoutWeight(1)
                Text('🔊 ' + COMPANIONS_84[0].voice)
                  .fontSize(11)
                  .fontColor(COLORS84.textSecondary)
              }
              .width('100%')
              .margin({ top: 10 })
            }
            .width('100%')
            .padding(16)
            .backgroundColor(COLORS84.cardBg)
            .borderRadius(16)
          }
          .width('92%')
          .padding(16)
          .backgroundColor(COLORS84.cardBg)
          .borderRadius(16)
          .margin({ bottom: 12 })

人气陪玩大卡是首页的视觉焦点,采用"标题栏+大卡片"的双层结构。标题栏使用layoutWeight(1)的空Text实现左右弹性布局,左侧标题、右侧"换一批"操作按钮。大卡片内部采用三列布局:左侧72×72圆形头像、中间信息区(名称+等级徽章+游戏段位+简介+标签行)、右侧价格区(价格+单位+在线状态)。等级徽章LV15使用强调色背景+白色小字+4圆角,紧跟在名称后面。标签行使用ForEach遍历tags数组,每个标签使用主色文字+浅色背景+4圆角。底部信息栏展示评分、成交单数和声音特征,通过空TextlayoutWeight(1)实现两端对齐。

5.3 双列陪玩瀑布流列表

          // 双列陪玩列表
          Row() {
            Text('全部陪玩')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS84.textPrimary)
            Text('').layoutWeight(1)
            Text('人气 ▼')
              .fontSize(13)
              .fontColor(COLORS84.textSecondary)
          }
          .width('92%')
          .margin({ bottom: 12 })

          Row() {
            Column() {
              ForEach(COMPANIONS_84.filter((_: Companion84, i: number) => i % 2 === 0), (comp: Companion84) => {
                Column() {
                  Row() {
                    Text(comp.avatar)
                      .fontSize(36)
                      .width(48)
                      .height(48)
                      .textAlign(TextAlign.Center)
                      .backgroundColor(COLORS84.bg)
                      .borderRadius(24)
                    Column() {
                      Text(comp.name)
                        .fontSize(13)
                        .fontWeight(FontWeight.Bold)
                        .fontColor(COLORS84.textPrimary)
                        .maxLines(1)
                      Text(comp.rank)
                        .fontSize(10)
                        .fontColor(COLORS84.accent)
                        .margin({ top: 2 })
                    }
                    .alignItems(HorizontalAlign.Start)
                    .margin({ left: 8 })
                    Text('').layoutWeight(1)
                    Text(comp.online ? '●' : '○')
                      .fontSize(10)
                      .fontColor(comp.online ? COLORS84.success : COLORS84.textHint)
                  }
                  .width('100%')
                  Text(comp.game)
                    .fontSize(11)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 6 })
                  Text(comp.intro)
                    .fontSize(10)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 4 })
                    .maxLines(2)
                  Row() {
                    Text('⭐' + comp.rating.toString())
                      .fontSize(10)
                      .fontColor(COLORS84.warning)
                    Text('').layoutWeight(1)
                    Text('¥' + comp.price.toString())
                      .fontSize(14)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(COLORS84.accent)
                  }
                  .width('100%')
                  .margin({ top: 6 })
                }
                .layoutWeight(1)
                .padding(10)
                .backgroundColor(COLORS84.cardBg)
                .borderRadius(12)
                .margin({ right: 6, bottom: 12 })
              }, (comp: Companion84) => comp.id.toString())
            }
            .layoutWeight(1)
            Column() {
              ForEach(COMPANIONS_84.filter((_: Companion84, i: number) => i % 2 === 1), (comp: Companion84) => {
                // ... 右列结构同左列,margin改为 { left: 6, bottom: 12 }
              }, (comp: Companion84) => comp.id.toString())
            }
            .layoutWeight(1)
          }
          .width('92%')

双列陪玩列表是整个应用中最具特色的布局设计之一。它通过COMPANIONS_84.filter((_, i) => i % 2 === 0)i % 2 === 1将陪玩师数据按奇偶索引分成两组,分别放入左右两个Column中,实现了类瀑布流的双列布局效果。每个陪玩卡片包含头像、名称、段位、在线状态、游戏、简介(最多2行)、评分和价格。左右两列的卡片分别使用margin({ right: 6 })margin({ left: 6 })来控制间距,确保列间有合理的视觉间隔。这种实现方式不依赖第三方瀑布流组件,纯粹使用ArkTS内置的布局能力,充分展示了声明式UI的灵活性。

六、代练服务Tab组件

@Component
export struct BoostTab84 {
  build() {
    Column() {
      Scroll() {
        Column() {
          // 代练分类
          Row() {
            Text('⚡ 代练大厅')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS84.textPrimary)
            Text('').layoutWeight(1)
            Text('发布需求 +')
              .fontSize(13)
              .fontColor(COLORS84.accent)
          }
          .width('92%')
          .margin({ top: 12, bottom: 12 })

          // 代练列表
          ForEach(BOOSTS_84, (boost: BoostItem84) => {
            Column() {
              Row() {
                Text(boost.image)
                  .fontSize(36)
                  .width(56)
                  .height(56)
                  .textAlign(TextAlign.Center)
                  .backgroundColor(COLORS84.bg)
                  .borderRadius(10)
                Column() {
                  Row() {
                    Text(boost.title)
                      .fontSize(15)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(COLORS84.textPrimary)
                    Text(boost.status)
                      .fontSize(10)
                      .fontColor(boost.status === '进行中' ? COLORS84.accent : (boost.status === '已完成' ? COLORS84.success : COLORS84.warning))
                      .border({ width: 1, color: boost.status === '进行中' ? COLORS84.accent : (boost.status === '已完成' ? COLORS84.success : COLORS84.warning) })
                      .borderRadius(4)
                      .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                      .margin({ left: 8 })
                  }
                  Text(boost.game + ' · ' + boost.fromRank + ' → ' + boost.toRank)
                    .fontSize(12)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 4 })
                  Text(boost.desc)
                    .fontSize(11)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 4 })
                    .maxLines(1)
                }
                .alignItems(HorizontalAlign.Start)
                .margin({ left: 12 })
                .layoutWeight(1)
                Column() {
                  Text('¥' + boost.price.toString())
                    .fontSize(18)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS84.accent)
                  Text(boost.duration)
                    .fontSize(11)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.End)
              }
              .width('100%')

              // 进度条
              if (boost.status === '进行中') {
                Row() {
                  Text('进度')
                    .fontSize(11)
                    .fontColor(COLORS84.textSecondary)
                  Column() {
                    Column()
                      .width((boost.progress / 100) * 100 + '%')
                      .height(6)
                      .backgroundColor(COLORS84.accent)
                      .borderRadius(3)
                  }
                  .width('100%')
                  .height(6)
                  .backgroundColor(COLORS84.bg)
                  .borderRadius(3)
                  .layoutWeight(1)
                  .margin({ left: 8, right: 8 })
                  Text(boost.progress.toString() + '%')
                    .fontSize(11)
                    .fontColor(COLORS84.accent)
                    .fontWeight(FontWeight.Bold)
                }
                .width('100%')
                .margin({ top: 12 })
              }

              Row() {
                Text('👤 ' + boost.booster)
                  .fontSize(12)
                  .fontColor(COLORS84.textSecondary)
                Text('⭐ ' + boost.rating.toString())
                  .fontSize(12)
                  .fontColor(COLORS84.warning)
                  .margin({ left: 12 })
                Text('').layoutWeight(1)
                Text(boost.status === '待接单' ? '立即下单 >' : '查看详情 >')
                  .fontSize(13)
                  .fontColor(COLORS84.accent)
              }
              .width('100%')
              .margin({ top: 12 })
            }
            .width('92%')
            .padding(16)
            .backgroundColor(COLORS84.cardBg)
            .borderRadius(12)
            .margin({ bottom: 10 })
          }, (boost: BoostItem84) => boost.id.toString())
        }
        .width('100%')
        .padding({ bottom: 100 })
      }
      .layoutWeight(1)
      .scrollable(ScrollDirection.Vertical)
    }
    .width('100%')
    .height('100%')
    .backgroundColor(COLORS84.bg)
  }
}

BoostTab84展示了代练服务列表,每个代练卡片包含三个核心区域:信息区(图标+标题+状态标签+段位描述+价格)、进度条区(仅"进行中"状态显示)和底部操作区(代练师+评分+操作入口)。状态标签的颜色通过嵌套三元表达式动态计算:进行中使用强调色、已完成使用成功色、待接单使用警告色,同时设置了border边框颜色与文字颜色保持一致。进度条使用嵌套的Column实现:外层Column作为轨道(浅色背景+3圆角),内层Column作为填充条(强调色背景),宽度通过(boost.progress / 100) * 100 + '%'动态计算。底部操作入口根据状态显示不同文案:待接单显示"立即下单",其他状态显示"查看详情"。

七、应用核心流程

下面的流程图展示了应用的主要交互流程和数据流转关系:

tab=0

tab=1

tab=2

tab=3

tab=4或5

进行中

待接单

已完成

确认预约

关闭

取消预约

删除收藏

应用启动

主入口组件初始化

当前Tab索引判断

陪玩大厅Tab

代练服务Tab

技能市场Tab

社区动态Tab

个人中心Tab

搜索栏 + 游戏分类横滑

人气陪玩推荐大卡

双列陪玩瀑布流列表

点击陪玩师 → 触发预约

代练大厅标题栏

代练列表 ForEach渲染

状态判断

显示进度条

显示立即下单

显示查看详情

分类标签横滑

热门技能横滑卡片

全部技能纵向列表

热门话题横滑卡片

社区动态信息流

渐变头部用户信息

消费柱状图统计

段位展示卡片

成就徽章网格

订单列表 + 收藏列表

bindSheet弹出预约面板

用户操作

关闭弹层 + 回调onConfirm

关闭弹层 + 回调onClose

bindContentCover弹出对话框

对话框类型

CancelBookingDialog

DeleteFavoriteDialog

回调处理

八、技能市场Tab组件

@Component
export struct SkillTab84 {
  build() {
    Column() {
      // 分类标签
      Scroll() {
        Row() {
          ForEach(SKILL_CATS_84, (cat: string, index: number) => {
            Text(cat)
              .fontSize(13)
              .fontColor(index === 0 ? COLORS84.white : COLORS84.textSecondary)
              .padding({ left: 14, right: 14, top: 8, bottom: 8 })
              .borderRadius(18)
              .backgroundColor(index === 0 ? COLORS84.primary : COLORS84.cardBg)
              .margin({ right: 8 })
          }, (cat: string) => cat)
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .height(44)

      Scroll() {
        Column() {
          // 热门技能横滑
          Text('🔥 热门技能')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS84.textPrimary)
            .margin({ bottom: 12 })

          Scroll() {
            Row() {
              ForEach(SKILLS_84.slice(0, 4), (skill: SkillItem84) => {
                Column() {
                  Text(skill.avatar)
                    .fontSize(32)
                    .width(48)
                    .height(48)
                    .textAlign(TextAlign.Center)
                    .backgroundColor(COLORS84.bg)
                    .borderRadius(24)
                  Text(skill.name)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS84.textPrimary)
                    .margin({ top: 8 })
                    .maxLines(1)
                  Text(skill.provider)
                    .fontSize(11)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 2 })
                  Text('¥' + skill.price.toString() + '/' + skill.unit.replace('元/', ''))
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS84.accent)
                    .margin({ top: 6 })
                }
                .width(120)
                .padding(14)
                .backgroundColor(COLORS84.cardBg)
                .borderRadius(12)
                .margin({ right: 10 })
              }, (skill: SkillItem84) => skill.id.toString())
            }
            .padding({ left: 16, right: 16 })
          }
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .margin({ bottom: 16 })

SkillTab84采用"分类标签+热门横滑+纵向列表"的三段式结构。顶部分类标签栏使用横向Scroll渲染SKILL_CATS_84数组(全部/教学/攻略/陪玩/陪聊),通过index === 0判断当前选中项(默认第一项选中),选中态使用主色背景+白色文字。热门技能区域取前4项数据,使用固定宽度120的卡片+横向滚动展示,每个卡片包含圆形头像、技能名称、提供者和价格。价格显示使用skill.unit.replace('元/', '')对单位字符串进行了处理,将"元/小时"简化为"小时"显示,使价格格式更加紧凑。

8.1 全部技能纵向列表

          // 全部技能列表
          Row() {
            Text('全部技能')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS84.textPrimary)
            Text('').layoutWeight(1)
            Text('价格 ▼')
              .fontSize(13)
              .fontColor(COLORS84.textSecondary)
          }
          .width('92%')
          .margin({ bottom: 12 })

          ForEach(SKILLS_84, (skill: SkillItem84) => {
            Row() {
              Text(skill.avatar)
                .fontSize(36)
                .width(56)
                .height(56)
                .textAlign(TextAlign.Center)
                .backgroundColor(COLORS84.bg)
                .borderRadius(10)
              Column() {
                Text(skill.name)
                  .fontSize(15)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS84.textPrimary)
                Text(skill.provider + ' · ' + skill.category)
                  .fontSize(12)
                  .fontColor(COLORS84.textSecondary)
                  .margin({ top: 4 })
                Text(skill.desc)
                  .fontSize(12)
                  .fontColor(COLORS84.textSecondary)
                  .margin({ top: 4 })
                  .maxLines(1)
                Row() {
                  ForEach(skill.tags, (tag: string) => {
                    Text(tag)
                      .fontSize(10)
                      .fontColor(COLORS84.primary)
                      .backgroundColor(COLORS84.bg)
                      .borderRadius(4)
                      .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                      .margin({ right: 4 })
                  }, (tag: string) => tag)
                }
                .margin({ top: 6 })
              }
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 12 })
              .layoutWeight(1)
              Column() {
                Text('¥' + skill.price.toString())
                  .fontSize(18)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS84.accent)
                Text(skill.unit)
                  .fontSize(10)
                  .fontColor(COLORS84.textSecondary)
                Text('⭐' + skill.rating.toString())
                  .fontSize(11)
                  .fontColor(COLORS84.warning)
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.End)
            }
            .width('92%')
            .padding(14)
            .backgroundColor(COLORS84.cardBg)
            .borderRadius(12)
            .margin({ bottom: 10 })
          }, (skill: SkillItem84) => skill.id.toString())

全部技能列表使用ForEach遍历完整的SKILLS_84数组,每个技能项采用三列布局:左侧56×56圆角头像、中间信息区(名称+提供者+分类+描述+标签行)、右侧价格区(价格+单位+评分)。标签行通过嵌套ForEach渲染skill.tags数组,每个标签使用主色文字+浅色背景+4圆角,与陪玩大卡中的标签样式保持一致。整个列表的卡片结构、配色方案和间距规则都与代练列表高度一致,体现了应用在不同Tab之间的视觉统一性。

九、社区动态Tab组件

@Component
export struct CommunityTab84 {
  build() {
    Column() {
      Scroll() {
        Column() {
          // 热门话题
          Text('🔥 热门话题')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS84.textPrimary)
            .margin({ top: 12, bottom: 12 })
          Scroll() {
            Row() {
              ForEach(TOPICS_84, (topic: TopicItem84) => {
                Column() {
                  Text(topic.title)
                    .fontSize(13)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS84.white)
                  Text('🔥 ' + topic.hot.toString())
                    .fontSize(11)
                    .fontColor('rgba(255,255,255,0.8)')
                    .margin({ top: 4 })
                  Text(topic.posts.toString() + '篇帖子')
                    .fontSize(10)
                    .fontColor('rgba(255,255,255,0.6)')
                    .margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.Start)
                .width(130)
                .padding(14)
                .borderRadius(12)
                .backgroundColor(COLORS84.primary)
                .margin({ right: 10 })
              }, (topic: TopicItem84) => topic.id.toString())
            }
            .padding({ left: 16, right: 16 })
          }
          .scrollable(ScrollDirection.Horizontal)
          .scrollBar(BarState.Off)
          .margin({ bottom: 16 })

CommunityTab84的顶部是热门话题横滑区域,每个话题卡片使用主色(陪玩蓝)背景,内部文字使用不同透明度的白色——标题用纯白、热度用0.8透明度、帖子数用0.6透明度,通过透明度递减建立信息层级。卡片固定宽度130,使用alignItems(HorizontalAlign.Start)使内容左对齐。这种深色背景+白色文字的卡片设计与应用整体的浅色基调形成对比,在视觉上突出了热门话题的重要性。

9.1 社区动态信息流

          Text('💬 社区动态')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS84.textPrimary)
            .margin({ bottom: 12 })

          ForEach(POSTS_84, (post: PostItem84) => {
            Column() {
              Row() {
                Text(post.avatar)
                  .fontSize(32)
                  .width(40)
                  .height(40)
                  .textAlign(TextAlign.Center)
                  .backgroundColor(COLORS84.bg)
                  .borderRadius(20)
                Column() {
                  Text(post.user)
                    .fontSize(14)
                    .fontWeight(FontWeight.Medium)
                    .fontColor(COLORS84.textPrimary)
                  Text(post.time)
                    .fontSize(11)
                    .fontColor(COLORS84.textHint)
                    .margin({ top: 2 })
                }
                .alignItems(HorizontalAlign.Start)
                .margin({ left: 10 })
                .layoutWeight(1)
                Text(post.tag)
                  .fontSize(10)
                  .fontColor(COLORS84.accent)
                  .border({ width: 1, color: COLORS84.accent })
                  .borderRadius(4)
                  .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              }
              .width('100%')
              Text(post.title)
                .fontSize(15)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS84.textPrimary)
                .margin({ top: 12 })
              Text(post.content)
                .fontSize(13)
                .fontColor(COLORS84.textSecondary)
                .margin({ top: 6 })
                .maxLines(3)
              Row() {
                Text('❤️ ' + post.likes.toString())
                  .fontSize(12)
                  .fontColor(COLORS84.danger)
                Text('💬 ' + post.comments.toString())
                  .fontSize(12)
                  .fontColor(COLORS84.textSecondary)
                  .margin({ left: 20 })
                Text('').layoutWeight(1)
                Text('分享')
                  .fontSize(12)
                  .fontColor(COLORS84.textSecondary)
              }
              .width('100%')
              .margin({ top: 12 })
            }
            .width('92%')
            .padding(16)
            .backgroundColor(COLORS84.cardBg)
            .borderRadius(12)
            .margin({ bottom: 10 })
          }, (post: PostItem84) => post.id.toString())

社区动态信息流使用ForEach遍历POSTS_84数组,每条动态卡片包含:用户信息行(头像+用户名+时间+分类标签)、标题、内容(最多3行)和互动栏(点赞数+评论数+分享)。分类标签使用强调色文字+强调色边框+透明背景的描边样式,与代练卡片中的状态标签形成了视觉区分。互动栏中点赞数使用danger红色(配合心形Emoji),评论数使用次要文字色,分享按钮靠右对齐。maxLines(3)限制了内容显示行数,避免过长内容破坏列表节奏。

十、个人中心Tab组件

10.1 渐变头部与消费统计

@Component
export struct ProfileTab84 {
  build() {
    Column() {
      Scroll() {
        Column() {
          // 渐变头部
          Column() {
            Row() {
              Text('🎮')
                .fontSize(40)
                .width(64)
                .height(64)
                .textAlign(TextAlign.Center)
                .borderRadius(32)
                .backgroundColor('rgba(255,255,255,0.2)')
              Column() {
                Text('电竞玩家')
                  .fontSize(18)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS84.white)
                Text('ID: PW20260824')
                  .fontSize(12)
                  .fontColor('rgba(255,255,255,0.7)')
                  .margin({ top: 4 })
                Text('陪玩爱好者 · 累计98场')
                  .fontSize(11)
                  .fontColor('rgba(255,255,255,0.6)')
                  .margin({ top: 2 })
              }
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 12 })
            }
            .width('100%')
            .padding(20)
          }
          .width('100%')
          .linearGradient({ angle: 135, colors: [[COLORS84.primary, 0], [COLORS84.primaryDark, 1]] })
          .borderRadius({ bottomLeft: 24, bottomRight: 24 })

          // 消费统计
          Column() {
            Text('📈 消费统计')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS84.textPrimary)
              .margin({ bottom: 16 })
            Row() {
              ForEach(BAR_DATA_84, (bar: BarData84) => {
                Column() {
                  Column()
                    .width(24)
                    .height(bar.value / 10)
                    .backgroundColor(COLORS84.accent)
                    .borderRadius({ topLeft: 4, topRight: 4 })
                  Text(bar.month)
                    .fontSize(10)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 4 })
                }
                .margin({ right: 16 })
              }, (bar: BarData84) => bar.month)
            }
            .height(120)
            .alignItems(VerticalAlign.Bottom)
            .margin({ bottom: 8 })
            Row() {
              Text('半年总消费')
                .fontSize(12)
                .fontColor(COLORS84.textSecondary)
              Text('').layoutWeight(1)
              Text('¥3,610')
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS84.accent)
            }
            .width('100%')
          }
          .width('92%')
          .padding(16)
          .backgroundColor(COLORS84.cardBg)
          .borderRadius(16)
          .margin({ top: 12, bottom: 12 })

ProfileTab84是应用中信息量最大的Tab。渐变头部使用linearGradient属性实现135度线性渐变,从primary(陪玩蓝)过渡到primaryDark(深蓝),底部两侧设置24圆角形成与下方内容的视觉过渡。头部内容使用半透明白色背景(rgba(255,255,255,0.2))的圆形头像,配合不同透明度的白色文字(0.7和0.6),营造层次感。消费统计区域使用纯ArkTS组件绘制柱状图:每个柱子是一个Column组件,宽度固定24,高度通过bar.value / 10动态计算(将消费金额缩放为像素高度),使用强调色填充和顶部圆角。整个柱状图容器设置alignItems(VerticalAlign.Bottom)使所有柱子底部对齐,形成标准的柱状图效果。

10.2 段位展示与成就徽章

          // 段位展示
          Column() {
            Text('🏆 我的段位')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS84.textPrimary)
              .margin({ bottom: 16 })
            Row() {
              Column() {
                Text('当前段位')
                  .fontSize(12)
                  .fontColor(COLORS84.textSecondary)
                Text(RANK_INFO_84.current)
                  .fontSize(20)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS84.primary)
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
              Column() {
                Text('目标段位')
                  .fontSize(12)
                  .fontColor(COLORS84.textSecondary)
                Text(RANK_INFO_84.target)
                  .fontSize(20)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS84.accent)
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
            }
            .width('100%')

            Row() {
              Text('星数')
                .fontSize(12)
                .fontColor(COLORS84.textSecondary)
              Text(RANK_INFO_84.star.toString() + '★')
                .fontSize(14)
                .fontColor(COLORS84.accent)
                .fontWeight(FontWeight.Bold)
                .margin({ left: 8 })
              Text('').layoutWeight(1)
              Text('胜率 ' + RANK_INFO_84.winRate.toString() + '%')
                .fontSize(12)
                .fontColor(COLORS84.success)
                .fontWeight(FontWeight.Bold)
            }
            .width('100%')
            .margin({ top: 12 })

            Row() {
              Text('KDA: ' + RANK_INFO_84.kda)
                .fontSize(12)
                .fontColor(COLORS84.textSecondary)
              Text('').layoutWeight(1)
              Text('主玩位置: ' + RANK_INFO_84.mainRole)
                .fontSize(12)
                .fontColor(COLORS84.textSecondary)
            }
            .width('100%')
            .margin({ top: 8 })
          }
          .width('92%')
          .padding(16)
          .backgroundColor(COLORS84.cardBg)
          .borderRadius(16)
          .margin({ bottom: 12 })

          // 成就徽章
          Text('🏅 成就徽章')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS84.textPrimary)
            .margin({ bottom: 12 })

          Flex({ wrap: FlexWrap.Wrap }) {
            ForEach(ACHIEVEMENTS_84, (ach: Achievement84) => {
              Column() {
                Text(ach.icon)
                  .fontSize(28)
                  .width(48)
                  .height(48)
                  .textAlign(TextAlign.Center)
                  .borderRadius(24)
                  .backgroundColor(ach.unlocked ? ach.color : COLORS84.bg)
                  .opacity(ach.unlocked ? 1 : 0.4)
                Text(ach.name)
                  .fontSize(10)
                  .fontColor(ach.unlocked ? COLORS84.textPrimary : COLORS84.textHint)
                  .margin({ top: 6 })
              }
              .width('30%')
              .margin({ bottom: 12 })
            }, (ach: Achievement84) => ach.id.toString())
          }
          .width('92%')
          .padding(16)
          .backgroundColor(COLORS84.cardBg)
          .borderRadius(16)
          .margin({ bottom: 12 })

段位展示卡片使用左右等分布局(layoutWeight(1)+layoutWeight(1)),当前段位使用主色文字、目标段位使用强调色文字,形成"现状-目标"的视觉对照。下方依次展示星数、胜率、KDA和主玩位置,信息密度高但通过合理的间距和对齐保持了可读性。成就徽章区域使用Flex+FlexWrap.Wrap实现自动换行的三列网格(每项宽度30%),通过ach.unlocked三元判断控制显示状态:已解锁徽章使用成就自身的color作为背景色+完全不透明,未解锁徽章使用浅灰背景+0.4透明度,使未解锁项视觉上"暗淡"但仍可辨识。这种设计既激励用户解锁更多成就,又不会让未解锁内容完全不可见。

10.3 订单列表与收藏列表

          // 我的订单
          Row() {
            Text('📦 我的订单')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS84.textPrimary)
            Text('').layoutWeight(1)
            Text('查看全部 >')
              .fontSize(13)
              .fontColor(COLORS84.accent)
          }
          .width('92%')
          .margin({ bottom: 12 })

          ForEach(ORDERS_84, (order: OrderItem84) => {
            Row() {
              Text(order.image)
                .fontSize(28)
                .width(44)
                .height(44)
                .textAlign(TextAlign.Center)
                .backgroundColor(COLORS84.bg)
                .borderRadius(8)
              Column() {
                Text(order.companion)
                  .fontSize(13)
                  .fontColor(COLORS84.textPrimary)
                  .maxLines(1)
                Text(order.game + ' · ' + order.duration)
                  .fontSize(11)
                  .fontColor(COLORS84.textSecondary)
                  .margin({ top: 2 })
                Text(order.orderNo + ' · ' + order.date)
                  .fontSize(10)
                  .fontColor(COLORS84.textHint)
                  .margin({ top: 2 })
              }
              .alignItems(HorizontalAlign.Start)
              .margin({ left: 10 })
              .layoutWeight(1)
              Column() {
                Text('¥' + order.price.toString())
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS84.accent)
                Text(order.status)
                  .fontSize(11)
                  .fontColor(order.status === '进行中' ? COLORS84.accent : COLORS84.success)
                  .margin({ top: 2 })
              }
              .alignItems(HorizontalAlign.End)
            }
            .width('92%')
            .padding(12)
            .backgroundColor(COLORS84.cardBg)
            .borderRadius(10)
            .margin({ bottom: 8 })
          }, (order: OrderItem84) => order.id.toString())

          // 收藏列表
          Row() {
            Text('❤️ 我的收藏 (' + FAVORITES_84.length.toString() + ')')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS84.textPrimary)
            Text('').layoutWeight(1)
            Text('管理 >')
              .fontSize(13)
              .fontColor(COLORS84.accent)
          }
          .width('92%')
          .margin({ top: 8, bottom: 12 })

          Row() {
            ForEach(FAVORITES_84, (fav: FavoriteItem84) => {
              Column() {
                Text(fav.avatar)
                  .fontSize(28)
                  .width('100%')
                  .height(56)
                  .textAlign(TextAlign.Center)
                  .backgroundColor(COLORS84.bg)
                  .borderRadius({ topLeft: 10, topRight: 10 })
                Text(fav.companion)
                  .fontSize(10)
                  .fontColor(COLORS84.textPrimary)
                  .maxLines(1)
                  .margin({ top: 4 })
                Text(fav.tag)
                  .fontSize(9)
                  .fontColor(fav.tag === '在线' ? COLORS84.success : COLORS84.textHint)
                Text('¥' + fav.price.toString())
                  .fontSize(12)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(COLORS84.accent)
              }
              .layoutWeight(1)
              .backgroundColor(COLORS84.cardBg)
              .borderRadius(10)
              .margin({ right: 6 })
              .padding(6)
            }, (fav: FavoriteItem84) => fav.id.toString())
          }
          .width('92%')

订单列表中每条订单使用紧凑的三列布局,头像使用44×44的8圆角方块(比陪玩列表中的24圆角更方正),信息区展示陪玩师名称、游戏+时长和订单号+日期三个层级的文字。右侧价格使用强调色,状态文字根据是否"进行中"动态切换颜色。收藏列表使用Row+ForEach+layoutWeight(1)实现等分横向排列,每个收藏卡片采用上下结构:顶部大头像区域(使用borderRadius的定向设置实现顶部圆角),下方依次是名称、在线状态标签和价格。收藏数量通过FAVORITES_84.length.toString()动态显示在标题中。

十一、主入口组件与模态调度

11.1 Tab导航与页面切换

interface TabItem84 {
  name: string
  icon: string
}

const TABS_84: TabItem84[] = [
  { name: '陪玩', icon: '🎮' },
  { name: '代练', icon: '⚡' },
  { name: '技能', icon: '🎓' },
  { name: '社区', icon: '💬' },
  { name: '订单', icon: '📦' },
  { name: '我的', icon: '👤' }
]

@Entry
@Component
struct DuoDuoCompanionApp {
  @State currentTab: number = 0
  @State showBookingSheet: boolean = false
  @State showCancelDialog: boolean = false
  @State showEditSheet: boolean = false
  @State showDeleteDialog: boolean = false
  @State selectedName: string = ''

  build() {
    Column() {
      if (this.currentTab === 0) {
        CompanionTab84()
      } else if (this.currentTab === 1) {
        BoostTab84()
      } else if (this.currentTab === 2) {
        SkillTab84()
      } else if (this.currentTab === 3) {
        CommunityTab84()
      } else if (this.currentTab === 4) {
        ProfileTab84()
      } else {
        ProfileTab84()
      }

      Row() {
        ForEach(TABS_84, (tab: TabItem84, index: number) => {
          Column() {
            Text(tab.icon)
              .fontSize(22)
            Text(tab.name)
              .fontSize(11)
              .fontColor(this.currentTab === index ? COLORS84.accent : COLORS84.textHint)
              .margin({ top: 2 })
          }
          .layoutWeight(1)
          .height(56)
          .justifyContent(FlexAlign.Center)
          .onClick(() => { this.currentTab = index })
        }, (tab: TabItem84) => tab.name)
      }
      .width('100%')
      .height(64)
      .backgroundColor(COLORS84.cardBg)
      .border({ width: 1, color: COLORS84.border, style: BorderStyle.Solid })
    }
    .width('100%')
    .height('100%')

主入口组件DuoDuoCompanionApp使用@Entry装饰器标记为应用入口,通过@State currentTab管理当前激活的Tab索引。页面内容区域使用if-else条件渲染链根据currentTab值动态切换Tab组件——当索引为0时渲染陪玩大厅,为1时渲染代练服务,以此类推。底部导航栏使用Row+ForEach+layoutWeight(1)构建六等分Tab按钮,每个按钮由Emoji图标和文字组成,选中态使用强调色文字、未选中态使用提示色文字。底部栏高度64,顶部设置1像素边框线作为视觉分隔。onClick回调通过this.currentTab = index触发状态更新,ArkTS的响应式机制会自动重新渲染页面内容区域。

11.2 弹层绑定与模态展示

    .bindSheet($$this.showBookingSheet, {
      builder: () => {
        BookingSheet84({
          companionName: this.selectedName,
          companionGame: '王者荣耀',
          companionPrice: 30,
          onConfirm: () => { this.showBookingSheet = false },
          onClose: () => { this.showBookingSheet = false }
        })
      }
    }, { height: SheetSize.LARGE, detents: [SheetSize.LARGE], backgroundColor: COLORS84.bg, blurStyle: BlurStyle.Transparent })
    .bindSheet($$this.showEditSheet, {
      builder: () => {
        EditProfileSheet84({
          onConfirm: () => { this.showEditSheet = false },
          onClose: () => { this.showEditSheet = false }
        })
      }
    }, { height: SheetSize.LARGE, detents: [SheetSize.LARGE], backgroundColor: COLORS84.bg, blurStyle: BlurStyle.Transparent })
    .bindContentCover(this.showCancelDialog, {
      builder: () => {
        CancelBookingDialog84({
          orderNo: 'PW840001',
          onConfirm: () => { this.showCancelDialog = false },
          onClose: () => { this.showCancelDialog = false }
        })
      }
    }, { modalTransitionOptions: ModalTransition.SLIDE })
    .bindContentCover(this.showDeleteDialog, {
      builder: () => {
        DeleteFavoriteDialog84({
          companionName: this.selectedName,
          onConfirm: () => { this.showDeleteDialog = false },
          onClose: () => { this.showDeleteDialog = false }
        })
      }
    }, { modalTransitionOptions: ModalTransition.SLIDE })
  }
}

主入口组件通过四种模态展示机制管理所有弹层:两个bindSheet和两个bindContentCoverbindSheet用于底部弹层(预约面板和编辑资料面板),使用$$this.showBookingSheet双向绑定布尔状态控制显示/隐藏,配置SheetSize.LARGE作为弹层高度和吸附点(detents),同时设置透明模糊背景。bindContentCover用于全屏覆盖型对话框(取消预约和删除收藏),使用ModalTransition.SLIDE滑入动画。每个弹层的builder回调中创建对应组件并传入属性和回调函数,回调函数统一将控制状态设为false以关闭弹层。这种集中式的弹层管理使得所有模态交互都在主入口组件中统一调度,各弹层组件本身保持无状态(除了自身的表单状态),实现了良好的关注点分离。

技术点对比

技术维度 实现方案 核心API/装饰器 优势分析 适用场景
状态管理 @State装饰器 @State、$$双向绑定 响应式UI自动更新,无需手动刷新 组件内部状态如选中索引、输入值
组件通信 回调函数属性 onConfirm/onClose回调 松耦合设计,子组件不依赖父组件实现 弹层确认/取消、列表项点击
布局策略 layoutWeight弹性分配 Text(‘’).layoutWeight(1) 自动填充剩余空间,实现左右/两端对齐 标题栏操作按钮靠右、价格靠右
横向滚动 Scroll+Row+ForEach ScrollDirection.Horizontal 可滑动展示超屏内容,隐藏滚动条保持简洁 游戏分类、技能热门、话题卡片
列表渲染 ForEach+key生成器 ForEach(data, itemFn, keyFn) 高效diff渲染,keyFn保证项的唯一标识 陪玩列表、代练列表、订单列表
双列布局 filter奇偶分组 Array.filter((_, i) => i%2===0/1) 纯ArkTS实现瀑布流,无需第三方依赖 陪玩师双列展示
底部弹层 bindSheet SheetSize.LARGE、detents 原生半模态弹层,支持拖拽吸顶 预约面板、编辑资料面板
全屏对话框 bindContentCover ModalTransition.SLIDE 原生模态覆盖,支持滑入动画 取消预约、删除收藏确认
条件渲染 if-else链 if(this.currentTab === N) 根据状态动态切换页面内容 Tab页面切换、进度条条件显示
数据可视化 Column柱状图 height动态计算+VerticalAlign.Bottom 纯组件绘制图表,无需图表库依赖 消费统计柱状图
渐变背景 linearGradient angle+colors数组 原生线性渐变,支持多色渐变 个人中心头部渐变
色彩管理 集中式ColorPalette interface+const实例化 统一管理主题色,一处修改全局生效 全局色彩体系
标签选择器 Flex+FlexWrap.Wrap Flex({wrap: FlexWrap.Wrap}) 自动换行排列,适应不同屏宽 时段选择、游戏选择、成就网格
进度条 嵌套Column 外层轨道+内层填充条 纯布局组件实现进度可视化 代练进度跟踪
状态色彩 嵌套三元表达式 condition ? A : (condition ? B : C) 动态切换文字色和边框色 代练状态标签、订单状态

安装DevEco Studio程序

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

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

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

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

在这里插入图片描述


完整代码:

// 主题:陪玩蓝 #1565C0 × 能量绿 #00C853,底色 #E3F2FD(电竞清新蓝调)
// 布局差异:游戏分类+陪玩大卡+双列陪玩 / 代练分类+代练列表+进度跟踪 / 技能标签+技能市场+热门技能 / 社区动态+热门话题 / 订单+收藏 / 渐变头+消费柱状图+陪玩记录+段位+成就

// ============ 类型定义 ============
interface Companion84 {
  id: number
  name: string
  avatar: string
  game: string
  rank: string
  tags: string[]
  price: number
  unit: string
  rating: number
  orders: number
  online: boolean
  gender: string
  intro: string
  level: number
  voice: string
}

interface Game84 {
  id: number
  name: string
  icon: string
  color: string
  bg: string
  players: string
}

interface BoostItem84 {
  id: number
  game: string
  title: string
  fromRank: string
  toRank: string
  price: number
  duration: string
  booster: string
  rating: number
  status: string
  progress: number
  image: string
  color: string
  desc: string
}

interface SkillItem84 {
  id: number
  name: string
  category: string
  provider: string
  avatar: string
  price: number
  unit: string
  rating: number
  orders: number
  tags: string[]
  desc: string
  color: string
}

interface PostItem84 {
  id: number
  user: string
  avatar: string
  title: string
  content: string
  likes: number
  comments: number
  time: string
  tag: string
}

interface TopicItem84 {
  id: number
  title: string
  hot: number
  posts: number
}

interface OrderItem84 {
  id: number
  companion: string
  game: string
  orderNo: string
  date: string
  price: number
  duration: string
  status: string
  image: string
}

interface FavoriteItem84 {
  id: number
  companion: string
  game: string
  price: number
  avatar: string
  tag: string
}

interface BarData84 {
  month: string
  value: number
}

interface RankInfo84 {
  current: string
  target: string
  star: number
  winRate: number
  kda: string
  mainRole: string
}

interface Achievement84 {
  id: number
  name: string
  icon: string
  desc: string
  unlocked: boolean
  color: string
}

interface ColorPalette84 {
  primary: string
  primaryLight: string
  primaryDark: string
  accent: string
  bg: string
  cardBg: string
  textPrimary: string
  textSecondary: string
  textHint: string
  border: string
  white: string
  success: string
  warning: string
  danger: string
}

const COLORS84: ColorPalette84 = {
  primary: '#1565C0',
  primaryLight: '#42A5F5',
  primaryDark: '#0D47A1',
  accent: '#00C853',
  bg: '#E3F2FD',
  cardBg: '#FFFFFF',
  textPrimary: '#0D47A1',
  textSecondary: '#5C7B8A',
  textHint: '#B0C4DE',
  border: '#BBDEFB',
  white: '#FFFFFF',
  success: '#00C853',
  warning: '#FF8F00',
  danger: '#E53935'
}

// ============ 静态数据 ============
const GAMES_84: Game84[] = [
  { id: 1, name: '王者荣耀', icon: '⚔️', color: '#1565C0', bg: '#E3F2FD', players: '5v5' },
  { id: 2, name: '英雄联盟', icon: '🛡️', color: '#0D47A1', bg: '#E8EAF6', players: '5v5' },
  { id: 3, name: '原神', icon: '⚔️', color: '#00838F', bg: '#E0F7FA', players: '4人' },
  { id: 4, name: '和平精英', icon: '🎯', color: '#2E7D32', bg: '#E8F5E9', players: '4人' },
  { id: 5, name: '永劫无间', icon: '🗡️', color: '#37474F', bg: '#ECEFF1', players: '3人' },
  { id: 6, name: 'DNF手游', icon: '💥', color: '#C62828', bg: '#FFEBEE', players: '4人' },
  { id: 7, name: '第五人格', icon: '🎭', color: '#4A148C', bg: '#F3E5F5', players: '1v4' },
  { id: 8, name: '金铲铲之战', icon: '🛡️', color: '#1565C0', bg: '#E3F2FD', players: '8人' }
]

const COMPANIONS_84: Companion84[] = [
  { id: 1, name: '小鹿姐姐', avatar: '🦌', game: '王者荣耀', rank: '王者50星', tags: ['国服前100', '温柔', '教学'], price: 30, unit: '元/局', rating: 4.9, orders: 3421, online: true, gender: '女', intro: '国服前100打野,耐心教学,带飞全场', level: 15, voice: '甜妹音' },
  { id: 2, name: '阿凯老师', avatar: '🦁', game: '英雄联盟', rank: '钻石一', tags: ['国服前500', '战术大师', '稳定'], price: 25, unit: '元/局', rating: 4.8, orders: 2156, online: true, gender: '男', intro: '十年老玩家,擅长打野和辅助,带你稳定上分', level: 12, voice: '青年音' },
  { id: 3, name: '糖糖喵', avatar: '🐱', game: '原神', rank: '满级玩家', tags: ['深渊满星', '温柔', '攻略'], price: 20, unit: '元/小时', rating: 5.0, orders: 1987, online: false, gender: '女', intro: '原神深渊满星攻略达人,带你轻松通关', level: 18, voice: '萝莉音' },
  { id: 4, name: '雷霆战神', avatar: '⚡', game: '和平精英', rank: '无敌战神', tags: ['吃鸡王', '搞笑', '带飞'], price: 28, unit: '元/局', rating: 4.7, orders: 1678, online: true, gender: '男', intro: '无敌战神段位,KD15+,带你把把吃鸡', level: 14, voice: '磁性音' },
  { id: 5, name: '月影小姐姐', avatar: '🌙', game: '永劫无间', rank: '罗刹', tags: ['赛季前50', '教学', '温柔'], price: 35, unit: '元/局', rating: 4.9, orders: 1234, online: true, gender: '女', intro: '永劫无间赛季前50,精通各武器连招', level: 16, voice: '御姐音' },
  { id: 6, name: '暴击大师', avatar: '💥', game: 'DNF手游', rank: '巅峰王者', tags: ['竞速达人', '幽默', '高效'], price: 22, unit: '元/小时', rating: 4.8, orders: 2345, online: false, gender: '男', intro: 'DNF手游竞速达人,高效带你通关', level: 13, voice: '青年音' },
  { id: 7, name: '暗夜猎手', avatar: '🦇', game: '第五人格', rank: '六阶监管', tags: ['国服前200', '教学', '稳定'], price: 26, unit: '元/局', rating: 4.7, orders: 1567, online: true, gender: '男', intro: '第五人格六阶监管者,擅长杰克红蝶', level: 11, voice: '青年音' },
  { id: 8, name: '星辰大海', avatar: '⭐', game: '金铲铲之战', rank: '最强王者', tags: ['赛季王者', '教学', '幽默'], price: 24, unit: '元/局', rating: 4.8, orders: 1876, online: true, gender: '男', intro: '金铲铲赛季王者,精通各阵容搭配', level: 14, voice: '青年音' },
  { id: 9, name: '樱花酱', avatar: '🌸', game: '王者荣耀', rank: '王者30星', tags: ['省服前50', '温柔', '带飞'], price: 28, unit: '元/局', rating: 4.9, orders: 2876, online: true, gender: '女', intro: '省服前50辅助,温柔带飞,耐心教学', level: 15, voice: '甜妹音' },
  { id: 10, name: '暗影刺客', avatar: '🥷', game: '英雄联盟', rank: '大师', tags: ['国服前1000', '高效', '稳定'], price: 32, unit: '元/局', rating: 4.9, orders: 1456, online: false, gender: '男', intro: '英雄联盟大师段位,中野精通', level: 16, voice: '磁性音' }
]

const BOOSTS_84: BoostItem84[] = [
  { id: 1, game: '王者荣耀', title: '星耀→王者', fromRank: '星耀3', toRank: '王者0星', price: 288, duration: '3天', booster: '小鹿姐姐', rating: 4.9, status: '进行中', progress: 60, image: '⚔️', color: '#1565C0', desc: '星耀3到王者0星,含3星保险' },
  { id: 2, game: '英雄联盟', title: '铂金→钻石', fromRank: '铂金2', toRank: '钻石4', price: 198, duration: '2天', booster: '阿凯老师', rating: 4.8, status: '待接单', progress: 0, image: '🛡️', color: '#0D47A1', desc: '铂金2到钻石4,保证胜率70%+' },
  { id: 3, game: '和平精英', title: '王牌→战神', fromRank: '王牌1', toRank: '无敌战神', price: 388, duration: '5天', booster: '雷霆战神', rating: 4.7, status: '进行中', progress: 35, image: '🎯', color: '#2E7D32', desc: '王牌1到无敌战神,保证前1000名' },
  { id: 4, game: 'DNF手游', title: '困难团本代打', fromRank: '普通', toRank: '困难通关', price: 68, duration: '1天', booster: '暴击大师', rating: 4.8, status: '已完成', progress: 100, image: '💥', color: '#C62828', desc: '困难团本代打,保证掉落装备' },
  { id: 5, game: '原神', title: '深渊满星代打', fromRank: '深渊11层', toRank: '深渊12层满星', price: 128, duration: '1天', booster: '糖糖喵', rating: 5.0, status: '待接单', progress: 0, image: '⚔️', color: '#00838F', desc: '深渊12层满星代打,含攻略' },
  { id: 6, game: '金铲铲之战', title: '白银→黄金', fromRank: '白银3', toRank: '黄金4', price: 98, duration: '2天', booster: '星辰大海', rating: 4.8, status: '进行中', progress: 80, image: '🛡️', color: '#1565C0', desc: '白银3到黄金4,含保星服务' },
  { id: 7, game: '永劫无间', title: '罗刹→罗刹王', fromRank: '罗刹1', toRank: '罗刹王', price: 268, duration: '4天', booster: '月影小姐姐', rating: 4.9, status: '待接单', progress: 0, image: '🗡️', color: '#37474F', desc: '罗刹1到罗刹王,保证胜率65%+' },
  { id: 8, game: '第五人格', title: '五阶→六阶', fromRank: '五阶监管', toRank: '六阶监管', price: 218, duration: '3天', booster: '暗夜猎手', rating: 4.7, status: '已完成', progress: 100, image: '🎭', color: '#4A148C', desc: '五阶到六阶监管代练' }
]

const SKILLS_84: SkillItem84[] = [
  { id: 1, name: '王者荣耀打野教学', category: '教学', provider: '小鹿姐姐', avatar: '🦌', price: 50, unit: '元/小时', rating: 4.9, orders: 876, tags: ['国服前100', '一对一', '实战'], desc: '国服前100打野一对一教学,从英雄选择到打野路线全教学', color: '#1565C0' },
  { id: 2, name: '英雄联盟中单教学', category: '教学', provider: '暗影刺客', avatar: '🥷', price: 45, unit: '元/小时', rating: 4.8, orders: 654, tags: ['大师段位', '一对一', '战术'], desc: '大师段位中单教学,对线技巧+团战思路', color: '#0D47A1' },
  { id: 3, name: '原神深渊攻略', category: '攻略', provider: '糖糖喵', avatar: '🐱', price: 30, unit: '元/次', rating: 5.0, orders: 543, tags: ['满星攻略', '阵容搭配'], desc: '深渊12层满星攻略,含阵容搭配和操作教学', color: '#00838F' },
  { id: 4, name: '吃鸡身法教学', category: '教学', provider: '雷霆战神', avatar: '⚡', price: 40, unit: '元/小时', rating: 4.7, orders: 432, tags: ['战神段位', '身法', '射击'], desc: '和平精英身法教学,压枪+走位+卡视角', color: '#2E7D32' },
  { id: 5, name: '永劫无间连招教学', category: '教学', provider: '月影小姐姐', avatar: '🌙', price: 55, unit: '元/小时', rating: 4.9, orders: 321, tags: ['赛季前50', '连招', '武器'], desc: '永劫无间全武器连招教学,含振刀技巧', color: '#37474F' },
  { id: 6, name: '金铲铲阵容教学', category: '攻略', provider: '星辰大海', avatar: '⭐', price: 35, unit: '元/小时', rating: 4.8, orders: 298, tags: ['赛季王者', '阵容', '运营'], desc: '金铲铲当前版本T0阵容教学+运营思路', color: '#1565C0' },
  { id: 7, name: '游戏陪聊解闷', category: '陪聊', provider: '樱花酱', avatar: '🌸', price: 20, unit: '元/小时', rating: 4.9, orders: 765, tags: ['温柔', '甜妹音', '解闷'], desc: '游戏陪聊,甜妹音温柔解闷,边玩边聊', color: '#E91E63' },
  { id: 8, name: '组队开黑带飞', category: '陪玩', provider: '阿凯老师', avatar: '🦁', price: 25, unit: '元/局', rating: 4.8, orders: 432, tags: ['稳定', '战术大师', '带飞'], desc: '英雄联盟组队开黑,稳定带飞,附战术指导', color: '#0D47A1' }
]

const POSTS_84: PostItem84[] = [
  { id: 1, user: '小鹿姐姐', avatar: '🦌', title: 'S35赛季打野英雄T度排行', content: '新赛季打野英雄强度分析,从T0到T3全面解读', likes: 3456, comments: 234, time: '1小时前', tag: '攻略' },
  { id: 2, user: '阿凯老师', avatar: '🦁', title: 'LOL中单克制关系全图', content: '135个中单英雄克制关系一图全汇总', likes: 4567, comments: 345, time: '3小时前', tag: '攻略' },
  { id: 3, user: '糖糖喵', avatar: '🐱', title: '原神3.8版本深渊满星攻略', content: '最新版本深渊12层满星阵容搭配和操作教学', likes: 2345, comments: 167, time: '5小时前', tag: '攻略' },
  { id: 4, user: '雷霆战神', avatar: '⚡', title: '吃鸡压枪终极指南', content: '全武器压枪参数设置+练习方法详解', likes: 3456, comments: 234, time: '8小时前', tag: '教学' },
  { id: 5, user: '陪玩小报', avatar: '📰', title: '本周陪玩人气排行榜', content: '本周最受欢迎陪玩TOP10,新晋陪玩推荐', likes: 5678, comments: 456, time: '12小时前', tag: '排行' },
  { id: 6, user: '赛事播报', avatar: '🎤', title: 'KPL夏季赛本周赛况', content: '本周KPL比赛回顾+下周赛程预告', likes: 6789, comments: 567, time: '1天前', tag: '赛事' }
]

const TOPICS_84: TopicItem84[] = [
  { id: 1, title: '#S35赛季上分攻略#', hot: 9999, posts: 1234 },
  { id: 2, title: '#陪玩推荐#', hot: 8765, posts: 987 },
  { id: 3, title: '#英雄克制关系#', hot: 7654, posts: 876 },
  { id: 4, title: '#段位上分日记#', hot: 6543, posts: 765 },
  { id: 5, title: '#游戏赛事讨论#', hot: 5432, posts: 654 },
  { id: 6, title: '#陪玩日常#', hot: 4321, posts: 543 }
]

const ORDERS_84: OrderItem84[] = [
  { id: 1, companion: '小鹿姐姐', game: '王者荣耀', orderNo: 'PW840001', date: '2026-08-20', price: 150, duration: '5局', status: '已完成', image: '🦌' },
  { id: 2, companion: '阿凯老师', game: '英雄联盟', orderNo: 'PW840002', date: '2026-08-18', price: 100, duration: '4局', status: '已完成', image: '🦁' },
  { id: 3, companion: '糖糖喵', game: '原神', orderNo: 'PW840003', date: '2026-08-15', price: 60, duration: '3小时', status: '已完成', image: '🐱' },
  { id: 4, companion: '雷霆战神', game: '和平精英', orderNo: 'PW840004', date: '2026-08-12', price: 84, duration: '3局', status: '已完成', image: '⚡' },
  { id: 5, companion: '月影小姐姐', game: '永劫无间', orderNo: 'PW840005', date: '2026-08-10', price: 105, duration: '3局', status: '进行中', image: '🌙' },
  { id: 6, companion: '星辰大海', game: '金铲铲之战', orderNo: 'PW840006', date: '2026-08-05', price: 72, duration: '3局', status: '已完成', image: '⭐' }
]

const FAVORITES_84: FavoriteItem84[] = [
  { id: 1, companion: '小鹿姐姐', game: '王者荣耀', price: 30, avatar: '🦌', tag: '在线' },
  { id: 2, companion: '糖糖喵', game: '原神', price: 20, avatar: '🐱', tag: '离线' },
  { id: 3, companion: '月影小姐姐', game: '永劫无间', price: 35, avatar: '🌙', tag: '在线' },
  { id: 4, companion: '樱花酱', game: '王者荣耀', price: 28, avatar: '🌸', tag: '在线' },
  { id: 5, companion: '暗影刺客', game: '英雄联盟', price: 32, avatar: '🥷', tag: '离线' }
]

const BAR_DATA_84: BarData84[] = [
  { month: '3月', value: 420 },
  { month: '4月', value: 580 },
  { month: '5月', value: 350 },
  { month: '6月', value: 720 },
  { month: '7月', value: 890 },
  { month: '8月', value: 650 }
]

const RANK_INFO_84: RankInfo84 = {
  current: '星耀3',
  target: '王者0星',
  star: 3,
  winRate: 68,
  kda: '5.2/3.1/8.7',
  mainRole: '打野'
}

const ACHIEVEMENTS_84: Achievement84[] = [
  { id: 1, name: '初次下单', icon: '🎯', desc: '完成第一笔陪玩订单', unlocked: true, color: '#1565C0' },
  { id: 2, name: '忠实客户', icon: '💎', desc: '累计消费超500元', unlocked: true, color: '#00C853' },
  { id: 3, name: '社交达人', icon: '🤝', desc: '收藏5位以上陪玩', unlocked: true, color: '#FF8F00' },
  { id: 4, name: '攻略达人', icon: '📖', desc: '发布3篇游戏攻略', unlocked: true, color: '#0D47A1' },
  { id: 5, name: '赛季王者', icon: '👑', desc: '达到王者段位', unlocked: false, color: '#E53935' },
  { id: 6, name: '百场达成', icon: '💯', desc: '完成100场陪玩', unlocked: false, color: '#4A148C' },
  { id: 7, name: '好评专家', icon: '⭐', desc: '获得10条好评', unlocked: true, color: '#FFB300' },
  { id: 8, name: '月度冠军', icon: '🏆', desc: '单月消费排名第一', unlocked: false, color: '#C62828' },
  { id: 9, name: '全勤玩家', icon: '📅', desc: '连续30天活跃', unlocked: false, color: '#1565C0' }
]

const SKILL_CATS_84: string[] = ['全部', '教学', '攻略', '陪玩', '陪聊']

// ============ 弹框组件 ============
@Component
export struct BookingSheet84 {
  @State companionName: string = ''
  @State companionGame: string = ''
  @State companionPrice: number = 30
  @State selectedGame: number = 0
  @State selectedSlot: number = 0
  @State duration: number = 1
  @State remark: string = ''
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Column()
        .width(40)
        .height(4)
        .borderRadius(2)
        .backgroundColor(COLORS84.border)
        .margin({ top: 12, bottom: 16 })

      Row() {
        Text('预约陪玩')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS84.textPrimary)
        Text('').layoutWeight(1)
        Text('✕')
          .fontSize(22)
          .fontColor(COLORS84.textHint)
          .onClick(() => { this.onClose() })
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 16 })

      // 陪玩信息
      Row() {
        Text('🦌')
          .fontSize(36)
          .width(56)
          .height(56)
          .textAlign(TextAlign.Center)
          .backgroundColor(COLORS84.bg)
          .borderRadius(12)
        Column() {
          Text(this.companionName)
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS84.textPrimary)
          Text(this.companionGame)
            .fontSize(12)
            .fontColor(COLORS84.textSecondary)
            .margin({ top: 4 })
          Row() {
            Text('⭐' + 4.9.toString())
              .fontSize(12)
              .fontColor(COLORS84.warning)
            Text('  在线')
              .fontSize(11)
              .fontColor(COLORS84.success)
          }
          .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Start)
        .margin({ left: 12 })
        .layoutWeight(1)
        Column() {
          Text('¥' + this.companionPrice.toString())
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(COLORS84.accent)
          Text('元/局')
            .fontSize(11)
            .fontColor(COLORS84.textSecondary)
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 20 })

      // 游戏选择
      Column() {
        Text('选择游戏')
          .fontSize(14)
          .fontColor(COLORS84.textSecondary)
          .margin({ bottom: 10 })
        Scroll() {
          Row() {
            ForEach(GAMES_84.slice(0, 6), (game: Game84, index: number) => {
              Column() {
                Text(game.icon)
                  .fontSize(24)
                  .width(40)
                  .height(40)
                  .textAlign(TextAlign.Center)
                  .borderRadius(10)
                  .backgroundColor(this.selectedGame === index ? COLORS84.white : game.bg)
                Text(game.name)
                  .fontSize(10)
                  .fontColor(this.selectedGame === index ? COLORS84.white : COLORS84.textPrimary)
                  .margin({ top: 4 })
              }
              .margin({ right: 10 })
              .onClick(() => { this.selectedGame = index })
            }, (game: Game84) => game.id.toString())
          }
        }
        .scrollable(ScrollDirection.Horizontal)
        .scrollBar(BarState.Off)
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 16 })

      // 时段选择
      Column() {
        Text('选择时段')
          .fontSize(14)
          .fontColor(COLORS84.textSecondary)
          .margin({ bottom: 10 })
        Flex({ wrap: FlexWrap.Wrap }) {
          ForEach(['09:00-12:00', '12:00-15:00', '15:00-18:00', '18:00-21:00', '21:00-24:00', '00:00-03:00'], (slot: string, index: number) => {
            Text(slot)
              .fontSize(12)
              .fontColor(this.selectedSlot === index ? COLORS84.white : COLORS84.textSecondary)
              .padding({ left: 12, right: 12, top: 8, bottom: 8 })
              .borderRadius(16)
              .backgroundColor(this.selectedSlot === index ? COLORS84.primary : COLORS84.bg)
              .margin({ right: 8, bottom: 8 })
              .onClick(() => { this.selectedSlot = index })
          }, (slot: string) => slot)
        }
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 16 })

      // 时长
      Row() {
        Text('预约时长')
          .fontSize(14)
          .fontColor(COLORS84.textSecondary)
        Text('').layoutWeight(1)
        Row() {
          Text('−')
            .fontSize(18)
            .fontColor(COLORS84.textPrimary)
            .width(36)
            .height(36)
            .textAlign(TextAlign.Center)
            .backgroundColor(COLORS84.bg)
            .borderRadius({ topLeft: 8, bottomLeft: 8 })
            .onClick(() => { if (this.duration > 1) { this.duration-- } })
          Text(this.duration.toString() + '局')
            .fontSize(15)
            .fontColor(COLORS84.textPrimary)
            .width(60)
            .height(36)
            .textAlign(TextAlign.Center)
            .backgroundColor(COLORS84.white)
          Text('+')
            .fontSize(18)
            .fontColor(COLORS84.textPrimary)
            .width(36)
            .height(36)
            .textAlign(TextAlign.Center)
            .backgroundColor(COLORS84.bg)
            .borderRadius({ topRight: 8, bottomRight: 8 })
            .onClick(() => { if (this.duration < 20) { this.duration++ } })
        }
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 16 })

      // 备注
      TextInput({ text: this.remark, placeholder: '选填:备注信息(如指定英雄、打法等)' })
        .fontSize(14)
        .height(44)
        .backgroundColor(COLORS84.bg)
        .borderRadius(10)
        .padding({ left: 12, right: 12 })
        .margin({ left: 20, right: 20, bottom: 12 })
        .onChange((val: string) => { this.remark = val })

      // 价格合计
      Row() {
        Text('合计')
          .fontSize(14)
          .fontColor(COLORS84.textSecondary)
        Text('').layoutWeight(1)
        Text('¥' + (this.companionPrice * this.duration).toString())
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS84.accent)
      }
      .width('92%')
      .margin({ bottom: 20 })

      Button() {
        Text('确认预约')
          .fontSize(16)
          .fontColor(COLORS84.white)
          .fontWeight(FontWeight.Bold)
      }
      .width('90%')
      .height(48)
      .backgroundColor(COLORS84.accent)
      .borderRadius(24)
      .margin({ bottom: 20 })
      .onClick(() => { this.onConfirm() })
    }
    .width('100%')
    .backgroundColor(COLORS84.cardBg)
    .borderRadius({ topLeft: 24, topRight: 24 })
  }
}

@Component
export struct CancelBookingDialog84 {
  orderNo: string = ''
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Text('取消预约')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS84.textPrimary)
        .margin({ bottom: 12 })
      Text('确认取消预约 ' + this.orderNo + ' ?取消后费用将原路退回。')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .textAlign(TextAlign.Center)
        .margin({ bottom: 24, left: 20, right: 20 })
      Row() {
        Button() {
          Text('再想想')
            .fontSize(15)
            .fontColor(COLORS84.textSecondary)
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor(COLORS84.bg)
        .borderRadius(22)
        .margin({ right: 8 })
        .onClick(() => { this.onClose() })
        Button() {
          Text('确认取消')
            .fontSize(15)
            .fontColor(COLORS84.white)
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor(COLORS84.danger)
        .borderRadius(22)
        .onClick(() => { this.onConfirm() })
      }
      .width('100%')
    }
    .width('80%')
    .padding(24)
    .backgroundColor(COLORS84.cardBg)
    .borderRadius(20)
  }
}

@Component
export struct EditProfileSheet84 {
  @State nickName: string = ''
  @State gameID: string = ''
  @State mainGame: string = ''
  @State bio: string = ''
  @State selectedGame: number = 0
  @State allowVoice: boolean = true
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Column()
        .width(40)
        .height(4)
        .borderRadius(2)
        .backgroundColor(COLORS84.border)
        .margin({ top: 12, bottom: 16 })

      Row() {
        Text('编辑玩家资料')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(COLORS84.textPrimary)
        Text('').layoutWeight(1)
        Text('✕')
          .fontSize(22)
          .fontColor(COLORS84.textHint)
          .onClick(() => { this.onClose() })
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 16 })

      // 昵称
      Text('昵称')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .margin({ left: 20, bottom: 6 })
      TextInput({ text: this.nickName, placeholder: '请输入昵称' })
        .fontSize(14)
        .height(44)
        .backgroundColor(COLORS84.bg)
        .borderRadius(10)
        .padding({ left: 12, right: 12 })
        .margin({ left: 20, right: 20, bottom: 12 })
        .onChange((val: string) => { this.nickName = val })

      // 游戏ID
      Text('游戏ID')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .margin({ left: 20, bottom: 6 })
      TextInput({ text: this.gameID, placeholder: '请输入游戏ID' })
        .fontSize(14)
        .height(44)
        .backgroundColor(COLORS84.bg)
        .borderRadius(10)
        .padding({ left: 12, right: 12 })
        .margin({ left: 20, right: 20, bottom: 12 })
        .onChange((val: string) => { this.gameID = val })

      // 主玩游戏
      Text('主玩游戏')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .margin({ left: 20, bottom: 6 })
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(GAMES_84.slice(0, 6), (game: Game84, index: number) => {
          Text(game.name)
            .fontSize(12)
            .fontColor(this.selectedGame === index ? COLORS84.white : COLORS84.textSecondary)
            .padding({ left: 12, right: 12, top: 6, bottom: 6 })
            .borderRadius(16)
            .backgroundColor(this.selectedGame === index ? COLORS84.primary : COLORS84.bg)
            .margin({ right: 8, bottom: 8 })
            .onClick(() => { this.selectedGame = index })
        }, (game: Game84) => game.id.toString())
      }
      .width('92%')
      .margin({ left: 20, right: 20, bottom: 12 })

      // 个人简介
      Text('个人简介')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .margin({ left: 20, bottom: 6 })
      TextArea({ text: this.bio, placeholder: '介绍一下自己吧...' })
        .fontSize(14)
        .height(80)
        .backgroundColor(COLORS84.bg)
        .borderRadius(10)
        .padding({ left: 12, right: 12, top: 8, bottom: 8 })
        .margin({ left: 20, right: 20, bottom: 12 })
        .onChange((val: string) => { this.bio = val })

      // 允许语音
      Row() {
        Text('允许语音通话')
          .fontSize(14)
          .fontColor(COLORS84.textPrimary)
        Text('').layoutWeight(1)
        Toggle({ type: ToggleType.Switch, isOn: this.allowVoice })
          .selectedColor(COLORS84.accent)
          .onChange((val: boolean) => { this.allowVoice = val })
      }
      .width('100%')
      .padding({ left: 20, right: 20 })
      .margin({ bottom: 20 })

      Button() {
        Text('保存资料')
          .fontSize(16)
          .fontColor(COLORS84.white)
          .fontWeight(FontWeight.Bold)
      }
      .width('90%')
      .height(48)
      .backgroundColor(COLORS84.primary)
      .borderRadius(24)
      .margin({ bottom: 20 })
      .onClick(() => { this.onConfirm() })
    }
    .width('100%')
    .backgroundColor(COLORS84.cardBg)
    .borderRadius({ topLeft: 24, topRight: 24 })
  }
}

@Component
export struct DeleteFavoriteDialog84 {
  companionName: string = ''
  onConfirm: () => void = () => {}
  onClose: () => void = () => {}

  build() {
    Column() {
      Text('删除收藏')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor(COLORS84.textPrimary)
        .margin({ bottom: 12 })
      Text('确认将 ' + this.companionName + ' 从收藏移除?')
        .fontSize(14)
        .fontColor(COLORS84.textSecondary)
        .textAlign(TextAlign.Center)
        .margin({ bottom: 24, left: 20, right: 20 })
      Row() {
        Button() {
          Text('取消')
            .fontSize(15)
            .fontColor(COLORS84.textSecondary)
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor(COLORS84.bg)
        .borderRadius(22)
        .margin({ right: 8 })
        .onClick(() => { this.onClose() })
        Button() {
          Text('删除')
            .fontSize(15)
            .fontColor(COLORS84.white)
        }
        .layoutWeight(1)
        .height(44)
        .backgroundColor(COLORS84.danger)
        .borderRadius(22)
        .onClick(() => { this.onConfirm() })
      }
      .width('100%')
    }
    .width('80%')
    .padding(24)
    .backgroundColor(COLORS84.cardBg)
    .borderRadius(20)
  }
}

// ============ Tab组件 ============
@Component
export struct CompanionTab84 {
  build() {
    Column() {
      // 搜索栏
      Row() {
        Text('🔍')
          .fontSize(18)
          .margin({ right: 8 })
        Text('搜索陪玩、游戏、段位...')
          .fontSize(14)
          .fontColor(COLORS84.textHint)
          .layoutWeight(1)
      }
      .width('92%')
      .height(40)
      .backgroundColor(COLORS84.cardBg)
      .borderRadius(20)
      .padding({ left: 16, right: 16 })
      .margin({ top: 12, bottom: 12 })

      // 游戏分类横滑
      Scroll() {
        Row() {
          ForEach(GAMES_84, (game: Game84) => {
            Column() {
              Text(game.icon)
                .fontSize(28)
                .width(48)
                .height(48)
                .textAlign(TextAlign.Center)
                .borderRadius(12)
                .backgroundColor(game.bg)
              Text(game.name)
                .fontSize(11)
                .fontColor(COLORS84.textPrimary)
                .margin({ top: 6 })
            }
            .margin({ right: 12 })
          }, (game: Game84) => game.id.toString())
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .margin({ bottom: 12 })

      Scroll() {
        Column() {
          // 人气陪玩大卡
          Column() {
            Row() {
              Text('🔥 人气陪玩推荐')
                .fontSize(18)
                .fontWeight(FontWeight.Bold)
                .fontColor(COLORS84.textPrimary)
              Text('').layoutWeight(1)
              Text('换一批 🔄')
                .fontSize(13)
                .fontColor(COLORS84.accent)
            }
            .width('100%')
            .margin({ bottom: 12 })

            // 大卡片
            Column() {
              Row() {
                Text(COMPANIONS_84[0].avatar)
                  .fontSize(48)
                  .width(72)
                  .height(72)
                  .textAlign(TextAlign.Center)
                  .backgroundColor(COLORS84.bg)
                  .borderRadius(36)
                Column() {
                  Row() {
                    Text(COMPANIONS_84[0].name)
                      .fontSize(16)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(COLORS84.textPrimary)
                    Text('LV' + COMPANIONS_84[0].level.toString())
                      .fontSize(10)
                      .fontColor(COLORS84.white)
                      .backgroundColor(COLORS84.accent)
                      .borderRadius(4)
                      .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                      .margin({ left: 8 })
                  }
                  Text(COMPANIONS_84[0].game + ' · ' + COMPANIONS_84[0].rank)
                    .fontSize(12)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 4 })
                  Text(COMPANIONS_84[0].intro)
                    .fontSize(12)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 4 })
                    .maxLines(1)
                  Row() {
                    ForEach(COMPANIONS_84[0].tags, (tag: string) => {
                      Text(tag)
                        .fontSize(10)
                        .fontColor(COLORS84.primary)
                        .backgroundColor(COLORS84.bg)
                        .borderRadius(4)
                        .padding({ left: 4, right: 4, top: 1, bottom: 1 })
                        .margin({ right: 4 })
                    }, (tag: string) => tag)
                  }
                  .margin({ top: 6 })
                }
                .alignItems(HorizontalAlign.Start)
                .margin({ left: 12 })
                .layoutWeight(1)
                Column() {
                  Text('¥' + COMPANIONS_84[0].price.toString())
                    .fontSize(20)
                    .fontWeight(FontWeight.Bold)
                    .fontColor(COLORS84.accent)
                  Text(COMPANIONS_84[0].unit)
                    .fontSize(10)
                    .fontColor(COLORS84.textSecondary)
                  Text(COMPANIONS_84[0].online ? '●在线' : '●离线')
                    .fontSize(11)
                    .fontColor(COMPANIONS_84[0].online ? COLORS84.success : COLORS84.textHint)
                    .margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.End)
              }
              .width('100%')

              Row() {
                Text('⭐ ' + COMPANIONS_84[0].rating.toString())
                  .fontSize(12)
                  .fontColor(COLORS84.warning)
                Text('  成交' + COMPANIONS_84[0].orders.toString() + '单')
                  .fontSize(12)
                  .fontColor(COLORS84.textSecondary)
                Text('').layoutWeight(1)
                Text('🔊 ' + COMPANIONS_84[0].voice)
                  .fontSize(11)
                  .fontColor(COLORS84.textSecondary)
              }
              .width('100%')
              .margin({ top: 10 })
            }
            .width('100%')
            .padding(16)
            .backgroundColor(COLORS84.cardBg)
            .borderRadius(16)
          }
          .width('92%')
          .padding(16)
          .backgroundColor(COLORS84.cardBg)
          .borderRadius(16)
          .margin({ bottom: 12 })

          // 双列陪玩列表
          Row() {
            Text('全部陪玩')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .fontColor(COLORS84.textPrimary)
            Text('').layoutWeight(1)
            Text('人气 ▼')
              .fontSize(13)
              .fontColor(COLORS84.textSecondary)
          }
          .width('92%')
          .margin({ bottom: 12 })

          Row() {
            Column() {
              ForEach(COMPANIONS_84.filter((_: Companion84, i: number) => i % 2 === 0), (comp: Companion84) => {
                Column() {
                  Row() {
                    Text(comp.avatar)
                      .fontSize(36)
                      .width(48)
                      .height(48)
                      .textAlign(TextAlign.Center)
                      .backgroundColor(COLORS84.bg)
                      .borderRadius(24)
                    Column() {
                      Text(comp.name)
                        .fontSize(13)
                        .fontWeight(FontWeight.Bold)
                        .fontColor(COLORS84.textPrimary)
                        .maxLines(1)
                      Text(comp.rank)
                        .fontSize(10)
                        .fontColor(COLORS84.accent)
                        .margin({ top: 2 })
                    }
                    .alignItems(HorizontalAlign.Start)
                    .margin({ left: 8 })
                    Text('').layoutWeight(1)
                    Text(comp.online ? '●' : '○')
                      .fontSize(10)
                      .fontColor(comp.online ? COLORS84.success : COLORS84.textHint)
                  }
                  .width('100%')
                  Text(comp.game)
                    .fontSize(11)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 6 })
                  Text(comp.intro)
                    .fontSize(10)
                    .fontColor(COLORS84.textSecondary)
                    .margin({ top: 4 })
                    .maxLines(2)
                  Row() {
                    Text('⭐' + comp.rating.toString())
                      .fontSize(10)
                      .fontColor(COLORS84.warning)
                    Text('').layoutWeight(1)
                    Text('¥' + comp.price.toString())
                      .fontSize(14)
                      .fontWeight(FontWeight.Bold)
                      .fontColor(COLORS84.accent)
                  }
                  .width('100%')
                  .margin({ top: 6 })
                }
                .layoutWeight(1)
                .padding(10)
                .backgroundColor(COLORS84.cardBg)
                .borderRadius(12)
                .margin({ right: 6, bottom: 12 })
            
---

## 总结
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/1fea9ce2ea804311bffed887d9bd2935.png#pic_center)

本文深入剖析了一个基于HarmonyOS ArkTS声明式UI范式构建的游戏陪玩平台应用。该应用以六大功能Tab为核心骨架,涵盖了陪玩大厅、代练服务、技能市场、社区动态、个人中心等完整业务模块,通过统一的色彩体系和一致的卡片设计语言,在视觉层面保持了高度的品牌识别度。在技术实现层面,应用充分运用了ArkTS的`@State`响应式状态管理、`ForEach`高效列表渲染、`layoutWeight`弹性布局、`bindSheet`/`bindContentCover`原生模态展示等核心能力,构建了一个功能丰富且交互流畅的移动端应用。

从架构设计的角度来看,该应用展现了良好的组件化思维。主入口组件作为中枢,统一管理Tab切换和弹层调度,各Tab组件和弹层组件保持高度自治。数据层通过TypeScript接口定义和静态数组初始化,为组件消费提供了类型安全的数据契约。每个组件内部的布局都遵循"容器-内容"的嵌套范式,通过`Row`/`Column`/`Flex`的组合实现了从简单列表到双列瀑布流、从柱状图到渐变头部的多样化UI效果。特别是双列陪玩列表的奇偶分组实现方式和纯组件柱状图绘制方案,充分展示了ArkTS声明式UI在复杂布局场景下的表达能力。

数据层可以从静态数组升级为网络请求+本地缓存+状态管理的完整数据流方案,引入`@Provide`/`@Consume``AppStorage`实现跨组件状态共享。交互层面可以增加下拉刷新、上拉加载、骨架屏等现代化列表体验,以及`animateTo`动画和`transition`转场动画提升视觉表现力。业务层面可以引入实时聊天、语音通话、支付集成等深度功能,使陪玩平台从展示型应用进化为全功能服务平台。ArkTS声明式UI所提供的丰富组件和API能力,完全能够支撑这些扩展需求,使应用在HarmonyOS生态中持续演进。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐