#星光不负 码向未来# 记账本应用开发指南 原创

一路向北545
发布于 2025-10-23 22:49
浏览
0收藏

下面我将为您创建一个基于HarmonyOS NEXT和ArkTS的记账本应用,使用relationalStore进行本地数据存储。

1. 数据模型设计

首先,我们设计记账本的数据模型:

export class Record {
  id?: number; // 主键,自增
  type: number; // 0-支出 1-收入
  amount: number; // 金额
  category: string; // 分类
  description: string; // 描述
  date: string; // 日期 YYYY-MM-DD
  time: string; // 时间 HH:mm:ss
  createTime: number; // 创建时间戳

  constructor(
    type: number,
    amount: number,
    category: string,
    description: string,
    date: string,
    time: string
  ) {
    this.type = type;
    this.amount = amount;
    this.category = category;
    this.description = description;
    this.date = date;
    this.time = time;
    this.createTime = new Date().getTime();
  }
}

2. 数据库管理

创建数据库管理类:

// database/RecordStore.ts
import relationalStore from '@ohos.data.relationalStore';
import { Record } from '../model/Record';
import { TotalInfo } from '../model/TotalInfo';

export class RecordStore {
  private static instance: RecordStore;
  private rdbStore: relationalStore.RdbStore | null = null;
  // 数据库配置
  private readonly STORE_CONFIG: relationalStore.StoreConfig = {
    name: 'FinanceRecord.db',
    securityLevel: relationalStore.SecurityLevel.S1
  };
  // 表结构
  private readonly TABLE_NAME = 'records';
  private readonly SQL_CREATE_TABLE = `
    CREATE TABLE IF NOT EXISTS ${this.TABLE_NAME} (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      type INTEGER NOT NULL,
      amount REAL NOT NULL,
      category TEXT NOT NULL,
      description TEXT,
      date TEXT NOT NULL,
      time TEXT NOT NULL,
      createTime INTEGER NOT NULL
    )
  `;

  private constructor() {
  }

  public static getInstance(): RecordStore {
    if (!RecordStore.instance) {
      RecordStore.instance = new RecordStore();
    }
    return RecordStore.instance;
  }

  // 初始化数据库
  async initialize(context: Context): Promise<void> {
    try {
      this.rdbStore = await relationalStore.getRdbStore(context, this.STORE_CONFIG);
      await this.rdbStore.executeSql(this.SQL_CREATE_TABLE);
      console.info('Database initialized successfully');
    } catch (error) {
      console.error(`Failed to initialize database: ${error}`);
    }
  }

  // 添加记录
  async addRecord(record: Record): Promise<number> {
    if (!this.rdbStore) {
      throw new Error('Database not initialized');
    }

    const valueBucket: relationalStore.ValuesBucket = {
      'type': record.type,
      'amount': record.amount,
      'category': record.category,
      'description': record.description,
      'date': record.date,
      'time': record.time,
      'createTime': record.createTime
    };
    try {
      const id = await this.rdbStore.insert(this.TABLE_NAME, valueBucket);
      console.info(`Record added with id: ${id}`);
      return id;
    } catch (error) {
      throw new Error(`Failed to add record: ${error}`);
    }
  }

  // 查询所有记录
  async getAllRecords(): Promise<Record[]> {
    if (!this.rdbStore) {
      throw new Error('Database not initialized');
    }

    const predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
    predicates.orderByDesc('date').orderByDesc('time');

    try {
      const resultSet = await this.rdbStore.query(predicates,
        ['id', 'type', 'amount', 'category', 'description', 'date', 'time', 'createTime']);

      const records: Record[] = [];
      while (resultSet.goToNextRow()) {
        const record = new Record(
          resultSet.getDouble(resultSet.getColumnIndex('type')),
          resultSet.getDouble(resultSet.getColumnIndex('amount')),
          resultSet.getString(resultSet.getColumnIndex('category')),
          resultSet.getString(resultSet.getColumnIndex('description')),
          resultSet.getString(resultSet.getColumnIndex('date')),
          resultSet.getString(resultSet.getColumnIndex('time'))
        );
        record.id = resultSet.getLong(resultSet.getColumnIndex('id'));
        record.createTime = resultSet.getLong(resultSet.getColumnIndex('createTime'));
        records.push(record);
      }
      resultSet.close();
      return records;
    } catch (error) {
      throw new Error(`Failed to query records: ${error}`);
    }
  }

  // 根据ID删除记录
  async deleteRecordById(id: number): Promise<number> {
    if (!this.rdbStore) {
      throw new Error('Database not initialized');
    }

    const predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
    predicates.equalTo('id', id);

    try {
      const deletedRows = await this.rdbStore.delete(predicates);
      console.info(`Deleted ${deletedRows} record(s)`);
      return deletedRows;
    } catch (error) {
      throw new Error(`Failed to delete record: ${error}`);
    }
  }

  // 更新记录
  async updateRecord(record: Record): Promise<number> {
    if (!this.rdbStore || !record.id) {
      throw new Error('Database not initialized or record id is missing');
    }

    const valueBucket: relationalStore.ValuesBucket = {
      'type': record.type,
      'amount': record.amount,
      'category': record.category,
      'description': record.description,
      'date': record.date,
      'time': record.time
    };

    const predicates = new relationalStore.RdbPredicates(this.TABLE_NAME);
    predicates.equalTo('id', record.id);

    try {
      const updatedRows = await this.rdbStore.update(valueBucket, predicates);
      console.info(`Updated ${updatedRows} record(s)`);
      return updatedRows;
    } catch (error) {
      throw new Error(`Failed to update record: ${error}`);
    }
  }

  // 获取统计信息
  async getStatistics(): Promise<TotalInfo> {
    const records = await this.getAllRecords();
    let totalIncome = 0;
    let totalExpense = 0;

    records.forEach(record => {
      if (record.type === 1) { // 收入
        totalIncome += record.amount;
      } else { // 支出
        totalExpense += record.amount;
      }
    });
    return new TotalInfo(totalIncome, totalExpense, totalIncome - totalExpense)
  }
}


export class TotalInfo {
  totalIncome: number = 0
  totalExpense: number = 0
  balance: number = 0

  constructor(totalIncome: number, totalExpense: number, balance: number) {
    this.totalIncome = totalIncome
    this.totalExpense = totalExpense
    this.balance = balance
  }

}

3. 主页面实现

#星光不负 码向未来# 记账本应用开发指南-鸿蒙开发者社区

import { Record } from '../model/Record';
import { RecordStore } from '../database/RecordStore';
import { router } from '@kit.ArkUI';

@Entry
@Component
struct HomePage  {
  @State records: Record[] = [];
  @State totalIncome: number = 0;
  @State totalExpense: number = 0;
  @State balance: number = 0;
  private recordStore = RecordStore.getInstance();

  aboutToAppear() {
    this.loadData();
  }

  async loadData() {
    try {
      this.records = await this.recordStore.getAllRecords();
      const stats = await this.recordStore.getStatistics();
      this.totalIncome = stats.totalIncome;
      this.totalExpense = stats.totalExpense;
      this.balance = stats.balance;
    } catch (error) {
      console.error(`Failed to load data: ${error}`);
    }
  }

  build() {
    Column() {
      // 顶部统计区域
      this.buildHeader()

      // 记录列表
      List({ space: 10 }) {
        ForEach(this.records, (record: Record) => {
          ListItem() {
            this.buildRecordItem(record)
          }
        }, (record: Record) => record.id?.toString() ?? '')
      }
      .layoutWeight(1)
      .width('100%')

      // 底部添加按钮
      Button('添加记录')
        .width('90%')
        .height(50)
        .fontSize(18)
        .backgroundColor('#007DFF')
        .onClick(() => {
          router.pushUrl({ url: 'pages/AddRecordPage' });
        })
        .margin(20)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
  }

  @Builder buildHeader() {
    Column() {
      Text('总余额')
        .fontSize(16)
        .fontColor('#666')
      Text(`¥${this.balance.toFixed(2)}`)
        .fontSize(32)
        .fontColor(this.balance >= 0 ? '#FF6B35' : '#FF4757')
        .fontWeight(FontWeight.Bold)
        .margin({ top: 5, bottom: 20 })

      Row() {
        Column() {
          Text('收入')
            .fontSize(14)
            .fontColor('#666')
          Text(`¥${this.totalIncome.toFixed(2)}`)
            .fontSize(18)
            .fontColor('#10B981')
            .fontWeight(FontWeight.Medium)
        }
        .layoutWeight(1)

        Column() {
          Text('支出')
            .fontSize(14)
            .fontColor('#666')
          Text(`¥${this.totalExpense.toFixed(2)}`)
            .fontSize(18)
            .fontColor('#EF4444')
            .fontWeight(FontWeight.Medium)
        }
        .layoutWeight(1)
      }
      .width('80%')
    }
    .width('100%')
    .padding(20)
    .backgroundColor('#FFFFFF')
  }

  @Builder buildRecordItem(record: Record) {
    Row() {
      Column() {
        Text(record.category)
          .fontSize(16)
          .fontColor('#1F2937')
        Text(record.description)
          .fontSize(12)
          .fontColor('#6B7280')
          .margin({ top: 2 })
        Text(`${record.date} ${record.time}`)
          .fontSize(10)
          .fontColor('#9CA3AF')
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)

      Text(`${record.type === 1 ? '+' : '-'}¥${record.amount.toFixed(2)}`)
        .fontSize(16)
        .fontColor(record.type === 1 ? '#10B981' : '#EF4444')
        .fontWeight(FontWeight.Medium)
    }
    .width('95%')
    .padding(15)
    .backgroundColor('#FFFFFF')
    .borderRadius(10)
    .margin({ left: 10, right: 10, top: 5 })
  }
}

4. 添加记录页面

#星光不负 码向未来# 记账本应用开发指南-鸿蒙开发者社区

// pages/AddRecordPage.ets
import { Record } from '../model/Record';
import { RecordStore } from '../database/RecordStore';
import { promptAction, router } from '@kit.ArkUI';

@Entry
@Component
struct AddRecordPage {
  @State recordType: number = 0; // 0-支出, 1-收入
  @State amount: string = '';
  @State category: string = '';
  @State description: string = '';
  @State date: string = this.getCurrentDate();
  @State time: string = this.getCurrentTime();
  private recordStore = RecordStore.getInstance();
  // 分类选项
  private expenseCategories: string[] = ['餐饮', '购物', '交通', '娱乐', '医疗', '教育', '其他'];
  private incomeCategories: string[] = ['工资', '奖金', '投资', '兼职', '其他'];

  getCurrentDate(): string {
    const now = new Date();
    return `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate()
      .toString()
      .padStart(2, '0')}`;
  }

  getCurrentTime(): string {
    const now = new Date();
    return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes()
      .toString()
      .padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
  }

  async saveRecord() {
    if (!this.amount || !this.category) {
      promptAction.showToast({ message: '请填写金额和分类', duration: 2000 });
      return;
    }

    const record = new Record(
      this.recordType,
      parseFloat(this.amount),
      this.category,
      this.description,
      this.date,
      this.time
    );

    try {
      await this.recordStore.addRecord(record);
      promptAction.showToast({ message: '记录添加成功', duration: 2000 });
      router.back();
    } catch (error) {
      promptAction.showToast({ message: '添加失败,请重试', duration: 2000 });
      console.error(`Failed to save record: ${error}`);
    }
  }

  build() {
    Column({ space: 20 }) {
      // 类型选择
      Row() {
        Button('支出')
          .layoutWeight(1)
          .backgroundColor(this.recordType === 0 ? '#EF4444' : '#F1F5F9')
          .fontColor(this.recordType === 0 ? '#FFFFFF' : '#64748B')
          .onClick(() => {
            this.recordType = 0;
            this.category = '';
          })

        Button('收入')
          .layoutWeight(1)
          .backgroundColor(this.recordType === 1 ? '#10B981' : '#F1F5F9')
          .fontColor(this.recordType === 1 ? '#FFFFFF' : '#64748B')
          .onClick(() => {
            this.recordType = 1;
            this.category = '';
          })
      }
      .width('90%')
      .height(50)
      .borderRadius(25)

      // 金额输入
      TextInput({ placeholder: '输入金额' })
        .width('90%')
        .height(50)
        .type(InputType.Number)
        .onChange((value: string) => {
          this.amount = value;
        })

      // 分类选择
      Text('选择分类')
        .width('90%')
        .fontSize(16)
        .fontColor('#374151')
        .textAlign(TextAlign.Start)

      List({ space: 10 }) {
        ForEach(this.recordType === 0 ? this.expenseCategories : this.incomeCategories,
          (item: string) => {
            ListItem() {
              Button(item)
                .backgroundColor(this.category === item ?
                  (this.recordType === 0 ? '#EF4444' : '#10B981') : '#F1F5F9')
                .fontColor(this.category === item ? '#FFFFFF' : '#64748B')
                .onClick(() => {
                  this.category = item;
                })
            }
          })
      }
      .width('90%')

      // 描述输入
      TextInput({ placeholder: '添加描述(可选)' })
        .width('90%')
        .height(50)
        .onChange((value: string) => {
          this.description = value;
        })

      // 日期时间
      Row() {
        Text('日期:')
          .fontSize(14)
          .fontColor('#6B7280')
        Text(this.date)
          .fontSize(14)
          .fontColor('#374151')
          .margin({ left: 10 })

        Text('时间:')
          .fontSize(14)
          .fontColor('#6B7280')
          .margin({ left: 20 })
        Text(this.time)
          .fontSize(14)
          .fontColor('#374151')
          .margin({ left: 10 })
      }
      .width('90%')
      .justifyContent(FlexAlign.Start)

      // 保存按钮
      Button('保存记录')
        .width('90%')
        .height(50)
        .backgroundColor('#007DFF')
        .fontColor('#FFFFFF')
        .onClick(() => this.saveRecord())
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F8FAFC')
  }
}

5. 应用入口和权限配置

import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
import { RecordStore } from './database/RecordStore';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    console.info('EntryAbility onCreate');
    
    // 初始化数据库
    const recordStore = RecordStore.getInstance();
    recordStore.initialize(this.context).then(() => {
      console.info('Database initialized in ability');
    }).catch((error) => {
      console.error(`Failed to initialize database: ${error}`);
    });
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    console.info('EntryAbility onWindowStageCreate');
    windowStage.loadContent('pages/HomePage', (err, data) => {
      if (err.code) {
        console.error('Failed to load the content. Cause:' + JSON.stringify(err));
        return;
      }
      console.info('Succeeded in loading the content. Data: ' + JSON.stringify(data));
    });
  }
}

6. 模块配置文件

// module.json5
{
  "module": {
    "name": "finance",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet"
    ],
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC"
      }
    ]
  }
}

使用说明

  1. 初始化数据库:应用启动时自动初始化数据库和表结构
  2. 添加记录:点击"添加记录"按钮,选择类型(支出/收入),输入金额,选择分类,添加描述
  3. 查看记录:主页显示所有记录列表,按时间倒序排列
  4. 统计信息:主页顶部显示总收入、总支出和余额
  5. 数据持久化:所有数据使用relationalStore本地存储

功能特点

  • ✅ 完整的CRUD操作(创建、读取、更新、删除)
  • ✅ 收入和支出分类管理
  • ✅ 实时统计计算
  • ✅ 本地数据持久化
  • ✅ 响应式UI设计
  • ✅ 错误处理和用户提示

这个记账本应用提供了完整的财务管理功能,使用了HarmonyOS NEXT的relationalStore进行数据存储,具有良好的用户体验和数据安全性。您可以根据需要进一步扩展功能,如添加数据导出、图表分析、预算设置等。


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