应用实例六:百分数与分数互化器

知识点:理解百分数的意义,掌握分数、小数、百分数之间的互化。
功能:一个三联动的转换器。输入分数,自动转换为小数和百分数;或者输入百分数,自动转换为分数和小数。图形化显示百分数的含义(如“80%”显示为100个格子填充了80个)。
在这里插入图片描述

// FractionDecimalPercentConverter.ets

interface ConverterState {
  fraction: string
  decimal: string
  percent: string
  isValid: boolean
  errorMessage: string
}

interface ConversionResult {
  fraction: string
  decimal: string
  percent: string
}

interface FractionParts {
  numerator: number
  denominator: number
}

@Entry
@Component
struct FractionDecimalPercentConverter {
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

  @State private state: ConverterState = {
    fraction: '',
    decimal: '',
    percent: '',
    isValid: true,
    errorMessage: ''
  }
  @State private activeInput: 'fraction' | 'decimal' | 'percent' | '' = ''

  build() {
    Column({ space: 12 }) {
      Text('🔄 分数-小数-百分数转换器')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor('#2C3E50')

      if (!this.state.isValid) {
        Text(this.state.errorMessage)
          .fontSize(12)
          .fontColor('#E74C3C')
          .padding(8)
          .backgroundColor('#FADBD8')
          .borderRadius(8)
          .width('95%')
      }

      Column({ space: 12 }) {
        this.InputField('分数', this.state.fraction, 'fraction')
        this.InputField('小数', this.state.decimal, 'decimal')
        this.InputField('百分数', this.state.percent, 'percent')
      }
      .width('95%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius(8)

      Canvas(this.context)
        .width(340)
        .height(120)
        .backgroundColor('#F8F9FA')
        .borderRadius(8)
        .shadow({ radius: 3, color: '#00000010' })
        .onReady(() => {
          this.drawPercentageGrid()
        })

      Column() {
        Text('💡 转换原理')
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .fontColor('#2C3E50')

        Text('• 分数 → 小数:分子 ÷ 分母')
          .fontSize(11)
          .fontColor('#7F8C8D')
          .margin({ top: 4 })

        Text('• 小数 → 百分数:小数 × 100%')
          .fontSize(11)
          .fontColor('#7F8C8D')
          .margin({ top: 2 })

        Text('• 百分数 → 分数:百分数 ÷ 100,约分')
          .fontSize(11)
          .fontColor('#7F8C8D')
          .margin({ top: 2 })

        Text('• 百分数网格:100个格子代表100%,填充部分代表具体百分比')
          .fontSize(11)
          .fontColor('#3498DB')
          .margin({ top: 2 })
      }
      .width('95%')
      .padding(12)
      .backgroundColor('#EBF5FB')
      .borderRadius(8)
      .alignItems(HorizontalAlign.Start)

      Row({ space: 12 }) {
        Button('🔄 转换')
          .fontSize(12)
          .height(36)
          .width('30%')
          .backgroundColor('#3498DB')
          .onClick(() => this.convertAll())

        Button('🗑️ 清空')
          .fontSize(12)
          .height(36)
          .width('30%')
          .backgroundColor('#95A5A6')
          .onClick(() => this.clearAll())
      }
      .width('95%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F0F3F6')
    .padding(12)
  }

  @Builder
  InputField(label: string, value: string, type: 'fraction' | 'decimal' | 'percent') {
    Row({ space: 8 }) {
      Text(label)
        .fontSize(12)
        .fontColor('#2C3E50')
        .width(50)

      TextInput({
        placeholder: `输入${label}`,
        text: value
      })
        .width('100%')
        .height(44)
        .backgroundColor('#F8F9FA')
        .borderRadius(6)
        .padding({ left: 12, right: 12 })
        .onFocus(() => {
          this.activeInput = type
        })
        .onBlur(() => {
          if (this.activeInput === type) {
            if (type === 'fraction') {
              this.processInput(type, this.state.fraction)
            } else if (type === 'decimal') {
              this.processInput(type, this.state.decimal)
            } else {
              this.processInput(type, this.state.percent)
            }
          }
        })
        .onChange((newValue: string) => {
          if (this.activeInput === type) {
            this.updateState(type, newValue)
          }
        })
        .onSubmit(() => {
          if (type === 'fraction') {
            this.processInput(type, this.state.fraction)
          } else if (type === 'decimal') {
            this.processInput(type, this.state.decimal)
          } else {
            this.processInput(type, this.state.percent)
          }
        })
    }
  }

  private updateState(type: 'fraction' | 'decimal' | 'percent', value: string) {
    let newState: ConverterState
    
    if (type === 'fraction') {
      newState = {
        fraction: value,
        decimal: this.state.decimal,
        percent: this.state.percent,
        isValid: this.state.isValid,
        errorMessage: this.state.errorMessage
      }
    } else if (type === 'decimal') {
      newState = {
        fraction: this.state.fraction,
        decimal: value,
        percent: this.state.percent,
        isValid: this.state.isValid,
        errorMessage: this.state.errorMessage
      }
    } else {
      newState = {
        fraction: this.state.fraction,
        decimal: this.state.decimal,
        percent: value,
        isValid: this.state.isValid,
        errorMessage: this.state.errorMessage
      }
    }
    
    this.state = newState
  }

  private processInput(type: 'fraction' | 'decimal' | 'percent', value: string) {
    try {
      let result: ConversionResult

      if (type === 'fraction') {
        result = this.fractionToOthers(value)
      } else if (type === 'decimal') {
        result = this.decimalToOthers(value)
      } else {
        result = this.percentToOthers(value)
      }

      this.state = {
        fraction: result.fraction,
        decimal: result.decimal,
        percent: result.percent,
        isValid: true,
        errorMessage: ''
      } as ConverterState
      this.drawPercentageGrid()
    } catch (error) {
      this.state = {
        fraction: this.state.fraction,
        decimal: this.state.decimal,
        percent: this.state.percent,
        isValid: false,
        errorMessage: error.message
      } as ConverterState
    }
  }

  private fractionToOthers(fraction: string): ConversionResult {
    if (!fraction) {
      return { fraction: '', decimal: '', percent: '' }
    }

    const match = fraction.match(/^(\d+)(?:\/(\d+))?$/)
    if (!match) {
      throw new Error('请输入正确的分数格式,如 3/4')
    }

    const numerator = parseInt(match[1])
    const denominator = match[2] ? parseInt(match[2]) : 1

    if (denominator === 0) {
      throw new Error('分母不能为0')
    }

    const decimal = (numerator / denominator).toFixed(4)
    const percent = (parseFloat(decimal) * 100).toFixed(2) + '%'

    return {
      fraction: this.simplifyFraction(numerator, denominator),
      decimal: decimal.replace(/\.?0+$/, ''),
      percent: percent
    }
  }

  private decimalToOthers(decimal: string): ConversionResult {
    if (!decimal) {
      return { fraction: '', decimal: '', percent: '' }
    }

    const value = parseFloat(decimal)
    if (isNaN(value)) {
      throw new Error('请输入正确的小数')
    }

    const percent = (value * 100).toFixed(2) + '%'
    const fractionParts = this.decimalToFraction(value)
    const fraction = this.simplifyFraction(fractionParts.numerator, fractionParts.denominator)

    return {
      fraction: fraction,
      decimal: decimal,
      percent: percent
    }
  }

  private percentToOthers(percent: string): ConversionResult {
    if (!percent) {
      return { fraction: '', decimal: '', percent: '' }
    }

    const value = parseFloat(percent.replace('%', ''))
    if (isNaN(value)) {
      throw new Error('请输入正确的百分数')
    }

    const decimal = (value / 100).toFixed(4).replace(/\.?0+$/, '')
    const fractionParts = this.decimalToFraction(value / 100)
    const fraction = this.simplifyFraction(fractionParts.numerator, fractionParts.denominator)

    return {
      fraction: fraction,
      decimal: decimal,
      percent: value.toFixed(2) + '%'
    }
  }

  private decimalToFraction(decimal: number): FractionParts {
    let numerator = decimal
    let denominator = 1

    while (numerator % 1 !== 0) {
      numerator *= 10
      denominator *= 10
    }

    return { numerator: Math.round(numerator), denominator: denominator }
  }

  private simplifyFraction(numerator: number, denominator: number): string {
    const gcd = this.gcd(numerator, denominator)
    const simplifiedNum = numerator / gcd
    const simplifiedDen = denominator / gcd

    if (simplifiedDen === 1) {
      return simplifiedNum.toString()
    }

    return `${simplifiedNum}/${simplifiedDen}`
  }

  private gcd(a: number, b: number): number {
    return b === 0 ? a : this.gcd(b, a % b)
  }

  private clearAll(): void {
    this.state = {
      fraction: '',
      decimal: '',
      percent: '',
      isValid: true,
      errorMessage: ''
    }
    this.activeInput = ''
    this.drawPercentageGrid()
  }

  private convertAll(): void {
    if (this.activeInput === 'fraction' && this.state.fraction) {
      this.processInput('fraction', this.state.fraction)
    } else if (this.activeInput === 'decimal' && this.state.decimal) {
      this.processInput('decimal', this.state.decimal)
    } else if (this.activeInput === 'percent' && this.state.percent) {
      this.processInput('percent', this.state.percent)
    } else if (this.state.fraction) {
      this.processInput('fraction', this.state.fraction)
    } else if (this.state.decimal) {
      this.processInput('decimal', this.state.decimal)
    } else if (this.state.percent) {
      this.processInput('percent', this.state.percent)
    }
  }

  private drawPercentageGrid(): void {
    const ctx = this.context
    const w = 340
    const h = 120

    ctx.clearRect(0, 0, w, h)
    ctx.fillStyle = '#F8F9FA'
    ctx.fillRect(0, 0, w, h)

    const gridSize = 10
    const cellSize = 10
    const startX = 20
    const startY = 20

    let percent = 0
    const percentMatch = this.state.percent.match(/^(\d+(?:\.\d+)?)%$/)
    if (percentMatch) {
      percent = parseFloat(percentMatch[1])
    }

    const filledCells = Math.min(Math.round(percent), 100)

    ctx.strokeStyle = '#E0E0E0'
    ctx.lineWidth = 1

    for (let i = 0; i < gridSize; i++) {
      for (let j = 0; j < gridSize; j++) {
        const x = startX + j * cellSize
        const y = startY + i * cellSize
        
        const cellIndex = i * gridSize + j
        if (cellIndex < filledCells) {
          ctx.fillStyle = '#3498DB'
          ctx.fillRect(x, y, cellSize, cellSize)
        }
        
        ctx.strokeRect(x, y, cellSize, cellSize)
      }
    }

    ctx.fillStyle = '#2C3E50'
    ctx.font = '12px sans-serif'
    ctx.textAlign = 'center'
    ctx.fillText(`${percent}%`, startX + gridSize * cellSize / 2, startY + gridSize * cellSize + 25)

    ctx.fillStyle = '#7F8C8D'
    ctx.font = '10px sans-serif'
    ctx.fillText('100个格子 = 100%', startX + gridSize * cellSize / 2, startY + gridSize * cellSize + 40)
  }
}
Logo

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

更多推荐