页面改名字
This commit is contained in:
parent
b665409d65
commit
b479dbe519
@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import './pages/home_page.dart';
|
import './pages/record_page.dart';
|
||||||
import './pages/statistics_page.dart';
|
import './pages/statistics_page.dart';
|
||||||
import './pages/profile_page.dart';
|
import './pages/profile_page.dart';
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ class _MainPageState extends State<MainPage> {
|
|||||||
|
|
||||||
// 页面列表
|
// 页面列表
|
||||||
final List<Widget> _pages = [
|
final List<Widget> _pages = [
|
||||||
const HomePage(),
|
const RecordPage(),
|
||||||
const StatisticsPage(),
|
const StatisticsPage(),
|
||||||
const ProfilePage(),
|
const ProfilePage(),
|
||||||
];
|
];
|
||||||
|
|||||||
532
lib/pages/edit_page.dart
Normal file
532
lib/pages/edit_page.dart
Normal file
@ -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<EditPage> createState() => _EditPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditPageState extends State<EditPage>
|
||||||
|
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<Category> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ 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 '../models/record.dart';
|
||||||
import './record_page.dart';
|
import 'edit_page.dart';
|
||||||
import '../widgets/record_list.dart';
|
import '../widgets/record_list.dart';
|
||||||
import '../services/database_service.dart';
|
import '../services/database_service.dart';
|
||||||
import '../widgets/weekly_spending_chart.dart';
|
import '../widgets/weekly_spending_chart.dart';
|
||||||
@ -152,11 +152,11 @@ class _HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
/// 导航到记账页面
|
/// 导航到记账页面
|
||||||
void _navigateToRecordPage([Record? record]) {
|
void _navigateToRecordPage([Record? record]) {
|
||||||
_isNavigating = true; // 设置导航标志
|
_isNavigating = true;
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => RecordPage(
|
builder: (context) => RecordEditPage(
|
||||||
record: record,
|
record: record,
|
||||||
onSave: (newRecord) async {
|
onSave: (newRecord) async {
|
||||||
if (record != null) {
|
if (record != null) {
|
||||||
|
|||||||
@ -1,327 +1,228 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 '../models/record.dart';
|
||||||
import '../data/categories.dart';
|
import 'edit_page.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import '../widgets/record_list.dart';
|
||||||
import 'package:decimal/decimal.dart';
|
import '../services/database_service.dart';
|
||||||
|
import '../widgets/weekly_spending_chart.dart';
|
||||||
|
import '../widgets/monthly_summary_card.dart';
|
||||||
|
|
||||||
class RecordPage extends StatefulWidget {
|
class RecordPage extends StatefulWidget {
|
||||||
final Record? record;
|
const RecordPage({Key? key}) : super(key: key);
|
||||||
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 {
|
StreamSubscription? _accelerometerSubscription;
|
||||||
late TabController _tabController;
|
DateTime? _lastShakeTime;
|
||||||
String? _selectedCategoryId;
|
final List<double> _recentXValues = []; // 存储原始x轴加速度值(不取绝对值)
|
||||||
String _note = '';
|
bool _isNavigating = false;
|
||||||
double _amount = 0.0;
|
final _dbService = DatabaseService();
|
||||||
DateTime _selectedDate = DateTime.now();
|
List<Record> records = [];
|
||||||
final _uuid = const Uuid();
|
DateTime _selectedMonth = DateTime.now(); // 添加选中月份状态
|
||||||
String _inputBuffer = '0.0'; // 用于显示的字符串
|
bool _isAmountVisible = true; // 添加可视状态
|
||||||
String? _pendingOperation; // 等待执行的运算符
|
|
||||||
String? _pendingValue; // 等待计算的第一个值
|
|
||||||
String? _inputValue; // 当前输入的值
|
|
||||||
|
|
||||||
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// 先初始化 TabController
|
_initShakeDetection();
|
||||||
_tabController = TabController(
|
_loadRecords();
|
||||||
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() {
|
void _resetShakeDetection() {
|
||||||
setState(() {
|
_lastShakeTime = null;
|
||||||
// 切换类型时选中新类型的第一个分类
|
_recentXValues.clear();
|
||||||
_selectedCategoryId = _currentCategories.first.id;
|
_isNavigating = false;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 获取当前记录类型
|
|
||||||
RecordType get _currentType =>
|
|
||||||
_tabController.index == 0 ? RecordType.expense : RecordType.income;
|
|
||||||
|
|
||||||
/// 获取当前分类列表
|
|
||||||
List<Category> 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 '+':
|
_accelerometerSubscription?.cancel();
|
||||||
result = (d(_pendingValue!) + d(_inputValue!)).toDouble();
|
_initShakeDetection();
|
||||||
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() {
|
Future<void> _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(() {
|
setState(() {
|
||||||
_inputBuffer = '0.0';
|
records = loadedRecords;
|
||||||
_amount = 0;
|
records.sort((a, b) => b.createTime.compareTo(a.createTime));
|
||||||
_pendingOperation = null;
|
|
||||||
_pendingValue = null;
|
|
||||||
_inputValue = null;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 格式化数字用于显示
|
/// 初始化摇晃检测
|
||||||
String _formatNumberForDisplay(double number) {
|
void _initShakeDetection() {
|
||||||
// 如果是整数,直接返回整数部分
|
_accelerometerSubscription = accelerometerEvents.listen(
|
||||||
if (number % 1 == 0) {
|
(event) {
|
||||||
return number.toInt().toString();
|
if (_isNavigating) return; // 如果正在导航,忽略摇晃检测
|
||||||
}
|
|
||||||
// 如果是小数,去掉末尾的0
|
final now = DateTime.now();
|
||||||
String numStr = number.toString();
|
|
||||||
while (numStr.endsWith('0')) {
|
// 添加新的x轴值(保留正负号)
|
||||||
numStr = numStr.substring(0, numStr.length - 1);
|
_recentXValues.add(event.x);
|
||||||
}
|
if (_recentXValues.length > _sampleSize) {
|
||||||
if (numStr.endsWith('.')) {
|
_recentXValues.removeAt(0);
|
||||||
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}';
|
// 检查是否处于冷却期
|
||||||
|
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<void> _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<RecordPage>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: TabBar(
|
title: const Text('记账本'),
|
||||||
controller: _tabController,
|
|
||||||
tabs: const [
|
|
||||||
Tab(text: '支出'),
|
|
||||||
Tab(text: '收入'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
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(
|
Expanded(
|
||||||
child: GridView.builder(
|
child: RecordList(
|
||||||
padding: const EdgeInsets.all(16),
|
records: records,
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
onRecordTap: _navigateToRecordPage,
|
||||||
crossAxisCount: 4,
|
onRecordDelete: _deleteRecord,
|
||||||
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tabController.dispose();
|
_accelerometerSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user