ccai 2026-09-18 17:38:18 发布摘要
很多车主并不是没有记账需求,而是觉得打开表格、找分类和计算里程成本太麻烦。本文以一个轻量“车账本”MVP 为例,介绍如何使用 ArkUI 搭建记录页和汇总页,以本地键值存储保存少量记录,并通过输入校验、单向状态更新和失败提示,让核心功能在无网络状态下仍然可用。本文不涉及云同步、自动识别发票等尚未实现的能力。
一、先收窄需求范围
第一版只保留三个使用场景:
- 加油后快速记下金额和里程;
- 查看本月支出和分类汇总;
- 没有网络时仍能查看和新增记录。
功能范围如下:
- 加油、保养、停车和其他四类费用;
- 日期、金额、可选里程和备注;
- 最近记录和本月汇总;
- 删除记录和空状态提示。
第一版明确不做账号、云同步、自动识别发票和复杂图表。对于个人工具来说,先完成闭环比一次加入很多功能更重要。
二、数据模型先统一单位
金额不建议直接以浮点数长期累加。可以在模型层保存“分”:
export type CostType = 'fuel' | 'maintenance' | 'parking' | 'other'
export interface CostRecord {
id: string
type: CostType
amountFen: number
date: string // yyyy-MM-dd
mileageKm?: number
note: string
}
export function parseAmount(text: string): number | undefined {
const value = Number(text.trim())
if (!Number.isFinite(value) || value <= 0 || value > 100000) {
return undefined
}
return Math.round(value * 100)
}
export function parseMileage(text: string): number | undefined {
if (text.trim().length === 0) {
return undefined
}
const value = Number(text.trim())
return Number.isFinite(value) && value >= 0 ? value : undefined
}显示时再将 amountFen / 100 格式化为金额。日期也应保存为稳定格式,不要把本地化显示文本直接当成排序字段。
三、用仓储层隔离 Preferences
Preferences 更适合少量配置和小规模记录,不适合保存图片、大型离线队列或复杂关系数据。下面是示意代码,具体 getPreferences 参数形式请以当前 SDK 为准。
import { preferences } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'
export class CostRepository {
private store?: preferences.Preferences
private readonly key: string = 'records_v1'
async open(context: common.UIAbilityContext): Promise<void> {
// 具体签名可能随 SDK/API 级别变化
this.store = await preferences.getPreferences(context, 'car_cost_book')
}
async list(): Promise<CostRecord[]> {
if (this.store === undefined) {
return []
}
const raw = await this.store.get(this.key, '[]') as string
try {
const value: Object = JSON.parse(raw)
return Array.isArray(value) ? value as CostRecord[] : []
} catch (_) {
return []
}
}
async save(records: CostRecord[]): Promise<void> {
if (this.store === undefined) {
throw new Error('repository is not opened')
}
await this.store.put(this.key, JSON.stringify(records))
await this.store.flush()
}
}几个边界必须写清:
put成功不等于已经完成可靠持久化,应在适当时机调用flush;- 不要在每一次键盘输入时写入和提交;
- 不要保存密码、长期令牌等敏感信息;
- 数据量增大后,应迁移到关系型数据库;
- 数据损坏时不能让页面直接白屏,应保留错误提示和恢复路径。
四、页面只维护必要状态
页面可以只维护记录数组、加载状态、错误文案和编辑器显示状态:
@Entry
@Component
struct HomePage {
private repo: CostRepository = new CostRepository()
@State private records: CostRecord[] = []
@State private loading: boolean = true
@State private errorText: string = ''
@State private editorVisible: boolean = false
aboutToAppear(): void {
this.loadRecords()
}
private async loadRecords(): Promise<void> {
try {
const context = getContext(this) as common.UIAbilityContext
await this.repo.open(context)
this.records = await this.repo.list()
} catch (_) {
this.errorText = '读取本地数据失败,请稍后重试'
} finally {
this.loading = false
}
}
private async appendRecord(record: CostRecord): Promise<void> {
const next: CostRecord[] = [record, ...this.records]
try {
await this.repo.save(next)
this.records = next
this.editorVisible = false
} catch (_) {
this.errorText = '保存失败,原记录未改变'
}
}
}新增流程应当是:
- 读取表单;
- 校验金额、日期和里程;
- 生成一条新的不可变记录;
- 写入仓储;
- 持久化成功后再更新页面状态。
如果保存失败,应保留用户已经填写的内容,不要先清空表单再显示错误。
五、测试用例比功能数量更重要
MVP 至少要验收:
- 首次安装无数据;
- 四种费用均可新增;
- 金额为空、负数、超大值;
- 里程为空或包含字母;
- 快速连续点击保存;
- 杀掉应用后重新打开;
- 删除最后一条记录;
- 无网络状态启动;
- 数据损坏时的恢复提示;
- 系统时间切换到下个月。
月份统计要使用本地日期逻辑。直接使用 toISOString() 可能在月末跨时区时产生归属偏差,正式版本应使用经过验证的本地日期格式化方法。
六、后续演进
当记录达到数千条时,可以迁移到关系型数据库,并按月份和类型建立索引。需要跨设备时,应重新设计同步和冲突策略,不要简单地把 Preferences 文件上传。
后续还可以增加:
- CSV 或图片报表导出;
- 桌面卡片只读展示月度汇总;
- 用户确认后的 AI 分类建议;
- 备份文件和数据恢复;
- 无障碍标签、读屏顺序和更大的触控区域。
七、总结
我认为一个可交付的 HarmonyOS 应用不一定要功能庞大。把需求收窄,选合适的存储方式,为输入错误和写入失败预留位置,再用真实异常数据验收,才能让“从想法到落地”真正闭环。
相关推荐
ccai
我还没有写个人简介......
帖子
提问
粉丝
一笔加油费到月度报表:用 ArkTS 做一个离线优先的“车账本”
2026-09-18 17:38:18 发布大文件上传的断点续传:HarmonyOS 客户端与 Java 服务端协作
2026-09-18 17:36:40 发布

0
京公网安备:11010502051901号