Merge remote-tracking branch 'daodaoshi/main'
This commit is contained in:
commit
9b2ee71519
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:vibration/vibration.dart';
|
||||
import 'dart:async';
|
||||
import '../models/record.dart';
|
||||
import './record_page.dart';
|
||||
import '../widgets/record_list.dart';
|
||||
import '../data/mock_data.dart'; // 暂时使用模拟数据
|
||||
import '../services/database_service.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
@ -16,11 +20,23 @@ class _HomePageState extends State<HomePage> {
|
||||
DateTime? _lastShakeTime;
|
||||
int _shakeCount = 0;
|
||||
bool _isNavigating = false;
|
||||
final _dbService = DatabaseService();
|
||||
List<Record> records = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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 {
|
||||
if (_isNavigating) return;
|
||||
|
||||
@ -61,10 +77,25 @@ class _HomePageState extends State<HomePage> {
|
||||
Vibration.vibrate(duration: 200);
|
||||
}
|
||||
|
||||
_navigateToRecordPage();
|
||||
}
|
||||
|
||||
/// 导航到记账页面
|
||||
void _navigateToRecordPage([Record? record]) {
|
||||
Navigator.push(
|
||||
context,
|
||||
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,
|
||||
),
|
||||
).then((_) {
|
||||
@ -72,26 +103,10 @@ class _HomePageState extends State<HomePage> {
|
||||
});
|
||||
}
|
||||
|
||||
/// 通过按钮触发的导航(无震动)
|
||||
void _navigateToRecordPage() {
|
||||
if (_isNavigating) return;
|
||||
|
||||
_isNavigating = true;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const RecordPage(),
|
||||
maintainState: false,
|
||||
),
|
||||
).then((_) {
|
||||
_isNavigating = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_accelerometerSubscription?.cancel();
|
||||
super.dispose();
|
||||
/// 删除记录
|
||||
void _deleteRecord(String recordId) async {
|
||||
await _dbService.deleteRecord(recordId);
|
||||
await _loadRecords();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -100,17 +115,21 @@ class _HomePageState extends State<HomePage> {
|
||||
appBar: AppBar(
|
||||
title: const Text('记账本'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: _navigateToRecordPage,
|
||||
child: const Text('记账'),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RecordList(
|
||||
records: records,
|
||||
onRecordTap: _navigateToRecordPage,
|
||||
onRecordDelete: _deleteRecord,
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _navigateToRecordPage(),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_accelerometerSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -1,32 +1,322 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/record.dart';
|
||||
import '../data/categories.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
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
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
Navigator.of(context).pop();
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('记账'),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(text: '支出'),
|
||||
Tab(text: '收入'),
|
||||
],
|
||||
),
|
||||
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://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -89,6 +97,14 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -153,7 +169,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
@ -205,6 +221,30 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -229,6 +269,14 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -245,6 +293,22 @@ packages:
|
||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -295,4 +359,4 @@ packages:
|
||||
version: "1.1.2"
|
||||
sdks:
|
||||
dart: ">=3.2.5 <4.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
flutter: ">=3.7.0"
|
||||
|
||||
@ -32,6 +32,9 @@ dependencies:
|
||||
sdk: flutter
|
||||
sensors_plus: ^1.4.1 # 用于检测手机摇晃
|
||||
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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user