#星光不负 码向未来# 计算器开发 原创

鸿蒙小白001
发布于 2025-10-24 10:16
浏览
0收藏

设计思路

  • 使用Grid布局实现计算器按钮
  • 使用LocalStorage实现历史记录的本地保存
  • 实现基本的四则运算和清除功能
  • 提供历史记录查看和删除功能

数据库管理

import relationalStore from '@ohos.data.relationalStore';

export interface CalculationRecord {
  id?: number;
  expression: string;
  result: string;
  timestamp: number;
}

export class CalculatorDB {
  private rdbStore: relationalStore.RdbStore | null = null;
  private readonly TABLE_NAME: string = 'calculation_history';
  private readonly STORE_CONFIG: relationalStore.StoreConfig = {
    name: 'calculator.db',
    securityLevel: relationalStore.SecurityLevel.S1
  };
  private readonly SQL_CREATE_TABLE: string = `
    CREATE TABLE IF NOT EXISTS ${this.TABLE_NAME} (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      expression TEXT NOT NULL,
      result TEXT NOT NULL,
      timestamp INTEGER NOT NULL
    )
  `;

  async initialize(): Promise<void> {
    try {
      this.rdbStore = await relationalStore.getRdbStore(globalThis.context, this.STORE_CONFIG);
      await this.rdbStore.executeSql(this.SQL_CREATE_TABLE);
      console.info('CalculatorDB initialized successfully');
    } catch (error) {
      console.error(`Failed to initialize database: ${JSON.stringify(error)}`);
    }
  }

  async addRecord(record: CalculationRecord): Promise<number> {
    if (!this.rdbStore) {
      console.error('Database not initialized');
      return -1;
    }

    try {
      const valueBucket: relationalStore.ValuesBucket = {
        'expression': record.expression,
        'result': record.result,
        'timestamp': record.timestamp
      };
      const insertId = await this.rdbStore.insert(this.TABLE_NAME, valueBucket);
      console.info('Record added successfully, id:', insertId);
      return insertId;
    } catch (error) {
      console.error(`Failed to add record: ${JSON.stringify(error)}`);
      return -1;
    }
  }

  async getAllRecords(): Promise<CalculationRecord[]> {
    if (!this.rdbStore) {
      console.error('Database not initialized');
      return [];
    }

    try {
      const predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
      predicates.orderByDesc('timestamp');
      const resultSet = await this.rdbStore.query(predicates,
        ['id', 'expression', 'result', 'timestamp']);

      const records: CalculationRecord[] = [];
      while (resultSet.goToNextRow()) {
        records.push({
          id: resultSet.getLong(resultSet.getColumnIndex('id')),
          expression: resultSet.getString(resultSet.getColumnIndex('expression')),
          result: resultSet.getString(resultSet.getColumnIndex('result')),
          timestamp: resultSet.getLong(resultSet.getColumnIndex('timestamp'))
        });
      }
      resultSet.close();
      return records;
    } catch (error) {
      console.error(`Failed to query records: ${JSON.stringify(error)}`);
      return [];
    }
  }

  async clearAllRecords(): Promise<boolean> {
    if (!this.rdbStore) {
      console.error('Database not initialized');
      return false;
    }

    try {
      const predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
      const deletedRows = await this.rdbStore.delete(predicates);
      console.info('All records cleared, deleted rows:', deletedRows);
      return deletedRows > 0;
    } catch (error) {
      console.error(`Failed to clear records: ${JSON.stringify(error)}`);
      return false;
    }
  }

  async deleteRecordById(id: number): Promise<boolean> {
    if (!this.rdbStore) {
      console.error('Database not initialized');
      return false;
    }

    try {
      const predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
      predicates.equalTo('id', id);
      const deletedRows = await this.rdbStore.delete(predicates);
      return deletedRows > 0;
    } catch (error) {
      console.error(`Failed to delete record: ${JSON.stringify(error)}`);
      return false;
    }
  }
}

计算引擎


import { OperationState } from "../model/OperationState";

export class CalculatorEngine {
  private currentInput: string = '0';
  private previousInput: string = '';
  private operator: string = '';
  private shouldResetInput: boolean = false;
  private lastCalculation: string = '';

  // 处理数字输入
  inputDigit(digit: string): void {
    if (this.shouldResetInput) {
      this.currentInput = '0';
      this.shouldResetInput = false;
    }

    if (this.currentInput === '0' && digit !== '.') {
      this.currentInput = digit;
    } else if (digit === '.' && this.currentInput.includes('.')) {
      return;
    } else if (digit === '.' && this.currentInput === '0') {
      this.currentInput = '0.';
    } else if (this.currentInput.length < 15) { // 限制输入长度
      this.currentInput += digit;
    }
  }

  // 处理操作符输入
  inputOperator(op: string): void {
    if (this.currentInput === 'Error') {
      return;
    }

    if (this.operator && !this.shouldResetInput && this.previousInput) {
      this.calculate();
    }

    if (this.currentInput !== '' && this.currentInput !== 'Error') {
      this.previousInput = this.currentInput;
      this.operator = op;
      this.shouldResetInput = true;
    }
  }

  // 执行计算
  calculate(): boolean {
    if (!this.previousInput || !this.operator) {
      return false;
    }

    const prev = parseFloat(this.previousInput);
    const current = parseFloat(this.currentInput);

    if (isNaN(prev) || isNaN(current)) {
      this.currentInput = 'Error';
      return false;
    }

    let result: number;

    try {
      switch (this.operator) {
        case '+':
          result = prev + current;
          break;
        case '-':
          result = prev - current;
          break;
        case '×':
          result = prev * current;
          break;
        case '÷':
          if (current === 0) {
            throw new Error('Division by zero');
          }
          result = prev / current;
          break;
        default:
          return false;
      }

      // 处理浮点数精度问题
      result = this.formatResult(result);

      this.currentInput = result.toString();
      this.lastCalculation = `${this.previousInput} ${this.operator} ${this.currentInput}`;
      this.previousInput = '';
      this.operator = '';
      this.shouldResetInput = true;

      return true;
    } catch (error) {
      this.currentInput = 'Error';
      this.previousInput = '';
      this.operator = '';
      this.shouldResetInput = true;
      return false;
    }
  }

  // 格式化结果,处理精度
  private formatResult(value: number): number {
    if (!isFinite(value)) {
      throw new Error('Invalid result');
    }

    // 处理浮点数精度问题
    if (!Number.isInteger(value)) {
      // 使用toFixed处理小数精度,然后转回数字
      return parseFloat(value.toFixed(10).replace(/\.?0+$/, ''));
    }
    return value;
  }

  // 处理等号
  handleEquals(): boolean {
    if (this.operator && this.previousInput) {
      return this.calculate();
    }
    return false;
  }

  // 清除所有
  clear(): void {
    this.currentInput = '0';
    this.previousInput = '';
    this.operator = '';
    this.shouldResetInput = false;
    this.lastCalculation = '';
  }

  // 清除当前输入
  clearEntry(): void {
    this.currentInput = '0';
    this.shouldResetInput = false;
  }

  // 删除最后一位
  deleteLast(): void {
    if (this.shouldResetInput || this.currentInput === 'Error') {
      this.currentInput = '0';
      this.shouldResetInput = false;
      return;
    }

    if (this.currentInput.length > 1) {
      this.currentInput = this.currentInput.slice(0, -1);
      // 如果删除后只剩下负号,重置为0
      if (this.currentInput === '-') {
        this.currentInput = '0';
      }
    } else {
      this.currentInput = '0';
    }
  }

  // 获取当前显示内容
  getCurrentDisplay(): string {
    return this.currentInput;
  }

  // 获取当前表达式
  getCurrentExpression(): string {
    if (this.previousInput && this.operator) {
      if (this.shouldResetInput) {
        return `${this.previousInput} ${this.operator}`;
      } else {
        return `${this.previousInput} ${this.operator} ${this.currentInput}`;
      }
    }
    return '';
  }

  // 获取最后一次计算的完整表达式(用于历史记录)
  getLastCalculation(): string {
    return this.lastCalculation;
  }

  // 百分比计算
  calculatePercentage(): void {
    if (this.currentInput !== 'Error' && this.currentInput !== '0') {
      const value = parseFloat(this.currentInput);
      if (!isNaN(value)) {
        const result = value / 100;
        this.currentInput = this.formatResult(result).toString();
        this.shouldResetInput = true;
      }
    }
  }

  // 正负号切换
  toggleSign(): void {
    if (this.currentInput !== 'Error' && this.currentInput !== '0') {
      if (this.currentInput.startsWith('-')) {
        this.currentInput = this.currentInput.slice(1);
      } else {
        this.currentInput = '-' + this.currentInput;
      }
    }
  }

  // 获取计算器状态(用于调试)
  getState() {
    let operationState = new OperationState()
    operationState.currentInput = this.currentInput
    operationState.previousInput = this.previousInput
    operationState.operator = this.operator
    operationState.shouldResetInput = this.shouldResetInput
    return operationState;
  }
}


export class OperationState{
  currentInput: string = ""
  previousInput: string = ""
  operator: string = ""
  shouldResetInput:  boolean = false;
}

计算器页面

#星光不负 码向未来# 计算器开发-鸿蒙开发者社区


#星光不负 码向未来# 计算器开发-鸿蒙开发者社区

import { CalculationRecord, CalculatorDB } from '../utils/CalculatorDB';
import { CalculatorEngine } from '../utils/CalculatorEngine';

@Entry
@Component
struct Index {
  @State currentDisplay: string = '0';
  @State expression: string = '';
  @State historyRecords: CalculationRecord[] = [];
  @State showHistory: boolean = false;
  @State isError: boolean = false;

  private calculator: CalculatorEngine = new CalculatorEngine();
  private  calculatorDB:CalculatorDB = new CalculatorDB()
  aboutToAppear() {
    this.calculatorDB.initialize().then(()=>{
      this.loadHistory();
    })
  }

  async loadHistory() {
    try {
      this.historyRecords = await this.calculatorDB.getAllRecords();
    } catch (error) {
      console.error('Failed to load history:', error);
    }
  }

  build() {
    Column() {
      if (this.showHistory) {
        this.HistoryView()
      } else {
        this.CalculatorView()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder CalculatorView() {
    Column({ space: 20 }) {
      // 显示区域
      Column({ space: 10 }) {
        Text(this.expression)
          .fontSize(18)
          .fontColor('#666')
          .textAlign(TextAlign.End)
          .width('90%')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text(this.currentDisplay)
          .fontSize(48)
          .fontColor(this.isError ? '#FF4444' : '#333333')
          .textAlign(TextAlign.End)
          .width('90%')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .padding(20)
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .width('90%')
      .shadow({ radius: 8, color: '#1A000000', offsetX: 0, offsetY: 2 })

      // 按钮区域
      Grid() {
        // 第一行
        GridItem() {
          Button('C')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FF6B6B')
            .fontColor('#FFFFFF')
            .borderRadius(12)
            .onClick(() => this.handleClear())
        }
        GridItem() {
          Button('±')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#E0E0E0')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleToggleSign())
        }
        GridItem() {
          Button('%')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#E0E0E0')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handlePercentage())
        }
        GridItem() {
          Button('÷')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#4CD964')
            .fontColor('#FFFFFF')
            .borderRadius(12)
            .onClick(() => this.handleOperator('÷'))
        }

        // 第二行
        GridItem() {
          Button('7')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FFFFFF')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleDigit('7'))
        }
        GridItem() {
          Button('8')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FFFFFF')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleDigit('8'))
        }
        GridItem() {
          Button('9')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FFFFFF')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleDigit('9'))
        }
        GridItem() {
          Button('×')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#4CD964')
            .fontColor('#FFFFFF')
            .borderRadius(12)
            .onClick(() => this.handleOperator('×'))
        }

        // 第三行
        GridItem() {
          Button('4')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FFFFFF')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleDigit('4'))
        }
        GridItem() {
          Button('5')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FFFFFF')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleDigit('5'))
        }
        GridItem() {
          Button('6')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FFFFFF')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleDigit('6'))
        }
        GridItem() {
          Button('-')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#4CD964')
            .fontColor('#FFFFFF')
            .borderRadius(12)
            .onClick(() => this.handleOperator('-'))
        }

        // 第四行
        GridItem() {
          Button('1')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FFFFFF')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleDigit('1'))
        }
        GridItem() {
          Button('2')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FFFFFF')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleDigit('2'))
        }
        GridItem() {
          Button('3')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FFFFFF')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleDigit('3'))
        }
        GridItem() {
          Button('+')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#4CD964')
            .fontColor('#FFFFFF')
            .borderRadius(12)
            .onClick(() => this.handleOperator('+'))
        }

        // 第五行
        GridItem() {
          Button('0')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FFFFFF')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleDigit('0'))
        }
        GridItem() {
          Button('.')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#FFFFFF')
            .fontColor('#333333')
            .borderRadius(12)
            .onClick(() => this.handleDigit('.'))
        }
        GridItem() {
          Button('=')
            .fontSize(24)
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#007AFF')
            .fontColor('#FFFFFF')
            .borderRadius(12)
            .onClick(() => this.handleEquals())
        }

      }
      .columnsTemplate('1fr 1fr 1fr 1fr')
      .rowsTemplate('1fr 1fr 1fr 1fr 1fr')
      .columnsGap(12)
      .rowsGap(12)
      .width('90%')
      .height(400)

      // 历史记录按钮
      Button('查看历史记录')
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .backgroundColor('#9370DB')
        .fontColor('#FFFFFF')
        .width('90%')
        .height(50)
        .borderRadius(12)
        .onClick(() => {
          this.showHistory = true;
          this.loadHistory();
        })
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .padding({ top: 40, bottom: 20 })
  }

  @Builder HistoryView() {
    Column() {
      // 标题栏
      Row() {
        Button('← 返回')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .backgroundColor('#9370DB')
          .fontColor('#FFFFFF')
          .borderRadius(8)
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
          .onClick(() => this.showHistory = false)

        Text('计算历史')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#333333')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)

        Button('清空')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .backgroundColor('#FF6B6B')
          .fontColor('#FFFFFF')
          .borderRadius(8)
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
          .onClick(async () => {
            const success = await this.calculatorDB.clearAllRecords();
            if (success) {
              this.historyRecords = [];
            }
          })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .alignItems(VerticalAlign.Center)

      if (this.historyRecords.length === 0) {
        Column() {
          Image($r('app.media.startIcon'))
            .width(120)
            .height(120)
            .margin({ bottom: 20 })
          Text('暂无历史记录')
            .fontSize(18)
            .fontColor('#999999')
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
      } else {
        List({ space: 12 }) {
          ForEach(this.historyRecords, (record: CalculationRecord, index?: number) => {
            ListItem() {
              Row() {
                Column({ space: 6 }) {
                  Text(record.expression)
                    .fontSize(16)
                    .fontColor('#666666')
                    .textAlign(TextAlign.Start)
                    .width('100%')
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })

                  Text(`= ${record.result}`)
                    .fontSize(20)
                    .fontColor('#333333')
                    .fontWeight(FontWeight.Medium)
                    .textAlign(TextAlign.Start)
                    .width('100%')
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })

                  Text(this.formatTimestamp(record.timestamp))
                    .fontSize(12)
                    .fontColor('#999999')
                    .textAlign(TextAlign.Start)
                    .width('100%')
                }
                .layoutWeight(1)

                Button('删除')
                  .fontSize(12)
                  .backgroundColor('#FF6B6B')
                  .fontColor('#FFFFFF')
                  .borderRadius(6)
                  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
                  .onClick(async () => {
                    if (record.id) {
                      const success = await this.calculatorDB.deleteRecordById(record.id);
                      if (success) {
                        this.historyRecords.splice(index!, 1);
                        this.historyRecords = [...this.historyRecords];
                      }
                    }
                  })
              }
              .padding(16)
              .backgroundColor('#FFFFFF')
              .borderRadius(12)
              .width('100%')
            }
          }, (record: CalculationRecord) => record.id?.toString() ?? record.timestamp.toString())
        }
        .width('100%')
        .layoutWeight(1)
        .padding(16)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  private handleDigit(digit: string): void {
    if (this.isError) {
      this.calculator.clear();
      this.isError = false;
    }
    this.calculator.inputDigit(digit);
    this.updateDisplay();
  }

  private handleOperator(operator: string): void {
    if (this.isError) {
      this.calculator.clear();
      this.isError = false;
    }
    this.calculator.inputOperator(operator);
    this.updateDisplay();
  }

  private async handleEquals(): Promise<void> {
    if (this.isError) {
      this.calculator.clear();
      this.isError = false;
      this.updateDisplay();
      return;
    }

    const success = this.calculator.handleEquals();
    if (success) {
      const expression = this.calculator.getLastCalculation();
      const result = this.calculator.getCurrentDisplay();

      if (result !== 'Error') {
        const record: CalculationRecord = {
          expression: expression,
          result: result,
          timestamp: new Date().getTime()
        };
        await this.calculatorDB.addRecord(record);
        await this.loadHistory();
      } else {
        this.isError = true;
      }
    }
    this.updateDisplay();
  }

  private handleClear(): void {
    this.calculator.clear();
    this.isError = false;
    this.updateDisplay();
  }

  private handleToggleSign(): void {
    if (!this.isError) {
      this.calculator.toggleSign();
      this.updateDisplay();
    }
  }

  private handlePercentage(): void {
    if (!this.isError) {
      this.calculator.calculatePercentage();
      this.updateDisplay();
    }
  }

  private updateDisplay(): void {
    this.currentDisplay = this.calculator.getCurrentDisplay();
    this.expression = this.calculator.getCurrentExpression();

    if (this.currentDisplay === 'Error') {
      this.isError = true;
    }
  }

  private formatTimestamp(timestamp: number): string {
    const date = new Date(timestamp);
    const now = new Date();
    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const recordDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());

    if (recordDate.getTime() === today.getTime()) {
      return `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
    } else {
      return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
    }
  }
}

功能特点

  1. 完整的计算功能
  • 基本四则运算(+、-、×、÷)
  • 正负号切换
  • 百分比计算
  • 小数点支持
  1. 历史记录功能
  • 自动保存计算历史
  • 历史记录本地存储
  • 清除历史记录功能
  • 限制历史记录数量(最多50条)
  1. 用户界面
  • 响应式布局设计
  • 清晰的视觉层次
  • 符合HarmonyOS设计规范
  • 支持横竖屏适配
  1. 数据持久化
  • 使用LocalStorage实现本地存储
  • 应用重启后历史记录不丢失

使用说明

  1. 数字输入:点击数字按钮输入数字
  2. 运算操作:点击运算符按钮选择运算类型
  3. 等号计算:点击"="执行计算并保存到历史记录
  4. 清除功能:点击"C"清除当前计算
  5. 历史查看:在底部查看历史计算记录
  6. 清除历史:点击"清除历史"按钮清空所有历史记录

这个计算器应用完全使用ArkTS开发,符合HarmonyOS Next的开发规范,具备良好的用户体验和完整的功能实现。


©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
分类
标签
收藏
回复
举报
回复
    相关推荐