530 lines
14 KiB
Dart
530 lines
14 KiB
Dart
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 RecordPage extends StatefulWidget {
|
||
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>
|
||
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;
|
||
// 设置输入缓冲区为当前金额的格式化字符串
|
||
_inputBuffer = _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 =>
|
||
_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('输入金额请大于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;
|
||
} 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();
|
||
|
||
// 计算完成后,设置新的运算符
|
||
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 = _formatNumberForDisplay(result);
|
||
_amount = result;
|
||
_pendingOperation = null;
|
||
_inputValue = null;
|
||
});
|
||
}
|
||
|
||
/// 重置到默认状态
|
||
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();
|
||
}
|
||
}
|