Flutter三方库适配OpenHarmony【expense_tracker】消费记录器项目完整实战
Flutter三方库适配OpenHarmony【expense_tracker】消费记录器项目完整实战
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
expense_tracker 是一个基于 Flutter 的消费记录器项目,核心代码位于 lib/main.dart。项目默认提供 Coffee、Bus Ticket、Lunch、Book、Gym 五条支出记录,顶部用渐变卡片展示总支出金额,中间横向展示各分类汇总,底部用 ListView.builder 展示每一笔明细。用户可以通过右下角 FloatingActionButton 打开新增支出弹窗,输入标题、金额并选择分类后添加记录,也可以通过长按列表项删除记录。
这个项目适合讲解 Flutter 记账类工具应用在 OpenHarmony 上的适配过程。它覆盖了 数据模型设计、列表派生统计、fold 汇总计算、Map 分类聚合、Dialog 表单输入、DropdownButtonFormField 分类选择、横向统计卡片、空状态展示 和 长按删除交互。

图片说明:本文围绕 Flutter 列表、弹窗、分类汇总和 OpenHarmony 承载工程展开,所有关键代码均来自 expense_tracker 的真实源码。
记账类应用的核心不只是金额相加,还要让用户清楚看到总额、分类、明细、日期和删除行为之间的关系。
一、项目背景与目标
1.1 项目定位
expense_tracker 是一个轻量消费记录工具。它没有数据库和后端服务,所有支出数据都保存在内存列表中。用户可以新增记录、查看总金额、查看分类汇总、浏览支出明细,并通过长按删除记录。
当前项目真实支持的功能包括:
- 默认展示 5 条支出记录。
- 每条支出包含标题、金额、分类和日期。
- 未传入日期时默认使用
DateTime.now()。 - 顶部显示总支出金额。
- 分类汇总横向展示。
- 分类汇总卡片包含图标、金额和分类名。
- 支持 Food、Transport、Shopping、Health、Entertainment、Bills、Other 七类。
- 新增弹窗支持输入标题。
- 新增弹窗支持输入金额。
- 新增弹窗支持下拉选择分类。
- 金额通过
double.tryParse解析。 - 标题非空且金额解析成功时才新增。
- 支出列表展示分类图标、标题、分类、金额和日期。
- 长按列表项删除记录。
- 列表为空时显示空状态。
1.2 技术目标
本文围绕真实源码拆解以下内容:
- Flutter 应用入口和蓝色 Material 3 主题。
Expense模型如何保存标题、金额、分类和日期。_expenses默认数据如何组织。_categoryIcons如何映射分类和图标。_totalExpenses如何通过fold计算总额。_categoryTotals如何使用Map按分类聚合。_addExpense如何通过弹窗新增记录。StatefulBuilder如何管理弹窗分类选择状态。_deleteExpense如何长按删除明细。- OpenHarmony 侧如何验证输入、下拉、列表、统计和空状态。
1.3 核心实现速览
| 能力 | 当前实现 | 适配关注点 |
|---|---|---|
| 应用入口 | runApp(const ExpenseTrackerApp()) |
确认首屏加载 |
| 主题 | ColorScheme.fromSeed(seedColor: Colors.blue) |
确认蓝色 Material 3 样式 |
| 数据模型 | Expense |
确认标题、金额、分类、日期 |
| 默认数据 | _expenses |
确认 5 条初始记录 |
| 分类图标 | _categoryIcons |
确认分类与图标映射 |
| 总金额 | _totalExpenses |
确认 fold 汇总 |
| 分类汇总 | _categoryTotals |
确认 Map 聚合 |
| 新增记录 | AlertDialog |
确认输入和下拉 |
| 删除记录 | onLongPress |
确认长按删除 |
| 空状态 | _expenses.isEmpty |
确认列表为空提示 |
二、环境准备与工程结构
2.1 工程结构
项目保持 Flutter 标准结构,同时包含 OpenHarmony 平台工程。
| 文件或目录 | 作用 |
|---|---|
lib/main.dart |
应用入口、支出模型、统计计算、弹窗和 UI |
pubspec.yaml |
SDK 约束、Flutter 依赖和 Material 图标配置 |
analysis_options.yaml |
Flutter lint 规则 |
test/ |
Flutter 测试目录 |
ohos/ |
OpenHarmony 平台承载工程 |
README.md |
项目说明文件 |
当前业务逻辑集中在 lib/main.dart,没有引入持久化插件、数据库或图表库。
2.2 依赖配置
项目使用 Dart SDK ^3.9.2,依赖 Flutter SDK。
environment:
sdk: ^3.9.2
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
支出统计、分类聚合和表单交互全部由 Dart 与 Flutter Material 基础能力完成。
2.3 常用命令
flutter pub get
flutter analyze
flutter test
flutter run
| 命令 | 用途 |
|---|---|
flutter pub get |
获取依赖 |
flutter analyze |
执行静态分析 |
flutter test |
执行测试 |
flutter run |
在目标设备运行 |
OpenHarmony 调试时,还需要结合本地 Flutter OpenHarmony 工具链完成构建、安装和运行。
三、应用入口与主题配置
3.1 import 依赖
项目只引入 Flutter Material。
import 'package:flutter/material.dart';
material.dart 提供 MaterialApp、Scaffold、AppBar、Card、AlertDialog、DropdownButtonFormField、ListTile、FloatingActionButton 等组件。
3.2 main 函数
入口函数启动根组件。
void main() {
runApp(const ExpenseTrackerApp());
}
3.3 ExpenseTrackerApp
根组件创建 MaterialApp。
class ExpenseTrackerApp extends StatelessWidget {
const ExpenseTrackerApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Expense Tracker',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const ExpenseTrackerHomePage(title: 'Expense Tracker'),
);
}
}
这段代码包含三个关键点:
- 应用标题为
Expense Tracker。 - 使用蓝色作为主题种子色。
- 首页为
ExpenseTrackerHomePage。
四、Expense 数据模型
4.1 模型源码
项目定义了 Expense 模型。
class Expense {
String title;
double amount;
String category;
DateTime date;
Expense({
required this.title,
required this.amount,
required this.category,
DateTime? date,
}) : date = date ?? DateTime.now();
}
这个模型封装一笔支出的核心信息。
4.2 字段说明
| 字段 | 类型 | 作用 |
|---|---|---|
title |
String |
支出标题 |
amount |
double |
支出金额 |
category |
String |
支出分类 |
date |
DateTime |
支出日期 |
4.3 默认日期
构造函数允许外部传入日期,也可以不传。
DateTime? date,
}) : date = date ?? DateTime.now();
如果没有传入日期,就使用创建记录时的当前时间。这符合随手记账场景。
4.4 模型边界
当前模型字段是可变字段。
String title;
double amount;
String category;
DateTime date;
在当前单页 Demo 中问题不大。正式项目中可以使用 final 字段和 copyWith 方法,减少意外修改。
五、默认支出数据
5.1 _expenses 列表
页面默认有 5 条支出。
final List<Expense> _expenses = [
Expense(title: 'Coffee', amount: 4.50, category: 'Food'),
Expense(title: 'Bus Ticket', amount: 2.00, category: 'Transport'),
Expense(title: 'Lunch', amount: 12.00, category: 'Food'),
Expense(title: 'Book', amount: 15.00, category: 'Shopping'),
Expense(title: 'Gym', amount: 30.00, category: 'Health'),
];
5.2 默认数据表
| 标题 | 金额 | 分类 |
|---|---|---|
| Coffee | 4.50 | Food |
| Bus Ticket | 2.00 | Transport |
| Lunch | 12.00 | Food |
| Book | 15.00 | Shopping |
| Gym | 30.00 | Health |
默认总金额为 63.50。
5.3 默认分类分布
| 分类 | 金额 |
|---|---|
| Food | 16.50 |
| Transport | 2.00 |
| Shopping | 15.00 |
| Health | 30.00 |
Entertainment、Bills、Other 在初始数据中没有记录,但新增弹窗可以选择这些分类。
六、分类图标映射
6.1 _categoryIcons
项目使用 Map<String, IconData> 维护分类图标。
final Map<String, IconData> _categoryIcons = {
'Food': Icons.restaurant,
'Transport': Icons.directions_car,
'Shopping': Icons.shopping_bag,
'Health': Icons.local_hospital,
'Entertainment': Icons.movie,
'Bills': Icons.receipt,
'Other': Icons.more_horiz,
};
6.2 分类表
| 分类 | 图标 |
|---|---|
| Food | Icons.restaurant |
| Transport | Icons.directions_car |
| Shopping | Icons.shopping_bag |
| Health | Icons.local_hospital |
| Entertainment | Icons.movie |
| Bills | Icons.receipt |
| Other | Icons.more_horiz |
6.3 图标使用位置
分类图标被用在两个地方:
- 横向分类汇总卡片。
- 支出明细列表左侧
CircleAvatar。
这能让用户通过视觉快速识别消费类型。
七、总支出计算
7.1 _totalExpenses getter
总支出通过 fold 计算。
double get _totalExpenses => _expenses.fold(0, (sum, e) => sum + e.amount);
这个 getter 每次读取时都会基于当前 _expenses 重新计算。
7.2 fold 计算过程
以默认数据为例:
0 + 4.50 = 4.50
4.50 + 2.00 = 6.50
6.50 + 12.00 = 18.50
18.50 + 15.00 = 33.50
33.50 + 30.00 = 63.50
7.3 顶部金额展示
Text(
'\$${_totalExpenses.toStringAsFixed(2)}',
style: const TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.bold,
),
)
toStringAsFixed(2) 保证金额显示两位小数。
八、分类汇总计算
8.1 _categoryTotals getter
分类汇总使用 Map 聚合。
Map<String, double> get _categoryTotals {
final totals = <String, double>{};
for (final expense in _expenses) {
totals[expense.category] = (totals[expense.category] ?? 0) + expense.amount;
}
return totals;
}
8.2 聚合逻辑
每一笔支出都会按分类累加:
totals[expense.category] =
(totals[expense.category] ?? 0) + expense.amount;
如果分类还不存在,初始值按 0 处理。
8.3 分类聚合示例
默认数据中 Food 有 Coffee 和 Lunch 两笔:
Food = 4.50 + 12.00 = 16.50
Transport、Shopping、Health 各有一笔。
8.4 getter 的价值
_categoryTotals 不额外保存状态,而是从 _expenses 派生。新增或删除支出后,只要 setState 触发重建,分类汇总就会自动基于最新列表重新计算。
统计值优先做成派生状态。只保存原始支出列表,能减少总额和分类汇总不同步的问题。
九、顶部总金额卡片
9.1 渐变 Card
顶部卡片展示总支出。
Card(
margin: const EdgeInsets.all(16),
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.blue.shade600],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
)
蓝色渐变让总金额区域更突出。
9.2 标题和金额
const Text(
'Total Expenses',
style: TextStyle(color: Colors.white70, fontSize: 16),
)
Text(
'\$${_totalExpenses.toStringAsFixed(2)}',
style: const TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.bold,
),
)
标题使用半透明白色,金额使用白色大字号。
9.3 金额变化
| 操作 | 影响 |
|---|---|
| 新增支出 | 总金额增加 |
| 删除支出 | 总金额减少 |
| 列表为空 | 总金额为 0.00 |
总金额是 getter,因此页面重建后会自动更新。
十、分类横向统计
10.1 显示条件
分类汇总不为空时显示横向统计区。
if (_categoryTotals.isNotEmpty)
SizedBox(
height: 80,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: _categoryTotals.entries.map((entry) {
return Container();
}).toList(),
),
)
10.2 分类卡片
每个分类卡片固定宽度 100。
Container(
width: 100,
margin: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
)
10.3 分类内容
Icon(_categoryIcons[entry.key], color: Colors.blue)
Text(
'\$${entry.value.toStringAsFixed(0)}',
style: const TextStyle(fontWeight: FontWeight.bold),
)
Text(
entry.key,
style: const TextStyle(fontSize: 10, color: Colors.grey),
overflow: TextOverflow.ellipsis,
)
分类金额显示整数,分类名称超出时省略。
十一、新增支出弹窗
11.1 _addExpense 方法
新增支出通过弹窗完成。
void _addExpense() async {
final titleController = TextEditingController();
final amountController = TextEditingController();
String selectedCategory = 'Other';
await showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: const Text('Add Expense'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [],
),
actions: [],
),
),
);
}
弹窗中使用两个输入控制器和一个局部分类变量。
11.2 标题输入
TextField(
controller: titleController,
decoration: const InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(),
),
)
标题为空时不会添加记录。
11.3 金额输入
TextField(
controller: amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Amount',
prefixText: '\$ ',
border: OutlineInputBorder(),
),
)
金额输入使用数字键盘,并在输入框前展示美元符号。
11.4 分类选择
DropdownButtonFormField<String>(
value: selectedCategory,
decoration: const InputDecoration(labelText: 'Category'),
items: _categoryIcons.keys.map((cat) {
return DropdownMenuItem(value: cat, child: Text(cat));
}).toList(),
onChanged: (value) =>
setDialogState(() => selectedCategory = value ?? 'Other'),
)
分类选项来自 _categoryIcons.keys,保证图标映射和下拉选项一致。
十二、添加与删除逻辑
12.1 Add 按钮
点击 Add 时,代码先解析金额。
final amount = double.tryParse(amountController.text);
if (titleController.text.isNotEmpty && amount != null) {
setState(() {
_expenses.add(Expense(
title: titleController.text,
amount: amount,
category: selectedCategory,
));
});
Navigator.pop(context);
}
标题非空且金额解析成功时才会新增记录。
12.2 Cancel 按钮
取消按钮只关闭弹窗。
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
)
12.3 删除方法
删除方法按索引移除。
void _deleteExpense(int index) {
setState(() {
_expenses.removeAt(index);
});
}
12.4 长按删除
列表项通过长按触发删除。
onLongPress: () => _deleteExpense(index),
当前源码没有删除确认,长按会直接删除对应支出。
十三、支出列表展示
13.1 空状态与列表状态
列表区域根据 _expenses.isEmpty 切换。
Expanded(
child: _expenses.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.receipt_long, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('No expenses yet', style: TextStyle(color: Colors.grey)),
],
),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: _expenses.length,
itemBuilder: (context, index) {
final expense = _expenses[index];
return Card(child: ListTile());
},
),
)
13.2 ListTile 结构
每条明细使用 ListTile。
ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue.shade50,
child: Icon(_categoryIcons[expense.category], color: Colors.blue),
),
title: Text(expense.title),
subtitle: Text(expense.category),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('\$${expense.amount.toStringAsFixed(2)}'),
Text('${expense.date.month}/${expense.date.day}'),
],
),
onLongPress: () => _deleteExpense(index),
)
13.3 明细信息表
| 区域 | 内容 |
|---|---|
| leading | 分类图标 |
| title | 支出标题 |
| subtitle | 支出分类 |
| trailing 第一行 | 金额 |
| trailing 第二行 | 月/日 |
| 长按 | 删除记录 |
13.4 日期展示
日期以月/日形式展示。
'${expense.date.month}/${expense.date.day}'
例如 6 月 9 日会显示为 6/9。
十四、OpenHarmony 适配要点
14.1 基础组件验证
当前项目使用的 Flutter 组件包括:
| 组件 | 作用 | OpenHarmony 关注点 |
|---|---|---|
MaterialApp |
应用根组件 | 首屏加载 |
Scaffold |
页面骨架 | AppBar、Body、FAB |
Card |
总额和列表卡片 | 圆角、阴影、渐变 |
ListView |
分类横向列表 | 横向滚动 |
ListView.builder |
支出明细 | 动态列表 |
AlertDialog |
新增弹窗 | 弹窗尺寸和输入 |
TextField |
标题和金额输入 | 键盘、焦点 |
DropdownButtonFormField |
分类选择 | 菜单展开 |
FloatingActionButton |
新增入口 | 悬浮按钮点击 |
ListTile |
明细行 | 长按手势 |
14.2 表单验证
OpenHarmony 上应重点验证:
- 点击 FAB 能打开新增弹窗。
- 标题输入框能正常输入。
- 金额输入框能弹出数字键盘。
- 分类下拉能展开并选择。
- 金额为合法数字时可以新增。
- 标题为空时不会新增。
- 金额解析失败时不会新增。
14.3 列表与统计验证
新增和删除会同时影响三个区域:
- 顶部总金额。
- 分类横向汇总。
- 底部支出明细列表。
每次新增或删除后,都应确认三个区域是否同步刷新。
14.4 空状态验证
长按删除所有记录后,页面应显示:
No expenses yet
同时顶部总金额应变为 $0.00,分类横向汇总区域应隐藏。
OpenHarmony 适配不能只看新增弹窗是否打开。记账类页面要同时验证输入、统计、列表、删除和空状态。
十五、测试与验证
15.1 初始页面测试
Widget 测试可以验证默认数据。
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('expense tracker shows default expenses', (tester) async {
await tester.pumpWidget(const ExpenseTrackerApp());
expect(find.text('Expense Tracker'), findsWidgets);
expect(find.text('Total Expenses'), findsOneWidget);
expect(find.text('Coffee'), findsOneWidget);
expect(find.text('Bus Ticket'), findsOneWidget);
expect(find.text('Lunch'), findsOneWidget);
});
}
15.2 总金额测试思路
可以把总金额计算抽成纯函数。
double calculateTotal(List<Expense> expenses) {
return expenses.fold(0, (sum, expense) => sum + expense.amount);
}
测试示例:
void main() {
test('calculate total expenses', () {
final expenses = [
Expense(title: 'A', amount: 1.5, category: 'Food'),
Expense(title: 'B', amount: 2.5, category: 'Other'),
];
expect(calculateTotal(expenses), 4.0);
});
}
15.3 分类聚合测试思路
分类聚合也可以抽成纯函数。
Map<String, double> calculateCategoryTotals(List<Expense> expenses) {
final totals = <String, double>{};
for (final expense in expenses) {
totals[expense.category] = (totals[expense.category] ?? 0) + expense.amount;
}
return totals;
}
测试示例:
void main() {
test('calculate category totals', () {
final totals = calculateCategoryTotals([
Expense(title: 'Coffee', amount: 4.5, category: 'Food'),
Expense(title: 'Lunch', amount: 12, category: 'Food'),
]);
expect(totals['Food'], 16.5);
});
}
15.4 手工验证矩阵
| 场景 | 操作 | 预期 |
|---|---|---|
| 首次打开 | 启动应用 | 显示 5 条默认支出 |
| 查看总额 | 查看顶部卡片 | 显示 $63.50 |
| 新增支出 | 输入标题、金额、分类 | 列表新增记录 |
| 分类汇总 | 新增 Food 支出 | Food 分类金额增加 |
| 删除记录 | 长按某条支出 | 记录移除,总额减少 |
| 删除全部 | 长按删除所有支出 | 显示空状态 |
| 非法金额 | 输入非数字金额 | 不新增记录 |
十六、常见问题与优化建议
16.1 为什么总金额用 getter
总金额是 _expenses 的派生值。
double get _totalExpenses => _expenses.fold(0, (sum, e) => sum + e.amount);
使用 getter 可以避免新增或删除后忘记同步总金额。
16.2 为什么分类汇总用 Map
分类聚合天然适合用 Map<String, double>。
final totals = <String, double>{};
分类名作为 key,分类金额作为 value,累加逻辑清晰。
16.3 为什么弹窗分类使用 StatefulBuilder
弹窗中的 selectedCategory 是局部状态。
StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(),
)
使用 StatefulBuilder 可以只刷新弹窗内部下拉选择,不必把分类选择提升到页面全局状态。
16.4 金额为什么需要更严格校验
当前代码只判断 double.tryParse 是否成功。
final amount = double.tryParse(amountController.text);
这意味着 0 或负数也可能通过解析。正式记账场景通常还需要校验金额大于 0。
if (amount != null && amount > 0) {
// 添加支出
}
16.5 为什么长按删除不够直观
长按手势不一定容易被用户发现。当前代码没有删除确认,误触长按也会直接删除。
onLongPress: () => _deleteExpense(index)
更稳妥的交互是显示删除图标、滑动删除,或者增加确认弹窗。
16.6 如何增加持久化
当前数据保存在内存中,应用重启后会恢复默认数据。可以把支出记录序列化到本地存储。
class ExpenseDto {
final String title;
final double amount;
final String category;
final String date;
const ExpenseDto({
required this.title,
required this.amount,
required this.category,
required this.date,
});
}
OpenHarmony 上实现持久化时,需要结合可用插件和平台存储能力。
十七、工程扩展方向
17.1 抽取 ExpenseTile
明细列表项可以拆成独立组件。
class ExpenseTile extends StatelessWidget {
final Expense expense;
final IconData? icon;
final VoidCallback onDelete;
const ExpenseTile({
super.key,
required this.expense,
required this.icon,
required this.onDelete,
});
}
拆分后,页面主体会更清晰。
17.2 抽取新增表单结果
新增弹窗可以返回一个结果对象。
class ExpenseFormResult {
final String title;
final double amount;
final String category;
const ExpenseFormResult({
required this.title,
required this.amount,
required this.category,
});
}
页面只负责接收结果并添加到 _expenses。
17.3 增加日期选择
当前新增支出默认使用当前日期。可以加入日期选择器。
class ExpenseInput {
final String title;
final double amount;
final String category;
final DateTime date;
const ExpenseInput({
required this.title,
required this.amount,
required this.category,
required this.date,
});
}
这样可以补录历史账单。
17.4 增加月度统计
可以按月份汇总支出。
String monthKey(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}';
}
月度统计适合扩展预算、趋势图和分类分析。
总结
expense_tracker 是一个结构清晰的 Flutter 记账类项目。它用 Expense 模型保存标题、金额、分类和日期,用 _expenses 保存当前支出列表,用 _totalExpenses 通过 fold 派生总金额,用 _categoryTotals 通过 Map 聚合分类支出,并用顶部总额卡片、横向分类统计和底部明细列表形成完整展示链路。
从 OpenHarmony 适配角度看,这个项目适合验证 Flutter 表单输入、数字键盘、下拉选择、弹窗局部状态、横向列表、动态明细列表、长按手势、空状态和渐变卡片。排查路径也很明确:总额不对看 _totalExpenses,分类不对看 _categoryTotals,新增失败看输入解析和表单校验,删除异常看列表索引和 setState。
掌握这个项目后,可以继续扩展删除确认、金额正数校验、日期选择、本地持久化、月度统计、预算管理和图表分析,让消费记录器从内存 Demo 演进为更完整的跨平台记账工具。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)