鸿蒙原生应用实战(三):游戏详情页与交互功能 — 400行ArkTS深度实践

一、前言

游戏详情页是整个 App 最复杂的页面,共计 444 行 ArkTS 代码,包含 11 个功能模块。本篇将深入剖析详情页的完整开发过程,涵盖:

  • 全屏渐变色 Header 设计
  • 游客观数据面板
  • 进度条与开发者信息
  • 游戏简介排版
  • 状态切换与评分交互
  • TextArea 评价输入框
  • 成就列表系统
  • 横向滚动"相似推荐"
  • 用户评测展示

二、页面整体布局

┌─────────────────────────────────┐
│     ←       ···         (全屏)  │
│  🎮                            │
│  艾尔登法环                      │  ← 渐变 Header (190px)
│  PC · 动作RPG · 2022-02-25      │
├─────────────────────────────────┤
│  49     186h   38/42   通关      │  ← 数据面板
├─────────────────────────────────┤
│  ████████░░░░░░ 100%            │  ← 进度条
├─────────────────────────────────┤
│  [游戏截图占位图]                │  ← 截图区(预留)
├─────────────────────────────────┤
│  开发商: FromSoftware           │  ← 开发者信息
│  发行商: Bandai Namco           │
│  发行日期: 2022-02-25          │
├─────────────────────────────────┤
│  游戏简介                        │  ← 简介区
│  ······                        │
├─────────────────────────────────┤
│  我的状态                        │
│  [想玩] [在玩] [通关]            │  ← 交互区
│  我的评分: ⭐⭐⭐⭐⭐              │
│  [📝 写评价]                     │
│  ┌────────────────────────┐     │
│  │ TextArea 输入框         │     │  ← 评价输入(展开)
│  └────────────────────────┘     │
├─────────────────────────────────┤
│  🏅 成就列表                     │  ← 成就系统
│  🏆 史诗级游戏收藏家     🔒     │
│  🏆 白金达人            🔒     │
├─────────────────────────────────┤
│  🎯 你可能也喜欢                  │  ← 相似推荐(横向)
│  [只狼] [黑暗之魂3] [仁王2]      │
├─────────────────────────────────┤
│  💬 用户评测                     │  ← 评测展示
│  ⭐⭐⭐⭐⭐ "神作!"              │
└─────────────────────────────────┘

三、扩展数据模型

详情页需要比列表页更丰富的数据,我们新增 GameDetail 接口:

interface GameDetail {
  id: number;
  title: string;
  platform: string;
  genre: string;
  status: string;
  rating: number;
  hours: number;
  progress: number;
  coverColor: string;
  developer: string;       // 开发商
  publisher: string;       // 发行商
  releaseDate: string;     // 发行日期
  description: string;     // 游戏简介
  achievements: number;    // 已获得成就
  totalAchievements: number; // 成就总数
}

与 GameItem 的区别

  • 新增 6 个字段:developer, publisher, releaseDate, description, achievements, totalAchievements
  • 承载详情展示所需的所有信息

四、状态管理

struct GameDetailPage {
  @State gameId: number = -1;
  @State game: GameDetail | null = null;
  @State myStatus: string = '';          // 用户自选状态
  @State myRating: number = 0;           // 用户评分 (0-50)
  @State showReviewBox: boolean = false; // 是否展开评价框
  @State userReview: string = '';        // 评价内容
}

aboutToAppear 生命周期

aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params && params['gameId'] !== undefined) {
    this.gameId = params['gameId'] as number;
  }
  this.loadGame();
}

五、核心功能模块实现

5.1 数据加载与查找

loadGame(): void {
  const allGames: GameDetail[] = [
    {
      id: 1,
      title: '艾尔登法环',
      platform: 'PC',
      genre: '动作RPG',
      status: '通关',
      rating: 49,
      hours: 186,
      progress: 100,
      coverColor: '#FFD700',
      developer: 'FromSoftware',
      publisher: 'Bandai Namco',
      releaseDate: '2022-02-25',
      description: '以黑暗奇幻世界为舞台的硬核动作RPG。玩家将穿越"狭间之地",挑战半神与古老之王,最终成为艾尔登之王。广阔的开放世界、深度的角色养成和极具挑战性的战斗系统令人着迷。',
      achievements: 38,
      totalAchievements: 42
    }
    // ... 更多游戏数据
  ];

  // 根据路由参数查找对应游戏
  for (let i: number = 0; i < allGames.length; i++) {
    if (allGames[i].id === this.gameId) {
      this.game = allGames[i];
      this.myStatus = allGames[i].status;
      this.myRating = allGames[i].rating;
      break;
    }
  }

  // 兜底逻辑:未找到时显示第一条数据
  if (!this.game) {
    this.game = allGames[0];
  }
}

注意:ArkTS 中 for 循环的计数变量必须显式声明类型 let i: number

5.2 全屏渐变 Header ⭐

这是详情页最亮眼的视觉设计:

@Builder buildHeader() {
  Stack() {
    // 背景色块(使用游戏封面颜色)
    Column()
      .width('100%').height(190)
      .backgroundColor(this.game ? this.game.coverColor : '#333333')
      .justifyContent(FlexAlign.Center)

    Column() {
      // 顶部导航行(返回 + 更多)
      Row() {
        Text('←').fontSize(22).fontColor(Color.White)
          .onClick(() => { router.back(); })
        Blank()
        Text('···').fontSize(22).fontColor(Color.White)
      }
      .width('100%').padding({ left: 16, right: 16 })
      .position({ top: 40 })

      // 游戏信息
      if (this.game) {
        Column() {
          Text('🎮').fontSize(48)
          Text(this.game.title)
            .fontSize(20).fontWeight(FontWeight.Bold)
            .fontColor(Color.White).margin({ top: 4 })
          Text(`${this.game.platform} · ${this.game.genre} · ${this.game.releaseDate}`)
            .fontSize(12).fontColor('rgba(255,255,255,0.8)').margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.Center)
        .width('100%').position({ top: 75 })
      }
    }
    .width('100%').height('100%')
  }
  .width('100%').height(190)
}

设计要点

  • 使用 Stack 实现层叠布局:背景层 + 内容层
  • 背景色取自游戏封面色,形成品牌一致性
  • 文字颜色使用 rgba(255,255,255,0.8) 半透明白,增强层次感
  • 返回按钮用 position({ top: 40 }) 固定在安全区域
  • 游戏标题居中显示在大约 75px 位置(顶部留白给导航行)

5.3 数据面板

@Builder buildInfoSection() {
  if (this.game) {
    Row() {
      Column() {
        Text(this.game.rating.toString())
          .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FF6B35')
        Text('评分(媒体)').fontSize(10).fontColor('#999999').margin({ top: 2 })
      }.layoutWeight(1).alignItems(HorizontalAlign.Center)

      Column() {
        Text(this.game.hours.toString())
          .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#3498DB')
        Text('游玩时长(h)').fontSize(10).fontColor('#999999').margin({ top: 2 })
      }.layoutWeight(1).alignItems(HorizontalAlign.Center)

      Column() {
        Text(`${this.game.achievements}/${this.game.totalAchievements}`)
          .fontSize(18).fontWeight(FontWeight.Bold).fontColor('#2ECC71')
        Text('成就进度').fontSize(10).fontColor('#999999').margin({ top: 2 })
      }.layoutWeight(1).alignItems(HorizontalAlign.Center)

      Column() {
        Text(this.game.status)
          .fontSize(14).fontWeight(FontWeight.Bold)
          .fontColor(this.getStatusColor(this.game.status))
        Text('游玩状态').fontSize(10).fontColor('#999999').margin({ top: 2 })
      }.layoutWeight(1).alignItems(HorizontalAlign.Center)
    }
    .width('100%').padding(14).backgroundColor('#FFFFFF')
    .margin({ top: 8, left: 16, right: 16 }).borderRadius(10)
  }
}

四个数据卡片:评分 / 时长 / 成就 / 状态,使用 layoutWeight(1) 均分宽度。

5.4 状态颜色映射

getStatusColor(status: string): ResourceStr {
  if (status === '通关') return '#2ECC71';   // 绿色
  if (status === '在玩') return '#3498DB';   // 蓝色
  if (status === '想玩') return '#9B59B6';   // 紫色
  return '#999999';                           // 灰色
}

5.5 开发者信息行

三列并排布局,显示开发商、发行商、发行日期:

@Builder buildDevInfo() {
  if (this.game) {
    Row() {
      Column() {
        Text('开发商').fontSize(11).fontColor('#999999')
        Text(this.game.developer)
          .fontSize(13).fontColor('#333333').margin({ top: 2 })
      }.layoutWeight(1).alignItems(HorizontalAlign.Start)

      Column() {
        Text('发行商').fontSize(11).fontColor('#999999')
        Text(this.game.publisher)
          .fontSize(13).fontColor('#333333').margin({ top: 2 })
      }.layoutWeight(1).alignItems(HorizontalAlign.Start)

      Column() {
        Text('发行日期').fontSize(11).fontColor('#999999')
        Text(this.game.releaseDate)
          .fontSize(13).fontColor('#333333').margin({ top: 2 })
      }.layoutWeight(1).alignItems(HorizontalAlign.Start)
    }
    // ...
  }
}

5.6 状态控制与评分交互 ⭐⭐

这是详情页交互最丰富的区域,包含三个子功能:

@Builder buildStatusControl() {
  Column() {
    Text('我的状态').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')

    // 状态切换按钮组
    Row() {
      ForEach(['想玩', '在玩', '通关'], (s: string) => {
        Text(s)
          .fontSize(13)
          .fontColor(this.myStatus === s ? '#FFFFFF' : '#666666')
          .padding({ left: 20, right: 20, top: 6, bottom: 6 })
          .backgroundColor(this.myStatus === s ? '#FF6B35' : '#F0F0F0')
          .borderRadius(16).margin({ right: 10 })
          .onClick(() => { this.myStatus = s; })
      }, (s: string) => s)
    }.width('100%').margin({ top: 8 })

    // 星级评分
    Row() {
      Text('我的评分:').fontSize(13).fontColor('#666666')
      ForEach([1, 2, 3, 4, 5], (star: number) => {
        Text(star <= Math.round(this.myRating / 10) ? '⭐' : '☆')
          .fontSize(18)
          .onClick(() => { this.myRating = star * 10; })
      }, (star: number) => star.toString())
    }.width('100%').margin({ top: 8 })

    // 展开/收起评价框
    Row() {
      Text(this.showReviewBox ? '📝 收起评价' : '📝 写评价')
        .fontSize(13).fontColor('#FF6B35')
        .onClick(() => { this.showReviewBox = !this.showReviewBox; })
    }.width('100%').margin({ top: 8 })

    // 条件渲染:评价输入框
    if (this.showReviewBox) {
      TextArea({ placeholder: '写下你的游戏评价...', text: this.userReview })
        .width('100%').height(80).backgroundColor('#F5F5F5')
        .borderRadius(8).fontSize(13).padding(8).margin({ top: 6 })
        .onChange((v: string) => { this.userReview = v; })
    }
  }
  // ...
}

交互逻辑详解

功能 实现方式 交互反馈
状态切换 点击更新 myStatus,三选一 选中态变橙色高亮
星级评分 点击更新 myRating,存储值为 10/20/30/40/50 实心/空心星切换
评价框 点击展开 TextArea,再点击收起 展开/收起动画

评分系统映射

用户点 1 星 → myRating = 10 → 对应 1/5 分制
用户点 5 星 → myRating = 50 → 对应 5/5 分制

5.7 成就列表系统

成就列表使用二维数组存储,每种成就有标题、描述、解锁状态:

@Builder buildAchievementList() {
  Column() {
    Text('🏅 成就列表')
      .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
      .width('100%')

    const achievementsData: string[][] = [
      ['史诗级游戏收藏家', '收藏10款游戏', '🔒'],
      ['白金达人', '完成一款游戏全部成就', '🔒'],
      ['初心者', '添加第一款游戏', '✅'],
      ['游戏鉴赏家', '评价5款游戏', '✅'],
      ['全平台制霸', '在3个不同平台拥有游戏', '🔒'],
      ['夜猫子', '凌晨2点后还在玩游戏', '✅']
    ];

    ForEach(achievementsData, (ach: string[]) => {
      Row() {
        Text(ach[2] === '✅' ? '🏆' : '🏅')
          .fontSize(18)
          .grayscale(ach[2] === '✅' ? 0 : 1)    // 未解锁=灰色

        Column() {
          Text(ach[0]).fontSize(13).fontWeight(FontWeight.Medium)
            .fontColor(ach[2] === '✅' ? '#333333' : '#BBBBBB')
          Text(ach[1]).fontSize(11).fontColor('#999999').margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start).margin({ left: 10 }).layoutWeight(1)

        Text(ach[2]).fontSize(14)    // "🔒" 或 "✅"
      }
      .width('100%').padding({ left: 16, right: 16, top: 8, bottom: 8 })
      .backgroundColor('#FFFFFF')
    }, (ach: string[]) => ach[0])
  }
  .width('100%').backgroundColor('#FFFFFF').margin({ top: 8 })
}

设计亮点

  • 已解锁成就:金色彩色奖杯 🏆 + 深色文字
  • 未解锁成就:灰色调奖牌 🏅 + 浅灰文字 + grayscale(1) 滤镜
  • 🔒 / 符号在行尾直观显示状态

5.8 相似推荐(横向滚动)

@Builder buildSimilarGames() {
  Column() {
    Text('🎯 你可能也喜欢')
      .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
      .width('100%').padding({ left: 16 })

    Scroll() {
      Row() {
        const similarGames: string[][] = [
          ['只狼:影逝二度', '动作', '#E74C3C'],
          ['黑暗之魂3', '动作RPG', '#2C3E50'],
          ['仁王2', '动作', '#FF6B35']
        ];

        ForEach(similarGames, (s: string[]) => {
          Column() {
            // 封面占位块
            Column().width(90).height(60).borderRadius(6)
              .backgroundColor(s[2] as string)
            Text('🎮').fontSize(24)

            Text(s[0]).fontSize(11).fontColor('#333333').margin({ top: 4 })
              .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
              .width(90)
            Text(s[1]).fontSize(10).fontColor('#999999')
          }
          .margin({ right: 10 })
        }, (s: string[]) => s[0])
      }
      .padding({ left: 16 })
    }
    .scrollable(ScrollDirection.Horizontal).height(110)
  }
  // ...
}

5.9 用户评测系统

展示三条模拟评测,包含头像、用户名、星级、内容和时间:

@Builder buildReviewSection() {
  Column() {
    Text('💬 用户评测')
      .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
      .width('100%').padding({ left: 16 })

    const reviewData: string[][] = [
      ['游戏达人', '⭐⭐⭐⭐⭐', '神作!无论是画面、剧情还是战斗系统都无可挑剔!', '2天前'],
      ['休闲玩家', '⭐⭐⭐⭐', '很好玩但难度较高,手残党需要多练习。', '1周前'],
      ['收集控', '⭐⭐⭐⭐⭐', '全成就达成!200+小时的极致体验。', '2周前']
    ];

    ForEach(reviewData, (review: string[]) => {
      Column() {
        Row() {
          // 用户头像(取用户名首字)
          Stack() {
            Column().width(28).height(28).borderRadius(14)
              .backgroundColor('#FF6B35')
              .justifyContent(FlexAlign.Center)
            Text(review[0].charAt(0)).fontSize(14).fontColor(Color.White)
          }
          Text(review[0]).fontSize(13).fontWeight(FontWeight.Medium)
            .fontColor('#333333').margin({ left: 8 })
          Blank()
          Text(review[3]).fontSize(11).fontColor('#BBBBBB')
        }.width('100%')

        Text(review[1]).fontSize(14).margin({ top: 4 })
        Text(review[2]).fontSize(12).fontColor('#666666')
          .lineHeight(18).width('100%').margin({ top: 4 })
      }
      .width('100%').padding({ left: 16, right: 16, top: 8, bottom: 8 })
      .backgroundColor('#FFFFFF')
    }, (review: string[]) => review[0])
  }
  .width('100%').backgroundColor('#FFFFFF').margin({ top: 8 })
}

5.10 截图区域(预留)

@Builder buildScreenshotGallery() {
  Stack() {
    Column().width('100%').height(140)
      .backgroundColor('#F0F0F0').borderRadius(8)

    Column() {
      Text('🎮 游戏截图').fontSize(13).fontColor('#999999')
      Text('(截图功能预留)').fontSize(11).fontColor('#BBBBBB').margin({ top: 4 })
    }
  }
  .width('100%').height(140)
  .margin({ top: 8, left: 16, right: 16 })
}

预留占位区域,后续可接入图片懒加载或 gallery 组件。


六、布局组装

build(): void {
  Column() {
    Scroll() {
      Column() {
        this.buildHeader()            // 全屏 Header
        this.buildInfoSection()       // 数据面板
        this.buildProgress()          // 进度条
        this.buildScreenshotGallery() // 截图区
        this.buildDevInfo()           // 开发者信息
        this.buildDescription()       // 游戏简介
        this.buildStatusControl()     // 状态 + 评分 + 评价
        this.buildAchievementList()   // 成就列表
        this.buildSimilarGames()      // 相似推荐
        this.buildReviewSection()     // 用户评测
      }
      .width('100%').padding({ bottom: 30 })
    }
    .scrollable(ScrollDirection.Vertical)
    .layoutWeight(1).width('100%')
  }
  .width('100%').height('100%').backgroundColor('#F5F5F5')
}

七、444行代码的分层架构

整个详情页的代码组织遵循清晰的分层:

代码段 行数 职责
Interface + State L1-30 数据模型与状态变量
loadGame() L31-58 数据加载与路由参数解析
@Builder × 11 L67-416 11 个 UI 构建函数
build() L418-444 页面布局组装

每个 @Builder 方法控制在 20-40 行,职责单一,可读性强。


八、与列表页的联动

从列表页跳转到详情页的完整数据流:

GameListPage                      GameDetailPage
    │                                   │
    │  router.pushUrl({                  │
    │    url: 'pages/GameDetailPage',    │
    │    params: { gameId: game.id }     │
    │  })                                │
    │ ───────────────────────────────►   │
    │                                   │
    │                            aboutToAppear()
    │                              │
    │                              ▼
    │                      router.getParams()
    │                              │
    │                              ▼
    │                      loadGame()
    │                       查找 gameId
    │                              │
    │                              ▼
    │                      @State 更新
    │                      触发 UI 刷新

在这里插入图片描述

九、小结

本篇我们完成了详情页的全部 11 个功能模块开发:

  1. ✅ 全屏渐变 Header + 游戏信息展示
  2. ✅ 四维数据面板(评分/时长/成就/状态)
  3. ✅ 线性进度条
  4. ✅ 开发者信息三列布局
  5. ✅ 游戏简介排版
  6. ✅ 状态切换按钮组 + 星级评分 + TextArea 评价
  7. ✅ 成就列表系统(解锁/未解锁视觉区分)
  8. ✅ 横向滚动相似推荐
  9. ✅ 用户评测展示

下一篇将开发 愿望单页面个人统计页面,实现数据聚合与可视化展示。


系列目录

  • 第一篇:项目搭建与首页开发
  • 第二篇:游戏库列表与筛选排序
  • 第三篇:游戏详情页与交互功能(本文)
  • 第四篇:愿望单与个人统计
  • 第五篇:路由导航与工程优化
Logo

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

更多推荐