基础功能实现,金额输入bug待解决
This commit is contained in:
parent
2bf0d8e22b
commit
ac33005dfd
54
lib/data/categories.dart
Normal file
54
lib/data/categories.dart
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../models/record.dart';
|
||||||
|
|
||||||
|
/// 预设支出分类
|
||||||
|
final List<Category> expenseCategories = [
|
||||||
|
Category(
|
||||||
|
id: 'food',
|
||||||
|
name: '餐饮',
|
||||||
|
icon: Icons.restaurant,
|
||||||
|
type: RecordType.expense,
|
||||||
|
),
|
||||||
|
Category(
|
||||||
|
id: 'shopping',
|
||||||
|
name: '购物',
|
||||||
|
icon: Icons.shopping_bag,
|
||||||
|
type: RecordType.expense,
|
||||||
|
),
|
||||||
|
Category(
|
||||||
|
id: 'transport',
|
||||||
|
name: '交通',
|
||||||
|
icon: Icons.directions_bus,
|
||||||
|
type: RecordType.expense,
|
||||||
|
),
|
||||||
|
Category(
|
||||||
|
id: 'entertainment',
|
||||||
|
name: '娱乐',
|
||||||
|
icon: Icons.sports_esports,
|
||||||
|
type: RecordType.expense,
|
||||||
|
),
|
||||||
|
// 可以继续添加更多分类...
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 预设收入分类
|
||||||
|
final List<Category> incomeCategories = [
|
||||||
|
Category(
|
||||||
|
id: 'salary',
|
||||||
|
name: '工资',
|
||||||
|
icon: Icons.account_balance_wallet,
|
||||||
|
type: RecordType.income,
|
||||||
|
),
|
||||||
|
Category(
|
||||||
|
id: 'bonus',
|
||||||
|
name: '奖金',
|
||||||
|
icon: Icons.card_giftcard,
|
||||||
|
type: RecordType.income,
|
||||||
|
),
|
||||||
|
Category(
|
||||||
|
id: 'investment',
|
||||||
|
name: '投资',
|
||||||
|
icon: Icons.trending_up,
|
||||||
|
type: RecordType.income,
|
||||||
|
),
|
||||||
|
// 可以继续添加更多分类...
|
||||||
|
];
|
||||||
21
lib/data/mock_data.dart
Normal file
21
lib/data/mock_data.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import '../models/record.dart';
|
||||||
|
|
||||||
|
final List<Record> mockRecords = [
|
||||||
|
Record(
|
||||||
|
id: '1',
|
||||||
|
type: RecordType.expense,
|
||||||
|
categoryId: 'food',
|
||||||
|
note: '午餐',
|
||||||
|
amount: 25.0,
|
||||||
|
createTime: DateTime.now().subtract(const Duration(hours: 2)),
|
||||||
|
),
|
||||||
|
Record(
|
||||||
|
id: '2',
|
||||||
|
type: RecordType.income,
|
||||||
|
categoryId: 'salary',
|
||||||
|
note: '工资',
|
||||||
|
amount: 5000.0,
|
||||||
|
createTime: DateTime.now().subtract(const Duration(days: 1)),
|
||||||
|
),
|
||||||
|
// 可以添加更多模拟数据...
|
||||||
|
];
|
||||||
73
lib/models/record.dart
Normal file
73
lib/models/record.dart
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 记账类型枚举
|
||||||
|
enum RecordType {
|
||||||
|
expense, // 支出
|
||||||
|
income, // 收入
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记账分类模型
|
||||||
|
class Category {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final IconData icon;
|
||||||
|
final RecordType type;
|
||||||
|
final String? parentId; // 为未来二级分类预留
|
||||||
|
|
||||||
|
const Category({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.icon,
|
||||||
|
required this.type,
|
||||||
|
this.parentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 记账记录模型
|
||||||
|
class Record {
|
||||||
|
final String id;
|
||||||
|
final RecordType type;
|
||||||
|
final String categoryId;
|
||||||
|
final String? note;
|
||||||
|
final double amount;
|
||||||
|
final DateTime createTime;
|
||||||
|
final String? accountId; // 为未来账户功能预留
|
||||||
|
final List<String>? imageUrls; // 为未来配图功能预留
|
||||||
|
|
||||||
|
Record({
|
||||||
|
required this.id,
|
||||||
|
required this.type,
|
||||||
|
required this.categoryId,
|
||||||
|
this.note,
|
||||||
|
required this.amount,
|
||||||
|
required this.createTime,
|
||||||
|
this.accountId,
|
||||||
|
this.imageUrls,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转换为JSON格式,方便存储
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'type': type.index,
|
||||||
|
'categoryId': categoryId,
|
||||||
|
'note': note,
|
||||||
|
'amount': amount,
|
||||||
|
'createTime': createTime.toIso8601String(),
|
||||||
|
'accountId': accountId,
|
||||||
|
'imageUrls': imageUrls,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从JSON格式转换回对象
|
||||||
|
factory Record.fromJson(Map<String, dynamic> json) => Record(
|
||||||
|
id: json['id'],
|
||||||
|
type: RecordType.values[json['type']],
|
||||||
|
categoryId: json['categoryId'],
|
||||||
|
note: json['note'],
|
||||||
|
amount: json['amount'],
|
||||||
|
createTime: DateTime.parse(json['createTime']),
|
||||||
|
accountId: json['accountId'],
|
||||||
|
imageUrls: json['imageUrls'] != null
|
||||||
|
? List<String>.from(json['imageUrls'])
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,7 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:sensors_plus/sensors_plus.dart';
|
import 'package:sensors_plus/sensors_plus.dart';
|
||||||
import 'package:vibration/vibration.dart';
|
import 'package:vibration/vibration.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import '../models/record.dart';
|
||||||
import './record_page.dart';
|
import './record_page.dart';
|
||||||
|
import '../widgets/record_list.dart';
|
||||||
|
import '../data/mock_data.dart'; // 暂时使用模拟数据
|
||||||
|
import '../services/database_service.dart';
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
const HomePage({Key? key}) : super(key: key);
|
const HomePage({Key? key}) : super(key: key);
|
||||||
@ -16,11 +20,23 @@ class _HomePageState extends State<HomePage> {
|
|||||||
DateTime? _lastShakeTime;
|
DateTime? _lastShakeTime;
|
||||||
int _shakeCount = 0;
|
int _shakeCount = 0;
|
||||||
bool _isNavigating = false;
|
bool _isNavigating = false;
|
||||||
|
final _dbService = DatabaseService();
|
||||||
|
List<Record> records = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initShakeDetection();
|
_initShakeDetection();
|
||||||
|
_loadRecords();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载记录
|
||||||
|
Future<void> _loadRecords() async {
|
||||||
|
final loadedRecords = await _dbService.getAllRecords();
|
||||||
|
setState(() {
|
||||||
|
records = loadedRecords;
|
||||||
|
records.sort((a, b) => b.createTime.compareTo(a.createTime));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 初始化摇晃检测
|
/// 初始化摇晃检测
|
||||||
@ -51,7 +67,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通过摇晃触发的导航(带震动)
|
/// 带震动的记账页面导航
|
||||||
void _navigateToRecordPageWithVibration() async {
|
void _navigateToRecordPageWithVibration() async {
|
||||||
if (_isNavigating) return;
|
if (_isNavigating) return;
|
||||||
|
|
||||||
@ -61,10 +77,25 @@ class _HomePageState extends State<HomePage> {
|
|||||||
Vibration.vibrate(duration: 200);
|
Vibration.vibrate(duration: 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_navigateToRecordPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 导航到记账页面
|
||||||
|
void _navigateToRecordPage([Record? record]) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const RecordPage(),
|
builder: (context) => RecordPage(
|
||||||
|
record: record,
|
||||||
|
onSave: (newRecord) async {
|
||||||
|
if (record != null) {
|
||||||
|
await _dbService.updateRecord(newRecord);
|
||||||
|
} else {
|
||||||
|
await _dbService.insertRecord(newRecord);
|
||||||
|
}
|
||||||
|
await _loadRecords();
|
||||||
|
},
|
||||||
|
),
|
||||||
maintainState: false,
|
maintainState: false,
|
||||||
),
|
),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
@ -72,26 +103,10 @@ class _HomePageState extends State<HomePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通过按钮触发的导航(无震动)
|
/// 删除记录
|
||||||
void _navigateToRecordPage() {
|
void _deleteRecord(String recordId) async {
|
||||||
if (_isNavigating) return;
|
await _dbService.deleteRecord(recordId);
|
||||||
|
await _loadRecords();
|
||||||
_isNavigating = true;
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => const RecordPage(),
|
|
||||||
maintainState: false,
|
|
||||||
),
|
|
||||||
).then((_) {
|
|
||||||
_isNavigating = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_accelerometerSubscription?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -100,17 +115,21 @@ class _HomePageState extends State<HomePage> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('记账本'),
|
title: const Text('记账本'),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: RecordList(
|
||||||
child: Column(
|
records: records,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
onRecordTap: _navigateToRecordPage,
|
||||||
children: [
|
onRecordDelete: _deleteRecord,
|
||||||
ElevatedButton(
|
),
|
||||||
onPressed: _navigateToRecordPage,
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Text('记账'),
|
onPressed: () => _navigateToRecordPage(),
|
||||||
),
|
child: const Icon(Icons.add),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_accelerometerSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,32 +1,322 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../models/record.dart';
|
||||||
|
import '../data/categories.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class RecordPage extends StatefulWidget {
|
class RecordPage extends StatefulWidget {
|
||||||
const RecordPage({Key? key}) : super(key: key);
|
final Record? record;
|
||||||
|
final Function(Record) onSave; // 添加保存回调
|
||||||
|
|
||||||
|
const RecordPage({
|
||||||
|
Key? key,
|
||||||
|
this.record,
|
||||||
|
required this.onSave,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RecordPage> createState() => _RecordPageState();
|
State<RecordPage> createState() => _RecordPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RecordPageState extends State<RecordPage> {
|
class _RecordPageState extends State<RecordPage> with SingleTickerProviderStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
String? _selectedCategoryId;
|
||||||
|
String _note = '';
|
||||||
|
double _amount = 0.0;
|
||||||
|
DateTime _selectedDate = DateTime.now();
|
||||||
|
final _uuid = const Uuid();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// 如果是编辑模式,初始化现有记录的数据
|
||||||
|
if (widget.record != null) {
|
||||||
|
_selectedCategoryId = widget.record!.categoryId;
|
||||||
|
_note = widget.record!.note ?? '';
|
||||||
|
_amount = widget.record!.amount;
|
||||||
|
_selectedDate = widget.record!.createTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
_tabController = TabController(
|
||||||
|
length: 2,
|
||||||
|
vsync: this,
|
||||||
|
initialIndex: widget.record?.type == RecordType.income ? 1 : 0,
|
||||||
|
);
|
||||||
|
_tabController.addListener(_onTabChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 标签切换监听
|
||||||
|
void _onTabChanged() {
|
||||||
|
setState(() {
|
||||||
|
_selectedCategoryId = null; // 切换类型时重置选中的分类
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取当前记录类型
|
||||||
|
RecordType get _currentType =>
|
||||||
|
_tabController.index == 0 ? RecordType.expense : RecordType.income;
|
||||||
|
|
||||||
|
/// 获取当前分类列表
|
||||||
|
List<Category> get _currentCategories =>
|
||||||
|
_currentType == RecordType.expense ? expenseCategories : incomeCategories;
|
||||||
|
|
||||||
|
/// 保存记录
|
||||||
|
void _saveRecord() {
|
||||||
|
if (_selectedCategoryId == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('请选择分类')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_amount <= 0) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('请输入金额')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final record = Record(
|
||||||
|
id: widget.record?.id ?? _uuid.v4(),
|
||||||
|
type: _currentType,
|
||||||
|
categoryId: _selectedCategoryId!,
|
||||||
|
note: _note.isEmpty ? null : _note,
|
||||||
|
amount: _amount,
|
||||||
|
createTime: _selectedDate,
|
||||||
|
);
|
||||||
|
|
||||||
|
widget.onSave(record);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return WillPopScope(
|
return Scaffold(
|
||||||
onWillPop: () async {
|
appBar: AppBar(
|
||||||
Navigator.of(context).pop();
|
title: TabBar(
|
||||||
return false;
|
controller: _tabController,
|
||||||
},
|
tabs: const [
|
||||||
child: Scaffold(
|
Tab(text: '支出'),
|
||||||
appBar: AppBar(
|
Tab(text: '收入'),
|
||||||
title: const Text('记账'),
|
],
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
body: const Center(
|
),
|
||||||
child: Text('记账页面'),
|
body: Column(
|
||||||
|
children: [
|
||||||
|
// 分类网格
|
||||||
|
Expanded(
|
||||||
|
child: GridView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 4,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
),
|
||||||
|
itemCount: _currentCategories.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final category = _currentCategories[index];
|
||||||
|
final isSelected = category.id == _selectedCategoryId;
|
||||||
|
return _buildCategoryItem(category, isSelected);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 底部编辑区域
|
||||||
|
_buildBottomEditor(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建分类项
|
||||||
|
Widget _buildCategoryItem(Category category, bool isSelected) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () => setState(() => _selectedCategoryId = category.id),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? Theme.of(context).primaryColor.withOpacity(0.1) : null,
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected ? Theme.of(context).primaryColor : Colors.grey,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
category.icon,
|
||||||
|
color: isSelected ? Theme.of(context).primaryColor : Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
category.name,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSelected ? Theme.of(context).primaryColor : Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 构建底部编辑区域
|
||||||
|
Widget _buildBottomEditor() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, -2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 备注和金额
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: '添加备注',
|
||||||
|
border: InputBorder.none,
|
||||||
|
),
|
||||||
|
onChanged: (value) => setState(() => _note = value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'¥${_amount.toStringAsFixed(2)}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// 配置按钮
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: 实现账户选择
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.account_balance_wallet),
|
||||||
|
label: const Text('默认账户'),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
final date = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _selectedDate,
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
if (date != null) {
|
||||||
|
setState(() => _selectedDate = date);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.calendar_today),
|
||||||
|
label: Text(_selectedDate == DateTime.now()
|
||||||
|
? '今天'
|
||||||
|
: '${_selectedDate.month}月${_selectedDate.day}日'),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: 实现图片选择
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.photo),
|
||||||
|
label: const Text('图片'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// 数字键盘
|
||||||
|
_buildNumberKeyboard(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建数字键盘
|
||||||
|
Widget _buildNumberKeyboard() {
|
||||||
|
return GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
crossAxisCount: 4,
|
||||||
|
childAspectRatio: 2,
|
||||||
|
children: [
|
||||||
|
_buildKeyboardButton('7'),
|
||||||
|
_buildKeyboardButton('8'),
|
||||||
|
_buildKeyboardButton('9'),
|
||||||
|
_buildKeyboardButton('删除', isFunction: true),
|
||||||
|
_buildKeyboardButton('4'),
|
||||||
|
_buildKeyboardButton('5'),
|
||||||
|
_buildKeyboardButton('6'),
|
||||||
|
_buildKeyboardButton('+'),
|
||||||
|
_buildKeyboardButton('1'),
|
||||||
|
_buildKeyboardButton('2'),
|
||||||
|
_buildKeyboardButton('3'),
|
||||||
|
_buildKeyboardButton('-'),
|
||||||
|
_buildKeyboardButton('again'),
|
||||||
|
_buildKeyboardButton('0'),
|
||||||
|
_buildKeyboardButton('.'),
|
||||||
|
_buildKeyboardButton('保存', isFunction: true),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建键盘按钮
|
||||||
|
Widget _buildKeyboardButton(String text, {bool isFunction = false}) {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () => _onKeyboardButtonPressed(text),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
color: isFunction ? Theme.of(context).primaryColor : Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 处理键盘按钮点击
|
||||||
|
void _onKeyboardButtonPressed(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case '删除':
|
||||||
|
setState(() {
|
||||||
|
final amountStr = _amount.toStringAsFixed(2);
|
||||||
|
if (amountStr.length > 1) {
|
||||||
|
_amount = double.parse(amountStr.substring(0, amountStr.length - 1));
|
||||||
|
} else {
|
||||||
|
_amount = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case '保存':
|
||||||
|
_saveRecord();
|
||||||
|
break;
|
||||||
|
case 'again':
|
||||||
|
// TODO: 实现again功能
|
||||||
|
break;
|
||||||
|
case '+':
|
||||||
|
case '-':
|
||||||
|
// TODO: 实现加减功能
|
||||||
|
break;
|
||||||
|
case '.':
|
||||||
|
// TODO: 实现小数点功能
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (_amount == 0) {
|
||||||
|
setState(() => _amount = double.parse(value));
|
||||||
|
} else {
|
||||||
|
setState(() => _amount = double.parse('$_amount$value'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
94
lib/services/database_service.dart
Normal file
94
lib/services/database_service.dart
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import 'package:sqflite/sqflite.dart' show Database, openDatabase, getDatabasesPath, ConflictAlgorithm;
|
||||||
|
import 'package:path/path.dart' as path_helper;
|
||||||
|
import '../models/record.dart';
|
||||||
|
|
||||||
|
class DatabaseService {
|
||||||
|
static Database? _database;
|
||||||
|
|
||||||
|
/// 获取数据库实例
|
||||||
|
Future<Database> get database async {
|
||||||
|
if (_database != null) return _database!;
|
||||||
|
_database = await _initDatabase();
|
||||||
|
return _database!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化数据库
|
||||||
|
Future<Database> _initDatabase() async {
|
||||||
|
final path = await getDatabasesPath();
|
||||||
|
final dbPath = path_helper.join(path, 'records.db');
|
||||||
|
|
||||||
|
return await openDatabase(
|
||||||
|
dbPath,
|
||||||
|
version: 1,
|
||||||
|
onCreate: (db, version) async {
|
||||||
|
await db.execute('''
|
||||||
|
CREATE TABLE records(
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
type INTEGER NOT NULL,
|
||||||
|
categoryId TEXT NOT NULL,
|
||||||
|
note TEXT,
|
||||||
|
amount REAL NOT NULL,
|
||||||
|
createTime TEXT NOT NULL,
|
||||||
|
accountId TEXT,
|
||||||
|
imageUrls TEXT
|
||||||
|
)
|
||||||
|
''');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 插入记录
|
||||||
|
Future<void> insertRecord(Record record) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.insert(
|
||||||
|
'records',
|
||||||
|
record.toJson()..addAll({
|
||||||
|
'imageUrls': record.imageUrls?.join(','),
|
||||||
|
}),
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 更新记录
|
||||||
|
Future<void> updateRecord(Record record) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.update(
|
||||||
|
'records',
|
||||||
|
record.toJson()..addAll({
|
||||||
|
'imageUrls': record.imageUrls?.join(','),
|
||||||
|
}),
|
||||||
|
where: 'id = ?',
|
||||||
|
whereArgs: [record.id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除记录
|
||||||
|
Future<void> deleteRecord(String id) async {
|
||||||
|
final db = await database;
|
||||||
|
await db.delete(
|
||||||
|
'records',
|
||||||
|
where: 'id = ?',
|
||||||
|
whereArgs: [id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取所有记录
|
||||||
|
Future<List<Record>> getAllRecords() async {
|
||||||
|
final db = await database;
|
||||||
|
final List<Map<String, dynamic>> maps = await db.query('records');
|
||||||
|
|
||||||
|
return List.generate(maps.length, (i) {
|
||||||
|
final map = maps[i];
|
||||||
|
return Record(
|
||||||
|
id: map['id'],
|
||||||
|
type: RecordType.values[map['type']],
|
||||||
|
categoryId: map['categoryId'],
|
||||||
|
note: map['note'],
|
||||||
|
amount: map['amount'],
|
||||||
|
createTime: DateTime.parse(map['createTime']),
|
||||||
|
accountId: map['accountId'],
|
||||||
|
imageUrls: map['imageUrls']?.split(','),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
99
lib/widgets/record_detail_dialog.dart
Normal file
99
lib/widgets/record_detail_dialog.dart
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../models/record.dart';
|
||||||
|
import '../data/categories.dart';
|
||||||
|
|
||||||
|
class RecordDetailDialog extends StatelessWidget {
|
||||||
|
final Record record;
|
||||||
|
final VoidCallback onEdit;
|
||||||
|
final VoidCallback onDelete;
|
||||||
|
|
||||||
|
const RecordDetailDialog({
|
||||||
|
Key? key,
|
||||||
|
required this.record,
|
||||||
|
required this.onEdit,
|
||||||
|
required this.onDelete,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// 获取分类信息
|
||||||
|
Category _getCategory(String categoryId) {
|
||||||
|
return [...expenseCategories, ...incomeCategories]
|
||||||
|
.firstWhere((c) => c.id == categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final category = _getCategory(record.categoryId);
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text('记录详情'),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
onEdit();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
onDelete();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: const Text('分类'),
|
||||||
|
subtitle: Text(category.name),
|
||||||
|
leading: Icon(category.icon),
|
||||||
|
onTap: () {
|
||||||
|
// TODO: 跳转到分类详情页
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (record.note != null)
|
||||||
|
ListTile(
|
||||||
|
title: const Text('备注'),
|
||||||
|
subtitle: Text(record.note!),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('金额'),
|
||||||
|
subtitle: Text(
|
||||||
|
'${record.type == RecordType.expense ? "-" : "+"}¥${record.amount.toStringAsFixed(2)}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: record.type == RecordType.expense
|
||||||
|
? Colors.red
|
||||||
|
: Colors.green,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('时间'),
|
||||||
|
subtitle: Text(
|
||||||
|
'${record.createTime.year}-${record.createTime.month}-${record.createTime.day} '
|
||||||
|
'${record.createTime.hour}:${record.createTime.minute}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('账户'),
|
||||||
|
subtitle: const Text('默认账户'),
|
||||||
|
onTap: () {
|
||||||
|
// TODO: 跳转到账户页
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
123
lib/widgets/record_list.dart
Normal file
123
lib/widgets/record_list.dart
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../models/record.dart';
|
||||||
|
import '../data/categories.dart';
|
||||||
|
import './record_detail_dialog.dart';
|
||||||
|
|
||||||
|
class RecordList extends StatelessWidget {
|
||||||
|
final List<Record> records;
|
||||||
|
final Function(Record) onRecordTap;
|
||||||
|
final Function(String) onRecordDelete;
|
||||||
|
|
||||||
|
const RecordList({
|
||||||
|
Key? key,
|
||||||
|
required this.records,
|
||||||
|
required this.onRecordTap,
|
||||||
|
required this.onRecordDelete,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// 获取分类信息
|
||||||
|
Category _getCategory(String categoryId) {
|
||||||
|
return [...expenseCategories, ...incomeCategories]
|
||||||
|
.firstWhere((c) => c.id == categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按日期分组记录
|
||||||
|
Map<String, List<Record>> _groupByDate() {
|
||||||
|
final groups = <String, List<Record>>{};
|
||||||
|
for (final record in records) {
|
||||||
|
final date = _formatDate(record.createTime);
|
||||||
|
if (!groups.containsKey(date)) {
|
||||||
|
groups[date] = [];
|
||||||
|
}
|
||||||
|
groups[date]!.add(record);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 格式化日期
|
||||||
|
String _formatDate(DateTime date) {
|
||||||
|
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 计算日汇总
|
||||||
|
String _getDailySummary(List<Record> dayRecords) {
|
||||||
|
double expense = 0;
|
||||||
|
double income = 0;
|
||||||
|
for (final record in dayRecords) {
|
||||||
|
if (record.type == RecordType.expense) {
|
||||||
|
expense += record.amount;
|
||||||
|
} else {
|
||||||
|
income += record.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final parts = <String>[];
|
||||||
|
if (expense > 0) parts.add('支:¥${expense.toStringAsFixed(2)}');
|
||||||
|
if (income > 0) parts.add('收:¥${income.toStringAsFixed(2)}');
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final groups = _groupByDate();
|
||||||
|
final dates = groups.keys.toList()..sort((a, b) => b.compareTo(a));
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: dates.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final date = dates[index];
|
||||||
|
final dayRecords = groups[date]!;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 日期头部
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
date,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(_getDailySummary(dayRecords)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 记录列表
|
||||||
|
...dayRecords.map((record) {
|
||||||
|
final category = _getCategory(record.categoryId);
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(category.icon),
|
||||||
|
title: Text(category.name),
|
||||||
|
subtitle: record.note != null ? Text(record.note!) : null,
|
||||||
|
trailing: Text(
|
||||||
|
'${record.type == RecordType.expense ? "-" : "+"}¥${record.amount.toStringAsFixed(2)}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: record.type == RecordType.expense
|
||||||
|
? Colors.red
|
||||||
|
: Colors.green,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => RecordDetailDialog(
|
||||||
|
record: record,
|
||||||
|
onEdit: () => onRecordTap(record),
|
||||||
|
onDelete: () => onRecordDelete(record.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
const Divider(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
68
pubspec.lock
68
pubspec.lock
@ -41,6 +41,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.18.0"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.3"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -89,6 +97,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -153,7 +169,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.10.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||||
@ -205,6 +221,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.10.0"
|
||||||
|
sprintf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sprintf
|
||||||
|
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
|
sqflite:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: sqflite
|
||||||
|
sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
sqflite_common:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sqflite_common
|
||||||
|
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.3"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -229,6 +269,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0+1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -245,6 +293,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
version: "0.6.1"
|
||||||
|
typed_data:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: typed_data
|
||||||
|
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.2"
|
||||||
|
uuid:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.1"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -295,4 +359,4 @@ packages:
|
|||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.2.5 <4.0.0"
|
dart: ">=3.2.5 <4.0.0"
|
||||||
flutter: ">=3.3.0"
|
flutter: ">=3.7.0"
|
||||||
|
|||||||
@ -32,6 +32,9 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
sensors_plus: ^1.4.1 # 用于检测手机摇晃
|
sensors_plus: ^1.4.1 # 用于检测手机摇晃
|
||||||
vibration: ^1.8.4 # 添加震动支持
|
vibration: ^1.8.4 # 添加震动支持
|
||||||
|
uuid: ^4.3.3 # 添加uuid包用于生成唯一ID
|
||||||
|
sqflite: ^2.3.2 # 添加 SQLite 支持
|
||||||
|
path: ^1.8.3 # 用于处理文件路径
|
||||||
|
|
||||||
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user