计算逻辑bug修复 记录分类icon拆分
This commit is contained in:
parent
2a2857132b
commit
bd8eee3bdc
@ -1,54 +1,200 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import '../models/record.dart';
|
import '../models/record.dart';
|
||||||
|
|
||||||
/// 预设支出分类
|
/// 分类配置
|
||||||
final List<Category> expenseCategories = [
|
class CategoryConfig {
|
||||||
Category(
|
static const Map<String, Map<String, dynamic>> expenseCategories = {
|
||||||
id: 'food',
|
'food': {
|
||||||
name: '餐饮',
|
'name': '餐饮',
|
||||||
icon: Icons.restaurant,
|
'iconKey': 'food',
|
||||||
type: RecordType.expense,
|
'sortOrder': 0,
|
||||||
),
|
},
|
||||||
Category(
|
'shopping': {
|
||||||
id: 'shopping',
|
'name': '购物',
|
||||||
name: '购物',
|
'iconKey': 'shopping',
|
||||||
icon: Icons.shopping_bag,
|
'sortOrder': 1,
|
||||||
type: RecordType.expense,
|
},
|
||||||
),
|
'transport': {
|
||||||
Category(
|
'name': '交通',
|
||||||
id: 'transport',
|
'iconKey': 'transport',
|
||||||
name: '交通',
|
'sortOrder': 2,
|
||||||
icon: Icons.directions_bus,
|
},
|
||||||
type: RecordType.expense,
|
'entertainment': {
|
||||||
),
|
'name': '娱乐',
|
||||||
Category(
|
'iconKey': 'entertainment',
|
||||||
id: 'entertainment',
|
'sortOrder': 3,
|
||||||
name: '娱乐',
|
},
|
||||||
icon: Icons.sports_esports,
|
'housing': {
|
||||||
type: RecordType.expense,
|
'name': '住房',
|
||||||
),
|
'iconKey': 'housing',
|
||||||
// 可以继续添加更多分类...
|
'sortOrder': 4,
|
||||||
];
|
},
|
||||||
|
'utilities': {
|
||||||
|
'name': '水电',
|
||||||
|
'iconKey': 'utilities',
|
||||||
|
'sortOrder': 5,
|
||||||
|
},
|
||||||
|
'medical': {
|
||||||
|
'name': '医疗',
|
||||||
|
'iconKey': 'medical',
|
||||||
|
'sortOrder': 6,
|
||||||
|
},
|
||||||
|
'education': {
|
||||||
|
'name': '教育',
|
||||||
|
'iconKey': 'education',
|
||||||
|
'sortOrder': 7,
|
||||||
|
},
|
||||||
|
'clothing': {
|
||||||
|
'name': '服饰',
|
||||||
|
'iconKey': 'clothing',
|
||||||
|
'sortOrder': 8,
|
||||||
|
},
|
||||||
|
'travel': {
|
||||||
|
'name': '旅行',
|
||||||
|
'iconKey': 'travel',
|
||||||
|
'sortOrder': 9,
|
||||||
|
},
|
||||||
|
'sports': {
|
||||||
|
'name': '运动',
|
||||||
|
'iconKey': 'sports',
|
||||||
|
'sortOrder': 10,
|
||||||
|
},
|
||||||
|
'beauty': {
|
||||||
|
'name': '美容',
|
||||||
|
'iconKey': 'beauty',
|
||||||
|
'sortOrder': 11,
|
||||||
|
},
|
||||||
|
'digital': {
|
||||||
|
'name': '数码',
|
||||||
|
'iconKey': 'digital',
|
||||||
|
'sortOrder': 12,
|
||||||
|
},
|
||||||
|
'pets': {
|
||||||
|
'name': '宠物',
|
||||||
|
'iconKey': 'pets',
|
||||||
|
'sortOrder': 13,
|
||||||
|
},
|
||||||
|
'gifts': {
|
||||||
|
'name': '礼物',
|
||||||
|
'iconKey': 'gifts',
|
||||||
|
'sortOrder': 14,
|
||||||
|
},
|
||||||
|
'books': {
|
||||||
|
'name': '书籍',
|
||||||
|
'iconKey': 'books',
|
||||||
|
'sortOrder': 15,
|
||||||
|
},
|
||||||
|
'insurance': {
|
||||||
|
'name': '保险',
|
||||||
|
'iconKey': 'insurance',
|
||||||
|
'sortOrder': 16,
|
||||||
|
},
|
||||||
|
'children': {
|
||||||
|
'name': '育儿',
|
||||||
|
'iconKey': 'children',
|
||||||
|
'sortOrder': 17,
|
||||||
|
},
|
||||||
|
'social': {
|
||||||
|
'name': '社交',
|
||||||
|
'iconKey': 'social',
|
||||||
|
'sortOrder': 18,
|
||||||
|
},
|
||||||
|
'car': {
|
||||||
|
'name': '汽车',
|
||||||
|
'iconKey': 'car',
|
||||||
|
'sortOrder': 19,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/// 预设收入分类
|
static const Map<String, Map<String, dynamic>> incomeCategories = {
|
||||||
final List<Category> incomeCategories = [
|
'salary': {
|
||||||
Category(
|
'name': '工资',
|
||||||
id: 'salary',
|
'iconKey': 'salary',
|
||||||
name: '工资',
|
'sortOrder': 0,
|
||||||
icon: Icons.account_balance_wallet,
|
},
|
||||||
type: RecordType.income,
|
'bonus': {
|
||||||
),
|
'name': '奖金',
|
||||||
Category(
|
'iconKey': 'bonus',
|
||||||
id: 'bonus',
|
'sortOrder': 1,
|
||||||
name: '奖金',
|
},
|
||||||
icon: Icons.card_giftcard,
|
'investment': {
|
||||||
type: RecordType.income,
|
'name': '投资',
|
||||||
),
|
'iconKey': 'investment',
|
||||||
Category(
|
'sortOrder': 2,
|
||||||
id: 'investment',
|
},
|
||||||
name: '投资',
|
'partTime': {
|
||||||
icon: Icons.trending_up,
|
'name': '兼职',
|
||||||
type: RecordType.income,
|
'iconKey': 'partTime',
|
||||||
),
|
'sortOrder': 3,
|
||||||
// 可以继续添加更多分类...
|
},
|
||||||
];
|
'dividend': {
|
||||||
|
'name': '分红',
|
||||||
|
'iconKey': 'dividend',
|
||||||
|
'sortOrder': 4,
|
||||||
|
},
|
||||||
|
'rental': {
|
||||||
|
'name': '租金',
|
||||||
|
'iconKey': 'rental',
|
||||||
|
'sortOrder': 5,
|
||||||
|
},
|
||||||
|
'refund': {
|
||||||
|
'name': '报销',
|
||||||
|
'iconKey': 'refund',
|
||||||
|
'sortOrder': 6,
|
||||||
|
},
|
||||||
|
'lottery': {
|
||||||
|
'name': '中奖',
|
||||||
|
'iconKey': 'lottery',
|
||||||
|
'sortOrder': 7,
|
||||||
|
},
|
||||||
|
'gifts_received': {
|
||||||
|
'name': '礼金',
|
||||||
|
'iconKey': 'gifts_received',
|
||||||
|
'sortOrder': 8,
|
||||||
|
},
|
||||||
|
'pension': {
|
||||||
|
'name': '养老金',
|
||||||
|
'iconKey': 'pension',
|
||||||
|
'sortOrder': 9,
|
||||||
|
},
|
||||||
|
'interest': {
|
||||||
|
'name': '利息',
|
||||||
|
'iconKey': 'interest',
|
||||||
|
'sortOrder': 10,
|
||||||
|
},
|
||||||
|
'business': {
|
||||||
|
'name': '经营',
|
||||||
|
'iconKey': 'business',
|
||||||
|
'sortOrder': 11,
|
||||||
|
},
|
||||||
|
'royalties': {
|
||||||
|
'name': '版权',
|
||||||
|
'iconKey': 'royalties',
|
||||||
|
'sortOrder': 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 获取分类配置
|
||||||
|
static List<Category> getCategoriesByType(RecordType type) {
|
||||||
|
final map = type == RecordType.expense ? expenseCategories : incomeCategories;
|
||||||
|
return map.entries.map((entry) => Category(
|
||||||
|
id: entry.key,
|
||||||
|
name: entry.value['name'],
|
||||||
|
iconKey: entry.value['iconKey'],
|
||||||
|
type: type,
|
||||||
|
sortOrder: entry.value['sortOrder'],
|
||||||
|
)).toList()
|
||||||
|
..sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 转换为JSON格式,方便存储和同步
|
||||||
|
static Map<String, dynamic> toJson() => {
|
||||||
|
'expense': expenseCategories,
|
||||||
|
'income': incomeCategories,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 从JSON格式转换回对象
|
||||||
|
static CategoryConfig fromJson(Map<String, dynamic> json) {
|
||||||
|
// TODO: 实现从JSON恢复配置的逻辑
|
||||||
|
return CategoryConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
49
lib/data/icons.dart
Normal file
49
lib/data/icons.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// 预设图标映射表
|
||||||
|
class IconMapping {
|
||||||
|
static const Map<String, IconData> icons = {
|
||||||
|
// 支出类图标
|
||||||
|
'food': Icons.restaurant,
|
||||||
|
'shopping': Icons.shopping_cart,
|
||||||
|
'transport': Icons.directions_bus,
|
||||||
|
'entertainment': Icons.sports_esports,
|
||||||
|
'medical': Icons.local_hospital,
|
||||||
|
'education': Icons.school,
|
||||||
|
'housing': Icons.home,
|
||||||
|
'utilities': Icons.power,
|
||||||
|
'clothing': Icons.checkroom,
|
||||||
|
'travel': Icons.flight,
|
||||||
|
'sports': Icons.sports_basketball,
|
||||||
|
'beauty': Icons.face,
|
||||||
|
'digital': Icons.devices,
|
||||||
|
'pets': Icons.pets,
|
||||||
|
'gifts': Icons.card_giftcard,
|
||||||
|
'books': Icons.book,
|
||||||
|
'insurance': Icons.security,
|
||||||
|
'children': Icons.child_care,
|
||||||
|
'social': Icons.people,
|
||||||
|
'car': Icons.directions_car,
|
||||||
|
|
||||||
|
// 收入类图标
|
||||||
|
'salary': Icons.account_balance_wallet,
|
||||||
|
'bonus': Icons.monetization_on,
|
||||||
|
'investment': Icons.trending_up,
|
||||||
|
'partTime': Icons.work,
|
||||||
|
'refund': Icons.replay,
|
||||||
|
'dividend': Icons.account_balance,
|
||||||
|
'rental': Icons.house,
|
||||||
|
'lottery': Icons.casino,
|
||||||
|
'gifts_received': Icons.redeem,
|
||||||
|
'pension': Icons.elderly,
|
||||||
|
'interest': Icons.savings,
|
||||||
|
'business': Icons.store,
|
||||||
|
'royalties': Icons.copyright,
|
||||||
|
'other': Icons.more_horiz,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 获取图标
|
||||||
|
static IconData getIcon(String iconKey) {
|
||||||
|
return icons[iconKey] ?? Icons.help_outline;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import '../models/record.dart';
|
import '../models/record.dart';
|
||||||
|
import 'categories.dart';
|
||||||
|
|
||||||
final List<Record> mockRecords = [
|
/// 模拟数据
|
||||||
|
final mockRecords = [
|
||||||
Record(
|
Record(
|
||||||
id: '1',
|
id: '1',
|
||||||
type: RecordType.expense,
|
type: RecordType.expense,
|
||||||
@ -14,8 +16,17 @@ final List<Record> mockRecords = [
|
|||||||
type: RecordType.income,
|
type: RecordType.income,
|
||||||
categoryId: 'salary',
|
categoryId: 'salary',
|
||||||
note: '工资',
|
note: '工资',
|
||||||
amount: 5000.0,
|
amount: 8000.0,
|
||||||
createTime: DateTime.now().subtract(const Duration(days: 1)),
|
createTime: DateTime.now().subtract(const Duration(days: 1)),
|
||||||
),
|
),
|
||||||
// 可以添加更多模拟数据...
|
// ... 其他模拟数据
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// 获取分类名称
|
||||||
|
String getCategoryName(String categoryId, RecordType type) {
|
||||||
|
final categories = type == RecordType.expense
|
||||||
|
? CategoryConfig.expenseCategories
|
||||||
|
: CategoryConfig.incomeCategories;
|
||||||
|
|
||||||
|
return categories[categoryId]?['name'] ?? '未知分类';
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../data/icons.dart';
|
||||||
|
|
||||||
/// 记账类型枚举
|
/// 记账类型枚举
|
||||||
enum RecordType {
|
enum RecordType {
|
||||||
@ -10,17 +11,38 @@ enum RecordType {
|
|||||||
class Category {
|
class Category {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
final IconData icon;
|
final String iconKey; // 改为使用图标key
|
||||||
final RecordType type;
|
final RecordType type;
|
||||||
final String? parentId; // 为未来二级分类预留
|
final int sortOrder; // 添加排序字段
|
||||||
|
|
||||||
const Category({
|
const Category({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.icon,
|
required this.iconKey,
|
||||||
required this.type,
|
required this.type,
|
||||||
this.parentId,
|
required this.sortOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取图标
|
||||||
|
IconData get icon => IconMapping.getIcon(iconKey);
|
||||||
|
|
||||||
|
// 转换为JSON格式
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'id': id,
|
||||||
|
'name': name,
|
||||||
|
'iconKey': iconKey,
|
||||||
|
'type': type.index,
|
||||||
|
'sortOrder': sortOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从JSON格式转换回对象
|
||||||
|
factory Category.fromJson(Map<String, dynamic> json) => Category(
|
||||||
|
id: json['id'],
|
||||||
|
name: json['name'],
|
||||||
|
iconKey: json['iconKey'],
|
||||||
|
type: RecordType.values[json['type']],
|
||||||
|
sortOrder: json['sortOrder'],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 记账记录模型
|
/// 记账记录模型
|
||||||
|
|||||||
@ -53,7 +53,7 @@ class _RecordPageState extends State<RecordPage>
|
|||||||
_amount = widget.record!.amount;
|
_amount = widget.record!.amount;
|
||||||
_selectedDate = widget.record!.createTime;
|
_selectedDate = widget.record!.createTime;
|
||||||
// 设置输入缓冲区为当前金额的格式化字符串
|
// 设置输入缓冲区为当前金额的格式化字符串
|
||||||
_inputBuffer = _formatNumberForDisplay(widget.record!.amount);
|
_inputValue = _formatNumberForDisplay(widget.record!.amount);
|
||||||
} else {
|
} else {
|
||||||
// 新建模式:默认选中第一个分类
|
// 新建模式:默认选中第一个分类
|
||||||
_selectedCategoryId = _currentCategories.first.id;
|
_selectedCategoryId = _currentCategories.first.id;
|
||||||
@ -73,8 +73,7 @@ class _RecordPageState extends State<RecordPage>
|
|||||||
_tabController.index == 0 ? RecordType.expense : RecordType.income;
|
_tabController.index == 0 ? RecordType.expense : RecordType.income;
|
||||||
|
|
||||||
/// 获取当前分类列表
|
/// 获取当前分类列表
|
||||||
List<Category> get _currentCategories =>
|
List<Category> get _currentCategories => CategoryConfig.getCategoriesByType(_currentType);
|
||||||
_currentType == RecordType.expense ? expenseCategories : incomeCategories;
|
|
||||||
|
|
||||||
/// 保存记录
|
/// 保存记录
|
||||||
void _saveRecord() {
|
void _saveRecord() {
|
||||||
@ -154,6 +153,8 @@ class _RecordPageState extends State<RecordPage>
|
|||||||
}
|
}
|
||||||
} else if (_pendingOperation != null) {
|
} else if (_pendingOperation != null) {
|
||||||
_pendingOperation = null;
|
_pendingOperation = null;
|
||||||
|
_inputValue = _pendingValue;
|
||||||
|
_pendingValue = null;
|
||||||
} else if (_pendingValue != null) {
|
} else if (_pendingValue != null) {
|
||||||
if (_pendingValue!.length > 1) {
|
if (_pendingValue!.length > 1) {
|
||||||
_pendingValue =
|
_pendingValue =
|
||||||
@ -174,6 +175,8 @@ class _RecordPageState extends State<RecordPage>
|
|||||||
// 先保存当前运算符,因为计算可能会重置状态
|
// 先保存当前运算符,因为计算可能会重置状态
|
||||||
final newOperator = operator;
|
final newOperator = operator;
|
||||||
_calculateResult();
|
_calculateResult();
|
||||||
|
_pendingValue = _inputValue;
|
||||||
|
_inputValue = null;
|
||||||
|
|
||||||
// 计算完成后,设置新的运算符
|
// 计算完成后,设置新的运算符
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -184,7 +187,7 @@ class _RecordPageState extends State<RecordPage>
|
|||||||
_pendingOperation = newOperator;
|
_pendingOperation = newOperator;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (_inputValue != null) {
|
} else if (_inputValue != null ) {
|
||||||
// 正常设置运算符
|
// 正常设置运算符
|
||||||
setState(() {
|
setState(() {
|
||||||
_pendingOperation = operator;
|
_pendingOperation = operator;
|
||||||
@ -231,10 +234,10 @@ class _RecordPageState extends State<RecordPage>
|
|||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
// 格式化结果,去掉不必要的小数位
|
// 格式化结果,去掉不必要的小数位
|
||||||
_pendingValue = _formatNumberForDisplay(result);
|
_pendingValue = null;
|
||||||
_amount = result;
|
_amount = result;
|
||||||
_pendingOperation = null;
|
_pendingOperation = null;
|
||||||
_inputValue = null;
|
_inputValue = _formatNumberForDisplay(result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,533 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import '../models/record.dart';
|
|
||||||
import '../data/categories.dart';
|
|
||||||
import 'package:uuid/uuid.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; // 等待执行的运算符
|
|
||||||
double? _pendingValue; // 等待计算的第一个值
|
|
||||||
bool _isNewInput = true; // 是否开始新的输入
|
|
||||||
|
|
||||||
@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);
|
|
||||||
_isNewInput = false;
|
|
||||||
} 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('请输入金额')),
|
|
||||||
);
|
|
||||||
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) {
|
|
||||||
return '$_pendingValue $_pendingOperation ${_isNewInput ? "" : _inputBuffer}';
|
|
||||||
}
|
|
||||||
return _inputBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理数字输入
|
|
||||||
void _handleNumber(String digit) {
|
|
||||||
setState(() {
|
|
||||||
if (_isNewInput) {
|
|
||||||
// 新输入时直接替换
|
|
||||||
_inputBuffer = digit;
|
|
||||||
_isNewInput = false;
|
|
||||||
} else if (_inputBuffer == '0' && !_inputBuffer.contains('.')) {
|
|
||||||
// 当前是0且没有小数点时,直接替换0
|
|
||||||
_inputBuffer = digit;
|
|
||||||
} else if (_inputBuffer == '0.0') {
|
|
||||||
// 如果是默认状态,直接替换为新数字
|
|
||||||
_inputBuffer = digit;
|
|
||||||
_isNewInput = false;
|
|
||||||
} else {
|
|
||||||
// 其他情况追加数字
|
|
||||||
_inputBuffer = _inputBuffer + digit;
|
|
||||||
}
|
|
||||||
_amount = double.parse(_inputBuffer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理小数点
|
|
||||||
void _handleDecimal() {
|
|
||||||
setState(() {
|
|
||||||
if (!_inputBuffer.contains('.')) {
|
|
||||||
// 如果是默认状态(0.0)或只有0,直接显示0.
|
|
||||||
if (_inputBuffer == '0.0' || _inputBuffer == '0') {
|
|
||||||
_inputBuffer = '0.';
|
|
||||||
_isNewInput = false;
|
|
||||||
} else {
|
|
||||||
// 其他数字直接加小数点
|
|
||||||
_inputBuffer = '$_inputBuffer.';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 已有小数点时震动反馈
|
|
||||||
HapticFeedback.heavyImpact();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理删除
|
|
||||||
void _handleDelete() {
|
|
||||||
setState(() {
|
|
||||||
if (_pendingOperation != null && !_isNewInput) {
|
|
||||||
// 删除第二个数字
|
|
||||||
if (_inputBuffer.length > 1) {
|
|
||||||
_inputBuffer = _inputBuffer.substring(0, _inputBuffer.length - 1);
|
|
||||||
if (_inputBuffer.isEmpty || _inputBuffer == '0') {
|
|
||||||
_inputBuffer = '0.0';
|
|
||||||
_isNewInput = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_inputBuffer = '0.0';
|
|
||||||
_isNewInput = true;
|
|
||||||
}
|
|
||||||
} else if (_pendingOperation != null) {
|
|
||||||
// 删除运算符
|
|
||||||
_inputBuffer = _pendingValue.toString();
|
|
||||||
_pendingOperation = null;
|
|
||||||
_pendingValue = null;
|
|
||||||
_isNewInput = false;
|
|
||||||
} else {
|
|
||||||
// 删除普通数字
|
|
||||||
if (_inputBuffer.length > 1) {
|
|
||||||
_inputBuffer = _inputBuffer.substring(0, _inputBuffer.length - 1);
|
|
||||||
if (_inputBuffer.isEmpty || _inputBuffer == '0') {
|
|
||||||
_inputBuffer = '0.0';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_inputBuffer = '0.0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_amount = double.parse(_inputBuffer);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 处理运算符
|
|
||||||
void _handleOperator(String operator) {
|
|
||||||
// 如果有待处理的运算,先计算结果
|
|
||||||
if (_pendingOperation != null && !_isNewInput) {
|
|
||||||
// 先保存当前运算符,因为计算可能会重置状态
|
|
||||||
final newOperator = operator;
|
|
||||||
_calculateResult();
|
|
||||||
|
|
||||||
// 如果计算导致重置(结果为0),不添加新的运算符
|
|
||||||
if (_inputBuffer == '0.0') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算完成后,设置新的运算符
|
|
||||||
setState(() {
|
|
||||||
_pendingOperation = newOperator;
|
|
||||||
_pendingValue = double.parse(_inputBuffer);
|
|
||||||
_isNewInput = true;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 正常设置运算符
|
|
||||||
setState(() {
|
|
||||||
_pendingOperation = operator;
|
|
||||||
_pendingValue = double.parse(_inputBuffer);
|
|
||||||
_isNewInput = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 计算结果
|
|
||||||
void _calculateResult() {
|
|
||||||
if (_pendingOperation != null && _pendingValue != null && !_isNewInput) {
|
|
||||||
final currentValue = double.parse(_inputBuffer);
|
|
||||||
double result;
|
|
||||||
|
|
||||||
switch (_pendingOperation) {
|
|
||||||
case '+':
|
|
||||||
result = _pendingValue! + currentValue;
|
|
||||||
if (result == 0) {
|
|
||||||
_resetToDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case '-':
|
|
||||||
result = _pendingValue! - currentValue;
|
|
||||||
if (result <= 0) {
|
|
||||||
_resetToDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
// 格式化结果,去掉不必要的小数位
|
|
||||||
_inputBuffer = _formatNumberForDisplay(result);
|
|
||||||
_amount = result;
|
|
||||||
_pendingOperation = null;
|
|
||||||
_pendingValue = null;
|
|
||||||
_isNewInput = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 重置到默认状态
|
|
||||||
void _resetToDefault() {
|
|
||||||
setState(() {
|
|
||||||
_inputBuffer = '0.0';
|
|
||||||
_amount = 0;
|
|
||||||
_pendingOperation = null;
|
|
||||||
_pendingValue = null;
|
|
||||||
_isNewInput = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 格式化数字用于显示
|
|
||||||
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) {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,14 +15,14 @@ class RecordDetailDialog extends StatelessWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// 获取分类信息
|
/// 获取分类信息
|
||||||
Category _getCategory(String categoryId) {
|
Category _getCategory(String categoryId, RecordType type) {
|
||||||
return [...expenseCategories, ...incomeCategories]
|
return CategoryConfig.getCategoriesByType(type)
|
||||||
.firstWhere((c) => c.id == categoryId);
|
.firstWhere((c) => c.id == categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final category = _getCategory(record.categoryId);
|
final category = _getCategory(record.categoryId, record.type);
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: Row(
|
title: Row(
|
||||||
|
|||||||
@ -17,8 +17,8 @@ class RecordList extends StatelessWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// 获取分类信息
|
/// 获取分类信息
|
||||||
Category _getCategory(String categoryId) {
|
Category _getCategory(String categoryId, RecordType type) {
|
||||||
return [...expenseCategories, ...incomeCategories]
|
return CategoryConfig.getCategoriesByType(type)
|
||||||
.firstWhere((c) => c.id == categoryId);
|
.firstWhere((c) => c.id == categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ class RecordList extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
// 记录列表
|
// 记录列表
|
||||||
...dayRecords.map((record) {
|
...dayRecords.map((record) {
|
||||||
final category = _getCategory(record.categoryId);
|
final category = _getCategory(record.categoryId, record.type);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(category.icon),
|
leading: Icon(category.icon),
|
||||||
title: Text(category.name),
|
title: Text(category.name),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user