HarmonyOS 诗词填空游戏开发实战教程(非AI生成 提供源代码和演示视频)

HarmonyOS 诗词填空游戏开发实战教程

效果视频

鸿蒙版古诗词填空游戏

前言

本教程将手把手教你开发一个古诗词填空小游戏,适合 HarmonyOS 初学者跟随学习。完成本教程后,你将学会:

  • 如何组织 HarmonyOS 项目结构
  • 如何管理游戏数据
  • 如何实现游戏界面和交互逻辑
  • 如何配置页面路由

开发环境要求:

  • DevEco Studio(推荐最新版本)
  • HarmonyOS SDK 6.0.0 或以上
  • 基础的 TypeScript 知识

预计用时: 30-45 分钟

让我们开始吧!


第一步:创建项目和目录结构

1.1 打开你的 HarmonyOS 项目

假设你已经有一个名为 tiankong 的 HarmonyOS 项目。如果没有,请先用 DevEco Studio 创建一个新项目。

1.2 创建所需目录

我们需要创建两个新文件夹来组织代码:

方法一:使用 DevEco Studio

  1. 在项目视图中,找到 entry/src/main/ets/ 目录
  2. 右键点击 ets 文件夹 → NewDirectory
  3. 输入 data,回车创建
  4. 再次右键点击 ets 文件夹 → NewDirectory
  5. 输入 pages/game(会自动创建两级目录),回车创建

方法二:使用命令行

打开终端(Terminal),执行:

bash 复制代码
cd 你的项目路径/tiankong
mkdir -p entry/src/main/ets/data
mkdir -p entry/src/main/ets/pages/game

验证结果:

你的目录结构应该如下:

复制代码
tiankong/
└── entry/
    └── src/
        └── main/
            └── ets/
                ├── data/           ← 新建(存放数据文件)
                ├── pages/
                │   ├── game/      ← 新建(存放游戏页面)
                │   └── Index.ets
                ├── entryability/
                └── entrybackupability/

第二步:创建诗词数据库

现在我们来创建第一个文件,存放所有的诗词数据。

2.1 创建文件

  1. 在 DevEco Studio 中,右键点击 entry/src/main/ets/data 文件夹
  2. 选择 NewArkTS File
  3. 输入文件名:CultureData(不需要 .ets 后缀)
  4. 点击确定

2.2 编写数据结构

打开刚创建的 CultureData.ets 文件,先定义数据类型:

typescript 复制代码
/*
 * 诗词园地数据
 */

// 定义文化类别:诗词、歇后语、故事
export type CultureCategory = 'poetry' | 'idiom' | 'story'

// 定义动物类型:鸡、鸭、鹅
export type AnimalType = 'chicken' | 'duck' | 'goose'

// 定义文化数据项的结构
export interface CultureItem {
  id: string              // 唯一标识
  category: CultureCategory  // 类别
  animalType: AnimalType     // 动物类型
  title: string           // 标题(诗词名)
  content: string         // 内容(诗词正文)
  author?: string         // 作者
  dynasty?: string        // 朝代
  explanation?: string    // 解释说明
  tags: string[]          // 标签
  isFavorite: boolean     // 是否收藏
}

💡 知识点:

  • export 表示导出,其他文件可以导入使用
  • type 定义类型别名
  • interface 定义对象的结构
  • ? 表示可选属性

2.3 创建数据库类

继续在同一个文件中添加数据库类:

typescript 复制代码
export class CultureDatabase {
  /**
   * 获取所有诗词数据
   */
  public static getPoetryData(): CultureItem[] {
    return [
      // 第一首诗:咏鹅
      {
        id: 'poetry_goose_001',
        category: 'poetry',
        animalType: 'goose',
        title: '咏鹅',
        author: '骆宾王',
        dynasty: '唐',
        content: '鹅,鹅,鹅,曲项向天歌。\n白毛浮绿水,红掌拨清波。',
        explanation: '这首诗是骆宾王七岁时所作,通过对鹅的形态、颜色、动作的描写,生动形象地展现了白鹅的美丽和可爱。',
        tags: ['骆宾王', '咏物诗', '儿童诗', '经典'],
        isFavorite: false
      },

      // 第二首诗:画鸡
      {
        id: 'poetry_chicken_001',
        category: 'poetry',
        animalType: 'chicken',
        title: '画鸡',
        author: '唐寅',
        dynasty: '明',
        content: '头上红冠不用裁,满身雪白走将来。\n平生不敢轻言语,一叫千门万户开。',
        explanation: '这首诗通过描绘大公鸡的形象,赞美了它的美丽和报晓的功劳。"一叫千门万户开"突出了公鸡报晓的重要作用。',
        tags: ['唐寅', '咏物诗', '寓意深刻'],
        isFavorite: false
      },

      // 第三首诗:惠崇春江晚景
      {
        id: 'poetry_duck_001',
        category: 'poetry',
        animalType: 'duck',
        title: '惠崇春江晚景',
        author: '苏轼',
        dynasty: '宋',
        content: '竹外桃花三两枝,春江水暖鸭先知。\n蒌蒿满地芦芽短,正是河豚欲上时。',
        explanation: '"春江水暖鸭先知"成为千古名句,生动描绘了鸭子在春江中嬉戏的情景。',
        tags: ['苏轼', '题画诗', '春景', '名句'],
        isFavorite: false
      }

      // 💡 这里可以继续添加更多诗词
      // 完整版包含17首诗词,为了简洁,这里只展示3首
      // 你可以从文末的完整代码中复制其余诗词
    ]
  }

  /**
   * 获取所有歇后语数据(暂时返回空数组)
   */
  public static getIdiomData(): CultureItem[] {
    return []
  }

  /**
   * 获取所有成语故事数据(暂时返回空数组)
   */
  public static getStoryData(): CultureItem[] {
    return []
  }

  /**
   * 获取所有文化数据
   */
  public static getAllCultureData(): CultureItem[] {
    let result: CultureItem[] = []
    result = result.concat(CultureDatabase.getPoetryData())
    result = result.concat(CultureDatabase.getIdiomData())
    result = result.concat(CultureDatabase.getStoryData())
    return result
  }

  /**
   * 根据分类获取数据
   */
  public static getDataByCategory(category: CultureCategory): CultureItem[] {
    const allData = CultureDatabase.getAllCultureData()
    return allData.filter(item => item.category === category)
  }

  /**
   * 根据动物类型获取数据
   */
  public static getDataByAnimalType(animalType: AnimalType): CultureItem[] {
    const allData = CultureDatabase.getAllCultureData()
    return allData.filter(item => item.animalType === animalType)
  }
}

💡 知识点:

  • static 表示静态方法,可以直接通过类名调用,无需创建实例
  • filter() 是数组方法,用于筛选符合条件的元素
  • concat() 用于合并数组

2.4 保存文件

Ctrl + S(Windows)或 Cmd + S(Mac)保存文件。

📝 小贴士: 完整的诗词数据(17首)请参考文末附录,现在继续下一步。


第三步:创建游戏数据管理器

这个文件负责把诗词转换成游戏题目。

3.1 创建文件

  1. 右键点击 entry/src/main/ets/data 文件夹
  2. 选择 NewArkTS File
  3. 输入文件名:GameDataManager
  4. 点击确定

3.2 定义题目数据结构

打开 GameDataManager.ets,先导入数据库和定义题目结构:

typescript 复制代码
/*
 * 游戏数据管理器
 */

import { CultureDatabase, CultureItem } from './CultureData'

/**
 * 游戏题目接口
 */
export interface GameQuestion {
  id: string              // 题目ID
  type: 'idiom' | 'proverb' | 'poetry'  // 题目类型
  question: string        // 题目内容(带填空的诗句)
  answer: string          // 正确答案
  options?: string[]      // 四个选项
  hint: string            // 提示信息
  animalType: string      // 动物类型
}

3.3 创建游戏数据管理器类

继续添加核心逻辑:

typescript 复制代码
/**
 * 游戏数据管理器
 */
export class GameDataManager {
  /**
   * 获取古诗词填空游戏题目
   */
  static getPoetryQuestions(): GameQuestion[] {
    // 1. 获取所有诗词数据
    const poetry = CultureDatabase.getPoetryData()
    const questions: GameQuestion[] = []

    // 2. 遍历每首诗
    poetry.forEach(poem => {
      // 定义要查找的家禽名称(按长度从长到短,避免误匹配)
      const animalNames = ['公鸡', '母鸡', '雏鸡', '小鸡', '鹅儿', '鸭儿', '鸡', '鸭', '鹅']

      // 3. 查找诗句中是否包含这些名称
      for (const animalName of animalNames) {
        if (poem.content.includes(animalName)) {

          // 4. 创建填空题:将家禽名称替换为 ____
          const index = poem.content.indexOf(animalName)
          const blankContent = poem.content.substring(0, index) +
                              '____' +
                              poem.content.substring(index + animalName.length)

          // 5. 生成答案和选项
          let correctOption = animalName
          let options: string[] = []

          // 将复合词简化为基础词作为答案
          if (animalName.includes('鸡')) {
            correctOption = '鸡'
            options = ['鸡', '鸭', '鹅', '雀']
          } else if (animalName.includes('鸭')) {
            correctOption = '鸭'
            options = ['鸭', '鸡', '鹅', '雁']
          } else if (animalName.includes('鹅')) {
            correctOption = '鹅'
            options = ['鹅', '鸡', '鸭', '雁']
          } else {
            correctOption = animalName
            options = [animalName, '鸡', '鸭', '鹅']
          }

          // 6. 打乱选项顺序
          options = GameDataManager.shuffleArray(options)

          // 7. 确保正确答案在选项中
          if (!options.includes(correctOption)) {
            options[options.length - 1] = correctOption
          }

          // 8. 创建题目对象
          questions.push({
            id: `${poem.id}_${animalName}`,
            type: 'poetry',
            question: `《${poem.title}》\n${poem.author ? `[${poem.dynasty}] ${poem.author}` : ''}\n\n${blankContent}`,
            answer: correctOption,
            options: options,
            hint: poem.explanation ? poem.explanation.substring(0, 50) + '...' : '这是一首咏物诗',
            animalType: poem.animalType
          })

          // 每首诗只生成一个题目
          break
        }
      }
    })

    // 9. 打乱题目顺序,并限制为8道题
    return GameDataManager.shuffleArray(questions).slice(0, 8)
  }

  /**
   * 打乱数组(Fisher-Yates 洗牌算法)
   */
  private static shuffleArray<T>(arr: T[]): T[] {
    const result: T[] = []
    // 复制数组
    for (let i = 0; i < arr.length; i++) {
      result.push(arr[i])
    }

    // 打乱顺序
    for (let i = result.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1))
      const temp = result[i]
      result[i] = result[j]
      result[j] = temp
    }
    return result
  }
}

💡 知识点:

  • substring() 截取字符串
  • includes() 检查字符串是否包含某个子串
  • indexOf() 查找子串位置
  • slice() 截取数组
  • Fisher-Yates 算法是经典的随机打乱算法

3.4 保存文件

Ctrl + S 保存。


第四步:创建游戏界面

这是最重要的一步,我们将创建游戏的用户界面。

4.1 创建文件

  1. 右键点击 entry/src/main/ets/pages/game 文件夹
  2. 选择 NewArkTS File
  3. 输入文件名:PoetryFillGame
  4. 点击确定

4.2 导入必要的模块

打开 PoetryFillGame.ets,首先导入需要的模块:

typescript 复制代码
/*
 * 古诗词填空游戏
 */

import { router } from '@kit.ArkUI'
import { GameDataManager, GameQuestion } from '../../data/GameDataManager'
import { promptAction } from '@kit.ArkUI'

4.3 创建组件和状态

添加组件定义和状态管理:

typescript 复制代码
@Entry
@Component
struct PoetryFillGame {
  // 状态变量
  @StorageProp('app_is_dark_mode') isDarkMode: boolean = false  // 是否深色模式
  @State questions: GameQuestion[] = []      // 题目列表
  @State currentIndex: number = 0            // 当前题目索引
  @State selectedAnswer: string = ''         // 用户选择的答案
  @State showHint: boolean = false           // 是否显示提示
  @State score: number = 0                   // 得分
  @State answered: boolean = false           // 是否已答题
  @State isCorrect: boolean = false          // 答案是否正确

  // 组件即将出现时调用
  aboutToAppear() {
    this.loadQuestions()
  }

  // 加载题目
  private loadQuestions() {
    this.questions = GameDataManager.getPoetryQuestions()
    console.info('PoetryFillGame', `加载了 ${this.questions.length} 道题目`)
  }

  // 获取当前题目
  private getCurrentQuestion(): GameQuestion | null {
    if (this.currentIndex < this.questions.length) {
      return this.questions[this.currentIndex]
    }
    return null
  }

  // 检查答案
  private checkAnswer(option: string) {
    if (this.answered) return  // 已经答过题了,不能再答

    this.selectedAnswer = option
    this.answered = true
    const question = this.getCurrentQuestion()

    if (question) {
      this.isCorrect = option === question.answer
      if (this.isCorrect) {
        this.score++  // 答对了,加分
      }
    }
  }

  // 下一题
  private nextQuestion() {
    if (this.currentIndex < this.questions.length - 1) {
      // 还有下一题
      this.currentIndex++
      this.selectedAnswer = ''
      this.answered = false
      this.showHint = false
    } else {
      // 已经是最后一题,显示结果
      this.showGameOver()
    }
  }

  // 显示游戏结束对话框
  private async showGameOver() {
    const totalQuestions = this.questions.length
    const percentage = Math.round((this.score / totalQuestions) * 100)

    promptAction.showDialog({
      title: '游戏结束',
      message: `你答对了 ${this.score}/${totalQuestions} 题\n正确率:${percentage}%`,
      buttons: [
        { text: '再玩一次', color: '#4CAF50' },
        { text: '返回', color: '#666666' }
      ]
    }).then((data) => {
      if (data.index === 0) {
        // 再玩一次
        this.currentIndex = 0
        this.score = 0
        this.selectedAnswer = ''
        this.answered = false
        this.showHint = false
        this.loadQuestions()
      } else {
        // 返回首页
        router.back()
      }
    })
  }

  // 构建界面(稍后填充)
  build() {
    Column() {
      Text('游戏界面')
    }
    .width('100%')
    .height('100%')
  }
}

💡 知识点:

  • @Entry 表示这是一个页面入口
  • @Component 表示这是一个组件
  • @State 表示状态变量,改变时会触发 UI 更新
  • aboutToAppear() 是生命周期函数,页面显示前调用

4.4 构建主界面

现在替换 build() 方法,构建完整界面:

typescript 复制代码
build() {
  Column() {
    // 1. 导航栏
    this.buildNavigationBar()

    // 2. 游戏内容
    if (this.questions.length > 0 && this.currentIndex < this.questions.length) {
      Scroll() {
        Column() {
          this.buildProgressIndicator()  // 进度指示器
          this.buildQuestionContent()    // 题目内容
          this.buildOptions()            // 选项按钮
          this.buildHintButton()         // 提示按钮

          if (this.showHint) {
            this.buildHintContent()      // 提示内容
          }

          if (this.answered) {
            this.buildNextButton()       // 下一题按钮
          }

          Row().height(40)  // 底部留白
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 16 })
      }
      .layoutWeight(1)
      .scrollBar(BarState.Auto)
    } else {
      // 加载中
      Column() {
        Text('加载题目中...')
          .fontSize(16)
          .fontColor(this.isDarkMode ? '#AAAAAA' : '#666666')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor(this.isDarkMode ? '#121212' : '#F5F5F5')
}

4.5 构建导航栏

build() 方法之后添加导航栏构建器:

typescript 复制代码
@Builder
buildNavigationBar() {
  Row() {
    // 返回按钮
    Row() {
      Text('←')
        .fontSize(24)
        .fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
    }
    .width(40)
    .height(40)
    .justifyContent(FlexAlign.Center)
    .onClick(() => {
      router.back()  // 返回上一页
    })

    // 标题
    Text('古诗词填空')
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
      .fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
      .layoutWeight(1)
      .textAlign(TextAlign.Center)

    // 得分
    Text(`${this.score}分`)
      .fontSize(16)
      .fontWeight(FontWeight.Medium)
      .fontColor('#95E1D3')
      .padding({ left: 12, right: 12 })
  }
  .width('100%')
  .height(56)
  .padding({ left: 8, right: 8 })
  .backgroundColor(this.isDarkMode ? '#1E1E1E' : '#FFFFFF')
}

4.6 构建进度指示器

typescript 复制代码
@Builder
buildProgressIndicator() {
  Column() {
    // 题目序号
    Row() {
      Text(`第 ${this.currentIndex + 1} / ${this.questions.length} 题`)
        .fontSize(14)
        .fontColor(this.isDarkMode ? '#AAAAAA' : '#666666')
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .margin({ bottom: 8 })

    // 进度条
    Stack() {
      // 背景条
      Row()
        .width('100%')
        .height(6)
        .backgroundColor(this.isDarkMode ? '#2D2D2D' : '#E0E0E0')
        .borderRadius(3)

      // 进度条
      Row()
        .width(`${((this.currentIndex + 1) / this.questions.length) * 100}%`)
        .height(6)
        .backgroundColor('#95E1D3')
        .borderRadius(3)
    }
    .width('100%')
    .height(6)
  }
  .width('100%')
  .margin({ bottom: 24 })
}

4.7 构建题目内容

typescript 复制代码
@Builder
buildQuestionContent() {
  if (this.currentIndex < this.questions.length) {
    Column() {
      // 提示文字
      Text('请填入合适的家禽名称')
        .fontSize(14)
        .fontColor('#95E1D3')
        .fontWeight(FontWeight.Medium)
        .margin({ bottom: 16 })

      // 题目内容
      Text(this.questions[this.currentIndex].question)
        .fontSize(18)
        .fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
        .lineHeight(32)
        .textAlign(TextAlign.Center)
    }
    .width('100%')
    .padding(24)
    .backgroundColor(this.isDarkMode ? '#1E1E1E' : '#FFFFFF')
    .borderRadius(16)
    .margin({ bottom: 24 })
  }
}

4.8 构建选项按钮(2x2 网格)

typescript 复制代码
@Builder
buildOptions() {
  if (this.currentIndex < this.questions.length &&
      this.questions[this.currentIndex].options &&
      this.questions[this.currentIndex].options!.length >= 4) {

    // 2x2 网格布局
    Column({ space: 12 }) {
      // 第一行
      Row({ space: 12 }) {
        this.buildOptionButton(this.questions[this.currentIndex].options![0])
        this.buildOptionButton(this.questions[this.currentIndex].options![1])
      }
      // 第二行
      Row({ space: 12 }) {
        this.buildOptionButton(this.questions[this.currentIndex].options![2])
        this.buildOptionButton(this.questions[this.currentIndex].options![3])
      }
    }
    .width('100%')
    .margin({ bottom: 16 })
  }
}

@Builder
buildOptionButton(option: string) {
  Row() {
    Text(option)
      .fontSize(24)
      .fontColor(this.getOptionTextColor(option))
      .fontWeight(FontWeight.Bold)
  }
  .layoutWeight(1)
  .height(80)
  .justifyContent(FlexAlign.Center)
  .backgroundColor(this.getOptionBackgroundColor(option))
  .borderRadius(12)
  .border({
    width: 2,
    color: this.getOptionBorderColor(option),
    style: BorderStyle.Solid
  })
  .onClick(() => {
    this.checkAnswer(option)
  })
}

// 获取选项文字颜色
private getOptionTextColor(option: string): string {
  const currentQuestion = this.getCurrentQuestion()
  const correctAnswer = currentQuestion?.answer || ''

  if (!this.answered) {
    // 未答题:默认颜色
    return this.isDarkMode ? '#FFFFFF' : '#212121'
  }
  if (option === correctAnswer) {
    // 正确答案:白色
    return '#FFFFFF'
  }
  if (option === this.selectedAnswer && !this.isCorrect) {
    // 错误选择:白色
    return '#FFFFFF'
  }
  // 其他选项:灰色
  return this.isDarkMode ? '#AAAAAA' : '#666666'
}

// 获取选项背景颜色
private getOptionBackgroundColor(option: string): string {
  const currentQuestion = this.getCurrentQuestion()
  const correctAnswer = currentQuestion?.answer || ''

  if (!this.answered) {
    // 未答题:白色背景
    return this.isDarkMode ? '#1E1E1E' : '#FFFFFF'
  }
  if (option === correctAnswer) {
    // 正确答案:绿色
    return '#4CAF50'
  }
  if (option === this.selectedAnswer && !this.isCorrect) {
    // 错误选择:红色
    return '#F44336'
  }
  return this.isDarkMode ? '#1E1E1E' : '#FFFFFF'
}

// 获取选项边框颜色
private getOptionBorderColor(option: string): string {
  const currentQuestion = this.getCurrentQuestion()
  const correctAnswer = currentQuestion?.answer || ''

  if (!this.answered) {
    return this.isDarkMode ? '#3D3D3D' : '#E0E0E0'
  }
  if (option === correctAnswer) {
    return '#4CAF50'
  }
  if (option === this.selectedAnswer && !this.isCorrect) {
    return '#F44336'
  }
  return this.isDarkMode ? '#3D3D3D' : '#E0E0E0'
}

4.9 构建提示按钮和内容

typescript 复制代码
@Builder
buildHintButton() {
  if (!this.answered) {
    Button('💡 查看提示')
      .width('100%')
      .height(48)
      .fontSize(16)
      .fontColor('#FFA726')
      .backgroundColor(this.isDarkMode ? '#2D2D2D' : '#FFF3E0')
      .borderRadius(12)
      .onClick(() => {
        this.showHint = !this.showHint  // 切换显示/隐藏
      })
      .margin({ bottom: 16 })
  }
}

@Builder
buildHintContent() {
  if (this.currentIndex < this.questions.length) {
    Column() {
      Row() {
        Text('💡')
          .fontSize(20)
          .margin({ right: 8 })

        Text('提示')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.isDarkMode ? '#FFFFFF' : '#212121')
      }
      .margin({ bottom: 12 })

      Text(this.questions[this.currentIndex].hint)
        .fontSize(14)
        .fontColor(this.isDarkMode ? '#CCCCCC' : '#555555')
        .lineHeight(22)
    }
    .width('100%')
    .padding(16)
    .backgroundColor(this.isDarkMode ? '#2D2D2D' : '#FFF3E0')
    .borderRadius(12)
    .margin({ bottom: 16 })
  }
}

4.10 构建下一题按钮

typescript 复制代码
@Builder
buildNextButton() {
  Button(this.currentIndex < this.questions.length - 1 ? '下一题 →' : '查看结果')
    .width('100%')
    .height(56)
    .fontSize(18)
    .fontWeight(FontWeight.Bold)
    .fontColor('#FFFFFF')
    .backgroundColor('#95E1D3')
    .borderRadius(12)
    .onClick(() => {
      this.nextQuestion()
    })
}

4.11 保存文件

Ctrl + S 保存。至此,游戏页面已经完成!


第五步:配置页面路由

5.1 打开路由配置文件

在项目中找到并打开:

复制代码
entry/src/main/resources/base/profile/main_pages.json

5.2 添加游戏页面路由

修改文件内容为:

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/game/PoetryFillGame"
  ]
}

💡 注意:

  • 第一个页面是应用首页
  • 路径不包含 .ets 后缀
  • 路径相对于 entry/src/main/ets/

5.3 保存文件

Ctrl + S 保存。


第六步:修改首页,添加游戏入口

6.1 打开首页文件

找到并打开:

复制代码
entry/src/main/ets/pages/Index.ets

6.2 替换全部内容

用以下代码替换文件的全部内容:

typescript 复制代码
import { router } from '@kit.ArkUI'

@Entry
@Component
struct Index {
  @State message: string = '古诗词填空游戏';

  build() {
    Column() {
      // 标题
      Text(this.message)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 40 })

      // 开始游戏按钮
      Button('开始游戏')
        .fontSize(20)
        .fontWeight(FontWeight.Medium)
        .width(200)
        .height(56)
        .backgroundColor('#95E1D3')
        .borderRadius(12)
        .onClick(() => {
          router.pushUrl({
            url: 'pages/game/PoetryFillGame'
          })
        })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#F5F5F5')
  }
}

6.3 保存文件

Ctrl + S 保存。


第七步:编译和运行

7.1 检查文件结构

确保你创建了以下文件:

复制代码
tiankong/
├── entry/src/main/ets/
│   ├── data/
│   │   ├── CultureData.ets          ✅ 已创建
│   │   └── GameDataManager.ets      ✅ 已创建
│   ├── pages/
│   │   ├── game/
│   │   │   └── PoetryFillGame.ets  ✅ 已创建
│   │   └── Index.ets                ✅ 已修改
│   └── ...
└── entry/src/main/resources/base/profile/
    └── main_pages.json               ✅ 已修改

7.2 编译项目

  1. 在 DevEco Studio 菜单栏,点击 BuildBuild Hap(s) / APP(s)Build Hap(s)
  2. 等待编译完成,查看底部的 Build 窗口

编译成功标志:

复制代码
BUILD SUCCESSFUL in 30s

常见错误:

错误 原因 解决方法
Cannot find module 导入路径错误 检查相对路径 ../../
Property does not exist 类型不匹配 检查接口定义
Page not found 路由未注册 检查 main_pages.json

7.3 运行应用

  1. 连接 HarmonyOS 设备或启动模拟器
  2. 点击 DevEco Studio 顶部的绿色运行按钮 ▶️
  3. 等待应用安装并启动

第八步:测试游戏

测试清单

✅ 测试 1:启动应用

  • 应显示首页,标题"古诗词填空游戏"
  • 应显示"开始游戏"按钮(青绿色)

✅ 测试 2:进入游戏

  • 点击"开始游戏"
  • 应跳转到游戏页面
  • 应显示第一道题目

✅ 测试 3:答题

  • 应显示诗词标题、作者、朝代
  • 应显示带填空的诗句(____
  • 应显示 4 个选项(2x2 网格)
  • 点击选项后:
    • 正确答案变绿色
    • 错误选择变红色,正确答案也变绿色
    • 显示"下一题"按钮

✅ 测试 4:提示功能

  • 答题前点击"💡 查看提示"
  • 应展开诗词解释
  • 再次点击可收起

✅ 测试 5:进度显示

  • 顶部应显示"第 X / 8 题"
  • 进度条应随题目推进
  • 右上角应显示当前得分

✅ 测试 6:游戏结束

  • 答完 8 道题后应弹出对话框
  • 显示得分和正确率
  • 点击"再玩一次"应重新开始
  • 点击"返回"应回到首页

第九步:添加更多诗词(可选)

目前我们只添加了 3 首诗词,你可以添加更多。

如何添加诗词

打开 CultureData.ets,在 getPoetryData() 方法的数组中添加:

typescript 复制代码
{
  id: 'poetry_goose_002',
  category: 'poetry',
  animalType: 'goose',
  title: '题鹅',
  author: '李商隐',
  dynasty: '唐',
  content: '眠沙卧水自成群,曲岸残阳极浦云。\n那解将心怜孔翠,羁雌长共故雄分。',
  explanation: '诗人借鹅的成群而居、夫妻相守来表达人间的情感和世态。',
  tags: ['李商隐', '咏物', '抒情'],
  isFavorite: false
},

添加规则:

  1. id 必须唯一
  2. content 中必须包含"鸡"、"鸭"或"鹅"
  3. \n 表示换行
  4. 多首诗之间用逗号分隔

完整代码参考

CultureData.ets 完整版(17首诗词)

由于篇幅限制,完整的 CultureData.ets(包含17首诗词)可以从以下位置获取:

方法一: 从源项目复制

复制代码
MyApplication8/entry/src/main/ets/data/CultureData.ets

方法二: 手动添加以下诗词到数组中
点击展开完整诗词列表(14首)

typescript 复制代码
// 第4首:题鹅
{
  id: 'poetry_goose_002',
  category: 'poetry',
  animalType: 'goose',
  title: '题鹅',
  author: '李商隐',
  dynasty: '唐',
  content: '眠沙卧水自成群,曲岸残阳极浦云。\n那解将心怜孔翠,羁雌长共故雄分。',
  explanation: '诗人借鹅的成群而居、夫妻相守来表达人间的情感和世态。',
  tags: ['李商隐', '咏物', '抒情'],
  isFavorite: false
},

// 第5首:舟前小鹅儿
{
  id: 'poetry_goose_003',
  category: 'poetry',
  animalType: 'goose',
  title: '舟前小鹅儿',
  author: '杜甫',
  dynasty: '唐',
  content: '鹅儿黄似酒,对酒爱新鹅。\n引颈嗔船逼,无行乱眼多。',
  explanation: '杜甫描写小鹅的可爱形象,充满童趣。',
  tags: ['杜甫', '咏物', '生动'],
  isFavorite: false
},

// 第6首:鹅赠鹤
{
  id: 'poetry_goose_004',
  category: 'poetry',
  animalType: 'goose',
  title: '鹅赠鹤',
  author: '白居易',
  dynasty: '唐',
  content: '君因风送入青云,我被人驱向鸭群。\n雪颈霜毛红网掌,请看何处不如君?',
  explanation: '白居易借鹅鹤对比,讽刺了重名位轻实际的社会现象。',
  tags: ['白居易', '寓言', '社会讽刺'],
  isFavorite: false
},

// 第7首:鸡
{
  id: 'poetry_chicken_002',
  category: 'poetry',
  animalType: 'chicken',
  title: '鸡',
  author: '崔道融',
  dynasty: '唐',
  content: '买得晨鸡共鸡语,常时不用等闲鸣。\n深山月黑风雨夜,欲近晓天啼一声。',
  explanation: '诗人描写了公鸡司晨的职责,表现了公鸡的尽职尽责。',
  tags: ['崔道融', '咏物', '职责'],
  isFavorite: false
},

// 第8首:春晓
{
  id: 'poetry_chicken_003',
  category: 'poetry',
  animalType: 'chicken',
  title: '春晓',
  author: '孟浩然',
  dynasty: '唐',
  content: '春眠不觉晓,处处闻啼鸟。\n夜来风雨声,花落知多少。',
  explanation: '这首诗虽未直接写鸡,但"春眠不觉晓"的"晓"正是由鸡鸣报晓而来。',
  tags: ['孟浩然', '春景', '经典名篇'],
  isFavorite: false
},

// 第9首:鸡鸣
{
  id: 'poetry_chicken_004',
  category: 'poetry',
  animalType: 'chicken',
  title: '鸡鸣',
  author: '诗经',
  dynasty: '先秦',
  content: '鸡既鸣矣,朝既盈矣。\n匪鸡则鸣,苍蝇之声。',
  explanation: '这是《诗经》中描写鸡鸣的诗句,通过鸡鸣来表现时间的推移。',
  tags: ['诗经', '古老', '朝政'],
  isFavorite: false
},

// 第10首:风入松·寄柯敬仲
{
  id: 'poetry_chicken_005',
  category: 'poetry',
  animalType: 'chicken',
  title: '风入松·寄柯敬仲',
  author: '虞集',
  dynasty: '元',
  content: '画堂红袖倚清酣,华发不胜簪。\n几回晚直金銮殿,东风软、花里停骖。\n书诏许传宫烛,香罗初试朝衫。\n御沟冰泮水挼蓝,飞燕语呢喃。\n重重帘幕卷轻寒,凭寄与、旧时相识。\n莫向春风笑我,花前须插红杉。',
  explanation: '这首词虽未直接写鸡,但"晓"指鸡鸣时分,充满了对往昔的怀念。',
  tags: ['虞集', '怀旧', '朝堂'],
  isFavorite: false
},

// 第11首:春江晚景
{
  id: 'poetry_duck_002',
  category: 'poetry',
  animalType: 'duck',
  title: '春江晚景',
  author: '张舜民',
  dynasty: '宋',
  content: '花映新林岸,云迎曲岛隈。\n江流添夜色,鸭宿鹭归来。',
  explanation: '诗人描绘春江晚景,一幅宁静和谐的春江晚景图。',
  tags: ['张舜民', '晚景', '和谐'],
  isFavorite: false
},

// 第12首:江上
{
  id: 'poetry_duck_003',
  category: 'poetry',
  animalType: 'duck',
  title: '江上',
  author: '王安石',
  dynasty: '宋',
  content: '江北秋阴一半开,晚云含雨却低徊。\n青山缭绕疑无路,忽见千帆隐映来。',
  explanation: '虽然此诗未直接写鸭,但江上秋景常有群鸭嬉戏,富有诗意。',
  tags: ['王安石', '江景', '秋色'],
  isFavorite: false
},

// 第13首:春日
{
  id: 'poetry_duck_004',
  category: 'poetry',
  animalType: 'duck',
  title: '春日',
  author: '朱熹',
  dynasty: '宋',
  content: '胜日寻芳泗水滨,无边光景一时新。\n等闲识得东风面,万紫千红总是春。',
  explanation: '诗人在春天的美好日子里到泗水边寻找春天的气息,春江水暖,鸭子嬉戏。',
  tags: ['朱熹', '春景', '哲理'],
  isFavorite: false
},

// 第14首:游园不值
{
  id: 'poetry_duck_005',
  category: 'poetry',
  animalType: 'duck',
  title: '游园不值',
  author: '叶绍翁',
  dynasty: '宋',
  content: '应怜屐齿印苍苔,小扣柴扉久不开。\n春色满园关不住,一枝红杏出墙来。',
  explanation: '"春色满园关不住,一枝红杏出墙来"表达了春天的勃勃生机,园中池塘里必有鸭子嬉戏。',
  tags: ['叶绍翁', '春游', '名句'],
  isFavorite: false
}

常见问题解答

Q1:编译时提示"找不到模块"?

A: 检查导入路径:

typescript 复制代码
// 正确:相对路径
import { CultureDatabase } from './CultureData'
import { GameDataManager } from '../../data/GameDataManager'

// 错误:绝对路径(不要用)
import { CultureDatabase } from 'entry/src/main/ets/data/CultureData'

Q2:运行时提示"页面未找到"?

A: 检查 main_pages.json 是否正确添加了路由:

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/game/PoetryFillGame"  // 确保这行存在
  ]
}

Q3:点击按钮没有反应?

A: 检查 onClick 事件是否正确绑定:

typescript 复制代码
.onClick(() => {
  this.checkAnswer(option)  // 确保方法名正确
})

Q4:题目没有显示?

A:

  1. 检查 CultureData.ets 中是否添加了诗词数据
  2. 打开 DevTools 查看控制台,看是否有错误信息
  3. 确认 aboutToAppear() 方法被调用

Q5:如何调试?

A: 使用 console.info() 输出调试信息:

typescript 复制代码
console.info('PoetryFillGame', `题目数量: ${this.questions.length}`)
console.info('PoetryFillGame', `当前答案: ${question.answer}`)

然后在 DevEco Studio 的 DevTools 中查看 Console 面板。


进阶扩展

扩展 1:添加音效

在答题正确/错误时播放音效:

typescript 复制代码
import { media } from '@kit.MediaKit'

// 答题时
if (this.isCorrect) {
  // 播放正确音效
  media.createSoundPool().play(correctSoundId)
} else {
  // 播放错误音效
  media.createSoundPool().play(wrongSoundId)
}

扩展 2:添加动画效果

为按钮添加点击动画:

typescript 复制代码
Button('开始游戏')
  .animation({
    duration: 300,
    curve: Curve.EaseInOut
  })
  .scale(this.buttonPressed ? 0.95 : 1.0)

扩展 3:保存最高分

使用 Preferences 保存历史最高分:

typescript 复制代码
import { preferences } from '@kit.ArkData'

// 保存最高分
const prefs = await preferences.getPreferences(getContext(), 'game_data')
await prefs.put('high_score', this.score)

// 读取最高分
const highScore = await prefs.get('high_score', 0)

扩展 4:添加难度选择

提供简单、中等、困难三种模式:

typescript 复制代码
@State difficulty: 'easy' | 'medium' | 'hard' = 'medium'

// 根据难度调整题目数量
private getQuestionCount(): number {
  switch (this.difficulty) {
    case 'easy': return 5
    case 'medium': return 8
    case 'hard': return 12
  }
}

总结

恭喜你!你已经完成了一个完整的诗词填空游戏。通过本教程,你学会了:

数据管理

  • 定义数据结构(interface、type)
  • 创建数据库类
  • 使用静态方法提供数据

游戏逻辑

  • 题目生成算法
  • 答案验证
  • 分数统计
  • 随机打乱算法

UI 开发

  • ArkUI 组件使用
  • 状态管理(@State)
  • 条件渲染
  • 事件处理

页面导航

  • 路由配置
  • 页面跳转
  • 页面返回

下一步学习

  • 学习更多 ArkUI 组件
  • 探索动画和过渡效果
  • 学习数据持久化(Preferences、数据库)
  • 尝试网络请求获取诗词数据

项目文件总结

文件 行数 作用
CultureData.ets ~500 诗词数据库
GameDataManager.ets ~140 游戏数据管理
PoetryFillGame.ets ~380 游戏界面
Index.ets ~30 首页
main_pages.json ~6 路由配置

总计: 约 1050+ 行代码


附录

颜色参考

用途 颜色代码 示例
主题色 #95E1D3 青绿色
正确 #4CAF50 绿色
错误 #F44336 红色
提示 #FFA726 橙色
背景 #F5F5F5 浅灰

快速命令

bash 复制代码
# 创建目录
mkdir -p entry/src/main/ets/data
mkdir -p entry/src/main/ets/pages/game

# 查看文件
find entry/src/main/ets -name "*.ets"

# 查看行数
wc -l entry/src/main/ets/data/CultureData.ets

参考文档


本教程完成!

如有问题,欢迎查阅 HarmonyOS 官方文档或在社区提问。

祝你开发愉快!🎉

https://developer.huawei.com/consumer/cn/training/classDetail/fd34ff9286174e848d34cde7f512ce22?type=1%3Fha_source%3Dhmosclass\&ha_sourceId=89000248

相关推荐
爱笑的眼睛112 小时前
HarmonyOS应用启动优化:深入技巧与最佳实践
华为·harmonyos
不叫猫先生3 小时前
基于华为昇腾CANN的自定义算子开发
华为·语言模型·大模型·cann
Android疑难杂症6 小时前
一文讲透鸿蒙开发应用框架体系
前端·harmonyos
mm-q29152227297 小时前
张云波ArkUI双范式超级实战鸿蒙社区App第一季课程分享
华为·harmonyos
一只小风华~7 小时前
HarmonyOS:相对布局(RelativeContainer)
深度学习·华为·harmonyos·鸿蒙
国服第二切图仔7 小时前
鸿蒙Next开发中三方库使用指南之privacy_dialog集成示例
华为·harmonyos
一只小风华~7 小时前
HarmonyOS:线性布局(Row/Column)
华为·harmonyos·鸿蒙
国服第二切图仔8 小时前
鸿蒙 Next 如何使用 AVRecorder 从0到1实现视频录制功能(ArkTS)
华为·音视频·harmonyos
全球通史10 小时前
鸿蒙开发之鸿蒙应用深色模式适配完整指南(上架过程之适配手机深色模式)
华为·harmonyos