diff --git a/lib/main.dart b/lib/main.dart index 4cc1db6..f50f0c9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import './pages/home_page.dart'; +import './pages/record_page.dart'; import './pages/statistics_page.dart'; import './pages/profile_page.dart'; @@ -38,7 +38,7 @@ class _MainPageState extends State { // 页面列表 final List _pages = [ - const HomePage(), + const RecordPage(), const StatisticsPage(), const ProfilePage(), ]; diff --git a/lib/pages/edit_page.dart b/lib/pages/edit_page.dart new file mode 100644 index 0000000..a873544 --- /dev/null +++ b/lib/pages/edit_page.dart @@ -0,0 +1,532 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../models/record.dart'; +import '../data/categories.dart'; +import 'package:uuid/uuid.dart'; +import 'package:decimal/decimal.dart'; + +class EditPage extends StatefulWidget { + final Record? record; + final Function(Record) onSave; // 添加保存回调 + + const EditPage({ + Key? key, + this.record, + required this.onSave, + }) : super(key: key); + + @override + State createState() => _EditPageState(); +} + +class _EditPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + String? _selectedCategoryId; + String _note = ''; + double _amount = 0.0; + DateTime _selectedDate = DateTime.now(); + final _uuid = const Uuid(); + String _inputBuffer = '0.0'; // 用于显示的字符串 + String? _pendingOperation; // 等待执行的运算符 + String? _pendingValue; // 等待计算的第一个值 + String? _inputValue; // 当前输入的值 + + final d = (String s) => Decimal.parse(s); + + @override + void initState() { + super.initState(); + // 先初始化 TabController + _tabController = TabController( + length: 2, + vsync: this, + initialIndex: widget.record?.type == RecordType.income ? 1 : 0, + ); + _tabController.addListener(_onTabChanged); + + // 初始化数据 + if (widget.record != null) { + // 编辑模式:初始化现有记录的数据 + _selectedCategoryId = widget.record!.categoryId; + _note = widget.record!.note ?? ''; + _amount = widget.record!.amount; + _selectedDate = widget.record!.createTime; + // 设置输入缓冲区为当前金额的格式化字符串 + _inputValue = _formatNumberForDisplay(widget.record!.amount); + } else { + // 新建模式:默认选中第一个分类 + _selectedCategoryId = _currentCategories.first.id; + } + } + + /// 标签切换监听 + void _onTabChanged() { + setState(() { + // 切换类型时选中新类型的第一个分类 + _selectedCategoryId = _currentCategories.first.id; + }); + } + + /// 获取当前记录类型 + RecordType get _currentType => + _tabController.index == 0 ? RecordType.expense : RecordType.income; + + /// 获取当前分类列表 + List get _currentCategories => CategoryConfig.getCategoriesByType(_currentType); + + /// 保存记录 + 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('输入金额请大于0')), + ); + 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(); + } + + /// 显示金额 + String get _displayAmount { + if (_pendingOperation != null || + _pendingValue != null || + _inputValue != null) { + _inputBuffer = + '${_pendingValue ?? ""} ${_pendingOperation ?? ""} ${_inputValue ?? ""}'; + } else { + _inputBuffer = '0.0'; + } + return _inputBuffer; + } + + /// 处理数字输入 + void _handleNumber(String digit) { + setState(() { + if (_inputValue == '0' || _inputValue == null) { + // 当前是0 + _inputValue = digit; + } else { + // 其他情况追加数字 + _inputValue = _inputValue! + digit; + } + }); + } + + /// 处理小数点 + void _handleDecimal() { + setState(() { + if (_inputValue == null || _inputValue == '0') { + _inputValue = '0.'; + } else if (_inputValue!.contains('.')) { + HapticFeedback.heavyImpact(); + } else { + _inputValue = '$_inputValue.'; + } + }); + } + + /// 处理删除 + void _handleDelete() { + setState(() { + if (_inputValue != null) { + if (_inputValue!.length > 1) { + _inputValue = _inputValue!.substring(0, _inputValue!.length - 1); + } else { + _inputValue = null; + } + } else if (_pendingOperation != null) { + _pendingOperation = null; + _inputValue = _pendingValue; + _pendingValue = null; + } else if (_pendingValue != null) { + if (_pendingValue!.length > 1) { + _pendingValue = + _pendingValue!.substring(0, _pendingValue!.length - 1); + } else { + _pendingValue = null; + } + } else { + _pendingValue = null; + } + }); + } + + /// 处理运算符 + void _handleOperator(String operator) { + // 如果有待处理的运算,先计算结果 + if (_pendingOperation != null) { + // 先保存当前运算符,因为计算可能会重置状态 + final newOperator = operator; + _calculateResult(); + _pendingValue = _inputValue; + _inputValue = null; + + // 计算完成后,设置新的运算符 + setState(() { + // 如果计算导致重置(结果为0),不添加新的运算符 + if (_inputBuffer == '0.0') { + return; + } else { + _pendingOperation = newOperator; + } + }); + } else if (_inputValue != null ) { + // 正常设置运算符 + setState(() { + _pendingOperation = operator; + _pendingValue = _inputValue!; + _inputValue = null; + }); + } else { + HapticFeedback.heavyImpact(); + } + } + + /// 计算结果 + void _calculateResult() { + + double result = 0; + if(_pendingValue == null && _inputValue== null){ + return; + } + else if (_pendingOperation != null && + _pendingValue != null && + _inputValue != null) { + + switch (_pendingOperation) { + case '+': + result = (d(_pendingValue!) + d(_inputValue!)).toDouble(); + if (result == 0) { + _resetToDefault(); + return; + } + break; + case '-': + result = (d(_pendingValue!) - d(_inputValue!)).toDouble(); + if (result <= 0) { + _resetToDefault(); + return; + } + break; + default: + return; + } + } else { + result = double.parse(_inputValue ?? _pendingValue!); + } + + setState(() { + // 格式化结果,去掉不必要的小数位 + _pendingValue = null; + _amount = result; + _pendingOperation = null; + _inputValue = _formatNumberForDisplay(result); + }); + } + + /// 重置到默认状态 + void _resetToDefault() { + setState(() { + _inputBuffer = '0.0'; + _amount = 0; + _pendingOperation = null; + _pendingValue = null; + _inputValue = null; + }); + } + + /// 格式化数字用于显示 + String _formatNumberForDisplay(double number) { + // 如果是整数,直接返回整数部分 + if (number % 1 == 0) { + return number.toInt().toString(); + } + // 如果是小数,去掉末尾的0 + String numStr = number.toString(); + while (numStr.endsWith('0')) { + numStr = numStr.substring(0, numStr.length - 1); + } + if (numStr.endsWith('.')) { + numStr = numStr.substring(0, numStr.length - 1); + } + return numStr; + } + + /// 修改键盘按钮处理逻辑 + void _onKeyboardButtonPressed(String value) { + if (_inputBuffer.length >= 20 && value != '删除' && value != '保存'&& value != 'again'){ + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('输入数字过大')), + ); + return; + } + switch (value) { + case '删除': + _handleDelete(); + break; + case '保存': + _calculateResult(); + _saveRecord(); + break; + case 'again': + // TODO: 实现again功能 + break; + case '+': + case '-': + _handleOperator(value); + break; + case '.': + _handleDecimal(); + break; + default: + _handleNumber(value); + } + } + + /// 格式化日期显示 + String _formatDate(DateTime date) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final dateToCheck = DateTime(date.year, date.month, date.day); + final difference = today.difference(dateToCheck).inDays; + + // 检查是否是今天、昨天、前天 + switch (difference) { + case 0: + return '今天'; + case 1: + return '昨天'; + case 2: + return '前天'; + default: + // 如果是当年 + if (date.year == now.year) { + return '${date.month}/${date.day}'; + } + // 不是当年 + return '${date.year}/${date.month}/${date.day}'; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: '支出'), + Tab(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: [ + // 备注和金额 + _buildNoteAndAmount(), + // 配置按钮 + 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(_formatDate(_selectedDate)), + ), + 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, + ), + ), + ); + } + + /// 构建底部编辑区域中的备注和金额行 + Widget _buildNoteAndAmount() { + return Row( + children: [ + Expanded( + child: TextField( + controller: TextEditingController(text: _note), // 使用控制器设置初始值 + decoration: const InputDecoration( + hintText: '添加备注', + border: InputBorder.none, + ), + onChanged: (value) => setState(() => _note = value), + ), + ), + Text( + _displayAmount, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 316cad3..256f6ce 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -3,7 +3,7 @@ import 'package:sensors_plus/sensors_plus.dart'; import 'package:vibration/vibration.dart'; import 'dart:async'; import '../models/record.dart'; -import './record_page.dart'; +import 'edit_page.dart'; import '../widgets/record_list.dart'; import '../services/database_service.dart'; import '../widgets/weekly_spending_chart.dart'; @@ -152,11 +152,11 @@ class _HomePageState extends State { /// 导航到记账页面 void _navigateToRecordPage([Record? record]) { - _isNavigating = true; // 设置导航标志 + _isNavigating = true; Navigator.push( context, MaterialPageRoute( - builder: (context) => RecordPage( + builder: (context) => RecordEditPage( record: record, onSave: (newRecord) async { if (record != null) { diff --git a/lib/pages/record_page.dart b/lib/pages/record_page.dart index 86a0a31..c713304 100644 --- a/lib/pages/record_page.dart +++ b/lib/pages/record_page.dart @@ -1,327 +1,228 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:sensors_plus/sensors_plus.dart'; +import 'package:vibration/vibration.dart'; +import 'dart:async'; import '../models/record.dart'; -import '../data/categories.dart'; -import 'package:uuid/uuid.dart'; -import 'package:decimal/decimal.dart'; +import 'edit_page.dart'; +import '../widgets/record_list.dart'; +import '../services/database_service.dart'; +import '../widgets/weekly_spending_chart.dart'; +import '../widgets/monthly_summary_card.dart'; class RecordPage extends StatefulWidget { - final Record? record; - final Function(Record) onSave; // 添加保存回调 - - const RecordPage({ - Key? key, - this.record, - required this.onSave, - }) : super(key: key); + const RecordPage({Key? key}) : super(key: key); @override State createState() => _RecordPageState(); } -class _RecordPageState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - String? _selectedCategoryId; - String _note = ''; - double _amount = 0.0; - DateTime _selectedDate = DateTime.now(); - final _uuid = const Uuid(); - String _inputBuffer = '0.0'; // 用于显示的字符串 - String? _pendingOperation; // 等待执行的运算符 - String? _pendingValue; // 等待计算的第一个值 - String? _inputValue; // 当前输入的值 +class _RecordPageState extends State { + StreamSubscription? _accelerometerSubscription; + DateTime? _lastShakeTime; + final List _recentXValues = []; // 存储原始x轴加速度值(不取绝对值) + bool _isNavigating = false; + final _dbService = DatabaseService(); + List records = []; + DateTime _selectedMonth = DateTime.now(); // 添加选中月份状态 + bool _isAmountVisible = true; // 添加可视状态 - final d = (String s) => Decimal.parse(s); + // 定义摇晃检测的常量 + static const double _shakeThreshold = 8.0; // 单次摇晃的加速度阈值 + static const double _directionThreshold = 2.0; // 方向改变的最小加速 + static const int _sampleSize = 3; // 采样数量 + static const Duration _shakeWindow = Duration(milliseconds: 800); + static const Duration _cooldown = Duration(milliseconds: 50); + + /// 检测有效的摇晃 + bool _isValidShake() { + if (_recentXValues.length < _sampleSize) return false; + + // 检查是否有足够大的加速度 + bool hasStrongShake = _recentXValues.any((x) => x.abs() > _shakeThreshold); + if (!hasStrongShake) return false; + + // 检查方向变化 + bool hasDirectionChange = false; + for (int i = 1; i < _recentXValues.length; i++) { + // 如果相邻两个值的符号相反,且都超过方向阈值,说明发生了方向改变 + if (_recentXValues[i].abs() > _directionThreshold && + _recentXValues[i - 1].abs() > _directionThreshold && + _recentXValues[i].sign != _recentXValues[i - 1].sign) { + hasDirectionChange = true; + break; + } + } + + return hasDirectionChange; + } @override void initState() { super.initState(); - // 先初始化 TabController - _tabController = TabController( - length: 2, - vsync: this, - initialIndex: widget.record?.type == RecordType.income ? 1 : 0, - ); - _tabController.addListener(_onTabChanged); - - // 初始化数据 - if (widget.record != null) { - // 编辑模式:初始化现有记录的数据 - _selectedCategoryId = widget.record!.categoryId; - _note = widget.record!.note ?? ''; - _amount = widget.record!.amount; - _selectedDate = widget.record!.createTime; - // 设置输入缓冲区为当前金额的格式化字符串 - _inputValue = _formatNumberForDisplay(widget.record!.amount); - } else { - // 新建模式:默认选中第一个分类 - _selectedCategoryId = _currentCategories.first.id; - } + _initShakeDetection(); + _loadRecords(); } - /// 标签切换监听 - void _onTabChanged() { - setState(() { - // 切换类型时选中新类型的第一个分类 - _selectedCategoryId = _currentCategories.first.id; - }); - } - - /// 获取当前记录类型 - RecordType get _currentType => - _tabController.index == 0 ? RecordType.expense : RecordType.income; - - /// 获取当前分类列表 - List get _currentCategories => CategoryConfig.getCategoriesByType(_currentType); - - /// 保存记录 - 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('输入金额请大于0')), - ); - 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(); - } - - /// 显示金额 - String get _displayAmount { - if (_pendingOperation != null || - _pendingValue != null || - _inputValue != null) { - _inputBuffer = - '${_pendingValue ?? ""} ${_pendingOperation ?? ""} ${_inputValue ?? ""}'; - } else { - _inputBuffer = '0.0'; - } - return _inputBuffer; - } - - /// 处理数字输入 - void _handleNumber(String digit) { - setState(() { - if (_inputValue == '0' || _inputValue == null) { - // 当前是0 - _inputValue = digit; - } else { - // 其他情况追加数字 - _inputValue = _inputValue! + digit; - } - }); - } - - /// 处理小数点 - void _handleDecimal() { - setState(() { - if (_inputValue == null || _inputValue == '0') { - _inputValue = '0.'; - } else if (_inputValue!.contains('.')) { - HapticFeedback.heavyImpact(); - } else { - _inputValue = '$_inputValue.'; - } - }); - } - - /// 处理删除 - void _handleDelete() { - setState(() { - if (_inputValue != null) { - if (_inputValue!.length > 1) { - _inputValue = _inputValue!.substring(0, _inputValue!.length - 1); - } else { - _inputValue = null; - } - } else if (_pendingOperation != null) { - _pendingOperation = null; - _inputValue = _pendingValue; - _pendingValue = null; - } else if (_pendingValue != null) { - if (_pendingValue!.length > 1) { - _pendingValue = - _pendingValue!.substring(0, _pendingValue!.length - 1); - } else { - _pendingValue = null; - } - } else { - _pendingValue = null; - } - }); - } - - /// 处理运算符 - void _handleOperator(String operator) { - // 如果有待处理的运算,先计算结果 - if (_pendingOperation != null) { - // 先保存当前运算符,因为计算可能会重置状态 - final newOperator = operator; - _calculateResult(); - _pendingValue = _inputValue; - _inputValue = null; - - // 计算完成后,设置新的运算符 - setState(() { - // 如果计算导致重置(结果为0),不添加新的运算符 - if (_inputBuffer == '0.0') { - return; - } else { - _pendingOperation = newOperator; - } - }); - } else if (_inputValue != null ) { - // 正常设置运算符 - setState(() { - _pendingOperation = operator; - _pendingValue = _inputValue!; - _inputValue = null; - }); - } else { - HapticFeedback.heavyImpact(); - } - } - - /// 计算结果 - void _calculateResult() { - - double result = 0; - if(_pendingValue == null && _inputValue== null){ - return; - } - else if (_pendingOperation != null && - _pendingValue != null && - _inputValue != null) { + /// 重置摇晃检测状态 + void _resetShakeDetection() { + _lastShakeTime = null; + _recentXValues.clear(); + _isNavigating = false; - switch (_pendingOperation) { - case '+': - result = (d(_pendingValue!) + d(_inputValue!)).toDouble(); - if (result == 0) { - _resetToDefault(); - return; - } - break; - case '-': - result = (d(_pendingValue!) - d(_inputValue!)).toDouble(); - if (result <= 0) { - _resetToDefault(); - return; - } - break; - default: - return; - } - } else { - result = double.parse(_inputValue ?? _pendingValue!); - } - - setState(() { - // 格式化结果,去掉不必要的小数位 - _pendingValue = null; - _amount = result; - _pendingOperation = null; - _inputValue = _formatNumberForDisplay(result); - }); + // 重新初始化传感器监听 + _accelerometerSubscription?.cancel(); + _initShakeDetection(); } - /// 重置到默认状态 - void _resetToDefault() { + /// 加载记录 + Future _loadRecords() async { + // 计算选中月份的起始和结束日期 + final startDate = DateTime(_selectedMonth.year, _selectedMonth.month, 1); + final endDate = DateTime(_selectedMonth.year, _selectedMonth.month + 1, 0); + + final loadedRecords = await _dbService.getRecordsByDateRange( + startDate, + endDate, + ); + setState(() { - _inputBuffer = '0.0'; - _amount = 0; - _pendingOperation = null; - _pendingValue = null; - _inputValue = null; + records = loadedRecords; + records.sort((a, b) => b.createTime.compareTo(a.createTime)); }); } - /// 格式化数字用于显示 - String _formatNumberForDisplay(double number) { - // 如果是整数,直接返回整数部分 - if (number % 1 == 0) { - return number.toInt().toString(); - } - // 如果是小数,去掉末尾的0 - String numStr = number.toString(); - while (numStr.endsWith('0')) { - numStr = numStr.substring(0, numStr.length - 1); - } - if (numStr.endsWith('.')) { - numStr = numStr.substring(0, numStr.length - 1); - } - return numStr; - } - - /// 修改键盘按钮处理逻辑 - void _onKeyboardButtonPressed(String value) { - if (_inputBuffer.length >= 20 && value != '删除' && value != '保存'&& value != 'again'){ - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('输入数字过大')), - ); - return; - } - switch (value) { - case '删除': - _handleDelete(); - break; - case '保存': - _calculateResult(); - _saveRecord(); - break; - case 'again': - // TODO: 实现again功能 - break; - case '+': - case '-': - _handleOperator(value); - break; - case '.': - _handleDecimal(); - break; - default: - _handleNumber(value); - } - } - - /// 格式化日期显示 - String _formatDate(DateTime date) { - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - final dateToCheck = DateTime(date.year, date.month, date.day); - final difference = today.difference(dateToCheck).inDays; - - // 检查是否是今天、昨天、前天 - switch (difference) { - case 0: - return '今天'; - case 1: - return '昨天'; - case 2: - return '前天'; - default: - // 如果是当年 - if (date.year == now.year) { - return '${date.month}/${date.day}'; + /// 初始化摇晃检测 + void _initShakeDetection() { + _accelerometerSubscription = accelerometerEvents.listen( + (event) { + if (_isNavigating) return; // 如果正在导航,忽略摇晃检测 + + final now = DateTime.now(); + + // 添加新的x轴值(保留正负号) + _recentXValues.add(event.x); + if (_recentXValues.length > _sampleSize) { + _recentXValues.removeAt(0); } - // 不是当年 - return '${date.year}/${date.month}/${date.day}'; + + // 检查是否处于冷却期 + if (_lastShakeTime != null && + now.difference(_lastShakeTime!) < _cooldown) { + return; + } + + // 检测有效的摇晃 + if (_isValidShake()) { + if (_lastShakeTime == null) { + _lastShakeTime = now; + _recentXValues.clear(); + } else { + final timeDiff = now.difference(_lastShakeTime!); + if (timeDiff < _shakeWindow) { + _navigateToRecordPageWithVibration(); + _lastShakeTime = null; + _recentXValues.clear(); + } else { + _lastShakeTime = now; + _recentXValues.clear(); + } + } + } + }, + onError: (error) { + debugPrint('Accelerometer error: $error'); + _resetShakeDetection(); // 发生错误时重置检测 + }, + cancelOnError: false, // 错误时不取消订阅,而是重置 + ); + } + + /// 带震动的记账页面导航 + void _navigateToRecordPageWithVibration() async { + if (_isNavigating) return; + + _isNavigating = true; + // 添加震动反馈 + if (await Vibration.hasVibrator() ?? false) { + Vibration.vibrate(duration: 200); + } + + _navigateToRecordPage(); + } + + /// 导航到记账页面 + void _navigateToRecordPage([Record? record]) { + _isNavigating = true; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EditPage( + record: record, + onSave: (newRecord) async { + if (record != null) { + await _dbService.updateRecord(newRecord); + } else { + await _dbService.insertRecord(newRecord); + } + await _loadRecords(); + }, + ), + maintainState: false, + ), + ).then((_) { + // 返回时重置检测状态 + _resetShakeDetection(); + }); + } + + /// 删除记录 + void _deleteRecord(String recordId) async { + await _dbService.deleteRecord(recordId); + await _loadRecords(); + } + + // 添加月份选择器组件 + Widget _buildMonthSelector() { + return GestureDetector( + onTap: () => _showMonthPicker(), + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8.0), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${_selectedMonth.year}年${_selectedMonth.month}月', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ); + } + + // 显示月份选择器 + Future _showMonthPicker() async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedMonth, + firstDate: DateTime(2000), + lastDate: DateTime(2100), + initialDatePickerMode: DatePickerMode.year, + ); + + if (picked != null) { + setState(() { + _selectedMonth = DateTime(picked.year, picked.month); + }); + _loadRecords(); // 重新加载数据 } } @@ -329,204 +230,40 @@ class _RecordPageState extends State Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: TabBar( - controller: _tabController, - tabs: const [ - Tab(text: '支出'), - Tab(text: '收入'), - ], - ), + title: const Text('记账本'), ), body: Column( children: [ - // 分类网格 + Padding( + padding: const EdgeInsets.all(16.0), + child: _buildMonthSelector(), + ), + MonthlySummaryCard( + records: records, + isVisible: _isAmountVisible, + onAddRecord: () => _navigateToRecordPage(), + onVisibilityChanged: (value) { + setState(() { + _isAmountVisible = value; + }); + }, + ), + WeeklySpendingChart(records: records), 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); - }, + child: RecordList( + records: records, + onRecordTap: _navigateToRecordPage, + onRecordDelete: _deleteRecord, ), ), - // 底部编辑区域 - _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: [ - // 备注和金额 - _buildNoteAndAmount(), - // 配置按钮 - 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(_formatDate(_selectedDate)), - ), - 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, - ), - ), - ); - } - - /// 构建底部编辑区域中的备注和金额行 - Widget _buildNoteAndAmount() { - return Row( - children: [ - Expanded( - child: TextField( - controller: TextEditingController(text: _note), // 使用控制器设置初始值 - decoration: const InputDecoration( - hintText: '添加备注', - border: InputBorder.none, - ), - onChanged: (value) => setState(() => _note = value), - ), - ), - Text( - _displayAmount, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ], ); } @override void dispose() { - _tabController.dispose(); + _accelerometerSubscription?.cancel(); super.dispose(); } -} +}