应用实例四:分数加减法训练器

知识点:掌握同分母分数加减法,理解异分母分数加减法的算理(通分)。
功能:系统随机生成分数加减法题目。对于同分母题目,直接输入结果;对于异分母题目,应用会分步引导:先提示寻找公分母,再展示通分过程,最后计算结果。设有计时和评分功能,适合课堂练习。
在这里插入图片描述

// FractionAddSubPractice.ets
interface Fraction {
  numerator: number
  denominator: number
}

interface Question {
  num1: Fraction
  num2: Fraction
  operator: string
  isSameDenominator: boolean
  answer: Fraction
  steps: StepInfo[]
}

interface StepInfo {
  title: string
  content: string
  type: string
}

@Entry
@Component
struct FractionAddSubPractice {
  @State currentQuestion: Question | null = null
  @State userAnswer: string = ''
  @State currentStep: number = 0
  @State showSteps: boolean = false
  @State score: number = 0
  @State totalQuestions: number = 0
  @State correctCount: number = 0
  @State timeElapsed: number = 0
  @State isTimerRunning: boolean = false
  @State showResult: boolean = false
  @State isCorrect: boolean = false
  @State practiceMode: string = 'practice'
  @State difficulty: string = 'mixed'
  private timer: number = -1

  aboutToAppear(): void {
    this.generateNewQuestion()
  }

  aboutToDisappear(): void {
    if (this.timer !== -1) {
      clearInterval(this.timer)
    }
  }

  build() {
    Column({
      space: 15
    }) {
      Row() {
        Text('分数加减法练习')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#2C3E50')
          .layoutWeight(1)

        Text(`⏱️ ${this.formatTime(this.timeElapsed)}`)
          .fontSize(16)
          .fontColor('#E74C3C')
      }
      .width('95%')

      Row({
        space: 15
      }) {
        Column() {
          Text('得分')
            .fontSize(12)
            .fontColor('#7F8C8D')
          Text(`${this.score}`)
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#27AE60')
        }
        .layoutWeight(1)
        .padding(10)
        .backgroundColor('#E8F5E9')
        .borderRadius(8)

        Column() {
          Text('正确率')
            .fontSize(12)
            .fontColor('#7F8C8D')
          Text(`${this.totalQuestions > 0 ? Math.round(this.correctCount / this.totalQuestions * 100) : 0}%`)
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#3498DB')
        }
        .layoutWeight(1)
        .padding(10)
        .backgroundColor('#E3F2FD')
        .borderRadius(8)

        Column() {
          Text('题数')
            .fontSize(12)
            .fontColor('#7F8C8D')
          Text(`${this.totalQuestions}`)
            .fontSize(24)
            .fontWeight(FontWeight.Bold)
            .fontColor('#9B59B6')
        }
        .layoutWeight(1)
        .padding(10)
        .backgroundColor('#F3E5F5')
        .borderRadius(8)
      }
      .width('95%')

      if (this.currentQuestion) {
        Column() {
          Text('当前题目')
            .fontSize(14)
            .fontColor('#7F8C8D')
            .margin({ bottom: 10 })

          Row({
            space: 10
          }) {
            this.FractionDisplay(this.currentQuestion.num1)
            Text(this.currentQuestion.operator)
              .fontSize(32)
              .fontWeight(FontWeight.Bold)
              .fontColor('#2C3E50')
            this.FractionDisplay(this.currentQuestion.num2)
            Text('=')
              .fontSize(32)
              .fontWeight(FontWeight.Bold)
              .fontColor('#2C3E50')
            Text('?')
              .fontSize(32)
              .fontWeight(FontWeight.Bold)
              .fontColor('#E74C3C')
          }
          .justifyContent(FlexAlign.Center)

          if (this.currentQuestion.isSameDenominator) {
            Text('💡 同分母分数,直接计算')
              .fontSize(14)
              .fontColor('#27AE60')
              .margin({ top: 10 })
          } else {
            Text('💡 异分母分数,需要通分')
              .fontSize(14)
              .fontColor('#E74C3C')
              .margin({ top: 10 })
          }
        }
        .width('95%')
        .padding(20)
        .backgroundColor('#FFF9C4')
        .borderRadius(12)

        if (!this.showSteps && !this.showResult) {
          Column() {
            Text('输入答案(格式:分子/分母)')
              .fontSize(14)
              .fontColor('#7F8C8D')
              .margin({ bottom: 10 })

            TextInput({ text: this.userAnswer, placeholder: '例如:3/4' })
              .width('80%')
              .height(50)
              .fontSize(20)
              .textAlign(TextAlign.Center)
              .backgroundColor('#FFFFFF')
              .onChange((value: string) => {
                this.userAnswer = value
              })

            Row({
              space: 15
            }) {
              Button('提交答案')
                .fontSize(16)
                .height(44)
                .backgroundColor('#3498DB')
                .onClick(() => this.checkAnswer())

              if (!this.currentQuestion.isSameDenominator) {
                Button('查看步骤')
                  .fontSize(16)
                  .height(44)
                  .backgroundColor('#9B59B6')
                  .onClick(() => this.showStepByStep())
              }
            }
            .margin({ top: 15 })
          }
          .width('95%')
          .padding(15)
          .backgroundColor('#F5F5F5')
          .borderRadius(12)
        }

        if (this.showSteps && this.currentQuestion.steps.length > 0) {
          Column() {
            Text('📝 解题步骤')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#2C3E50')
              .margin({ bottom: 15 })

            ForEach(this.currentQuestion.steps.slice(0, this.currentStep + 1), (step: StepInfo, index: number) => {
              Column() {
                Text(`步骤 ${index + 1}: ${step.title}`)
                  .fontSize(14)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#3498DB')
                Text(step.content)
                  .fontSize(13)
                  .fontColor('#2C3E50')
                  .margin({ top: 5 })
              }
              .width('100%')
              .padding(10)
              .backgroundColor('#E3F2FD')
              .borderRadius(8)
              .margin({ bottom: 10 })
              .alignItems(HorizontalAlign.Start)
            })

            if (this.currentStep < this.currentQuestion.steps.length - 1) {
              Button('下一步')
                .fontSize(14)
                .height(40)
                .backgroundColor('#27AE60')
                .onClick(() => this.nextStep())
            } else {
              Column() {
                Text('现在你知道怎么做了,请输入答案:')
                  .fontSize(14)
                  .fontColor('#7F8C8D')
                  .margin({ bottom: 10 })

                TextInput({ text: this.userAnswer, placeholder: '例如:3/4' })
                  .width('80%')
                  .height(44)
                  .fontSize(18)
                  .textAlign(TextAlign.Center)
                  .backgroundColor('#FFFFFF')
                  .onChange((value: string) => {
                    this.userAnswer = value
                  })

                Button('提交答案')
                  .fontSize(14)
                  .height(40)
                  .backgroundColor('#3498DB')
                  .margin({ top: 10 })
                  .onClick(() => this.checkAnswer())
              }
              .width('100%')
              .padding(10)
              .backgroundColor('#E8F5E9')
              .borderRadius(8)
            }
          }
          .width('95%')
          .padding(15)
          .backgroundColor('#F5F5F5')
          .borderRadius(12)
          .alignItems(HorizontalAlign.Start)
        }

        if (this.showResult) {
          Column() {
            if (this.isCorrect) {
              Text('✅ 回答正确!')
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor('#27AE60')
            } else {
              Column() {
                Text('❌ 回答错误')
                  .fontSize(20)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#E74C3C')
                Text(`正确答案: ${this.currentQuestion.answer.numerator}/${this.currentQuestion.answer.denominator}`)
                  .fontSize(16)
                  .fontColor('#2C3E50')
                  .margin({ top: 10 })
              }
            }

            Button('下一题')
              .fontSize(16)
              .height(44)
              .backgroundColor('#27AE60')
              .margin({ top: 15 })
              .onClick(() => this.nextQuestion())
          }
          .width('95%')
          .padding(20)
          .backgroundColor(this.isCorrect ? '#E8F5E9' : '#FFEBEE')
          .borderRadius(12)
        }
      }

      Column() {
        Text('📖 学习提示')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#2C3E50')
        Text('同分母分数相加减:分母不变,分子相加减')
          .fontSize(12)
          .fontColor('#7F8C8D')
          .margin({ top: 5 })
        Text('异分母分数相加减:先通分,再按同分母计算')
          .fontSize(12)
          .fontColor('#7F8C8D')
          .margin({ top: 3 })
      }
      .width('90%')
      .padding(12)
      .backgroundColor('#FFF9C4')
      .borderRadius(8)
      .alignItems(HorizontalAlign.Start)
      .margin({ top: 10 })
    }
    .width('100%')
    .height('100%')
    .padding(15)
  }

  @Builder
  FractionDisplay(frac: Fraction) {
    Column() {
      Text(`${frac.numerator}`)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#2C3E50')
      Divider()
        .width(40)
        .color('#2C3E50')
        .strokeWidth(2)
      Text(`${frac.denominator}`)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#2C3E50')
    }
    .width(50)
  }

  private formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60)
    const secs = seconds % 60
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
  }

  private startTimer(): void {
    if (this.timer !== -1) {
      clearInterval(this.timer)
    }
    this.isTimerRunning = true
    this.timer = setInterval(() => {
      this.timeElapsed++
    }, 1000)
  }

  private generateNewQuestion(): void {
    const isAddition = Math.random() > 0.5
    const operator = isAddition ? '+' : '-'

    let num1: Fraction
    let num2: Fraction
    let isSameDenominator: boolean

    if (this.difficulty === 'same') {
      isSameDenominator = true
    } else if (this.difficulty === 'different') {
      isSameDenominator = false
    } else {
      isSameDenominator = Math.random() > 0.5
    }

    if (isSameDenominator) {
      const denom = this.randomInt(2, 10)
      let num1Num = this.randomInt(1, denom - 1)
      let num2Num = this.randomInt(1, denom - 1)

      if (!isAddition && num1Num < num2Num) {
        const temp = num1Num
        num1Num = num2Num
        num2Num = temp
      }

      num1 = { numerator: num1Num, denominator: denom }
      num2 = { numerator: num2Num, denominator: denom }
    } else {
      const denom1 = this.randomInt(2, 8)
      const denom2 = this.randomInt(2, 8)

      let validDenom2 = denom2
      while (validDenom2 === denom1 || this.gcd(denom1, validDenom2) === denom1 || this.gcd(denom1, validDenom2) === validDenom2) {
        validDenom2 = this.randomInt(2, 8)
      }

      const num1Num = this.randomInt(1, denom1 - 1)
      let num2Num = this.randomInt(1, validDenom2 - 1)

      num1 = { numerator: num1Num, denominator: denom1 }
      num2 = { numerator: num2Num, denominator: validDenom2 }

      if (!isAddition) {
        const result = this.calculateAnswer(num1, num2, operator)
        if (result.numerator < 0) {
          const temp = num1
          num1 = num2
          num2 = temp
        }
      }
    }

    const answer = this.calculateAnswer(num1, num2, operator)
    const steps = this.generateSteps(num1, num2, operator, isSameDenominator)

    this.currentQuestion = {
      num1: num1,
      num2: num2,
      operator: operator,
      isSameDenominator: isSameDenominator,
      answer: answer,
      steps: steps
    }

    this.userAnswer = ''
    this.showSteps = false
    this.showResult = false
    this.currentStep = 0

    if (!this.isTimerRunning) {
      this.startTimer()
    }
  }

  private randomInt(min: number, max: number): number {
    return Math.floor(Math.random() * (max - min + 1)) + min
  }

  private gcd(a: number, b: number): number {
    a = Math.abs(a)
    b = Math.abs(b)
    while (b !== 0) {
      const temp = b
      b = a % b
      a = temp
    }
    return a
  }

  private lcm(a: number, b: number): number {
    return Math.abs(a * b) / this.gcd(a, b)
  }

  private calculateAnswer(num1: Fraction, num2: Fraction, operator: string): Fraction {
    const commonDenom = this.lcm(num1.denominator, num2.denominator)
    const factor1 = commonDenom / num1.denominator
    const factor2 = commonDenom / num2.denominator

    let resultNum: number
    if (operator === '+') {
      resultNum = num1.numerator * factor1 + num2.numerator * factor2
    } else {
      resultNum = num1.numerator * factor1 - num2.numerator * factor2
    }

    const divisor = this.gcd(Math.abs(resultNum), commonDenom)
    return {
      numerator: resultNum / divisor,
      denominator: commonDenom / divisor
    }
  }

  private generateSteps(num1: Fraction, num2: Fraction, operator: string, isSameDenominator: boolean): StepInfo[] {
    const steps: StepInfo[] = []

    if (isSameDenominator) {
      let resultNum: number
      if (operator === '+') {
        resultNum = num1.numerator + num2.numerator
        steps.push({
          title: '同分母加法',
          content: `分母不变,分子相加:${num1.numerator} + ${num2.numerator} = ${resultNum}`,
          type: 'calculate'
        })
      } else {
        resultNum = num1.numerator - num2.numerator
        steps.push({
          title: '同分母减法',
          content: `分母不变,分子相减:${num1.numerator} - ${num2.numerator} = ${resultNum}`,
          type: 'calculate'
        })
      }

      const divisor = this.gcd(Math.abs(resultNum), num1.denominator)
      if (divisor > 1) {
        steps.push({
          title: '约分',
          content: `分子分母同时除以${divisor}${resultNum}/${num1.denominator} = ${resultNum / divisor}/${num1.denominator / divisor}`,
          type: 'simplify'
        })
      }
    } else {
      const commonDenom = this.lcm(num1.denominator, num2.denominator)
      steps.push({
        title: '找公分母',
        content: `${num1.denominator}${num2.denominator}的最小公倍数是${commonDenom}`,
        type: 'find_lcm'
      })

      const factor1 = commonDenom / num1.denominator
      const factor2 = commonDenom / num2.denominator

      steps.push({
        title: '通分',
        content: `${num1.numerator}/${num1.denominator} = ${num1.numerator * factor1}/${commonDenom}\n${num2.numerator}/${num2.denominator} = ${num2.numerator * factor2}/${commonDenom}`,
        type: 'convert'
      })

      let resultNum: number
      if (operator === '+') {
        resultNum = num1.numerator * factor1 + num2.numerator * factor2
        steps.push({
          title: '计算',
          content: `${num1.numerator * factor1} + ${num2.numerator * factor2} = ${resultNum}`,
          type: 'calculate'
        })
      } else {
        resultNum = num1.numerator * factor1 - num2.numerator * factor2
        steps.push({
          title: '计算',
          content: `${num1.numerator * factor1} - ${num2.numerator * factor2} = ${resultNum}`,
          type: 'calculate'
        })
      }

      const divisor = this.gcd(Math.abs(resultNum), commonDenom)
      if (divisor > 1) {
        steps.push({
          title: '约分',
          content: `分子分母同时除以${divisor}${resultNum}/${commonDenom} = ${resultNum / divisor}/${commonDenom / divisor}`,
          type: 'simplify'
        })
      }
    }

    return steps
  }

  private showStepByStep(): void {
    this.showSteps = true
    this.currentStep = 0
  }

  private nextStep(): void {
    if (this.currentStep < this.currentQuestion!.steps.length - 1) {
      this.currentStep++
    }
  }

  private checkAnswer(): void {
    if (!this.userAnswer.trim()) {
      return
    }

    const parts = this.userAnswer.split('/')
    if (parts.length !== 2) {
      return
    }

    const userNum = parseInt(parts[0])
    const userDenom = parseInt(parts[1])

    if (isNaN(userNum) || isNaN(userDenom) || userDenom === 0) {
      return
    }

    const divisor = this.gcd(Math.abs(userNum), userDenom)
    const simplifiedNum = userNum / divisor
    const simplifiedDenom = userDenom / divisor

    this.isCorrect = simplifiedNum === this.currentQuestion!.answer.numerator &&
      simplifiedDenom === this.currentQuestion!.answer.denominator

    this.totalQuestions++
    if (this.isCorrect) {
      this.correctCount++
      this.score += this.currentQuestion!.isSameDenominator ? 10 : 20
    }

    this.showResult = true
  }

  private nextQuestion(): void {
    this.generateNewQuestion()
  }
}
Logo

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

更多推荐